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:
- Türün aralık algoritmalarıyla da kullanılmasına olanak veren aralık işlevleri tanımlamak
- Tür için
opApply
üye işlevleri tanımlamak
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:
.empty()
: Aralık tükenmişsetrue
, değilsefalse
döndürür.popFront()
: Bir sonrakine geçer (aralığı baş tarafından daraltır).front()
: Baştaki elemanı döndürür
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:
.popBack()
: Bir öncekine geçer (aralığı son tarafından daraltır).back()
: Sondaki elemanı döndürür
Ancak, retro
'nun o iki işlevi kullanabilmesi için bir işlevin daha tanımlanmış olması gerekir:
.save()
: Aralığın şu andaki durumunun kopyasını döndürür
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:
foreach
'in işlemleri temsilciyi oluşturan işlemler haline gelirler. Bu temsilciopApply
tarafından çağrılmalıdır.- Döngü değişkenleri temsilcinin parametreleri haline gelirler. Bu parametrelerin
opApply
'ın tanımındaref
olarak işaretlenmeleri gerekir. (Parametreler aslındaref
anahtar sözcüğünü kullanmadan da tanımlanabilirler ama o zaman elemanlara referans olarak erişilemez.) - Temsilcinin dönüş türü
int
'tir. Buna uygun olarak, temsilcinin sonuna derleyici tarafından birreturn
satırı eklenir.return
'ün döndürdüğü bilgi, döngününbreak
veyareturn
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. - Asıl döngü
opApply
'ın içinde programcı tarafından gerçekleştirilir. opApply
, temsilcinin döndürmüş olduğusonlandı_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
- 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
- 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.