You are currently viewing Rust Programlama Diline Bakış

Rust Programlama Diline Bakış

Herkese merhaba, bu yazımızda son zamanlarda oldukça popüler hale gelmiş, kullanımı gittikçe yaygınlaşan, ekosistemi genişleyen, Linux kerneli kod altyapısına eklenmiş ve daha güvenli uygulamalar için ABD federal devleti tarafından önerilen bir programlama dili olan Rust programlama dilini inceleyeceğiz. Rust için orta seviyeli, genel amaçlı, çöp toplama (garbage collection) mekanizması bulunamayan fakat çok büyük oranda bellek yönetimi açısından güvenli (memory safe), C tarzı sentaks yapısına sahip bir dil diyebiliriz. Gelin çok vakit kaybetmeden ilk Rust programımızı yazalım ve dilin ön plana çıkan kendine has özelliklerine göz atalım.

Rust Kurulumu

Rust programlama dili, derleyici versiyonlarını ve rus ekosisteminde bulunan diğer araçları yönetmek için rustup denilen bir araç kullanmaktadır. Linux ve MacOS sistemlerde tek bir komut ile rustup kurabilirsiniz:

$ curl –proto ‘=https’ –tlsv1.2 https://sh.rustup.rs -sSf | sh

Windows sistemler ise aşağıdaki linkten kurulum dosyasına ulaşabilirsiniz:

https://www.rust-lang.org/tools/install

rustup kurulumu yapıldıktan sonra rustup update komutu ile Rust derleyicisinin en son versiyona güncellenmesi sağlanabilir. Bu adımlar yapıldıktan sonra Rust derleyicinin versiyonunu kontrol etmek için şu komutu kullanabilirsiniz.

Kurulumun başarılı olduğunu doğruladıktan sonra gelin ilk projemizi oluşturalım. Bunun için Rust paket yöneticisi aracı (package manager) cargo‘yu kullanabiliriz. Bunun için şu komutu çalıştıralım.

Böylece “HelloWorld” isimli ilk projemizi oluşturmuş olduk. Gelin projemiz içine göz atalım.

cargo init komutu bizim için otomatik olarak git repository’si ve .gitignore dosyasını oluşturmaktadır. Derleme sonucu oluşan dosyalar target dizini altında tutulmaktadır. Cargo.lock ve Cargo.toml dosyaları ise projemizde kullandığımız paketleri ve projemize ait bazı bilgileri tuttuğumuz bir dosyadır. Bu dosyayı Javascript ekosisteminde bulunan package.json dosyasına benzetebiliriz. Bu dosya içeriğine bakacak olursak:

Mevcut paketimize ait bazı bilgiler ve paketlerin listeneceği bir alan görüyoruz. Henüz bir paket kullanmadığımızdan bu liste şu anda boş. Projemizin kodları ise src dizininin altında bulunmaktadır. cargo init komutu bizim için halihazırda bir “Hello, world!” programı yazmaktadır. Gelin nasıl yazılmış bakalım.

main fonksiyonumuzun parametre almayacak ve bir geri değer döndürmeyecek bir şekilde tanıımlandığını görüyoruz. Ayrıca standart çıkış’a (stdout) yazdırmak ise println makrosunun kullanıldığını görüyoruz. Rust dilinde declarative ve procedural olmak üzere iki çeşit makro vardır. println makrosu ise declarative makro sınıfına girmektedir. Bu tür makrolar ünlem işareti (!) ile bir fonksiyon çağırmaya benzer şekilde çağırılırlar. Makro konusunun detayları ileri bir konu olduğundan bu yazımızda işlenmeyecektir.

Programımızı derleyip çalıştırmak için cargo run komutunu çalıştırabiliriz. cargo build komutu ise yalnızca derleme işlemini yapacaktır.

Bu yazımızda yalnızca Rust’a has özelliklere bakacağız. Detaylı bir şekilde Rust sentaksını ve temel programlama konseptlerini öğrenmeniz için resmi Rust başlangıç kitabını okumanızı tavsiye ediyoruz. Bu yazımızda vurgulayacağımız özellikleri şu şekilde sıralayabiliriz:

  • Varsayılan olarak değişkenlerin değiştirilemezliği (immutability by default)
  • Değişkenlerin sahipliği ve ödünç verme (ownership ve borrowing)
  • Referanslar (references)
  • Akıllı referanslar (smart pointers)
  • Nesne yönelimli yaklaşım (Object oriented approach)

Değişkenlerin Değiştirilemezliği (Immutability)

Rust programlama dilinde değişkenler varsayılan olarak değiştirilemezlerdir. Bir örnekte bunu görelim.

Burada n değişkenini değiştirmeye çalıştırdığımızda bu değişkenin değiştirilemez olduğu ile ilgili bir hata alıyoruz. Bir değişkenin değiştirilebilir olduğunu açık olarak tanımlamak için değişken tanımında let anahtar sözcüğünün yanında mut anahtar kelimesini kullanmalıyız.

mut anahtar sözcüğü eklendiğinde sorunun giderilmiş olduğunu görüyoruz. Bazılarınız buna ne gerek olduğunu, bir değişkenin doğası gereği değiştirilebilir olduğunu düşünebilir. C, C++, Java gibi pek çok dil bu şekildedir ve bir değişken sabit olarak tanımlanmak istendiğinde const gibi anahtar sözcükler ile açıkça belirtilir. Rust dilinde ise bu ters bir yaklaşım bulunmaktadır. Ben şahsen bu yaklaşımı beğenmekteyim çünkü bu yaklaşıp hangi değişkenin değiştirilebileceği konusunda bir kontrol sağladığından hata tespitlerinde büyük kolaylık sağlamaktadır.

Sahiplik ve Ödünç Verme (Ownership and Borrowing)

Rust dilinde bellekte bulunan herhangi bir değişkenin değerine yalnızca bir değişken sahip olabilmektedir. Bu durum değişkenin sahipliği konusunda net bir kontrole sahip olmamızı sağlar. Temel veri tipleri atamalar sırasında doğrudan kopyalanabilirlerken karmaşık tiplerde bu söz konusu değildir. Karmaşık veri tipleri arasında atama yapıldığında değişkenin sahipliği taşınır ve taşınan değişken programın devamında kullanılamaz.

Bu örneğimizde String veri tipini kullandık. Bu veri tipi içinde barındırdığı sabitin sahipliğini taşır. Bu yüzden varsayılan olarak doğrudan kopyalanamaz. Eğer açıkça klonlama işlemi yapmak istiyorsak a üzerinde clone metodunu kullanabiliriz.

Bu örnekte b değişkeni için a nesnesinin bir kopyasını oluşturmak istediğimizi açıkça belirtmiş olduk. Benzer sahiplik kuralları fonksiyonlara parametre geçerken de geçerlidir. Örneğimizde bakalım.

Bu örneğimizde Flo isimli bir yapı tanımlanmış ve bu yapının bir örneği oluşturularak proc_foo fonksiyonuna parametre olarak verilmiştir. Doğrudan değer olarak verildiğinden bu yapının sahipliği fonksiyona geçmiştir. Bu noktada Rust derleyicisi bize meseleyi açık şekilde anlatmaktadır ve bir çözüm önermektedir. Bu öneri ise bizi bir sonraki konumuza getirmektedir: referanslar.

Referanslar

Referansları C ve C++ dillerindeki pointer’lara benzetebilir. Değişkenin doğrudan değerini tutmak yerine adreslerini tutarlar. Böylece diğer fonksiyon ve değişkenlere sahiplik vermeden referans gönderebiliriz. Bir önceki görselimizdeki hatayı referans kullanarak çözebiliriz.

Burada iki temel değişiklik yaptık. Birincisi metod imzamızı değiştirerek Flo nesnesini değer olarak değil, referans olarak kabul edeceğimizi belirttik. İkincisi ise fonksiyonu çağırırken başına ‘&’ karakteri koyarak referans olarak verdik. Böylece Foo yapısının sahipliği a değişkeninde kalmış oldu. Peki ya referans üzerinden Foo yapısının elemanları üzerinde değişiklik yapmak isteseydik o zaman ne olacaktı?

Tıpkı değişkenlerde olduğu gibi referanslar da varsayılan olarak değiştirilemezlerdir. Bunun için değiştirilebilir referans (mutable reference) kullanmalıyız. Bunun için yine mut anahtar sözcüğünü kullanıyoruz.

Bir önceki değişikliğimize benzer şekilde burada da method imzamızı ve parametre geçme şeklimizi değiştiriyoruz. Burada bir detaydan bahsetmemiz gerekiyor. Rust dili kuralları gereği bir değişkenin istenildiği kadar referansı bulunabilir. Fakat değiştirilebilir referanslar ile değiştirilemez referanslar aynı anda bulunamaz. Daha iyi anlaşılabilmesi için bir örnek ile canlandıralım.

Gördüğünüz üzere değişkenin bir referansı mevcutken değiştirilebilir olan referans veremiyoruz. Aslında bu davranışın çok mantıklı bir sebebi vardır. Değiştirilemez bir referans adı üzerinde değiştirilemez olduğundan dolayı yaşam süreci boyunda referans olduğu değişkenin değişmemesini bekleyecektir. Fakat bir başka değiştirilebilir referans bu durumu bozmaktadır. Bu sebeple Rust dilinin sahip olduğu borrow checker mekanizması bu durumu önlemektedir. Bu mekanizma değişkenlerin ve referansların nerelerde ne şekilde şekilde değiştirilebileceğine ilişkin bu şekilde katı kurallar koymaktadır. Programcılar programlarını bu kurallara uygun şekilde dizayn etmek zorundadır. Özellikle de paralel olarak çalışan programların (concurrent) geliştirmesinde zorluk çıkarabilmektedir. Fakat bu kısıtlamalar, çöp toplayıcı (garbage collector) kullanmadan güvenli ve performanslı uygulamalar geliştirmek için ödediğimiz bir bedeldir.

Akıllı Referanslar (Smart Pointers)

Akıllı referanslar, bir başka değerin sahipliğini taşıyan ve referanslarını manipüle etmemize yardımcı olan yapılardır. İlk olarak C++ dilinde ortaya çıkmışlardır. Bu anlamda bir metinsel ifadeye sahiplik yapan String yapısı da bir akıllı referanstır. Rust dilinde bulunan özel akıllı referans yapıları şu şekildedir.

  • Box<T>: Heap’te saklanan değişkenlerden oluşturmak ve heap’e referans vermek için kullanılır. Değiştirilebilir ve değiştirilemez referans olarak verilebilir. Ayrıca büyüklüğü çalışma zamanında bilinemeyen yapıların da saklanmasını sağlamaktadır. Örneğin bir bağlı liste.
  • Rc<T>: Açılımı reference counter’dır. Bir değer için birden çok referans verilebilir ve bu referans sayısı otomatik olarak takip edilir. Referans sayısı 0’a düştüğünde saklanan veri otomatik olarak yok edilir. Yalnızca değiştirilemez referans verebilir.
  • RefCell<T>: Borrow checker kuralları varsayılan olarak derleme zamanında (compile time) kontrol edilmektedir. RefCell yapısı bize bu kuralların çalışma zamanında (runtime) kontrol edilmesine olanak sağlamaktadır. Bu da bize dışarıdan değiştirilemez olarak gözüken referansların yapı içerisinde değiştirilebilmesine olanak sağlamaktadır. (internal mutability pattern)

Akıllı referanslar hakkında detaylı bilgi ve örnekler için resmi Rust başlangıç kitabını okuyabilirsiniz.

Yaşam Süreleri (Lifetimes)

Bir değişkenin yaşam süresi program akışında tanımlandığı yerden, tanımlanmış olduğu bloğun sonuna kadardır. Her referansın da bir yaşam süresi vardır. Bu referansların artık var olmayan bir değişkeni göstermediğinden emin olmak için bu referansların yaşam sürelerinin, tuttukları değerlerin yaşam sürelerinden daha uzun olduğundan emin olmak zorundayız. Rust varsayılan olarak bizim için bundan emin olmaktadır. Ve bunun için gereken yerlerde yaşam süresi parametrelerini kullanmaktadır. Bu da Rust’ın çöp toplayıcı olmadan bellek açısından güvenli olmasını sağlamaktadır. Örneğimize bakalım.

Burada b değişkeni, tanımlandığı bloktan çıkıldığında yok edilecektir. Fakat a referansı hala hayatına devam etmektedir. Bu durum borrow checker tarafından tespit edildi.

Yapılar ve fonksiyonlar referans tutabileceğinden bu referanslar için de yaşam süresi kuralları geçerlidir. Bir yapıda referans tutmayı deneyelim.

Derleyici bu durumda bizden yaşam süresi parametresi istemektedir (lifetime parameter). Yaşam süresi parametreleri bir referansın yaşam süresi hakkında derleyiciye bilgi vermek için kullanılır. Bu noktada a referansının yaşam süresinin yapının yaşam süresi ile aynı olduğunu belirtmek için yaşam süresi parametresi vermeliyiz.

Yaşam süresi parametreleri (‘) işareti ile başlar ve genellikle kısa ve küçük harfli isimler kullanılır. Bu parametreler kendi başlarında pek anlam ifade etmezler fakat referanslarının birbirlerine göre durumlarını belirtirler. Yukarındaki örnekte a referansının yaşam süresinin, yapının yaşam süresi kadar olacağı belirtilmiştir.

‘static isimli özel bir yaşam süresi parametresi vardır. Bu yaşam süresi, değişkenin çalışan programın çalışma zamanı boyunca geçerli olacağını ifade eder.

Yukarıdaki örneklerde üstteki kod geçerlidir. Çünkü global blokta tanımlanmış sabit değerler program çalışma zamanı boyunca yaşarlar. O yüzden Foo yapısındaki ‘static yaşam zamanlı referans tarafından kabul edilebilir. Fakat aşağıdaki örneğimizde a değişkeninin yerel bir yaşam süresi vardır. ‘static bir referansın yaşam süresi daha uzun olduğundan kendisinden daha kısa bir yaşam süresine sahip referansı kabul etmeyecektir. Benzer yaşam süresi kuralları fonksiyon parametrelerinin ve geri dönüş değerleri de eğer bir referans türünden ise geçerlidir.

Nesne Yönelimli Yaklaşım (Object Oriented Approach)

Rust dilinde sınıflar yerine yapılar kullanmaktadır. Bu yapılar varsayılan olarak stack’te tutulur (Heap’te oluşturmak için Box<T> yapısı kullanılabileceğinden bahsetmiştik.). Rust dilinde bu yapılara methodlar atanabilmektedir. Bunun için implementasyon blokları kullanılır.

Burada implementasyon blokları ve fonksiyon parametreleri için de yaşam süresi parametresini kullandığımızı görüyoruz. Bir yapı referansı olarak çağrılacak metodlar ilk parametre olarak &self parametresini alır. Bu şekilde çağrılan fonksiyonlar o yapının elemanlarına ulaşabilirler. Eğer o yapının elemanlarını değiştirmek istiyorsa &mut self tanımlaması ile kendilerinin değiştirilebilir bir referansını almak zorundadırlar.

Rust dilinde arayüzler (intefaces) bulunmaz. Fakat benzer amaçlar için kullanılabilecek trait denen bir özellik sunmaktadır. Bu trait’ler aynı arayüzler gibi yapılarda belirli imzalara sahip metodların bulunmasını zorunlu kılar. Örneğin öğrenci için notlandırıldıklarını belirten bir trait tanımlayalım.

Traitler içerisinde gövdesi bulunmayan methodlar barındırır. Trait’leri implemente etmek içinse yine implementasyon blokları kullanılır. Bir trait implementasyon bloğunda görebileceğimiz üzere tüm metodlar implemente edilmek zorundadır.

Generic yapı veya fonksiyon tanımlamalarımızda ve fonksiyon parametrelerimizde belirli trait’lere sahip yapılar isteyebiliriz.

Bu yazımızda Rust programlama dilinde öne çıkan bazı özelliklere göz attık. Rust ile ilgili daha detaylı eğitim ve içerikler için bizleri takip etmeeye devam edebilirsiniz. Yorum ve görüşlerinizi yazımızın altında bulunan yorum kısmından bizlere iletebilirsiniz.

Bir yanıt yazın