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

çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
değer türü: [value type], değer taşıyan tür
kurma: [construct], yapı veya sınıf nesnesini kullanılabilir duruma getirmek
kurucu işlev: [constructor], nesneyi kuran işlev
referans türü: [reference type], başka bir nesneye erişim sağlayan tür
sonlandırıcı işlev: [destructor], nesneyi sonlandıran işlev
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
vekil: [proxy], başka nesne yerine kullanılan nesne
... bütün sözlük



İngilizce Kaynaklar


Diğer




destroy ve scoped

Yaşam Süreçleri ve Temel İşlemler bölümünde değişkenlerin kurma işlemiyle başlayan ve sonlandırma işlemiyle biten yaşam süreçlerini görmüştük.

Daha sonraki bölümlerde de nesnelerin kurulması sırasında gereken işlemlerin this isimli kurucu işlevde, sonlandırılması sırasında gereken işlemlerin de ~this isimli sonlandırıcı işlevde tanımlandıklarını öğrenmiştik.

Sonlandırıcı işlev, yapılarda ve başka değer türlerinde nesnenin yaşamı sona ererken hemen işletilir. Sınıflarda ve başka referans türlerinde ise çöp toplayıcı tarafından sonraki bir zamanda işletilir.

Burada önemli bir ayrım vardır: bir sınıf nesnesinin yaşamının sona ermesi ile sonlandırıcı işlevinin işletilmesi aynı zamanda gerçekleşmez. Nesnenin yaşamı, örneğin geçerli olduğu kapsamdan çıkıldığı an sona erer. Sonlandırıcı işlevi ise çöp toplayıcı tarafından belirsiz bir zaman sonra otomatik olarak işletilir.

Sonlandırıcı işlevlerin görevlerinden bazıları, nesne için kullanılmış olan sistem kaynaklarını geri vermektir. Örneğin std.stdio.File yapısı, işletim sisteminden kendi işi için almış olduğu dosya kaynağını sonlandırıcı işlevinde geri verir. Artık sonlanmakta olduğu için zaten o kaynağı kullanması söz konusu değildir.

Sınıfların sonlandırıcılarının çöp toplayıcı tarafından tam olarak ne zaman çağrılacakları belli olmadığı için, bazen kaynakların sisteme geri verilmeleri gecikebilir ve yeni nesneler için kaynak kalmayabilir.

Sınıf sonlandırıcı işlevlerinin geç işletilmesini gösteren bir örnek

Sınıfların sonlandırıcı işlevlerinin ilerideki belirsiz bir zamanda işletildiklerini göstermek için bir sınıf tanımlayalım. Bu sınıfın kurucu işlevi sınıfın static bir sayacını arttırsın ve sonlandırıcı işlevi de o sayacı azaltsın. Hatırlarsanız, static üyelerden bir tane bulunur: Sınıfın bütün nesneleri o tek üyeyi ortaklaşa kullanırlar. Böylece o sayacın değerine bakarak sınıfın nesnelerinden kaç tanesinin henüz sonlandırılmadıklarını anlayabileceğiz.

class YaşamıGözlenen {
    int[] dizi;       // ← her nesnenin kendisine aittir

    static int sayaç; // ← bütün nesneler tarafından
                      //   paylaşılır

    this() {
        /* Her nesne bellekte çok yer tutsun diye bu diziyi
         * çok sayıda int'lik hale getiriyoruz. Nesnelerin
         * böyle büyük olmalarının sonucunda çöp
         * toplayıcının bellek açmak için onları daha sık
         * sonlandıracağını umuyoruz. */
        dizi.length = 30_000;

        /* Bir nesne daha kurulmuş olduğundan nesne sayacını
         * bir arttırıyoruz. */
        ++sayaç;
    }

    ~this() {
        /* Bir nesne daha sonlandırılmış olduğundan nesne
         * sayacını bir azaltıyoruz. */
        --sayaç;
    }
}

O sınıfın nesnelerini bir döngü içinde oluşturan bir program:

import std.stdio;

void main() {
    foreach (i; 0 .. 20) {
        auto değişken = new YaşamıGözlenen;  // ← baş
        write(YaşamıGözlenen.sayaç, ' ');
    } // ← son

    writeln();
}

O programda oluşan her YaşamıGözlenen nesnesinin yaşamı aslında çok kısadır: new anahtar sözcüğüyle başlar, ve foreach döngüsünün kapama parantezinde son bulur. Yaşamları sona eren bu nesneler çöp toplayıcının sorumluluğuna girerler.

Programdaki baş ve son açıklamaları her nesnenin yaşamının başladığı ve sona erdiği noktayı gösteriyor. Nesnelerin sonlandırıcı işlevlerinin, yaşamlarının sona erdiği an işletilmediklerini sayacın değerine bakarak görebiliyoruz:

1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 

Yukarıdaki çıktıdan anlaşıldığına göre, çöp toplayıcının bellek ayırma algoritması, bu deneyde bu sınıfın sonlandırıcısını en fazla 7 nesne için ertelemiştir. (Not: Bu çıktı çöp toplayıcının yürüttüğü algoritmaya, boş bellek miktarına ve başka etkenlere bağlı olarak farklı olabilir.)

Nesnenin sonlandırıcısını işletmek için destroy()

"Ortadan kaldır" anlamına gelen destroy() nesnenin sonlandırıcı işlevini çağırır:

void main() {
    foreach (i; 0 .. 20) {
        auto değişken = new YaşamıGözlenen;
        write(YaşamıGözlenen.sayaç, ' ');
        destroy(değişken);
    }

    writeln();
}

YaşamıGözlenen.sayaç'ın değeri new satırında kurucu işlevin işletilmesi sonucunda arttırılır ve 1 olur. Değerinin yazdırıldığı satırdan hemen sonraki destroy() satırında da sonlandırıcı işlev tarafından tekrar sıfıra indirilir. O yüzden yazdırıldığı satırda hep 1 olduğunu görüyoruz:

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 

Açıkça sonlandırılan nesneler geçersiz kabul edilmelidirler ve artık kullanılmamalıdırlar:

    destroy(değişken);
    // ...
    // Dikkat: Geçersiz bir nesneye erişiliyor
    writeln(değişken.dizi);

Normalde referans türleri ile kullanılan destroy, gerektiğinde struct nesnelerinin erkenden sonlandırılmaları için de kullanılabilir.

Ne zaman kullanmalı

Yukarıdaki örnekte gördüğümüz gibi, kaynakların çöp toplayıcının kararına kalmadan hemen geri verilmesi gerektiğinde kullanılır.

Örnek

Kurucu ve Diğer Özel İşlevler bölümünde XmlElemanı isminde bir yapı tanımlamıştık. O yapı, XML elemanlarını <etiket>değer</etiket> şeklinde yazdırmak için kullanılıyordu. XML elemanlarının kapama etiketlerinin yazdırılması sonlandırıcı işlevin göreviydi:

struct XmlElemanı {
    // ...

    ~this() {
        writeln(girinti, "</", isim, '>');
    }
}

O yapıyı kullanan bir programla aşağıdaki çıktıyı elde etmiştik:

<dersler>
  <ders0>
    <not>
      72
    </not>   ← Kapama etiketleri doğru satırlarda beliriyor
    <not>
      97
    </not>   
    <not>
      90
    </not>   
  </ders0>   
  <ders1>
    <not>
      77
    </not>   
    <not>
      87
    </not>   
    <not>
      56
    </not>   
  </ders1>   
</dersler>   

O çıktının doğru belirmesinin nedeni, XmlElemanı'nın bir yapı olmasıdır. Yapıların sonlandırıcıları hemen çağrıldıklarından, istenen çıktı, nesneleri uygun kapsamlara yerleştirerek elde edilir:

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

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

        foreach (i; 0 .. 3) {
            const not = XmlElemanı("not", 2);
            const rasgeleNot = uniform(50, 101);

            writeln(girintiDizgisi(3), rasgeleNot);

        } // ← not sonlanır

    } // ← ders sonlanır

} // ← dersler sonlanır

Nesneler açıklama satırları ile belirtilen noktalarda sonlandıkça XML kapama etiketlerini de çıkışa yazdırırlar.

Sınıfların farkını görmek için aynı programı bu sefer XmlElemanı bir sınıf olacak şekilde yazalım:

import std.stdio;
import std.array;
import std.random;
import std.conv;

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

class 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() {
    const dersler = new XmlElemanı("dersler", 0);

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

        foreach (i; 0 .. 3) {
            const not = new XmlElemanı("not", 2);
            const rasgeleNot = uniform(50, 101);

            writeln(girintiDizgisi(3), rasgeleNot);
        }
    }
}

Referans türleri olan sınıfların sonlandırıcı işlevleri çöp toplayıcıya bırakılmış olduğu için programın çıktısı artık istenen düzende değildir:

<dersler>
  <ders0>
    <not>
      57
    <not>
      98
    <not>
      87
  <ders1>
    <not>
      84
    <not>
      60
    <not>
      99
    </not>   ← Kapama etiketlerinin hepsi en sonda beliriyor
    </not>   
    </not>   
  </ders1>   
    </not>   
    </not>   
    </not>   
  </ders0>   
</dersler>   

Bütün sonlandırıcı işlevler işletilmişlerdir ama kapama etiketleri beklenen yerlerde değildir. (Not: Aslında çöp toplayıcı bütün nesnelerin sonlandırılacakları garantisini vermez. Örneğin programın çıktısında hiçbir kapama parantezi bulunmayabilir.)

XmlElemanı'nın sonlandırıcı işlevinin doğru noktalarda işletilmesini sağlamak için destroy() çağrılır:

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

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

        foreach (i; 0 .. 3) {
            const not = new XmlElemanı("not", 2);
            const rasgeleNot = uniform(50, 101);

            writeln(girintiDizgisi(3), rasgeleNot);

            destroy(not);
        }

        destroy(ders);
    }

    destroy(dersler);
}

Sonuçta, nesneler kapsamlardan çıkılırken sonlandırıldıkları için programın çıktısı yapı tanımında olduğu gibi düzgündür:

<dersler>
  <ders0>
    <not>
      66
    </not>   ← Kapama etiketleri doğru satırlarda belirmiş
    <not>
      75
    </not>   
    <not>
      68
    </not>   
  </ders0>   
  <ders1>
    <not>
      73
    </not>   
    <not>
      62
    </not>   
    <not>
      100
    </not>   
  </ders1>   
</dersler>   
Sonlandırıcı işlevi otomatik olarak çağırmak için scoped

Yukarıdaki programın bir yetersizliği vardır: Kapsamlardan daha destroy() satırlarına gelinemeden atılmış olan bir hata nedeniyle çıkılmış olabilir. Eğer destroy() satırlarının kesinlikle işletilmeleri gerekiyorsa, bunun bir çözümü Hatalar bölümünde gördüğümüz scope ve diğer olanaklardan yararlanmaktır.

Başka bir yöntem, sınıf nesnesini new yerine std.typecons.scoped ile kurmaktır. scoped(), sınıf değişkenini perde arkasında bir yapı nesnesi ile sarmalar. O yapı nesnesinin sonlandırıcısı kapsamdan çıkılırken otomatik olarak çağrıldığında sınıf nesnesinin sonlandırıcısını da çağırır.

scoped'un etkisi, yaşam süreçleri açısından sınıf nesnelerini yapı nesnelerine benzetmesidir.

Aşağıdaki değişikliklerden sonra program yine beklenen sonucu üretir:

import std.typecons;
// ...
void main() {
    const dersler = scoped!XmlElemanı("dersler", 0);

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

        foreach (i; 0 .. 3) {
            const not = scoped!XmlElemanı("not", 2);
            const rasgeleNot = uniform(50, 101);

            writeln(girintiDizgisi(3), rasgeleNot);
        }
    }
}

destroy() satırlarının çıkartılmış olduklarına dikkat edin.

scoped(), asıl sınıf nesnesini sarmalayan özel bir yapı nesnesi döndürür. Döndürülen nesne asıl nesnenin vekili (proxy) olarak görev görür. (Aslında, yukarıdaki dersler nesnesinin türü XmlElemanı değil, Scoped'dur.)

Kendisi otomatik olarak sonlandırılırken vekil nesne sarmaladığı sınıf nesnesini de destroy() ile sonlandırır. (Bu, RAII yönteminin bir uygulamasıdır. scoped() bunu ilerideki bölümlerde göreceğimiz şablon ve alias this olanaklarından yararlanarak gerçekleştirir.)

Vekil nesnelerinin kullanımlarının asıl nesne kadar doğal olması istenir. Bu yüzden, scoped()'un döndürdüğü nesne sanki asıl türdenmiş gibi kullanılabilir. Örneğin, asıl türün üye işlevleri vekil nesne üzerinde çağrılabilirler:

import std.typecons;

class C {
    void foo() {
    }
}

void main() {
    auto v= scoped!C();
    v.foo();    // Vekil nesnesi v, C gibi kullanılıyor
}

Ancak, bu kolaylığın bir bedeli vardır: Vekil nesnesi asıl nesneye referans döndürdükten hemen sonra sonlanmış ve döndürülen referans o yüzden geçersiz kalmış olabilir. Bu durum asıl sınıf türü sol tarafta açıkça belirtildiğinde ortaya çıkabilir:

    C c = scoped!C();    // ← HATALI
    c.foo();             // ← Sonlanmış bir nesneye erişir

Yukarıdaki c vekil nesnesi değil, açıkça C olarak tanımlandığından asıl nesneye erişim sağlamakta olan bir sınıf değişkenidir. Ne yazık ki bu durumda sağ tarafta kurulmuş olan vekil nesnesi kurulduğu ifadenin sonunda sonlandırılacaktır. Sonuçta, geçersiz bir nesneye erişim sağlamakta olan c'nin kullanılması tanımsız davranıştır. Örneğin, program bir çalışma zamanı hatasıyla çökebilir:

Segmentation fault

O yüzden, scoped() değişkenlerini asıl tür ile tanımlamayın:

    C         a = scoped!C();    // ← HATALI
    auto      b = scoped!C();    // ← doğru
    const     c = scoped!C();    // ← doğru
    immutable d = scoped!C();    // ← doğru
Özet