Kurucu ve Diğer Özel İşlevler
Her ne kadar bu bölümde yalnızca yapıları kullanıyor olsak da bu temel işlemler daha sonra göreceğimiz sınıflar için de geçerlidir. Sınıflardaki farklılıklarını daha sonraki bölümlerde göstereceğim.
Yapıların üye işlevleri arasından dört tanesi nesnelerin temel işlemlerini belirledikleri için ayrıca önemlidir:
- Kurucu işlev
this
- Sonlandırıcı işlev
~this
- Kopyalayıcı işlev
this(ref const(S))
(Yukarıdaki
S
, yapı türünü gösteren yalnızca bir örnektir.) - Atama işleci
opAssign
Ek olarak, yeni yazılacak olan kodlar için önerilmeyen ve geçmişten kalmış olan bir işlev daha vardır:
- Kopya sonrasını belirleyen
this(this)
Bu temel işlemlerin normalde yapılar için özel olarak tanımlanmaları gerekmez çünkü o işlemler zaten derleyici tarafından otomatik olarak halledilirler. Yine de bu işlevlerin özel olarak kendi isteğimiz doğrultusunda tanımlanmalarının gerektiği durumlar olabilir.
Kurucu işlev
Kurucu işlevin asıl görevi bir nesnenin üyelerine gerekli değerleri atayarak onu kullanılabilir duruma getirmektir.
Kurucu işlevleri şimdiye kadar hem bütün yapı örneklerinde hem de File
gibi kütüphane türlerinde gördük. Türün ismi işlev çağrısı gibi kullanıldığında o türün kurucu işlevi çağrılır. Bunu aşağıdaki satırın sağ tarafında görüyoruz:
auto dersBaşı = GününSaati(8, 30);
Benzer biçimde, aşağıdaki satırın sağ tarafında da bir sınıf nesnesi kurulmaktadır:
auto değişken = new BirSınıf();
Tür ismi işlev çağrısı gibi kullanılırken parantez içinde yazılanlar da kurucu işleve gönderilen parametre değerleri haline gelirler. Örneğin, yukarıdaki 8 ve 30 değerleri GününSaati
kurucu işlevine gönderilen parametre değerleridir.
Şimdiye kadar gördüğümüz nesne kurma söz dizimlerine ek olarak; const
, immutable
, ve shared
nesneler tür kurucusu söz dizimiyle de kurulabilirler. (shared
anahtar sözcüğünü ilerideki bir bölümde göreceğiz.)
Örneğin, aşağıdaki üç değişken de immutable
oldukları halde, a
değişkeninin kurulma işlemi b
ve c
değişkenlerininkinden anlamsal olarak farklıdır:
/* Yaygın söz dizimi; değişebilen bir türün immutable bir * değişkeni: */ immutable a = S(1); /* Tür kurucusu söz dizimi; immutable bir türün bir * değişkeni: */ auto b = immutable(S)(2); /* 'b' ile aynı anlamda: */ immutable c = immutable(S)(3);
Söz dizimi
Diğer işlevlerden farklı olarak, kurucu işlevlerin dönüş değerleri yoktur ve dönüş türü olarak void
bile yazılmaz. Kurucu işlevin ismi this
olmak zorundadır. "Bu" anlamına gelen "this"in "bu türden nesne kuran işlev" sözünden geldiğini düşünebilirsiniz:
struct BirYapı { // ... this(/* kurucu parametreleri */) { // ... nesneyi kuran işlemler ... } }
Kurucu parametreleri nesneyi kullanıma hazırlamak için gereken bilgilerden oluşur.
Otomatik kurucu işlev
Şimdiye kadar gördüğümüz bütün yapı örneklerinde derleyici tarafından sağlanan otomatik kurucu işlevden yararlandık. O kurucunun işi, parametre değerlerini sırayla üyelere atamaktır.
Yapılar bölümünden hatırlayacağınız gibi, parametre listesinde sonda bulunan parametrelerin değerlerinin belirtilmesi gerekmez. Değerleri belirtilmeyen üyeler kendi türlerinin .init
değerlerini alırlar. Yine aynı bölümden hatırlayacağınız gibi, üyelerin .init
değerleri üye tanımı sırasında =
işleciyle belirlenebilir:
struct Deneme { int üye = 42; }
Parametre Serbestliği bölümünde gösterilen varsayılan parametre değerleri olanağını da hatırlarsak, otomatik kurucu işlevin derleyici tarafından aşağıdaki gibi oluşturulduğunu düşünebiliriz:
struct Deneme { char karakter; int tamsayı; double kesirli; /* Derleyicinin sağladığı kurucu işlevin eşdeğeri. (Not: * Bu işlev nesneyi Deneme() yazımıyla kurarken çağrılmaz; * açıklama amacıyla gösteriyorum.) */ this(in char karakter_parametre = char.init, in int tamsayı_parametre = int.init, in double kesirli_parametre = double.init) { karakter = karakter_parametre; tamsayı = tamsayı_parametre; kesirli = kesirli_parametre; } }
Eğer çoğu yapıda olduğu gibi o kadarı yeterliyse, bizim ayrıca kurucu işlev tanımlamamız gerekmez. Bütün üyelere geçerli değerler verilmesi nesnenin kurulmuş olması için çoğu durumda yeterlidir.
Üyelere this.
ile erişim
Yukarıdaki kodda parametrelerle üyeler karışmasınlar diye parametrelerin sonlarına _parametre
diye bir belirteç ekledim. Parametrelerin isimlerini de üyelerle aynı yapsaydım kod hatalı olurdu:
struct Deneme { char karakter; int tamsayı; double kesirli; this(in char karakter = char.init, in int tamsayı = int.init, in double kesirli = double.init) { // 'in' bir parametreyi kendisine atamaya çalışıyor! karakter = karakter; // ← derleme HATASI tamsayı = tamsayı; kesirli = kesirli; } }
Bunun nedeni, işlev içinde karakter
yazıldığında üyenin değil, parametrenin anlaşılmasıdır. Yukarıdaki parametreler in
olarak işaretlendiklerinden sabit değerin değiştirilemeyeceğini bildiren derleme hatası alınır:
Error: variable deneme.Deneme.this.karakter cannot modify const
Bu konuda bir çözüm olarak this.
'dan yararlanılır: üye işlevler içinde this.
, bu nesnenin anlamına gelir. Bu olanağı kullanınca, parametrelerin isimlerinin sonlarına artık _parametre
gibi ekler yazmak da gerekmez:
this(in char karakter = char.init, in int tamsayı = int.init, in double kesirli = double.init) { this.karakter = karakter; this.tamsayı = tamsayı; this.kesirli = kesirli; }
karakter
yazıldığında parametre, this.karakter
yazıldığında da "bu nesnenin üyesi" anlaşılır ve kod artık istediğimizi yapacak biçimde derlenir ve çalışır.
Programcı tarafından tanımlanan kurucu işlev
Yukarıda derleyicinin otomatik olarak yazdığı kurucu işlevin perde arkasında nasıl çalıştığını anlattım. Daha önce de belirttiğim gibi, eğer yapının kurulması için bu kadarı yeterliyse ayrıca kurucu tanımlamak gerekmez. Çoğu duruma uygun olan kurucu perde arkasında zaten derleyici tarafından otomatik olarak yazılır ve çağrılır.
Bazen nesnenin kurulabilmesi için üyelere sırayla değer atamaktan daha karmaşık işlemler gerekebilir. Örnek olarak daha önce tanımlamış olduğumuz Süre
yapısına bakalım:
struct Süre { int dakika; }
Tek bir tamsayı üyesi bulunan bu yapı için derleyicinin sağladığı kurucu çoğu durumda yeterlidir:
zaman.azalt(Süre(12));
Ancak, o kurucu yalnızca dakika miktarını aldığından bazı durumlarda programcıların hesaplar yapmaları gerekebilir:
// 23 saat ve 18 dakika öncesi zaman.azalt(Süre(23 * 60 + 18)); // 22 saat ve 20 dakika sonrası zaman.ekle(Süre(22 * 60 + 20));
Programcıları böyle hesaplardan kurtarmak için saat ve dakika miktarlarını iki ayrı parametre olarak alan bir Süre
kurucusu düşünülebilir. Böylece toplam dakika hesabı kurucu içinde yapılır:
struct Süre { int dakika; this(int saat, int dakika) { this.dakika = saat * 60 + dakika; } }
Saat ve dakika farklı iki parametre olduklarından, programcılar da hesabı artık kendileri yapmak zorunda kalmamış olurlar:
// 23 saat ve 18 dakika öncesi zaman.azalt(Süre(23, 18)); // 22 saat ve 20 dakika sonrası zaman.ekle(Süre(22, 20));
İlk atama işlemi, kurmadır
Üyelerin değerleri kurucu içinde belirlenirken her üyeye yapılan ilk atama işlemi özeldir: O işlem, üyenin .init
değerinin üzerine başka bir değer atanması değil, üyenin belirtilen değerle kurulmasıdır. Üyeye yapılan daha sonraki atama işlemleri normal atama olarak işletilir.
Bu özel kuralın nedeni, immutable
ve const
üyelerin çalışma zamanında bilinen değerlerle kurulabilmelerini sağlamaktır. Aksi taktirde, immutable
ve const
değişkenler değiştirilemediklerinden, bu çeşit üyelerin değerlerini çalışma zamanında belirlemek mümkün olmazdı.
Aşağıdaki program atama işleminin immutable
bir üye için nasıl tek kere mümkün olduğunu gösteriyor:
struct S { int m; immutable int i; this(int m, int i) { this.m = m; // ← kurma this.m = 42; // ← atama (değişebilen üye için mümkün) this.i = i; // ← kurma this.i = i; // ← derleme HATASI } } void main() { auto s = S(1, 2); }
Programcının kurucusu otomatik kurucunun bazı kullanımlarını geçersizleştirir
Programcı tarafından tek bir kurucu işlevin bile tanımlanmış olması, derleyicinin oluşturduğu kurucu işlevin varsayılan parametre değerleri ile kullanımını geçersiz hale getirir. Örneğin Süre
'nin tek parametre ile kurulması derleme hatasına neden olur:
zaman.azalt(Süre(12)); // ← derleme HATASI
O tek parametreli kullanım, programcının tanımlamış olduğu iki parametreli kurucuya uymamaktadır. Ek olarak, Süre
'nin otomatik kurucusu o kullanımda artık geçersizdir.
Çözüm olarak kurucuyu yükleyebilir ve bir tane de tek parametreli kurucu tanımlayabiliriz:
struct Süre { int dakika; this(int saat, int dakika) { this.dakika = saat * 60 + dakika; } this(int dakika) { this.dakika = dakika; } }
Programcı tarafından tanımlanan kurucu, nesnelerin { }
karakterleriyle kurulmaları olanağını da ortadan kaldırır:
Süre süre = { 5 }; // ← derleme HATASI
Buna rağmen, hiç parametre yazılmadan kurulum her zaman için geçerlidir:
auto s = Süre(); // derlenir
Bunun nedeni, her türün .init
değerinin derleme zamanında bilinmesinin D'de şart olmasıdır. Yukarıdaki s
'nin değeri Süre
türünün ilk değerine eşittir:
assert(s == Süre.init);
Varsayılan kurucu yerine static opCall
Her türün ilk değerinin derleme zamanında bilinmesinin gerekli olması varsayılan kurucunun programcı tarafından tanımlanmasını olanaksız hale getirir.
Her nesne kurulduğunda çıktıya bir satır yazdırmaya çalışan aşağıdaki kurucuya bakalım:
struct Deneme { this() { // ← derleme HATASI writeln("Deneme nesnesi kuruluyor"); } }
Derleyici bunun mümkün olmadığını bildirir:
Error: constructor deneme.Deneme.this default constructor for structs only allowed with @disable and no body
Not: Varsayılan kurucunun sınıflar için tanımlanabildiğini ileride göreceğiz.
Bu kısıtlamaya rağmen yapı nesnelerinin parametresiz olarak nasıl kurulacakları parametre almayan bir static opCall
ile belirlenebilir. Bunun yapının .init
değerine bir etkisi yoktur: static opCall
yalnızca nesnelerin parametresiz olarak kurulmalarını sağlar.
Bunun mümkün olması için static opCall
işlecinin o yapının türünden bir nesne oluşturması ve döndürmesi gerekir:
import std.stdio; struct Deneme { static Deneme opCall() { writeln("Deneme nesnesi kuruluyor"); Deneme deneme; return deneme; } } void main() { auto deneme = Deneme(); }
main
içindeki Deneme()
çağrısı static opCall
'u işletir:
Deneme nesnesi kuruluyor
Not: static opCall
'un içindeyken Deneme()
yazılmaması gerektiğine dikkat edin. O yazım da static opCall
'u çağıracağından static opCall
'dan hiç çıkılamaz:
static Deneme opCall() { writeln("Deneme nesnesi kuruluyor"); return Deneme(); // ← Yine 'static opCall'u çağırır }
Çıktısı:
Deneme nesnesi kuruluyor
Deneme nesnesi kuruluyor
Deneme nesnesi kuruluyor
... ← sürekli olarak tekrarlanır
Başka kurucu işlevleri çağırmak
Kurucu işlevler başka kurucu işlevleri çağırabilirler ve böylece olası kod tekrarlarının önüne geçilmiş olur. Süre
gibi basit bir yapı bunun yararını anlatmak için uygun olmasa da bu olanağın kullanımını aşağıdaki gibi iki kurucu ile gösterebiliriz:
this(int saat, int dakika) { this.dakika = saat * 60 + dakika; } this(int dakika) { this(0, dakika); // diğer kurucuyu çağırıyor }
Yalnızca dakika alan kurucu diğer kurucuyu saat değeri yerine 0 göndererek çağırmaktadır.
Uyarı: Yukarıdaki Süre
kurucularında bir tasarım hatası bulunduğunu söyleyebiliriz. Nesneler tek parametre ile kurulduklarında ne istendiği açık değildir:
auto yolSüresi = Süre(10); // 10 saat mi, 10 dakika mı?
Süre
'nin belgelerine veya tanımına bakarak "10 dakika" dendiğini anlayabiliriz. Öte yandan, iki parametre alan kurucuda ilk parametrenin saat olması bir tutarsızlık oluşturmaktadır.
Böyle tasarımlar karışıklıklara neden olacaklarından kaçınılmaları gerekir.
Kurucu nitelendiricileri
Değişebilen, const
, immutable
, ve shared
nesneler normalde aynı kurucu ile kurulur:
import std.stdio; struct S { this(int i) { writeln("Bir nesne kuruluyor"); } } void main() { auto d = S(1); const c = S(2); immutable i = S(3); shared s = S(4); }
Yukarıda sağ taraftaki ifadelerde kurulmakta olan nesneler anlamsal olarak değişebilen türdendir. Aralarındaki fark, değişkenlerin tür nitelendiricileridir. Bu yüzden, bütün nesneler aynı kurucu ile kurulur:
Bir nesne kuruluyor Bir nesne kuruluyor Bir nesne kuruluyor Bir nesne kuruluyor
Kurulmakta olan nesnenin nitelendiricisine bağlı olarak bazen bazı üyelerinin farklı kurulmaları veya hiç kurulmamaları istenebilir veya gerekebilir. Örneğin, immutable
bir nesnenin hiçbir üyesinin o nesnenin yaşamı boyunca değişmesi söz konusu olmadığından, nesnenin değişebilen bazı nesnelerinin hiç ilklenmemeleri program hızı açısından yararlı olabilir.
Nitelendirilmiş kurucular farklı niteliklere sahip nesnelerin kurulmaları için farklı tanımlanabilirler:
import std.stdio; struct S { this(int i) { writeln("Bir nesne kuruluyor"); } this(int i) const { writeln("const bir nesne kuruluyor"); } this(int i) immutable { writeln("immutable bir nesne kuruluyor"); } /* 'shared' anahtar sözcüğünü ilerideki bir bölümde * göreceğiz. */ this(int i) shared { writeln("shared bir nesne kuruluyor"); } } void main() { auto d = S(1); const c = S(2); immutable i = S(3); shared s = S(4); }
Ancak, yukarıda da belirtildiği gibi, sağ taraftaki ifadeler anlamsal olarak değişebilen olduklarından, yukarıdaki nesneler yine de değişebilen nesne kurucusu ile kurulurlar:
Bir nesne kuruluyor Bir nesne kuruluyor ← const kurucu DEĞİL Bir nesne kuruluyor ← immutable kurucu DEĞİL Bir nesne kuruluyor ← shared kurucu DEĞİL
Nitelendirilmiş kuruculardan yararlanabilmek için tür kurucusu söz dizimini kullanmak gerekir. (Tür kurucusu terimi nesne kurucularıyla karıştırılmamalıdır; tür kurucusu türlerle ilgilidir, nesnelerle değil.) Bu söz dizimi, bir nitelendiriciyi ve var olan bir türü birleştirerek farklı bir tür oluşturur. Örneğin, immutable(S)
türü, immutable
ile S
'nin birleşmesinden oluşur:
auto d = S(1); auto c = const(S)(2); auto i = immutable(S)(3); auto s = shared(S)(4);
Sağ taraftaki ifadelerdeki nesneler bu sefer farklıdır: değişebilen, const
, immutable
, ve shared
. Dolayısıyla, her nesne kendi türüne uyan kurucu ile kurulur:
Bir nesne kuruluyor const bir nesne kuruluyor immutable bir nesne kuruluyor shared bir nesne kuruluyor
Ek olarak, yukarıdaki nesneler auto
ile kurulduklarından; türleri değişebilen, const
, immutable
, ve shared
olarak çıkarsanır.
Kurucu parametresinin değişmezliği
Değişmezlik bölümünde referans türünden olan işlev parametrelerinin const
olarak mı yoksa immutable
olarak mı işaretlenmelerinin daha uygun olduğunun kararının güç olabildiğini görmüştük. Bu güçlük kurucu parametreleri için de geçerlidir. Ancak, kurucu parametrelerinin immutable
olarak seçilmeleri bazı durumlarda const
'tan daha uygundur.
Bunun nedeni, kurucu parametrelerinin daha sonradan kullanılmak üzere sıklıkla nesne içerisinde saklanmalarıdır. immutable
olmadığı zaman, parametrenin kurucu çağrıldığındaki değeriyle daha sonradan kullanıldığındaki değeri farklı olabilir.
Bunun örneği olarak öğrencinin notlarını yazacağı dosyanın ismini parametre olarak alan bir kurucuya bakalım. Değişmezlik bölümündeki ilkeler doğrultusunda ve daha kullanışlı olabilmek amacıyla parametresi const char[]
olarak tanımlanmış olsun:
import std.stdio; struct Öğrenci { const char[] kayıtDosyası; size_t[] notlar; this(const char[] kayıtDosyası) { this.kayıtDosyası = kayıtDosyası; } void notlarıKaydet() { auto dosya = File(kayıtDosyası.idup, "w"); dosya.writeln("Öğrencinin notları:"); dosya.writeln(notlar); } // ... } void main() { char[] dosyaİsmi; dosyaİsmi ~= "ogrenci_notlari"; auto öğrenci = Öğrenci(dosyaİsmi); // ... /* dosyaİsmi sonradan değiştiriliyor olsun (bu örnekte * bütün harfleri 'A' oluyor): */ dosyaİsmi[] = 'A'; // ... /* Notlar yanlış dosyaya kaydedilecektir: */ öğrenci.notlarıKaydet(); }
Yukarıdaki program öğrencinin notlarını "ogrenci_notlari"
dosyasına değil, ismi bütünüyle A harflerinden oluşan bir dosyaya yazar. O yüzden referans türünden olan üyelerin ve parametrelerin immutable
olarak tanımlanmalarının daha uygun oldukları düşünülebilir. Bunun dizgilerde string
ile kolayca sağlanabildiğini biliyoruz. Yapının yalnızca değişen satırlarını gösteriyorum:
struct Öğrenci { string kayıtDosyası; // ... this(string kayıtDosyası) { // ... } // ... }
Kullanıcılar nesneleri artık immutable
dizgilerle kurmak zorundadırlar ve kayıtların yazıldığı dosya konusundaki karışıklık böylece giderilmiş olur.
Tek parametreli kurucu yoluyla tür dönüşümü
Tek parametre alan kurucu işlevlerin aslında tür dönüşümü sağladıkları düşünülebilir: Kurucu işlevin parametresinin türünden yola çıkarak yapının türünde bir nesne üretilmektedir. Örneğin, aşağıdaki yapının kurucusu verilen bir string
'e karşılık bir Öğrenci
üretmektedir:
struct Öğrenci { string isim; this(string isim) { this.isim = isim; } }
Bu dönüşüm özellikleri nedeniyle kurucu işlevler to
ve cast
tarafından da dönüşüm amacıyla kullanılırlar. Bunun bir örneğini görmek için aşağıdaki selamVer
işlevine bakalım. O işlev bir Öğrenci
beklediği halde ona string
gönderilmesi doğal olarak derleme hatasına yol açar:
void selamVer(Öğrenci öğrenci) { writeln("Merhaba ", öğrenci.isim); } // ... selamVer("Eray"); // ← derleme HATASI
Öte yandan, aşağıdaki üç satır da derlenir ve selamVer
işlevi üçünde de geçici bir Öğrenci
nesnesi ile çağrılır:
import std.conv; // ... selamVer(Öğrenci("Eray")); selamVer(to!Öğrenci("Ercan")); selamVer(cast(Öğrenci)"Erdost");
Sonlandırıcı işlev
Nesnenin yaşam süreci sona ererken gereken işlemler sonlandırıcı işlev tarafından işletilir.
Derleyicinin sunduğu otomatik sonlandırıcı sıra ile bütün üyelerin kendi sonlandırıcılarını çağırır. Kurucu işlevde de olduğu gibi, çoğu yapı türünde bu kadarı zaten yeterlidir.
Bazı durumlarda ise nesnenin sonlanmasıyla ilgili bazı özel işlemler gerekebilir. Örneğin, nesnenin sahiplenmiş olduğu bir işletim sistemi kaynağının geri verilmesi gerekiyordur; başka bir nesnenin bir üye işlevi çağrılacaktır; başka bir bilgisayar üzerinde çalışmakta olan bir programa onunla olan bağlantının kesilmekte olduğu bildirilecektir; vs.
Sonlandırıcı işlevin ismi ~this
'tir ve kurucuda olduğu gibi onun da dönüş türü yoktur.
Sonlandırıcı işlev yapılarda otomatik olarak işletilir
Sonlandırıcı işlev yapı nesnesinin geçerliliği bittiği an işletilir. (Yapılardan farklı olarak, sonlandırıcı işlev sınıflarda hemen işletilmez.)
Yaşam Süreçleri bölümünden hatırlayacağınız gibi, nesnelerin yaşam süreçleri tanımlandıkları kapsamdan çıkılırken sona erer. Bir yapı nesnesinin yaşamının sona erdiği durumlar şunlardır:
- Nesnenin tanımlandığı kapsamdan normal olarak veya atılan bir hata ile çıkılırken:
if (birKoşul) { auto süre = Süre(7); // ... } // ← Sonlandırıcı işlev 'süre' için burada işletilir
- İsimsiz bir nesne o nesnenin tanımlandığı ifadenin en sonunda sonlanır:
zaman.ekle(Süre(5)); // ← Süre(5) hazır değeri bu // ifadenin sonunda sonlanır
- Bir nesnenin yapı türündeki bütün üyeleri de asıl nesne ile birlikte sonlanırlar.
Sonlandırıcı örneği
Sonlandırıcı örneği olarak XML düzeni oluşturmaya yarayan bir yapı tasarlayalım. XML elemanları açılı parantezlerlerle belirtilirler, ve verilerden ve başka XML elemanlarından oluşurlar. XML elemanlarının nitelikleri de olabilir; onları bu örnekte dikkate almayacağız.
Burada amacımız, <isim>
şeklinde açılan bir XML elemanının doğru olarak ve mutlaka </isim>
şeklinde kapatılmasını sağlamak olacak:
<ders1> ← dıştaki XML elemanının açılması <not> ← içteki XML elemanının açılması 57 ← veri </not> ← içtekinin kapatılması </ders1> ← dıştakinin kapatılması
Bunu sağlayacak bir yapıyı iki üye ile tasarlayabiliriz. Bu üyeler XML elemanının ismini ve çıkışta ne kadar girintiyle yazdırılacağını temsil edebilirler:
struct XmlElemanı {
string isim;
string girinti;
}
Eğer XML elemanını açma işini kurucu işleve ve kapama işini de sonlandırıcı işleve yaptırırsak, nesnelerin yaşam süreçlerini ayarlayarak istediğimiz çıktıyı elde edebiliriz. Örneğin çıktıya nesne kurulduğunda <eleman>
, sonlandırıldığında da </eleman>
yazdırabiliriz.
Kurucuyu bu amaca göre şöyle yazabiliriz:
this(string isim, int düzey) { this.isim = isim; this.girinti = girintiDizgisi(düzey); writeln(girinti, '<', isim, '>'); }
Kurucunun son satırı XML elemanının açılmasını sağlamaktadır. girintiDizgisi
, o düzeyin girintisini belirleyen ve boşluklardan oluşan bir string
üretir:
import std.array; // ... string girintiDizgisi(int girintiAdımı) { return replicate(" ", girintiAdımı * 2); }
Yararlandığı replicate
işlevi, kendisine verilen dizgiyi belirtilen sayıda uç uca ekleyerek yeni bir dizgi üreten bir işlevdir; std.array
modülünde tanımlıdır. Bu durumda yalnızca boşluk karakterlerinden oluşuyor ve satır başlarındaki girintileri oluşturmak için kullanılıyor.
Sonlandırıcı işlevi de XML elemanını kapatmak için benzer biçimde yazabiliriz:
~this() { writeln(girinti, "</", isim, '>'); }
O yapıyı kullanan bir deneme programı aşağıdaki gibi yazılabilir:
import std.conv; import std.random; import std.array; string girintiDizgisi(int girintiAdımı) { return replicate(" ", girintiAdımı * 2); } struct XmlElemanı { string isim; string girinti; this(string isim, int düzey) { this.isim = isim; this.girinti = girintiDizgisi(düzey); writeln(girinti, '<', isim, '>'); } ~this() { writeln(girinti, "</", isim, '>'); } } void main() { immutable dersler = XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { immutable ders = XmlElemanı("ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { immutable not = XmlElemanı("not", 2); immutable rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); } } }
XmlElemanı
nesnelerinin üç kapsamda oluşturulduklarına dikkat edin. Bu programdaki XML elemanlarının açılıp kapanmaları bütünüyle o nesnelerin kurucu ve sonlandırıcı işlevleri tarafından oluşturulmaktadır.
<dersler> <ders0> <not> 72 </not> <not> 97 </not> <not> 90 </not> </ders0> <ders1> <not> 77 </not> <not> 87 </not> <not> 56 </not> </ders1> </dersler>
Çıktıda örnek olarak <dersler>
elemanına bakalım: main
içinde ilk olarak dersler
nesnesi kurulduğu için ilk olarak onun kurucusunun çıktısını görüyoruz; ve main
'den çıkılırken sonlandırıldığı için de en son onun sonlandırıcısının çıktısını görüyoruz.
Kopyalayıcı işlev
Kopyalama, yeni bir nesneyi var olan başka bir nesnenin kopyası olarak kurmaktır.
S
'nin bir yapı türü olduğunu varsayarsak, nesneler aşağıdaki durumlarda kopyalanırlar:
- Parametresini değer olarak alan bir işlev çağrılırken:
void foo(S s) { // Çağrılan taraftaki parametre değeri, // parametreye kopyalanır // ... }
- İşlevden değer türü döndürürken:
S foo() { S sonuç; // ... return sonuç; // Dönüş değeri, çağıran kapsama kopyalanır }
Not: Derleyicinin "isimli dönüş değeri eniyileştirmesi" ("named return value optimization" (NRVO)) denen bir eniyileştirme uygulayabildiği durumlarda o kopya gerçekleşmez.
- Nesneler açıkça kopyalandıklarında
Atama işlecinin bu durumda da kullanılıyor olması, bu durumda söz dizimi açısından karışıklığa neden olabilir. Örneğin, aşağıdaki ikinci satır, yeni nesne
a
'nınvarolanNesne
'den kopyalanmasıdır. Yeni bir nesne tanımlanmakta olduğunun göstergesi, oradakiauto
anahtar sözcüğüdür.auto varolanNesne = S(); auto a = varolanNesne; // kopyalama a = varolanNesne; // atama a = a; // atama a = S(); // atama
Öte yandan, o kopyalama satırından sonraki bütün satırlar atama işlemi içerirler çünkü
a
o satırlar işletildiğinde zaten var olan bir nesnedir.
Normalde, derleyicinin yazmış olduğu kodlar kopyalama işlemini otomatik olarak gerçekleştirir: yeni nesnenin üyeleri, var olan nesnenin üyelerinden art arda kopyalanır. Aşağıdaki yapı tanımına ve a
'nın varolanNesne
'den kopyalandığı aşağıdaki koda bakalım:
struct S { int i; double d; } // ... auto varolanNesne = S(); auto a = varolanNesne; // kopyalama
Otomatik kopyalayıcı aşağıdaki adımları gerçekleştirir:
a.i
'yivarolanNesne.i
'den kopyalara.d
'yivarolanNesne.d
'den kopyalar
Otomatik kopyalayıcının uygun olmadığı bir örnek, Yapılar bölümünde gördüğümüz Öğrenci
türüdür. Hatırlarsanız, o türden nesnelerin kopyalanmalarıyla ilgili bir sorun vardı:
struct Öğrenci { int numara; int[] notlar; }
Oradaki notlar
üyesi, bir dilim olduğundan bir referans türüdür. O yüzden, Öğrenci
nesnelerinin otomatik olarak kopyalanmaları, birden fazla nesnenin notlar
üyelerinin aynı elemanları paylaşacak olmalarıdır. Bir nesnenin notlarını değiştirmek, kopyalandığı nesnelerin notlarının da değişmesine neden olur:
auto öğrenci1 = Öğrenci(1, [ 70, 90, 85 ]); auto öğrenci2 = öğrenci1; // kopyalama öğrenci2.numara = 2; öğrenci1.notlar[0] += 5; // bu, ikinci öğrencinin // notlarıni da değiştirir assert(öğrenci2.notlar[0] == 75);
Bu karışıklığın engellenmesi için ikinci nesnenin notlar
dizisinin kendi elemanlarına sahip olması gerekir. İşte, kopyalayıcı işlev bunun gibi özel kopyalama kodları gerektiği durumlar içindir.
Nesne kurmayla ilgili olduğundan, kopyalayıcının ismi de this
'tir ve dönüş türü yoktur. Parametresi, yapıyla aynı türden ve ref
olmalıdır. Var olan nesnenin değiştirilmesi düşünülmediğinden parametrenin const
(veya inout
) olarak tanımlanması uygundur. İngilizce'de "bu" anlamına gelen this
anahtar sözcüğü ile uyumlu olmak adına, parametreyi "o" diye adlandırmak okuma kolaylığı sağlayabilir:
struct Öğrenci { int numara; int[] notlar; this(ref const(Öğrenci) o) { this.numara = o.numara; this.notlar = o.notlar.dup; } }
O kopyalayıcı, notlar
'ın elemanlarını yeni nesne için .dup
'u da çağırarak üyeleri sırayla kopyalamaktadır. Sonuçta, yeni nesnenin notlar
diliminin elemanları yeni nesneye ait olur.
Not: Yukarıdaki "İlk atama işlemi, kurmadır" başlığında anlatıldığı gibi, yukarıdaki atama işleçleri atama işlemleri değil, üyelerin kopyalanarak kurulmalarıdır.
Birinci nesnede yapılan değişiklik artık ikinci nesneyi etkilemez:
öğrenci1.notlar[0] += 5; assert(öğrenci2.notlar[0] == 70);
Okunurluğu biraz daha az olsa da, parametre türünü Öğrenci
diye tekrarlamak yerine, kopyalayıcı parametresini bütün yapılar için "bu nesnenin türü" anlamına gelen typeof(this)
diye de yazabilirsiniz:
this(ref const(typeof(this)) o) { // ... }
Kopya sonrası işlevi
Kopya sonrası işlevi, D'nin yeni kodlar için önerilmeyen eski bir olanağıdır. Yeni yazılan kodlar için kopyalayıcı işlev kullanılmalıdır. Kopya sonrası işlevi, eski kodları desteklemek adına yine de geçerlidir ama kopyalayıcı işlevle uyumsuzdur: Kopya sonrası işlevi tanımlanmış olduğunda kopyalayıcı işlev kullanılamaz.
D'de nesne kopyalamanın eski yöntemi şöyledir:
- Yeni nesnenin üyeleri var olan nesnenin üyelerinden bit değerleri düzeyinde kopyalanırlar. Bit düzeyindeki bu kopyalamaya İngilizce'de "bit level transfer" (kısaca "blit") denir.
- Kopyalama ile ilgili olarak gereken olası özel işlemler gerçekleştirilir. Bu adıma "blit sonrası" anlamında postblit denir.
Kopya sonrası işlevinin ismi de this
'tir ve dönüş değeri yoktur. Diğer özel işlevlerden ayırt edilebilmesi için parametre listesine özel olarak this
yazılır:
this(this) { // ... }
Kopya sonrası işlevinin kopyalayıcı işlevden farkı, bu işleve girildiğinde üyelerin zaten kopyalanmış olmalarıdır. Ek olarak, zaten var olan ve kopyalanmakta olan nesne bu işlev içinden erişilemez. Kopya sonrası işlevinde yapılabilen işlemler, kopyalanmakta olan yeni nesne üzerinde yapılacak olan düzeltmelerdir.
Kopya sonrası işlevi Öğrenci
türü için aşağıdaki gibi yazılabilir:
struct Öğrenci { int numara; int[] notlar; this(this) { // Buraya gelindiğinde 'numara ve 'notlar' zaten // kopyalanmışlardır. Ek olarak yapılması gereken, // elemanların da kopyalanmalarıdır: notlar = notlar.dup; } }
Atama işleci
Atama, zaten var olan bir nesneye yeni bir değer vermek anlamına gelir:
dönüşSüresi = gidişSüresi; // atama
Atama temel işlemler arasında diğerlerinden biraz daha karmaşıktır çünkü atama işlemi aslında iki parçadan oluşur:
- Soldaki nesnenin sonlandırılması
- Sağdaki nesnenin soldaki nesneye kopyalanması
Ancak, o iki işlemin yukarıdaki sırada işletilmelerinin önemli bir sakıncası vardır: Nesnenin başarıyla kopyalanacağından emin olunmadan önce sonlandırılması hataya açıktır. Yoksa, nesnenin kopyalanması aşamasında bir hata atılsa elimizde sonlandırılmış ama tam kopyalanamamış bir nesne kalır.
Derleyicinin sunduğu otomatik atama işleci bu yüzden güvenli hareket eder ve perde arkasında aşağıdaki işlemleri gerçekleştirir:
- Sağdaki nesneyi geçici bir nesneye kopyalar.
Atama işleminin parçası olan asıl kopyalama işlemi bu adımdır. Henüz soldaki nesnede hiçbir değişiklik yapılmamış olduğundan kopyalama sırasında hata atılsa bile kaybedilen bir şey olmaz.
- Soldaki nesneyi sonlandırır.
Atama işleminin diğer parçası bu adımdır.
- Geçici nesneyi soldaki nesneye aktarır.
Bu adım ve sonrasında kopya sonrası işlevi veya sonlandırıcı işletilmez. Soldaki nesne ve geçici nesne birbirlerinin yerine kullanılabilir durumda olan iki nesnedir.
Yalnızca perde arkasındaki bu işlemler süresince geçerli olan geçici nesne yok olduğunda geriye yalnızca sağdaki nesne ve onun kopyası olan soldaki nesne kalır.
Derleyicinin sunduğu otomatik atama işleci hemen hemen her durumda yeterlidir. Eğer herhangi bir nedenle kendiniz tanımlamak isterseniz, atılabilecek olan hatalara karşı dikkatli olmak gerektiğini unutmayın.
Söz dizimi şu şekildedir:
- İsmi
opAssign
'dır. - Parametre türü yapının kendi türüdür. (Kopyalayıcıda olduğu gibi,
ref const(typeof(this))
de yazılabilir.) - Dönüş türü yapının kendi türüdür.
- İşlevden
return this
ile çıkılır.
Ben burada basit Süre
yapısı üzerinde ve çıktıya bir mesaj yazdıracak şekilde tanımlayacağım:
struct Süre { int dakika; Süre opAssign(Süre sağdaki) { writefln( "dakika, %s değerinden %s değerine değişiyor", this.dakika, sağdaki.dakika); this.dakika = sağdaki.dakika; return this; } } // ... auto süre = Süre(100); süre = Süre(200); // atama
Çıktısı:
dakika, 100 değerinden 200 değerine değişiyor
Başka türlerden atamak
Bazı durumlarda nesnelere kendi türlerinden farklı türlerin değerlerini de atamak isteyebiliriz. Örneğin atama işlecinin sağ tarafında her zaman için Süre
türü kullanmak yerine, doğrudan bir tamsayı değer kullanmak isteyebiliriz:
süre = 300;
Bunu, parametre olarak int
alan bir atama işleci daha tanımlayarak sağlayabiliriz:
struct Süre { int dakika; Süre opAssign(Süre sağdaki) { writefln( "dakika, %s değerinden %s değerine değişiyor", this.dakika, sağdaki.dakika); this.dakika = sağdaki.dakika; return this; } Süre opAssign(int dakika) { writeln( "dakika, bir tamsayı değer ile değiştiriliyor"); this.dakika = dakika; return this; } } // ... süre = Süre(200); süre = 300;
Çıktısı:
dakika, 100 değerinden 200 değerine değişiyor dakika, bir tamsayı değer ile değiştiriliyor
Uyarı: Farklı türleri bu şekilde birbirlerine eşitleyebilmek veya daha genel olarak birbirlerinin yerine kullanabilmek, kolaylık getirdiği kadar karışıklıklara ve hatalara da neden olabilir.
Üye işlevlerin etkisizleştirilmesi
@disable
olarak işaretlenen işlevler kullanılamaz.
Bazı durumlarda üyeler için varsayılan mantıklı ilk değerler bulunmayabilir ve nesnelerin özel bir kurucu ile kurulmaları gerekebilir. Örneğin, aşağıdaki türün dosya isminin boş olmaması gerekiyor olabilir:
struct Arşiv {
string dosyaİsmi;
}
Ne yazık ki, derleyicinin oluşturduğu kurucu dosyaİsmi
'ni boş olarak ilkleyecektir:
auto arşiv = Arşiv(); // ← dosyaİsmi üyesi boş
Böyle bir durumu önlemenin yolu, varsayılan kurucuyu tanımını vermeden @disable
olarak bildirmek ve böylece var olan diğer kuruculardan birisinin kullanılmasını şart koşmaktır:
struct Arşiv { string dosyaİsmi; @disable this(); // ← kullanılamaz this(string dosyaİsmi) { // ← kullanılabilir // ... } } // ... auto arşiv = Arşiv(); // ← derleme HATASI
Bu sefer derleyici this()
'in kullanılamayacağını bildirir:
Error: constructor deneme.Arşiv.this is not callable because
it is annotated with @disable
Arşiv
nesneleri ya başka bir kurucu ile ya da doğrudan .init
değeriyle kurulmak zorundadır:
auto a = Arşiv("kayitlar"); // ← derlenir auto b = Arşiv.init; // ← derlenir
Kopyalayıcı, kopya sonrası işlevi, ve atama işleci de etkisizleştirilebilir:
struct Arşiv { // ... // Kopyalayıcı işlevi etkisizleştirir @disable this(ref const(typeof(this))); // Kopya sonrası işlevini etkisizleştirir @disable this(this); // Atama işlecini etkisizleştirir @disable typeof(this) opAssign(ref const(typeof(this))); } // ... auto a = Arşiv("kayitlar"); auto b = a; // ← derleme HATASI b = a; // ← derleme HATASI
Sonlandırıcının içerdiği işlemlerin tek kere işletilmelerinin gerektiği durumlar olabilir. Böyle durumlarda yararlı olan bir yöntem, kopyalayıcının ve kopya sonrası işlevinin etkisizleştirilmeleri, ve dolayısıyla sonlandırıcıdaki kodların birden fazla kopya için birden fazla sayıda işletilmelerinin önlenmesidir.
Örneğin, aşağıdaki sonlandırıcı işlev, mesajların yazıldığı (loglandıkları) bir dosyaya en son olarak "Sonlanıyor" mesajını yazacak biçimde tasarlanmış:
import std.stdio; import std.datetime; // Mesaj yazıcı struct Logger { File dosya; this(File dosya) { this.dosya = dosya; log("Başlıyor"); } ~this() { log("Sonlanıyor"); // ← Son mesaj olması istenmiş } void log(string mesaj) { dosya.writefln("%s %s", Clock.currTime(), mesaj); } } void main() { auto logger = Logger(stdout); logger.log("main başladı"); logger.log("foo çağrılıyor"); foo(logger); logger.log("main'e dönüldü"); } void foo(Logger logger) { logger.log("foo başladı"); }
Programın çıktısı, son mesajın, istenenin aksine iki kere yazdırıldığını göstermektedir:
2022-Jan-03 22:21:24.3143894 Başlıyor 2022-Jan-03 22:21:24.3144467 main başladı 2022-Jan-03 22:21:24.3144628 foo çağrılıyor 2022-Jan-03 22:21:24.3144767 foo başladı 2022-Jan-03 22:21:24.3144906 Sonlanıyor 2022-Jan-03 22:21:24.3145035 main'e dönüldü 2022-Jan-03 22:21:24.3145155 Sonlanıyor
Bu sorun, programda birden fazla Logger
nesnesinin oluşturulması ve sonlandırıcının bunların her birisi için işletiliyor olmasıdır. "Sonlanıyor" mesajının istenenden erken yazılmasına neden olan nesne, foo
'nun parametresidir çünkü o parametre, kopyalanacak biçimde tanımlamıştır.
Bu durumdaki en basit çözüm, kopyalamayı ve atamayı etkisizleştirilmektir:
struct Logger { @disable this(this); @disable this(ref const(typeof(this))); @disable Logger opAssign(ref const(typeof(this))); // ... }
Logger
nesneleri artık kopyalanamayacaklarından, foo
'nun da parametresini referans olarak alacak şekilde değiştirilmesi gerekir:
void foo(ref Logger logger) { // ... }
Özet
- Kurucu işlev (
this
) nesneleri kullanıma hazırlar. Derleyicinin otomatik olarak tanımladığı kurucu çoğu durumda yeterlidir. - Varsayılan kurucunun davranışı yapılarda değiştirilemez. Gerektiğinde onun yerine
static opCall
tanımlanır. - Tek parametreli kurucular
to
vecast
tarafından tür dönüşümü sırasında kullanılırlar. - Sonlandırıcı işlev (
~this
) nesnenin yaşamı sona ererken işletilmesi gereken işlemleri içerir. - Kopyalayıcı işlev (
this(ref const(typeof(this)))
) nesnenin var olan başka bir nesneden kopyalanarak nasıl kurulacağını belirler. - Kopya sonrası işlevinin (
this(this)
) yeni yazılan kodlarda kullanılması önerilmez; derleyicinin otomatik olarak gerçekleştirdiği kopyadan sonra gereken düzeltmeleri içerir. - Atama işlevi (
opAssign
) var olan nesnelerin başka nesnelerden atanmaları sırasında işletilir. - Üye işlevler
@disable
ile etkisizleştirilirler.