Tüm Rust rehberleri
TEKNİK REHBER BELLEK SMART POINTER 2026

Smart Pointer'lar
Box, Rc, Arc, RefCell

Heap tahsisi, paylaşımlı sahiplik ve iç değişebilirlik (interior mutability); ne zaman hangisi — C++ unique_ptr/shared_ptr ve const_cast ile karşılaştırmalı.

00 C++ smart pointer'larından Rust'a

C++'taki üçlüyü zaten biliyorsun: unique_ptr tek sahip, shared_ptr paylaşımlı sahip, weak_ptr sahiplenmeyen gözlemci. Rust'ın smart pointer manzarası birebir bu kavramlar üzerine kuruludur — ama sahiplik kuralları kütüphane değil, dilin çekirdeği tarafından zorlanır.

C++'ta smart pointer bir kütüphane sözleşmesidir: doğru kullanırsan güvenli, ama .get() ile çıplak pointer'ı kaçırdığında ya da shared_ptr döngüsü kurduğunda derleyici tek kelime etmez. Rust'ta aynı tipler standart kütüphanededir, fakat ownership ve borrow check derleme zamanında devrede olduğu için yanlış kullanımın büyük kısmı zaten derlenmez. Geri kalan az sayıdaki dinamik kural (örneğin RefCell borrow ihlali) runtime'da panik ile yakalanır — sessiz UB ile değil.

C++RustAnlam
unique_ptr<T>Box<T>Heap'te tek sahipli değer
shared_ptr<T>Rc<T> / Arc<T>Reference counting ile paylaşımlı sahiplik
weak_ptr<T>Weak<T>Sahiplenmeyen, döngü kıran zayıf referans
const_cast + mutableRefCell<T> / Cell<T>Immutable görünen veriyi değiştirme (interior mutability)

Kritik fark Rc ile Arc arasındaki ayrımdır — C++'ta shared_ptr her zaman atomik sayaç kullanır (her platformda kilit/atomik maliyeti ödersin). Rust bunu ikiye böler: tek-thread için ucuz Rc (atomik olmayan sayaç), thread'ler arası için Arc (atomik sayaç). İhtiyacın kadar ödeme yaparsın; tek-thread kodda atomik maliyeti boşuna taşımazsın.

NOT

Rust'ta "smart pointer" bir tür tanımı değil, bir desendir: Deref trait'ini implement eden (yani * ile hedefine erişilebilen) ve genellikle Drop ile sahip olduğu kaynağı otomatik temizleyen herhangi bir tip. Box, Rc, Vec, String hepsi bu desene uyar.

Tek sahip, heap          → Box<T>
Paylaşımlı, tek-thread    → Rc<T>
Paylaşımlı, çok-thread    → Arc<T>
Paylaşımlı + değişebilir  → Rc/Arc <RefCell/Mutex<T>>
Döngü kır                 → Weak<T>

Bu makale yukarıdaki haritayı satır satır açar. Önce tek sahipli Box ile başlıyoruz; ardından her smart pointer'ı mümkün kılan Deref/Drop mekanizmasına iniyoruz; sonra paylaşımlı sahiplik (Rc/Arc) ve iç değişebilirlik (RefCell/Cell) eksenlerini ekleyip, en sona referans döngüsü tuzağını ve Weak ile çözümünü bırakıyoruz.

Bu bölümde

  • C++ üçlüsü unique_ptr/shared_ptr/weak_ptr → Rust'ta Box/Rc(Arc)/Weak.
  • Smart pointer = Deref (+ genellikle Drop) implement eden tip; özel bir dil yapısı değil.
  • Rust, C++'ın tek shared_ptr'ını atomik olmayan Rc ve atomik Arc diye ayırarak gereksiz maliyeti keser.
  • const_cast'in güvenli karşılığı RefCell/Cell ile sağlanan interior mutability'dir.

01 Box<T>

Box<T> en sade smart pointer'dır: değeri heap'e koyar, stack'te sadece bir pointer tutar ve tek sahiplidir. Düşünce olarak C++'ın unique_ptr<T>'sidir — ama move semantiği dilin varsayılanı olduğu için std::move yazmana gerek yoktur.

Çoğu Rust değeri zaten stack'te yaşar ve heap'e ihtiyaç duymaz. Box'a üç durumda başvurursun: (1) derleme zamanında boyutu bilinmeyen recursive tip; (2) stack'te taşımak istemediğin çok büyük veri (move sırasında sadece pointer kopyalanır, koca yapı değil); (3) somut tip yerine trait object (Box<dyn Trait>) tutma ihtiyacı.

main.rs
fn main() {
    let b = Box::new(5);   // 5 heap'te, b stack'te pointer
    println!("b = {}", b);  // Deref ile otomatik &i32 gibi davranır
}                          // b drop edilir → heap belleği iade edilir

Recursive tip klasik örnektir. Bir cons list düğümü kendi türünden bir alan içerir; doğrudan gömersen derleyici boyutu hesaplayamaz (sonsuz büyür). Box araya girerek boyutu sabitler: bir Box her zaman bir pointer boyutundadır, hedefi ne olursa olsun.

main.rs
// Box olmadan: "recursive type has infinite size" hatası
enum List {
    Cons(i32, Box<List>),  // kuyruk heap'te → boyut sabit (pointer)
    Nil,
}

use List::{Cons, Nil};

fn main() {
    // 1 -> 2 -> 3 -> Nil   (C'deki tek yönlü bağlı liste)
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    match list {
        Cons(head, _) => println!("baş = {head}"),
        Nil => {}
    }
}

Trait object durumu, C++'ta unique_ptr<Base> ile sanal fonksiyon (vtable) tutmaya karşılık gelir. Box<dyn Trait> heap'te somut tipi saklar, stack'te ise (veri pointer'ı + vtable pointer'ı) çiftini tutar — yani fat pointer'dır.

main.rs
trait Draw { fn draw(&self) -> String; }

struct Button; struct Slider;
impl Draw for Button { fn draw(&self) -> String { "[Düğme]".into() } }
impl Draw for Slider { fn draw(&self) -> String { "[Kaydırıcı]".into() } }

fn main() {
    // Farklı somut tipleri tek vektörde tut (heterojen koleksiyon)
    let bilesenler: Vec<Box<dyn Draw>> = vec![Box::new(Button), Box::new(Slider)];
    for b in &bilesenler {
        println!("{}", b.draw());  // dinamik dispatch (vtable)
    }
}
NOT

Box::new(x) ile değer move edilir; sahiplik Box'a geçer. unique_ptr'da olduğu gibi kopyalanamaz — ama Rust'ta bunu std::move ile elle işaretlemen gerekmez, move dilin varsayılan davranışıdır.

Bu bölümde

  • Box<T> = heap'te tek sahipli değer; C++ unique_ptr muadili, std::move gerekmez.
  • Üç ana kullanım: recursive tip, büyük veriyi ucuz taşıma, Box<dyn Trait> trait object.
  • Recursive enum'da Box boyutu sonsuzdan sabit pointer boyutuna indirir.
  • Box<dyn Trait> bir fat pointer'dır (veri + vtable) ve dinamik dispatch sağlar.

02 Deref ve Drop

Bir tipi "smart pointer" yapan iki trait vardır: Deref (* operatörünü ve deref coercion'ı sağlar) ve Drop (kapsam dışına çıkınca otomatik temizlik). İkisi birlikte C++'ın operator* + RAII yıkıcısını oluşturur.

Deref implement eden bir tip, * ile sarmaladığı değere erişim verir. Ama asıl güçlü olan kısım deref coercion'dır: derleyici gerektiğinde &Box<T>'yi otomatik olarak &T'ye çevirir. Bu sayede &String bekleyen yere &Box<String>, &str bekleyen yere &String geçirebilirsin — zincirleme çalışır.

main.rs
use std::ops::Deref;

// Kendi mini-Box'ımız: unique_ptr'ı elle yazmak gibi
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> { MyBox(x) }
}

impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &T { &self.0 }  // *x → *(x.deref())
}

fn selamla(ad: &str) { println!("Merhaba, {ad}!"); }

fn main() {
    let m = MyBox::new(String::from("Rust"));
    // deref coercion zinciri: &MyBox<String> → &String → &str
    selamla(&m);
    println!("uzunluk = {}", m.len());  // String metodu Deref ile erişilebilir
}

DerefMut aynı işi &mut için yapar; *box = yeni_deger ataması bunu kullanır. Deref immutable erişimi, DerefMut mutable erişimi besler — tıpkı C++'ta operator*'ın const ve non-const aşırı yüklemeleri gibi.

Drop ise RAII'nin Rust karşılığıdır. Bir değer kapsam dışına çıktığında drop metodu otomatik çağrılır; manuel free/delete yoktur. Sıralama da tanımlıdır: değerler bildirimin ters sırasında, iç içe yapılar dıştan içe drop edilir.

main.rs
struct Kaynak { ad: String }

impl Drop for Kaynak {
    // C++ yıkıcısının (~Kaynak) karşılığı
    fn drop(&mut self) {
        println!("'{}' serbest bırakılıyor", self.ad);
    }
}

fn main() {
    let _a = Kaynak { ad: "A".into() };
    let _b = Kaynak { ad: "B".into() };
    drop(_a);  // erken bırakmak istersen: std::mem::drop (manuel .drop() YASAK)
    println!("main bitiyor...");
}  // çıktı sırası: A, "main bitiyor...", B (ters sıra)
DİKKAT

x.drop()'ı kendin çağıramazsın — derleyici buna izin vermez (sonra otomatik drop ile double free olurdu). Erken bırakmak için std::mem::drop(x) kullan; bu değeri move ederek tüketir ve sahipliği biten değer kapsam sonunda tekrar drop edilmez.

Bu bölümde

  • Deref * operatörünü sağlar; deref coercion &Box<T> → &T dönüşümünü otomatik yapar.
  • DerefMut aynı işi &mut için yapar (C++'ın const/non-const operator* ayrımı).
  • Drop = RAII yıkıcısı; kapsam sonunda otomatik, ters sırada çalışır.
  • Manuel .drop() yasaktır; erken bırakma için std::mem::drop() kullanılır.

03 Rc<T>

Rc<T> (reference counted) tek-thread paylaşımlı sahiplik sağlar: bir değerin birden çok sahibi olur, sayaç sıfıra düşünce değer drop edilir. Mantığı shared_ptr ile birebir aynıdır — tek farkı sayacın atomik olmamasıdır.

Bazen bir verinin "son kullanıcının" kim olacağını derleme zamanında bilemezsin: grafda bir düğümü birden çok kenar gösterir, GUI'de bir veriyi birden çok widget okur. Tek sahiplik (move) bu durumu ifade edemez. Rc::clone ucuzdur — veriyi kopyalamaz, sadece sayacı bir artırır ve aynı heap tahsisine yeni bir sahip ekler.

main.rs
use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("paylaşılan veri"));
    println!("oluşturuldu, count = {}", Rc::strong_count(&a)); // 1

    let b = Rc::clone(&a);  // derin kopya DEĞİL — sadece sayaç++
    println!("b klonlandı, count = {}", Rc::strong_count(&a)); // 2

    {
        let c = Rc::clone(&a);
        println!("iç blok, count = {}", Rc::strong_count(&a)); // 3
    }  // c drop → count 2'ye iner

    println!("iç blok bitti, count = {}", Rc::strong_count(&a)); // 2
    println!("içerik: {b}");
}  // a, b drop → count 0 → String serbest

Rc::clone(&a) yazımı kasıtlıdır: a.clone() de çalışır ama o, türe göre derin kopya çağrısına benzer ve niyeti gizler. Rc::clone okuyucuya "burada ucuz bir sayaç artışı var" der. C++'ta shared_ptr kopya constructor'ı bunu örtük yapar; Rust topluluğu açık olanı tercih eder.

Rc::clone(&a)Sayacı artırır, aynı heap verisine yeni sahip; O(1), veri kopyalanmaz.
Rc::strong_count(&a)O an kaç güçlü (strong) sahip olduğunu döndürür; debug/teşhis için.
Rc::downgrade(&a)Sahiplenmeyen bir Weak<T> üretir (s8'de detaylı).
DİKKAT

Rc<T> verdiği erişim immutable'dır; &mut alamazsın (birden çok sahip varken aliasing + mutation = veri yarışı). Paylaşırken değiştirmek için içe bir RefCell koymak gerekir — s5'in konusu. Ayrıca Rc Send/Sync değildir: thread'ler arasında taşıyamazsın, derleyici durdurur. Çok-thread için Arc (s7).

Bu bölümde

  • Rc<T> = tek-thread paylaşımlı sahiplik; shared_ptr mantığı ama atomik olmayan sayaç (daha ucuz).
  • Rc::clone O(1)'dir: veriyi değil sayacı kopyalar; niyet için a.clone() yerine tercih edilir.
  • strong_count ile sahip sayısını izleyebilirsin; sayaç 0 olunca veri drop edilir.
  • Rc immutable paylaşım verir ve Send değildir — mutation için RefCell, thread için Arc gerekir.

04 RefCell<T> ve interior mutability

RefCell<T> borrow kuralını derleme zamanından runtime'a taşır. Dışarıdan &RefCell (immutable) tutarken içeriği değiştirebilirsin — bu, const_cast'in güvenli, kontrollü karşılığıdır.

Rust'ın temel kuralı: ya tek bir &mut, ya da çok sayıda & — ikisi aynı anda olmaz. Normalde bunu borrow checker statik olarak doğrular. Ama bazen kuralın doğru olduğunu sen bilirsin, derleyici ise kanıtlayamaz (örneğin Rc ile paylaşılan bir düğümü güncellemek). Interior mutability bu boşluğu doldurur: kuralı esnetmeden, sadece kontrolün zamanını kaydırır. RefCell borrow sayaçlarını çalışma zamanında tutar; kuralı ihlal edersen UB değil, temiz bir panik alırsın.

YaklaşımBorrow kontrolüİhlal sonucu
&T / &mut TDerleme zamanı (statik)Derlenmez
RefCell<T>Runtime (dinamik sayaç)panic!
C++ const_castYok (programcıya emanet)UB
main.rs
use std::cell::RefCell;

fn main() {
    let hucre = RefCell::new(vec![1, 2, 3]);

    // hucre `mut` DEĞİL — yine de içeriği değiştiriyoruz (interior mutability)
    hucre.borrow_mut().push(4);   // RefMut alır, blok sonunda bırakır

    let okuma = hucre.borrow();    // Ref (paylaşımlı) alır
    println!("len = {}", okuma.len()); // 4
}

Asıl mesele kontrol zamanlamasıdır. borrow() bir Ref, borrow_mut() bir RefMut döndürür ve bu nöbetçi (guard) nesneler yaşadıkça sayaç artar, drop olunca azalır. Aynı anda bir RefMut aktifken ikinci bir borrow denersen — derleme geçer ama runtime panikler.

main.rs
use std::cell::RefCell;

fn main() {
    let c = RefCell::new(10);

    let m1 = c.borrow_mut();        // 1. mutable borrow — OK
    let m2 = c.borrow_mut();        // 2. mutable borrow — RUNTIME PANIC:
    // thread 'main' panicked: already borrowed: BorrowMutError

    println!("{} {}", m1, m2);
}
NOT

C++'ta const bir nesnede mutable üye ya da const_cast ile değişiklik yaptığında derleyici sessizdir; kuralı ihlal edersen UB. RefCell aynı esnekliği verir ama ihlali asla sessizce geçirmez — borrow sayacı her zaman doğruyu söyler. Bu güvenlik tek-thread içindir; RefCell de Sync değildir.

Bu bölümde

  • RefCell<T> borrow kontrolünü runtime'a taşır; immutable görünen veriyi içeriden değiştirir.
  • borrow()Ref, borrow_mut()RefMut; guard'lar yaşadıkça sayaç tutulur.
  • Borrow kuralı ihlal edilirse derlenmez değil — BorrowMutError ile panikler (UB değil).
  • const_cast'in güvenli karşılığıdır; ama tek-thread'liktir (Sync değil).

05 Rc<RefCell<T>> deseni

Çoğu gerçek veri yapısı iki şeyi aynı anda ister: paylaşımlı sahiplik (Rc) ve değiştirilebilirlik (RefCell). İkisini iç içe koyarsın: Rc<RefCell<T>>. Bu, "birden çok sahip tarafından paylaşılan, değiştirilebilir düğüm" demektir.

Katmanları niyetlerine göre oku: Rc dışta kim sahip sorusunu (paylaşımlı, sayılı), RefCell içte nasıl değiştirilir sorusunu (runtime borrow) yanıtlar. Tek başına Rc immutable, tek başına RefCell tek sahipli; birleşince ağaç, graf ve doubly-linked list gibi yapıların temel taşı olur.

Rc<RefCell<T>>
└─ Rc      : paylaşımlı sahiplik (sayaç)
   └─ RefCell : iç değişebilirlik (runtime borrow)
      └─ T   : asıl veri
main.rs
use std::rc::Rc;
use std::cell::RefCell;

// Çok kenarlı grafta paylaşılan, güncellenebilir düğüm
type Dugum = Rc<RefCell<i32>>;

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

    let sahip_a = Rc::clone(&paylasilan);  // count = 2
    let sahip_b = Rc::clone(&paylasilan);  // count = 3

    // A üzerinden değiştir → B ve orijinal de görür (aynı heap verisi)
    *sahip_a.borrow_mut() += 10;

    println!("orijinal = {}", paylasilan.borrow()); // 15
    println!("sahip_b  = {}", sahip_b.borrow());   // 15
    println!("count    = {}", Rc::strong_count(&paylasilan)); // 3
}

Bu desen güçlüdür ama iki tuzağı vardır. Birincisi: tüm borrow ihlalleri artık runtime hatasına dönüşür — aynı düğümü iki yerde aynı anda borrow_mut() edersen panik alırsın, üstelik bunu testle yakalaman gerekir. İkincisi ve daha tehlikelisi: Rc döngü kurmaya izin verir; iki düğüm birbirini Rc ile gösterirse sayaç asla sıfıra inmez → bellek sızıntısı (s8).

DİKKAT

İç içe veri yapılarında borrow_mut() guard'ını gereğinden uzun tutma. Bir düğümün RefMut'ını elinde tutarken aynı düğüme dolaylı yoldan (örneğin bir fonksiyon çağrısı üzerinden) tekrar erişirsen BorrowMutError ile çökersin. Guard'ları kısa, dar bir blokta kullan.

Bu bölümde

  • Rc<RefCell<T>> = paylaşımlı sahiplik + iç değişebilirlik; ağaç/graf düğümlerinin standart deseni.
  • Rc "kim sahip", RefCell "nasıl değişir" sorusunu yanıtlar; bir sahip üzerinden değişiklik hepsine yansır.
  • Tuzak 1: borrow ihlalleri runtime panik olur — testle yakalanmalı.
  • Tuzak 2: Rc döngüleri sayacı sıfırlatmaz → bellek sızıntısı (çözüm: Weak).

06 Cell<T>

Cell<T>, Copy tipler için borrow'suz, kopya-tabanlı bir iç değişebilirlik sağlar. RefCell'in aksine referans vermez — değeri içeri/dışarı kopyalar, dolayısıyla runtime borrow sayacı tutmaz ve hiç panik etmez.

Fark, erişim modelindedir. RefCell içerideki veriye bir referans (Ref/RefMut) verir; bu yüzden borrow kuralını dinamik takip etmek zorundadır. Cell ise hiç referans vermez: get() değerin kopyasını döndürür, set() yeni bir değerle üzerine yazar, replace() eskiyi alıp yenisini koyar. Referans olmadığı için ihlal edilecek bir borrow kuralı da yoktur.

main.rs
use std::cell::Cell;

// İmmutable struct içinde sayaç gibi küçük durum tutmak
struct Sayac {
    deger: Cell<u32>,
}

fn main() {
    let s = Sayac { deger: Cell::new(0) };

    // s `mut` değil — yine de güncelliyoruz, borrow guard YOK
    s.deger.set(s.deger.get() + 1);
    s.deger.set(s.deger.get() + 1);

    let eski = s.deger.replace(100);  // eskiyi al, 100 yaz
    println!("eski = {eski}, yeni = {}", s.deger.get()); // eski = 2, yeni = 100
}
ÖzellikCell<T>RefCell<T>
Tip kısıtıT: Copy (get için)Herhangi bir T
ErişimKopya (get/set)Referans (borrow/borrow_mut)
Runtime maliyetSıfır (sayaç yok)Borrow sayacı kontrolü
Panik riskiYokVar (BorrowError)
NOT

Kural basit: veri küçük ve Copy ise (sayaç, flag, u32, bool) Cell kullan — daha ucuz ve panik etmez. Veriye referans alıp üzerinde çalışman gerekiyorsa ya da tip Copy değilse (Vec, String) RefCell şarttır.

Bu bölümde

  • Cell<T> kopya-tabanlı iç değişebilirlik sağlar; referans vermez, borrow sayacı tutmaz.
  • get() kopya döndürür (T: Copy gerekir), set()/replace() üzerine yazar.
  • RefCell'den farkı: sıfır runtime maliyet ve panik riski yok — ama sadece küçük/Copy veri için.
  • Sayaç/flag gibi durumlarda Cell, referans gereken/Copy olmayan veride RefCell seç.

07 Arc<T>

Arc<T> (atomically reference counted), Rc'nin thread-safe ikizidir. API'si birebir aynıdır; tek fark sayacın atomik artırılıp azaltılmasıdır. Bu, C++'ın shared_ptr'ının her zaman yaptığı şeydir.

Rc tek-thread'de hızlıdır çünkü sayacını sıradan bir tam sayı ile günceller. Ama iki thread aynı Rc'yi aynı anda klonlasa, sayaç yarışa girer (data race). Bu yüzden Rc Send değildir — derleyici onu thread sınırından geçirmeni engeller. Arc sayacını atomik işlemlerle yönetir; bu küçük bir ek maliyettir ama thread'ler arası paylaşımı güvenli kılar. Onun için ödediğin bedeli sadece gerçekten ihtiyacın olduğunda öde.

main.rs
use std::sync::Arc;
use std::thread;

fn main() {
    let veri = Arc::new(vec![1, 2, 3]);
    let mut kollar = vec![];

    for i in 0..3 {
        let v = Arc::clone(&veri);  // atomik sayaç++ → thread'e taşı
        kollar.push(thread::spawn(move || {
            println!("thread {i}: {:?}", v);  // hepsi aynı veriyi okur
        }));
    }
    for k in kollar { k.join().unwrap(); }
}

Tıpkı Rc gibi Arc da yalnızca immutable paylaşım verir. Thread'ler arasında paylaşıp değiştirmek istiyorsan, içe bir senkronizasyon ilkeli koyman gerekir: Arc<Mutex<T>>. Bu, Rc<RefCell<T>> deseninin çok-thread karşılığıdır — RefCell yerine kilitli Mutex.

main.rs
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let sayac = Arc::new(Mutex::new(0));  // paylaşımlı + değişebilir
    let mut kollar = vec![];

    for _ in 0..10 {
        let c = Arc::clone(&sayac);
        kollar.push(thread::spawn(move || {
            let mut n = c.lock().unwrap();  // kilidi al → MutexGuard
            *n += 1;                       // guard drop olunca kilit serbest
        }));
    }
    for k in kollar { k.join().unwrap(); }
    println!("toplam = {}", *sayac.lock().unwrap()); // 10
}
NOT

Mutex/RwLock ve kilit mekaniğinin detayları ayrı bir concurrency makalesinin konusudur; burada amacımız Arc<Mutex<T>>'ün, Rc<RefCell<T>>'ün thread-safe analoğu olduğunu görmek. Eksen aynı: paylaşım (Rc/Arc) × değişebilirlik (RefCell/Mutex).

Bu bölümde

  • Arc<T> = atomik sayaçlı Rc; thread-safe (Send+Sync), API'si aynı.
  • Atomik işlemler küçük bir maliyet getirir; bu yüzden tek-thread'de Rc, çok-thread'de Arc seçilir.
  • Rc gibi Arc da yalnızca immutable paylaşım verir.
  • Thread'ler arası değiştirilebilir paylaşım için Arc<Mutex<T>> = Rc<RefCell<T>>'ün çok-thread analoğu.

08 Weak<T> ve referans döngüleri

Rc (ve Arc) güçlü sahipliktir; iki düğüm birbirini Rc ile gösterdiğinde sayaç asla sıfıra inmez ve bellek sızar. Weak<T> sahiplenmeyen bir referanstır — sayacı tutmaz, döngüyü kırar. Tam olarak weak_ptr'nin işidir.

Senaryoyu somutlaştıralım: bir ağaçta her child, parent'ını da gösterir. Eğer hem parent → child hem child → parent Rc ile olursa, ikisi de birbirini canlı tutar; kapsam bitse bile sayaçlar 1'de takılır, Drop hiç çalışmaz. Bu, C++'ta iki shared_ptr'ın birbirini göstermesiyle birebir aynı sızıntıdır.

Parent --Rc--> Child        (sahiplik: parent child'ı tutar)
Child  --Weak--> Parent      (gözlem: child parent'ı tutmaz, döngü yok)

Çözüm asimetridir: sahip olan yön Rc, geri-işaret eden yön Weak. Weak sayacı (strong_count) artırmaz, ayrı bir weak_count tutar. Hedefe erişmek için upgrade() çağırırsın — bu bir Option<Rc<T>> döndürür: hedef hâlâ canlıysa Some(Rc), çoktan drop edildiyse None. Yani dangling olamazsın; "belki yok" durumu tür sisteminde kodlanır.

main.rs
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Dugum {
    deger: i32,
    parent: RefCell<Weak<Dugum>>,   // yukarı: Weak (sahiplik YOK)
    cocuklar: RefCell<Vec<Rc<Dugum>>>, // aşağı: Rc (sahiplik)
}

fn main() {
    let kok = Rc::new(Dugum {
        deger: 1,
        parent: RefCell::new(Weak::new()),  // boş weak
        cocuklar: RefCell::new(vec![]),
    });

    let yaprak = Rc::new(Dugum {
        deger: 2,
        parent: RefCell::new(Weak::new()),
        cocuklar: RefCell::new(vec![]),
    });

    // bağla: kök yaprağı SAHİPLENİR (Rc), yaprak köke GERİ BAKAR (Weak)
    kok.cocuklar.borrow_mut().push(Rc::clone(&yaprak));
    *yaprak.parent.borrow_mut() = Rc::downgrade(&kok); // Rc → Weak

    // parent'a erişim: upgrade() → Option<Rc<Dugum>>
    if let Some(p) = yaprak.parent.borrow().upgrade() {
        println!("yaprağın parent değeri = {}", p.deger); // 1
    }

    println!("kök strong={}, weak={}",
        Rc::strong_count(&kok), Rc::weak_count(&kok)); // strong=1, weak=1
}  // döngü YOK → kök ve yaprak temiz drop edilir
Rc::downgrade(&rc)Rc<T>'den Weak<T> üretir; strong_count'u artırmaz.
weak.upgrade()Option<Rc<T>> döndürür; hedef canlıysa Some, drop edildiyse None.
Rc::weak_count(&rc)Kaç zayıf referans olduğunu döndürür (sızıntı teşhisi için).
DİKKAT

weak_ptr::lock()'ın Rust karşılığı upgrade()'tir. C++'ta weak_ptr'yi lock'lamadan expired() kontrolü yapmadan kullanmaya kalkışmak hata kaynağıdır; Rust'ta upgrade() seni Option ile zorlar — "belki ölmüş" ihtimalini ele almadan hedefe erişemezsin. Dangling imkânsızdır.

Bu bölümde

  • İki Rc'nin birbirini göstermesi döngü kurar → sayaç sıfıra inmez → bellek sızıntısı.
  • Weak<T> sahiplenmeyen referanstır; strong_count'u artırmaz, döngüyü kırar (weak_ptr muadili).
  • Desen asimetriktir: sahiplik yönü Rc, geri-işaret yönü Weak (parent → Weak, child → Rc).
  • downgrade() RcWeak; upgrade() Option<Rc<T>> verir → dangling tür sisteminde imkânsız.

09 Karar tablosu + gerçek örnek

Smart pointer seçimi iki eksene indirgenir: sahiplik (tek mi, paylaşımlı mı; tek-thread mi, çok-thread mi) ve değişebilirlik (immutable mi, iç değişebilir mi). Tablo bu iki eksenin tüm kesişimlerini verir.

İhtiyaçPointerC++ karşılığı
Heap'te tek sahipBox<T>unique_ptr<T>
Paylaşımlı sahip, tek-threadRc<T>shared_ptr<T> (atomik)
Paylaşımlı sahip, çok-threadArc<T>shared_ptr<T>
İç değişebilir, Copy veriCell<T>mutable üye
İç değişebilir, referanslıRefCell<T>const_cast + mutable
Paylaşımlı + değişebilir, tek-threadRc<RefCell<T>>shared_ptr + mutation
Paylaşımlı + değişebilir, çok-threadArc<Mutex<T>>shared_ptr + mutex
Sahiplenmeyen geri-referansWeak<T>weak_ptr<T>
NOT

Pratik sezgi: önce en kısıtlı olanı dene. Box yetiyorsa Rc kullanma; Rc yetiyorsa RefCell ekleme; tek-thread'de Arc'a uzanma. Her ek katman (paylaşım, runtime borrow, atomiklik, kilit) bir maliyet ve bir hata yüzeyidir. En aza in.

Kapanış olarak öğrendiğimiz her şeyi tek bir gerçek yapıda birleştirelim: parent pointer'lı bir ağaç. Aşağı yön Rc<RefCell<...>> (paylaşımlı, değiştirilebilir çocuk listesi), yukarı yön Weak (döngüsüz parent referansı). Bu, gerçek dünyada DOM ağaçlarından sahne grafiklerine kadar her yerde kullanılan kanonik desendir.

main.rs
use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Dugum {
    ad: String,
    parent: RefCell<Weak<Dugum>>,        // Weak: döngü yok
    cocuklar: RefCell<Vec<Rc<Dugum>>>,    // Rc: sahiplik
}

impl Dugum {
    fn yeni(ad: &str) -> Rc<Dugum> {
        Rc::new(Dugum {
            ad: ad.into(),
            parent: RefCell::new(Weak::new()),
            cocuklar: RefCell::new(vec![]),
        })
    }

    // child ekle ve parent'ı (Weak ile) bağla
    fn cocuk_ekle(parent: &Rc<Dugum>, cocuk: Rc<Dugum>) {
        *cocuk.parent.borrow_mut() = Rc::downgrade(parent);
        parent.cocuklar.borrow_mut().push(cocuk);
    }
}

fn main() {
    let kok = Dugum::yeni("kök");
    let a   = Dugum::yeni("a");
    let b   = Dugum::yeni("b");

    Dugum::cocuk_ekle(&kok, Rc::clone(&a));
    Dugum::cocuk_ekle(&kok, Rc::clone(&b));

    // aşağı gezinme: kök → çocuklar
    for c in kok.cocuklar.borrow().iter() {
        println!("çocuk: {}", c.ad);
    }

    // yukarı gezinme: a → parent (upgrade ile güvenli)
    if let Some(p) = a.parent.borrow().upgrade() {
        println!("a'nın parent'ı: {}", p.ad);  // kök
    }

    println!("kök strong={}, weak={}",
        Rc::strong_count(&kok), Rc::weak_count(&kok)); // strong=1, weak=2
}  // hepsi temiz drop → sıfır sızıntı

Bu yapının zarafeti şudur: C++'ta aynı ağacı kurmak için shared_ptr aşağı, weak_ptr yukarı koyman ve döngüyü kendin önlemen gerekirdi — yanlış yaparsan sessiz sızıntı. Rust'ta da aynı kararı verirsin, ama upgrade()'in Option dönüşü ve borrow checker, "parent ölmüş olabilir" ve "aynı anda iki mutable erişim" hatalarını sana zorla hatırlatır. Tasarım aynı; güvenlik ağı farklı.

Bu bölümde

  • Seçim iki eksen: sahiplik (tek/paylaşımlı, tek/çok-thread) × değişebilirlik (immutable/iç değişebilir).
  • Kural: en kısıtlı pointer'ı seç — gereksiz paylaşım, borrow, atomiklik ve kilit katmanından kaçın.
  • Kanonik ağaç deseni: aşağı Rc<RefCell<...>> (sahiplik+mutasyon), yukarı Weak (döngüsüz parent).
  • Rust'ta tasarım C++ ile aynıdır; fark, sızıntı ve borrow hatalarını derleyici/tür sisteminin zorla yakalatmasıdır.