Rc<T>, bir değerin birden fazla sahibi olmasını sağlayan referans sayımlı akıllı işaretçidir; RefCell<T> ise ödünç alma kurallarını derleme zamanı yerine çalışma zamanında denetleyerek paylaşılan bir referans üzerinden bile değişiklik yapmanıza izin verir. İkisi çoğu zaman Rc<RefCell<T>> biçiminde birlikte kullanılır: Rc sahipliği paylaştırır, RefCell içerideki veriyi değiştirilebilir kılar. İkisi de tek thread içindir; derleyici bunları başka bir thread'e göndermenize izin vermez.

Bu yazıda Rc::clone ile referans sayısının nasıl değiştiğini, RefCell ile iç değişebilirlik desenini ve çalışma zamanı paniğini, daha hafif bir seçenek olan Cell tipini, referans döngülerinin neden bellek sızıntısına yol açtığını ve Weak<T> ile nasıl önlendiğini adım adım görüyoruz. Tek sahipli kutuyla başlamadıysanız önce Box yazısını okumanızı öneririz; ödünç kuralları için borrowing yazısı temel oluşturur.

Rc&lt;T&gt;: tek thread'de paylaşılan sahiplik

Rc<T>: tek thread'de paylaşılan sahiplik

Sahiplik çoğu zaman nettir: bir değerin tek bir sahibi vardır. Ama bazı yapılarda bu model zorlanır. Bir grafikte birden çok kenar aynı düğümü gösterebilir ve düğüm, onu gösteren son kenar kalkana kadar yaşamalıdır. Hangi parçanın veriyi en son kullanacağını derleme zamanında bilemiyorsanız Rc<T> kullanırsınız. Rc::new değeri heap'e koyar ve sayacı 1'den başlatır. Her Rc::clone çağrısı yeni bir sahip ekler ve sayacı artırır; bir Rc kapsam dışına çıktığında Drop uygulaması sayacı azaltır. Sayaç sıfıra indiğinde değer bırakılır.

rust
use std::rc::Rc;

#[derive(Debug)]
struct Ayar {
    tema: String,
}

fn main() {
    let ortak = Rc::new(Ayar { tema: String::from("koyu") });
    println!("başta: {}", Rc::strong_count(&ortak));

    let pencere = Rc::clone(&ortak);
    println!("pencere sonrası: {}", Rc::strong_count(&ortak));
    {
        let panel = Rc::clone(&ortak);
        println!("panel içinde: {} tema={}", Rc::strong_count(&ortak), panel.tema);
    }
    println!("panel düştü: {}", Rc::strong_count(&ortak));
    println!("aynı mı: {}", Rc::ptr_eq(&ortak, &pencere));
    drop(pencere);
    println!("{:?}", Rc::try_unwrap(ortak));
}
// çıktı: başta: 1
// çıktı: pencere sonrası: 2
// çıktı: panel içinde: 3 tema=koyu
// çıktı: panel düştü: 2
// çıktı: aynı mı: true
// çıktı: Ok(Ayar { tema: "koyu" })

Kitap burada bir adlandırma geleneğinin altını çizer: ortak.clone() da aynı işi yapar, ama Rc::clone(&ortak) yazmak tercih edilir. Sebebi okunabilirliktir. Çoğu tipin clone metodu verinin derin bir kopyasını çıkarır ve pahalı olabilir; Rc::clone ise yalnızca sayacı artırır. Performans sorunu ararken Rc::clone satırlarını gözden çıkarabilir, sadece gerçek kopyalara odaklanabilirsiniz. Std belgesi ayrıca Rc metotlarının ilişkili fonksiyon olarak tanımlandığını hatırlatır; Rc otomatik olarak iç tipe dereference edildiği için panel.tema gibi erişimler doğrudan çalışır ve isim çakışması yaşanmaz.

Örneğin sonundaki Rc::ptr_eq iki Rc değerinin aynı ayırmayı gösterip göstermediğini söyler. Rc::try_unwrap ise tek bir güçlü referans kaldıysa iç değeri Ok içinde geri verir, aksi hâlde aynı Rc değerini Err içinde iade eder. pencere düşürüldükten sonra sahip yalnızca ortak kaldığı için değer geri alınabildi.

Rc<T> yalnızca paylaşılan, yani değiştirilemez erişim verir. Birden çok sahibin aynı anda değiştirilebilir referans tutması, ödünç kurallarının engellemeye çalıştığı veri tutarsızlıklarına kapı açardı. Bu kısıtın iki istisnası var: Rc::get_mut, başka Rc ya da Weak işaretçisi yoksa değiştirilebilir referans verir, varsa None döndürür. Rc::make_mut ise paylaşım varsa iç değeri klonlayıp yeni bir ayırmaya geçer; std belgesi buna clone-on-write der.

rust
use std::rc::Rc;

fn main() {
    let mut a = Rc::new(vec![1, 2, 3]);
    Rc::get_mut(&mut a).unwrap().push(4);
    println!("{a:?}");

    let b = Rc::clone(&a);
    println!("paylaşılırken get_mut: {:?}", Rc::get_mut(&mut a));

    Rc::make_mut(&mut a).push(5);
    println!("a = {a:?}");
    println!("b = {b:?}");
    println!("aynı bellek mi: {}", Rc::ptr_eq(&a, &b));
}
// çıktı: [1, 2, 3, 4]
// çıktı: paylaşılırken get_mut: None
// çıktı: a = [1, 2, 3, 4, 5]
// çıktı: b = [1, 2, 3, 4]
// çıktı: aynı bellek mi: false

RefCell&lt;T&gt;: ödünç kurallarını çalışma zamanına taşımak

Ödünç kuralları değişmez: aynı anda ya bir değiştirilebilir referans ya da istediğiniz kadar paylaşılan referans olabilir. Normal referanslarda ve Box türünde bu kurallar derleme zamanında denetlenir, ihlal derleme hatasıdır. RefCell<T> ise aynı kuralları çalışma zamanında denetler; ihlal edilirse program panikler. Kitap bunun gerekçesini derleyicinin muhafazakâr olmasıyla açıklar: statik analiz bazı doğru programları kanıtlayamaz ve emin olamadığında reddeder. Kodunuzun kurallara uyduğundan eminseniz ama derleyici bunu göremiyorsa RefCell bu boşluğu doldurur.

borrow metodu Ref<T>, borrow_mut metodu RefMut<T> döndürür. İkisi de Deref uyguladığı için normal referans gibi kullanılır. RefCell etkin Ref ve RefMut sayısını tutar; bu değerler kapsam dışına çıkınca sayaç düşer. Panik istemiyorsanız try_borrow ve try_borrow_mut sonucu Result olarak verir.

rust
use std::cell::RefCell;

fn main() {
    let kayitlar = RefCell::new(vec![String::from("başladı")]);

    kayitlar.borrow_mut().push(String::from("devam"));
    println!("{:?}", kayitlar.borrow());

    {
        let okuma = kayitlar.borrow();
        println!("okuma açıkken yazma denemesi: {}", kayitlar.try_borrow_mut().is_err());
        println!("ilk kayıt: {}", okuma[0]);
    }
    println!("okuma kapandı: {}", kayitlar.try_borrow_mut().is_ok());

    let a = kayitlar.borrow_mut();
    let b = kayitlar.borrow_mut();
    println!("{} {}", a.len(), b.len());
}
// çıktı: ["başladı", "devam"]
// çıktı: okuma açıkken yazma denemesi: true
// çıktı: ilk kayıt: başladı
// çıktı: okuma kapandı: true
// panik: thread 'main' (...) panicked at r2.rs:17:22:
// panik: RefCell already borrowed

Örnekte okuma açıkken try_borrow_mut hata döndürüyor, blok kapandıktan sonra başarılı oluyor. Son iki satır ise kuralı kasıtlı olarak çiğniyor: ilk borrow_mut sonucu a değişkeninde yaşarken ikinci bir değiştirilebilir ödünç isteniyor. Kod derlenir, ama çalışınca RefCell already borrowed mesajıyla panikler. Paniğin satır ve sütun bilgisi dosyanıza göre değişir, thread kimliği de her çalıştırmada farklıdır. Hatanın derleme yerine çalışma zamanında ortaya çıkması RefCell seçiminin bedelidir; kitap ayrıca ödünç takibinin küçük bir çalışma zamanı maliyeti olduğunu not eder.

Rc&lt;RefCell&lt;T&gt;&gt;: birden çok sahip, değiştirilebilir veri

Rc<RefCell<T>>: birden çok sahip, değiştirilebilir veri

Gerçek kodda en çok karşılaşılan kalıp ikisinin birleşimidir. Aşağıdaki sepet, mobil ve web arayüzü tarafından paylaşılıyor. Her iki taraf aynı sepete ürün ekleyebiliyor ve değişiklikler herkese görünüyor. mobil.borrow_mut() çağrısında otomatik dereference önce Rc katmanını aşar, sonra RefCell metodunu çağırır. Std belgesindeki cell modülü bu deseni iç değişebilirliğin en yaygın kullanımı olarak gösterir ve Rc ile Arc gibi paylaşılan işaretçilerin içine yeniden değişebilirlik eklemek için hücre tiplerine ihtiyaç duyulduğunu açıklar.

rust
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Sepet {
    urunler: Vec<String>,
}

fn main() {
    let sepet = Rc::new(RefCell::new(Sepet { urunler: vec![] }));

    let mobil = Rc::clone(&sepet);
    let web = Rc::clone(&sepet);

    mobil.borrow_mut().urunler.push(String::from("kitap"));
    web.borrow_mut().urunler.push(String::from("kalem"));

    println!("{:?}", sepet.borrow());
    println!("sahip sayısı: {}", Rc::strong_count(&sepet));
}
// çıktı: Sepet { urunler: ["kitap", "kalem"] }
// çıktı: sahip sayısı: 3

Kitap aynı kalıbı test yazarken de kullanır. Bir trait'in metodu &self alıyorsa ve sahte bir nesnenin bu çağrıları kaydetmesi gerekiyorsa, kayıt listesini RefCell<Vec<String>> yaparsınız. Dışarıdan bakınca nesne değişmez görünür, içeride liste büyür. Test yazımının geri kalanı için test yazma yazısına göz atabilirsiniz.

Cell&lt;T&gt;: kopyalanabilir küçük değerler için

Std belgesi hücre tiplerini dört gruba ayırır: Cell, RefCell, OnceCell ve LazyCell. Cell<T> içerdeki değere asla referans vermez; değeri içeri koyar ve dışarı alırsınız. Copy tipler için get değerin bir kopyasını döndürür, set yeni değeri yazar, replace yenisini koyup eskisini verir. Referans verilmediği için ödünç sayımına gerek yoktur ve panik ihtimali de yoktur. Belge, sayı gibi kopyalaması ucuz tipler için mümkün olduğunda Cell tercih edilmesini önerir.

rust
use std::cell::Cell;

struct Sayac {
    ad: String,
    okunma: Cell<u32>,
}

impl Sayac {
    fn oku(&self) -> &str {
        self.okunma.set(self.okunma.get() + 1);
        &self.ad
    }
}

fn main() {
    let s = Sayac { ad: String::from("rapor"), okunma: Cell::new(0) };
    s.oku();
    s.oku();
    println!("{} {} kez okundu", s.oku(), s.okunma.get());

    let c = Cell::new(5);
    c.update(|x| x * 3);
    println!("{}", c.replace(1));
    println!("{}", c.get());
}
// çıktı: rapor 3 kez okundu
// çıktı: 15
// çıktı: 1

oku metodu &self alıyor ama sayaç yine de artıyor. Belgedeki ifadeyle bu, mantıksal olarak değişmeyen bir metodun uygulama ayrıntısıdır: dışarıya bir okuma işlemi sunuyorsunuz, içeride istatistik tutuyorsunuz. Aynı belge Rc türünün de referans sayılarını bir Cell içinde tuttuğunu belirtir; clone metodu &self aldığı için sayacı artırmanın başka yolu yoktur. Bir kez atanıp sonra yalnızca okunacak değerler için OnceCell ve onun tembel sürümü LazyCell daha uygun seçeneklerdir.

OnceCell için std belgesi dört temel metot sayar: get iç değere referans verir, set değeri yalnızca henüz atanmamışsa yazar ve sonucu Result olarak döndürür, get_or_init değer yoksa verdiğiniz closure ile üretir, get_mut ise hücreye değiştirilebilir erişiminiz varsa iç değeri değiştirmenize izin verir. Belge, pahalı bir hesabın sonucunu &self alan bir metotta önbelleğe almayı bu tipin tipik kullanımı olarak gösterir. Çok thread'li karşılıkları OnceLock ve LazyLock türleridir.

Referans döngüleri ve Weak&lt;T&gt;

Rust'ın bellek güvenliği garantileri bellek sızıntısını tamamen yasaklamaz; kitap sızıntının Rust'ta bellek açısından güvenli sayıldığını açıkça söyler. Rc ve RefCell ile iki değerin birbirini gösterdiği bir döngü kurarsanız, her birinin sayacı en az bir kalır ve hiçbiri bırakılmaz. Derleyici bu döngüleri yakalamaz; testler ve kod incelemesiyle önlemeniz gereken bir mantık hatasıdır. Kalıcı çözüm, ilişkilerin bir kısmını sahiplik ifade etmeyecek şekilde tasarlamaktır.

Rc::downgrade bir Weak<T> üretir. Zayıf referanslar strong_count yerine weak_count sayacını artırır ve değerin bırakılmasını engellemez. Değer bırakılmış olabileceği için Weak üzerinden doğrudan erişemezsiniz; upgrade çağırırsınız ve sonuç Option<Rc<T>> olur. Ağaç yapıları bunun tipik örneğidir: üst düğüm alt düğümlerine sahip olmalı, alt düğüm ise üstünü yalnızca tanımalıdır. Aşağıdaki klasör ağacı kitabın düğüm örneğinin uyarlamasıdır.

rust
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Klasor {
    ad: String,
    ust: RefCell<Weak<Klasor>>,
    alt: RefCell<Vec<Rc<Klasor>>>,
}

fn main() {
    let belgeler = Rc::new(Klasor {
        ad: String::from("belgeler"),
        ust: RefCell::new(Weak::new()),
        alt: RefCell::new(vec![]),
    });
    println!("üst: {:?}", belgeler.ust.borrow().upgrade().map(|k| k.ad.clone()));

    {
        let ev = Rc::new(Klasor {
            ad: String::from("ev"),
            ust: RefCell::new(Weak::new()),
            alt: RefCell::new(vec![Rc::clone(&belgeler)]),
        });
        *belgeler.ust.borrow_mut() = Rc::downgrade(&ev);

        println!("üst: {:?}", belgeler.ust.borrow().upgrade().map(|k| k.ad.clone()));
        println!("ev alt klasör sayısı: {}", ev.alt.borrow().len());
        println!("ev strong={} weak={}", Rc::strong_count(&ev), Rc::weak_count(&ev));
        println!("belgeler strong={} weak={}", Rc::strong_count(&belgeler), Rc::weak_count(&belgeler));
    }

    println!("üst: {:?}", belgeler.ust.borrow().upgrade().map(|k| k.ad.clone()));
    println!("belgeler strong={} weak={}", Rc::strong_count(&belgeler), Rc::weak_count(&belgeler));
}
// çıktı: üst: None
// çıktı: üst: Some("ev")
// çıktı: ev alt klasör sayısı: 1
// çıktı: ev strong=1 weak=1
// çıktı: belgeler strong=2 weak=0
// çıktı: üst: None
// çıktı: belgeler strong=1 weak=0

Sayıları izleyelim. ev oluşturulduğunda belgeler türünün güçlü sayısı 2 olur: biri belgeler değişkeni, biri ev.alt içindeki klon. ev türünün güçlü sayısı 1, zayıf sayısı 1'dir; zayıf olan belgeler.ust içindeki referanstır. İç blok bittiğinde ev türünün güçlü sayısı sıfıra iner ve değer bırakılır; zayıf referansın varlığı bunu engellemez. Bu yüzden sonraki upgrade çağrısı None döndürür ve belgeler sayısı yeniden 1'e düşer. Sızıntı yok. Std belgesi bir ayrıntı daha ekler: Weak işaretçileri iç değeri yaşatmaz, ama ayrılan bellek bloğunu, son zayıf referans da gidene kadar tutar.

Thread'ler arasında neden kullanılamaz?

Rc referans sayısını atomik olmayan işlemlerle günceller; bu yüzden maliyeti çok düşüktür ama aynı sayaca iki thread'den dokunmak güvenli değildir. Std belgesine göre Rc bu yüzden Send uygulamaz ve derleyici bir Rc değerini başka bir thread'e göndermenizi derleme zamanında engeller. RefCell de Sync uygulamaz. Çok thread'li kodda karşılıkları Arc<T> ile Mutex<T> veya RwLock<T> ikilisidir; ayrıntılar Arc ve Mutex yazısında. Std belgesi Cell, RefCell ve OnceCell tiplerinin hiçbirinin Sync uygulamadığını, yani thread'ler arasında paylaşılamadığını açıkça belirtir.

rust
use std::rc::Rc;
use std::thread;

fn main() {
    let veri = Rc::new(5);
    let kopya = Rc::clone(&veri);
    let h = thread::spawn(move || println!("{kopya}"));
    h.join().unwrap();
}
// error[E0277]: `Rc<i32>` cannot be sent between threads safely

Sık yapılan hatalar

Sık yapılan hatalar: Rc ile değişiklik yapmaya çalışmak., borrow_mut sonucunu uzun süre tutmak., Döngüleri fark etmemek., Döngüyü Debug ile yazdırmak.
  • Rc ile değişiklik yapmaya çalışmak. Rc<T> yalnızca paylaşılan referans verir. Paylaşılan veriyi değiştirmeniz gerekiyorsa içine RefCell ya da Cell koyun veya tek sahip kaldığından eminseniz Rc::get_mut kullanın.
  • borrow_mut sonucunu uzun süre tutmak. RefMut yaşadığı sürece başka hiçbir ödünç alınamaz. Özellikle döngü içinde ya da başka bir fonksiyonu çağırmadan önce ödüncün bittiğinden emin olun; aksi hâlde panik çağrılan fonksiyonun içinde ortaya çıkar ve sebebini bulmak zorlaşır.
  • Döngüleri fark etmemek. Hem Rc hem RefCell içeren iç içe yapılar, örneğin çift yönlü listeler ve ebeveynini bilen düğümler, kolayca döngü kurar. Geri yönü gösteren bağlantıları Weak yapın ve testlerde Rc::strong_count değerlerini kontrol edin.
  • Döngüyü Debug ile yazdırmak. Kitap, döngülü bir listeyi {:?} ile yazdırmaya çalışmanın sonsuz özyinelemeye girip stack taşmasıyla bittiğini gösterir. Weak alanlar ise (Weak) olarak yazdırılır ve döngüyü keser.
  • Gerekmediği hâlde iç değişebilirlik kullanmak. Std belgesi, normal ve kalıtılan değişebilirliğin tercih edilmesi gerektiğini, iç değişebilirliğin ise bir tür son çare olduğunu belirtir. Önce veriyi tek sahibe ve &mut erişime göre tasarlamayı deneyin.
TipSahip sayısıDeğiştirmeDenetim zamanıThread güvenli mi
Box<T>TekDeğiştirilebilir ödünçleDerleme zamanıT izin veriyorsa
Rc<T>ÇokYalnızca okumaDerleme zamanıHayır, Send uygulamaz
RefCell<T>TekPaylaşılan referansla daÇalışma zamanı, ihlalde panikPaylaşılamaz, Sync uygulamaz
Cell<T>TekDeğer koyup alarakGerekmez, referans verilmezPaylaşılamaz, Sync uygulamaz
Rc<RefCell<T>>ÇokEvetÇalışma zamanıHayır

Sık Sorulan Sorular

Rc yerine her yerde referans kullanamaz mıyım?

Verinin hangi parçada en son kullanılacağını biliyorsanız o parçayı sahip yapıp diğerlerine referans vermek daha basittir. Kitap, listedeki elemanlara referans koymanın her elemanın listenin tamamı kadar yaşamasını gerektiren lifetime kısıtları getireceğini ve bunun her senaryoda geçerli olmadığını belirtir.

RefCell panikliyorsa kodum hatalı mı?

Evet, panik iki ödüncün çakıştığını gösterir ve bir mantık hatasıdır. Ödünçlerin kapsamını daraltın ya da çakışma beklenen yerlerde try_borrow_mut ile hatayı ele alın.

Weak ne zaman None döndürür?

Gösterdiği değerin bütün güçlü referansları bırakıldığında. Weak::new() ile oluşturulan boş bir zayıf referans da hiçbir değere bağlı olmadığı için upgrade çağrısında None verir.

Rc::clone derin kopya yapar mı?

Hayır. Yalnızca sayaç artar ve yeni işaretçi aynı ayırmayı gösterir. İç değerin bağımsız bir kopyasını istiyorsanız (*rc).clone() ya da paylaşım varsa kopyalayan Rc::make_mut kullanın.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Box akıllı işaretçi · Arc, Mutex ve paylaşılan durum · Referans ve borrowing · E0499 ve E0502 ödünç hataları · Rust nedir?