D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

bağlı liste: [linked list], her elemanı bir sonraki elemanı gösteren veri yapısı
blok: [block], küme parantezleriyle gruplanmış ifadelerin tümü
çağrı yığıtı: [call stack], belleğin kısa ömürlü değişkenler ve işlev çağrıları için kullanılan bölgesi
deyim: [statement], ifadelerin işletilmelerini ve sıralarını etkileyen program yapısı
hata atma: [throw exception], işlemin devam edilemeyeceği için sonlandırılması
ifade: [expression], programın değer oluşturan veya yan etki üreten bir bölümü
ikincil hata: [collateral exception], hata atılması sırasında atılan başka hata
işlev: [function], programdaki bir kaç adımı bir araya getiren program parçası
kapsam: [scope], küme parantezleriyle belirlenen bir alan
klasör: [directory], dosyaları barındıran dosya sistemi yapısı, "dizin"
kurma: [construct], yapı veya sınıf nesnesini kullanılabilir duruma getirmek
nesne: [object], belirli bir sınıf veya yapı türünden olan değişken
nitelik: [property, attribute], bir türün veya nesnenin bir özelliği
Phobos: [Phobos], D dilinin standart kütüphanesi
sınıf: [class], kendi üzerinde kullanılan işlevleri de tanımlayan veri yapısı
sonlandırıcı işlev: [destructor], nesneyi sonlandıran işlev
türetmek: [inherit], bir sınıfı başka sınıfın alt türü olarak tanımlamak
... bütün sözlük



İngilizce Kaynaklar


Diğer




Hata Yönetimi

Beklenmedik durumlar programların yaşamlarının doğal parçalarıdır. Kullanıcı hataları, programcı hataları, ortamdaki beklenmedik değişiklikler, vs. programların çalışmaları sırasında her zaman karşılaşılan durumlardır.

Bu durumlar bazen normal işleyişe devam edilemeyecek kadar vahim olabilir. Örneğin gereken bir bilgi elde edilemiyordur, eldeki bilgi geçersizdir, bir çevre aygıtı çalışmıyordur, vs. Böyle çaresiz kalınan durumlarda D'nin hata atma düzeneği kullanılarak işleme son verilir.

Devam edilemeyecek kadar kötü bir durum örneği olarak yalnızca dört aritmetik işlemi destekleyen bir işlevin bunların dışındaki bir işlemle çağrılması durumunu düşünebilirsiniz. Önceki bölümün problem çözümlerinde de olduğu gibi:

    switch (işlem) {

    case "+":
        writeln(birinci + ikinci);
        break;

    case "-":
        writeln(birinci - ikinci);
        break;

    case "x":
        writeln(birinci * ikinci);
        break;

    case "/":
        writeln(birinci / ikinci);
        break;

    default:
        throw new Exception(format("Geçersiz işlem: %s", işlem));
    }

Yukarıdaki switch deyiminde case'lerle belirtilmiş olan dört işlem dışında ne yapılacağı bilinmemektedir. O yüzden deyimin default kapsamında bir hata atılmaktadır.

Çaresiz durumlarda atılan hata örnekleriyle Phobos'ta da karşılaşırız. Örneğin bir dizgiyi int türüne dönüştürmek için kullanılan to!int, int olamayacak bir dizgiyle çağrıldığında hata atar:

import std.conv;

void main() {
    const int sayı = to!int("merhaba");
}

"merhaba" dizgisi bir tamsayı değer ifade etmediği için; o program, to!int'in attığı bir hatayla sonlanır.

# ./deneme
std.conv.ConvException@std/conv.d(37): std.conv(1161): Can't convert
value `merhaba' of type const(char)[] to type int

to!int'in attığı yukarıdaki hatayı şu şekilde çevirebiliriz: "const(char)[] türündeki `merhaba' değeri int türüne dönüştürülemez".

Hata mesajının baş tarafındaki std.conv.ConvException da hatanın türünü belirtir. Bu hatanın ismine bakarak onun std.conv modülü içinde tanımlanmış olduğunu anlayabiliyoruz. İsmi de "dönüşüm hatası" anlamına gelen "conversion exception"dan türemiş olan ConvException'dır.

Hata atmak için throw

Bunun örneklerini hem yukarıdaki switch deyiminde, hem de daha önceki bölümlerde gördük.

Anlamı "at, fırlat" olan throw deyimi, kendisinden sonra yazılan ifadenin değerini bir hata nesnesi olarak atar ve işleme hemen son verilmesine neden olur. throw deyiminden sonraki adımlar işletilmez. Bu, hata kavramına uygun bir davranıştır: hatalar işlemlere devam edilemeyecek durumlarda atıldıkları için, zaten devam etmek söz konusu olmamalıdır.

Başka bir bakış açısıyla; eğer işleme devam edilebilecek gibi bir durumla karşılaşmışsak, hata atılacak kadar çaresiz bir durum yok demektir. O durumda hata atılmaz ve işlev bir çaresini bulur ve işine devam edebilir.

Exception ve Error hata türleri

throw deyimi ile yalnızca Throwable türünden türemiş olan nesneler atılabilir. Buna rağmen, programlarda ondan da türemiş olan Exception ve Error türleri kullanılır. Örneğin Phobos'taki hatalar ya Exception sınıfından, ya da Error sınıfından türemişlerdir. Error, giderilemez derecede hatalı durumları ifade eder. O hatanın yakalanması önerilmez. Bu yüzden, atacağınız hataları ya doğrudan Exception'dan, ya da ondan türeteceğiniz daha belirgin türlerden atmanız gerekir. (Not: Sınıflarla ilgili bir konu olan türemeyi daha sonra göreceğiz.)

Exception nesneleri, kurulurlarken hata mesajını string olarak alırlar. Bu mesajı, std.string modülündeki format işlevi ile oluşturmak kolaylık sağlar:

import std.stdio;
import std.string;
import std.random;

int[] rasgeleZarlarAt(int adet) {
    if (adet < 0) {
        throw new Exception(
            format("Geçersiz 'adet' değeri: %s", adet));
    }

    int[] sayılar;

    foreach (i; 0 .. adet) {
        sayılar ~= uniform(1, 7);
    }

    return sayılar;
}

void main() {
    writeln(rasgeleZarlarAt(-5));
}
# ./deneme
object.Exception: Geçersiz 'adet' değeri: -5

Çoğu durumda, new ile açıkça hata nesnesi oluşturmak ve throw ile açıkça atmak yerine bu adımları kapsayan enforce() işlevi kullanılır. Örneğin, yukarıdaki denetimin eşdeğeri aşağıdaki enforce() çağrısıdır:

    enforce(adet >= 0, format("Geçersiz 'adet' değeri: %s", adet));

enforce() ve assert() işlevlerinin farklarını daha sonraki bir bölümde göreceğiz.

Hata atıldığında bütün kapsamlardan çıkılır

Programın, main işlevinden başlayarak başka işlevlere, onlardan da daha başka işlevlere dallandığını görmüştük. İşlevlerin birbirlerini katmanlar halinde çağırmaları, çağrılan işlevlerin kendilerini çağıran işlevlere dönmeleri, ardından başka işlevlerin çağrılmaları, vs. bir ağacın dalları halinde gösterilebilir.

Örneğin main'den çağrılan yumurtaYap adlı bir işlev, kendisi malzemeleriHazırla adlı başka bir işlevi çağırabilir, ve o işlev de yumurtaHazırla adlı başka bir işlevi çağırabilir. Okların işlev çağrıları anlamına geldiklerini kabul edersek, böyle bir programın dallanmasını şu şekilde gösterebiliriz:

main
  │
  ├──▶ yumurtaYap
  │      │
  │      ├──▶ malzemeleriHazırla
  │      │          │
  │      │          ├─▶ yumurtaHazırla
  │      │          ├─▶ yağHazırla
  │      │          └─▶ tavaHazırla
  │      │
  │      ├──▶ yumurtalarıPişir
  │      └──▶ malzemeleriKaldır
  │
  └──▶ yumurtaYe

Toplam 3 alt düzeye dallanan bu programı, dallanma düzeylerini değişik miktarlarda girintiyle gösterecek şekilde aşağıdaki gibi yazabiliriz. Tabii bu programda işlevler yararlı işler yapmıyorlar; burada amaç, yalnızca programın dallanmasını göstermek:

import std.stdio;

void girinti(int miktar) {
    foreach (i; 0 .. miktar * 2) {
        write(' ');
    }
}

void başlıyor(string işlev, int girintiMiktarı) {
    girinti(girintiMiktarı);
    writeln("▶ ", işlev, " ilk satır");
}

void bitiyor(string işlev, int girintiMiktarı) {
    girinti(girintiMiktarı);
    writeln("◁ ", işlev, " son satır");
}

void main() {
    başlıyor("main", 0);
    yumurtaYap();
    yumurtaYe();
    bitiyor("main", 0);
}

void yumurtaYap() {
    başlıyor("yumurtaYap", 1);
    malzemeleriHazırla();
    yumurtalarıPişir();
    malzemeleriKaldır();
    bitiyor("yumurtaYap", 1);
}

void yumurtaYe() {
    başlıyor("yumurtaYe", 1);
    bitiyor("yumurtaYe", 1);
}

void malzemeleriHazırla() {
    başlıyor("malzemeleriHazırla", 2);
    yumurtaHazırla();
    yağHazırla();
    tavaHazırla();
    bitiyor("malzemeleriHazırla", 2);
}

void yumurtalarıPişir() {
    başlıyor("yumurtalarıPişir", 2);
    bitiyor("yumurtalarıPişir", 2);
}

void malzemeleriKaldır() {
    başlıyor("malzemeleriKaldır", 2);
    bitiyor("malzemeleriKaldır", 2);
}

void yumurtaHazırla() {
    başlıyor("yumurtaHazırla", 3);
    bitiyor("yumurtaHazırla", 3);
}

void yağHazırla() {
    başlıyor("yağHazırla", 3);
    bitiyor("yağHazırla", 3);
}

void tavaHazırla() {
    başlıyor("tavaHazırla", 3);
    bitiyor("tavaHazırla", 3);
}

Normal işleyişi sırasında program şu çıktıyı üretir:

▶ main ilk satır
  ▶ yumurtaYap ilk satır
    ▶ malzemeleriHazırla ilk satır
      ▶ yumurtaHazırla ilk satır
      ◁ yumurtaHazırla son satır
      ▶ yağHazırla ilk satır
      ◁ yağHazırla son satır
      ▶ tavaHazırla ilk satır
      ◁ tavaHazırla son satır
    ◁ malzemeleriHazırla son satır
    ▶ yumurtalarıPişir ilk satır
    ◁ yumurtalarıPişir son satır
    ▶ malzemeleriKaldır ilk satır
    ◁ malzemeleriKaldır son satır
  ◁ yumurtaYap son satır
  ▶ yumurtaYe ilk satır
  ◁ yumurtaYe son satır
◁ main son satır

başlıyor ve bitiyor işlevleri sayesinde işareti ile işlevin ilk satırını, işareti ile de son satırını gösterdik. Program main'in ilk satırıyla başlıyor, başka işlevlere dallanıyor, ve en son main'in son satırıyla sonlanıyor.

Şimdi, programı yumurtaHazırla işlevine dolaptan kaç yumurta çıkartacağını parametre olarak belirtecek şekilde değiştirelim; ve bu işlev birden az bir değer geldiğinde hata atsın:

import std.string;

// ...

void yumurtaHazırla(int adet) {
    başlıyor("yumurtaHazırla", 3);

    if (adet < 1) {
        throw new Exception(
            format("Dolaptan %s yumurta çıkartılamaz", adet));
    }

    bitiyor("yumurtaHazırla", 3);
}

Programın doğru olarak derlenebilmesi için tabii başka işlevleri de değiştirmemiz gerekir. Dolaptan kaç yumurta çıkartılacağını işlevler arasında main'den başlayarak elden elden iletebiliriz. Bu durumda programın diğer tarafları da aşağıdaki gibi değiştirilebilir. Bu örnekte, main'den bilerek geçersiz olan -8 değerini gönderiyoruz; amaç, programın dallanmasını bir kere de hata atıldığında görmek:

// ...

void main() {
    başlıyor("main", 0);
    yumurtaYap(-8);
    yumurtaYe();
    bitiyor("main", 0);
}

void yumurtaYap(int adet) {
    başlıyor("yumurtaYap", 1);
    malzemeleriHazırla(adet);
    yumurtalarıPişir();
    malzemeleriKaldır();
    bitiyor("yumurtaYap", 1);
}

// ...

void malzemeleriHazırla(int adet) {
    başlıyor("malzemeleriHazırla", 2);
    yumurtaHazırla(adet);
    yağHazırla();
    tavaHazırla();
    bitiyor("malzemeleriHazırla", 2);
}

// ...

Programın bu halini çalıştırdığımızda, throw ile hata atıldığı yerden sonraki hiçbir satırın işletilmediğini görürüz:

▶ main ilk satır
  ▶ yumurtaYap ilk satır
    ▶ malzemeleriHazırla ilk satır
      ▶ yumurtaHazırla ilk satır
object.Exception: Dolaptan -8 yumurta çıkartılamaz

Hata oluştuğu an; en alt düzeyden en üst düzeye doğru, önce yumurtaHazırla işlevinden, sonra malzemeleriHazırla işlevinden, daha sonra yumurtaYap işlevinden, ve en sonunda da main işlevinden çıkılır. Bu çıkış sırasında, işlevlerin henüz işletilmemiş olan adımları işletilmez.

İşlemlere devam etmeden bütün işlevlerden çıkılmasının mantığı; en alt düzeydeki yumurtaHazırla işlevinin başarısızlıkla sonuçlanmış olmasının, onu çağıran daha üst düzeydeki işlevlerin de başarısız olacakları anlamına gelmesidir.

Alt düzey bir işlevden atılan hata, teker teker o işlevi çağıran üst düzey işlevlere geçer ve en sonunda main'den de çıkarak programın sonlanmasına neden olur. Hatanın izlediği yolu işaretli olarak aşağıdaki gibi gösterebiliriz:

     
     
     
main  ◀───────────┐
  ├──▶ yumurtaYap  ◀─────────────┐
  │      │                       
  │      │                       
  │      ├──▶ malzemeleriHazırla  ◀─────┐
  │      │          │                   
  │      │          │                   
  │      │          ├─▶ yumurtaHazırla  X atılan hata
  │      │          ├─▶ yağHazırla
  │      │          └─▶ tavaHazırla
  │      │
  │      ├──▶ yumurtalarıPişir
  │      └──▶ malzemeleriKaldır
  │
  └──▶ yumurtaYe

Hata atma düzeneğinin yararı, hatalı bir durumla karşılaşıldığında dallanılmış olan bütün işlevlerin derhal terkedilmelerini sağlamasıdır.

Bazı durumlarda, atılan hatanın yakalanması ve programın devam edebilmesi de mümkündür. Bunu sağlayan catch anahtar sözcüğünü biraz aşağıda göreceğiz.

throw'u ne zaman kullanmalı

throw'u gerçekten işe devam edilemeyecek durumlarda kullanın. Örneğin kayıtlı öğrenci adedini bir dosyadan okuyan bir işlev, bu değer sıfırdan küçük çıktığında hata atabilir. Çünkü örneğin eksi adet öğrenci ile işine devam etmesi olanaksızdır.

Öte yandan; eğer devam edilememesinin nedeni kullanıcının girdiği bir bilgiyse, kullanıcının girdiği bu bilgiyi denetlemek daha uygun olabilir. Kullanıcıya bir hata mesajı gösterilebilir ve bilgiyi geçerli olacak şekilde tekrar girmesi istenebilir. Kullanıcıyla etkileşilen böyle bir durum, programın atılan bir hata ile sonlanmasından daha uygun olabilir.

Hata yakalamak için try-catch deyimi

Yukarıda, atılan hatanın bütün işlevlerden ve en sonunda da programdan hemen çıkılmasına neden olduğunu anlattım. Aslında atılan bu hata yakalanabilir ve hatanın türüne veya duruma göre davranılarak programın sonlanması önlenebilir.

Hata, atıldığı işlevden üst düzey işlevlere doğru adım adım ilerlerken, onunla ilgilenen bir noktada try-catch deyimi ile yakalanabilir. "try"ın anlamı "dene", "catch"in anlamı da "yakala"dır. try-catch deyimini, bu anlamları göze alarak "çalıştırmayı dene, eğer hata atılırsa yakala" olarak anlatabiliriz. Söz dizimi şöyledir:

    try {
        // çalıştırılması istenen ve belki de
        // hata atacak olan kod bloğu

    } catch (ilgilenilen_bir_hata_türü_nesnesi) {
        // bu türden hata atıldığında
        // işletilecek olan işlemler

    } catch (ilgilenilen_diğer_bir_hata_türü_nesnesi) {
        // bu diğer türden hata atıldığında
        // işletilecek olan işlemler

    // ... seçime bağlı olarak başka catch blokları ...

    } finally {
        // hata atılsa da atılmasa da;
        // mutlaka işletilmesi gereken işlemler
    }

Bu bloğu anlamak için önce aşağıdaki try-catch kullanmayan programa bakalım. Bu program, zar değerini bir dosyadan okuyor ve çıkışa yazdırıyor:

import std.stdio;

int dosyadanZarOku() {
    auto dosya = File("zarin_yazili_oldugu_dosya", "r");

    int zar;
    dosya.readf(" %s", &zar);

    return zar;
}

void main() {
    const int zar = dosyadanZarOku();

    writeln("Zar: ", zar);
}

Dikkat ederseniz, dosyadanZarOku işlevi hiç hatalarla ilgilenmeden ve sanki dosya başarıyla açılacakmış ve içinden bir zar değeri okunacakmış gibi yazılmış. O, yalnızca kendi işini yapıyor. Bu, hata atma düzeneğinin başka bir yararıdır: işlevler her şey yolunda gidecekmiş gibi yazılabilirler.

Şimdi o programı klasörde zarin_yazili_oldugu_dosya isminde bir dosya bulunmadığı zaman başlatalım:

# ./deneme
std.exception.ErrnoException@std/stdio.d(286): Cannot open file
`zarin_yazili_oldugu_dosya' in mode `r' (No such file or directory)

Klasörde dosya bulunmadığı zaman, mesajı "'zarin_yazili_oldugu_dosya' 'r' modunda açılamıyor" olan bir ErrnoException atılmıştır. Yukarıda gördüğümüz diğer örneklere uygun olarak, program çıkışına "Zar: " yazdıramamış ve hemen sonlanmıştır.

Şimdi programa dosyadanZarOku işlevini bir try bloğu içinde çağıran bir işlev ekleyelim, ve main'den bu işlevi çağıralım:

import std.stdio;
import std.exception;

int dosyadanZarOku() {
    auto dosya = File("zarin_yazili_oldugu_dosya", "r");

    int zar;
    dosya.readf(" %s", &zar);

    return zar;
}

int dosyadanZarOkumayıDene() {
    int zar;

    try {
        zar = dosyadanZarOku();

    } catch (std.exception.ErrnoException hata) {
        writeln("(Dosyadan okuyamadım; 1 varsayıyorum)");
        zar = 1;
    }

    return zar;
}

void main() {
    const int zar = dosyadanZarOkumayıDene();

    writeln("Zar: ", zar);
}

Eğer programı yine aynı şekilde klasörde zarin_yazili_oldugu_dosya dosyası olmadan başlatırsak, bu sefer programın hata ile sonlanmadığını görürüz:

$ ./deneme
(Dosyadan okuyamadım; 1 varsayıyorum)
Zar: 1

Bu kodda, dosyadanZarOku işlevinin işleyişi bir try bloğu içinde denenmektedir. Eğer hatasız çalışırsa, işlev ondan sonra return zar; satırı ile normal olarak sonlanır. Ama eğer özellikle belirtilmiş olan std.exception.ErrnoException hatası atılırsa, işlevin işleyişi o catch bloğuna geçer ve o bloğun içindeki kodları çalıştırır. Bunu programın yukarıdaki çıktısında görüyoruz.

Özetle, klasörde zar dosyası bulunmadığı için

İşte catch, atılabilecek olan hataları yakalayarak o durumlara uygun olarak davranılmasını, ve programın işleyişine devam etmesini sağlar.

Başka bir örnek olarak, yumurtalı programa dönelim ve onun main işlevine bir try-catch deyimi ekleyelim:

void main() {
    başlıyor("main", 0);

    try {
        yumurtaYap(-8);
        yumurtaYe();

    } catch (Exception hata) {
        write("Yumurta yiyemedim: ");
        writeln('"', hata.msg, '"');
        writeln("Komşuda yiyeceğim...");
    }

    bitiyor("main", 0);
}

(Not: .msg niteliğini biraz aşağıda göreceğiz.)

Yukarıdaki try bloğunda iki satır kod bulunuyor. catch, bu satırların herhangi birisinden atılacak olan hatayı yakalar.

▶ main ilk satır
  ▶ yumurtaYap ilk satır
    ▶ malzemeleriHazırla ilk satır
      ▶ yumurtaHazırla ilk satır
Yumurta yiyemedim: "Dolaptan -8 yumurta çıkartılamaz"
Komşuda yiyeceğim...
◁ main son satır

Görüldüğü gibi, bu program bir hata atıldı diye artık hemen sonlanmamaktadır. Program; hataya karşı önlem almakta, işleyişine devam etmekte, ve main işlevi normal olarak sonuna kadar işletilmektedir.

catch blokları sırayla taranır

Örneklerde kendimiz hata atarken kullandığımız Exception, genel bir hata türüdür. Bu hatanın atılmış olması, programda bir hata olduğunu belirtir; ve hatanın içinde saklanmakta olan mesaj, o mesajı okuyan insanlara da hatayla ilgili bilgi verir. Ancak, Exception sınıfı hatanın türü konusunda bir bilgi taşımaz.

Bu bölümde daha önce gördüğümüz ConvException ve ErrnoException ise daha özel hata türleridir: birincisi, atılan hatanın bir dönüşüm ile ilgili olduğunu; ikincisi ise sistem işlemleriyle ilgili olduğunu anlatır.

Phobos'taki çoğu başka hata gibi ConvException ve ErrnoException, Exception sınıfından türemişlerdir. Atılan hata türleri, Error ve Exception genel hata türlerinin daha özel halleridir. Error ve Exception da kendilerinden daha genel olan Throwable sınıfından türemişlerdir. ("Throwable"ın anlamı "atılabilen"dir.)

Her ne kadar catch ile yakalanabiliyor olsa da, Error türünden veya ondan türemiş olan hataların yakalanmaları önerilmez. Error'dan daha genel olduğu için Throwable'ın yakalanması da önerilmez. Yakalanmasının doğru olduğu sıradüzen, Exception sıradüzenidir.

           Throwable (yakalamayın)
             ↗   ↖
    Exception     Error (yakalamayın)
     ↗    ↖        ↗    ↖
   ...    ...    ...    ...

Not: Sıradüzen gösterimini daha sonraki Türeme bölümünde göstereceğim. Yukarıdaki şekil, Throwable'ın en genel, Exception ve Error'ın daha özel türler olduklarını ifade eder.

Atılan hataları özellikle belirli bir türden olacak şekilde yakalayabiliriz. Örneğin ErrnoException türünü yakalayarak dosya açma sorunu ile karşılaşıldığını anlayabilir ve programda buna göre hareket edebiliriz.

Atılan hata, ancak eğer catch bloğunda belirtilen türe uyuyorsa yakalanır. Örneğin ÖzelBirHata türünü yakalamaya çalışan bir catch bloğu, ErrnoException hatasını yakalamaz.

Bir try deyimi içerisindeki kodların (veya onların çağırdığı başka kodların) attığı hata, o try deyiminin catch bloklarında belirtilen hata türlerine sırayla uydurulmaya çalışılır. Eğer atılan hatanın türü sırayla bakılan catch bloğunun hata türüne uyuyorsa, o hata yakalanmış olur ve o catch bloğunun içerisindeki kodlar işletilir. Uyan bir catch bloğu bulunursa, artık diğer catch bloklarına bakılmaz.

catch bloklarının böyle sırayla taranmalarının doğru olarak çalışması için catch bloklarının daha özel hata türlerinden daha genel hata türlerine doğru sıralanmış olmaları gerekir. Buna göre; genel bir kural olarak eğer yakalanması uygun bulunuyorsa, yakalanması önerilen en genel hata türü olduğu için Exception her zaman en sondaki catch bloğunda belirtilmelidir.

Örneğin öğrenci kayıtlarıyla ilgili hataları yakalamaya çalışan bir try deyimi, catch bloklarındaki hata türlerini özelden genele doğru şu şekilde yazabilir:

    try {
        // ... hata atabilecek kayıt işlemleri ...

    } catch (KayıtNumarasıHanesiHatası hata) {

        // özellikle kayıt numarasının bir hanesiyle ilgili
        // olan bir hata

    } catch (KayıtNumarasıHatası hata) {

        // kayıt numarasıyla ilgili olan, ama hanesi ile
        // ilgili olmayan daha genel bir hata

    } catch (KayıtHatası hata) {

        // kayıtla ilgili daha genel bir hata

    } catch (Exception hata) {

        // kayıtla ilgisi olmayan genel bir hata

    }
finally bloğu

try-catch deyiminin son bloğu olan finally, hata atılsa da atılmasa da mutlaka işletilecek olan işlemleri içerir. finally bloğu isteğe bağlıdır; gerekmiyorsa yazılmayabilir.

finally'nin etkisini görmek için %50 olasılıkla hata atan şu programa bakalım:

import std.stdio;
import std.random;

void yüzdeElliHataAtanİşlev() {
    if (uniform(0, 2) == 1) {
        throw new Exception("hata mesajı");
    }
}

void deneme() {
    writeln("ilk satır");

    try {
        writeln("try'ın ilk satırı");
        yüzdeElliHataAtanİşlev();
        writeln("try'ın son satırı");

    // ... isteğe bağlı olarak catch blokları da olabilir ...

    } finally {
        writeln("finally işlemleri");
    }

    writeln("son satır");
}

void main() {
    deneme();
}

O işlev hata atmadığında programın çıktısı şöyledir:

ilk satır
try'ın ilk satırı
try'ın son satırı
finally işlemleri
son satır

Hata attığında ise şöyle:

ilk satır
try'ın ilk satırı
finally işlemleri
object.Exception@deneme.d(7): hata mesajı

Görüldüğü gibi, hata atıldığında "try'ın son satırı" ve "son satır" yazdırılmamış, ama finally bloğunun içi iki durumda da işletilmiştir.

try-catch'i ne zaman kullanmalı

try-catch deyimi, atılmış olan hataları yakalamak ve bu durumlarda özel işlemler yapmak için kullanılır.

Dolayısıyla, try-catch deyimini ancak ve ancak atılan bir hata ile ilgili özel işlemler yapmanız gereken veya yapabildiğiniz durumlarda kullanın. Başka durumlarda hatalara karışmayın. Hataları, onları yakalamaya çalışan işlevlere bırakın.

Hata nitelikleri

Program hata ile sonlandığında çıktıya otomatik olarak yazdırılan bilgiler yakalanan hata nesnelerinin niteliklerinden de edinilebilir. Bu nitelikler Throwable arayüzü tarafından sunulur:

finally bloğunun hata atılan durumda otomatik olarak işletildiğini gördük. (Bir sonraki bölümde göreceğimiz scope deyimi ve daha ilerideki bir bölümde göreceğimiz sonlandırıcı işlevler de kapsamlardan çıkılırken otomatik olarak işletilirler.)

Doğal olarak, kapsamlardan çıkılırken işletilen kodlar da hata atabilirler. İkincil olarak adlandırılan bu hatalar birbirlerine bir bağlı liste olarak bağlanmışlardır; her birisine asıl hatadan başlayarak .next niteliği ile erişilir. Sonuncu hatanın .next niteliğinin değeri null'dır. (null değerini ilerideki bir bölümde göreceğiz.)

Aşağıdaki örnekte toplam üç adet hata atılmaktadır: foo() içinde atılan asıl hata ve foo()'nun ve onu çağıran bar()'ın finally bloklarında atılan ikincil hatalar. Program, ikincil hatalara .next nitelikleri ile nasıl erişildiğini gösteriyor.

Bu programdaki bazı kavramları daha sonraki bölümlerde göreceğiz. Örneğin, for döngüsünün yalnızca hata ifadesinden oluşan devam koşulu hata null olmadığı sürece anlamına gelir.

import std.stdio;

void foo() {
    try {
        throw new Exception("foo'daki asıl hata");

    } finally {
        throw new Exception("foo'daki finally hatası");
    }
}

void bar() {
    try {
        foo();

    } finally {
        throw new Exception("bar'daki finally hatası");
    }
}

void main() {
    try {
        bar();

    } catch (Exception yakalananHata) {

        for (Throwable hata = yakalananHata;
             hata;    // ← Anlamı: null olmadığı sürece
             hata = hata.next) {

            writefln("mesaj: %s", hata.msg);
            writefln("dosya: %s", hata.file);
            writefln("satır: %s", hata.line);
            writeln();
        }
    }
}

Çıktısı:

mesaj: foo'daki asıl hata
dosya: deneme.d
satır: 6

mesaj: foo'daki finally hatası
dosya: deneme.d
satır: 9

mesaj: bar'daki finally hatası
dosya: deneme.d
satır: 19
Hata çeşitleri

Hata atma düzeneğinin ne kadar yararlı olduğunu gördük. Hem alt düzeydeki işlemlerin, hem de o işleme bağımlı olan daha üst düzey işlemlerin hemen sonlanmalarına neden olur. Böylece program yanlış bilgiyle veya eksik işlemle devam etmemiş olur.

Buna bakarak her hatalı durumda hata atılmasının uygun olduğunu düşünmeyin. Hatanın çeşidine bağlı olarak farklı davranmak gerekebilir.

Kullanıcı hataları

Hataların bazıları kullanıcıdan gelir. Yukarıda da gördüğümüz gibi, örneğin bir sayı beklenen durumda "merhaba" gibi bir dizgi girilmiş olabilir. Programın kullanıcıyla etkileştiği bir durumda programın hata ile sonlanması uygun olmayacağı için, böyle durumlarda kullanıcıya bir hata mesajı göstermek ve doğru bilgi girmesini istemek daha uygun olabilir.

Yine de, kullanıcının girdiği bilginin doğrudan işlenmesinde ve o işlemler sırasında bir hata atılmasında da bir sakınca olmayabilir. Önemli olan, bu tür bir hatanın programın sonlanmasına neden olmak yerine, kullanıcıya geçerli bilgi girmesini söylemesidir.

Bir örnek olarak, kullanıcıdan dosya ismi alan bir programa bakalım. Aldığımız dosya isminin geçerli olup olmadığı konusunda iki yol izleyebiliriz:

Programcı hataları

Bazı hatalar programcının kendisinden kaynaklanır. Örneğin yazılan bir işlevin programda kesinlikle sıfırdan küçük bir değerle çağrılmayacağından eminizdir. Programın tasarımına göre bu işlev kesinlikle eksi bir değerle çağrılmıyordur. İşlevin buna rağmen eksi bir değer alması; ya programın mantığındaki bir hatadan kaynaklanıyordur, ya da o mantığın gerçekleştirilmesindeki bir hatadan. Bunların ikisi de programcı hatası olarak kabul edilir.

Böyle, programın yazımıyla ilgili olan, yani programcının kendisinden kaynaklanan hatalı durumlarda hata atmak yerine bir assert kullanmak daha uygun olabilir (Not: assert'ü daha sonraki bir bölümde göreceğiz.):

void menüSeçeneği(int sıraNumarası) {
    assert(sıraNumarası >= 0);
    // ...
}

void main() {
    menüSeçeneği(-1);
}

Program bir assert hatası ile sonlanır:

core.exception.AssertError@deneme.d(2): Assertion failure

assert hangi kaynak dosyanın hangi satırındaki beklentinin gerçekleşmediğini de bildirir. (Bu mesajda deneme.d dosyasının ikinci satırı olduğu anlaşılıyor.)

Beklenmeyen durumlar

Yukarıdaki iki durumun dışında kalan her türlü hatalı durumda hata atmak uygundur. Zaten başka çare kalmamıştır: ne bir kullanıcı hatasıyla ne de bir programcı hatasıyla karşı karşıyayızdır. Eğer işimize devam edemiyorsak, hata atmaktan başka çare yoktur.

Bizim attığımız hatalar karşısında ne yapacakları bizi çağıran üst düzey işlevlerin görevidir. Eğer uygunsa, attığımız hatayı yakalayarak bir çare bulabilirler.

Özet