clear ve scoped
Yaşam Süreçleri ve Temel İşlemler dersinde 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 derslerde 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öylece büyük olmaları nedeniyle, çö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ğu için nesne sayacını * bir arttırıyoruz. */ ++sayaç; } ~this() { /* * Bir nesne daha sonlandırılmış olduğu için 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
Çöp toplayıcının bellek ayırma algoritması, bu deneyde bu sınıfın nesnelerinden en fazla 7 adet bulunmasına izin veriyor. (Not: Bu çıktı çöp toplayıcının yürüttüğü algoritmaya, boş bellek miktarına ve başka etkenlere bağlı olarak farklı da olabilir.)
Nesnenin sonlandırıcısını işletmek için clear()
"Temizle, boşalt" anlamına gelen clear(), 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ç, ' '); clear(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 clear() 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
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.
Başka bir nedeni, nesnelerin belirli bir sırada sonlandırılıyor olmalarının programın akışı açısından önemli olduğu durumlardır. Örneğin RAII yöntemi, sonlandırıcılardaki kodların nesnelerin yaşamlarının sona erdiği anda işletilmeleri beklentisi üzerine kuruludur.
RAII, "resource acquisition is initialization"ın kısaltmasıdır. Tam çevirisi "kaynak ayırmak ilklemektir" olsa da, daha çok bunun tümleyeni olan "kaynaklar sonlandırıcı işlevlerde geri verilmelidir" ilkesini temsil eder.
Aşağıdaki XmlElemanı, sonlandırıcı işlevindeki etiket kapama işlemi nedeniyle RAII yönteminin bir örneği olarak görülebilir.
Örnek
Kurucu ve Diğer Özel İşlevler dersinde 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. Aynı düzeyde bulunan açma ve kapama etiketlerini aynı renkte gösteriyorum:
<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 çalışmasının nedeni, XmlElemanı'nın bir yapı olmasıdır. Yapıların sonlandırıcıları hemen çağrıldığı için; nesneleri uygun kapsamlara yerleştirmek, istenen çıktıyı elde etmek için yeterlidir:
void main() { auto dersler = XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { auto ders = XmlElemanı("ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { auto not = XmlElemanı("not", 2); const int 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(in int girintiAdımı) { return replicate(" ", girintiAdımı * 2); } class XmlElemanı { string isim; string girinti; this(in string isim, in int düzey) { this.isim = isim; this.girinti = girintiDizgisi(düzey); writeln(girinti, '<', isim, '>'); } ~this() { writeln(girinti, "</", isim, '>'); } } void main() { auto dersler = new XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { auto ders = new XmlElemanı( "ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { auto not = new XmlElemanı("not", 2); const int 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 clear() çağrılır:
void main() { auto dersler = new XmlElemanı("dersler", 0); foreach (dersNumarası; 0 .. 2) { auto ders = new XmlElemanı( "ders" ~ to!string(dersNumarası), 1); foreach (i; 0 .. 3) { auto not = new XmlElemanı("not", 2); const int rasgeleNot = uniform(50, 101); writeln(girintiDizgisi(3), rasgeleNot); clear(not); } clear(ders); } clear(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 clear() satırlarına gelinemeden atılmış olan bir hata nedeniyle çıkılmış olabilir. Eğer clear() satırlarının kesinlikle işletilmeleri gerekiyorsa, bunun bir çözümü Hatalar dersinde 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.
Programın yalnızca değişen satırlarını gösteriyorum:
import std.typecons; // ... auto dersler = scoped!XmlElemanı("dersler", 0); // ... auto ders = scoped!XmlElemanı( "ders" ~ to!string(dersNumarası), 1); // ... auto not = scoped!XmlElemanı("not", 2);
Bu değişikliklerden sonra programın çıktısı yine istenen düzende olur.
Özet
- Bir sınıf nesnesinin sonlandırıcı işlevinin istenen bir anda çağrılması için
clear()işlevi kullanılır. scopedile kurulan sınıf nesnelerinin sonlandırıcıları kapsamdan çıkılırken otomatik olarak çağrılır.
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları