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

alt sınıf: [subclass], başka sınıftan türetilen sınıf
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
çökme: [crash], programın hata ile sonlanması
gerçekleştirme: [implementation], kodun oluşturulması
kalıtım: [inheritance], başka bir türün üyelerini türeme yoluyla edinmek
kurucu işlev: [constructor], nesneyi kuran işlev
Phobos: [Phobos], D dilinin standart kütüphanesi
sanal işlev: [virtual function], tanımı alt sınıfta değiştirilebilen işlev
sanal işlev tablosu: [virtual function table, vtbl], sınıfın sanal işlev göstergelerinden oluşan tablo
sıradüzen: [hierarchy], sınıfların türeyerek oluşturdukları aile ağacı
soyut: [abstract], somut gerçekleştirmesi verilmemiş olan
türetmek: [inherit], bir sınıfı başka sınıfın alt türü olarak tanımlamak
üst sınıf: [super class], kendisinden sınıf türetilen sınıf
varsayılan: [default], özellikle belirtilmediğinde kullanılan
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




Türeme

Daha genel bir türün daha özel bir alt türünü tanımlamaya türetme denir. Türetilen alt tür; genel türün üyelerini edinir, onun gibi davranır, ve onun yerine geçebilir.

D'de türeme yalnızca sınıflar arasında geçerlidir. Yeni bir sınıf, mevcut başka bir sınıftan türetilerek tanımlanabilir. Bir sınıfın türetildiği türe üst sınıf, ondan türetilen yeni sınıfa da alt sınıf adı verilir. Üst sınıfın özelliklerinin alt sınıf tarafından edinilmesine kalıtım denir.

D'de iki tür türeme vardır. Bu bölümde gerçekleştirme türemesi olan class'tan türemeyi göstereceğim; arayüz türemesi olan interface'ten türemeyi ise daha sonraki bir bölüme bırakacağım.

Sınıfın hangi sınıftan türetildiği, tanımlanırken isminden sonra yazılan : karakterinden sonra belirtilir:

class AltSınıf : ÜstSınıf {
    // ...
}

Masa saati kavramını temsil eden bir sınıf olduğunu varsayalım:

class Saat {
    int saat;
    int dakika;
    int saniye;

    void ayarla(int saat, int dakika, int saniye = 0) {
        this.saat = saat;
        this.dakika = dakika;
        this.saniye = saniye;
    }
}

Bu sınıfın üyelerinin, nesne oluşturulduğu an özel değerler almalarının şart olmadığını varsayalım. O yüzden bu sınıfın kurucu işlevine gerek yok. Saat, daha sonraki bir zamanda ayarla üye işlevi ile ayarlanabiliyor; ve varsayılan değeri belirtilmiş olduğu için de saniye değerini vermek isteğe bağlı:

    auto masaSaati = new Saat;
    masaSaati.ayarla(20, 30);
    writefln(
        "%02s:%02s:%02s",
        masaSaati.saat, masaSaati.dakika, masaSaati.saniye);

Not: Zaman bilgisini toString üye işlevi ile yazdırmak çok daha uygun olurdu. O işlevi biraz aşağıda override anahtar sözcüğünü tanırken ekleyeceğiz.

Yukarıdaki kodun çıktısı:

20:30:00

Bu kadarına bakarak Saat sınıfının bir yapı olarak da tanımlanabileceğini düşünebiliriz. Bu üç üyeyi bir yapı olarak da bir araya getirebilirdik, ve o yapı için de üye işlevler tanımlayabilirdik. Programa bağlı olarak, bu kadarı yeterli de olabilirdi.

Oysa Saat'in sınıf olması, bize ondan yararlanarak yeni türler tanımlama olanağı sunar.

Örneğin, temelde bu Saat sınıfının olanaklarını olduğu gibi içeren, ve ek olarak alarm bilgisi de taşıyan bir ÇalarSaat sınıfı düşünebiliriz. Bu sınıfı tek başına tanımlamak istesek; Saat'in mevcut üç üyesinin aynılarına ek olarak iki tane de alarm üyesi, ve saati ayarlamak için kullanılan ayarla işlevinin yanında da bir alarmıKur işlevi gerekirdi.

Bu sınıf, bu anlatıma uygun olarak şöyle gerçekleştirilebilir:

class ÇalarSaat {
    int saat;
    int dakika;
    int saniye;
    int alarmSaati;
    int alarmDakikası;

    void ayarla(int saat, int dakika, int saniye = 0) {
        this.saat = saat;
        this.dakika = dakika;
        this.saniye = saniye;
    }

    void alarmıKur(int saat, int dakika) {
        alarmSaati = saat;
        alarmDakikası = dakika;
    }
}

Saat sınıfında da bulunan üyelerini sarı ile gösterdim. Görüldüğü gibi; Saat ve ÇalarSaat sınıflarını aynı program içinde bu şekilde ayrı ayrı tanımlamak oldukça fazla kod tekrarına neden olur.

class'tan türetmek, bir sınıfın üyelerinin başka bir sınıf tarafından oldukları gibi edinilmelerini sağlar. ÇalarSaat'i Saat'ten türeterek tanımlamak, yeni sınıfı büyük ölçüde kolaylaştırır ve kod tekrarını ortadan kaldırır:

class ÇalarSaat : Saat {
    int alarmSaati;
    int alarmDakikası;

    void alarmıKur(int saat, int dakika) {
        alarmSaati = saat;
        alarmDakikası = dakika;
    }
}

ÇalarSaat'in Saat'ten türetildiği bu tanım öncekinin eşdeğeridir. Bu tanımdaki sarı ile işaretli bölüm, bir önceki tanımdaki sarı ile işaretli bölüm yerine geçer.

ÇalarSaat, Saat'in bütün üye değişkenlerini ve işlevlerini kalıtım yoluyla edindiği için bir Saat gibi de kullanılabilir:

    auto başucuSaati = new ÇalarSaat;
    başucuSaati.ayarla(20, 30);
    başucuSaati.alarmıKur(7, 0);

Yeni türün Saat'ten kalıtım yoluyla edindiği üyeleri de kendi üyeleri haline gelir, ve istendiğinde dışarıdan erişilebilir:

    writefln("%02s:%02s:%02s ♫%02s:%02s",
             başucuSaati.saat,
             başucuSaati.dakika,
             başucuSaati.saniye,
             başucuSaati.alarmSaati,
             başucuSaati.alarmDakikası);

Yukarıdaki kodun çıktısı:

20:30:00 ♫07:00

Not: Onun yerine biraz aşağıda gösterilecek olan ÇalarSaat.toString işlevini kullanmak çok daha doğru olur.

Bu örnekte görüldüğü gibi, üye veya üye işlev edinmek amacıyla yapılan türemeye gerçekleştirme türemesi denir.

Saat'ten kalıtım yoluyla edinilen üyeler de ÇalarSaat'in parçaları haline gelirler. Her ÇalarSaat nesnesinin artık hem kendi tanımladığı alarmla ilgili üyeleri, hem de kalıtımla edindiği saatle ilgili üyeleri vardır.

Belleği bu sefer aşağıya doğru ilerleyen bir şerit olarak hayal edersek, ÇalarSaat nesnelerinin bellekte aşağıdakine benzer biçimde durduklarını düşünebiliriz:

                       │       .       │
                       │       .       │
    nesnenin adresi →  ├───────────────┤
                       │(başka veriler)│
                       │ saat          │
                       │ dakika        │
                       │ saniye        │
                       │ alarmSaati    │
                       │ alarmDakikası │
                       ├───────────────┤
                       │       .       │
                       │       .       │

Yukarıdaki şekli yalnızca bir fikir vermesi için gösteriyorum. Üyelerin bellekte tam olarak nasıl durdukları derleyicinin kodu derlerken aldığı kararlara bağlıdır. Örneğin, başka veriler diye işaretlenmiş olan bölümde o türün sanal işlev tablosunu gösteren bir gösterge bulunur. (Nesnelerin belleğe tam olarak nasıl yerleştirildikleri bu kitabın kapsamı dışındadır.)

Uyarı: "o türden" ise türetin

Gerçekleştirme türemesinin üye edinme ile ilgili olduğunu gördük. Bu amaçla türetmeyi ancak türler arasında "bu özel tür, o genel türdendir" gibi bir ilişki (is-a) kurabiliyorsanız düşünün. Yukarıdaki örnek için böyle bir ilişkinin var olduğunu söyleyebiliriz, çünkü "çalar saat bir saattir."

Bazı türler arasında ise böyle bir ilişki yoktur. Çoğu durumda türler arasında bir içerme ilişkisi (has-a) vardır. Örneğin Saat sınıfına Pil de eklemek istediğimizi düşünelim. Pil üyesini türeme yoluyla edinmek uygun olmaz, çünkü "saat bir pildir" ifadesi doğru değildir:

class Saat : Pil {    // ← YANLIŞ TASARIM
    // ...
}

Bunun nedeni saatin bir pil olmaması ama bir pil içermesidir. Türler arasında böyle bir içerme ilişkisi bulunduğunda doğru olan içeren türün diğerini üye olarak tanımlamasıdır:

class Saat {
    Pil pil;          // ← Doğru tasarım
    // ...
}
En fazla bir class'tan türetilebilir

Sınıflar birden çok class'tan türetilemezler.

Örneğin "çalar saat sesli bir alettir" ilişkisini gerçekleştirmek için ÇalarSaat'i bir de SesliAlet sınıfından türetmek istesek, derleme hatası ile karşılaşırız:

class SesliAlet {
    // ...
}

class ÇalarSaat : Saat, SesliAlet {    // ← derleme HATASI
    // ...
}

interface'lerden ise istenildiği kadar sayıda türetilebilir. Bunu da daha sonra göreceğiz.

Öte yandan, sınıfların ne kadar derinlemesine türetildiklerinin bir sınırı yoktur:

class Çalgı {
    // ...
}

class TelliÇalgı : Çalgı {
    // ...
}

class Kemençe : TelliÇalgı {
    // ...
}

Yukarıdaki kodda Kemençe TelliÇalgı'dan, TelliÇalgı da Çalgı'dan türetilmiştir. Bu tanımda Kemençe, TelliÇalgı ve Çalgı özelden genele doğru bir sıradüzen oluştururlar.

Sıradüzenin gösterimi

Aralarında türeme ilişkisi bulunan türlerin hepsine birden sıradüzen ismi verilir.

Nesne yönelimli programlamada sıradüzenin geleneksel bir gösterimi vardır: üst sınıflar yukarıda ve alt sınıflar aşağıda olacak şekilde gösterilirler. Sınıflar arasındaki türeme ilişkisi de alt sınıftan üst sınıfa doğru bir okla belirtilir.

Örneğin yukarıdaki sınıf ilişkisini de içeren bir sıradüzen şöyle gösterilir:

             Çalgı
             ↗   ↖
    TelliÇalgı   NefesliÇalgı
      ↗    ↖        ↗    ↖
  Kemençe  Saz   Kaval   Ney
Üst sınıf üyelerine erişmek için super anahtar sözcüğü

Alt sınıf içinden üst sınıfın üyelerine erişilmek istendiğinde, üst sınıfı temsil etmek için super anahtar sözcüğü kullanılır.

Örneğin ÇalarSaat sınıfının üye işlevlerinin içindeyken, Saat'ten edindiği bir üyeye super.dakika diye erişilebilir:

class ÇalarSaat : Saat {
    // ...

    void birÜyeİşlev() {
        super.dakika = 10; // Saat'ten edindiği dakika değişir
        dakika = 10;       // ... aynı şey
    }
}

Yukarıdaki koddan da anlaşıldığı gibi, super anahtar sözcüğü her zaman gerekli değildir çünkü bu durumda yalnızca dakika yazıldığında da üst sınıftaki dakika anlaşılır. super'in bu kullanımı, hem üst sınıfta hem de alt sınıfta aynı isimde üyeler bulunduğu durumlardaki karışıklıkları gidermek için yararlıdır. Bunu biraz aşağıdaki super.sıfırla() ve super.toString() kullanımlarında göreceğiz.

Sıradüzendeki iki sınıfın aynı isimde üyeleri varsa isim karışıklıkları üyelerin tam isimleri belirtilerek giderilir:

class Alet {
    string üretici;
}

class Saat : Alet {
    string üretici;
}

class ÇalarSaat : Saat {
    // ...

    void foo() {
        Alet.üretici = "Öz Saatçilik";
        Saat.üretici = "En Öz Saatçilik";
    }
}
Üst sınıf üyelerini kurmak için super anahtar sözcüğü

super anahtar sözcüğü, üst sınıfın kurucusu anlamına da gelir. Alt sınıfın kurucusundan üst sınıfın kurucusunu çağırmak için kullanılır. Bu kullanımda; this nasıl bu sınıfın kurucusu ise, super de üst sınıfın kurucusudur.

Üst sınıfın kurucusunun açıkça çağrılması gerekmez. Eğer alt sınıfın kurucusu üst sınıfın herhangi bir kurucusunu açıkça çağırıyorsa, üst sınıfın kurucusu çağrıldığı noktada işletilir. Öte yandan (ve eğer üst sınıfın varsayılan kurucusu varsa), üst sınıfın varsayılan kurucusu henüz alt sınıf kurucusuna girilmeden otomatik olarak işletilir.

Yukarıdaki Saat ve ÇalarSaat sınıflarının kurucularını tanımlamamıştık. Bu yüzden her ikisinin üyeleri de kendi .init değerleri ile ilklenirler. Hatırlarsanız, o değer int için sıfırdır.

Saat'in aşağıdaki gibi bir kurucusu olduğunu varsayalım:

class Saat {
    this(int saat, int dakika, int saniye) {
        this.saat = saat;
        this.dakika = dakika;
        this.saniye = saniye;
    }

    // ...
}

Kullanıcıların Saat nesnelerini bu kurucu ile kurmaları gerektiğini biliyoruz:

    auto saat = new Saat(17, 15, 0);

Bir Saat nesnesinin öyle tek başına kurulması doğaldır.

Ancak, kullanıcıların bir ÇalarSaat kurdukları durumda, onun türemeyle edindiği Saat parçasını açıkça kurmaları olanaksızdır. Hatta kullanıcılar bazı durumlarda ÇalarSaat'in bir Saat'ten türediğini bile bilmek zorunda değillerdir.

Kullanıcının tek amacı, yalnızca alt sınıftan bir nesne kurmak ve onu kullanmak olabilir:

    auto başucuSaati = new ÇalarSaat(/* ... */);
    // ... bir ÇalarSaat olarak kullan ...

Bu yüzden, kalıtımla edindiği üst sınıf parçasını kurmak alt sınıfın görevidir. Üst sınıfın kurucusu super ismiyle çağrılır:

class ÇalarSaat : Saat {
    this(int saat, int dakika, int saniye,    // Saat için
         int alarmSaati, int alarmDakikası) { // ÇalarSaat için
        super(saat, dakika, saniye);
        this.alarmSaati = alarmSaati;
        this.alarmDakikası = alarmDakikası;
    }

    // ...
}

ÇalarSaat'in kurucusu, hem kendisi için gereken alarmla ilgili bilgileri hem de üst sınıf için gereken saat bilgilerini parametre olarak almakta ve Saat'in üyelerini super'i çağırarak kurmaktadır.

Üye işlevleri override ile özel olarak tanımlamak

Türemenin önemli bir yararı, üst sınıfta bulunan işlevlerin alt sınıf tarafından özel olarak yeniden tanımlanabilmesidir. override, bu kullanımda "hükümsüz kılmak, bastırmak" anlamına gelir. Alt sınıf, üst sınıfın işlevini kendisine uygun olacak şekilde yeniden tanımlayabilir.

Alt sınıfta yeniden tanımlanabilen işlevlere sanal işlev denir. Derleyiciler sanal işlevleri sanal işlev gösterge tabloları (virtual function pointer table (vtbl)) ve vtbl göstergeleri ile gerçekleştirirler. Bu konu bu kitabın kapsamı dışında olsa da, sanal işlev çağrılarının normal işlev çağrılarından biraz daha yavaş olduklarını bilmeniz gerekir. D'de bütün sınıf işlevleri sanal varsayılırlar. O yüzden, üst sınıfın işlevinin yeniden tanımlanmasının gerekmediği bir durumda o işlevin sanal olmaması için final olarak işaretlenmesi uygun olur. final anahtar sözcüğünü daha sonra Arayüzler bölümünde göreceğiz.

Saat'in sıfırla isminde bir üye işlevi olduğunu düşünelim. Bu işlev bütün üyelerin değerlerini sıfırlıyor olsun:

class Saat {
    void sıfırla() {
        saat = 0;
        dakika = 0;
        saniye = 0;
    }

    // ...
}

Hatırlayacağınız gibi, aynı işlev kalıtım yoluyla ÇalarSaat tarafından da edinilir ve onun nesneleri ile de kullanılabilir:

    auto başucuSaati = new ÇalarSaat(20, 30, 0, 7, 0);
    // ...
    başucuSaati.sıfırla();

Ancak, Saat'in bu sıfırla işlevinin alarmla ilgili üyelerden haberi yoktur; o, yalnızca kendi sınıfının üyeleri ile ilgili olabilir. Bu yüzden, alt sınıfın üyelerinin de sıfırlanabilmeleri için; üst sınıftaki sıfırla işlevinin bastırılması, ve alt sınıfta yeniden tanımlanması gerekir:

class ÇalarSaat : Saat {
    override void sıfırla() {
        super.sıfırla();
        alarmSaati = 0;
        alarmDakikası = 0;
    }

    // ...
}

Alt sınıfın yalnızca kendi üyelerini sıfırladığına, ve üst sınıfın işini super.sıfırla() çağrısı yoluyla üst sınıfa havale ettiğine dikkat edin.

Yukarıdaki kodda super.sıfırla() yerine yalnızca sıfırla() yazamadığımıza da ayrıca dikkat edin. Eğer yazsaydık, ÇalarSaat sınıfı içinde bulunduğumuz için öncelikle onun işlevi anlaşılırdı, ve içinde bulunduğumuz bu işlev tekrar tekrar kendisini çağırırdı. Sonuçta da program sonsuz döngüye girer, bir bellek sorunu yaşar, ve çökerek sonlanırdı.

toString'in tanımını bu noktaya kadar geciktirmemin nedeni, her sınıfın bir sonraki bölümde anlatacağım Object isminde bir sınıftan otomatik olarak türemiş olması ve Object'in zaten bir toString işlevi tanımlamış olmasıdır.

Bu yüzden, bir sınıfın toString işlevinin tanımlanabilmesi için override anahtar sözcüğünün de kullanılması gerekir:

import std.string;

class Saat {
    override string toString() const {
        return format("%02s:%02s:%02s", saat, dakika, saniye);
    }

    // ...
}

class ÇalarSaat : Saat {
    override string toString() const {
        return format("%s ♫%02s:%02s", super.toString(),
                      alarmSaati, alarmDakikası);
    }

    // ...
}

ÇalarSaat'in işlevinin Saat'in işlevini super.toString() diye çağırdığına dikkat edin.

Artık ÇalarSaat nesnelerini de dizgi olarak ifade edebiliriz:

void main() {
    auto masaSaatim = new ÇalarSaat(10, 15, 0, 6, 45);
    writeln(masaSaatim);
}

Çıktısı:

10:15:00 ♫06:45
Alt sınıf nesnesi, üst sınıf nesnesi yerine geçebilir

Üst sınıf daha genel, ve alt sınıf daha özel olduğu için; alt sınıf nesneleri üst sınıf nesneleri yerine geçebilirler. Buna çok şekillilik denir.

Bu genellik ve özellik ilişkisini "bu tür o türdendir" gibi ifadelerde görebiliriz: "çalar saat bir saattir", "öğrenci bir insandır", "kedi bir omurgalı hayvandır", vs. Bu ifadelere uygun olarak; saatin gerektiği yerde çalar saat, insanın gerektiği yerde öğrenci, omurgalı hayvanın gerektiği yerde de kedi kullanılabilir.

Üst sınıfın yerine kullanılan alt sınıf nesneleri kendi türlerini kaybetmezler. Nasıl normal hayatta bir çalar saatin bir saat olarak kullanılması onun aslında bir çalar saat olduğu gerçeğini değiştirmiyorsa, türemede de değiştirmez. Alt sınıf kendisi gibi davranmaya devam eder.

Elimizde Saat nesneleri ile çalışabilen bir işlev olsun. Bu işlev kendi işlemleri sırasında bu verilen saati de sıfırlıyor olsun:

void kullan(Saat saat) {
    // ...
    saat.sıfırla();
    // ...
}

Çok şekilliliğin yararı böyle durumlarda ortaya çıkar. Yukarıdaki işlev Saat türünden bir parametre beklediği halde, onu bir ÇalarSaat nesnesi ile de çağırabiliriz:

    auto masaSaatim = new ÇalarSaat(10, 15, 0, 6, 45);
    writeln("önce : ", masaSaatim);
    kullan(masaSaatim);
    writeln("sonra: ", masaSaatim);

kullan işlevi, masaSaatim nesnesini bir ÇalarSaat olmasına rağmen kabul eder, ve bir Saat gibi kullanır. Bu, aralarındaki türemenin "çalar saat bir saattir" ilişkisini oluşturmuş olmasındandır. Sonuçta, masaSaatim nesnesi sıfırlanmıştır:

önce : 10:15:00 ♫06:45
sonra: 00:00:00 ♫00:00

Burada dikkatinizi çekmek istediğim önemli nokta, yalnızca saat bilgilerinin değil, alarm bilgilerinin de sıfırlanmış olmasıdır.

kullan işlevinde bir Saat'in sıfırla işlevinin çağrılıyor olmasına karşın; asıl nesne bir ÇalarSaat olduğu için, o türün özel sıfırla işlevi çağrılır ve onun tanımı gereği hem saatle ilgili üyeleri, hem de alarmla ilgili üyeleri sıfırlanır.

masaSaatim'in kullan işlevine bir Saat'miş gibi gönderilebilmesi, onun asıl türünde bir değişiklik yapmaz. Görüldüğü gibi, kullan işlevi bir Saat nesnesi kullandığını düşündüğü halde, elindeki nesnenin asıl türüne uygun olan sıfırla işlevi çağrılmıştır.

Saat sıradüzenine bir sınıf daha ekleyelim. Sıfırlanmaya çalışıldığında üyelerinin rasgele değerler aldığı BozukSaat sınıfı:

import std.random;

class BozukSaat : Saat {
    this() {
        super(0, 0, 0);
    }

    override void sıfırla() {
        saat = uniform(0, 24);
        dakika = uniform(0, 60);
        saniye = uniform(0, 60);
    }
}

O türün parametre kullanmadan kurulabilmesi için parametresiz bir kurucu işlev tanımladığına da dikkat edin. O kurucunun tek yaptığı, kendi sorumluğunda bulunan üst sınıfını kurmaktır.

kullan işlevine bu türden bir nesne gönderdiğimiz durumda da bu türün özel sıfırla işlevi çağrılır. Çünkü bu durumda da kullan içindeki saat parametresinin asıl türü BozukSaat'tir:

    auto raftakiSaat = new BozukSaat;
    kullan(raftakiSaat);
    writeln(raftakiSaat);

BozukSaat'in kullan içinde sıfırlanması sonucunda oluşan rasgele saat değerleri:

22:46:37
Türeme geçişlidir

Sınıfların birbirlerinin yerine geçmeleri yalnızca türeyen iki sınıfla sınırlı değildir. Alt sınıflar, üst sınıflarının türedikleri sınıfların da yerine geçerler.

Yukarıdaki Çalgı sıradüzenini hatırlayalım:

class Çalgı {
    // ...
}

class TelliÇalgı : Çalgı {
    // ...
}

class Kemençe : TelliÇalgı {
    // ...
}

Oradaki türemeler şu iki ifadeyi gerçekleştirirler: "telli çalgı bir çalgıdır" ve "kemençe bir telli çalgıdır". Dolayısıyla, "kemençe bir çalgıdır" da doğru bir ifadedir. Bu yüzden, Çalgı beklenen yerde Kemençe de kullanılabilir.

Gerekli türlerin ve üye işlevlerin tanımlanmış olduklarını varsayarsak:

void güzelÇal(Çalgı çalgı, Parça parça) {
    çalgı.akortEt();
    çalgı.çal(parça);
}

// ...

    auto kemençem = new Kemençe;
    güzelÇal(kemençem, doğaçlama);

güzelÇal işlevi bir Çalgı beklediği halde, onu bir Kemençe ile çağırabiliyoruz; çünkü geçişli olarak "kemençe bir çalgıdır".

Türeme yalnızca iki sınıfla sınırlı değildir. Eldeki probleme bağlı olarak, ve her sınıfın tek bir class'tan türeyebileceği kuralına uyulduğu sürece, sıradüzen gerektiği kadar kapsamlı olabilir.

Soyut üye işlevler ve soyut sınıflar

Bazen bir sınıfta bulunmasının doğal olduğu, ama o sınıfın kendisinin tanımlayamadığı işlevlerle karşılaşılabilir. Somut bir gerçekleştirmesi bulunmayan bu işleve bu sınıfın bir soyut işlevi denir. En az bir soyut işlevi bulunan sınıflara da soyut sınıf ismi verilir.

Örneğin satranç taşlarını ifade eden bir sıradüzende SatrançTaşı sınıfının taşın hamlesinin yasal olup olmadığını sorgulamaya yarayan yasal_mı isminde bir işlevi olduğunu varsayalım. Böyle bir sıradüzende bu üst sınıf, taşın hangi karelere ilerletilebileceğini bilemiyor olabilir; her taşın hareketi, onunla ilgili olan alt sınıf tarafından biliniyordur: piyonun hareketini Piyon sınıfı biliyordur, şahın hareketini Şah sınıfı, vs.

abstract anahtar sözcüğü, o üye işlevin bu sınıfta gerçekleştirilmediğini, ve alt sınıflardan birisinde gerçekleştirilmesinin şart olduğunu bildirir:

class SatrançTaşı {
    abstract bool yasal_mı(in Kare nereden, in Kare nereye);
}

Görüldüğü gibi; o işlev o sınıfta tanımlanmamış, yalnızca abstract olarak bildirilmiştir.

Soyut sınıf türlerinin nesneleri oluşturulamaz:

    auto taş = new SatrançTaşı;       // ← derleme HATASI

Bunun nedeni, eksik işlevi yüzünden bu sınıfın kullanılamaz durumda bulunmasıdır. Çünkü; eğer oluşturulabilse, taş.yasal_mı(buKare, şuKare) gibi bir çağrının sonucunda ne yapılacağı bu sınıf tarafından bilinemez.

Öte yandan, bu işlevin tanımını veren alt sınıfların nesneleri oluşturulabilir; çünkü alt sınıf bu işlevi kendisine göre tanımlamıştır ve işlev çağrısı sonucunda ne yapılacağı böylece bilinir:

class Piyon : SatrançTaşı {
    override bool yasal_mı(in Kare nereden, in Kare nereye) {
        // ... işlevin piyon tarafından gerçekleştirilmesi ...
        return karar;
    }
}

Bu işlevin tanımını da sunduğu için bu alt sınıftan nesneler oluşturulabilir:

    auto taş = new Piyon;             // derlenir

Soyut işlevlerin de tanımları bulunabilir. (Alt sınıf yine de kendi tanımını vermek zorundadır.) Örneğin, SatrançTaşı türünün yasal_mı işlevi genel denetimler içerebilir:

class SatrançTaşı {
    abstract bool yasal_mı(in Kare nereden, in Kare nereye) {
        // 'nereden' karesinin 'nereye' karesinden farklı
        // olduğunu denetliyoruz
        return nereden != nereye;
    }
}

class Piyon : SatrançTaşı{
    override bool yasal_mı(in Kare nereden, in Kare nereye) {
        // Öncelikle hamlenin herhangi bir SatrançTaşı için
        // yasal olduğundan emin oluyoruz
        if (!super.yasal_mı(nereden, nereye)) {
            return false;
        }

        // ... sonra Piyon için özel karar veriyoruz ...

        return karar;
    }
}

SatrançTaşı sınıfı yasal_mı işlevi tanımlanmış olduğu halde yine de sanaldır. Piyon sınıfının ise nesneleri oluşturulabilir.

Örnek

Bir örnek olarak demir yolunda ilerleyen araçlarla ilgili bir sıradüzene bakalım. Bu örnekte sonuçta şu sıradüzeni gerçekleştirmeye çalışacağız:

           DemirYoluAracı
           /     |      \
    Lokomotif   Tren   Vagon { bindir()?, indir()? }
                       /   \
             YolcuVagonu   YükVagonu

Vagon sınıfının soyut olarak bıraktığı işlevleri aynı satırda soru işaretleriyle belirttim.

Burada amacım yalnızca sınıf ve sıradüzen tasarımlarını göstermek olduğu için fazla ayrıntıya girmeyeceğim ve yalnızca gerektiği kadar kod yazacağım. O yüzden aşağıdaki işlevlerde gerçek işler yapmak yerine yalnızca çıkışa mesaj yazdırmakla yetineceğim.

Yukarıdaki tasarımdaki en genel araç olan DemirYoluAracı yalnızca ilerleme işiyle ilgilenecek şekilde tasarlanmış olsun. Genel olarak bir demir yolu aracı olarak kabul edilebilmek için bu tasarımda bundan daha fazlası da gerekmiyor. O sınıfı şöyle tanımlayabiliriz:

class DemirYoluAracı {
    void ilerle(in size_t kilometre) {
        writefln("Araç %s kilometre ilerliyor", kilometre);
    }
}

DemirYoluAracı'ndan türemiş olan bir tür Lokomotif. Bu sınıfın henüz bir özelliği bulunmuyor:

class Lokomotif : DemirYoluAracı {
}

Problemler bölümünde DemirYoluAracı'na soyut sesÇıkart işlevini eklediğimiz zaman Lokomotif türü de sesÇıkart işlevinin tanımını vermek zorunda kalacak.

Benzer biçimde, Vagon da bir DemirYoluAracı'dır. Ancak, eğer vagonların programda yük ve yolcu vagonu olarak ikiye ayrılmaları gerekiyorsa indirme ve bindirme işlemlerinin farklı olarak tanımlanmaları gerekebilir. Kullanım amacına uygun olarak her vagon mal veya yolcu taşır. Bu genel tanıma uygun olarak bu sınıfa iki işlev ekleyelim:

class Vagon : DemirYoluAracı {
    abstract void bindir();
    abstract void indir();
}

Görüldüğü gibi, bu işlevlerin Vagon arayüzünde soyut olarak tanımlanmaları gerekiyor çünkü vagonun indirme ve bindirme işlemleri sırasında tam olarak ne olacağı o vagonun türüne bağlıdır. Bu işlemler Vagon düzeyinde bilinemez. Yolcu vagonlarının indirme işlemi vagon kapılarının açılması ve yolcuların trenden çıkmalarını beklemek kadar basit olabilir. Yük vagonlarında ise yük taşıyan görevlilere ve belki de vinç gibi bazı araçlara gerek duyulabilir. Bu yüzden indir ve bindir işlevlerinin sıradüzenin bu aşamasında soyut olmaları gerekir.

Soyut Vagon sınıfının soyut işlevlerini gerçekleştirmek, ondan türeyen somut iki sınıfın görevidir:

class YolcuVagonu : Vagon {
    override void bindir() {
        writeln("Yolcular biniyor");
    }

    override void indir() {
        writeln("Yolcular iniyor");
    }
}

class YükVagonu : Vagon {
    override void bindir() {
        writeln("Mal yükleniyor");
    }

    override void indir() {
        writeln("Mal boşalıyor");
    }
}

Soyut bir sınıf olması Vagon'un kullanılamayacağı anlamına gelmez. Vagon sınıfının kendisinden nesne oluşturulamasa da Vagon sınıfı bir arayüz olarak kullanılabilir. Yukarıdaki türetmeler "yük vagonu bir vagondur" ve "yolcu vagonu bir vagondur" ilişkilerini gerçekleştirdikleri için bu iki sınıfı Vagon yerine kullanabiliriz. Bunu biraz aşağıda Tren sınıfı içinde göreceğiz.

Treni temsil eden sınıfı bir lokomotif ve bir vagon dizisi içerecek biçimde tanımlayabiliriz:

class Tren : DemirYoluAracı {
    Lokomotif lokomotif;
    Vagon[] vagonlar;

    // ...
}

Burada çok önemli bir konuya tekrar dikkatinizi çekmek istiyorum. Her ne kadar Lokomotif ve Vagon demir yolu araçları olsalar da, trenin onlardan türetilmesi doğru olmaz. Yukarıda değindiğimiz kuralı hatırlayalım: sınıfların türemeleri için, "bu özel tür, o genel türdendir" gibi bir ilişki bulunması gerekir. Oysa tren ne bir lokomotiftir, ne de vagondur. Tren onları içerir. Bu yüzden lokomotif ve vagon kavramlarını trenin üyeleri olarak tanımladık.

Bir trenin her zaman için lokomotifinin olması gerektiğini kabul edersek geçerli bir Lokomotif nesnesini şart koşan bir kurucu tanımlamamız gerekir. Vagonlar seçime bağlı iseler onlar da vagon eklemeye yarayan bir işlevle eklenebilirler:

import std.exception;
// ...

class Tren : DemirYoluAracı {
    // ...

    this(Lokomotif lokomotif) {
        enforce(lokomotif !is null, "Lokomotif null olamaz");
        this.lokomotif = lokomotif;
    }

    void vagonEkle(Vagon[] vagonlar...) {
        this.vagonlar ~= vagonlar;
    }

    // ...
}

Kurucuya benzer biçimde, vagonEkle işlevi de vagon nesnelerinin null olup olmadıklarına bakabilirdi. Bu konuyu gözardı ediyorum.

Trenle ilgili bir durum daha düşünebiliriz. Trenin istasyondan ayrılma ve istasyona gelme işlemlerinin de desteklenmesinin gerektiğini varsayalım:

class Tren : DemirYoluAracı {
    // ...

    void istasyondanAyrıl(string istasyon) {
        foreach (vagon; vagonlar) {
            vagon.bindir();
        }

        writefln("%s garından ayrılıyoruz", istasyon);
    }

    void istasyonaGel(string istasyon) {
        writefln("%s garına geldik", istasyon);

        foreach (vagon; vagonlar) {
            vagon.indir();
        }
    }
}

Bu programda geriye kalan, std.stdio modülünün eklenmesi ve bu sınıfları kullanan bir main işlevinin yazılmasıdır:

import std.stdio;
// ...
void main() {
    auto lokomotif = new Lokomotif;
    auto tren = new Tren(lokomotif);

    tren.vagonEkle(new YolcuVagonu, new YükVagonu);

    tren.istasyondanAyrıl("Ankara");
    tren.ilerle(500);
    tren.istasyonaGel("Haydarpaşa");
}

Programda trene farklı türden iki vagon eklenmektedir: YolcuVagonu ve YükVagonu. Ek olarak, Tren sınıfı programda iki farklı arayüzün sunduğu işlevlerle kullanılmaktadır:

  1. ilerle işlevi çağrıldığında tren nesnesi bir DemirYoluAracı olarak kullanılmaktadır çünkü o işlev DemirYoluAracı düzeyinde bildirilmiştir ve Tren tarafından türeme yoluyla edinilmiştir.
  2. istasyondanAyrıl ve istasyonaGel işlevleri çağrıldığında ise tren nesnesi bir Tren olarak kullanılmaktadır çünkü o işlevler Tren düzeyinde bildirilmişlerdir.

Programın çıktısı indir ve bindir işlevlerinin vagonların türüne bağlı olarak uygulandığını gösteriyor:

Yolcular biniyor    
Mal yükleniyor      
Ankara garından ayrılıyoruz
Araç 500 kilometre ilerliyor
Haydarpaşa garına geldik
Yolcular iniyor     
Mal boşalıyor       
Özet
Problemler
  1. Yukarıdaki sıradüzenin en üst sınıfı olan DemirYoluAracı'nı değiştirelim. Kaç kilometre ilerlediğini bildirmenin yanında her yüz kilometre için bir de ses çıkartsın:
    class DemirYoluAracı {
        void ilerle(in size_t kilometre) {
            writefln("Araç %s kilometre ilerliyor:", kilometre);
    
            foreach (i; 0 .. kilometre / 100) {
                writefln("  %s", sesÇıkart());
            }
        }
    }
    

    Ancak, sesÇıkart işlevi DemirYoluAracı sınıfında tanımlanamasın çünkü her aracın kendi özel sesi olsun:

    • Lokomotif için "çuf çuf"
    • Vagon için "takıtak tukutak"

    Not: Tren.sesÇıkart işlevini şimdilik bir sonraki soruya bırakın.

    Her aracın farklı sesi olduğu için sesÇıkart'ın genel bir tanımı verilemez. O yüzden üst sınıfta soyut olarak bildirilmesi gerekir:

    class DemirYoluAracı {
        // ...
    
        abstract string sesÇıkart();
    }
    

    sesÇıkart işlevini alt sınıflar için gerçekleştirin ve şu main ile deneyin:

    void main() {
        auto vagon1 = new YolcuVagonu;
        vagon1.ilerle(100);
    
        auto vagon2 = new YükVagonu;
        vagon2.ilerle(200);
    
        auto lokomotif = new Lokomotif;
        lokomotif.ilerle(300);
    }
    

    Bu programın aşağıdaki çıktıyı vermesini sağlayın:

    Araç 100 kilometre ilerliyor:
      takıtak tukutak
    Araç 200 kilometre ilerliyor:
      takıtak tukutak
      takıtak tukutak
    Araç 300 kilometre ilerliyor:
      çuf çuf
      çuf çuf
      çuf çuf
    

    Dikkat ederseniz, YolcuVagonu ile YükVagonu aynı sesi çıkartıyorlar. O yüzden onların sesi, ortak üst sınıfları olan Vagon tarafından sağlanabilir.

  2. sesÇıkart işlevini Tren sınıfı için nasıl tanımlayabileceğinizi düşünün.

    Bir fikir: Tren'in sesini, içerdiği lokomotifin ve vagonların seslerinin birleşimi olarak oluşturabilirsiniz.