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

alt düzey: [low level], donanıma yakın olanak
bayt sırası: [endianness], veriyi oluşturan baytların bellekte sıralanma düzeni
birlik: [union], birden fazla değişkeni aynı bellek bölgesinde depolayan veri yapısı
büyük soncul: [big endian], değerin üst bitlerini oluşturan baytın bellekte önceki adreste bulunduğu işlemci mimarisi
korumalı birlik: [discriminated union], yalnızca geçerli üyesine eriştiren birlik
küçük soncul: [little endian], değerin alt bitlerini oluşturan baytın bellekte önceki adreste bulunduğu işlemci mimarisi
veri yapıları: [data structures], verilerin bilgisayar biliminin tanımladığı biçimde saklanmaları ve işlenmeleri
yapı: [struct], başka verileri bir araya getiren veri yapısı
... bütün sözlük



İngilizce Kaynaklar


Diğer




Birlikler

Birlikler, birden fazla üyenin aynı bellek alanını paylaşmalarını sağlarlar. D'ye C dilinden geçmiş olan alt düzey bir olanaktır.

İki fark dışında yapılarla aynı şekilde kullanılır:

Yapılar gibi, birliklerin de üye işlevleri bulunabilir.

Aşağıdaki örnek programlar derlendikleri ortamın 32 bit veya 64 bit olmasına bağlı olarak farklı sonuçlar üreteceklerdir. Bu yüzden, bu bölümdeki programları derlerken -m32 derleyici seçeneğini kullanmanızı öneririm. Aksi taktirde sizin sonuçlarınız aşağıda gösterilenlerden farklı olabilir.

Şimdiye kadar çok karşılaştığımız yapı türlerinin kullandıkları bellek alanı bütün üyelerini barındıracak kadar büyüktü:

struct Yapı {
    int i;
    double d;
}

// ...

    writeln(Yapı.sizeof);

Dört baytlık int'ten ve sekiz baytlık double'dan oluşan o yapının büyüklüğü 12'dir:

12

Aynı şekilde tanımlanan bir birliğin büyüklüğü ise, üyeleri aynı bellek bölgesini paylaştıkları için, üyelerden en büyüğü için gereken yer kadardır:

union Birlik {
    int i;
    double d;
}

// ...

    writeln(Birlik.sizeof);

Dört baytlık int ve sekiz baytlık double aynı alanı paylaştıkları için bu birliğin büyüklüğü en büyük üye için gereken yer kadardır:

8

Bunun bellek kazancı sağlayan bir olanak olduğunu düşünmeyin. Aynı bellek alanına birden fazla veri sığdırmak olanaksızdır. Birliklerin yararı, aynı bölgenin farklı zamanlarda farklı türden veriler için kullanılabilmesidir. Belirli bir anda ancak tek üyenin değerine güvenilebilir. Buna rağmen, her ortamda aynı şekilde çalışmasa da, birliklerin yararlarından birisi, geçerli olan verinin parçalarına diğer üyeler yoluyla erişilebilmesidir.

Aşağıdaki örneklerden birisi, geçerli üye dışındakilere erişimin typeid'den yararlanılarak nasıl engellenebileceğini göstermektedir.

Yukarıdaki birliği oluşturan sekiz baytın bellekte nasıl durduklarını ve üyeler için nasıl kullanıldıklarını şöyle gösterebiliriz:

       0      1      2      3      4      5      6      7
───┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬───
   │<───  int için 4 bayt  ───>                            │
   │<───────────────  double için 8 bayt  ────────────────>│
───┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┴───

Ya sekiz baytın hepsi birden double üye için kullanılır, ya da ilk dört bayt int üye için kullanılır ve gerisine dokunulmaz.

Ben örnek olarak iki üye kullandım; birlikleri istediğiniz kadar üye ile tanımlayabilirsiniz. Üyelerin hepsi aynı alanı paylaşırlar.

Aynı bellek bölgesinin kullanılıyor olması ilginç sonuçlar doğurabilir. Örneğin, birliğin bir int ile ilklenmesi ama bir double olarak kullanılması, baştan kestirilemeyecek double değerleri verebilir:

    auto birlik = Birlik(42);    // int üyenin ilklenmesi
    writeln(birlik.d);           // double üyenin kullanılması

int üyeyi oluşturan dört baytın 42 değerini taşıyacak şekilde kurulmaları, double üyenin değerini de etkiler:

2.07508e-322

Mikro işlemcinin bayt sıralarına bağlı olarak int üyeyi oluşturan dört bayt bellekte 0|0|0|42, 42|0|0|0, veya daha başka bir düzende bulunabilir. Bu yüzden yukarıdaki double üyenin değeri başka ortamlarda daha farklı da olabilir.

İsimsiz birlikler

İsimsiz birlikler, içinde bulundukları bir yapının hangi üyelerinin paylaşımlı olarak kullanıldıklarını belirlerler:

struct BirYapı {
    int birinci;

    union {
        int ikinci;
        int üçüncü;
    }
}

// ...

    writeln(BirYapı.sizeof);

Yukarıdaki yapının son iki üyesi aynı alanı paylaşırlar ve bu yüzden yapı, toplam iki int'in büyüklüğü kadar yer tutar. Birlik üyesi olmayan birinci için gereken 4 bayt, ve ikinci ile üçüncü'nün paylaştıkları 4 bayt:

8
Başka bir türün baytlarını ayrıştırmak

Birlikler, türleri oluşturan baytlara teker teker erişmek için kullanılabilirler. Örneğin aslında 32 bitten oluşan IPv4 adreslerinin 4 bölümünü elde etmek için bu 32 biti paylaşan 4 baytlık bir dizi kullanılabilir. Adres değerini oluşturan üye ve dört bayt bir birlik olarak şöyle bir araya getirilebilir:

union IpAdresi {
    uint değer;
    ubyte[4] baytlar;
}

O birliği oluşturan iki üye, aynı belleği şu şekilde paylaşırlar:

          0            1            2            3
───┬────────────┬────────────┬────────────┬────────────┬───
   │<───────  IPv4 adresini oluşturan 32 bit  ────────>│
   │ baytlar[0] │ baytlar[1] │ baytlar[2] │ baytlar[3] │
───┴────────────┴────────────┴────────────┴────────────┴───

Bu birlik, daha önceki bölümlerde 192.168.1.2 adresinin değeri olarak karşılaştığımız 0xc0a80102 ile ilklendiğinde, baytlar dizisinin elemanları teker teker adresin dört bölümüne karşılık gelirler:

void main() {
    auto adres = IpAdresi(0xc0a80102);
    writeln(adres.baytlar);
}

Adresin bölümleri, bu programı denediğim ortamda alışık olunduğundan ters sırada çıkmaktadır:

[2, 1, 168, 192]

Bu, programı çalıştıran mikro işlemcinin küçük soncul olduğunu gösterir. Başka ortamlarda başka sırada da çıkabilir.

Bu örnekte özellikle belirtmek istediğim, birlik üyelerinin değerlerinin belirsiz olabilecekleridir. Birlikler, ancak ve ancak tek bir üyeleri ile kullanıldıklarında beklendiği gibi çalışırlar. Hangi üyesi ile kurulmuşsa, birlik nesnesinin yaşamı boyunca o üyesi ile kullanılması gerekir. O üye dışındaki üyelere erişildiğinde ne tür değerlerle karşılaşılacağı ortamdan ortama farklılık gösterebilir.

Bu bölümle ilgisi olmasa da, core.bitop modülünün bswap işlevinin bu konuda yararlı olabileceğini belirtmek istiyorum. bswap, kendisine verilen uint'in baytları ters sırada olanını döndürür. std.system modülündeki endian değerinden de yararlanırsak, küçük soncul bir ortamda olduğumuzu şöyle belirleyebilir ve yukarıdaki IPv4 adresini oluşturan baytları tersine çevirebiliriz:

import std.system;
import core.bitop;

// ...

    if (endian == Endian.littleEndian) {
        adres.değer = bswap(adres.değer);
    }

Endian.littleEndian değeri sistemin küçük soncul olduğunu, Endian.BigEndian değeri de büyük soncul olduğunu belirtir. Yukarıdaki dönüşüm sonucunda IPv4 adresinin bölümleri alışık olunan sırada çıkacaktır:

[192, 168, 1, 2]

Bunu yalnızca birliklerle ilgili bir kullanım örneği olarak gösterdim. Normalde IPv4 adresleriyle böyle doğrudan ilgilenmek yerine, o iş için kullanılan bir kütüphanenin olanaklarından yararlanmak daha doğru olur.

Örnekler
Haberleşme protokolü

Bazı protokollerde, örneğin ağ protokollerinde, bazı baytların anlamı başka bir üye tarafından belirleniyor olabilir. Ağ pakedinin daha sonraki bir bölümü, o üyenin değerine göre farklı bir şekilde kullanılıyor olabilir:

struct Adres {
    // ...
}

struct BirProtokol {
    // ...
}

struct BaşkaProtokol {
    // ...
}

enum ProtokolTürü { birTür, başkaTür }

struct AğPakedi {
    Adres hedef;
    Adres kaynak;
    ProtokolTürü tür;

    union {
        BirProtokol birProtokol;
        BaşkaProtokol başkaProtokol;
    }

    ubyte[] geriKalanı;
}

Yukarıdaki AğPakedi yapısında hangi protokol üyesinin geçerli olduğu tür'ün değerinden anlaşılabilir, programın geri kalanı da yapıyı o değere göre kullanır.

Korumalı birlik

Korumalı birlik, union kullanımını güvenli hale getiren bir veri yapısıdır. union'ın aksine, yalnızca belirli bir anda geçerli olan üyeye erişilmesine izin verir.

Aşağıdaki, yalnızca int ve double türlerini kullanan basit bir korumalı birlik örneğidir. Veri saklamak için kullandığı union üyesine ek olarak bir de o birliğin hangi üyesinin geçerli olduğunu bildiren bir TypeInfo üyesi vardır.

import std.stdio;
import std.exception;

struct Korumalı {
private:

    TypeInfo geçerliTür_;

    union {
        int i_;
        double d_;
    }

public:

    this(int değer) {
        // Bu atama, aşağıdaki nitelik işlevini çağırır
        i = değer;
    }

    // 'int' üyeyi değiştirir
    void i(int değer) {
        i_ = değer;
        geçerliTür_ = typeid(int);
    }

    // 'int' veriyi döndürür
    int i() const {
        enforce(geçerliTür_ == typeid(int),
                "Veri 'int' değil.");
        return i_;
    }

    this(double değer) {
        // Bu atama, aşağıdaki nitelik işlevini çağırır
        d = değer;
    }

    // 'double' üyeyi değiştirir
    void d(double değer) {
        d_ = değer;
        geçerliTür_ = typeid(double);
    }

    // 'double' veriyi döndürür
    double d() const {
        enforce(geçerliTür_ == typeid(double),
                "Veri 'double' değil." );
        return d_;
    }

    // Geçerli verinin türünü bildirir
    const(TypeInfo) tür() const {
        return geçerliTür_;
    }
}

unittest {
    // 'int' veriyle başlayalım
    auto k = Korumalı(42);

    // Geçerli tür 'int' olarak bildirilmelidir
    assert(k.tür == typeid(int));

    // 'int' veri okunabilmelidir
    assert(k.i == 42);

    // 'double' veri okunamamalıdır
    assertThrown(k.d);

    // 'int' yerine 'double' veri kullanalım
    k.d = 1.5;

    // Geçerli tür 'double' olarak bildirilmelidir
    assert(k.tür == typeid(double));

    // Bu sefer 'double' veri okunabilmelidir ...
    assert(k.d == 1.5);

    // ... ve 'int' veri okunamamalıdır
    assertThrown(k.i);
}

Bunu basit bir örnek olarak kabul edin. Kendi programlarınızda std.variant modülünde tanımlı olan Algebraic ve Variant türlerini kullanmanızı öneririm. Ek olarak, bu örnek şablonlar ve katmalar gibi diğer D olanaklarından yararlanabilir ve en azından kod tekrarının önüne geçebilirdi.

Dikkat ederseniz, içinde tuttuğu verinin türünden bağımsız olarak Korumalı diye tek tür bulunmaktadır. (Öte yandan, şablon kullanan bir gerçekleştirme verinin türünü bir şablon parametresi olarak alabilir ve bunun sonucunda şablonun her farklı parametre değeri için kullanımının farklı bir tür olmasına neden olabilirdi.) Korumalı, bunun sayesinde dizi elemanı türü olarak kullanılabilir ve bunun sonucunda da farklı türden verilerin aynı dizide bir araya getirilmeleri sağlanmış olur. Ancak, kullanıcılar yine de veriye erişmeden önce hangi verinin geçerli olduğundan emin olmak zorundadırlar. Örneğin, aşağıdaki işlev bunun için Korumalı türünün tür niteliğinden yararlanmaktadır:

void main() {
    Korumalı[] dizi = [ Korumalı(1), Korumalı(2.5) ];

    foreach (değer; dizi) {
        if (değer.tür == typeid(int)) {
            writeln("'int' veri kullanıyoruz   : ", değer.i);

        } else if (değer.tür == typeid(double))  {
            writeln("'double' veri kullanıyoruz: ", değer.d);

        } else {
            assert(0);
        }
    }
}
'int' veri kullanıyoruz   : 1
'double' veri kullanıyoruz: 2.5