Ömürlerle Referansları Doğrulama
Ömürler (lifetimes), aslında şimdiye dek farkında olmadan kullandığımız bir başka jenerik türüdür. Ömürler, bir türün istediğimiz davranışa sahip olmasını sağlamak yerine, referansların onlara ihtiyaç duyduğumuz süre boyunca geçerli kalmasını güvence altına alır.
Bölüm 4’teki “Referanslar ve Ödünç Alma” kısmında değinmediğimiz bir ayrıntı, Rust’taki her referansın bir ömrü olmasıdır; yani o referansın geçerli olduğu kapsam. Tıpkı çoğu zaman türlerin derleyici tarafından çıkarılması gibi, ömürler de çoğu durumda örtüktür ve kendiliğinden çıkarılır. Nasıl yalnızca birden fazla türün mümkün olduğu belirsiz durumlarda açık tür bildirimi yazmamız gerekiyorsa, referansların ömürlerinin birbirleriyle birkaç farklı biçimde ilişkili olabileceği durumlarda da ömür bildirimleri eklememiz gerekir. Rust, çalışma zamanında kullanılan gerçek referansların kesinlikle geçerli olacağından emin olmak için bu ilişkileri jenerik ömür parametreleriyle bildirmemizi zorunlu tutar.
Ömür bildirimi kavramı diğer programlama dillerinin neredeyse hiçbirinde bulunmadığından ilk başta size yabancı gelecektir. Bu bölümde ömürlerin tüm ayrıntılarını ele almayacak olsak da, kavrama aşinalık kazanabilmeniz için ömür sözdizimiyle karşılaşacağınız yaygın senaryoları inceleyeceğiz.
Sarkan Referanslar
Ömürlerin temel amacı, sarkan referansları (dangling references) önlemektir. Sarkan referansların var olmasına izin verilseydi, bir program işaret etmeyi amaçladığı veri yerine bambaşka bir bellek alanındaki veriye başvururdu. Bir dış kapsama ve bir iç kapsama sahip olan Liste 10-16’daki programı ele alalım.
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
Not: Liste 10-16, 10-17 ve 10-23’teki örnekler değişkenleri ilk değer atamadan
bildirir. Bu, değişkenin null değerini tuttuğu anlamına gelmez; henüz
başlatılmamış olduğu anlamına gelir. Derleyici, kullanılmadan önce bütün olası
yürütme yollarında bir değer atanmış olmasını zorunlu tutar. Değerin
bulunmamasını çalışma zamanında temsil etmek istediğimizde ise Option<T>
kullanırız.
Dış kapsam ilk değeri olmayan r adında bir değişken bildirir; iç kapsam ise ilk değeri 5 olan x adında bir değişken bildirir. İç kapsamın içinde r’nin değerini x’e bir referans olarak atamaya çalışırız. Ardından iç kapsam sona erer ve r’deki değeri yazdırmaya çalışırız. Bu kod derlenmez; çünkü biz r’yi kullanmaya çalışmadan önce r’nin başvurduğu değer kapsam dışına çıkmıştır. Aldığımız hata mesajı şöyledir:
$ cargo run
Compiling bolum10 v0.1.0 (file:///projects/bolum10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `bolum10` (bin "bolum10") due to 1 previous error
Hata mesajı, x değişkeninin “yeterince uzun yaşamadığını” (does not live long enough) bildirir. Bunun nedeni, iç kapsam 7. satırda sona erdiğinde x değişkeninin kapsam dışına çıkıp düşürülecek (dropped here while still borrowed) olmasıdır. Oysa r dış kapsam boyunca hâlâ geçerlidir; kapsamı daha geniş olduğu için onun “daha uzun yaşadığını” söyleriz. Rust bu kodun çalışmasına izin verseydi, r değişkeni x kapsam dışına çıktığında serbest bırakılan belleğe başvuruyor olurdu ve r ile yapmaya çalıştığımız hiçbir işlem doğru çalışmazdı. Peki Rust bu kodun geçersiz olduğunu nasıl anlar? Bir ödünç alma denetleyicisi kullanır.
Ödünç Alma Denetleyicisi
Rust derleyicisi, tüm ödünç almaların geçerli olup olmadığını belirlemek amacıyla kapsamları karşılaştıran bir ödünç alma denetleyicisine (borrow checker) sahiptir. Liste 10-17, Liste 10-16’daki kodun değişken ömürlerini gösteren açıklamalar eklenmiş hâlini sunar.
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
r ve x değişkenlerinin sırasıyla 'a ve 'b olarak adlandırılan ömür bildirimleriBurada r’nin ömrünü 'a, x’in ömrünü ise 'b ile işaretledik. Gördüğünüz gibi içteki 'b bloğu, dıştaki 'a ömür bloğundan çok daha küçüktür. Derleme zamanında Rust bu iki ömrün büyüklüğünü karşılaştırır ve r’nin 'a ömrüne sahip olmasına karşın 'b ömrüne sahip bir belleğe başvurduğunu görür. 'b ömrü 'a’dan daha kısa olduğu için program reddedilir: Referansın işaret ettiği veri, referansın kendisi kadar uzun yaşamamaktadır.
Liste 10-18, sarkan referans oluşmayacak şekilde kodu düzeltir ve hiçbir hata vermeden derlenir.
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
Burada x, bu kez 'a’dan daha geniş olan 'b ömrüne sahiptir. Bu durum r’nin x’e güvenle başvurabileceği anlamına gelir; çünkü Rust, x geçerli olduğu sürece r’deki referansın da her zaman geçerli olacağını bilir.
Referansların ömürlerinin ne anlama geldiğini ve Rust’ın referansların her zaman geçerli kalmasını sağlamak için kapsamları nasıl analiz ettiğini gördüğümüze göre, şimdi fonksiyon parametrelerindeki ve dönüş değerlerindeki jenerik ömürleri inceleyelim.
Fonksiyonlarda Jenerik Ömürler
İki string diliminden daha uzun olanını döndüren bir fonksiyon yazacağız. Bu fonksiyon iki string dilimi alacak ve tek bir string dilimi döndürecektir. en_uzun fonksiyonunu uyguladıktan sonra Liste 10-19’daki kod ekrana En uzun dizgi: abcd yazdırmalıdır.
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "xyz";
let sonuc = en_uzun(dizgi1.as_str(), dizgi2);
println!("En uzun dizgi: {sonuc}");
}
en_uzun fonksiyonunu çağıran main fonksiyonuen_uzun fonksiyonunun kendi parametrelerinin sahipliğini devralmasını istemediğimiz için, fonksiyonun String değerleri yerine birer referans olan string dilimlerini kabul etmesini istediğimize dikkat edin. Liste 10-19’da kullandığımız parametrelerin neden tam da aradığımız türler olduğuna dair daha ayrıntılı bilgi için Bölüm 4’teki “Parametre Olarak String Dilimleri” kısmına göz atabilirsiniz.
en_uzun fonksiyonunu Liste 10-20’de gösterildiği gibi yazmaya çalışırsak kod derlenmez.
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "xyz";
let sonuc = en_uzun(dizgi1.as_str(), dizgi2);
println!("En uzun dizgi: {sonuc}");
}
fn en_uzun(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
en_uzun fonksiyonu uygulamasıBunun yerine ömürlerle ilgili şu hatayı alırız:
$ cargo run
Compiling bolum10 v0.1.0 (file:///projects/bolum10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn en_uzun(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn en_uzun<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `bolum10` (bin "bolum10") due to 1 previous error
Hata başlığı ömür belirtecinin eksik olduğunu (missing lifetime specifier) bildirir; yardım (help) metni ise dönüş türünün ödünç alınmış bir değer içerdiğini ancak imzanın bu değerin x’ten mi yoksa y’den mi ödünç alındığını söylemediğini açıklayarak adlandırılmış bir jenerik ömür parametresi eklememizi önerir. Doğrusu döndürülen referansın hangisinden geleceğini biz de bilemeyiz; çünkü fonksiyon gövdesindeki if bloğu x’e ait bir referans döndürürken else bloğu y’ye ait bir referans döndürmektedir!
Bu fonksiyonu tanımlarken fonksiyona hangi somut değerlerin geçirileceğini bilmediğimizden if kolunun mu yoksa else kolunun mu çalışacağını bilemeyiz. Aynı şekilde fonksiyona geçirilecek referansların somut ömürlerini de bilmiyoruz; dolayısıyla döndürdüğümüz referansın her zaman geçerli olup olmayacağını anlamak için Liste 10-17 ve 10-18’de yaptığımız gibi doğrudan kapsamlara bakamayız. Ödünç alma denetleyicisi de bunu belirleyemez; çünkü x ve y’nin ömürlerinin dönüş değerinin ömrüyle nasıl bir ilişki içinde olduğunu bilmez. Bu hatayı gidermek için referanslar arasındaki ilişkiyi tanımlayan jenerik ömür parametreleri ekleyeceğiz; böylece ödünç alma denetleyicisi gerekli analizi yapabilecektir.
Ömür Bildirim Sözdizimi
Ömür bildirimleri (lifetime annotations), hiçbir referansın ne kadar yaşayacağını değiştirmez. Bunun yerine, ömürlerin kendisini etkilemeden birden fazla referansın ömürlerinin birbiriyle olan ilişkisini tarif eder. Tıpkı imzasında jenerik tür parametresi bulunan bir fonksiyonun herhangi bir türü kabul edebilmesi gibi, jenerik ömür parametresi belirten fonksiyonlar da herhangi bir ömre sahip referansları kabul edebilir.
Ömür bildirimlerinin biraz alışılmadık bir sözdizimi vardır: Ömür parametrelerinin adları kesme işaretiyle (') başlamalıdır ve tıpkı jenerik türlerde olduğu gibi genellikle tamamen küçük harfli ve çok kısa seçilir. Çoğu geliştirici ilk ömür bildirimi için 'a adını kullanır. Ömür parametresi bildirimlerini referansın & işaretinden hemen sonraya yazar, bildirim ile referansın türünü ayırmak için araya bir boşluk bırakırız.
Aşağıda birkaç örnek verilmiştir: ömür parametresi belirtilmemiş bir i32 referansı, 'a adında ömür parametresine sahip bir i32 referansı ve yine 'a ömrüne sahip değiştirilebilir bir i32 referansı:
&i32 // bir referans
&'a i32 // açık ömür bildirimine sahip bir referans
&'a mut i32 // açık ömür bildirimine sahip değiştirilebilir bir referans
Tek başına bir ömür bildiriminin pek bir anlamı yoktur; çünkü bu bildirimlerin amacı Rust’a birden fazla referansın jenerik ömür parametrelerinin birbiriyle nasıl ilişkili olduğunu anlatmaktır. Gelin en_uzun fonksiyonu bağlamında ömür bildirimlerinin birbirleriyle nasıl ilişkilendirildiğini inceleyelim.
Fonksiyon İmzalarında
Fonksiyon imzalarında ömür bildirimlerini kullanmak için, tıpkı jenerik tür parametrelerinde yaptığımız gibi, fonksiyon adı ile parametre listesi arasındaki açılı ayraçların (<>) içinde jenerik ömür parametrelerini bildirmemiz gerekir.
İmzanın şu kısıtlamayı ifade etmesini istiyoruz: Döndürülen referans, parametrelerin her ikisi de geçerli olduğu sürece geçerli olacaktır. Parametrelerin ömürleri ile dönüş değerinin ömrü arasındaki ilişki budur. Liste 10-21’de gösterildiği gibi ömre 'a adını verecek ve onu imzadaki her referansa ekleyeceğiz.
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "xyz";
let sonuc = en_uzun(dizgi1.as_str(), dizgi2);
println!("En uzun dizgi: {sonuc}");
}
fn en_uzun<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
'a ömrüne sahip olması gerektiğini belirten en_uzun fonksiyonu tanımıBu kod, Liste 10-19’daki main fonksiyonuyla birlikte kullanıldığında sorunsuz derlenmeli ve istediğimiz çıktıyı üretmelidir.
Fonksiyon imzası artık Rust’a şunu söyler: Belirli bir 'a ömrü için bu fonksiyon iki parametre alır ve her iki parametre de en az 'a ömrü kadar yaşayan birer string dilimidir. Fonksiyon imzası aynı zamanda fonksiyondan döndürülen string diliminin de en az 'a ömrü kadar yaşayacağını belirtir. Pratikte bu, en_uzun fonksiyonunun döndürdüğü referansın ömrünün, fonksiyon argümanlarının başvurduğu değerlerin ömürlerinden daha kısa olanına eşit olduğu anlamına gelir. Rust’ın bu kodu analiz ederken temel almasını istediğimiz ilişki tam olarak budur.
Unutmayın: Bu fonksiyon imzasında ömür parametrelerini belirttiğimizde, fonksiyona geçirilen ya da fonksiyondan döndürülen hiçbir değerin kapsamını veya bellekte kalma süresini değiştirmiyoruz. Yalnızca ödünç alma denetleyicisinin bu ömür kısıtlamalarına uymayan referansları reddetmesi gerektiğini belirtiyoruz. en_uzun fonksiyonunun x ve y’nin tam olarak ne kadar yaşayacağını bilmesi gerekmez; bu imzayı karşılayacak bir kapsamın 'a yerine konabilmesi yeterlidir.
Not: Değerin Kapsamı ile Referansın Ömrü ('a) Arasındaki Ayrım — Yeni başlayanların en sık düştüğü yanılgı, 'a gibi bir ömür bildiriminin veriyi bellekte daha uzun süre hayatta tutacağını sanmaktır. Bir değerin kapsamı (scope), o değerin bellekte ne zaman oluşturulup kapsam sonunda ne zaman temizleneceğini (drop edileceğini) belirler; bir referansın ömrü (lifetime) ise o referansın söz konusu değere güvenle başvurabildiği geçerlilik bölgesidir (ve bu ömür, değerin bellekte kaldığı süreyi asla aşamaz). Dolayısıyla açık bir 'a bildirimi:
- Değerlerin bellekte kalma süresini değiştirmez: Değerler kendi sahiplik ve düşürülme kurallarına göre temizlenir; bir ömür bildirimi bunların zamanını değiştirmez.
- Referansların ömür ilişkisini tarif eder: Ödünç alma denetleyicisine girdi referansları ile çıktı referansı arasındaki sözleşmeyi açıklar (örneğin
en_uzun<'a>imzasında: “Dönen referansı yalnızcaxveyreferanslarının ikisinin birden geçerli olduğu ortak kesişim bölgesi içinde kullanabilirsin”). - Sıfır çalışma zamanı maliyetlidir: Tüm doğrulama derleme zamanında yapılır; derlenmiş ikili dosyaya hiçbir ek işlem eklenmez.
Fonksiyonlarda ömürleri bildirirken bildirimler fonksiyon gövdesine değil, fonksiyon imzasına yazılır. Ömür bildirimleri tıpkı imzadaki türler gibi fonksiyon sözleşmesinin bir parçası hâline gelir. Fonksiyon imzalarının ömür sözleşmesini içermesi, Rust derleyicisinin yaptığı analizin çok daha yalın ve öngörülebilir olmasını sağlar. Bir fonksiyonun ömür bildirimlerinde veya çağrılma biçiminde bir sorun varsa, derleyici hataları kodumuzun tam olarak ilgili noktasına ve ihlal edilen kısıtlamaya kesin biçimde işaret edebilir. Rust derleyicisi ömür ilişkilerinde neyi kastettiğimize dair arka planda daha fazla tahminde bulunsaydı, sorunun asıl kaynağından çok uzaktaki bir kullanım noktasına işaret etmek zorunda kalabilirdi.
en_uzun fonksiyonuna somut referanslar geçirdiğimizde 'a yerine geçen somut ömür, x kapsamının y kapsamıyla örtüşen kısmıdır. Başka bir deyişle jenerik 'a ömrü, x ve y’nin ömürlerinden daha kısa olanına eşit somut ömrü alır. Döndürülen referansı da aynı 'a ömür parametresiyle bildirdiğimiz için, döndürülen referans da x ve y ömürlerinden kısa olanı boyunca geçerli olur.
Şimdi farklı somut ömürlere sahip referanslar geçirerek ömür bildirimlerinin en_uzun fonksiyonunu nasıl kısıtladığına bakalım. Liste 10-22 doğrudan çalışan yalın bir örnektir.
fn main() {
let dizgi1 = String::from("uzun dizgi uzundur");
{
let dizgi2 = String::from("xyz");
let sonuc = en_uzun(dizgi1.as_str(), dizgi2.as_str());
println!("En uzun dizgi: {sonuc}");
}
}
fn en_uzun<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
String değerlerinin referanslarıyla en_uzun fonksiyonunu kullanmaBu örnekte dizgi1 dış kapsamın sonuna kadar geçerlidir, dizgi2 iç kapsamın sonuna kadar geçerlidir ve sonuc da iç kapsamın sonuna kadar geçerli olan bir veriye başvurur. Bu kodu çalıştırdığınızda ödünç alma denetleyicisinin kullanımı onayladığını göreceksiniz; kod derlenecek ve ekrana En uzun dizgi: uzun dizgi uzundur yazdıracaktır.
Şimdi de sonuc değişkenindeki referansın ömrünün iki argümandan daha kısa ömürlü olanına uymak zorunda olduğunu gösteren bir örnek deneyelim. sonuc değişkeninin bildirimini iç kapsamın dışına taşıyacağız, ancak sonuc değişkenine değer atamasını dizgi2 ile aynı iç kapsamda bırakacağız. Ardından sonuc değişkenini kullanan println! çağrısını iç kapsam sona erdikten sonraya, yani dış kapsama taşıyacağız. Liste 10-23’teki bu kod derlenmeyecektir.
fn main() {
let dizgi1 = String::from("uzun dizgi uzundur");
let sonuc;
{
let dizgi2 = String::from("xyz");
sonuc = en_uzun(dizgi1.as_str(), dizgi2.as_str());
}
println!("En uzun dizgi: {sonuc}");
}
fn en_uzun<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
dizgi2 kapsam dışına çıktıktan sonra sonuc değişkenini kullanma girişimiBu kodu derlemeye çalıştığımızda şu hatayı alırız:
$ cargo run
Compiling bolum10 v0.1.0 (file:///projects/bolum10)
error[E0597]: `dizgi2` does not live long enough
--> src/main.rs:6:42
|
5 | let dizgi2 = String::from("xyz");
| ------ binding `dizgi2` declared here
6 | sonuc = en_uzun(dizgi1.as_str(), dizgi2.as_str());
| ^^^^^^ borrowed value does not live long enough
7 | }
| - `dizgi2` dropped here while still borrowed
8 | println!("En uzun dizgi: {sonuc}");
| ----- borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `bolum10` (bin "bolum10") due to 1 previous error
Hata mesajı, sonuc değişkeninin println! deyiminde geçerli olabilmesi için dizgi2’nin dış kapsamın sonuna kadar geçerli kalması gerektiğini gösterir. Rust bunu, fonksiyon parametrelerinin ve dönüş değerinin ömürlerini aynı 'a ömür parametresiyle bildirdiğimiz için bilir.
İnsan gözüyle bu koda baktığımızda dizgi1’in dizgi2’den daha uzun olduğunu ve dolayısıyla sonuc değişkeninin her zaman dizgi1’e bir referans tutacağını görebiliriz. dizgi1 henüz kapsam dışına çıkmadığı için dizgi1’e yapılan bir referans println! satırında hâlâ geçerli olacaktır. Ancak derleyici bu özel durumda referansın geçerli olduğunu göremez. Biz Rust’a en_uzun fonksiyonunun döndürdüğü referansın ömrünün, geçirilen referansların ömürlerinden kısa olanına eşit olduğunu bildirdik. Bu nedenle ödünç alma denetleyicisi, geçersiz bir referans barındırma ihtimali bulunduğu gerekçesiyle Liste 10-23’teki koda izin vermez.
en_uzun fonksiyonuna geçirilen referansların değerlerini, ömürlerini ve döndürülen referansın nasıl kullanıldığını değiştiren farklı deneyler tasarlamayı deneyin. Derlemeden önce deneylerinizin ödünç alma denetleyicisinden geçip geçmeyeceğine dair tahminler yürütün, ardından derleyerek haklı çıkıp çıkmadığınızı kontrol edin!
İlişkiler
Ömür parametrelerini nasıl belirtmeniz gerektiği, fonksiyonunuzun ne yaptığına bağlıdır. Örneğin en_uzun fonksiyonunun gövdesini en uzun string dilimi yerine her zaman ilk parametreyi döndürecek şekilde değiştirseydik, y parametresi üzerinde bir ömür belirtmemize gerek kalmazdı. Aşağıdaki kod sorunsuz derlenir:
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "efghijklmnopqrstuvwxyz";
let sonuc = en_uzun(dizgi1.as_str(), dizgi2);
println!("En uzun dizgi: {sonuc}");
}
fn en_uzun<'a>(x: &'a str, y: &str) -> &'a str {
x
}
Burada x parametresi ve dönüş türü için 'a ömür parametresini belirttik, ancak y parametresi için belirtmedik; çünkü y’nin ömrünün x’in ömrüyle ya da dönüş değeriyle hiçbir ilişkisi yoktur.
Bir fonksiyondan referans döndürürken, dönüş türünün ömür parametresi parametrelerden en az birinin ömür parametresiyle eşleşmelidir. Döndürülen referans parametrelerden birine başvurmuyorsa, fonksiyonun kendi içinde oluşturulmuş bir değere başvuruyor demektir. Ancak bu değer fonksiyon sonunda kapsam dışına çıkacağı için böyle bir referans kaçınılmaz olarak sarkan referans olur. en_uzun fonksiyonunun derlenmeyecek olan şu uygulama denemesini inceleyelim:
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "xyz";
let sonuc = en_uzun(dizgi1.as_str(), dizgi2);
println!("En uzun dizgi: {sonuc}");
}
fn en_uzun<'a>(x: &str, y: &str) -> &'a str {
let sonuc = String::from("gerçekten uzun dizgi");
sonuc.as_str()
}
Burada dönüş türü için 'a ömür parametresini belirtmiş olsak bile bu uygulama derlenmez; çünkü dönüş değerinin ömrü parametrelerin ömrüyle hiçbir şekilde ilişkili değildir. Aldığımız hata mesajı şöyledir:
$ cargo run
Compiling bolum10 v0.1.0 (file:///projects/bolum10)
error[E0515]: cannot return value referencing local variable `sonuc`
--> src/main.rs:11:5
|
11 | sonuc.as_str()
| -----^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `sonuc` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `bolum10` (bin "bolum10") due to 1 previous error
Sorun şudur: sonuc yerel değişkeni en_uzun fonksiyonunun sonunda kapsam dışına çıkar ve bellekten temizlenir; biz ise fonksiyondan sonuc değişkenine bir referans döndürmeye çalışıyoruz (hata mesajındaki cannot return value referencing local variable `sonuc` ifadesi tam olarak bunu söyler). Sarkan referansı ortadan kaldıracak hiçbir ömür parametresi bildirimi yoktur ve Rust sarkan referans oluşturmamıza izin vermez. Böyle bir durumda en doğru çözüm, referans yerine String gibi sahipli bir veri türü döndürmektir; böylece değerin sahipliği çağıran fonksiyona taşınır ve değeri temizleme sorumluluğu çağıran fonksiyona geçer.
Özetle ömür sözdizimi, fonksiyonların çeşitli parametrelerinin ve dönüş değerlerinin ömürlerini birbirine bağlamakla ilgilidir. Bu bağlantılar kurulduktan sonra Rust, bellek açısından güvenli işlemlere izin vermek ve sarkan işaretçiler oluşturacak ya da bellek güvenliğini ihlal edecek işlemleri engellemek için yeterli bilgiye sahip olur.
Struct Tanımlarında
Şimdiye kadar tanımladığımız struct’ların tümü sahipli türler barındırıyordu. Struct’ları referans tutacak şekilde de tanımlayabiliriz; ancak bu durumda struct tanımındaki her referansa bir ömür bildirimi eklememiz gerekir. Liste 10-24’te bir string dilimi tutan OnemliAlinti adlı bir struct yer almaktadır.
struct OnemliAlinti<'a> {
bolum: &'a str,
}
fn main() {
let roman = String::from("Bana İsmail deyin. Birkaç yıl önce...");
let ilk_cumle = roman.split('.').next().unwrap();
let i = OnemliAlinti { bolum: ilk_cumle };
}
Bu struct, bir referans olan string dilimini barındıran bolum adlı tek bir alana sahiptir. Tıpkı jenerik veri türlerinde olduğu gibi, jenerik ömür parametresinin adını struct adından sonra açılı ayraçlar içinde bildiririz; böylece ömür parametresini struct tanımının gövdesinde kullanabiliriz. Bu bildirim, bir OnemliAlinti örneğinin bolum alanında tuttuğu referansın işaret ettiği veriden daha uzun yaşayamayacağı anlamına gelir.
Buradaki main fonksiyonu, roman değişkeninin sahip olduğu String değerinin ilk cümlesine referans tutan bir OnemliAlinti örneği oluşturur. roman içindeki veri OnemliAlinti örneği oluşturulmadan önce zaten mevcuttur. Ayrıca roman, OnemliAlinti örneği kapsam dışına çıktıktan sonra kapsam dışına çıkar; dolayısıyla OnemliAlinti örneğindeki referans geçerlidir.
Not: Struct tanımındaki 'a ömür parametresi, bu struct’ın bir veriyi ödünç
aldığını türün adına kazır: OnemliAlinti<'a> değeri, işaret ettiği verinin
en az 'a boyunca geçerli olması gereken bir referans taşır. Struct üzerinden
bu referansı kullanabildiğimiz süre, verinin geçerlilik süresini aşamaz.
Derleyici bu bilgiyle, struct örneğinin ödünç aldığı veriden daha uzun
yaşamasını engeller. Örneğin roman değişkenini bir iç kapsamda oluşturup
OnemliAlinti örneğini o kapsamın dışında kullanmaya çalışırsak, Liste
10-16’dakine benzer bir error[E0597]: `roman` does not live long enough
hatası alırız:
let alinti;
{
let roman = String::from("Bana İsmail deyin. Birkaç yıl önce...");
let ilk_cumle = roman.split('.').next().unwrap();
alinti = OnemliAlinti { bolum: ilk_cumle };
} // `roman` burada düşürülür; ancak `alinti` hâlâ ona başvuruyor
println!("{}", alinti.bolum);
Bir struct’ın referans yerine String gibi sahipli bir alan tutması bu kısıtlamayı ortadan kaldırır; ancak o zaman veri kopyalanır veya taşınır. Referans tutan struct’lar, büyük bir veriyi kopyalamadan onun bir parçasına “pencere” açmak istediğiniz durumlar için uygundur.
Ömür Elizyonu
Her referansın bir ömrü olduğunu ve referans kullanan fonksiyonlar ya da struct’lar için ömür parametreleri belirtmeniz gerektiğini öğrendiniz. Ancak Bölüm 4’teki Liste 4-9’da yazdığımız ve Liste 10-25’te yeniden gösterilen fonksiyon, hem parametresi hem de dönüş türü referans olmasına rağmen hiçbir ömür bildirimi olmadan derlenmişti.
fn ilk_kelime(s: &str) -> &str {
let baytlar = s.as_bytes();
for (i, &oge) in baytlar.iter().enumerate() {
if oge == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let benim_dizgim = String::from("merhaba dünya");
// ilk_kelime `String`lerin dilimleri üzerinde çalışır
let kelime = ilk_kelime(&benim_dizgim[..]);
let benim_dizgi_sabitim = "merhaba dünya";
// ilk_kelime string sabitlerinin dilimleri üzerinde çalışır
let kelime = ilk_kelime(&benim_dizgi_sabitim[..]);
// String sabitleri *zaten* string dilimi olduğundan,
// bu dilim sözdizimi olmadan da çalışır!
let kelime = ilk_kelime(benim_dizgi_sabitim);
}
Bu fonksiyonun ömür bildirimleri olmadan derlenebilmesinin tarihsel bir nedeni vardır: Rust’ın erken sürümlerinde (1.0 öncesinde) her referans açık bir ömür gerektirdiği için bu kod derlenmezdi. O dönemde fonksiyon imzası şöyle yazılırdı:
fn ilk_kelime<'a>(s: &'a str) -> &'a str {
Çok sayıda Rust kodu yazıldıktan sonra Rust ekibi, geliştiricilerin belirli durumlarda hep aynı ömür bildirimlerini tekrar tekrar yazdığını fark etti. Bu durumlar öngörülebilirdi ve birkaç belirlenimci (deterministic) deseni izliyordu. Ekip bu desenleri doğrudan derleyiciye kodladı; böylece ödünç alma denetleyicisi bu senaryolarda ömürleri kendiliğinden çıkarabilir hâle geldi ve açık bildirimlere gerek kalmadı.
Rust tarihinin bu parçası önemlidir; çünkü ileride yeni belirlenimci desenlerin ortaya çıkıp derleyiciye eklenmesi mümkündür. Gelecekte daha da az ömür bildirimi yazmamız gerekebilir.
Rust’ın referans analizine kodlanmış olan bu desenlere ömür elizyon kuralları (lifetime elision rules) denir. Elizyon sözcüğü “atlama, düşürme” anlamına gelir: Bu kurallar, belirli kalıplara uyan ömür bildirimlerinin yazılmadan atlanabilmesini sağlar. Bunlar programcıların uyması gereken kurallar değildir; derleyicinin göz önünde bulundurduğu bir dizi özel durumdur ve kodunuz bu durumlara uyuyorsa ömürleri açıkça yazmanız gerekmez.
Elizyon kuralları, tür çıkarımındaki gibi eksiksiz bir çıkarım (full inference) sağlamaz. Rust bu kuralları uyguladıktan sonra referansların ömürleri konusunda hâlâ bir belirsizlik kalırsa, derleyici kalan referansların ömrünün ne olması gerektiğini tahmin etmeye çalışmaz. Tahmin yürütmek yerine size bir derleme hatası verir; siz de amaçladığınız ilişkiyi ifade eden ömür bildirimlerini ekleyerek hatayı çözersiniz.
Fonksiyon veya metot parametrelerindeki ömürlere girdi ömürleri (input lifetimes), dönüş değerlerindeki ömürlere ise çıktı ömürleri (output lifetimes) denir.
Derleyici, açık bildirimlerin bulunmadığı durumlarda referansların ömürlerini belirlemek için üç kural uygular. Birinci kural girdi ömürleri için, ikinci ve üçüncü kurallar ise çıktı ömürleri için geçerlidir. Derleyici üç kuralın sonuna geldiğinde hâlâ ömrünü belirleyemediği çıktı referansları kalmışsa bir hata vererek durur. Bu kurallar fn tanımlarının yanı sıra impl blokları için de geçerlidir.
- Birinci kural: Derleyici, referans olan her parametreye ayrı bir ömür parametresi atar. Başka bir deyişle tek referans parametreli bir fonksiyon tek bir ömür parametresi alır:
fn foo<'a>(x: &'a i32); iki referans parametreli bir fonksiyon iki ayrı ömür parametresi alır:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)ve bu böyle devam eder. - İkinci kural: Yalnızca tek bir girdi ömür parametresi varsa, o ömür tüm çıktı ömür parametrelerine atanır:
fn foo<'a>(x: &'a i32) -> &'a i32. - Üçüncü kural: Birden fazla girdi ömür parametresi varsa ancak fonksiyon bir metot olduğu için bunlardan biri
&selfveya&mut selfise,self’in ömrü tüm çıktı ömür parametrelerine atanır. Bu üçüncü kural, çok daha az sembol gerektirdiği için metotları okumayı ve yazmayı önemli ölçüde kolaylaştırır.
Şimdi kendimizi derleyicinin yerine koyalım ve Liste 10-25’teki ilk_kelime fonksiyonunun imzasında yer alan referansların ömürlerini bulmak için bu kuralları adım adım uygulayalım. İmza başlangıçta referanslarla ilişkilendirilmiş hiçbir ömür olmadan şöyledir:
fn ilk_kelime(s: &str) -> &str {
Ardından derleyici, her parametrenin kendi ömrünü almasını belirten birinci kuralı uygular. Alışıldığı üzere buna 'a adını verirsek imza şu hâle gelir:
fn ilk_kelime<'a>(s: &'a str) -> &str {
Tam olarak bir tane girdi ömrü bulunduğu için ikinci kural devreye girer. İkinci kural, tek girdi parametresinin ömrünün çıktı ömrüne atanmasını söyler; böylece imza şu şekli alır:
fn ilk_kelime<'a>(s: &'a str) -> &'a str {
Artık bu fonksiyon imzasındaki tüm referansların ömürleri bellidir ve derleyici, programcının imzaya açık ömür bildirimi eklemesine gerek kalmadan analizine devam edebilir.
Şimdi bir başka örneğe bakalım ve Liste 10-20’de üzerinde çalışmaya başladığımızda hiçbir ömür parametresi bulunmayan en_uzun fonksiyonunu ele alalım:
fn en_uzun(x: &str, y: &str) -> &str {
Birinci kuralı uygulayalım: Her referans parametresi kendi ömrünü alır. Bu kez bir yerine iki parametremiz olduğundan iki ayrı ömür elde ederiz:
fn en_uzun<'a, 'b>(x: &'a str, y: &'b str) -> &str {
Birden fazla girdi ömrü olduğu için ikinci kuralın uygulanamadığını görebilirsiniz. en_uzun bir metot değil serbest bir fonksiyon olduğundan ve parametrelerinden hiçbiri self olmadığından üçüncü kural da uygulanamaz. Üç kuralın tamamını işlettikten sonra dönüş türünün ömrünün ne olduğunu hâlâ belirleyemedik. Liste 10-20’deki kodu derlemeye çalışırken hata almamızın nedeni tam olarak budur: Derleyici ömür elizyon kurallarını sırasıyla uygulamış, ancak imzadaki referansların tüm ömürlerini yine de çözememiştir.
Üçüncü kural yalnızca metot imzalarında devreye girdiğinden, şimdi metot bağlamındaki ömürleri inceleyelim ve bu kural sayesinde metot imzalarında neden çoğu zaman açık ömür bildirimi yazmak zorunda kalmadığımızı görelim.
Metot Tanımlarında
Ömür parametrelerine sahip bir struct üzerinde metot tanımlarken, Liste 10-11’de gördüğümüz jenerik tür parametreleriyle aynı sözdizimini kullanırız. Ömür parametrelerini nerede bildirip nerede kullanacağımız, bunların struct alanlarıyla mı yoksa metot parametreleri ve dönüş değerleriyle mi ilişkili olduğuna bağlıdır.
Struct alanlarına ait ömür adlarının her zaman impl anahtar kelimesinden sonra bildirilmesi ve ardından struct adından sonra kullanılması gerekir; çünkü bu ömürler struct türünün ayrılmaz bir parçasıdır.
impl bloğu içindeki metot imzalarında yer alan referanslar, struct alanlarındaki referansların ömrüne bağlı olabileceği gibi onlardan tamamen bağımsız da olabilir. Ayrıca ömür elizyon kuralları sayesinde metot imzalarında açık ömür bildirimlerine çoğu zaman gerek kalmaz. Liste 10-24’te tanımladığımız OnemliAlinti struct’ını kullanarak birkaç örneğe bakalım.
İlk olarak tek parametresi self referansı olan ve dönüş değeri herhangi bir veriye referans olmayan (i32 döndüren) seviye adlı bir metot tanımlayalım:
struct OnemliAlinti<'a> {
bolum: &'a str,
}
impl<'a> OnemliAlinti<'a> {
fn seviye(&self) -> i32 {
3
}
}
impl<'a> OnemliAlinti<'a> {
fn duyur_ve_bolumu_dondur(&self, duyuru: &str) -> &str {
println!("Lütfen dikkat: {duyuru}");
self.bolum
}
}
fn main() {
let roman = String::from("Bana İsmail deyin. Birkaç yıl önce...");
let ilk_cumle = roman.split('.').next().unwrap();
let i = OnemliAlinti { bolum: ilk_cumle };
}
impl sonrasındaki ömür parametresi bildirimi (impl<'a>) ve tür adından sonraki kullanımı (OnemliAlinti<'a>) zorunludur; ancak birinci elizyon kuralı sayesinde self referansının ömrünü açıkça bildirmek zorunda değiliz.
Şimdi de üçüncü ömür elizyon kuralının devreye girdiği bir örneğe bakalım:
struct OnemliAlinti<'a> {
bolum: &'a str,
}
impl<'a> OnemliAlinti<'a> {
fn seviye(&self) -> i32 {
3
}
}
impl<'a> OnemliAlinti<'a> {
fn duyur_ve_bolumu_dondur(&self, duyuru: &str) -> &str {
println!("Lütfen dikkat: {duyuru}");
self.bolum
}
}
fn main() {
let roman = String::from("Bana İsmail deyin. Birkaç yıl önce...");
let ilk_cumle = roman.split('.').next().unwrap();
let i = OnemliAlinti { bolum: ilk_cumle };
}
Burada iki girdi ömrü vardır; dolayısıyla Rust birinci ömür elizyon kuralını uygulayarak hem &self hem de duyuru parametrelerine kendi ömürlerini verir. Ardından parametrelerden biri &self olduğu için üçüncü kural uyarınca dönüş türü &self’in ömrünü alır ve böylece tüm ömürler belirlenmiş olur.
Not: Üçüncü kuralın dönüş türüne &self’in ömrünü vermesi her zaman istediğiniz ilişki olmayabilir. Yukarıdaki metot self.bolum döndürdüğü için bu varsayılan doğrudur; ancak metot duyuru parametresine ait bir referans döndürseydi derleyici, duyuru’nun ömrü &self’in ömrüyle ilişkilendirilmediği için lifetime may not live long enough benzeri bir hata verirdi. Böyle durumlarda imzaya fn ornek<'b>(&self, duyuru: &'b str) -> &'b str gibi açık bir ömür bildirimi ekleyerek kastettiğiniz ilişkiyi belirtirsiniz. Rust’ta ayrıca, elizyon kurallarına bırakılan bir ömrü açıkça “atlanmış” olarak işaretleyen '_ (anonim ömür) sözdizimi vardır. Örneğin fn yazdir(a: OnemliAlinti<'_>) imzası, OnemliAlinti türünün bir referans taşıdığını okuyucuya gösterir ama ömre ad vermeye gerek bırakmaz; derleyici '_ yerine elizyon kurallarıyla belirlediği ömrü koyar. Başkalarının yazdığı Rust kodunda impl Trait for Sarmalayici<'_> ya da fmt::Formatter<'_> gibi kullanımlara sıkça rastlayacaksınız.
Statik Ömür
Değinmemiz gereken özel ömürlerden biri de 'static ömrüdür; bu ömür, ilgili referansın programın tüm çalışma süresi boyunca yaşayabileceğini ifade eder. Tüm string sabitleri 'static ömre sahiptir ve bunu açıkça şu şekilde yazabiliriz:
#![allow(unused)]
fn main() {
let s: &'static str = "Statik bir ömrüm var.";
}
Bu metnin baytları doğrudan programın derlenmiş ikili dosyasının (binary) içine gömülür ve program çalıştığı sürece her zaman erişilebilirdir. Bu nedenle tüm string sabitlerinin ömrü 'static’tir.
Derleyici hata mesajlarının yardım kısımlarında 'static ömrünü kullanmanız yönünde öneriler görebilirsiniz. Ancak bir referans için 'static ömrünü yazmadan önce, elinizdeki referansın işaret ettiği verinin gerçekten programınızın sonuna dek yaşayıp yaşamadığını —ve yaşamasını gerçekten isteyip istemediğinizi— dikkatlice düşünün. Çoğu zaman 'static ömrünü öneren bir hata mesajı, sarkan bir referans oluşturma girişiminden ya da mevcut ömürlerin birbiriyle uyuşmamasından kaynaklanır. Bu tür durumlarda doğru çözüm 'static yazmak değil, altta yatan sahiplik veya ömür uyumsuzluğunu gidermektir.
Not: &'static T ve T: 'static Uyarısına Yaklaşım — Derleyici çıktılarında 'static ifadesiyle iki farklı biçimde karşılaşabilirsiniz ve bunları birbirine karıştırmamak çok önemlidir:
&'static T(statik referans): Yalnızca program kapanana dek bellekte kalacağı garanti edilen verilere (string sabitleri veyastaticdeğişkenler gibi) işaret edebilir. Fonksiyon içindeki yerel bir değişkenden ödünç alınan referansa&'staticyazmak hatayı çözmez; çünkü yerel değişken fonksiyon bittiğinde bellekten silinir.T: 'static(statik ömür sınırı): Özellikle Bölüm 16’da göreceğimizthread::spawngibi veriyi başka bir thread’e (iş parçacığına) aktaran API’lerde derleyiciT: 'statickısıtlamasından söz edebilir. Bu kısıtlama verinin mutlaka&'staticbir referans olmasını gerektirmez; türün içinde kısa ömürlü ödünç alınmış referans bulunmamasını ister. DolayısıylaString,Vec<T>veyai32gibi sahipli türler deT: 'staticsınırını doğal olarak karşılar. Derleyici'staticönerdiğinde kendinize önce “Burada ödünç alınmış kısa ömürlü bir referans yerineStringgibi sahipli bir değeri taşıyarak mı ilerlemeliyim?” sorusunu sormanız neredeyse her zaman doğru çözüme götürür.
Jenerik Tür Parametreleri, Trait Sınırları ve Ömürler
Şimdi jenerik tür parametrelerini, trait sınırlarını ve ömürleri tek bir fonksiyonda bir arada belirtmenin sözdizimine kısaca göz atalım!
fn main() {
let dizgi1 = String::from("abcd");
let dizgi2 = "xyz";
let sonuc =
duyuruyla_en_uzun(dizgi1.as_str(), dizgi2, "Bugün birinin doğum günü!");
println!("En uzun dizgi: {sonuc}");
}
use std::fmt::Display;
fn duyuruyla_en_uzun<'a, T>(x: &'a str, y: &'a str, dyr: T) -> &'a str
where
T: Display,
{
println!("Duyuru! {dyr}");
if x.len() > y.len() {
x
} else {
y
}
}
Bu duyuruyla_en_uzun fonksiyonu, Liste 10-21’de yazdığımız ve iki string diliminden daha uzun olanını döndüren en_uzun fonksiyonunun genişletilmiş hâlidir. Şimdi ek olarak, where cümleciğinde belirtildiği üzere Display trait’ini uygulayan herhangi bir somut türle doldurulabilen T jenerik türünde dyr adında fazladan bir parametre alır. Bu ek parametre {} biçimlendiricisiyle ekrana yazdırıldığı için Display trait sınırı zorunludur. Ömürler de bir çeşit jenerik olduğundan, 'a ömür parametresi ile T jenerik tür parametresinin bildirimleri fonksiyon adından sonraki açılı ayraçların içinde aynı listede yer alır (ömür parametreleri tür parametrelerinden önce yazılır).
Özet
Bu bölümde pek çok önemli konuyu ele aldık! Artık jenerik tür parametrelerini, trait’leri, trait sınırlarını ve jenerik ömür parametrelerini tanıdığınıza göre, kod tekrarına düşmeden pek çok farklı senaryoda çalışabilen esnek kodlar yazmaya hazırsınız. Jenerik tür parametreleri kodunuzu farklı türlere uygulamanıza olanak tanır. Trait’ler ve trait sınırları, türler jenerik olsa bile kodun ihtiyaç duyduğu davranışlara sahip olmalarını garanti eder. Ömür bildirimleri ise bu esnek kodda hiçbir sarkan referans oluşmamasını sağlar. Üstelik tüm bu analizler derleme zamanında gerçekleştirildiğinden çalışma zamanı performansına hiçbir ek yük getirmez!
İster inanın ister inanmayın, bu bölümde incelediğimiz konularda öğrenilecek daha pek çok ayrıntı var: Bölüm 18’de trait’leri kullanmanın bir başka yolu olan trait nesnelerini (trait objects) ele alacağız. Ayrıca yalnızca çok ileri düzey senaryolarda ihtiyaç duyacağınız daha karmaşık ömür bildirimi durumları da mevcuttur; bunlar için Rust Referansı’nı inceleyebilirsiniz. Sırada ise yazdığınız kodun tam olarak beklediğiniz gibi çalıştığından emin olabilmeniz için Rust’ta nasıl otomatik test yazacağınızı öğreneceğiniz bölüm var.