canlı ~/duyurular $
Blog & Notlar
not · C#

Döngülerde off-by-one: en sık yaptığımız hata

Dizinin son elemanına ulaşmaya çalışırken bir adım öteye geçmek. Neden olur, nasıl fark edilir, nasıl düzeltilir — haftanın bug'ının uzun anlatımı.

8 dk 8 Eylül 39 görüntüleme #döngü #dizi #hata-ayıklama

Kod derleniyor, tek bir uyarı bile yok. Çalıştırıyorsun ve program tam ortalamayı yazacakken duruyor. Bu notta o tek karakterin peşine düşüyoruz — çünkü sınıfta en çok gördüğüm hata bu.

Hata neye benziyor?

Aşağıdaki program beş öğrencinin notunu topluyor ve ortalamayı yazdırıyor. Mantık ilk bakışta doğru görünüyor: sıfırdan başla, dizinin uzunluğuna kadar git, hepsini topla.

int[] notlar = { 85, 90, 78, 95, 60 };
int toplam = 0;

for (int i = 0; i <= notlar.Length; i++)   // dikkat: <=
{
    toplam += notlar[i];
}

Console.WriteLine("Ortalama: " + toplam / notlar.Length);

Çalıştırınca aldığın mesaj şu:

Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.

Derleyici susuyor, çünkü yazdığın şey dilbilgisi açısından kusursuz. Hata çalışma anında, hem de altıncı turda ortaya çıkıyor.

Neden oluyor?

Çünkü dizinin uzunluğu ile son dizini aynı sayı değil. Saymaya sıfırdan başladığımız için son eleman her zaman bir eksikte durur.

inotlar[i]Ne oluyor?
085tamam
190tamam
278tamam
395tamam
460son geçerli eleman
5—dizinin dışı → program duruyor
Dizilerde ilk elemanın indeksi 0, sonuncusunun indeksi Length − 1’dir.

Beş elemanlı bir dizide Length 5'tir, ama geçerli dizinler 0, 1, 2, 3, 4'tür. Döngü i = 5 olduğunda koşulu hâlâ sağlıyor ve notlar[5] istiyor. Orada senin verin yok; çalışma zamanı kapıyı kapatıyor.

Nasıl düzeltilir

Tek karakter. Küçük eşittir yerine küçüktür:

for (int i = 0; i < notlar.Length; i++)   // doğrusu
{
    toplam += notlar[i];
}
Uzunluk ile son indeks aynı sayı değildir. Karıştırdığında derleyici seni uyarmaz.

Şunu bir kural olarak aklında tut: i < Length ile i <= Length - 1 aynı şeydir. İlkini yaz, ikincisini okuyanı yorma.

foreach ile hiç uğraşmazsın

Dizinin sırasıyla değil de sadece elemanlarıyla işin varsa indeksi hiç yazma:

foreach (int not in notlar)
{
    toplam += not;
}
Sayaç gerekmiyorsa foreach kullan; off-by-one yapma ihtimalin kalmaz.

Burada off-by-one yapma ihtimalin yok, çünkü ortada sayaç yok. Sayaca yalnızca "kaçıncı elemandayım" bilgisi gerektiğinde dön.

İkinci tuzak: tam sayı bölmesi

Döngüyü düzelttin, program çalıştı. Ama ekrana Ortalama: 81 yazıyor — oysa gerçek ortalama 81,6. Toplam 408, bölen 5; ikisi de tam sayı olduğu için C# tam sayı bölmesi yapıyor ve virgülden sonrasını atıyor.

Console.WriteLine(toplam / notlar.Length);           // 81
Console.WriteLine((double)toplam / notlar.Length);   // 81,6
i <= notlar.Length yazarsan program çalışma anında çöker; derleme aşamasında hiçbir uyarı almazsın.

Bölme işleminde bir taraf ondalıklıysa sonuç da ondalıklı olur. (double) yazmak toplamı bozmuyor, yalnızca o işlem için türünü değiştiriyor.

Kendin nasıl yakalarsın

Bir dahaki sefere bana sormadan bulman için üç alışkanlık:

  1. Sınırları elle say. Beş elemanlı dizide döngünün ilk ve son turunu kâğıda yaz: i kaçtan başlıyor, kaçta bitiyor?
  2. Hata mesajını sonuna kadar oku. IndexOutOfRange yazıyorsa sorun mantıkta değil, sınırda. Mesaj konuyu sana zaten söylüyor.
  3. Döngü içine geçici bir yazdırma koy. Console.WriteLine(i); ile hangi turda patladığını tek denemede görürsün.
Hata, kodun sana konuştuğu tek andır. Kızma, dinle.

Sıra sende

  • Yukarıdaki programı yaz, <= ile çalıştır, hatayı kendi ekranında gör.
  • Sonra düzelt ve ortalamayı ondalıklı yazdır.
  • Aynı hatanın bir başka hâli C# Hata Avcısı bölümünde seni bekliyor: "Toplam neden eksik çıkıyor?"
Okuduğunu işaretlemek ve XP kazanmak için giriş yap.
sıradaki not Diziler ve bellekte yer kaplama