D.ershane D Programlama Dili Dersleri

aralık: [range], belirli biçimde erişilen bir grup eleman
kapama: [closure], işlemleri ve işledikleri kapsamı bir arada saklayan program yapısı
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ı
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

Bölümler
İngilizce Kaynaklar
Diğer



Yapı ve Sınıflarda foreach

foreach Döngüsü dersinden 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 yöntem kullanılabilir: türümüzün aralık algoritmalarıyla da kullanılmasına olanak veren aralık işlevleri tanımlamak, veya opApply üye işlevlerini tanımlamak.

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

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ı olabilir.

Ö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;

    this(int baş, int son)
    {
        this.baş = baş;
        this.son = 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:

import std.range;

// ...

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

Türün retro ile kullanılabilmesi için empty yanında iki üye işlev daha gerekir:

Bu iki yeni işlevi Aralık 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;
    }
}

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 işlevleri ile sağlamak

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 kapama oluşturur ve nesnenin opApply işlevini o kapama 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. Kapamayı oluşturan kapsam parantezlerini sarı ile işaretliyorum:

// Derleyicinin arka planda kullandığı kod:

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

Yani, foreach döngüsü ortadan kalkar; onun yerine nesnenin opApply işlevi derleyicinin oluşturduğu bir kapama ile çağrılır. Derleyicinin oluşturduğu bir kapamanın 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, kapamayı oluşturan işlemler haline gelirler; bu kapama, opApply tarafından çağrılmalıdır
  2. döngü değişkenleri, kapamanın parametreleri haline gelirler; bu parametrelerin opApply'ın tanımında ref olarak işaretlenmeleri gerekir
  3. kapamanın dönüş türü int'tir; buna uygun olarak, kapamanın sonuna derleyici tarafından bir return satırı eklenir. return'ün döndürdüğü bilgi, döngünün örneğin break 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, kapamanın döndürmüş olduğu sonlanmaBilgisi'ni döndürmelidir

Aralık sınıfı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;

    this(int baş, int son)
    {
        this.baş = baş;
        this.son = son;
    }

//  (3)                        (2)      (1)
    int opApply(int delegate(ref int) işlemler) const
    {
        int sonuç;

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

        return sonuç;                // (5)
    }
}

Bu sınıfı 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 kapamalarla 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)) {
        write(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 kapama ile çalışan yeni bir opApply tanımlamak gerekir. O kapama, 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ç;

        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 kapama 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 kapama üretecek, ve o kapamaya uyan opApply işlevini çağıracaktır.

Döngü sayacı

foreach'in dizilerle kullanımında kolaylık sağlayan döngü sayacı her tür için otomatik değildir; istendiğinde kendi türlerimiz için açıkça programlamamız gerekir.

Sayaç olanağı aralık işlevleri durumunda uygun değildir, çünkü o kullanımda front'un aralığın elemanlarını döndürmesi beklenir. Bu yüzden sayaç olanağının bir opApply yüklemesi olarak sunulması daha doğru olur. Bunu göstermek için noktalardan oluşan ve kendi rengine sahip olan bir poligon yapısına bakalım:

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

struct Nokta
{
    int x;
    int y;
}

struct Poligon
{
    Renk renk;
    Nokta[] noktalar;

    this(Renk renk, Nokta[] noktalar)
    {
        this.renk = renk;
        this.noktalar = noktalar;
    }
}

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

import std.stdio;

// ...

struct Poligon
{
// ...

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

        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);
    }
}

Not: 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ış oluyor.

Çıktısı:

Nokta(0, 0)
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 bir derleme hatasıyla karşılaşırız:

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

Derleme hatası sayaç'ın ve nokta'nın türlerinin anlaşılamadıklarını bildirir:

Error: cannot infer type for sayaç
Error: cannot infer type for nokta

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

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

        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: Nokta(0, 0)
1: Nokta(1, 1)

Bu opApply'ın tanımında ise noktalar üyesi üzerinde işleyen foreach döngüsünün otomatik sayacından yararlanıldığına dikkat edin. 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ç;
        bool devam_mı = true;

        size_t sayaç;
        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.

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 sınıf tanımlayın. Adım bilgisini kurucu işlevinin üçüncü parametresi olarak alsın:
  2.     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 
    
  3. 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.

... çözümler