D.ershane D Programlama Dili Dersleri

atama: [assign], değişkene yeni bir değer vermek
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
varsayılan: [default], özellikle belirtilmediğinde kullanılan
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



Kurucu ve Diğer Özel İşlevler

Bu derste her ne kadar 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 derslerde göstereceğim.

Yapıların üye işlevleri arasından dört tanesi, nesnelerin temel işlemlerini belirledikleri için ayrıca önemlidir:

Nesnelerin bu temel işlemlerini de kendimiz belirleyebiliriz.

Bu dört temel işlemin normalde yapılar için özel olarak tanımlanmaları gerekmez. Çünkü o işlemler zaten derleyici tarafından otomatik olarak halledilirler. Yine de, bazı durumlarda bu işlevleri kendimiz tanımlamak isteyebiliriz.

Kurucu işlev

Kurucu işlevin asıl görevi, bir nesnenin üyelerine gerekli değerleri atayarak onu kullanılabilir hale getirmektir.

Kurucu işlevleri şimdiye kadar hem bütün yapı örneklerinde hem de örneğin File gibi başka türlerde gördük. Türün ismini bir işlev çağrısı gibi kullandığımız noktalarda o türün kurucu işlevi çağrılır. Örneğin şu satırın sağ tarafında:

    auto dersBaşı = GününSaati(8, 30);

Daha önce de gördüğümüz gibi, şu dosya kurulurken de sağ tarafta:

    auto dosya = new File("resim_listesi", FileMode.In);

Tür ismi işlev çağrısı gibi kullanılırken parantez içinde yazılanlar da kurucu işlevin parametreleri olurlar. Örneğin yukarıdaki "resim_listesi" ve FileMode.In değerleri File kurucu işlevinin parametreleridir.

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 ...
    }
}
Derleyici tarafından sağlanan otomatik kurucu işlev

Şimdiye kadar gördüğümüz bütün yapı örneklerinde derleyici tarafından sağlanan otomatik kurucu işlevi kullandık. O kurucunun işi, parametre değerlerini sırayla üyelere atamaktır.

Ayrıca Yapılar dersinden hatırlayacağınız gibi, parametre listesinde sonda bulunan parametreler için değer belirtilmesi gerekmez. Değerleri belirtilmeyen üyeler, kendi türlerinin .init değerlerini alırlar.

Parametre Serbestliği dersinde gösterilen varsayılan parametre değerleri olanağını da hatırlarsak, derleyicinin sağladığı otomatik kurucu işlevin şu şekilde yazılmış olduğunu düşünebiliriz:

struct Deneme
{
    char   karakter;
    int    tamsayı;
    double kesirli;

    /* Derleyicinin sağladığı kurucu işlevin eşdeğeri */
    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 üyeler için geçerli değerlerin parametre olarak verilmesi, nesnelerin kurulmuş olmaları için yeterlidir.

Üyelere this. ile erişim

Yukarıdaki kodda parametrelerle üyeler karışmasınlar diye parametrelerin sonlarına _parametre diye bir belirteç ekledim. Çünkü 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)
    {
        // parametreyi kendisine atıyor!
        karakter = karakter;    // ← derleme HATASI
        tamsayı  = tamsayı;
        kesirli  = kesirli;
    }
}

İşlev içinde karakter yazıldığında üye değil, parametre anlaşılır. Parametreler de giriş parametresi oldukları için in olarak işaretlendiklerinden sabit değerin değiştirilemeyeceğini bildiren derleme hatası alırız:

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 nesne anlamına gelir. Bu olanağı kullanınca, parametrelerin isimlerinin sonlarına artık _parametre gibi ekler yazmak 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 şekilde derlenir ve çalışır.

Bunları, derleyicinin otomatik olarak yazdığı kurucu işlevin perde arkasında nasıl çalıştığını göstermek için anlattım. Yukarıda da belirttiğim gibi, eğer yapının kurulması için bu kadarı yeterliyse, bu kurucuyu yazmanız gerekmez. Bu kurucu, perde arkasında zaten derleyici tarafından otomatik olarak yazılır ve çağrılır.

Programcı tarafından tanımlanan kurucu işlev

Bazen nesnenin kurulabilmesi için üyelere sırayla değer atamaktan daha karmaşık işlemler gerekebilir. Örnek olarak önceki derste kullandığımız 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ığı için, 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ı bu fazladan hesaplardan kurtarmak için, saat ve dakika miktarlarını iki ayrı parametre olarak alan bir Süre kurucusu tanımlayabiliriz. Böylece toplam dakika hesabı kurucu içinde yapılabilir:

struct Süre
{
    int dakika;

    this(int saat, int dakika)
    {
        this.dakika = saat * 60 + dakika;
    }
}

Saat ve dakika farklı iki parametre olunca, programcılar da aritmetik hesabı 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 kurucuyu iptal eder

Programcı tarafından tek bir kurucu işlev bile tanımlanmış olması, derleyicinin kurma işini bütünüyle programcıya bırakmasına neden olur. Artık otomatik kurucu yoktur, ve Süre'nin şu daha basit kullanımı 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 uymaz. Çözüm olarak kurucuyu yükleyebilir ve bir tane de tek parametreli olanını 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, 0 };    // ← derleme HATASI

Görüldüğü gibi; tek bir kurucu işlevin bile tanımlanmış olması, derleyicinin kurma işini bütünüyle programcıya bırakmasına neden olur.

Başka kurucu işlevleri çağırmak

Kurucu işlevler başka kurucu işlevleri çağırabilirler. Böylece kod tekrarı azaltılmış olur. Süre gibi basit bir yapı bunun yararını görmek için uygun değil. Yine de kullanımını şöyle 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 olarak 0 göndererek çağırıyor.

Uyarı

Yukarıdaki Süre kurucularında bir tasarım hatası bulunduğunu söyleyebiliriz. Bunun nedeni, nesneler tek parametre ile kurulduklarında ne yapılmak istendiğinin açık olmayabileceğidir:

    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ıktır.

Böyle tasarımlar programlarda karışıklıklara neden olur; kaçınılmaları gerekir.

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ı için 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ımızı kesmekte olduğumuz bildirilecektir; vs.

Kurucuda olduğu gibi, sonlandırıcı işlevin de dönüş türü yoktur ve ismi ~this'tir.

Sonlandırıcı işlev mutlaka işletilir

Sonlandırıcı işlev, yapı nesnesinin geçerliliği bittiği an işletilir. Yaşam Süreçleri dersinden hatırlayacağınız gibi; nesnelerin yaşam süreçlerinin, tanımlandıkları kapsamdan çıkılırken sona erdiğini görmüştük.

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ıya bakalı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 olsun:

  <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 üyesi olacak şekilde 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 belirleyerek istediğimiz çıktıyı elde edebiliriz. Örneğin çıktıya nesne kurulduğunda <eleman>, sonlandırıldığında da </eleman> yazdırabiliriz.

Kurucu işlevi bu amaca göre şöyle yazabiliriz:

    this(in string isim, in int düzey)
    {
        this.isim = isim;
        this.girinti = girintiDizgisi(düzey);

        dout.writefln(girinti, '<', isim, '>');
    }

Kurucunun son satırı, XML elemanının açılmasını sağlar. girintiDizgisi, o düzeyin girintisini belirleyen ve boşluklardan oluşan bir string üretir:

string girintiDizgisi(in int girintiAdımı)
{
    return repeat(" ", girintiAdımı * 2);
}

Yararlandığı repeat işlevi, kendisine verilen dizgiyi belirtilen sayıda uç uca ekleyerek yeni bir dizgi üreten bir işlevdir; std.string 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 benzer şekilde ve XML elemanını kapatmak için şöyle yazabiliriz:

    ~this()
    {
        dout.writefln(girinti, "</", isim, '>');
    }

O yapıyı kullanan bir deneme programı aşağıdaki gibi yazılabilir:

void main()
{
    auto dersler = XmlElemanı("dersler", 0);

    foreach (dersNumarası; 0 .. 2) {
        auto ders =
            XmlElemanı("ders" ~ to!string(dersNumarası), 1);

        foreach (i; 0 .. 3) {
            auto not = XmlElemanı("not", 2);

            const int rastgeleNot = uniform(50, 101);
            dout.writefln(girintiDizgisi(3), rastgeleNot);
        }
    }
}

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.

Programın çıktısında birbirlerine karşılık gelen kurucu ve sonlandırıcı çıktılarını aynı renkle gösterdim. Dış kapsam için kırmızı, ortadaki için mavi, ve içerdeki için yeşil:

<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 sırayla, var olan nesnenin üyelerinden kopyalar:

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

Bu işlemi atama işlemi ile karıştırmayın. 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ı gerekir:

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

Eğer herhangi bir nedenle kopyalama ile ilgili olarak özel bir işlem gerçekleştirmek istersek, bunu ancak otomatik kopyalama işleminden sonrasını belirleyecek şekilde yapabiliriz. Biz otomatik kopyalamaya karışamayız, ancak sonrasını belirleyebiliriz.

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

    this(this)
    {
        // ...
    }

Yapılar dersinde 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 diziye erişim sağlamalarına neden olur. 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 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 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);
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, nesne temel işlemleri arasında diğerlerinden biraz daha karmaşıktır ve bu nedenle hatalara açıktır. Bunun nedeni, atama işleminin aslında iki adımdan oluşuyor olmasıdır:

Ancak, bu iki adımın yazdığım sırada işletilmelerinde önemli bir sorun vardır: Daha nesnenin ikinci adımda başarıyla kurulabileceğinden emin olmadan, onu birinci adımda güvenle sonlandıramayız. Yoksa, nesnenin yeni değerle tekrar kurulması aşamasında bir hata atılsa, elimizde sonlandırılmış ama tekrar kurulamamış bir nesne kalır.

Derleyicinin sunduğu otomatik atama işleci bu yüzden güvenli hareket etmek zorundadır ve perde arkasında şu işlemleri gerçekleştirir:

  1. bu nesneyi geçici bir nesneye kopyalar
  2. yeni değeri bu nesneye kopyalar
  3. geçici nesneyi sonlandırı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;

    ref Süre opAssign(const ref Süre sağdaki)
    {
        dout.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
dakika, 100 değerinden 200 değerine değişiyor

Süre gibi küçük yapılarda parametre türü const ref yerine in olarak da belirtilebilir:

    ref Süre opAssign(in Süre sağdaki)
    {
        // ...
    }

Farkı, in parametrenin işleve kopyalanarak gönderilmesidir. Anlamsal olarak farkları olmadığı gibi, küçük yapılarda hız kaygısı da doğurmaz.

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;

    ref Süre opAssign(const ref Süre sağdaki)
    {
        dout.writefln(
            "dakika, %s değerinden %s değerine değişiyor",
            this.dakika, sağdaki.dakika);

        this.dakika = sağdaki.dakika;

        return this;
    }

    ref Süre opAssign(int dakika)
    {
        dout.writefln(
            "dakika, bir tamsayı değer ile değiştiriliyor");

        this.dakika = dakika;

        return this;
    }
}
// ...
    süre = Süre(200);
    süre = 300;
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.