00 Neden ownership?
C bellek hatalarını çalışma zamanına bırakır, GC ise runtime maliyet ekler — Rust üçüncü yolu seçer: derleme-zamanı sahiplik.
Bir C/C++ programcısı olarak bellek hatalarının taksonomisini ezbere biliyorsundur: use-after-free (serbest bırakılmış belleğe erişim), double-free (aynı bloğu iki kez serbest bırakma), memory leak (hiç bırakmama) ve dangling pointer (ömrü bitmiş nesneye işaret eden pointer). Bu hataların ortak özelliği: derleyici sessizdir, sorun çalışma zamanında — çoğu zaman üretimde — patlar. Valgrind, ASan ve code review bunları azaltır ama matematiksel bir garanti vermez.
Geleneksel olarak iki çözüm vardı. Birincisi manuel yönetim (C/C++): hızlı ve deterministik, ama güvenliği programcının disiplinine bırakır. İkincisi garbage collection (Java, Go, C#): güvenli, ama runtime'da tarama döngüleri, durdur-dünya (stop-the-world) duraklamaları ve öngörülemez gecikme getirir — gömülü ve gerçek-zamanlı sistemlerde kabul edilemez.
Rust üçüncü yolu seçer: belleği derleme zamanında bir sahiplik (ownership) modeliyle yönetir. Çalışma zamanında GC yoktur, ekstra metadata yoktur; kontroller derleyicide yapılır ve geçen kod sıfır-maliyet (zero-cost) çalışır. Aşağıdaki C örneği klasik bir use-after-free içerir — Rust'ta karşılığı derlenmez bile.
// C: derleyici hiçbir şikayet etmez, runtime'da UB.
char *p = malloc(16);
strcpy(p, "merhaba");
free(p);
printf("%s\n", p); // use-after-free — patlayabilir, patlamayabilir
fn main() {
let s = String::from("merhaba");
drop(s); // belleği elle bıraktık (genelde gerek yok)
println!("{s}"); // HATA: borrow of moved value: `s`
}
// error[E0382]: derleyici durdurur; runtime UB asla oluşmaz.
Rust'ın garantisi tek bir cümleyle: güvenli (safe) kodda veri yarışı, dangling pointer ve use-after-free derleme zamanında imkânsızdır. Bu garantilerden çıkmak istersen unsafe blok kullanırsın — ama bu açık ve aranabilir bir karardır.
Bu bölümde
- C bellek hataları (use-after-free, double-free, leak, dangling) derleme zamanında yakalanmaz.
- GC güvenlik sağlar ama runtime tarama ve öngörülemez duraklama maliyeti getirir.
- Rust üçüncü yol: sahiplik modeli ile derleme-zamanı, sıfır-maliyet bellek güvenliği.
- Tüm bu garantiler "safe" kod içindir; bilinçli olarak
unsafeile devre dışı bırakılabilir.
01 Ownership kuralları
Tüm modelin üç kurala indiği nokta — bunları içselleştirince borrow checker artık düşman değil refleks olur.
Rust'ın bütün bellek modeli üç basit aksiyoma dayanır:
Üçüncü kural bir C++ programcısına RAII gibi gelmeli — çünkü tam olarak öyle. Fark şu: C++'ta RAII bir kütüphane deseni ve programcı disiplinidir; Rust'ta dilin değişmez kuralıdır ve sahiplik takibi derleyici tarafından zorlanır.
Stack vs heap: nerede yaşar?
Sabit boyutlu ve Copy olan değerler (i32, bool, char) stack'te yaşar; ucuz kopyalanır. String ise üç alandan oluşan bir fat değerdir: stack'te bir başlık (heap'e pointer + uzunluk + kapasite), asıl bayt verisi heap'te. Sahip scope'tan çıkınca heap belleği String'in Drop implementasyonu tarafından serbest bırakılır.
fn main() {
let n = 42; // stack: i32, Copy
{
let s = String::from("selam"); // başlık stack'te, "selam" heap'te
println!("{s} / {n}");
} // s burada scope'tan çıktı -> String::drop çağrıldı, heap free
println!("{n}"); // n hâlâ geçerli; s artık yok
}
let s = String::from(...) → scope sonu → drop(s) → heap free
C++'ta delete unutmak leak, iki kez çağırmak double-free üretir. Rust'ta drop'u derleyici yerleştirir; ne unutabilirsin ne de elle iki kez çağırabilirsin — sahiplik kuralları double-drop'u baştan engeller.
Bu bölümde
- Üç kural: tek sahip vardır, aynı anda tek sahip olur, sahip scope'tan çıkınca değer drop edilir.
- Kural 3 = derleyicinin zorladığı, garantili RAII.
Stringstack başlığı (ptr/len/cap) + heap verisi olan fat bir değerdir.- Drop çağrısını derleyici otomatik yerleştirir; leak veya double-free elle yapılamaz.
02 Move semantiği
Rust'ta varsayılan atama move'dur, kopya değil — ve move sonrası kaynak değişken kullanılamaz hale gelir.
C++'ta auto b = a; varsayılan olarak kopyalar (copy constructor); taşımak istersen açıkça std::move(a) yazarsın. Rust tam tersini yapar: non-Copy tipler için varsayılan atama move'dur. Move sırasında stack başlığı bit-bit yeni değişkene aktarılır ve eski değişken geçersizleştirilir. Heap verisi kopyalanmaz — yalnızca tek geçerli sahip vardır (Kural 2).
fn main() {
let a = String::from("veri");
let b = a; // MOVE: başlık a'dan b'ye taşındı, a artık geçersiz
println!("{b}"); // OK
println!("{a}"); // HATA[E0382]: borrow of moved value: `a`
}
Bu neden hatadır? Çünkü ikisi de geçerli kalsaydı, ikisi de aynı heap bloğunu işaret eder, ikisi de scope sonunda drop çalıştırır — klasik double-free. Rust kaynağı move ile geçersizleştirerek bu durumu kökünden imkânsız kılar. C++'ın "moved-from" nesnesi geçerli-ama-belirsiz durumdadır; Rust'ta moved-from değer derleyici tarafından kullanılamaz ilan edilir.
Fonksiyona move
Bir değeri fonksiyona değer olarak (by value) geçirmek de move'dur. Çağrıdan sonra kaynak değişken artık senin değildir.
fn yut(s: String) {
println!("yutuldu: {s}");
} // s burada drop edilir
fn main() {
let t = String::from("merhaba");
yut(t); // t fonksiyona MOVE edildi
// println!("{t}"); // HATA: t artık geçersiz
}
| Dil | b = a varsayılanı | Taşımak için |
|---|---|---|
| C++ | copy (kopya kurucu) | std::move(a) |
| Rust | move (kaynak geçersizleşir) | zaten varsayılan |
Bu bölümde
- Non-
Copytiplerde atama ve fonksiyon argümanı varsayılan olarak move'dur. - Move sonrası kaynak değişken derleyicide geçersizdir — kullanımı E0382 hatası verir.
- Move heap verisini kopyalamaz; yalnızca stack başlığını taşır (ucuz).
- Tek-sahip kuralı double-free'yi tasarımdan eler; C++'ın "moved-from" belirsizliği yoktur.
03 Copy vs Clone
Bazı tipler move yerine sessizce kopyalanır (Copy); derin kopya istiyorsan açıkça .clone() dersin.
Her tip move semantiğine uymaz. Sabit boyutlu, tamamen stack'te yaşayan ve "bitlerini kopyalamak yeterli" olan tipler Copy trait'ini implement eder: i32, u64, bool, char, f64, ve yalnızca Copy alanlardan oluşan tuple/struct'lar. Bunlarda let b = a; kaynağı geçersizleştirmez — çünkü ucuz bir bit kopyası yapılır ve iki bağımsız değer ortaya çıkar, double-free riski yoktur.
fn main() {
let x = 5;
let y = x; // COPY: i32 Copy'dir, x hâlâ geçerli
println!("{x} {y}"); // OK -> 5 5
}
Clone: açık ve potansiyel pahalı
String, Vec<T>, HashMap gibi heap sahibi tipler Copy değildir; çünkü ucuz bit kopyası iki sahip yaratır. Gerçek bir derin kopya istiyorsan .clone() çağırırsın — bu, heap verisini yeni bir blok olarak çoğaltır. Çağrı açıktır: kodda .clone() görmek "burada heap tahsisi/kopyası var" demektir.
fn main() {
let a = String::from("veri");
let b = a.clone(); // DERİN kopya: yeni heap bloğu, ayrı veri
println!("{a} | {b}"); // her ikisi de geçerli -> veri | veri
}
| Özellik | Copy | Clone |
|---|---|---|
| Tetikleme | örtük (atama anında) | açık (.clone()) |
| Maliyet | ucuz bit kopyası | keyfi (heap, derin) |
| Tipik tipler | i32, bool, char, &T | String, Vec, HashMap |
| İlişki | Copy ⊂ Clone (Copy ise Clone de olmalı) | tek başına olabilir |
Borrow checker hatasından kurtulmak için refleks olarak .clone() serpiştirmek yaygın bir acemi tuzağıdır. Her clone bir tahsis/kopya maliyetidir. Doğru çözüm çoğu zaman borrow etmektir (sonraki bölümler) — clone'u gerçekten ayrı bir kopya gerektiğinde sakla.
Bu bölümde
Copytipler (sabit boyut, stack) atamada örtük kopyalanır; kaynak geçerli kalır.- Heap sahibi tipler Copy değildir; derin kopya için açık
.clone()gerekir. .clone()kodda görünür bir maliyet işaretidir.- Hatayı clone ile susturmak yerine genelde borrow etmek doğru çözümdür.
04 Sahipliği fonksiyonlarla taşımak
Sadece move ile çalışmak değeri fonksiyonlara "ver-geri al" döngüsüne sokar — bu hantallık borrowing'in doğuş sebebidir.
Move semantiği tutarlıdır ama saf haliyle kullanışsızdır. Bir String'i bir fonksiyona verip sonra ana fonksiyonda kullanmaya devam etmek istersen, fonksiyonun onu geri döndürmesi gerekir. Tek bir veri için bu katlanılabilir; ama hem değeri kullanıp hem de bir sonuç döndürmek istediğinde tuple iadelerine boğulursun.
// Borrowing yokken: sahipliği al, geri döndür — hantal.
fn uzunluk(s: String) -> (String, usize) {
let n = s.len();
(s, n) // sahipliği çağırana geri verebilmek için s'i iade et
}
fn main() {
let s = String::from("merhaba");
let (s, n) = uzunluk(s); // move ettik, geri aldık... yorucu
println!("'{s}' uzunluğu {n}");
}
Buradaki hantallık tesadüf değil — Rust seni doğal olarak doğru araca itiyor. İhtiyacın olan şey değeri sahiplenmeden, sadece geçici olarak okumak/değiştirmek. Bu da borrowing (ödünç alma): sahibi değiştirmeden bir referans (reference) ödünç vermek.
move-and-return (hantal) → borrow &T / &mut T (zarif)
// Borrowing ile: sahiplik s'te kalır, sadece referans veririz.
fn uzunluk(s: &String) -> usize {
s.len() // okuruz ama sahiplenmeyiz
}
fn main() {
let s = String::from("merhaba");
let n = uzunluk(&s); // & ile ödünç verdik
println!("'{s}' uzunluğu {n}"); // s hâlâ bizim
}
Bu bölümde
- Değeri by-value geçirmek sahipliği fonksiyona taşır (move).
- Sonra kullanmaya devam için değeri geri döndürmek gerekir — sadece move ile kod hantallaşır.
- Çözüm borrowing: sahibi taşımadan geçici referans vermek.
&sile ödünç verince sahiplik kaynakta kalır, çağrı sonrası değer hâlâ kullanılabilir.
05 Borrowing — &T
Paylaşımlı referans &T: veriyi sahiplenmeden okumana izin verir, aynı anda istediğin kadar olabilir.
&T bir paylaşımlı (shared), değiştirilemez (immutable) referanstır. C'deki const T* ile kavramsal olarak benzer ama kritik bir farkla: Rust'ta referansın işaret ettiği değerin &T yaşadığı sürece geçerli kalacağı (dangling olmayacağı) derleyici tarafından garanti edilir. Bir referans, ödünç verenden daha uzun yaşayamaz.
Aynı veriye aynı anda istediğin kadar &T alabilirsin — okuyucular birbirini rahatsız etmez. Hiçbiri veriyi değiştiremediği için bu güvenlidir.
fn main() {
let s = String::from("merhaba");
let r1 = &s; // paylaşımlı borrow
let r2 = &s; // ikinci paylaşımlı borrow — sorun yok
println!("{r1} ve {r2} ve {s}"); // üçü de okuyabilir
}
Paylaşımlı referans salt okunur bir görünümdür: r1.push_str(...) derlenmez. Değiştirmek için &mut gerekir (sonraki bölüm). C'de const'ı cast'le delebilirsin; Rust'ta &T üzerinden mutasyon hiçbir cast ile mümkün değildir (interior mutability hariç — ileri konu).
Referansları dereference etmek için * kullanılır, ama method çağrıları ve çoğu operatör otomatik dereference (auto-deref) yaptığı için çoğu zaman * yazmazsın:
fn topla(v: &Vec<i32>) -> i32 {
let mut acc = 0;
for x in v { // &Vec üzerinde iter; x: &i32
acc += *x; // *x ile i32 değerine eriş
}
acc
}
Bu bölümde
&Tpaylaşımlı, salt-okunur referanstır;const T*'a benzer ama güvenli.- Aynı veriye aynı anda sınırsız sayıda
&Talınabilir. - Referans, ödünç verdiği değerden daha uzun yaşayamaz — dangling derleme zamanında engellenir.
&Tüzerinden mutasyon yoktur; auto-deref sayesinde*çoğu zaman gerekmez.
06 &mut T — değiştirilebilir ödünç
Değiştirilebilir referans &mut T veriyi değiştirmene izin verir — ama aynı anda tek tane olabilir.
&mut T ile ödünç aldığın veriyi değiştirebilirsin. Kritik kısıt şudur: bir değer için aynı anda yalnızca bir &mut var olabilir ve bir &mut aktifken aynı veriye başka hiçbir referans (ne & ne &mut) olamaz. Üstelik veriyi &mut ile ödünç verebilmek için değişkenin de mut olması gerekir.
fn ekle(s: &mut String) {
s.push_str(" dünya"); // &mut üzerinden mutasyon serbest
}
fn main() {
let mut s = String::from("merhaba"); // mut: değişebilir
ekle(&mut s);
println!("{s}"); // -> merhaba dünya
}
Tek &mut kuralı neden var?
Cevap: veri yarışını (data race) derleme zamanında imkânsız kılmak. Bir veri yarışı, en az iki erişim aynı belleğe ulaşırken en az biri yazma yapıyor ve erişimler senkronize değilse oluşur. Eğer aynı anda hem bir yazıcı (&mut) hem de başka bir okuyucu/yazıcı olsaydı, klasik data race elimizde olurdu. Rust bunu tip sisteminde yasaklar.
fn main() {
let mut s = String::from("x");
let r1 = &mut s;
let r2 = &mut s; // HATA[E0499]: ikinci &mut, aynı anda olamaz
println!("{r1} {r2}");
}
Aynı garanti tek iş parçacığında bile geçerlidir; çünkü mutable aliasing iterator invalidation gibi tek-thread hatalarına da yol açar (örn. bir Vec'i üzerinde iter ederken değiştirmek). C++'ta bu UB; Rust'ta derlenmeyen koddur.
Bu bölümde
&mut Tveriyi değiştirmeye izin verir; değişken demutolmalıdır.- Aynı anda yalnızca bir
&mutolabilir ve o aktifken başka referans olamaz. - Bu kuralın amacı veri yarışını ve mutable aliasing'i derleme zamanında elemektir.
- Tek-thread iterator invalidation gibi hatalar da aynı kural sayesinde imkânsızdır.
07 Borrow checker kuralları
Tüm borrow sistemi tek ilkeye iner: aliasing XOR mutation — ya çok okuyucu, ya tek yazıcı; ikisi birden asla.
Önceki iki bölümün özeti tek bir invariant'tır. Herhangi bir an için, bir veriye ya:
çok sayıda &T (okuyucu) XOR tam bir tane &mut T (yazıcı)
Aynı anda hem paylaşımlı okuyucular hem de bir yazıcı olamaz. Bu "aliasing XOR mutability" ilkesi, hem veri yarışlarını hem de bellek bozulmasını (memory corruption) statik olarak engelleyen tek kuraldır. Borrow checker dediğimiz derleyici aşaması tam olarak bunu kontrol eder.
NLL: referanslar son kullanımda biter
Erken Rust'ta bir referans, içinde tanımlandığı blok bitene kadar "canlı" sayılırdı. Bugün Non-Lexical Lifetimes (NLL) sayesinde bir borrow, son kullanıldığı satırda sona erer — kapanış parantezinde değil. Bu sayede aşağıdaki kod derlenir:
fn main() {
let mut s = String::from("x");
let r = &s; // paylaşımlı borrow başlar
println!("{r}"); // r'nin SON kullanımı -> borrow burada biter (NLL)
let m = &mut s; // OK: artık aktif paylaşımlı borrow yok
m.push_str("y");
println!("{m}");
}
Tipik hata ve düzeltmesi
En sık görülen ihlal: bir okuyucu hâlâ canlıyken yazmaya kalkmak. Aşağıda hata ve iki düzeltme yolu:
fn main() {
let mut v = vec![1, 2, 3];
let ilk = &v[0]; // paylaşımlı borrow, ilk hâlâ canlı
v.push(4); // HATA[E0502]: v'yi &mut almaya çalışır; ilk borrow aktif
println!("{ilk}"); // ilk'in kullanımı borrow'u canlı tutuyor
}
// DÜZELTME: ilk'i push'tan ÖNCE kullanıp bitir (NLL),
// ya da değeri kopyala: let ilk = v[0]; (i32 Copy).
v.push(4)'ün tehlikesi teorik değil: Vec büyürken heap'i yeniden tahsis edip taşıyabilir; o anda ilk dangling pointer olurdu. İşte borrow checker'ın engellediği tam C/C++ hatası — iterator/pointer invalidation.
Bu bölümde
- Tek invariant: aliasing XOR mutation — ya çok
&T, ya tek&mut T. - NLL ile bir borrow son kullanımında biter, blok sonunda değil — kod daha esnek.
- En sık hata: canlı bir
&Tvarken&mutalmak (E0502). - Bu kural iterator/pointer invalidation gibi gerçek C/C++ hatalarını statik olarak engeller.
08 Slice'lar — &[T] ve &str
Slice, sahiplik almadan bir koleksiyonun bitişik bir parçasına açılan ödünç pencere — ptr + uzunluktan ibaret.
Bir slice (dilim), bir koleksiyonun bitişik (contiguous) bir aralığına ödünç verilmiş görünümdür. Sahiplik almaz; içinde sadece bir pointer ve bir uzunluk taşır (fat pointer). C'deki "pointer + length" idiomunun tip-güvenli, sınır-takipli halidir. İki temel slice tipi vardır:
fn main() {
let s = String::from("merhaba dünya");
let ilk: &str = &s[0..7]; // "merhaba" — sahiplik yok, ödünç
let v = vec![10, 20, 30, 40];
let orta: &[i32] = &v[1..3]; // [20, 30]
println!("{ilk} / {orta:?}");
}
Parametre tipi: neden &str ve &[T]?
Bir fonksiyon parametresinde &String yerine &str, &Vec<T> yerine &[T] almak en iyi pratiktir. Sebep: slice daha geneldir. &str alan bir fonksiyon hem String (deref ile) hem string literal hem alt-dilim kabul eder; tek bir imza her kaynağa uyar.
// İYİ: &str daha esnek — String, literal, dilim hepsi geçer.
fn ilk_kelime(s: &str) -> &str {
match s.find(' ') {
Some(i) => &s[..i],
None => s,
}
}
fn main() {
let sahip = String::from("merhaba dünya");
println!("{}", ilk_kelime(&sahip)); // String -> &str (deref coercion)
println!("{}", ilk_kelime("selam ck")); // literal zaten &str
}
ilk_kelime'nin dönüşü girdiyi ödünç alır; derleyici lifetime ilişkisini otomatik çıkarır. Bu yüzden döndürülen &str, kaynak String hayatta olduğu sürece geçerlidir — kaynağı drop edersen slice'ı kullanmak derlenmez. Dangling slice imkânsızdır.
Bu bölümde
- Slice = sahiplik almayan, ptr+len taşıyan ödünç görünüm; sınır-takipli C "ptr+length".
&strString/literal üzerine,&[T]Vec/dizi üzerine dilimdir.- Parametrede
&str/&[T]tercih et — deref coercion sayesinde daha çok kaynağa uyar. - Slice kaynağına bağlı yaşar; kaynak ölünce slice kullanımı derlenmez (dangling yok).
09 Drop, RAII ve sık hatalar
Drop trait'i Rust'ın RAII'sidir — C++ destructor'ının birebir karşılığı, ama move sayesinde double-drop garantili imkânsız.
Bir tip scope'tan çıkarken kaynak bırakması gerekiyorsa Drop trait'ini implement eder. Bu, C++ destructor'ının doğrudan karşılığıdır: dosya kapatma, soket bırakma, kilit çözme gibi temizlikler burada yapılır. String, Vec, File, MutexGuard hepsinin bir Drop'u vardır ve derleyici çağrıyı scope sonuna otomatik yerleştirir.
struct Kaynak {
ad: String,
}
impl Drop for Kaynak {
fn drop(&mut self) { // C++ ~Kaynak() gibi
println!("bırakılıyor: {}", self.ad);
}
}
fn main() {
let _a = Kaynak { ad: String::from("A") };
{
let _b = Kaynak { ad: String::from("B") };
} // -> "bırakılıyor: B" (ters sırada, B önce)
} // -> "bırakılıyor: A"
C++'ta unique_ptr taşındığında kaynak nesne null'a set edilir ki yıkıcı iki kez free yapmasın. Rust'ta bu davranış dile gömülüdür: bir değer move edildiğinde derleyici eski sahibin drop'unu çalıştırmaz; yalnızca son geçerli sahip drop edilir. Böylece double-drop tasarımdan imkânsızdır — programcı disiplinine değil, sahiplik kurallarına dayanır.
| Kavram | C++ | Rust |
|---|---|---|
| Otomatik temizlik | destructor (~T()) | Drop::drop |
| Akıllı pointer | unique_ptr<T> | Box<T> |
| Move sonrası double-free | elle null'lamaya bağlı | dilce imkânsız |
| Erken bırakma | scope/blok hilesi | drop(x) fonksiyonu |
En sık 3 borrow checker hatası — oku ve çöz
.clone() ya da baştan borrow (&) ver.&mut. Çözüm: borrow'ları zamanda ayır (NLL), kapsamı daralt.Rust derleyici hata mesajları olağanüstü açıklayıcıdır: hatanın yerini, borrow'un nerede başlayıp nerede çakıştığını ve çoğu zaman somut bir düzeltme önerisini gösterir. Hata kodunu (E0502 gibi) rustc --explain E0502 ile detaylı okuyabilirsin.
Bu bölümde
Drop= Rust'ın RAII'si; derleyici drop çağrısını scope sonuna ters sırayla yerleştirir.- Move edilen değerin eski sahibi drop edilmez; double-free/double-drop dilce imkânsızdır.
Box<T>≈unique_ptr, erken bırakma içindrop(x)kullanılır.- En sık hatalar E0382/E0499/E0502; mesajlar açıklayıcıdır,
rustc --explainile derinleşir.