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:
struct
yerineunion
anahtar sözcüğü ile tanımlanır- üyeleri aynı bellek alanını paylaşırlar; birbirlerinden bağımsız değillerdir
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