Merhaba arkadaşlar, geçen seferki yazımda Rust programlama dili ile hakkında bir ön bilgi verip, onda bulduğum olumlu ve olumsuz gözlemlerimi ve düşüncelerimi aktarmaya çalışmıştım. Yazıya buradan ulaşabilirsiniz. Aslında ownership(sahiplik), borrowing(borçlanma) ve lifetime(kullanım süresi, yaşam) konularını o yazının içinde açıklamak istemiştim fakat daha sonra yazının çok uzayacağını ve ana konudan şaşacağını düşündüğüm için ayrı ayrı yazılar halinde yazmak istedim. Rust programlama dilini diğer dillerden ayıran özelliklerin başında bu üçü geliyor bence. Başka bir programlama dilini biliyor ve Rust’a geçiş yapıyorsanız, diğer dillerin perspektiflerinden baktığımızda tamamen mantıklı olan bir kodu çalıştırdığınızda Rust’da çalışmadığını, hata verdiğini görebiliyorsunuz 🙂 . Bunun en büyük nedeni tabi ki Rust programlama dilinin bakış açısına göre (Ownership, borrowing ve lifetime mantığına göre) düşünemiyor oluşumuz oluyor. Ayrıca Rust’ın diğer dillere göre çok daha katı bir dil olduğunu söylemek gerekir sanırım. Rust bazı şeyleri runtime’da kontrol etmek veya yazılımcının inisiyatifine bırakmak yerine derleme sırasında yüzünüze çarpabiliyor. Bunların önüne geçmek için bu üç konu hakkında iyi bir bilgiye sahip olmak gerekiyor. Açık olması için elimden geldiğince örnekli bir biçimde anlatmaya çalışacağım.

Ownership Kavramı

Diğer dillerde işler nasıl yürüyor?

Düşük seviyeli, garbage collection‘ı olmayan dillerde; değişkenlerin, sakladıkları kaynakların serbest bırakılmalarından yazılımcının kendisi sorumludur. Genelde o kaynak ile işlemler bittiğinde ve ona bir daha ihtiyaç duyulmadığında kaynak serbest bırakılır ve kullanılan bellek alanı iade edilir. Örneğin siz bir C dilinde pointer’lar ile bu işlemi yapmak isterseniz şu şekilde yazmanız gerekir.

int main(int argc, const char * argv[]) {

    char *str;
    str = (char *) malloc(10); // Bellekte bir yer allocate ediyoruz.
    // str ile bir takım işlemler...

    free(str); // str pointer'ını ve kullandığı resource'u serbest bırakıyoruz.
    
    return 0;
}

Basit bir şekilde bir char pointer’ı tanımladık ve bu pointer’ı malloc ile 10 karakterlik bir bellek alanı ayırdık. Ardından pointer ile işimiz bittiğinde, free fonksiyonu ile kaynağı serbest bırakmamız gerekti. Eğer bırakmazsak program bu kaynağı işletim sistemine iade edemeyecek ve bu kaynak bellekte yer tutmaya devam edecektir. Ta ki bilgisayar yeniden başlatılana kadar. Bu bahsettiğimiz olayın adına bellek sızıntısı (memory leak) adı veriliyor. Büyük çaplı programlarda eğer dikkat edilmezse çok fazla baş ağrıtabilecek bir sorun. Yüksek seviyeli, garbage collection’a sahip dillerde bunlar garbage collection’ın yapmakla yükümlü olduğu şeylerdir. Bu nedenle o dillerde bunlara çok fazla dikkat etmemiz gerekmiyor.

Peki Rust’da durumlar nasıl?

Rust’da ise işler biraz farklı yürüyor. Rust, düşük seviyeli bir fonksiyonel programlama dili olmasından dolayı garbage collection’ı bulunmuyor. Fakat Rust bu konuda değineceğimiz özellik olan ownership ve diğer yazılarımda değineceğim borrowing ve lifetime sayesinde garbage collection’ı olmayan diğer dillere göre bir adım öne çıkıyor. Bunlar sayesinde kendimiz bir resource’u serbest bırakmadan compiler bu işi kendisi garbage collection olmadan halledebiliyor. Fakat bu özellik bize aynı zamanda bazı kısıtlamalar katıyor. Buradaki ana kavram ownership. Şimdi hemen Rust bu işi ownership ile nasıl hallediyor ona geçelim.

Rust’da her değişken kendi kaynakları serbest bırakmada yine kendileri sorumludur.(C gibi dillerde bundan yazılımcılar sorumluydu. Daha yüksek seviyelilerde ise garbage collection.) Bir kaynağın sadece bir sahibi(owner) olabilir. Tabi bazı değişkenler hiçbir kaynağa sahip olmayabilir de. (Örneğin referanslar) Bir başka değişkene atama yaptığımızda (Örn: let x = y;) veya fonksiyona parametre olarak değişkeni verdiğimizde(Örn: fonk(x)) o kaynağın sahibi de aktarılır. Buna Rust terminolojisinde move(yani taşıma) adı veriliyor. Bir kaynağı taşıdığımızda, artık o kaynağın bir önceki sahibi kullanılamaz olur. Bu da silinen bir kaynağı hala işaret eden bir pointer’ın olmasının önüne geçiyor.

Bir şey de açıklığa kavuşturmak lazım. Yine C gibi stack’de depolanan(stack allocated) bir değişkenler için bu dediğimiz ownership geçerli değil. Yani bunlar integer bool, char gibi primitive’ler. Bu bahsettiğimiz, heap’de depolanan(heap allocated) değişkenlerin pointer’ları için geçerli. Bu bahsetiğimi birkaç örnekle açıklayayım:

fn main() {
    // 'Stack' allocated integer değeri
    let x = 5i32;

    // 'x' değişkenini `y` değişkenine 'kopyalıyoruz' (taşımıyoruz). - Herhangi bir kaynak taşınmıyor.
    let y = x;

    // İki değişken de birbirinden bağımsız olarak kullanılabilir.
    println!("x değişkeni: {}, ve y değişkeni {}", x, y);
}

Gördüğünüz gibi üst taraftaki kod normal bir c kodu mantığına eşdeğer gibi.
Şimdi heap allocated bir değişkene gelelim. Bu sefer bu ownership dediğimiz kavram geçerli olacak:

fn main() {
    // `a` bir 'heap' allocated integer değerin pointer'ı.
    let a = Box::new(5i32);

   println!("a'nın taşıdığı değer: {}", a);

    // Artık a değişkenini b'ye 'taşıyoruz'. Bundan sonra a kullanım dışı olacak. Eğer a'yı kullanmaya çalışırsak compiler'dan 'use of moved value' diye bir hata alırız.
    let b = a; // <----- a'nın kaynağı taşındığı için kullanılabilirliği burada sonlanıyor.

   println!("b'nin taşıdığı değer: {}", b); // Sorunsuz çalışacak
   println!("a'nın taşıdığı değer: {}", a); // Taşınmış bir değişkeni kullanmaya çalışıyorsun diye hata verecek!
}

Gördüğünüz gibi bir kaynak sadece bir değişken(pointer) tarafından kullanılabildi. İlk pointer artık geçerliliğini yitirdi.
Eğer bir fonksiyona parametre olarak verirsek ise artık o kaynağın geçerli olduğu kısım fonksiyon bloğunun içi oluyor. gönderdiğimiz yerdeki değişken artık kullanılamaz bir hal alıyor:

// Parametre olarak verilen Box'ın sahipliğini alacak. Bu nedenle fonksiyon bitiminde c'nin taşıdığı kaynak silinecek
fn box_sil(c: Box<i32>) {
    println!("c'yi istediğimiz gibi kullanıyoruz: {}", c);

} // <----- Burada artık c nin geçerli olduğu blok sonlandığı için c değişkeni ve taşıdığı kaynak siliniyor.

fn main() {
    let a = Box::new(5i32);

   println!("a'nın taşıdığı değer: {}", a);

   box_sil(a); // <----- Burada a'nın kaynağını taşıdığımız için artık a kullanılamaz oluyor.

   println!("a'nın taşıdığı değer: {}", a); // a kullanılamaz olduğu için hata verecektir.

}  // <----- Eğer box_sil'i kullanmasaydık değişken burada kendiliğinde silinecekti.

Ayrıca son olarak bahsetmem gereken olay ownership ile mutability(değişebilirlik) arasındaki ilişki. Mutability yani bir değişkenin değişebilir olup olmaması Rust’da mut anahtar kelimesi ile sağlanıyordu. Kısaca hatırlamak gerekirse:

let x = 10;
x = 20; // Değişkenler Rust'da varsayılan olarak değiştirilemez(immutable) olduğu için hata verecektir.

let mut y = 15;
y = 25; // Burada 'mut' anahtar kelimesi kullandığımız için değişebilir(mutable) bir değişken elde ettik. Buradaki kod sorunsuz çalışacaktır.

Bir değişkenin mutable olup olmadığı sahipliğin transferi sırasında değiştirilebilir. Yani bir değiştirilemez bir değeri değiştirilebilir olarak başka bir değişkene şu şekilde aktarabilirsiniz:

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

    println!("immutable_vec değişkeni: {:?}", immutable_vec);

    // Değiştirilemez bir değeri değiştirilebilir olarak başka bir değişkene taşıyoruz.
    let mut mutable_vec = immutable_vec;

    println!("mutable_vec değişkeni: {:?}", mutable_vec);

    // Artık bu değişkeni istediğimiz gibi değiştirebiliriz.
    mutable_vec.push(4);

    println!("şimdi de mutable_vec değişkeni: {:?}", mutable_vec);
}

Evet, artık Rust’da ownership nedir öğrenmiş olduk. Bunun ardından bu kavramların devamı olan borrowing ve lifetime üzerinde yazmayı planlıyorum. Umarım sizin için yararlı bir yazı olmuştur. Diğer yazılarımda görüşmek üzere, şimdilik hoşça kalın!