Nazım Can Altınova

Rust'ta Borrowing ve Referans Kavramları

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’i öğ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’te 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ı 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.

Buradaki güzel nokta, compiler’ın her zaman referansın geçerli bir nesneye işaret 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şturduğumuzu görelim:

fn main() {
    // Box içinde bir integer oluşturuyoruz.
    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 onun referansını alabiliyoruz. 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‘e bir referans olduğu için ikisini de kullanabiliyoruz.

Şimdi biraz daha gerçekçi bir örnek görelim. Örneğin bir değişkeni fonksiyona parametre olarak vereceksiniz, fakat fonksiyondan sonra o değişkeni kullanmak da istiyorsunuz. Borrowing olmasaydı 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'te 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’ta değişkenlerle işlemler yapmak gerçekten çok zor bir hal alırdı.

Şimdi de biraz önce bahsettiğimiz, referanslar var olduğu sürece kaynağın silinmemesi konusuna gelelim. Eğer kaynak referanstan önce silinirse, referans ya null bir alanı gösterecek ya da başka bir kaynağı gösterecekti. Bu da yazılımcı için büyük bir sorun olurdu. 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şlemleri yaptık, hiç referansın değerini düzenlemedik. Peki bir referansı aynı zamanda yazılabilir, yani mutable nasıl yaparız? 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 kaynağı bir artı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ı. Bu referansı mutable olarak almasaydık, compiler *y += 1 satırında immutable bir borrow’a atama yapamayacağımızı söyleyen bir 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, sayi değişkeni referans alındığı için, hata verecektir.
    } // <--- Referans scope'tan çı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 kelimesini 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ı kolaylaşıyor. Kısaca anlatmak gerekirse, yukarıda önce Point adlı bir 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ğişkene kopyaladık.

Struct’larda olduğu gibi, tuple’lar ile çalışırken de tuple’ın içindeki değerlerin referanslarını almak için 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 edilmesi 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. :)