Akıllı İşaretçilere Normal Referanslar Gibi Davranma
Deref trait’ini uygulamak, başvuru çözme operatörü (dereference operator) olan * sembolünün (çarpma veya glob operatörüyle karıştırılmamalıdır) davranışını özelleştirmenize olanak tanır. Deref trait’ini bir akıllı işaretçiye normal bir referans gibi davranılabilecek biçimde uygulayarak, referanslar üzerinde çalışan kodlar yazabilir ve aynı kodu akıllı işaretçilerle de kullanabilirsiniz.
Öncelikle başvuru çözme operatörünün normal referanslarla nasıl çalıştığına bakalım. Ardından Box<T> gibi davranan özel bir tür tanımlamayı deneyecek ve başvuru çözme operatörünün yeni tanımladığımız bu tür üzerinde neden bir referanstaki gibi çalışmadığını göreceğiz. Deref trait’ini uygulamanın, akıllı işaretçilerin referanslara benzer şekillerde çalışmasını nasıl mümkün kıldığını inceleyeceğiz. Son olarak da Rust’ın deref zorlaması (deref coercion) özelliğine ve bu özelliğin hem referanslarla hem de akıllı işaretçilerle çalışmamıza nasıl imkân tanıdığına göz atacağız.
Referansı İzleyerek Değere Ulaşmak
Normal bir referans bir işaretçi türüdür ve bir işaretçiyi düşünmenin yollarından biri, onu başka bir yerde saklanan bir değere uzanan bir ok olarak hayal etmektir. Liste 15-6’da bir i32 değerine referans oluşturuyor ve ardından referansı izleyip değere ulaşmak için başvuru çözme operatörünü kullanıyoruz.
fn main() {
let sayi = 5;
let referans = &sayi;
assert_eq!(5, sayi);
assert_eq!(5, *referans);
}
i32 değerine işaret eden referansı başvuru çözme operatörüyle izlemeksayi değişkeni 5 değerinde bir i32 tutar. referans değişkenini sayi değişkenine bir referans olacak şekilde ayarlarız. sayi değişkeninin 5’e eşit olduğunu doğrulayabiliriz. Ancak referans içindeki değer hakkında bir doğrulama yapmak istiyorsak, derleyicinin gerçek değeri karşılaştırabilmesi için *referans yazarak referansı işaret ettiği değere kadar izlememiz (bu işleme başvuru çözme denir) gerekir. referans üzerinde başvuru çözme yaptığımızda, referans’ın işaret ettiği tam sayı değerine erişir ve bu değeri 5 ile karşılaştırabiliriz.
Bunun yerine assert_eq!(5, referans); yazmayı deneseydik şu derleme hatasını alırdık:
$ cargo run
Compiling deref-ornegi v0.1.0 (file:///projects/deref-ornegi)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, referans);
| ^^^^^^^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f128
f16
f32
f64
i128
i16
i32
i64
and 8 others
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-ornegi` (bin "deref-ornegi") due to 1 previous error
Bir sayıyı ve bir sayıya referansı karşılaştırmaya izin verilmez; çünkü bunlar farklı türlerdir ({integer} ve &{integer}). Derleyicinin E0277 hatasında belirttiği üzere tam sayı türü için &{integer} ile eşitlik karşılaştırması (PartialEq<&{integer}>) tanımlı değildir. Referansın işaret ettiği değere ulaşmak için başvuru çözme operatörünü kullanmamız gerekir.
Box<T>’yi Referans Gibi Kullanmak
Liste 15-6’daki kodu referans yerine bir Box<T> kullanacak şekilde yeniden yazabiliriz; Liste 15-7’de Box<T> üzerinde kullanılan başvuru çözme operatörü, Liste 15-6’da referans üzerinde kullanılan başvuru çözme operatörüyle birebir aynı şekilde çalışır.
fn main() {
let sayi = 5;
let kutu = Box::new(sayi);
assert_eq!(5, sayi);
assert_eq!(5, *kutu);
}
Box<i32> üzerinde başvuru çözme operatörünü kullanmakListe 15-7 ile Liste 15-6 arasındaki temel fark, burada kutu değişkenini sayi değerine işaret eden bir referans yerine, sayi değerinin öbekteki bir kopyasına işaret eden bir kutu örneği olarak ayarlamamızdır. Son doğrulamada, Liste 15-6’da referans değişkeniyle yaptığımız gibi kutunun işaretçisini izlemek için başvuru çözme operatörünü (*kutu) kullanabiliriz. Şimdi kendi kutu türümüzü tanımlayarak, Box<T>’yi başvuru çözme operatörünü kullanmamıza olanak tanıyacak şekilde özel kılan şeyin ne olduğunu keşfedelim.
Kendi Akıllı İşaretçimizi Tanımlamak
Akıllı işaretçi türlerinin varsayılan olarak referanslardan nasıl farklı davrandığını deneyimlemek için standart kütüphanenin sunduğu Box<T> türüne benzer sarmalayıcı bir tür oluşturalım. Ardından başvuru çözme operatörünü kullanma yeteneğini nasıl ekleyeceğimize bakacağız.
Not: Birazdan oluşturacağımız BenimKutu<T> türü ile gerçek Box<T> arasında
büyük bir fark vardır: Bizim sürümümüz verisini öbekte saklamayacaktır. Bu
örnekte Deref trait’ine odaklandığımız için, verinin gerçekte nerede
saklandığı işaretçi benzeri davranıştan daha az önemlidir.
Box<T> türü özünde tek elemanlı bir demet struct’ı olarak tanımlanır; bu yüzden Liste 15-8 de BenimKutu<T> türünü aynı şekilde tanımlar. Ayrıca Box<T> üzerinde tanımlı new fonksiyonuna karşılık gelecek bir yeni fonksiyonu da tanımlayacağız.
struct BenimKutu<T>(T);
impl<T> BenimKutu<T> {
fn yeni(x: T) -> BenimKutu<T> {
BenimKutu(x)
}
}
fn main() {}
BenimKutu<T> türü tanımlamakTürümüzün her türden değeri tutabilmesini istediğimiz için BenimKutu adında bir struct tanımlıyor ve jenerik bir T parametresi bildiriyoruz. BenimKutu türü, T türünde tek bir elemana sahip bir demet struct’ıdır. BenimKutu::yeni fonksiyonu T türünde bir parametre alır ve aktarılan değeri tutan bir BenimKutu örneği döndürür.
Liste 15-7’deki main fonksiyonunu Liste 15-8’e eklemeyi ve Box<T> yerine tanımladığımız BenimKutu<T> türünü kullanacak şekilde değiştirmeyi deneyelim. Liste 15-9’daki kod derlenmeyecektir; çünkü Rust, BenimKutu üzerinde nasıl başvuru çözme yapacağını bilmez.
struct BenimKutu<T>(T);
impl<T> BenimKutu<T> {
fn yeni(x: T) -> BenimKutu<T> {
BenimKutu(x)
}
}
fn main() {
let sayi = 5;
let kutu = BenimKutu::yeni(sayi);
assert_eq!(5, sayi);
assert_eq!(5, *kutu);
}
BenimKutu<T> türünü referansları ve Box<T>’yi kullandığımız şekilde kullanmayı denemekOrtaya çıkan derleme hatası şöyledir:
$ cargo run
Compiling deref-ornegi v0.1.0 (file:///projects/deref-ornegi)
error[E0614]: type `BenimKutu<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *kutu);
| ^^^^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-ornegi` (bin "deref-ornegi") due to 1 previous error
BenimKutu<T> türümüz üzerinde bu yeteneği henüz uygulamadığımız için başvuru çözme işlemi yapılamaz (type 'BenimKutu<{integer}>' cannot be dereferenced). * operatörüyle başvuru çözmeyi etkinleştirmek için Deref trait’ini uygularız.
Deref Trait’ini Uygulamak
Bölüm 10’daki “Bir Tür Üzerinde Trait Uygulama” başlığında ele aldığımız gibi, bir trait’i uygulamak için trait’in zorunlu kıldığı metotların uygulamalarını sağlamamız gerekir. Standart kütüphane tarafından sunulan Deref trait’i, self’i ödünç alan ve içteki veriye bir referans döndüren deref adında tek bir metodu uygulamamızı ister. Liste 15-10, BenimKutu<T> tanımına eklenecek Deref uygulamasını içerir.
use std::ops::Deref;
impl<T> Deref for BenimKutu<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct BenimKutu<T>(T);
impl<T> BenimKutu<T> {
fn yeni(x: T) -> BenimKutu<T> {
BenimKutu(x)
}
}
fn main() {
let sayi = 5;
let kutu = BenimKutu::yeni(sayi);
assert_eq!(5, sayi);
assert_eq!(5, *kutu);
}
BenimKutu<T> üzerinde Deref trait’ini uygulamaktype Target = T; sözdizimi, Deref trait’inin kullanması için bir ilişkili tür (associated type) tanımlar. İlişkili türler jenerik bir parametre bildirmenin biraz farklı bir yoludur, ancak şimdilik bunlar için endişelenmenize gerek yoktur; ilişkili türleri Bölüm 20’de daha ayrıntılı olarak ele alacağız.
deref metodunun * operatörüyle erişmek istediğimiz değere bir referans döndürmesi için metot gövdesini &self.0 ile dolduruyoruz; Bölüm 5’teki “Demet Struct’ları ile Farklı Türler Oluşturma” başlığından .0 sözdiziminin bir demet struct’ındaki ilk değere eriştiğini hatırlayın. Liste 15-9’da BenimKutu<T> değeri üzerinde * çağıran main fonksiyonu artık derlenir ve doğrulamalar başarıyla geçer!
Deref trait’i olmadan derleyici yalnızca & referanslarının başvurusunu çözebilir. deref metodu derleyiciye, Deref trait’ini uygulayan herhangi bir türdeki değeri alıp deref metodunu çağırarak nasıl çözeceğini bildiği bir & referansı elde etme yeteneği kazandırır.
Liste 15-9’da *kutu yazdığımızda, perde arkasında Rust aslında şu kodu çalıştırdı:
*(kutu.deref())
Rust, deref metodunu çağırmamız gerekip gerekmediğini düşünmek zorunda kalmamamız için * operatörünü önce bir deref metodu çağrısıyla, ardından da sıradan bir başvuru çözme işlemiyle değiştirir. Rust’ın bu özelliği, elimizde ister normal bir referans ister Deref uygulayan bir tür olsun, birebir aynı şekilde çalışan kodlar yazmamızı sağlar.
deref metodunun bir değere referans döndürmesinin ve *(kutu.deref()) ifadesinde parantez dışındaki sıradan başvuru çözme işleminin hâlâ gerekli olmasının nedeni sahiplik sistemiyle ilgilidir. Eğer deref metodu değere referans yerine doğrudan değerin kendisini döndürseydi, değer self’in dışına taşınmış (move) olurdu. Bu örnekte —ve başvuru çözme operatörünü kullandığımız çoğu durumda— BenimKutu<T> içindeki değerin sahipliğini devralmak istemeyiz.
Kodumuzda her * kullandığımızda, * operatörünün bir deref metodu çağrısı ve ardından yalnızca bir kez * operatörü çağrısı ile değiştirildiğine dikkat edin. * operatörünün bu dönüşümü sonsuza dek özyineli olarak tekrarlanmadığı için sonunda Liste 15-9’daki assert_eq! makrosunda yer alan 5 ile eşleşen i32 türünde veriyi elde ederiz.
Not: *(kutu.deref()) dönüşümünün sahiplik açısından sağladığı korumayı somut
olarak görmek için kutu değişkeninin Copy olmayan bir BenimKutu<String>
olduğunu düşünün. kutu.deref() çağrısı &self üzerinden ödünç alarak
&String döndürür; dıştaki * operatörü ise değeri taşımak yerine bellekteki
veriyi temsil eden bir konum ifadesi (place expression) üretir. Böylece
&*kutu ile referans alabilir veya içteki değerin metotlarını sahipliği
bozmadan çağırabilirsiniz. Buna karşılık let s = *kutu; yazarak içteki
String’i doğrudan dışarı taşımaya kalkışırsanız derleyici error[E0507]: cannot move out of dereference of BenimKutu<String> (BenimKutu<String>
başvuru çözmesinden dışarı taşıma yapılamaz) hatasıyla akıllı işaretçinin
bütünlüğünü korur. Liste 15-9’da *kutu ifadesinin doğrudan çalışmasının
nedeni ise i32 türünün Copy trait’ini uygulamasıdır; derleyici konumdan
değeri taşımak yerine, bit düzeyinde kopyalayarak okur.
Gerçek Box<T> ise bu kuralın derleyiciye yerleşik tek istisnasıdır: Kutu,
içindeki değerin tek sahibi olduğundan let s = *kutu; yazarak değeri
sahipliğiyle birlikte dışarı taşıyabilirsiniz. Deref uygulayan başka hiçbir
tür (Rc<T> dahil) bu olanağı sunmaz.
Fonksiyon ve Metotlarda Deref Zorlaması Kullanmak
Deref zorlaması, Deref trait’ini uygulayan bir türe ait referansı başka bir türe ait referansa dönüştürür. Örneğin String türü, &str döndürecek şekilde Deref trait’ini uyguladığı için deref zorlaması &String’i &str’ye dönüştürebilir. Deref zorlaması, Rust’ın fonksiyon ve metot argümanları üzerinde gerçekleştirdiği bir kolaylıktır ve yalnızca Deref trait’ini uygulayan türler üzerinde çalışır. Belirli bir türdeki değere ait referansı, fonksiyon veya metot tanımındaki parametre türüyle eşleşmeyen bir argüman olarak aktardığımızda otomatik olarak devreye girer. Arka arkaya yapılan bir dizi deref metodu çağrısı, sağladığımız türü parametrenin ihtiyaç duyduğu türe dönüştürür.
Deref zorlaması Rust’a, fonksiyon ve metot çağrıları yazan programcıların & ve * ile çok sayıda açık referans alma ve başvuru çözme işlemi eklemek zorunda kalmaması için dahil edilmiştir. Deref zorlaması özelliği ayrıca hem referanslarla hem de akıllı işaretçilerle çalışabilen daha fazla kod yazmamıza olanak tanır.
Deref zorlamasını çalışırken görmek için Liste 15-8’de tanımladığımız BenimKutu<T> türünü ve Liste 15-10’da eklediğimiz Deref uygulamasını kullanalım. Liste 15-11, string dilimi parametresine sahip bir fonksiyonun tanımını göstermektedir.
fn merhaba(isim: &str) {
println!("Merhaba, {isim}!");
}
fn main() {}
&str türünde isim parametresine sahip bir merhaba fonksiyonumerhaba fonksiyonunu argüman olarak örneğin merhaba("Rust"); şeklinde bir string dilimiyle çağırabiliriz. Deref zorlaması ise Liste 15-12’de gösterildiği gibi merhaba fonksiyonunu BenimKutu<String> türündeki bir değere referansla çağırmayı mümkün kılar.
use std::ops::Deref;
impl<T> Deref for BenimKutu<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct BenimKutu<T>(T);
impl<T> BenimKutu<T> {
fn yeni(x: T) -> BenimKutu<T> {
BenimKutu(x)
}
}
fn merhaba(isim: &str) {
println!("Merhaba, {isim}!");
}
fn main() {
let kutu = BenimKutu::yeni(String::from("Rust"));
merhaba(&kutu);
}
merhaba fonksiyonunu bir BenimKutu<String> değerine referansla çağırmakBurada merhaba fonksiyonunu, bir BenimKutu<String> değerine referans olan &kutu argümanıyla çağırıyoruz. Liste 15-10’da BenimKutu<T> üzerinde Deref trait’ini uyguladığımız için Rust, deref metodunu çağırarak &BenimKutu<String> türünü &String türüne dönüştürebilir. Standart kütüphane, String üzerinde string dilimi döndüren bir Deref uygulaması sunar ve bu durum Deref’in API belgelerinde yer alır. Rust, &String’i &str’ye dönüştürmek için deref metodunu bir kez daha çağırır ve böylece merhaba fonksiyonunun tanımıyla eşleşme sağlanır.
Eğer Rust deref zorlamasını uygulamasaydı, merhaba fonksiyonunu &BenimKutu<String> türünde bir değerle çağırmak için Liste 15-12’deki kod yerine Liste 15-13’teki kodu yazmamız gerekirdi.
use std::ops::Deref;
impl<T> Deref for BenimKutu<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct BenimKutu<T>(T);
impl<T> BenimKutu<T> {
fn yeni(x: T) -> BenimKutu<T> {
BenimKutu(x)
}
}
fn merhaba(isim: &str) {
println!("Merhaba, {isim}!");
}
fn main() {
let kutu = BenimKutu::yeni(String::from("Rust"));
merhaba(&(*kutu)[..]);
}
(*kutu) ifadesi BenimKutu<String> değerinin başvurusunu çözerek bir String elde eder. Ardından & ve [..], merhaba fonksiyonunun imzasıyla eşleşmek üzere String’in tamamına eşit bir string dilimi alır. Deref zorlaması olmadan yazılan bu kodu tüm bu sembollerle okumak, yazmak ve anlamak daha güçtür. Deref zorlaması, Rust’ın bu dönüşümleri bizim adımıza otomatik olarak ele almasını sağlar.
İlgili türler için Deref trait’i tanımlandığında Rust, türleri analiz eder ve parametrenin türüyle eşleşecek bir referans elde etmek için gerektiği kadar Deref::deref kullanır. Kaç kez Deref::deref çağrısı ekleneceği derleme zamanında çözümlenir; dolayısıyla deref zorlamasından yararlanmanın hiçbir çalışma zamanı maliyeti yoktur!
Not: Deref zorlaması yalnızca fonksiyon argümanlarında değil, metot çağrısı
sözdiziminde de iş başındadır. kutu.len() yazdığınızda Rust, len adında
bir metot bulana dek BenimKutu<String>, String ve str türlerini sırayla
dener; bu yüzden değiştirilebilir bir Box<Vec<i32>> üzerinde doğrudan
.push(1) çağırabilir ya da bir Rc<String> üzerinde .len()
kullanabilirsiniz. Bölüm 5’te gördüğünüz otomatik referans alma ve başvuru
çözme mekanizması, perde arkasında işte bu Deref zincirini izler.
Değiştirilebilir Referanslarla Deref Zorlamasını Ele Almak
Değiştirilemez referanslar üzerinde * operatörünü geçersiz kılmak için Deref trait’ini kullandığınız gibi, değiştirilebilir referanslar üzerinde * operatörünü geçersiz kılmak için de DerefMut trait’ini kullanabilirsiniz.
Rust, aşağıdaki üç durumda uygun türleri ve trait uygulamalarını bulduğunda deref zorlaması gerçekleştirir:
T: Deref<Target=U>olduğunda&T’den&U’yaT: DerefMut<Target=U>olduğunda&mut T’den&mut U’yaT: Deref<Target=U>olduğunda&mut T’den&U’ya
İlk iki durum, ikincisinin değiştirilebilirlik içermesi dışında birbiriyle aynıdır. İlk durum, elinizde bir &T varsa ve T türü bir U türüne Deref uyguluyorsa şeffaf biçimde bir &U elde edebileceğinizi belirtir. İkinci durum, aynı deref zorlamasının değiştirilebilir referanslar için de gerçekleştiğini ifade eder.
Üçüncü durum ise daha inceliklidir: Rust, değiştirilebilir bir referansı da değiştirilemez bir referansa zorlayarak dönüştürebilir. Ancak bunun tersi mümkün değildir: Değiştirilemez referanslar asla değiştirilebilir referanslara dönüştürülemez. Ödünç alma kuralları gereği, elinizde değiştirilebilir bir referans varsa bu referans söz konusu veriye olan tek referans olmak zorundadır (aksi takdirde program derlenmezdi). Tek bir değiştirilebilir referansı tek bir değiştirilemez referansa dönüştürmek ödünç alma kurallarını asla ihlal etmez. Buna karşılık değiştirilemez bir referansı değiştirilebilir bir referansa dönüştürmek, başlangıçtaki değiştirilemez referansın o veriye olan tek değiştirilemez referans olmasını gerektirirdi; oysa ödünç alma kuralları bunu garanti etmez. Bu nedenle Rust, değiştirilemez bir referansı değiştirilebilir bir referansa dönüştürmenin mümkün olduğu varsayımında bulunamaz.
Not: Derleme zamanında sıfır çalışma zamanı maliyetiyle çözümlenen referans
dönüşümü kurallarını aşağıdaki tabloyla özetleyebiliriz (DerefMut trait’i,
üst trait olarak Deref’i zorunlu kılar; yani bir tür üzerinde DerefMut
uygulamak için önce Deref uygulanmalıdır):
| Kaynak Referans | Hedef Referans | Gerekli Trait Sınırı | İzin Durumu ve Nedeni |
|---|---|---|---|
&T | &U | T: Deref<Target = U> | Geçerli — paylaşımlı referanstan paylaşımlı referansa dönüşüm |
&mut T | &mut U | T: DerefMut<Target = U> | Geçerli — tekil (münhasır) erişim korunur |
&mut T | &U | T: Deref<Target = U> | Geçerli — tekil referans güvenle paylaşımlı referansa daraltılabilir |
&T | &mut U | — | Geçersiz — aynı anda başka &T referansları da bulunabileceğinden ödünç alma kurallarını bozar |