Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

RefCell<T> ve İçsel Değiştirilebilirlik Deseni

İçsel değiştirilebilirlik (interior mutability), bir veriye yönelik değiştirilemez referanslar varken bile o veriyi değiştirmenize olanak tanıyan bir Rust tasarım desenidir; normalde ödünç alma kuralları bu işleme izin vermez. Veriyi değiştirmek için bu desen, bir veri yapısının içinde unsafe (güvenli olmayan) kod kullanarak Rust’ın değiştirme ve ödünç almayı yöneten olağan kurallarını esnetir. Güvenli olmayan kod, kuralları bizim adımıza derleyicinin denetlemesine güvenmek yerine onları elle denetlediğimizi derleyiciye bildirir; güvenli olmayan kodu Bölüm 20’de daha ayrıntılı ele alacağız.

İçsel değiştirilebilirlik desenini kullanan türleri, derleyici bunu garanti edemese bile ödünç alma kurallarına çalışma zamanında uyulacağından emin olabildiğimiz durumlarda kullanabiliriz. İşin içindeki unsafe kod güvenli bir API ile sarmalanır ve dıştaki tür yine değiştirilemez kalır.

Bu kavramı, içsel değiştirilebilirlik desenini izleyen RefCell<T> türünü inceleyerek keşfedelim.

Ödünç Alma Kurallarını Çalışma Zamanında Uygulamak

Rc<T> türünden farklı olarak RefCell<T> türü, tuttuğu veri üzerinde tek sahipliği temsil eder. Peki RefCell<T> türünü Box<T> gibi bir türden farklı kılan nedir? Bölüm 4’te öğrendiğiniz ödünç alma kurallarını hatırlayın:

  • Herhangi bir anda ya tek bir değiştirilebilir referansa ya da istediğiniz sayıda değiştirilemez referansa sahip olabilirsiniz (ancak ikisine birden sahip olamazsınız).
  • Referanslar her zaman geçerli olmalıdır.

Referanslar ve Box<T> ile ödünç alma kurallarının değişmezleri (invariants) derleme zamanında uygulanır. RefCell<T> ile bu değişmezler çalışma zamanında uygulanır. Referanslarda bu kuralları çiğnerseniz bir derleyici hatası alırsınız. RefCell<T> ile bu kuralları çiğnerseniz programınız panikler ve sonlanır.

Ödünç alma kurallarını derleme zamanında denetlemenin avantajları, hataların geliştirme sürecinde daha erken yakalanması ve tüm analiz önceden tamamlandığı için çalışma zamanı performansı üzerinde hiçbir etkisinin olmamasıdır. Bu nedenlerle ödünç alma kurallarını derleme zamanında denetlemek vakaların büyük çoğunluğunda en iyi seçimdir; Rust’ta varsayılan yaklaşımın bu olmasının nedeni de budur.

Ödünç alma kurallarını bunun yerine çalışma zamanında denetlemenin avantajı ise derleme zamanı denetimlerinin izin vermeyeceği bazı bellek açısından güvenli senaryolara olanak tanımasıdır. Rust derleyicisi gibi statik analiz araçları doğaları gereği temkinlidir (conservative). Kodun bazı özelliklerini yalnızca kodu analiz ederek tespit etmek imkânsızdır: Bunun en ünlü örneği, bu kitabın kapsamı dışında kalan ancak araştırmak için ilginç bir konu olan Durma Problemi’dir (Halting Problem).

Bazı analizler imkânsız olduğundan, Rust derleyicisi kodun sahiplik kurallarına uyduğundan emin olamazsa doğru bir programı bile reddedebilir; işte bu açıdan temkinlidir. Rust hatalı bir programı kabul etseydi, kullanıcılar Rust’ın sunduğu güvencelere güvenemezdi. Buna karşılık Rust doğru bir programı reddederse programcı biraz zahmete girer, ancak felaketle sonuçlanacak hiçbir şey meydana gelmez. RefCell<T> türü, kodunuzun ödünç alma kurallarına uyduğundan emin olduğunuz, ancak derleyicinin bunu anlayıp garanti edemediği durumlarda kullanışlıdır.

RefCell<T> aynı anda birden fazla thread arasında paylaşılamaz; çünkü Sync trait’ini uygulamaz. Ancak T: Send olduğunda bir RefCell<T> değerinin sahipliği başka bir thread’e taşınabilir. Bu yönüyle ne Send ne de Sync olan Rc<T> türünden farklıdır. Thread’ler arasında paylaşılan veriyi değiştirmek için kullanabileceğimiz Mutex<T> gibi araçları Bölüm 16’da ele alacağız.

Box<T>, Rc<T> veya RefCell<T> türlerinden birini seçme nedenlerinin özeti şöyledir:

  • Rc<T> aynı verinin birden fazla sahibi olmasını sağlar; Box<T> ve RefCell<T> tek sahiplidir.
  • Box<T> derleme zamanında denetlenen değiştirilemez veya değiştirilebilir ödünç almalara izin verir; Rc<T> yalnızca derleme zamanında denetlenen değiştirilemez ödünç almalara izin verir; RefCell<T> ise çalışma zamanında denetlenen değiştirilemez veya değiştirilebilir ödünç almalara izin verir.
  • RefCell<T> çalışma zamanında denetlenen değiştirilebilir ödünç almalara izin verdiği için, RefCell<T> değerinin kendisi değiştirilemez olsa bile RefCell<T> içindeki değeri değiştirebilirsiniz.

Değiştirilemez bir değerin içindeki değeri değiştirmek, içsel değiştirilebilirlik desenidir. Şimdi içsel değiştirilebilirliğin yararlı olduğu bir duruma bakalım ve bunun nasıl mümkün olduğunu inceleyelim.

Not: Bölüm boyunca incelediğimiz üç temel akıllı işaretçi türünü (ve sıkça birlikte kullanılan Rc<RefCell<T>> bileşimini) tek bakışta şöyle karşılaştırabilirsiniz. Box<T> ve Rc<T> öbekte sahip olunan veriye erişim sağlarken, RefCell<T> tek başına ek bir öbek tahsisi yapmaz; içindeki T değerini ve ödünç sayacını doğrudan bulunduğu yerde (örneğin yığında veya bir struct/enum alanının içinde) sarmalar:

TürSahiplik Sayısıİzin Verilen Ödünç Alma TürüKural Denetim ZamanıKural İhlali Sonucu
Box<T>Tek sahipDeğiştirilemez (&T) veya değiştirilebilir (&mut T)Derleme zamanıDerleme hatası
Rc<T>Çoklu sahipYalnızca değiştirilemez (&T)Derleme zamanıDerleme hatası
RefCell<T>Tek sahipDeğiştirilemez (Ref<T>) veya değiştirilebilir (RefMut<T>)Çalışma zamanıÇalışma zamanında panic!
Rc<RefCell<T>>Çoklu sahipDeğiştirilemez veya değiştirilebilir (içteki RefCell<T> üzerinden)Çalışma zamanıÇalışma zamanında panic!

Copy olan küçük değerler (i32, bool gibi) için standart kütüphane, RefCell<T> yerine daha hafif olan Cell<T> türünü de sunar: Cell<T>, içteki değere referans vermek yerine get ile bir kopyasını döndürür ve set ile değeri yenisiyle değiştirir; dolayısıyla çalışma zamanında ödünç takibi yapmasına gerek kalmaz. Bu, Cell<T> üzerindeki bütün işlemlerin hiçbir koşulda paniklemeyeceği anlamına gelmez; örneğin eski bir değerin düşürülmesi o türün kendi Drop kodunu çalıştırabilir.

İçsel Değiştirilebilirlik Kullanmak

Ödünç alma kurallarının bir sonucu olarak, değiştirilemez bir değeriniz olduğunda onu değiştirilebilir biçimde ödünç alamazsınız. Örneğin şu kod derlenmeyecektir:

fn main() {
    let sayi = 5;
    let degistirilebilir_referans = &mut sayi;
}

Bu kodu derlemeye çalışırsanız aşağıdaki hatayı alırsınız:

$ cargo run
   Compiling odunc-alma v0.1.0 (file:///projects/odunc-alma)
error[E0596]: cannot borrow `sayi` as mutable, as it is not declared as mutable
 --> src/main.rs:3:37
  |
3 |     let degistirilebilir_referans = &mut sayi;
  |                                     ^^^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
2 |     let mut sayi = 5;
  |         +++

For more information about this error, try `rustc --explain E0596`.
error: could not compile `odunc-alma` (bin "odunc-alma") due to 1 previous error

Bununla birlikte, bir değerin kendi metotları içinde kendisini değiştirmesinin ancak diğer kodlara değiştirilemez görünmesinin yararlı olacağı durumlar vardır. Değerin kendi metotları dışındaki kodlar değeri değiştiremeyecektir. RefCell<T> kullanmak içsel değiştirilebilirlik yeteneği kazanmanın bir yoludur; ancak RefCell<T> ödünç alma kurallarını tamamen aşmaz: Derleyicideki ödünç alma denetleyicisi (borrow checker) bu içsel değiştirilebilirliğe izin verir ve ödünç alma kuralları bunun yerine çalışma zamanında denetlenir. Kuralları ihlal ederseniz derleyici hatası yerine bir panic! alırsınız.

Değiştirilemez bir değeri değiştirmek için RefCell<T> kullanabileceğimiz pratik bir örnek üzerinde çalışalım ve bunun neden yararlı olduğunu görelim.

Taklit Nesnelerle Test Yazmak

Test sırasında bir programcı bazen belirli bir davranışı gözlemlemek ve doğru uygulanıp uygulanmadığını doğrulamak amacıyla bir türün yerine başka bir tür kullanır. Bu yer tutucu türe test dublörü (test double) denir. Bunu film yapımcılığındaki dublör (stunt double) kavramı gibi düşünebilirsiniz: Özellikle zorlu bir sahneyi çekmek için bir kişinin oyuncunun yerine geçmesi gibi, test dublörleri de testleri çalıştırırken diğer türlerin yerine geçer. Taklit nesneler (mock objects) ise test sırasında neler olup bittiğini kaydeden özel test dublörü türleridir; böylece doğru eylemlerin gerçekleştiğini doğrulayabilirsiniz.

Rust’ta diğer dillerdeki anlamıyla nesneler yoktur ve bazı dillerin aksine Rust’ın standart kütüphanesinde yerleşik bir taklit nesne işlevselliği bulunmaz. Ancak taklit nesneyle aynı amaçlara hizmet edecek bir struct’ı rahatlıkla oluşturabilirsiniz.

Test edeceğimiz senaryo şudur: Bir değeri bir üst sınıra karşı izleyen ve mevcut değerin üst sınıra ne kadar yaklaştığına bağlı olarak iletiler gönderen bir kütüphane oluşturacağız. Bu kütüphane, örneğin bir kullanıcının yapmasına izin verilen API çağrısı sayısına ilişkin kotasını takip etmek için kullanılabilir.

Kütüphanemiz yalnızca bir değerin üst sınıra ne kadar yakın olduğunu takip etme ve hangi durumlarda hangi iletilerin gönderilmesi gerektiğini belirleme işlevselliğini sunacaktır. Kütüphanemizi kullanan uygulamaların ise iletileri gönderme mekanizmasını sağlaması beklenecektir: Uygulama iletiyi doğrudan kullanıcıya gösterebilir, e-posta gönderebilir, kısa mesaj atabilir veya başka bir şey yapabilir. Kütüphanenin bu ayrıntıyı bilmesine gerek yoktur. Tek ihtiyacı olan, bizim sağlayacağımız Iletici adlı trait’i uygulayan bir türdür. Liste 15-20 bu kütüphane kodunu göstermektedir.

Dosya adı: src/lib.rs
pub trait Iletici {
    fn gonder(&self, ileti: &str);
}

pub struct SinirIzleyici<'a, T: Iletici> {
    iletici: &'a T,
    deger: usize,
    en_buyuk: usize,
}

impl<'a, T> SinirIzleyici<'a, T>
where
    T: Iletici,
{
    pub fn yeni(iletici: &'a T, en_buyuk: usize) -> SinirIzleyici<'a, T> {
        SinirIzleyici {
            iletici,
            deger: 0,
            en_buyuk,
        }
    }

    pub fn deger_ata(&mut self, deger: usize) {
        self.deger = deger;

        let en_buyugun_yuzdesi = self.deger as f64 / self.en_buyuk as f64;

        if en_buyugun_yuzdesi >= 1.0 {
            self.iletici.gonder("Hata: Kotanızı aştınız!");
        } else if en_buyugun_yuzdesi >= 0.9 {
            self.iletici
                .gonder("Acil uyarı: Kotanızın %90'ını geçtiniz!");
        } else if en_buyugun_yuzdesi >= 0.75 {
            self.iletici.gonder("Uyarı: Kotanızın %75'ini geçtiniz!");
        }
    }
}
Liste 15-20: Bir değerin üst sınıra ne kadar yaklaştığını takip eden ve belirli seviyelerde uyarı veren bir kütüphane

Bu kodun önemli bir parçası, Iletici trait’inin self’e değiştirilemez bir referans ve iletinin metnini alan gonder adında tek bir metoda sahip olmasıdır. Bu trait, taklit nesnemizin gerçek bir nesneyle aynı şekilde kullanılabilmesi için uygulaması gereken arayüzdür. Diğer önemli parça ise SinirIzleyici üzerindeki deger_ata metodunun davranışını test etmek istememizdir. deger parametresi için geçirdiğimiz sayıyı değiştirebiliriz, ancak deger_ata üzerinde doğrulama (assertion) yapabileceğimiz hiçbir şey döndürmez. Iletici trait’ini uygulayan bir tür ve belirli bir en_buyuk değeriyle bir SinirIzleyici oluşturduğumuzda, deger için farklı sayılar geçirdiğimizde ileticiye uygun iletileri göndermesinin söylendiğini doğrulayabilmek istiyoruz.

gonder metodunu çağırdığımızda e-posta veya kısa mesaj göndermek yerine yalnızca göndermesi söylenen iletilerin kaydını tutacak bir taklit nesneye ihtiyacımız var. Taklit nesnenin yeni bir örneğini oluşturabilir, bu taklit nesneyi kullanan bir SinirIzleyici yaratabilir, SinirIzleyici üzerinde deger_ata metodunu çağırabilir ve ardından taklit nesnenin beklediğimiz iletilere sahip olup olmadığını kontrol edebiliriz. Liste 15-21 tam olarak bunu yapacak bir taklit nesne uygulama girişimini göstermektedir, ancak ödünç alma denetleyicisi buna izin vermez.

Dosya adı: src/lib.rs
pub trait Iletici {
    fn gonder(&self, ileti: &str);
}

pub struct SinirIzleyici<'a, T: Iletici> {
    iletici: &'a T,
    deger: usize,
    en_buyuk: usize,
}

impl<'a, T> SinirIzleyici<'a, T>
where
    T: Iletici,
{
    pub fn yeni(iletici: &'a T, en_buyuk: usize) -> SinirIzleyici<'a, T> {
        SinirIzleyici {
            iletici,
            deger: 0,
            en_buyuk,
        }
    }

    pub fn deger_ata(&mut self, deger: usize) {
        self.deger = deger;

        let en_buyugun_yuzdesi = self.deger as f64 / self.en_buyuk as f64;

        if en_buyugun_yuzdesi >= 1.0 {
            self.iletici.gonder("Hata: Kotanızı aştınız!");
        } else if en_buyugun_yuzdesi >= 0.9 {
            self.iletici
                .gonder("Acil uyarı: Kotanızın %90'ını geçtiniz!");
        } else if en_buyugun_yuzdesi >= 0.75 {
            self.iletici.gonder("Uyarı: Kotanızın %75'ini geçtiniz!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct SahteIletici {
        gonderilen_iletiler: Vec<String>,
    }

    impl SahteIletici {
        fn yeni() -> SahteIletici {
            SahteIletici {
                gonderilen_iletiler: vec![],
            }
        }
    }

    impl Iletici for SahteIletici {
        fn gonder(&self, ileti: &str) {
            self.gonderilen_iletiler.push(String::from(ileti));
        }
    }

    #[test]
    fn yuzde_75_ustu_uyari_iletisi_gonderir() {
        let sahte_iletici = SahteIletici::yeni();
        let mut sinir_izleyici = SinirIzleyici::yeni(&sahte_iletici, 100);

        sinir_izleyici.deger_ata(80);

        assert_eq!(sahte_iletici.gonderilen_iletiler.len(), 1);
    }
}
Liste 15-21: Ödünç alma denetleyicisinin izin vermediği bir SahteIletici uygulaması denemesi

Bu test kodu, göndermesi söylenen iletileri takip etmek üzere String değerlerinden oluşan bir Vec tutan gonderilen_iletiler alanına sahip bir SahteIletici struct’ı tanımlar. Ayrıca boş bir ileti listesiyle başlayan yeni SahteIletici değerleri oluşturmayı kolaylaştırmak için yeni adında bir ilişkili fonksiyon tanımlarız. Ardından bir SinirIzleyici’ye SahteIletici verebilmek için SahteIletici üzerinde Iletici trait’ini uygularız. gonder metodunun tanımında ise parametre olarak geçirilen iletiyi alır ve SahteIletici içindeki gonderilen_iletiler listesinde saklarız.

Testte, SinirIzleyici’ye deger alanını en_buyuk değerinin yüzde 75’inden büyük bir sayıya ayarlaması söylendiğinde ne olduğunu sınıyoruz. İlk olarak, boş bir ileti listesiyle başlayacak yeni bir SahteIletici oluşturuyoruz. Ardından yeni bir SinirIzleyici oluşturuyor ve ona yeni SahteIletici’ye bir referans ile en_buyuk değeri olarak 100 veriyoruz. SinirIzleyici üzerindeki deger_ata metodunu, 100’ün yüzde 75’inden fazla olan 80 değeriyle çağırıyoruz. Sonrasında SahteIletici’nin kaydını tuttuğu ileti listesinde artık bir adet ileti bulunması gerektiğini doğruluyoruz.

Ancak aşağıda gösterildiği gibi bu testte bir sorun vardır:

$ cargo test
   Compiling sinir-izleyici v0.1.0 (file:///projects/sinir-izleyici)
error[E0596]: cannot borrow `self.gonderilen_iletiler` as mutable, as it is behind a `&` reference
  --> src/lib.rs:57:13
   |
57 |             self.gonderilen_iletiler.push(String::from(ileti));
   |             ^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be borrowed as mutable
   |
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
   |
 2 ~     fn gonder(&mut self, ileti: &str);
 3 | }
...
55 |     impl Iletici for SahteIletici {
56 ~         fn gonder(&mut self, ileti: &str) {
   |

For more information about this error, try `rustc --explain E0596`.
error: could not compile `sinir-izleyici` (lib test) due to 1 previous error

gonder metodu self’e değiştirilemez bir referans aldığı için iletileri kaydetmek üzere SahteIletici’yi değiştiremiyoruz. Hata metnindeki hem impl metodunda hem de trait tanımında &mut self kullanma önerisini de uygulayamayız. Yalnızca test uğruna Iletici trait’ini değiştirmek istemiyoruz. Bunun yerine, test kodumuzun mevcut tasarımımızla doğru biçimde çalışmasını sağlayacak bir yol bulmamız gerekir.

İşte bu, içsel değiştirilebilirliğin yardımcı olabileceği bir durumdur! gonderilen_iletiler alanını bir RefCell<T> içinde saklayacağız; böylece gonder metodu gördüğümüz iletileri saklamak üzere gonderilen_iletiler üzerinde değişiklik yapabilecektir. Liste 15-22 bunun nasıl göründüğünü göstermektedir.

Dosya adı: src/lib.rs
pub trait Iletici {
    fn gonder(&self, ileti: &str);
}

pub struct SinirIzleyici<'a, T: Iletici> {
    iletici: &'a T,
    deger: usize,
    en_buyuk: usize,
}

impl<'a, T> SinirIzleyici<'a, T>
where
    T: Iletici,
{
    pub fn yeni(iletici: &'a T, en_buyuk: usize) -> SinirIzleyici<'a, T> {
        SinirIzleyici {
            iletici,
            deger: 0,
            en_buyuk,
        }
    }

    pub fn deger_ata(&mut self, deger: usize) {
        self.deger = deger;

        let en_buyugun_yuzdesi = self.deger as f64 / self.en_buyuk as f64;

        if en_buyugun_yuzdesi >= 1.0 {
            self.iletici.gonder("Hata: Kotanızı aştınız!");
        } else if en_buyugun_yuzdesi >= 0.9 {
            self.iletici
                .gonder("Acil uyarı: Kotanızın %90'ını geçtiniz!");
        } else if en_buyugun_yuzdesi >= 0.75 {
            self.iletici.gonder("Uyarı: Kotanızın %75'ini geçtiniz!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct SahteIletici {
        gonderilen_iletiler: RefCell<Vec<String>>,
    }

    impl SahteIletici {
        fn yeni() -> SahteIletici {
            SahteIletici {
                gonderilen_iletiler: RefCell::new(vec![]),
            }
        }
    }

    impl Iletici for SahteIletici {
        fn gonder(&self, ileti: &str) {
            self.gonderilen_iletiler
                .borrow_mut()
                .push(String::from(ileti));
        }
    }

    #[test]
    fn yuzde_75_ustu_uyari_iletisi_gonderir() {
        // --snip--
        let sahte_iletici = SahteIletici::yeni();
        let mut sinir_izleyici = SinirIzleyici::yeni(&sahte_iletici, 100);

        sinir_izleyici.deger_ata(80);

        assert_eq!(sahte_iletici.gonderilen_iletiler.borrow().len(), 1);
    }
}
Liste 15-22: Dıştaki değer değiştirilemez kabul edilirken içteki değeri değiştirmek için RefCell<T> kullanmak

gonderilen_iletiler alanı artık Vec<String> yerine RefCell<Vec<String>> türündedir. yeni fonksiyonunda boş vektörün etrafında yeni bir RefCell<Vec<String>> örneği oluşturuyoruz.

gonder metodunun uygulamasında ilk parametre hâlâ self’in değiştirilemez ödünç alımıdır; bu da trait tanımıyla eşleşir. self.gonderilen_iletiler içindeki RefCell<Vec<String>> üzerinde borrow_mut çağırarak RefCell<Vec<String>> içindeki değere — yani vektöre — değiştirilebilir bir referans elde ederiz. Ardından test sırasında gönderilen iletilerin kaydını tutmak için vektöre olan değiştirilebilir referans üzerinde push çağırabiliriz.

Yapmamız gereken son değişiklik doğrulama satırındadır: İçteki vektörde kaç öğe bulunduğunu görmek için RefCell<Vec<String>> üzerinde borrow çağırarak vektöre değiştirilemez bir referans alırız.

RefCell<T> türünün nasıl kullanıldığını gördüğünüze göre, şimdi nasıl çalıştığının ayrıntılarına inelim!

Ödünç Almaları Çalışma Zamanında İzlemek

Değiştirilemez ve değiştirilebilir referanslar oluştururken sırasıyla & ve &mut sözdizimini kullanırız. RefCell<T> ile ise RefCell<T> türüne ait güvenli API’nin bir parçası olan borrow ve borrow_mut metotlarını kullanırız. borrow metodu Ref<T> akıllı işaretçi türünü, borrow_mut metodu ise RefMut<T> akıllı işaretçi türünü döndürür. Her iki tür de Deref trait’ini uyguladığından onlara normal referanslar gibi davranabiliriz.

RefCell<T>, o anda kaç tane Ref<T> ve RefMut<T> akıllı işaretçisinin etkin olduğunu takip eder. borrow metodunu her çağırdığımızda RefCell<T>, etkin olan değiştirilemez ödünç almaların sayısını artırır. Bir Ref<T> değeri kapsam dışına çıktığında değiştirilemez ödünç almaların sayısı 1 azalır. Tıpkı derleme zamanı ödünç alma kuralları gibi, RefCell<T> de herhangi bir anda çok sayıda değiştirilemez ödünç almaya veya tek bir değiştirilebilir ödünç almaya sahip olmamıza izin verir.

Bu kuralları ihlal etmeye çalışırsak, referanslarda olacağı gibi bir derleyici hatası almak yerine RefCell<T> uygulaması çalışma zamanında panikleyecektir. Liste 15-23, Liste 15-22’deki gonder uygulamasının değiştirilmiş bir hâlini göstermektedir. RefCell<T> türünün çalışma zamanında bunu yapmamızı engellediğini göstermek amacıyla aynı kapsamda etkin iki değiştirilebilir ödünç almayı bilerek oluşturmaya çalışıyoruz.

Dosya adı: src/lib.rs
pub trait Iletici {
    fn gonder(&self, ileti: &str);
}

pub struct SinirIzleyici<'a, T: Iletici> {
    iletici: &'a T,
    deger: usize,
    en_buyuk: usize,
}

impl<'a, T> SinirIzleyici<'a, T>
where
    T: Iletici,
{
    pub fn yeni(iletici: &'a T, en_buyuk: usize) -> SinirIzleyici<'a, T> {
        SinirIzleyici {
            iletici,
            deger: 0,
            en_buyuk,
        }
    }

    pub fn deger_ata(&mut self, deger: usize) {
        self.deger = deger;

        let en_buyugun_yuzdesi = self.deger as f64 / self.en_buyuk as f64;

        if en_buyugun_yuzdesi >= 1.0 {
            self.iletici.gonder("Hata: Kotanızı aştınız!");
        } else if en_buyugun_yuzdesi >= 0.9 {
            self.iletici
                .gonder("Acil uyarı: Kotanızın %90'ını geçtiniz!");
        } else if en_buyugun_yuzdesi >= 0.75 {
            self.iletici.gonder("Uyarı: Kotanızın %75'ini geçtiniz!");
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct SahteIletici {
        gonderilen_iletiler: RefCell<Vec<String>>,
    }

    impl SahteIletici {
        fn yeni() -> SahteIletici {
            SahteIletici {
                gonderilen_iletiler: RefCell::new(vec![]),
            }
        }
    }

    impl Iletici for SahteIletici {
        fn gonder(&self, ileti: &str) {
            let mut ilk_odunc = self.gonderilen_iletiler.borrow_mut();
            let mut ikinci_odunc = self.gonderilen_iletiler.borrow_mut();

            ilk_odunc.push(String::from(ileti));
            ikinci_odunc.push(String::from(ileti));
        }
    }

    #[test]
    fn yuzde_75_ustu_uyari_iletisi_gonderir() {
        let sahte_iletici = SahteIletici::yeni();
        let mut sinir_izleyici = SinirIzleyici::yeni(&sahte_iletici, 100);

        sinir_izleyici.deger_ata(80);

        assert_eq!(sahte_iletici.gonderilen_iletiler.borrow().len(), 1);
    }
}
Liste 15-23: RefCell<T> türünün panikleyeceğini görmek için aynı kapsamda iki değiştirilebilir referans oluşturmak

borrow_mut çağrısından dönen RefMut<T> akıllı işaretçisi için ilk_odunc değişkenini oluşturuyoruz. Ardından ikinci_odunc değişkeninde aynı şekilde başka bir değiştirilebilir ödünç alma daha oluşturuyoruz. Bu durum aynı kapsamda iki değiştirilebilir referans oluşturur ki buna izin verilmez. Kütüphanemiz için testleri çalıştırdığımızda Liste 15-23’teki kod hiçbir hata vermeden derlenecek, ancak test başarısız olacaktır:

$ cargo test
   Compiling sinir-izleyici v0.1.0 (file:///projects/sinir-izleyici)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
     Running unittests src/lib.rs (target/debug/deps/sinir_izleyici-e599811fa246dbde)

running 1 test
test tests::yuzde_75_ustu_uyari_iletisi_gonderir ... FAILED

failures:

---- tests::yuzde_75_ustu_uyari_iletisi_gonderir stdout ----

thread 'tests::yuzde_75_ustu_uyari_iletisi_gonderir' (6028024) panicked at src/lib.rs:59:61:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::yuzde_75_ustu_uyari_iletisi_gonderir

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Kodun RefCell already borrowed (önceki Rust sürümlerinde already borrowed: BorrowMutError) mesajıyla paniklediğine dikkat edin. RefCell<T>, ödünç alma kurallarının ihlallerini çalışma zamanında işte bu şekilde ele alır.

Burada yaptığımız gibi ödünç alma hatalarını derleme zamanı yerine çalışma zamanında yakalamayı seçmek, kodunuzdaki hataları geliştirme sürecinin daha ileriki aşamalarında — belki de kodunuz canlı ortama (production) alınana dek — fark edemeyebileceğiniz anlamına gelir. Ayrıca ödünç almaları derleme zamanı yerine çalışma zamanında takip etmenin bir sonucu olarak kodunuz küçük bir çalışma zamanı performans maliyeti üstlenir. Bununla birlikte RefCell<T> kullanmak, yalnızca değiştirilemez değerlere izin verilen bir bağlamda kullanılırken bile gördüğü iletilerin kaydını tutmak üzere kendisini değiştirebilen bir taklit nesne yazmayı mümkün kılar. Bu ödünleşimlere rağmen, normal referansların sunduğundan daha fazla işlevsellik elde etmek için RefCell<T> kullanabilirsiniz.

Not: Çalışma zamanında ödünç çakışması ihtimali bulunan durumlarda programın panic! ile çökmesini istemiyorsanız, RefCell<T> türünün paniklemeyen alternatifleri olan try_borrow() ve try_borrow_mut() metotlarını kullanabilirsiniz:

  • try_borrow(), değer zaten değiştirilebilir olarak ödünç alınmışsa paniklemek yerine Err(BorrowError) döndüren bir Result<Ref<'_, T>, BorrowError> üretir.
  • try_borrow_mut(), değer o anda değiştirilemez veya değiştirilebilir olarak ödünç alınmışsa Err(BorrowMutError) döndüren bir Result<RefMut<'_, T>, BorrowMutError> üretir.

Ayrıca Ref<T> ve RefMut<T> akıllı işaretçileri ödünç kilidini kapsam sonuna kadar tuttuğu için, aynı fonksiyon içinde art arda ödünç almanız gerektiğinde ilk ödüncü bir iç blok ({ ... }) içinde sınırlayabilir ya da bu bölümde daha önce gördüğünüz drop(ilk_odunc) çağrısıyla erken bırakabilirsiniz.

Değiştirilebilir Verinin Birden Fazla Sahibi Olmasına İzin Vermek

RefCell<T> kullanmanın yaygın bir yolu, onu Rc<T> ile birleştirmektir. Rc<T> türünün bir verinin birden fazla sahibi olmasına izin verdiğini, ancak o veriye yalnızca değiştirilemez erişim sağladığını hatırlayın. İçinde bir RefCell<T> tutan bir Rc<T> değeriniz varsa, hem birden fazla sahibi olabilen hem de değiştirebileceğiniz bir değer elde edersiniz!

Örneğin, birden fazla listenin başka bir listenin sahipliğini paylaşmasına olanak tanımak için Rc<T> kullandığımız Liste 15-18’deki cons listesi örneğini hatırlayın. Rc<T> yalnızca değiştirilemez değerler tuttuğu için, listeyi oluşturduktan sonra içindeki değerlerin hiçbirini değiştiremeyiz. Listelerdeki değerleri değiştirebilme yeteneği için işin içine RefCell<T> ekleyelim. Liste 15-24, Dugum tanımında bir RefCell<T> kullanarak tüm listelerde saklanan değeri değiştirebileceğimizi göstermektedir.

Dosya adı: src/main.rs
#[derive(Debug)]
enum Liste {
    Dugum(Rc<RefCell<i32>>, Rc<Liste>),
    Bos,
}

use crate::Liste::{Bos, Dugum};
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let deger = Rc::new(RefCell::new(5));

    let a = Rc::new(Dugum(Rc::clone(&deger), Rc::new(Bos)));

    let b = Dugum(Rc::new(RefCell::new(3)), Rc::clone(&a));
    let c = Dugum(Rc::new(RefCell::new(4)), Rc::clone(&a));

    *deger.borrow_mut() += 10;

    println!("a sonrası = {a:?}");
    println!("b sonrası = {b:?}");
    println!("c sonrası = {c:?}");
}
Liste 15-24: Değiştirebileceğimiz bir Liste oluşturmak için Rc<RefCell<i32>> kullanmak

Rc<RefCell<i32>> örneği olan bir değer oluşturuyor ve daha sonra doğrudan erişebilmek için onu deger adlı bir değişkende saklıyoruz. Ardından a içinde deger’i tutan bir Dugum varyantıyla bir Liste oluşturuyoruz. Sahipliği deger’den a’ya devretmek ya da a’nın deger’den ödünç almasını sağlamak yerine hem a’nın hem de deger’in içteki 5 değerinin sahipliğine sahip olması için deger’i klonlamamız gerekir.

Sonrasında b ve c listelerini oluşturduğumuzda her ikisinin de a’ya başvurabilmesi için a listesini de bir Rc<T> içine sarıyoruz; bu, tıpkı Liste 15-18’de yaptığımız işlemdir.

a, b ve c içindeki listeleri oluşturduktan sonra deger içindeki sayıya 10 eklemek istiyoruz. Bunu deger üzerinde borrow_mut çağırarak yapıyoruz; bu çağrı, Rc<T> türünün başvurusunu içteki RefCell<T> değerine çözmek için Bölüm 5’teki “-> Operatörü Nerede?” başlığında ele aldığımız otomatik başvuru çözme özelliğini kullanır. borrow_mut metodu bir RefMut<T> akıllı işaretçisi döndürür; biz de onun üzerinde başvuru çözme operatörünü (*) kullanarak içteki değeri değiştiririz.

a, b ve c’yi yazdırdığımızda hepsinin 5 yerine değiştirilmiş 15 değerine sahip olduğunu görebiliriz:

$ cargo run
   Compiling kons-liste v0.1.0 (file:///projects/kons-liste)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
     Running `target/debug/kons-liste`
a sonrası = Dugum(RefCell { value: 15 }, Bos)
b sonrası = Dugum(RefCell { value: 3 }, Dugum(RefCell { value: 15 }, Bos))
c sonrası = Dugum(RefCell { value: 4 }, Dugum(RefCell { value: 15 }, Bos))

Bu teknik oldukça zariftir! RefCell<T> kullanarak dışarıdan bakıldığında değiştirilemez bir Liste değerine sahip oluruz. Ancak verimizi değiştirmemiz gerektiğinde içsel değiştirilebilirliğine erişim sağlayan RefCell<T> metotlarını kullanabiliriz. Ödünç alma kurallarının çalışma zamanı denetimleri bizi veri yarışlarından (data race) korur ve veri yapılarımızda bu esnekliği kazanmak için bazen bir miktar hızdan feragat etmeye değer. RefCell<T> türünün çok thread’li kodlarda çalışmadığını unutmayın! Mutex<T>, RefCell<T> türünün thread güvenli (thread-safe) karşılığıdır ve Mutex<T> türünü Bölüm 16’da ele alacağız.