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

arayüz: [interface], yapının, sınıfın, veya modülün sunduğu işlevler
çok şekillilik: [polymorphism], başka bir tür gibi davranabilmek
değer türü: [value type], değer taşıyan tür
değişken: [variable], kavramları temsil eden veya sınıf nesnesine erişim sağlayan program yapısı
gerçekleştirme: [implementation], kodun oluşturulması
kalıtım: [inheritance], başka bir türün üyelerini türeme yoluyla edinmek
nesne: [object], belirli bir sınıf veya yapı türünden olan değişken
referans türü: [reference type], başka bir nesneye erişim sağlayan tür
sarma: [encapsulation], üyelere dışarıdan erişimi kısıtlamak
sınıf: [class], kendi üzerinde kullanılan işlevleri de tanımlayan veri yapısı
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
türetmek: [inherit], bir sınıfı başka sınıfın alt türü olarak tanımlamak
yapı: [struct], başka verileri bir araya getiren veri yapısı
yeniden tanımlama: [override], üye işlevin alt sınıf tarafından yeniden tanımlanması
... bütün sözlük



İngilizce Kaynaklar


Diğer




Sınıflar

Sınıflar kullanıcı türü tanımlamaya yarayan başka bir olanaktır. D'nin nesne yönelimli programlama olanakları sınıflar yoluyla gerçekleştirilir. Nesne yönelimli programlamayı üç temel kavram üzerinde düşünebiliriz:

Sarma, daha sonra göreceğimiz erişim hakları ile sağlanır. Kalıtım, gerçekleştirme türemesidir. Çok şekillilik ise arayüz türemesi yoluyla gerçekleştirilir.

Bu bölümde sınıfları genel olarak tanıtacağım ve özellikle referans türü olduklarına dikkat çekeceğim. Sınıfların diğer olanaklarını daha sonraki bölümlere bırakacağım.

Yapılarla karşılaştırılması

Sınıflar yapılara temelde çok benzerler. Bu yüzden daha önce şu bölümlerde yapılar üzerinde gördüğümüz hemen hemen her konu sınıflar için de geçerlidir:

Sınıfları yapılardan ayıran önemli farklar da vardır. Bu farkları aşağıdaki bölümlerde anlatıyorum.

Referans türleridir

Sınıfların yapılardan farklı olmalarının en büyük nedeni, yapıların değer türü olmalarına karşın sınıfların referans türü olmalarıdır. Aşağıdaki farklılıkların büyük bir çoğunluğu, sınıfların bu özelliğinden kaynaklanır.

Sınıf değişkenleri null olabilirler

Sınıf değişkenlerinin kendileri değer taşımadıklarından, asıl nesne new anahtar sözcüğü ile oluşturulur. Aynı nedenden, null ve is bölümünde de gösterildiği gibi, sınıf değişkenleri null da olabilirler. Yani, "hiçbir nesneye erişim sağlamıyor" olabilirler.

Hatırlayacağınız gibi, bir değişkenin null olup olmadığı == ve != işleçleriyle değil, duruma göre is ve !is işleçleriyle denetlenir:

    BirSınıf erişimSağlayan = new BirSınıf;
    assert(erişimSağlayan !is null);

    BirSınıf değişken;  // erişim sağlamayan
    assert(değişken is null);

Bunun nedeni == işlecinin nesnenin üyelerini de kullanmasının gerekebileceğidir. O üye erişimi, değişkenin null olduğu durumda programın bir bellek hatası ile sonlanmasına neden olur. O yüzden sınıf değişkenlerinin is veya !is ile karşılaştırılmaları gerekir.

Sınıf nesneleri ve değişkenleri

Sınıf nesnesi ile sınıf değişkeni farklı kavramlardır.

Sınıf nesnesi, new anahtar sözcüğü ile oluşturulan ve kendi ismi olmayan bir program yapısıdır. Temsil ettiği kavramı gerçekleştiren, onun işlemlerini yapan, ve o türün davranışını belirleyen hep bu sınıf nesnesidir. Sınıf nesnelerine doğrudan erişemeyiz.

Sınıf değişkeni ise sınıf nesnesine erişim sağlayan bir program yapısıdır. Kendisi iş yapmasa da eriştirdiği nesnenin aracısı gibi işlem görür.

Daha önce Değerler ve Referanslar bölümünde gördüğümüz şu koda bakalım:

    auto değişken1 = new BirSınıf;
    auto değişken2 = değişken1;

İlk satırda sağ taraftaki new, isimsiz bir BirSınıf nesnesi oluşturur. değişken1 ve değişken2 ise yalnızca bu isimsiz nesneye erişim sağlayan değişkenlerdir:

  (isimsiz BirSınıf nesnesi)   değişken1    değişken2
 ───┬───────────────────┬───  ───┬───┬───  ───┬───┬───
    │        ...        │        │ o │        │ o │
 ───┴───────────────────┴───  ───┴─│─┴───  ───┴─│─┴───
              ▲                    │            │
              │                    │            │
              └────────────────────┴────────────┘
Kopyalama

Değişkenleri etkiler.

Referans türü oldukları için; sınıf değişkenlerinin kopyalanarak oluşturulmaları, onların hangi nesneye erişim sağlayacaklarını belirler. Bu işlem sırasında asıl nesne kopyalanmaz.

Bir nesne kopyalanmadığı için de, yapılarda kopya sonrası işlevi olarak öğrendiğimiz this(this) üye işlevi sınıflarda bulunmaz.

    auto değişken2 = değişken1;

Yukarıdaki kodda değişken2, değişken1'in kopyası olarak oluşturulmaktadır. O işlem her ikisinin de aynı nesneye erişim sağlamalarına neden olur.

Sınıf nesnelerinin kopyalanmaları gerektiğinde bunu sağlayan bir üye işlev tanımlanmalıdır. Bu işleve kopyala() gibi bir isim verebileceğiniz gibi, dizilere benzemesi açısından dup() isminin daha uygun olduğunu düşünebilirsiniz. Bu işlev yeni bir nesne oluşturmalı ve ona erişim sağlayan bir değişken döndürmelidir:

class Sınıf {
    Yapı   yapıNesnesi;
    char[] dizgi;
    int    tamsayı;

// ...

    this(Yapı yapıNesnesi, const char[] dizgi, int tamsayı) {
        this.yapıNesnesi = yapıNesnesi;
        this.dizgi       = dizgi.dup;
        this.tamsayı     = tamsayı;
    }

    Sınıf dup() const {
        return new Sınıf(yapıNesnesi, dizgi, tamsayı);
    }
}

dup() içinde oluşturulan yeni nesne için yalnızca Sınıf'ın kurucusundan yararlanıldığına dikkat edin. Kurucu dizgi üyesini dup() ile açıkça kopyalıyor. yapıNesnesi ve tamsayı üyeleri ise değer türleri olduklarından onlar zaten otomatik olarak kopyalanırlar.

O işlevden örneğin şöyle yararlanılabilir:

    auto nesne1 = new Sınıf(Yapı(1.5), "merhaba", 42);
    auto nesne2 = nesne1.dup();

Sonuçta, nesne2 nesne1'in hiçbir üyesini paylaşmayan ayrı bir nesnedir.

Benzer biçimde, nesnenin immutable bir kopyası da ismi idup olan bir işlev tarafından sağlanabilir. Ancak, bu örnekteki kurucu işlevin de pure olarak tanımlanması gerekir. pure anahtar sözcüğünü ilerideki bir bölümde göreceğiz.

class Sınıf {
// ...
    this(Yapı yapıNesnesi, const char[] dizgi, int tamsayı) pure {
        // ...
    }

    immutable(Sınıf) idup() const {
        return new immutable(Sınıf)(yapıNesnesi, dizgi, tamsayı);
    }
}

// ...

    immutable(Sınıf) imm = nesne1.idup();
Atama

Değişkenleri etkiler.

Referans türü oldukları için; sınıf değişkenlerinin atanmaları, daha önce erişim sağladıkları nesneyi bırakmalarına ve yeni bir nesneye erişim sağlamalarına neden olur.

Eğer bırakılan nesneye erişim sağlayan başka değişken yoksa, asıl nesne ilerideki belirsiz bir zamanda çöp toplayıcı tarafından sonlandırılacak demektir.

    auto değişken1 = new BirSınıf;
    auto değişken2 = new BirSınıf;
    değişken1 = değişken2;

Yukarıdaki atama işlemi, değişken1'in kendi nesnesini bırakmasına ve değişken2'nin nesnesine erişim sağlamaya başlamasına neden olur. Kendisine erişim sağlayan başka bir değişken olmadığı için bırakılan nesne daha sonra çöp toplayıcı tarafından sonlandırılacaktır.

Atama işleminin davranışı sınıflar için değiştirilemez; yani opAssign sınıflarda yüklenemez.

Tanımlama

struct yerine class anahtar sözcüğü kullanılır:

class SatrançTaşı {
    // ...
}
Kurma

Kurucu işlevin ismi, yapılarda olduğu gibi this'tir. Yapılardan farklı olarak sınıf nesneleri {} karakterleri ile kurulamaz.

class SatrançTaşı {
    dchar şekil;

    this(dchar şekil) {
        this.şekil = şekil;
    }
}

Yapıların aksine, sınıf üyeleri kurucu parametre değerlerinden sırayla otomatik olarak kurulamazlar:

class SatrançTaşı {
    dchar şekil;
    size_t değer;
}

void main() {
    auto şah = new SatrançTaşı('♔', 100);  // ← derleme HATASI
}
Error: no constructor for SatrançTaşı

Nesnelerin o yazımla kurulabilmeleri için programcının açıkça bir kurucu tanımlamış olması şarttır.

Sonlandırma

Sonlandırıcı işlevin ismi yapılarda olduğu gibi ~this'tir:

    ~this() {
        // ...
    }

Ancak, yapılardan farklı olarak, sınıfların sonlandırıcıları nesnenin yaşamı sona erdiği an işletilmez. Yukarıda da değinildiği gibi, sonlandırıcı ilerideki belirsiz bir zamandaki bir çöp toplama işlemi sırasında işletilir.

Daha sonra Bellek Yönetimi bölümünde de göreceğimiz gibi, sınıf sonlandırıcılarının aşağıdaki kurallara uymaları şarttır:

Bu kurallara uymamak tanımsız davranıştır. Tanımsız davranışın bir etkisini sonlandırıcı içinde yeni bir sınıf nesnesi kurmaya çalışarak görebiliriz:

class C {
    ~this() {
        auto c = new C(); // ← YANLIŞ: Sınıf sonlandırıcısında
                          //           yeni nesne kuruluyor
    }
}

void main() {
    auto c = new C();
}

Program bir hata ile sonlanır:

core.exception.InvalidMemoryOperationError@(0)

Sonlandırıcı içinde çöp toplayıcıdan dolaylı olarak bellek ayırmak da aynı derecede yanlıştır. Örneğin, dinamik dizi elemanları için kullanılan bellek bölgesi de çöp toplayıcı tarafından yönetilir. Bu yüzden, bir dinamik dizinin yeni bellek ayrılmasını gerektirecek herhangi biçimde kullanılması da tanımsız davranıştır:

    ~this() {
        auto dizi = [ 1 ];  // ← YANLIŞ: Sınıf sonlandırıcısında
                            //           çöp toplayıcıdan
                            //           dolaylı olarak bellek
                            //           ayrılıyor
    }
core.exception.InvalidMemoryOperationError@(0)
Üye erişimi

Yapılarda olduğu gibi, üyelere nokta karakteri ile erişilir.

    auto şah = new SatrançTaşı('♔');
    writeln(şah.şekil);

Her ne kadar değişkenin bir üyesine erişiliyor gibi yazılsa da, erişilen asıl nesnenin üyesidir. Sınıf değişkenlerinin üyeleri yoktur, sınıf nesnelerinin üyeleri vardır. Bir başka deyişle, şah'ın şekil diye bir üyesi yoktur, isimsiz sınıf nesnesinin şekil diye bir üyesi vardır.

Not: Üye değişkenlere böyle doğrudan erişilmesi çoğu durumda doğru kabul edilmez. Onun yerine daha sonra Nitelikler bölümünde göreceğimiz sınıf niteliklerinden yararlanmak daha uygundur.

İşleç yükleme

Yapılardaki gibidir.

Bir fark, opAssign'ın sınıflar için özel olarak tanımlanamamasıdır. Yukarıda atama başlığında anlatıldığı gibi, sınıflarda atama işleminin anlamı yeni nesneye erişim sağlamaktır; bu anlam değiştirilemez.

Üye işlevler

Sınıf üye işlevleri yapılarda olduğu gibi tanımlanırlar ve kullanılırlar. Buna rağmen, aralarında önemli bir fark vardır: Sınıf üye işlevleri yeniden tanımlanabilirler ve bu, onlar için varsayılan durumdur. Yeniden tanımlama kavramını daha sonra Türeme bölümünde göreceğiz.

Yeniden tanımlama düzeneğinin program hızına kötü bir etkisi olduğundan, burada daha fazla ayrıntısına girmeden bütün sınıf üye işlevlerini final olarak tanımlamanızı öneririm. Bu ilkeyi derleyici hatası almadığınız sürece bütün sınıf üyeleri için uygulayabilirsiniz:

class Sınıf {
    final int işlev() {    // ← Önerilir
        // ...
    }
}

Yapılardan başka bir fark, bazı işlevlerin Object sınıfından kalıtım yoluyla hazır olarak edinilmiş olmalarıdır. Bunlar arasından toString işlevinin override anahtar sözcüğü ile nasıl tanımlandığını bir sonraki bölümde göreceğiz.

is ve !is işleçleri

Sınıf değişkenleri üzerinde işler.

is işleci, sınıf değişkenlerinin aynı nesneye erişim sağlayıp sağlamadıklarını bildirir. İki değişken de aynı nesneye erişim sağlıyorlarsa true, değilse false değerini üretir. !is işleci de bunun tersi olarak işler: Aynı nesneye erişim sağlıyorlarsa false, değilse true değerini üretir.

    auto benimŞah = new SatrançTaşı('♔');
    auto seninŞah = new SatrançTaşı('♔');
    assert(benimŞah !is seninŞah);

Yukarıdaki koddaki benimŞah ve seninŞah değişkenleri new ile oluşturulmuş olan iki farklı nesneye erişim sağladıkları için !is'in sonucu true'dur. Bu iki nesnenin aynı şekilde kurulmuş olmaları, yani ikisinin şekil üyelerinin de '♔' olması bunu değiştirmez; nesneler birbirlerinden ayrı iki nesnedir.

İki değişkenin aynı nesneye erişim sağladıkları durumda ise is işleci true üretir:

    auto benimŞah2 = benimŞah;
    assert(benimŞah2 is benimŞah);

Yukarıdaki iki değişken de aynı nesneye erişim sağlamaya başlarlar. is işleci bu durumda true üretir.

Özet