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

atama: [assign], değişkene yeni bir değer vermek
eniyileştirme: [optimization], kodun daha hızlı çalışacak biçimde davranışı bozulmadan değiştirilmesi
işleç: [operator], bir veya daha fazla ifadeyle iş yapan özel işaret (+, -, =, [], vs.)
kısıtlama: [constraint], şablon parametrelerinin uyması gereken koşulların belirlenmesi
kurma: [construct], yapı veya sınıf nesnesini kullanılabilir duruma getirmek
sağ değer: [rvalue], adresi alınamayan değer
sol değer: [lvalue], adresi alınabilen değer
şablon: [template], derleyicinin örneğin 'türden bağımsız programlama' için kod üretme düzeneği
yükleme: [overloading], aynı isimde birden çok işlev tanımlama
... bütün sözlük



İngilizce Kaynaklar


Diğer




İşleç Yükleme

İşleç yükleme olanağını bu bölümde yapılar üzerinde göreceğiz. Burada anlatılanlar daha sonra göreceğimiz sınıflar için de hemen hemen aynen geçerlidir. En belirgin fark, Kurucu ve Diğer Özel İşlevler bölümünde gördüğümüz atama işleci opAssign'ın sınıflar için tanımlanamıyor olmasıdır.

İşleç yükleme çok sayıda kavram içerir (şablonlar, auto ref, vs.). Bu kavramların bazılarını kitabın ilerideki bölümlerinde göreceğimizden bu bölüm size bu aşamada diğer bölümlerden daha zor gelebilir.

İşleç yükleme, işleçlerin kendi türlerimizle nasıl çalışacaklarını belirleme olanağıdır.

Yapıların ve üye işlevlerin yararlarını önceki bölümlerde görmüştük. Bunun bir örneği, GününSaati nesnelerine Süre nesnelerini ekleyebilmekti. Kodu kısa tutmak için yalnızca bu bölümü ilgilendiren üyelerini gösteriyorum:

struct Süre {
    int dakika;
}

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

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

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

void main() {
    auto yemekZamanı = GününSaati(12, 0);
    yemekZamanı.ekle(Süre(10));
}

Üye işlevlerin yararı, yapıyı ilgilendiren işlemlerin yapının içinde tanımlanabilmeleridir. Üye değişkenler ve üye işlevler bir arada tanımlanınca yapının üyelerinin bütün işlevler tarafından doğru olarak kullanıldıkları daha kolay denetlenebilir.

Yapıların bütün bu yararlarına karşın, temel türlerin işleç kullanımı konusunda yapılara karşı üstünlükleri vardır: Temel türler özel tanımlar gerekmeden işleçlerle rahatça kullanılabilirler:

    int ağırlık = 50;
    ağırlık += 10;                     // işleçle

Şimdiye kadar öğrendiğimiz bilgiler doğrultusunda benzer işlemleri yapılar için ancak üye işlevlerle gerçekleştirebiliyoruz:

    auto yemekZamanı = GününSaati(12, 0);
    yemekZamanı.ekle(Süre(10));        // üye işlevle

İşleç yükleme, yapıları da temel türler gibi işleçlerle kullanabilme olanağı sunar. Örneğin, GününSaati yapısı için tanımlayabileceğimiz += işleci, yukarıdaki işlemin yazımını kolaylaştırır ve daha okunaklı hale getirir:

    yemekZamanı += Süre(10);           // yapı için de işleçle

Yüklenebilecek bütün işleçlere geçmeden önce bu kullanımın nasıl sağlandığını göstermek istiyorum. Daha önce ismini ekle olarak yazdığımız işlevi D'nin özel olarak belirlediği opOpAssign(string işleç) ismiyle tanımlamak ve bu tanımın "+" karakteri için yapılmakta olduğunu belirtmek gerekir. Biraz aşağıda açıklanacağı gibi, bu aslında += işleci içindir.

Aşağıdaki tanımın şimdiye kadar gördüğümüz işlev tanımlarına benzemediğini farkedeceksiniz. Bunun nedeni, opOpAssign'ın aslında bir işlev şablonu olmasıdır. Şablonları daha ilerideki bölümlerde göreceğiz; şimdilik işleç yükleme konusunda bu söz dizimini bir kalıp olarak uygulamak gerektiğini kabul etmenizi rica ediyorum:

struct GününSaati {
// ...
    ref GününSaati opOpAssign(string işleç)(Süre süre)  // (1)
            if (işleç == "+") {                         // (2)

        dakika += süre.dakika;
        saat += dakika / 60;
        dakika %= 60;
        saat %= 24;

        return this;
    }
}

Yukarıdaki şablon tanımı iki parçadan oluşur:

  1. opOpAssign(string işleç): Bunun aynen yazılması gerekir. (opOpAssign'dan başka işleç işlevlerinin de bulunduğunu aşağıda göreceğiz.)
  2. if (işleç == "+"): opOpAssign birden fazla işlecin tanımında kullanılabildiği için hangi işleç karakterinin tanımlanmakta olduğu bu söz dizimiyle belirtilir. Aslında bir şablon kısıtlaması olan bu söz diziminin ayrıntılarını da daha sonraki bir bölümde göreceğiz.

ekle işlevinden farklı olarak, dönüş türünün bu tanımda void olmadığına dikkat edin. İşleçlerin dönüş türlerini biraz aşağıda açıklayacağım.

Derleyici, GününSaati nesnelerinin += işleciyle kullanıldığı yerlerde perde arkasında opOpAssign!"+" işlevini çağırır:

    yemekZamanı += Süre(10);

    // Aşağıdaki satır üsttekinin eşdeğeridir:
    yemekZamanı.opOpAssign!"+"(Süre(10));

opOpAssign'dan sonra yazılan !"+", opOpAssign'ın + karakteri için tanımı olan işlevin çağrılmakta olduğu anlamına gelir. Şablonlarla ilgili olan bu söz dizimini de daha sonraki bir bölümde göreceğiz.

Kod içindeki += işlecine karşılık gelen yukarıdaki üye işlevde "+=" değil, "+" kullanıldığına dikkat edin. opOpAssign'ın isminde geçen ve "değer ata" anlamına gelen "assign" zaten atama kavramını içerir. Sonuçta, opOpAssign!"+", atamalı toplama işlemi olan += işlecinin tanımıdır.

İşleçlerin davranışlarını bizim belirleyebiliyor olmamız bize çoğu işleç için istediğimiz şeyi yapma serbestisi verir. Örneğin, yukarıdaki işleci süre ekleyecek şekilde değil, tam tersine süre azaltacak şekilde de tanımlayabilirdik. Oysa kodu okuyanlar += işlecini gördüklerinde doğal olarak değerin artmasını bekleyeceklerdir.

Genel beklentilere uyulması işleçlerin dönüş türleri için de önemlidir.

İşleçleri doğal davranışları dışında yazdığınızda bunun herkesi yanıltacağını ve programda hatalara neden olacağını aklınızda bulundurun.

Yüklenebilen işleçler

İşleçler kullanım çeşitlerine göre farklı yüklenirler.

Tekli işleçler

Tek nesneyle işleyen işleçlere tekli işleç denir:

    ++ağırlık;

Yukarıdaki ++ işleci tekli işleçtir çünkü işini yaparken tek değişken kullanmaktadır ve onun değerini bir arttırmaktadır.

Bu işleçler opUnary üye işlev ismiyle tanımlanırlar. opUnary parametre almaz çünkü yalnızca işlecin üzerinde uygulandığı nesneyi (yani this'i) etkiler.

İşlev tanımında kullanılması gereken işleç dizgileri şunlardır:

İşleç Anlamı İşleç Dizgisi
-nesne ters işaretlisini üret "-"
+nesne aynı işaretlisini üret "+"
~nesne bit düzeyinde tersini al "~"
*nesne gösterdiğine eriş "*"
++nesne bir arttır "++"
--nesne bir azalt "--"

Örneğin ++ işlecini Süre için şöyle tanımlayabiliriz:

struct Süre {
    int dakika;

    ref Süre opUnary(string işleç)()
            if (işleç == "++") {
        ++dakika;
        return this;
    }
}

İşlecin dönüş türünün ref olarak işaretlendiğine dikkat edin. İşleçlerin dönüş türlerini aşağıda açıklayacağım.

Süre nesneleri bu sayede artık ++ işleci ile arttırılabilirler:

    auto süre = Süre(20);
    ++süre;

Önceki değerli (sonek) arttırma ve önceki değerli (sonek) azaltma işleçleri olan nesne++ ve nesne-- kullanımları yüklenemez. O kullanımlardaki önceki değerleri derleyici otomatik olarak üretir. Örneğin, süre++ kullanımının eşdeğeri şudur:

    /* Önceki değer derleyici tarafından otomatik olarak
     * kopyalanır: */
    Süre __öncekiDeğer__ = süre;

    /* Tanımlanmış olan normal ++ işleci çağrılır: */
    ++süre;

    /* Daha sonra bütün ifadede __öncekiDeğer__ kullanılır. */

Bazı programlama dillerinden farklı olarak, önceki değerin programda kullanılmadığı durumlarda yukarıdaki kopyanın D'de bir masrafı yoktur. İfadenin kullanılmadığı durumlarda önceki değerli arttırma işleçleri normal arttırma işleçleriyle değiştirilirler:

    /* Aşağıdaki ifadenin değeri programda kullanılmamaktadır.
     * İfadenin tek etkisi, 'i'nin değerini arttırmaktır. */
    i++;

i'nin önceki değeri programda kullanılmadığından derleyici o ifadenin yerine aşağıdakini yerleştirir:

    /* Derleyicinin kullandığı ifade: */
    ++i;

Ek olarak, eğer aşağıda göreceğimiz opBinary yüklemesi süre += 1 kullanımını destekliyorsa, ++süre ve süre++ kullanımları için opUnary gerekmez; derleyici onların yerine süre += 1 ifadesinden yararlanır. Benzer biçimde, süre -= 1 yüklemesi de --süre ve süre-- kullanımlarını karşılar.

İkili işleçler

İki nesne kullanan işleçlere ikili işleç denir:

    toplamAğırlık = kutuAğırlığı + çikolataAğırlığı;

Yukarıdaki satırda iki farklı ikili işleç görülüyor: + işleci solundaki ve sağındaki değerleri toplar, = işleci de sağındakinin değerini solundakine atar.

İşleçleri gruplandırmak için aşağıdaki tabloda işleçlerin çeşitlerini de belirttim. "=" ile işaretli olanlar sağ tarafın değerini sol tarafa atarlar.


İşleç

Anlamı

İşlev İsmi
Sağ Taraf için
İşlev İsmi

Çeşit
+ topla opBinary opBinaryRight aritmetik
- çıkar opBinary opBinaryRight aritmetik
* çarp opBinary opBinaryRight aritmetik
/ böl opBinary opBinaryRight aritmetik
% kalanını hesapla opBinary opBinaryRight aritmetik
^^ üssünü al opBinary opBinaryRight aritmetik
& bit işlemi ve opBinary opBinaryRight bit
| bit işlemi veya opBinary opBinaryRight bit
^ bit işlemi ya da opBinary opBinaryRight bit
<< sola kaydır opBinary opBinaryRight bit
>> sağa kaydır opBinary opBinaryRight bit
>>> işaretsiz sağa kaydır opBinary opBinaryRight bit
~ birleştir opBinary opBinaryRight
in içinde mi? opBinary opBinaryRight
== eşittir opEquals - mantıksal
!= eşit değildir opEquals - mantıksal
< öncedir opCmp - sıralama
<= sonra değildir opCmp - sıralama
> sonradır opCmp - sıralama
>= önce değildir opCmp - sıralama
= ata opAssign - =
+= arttır opOpAssign - =
-= azalt opOpAssign - =
*= katını ata opOpAssign - =
/= bölümünü ata opOpAssign - =
%= kalanını ata opOpAssign - =
^^= üssünü ata opOpAssign - =
&= & sonucunu ata opOpAssign - =
|= | sonucunu ata opOpAssign - =
^= ^ sonucunu ata opOpAssign - =
<<= << sonucunu ata opOpAssign - =
>>= >> sonucunu ata opOpAssign - =
>>>= >>> sonucunu ata opOpAssign - =
~= sonuna ekle opOpAssign - =

Tabloda sağ taraf için olarak belirtilen işlev isimleri, nesne işlecin sağ tarafında da kullanılabildiğinde işletilir. Kodda şöyle bir ikili işleç bulunduğunu düşünelim:

    x işleç y

Derleyici hangi üye işlevi işleteceğine karar vermek için yukarıdaki ifadeyi şu iki üye işlev çağrısına dönüştürür:

    x.opBinary!"işleç"(y);       // x'in solda olduğu durumun tanımı
    y.opBinaryRight!"işleç"(x);  // y'nin sağda olduğu durumun tanımı

O işlev çağrılarından daha uygun olanı seçilir ve işletilir.

opBinaryRight, işleçlerin her iki tarafında kullanılması beklenen (örneğin int gibi işleyen) türler tanımlarken kullanışlıdır:

    auto x = ÖzelTamsayı(42);
    x + 1;    // opBinary!"+" çağrılır
    1 + x;    // opBinaryRight!"+" çağrılır

opBinaryRight'ın bir başka yaygın kullanımı in işlecidir çünkü in işleci genellikle sağ tarafındaki nesne ile ilişkilidir. Bunun bir örneğini aşağıda göreceğiz.

Aşağıdaki örneklerde üye işlevleri tanımlarken parametre ismini sağdaki olarak seçtim. Bununla parametrenin işlecin sağındaki nesne olduğunu vurguluyorum:

    x işleç y

O ifade kullanıldığında üye işlevin sağdaki ismindeki parametresi y'yi temsil edecektir.

Dizi ve dilim işleçleri

Aşağıdaki işleçler bir türün topluluk olarak kullanılabilmesini sağlarlar.

Anlamı İşlev İsmi Örnek kullanım
eleman erişimi opIndex topluluk[i]
elemana atama opIndexAssign topluluk[i] = 7
eleman üzerinde tekli işlem opIndexUnary ++topluluk[i]
atamalı eleman işlemi opIndexOpAssign topluluk[i] *= 2
eleman adedi opDollar topluluk[$ - 1]
bütün elemanlara eriştiren dilim opSlice topluluk[]
bazı elemanlara eriştiren dilim opSlice(size_t, size_t) topluluk[i..j]

Bu işleçleri aşağıda kendi başlıkları altında göreceğiz.

Aşağıdaki tablodaki işleçler D'nin önceki sürümlerinden kalma olduklarından kullanımları önerilmez. Onların yerine yukarıdaki tablodaki işleçler kullanılır.

Anlamı İşlev İsmi Örnek kullanım
bütün elemanlar üzerinde tekli işlem opSliceUnary (önerilmez) ++topluluk[]
bazı elemanlar üzerinde tekli işlem opSliceUnary (önerilmez) ++topluluk[i..j]
bütün elemanlara atama opSliceAssign (önerilmez) topluluk[] = 42
bazı elemanlara atama opSliceAssign (önerilmez) topluluk[i..j] = 7
bütün elemanlar üzerinde atamalı işlem opSliceOpAssign (önerilmez) topluluk[] *= 2
bazı elemanlar üzerinde atamalı işlem opSliceOpAssign (önerilmez) topluluk[i..j] *= 2
Diğer işleçler

Yukarıdaki işleçlere ek olarak aşağıdaki işleçler de yüklenebilir:

Anlamı İşlev İsmi Örnek kullanım
işlev çağrısı opCall nesne(42)
tür dönüşümü işleci opCast to!int(nesne)
var olmayan üye işlev için sevk opDispatch nesne.varOlmayanİşlev()

Bu işleçleri daha aşağıda kendi başlıkları altında açıklayacağım.

Birden fazla işleci aynı zamanda tanımlamak

Örnekleri kısa tutmak için yukarıda yalnızca ++, + ve += işleçlerini kullandık. En az bir işlecinin yüklenmesi gereken bir türün başka işleçlerinin de yüklenmelerinin gerekeceği beklenebilir. Örneğin, aşağıdaki Süre türü için -- ve -= işleçleri de tanımlanmaktadır:

struct Süre {
    int dakika;

    ref Süre opUnary(string işleç)()
            if (işleç == "++") {
        ++dakika;
        return this;
    }

    ref Süre opUnary(string işleç)()
            if (işleç == "--") {
        --dakika;
        return this;
    }

    ref Süre opOpAssign(string işleç)(int miktar)
            if (işleç == "+") {
        dakika += miktar;
        return this;
    }

    ref Süre opOpAssign(string işleç)(int miktar)
            if (işleç == "-") {
        dakika -= miktar;
        return this;
    }
}

unittest {
    auto süre = Süre(10);

    ++süre;
    assert(süre.dakika == 11);

    --süre;
    assert(süre.dakika == 10);

    süre += 5;
    assert(süre.dakika == 15);

    süre -= 3;
    assert(süre.dakika == 12);
}

void main() {
}

Yukarıdaki işleç yüklemelerinde kod tekrarı bulunmaktadır. Benzer işlevlerin farklı olan karakterlerini işaretlenmiş olarak gösterdim. Bu kod tekrarı D'nin dizgi katmaları (mixin) olanağı ile giderilebilir. Daha ilerideki bölümlerde daha ayrıntılı olarak öğreneceğimiz mixin anahtar sözcüğünün işleç yüklemedeki yararını burada kısaca görelim.

mixin, kendisine verilen dizgiyi bulunduğu yere kaynak kod olarak yerleştirir. Örneğin, işleç'in "++" olduğu durumda aşağıdaki iki satır eşdeğerdir:

    mixin (işleç ~ "dakika;");
    ++dakika;                     // üsttekinin eşdeğeri

Dolayısıyla, bu olanaktan yararlanan aşağıdaki yapı yukarıdakinin eşdeğeridir:

struct Süre {
    int dakika;

    ref Süre opUnary(string işleç)()
            if ((işleç == "++") || (işleç == "--")) {
        mixin (işleç ~ "dakika;");
        return this;
    }

    ref Süre opOpAssign(string işleç)(int miktar)
            if ((işleç == "+") || (işleç == "-")) {
        mixin ("dakika " ~ işleç ~ "= miktar;");
        return this;
    }
}

Süre nesnelerinin belirli bir miktar ile çarpılmalarının veya bölünmelerinin de desteklenmesi istendiğinde yapılması gereken, o işleç karakterlerini de şablon kısıtlamalarına eklemektir:

struct Süre {
// ...

    ref Süre opOpAssign(string işleç)(int miktar)
            if ((işleç == "+") || (işleç == "-") ||
                (işleç == "*") || (işleç == "/")) {
        mixin ("dakika " ~ işleç ~ "= miktar;");
        return this;
    }
}

unittest {
    auto süre = Süre(12);

    süre *= 4;
    assert(süre.dakika == 48);

    süre /= 2;
    assert(süre.dakika == 24);
}

Şablon kısıtlamaları bu durumda isteğe bağlıdır; özellikle gerekmedikçe yazılmayabilirler:

    ref Süre opOpAssign(string işleç)(int miktar)
            /* kısıtlama yok */ {
        mixin ("dakika " ~ işleç ~ "= miktar;");
        return this;
    }
İşleçlerin dönüş türleri

İşleçleri kendi türleriniz için tanımlarken o işleçlerin hem davranışlarının hem de dönüş türlerinin temel türlerdeki gibi olmalarına dikkat edin. Bu, kodun anlaşılırlığı ve hataların önlenmesi açısından önemlidir.

Temel türlerle kullanılan hiçbir işlecin dönüş türü void değildir. Bu, bazı işleçler için barizdir. Örneğin, iki int değerin a + b biçiminde toplanmalarının sonucunun yine int türünde bir değer olduğu açıktır:

    int a = 1;
    int b = 2;
    int c = a + b;    // c, işlecin değeri ile ilklenir

Başka bazı işleçlerin dönüş değerleri ve türleri ise bariz olmayabilir. Örneğin, ++i gibi bir işlecin bile değeri vardır:

    int i = 1;
    writeln(++i);    // 2 yazar

++ işleci i'yi arttırmakla kalmaz, ayrıca i'nin yeni değerini de döndürür. Üstelik, ++ işlecinin döndürdüğü değer i'nin yeni değeri değil, aslında i'nin ta kendisidir. Bunu ++i işleminin sonucunun adresini yazdırarak gösterebiliriz:

    int i = 1;
    writeln("i'nin adresi                   : ", &i);
    writeln("++i ifadesinin sonucunun adresi: ", &(++i));

Çıktısı, iki adresin aynı olduklarını gösterir:

i'nin adresi                   : 7FFFAFECB2A8
++i ifadesinin sonucunun adresi: 7FFFAFECB2A8

Tanımladığınız işleçlerin dönüş türlerinin aşağıdaki listedekilere uymalarına özen göstermenizi öneririm:

Eşitlik karşılaştırmaları için opEquals

== ve != işleçlerinin davranışını belirler.

opEquals işlecinin dönüş türü bool'dur.

Yapılarda opEquals işlevinin parametresi in olarak işaretlenebilir. Ancak, çok büyük yapılarda hız kaybını önlemek için opEquals, parametresi auto ref const olan bir şablon olarak da tanımlanabilir (boş parantezler bu tanımın bir şablon olmasını sağlarlar):

    bool opEquals()(auto ref const GününSaati sağdaki) const {
        // ...
    }

Sol Değerler ve Sağ Değerler bölümünde gördüğümüz gibi, auto ref sol değerlerin referans olarak, sağ değerlerin ise kopyalanarak geçirilmelerini sağlar. Ancak, D'de sağ değerler kopyalanmak yerine taşındıklarından yukarıdaki işlev bildirimi hem sol değerler hem de sağ değerler için hızlı işler.

Karışıklıklara önlemek için opEquals ve opCmp birbirleriyle tutarlı olmalıdır. opEquals'ın true döndürdüğü iki nesne için opCmp sıfır döndürmelidir.

opEquals üye işlevi == ve != işleçlerinin ikisini de karşılar. Programcı işlevi == işleci için tanımlar; derleyici de != işleci için onun tersini kullanır:

    x == y;
    x.opEquals(y);       // üsttekinin eşdeğeri

    x != y;
    !(x.opEquals(y));    // üsttekinin eşdeğeri

Normalde opEquals işlevini yapılar için tanımlamaya gerek yoktur; derleyici bütün üyelerin eşitliklerini sırayla otomatik olarak karşılaştırır ve nesnelerin eşit olup olmadıklarına öyle karar verir.

Bazen nesnelerin eşitliklerinin özel olarak belirlenmeleri gerekebilir. Örneğin bazı üyeler eşitlik karşılaştırması için önemsiz olabilirler veya bir türün nesnelerinin eşit kabul edilmeleri özel bir kurala bağlı olabilir, vs.

Bunu göstermek için GününSaati yapısı için dakika bilgisini gözardı eden bir opEquals tanımlayalım:

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

    bool opEquals(GününSaati sağdaki) const {
        return saat == sağdaki.saat;
    }
}
// ...
    assert(GününSaati(20, 10) == GününSaati(20, 59));

Eşitlik karşılaştırmasında yalnızca saat bilgisine bakıldığı için 20:10 ve 20:59 zamanları eşit çıkmaktadır. (Not: Bunun karışıklık doğuracağı açıktır; gösterim amacıyla basit bir örnek olarak kabul edelim.)

Sıra karşılaştırmaları için opCmp

Sıralama işleçleri nesnelerin öncelik/sonralık ilişkilerini belirler. Sıralama ile ilgili olan <, <=, >, ve >= işleçlerinin hepsi birden opCmp üye işlevi tarafından karşılanır.

Yapılarda opCmp işlevinin parametresi in olarak işaretlenebilir. Ancak, opEquals'da olduğu gibi, çok büyük yapılarda hız kaybını önlemek için opCmp, parametresi auto ref const olan bir şablon olarak da tanımlanabilir:

    int opCmp()(auto ref const GününSaati sağdaki) const {
        // ...
    }

Karışıklıkları önlemek için opEquals ve opCmp birbirleriyle tutarlı olmalıdır. opEquals'ın true döndürdüğü iki nesne için opCmp sıfır döndürmelidir.

Bu dört işleçten birisinin şu şekilde kullanıldığını düşünelim:

    if (x işleç y) {  // ← işleç <, <=, >, veya >= olabilir

Derleyici o ifadeyi aşağıdaki mantıksal ifadeye dönüştürür ve onun sonucunu kullanır:

    if (x.opCmp(y) işleç 0) {

Örnek olarak,

    if (x <= y) {

ifadesi şuna dönüştürülür:

    if (x.opCmp(y) <= 0) {

Kendi yazdığımız bu işlevin bu kurala göre doğru çalışabilmesi için işlevin şu değerleri döndürmesi gerekir:

Bu sonuçlardan anlaşılacağı gibi, opCmp'ın dönüş türü bool değil, int'tir.

GününSaati nesnelerinin sıralama ilişkilerini öncelikle saat değerine, saatleri eşit olduğunda da dakika değerlerine bakacak şekilde şöyle belirleyebiliriz:

    int opCmp(GününSaati sağdaki) const {
        /* Not: Buradaki çıkarma işlemleri sonucun alttan
         * taşabileceği durumlarda hatalıdır. (Metin içindeki
         * uyarıyı okuyunuz.) */

        return (saat == sağdaki.saat
                ? dakika - sağdaki.dakika
                : saat - sağdaki.saat);
    }

Saat değerleri aynı olduğunda dakika değerlerinin farkı, saatler farklı olduğunda da saatlerin farkı döndürülüyor. Bu tanım, zaman sıralamasında soldaki nesne önce olduğunda eksi bir değer, sağdaki nesne önce olduğunda artı bir değer döndürür.

Uyarı: Yasal değerlerinin taşmaya neden olabildiği türlerde opCmp işlecinin çıkarma işlemi ile gerçekleştirilmesi hatalıdır. Örneğin, aşağıdaki -2 değerine sahip olan nesne int.max değerine sahip olan nesneden daha büyük çıkmaktadır:

struct S {
    int i;

    int opCmp(S rhs) const {
        return i - rhs.i;          // ← HATA
    }
}

void main() {
    assert(S(-2) > S(int.max));    // ← yanlış sonuç
}

Öte yandan, çıkarma işleminin GününSaati yapısında kullanılmasında bir sakınca yoktur çünkü o türün hiçbir üyesinin yasal değerleri çıkarma işleminde taşmaya neden olmaz.

Bütün dizgi türleri ve aralıklar dahil olmak üzere dilimleri karşılaştırırken std.algorithm.cmp işlevinden yararlanabilirsiniz. cmp() iki dilimin sıra değerlerine uygun olarak eksi bir değer, sıfır, veya artı bir değer döndürür. Bu değer doğrudan opCmp işlevinin dönüş değeri olarak kullanılabilir:

import std.algorithm;

struct S {
    string isim;

    int opCmp(S rhs) const {
        return cmp(isim, rhs.isim);
    }
}

opCmp'un tanımlanmış olması bu türün std.algorithm.sort gibi sıralama algoritmalarıyla kullanılabilmesini de sağlar. sort sıralamayı belirlemek için nesneleri karşılaştırırken perde arkasında hep opCmp işletilir. Aşağıdaki program 10 adet rasgele zaman değeri oluşturuyor ve onları sort ile sıralıyor:

import std.random;
import std.stdio;
import std.string;
import std.algorithm;

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

    int opCmp(GününSaati sağdaki) const {
        return (saat == sağdaki.saat
                ? dakika - sağdaki.dakika
                : saat - sağdaki.saat);
    }

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

void main() {
    GününSaati[] zamanlar;

    foreach (i; 0 .. 10) {
        zamanlar ~= GününSaati(uniform(0, 24), uniform(0, 60));
    }

    sort(zamanlar);

    writeln(zamanlar);
}

Beklendiği gibi, çıktıdaki saat değerleri zamana göre sıralanmışlardır:

[03:40,04:10,09:06,10:03,10:09,11:04,13:42,16:40,18:03,21:08]
İşlev gibi çağırmak için opCall

İşlev çağırırken kullanılan parantezler de işleçtir. Bu işlecin türün ismi ile kullanımını bir önceki bölümde static opCall olanağında görmüştük. O kullanım yapı nesnelerinin varsayılan olarak kurulmalarını sağlıyordu.

opCall türün nesnelerinin de işlev gibi kullanılabilmelerini sağlar:

    BirTür nesne;
    nesne();

O kodda nesne bir işlev gibi çağrılmaktadır. Bu kullanım static olmayan opCall üye işlevleri tarafından belirlenir.

Bunun bir örneği olarak bir doğrusal denklemin x değerlerine karşılık y değerlerini hesaplayan bir yapı düşünelim:

    y = ax + b

O hesaptaki y değerlerini opCall işlevi içinde şöyle hesaplayabiliriz:

struct DoğrusalDenklem {
    double a;
    double b;

    double opCall(double x) const {
        return a * x + b;
    }
}

O işlev sayesinde yapının nesneleri işlev gibi kullanılabilir ve verilen x değerlerine karşılık y değerleri hesaplanabilir:

    DoğrusalDenklem denklem = { 1.2, 3.4 };
    // nesne işlev gibi kullanılıyor:
    double y = denklem(5.6);

Not: opCall işlevi tanımlanmış olan yapıları Tür(parametreler) yazımıyla kuramayız çünkü o yazım da bir opCall çağrısı olarak kabul edilir. O yüzden, yukarıdaki nesnenin { } yazımıyla kurulması gerekmiştir. DoğrusalDenklem(1.2, 3.4) yazımı gerçekten gerektiğinde iki double parametre alan bir static opCall işlevi tanımlanmalıdır.

İlk satırda nesne kurulurken denklemin çarpanı olarak 1.2, ekleneni olarak da 3.4 değerinin kullanılacağı belirleniyor. Bunun sonucunda denklem nesnesi, y = 1.2x + 3.4 denklemini ifade etmeye başlar. Ondan sonra nesneyi artık bir işlev gibi kullanarak x değerlerini parametre olarak gönderiyor ve dönüş değeri olarak y değerlerini elde ediyoruz.

Bunun yararı, çarpan ve eklenen değerlerin baştan bir kere belirlenebilmesidir. Nesne o bilgiyi kendi içinde barındırır ve sonradan işlev gibi kullanıldığında yararlanır.

Başka çarpan ve eklenen değerleri ile kurulan bir nesneyi bu sefer de bir döngü içinde kullanan bir örneğe bakalım:

    DoğrusalDenklem denklem = { 0.01, 0.4 };

    for (double x = 0.0; x <= 1.0; x += 0.125) {
        writefln("%f: %f", x, denklem(x));
    }

O da y = 0.01x + 0.4 denklemini x'in 0.0 ile 1.0 aralığındaki her 0.125 adımı için hesaplar.

Dizi erişim işleçleri

opIndex, opIndexAssign, opIndexUnary, opIndexOpAssign, ve opDollar nesneyi nesne[konum] biçiminde dizi gibi kullanma olanağı sağlarlar.

Dizilerden farklı olarak, bu işleçler çok boyutlu indeksleri de desteklerler. Çok boyutlu indeksler köşeli parantezler içinde birden fazla konum değeri ile sağlanır. Örneğin, iki boyutlu dizi gibi işleyen bir tür nesne[konum0, konum1] söz dizimini destekleyebilir. Bu bölümde bu işleçleri yalnızca tek boyutlu olarak kullanacağız ve çok boyutlu örneklerini Ayrıntılı Şablonlar bölümünde göreceğiz.

Aşağıdaki satırlardaki kuyruk, biraz aşağıda tanıyacağımız ÇiftUçluKuyruk türünün bir nesnesi, e ise int türünde bir değişkendir.

opIndex eleman erişimi amacıyla kullanılır. Köşeli parantezler içindeki konum değeri işlevin parametresi haline gelir:

    e = kuyruk[3];                      // 3 numaralı eleman
    e = kuyruk.opIndex(3);              // üsttekinin eşdeğeri

opIndexAssign atama amacıyla kullanılır. İlk parametresi atanan değer, sonraki parametresi de köşeli parantezler içindeki konum değeridir:

    kuyruk[5] = 55;                     // 5 numaralı elemana 55 ata
    kuyruk.opIndexAssign(55, 5);        // üsttekinin eşdeğeri

opIndexUnary, opUnary'nin benzeridir. Farkı, işlemin belirtilen konumdaki eleman üzerinde işleyecek olmasıdır:

    ++kuyruk[4];                        // 4 numaralı elemanı arttır
    kuyruk.opIndexUnary!"++"(4);        // üsttekinin eşdeğeri

opIndexOpAssign, opOpAssign'ın benzeridir. Farkı, atamalı işlemin belirtilen konumdaki eleman üzerinde işleyecek olmasıdır:

    kuyruk[6] += 66;                    // 6 numaralı elemana 66 ekle
    kuyruk.opIndexOpAssign!"+"(66, 6);  // üsttekinin eşdeğeri

opDollar, dilimlerden tanınan $ karakterini tanımlar. İçerilen eleman adedini döndürmek içindir:

    e = kuyruk[$ - 1];                  // sonuncu eleman
    e = kuyruk[kuyruk.opDollar() - 1];  // üsttekinin eşdeğeri
Eleman erişimi işleçleri örneği

Çift uçlu kuyruk (double-ended queue, veya kısaca deque) bir dizi gibi işleyen ama başa eleman eklemenin de sona eleman eklemek kadar hızlı olduğu bir veri yapısıdır. (Dizilerde ise başa eleman eklemek bütün elemanların yeni bir diziye taşınmalarını gerektirdiğinden yavaş bir işlemdir.)

Çift uçlu kuyruk veri yapısını gerçekleştirmenin bir yolu, perde arkasında iki adet diziden yararlanmak ama bunlardan birincisini ters sırada kullanmaktır. Başa eklenen eleman aslında birinci dizinin sonuna eklenir ve böylece o işlem de sona eklemek kadar hızlı olur.

Bu veri yapısını gerçekleştiren aşağıdaki yapı bu bölümde gördüğümüz erişim işleçlerinin hepsini tanımlamaktadır:

import std.stdio;
import std.string;
import std.conv;

struct ÇiftUçluKuyruk
{
private:

    /* Elemanlar bu iki üyenin hayalî olarak uç uca
     * gelmesinden oluşurlar. Ancak, 'baş' ters sırada
     * kullanılır: İlk eleman baş[$-1]'dir, ikinci eleman
     * baş[$-2]'dir, vs.
     *
     *   baş[$-1], baş[$-2], ... baş[0], son[0], ... son[$-1]
     */
    int[] baş;    // baş taraftaki elemanlar
    int[] son;    // son taraftaki elemanlar

    /* Belirtilen konumdaki elemanın hangi dilimde olduğunu
     * bulur ve o elemana bir referans döndürür. */
    ref inout(int) eleman(size_t konum) inout {
        return (konum < baş.length
                ? baş[$ - 1 - konum]
                : son[konum - baş.length]);
    }

public:

    string toString() const {
        string sonuç;

        foreach_reverse (eleman; baş) {
            sonuç ~= format("%s ", to!string(eleman));
        }

        foreach (eleman; son) {
            sonuç ~= format("%s ", to!string(eleman));
        }

        return sonuç;
    }

    /* Not: Sonraki bölümlerde göreceğimiz olanaklardan
     * yararlanıldığında toString() çok daha etkin olarak
     * aşağıdaki gibi de yazılabilir: */
    version (none) {
        void toString(void delegate(const(char)[]) hedef) const {
            import std.format;
            import std.range;

            formattedWrite(
                hedef, "%(%s %)", chain(baş.retro, son));
        }
    }

    /* Başa eleman ekler. */
    void başınaEkle(int değer) {
        baş ~= değer;
    }

    /* Sona eleman ekler.
     *
     * Örnek: kuyruk ~= değer
     */
    ref ÇiftUçluKuyruk opOpAssign(string işleç)(int değer)
            if (işleç == "~") {
        son ~= değer;
        return this;
    }

    /* Belirtilen elemanı döndürür.
     *
     * Örnek: kuyruk[konum]
     */
    inout(int) opIndex(size_t konum) inout {
        return eleman(konum);
    }

    /* Tekli işleci belirtilen elemana uygular.
     *
     * Örnek: ++kuyruk[konum]
     */
    int opIndexUnary(string işleç)(size_t konum) {
        mixin ("return " ~ işleç ~ "eleman(konum);");
    }

    /* Belirtilen elemana belirtilen değeri atar.
     *
     * Örnek: kuyruk[konum] = değer
     */
    int opIndexAssign(int değer, size_t konum) {
        return eleman(konum) = değer;
    }

    /* Belirtilen değeri belirtilen işlemde kullanır ve sonucu
     * belirtilen elemana atar.
     *
     * Örnek: kuyruk[konum] += değer
     */
    int opIndexOpAssign(string işleç)(int değer, size_t konum) {
        mixin ("return eleman(konum) " ~ işleç ~ "= değer;");
    }

    /* Uzunluk anlamına gelen $ karakterini tanımlar.
     *
     * Örnek: kuyruk[$ - 1]
     */
    size_t opDollar() const {
        return baş.length + son.length;
    }
}

void main() {
    auto kuyruk = ÇiftUçluKuyruk();

    foreach (i; 0 .. 10) {
        if (i % 2) {
            kuyruk.başınaEkle(i);

        } else {
            kuyruk ~= i;
        }
    }

    writefln("Üç numaralı eleman: %s",
             kuyruk[3]);    // erişim
    ++kuyruk[4];            // arttırım
    kuyruk[5] = 55;         // atama
    kuyruk[6] += 66;        // atamalı arttırım

    (kuyruk ~= 100) ~= 200;

    writeln(kuyruk);
}

opOpAssign işlevinin dönüş türünün de yukarıdaki ilkeler doğrultusunda ref olarak işaretlendiğine dikkat edin. ~= işleci bu sayede zincirleme olarak kullanılabilmektedir:

    (kuyruk ~= 100) ~= 200;

O ifadelerin sonucunda 100 ve 200 değerleri aynı kuyruk nesnesine eklenmiş olurlar:

Üç numaralı eleman: 3
9 7 5 3 2 55 68 4 6 8 100 200 
Dilim işleçleri

opSlice nesneyi [] işleciyle kullanma olanağı verir.

Bu işlece ek olarak opSliceUnary, opSliceAssign, ve opSliceOpAssign işleçleri de vardır ama onların kullanımları önerilmez.

D, birden fazla boyutta dilimlemeyi destekler. Çok boyutlu bir dizi dilimleme örneğini ilerideki Ayrıntılı Şablonlar bölümünde göreceğiz. O bölümde anlatılacak olan yöntemler tek boyutta da kullanılabilseler de, hem yukarıdaki indeksleme işleçlerine uymazlar hem de henüz görmediğimiz şablonlar olarak tanımlanırlar. Bu yüzden, bu bölümde opSlice'ın şablon olmayan ve yalnızca tek boyutta kullanılabilen bir kullanımını göreceğiz. (opSlice'ın bu kullanımı da önerilmez.)

opSlice'ın iki farklı kullanımı vardır:

Hem elemanları bir araya getiren topluluk kavramıyla hem de o elemanlara erişim sağlayan aralık kavramıyla ilgili olduklarından bu işleçler diğerlerinden daha karmaşık gelebilirler. Topluluk ve aralık kavramlarını ilerideki bölümlerde daha ayrıntılı olarak göreceğiz.

Şablon olmayan ve yalnızca tek boyutta işleyen opSlice'ın buradaki kullanımı, topluluktaki belirli bir aralıktaki elemanları temsil eden bir nesne döndürür. O aralıktaki elemanlara uygulanan işleçleri tanımlamak o nesnenin görevidir. Örneğin, aşağıdaki kullanım perde arkasında önce opSlice yüklemesinden yararlanarak bir aralık nesnesi üretir, sonra opOpAssign!"*" işlecini o aralık nesnesi üzerinde işletir:

    kuyruk[] *= 10;             // bütün elemanları 10'la çarp

    // Üsttekinin eşdeğeri:
    {
        auto aralık = kuyruk.opSlice();
        aralık.opOpAssign!"*"(10);
    }

Buna uygun olarak, ÇiftUçluKuyruk türünün opSlice işlevleri özel bir Aralık nesnesi döndürür:

import std.exception;

struct ÇiftUçluKuyruk {
// ...

    /* Bütün elemanları kapsayan bir aralık döndürür.
     * ('Aralık' yapısı aşağıda tanımlanıyor.)
     *
     * Örnek: kuyruk[]
     */
    inout(Aralık) opSlice() inout {
        return inout(Aralık)(baş[], son[]);
    }

    /* Belirli elemanları kapsayan bir aralık döndürür.
     *
     * Örnek: kuyruk[ilkKonum .. sonKonum]
     */
    inout(Aralık) opSlice(size_t ilkKonum, size_t sonKonum) inout {
        enforce(sonKonum <= opDollar());
        enforce(ilkKonum <= sonKonum);

        /* Belirtilen aralığın 'baş' ve 'son' dilimlerinin
         * hangi bölgelerine karşılık geldiklerini hesaplamaya
         * çalışıyoruz. */

        if (ilkKonum < baş.length) {
            if (sonKonum < baş.length) {
                /* Aralık bütünüyle 'baş' içinde. */
                return inout(Aralık)(
                    baş[$ - sonKonum .. $ - ilkKonum],
                    []);

            } else {
                /* Aralığın bir bölümü 'baş' içinde, geri
                 * kalanı 'son' içinde. */
                return inout(Aralık)(
                    baş[0 .. $ - ilkKonum],
                    son[0 .. sonKonum - baş.length]);
            }

        } else {
            /* Aralık bütünüyle 'son' içinde. */
            return inout(Aralık)(
                [],
                son[ilkKonum - baş.length .. sonKonum - baş.length]);
        }
    }

    /* Kuyruğun belirli bir aralığını temsil eder. opUnary,
     * opAssign, ve opOpAssign işleçlerinin tanımları bu yapı
     * içindedir. */
    struct Aralık {
        int[] başAralık;    // 'baş' içindeki elemanlar
        int[] sonAralık;    // 'son' içindeki elemanlar

        /* Belirtilen tekli işleci elemanlara uygular. */
        Aralık opUnary(string işleç)() {
            mixin (işleç ~ "başAralık[];");
            mixin (işleç ~ "sonAralık[];");
            return this;
        }

        /* Belirtilen değeri elemanlara atar. */
        Aralık opAssign(int değer) {
            başAralık[] = değer;
            sonAralık[] = değer;
            return this;
        }

        /* Belirtilen değeri her eleman için belirtilen
         * işlemde kullanır ve sonucu o elemana atar. */
        Aralık opOpAssign(string işleç)(int değer) {
            mixin ("başAralık[] " ~ işleç ~ "= değer;");
            mixin ("sonAralık[] " ~ işleç ~ "= değer;");
            return this;
        }
    }
}

void main() {
    auto kuyruk = ÇiftUçluKuyruk();

    foreach (i; 0 .. 10) {
        if (i % 2) {
            kuyruk.başınaEkle(i);

        } else {
            kuyruk ~= i;
        }
    }

    writeln(kuyruk);
    kuyruk[] *= 10;
    kuyruk[3 .. 7] = -1;
    writeln(kuyruk);
}

Çıktısı:

9 7 5 3 1 0 2 4 6 8 
90 70 50 -1 -1 -1 -1 40 60 80 
Tür dönüşümü işleci opCast

opCast elle açıkça yapılan tür dönüşümünü belirler ve dönüştürülecek her tür için ayrı ayrı yüklenebilir. Daha önceki bölümlerden hatırlayacağınız gibi, açıkça tür dönüşümü hem to işlevi ile hem de cast işleciyle sağlanabilir.

Bu işleç de şablon olarak tanımlanır ama kalıbı farklıdır: Hangi dönüşümün tanımlanmakta olduğu (T : dönüştürülecek_tür) söz dizimiyle belirtilir:

    dönüştürülecek_tür opCast(T : dönüştürülecek_tür)() {
        // ...
    }

Yine şimdilik bir kalıp olarak kabul etmenizi istediğim bu söz dizimini de daha sonraki Şablonlar bölümünde göreceğiz.

Süre'nin saat ve dakikadan oluşan bir tür olduğunu kabul edelim. Bu türün nesnelerini double türüne dönüştüren işlev aşağıdaki gibi tanımlanabilir:

import std.stdio;
import std.conv;

struct Süre {
    int saat;
    int dakika;

    double opCast(T : double)() const {
        return saat + (to!double(dakika) / 60);
    }
}

void main() {
    auto süre = Süre(2, 30);
    double kesirli = to!double(süre); // cast(double)süre de olabilirdi
    writeln(kesirli);
}

Yukarıdaki tür dönüşümü satırında derleyici üye işlevi perde arkasında şöyle çağırır:

    double kesirli = süre.opCast!double();

double türüne dönüştüren yukarıdaki işleç iki saat otuz dakikaya karşılık 2.5 değerini üretmektedir:

2.5

opCast açıkça yapılan tür dönüşümleri için olduğu halde, onun bool özellemesi mantıksal ifadelerde otomatik olarak işletilir:

struct Süre {
// ...

    bool opCast(T : bool)() const {
        return (saat != 0) || (dakika != 0);
    }
}

// ...

    if (süre) {               // derlenir
        // ...
    }

    while (süre) {            // derlenir
        // ...
    }

    auto r = süre ? 1 : 2;    // derlenir

Yine de, opCast'in bool özellemesi bütün otomatik bool dönüşümleri için değildir:

void foo(bool b) {
    // ...
}

// ...

    foo(süre);                // ← derleme HATASI
    bool b = süre;            // ← derleme HATASI
Error: cannot implicitly convert expression (süre) of type Süre to bool
Error: function deneme.foo (bool b) is not callable using argument types (Süre)
Sevk işleci opDispatch

Nesnenin var olmayan bir üyesine erişildiğinde çağrılacak olan üye işlevdir. Var olmayan üyelere yapılan bütün erişimler bu işlece sevk edilir.

Var olmayan üyenin ismi opDispatch'in bir şablon parametresi olarak belirir.

Bu işleci çok basit olarak gösteren bir örnek:

import std.stdio;

struct BirTür {
    void opDispatch(string isim, T)(T parametre) {
        writefln("BirTür.opDispatch - isim: %s, değer: %s",
                 isim, parametre);
    }
}

void main() {
    BirTür nesne;
    nesne.varOlmayanİşlev(42);
    nesne.varOlmayanBaşkaİşlev(100);
}

Var olmayan üyelerine erişildiği halde derleme hatası alınmaz. Bütün o çağrılar opDispatch işlevinin çağrılmasını sağlarlar. Birinci şablon parametresi işlevin ismidir. Çağrılan noktada kullanılan parametreler de opDispatch'in parametreleri haline gelirler:

BirTür.opDispatch - isim: varOlmayanİşlev, değer: 42
BirTür.opDispatch - isim: varOlmayanBaşkaİşlev, değer: 100

isim şablon parametre değeri normalde opDispatch içinde kullanılabilir ve işlemler onun değerine bağlı olarak seçilebilirler:

   switch (isim) {
       // ...
   }
İçerme sorgusu için opBinaryRight!"in"

Eşleme tablolarından tanıdığımız in işlecini nesneler için de tanımlama olanağı sağlar.

Diğer işleçlerden farklı olarak, bu işleç için nesnenin sağda yazıldığı durum daha doğaldır:

        if (zaman in öğleTatili) {

O yüzden bu işleç için daha çok opBinaryRight!"in" yüklenir ve derleyici perde arkasında o üye işlevi çağırır:

                                        // üsttekinin eşdeğeri
        if (öğleTatili.opBinaryRight!"in"(zaman)) {

!in işleci ise bir değerin eşleme tablosunda bulunmadığını belirlemek için kullanılır:

        if (a !in b) {

!in yüklenemez çünkü derleyici perde arkasında in işlecinin sonucunun tersini kullanır:

        if (!(a in b)) {    // üsttekinin eşdeğeri
in işleci örneği

Bu örnek daha önce gördüğümüz Süre ve GününSaati yapılarına ek olarak bir de ZamanAralığı yapısı tanımlıyor. Bu yapı için tanımlanan in işleci belirli bir zamanın belirli bir aralıkta olup olmadığını bildirmek için kullanılacak.

Bu örnekte de yalnızca gerektiği kadar üye işlev kullandım.

GününSaati nesnesinin for döngüsünde nasıl temel türler kadar rahat kullanıldığına özellikle dikkat edin. O döngü işleç yüklemenin yararını gösteriyor.

import std.stdio;
import std.string;

struct Süre {
    int dakika;
}

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

    ref GününSaati opOpAssign(string işleç)(Süre süre)
            if (işleç == "+") {
        dakika += süre.dakika;

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

        return this;
    }

    int opCmp(GününSaati sağdaki) const {
        return (saat == sağdaki.saat
                ? dakika - sağdaki.dakika
                : saat - sağdaki.saat);
    }

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

struct ZamanAralığı {
    GününSaati baş;
    GününSaati son;    // son aralığın dışında kabul edilir

    bool opBinaryRight(string işleç)(GününSaati zaman) const
            if (işleç == "in") {
        return (zaman >= baş) && (zaman < son);
    }
}

void main() {
    auto öğleTatili = ZamanAralığı(GününSaati(12, 00),
                                   GününSaati(13, 00));

    for (auto zaman = GününSaati(11, 30);
         zaman < GününSaati(13, 30);
         zaman += Süre(15)) {

        if (zaman in öğleTatili) {
            writeln(zaman, " öğle tatilinde");

        } else {
            writeln(zaman, " öğle tatili dışında");
        }
    }
}

Çıktısı:

11:30 öğle tatili dışında
11:45 öğle tatili dışında
12:00 öğle tatilinde
12:15 öğle tatilinde
12:30 öğle tatilinde
12:45 öğle tatilinde
13:00 öğle tatili dışında
13:15 öğle tatili dışında
Problem

Payını ve paydasını long türünde iki üye olarak tutan bir kesirli sayı türü tanımlayın. Böyle bir yapının bir yararı, float, double, ve real'deki değer kayıplarının bulunmamasıdır. Örneğin, 1.0/3 gibi bir double değerin 3 ile çarpılmasının sonucu 1.0 olmadığı halde 1/3'ü temsil eden Kesirli bir nesnenin 3 ile çarpılmasının sonucu tam olarak 1'dir:

struct Kesir {
    long pay;
    long payda;

    /* Kurucu işlev kolaylık olsun diye paydanın
     * belirtilmesini gerektirmiyor ve 1 varsayıyor. */
    this(long pay, long payda = 1) {
        enforce(payda != 0, "Payda sıfır olamaz");

        this.pay = pay;
        this.payda = payda;

        /* Paydanın eksi değer almasını başından önlemek daha
         * sonraki işlemleri basitleştirecek. */
        if (this.payda < 0) {
            this.pay = -this.pay;
            this.payda = -this.payda;
        }
    }

    /* ... işleçleri siz tanımlayın ... */
}

Bu yapı için işleçler tanımlayarak olabildiğince temel türler gibi işlemesini sağlayın. Yapının tanımı tamamlandığında aşağıdaki birim testi bloğu hatasız işletilebilsin. O blokta şu işlemler bulunuyor:

unittest {
    /* Payda 0 olduğunda hata atılmalı. */
    assertThrown(Kesir(42, 0));

    /* 1/3 değeriyle başlayacağız. */
    auto a = Kesir(1, 3);

    /* -1/3 */
    assert(-a == Kesir(-1, 3));

    /* 1/3 + 1 == 4/3 */
    ++a;
    assert(a == Kesir(4, 3));

    /* 4/3 - 1 == 1/3 */
    --a;
    assert(a == Kesir(1, 3));

    /* 1/3 + 2/3 == 3/3 */
    a += Kesir(2, 3);
    assert(a == Kesir(1));

    /* 3/3 - 2/3 == 1/3 */
    a -= Kesir(2, 3);
    assert(a == Kesir(1, 3));

    /* 1/3 * 8 == 8/3 */
    a *= Kesir(8);
    assert(a == Kesir(8, 3));

    /* 8/3 / 16/9 == 3/2 */
    a /= Kesir(16, 9);
    assert(a == Kesir(3, 2));

    /* double türünde bir değere dönüştürülebilmeli.
     *
     * Hatırlarsanız, double türü her değeri tam olarak ifade
     * edemez. 1.5 değeri tam olarak ifade edilebildiği için
     * bu testi bu noktada uyguladım. */
    assert(to!double(a) == 1.5);

    /* 1.5 + 2.5 == 4 */
    assert(a + Kesir(5, 2) == Kesir(4, 1));

    /* 1.5 - 0.75 == 0.75 */
    assert(a - Kesir(3, 4) == Kesir(3, 4));

    /* 1.5 * 10 == 15 */
    assert(a * Kesir(10) == Kesir(15, 1));

    /* 1.5 / 4 == 3/8 */
    assert(a / Kesir(4) == Kesir(3, 8));

    /* Sıfırla bölmek hata atmalı. */
    assertThrown(Kesir(42, 1) / Kesir(0));

    /* Payı az olan öncedir. */
    assert(Kesir(3, 5) < Kesir(4, 5));

    /* Paydası büyük olan öncedir. */
    assert(Kesir(3, 9) < Kesir(3, 8));
    assert(Kesir(1, 1_000) > Kesir(1, 10_000));

    /* Değeri küçük olan öncedir. */
    assert(Kesir(10, 100) < Kesir(1, 2));

    /* Eksi değer öncedir. */
    assert(Kesir(-1, 2) < Kesir(0));
    assert(Kesir(1, -2) < Kesir(0));

    /* Aynı değerler hem <= hem de >= olmalı.  */
    assert(Kesir(-1, -2) <= Kesir(1, 2));
    assert(Kesir(1, 2) <= Kesir(-1, -2));
    assert(Kesir(3, 7) <= Kesir(9, 21));
    assert(Kesir(3, 7) >= Kesir(9, 21));

    /* Değerleri aynı olanlar eşit olmalı. */
    assert(Kesir(1, 3) == Kesir(20, 60));

    /* Karışık işaretler aynı sonucu üretmeli. */
    assert(Kesir(-1, 2) == Kesir(1, -2));
    assert(Kesir(1, 2) == Kesir(-1, -2));
}