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
- önceki programdaki gibi bir
std.exception.ErrnoException
hatası atılmakta, (bunu bizim kodumuz değil,File
atıyor) - bu hata
catch
ile yakalanmakta, catch
bloğunun normal işleyişi sırasında zar için 1 değeri varsayılmakta,- ve programın işleyişine devam edilmektedir.
İş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:
-
.file
: Hatanın atıldığı kaynak dosya -
.line
: Hatanın atıldığı satır -
.msg
: Hata mesajı -
.info
: Çağrı yığıtının hata atıldığındaki durumu -
.next
: Bir sonraki ikincil hata
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:
- Bilgiyi denetlemek:
std.file
modülündekiexists
işlevini kullanarak verilen isimde bir dosya olup olmadığına bakabiliriz:if (exists(dosya_ismi)) { // dosya mevcut } else { // dosya mevcut değil }
Dosyayı ancak dosya mevcut olduğunda açarız. Ancak; dosya, program bu denetimi yaptığı anda mevcut olduğu halde, az sonra
File
ile açılmaya çalışıldığında mevcut olmayabilir. Çünkü örneğin sistemde çalışmakta olan başka bir program tarafından silinmiş veya ismi değiştirilmiş olabilir.Bu yüzden, belki de aşağıdaki diğer yöntem daha uygundur.
- Bilgiyi doğrudan kullanmak: Kullanıcıdan alınan bilgiye güvenebilir ve doğrudan işlemlere geçebiliriz. Eğer verilen bilgi geçersizse, zaten
File
bir hata atacaktır:import std.stdio; import std.exception; import std.string; void dosyayıKullan(string dosyaİsmi) { auto dosya = File(dosyaİsmi, "r"); // ... } string dizgiOku(string soru) { write(soru, ": "); string dizgi = strip(readln()); return dizgi; } void main() { bool dosyaKullanılabildi = false; while (!dosyaKullanılabildi) { try { dosyayıKullan( dizgiOku("Dosyanın ismini giriniz")); /* Eğer bu noktaya gelebildiysek, dosyayıKullan * işlevi başarıyla sonlanmış demektir. Yani, * verilen dosya ismi geçerlidir. * * Bu yüzden bu noktada bu değişkenin değerini * 'true' yaparak while'ın sonlanmasını * sağlıyoruz. */ dosyaKullanılabildi = true; writeln("Dosya başarıyla kullanıldı"); } catch (std.exception.ErrnoException açmaHatası) { stderr.writeln("Bu dosya açılamadı"); } } }
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
- Eğer bir kullanıcı hatasıyla karşılaşmışsanız ya kullanıcıyı uyarın ya da yine de işlemlere devam ederek nasıl olsa bir hata atılacağına güvenin.
- Programın mantığında veya gerçekleştirilmesinde hata olmadığını garantilemek için
assert
'ü kullanın. (Not:assert
'ü ilerideki bir bölümde göreceğiz.) - Bunların dışındaki durumlarda
throw
veyaenforce()
ile hata atın. (Not:enforce()
'u ilerideki bir bölümde göreceğiz.) - Hataları ancak ve ancak yakaladığınızda yararlı bir işlem yapabilecekseniz yakalayın. Yoksa hiç
try-catch
deyimi içine almayın; belki de işlevinizi çağıran daha üst düzeydeki bir işlev yakalayacaktır. catch
bloklarını özelden genele doğru sıralayın.- İşletilmeleri mutlaka gereken işlemleri
finally
bloğuna yazın.