D.ershane D Programlama Dili Dersleri

çö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
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



scope

Yaşam Süreçleri ve Temel İşlemler dersinde 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 derslerde 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 tek adet 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ı anlayabiliriz.

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öylece büyük olmaları nedeniyle, çö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ğu için nesne sayacını
         * bir arttırıyoruz.
         */
        ++sayaç;
    }

    ~this()
    {
        /*
         * Bir nesne daha sonlandırılmış olduğu için 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 

Çöp toplayıcının bellek ayırma algoritması, bu deneyde bu sınıfın nesnelerinden en fazla 7 adet bulunmasına izin veriyor. (Bu çıktı siz denediğinizde farklı olabilir.)

Tek nesne için scope

"Kapsam" anlamına gelen scope, nesnenin sonlandırıcı işlevinin; nesneyi oluşturan kapsamdan çıkılırken hemen çağrılmasını sağlar:

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

    writeln();
}

Yukarıdaki program artık her an tek nesne kullanacaktır. scope olarak işaretlenen nesnelerin sonlandırıcı işlevleri hemen çağrılmaktadır:

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 
Bütün sınıf için scope

Bazı durumlarda, tasarladığımız bir sınıfın nesnelerinin böyle tek tek seçime bağlı olarak değil, hep birden scope olarak tanımlanmalarını isteriz. Bu, scope anahtar sözcüğü sınıf tanımından önce yazılarak sağlanır:

scope class YaşamıGözlenen
{
    // ...
}

Sınıfın scope olarak işaretlenmesi, o türün nesnelerinin de scope olarak işaretlenmelerini şart koşar. Bu da, bu türün nesnelerinin sonlandırıcılarının hemen çağrılmalarını sağlar.

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.

Başka bir nedeni, nesnelerin belirli bir sırada sonlandırılıyor olmalarının programın akışı açısından önemli olduğu durumlardır. Örneğin RAII yöntemi, sonlandırıcılardaki kodların nesnelerin sonlandırıldıkları anda işletilmeleri beklentisi üzerine kuruludur.

RAII, "resource acquisition is initialization"ın kısaltmasıdır. Tam çevirisi "kaynak ayırmak ilklemektir" olsa da, daha çok bunun tümleyeni olan "kaynaklar sonlandırıcı işlevlerde geri verilmelidir" ilkesini temsil eder.

RAII yönteminin bir örneği olarak aşağıdaki XML programına bakabiliriz.

Örnek

Kurucu ve Diğer Özel İşlevler dersinde 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. Aynı düzeyde bulunan açma ve kapama etiketlerini aynı renkte göstererek:

<dersler>
  <ders0>
    <not>
      72
    </not>   ← Kapama etiketleri doğru satırlarda belirmiş
    <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 çalışmasının nedeni, XmlElemanı'nın bir yapı, yani değer türü olmasıdır. Yapıların sonlandırıcıları hemen çağrıldığı için; nesneleri uygun kapsamlara yerleştirmek, istenen çıktıyı elde etmek için yeterlidir:

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 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 sonlanırken, 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.string;
import std.random;
import std.conv;

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

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

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

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

            const int rasgeleNot = uniform(50, 101);
            writeln(girintiDizgisi(3), rasgeleNot);
        }
    }
}

Sınıflar referans türleri oldukları için sonlandırıcı işlevleri çöp toplayıcıya bırakılınca, programın çıktısı artık doğru 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 belirmiş
    </not></not></ders1></not></not></not></ders0></dersler>

Elemanların kapama etiketleri beklendikleri yerlerde değil, çıktının en sonunda belirmiştir.

Bu durumu düzeltmek ve XmlElemanı sınıfının doğru çalışmasını garanti etmek için onu bir scope sınıfı olarak işaretleyebiliriz:

scope class XmlElemanı
{

O işaret, programdaki bütün XmlElemanı nesnelerinin de scope olarak işaretlenmelerini gerektirir:

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

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

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

            const int rasgeleNot = uniform(50, 101);
            writeln(girintiDizgisi(3), rasgeleNot);
        }
    }
}

Sonuçta da, 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>
Özet

Nesnelerin normalde çöp toplayıcı tarafından sonraki bir zamanda sonlandırılmaları yerine, yaşamlarının sona erdiği anda sonlandırılmaları için scope anahtar sözcüğü kullanılır.