RUST OWNERSHIP VE BORROWING NEDİR?

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.
1. 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.
2. Use-After-Free'nin Compile-Time'da Yakalanması

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.
3. 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.
4. İ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.
5. Lifetime'lar: Referansların Geçerlilik Süresi

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.
6. 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.
- C++'da kuralları siz bilir ve uygularsınız; ihlal runtime'da patlar.
- Rust'ta kuralları derleyici uygular; ihlal compile etmez.
- 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.
7. 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.

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.
Sıkça Sorulan Sorular
Ownership ile garbage collection arasındaki temel fark nedir?
Garbage collector runtime'da çalışır ve belleği periyodik olarak tarar; bu hem CPU hem latency maliyeti getirir. Ownership ise compile-time'da çözülür: derleyici her değerin nereye kadar yaşayacağını bilir ve drop çağrılarını otomatik olarak yerleştirir. Runtime'da ek bir iş yapılmaz, yani zero-cost abstraction sağlanır.
Move semantics C++'taki std::move ile aynı mı?
Kavramsal olarak benzerdir ama davranış farklıdır. C++'ta std::move sonrası kaynak nesne hala kullanılabilir (boş/geçerli durumda); kullanırsanız derleyici uyarmaz. Rust'ta move sonrası eski binding tamamen geçersizdir ve kullanmaya çalışırsanız compile hatası alırsınız. Bu fark use-after-move bug sınıfını tamamen ortadan kaldırır.
Bir fonksiyona değer mi referans mı geçmeliyim?
Kural basittir: fonksiyon değerin sahipliğini almak ve onu tutmak istiyorsa değer geçin, sadece okuyacak veya değiştirecekse referans geçin. Büyük struct'ları kopyalamak yerine &T geçmek hem daha verimlidir hem de niyetinizi imzada belgeler. Ownership'i transfer etmek istemiyorsanız asla move etmeyin.
İki mutable referansa neden gerçekten ihtiyaç duyabilirim?
Çoğu durumda bu ihtiyaç gerçek değil, alışılmış bir pattern'in yansımasıdır. Aynı veriye iki yerden yazmak istediğinizde genelde veri yapısını parçalamak (split_at_mut gibi) veya interior mutability sağlayan RefCell, Mutex kullanmak gerekir. Tasarımı yeniden düşünmek çoğu zaman daha temiz bir çözüm üretir.
Lifetime annotation her zaman elle yazılmalı mı?
Hayır. Rust'ın lifetime elision kuralları çoğu fonksiyon imzasında lifetime'ları otomatik çıkarır. Tek referans alıp tek referans dönen fonksiyonlarda yazmaya gerek yoktur. Sadece birden fazla referans parametresi olup dönen referansın hangisine bağlı olduğu belirsiz kaldığında derleyici sizden açık annotation ister.
Unsafe blok ownership kurallarını devre dışı bırakır mı?
Tam olarak değil. Unsafe blok size raw pointer dereference, FFI çağrısı ve mutable static erişimi gibi ek yetkiler verir; ama ownership ve borrow checker yine çalışır. Unsafe içinde yazdığınız mantığın güvenli olduğunu programcı garanti eder. İyi tasarlanmış kütüphanelerde unsafe küçük tutulur ve güvenli API'nin arkasına gizlenir.
Borrow checker hatalarıyla başa çıkmak için pratik bir strateji var mı?
Önce hatanın hangi referansın hangi scope'ta tutulduğuna işaret ettiğini anlayın. Çoğu zaman çözüm: borrow'u daha kısa scope'a indirmek, klonlama yapmak, veya veriyi struct olarak yeniden modellemek. clone() kullanmak başlangıçta meşrudur; profilleme sonrası darboğaz olduğu yerlerde optimize edebilirsiniz.


