D.ershane D Programlama Dili Dersleri

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ğer
null: [null], hiçbir nesneye erişim sağlamayan
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ı
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ı
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



Sınıflar

Sınıflar, kullanıcı türleri tanımlamaya yarayan bir başka olanaktır.

D'nin nesneye yönelik programlama olanakları sınıflar yoluyla gerçekleştirilir. Nesneye yönelik 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 derste 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 derslere bırakacağım.

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

Sınıflar yapılara temelde çok benzerler. Bu yüzden daha önce şu derslerde 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.

new ile kurulurlar

Sınıf değişkenlerinin kendileri değer taşımadıkları için, asıl nesne new anahtar sözcüğü ile oluşturulur. Aynı nedenden, null ve is dersinde 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ığını == işleciyle değil, is işleciyle denetlememiz gerekir:

import std.stream;
// ...
    File erişimSağlayan = new File("mektup");

    File dosya;  // erişim sağlamayan
    assert(dosya is null);

Bunun nedeni; is yerine == kullanılması durumunda bir değişkenin null olmasının, programın bir bellek hatası ile sonlanmasına neden olacağıdır. O yüzden sınıf değişkenlerinin 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 dersinde gördüğümüz şu koda bakalım:

    auto dosya1 = new File("oyuncu_listesi", FileMode.Out);
    auto dosya2 = dosya1;

İlk satırda sağ taraftaki new, isimsiz bir File nesnesi oluşturur. O nesne, diskte "oyuncu_listesi" isminde bir dosya açmak ve bu dosyaya yazmak gibi görevleri üstlenir. dosya1 ve dosya2 ise yalnızca bu isimsiz nesneye erişim sağlayan değişkenlerdir:

                                dosya1       dosya2
    (isimsiz File nesnesi)     değişkeni    değişkeni
 ---+-------------------+---  ---+---+---  ---+---+---
    |        ...        |        | 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 dosya2 = dosya1;

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

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 dosya1 = new File("mektup");
    auto dosya2 = new File("yemek_tarifi");
    dosya1 = dosya2;

Yukarıdaki atama işlemi, dosya1'in "mektup" dosyasını bırakmasına ve "yemek_tarifi" dosyasına erişim sağlamasına neden olur. "mektup" dosyası ile ilgili olan asıl nesne çö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;
    }
}

Eğer sınıf başka bir sınıftan türetilmişse, türetildiği üst sınıfın kurucusunu çağırmak bu sınıfın görevidir. Bunu bir sonraki derste super anahtar sözcüğünü tanıtırken göstereceğim.

Sonlandırma

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

    ~this()
    {
        // ... nesne sonlanırken gereken işlemler ...
    }
Üye erişimi

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

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

Her ne kadar değişkenin bir üyesine erişiliyor gibi yazılsa da; erişilen, asıl nesnenin üyesidir.

Not: Üye değişkenlere böyle doğrudan erişilmesi çoğu durumda doğru kabul edilmez. Onun yerine, daha sonra anlatacağım 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; ve bu anlam değiştirilemez.

Üye işlevler

Yapılardaki gibidir.

Bir fark, bazı işlevlerin Object sınıfından kalıtım yoluyla hazır olarak edinilmiş olmalarıdır. Örneğin toString işlevini kendimiz tanımlamamış olsak bile kullanabiliriz:

    dout.writefln("%s", şah);

Hatırlarsanız, bir nesnenin "%s" düzeniyle yazdırılması, onun toString üye işlevini çağırıyordu. Yukarıdaki SatrançTaşı sınıfında böyle bir işlev tanımlı olmadığı halde, o satır derlenir ve bir çıktı üretir:

deneme.SatrançTaşı

Hemen hemen bütün durumlarda kullanışsız olan bu çıktının override sözcüğü ile nasıl değiştirildiğini bir sonraki derste göstereceğim.

is ve !is işleçleri

Sınıf değişkenleri üzerinde çalışır.

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 çalışır: 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 atama işlemi sonucunda iki değişken de aynı nesneye erişim sağlamaya başlarlar. is işleci bu durumda true üretir.

Problemler

    Aşağıdaki programda çok basit bir sınıf tanımlı. Program [0,19] aralığındaki sayıları sayılar dizisine, çift olanlarını da ayrıca çiftler dizisine yerleştiriyor:

    import std.cstream;
    import std.random;
    
    class Sayı
    {
        int değer;
    
        this(int değer)
        {
            this.değer = değer;
        }
    
        /*
          Not: Normalde bu sınıfın toString işlevinin de
               tanımlanması gerekir. Onu 'override' anahtar
               sözcüğünü tanıttığım bir sonraki derste
               göstereceğim.
    
          Bu sınıfın toString işlevi tanımlanmadığı için 'değer'
          üyesine aşağıda doğrudan erişmek zorunda kalıyoruz.
        */
    }
    
    void göster(string başlık, Sayı[] sayılar)
    {
        dout.writef("%-16s: ", başlık);
    
        foreach (eleman; sayılar) {
            dout.writef("%4s", eleman.değer);
        }
    
        dout.writefln();
    }
    
    void main()
    {
        Sayı[] sayılar;
        Sayı[] çiftler;
    
        // [0,19] aralığında 20 sayı
        foreach (i; 0 .. 20) {
            auto sayı = new Sayı(i);
            sayılar ~= sayı;
    
            // Çift olanlarını ayrı bir diziye de yerleştiriyoruz
            if ((i % 2) == 0) {
                çiftler ~= sayı;
            }
        }
    
        göster("çiftler (önce)", çiftler);
    
        // 'sayılar' dizisinin elemanlarına rastgele değerler
        // ekliyoruz
        foreach (eleman; sayılar) {
            eleman.değer += uniform(0, 20);
        }
    
        göster("çiftler (sonra)", çiftler);
    }
    

    Programda göster işlevinin çağrıldığı iki satır arasında çiftler dizisine hiç dokunulmadığı halde göster işlevi iki farklı çıktı üretiyor:

    çiftler (önce)  :    0   2   4   6   8  10  12  14  16  18
    çiftler (sonra) :    4  14  11  13  23  17  19  26  18  26
    
  1. Bunun neden olduğunu açıklayın.
  2. Programda tek bir değişiklik yapın:
  3.         eleman.değer += uniform(0, 20);
    

    satırını şu satırla değiştirin:

            eleman = new Sayı(uniform(0, 20));
    

    çiftler dizisinin elemanlarının artık değişmediğini görün:

    çiftler (önce)  :    0   2   4   6   8  10  12  14  16  18
    çiftler (sonra) :    0   2   4   6   8  10  12  14  16  18
    

    Bunun da nedenini açıklayın.

  4. Tekrar ilk programa dönün ve üç değişiklik yapın:
    • class anahtar sözcüğünü struct ile değiştirin
    • new anahtar sözcüğünü silin (programın struct ile derlenebilmesi için silinmesi gerekir)
    • sayılar'ın son halini göstermek için programın en sonuna şu satırı ekleyin:
    •     göster("sayılar", sayılar);
      

    Hem çiftler'in bu durumda da değişmediğini, hem de programdaki eleman.değer += uniform(0, 20); satırına rağmen sayılar dizisinin elemanlarının da rastgele değerleri olmadığını gözlemleyin:

    çiftler (önce)  :    0   2   4   6   8  10  12  14  16  18
    çiftler (sonra) :    0   2   4   6   8  10  12  14  16  18
    sayılar         :    0   1   2   3   4   5   6   7   8   9
    10  11  12  13  14  15  16  17  18  19
    

    Bu durumu da açıklayın.

(çözümler sonra gelecek...)