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

belirsiz sayıda parametre: [variadic], aynı işlevi (veya şablonu) farklı sayıda parametre ile çağırabilme olanağı
gösterge: [pointer], bir değişkeni gösteren değişken
varsayılan: [default], özellikle belirtilmediğinde kullanılan
... bütün sözlük



İngilizce Kaynaklar


Diğer




Parametre Serbestliği

Bu bölümde İşlev Parametreleri bölümünde anlatılanlarla doğrudan ilgili olan ve işlev çağırma konusunda bazı serbestlikler sağlayan iki olanağı göstereceğim:

Varsayılan parametre değerleri

İşlevlerle ilgili bir kolaylık, parametrelere varsayılan değerler atanabilmesidir. Bu, yapı üyelerinin varsayılan değerlerinin belirlenebilmesine benzer.

Bazı işlevlerin bazı parametreleri çoğu durumda hep aynı değerle çağrılıyor olabilirler. Örnek olarak, bir eşleme tablosunu çıkışa yazdıran bir işlev düşünelim. Yazdırdığı eşleme tablosunun hem indeks türü hem de değer türü string olsun. Bu işlev, çıktıda kullanacağı ayraç karakterlerini de parametre olarak alacak şekilde esnek tasarlanmış olsun:

import std.algorithm;

// ...

void tabloYazdır(in char[] başlık,
                 in string[string] tablo,
                 in char[] indeksAyracı,
                 in char[] elemanAyracı) {
    writeln("-- ", başlık, " --");

    auto indeksler = sort(tablo.keys);

    foreach (sayaç, indeks; indeksler) {

        // İlk elemandan önce ayraç olmamalı
        if (sayaç != 0) {
            write(elemanAyracı);
        }

        write(indeks, indeksAyracı, tablo[indeks]);
    }

    writeln();
}

O işlev, indekslerle değerler arasına ":", elemanlar arasına da ", " gelecek şekilde şöyle çağrılabilir:

    string[string] sözlük = [
        "mavi":"blue", "kırmızı":"red", "gri":"gray" ];

    tabloYazdır("Renk Sözlüğü", sözlük, ":", ", ");

Çıktısı:

-- Renk Sözlüğü --
gri:gray, kırmızı:red, mavi:blue

Aynı programda başka tabloların da yazdırıldıklarını, ve çoğu durumda hep aynı ayraçların kullanıldıklarını varsayalım. Yalnızca bazı özel durumlarda farklı ayraçlar kullanılıyor olsun.

Parametre değerlerinin çoğunlukla aynı değeri aldıkları durumlarda, o değerler varsayılan değer olarak belirtilebilirler:

void tabloYazdır(in char[] başlık,
                 in string[string] tablo,
                 in char[] indeksAyracı = ":",
                 in char[] elemanAyracı = ", ") {
    // ...
}

Varsayılan değerleri olan parametreler işlev çağrısı sırasında belirtilmeyebilirler:

    tabloYazdır("Renk Sözlüğü",
                sözlük); /* ← ayraçlar belirtilmemiş;
                          *   varsayılan değerlerini alırlar */

O durumda, belirtilmeyen parametrelerin varsayılan değerleri kullanılır.

Normalin dışında değer kullanılacağı durumlarda işlev çağrılırken o parametreler için yine de özel değerler verilebilir. Gerekiyorsa yalnızca ilki:

    tabloYazdır("Renk Sözlüğü", sözlük, "=");

Çıktısı:

-- Renk Sözlüğü --
gri=gray, kırmızı=red, mavi=blue

Veya gerekiyorsa her ikisi birden:

    tabloYazdır("Renk Sözlüğü", sözlük, "=", "\n");

Çıktısı:

-- Renk Sözlüğü --
gri=gray
kırmızı=red
mavi=blue

Varsayılan değerler yalnızca parametre listesinin son tarafındaki parametreler için belirtilebilir. Baştaki veya aradaki parametrelerin varsayılan değerleri belirtilemez.

Özel anahtar sözcüklerin varsayılan değer olarak kullanılmaları

Aşağıdaki anahtar sözcükler kodda geçtikleri yeri gösteren hazır değerler olarak işlem görürler:

Kodun başka noktalarında da kullanışlı olsalar da varsayılan parametre değeri olarak kullanıldıklarında etkileri farklıdır. Normal kod içinde geçtiklerinde değerleri bulundukları yerle ilgilidir:

import std.stdio;

void işlev(int parametre) {
    writefln("%s dosyasının %s numaralı satırında, %s " ~
             "işlevi içindeyiz.",
             __FILE__, __LINE__, __FUNCTION__);    // ← satır 6
}

void main() {
    işlev(42);
}

Bildirilen 6 numaralı satır işlevin kendi kodlarına işaret eder:

deneme.d dosyasının 6 numaralı satırında, deneme.işlev
işlevi içindeyiz.

Ancak, bazı durumlarda işlevin hangi satırda tanımlandığı değil, hangi satırdan çağrıldığı bilgisi daha önemlidir. Bu özel anahtar sözcükler varsayılan parametre değeri olarak kullanıldıklarında kendi bulundukları satırla değil, işlevin çağrıldığı satırla ilgili bilgi verirler:

import std.stdio;

void işlev(int parametre,
           string işlevİsmi = __FUNCTION__,
           string dosya = __FILE__,
           int satır = __LINE__) {
    writefln("%s dosyasının %s numaralı satırındaki %s " ~
             "işlevi tarafından çağrılıyor.",
             dosya, satır, işlevİsmi);
}

void main() {
    işlev(42);    // ← satır 13
}

Bu sefer özel anahtar sözcüklerin değerleri işlevin çağrıldığı main() işlevine işaret eder:

deneme.d dosyasının 13 numaralı satırındaki deneme.main
işlevi tarafından çağrılıyor.

Yukarıdakilere ek olarak aşağıdaki özel değişkenler de kullanılan derleyiciye ve derleme saatine göre değerler alırlar:

Belirsiz sayıda parametreler

Varsayılan parametre değerleri, işlevin aslında kaç tane parametre aldığını değiştirmez. Örneğin yukarıdaki tabloYazdır işlevi her zaman için dört adet parametre alır; ve işini yaparken o dört parametreyi kullanır.

D'nin başka bir olanağı, işlevleri belirsiz sayıda parametre ile çağırabilmemizi sağlar. Bu olanağı aslında daha önce de çok kere kullandık. Örneğin writeln'i hiçbir kısıtlamayla karşılaşmadan sınırsız sayıda parametre ile çağırabiliyorduk:

    writeln(
        "merhaba", 7, "dünya", 9.8 /*, ve istediğimiz kadar
                                    *  daha parametre */);

D'de belirsiz sayıda parametre kullanmanın dört yolu vardır:

D, belirsiz sayıdaki parametreleri o tür işlevlere bir dizi halinde sunar. Belirsiz sayıda parametre alacak olan işlevin parametresi olarak bir dizi belirtilir ve hemen arkasından ... karakterleri yazılır:

double topla(in double[] sayılar...) {
    double sonuç = 0.0;

    foreach (sayı; sayılar) {
        sonuç += sayı;
    }

    return sonuç;
}

O şekilde tanımlanan topla, belirsiz sayıda parametre alan bir işlev haline gelmiş olur. Onu istediğimiz sayıda double ile çağırabiliriz:

    writeln(topla(1.1, 2.2, 3.3));

Bütün parametre değerleri tek dilim olarak da verilebilirler:

    writeln(sum([ 1.1, 2.2, 3.3 ]));    // üsttekinin eşdeğeri

Parametre listesindeki dizi ve ondan sonra gelen ... karakterleri işlevin çağrıldığı sırada kullanılan parametreleri temsil ederler. topla işlevi örneğin beş double parametre ile çağrıldığında, topla'nın sayılar parametresi o beş sayıyı içerir.

Böyle işlevlerin şart koştukları parametreler de bulunabilir. Örneğin, belirsiz sayıdaki sözcüğü parantezler arasında yazdıran bir işlev düşünelim. Bu işlev, her ne kadar sözcük sayısını serbest bıraksa da, ne tür parantezler kullanılacağının belirtilmesini şart koşsun.

Kesinlikle belirtilmesi gereken parametreler parametre listesinde baş tarafa yazılırlar. Belirsiz sayıdaki parametreyi temsil eden dizi ise en sona yazılır:

char[] parantezle(
        in char[] açma,     // ← işlev çağrılırken belirtilmelidir
        in char[] kapama,   // ← işlev çağrılırken belirtilmelidir
        in char[][] sözcükler...) {    // ← hiç belirtilmeyebilir
    char[] sonuç;

    foreach (sözcük; sözcükler) {
        sonuç ~= açma;
        sonuç ~= sözcük;
        sonuç ~= kapama;
    }

    return sonuç;
}

O işlevi çağırırken ilk iki parametre mutlaka belirtilmelidir:

    parantezle("{");     // ← derleme HATASI

Kesinlikle belirtilmeleri gereken baştaki parametreler verildiği sürece, geri kalan parametreler konusunda serbestlik vardır. Buna uygun olarak açma ve kapama parantezlerini kullanan bir örnek:

    writeln(parantezle("{", "}", "elma", "armut", "muz"));

Çıktısı:

{elma}{armut}{muz}
Parametre dizisinin ömrü kısadır

Belirsiz sayıdaki parametrelerin sunulduğu dilim, ömrü kısa olan geçici bir dizinin elemanlarını gösterir. Bu elemanlar yalnızca işlevin işleyişi sırasında kullanmalıdır. Ömürleri kısa olduğundan, işlevin böyle bir dilimi daha sonradan kullanmak üzere saklaması hatalıdır:

int[] sonraKullanmakÜzereSayılar;

void foo(int[] sayılar...) {
    sonraKullanmakÜzereSayılar = sayılar;    // ← HATALI
}

struct S {
    string[] sonraKullanmakÜzereİsimler;

    void foo(string[] isimler...) {
        sonraKullanmakÜzereİsimler = isimler;    // ← HATALI
    }
}

void bar() {
    foo(1, 10, 100);  /* Geçici [ 1, 10, 100 ] dizisi bu
                       * noktadan sonra geçerli değildir. */

    auto s = S();
    s.foo("merhaba", "dünya");/* Geçici [ "merhaba", "dünya" ]
                               * dizisi bu noktadan sonra
                               * geçerli değildir. */

    // ...
}

void main() {
    bar();
}

Program yığıtında yaşayan geçici dizilerin elemanlarına erişim sağlayan dilimler sakladıklarından hem serbest işlev foo() hem de üye işlev S.foo() hatalıdır. Belirsiz sayıda parametre alan işlev çağrılırken otomatik olarak oluşturulan diziler yalnızca o işlevin işleyişi sırasında geçerlidirler.

Bu yüzden, parametreleri gösteren bir dilimi sonradan kullanmak üzere saklamak isteyen bir işlevin dilim elemanlarının kopyalarını alması gerekir:

void foo(int[] sayılar...) {
    sonraKullanmakÜzereSayılar = sayılar.dup;    // ← doğru
}

// ...

    void foo(string[] isimler...) {
        sonraKullanmakÜzereİsimler = isimler.dup;    // ← doğru
    }

Ancak, böyle işlevler normal dizi dilimleriyle de çağrılabildiklerinden, normal dilimlerin elemanlarının kopyalanmaları gereksizce masraflı olacaktır.

Hem doğru hem de hızlı olan bir çözüm, birisi belirsiz sayıda parametre, diğeri ise normal dilim alan aynı isimde iki işlev tanımlamaktır. İşlev belirsiz sayıda parametre ile çağrıldığında birisi, normal dilimle çağrıldığında diğeri işletilir:

int[] sonraKullanmakÜzereSayılar;

void foo(int[] sayılar...) {
    /* Bu, belirsiz sayıda parametre alan foo() işlevi
     * olduğundan, kendilerini gösteren dilim saklamadan önce
     * elemanların kopyalarını almak gerekir. */
    sonraKullanmakÜzereSayılar = sayılar.dup;
}

void foo(int[] sayılar) {
    /* Bu, normal dilim alan foo() işlevi olduğundan, dilimi
     * olduğu gibi saklayabiliriz. */
    sonraKullanmakÜzereSayılar = sayılar;
}

struct S {
    string[] sonraKullanmakÜzereİsimler;

    void foo(string[] isimler...) {
        /* Bu, belirsiz sayıda parametre alan S.foo() işlevi
         * olduğundan, kendilerini gösteren dilim saklamadan
         * önce elemanların kopyalarını almak gerekir. */
        sonraKullanmakÜzereİsimler = isimler.dup;
    }

    void foo(string[] isimler) {
        /* Bu, normal dilim alan S.foo() işlevi olduğundan,
         * dilimi olduğu gibi saklayabiliriz. */
        sonraKullanmakÜzereİsimler = isimler;
    }
}

void bar() {
    /* Bu çağrı, belirsiz sayıda parametre alan işleve
     * yönlendirilir. */
    foo(1, 10, 100);

    /* Bu çağrı, normal dilim alan işleve yönlendirilir. */
    foo([ 2, 20, 200 ]);

    auto s = S();

    /* Bu çağrı, belirsiz sayıda parametre alan işleve
     * yönlendirilir. */
    s.foo("merhaba", "dünya");

    /* Bu çağrı, normal dilim alan işleve yönlendirilir. */
    s.foo([ "selam", "ay" ]);

    // ...
}

void main() {
    bar();
}

Aynı isimde ama farklı parametreli işlevler tanımlamaya işlev yükleme denir. İşlev yüklemeyi bir sonraki bölümde göreceğiz.

Problem

Daha önce gördüğümüz aşağıdaki enum türünün tanımlı olduğunu varsayın:

enum İşlem { toplama, çıkarma, çarpma, bölme }

O işlem çeşidini ve işlemde kullanılacak iki kesirli sayıyı içeren bir de yapı olsun:

struct Hesap {
    İşlem işlem;
    double birinci;
    double ikinci;
}

Örneğin Hesap(İşlem.bölme, 7.7, 8.8) nesnesi, 7.7'nin 8.8'e bölüneceği anlamına gelsin.

Bu yapı nesnelerinden belirsiz sayıda parametre alan, her birisini teker teker hesaplayan, ve bütün sonuçları bir double dizisi olarak döndüren hesapla isminde bir işlev yazın.

Bu işlev örneğin şöyle çağrılabilsin:

void main() {
    writeln(hesapla(Hesap(İşlem.toplama, 1.1, 2.2),
                    Hesap(İşlem.çıkarma, 3.3, 4.4),
                    Hesap(İşlem.çarpma, 5.5, 6.6),
                    Hesap(İşlem.bölme, 7.7, 8.8)));
}

Yukarıdaki gibi kullanıldığında, hesapla'nın işlem sonuçlarını yerleştirdiği dizi writeln tarafından çıktıya şöyle yazdırılacaktır:

[3.3, -1.1, 36.3, 0.875]