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:
- Kurma: Yaşamın başlangıcı.
- Sonlandırma: Yaşamın sonu.
- Atama: Değerin değişmesi.
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:
- Yer ayrılması: Değişkenin yaşayacağı yer belirlenir.
- İ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:
- Varsayılan şekilde: Programcı değer belirtmemişse
- Kopyalanarak: Başka bir değişkenin değeriyle
- Belirli bir değerle: Programcının belirlediği değerle
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:
- Son işlemler: Değişkenin yapması gereken son işlemler işletilir
- 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:
- Hemen: Sonlandırma işlemleri hemen işletilir
- Sonra: Çöp toplayıcı tarafından ilerideki bir zamanda
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:
- 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
- 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.