RefCell<T> ve İçsel Değiştirilebilirlik Deseni
İçsel değiştirilebilirlik (interior mutability), elde yalnızca değiştirilemez
referanslar olsa bile veriyi değiştirmeye izin veren bir Rust tasarım desenidir.
Normalde ödünç alma kuralları buna izin vermez. Bu desen, değişiklik ve ödünç
alma kurallarını yöneten normal Rust davranışını esnetmek için veri yapısının
içinde unsafe kod kullanır. Unsafe, kuralları derleyiciye değil bizim
manuel denetlediğimizi söyler; ayrıntısını 20. bölümde göreceğiz.
Bu deseni kullanan türleri ancak ödünç alma kurallarının çalışma zamanında da
izleneceğinden eminseniz kullanmalısınız. İçteki unsafe kod güvenli bir API
arkasına saklanır; dışarıdan bakınca tür yine değiştirilemez görünür.
Bu fikri, içsel değiştirilebilirliği kullanan RefCell<T> türü üzerinden
inceleyelim.
Ödünç Alma Kurallarını Çalışma Zamanında Uygulamak
Rc<T>den farklı olarak RefCell<T>, tuttuğu veri üzerinde tek sahipliği
temsil eder. Peki onu Box<T>den ayıran nedir? 4. bölümdeki ödünç alma
kurallarını hatırlayın:
- Aynı anda ya tek bir değiştirilebilir referansınız olabilir ya da istediğiniz kadar değiştirilemez referansınız olabilir; ikisi bir arada olamaz.
- Referanslar her zaman geçerli olmalıdır.
Referanslar ve Box<T> ile bu kuralların değişmezleri derleme zamanında
uygulanır. RefCell<T> ileyse çalışma zamanında uygulanır. Referanslarla
kuralları bozarsanız derleyici hata verir. RefCell<T> ile bozarsanız program
panic! ile kapanır.
Derleme zamanında denetlemenin avantajı, hataların daha erken yakalanması ve çalışma zamanı maliyeti olmamasıdır. Bu yüzden Rust’ta varsayılan yaklaşım budur.
Çalışma zamanında denetlemenin avantajıysa, derleme zamanında fazla katı kalan denetimlerin reddedeceği bazı bellek-güvenli senaryoları mümkün kılmasıdır. Rust derleyicisi gibi durağan analiz araçları doğaları gereği temkinlidir. Bazı özellikleri yalnızca kodu analiz ederek belirlemek imkânsızdır; en bilinen örnek Duruş Problemi’dir.
Bu yüzden derleyici kurallara uyulduğundan emin değilse, doğru bir programı
bile reddedebilir. Bu rahatsız edicidir ama felaket değildir. Buna karşılık
yanlış programı kabul etseydi, Rust’ın verdiği güvencelere güvenemezdik.
RefCell<T>, kurallara uyduğunuzdan emin olduğunuz ama derleyicinin bunu
kanıtlayamadığı durumlarda işe yarar.
RefCell<T> de Rc<T> gibi yalnızca tek iş parçacıklı kullanım içindir.
Çok iş parçacıklı bağlamda kullanırsanız derleme hatası alırsınız. 16. bölümde,
aynı işlevselliğin çok iş parçacıklı sürümünü göreceğiz.
Box<T>, Rc<T> ve RefCell<T> arasında seçim yaparken akılda tutulacak kısa
özet şöyledir:
Rc<T>aynı verinin birden çok sahibi olmasına izin verir;Box<T>veRefCell<T>tek sahiplidir.Box<T>, derleme zamanında denetlenen değiştirilemez ya da değiştirilebilir ödünçler sunar;Rc<T>yalnızca derleme zamanında denetlenen değiştirilemez ödünçler sunar;RefCell<T>ise çalışma zamanında denetlenen her iki türü de sunar.RefCell<T>çalışma zamanında denetlenen değiştirilebilir ödünçlere izin verdiği için, kendisi değiştirilemez olsa bile içindeki değeri değiştirebilirsiniz.
Değiştirilemez bir değerin içindeki veriyi değiştirmek, işte bu içsel değiştirilebilirlik desenidir.
İçsel Değiştirilebilirlik Kullanmak
Ödünç alma kurallarının sonucu olarak, elinizde değiştirilemez bir değer varken onu değiştirilebilir olarak ödünç alamazsınız. Örneğin şu kod derlenmez:
fn main() {
let sayi = 5;
let degistirilebilir_referans = &mut sayi;
}
Derlerseniz şu 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:13
|
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, bazı durumlarda bir değerin kendi metotları içinde kendini
değiştirmesi ama dış dünyaya değiştirilemez görünmesi faydalıdır. RefCell<T>
bunu yapmanın yollarından biridir. Tam anlamıyla kurallardan kaçmaz; yalnızca
kontrolü derleme zamanından çalışma zamanına taşır. Kuralı ihlal ederseniz
derleme hatası değil panic! alırsınız.
Şimdi RefCell<T>yi gerçekten işimize yarayan bir örnek üzerinde görelim.
Sahte Nesnelerle Test Yazmak
Test sırasında programcı bazen bir türün yerine başka bir tür kullanır; amaç belirli davranışı gözlemek ve doğru uygulanıp uygulanmadığını denetlemektir. Bu geçici türe test double denir. Bunun özel bir biçimi olan mock object, test boyunca neler olduğunu kaydeder; böylece doğru eylemlerin gerçekleşip gerçekleşmediğini doğrulayabilirsiniz.
Rust’ta bazı dillerdeki anlamıyla nesne yoktur ve standart kütüphanede hazır mock altyapısı gelmez. Ama aynı işi görecek struct’ları rahatlıkla tanımlayabilirsiniz.
Şu senaryoyu test edelim: bir değerin üst sınıra ne kadar yaklaştığını izleyen ve mevcut değerin sınıra yaklaşmasına göre mesaj gönderen bir kütüphane yazacağız. Örneğin bir kullanıcının yapabileceği API çağrısı kotasını izlemek için kullanılabilir.
Bu kütüphane yalnızca sınıra yakınlığı ve hangi eşiklerde hangi mesajın
gönderileceğini bilir. Mesajların nasıl gönderileceğini ise kütüphaneyi kullanan
uygulama sağlayacaktır. Bunun için Iletici adlı bir trait tanımlıyoruz.
Liste 15-20 kütüphane kodunu gösterir.
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: Kotanizi astiniz!");
} else if en_buyugun_yuzdesi >= 0.9 {
self.iletici
.gonder("Acil uyari: Kotanizin %90'ini gectiniz!");
} else if en_buyugun_yuzdesi >= 0.75 {
self.iletici.gonder("Uyari: Kotanizin %75'ini gectiniz!");
}
}
}
Buradaki önemli noktalardan biri, Iletici trait’inin selfi değiştirilemez
referans alan gonder metodunu tanımlamasıdır. Sahte nesnemiz, gerçek nesneyle
aynı şekilde kullanılabilmek için bu arayüzü uygulamalıdır. İkinci önemli nokta
ise SinirIzleyici üzerindeki deger_ata davranışını test etmek istememizdir.
deger parametresine verdiğimiz şeyi değiştirebiliriz ama deger_ata bize
doğrulama yapacağımız bir sonuç döndürmez. Biz de “belirli bir en_buyuk
değeriyle oluşturulmuş SinirIzleyici, farklı sayılar verildiğinde doğru
iletileri gönderiyor mu?” sorusunu sınamak isteriz.
Gerçekten e-posta ya da mesaj göndermek yerine, yalnızca gönderilmesi istenen
mesajları kaydeden bir sahte nesneye ihtiyacımız var. Sahte nesnenin örneğini
oluşturup SinirIzleyiciye verecek, sonra deger_ata çağıracak ve sonrasında
beklediğimiz mesajların kaydedilip kaydedilmediğine bakacağız. Liste 15-21 bu
yönde bir girişimi gösteriyor; ama ödünç alma denetleyicisi buna izin vermiyor.
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: Kotanizi astiniz!");
} else if en_buyugun_yuzdesi >= 0.9 {
self.iletici
.gonder("Acil uyari: Kotanizin %90'ini gectiniz!");
} else if en_buyugun_yuzdesi >= 0.75 {
self.iletici.gonder("Uyari: Kotanizin %75'ini gectiniz!");
}
}
}
#[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);
}
}
SahteIletici gerceklemesi denemesiBu test kodu, gönderilen iletileri tutmak için Vec<String> kullanan
SahteIletici yapısını tanımlar. Boş ileti listesiyle başlayan örnekler
oluşturmayı kolaylaştırmak için yeni ilişkili fonksiyonunu da ekleriz.
Ardından Iletici trait’ini uygularız.
Testte, SinirIzleyiciye deger olarak 80 verdiğimizde, yani 100lük
sınırın yüzde 75’ini geçtiğimizde ne olduğunu sınarız. Önce yeni bir
SahteIletici, sonra ona referans verilen bir SinirIzleyici oluştururuz.
deger_ata(80) çağırdıktan sonra, sahte ileticinin bir mesaj kaydetmiş olmasını
bekleriz.
Ama burada bir sorun var:
$ 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:58:13
|
58 | self.gonderilen_iletiler.push(String::from(ileti));
| ^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to 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 | }
...
56 | impl Iletici for SahteIletici {
57 ~ 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, selfi değiştirilemez referans aldığı için SahteIletici içindeki
ileti listesini değiştiremiyoruz. Hata mesajının önerdiği gibi trait’i ve
uygulamayı &mut self yapamayız; çünkü sırf test kolaylığı için Iletici
trait’ini değiştirmek istemiyoruz.
İşte burada içsel değiştirilebilirlik devreye girer. gonderilen_iletiler
alanını RefCell<T> içine alırız; böylece gonder metodu self
değiştirilemez referans alsa bile, içerideki veriyi değiştirebilir. Liste 15-22
bunu gösterir.
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: Kotanizi astiniz!");
} else if en_buyugun_yuzdesi >= 0.9 {
self.iletici
.gonder("Acil uyari: Kotanizin %90'ini gectiniz!");
} else if en_buyugun_yuzdesi >= 0.75 {
self.iletici.gonder("Uyari: Kotanizin %75'ini gectiniz!");
}
}
}
#[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);
}
}
RefCell<T> kullanmakgonderilen_iletiler alanı artık Vec<String> değil
RefCell<Vec<String>>dir. yeni içinde boş vektörün etrafına yeni bir
RefCell örneği sararız.
gonder uygulamasında ilk parametre hâlâ selfin değiştirilemez ödüncüdür;
trait tanımıyla uyumludur. self.gonderilen_iletiler üzerinde borrow_mut
çağırarak içteki vektöre değiştirilebilir erişim alır, ardından push
kullanarak iletiyi kaydederiz.
Doğrulamada da vektörün boyuna bakmak için borrow çağırıp değiştirilemez
referans alırız.
Ödünçleri Çalışma Zamanında İzlemek
Normal referanslarda & ve &mut kullanırız. RefCell<T> ileyse borrow ve
borrow_mut kullanırız. borrow, Ref<T>; borrow_mut ise RefMut<T>
döndürür. Her iki tür de Deref uyguladığı için normal referanslar gibi
davranabilir.
RefCell<T>, o anda etkin olan Ref<T> ve RefMut<T> akıllı işaretçilerinin
sayısını izler. borrow her çağrıldığında değiştirilemez ödünç sayısını
artırır. Ref<T> kapsam dışına çıkınca sayı bir azalır. Tıpkı derleme zamanı
kuralları gibi, RefCell<T> de aynı anda çok sayıda değiştirilemez ödünç ya da
yalnızca tek değiştirilebilir ödünç olmasına izin verir.
Kural ihlali yaparsak, referanslarda olduğu gibi derleme hatası değil çalışma
zamanında panic! alırız. Liste 15-23, Liste 15-22’deki gonder
uygulamasının bilerek bozulmuş sürümüdür: aynı kapsam içinde iki
değiştirilebilir ödünç oluşturmaya çalışıyoruz.
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: Kotanizi astiniz!");
} else if en_buyugun_yuzdesi >= 0.9 {
self.iletici
.gonder("Acil uyari: Kotanizin %90'ini gectiniz!");
} else if en_buyugun_yuzdesi >= 0.75 {
self.iletici.gonder("Uyari: Kotanizin %75'ini gectiniz!");
}
}
}
#[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);
}
}
RefCell<T>nin panic vermesini gormekÖnce borrow_mutten dönen RefMut<T> için ilk_odunc değişkenini, ardından
aynı kapsamda ikinci bir RefMut<T> için ikinci_oduncu oluşturuyoruz. Bu,
aynı kapsamda iki değiştirilebilir referans demektir ve yasaktır. Kod derlenir
ama test başarısız olur:
$ 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::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
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 already borrowed: BorrowMutError mesajıyla paniklediğine dikkat edin.
RefCell<T>, kuralları çalışma zamanında işte böyle uygular.
Bu yaklaşımın bedeli vardır: hataları geliştirme sürecinde daha geç fark
edebilirsiniz ve çalışma zamanında küçük de olsa ek maliyet oluşur. Buna
rağmen, yalnızca değiştirilemez değerlerin izin verildiği bağlamda kendini
değiştirebilen sahte nesneler yazmak gibi durumlarda RefCell<T> çok işe yarar.
Değiştirilebilir Verinin Birden Fazla Sahibi Olmasına İzin Vermek
RefCell<T> çok sık Rc<T> ile birlikte kullanılır. Rc<T>, verinin birden
çok sahibi olmasına izin verir ama yalnızca değiştirilemez erişim sunar. Eğer
Rc<T> içinde RefCell<T> taşırsanız, hem birden çok sahipliğe hem de
değiştirilebilirliğe sahip olursunuz.
Liste 15-18’de Rc<T> kullanarak bir listenin birden çok yerde
paylaşılabildiğini görmüştük. Ama Rc<T> yalnızca değiştirilemez değerleri
tuttuğu için, liste oluştuktan sonra içindeki değerleri değiştiremiyorduk.
Şimdi RefCell<T> ekleyerek bunu mümkün kılacağız. Liste 15-24 bunu gösterir.
#[derive(Debug)]
enum List {
Dugum(Rc<RefCell<i32>>, Rc<List>),
Bos,
}
use crate::List::{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(°er), 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 olusturmak icin Rc<RefCell<i32>> kullanmakÖnce Rc<RefCell<i32>> türünden deger oluşturuyoruz ki sonra doğrudan
erişebilelim. Ardından a listesini, degeri taşıyan Dugum varyantıyla
kuruyoruz. Burada degeri klonlamamız gerekir; böylece içteki 5in sahipliği
hem degerde hem ada olur.
a listesini Rc<T> içine sarıyoruz ki b ve c oluşturulurken ikisi de
ayı işaret edebilsin.
Listeler kurulduktan sonra degere 10 eklemek istiyoruz. Bunun için
borrow_mut çağırıyoruz. Rust’ın otomatik başvuru çözmesi, Rc<T>yi içteki
RefCell<T>ye indirger. borrow_mut, RefMut<T> döndürür; biz de bunu
çözerek iç değeri değiştiririz.
a, b ve cyi yazdırdığımızda hepsinin artık 15 içerdiğini görürüz:
$ 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 sonrasi = Dugum(RefCell { value: 15 }, Bos)
b sonrasi = Dugum(RefCell { value: 3 }, Dugum(RefCell { value: 15 }, Bos))
c sonrasi = Dugum(RefCell { value: 4 }, Dugum(RefCell { value: 15 }, Bos))
Bu teknik oldukça kullanışlıdır. Dışarıdan bakınca değiştirilemez bir Liste
gibi görünür; ama RefCell<T>nin sunduğu API ile gerektiğinde iç veriyi
değiştirebiliriz. Çalışma zamanındaki ödünç denetimi veri yarışlarını önler;
bazı veri yapılarında biraz performans kaybına karşılık bu esneklik gayet
değerlidir. Elbette RefCell<T> çok iş parçacıklı kodda çalışmaz; onun güvenli
karşılığı olan Mutex<T>yi 16. bölümde göreceğiz.