Rust'ta borrowing (ödünç alma), bir değerin sahipliğini almadan ona referans üzerinden erişmektir: &x paylaşılan (salt okunur) bir referans, &mut x ise değiştirilebilir bir referans oluşturur. Temel kural şudur: aynı anda ya tek bir &mut referans ya da istediğiniz kadar & referans olabilir ve her referans her zaman geçerli bir değeri göstermelidir.

Bu yazıda referansların sözdizimini, borrow checker'ın bu iki kuralı nasıl uyguladığını, bir referansın ne zaman "bittiğini", sarkan (dangling) referansların neden derlenmediğini ve gerçek kodda en sık görülen ödünç alma hatalarını çalışan örneklerle inceliyoruz. Sahiplik ve move konusunu henüz okumadıysanız önce Rust ownership kuralları yazısına bakmanızı öneririz.

Referans nedir, neden gerekir?

Referans nedir, neden gerekir?

Sahiplik yazısında bir String'i fonksiyona verip geri almak için demet döndürmek zorunda kalmıştık. Referans bu zahmeti ortadan kaldırır. Referans, bir işaretçi gibi başka bir değişkenin sahip olduğu veriye giden adrestir; işaretçiden farkı, ömrü boyunca belirli bir tipin geçerli bir değerini göstereceğinin garanti edilmesidir. Referans değerin sahibi olmadığı için, referans kullanılmaz hâle geldiğinde gösterdiği değer düşürülmez.

rust
fn uzunluk(s: &String) -> usize {
    s.len()
} // s kapsamdan çıkar ama String düşürülmez: sahibi değil

fn ekle(s: &mut String) {
    s.push_str(", dünya");
}

fn main() {
    let mut s = String::from("merhaba");
    let n = uzunluk(&s);
    ekle(&mut s);
    println!("{n} {s}");
}
// çıktı: 7 merhaba, dünya

Çağrı tarafında &s yazmak ile imzada &String yazmak birbirini tamamlar. Referans oluşturma eylemine borrowing denir: gerçek hayattaki gibi, işiniz bitince geri verirsiniz ve sahibi siz olmazsınız. Referansın tersi olan dereference işlemi * operatörüyle yapılır; örneğin &mut i32 üzerinden değeri artırmak için *n += 1 yazılır. Metot çağrılarında ise s.len() gibi yazdığınızda Rust gereken referans veya dereference işlemini kendisi ekler.

Paylaşılan referanslar değiştirme izni vermez

Değişkenler varsayılan olarak değiştirilemez olduğu gibi referanslar da öyledir. &String alan bir fonksiyonda push_str çağırırsanız derleyici durur:

rust
fn degistir(s: &String) {
    s.push_str(", dünya");
}

fn main() {
    let s = String::from("merhaba");
    degistir(&s);
}
// error[E0596]: cannot borrow `*s` as mutable, as it is behind a `&` reference

Düzeltme üç adımdır: değişkeni let mut ile tanımlamak, çağrıda &mut s yazmak ve imzayı s: &mut String yapmak. Bu hâliyle imza, fonksiyonun ödünç aldığı değeri değiştireceğini açıkça ilan eder. Kodu okuyan biri, değişikliğin yalnızca &mut görülen yerlerde olabileceğini bilir.

&mut ile değer döndürmeden sonuç üretmek

Değiştirilebilir referansın tipik kullanımı, fonksiyonun çağıranın değişkenini yerinde güncellemesidir. Bunun için fonksiyon gövdesinde referansın gösterdiği değere * ile ulaşırsınız. Sayı gibi Copy tiplerde bile referans kullanmak, fonksiyonun yeni bir değer döndürmek yerine var olanı değiştirmesini sağlar:

rust
fn artir(n: &mut i32) {
    *n += 1; // referansın gösterdiği değeri değiştir
}

fn main() {
    let mut sayac = 0;
    artir(&mut sayac);
    artir(&mut sayac);
    println!("sayaç: {sayac}");
}
// çıktı: sayaç: 2

Burada iki &mut sayac ifadesi aynı satırda değil, art arda iki ayrı çağrıda oluşturulur. Her referans kendi çağrısı bitince kullanılmaz hâle gelir, bu yüzden aynı anda iki değiştirilebilir referans hiçbir zaman var olmaz. Rust Reference'taki tip zorlamaları (coercion) listesine göre &mut T gerektiğinde &T değerine dönüşebilir; yani değiştirilebilir bir referansı yalnızca okuyan bir fonksiyona da verebilirsiniz.

Kural 1: aynı anda tek bir &mut

Değiştirilebilir referansın büyük bir kısıtı vardır: bir değere &mut referans varken aynı değere başka hiçbir referans olamaz. İki değiştirilebilir referansı aynı anda kullanmaya çalışan kod derlenmez:

rust
fn main() {
    let mut s = String::from("merhaba");
    let r1 = &mut s;
    let r2 = &mut s;
    println!("{r1}, {r2}");
}
// error[E0499]: cannot borrow `s` as mutable more than once at a time

Bu kısıt, Rust'ın veri yarışlarını (data race) derleme zamanında engellemesini sağlar. The Book veri yarışını üç koşulun birlikte oluşması olarak tanımlar: iki veya daha fazla işaretçi aynı veriye aynı anda erişir, en az biri yazar ve erişimi eşitleyen bir mekanizma yoktur. Veri yarışları tanımsız davranışa yol açar ve çalışma anında bulunması zordur. Ayrı süslü parantezlerle yeni bir kapsam açarak birden çok &mut oluşturabilirsiniz; yasak olan, bunların aynı anda canlı olmasıdır.

rust
fn main() {
    let mut s = String::from("merhaba");
    {
        let r1 = &mut s;
        r1.push_str(" dünya");
    } // r1 burada kapsamdan çıkar
    let r2 = &mut s; // yeni bir &mut sorunsuz
    r2.push('!');
    println!("{s}");
}
// çıktı: merhaba dünya!

Son satırda s'i doğrudan yazdırabilmemizin nedeni de aynıdır: r2'nin son kullanımı bir önceki satırdır. Pek çok dilde aynı nesneye istediğiniz kadar değiştirilebilir takma ad (alias) oluşturup her yerden yazabilirsiniz; Rust ise yazma iznini tek bir yerde toplar. Bu ilk bakışta kısıtlayıcı görünür ama hangi kod parçasının veriyi o an değiştirebileceğini her zaman tek bir yere indirger.

Kural 2: &mut ile & birlikte olamaz

Kural 2: &mut ile & birlikte olamaz: Birden çok & aynı anda, Tek &mut, İki &mut aynı anda, & canlıyken &mut

Aynı mantık paylaşılan ve değiştirilebilir referansların karışımı için de geçerlidir. Bir değeri okuyan biri, değerin elinin altında aniden değişmesini beklemez. Buna karşılık birden çok okuyucu sorun değildir, çünkü yalnızca okuyan kimse diğerinin okumasını etkileyemez.

rust
fn main() {
    let mut s = String::from("merhaba");
    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;
    println!("{r1}, {r2} ve {r3}");
}
// error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
DurumİzinHata kodu
Birden çok & aynı andaEvetYok
Tek &mutEvetYok
İki &mut aynı andaHayırE0499
& canlıyken &mutHayırE0502
& referansla değiştirmeHayırE0596
Ödünç verilmişken değeri taşımaHayırE0505

Bu hataların ayrıntılı çözüm kalıpları için E0499 ve E0502 borrow checker hataları yazısına bakabilirsiniz.

Bir referans ne zaman biter?

Yeni başlayanların en çok şaşırdığı nokta şudur: referansın geçerlilik alanı, tanımlandığı bloğun sonuna kadar değil, son kullanıldığı yere kadar sürer. Derleyici bir referansın artık kullanılmayacağını kapsamın bitiminden önce görebilir. Bu yüzden aşağıdaki kod, bir önceki hatalı örneğe çok benzese de derlenir:

rust
fn main() {
    let mut s = String::from("merhaba");

    let r1 = &s;
    let r2 = &s;
    println!("{r1} ve {r2}");
    // r1 ve r2 bundan sonra kullanılmıyor

    let r3 = &mut s; // sorun yok
    r3.push('!');
    println!("{r3}");
}
// çıktı: merhaba ve merhaba
// çıktı: merhaba!

Okuma referanslarının son kullanımı ilk println! satırındadır ve değiştirilebilir referans ondan sonra oluşturulur. Alanlar çakışmadığı için kural ihlali yoktur. Hatalı sürümde sorun r3'ün varlığı değil, r1 ve r2'nin r3'ten sonra tekrar kullanılmasıydı. Hata mesajındaki "immutable borrow later used here" notu tam olarak bu sonraki kullanımı işaret eder.

Ödünç verilmiş bir değer taşınamaz

Referans canlıyken değerin sahibini değiştirmek de yasaktır; aksi hâlde referans artık başka birine ait, belki de düşürülmüş bir veriyi gösterirdi. Rust by Example bunu şöyle özetler: bir nesneye referanslar var olduğu sürece nesne yok edilemez.

rust
fn main() {
    let v = vec![1, 2, 3];
    let ilk = &v[0];
    let w = v;
    println!("{ilk} {}", w.len());
}
// error[E0505]: cannot move out of `v` because it is borrowed

Çözüm, taşımayı referansın son kullanımından sonraya almak ya da referans yerine değerin kopyasını tutmaktır: let ilk = v[0]; yazarsanız i32 Copy olduğu için referans değil kopya elde edersiniz.

Sarkan referanslar derlenmez

İşaretçili dillerde belleği serbest bırakıp işaretçiyi saklamak kolaydır; böylece başkasına verilmiş olabilecek bir adresi gösteren sarkan (dangling) işaretçi oluşur. Rust'ta derleyici, verinin kendisine ait referanslardan önce kapsamdan çıkmamasını garanti eder. Fonksiyon içinde oluşturulan bir değere referans döndürmeye çalışırsanız hata alırsınız:

rust
fn sarkan() -> &String {
    let s = String::from("merhaba");
    &s
}

fn main() {
    let _r = sarkan();
}
// error[E0106]: missing lifetime specifier

Mesajın yardım notu sorunun özünü söyler: dönüş tipi ödünç alınmış bir değer içeriyor ama ödünç alınabilecek bir girdi yok. s fonksiyon bitince düşürüleceği için referans geçersiz kalırdı. Doğru çözüm genellikle String'in kendisini döndürüp sahipliği çağırana taşımaktır. Lifetime notasyonunun ayrıntıları Rust lifetime rehberi yazısında.

Referans döndürmek her zaman yasak değildir. Dönen referans parametrelerden birinden türetiliyorsa sorun yoktur: fn ilk(v: &Vec<i32>) -> &i32 gibi tek referans parametreli bir imzada derleyici, dönen referansın v'nin gösterdiği veriye bağlı olduğunu kendisi çıkarır. Yasak olan, fonksiyonun kendi yerel değişkenine referans döndürmesidir; çünkü o değişken fonksiyonla birlikte yok olur ve geriye bağlanacak hiçbir veri kalmaz.

ref anahtar kelimesi

Desenle parçalama yaparken bir alanı taşımak yerine ona referans almak için ref ve ref mut kullanılır. Rust by Example'a göre atamanın sol tarafındaki ref, sağ taraftaki & ile eşdeğerdir. Bir demetin tek bir elemanını yerinde değiştirmek için de kullanışlıdır:

rust
fn main() {
    let c = 'Q';
    let ref r1 = c;
    let r2 = &c;
    println!("{}", *r1 == *r2);

    let mut demet = (Box::new(5u32), 3u32);
    {
        let (_, ref mut son) = demet;
        *son = 2;
    }
    println!("{demet:?}");
}
// çıktı: true
// çıktı: (5, 2)

Günlük kodda match ve if let bir referans üzerinde çalıştığında bağlamalar zaten otomatik olarak referans olur; bu kurala binding mode denir ve pattern sözdizimi yazısında anlatılıyor.

Gerçek kodda sık görülen durumlar

Gerçek kodda sık görülen durumlar: Vec'e referans tutarken eleman eklemek, Aynı dizinin iki elemanını birlikte değiştirmek, Struct'ın farklı alanları

Vec'e referans tutarken eleman eklemek

Bir vektörün elemanına referans tutarken push çağırmak E0502 verir. Bunun nedeni yalnızca kural değildir: push kapasite yetmezse vektörü bellekte başka bir yere taşıyabilir ve eski referans boşalmış belleği gösterirdi.

rust
fn main() {
    let mut v = vec![1, 2, 3];
    let ilk = &v[0];
    v.push(4);
    println!("ilk: {ilk}");
}
// error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

Aynı dizinin iki elemanını birlikte değiştirmek

&mut v[0] ve &mut v[1] farklı elemanlar olsa da derleyici indekslemede ikisini de dizinin tamamının ödünç alınması olarak görür. Standart kütüphanedeki split_at_mut diziyi çakışmayan iki değiştirilebilir dilime böler ve bu durumu güvenle çözer:

rust
fn main() {
    let mut dizi = [1, 2, 3, 4];
    let (sol, sag) = dizi.split_at_mut(2);
    sol[0] += 10;
    sag[0] += 20;
    println!("{dizi:?}");
}
// çıktı: [11, 2, 23, 4]

Struct'ın farklı alanları

İndekslemenin aksine, bir struct'ın farklı alanlarına aynı anda ayrı &mut referanslar alabilirsiniz; derleyici alanların çakışmadığını görür. Aşağıdaki program Rust 1.99.1 ile sorunsuz derlenir:

rust
struct Nokta {
    x: i32,
    y: i32,
}

fn main() {
    let mut p = Nokta { x: 1, y: 2 };
    let a = &mut p.x;
    let b = &mut p.y;
    *a += 10;
    *b += 20;
    println!("{} {}", p.x, p.y);
}
// çıktı: 11 22

Sık yapılan hatalar

  • Gerekmediği hâlde &mut istemek: Yalnızca okuyan fonksiyonlarda & kullanın; böylece çağıran aynı anda başka okuyucular da tutabilir.
  • &String almak: Okuma amaçlı metin parametrelerinde &str daha esnektir; hem String referansı hem literal kabul eder. Ayrıntı: Rust slice ve &str.
  • Referansı uzun tutmak: Referansı bir değişkende saklayıp çok sonra kullanmak, aradaki tüm değişiklikleri engeller. Gerekli değeri erken kopyalamak ya da referansı kısa bir blokta kullanmak çatışmayı çözer.
  • Sorunu clone ile örtmek: Çatışmanın nedeni çoğu zaman sıralamadır. Önce satırların sırasını değiştirmeyi deneyin; kopya ancak gerçekten iki bağımsız değer gerektiğinde doğru çözümdür.

Sık Sorulan Sorular

Referans ile işaretçi arasındaki fark nedir?

Rust referansları her zaman geçerli bir değeri gösterir; sarkan olamaz ve ödünç alma kuralları derleyici tarafından denetlenir. Bu garantilerin olmadığı ham işaretçiler unsafe kod gerektirir, bkz. Rust unsafe ve ham işaretçiler.

Neden aynı anda iki &amp;mut olamıyor?

Çünkü iki yazıcı aynı veriye eşitleme olmadan eriştiğinde veri yarışı oluşur. Rust bu kombinasyonu derleme zamanında reddederek tanımsız davranışın bütün bir sınıfını ortadan kaldırır.

println! bir değişkeni ödünç alır mı?

Evet, Rust by Example'ın Aliasing örneğindeki açıklamaya göre println! argümanlarını paylaşılan referansla alır. Bu yüzden canlı bir &mut varken asıl değişkeni yazdırmak hata verir; ama &mut referansın kendisi üzerinden alanları yazdırmak serbesttir.

Referanslar çalışma anında ek maliyet getirir mi?

Ödünç alma kurallarının denetimi tamamen derleme sırasında yapılır. Çalışma anında bir referans yalnızca bir adrestir; slice gibi bazı referanslar buna ek olarak bir uzunluk taşır. Kuralları çalışma anında denetleyen bir yapıya ihtiyaç duyarsanız standart kütüphanedeki RefCell kullanılır.

Borrow checker hatalarını nasıl okumalıyım?

Mesajdaki üç etikete bakın: ilk ödünç almanın oluştuğu yer, çakışan ikinci erişim ve ilk ödüncün daha sonra kullanıldığı yer. Çoğu zaman çözüm, son kullanımı çakışan erişimden önceye almaktır. Ek açıklama için rustc --explain E0502 komutunu çalıştırabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Ownership kuralları · Slice ve &str · Lifetime · E0499 ve E0502 hataları · Rc ve RefCell · Rust nedir?