Sınıflar
Sınıflar kullanıcı türü tanımlamaya yarayan başka bir olanaktır. D'nin nesne yönelimli programlama olanakları sınıflar yoluyla gerçekleştirilir. Nesne yönelimli programlamayı üç temel kavram üzerinde düşünebiliriz:
- Sarma: Üyelere erişimin kısıtlanması (Not: Aslında yapılarda da bulunan bu olanağı genelde yapıların kullanım amaçlarının dışında kaldığı için göstermedim.)
- Kalıtım: Başka bir türün üyelerini ve üye işlevlerini kendisininmiş gibi edinmek
- Çok şekillilik: Birbirlerine yakın türlerin daha genel ortak bir tür gibi kullanılabilmeleri
Sarma, daha sonra göreceğimiz erişim hakları ile sağlanır. Kalıtım, gerçekleştirme türemesidir. Çok şekillilik ise arayüz türemesi yoluyla gerçekleştirilir.
Bu bölümde sınıfları genel olarak tanıtacağım ve özellikle referans türü olduklarına dikkat çekeceğim. Sınıfların diğer olanaklarını daha sonraki bölümlere bırakacağım.
Yapılarla karşılaştırılması
Sınıflar yapılara temelde çok benzerler. Bu yüzden daha önce şu bölümlerde yapılar üzerinde gördüğümüz hemen hemen her konu sınıflar için de geçerlidir:
- Yapılar
- Üye İşlevler
const ref
Parametreler veconst
Üye İşlevler- Kurucu ve Diğer Özel İşlevler
- İşleç Yükleme
Sınıfları yapılardan ayıran önemli farklar da vardır. Bu farkları aşağıdaki bölümlerde anlatıyorum.
Referans türleridir
Sınıfların yapılardan farklı olmalarının en büyük nedeni, yapıların değer türü olmalarına karşın sınıfların referans türü olmalarıdır. Aşağıdaki farklılıkların büyük bir çoğunluğu, sınıfların bu özelliğinden kaynaklanır.
Sınıf değişkenleri null
olabilirler
Sınıf değişkenlerinin kendileri değer taşımadıklarından, asıl nesne new
anahtar sözcüğü ile oluşturulur. Aynı nedenden, null
ve is
bölümünde de gösterildiği gibi, sınıf değişkenleri null
da olabilirler. Yani, "hiçbir nesneye erişim sağlamıyor" olabilirler.
Hatırlayacağınız gibi, bir değişkenin null
olup olmadığı ==
ve !=
işleçleriyle değil, duruma göre is
ve !is
işleçleriyle denetlenir:
BirSınıf erişimSağlayan = new BirSınıf; assert(erişimSağlayan !is null); BirSınıf değişken; // erişim sağlamayan assert(değişken is null);
Bunun nedeni ==
işlecinin nesnenin üyelerini de kullanmasının gerekebileceğidir. O üye erişimi, değişkenin null
olduğu durumda programın bir bellek hatası ile sonlanmasına neden olur. O yüzden sınıf değişkenlerinin is
veya !is
ile karşılaştırılmaları gerekir.
Sınıf nesneleri ve değişkenleri
Sınıf nesnesi ile sınıf değişkeni farklı kavramlardır.
Sınıf nesnesi, new
anahtar sözcüğü ile oluşturulan ve kendi ismi olmayan bir program yapısıdır. Temsil ettiği kavramı gerçekleştiren, onun işlemlerini yapan, ve o türün davranışını belirleyen hep bu sınıf nesnesidir. Sınıf nesnelerine doğrudan erişemeyiz.
Sınıf değişkeni ise sınıf nesnesine erişim sağlayan bir program yapısıdır. Kendisi iş yapmasa da eriştirdiği nesnenin aracısı gibi işlem görür.
Daha önce Değerler ve Referanslar bölümünde gördüğümüz şu koda bakalım:
auto değişken1 = new BirSınıf; auto değişken2 = değişken1;
İlk satırda sağ taraftaki new
, isimsiz bir BirSınıf
nesnesi oluşturur. değişken1
ve değişken2
ise yalnızca bu isimsiz nesneye erişim sağlayan değişkenlerdir:
(isimsiz BirSınıf nesnesi) değişken1 değişken2 ───┬───────────────────┬─── ───┬───┬─── ───┬───┬─── │ ... │ │ o │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
Kopyalama
Değişkenleri etkiler.
Referans türü oldukları için; sınıf değişkenlerinin kopyalanarak oluşturulmaları, onların hangi nesneye erişim sağlayacaklarını belirler. Bu işlem sırasında asıl nesne kopyalanmaz.
Yapılarda kopya sonrası işlevi olarak öğrendiğimiz this(this)
üye işlevi sınıflarda bulunmaz.
auto değişken2 = değişken1;
Yukarıdaki kodda değişken2
, değişken1
'in kopyası olarak oluşturulmaktadır. O işlem her ikisinin de aynı nesneye erişim sağlamalarına neden olur.
Sınıf nesnelerinin kopyalanmaları gerektiğinde bunu sağlayan bir üye işlev tanımlanmalıdır. Bu işleve kopyala()
gibi bir isim verebileceğiniz gibi, dizilere benzemesi açısından dup()
isminin daha uygun olduğunu düşünebilirsiniz. Bu işlev yeni bir nesne oluşturmalı ve ona erişim sağlayan bir değişken döndürmelidir:
class Sınıf { Yapı yapıNesnesi; char[] dizgi; int tamsayı; // ... this(Yapı yapıNesnesi, const char[] dizgi, int tamsayı) { this.yapıNesnesi = yapıNesnesi; this.dizgi = dizgi.dup; this.tamsayı = tamsayı; } Sınıf dup() const { return new Sınıf(yapıNesnesi, dizgi, tamsayı); } }
dup()
içinde oluşturulan yeni nesne için yalnızca Sınıf
'ın kurucusundan yararlanıldığına dikkat edin. Kurucu dizgi
üyesini dup()
ile açıkça kopyalıyor. yapıNesnesi
ve tamsayı
üyeleri ise değer türleri olduklarından onlar zaten otomatik olarak kopyalanırlar.
O işlevden örneğin şöyle yararlanılabilir:
auto nesne1 = new Sınıf(Yapı(1.5), "merhaba", 42); auto nesne2 = nesne1.dup();
Sonuçta, nesne2
nesne1
'in hiçbir üyesini paylaşmayan ayrı bir nesnedir.
Benzer biçimde, nesnenin immutable
bir kopyası da ismi idup
olan bir işlev tarafından sağlanabilir. Ancak, bu örnekteki kurucu işlevin de pure
olarak tanımlanması gerekir. pure
anahtar sözcüğünü ilerideki bir bölümde göreceğiz.
class Sınıf { // ... this(Yapı yapıNesnesi, const char[] dizgi, int tamsayı) pure { // ... } immutable(Sınıf) idup() const { return new immutable(Sınıf)(yapıNesnesi, dizgi, tamsayı); } } // ... immutable(Sınıf) imm = nesne1.idup();
Atama
Değişkenleri etkiler.
Referans türü oldukları için; sınıf değişkenlerinin atanmaları, daha önce erişim sağladıkları nesneyi bırakmalarına ve yeni bir nesneye erişim sağlamalarına neden olur.
Eğer bırakılan nesneye erişim sağlayan başka değişken yoksa, asıl nesne ilerideki belirsiz bir zamanda çöp toplayıcı tarafından sonlandırılacak demektir.
auto değişken1 = new BirSınıf; auto değişken2 = new BirSınıf; değişken1 = değişken2;
Yukarıdaki atama işlemi, değişken1
'in kendi nesnesini bırakmasına ve değişken2
'nin nesnesine erişim sağlamaya başlamasına neden olur. Kendisine erişim sağlayan başka bir değişken olmadığı için bırakılan nesne daha sonra çöp toplayıcı tarafından sonlandırılacaktır.
Atama işleminin davranışı sınıflar için değiştirilemez; yani opAssign
sınıflarda yüklenemez.
Tanımlama
struct
yerine class
anahtar sözcüğü kullanılır:
class SatrançTaşı { // ... }
Kurma
Kurucu işlevin ismi, yapılarda olduğu gibi this
'tir. Yapılardan farklı olarak sınıf nesneleri {}
karakterleri ile kurulamaz.
class SatrançTaşı { dchar şekil; this(dchar şekil) { this.şekil = şekil; } }
Yapıların aksine, sınıf üyeleri kurucu parametre değerlerinden sırayla otomatik olarak kurulamazlar:
class SatrançTaşı { dchar şekil; size_t değer; } void main() { auto şah = new SatrançTaşı('♔', 100); // ← derleme HATASI }
Error: no constructor for SatrançTaşı
Nesnelerin o yazımla kurulabilmeleri için programcının açıkça bir kurucu tanımlamış olması şarttır.
Sonlandırma
Sonlandırıcı işlevin ismi yapılarda olduğu gibi ~this
'tir:
~this() { // ... }
Ancak, yapılardan farklı olarak, sınıfların sonlandırıcıları nesnenin yaşamı sona erdiği an işletilmez. Yukarıda da değinildiği gibi, sonlandırıcı ilerideki belirsiz bir zamandaki bir çöp toplama işlemi sırasında işletilir.
Daha sonra Bellek Yönetimi bölümünde de göreceğimiz gibi, sınıf sonlandırıcılarının aşağıdaki kurallara uymaları şarttır:
- Sınıf sonlandırıcısındaki kodlar, yaşamı çöp toplayıcı tarafından yönetilen hiçbir üyeye erişmemelidir. Bunun nedeni, çöp toplayıcının nesneyi veya üyelerini hangi sırada sonlandıracağı garantisini vermek zorunda olmamasıdır. Sonlandırıcı işletilmeye başladığında bütün üyeler zaten sonlandırılmış olabilirler.
- Sınıf sonlandırıcısı çöp toplayıcıdan yeni bellek ayırmamalıdır. Bunun nedeni, çöp toplayıcının temizlik işlemleri sırasında yeni bellek ayırabilme garantisini vermek zorunda olmamasıdır.
Bu kurallara uymamak tanımsız davranıştır. Tanımsız davranışın bir etkisini sonlandırıcı içinde yeni bir sınıf nesnesi kurmaya çalışarak görebiliriz:
class C { ~this() { auto c = new C(); // ← YANLIŞ: Sınıf sonlandırıcısında // yeni nesne kuruluyor } } void main() { auto c = new C(); }
Program bir hata ile sonlanır:
core.exception.InvalidMemoryOperationError@(0)
Sonlandırıcı içinde çöp toplayıcıdan dolaylı olarak bellek ayırmak da aynı derecede yanlıştır. Örneğin, dinamik dizi elemanları için kullanılan bellek bölgesi de çöp toplayıcı tarafından yönetilir. Bu yüzden, bir dinamik dizinin yeni bellek ayrılmasını gerektirecek herhangi biçimde kullanılması da tanımsız davranıştır:
~this() { auto dizi = [ 1 ]; // ← YANLIŞ: Sınıf sonlandırıcısında // çöp toplayıcıdan // dolaylı olarak bellek // ayrılıyor }
core.exception.InvalidMemoryOperationError@(0)
Üye erişimi
Yapılarda olduğu gibi, üyelere nokta karakteri ile erişilir.
auto şah = new SatrançTaşı('♔'); writeln(şah.şekil);
Her ne kadar değişkenin bir üyesine erişiliyor gibi yazılsa da, erişilen asıl nesnenin üyesidir. Sınıf değişkenlerinin üyeleri yoktur, sınıf nesnelerinin üyeleri vardır. Bir başka deyişle, şah
'ın şekil
diye bir üyesi yoktur, isimsiz sınıf nesnesinin şekil
diye bir üyesi vardır.
Not: Üye değişkenlere böyle doğrudan erişilmesi çoğu durumda doğru kabul edilmez. Onun yerine daha sonra Nitelikler bölümünde göreceğimiz sınıf niteliklerinden yararlanmak daha uygundur.
İşleç yükleme
Yapılardaki gibidir.
Bir fark, opAssign
'ın sınıflar için özel olarak tanımlanamamasıdır. Yukarıda atama başlığında anlatıldığı gibi, sınıflarda atama işleminin anlamı yeni nesneye erişim sağlamaktır; bu anlam değiştirilemez.
Üye işlevler
Sınıf üye işlevleri yapılarda olduğu gibi tanımlanırlar ve kullanılırlar. Buna rağmen, aralarında önemli bir fark vardır: Sınıf üye işlevleri yeniden tanımlanabilirler ve bu, onlar için varsayılan durumdur. Yeniden tanımlama kavramını daha sonra Türeme bölümünde göreceğiz.
Yeniden tanımlama düzeneğinin program hızına kötü bir etkisi olduğundan, burada daha fazla ayrıntısına girmeden bütün sınıf üye işlevlerini final
olarak tanımlamanızı öneririm. Bu ilkeyi derleyici hatası almadığınız sürece bütün sınıf üyeleri için uygulayabilirsiniz:
class Sınıf { final int işlev() { // ← Önerilir // ... } }
Yapılardan başka bir fark, bazı işlevlerin Object
sınıfından kalıtım yoluyla hazır olarak edinilmiş olmalarıdır. Bunlar arasından toString
işlevinin override
anahtar sözcüğü ile nasıl tanımlandığını bir sonraki bölümde göreceğiz.
is
ve !is
işleçleri
Sınıf değişkenleri üzerinde işler.
is
işleci, sınıf değişkenlerinin aynı nesneye erişim sağlayıp sağlamadıklarını bildirir. İki değişken de aynı nesneye erişim sağlıyorlarsa true
, değilse false
değerini üretir. !is
işleci de bunun tersi olarak işler: Aynı nesneye erişim sağlıyorlarsa false
, değilse true
değerini üretir.
auto benimŞah = new SatrançTaşı('♔'); auto seninŞah = new SatrançTaşı('♔'); assert(benimŞah !is seninŞah);
Yukarıdaki koddaki benimŞah
ve seninŞah
değişkenleri new
ile oluşturulmuş olan iki farklı nesneye erişim sağladıkları için !is
'in sonucu true
'dur. Bu iki nesnenin aynı şekilde kurulmuş olmaları, yani ikisinin şekil
üyelerinin de '♔'
olması bunu değiştirmez; nesneler birbirlerinden ayrı iki nesnedir.
İki değişkenin aynı nesneye erişim sağladıkları durumda ise is
işleci true
üretir:
auto benimŞah2 = benimŞah; assert(benimŞah2 is benimŞah);
Yukarıdaki iki değişken de aynı nesneye erişim sağlamaya başlarlar. is
işleci bu durumda true
üretir.
Özet
- Sınıfların yapılarla çok sayıda ortak yanları olduğu kadar büyük farkları da vardır.
- Sınıflar referans türleridir;
new
ile isimsiz bir sınıf nesnesi kurulur; döndürülen, o nesneye erişim sağlayan bir sınıf değişkenidir. - Hiçbir nesneye erişim sağlamayan sınıf değişkenlerinin değeri
null
'dır; bu durumis
veya!is
ile denetlenir (==
veya!=
ile değil). - Kopyalama normalde değişkeni kopyalar; nesnenin kopyalanabilmesi için
dup()
gibi bir üye işlev yazılması gerekir. - Atama, değişkenin başka bir nesneyi göstermesini sağlar; bu davranış değiştirilemez.