İşlev Göstergeleri, İsimsiz İşlevler, ve Temsilciler
İşlev göstergeleri işlevlerin adreslerinin saklanabilmelerini ve daha sonraki bir zamanda bu göstergeler yoluyla çağrılabilmelerini sağlarlar. İşlev göstergeleri D'ye C'den geçmiştir.
Temsilciler hem işlev göstergelerini hem de o işlevlerin kullandıkları kapsamları bir arada saklayan olanaklardır. Saklanan kapsam o temsilcinin içinde oluşturulduğu ortam olabileceği gibi, bir yapı veya sınıf nesnesinin kendisi de olabilir.
Temsilciler çoğu fonksiyonel dilde bulunan kapama olanağını da gerçekleştirirler.
İşlev göstergeleri
Bundan önceki bölümde is
ifadesini denerken &
işleci ile işlevlerin adreslerinin de alınabildiğini görmüştük. O adresi bir işlev şablonuna parametre olarak göndermiştik.
Şablonların çeşitli türlerle çağrılabilmelerinden ve türlerin .stringof
niteliğinden yararlanarak, işlev göstergelerinin türleri hakkında bilgi edinebiliriz:
import std.stdio; int işlev(char c, double d) { return 42; } void main() { şablon(&işlev); // adresinin alınması ve // parametre olarak gönderilmesi } void şablon(T)(T parametre) { writeln("türü : ", T.stringof); writeln("değeri: ", parametre); }
O program çalıştırıldığında, işlev
isimli işlevin adresinin türü konusunda bir fikir sahibi olabiliyoruz:
türü : int function(char c, double d) değeri: 80495B4
Üye işlev göstergeleri
Üye işlevlerin adresleri hem doğrudan tür üzerinden hem de o türün bir nesnesi üzerinden alınabilir. Bu iki yöntemin etkisi farklıdır:
struct Yapı { void işlev() { } } void main() { auto nesne = Yapı(); auto f = &Yapı.işlev; // tür üzerinden auto d = &nesne.işlev; // nesne üzerinden static assert(is (typeof(f) == void function())); static assert(is (typeof(d) == void delegate())); }
Yukarıdaki static assert
satırlarından da görüldüğü gibi, f
bir function
, d
ise bir delegate
'tir. Daha aşağıda göreceğimiz gibi, d
doğrudan çağrılabilir ama f
'nin çağrılabilmesi için önce hangi nesne üzerinde çağrılacağının da belirtilmesi gerekir.
Tanımlanması
İşlev göstergeleri function
anahtar sözcüğü ile tanımlanır. Bu sözcükten önce işlevin dönüş türü, sonra da işlevin aldığı parametreler yazılır:
dönüş_türü function(aldığı_parametreler) gösterge;
Bu tanımda parametrelere isim verilmesi gerekmez; yukarıdaki çıktıda gördüğümüz parametre isimleri olan c
'nin ve d
'nin yazılmaları isteğe bağlıdır. Bir örnek olarak, yukarıdakı işlev
isimli işlevi gösteren bir değişkeni şöyle tanımlayabiliriz:
int function(char, double) gösterge = &işlev;
İşlev göstergelerinin yazımı oldukça karmaşık olduğundan o türe alias
ile yeni bir isim vermek kodun okunaklılığını arttırır:
alias Hesapİşlevi = int function(char, double);
Artık function
'lı uzun yazım yerine kısaca Hesapİşlevi
yazmak yeterlidir.
Hesapİşlevi gösterge = &işlev;
auto
'dan da yararlanılabilir:
auto gösterge = &işlev;
Çağrılması
İşlev göstergesi olarak tanımlanan değişken, sanki kendisi bir işlevmiş gibi isminden sonraki parametre listesiyle çağrılır ve dönüş değeri kullanılabilir:
int sonuç = gösterge('a', 5.67); assert(sonuç == 42);
Yukarıdaki çağrı, işlevin kendi ismiyle işlev('a', 5.67)
olarak çağrılmasının eşdeğeridir.
Ne zaman kullanmalı
İşlev göstergeleri değerlerin saklanmalarına benzer şekilde, işlemlerin de saklanabilmelerini sağlar. Saklanan göstergeler programda daha sonradan işlev gibi kullanılabilirler. Bir anlamda, daha sonradan uygulanacak olan davranışları saklarlar.
Aslında davranış farklılıklarının D'nin başka olanakları ile de sağlanabildiğini biliyorsunuz. Örneğin Çalışan
gibi bir yapının ücretinin hesaplanması sırasında hangi işlevin çağrılacağı, bu yapının bir enum
değeri ile belirlenebilir:
final switch (çalışan.tür) { case ÇalışanTürü.maaşlı: maaşlıÜcretHesabı(); break; case ÇalışanTürü.saatli: saatliÜcretHesabı(); break; }
O yöntemin bir yetersizliği, o kod bir kütüphane içinde bulunduğu zaman ortaya çıkar: Bütün enum
değerlerinin ve onlara karşılık gelen bütün işlevlerin kütüphane kodu yazıldığı sırada biliniyor olması gerekmektedir. Farklı bir ücret hesabı gerektiğinde, kütüphane içindeki ilgili switch
deyimlerinin hepsinin yeni türü de içerecek şekilde değiştirilmeleri gerekir.
Davranış farkı konusunda başka bir yöntem, nesne yönelimli programlama olanaklarından yararlanmak olabilir. Çalışan
diye bir arayüz tanımlanabilir ve ücret hesabı ondan türeyen alt sınıflara yaptırılabilir:
interface Çalışan { double ücretHesabı(); } class MaaşlıÇalışan : Çalışan { double ücretHesabı() { double sonuç; // ... return sonuç; } } class SaatliÇalışan : Çalışan { double ücretHesabı() { double sonuç; // ... return sonuç; } } // ... double ücret = çalışan.ücretHesabı();
Bu, nesne yönelimli programlama dillerine uygun olan yöntemdir.
İşlev göstergeleri, davranış farklılığı konusunda kullanılan başkaca bir yöntemdir. İşlev göstergeleri, nesne yönelimli olanakları bulunmayan C dilinde yazılmış olan kütüphanelerde görülebilirler.
Parametre örneği
Kendisine verilen bir dizi sayı ile işlem yapan bir işlev tasarlayalım. Bu işlev, sayıların yalnızca sıfırdan büyük olanlarının on katlarını içeren bir dizi döndürsün:
int[] süz_ve_dönüştür(const int[] sayılar) { int[] sonuç; foreach (sayı; sayılar) { if (sayı > 0) { // süzme, immutable yeniDeğer = sayı * 10; // ve dönüştürme sonuç ~= yeniDeğer; } } return sonuç; }
O işlevi şöyle bir programla deneyebiliriz:
import std.stdio; import std.random; void main() { int[] sayılar; // Rasgele 20 sayı foreach (i; 0 .. 20) { sayılar ~= uniform(0, 10) - 5; } writeln("giriş: ", sayılar); writeln("sonuç: ", süz_ve_dönüştür(sayılar)); }
Çıktısından görüldüğü gibi, sonuç yalnızca sıfırdan büyük olanların on katlarını içermektedir:
giriş: -2 0 3 2 4 -3 2 -4 4 2 2 4 2 1 -2 -1 0 2 -2 4 sonuç: 30 20 40 20 40 20 20 40 20 10 20 40
süz_ve_dönüştür
işlevinin bu haliyle fazla kullanışlı olduğunu düşünemeyiz çünkü her zaman için sıfırdan büyük değerlerin on katlarını üretmektedir. Oysa süzme ve dönüştürme işlemlerini nasıl uygulayacağını dışarıdan alabilse çok daha kullanışlı olabilir.
Dikkat ederseniz, süzme işlemi int
'ten bool
'a bir dönüşüm, sayı dönüştürme işlemi de int
'ten yine int
'e bir dönüşümdür:
sayı > 0
,int
olan sayıya bakarakbool
sonuç elde ediyor.sayı * 10
,int
olan sayı kullanarak yineint
üretiyor.
Bu işlemleri işlev göstergeleri yoluyla yapmaya geçmeden önce, bu dönüşümleri sağlayacak olan işlev gösterge türlerini şöyle tanımlayabiliriz:
alias Süzmeİşlemi = bool function(int); // int'ten bool alias Dönüşümİşlemi = int function(int); // int'ten int
Süzmeİşlemi
, "int alan ve bool döndüren" işlev göstergesi, Dönüşümİşlemi
de "int alan ve int döndüren" işlev göstergesi anlamındadır.
Bu türlerden olan işlev göstergelerini süz_ve_dönüştür
işlevine dışarıdan parametre olarak verirsek süzme ve dönüştürme işlemlerini o işlev göstergelerine yaptırabiliriz. Böylece işlev daha kullanışlı hale gelir:
int[] süz_ve_dönüştür(const int[] sayılar, Süzmeİşlemi süzücü, Dönüşümİşlemi dönüştürücü) { int[] sonuç; foreach (sayı; sayılar) { if (süzücü(sayı)) { immutable yeniDeğer = dönüştürücü(sayı); sonuç ~= yeniDeğer; } } return sonuç; }
Bu işlev artık asıl süzme ve dönüştürme işlemlerinden bağımsız bir hale gelmiştir çünkü o işlemleri kendisine verilen işlev göstergelerine yaptırmaktadır. Yukarıdaki gibi sıfırdan büyük olanlarının on katlarını üretebilmesi için şöyle iki küçük işlev tanımlayabiliriz ve süz_ve_dönüştür
işlevini onların adresleri ile çağırabiliriz:
bool sıfırdanBüyük_mü(int sayı) { return sayı > 0; } int onKatı(int sayı) { return sayı * 10; } // ... writeln("sonuç: ", süz_ve_dönüştür(sayılar, &sıfırdanBüyük_mü, &onKatı));
Bunun yararı, süz_ve_dönüştür
işlevinin artık bambaşka süzücü ve dönüştürücü işlevleriyle de serbestçe çağrılacak hale gelmiş olmasıdır. Örneğin çift olanlarının ters işaretlileri şöyle elde edilebilir:
bool çift_mi(int sayı) { return (sayı % 2) == 0; } int tersİşaretlisi(int sayı) { return -sayı; } // ... writeln("sonuç: ", süz_ve_dönüştür(sayılar, &çift_mi, &tersİşaretlisi));
Çıktısı:
giriş: 2 -3 -3 -2 4 4 3 1 4 3 -4 -1 -2 1 1 -5 0 2 -3 2 sonuç: -2 2 -4 -4 -4 4 2 0 -2 -2
İşlevler çift_mi
ve tersİşaretlisi
gibi çok kısa olduklarında başlı başlarına tanımlanmaları gerekmeyebilir. Bunun nasıl gerçekleştirildiğini biraz aşağıda İsimsiz işlevler ve özellikle onların =>
söz dizimini tanırken göreceğiz:
writeln("sonuç: ", süz_ve_dönüştür(sayılar,
sayı => (sayı % 2) == 0,
sayı => -sayı));
Üye örneği
İşlev göstergeleri değişken olarak kullanılabildikleri için yapı ve sınıf üyeleri de olabilirler. Yukarıdaki süz_ve_dönüştür
işlevi yerine, süzme ve dönüştürme işlemlerini kurucu parametreleri olarak alan bir sınıf da yazılabilir:
class SüzücüDönüştürücü { Süzmeİşlemi süzücü; Dönüşümİşlemi dönüştürücü; this(Süzmeİşlemi süzücü, Dönüşümİşlemi dönüştürücü) { this.süzücü = süzücü; this.dönüştürücü = dönüştürücü; } int[] işlemYap(const int[] sayılar) { int[] sonuç; foreach (sayı; sayılar) { if (süzücü(sayı)) { immutable yeniDeğer = dönüştürücü(sayı); sonuç ~= yeniDeğer; } } return sonuç; } }
Daha sonra o türden bir nesne oluşturulabilir ve yukarıdaki sonuçların aynıları şöyle elde edilebilir:
auto işlemci = new SüzücüDönüştürücü(&çift_mi, &tersİşaretlisi); writeln("sonuç: ", işlemci.işlemYap(sayılar));
İsimsiz işlevler
Yukarıdaki örnek programlarda süz_ve_dönüştür
işlevinin esnekliğinden yararlanmak için küçük işlevler tanımlandığını ve süz_ve_dönüştür
çağrılırken o küçük işlevlerin adreslerinin gönderildiğini gördük.
Yukarıdaki örneklerde de görüldüğü gibi, işlevin asıl işi az olduğunda başlı başına işlevler tanımlamak külfetli olabilir. Örneğin, sayı > 0
ve sayı * 10
oldukça basit ve küçük işlemlerdir.
İşlev hazır değeri olarak da adlandırabileceğimiz isimsiz işlev olanağı (lambda), başka ifadelerin arasında küçük işlevler tanımlamaya yarar. İsimsiz işlevler işlev göstergesi kullanılabilen her yerde şu söz dizimiyle tanımlanabilirler:
function dönüş_türü(parametreleri) { /* ... işlemleri ... */ }
Örneğin, yukarıdaki örnekte tanımladığımız sınıftan olan bir nesneyi ikiden büyük olanlarının yedi katlarını üretecek şekilde şöyle kullanabiliriz:
new SüzücüDönüştürücü( function bool(int sayı) { return sayı > 2; }, function int(int sayı) { return sayı * 7; });
Böylece, hem bu kadar küçük işlemler için ayrıca işlevler tanımlamak zorunda kalmamış oluruz hem de istediğimiz davranışı tam da gereken noktada belirtmiş oluruz.
Yukarıdaki söz dizimlerinin normal işlevlere ne kadar benzediğine dikkat edin. Normal işlevlerle isimsiz işlevlerin söz dizimlerinin bu derece yakın olmaları kolaylık olarak kabul edilebilir. Öte yandan, bu ağır söz dizimi isimsiz işlevlerin kullanım amaçlarıyla hâlâ çelişmektedir çünkü isimsiz işlevler özellikle kısa işlemleri kolayca tanımlama amacını taşırlar.
Bu yüzden isimsiz işlevler çeşitli kısa söz dizimleri ile de tanımlanabilirler.
Kısa söz dizimi
İsimsiz işlevlerin yazımlarında bazı kolaylıklar da vardır. İşlevin dönüş türünün return
satırından anlaşılabildiği durumlarda dönüş türü yazılmayabilir:
new SüzücüDönüştürücü( function (int sayı) { return sayı > 2; }, function (int sayı) { return sayı * 7; });
İsimsiz işlevin parametre almadığı durumlarda da parametre listesi yazılmayabilir. Bunu görmek için işlev göstergesi alan bir işlev düşünelim:
void birİşlev(/* ... işlev göstergesi alsın ... */) { // ... }
O işlevin aldığı parametre, double
döndüren ama parametre almayan bir işlev göstergesi olsun:
void birİşlev(double function() gösterge) { // ... }
O parametrenin tanımındaki function
'dan sonraki parantezin boş olması, o göstergenin parametre almayan bir işlev göstergesi olduğunu ifade eder. Böyle bir durumda, isimsiz işlevin oluşturulduğu noktada boş parantez yazmaya da gerek yoktur. Şu üç isimsiz işlev tanımı birbirlerinin eşdeğeridir:
birİşlev(function double() { return 42.42; }); birİşlev(function () { return 42.42; }); // üsttekiyle aynı birİşlev(function { return 42.42; }); // üsttekiyle aynı
Birincisi hiçbir kısaltmaya başvurmadan yazılmıştır. İkincisi dönüş türünün return
satırından çıkarsanmasından yararlanmıştır. Üçüncüsü de gereksiz olan boş parametre listesini de yazmamıştır.
Bir adım daha atılabilir ve function
da yazılmayabilir. O zaman bunun isimsiz bir işlev mi yoksa isimsiz bir temsilci mi olduğuna derleyici karar verir. Oluşturulduğu ortamdaki değişkenleri kullanıyorsa temsilcidir, kullanmıyorsa function
'dır:
birİşlev({ return 42.42; }); // bu durumda 'function' çıkarsanır
Bazı isimsiz işlevler =>
söz dizimiyle daha da kısa yazılabilirler.
Tek return
ifadesi yerine =>
söz dizimi
Yukarıdaki en kısa söz dizimi bile gereğinden fazla karmaşık olarak görülebilir. İşlevin parametre listesinin hemen içindeki küme parantezleri okumayı güçleştirmektedirler. Üstelik çoğu isimsiz işlev tek return
deyiminden oluşur. Öyle durumlarda ne return
anahtar sözcüğüne gerek olmalıdır ne de sonundaki noktalı virgüle. D'nin isimsiz işlevlerinin en kısa söz dizimi başka dillerde de bulunan =>
ile sağlanır.
Yalnızca tek return
deyimi içeren bir isimsiz işlevin söz dizimini hatırlayalım:
function dönüş_türü(parametreler) { return ifade; }
function
anahtar sözcüğünün ve dönüş türünün belirtilmelerinin gerekmediğini yukarıda görmüştük:
(parametreler) { return ifade; }
Aynı isimsiz işlev =>
ile çok daha kısa olarak şöyle tanımlanabilir:
(parametreler) => ifade
Yukarıdaki söz diziminin anlamı, "o parametreler verildiğinde şu ifadeyi (değeri) üret" olarak açıklanabilir.
Dahası, yalnızca tek parametre bulunduğunda etrafındaki parantezler de yazılmayabilir:
tek_parametre => ifade
Buna rağmen, D'nin gramerinin bir gereği olarak hiç parametre bulunmadığında parametre listesinin boş olarak verilmesi şarttır:
() => ifade
İsimsiz işlevleri başka dillerden tanıyan programcılar =>
karakterlerinden sonra küme parantezleri yazma hatasına düşebilirler. O söz dizimi başka bir anlam taşır:
// 'a + 1' döndüren isimsiz işlev auto l0 = (int a) => a + 1 // 'a + 1' döndüren isimsiz işlev döndüren isimsiz işlev auto l1 = (int a) => { return a + 1; } assert(l0(42) == 43); assert(l1(42)() == 43); // l1'in döndürdüğünün işletilmesi
Kısa söz diziminin bir örneğini std.algorithm
modülündeki filter
algoritmasının kullanımında görelim. filter
, şablon parametresi olarak bir kıstas, işlev parametresi olarak da bir aralık alır. Kıstası elemanlara teker teker uygular; false
çıkan elemanları eler ve diğerlerini geçirir. Kıstas, isimsiz işlevler de dahil olmak üzere çeşitli yollarla bildirilebilir.
(Not: Aralık kavramını daha sonraki bir bölümde göreceğiz. Şimdilik dilimlerin aralık olduklarını kabul edebilirsiniz.)
Örneğin, değerleri 10'dan büyük olan elemanları geçiren ve diğerlerini eleyen bir filter
ifadesine şablon parametresi olarak aşağıdaki gibi bir isimsiz işlev verilebilir:
import std.stdio; import std.algorithm; void main() { int[] sayılar = [ 20, 1, 10, 300, -2 ]; writeln(sayılar.filter!(sayı => sayı > 10)); }
Çıktısı:
[20, 300]
O kıstası şöyle açıklayabiliriz: bir sayı verildiğinde o sayı 10'dan büyük ise true
üret. Bu açıdan bakıldığında =>
söz diziminin solundaki değere karşılık sağındaki ifadeyi üreten bir söz dizimi olduğunu düşünebiliriz.
O kısa söz diziminin yerine bir kere de onun eşdeğeri olan en uzun söz dizimini yazalım. İsimsiz işlevin tanımını belirleyen küme parantezlerini işaretlenmiş olarak gösteriyorum:
writeln(sayılar.filter!(function bool(int sayı) { return sayı > 10; }));
Görüldüğü gibi, =>
söz dizimi tek return
deyimi içeren isimsiz işlevlerde büyük kolaylık ve okunaklılık sağlamaktadır.
Başka bir örnek olarak iki parametre kullanan bir isimsiz işlev tanımlayalım. Aşağıdaki algoritma kendisine verilen iki dilimin birbirlerine karşılık olan elemanlarını iki parametre alan bir işleve göndermektedir. O işlevin döndürdüğü sonuçları da bir dizi olarak döndürüyor:
import std.exception; int[] ikiliHesap(int function(int, int) işlem, const int[] soldakiler, const int[] sağdakiler) { enforce(soldakiler.length == sağdakiler.length); int[] sonuçlar; foreach (i; 0 .. soldakiler.length) { sonuçlar ~= işlem(soldakiler[i], sağdakiler[i]); } return sonuçlar; }
Oradaki işlev göstergesi iki parametre aldığından, ikiliHesap
'ın çağrıldığı yerde =>
karakterlerinden önce parantez içinde iki parametre belirtilmelidir:
import std.stdio; void main() { writeln(ikiliHesap((a, b) => (a * 10) + b, [ 1, 2, 3 ], [ 4, 5, 6 ])); }
Çıktısı:
[14, 25, 36]
Temsilciler
Temsilci, işlev göstergesine ek olarak onun içinde tanımlandığı kapsamın da saklanmasından oluşur. Temsilciler daha çok fonksiyonel programlama dillerinde görülen kapamaları da gerçekleştirirler. Temsilciler çoğu emirli dilde bulunmasalar da D'nin güçlü olanakları arasındadırlar.
Yaşam Süreçleri ve Temel İşlemler bölümünde gördüğümüz gibi, değişkenlerin yaşamları tanımlı oldukları kapsamdan çıkıldığında son bulur:
{ int artış = 10; // ... } // ← artış'ın yaşamı burada son bulur
artış
gibi yerel değişkenlerin adresleri bu yüzden işlevlerden döndürülemezler.
artış
'ın işlev göstergesi döndüren bir işlev içinde tanımlanmış olan yerel bir değişken olduğunu düşünelim. Bu işlevin sonuç olarak döndürdüğü isimsiz işlev bu yerel değişkeni de kullanıyor olsun:
alias Hesapİşlevi = int function(int); Hesapİşlevi hesapçı() { int artış = 10; return sayı => artış + sayı; // ← derleme HATASI }
Döndürülen isimsiz işlev yerel bir değişkeni kullanmaya çalıştığı için o kod hatalıdır. Derlenmesine izin verilmiş olsa, isimsiz işlev daha sonradan işletildiği sırada yaşamı çoktan sona ermiş olan artış
değişkenine erişmeye çalışacaktır.
O kodun derlenip doğru olarak çalışabilmesi için artış
'ın yaşam sürecinin isimsiz işlev yaşadığı sürece uzatılması gerekir. Temsilciler işte böyle durumlarda yararlıdırlar: Hem işlev göstergesini hem de onun kullandığı kapsamları sakladıkları için o kapsamlardaki değişkenlerin yaşamları, temsilcinin yaşamı kadar uzamış olur.
Temsilcilerin kullanımı işlev göstergelerine çok benzer: Tek farkları function
yerine delegate
anahtar sözcüğünün kullanılmasıdır. Yukarıdaki kodun derlenip doğru olarak çalışması için o kadarı yeterlidir:
alias Hesapİşlevi = int delegate(int); Hesapİşlevi hesapçı() { int artış = 10; return sayı => artış + sayı; }
O temsilcinin kullandığı yerel kapsamdaki artış
gibi değişkenlerin yaşamları temsilci yaşadığı sürece devam edecektir. Bu yüzden temsilciler ilerideki bir zamanda çağrıldıklarında o yerel değişkenleri değiştirebilirler de. Bunun örneklerini daha sonraki bir bölümde öğreneceğimiz yapı ve sınıfların opApply
üye işlevlerinde göreceğiz.
Yukarıdaki temsilciyi şöyle bir kodla deneyebiliriz:
auto işlev = hesapçı(); writeln("hesap: ", işlev(3));
hesapçı
, isimsiz bir temsilci döndürmektedir. Yukarıdaki kod o temsilciyi işlev
isimli bir değişkenin değeri olarak kullanmakta ve işlev(3)
yazımıyla çağırmaktadır. Temsilcinin işi de kendisine verilen sayı ile artış
'ın toplamını döndürmek olduğu için çıkışa 3 ve 10'un toplamı yazdırılacaktır:
hesap: 13
Kısa söz dizimi
Yukarıdaki örnekte de kullandığımız gibi, temsilciler de kısa söz dizimiyle ve hatta =>
söz dizimiyle yazılabilirler. function
veya delegate
yazılmadığında hangisinin uygun olduğuna derleyici karar verir. Kapsam saklama kaygısı olmadığından daha etkin olarak çalıştığı için öncelikle function
'ı dener, olamıyorsa delegate
'i seçer.
Kısa söz dizimini bir kere de parametre almayan bir temsilci ile görelim:
int[] özelSayılarOluştur(int adet, int delegate() sayıÜretici) { int[] sonuç = [ -1 ]; sonuç.reserve(adet + 2); foreach (i; 0 .. adet) { sonuç ~= sayıÜretici(); } sonuç ~= -1; return sonuç; }
O işlev ilk ve son sayıları -1 olan bir dizi sayı oluşturmaktadır. Bu iki özel sayının arasına kaç adet başka sayı geleceğini ve bu sayıların nasıl üretileceklerini ise parametre olarak almaktadır.
O işlevi, her çağrıldığında aynı sabit değeri döndüren aşağıdaki gibi bir temsilciyle çağırabiliriz. Yukarıda belirtildiği gibi, parametre almayan isimsiz işlevlerin parametre listesinin boş olarak belirtilmesi şarttır:
writeln(özelSayılarOluştur(3, () => 42));
Çıktısı:
-1 42 42 42 -1
Aynı işlevi bir de yerel bir değişken kullanan bir temsilci ile çağıralım:
int sonSayı; writeln(özelSayılarOluştur(15, () => sonSayı += uniform(0, 3))); writeln("son üretilen sayı: ", sonSayı);
O temsilci rasgele bir değer üretmekte, ama her zaman için son sayıya eklediği için rasgele sayıların değerleri hep artan yönde gitmektedir. Yerel değişkenin temsilcinin işletilmesi sırasında nasıl değişmiş olduğunu da çıktının son satırında görüyoruz:
-1 0 2 3 4 6 6 8 9 9 9 10 12 14 15 17 -1 son üretilen sayı: 17
Temsilci olarak nesne ve üye işlevi
Temsilcinin bir işlev göstergesini ve onun oluşturulduğu kapsamı bir arada sakladığını gördük. Bu ikisinin yerine belirli bir nesne ve onun bir üye işlevi de kullanılabilir. Böyle oluşturulan temsilci, o üye işlevi ve nesnenin kendisini bir araya getirmiş olur.
Bunun söz dizimi aşağıdaki gibidir:
&nesne.üye_işlev
Önce bu söz diziminin gerçekten de bir delegate
oluşturduğunu yine .stringof
'tan yararlanarak görelim:
import std.stdio; struct Konum { long x; long y; void sağa(size_t adım) { x += adım; } void sola(size_t adım) { x -= adım; } void yukarıya(size_t adım) { y += adım; } void aşağıya(size_t adım) { y -= adım; } } void main() { auto nokta = Konum(); writeln(typeof(&nokta.sağa).stringof); }
Çıktısı:
void delegate(ulong adım)
O söz dizimi yalnızca bir temsilci oluşturur. Nesnenin üye işlevi temsilci oluşturulduğu zaman çağrılmaz. O işlev, temsilci daha sonradan işlev gibi kullanıldığında çağrılacaktır. Bunun örneğini görmek için bir temsilci değişken tanımlayabiliriz:
auto yönİşlevi = &nokta.sağa; // burada tanımlanır yönİşlevi(3); // burada çağrılır writeln(nokta);
Çıktısı:
Konum(3, 0)
İşlev göstergeleri, isimsiz işlevler, ve temsilciler kendileri değişken olabildiklerinden; değişkenlerin kullanılabildikleri her yerde kullanılabilirler. Örneğin yukarıdaki nesne ve üye işlevlerinden oluşan bir temsilci dizisi şöyle oluşturulabilir ve daha sonra işlemleri işletilebilir:
auto nokta = Konum(); void delegate(size_t)[] işlemler = [ &nokta.sağa, &nokta.yukarıya, &nokta.sağa ]; foreach (işlem; işlemler) { işlem(1); } writeln(nokta);
O dizide iki kere sağa bir kere de yukarıya gitme işlemi bulunduğundan bütün temsilciler işletildiklerinde noktanın durumu şöyle değişmiş olur:
Konum(2, 1)
Temsilci nitelikleri
Bir temsilcinin işlev ve kapsam göstergeleri .funcptr
ve .ptr
nitelikleri ile elde edilebilir:
struct Yapı { void işlev() { } } void main() { auto nesne = Yapı(); auto d = &nesne.işlev; assert(d.funcptr == &Yapı.işlev); assert(d.ptr == &nesne); }
Bu niteliklere değerler atayarak delegate
oluşturmak mümkündür:
struct Yapı { int i; void işlev() { import std.stdio; writeln(i); } } void main() { auto nesne = Yapı(42); void delegate() d; assert(d is null); // null temsilci ile başlıyoruz d.funcptr = &Yapı.işlev; d.ptr = &nesne; d(); }
Yukarıdaki d()
söz dizimi ile temsilcinin çağrılması nesne.işlev()
ifadesinin (yani, Yapı.işlev
'in nesne
üzerinde işletilmesinin) eşdeğeridir:
42
lazy
parametre temsilcidir
lazy
anahtar sözcüğünü İşlev Parametreleri bölümünde görmüştük:
void logla(Önem önem, lazy string mesaj) { if (önem >= önemAyarı) { writeln(mesaj); } } // ... if (!bağlanıldı_mı) { logla(Önem.orta, format("Hata. Bağlantı durumu: '%s'.", bağlantıDurumunuÖğren())); }
Yukarıdaki mesaj
isimli parametre lazy
olduğundan, işleve o parametreye karşılık gönderilen bütün format
ifadesi (yaptığı bağlantıDurumunuÖğren()
çağrısı dahil), ancak o parametre işlev içinde kullanıldığında işletilir.
Perde arkasında lazy
parametreler aslında temsilcidirler. O parametrelere karşılık olarak gönderilen ifadeler otomatik olarak temsilci nesnelerine dönüştürülürler. Buna göre, aşağıdaki kod yukarıdakinin eşdeğeridir:
void logla(Önem önem, string delegate() tembelMesaj) { // (1) if (önem >= önemAyarı) { writefln("%s", tembelMesaj()); // (2) } } // ... if (!bağlanıldı_mı) { logla(Önem.orta, delegate string() { // (3) return format("Hata. Bağlantı durumu: '%s'.", bağlantıDurumunuÖğren()); }); }
lazy
parametrestring
değildir;string
döndüren bir temsilcidir.- O temsilci çağrılır ve dönüş değeri kullanılır.
- Bütün ifade onu döndüren bir temsilci ile sarmalanır.
Belirsiz sayıda lazy
parametre
Belirsiz sayıda lazy
parametresi olan bir işlevin sayıları belirsiz olan bu parametreleri lazy
olarak işaretlemesi olanaksızdır.
Bu durumda kullanılan çözüm, belirsiz sayıda delegate
parametre tanımlamaktır. Böyle parametreler temsilcilerin dönüş türüne uyan bütün ifadeleri parametre değeri olarak kabul ederler. Bir koşul, bu temsilcilerin kendilerinin parametre almamasıdır:
import std.stdio; void foo(double delegate()[] parametreler...) { foreach (parametre; parametreler) { writeln(parametre()); // Temsilcinin çağrılması } } void main() { foo(1.5, () => 2.5); /* 'double' ifade, temsilci * olarak gönderiliyor. */ }
Yukarıdaki hem double
ifade hem de isimsiz işlev belirsiz sayıdaki parametreye uyar. double
ifade otomatik olarak bir temsilci ile sarmalanır ve işlev gereğinde tembel olarak işletilebilecek olan bu parametrelerinin değerlerini çıkışa yazdırır:
1.5 2.5
Bu yöntemin bir yetersizliği, bütün parametrelerin aynı türden olmalarının gerekmesidir (bu örnekte double
). Daha sonraki Ayrıntılı Şablonlar bölümünde göreceğimiz çokuzlu şablon parametreleri bu yetersizliği giderir.
delegate
parametreli toString
Nesneleri string
türünde ifade etmek için kullanılan toString
işlevini kitabın bu noktasına kadar hep parametre almayan ve string
döndüren işlevler olarak tanımladık. Yapılar ve sınıflar kendi üyelerinin toString
işlevlerini format
aracılığıyla dolaylı olarak çağırıyorlardı ve toString
işlevleri kolaylıkla tanımlanabiliyordu:
import std.stdio; import std.string; struct Nokta { int x; int y; string toString() const { return format("(%s,%s)", x, y); } } struct Renk { ubyte r; ubyte g; ubyte b; string toString() const { return format("RGB:%s,%s,%s", r, g, b); } } struct RenkliNokta { Renk renk; Nokta nokta; string toString() const { // Bu, Renk.toString ve Nokta.toString'den yararlanıyor: return format("{%s;%s}", renk, nokta); } } struct Poligon { RenkliNokta[] noktalar; string toString() const { // Bu, RenkliNokta.toString'den yararlanıyor return format("%s", noktalar); } } void main() { auto poligon = Poligon( [ RenkliNokta(Renk(10, 10, 10), Nokta(1, 1)), RenkliNokta(Renk(20, 20, 20), Nokta(2, 2)), RenkliNokta(Renk(30, 30, 30), Nokta(3, 3)) ]); writeln(poligon); }
Yukarıdaki poligon
nesnesinin programın son satırında çıktıya yazdırılabilmesi için Poligon
, RenkliNokta
, Renk
, ve Nokta
yapılarının toString
işlevlerinden dolaylı olarak yararlanıldığında toplam 10 farklı string
nesnesi oluşturulmaktadır. Dikkat ederseniz, alt düzeylerde oluşturulan her string
nesnesi yalnızca kendi üst düzeyindeki string
nesnesini oluşturmak için kullanılmakta ve ondan sonra yaşamı sona ermektedir.
Sonuçta çıktıya tek mesaj yazdırılmış olmasına rağmen 10 adet string
nesnesi oluşturulmuş, ancak bunlardan yalnızca sonuncusu çıktıya yazdırılmak için kullanılmıştır:
[{RGB:10,10,10;(1,1)}, {RGB:20,20,20;(2,2)}, {RGB:30,30,30;(3,3)}]
Bu yöntem kodun gereksizce yavaş işlemesine neden olabilir.
Bu yavaşlığın önüne geçmek için toString
işlevinin delegate
türünde parametre alan ve genel olarak daha hızlı işleyen çeşidi de kullanılabilir:
void toString(void delegate(const(char)[]) çıkış) const;
Dönüş türünün void
olmasından anlaşıldığı gibi, toString
'in bu tanımı string
döndürmez. Onun yerine, çıktıya yazılacak olan karakterleri kendisine verilen temsilciye gönderir. O temsilci de verilen karakterleri sonuçta yazdırılacak olan tek string
'in sonuna ekler.
Bu toString
işlevinden yararlanmak için yapılması gereken, std.string.format
yerine std.format.formattedWrite
'ı çağırmak ve çıkış
isimli parametreyi onun ilk parametresi olarak vermektir (aşağıda UFCS söz dizimi ile). Ek olarak, aşağıdaki çağrılar düzen dizgilerini şablon parametreleri olarak belirtmekte ve böylece formattedWrite
'ın düzen dizgilerini derleme zamanında denetlemesinden yararlanmaktadırlar.
import std.stdio; import std.format; struct Nokta { int x; int y; void toString(void delegate(const(char)[]) çıkış) const { çıkış.formattedWrite!"(%s,%s)"(x, y); } } struct Renk { ubyte r; ubyte g; ubyte b; void toString(void delegate(const(char)[]) çıkış) const { çıkış.formattedWrite!"RGB:%s,%s,%s"(r, g, b); } } struct RenkliNokta { Renk renk; Nokta nokta; void toString(void delegate(const(char)[]) çıkış) const { çıkış.formattedWrite!"{%s;%s}"(renk, nokta); } } struct Poligon { RenkliNokta[] noktalar; void toString(void delegate(const(char)[]) çıkış) const { çıkış.formattedWrite!"%s"(noktalar); } } void main() { auto poligon = Poligon( [ RenkliNokta(Renk(10, 10, 10), Nokta(1, 1)), RenkliNokta(Renk(20, 20, 20), Nokta(2, 2)), RenkliNokta(Renk(30, 30, 30), Nokta(3, 3)) ]); writeln(poligon); }
Bu programın farkı, yine toplam 10 adet toString
işlevi çağrılmış olmasına rağmen, o çağrıların tek string
'in sonuna karakter eklenmesine neden olmalarıdır.
Özet
function
anahtar sözcüğü ile işlev göstergeleri tanımlanabilir ve bu göstergeler daha sonra işlev gibi kullanılabilir.delegate
anahtar sözcüğü temsilci tanımlar. Temsilci, işlev göstergesine ek olarak o işlev göstergesinin kullandığı kapsamı da barındırır.- Bir nesne ve onun bir üye işlevi
&nesne.üye_işlev
söz dizimi iledelegate
oluşturur. - İşlev göstergesi veya temsilci gereken yerlerde isimsiz işlevler veya isimsiz temsilciler tanımlanabilir.
- Temsilciler
.funcptr
ve.ptr
niteliklerine değer atanarak açıkça oluşturulabilirler. - İsimsiz işlevlerin çeşitli kısa söz dizimleri vardır. Tek
return
deyimi içeren isimsiz işlevler bu söz dizimlerinin en kısası olanparametre => ifade
söz dizimi ile tanımlanabilirler. toString
'in daha hızlı işleyen çeşididelegate
parametre alır.