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

değişmez: [immutable], programın çalışması süresince kesinlikle değişmeyen
derleyici: [compiler], programlama dili kodunu bilgisayarın anladığı makine koduna çeviren program
dinamik: [dynamic], çalışma zamanında değişebilen
geçici: [temporary], bir işlem için geçici olarak oluşturulan ve yaşamı kısa süren değişken veya nesne
hazır değer: [literal], kod içinde hazır olarak yazılan değerler
işaretli tür: [signed type], eksi ve artı değer alabilen tür
işaretsiz tür: [unsigned type], yalnızca artı değer alabilen tür
mikro işlemci: [CPU], bilgisayarın beyni
otomatik: [implicit], derleyici tarafından otomatik olarak yapılan
referans türü: [reference type], başka bir nesneye erişim sağlayan tür
sabit: [const], bir bağlamda değiştirilmeyen
söz dizimi: [syntax], dilin yazım ile ilgili olan kuralları
statik: [static], derleme zamanında belirli olan
tanımsız davranış: [undefined behavior], programın ne yapacağının dil tarafından tanımlanmamış olması
tür dönüşümü: [type conversion], bir değeri kullanarak başka bir türden değer elde etmek
... bütün sözlük



İngilizce Kaynaklar


Diğer




Tür Dönüşümleri

İşlemlerde kullanılan değişken ve nesne türlerinin hem o işlemlerle hem de birbirleriyle uyumlu olmaları gerekir. Yoksa anlamsız veya yanlış sonuçlar doğabilir. D'nin de aralarında bulunduğu bazı diller türlerin uyumluluklarını derleme zamanında denetlerler. Böyle dillere "türleri derleme zamanında belli olan" anlamında "statically typed" dil denir.

Anlamsız işlem örneği olarak, bir toplama işleminde sanki bir sayıymış gibi dizgi kullanmaya çalışan şu koda bakalım:

    char[] dizgi;
    writeln(dizgi + 5);    // ← derleme HATASI

Derleyici o kodu tür uyuşmazlığı nedeniyle reddeder. Bu yazıyı yazdığım sırada kullandığım derleyici, türlerin uyumsuz olduğunu bildiren şu hatayı veriyor:

Error: incompatible types for ((dizgi) + (5)): 'char[]' and 'int'

O hata mesajı, ((dizgi) + (5)) ifadesinde uyumsuz türler olduğunu belirtir: char[] ve int.

Tür uyumsuzluğu, farklı tür demek değildir. Çünkü farklı türlerin güvenle kullanılabildiği işlemler de vardır. Örneğin double türündeki bir değişkene int türünde bir değer eklenmesinde bir sakınca yoktur:

    double toplam = 1.25;
    int artış = 3;
    toplam += artış;

toplam ve artış farklı türlerden oldukları halde o işlemde bir yanlışlık yoktur; çünkü bir kesirli sayı değişkeninin bir int değer kadar arttırılmasında bir uyumsuzluk yoktur.

Otomatik tür dönüşümleri

Her ne kadar bir double değerin bir int değer kadar arttırılmasında bir sakınca olmasa da, o işlemin mikro işlemcide yine de belirli bir türde yapılması gerekir. Kesirli Sayılar bölümünden hatırlayacağınız gibi; 64 bitlik olan double, 32 bitlik olan int'ten daha büyük (veya geniş) bir türdür. Bir int'e sığabilen her değer bir double'a da sığabilir.

Birbirinden farklı türler kullanılan işlemlerle karşılaştığında, derleyici önce değerlerden birisini diğer türe dönüştürür, ve işlemi ondan sonra gerçekleştirir. Bu dönüşümde kullanılan tür, değer kaybına neden olmayacak şekilde seçilir. Örneğin double türü int türünün bütün değerlerini tutabilir, ama bunun tersi doğru değildir. O yüzden yukarıdaki toplam += artış işlemi double türünde güvenle gerçekleştirilebilir.

Dönüştürülen değer, her zaman için isimsiz ve geçici bir değişken veya nesnedir. Asıl değerin kendisi değişmez. Örneğin yukarıdaki += işlemi sırasında artış'ın kendi türü değiştirilmez, ama artış'ın değerine eşit olan geçici bir değer kullanılır. Yukarıdaki işlemde perde arkasında neler olduğunu şöyle gösterebiliriz:

    {
        double aslında_isimsiz_olan_double_bir_deger = artış;
        toplam += aslında_isimsiz_olan_double_bir_deger;
    }

Derleyici, int değeri önce double türündeki geçici bir ara değere dönüştürür ve işlemde o dönüştürdüğü değeri kullanır. Bu örnekteki geçici değer yalnızca += işlemi süresince yaşar.

Böyle otomatik dönüşümler aritmetik işlemlerle sınırlı değildir. Birbirinin aynısı olmayan türlerin kullanıldığı başka durumlarda da otomatik tür dönüşümleri uygulanır. Eğer kullanılan türler bir dönüşüm sonucunda birlikte kullanılabiliyorlarsa, derleyici gerektikçe değerleri otomatik olarak dönüştürür. Örneğin int türünde parametre alan bir işleve byte türünde bir değer gönderilebilir:

void birİşlem(int sayı) {
    // ...
}

void main() {
    byte küçükDeğer = 7;
    birİşlem(küçükDeğer);    // otomatik tür dönüşümü
}

Orada da önce küçükDeğer'e eşit geçici bir int oluşturulur, ve birİşlem o geçici int değeri ile çağrılır.

int terfileri

Aşağıdaki tabloda sol taraftaki türler çoğu aritmetik işlemde doğrudan kullanılmazlar, önce otomatik olarak sağ taraftaki türlere dönüştürülürler:

Hangi Türden Hangi Türe
bool int
byte int
ubyte int
short int
ushort int
char int
wchar int
dchar uint

int terfileri enum türlerine de uygulanır.

Bu terfilerin nedeni mikro işlemcinin doğal türünün int olmasıdır. Örneğin, aşağıdaki her iki değişken de ubyte oldukları halde toplama işlemi o değişkenlerin değerleri int türüne terfi edildikten sonra gerçekleştirilir:

    ubyte a = 1;
    ubyte b = 2;
    writeln(typeof(a + b).stringof);  // işlem ubyte değildir

Çıktısı:

int

Terfi edilen a ve b değişkenleri değildir. Toplama işleminde kullanılabilsinler diye yalnızca onların değerleri geçici değerler olarak terfi edilirler.

Aritmetik dönüşümler

Aritmetik işlemlerde kullanılan değerler güvenli yönde, yani küçük türden büyük türe doğru gerçekleştirilirler. Bu kadarını akılda tutmak çoğu durumda yeterli olsa da aslında bu kurallar oldukça karışıktır, ve işaretli türlerden işaretsiz türlere yapılan dönüşümlerde de hataya yol açabilirler.

Dönüşüm kuralları şöyledir:

  1. Değerlerden birisi real ise diğeri real'e dönüştürülür
  2. Değilse ama birisi double ise diğeri double'a dönüştürülür
  3. Değilse ama birisi float ise diğeri float'a dönüştürülür
  4. Değilse yukarıdaki int terfisi dönüşümleri uygulanır ve sonra şu işlemlere geçilir:
    1. Eğer iki tür de aynı ise durulur
    2. Eğer her ikisi de işaretli ise, veya her ikisi de işaretsiz ise; küçük tür büyük türe dönüştürülür
    3. Eğer işaretli tür işaretsiz türden büyükse, işaretsiz olan işaretliye dönüştürülür
    4. Hiçbirisi değilse işaretli tür işaretsiz türe dönüştürülür

Yukarıdaki son kural ne yazık ki hatalara yol açabilir:

    int    a = 0;
    int    b = 1;
    size_t c = 0;
    writeln(a - b + c);  // Şaşırtıcı sonuç!

Çıktısı şaşırtıcı biçimde size_t.max olur:

18446744073709551615

Yukarıdaki son kural nedeniyle ifade int türünde değil, size_t türünde gerçekleştirilir. size_t de işaretsiz bir tür olduğundan -1 değerini taşıyamaz ve sonuç alttan taşarak size_t.max olur.

Dilim dönüşümleri

Bir kolaylık olarak, sabit uzunluklu diziler işlev çağrılarında otomatik olarak dilimlere dönüşebilirler:

import std.stdio;

void foo() {
    int[2] dizi = [ 1, 2 ];

    // Sabit uzunluklu dizi dilim olarak geçiriliyor:
    bar(dizi);
}

void bar(int[] dilim) {
    writeln(dilim);
}

void main() {
    foo();
}

bar()'ın parametresi bütün elemanlara erişim sağlayan bir dilimdir:

[1, 2]

Uyarı: Eğer işlev, dilimi sonradan kullanmak üzere saklıyorsa yerel bir sabit uzunluklu dizinin o işleve geçirilmesi yanlıştır. Örneğin, aşağıdaki programda bar()'ın sonradan kullanılmak üzere sakladığı dilim foo()'dan çıkıldığında geçerli değildir:

import std.stdio;

void foo() {
    int[2] dizi = [ 1, 2 ];

    // Sabit uzunluklu dizi dilim olarak geçiriliyor:
    bar(dizi);

}  // ← NOT: 'dizi' bu noktadan sonra geçerli değildir

int[] saklananDilim;

void bar(int[] dilim) {
    // Yakında geçersiz olacak bir dilim saklamaktadır:
    saklananDilim = dilim;
    writefln("bar içinde : %s", saklananDilim);
}

void main() {
    foo();

    /* HATA: Artık dizi elemanı olmayan belleğe erişir */
    writefln("main içinde: %s", saklananDilim);
}

Böyle bir hatanın sonucunda programın davranışı tanımsızdır. Örneğin, dizi'nin elemanlarının bulunduğu belleğin çoktan başka amaçlarla kullanıldığı gözlemlenebilir:

bar içinde : [1, 2]        ← asıl elemanlar
main içinde: [4396640, 0]  ← tanımsız davranışın gözlemlenmesi
const dönüşümleri

Her referans türü kendisinin const olanına otomatik olarak dönüşür. Bu güvenli bir dönüşümdür çünkü hem zaten türün büyüklüğünde bir değişiklik olmaz hem de const değerler değiştirilemezler:

char[] parantezİçinde(const char[] metin) {
    return "{" ~ metin ~ "}";
}

void main() {
    char[] birSöz;
    birSöz ~= "merhaba dünya";
    parantezİçinde(birSöz);
}

O kodda sabit olmayan birSöz, sabit parametre alan işleve güvenle gönderilebilir, çünkü değerler sabit referanslar aracılığıyla değiştirilemezler.

Bunun tersi doğru değildir. const bir referans türü, const olmayan bir türe dönüşmez:

char[] parantezİçinde(const char[] metin) {
    char[] parametreDeğeri = metin;  // ← derleme HATASI
// ...
}

Bu konu yalnızca referans değişkenleri ve referans türleri ile ilgilidir. Çünkü değer türlerinde zaten değer kopyalandığı için, kopyanın const olan asıl nesneyi değiştirmesi söz konusu olamaz:

    const int köşeAdedi = 4;
    int kopyası = köşeAdedi;      // derlenir (değer türü)

Yukarıdaki durumda const türden const olmayan türe dönüşüm yasaldır çünkü dönüştürülen değer asıl değerin bir kopyası haline gelir.

immutable dönüşümleri

immutable belirteci kesinlikle değişmezlik gerektirdiğinden ne immutable türlere dönüşümler ne de immutable türlerden dönüşümler otomatiktir:

    string a = "merhaba";
    char[] b = a;          // ← derleme HATASI
    string c = b;          // ← derleme HATASI

const dönüşümlerde olduğu gibi bu konu da yalnızca referans türleriyle ilgilidir. Değer türlerinin değerleri kopyalandıklarından, değer türlerinde her iki yöne doğru dönüşümler de otomatiktir:

    immutable a = 10;
    int b = a;           // derlenir (değer türü)
enum dönüşümleri

enum bölümünden hatırlayacağınız gibi, enum türleri isimli değerler kullanma olanağı sunarlar:

    enum OyunKağıdıRengi { maça, kupa, karo, sinek }

Değerleri özellikle belirtilmediği için o tanımda değerler sıfırdan başlayarak ve birer birer arttırılarak atanır. Buna göre örneğin OyunKağıdıRengi.sinek'in değeri 3 olur.

Böyle isimli enum değerleri, otomatik olarak tamsayı türlere dönüşürler. Örneğin aşağıdaki koddaki toplama işlemi sırasında OyunKağıdıRengi.kupa 1 değerini alır ve sonuç 11 olur:

    int sonuç = 10 + OyunKağıdıRengi.kupa;
    assert(sonuç == 11);

Bunun tersi doğru değildir: tamsayı değerler enum türlerine otomatik olarak dönüşmezler. Örneğin aşağıdaki kodda renk değişkeninin 2 değerinin karşılığı olan OyunKağıdıRengi.karo değerini almasını bekleyebiliriz; ama derlenemez:

    OyunKağıdıRengi renk = 2;   // ← derleme HATASI

Tamsayıdan enum değerlere dönüşümün açıkça yapılması gerekir. Bunu aşağıda göreceğiz.

bool dönüşümleri

false 0'a, true da 1'e otomatik olarak dönüşür:

    int birKoşul = false;
    assert(birKoşul == 0);

    int başkaKoşul = true;
    assert(başkaKoşul == 1);

Hazır değer kullanıldığında bunun tersi ancak iki özel değer için doğrudur: 0 hazır değeri false'a, 1 hazır değeri de true'ya otomatik olarak dönüşür:

    bool birDurum = 0;
    assert(!birDurum);     // false

    bool başkaDurum = 1;
    assert(başkaDurum);    // true

Sıfır ve bir dışındaki hazır değerler otomatik olarak dönüşmezler:

    bool b = 2;    // ← derleme HATASI

Bazı deyimlerin mantıksal ifadelerden yararlandıklarını biliyorsunuz: if, while, vs. Aslında böyle deyimlerde yalnızca bool değil, başka türler de kullanılabilir. Başka türler kullanıldığında sıfır değeri false'a, sıfırdan başka değerler de true'ya otomatik olarak dönüşürler:

    int i;
    // ...

    if (i) {    // ← int, mantıksal ifade yerine kullanılıyor
        // ... 'i' sıfır değilmiş

    } else {
        // ... 'i' sıfırmış
    }

Benzer biçimde, null değerler otomatik olarak false'a, null olmayan değerler de true'ya dönüşürler. Bu, referansların null olup olmadıklarının denetlenmesinde kolaylık sağlar:

    int[] a;
    // ...

    if (a) {    // ← otomatik bool dönüşümü
        // ... null değil; 'a' kullanılabilir ...

    } else {
        // ... null; 'a' kullanılamaz ...
    }
Açıkça yapılan tür dönüşümleri

Bazı durumlarda bazı tür dönüşümlerinin elle açıkça yapılması gerekebilir çünkü bazı dönüşümler veri kaybı tehlikesi ve güvensizlik nedeniyle otomatik değillerdir:

Programcının isteği ile açıkça yapılan tür dönüşümleri için aşağıdaki yöntemler kullanılabilir:

Kurma söz dizimi

Yapı ve sınıf nesnelerinin kurma söz dizimi başka türlerle de kullanılabilir:

    HedefTür(değer)

Örneğin, aşağıdaki dönüşüm bir int değerinden bir double değeri elde etmektedir (örneğin, sonucun virgülden sonrasını kaybetmemek için):

    int i;
    // ...
    const sonuç = double(i) / 2;
Çoğu dönüşüm için to()

Daha önce hep değerleri string türüne dönüştürmek için to!string olarak kullandığımız to aslında mümkün olan her dönüşümü sağlayabilir. Söz dizimi şöyledir:

    to!(HedefTür)(değer)

Aslında bir şablon olan to, şablonların daha ileride göreceğimiz kısa söz diziminden de yararlanabildiği için hedef türün tek sözcükle belirtilebildiği durumlarda hedef tür parantezsiz olarak da yazılabilir:

    to!HedefTür(değer)

to'nun kullanımını görmek için bir double değerini short türüne ve bir string değerini de int türüne dönüştürmeye çalışan aşağıdaki koda bakalım:

void main() {
    double d = -1.75;

    short s = d;     // ← derleme HATASI
    int i = "42";    // ← derleme HATASI
}

Her double değer short olarak ifade edilemeyeceğinden ve her dizgi int olarak kabul edilebilecek karakterler içermediğinden o dönüşümler otomatik değildir. Programcı, uygun olan durumlarda bu dönüşümleri açıkça to ile gerçekleştirebilir:

import std.conv;

void main() {
    double d = -1.75;

    short s = to!short(d);
    assert(s == -1);

    int i = to!int("42");
    assert(i == 42);
}

Dikkat ederseniz short türü kesirli değer alamadığı için s'nin değeri -1 olarak dönüştürülebilmiştir.

to() güvenlidir: Mümkün olmayan dönüşümlerde hata atar.

Hızlı immutable dönüşümleri için assumeUnique()

to(), immutable dönüşümlerini de gerçekleştirebilir:

    int[] dilim = [ 10, 20, 30 ];
    auto değişmez = to!(immutable int[])(dilim);

Yukarıdaki koddaki değiştirilebilen elemanlardan oluşan dilim'e ek olarak immutable bir dilim daha oluşturulmaktadır. değişmez'in elemanlarının gerçekten değişmemelerinin sağlanabilmesi için dilim ile aynı elemanları paylaşmaması gerekir. Aksi taktirde, dilim yoluyla yapılan değişiklikler değişmez'in elemanlarının da değişmesine ve böylece immutable belirtecine aykırı duruma düşmesine neden olurdu.

Bu yüzden to(), immutable dönüşümlerini asıl değerin kopyasını alarak gerçekleştirir. Aynı durum dizilerin .idup niteliği için de geçerlidir; hatırlarsanız .idup'un ismi "kopyala" anlamına gelen "duplicate"ten türemiştir. değişmez'in elemanlarının dilim'inkilerden farklı olduklarını ilk elemanlarının adreslerinin farklı olmasına bakarak gösterebiliriz:

    assert(&(dilim[0]) != &(değişmez[0]));

Bazen bu kopya gereksiz olabilir ve nadiren de olsa program hızını etkileyebilir. Bunun bir örneğini görmek için değişmez bir tamsayı dilimi bekleyen bir işleve bakalım:

void işlev(immutable int[] koordinatlar) {
    // ...
}

void main() {
    int[] sayılar;
    sayılar ~= 10;
    // ... çeşitli değişiklikler ...
    sayılar[0] = 42;

    işlev(sayılar);    // ← derleme HATASI
}

Yukarıdaki kod, sayılar parametresi işlevin gerekçesini yerine getirmediği için derlenemez çünkü programın derlenebilmesi için işlev()'e immutable bir dilim verilmesi şarttır. Bunun bir yolunun to() olduğunu gördük:

import std.conv;
// ...
    auto değişmezSayılar = to!(immutable int[])(sayılar);
    işlev(değişmezSayılar);

Ancak, eğer sayılar dilimi yalnızca bu parametreyi oluşturmak için gerekmişse ve işlev() çağrıldıktan sonra bir daha hiç kullanılmayacaksa, elemanların değişmezSayılar dilimine kopyalanmaları gereksiz olacaktır. assumeUnique(), bir dilimin elemanlarının belirli bir noktadan sonra değişmez olarak işaretlenmelerini sağlar:

import std.exception;
// ...
    auto değişmezSayılar = assumeUnique(sayılar);
    işlev(değişmezSayılar);
    assert(sayılar is null);    // asıl dilim null olur

"Tek kopya olduğunu varsay" anlamına gelen assumeUnique() eleman kopyalamaz; aynı elemanlara immutable olarak erişim sağlayan yeni bir dilim döndürür. Elemanların asıl dilim aracılığıyla yanlışlıkla değiştirilmelerini önlemek için de asıl dilimi null'a eşitler.

cast işleci

to()'nun ve assumeUnique()'in kendi gerçekleştirmelerinde de yararlandıkları alt düzey dönüşüm işleci cast işlecidir.

Hedef tür cast parantezinin içine yazılır:

    cast(HedefTür)değer

cast, to()'nun güvenle gerçekleştiremediği dönüşümleri de yapacak kadar güçlüdür. Örneğin, aşağıdaki dönüşümler to()'nun çalışma zamanında hata atmasına neden olur:

    OyunKağıdıRengi renk = to!OyunKağıdıRengi(7); // ← hata atar
    bool b = to!bool(2);                          // ← hata atar

Örneğin, atılan hata dönüştürülmek istenen 7 değerinin OyunKağıdıRengi türünde bir karşılığı olmadığını bildirir:

std.conv.ConvException@phobos/std/conv.d(1778): Value (7)
does not match any member value of enum 'OyunKağıdıRengi'

Bir tamsayının OyunKağıdıRengi değeri olarak kullanılabileceğinden veya bir tamsayı değerin bool anlamında kullanılabileceğinden ancak programcı emin olabilir. Bu gibi durumlarda cast işlecinden yararlanılmalıdır:

    // Olasılıkla hatalı ama mümkün:
    OyunKağıdıRengi renk = cast(OyunKağıdıRengi)7;

    bool b = cast(bool)2;
    assert(b);

Gösterge türleri arasındaki dönüşümler cast ile yapılmak zorundadır:

    void * v;
    // ...
    int * p = cast(int*)v;

Yaygın olmasa da, bazı C kütüphane arayüzleri gösterge değerlerinin gösterge olmayan değişkenlerde tutulmalarını gerektirebilir. Asıl gösterge değeri sonuçta tekrar elde edilebildiği sürece böyle dönüşümler de cast ile gerçekleştirilir:

    size_t saklananGöstergeDeğeri = cast(size_t)p;
    // ...
    int * p2 = cast(int*)saklananGöstergeDeğeri;
Özet