Mert Özen Her Satırda Daha İleri
Backend C# Temelleri — #09

C# Döngüler: for, while, do-while ve foreach

Mert Özen 7 Kas 2026 10 dk 1 görüntülenme
C# Döngüler: for, while, do-while ve foreach

C#'taki dört döngü ve ne zaman hangisi: for, while, do-while ve foreach. break ve continue, iç içe döngüden temiz çıkmak, sonsuz döngü pattern'i ve gezerken collection'ı değiştirmenin neden exception fırlattığı.

Sürüm notu: Bu yazıdaki kodlar C# 10 ve .NET 6 API'leriyle yazılmıştır; .NET 10 SDK ile derlenip denenmiştir.

Önce Tahmin Et

Müşterinin sepetinde üç kitap var ve "Emma"yı sepetten çıkarmak istiyoruz. Sepeti foreach ile geziyor, aradığımız kitabı bulunca listeden siliyoruz. Kod derleniyor. Sonunda ekrana 2 mi yazar, 3 mü, yoksa başka bir şey mi olur?

var cart = new List<string> { "Dune", "Emma", "Ulysses" };

foreach (string title in cart)
{
    if (title == "Emma")
    {
        cart.Remove(title);
    }
}

Console.WriteLine(cart.Count);

Ekrana hiçbir sayı yazılmaz; program exception ile kapanır:

Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 3

"Emma" listeden gerçekten silindi, ama foreach bir sonraki element'e geçmeye çalıştığı anda durdu. List<T> her değişiklikte içindeki bir sürüm sayacını artırır; foreach'in kullandığı enumerator her adımda bu sayacı kontrol eder ve gezdiği listenin değiştiğini görünce devam etmeyi reddeder. Gezerken ayağının altındaki halıyı çekmene izin vermez. Bu yazıda C#'taki dört döngüyü, break ve continue'yu, iç içe döngüden çıkmanın yollarını ve bu tuzağın doğru çözümünü göreceğiz.

for Döngüsü

Kaç kez döneceğini bildiğin ya da bir index'e ihtiyaç duyduğun durumlarda for kullanırsın:

string[] titles = { "Dune", "Emma", "Ulysses" };

for (int i = 0; i < titles.Length; i++)
{
    Console.WriteLine($"{i + 1}. {titles[i]}");
}

for (int i = titles.Length - 1; i >= 0; i--)
{
    Console.Write(titles[i] + " ");
}
Console.WriteLine();

for (int i = 0; i < 10; i += 3)
{
    Console.Write(i + " ");
}
Console.WriteLine();
1. Dune
2. Emma
3. Ulysses
Ulysses Emma Dune
0 3 6 9

Parantezin içinde noktalı virgülle ayrılmış üç parça var. İlki başlangıç: döngü değişkeni bir kez tanımlanır. İkincisi koşul: her turdan önce kontrol edilir, false olduğu anda döngü biter. Üçüncüsü adım: her turun sonunda çalışır. İkinci döngü sondan başa sayıyor, üçüncüsü üçer üçer ilerliyor; üç parçaya istediğin expression'ı yazabilirsin.

for'un parantezinde tanımlanan değişken yalnızca döngünün içinde yaşar:

for (int i = 0; i < 3; i++)
{
}

Console.WriteLine(i);
Program.cs(5,19): error CS0103: The name 'i' does not exist in the current context

Bu iyi bir şey. Arka arkaya iki döngüde de i adını rahatça kullanabilmeni sağlıyor ve döngü bittikten sonra yanlışlıkla eski bir index değerine dokunmanı engelliyor. Döngü sonrasında son değere gerçekten ihtiyacın varsa değişkeni döngüden önce tanımlarsın.

while Döngüsü

Kaç kez döneceğini önceden bilmiyorsan, yalnızca "şu koşul sürdükçe" diyebiliyorsan while daha doğal. Her gün 7 kitap satan bir stoğun kaç günde biteceği:

int stock = 20;
int day = 0;

while (stock > 0)
{
    day++;
    stock -= 7;
    Console.WriteLine($"Day {day}: {Math.Max(stock, 0)} left");
}
Day 1: 13 left
Day 2: 6 left
Day 3: 0 left

while koşulu en başta kontrol eder. Stok baştan 0 olsaydı döngünün body'si bir kez bile çalışmazdı. Dikkat etmen gereken tek şey, body'nin içinde koşulu eninde sonunda false yapacak bir değişikliğin olması; burada stock -= 7 o işi görüyor. Bu satırı unutursan döngü sonsuza kadar döner.

do-while Döngüsü

do-while, koşulu sonda kontrol eder; body en az bir kez mutlaka çalışır. Konsol uygulamalarındaki menüler bunun klasik örneği, çünkü menüyü kullanıcı daha hiçbir şey seçmeden göstermen gerekir:

string choice;

do
{
    Console.WriteLine("1) List books  2) Add book  q) Quit");
    choice = Console.ReadLine() ?? "q";
} while (choice != "q");

Console.WriteLine("Bye");

Programı çalıştırıp sırayla x, 2 ve q yazdığımda konsol şöyle görünüyor:

1) List books  2) Add book  q) Quit
x
1) List books  2) Add book  q) Quit
2
1) List books  2) Add book  q) Quit
q
Bye

Console.ReadLine() input stream bittiğinde null döndürebilir; bu yüzden dönüş tipi string?. ?? "q" ile o durumda da döngüden çıkıyoruz. Null konusunu ayrı bir yazıda açacağız. while (choice != "q"); satırının sonundaki noktalı virgül burada zorunlu; birazdan aynı noktalı virgülün while döngüsünde nasıl bir felakete yol açtığını göreceğiz.

foreach Döngüsü

Bir collection'ın bütün element'lerini baştan sona gezeceksen ve index'e ihtiyacın yoksa foreach en okunaklı seçenek:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

decimal[] prices = { 12.50m, 8.90m, 21.00m };
decimal total = 0m;

foreach (decimal price in prices)
{
    total += price;
}

Console.WriteLine(total);

foreach (char c in "ISBN")
{
    Console.Write(c + "-");
}
Console.WriteLine();

var stockByTitle = new Dictionary<string, int> { ["Dune"] = 3, ["Emma"] = 0 };
foreach (KeyValuePair<string, int> entry in stockByTitle)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}
42.40
I-S-B-N-
Dune: 3
Emma: 0

foreach array'lerde, List<T>'de, string'in karakterlerinde, Dictionary'de ve kısaca IEnumerable olan her şeyde çalışır. Dictionary'de her element bir KeyValuePair olarak gelir. Index hesabı olmadığı için "bir eksik, bir fazla" hatası yapma ihtimalin de yoktur.

foreach'in döngü değişkeni read-only'dir:

decimal[] prices = { 12.50m, 8.90m, 21.00m };

foreach (decimal price in prices)
{
    price = price * 0.9m;
}
Program.cs(5,5): error CS1656: Cannot assign to 'price' because it is a 'foreach iteration variable'

Değişkene yeni bir değer atasaydın bile bu, array'deki değeri değiştirmezdi; price element'in bir kopyası. Array'deki fiyatları güncellemek istiyorsan index'e ihtiyacın var, yani for. Bu kopyalama meselesinin arkasındaki value type ve reference type farkını ileride ayrı bir yazıda konuşacağız.

break ve continue

continue, döngünün o turunu yarıda bırakıp bir sonraki tura geçer. break ise döngüyü tamamen bitirir:

int[] stock = { 3, 0, 5, 0, 2 };

for (int i = 0; i < stock.Length; i++)
{
    if (stock[i] == 0)
    {
        continue;
    }

    Console.WriteLine($"Shelf {i}: {stock[i]} copies");
}

int firstEmpty = -1;
for (int i = 0; i < stock.Length; i++)
{
    if (stock[i] == 0)
    {
        firstEmpty = i;
        break;
    }
}
Console.WriteLine($"First empty shelf: {firstEmpty}");
Shelf 0: 3 copies
Shelf 2: 5 copies
Shelf 4: 2 copies
First empty shelf: 1

İlk döngü stoğu sıfır olan rafları atlıyor. continue'yu "bu element beni ilgilendirmiyor" demenin kısa yolu olarak düşün: body'nin geri kalanını koca bir if'in içine almaktan kurtarır. İkinci döngü ilk boş rafı bulunca duruyor; aradığını bulduktan sonra listenin geri kalanını gezmenin anlamı yok.

İç İçe Döngüden Çıkmak

break yalnızca içinde bulunduğu en yakın döngüyü bitirir. Raflardaki kitapları iki döngüyle arıyorsak ve kitabı bulduğumuzda ikisinden de çıkmak istiyorsak bu bir sorun:

string[][] shelves =
{
    new[] { "Dune", "Emma" },
    new[] { "Ulysses", "Hamlet", "Beloved" },
    new[] { "Ivanhoe" }
};

string target = "Hamlet";
bool found = false;

for (int shelf = 0; shelf < shelves.Length; shelf++)
{
    for (int slot = 0; slot < shelves[shelf].Length; slot++)
    {
        Console.WriteLine($"Checking {shelf}/{slot}");
        if (shelves[shelf][slot] == target)
        {
            found = true;
            break;
        }
    }

    if (found)
    {
        break;
    }
}

Console.WriteLine(found);
Checking 0/0
Checking 0/1
Checking 1/0
Checking 1/1
True

İç döngüdeki break sadece o rafın taramasını bitirdi; dış döngüden çıkmak için bir found bayrağı tutup dış döngüde de kontrol etmemiz gerekti. Çalışıyor, ama iki seviyede bile kod kalabalıklaşıyor. Benim tercihim aramayı bir method'a çıkarmak ve sonucu bulunca return etmek:

string[][] shelves =
{
    new[] { "Dune", "Emma" },
    new[] { "Ulysses", "Hamlet", "Beloved" },
    new[] { "Ivanhoe" }
};

Console.WriteLine(FindBook(shelves, "Hamlet"));
Console.WriteLine(FindBook(shelves, "Emma"));
Console.WriteLine(FindBook(shelves, "Odyssey"));

static string FindBook(string[][] shelves, string title)
{
    for (int shelf = 0; shelf < shelves.Length; shelf++)
    {
        for (int slot = 0; slot < shelves[shelf].Length; slot++)
        {
            if (shelves[shelf][slot] == title)
            {
                return $"shelf {shelf}, slot {slot}";
            }
        }
    }

    return "not found";
}
shelf 1, slot 1
shelf 0, slot 1
not found

return method'u, dolayısıyla içindeki bütün döngüleri tek hamlede bitirir. Flag yok, ek kontrol yok ve method'un adı ne yaptığını anlatıyor. C#'ta goto ile bir etikete atlamak da mümkün, ama gerçek kodda çok nadir görürsün ve okuyan herkesi durdurur. C# 15 ile döngülere etiket verip break ve continue'yu doğrudan dış döngüye yönlendirmek de mümkün hâle geliyor; bunu C# Sürümleri serisinde göreceğiz.

Sonsuz Döngü

Bazen döngünün ne zaman biteceği en başta değil, body'nin ortasında belli olur. Kullanıcıdan "quit" yazana kadar komut alan bir konsol uygulaması gibi. Bunun yerleşik kalıbı while (true) ve içeride bir break:

while (true)
{
    Console.Write("Command: ");
    string? command = Console.ReadLine();
    if (command is null or "quit")
    {
        break;
    }

    Console.WriteLine($"Running {command}");
}

Console.WriteLine("Loop finished");

Sırayla list, add ve quit yazdığımda:

Command: list
Running list
Command: add
Running add
Command: quit
Loop finished

is null or "quit" önceki yazıda gördüğümüz logical pattern; input stream biterse de döngüden çıkıyoruz. Aynı pattern'in for (;;) şeklinde yazılmış hâlini de eski kodlarda görebilirsin, ikisi aynı işi yapar. Sonsuz döngü kendi başına kötü bir şey değil; sunucular, oyun döngüleri ve queue dinleyen arka plan işleri özünde birer sonsuz döngüdür. Önemli olan çıkış yolunun açık ve görünür olması.

Sık Düşülen Tuzaklar

while'dan sonra noktalı virgül koymak. Aşağıdaki kod derlenir ve hiçbir zaman bitmez:

int count = 0;

while (count < 3);
{
    count++;
}

Console.WriteLine(count);
Program.cs(3,18): warning CS0642: Possible mistaken empty statement

while (count < 3); satırı, body'si boş bir döngü. count hiç artmadığı için koşul hep doğru kalıyor ve program o satırda sonsuza kadar dönüyor; altındaki süslü parantezli blok ise döngüyle ilgisi olmayan sıradan bir blok. Compiler'ın CS0642 uyarısı tam olarak bunu söylüyor. Bu uyarıyı gördüğünde başka hiçbir şeye bakmadan noktalı virgülü sil.

Sınırı bir fazla almak. < yerine <= yazmak klasik "off-by-one" hatası:

string[] titles = { "Dune", "Emma", "Ulysses" };

for (int i = 0; i <= titles.Length; i++)
{
    Console.WriteLine(titles[i]);
}
Dune
Emma
Ulysses
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 5

Index'ler 0'dan başlar, son geçerli index Length - 1'dir. Bir collection'ın tamamını gezeceksen foreach kullanmak bu hatayı tamamen ortadan kaldırır.

Döngü sayacı olarak double kullanmak. 0.1'i on kez topladığında tam olarak 1.0 elde etmezsin, sayılar yazısında gördüğümüz kayan nokta hatası yüzünden:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

int steps = 0;
for (double x = 0.0; x != 1.0; x += 0.1)
{
    steps++;
    if (steps > 15)
    {
        Console.WriteLine($"Still running, x = {x:R}");
        break;
    }
}
Still running, x = 1.5000000000000002

Koşul != olduğu için döngü 1.0'ı hiç tutturamadan yanından geçti. Örnekteki 15 adımlık emniyet olmasaydı sonsuza kadar dönecekti. Sayacı int tut, ondalık değeri body'de hesapla; mecbur kalırsan da koşulda != yerine < kullan.

Gezerken collection'ı değiştirmek. Yazının başındaki exception'ın iki doğru çözümü var. Index ile sondan başa doğru gezip silmek, ya da işi List<T>'nin kendisine bırakmak:

var cart = new List<string> { "Dune", "Emma", "Ulysses", "Emma" };

for (int i = cart.Count - 1; i >= 0; i--)
{
    if (cart[i] == "Emma")
    {
        cart.RemoveAt(i);
    }
}
Console.WriteLine(string.Join(", ", cart));

var cart2 = new List<string> { "Dune", "Emma", "Ulysses", "Emma" };
cart2.RemoveAll(title => title == "Emma");
Console.WriteLine(string.Join(", ", cart2));
Dune, Ulysses
Dune, Ulysses

Sondan başa gezmek önemli, çünkü baştan gezerken bir element'i sildiğinde ondan sonraki element'ler bir sola kayar ve hemen arkadaki element'i atlarsın. RemoveAll ise niyeti tek satırda anlatıyor; içindeki title => title == "Emma" bir lambda, onu da ileride ayrıntılı göreceğiz.

Küçük Bir Deneme

★☆☆ 1'den 100'e kadar olan sayılardan 7'ye tam bölünenleri for ve continue kullanarak ekrana yazdır.

★★☆ Bir decimal[] fiyat listesinde, toplam 50'yi geçene kadar fiyatları topla ve kaçıncı kitapta sınırı aştığını yazdır. Önce for ve break ile, sonra while ile yaz; hangisi daha okunaklı?

★★★ do-while ile bir menü yaz: 1 kitap ekler (adını Console.ReadLine() ile alır), 2 listeyi yazar, 3 adı verilen kitabı siler, q çıkar. Silme işleminde yazının başındaki exception'a düşmediğinden emin ol.

Döngülerde en çok gezdiğimiz şey array'lerdi, ama onları hep hazır gibi kullandık. Bir sonraki yazıda array'leri yakından tanıyoruz: tek ve çok boyutlu array'ler, jagged array'ler, Array class'ının method'ları, C# 8 ile gelen Index ve Range (^1, ..) ve array'in neden bir reference type olduğu.