RUST OWNERSHIP VE BORROWING NEDİR?

Rust değer kutusu üzerinden tek sahibe işaret eden ownership oku ve borrow check kalkanı

Production'a çıkmış bir C++ servisi var; intermittent segfault atıyor. Stack trace karışık, valgrind bazen yakalıyor bazen yakalamıyor. Üç gün sonra sebep ortaya çıkıyor: bir thread, başka bir fonksiyonun stack'inde tutulan vektörün elemanına pointer'ı saklamış, vektör realloc olunca o pointer havaya kalmış. Klasik use-after-free. Aynı kod Rust'ta yazılsa derleyici daha siz cargo build yazarken durdurur — çünkü ownership ve borrowing kuralları bu hatayı runtime'a bırakmaz, compile-time'da hata olarak işaretler.

Ownership: Her Değerin Tek Sahibi Vardır

Rust'ın bellek modelinin temeli üç kurala dayanır: her değerin bir sahibi (owner) vardır, aynı anda sadece bir sahibi olabilir, ve sahibi scope dışına çıktığında değer otomatik olarak drop edilir. Bu kurallar garbage collector olmadan, runtime overhead'i olmadan bellek güvenliği sağlar.

fn main() {
    let s1 = String::from("merhaba");
    let s2 = s1;
    println!("{}", s1); // hata: value borrowed here after move
}

C++ alışkanlığıyla bakan biri için bu kod garip görünür. s1'i s2'ye atadığımızda heap üzerindeki string'in sahipliği s2'ye geçer; s1 artık geçersizdir. C++'taki copy constructor veya implicit kopyalama yoktur — değer move edilmiştir. Derleyici bunu fark eder ve s1'i kullanmaya çalışan kodu reddeder.

Use-After-Free'nin Compile-Time'da Yakalanması

Serbest bırakılmış bellek bölgesine sarkan referansın derleme zamanında kalkanla engellenmesi

C++ tarafında en sinsi bug sınıflarından biri use-after-free'dir çünkü bellek bölgesi serbest bırakıldıktan sonra hala "geçerli" görünebilir; içerik bir süre değişmez, sonra başka bir tahsis o adresi alır ve veri sessizce bozulur. Rust'ta aşağıdaki senaryo hiç compile etmez: Konunun ayrıntılarına inmek isteyenler konunun teknik kaynakları üzerinden ek bilgi edinebilir.

fn dangle() -> &String {
    let s = String::from("veri");
    &s
}

Fonksiyon biterken s drop olacağı için döndürülen referans bir sonraki satırda zaten geçersizdir. Borrow checker fonksiyon imzasındaki lifetime'ları analiz eder ve "dangling reference" yaratan bu kodu reddeder. C++'da aynı pattern uyarısız derlenir ve aylar sonra production'da segfault olarak karşınıza çıkar.

Borrowing: Sahipliği Taşımadan Erişim

Her seferinde değeri move etmek pratik değil. borrow yani referans verme bu noktada devreye girer. Rust'ta iki tür borrow vardır:

  • Immutable borrow (&T): Aynı anda istediğiniz kadar okuma referansı olabilir.
  • Mutable borrow (&mut T): Aynı anda yalnızca bir tane olabilir, ve o sırada immutable borrow da bulunamaz.

Bu kural "aliasing XOR mutability" olarak bilinir. Aynı veriye hem yazıp hem de başka yerden okumaya izin vermediği için data race ve iterator invalidation gibi hatalar dilin tip sisteminde imkansız hale gelir.

İki Mutable Referans Neden Yasak?

let mut v = vec![1, 2, 3];
let r1 = &mut v;
let r2 = &mut v; // hata: cannot borrow `v` as mutable more than once
r1.push(4);

C++'da std::vector üzerinde iterasyon yaparken push_back çağırmak iterator'ları invalidate eder ve undefined behavior'a yol açar. Rust'ta iki mutable referans aynı anda var olamadığı için bu pattern compile etmez. push, self'i mutable borrow ettiği için, aktif başka mutable borrow varken çağrılamaz.

Lifetime'lar: Referansların Geçerlilik Süresi

Immutable borrow çoğul okuma ile mutable borrow tekil yazma kuralının yan yana karşılaştırması

Lifetime, bir referansın geçerli olduğu kod bölgesidir. Çoğu durumda derleyici lifetime elision kuralları sayesinde bunu kendi çıkarır. Ama referansların kaynağı belirsizse açıkça yazmanız gerekir:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

'a generic bir lifetime parametresidir. "Dönen referans, x ve y'nin yaşadığı bölgenin kesişimi kadar yaşar" demektir. Bu sayede dönen referansın kullanılacağı scope'ta hala geçerli olduğu garanti altına alınır.

C++ ile Yan Yana: Aynı Bug, Farklı Sonuç

Bir C++ ekibi kendi kod tabanında bellek güvenliği için şu araçları kullanmak zorundadır: AddressSanitizer, ThreadSanitizer, valgrind, statik analiz araçları, code review disiplini. Bu araçların hiçbiri tüm yolları test etmez — sadece test edilen yolu kontrol eder. Rust'ta ise borrow checker, kodun derlenmiş olan tüm yollarını analiz eder. Test coverage'a bağımlı değildir.

  1. C++'da kuralları siz bilir ve uygularsınız; ihlal runtime'da patlar.
  2. Rust'ta kuralları derleyici uygular; ihlal compile etmez.
  3. Performans karakteristiği neredeyse aynıdır — Rust'ın güvenliği zero-cost abstraction prensibiyle gelir.

Dilin daha derinine inmek isteyenler için Rust eğitimi içeriklerinden yararlanabilirsiniz. Orada smart pointer'lar (Box, Rc, Arc), interior mutability (RefCell, Mutex) ve unsafe blokların ne zaman gerekli olduğu konusuna girilir.

Ownership'in Pratik Maliyeti

Rust'a yeni geçen geliştiriciler ilk haftalarda "fighting the borrow checker" aşamasından geçer. Bu, dilin sizi cezalandırdığı için değil, C++ ve diğer dillerden gelen alışkanlıkların Rust'ın veri sahipliği modeline uymadığı için yaşanır. Birkaç pattern öğrenildikten sonra borrow checker mücadele edilen değil, hata yapmadan önce sizi durduran bir yardımcıya dönüşür.

Lifetime parametresi a ile referans geçerlilik aralığının başlangıç ve bitişinin köşeli parantezle gösterimi

Sonuç olarak ownership ve borrowing, Rust'ın "systems programming language" sınıfında olmasına rağmen güvenli olmasının nedenidir. C++ projesinde haftalarca süren bir use-after-free avı, Rust'ta üç satırlık compile hatasıyla biter. Dilin başlangıçtaki dik öğrenme eğrisinin karşılığı, production'da ortaya çıkmayan bug sınıflarıdır. Konunun pratik tarafına geçmek için Rust eğitimi materyallerini inceleyebilirsiniz.