D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

atama: [assign], değişkene yeni bir değer vermek
çıkarsama: [deduction, inference], derleyicinin kendiliğinden anlaması
işleç: [operator], bir veya daha fazla ifadeyle iş yapan özel işaret (+, -, =, [], vs.)
kapsam: [scope], küme parantezleriyle belirlenen bir alan
kopya sonrası: [post blit], üyelerin kopyalanmalarından sonraki işlemler
kopyalama: [copy construct], nesneyi başka bir nesnenin kopyası olarak kurmak
kopyalayıcı işlev: [copy constructor], nesneyi kopyalayan işlev
kurma: [construct], yapı veya sınıf nesnesini kullanılabilir duruma getirmek
kurucu işlev: [constructor], nesneyi kuran işlev
sonlandırıcı işlev: [destructor], nesneyi sonlandıran işlev
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
tür nitelendirici: [type qualifier], const, immutable, shared, ve inout
varsayılan: [default], özellikle belirtilmediğinde kullanılan
... bütün sözlük



İngilizce Kaynaklar


Diğer




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:

Ek olarak, yeni yazılacak olan kodlar için önerilmeyen ve geçmişten kalmış olan bir işlev daha vardır:

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:

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:

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:

  1. a.i'yi varolanNesne.i'den kopyalar
  2. a.d'yi varolanNesne.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:

  1. 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.
  2. 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:

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:

  1. 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.

  2. Soldaki nesneyi sonlandırır.

    Atama işleminin diğer parçası bu adımdır.

  3. 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:

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