Merhaba arkadaşlar, bir önceki yazımızda Rust’ta bulunan ownership kavramından bahsetmiştik. O yazıya buradan ulaşabilirsiniz. Bu konuyu anlayabilmek için bir önceki ownership kavramını da anlamış olmak gerekiyor. Bu nedenle bu yazıya başlamadan önce, eğer ownership kavramını bilmiyorsanız, diğer yazımı okumanızı tavsiye ederim.

Ownership öğrendikten sonra yine aynı derecede önemli olan ikinci kavrama geçiş yapıyoruz, o da borrowing (ödünç almak) kavramı.

Diğer yazımızda Rust’ın diğer dillere göre çok farklı bir kaynak yönetim mekanizması olduğundan bahsetmiş ve bir kısmını açıklamıştık. O yüzden çok fazla uzatmadan borrowing kavramını açıklamaya başlıyorum.

Borrowing Kavramı

Heap’de depolanan bir değişkeni biz başka bir değişkene atadığımızda o değişkenin kaynağının(resource) sahipliği(ownership) de aktarılıyordu ve ilk oluşturduğumuz değişken artık o kaynağa erişim sağlayamıyordu. Düşündüğümüzde aslında bazı konularda çok fazla dezavantajlı bir özellikmiş gibi duruyor. Örneğin bir değişkeni bir fonksiyona parametre olarak gönderdikten sonra artık o değişkeni kullanamamak gerçekten can sıkıcı bir şey olabilir. Bazı durumlarda değişkenin sahipliğini devretmeden değişkenin verisine erişmek isteyebiliriz. İşte bunun gibi durumlarda, Rust’ın borrowing mekanizmasını devreye giriyor. Bir nesneyi değer olarak göndermek yerine(T), onu referans olarak(&T) gönderebiliyoruz. Referanslar, en basit haliyle, veri yerine diğer bir değişkenin bellek adresini tutan yapılardır.

Burdaki güzel nokta, compiler’ın her zaman referans nesnesinin geçerli bir nesneyi refere ettiğini statik olarak kontrol etmesi. Bunu borrow checker denilen bir mekanizma ile sağlıyor. Yani bir nesnenin referansı var olduğu sürece o nesnenin silinmemesi sağlanıyor. Hemen bir örnekle nasıl referans oluşturuyoruz görelim:

fn main() {
    // Box içinde bir integer oluşturuyoruz.g
    let boxed_int = Box::new(5);
    let ref_int = &boxed_int; // '&' ile referans oluşturuyoruz.
    
    println!("değişken: {}, referansı: {}", boxed_int, ref_int);
}

Gördüğünüz gibi atama yaparken ya da parametre olarak gönderirken değişkenin başına & yazarak ona referans etmesini sağlayabiliyoruz. Eğer & yazmasaydık ownership artık ref_int adlı değişkene geçecek ve boxed_int kullanılamaz olacaktı. Ama artık ref_int boxed_int‘i referans olarak gösterdiği için ikisini de kullanabiliyoruz.

Şimdi biraz daha gerçekçi bir örnek görelim. Örneğin siz bir fonksiyona parametre olarak değişkeni vereceksiniz, fakat fonksiyondan sonra o değişkeni kullanmak da istiyorsunuz. Borrowing olmasa ownership transfer olacak ve bunu gerçekleştiremeyecektik. Fakat referans göndererek şu şekilde gerçekleştirebiliriz:

fn foo(v1: &Vec<i32>) { // <--- Gördüğünüz gibi '&Vec' diyerek parametrenin bir referans aldığını bildiriyoruz.
    println!("referans vektör: {:?}", v1);
}

fn main() {
    let v1 = vec![1, 2, 3]; // Bir vektör oluşturduk, vektörler heap'de depolanır.
    foo(&v1); // foo adlı fonksiyona referans olarak v1'i gönderdik.
    println!("vektörün kendisi: {:?}", v1); // Artık burada kullanabiliyoruz.
}

Gördüğünüz gibi referanslar aslında Rust’ta hayat kurtaracak değişkenler. Referanslar olmasaydı Rust’da değişkenlerle işlemler yapmak gerçekten çok zor bir hal alırdı.
Peki şimdi de biraz önce bahsettiğimiz referanslar var olduğu sürece değişken kaynağının silinmemesine geldi. Eğer kaynak referanstan önce silinirse, referans ya null bir alanı gösterecek ya da başka bir kaynağı gösterecekti. Bu da büyük bir sorun olurdu yazılımcı için. Rust ise bunun mümkün olmamasını şu şekilde garanti altına alıyor:

fn foo(v1: Vec<i32>) { // <--- Bu sefer referans almayan bir fonksiyonumuz var.
    println!("fonksiyondaki vektör: {:?}", v1);
}

fn main() {
    let v1 = vec![1, 2, 3];
    let referans_vec = &v1;

    foo(v1); // <--- Bu satır 'ödünç alınmış bir değişkeni taşıyamazsınız' diye bir hata verecektir!
}

Bir de referans yıkıldıktan sonra tekrar bu fonksiyona göndermeye çalışalım:

fn foo(v1: Vec<i32>) { // <--- Bu sefer referans almayan bir fonksiyonumuz var.
    println!("fonksiyondaki vektör: {:?}", v1);
}

fn main() {
    let v1 = vec![1, 2, 3];

    { // <--- Anonim bir blok açıyoruz
        let referans_vec = &v1;
    } // <--- Bir değişken, sadece içinde bulunduğu blok içinde var olduğu için bu satırda artık referans yıkıldı.

    foo(v1); // <--- Artık nesnenin var olan bir referansı olmadığı için bu fonksiyon çalışacaktır.
}

Bu örnekte referans anonim bir blok içinde yaratıldı ve yıkıldı. foo() fonksiyonunun çağırıldığı yerde v1 adlı nesnenin bir referansı olmadığı için sorunsuz bir şekilde ownership fonksiyona aktarılacaktır.

Mutable Borrow

Şimdiye kadar hep immutable borrow oluşturduk. Bu nedenle örneklerdeki referansların hepsinde sadece okuma işlemler yaptık, hiç referansın değerini düzenlemedik. Peki şimdi referansı aynı zamanda yazılabilir yani mutable nasıl yaparız? Normal referansı tanımlarken &T yerine &mut T yazdığımızda o referansı mutable olarak tanımlamış oluyoruz. Ancak immutable olan bir değişkenin mutable referansını oluşturamıyoruz. Zaten mantık çerçevesinden bakınca doğru olan da bu gibi gözüküyor. Mutable referansa bir örnekle bakalım hemen:

fn main() {
    let mut x = 5;
    
    {
        let y = &mut x; // Mutable olarak referans alıyoruz.
        *y += 1;  // Referansın gösterdiği kaynığı bir arttırıyoruz.
    }
    
    println!("{}", x); // Artık değişken 6 olduğu için '6' çıktısı verecektir.
}

let y = &mut x satırında zaten eşitliğin sağ tarafında mut anahtar kelimesini yazdığımız için let kelimesinin yanına bir daha mut yazmamıza gerek kalmadı. Compiler zaten otomatik olarak değişkenin ne olduğunu anladı. Mutable olarak almasaydık bu referansı, *y += 1 satırında immutable borrow’a sen bir şey atamaya çalışıyorsun, bunu yapamazsın diye hata verecekti.
Değişkenin kendisini tanımlarken(let mut x = 5;) mut yazmasaydık bu sefer de bize immutable bir değişkenin mutable olarak referansını almaya çalışıyorsun diye hata verecekti. Bunlar genel olarak referanslarla işlemler yaparken dikkat etmemiz gereken noktalar.

Ayrıca eğer bir değişken borrow edilmişse, değişkenin kendisini değiştirmek imkansız oluyor. Buna freezing(dondurma) deniliyor. Bu nedenle bir değişken borrow edilmişse o değişken ile işlemler yaparken dikkat etmek lazım:

fn main() {
    let mut sayi = 7i32;

    {
        // 'sayi' borrow ediliyor.
        let ref_sayi = &sayi;

        sayi = 50; // <--- Buradaki satır, sayı değişkeni referans alındığı için, hata verecektir.
    } // <--- Referans scope dan çıkıyor.

    sayi = 3; <--- Artık referansı bulunmadığı için bu satır hata vermeyecektir.
}
ref Kalıbı

Şimdiye kadar referansları her seferinde &T olarak oluşturduk. Fakat referansları oluşturmanın bir yolu daha var. Bu da ref anahtar kelimesi kullanarak sağlanıyor:

fn main() {
    let sayi = 10;

    let ref ref_sayi1 = sayi;
    let ref_sayi2 = &sayi;
}

Bu gördüğünüz iki yöntem de birbirinin aynısı. İkisi arasında hiçbir fark yok. Bazen struct’lardan referans almak için ref kelimesini kullanmamız gerekebiliyor. Bir struct’tan nasıl referans alacağımızı gösterip bu yazımızı yavaş yavaş sonlandıralım:

struct Point { x: i32, y: i32 }

fn main() {
    let nokta = Point { x: 0, y: 0 };

    let x_in_kopyasi = {
        // 'x_referansi' değişkeni, 'nokta' adlı struct'ın x değerini tutuyor.
        let Point { x: ref x_referansi, y: _ } = nokta;

        // 'x_referansi' değerini döndürerek x değişkeninin kopyasını oluşturuyoruz.
        *x_referansi
    };
}

Bu yukarıda gördüğünüz kod parçası eğer Rust’a çok aşina değilseniz biraz karışık gelebilir. Rust’la içli dışlı oldukça anlaması daha kolay bir hale gelmeye başlıyor. Kısaca anlatmak gerekirse, yukarıda yaptığımız bir Point adlı struct oluşturduk, ardından nokta adında bir struct nesnesi oluşturduk ve bu nokta değişkeninin ‘x’ değerini başka bir değere kopyaladık.
Struct’lar gibi aynı zamanda Tuple’lar ile çalışırken de Tuple’ın içindeki değerlerin referanslarını alırken ref anahtar kelimesi yardımcı olabiliyor:

fn main() {
    let mut mutable_tuple = (5, 3);

    {
        // İkinci elemanını mutable referans olarak almak için tuple'ı parçalıyoruz.
        let (_, ref mut ikinci) = mutable_tuple;
        *ikinci = 2; // İkinci elemanının değerini değiştiriyoruz.
    }

    println!("Tuple: {:?}", mutable_tuple); // Çıktı "Tuple: (5, 2)" olacaktır.
}

Gördüğünüz gibi bazı özel durumlarda bu şekilde ref kalıbını kullanmamız gerekebiliyor. Bu durumlarda & kullansaydık hatayla karşılaşacaktık.

Artık borrowing nedir, referans nedir, nasıl kullanmamız gerekir, belirli bir durumda mutable mı yoksa immutable mı kullanmalıyız, borrow alınması durumunda değişkenin kendisine ne olur gibi soruların yanıtlarını bulmuş olduk. Bundan sonraki yazımda lifetime konusunu anlatmayı düşünüyorum. Umarım sizin için yararlı bir yazı olmuştur. Bir sonraki yazıda görüşmek üzere, hoşça kalın. 🙂