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

dönüş değeri: [return value], işlevin üreterek döndürdüğü değer
iç olanak: [core feature], dilin kütüphane gerektirmeyen bir olanağı
ifade: [expression], programın değer oluşturan veya yan etki üreten bir bölümü
yan etki: [side effect], bir ifadenin, ürettiği değer dışındaki etkisi
... bütün sözlük



İngilizce Kaynaklar


Diğer




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
  1. 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, bu assert 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 olarak 1: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.

  2. Bu sefer programa 06:09 ve 15:2 zamanlarını girin. Bir AssertError 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.
  3. Bu sefer programa 06:09 ve 1:1 zamanlarını girin. Yeni bir hata ile karşılaşacaksınız. O satıra da gidin ve o hatayı da giderin.
  4. Bu sefer programa 06:09 ve 20:0 bilgilerini girin. Yine assert tarafından yakalanan bir program hatası ile karşılaşacaksınız. O hatayı da giderin.
  5. Bu sefer programa 06:09 ve 1: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.