Hata Atma ve Yakalama
Beklenmedik durumlar, programların yaşamlarının doğal parçalarıdır. Kullanıcı hataları, programcı hataları, ortamdaki beklenmedik durumlar, 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 dersin problem çözümlerinde de olduğu gibi:
switch (işlem) { case "+": dout.writefln(birinci + ikinci); break; case "-": dout.writefln(birinci - ikinci); break; case "x": dout.writefln(birinci * ikinci); break; case "/": dout.writefln(birinci / ikinci); break; default: throw new Exception("Geçersiz işlem: " ~ 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.ConvError: 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.ConvError 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 error"dan türemiş olan ConvError'dır.
Hata atmak için throw
Bunun örneklerini hem yukarıdaki switch deyiminde, hem de daha önceki derslerde 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 işine devam edebilir.
Exception ve Error hata türleri
throw deyimi ile yalnızca sınıf türünden olan nesneler atılabilir. Phobos'taki hatalar ya object.Exception sınıfından, ya da object.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: Türetmeyi daha sonra sınıflar dersinde göreceğiz.)
Exception nesneleri, kurulurlarken hata mesajını string olarak alırlar. Bu mesajı, dizi birleştirme işleci olan ~ ile hata nesnesini attığınız noktada oluşturmak kolaylık sağlar:
import std.cstream; import std.conv; import std.random; int[] rastgeleZarlarAt(int adet) { if (adet < 0) { throw new Exception( "Geçersiz 'adet' değeri: " ~ to!string(adet)); } int[] sayılar; foreach (i; 0 .. adet) { sayılar ~= uniform(1, 7); } return sayılar; } void main() { dout.writefln(rastgeleZarlarAt(-5)); }
# ./deneme
object.Exception: Geçersiz 'adet' değeri: -5
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.cstream; import std.conv; void girinti(in int miktar) { foreach (i; 0 .. miktar * 2) { dout.writef(' '); } } void başlıyor(in char[] işlev, in int girintiMiktarı) { girinti(girintiMiktarı); dout.writefln("▶ ", işlev, " ilk satır"); } void bitiyor(in char[] işlev, in int girintiMiktarı) { girinti(girintiMiktarı); dout.writefln("◁ ", 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 eksi bir değer geldiğinde hata atsın:
void yumurtaHazırla(int adet) { başlıyor("yumurtaHazırla", 3); if (adet < 0) { throw new Exception("Dolaptan " ~ to!string(adet) ~ " yumurta çıkartılamaz"); } 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 kırmızı ile şu şekilde gösterebiliriz:
▲ | main ◀---+ | | +--▶ yumurtaYap ◀-+ | | | | +--▶ malzemeleriHazırla ◀-------+ | | | |hata | | +-▶ yumurtaHazırla X-+ | | +-▶ 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 edebilecek şekilde düzeltilmesi de mümkündür. Bunu sağlayan catch anahtar sözcüğünü biraz aşağıda gösteriyorum.
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 kullanmayan programa bakalım. Bu program, zar değerini bir dosyadan okuyor ve çıkışa yazdırıyor:
import std.cstream; import std.stream; import std.conv; int dosyadanZarOku() { auto dosya = new File("zarin_yazili_oldugu_dosya", FileMode.In); int zar; dosya.readf(&zar); return zar; } void main() { const int zar = dosyadanZarOku(); dout.writefln("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.stream.OpenException: Cannot open or create file
'zarin_yazili_oldugu_dosya'
Klasörde dosya bulunmadığı zaman, mesajı "'zarin_yazili_oldugu_dosya' açılamıyor veya oluşturulamıyor" olan bir OpenException (açma hatası) 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.cstream; import std.stream; import std.conv; int dosyadanZarOku() { auto dosya = new File("zarin_yazili_oldugu_dosya", FileMode.In); int zar; dosya.readf(&zar); return zar; } int dosyadanZarOkumayıDene() { int zar; try { zar = dosyadanZarOku(); } catch (OpenException hata) { dout.writefln( "(Dosyadan okuyamadım; 1 varsayıyorum)"); zar = 1; } return zar; } void main() { const int zar = dosyadanZarOkumayıDene(); dout.writefln("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 OpenException 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
OpenExceptionhatası atılmakta, (bunu bizim kodumuz değil,Fileatıyor) - bu hata
catchile yakalanmakta, - 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) { dout.writef("Yumurta yiyemedim: "); dout.writefln('"', hata.msg, '"'); dout.writefln("Komşuda yiyeceğim..."); } bitiyor("main", 0); }
▶ 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 derste daha önce gördüğümüz ConvError ve OpenException ise daha özel hata türleridir: birincisi, atılan hatanın bir dönüşüm ile ilgili olduğunu; ikincisi ise dosya açma ile ilgili olduğunu anlatır.
Phobos'taki çoğu başka hata gibi; ConvError, Error sınıfından; OpenException ise Exception sınıfından türemiştir. Bu türler, 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. Aynı nedenden, 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 dersinde 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 OpenException 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, OpenException 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. catch gibi, finally bloğu da 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.cstream; import std.random; void yüzdeElliHataAtanİşlev() { if (uniform(0, 2) == 1) { throw new Exception("hata mesajı"); } } void deneme() { dout.writefln("ilk satır"); try { dout.writefln("try'ın ilk satırı"); yüzdeElliHataAtanİşlev(); dout.writefln("try'ın son satırı"); // ... isteğe bağlı olarak catch blokları da olabilir ... } finally { dout.writefln("finally işlemleri"); } dout.writefln("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: 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.
scope(exit), scope(success), ve scope(failure)
Kesinlikle işletilmeleri gereken ifadelerin finally bloklarına, hatalı durumlarda işletilmeleri gereken ifadelerin de catch bloklarına yazıldıklarını gördük. Bu blokların kullanımlarıyla ilgili bir kaç gözlemde bulunabiliriz:
catchvefinallyblokları,trybloğu olmadan kullanılamaz- bu bloklarda kullanılmak istenen bazı değişkenler o noktalarda geçerli olmayabilir:
void birİşlev(ref int çıkış) { try { int birDeğer = 42; çıkış += birDeğer; hataAtabilecekBirİşlev(); } catch (Exception hata) { çıkış -= birDeğer; // ← derleme HATASI } }
Yukarıdaki işlev, referans türündeki parametresinde değişiklik yapmakta, ve hata atılması durumunda onu eski haline getirmeye çalışmaktadır. Ne yazık ki, birDeğer yalnızca try bloğu içinde tanımlı olduğu için bir derleme hatası alınır. (Not: Yaşam süreçleriyle ilgili olan bu konuyu ilerideki bir derste anlatacağım.)
finally bloğunda bulunmaları, ilgili oldukları kodlardan uzakta yazıldıkları istenmeyebilircatch ve finally bloklarına benzer şekilde işleyen ve bazı durumlarda daha uygun olan bir olanak, scope deyimleridir. Üç farklı scope deyimi, yine ifadelerin kapsamlardan çıkılırken kesinlikle işletilmeleri ile ilgilidir:
scope(exit): ifade, kapsamdan ne şekilde çıkılırsa çıkılsın işletilirscope(success): ifade, kapsamdan başarıyla çıkılırken işletilirscope(failure): ifade, kapsamdan hata ile çıkılırken işletilir
Bu deyimler yine atılan hatalarla ilgili olsalar da, try-catch bloğu olmadan kullanılabilirler.
Örneğin, hata atıldığında çıkış'ın değerini düzeltmeye çalışan yukarıdaki işlevi bir scope(failure) deyimiyle ve daha kısa olarak şöyle yazabiliriz:
void birİşlev(ref int çıkış) { int birDeğer = 42; çıkış += birDeğer; scope(failure) çıkış -= birDeğer; hataAtabilecekBirİşlev(); }
Yukarıdaki scope deyimi, kendisinden sonra yazılan ifadenin işlevden hata ile çıkıldığı durumda işletileceğini bildirir. Bunun bir yararı, yapılan bir değişikliğin hatalı durumda geri çevrilecek olduğunun tam da değişikliğin yapıldığı yerde görülebilmesidir.
scope deyimleri bloklar halinde de bildirilebilirler:
scope(exit) { // ... ifadeler ... }
Bu kavramları deneyen bir işlevi şöyle yazabiliriz:
void deneme() { scope(exit) writeln("çıkarken 1"); scope(success) { writeln("başarılıysa 1"); writeln("başarılıysa 2"); } scope(failure) writeln("hata atılırsa 1"); scope(exit) writeln("çıkarken 2"); scope(failure) writeln("hata atılırsa 2"); yüzdeElliHataAtanİşlev(); }
İşlevin çıktısı, hata atılmayan durumda scope(exit) ve scope(success) ifadelerini içerir:
çıkarken 2 başarılıysa 1 başarılıysa 2 çıkarken 1
Hata atılan durumda ise scope(exit) ve scope(failure) ifadelerini içerir:
hata atılırsa 2 çıkarken 2 hata atılırsa 1 çıkarken 1 object.Exception: hata mesajı
Çıktılardan anlaşıldığı gibi, scope deyimlerinin ifadeleri ters sırada işletilmektedir. Bunun nedeni, daha sonra gelen kodların daha önceki değişkenlerin durumlarına bağlı olmalarıdır. scope deyimlerindeki ifadelerinin ters sırada işletilmeleri, programın durumunda yapılan değişikliklerin geri adımlar atılarak ters sırada yapılmalarını sağlar.
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.filemodülündekiexistsiş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.
File bir hata atacaktır:
import std.cstream; import std.stream; void dosyayıKullan(in char[] dosyaİsmi) { auto dosya = new File(dosyaİsmi.idup, FileMode.In); // ... } char[] dizgiOku(in char[] soru) { char[] dizgi; dout.writef(soru, ": "); din.readf(&dizgi); 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; dout.writefln("Dosya başarıyla kullanıldı"); } catch (OpenException açmaHatası) { derr.writefln("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'ü bir sonraki derste anlatacağım.):
void menüSeçeneği(int sıraNumarası) { assert(sıraNumarası >= 0); } void main() { menüSeçeneği(-1); }
core.exception.AssertError@deneme.d(3): Assertion failure
assert, programcıya hangi kaynak dosyanın hangi satırındaki beklentinin gerçekleşmediği bilgisini verir. (Bu mesajda deneme.d dosyasının üçüncü 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 - Bunların dışındaki durumlarda
throwile hata atın - Hataları ancak ve ancak, yakaladığınızda yararlı bir işlem yapabilecekseniz yakalayın. Yoksa hiç
try-catchdeyimi içine almayın; belki de işlevinizi çağıran daha üst düzeydeki bir işlev yakalayacaktır catchbloklarını özelden genele doğru sıralayın- İşletilmeleri mutlaka gereken işlemleri
finallybloğuna yazın trybloğu kullanmanın gerekmediği durumlarda, işletilmeleri kesinlikle gereken işlemleriscope(exit),scope(success), vescope(failure)deyimleri ile belirtin
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları