Referanslar ve Ödünç Alma
Liste 4-5’teki demet kullanan kodun temel sorunu şudur: String değeri uzunlugu_hesapla fonksiyonuna taşındığı için, fonksiyon çağrısından sonra bu String’i hâlâ kullanabilmek adına onu çağıran fonksiyona geri döndürmek zorunda kalırız. Bunun yerine, String değerine bir referans verebiliriz. Referans (reference), bellekteki bir adreste saklanan veriye ulaşmak için takip edebileceğimiz bir adres olması bakımından işaretçiye (pointer) benzer; o adresteki verinin sahibi ise başka bir değişkendir. Sıradan bir işaretçiden farklı olarak referansın, kendi ömrü boyunca belirli bir türden geçerli bir değere işaret edeceği garanti edilir.
Değerin sahipliğini devralmak yerine parametre olarak bir değere referans kabul eden uzunlugu_hesapla fonksiyonunu şu şekilde tanımlayıp kullanabilirsiniz:
fn main() {
let metin1 = String::from("merhaba");
let uzunluk = uzunlugu_hesapla(&metin1);
println!("'{metin1}' metninin uzunluğu: {uzunluk}.");
}
fn uzunlugu_hesapla(metin: &String) -> usize {
metin.len()
}
Öncelikle, değişken bildirimindeki ve fonksiyonun dönüş değerindeki tüm demet kodlarının ortadan kalktığına dikkat edin. İkinci olarak, uzunlugu_hesapla fonksiyonuna &metin1 aktardığımıza ve fonksiyon tanımında String yerine &String kullandığımıza bakın. Bu & işaretleri (ampersand) referansları temsil eder ve bir değerin sahipliğini üstlenmeden ona başvurmanıza olanak tanır. Şekil 4-6 bu kavramı görselleştirmektedir.
Şekil 4-6: String türündeki metin1 değişkenine işaret eden &String türündeki metin diyagramı
Not: & kullanarak referans oluşturmanın tam tersi başvuru çözme (dereferencing) işlemidir ve bu işlem başvuru çözme operatörü olan * ile gerçekleştirilir. Başvuru çözme operatörünün bazı kullanımlarını Bölüm 8’de görecek, başvuru çözmenin ayrıntılarını ise Bölüm 15’te inceleyeceğiz.
Şimdi buradaki fonksiyon çağrısına daha yakından bakalım:
fn main() {
let metin1 = String::from("merhaba");
let uzunluk = uzunlugu_hesapla(&metin1);
println!("'{metin1}' metninin uzunluğu: {uzunluk}.");
}
fn uzunlugu_hesapla(metin: &String) -> usize {
metin.len()
}
&metin1 sözdizimi, metin1’in değerine başvuran ancak onun sahibi olmayan bir referans oluşturmamızı sağlar. Referans bu değerin sahibi olmadığından, kullanımı sona erdiğinde işaret ettiği değer düşürülmez (drop edilmez).
Benzer şekilde fonksiyon imzası da metin parametresinin türünün bir referans olduğunu belirtmek için & kullanır. Şimdi koda açıklayıcı yorumlar ekleyelim:
fn main() {
let metin1 = String::from("merhaba");
let uzunluk = uzunlugu_hesapla(&metin1);
println!("'{metin1}' metninin uzunluğu: {uzunluk}.");
}
fn uzunlugu_hesapla(metin: &String) -> usize {
// metin bir String'e referanstır
metin.len()
} // Burada, metin kapsam dışına çıkar. Ancak metin referans verdiği şeyin
// sahipliğine sahip olmadığı için String düşürülmez (drop edilmez).
metin değişkeninin geçerli olduğu kapsam herhangi bir fonksiyon parametresinin kapsamıyla aynıdır; ancak metin değerin sahibi olmadığı için, kullanımı bittiğinde referansın işaret ettiği değer düşürülmez. Fonksiyonlar asıl değerler yerine referansları parametre olarak aldığında, sahipliği geri vermek amacıyla değerleri döndürmemize gerek kalmaz; çünkü sahipliği zaten hiç devralmamışızdır.
Referans oluşturma eylemine ödünç alma (borrowing) adını veririz. Tıpkı günlük hayatta olduğu gibi, bir başkasının sahip olduğu bir eşyayı ondan ödünç alabilirsiniz. İşiniz bittiğinde ise onu geri vermeniz gerekir; eşyanın sahibi siz değilsinizdir.
Peki ödünç aldığımız bir şeyi değiştirmeye çalışırsak ne olur? Liste 4-6’daki kodu deneyin. Şimdiden söyleyelim: Çalışmayacaktır!
fn main() {
let metin = String::from("merhaba");
degistir(&metin);
}
fn degistir(bir_metin: &String) {
bir_metin.push_str(", dünya");
}
İşte hata:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*bir_metin` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | bir_metin.push_str(", dünya");
| ^^^^^^^^^ `bir_metin` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn degistir(bir_metin: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Değişkenler varsayılan olarak nasıl değiştirilemez ise referanslar da öyledir. Bir değere yalnızca sıradan (değiştirilemez) bir referansımız varsa o değeri değiştirmemize izin verilmez (hata mesajındaki *bir_metin ifadesi bir_metin referansının işaret ettiği asıl değeri temsil eder; derleyici, bu değer bir & referansı arkasında olduğu için değiştirilebilir olarak ödünç alınamayacağını bildirir).
Değiştirilebilir Referanslar
Liste 4-6’daki kodu, ödünç alınan değeri değiştirmemize izin verecek şekilde birkaç küçük dokunuşla — bir değiştirilebilir referans (mutable reference) kullanarak — düzeltebiliriz:
fn main() {
let mut metin = String::from("merhaba");
degistir(&mut metin);
}
fn degistir(bir_metin: &mut String) {
bir_metin.push_str(", dünya");
}
İlk olarak metin değişkenini mut olarak tanımlarız. Ardından degistir fonksiyonunu çağırdığımız yerde &mut metin ifadesiyle değiştirilebilir bir referans oluşturur ve fonksiyon imzasını da bir_metin: &mut String yazarak değiştirilebilir referans kabul edecek biçimde güncelleriz. Bu imza, degistir fonksiyonunun ödünç aldığı değeri değiştireceğini açıkça ortaya koyar.
Değiştirilebilir referansların çok önemli bir kısıtlaması vardır: Bir değere ait etkin bir değiştirilebilir referansınız varsa, o değere başka hiçbir referansınız olamaz. metin değişkenine iki ayrı değiştirilebilir referans oluşturmayı deneyen aşağıdaki kod derlenmeyecektir:
fn main() {
let mut metin = String::from("merhaba");
let r1 = &mut metin;
let r2 = &mut metin;
println!("{r1}, {r2}");
}
İşte hata:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `metin` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut metin;
| ---------- first mutable borrow occurs here
5 | let r2 = &mut metin;
| ^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Bu hata mesajı, metin değişkenini aynı anda birden fazla kez değiştirilebilir olarak ödünç alamayacağımız (cannot borrow `metin` as mutable more than once at a time) için kodun geçersiz olduğunu belirtir. İlk değiştirilebilir ödünç alma işlemi r1 içindedir ve println! satırında kullanılana kadar geçerliliğini korumak zorundadır; ancak biz bu değiştirilebilir referansın oluşturulması ile kullanılması arasında, r1 ile aynı veriyi ödünç alan r2 içinde ikinci bir değiştirilebilir referans oluşturmaya çalıştık.
Aynı veriye aynı anda birden fazla değiştirilebilir referans oluşturulmasını engelleyen bu kısıtlama, veri değiştirmeye izin verir ancak bunu son derece kontrollü bir biçimde yapar. Çoğu programlama dili verileri dilediğiniz zaman değiştirmenize izin verdiğinden, yeni Rust geliştiricilerinin (Rustaceans) alışmakta zorlandığı konulardan biri budur. Bu kısıtlamanın en büyük getirisi ise Rust’ın derleme zamanında veri yarışlarını önleyebilmesidir. Veri yarışı (data race), yarış koşuluna (race condition) benzer ve şu üç davranış bir arada gerçekleştiğinde ortaya çıkar:
- İki veya daha fazla işaretçi aynı veriye aynı anda erişir.
- İşaretçilerden en az biri veriye yazmak için kullanılır.
- Veriye erişimi senkronize edecek herhangi bir mekanizma kullanılmaz.
Veri yarışları tanımsız davranışa (undefined behavior) yol açar ve çalışma zamanında izlerini sürüp teşhis etmek ve düzeltmek son derece güç olabilir. Rust ise veri yarışı barındıran kodları derlemeyi reddederek bu sorunu henüz ortaya çıkmadan engeller!
Her zaman olduğu gibi, süslü parantezler kullanarak yeni bir kapsam oluşturabilir ve böylece eşzamanlı olmamak kaydıyla birden fazla değiştirilebilir referans tanımlayabiliriz:
fn main() {
let mut metin = String::from("merhaba");
{
let r1 = &mut metin;
} // r1 burada kapsam dışına çıkar, bu yüzden hiçbir sorun olmadan yeni bir referans yapabiliriz.
let r2 = &mut metin;
}
Rust, değiştirilebilir ve değiştirilemez referansların bir arada kullanılması için de benzer bir kural uygular. Aşağıdaki kod derleme hatasıyla sonuçlanır:
fn main() {
let mut metin = String::from("merhaba");
let r1 = &metin; // sorun yok
let r2 = &metin; // sorun yok
let r3 = &mut metin; // BÜYÜK SORUN
println!("{r1}, {r2}, ve {r3}");
}
İşte hata:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `metin` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &metin; // sorun yok
| ------ immutable borrow occurs here
5 | let r2 = &metin; // sorun yok
6 | let r3 = &mut metin; // BÜYÜK SORUN
| ^^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, ve {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Gördüğünüz gibi, aynı değere ait değiştirilemez bir referansımız varken aynı zamanda değiştirilebilir bir referansımız da olamaz!
Değiştirilemez bir referansı kullanan kod bölümleri, ellerindeki değerin bir anda altlarından kayıp değişmesini beklemez. Buna karşılık, birden fazla değiştirilemez referansa izin verilir; çünkü veriyi yalnızca okuyan hiçbir referansın, diğerlerinin okuduğu veriyi etkileme imkânı yoktur.
Bir referansın kapsamının tanımlandığı noktada başladığına ve o referansın son kez kullanıldığı noktaya kadar devam ettiğine dikkat edin. Örneğin aşağıdaki kod sorunsuzca derlenir; çünkü değiştirilemez referansların son kullanımı, değiştirilebilir referans tanımlanmadan hemen önceki println! satırındadır:
fn main() {
let mut metin = String::from("merhaba");
let r1 = &metin; // sorun yok
let r2 = &metin; // sorun yok
println!("{r1} ve {r2}");
// r1 ve r2 değişkenleri bu noktadan sonra kullanılmayacak.
let r3 = &mut metin; // sorun yok
println!("{r3}");
}
Değiştirilemez r1 ve r2 referanslarının kapsamları, son kez kullanıldıkları println! çağrısından hemen sonra sona erer; bu da değiştirilebilir r3 referansı oluşturulmadan öncedir. Bu kapsamlar birbiriyle çakışmadığından koda izin verilir: Derleyici, süslü parantezle belirlenen blok sonuna henüz gelinmemiş olsa bile referansın o noktadan sonra artık kullanılmadığını anlayabilir.
Not: Rust derleyicisinin ödünç alma denetleyicisi (borrow checker), bir referansın geçerlilik süresini yalnızca süslü parantezlerin ({ ... }) oluşturduğu sözcüksel blok sınırlarına göre değil, referansın kod akışı içinde gerçekte son kullanıldığı yere göre hesaplar. Rust dünyasında Sözcüksel Olmayan Ömürler (Non-Lexical Lifetimes — NLL) olarak adlandırılan bu özellik sayesinde, işi biten referanslar için fazladan iç içe { ... } blokları açmanıza gerek kalmaz. Yukarıdaki örnekte let r3 = &mut metin; satırından sonra println!("{r1}"); gibi bir satır daha ekleseydik, r1’in kapsamı o satıra kadar uzayacak, r3 ile çakışacak ve derleyici E0502 hatası üretecekti.
Ödünç alma hataları zaman zaman zorlayıcı görünse de bunların aslında Rust derleyicisinin olası bir hatayı erkenden (çalışma zamanı yerine derleme zamanında) yakalayıp sorunun tam olarak nerede olduğunu size göstermesinden kaynaklandığını unutmayın. Bu sayede, programınız çalışırken verilerinizin neden beklediğiniz gibi görünmediğinin izini sürmek zorunda kalmazsınız.
Sarkan Referanslar
İşaretçilerin doğrudan kullanıldığı dillerde, bellekteki bir alanı serbest bırakırken o alana yönelen işaretçiyi korumaya devam ederek yanlışlıkla bir sarkan işaretçi (dangling pointer) — yani artık bir başkasına tahsis edilmiş olabilecek geçersiz bir bellek konumunu gösteren işaretçi — oluşturmak oldukça kolaydır. Rust’ta ise derleyici, referansların hiçbir zaman sarkan referans (dangling reference) hâline gelmeyeceğini garanti eder: Bir veriye referansınız varsa derleyici, referansın kendisi kapsam dışına çıkmadan önce verinin kapsam dışına çıkmamasını sağlar.
Rust’ın bunları derleme zamanı hatasıyla nasıl engellediğini görmek için sarkan bir referans oluşturmayı deneyelim:
fn main() {
let hicbir_seye_referans = sarkan_isaretci();
}
fn sarkan_isaretci() -> &String {
let metin = String::from("merhaba");
&metin
}
İşte hata:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:25
|
5 | fn sarkan_isaretci() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn sarkan_isaretci() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn sarkan_isaretci() -> &String {
5 + fn sarkan_isaretci() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Bu hata mesajı henüz ele almadığımız bir özelliğe değinmektedir: ömürler (lifetimes). Ömürleri Bölüm 10’da ayrıntılı olarak inceleyeceğiz. Ancak ömürlerle ilgili kısımları şimdilik bir kenara bırakırsanız, mesaj bu kodun neden sorunlu olduğuna dair temel ipucunu açıkça verir:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
Başka bir deyişle derleyici, bu fonksiyonun dönüş türünün ödünç alınmış bir değer içerdiğini ancak kendisinden ödünç alınabilecek hiçbir değer bulunmadığını bildirir (sarkan_isaretci fonksiyonu hiçbir parametre almadığından, döndürülen referansın dışarıdaki geçerli bir veriden ödünç alınmış olması imkânsızdır).
sarkan_isaretci kodumuzun her aşamasında tam olarak neler olup bittiğine daha yakından bakalım:
fn main() {
let hicbir_seye_referans = sarkan_isaretci();
}
fn sarkan_isaretci() -> &String {
// sarkan_isaretci, bir String'e referans döndürür
let metin = String::from("merhaba"); // metin yeni bir String'dir
&metin // String'e, yani metin'e bir referans döndürüyoruz
} // Burada metin kapsam dışına çıkar ve düşürülür (drop edilir), bu yüzden belleği gider.
// Tehlike!
metin değişkeni sarkan_isaretci fonksiyonunun içinde oluşturulduğu için, sarkan_isaretci kodu tamamlandığında metin değişkeninin belleği serbest bırakılacaktır. Ancak biz ona ait bir referansı döndürmeye çalıştık. Bu durum, döndürülen referansın geçersiz bir String’e işaret edeceği anlamına gelir. Bu hiç iyi bir durum değildir ve Rust bunu yapmamıza izin vermez.
Buradaki çözüm, referans yerine doğrudan String değerini döndürmektir:
fn main() {
let metin = sarkmayan_isaretci();
}
fn sarkmayan_isaretci() -> String {
let metin = String::from("merhaba");
metin
}
Bu kod çalışır: String değerinin sahipliği çağıran fonksiyona taşındığı için ona ait öbek belleği bu fonksiyondan dönülürken serbest bırakılmaz. Bellek, değerin yeni sahibi onu düşürdüğünde serbest bırakılır.
Referans Kuralları
Referanslar hakkında ele aldıklarımızı özetleyelim:
- Herhangi bir anda, bir veriye ait ya yalnızca bir tane değiştirilebilir referansa ya da istediğiniz sayıda değiştirilemez referansa sahip olabilirsiniz.
- Referanslar her zaman geçerli olmalıdır.
Not: İlk kuralı akılda tutmanın en sezgisel yolu onu paylaşımlı okuma veya
tekelci yazma ilkesi olarak düşünmektir: Bir veriyi aynı anda dilediğiniz
kadar referans üzerinden güvenle okuyabilirsiniz (&T); ancak veriyi
değiştirmek istediğinizde (&mut T), değişiklik süresince o veriye erişebilen
tek etkin referans sizinki olmalıdır. Örneğimizde yerel String değişkenini
&mut ile ödünç alabilmek için bağlamayı let mut ile tanımladık. Elinizde
zaten bir &mut T varsa, referansı tutan bağlamanın kendisi mut olmasa da
işaret ettiği değeri değiştirebilirsiniz; bağlamayı yeniden atamak ile
referansın işaret ettiği veriyi değiştirmek farklı işlemlerdir.
Sırada, farklı bir referans türüne bakacağız: dilimler.