00 Dangling pointer problemi
C'de yerel bir değişkenin adresini döndürmek tanımsız davranıştır; Rust aynı kodu derleme zamanında reddeder. Lifetime, bu reddin matematiksel temelidir.
C/C++ programcısı olarak bu hatayı binlerce kez görmüşsündür: fonksiyon yerel bir değişken oluşturur, ona bir pointer döndürür, fonksiyon biter, stack frame çözülür ve elinde artık geçersiz bir belleği gösteren bir pointer kalır. Derleyici çoğu zaman uyarır ama seni durdurmaz — kod derlenir, çalışır, hatta bazen "doğru" sonuç verir, ta ki o stack alanı başka bir şeyle ezilene kadar. Bu, dangling pointer sınıfının klasik örneğidir ve C'de tamamen senin sorumluluğundadır.
// C: derlenir, -Wall ile uyarır, AMA seni durdurmaz
const char *greet(void) {
char buf[32];
strcpy(buf, "merhaba");
return buf; // buf stack'te — fonksiyon bitince yok olur
}
int main(void) {
const char *p = greet();
printf("%s\n", p); // UB: p artık geçersiz belleği gösteriyor
}
Şimdi aynı niyeti Rust'ta ifade edelim. Rust bu kodu çalıştırmaz — bırak çalıştırmayı, derlemez bile. Hata mesajı net: yerel değişken yeterince uzun yaşamıyor.
fn greet() -> &String {
let s = String::from("merhaba");
&s // ERROR: cannot return reference to local variable `s`
} // s burada drop edilir — referans kime gösterecek?
C'deki "stack frame'in ömrü" kavramı Rust'ta birinci sınıf bir tür özelliğine dönüşür. Her referansın geçerli olduğu bir kod bölgesi vardır; buna lifetime denir. Derleyici, bir referansın işaret ettiği veriden daha uzun süre kullanılabilir olup olmadığını statik olarak kontrol eder. Cevap "evet" ise — yani potansiyel bir dangling varsa — derleme durur.
C: pointer = adres (anlamsız tam sayı, ömür bilgisi yok) Rust: referans = adres + lifetime (derleyicinin takip ettiği geçerlilik bölgesi)
Lifetime bir runtime nesnesi değildir. Üretilen makine kodunda lifetime'a ait tek bir bayt yoktur — tıpkı C'de bir pointer'ın "const" olmasının assembly'de iz bırakmaması gibi. Lifetime tamamen borrow checker'ın kafasında yaşayan, derleme zamanı bir kanıttır.
Bu bölümde
- C'de yerel değişkenin adresini döndürmek UB'dir ve derleyici seni durdurmaz.
- Rust aynı niyeti derleme zamanında reddeder — dangling referans tür sisteminde imkânsızdır.
- Lifetime = bir referansın geçerli kaldığı kod bölgesi.
- Lifetime'lar tamamen derleme zamanıdır; runtime'da sıfır maliyet.
01 Lifetime nedir?
Her referansın bir lifetime'ı vardır ve değişmez bir kural geçerlidir: referans, gösterdiği veriden daha uzun yaşayamaz. Derleyici bunu çoğu zaman sessizce kendisi çıkarsar.
Lifetime'ı bir kapsam ilişkisi olarak düşün. Veri bir bölgede yaşar; referans başka (genellikle daha dar) bir bölgede kullanılır. Geçerli olmasının tek koşulu, referansın kullanıldığı her noktada verinin hâlâ canlı olmasıdır. C'de bu ilişkiyi kafanda tutarsın; Rust'ta borrow checker tutar.
fn main() {
let r; // r'nin kapsamı: dış blok
{
let x = 5; // x'in kapsamı: iç blok
r = &x; // r, x'i borrow ediyor
} // x burada drop edilir
println!("{}", r); // ERROR: `x` does not live long enough
}
Derleyici bu örneği şöyle akıl yürüterek reddeder: r dış blok boyunca kullanılabilir, ama x yalnızca iç blokta yaşar. r = &x satırı, kısa ömürlü x'in referansını uzun ömürlü r'ye verir. Eğer buna izin verilseydi, son println! ölü belleğe erişirdi. NLL (Non-Lexical Lifetimes) sayesinde derleyici lifetime'ı sözdizimsel bloklara değil, fiili son kullanım noktasına kadar takip eder.
Çıkarsama: çoğu zaman 'a yazmazsın
Tek bir referansın söz konusu olduğu sıradan kodda lifetime'lar tamamen örtüktür. Aşağıdaki fonksiyonun gerçek imzası lifetime içerir, ama sen yazmazsın — derleyici doldurur (bunun kuralları s4'te).
// Yazdığın:
fn first_byte(s: &[u8]) -> u8 { s[0] }
// Derleyicinin gördüğü (elision sonrası):
fn first_byte<'a>(s: &'a [u8]) -> u8 { s[0] }
Lifetime'lar referansın ne kadar yaşayacağını uzatmaz veya kısaltmaz — sadece var olan ömürleri tanımlar ve doğrular. 'a yazmak bir şeyi canlı tutmaz; yalnızca derleyiciye "bu ilişkilerin tutarlı olmasını bekliyorum" der.
Bu bölümde
- Temel kural: referans gösterdiği veriden uzun yaşayamaz.
- Borrow checker, fiili son kullanım noktasına kadar (NLL) ömürleri takip eder.
- Tek referanslı sıradan kodda lifetime'lar örtüktür, sen yazmazsın.
- Lifetime notasyonu ömrü değiştirmez — yalnızca tanımlar ve doğrular.
02 Fonksiyon imzasında 'a
Derleyici, çıktı referansının hangi girdiye bağlı olduğunu kendi çıkaramadığında lifetime parametresi ister. Klasik örnek: iki string'den uzun olanı döndürmek.
Çıktı bir referans olduğunda ve birden fazla girdi referansı bulunduğunda derleyici çaresiz kalır: dönen referans x'in mi yoksa y'nin mi belleğini gösteriyor? Çalışma zamanı kararı (if) bunu derleme zamanında belirsiz kılar. Bu durumda sen ilişkiyi bildirmek zorundasın.
// 'a bir generic parametre: x, y ve dönüş değeri AYNI 'a'yı paylaşır.
// Anlamı: dönen referans, x ve y'den HANGİSİ daha kısa yaşıyorsa
// o kadar yaşar (lifetime'ların kesişimi).
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("uzun bir cümle");
let sonuc;
{
let s2 = String::from("kısa");
sonuc = longest(&s1, &s2); // dönüş 's2 kadar kısa yaşar
println!("{}", sonuc); // OK: burada s2 hâlâ canlı
}
// println!("{}", sonuc); // ERROR: s2 does not live long enough
}
<'a>, fonksiyon adından sonra tıpkı bir generic tür parametresi gibi bildirilir. x: &'a str "x, en az 'a kadar yaşayan bir str referansıdır" demektir. Üçünün de aynı 'a'yı taşıması, çıktının ömrünü en kısa girdiye bağlar; derleyici çağrı yerinde bu kısıtı somut ömürlerle çözer.
İmzanın anlamı: bir sözleşme
Lifetime parametreleri kesişler — birleşmez. 'a her zaman girdilerin en kısa ortak ömrüne çözülür. Bu yüzden çağıran taraf, dönen referansı en kısa ömürlü argümandan daha uzun kullanamaz.
Bu bölümde
- Birden çok girdi referansı + referans dönüşü → derleyici lifetime bağını çıkaramaz.
<'a>generic listesi gibi bildirilir; girdileri ve çıktıyı birbirine bağlar.- Paylaşılan
'a, çıktıyı en kısa girdinin ömrüne kilitler. - İmza bir sözleşmedir; gövde değil, imza borrow checker'ı yönlendirir.
03 Birden çok lifetime parametresi
Tek bir 'a, tüm referansları aynı ömre zorlar. Bazen referansların bağımsız ömürleri olduğunu belirtmek gerekir — işte o zaman <'a, 'b> devreye girer.
Önceki longest imzası, x ve y'yi aynı 'a'ya bağladığı için çağıranı gereksiz yere kısıtlar: dönüş hangisini seçerse seçsin, ömür ikisinin de kesişimine düşer. Ama çıktının yalnızca bir girdiye bağlı olduğunu biliyorsan, diğerini ayrı bir lifetime ile serbest bırakabilirsin.
// Dönüş yalnızca x'e bağlı. y bambaşka (daha kısa) yaşayabilir.
// y'yi log için kullanıyoruz ama referansını döndürmüyoruz.
fn prefix<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("karşılaştırılan: {}", y); // y sadece okunuyor
&x[..1] // dönüş x'in bir slice'ı
}
fn main() {
let uzun = String::from("merhaba");
let ilk;
{
let kisa = String::from("x"); // 'b kısa ömürlü
ilk = prefix(&uzun, &kisa); // dönüş 'a (uzun) kadar yaşar
} // kisa drop oldu — sorun yok
println!("{}", ilk); // OK: ilk, uzun'a bağlı
}
Burada 'b, kisa dış bloktan önce yok olsa bile sorun değildir; çünkü dönen referans 'b ile hiçbir ilişki kurmaz. Tek bir 'a kullansaydık bu kod derlenmezdi — kisa'nın erken ölümü ilk'i de geçersiz kılardı. Bağımsız parametreler, gereksiz kısıtları ortadan kaldırarak API'yi daha kullanışlı yapar.
| İmza | Anlam | Ne zaman |
|---|---|---|
| <'a> (x,y)→'a | Çıktı her iki girdiye bağlı | Hangisinin döneceği belirsiz |
| <'a,'b> (x:'a,y:'b)→'a | Çıktı yalnızca x'e bağlı | y sadece okunur/yan etki |
| <'a:'b> | 'a en az 'b kadar yaşar | Ömürler arası alt-küme kısıtı |
İhtiyacın yoksa fazladan lifetime parametresi ekleme. Çoğu doğru kod tek bir 'a ile çalışır; ayrı parametreler yalnızca derleyici "aynı ömrü zorlama" hatası verdiğinde veya API'yi kasıtlı gevşetmek istediğinde gereklidir. Aksi hâlde imza okunaksızlaşır.
Bu bölümde
- Tek
'atüm referansları aynı ömre zorlar; bazen bu fazla kısıtlayıcıdır. <'a, 'b>bağımsız ömürleri ayırır — çıktı yalnızca gerçekten bağlı olduğuna bağlanır.'a: 'bsözdizimi "'a en az 'b kadar yaşar" alt-küme kısıtını ifade eder.- Gereksiz lifetime parametresi imzayı kirletir; yalnızca derleyici zorladığında ekle.
04 Lifetime elision kuralları
Çoğu imzada 'a yazmadığımız için işler kolay görünür — bunun arkasında derleyicinin uyguladığı üç deterministik elision kuralı vardır.
Elision, derleyicinin yaygın ve belirsizliği olmayan kalıplarda lifetime'ları otomatik doldurmasıdır. Bu bir tahmin değil — kapalı bir kural setidir. Kurallar uygulanır; eğer hepsi bittiğinde çıktıdaki bir referansın lifetime'ı belirlenememişse, derleyici senden açıkça yazmanı ister. Üç kural şunlardır:
// Kural 2: tek girdi → çıktı o girdinin ömrünü alır. 'a yazmaya gerek yok.
fn trim_left(s: &str) -> &str {
s.trim_start()
}
// Derleyicinin gördüğü: fn trim_left<'a>(s: &'a str) -> &'a str
// Kural 3: &self varsa çıktı self'e bağlanır.
struct Buf { data: String }
impl Buf {
fn head(&self) -> &str { // dönüş &self ömründe
&self.data[..1]
}
}
Şimdi kuralların yetmediği duruma bak. İki girdi referansı vardır (Kural 1 her birine ayrı lifetime verir), self yoktur (Kural 3 geçersiz) ve tek girdi olmadığı için Kural 2 de uygulanamaz. Çıktının hangi girdiye bağlı olduğu belirsizdir — derleyici durur:
// ERROR: missing lifetime specifier — derleyici çözemez
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// Çözüm: s2'deki gibi açıkça yaz → fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
Elision yalnızca fonksiyon imzalarında ve impl metotlarında çalışır. Struct tanımlarında lifetime'ı asla atlayamazsın (s5) — bir struct referans tutuyorsa, lifetime parametresini açıkça yazmak zorundasın.
Bu bölümde
- Üç elision kuralı: ayrı girdi ömürleri, tek girdi → çıktı, &self → çıktı.
- Kurallar deterministiktir; tahmin değil, kapalı bir set.
- Kurallar çıktı ömrünü belirleyemezse derleyici açık
'aister (örn.longest). - Elision struct tanımlarında çalışmaz — orada lifetime zorunludur.
05 Struct'larda lifetime
Bir struct referans tutuyorsa, derleyiciye o referansın gösterdiği verinin struct'tan daha uzun yaşayacağını garanti etmelisin. İşte bu yüzden struct'lar lifetime parametresi taşır.
C'de bir struct içinde pointer tutmak gündelik bir iştir ve hiçbir garanti gerektirmez — pointer'ın gösterdiği bellek struct'tan önce serbest bırakılırsa, kullandığında use-after-free olur, derleyici umursamaz. Rust'ta referans tutan bir struct, o referansın geçerliliğini tür düzeyinde kanıtlamak zorundadır. Bu kanıt, struct'a eklenen bir lifetime parametresidir.
// Parser, sahip OLMADIĞI bir str'yi ödünç tutar. 'a şunu der:
// "Parser örneği, input'un gösterdiği veriden uzun yaşayamaz."
struct Parser<'a> {
input: &'a str,
pos: usize,
}
fn main() {
let kaynak = String::from("a=1;b=2");
let p = Parser { input: &kaynak, pos: 0 };
println!("{} konumdan başla", p.pos);
// p, kaynak'tan önce drop edilmek zorunda — derleyici doğrular
}
Lifetime, struct adının yanına generic parametre olarak gelir: Parser<'a>. Alan bildiriminde input: &'a str, "bu alan en az 'a kadar yaşayan bir str referansıdır" anlamına gelir. Artık derleyici, Parser örneğinin kullanıldığı her noktada input'un işaret ettiği verinin canlı olmasını garanti eder.
fn main() {
let p;
{
let kaynak = String::from("veri"); // dar kapsam
p = Parser { input: &kaynak, pos: 0 };
} // kaynak drop edildi
// ERROR: `kaynak` does not live long enough
println!("{}", p.input); // p hâlâ ölü veriyi tutuyordu
}
Referans tutan struct = "borrow eden konteyner". Bu pratik bir kısıt getirir: böyle bir struct, ödünç aldığı veriyi aşamayacağı için genellikle kısa ömürlüdür. Eğer verinin sahibi olmasını istiyorsan referans yerine String, Vec<T> gibi owned tipler kullan — o zaman lifetime parametresine hiç gerek kalmaz.
Bu bölümde
- Referans tutan struct, lifetime parametresini açıkça taşımak zorundadır:
struct Parser<'a>. input: &'a stralanı, struct'ı borrow edilen verinin ömrüne bağlar.- Struct örneği, ödünç aldığı veriden uzun yaşayamaz — derleyici garanti eder.
- Sahiplik istiyorsan owned tip kullan; lifetime ihtiyacı kalkar.
06 impl bloklarında lifetime
Lifetime taşıyan bir struct'a metot yazarken, lifetime'ı hem impl başlığında bildirmen hem de tip adında kullanman gerekir. Metot dönüşlerinde &self elision çoğu işi halleder.
Parser<'a> bir generic tiptir; tıpkı Vec<T> için impl<T> Vec<T> yazman gibi, lifetime için de impl<'a> Parser<'a> yazarsın. <'a> başlıkta bildirilir, Parser<'a>'da kullanılır. Bunu unutmak ("impl Parser") doğrudan derleme hatasıdır.
struct Parser<'a> { input: &'a str, pos: usize }
// 'a hem impl'de bildirilir hem de Parser<'a>'da kullanılır.
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, pos: 0 }
}
// Kural 3 (elision): &self varsa dönüş &self ömründe.
// Açık hâli: fn rest<'b>(&'b self) -> &'b str — biz yazmıyoruz.
fn rest(&self) -> &str {
&self.input[self.pos..]
}
}
Dikkat: dönüş 'self' mi yoksa 'a' mı?
İncelik şurada: rest bir slice döndürüyor. Bu slice self.input'un (yani 'a verisinin) bir parçasıdır, ama elision Kural 3 onu &self'in lifetime'ına bağlar. Çoğu durumda bu istediğin şeydir ve sorunsuz çalışır. Ancak dönen referansın self ölse bile 'a kadar yaşamasını istiyorsan, elision'ı geçersiz kılıp açıkça 'a yazmalısın:
impl<'a> Parser<'a> {
// Dönüş 'a'ya bağlı: self drop olsa bile slice yaşar.
// Çünkü input zaten 'a verisini gösteriyor, self'i değil.
fn whole(&self) -> &'a str {
self.input // &'a str döner, &self'e bağlı DEĞİL
}
}
fn whole(&self) -> &'a str ile fn rest(&self) -> &str arasındaki fark inceliklidir ama önemlidir: ilki dönüşü struct'ın tuttuğu veriye, ikincisi struct örneğinin kendisine (geçici borrow) bağlar. Veriyi struct'tan bağımsız döndürmek istiyorsan açık 'a şarttır.
Bu bölümde
impl<'a> Parser<'a>: lifetime başlıkta bildirilir, tipte kullanılır.- Metotlarda
&selfvarsa, elision Kural 3 dönüşü&selfömrüne bağlar. - Dönüşü struct'ın tuttuğu
'averisine bağlamak için açık-> &'a stryaz. -> &str(self'e bağlı) ile-> &'a str(veriye bağlı) anlamca farklıdır.
07 'static lifetime
'static, programın tüm çalışma süresi boyunca geçerli olan özel lifetime'dır. String literal'ler 'static'tir — ama &'static T ile T: 'static bound'unu karıştırmak en sık yapılan hatalardandır.
'static, "bu referans programın başından sonuna kadar geçerli" demektir. En yaygın örneği string literal'lerdir: kaynak koda gömülü olduklarından binary'nin salt-okunur (read-only) bölümünde yaşarlar ve hiçbir zaman drop edilmezler. C'deki static const char * ya da .rodata içindeki bir string'in tam karşılığıdır.
// String literal'in tipi &'static str — programın ömrü kadar yaşar.
let s: &'static str = "derlenmiş binary'de gömülü";
// 'static referans döndürmek güvenlidir: veri zaten ebedidir.
fn banner() -> &'static str {
"=== SİSTEM ===" // .rodata'da, dangling imkânsız
}
İki farklı 'static: &'static T vs T: 'static
Burada Rust öğrenenlerin en sık tökezlediği nokta var. 'static iki tamamen farklı bağlamda görünür ve anlamları aynı değildir:
| Form | Anlam | Örnek |
|---|---|---|
| &'static T | Bu REFERANS sonsuza dek geçerli (veri ebedi) | string literal |
| T: 'static (bound) | Bu TİP içinde 'static-olmayan referans TAŞIMAZ | String, i32, Vec<u8> |
Kritik fark: bir String, owned ve heap'te olduğu için T: 'static bound'unu sağlar — çünkü içinde başka bir veriye ödünç referans tutmaz, dolayısıyla istediğin kadar uzun yaşayabilir. Ama bu, o String'in &'static String olduğu anlamına gelmez; sıradan bir String kapsamı bitince drop edilir.
// T: 'static, T'nin sahip olduğu (owned) olmasını ister — referans değil.
fn spawn_uyumlu<T: 'static>(v: T) { /* thread'e taşınabilir */ }
fn main() {
let sahipli = String::from("owned");
spawn_uyumlu(sahipli); // OK: String, T: 'static sağlar (ödünç tutmaz)
let x = 5;
let r = &x;
// spawn_uyumlu(r); // ERROR: &x 'static değil, x ile sınırlı
}
Bir derleyici hatasını "&'static ekleyerek" susturmaya çalışma — bu neredeyse her zaman yanlış düzeltmedir. Genellikle gerçek sorun, verinin yeterince yaşamamasıdır; çözümü ömrü uzatmak değil, sahipliği almak (owned tip) veya yapıyı yeniden tasarlamaktır. &'static bir "kaçış kapısı" değildir.
Bu bölümde
'static= programın tüm ömrü; string literal'ler doğal olarak&'static str'dir.&'static T"referans ebedi" demek;T: 'static"tip ödünç referans taşımaz" demek.Stringgibi owned tiplerT: 'staticbound'unu sağlar ama&'staticdeğildir.&'static'i hata susturmak için kullanma — gerçek çözüm genelde sahipliktir.
08 Lifetime bound'lar
Generic kod ile lifetime'lar birleştiğinde, "bu tipin içindeki referanslar en az 'a kadar yaşamalı" gibi kısıtlar gerekir. Bunlar T: 'a bound'larıdır ve trait nesnelerinde özel önem kazanır.
Bir generic T, içinde referanslar barındırabilir (örneğin T = &'b str). Eğer böyle bir T'yi 'a ömürlü bir bağlamda kullanacaksan, T'nin içindeki tüm referansların da en az 'a kadar yaşadığını garanti etmen gerekir. Bunu T: 'a bound'u ifade eder: "T tipi 'a süresince geçerlidir".
// Wrapper, 'a ömürlü bir T referansı tutar. T: 'a şunu garanti eder:
// T'nin İÇİNDEKİ referanslar da en az 'a kadar yaşar.
struct Wrapper<'a, T: 'a> {
item: &'a T,
}
// where ile aynı kısıt, daha okunur:
fn ilk<'a, T>(slice: &'a [T]) -> &'a T
where
T: 'a,
{
&slice[0]
}
Trait nesnelerinde lifetime: Box<dyn Trait + 'a>
Trait nesneleri (dyn Trait) varsayılan olarak 'static ömür varsayar — çünkü kutunun içindeki somut tipin ne kadar yaşadığını derleyici bilemez ve en güvenli varsayım "ebedi"dir. Eğer trait nesnesi ödünç veri tutuyorsa, bu varsayımı açıkça gevşetmen gerekir: + 'a ekleyerek.
trait Yaz { fn yaz(&self) -> String; }
// + 'a: bu trait nesnesi 'a ömürlü veri tutabilir, 'static olmak zorunda değil.
fn kutula<'a>(d: &'a str) -> Box<dyn Yaz + 'a> {
struct S<'a>(&'a str);
impl<'a> Yaz for S<'a> {
fn yaz(&self) -> String { self.0.to_string() }
}
Box::new(S(d))
}
// '+ 'a' yazmazsan: dyn Yaz varsayılan 'static olur, &'a str sığmaz → ERROR
Bu bölümde
T: 'abound'u, T'nin içerdiği referansların en az'ayaşamasını garanti eder.where T: 'aaynı kısıtı daha okunur biçimde ifade eder.dyn Traitvarsayılan olarak'staticvarsayar; ödünç veri için+ 'aşarttır.Box<dyn Trait + 'a>, trait nesnesini somut bir ömre bağlar.
09 Yaygın hatalar ve gerçek örnek
İki kanonik borrow checker hatasını, neden self-referential struct'ların yasak olduğunu ve lifetime'ların gerçek işe yaradığı yeri görelim: küçük bir slice tabanlı tokenizer.
Hata 1: "borrowed value does not live long enough"
Geçici bir değerden referans alıp onu kapsam dışına taşımaya çalıştığında çıkar. Veri yok oldu, referans hâlâ ona bakıyor.
fn main() {
let r;
{
let gecici = String::from("yok olacak");
r = &gecici; // ERROR: borrowed value does not live long enough
} // gecici drop edildi
println!("{}", r);
}
Hata 2: "cannot return reference to local variable"
s0'daki dangling örneğinin aynısı. Fonksiyon içinde oluşturulan veriye referans döndürülemez — fonksiyon bitince veri ölür. Çözüm: referans değil, owned değer döndür.
// YANLIŞ: yerel String'e referans dönemez
fn uret_yanlis() -> &str {
let s = String::from("yeni");
&s // ERROR: cannot return reference to local variable `s`
}
// DOĞRU: sahipliği taşı, referans verme
fn uret_dogru() -> String {
String::from("yeni") // owned, çağırana taşınır (move)
}
Neden self-referential struct olmaz?
Bir struct'ın bir alanı, aynı struct'ın başka bir alanına referans tutamaz. Sebebi şudur: Rust değerleri bellekte taşınabilir (move semantiği — struct kopyalanıp eski yer geçersizleşebilir). Taşıma sırasında iç referans eski adresi gösterir ve dangling olur. Derleyici bu yüzden self-referential yapıları reddeder.
Self-referential yapı gerçekten gerekiyorsa çözümler vardır: index/offset tutmak (referans yerine usize), Pin + unsafe, ya da Rc/Arc ile paylaşımlı sahiplik. Ama %99 durumda yapıyı yeniden tasarlamak — örneğin veriyi ayrı tutup sadece slice index'leri saklamak — doğru cevaptır.
Kapanış: lifetime'larla slice tabanlı tokenizer
Lifetime'ların asıl gücü burada görünür: girdi string'ini hiç kopyalamadan, yalnızca onun dilimlerini (zero-copy) döndüren bir tokenizer. Her token, orijinal girdinin bir &'a str slice'ıdır — kopya yok, ek heap tahsisi yok. Lifetime sistemi, döndürülen token'ların girdiden uzun yaşamayacağını garanti eder.
// Tokenizer girdiyi ödünç tutar; ürettiği token'lar girdinin slice'ları.
struct Tokenizer<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Tokenizer<'a> {
fn new(input: &'a str) -> Self {
Tokenizer { input, pos: 0 }
}
// Dönüş &'a str: token, girdiyle aynı ömürde — kopya değil, slice.
fn next_token(&mut self) -> Option<&'a str> {
let bytes = self.input.as_bytes();
// boşlukları atla
while self.pos < bytes.len() && bytes[self.pos] == b' ' {
self.pos += 1;
}
if self.pos >= bytes.len() {
return None; // girdi bitti
}
let bas = self.pos;
while self.pos < bytes.len() && bytes[self.pos] != b' ' {
self.pos += 1;
}
Some(&self.input[bas..self.pos]) // zero-copy slice
}
}
fn main() {
let kaynak = String::from("let x = 5");
let mut tk = Tokenizer::new(&kaynak);
while let Some(tok) = tk.next_token() {
println!("token: {:?}", tok); // "let" "x" "=" "5"
}
// tk ve token'lar kaynak'tan uzun yaşayamaz — derleyici garanti eder
}
Bu tasarımda 'a tek bir şeyi söylüyor ama her şeyi güvenli kılıyor: Tokenizer ve onun ürettiği her token, kaynak string'inden daha uzun yaşayamaz. C'de aynı zero-copy tokenizer'ı yazabilirsin — ama girdi buffer'ı erken serbest bırakıldığında token pointer'ların sessizce dangling olur. Rust'ta bu hata derleme zamanında imkânsızdır.
Bu bölümde
- "does not live long enough" = geçici veriden alınan referansı taşımaya çalışmak.
- "cannot return reference to local variable" = yerel veriye referans döndürmek; çözümü owned dönüş.
- Self-referential struct yasaktır çünkü değerler taşınabilir; çözüm index/Pin/Rc.
- Zero-copy tokenizer: lifetime'lar slice tabanlı parsing'i kopyasız ve dangling'siz kılar.