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 indeksiz olarak tablo elemanlarına; sayı aralıklarında, değerlere; kütüphane türlerinde, her türe uygun ş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 vermeyeceğine ve verecekse 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 */; /* sonrakine geç */) { auto eleman = /* aralığın_başındaki */; // ... ifadeler ... }
foreach'in kendi türlerimizle de çalışabilmesi için yukarıdaki bölümlerde 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. Dolayısıyla, derleyicinin arka planda ürettiği kod şunun eşdeğeri olacaktır:
for ( ; !aralık.empty(); aralık.popFront()) { auto eleman = aralık.front(); // ... ifadeler ... }
Bu üye işlevlerin şu anlamlarda tanımlanmaları gerekir:
.empty(): aralık tükenmişsetrue, değilsefalsedöndürür.popFront(): bir sonrakine geçer (aralığı baş tarafından daraltır).front(): baştaki elemanı döndürür
Bu üç üye işleve sahip olmak, 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 belirlensin. 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, ' '); }
retro, türün iki üye işlevini daha gerektirir:
.popBack(): bir öncekine geçer (aralığı son tarafından daraltır).back(): sondaki elemanı döndürür
Bu iki 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şilebilir. 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 gerekir.
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:
foreach'in işlemleri, kapamayı oluşturan işlemler haline gelirler; bu kapama,opApplytarafından çağrılmalıdır- döngü değişkenleri, kapamanın parametreleri haline gelirler; bu parametrelerin
opApply'ın tanımındarefolarak işaretlenmeleri gerekir - kapamanın dönüş türü
int'tir; buna uygun olarak, kapamanın sonuna derleyici tarafından birreturnsatırı eklenir.return'ün döndürdüğü bilgi, döngünün örneğinbreakile 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ü, artık
opApply'ın içindedir opApply, kapamanın döndürmüş olduğusonlanmaBilgisi'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 öyle iki değişkenle kullanıldığında art arda iki değere erişilsin; 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 uygun 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.
Ters sırada ilerlemek için foreach_reverse ve opApplyReverse
retro, yalnızca aralık işlevleri ile çalışabilir; opApply üye işlevlerinden yararlanamaz. Böyle bir türün ters sırada ilerletilebilmesi için opApplyReverse işlevinin tanımlanmış olması gerekir.
opApplyReverse, opApply ile aynı şekilde tanımlanır ve aynı şekilde işler. Tek farkı, aralıktaki elemanlara ters sırada erişim sağlayacak şekilde yazılmasının gerekmesidir.
D2'de emekliye ayrılan bir döngü türü foreach_reverse'tür. foreach'ten farkı, aralığı ters sırada ilerletmesidir. opApplyReverse işlevini denemek için bu döngüden yararlanabilirsiniz:
foreach_reverse (eleman; Aralık(3, 7)) {
write(eleman, ' ');
}
foreach_reverse ve opApplyReverse olanaklarını, ilerideki bir zamanda dilden çıkartılacaklarını bilerek kullanın.
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
- Yukarıdaki
Aralıkgibi ç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: - Yazı içinde geçen
Okulsınıfını,foreach'in döngü değişkenlerine göre öğrencilere veya öğretmenlere erişim sağlayacak şekilde yazın.
foreach (sayı; Aralık(0, 10, 2)) {
write(sayı, ' ');
}
O kodun çıktısı şöyle olsun:
0 2 4 6 8
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları