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++ | Rust | Anlam |
|---|---|---|
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 + mutable | RefCell<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.
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'taBox/Rc(Arc)/Weak. - Smart pointer =
Deref(+ genellikleDrop) implement eden tip; özel bir dil yapısı değil. - Rust, C++'ın tek
shared_ptr'ını atomik olmayanRcve atomikArcdiye ayırarak gereksiz maliyeti keser. const_cast'in güvenli karşılığıRefCell/Cellile 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ı.
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.
// 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.
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)
}
}
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_ptrmuadili,std::movegerekmez.- Üç ana kullanım: recursive tip, büyük veriyi ucuz taşıma,
Box<dyn Trait>trait object. - Recursive enum'da
Boxboyutu 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.
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.
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)
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> → &Tdönüşümünü otomatik yapar.DerefMutaynı işi&mutiçin yapar (C++'ın const/non-constoperator*ayrımı).Drop= RAII yıkıcısı; kapsam sonunda otomatik, ters sırada çalışır.- Manuel
.drop()yasaktır; erken bırakma içinstd::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.
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.
Weak<T> üretir (s8'de detaylı).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_ptrmantığı ama atomik olmayan sayaç (daha ucuz).Rc::cloneO(1)'dir: veriyi değil sayacı kopyalar; niyet içina.clone()yerine tercih edilir.strong_countile sahip sayısını izleyebilirsin; sayaç 0 olunca veri drop edilir.Rcimmutable paylaşım verir veSenddeğildir — mutation içinRefCell, thread içinArcgerekir.
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şım | Borrow kontrolü | İhlal sonucu |
|---|---|---|
&T / &mut T | Derleme zamanı (statik) | Derlenmez |
RefCell<T> | Runtime (dinamik sayaç) | panic! |
C++ const_cast | Yok (programcıya emanet) | UB |
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.
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);
}
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 —
BorrowMutErrorile panikler (UB değil). const_cast'in güvenli karşılığıdır; ama tek-thread'liktir (Syncdeğ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
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).
İç 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:
Rcdö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.
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
}
| Özellik | Cell<T> | RefCell<T> |
|---|---|---|
| Tip kısıtı | T: Copy (get için) | Herhangi bir T |
| Erişim | Kopya (get/set) | Referans (borrow/borrow_mut) |
| Runtime maliyet | Sıfır (sayaç yok) | Borrow sayacı kontrolü |
| Panik riski | Yok | Var (BorrowError) |
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 verideRefCellseç.
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.
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.
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
}
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'deArcseçilir. RcgibiArcda 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.
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<T>'den Weak<T> üretir; strong_count'u artırmaz.Option<Rc<T>> döndürür; hedef canlıysa Some, drop edildiyse None.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_ptrmuadili).- Desen asimetriktir: sahiplik yönü
Rc, geri-işaret yönüWeak(parent → Weak, child → Rc). downgrade()Rc→Weak;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ç | Pointer | C++ karşılığı |
|---|---|---|
| Heap'te tek sahip | Box<T> | unique_ptr<T> |
| Paylaşımlı sahip, tek-thread | Rc<T> | shared_ptr<T> (atomik) |
| Paylaşımlı sahip, çok-thread | Arc<T> | shared_ptr<T> |
| İç değişebilir, Copy veri | Cell<T> | mutable üye |
| İç değişebilir, referanslı | RefCell<T> | const_cast + mutable |
| Paylaşımlı + değişebilir, tek-thread | Rc<RefCell<T>> | shared_ptr + mutation |
| Paylaşımlı + değişebilir, çok-thread | Arc<Mutex<T>> | shared_ptr + mutex |
| Sahiplenmeyen geri-referans | Weak<T> | weak_ptr<T> |
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.
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.