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
işleç: [operator], bir veya daha fazla ifadeyle iş yapan özel işaret (+, -, =, [], vs.)
Phobos: [Phobos], D dilinin standart kütüphanesi
üye işlev: [member function], yapı veya sınıfın kendi tanımladığı işlemleri
yapı: [struct], başka verileri bir araya getiren veri yapısı
yükleme: [overloading], aynı isimde birden çok işlev tanımlama
... bütün sözlük



İngilizce Kaynaklar


Diğer




Üye İşlevler

Bu bölümde her ne kadar yapıları kullanıyor olsak da buradaki bilgilerin çoğu daha sonra göreceğimiz sınıflar için de geçerlidir.

Bu bölümde yapıların ve sınıfların üye işlevlerini tanıyacağız, ve bunların içerisinden özel olarak, nesneleri string türüne dönüştürmede kullanılan toString üye işlevini göreceğiz.

Bir yapının tanımlandığı çoğu durumda, o yapıyı kullanan bir grup işlev de onunla birlikte tanımlanır. Bunun örneklerini önceki bölümlerde zamanEkle ve bilgiVer işlevlerinde gördük. O işlevler bir anlamda GününSaati yapısı ile birlikte sunulan ve o yapının arayüzünü oluşturan işlevlerdir.

Hatırlarsanız; zamanEkle ve bilgiVer işlevlerinin ilk parametresi, üzerinde işlem yaptıkları nesneyi belirliyordu. Şimdiye kadar tanımladığımız bütün diğer işlevler gibi, onlar da bağımsız olarak, tek başlarına, ve modül kapsamında tanımlanmışlardı.

Bir yapının arayüzünü oluşturan işlevler çok karşılaşılan bir kavram olduğu için; o işlevler yapının içinde, yapının üye işlevleri olarak da tanımlanabilirler.

Üye işlev

Bir yapının veya sınıfın küme parantezlerinin içinde tanımlanan işlevlere üye işlev denir:

struct BirYapı {
    void üye_işlev(/* parametreleri */) {
        // ... işlevin tanımı ...
    }

    // ... yapının üyeleri ve diğer işlevleri ...
}

Üye işlevlere yapının diğer üyelerinde olduğu gibi nesne isminden sonraki nokta karakteri ve ardından yazılan işlev ismi ile erişilir:

    nesne.üye_işlev(parametreleri);

Üye işlevleri aslında daha önce de kullandık; örneğin standart giriş ve çıkış işlemlerinde stdin ve stdout nesnelerini açıkça yazabiliyorduk:

    stdin.readf(" %s", &numara);
    stdout.writeln(numara);

O satırlardaki readf ve writeln üye işlevlerdir.

İlk örneğimiz olarak GününSaati yapısını yazdıran bilgiVer işlevini bir üye işlev olarak tanımlayalım. O işlevi daha önce serbest olarak şöyle tanımlamıştık:

void bilgiVer(in GününSaati zaman) {
    writef("%02s:%02s", zaman.saat, zaman.dakika);
}

Üye işlev olarak yapının içinde tanımlanırken bazı değişiklikler gerekir:

struct GününSaati {
    int saat;
    int dakika;

    void bilgiVer() {
        writef("%02s:%02s", saat, dakika);
    }
}

Daha önce yapı dışında serbest olarak tanımlanmış olan bilgiVer işlevi ile bu üye işlev arasında iki fark vardır:

Bunun nedeni, üye işlevlerin zaten her zaman için bir nesne üzerinde çağrılıyor olmalarıdır:

    auto zaman = GününSaati(10, 30);
    zaman.bilgiVer();

Orada, bilgiVer işlevi zaman nesnesini yazdıracak şekilde çağrılmaktadır. Üye işlevin tanımı içinde noktasız olarak yazılan saat ve dakika, zaman nesnesinin üyeleridir; ve sırasıyla zaman.saat ve zaman.dakika üyelerini temsil ederler.

O üye işlev çağrısı, daha önceden serbest olarak yazılmış olan bilgiVer'in şu şekilde çağrıldığı durumla eşdeğerdir:

    zaman.bilgiVer();    // üye işlev
    bilgiVer(zaman);     // serbest işlev (önceki tanım)

Üye işlev her çağrıldığında, üzerinde çağrıldığı nesnenin üyelerine erişir:

    auto sabah = GününSaati(10, 0);
    auto akşam = GününSaati(22, 0);

    sabah.bilgiVer();
    write('-');
    akşam.bilgiVer();
    writeln();

bilgiVer, sabah üzerinde çağrıldığında sabah'ın değerini, akşam üzerinde çağrıldığında da akşam'ın değerini yazdırır:

10:00-22:00
Nesneyi string olarak ifade eden toString

Bir önceki bölümde bilgiVer işlevinin eksikliklerinden söz etmiştim. Rahatsız edici bir diğer eksikliğini burada göstermek istiyorum: Her ne kadar zamanı okunaklı bir düzende çıktıya gönderiyor olsa da, genel çıktı düzeni açısından '-' karakterini yazdırmayı ve satırın sonlandırılmasını kendimiz ayrıca halletmek zorunda kalıyoruz.

Oysa, nesnelerin diğer türler gibi kullanışlı olabilmeleri için örneğin şu şekilde yazabilmemiz çok yararlı olurdu:

    writefln("%s-%s", sabah, akşam);

Öyle yazabilseydik; daha önceki 4 satırı böyle tek satıra indirgemiş olmanın yanında, nesneleri stdout'tan başka akımlara da, örneğin bir dosyaya da aynı şekilde yazdırabilirdik:

    auto dosya = File("zaman_bilgisi", "w");
    dosya.writefln("%s-%s", sabah, akşam);

Yapıların toString ismindeki üye işlevleri özeldir ve nesneleri string türüne dönüştürmek için kullanılır. Bunun doğru olarak çalışabilmesi için, ismi "string'e dönüştür"den gelen bu işlev o nesneyi ifade eden bir string döndürmelidir.

Bu işlevin içeriğini sonraya bırakalım, ve önce yapı içinde nasıl tanımlandığına bakalım:

import std.stdio;

struct GününSaati {
    int saat;
    int dakika;

    string toString() {
        return "deneme";
    }
}

void main() {
    auto sabah = GününSaati(10, 0);
    auto akşam = GününSaati(22, 0);

    writefln("%s-%s", sabah, akşam);
}

Nesneleri dizgi olarak kullanabilen kütüphane işlevleri onların toString işlevlerini çağırırlar ve döndürülen dizgiyi kendi amaçlarına uygun biçimde kullanırlar.

Bu örnekte henüz anlamlı bir dizgi üretmediğimiz için çıktı da şimdilik anlamsız oluyor:

deneme-deneme

Ayrıca bilgiVer'i artık emekliye ayırmakta olduğumuza da dikkat edin; toString'in tanımını tamamlayınca ona ihtiyacımız kalmayacak.

toString işlevini yazmanın en kolay yolu, std.string modülünde tanımlanmış olan format işlevini kullanmaktır. Bu işlev, çıktı düzeni için kullandığımız bütün olanaklara sahiptir ve örneğin writef ile aynı şekilde çalışır. Tek farkı, ürettiği sonucu bir akıma göndermek yerine, bir string olarak döndürmesidir.

toString'in de zaten bir string döndürmesi gerektiği için, format'ın döndürdüğü değeri olduğu gibi döndürebilir:

import std.string;
// ...
struct GününSaati {
// ...
    string toString() {
        return format("%02s:%02s", saat, dakika);
    }
}

toString'in yalnızca bu nesneyi string'e dönüştürdüğüne dikkat edin. Çıktının geri kalanı, writefln çağrısı tarafından halledilmektedir. writefln, "%s" düzen bilgilerine karşılık olarak toString'i otomatik olarak iki nesne için ayrı ayrı çağırır, aralarına '-' karakterini yerleştirir, ve en sonunda da satırı sonlandırır:

10:00-22:00

Görüldüğü gibi, burada anlatılan toString işlevi parametre almamaktadır. toString'in parametre olarak delegate alan bir tanımı daha vardır. O tanımını daha ilerideki İşlev Göstergeleri, İsimsiz İşlevler, ve Temsilciler bölümünde göreceğiz.

Örnek: ekle üye işlevi

Bu sefer de GününSaati nesnelerine zaman ekleyen bir üye işlev tanımlayalım.

Ama ona geçmeden önce, önceki bölümlerde yaptığımız bir yanlışı gidermek istiyorum. Yapılar bölümünde tanımladığımız zamanEkle işlevinin, GününSaati nesnelerini toplamasının normal bir işlem olmadığını görmüş, ama yine de o şekilde kullanmıştık:

GününSaati zamanEkle(in GününSaati başlangıç,
                     in GününSaati eklenecek) {    // anlamsız
    // ...
}

Gün içindeki iki zamanı birbirine eklemek doğal bir işlem değildir. Örneğin yola çıkma zamanına sinemaya varma zamanını ekleyemeyiz. Gün içindeki bir zamana eklenmesi normal olan, bir süredir. Örneğin yola çıkma zamanına yol süresini ekleyerek sinemaya varış zamanını buluruz.

Öte yandan, gün içindeki iki zamanın birbirlerinden çıkartılmaları normal bir işlem olarak görülebilir. O işlemin sonucu da örneğin Süre türünden olmalıdır.

Bu bakış açısı ile, dakika duyarlığıyla çalışan bir Süre yapısını ve onu kullanan zamanEkle işlevini şöyle yazabiliriz:

struct Süre {
    int dakika;
}

GününSaati zamanEkle(in GününSaati başlangıç, in Süre süre) {
    // başlangıç'ın kopyasıyla başlıyoruz
    GününSaati sonuç = başlangıç;

    // Süreyi ekliyoruz
    sonuç.dakika += süre.dakika;

    // Taşmaları ayarlıyoruz
    sonuç.saat += sonuç.dakika / 60;
    sonuç.dakika %= 60;
    sonuç.saat %= 24;

    return sonuç;
}

unittest {
    // Basit bir test
    assert(zamanEkle(GününSaati(10, 30), Süre(10))
           == GününSaati(10, 40));

    // Gece yarısı testi
    assert(zamanEkle(GününSaati(23, 9), Süre(51))
           == GününSaati(0, 0));

    // Sonraki güne taşma testi
    assert(zamanEkle(GününSaati(17, 45), Süre(8 * 60))
           == GününSaati(1, 45));
}

Şimdi aynı işlevi bir üye işlev olarak tanımlayalım. Üye işlev zaten bir nesne üzerinde çalışacağı için GününSaati parametresine gerek kalmaz ve parametre olarak yalnızca süreyi geçirmek yeter:

struct Süre {
    int dakika;
}

struct GününSaati {
    int saat;
    int dakika;

    string toString() {
        return format("%02s:%02s", saat, dakika);
    }

    void ekle(in Süre süre) {
        dakika += süre.dakika;

        saat += dakika / 60;
        dakika %= 60;
        saat %= 24;
    }

    unittest {
        auto zaman = GününSaati(10, 30);

        // Basit bir test
        zaman.ekle(Süre(10));
        assert(zaman == GününSaati(10, 40));

        // 15 saat sonra bir sonraki güne taşmalı
        zaman.ekle(Süre(15 * 60));
        assert(zaman == GününSaati(1, 40));

        // 22 saat ve 20 dakika sonra gece yarısı olmalı
        zaman.ekle(Süre(22 * 60 + 20));
        assert(zaman == GününSaati(0, 0));
    }
}

ekle, nesnenin zamanını belirtilen süre kadar ilerletir. Daha sonraki bölümlerde göreceğimiz işleç yükleme olanağı sayesinde bu konuda biraz daha kolaylık kazanacağız. Örneğin += işlecini yükleyerek yapı nesnelerini de temel türler gibi kullanabileceğiz:

    zaman += Süre(10);      // bunu daha sonra öğreneceğiz

Ayrıca gördüğünüz gibi, üye işlevler için de unittest blokları yazılabilir. O blokların yapı tanımını kalabalıklaştırdığını düşünüyorsanız, bloğu bütünüyle yapının dışında da tanımlayabilirsiniz:

struct GününSaati {
    // ... yapı tanımı ...
}

unittest {
    // ... yapı testleri ...
}

Bunun nedeni, unittest bloklarının aslında belirli bir noktada tanımlanmalarının gerekmemesidir. Denetledikleri kodlarla bir arada bulunmaları daha doğal olsa da, onları uygun bulduğunuz başka yerlerde de tanımlayabilirsiniz.

Problemler
  1. GününSaati yapısına nesnelerin değerini Süre kadar azaltan bir üye işlev ekleyin. ekle işlevinde olduğu gibi, süre azaltıldığında bir önceki güne taşsın. Örneğin 00:05'ten 10 dakika azaltınca 23:55 olsun.

    Başka bir deyişle, azalt işlevini şu birim testlerini geçecek biçimde gerçekleştirin:

    struct GününSaati {
        // ...
    
        void azalt(in Süre süre) {
            // ... burasını siz yazın ...
        }
    
        unittest {
            auto zaman = GününSaati(10, 30);
    
            // Basit bir test
            zaman.azalt(Süre(12));
            assert(zaman == GününSaati(10, 18));
    
            // 3 gün ve 11 saat önce
            zaman.azalt(Süre(3 * 24 * 60 + 11 * 60));
            assert(zaman == GününSaati(23, 18));
    
            // 23 saat ve 18 dakika önce gece yarısı olmalı
            zaman.azalt(Süre(23 * 60 + 18));
            assert(zaman == GününSaati(0, 0));
    
            // 1 dakika öncesi
            zaman.azalt(Süre(1));
            assert(zaman == GününSaati(23, 59));
        }
    }
    
  2. Daha önce İşlev Yükleme bölümünün çözümünde kullanılan diğer bütün bilgiVer işlevlerinin yerine Toplantı, Yemek, ve GünlükPlan yapıları için toString üye işlevlerini tanımlayın.

    Çok daha kullanışlı olmalarının yanında, her birisinin tek satırda yazılabildiğini göreceksiniz.