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

algoritma: [algorithm], verilerin işlenme adımları, işlev
anahtar sözcük: [keyword], dilin kendisi için ayırmış olduğu ve iç olanakları için kullandığı sözcük
bağlayıcı: [linker], derleyicinin oluşturduğu program parçalarını bir araya getiren program
bağlı liste: [linked list], her elemanı bir sonraki elemanı gösteren veri yapısı
çalışma ortamı: [runtime], çalışma zamanında dil desteği veren ve her programa otomatik olarak eklenmiş olan program parçası
çıkarsama: [deduction, inference], derleyicinin kendiliğinden anlaması
indeks: [index], topluluk elemanlarına erişmek için kullanılan bilgi
kısıtlama: [constraint], şablon parametrelerinin uyması gereken koşulların belirlenmesi
nitelik: [property, attribute], bir türün veya nesnenin bir özelliği
özelleme: [specialization], şablonun bir özel tanımı
özyineleme: [recursion], bir işlevin doğrudan veya dolaylı olarak kendisini çağırması
şablon: [template], derleyicinin örneğin 'türden bağımsız programlama' için kod üretme düzeneği
taşma; üstten veya alttan: [overflow veya underflow], değerin bir türe sığamayacak kadar büyük veya küçük olması
topluluk: [container], aynı türden birden fazla veriyi bir araya getiren veri yapısı
özgün isim üretme: [name mangling], bağlayıcı tanıyabilsin diye programdaki isimlerin özgünleştirilmeleri
veri yapıları: [data structures], verilerin bilgisayar biliminin tanımladığı biçimde saklanmaları ve işlenmeleri
yükleme: [overloading], aynı isimde birden çok işlev tanımlama
... bütün sözlük



İngilizce Kaynaklar


Diğer




Şablonlar

Şablonlar derleyicinin belirli bir kalıba uygun olarak kod üretmesini sağlayan olanaktır. Herhangi bir kod parçasının bazı bölümleri sonraya bırakılır; derleyici o kod bölümlerini uygun olan türler, değerler, vs. ile kendisi oluşturur.

Şablonlar algoritmaların ve veri yapılarının türden bağımsız olarak yazılabilmelerini sağlarlar ve bu yüzden özellikle kütüphanelerde çok yararlıdırlar.

D'nin şablon olanağı bazı başka dillerdekilerle karşılaştırıldığında çok güçlü ve çok kapsamlıdır. Bu yüzden şablonların bütün ayrıntılarına bu bölümde giremeyeceğim. Burada, gündelik kullanımda en çok karşılaşılan işlev, yapı, ve sınıf şablonlarının türlerle nasıl kullanıldıklarını göstereceğim.

Kendisine verilen değeri parantez içinde yazdıran basit bir işleve bakalım:

void parantezliYazdır(int değer) {
    writefln("(%s)", değer);
}

Parametresi int olarak tanımlandığından, o işlev yalnızca int türüyle veya otomatik olarak int'e dönüşebilen türlerle kullanılabilir. Derleyici, örneğin kesirli sayı türleriyle çağrılmasına izin vermez.

O işlevi kullanan programın zamanla geliştiğini ve artık başka türlerden olan değerlerin de parantez içinde yazdırılması gerektiğini düşünelim. Bunun için bir çözüm, D'nin işlev yükleme olanağıdır; aynı işlev başka türler için de tanımlanır:

// Daha önce yazılmış olan işlev
void parantezliYazdır(int değer) {
    writefln("(%s)", değer);
}

// İşlevin double türü için yüklenmesi
void parantezliYazdır(double değer) {
    writefln("(%s)", değer);
}

Bu da ancak belirli bir noktaya kadar yeterlidir çünkü bu işlevi bu sefer de örneğin real türüyle veya kendi tanımlamış olabileceğimiz başka türlerle kullanamayız. Tabii işlevi o türler için de yüklemeyi düşünebiliriz ama her tür için ayrı ayrı yazılmasının çok külfetli olacağı açıktır.

Burada dikkatinizi çekmek istediğim nokta, tür ne olursa olsun işlevin içeriğinin hep aynı olduğudur. Türler için yüklenen bu işlevdeki işlemler, türden bağımsız olarak hepsinde aynıdır. Benzer durumlar özellikle algoritmalarda ve veri yapılarında karşımıza çıkar.

Örneğin, ikili arama algoritması türden bağımsızdır: O algoritma yalnızca işlemlerle ilgilidir. Aynı biçimde, örneğin bağlı liste veri yapısı da türden bağımsızdır: Yalnızca topluluktaki elemanların nasıl bir arada tutulduklarını belirler.

İşte şablonlar bu gibi durumlarda yararlıdır: Kod bir kalıp halinde tarif edilir ve derleyici, programda kullanılan türler için kodu gerektikçe kendisi üretir.

İşlev şablonları

İşlevi bir kalıp olarak tarif etmek, içinde kullanılan bir veya daha fazla türün belirsiz olarak sonraya bırakılması anlamına gelir.

İşlevdeki hangi türlerin sonraya bırakıldıkları işlev parametrelerinden hemen önce yazılan şablon parametreleriyle belirtilir. Bu yüzden işlev şablonlarında iki adet parametre parantezi bulunur; birincisi şablon parametreleridir, ikincisi de işlev parametreleri:

void parantezliYazdır(T)(T değer) {
    writefln("(%s)", değer);
}

Yukarıda şablon parametresi olarak kullanılan T, "bu işlevde T yazdığımız yerlerde asıl hangi türün kullanılacağına derleyici gerektikçe kendisi karar versin" anlamına gelir. T yerine herhangi başka bir isim de yazılabilir. Ancak, "type"ın baş harfi olduğu için T harfi gelenekleşmiştir. "Tür"ün baş harfine de uyduğu için aksine bir neden olmadığı sürece T kullanmak yerinde olacaktır.

O şablonu yukarıdaki gibi türden bağımsız olarak yazmak, kendi türlerimiz de dahil olmak üzere onu çeşitli türlerle çağırma olanağı sağlar:

import std.stdio;

void parantezliYazdır(T)(T değer) {
    writefln("(%s)", değer);
}

void main() {
    parantezliYazdır(42);           // int ile
    parantezliYazdır(1.2);          // double ile

    auto birDeğer = BirYapı();
    parantezliYazdır(birDeğer);     // BirYapı nesnesi ile
}

struct BirYapı {
    string toString() const {
        return "merhaba";
    }
}

Derleyici, programdaki kullanımlarına bakarak yukarıdaki işlev şablonunu gereken her tür için ayrı ayrı üretir. Program, sanki o işlev T'nin kullanıldığı üç farklı tür için, yani int, double, ve BirYapı için ayrı ayrı yazılmış gibi derlenir:

/* Not: Bu işlevlerin hiçbirisi programa dahil değildir.
 *      Derleyicinin kendi ürettiği işlevlerin eşdeğerleri
 *      olarak gösteriyorum. */

void parantezliYazdır(int değer) {
    writefln("(%s)", değer);
}

void parantezliYazdır(double değer) {
    writefln("(%s)", değer);
}

void parantezliYazdır(BirYapı değer) {
    writefln("(%s)", değer);
}

Programın çıktısı da o üç farklı işlevin etkisini gösterecek biçimde her tür için farklıdır:

(42)
(1.2)
(merhaba)

Her şablon parametresi birden fazla işlev parametresini belirliyor olabilir. Örneğin, tek parametresi bulunan aşağıdaki şablonun hem iki işlev parametresinin hem de dönüş değerinin türü o şablon parametresi ile belirlenmektedir:

/* 'dilim'in 'değer'e eşit olmayan elemanlarından oluşan yeni
 * bir dilim döndürür. */
T[] süz(T)(const(T)[] dilim, T değer) {
    T[] sonuç;

    foreach (eleman; dilim) {
        if (eleman != değer) {
            sonuç ~= eleman;
        }
    }

    return sonuç;
}
Birden fazla şablon parametresi kullanılabilir

Aynı işlevi, açma ve kapama parantezlerini de kullanıcıdan alacak şekilde değiştirdiğimizi düşünelim:

void parantezliYazdır(T)(T değer, char açma, char kapama) {
    writeln(açma, değer, kapama);
}

Artık o işlevi, istediğimiz parantez karakterleri ile çağırabiliriz:

    parantezliYazdır(42, '<', '>');

Parantezleri belirleyebiliyor olmak işlevin kullanışlılığını arttırmış olsa da, parantezlerin türünün char olarak sabitlenmiş olmaları işlevin kullanışlılığını tür açısından düşürmüştür. İşlevi örneğin ancak wchar ile ifade edilebilen Unicode karakterleri arasında yazdırmaya çalışsak, wchar'ın char'a dönüştürülemeyeceği ile ilgili bir derleme hatası alırız:

    parantezliYazdır(42, '→', '←');      // ← derleme HATASI
Error: template deneme.parantezliYazdır(T) cannot deduce
template function from argument types !()(int,wchar,wchar)

Bunun bir çözümü, parantez karakterlerini her karakteri ifade edebilen dchar olarak tanımlamaktır. Bu da yetersiz olacaktır çünkü işlev bu sefer de örneğin string ile veya kendi özel türlerimizle kullanılamaz.

Başka bir çözüm, yine şablon olanağından yararlanmak ve parantezin türünü de derleyiciye bırakmaktır. Yapmamız gereken, işlev parametresi olarak char yerine yeni bir şablon parametresi kullanmak ve onu da şablon parametre listesinde belirtmektir:

void parantezliYazdır(T, ParantezTürü)(T değer,
                                       ParantezTürü açma,
                                       ParantezTürü kapama) {
    writeln(açma, değer, kapama);
}

Yeni şablon parametresinin anlamı da T'ninki gibidir: "bu işlev tanımında ParantezTürü geçen yerlerde hangi tür gerekiyorsa o kullanılsın".

Artık parantez olarak herhangi bir tür kullanılabilir. Örneğin wchar ve string türleriyle:

    parantezliYazdır(42, '→', '←');
    parantezliYazdır(1.2, "-=", "=-");
→42←
-=1.2=-

Bu şablonun yararı, tek işlev tanımlamış olduğumuz halde T ve ParantezTürü şablon parametrelerinin otomatik olarak belirlenebilmeleridir.

Tür çıkarsama

Derleyici yukarıdaki iki kullanımda şu türleri otomatik olarak seçer:

İşlevin çağrıldığı noktalarda hangi türlerin gerektiği işlevin parametrelerinden kolayca anlaşılabilmektedir. Derleyicinin, türü işlev çağrılırken kullanılan parametrelerden anlamasına tür çıkarsaması denir.

Derleyici şablon parametrelerini ancak ve ancak işlev çağrılırken kullanılan türlerden çıkarsayabilir.

Türün açıkça belirtilmesi

Bazı durumlarda ise şablon parametreleri çıkarsanamazlar, çünkü örneğin işlevin parametresi olarak geçmiyorlardır. Öyle durumlarda derleyicinin şablonun kullanımına bakarak çıkarsaması olanaksızdır.

Örnek olarak kullanıcıya bir soru soran ve o soru karşılığında girişten bir değer okuyan bir işlev düşünelim; okuduğu değeri döndürüyor olsun. Ayrıca, bütün türler için kullanılabilmesi için de dönüş türünü sabitlemeyelim ve bir şablon parametresi olarak tanımlayalım:

T giriştenOku(T)(string soru) {
    writef("%s (%s): ", soru, T.stringof);

    T cevap;
    readf(" %s", &cevap);

    return cevap;
}

O işlev, girişten okuma işini türden bağımsız olarak gerçekleştirdiği için programda çok yararlı olacaktır. Örneğin, kullanıcı bilgilerini edinmek için şu şekilde çağırmayı düşünebiliriz:

    giriştenOku("Yaşınız?");

Ancak, o çağırma sırasında T'nin hangi türden olacağını belirten hiçbir ipucu yoktur. Soru işleve string olarak gitmektedir ama derleyici dönüş türü için hangi türü istediğimizi bilemez ve T'yi çıkarsayamadığını bildiren bir hata verir:

Error: template deneme.giriştenOku(T) cannot deduce template
function from argument types !()(string)

Bu gibi durumlarda şablon parametrelerinin ne oldukları programcı tarafından açıkça belirtilmek zorundadır. Şablonun hangi türlerle üretileceği, yani şablon parametreleri, işlev isminden sonraki ünlem işareti ve hemen ardından gelen şablon parametre listesi ile bildirilir:

    giriştenOku!(int)("Yaşınız?");

O kod artık derlenir ve yukarıdaki şablon, T yerine int yazılmış gibi derlenir.

Tek şablon parametresi belirtilen durumlarda bir kolaylık olarak şablon parantezleri yazılmayabilir:

    giriştenOku!int("Yaşınız?");    // üsttekinin eşdeğeri

O yazılışı şimdiye kadar çok kullandığımız to!string'den tanıyorsunuz. to bir işlev şablonudur. Ona verdiğimiz değerin hangi türe dönüştürüleceğini bir şablon parametresi olarak alır. Tek şablon parametresi gerektiği için de to!(string) yerine onun kısası olan to!string yazılır.

Şablon özellemeleri

giriştenOku işlevini başka türlerle de kullanabiliriz. Ancak, derleyicinin ürettiği kod her tür için geçerli olmayabilir. Örneğin, iki boyutlu düzlemdeki bir noktayı ifade eden bir yapı olsun:

struct Nokta {
    int x;
    int y;
}

Her ne kadar yasal olarak derlenebilse de, giriştenOku şablonunu bu yapı ile kullanırsak şablon içindeki readf işlevi doğru çalışmaz. Şablon içinde Nokta türüne karşılık olarak üretilen kod şöyle olacaktır:

    Nokta cevap;
    readf(" %s", &cevap);    // YANLIŞ

Doğrusu, Nokta'yı oluşturacak olan x ve y değerlerinin girişten ayrı ayrı okunmaları ve nesnenin bu değerlerle kurulmasıdır.

Böyle durumlarda, şablonun belirli bir tür için özel olarak tanımlanmasına özelleme denir. Hangi tür için özellendiği, şablon parametre listesinde : karakterinden sonra yazılarak belirtilir:

// Şablonun genel tanımı (yukarıdakinin aynısı)
T giriştenOku(T)(string soru) {
    writef("%s (%s): ", soru, T.stringof);

    T cevap;
    readf(" %s", &cevap);

    return cevap;
}

// Şablonun Nokta türü için özellenmesi
T giriştenOku(T : Nokta)(string soru) {
    writefln("%s (Nokta)", soru);

    auto x = giriştenOku!int("  x");
    auto y = giriştenOku!int("  y");

    return Nokta(x, y);
}

giriştenOku işlevi bir Nokta için çağrıldığında, derleyici artık o özel tanımı kullanır:

    auto merkez = giriştenOku!Nokta("Merkez?");

O işlev de kendi içinde giriştenOku!int'i iki kere çağırarak x ve y değerlerini ayrı ayrı okur:

Merkez? (Nokta)
  x (int): 11
  y (int): 22

giriştenOku!int çağrıları şablonun genel tanımına, giriştenOku!Nokta çağrıları da şablonun özel tanımına yönlendirilecektir.

Başka bir örnek olarak, şablonu string ile kullanmayı da düşünebiliriz. Ne yazık ki şablonun genel tanımı girişin sonuna kadar okunmasına neden olur:

    // bütün girişi okur:
    auto isim = giriştenOku!string("İsminiz?");

Eğer string'lerin tek satır olarak okunmalarının uygun olduğunu kabul edersek, bu durumda da çözüm şablonu string için özel olarak tanımlamaktır:

T giriştenOku(T : string)(string soru) {
    writef("%s (string): ", soru);

    // Bir önceki kullanıcı girişinin sonunda kalmış
    // olabilecek boşluk karakterlerini de oku ve gözardı et
    string cevap;
    do {
        cevap = strip(readln());
    } while (cevap.length == 0);

    return cevap;
}
Yapı ve sınıf şablonları

Yukarıdaki Nokta sınıfının iki üyesi int olarak tanımlandığından, işlev şablonlarında karşılaştığımız yetersizlik onda da vardır.

Nokta yapısının daha kapsamlı olduğunu düşünelim. Örneğin, kendisine verilen başka bir noktaya olan uzaklığını hesaplayabilsin:

import std.math;

// ...

struct Nokta {
    int x;
    int y;

    int uzaklık(in Nokta diğerNokta) const {
        immutable real xFarkı = x - diğerNokta.x;
        immutable real yFarkı = y - diğerNokta.y;

        immutable uzaklık = sqrt((xFarkı * xFarkı) +
                                 (yFarkı * yFarkı));

        return cast(int)uzaklık;
    }
}

O yapı, örneğin kilometre duyarlığındaki uygulamalarda yeterlidir:

    auto merkez = giriştenOku!Nokta("Merkez?");
    auto şube = giriştenOku!Nokta("Şube?");

    writeln("Uzaklık: ", merkez.uzaklık(şube));

Ancak, kesirli değerler gerektiren daha hassas uygulamalarda kullanışsızdır.

Yapı ve sınıf şablonları, onları da belirli bir kalıba uygun olarak tanımlama olanağı sağlarlar. Bu durumda, yapıya (T) parametresi eklemek ve tanımındaki int'ler yerine T kullanmak, bu tanımın bir şablon haline gelmesi ve üyelerin türlerinin derleyici tarafından belirlenmesi için yeterlidir:

struct Nokta(T) {
    T x;
    T y;

    T uzaklık(in Nokta diğerNokta) const {
        immutable real xFarkı = x - diğerNokta.x;
        immutable real yFarkı = y - diğerNokta.y;

        immutable uzaklık = sqrt((xFarkı * xFarkı) +
                                 (yFarkı * yFarkı));

        return cast(T)uzaklık;
    }
}

Yapı ve sınıflar işlev olmadıklarından, çağrılmaları söz konusu değildir. O yüzden derleyicinin şablon parametrelerini çıkarsaması olanaksızdır; türleri açıkça belirtmemiz gerekir:

    auto merkez = Nokta!int(0, 0);
    auto şube = Nokta!int(100, 100);

    writeln("Uzaklık: ", merkez.uzaklık(şube));

Yukarıdaki kullanım, derleyicinin Nokta şablonunu T yerine int gelecek şekilde üretmesini sağlar. Bir şablon olduğundan başka türlerle de kullanabiliriz. Örneğin, virgülden sonrasının önemli olduğu bir uygulamada:

    auto nokta1 = Nokta!double(1.2, 3.4);
    auto nokta2 = Nokta!double(5.6, 7.8);

    writeln(nokta1.uzaklık(nokta2));

Yapı ve sınıf şablonları, veri yapılarını böyle türden bağımsız olarak tanımlama olanağı sağlar. Dikkat ederseniz, Nokta şablonundaki üyeler ve işlemler tamamen T'nin asıl türünden bağımsız olarak yazılmışlardır.

Nokta'nın artık bir yapı şablonu olması, giriştenOku işlev şablonunun daha önce yazmış olduğumuz Nokta özellemesinde bir sorun oluşturur:

T giriştenOku(T : Nokta)(string soru) {    // ← derleme HATASI
    writefln("%s (Nokta)", soru);

    auto x = giriştenOku!int("  x");
    auto y = giriştenOku!int("  y");

    return Nokta(x, y);
}

Hatanın nedeni, artık Nokta diye bir tür bulunmamasıdır: Nokta artık bir tür değil, bir yapı şablonudur. Bir tür olarak kabul edilebilmesi için, mutlaka şablon parametresinin de belirtilmesi gerekir. giriştenOku işlev şablonunu bütün Nokta kullanımları için özellemek için aşağıdaki değişiklikleri yapabiliriz. Açıklamalarını koddan sonra yapacağım:

Nokta!T giriştenOku(T : Nokta!T)(string soru) {   // 2, 1
    writefln("%s (Nokta!%s)", soru, T.stringof);  // 5

    auto x = giriştenOku!T("  x");                // 3a
    auto y = giriştenOku!T("  y");                // 3b

    return Nokta!T(x, y);                         // 4
}
  1. Bu işlev şablonu özellemesinin Nokta'nın bütün kullanımlarını desteklemesi için, şablon parametre listesinde Nokta!T yazılması gerekir; bir anlamda, T ne olursa olsun, bu özellemenin Nokta!T türleri için olduğu belirtilmektedir: Nokta!int, Nokta!double, vs.
  2. Girişten okunan türe uyması için dönüş türünün de Nokta!T olarak belirtilmesi gerekir.
  3. Bu işlevin önceki tanımında olduğu gibi giriştenOku!int'i çağıramayız çünkü Nokta'nın üyeleri herhangi bir türden olabilir. Bu yüzden, T ne ise, giriştenOku şablonunu o türden değer okuyacak şekilde, yani giriştenOku!T şeklinde çağırmamız gerekir.
  4. 1 ve 2 numaralı maddelere benzer şekilde, döndürdüğümüz değer de bir Nokta!T olmak zorundadır.
  5. Okumakta olduğumuz türün "(Nokta)" yerine örneğin "(Nokta!double)" olarak bildirilmesi için şablon türünün ismini T.stringof'tan ediniyoruz.
Varsayılan şablon parametreleri

Şablonların getirdiği bu esneklik çok kullanışlı olsa da şablon parametrelerinin her durumda belirtilmeleri bazen gereksiz olabilir. Örneğin, giriştenOku işlev şablonu programda hemen hemen her yerde int ile kullanılıyordur ve belki de yalnızca bir kaç noktada örneğin double ile de kullanılıyordur.

Böyle durumlarda şablon parametrelerine varsayılan türler verilebilir ve açıkça belirtilmediğinde o türler kullanılır. Varsayılan şablon parametre türleri = karakterinden sonra belirtilir:

T giriştenOku(T = int)(string soru) {
    // ...
}

// ...

    auto yaş = giriştenOku("Yaşınız?");

Yukarıdaki işlev çağrısında şablon parametresi belirtilmediği halde int varsayılır; yukarıdaki çağrı giriştenOku!int ile aynıdır.

Yapı ve sınıf şablonları için de varsayılan parametre türleri bildirilebilir. Ancak, şablon parametre listesinin boş olsa bile yazılması şarttır:

struct Nokta(T = int) {
    // ...
}

// ...

    Nokta!() merkez;

Parametre Serbestliği bölümünde işlev parametreleri için anlatılana benzer şekilde, varsayılan şablon parametreleri ya bütün parametreler için ya da yalnızca sondaki parametreler için belirtilebilir:

void birŞablon(T0, T1 = int, T2 = char)() {
    // ...
}

O şablonun son iki parametresinin belirtilmesi gerekmez ama birincisi şarttır:

    birŞablon!string();

O kullanımda ikinci parametre int, üçüncü parametre de char olur.

Her şablon gerçekleştirmesi farklı bir türdür

Bir şablonun belirli bir tür veya türler için üretilmesi yepyeni bir tür oluşturur. Örneğin Nokta!int başlıbaşına bir türdür. Aynı şekilde, Nokta!double da başlıbaşına bir türdür.

Bu türler birbirlerinden farklıdırlar:

Nokta!int nokta3 = Nokta!double(0.25, 0.75); // ← derleme HATASI

Türlerin uyumsuz olduklarını gösteren bir derleme hatası alınır:

Error: cannot implicitly convert expression (Nokta(0.25,0.75))
of type Nokta!(double) to Nokta!(int)
Derleme zamanı olanağıdır

Şablon olanağı bütünüyle derleme zamanında işleyen ve derleyici tarafından işletilen bir olanaktır. Derleyicinin kod üretmesiyle ilgili olduğundan, program çalışmaya başladığında şablonların koda çevrilmeleri ve derlenmeleri çoktan tamamlanmıştır.

Sınıf şablonu örneği: yığın veri yapısı

Yapı ve sınıf şablonları veri yapılarında çok kullanılırlar. Bunun bir örneğini görmek için bir yığın topluluğu (stack container) tanımlayalım.

Yığın topluluğu veri yapılarının en basit olanlarındandır: Elemanların üst üste durdukları düşünülür. Eklenen her eleman en üste yerleştirilir ve yalnızca bu üstteki elemana erişilebilir. Topluluktan eleman çıkartılmak istendiğinde de yalnızca en üstteki eleman çıkartılabilir.

Kullanışlı olsun diye topluluktaki eleman sayısını veren bir nitelik de tasarlarsak, bu basit veri yapısının işlemlerini şöyle sıralayabiliriz:

Bu veri yapısını gerçekleştirmek için D'nin iç olanaklarından olan dizilerden yararlanabiliriz. Dizinin sonuncu elemanı, yığın topluluğunun üstteki elemanı olarak kabul edilebilir.

Dizi elemanı türünü de sabit bir tür olarak yazmak yerine şablon parametresi olarak belirlersek, bu veri yapısını her türle kullanabilecek şekilde şöyle tanımlayabiliriz:

class Yığın(T) {
private:

    T[] elemanlar;

public:

    void ekle(T eleman) {
        elemanlar ~= eleman;
    }

    void çıkart() {
        --elemanlar.length;
    }

    T üstteki() const @property {
        return elemanlar[$ - 1];
    }

    size_t uzunluk() const @property {
        return elemanlar.length;
    }
}

Ekleme ve çıkartma işlemlerinin üye işlevler olmaları doğaldır. üstteki ve uzunluk işlevlerini ise nitelik olarak tanımlamayı daha uygun buldum. Çünkü ikisi de bu veri yapısıyla ilgili basit bir bilgi sunuyorlar.

Bu sınıf için bir unittest bloğu tanımlayarak beklediğimiz şekilde çalıştığından emin olabiliriz. Aşağıdaki blok bu türü int türündeki elemanlarla kullanıyor:

unittest {
    auto yığın = new Yığın!int;

    // Eklenen eleman üstte görünmeli
    yığın.ekle(42);
    assert(yığın.üstteki == 42);
    assert(yığın.uzunluk == 1);

    // .üstteki ve .uzunluk elemanları etkilememeli
    assert(yığın.üstteki == 42);
    assert(yığın.uzunluk == 1);

    // Yeni eklenen eleman üstte görünmeli
    yığın.ekle(100);
    assert(yığın.üstteki == 100);
    assert(yığın.uzunluk == 2);

    // Eleman çıkartılınca önceki görünmeli
    yığın.çıkart();
    assert(yığın.üstteki == 42);
    assert(yığın.uzunluk == 1);

    // Son eleman çıkartılınca boş kalmalı
    yığın.çıkart();
    assert(yığın.uzunluk == 0);
}

Bu veri yapısını bir şablon olarak tanımlamış olmanın yararını görmek için onu kendi tanımladığımız bir türle kullanalım:

struct Nokta(T) {
    T x;
    T y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}

double türünde üyeleri bulunan Nokta'ları içeren bir Yığın şablonu şöyle oluşturulabilir:

    auto noktalar = new Yığın!(Nokta!double);

Bu veri yapısına on tane rasgele değerli nokta ekleyen ve sonra onları teker teker çıkartan bir deneme programı şöyle yazılabilir:

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

struct Nokta(T) {
    T x;
    T y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}

// -0.50 ile 0.50 arasında rasgele bir değer döndürür
double rasgele_double()
out (sonuç) {
    assert((sonuç >= -0.50) && (sonuç < 0.50));

} body {
    return (double(uniform(0, 100)) - 50) / 100;
}

// Belirtilen sayıda rasgele Nokta!double içeren bir Yığın
// döndürür
Yığın!(Nokta!double) rasgeleNoktalar(size_t adet)
out (sonuç) {
    assert(sonuç.uzunluk == adet);

} body {
    auto noktalar = new Yığın!(Nokta!double);

    foreach (i; 0 .. adet) {
        immutable nokta = Nokta!double(rasgele_double(),
                                       rasgele_double());
        writeln("ekliyorum   : ", nokta);
        noktalar.ekle(nokta);
    }

    return noktalar;
}

void main() {
    auto üstÜsteNoktalar = rasgeleNoktalar(10);

    while (üstÜsteNoktalar.uzunluk) {
        writeln("çıkartıyorum: ", üstÜsteNoktalar.üstteki);
        üstÜsteNoktalar.çıkart();
    }
}

Programın çıktısından anlaşılacağı gibi, eklenenlerle çıkartılanlar ters sırada olmaktadır:

ekliyorum   : (0.02,0.1)
ekliyorum   : (0.23,-0.34)
ekliyorum   : (0.47,0.39)
ekliyorum   : (0.03,-0.05)
ekliyorum   : (0.01,-0.47)
ekliyorum   : (-0.25,0.02)
ekliyorum   : (0.39,0.35)
ekliyorum   : (0.32,0.31)
ekliyorum   : (0.02,-0.27)
ekliyorum   : (0.25,0.24)
çıkartıyorum: (0.25,0.24)
çıkartıyorum: (0.02,-0.27)
çıkartıyorum: (0.32,0.31)
çıkartıyorum: (0.39,0.35)
çıkartıyorum: (-0.25,0.02)
çıkartıyorum: (0.01,-0.47)
çıkartıyorum: (0.03,-0.05)
çıkartıyorum: (0.47,0.39)
çıkartıyorum: (0.23,-0.34)
çıkartıyorum: (0.02,0.1)
İşlev şablonu örneği: ikili arama algoritması

İkili arama algoritması, bir dizi halinde yan yana ve sıralı olarak duran değerler arasında arama yapan en hızlı algoritmadır. Bu algoritmanın bir diğer adı "ikiye bölerek arama", İngilizcesi de "binary search"tür.

Çok basit bir algoritmadır: Sıralı olarak duran değerlerin ortadakine bakılır. Eğer aranan değere eşitse, değer bulunmuş demektir. Eğer değilse, o orta değerin aranan değerden daha küçük veya büyük olmasına göre ya sol yarıda ya da sağ yarıda aynı algoritma tekrarlanır.

Böyle kendisini tekrarlayarak tarif edilen algoritmalar özyinelemeli olarak da programlanabilirler. Ben de bu işlevi yukarıdaki tanımına da çok uyduğu için kendisini çağıran bir işlev olarak yazacağım.

İşlevi şablon olarak yazmak yerine, önce int için gerçekleştireceğim. Ondan sonra algoritmada kullanılan int'leri T yaparak onu bir şablona dönüştüreceğim.

/* Aranan değer dizide varsa değerin indeksini, yoksa
 * size_t.max döndürür. */
size_t ikiliAra(const int[] değerler, in int değer) {
    // Dizi boşsa bulamadık demektir.
    if (değerler.length == 0) {
        return size_t.max;
    }

    immutable ortaNokta = değerler.length / 2;

    if (değer == değerler[ortaNokta]) {
        // Bulduk.
        return ortaNokta;

    } else if (değer < değerler[ortaNokta]) {
        // Sol tarafta aramaya devam etmeliyiz
        return ikiliAra(değerler[0 .. ortaNokta], değer);

    } else {
        // Sağ tarafta aramaya devam etmeliyiz
        auto indeks =
            ikiliAra(değerler[ortaNokta + 1 .. $], değer);

        if (indeks != size_t.max) {
            // İndeksi düzeltmemiz gerekiyor çünkü bu noktada
            // indeks, sağ taraftaki dilim ile ilgili olan
            // ve sıfırdan başlayan bir değerdedir.
            indeks += ortaNokta + 1;
        }

        return indeks;
    }

    assert(false, "Bu satıra hiç gelinmemeliydi");
}

Yukarıdaki işlev bu basit algoritmayı şu dört adım halinde gerçekleştiriyor:

O işlevi deneyen bir kod da şöyle yazılabilir:

unittest {
    auto dizi = [ 1, 2, 3, 5 ];
    assert(ikiliAra(dizi, 0) == size_t.max);
    assert(ikiliAra(dizi, 1) == 0);
    assert(ikiliAra(dizi, 4) == size_t.max);
    assert(ikiliAra(dizi, 5) == 3);
    assert(ikiliAra(dizi, 6) == size_t.max);
}

O işlevi bir kere int dizileri için yazıp doğru çalıştığından emin olduktan sonra, şimdi artık bir şablon haline getirebiliriz. Dikkat ederseniz, işlevin tanımında yalnızca iki yerde int geçiyor:

size_t ikiliAra(const int[] değerler, in int değer) {
    // ... burada hiç int bulunmuyor ...
}

Parametrelerde geçen int'ler bu işlevin kullanılabildiği değerlerin türünü belirlemekteler. Onları şablon parametreleri olarak tanımlamak bu işlevin bir şablon haline gelmesi ve dolayısıyla başka türlerle de kullanılabilmesi için yeterlidir:

size_t ikiliAra(T)(const T[] değerler, in T değer) {
    // ...
}

Artık o işlevi içindeki işlemlere uyan her türle kullanabiliriz. Dikkat ederseniz, elemanlar işlev içinde yalnızca == ve < işleçleriyle kullanılıyorlar:

    if (değer == değerler[ortaNokta]) {
        // ...

    } else if (değer < değerler[ortaNokta]) {
        // ...

O yüzden, yukarıda tanımladığımız Nokta şablonu henüz bu türle kullanılmaya hazır değildir:

import std.string;

struct Nokta(T) {
    T x;
    T y;

    string toString() const {
        return format("(%s,%s)", x, y);
    }
}

void main() {
    Nokta!int[] noktalar;

    foreach (i; 0 .. 15) {
        noktalar ~= Nokta!int(i, i);
    }

    assert(ikiliAra(noktalar, Nokta!int(10, 10)) == 10);
}

Bir derleme hatası alırız:

Error: need member function opCmp() for struct
const(Nokta!(int)) to compare

O hata, Nokta!int'in bir karşılaştırma işleminde kullanılabilmesi için opCmp işlevinin tanımlanmış olması gerektiğini bildirir. Bu eksikliği gidermek için İşleç Yükleme bölümünde gösterildiği biçimde bir opCmp tanımladığımızda program artık derlenir ve ikili arama işlevi Nokta şablonu ile de kullanılabilir:

struct Nokta(T) {
// ...

    int opCmp(const ref Nokta sağdaki) const {
        return (x == sağdaki.x
                ? y - sağdaki.y
                : x - sağdaki.x);
    }
}
Özet

Şablonlar bu bölümde gösterdiklerimden çok daha kapsamlıdır. Devamını sonraya bırakarak bu bölümü şöyle özetleyebiliriz: