Değerler ve Referanslar
Değer türü ile referans türü arasındaki fark, özellikle daha sonra göreceğimiz yapıları ve sınıfları anlamada yararlı olacak.
Bu bölümde ayrıca değişkenlerin adreslerini bildiren &
işlecini de tanıtacağım.
En sonda da şu iki önemli kavramı gösteren bir tablo vereceğim:
- değer karşılaştırması
- adres karşılaştırması
Değer türü
Bunun tanımı son derece basittir: Değişkenleri değer taşıyan türlere değer türü denir. Örneğin bütün tamsayı ve kesirli sayı türleri değer türleridir çünkü bu türlerden olan her değişkenin kendi değeri vardır. Pek açık olmasa da sabit uzunluklu diziler de değer türleridir.
Örneğin, int
türünden olan bir değişken bir tamsayı değer taşır:
int hız = 123;
hız
değişkeninin büyüklüğü int
'in büyüklüğü olan 4 bayttır. Belleği soldan sağa doğru bir şerit halinde devam ediyormuş gibi gösterirsek, o değişkenin bellekte şu şekilde yaşadığını düşünebiliriz:
hız ───┬─────┬─── │ 123 │ ───┴─────┴───
Değer türlerinin değişkenleri kopyalandıklarında kendi özel değerlerini edinirler:
int yeniHız = hız;
Yeni değişkene bellekte kendisine ait bir yer ayrılır ve yeniHız
'ın da kendi değeri olur:
hız yeniHız ───┬─────┬─── ───┬─────┬─── │ 123 │ │ 123 │ ───┴─────┴─── ───┴─────┴───
Doğal olarak, artık bu değişkenlerde yapılan değişiklikler birbirlerinden bağımsızdır:
hız = 200;
Diğer değişkenin değeri değişmez:
hız yeniHız ───┬─────┬─── ───┬─────┬─── │ 200 │ │ 123 │ ───┴─────┴─── ───┴─────┴───
assert
hatırlatması
Bu bölümde kavramların doğruluklarını göstermek için assert
ve enforce
bölümünde gördüğümüz assert
denetimlerinden yararlanacağım. Aşağıdaki örneklerde kullandığım assert
denetimlerini "bu doğrudur" demişim gibi kabul edin.
Örneğin aşağıdaki assert(hız == yeniHız)
ifadesi, "hız, yeniHız'a eşittir" anlamına geliyor.
Değer kimliği
Yukarıdaki gösterimlerden de anlaşılabileceği gibi, değişkenlerin eşitlikleri iki anlamda ele alınabilir:
- Değer eşitliği: Şimdiye kadar bir çok örnekte kullandığımız
==
işleci, değişkenlerin değerlerini karşılaştırır. Birbirlerinden farklı olan iki değişkenin bu açıdan eşit olmaları, onların değerlerinin eşit olmaları anlamına gelir. - Değer kimliği: Kendi değerlerine sahip olmaları açısından bakıldığında,
hız
veyeniHız
'ın ayrı kimlikleri vardır. Değerleri eşit olsalar bile, birisinde yapılan değişiklik diğerini etkilemez.
int hız = 123; int yeniHız = hız; assert(hız == yeniHız); hız = 200; assert(hız != yeniHız);
Adres alma işleci &
Daha önce readf
kullanımında gördüğümüz gibi, bu işleç değişkenin adresini döndürür. Okuduğu bilgiyi hangi değişkene yazacağını readf
'e o değişkenin adresini vererek bildiriyorduk.
Not: Girişten Bilgi Almak bölümünde de gördüğümüz gibi, readf
aslında gösterge gerektirmez.
Değişkenlerin adreslerini başka amaçlar için de kullanabiliriz. Bir örnek olarak iki farklı değişkenin adresini yazdıran bir kod şöyle yazılabilir:
int hız = 123; int yeniHız = hız; writeln("hız : ", hız, " adresi: ", &hız); writeln("yeniHız: ", yeniHız, " adresi: ", ¥iHız);
hız
ve yeniHız
değişkenlerinin değerleri aynıdır, ama yukarıda da gösterildiği gibi bu değerler belleğin farklı adreslerinde bulunmaktadırlar:
hız : 123 adresi: BF9A78F0 yeniHız: 123 adresi: BF9A78F4
Not: Programı her çalıştırdığınızda farklı adresler görmeniz normaldir. Bu değişkenler işletim sisteminden alınan belleğin boş yerlerine yerleştirilirler.
Değişken adresleri normalde on altılı sayı sisteminde yazdırılır.
Ayrıca, adreslerin int
'in uzunluğu olan 4 kadar farklı olmalarına bakarak o değişkenlerin bellekte yan yana durduklarını da anlayabiliriz.
Referans değişkenleri
Referans türlerini anlatmaya geçmeden önce referans değişkenlerini tanıtmam gerekiyor.
Referans değişkenleri, başka değişkenlerin takma isimleri gibi kullanılan değişkenlerdir. Her ne kadar kendileri değişken gibi olsalar da, kendi özel değerleri yoktur. Böyle bir değişkende yapılan bir değişiklik asıl değişkeni etkiler.
Referans değişkenlerini aslında şimdiye kadar iki konuda görmüş ama üzerinde fazla durmamıştık:
foreach
döngüsünderef
ile: Bir grup elemanaforeach
döngüsünde sırayla erişilirkenref
anahtar sözcüğü kullanıldığında; eleman, dizi elemanının kendisi anlamına geliyordu. Kullanılmadığında ise dizi elemanının kopyası oluyordu.Bunu,
&
işleci ile de gösterebiliriz. Adresleri aynı ise, iki farklı değişken aslında belleğin aynı yerindeki bir değere erişim sağlıyorlar demektir:int[] dilim = [ 0, 1, 2, 3, 4 ]; foreach (i, ref eleman; dilim) { assert(&eleman == &dilim[i]); }
Her ne kadar farklı iki değişken olsalar da,
&
işleci ile alınan adreslerinin eşit olmaları, döngünün her tekrarında tanımlananeleman
vedilim[i]
'nin değer kimliği açısından aslında aynı olduklarını gösterir.Bir başka deyişle,
eleman
,dilim[i]
'nin takma ismidir. Daha başka bir deyişle,eleman
iledilim[i]
aynı değere erişim sağlarlar. Birisinde yapılan değişiklik, asıl değeri etkiler.eleman
takma ismi, sırayla asıl dizi elemanlarının takma ismi haline gelir. Bu durumu döngününi
'nin örneğin 3 olduğu tekrarı için şöyle gösterebiliriz:dilim[0] dilim[1] dilim[2] dilim[3] dilim[4] ⇢ ⇢ ⇢ (eleman) ──┬────────┬────────┬────────┬────────┬─────────┬── │ 0 │ 1 │ 2 │ 3 │ 4 │ ──┴────────┴────────┴────────┴────────┴─────────┴──
ref
veout
işlev parametrelerinde: İşlev parametreleriref
veyaout
olarak tanımlandıklarında işleve gönderilen asıl değişkenin takma ismi gibi işlem görürler.Bunu görmek için böyle iki parametre alan bir işlevin iki parametresine de aynı değişkeni gönderelim. Aynı değer kimliğine sahip olduklarını yine
&
işleci ile gösterebiliriz:import std.stdio; void main() { int asılDeğişken; writeln("asılDeğişken'in adresi : ", &asılDeğişken); işlev(asılDeğişken, asılDeğişken); } void işlev(ref int refParametre, out int outParametre) { writeln("refParametre'nin adresi: ", &refParametre); writeln("outParametre'nin adresi: ", &outParametre); assert(&refParametre == &outParametre); }
Her ne kadar farklı parametre olarak tanımlansalar da,
refParametre
veoutParametre
aslında aynı değere erişim sağlarlar çünkü zaten ikisi demain
içindekiasılDeğişken
'in takma ismidir:asılDeğişken'in adresi : 7FFF1DC7D7D8 refParametre'nin adresi: 7FFF1DC7D7D8 outParametre'nin adresi: 7FFF1DC7D7D8
Referans türü
Bazı türlerden olan değişkenler kendi kimlikleri olduğu halde kendileri değer taşımazlar; değer taşıyan başka değişkenlere erişim sağlarlar. Böyle türlere referans türü denir.
Bu kavramı daha önce dizi dilimlerinde görmüştük. Dilimler, var olan başka bir dizinin elemanlarına erişim sağlayan türlerdir; kendi elemanları yoktur:
// İsmi burada 'dizi' olarak yazılmış olsa da aslında bu // değişken de dilimdir; bütün elemanlara erişim sağlar. int[] dizi = [ 0, 1, 2, 3, 4 ]; // Baştaki ve sondaki elemanı dışlayarak ortadaki üçüne // erişim sağlayan bir dilim: int[] dilim = dizi[1 .. $ - 1]; // Şimdi dilim[0] ile dizi[1] aynı değere erişirler: assert(&dilim[0] == &dizi[1]); // Gerçekten de dilim[0]'da yapılan değişiklik dizi[1]'i // etkiler: dilim[0] = 42; assert(dizi[1] == 42);
Referans değişkenlerinin tersine, referans türleri yalnızca takma isim değildirler. Bunu görmek için aynı dilimin kopyası olan bir dilim daha oluşturalım:
int[] dilim2 = dilim;
Bu iki dilim kendi adresleri olan, bir başka deyişle kendi kimlikleri olan değişkenlerdir; dilim2
ile dilim
farklı adreslerde yaşarlar:
assert(&dilim != &dilim2);
İşte referans değişkenleri ile referans türlerinin ayrımı buna dayanır:
- Referans değişkenlerinin kendi kimlikleri yoktur, başka değişkenlerin takma isimleridirler.
- Referans türünden olan değişkenler ise kendi kimlikleri olan değişkenlerdir ama yine başka değerlere erişim sağlarlar.
Örneğin yukarıdaki dilim
ve dilim2
'yi bellek üzerinde şöyle gösterebiliriz:
dilim dilim2 ───┬───┬───┬───┬───┬───┬─── ───┬───┬─── ───┬───┬─── │ 0 │ 1 │ 2 │ 3 │ 4 │ │ o │ │ o │ ───┴───┴───┴───┴───┴───┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
İki dilimin erişim sağladıkları asıl elemanlar işaretli olarak gösteriliyor.
D'nin güçlü bir olanağı olan sınıfları daha ilerideki bölümlerde göreceğiz. D'yi C++'dan ayıran önemli farklardan birisi, D'nin sınıflarının referans türleri olmalarıdır. Yalnızca bunu göstermiş olmak için çok basit bir sınıf tanımlayacağım:
class BirSınıf { int üye; }
Sınıf nesneleri, daha önce hata atarken de kullandığımız new
ile oluşturulurlar:
auto değişken = new BirSınıf;
Bu durumda değişken
, new
işleciyle oluşturulmuş olan isimsiz bir BirSınıf
nesnesine erişim sağlayan bir referanstır:
(isimsiz BirSınıf nesnesi) değişken ───┬───────────────────┬─── ───┬───┬─── │ ... │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ▲ │ │ │ └────────────────────┘
Dilimlere benzer şekilde, değişken
kopyalandığında kopyası da aynı nesneye erişim sağlar ama kopyanın farklı adresi vardır:
auto değişken = new BirSınıf; auto değişken2 = değişken; assert(değişken == değişken2); assert(&değişken != &değişken2);
Yukarıda görüldüğü gibi, erişim sağlama açısından eşit olsalar da, adresleri farklı olduğu için farklı değişkenlerdir:
(isimsiz BirSınıf nesnesi) değişken değişken2 ───┬───────────────────┬─── ───┬───┬─── ───┬───┬─── │ ... │ │ o │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
Böyle iki farklı BirSınıf
nesnesinin gerçekten de aynı nesneye erişim sağladıklarını bir de şöyle gösterebiliriz:
auto değişken = new BirSınıf; değişken.üye = 1; auto değişken2 = değişken; // aynı nesneyi paylaşırlar değişken2.üye = 2; assert(değişken.üye == 2); // değişken'in de erişim // sağladığı nesne // değişmiştir
değişken2
yoluyla 2 değerini alan üye, değişken
'in de erişim sağladığı nesnenin üyesidir.
Başka bir referans türü, eşleme tablolarıdır. Eşleme tabloları da atandıklarında aynı asıl tabloya erişim sağlarlar:
string[int] isimleSayılar = [ 1 : "bir", 10 : "on", 100 : "yüz", ]; // Aynı asıl tabloyu paylaşmaya başlarlar: string[int] isimleSayılar2 = isimleSayılar; // Örneğin ikincisine eklenen eleman ... isimleSayılar2[4] = "dört"; // ... birincisinde de görünür. assert(isimleSayılar[4] == "dört");
Bir sonraki bölümde göreceğimiz gibi, baştaki eşleme tablosu null
ise eleman paylaşımı yoktur.
Atama işleminin farkı
Değer türlerinde ve referans değişkenlerinde atama işleminin sonucunda asıl değer değişir:
void main() { int sayı = 8; yarıyaBöl(sayı); // asıl değer değişir assert(sayı == 4); } void yarıyaBöl(ref int bölünen) { bölünen /= 2; }
Referans türlerinde ise atama işlemi, hangi asıl nesneye erişim sağlandığını değiştirir. Örneğin aşağıdaki kodda dilim3
'e yapılan atama işlemi onun eriştirdiği elemanların değerlerini değiştirmez; dilim3
'ün başka elemanları göstermesini sağlar:
void main() { int[] dilim1 = [ 10, 11, 12, 13, 14 ]; int[] dilim2 = [ 20, 21, 22 ]; int[] dilim3 = dilim1[1 .. 3]; // 1 ve 2 indeksli elemanlara // eriştirir dilim3[0] = 777; assert(dilim1 == [ 10, 777, 12, 13, 14 ]); // Bu atama işlemi dilim3'ün eriştirdiği elemanları // değiştirmez, dilim3'ün artık başka elemanlara // erişim sağlamasına neden olur dilim3 = dilim2[$ - 1 .. $]; // sonuncu elemana eriştirir dilim3[0] = 888; assert(dilim2 == [ 20, 21, 888 ]); }
Atama işlecinin referans türlerindeki bu etkisini bir de BirSınıf
türünde görelim:
auto değişken1 = new BirSınıf; değişken1.üye = 1; auto değişken2 = new BirSınıf; değişken2.üye = 2; auto kopya = değişken1; kopya.üye = 3; kopya = değişken2; kopya.üye = 4; assert(değişken1.üye == 3); assert(değişken2.üye == 4);
Oradaki atama işlemleri sonucunda kopya
önce değişken1
'in nesnesine, sonra da değişken2
'nin nesnesine erişim sağlar. kopya
yoluyla değeri değiştirilen üye
ilk seferde değişken1
'inkidir, sonra ise değişken2
'ninkidir.
Referans türleri hiçbir değere erişim sağlamıyor olabilirler
Referans değişkenlerinde mutlaka bir asıl değer vardır; onların yaşam süreçleri erişim sağladıkları bir asıl değer olmadan başlamaz. Referans türlerinin değişkenleri ise, henüz hiçbir değere erişim sağlamayacak şekilde oluşturulabilirler.
Örneğin bir BirSınıf
değişkeni, erişim sağladığı nesne henüz belli olmadan şöyle tanımlanabilir:
BirSınıf değişken;
Böyle değişkenler null
özel değerine eşittirler. Bu özel değeri ve is
anahtar sözcüğünü daha sonraki bir bölümde göreceğiz.
Sabit uzunluklu diziler değer türü, dinamik diziler referans türü
D'nin iki dizi türü bu konuda farklılık gösterir.
Dinamik diziler (dilimler), yukarıdaki örneklerde de görüldüğü gibi referans türleridir. Dinamik diziler kendilerine ait olmayan elemanlara erişim sağlarlar. Temel işlemler açısından referans olarak davranırlar.
Sabit uzunluklu diziler ise değer türleridir. Kendi elemanlarına sahiptirler ve değer türü olarak davranırlar:
int[3] dizi1 = [ 10, 20, 30 ]; // dizi2'nin elemanları dizi1'inkilerden farklı olur auto dizi2 = dizi1; dizi2[0] = 11; // İlk dizi değişmez assert(dizi1[0] == 10);
Tanımlandığı zaman uzunluğu da belirlendiği için dizi1
sabit uzunluklu bir dizidir. auto
anahtar sözcüğü nedeniyle dizi2
de aynı türü edinir. Her ikisi de kendi elemanlarına sahiptirler. Birisinde yapılan değişiklik diğerini etkilemez.
Deney
Yukarıda anlatılan farklı türlerin değişkenlerine ve onların adreslerine ==
işlecini uygulayınca ortaya şöyle bir tablo çıkıyor:
Değişken Türü a == b &a == &b ===================================================================== aynı değerli değişkenler (değer türü) true false farklı değerli değişkenler (değer türü) false false ref değişkenli foreach true true ref olmayan değişkenli foreach true false out parametreli işlev true true ref parametreli işlev true true in parametreli işlev true false aynı elemanlara erişen dilimler true false farklı elemanlara erişen dilimler false false aynı nesneye erişen BirSınıf'lar (referans türü) true false farklı nesneye erişen BirSınıf'lar (referans türü) false false
O tablo aşağıdaki programla üretilmiştir:
import std.stdio; import std.array; int modülDeğişkeni = 9; class BirSınıf { int üye; } void başlıkÇiz() { immutable dchar[] başlık = " Değişken Türü" ~ " a == b &a == &b"; writeln(); writeln(başlık); writeln(replicate("=", başlık.length)); } void bilgiSatırı(const dchar[] başlık, bool değerEşitliği, bool adresEşitliği) { writefln("%50s%9s%9s", başlık, değerEşitliği, adresEşitliği); } void main() { başlıkÇiz(); int sayı1 = 12; int sayı2 = 12; bilgiSatırı("aynı değerli değişkenler (değer türü)", sayı1 == sayı2, &sayı1 == &sayı2); int sayı3 = 3; bilgiSatırı("farklı değerli değişkenler (değer türü)", sayı1 == sayı3, &sayı1 == &sayı3); int[] dilim = [ 4 ]; foreach (i, ref eleman; dilim) { bilgiSatırı("ref değişkenli foreach", eleman == dilim[i], &eleman == &dilim[i]); } foreach (i, eleman; dilim) { bilgiSatırı("ref olmayan değişkenli foreach", eleman == dilim[i], &eleman == &dilim[i]); } outParametre(modülDeğişkeni); refParametre(modülDeğişkeni); inParametre(modülDeğişkeni); int[] uzunDilim = [ 5, 6, 7 ]; int[] dilim1 = uzunDilim; int[] dilim2 = dilim1; bilgiSatırı("aynı elemanlara erişen dilimler", dilim1 == dilim2, &dilim1 == &dilim2); int[] dilim3 = dilim1[0 .. $ - 1]; bilgiSatırı("farklı elemanlara erişen dilimler", dilim1 == dilim3, &dilim1 == &dilim3); auto değişken1 = new BirSınıf; auto değişken2 = değişken1; bilgiSatırı( "aynı nesneye erişen BirSınıf'lar (referans türü)", değişken1 == değişken2, &değişken1 == &değişken2); auto değişken3 = new BirSınıf; bilgiSatırı( "farklı nesneye erişen BirSınıf'lar (referans türü)", değişken1 == değişken3, &değişken1 == &değişken3); } void outParametre(out int parametre) { bilgiSatırı("out parametreli işlev", parametre == modülDeğişkeni, ¶metre == &modülDeğişkeni); } void refParametre(ref int parametre) { bilgiSatırı("ref parametreli işlev", parametre == modülDeğişkeni, ¶metre == &modülDeğişkeni); } void inParametre(in int parametre) { bilgiSatırı("in parametreli işlev", parametre == modülDeğişkeni, ¶metre == &modülDeğişkeni); }
Notlar:
- Programda işlev parametrelerini karşılaştırmak için bir de modül değişkeni kullanılıyor. Modül değişkenleri işlevlerin dışında tanımlanırlar ve içinde tanımlandıkları modüldeki bütün kodlar tarafından erişilebilirler.
-
std.array
modülününreplicate()
işlevi kendisine verilen aralığı (yukarıdaki"="
) belirtilen sayıda tekrarlar.
Özet
- Değer türlerinden olan her değişkenin kendi değeri ve kendi adresi vardır.
- Referans değişkenlerinin ne değerleri vardır ne de adresleri; takma isim gibidirler.
- Referans türlerinden olan değişkenlerin kendi adresleri vardır; ama erişim sağladıkları değer kendilerinin değildir.
- Referans türlerinde atama işlemi değer değiştirmez, hangi asıl nesneye erişildiğini değiştirir.
- Referans türlerinden olan değişkenler
null
olabilirler.