Ş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:
- 42'nin yazdırıldığı satırda
int
vewchar
- 1.2'nin yazdırıldığı satırda
double
vestring
İş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(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(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 }
- Bu işlev şablonu özellemesinin
Nokta
'nın bütün kullanımlarını desteklemesi için, şablon parametre listesindeNokta!T
yazılması gerekir; bir anlamda,T
ne olursa olsun, bu özellemeninNokta!T
türleri için olduğu belirtilmektedir:Nokta!int
,Nokta!double
, vs. - Girişten okunan türe uyması için dönüş türünün de
Nokta!T
olarak belirtilmesi gerekir. - 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, yanigiriştenOku!T
şeklinde çağırmamız gerekir. - 1 ve 2 numaralı maddelere benzer şekilde, döndürdüğümüz değer de bir
Nokta!T
olmak zorundadır. - 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:
- Eleman eklemek
- Eleman çıkartmak
- Üsttekine eriştirmek
- Eleman adedini bildirmek
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 { return elemanlar[$ - 1]; } size_t uzunluk() const { return elemanlar.length; } }
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)); } do { 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); } do { 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, 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:
- Dizi boşsa bulamadığımızı bildirmek için
size_t.max
döndür. - Ortadaki değer aranan değere eşitse ortadaki değerin indeksini döndür.
- Aranan değer ortadaki değerden önceyse aynı işlevi sol tarafta devam ettir.
- Değilse aynı işlevi sağ tarafta devam ettir.
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, 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, 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:
- Şablonlar kodun kalıp halinde tarif edilmesini ve derleyici tarafından gereken her tür için otomatik olarak üretilmesini sağlayan olanaktır.
- Şablonlar bütünüyle derleme zamanında işleyen bir olanaktır.
- Tanımlarken isimlerinden sonra şablon parametresi de belirtmek; işlevlerin, yapıların, ve sınıfların şablon haline gelmeleri için yeterlidir.
void işlevŞablonu(T)(T işlevParametresi) { // ... } class SınıfŞablonu(T) { // ... }
- Şablon parametreleri ünlem işaretinden sonra açıkça belirtilebilirler. Tek parametre için parantez kullanmaya gerek yoktur.
auto nesne1 = new SınıfŞablonu!(double); auto nesne2 = new SınıfŞablonu!double; // aynı şey
- Şablonun farklı türlerle her kullanımı farklı bir türdür.
assert(typeid(SınıfŞablonu!int) != typeid(SınıfŞablonu!uint));
- Şablon parametreleri yalnızca işlev şablonlarında çıkarsanabilirler.
işlevŞablonu(42); // işlevŞablonu!int(42) çağrılır
- Şablonlar
:
karakterinden sonra belirtilen tür için özellenebilirler.class SınıfŞablonu(T : dchar) { // ... }
- Varsayılan şablon parametre türleri
=
karakterinden sonra belirtilebilir.void işlevŞablonu(T = long)(T işlevParametresi) { // ... }
pragma(msg)
şablon yazarken yararlı olabilir.