Drop Trait’i ile Temizlik Sırasında Kod Çalıştırma
Akıllı işaretçi deseni açısından önemli ikinci trait, bir değer kapsam dışına çıkmak üzereyken ne olacağını özelleştirmenize olanak tanıyan Drop trait’idir. Herhangi bir tür üzerinde Drop trait’i için bir uygulama sağlayabilirsiniz ve bu kod, dosyalar veya ağ bağlantıları gibi kaynakları serbest bırakmak amacıyla kullanılabilir.
Drop trait’ini akıllı işaretçiler bağlamında tanıtıyoruz; çünkü Drop trait’inin sunduğu işlevsellik, bir akıllı işaretçi uygulanırken neredeyse her zaman kullanılır. Örneğin bir Box<T> düşürüldüğünde, kutunun işaret ettiği öbek (heap) alanını serbest bırakır.
Bazı dillerde, belirli türlerin örnekleriyle işiniz her bittiğinde belleği veya kaynakları serbest bırakacak kodu programcının elle çağırması gerekir. Dosya tanıtıcıları (file handle), soketler ve kilitler buna örnektir. Programcı bunu unutursa sistem aşırı yüklenebilir ve çökebilir. Rust’ta ise bir değer her kapsam dışına çıktığında belirli bir kod parçasının çalıştırılmasını belirtebilirsiniz ve derleyici bu kodu otomatik olarak ekler. Sonuç olarak, bir programda belirli bir türün örneğiyle işinizin bittiği her yere temizlik kodu yerleştirme konusunda titizlenmeniz gerekmez; yine de kaynak sızıntısı yaşamazsınız!
Bir değer kapsam dışına çıktığında çalışacak kodu Drop trait’ini uygulayarak belirtirsiniz. Drop trait’i, self’e değiştirilebilir bir referans (&mut self) alan drop adında tek bir metodu uygulamanızı zorunlu kılar. Rust’ın drop metodunu ne zaman çağırdığını görmek için şimdilik drop metodunu println! deyimleriyle uygulayalım.
Liste 15-14, Rust’ın drop metodunu ne zaman çalıştırdığını göstermek amacıyla, tek özel işlevi örnek kapsam dışına çıktığında ekrana `{}` verisine sahip OzelAkilliIsaretci bırakılıyor! yazdırmak olan bir OzelAkilliIsaretci struct’ını göstermektedir.
struct OzelAkilliIsaretci {
veri: String,
}
impl Drop for OzelAkilliIsaretci {
fn drop(&mut self) {
println!(
"`{}` verisine sahip OzelAkilliIsaretci bırakılıyor!",
self.veri
);
}
}
fn main() {
let c = OzelAkilliIsaretci {
veri: String::from("benim şeylerim"),
};
let d = OzelAkilliIsaretci {
veri: String::from("diğer şeyler"),
};
println!("OzelAkilliIsaretciler oluşturuldu");
}
Drop trait’ini uygulayan bir OzelAkilliIsaretci struct’ıDrop trait’i başlangıç bildirimlerinde (prelude) yer aldığı için onu ayrıca kapsama dahil etmemiz gerekmez. OzelAkilliIsaretci üzerinde Drop trait’ini uyguluyor ve println! makrosunu çağıran drop metodu için bir uygulama sağlıyoruz. drop metodunun gövdesi, türünüzün bir örneği kapsam dışına çıktığında çalışmasını istediğiniz her türlü mantığı yerleştireceğiniz yerdir. Burada Rust’ın drop metodunu ne zaman çağıracağını görsel olarak göstermek için ekrana metin yazdırıyoruz.
main fonksiyonunda iki OzelAkilliIsaretci örneği oluşturuyor ve ardından OzelAkilliIsaretciler oluşturuldu metnini yazdırıyoruz. main’in sonunda OzelAkilliIsaretci örneklerimiz kapsam dışına çıkacak ve Rust, drop metoduna koyduğumuz kodu çağırarak son mesajlarımızı yazdıracaktır. drop metodunu açıkça çağırmamız gerekmediğine dikkat edin.
Bu programı çalıştırdığımızda aşağıdaki çıktıyı görürüz:
$ cargo run
Compiling birak-ornegi v0.1.0 (file:///projects/birak-ornegi)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/birak-ornegi`
OzelAkilliIsaretciler oluşturuldu
`diğer şeyler` verisine sahip OzelAkilliIsaretci bırakılıyor!
`benim şeylerim` verisine sahip OzelAkilliIsaretci bırakılıyor!
Örneklerimiz kapsam dışına çıktığında Rust bizim yerimize otomatik olarak drop metodunu çağırdı ve belirttiğimiz kodu çalıştırdı. Değişkenler, yığın (stack) üzerindeki son giren ilk çıkar düzenine uygun biçimde oluşturuldukları sıranın tersine düşürülür; bu yüzden d değişkeni c’den önce düşürülmüştür. Bu örneğin amacı drop metodunun nasıl çalıştığına dair size görsel bir rehber sunmaktır; normalde bir yazdırma mesajı yerine türünüzün çalıştırması gereken temizlik kodunu belirtirsiniz.
Not: Düşürme sırası yalnızca yerel değişkenler için değil, bileşik değerlerin parçaları için de kesin olarak belirlidir ve bu kurallara güvenebilirsiniz:
- Aynı kapsamdaki yerel değişkenler, bildirildikleri sıranın tersine düşürülür.
- Bir struct ya da enum değeri düşürülürken önce (varsa) türün kendi
dropmetodu çalışır, ardından alanları bildirim sırasıyla düşürülür. Demetlerde, dizilerde veVec<T>gibi koleksiyonlarda da öğeler baştan sona sırayla düşürülür. - Bir değişkene yeni bir değer atandığında eski değer, Bölüm 4’te gördüğümüz gibi atama anında hemen düşürülür.
drop metodunun gövdesinde self.veri gibi alanlara hâlâ erişebilmenizin
nedeni de budur: Alanlar, türün kendi drop metodu tamamlandıktan sonra
düşürülür.
Ne yazık ki otomatik drop işlevini devre dışı bırakmak doğrudan mümkün değildir. Aslında drop işlevini devre dışı bırakmak genellikle gerekli de değildir; Drop trait’inin tüm amacı bu işlemin otomatik olarak halledilmesidir. Bununla birlikte zaman zaman bir değeri erkenden temizlemek isteyebilirsiniz. Bunun bir örneği kilitleri yöneten akıllı işaretçilerin kullanımıdır: Aynı kapsam içindeki başka bir kodun kilidi edinebilmesi için kilidi serbest bırakan drop metodunu çalışmaya zorlamak isteyebilirsiniz. Rust, Drop trait’inin drop metodunu elle çağırmanıza izin vermez; bunun yerine bir değerin kendi kapsamının sonundan önce düşürülmesini zorlamak istiyorsanız standart kütüphane tarafından sunulan std::mem::drop fonksiyonunu çağırmanız gerekir.
Liste 15-14’teki main fonksiyonunu değiştirerek Drop trait’inin drop metodunu elle çağırmayı denemek, Liste 15-15’te gösterildiği gibi çalışmayacaktır.
struct OzelAkilliIsaretci {
veri: String,
}
impl Drop for OzelAkilliIsaretci {
fn drop(&mut self) {
println!(
"`{}` verisine sahip OzelAkilliIsaretci bırakılıyor!",
self.veri
);
}
}
fn main() {
let c = OzelAkilliIsaretci {
veri: String::from("bir miktar veri"),
};
println!("OzelAkilliIsaretci oluşturuldu");
c.drop();
println!("OzelAkilliIsaretci, main bitmeden önce bırakıldı");
}
Drop trait’indeki drop metodunu elle çağırmayı denemekBu kodu derlemeye çalıştığımızda şu hatayı alırız:
$ cargo run
Compiling birak-ornegi v0.1.0 (file:///projects/birak-ornegi)
error[E0040]: explicit use of destructor method
--> src/main.rs:19:7
|
19 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
19 - c.drop();
19 + drop(c);
|
For more information about this error, try `rustc --explain E0040`.
error: could not compile `birak-ornegi` (bin "birak-ornegi") due to 1 previous error
Bu hata mesajı (explicit use of destructor method — yıkıcı metodun açık kullanımı), drop metodunu açıkça çağırmamıza izin verilmediğini belirtir. Hata mesajında geçen yıkıcı (destructor) ifadesi, bir örneği temizleyen fonksiyonlar için kullanılan genel programlama terimidir. Bir yıkıcı, bir örnek oluşturan yapıcı (constructor) kavramının karşıtı niteliğindedir. Rust’taki drop metodu da bu tür yıkıcılardan biridir.
Rust, drop metodunu açıkça çağırmamıza izin vermez; çünkü main fonksiyonunun sonunda değer üzerinde drop metodunu yine otomatik olarak çağıracaktır. Bu durum, Rust aynı değeri iki kez temizlemeye çalışacağı için bir çift serbest bırakma (double free) hatasına yol açardı.
Bir değer kapsam dışına çıktığında drop çağrısının otomatik olarak eklenmesini devre dışı bırakamayız ve drop metodunu açıkça çağıramayız. Dolayısıyla bir değerin erkenden temizlenmesini zorlamamız gerekiyorsa std::mem::drop fonksiyonunu kullanırız.
std::mem::drop fonksiyonu, Drop trait’indeki drop metodundan farklıdır. Bu fonksiyonu, düşürülmesini zorlamak istediğimiz değeri argüman olarak aktararak çağırırız. Fonksiyon başlangıç bildirimlerinde yer aldığı için Liste 15-15’teki main fonksiyonunu Liste 15-16’da gösterildiği gibi drop fonksiyonunu çağıracak şekilde değiştirebiliriz.
struct OzelAkilliIsaretci {
veri: String,
}
impl Drop for OzelAkilliIsaretci {
fn drop(&mut self) {
println!(
"`{}` verisine sahip OzelAkilliIsaretci bırakılıyor!",
self.veri
);
}
}
fn main() {
let c = OzelAkilliIsaretci {
veri: String::from("bir miktar veri"),
};
println!("OzelAkilliIsaretci oluşturuldu");
drop(c);
println!("OzelAkilliIsaretci, main bitmeden önce bırakıldı");
}
std::mem::drop fonksiyonunu çağırmakBu kodu çalıştırmak aşağıdaki çıktıyı üretecektir:
$ cargo run
Compiling birak-ornegi v0.1.0 (file:///projects/birak-ornegi)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/birak-ornegi`
OzelAkilliIsaretci oluşturuldu
`bir miktar veri` verisine sahip OzelAkilliIsaretci bırakılıyor!
OzelAkilliIsaretci, main bitmeden önce bırakıldı
`bir miktar veri` verisine sahip OzelAkilliIsaretci bırakılıyor! metni, OzelAkilliIsaretci oluşturuldu ile OzelAkilliIsaretci, main bitmeden önce bırakıldı metinleri arasında yazdırılır; bu da c değerini düşürmek için drop metodu kodunun tam o noktada çağrıldığını gösterir.
Not: Standart kütüphanedeki std::mem::drop fonksiyonunun arkasında özel bir
derleyici sihri yoktur; tanımı kelimenin tam anlamıyla gövdesi tamamen boş bir
fonksiyondur:
#![allow(unused)]
fn main() {
pub fn drop<T>(_x: T) {}
}
Buradaki bütün incelik sahiplik sistemindedir: Drop::drop(&mut self) metodu
değeri yalnızca değiştirilebilir referansla ödünç alırken, std::mem::drop
fonksiyonu _x: T parametresiyle değerin sahipliğini devralır (move).
drop(c) çağrıldığında c değişkeni fonksiyonun içine taşınır ve fonksiyonun
kapanış süslü parantezi } hizasında kapsam dışına çıkarak Rust’ın normal
otomatik düşürme mekanizmasını (C++ dünyasında RAII — Resource Acquisition Is
Initialization olarak bilinen deterministik kaynak yönetimini) tetikler.
Sahiplik taşındığı için derleyici c değişkeninin main içinde tekrar
kullanılmasını engeller ve main sonunda ikinci kez düşürülmemesini garanti
eder.
Aynı sahiplik kuralı, bir türün neden hem Copy hem de Drop trait’ini aynı
anda uygulayamayacağını da açıklar (denerseniz derleyici error[E0184]: the trait 'Copy' cannot be implemented for this type; the type has a destructor —
bu tür bir yıkıcıya sahip olduğundan Copy trait’i uygulanamaz hatasını
verir). Copy türleri atama ve fonksiyon çağrılarında sahipliği taşımak
yerine örtük olarak bit düzeyinde kopyalandığından, bir tür hem Copy hem
Drop olsaydı drop(x) çağrısı yalnızca bir kopyayı düşürür, asıl x
değişkeni yaşamaya devam eder ve her kopya kapsam dışına çıktığında aynı
kaynak üzerinde yıkıcı tekrar tekrar çalışarak çift serbest bırakma hatasına
yol açardı.
Temizlik işlemlerini kullanışlı ve güvenli hâle getirmek için bir Drop trait’i uygulamasında belirttiğiniz kodu pek çok farklı biçimde kullanabilirsiniz: Örneğin bu sayede kendi bellek ayırıcınızı oluşturabilirsiniz! Drop trait’i ve Rust’ın sahiplik sistemi sayesinde temizlik yapmayı hatırlamak zorunda kalmazsınız; çünkü Rust bunu otomatik olarak gerçekleştirir.
Ayrıca hâlâ kullanımda olan değerleri yanlışlıkla temizlemekten kaynaklanacak sorunlar için de endişelenmeniz gerekmez: Referansların her zaman geçerli olmasını sağlayan sahiplik sistemi, drop metodunun yalnızca değer artık kullanılmadığında ve tek bir kez çağrılmasını da garanti eder.
Box<T> türünü ve akıllı işaretçilerin bazı karakteristik özelliklerini incelediğimize göre, şimdi standart kütüphanede tanımlı diğer birkaç akıllı işaretçiye göz atalım.