~/Ali GÖREN

Rust’ın Ownership Yapısını Anlamak

Ali Goren · · 5 dk okuma

Rust’ın Ownership Yapısını Anlamak


Selamlar. Bir süredir Rust öğrenme aşamasındayım. Şu anda basit sayılabilecek başlıkları aşmış, daha kafa karıştırıcı noktalara gelmiş durumdayım.

Rust tıpkı diğer sistem programlama dilleri gibi bellek yönetimini ciddiye alan bir dil. Bu noktada dilin yaratılış amacını savunması nedeniyle ortaya çıkartmış olduğu birkaç benzersiz özellik var. Ownership de bunlardan birisi. Bu yazıda buna değinmek istiyorum.

Ownership yaklaşımı, Rust’ın herhangi bir Garbage Collection mekanizmasını devreye almadan memory safety konusunda garanti vermesini sağlıyor. Bu konu Rust dilinin önemli konularından birisi aynı zamanda bu konuya bağımlı Borrowing, References ve Slices gibi konular da var ancak başka bir yazının konusu olması gerektiğini düşünüyorum.

Nedir Ya Bu Ownership?

Aslında ownership’i mevcut programlama paradigmaları ile açıklamak biraz zor. Ownership aslında Rust’ın bellek konusunda davranışlarını tanımlayan bir dizi kuraldan ibaret.

Çalışan bütün programlar bir şekilde çalışma esnasında bellek yönetim mekanizmasını kurcalamak zorundadır.

Diğer Dillerde Nasıl?

Bu bazı dillerde karşımıza Garbage Collector olarak çıkar. Bu işlemden bahsederken, programın çalışma anında sürekli aktif olan ve artık kullanılmayan bellek alanıın gözetleyen bir özellikten bahsedebiliriz.

Yine bazı dillerde ise bellekte allocation yani alan tahsisi ve free yani tahsis edilen alanın geri verilmesi tamamen programcıya bağlıdır.

Bu iki yaklaşım bize bu işlemlerin runtime’da yapıldığını ve bazı riskler barındırdığını söylüyor. İşte Rust burada üçüncü türden kendine has bir özellikle geliyor.

Rust ile ownership kuralına takılan kodlar, programın derlenmesini engelleyecektir. Yani burada bir compile-time error alacağımız durumlar ortaya çıkabilir şeklinde bir varsayımda bulunabiliriz. Bu işlemin henüz derleme aşamasında kontrol edilmesi, programınızın yavaş çalışmasının da önüne geçecektir.

İşte Rust dilinde bu ownership kavramını anladığınızda, dilin büyük bir çoğunluğunu da işgal edecek olan konuyu anlamış olacaksınız.

Stack ve Heap Konusu

Rust tıpkı diğer diller gibi bellek yönetimini birkaç şekilde ele alır. Bunlardan ikisi Stack ve Heap.

Bellek kullanımı her programcının kabusudur ancak diğer dillere göre, sistem programlama dillerinde bu işi biraz daha odaklanma, doğru kararlar alma gerektirebiliyor. Çünkü sistem programlama yapacağınız dillerde bir verinin stack ya da heap üzerinde saklanması dilin davranışından tutun da tasarımsal kararların alınması noktasına kadar önemli bir husustur.

Programlarınız belli noktalarda stack belli noktalarda heap kullanır yani her ikisini de zaten kullanıyorsunuzdur. Sadece bu alanlar yapısal olarak farklı şekillerde bulunmaktadırlar. Stack için konuşmak gerekiyorsa LIFO burada işler, son giren ilk çıkar şeklinde.

Stackte veriyi ortaya ya da aşağıya ekleyeyim burada iyi görünüyor demek mümkün değil ve buraya eklenen verinin boyutu compile-time’da yani derleme zamanında bellidir. Eğer ki boyutu belirsiz bir tür saklayacaksanız işte burada devreye heap giriyor.

Tabii ki burada heap göründüğü gibi daha düzensiz formatta. Heap’e veri eklerken bir miktar alana ihtiyacınız olduğunu belirtirsiniz. Memory allocate işlemini yürüten process, yeteri kadar büyüklükte alanı bulduktan sonra “bu alan kullanılıyor arkadaş” diyor ve bu alanın pointer’ını size döndürüyor.

Kalınla işaretlediğim bulduktan sonra kısmı işleri biraz karıştıran, maliyetliymiş be dediğimiz noktayı size getiriyor olabilir. Bunu çok büyük ama içerisi kalabalık bir restoranda boş masa aramaya benzetebilirsiniz. Bu temel bilgiler daha da genişletilebilir ancak asıl konuda bizi koparacak gibi görünüyor.

Ownership Kuralları

Rust’ın ownership mekanizması üç farklı kurala sahiptir.

  • Rust’ta her değerin owner olarak bilinen bir değişkeni vardır
  • Bir değer için aynı anda sadece bir owner mevcuttur
  • Eğer owner, bulunduğu scope’un dışına çıkarsa, değer droplanacaktır yani invalidate olur

String Türü

Bu yazıda String türü üzerinden ilerleyip ownership yaklaşımını anlamaya çalışacağız.

let ad_soyad = String::from("Ali GÖREN");

let ad_soyad_copy = ad_soyad;

println!(ad_soyad);

Yukarıdaki kod derleme zamanı hatası verecektir. Çünkü ad_soyad değişkeni sahipliğini kopyalama aşamasında ad_soyad_copy’e devretmiştir. Eğer ad_soyad değişkeni invalidate olmasaydı yani bellekteki alanı geri almasaydı şöyle bir şey olacaktı

Soldaki kutucuk ad_soyad’ın işaret ettiği alanı, sağdaki ise ad_soyad_copy’nin işaret ettiği alanı bize verecek. Bir bellek noktasına 2 farklı değişken işaret ediyor olacaktı.

Bu da bize double-free gibi memory deallocation hatalarını getirecekti ki bu nedenle bellek güvenliğinde problemler yaşayabilirdik. İşte Rust bu konuları daha derleme aşamasında ele alarak gereksiz alan kullanımını ve olası bugları elimine ediyor.

Rust bu aşamada bir değişkenin ownerlığı farklı bir scope’a geçiyorsa otomatik olarak drop adlı bir fonksiyonu çağırıyor. Bu işlemle beraber yukarıda berbat bir şekilde çizmiş olduğum görsel aslında şöyle bir hale alıyor

Yani ad_soyad artık geçersiz bir değişken, memory tarafında cleanup gerçekleştirilmiş.

E Ben İki Değişkeni de Kullanmak İstiyorum?

Rust bize bu konuda biraz daha maliyetli de olsa bir yöntem sunuyor. Değişkeni kopyalamak. Diğer dillerden de deep copy, shallow copy kavramlarına aşinasınızdır.

let ad_soyad = String::from("Ali GOREN");

let ad_soyad_copy = ad_soyad.clone();

println!("Ad Soyad: {}, Ad Soyad Copy: {}", ad_soyad, ad_soyad_copy);

Bu kod parçası herhangi bir derleme hatası oluşturmayacaktır. Çünkü bir owner transferi yok, mevcut datadan bir tane daha oluşturma işlemi burada var. Bu nedenle maliyetli bir yapı burada karşımıza çıkabiliyor

Primitive Türlerde İşler Nasıl?

Örneğin şöyle bir yapımız olsun

let number = 10;

let number_copy = number;

println!("Number: {}, Number Copy {}, number, number_copy);

Yukarıdaki kod tek başına derleme zamanında hata oluşturmayacaktır. Çünkü ilk başta açıkladığımız stack ve heap kavramlarından da hatırlayacağımız üzere stack, boyutu belli değerleri saklar. Burada da integer bir değerin derleme anında boyutu bellidir. Bu nedenle number adlı değişkenin deallocate edilmesi gibi bir gereksinimi burada duymadık.

Ownership Başka Nasıl Gerçekleşir?

Bu uzun yazının sonuna gelirken birkaç ownership örneğini sunmak isterim.

İlk örnek bir fonksiyona değer paslarken;

let name = String::from("Ali GÖREN");

get_name_with_ownership(name);

get_name_with_ownership fonksiyonuna name değişkenini pasladığımız anda değişken artık invalid durumdadır yani o satırdan sonrasında yoktur. Bunu istemiyorsak ya clone kullanırız ya da aynı fonksiyondan değer return ederek tekrar veriyi elde edebiliriz.

fn get_name_with_ownership(name: String) -> {

    return name;
}

Yukarıdaki fonksiyon ile aldığımız ve invalid ettiğimiz değişkenin sahipliğinin yeni atanacağı noktaya transfer edildiğini söylüyoruz. Yani artık sahiplik bu fonksiyon içerisinde de droplandı.

Yazı biraz uzun oldu ancak Rust’ın en çok kafa karıştıran konularından birisi. Umarım açıklayabilmişimdir. Okuduğunuz için teşekkür ederim :)

Kaynaklar

What is Ownership