destroy
ve scoped
Yaşam Süreçleri ve Temel İşlemler bölümünde değişkenlerin kurma işlemiyle başlayan ve sonlandırma işlemiyle biten yaşam süreçlerini görmüştük.
Daha sonraki bölümlerde de nesnelerin kurulması sırasında gereken işlemlerin this
isimli kurucu işlevde, sonlandırılması sırasında gereken işlemlerin de ~this
isimli sonlandırıcı işlevde tanımlandıklarını öğrenmiştik.
Sonlandırıcı işlev, yapılarda ve başka değer türlerinde nesnenin yaşamı sona ererken hemen işletilir. Sınıflarda ve başka referans türlerinde ise çöp toplayıcı tarafından sonraki bir zamanda işletilir.
Burada önemli bir ayrım vardır: bir sınıf nesnesinin yaşamının sona ermesi ile sonlandırıcı işlevinin işletilmesi aynı zamanda gerçekleşmez. Nesnenin yaşamı, örneğin geçerli olduğu kapsamdan çıkıldığı an sona erer. Sonlandırıcı işlevi ise çöp toplayıcı tarafından belirsiz bir zaman sonra otomatik olarak işletilir.
Sonlandırıcı işlevlerin görevlerinden bazıları, nesne için kullanılmış olan sistem kaynaklarını geri vermektir. Örneğin std.stdio.File
yapısı, işletim sisteminden kendi işi için almış olduğu dosya kaynağını sonlandırıcı işlevinde geri verir. Artık sonlanmakta olduğu için zaten o kaynağı kullanması söz konusu değildir.
Sınıfların sonlandırıcılarının çöp toplayıcı tarafından tam olarak ne zaman çağrılacakları belli olmadığı için, bazen kaynakların sisteme geri verilmeleri gecikebilir ve yeni nesneler için kaynak kalmayabilir.
Sınıf sonlandırıcı işlevlerinin geç işletilmesini gösteren bir örnek
Sınıfların sonlandırıcı işlevlerinin ilerideki belirsiz bir zamanda işletildiklerini göstermek için bir sınıf tanımlayalım. Bu sınıfın kurucu işlevi sınıfın static
bir sayacını arttırsın ve sonlandırıcı işlevi de o sayacı azaltsın. Hatırlarsanız, static
üyelerden bir tane bulunur: Sınıfın bütün nesneleri o tek üyeyi ortaklaşa kullanırlar. Böylece o sayacın değerine bakarak sınıfın nesnelerinden kaç tanesinin henüz sonlandırılmadıklarını anlayabileceğiz.
class YaşamıGözlenen { int[] dizi; // ← her nesnenin kendisine aittir static int sayaç; // ← bütün nesneler tarafından // paylaşılır this() { /* Her nesne bellekte çok yer tutsun diye bu diziyi * çok sayıda int'lik hale getiriyoruz. Nesnelerin * böyle büyük olmalarının sonucunda çöp * toplayıcının bellek açmak için onları daha sık * sonlandıracağını umuyoruz. */ dizi.length = 30_000; /* Bir nesne daha kurulmuş olduğundan nesne sayacını * bir arttırıyoruz. */ ++sayaç; } ~this() { /* Bir nesne daha sonlandırılmış olduğundan nesne * sayacını bir azaltıyoruz. */ --sayaç; } }
O sınıfın nesnelerini bir döngü içinde oluşturan bir program:
import std.stdio; void main() { foreach (i; 0 .. 20) { auto değişken = new YaşamıGözlenen; // ← baş write(YaşamıGözlenen.sayaç, ' '); } // ← son writeln(); }
O programda oluşan her YaşamıGözlenen
nesnesinin yaşamı aslında çok kısadır: new
anahtar sözcüğüyle başlar, ve foreach
döngüsünün kapama parantezinde son bulur. Yaşamları sona eren bu nesneler çöp toplayıcının sorumluluğuna girerler.
Programdaki baş ve son açıklamaları her nesnenin yaşamının başladığı ve sona erdiği noktayı gösteriyor. Nesnelerin sonlandırıcı işlevlerinin, yaşamlarının sona erdiği an işletilmediklerini sayacın değerine bakarak görebiliyoruz:
1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6
Yukarıdaki çıktıdan anlaşıldığına göre, çöp toplayıcının bellek ayırma algoritması, bu deneyde bu sınıfın sonlandırıcısını en fazla 7 nesne için ertelemiştir. (Not: Bu çıktı çöp toplayıcının yürüttüğü algoritmaya, boş bellek miktarına ve başka etkenlere bağlı olarak farklı olabilir.)
Nesnenin sonlandırıcısını işletmek için destroy()
"Ortadan kaldır" anlamına gelen destroy()
nesnenin sonlandırıcı işlevini çağırır:
void main() { foreach (i; 0 .. 20) { auto değişken = new YaşamıGözlenen; write(YaşamıGözlenen.sayaç, ' '); destroy(değişken); } writeln(); }
YaşamıGözlenen.sayaç
'ın değeri new
satırında kurucu işlevin işletilmesi sonucunda arttırılır ve 1 olur. Değerinin yazdırıldığı satırdan hemen sonraki destroy()
satırında da sonlandırıcı işlev tarafından tekrar sıfıra indirilir. O yüzden yazdırıldığı satırda hep 1 olduğunu görüyoruz:
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Açıkça sonlandırılan nesneler geçersiz kabul edilmelidirler ve artık kullanılmamalıdırlar:
destroy(değişken); // ... // Dikkat: Geçersiz bir nesneye erişiliyor writeln(değişken.dizi);
Normalde referans türleri ile kullanılan destroy
, gerektiğinde struct
nesnelerinin erkenden sonlandırılmaları için de kullanılabilir.
Ne zaman kullanmalı
Yukarıdaki örnekte gördüğümüz gibi, kaynakların çöp toplayıcının kararına kalmadan hemen geri verilmesi gerektiğinde kullanılır.
Örnek
Kurucu ve Diğer Özel İşlevler bölümünde XmlElemanı
isminde bir yapı tanımlamıştık. O yapı, XML elemanlarını <etiket>değer</etiket>
şeklinde yazdırmak için kullanılıyordu. XML elemanlarının kapama etiketlerinin yazdırılması sonlandırıcı işlevin göreviydi:
struct XmlElemanı { // ... ~this() { writeln(girinti, "</", isim, '>'); } }
O yapıyı kullanan bir programla aşağıdaki çıktıyı elde etmiştik:
<dersler> <ders0> <not> 72 </not> ← Kapama etiketleri doğru satırlarda beliriyor <not> 97 </not> ← <not> 90 </not> ← </ders0> ← <ders1> <not> 77 </not> ← <not> 87 </not> ← <not> 56 </not> ← </ders1> ← </dersler> ←
O çıktının doğru belirmesinin nedeni, XmlElemanı
'nın bir yapı olmasıdır. Yapıların sonlandırıcıları hemen çağrıldıklarından, istenen çıktı, nesneleri uygun kapsamlara yerleştirerek elde edilir:
void main() { const dersler = XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { const ders = XmlElemanı("ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { const not = XmlElemanı("not", 2); const rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); } // ← not sonlanır } // ← ders sonlanır } // ← dersler sonlanır
Nesneler açıklama satırları ile belirtilen noktalarda sonlandıkça XML kapama etiketlerini de çıkışa yazdırırlar.
Sınıfların farkını görmek için aynı programı bu sefer XmlElemanı
bir sınıf olacak şekilde yazalım:
import std.stdio; import std.array; import std.random; import std.conv; string girintiDizgisi(int girintiAdımı) { return replicate(" ", girintiAdımı * 2); } class XmlElemanı { string isim; string girinti; this(string isim, int düzey) { this.isim = isim; this.girinti = girintiDizgisi(düzey); writeln(girinti, '<', isim, '>'); } ~this() { writeln(girinti, "</", isim, '>'); } } void main() { const dersler = new XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { const ders = new XmlElemanı( "ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { const not = new XmlElemanı("not", 2); const rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); } } }
Referans türleri olan sınıfların sonlandırıcı işlevleri çöp toplayıcıya bırakılmış olduğu için programın çıktısı artık istenen düzende değildir:
<dersler> <ders0> <not> 57 <not> 98 <not> 87 <ders1> <not> 84 <not> 60 <not> 99 </not> ← Kapama etiketlerinin hepsi en sonda beliriyor </not> ← </not> ← </ders1> ← </not> ← </not> ← </not> ← </ders0> ← </dersler> ←
Bütün sonlandırıcı işlevler işletilmişlerdir ama kapama etiketleri beklenen yerlerde değildir. (Not: Aslında çöp toplayıcı bütün nesnelerin sonlandırılacakları garantisini vermez. Örneğin programın çıktısında hiçbir kapama parantezi bulunmayabilir.)
XmlElemanı
'nın sonlandırıcı işlevinin doğru noktalarda işletilmesini sağlamak için destroy()
çağrılır:
void main() { const dersler = new XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { const ders = new XmlElemanı( "ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { const not = new XmlElemanı("not", 2); const rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); destroy(not); } destroy(ders); } destroy(dersler); }
Sonuçta, nesneler kapsamlardan çıkılırken sonlandırıldıkları için programın çıktısı yapı tanımında olduğu gibi düzgündür:
<dersler> <ders0> <not> 66 </not> ← Kapama etiketleri doğru satırlarda belirmiş <not> 75 </not> ← <not> 68 </not> ← </ders0> ← <ders1> <not> 73 </not> ← <not> 62 </not> ← <not> 100 </not> ← </ders1> ← </dersler> ←
Sonlandırıcı işlevi otomatik olarak çağırmak için scoped
Yukarıdaki programın bir yetersizliği vardır: Kapsamlardan daha destroy()
satırlarına gelinemeden atılmış olan bir hata nedeniyle çıkılmış olabilir. Eğer destroy()
satırlarının kesinlikle işletilmeleri gerekiyorsa, bunun bir çözümü Hatalar bölümünde gördüğümüz scope
ve diğer olanaklardan yararlanmaktır.
Başka bir yöntem, sınıf nesnesini new
yerine std.typecons.scoped
ile kurmaktır. scoped()
, sınıf değişkenini perde arkasında bir yapı nesnesi ile sarmalar. O yapı nesnesinin sonlandırıcısı kapsamdan çıkılırken otomatik olarak çağrıldığında sınıf nesnesinin sonlandırıcısını da çağırır.
scoped
'un etkisi, yaşam süreçleri açısından sınıf nesnelerini yapı nesnelerine benzetmesidir.
Aşağıdaki değişikliklerden sonra program yine beklenen sonucu üretir:
import std.typecons; // ... void main() { const dersler = scoped!XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { const ders = scoped!XmlElemanı( "ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { const not = scoped!XmlElemanı("not", 2); const rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); } } }
destroy()
satırlarının çıkartılmış olduklarına dikkat edin.
scoped()
, asıl sınıf nesnesini sarmalayan özel bir yapı nesnesi döndürür. Döndürülen nesne asıl nesnenin vekili (proxy) olarak görev görür. (Aslında, yukarıdaki dersler
nesnesinin türü XmlElemanı
değil, Scoped
'dur.)
Kendisi otomatik olarak sonlandırılırken vekil nesne sarmaladığı sınıf nesnesini de destroy()
ile sonlandırır. (Bu, RAII yönteminin bir uygulamasıdır. scoped()
bunu ilerideki bölümlerde göreceğimiz şablon ve alias this
olanaklarından yararlanarak gerçekleştirir.)
Vekil nesnelerinin kullanımlarının asıl nesne kadar doğal olması istenir. Bu yüzden, scoped()
'un döndürdüğü nesne sanki asıl türdenmiş gibi kullanılabilir. Örneğin, asıl türün üye işlevleri vekil nesne üzerinde çağrılabilirler:
import std.typecons; class C { void foo() { } } void main() { auto v= scoped!C(); v.foo(); // Vekil nesnesi v, C gibi kullanılıyor }
Ancak, bu kolaylığın bir bedeli vardır: Vekil nesnesi asıl nesneye referans döndürdükten hemen sonra sonlanmış ve döndürülen referans o yüzden geçersiz kalmış olabilir. Bu durum asıl sınıf türü sol tarafta açıkça belirtildiğinde ortaya çıkabilir:
C c = scoped!C(); // ← HATALI c.foo(); // ← Sonlanmış bir nesneye erişir
Yukarıdaki c
vekil nesnesi değil, açıkça C
olarak tanımlandığından asıl nesneye erişim sağlamakta olan bir sınıf değişkenidir. Ne yazık ki bu durumda sağ tarafta kurulmuş olan vekil nesnesi kurulduğu ifadenin sonunda sonlandırılacaktır. Sonuçta, geçersiz bir nesneye erişim sağlamakta olan c
'nin kullanılması tanımsız davranıştır. Örneğin, program bir çalışma zamanı hatasıyla çökebilir:
Segmentation fault
O yüzden, scoped()
değişkenlerini asıl tür ile tanımlamayın:
C a = scoped!C(); // ← HATALI auto b = scoped!C(); // ← doğru const c = scoped!C(); // ← doğru immutable d = scoped!C(); // ← doğru
Özet
- Bir sınıf nesnesinin sonlandırıcı işlevinin istenen bir anda çağrılması için
destroy()
işlevi kullanılır. scoped()
ile kurulan sınıf nesnelerinin sonlandırıcıları kapsamdan çıkılırken otomatik olarak çağrılır.scoped()
değişkenlerini asıl türün ismiyle tanımlamak hatalıdır.