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

adres: [address], değişkenin (veya nesnenin) bellekte bulunduğu yer
atama: [assign], değişkene yeni bir değer vermek
çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
değer türü: [value type], değer taşıyan tür
değişken: [variable], kavramları temsil eden veya sınıf nesnesine erişim sağlayan program yapısı
kopyalama: [copy construct], nesneyi başka bir nesnenin kopyası olarak kurmak
kurma: [construct], yapı veya sınıf nesnesini kullanılabilir duruma getirmek
nesne: [object], belirli bir sınıf veya yapı türünden olan değişken
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ı
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
varsayılan: [default], özellikle belirtilmediğinde kullanılan
yapı: [struct], başka verileri bir araya getiren veri yapısı
yaşam süreci: [object lifetime], bir değişkenin veya nesnenin tanımlanmasından işinin bitmesine kadar geçen süre
... bütün sözlük



İngilizce Kaynaklar


Diğer




Yaşam Süreçleri ve Temel İşlemler

Çok yakında yapı ve sınıfları anlatmaya başlayacağım. Yapıların kullanıcı türlerinin temeli olduklarını göreceğiz. Onlar sayesinde temel türleri ve başka yapıları bir araya getirerek yeni türler oluşturabileceğiz.

Daha sonra D'nin nesneye dayalı programlama olanaklarının temelini oluşturan sınıfları tanıyacağız. Sınıflar başka türleri bir araya getirmenin yanında o türlerle ilgili özel işlemleri de belirlememizi sağlayacaklar.

O konulara geçmeden önce şimdiye kadar hiç üzerinde durmadan kullandığımız bazı temel kavramları ve temel işlemleri açıklamam gerekiyor. Bu kavramlar ileride yapı ve sınıf tasarımları sırasında yararlı olacak.

Şimdiye kadar kavramları temsil eden veri yapılarına değişken adını verdik. Bir kaç noktada da yapı ve sınıf türünden olan değişkenlere özel olarak nesne dedik. Ben bu bölümde bunların hepsine birden genel olarak değişken diyeceğim. Herhangi bir türden olan herhangi bir veri yapısı en azından bu bölümde değişken adını alacak.

Bu bölümde yalnızca şimdiye kadar gördüğümüz temel türleri, dizileri, ve eşleme tablolarını kullanacağım; siz bu kavramların bütün türler için geçerli olduklarını aklınızda tutun.

Değişkenlerin yaşam süreçleri

Bir değişkenin tanımlanması ile başlayan ve geçerliliğinin bitmesine kadar geçen süreye o değişkenin yaşam süreci denir.

Geçerliliğin bitmesi kavramını İsim Alanı bölümünde değişkenin tanımlandığı kapsamdan çıkılması olarak tanımlamıştım.

O konuyu hatırlamak için şu örneğe bakalım:

void hızDenemesi() {
    int hız;                      // tek değişken ...

    foreach (i; 0 .. 10) {
        hız = 100 + i;            // ... 10 farklı değer alır
        // ...
    }
} // ← yaşamı burada sonlanır

O koddaki hız değişkeninin yaşam süreci hızDenemesi işlevinden çıkıldığında sona erer. Orada 100 ile 109 arasında 10 değişik değer alan tek değişken vardır.

Aşağıdaki kodda ise durum yaşam süreçleri açısından çok farklıdır:

void hızDenemesi() {
    foreach (i; 0 .. 10) {
        int hız = 100 + i;        // 10 farklı değişken vardır
        // ...
    } // ← yaşamları burada sonlanır
}

O kodda her birisi tek değer alan 10 farklı değişken vardır: döngünün her tekrarında hız isminde yeni bir değişken yaşamaya başlar; yaşamı, döngünün kapama parantezinde sona erer.

Parametrelerin yaşam süreçleri

İşlev Parametreleri bölümünde gördüğümüz parametre türlerine bir de yaşam süreçleri açısından bakalım:

ref: Parametre aslında işlev çağrıldığında kullanılan değişkenin takma ismidir. Parametrenin asıl değişkenin yaşam süreci üzerinde etkisi yoktur.

in: Değer türündeki bir parametrenin yaşamı işleve girildiği an başlar ve işlevden çıkıldığı an sona erer. Referans türündeki bir parametrenin yaşamı ise ref'te olduğu gibidir.

out: Parametre aslında işlev çağrıldığında kullanılan değişkenin takma ismidir. ref'ten farklı olarak, işleve girildiğinde asıl değişkene önce otomatik olarak türünün .init değeri atanır. Bu değer daha sonra işlev içinde değiştirilebilir.

lazy: Parametre tembel olarak işletildiğinden yaşamı kullanıldığı an başlar ve o an sona erer.

Bu dört parametre türünü kullanan ve yaşam süreçlerini açıklayan bir örnek şöyle yazılabilir:

void main() {
    int main_in;      // değeri işleve kopyalanır

    int main_ref;     // işleve kendisi olarak ve kendi
                      // değeriyle gönderilir

    int main_out;     // işleve kendisi olarak gönderilir;
                      // işleve girildiği an değeri sıfırlanır

    işlev(main_in, main_ref, main_out, birHesap());
}

void işlev(
    in int p_in,       // yaşamı main_in'in kopyası olarak
                       // işleve girilirken başlar ve işlevden
                       // çıkılırken sonlanır

    ref int p_ref,     // main_ref'in takma ismidir

    out int p_out,     // main_out'un takma ismidir; ref'ten
                       // farklı olarak, işleve girildiğinde
                       // değeri önce int.init olarak atanır

    lazy int p_lazy) { // yaşamı işlev içinde kullanıldığı an
                       // başlar ve eğer kullanımı bitmişse
                       // hemen o an sonlanır; değeri için,
                       // her kullanıldığı an 'birHesap'
                       // işlevi çağrılır
    // ...
}

int birHesap() {
    int sonuç;
    // ...
    return sonuç;
}
Temel işlemler

Hangi türden olursa olsun, bir değişkenin yaşamı boyunca etkili olan üç temel işlem vardır:

Değişkenlerin yaşam süreçleri kurma işlemiyle başlar ve sonlandırma işlemiyle sona erer. Bu süreç boyunca değişkene yeni değerler atanabilir.

Kurma

Her değişken, kullanılmadan önce kurulmak zorundadır. Burada "kurma" sözcüğünü "hazırlamak, inşa etmek" anlamlarında kullanıyorum. Kurma iki alt adımdan oluşur:

  1. Yer ayrılması: Değişkenin yaşayacağı yer belirlenir.
  2. İlk değerinin verilmesi: O adrese ilk değeri yerleştirilir.

Her değişken bilgisayarın belleğinde kendisine ayrılan bir yerde yaşar. Derleyicinin istediğimiz işleri yaptırmak için mikro işlemcinin anladığı dilde kodlar ürettiğini biliyorsunuz. Derleyicinin ürettiği kodların bir bölümünün görevi, tanımlanan değişkenler için bellekten yer ayırmaktır.

Örneğin, hızı temsil eden şöyle bir değişken olsun:

    int hız = 123;

Daha önce Değerler ve Referanslar bölümünde gördüğümüz gibi, o değişkenin belleğin bir noktasında yaşadığını düşünebiliriz:

   ──┬─────┬─────┬─────┬──
     │     │ 123 │     │
   ──┴─────┴─────┴─────┴──

Her değişkenin bellekte bulunduğu yere o değişkenin adresi denir. Bir anlamda o değişken o adreste yaşamaktadır. Programda bir değişkenin değerini değiştirdiğimizde, değişkenin yeni değeri aynı yere yerleştirilir:

    ++hız;

Aynı adresteki değer bir artar:

   ──┬─────┬─────┬─────┬──
     │     │ 124 │     │
   ──┴─────┴─────┴─────┴──

Kurma, değişkenin yaşamı başladığı anda gerçekleştirilir çünkü değişkeni kullanıma hazırlayan işlemleri içerir. Değişkenin herhangi bir biçimde kullanılabilmesi için kurulmuş olması önemlidir.

Değişkenler üç farklı şekilde kurulabilirler:

Hiçbir değer kullanılmadan kurulduğunda değişkenin değeri o türün varsayılan değeridir. Varsayılan değer, her türün .init niteliğidir:

    int hız;

O durumda hız'ın değeri int.init'tir (yani 0). Varsayılan değerle kurulmuş olan bir değişkenin programda sonradan başka değerler alacağını düşünebiliriz.

    File dosya;

Dosyalar bölümünde gördüğümüz std.stdio.File türünden olan yukarıdaki dosya nesnesi dosya sisteminin hiçbir dosyasına bağlı olmayan bir File yapısı nesnesidir. Onun dosya sisteminin hangi dosyasına erişmek için kullanılacağının daha sonradan belirleneceğini düşünebiliriz; varsayılan şekilde kurulmuş olduğu için henüz kullanılamaz.

Değişken bazen başka bir değişkenin değeri kopyalanarak kurulur:

    int hız = başkaHız;

O durumda hız'ın değeri başkaHız'ın değerinden kopyalanır ve hız yaşamına o değerle başlar. Sınıf değişkenlerinde ise durum farklıdır:

    auto sınıfDeğişkeni = başkaSınıfDeğişkeni;

sınıfDeğişkeni de yaşamına başkaSınıfDeğişkeni'nin kopyası olarak başlar.

Aralarındaki önemli ayrım, hız ile başkaHız'ın birbirlerinden farklı iki değer olmalarına karşın sınıfDeğişkeni ile başkaSınıfDeğişkeni'nin aynı nesneye erişim sağlamalarıdır. Bu çok önemli ayrım değer türleri ile referans türleri arasındaki farktan ileri gelir.

Son olarak, değişkenler belirli değerlerle veya özel şekillerde kurulabilirler:

   int hız = birHesabınSonucu();

Yukarıdaki hız'ın ilk değeri programın çalışması sırasındaki bir hesabın değeri olarak belirlenmektedir.

   auto sınıfDeğişkeni = new BirSınıf;

Yukarıdaki sınıfDeğişkeni, yaşamına new ile kurulan nesneye erişim sağlayacak şekilde başlamaktadır.

Sonlandırma

Değişkenin yaşamının sona ermesi sırasında yapılan işlemlere sonlandırma denir. Kurma gibi sonlandırma da iki adımdan oluşur:

  1. Son işlemler: Değişkenin yapması gereken son işlemler işletilir
  2. Belleğin geri verilmesi: Değişkenin yaşadığı yer geri verilir

Temel türlerin çoğunda sonlandırma sırasında özel işlemler gerekmez. Örneğin int türünden bir değişkenin bellekte yaşamakta olduğu yere sıfır gibi özel bir değer atanmaz. Program o adresin artık boş olduğunun hesabını tutar ve orayı daha sonra başka değişkenler için kullanır.

Öte yandan, bazı türlerden olan değişkenlerin yaşamlarının sonlanması sırasında özel işlemler gerekebilir. Örneğin bir File nesnesi, eğer varsa, ara belleğinde tutmakta olduğu karakterleri diske yazmak zorundadır. Ek olarak, dosyayla işinin bittiğini dosya sistemine bildirmek için de dosyayı kapatmak zorundadır. Bu işlemler File'ın sonlandırma işlemleridir.

Dizilerde durum biraz daha üst düzeydedir: o dizinin erişim sağlamakta olduğu bütün elemanlar da sonlanırlar. Eğer dizinin elemanları temel türlerdense özel bir sonlanma işlemi gerekmez. Ama eğer dizinin elemanları sonlanma gerektiren bir yapı veya sınıf türündense, o türün sonlandırma işlemleri her eleman için uygulanır.

Sonlandırma eşleme tablolarında da dizilerdeki gibidir. Ek olarak, eşleme tablosunun sahip olduğu indeks değişkenleri de sonlandırılırlar. Eğer indeks türü olarak bir yapı veya sınıf türü kullanılmışsa, her indeks nesnesi için o türün gerektirdiği sonlandırma işlemleri uygulanır.

Çöp toplayıcı: D çöp toplayıcılı bir dildir. Bu tür dillerde sonlandırma işlemleri programcı tarafından açıkça yapılmak zorunda değildir. Yaşamı sona eren bir değişkenin sonlandırılması otomatik olarak çöp toplayıcı denen düzenek tarafından halledilir. Çöp toplayıcının ayrıntılarını ilerideki bir bölümde göreceğiz.

Değişkenler iki şekilde sonlandırılabilirler:

Bir değişkenin bunlardan hangi şekilde sonlandırılacağı öncelikle kendi türüne bağlıdır. Temel türlerin hemen sonlandırıldıklarını düşünebilirsiniz çünkü zaten sonlandırma için özel işlemleri yoktur. Bazı türlerin değişkenlerinin son işlemleri ise çöp toplayıcı tarafından daha sonraki bir zamanda işletilebilir.

Atama

Bir değişkenin yaşamı boyunca karşılaştığı diğer önemli işlem atamadır.

Temel türlerde atama işlemi yalnızca değişkenin değerinin değiştirilmesi olarak görülebilir. Yukarıdaki bellek gösteriminde olduğu gibi, değişken örneğin 123 olan bir değer yerine artık 124 değerine sahip olabilir.

Daha genel olarak aslında atama işlemi de iki adımdan oluşur:

  1. Eski değerin sonlandırılması: Eğer varsa, sonlandırma işlemleri ya hemen ya da çöp toplayıcı tarafından daha sonra işletilir
  2. Yeni değerin verilmesi: Eski değerin yerine yeni değer atanır

Bu iki adım sonlandırma işlemleri bulunmadığı için temel türlerde önemli değildir. Ama sonlandırma işlemleri bulunan türlerde atamanın böyle iki adımdan oluştuğunu akılda tutmakta yarar vardır: atama aslında bir sonlandırma ve bir yeni değer verme işlemidir.