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

Çoğu durumda, sahiplik açıktır: Belirli bir değere hangi değişkenin sahip olduğunu tam olarak bilirsiniz. Ancak, tek bir değerin birden çok sahibi olabileceği durumlar vardır. Örneğin, grafik veri yapılarında, birden çok kenar aynı düğüme işaret edebilir ve bu düğüm kavramsal olarak ona işaret eden tüm kenarlara aittir. Bir düğüm, kendisine işaret eden herhangi bir kenarı olmadığı ve dolayısıyla sahibi olmadığı sürece temizlenmemelidir.

Referans sayımının kısaltması olan Rust türü Rc<T>'yi kullanarak birden çok sahipliği açıkça etkinleştirmeniz gerekir. Rc<T> türü, değerin hala kullanımda olup olmadığını belirlemek için bir değere yapılan başvuruların sayısını takip eder. Bir değere sıfır referans varsa, hiçbir referans geçersiz hale gelmeden değer temizlenebilir.

Rc<T>'yi bir aile odasındaki bir TV olarak hayal edin. Bir kişi TV izlemek için girdiğinde onu açar. Diğerleri odaya gelip televizyon izleyebilir. Son kişi odadan ayrıldığında, artık kullanılmadığı için televizyonu kapatırlar. Biri televizyonu kapatırsa, diğerleri hala onu seyrediyorsa, kalan televizyon izleyicilerinden büyük bir kargaşa çıkar!

Rc<T> türünü, programımızın birden fazla bölümünün okuması için yığın üzerinde bazı verileri tahsis etmek istediğimizde kullanırız ve derleme zamanında verileri en son hangi bölümün bitireceğini belirleyemeyiz. Hangi bölümün en son biteceğini bilseydik, o bölümü verinin sahibi yapabilirdik ve derleme zamanında uygulanan normal sahiplik kuralları yürürlüğe girecekti.

Rc<T> öğesinin yalnızca tek iş parçacıklı senaryolarda kullanım için olduğunu unutmayın. Bölüm 16'da eşzamanlılığı tartıştığımızda, çok iş parçacıklı programlarda referans sayımının nasıl yapıldığını ele alacağız.

Veri Paylaşmak için Rc<T> Kullanmak

Liste 15-5'teki liste örneğimize dönelim. Box<T> kullanarak tanımladığımızı hatırlayın. Bu sefer, her ikisi de üçüncü bir listenin sahipliğini paylaşan iki liste oluşturacağız. Kavramsal olarak, bu Şekil 15-3'e benziyor:

Üçüncü bir listenin sahipliğini paylaşan iki liste

Şekil 15-3: İki liste, b ve c, üçüncü bir listenin sahipliğini paylaşan a

5 ve ardından 10'u içeren bir a listesi oluşturacağız. Ardından, 3 ile başlayan b ve 4 ile başlayan c olmak üzere iki liste daha yapacağız. Daha sonra hem b hem de c listeleri, 5'i ve 10'u içeren ilk listeye devam edecek. Başka bir deyişle, her iki liste de 5 ve 10'u içeren ilk listeyi paylaşacaktır.

Liste 15-17'de gösterildiği gibi, Box<T>'li List tanımımızı kullanarak bu senaryoyu uygulamaya çalışmak işe yaramaz:

Dosya adı: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Liste 15-17: Üçüncü bir listenin sahipliğini paylaşmaya çalışan Box<T> kullanan iki listeye sahip olmamıza izin verilmediğini gösteriyor

Kodu derlediğimizde, şu hatayı alırız:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error

Cons varyantları, tuttukları verilere sahiptir, bu nedenle b listesini oluşturduğumuzda a, b'ye taşınır ve b, a'ya sahiptir. Ardından, c'yi oluştururken a'yı tekrar kullanmaya çalıştığımızda, a taşınmış olduğu için buna izin verilmiyor.

Bunun yerine referansları tutmak için Cons'un tanımını değiştirebilirdik, ancak daha sonra ömür boyu parametreleri belirtmemiz gerekecekti. Yaşam süresi parametreleri belirterek, listedeki her öğenin en az tüm liste kadar yaşayacağını belirtmiş oluruz. Bu, Liste 15-17'deki öğeler ve listeler için geçerlidir, ancak her senaryoda geçerli değildir.

Bunun yerine List tanımımızı, Liste 15-18'de gösterildiği gibi Box<T> yerine Rc<T> kullanacak şekilde değiştireceğiz. Her Cons varyantı artık bir değere ve bir Listeye işaret eden bir Rc<T>'ye sahip olacaktır. b'yi yarattığımızda, a'nın sahipliğini almak yerine, a'nın sahip olduğu Rc<List>'i klonlayacağız, böylece referans sayısını birden ikiye çıkaracağız ve a ve b'nin o Rc<List>'deki verilerin sahipliğini paylaşmasına izin vereceğiz. Ayrıca c'yi oluştururken a'yı klonlayarak referans sayısını ikiden üçe çıkaracağız. Rc::clone'u her çağırdığımızda, Rc<List> içindeki verilere referans sayısı artacak ve sıfır referans olmadıkça veriler temizlenmeyecektir.

Dosya adı: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Liste 15-18: Rc<T> kullanan bir List tanımı

Rc<T>'yi kapsam içine almak için bir use ifadesi eklememiz gerekiyor çünkü bu başlangıç kısmında değil. main'de, 5 ve 10'u tutan listeyi oluşturuyoruz ve onu a'da yeni bir Rc<List> içinde saklıyoruz. Daha sonra b ve c'yi oluşturduğumuzda, Rc::clone fonksiyonunu çağırırız ve argüman olarak a'daki Rc<List>'e bir referansını iletiriz.

Rc::clone(&a) yerine a.clone()'u çağırabilirdik, ancak Rust'ın kuralı bu durumda Rc::clone kullanmaktır. Rc::clone'un süreklenmesi, çoğu türde clone süreklemesinin yaptığı gibi tüm verilerin derin bir kopyasını oluşturmaz. Rc::clone çağrısı yalnızca referans sayısını artırır, bu da fazla zaman almaz. Verilerin derin kopyaları çok zaman alabilir. Referans sayımı için Rc::clone kullanarak, derin kopyalı klon türleri ile referans sayısını artıran klon türleri arasında görsel olarak ayrım yapabiliriz. Kodda performans sorunları ararken, yalnızca derin kopya klonlarını dikkate almamız gerekir ve Rc::clone'a yapılan çağrıları göz ardı edebiliriz.

Rc<T>'yi Klonlamak Referans Sayısını Artırır

Liste 15-18'deki çalışma örneğimizi değiştirelim, böylece a içindeki Rc<List>e referansları oluşturup bıraktıkça referans sayılarının değiştiğini görebilelim.

Liste 15-19'da, main'i, c listesi etrafında bir iç kapsama sahip olacak şekilde değiştireceğiz; o zaman 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 List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Liste 15-19: Referans sayısının yazdırılması

Referans sayısının değiştiği programdaki her noktada, Rc::strong_count fonksiyonunu çağırarak elde ettiğimiz referans sayısını yazdırırız. Bu fonksiyon, count yerine strong_count olarak adlandırılır, çünkü Rc<T> türünde bir weak_count da vardır; “Referans Döngülerini Önleme: Bir Rc<T>'yi Weak<T>'ye Çevirme” bölümünde weak_count'ın ne için kullanıldığını göreceğiz.

Bu kod şunları yazdırır:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

a içindeki Rc<List> öğesinin ilk referans sayısının 1 olduğunu görebiliriz; sonra clone'u her çağırdığımızda, sayı 1 artar. c kapsam dışına çıktığında, sayı 1 azalır. Rc'yi çağırmamız gerektiği gibi referans sayısını azaltmak için bir fonksiyon çağırmamız gerekmez: referans sayısını artırmak için klonlayın: Drop tanımının süreklenmesi, bir Rc<T> değeri kapsam dışına çıktığında referans sayısını otomatik olarak azaltır.

Bu örnekte göremediğimiz şey, main sonunda b ve ardından a kapsam dışına çıktığında, sayının 0 olduğu ve Rc<List>'in tamamen temizlendiğidir. Rc<T> kullanmak, tek bir değerin birden çok sahibine sahip olmasına izin verir ve sayı, sahiplerden herhangi biri var olduğu sürece değerin geçerli kalmasını sağlar.

Değişmez referanslar aracılığıyla, Rc<T>, programınızın birden çok bölümü arasında salt okuma için veri paylaşmanıza olanak tanır. Eğer Rc<T> birden çok değişken referansa sahip olmanıza da izin veriyorsa, Bölüm 4'te tartışılan ödünç alma kurallarından birini ihlal etmiş olabilirsiniz: aynı yere birden fazla değişken ödünç alma veri yarışlarına ve tutarsızlıklara neden olabilir. Ancak verileri değiştirebilmek çok faydalıdır! Bir sonraki bölümde, bu değişmezlik kısıtlamasıyla çalışmak için bir Rc<T> ile birlikte kullanabileceğiniz iç değişkenlik modelini ve RefCell<T> türünü tartışacağız.