D.ershane D Programlama Dili Dersleri

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

Bölümler
İngilizce Kaynaklar
Diğer



Türeme

Türeme, daha genel bir türün daha özel bir alt türü olarak tanımlanmak demektir. 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 derste 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 derse 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);
    dout.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.

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.

İşte 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:

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

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

Yukarıdaki kodun çıktısı:

20:30:00 ♫07:00

Not: Onun yerine de, 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 adı verilir.

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 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 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    ← Yanlış tasarım
{
    // ...
}

Bunun nedeni; saatin bir pil olmaması, ama bir pil içermesidir. Türler arasında böyle bir içerme ilişkisi bulunduğunda; içerilen türün, içeren türün bir üyesi olarak tanımlanması doğru olur:

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.

Sıradüzenin gösterimi

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

Nesneye yönelik 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

Bundan sonraki gösterimlerde ok yerine, yeterince anlaşılır olduklarını düşündüğüm için /, \, ve | ASCII karakterlerini kullanacağım.

Ü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. Ama bu şart değildir; çünkü yalnızca dakika yazıldığında da üst sınıftaki dakika anlaşılır:

class ÇalarSaat : Saat
{
    // ...

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

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.

Ü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.

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; ve hatırlarsanız o değer int için sıfırdır.

Saat'in kurucusunu basitçe şöyle tanımlamış olalım:

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

    // ...
}

Artık 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 amacı yalnızca alt sınıfı kurmak ve o tür olarak kullanmaktır:

    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, bu amaçla super yazımıyla ç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.

Eğer üst sınıfın bir otomatik kurucusu varsa, alt sınıfın super'i çağırması şart değildir. Çünkü o durumda üst sınıfın üyeleri varsayılan değerleriyle zaten otomatik olarak ilklenirler.

Ü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.

Saat'in sıfırla isminde bir üye işlevi olduğunu düşünelim; bütün üyelerinin 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 derste 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:

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);
    dout.writefln(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; saat gereken yerde çalar saat, insan gereken yerde öğrenci, omurgalı hayvan gereken 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 şekilliğ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);
    dout.writefln("önce : ", masaSaatim);
    kullan(masaSaatim);
    dout.writefln("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 rastgele 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);
    dout.writefln(raftakiSaat);

BozukSaat'in kullan içinde sıfırlanması sonucunda oluşan rastgele 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 yerine de 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ı.akordEt();
    ç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".

Sıradüzen daha kapsamlı olabilir

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.

Bir örnek olarak, şimdiye kadar bir çok örnekte kullandığımız File sınıfının Phobos kütüphanesinin başka sınıflarıyla bağlı olduğu sıradüzene bakalım:

                InputStream  OutputStream
                (interface)  (interface)
                         \    /
              CFile -->  Stream  <-- SocketStream
                       /  |  |  \
             MemoryStream | File \
                          |      TArrayStream
                    FilterStream
                  /       |      \
                 / BufferedStream \
      EndianStream        |       SliceStream
                    BufferedFile

Doğal olarak, hiçbir programcının bu sıradüzeni ezbere bilmesi gerekmez. Yalnızca bir fikir vermesi açısından gösteriyorum.

Yukarıdaki sıraüzende her sınıfın tek bir class'tan türediğine dikkat edin. Üstünde iki tür görünen tek sınıf Stream'dir, çünkü türetildiği iki tür de interface'tir.

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 bir alt sınıfta 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ıfların 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 vezirPiyonu = new Piyon;           // çalışır
Ö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ı { ilerle() }
           /  |  \    \
      Drezin  | Vagon  Lokomotif
              |
            Tren { lokomotif, vagonlar, indir()?, bindir()? }
            /   \
      YükTreni YolcuTreni { kondüktörler }

Her sınıfın kendisinin tanımladığı üyeleri gri renkle gösterdim. Soyut olan iki işlevi de kırmızı soru işaretiyle belirttim. Aşağıda bu sınıfları ve üyelerini en üstten en alta doğru teker teker göstereceğim.

Burada amacım yalnızca sınıf ve sıradüzen tasarımlarını göstermek olduğu için, hiç 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ış. 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 int kilometre)
    {
        dout.writefln(kilometre, " kilometre ilerliyorum");
    }
}

DemirYoluAracı'ndan türetilebilecek en basit araç, insan gücüyle giden drezin olabilir. Bu sınıfa özel bir üye eklemeye şimdilik gerek duymuyorum:

class Drezin : DemirYoluAracı
{}

Sırada, en yaygın demir yolu aracı olan tren var. Bir trenin lokomotifinin ve bazı vagonlarının olması doğaldır. Önce bu yardımcı sınıfları tanımlayalım:

class Lokomotif : DemirYoluAracı
{}

class Vagon : DemirYoluAracı
{}

Treni temsil eden sınıfı bir lokomotif ve bir dizi vagon olarak şöyle tanımlayabiliriz:

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

    // ...
}

Burada çok önemli bir konuya 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.

Trenle ilgili başka bir durumu daha düşünmemiz gerekiyor: Her trenin indirme ve bindirme işlemlerini desteklemesi gerekir. Kullanım amacı gereği, her tren mal veya yolcu taşır. Bu genel tanıma uygun olarak bu sınıfa iki tane de işlev ekleyelim:

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

    abstract void indir();
    abstract void bindir();
}

Gördüğünüz gibi, bu işlevleri soyut olarak tanımlamak zorunda kaldık. Çünkü trenin indirme ve bindirme işlemleri sırasında tam olarak ne yapılacağı, o trenin türüne bağlıdır. Yolcu trenlerinin indirme işlemi, vagon kapılarının açılması ve yolcuların trenden çıkmalarını beklemek kadar basittir. Yük trenlerinde 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 Tren sınıfının soyut işlevlerini gerçekleştirmek, ondan türeyen somut iki sınıfın görevidir:

class YükTreni : Tren
{
    override void indir()
    {
        dout.writefln("mal boşalıyor");
    }

    override void bindir()
    {
        dout.writefln("mal yükleniyor");
    }
}

Bu iki somut tür arasında başka farklar da olabileceğini göstermek için YolcuTreni'ne kondüktörlerini ifade eden bir de dizi ekleyelim:

class Kondüktör
{}

class YolcuTreni : Tren
{
    Kondüktör[] kondüktörler;

    override void indir()
    {
        dout.writefln("yolcular iniyor");
    }

    override void bindir()
    {
        dout.writefln("yolcular biniyor");
    }
}

Soyut bir sınıf olması, Tren'in kullanılamayacağı anlamına gelmez. Tren sınıfının nesneleri oluşturulamaz, ama Tren sınıfı bir arayüz olarak kullanılabilir. Örneğin parametre olarak Tren alan şu işlev, trenlerin duraktaki işleriyle ilgileniyor olsun:

void duraktaAğırla(Tren tren)
{
    tren.indir();
    tren.bindir();
}

Yukarıdaki türetmeler "yük treni bir trendir" ve "yolcu treni bir trendir" ilişkilerini gerçekleştirdikleri için, bu iki sınıfı Tren yerine kullanabiliyoruz.

Bu programda geriye kalan, dout'u tanımlayan modülün eklenmesi, ve bu sınıfları kullanan bir main işlevinin yazılmasıdır:

import std.cstream;

void main()
{
    auto bizimEkspres = new YolcuTreni;
    bizimEkspres.ilerle(30);
    duraktaAğırla(bizimEkspres);

    auto sebzeTreni = new YükTreni;
    bizimEkspres.ilerle(400);
    duraktaAğırla(sebzeTreni);
}

O kod, farklı türden iki nesne oluşturur: YolcuTreni ve YükTreni. Bu iki sınıf, o kodda iki değişik arayüzle kullanılmaktadır:

  1. ilerle işlevinin çağrıldığı noktalarda, bu nesneler DemirYoluAracı olarak kullanılmaktadırlar; çünkü o işlev o sınıf düzeyinde bildirilmiştir
  2. duraktaAğırla işlevi, nesneleri bir Tren olarak kullanmaktadır; çünkü nesneler işlevin parametresine Tren olarak gönderilirler

Çıktılarını karşılaştıralım:

30 kilometre ilerliyorum
yolcular iniyor
yolcular biniyor
400 kilometre ilerliyorum
mal boşalıyor
mal yükleniyor

ilerle işlevinin her ikisi için de aynı şekilde çalıştığına dikkat edin.

Öte yandan, duraktaAğırla işlevinin çağırdığı indir ve bindir üye işlevleri, her ikisinin kendi asıl türlerinin tanımladığı şekilde çalışmıştır.

Özet
Problemler
  1. Yukarıdaki sıradüzenin en üst sınıfı olan DemirYoluAracı'nı değiştirelim. Kaç kilometre ilerlediğini bildirmek yanında, her kilometre için bir de ses çıkartsın:
  2. class DemirYoluAracı
    {
        void ilerle(in int kilometre)
        {
            dout.writef(kilometre, " kilometre: ");
    
            foreach (i; 0 .. kilometre) {
                dout.writef(ses(), ' ');
            }
    
            dout.writefln();
        }
    }
    

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

    • insan gücüyle çalışan drezin için "of puf"
    • vagon için "takıtak tukutak"
    • lokomotif için "çuf çuf"

    Not: Diğer sınıfları şimdilik bir sonraki soruya bırakın.

    Her aracın ayrı sesinin olabilmesi için, ses işlevinin üst sınıfta soyut olarak bildirilmesi gerekir:

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

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

    void main()
    {
        auto drezin = new Drezin;
        drezin.ilerle(2);
    
        auto vagon = new Vagon;
        vagon.ilerle(3);
    
        auto lokomotif = new Lokomotif;
        lokomotif.ilerle(4);
    }
    

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

    2 kilometre: of puf of puf 
    3 kilometre: takıtak tukutak takıtak tukutak takıtak tukutak 
    4 kilometre: çuf çuf çuf çuf çuf çuf çuf çuf 
    
  3. ses işlevini Tren, YükTreni, ve YolcuTreni sınıfları için nasıl tanımlayabileceğinizi düşünün.
  4. Bir fikir: ses işlevini, yalnızca bunların en üstündeki sınıf olan Tren'de tanımlamak isteyebilirsiniz. Hatta, üç tren türünün sesinin de lokomotiften kaynaklanacağını varsayarsak; bu işlevi, doğrudan lokomotifin sesini döndürecek şekilde yazabilirsiniz:

    class Tren : DemirYoluAracı
    {
        Lokomotif lokomotif;
        Vagon[] vagonlar;
    
        abstract void indir();
        abstract void bindir();
    
        override string ses()
        {
            return lokomotif.ses();
        }
    }
    

    Ne yazık ki, yukarıdaki sınıfı şu şekilde denediğinizde programın çöktüğünü göreceksiniz:

        auto tren = new YolcuTreni;
        tren.ilerle(1);
    
    Segmentation fault       ← Program çöker
    

    Neden? Hatayı giderin ve o kodun beklenen şu çıktıyı vermesini sağlayın:

    1 kilometre: çuf çuf
    
(çözümler sonra gelecek...)