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 var: 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 File sınıfı, işletim sisteminden kendi işi için almış olduğu dosya kaynağını sonlandırıcı işlevde 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.

Örnek olarak; çalıştırıldığı klasörde "deneme_dosyasi" isminde bir dosya olduğunu varsayarsak, bir döngü içinde çok sayıda File nesnesi oluşturan şu programa bakabiliriz:

import std.cstream;
import std.stream;

void main()
{
    foreach (sayaç; 1 .. int.max) {
        auto dosya = new File("deneme_dosyasi",   // ← baş
                              FileMode.In);
        dout.writef(sayaç, ' ');
    } // ← son
}

O programda oluşan her File 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.

Bu durumda sorun; çöp toplayıcı sonlandırıcı işlevleri çağırmaya karar verene kadar, dosya erişimi kaynaklarının tükenebileceğidir.

Yukarıdaki programın, bir Linux işletim sistemindeki kısaltılmış çıktısı şöyle:

$ ./deneme
std.stream.OpenException: Cannot open or create file 'deneme_dosyasi'
1 2 3 ... ... ... 1019 1020 1021

O çıktıdan anlaşıldığına göre; program başladığında otomatik olarak açık olarak gelen din, dout, ve derr akımlarını da sayarsak; bu işletim sistemi, programların belirli bir anda 1024'ten daha fazla dosyaya erişmesine izin vermemektedir.

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:

import std.cstream;
import std.stream;

void main()
{
    foreach (sayaç; 1 .. int.max) {
        scope auto dosya = new File("deneme_dosyasi",
                                    FileMode.In);
        dout.writef(sayaç, ' ');
    }
}

Yukarıdaki program artık bir seferde en fazla 4 tane dosya erişicisi kullanacaktır: din, dout, derr, ve döngü içinde oluşturduğu File nesnesinin kullandığı... Bu yüzden de program; hiçbir kaynak sıkıntısı yaşamadan, sayaç int.max'e gelene kadar çalışmaya devam edecektir.

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 XmlElemanı
{
    // ...
}

Sınıfın scope olarak işaretlenmesi, o türün nesnelerinin de scope olarak işaretlenmelerini mecburi hale gelir. 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ıyı beklemeden 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()
    {
        dout.writefln(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 rastgeleNot = uniform(50, 101);
            dout.writefln(girintiDizgisi(3), rastgeleNot);

        } // ← 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.cstream;
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);

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

    ~this()
    {
        dout.writefln(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 rastgeleNot = uniform(50, 101);
            dout.writefln(girintiDizgisi(3), rastgeleNot);
        }
    }
}

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 rastgeleNot = uniform(50, 101);
            dout.writefln(girintiDizgisi(3), rastgeleNot);
        }
    }
}

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.