D.ershane D Programlama Dili Dersleri

çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
değer: [value], ay adedi 12 gibi isimsiz bir büyüklük
değer türü: [value type], değer taşıyan tür
dilim: [slice], başka bir dizinin bir bölümüne erişim sağlayan yapı
eşleme tablosu: [associative array], elemanlarına tamsayı olmayan indekslerle de erişilebilen veri yapısı (bir 'hash table' gerçekleştirmesi)
evrensel: [global], evrensel isim alanında tanımlanmış, erişimi kısıtlanmamış
nesne: [object], belirli bir sınıf veya yapı türünden olan değişken
referans: [reference], asıl nesneye, onun takma ismi gibi erişim sağlayan program yapısı
referans türü: [reference type], başka bir nesneye erişim sağlayan tür
sınıf: [class], kendi üzerinde kullanılan işlevleri de tanımlayan veri yapısı
yapı: [struct], başka verileri bir araya getiren veri yapısı
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



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 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, 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:

    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 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;

   writeln("hız    : ", hız,     " adresi: ", &hız);
   writeln("yeniHız: ", yeniHız, " adresi: ", &yeniHı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:

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:

Ö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.

D'nin güçlü bir olanağı olan sınıfları daha ilerideki derslerde 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. Bunu == işleci ile görebiliriz. Sınıflarla kullanıldığında == işleci, aynı nesneye erişim açısından eşit olup olmama bilgisini verir:

    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. Dilimlerde olduğu gibi:

  (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");
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 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ü 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. 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.

Özet

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ı 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 tabloyu şu programla ürettim:

import std.stdio;
import std.conv;

int evrenselDeğişken = 9;

class BirSınıf
{
    int üye;
}

void başlıkÇiz()
{
    const dchar[] başlık =
        "                    Değişken Türü"
        "                    a == b  &a == &b";

    writeln();
    writeln(başlık);

    foreach (i; 0 .. başlık.length) {
        write('=');
    }

    writeln();
}

void bilgiSatırı(const dchar[] başlık,
                 bool değerEşitliği,
                 bool adresEşitliği)
{
    writefln("%50s%9s%9s",
             başlık,
             to!string(değerEşitliği),
             to!string(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 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şken1,
        &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 == evrenselDeğişken,
                &parametre == &evrenselDeğişken);
}

void refParametre(ref int parametre)
{
    bilgiSatırı("ref parametreli işlev",
                parametre == evrenselDeğişken,
                &parametre == &evrenselDeğişken);
}

void inParametre(in int parametre)
{
    bilgiSatırı("in parametreli işlev",
                parametre == evrenselDeğişken,
                &parametre == &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.