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:
- Değerlerden birisi
real
ise diğerireal
'e dönüştürülür - Değilse ama birisi
double
ise diğeridouble
'a dönüştürülür - Değilse ama birisi
float
ise diğerifloat
'a dönüştürülür - Değilse yukarıdaki
int
terfisi dönüşümleri uygulanır ve sonra şu işlemlere geçilir:- Eğer iki tür de aynı ise durulur
- 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
- Eğer işaretli tür işaretsiz türden büyükse, işaretsiz olan işaretliye dönüştürülür
- 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
bool
mantıksal ifadelerin doğal türü olduğu halde, yalnızca iki değeri olması nedeniyle tek bitlik bir tamsayı gibi görülebilir ve bazı durumlarda öyle işlem görür. 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:
- Büyük türden küçük türe dönüşümler
const
türden değişebilen türe dönüşümlerimmutable
dönüşümleri- Tamsayılardan
enum
değerlere dönüşümler - vs.
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
std.conv.to
işlevistd.exception.assumeUnique
işlevicast
işleci
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
- Otomatik tür dönüşümleri güvenli yönde yapılır: Küçük türden büyük türe doğru ve değişebilen türden değişmez türe doğru.
- Ancak, işaretsiz türlere doğru yapılan dönüşümler o türler eksi değerler tutamadıkları için şaşırtıcı sonuçlar doğurabilirler.
enum
türler tamsayı türlere otomatik olarak dönüşürler ama tamsayılarenum
türlere otomatik olarak dönüşmezler.false
0'a,true
da 1'e otomatik olarak dönüşür. Benzer biçimde, sıfır değerlerfalse
'a, sıfır olmayan değerler detrue
'ya otomatik olarak dönüşür.null
referanslar otomatik olarakfalse
değerine,null
olmayan referanslar datrue
değerine dönüşürler.- Bazı tür dönüşümleri için kurma söz dizimi kullanılabilir.
- Açıkça yapılan çoğu dönüşüm için
to()
kullanılır. - Kopyalamadan
immutable
'a dönüştürmek içinassumeUnique()
kullanılır. cast
en alt düzey ve en güçlü dönüşüm işlecidir.