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

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));
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ı 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");
Varsayılan kurucunun 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 kesinlikle ö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
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(in string isim, in 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(in 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(in int girintiAdımı) {
    return replicate(" ", girintiAdımı * 2);
}

struct XmlElemanı {
    string isim;
    string girinti;

    this(in string isim, in 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.

Kopya sonrası işlevi

Kopyalama, var olan bir nesnenin kopyası olarak yeni bir nesne oluşturmaktır.

Yapılarda kopyalama işinin ilk aşamasını derleyici gerçekleştirir. Yeni nesnenin bütün üyelerini var olan nesnenin üyelerinden sırayla kopyalar:

    auto dönüşSüresi = gidişSüresi;   // kopyalama

Bu işlemi atama işlemi ile karıştırmayın. Yukarıda sol taraftaki auto, dönüşSüresi isminde yeni bir nesne kurulduğunu gösterir. auto, oradaki tür ismi yerine geçmektedir.

Atama olması için dönüşSüresi'nin daha önceden tanımlanmış bir nesne olması gerekirdi:

    dönüşSüresi = gidişSüresi;  // atama (aşağıda anlatılıyor)

Kopyalama ile ilgili olarak gereken olası özel işlemler otomatik kopyalama işlemi sonlandıktan sonra gerçekleştirilirler.

Kurma ile ilgili olduğu için kopya sonrası işlevinin ismi de this'tir. Diğer kuruculardan ayırt edilebilmesi için parametre listesine özel olarak this yazılır:

    this(this) {
        // ...
    }

Yapılar bölümünde basit bir Öğrenci yapısı kullanmış ve onun nesnelerinin kopyalanmaları ile ilgili bir sorundan söz etmiştik:

struct Öğrenci {
    int numara;
    int[] notlar;
}

O yapının notlar üyesi dinamik dizi olduğu için bir referans türüdür. Bu, bir Öğrenci nesnesinin bir başkasına kopyalanması durumunda ikisinin notlar üyelerinin aynı asıl elemanlara erişim sağlamalarına neden olur. Bu yüzden, birinin notlarında yapılan değişiklik diğerinde de görülür:

    auto öğrenci1 = Öğrenci(1, [ 70, 90, 85 ]);

    auto öğrenci2 = öğrenci1;   // kopyalama
    öğrenci2.numara = 2;

    öğrenci1.notlar[0] += 5;    // ikincinin notu da değişir:
    assert(öğrenci2.notlar[0] == 75);

Bunun önüne geçmek için ikinci öğrencinin notlar üyesinin yalnızca o nesneye ait bir dizi olması sağlanmalıdır. Bunu kopya sonrası işlevinde gerçekleştirebiliriz:

struct Öğrenci {
    int numara;
    int[] notlar;

    this(this) {
        notlar = notlar.dup;
    }
}

this(this) işlevine girildiğinde bütün üyelerin çoktan asıl nesnenin kopyaları olarak otomatik olarak kurulduklarını hatırlayın. İşleve girildiğindeki notlar, asıl nesnenin notlar'ı ile aynı diziye erişim sağlamaktadır. Yukarıdaki tek satırda yapılan ise, notlar'ın erişim sağladığı asıl dizinin bir kopyasını almak ve onu yine bu nesnenin notlar'ına atamaktır. Böylece bu nesnenin notlar'ı yeni bir diziye erişim sağlamaya başlar.

Birinci öğrencinin notlarında yapılan değişiklik artık ikinci öğrencinin notlarını etkilemez:

    öğrenci1.notlar[0] += 5;
    assert(öğrenci2.notlar[0] == 70);
Kopya sonrası işlevinin etkisizleştirilmesi

Kopya sonrası işlevi de @disable ile etkisizleştirilebilir. Böyle bir türün nesneleri otomatik olarak kopyalanamazlar:

struct Arşiv {
// ...

    @disable this(this);
}

// ...

    auto a = Arşiv("kayitlar");
    auto b = a;                    // ← derleme HATASI

Derleyici Arşiv nesnelerinin kopyalanamayacağını bildirir:

Error: struct deneme.Arşiv is not copyable because it is
annotated with @disable
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.

Atama işlecini farklı türlerden parametre alacak şekilde tanımlamanın yararlı olduğunu düşündüğünüz zamanlarda bunun gerçekten gerekli olup olmadığını iyice tartmanızı öneririm. Kimi zaman yararlıdır, kimi zaman gereksiz ve sorunludur.

Özet