Sınıflar
Sınıflar, kullanıcı türleri tanımlamaya yarayan bir başka olanaktır.
D'nin nesneye yönelik programlama olanakları sınıflar yoluyla gerçekleştirilir. Nesneye yönelik programlamayı üç temel kavram üzerinde düşünebiliriz:
- sarma: üyelere erişimin kısıtlanması (aslında yapılarda da bulunan bu olanağı, 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 derste 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 derslere bırakacağım.
Yapılarla karşılaştırılması
Sınıflar yapılara temelde çok benzerler. Bu yüzden daha önce şu derslerde yapılar üzerinde gördüğümüz hemen hemen her konu sınıflar için de geçerlidir:
- Yapılar
- Üye İşlevler
const refParametreler 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.
new ile kurulurlar
Sınıf değişkenlerinin kendileri değer taşımadıkları için, asıl nesne new anahtar sözcüğü ile oluşturulur. Aynı nedenden, null ve is dersinde 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ığını == işleciyle değil, is işleciyle denetlememiz gerekir:
import std.stream; // ... File erişimSağlayan = new File("mektup"); File dosya; // erişim sağlamayan assert(dosya is null);
Bunun nedeni; is yerine == kullanılması durumunda bir değişkenin null olmasının, programın bir bellek hatası ile sonlanmasına neden olacağıdır. O yüzden sınıf değişkenlerinin 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 dersinde gördüğümüz şu koda bakalım:
auto dosya1 = new File("oyuncu_listesi", FileMode.Out); auto dosya2 = dosya1;
İlk satırda sağ taraftaki new, isimsiz bir File nesnesi oluşturur. O nesne, diskte "oyuncu_listesi" isminde bir dosya açmak ve bu dosyaya yazmak gibi görevleri üstlenir. dosya1 ve dosya2 ise yalnızca bu isimsiz nesneye erişim sağlayan değişkenlerdir:
dosya1 dosya2
(isimsiz File nesnesi) değişkeni değişkeni
---+-------------------+--- ---+---+--- ---+---+---
| ... | | 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.
Bir nesne kopyalanmadığı için de, yapılarda kopya sonrası işlevi olarak öğrendiğimiz this(this) üye işlevi sınıflarda bulunmaz.
auto dosya2 = dosya1;
Yukarıdaki kodda dosya2, dosya1'in kopyası olarak oluşturulmaktadır. O işlem, her ikisinin de aynı File nesnesine erişim sağlamalarına neden olur.
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 dosya1 = new File("mektup"); auto dosya2 = new File("yemek_tarifi"); dosya1 = dosya2;
Yukarıdaki atama işlemi, dosya1'in "mektup" dosyasını bırakmasına ve "yemek_tarifi" dosyasına erişim sağlamasına neden olur. "mektup" dosyası ile ilgili olan asıl nesne çö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; } }
Eğer sınıf başka bir sınıftan türetilmişse, türetildiği üst sınıfın kurucusunu çağırmak bu sınıfın görevidir. Bunu bir sonraki derste super anahtar sözcüğünü tanıtırken göstereceğim.
Sonlandırma
Sonlandırıcı işlevin ismi, yapılarda olduğu gibi ~this'tir:
~this() { // ... nesne sonlanırken gereken işlemler ... }
Üye erişimi
Yapılarda olduğu gibi, üyelere nokta karakteri ile erişilir.
auto şah = new SatrançTaşı('♔'); dout.writefln(şah.şekil);
Her ne kadar değişkenin bir üyesine erişiliyor gibi yazılsa da; erişilen, asıl nesnenin üyesidir.
Not: Üye değişkenlere böyle doğrudan erişilmesi çoğu durumda doğru kabul edilmez. Onun yerine, daha sonra anlatacağım 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; ve bu anlam değiştirilemez.
Üye işlevler
Yapılardaki gibidir.
Bir fark, bazı işlevlerin Object sınıfından kalıtım yoluyla hazır olarak edinilmiş olmalarıdır. Örneğin toString işlevini kendimiz tanımlamamış olsak bile kullanabiliriz:
dout.writefln("%s", şah);
Hatırlarsanız, bir nesnenin "%s" düzeniyle yazdırılması, onun toString üye işlevini çağırıyordu. Yukarıdaki SatrançTaşı sınıfında böyle bir işlev tanımlı olmadığı halde, o satır derlenir ve bir çıktı üretir:
deneme.SatrançTaşı
Hemen hemen bütün durumlarda kullanışsız olan bu çıktının override sözcüğü ile nasıl değiştirildiğini bir sonraki derste göstereceğim.
is ve !is işleçleri
Sınıf değişkenleri üzerinde çalışır.
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 çalışır: 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 atama işlemi sonucunda iki değişken de aynı nesneye erişim sağlamaya başlarlar. is işleci bu durumda true üretir.
Problemler
- Bunun neden olduğunu açıklayın.
- Programda tek bir değişiklik yapın:
- Tekrar ilk programa dönün ve üç değişiklik yapın:
classanahtar sözcüğünüstructile değiştirinnewanahtar sözcüğünü silin (programınstructile derlenebilmesi için silinmesi gerekir)sayılar'ın son halini göstermek için programın en sonuna şu satırı ekleyin:
Aşağıdaki programda çok basit bir sınıf tanımlı. Program [0,19] aralığındaki sayıları sayılar dizisine, çift olanlarını da ayrıca çiftler dizisine yerleştiriyor:
import std.cstream; import std.random; class Sayı { int değer; this(int değer) { this.değer = değer; } /* Not: Normalde bu sınıfın toString işlevinin de tanımlanması gerekir. Onu 'override' anahtar sözcüğünü tanıttığım bir sonraki derste göstereceğim. Bu sınıfın toString işlevi tanımlanmadığı için 'değer' üyesine aşağıda doğrudan erişmek zorunda kalıyoruz. */ } void göster(string başlık, Sayı[] sayılar) { dout.writef("%-16s: ", başlık); foreach (eleman; sayılar) { dout.writef("%4s", eleman.değer); } dout.writefln(); } void main() { Sayı[] sayılar; Sayı[] çiftler; // [0,19] aralığında 20 sayı foreach (i; 0 .. 20) { auto sayı = new Sayı(i); sayılar ~= sayı; // Çift olanlarını ayrı bir diziye de yerleştiriyoruz if ((i % 2) == 0) { çiftler ~= sayı; } } göster("çiftler (önce)", çiftler); // 'sayılar' dizisinin elemanlarına rastgele değerler // ekliyoruz foreach (eleman; sayılar) { eleman.değer += uniform(0, 20); } göster("çiftler (sonra)", çiftler); }
Programda göster işlevinin çağrıldığı iki satır arasında çiftler dizisine hiç dokunulmadığı halde göster işlevi iki farklı çıktı üretiyor:
çiftler (önce) : 0 2 4 6 8 10 12 14 16 18 çiftler (sonra) : 4 14 11 13 23 17 19 26 18 26
eleman.değer += uniform(0, 20);
satırını şu satırla değiştirin:
eleman = new Sayı(uniform(0, 20));
çiftler dizisinin elemanlarının artık değişmediğini görün:
çiftler (önce) : 0 2 4 6 8 10 12 14 16 18 çiftler (sonra) : 0 2 4 6 8 10 12 14 16 18
Bunun da nedenini açıklayın.
göster("sayılar", sayılar);
Hem çiftler'in bu durumda da değişmediğini, hem de programdaki eleman.değer += uniform(0, 20); satırına rağmen sayılar dizisinin elemanlarının da rastgele değerleri olmadığını gözlemleyin:
çiftler (önce) : 0 2 4 6 8 10 12 14 16 18 çiftler (sonra) : 0 2 4 6 8 10 12 14 16 18 sayılar : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Bu durumu da açıklayın.
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları