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

adres: [address], değişkenin (veya nesnenin) bellekte bulunduğu yer
belirsiz sayıda parametre: [variadic], aynı işlevi (veya şablonu) farklı sayıda parametre ile çağırabilme olanağı
emirli programlama: [imperative programming], işlemlerin deyimler halinde adım adım belirlendikleri programlama yöntemi
fonksiyonel programlama: [functional programming], yan etki üretmeme ilkesine dayalı programlama yöntemi
gösterge: [pointer], bir değişkeni gösteren değişken
hazır değer: [literal], kod içinde hazır olarak yazılan değerler
isimsiz işlev: [lambda], çoğunlukla işlevlere parametre değeri olarak gönderilen kısa ve isimsiz işlev
kapama: [closure], işlemi ve işlediği kapsamı bir arada saklayan program yapısı
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
tembel değerlendirme: [lazy evaluation], işlemlerin gerçekten gerekene kadar geciktirilmesi
temsilci: [delegate], oluşturulduğu ortamdaki değişkenlere erişebilen isimsiz işlev
... bütün sözlük



İngilizce Kaynaklar


Diğer




İş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:

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 sarı ile işaretliyorum:

    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ı) {
        writefln("%s", 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());
              });
    }
  1. lazy parametre string değildir; string döndüren bir temsilcidir.
  2. O temsilci çağrılır ve dönüş değeri kullanılır.
  3. 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:

import std.stdio;
import std.format;

struct Nokta {
    int x;
    int y;

    void toString(void delegate(const(char)[]) çıkış) const {
        formattedWrite(çıkış, "(%s,%s)", x, y);
    }
}

struct Renk {
    ubyte r;
    ubyte g;
    ubyte b;

    void toString(void delegate(const(char)[]) çıkış) const {
        formattedWrite(çıkış, "RGB:%s,%s,%s", r, g, b);
    }
}

struct RenkliNokta {
    Renk renk;
    Nokta nokta;

    void toString(void delegate(const(char)[]) çıkış) const {
        formattedWrite(çıkış, "{%s;%s}", renk, nokta);
    }
}

struct Poligon {
    RenkliNokta[] noktalar;

    void toString(void delegate(const(char)[]) çıkış) const {
        formattedWrite(çıkış, "%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