D.ershane D Programlama Dili Dersleri

algoritma: [algorithm], verilerin işlenme adımları, işlev
bağlı liste: [linked list], her elemanı bir sonraki elemanı gösteren veri yapısı
çıkarsama: [deduction], 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], 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ı
veri yapıları: [data structures], verilerin bilgisayar biliminin tanımladığı şekilde saklanmaları ve işlenmeleri
yükleme: [overloading], aynı isimde birden çok işlev tanımlama
... bütün sözlük

Bölümler
İ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ılabilir; ve derleyici o kod bölümlerini gereken türler, değerler, vs. için 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 derste 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.

Şablonların yararlarını anlamak için, 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ığı için, 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 geliştiğini, ve artık başka türlerden olan değerleri de parantez içinde yazdırmaya ihtiyaç duyulduğunu düşünelim. Bunun için bir çözüm, D'nin işlev yükleme olanağıdır; aynı işlevi örneğin double için de tanımlayabiliriz:

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 çıkarlar.

Örneğin ikili arama algoritması türden bağımsızdır: o algoritma, yalnızca işlemlerle ilgilidir. Aynı şekilde, ö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 parametrelerinde 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ındadır. 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 öyle bir kere yazmış olmak; 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 şekilde, her tür için farklıdır:

(42)
(1.2)
(merhaba)
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ü 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, aslında 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 bir 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. Onların derleyici tarafından çıkarsanmaları 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. Bu işlev, 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 bir şablon parametresinin belirtildiği durumlarda, bir kolaylık olarak şablon parantezleri yazılmayabilir:

    giriştenOku!int("Yaşınız?");    // üsttekiyle aynı şey

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 bir ş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ımız 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. Özelleme, şablon parametre listesinde, hangi tür için özellendiği : karakterinden sonra yazılarak belirtilir:

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

Başka bir örnek olarak, şablonu string ile kullanmayı da düşünebiliriz. Ne yazık ki şablonun genel tanımı, bu durumda dizginin 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 = chomp(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ımlanmış oldukları için, işlev şablonlarında karşılaştığımız yetersizlikler 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ının 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. Bu 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. Doğal olarak, başka türler 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ğı sunar. 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 yapısını bir yapı şablonu olarak yazmak, giriştenOku işlev şablonunun daha önce yazmış olduğumuz ö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.
  2. Okuduğumuz 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ür 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 sefer belirtilmeleri bazen gereksiz olabilir. Örneğin giriştenOku işlev şablonu programda hemen hemen her yerde int ile kullanılıyordur. Ek olarak, 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 için int varsayılır ve o ç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 dersinde 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ğu için, 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, 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 kullanılabilir.

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)
{
    T[] elemanlar_;

public:

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

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

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

    @property size_t uzunluk() const
    {
        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 onu 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);

    // Tek 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 türlerimizle de kullanmayı deneyebiliriz. Örneğin yukarıdaki Nokta şablonunun bir benzeri olsa:

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 (cast(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(int adet)
in
{
    assert(adet >= 0);
}
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 bulunan 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 bulunan değerlerin en 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 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
            // 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:

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 dersinde gösterildiği biçimde bir opCmp tanımladığımızda program artık derlenir ve ikili arama işlevi Nokta şablonu için 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 derste gösterdiklerimden çok daha kapsamlıdır. Devamını sonraya bırakarak bu dersin özeti: