Değerler ve Referanslar
Yakında başlayacağımız yapılara ve sınıflara geçmeden önce değer türleri ve referans türleri kavramlarını anlamak gerekiyor. Bu bilgileri bir önceki dersteki nesne yaşam süreçleri konusuyla bağdaştırınca yapı ve sınıf nesnelerini anlamak daha kolay olacak.
Bu derste 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 temel türler ve sabit uzunluklu diziler değer türleridir, çünkü bu türlerden olan her değişkenin kendi değeri vardır.
Örneğin int türünden bir değişken, bir tamsayı değer taşır:
int hız = 123;
Belleği önceki derste gördüğümüz gibi bir şerit olarak hayal edersek, bu 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;
Yeni değişkenin değeri değişmez:
hız yeniHız
---+-----+--- ---+-----+---
| 200 | | 123 |
---+-----+--- ---+-----+---
assert hatırlatması
Bu derste kavramların doğruluklarını göstermek için assert dersinde gördüğümüz assert ifadelerini kullanacağım. Aşağıdaki örneklerde kullandığım assert ifadelerini "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ızveyeniHı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 işleci &
Daha önce din.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.
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; dout.writefln("hız : ", hız, " adresi: ", &hız); dout.writefln("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 onaltı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:
foreachdöngüsünderefile: Bir grup elemanaforeachdöngüsünde sırayla erişilirkenrefanahtar 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[] dizi = [ 0, 1, 2, 3, 4 ]; foreach (i, ref eleman; dizi) { assert(&eleman == &dizi[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ımlanan eleman ve dizi[i]'nin değer kimliği açısından aslında aynı olduklarını gösterir.
Bir başka deyişle, eleman, dizi[i]'nin takma ismidir. Daha başka bir deyişle, eleman ile dizi[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ün i'nin örneğin 3 olduğu tekrarı için şöyle gösterebiliriz:
dizi[0] dizi[1] dizi[2] dizi[3] dizi[4]
⇢ ⇢ ⇢ (eleman)
--+--------+--------+--------+--------+---------+--
| 0 | 1 | 2 | 3 | 4 |
--+--------+--------+--------+--------+---------+--
ref ve out işlev parametrelerinde: İşlev parametreleri ref veya out 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ı & işleci ile şu şekilde kanıtlayabiliriz:
import std.cstream; void main() { int asıl; dout.writefln("asıl'ın adresi : ", &asıl); işlev(asıl, asıl); } void işlev(ref int referans, out int çıkış) { dout.writefln("referans'ın adresi: ", &referans); dout.writefln("çıkış'ın adresi : ", &çıkış); assert(&referans == &çıkış); }
Her ne kadar farklı parametre olarak tanımlansalar da, işlev içindeki referans ve çıkış aslında aynı değere erişim sağlarlar; çünkü zaten ikisi de main içindeki asıl'ın takma ismidir:
asıl'ın adresi : BFCA9F34 referans'ın adresi: BFCA9F34 çıkış'ın adresi : BFCA9F34
O koddaki referans değişkenlerinin bellekte kendilerine ait yerleri olmadığını ve asıl'ın takma isimleri olduklarını aşağıdaki şekilde ifade edebiliriz.:
asıl
(referans)
(çıkış)
---+-----+---
| 0 |
---+-----+---
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, varolan başka bir dizinin elemanlarına erişim sağlayan türlerdir; kendi elemanları yoktur:
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. Hatta dizi'yi de katacak olursak, üçünün de adresi farklıdır:
assert(&dizi != &dilim); assert(&dizi != &dilim2); assert(&dilim != &dilim2);
Not: Aslında dinamik diziler (uzunluğu sabit olmayan diziler) de teknik olarak dilimdirler.
İşte referans değişkenleri ile referans türlerinin ayrımı buna dayanır:
- Referans değişkenlerinin kendi kimlikleri yoktur, onlar başka bir değişkenin takma ismidirler
- Referans türünden olan değişkenler ise, kendi kimlikleri olan değişkenlerdir; ama yine başka bir değere 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 |
---+---+---+---+---+---+--- ---+-|-+--- ---+-|-+---
▲ | |
| | |
+--------------------+------------+
O dilimlerin erişim sağladıkları asıl dizi elemanlarını mavi ile gösterdim.
Sınıfların da referans türleri olduklarını göreceğiz. Örnek olarak yine daha önce karşımıza çıkan File sınıfını kullanabiliriz:
auto dosya = new File("oyuncu_listesi", FileMode.Out);
Bu durumda dosya, new işleciyle oluşturulmuş olan isimsiz bir File nesnesine erişim sağlayan bir referanstır:
(isimsiz File nesnesi) dosya
---+-------------------+--- ---+---+---
| ... | | o |
---+-------------------+--- ---+-|-+---
▲ |
| |
+--------------------+
Not: Burada "isimsiz" derken, nesnenin kendisinin isminin olmadığını belirtiyorum; dosya isminden bahsetmiyorum.
Dilimlere benzer şekilde, dosya kopyalandığında, kopyası da aynı nesneye erişim sağlar. Bunu == işleci ile görebiliriz. Dosyalarla kullanıldığında == işleci, aynı işletim sistemi dosyasına erişim açısından eşit olup olmama bilgisini verir:
auto dosya2 = dosya; assert(dosya == dosya2); assert(&dosya != &dosya2);
Orada 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. Dilimlerde olduğu gibi:
(isimsiz File nesnesi) dosya dosya2
---+-------------------+--- ---+---+--- ---+---+---
| ... | | o | | o |
---+-------------------+--- ---+-|-+--- ---+-|-+---
▲ | |
| | |
+--------------------+------------+
Böyle iki farklı File sınıfı nesnesinin işletim sisteminde gerçekten de aynı dosyaya erişim sağladığını şöyle gösterebiliriz:
import std.stream; void main() { auto dosya1 = new File("iki_File_ile_erisilen_dosya", FileMode.Out); dosya1.writefln("dosya1 ile ilk satır"); auto dosya2 = dosya1; dosya2.writefln("dosya2 ile orta satır"); dosya1.writefln("dosya1 ile son satır"); }
dosya1 ve dosya2'nin asıl dosyaya erişim sağladıklarını dosyanın içeriğini komut satırında yazdırarak görebiliriz:
$ cat iki_File_ile_erisilen_dosya
dosya1 ile ilk satır
dosya2 ile orta satır
dosya1 ile son satır
Not: Ben Unix ortamlarında bulunan cat programını kullandım; siz dosyanın içeriğine herhangi başka bir yolla da bakabilirsiniz.
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");
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 dilim'e yapılan atama işlemi onun eriştirdiği elemanları değiştirmez; dilim'in başka elemanları göstermesini sağlar:
void main() { int[] dizi1 = [ 10, 11, 12, 13, 14 ]; int[] dizi2 = [ 20, 21, 22 ]; int[] dilim = dizi1[1 .. 3]; // 1 ve 2 indeksli elemanlara // eriştirir dilim[0] = 777; assert(dizi1 == [ 10, 777, 12, 13, 14 ]); // Bu atama işlemi dilim'in eriştirdiği elemanları // değiştirmez, dilim'in artık başka elemanlara // erişim sağlamasına neden olur dilim = dizi2[$ - 1 .. $]; // sonuncu elemana eriştirir dilim[0] = 888; assert(dizi2 == [ 20, 21, 888 ]); }
Atama işlecinin referans türlerindeki bu etkisini bir de File türünde görelim:
import std.stream; void main() { auto dosya1 = new File("deneme_dosyasi_1", FileMode.Out); auto dosya2 = new File("deneme_dosyasi_2", FileMode.Out); auto kopya = dosya1; kopya.writefln("birinci satır"); kopya = dosya2; kopya.writefln("ikinci satır"); }
Oradaki atama işlemi sonucunda kopya artık ikinci dosyaya erişim sağlar. Bunun etkisini dosyaların içeriklerine bakarak görebiliriz:
$ cat deneme_dosyasi_1 birinci satır $ cat deneme_dosyasi_2 ikinci satır
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 dosya, erişim sağladığı işletim sistemi dosyası henüz belli olmadan şöyle tanımlanabilir:
File dosya;
Böyle değişkenler null özel değerine eşittirler. Bu özel değeri ve is anahtar sözcüğünü bir sonraki derste anlatacağım.
Sabit uzunluklu diziler değer, dinamik diziler referans
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. Erişim sağladıkları bu elemanlar kendilerine ait değildir. 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 ]; // Kendi elemanlarına sahiptir 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.
Ö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
nullolabilirler
Yukarıda anlatılan değişik türlerin değerlerine ve 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ı dosyaya erişen File'lar (referans türü) true false
farklı dosyaya erişen File'lar (referans türü) false false
O tabloyu şu programla ürettim:
import std.stream; import std.cstream; int evrenselDeğişken = 9; void başlıkÇiz() { const dchar[] başlık = " Değişken Türü" " a == b &a == &b"; dout.writefln(); dout.writefln(başlık); foreach (i; 0 .. başlık.length) { dout.writef('='); } dout.writefln(); } void bilgiSatırı(const dchar[] başlık, bool değerEşitliği, bool adresEşitliği) { dout.writefln( "%47s%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[] dizi = [ 4 ]; foreach (i, ref eleman; dizi) { bilgiSatırı("ref değişkenli foreach", eleman == dizi[i], &eleman == &dizi[i]); } foreach (i, eleman; dizi) { bilgiSatırı("ref olmayan değişkenli foreach", eleman == dizi[i], &eleman == &dizi[i]); } outParametre(evrenselDeğişken); refParametre(evrenselDeğişken); inParametre(evrenselDeğişken); int[] uzunDizi = [ 5, 6, 7 ]; int[] dilim1 = uzunDizi; 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 dosya1 = new File("deneme_dosyasi_12", FileMode.Out); auto dosya2 = dosya1; bilgiSatırı( "aynı dosyaya erişen File'lar (referans türü)", dosya1 == dosya2, &dosya1 == &dosya2); auto dosya3 = new File("deneme_dosyasi_3", FileMode.Out); bilgiSatırı( "farklı dosyaya erişen File'lar (referans türü)", dosya1 == dosya3, &dosya1 == &dosya3); } void outParametre(out int parametre) { bilgiSatırı("out parametreli işlev", parametre == evrenselDeğişken, ¶metre == &evrenselDeğişken); } void refParametre(ref int parametre) { bilgiSatırı("ref parametreli işlev", parametre == evrenselDeğişken, ¶metre == &evrenselDeğişken); } void inParametre(in int parametre) { bilgiSatırı("in parametreli işlev", parametre == evrenselDeğişken, ¶metre == &evrenselDeğişken); }
Programda işlev parametrelerini karşılaştırmak için bir de evrensel değişken kullandım. Evrensel değişkenler işlevlerin dışında tanımlanırlar ve içinde tanımlandıkları kaynak dosyadaki (modüldeki) bütün kodlar tarafından erişilebilirler.
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları