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

aralık: [range], belirli biçimde erişilen bir grup eleman
kapsam: [scope], küme parantezleriyle belirlenen bir alan
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
temsilci: [delegate], oluşturulduğu ortamdaki değişkenlere erişebilen isimsiz işlev
topluluk: [container], aynı türden birden fazla veriyi bir araya getiren veri yapısı
yükleme: [overloading], aynı isimde birden çok işlev tanımlama
... bütün sözlük



İngilizce Kaynaklar


Diğer




Yapı ve Sınıflarda foreach

foreach Döngüsü bölümünden hatırlayacağınız gibi, bu döngü uygulandığı türe göre değişik şekillerde işler. Nasıl kullanıldığına bağlı olarak farklı elemanlara erişim sağlar: dizilerde, sayaçlı veya sayaçsız olarak dizi elemanlarına; eşleme tablolarında, indeksli veya indekssiz olarak tablo elemanlarına; sayı aralıklarında, değerlere; kütüphane türlerinde, o türe özel bir şekilde, örneğin File için dosya satırlarına...

foreach'in nasıl işleyeceğini kendi türlerimiz için de belirleyebiliriz. Bunun için iki farklı yöntem kullanılabilir:

Bu iki yöntemden opApply işlevleri önceliklidir: Tanımlanmışlarsa derleyici o üye işlevleri kullanır; tanımlanmamışlarsa aralık işlevlerine başvurur. Öte yandan, aralık işlevleri yöntemi çoğu durumda yeterli, daha basit, ve daha kullanışlıdır.

Bu yöntemlere geçmeden önce, foreach'in her türe uygun olamayacağını vurgulamak istiyorum. Bir nesne üzerinde foreach ile ilerlemek, ancak o tür herhangi bir şekilde bir topluluk olarak kabul edilebiliyorsa anlamlıdır.

Örneğin, Öğrenci gibi bir sınıfın foreach ile kullanılmasında ne tür değişkenlere erişileceği açık değildir. O yüzden Öğrenci sınıfının böyle bir konuda destek vermesi beklenmeyebilir. Öte yandan, başka bir bakış açısı ile, foreach döngüsünün Öğrenci nesnesinin notlarına erişmek için kullanılacağı da düşünülebilir.

Kendi türlerinizin foreach desteği verip vermeyeceklerine ve vereceklerse ne tür değişkenlere erişim sağlayacaklarına siz karar vermelisiniz.

foreach desteğini aralık işlevleri ile sağlamak

foreach'in for'un daha kullanışlısı olduğunu biliyoruz. Şöyle bir foreach döngüsü olsun:

    foreach (eleman; aralık) {
        // ... ifadeler ...
    }

O döngü, derleyici tarafından arka planda bir for döngüsü olarak şöyle gerçekleştirilir:

    for ( ; /* bitmediği sürece */; /* başından daralt */) {

        auto eleman = /* aralığın başındaki */;

        // ... ifadeler ...
    }

foreach'in kendi türlerimizle de çalışabilmesi için yukarıdaki üç özel bölümde kullanılacak olan üç özel üye işlev tanımlamak gerekir. Bu üç işlev; döngünün sonunu belirlemek, sonrakine geçmek (aralığı baş tarafından daraltmak), ve en baştakine erişim sağlamak için kullanılır.

Bu üç üye işlevin isimleri sırasıyla empty, popFront, ve front'tur. Derleyicinin arka planda ürettiği kod bu üye işlevleri kullanır:

    for ( ; !aralık.empty(); aralık.popFront()) {

        auto eleman = aralık.front();

        // ... ifadeler ...
    }

Bu üç işlev aşağıdaki gibi işlemelidir:

O şekilde işleyen böyle üç üye işleve sahip olması, türün foreach ile kullanılabilmesi için yeterlidir.

Örnek

Belirli aralıkta değerler üreten bir yapı tasarlayalım. Aralığın başını ve sonunu belirleyen değerler, nesne kurulurken belirlensinler. Geleneklere uygun olarak, son değer aralığın dışında kabul edilsin. Bir anlamda, D'nin baş..son şeklinde yazılan aralıklarının eşdeğeri olarak çalışan bir tür tanımlayalım:

struct Aralık {
    int baş;
    int son;

    invariant() {
        // baş'ın hiçbir zaman son'dan büyük olmaması gerekir
        assert(baş <= son);
    }

    bool empty() const {
        // baş, son'a eşit olduğunda aralık tükenmiş demektir
        return baş == son;
    }

    void popFront() {
        // Bir sonrakine geçmek, baş'ı bir arttırmaktır. Bu
        // işlem, bir anlamda aralığı baş tarafından kısaltır.
        ++baş;
    }

    int front() const {
        // Aralığın başındaki değer, baş'ın kendisidir
        return baş;
    }
}

Not: Ben güvenlik olarak yalnızca invariant bloğundan yararlandım. Ona ek olarak, popFront ve front işlevleri için in blokları da düşünülebilirdi; o işlevlerin doğru olarak çalışabilmesi için ayrıca aralığın boş olmaması gerekir.

O yapının nesnelerini artık foreach ile şöyle kullanabiliriz:

    foreach (eleman; Aralık(3, 7)) {
        write(eleman, ' ');
    }

foreach, o üç işlevden yararlanarak aralıktaki değerleri sonuna kadar, yani empty'nin dönüş değeri true olana kadar kullanır:

3 4 5 6 
Ters sırada ilerlemek için std.range.retro

std.range modülü aralıklarla ilgili çeşitli olanaklar sunar. Bunlar arasından retro, kendisine verilen aralığı ters sırada kullanır. Türün retro ile kullanılabilmesi için bu amaca yönelik iki üye işlev daha gerekir:

Ancak, retro'nun o iki işlevi kullanabilmesi için bir işlevin daha tanımlanmış olması gerekir:

Bu üye işlevler hakkında daha ayrıntılı bilgiyi daha sonra Aralıklar bölümünde göreceğiz.

Bu üç işlevi Aralık yapısı için şöyle tanımlayabiliriz:

struct Aralık {
// ...

    void popBack() {
        // Bir öncekine geçmek, son'u bir azaltmaktır. Bu
        // işlem, bir anlamda aralığı son tarafından kısaltır.
        --son;
    }

    int back() const {
        // Aralığın sonundaki değer, son'dan bir önceki
        // değerdir; çünkü gelenek olarak aralığın sonu,
        // aralığa dahil değildir.
        return son - 1;
    }

    Aralık save() const {
        // Aralık nesnesinin şu andaki durumu bir kopyası
        // döndürülerek sağlanabilir.
        return this;
    }
}

Bu türün nesneleri retro ile kullanılmaya hazırdır:

import std.range;

// ...

    foreach (eleman; Aralık(3, 7).retro) {
        write(eleman, ' ');
    }

Kodun çıktısından anlaşıldığı gibi, retro yukarıdaki üye işlevlerden yararlanarak bu aralığı ters sırada kullanır:

6 5 4 3 
foreach desteğini opApply ve opApplyReverse işlevleri ile sağlamak

Bu başlık altında opApply için anlatılanlar opApplyReverse için de geçerlidir. opApplyReverse, nesnenin foreach_reverse döngüsüyle kullanımını belirler.

Yukarıdaki üye işlevler, nesneyi sanki bir aralıkmış gibi kullanmayı sağlarlar. O yöntem, nesnelerin foreach ile tek bir şekilde kullanılmaları durumuna daha uygundur. Örneğin Öğrenciler gibi bir türün nesnelerinin, öğrencilere foreach ile teker teker erişim sağlaması, o yöntemle kolayca gerçekleştirilebilir.

Öte yandan, bazen bir nesne üzerinde farklı şekillerde ilerlemek istenebilir. Bunun örneklerini eşleme tablolarından biliyoruz: Döngü değişkenlerinin tanımına bağlı olarak ya yalnızca elemanlara, ya da hem elemanlara hem de indekslere erişilebiliyordu:

    string[string] ingilizcedenTürkçeye;

    // ...

    foreach (türkçesi; ingilizcedenTürkçeye) {
        // ... yalnızca elemanlar ...
    }

    foreach (ingilizcesi, türkçesi; ingilizcedenTürkçeye) {
        // ... indeksler ve elemanlar ...
    }

opApply işlevleri, kendi türlerimizi de foreach ile birden fazla şekilde kullanma olanağı sağlarlar. opApply'ın nasıl tanımlanması gerektiğini görmeden önce opApply'ın nasıl çağrıldığını anlamamız gerekiyor.

Programın işleyişi, foreach'in kapsamına yazılan işlemler ile opApply işlevinin işlemleri arasında, belirli bir anlaşmaya uygun olarak gider gelir. Önce opApply'ın içi işletilir; opApply kendi işi sırasında foreach'in işlemlerini çağırır; ve bu karşılıklı gidiş geliş döngü sonuna kadar devam eder.

Bu anlaşmayı açıklamadan önce foreach döngüsünün yapısını tekrar hatırlatmak istiyorum:

// Programcının yazdığı döngü:

    foreach (/* döngü değişkenleri */; nesne) {
        // ... işlemler ...
    }

Eğer döngü değişkenlerine uyan bir opApply işlevi tanımlanmışsa; derleyici, döngü değişkenlerini ve döngü kapsamını kullanarak bir temsilci oluşturur ve nesnenin opApply işlevini o temsilci ile çağırır.

Buna göre, yukarıdaki döngü derleyici tarafından arka planda aşağıdaki koda dönüştürülür. Temsilciyi oluşturan kapsam parantezlerini işaretlenmiş olarak gösteriyorum:

// Derleyicinin arka planda kullandığı kod:

    nesne.opApply(delegate int(/* döngü değişkenleri */) {
        // ... işlemler ...
        return sonlandı_mı;
    });

Yani, foreach döngüsü ortadan kalkar; onun yerine nesnenin opApply işlevi derleyicinin oluşturduğu bir temsilci ile çağrılır. Derleyicinin oluşturduğu bir temsilcinin kullanılıyor olması opApply işlevinin yazımı konusunda bazı zorunluluklar getirir.

Bu dönüşümü ve uyulması gereken zorunlulukları şu maddelerle açıklayabiliriz:

  1. foreach'in işlemleri temsilciyi oluşturan işlemler haline gelirler. Bu temsilci opApply tarafından çağrılmalıdır.
  2. Döngü değişkenleri temsilcinin parametreleri haline gelirler. Bu parametrelerin opApply'ın tanımında ref olarak işaretlenmeleri gerekir. (Parametreler aslında ref anahtar sözcüğünü kullanmadan da tanımlanabilirler ama o zaman elemanlara referans olarak erişilemez.)
  3. Temsilcinin dönüş türü int'tir. Buna uygun olarak, temsilcinin sonuna derleyici tarafından bir return satırı eklenir. return'ün döndürdüğü bilgi, döngünün break veya return ile sonlanıp sonlanmadığını anlamak için kullanılır. Eğer sıfır ise döngü devam etmelidir; sıfırdan farklı ise döngü sonlanmalıdır.
  4. Asıl döngü opApply'ın içinde programcı tarafından gerçekleştirilir.
  5. opApply, temsilcinin döndürmüş olduğu sonlandı_mı değerini döndürmelidir.

Aralık yapısını bu anlaşmaya uygun olarak aşağıdaki gibi tanımlayabiliriz. Yukarıdaki maddeleri, ilgili oldukları yerlerde açıklama satırları olarak belirtiyorum:

struct Aralık {
    int baş;
    int son;
                         //    (2)      (1)
    int opApply(int delegate(ref int) işlemler) const {
        int sonuç = 0;

        for (int sayı = baş; sayı != son; ++sayı) {  // (4)
            sonuç = işlemler(sayı);  // (1)

            if (sonuç) {
                break;               // (3)
            }
        }

        return sonuç;                // (5)
    }
}

Bu yapıyı da foreach ile aynı şekilde kullanabiliriz:

    foreach (eleman; Aralık(3, 7)) {
        write(eleman, ' ');
    }

Çıktısı, aralık işlevleri kullanıldığı zamanki çıktının aynısı olacaktır:

3 4 5 6 
Farklı biçimlerde ilerlemek için opApply'ın yüklenmesi

Nesne üzerinde farklı şekillerde ilerleyebilmek, opApply'ın değişik türlerdeki temsilcilerle yüklenmesi ile sağlanır. Derleyici, foreach değişkenlerinin uyduğu bir opApply yüklemesi bulur ve onu çağırır.

Örneğin, Aralık nesnelerinin iki foreach değişkeni ile de kullanılabilmelerini isteyelim:

    foreach (birinci, ikinci; Aralık(0, 15)) {
        writef("%s,%s ", birinci, ikinci);
    }

O kullanım, eşleme tablolarının hem indekslerine hem de elemanlarına foreach ile erişildiği duruma benzer.

Bu örnekte, Aralık yukarıdaki gibi iki değişkenle kullanıldığında art arda iki değere erişiliyor olsun; ve döngünün her ilerletilişinde değerler beşer beşer artsın. Yani yukarıdaki döngünün çıktısı şöyle olsun:

0,1 5,6 10,11 

Bunu sağlamak için iki değişkenli bir temsilci ile çalışan yeni bir opApply tanımlamak gerekir. O temsilci opApply tarafından ve bu kullanıma uygun olan iki değerle çağrılmalıdır:

    int opApply(int delegate(ref int, ref int) işlemler) const {
        int sonuç = 0;

        for (int i = baş; (i + 1) < son; i += 5) {
            int birinci = i;
            int ikinci = i + 1;

            sonuç = işlemler(birinci, ikinci);

            if (sonuç) {
                break;
            }
        }

        return sonuç;
    }

İki değişkenli döngü kullanıldığında üretilen temsilci bu opApply yüklemesine uyduğu için, derleyici bu tanımı kullanır.

Tür için anlamlı olduğu sürece başka opApply işlevleri de tanımlanabilir.

Hangi opApply işlevinin seçileceği döngü değişkenlerinin adedi yanında, türleri ile de belirlenebilir. Değişkenlerin türleri foreach döngüsünde açıkça yazılabilir ve böylece ne tür elemanlar üzerinde ilerlenmek istendiği açıkça belirtilebilir.

Buna göre, foreach döngüsünün hem öğrencilere hem de öğretmenlere erişmek için kullanılabileceği bir Okul sınıfı şöyle tanımlanabilir:

class Okul {
    int opApply(int delegate(ref Öğrenci) işlemler) const {
        // ...
    }

    int opApply(int delegate(ref Öğretmen) işlemler) const {
        // ...
    }
}

Bu Okul türünü kullanan programlar, hangi elemanlar üzerinde ilerleneceğini döngü değişkenini açık olarak yazarak seçebilirler:

    foreach (Öğrenci öğrenci; okul) {
        // ...
    }

    foreach (Öğretmen öğretmen; okul) {
        // ...
    }

Derleyici, değişkenin türüne uyan bir temsilci üretecek ve o temsilciye uyan opApply işlevini çağıracaktır.

Döngü sayacı

foreach'in dizilerle kullanımında kolaylık sağlayan döngü sayacı bütün türler için otomatik değildir. İstendiğinde kendi türlerimiz için açıkça programlamamız gerekir.

Aralık işlevleriyle döngü sayacı

Eğer foreach aralık işlevleriyle sağlanmışsa sayaç elde etmenin en kolay yolu std.range modülünde tanımlı olan ve "numaralandır" anlamına gelen enumerate'ten yararlanmaktır:

import std.range;

// ...

    foreach (i, eleman; Aralık(42, 47).enumerate) {
        writefln("%s: %s", i, eleman);
    }

enumerate sıfırdan başlayan sayılar üretir ve bu sayıları asıl aralığın elemanları ile eşleştirir. (Sıfırdan farklı başlangıç değeri de seçilebilir.) Sonuçta, sayaç ve asıl aralıktaki değerler foreach'in iki döngü değişkeni olarak elde edilirler:

0: 42
1: 43
2: 44
3: 45
4: 46
opApply ile döngü sayacı

foreach desteğinin opApply ile sağlandığı durumda ise sayaç değişkeninin size_t türünde ek bir değişken olarak tanımlanması gerekir. Bunu göstermek için noktalardan oluşan ve kendi rengine sahip olan bir poligon yapısı tasarlayalım.

Bu yapının noktalarını sunan sayaçsız bir opApply yukarıdakilere benzer biçimde şöyle tanımlanabilir:

import std.stdio;

enum Renk { mavi, yeşil, kırmızı }

struct Nokta {
    int x;
    int y;
}

struct Poligon {
    Renk renk;
    Nokta[] noktalar;

    int opApply(int delegate(ref const(Nokta)) işlemler) const {
        int sonuç = 0;

        foreach (nokta; noktalar) {
            sonuç = işlemler(nokta);

            if (sonuç) {
                break;
            }
        }

        return sonuç;
    }
}

void main() {
    auto poligon = Poligon(Renk.mavi,
                           [ Nokta(0, 0), Nokta(1, 1) ] );

    foreach (nokta; poligon) {
        writeln(nokta);
    }
}

opApply'ın tanımında da foreach'ten yararlanıldığına dikkat edin. main içinde poligon nesnesi üzerinde işleyen foreach, poligonun noktalar üyesi üzerinde işletilen bir foreach'ten yararlanmış olur.

delegate'in parametresinin ref const(Nokta) olduğuna dikkat edin. Bu, bu opApply'ın elemanların foreach içinde değiştirilmelerine izin vermediği anlamına gelir. Elemanların değiştirilmelerine izin verilmesi için hem opApply'ın hem de parametresinin const belirteci olmadan tanımlanmaları gerekir.

Çıktısı:

const(Nokta)(0, 0)
const(Nokta)(1, 1)

Poligon türünü bu tanımı ile sayaçlı olarak kullanmaya çalıştığımızda bu kullanım opApply yüklemesine uymayacağından doğal olarak bir derleme hatasıyla karşılaşırız:

    foreach (sayaç, nokta; poligon) {    // ← derleme HATASI
        writefln("%s: %s", sayaç, nokta);
    }

Derleme hatası foreach değişkenlerinin anlaşılamadıklarını bildirir:

Error: cannot uniquely infer foreach argument types

Böyle bir kullanımı destekleyen bir opApply yüklemesi, opApply'ın aldığı temsilcinin size_t ve Nokta türlerinde iki parametre alması ile sağlanmalıdır:

    int opApply(int delegate(ref size_t,
                             ref const(Nokta)) işlemler) const {
        int sonuç = 0;

        foreach (sayaç, nokta; noktalar) {
            sonuç = işlemler(sayaç, nokta);

            if (sonuç) {
                break;
            }
        }

        return sonuç;
    }

Program foreach'in son kullanımını bu opApply yüklemesine uydurur ve artık derlenir:

0: const(Nokta)(0, 0)
1: const(Nokta)(1, 1)

Bu opApply'ın tanımında noktalar üyesi üzerinde işleyen foreach döngüsünün otomatik sayacından yararlanıldığına dikkat edin. (Temsilci parametresi ref size_t olarak tanımlanmış olduğu halde, main içindeki foreach döngüsü noktalar üzerinde ilerleyen otomatik sayacı değiştiremez.)

Gerektiğinde sayaç değişkeni açıkça tanımlanabilir ve arttırılabilir. Örneğin, aşağıdaki opApply bu sefer bir while döngüsünden yararlandığı için sayacı kendisi tanımlıyor ve arttırıyor:

    int opApply(int delegate(ref size_t,
                             ref Eleman) işlemler) const {
        int sonuç = 0;
        bool devam_mı = true;

        size_t sayaç = 0;
        while (devam_mı) {
            // ...

            sonuç = işlemler(sayaç, sıradakiEleman);

            if (sonuç) {
                break;
            }

            ++sayaç;
        }

        return sonuç;
    }
Uyarı: foreach'in işleyişi sırasında topluluk değişmemelidir

Hangi yöntemle olursa olsun, foreach desteği veren bir tür, döngünün işleyişi sırasında sunduğu topluluk kavramında bir değişiklik yapmamalıdır: döngünün işleyişi sırasında yeni elemanlar eklememeli ve var olan elemanları silmemelidir. (Var olan elemanların değiştirilmelerinde bir sakınca yoktur.)

Bu kurala uyulmaması tanımsız davranıştır.

Problemler
  1. Yukarıdaki Aralık gibi çalışan, ama aralıktaki değerleri birer birer değil, belirtilen adım kadar ilerleten bir yapı tanımlayın. Adım bilgisini kurucu işlevinin üçüncü parametresi olarak alsın:
        foreach (sayı; Aralık(0, 10, 2)) {
            write(sayı, ' ');
        }
    

    Sıfırdan 10'a kadar ikişer ikişer ilerlemesi beklenen o Aralık nesnesinin çıktısı şöyle olsun:

    0 2 4 6 8 
    
  2. Yazı içinde geçen Okul sınıfını, foreach'in döngü değişkenlerine göre öğrencilere veya öğretmenlere erişim sağlayacak şekilde yazın.