foreach
Döngüsü
foreach
D'nin en kullanışlı deyimlerinden birisidir. "Her birisi için" anlamına gelir. Belirli işlemleri bir topluluktaki (veya bir aralıktaki) elemanların her birisi ile yapmayı sağlar.
Topluluk elemanlarının tümüyle yapılan işlemler programcılıkta çok yaygındır. for
döngüsünün bir dizinin bütün elemanlarına erişmek için nasıl kullanıldığını görmüştük:
for (int i = 0; i != dizi.length; ++i) { writeln(dizi[i]); }
Bu iş için gereken adımları şöyle özetleyebiliriz:
- İsmi geleneksel olarak
i
olan bir sayaç tanımlamak (aslında biz önceki örneklerde hepsayaç
dedik) - Döngüyü topluluğun
.length
niteliğine kadar ilerletmek i
'yi arttırmak- Elemana erişmek
Bu adımlar ayrı ayrı elle yapılmak yerine foreach
ile çok daha basit olarak şöyle ifade edilir:
foreach (eleman; dizi) {
writeln(eleman);
}
foreach
'in güçlü yanlarından birisi, eşleme tabloları ile de aynı biçimde kullanılabilmesidir. for
döngüsünde ise, örneğin bir eşleme tablosunun bütün elemanlarına erişmek için tablo'nun .values
niteliği çağrılır:
auto elemanlar = tablo.values; for (int i = 0; i != elemanlar.length; ++i) { writeln(elemanlar[i]); }
foreach
eşleme tabloları için özel bir kullanım gerektirmez; eşleme tabloları da dizilerle aynı biçimde kullanılır:
foreach (eleman; tablo) {
writeln(eleman);
}
Söz dizimi
foreach
üç bölümden oluşur:
foreach (isimler; topluluk_veya_aralık) {
işlem_bloğu
}
- topluluk_veya_aralık: döngünün işletileceği elemanları belirler
- işlem_bloğu: her elemanla yapılacak işlemleri belirler
- isimler: erişilen elemanın ve varsa başka nesnelerin isimlerini belirler; seçilen isimler programcıya bağlı olsa da, bunların anlamı ve adedi topluluk çeşidine göre değişir
continue
ve break
Bu anahtar sözcüklerin ikisi de burada da aynı anlama gelirler: continue
döngünün erkenden ilerletilmesini, break
de döngünün sonlandırılmasını bildirir.
Dizilerle kullanımı
isimler bölümüne yazılan tek isim, dizinin elemanını ifade eder:
foreach (eleman; dizi) {
writeln(eleman);
}
Eğer iki isim yazılırsa birincisi otomatik bir sayaçtır, ikincisi yine elemanı ifade eder:
foreach (sayaç, eleman; dizi) { writeln(sayaç, ": ", eleman); }
Sayacın değeri foreach
tarafından otomatik olarak arttırılır. Sayaç değişkeninin ismi programcıya kalmış olsa da isim olarak i
de çok yaygındır.
Dizgilerle kullanımı ve std.range.stride
Dizilerle aynı şekilde kullanılır. Tek isim yazılırsa dizginin karakterini ifade eder, çift isim yazılırsa sayaç ve karakterdir:
foreach (karakter; "merhaba") { writeln(karakter); } foreach (sayaç, karakter; "merhaba") { writeln(sayaç, ": ", karakter); }
char
ve wchar
türlerinin Unicode karakterlerini barındırmaya genel olarak uygun olmadıklarını hatırlayın. foreach
bu türlerle kullanıldığında karakterlere değil, kod birimlerine erişilir:
foreach (sayaç, kod; "abcçd") { writeln(sayaç, ": ", kod); }
Örneğin ç'yi oluşturan kodlara ayrı ayrı erişilir:
0: a 1: b 2: c 3: 4: � 5: d
UTF kodlamasından bağımsız olarak her tür dizginin foreach
ile karakter karakter erişilmesini sağlayan olanak, std.range
modülündeki stride
'dır. stride
"adım" anlamına gelir ve karakterlerin kaçar kaçar atlanacağı bilgisini de alır:
import std.range; // ... foreach (harf; stride("abcçd", 1)) { writeln(harf); }
stride
kullanıldığında UTF kodlarına değil Unicode karakterlerine erişilir:
a b c ç d
Bu kodda neden sayaç kullanılamadığını biraz aşağıda açıklayacağım.
Eşleme tablolarıyla kullanımı
Tek isim yazılırsa eleman değerini, iki isim yazılırsa indeks ve eleman değerini ifade eder:
foreach (eleman; tablo) { writeln(eleman); } foreach (indeks, eleman; tablo) { writeln(indeks, ": ", eleman); }
Not: Eşleme tablolarında indeksin de herhangi bir türden olabileceğini hatırlayın. O yüzden bu döngüde sayaç
yazmadım.
Eşleme tabloları indekslerini ve elemanlarını aralıklar olarak da sunabilirler. Aralıkları daha ilerideki bir bölümde göreceğiz. Eşleme tablolarının .byKey
, .byValue
, ve .byKeyValue
nitelikleri foreach
döngülerinden başka ortamlarda da kullanılabilen hızlı aralık nesneleri döndürürler.
.byValue
, foreach
döngülerinde yukarıdaki elemanlı döngü ile karşılaştırıldığında fazla bir yarar sağlamaz. .byKey
ise bir eşleme tablosunun yalnızca indeksleri üzerinde ilerlemenin en hızlı yoludur:
foreach (indeks; tablo.byKey) { writeln(indeks); }
.byKeyValue
çokuzlu gibi kullanılan bir değişken döndürür. İndeks ve eleman değerleri o değişkenin .key
ve .value
nitelikleri ile elde edilir:
foreach (eleman; tablo.byKeyValue) { writefln("%s indeksinin değeri: %s", eleman.key, eleman.value); }
Sayı aralıklarıyla kullanımı
Sayı aralıklarını Başka Dizi Olanakları bölümünde görmüştük. foreach
'in topluluk_veya_aralık bölümüne bir sayı aralığı da yazılabilir:
foreach (sayı; 10..15) {
writeln(sayı);
}
Hatırlarsanız; yukarıdaki kullanımda 10 aralığa dahildir, 15 değildir.
Yapılarla, sınıflarla, ve aralıklarla kullanımı
foreach
, bu desteği veren yapı, sınıf, ve aralık nesneleriyle de kullanılabilir. Nasıl kullanıldığı hakkında burada genel bir şey söylemek olanaksızdır, çünkü tamamen o tür tarafından belirlenir. foreach
'in nasıl işlediğini ancak söz konusu yapının, sınıfın, veya aralığın belgesinden öğrenebiliriz.
Yapılar ve sınıflar foreach
desteğini ya opApply()
isimli üye işlevleri ya da aralık (range) üye işlevleri aracılığıyla verirler; aralıklar ise bu iş için aralık üye işlevleri tanımlarlar. Bu olanakları daha sonraki bölümlerde göreceğiz.
Sayaç yalnızca dizilerde otomatiktir
Otomatik sayaç olanağı yalnızca dizilerde bulunur. foreach
başka çeşit türlerle kullanılırken sayaç gerektiğinde iki seçenek vardır:
- Daha sonra Yapı ve Sınıflarda
foreach
bölümünde göreceğimizstd.range.enumerate
'ten yararlanmak. - Bir sayaç değişkeni tanımlamak ve elle arttırmak.
size_t sayaç = 0; foreach (eleman; topluluk) { // ... ++sayaç; }
Böyle bir değişken sayacın döngünün her ilerletilişinde değil, belirli bir koşul sağlandığında arttırılması gerektiğinde de yararlı olur. Örneğin, aşağıdaki döngü yalnızca 10'a tam olarak bölünen sayıları sayar:
import std.stdio; void main() { auto dizi = [ 1, 0, 15, 10, 3, 5, 20, 30 ]; size_t sayaç = 0; foreach (sayı; dizi) { if ((sayı % 10) == 0) { ++sayaç; write(sayaç); } else { write(' '); } writeln(": ", sayı); } }
Çıktısı:
: 1 1: 0 : 15 2: 10 : 3 : 5 3: 20 4: 30
Elemanın kopyası, kendisi değil
foreach
döngüsü; normalde elemanın kendisine değil, bir kopyasına erişim sağlar. Topluluk elemanlarının yanlışlıkla değiştirilmelerini önlemek amacıyla böyle tasarlandığını düşünebilirsiniz.
Bir dizinin elemanlarının her birisini iki katına çıkartmaya çalışan şu koda bakalım:
import std.stdio; void main() { double[] sayılar = [ 1.2, 3.4, 5.6 ]; writefln("Önce : %s", sayılar); foreach (sayı; sayılar) { sayı *= 2; } writefln("Sonra: %s", sayılar); }
Programın çıktısı, foreach
kapsamında sayı
'ya yapılan atamanın etkisi olmadığını gösteriyor:
Önce : 1.2 3.4 5.6 Sonra: 1.2 3.4 5.6
Bunun nedeni, sayı
'nın dizi elemanının kendisi değil, onun bir kopyası olmasıdır. Dizi elemanının kendisinin ifade edilmesi istendiğinde, isim bir referans olarak tanımlanır:
foreach (ref sayı; sayılar) { sayı *= 2; }
Yeni çıktıda görüldüğü gibi, ref
anahtar sözcüğü dizideki asıl elemanın etkilenmesini sağlamıştır:
Önce : 1.2 3.4 5.6 Sonra: 2.4 6.8 11.2
Oradaki ref
anahtar sözcüğü, sayı
'yı asıl elemanın bir takma ismi olarak tanımlar. sayı
'da yapılan değişiklik artık elemanın kendisini etkilemektedir.
Topluluğun kendisi değiştirilmemelidir
Topluluk elemanlarını ref
olarak tanımlanmış olan değişkenler aracılığıyla değiştirmekte bir sakınca yoktur. Ancak, foreach
döngüsü kapsamında topluluğun kendi yapısını etkileyecek hiçbir işlem yapılmamalıdır. Örneğin diziden eleman silinmemeli veya diziye eleman eklenmemelidir.
Bu tür işlemler topluluğun yapısını değiştireceklerinden, ilerlemekte olan foreach
döngüsünün işini bozarlar. O noktadan sonra programın davranışının ne olacağı bilinemez.
Ters sırada ilerlemek için foreach_reverse
foreach_reverse
foreach
ile aynı biçimde işler ama aralığı ters sırada ilerler:
auto elemanlar = [ 1, 2, 3 ]; foreach_reverse (eleman; elemanlar) { writefln("%s ", eleman); }
Çıktısı:
3 2 1
foreach_reverse
'ün kullanımı yaygın değildir. Çoğunlukla onun yerine daha sonra göreceğimiz retro()
isimli aralık işlevi kullanılır.
Problem
Eşleme tablolarının indeks değerleri ile eleman değerlerini eşlediklerini görmüştük. Bu tek yönlüdür: indeks verildiğinde eleman değerini elde ederiz, ama eleman değeri verildiğinde indeks değerini elde edemeyiz.
Elinizde hazırda şöyle bir eşleme tablosu olsun:
string[int] isimle = [ 1:"bir", 7:"yedi", 20:"yirmi" ];
O tablodan ve tek bir foreach
döngüsünden yararlanarak, rakamla
isminde başka bir eşleme tablosu oluşturun. Bu yeni tablo, isimle
tablosunun tersi olarak çalışsın: isime karşılık rakam elde edebilelim. Örneğin
writeln(rakamla["yirmi"]);
yazdığımızda çıktı şöyle olsun:
20