D.ershane D Programlama Dili Dersleri

adres: [address], değişkenin (veya nesnenin) bellekte bulunduğu yer
emirli programlama: [imperative programming], işlemlerin deyimler halinde adım adım belirlendikleri programlama yöntemi
fonksiyonel programlama: [functional programming], yan etki üretmeyen 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
kapama: [closure], işlemleri ve işledikleri 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ı
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



İşlev Göstergeleri ve Kapamalar

İşlev göstergeleri; işlevlerin adreslerinin saklanabilmelerini ve daha sonraki bir zamanda bu göstergeler yoluyla çağrılabilmelerini sağlar. İşlev göstergeleri D'ye C'den geçmiştir.

Kapamalar; hem işlev göstergelerini, hem de o işlevlerin kullandıkları kapsamları bir arada saklayan olanaklardır.

İşlev göstergeleri

Bundan önceki derste 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
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 int function(char, double) Hesapİşlevi;

Artık function'lı uzun yazım yerine kısaca Hesapİşlevi yazmak yeterlidir.

    Hesapİşlevi 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);
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.

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 ücret hesabı için çağrılacak olan işlev, bu yapının tür gibi 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ştirilmesi 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ç;
    }
}

// ...

    MaaşlıÇalışan çalışan;
    double ücret = çalışan.ücretHesabı();

Bu, nesne yönelimli programlama dillerine uygun olan bir yöntemdir.

Davranış farklılığı konusunda başka bir yöntem, işlev göstergelerinden yararlanmaktır. Bu, özellikle nesne yönelimli olanakları bulunmayan C dili kütüphaneleri kullanırken gerekebilir.

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,
            int 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;

    // Rastgele 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. Süzme ve dönüştürme işlemlerini ona dışarıdan verebilirsek, kullanışlılığını arttırabiliriz.

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 bakarak bool sonuç elde ediliyor
sayı * 10 : int olan sayı kullanılarak yine int üretiliyor

Bu işlemleri işlev göstergelerine yaptırmadan önce, bu dönüşümleri sağlayacak olan işlev gösterge türlerini şöyle tanımlayabiliriz:

alias bool function(int) Süzmeİşlemi;
alias int function(int) Dönüşümİşlemi;

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 bu göstergelere 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ı)) {
            int 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ş olur; o işlemleri, kendisine verilen işlev göstergelerine yaptırmaktadır. Yukarıdaki gibi "sıfırdan büyüklerinin 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şaretlilerini" elde etmek için:

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
Ü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şlevini, süzme ve dönüştürme işlemlerini kurucu parametreleri olarak alacak şekilde şöyle yazabiliriz:

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ı)) {
                int yeniDeğer = dönüştürücü(sayı);
                sonuç ~= yeniDeğer;
            }
        }

        return sonuç;
    }
}

Daha sonra o türden bir nesne oluşturabiliriz ve yukarıdaki sonuçları elde etmek için şu şekilde kullanabiliriz:

    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 bu küçük işlevlerin adreslerinin gönderildiğini gördük.

Yukarıdaki örneklerde de görüldüğü gibi, bazı durumlarda başlı başına işlevler yazmak gereksiz gelebilir. Bu gereksizlik özellikle sayı > 0 ve sayı * 10 gibi küçük işlemler kullanırken hissedilir.

İşlev hazır değeri olarak da adlandırabileceğimiz isimsiz işlev olanağı, 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.

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. Örneğin parametre olarak işlev göstergesi alan bir işlev olsun:

void birİşlev(/* ... işlev göstergesi alsın ... */)
{
    // ...
}

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; });
    birİşlev(function { return 42.42; });

Birincisi hiçbir kısaltmaya başvurmadan yazılmıştır; ikincisi, dönüş türünün return satırından çıkarsanmasından yararlanmıştır; üçüncüsü de gereksiz olan boş parametre listesini yazmamıştır.

Bazı durumlarda bir adım daha atabilir ve function da yazmayabiliriz. Ama o zaman bir isimsiz kapama kullanıldığı varsayılır. Bunu biraz aşağıda göstereceğim.

Kapamalar

Kapama, daha çok fonksiyonel programlama dillerinde görülen bir olanaktır. Kapama, işlev göstergesine ek olarak, onun içinde tanımlandığı kapsamın da saklanmasından oluşur.

C ailesine bağlı olan D, emirli bir dildir. Bu gibi dillerde programın işlemleri adım adım programcı tarafından emir verir gibi tarif edilir: bütün öğrenci nesnelerine eriş, notlarını topla, öğrenci sayısına böl, çıkışa yazdır, vs. Kapamalar genelde böyle dillerde bulunmazlar; D, sistem dilleri arasında bu konuda bir istisnadır.

Yaşam Süreçleri ve Temel İşlemler dersinde 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

İşlevlerden artış gibi yerel değişkenlerin adreslerinin döndürülmesi de zaten bu yüzden tanımsız davranıştır.

artış'ın işlev göstergesi döndüren bir işlevin bir yerel değişkeni olduğunu düşünelim. Bu işlevin döndürdüğü isimsiz işlev, bu yerel değişkeni kullanmak istesin:

alias int function(int) Hesapİşlevi;

Hesapİşlevi hesapçı()
{
    int artış = 10;                       // ↓ derleme HATASI
    return function int(int sayı) { return artış + sayı; };
}

Döndürülen işlev, yerel 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. Bu yüzden derleyici hata verir:

Error: function deneme.hesapçı.__funcliteral1 cannot access
frame of function hesapçı

O kodun derlenip doğru olarak çalışabilmesi için yerel değişkenlerin yaşam süreçlerinin isimsiz işlev yaşadığı sürece uzatılması gerekir. Kapamalar işte bu gibi durumlarda kullanılırlar: işlev göstergesi yanında, onun kullandığı kapsamları da saklayarak kodun doğru olarak çalışmasını sağlarlar.

Kullanılması

Kapamalar işlev göstergelerine çok benzer şekilde kullanılırlar. 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 int delegate(int) Hesapİşlevi;

Hesapİşlevi hesapçı()
{
    int artış = 10;
    return delegate int(int sayı) { return artış + sayı; };
}

O kapamanın kullandığı yerel kapsamdaki artış gibi değişkenlerin yaşamları, kapama yaşadığı sürece devam edecektir. Bu yüzden; kapamalar her çağrıldıklarında yerel değişkenleri değiştirebilirler. Bunun örneklerini daha sonraki bir derste öğreneceğimiz yapı ve sınıfların opApply üye işlevlerinde göreceğiz.

Yukarıdaki kapamayı şöyle bir kodla deneyebiliriz:

    auto işlev = hesapçı();
    writeln("hesap: ", işlev(3));

hesapçı, isimsiz bir kapama göstergesi döndürmektedir. Yukarıdaki kod, o göstergeyi işlev isimli bir değişkenin değeri olarak kullanmakta, ve işlev(3) olarak çağırmaktadır. Kapamanın 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

İsimsiz işlevlerde olduğu gibi; eğer çıkarsanabiliyorsa, isimsiz kapamaların dönüş türleri yazılmayabilir, eğer boşsa parametre listeleri belirtilmeyebilir, ve ek olarak delegate sözcüğü de yazılmayabilir. function veya delegate yazılmadığında delegate varsayılır.

Bütün bu kısaltmaları görmek için, parametresiz bir kapama kullanan bir koda bakalım:

int[] özelSayılarOluştur(int adet, int delegate() sayıÜretici)
{
    int[] sonuç = [ -1 ];

    foreach (i; 0 .. adet) {
        sonuç ~= sayıÜretici();
    }

    return sonuç ~ -1;
}

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 kapamayla çağırabiliriz. Bu isimsiz kapamayı belirleyen kapsam parantezlerini sarı ile işaretliyorum:

    writeln(özelSayılarOluştur(3, { return 42; }));

Gördüğünüz gibi, isimsiz kapama için delegate anahtar sözcüğü kullanılmamış; return satırından çıkarsanabildiği için dönüş türü belirtilmemiş; ve boş olduğu için parametre listesi de yazılmamıştır. Çıktısı:

-1 42 42 42 -1

Aynı işlevi, bir de yerel bir değişken kullanan bir kapama ile çağıralım:

    int sonSayı;
    writeln(özelSayılarOluştur(15,
                               { sonSayı += uniform(0, 3);
                                 return sonSayı; }));

    writeln("son üretilen sayı: ", sonSayı);

O kapama; rastgele bir değer üretmekte, ama her zaman için son sayıya eklediği için, rastgele sayıların değerleri hep artan yönde gitmektedir. Kapamanın yerel değişkeni nasıl değiştirmiş 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
Özet