assert
ve enforce
Programları yazarken çok sayıda varsayımda bulunuruz ve bazı beklentilerin doğru çıkmalarını umarız. Programlar ancak bu varsayımlar ve beklentiler doğru çıktıklarında doğru çalışırlar. assert
, programın dayandığı bu varsayımları ve beklentileri denetlemek için kullanılır. Programcının en etkili yardımcılarındandır.
Bazen hata atmakla assert
'ten yararlanmak arasında karar vermek güçtür. Aşağıdaki örneklerde fazla açıklamaya girmeden assert
'ler kullanacağım. Hangi durumda hangi yöntemin daha uygun olduğunu ise daha aşağıda açıklayacağım.
Çoğu zaman programdaki varsayımların farkına varılmaz. Örneğin iki kişinin yaşlarının ortalamasını alan aşağıdaki işlevde kullanılan hesap, yaş parametrelerinin ikisinin de sıfır veya daha büyük olacakları varsayılarak yazılmıştır:
double ortalamaYaş(double birinciYaş, double ikinciYaş) { return (birinciYaş + ikinciYaş) / 2; }
Yaşlardan en az birisinin eksi bir değer olarak gelmesi hatalı bir durumdur. Buna rağmen, işlev mantıklı bir ortalama üretebilir ve program bu hata hiç farkedilmeden işine yanlış da olsa devam edebilir.
Başka bir örnek olarak, aşağıdaki işlev yalnızca iki komuttan birisi ile çağrılacağını varsaymaktadır: "şarkı söyle" ve "dans et":
void komutİşlet(string komut) { if (komut == "şarkı söyle") { robotaŞarkıSöylet(); } else { robotuDansEttir(); } }
Böyle bir varsayımda bulunduğu için, "şarkı söyle" dışındaki geçerli olsun olmasın her komuta karşılık robotuDansEttir
işlevini çağıracaktır.
Bu varsayımları kendimize sakladığımızda sonuçta ortaya çıkan program hatalı davranabilir. assert
, bu varsayımlarımızı dile getirmemizi sağlayan ve varsayımlar hatalı çıktığında işlemlerin durdurulmalarına neden olan bir olanaktır.
assert
, bir anlamda programa "böyle olduğunu varsayıyorum, eğer yanlışsa işlemi durdur" dememizi sağlar.
Söz dizimi
assert
iki biçimde yazılabilir:
assert(mantıksal_ifade); assert(mantıksal_ifade, mesaj);
assert
, kendisine verilen mantıksal ifadeyi işletir. İfadenin değeri true
ise varsayım doğru çıkmış kabul edilir ve assert
denetiminin hiçbir etkisi yoktur. İfadenin değeri false
olduğunda ise varsayım yanlış çıkmış kabul edilir ve bir AssertError
hatası atılır. İsminden de anlaşılabileceği gibi, bu hata Error
'dan türemiştir ve Hatalar bölümünde gördüğümüz gibi, yakalanmaması gereken bir hata türüdür. Böyle bir hata atıldığında programın hemen sonlanması önemlidir çünkü programın yanlış varsayımlara dayanarak yanlış olabilecek sonuçlar üretmesi böylece önlenmiş olur.
Yukarıdaki ortalamaYaş
işlevindeki varsayımlarımızı iki assert
ile şöyle ifade edebiliriz:
double ortalamaYaş(double birinciYaş, double ikinciYaş) { assert(birinciYaş >= 0); assert(ikinciYaş >= 0); return (birinciYaş + ikinciYaş) / 2; } void main() { auto sonuç = ortalamaYaş(-1, 10); }
O assert
'ler "birinciYaş'ın 0 veya daha büyük olduğunu varsayıyorum" ve "ikinciYaş'ın 0 veya daha büyük olduğunu varsayıyorum" anlamına gelir. Başka bir bakış açısıyla, "assert" sözcüğünün "emin olarak öne sürmek" karşılığını kullanarak, "birinciYaş'ın 0 veya daha büyük olduğundan eminim" gibi de düşünülebilir.
assert
bu varsayımları denetler ve yukarıdaki programda olduğu gibi, varsayımın yanlış çıktığı durumda programı bir AssertError
hatasıyla sonlandırır:
core.exception.AssertError@deneme(2): Assertion failure
Hatanın @
karakterinden sonra gelen bölümü hangi dosyanın hangi satırındaki varsayımın doğru çıkmadığını gösterir. Bu örnekteki deneme(2)
'ye bakarak hatanın deneme.d
dosyasının ikinci satırında olduğu anlaşılır.
assert
beklentisinin yanlış çıktığı durumda açıklayıcı bir mesaj yazdırılmak istendiğinde assert
denetiminin ikinci kullanımından yararlanılır:
assert(birinciYaş >= 0, "Yaş sıfırdan küçük olamaz");
Çıktısı:
core.exception.AssertError@deneme.d(2): Yaş sıfırdan küçük olamaz
Programda kesinlikle gelinmeyeceği düşünülen veya gelinmemesi gereken noktalarda, özellikle başarısız olsun diye mantıksal ifade olarak bilerek false
sabit değeri kullanılır. Örneğin yukarıdaki "şarkı söyle" ve "dans et" örneğinde başka komutların geçersiz olduklarını belirtmek ve bu durumlarda hata atılmasını sağlamak için şöyle bir assert
denetimi kullanılabilir:
void komutİşlet(string komut) { if (komut == "şarkı söyle") { robotaŞarkıSöylet(); } else if (komut == "dans et") { robotuDansEttir(); } else { assert(false); } }
Artık işlev yalnızca o iki komutu kabul eder ve başka komut geldiğinde assert(false)
nedeniyle işlem durdurulur. (Not: Burada aynı amaç için bir final switch
deyimi de kullanılabilir.)
static assert
assert
denetimleri programın çalışması sırasında işletilirler çünkü programın doğru işleyişi ile ilgilidirler. Bazı denetimler ise daha çok programın yapısı ile ilgilidirler ve derleme zamanında bile işletilebilirler.
static assert
, derleme zamanında işletilebilecek olan denetimler içindir. Bunun bir yararı, belirli koşulların sağlanamaması durumunda programın derlenmesinin önlenebilmesidir. Doğal olarak, bütün ifadenin derleme zamanında işletilebiliyor olması şarttır.
Örneğin, çıkış aygıtının genişliği gibi bir kısıtlama nedeniyle menü başlığının belirli bir uzunluktan kısa olması gereken bir durumda static assert
'ten yararlanılabilir:
enum dstring menüBaşlığı = "Komut Menüsü"; static assert(menüBaşlığı.length <= 16);
İfadenin derleme zamanında işletilebilmesi için dizginin enum
olarak tanımlandığına dikkat edin. Yalnızca dstring
olsaydı bir derleme hatası oluşurdu.
Bir programcının o başlığı daha açıklayıcı olduğunu düşündüğü için değiştirdiğini düşünelim:
enum dstring menüBaşlığı = "Yön Komutları Menüsü"; static assert(menüBaşlığı.length <= 16);
Program artık static assert
denetimini geçemediği için derlenemez:
Error: static assert (20LU <= 16LU) is false
Programcı da böylece programın uyması gereken bu kısıtlamayı farketmiş olur.
static assert
'ün yararı, yukarıda olduğu gibi türlerin ve değerlerin açıkça belli oldukları örneklerde anlaşılamıyor. static assert
özellikle şablon ve koşullu derleme olanakları ile kullanıldığında yararlıdır. Bu olanakları ilerideki bölümlerde göreceğiz.
Kesinlikle doğru olan (!) varsayımlar için bile assert
"Kesinlikle doğru olan"ın özellikle üzerine basıyorum. Hiçbir varsayım bilerek yanlış olmayacağı için, zaten çoğu hata kesinlikle doğru olan varsayımlara dayanır.
Bu yüzden bazen kesinlikle gereksizmiş gibi duran assert
denetimleri de kullanılır. Örneğin belirli bir senenin aylarının kaç gün çektikleri bilgisini bir dizi olarak döndüren bir işlev ele alalım:
int[] ayGünleri(int yıl) { int[] günler = [ 31, şubatGünleri(yıl), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]; assert((diziToplamı(günler) == 365) || (diziToplamı(günler) == 366)); return günler; }
Doğal olarak bu işlevin döndürdüğü dizideki gün toplamları ya 365 olacaktır, ya da 366. Bu yüzden yukarıdaki assert
denetiminin gereksiz olduğu düşünülebilir. Oysa, her ne kadar gereksiz gibi görünse de, o denetim şubatGünleri
işlevinde ilerideki bir zamanda yapılabilecek bir hataya karşı bir güvence sağlar. şubatGünleri
işlevi bir hata nedeniyle örneğin 30 değerini döndürse, o assert
sayesinde bu hata hemen farkedilecektir.
Hatta biraz daha ileri giderek dizinin uzunluğunun her zaman için 12 olacağını da denetleyebiliriz:
assert(günler.length == 12);
Böylece kodu diziden yanlışlıkla silinebilecek veya diziye yanlışlıkla eklenebilecek bir elemana karşı da güvence altına almış oluruz.
Böyle denetimler her ne kadar gereksizmiş gibi görünseler de son derece yararlıdırlar. Kodun sağlamlığını arttıran ve kodu ilerideki değişiklikler karşısında güvencede tutan çok etkili yapılardır.
Kodun sağlamlığını arttıran ve programın yanlış sonuçlar doğuracak işlemlerle devam etmesini önleyen bir olanak olduğu için, assert
bundan sonraki bölümlerde göreceğimiz birim testleri ve sözleşmeli programlama olanaklarının da temelini oluşturur.
Değer üretmez ve yan etkisi yoktur
İfadelerin değer üretebildiklerini ve yan etkilerinin olabildiğini görmüştük. assert
değer üretmeyen bir denetimdir.
Ek olarak, assert
denetiminin kendisinin bir yan etkisi de yoktur. Ona verilen mantıksal ifadenin yan etkisinin olmaması da D standardı tarafından şart koşulmuştur. assert
, programın durumunu değiştirmeyen ve yalnızca varsayımları denetleyen bir yapı olarak kalmak zorundadır.
assert
denetimlerini etkisizleştirmek
assert
programın doğruluğu ile ilgilidir. Programın yeterince denenip amacı doğrultusunda doğru olarak işlediğine karar verildikten sonra programda başkaca yararı yoktur. Üstelik, ne değerleri ne de yan etkileri olduğundan, assert
denetimleri programdan bütünüyle kaldırılabilmelidirler ve bu durumda programın işleyişinde hiçbir değişiklik olmamalıdır.
Derleyici seçeneği -release
, assert
denetimlerinin sanki programa hiç yazılmamışlar gibi gözardı edilmelerini sağlar:
dmd deneme.d -release
Böylece olasılıkla uzun süren denetimlerin programı yavaşlatmaları önlenmiş olur.
Bir istisna olarak, false
veya ona otomatik olarak dönüşen bir hazır değerle çağrılan assert
'ler ‑release
ile derlendiklerinde bile programdan çıkartılmazlar. Bunun nedeni, assert(false)
denetimlerinin hiçbir zaman gelinmemesi gereken satırları belirliyor olmaları ve o satırlara gelinmesinin her zaman için hatalı olacağıdır.
Hata atmak için enforce
Programın çalışması sırasında karşılaşılan her beklenmedik durum programdaki bir yanlışlığı göstermez. Beklenmedik durumlar programın elindeki verilerle veya çevresiyle de ilgili olabilir. Örneğin, kullanıcının girmiş olduğu geçersiz bir değerin assert
ile denetlenmesi doğru olmaz çünkü kullanıcının girdiği yanlış değerin programın doğruluğu ile ilgisi yoktur. Bu gibi durumlarda assert
'ten yararlanmak yerine daha önceki bölümlerde de yaptığımız gibi throw
ile hata atmak doğru olur.
std.exception
modülünde tanımlanmış olan ve buradaki kullanımında "şart koşuyorum" anlamına gelen enforce
, hata atarken daha önce de kullandığımız throw
ifadesinin yerine geçer.
Örneğin, belirli bir koşula bağlı olarak bir hata atıldığını varsayalım:
if (adet < 3) { throw new Exception("En az 3 tane olmalı."); }
enforce
bir anlamda if
denetimini ve throw
deyimini sarmalar. Aynı kod enforce
ile aşağıdaki gibi yazılır:
import std.exception; // ... enforce(adet >= 3, "En az 3 tane olmalı.");
Mantıksal ifadenin öncekinin tersi olduğuna dikkat edin. Bunun nedeni, enforce
'un "bunu şart koşuyorum" anlamını taşımasıdır. Görüldüğü gibi, enforce
koşul denetimine ve throw
deyimine gerek bırakmaz.
Nasıl kullanmalı
assert
programcı hatalarını yakalamak için kullanılır. Örneğin, yukarıdaki ayGünleri
işlevinde ve menüBaşlığı
değişkeniyle ilgili olarak kullanılan assert
'ler tamamen programcılıkla ilgili hatalara karşı bir güvence olarak kullanılmışlardır.
Bazı durumlarda assert
kullanmakla hata atmak arasında karar vermek güç olabilir. Böyle durumlarda beklenmedik durumun programın kendisi ile mi ilgili olduğuna bakmak gerekir. Eğer denetim programın kendisi ile ilgili ise assert
kullanılmalıdır.
Herhangi bir işlem gerçekleştirilemediğinde ise hata atılmalıdır. Bu iş için daha kullanışlı olduğu için enforce
'tan yararlanmanızı öneririm.
Bu konudaki başka bir kıstas, karşılaşılan durumun giderilebilen bir hata çeşidi olup olmadığıdır. Eğer giderilebilen bir durumsa hata atmak uygun olabilir. Böylece daha üst düzeydeki bir işlev atılan bu hatayı yakalayabilir ve duruma göre farklı davranabilir.
Problemler
- Bu problemde size önceden yazılmış bir program göstermek istiyorum. Bu programın hata olasılığını azaltmak için bazı noktalarına
assert
denetimleri yerleştirilmiş. Amacım, buassert
denetimlerinin programdaki hataları ortaya çıkartma konusunda ne kadar etkili olduklarını göstermek.Program kullanıcıdan bir başlangıç zamanı ve bir işlem süresi alıyor ve o işlemin ne zaman sonuçlanacağını hesaplıyor. Program, sayılardan sonra gelen 'da' eklerini de doğru olarak yazdırıyor:
09:06'da başlayan ve 1 saat 2 dakika süren işlem 10:08'de sonlanır.
import std.stdio; import std.string; import std.exception; /* Verilen mesajı kullanıcıya gösterir ve girilen zaman * bilgisini saat ve dakika olarak okur. */ void zamanOku(string mesaj, out int saat, out int dakika) { write(mesaj, "? (SS:DD) "); readf(" %s:%s", &saat, &dakika); enforce((saat >= 0) && (saat <= 23) && (dakika >= 0) && (dakika <= 59), "Geçersiz zaman!"); } /* Zamanı dizgi düzeninde döndürür. */ string zamanDizgisi(int saat, int dakika) { assert((saat >= 0) && (saat <= 23)); assert((dakika >= 0) && (dakika <= 59)); return format("%02s:%02s", saat, dakika); } /* İki zaman bilgisini birbirine ekler ve üçüncü parametre * çifti olarak döndürür. */ void zamanEkle( int başlangıçSaati, int başlangıçDakikası, int eklenecekSaat, int eklenecekDakika, out int sonuçSaati, out int sonuçDakikası) { sonuçSaati = başlangıçSaati + eklenecekSaat; sonuçDakikası = başlangıçDakikası + eklenecekDakika; if (sonuçDakikası > 59) { ++sonuçSaati; } } /* Sayılardan sonra kesme işaretiyle ayrılarak kullanılacak * olan "de, da" ekini döndürür. */ string daEki(int sayı) { string ek; immutable int sonHane = sayı % 10; switch (sonHane) { case 1, 2, 7, 8: ek = "de"; break; case 3, 4, 5: ek = "te"; break; case 6, 9: ek = "da"; break; default: break; } assert(ek.length != 0); return ek; } void main() { int başlangıçSaati; int başlangıçDakikası; zamanOku("Başlangıç zamanı", başlangıçDakikası, başlangıçSaati); int işlemSaati; int işlemDakikası; zamanOku("İşlem süresi", işlemSaati, işlemDakikası); int bitişSaati; int bitişDakikası; zamanEkle(başlangıçSaati, başlangıçDakikası, işlemSaati, işlemDakikası, bitişSaati, bitişDakikası); sonucuYazdır(başlangıçSaati, başlangıçDakikası, işlemSaati, işlemDakikası, bitişSaati, bitişDakikası); } void sonucuYazdır( int başlangıçSaati, int başlangıçDakikası, int işlemSaati, int işlemDakikası, int bitişSaati, int bitişDakikası) { writef("%s'%s başlayan", zamanDizgisi(başlangıçSaati, başlangıçDakikası), daEki(başlangıçDakikası)); writef(" ve %s saat %s dakika süren işlem", işlemSaati, işlemDakikası); writef(" %s'%s sonlanır.", zamanDizgisi(bitişSaati, bitişDakikası), daEki(bitişDakikası)); writeln(); }
Bu programı çalıştırın ve girişine başlangıç olarak
06:09
ve süre olarak1:2
verin. Programın normal olarak sonlandığını göreceksiniz.Not: Aslında çıktının hatalı olduğunu farkedebilirsiniz. Bunu şimdilik görmezden gelin; çünkü az sonra
assert
'lerin yardımıyla bulacaksınız. - Bu sefer programa
06:09
ve15:2
zamanlarını girin. BirAssertError
atıldığını göreceksiniz. Hatada belirtilen satıra gidin ve programla ilgili olan hangi beklentinin gerçekleşmediğine bakın. Bu hatanın kaynağını bulmanız zaman alabilir. - Bu sefer programa
06:09
ve1:1
zamanlarını girin. Yeni bir hata ile karşılaşacaksınız. O satıra da gidin ve o hatayı da giderin. - Bu sefer programa
06:09
ve20:0
bilgilerini girin. Yineassert
tarafından yakalanan bir program hatası ile karşılaşacaksınız. O hatayı da giderin. - Bu sefer programa
06:09
ve1:41
bilgilerini girin. Programın da ekinin doğru çalışmadığını göreceksiniz:Başlangıç zamanı? (SS:DD) 06:09 İşlem süresi? (SS:DD) 1:41 06:09'da başlayan ve 1 saat 41 dakika süren işlem 07:50'da sonlanır
Bunu düzeltin ve duruma göre doğru ek yazmasını sağlayın: 7:10'da, 7:50'de, 7:40'ta, vs.