Ayrıntılı Şablonlar
Şablonların ne kadar kullanışlı olduklarını Şablonlar bölümünde görmüştük. Algoritmaların veya veri yapılarının tek tanımını yazarak birden çok türle çalışmalarını sağlayabiliyorduk.
O bölümde şablonların en çok karşılaşılan kullanımlarını göstermiştim. Bu bölümde şablon olanağını daha ayrıntılı olarak göreceğiz. Devam etmeden önce en azından o bölümün sonundaki özeti bir kere daha gözden geçirmenizi öneririm; o bölümde anlatılanları burada tekrarlamamaya çalışacağım.
Daha önce işlev, yapı, ve sınıf şablonlarını tanımıştık ve şablon parametrelerinin türler konusunda serbestlik getirdiklerini görmüştük. Bu bölümde; hem birlik ve arayüz şablonlarını da tanıyacağız; hem de şablon parametrelerinin değer, this
, alias
, ve çokuzlu çeşitleri olduğunu da göreceğiz.
Kestirme ve uzun söz dizimi
C++ gibi başka dillerde de olduğu gibi D'nin şablonları çok güçlü olanaklardır. Buna rağmen, en çok yararlanılan kullanımlarının olabildiğince rahat ve anlaşılır olmasına çalışılmıştır. İşlev, yapı, veya sınıf şablonu tanımlamak; isminden sonra şablon parametre listesi eklemek kadar kolaydır:
T ikiKatı(T)(T değer) { return 2 * değer; } class Kesirli(T) { T pay; T payda; // ... }
Daha önce de görmüş olduğunuz yukarıdaki tanımlar, D'nin kestirme şablon tanımlarıdır.
Aslında şablonlar daha uzun olarak template
anahtar sözcüğü ile tanımlanırlar. Yukarıdaki söz dizimleri, aşağıdaki tanımların kısa eşdeğerleridir:
template ikiKatı(T) { T ikiKatı(T değer) { return 2 * değer; } } template Kesirli(T) { class Kesirli { T pay; T payda; // ... } }
Derleyicinin her zaman için uzun tanımı kullandığını, ve kestirme söz dizimini arka planda şu şekilde uzun tanıma dönüştürdüğünü düşünebiliriz:
- Tanımladığımız şablonu bir
template
kapsamı içine alır. - O kapsama da aynı ismi verir.
- Şablon parametre listesini bizim tanımladığımız şablondan alır ve o kapsama verir.
Kestirme tanım; biraz aşağıda göreceğimiz tek tanım içeren şablon olanağı ile ilgilidir.
Şablon isim alanı
template
bloğu, aslında bir seferde birden çok şablon tanımlanmasına da olanak verir:
template ŞablonBloğu(T) { T birİşlev(T değer) { return değer / 3; } struct BirYapı { T üye; } }
Yukarıdaki blokta bir işlev bir de yapı şablonu tanımlamaktadır. O şablonları örneğin int
ve double
türleri için, ve uzun isimleriyle şöyle kullanabiliriz:
auto sonuç = ŞablonBloğu!int.birİşlev(42); writeln(sonuç); auto nesne = ŞablonBloğu!double.BirYapı(5.6); writeln(nesne.üye);
Şablonun belirli bir türle kullanımı bir isim alanı tanımlar. Bloğun içindeki isimler o isim alanı açıkça belirtilerek kullanılabilirler. Bu isimler fazla uzun olabileceklerinden onlara alias
bölümünde gördüğümüz alias
anahtar sözcüğü ile kısa takma isimler verilebilir:
alias KarakterYapısı = ŞablonBloğu!dchar.BirYapı; // ... auto nesne = KarakterYapısı('ğ'); writeln(nesne.üye);
Aynı isimde tanım içeren template
blokları
Şablon bloğunun ismi ile aynı isimde tanım içeren şablon blokları içerdikleri o tanımın yerine geçerler. Bu, şimdiye kadarki şablonlarda kullandığımız kestirme söz dizimini sağlayan olanaktır. (Not: Bu olanağa İngilizce'de eponymous templates denir.)
Örnek olarak, büyüklüğü 20 bayttan fazla olan türlerin büyük olarak kabul edildiği bir program olsun. Bir türün büyük olup olmadığının kararı şöyle bir şablonun içindeki bir bool
değişken ile belirlenebilir:
template büyük_mü(T) { enum büyük_mü = T.sizeof > 20; }
Dikkat ederseniz, hem şablonun hem de içindeki tanımın isimleri aynıdır. Öyle olduğunda bu uzun şablon tanımının isim alanı ve içindeki tanım açıkça büyük_mü!int.büyük_mü
diye yazılmaz, kısaca yalnızca şablonun isim alanı yazılır:
writeln(büyük_mü!int);
Yukarıdaki işaretli bölüm, şablon içindeki aynı isimli bool
yerine geçer. Yukarıdaki kod çıktıya false
yazar çünkü büyük_mü!int
, şablon içindeki bool
türündeki değişkendir ve int
'in uzunluğu 4 bayt olduğundan o bool
değişkenin değeri false
'tur.
Yukarıdaki aynı isimde tanım içeren şablon, kısa söz dizimiyle de tanımlanabilir:
enum büyük_mü(T) = T.sizeof > 20;
Aynı isimde tanım içeren şablonların yaygın bir kullanımı, türlere takma isimler vermektir. Örneğin, aşağıdaki şablon verilen türlerden büyük olanına eşdeğer olan bir alias
tanımlamaktadır:
template Büyüğü(A, B) { static if (A.sizeof < B.sizeof) { alias Büyüğü = B; } else { alias Büyüğü = A; } }
Sekiz bayttan oluşan long
türü dört bayttan oluşan int
türünden daha büyük olduğundan Büyüğü!(int, long)
, long
'un eşdeğeri olur. Bu çeşit şablonlar A
ve B
gibi türlerin kendilerinin şablon parametreleri oldukları durumlarda özellikle yararlıdırlar:
// ... /* Bu işlevin dönüş türü, şablon parametrelerinden büyük * olanıdır: Ya A ya da B. */ auto hesapla(A, B)(A a, B b) { Büyüğü!(A, B) sonuç; // ... return sonuç; } void main() { auto h = hesapla(1, 2L); static assert(is (typeof(h) == long)); }
Şablon çeşitleri
İşlev, sınıf, ve yapı şablonları
Bu alt başlığı bütünlük amacıyla yazdım.
Yukarıda da görüldüğü gibi, bu tür şablonlarla hem Şablonlar bölümünde hem de daha sonraki örneklerde çok karşılaştık.
Üye işlev şablonları
Yapı ve sınıf üye işlevleri de şablon olabilir. Örneğin, aşağıdaki ekle()
üye işlev şablonu, içindeki işlemlerle uyumlu olduğu sürece her türden değişkeni kabul eder (bu örnekteki tek şart, o değişkenin to!string
ile kullanılabilmesidir):
class Toplayıcı { string içerik; void ekle(T)(auto ref const T değer) { import std.conv; içerik ~= değer.to!string; } }
Ancak, şablonların teoride sonsuz farklı kullanımı olabileceğinden, sanal işlev olamazlar çünkü derleyici şablonun hangi kullanımlarının sınıfın arayüzüne dahil edileceğine karar veremez. (Sanal işlev olamadıklarından abstract
anahtar sözcüğü ile de tanımlanamazlar.)
Örneğin, aşağıdaki alt sınıfın ekle()
şablonu üst sınıftaki aynı isimli işlevin yeni tanımını veriyormuş gibi görünse de aslında isim gizlemeye neden olur (isim gizlemeyi alias bölümünde görmüştük):
class Toplayıcı { string içerik; void ekle(T)(auto ref const T değer) { import std.conv; içerik ~= değer.to!string; } } class KümeParantezliToplayıcı : Toplayıcı { /* Bu şablon üst sınıftakinin yeni tanımı değildir; üst * sınıftaki 'ekle' ismini gizlemektedir. */ void ekle(T)(auto ref const T değer) { import std.string; super.ekle(format("{%s}", değer)); } } void toplayıcıyıDoldur(Toplayıcı toplayıcı) { /* Aşağıdaki işlev çağrıları sanal değildir. Buradaki * 'toplayıcı' parametresinin türü 'Toplayıcı' olduğundan * her iki çağrı da Toplayıcı.ekle şablonuna * devredilirler. */ toplayıcı.ekle(42); toplayıcı.ekle("merhaba"); } void main() { auto toplayıcı = new KümeParantezliToplayıcı(); toplayıcıyıDoldur(toplayıcı); import std.stdio; writeln(toplayıcı.içerik); }
Sonuçta, asıl nesnenin türü KümeParantezliToplayıcı
olduğu halde, toplayıcıyıDoldur()
işlevinin içindeki bütün çağrılar parametresinin türü olan Toplayıcı
'ya sevk edilir. İçerik KümeParantezliToplayıcı.ekle()
işlevinin yerleştirdiği küme parantezlerini içermemektedir:
42merhaba ← KümeParantezliToplayıcı'nın işi değil
Birlik şablonları
Birlik şablonları, yapı şablonları ile aynı şekilde tanımlanırlar. Birlik şablonları için de kestirme şablon söz dizimi kullanılabilir.
Bir örnek olarak, Birlikler bölümünde tanımladığımız IpAdresi
birliğinin daha genel ve daha kullanışlı olanını tasarlamaya çalışalım. O bölümdeki birlik; değer olarak uint
türünü kullanıyordu. O değerin parçalarına erişmek için kullanılan dizinin elemanlarının türü de ubyte
idi:
union IpAdresi { uint değer; ubyte[4] baytlar; }
O birlik, hem IPv4 adresi değeri tutuyordu, hem de o değerin parçalarına ayrı ayrı erişme olanağı veriyordu.
Aynı kavramı daha genel isimler de kullanarak bir şablon halinde şöyle tanımlayabiliriz:
union ParçalıDeğer(AsılTür, ParçaTürü) { AsılTür değer; ParçaTürü[/* gereken eleman adedi */] parçalar; }
Bu birlik şablonu, asıl değerin ve alt parçalarının türünü serbestçe tanımlama olanağı verir. Asıl tür ve parça türü, birbirlerinden bağımsız olarak seçilebilirler.
Burada gereken bir işlem, parça dizisinin uzunluğunun kullanılan türlere bağlı olarak hesaplanmasıdır. IpAdresi
birliğinde, uint
'in dört adet ubyte
parçası olduğunu bildiğimiz için sabit olarak 4 yazabilmiştik. Bu şablonda ise dizinin uzunluğu, kullanılan türlere göre otomatik olarak hesaplanmalıdır.
Türlerin bayt olarak uzunluklarının .sizeof
niteliğinden öğrenilebildiğini biliyoruz. Kaç parça gerektiği bilgisini .sizeof
niteliğinden yararlanan ve kısa söz dizimine olanak veren bir şablon içinde hesaplayabiliriz:
template elemanAdedi(AsılTür, ParçaTürü) { enum elemanAdedi = (AsılTür.sizeof + (ParçaTürü.sizeof - 1)) / ParçaTürü.sizeof; }
Not: O hesaptaki (ParçaTürü.sizeof - 1)
ifadesi, türlerin uzunluklarının birbirlerine tam olarak bölünemediği durumlarda gerekir. Asıl türün 5 bayt, parça türünün 2 bayt olduğunu düşünün. Aslında 3 parça gerektiği halde o ifade eklenmediğinde 5/2 hesabının sonucu tamsayı kırpılması nedeniyle 2 çıkar.
Artık parça dizisinin eleman adedi olarak o şablonun değerini kullanabiliriz ve böylece birliğin tanımı tamamlanmış olur:
union ParçalıDeğer(AsılTür, ParçaTürü) {
AsılTür değer;
ParçaTürü[elemanAdedi!(AsılTür, ParçaTürü)] parçalar;
}
Daha önce tanımladığımız IpAdresi
birliğinin eşdeğeri olarak bu şablonu kullanmak istesek, türleri IpAdresi
'nde olduğu gibi sırasıyla uint
ve ubyte
olarak belirtmemiz gerekir:
import std.stdio; void main() { auto adres = ParçalıDeğer!(uint, ubyte)(0xc0a80102); foreach (eleman; adres.parçalar) { write(eleman, ' '); } }
Birlikler bölümünde gördüğümüz çıktının aynısını elde ederiz:
2 1 168 192
Bu şablonun getirdiği esnekliği görmek için IPv4 adresinin parçalarını iki adet ushort
olarak edinmek istediğimizi düşünelim. Bu sefer, ParçalıDeğer
şablonunun ParçaTürü
parametresi olarak ushort
yazmak yeterlidir:
auto adres = ParçalıDeğer!(uint, ushort)(0xc0a80102);
Alışık olmadığımız bir düzende olsa da, bu seferki çıktı iki ushort
'tan oluşmaktadır:
258 49320
Arayüz şablonları
Arayüz şablonları arayüzde kullanılan türler, değerler, vs. konusunda serbestlik getirirler. Arayüz şablonlarında da kestirme tanım kullanılabilir.
Örnek olarak, renkli nesnelerin arayüzünü tanımlayan ama renk olarak hangi türün kullanılacağını serbest bırakan bir arayüz tasarlayalım:
interface RenkliNesne(RenkTürü) { void renklendir(RenkTürü renk); }
O arayüz, kendisinden türeyen sınıfların renklendir
işlevini tanımlamalarını gerektirir; ama renk olarak ne tür kullanılacağı konusunu serbest bırakır.
Bir sitedeki bir çerçeveyi temsil eden bir sınıf; renk olarak kırmızı, yeşil, ve maviden oluşan üçlü bir yapı kullanabilir:
struct KırmızıYeşilMavi { ubyte kırmızı; ubyte yeşil; ubyte mavi; } class SiteÇerçevesi : RenkliNesne!KırmızıYeşilMavi { void renklendir(KırmızıYeşilMavi renk) { // ... } }
Öte yandan, renk olarak ışığın frekansını kullanmak isteyen bir sınıf, renk için frekans değerine uygun olan başka bir türden yararlanabilir:
alias Frekans = double; class Lamba : RenkliNesne!Frekans { void renklendir(Frekans renk) { // ... } }
Yine Şablonlar bölümünden hatırlayacağınız gibi, "her şablon gerçekleştirmesi farklı bir türdür". Buna göre, RenkliNesne!KırmızıYeşilMavi
ve RenkliNesne!Frekans
arayüzleri, farklı arayüzlerdir. Bu yüzden, onlardan türeyen sınıflar da birbirlerinden bağımsız sıradüzenlerin parçalarıdırlar; SiteÇerçevesi
ve Lamba
, birbirlerinden bağımsızdır.
Şablon parametre çeşitleri
Şimdiye kadar gördüğümüz şablonlar, hep türler konusunda serbestlik getiriyorlardı.
Yukarıdaki örneklerde de kullandığımız T
ve RenkTürü
gibi şablon parametreleri, hep türleri temsil ediyorlardı. Örneğin T
'nin anlamı, şablonun kod içindeki kullanımına bağlı olarak int
, double
, Öğrenci
, vs. gibi bir tür olabiliyordu.
Şablon parametreleri; değer, this
, alias
, ve çokuzlu da olabilirler.
Tür parametreleri
Bu alt başlığı bütünlük amacıyla yazdım.
Şimdiye kadar gördüğümüz bütün şablon parametreleri zaten hep tür parametreleriydi.
Değer parametreleri
Şablon parametresi olarak değerler de kullanılabilir. Bu, şablonun tanımı ile ilgili bir değerin serbest bırakılmasını sağlar.
Şablonlar derleme zamanı olanakları olduklarından, değer olarak kullanılan şablon parametresinin derleme zamanında hesaplanabilmesi şarttır. Bu yüzden, programın çalışması sırasında hesaplanan, örneğin girişten okunan bir değer kullanılamaz.
Bir örnek olarak, belirli sayıda köşeden oluşan şekilleri temsil eden yapılar tanımlayalım:
struct Üçgen { Nokta[3] köşeler; // ... } struct Dörtgen { Nokta[4] köşeler; // ... } struct Beşgen { Nokta[5] köşeler; // ... }
Örnek kısa olsun diye başka üyelerini göstermedim. Normalde, o türlerin başka üyelerinin ve işlevlerinin de bulunduğunu ve hepsinde tamamen aynı şekilde tanımlandıklarını varsayalım. Sonuçta, dizi uzunluğunu belirleyen değer dışında, o yapıların tanımları aynı olsun.
Değer şablon parametreleri böyle durumlarda yararlıdır. Yukarıdaki tanımlar yerine tek yapı şablonu tanımlanabilir. Yeni tanım genel amaçlı olduğu için, ismini de o şekillerin genel ismi olan poligon koyarak şöyle tanımlayabiliriz:
struct Poligon(size_t köşeAdedi) { Nokta[köşeAdedi] köşeler; // ... }
O yapı şablonu parametre olarak size_t
türünde ve köşeAdedi
isminde bir şablon parametresi almaktadır. O parametre değeri yapının tanımında herhangi bir yerde kullanılabilir.
Artık o şablonu istediğimiz sayıda köşesi olan poligonları ifade etmek için kullanabiliriz:
auto yüzKöşeli = Poligon!100();
Yine alias
'tan yararlanarak kullanışlı isimler verebiliriz:
alias Üçgen = Poligon!3; alias Dörtgen = Poligon!4; alias Beşgen = Poligon!5; // ... auto üçgen = Üçgen(); auto dörtgen = Dörtgen(); auto beşgen = Beşgen();
Yukarıdaki değer şablon parametresinin türü size_t
idi. Değer derleme zamanında bilindiği sürece değer türü olarak bütün temel türler, yapılar, diziler, dizgiler, vs. kullanılabilir.
struct S { int i; } // Türü S yapısı olan değer şablon parametresi void foo(S s)() { // ... } void main() { // İşlev şablonunun S(42) hazır değeriyle kullanılması foo!(S(42))(); }
Başka bir örnek olarak, basit XML elemanları oluşturmakta kullanılan bir sınıf şablonu tasarlayalım. Bu basit XML tanımı, çok basitçe şu çıktıyı üretmek için kullanılsın:
- Önce
<
>
karakterleri arasında elemanın ismi:<isim>
- Sonra elemanın değeri
- En sonunda da
</
>
karakterleri arasında yine elemanın ismi:</isim>
Örneğin değeri 42 olan bir elemanın <isim>42</isim>
şeklinde görünmesini isteyelim.
Eleman isimlerini bir sınıf şablonunun string
türündeki bir değer parametresi olarak belirleyebiliriz:
import std.string; class XmlElemanı(string isim) { double değer; this(double değer) { this.değer = değer; } override string toString() const { return format("<%s>%s</%s>", isim, değer, isim); } }
Bu örnekteki şablon parametresi, şablonda kullanılan bir türle değil, bir string
değeriyle ilgilidir. O string
'in değeri de şablon içinde gereken her yerde kullanılabilir.
alias
'tan yararlanarak kullanışlı tür isimleri de tanımlayarak:
alias Konum = XmlElemanı!"konum"; alias Sıcaklık = XmlElemanı!"sıcaklık"; alias Ağırlık = XmlElemanı!"ağırlık"; void main() { Object[] elemanlar; elemanlar ~= new Konum(1); elemanlar ~= new Sıcaklık(23); elemanlar ~= new Ağırlık(78); writeln(elemanlar); }
Not: Ben bu örnekte kısa olsun diye ve nasıl olsa bütün sınıf sıradüzenlerinin en üstünde bulunduğu için bir Object
dizisi kullandım. O sınıf şablonu aslında daha uygun bir arayüz sınıfından da türetilebilirdi.
Yukarıdaki kodun çıktısı:
[<konum>1</konum>, <sıcaklık>23</sıcaklık>, <ağırlık>78</ağırlık>]
Değer parametrelerinin de varsayılan değerleri olabilir. Örneğin, herhangi boyutlu bir uzaydaki noktaları temsil eden bir yapı tasarlayalım. Noktaların koordinat değerleri için kullanılan tür ve uzayın kaç boyutlu olduğu, şablon parametreleri ile belirlensin:
struct Konum(T, size_t boyut = 3) { T[boyut] koordinatlar; }
boyut
parametresinin varsayılan bir değerinin bulunması, bu şablonun o parametre belirtilmeden de kullanılabilmesini sağlar:
Konum!double merkez; // üç boyutlu uzayda bir nokta
Gerektiğinde farklı bir değer de belirtilebilir:
Konum!(int, 2) nokta; // iki boyutlu düzlemde bir nokta
Parametre Serbestliği bölümünde özel anahtar sözcüklerin varsayılan parametre değeri olarak kullanıldıklarında farklı etkileri olduğunu görmüştük.
Benzer biçimde, varsayılan şablon parametre değeri olarak kullanıldıklarında şablonun tanımlandığı yerle değil, şablonun kullanıldığı yerle ilgili bilgi verirler:
import std.stdio; void işlev(T, string işlevİsmi = __FUNCTION__, string dosya = __FILE__, size_t satır = __LINE__)(T parametre) { writefln("%s dosyasının %s numaralı satırındaki %s " ~ "işlevi tarafından kullanılıyor.", dosya, satır, işlevİsmi); } void main() { işlev(42); // ← satır 13 }
Yukarıdaki özel anahtar sözcükler şablonun tanımında geçtikleri halde şablonu kullanmakta olan main()
işlevine işaret ederler:
deneme.d dosyasının 13 numaralı satırındaki deneme.main işlevi tarafından kullanılıyor.
__FUNCTION__
anahtar sözcüğünü aşağıdaki işleç yükleme örneğinde de kullanacağız.
Üye işlevler için this
şablon parametreleri
Üye işlevler de şablon olarak tanımlanabilirler. Üye işlev şablonlarının da tür ve değer parametreleri bulunabilir, ve normal işlev şablonlarından beklendiği gibi çalışırlar.
Ek olarak, üye işlev şablonlarının parametreleri this
anahtar sözcüğü ile de tanımlanabilir. Bu durumda, o anahtar sözcükten sonra yazılan isim, o nesnenin this
referansının türü haline gelir. (Not: Burada, çoğunlukla kurucu işlevler içinde gördüğümüz this.üye = değer
kullanımındaki this
referansından, yani nesnenin kendisini ifade eden referanstan bahsediyoruz.)
struct BirYapı(T) { void birİşlev(this KendiTürüm)() const { writeln("Bu nesnenin türü: ", KendiTürüm.stringof); } }
KendiTürüm
şablon parametresi o üye işlevin işlemekte olduğu nesnenin asıl türüdür:
auto m = BirYapı!int(); auto c = const(BirYapı!int)(); auto i = immutable(BirYapı!int)(); m.birİşlev(); c.birİşlev(); i.birİşlev();
Çıktısı:
Bu nesnenin türü: BirYapı!int Bu nesnenin türü: const(BirYapı!int) Bu nesnenin türü: immutable(BirYapı!int)
Görüldüğü gibi, KendiTürüm
hem T
'nin bu kullanımda int
olan karşılığını hem de const
ve immutable
gibi tür belirteçlerini içerir.
this
şablon parametresi, şablon olmayan yapıların veya sınıfların üye işlevlerinde de kullanılabilir.
this
şablon parametreleri özellikle iki bölüm sonra göreceğimiz şablon katmalarında yararlıdır. O bölümde bir örneğini göreceğiz.
alias
parametreleri
alias
şablon parametrelerine karşılık olarak D programlarında geçebilen bütün yasal isimler veya ifadeler kullanılabilir. Bu isimler yerel isimler, modül isimleri, başka şablon isimleri, vs. olabilirler. Tek koşul, o parametrenin şablon içindeki kullanımının o parametreye uygun olmasıdır.
Bu olanak, filter
ve map
gibi şablonların da işlemleri dışarıdan almalarını sağlayan olanaktır.
Örnek olarak, hangi yerel değişkeni değiştireceği kendisine bir alias
parametre olarak bildirilen bir yapıya bakalım:
struct BirYapı(alias değişken) { void birİşlev(int değer) { değişken = değer; } }
O yapının üye işlevi, değişken
isminde bir değişkene bir atama yapmaktadır. O değişkenin programdaki hangi değişken olduğu; bu şablon tanımlandığı zaman değil, şablon kullanıldığı zaman belirlenir:
int x = 1; int y = 2; auto nesne = BirYapı!x(); nesne.birİşlev(10); writeln("x: ", x, ", y: ", y);
Yapı şablonunun yukarıdaki kullanımında yerel x
değişkeni belirtildiği için, birİşlev
içindeki atama onu etkiler:
x: 10, y: 2
Öte yandan, BirYapı!y
biçimindeki kullanımda değişken
değişkeni y
yerine geçerdi.
Başka bir örnek olarak, filter
ve map
gibi alias
parametresini işlev olarak kullanan bir işlev şablonuna bakalım:
void çağıran(alias işlev)() { write("çağırıyorum: "); işlev(); }
()
parantezlerinden anlaşıldığı gibi, çağıran
ismindeki işlev şablonu, kendisine verilen parametreyi bir işlev olarak kullanmaktadır. Ayrıca, parantezlerin içinin boş olmasından anlaşıldığı gibi, o işlev parametre göndermeden çağrılmaktadır.
Parametre almadıkları için o kullanıma uyan iki de işlev bulunduğunu varsayalım:
void birinci() { writeln("birinci"); } void ikinci() { writeln("ikinci"); }
O işlevler, çağıran
şablonu içindeki kullanıma uydukları için o şablonun alias
parametresinin değeri olabilirler:
çağıran!birinci(); çağıran!ikinci();
Belirtilen işlevin çağrıldığını görürüz:
çağırıyorum: birinci çağırıyorum: ikinci
alias
şablon parametrelerini her çeşit şablonla kullanabilirsiniz. Önemli olan, o parametrenin şablon içindeki kullanıma uymasıdır. Örneğin, yukarıdaki alias
parametresi yerine bir değişken kullanılması derleme hatasına neden olacaktır:
int değişken; çağıran!değişken(); // ← derleme HATASI
Aldığımız hata, ()
karakterlerinden önce bir işlev beklendiğini, int
türündeki değişken
'in uygun olmadığını belirtir:
Error: function expected before (), not değişken of type int
Her ne kadar işaretlediğim satır nedeniyle olsa da, derleme hatası aslında çağıran
işlevinin içindeki işlev()
satırı için verilir. Derleyicinin gözünde hatalı olan; şablona gönderilen parametre değil, o parametrenin şablondaki kullanılışıdır. Uygunsuz şablon parametrelerini önlemenin bir yolu, şablon kısıtlamaları tanımlamaktır. Bunu aşağıda göreceğiz.
Öte yandan, bir işlev gibi çağrılabilen her olanak bu örnekteki alias
parametresi yerine kullanılabilir. Aşağıda hem opCall()
işlecini yüklemiş olan bir sınıf ile hem de bir isimsiz işlev ile kullanımını görüyoruz:
class Sınıf { void opCall() { writeln("Sınıf.opCall çağrıldı."); } } // ... auto nesne = new Sınıf(); çağıran!nesne(); çağıran!({ writeln("İsimsiz işlev çağrıldı."); })();
Çıktısı:
çağırıyorum: Sınıf.opCall çağrıldı. çağırıyorum: İsimsiz işlev çağrıldı.
alias
parametreleri de özellenebilirler. Ancak, özelleme söz dizimi diğer parametre çeşitlerinden farklıdır; özellenen tür alias
ile parametre ismi arasına yazılır:
import std.stdio; void foo(alias değişken)() { writefln("Genel tanım %s türündeki '%s' değişkeni için işliyor.", typeof(değişken).stringof, değişken.stringof); } void foo(alias int i)() { writefln("int özellemesi '%s' değişkeni için işliyor.", i.stringof); } void foo(alias double d)() { writefln("double özellemesi '%s' değişkeni için işliyor.", d.stringof); } void main() { string isim; foo!isim(); int adet; foo!adet(); double uzunluk; foo!uzunluk(); }
Asıl değişkenlerin isimlerinin de şablon içinde görülebildiklerine ayrıca dikkat edin:
Genel tanım string türündeki 'isim' değişkeni için işliyor. int özellemesi 'adet' değişkeni için işliyor. double özellemesi 'uzunluk' değişkeni için işliyor.
Çokuzlu parametreleri
İşlevlerin belirsiz sayıda parametre alabildiklerini biliyoruz. Örneğin, writeln
işlevini istediğimiz sayıda parametre ile çağırabiliriz. Bu tür işlevlerin nasıl tanımlandıklarını Parametre Serbestliği bölümünde görmüştük.
Aynı serbestlik şablon parametrelerinde de bulunur. Şablon parametrelerinin sayısını ve çeşitlerini serbest bırakmak şablon parametre listesinin en sonuna bir çokuzlu ismi ve ...
karakterleri yazmak kadar basittir. İsmi belirtilen çokuzlu, şablon parametre değerlerini ifade eden bir AliasSeq
gibi kullanılabilir.
Bunu parametreleri hakkında bilgi veren basit bir işlev şablonunda görelim:
void bilgiVer(T...)(T parametreler) { // ... }
Şablon parametresi olan T...
, bilgiVer
işlev şablonunun belirsiz sayıda parametre ile çağrılabilmesini sağlar. Hem T
hem de parametreler
çokuzludur:
T
, işlev parametre değerlerinin türlerinden oluşan çokuzludur.parametreler
, işlev parametre değerlerinden oluşan çokuzludur.
İşlevin üç farklı türden parametre ile çağrıldığı duruma bakalım:
import std.stdio; // ... void main() { bilgiVer(1, "abc", 2.3); }
Aşağıda parametreler
'in foreach
ile kullanımını görüyoruz:
void bilgiVer(T...)(T parametreler) { // 'parametreler' bir AliasSeq gibi kullanılır: foreach (i, parametre; parametreler) { writefln("%s: %s türünde %s", i, typeof(parametre).stringof, parametre); } }
Not: Bir önceki bölümde gördüğümüz gibi, parametre değerleri çokuzlu olduğundan, yukarıdaki foreach
bir derleme zamanı foreach
'idir.
Çıktısı:
0: int türünde 1 1: string türünde abc 2: double türünde 2.3
Parametrelerin türleri typeof(parametre)
yerine T[i]
ile de edinilebilir.
İşlev şablonu parametre türlerinin derleyici tarafından çıkarsanabildiklerini biliyorsunuz. Yukarıdaki bilgiVer()
çağrısı sırasında parametre değerlerine bakılarak onların türlerinin sırasıyla int
, string
, ve double
oldukları derleyici tarafından çıkarsanmıştır.
Bazı durumlarda ise şablon parametrelerinin açıkça belirtilmeleri gerekebilir. Örneğin, std.conv.to
şablonu hedef türü açıkça belirtilmesi gereken bir şablon parametresi olarak alır:
to!string(42);
Şablon parametreleri açıkça belirtildiğinde o parametreler değer, tür, veya başka çeşitlerden karışık olabilirler. Öyle durumlarda her şablon parametresinin tür veya başka çeşitten olup olmadığının belirlenmesi ve şablon kodlarının buna uygun olarak yazılması gerekebilir. Şablon çeşitlerini ayırt etmenin yolu, parametreleri yine AliasSeq
gibi kullanmaktır.
Bunun örneğini görmek için yapı tanımı üreten bir işlev tasarlayalım. Bu işlev belirtilen türlerde ve isimlerde üyeleri olan yapı tanımı içeren kaynak kod üretsin ve string
olarak döndürsün. İlk olarak yapının ismi verildikten sonra üyelerin tür ve isimleri çiftler halinde belirtilsinler:
import std.stdio; void main() { writeln(yapıTanımla!("Öğrenci", string, "isim", int, "numara", int[], "notlar")()); }
Yukarıdaki programın çıktısı aşağıdaki kaynak kod olmalıdır:
struct Öğrenci { string isim; int numara; int[] notlar; }
Not: yapıTanımla
gibi kod üreten işlevlerin yararlarını daha sonraki bir bölümdeki mixin
anahtar sözcüğünü öğrenirken göreceğiz.
O sonucu üreten şablon aşağıdaki gibi tanımlanabilir. Koddaki denetimlerde is
ifadesinden de yararlanıldığına dikkat edin. Hatırlarsanız, is (parametre)
ifadesi parametre
geçerli bir tür olduğunda true
üretiyordu:
import std.string; string yapıTanımla(string isim, Üyeler...)() { /* Üyeler tür ve isim olarak çiftler halinde * belirtilmelidir. Önce bunu denetleyelim. */ static assert((Üyeler.length % 2) == 0, "Üyeler çiftler halinde belirtilmedilir."); /* Önce yapı tanımının ilk satırını oluşturuyoruz. */ string sonuç = "struct " ~ isim ~ "\n{\n"; foreach (i, parametre; Üyeler) { static if (i % 2) { /* Tek numaralı parametreler üye isimlerini * belirliyorlar. Onları hemen burada kullanmak * yerine aşağıdaki 'else' bölümünde Üyeler[i+1] * söz dizimiyle kullanacağız. * * Yine de üye isimlerinin string olarak * belirtildiklerini burada denetleyelim. */ static assert(is (typeof(parametre) == string), "Üye ismi " ~ parametre.stringof ~ " string değil."); } else { /* Bu durumda 'parametre' üyenin türünü * belirtiyor. Öncelikle bu parametrenin gerçekten * bir tür olduğunu denetleyelim. */ static assert(is (parametre), parametre.stringof ~ " tür değil."); /* Tür ve üye isimlerini kullanarak satırı * oluşturuyoruz. * * Not: Burada Üyeler[i] yerine 'parametre' de * yazabilirdik. */ sonuç ~= format(" %s %s;\n", Üyeler[i].stringof, Üyeler[i+1]); } } /* Yapı tanımının kapama parantezi. */ sonuç ~= "}"; return sonuç; } import std.stdio; void main() { writeln(yapıTanımla!("Öğrenci", string, "isim", int, "numara", int[], "notlar")()); }
typeof(this)
, typeof(super)
, ve typeof(return)
Şablonların genel tanımlar ve genel algoritmalar olmalarının bir etkisi, bazı tür isimlerinin yazımlarının güç veya olanaksız olmasıdır. Aşağıdaki üç özel typeof
çeşidi böyle durumlarda yararlıdır. Her ne kadar bu bölümde tanıtılıyor olsalar da, bu olanaklar şablon olmayan kodlarda da geçerlidirler.
typeof(this)
, yapının veya sınıfınthis
referansının türünü (yani, kendi tam türünü) verir. Bu olanak yapı veya sınıfın tanımı içinde olmak koşuluyla üye işlevler dışında da kullanılabilir.struct Liste(T) { // T int olduğunda 'sonraki'nin türü Liste!int'tir typeof(this) *sonraki; // ... }
typeof(super)
bir sınıfın üst sınıfının türünü (yani,super
referansının türünü) verir.class ListeOrtak(T) { // ... } class Liste(T) : ListeOrtak!T { // T int olduğunda 'sonraki'nin türü ListeOrtak!int'tir typeof(super) *sonraki; // ... }
typeof(return)
bir işlevin dönüş türünü o işlev içerisindeyken verir.Örneğin, yukarıdaki
hesapla()
işlevinin dönüş türünüauto
yerine daha açıklayıcı olmak içinBüyüğü!(A, B)
olarak tanımlayabiliriz. (Daha açık olmanın bir yararı, işlev açıklamalarının en azından bir bölümünün gereksiz hale gelmesidir.)Büyüğü!(A, B) hesapla(A, B)(A a, B b) { // ... }
typeof(return)
, dönüş türünün işlevin içinde tekrarlanmasını önler:Büyüğü!(A, B) hesapla(A, B)(A a, B b) { typeof(return) sonuç; // Ya A ya da B // ... return sonuç; }
Şablon özellemeleri
Özellemeleri de Şablonlar bölümünde görmüştük. Aşağıdaki meta programlama başlığında da özelleme örnekleri göreceksiniz.
Tür parametrelerinde olduğu gibi, başka çeşit şablon parametreleri de özellenebilir. Aşağıdaki şablonun hem genel hem de 0 değeri için özel tanımı görülüyor:
void birİşlev(int birDeğer)() { // ... genel tanımı ... } void birİşlev(int birDeğer : 0)() { // ... sıfıra özel tanımı ... }
Meta programlama
Kod üretme ile ilgili olduklarından şablonlar diğer D olanaklarından daha üst düzey programlama araçları olarak kabul edilirler. Şablonlar bir anlamda kod oluşturan kodlardır. Kodların daha üst düzey kodlarla oluşturulmaları kavramına meta programlama denir.
Şablonların derleme zamanı olanakları olmaları normalde çalışma zamanında yapılan işlemlerin derleme zamanına taşınmalarına olanak verir. (Not: Aynı amaçla Derleme Zamanında İşlev İşletme (CTFE) olanağı da kullanılabilir. Bu konuyu daha sonraki bir bölümde göreceğiz.)
Şablonların bu amaçla derleme zamanında işletilmeleri, çoğunlukla özyineleme üzerine kuruludur.
Bunun bir örneğini görmek için 0'dan başlayarak belirli bir sayıya kadar olan bütün sayıların toplamını hesaplayan normal bir işlev düşünelim. Bu işlev, parametre olarak örneğin 4 aldığında 0+1+2+3+4'ün toplamını döndürsün:
int topla(int sonDeğer) { int sonuç = 0; foreach (değer; 0 .. sonDeğer + 1) { sonuç += değer; } return sonuç; }
Aynı işlevi özyinelemeli olarak da yazabiliriz:
int topla(int sonDeğer) { return (sonDeğer == 0 ? sonDeğer : sonDeğer + topla(sonDeğer - 1)); }
Özyinelemeli işlev kendi düzeyindeki değeri bir önceki hesaba eklemektedir. İşlevde 0 değerinin özel olarak kullanıldığını görüyorsunuz; özyineleme onun sayesinde sonlanmaktadır.
İşlevlerin normalde çalışma zamanı olanakları olduklarını biliyoruz. topla
'yı çalışma zamanında gerektikçe çağırabilir ve sonucunu kullanabiliriz:
writeln(topla(4));
Aynı sonucun yalnızca derleme zamanında gerektiği durumlarda ise, o hesap bir işlev şablonuyla da gerçekleştirilebilir. Yapılması gereken; değerin işlev parametresi olarak değil, şablon parametresi olarak kullanılmasıdır:
// Uyarı: Bu kod yanlıştır int topla(int sonDeğer)() { return (sonDeğer == 0 ? sonDeğer : sonDeğer + topla!(sonDeğer - 1)()); }
Bu şablon da hesap sırasında kendisinden yararlanmaktadır. Kendisini, sonDeğer
'in bir eksiği ile kullanmakta ve hesabı yine özyinelemeli olarak elde etmeye çalışmaktadır. Ne yazık ki o kod yazıldığı şekilde çalışamaz.
Derleyici, ?:
işlecini çalışma zamanında işleteceği için, yukarıdaki özyineleme derleme zamanında sonlanamaz:
writeln(topla!4()); // ← derleme HATASI
Derleyici, aynı şablonun sonsuz kere dallandığını anlar ve bir hata ile sonlanır:
Error: template instance deneme.topla!(-296) recursive expansion
Şablon parametresi olarak verdiğimiz 4'ten geriye doğru -296'ya kadar saydığına bakılırsa, derleyici şablonların özyineleme sayısını 300 ile sınırlamaktadır.
Meta programlamada özyinelemeyi kırmanın yolu, şablon özellemeleri kullanmaktır. Bu durumda, aynı şablonu 0 değeri için özelleyebilir ve özyinelemenin kırılmasını bu sayede sağlayabiliriz:
// Genel tanım int topla(int sonDeğer)() { return sonDeğer + topla!(sonDeğer - 1)(); } // Sıfır değeri için özellemesi int topla(int sonDeğer : 0)() { return 0; }
Derleyici, sonDeğer
'in sıfırdan farklı değerleri için hep genel tanımı kullanır ve en sonunda 0 değeri için özel tanıma geçer. O tanım da basitçe 0 değerini döndürdüğü için özyineleme sonlanmış olur.
O işlev şablonunu şöyle bir programla deneyebiliriz:
import std.stdio; void main() { writeln(topla!4()); }
Şimdi hatasız olarak derlenecek ve 4+3+2+1+0'ın toplamını üretecektir:
10
Burada dikkatinizi çekmek istediğim önemli nokta, topla!4()
işlevinin bütünüyle derleme zamanında işletiliyor olmasıdır. Sonuçta derleyicinin ürettiği kod, writeln
'e doğrudan 10 hazır değerini göndermenin eşdeğeridir:
writeln(10); // topla!4()'lü kodun eşdeğeri
Derleyicinin ürettiği kod, 10 hazır değerini doğrudan programa yazmak kadar hızlı ve basittir. O 10 hazır değeri, yine de 4+3+2+1+0 hesabının sonucu olarak bulunmaktadır; ancak o hesap, şablonların özyinelemeli olarak kullanılmalarının sonucunda derleme zamanında işletilmektedir.
Burada görüldüğü gibi, meta programlamanın yararlarından birisi, şablonların derleme zamanında işletilmelerinden yararlanarak normalde çalışma zamanında yapılmasına alıştığımız hesapların derleme zamanına taşınabilmesidir.
Yukarıda da söylediğim gibi, daha sonraki bir bölümde göstereceğim CTFE olanağı bazı meta programlama yöntemlerini D'de gereksiz hale getirir.
Derleme zamanı çok şekilliliği
Bu kavram, İngilizce'de "compile time polymorphism" olarak geçer.
Nesne yönelimli programlamada çok şekilliliğin sınıf türetme ile sağlandığını biliyorsunuz. Örneğin bir işlev parametresinin bir arayüz olması, o parametre yerine o arayüzden türemiş her sınıfın kullanılabileceği anlamına gelir.
Daha önce gördüğümüz bir örneği hatırlayalım:
import std.stdio; interface SesliAlet { string ses(); } class Keman : SesliAlet { string ses() { return "♩♪♪"; } } class Çan : SesliAlet { string ses() { return "çın"; } } void sesliAletKullan(SesliAlet alet) { // ... bazı işlemler ... writeln(alet.ses()); // ... başka işlemler ... } void main() { sesliAletKullan(new Keman); sesliAletKullan(new Çan); }
Yukarıdaki sesliAletKullan
işlevi çok şekillilikten yararlanmaktadır. Parametresi SesliAlet
olduğu için, ondan türemiş olan bütün türlerle kullanılabilir.
Yukarıdaki son cümlede geçen bütün türlerle kullanılabilme kavramını şablonlardan da tanıyoruz. Böyle düşününce, şablonların da bir çeşit çok şekillilik sunduklarını görürüz. Şablonlar bütünüyle derleyicinin derleme zamanındaki kod üretmesiyle ilgili olduklarından, şablonların sundukları çok şekilliliğe derleme zamanı çok şekilliliği denir.
Doğrusu, her iki çok şekillilik de bütün türlerle kullanılamaz. Her ikisinde de türlerin uymaları gereken bazı koşullar vardır.
Çalışma zamanı çok şekilliliği, belirli bir arayüzden türeme ile kısıtlıdır.
Derleme zamanı çok şekilliliği ise şablon içindeki kullanıma uyma ile kısıtlıdır. Şablon parametresi, şablon içindeki kullanımda derleme hatasına neden olmuyorsa, o şablonla kullanılabilir. (Not: Eğer tanımlanmışsa, şablon kısıtlamalarına da uyması gerekir. Şablon kısıtlamalarını biraz aşağıda anlatacağım.)
Örneğin, yukarıdaki sesliAletKullan
işlevi bir şablon olarak yazıldığında, ses
üye işlevi bulunan bütün türlerle kullanılabilir:
void sesliAletKullan(T)(T alet) { // ... bazı işlemler ... writeln(alet.ses()); // ... başka işlemler ... } class Araba { string ses() { return "düt düt"; } } // ... sesliAletKullan(new Keman); sesliAletKullan(new Çan); sesliAletKullan(new Araba);
Yukarıdaki şablon, diğerleriyle kalıtım ilişkisi bulunmayan Araba
türü ile de kullanılabilmiştir.
Derleme zamanı çok şekilliliği ördek tipleme olarak da bilinir. Ördek tipleme, asıl türü değil, davranışı ön plana çıkartan mizahi bir terimdir.
Kod şişmesi
Şablonlar kod üretme ile ilgilidirler. Derleyici, şablonun farklı parametrelerle her kullanımı için farklı kod üretir.
Örneğin yukarıda en son yazdığımız sesliAletKullan
işlev şablonu, programda kullanıldığı her tür için ayrı ayrı üretilir ve derlenir. Programda yüz farklı tür ile çağrıldığını düşünürsek; derleyici o işlev şablonunun tanımını, her tür için ayrı ayrı, toplam yüz kere oluşturacaktır.
Programın boyutunun büyümesine neden olduğu için bu etkiye kod şişmesi (code bloat) denir. Çoğu programda sorun oluşturmasa da, şablonların bu özelliğinin akılda tutulması gerekir.
Öte yandan, sesliAletKullan
işlevinin ilk yazdığımız SesliAlet
alan tanımında, yani şablon olmayan tanımında, böyle bir kod tekrarı yoktur. Derleyici, o işlevi bir kere derler ve her SesliAlet
türü için aynı işlevi çağırır. İşlev tek olduğu halde her hayvanın kendisine özel olarak davranabilmesi, derleyici tarafından işlev göstergeleriyle sağlanır. Derleyici her tür için farklı bir işlev göstergesi kullanır ve böylece her tür için farklı üye işlev çağrılır. Çalışma zamanında çok küçük bir hız kaybına yol açsa da, işlev göstergeleri kullanmanın çoğu programda önemi yoktur ve zaten bu çözümü sunan en hızlı gerçekleştirmedir.
Burada sözünü ettiğim hız etkilerini tasarımlarınızda fazla ön planda tutmayın. Program boyutunun artması da, çalışma zamanında fazladan işlemler yapılması da hızı düşürecektir. Belirli bir programda hangisinin etkisinin daha fazla olduğuna ancak o program denenerek karar verilebilir.
Şablon kısıtlamaları
Şablonların her tür ve değerdeki şablon parametresi ile çağrılabiliyor olmalarının getirdiği bir sorun vardır. Uyumsuz bir parametre kullanıldığında, bu uyumsuzluk ancak şablonun kendi kodları derlenirken farkedilebilir. Bu yüzden, derleme hatasında belirtilen satır numarası, şablon bloğuna işaret eder.
Yukarıdaki sesliAletKullan
şablonunu ses
isminde üye işlevi bulunmayan bir türle çağıralım:
class Fincan { // ... ses() işlevi yok ... } // ... sesliAletKullan(new Fincan); // ← uyumsuz bir tür
Oradaki hata, şablonun uyumsuz bir türle çağrılıyor olmasıdır. Oysa derleme hatası, şablon içindeki kullanıma işaret eder:
void sesliAletKullan(T)(T alet) { // ... bazı işlemler ... writeln(alet.ses()); // ← derleme HATASI // ... başka işlemler ... }
Bunun bir sakıncası, belki de bir kütüphane modülünde tanımlı olan bir şablona işaret edilmesinin, hatanın o kütüphanede olduğu yanılgısını uyandırabileceğidir. Daha önemlisi, asıl hatanın hangi satırda olduğunun hiç bildirilmiyor olmasıdır.
Böyle bir sorunun arayüzlerde bulunmadığına dikkat edin. Parametre olarak arayüz alacak şekilde yazılmış olan bir işlev, ancak o arayüzden türemiş olan türlerle çağrılabilir. Türeyen her tür arayüzün işlevlerini gerçekleştirmek zorunda olduğu için, işlevin uyumsuz bir türle çağrılması olanaksızdır. O durumda derleme hatası, işlevi uygunsuz türle çağıran satıra işaret eder.
Şablonların yalnızca belirli koşulları sağlayan türlerle kullanılmaları şablon kısıtlamaları ile sağlanır. Şablon kısıtlamaları, şablon bloğunun hemen öncesine yazılan if
deyiminin içindeki mantıksal ifadelerdir:
void birŞablon(T)() if (/* ... kısıtlama koşulu ... */) { // ... şablonun tanımı ... }
Derleyici bu şablon tanımını ancak kısıtlama koşulu true
olduğunda göze alır. Koşulun false
olduğu durumda ise bu şablon tanımını gözardı eder.
Şablonlar derleme zamanı olanakları olduklarından şablon kısıtlamaları da derleme zamanında işletilirler. Bu yüzden, is
İfadesi bölümünde gördüğümüz ve derleme zamanında işletildiğini öğrendiğimiz is
ifadesi ile de çok kullanılırlar. Bunun örneklerini aşağıda göstereceğim.
Tek üyeli çokuzlu parametre yöntemi
Bazen tek şablon parametresi gerekebilir ama o parametrenin tür, değer, veya alias
çeşidinden olabilmesi istenir. Bunu sağlamanın bir yolu, çokuzlu çeşidinde parametre kullanmak ama çokuzlunun uzunluğunu bir şablon kısıtlaması ile tek olarak belirlemektir:
template birŞablon(T...) if (T.length == 1) { static if (is (T[0])) { // Şablonun tek parametresi bir türmüş enum bool birŞablon = /* ... */; } else { // Şablonun tek parametresi tür değilmiş enum bool birŞablon = /* ... */; } }
Daha ileride göreceğimiz std.traits
modülündeki bazı şablonlar bu yöntemden yararlanır.
İsimli kısıtlama yöntemi
Şablon kısıtlamaları bazı durumlarda yukarıdakinden çok daha karmaşık olabilirler. Bunun üstesinden gelmenin bir yolu, benim isimli kısıtlama olarak adlandırdığım bir yöntemdir. Bu yöntem D'nin dört olanağından yararlanarak kısıtlamaya anlaşılır bir isim verir. Bu dört olanak; isimsiz işlev, typeof
, is
ifadesi, ve tek tanım içeren şablonlardır.
Bu yöntemi burada daha çok bir kalıp olarak göstereceğim ve her ayrıntısına girmemeye çalışacağım.
Parametresini belirli şekilde kullanan bir işlev şablonu olsun:
void kullan(T)(T nesne) { // ... nesne.hazırlan(); // ... nesne.uç(42); // ... nesne.kon(); // ... }
Şablon içindeki kullanımından anlaşıldığı gibi, bu şablonun kullanıldığı türlerin hazırlan
, uç
, ve kon
isminde üç üye işlevinin bulunması gerekir (UFCS olanağı sayesinde normal işlevler de olabilirler.) O işlevlerden uç
'un ayrıca int
türünde bir de parametresi olmalıdır.
Bu kısıtlamayı is
ve typeof
ifadelerinden yararlanarak şöyle yazabiliriz:
void kullan(T)(T nesne) if (is (typeof(nesne.hazırlan())) && is (typeof(nesne.uç(1))) && is (typeof(nesne.kon()))) { // ... }
O koşulun anlamını aşağıda daha ayrıntılı olarak göreceğiz. Şimdilik is (typeof(nesne.hazırlan()))
kullanımını bir kalıp olarak eğer o tür nesne.hazırlan()
çağrısını destekliyorsa anlamında kabul edebilirsiniz. İşleve is (typeof(nesne.uç(1)))
biçiminde parametre verildiğinde ise, o işlev int
türünde parametre de alıyorsa diye kabul edebilirsiniz.
Yukarıdaki gibi bir kısıtlama istendiği gibi çalışıyor olsa da, her zaman için tam açık olmayabilir. Onun yerine, o şablon kısıtlamasının ne anlama geldiğini daha iyi açıklayan bir isim verilebilir:
void kullan(T)(T nesne) if (uçabilir_mi!T) { // ← isimli kısıtlama // ... }
Bu kısıtlama bir öncekinden daha açıktır. Bu şablonun uçabilen türlerle çalıştığını okunaklı bir şekilde belgeler.
Yukarıdaki gibi isimli kısıtlamalar şu kalıba uygun olarak tanımlanırlar:
template uçabilir_mi(T) { enum uçabilir_mi = is (typeof( { T uçan; uçan.hazırlan(); // uçmaya hazırlanabilmeli uçan.uç(1); // belirli mesafe uçabilmeli uçan.kon(); // istendiğinde konabilmeli }())); }
O yöntemde kullanılan D olanaklarını ve birbirleriyle nasıl etkileştiklerini çok kısaca göstermek istiyorum:
template uçabilir_mi(T) { // (6) (5) (4) enum uçabilir_mi = is (typeof( { // (1) T uçan; // (2) uçan.hazırlan(); uçan.uç(1); uçan.kon(); // (3) }())); }
- İsimsiz işlev: İsimsiz işlevleri İşlev Göstergeleri, İsimsiz İşlevler, ve Temsilciler bölümünde görmüştük. İşaretli olarak gösterilmiş olan yukarıdaki blok parantezleri, isimsiz bir işlev tanımlar.
- İşlev bloğu: İşlev bloğu, kısıtlaması tanımlanmakta olan türü asıl şablonda kullanıldığı gibi kullanır. Yukarıdaki blokta önce bu türden bir nesne oluşturulmakta ve o türün sahip olması gereken üç üye işlevi çağrılmaktadır. (Not: Bu kodlar
typeof
tarafından kullanılırlar ama hiçbir zaman işletilmezler.) - İşlevin işletilmesi: Bir işlevin sonuna yazılan
()
parantezleri normalde o işlevi işletir. Ancak, yukarıdaki işletme birtypeof
içinde olduğundan bu işlev hiçbir zaman işletilmez. (Bu, bir sonraki maddede açıklanıyor.) -
typeof
ifadesi:typeof
, şimdiye kadarki örneklerde çok kullandığımız gibi, kendisine verilen ifadenin türünü üretir.typeof
'un önemli bir özelliği, türünü ürettiği ifadeyi işletmemesidir.typeof
, bir ifadenin eğer işletilse ne türden bir değer üreteceğini bildirir:int i = 42; typeof(++i) j; // 'int j;' yazmakla aynı anlamdadır assert(i == 42); // ++i işletilmemiştir
Yukarıdaki
assert
'ten de anlaşıldığı gibi,++i
ifadesi işletilmemiştir.typeof
, yalnızca o ifadenin türünü üretmiş ve böylecej
deint
olarak tanımlanmıştır.Eğer
typeof
'a verilen ifadenin geçerli bir türü yoksa,typeof
void
bile olmayan geçersiz bir tür döndürür.Eğer
uçabilir_mi
şablonuna gönderilen tür, o isimsiz işlev içindeki kodlarda gösterildiği gibi derlenebiliyorsa,typeof
geçerli bir tür üretir. Eğer o tür işlev içindeki kodlardaki gibi derlenemiyorsa,typeof
geçersiz bir tür döndürür. is
ifadesi:is
İfadesi bölümündeis
ifadesinin birden fazla kullanımını görmüştük. Buradakiis (Tür)
şeklindeki kullanımı, kendisine verilen türün anlamlı olduğu durumdatrue
değerini üretir:int i; writeln(is (typeof(i))); // true writeln(is (typeof(varOlmayanBirİsim))); // false
Yukarıdaki ikinci ifadede bilinmeyen bir isim kullanıldığı halde derleyici hata vermez. Programın çıktısı ikinci satır için
false
değerini içerir:true false
Bunun nedeni,
typeof
'un ikinci kullanım için geçersiz bir tür üretmiş olmasıdır.- Tek tanım içeren şablon: Daha yukarıda anlatıldığı gibi,
uçabilir_mi
şablonunun içinde tek tanım bulunduğundan ve o tanımın ismi şablonun ismiyle aynı olduğundan;uçabilir_mi
şablonu, içerdiğiuçabilir_mi
enum
sabit değeri yerine geçer.
Sonuçta, yukarıdaki kullan
işlev şablonu bütün bu olanaklar sayesinde isimli bir kısıtlama edinmiş olur:
void kullan(T)(T nesne) if (uçabilir_mi!T) { // ... }
O şablonu birisi uyumlu, diğeri uyumsuz iki türle çağırmayı deneyelim:
// Şablondaki kullanıma uyan bir tür class ModelUçak { void hazırlan() { } void uç(int mesafe) { } void kon() { } } // Şablondaki kullanıma uymayan bir tür class Güvercin { void uç(int mesafe) { } } // ... kullan(new ModelUçak); // ← derlenir kullan(new Güvercin); // ← derleme HATASI
İsimli veya isimsiz, bir şablon kısıtlaması tanımlanmış olduğundan, bu derleme hatası artık şablonun içine değil şablonun uyumsuz türle kullanıldığı satıra işaret eder.
Şablonların çok boyutlu işleç yüklemedeki kullanımı
opDollar
, opIndex
, ve opSlice
işlevlerinin eleman erişimi ve dilimleme amacıyla kullanıldıklarını İşleç Yükleme bölümünde görmüştük. Bu işlevler o bölümdeki gibi tek boyutlu kullanımlarında aşağıdaki görevleri üstlenirler:
opDollar
: Topluluktaki eleman adedini döndürür.opSlice
: Topluluğun ya bütün elemanlarını ya da bir bölümünü ifade eden aralık nesnesi döndürür.opIndex
: Belirtilen elemana erişim sağlar.
O işlevlerin şablon olarak yüklenebilen çeşitleri de vardır. Bu işlev şablonlarının anlamları yukarıdakilerden farklıdır. Özellikle opSlice
'ın görevinin opIndex
tarafından üstlenilmiş olduğuna dikkat edin:
-
opDollar
şablonu: Topluluğun belirli bir boyutunun uzunluğunu döndürür. Hangi boyutun uzunluğunun döndürüleceği şablon parametresinden anlaşılır:size_t opDollar(size_t boyut)() const { // ... }
-
opSlice
şablonu: Dilimi belirleyen sayı aralığı bilgisini döndürür. (Örneğin,dizi[baş..son]
yazımındakibaş
veson
değerleri.) Bu bilgiTuple!(size_t, size_t)
veya eşdeğeri bir tür olarak döndürülebilir. Aralığın hangi boyutla ilgili olduğu şablon parametresinden anlaşılır:Tuple!(size_t, size_t) opSlice(size_t boyut)(size_t baş, size_t son) { return tuple(baş, son); }
-
opIndex
şablonu: Belirtilen alt topluluğu ifade eden bir aralık döndürür. Aralığın sınırları şablonun çokuzlu parametrelerinden anlaşılır:Aralık opIndex(A...)(A parametreler) { // ... }
opIndexAssign
ve opIndexOpAssign
'ın da şablon çeşitleri vardır. Bunlar da belirli bir alt topluluktaki elemanlar üzerinde işlerler.
Çok boyutlu işleçleri tanımlayan türler aşağıdaki gibi çok boyutlu erişim ve dilimleme söz dizimlerinde kullanılabilirler:
// İndekslerle belirlenen alt topluluktaki // elemanların değerlerini 42 yapar: m[a, b..c, $-1, d..e] = 42; // ↑ ↑ ↑ ↑ // boyutlar: 0 1 2 3
Öyle bir ifade görüldüğünde önce $
karakterleri için opDollar
ve konum aralıkları için opSlice
perde arkasında otomatik olarak çağrılır. Elde edilen uzunluk ve aralık bilgileri yine otomatik olarak opIndexAssign
'a parametre olarak geçirilir. Sonuçta, yukarıdaki ifade yerine aşağıdaki ifade işletilmiş olur (boyut değerleri işaretlenmiş olarak gösteriliyor):
// Üsttekinin eşdeğeri: m.opIndexAssign( 42, // ← atanan değer a, // ← sıfırıncı boyutun parametresi m.opSlice!1(b, c), // ← birinci boyutun parametresi m.opDollar!2() - 1, // ← ikinci boyutun parametresi m.opSlice!3(d, e)); // ← üçüncü boyutun parametresi
Sonuçta, opIndexAssign
işlemde kullanacağı alt aralığı çokuzlu şablon parametrelerinin türlerine ve değerlerine bakarak belirler.
İşleç yükleme örneği
Aşağıdaki Matris
türü bu işleçlerin nasıl tanımlandıklarının bir örneğini içeriyor.
Bu örnek çok daha hızlı işleyecek biçimde de gerçekleştirilebilir. Örneğin, aşağıdaki kodun tek elemana m[i, j]
biçiminde erişirken bile tek elemanlı bir alt matris oluşturması gereksiz kabul edilebilir.
Ek olarak, işlev başlarındaki writeln(__FUNCTION__)
ifadelerinin kodun işlevselliğiyle bir ilgisi bulunmuyor. Onlar yalnızca perde arkasında hangi işlevlerin hangi sırada çağrıldıklarını göstermek amacıyla eklenmişlerdir.
Boyut değerlerini denetlemek için şablon kısıtlamalarından yararlanıldığına da dikkat edin.
import std.stdio; import std.format; import std.string; /* İki boyutlu bir int dizisi gibi işler. */ struct Matris { private: int[][] satırlar; /* İndekslerle belirlenen satır ve sütun aralığı bilgisini * bir araya getirir. */ struct Aralık { size_t baş; size_t son; } /* Satır ve sütun aralıklarıyla belirlenen alt matrisi * döndürür. */ Matris altMatris(Aralık satırAralığı, Aralık sütunAralığı) { writeln(__FUNCTION__); int[][] dilimler; foreach (satır; satırlar[satırAralığı.baş .. satırAralığı.son]) { dilimler ~= satır[sütunAralığı.baş .. sütunAralığı.son]; } return Matris(dilimler); } public: this(size_t yükseklik, size_t genişlik) { writeln(__FUNCTION__); satırlar = new int[][](yükseklik, genişlik); } this(int[][] satırlar) { writeln(__FUNCTION__); this.satırlar = satırlar; } void toString(void delegate(const(char)[]) hedef) const { hedef.formattedWrite!"%(%(%5s %)\n%)"(satırlar); } /* Belirtilen değeri matrisin bütün elemanlarına atar. */ Matris opAssign(int değer) { writeln(__FUNCTION__); foreach (satır; satırlar) { satır[] = değer; } return this; } /* Belirtilen işleci ve değeri her elemana uygular ve * sonucu o elemana atar. */ Matris opOpAssign(string işleç)(int değer) { writeln(__FUNCTION__); foreach (satır; satırlar) { mixin ("satır[] " ~ işleç ~ "= değer;"); } return this; } /* Belirtilen boyutun uzunluğunu döndürür. */ size_t opDollar(size_t boyut)() const if (boyut <= 1) { writeln(__FUNCTION__); static if (boyut == 0) { /* Sıfırıncı boyutun uzunluğu isteniyor; * 'satırlar' dizisinin uzunluğudur. */ return satırlar.length; } else { /* Birinci boyutun uzunluğu isteniyor; 'satırlar' * dizisinin elemanlarının uzunluğudur. */ return satırlar.length ? satırlar[0].length : 0; } } /* 'baş' ve 'son' ile belirlenen aralığı ifade eden bir * nesne döndürür. * * Not: Bu gerçekleştirmede 'boyut' parametresi * kullanılmıyor olsa da, bu bilgi başka bir tür için * yararlı olabilir. */ Aralık opSlice(size_t boyut)(size_t baş, size_t son) if (boyut <= 1) { writeln(__FUNCTION__); return Aralık(baş, son); } /* Parametrelerle belirlenen alt matrisi döndürür. */ Matris opIndex(A...)(A parametreler) if (A.length <= 2) { writeln(__FUNCTION__); /* Bütün elemanları temsil eden aralıklarla * başlıyoruz. Böylece opIndex'in parametresiz * kullanımında bütün elemanlar kapsanırlar. */ Aralık[2] aralıklar = [ Aralık(0, opDollar!0), Aralık(0, opDollar!1) ]; foreach (boyut, p; parametreler) { static if (is (typeof(p) == Aralık)) { /* Bu boyut için 'matris[baş..son]' gibi * aralık belirtilmiş; parametreyi olduğu gibi * aralık olarak kullanabiliriz. */ aralıklar[boyut] = p; } else static if (is (typeof(p) : size_t)) { /* Bu boyut için 'matris[i]' gibi tek konum * değeri belirtilmiş; kullanmadan önce tek * uzunluklu aralık oluşturmak gerekiyor. */ aralıklar[boyut] = Aralık(p, p + 1); } else { /* Bu işlevin başka bir türle çağrılmasını * beklemiyoruz. */ static assert( false, format("Geçersiz indeks türü: %s", typeof(p).stringof)); } } /* 'parametreler'in karşılık geldiği alt matrisi * döndürüyoruz. */ return altMatris(aralıklar[0], aralıklar[1]); } /* Belirtilen değeri belirtilen elemanlara atar. */ Matris opIndexAssign(A...)(int değer, A parametreler) if (A.length <= 2) { writeln(__FUNCTION__); Matris altMatris = opIndex(parametreler); return altMatris = değer; } /* Belirtilen işleci ve değeri belirtilen elemanlara * uygular ve sonuçları yine aynı elemanlara atar. */ Matris opIndexOpAssign(string işleç, A...)(int değer, A parametreler) if (A.length <= 2) { writeln(__FUNCTION__); Matris altMatris = opIndex(parametreler); mixin ("return altMatris " ~ işleç ~ "= değer;"); } } /* Dizgi halinde belirtilen ifadeyi işletir ve hem işlemin * sonucunu hem de matrisin yeni durumunu yazdırır. */ void işlet(string ifade)(Matris m) { writefln("\n--- %s ---", ifade); mixin ("auto sonuç = " ~ ifade ~ ";"); writefln("sonuç:\n%s", sonuç); writefln("m:\n%s", m); } void main() { enum yükseklik = 10; enum genişlik = 8; auto m = Matris(yükseklik, genişlik); int sayaç = 0; foreach (satır; 0 .. yükseklik) { foreach (sütun; 0 .. genişlik) { writefln("%s / %s ilkleniyor", sayaç + 1, yükseklik * genişlik); m[satır, sütun] = sayaç; ++sayaç; } } writeln(m); işlet!("m[1, 1] = 42")(m); işlet!("m[0, 1 .. $] = 43")(m); işlet!("m[0 .. $, 3] = 44")(m); işlet!("m[$-4 .. $-1, $-4 .. $-1] = 7")(m); işlet!("m[1, 1] *= 2")(m); işlet!("m[0, 1 .. $] *= 4")(m); işlet!("m[0 .. $, 0] *= 10")(m); işlet!("m[$-4 .. $-2, $-4 .. $-2] -= 666")(m); işlet!("m[1, 1]")(m); işlet!("m[2, 0 .. $]")(m); işlet!("m[0 .. $, 2]")(m); işlet!("m[0 .. $ / 2, 0 .. $ / 2]")(m); işlet!("++m[1..3, 1..3]")(m); işlet!("--m[2..5, 2..5]")(m); işlet!("m[]")(m); işlet!("m[] = 20")(m); işlet!("m[] /= 4")(m); işlet!("(m[] += 5) /= 10")(m); }
Özet
Önceki şablonlar bölümünün sonunda şunları hatırlatmıştım:
- Ş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.
- Şablon parametreleri ünlem işaretinden sonra açıkça belirtilebilirler. Tek parametre için parantez kullanmaya gerek yoktur.
- Şablonun farklı türlerle her kullanımı farklı bir türdür.
- Şablon parametreleri yalnızca işlev şablonlarında çıkarsanabilirler.
- Şablonlar
:
karakterinden sonra belirtilen tür için özellenebilirler. - Varsayılan şablon parametre türleri
=
karakterinden sonra belirtilebilir.
Bu bölümde de şu kavramları gördük:
- Şablonlar kestirme veya uzun söz dizimleriyle tanımlanabilirler.
- Şablon kapsamı bir isim alanı belirler.
- İçinde bir tanımla aynı isime sahip olan şablon o tanım yerine geçer.
- İşlev, sınıf, yapı, birlik, ve arayüz şablonları tanımlanabildiği gibi, bu tanımlar şablon kapsamı içinde karışık olarak bulunabilirler.
- Şablon parametrelerinin tür, değer,
this
,alias
, ve çokuzlu çeşitleri vardır. - Şablonlar parametrelerinin herhangi bir kullanımı için özellenebilirler.
typeof(this)
,typeof(super)
, vetypeof(return)
tür yazımlarında kolaylık sağlarlar.- Meta programlama, işlemlerin derleme zamanında yapılmalarını sağlar.
- Şablonlar derleme zamanı çok şekilliliği olanaklarıdır.
- Şablonun her farklı parametreli kullanımı için ayrı kod üretilmesi kod şişmesine neden olabilir.
- Olası derleme hatalarının şablonun yanlış kullanıldığı satıra işaret edebilmesi için şablon kısıtlamaları tanımlanabilir.
- İsimli kısıtlama yöntemi kısıtlamalara okunaklı isimler vermeye yarar.
opDollar
,opSlice
,opIndex
,opIndexAssign
, veopIndexOpAssign
işlevlerinin şablon çeşitleri çok boyutlu eleman erişimine ve dilimlemeye olanak sağlar.