Rust'ta ownership (sahiplik), belleği çöp toplayıcı olmadan ve elle free çağırmadan yöneten derleme zamanı kurallarıdır: her değerin tek bir sahibi vardır, sahip kapsamdan çıkınca değer düşürülür (drop) ve bir değeri başka bir değişkene ya da fonksiyona verdiğinizde sahiplik taşınır (move). Kurallar derleyici tarafından denetlenir; ihlal eden program derlenmez ve çalışma anında ek bir maliyet oluşmaz.

Bu yazı, Rust nedir? yazısındaki kısa özetin ötesine geçer: move ile kopyanın farkını, drop sırasını, kısmi taşımayı, Copy ile Drop çatışmasını ve std::mem::take gibi araçları çalışan örneklerle anlatır. Referanslarla ödünç alma konusu ayrı bir yazıda: Rust referans ve borrowing.

Stack, heap ve sahipliğin asıl işi

Sahiplik kurallarının neden böyle olduğunu anlamak için verinin nerede durduğunu bilmek gerekir. Stack, son giren ilk çıkar mantığıyla çalışır; buraya konan her verinin boyutu derleme zamanında bilinmeli ve sabit olmalıdır. Boyutu çalışma anında belli olan ya da büyüyüp küçülebilen veri heap üzerinde tutulur: ayırıcı (allocator) yeterince büyük boş bir alan bulur, işaretler ve size bir işaretçi döndürür. İşaretçinin kendisi sabit boyutlu olduğu için stack'te durabilir.

Stack'e veri koymak heap'te yer ayırmaktan hızlıdır, çünkü ayırıcının yer araması gerekmez. Heap'teki veriye erişmek de genellikle daha yavaştır, çünkü işaretçiyi takip etmek gerekir. The Rust Book, sahipliğin temel amacının heap verisini yönetmek olduğunu vurgular: hangi kodun hangi heap verisini kullandığını izlemek, gereksiz kopyaları azaltmak ve artık kullanılmayan veriyi temizlemek. i32 gibi küçük, sabit boyutlu tiplerde neden farklı davranış gördüğünüz de buradan gelir.

Üç sahiplik kuralı

Üç sahiplik kuralı: Rust'taki her değerin bir sahibi (owner) vardır., Aynı anda yalnızca bir sahip olabilir., Sahip kapsamdan çıktığında değer düşürülür (drop edilir).
  • Rust'taki her değerin bir sahibi (owner) vardır.
  • Aynı anda yalnızca bir sahip olabilir.
  • Sahip kapsamdan çıktığında değer düşürülür (drop edilir).

Kapsam, bir öğenin geçerli olduğu program aralığıdır. Değişken tanımlandığı satırdan bulunduğu bloğun kapanış süslü parantezine kadar geçerlidir. String gibi heap kullanan bir tipte, kapanış parantezinde Rust otomatik olarak drop fonksiyonunu çağırır ve bellek ayırıcıya geri verilir. C++ dünyasında bu desene RAII denir. Kendi tipinize Drop trait'ini uygularak bu anı gözle görebilirsiniz:

rust
struct Iz(&'static str);

impl Drop for Iz {
    fn drop(&mut self) {
        println!("drop: {}", self.0);
    }
}

fn main() {
    let _a = Iz("a");
    let mut b = Iz("b1");
    println!("b önce {}", b.0);
    b = Iz("b2"); // eski değer (b1) atama anında düşürülür
    println!("b şimdi {}", b.0);
    {
        let _c = Iz("c");
        println!("iç kapsam bitiyor");
    }
    println!("main bitiyor");
}
// çıktı: b önce b1
// çıktı: drop: b1
// çıktı: b şimdi b2
// çıktı: iç kapsam bitiyor
// çıktı: drop: c
// çıktı: main bitiyor
// çıktı: drop: b2
// çıktı: drop: a

Çıktıda üç ayrıntı var. Birincisi, var olan bir değişkene yeni değer atadığınızda eski değer hemen düşürülür; Rust Reference bunu atamanın sol taraftaki başlatılmış değerin yıkıcısını çalıştırması olarak tanımlar. İkincisi, iç blok bitince _c o anda temizlenir. Üçüncüsü, main sonunda değişkenler tanımlanma sırasının tersiyle düşürülür: önce b, sonra _a. Bir struct'ın alanları ise tanımlandıkları sırayla düşürülür.

String literal ile String arasındaki fark

Sahipliği göstermek için The Book String tipini seçer, çünkü string literal'lerden farklı davranır. "merhaba" gibi bir literal'in içeriği derleme zamanında bilinir ve doğrudan çalıştırılabilir dosyanın içine gömülür; bu yüzden hızlıdır ama değiştirilemez. Kullanıcıdan okunan bir metin gibi boyutu önceden bilinmeyen veri için ise String kullanılır. String::from çağrıldığında gereken bellek çalışma anında heap'ten istenir ve metin büyüyebilir, örneğin push_str ile sonuna ekleme yapılabilir.

Belleği istemek her dilde benzerdir; asıl fark geri verme kısmındadır. Çöp toplayıcılı dillerde bunu çalışma zamanı üstlenir. Çöp toplayıcı olmayan dillerde programcı her ayırma için tam bir serbest bırakma yazmak zorundadır: unutursa bellek boşa gider, erken yaparsa geçersiz bir değişken kalır, iki kez yaparsa yine hata olur. Rust üçüncü yolu seçer: bellek, sahibi olan değişken kapsamdan çıktığı anda otomatik olarak geri verilir.

Move: atama kopyalamak değildir

Bir String stack'te üç parçadan oluşur: heap'teki içeriği gösteren işaretçi, uzunluk ve kapasite. let s2 = s1; yazdığınızda bu üç değer kopyalanır, heap'teki metin kopyalanmaz. Eğer Rust iki değişkeni de geçerli sayısaydı, kapsam sonunda ikisi de aynı belleği serbest bırakmaya çalışırdı. Buna double free denir ve bellek bozulmasına yol açabilir. Rust bunu önlemek için atamadan sonra s1'i geçersiz sayar. Bu işleme move (taşıma) denir; sığ kopyaya benzer ama ilk değişken artık kullanılamaz.

rust
fn main() {
    let s1 = String::from("merhaba");
    let s2 = s1;
    println!("{s1}, dünya");
}
// error[E0382]: borrow of moved value: `s1`

Derleyici hatanın yanında üç ipucu verir: s1'in tipi Copy trait'ini uygulamadığı için move olduğunu, değerin hangi satırda taşındığını ve taşındıktan sonra nerede kullanılmaya çalışıldığını gösterir. Ayrıca maliyeti kabul edilebilirse clone önerir. Bu hatanın tüm çeşitleri ve çözüm yolları için E0382 borrow of moved value rehberi yazısına bakabilirsiniz.

Bu tasarımın önemli bir sonucu var: Rust verinizin derin kopyasını hiçbir zaman kendiliğinden oluşturmaz. Kodda otomatik gerçekleşen her kopyanın ucuz olduğunu varsayabilirsiniz. Pahalı olabilecek bir kopya her zaman clone() çağrısıyla görünür hâle gelir.

Clone ve Copy farkı

Clone ve Copy farkı: u32, i64 gibi tüm tamsayılar, f64 gibi kayan noktalı sayılar, bool ve char, (i32, i32)

Heap içeriğiyle birlikte gerçek bir kopya istiyorsanız clone çağırırsınız. Tamsayılar ise farklı davranır: let y = x; sonrasında x geçerliliğini korur, çünkü i32 tamamen stack'te duran, boyutu bilinen bir tiptir ve Copy trait'ini uygular. Böyle tiplerde sığ ve derin kopya arasında fark yoktur.

rust
fn main() {
    let s1 = String::from("merhaba");
    let s2 = s1.clone(); // heap verisi de kopyalanır
    println!("s1 = {s1}, s2 = {s2}");

    let x = 5;
    let y = x; // i32 Copy olduğu için x geçerli kalır
    println!("x = {x}, y = {y}");

    let t = (1, 'a');
    let u = t; // içindeki her tip Copy olduğundan demet de Copy
    println!("{t:?} {u:?}");
}
// çıktı: s1 = merhaba, s2 = merhaba
// çıktı: x = 5, y = 5
// çıktı: (1, 'a') (1, 'a')
TipAtamada ne olurNot
u32, i64 gibi tüm tamsayılarCopyDeğer kopyalanır, kaynak geçerli kalır
f64 gibi kayan noktalı sayılarCopyAynı şekilde
bool ve charCopySabit boyutlu skaler tipler
(i32, i32)CopyTüm elemanlar Copy ise demet de Copy
(i32, String)MoveBir eleman Copy değilse demet de değil
String, Vec<T>, Box<T>MoveHeap kaynağı tutar, kopya için clone gerekir

Bir tip veya parçalarından biri Drop uyguluyorsa ona Copy eklenemez. Kapsam sonunda özel bir iş yapması gereken bir değerin sessizce çoğaltılması anlamsız olurdu; derleyici bunu E0184 koduyla reddeder:

rust
#[derive(Clone, Copy)]
struct Kaynak;

impl Drop for Kaynak {
    fn drop(&mut self) {}
}

fn main() {}
// error[E0184]: the trait `Copy` cannot be implemented for this type; the type has a destructor

Kendi struct'larınıza Copy ve Clone eklemenin ayrıntıları için Rust derive rehberi yazısına göz atın.

Fonksiyonlara değer verme ve geri alma

Fonksiyona argüman geçirmek, atamayla aynı kurallara uyar: Copy olmayan bir değer fonksiyona taşınır ve çağıran taraf artık onu kullanamaz. Fonksiyonun dönüş değeri de sahipliği çağırana taşır. Sahipliği geri almak için değeri demet içinde döndürmek mümkündür ama hantaldır; bu yüzden pratikte referans kullanılır.

rust
fn uzunluk_al(s: String) -> (String, usize) {
    let n = s.len();
    (s, n) // sahiplik çağırana geri döner
}

fn tuket(s: String) {
    println!("tüketildi: {s}");
} // s burada düşürülür

fn main() {
    let ad = String::from("Ayşe");
    let (ad, n) = uzunluk_al(ad);
    println!("{ad} {n} bayt");
    tuket(ad);
    // buradan sonra ad kullanılamaz
}
// çıktı: Ayşe 5 bayt
// çıktı: tüketildi: Ayşe

Dikkat: len() karakter değil bayt sayısını döndürür. "Ayşe" dört karakterdir ama ş harfi UTF-8'de iki bayt tuttuğu için sonuç 5'tir. Metin ayrıntıları için String ve &str farkı yazısına bakın.

Taşındıktan sonra değişebilirlik

Değişebilirlik (mutability) değerin değil bağlamanın (binding) özelliğidir. Bu yüzden sahiplik el değiştirirken değişebilirlik de değişebilir: mut olmadan tanımlanmış bir Box'ı let mut ile yeni bir değişkene taşırsanız içeriğini değiştirebilirsiniz.

rust
fn main() {
    let sabit = Box::new(5u32);
    // *sabit = 4; // derlenmez: sabit mut değil
    let mut degisken = sabit; // move, artık sahibi degisken
    *degisken = 4;
    println!("{degisken}");
}
// çıktı: 4

Kısmi move (partial move)

Bir struct'ı desenle parçalarken bazı alanları taşıyıp bazılarını referansla bağlayabilirsiniz. Buna kısmi move denir. Taşınan alanlar artık eski değişkenden kullanılamaz ve struct bir bütün olarak da kullanılamaz; ama taşınmayan alanlara erişmeye devam edebilirsiniz. Desendeki ref anahtar kelimesi alanı taşımak yerine ona referans alır.

rust
#[derive(Debug)]
struct Kisi {
    ad: String,
    yas: Box<u8>,
}

fn main() {
    let kisi = Kisi { ad: String::from("Ali"), yas: Box::new(30) };
    let Kisi { ad, ref yas } = kisi; // ad taşınır, yas ödünç alınır
    println!("ad: {ad}, yaş: {yas}");
    println!("kisi.yas hâlâ kullanılabilir: {}", kisi.yas);
}
// çıktı: ad: Ali, yaş: 30
// çıktı: kisi.yas hâlâ kullanılabilir: 30

Aynı programın sonuna println!("{kisi:?}"); eklerseniz derleyici bütünü kullanmanıza izin vermez:

rust
#[derive(Debug)]
struct Kisi {
    ad: String,
    yas: Box<u8>,
}

fn main() {
    let kisi = Kisi { ad: String::from("Ali"), yas: Box::new(30) };
    let Kisi { ad, ref yas } = kisi;
    println!("{ad} {yas}");
    println!("{kisi:?}");
}
// error[E0382]: borrow of partially moved value: `kisi`

Rust by Example bir kısıtı daha hatırlatır: Drop uygulayan tiplerden kısmi move yapılamaz, çünkü drop metodu değeri daha sonra bütün olarak kullanacaktır.

Ödünç alınmış değerden taşımak: mem::take ve mem::replace

Sık karşılaşılan bir durum: elinizde &mut referans var ve içindeki bir alanı dışarı almak istiyorsunuz. Referans sahiplik vermediği için alanı doğrudan taşıyamazsınız; derleyici E0507 hatası verir.

rust
struct Kutu {
    icerik: String,
}

fn bosalt(k: &mut Kutu) -> String {
    k.icerik
}

fn main() {}
// error[E0507]: cannot move out of `k.icerik` which is behind a mutable reference

Çözüm, alanın yerine geçerli bir değer bırakarak eskisini almaktır. std::mem::take hedefi tipin Default değeriyle değiştirip eski değeri döndürür; std::mem::replace ise yerine sizin verdiğiniz değeri koyar. Belgelere göre replace iki değerden hiçbirini düşürmez. Değeri kapsam bitmeden erken temizlemek istediğinizde drop fonksiyonunu çağırırsınız; bu fonksiyon gerçekte boş gövdeli pub fn drop<T>(_x: T) {} tanımından ibarettir, değeri içine taşıdığı için dönmeden önce düşürür.

rust
use std::mem;

struct Kutu {
    icerik: String,
}

fn bosalt(k: &mut Kutu) -> String {
    mem::take(&mut k.icerik) // yerine String::new() bırakılır
}

fn main() {
    let mut k = Kutu { icerik: String::from("kitap") };
    let alinan = bosalt(&mut k);
    println!("alınan: {alinan}, kalan: {:?}", k.icerik);

    let mut durum = String::from("eski");
    let onceki = mem::replace(&mut durum, String::from("yeni"));
    println!("{onceki} yerine {durum}");

    let v = vec![1, 2, 3];
    drop(v); // v burada düşürülür, sonra kullanılamaz
    println!("bitti");
}
// çıktı: alınan: kitap, kalan: ""
// çıktı: eski yerine yeni
// çıktı: bitti

Fonksiyon imzasından sahipliği okumak

Fonksiyon imzasından sahipliği okumak: s: String, s: &String veya s: &str, s: &mut String

Sahiplik kurallarını içselleştirmenin pratik yolu, her fonksiyon imzasında parametrenin hangi biçimde alındığına bakmaktır. Deyimsel Rust'ta fonksiyonlar gerçekten ihtiyaç duymadıkça argümanlarının sahipliğini almaz. Aşağıdaki tablo üç temel biçimi karşılaştırır:

ParametreÇağıran açısındanNe zaman seçilir
s: StringDeğer taşınır, sonra kullanılamazFonksiyon değeri saklayacak ya da tüketecekse
s: &String veya s: &strDeğer ödünç verilir, sonra kullanılabilirFonksiyon yalnızca okuyacaksa
s: &mut StringDeğiştirilebilir ödünç, çağrı boyunca tek erişimFonksiyon değeri yerinde değiştirecekse

Dönüş tarafında da benzer bir okuma yapılır: String döndüren bir fonksiyon sahipliği size verir ve değeri temizlemek artık sizin kapsamınızın işidir. Referans döndüren fonksiyonlarda ise dönen referansın hangi girdiye bağlı olduğu lifetime kurallarıyla belirlenir; bu konu Rust lifetime rehberi yazısında anlatılıyor.

Sık yapılan hatalar

  • Döngü içinde taşımak: for döngüsünde String alan bir fonksiyona aynı değişkeni vermek ilk turda değeri taşır, ikinci turda derleyici "value moved here, in previous iteration of loop" notuyla E0382 verir. Referans geçirin ya da döngü içinde yeni değer üretin.
  • Her yerde clone kullanmak: Hata mesajı clone önerse de bu her zaman doğru çözüm değildir. Fonksiyon yalnızca okuyorsa parametreyi &String yerine &str referansı olarak almak hem kopyayı hem taşımayı ortadan kaldırır.
  • Tamsayı ile String davranışını karıştırmak: let y = x; sonrası x'in çalışmaya devam etmesi tipin Copy olmasından kaynaklanır; aynı kodu String ile yazınca taşınma olur.
  • Atamanın eski değeri düşürdüğünü unutmak: Drop uygulayan bir değişkene yeni değer atamak, eski değerin yıkıcısını o satırda çalıştırır. Dosya veya kilit gibi kaynaklarda bu zamanlama önemlidir.
  • _ ile _ad farkı: let _ = deger; hiçbir şeyi bağlamaz ve taşımaz, let _ad = deger; ise değeri taşır. Ayrıntı: Rust pattern sözdizimi.

Sık Sorulan Sorular

Ownership çalışma anında yavaşlığa yol açar mı?

Hayır. The Book'a göre sahiplik kurallarının hiçbiri programın çalışmasını yavaşlatmaz; tüm denetim derleme sırasında yapılır. drop çağrıları da derleyicinin kapsam sonuna yerleştirdiği sıradan fonksiyon çağrılarıdır.

Move olunca veri bellekte kopyalanır mı?

String için yalnızca stack'teki işaretçi, uzunluk ve kapasite kopyalanır; heap'teki içerik yerinde kalır ve yeni sahibe geçer. Heap verisinin kopyalanması yalnızca clone çağırdığınızda olur.

Değişkenlerin düşürülme sırası nedir?

Bir kapsamdan çıkılırken o kapsamdaki değişkenler tanımlanma sırasının tersiyle düşürülür. Struct alanları ve demet elemanları ise tanım sırasıyla düşürülür. Ayrıntılar Rust Reference'ın Destructors sayfasındadır.

Bir değeri kapsam bitmeden nasıl serbest bırakırım?

drop(deger) çağırın. Değer fonksiyona taşındığı için dönüşten önce temizlenir ve sonrasında kullanılamaz. Copy tiplerde bu çağrının etkisi yoktur, çünkü fonksiyona kopya gider.

Sahiplik kurallarını aşmanın yolu var mı?

Kuralları aşmak yerine onlara uygun araçlar kullanılır: okumak için referans, birden fazla sahip için Rc ve RefCell, iş parçacıkları arasında paylaşım için Arc ve Mutex.

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 · Referans ve borrowing · Slice ve &str · Lifetime · E0382 hatası · Box akıllı işaretçi · Rust nedir?