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

Rc<T>, Referans Sayılı Akıllı İşaretçi

Çoğu durumda sahiplik nettir: Belirli bir değerin sahibinin hangi değişken olduğunu tam olarak bilirsiniz. Bununla birlikte, tek bir değerin birden fazla sahibi olabileceği durumlar da vardır. Örneğin graf veri yapılarında birden fazla kenar (edge) aynı düğüme (node) işaret edebilir ve bu düğüm kavramsal olarak ona işaret eden kenarların tümü tarafından sahiplenilir. Bir düğüm, kendisine işaret eden hiçbir kenar kalmadıkça ve dolayısıyla hiçbir sahibi olmadıkça bellekten temizlenmemelidir.

Çoklu sahipliği, referans sayımı (reference counting) ifadesinin kısaltması olan Rust’ın Rc<T> türünü kullanarak açıkça etkinleştirmeniz gerekir. Rc<T> türü, bir değerin hâlâ kullanımda olup olmadığını belirlemek için o değere olan referansların sayısını takip eder. Bir değere olan referans sayısı sıfırsa, herhangi bir referans geçersiz hâle gelmeden değer bellekten temizlenebilir.

Rc<T> türünü oturma odasındaki bir televizyon gibi hayal edin. Bir kişi televizyon izlemek için odaya girdiğinde televizyonu açar. Başkaları da odaya gelip televizyonu izleyebilir. Son kişi odadan ayrıldığında, artık kullanılmadığı için televizyonu kapatır. Başkaları hâlâ izlerken biri televizyonu kapatacak olursa, kalan izleyicilerden büyük bir itiraz yükselir!

Programımızın birden fazla bölümünün okuması için öbekte (heap) bir miktar veri ayırmak istediğimizde ve veriyi kullanmayı en son hangi bölümün bitireceğini derleme zamanında belirleyemediğimizde Rc<T> türünü kullanırız. Hangi bölümün en son bitireceğini bilseydik, doğrudan o bölümü verinin sahibi yapabilirdik ve derleme zamanında uygulanan normal sahiplik kuralları devreye girerdi.

Rc<T> türünün yalnızca tek thread’li (single-threaded) senaryolarda kullanıma yönelik olduğuna dikkat edin. Bölüm 16’da eşzamanlılığı ele aldığımızda, çok thread’li (multithreaded) programlarda referans sayımının nasıl yapılacağını inceleyeceğiz.

Veri Paylaşmak

Liste 15-5’teki cons listesi örneğimize geri dönelim. Onu Box<T> kullanarak tanımladığımızı hatırlayın. Bu kez üçüncü bir listenin sahipliğini paylaşan iki liste oluşturacağız. Kavramsal olarak bu yapı Şekil 15-3’e benzer.

Üç öğeye işaret eden 'a' etiketli bir bağlı liste. İlk öğe 5 tamsayısını içerir ve ikinci öğeye işaret eder. İkinci öğe 10 tamsayısını içerir ve üçüncü öğeye işaret eder. Üçüncü öğe listenin sonunu belirten 'Nil' (kodumuzda 'Bos') değerini içerir ve hiçbir yere işaret etmez. 'b' etiketli bir bağlı liste 3 tamsayısını içeren bir öğeye, o da 'a' listesinin ilk öğesine işaret eder. 'c' etiketli bir bağlı liste ise 4 tamsayısını içeren bir öğeye ve aynı şekilde 'a' listesinin ilk öğesine işaret eder; böylece hem 'b' hem de 'c' listesinin kuyruğu 'a' listesi olur.

Şekil 15-3: Üçüncü bir liste olan a’nın sahipliğini paylaşan b ve c adlı iki liste

Önce 5 ve ardından 10 değerlerini içeren a listesini oluşturacağız. Sonra iki liste daha yapacağız: 3 ile başlayan b ve 4 ile başlayan c. Hem b hem de c listeleri, sonrasında 5 ve 10 değerlerini içeren ilk a listesiyle devam edecek. Başka bir deyişle, her iki liste de 5 ve 10’u içeren ilk listeyi paylaşacak.

Bu senaryoyu Box<T> kullanan Liste tanımımızla uygulamaya çalışmak, Liste 15-17’de gösterildiği gibi işe yaramayacaktır.

Dosya adı: src/main.rs
enum Liste {
    Dugum(i32, Box<Liste>),
    Bos,
}

use crate::Liste::{Bos, Dugum};

fn main() {
    let a = Dugum(5, Box::new(Dugum(10, Box::new(Bos))));
    let b = Dugum(3, Box::new(a));
    let c = Dugum(4, Box::new(a));
}
Liste 15-17: Box<T> kullanan iki listenin üçüncü bir listenin sahipliğini paylaşmaya çalışmasına izin verilmediğini göstermek

Bu kodu derlediğimizde şu hatayı alırız:

$ cargo run
   Compiling kons-liste v0.1.0 (file:///projects/kons-liste)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:31
   |
 9 |     let a = Dugum(5, Box::new(Dugum(10, Box::new(Bos))));
   |         - move occurs because `a` has type `Liste`, which does not implement the `Copy` trait
10 |     let b = Dugum(3, Box::new(a));
   |                               - value moved here
11 |     let c = Dugum(4, Box::new(a));
   |                               ^ value used here after move
   |
note: if `Liste` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum Liste {
   | ^^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Dugum(3, Box::new(a));
   |                               - you could clone this value

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

Dugum varyantları tuttukları verinin sahibidir; bu nedenle b listesini oluşturduğumuzda a değeri b’nin içine taşınır ve a’nın sahibi artık b olur. Ardından c’yi oluştururken a’yı yeniden kullanmaya çalıştığımızda, a daha önce taşındığı için buna izin verilmez.

Dugum tanımını bunun yerine referans tutacak şekilde değiştirebilirdik; ancak o zaman ömür (lifetime) parametreleri belirtmemiz gerekirdi. Ömür parametreleri belirterek, listedeki her öğenin en az tüm listenin kendisi kadar uzun yaşayacağını şart koşmuş olurduk. Liste 15-17’deki öğeler ve listeler için bu durum geçerlidir, ancak her senaryoda böyle olmayabilir.

Bunun yerine Liste tanımımızı Liste 15-18’de gösterildiği gibi Box<T> yerine Rc<T> kullanacak şekilde değiştireceğiz. Her Dugum varyantı artık bir değer ve bir Liste’ye işaret eden bir Rc<T> tutacaktır. b’yi oluştururken a’nın sahipliğini almak yerine, a’nın tuttuğu Rc<Liste> değerini klonlayacağız; böylece referans sayısını birden ikiye çıkaracak ve a ile b’nin o Rc<Liste> içindeki verinin sahipliğini paylaşmasını sağlayacağız. c’yi oluştururken de a’yı klonlayarak referans sayısını ikiden üçe çıkaracağız. Rc::clone fonksiyonunu her çağırdığımızda Rc<Liste> içindeki veriye olan referans sayısı artacak ve kendisine yönelik referans sayısı sıfıra düşmedikçe bu veri bellekten temizlenmeyecektir.

Dosya adı: src/main.rs
enum Liste {
    Dugum(i32, Rc<Liste>),
    Bos,
}

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

fn main() {
    let a = Rc::new(Dugum(5, Rc::new(Dugum(10, Rc::new(Bos)))));
    let b = Dugum(3, Rc::clone(&a));
    let c = Dugum(4, Rc::clone(&a));
}
Liste 15-18: Rc<T> kullanan bir Liste tanımı

Rc<T> başlangıç bildirimlerinde (prelude) yer almadığı için onu kapsama dahil etmek üzere bir use deyimi eklememiz gerekir. main içinde 5 ve 10 değerlerini tutan listeyi oluşturup a değişkenindeki yeni bir Rc<Liste> içinde saklıyoruz. Ardından b ve c’yi oluştururken Rc::clone fonksiyonunu çağırıyor ve argüman olarak a içindeki Rc<Liste> değerine bir referans geçiriyoruz.

Rc::clone(&a) yerine a.clone() çağrısı da yapabilirdik; ancak bu durumda Rust’ın genel kabul gören kuralı Rc::clone kullanmaktır. Rc::clone uygulaması, çoğu türün clone uygulamasının yaptığı gibi tüm verinin derin kopyasını (deep copy) çıkarmaz. Rc::clone çağrısı yalnızca referans sayısını artırır ve bu işlem çok az zaman alır. Verilerin derin kopyalarını çıkarmak ise oldukça uzun sürebilir. Referans sayımı için Rc::clone kullanarak, derin kopyalama yapan klonlamalar ile referans sayısını artıran klonlamaları görsel olarak birbirinden ayırt edebiliriz. Kodda performans sorunlarını ararken yalnızca derin kopya oluşturan klonlamaları dikkate almamız yeterli olur; Rc::clone çağrılarını göz ardı edebiliriz.

Not: Rc<T> türünün perde arkasındaki çalışma biçimine dair iki önemli noktayı aklınızda bulundurmak faydalıdır:

  • Neden a.clone() yerine Rc::clone(&a) yazılır? Rc::new çağrıldığında öbekte tek bir bellek bloğu ayrılır ve bu blokta asıl veriyle birlikte güçlü (strong) ve zayıf (weak) referans sayılarını tutan iki adet usize sayaç saklanır. Rc::clone(&a) çağrısı öbekte yeni bir bellek tahsisi yapmaz ve içteki veriyi derinlemesine kopyalamaz (hatta içteki T türünün Clone trait’ini uygulaması bile gerekmez); yalnızca öbekteki mevcut usize sayacını 1 artırır ve aynı bellek adresine işaret eden yeni bir Rc<T> işaretçisi döndürür. Metot sözdizimi olan a.clone() yerine Rc::clone(&a) yazma geleneği, kod okunurken burada maliyetli bir derin kopya değil yalnızca hafif bir sayaç artırımı yapıldığını görsel olarak vurgular.
  • Rc<T> neden !Send olup yalnızca tek thread’li senaryolarda kullanılır? Rc<T>, tek thread’li kodda ek maliyet yaratmamak için referans sayacını sıradan (atomik olmayan) işlemci komutlarıyla günceller. İki farklı thread aynı Rc<T> sayacını eşzamanlı olarak artırıp azaltmaya çalışsaydı sayaç üzerinde veri yarışı (data race) oluşabilir, bu da bellek sızıntısına veya geçersiz bellek erişimine yol açabilirdi. Bunu derleme zamanında kesin olarak engellemek için Rc<T> türü Send trait’ini uygulamaz (!Send); dolayısıyla bir Rc<T> değerini başka bir thread’e aktarmaya çalıştığınızda Rust derleyicisi hata verir. Çok thread’li senaryolarda paylaşımlı sahiplik için Bölüm 16’da inceleyeceğimiz, sayaç güncellemelerini atomik komutlarla güvenle yapan Arc<T> (Atomically Reference Counted) türü kullanılır.

Klonlayarak Referans Sayısını Artırmak

Liste 15-18’deki çalışan örneğimizi, a içindeki Rc<Liste> değerine yönelik referanslar oluşturup düşürdükçe referans sayılarının nasıl değiştiğini görebileceğimiz şekilde değiştirelim.

Liste 15-19’da main fonksiyonunu, c listesinin etrafında bir iç kapsam olacak biçimde değiştireceğiz; böylece c kapsam dışına çıktığında referans sayısının nasıl değiştiğini görebiliriz.

Dosya adı: src/main.rs
enum Liste {
    Dugum(i32, Rc<Liste>),
    Bos,
}

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

// --snip--

fn main() {
    let a = Rc::new(Dugum(5, Rc::new(Dugum(10, Rc::new(Bos)))));
    println!("a oluşturulduktan sonraki sayı = {}", Rc::strong_count(&a));
    let b = Dugum(3, Rc::clone(&a));
    println!("b oluşturulduktan sonraki sayı = {}", Rc::strong_count(&a));
    {
        let c = Dugum(4, Rc::clone(&a));
        println!("c oluşturulduktan sonraki sayı = {}", Rc::strong_count(&a));
    }
    println!("c kapsamdan çıkınca sayı = {}", Rc::strong_count(&a));
}
Liste 15-19: Referans sayısını yazdırmak

Programda referans sayısının değiştiği her noktada, Rc::strong_count fonksiyonunu çağırarak elde ettiğimiz referans sayısını yazdırıyoruz. Rc<T> türünün bir de weak_count değeri bulunduğu için bu fonksiyona count yerine strong_count adı verilmiştir; weak_count değerinin ne işe yaradığını “Weak<T> Kullanarak Referans Döngülerini Önlemek” bölümünde göreceğiz.

Bu kod aşağıdakileri yazdırır:

$ cargo run
   Compiling kons-liste v0.1.0 (file:///projects/kons-liste)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/kons-liste`
a oluşturulduktan sonraki sayı = 1
b oluşturulduktan sonraki sayı = 2
c oluşturulduktan sonraki sayı = 3
c kapsamdan çıkınca sayı = 2

a içindeki Rc<Liste> değerinin başlangıçta 1 referans sayısına sahip olduğunu, ardından clone fonksiyonunu her çağırdığımızda bu sayının 1 arttığını görebiliriz. c kapsam dışına çıktığında ise sayı 1 azalır. Referans sayısını artırmak için Rc::clone çağırmamız gerekirken, sayıyı azaltmak için herhangi bir fonksiyon çağırmamız gerekmez: Bir Rc<T> değeri kapsam dışına çıktığında Drop trait’inin uygulaması referans sayısını otomatik olarak azaltır.

Bu örnekte göremediğimiz şey ise main fonksiyonunun sonunda önce b, ardından da a kapsam dışına çıktığında sayının 0 olması ve Rc<Liste> değerinin bellekten tamamen temizlenmesidir. Rc<T> kullanmak tek bir değerin birden fazla sahibi olmasına olanak tanır ve referans sayacı, sahiplerden herhangi biri var olduğu sürece değerin geçerli kalmasını güvence altına alır.

Rc<T>, değiştirilemez referanslar aracılığıyla programınızın birden fazla bölümü arasında veriyi yalnızca okuma amacıyla paylaşmanıza olanak tanır. Eğer Rc<T> birden fazla değiştirilebilir referansa da sahip olmanıza izin verseydi, Bölüm 4’te ele aldığımız ödünç alma kurallarından birini ihlal edebilirdiniz: Aynı yere yönelik birden fazla değiştirilebilir ödünç alma, veri yarışlarına ve tutarsızlıklara yol açabilir. Öte yandan veriyi değiştirebilmek son derece kullanışlıdır! Bir sonraki bölümde, bu değiştirilemezlik kısıtlamasıyla başa çıkmak üzere bir Rc<T> ile birlikte kullanabileceğiniz içsel değiştirilebilirlik (interior mutability) desenini ve RefCell<T> türünü ele alacağız.