Neden yeni bir programlama dili öğrenmelisiniz?

Rust hakkında bir karikatür

David Marino'ya teşekkürlerle!

Karikatürün çevirisi:

Rust: Bu gece şöförlük yapacağım.

Dış ses: Biliyoruz.

Rust: Konuşurken yemek yeme çünkü boğazına takılırsa ölürsün. Haha.

Bu rehberin amacı kutsal kitabımız gibi İnternet'te bulabileceğiniz çeşitli kaynakları anlayacak kadar Rust okuryazarlığı aşılamaktır. Rehberi, bu programlama dilinin gücünü tonla şeyin arasına girmeden anlamak ve denemek için bir fırsat olarak düşünebilirsiniz.

Einstein'in dediği gibi, "Her şeyi olabildiğince yumuşak yapın, ama yumuş yumuş olmasın." (Ç.N: Einstein'in "Her şeyi daha sade yapın, ama basit değil" sözüne gönderme.) Rust'a dair öğrenecek epey şey var ve pek çok şey iyice kafanızı bulandırabilir. Burada kastedilen "yumuşaklık", zorluğuna karşın Rust'ın çözümlerinin uygulamalı olarak sunulmasıdır. Sorunları anlamak, hemen çözümleri görmekten çok daha faydalıdır. Bunu kayaçların süreçlerini anlamak için coğrafya dersinden sonra hemen dağ bayır gezintiye çıkmak gibi düşünebilirsiniz. Bunun elbette zorluğu olacak ama neticesi oldukça hoş olacak. Katılanlar oldukça memnun olacak ve birbirlerine yardımcı olmaktan çekinmeyecekler. (Buna benzer olarak) Rust kullanıcıları forumu (İngilizce) ve oldukça aktif bir subreddit (Bu da İngilizce) var. Aynı zamanda bazı sorularınız varsa sıkça sorular sorular (Herhâlde İngilizce)'a da bakabilirsiniz.

Ama hepsinden önce, neden durup dururken yeni bir dil öğrenelim ki? Bu öyle ya da böyle epey vakit ve enerji alan, durup dururken yapılmayacak bir iş. Bu iş size çok süper, über bir iş buldurmayacak olsa bile beyin kaslarınızı çalıştıracak ve sizi daha iyi bir programcı yapacaktır. İyi bir yatırım mı? Tartışılır. Yine de gerçek anlamda bir şey öğrenmezseniz zaman içerisinde durgunlaşacak ve on yılı aşkın aynı şeylere bakan birisi olacaksınız.

Rust'ın Parladığı Nokta

Rust statik ve güçlü tiplenen bir sistem programlama dilidir. Statik bütün tiplerin derleme zamanında bilinmesi anlamına gelir, güçlü ise programın çalışma mantığının tip mantığının dışına çıkmaması demektir. Başarılı bir derleme aynı zamanda C gibi tekinsiz bir dile göre çok daha fazla şeyi garanti ettiğiniz anlamına gelir. Sistem (Programlama)'dan kasıt makine için en uygun kodun sıkı bir bellek denetimi ile oluşturulmasıdır. Kullanım alanları biraz ilginç: işletim sistemleri, donanım sürücüleri ve bir işletim sistemi bile olmayan gömülü sistemler. Ancak Rust normal uygulamaları programlamak için de gayet uygundur.

C ve C++ ile Rust arasındaki en büyük fark doğal olarak emniyetli olmasıdır. Bütün bellek erişimleri denetlendiğinden kazara belleği darmaduman etmeniz mümkün değildir.

Rust'ın ana prensipleri şunlardır:

  • Verinin emniyetli olarak ödünç alınmasını zorlamak
  • Fonksiyon, metot ve kapamalar (closure) ile veriye müdahale etmek
  • Verileri çokuzlu (tuple), yapılar (struct) ve numaralandırmalar (enum) ile bir araya toplamak
  • Verileri örüntü eşleştirme (pattern matching) ile seçmek ve parçalarına ayırmak
  • Verilerin görevini özellikler (trait) aracılığı ile tanımlamak

Cargo sayesinde oldukça hızlı büyüyen bir ekosistem olsa da biz çoğunlukla dilin temel özelliklerini anlamaya ve standart kütüphaneyi kullanmaya eğileceğiz. Tavsiyem çok sayıda ufak programlar yazmanızdır, bundan dolayı rustc kullanmayı öğrenmek en önemli beceri olacaktır hâliyle. Bu rehberde bulacağınız pek çok örneği çalıştırmak için derleyen ve programı yürüten rrun diye minik bir betik hazırladım:

rustc $1.rs && ./$1

Kurulum

Bu rehber makinenize Rust'ı kurmayı gerektirir. Neyse ki bunu yapmak çok basit.

$ curl https://sh.rustup.rs -sSf | sh
$ rustup component add rust-docs

Kararlı sürümü kullanmanızı tavsiye ediyorum; sonradan kararsız sürümü kurup aralarında geçiş yapmak da kolaydır.

Bu komut derleyiciyi Cargo paket yöneticisini, API belgelendirmesini ve Rust kitabını indirir. Uzun yolculukların hepsi bir adımla başlar ve bu adımı atmak zahmetsizdir.

rustup komutu Rust kurulumunuzu kontrol eden komuttur. Yeni bir kararlı sürüm yayınlandığında yalnızca rustup yazarak güncelleyebilirsiniz. rustup doc ise resmi Rust belgelerini çevrimdışı olarak tarayıcınızda açar.

Muhtemelen beğendiğiniz bir editor vardır ve bu editörün temel Rust desteği varsa o editörü istediğiniz gibi kullanabilirsiniz. Öncelikle sözdiziminin renklendirilmesi ile yetinmenizi öneririm, programlarınız büyüdükçe daha ötesine geçersiniz.

Şahsen varsayılan olarak Rust desteği ile gelen nadir editörlerden birisi olan Geany'i beğeniyorum; paket yöneticisiyle kurulabildiği için Linux üzerinde kullanmak gayet kolaydır, diğer platformlarda da pekâlâ kullanılabilir.

Esas nokta Rust programlarını düzenleyebilmek, derleyebilmek ve çalıştırabilmektir. Programlamayı parmaklarınızla anlayın, kodu kendiniz yazın ve editörün kodu nasıl düzelteceğini öğrenin.

Zed Shaw'ın programlamayı Python ile öğrenme tavsiyesi geçen yıllara rağmen dil fark etmeksizin dikkate değerdir. Ona göre programlamayı öğrenmenin bir müzik enstürmanı öğrenmek gibidir - esas sır pratik ve sabırdır. Taiciçüen gibi yumuşak dövüş sanatlarının ve Yoga'nın bir güzel öğüdü vardır; gerilimi hissedin, ama aşırı gerilmeyin. Karanlık bir spor salonunda sert bir disiplini sadakatle zayıflamaya çalışmıyorsunuz; rahatlayın.

Kötü İngilizcemi veya yetersiz Rust bilgimi yakalayarak beni uyaran destekçilere teşekkürler etmek isterim, aynı zamanda David Marino'ya Rust'ı parlayan zırhı içerisinde sempatik fakat mantığına sıkı sıkıya bağlı bir şovalye olarak çizdiği için de teşekkürlerimi sunarım.

Steve Donovan © 2017-2018 MIT Lisans Versiyonu 0.4.0

Çeviri: Emrecan Şuşter

Bazı Çeviri Notları

Bu çeviriyi motamot çevirmek yerine, kaldı ki bunu makineler de yapabiliyor, içeriğinde de kültüre özgü değişiklikler yaptım. Maksat rehberin ana dilinde yazılmış gibi bir akıcılıkla okunabilmesi idi. Çoğu yerin birebir çevrilmediğini, bazı şeylerin eklendiğini de göreceksiniz. Bu İngilizce ile Türkçe arasındaki hem kültürel hem de anlamsal farklardan dolayıdır.

Epey yerleşmiş, Türkçesi de (göreceli olarak) oldukça anlamsız gelen bazı kavramları doğrudan İngilizce hâliyle metin içinde kullandım. (Heap ve Stack gibi) Ancak bunun dışında çoğu yerde Türkçe karşılıkları kullanmaya özen gösterdim ve yanların parantez içinde veya "/" işareti ile İngilizce karşılıklarını bildirdim. Terimler için "Rust Dili" projesinin sözlüğünden faydalandım.

Bunun dışında kod parçalarına dokunmadım, içindeki yorumları bile olduğu gibi bıraktım zira bu kod parçaları değiştiği zaman çıkacak ekstra iş gücünden çekindim.

Çeviriyi geliştirmek için her zaman çevirinin Github deposuna uğrayabilirsiniz.

Merhaba Dünya

"Merhaba Dünya"'nın esas amacı, C'nin ilk versiyonu yazıldığından beri, derleyiciyi test etmek ve gerçek bir program çalıştırmaktır.

// hello.rs
fn main() {
    println!("Hello, World!");
}
$ rustc hello.rs
$ ./hello
Hello, World!

Rust'ta süslü ayraçlar ve noktalı virgül vardır, C++ tarzı yorum satırları bulunur ve bir de main fonksiyonu bulunur. Şimdiye kadar bu kısmı tanıyorsunuz. Ünlem işareti, bunun bir makro çağrısı olduğunu gösterir. C++ programcıları için bu biraz caydırıcı olabilir, çünkü onların tek bildiği makrolar o abuk subuk C makrolarıdır - ama bu makroların çok daha yetenekli ve kabul edilebilir olduğunu rahatlıkla söyleyebilirim.

"Güzel de bu ünlem işaretini nereye sıkıştıracağımı nereden bileyim" diye aklından geçirenler olmuştur. Ancak derleyici beklemediğiniz kadar yardımsever; eğer ünlem işaretini unutursanız şunu görürsünüz:


#![allow(unused)]
fn main() {
error[E0425]: unresolved name `println`
 --> hello2.rs:2:5
  |
2 |     println("Hello, World!");
  |     ^^^^^^^ did you mean the macro `println!`?
}

Bir dili öğrenmek o dilin hatalarıyla barışık olmak demektir. Derleyiciyi sizi azarlayan bir bilgisayar olarak görmek yerine katı ama dostane davranan bir yardımcı olarak görmeye çalışın, çünkü başlangıçta epeyce kırmızı yazılar göreceksiniz. Derleyicinin sizin hatalarınızı yüzünüze vurması, insanların sizin yüzünüze vurmasından kat kat daha iyidir.

Bir sonraki aşama değişken atamaktır.

// let1.rs
fn main() {
    let answer = 42;
    println!("Hello {}", answer);
}

Yazım hataları derleme zamanında anlaşılır, Python ya da JavaScript gibi çalışma zamanını beklemenize gerek yoktur. Bu, sizi daha sonra pek çok stresten kurtaracak! Eğer "answer" yerine "answr" yazarsam, derleyici bu konuda epey kibar davranır:


#![allow(unused)]
fn main() {
4 |     println!("Hello {}", answr);
  |                         ^^^^^ did you mean `answer`?
}

println! makrosu bir format karakter dizesi alır; Python3'te kullanılan formatlama stiline epey benzerdir.

Bir başka kullanışlı makro ise assert_eq!. Bu Rust testlerinin direğidir, iki şeyin birbirine eşit olduğu varsayarsınız. (assert = varsaymak) Eğer eşit değillerse, panik.

// let2.rs
fn main() {
    let answer = 42;
    assert_eq!(answer,42);
}

Herhangi bir çıktı olmayacaktır. Ancak 42'yi 40 ile değiştirirseniz:

thread 'main' panicked at
'assertion failed: `(left == right)` (left: `42`, right: `40`)',
let2.rs:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Ve bu bizim Rust'taki karşımıza çıkan ilk çalışma zamanı hatası.

Döngüler ve Koşullamalar

Enteresan olan her şey tekrar tekrar yapılabilir:

// for1.rs
fn main() {
    for i in 0..5 {
        println!("Hello {}", i);
    }
}

Aralık (Range) kapsayıcı değildir, bundan dolayı i'nin değeri 0 ila 4 arasında değişir. Dizilerin indekslerinin sıfırdan başladığı bir dilde pek de olağandışı değildir.

Enteresan şeyler de koşula bağlı olarak da gerçekleştirilebilir.

// for2.rs
fn main() {
    for i in 0..5 {
        if i % 2 == 0 {
            println!("even {}", i);
        } else {
            println!("odd {}", i);
        }
    }
}
even 0
odd 1
even 2
odd 3
even 4

i % 2 eğer i, 2'ye tam olarak bölünebiliyorsa sıfır olur; Rust C-tarzı operatörler kullanır. Koşulların etrafında parantez yoktur, tıpkı Go'daki gibi, ama blokların etrafında süslü parantezlerin kullanımı zorunludur.

Aynı şey, daha da ilginç bir yoldan yapılabilir:

// for3.rs
fn main() {
    for i in 0..5 {
        let even_odd = if i % 2 == 0 {"even"} else {"odd"};
        println!("{} {}", even_odd, i);
    }
}

Klasik olarak, programlama dillerinde deyimler (statement) (If gibi) ve ifadeler (expression) (1+i gibi) bulunur. Rust'ta, her şeyin değeri olabilir ve bunlar bir ifade olabilir. C'nin o garabet "ternary/üçlü operatörüne" burada ihtiyacımız yok.

Aynı zamanda bloklarda noktalı virgül olmadığına da dikkat edin!

Şeyleri Şeylere Eklemek

Bilgisayarlar aritmatik konusunda epey iyidir. 0'dan 4'e kadar bütün sayıları toplamayı deneyelim.

// add1.rs
fn main() {
    let sum = 0;
    for i in 0..5 {
        sum += i;
    }
    println!("sum is {}", sum);
}

Ama derlenirken hata verecektir:

error[E0384]: re-assignment of immutable variable `sum`
 --> add1.rs:5:9
3 |     let sum = 0;
  |         --- first assignment to `sum`
4 |     for i in 0..5 {
5 |         sum += i;
  |         ^^^^^^^^ re-assignment of immutable variable

"Immutable"? Değişemeyen değişen mi? let değişkenlerinin değeri sadece atanırken belirtilebilir. mut ismindeki sihirli sözlük (nolur bu değişkeni değişebilir yap) işi halledecektir:

// add2.rs
fn main() {
    let mut sum = 0;
    for i in 0..5 {
        sum += i;
    }
    println!("sum is {}", sum);
}

Değişkenlerin varsayılan olarak yeniden yazılabilir olduğu dillerden geçerken bu biraz kafa karıştırıcı olabilir. Bir şeyi değişken yapan şey onun değerinin çalışma zamanında atanmasıdır, sabitlerin (constant) aksine. Bu kavramlar matematikte de kullanılır, mesela "let n be the largest number in set S (N'i S kümesi içerisindeki en büyük değer yap)" derken.

Değişkenlerin varsayılan olarak salt okunur olmasının ardında bir neden vardır. Büyük bir programda, değerlerin nerede atandığını bulmak oldukça güçleşebilir. Bundan dolayı Rust, değişimlerin bildirilmesini ister. Dilde zekice epey şey var ancak dil hiçbir şeyin örtük kalmamasına ayrıca özen gösteriyor.

Rust hem statik hem de güçlü tiplenen bir dildir - bu kavramlar genelde karıştırılır, ama C (statik ama zayıf tiplenen) ve Python'u (dinamik ama güçlü tiplenen) göz önüne getirin. Statik tiplemede tip derleme zamanında bilinir, dinamik tiplemede ise çalışma zamanında.

Tam da bu anda, Rust'ın sizden tipleri gizlediğini sezebilirsiniz. Mesela i'nin tam değeri nedir? Derleyici için bu sorun değildir, 0'dan başlarken, tip çıkarımı ile (type referance) bu sayılar i32 (Dört bitlik işaretli tam sayı) oluverir.

Hadi net bir değişim yapalım, 0'ı 0.0 ile değiştirip hataları görelim:

error[E0277]: the trait bound `{float}: std::ops::AddAssign<{integer}>` is not satisfied
 --> add3.rs:5:9
  |
5 |         sum += i;
  |         ^^^^^^^^ the trait `std::ops::AddAssign<{integer}>` is not implemented for `{float}`
  |

Pekâlâ, şimdi güldük eğlendik ama bu da nesi? Bütün operatörler (Mesela +=) bir özelliğe (trait) denk gelir ki özellik (trait) somut tiplere yeni özellikler ekleyen soyut arabirimlerdir. Özelliklerle daha sonra ilgileneceğiz, ama burada bilmeniz gereken bütün şey AddAssign, += operatörünü sağlayan özelliğin adı olduğudur ve hata mesajının demek istediği şey bu özelliğin noktalı sayılara bu operatör tam sayılarla işlem yapmak için uygulanmadığıdır. (Operatör özelliklerinin tam listesi burada.)

Rust'ta her şey bellidir - sırf sizin gönlünüz olsun diye tam sayıyı noktalı sayıya gizlice çevirmeyecektir.

// add3.rs
fn main() {
    let mut sum = 0.0;
    for i in 0..5 {
        sum += i as f64;
    }
    println!("sum is {}", sum);
}

Fonksiyonların Tipleri de Apaçık Ortadadır

Fonksiyonlar da derleyicinin sizin için tipleri tahmin etmekle uğraşmayacağı yerlerden birisidir. Aslında bu üzerinde düşünülerek alınmış bir karardır çünkü Haskell gibi güçlü tip çıkarımlarına sahip dillerde tip isimleri nadiren yazılır. Aslında Haskell için tipleri açıkça yazmak iyi yaklaşımdır. Rust ise her zaman bunu mecbur tutar.

İşte tanımladığımız basit bir fonksiyon:

// fun1.rs

fn sqr(x: f64) -> f64 {
    return x * x;
}

fn main() {
    let res = sqr(2.0);
    println!("square is {}", res);
}

Rust biraz eski bir argüman bildirimi tarzı kullanmakta, tip isimden sonra gelir. Bu, Pascal gibi Algol'dan türemiş dillerde kullanılan tarzdır.

Hatırlatalım, tam sayı noktalı sayıya dönüşmez - eğer 2.02 ile değiştirirseniz nurtopu gibi bir hatanız olmuş olur:


#![allow(unused)]
fn main() {
8 |     let res = sqr(2);
  |                   ^ expected f64, found integral variable
  |
}

Rust'da fonksiyonlarda çok az return deyiminin kullanıldığının görürsünüz. Daha çok, şuna benzer ifadeler vardır:


#![allow(unused)]
fn main() {
fn sqr(x: f64) -> f64 {
    x * x
}
}

Fonksiyonun gövdesi ({ } içi) tıpkı "ifade olarak kullanılan if"teki gibi son ifadenin değerini alır.

Noktalı virgülleri refleks olarak kazara ekleyebilirsiniz ve o zaman şöyle bir hata alırsınız:


#![allow(unused)]
fn main() {
  |
3 | fn sqr(x: f64) -> f64 {
  |                       ^ expected f64, found ()
  |
  = note: expected type `f64`
  = note:    found type `()`
help: consider removing this semicolon:
 --> fun2.rs:4:8
  |
4 |     x * x;
  |       ^

}

() tipi boş tiptir, yokluktur, voiddir, "nothing"dir, tasavvuftaki fakrdır. Rust'ta her şeyin değeri vardır, ama bazen sadece yoktur. Derleyici bunun sıkça karşılaşılan bir durum olduğunu bilir, ve size aslında yardım eder. (C++ derleyicileriyle vakit harcamış zavallı ruhlar bunun ne kadar faydalı olduğunun farkındadır.)

Return kullanılmayan ifadelere biraz daha örnek verelim:


#![allow(unused)]
fn main() {
// absolute value of a floating-point number
fn abs(x: f64) -> f64 {
    if x > 0.0 {
        x
    } else {
        -x
    }
}

// ensure the number always falls in the given range
fn clamp(x: f64, x1: f64, x2: f64) -> f64 {
    if x < x1 {
        x1
    } else if x > x2 {
        x2
    } else {
        x
    }
}

Return kullanmak yanlış değil, ama kod onsuz daha temiz. Yine de, bir fonksiyondan erken dönmek için return kullanabilirsiniz.

Bazı işlemler zarif bir yoldan özyinelemeli olarak yazılabilir:


#![allow(unused)]
fn main() {
fn factorial(n: u64) -> u64 {
    if n == 0 {
        1
    } else {
        n * factorial(n-1)
    }
}
}

Başta tuhaf görünebilir ve en iyisi kağıt kalemle örnekler üzerinde düşünmektir. Ancak, bir işlemi yapmanın en etkili yolu değildir.

Değerler aynı zamanda referans olarak da iletilebilir. & ile yaratılmış bir referans * dereferans edilebilir. (Ç.N: De- olumsuzlaştırma öneki)

fn by_ref(x: &i32) -> i32{
    *x + 1
}

fn main() {
    let i = 10;
    let res1 = by_ref(&i);
    let res2 = by_ref(&41);
    println!("{} {}", res1,res2);
}
// 11 42

Bir fonksiyonun argümanlarını değiştirebilmesini mi istiyorsunuz? Değişebilir referans (Mutable referance) kullanın:

// fun4.rs

fn modifies(x: &mut f64) {
    *x = 1.0;
}

fn main() {
    let mut res = 0.0;
    modifies(&mut res);
    println!("res is {}", res);
}

Bu C++'dan çok C'ye benzedi. Açıkça referansı (& ile) belirtmelisiniz ve aynı şekilde * ile deferans etmelisiniz. Sonra da mut'u ekleyin çünkü varsayılan değişebilir değiller. (Bana hep C++ referansları C'ye göre gözden kaçırılmaya daha müsaitmiş gibi gelir.)

Temel olarak, Rust burada biraz bizi yoruyor ve fonksiyonlardan değer döndürmeye zorluyor. Neyse ki, Rust'ın "işlem başarılı, bu da sonucu" gibi güçlü ifadeleri olduğundan &mut'u sıklıkla kullanmayız. Referans kullanmak, büyük bir nesnemiz olduğunda ve onu kopyalamak istemediğimizde dikkate değerdir.

"Değişkenden sonra tip gelir" tarzı let için de gayet uyuyor, bir değişkenin türünü belirtmek istersek eğer:


#![allow(unused)]
fn main() {
let bigint: i64 = 0;
}

Yolumuzu Yordamımızı Bilmek

Şimdi belgelendirmeye bakmanın tam zamanı. Belgeler makinenize yüklenmiş olmalı ve onu rustup doc --std komutu ile tarayıcınızda açabilir olmalısınız.

Arama kutucuğunun en üstte olduğuna dikkat edin, zira bu sizin en yakın dostunuz olacak; çalışmak için İnternet'e gerek duymaz.

Diyelim ki matematiksel fonksiyonların nerede olduğunu merak ediyorsunuz, "cos" diye aratmanız yeterli. Klavyede dokunduğunuz ilk iki tuş her iki noktalı sayı tipi için de var olduğunu gösterir. Aradığımız işlem, değerin kendisinde metot olarak tanımlıdır, mesela şöyle:


#![allow(unused)]
fn main() {
let pi: f64 = 3.1416;
let x = pi/2.0;
let cosine = x.cos();
}

Sonuç sıfıra epey yakın çıkacaktır, belli ki tahmini değere değil gerçek PI sayısına ihtiyacımız var.

(Sahi, neden f64 diye belirtmemize gerek var ki? Aslına bakarsanız o olmadan değerimiz f32 veya f64 olabilir ki bunlar epey farklı şeyler.) (Ç.N: Noktalı sayı tutan)

Cos için verilen örneğe bakalım, bunu çalışabilir bir programa çevirdik. (assert! de assert_eq!'in amcaoğlu oluyor, verilen ifade kesinlikle doğru olmalıdır.)

fn main() {
    let x = 2.0 * std::f64::consts::PI;

    let abs_difference = (x.cos() - 1.0).abs();

    assert!(abs_difference < 1e-10);
}

std::f64::consts::PI şu güzel ortamı iyice bozdu! :: C++'daki anlamıyla aynı şeye denk geliyor, (Bazı dillerde yerine . kullanılır) - bu tam yolu belirtilmiş bir isim. Bu tam adı, PI için yaptığımız aramayı yaparken ikinci klavye tuşlamasında alıyoruz.

Şimdiye dek, bizim ufak Rust programımıza "Merhaba Dünya" tartışmalarında gündemi meşgul eden import ya da include gibi şeyleri eklemedik. Hadi, programımızı bir use deyimi ile şenlendirelim:

use std::f64::consts;

fn main() {
    let x = 2.0 * consts::PI;

    let abs_difference = (x.cos() - 1.0).abs();

    assert!(abs_difference < 1e-10);
}

Tamam da buna neden şimdiye dek ihtiyaç duymadık? Çünkü Rust prelude aracılığıyla, use deyimini kullanmaya gerek bırakmadan pek çok temel işlevi görünür kılar (ama siz kullanana kadar yüklemez).

Diziler ve Dilimler

Bütün statik tiplenen dillerde diziler (array) bulunur, bu birden çok veriyi bellek içerisinde baştan sona kontrol eder. Diziler, sıfırdan itibaren indekslenir.

// array1.rs
fn main() {
    let arr = [10, 20, 30, 40];
    let first = arr[0];
    println!("first {}", first);

    for i in 0..4 {
        println!("[{}] = {}", i,arr[i]);
    }
    println!("length {}", arr.len());
}

Ve çıktı:

first 10
[0] = 10
[1] = 20
[2] = 30
[3] = 40
length 4

Burada Rust dizinin büyüklüğünü net olarak bilir ve eğer arr[4]'e erişmeye çalışırsanız derleme hatası alırsınız.

Yeni bir dil öğrenmek aynı zamanda diğer dillerden edindiğiniz alışkanlıkları da terk etmek demektir; eğer bir Pythonista iseniz bu köşeli parantezleri List diye isimlendirebilirsiniz. Rust'taki List'in muadiline daha sonra bakacağız, ancak diziler düşündüğünüz işi yapmıyor; sabit bir büyüklükleri vardır. (Eğer yalvarırsak) değişebilirler ancak yeni değerler ekleyemeyiz.

Diziler Rust'ta o kadar çok kullanılmaz, çünkü her dizi tipi uzunluğunun bilgisini de taşır. Mesela [i32; 4] dizi tipine bakabilirsiniz; aynı zamanda [10, 20] olan bir dizinin tipi de [i32; 2] olacaktır vs, hepsinin farklı tipi vardır. Yani bunlar aslında fonksiyon argümanı olmaktan başka şeye yaramayan başıboş serserilerdir.

Esas sık kullanılanlar dilimlerdir. Bunları bir dizinin parçalanmış hâli1 olarak düşünebilirsiniz. Tıpkı dizilerin davrandığı gibi davranırlar ve uzunluklarını bilirler, C'deki gösterici (pointer) denen korkunç yaratıkların tam tersi olarak.

İki önemli noktaya dikkat edin - bir dilimin tipi nasıl yazıldığına ve fonksiyona ne zaman & eklemeniz gerektiğine.

1

Ç.N: Esas çeviride "parçalanmış hâl" yerine "görünüm (view)" kelimesi kullanılıyor. İngilizce için cümle gayet geçerli, ancak Türkçe'de tuhaf duruyor.

// array2.rs
// read as: slice of i32
fn sum(values: &[i32]) -> i32 {
    let mut res = 0;
    for i in 0..values.len() {
        res += values[i]
    }
    res
}

fn main() {
    let arr = [10,20,30,40];
    // look at that &
    let res = sum(&arr);
    println!("sum {}", res);
}

Sum'da kodu bir saniyeliğine görmezden gelin ve &[i32]'ye bakın. Rust dizileri ve dilimleri arasındaki ilişki C'deki diziler ve göstericiler arasındaki ilişkiye benzer, iki detay hariç - Rust dilimleri kendi uzunluğunun takibini yaparlar (ve eğer bu uzunluğun dışına çıkarlarsa paniklerler) sonra da & 'ı operatörünü kullanarak dilim olarak kullanmak istediğinizi açıkça belirtmeniz gereklidir.

Bir C programcısı &'ı gördüğü zaman "falancanın adresi" diye okur, Rust programcısı ise "ödünç (borrow)" olarak. Bu, Rust öğrenirken dikkat etmeniz gereken kilit sözcüktür. Ödünç alma, esasında programlamada kullanılan genel bir terimdir ve (Dinamik dillerde her zaman olduğu gibi) referans olarak bir veriyi ya da C'de bir gösterici (pointer) yollamanıza denir. Ödünç alınan her şey esas sahibinde kalır.

Dilimleme ve Biçme

Bir diziyi {} yolu ile ekrana yazamazsınız fakat hata ayıklama (debug) yani {:?} ile ekrana yazdırabilirsiniz.

Bu:

// array3.rs
fn main() {
    let ints = [1, 2, 3];
    let floats = [1.1, 2.1, 3.1];
    let strings = ["hello", "world"];
    let ints_ints = [[1, 2], [10, 20]];
    println!("ints {:?}", ints);
    println!("floats {:?}", floats);
    println!("strings {:?}", strings);
    println!("ints_ints {:?}", ints_ints);
}

Bunu yazdırır:


#![allow(unused)]
fn main() {
ints [1, 2, 3]
floats [1.1, 2.1, 3.1]
strings ["hello", "world"]
ints_ints [[1, 2], [10, 20]]
}

Bu arada, dizilerin dizileri de olabilir ancak dizide sadece bir tipten değerler bulunmalıdır. Dizideki değerler verimlilikten dolayı bellekte yanyana bulunurlar ki bu erişim için oldukça faydalıdır.

Eğer bir değişkenin gerçek tipini merak ediyorsanız, size bir hile gösterebilirim. Bir değişkeni, geçersiz olduğunu bildiğiniz bir tiple bildirin:


#![allow(unused)]
fn main() {
let var: () = [1.1, 1.2];
}

İşte aradığınız şeyi gösteren hata:

3 |     let var: () = [1.1, 1.2];
  |                   ^^^^^^^^^^ expected (), found array of 2 elements
  |
  = note: expected type `()`
  = note:    found type `[{float}; 2]`

({float}, "bir nevi noktalı sayı ama tam tipi henüz belirtilmedi." demektir.)

Dilimler size aynı dizinin farklı parçalarını sunar:

// slice1.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice1 = &ints[0..2];
    let slice2 = &ints[1..];  // open range!

    println!("ints {:?}", ints);
    println!("slice1 {:?}", slice1);
    println!("slice2 {:?}", slice2);
}

Bu, Python'daki dilim anlayışına epey yakındır ancak arada büyük bir fark vardır: Veri asla kopyalanmadı. Bu dilimler, bütün verilerini dizilerden ödünç alırlar. Dilimlerin dizilerle epey sıkı bir bağ vardır ve Rust bu bağın kopmaması için elinden gelen her şeyi kuvvetle yapar.

Opsiyonel Değerler (Optional Values)

Dilimler, diziler gibi, indekslenebilir. Rust, derleme zamanında dizinin değerini bilir, ama bir dilimin değeri ancak çalışma zamanında bilinebilir. Bundan dolayı, s[i] kullanımı belleğin yanlış bir yerine erişmeye sebep olabilir ve bu durumda program panikleyecektir. Bu gerçekten de olmasını istediğiniz şey değildir - aradaki fark Florida'dan atılacak çok pahalı bir uydunun gökyüzünde parçalanması ile atışın güvenlice iptal edilmesine kadar varabilir. Ve burada hata yakalama mekanizmaları (exceptions) yok.

Şimdi hazır olun zira gelecek şey sizi şok edecek. Burada panikleyebilecek kodları try-bloğu ve hatayı yakala (try - catch) yapısı yok - en azından her gün kullandığınız şekliyle yok. Peki, Rust nasıl güvenli kalabiliyor?

İşte size bir paniklemeyen get metotu. İyi de, bu ne dönüyor?

// slice2.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice = &ints;
    let first = slice.get(0);
    let last = slice.get(5);

    println!("first {:?}", first);
    println!("last {:?}", last);
}
// first Some(1)
// last None

last çuvalladı. (Sıfır temelli indekslemeyi unuttuk.) Ama None diye bir şey döndü. first için sorun yok, ama Some diye bir şey dönüverdi. Option tipini selamlayın! Bu tip Some olabilir, None olabilir.

Option tipinin gayet faydalı metotları vardır:


#![allow(unused)]
fn main() {
    println!("first {} {}", first.is_some(), first.is_none());
    println!("last {} {}", last.is_some(), last.is_none());
    println!("first value {}", first.unwrap());

// first true false
// last false true
// first value 1
}

Eğer last üzerinde unwrap kullanırsanız nurtopu gibi bir paniğiniz olur. Bunun yerine en azından is_some kullanabilirsiniz - varsayılan bir değeriniz varsa gayet faydalıdır:


#![allow(unused)]
fn main() {
    let maybe_last = slice.get(5);
    let last = if maybe_last.is_some() {
        *maybe_last.unwrap()
    } else {
        -1
    };
}

* operatörünün kullanıldığına dikkat edin - Some içindeki esas tip bir referans olan &i32'dir. i32 verisini almak için veriyi deferans ediyoruz.

Bu biraz işi uzatıyor, onun yerine bir kısayol kullanabiliriz - unwrap_or metodu Option None ise yerine bir değer atayabilir. Tipler muhakkak uyuşmalı -get referans dönecek. Bundan ötürü bir &i32 olan &-1'ı kullanmamız gereklidir. Şimdi tekrar * operatörünü i32 değeri almak için kullanalım.


#![allow(unused)]
fn main() {
    let last = *slice.get(5).unwrap_or(&-1);
}

&'ı unutmak gayet olası ama derleyici arkanızı toplayacaktır. -1 yazsaydık, rustc şuna benzer bir hata verecekti: "&{integer} bekleniyordu ancak tam sayı alındı" ve eklerdi ki: "yardım: &-1'ı deneyin" *

* "expected &{integer}, found integral variable", "help: try with &-1"

Option tipini veri taşıyan bir paket olarak zihninizde canlandırabilirsiniz, ya da hiçbir şey ifade etmeyen bir değer (None). (Haskell'deki karşılığı Maybedir.) Bu tip tipi belirtilebilen herhangi bir veriyi barındırabilir. Bizim örneğimizde üzerinde çalıştığımız tip Option<&i32>'dir, C++'ın "Genellemeler (generics)" yazılımı ile gösterirsek. Paketi açmak bir patlamaya sebep olabilir ancak bu mevzu Schrödinger'in kedisi kadar karmaşık değil ve önceden paketin içinde ne var bilebiliriz.

Rust fonksiyonlarının ve metotların bu tür paketleri döndürmesi gayet olağandır ve üzerinde uzmanlaşana kadar nasıl kullanıldığını öğrenin.

Vektörler

Ç.N: Doğrusunu isterseniz ilk gördüğümde bu vektörleri geometrideki vektörlerle ilişkili zannetmiştim. Yüzde yüz programlama deyimi, aklınıza farklı şeyler gelmesin. :)

Tekrar dilim metotlarına döneceğiz ancak vektörleri gözden geçirelim. Bunlar yeniden biçimlendirilebilen dizilerdir ve Python'un Listine ve C++'ın std::vector'üne epey benzerler. Rust'ın Vec tipi ("vektör" olarak okunur.) dilimlere çok benzerler; esas farklılıkları ise vektöre yeni bir veri ekleyebiliyor olmanız - değişebilir (mutable) olarak değişkenin bildirilmesi kaydı ile.

// vec1.rs
fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    let first = v[0];  // will panic if out-of-range
    let maybe_first = v.get(0);

    println!("v is {:?}", v);
    println!("first is {}", first);
    println!("maybe_first is {:?}", maybe_first);
}
// v is [10, 20, 30]
// first is 10
// maybe_first is Some(10)

Yeni başlayanların başına sıklıkla gelen şey mut eklemeyi unutmalarıdır; bunu yaparsanız dostça uyarılırsınız.

3 |     let v = Vec::new();
  |         - use `mut v` here to make mutable
4 |     v.push(10);
  |     ^ cannot borrow mutably

Vektörler ve dilimler arasında çok yakın bir bağ vardır.

// vec2.rs
fn dump(arr: &[i32]) {
    println!("arr is {:?}", arr);
}

fn main() {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v.push(30);

    dump(&v);

    let slice = &v[1..];
    println!("slice is {:?}", slice);
}

Ufak ama önemli ödünç alma operatörümüz &, vektörü dilime çevirmeye zorluyor. (coercing). Ve bu pek mantıksız değil çünkü vektörler bellekte dinamik bir yer tutarlar ve dizi gibi çalışırlar.

Eğer dinamik tipli bir dilden geliyorsanız, sizinle bazı şeyleri konuşmanın vakti geldi. Sistem programlama dillerinde iki farklı bellek yönetim tarzı vardır: Yığıt (Stack) ve Öbek (Heap).2 Stack bellek üzerinde oldukça hızlı bir şekilde alan tahsis ederler ancak yapıları ancak bir kaç megabaytla çıkabilecek kadar sınırlıdır. Heap ise gigabaytlara kadar çıkabilir ancak alan tahsis etme süreci biraz meşakkatlidir ve bu bellek alanının sonradan temizlemesi gereklidir. Bazı sözüm ona "yönetilen (managed)" dillerde (Bunlar Java olur, Go olur, bazı sözde betik dilleri olur) bu tarz detaylar sizden gizlenir ve belediyemizin çöp toplayıcıları (garbage collector) tarafından bu pis işler halledilir. Sistem, bir verinin başka bir veriye referans gösterilmediğine emin olunca kullanılabilir bellek alanına geri döner.

2

Yığıt ve Öbek, benim çeviri standartlarıma göre bile aşırı yapay kalıyor. Bundan ötürü kafa karışıklığını ortadan kaldırmak için Heap ve Stack kelimelerinden devam ettim.

İşin özü bu durumun faydaları olsa da bazı sorunları da vardır. Stack ile oynamanın bazı tehlikeleri var ve içinde bulunduğunuz fonksiyonun dönüş adresini bozabilirsiniz, sonra da iğrenç bir şekilde can verirsiniz. Ya da daha da kötüsü, Hacker Okan'ın elini öpmek zorunda kalabilirsiniz.

İlk C programım (DOS'ta yazmıştım) tüm bilgisayarı çökertmişti. Unix sistemleri bu tarz şeylere karşı daha iyi tavır alırdı ve segfault mekanizması ile kontrolden çıkan süreçler "öldürülür". Peki, bu neden Rust'ın (ya da Go'nun) paniklemesinden daha kötüdür? Çünkü panik sorunun olduğu yerde meydana gelir, bütün program birbirine girdiğinde ve ev ödevlerine dadandığında değil. Panikler bellek için emniyetlidir (memory safe) çünkü belleğin canına okunmadan hemen önce gerçekleşirler. Bu, C'deki güvenlik sorunlarının yaygın bir nedenidir çünkü bütün bellek erişimleri emniyetsizdir ve işi bilen bir saldırgan bu güvensizlikten faydalanabilir.

Panikler kulağınıza korkunç ve plansız gelebilir ama Rust'ın panikleri bile yapılandırılmıştır - stack tek tek serbest bırakılır. Bellekte tahsis edilmiş alanı olan bütün veriler boşatılır ve geriye dönük bir rapor oluşturulur.

Peki çöp toplayıcıların (garbage collector) dezavantajları nedir? Birincisi belleği çok hoyratça kullanıyorlar, sizin için önemli olmayabilir ama gömülü mikroçiplerde bu çok fena bir sorun oluşturur. İkincisi, en olur olmaz zamanlarda belleği temizlemeye başlamasıdır. (Odanızda uzanmış telefonda sevgilinizle hassas bir konuşma yaparken birden odanızı temizlemeye kalkışan annenizi düşünün.) Gömülü sistemlerin olaylara gerçekleştiği anda yanıt vermesi gerekir ve planlanmamış bir temizliğe hiç tahammütleri yoktur. Roberto Lerusalimsch, Lua gibi çok zarif bir dinamik dilin baş tasarımcısı, çöp toplayıcılı bir yazılımın kullanıldığı uçakta asla uçmak isteyemeyeceğini söylemiştir.

Vektörlere geri dönelim, bir vektör yaratıldığı ya da düzenlendiğinde heap içerisinden alan tahsis eder ve bu tahsis edilen alanın sahibi olur. Vektör öldüğünde ya da bellekten temizlendiğinde, bellek de serbest bırakılır.

Döngüleyiciler (Iterators)

Rust bilinmezinin en temel noktasından henüz bahsetmedik - döngüleyiciler. Bir aralık (range) üzerinde kullanılan for döngüsü bir döngüleyici (iterator) kullanır. (0..n Python3'teki range fonksiyonuna benzer.)

Bir döngüleyiciyi fark etmek oldukça kolaydır. Option değerini bize dönen next metotuna sahip bir "objeye" döngüleyici deriz. None dönene kadar, next kullanabiliriz.

// iter1.rs
fn main() {
    let mut iter = 0..3;
    assert_eq!(iter.next(), Some(0));
    assert_eq!(iter.next(), Some(1));
    assert_eq!(iter.next(), Some(2));
    assert_eq!(iter.next(), None);
}

for var in iter {} 'in yaptığı da tam olarak budur.

Bu size for döngüsü tanımlamanın faydasız bir yolu gibi görünebilir ancak rustc'nin yapacağı akıl almaz optimizasyonların sonucunda While döngüsü kadar hızlı çalışacaktır.

Bir dizi üzerinde döngü kurmayı deneyelim:

// iter2.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr {
        println!("{}", i);
    }
}

Elbette ki hata dönecek. Ama çıktıya bakınca:

4 |     for i in arr {
  |     ^ the trait `std::iter::Iterator` is not implemented for `[{integer}; 3]`
  |
  = note: `[{integer}; 3]` is not an iterator; maybe try calling
   `.iter()` or a similar method
  = note: required by `std::iter::IntoIterator::into_iter`

Rustc'nin tavsiyesiyle programımız bir kez daha çalıştı:

`// iter3.rs
fn main() {
    let arr = [10, 20, 30];
    for i in arr.iter() {
        println!("{}", i);
    }

    // slices will be converted implicitly to iterators...
    let slice = &arr;
    for i in slice {
        println!("{}", i);
    }
}

Aslında, dizi üzerinde for i in 0..slice.len() {} gibi bir kullanımdansa bu yöntem çok daha verimlidir çünkü Rust'ı obsesifçe her indeks operasyonunda bir ton şeyi kontrol etmeye yönlendirmemiş oluyoruz.

Bir de bir aralığın hepsini hızlıca toplamanın bir başka örneğine bakalım. Daha önce bir döngü ve mut değişkenini kullanıyordum. Burada ise toplamanın "idiomatic" ve profesyonelce bir yolu var:

// sum1.rs
fn main() {
    let sum: i32  = (0..5).sum();
    println!("sum was {}", sum);

    let sum: i64 = [10, 20, 30].iter().sum();
    println!("sum was {}", sum);
}

Rust'ta tiplerin önceden bildirilmiş olması gereken durumlardan birisi olduğuna dikkat edin, aksi taktirde Rust tam olarak ne yapması gerektiğini bilemeyecektir. Burada farklı tam sayı tipleriyle çalışmaktayız ki bu sorun teşkil etmez. (Aynı zamanda, isim sıkıntısı çektiğiniz zamanlarda aynı ismi tekrar kullanmanız da sorun teşkil etmez.)

Bu bilgiyle birlikte, dilim metotları gözünüze biraz daha anlamlı gelecektir. (Belgelendirme hakkında bir bilgi; her belgenin sağ tarafında '[-]' işareti bulunur ve bu butona tıklayınca metot listesini kapatabilirsiniz. İlginizi çeken şeylerin detaylarını da genişletebilirsiniz. Şimdilik biraz tuhaf görünüyor, görmezden geliverin.)

windows metotu size dilimlerin döngüleyicisini verir - birbiriyle örtüşen pencereler olarak değerler (Ç.N: Burada ne demek istediği benim için bile bir bilinmez.)


// slice4.rs
fn main() {
    let ints = [1, 2, 3, 4, 5];
    let slice = &ints;

    for s in slice.windows(2) {
        println!("window {:?}", s);
    }
}
// window [1, 2]
// window [2, 3]
// window [3, 4]
// window [4, 5]

Chunks da benzer işlevi yapabilir.


#![allow(unused)]
fn main() {
 for s in slice.chunks(2) {
        println!("chunks {:?}", s);
    }
// chunks [1, 2]
// chunks [3, 4]
// chunks [5]
}

Vektörler Üzerine Biraz Daha Konuşalım

Bir vektör kurmak için vec! isminde oldukça kullanışlı bir makromuz var. Bununla birlikte vektörün sonundaki verileri pop ile silebiliriz ve vektörü bir başka vektörle genişletebiliriz (extend).

// vec3.rs
fn main() {
    let mut v1 = vec![10, 20, 30, 40];
    v1.pop();

    let mut v2 = Vec::new();
    v2.push(10);
    v2.push(20);
    v2.push(30);

    assert_eq!(v1, v2);

    v2.extend(0..2);
    assert_eq!(v2, &[10, 20, 30, 0, 1]);
}

Vektörler birbirleriyle kıyaslanacağı zaman dilim olarak karşılaştırılır.

Vektörün belirli noktalarına insert kullanarak verileri yerleştirebilirsiniz, remove ile de silebilirsiniz. Bu vektörün sonuna veri eklemekten (push) ya da veri çıkartmaktan (pop) daha verimsizdir çünkü veriler yeni yer yaratmak için taşınır, büyük boyutlu vektörlerle çalışırken bu duruma dikkat etmeniz gerekir.

Vektörlerin bir büyüklüğü ve kapasitesi vardır. Eğer bir vektörü clear ile temizlerseniz, büyüklüğü sıfır olur ama eski kapasitesini korur. Bu durumda push vs ile yeni veriler eklerken yeniden bellek alanı tahsis edilmesi yalnızca eski kapasitenin aşımıyla gerekli olur.

Vektörler sıralanabilir ve içinde tekrarlayan veriler temizlenebilir - bu işlemler vektörü değiştirir. (Eğer önce vektörü kopyalamak isterseniz, clone kullanın.)

// vec4.rs
fn main() {
    let mut v1 = vec![1, 10, 5, 1, 2, 11, 2, 40];
    v1.sort();
    v1.dedup();
    assert_eq!(v1, &[1, 2, 5, 10, 11, 40]);
}

Karakter Dizileri (String)

Rust'taki karakter dizileri diğer dillerden biraz daha gelişkindir. String tipi, Vec gibi, belleği dinamik olarak tahsis eder ve yeniden boyutlandırılabilir. (C++'ın std::string tipine çok benzer ancak Java'nın ve Python'nun değişemez karakter dizileri gibi değildir.) Ancak bir program, pek çok string kalıbı (string literal) de barındırabilir ("merhaba" gibi) ve bir sistem programlama dili bunları çıktı dosyasının içinde barındırabilmelidir. Gömülü mikroçiplerde bunun anlamı, bunları pahalı RAM yerine ucuz ROM'a yerleştirmektir. (Düşük seviyeli cihazlar için, RAM'ın pahalılığı aynı zamanda enerji üretimi pahalılığıdır.) Bir sistem programlama dilinde iki tür karakter dizisi bulunmalıdır, statik ya da bellekte yeri tahsis edilmiş.

Yani "merhaba" bir String değildir. Onun tipi &str'dir. ("Karakter dizisi dilimi String Slice olarak okunur.") Bu ayrım, C++'daki const char* ve std::string arasındaki fark gibidir ancak &str biraz daha kullanışlıdır. Doğrusu, &str ve String ilişki &[T] ile Vec<T> arasındaki ilişkiye çok benzer.

// string1.rs
fn dump(s: &str) {
    println!("str '{}'", s);
}

fn main() {
    let text = "hello dolly";  // the string slice
    let s = text.to_string();  // it's now an allocated string

    dump(text);
    dump(&s);
}

Tekrar edelim, ödünç alma operatörü tıpkı Vec<T>'yi &[T]'ye çevirmesi gibi String'i de &str'ye çevirir.

Aslında içten içe, String aslında bir Vec<u8>'dir ve &str de bir &[u8]'dir, ancak bu baytlar UTF-8'e kesinlikle uygun olmalıdır.

Vektör gibi, bir karakteri pushlayabilirsiniz veyahut sonundaki karakteri poplayabilirsiniz.

// string5.rs
fn main() {
    let mut s = String::new();
    // initially empty!
    s.push('H');
    s.push_str("ello");
    s.push(' ');
    s += "World!"; // short for `push_str`
    // remove the last char
    s.pop();

    assert_eq!(s, "Hello World");
}

Pek çok tipi String'e to_string diyerek çevirebilirsiniz. (Eğer onları "{}" ile ekranda gösterebiliyorsanız, çevrilebilirler.) format! makrosu da tıpkı println! gibi karmaşık karakter dizileri üretmek için kullanılabilir.

// string6.rs
fn array_to_str(arr: &[i32]) -> String {
    let mut res = '['.to_string();
    for v in arr {
        res += &v.to_string();
        res.push(',');
    }
    res.pop();
    res.push(']');
    res
}

fn main() {
    let arr = array_to_str(&[10, 20, 30]);
    let res = format!("hello {}", arr);

    assert_eq!(res, "hello [10,20,30]");
}

v.to_string()'in önündeki & operatörüne dikkat edin - operatör bir karakter dizesi dilimi üzerinde tanımlanmış, String'in kendisine değil, uyuşması için bazı detaylar eklememiz gerekiyor.

Dilimlerde kullanılan ifade şekli karakter dizilerinde de gösterilebilir:

// string2.rs
fn main() {
    let text = "static";
    let string = "dynamic".to_string();

    let text_s = &text[1..];
    let string_s = &string[2..4];

    println!("slices {:?} {:?}", text_s, string_s);
}
// slices "tatic" "na"

Ancak karakter dizilerini indeksleyemezsiniz. Çünkü onlar tek ve gerçek kodlama olan UTF-8'i kullanırlar ki bu kodlamada bazı "karakterler" sadece baytların sayısı olabilir.

// string3.rs
fn main() {
    let multilingual = "Hi! ¡Hola! привет!";
    for ch in multilingual.chars() {
        print!("'{}' ", ch);
    }
    println!("");
    println!("len {}", multilingual.len());
    println!("count {}", multilingual.chars().count());

    let maybe = multilingual.find('п');
    if maybe.is_some() {
        let hi = &multilingual[maybe.unwrap()..];
        println!("Russian hi {}", hi);
    }
}
// 'H' 'i' '!' ' ' '¡' 'H' 'o' 'l' 'a' '!' ' ' 'п' 'р' 'и' 'в' 'е' 'т' '!'
// len 25
// count 18
// Russian hi привет!

Şimdi şuna bakalım - 25 baytımız var ama sadece 18 kataktere sahibiz! Fakat, eğer find gibi bir metot kullanırsak (bulunması hâlinde) geçerli bir indeks elde alırsınız ve herhangi bir dilimleme doğru çalışacaktır.

(Rust'ın char tipi 4-baytlık Unicode karakteridir. Karakter dizileri ise charların dizisi değildir!)

Karakter dizelerini dilimlemek vektör dilimlemek gibi riskli bir iştir, çünkü bayt "aralıkları" kullanılır. Alttaki koşulda karakter dizesi iki bayttan oluşur, bunun ilk baytını almaya çalışmak bir Unicode hatasıdır. Bundan dolayı karakter dizisi metotlarından gelen uygun aralıkları kullanmaya dikkat edin.


#![allow(unused)]
fn main() {
    let s = "¡";
    println!("{}", &s[0..1]); <-- bad, first byte of a multibyte character
}

Karakter dizilerini parçalamak popüler ve faydalı bir meşgaledir. split_whitespace metotu bir döngüleyici döner ve bunun ne yapacağımızı biz belirleriz. Genelde bir karakter dizisini daha ufak karakter dizilerinin vektörünü kurmak için buna ihtiyaç duyarız.

collect ise çok geneldir ve neyi topladığımız (collect) hakkında bir ipucu ister - bundan dolayı açıkça tip belirtilir.


#![allow(unused)]
fn main() {
    let text = "the red fox and the lazy dog";
    let words: Vec<&str> = text.split_whitespace().collect();
    // ["the", "red", "fox", "and", "the", "lazy", "dog"]
}

Döngüleyicilerin extend metotuyla da aynı işi yapabilirdiniz.


#![allow(unused)]
fn main() {
    let mut words = Vec::new();
    words.extend(text.split_whitespace());
}

Pek çok dilde bunları yapmak için bellekte ayrıca alanı tahsis edilmiş karakter dizilerine ihtiyacımız olurdu, oysa burada sadece bir ödünç alma olayı var. Tek tahsis edilen alan, dilimleri bellekte tutacak alandır.

Şu şirin çift-satıra bir bakınız; karakterler üzerine bir döngüleyici kuruyoruz ve boşluk olmayan karakterleri alıyoruz. Hatırlatalım, collect ipucu ister. (Ve biz de karakterler vektörü istemiş olabiliriz)


#![allow(unused)]
fn main() {
	let stripped: String = text.chars()
        .filter(|ch| ! ch.is_whitespace()).collect();
    // theredfoxandthelazydog
}

filter metotu ise argüman olarak kapama (closure) alır, kapama dediğimiz de Rust'ın dilinde lambdalara veya anonim fonksiyonlara verdiğimiz isim. Argüman, işlevsel olarak çalışmayı bozmadığı için apaçık tip belirtme kuralını genişletebiliyoruz.

Tabii bunca şeyi apaçık bir döngü ile değişebilir bir vektöre karakter dizilerini iterek de yapabilirsiniz, ama şimdi yaptığımız daha kısa, daha okunaklı (alışınca tabii) ve denk bir hızda. Bir döngü kullanmak elbet ayıp değildir ama bu şekilde yazmanızı şiddetle tavsiye ederim.

Reklam Arası: Komut Satırından Argümanları Almak

Şimdiye kadar programlarımız neyin ne olduğundan habersiz bir şekilde kendi kendilerine yaşayıp gittiler. Artık onları gerçek dünya ile tanıştırmalıyız.

std::env::args ile komut satırındaki argümanlara ulaşabilirsiniz; size bütün argümanları, programın ismi de dahil olmak üzere, birer karakter dizisi olarak döner.

// args0.rs
fn main() {
    for arg in std::env::args() {
        println!("'{}'", arg);
    }
} 
src$ rustc args0.rs
src$ ./args0 42 'hello dolly' frodo
'./args0'
'42'
'hello dolly'
'frodo'

Bir Vec dönse daha iyi olmaz mıydı? Bunu collect ile bir vektöre çevirmek de pek zor değildir, hatta döngüleyicilerin skip metotu ile programın adını atlayabilirsiniz.


#![allow(unused)]
fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    if args.len() > 0 { // we have args!
        ...
    }
}

İşin en iyi tarafı, bütün dillerde bunu bu şekilde kullanıyor olmanız.

Biraz daha Rustça yaklaşım ise tek bir argümanı okumak. (Ve aynı zamanda bir tam sayı verisini okumak):

// args1.rs
use std::env;

fn main() {
    let first = env::args().nth(1).expect("please supply an argument");
    let n: i32 = first.parse().expect("not an integer!");
    // do your magic
}

nth(1) size bir döngüleyicinin ikinci verisini döner, expect ise unwrap gibi çalışır ancak bir de mesaj belirtmenize izin verir.

Bir karakter dizisini bir sayıya çevirmenin yolu gayet bariz, ancak dönüştürülecek tipi açıkça belirtmeniz gerekmekte - yoksa parse bunu nereden bilecek?

Örüntü Eşleştirme (Matching)

string3.rs dosyasındaki Rusça selamlamayı kullandığımız kodda aslında bu tarz durumları o şekilde çözmeyiz. Match ile deneyelim:


#![allow(unused)]
fn main() {
    match multilingual.find('п') {
        Some(idx) => {
            let hi = &multilingual[idx..];
            println!("Russian hi {}", hi);
        },
        None => println!("couldn't find the greeting, Товарищ")
    };
}

Match şu kızılderili okunu barındıran, virgüllerle ayrılmış pek çok örüntü tanımından oluşur. Bu ifade, çok rahat bir şekilde Option içerisinden ifadeyi ayıklayabilir ve idx'e bağlayabilir. Bütün koşulların muhakkak karşılanması gerektiği için None'u da ele alıyoruz.

Buna bir kere alışınca (yani, bir kaç kez yazınca) size ayrıca Option tutacak ek bir değişken gerektiren is_some yazmaktan çok daha rahat gelecek.

Ancak hatalarla ilgilenmiyorsanız, mahalleden if let'i çağırabiliriz:


#![allow(unused)]
fn main() {
    if let Some(idx) = multilingual.find('п') {
        println!("Russian hi {}", &multilingual[idx..]);
    }
}

Match C'deki switch gibi de çalışabilir ve diğer Rust kurucuları gibi veri de dönebilir:


#![allow(unused)]
fn main() {
    let text = match n {
        0 => "zero",
        1 => "one",
        2 => "two",
        _ => "many",
    };
}

_'ı C'deki default olarak düşünebilirsiniz - varsayılan değer ifadesidir kendileri. Eğer bunu belirtmezseniz rustc bunun bir hata olduğunu düşünür. (C++'da bekleyebileceğiniz ilgili koşullar hakkında pek çok şeyi belirtilen bir uyarıyı almak olur.)

Rust'ın match deyimleri aralıkları da eşleştirebilir. Bu aralıkların üç noktalı olduğuna dikkat edin, bunlar kapsayan aralıklardır, mesela ilk koşul "3" sayısıyla eşleşecektir.


#![allow(unused)]
fn main() {
    let text = match n {
        0...3 => "small",
        4...6 => "medium",
        _ => "large",
     };
}

Dosyaları Okumak

Bizim programlarımızı dünyaya açacak olan bir sonraki adımımız ise dosyaları okumaktır.

expect'in unwrap gibi çalıştığını ancak fazladan bir hata mesajı girmemize izin verdiğini aklınızda tutun. Şimdi birden çok hata hortlatacağız:

// file1.rs
use std::env;
use std::fs::File;
use std::io::Read;

fn main() {
    let first = env::args().nth(1).expect("please supply a filename");

    let mut file = File::open(&first).expect("can't open the file");

    let mut text = String::new();
    file.read_to_string(&mut text).expect("can't read the file");

    println!("file had {} bytes", text.len());

}
src$ file1 file1.rs
file had 366 bytes
src$ ./file1 frodo.txt
thread 'main' panicked at 'can't open the file: Error { repr: Os { code: 2, message: "No such file or directory" } }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.
src$ file1 file1
thread 'main' panicked at 'can't read the file: Error { repr: Custom(Custom { kind: InvalidData, error: StringError("stream did not contain valid UTF-8") }) }', ../src/libcore/result.rs:837
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Dosyanın var olmadığı veyahut okunmasına izin olmadığı durumlarda open hata dönebilir, read_to_string ise dosya içeriğinin UTF-8 olmaması durumunda hata döner. (Tabii, bu koşulda yerine read_to_end kullanıp içeriği bayt vektörlerine koymak da bir seçenek.) Çok da büyük olmayan dosyaları tek hamlede okumak daha faydalı ve basittir.

Eğer diğer dillerde dosya işleme nasıl olur bir fikriniz varsa dosyanın ne zaman kapatılması gerektiğini düşünüyor olabilirsiniz. Eğer dosyaya bir şeyler yazdırsaydık kapatmamak veri kaybına sebebiyet verebilirdi ancak burada dosya kendiliğinden kapatılıyor ve fonksiyon sona erdiği zaman da file değişkeni düşürülüyor.

Bu "hata hortlatma işi"ne biraz fazla alıştık galiba. Bütün programı böyle çökertebilen bir kodu kendi fonksiyonlarınıza yerleştirmek istemezsiniz. O zaman File::open'ın ne döndüğüne bakalım. Eğer Option bir şeyin varlığını ya da yokluğunu işaret ediyorsa Result da bir şeyin olup olmadığını gösterir. İkisi de unwrap'i bilir (ve amcaoğlu expecti de) ancak biraz farklıdırlar. Result, Ok ve Err için iki farklı tür parametre içerir. Result "paketi" iki farklı kompartmana sahiptir, birisi Ok ve diğeri de Err.

fn good_or_bad(good: bool) -> Result<i32,String> {
    if good {
        Ok(42)
    } else {
        Err("bad".to_string())
    }
}

fn main() {
    println!("{:?}",good_or_bad(true));
    //Ok(42)
    println!("{:?}",good_or_bad(false));
    //Err("bad")

    match good_or_bad(true) {
        Ok(n) => println!("Cool, I got {}",n),
        Err(e) => println!("Huh, I just got {}",e)
    }
    // Cool, I got 42

}

(Aslında "hata (error)" için seçtiğimiz tip biraz gereksiz - pek çok insan Rust'ın hata tiplerine alışana kadar karakter dizelerini tercih eder.) Bu, bir veriyi ya da başka bir veriyi döndürmenin gayet uygun bir yoludur.

Dosya okumamızın bu şekli çökmez. Result döner ve onu çağırana gelen verinin nasıl işlenmesi gerektiğini seçtirir.

// file2.rs
use std::env;
use std::fs::File;
use std::io::Read;
use std::io;

fn read_to_string(filename: &str) -> Result<String,io::Error> {
    let mut file = match File::open(&filename) {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    let mut text = String::new();
    match file.read_to_string(&mut text) {
        Ok(_) => Ok(text),
        Err(e) => Err(e),
    }
}

fn main() {
    let file = env::args().nth(1).expect("please supply a filename");

    let text = read_to_string(&file).expect("bad file man!");

    println!("file had {} bytes", text.len());
}

Birinci eşleşme Ok içindeki veriyi güvenli bir şekilde dışarı çıkartır ve eşleşmenin değeri yapar. Eğer bir Err verisi ise, hatayı döner ve onu tekrar Err içerisine paketler.

İkinci eşleşme ise Ok içerisine paketlenmiş bir karakter dizesi döner ya da hatayı tekrar eder. Ok içindeki esas veriye ihtiyacımız yok ondan dolayı _ ile yok sayıyoruz.

Bu biraz sıkıcı, yazdığımız kodu büyük kısmı hatayı işlemekten ibaret olunca "işin ruhunu" kaybediyoruz. Mesela Go'da bunu hissedersiniz, düzinesiyle erken dönen hataları kontrol etmeniz gerekir ya da sadece görmezden gelirsiniz. (Rust evreninde bu tuvalette ekmek çiğnemek kadar kötü bir şeydir.)

Neyse ki, bir kısayolumuz var.

std::io modülü io::Result<T> diye bir tipe sahiptir ki bu Result<T, io::Error> ile aynı şeydir ve daha kolay yazılabilir.


#![allow(unused)]
fn main() {
fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = File::open(&filename)?;
    let mut text = String::new();
    file.read_to_string(&mut text)?;
    Ok(text)
}
}

? operatörü File::open üzerinde denediğimiz eşleştirmelerle birebir aynı şeyi yapıyor; eğer sonuç hataysa hemen fonksiyonu döndürüyor. Değilse, Ok sonucunu dönüyor. Sonuç olarak hâlâ daha karakter dizisini paketlememiz gerekiyor.

2017 senesi Rust için iyi bir yıldı ve ? gibi karizmatik şeyler kararlı hâle geldi. Eski kodlarda try! diye bir makroyu görebilirsiniz:


#![allow(unused)]
fn main() {
fn read_to_string(filename: &str) -> io::Result<String> {
    let mut file = try!(File::open(&filename));
    let mut text = String::new();
    try!(file.read_to_string(&mut text));
    Ok(text)
}
}

Sonuç olarak, tek tek hataları bildirmeden güvenli Rust kodu yazmak düşündüğünüz kadar çirkin değil.

Yapılar, Numaralandırmalar ve Eşleştirme

Rust Lekta Movik Movik

Fazla ileri gitmiyor muyuz? Mesela kaçırdığımız bazı şeyler var:

// move1.rs
fn main() {
    let s1 = "hello dolly".to_string();
    let s2 = s1;
    println!("s1 {}", s1);
}

Kod çalışınca da şu hatayı alırız:

error[E0382]: use of moved value: `s1`
 --> move1.rs:5:22
  |
4 |     let s2 = s1;
  |         -- value moved here
5 |     println!("s1 {}", s1);
  |                      ^^ value used here after move
  |
  = note: move occurs because `s1` has type `std::string::String`,
  which does not implement the `Copy` trait

Rust diğer dillerden biraz daha farklı davranır. Bütün değişkenleri birer referans olduğu dillerde (Java ve Python gibi) s2 s1'in karakter dizesi objesine bir başka referans olur. C++'da ise s1 bir veridir ve s2'ye kopyalanır. Ancak Rust veriyi taşır (move), karakter dizelerini ise kopyalanabilir bir tür olarak da görmez. ("does not implement the Copy trait" - "Kopyala özelliğini barındırmıyor")

Böyle bir şeyi sayılar gibi "ilkel (primitive)" tiplerde görmeyiz çünkü onlar sadece veridir; kopyalanabilmelerine izin vardır çünkü kopyalaması ucuzdur. Ama String "Hello Dolly" için bellekte yer tahsis eder ve kopyalama daha fazla belleğin tahsis edilmesini ve karakterlerin tek tek kopyalanmasını içerir. Rust'ın bunu sessiz sedasız yapmasını bekleyemezsiniz.

String'in bütün "Moby Dick"i barındırdığını düşünün. Bu karmaşık bir yapı (struct) olmazdı; sadece yazının bulunduğu bellek bölgesini tutan adresi, büyüklüğünü ve ne kadar bellekte alan tahsis edildiğini barındırırdı. Kopyalamak epey bir yük olurdu çünkü bellek heap bölgesinde tahsis edilmişti ve kopyalamanın kendisi de bellekte alan tahsis etmeyi gerektirirdi.

    String
    | addr | ---------> Call me Ishmael.....
    | size |                    |
    | cap  |                    |
                                |
    &str                        |
    | addr | -------------------|
    | size |

    f64
    | 8 bytes |

İkinci veri ise karakter dizisi dilimidir (&str) ve String ile aynı bellek alanına yönlendirir, büyüklüğü ile birlikte. Kopyalaması çok basit!

Üçüncü verimiz ise f64 - sadece 8 bayt tutuyor. Herhangi bir bellek alanına yönlendirilmiyor, yani kopyalaması onu taşımak kadar basit.

Copy verileri bellekteki karşılıklarıyla tanımlanır ve Rust kopyaladığı zaman bu baytları sadece başka bir yere kopyalar. Buna benzer olarak Copy olmayan bir veri ise sadece taşınır. C++'ın aksine kopyalama ve taşımada herhangi bir karmaşa yoktur.

Aynı şeyi bir fonksiyon çağrısı olarak yazmak da aynı soruna sebep olur:

// move2.rs

fn dump(s: String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(s1);
    println!("s1 {}", s1); // <---error: 'value used here after move'
}

Şimdi bir tercih yapmanız gerekiyor. Ya String'i bir referans olarak kullanacaksınız ya da açık açık clone metotu ile onu kopyalayacaksınız. Genelde ilk olan daha iyi bir seçenektir.

fn dump(s: &String) {
    println!("{}", s);
}

fn main() {
    let s1 = "hello dolly".to_string();
    dump(&s1);
    println!("s1 {}", s1);
}

Artık hatadan çok uzaktayız. Ancak String referansını çok nadir görürsünüz, çünkü bir karakter dizisi kalıbını bu şekilde kullanmak gerçekten çirkin ve bu yolla geçici bir String oluşturmak zorunda kalırsınız.


#![allow(unused)]
fn main() {
    dump(&"hello world".to_string());
}

Onun yerine en iyi yol şudur:


#![allow(unused)]
fn main() {
fn dump(s: &str) {
    println!("{}", s);
}
}

Ve böylece dump(&s1) ve dump("hello world") kullanımlarının ikisi de geçerli olacaktır. (Burada Rust'ın Deref zorlaması işin içine girer ve &String'i &str yapar.)

Sonuç olarak, Copy olmayan bir değerin değişkene atanması bir konumdan öbürüne taşınmasıdır. Eğer bu olmasaydı Rust gizlice kopyalamak zorunda kalırdı ve bellek tahsislerini açıkça yapma sözünü gerçekleştiremezdi.

Değişkenlerin Kapsamları

Birinci kural, verileri kopyalamak yerine orijinal veriye referans göstermektir - yani "ödünç almak."

Ancak bir referans sahibinden daha uzun asla yaşayamaz!

Öncelikle Rust blok kapsamlı bir dildir. Değişkenler kendi blokları kadar yaşar:


#![allow(unused)]
fn main() {
{
    let a = 10;
    let b = "hello";
    {
        let c = "hello".to_string();
        // a,b and c are visible
    }
    // the string c is dropped
    // a,b are visible
    for i in 0..a {
        let b = &b[1..];
        // original b is no longer visible - it is shadowed.
    }
    // the slice b is dropped
    // i is _not_ visible!
}
}

(i gibi) Döngü değişkenleri biraz farklıdır, onlar sadece döngülerinin blokları için geçerlidir. Aynı isimle yeni bir değişken oluşturmak ("gölgelemek/shadowing") bir hata değildir ama kafa karıştırıcı olabilir.

Bir değişken "kapsam dışına çıkınca" düşürülür (dropped). Kullanılan her bir bellek tanesi geri dönüştürülür ve sistemden alınan kaynaklar iade edilir - örneğin, File'ı düşürmek onu kapatır. Bu iyi bir şey. Kullanılmayan kaynaklar ihtiyaç olmayınca hemen geri teslim edilir.

(Rust'a özgü bir başka sorun da verinin taşınmış olmasına rağmen kapsam dahilinde görünmüş olmasıdır.)

Bu örnekte rs1 isminde bir referans hazırladık ve değerini sadece iç bloğun ömrü kadar uzun kalan tmp'ye ayarladık.

01 // ref1.rs
02 fn main() {
03    let s1 = "hello dolly".to_string();
04    let mut rs1 = &s1;
05    {
06        let tmp = "hello world".to_string();
07        rs1 = &tmp;
08    }
09    println!("ref {}", rs1);
10 }

s1'in verisini ödünç aldık ve sonra da tmp'i ödünç aldık. Ancak tmp, bloğun dışında yok!

error: `tmp` does not live long enough
  --> ref1.rs:8:5
   |
7  |         rs1 = &tmp;
   |                --- borrow occurs here
8  |     }
   |     ^ `tmp` dropped here while still borrowed
9  |     println!("ref {}", rs1);
10 | }
   | - borrowed value needs to live until here

Tmp nerede? Gitti, yok, öldü o artık: düşürüldü. Rust sizi burada C'nin "işaretçiler (dangling pointer)" belasından koruyor - çoktan yitip gitmiş bir veriye işaret eden referanslardan yani.

Demetler (Tuple)

Bir fonksiyondan çoklu veriler dönmeyi gerektiren zamanlar muhakkak gelecek. Demetler bunun için gayet uygun bir gözümdür.

// tuple1.rs

fn add_mul(x: f64, y: f64) -> (f64,f64) {
    (x + y, x * y)
}

fn main() {
    let t = add_mul(2.0,10.0);

    // can debug print
    println!("t {:?}", t);

    // can 'index' the values
    println!("add {} mul {}", t.0,t.1);

    // can _extract_ values
    let (add,mul) = t;
    println!("add {} mul {}", add,mul);
}
// t (12, 20)
// add 12 mul 20
// add 12 mul 20

Demetlerin dizilerden temel farkları, demetlerin farklı tipler barındırabilmesidir.


#![allow(unused)]
fn main() {
let tuple = ("hello", 5, 'c');

assert_eq!(tuple.0, "hello");
assert_eq!(tuple.1, 5);
assert_eq!(tuple.2, 'c');
}

Bazen Iterator metotlarından karşınıza fırlarlar. enumerate tıpkı Python'daki aynı isimli oluşturucu gibi çalışır:


#![allow(unused)]
fn main() {
    for t in ["zero","one","two"].iter().enumerate() {
        print!(" {} {};",t.0,t.1);
    }
    //  0 zero; 1 one; 2 two;
}

zip ise iki döngüleyiciyi birbiriyle eşleştirir ve bir demet içerisinde veri dönen tek bir döngüleyici olarak birleştirir.


#![allow(unused)]
fn main() {
    let names = ["ten","hundred","thousand"];
    let nums = [10,100,1000];
    for p in names.iter().zip(nums.iter()) {
        print!(" {} {};", p.0,p.1);
    }
    //  ten 10; hundred 100; thousand 1000;
}

Yapılar (Struct)

Demetler fena şeyler değiller ancak t.1 gibi bir anlam içermeyen parçalarını incelerken biraz kafa karıştırıcı olabilir.

Rust yapıları ise isimli alanlar (field) barındırır:

// struct1.rs

struct Person {
    first_name: String,
    last_name: String
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Smith".to_string()
    };
    println!("person {} {}", p.first_name,p.last_name);
}

Sizin bunu fark etmemenize rağmen yapıların verileri bellekte yanyana dururlar çünkü derleyici belleği verimliliğe göre düzenler, büyüklüğüne göre değil ve arada bazı boşluklar olabilir.

Bu yapıyı ilklemek (initalize) biraz garip görünebilir, bundan dolayı Person yapısını oluşturmayı bir fonksiyon içerisine taşıyorum. Bu fonksiyon bir impl bloğunun içerisine taşınarak Person'a ait bir ilişkili fonksiyona (associated function) dönüştürülebilir.

// struct2.rs

struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

}

fn main() {
    let p = Person::new("John","Smith");
    println!("person {} {}", p.first_name,p.last_name);
}

new ile ilişkili özel bir şey yok. C++ tarzı :: notasyonu ile bu fonksiyona ulaşabiliyoruz.

Bir de argüman olarak kendisini referans alan (reference self) Person metotunu hazırlayalım.


#![allow(unused)]
fn main() {
impl Person {
    ...

    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }

}
...
    println!("fullname {}", p.full_name());
// fullname John Smith
}

self, bir referans olarak açıkça belirtildi. (&self'i self: &Person'un kısaltması olarak düşünebilirsiniz.)

Self kelimesi struct tipine atıfta bulunur - Person yerine Self yazdığınızı düşünebilirsiniz:


#![allow(unused)]
fn main() {
    fn copy(&self) -> Self {
        Self::new(&self.first_name,&self.last_name)
    }
}

Metotlar veri düzenlemek için kendilerini mutable self olarak argüman alırlar.


#![allow(unused)]
fn main() {
    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }
}

Ve sadece self kullanıldığında veri taşınacaktır:


#![allow(unused)]
fn main() {
    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
}

(Bunu bir de &self ile deneyin ve yapıların (struct) kendi verileri konusunda ne kadar inatçı olduğunu bir de siz görün!)

v.to_tuple() çağrıldığı zaman v'nin taşındığını ve kullanılamaz hâle geldiğini göreceksiniz.

Özetlersek:

  • self kullanılmazsa: fonksiyonları bu şekilde bağlayabilirsiniz, new "oluşturucusu" gibi .
  • &self ile: Yapının verilerini kullanabilir ancak değiştiremezsiniz.
  • &mut self ile: Yapının verilerini düzenleyebilirsiniz.
  • self ile: Yapıyı yok edersiniz, yani içindeki verileri taşırsınız.

Eğer Person'u veri ayıklama şeklinde ekrana yazdırırsanız, bilgilendirici bir hata alırsınız:

error[E0277]: the trait bound `Person: std::fmt::Debug` is not satisfied
  --> struct2.rs:23:21
   |
23 |     println!("{:?}", p);
   |                     ^ the trait `std::fmt::Debug` is not implemented for `Person`
   |
   = note: `Person` cannot be formatted using `:?`; if it is defined in your crate,
    add `#[derive(Debug)]` or manually implement it
   = note: required by `std::fmt::Debug::fmt`

Derleyici bazı tavsiyesine uyuyoruz ve Person'un tanımı üstüne #[derive(Debug)] ekliyoruz, böylece işe yarar bir çıktımız oluyor:

Person { first_name: "John", last_name: "Smith" }

Bu direktif, derleyicinin faydalı bir özellik olan Debug'u eklemesine yarıyor ki bu da sizin kendi yapılarınızla (struct) ekrana yazdırarak pratik yapmanıza yardımcı olur. (Ya da format! ile yazdırabilirsiniz). (Bunu varsayılan olarak gerçekleştirmek Rust'ın tarzı değil doğrusu.)

İşte minik programımızın son hâli:

// struct4.rs
use std::fmt;

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

    fn full_name(&self) -> String {
        format!("{} {}",self.first_name, self.last_name)
    }

    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
}

fn main() {
    let mut p = Person::new("John","Smith");

    println!("{:?}", p);

    p.set_first_name("Jane");

    println!("{:?}", p);

    println!("{:?}", p.to_tuple());
    // p has now moved.

}
// Person { first_name: "John", last_name: "Smith" }
// Person { first_name: "Jane", last_name: "Smith" }
// ("Jane", "Smith")

Yaşam Sürelerinin Yüreğimizi Dağlamaya Başladığı O An

Yapıların çoğu zaman veri taşır ancak bazen referans taşıması da gerekebilir. Mesela düşünelim ki yapımıza karakter dizisi değeri yerine bir karakter dizisi dilimi ekleyeceğiz.

// life1.rs

#[derive(Debug)]
struct A {
    s: &str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
error[E0106]: missing lifetime specifier
 --> life1.rs:5:8
  |
5 |     s: &str
  |        ^ expected lifetime parameter

Buradaki sorunu anlayabilmek için problemi bir de Rust'ın gözünden görmeniz gerekmekte. Rust, bir referansın ömrünün ne kadar uzun süreceğini hesaplamadan o referansa izin vermeyecektir. Bütün referanslar bir veriyi önüç alır ve her verinin bir yaşam süresi vardır. Referansların yaşam süreleri o verinin yaşam süresinden uzun olamaz. Rust, referansın geçersiz olduğu bir koşulun oluşma ihtimaline izin vermeyecektir.

Şimdi, karakter dizisi diliminin referansı bir String değerini ya da "merhaba" gibi bir karakter dizisi kalıbını ödünç alır. Karakter dizesi kalıpları programın yaşamı boyunca yaşar ki buna "statik (static)" yaşam süresi deriz.

İşte şimdi tıkır tıkır çalışıyor - Rust'ın bir karakter dizisi kalıbının sürekli olarak var olacağını garanti etmiş olduk.

// life2.rs

#[derive(Debug)]
struct A {
    s: &'static str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
// A { s: "hello dammit" }

Tabii bu hâli de çok şık görünmüyor ama net olmak için bazı bedeller ödemek gerekir.

Bunu bir fonksiyondan karakter dizisi dilimi döndürmek için de kullanabiliriz.


#![allow(unused)]
fn main() {
fn how(i: u32) -> &'static str {
    match i {
    0 => "none",
    1 => "one",
    _ => "many"
    }
}
}

Kısıtlayıcı olmasına karşın statik karakter dizilerinin bu tarz durumları için işe yarar.

Buna karşın, biz bir referansın yaşam ömrünü en az yapının ömrü kadar uzun olarak da belirtebiliriz.

// life3.rs

#[derive(Debug)]
struct A<'a> {
    s: &'a str
}

fn main() {
    let s = "I'm a little string".to_string();
    let a = A { s: &s };

    println!("{:?}", a);
}

Yaşam ömürleri geleneksel olarak "a", "b" gibi harflerle belirtilir ancak siz dilerseniz "patlıcan" gibi kelimelerle de ifade edebilirsiniz.

Bu ekleme ile beraber, bizim A yapısı ile s karakter dizisi birbirine sıkı sıkıya bağlanmıştır: a, sten ödünç alır ve o olmadan yaşayamaz.

Bu tanımla birlikte şu şekilde A dönen bir fonksiyon yazabiliriz.


#![allow(unused)]
fn main() {
fn makes_a() -> A {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
}

Ancak bu sefer de A'nın açıkça yaşam süresinin belirtilmesine ihtiyaç vardır - "expected lifetime parameter" (beklenilen yaşam süresi parametresi)

  = help: this function's return type contains a borrowed value,
   but there is no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

rustc'nin verdiği tavsiyeye uyalım:


#![allow(unused)]
fn main() {
fn makes_a() -> A<'static> {
    let string = "I'm a little string".to_string();
    A { s: &string }
}
}

Ve hatamız:

8 |      A { s: &string }
  |              ^^^^^^ does not live long enough
9 | }
  | - borrowed value only lives until here

Bunu güvenli bir şekilde yapmanın bir yolu yok, çünkü fonksiyon sona verdiği zaman string düşecek ve stringe yapılan referanslar kendinden daha uzun süre yaşayamaz.

Bazen, bir yapının değer ve o değeri içeren bir referans taşıması iyi bir fikirmiş gibi görünebilir. Ama bu çok basit bir şekilde imkansızdır çünkü yapılar taşınabilir olmalıdır, ve her türlü taşınma referansı geçersiz kılacaktır. Üstelik bunu yapmanın bir gereği de yok - mesela yapınızın karakter dizisi alanı varsa ve bunun dilimlerini sunmaya ihtiyacınız varsa, indeks numaralarını tutabilir ve bir metot içerisinde gerçek dilimleri dönebilirsiniz.

Özellikler (Trait)

Rust'ta struct'ın sınıf (class) olmadığına dikkat edin. class kelimesinin anlamı diğer dillerde içi öylesine doldurulmuştur ki size nasıl düşüneceğinizi dikte eder hâle gelmiştir.

Şimdi şunlara dikkat edin: Rust'ta yapılar birbirini miras (inherit) alamaz; hepsi özgün tiplerdir. Alt-tip diye bir şey yok, o tarz şeyler sadece bir saçmalıktan ibaret.

Peki ya tipler arasındaki ilişkiler nasıl kurulur?

rustc bazen "implementing X trait (X özelliğini uygulamak)" diye gevezelik eder ve şimdi özellikler (tipler) hakkında konuşmanın tam zamanı.

Aşağıdaki bir özellik tanımlamanın ve belirli tiplere nasıl uygulandığının örneğini görüyorsunuz.

// trait1.rs

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

fn main() {
    let answer = 42;
    let maybe_pi = 3.14;
    let s1 = answer.show();
    let s2 = maybe_pi.show();
    println!("show {}", s1);
    println!("show {}", s2);
}
// show four-byte signed 42
// show eight-byte float 3.14

Şahane; i32 ve f64 içerisine yeni bir metot ekledik.

Rust ile haşır neşir oldum diyebilmek için standart kütüphanedeki basit özellikleri de bilmeniz gerekir. (Ki genelde bir arada bulunurlar.)

Debug epey yaygındır. Person üzerinde #[derive(Debug)] ile uyguladık, ancak isteseydik tam ismi görüntüleyecek şekilde de uygulayabilirdik.


#![allow(unused)]
fn main() {
use std::fmt;

impl fmt::Debug for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.full_name())
    }
}
...
    println!("{:?}", p);
    // John Smith
}

write! da epey kullanışlı bir makrodur - burada f Write özelliğini barındıran her şeyi temsil ediyor. (Mesela bu bir File olabilir - ya da sadece bir String)

Display ise "{}" ile yazdırılabilen verileri kontrol ve tıpkı Debug gibi uygulanır. Ve faydalı bir yan etki olarak, ToString Display'e sahip olan her türlü tipe uygulanır. Mesela DisplayPerson için uygularsak p.to_string() de çalışır hâle gelir.

Clone ise clone metotunu tanımlar ve sadece #[derive(Clone)] ile tanımlanabilir - eğer bütün alanların (fields) tipleri Clone'a sahipse. (Ç.N: Clone - İngilizce Klonlamak)

Örnek: Noktalı sayı aralıklarının döngüleyicisi

Daha önce aralıklarla (range, 0..n) karşılaştık ancak noktalı sayı kabul etmiyorlar. (Şansınızı zorlayabilirsiniz ancak pek de numarası olmayan 1.0'da takılıp kalırsınız.)

Bir döngüleyici (iterator) için yaptığımız gayriresmi tanımı hatırlayın; Some veya None dönebilen bir next metotuna sahip yapı. Bu süreçte, döngüleyicinin kendisi düzenlenir ve döngülemenin durumu hakkında bilgi tutar. (Sonraki indeks gibi) Döngülenen verinin içeriği genellikle değişmez. (Ancak Vec::drain gibi kendi verisini düzenleyen enteresan bir döngüleyiciyi de inceleyeceğiz.)

Ve şimdi de resmi tanımı görelim: "Iterator" özelliği


#![allow(unused)]
fn main() {
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    ...
}
}

Iterator için ilişkili tipi (associated type) de tanımış olduk. Bu özelliğin (trait) çalışması için bir tipe ihtiyaç vardır ve dönüş tipini de belirtmeniz gerekmektedir. next metotu belirli bir tip belirtilmeden çalışabilir, sadece Self üzerinden Item'e atışta bulunulması yeterlidir.

f64 tipi için uygulanmış bir Iterator, Iterator<Item=f64> ile belirtilir ki bunu "f64 tipi ile ilişkilendirilmiş bir döngüleyici" olarak okuyabilirsiniz.

... ile gösterilen kısım Iteratorün tedarik ettiği metotlardır. Sadece Item ve next'i belirttikten sonra pek çok metot da sizin için sunulacaktır.

// trait3.rs

struct FRange {
    val: f64,
    end: f64,
    incr: f64
}

fn range(x1: f64, x2: f64, skip: f64) -> FRange {
    FRange {val: x1, end: x2, incr: skip}
}

impl Iterator for FRange {
    type Item = f64;

    fn next(&mut self) -> Option<Self::Item> {
        let res = self.val;
        if res >= self.end {
            None
        } else {
            self.val += self.incr;
            Some(res)
        }
    }
}


fn main() {
    for x in range(0.0, 1.0, 0.1) {
        println!("{} ", x);
    }
}

Ve şöyle biçimsiz bir görüntüyü elde etmiş oluyoruz:

0
0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999

0.1 tam olarak noktalı sayı olarak gösterilemediğinden böyle tuhaf şeyler yaşıyoruz, minik bir formatlama yardımı ile bundan kurtulabiliriz. println! kısımını şöyle düzeltelim:


#![allow(unused)]
fn main() {
println!("{:.1} ", x);
}

Ve daha temiz bir çıktımız olmuş oluyor. (Bu formatlama "noktadan sonra bir rakam" anlamına geliyor.)

Şimdi bütün döngüleyici metotlarını kullanabiliriz, hadi bütün verileri bir vektörde toplayalım, daha da coşmak için bunu map ile yapalım:


#![allow(unused)]
fn main() {
    let v: Vec<f64> = range(0.0, 1.0, 0.1).map(|x| x.sin()).collect();
}

Genellenen Fonksiyonlar

Diyelim ki Debug özelliiğine sahip herhangi bir tipi argüman olarak alan bir fonksiyon yazacağız. Burada jenerik fonksiyon kullanmamızın bir örneğini görüyorsunuz, herhangi bir verinin referansını argüman olarak alabilir. T, tip parametresi oluyor ki fonksiyon ismi yazıldıktan hemen sonra tanımlandı:


#![allow(unused)]
fn main() {
fn dump<T> (value: &T) {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
}

Ancak, Rust kelimenin tam anlamıyla T tipi hakkında hiçbir şey bilmiyor.

error[E0277]: the trait bound `T: std::fmt::Debug` is not satisfied
...
   = help: the trait `std::fmt::Debug` is not implemented for `T`
   = help: consider adding a `where T: std::fmt::Debug` bound

Bunun çalışması için, T'nin Debug içermesi gerektiğinden bahsetmeliyiz:


#![allow(unused)]
fn main() {
fn dump<T> (value: &T)
where T: std::fmt::Debug {
    println!("value is {:?}",value);
}

let n = 42;
dump(&n);
// value is 42
}

Rust'ın genellenen fonksiyonlarının tipe özellikleri sağlaması (trait bounds) gerekir - burada "T is any type that implements Debug" kısmını anlatıyoruz. (T, Debug'ı içeren herhangi bir tiptir) rustc epey yardımcı oluyor ve hangi tipin tam olarak belirtilmesi gerektiğini bize bildiriyor.

Şimdi Rust, T için tip bağlarını biliyor, artık derleyiciden mantıklı mesajlar alabiliriz.


#![allow(unused)]
fn main() {
struct Foo {
    name: String
}

let foo = Foo{name: "hello".to_string()};

dump(&foo)
}

Buradaki hata ise "the trait std::fmt::Debug is not implemented for Foo (std::fmt::Debug özelliği Foo için uygulanmadı)"

Fonksiyonlar dinamik dillerde aslında genellenir çünkü değerler beraberinde türlerini taşırlar ve tür denetimi çalışma zamanı denetlenir - ya da başarısız olur. Karmaşık programlarda daha derleme zamanında tiplerin kontrol edilmesini ciddi anlamda isteriz! Bu dillerdeki bir programcı, derleme hatalarını sakince incelemek yerine programın çalışma anındadaki sürprizleri incelemek zorundadır. Murphy kanununa göre sorunlar en uygunsuz, ters zamanda ortaya çıkmaya meyillidir.

Bir sayının karesini almak jeneriktir; tam sayılar, noktalı sayılar ve çarpım operatörünü içeren her türlü şeyin karesini x*x ile alabilirsiniz. Peki ya tip bağları?

// gen1.rs

fn sqr<T> (x: T) -> T {
    x * x
}

fn main() {
    let res = sqr(10.0);
    println!("res {}",res);
}

Sorun, Rust'ın T'nin çarpılabilir olduğunu bilmemesidir.

error[E0369]: binary operation `*` cannot be applied to type `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^
  |
note: an implementation of `std::ops::Mul` might be missing for `T`
 --> gen1.rs:4:5
  |
4 |     x * x
  |     ^

Derleyicinin tavsiyesine uyarak bu tipi * çarpım operatörünü barındıran ilgili özelliğe zorlamayı deneyelim.


#![allow(unused)]
fn main() {
fn sqr<T> (x: T) -> T
where T: std::ops::Mul {
    x * x
}
}

Yine de hâlen daha çalışmıyor:

error[E0308]: mismatched types
 --> gen2.rs:6:5
  |
6 |     x * x
  |     ^^^ expected type parameter, found associated type
  |
  = note: expected type `T`
  = note:    found type `<T as std::ops::Mul>::Output`

Bu tipi daha da kısıtlamayı deneyelim:


#![allow(unused)]
fn main() {
fn sqr<T> (x: T) -> T::Output
where T: std::ops::Mul + Copy {
    x * x
}
}

(Ancak) şimdi oldu! Derleyiciyi sakince dinlemek sizi esas noktaya yaklaştırır, ta ki temizce program derlenene dek.

Tabii bunu C++'da yapmak daha kolay.

template <typename T>
T sqr(x: T) {
    return x * x;
}

Ama (dürüst olmak gerekirse), C++ laz müteahhit mantığını benimsiyor. C++'ın şablon (template) hataları berbattır çünkü derleyicinin tek bildiği şey bazı metotların ya da operatörlerin tanımlanıp tanımlanmadığıdır. C++ komitesi bu sorunu biliyor ve konseptler üzerinde çalışıyorlar ki bunlar daha çok özelliklerle kısıtlanmış tip parametrelerine çok benziyorlar.

Genellenmiş fonksiyonlar başta biraz zorlayıcı gelebilir ancak net olmak, ne tür değerleri güvenle kullanabileceğinizi sadece tanıma bakarak kullanabileceğiniz anlamına geliyor.

Bu fonksiyonlar çok biçimlinin tersi olarak tek biçimli olarak bilinir. (ÇN: Tek biçimli - monomorfik, çok biçimli - polimorfik) Fonksiyonun gövdesi her bir tip için ayrı ayrı derleme yapar. Çok biçimli fonksiyonlarda ise makine eşlesen her tip için aynı kodu kullanır, dinamik olarak doğru metota yönlendirir (dispatch).

Tek biçimlilik hızlı kod üretir, tipler için özelleştirilmiştir ve satır içi çalışabilirler. sqr(x) görüldüğü anda hemen x*x ile değiştirirlir. Ancak bunun dezavantajı, büyük genellenmiş fonksiyonların her için çok fazla kod üretmesidir ki buna kod şişmesi (code bloat) denir. Her zaman bir takas vardır ve deneyimli bir kişi hangi iş için doğru aracı seçeceğini bilmelidir.

Basit Numaralandırmalar

Numaralandırmalar (Enums) birkaç verisi bulunan tiplerdir. Örneğin, bir yön dört farklı şekil alabilir:


#![allow(unused)]
fn main() {
enum Direction {
    Up,
    Down,
    Left,
    Right
}
...
    // `start` is type `Direction`
    let start = Direction::Left;
}

Çeşitli metotlar alabilirler, tıpkı yapılar gibi. Match ifadesi enum tiplerini kontrol etmenin en basit yoludur.


#![allow(unused)]
fn main() {
impl Direction {
    fn as_str(&self) -> &'static str {
        match *self { // *self has type Direction
            Direction::Up => "Up",
            Direction::Down => "Down",
            Direction::Left => "Left",
            Direction::Right => "Right"
        }
    }
}
}

Noktalama da önemlidir. self'ten önce * operatörünü kullandığımıza dikkat edin. Unutması kolaydır çünkü çoğu zaman Rust böyle düşünür. (self.first_name deriz, (*self).first_name değil.) Fakat eşleştirmenin biraz daha net olması gerekir. Olduğu gibi bırakmak buna kadar varan bir sürü çıktıya sebep olur:

   = note: expected type `&Direction`
   = note:    found type `Direction`

Çünkü self &Direction tipidir, bundan dolayı * ile deferans ederiz.

Yapılar gibi numaralandırmalar da özellikleri içerebilir, #[derive(Debug)] arkadaş da Direction'a eklenebilir.


#![allow(unused)]
fn main() {
        println!("start {:?}",start);
        // start Left
}

Yani as_str metotu aslında o kadar da gerekli değil, Debug ile isimleri her zaman alabiliriz. (Ancak as_str alan tahsis etmez, ki bu önemli olabilir.)

Ancak burada net bir sıralama aramamalısınız - numaralandırmalar tam sayı değeri barındırmaz.

(Ç.N: Numaralandırma olarak çevrilen enum sözcüğü gerçekten de C ve C++'da sayılandırma işlemi için kullanılır ancak Rust'ta böyle bir özellik yoktur. C++'daki karşılığı enum değil, enum class'tır.)

Şimdi her Direction değerinin ardılını gösteren bir metot yazdık. use içinde yıldız jokerini kullanmak metotun içeriğine bütün numaralandırma değerlerini sıraladığı için epey kullanışlıdır.


#![allow(unused)]
fn main() {
    fn next(&self) -> Direction {
        use Direction::*;
        match *self {
            Up => Right,
            Right => Down,
            Down => Left,
            Left => Up
        }
    }
    ...

    let mut d = start;
    for _ in 0..8 {
        println!("d {:?}", d);
        d = d.next();
    }
    // d Left
    // d Up
    // d Right
    // d Down
    // d Left
    // d Up
    // d Right
    // d Down
}

Bu şekilde istenen ve belirlenmiş düzende bütün yönleri sonsuza dek sıralamaya izin verir. Aslında bu oldukça basit bir durum makinesidir.

Numaralandırma verileri kıyaslanamaz:

assert_eq!(start, Direction::Left);

error[E0369]: binary operation `==` cannot be applied to type `Direction`
  --> enum1.rs:42:5
   |
42 |     assert_eq!(start, Direction::Left);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
note: an implementation of `std::cmp::PartialEq` might be missing for `Direction`
  --> enum1.rs:42:5

Çözüm, enum Direction tanınımının üstüne #[derive(Debug,PartialEq)] eklemektir.

Önemli bir nokta, Rust'ın kullanıcı tiplerinin bir eklenti ile birlikte gelmemesidir. Genel özellikleri (trait) ekleyerek onlara olağan davranışları verirsiniz. Bu yapılar için de geçerlidir - eğer bir yapıya PartialEq verirseniz akla yatkın bir şey belirlenecek, tüm alanların PartialEq'e sahip olduğunu düşünerek bir kıyas yapacaktır. Eğer alanlar buna sahip değilse, eşitliği tanımlananız gerekmektedir ki bunu açıkça tanımlamanıza izin vardır.

Rust'ta "C tarzı numaralandırmalar" da kullanılabilir.

// enum2.rs

enum Speed {
    Slow = 10,
    Medium = 20,
    Fast = 50
}

fn main() {
    let s = Speed::Slow;
    let speed = s as u32;
    println!("speed {}", speed);
}

İlklendiği zaman tam sayı değeri alırlar ve tip dönüşümleriyle tam sayıya dönüşebilirler.

Bunun için sadece ilk isme değer vermeniz yeterlidir, diğerleri de bir arttırarak onu takip edecektir:


#![allow(unused)]
fn main() {
enum Difficulty {
    Easy = 1,
    Medium,  // is 2
    Hard   // is 3
}
}

Tabii isim diyince anlamı tam oturmadı, tıpkı her şeye "şey" demek gibi. Esas kullanılması gereken terim varyanttır - Speedin varyantları Slow, Medium ve Fasttır.

Numaralandırmalar doğal bir sıralama da alabilir, ancak bunu kibarca istemelisiniz. enum Speed'in başına #[derive(PartialEq,PartialOrd)] ekledikten sonra Speed::Fast > Speed::Slow ve Speed::Medium != Speed::Slow gibi ifadeler kullanılabilir olur.

Numaralandırmalar Tam Teçhizatlıyken

Rust'ın numaralandırmaları tam anlamıyla kullanıldığı zaman C'deki birliklerin (union) steroidli hâline benzer, tıpkı Ferrari ile Fiat Uno gibi. Çeşitli tiplerden verileri bir araya güvenlice toplamanın zorluğunu düşünün.

// enum3.rs

#[derive(Debug)]
enum Value {
    Number(f64),
    Str(String),
    Bool(bool)
}

fn main() {
    use Value::*;
    let n = Number(2.3);
    let s = Str("hello".to_string());
    let b = Bool(true);

    println!("n {:?} s {:?} b {:?}", n,s,b);
}
// n Number(2.3) s Str("hello") b Bool(true)

Numaralandırma bu verilerden sadece birisini taşıyabilir, büyüklüğü bir varyantın en büyük değeri kadardır.

Şimdiye kadar bir süper araba etmese de numaralandırmaların kendilerini yazdırabilmeleri de güzel şey. Bunun yanında verilerinin ne tarz veriler olduğunu da biliyorlar ki bu match'ın süpergücüdür.


#![allow(unused)]
fn main() {
fn eat_and_dump(v: Value) {
    use Value::*;
    match v {
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
....
eat_and_dump(n);
eat_and_dump(s);
eat_and_dump(b);
//number is 2.3
//string is 'hello'
//boolean is true
}

(Result ve Option kardeşleri hatırladınız mı? Onlar da bir numaralandırma.)

eat_and_dump fonksiyonu hiç fena değil ancak veriyi bir referans olarak iletsek iyi olur çünkü şu an verinin yerini taşıyor ve onu "yiyor":


#![allow(unused)]
fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {  // type of *v is Value
        Number(n) => println!("number is {}", n),
        Str(s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}

error[E0507]: cannot move out of borrowed content
  --> enum3.rs:12:11
   |
12 |     match *v {
   |           ^^ cannot move out of borrowed content
13 |     Number(n) => println!("number is {}",n),
14 |     Str(s) => println!("string is '{}'",s),
   |         - hint: to prevent move, use `ref s` or `ref mut s`
}

Ödünç alınmış referanslarla yapamayacağınız bazı şeyler var. Rust, orijinal değerin içerisindeki karakter dizisini dışarı çıkartmanıza izin vermeyecektir. Number üzerinde sorun yok çünkü f64'ün kopyalanmasında bir sakınca yok ama String Copy'i içermez.

match'ın kesin tipler hakkında seçici olduğunu söyledim, ipucunu takip edelim ve sıkıntı çıkartmayacaktır, şimdi içerideki karakter dizisine bir referans ödünç alıyoruz.


#![allow(unused)]
fn main() {
fn dump(v: &Value) {
    use Value::*;
    match *v {
        Number(n) => println!("number is {}", n),
        Str(ref s) => println!("string is '{}'", s),
        Bool(b) => println!("boolean is {}", b)
    }
}
    ....

    dump(&s);
    // string is 'hello'
}

Devam etmeden önce, başarılı bir Rust derlemesinin mutluluğu ile dolup taşmışken, bir saniye bekleyelim. Rustc o kadar iyi ki sorunu tam olarak anlamadan onu çözmemizi sağlıyor.

Sorun, eşleştirmenin kesinliğinden ve ödünç kontrolünün kuralların çiğnenmemesinden kaynaklanıyor. Bu kurallardan birisi, sahipliği olan bir tipe dahil olan veriyi zart diye çekemiyor olmamızdan geliyor. Biraz C++ bilmek burada kafa karıştırabilir çünkü akla yatkın olsa bile C++ problemin yolunu kopyalayacaktır. Bir vektörden karakter dizesi alırken de aynı hatayı alabilirsiniz, mesela *v.get(0).unwrap ile deneyin. (* kullanmanızın sebebi indekslemenin referans dönmesi) Buna yapmanıza izin vermecektir. (Bu tarz durumlarda Clone çok da kötü bir tercih olmayabilir.)

(Bu arada, v[0] karakter dizeleri gibi kopyalanamaz verilerde tam olarak bundan dolayı çalışmayacaktır. &v[0] ile ödünç almanız ya da v[0].clone() kullanmanız gerekmektedir.)

match kullanırken Str(s: String) => yerine Str(s) yazıldığını görebilirsiniz. Yeni bir yerel değişken yaratılır. (bazen bağlama (binding) olarak anılır) Çoğu zaman tatmin edilen tip uyar, mesela veriyi alıp onun içinden çıkartırken. Ancak burada s: &String yazmaya ihtiyacımız oldu ve ref ile sadece String'i ödünç almak istediğimizi bildirmiş olduk.

Burada da bir karakter dizisini dışarı çıkartıyoruz ve değerin daha sonra ne olacağını umursamıyoruz. _ geri kalan her şeyle eşleşecektir.


#![allow(unused)]
fn main() {
impl Value {
    fn to_str(self) -> Option<String> {
        match self {
        Value::Str(s) => Some(s),
        _ => None
        }
    }
}
    ...
    println!("s? {:?}", s.to_str());
    // s? Some("hello")
    // println!("{:?}", s) // error! s has moved...
}

İsimlendirme önemlidir -, as_str olarak değil de to_str olarak tanımlamamıza dikkat edin. (Ç.N: To Str - Str'ye çevir, As Str - Str olarak) Bir karakter dizisini Option<&String> olarak dönen bir metot yazabilirsiniz. (Referansın da numaralandırma değeri ile aynı yaşam süresinde olmasına gerek vardır) Ancak onu to_str olarak isimlendirmemelisiniz.

to_str örneğimizi şöyle yazabilirsiniz - tamamen aynı işi yapar:


#![allow(unused)]
fn main() {
    fn to_str(self) -> Option<String> {
        if let Value::Str(s) = self {
            Some(s)
        } else {
            None
        }
    }
}

Eşleştirme Hakkında Daha Fazlası

"()" kullanarak bir demeti dışarı çıkartabileceğinizi hatırladınız mı?


#![allow(unused)]
fn main() {
    let t = (10,"hello".to_string());
    ...
    let (n,s) = t;
    // t has been moved. It is No More
    // n is i32, s is String
}

Bu parçalama işleminin özel bir durumudur; elimizdeki bazı veriler var ve (buradaki gibi) parçalara ayırmayı ya da verilerini ödünç almayı düşünebiliriz. Her iki durum da da bir bütünün parçalarına ulaşmaya çalışıyoruz.

Sözdizimi match'taki gibi kullanılabilir. Burada açıkça ödünç alınmış verileri ödünç alıyoruz.


#![allow(unused)]
fn main() {
    let (ref n,ref s) = t;
    // n and s are borrowed from t. It still lives!
    // n is &i32, s is &String
}

Yapıları parçalamak da pekâlâ mümkün:


#![allow(unused)]
fn main() {
    struct Point {
        x: f32,
        y: f32
    }

    let p = Point{x:1.0,y:2.0};
    ...
    let Point{x,y} = p;
    // p still lives, since x and y can and will be copied
    // both x and y are f32
}

match'ı yeni örüntülerle tekrar inceleyelim. İlk iki örüntü let parçalaması gibi çalışır - ilki ilk elemanı sıfır olan, ikinci indeksi karakter dizesi olan her türlü demetle eşleşir, ikincisi ise sadece (1, "hello") ile eşleşir. Son koşulda ise olarak, bir değişken herhangi bir şeyle eşleşir. Eğer match bir ifadeyi eşleştiriyorsa ancak bunu değişkene bağlamak istemiyorsanız bu epey kullanışlıdır. _ da bir değişken gibi çalışır ancak görmezden gelinir, bir match'ı bitirmenin yaygın bir yoludur.


#![allow(unused)]
fn main() {
fn match_tuple(t: (i32,String)) {
    let text = match t {
        (0, s) => format!("zero {}", s),
        (1, ref s) if s == "hello" => format!("hello one!"),
        tt => format!("no match {:?}", tt),
        // or say _ => format!("no match") if you're not interested in the value
     };
    println!("{}", text);
}
}

Peki neden sadece (1, "hello") kullanmıyoruz? Eşleştirme kesin olarak çalışır ve derleyici de bundan bahsedecektir:

  = note: expected type `std::string::String`
  = note:    found type `&'static str`

Neden ref s'e ihtiyacımız var? Bu biraz belirsiz bir durum (E00008 numaralı hataya bakın.) ve eğer bir koşula bağlayacaksanız bunu ödünç almanız gerekir, koşula bağlamanız farklı bir bağlamda gerçekleştiğinden bellekteki alanın taşınması gerekebilir. Bu, işin en civcivli olduğu yerlerden birisi.

Eğer tipimiz &str olsaydı bunu doğrudan eşleştirebilirdik:


#![allow(unused)]
fn main() {
    match (42,"answer") {
        (42,"answer") => println!("yes"),
        _ => println!("no")
    };
}

match için geçerli olan if let için de geçerlidir. Bu mesela güzel bir örnek, bir Some verimiz olduğu için içindeki veriyi çekebiliriz ve içinden sadece bir karakter dizisini çıkartabiliriz. İç içe geçmiş if let ifadelerine ihtiyacımız da yok üstelik. Burada _ kullanıyoruz çünkü demetin ilk parçası ilgimizi çekmiyor.


#![allow(unused)]
fn main() {
    let ot = Some((2,"hello".to_string());

    if let Some((_,ref s)) = ot {
        assert_eq!(s, "hello");
    }
    // we just borrowed the string, no 'destructive destructuring'
}

parse ile ilgili bir enteresan bir sorunumuz da var. (Ya da dönüş tipini bilmesi gereken fonksiyonlar için de bunu düşünebiliriz)


#![allow(unused)]
fn main() {
    if let Ok(n) = "42".parse() {
        ...
    }
}

n'in tipi nedir? Bir ipucu vermeniz gerekir, ne tür bir tam sayılı değer bu? Hatta bu tam sayı mıdır?


#![allow(unused)]
fn main() {
    if let Ok(n) = "42".parse::<i32>() {
        ...
    }
}

Bu rezil söz diziminin adı "turbofish operatörüdür".

Eğer Result dönen bir fonksiyonun içerisindeyseniz, soru işareti ile çok daha şık bir çözüm kullanabilirsiniz:


#![allow(unused)]
fn main() {
    let n: i32 = "42".parse()?;
}

Her neyse, herhangi bir parse hatası Result'ın hata tipine dönüştürülebilir bir tipe ihtiyaç duyar ki bunu sonra hata kontrolü kısmında ele alacağız.

Kapamalar (Closure)

Rust'ın gücünün büyük bir kısmı bu kapamalardan gelir. En basit hâliyle bir fonksiyonun kısa yoluna benzerler:


#![allow(unused)]
fn main() {
    let f = |x| x * x;

    let res = f(10);

    println!("res {}", res);
    // res 100
}

Burada açıkça belirtilmiş bir tip yoktur - bir "10" tam sayı kalıbının kullanılmasına kadar her şey tahmin edilmiştir.

Ancak f'i farklı farklı tipler için kullanırsak hata alırız - Rust f'in tam sayılarla çalışması gerektiğine karar vermişti.

    let res = f(10);

    let resf = f(1.2);
  |
8 |     let resf = f(1.2);
  |                  ^^^ expected integral variable, found floating-point variable
  |
  = note: expected type `{integer}`
  = note:    found type `{float}`

İlk kullanım x için argümanı belirlemişti. Aslında yaptığımız şey şudur:


#![allow(unused)]
fn main() {
    fn f (x: i32) -> i32 {
        x * x
    }
}

Ancak açıkça tiplerin yazılmaması dışında fonksiyonlar ve kapamaların bir farkı daha vardır. Doğru fonksiyonunu inceleyelim:


#![allow(unused)]
fn main() {
    let m = 2.0;
    let c = 1.0;

    let lin = |x| m*x + c;

    println!("res {} {}", lin(1.0), lin(2.0));
    // res 3 5
}

Bunu fn ile böyle yapamayız, kapsamının dışında kalan hiçbir şeyle fn ilgilenmez. Buradaki kapama, m ve c'yi kendi kapsamı içerisine ödünç aldı.

Peki ya lin'in tipi nedir? Ancak rustc bilebilir. Aslında görünenin altında kapama, çağrılabilir bir (çağırma operatörünü içeren bir) yapıdır (struct). Şu şekilde yazılmış gibi davranır:


#![allow(unused)]
fn main() {
struct MyAnonymousClosure1<'a> {
    m: &'a f64,
    c: &'a f64
}

impl <'a>MyAnonymousClosure1<'a> {
    fn call(&self, x: f64) -> f64 {
        self.m * x  + self.c
    }
}
}

Derleyici bu konuda epey yardımcı oluyor ve basit bir kapamayı buna dönüştürüyor! Tek bilmeniz gereken kapama bir yapıdır ve verileri içinde bulunduğu çevreden ödünç alır. Bu referansların da bir yaşam ömrü vardır.

Bütün kapamaların benzersiz tipleri vardır ancak benzer özellikleri (trait) içerirler. Türü tam bilmesek de en azından jeneriklerde nasıl ifade edeceğimi biliyoruz:


#![allow(unused)]
fn main() {
fn apply<F>(x: f64, f: F) -> f64
where F: Fn(f64)->f64  {
    f(x)
}
...
    let res1 = apply(3.0,lin);
    let res2 = apply(3.14, |x| x.sin());
}

El-meal: apply Fn(f64)->f64'e sahip herhangi bir T tipi ile çalışabilir - yani f64 alıp f64 dönen bir fonksiyon olabilir bu.

apply(3.0, lin) şeklinde çağırdıktan sonra lin'e erişmek şu tuhaf hatayı ortaya çıkartıyor:

    let l = lin;
error[E0382]: use of moved value: `lin`
  --> closure2.rs:22:9
   |
16 |     let res = apply(3.0,lin);
   |                         --- value moved here
...
22 |     let l = lin;
   |         ^ value used here after move
   |
   = note: move occurs because `lin` has type
    `[closure@closure2.rs:12:15: 12:26 m:&f64, c:&f64]`,
     which does not implement the `Copy` trait

Ve bu kadar, apply bizim kapamamızı yedi. Ve ayrıca, rustc'nin kullanmaya çalıştığı yapının (struct) gerçek tipi. Kapamaları bir yapı olarak düşünmek işi epey kolaylaştırıyor.

Bir kapama çağırmak aslında metot çağrısıdır: Üç tip fonksiyon özelliği (trait) üç tip metoda sahiptir:

  • Fn, &self olarak geçer.
  • FnMut, &mut self olarak geçer.
  • FnOnce ise sadece self olarak geçer.

Bir kapama içerisinde yakalanmış referansları düzenlemek de mümkündür.


#![allow(unused)]
fn main() {
   fn mutate<F>(mut f: F)
   where F: FnMut() {
       f()
   }
   let mut s = "world";
   mutate(|| s = "hello");
   assert_eq!(s, "hello");
}

mut'a dikkat edin - f'in değişebilir olması gerekiyor.

Yine de, ödünç alma ile ilgili kurallardan kaçınamazsınız. Şuna bakın:


#![allow(unused)]
fn main() {
let mut s = "world";

// closure does a mutable borrow of s
let mut changer = || s = "world";

changer();
// does an immutable borrow of s
assert_eq!(s, "world");
}

Çalışamaz! Çünkü s'i assert deyiminde ödünç alamıyoruz, çünkü daha önce changer kapamasında değişken olarak ödünç almıştır. Kapama düşürülmediği sürece s'e hiç kimse erişemez, bundan dolayı iç bir kapsam alanı içerisinde kullanarak yaşam süresini kontrol etmek en iyi çözümdür.


#![allow(unused)]
fn main() {
let mut s = "world";
{
    let mut changer = || s = "world";
    changer();
}
assert_eq!(s, "world");
}

Eğer Lua ve JavaScript gibi dillere aşinaysanız, bu dillerde basit olmasına karşın Rust'ta kapamaların bu denli karmaşık olduğunu merak ediyor olabilirsiniz. Bu, Rust'ın gizlice bellek tahsis etmemesi için gerekli bir bedeldir. JavaScript'te, mutate(function() {s = "hello";}) gibi bir ifadenin karşılığı her zaman dinamik bellek tahsis edilmiş kapamadır.

Bazen kapamaların verileri ödünç almasını değil direkt taşımasını isteyebilirsiniz.


#![allow(unused)]
fn main() {
    let name = "dolly".to_string();
    let age = 42;

    let c = move || {
        println!("name {} age {}", name,age);
    };

    c();

    println!("name {}",name);
}

Burada alacağımız hata son println'dadır: "taşınmış verinin kullanımı: name (use of moved value: name)". Burada tek bir çözüm var, kapamanın içine veriyi taşımak:


#![allow(unused)]
fn main() {
    let cname = name.to_string();
    let c = move || {
        println!("name {} age {}",cname,age);
    };
}

Neden taşıyan kapamalara ihtiyacımız var? Çünkü orijinal verinin erişilemeyeceği bir durumda onları çağırmamız gerekebilir. En basit örneği iş parçacıklarıdır. Taşıyan kapamalar ödünç almaz, bundan dolayı yaşam süresi açısından hiçbir sorunları olmaz.

Kapamaların esas kullanımı döngüleyici metotlarıdır. Noktalı sayılar için hazırladığımız range döngüleyicisini hatırlayın. Kapama kullanarak bu döngüleyici (veya başka döngüleyiciler) üzerinde işlem yapmak gayet kolay:


#![allow(unused)]
fn main() {
    let sine: Vec<f64> = range(0.0,1.0,0.1).map(|x| x.sin()).collect();
}

map vektörler üzerinde tanımlanmadı (Bunu kullanan bir özellik (trait) yaratmak oldukça kolay olmasına rağmen) çünkü map'ın yeni bir vektör yaratması gerekirdi. Bu şekilde elimizde seçeneklerimiz oluyor. Üstelik, geçici hiçbir öğe yaratılmış olmuyor:


#![allow(unused)]
fn main() {
 let sum: f64 = range(0.0,1.0,0.1).map(|x| x.sin()).sum();
}

Tıpkı bir döngü yazmak kadar kadar hızlı. Eğer Rust kapamaları JavaScript kapamaları kadar "acısız" olsaydı bu performansı garanti edemezdik.

filter da ayrıca bir iterator metotudur - geriye sadece koşullara uyanlar kalır:


#![allow(unused)]
fn main() {
    let tuples = [(10,"ten"),(20,"twenty"),(30,"thirty"),(40,"forty")];
    let iter = tuples.iter().filter(|t| t.0 > 20).map(|t| t.1);

    for name in iter {
        println!("{} ", name);
    }
    // thirty
    // forty
}

Üç Tarz-ı Döngüleyici

Üç farklı çeşit (yine) üç basit argüman tipine denk düşüyor. Bir String vektörümüz olduğunu düşünelim. Bunlar bizim döngüleyici tiplerimiz, ilk üçü aleni bir şekilde sonraki üçü de gizil bir şekilde belirtilmiştir.


#![allow(unused)]
fn main() {
for s in vec.iter() {...} // &String
for s in vec.iter_mut() {...} // &mut String
for s in vec.into_iter() {...} // String

// implicit!
for s in &vec {...} // &String
for s in &mut vec {...} // &mut String
for s in vec {...} // String
}

Şahsen ben aleni bir şekilde ifade etmeyi tercih ediyorum, ancak iki formu da anlamak ve nasıl kullanıldığını bilmek önemlidir.

into_iter vektörü tüketir ve içeriğindeki karakter dizilerini çıkartır, ve ardından artık vektör kullanılamaz - artık taşınmış olur. Pythonistalar alışkanlıktan for s in vec dediği zaman başlarına bu gelir.

for s in &vec şeklindeki gizil form muhtemelen kullanmak isteyeceğiniz şekildir, tıpkı fonksiyon argümanlarında &T kullanmak gibi.

Üç çeşidi de anlamak önemlidir çünkü Rust tip tahminlerini epeyce kullanır - kapama argümanlarında tip bildirimlerini pek görmezsiniz. Bu iyi bir şey çünkü bu tiplerin hepsi yazılsaydı kafa şişirici olurdu. Ancak, bu ufak kodun bedeli gizil tiplerin ne olduğunu net olarak bilmenizin gerekmesidir!

map döngüleyicinin değerini ne olursa olsun alır ve onu başka bir şeye dönüştürür, ancak filter veriye bir referans alır. Aşağıda, iter kullanıyoruz ve bundan dolayı döngüleyici tipi &Stringtir. filter'ın her veriyi referansını aldığını gözden kaçırmayın:


#![allow(unused)]
fn main() {
for n in vec.iter().map(|x: &String| x.len()) {...} // n is usize
....
}

for s in vec.iter().filter(|x: &&String| x.len() > 2) { // s is &String
...
}
}

Metotları çağırdığınız zaman Rust kendiliğinden dereferans eder, ondan dolayı sorunu pek anlamazsınız. Ancak |x: &&String| x == "one" çalışmayacaktır çünkü operatörler tip eşleştirmesinden daha katıdır. rustc, &str ve &&String'i kıyaslayacak bir operatör olmadığını bildirecektir. Bundan dolayı eşleşme yapabilmek için &&String'i &Stringe çevirmek için dereferans etmeniz gerekecektir.


#![allow(unused)]
fn main() {
for s in vec.iter().filter(|x: &&String| *x == "one") {...}
// same as implicit form:
for s in vec.iter().filter(|x| *x == "one") {...}
}

Eğer tipleri bildirmeyi bırakırsanız, argümanı şu şekilde düzeltebilirsiniz ki bu sefer s'in tipi &String olur.


#![allow(unused)]
fn main() {
for s in vec.iter().filter(|&x| x == "one")
}

Ve çoğu zaman bu şekilde yazıldığını görürsünüz.

Dinamik Verili Yapılar

Kendisine refereans barındıran yapı tekniği çok güçlü bir tekniktir.

Aşağıda C ile yazılmış bir ikili ağacın temel tuğlasını görüyorsunuz. (C... Âdeta Beyoğlu'nun arka sokakları gibi... "Acaba başıma ne gelecek?" demeden dolaştığınız tarihî sokaklarda nefes kesici bir gezi...)


#![allow(unused)]
fn main() {
    struct Node {
        const char *payload;
        struct Node *left;
        struct Node *right;
    };
}

Bunu doğrudan Node alanlarını içererek yapamazsınız çünkü Node'un büyüklüğü yine Node'a dayanır. Ki bu hesaplanamaz. Bundan dolayı Node yapılarının göstericilerini (pointer) kullanıyoruz, ki göstericinin boyutu her zaman kestirilebilir.

Eğer left, NULL değilse Node'un left tarafı bir başka Node gösteriyordur ve bu böyle sonsuza kadar gidebilir.

Rust'ta NULL yoktur (en azından bu güvensiz hâliyle yok), bu Option'un işidir. Ancak Node'u doğrudan Option içerisine ekleyemezsiniz çünkü Node'un boyutunu bilemezsiniz. (gibi gibi) Bu da Box'un işidir, kendisinin sabit bir boyutu vardır ancak bellekte alanı tahsis edilmiş veriyi işaret eder.

İşte Rust'taki karşılığına bakalım, type ile tipimize bir takma ad verdik:


#![allow(unused)]
fn main() {
type NodeBox = Option<Box<Node>>;

#[derive(Debug)]
struct Node {
    payload: String,
    left: NodeBox,
    right: NodeBox
}
}

(Rust işte böyle kalender meşreptir, ileriye dönük bildirimlere ihtiyacınız yoktur.)

Şimdi bunu test edelim:

impl Node {
    fn new(s: &str) -> Node {
        Node{payload: s.to_string(), left: None, right: None}
    }

    fn boxer(node: Node) -> NodeBox {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node) {
        self.right = Self::boxer(node);
    }

}


fn main() {
    let mut root = Node::new("root");
    root.set_left(Node::new("left"));
    root.set_right(Node::new("right"));

    println!("arr {:#?}", root);
}

Çıktı beklediğimizden çok daha iyi, "{:#?}" sağolsun. ("#" genişletilmiş demektir.)

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "left",
            left: None,
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "right",
            left: None,
            right: None
        }
    )
}

Peki ya root düşerse? Bütün alanlar da düşer, ağacın dalları düşerse kendi alanlarını da kaybolur ve böyle devam eder. Box::new, C++'daki new anahtar kelimesine en çok ulaşacağınız alandır ancak delete veyahut free gibi bir kelimeye ihtiyacınız yoktur.

Bu ağacı kullanmak için bir yol bulmalıyız. Karakter dizilerinin sıralanabildiğine dikkat edin: "hede" < "hödö", "ayı" > "abi"; sözde alfabetik sıralama olarak anılır. (Aslını söylemek gerekirse, insan dillerinin çeşitliliğinden ve tuhaf kurallarına istinaden buna sözlüksel sıralama denir.)

Aşağıda Nodeları sözlüksel sıralamaya göre yerleştiren bir metot görüyorsunuz. Veriyi mevcut Node ile kıyaslıyoruz - eğer küçükse soluna yerleştiriyoruz, değilse de sağına yerleştirmeye çalışıyoruz. Solda bir Node olmayabilir, bundan dolayı set_left kullanıyoruz.

    fn insert(&mut self, data: &str) {
        if data < &self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }

    ...
    fn main() {
        let mut root = Node::new("root");
        root.insert("one");
        root.insert("two");
        root.insert("four");

        println!("root {:#?}", root);
    }

match'a dikkat edin - Box içerisinden değişken bir referans çıkartıyoruz, eğer Option'un içeriği Some ise insert kullanıyoruz. Değilse, sol tarafa yeni bir Node ekliyoruz ve böyle devam ediyoruz. Box, akıllı bir göstericidir; Node metotlarını çağırmak için "kutudan çıkarmamıza" gerek yok!

İşte ağacımızın görüntüsü:

root Node {
    payload: "root",
    left: Some(
        Node {
            payload: "one",
            left: Some(
                Node {
                    payload: "four",
                    left: None,
                    right: None
                }
            ),
            right: None
        }
    ),
    right: Some(
        Node {
            payload: "two",
            left: None,
            right: None
        }
    )
}

Diğerlerinden daha "küçük" olan karakter dizileri sol eklenir, aksi durumda ise sağa eklenirler.

Şimdi gezinti zamanı. Bu iç-sıralı gezinmedir. (inorder traversal) - önce solu ziyaret ediyoruz, bir şeyler yapıyoruz ve sonra da sağa geçiyoruz.


#![allow(unused)]
fn main() {
    fn visit(&self) {
        if let Some(ref left) = self.left {
            left.visit();
        }
        println!("'{}'", self.payload);
        if let Some(ref right) = self.right {
            right.visit();
        }
    }
    ...
    ...
    root.visit();
    // 'four'
    // 'one'
    // 'root'
    // 'two'
}

Karakter dizilerini bir sıralamaya göre geziyoruz! ref'in if let için kullanıldığına dikkat edin, match ile aynı kurallara sahiptir.

Genellenen Yapılar

Önceki örneğimizde kullandığımız ikili ağaç yapısını düşünün. Bütün payload tipleri için yeniden yazmak epey çıldırtıcı olurdu doğrusu. T tip parametresiyle Node'u yeniden jenerik şekilde yazıyoruz.


#![allow(unused)]
fn main() {
type NodeBox<T> = Option<Box<Node<T>>>;

#[derive(Debug)]
struct Node<T> {
    payload: T,
    left: NodeBox<T>,
    right: NodeBox<T>
}
}

Bu kullanım diller arasındaki farkları da belli ediyor. Payload üzerindeki temel işlem karşılaştırmadır, bundan dolayı T ile < kullanılabilmelidir ki PartialOrd bunu sağlar. Tip parametresi impl bloğu içerisinde özellik kısıtlamasıyla birlikte yazılmalıdır.

impl <T: PartialOrd> Node<T> {
    fn new(s: T) -> Node<T> {
        Node{payload: s, left: None, right: None}
    }

    fn boxer(node: Node<T>) -> NodeBox<T> {
        Some(Box::new(node))
    }

    fn set_left(&mut self, node: Node<T>) {
        self.left = Self::boxer(node);
    }

    fn set_right(&mut self, node: Node<T>) {
        self.right = Self::boxer(node);
    }

    fn insert(&mut self, data: T) {
        if data < self.payload {
            match self.left {
                Some(ref mut n) => n.insert(data),
                None => self.set_left(Self::new(data)),
            }
        } else {
            match self.right {
                Some(ref mut n) => n.insert(data),
                None => self.set_right(Self::new(data)),
            }
        }
    }
}


fn main() {
    let mut root = Node::new("root".to_string());
    root.insert("one".to_string());
    root.insert("two".to_string());
    root.insert("four".to_string());

    println!("root {:#?}", root);
}

Tıpkı C++ gibi genellenen yapımız tip parametrelerinin köşeli ayraçlarla gösterilmesine ihtiyaç duyar. Rust genellikle bu tür tip parametresini bağlamdan tahmin edebilecek kadar zekidir - Bunun Node<T> olduğunu biliyor ve T üzerinde insert kullanıyor. İlk insert tasarısı sadece String ile takılıp kalmıştı. Ancak yeni kullanım uymuyorsa muhtemelen bir şekilde bunu bildirecektir.

Ancak, tipi uygun biçimde kısıtlamanız gerektiğine dikkat edin.

Dosya Sistemi ve Süreçler

Dosya Okumaya Farklı Bir Bakış

Birinci bölümün sonunda bütün dosyayı bir karakter dizisi içerisine aktarmayı göstermiştim. Doğal olarak bu her zaman iyi bir fikir olmayabilir, bu yüzden bir dosyayı satır satır okumayı göstereceğim.

fs::File, io::Read'ı tanımlar ki bu okunabilen her şeyin özelliğidir. (trait) Bu özellik, u8 ile bayt içeren bir dilimi dolduran read metotunu tanımlar - bu özelliğin gerekli tek metotudur ve beraberinde gelen pek çok metotu bedavaya kapmış olursunuz, Iterator gibi. Bir bayt vektörünü doldurmak için read_to_end'u veya bir karakter dizisini doldurmak için read_to_string'i kullanabilirsiniz - UTF-8 ile kodlanmış olması bir zorunluluktur.

Bu arabellek kullanılmayan (buffering) "saf" bir okumadır. Arabellekli okuma için read_line ve lines döngüleyicisini sunan io::BufRead özelliğini kullanabilirsiniz. io::BufReader, io::BufRead'ın kullanımlarını okunabilir herhangi bir şey için sunacaktır.

fs::File ise aynı zamanda io::Write'ı da barındırır.

Bütün bu özelliklerin kullanılabilir olduğunu görmenin en kolay yolu use std::io::prelude::* kullanmaktır.


#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let reader = io::BufReader::new(file);

    for line in reader.lines() {
        let line = line?;
        println!("{}", line);
    }
    Ok(())
}
}

let line = line? gözünüze biraz tuhaf görünebilir. Döngüleyiciden dönen line aslında io::Result<String> tipidir ve ? ile onu paketinden dışarı çıkartıyoruz. Bunu yapmamızın sebebi döngü esnasında bir şeylerin yanlış gidebileceğidir. - Girdi/çıktı hataları, UTF-8 olmayan bir bayt serisini almak gibi şeyler.

Bir döngüleyici olarak lines'i collect ile bir vektöre kolayca çevirebiliriz ya da enumerate döngüleyicisi ile her satırın sırasını öğrenebiliriz.

Yine de bütün satırları okumak için en iyi tercih bu değildir çünkü her satır için yeni bir String tahsis edilir. En iyi yöntem read_line kullanmaktır, biraz daha acayip görünse de. Satırın bir satır sonu karakteri içerdiğine dikkat edin ki bunu trim_right ile siliyoruz.


#![allow(unused)]
fn main() {
    let mut reader = io::BufReader::new(file);
    let mut buf = String::new();
    while reader.read_line(&mut buf)? > 0 {
        {
            let line = buf.trim_right();
            println!("{}", line);
        }
        buf.clear();
    }
}

Sonuçta çok daha az tahsis etme işlemimiz oluyor çünkü bir karakter dizisini temizlemek onun tahsis edilmiş alanını boşaltmıyor, bu alanı işgal eden yeni tahsis işlemleriyle uğraşmıyoruz.

Bu arada ödünç alma işlemini bozmamak için blok kullandığımız durumlardan birisiyle karşı karşıyayız. line, buf tarafından ödünç alınıyor ve bu ödünç buf'ı tekrardan düzenlemeden önce belleği terk etmelidir. Yine Rust bizi aptalca bir şey yapmaktan alıkoymaya çalışıyor, ara belleği boşalttıktan sonra line'a ulaşmak gibi. (Ödünç alma mekanizması bazen sizi darlayabilir. Rust bu kodu inceleyecek ve line'ın, buf.clear()'dan sonra kullanılmadığını görecektir, bu "sözcüksel olmayan yaşam sürelerini (non-lexical lifetimes)" ayıklamasından kaynaklanır.

Bu pek şık görünmüyor. Belki size arabelleğe referanslar dönen bir döngüleyici veremem ama en azından döngüleyiciye benzeyen bir şeyler verebilirim.

Şimdi jenerik bir yapı tanımlayalım; R ismindeki tip parametremiz Read içeren her tipi kabul edebilir. Bu yapının içeriğinde okuyucu (reader) ve referansını alabileceğimiz bir arabelleğimiz (buffer, ya da kısaca buf) olacak.


#![allow(unused)]
fn main() {
// file5.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

struct Lines<R> {
    reader: io::BufReader<R>,
    buf: String
}

impl <R: Read> Lines<R> {
    fn new(r: R) -> Lines<R> {
        Lines{reader: io::BufReader::new(r), buf: String::new()}
    }
    ...
}
}

Şimdi next metotunu kullanalım. Tıpkı bir döngüleyici gibi Option dönecek, None döndüğü zaman döngüleyici başa dönmüş olacak. İçerisinden dönen tip Result olacak çünkü read_line başarısız olabilir ve asla hataları görmezden gelmiyoruz. Eğer başarısız olursa hatayı Some<Result> olarak dönebiliriz. Eğer dosyanın doğal sınırı olan "sıfır baytları" okunursa bu bir hata değildir, sadece None'dur.


#![allow(unused)]
fn main() {
    fn next<'a>(&'a mut self) -> Option<io::Result<&'a str>>{
        self.buf.clear();
        match self.reader.read_line(&mut self.buf) {
            Ok(nbytes) => if nbytes == 0 {
                None // no more lines!
            } else {
                let line = self.buf.trim_right();
                Some(Ok(line))
            },
            Err(e) => Some(Err(e))
        }
    }
}

Şimdi, yaşam sürelerinin nasıl çalıştığına dikkat edin. Açık bir yaşam süresi belirtmemiz gerekti çünkü Rust hiçbir zaman karakter dizelerini yaşam sürelerini belirtmeden ödünç almaya izin vermeyecektir ve burada ödünç alınan karakter dizisinin yaşam süresini self ile birlikte belirtiyoruz.

Bu yaşam süresiyle birlikte bu tanım Iterator özelliği ile uyumsuz. Ancak uyumlu olsaydı sorunları kolayca görebilirdik, collect'in karakter dizileririnden vektör yapmaya çalıştığını düşünün. Bu mümkün değil zira hepsi aynı değişebilir (mutable) karakter dizisini ödünç almış olurdu! (Eğer bütün dosyayı bir karakter dizesine çevirmek isteseydiniz karakter dizelerinin lines metotu karakter dizeleri dönebilirdi çünkü hepsi esas karakter dizilerinden ödünç alınmıştır.)

Neticedeki döngü çok daha temizdir ve ara belleğe alma işlemi pek çok kullanıcı tarafından görünmez.


#![allow(unused)]
fn main() {
fn read_all_lines(filename: &str) -> io::Result<()> {
    let file = File::open(&filename)?;

    let mut lines = Lines::new(file);
    while let Some(line) = lines.next() {
        let line = line?;
        println!("{}", line);
    }

    Ok(())
}
}

Hatta eşleştirme karakter dizisi dilimini dışarı çıkartacağından, döngüyü bu şekilde de yazabilirsiniz:


#![allow(unused)]
fn main() {
    while let Some(Ok(line)) = lines.next() {
        println!("{}", line)?;
    }
}

Bunu yapmak isteyebilirsiniz ancak muhtemel hataları halının altına süpürmüş olursunuz; döngü bir hata söz konusu olduğu zaman sessizce duracaktır. Daha da ötesi, Rust'ın UTF-8'e çeviremediği ilk yerde duracaktır. Gündelik kodlar için kabul edilebilir ancak kodu yayına aldığınız zaman kötüdür.

Dosyalara Yazmak

Debug'u kullanırken write! ile tanışmıştık - Write'ın kullanıldığı her yerde aynı zamanda write! kullanabiliriz. print! demenin farklı bir yoluna bakalım:


#![allow(unused)]
fn main() {
    let mut stdout = io::stdout();
    ...
    write!(stdout,"answer is {}\n", 42).expect("write failed");
}

Eğer bir hata söz konusu olabilirse bunu idare edebilirsiniz. Bu genellikle gerçekleşmez ancak yine de söz konusu olabilir. Eğer bir girdi/çıktı işlemi yapıyor Bu genellikle kabul edilebilir çünkü bir dosyayla oynaşıyorsanız ? eklemeniz gereken bazı yerler olabilir.

Ancak bir fark var; print!, stdout'u her yazım için kitler. Çoğu zaman istediğiniz şey budur çünkü çoklu süreçlerin yazıldığı programlarda stdout'u kitlemezseniz çıktınız tuhaf bir şekilde karmaşıklaşabilir. Ancak çok fazla metin yolluyorsanız write! çok daha hızlı davranacaktır.

Çeşitli dosyalar için write! kullanmamız gerekiyor. out, write_out'un sonunda düşürüldüğü zaman dosya kapanır ki bu hem istenen şeydir hem de önemlidir.

// file6.rs
use std::fs::File;
use std::io;
use std::io::prelude::*;

fn write_out(f: &str) -> io::Result<()> {
    let mut out = File::create(f)?;
    write!(out,"answer is {}\n", 42)?;
    Ok(())
}

fn main() {
  write_out("test.txt").expect("write failed");
}

Eğer performansı önemsiyorsanız Rust'ın varsayılan olarak önbelleğe alınmadığını bilmeniz gerekir. Her bir yazma talebi doğrudan işletim sistemine gönderilir ki bu işleri oldukça yavaşlatır. Bundan bahsediyorum çünkü diğer programlama dillerinde varsayılan davranış farklıdır ve Rust'ın betik dilleri tarafından geride bırakıldığını görmek sizi epeyce şaşırtabilir! Nasıl ki Read'ın io::BufReader'ı varsa io::BufWriterın da Write'ı var.

Dosyalar, Konumlar ve Dizinler

Şimdi makinedeki Cargo dizinini bulan bir program yazalım. En basit yöntem ~/.cargo altına bakmaktır. Ancak bu Unix kabuğu için geçerlidir, çoklu ortam desteği için env::home_dir fonksiyonunu kullanacağız. (Başarısız olabilir ancak ev dizini olmayan bir bilgisayar da Rust araçlarını barındırmaz.)

Ç.N: env::home_dir fonksiyonu beklenildiği gibi çalışmadığı için 1.29.0'dan itibaren tedavülden kaldırılmıştır. Bu fonksiyonu kullanmayın, Windows ve Unix ortamları için ev dizinlerini kendi yöntemlerinizle bulmanızı veya bir crates.io'yu karıştırmanızı tavsiye ederim.

Sonra bir PathBuf yaratalım ve push metotunu parçalardan tam bir dosya konumu inşa etmek için kullanalım. (Bu / gibi bir şeyle ile debelenmekten çok daha kolaydır.)

// file7.rs
use std::env;
use std::path::PathBuf;

fn main() {
    let home = env::home_dir().expect("no home!");
    let mut path = PathBuf::new();
    path.push(home);
    path.push(".cargo");

    if path.is_dir() {
        println!("{}", path.display());
    }
}

PathBuf, String gibi çalışır - karakterlerin büyüyebilen bir paketidir ancak konum inşa etmek için kendi araçlarını kullanır. Ancak özelliklerinin çoğunluğu Path'ın referans versiyonundan gelir, tıpkı &str gibi. Yani, mesela, is_dir bir Path metotudur.

Bu kulağınıza şüphe uyandıran bir miras alma (inheritance) tarzı gibi gelebilir, ancak bu Deref özelliğinin maharetidir. Gözünüze tıpkı String/&str gibi görünebilir - bir PathBuf referansı bir Path referansına dönüşür/zorlanır. (coerced) ("Zorlamak (Coerce)" kelimesi biraz ağır kaçmış olabilir ancak bu Rust sizin için dönüşüm uyguladığı nadir yerlerden birisidir.)


#![allow(unused)]
fn main() {
fn foo(p: &Path) {...}
...
let path = PathBuf::from(home);
foo(&path);
}

PathBuf'un en yakın arkadaşı OsString'tir, kendisi sistemden doğrudan aldığımız karakter dizilerini gösterir. (Aynı şekilde buna karşılık OsString/&OsStr ilişkisi de vardır.)

Bu tarz karakter dizilerinin UTF-8 olacağını garanti edilmemiştir! Gerçek hayatta her şey karmaşıktır, özellikle "Her şey neden bu kadar zor" diye düşünürken. Sadede gelelim. Birincisi antik ASCII kodlamanın ve diğer diller için özel kodlamanın kullanıldığı yıllar oldu. İkincisi kendi aramızda konuştuğumuz dillerin kendisi de epey karmaşıktır. Mesela "noël" kelimesi beş Unikod kodu kadar yer tutar.

Modern işletim sistemlerinin dosya adlarının çoğu zaman Unikod olabileceği doğrudur. (Unix tarafı için UTF-8, Windows için UTF-16) Ama olmadığı zamanlar da vardır! Ve Rust bu olasılığı dikkatlice ele almalıdır. Örneğin Path, as_os_str diye &OsStr dönen bir metota sahiptir. Ancak to_str, bazen Option<&str> döner. Yani her zaman mümkün değildir!

İnsanlar genelde bu konuda biraz takılır çünkü "karakter" ve "karakter dizesi"ne fazlasıyla alıştılar. Einstein'ın dediği gibi programlama dilleri sade olmalı, basit değil. Bir sistem programlama dilinin String/&str ayrımına ihtiyacı vardır (ödünç alınmışa karşılık sahiplenmiş: kafaya epeyce yatıyor) ve Unikod karakter dizilerini standartlaştırmak için Unikod olmayan stilleri de kapsamalıdırlar - işte OsString/&OsStr kardeşlerin doğuşu. Bunların içeriğinde String benzeri enteresan metotlar bulunmadığına dikkat edin, çünkü tiplemelerinden tam olarak emin değiliz.

Ancak, insanlar dosya isimlerini olağan karakter dizileriymiş gibi işlemeye alışkınlardır ki kolayca dosya konumlarını kullanmak ve değiştirmek için Rust'ta PathBuf vardır.

Bir konumun adresinin parçalarını temizleyebilmek için pop kullanabilirsiniz. Mesela bulunduğumuz dizinden başlayalım.

// file8.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        println!("{}", path.display());
        if ! path.pop() {
            break;
        }
    }
}
// /home/steve/rust/gentle-intro/code
// /home/steve/rust/gentle-intro
// /home/steve/rust
// /home/steve
// /home
// /

Bu da daha kullanışlı bir hâli. Bir konfigrasyon dosyasını aramak için bir program yazdık ve bütün altdizinleri bu dosya için arıyoruz. Bunun için /home/steve/rust/config.txt diye bir dosya yazdık ve program /home/steve/rust/gentle-intro/code içerisinden başlıyor:

// file9.rs
use std::env;

fn main() {
    let mut path = env::current_dir().expect("can't access current dir");
    loop {
        path.push("config.txt");
        if path.is_file() {
            println!("gotcha {}", path.display());
            break;
        } else {
            path.pop();
        }
        if !path.pop() {
            break;
        }
    }
}
// gotcha /home/steve/rust/config.txt

Bu git'in nasıl çalıştığı depoyu nasıl bulduğunun yöntemidir de aynı zamanda.

Bir dosya hakkındaki bilgiler metaveri (metadata) olarak geçer. Her zaman olduğu gibi bir hata olabilir - "Bulunamadı" dışında mesela dosyayı okumamız için gerekli izinlere sahip olamayabiliriz.

// file10.rs
use std::env;
use std::path::Path;

fn main() {
    let file = env::args().skip(1).next().unwrap_or("file10.rs".to_string());
    let path = Path::new(&file);
    match path.metadata() {
        Ok(data) => {
            println!("type {:?}", data.file_type());
            println!("len {}", data.len());
            println!("perm {:?}", data.permissions());
            println!("modified {:?}", data.modified());
        },
        Err(e) => println!("error {:?}", e)
    }
}
// type FileType(FileType { mode: 33204 })
// len 488
// perm Permissions(FilePermissions { mode: 436 })
// modified Ok(SystemTime { tv_sec: 1483866529, tv_nsec: 600495644 })

Bir dosyanın (bayt cinsinden) uzunluğu ve son değiştirilme tarihini bulmak oldukça kolaydır. (Ancak bunu yapamayacağımız zamanlar da vardır.) File tipinin ilgili metotları is_dir (Ç.N: Dizin midir?), is_file (Ç.N: Dosya mıdır?) ve is_symlink'dir. (Ç.N: "Sembolik bağ mıdır?")

permissions da ilginç bir metottur. Rust platformlar arası geçerli olmaya özen gösterir ve bu da bir "en düşük ortak paydaya erişme" durumudur. Genel olarak, dosya salt okunur ise onu sorgulayabilirsiniz - "yetkiler (permissions) konsepti Unix'te oldukça geniştir ve kullanıcı/grup/diğerleri için okuma/yazma/çalıştırma yetkilerini barındırır.

Ancak Windows ile çalışmıyorsanız en azından yetki bitlerini öğrenmek için platforma yönelik özellikleri (trait) kapsama çağırabilirsiniz. (Genelde olduğu gibi özellikler kapsama çağrıldığı zaman etkinleşirler.) Sonrasında da programın dosyasını şöyle değiştirebiliriz:


#![allow(unused)]
fn main() {
use std::os::unix::fs::PermissionsExt;
...
println!("perm {:o}",data.permissions().mode());
// perm 755
}

("{:o}" sekizli (octal) sayı sistemine göre biçimlendirir.)

(Windows'ta bir dosyanın çalıştırılıp çalıştırılamayacağı dosyanın uzantısından tanımlanır. Çalıştırılabilir dosya uzantıları PATHEXT çevre değişkeninden öğrenilebilir - ".exe", ".bat" vs.)

Dosyalarla çalışmak için std::fs bize pek çok faydalı fonksiyonlar sunar, bir dosyayı kopyalayıp taşımak, sembolik bağ kurmak ve dizin oluşturmak gibi.

Bir dizinin içeriğini öğrenmek için bir döngüleyici sunan std::fs::read_dir'i kullanabilirsiniz. İşte karşınızda boyutları 1024 bayttan büyük olan ve uzantısı .rs ile biten bütün dosyalar!


#![allow(unused)]
fn main() {
fn dump_dir(dir: &str) -> io::Result<()> {
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let data = entry.metadata()?;
        let path = entry.path();
        if data.is_file() {
            if let Some(ex) = path.extension() {
                if ex == "rs" && data.len() > 1024 {
                    println!("{} length {}", path.display(),data.len());
                }
            }
        }
    }
    Ok(())
}
// ./enum4.rs length 2401
// ./struct7.rs length 1151
// ./sexpr.rs length 7483
// ./struct6.rs length 1359
// ./new-sexpr.rs length 7719
}

read_dir'in başarısız olabileceği aşikar ("bulunamadı" ya da "yetki yok" gibi bir hata çıkarabilir) ancak aynı zamanda her yeni bir girdiye erişmek de başarısız olabilir. (Okunmuş veriyi arabelleğe alan lines döngüleyicini düşünün.) Ek olarak, ilgili girdi için uygun metaveriye ulaşamayabiliriz de. Bir dosyanın uzantısı da pekala olmayabilir, bunu ayrıca kontrol etmemiz gerekir.

Neden konumların üzerinde bir döngüleyici kullanmıyoruz? Unix'te opendir sistem çağrısı bu şekilde çalışır ancak Windows'ta dosyaların metaverisini almadan üzerinde bir döngü kuramazsınız. Dolayısıyla platformlar arasındaki kodun en verimli çalışmasının en zarif yoludur.

Bu noktada "hatalarla boğuştuğunuz" için kendinizi bitkin hissedebilirsiniz. Ancak hatalar her zaman vardı - Rust sizin için yeni hatalar icat etmedi. Sadece hataları görmezden gelmemeniz için elinden geleni yapıyor. Herhangi bir işletim sistemi çağrısı başarısız olabilir.

Java ve Python gibi dillerde hata atarsınız (throw exceptions); Go ve Lua gibi diller ise size iki veri döner ve birincisi sonuç ikincisi de hata olur, Rust'ta kitaplık fonksiyonlarının hata oluşturması kötü bir davranış olarak kabul edilir. Bu nedenle pek çok hata denetimi vardır ve fonksiyonlar erkenden dönebilir.

Ya hata alırsınız ya da almazsınız, Rust bu yüzden Result kullanır: hem hata hem de sonuç elde edemezsiniz. Ve soru işareti operatörü kontrol oldukça kolaylaştırır.

Süreçler

Esas ihtiyaç duyduğumuz şeylerden bir şey programların başka bir programı çalıştırması ya da süreç başlatmaktır. Programınız pek çok alt süreç çalıştırabilir ve alt süreçler üst süreçlerle pek çok ilişkide bulunabilir.

Bir programı, argümanları da beraberinde kullanmanızı sağlayan Command yapısı ile çalıştırabilirsiniz.

use std::process::Command;

fn main() {
    let status = Command::new("rustc")
        .arg("-V")
        .status()
        .expect("no rustc?");

    println!("cool {} code {}", status.success(), status.code().unwrap());
}
// rustc 1.15.0-nightly (8f02c429a 2016-12-15)
// cool true code 0

new programın adını alır (Eğer kesin bir dosya konumu değilse PATH içinde arar), arg yeni argümanlar ekler ve status programın çalışmasını tetikler. Program çalışınca bir Result alırsınız, Ok programın çalıştığını belirtir ki içeriğinde ExitStatus bulunur. Bizim örneğimizde programımız başarıyla çalıştı ve çıkış değeri olarak 0 döndü. (unwrap kullanmamızın sebebi eğer program bir sinyal ile "öldürülürse" her zaman çıkış değerini öğrenemeyecek olmamız.)

Eğer -V'yi -v ile değiştirirsek rustc başarısız olur.

error: no input filename given

cool false code 101

Üç farklı koşul gerçekleşebilir:

  • Program var olmayabilir, çalıştırma iznimiz olmayabilir ya da kullanılamaz hâlde olabilir
  • Program çalışmış ancak başarısız olmamış olabilir - sıfır olmayan çıkış değeri
  • Program sıfır olan çıkış değeri ile sonuçlanmış olabilir. Yani başarılıdır!

Varsayılan olarak programın standart çıktısı (stdout/Standart output) ve standart hata çıktısı (stderr/Standart Error) terminale yönlendirilir.

Bazen çıktıyı yakalamakla da ilgilenebiliriz ki output metotu bu işe yarar.

// process2.rs
use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("-V")
        .output()
        .expect("no rustc?");

    if output.status.success() {
        println!("ok!");
    }
    println!("len stdout {} stderr {}", output.stdout.len(), output.stderr.len());
}
// ok!
// len stdout 44 stderr 0

status ile alt süreçler sonuçlanana dek programımız durduruluyordu ve üç şey alıyorduk - sonuç (Önceden olduğu gibi), stdout'un ve stderr'un içeriği.

Şimdi ise yakaladığımız çıktı Vec<u8> içerisinde tutuluyor - sadece bayt olarak. İşletim sisteminden aldığımız şeylerin her zaman geçerli bir UTF-8 karakter dizisi olamayacağını hatırlayın. Aslında, bunun bir karakter dizisi bile olamayacağını bilmemiz gerekiyor - programlar tuhaf ikili veriler dönebilirler.

Eğer çıktının UTF-8 olacağından eminsek bu vektörü ya da baytları String::from_utf8 ile dönüştürebiliriz. Sonuç Result dönecektir çünkü dönüşümün gerçekleşeceğinden emin değiliz. İşi biraz daha gevşekçe yapan başka bir fonksiyonumuz var, String::from_utf8_lossy, bununla çeviriyi deneyebilir ve dönüştürülemeyen karakterlerin yerlerine � koyabilirsiniz.

Aşağıda kabukta programda çalıştıran kullanışlı bir fonksiyon görmektesiniz. Programımız stderr ile stdout'u birleştirmek için sıradan bir kabuk tekniği kullanıyor. Kabuğun ismi Windows'ta biraz farklı ancak diğerleriyle de sorunsuz çalışacaktır.


#![allow(unused)]
fn main() {
fn shell(cmd: &str) -> (String,bool) {
    let cmd = format!("{} 2>&1",cmd);
    let shell = if cfg!(windows) {"cmd.exe"} else {"/bin/sh"};
    let flag = if cfg!(windows) {"/c"} else {"-c"};
    let output = Command::new(shell)
        .arg(flag)
        .arg(&cmd)
        .output()
        .expect("no shell?");
    (
        String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
        output.status.success()
    )
}


fn shell_success(cmd: &str) -> Option<String> {
    let (output,success) = shell(cmd);
    if success {Some(output)} else {None}
}
}

Sağ taraftaki boşlukları biraz törpülüyorum ve böylece shell("which rustc") dediğimiz zaman doğrudan konumu alabiliyoruz.

Process ile çalıştırılmış bir programın current_dir ile çalışma dizinini, env ile çevre değişkenlerini belirleyerek çalışma şeklini kontrol edebilirsiniz.

Şimdiye kadar programımız alt süreçlerin tamamlanmasını bekledi. Eğer spawn metotunu kullanırsanız program size hemen döner ve programın basitçe bitişini bekler ki bu esnada gidip başka şeyler yapabiliriz. Aşağıdaki örnek stdout ve stderrin de aynı zamanda susturulmasına da örnektir.

// process5.rs
use std::process::{Command,Stdio};

fn main() {
    let mut child = Command::new("rustc")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .expect("no rustc?");

    let res = child.wait();
    println!("res {:?}", res);
}

Varsayılan olarak alt süreç üst sürecin standart girdisini ve çıktısını "miras alır". Ancak bu örnekte alt sürecin çıktısını "hiçliğe" yönlendirmiş olduk. Unix kabuğunda > /dev/null 2> /dev/null demekle aynı şey yani.

Rustta yaptığımız bu şeyleri sistem kabuğu (sh veya cmd) ile de yapabilirdik. Ancak bu yolla tamamen programatik bir şekilde süreç oluşturmayı kontrol etmiş oldunuz.

Bu örnekte eğer sadece .stdout(Stdio::piped()) kullanmış olsaydık alt sürecin çıktısını bir izole etmiş olurduk. Sonra da child.stdout üzerinden izole ettiğimiz çıktıyı doğrudan okuyabilirdik. (Read özelliğini kullanıyor). Aynı şekilde doğrudan child.stdin'e yazabilmek için de .stdout(Stdio::piped()) kullanabilirsiniz.

Ancak wait yerine wait_with_output'u kullansaydık bize Result<Output> dönerdi ve alt sürecin çıktısı Output'un stdout alanında daha önce olduğu gibi Vec<u8> olarak sunulurdu.

Child yapısında aynı zamanda aleni bir şekilde kill (öldür) metotu da bulunur.

Ç.N: Alt süreç İngilizce'de "çocuk süreç" anlamına gelen "Child process" olarak bahsedilir.

Modüller ve Cargo

Modüller

Programlar büyüdükçe onları bir dosyanın dışına taşımak ve fonksiyonlarla tipleri farklı isim alanlarına (namespace) taşımak gereklidir. Rust'ın bu iki şeye çözümü modüllerdir. (modules)

C ile başladı ama C ile bitmedi, bir süre sonra kendinizi primitive_display_set_width gibi rezilce isimler koyarken bulabilirsiniz. Sadece dosya isimlerini keyfinizce isimlendirebiliyorsunuz.

Rust'ta aynı şeyi primitive::display::set_width şeklinde isimlendirebiliyorsunuz. Üstelik use primitive::display kullandıktan sonra bunu kısaca display::set_width olarak çağırabilirsiniz. Hatta use primitive::display::set_width dedikten sonra onu doğrudan set_width diye çağırabilirsiniz fakat nasıl kullandığınıza dikkat etmelisiniz. rustc tarafında sorun olmaz ancak sizin kafanız karışabilir. Ancak, bu sistemin çalışabilmesi için dosya isimlerinin basit bir kaç kurala bakması gereklidir.

Yeni bir anahtar kelimemiz var, mod, bir bloğu içine yazılan tip ve fonksiyonlarla beraber topyekûn modül olarak ilan etmeye yarar.

mod foo {
    #[derive(Debug)]
    struct Foo {
        s: &'static str
    }
}

fn main() {
    let f = foo::Foo{s: "hello"};
    println!("{:?}", f);
}

Ancak bu çalışmayacaktır - "Foo yapısı gizlidir (struct Foo is private)" diye bir hata alacağız. Bunu çözmek için pub anahtar kelimesi aracılığıyla Foo'yu görünür kılmalıyız. Sonra bu hata "foo::Foo yapısının s alanı gizlidir (field s of struct foo::Foo is private)" olacaktır, pub anahtar kelimesini alanın başına eklemeliyiz ki Foo::s de görünür olsun. Sonra güzelinden çalışacaktır.


#![allow(unused)]
fn main() {
    pub struct Foo {
        pub s: &'static str
    }
}

Bir alanı açıkça pub olarak belirmek bir modulün içerisinden neyin ulaşılabilir olduğunu seçmek demektir. Bir modülün içerisindeki erişebilen tiplere ve fonksiyonlara modulün arayüzü (interface) denir.

Bir yapının içindekileri gizlemek ve erişimi metotlarla sağlamak çoğunlukla doğru bir tercihtir.

mod foo {
    #[derive(Debug)]
    pub struct Foo {
        s: &'static str
    }

    impl Foo {
        pub fn new(s: &'static str) -> Foo {
            Foo{s: s}
        }
    }
}

fn main() {
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

Neden bu yapıları gizlemek daha iyidir? Çünkü arayüzün canına okumadan ve modüle erişen diğer parçaların ayrıntılarıyla boğuşmadan onu değişebilirsiniz. Geniş ölçekli bir programın en büyük belası kodun birbirine girmeye olan meyilidir ki kodun gerekli parçasını izole etmeyi imkansız hâle getirir bu.

Cesur yeni dünyada modüller tek bir şeyi yapar ve kendi sırlarını kendilerine saklarlar.

Peki ne zaman gizlememeliyiz? Stroustrup'ın dediği gibi arayüzün kendisi kullanıldığı zaman, mesela struct Point{x: f32, y: f32}.

Bir modülün içinde bütün nesneler birbirine görünürler. Burada herkesin birbirini tanıdığı ve sırlarını bildiği bir mahalle yaşantısı vardır.

Herkesin programı çeşitli dosyalara ayırmaya başladığı bir sınırı vardır. Ben mesela 500 satıra geldiğimde bunu düşünmeye başlıyorum ancak hepimiz 2000 satırdan sonra sıkılırız.

Peki ya bir programı çeşitli dosyalara nasıl ayırırız?

foo kodunu foo.rs içine koyalım.


#![allow(unused)]
fn main() {
// foo.rs
#[derive(Debug)]
pub struct Foo {
    s: &'static str
}

impl Foo {
    pub fn new(s: &'static str) -> Foo {
        Foo{s: s}
    }
}
}

Ve sonra da ana dosyada mod foo deyimini bir blok olmadan kullanalım.

// mod3.rs
mod foo;

fn main() {
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

Şimdi rustc mod3.rs komutu herhangi bir hata olmadan derlenecektir. "Makefile"lar ile boğuşmaya hiç ihtiyacımız yok!

Ç.N: Makefile çoğunlukla C ve C++ ile kullanılan ancak Crystal, Go gibi yüksek seviye dillerde bile tercih edilen bir dosya. İşlevi birden çok kod dosyasını bir araya getirmek, onu yönetmektir.

Derleyici aynı zamanda MODULADI/mod.rs içine de bakacaktır, mesela ben boo isminde bir dizin açıp içerisine mod.rs diye bir dosya yerleştirebilirim:


#![allow(unused)]
fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}
}

Ana dosya bunu farklı bir dosyadaki farklı bir modül olarak tanımlayacaktır:

// mod3.rs
mod foo;
mod boo;

fn main() {
    let f = foo::Foo::new("hello");
    let res = boo::answer();
    println!("{:?} {}", f,res);
}

Şu ana kadar içinde main fonksiyonu olan bir mod3.rs dosyamızla beraber boo/mod.rs dosyamız da vardır ki diğer modüller bunu boo olarak görür. Genel alışkanlık, main fonksiyonunu barındıran dosyanın adını main.rs yapmaktır.

Neden bir şeyi yapmanın iki farklı yolu var? Çünkü boo/mod.rs aracılığıyla boo içerisinde yeni modüller tanımlayabilirsiniz. boo/mod.rs'yi değiştirelim ve yeni bir modül ekleyelim - bunu dışarıdan erişilebilir olmasına dikkat edin. (pub olmazsa bara sadece boo modülü içerisinden erişebilirsiniz.)


#![allow(unused)]
fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}

pub mod bar {
    pub fn question() -> &'static str {
        "the meaning of everything"
    }
}
}

Ç.N: Question: Soru, Answer: Cevap

Şimdi, cevabımızı anlamlandıracak bir sorumuz var. (bar modülü, boo'nun içindedir.)


#![allow(unused)]
fn main() {
let q = boo::bar::question();
}

Dilersek modül bloğunu boo/bar.rs altına taşıyabiliriz.


#![allow(unused)]
fn main() {
// boo/bar.rs
pub fn question() -> &'static str {
    "the meaning of everything"
}
}

Ve boo/mod.rs şuna dönüşür:


#![allow(unused)]
fn main() {
// boo/mod.rs
pub fn answer()->u32 {
    42
}

pub mod bar;
}

Sonuç olarak modüller organizasyon ve erişilebilirlikle alakalı ve tercihen başka dosyalara erişebilir.

Lütfen use'ın herhangi bir içe aktarma işleminde kullanılmadığını ve kısayol oluşturduğumuza not edin. Örneğin:


#![allow(unused)]
fn main() {
{
    use boo::bar;
    let q = bar::question();
    ...
}
{
    use boo::bar::question();
    let q = question();
    ...
}
}

Bir başka önemli nokta ise Rust'ta parçalı derleme işlemlerinin bulunmamasıdır. Ana program ve onun modül dosyaları sil baştan yeniden derlenir. Büyük programların derlenme süresini kayda değer bir sürede uzatır, rustc zaman içerisinde artımlı derlemede iyileşmesine rağmen.

Sandıklar

Ç.N: Crate kelimesinin yaygınlığından ötürü "sandık" ya da "crate" arasında aklımda uzunca bir süre düşündüm. Çünkü bu genel programlamaya ait bir kelime değil, Rust terminolojisinin bir parçası ki bu da onu çevrilmemesi gereken bir özel isim yapar. Ancak "Sandık" kelimesi gerçekten mantığa uygun ve "Sandık" olarak düşünmenin hiçbir zararı yok. "Crate" diyerek geçseydim, İngilizce bilmeyen kişiler için bunu salt ezberlenmesi gereken, anlamsız bir kelimeye dönüştürürdüm.

"Her bir derleme parçasına" sandık (crate) denir ki bu bir kütüphane veyahut çalıştırılabilir bir dosya olabilir.

Geçen bölümdeki dosyaları hep birlikte değil de ayrıca derlemek için, önce foo.rs'ı bir statik kütüphane sandığına çevirelim.

src$ rustc foo.rs --crate-type=lib
src$ ls -l libfoo.rlib
-rw-rw-r-- 1 steve steve 7888 Jan  5 13:35 libfoo.rlib

Şimdi bunu bizim ana programımıza ilişkilendirebiliriz. (linking)

src$ rustc mod4.rs --extern foo=libfoo.rlib

Ana programımızın bu yeni yapıya uyum sağlaması gerekmektedir, extern (Dışsal) ile kullandığımız isim ilişkilendirdiğimiz zaman kullandığımız isimle aynı olmalıdır. Yeni kütüphanemiz artık foo modülü aracılığıyla görünür olacaktır:

// mod4.rs
extern crate foo;

fn main() {
    let f = foo::Foo::new("hello");
    println!("{:?}", f);
}

İnsanlar "Cargo! Cargo!" diye zikre başlamadan önce Rust'ın inşa araçlarını neden bu kadar düşük seviyeden gösterdiğini anlatmam için bana izin verin. Ben "Aletlerin Fıkhı"na dahilim ve bunları bilmek sizin Cargo ile yeni projeler yönetirken daha az "sinir"le karşılaşmanızı sağlar. Modüller basit dil işlevleridir ve Cargo olmadan da kullabilirler.

Şimdi, Rust'ın çalıştırılabilir dosyaları neden bu kadar büyük onu anlayalım:

src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 3,4M Jan  5 13:39 mod4

Yarım dünya olmuş! Aslında bu çalıştırılabilir dosyada pek çok hata ayıklama bilgisi bulunur. Eğer niyetiniz bir hata ayıklayıcı kullanmaksa ve program paniklediği zaman anlamlı geri dönüşler almak istiyorsanız bu kötü bir şey değildir.

Hata ayıklama bilgisini silelim ve bir de böyle bakalım:

src$ strip mod4
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 300K Jan  5 13:49 mod4

Yine de basit bir şey için büyük bir dosya olduğunu düşünebilirsiniz ancak bu program Rust'ın standart kütüphanesine statik linklenmiştir. Bu iyi bir şey, bu programı doğru işletim sistemini kullanan herkesle paylaşabilirsiniz - Rust ile ilişkili hiçbir araca ihtiyaçları yoktur. (rustup sayesinde farklı işletim sistemlerine ve platformlara derleyebilirsiniz.)

Rust'ın kütüphanelerine dinamik linkleyebiliriz ki bu koşulda gerçekten küçük çalıştırılabilir dosyalar elde ederiz.

src$ rustc -C prefer-dynamic mod4.rs --extern foo=libfoo.rlib
src$ ls -lh mod4
-rwxrwxr-x 1 steve steve 14K Jan  5 13:53 mod4
src$ ldd mod4
    linux-vdso.so.1 =>  (0x00007fffa8746000)
    libstd-b4054fae3db32020.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3cd47aa000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f3cd4d72000)

"not found (bulunamadı)" çıktısının sebebi rustup'ın dinamik kütüphanelerinin sistem çapında kurulamamış olması. En azından Unix'te şöyle bir şey yapabiliriz. (Sembolik bağların en iyi çözüm olduğunu ben de biliyorum.)

src$ export LD_LIBRARY_PATH=~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib
src$ ./mod4
Foo { s: "hello" }

Teorik olarak Rust'ın dinamik linklemeyle ilgili herhangi bir sorunu yok, tıpkı Go gibi. Sadece her altı haftada yeni bir stabil sürüm yayınlandığı için her şeyi tekrar tekrar derlemek biraz tuhaf kaçacaktır. Eğer her şeyin sizin için uygun olacağı bir stabil sürüm bulursanız, bunda sorun olmayacaktır. İşletim sistemlerinin paket yöneticileri Rust'ın standart kütüphanelerini sunmaya başlayacağı zaman dinamik linkleme daha popüler olacaktır.

Cargo

Java veya Python ile kıyaslarsanız Rust'ın standart kütüphanesi o kadar da büyük değildir, tabii yine de çoğu şeyini işletim sistemi kütüphanelerinden alan C ve C++'dan daha fazla şey bulursunuz.

Bu durumu telafi etmek için Cargo aracılığı ile crates.io'da yayınlanan topluluk kütüphanelerine ulaşabilirsiniz. Cargo sizin için doğru sürümü arayacak, kaynağı indirecek ve diğer bağımlılıkların kurulduğunu da kontrol edecektir.

JSON okuyan basit bir program yapalım. Bu veri formatı yaygın olarak kullanılır ancak standart kütüphaneye eklenemeyecek kadar da karmaşıktır. Bundan ötürü yeni bir Cargo projesi açıyoruz, "--bin" de ekliyoruz ki çalıştırılabilir bir proje yapalım yoksa kütüphane projesi hazırlar.

Ç.N: Hayır hazırlamaz. Çevrilen belgenin eskiliğinden dolayı böyle bahsetmiş. Varsayılan davranış çalıştırılabilir proje hazırlamaktır, kütüphanesi projesi başlatmak için --lib kullanmanız gerekir. Yine de --bin kullanabilirsiniz ancak buna gerek yoktur. Bu arada bahsi geçen JSON sandığı bu yazının yazıldığı tarihe (8 şubat 2022) iki yıldır güncellenmemiş görünmektedir. Rust'ta kullanılan esas JSON çözümü serde_json sandığıdır. Yazının devamını okuyabilirsiniz çünkü serde_json ve json sandıkları arasında pratikte pek fark yoktur. Kaldı ki bu bölümün ardından yazar serde_json'u inceliyor.

test$ cargo init --bin test-json
     Created binary (application) project
test$ cd test-json
test$ cat Cargo.toml
[package]
name = "test-json"
version = "0.1.0"
authors = ["Your Name <you@example.org>"]

[dependencies]

JSON sandığını kullanan bir proje yapmak için "Cargo.toml" dosyasını düzenleyin:

[dependencies]
json="0.11.4"

Sonra Cargo ile ilk derlememizi yapalım:

test-json$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading json v0.11.4
   Compiling json v0.11.4
   Compiling test-json v0.1.0 (file:///home/steve/c/rust/test/test-json)
    Finished debug [unoptimized + debuginfo] target(s) in 1.75 secs

Programımızın main dosyası hâlihazırda oluşturdu - "src" dizinindeki "main.rs" dosyasıdır. Şimdilik henüz "hello world" çıktısı vermekten başka bir şeye yaramıyor, hadi onu doğru düzgün bir test programına çevirelim.

"raw" karakter dizesinin nasıl kullanıldığına da dikkat edin - eğer kullanmasaydık kaçış dizelerini kullanmamız gerekirdi ki bu bayağı bir çirkinliğe sebep olurdu:

// test-json/src/main.rs
extern crate json;

fn main() {
    let doc = json::parse(r#"
    {
        "code": 200,
        "success": true,
        "payload": {
            "features": [
                "awesome",
                "easyAPI",
                "lowLearningCurve"
            ]
        }
    }
    "#).expect("parse failed");

    println!("debug {:?}", doc);
    println!("display {}", doc);
}

Ç.N: Cargo aracılığıyla kurduğunuz sandıkların "extern crate sandık_adı" şeklinde çağrılmasına gerek yoktur.

Şimdi projeyi inşa edip çalıştırabilir - sadece main.rs değişti.

test-json$ cargo run
   Compiling test-json v0.1.0 (file:///home/steve/c/rust/test/test-json)
    Finished debug [unoptimized + debuginfo] target(s) in 0.21 secs
     Running `target/debug/test-json`
debug Object(Object { store: [("code", Number(Number { category: 1, exponent: 0, mantissa: 200 }),
 0, 1), ("success", Boolean(true), 0, 2), ("payload", Object(Object { store: [("features",
 Array([Short("awesome"), Short("easyAPI"), Short("lowLearningCurve")]), 0, 0)] }), 0, 0)] })
display {"code":200,"success":true,"payload":{"features":["awesome","easyAPI","lowLearningCurve"]}}

Hata ayıklama çıktısı JSON belgesi hakkında bazı iç detayları sundu ancak Display özelliğini kullanan sade {} bizim için taranmış JSON'u döner.

Şimdi JSON API'sini keşfe çıkalım. Eğer verileri dışarı çıkartamasaydık bunun pek anlamı olmazdı. as_TİP metotu bize Option<TİP> döner, bunun nedeni belirtilen alanın varlığı kesin değildir ve doğru tipe çevirmeyebiliriz.


#![allow(unused)]
fn main() {
    let code = doc["code"].as_u32().unwrap_or(0);
    let success = doc["success"].as_bool().unwrap_or(false);

    assert_eq!(code, 200);
    assert_eq!(success, true);

    let features = &doc["payload"]["features"];
    for v in features.members() {
        println!("{}", v.as_str().unwrap()); // MIGHT explode
    }
    // awesome
    // easyAPI
    // lowLearningCurve
}

features, JsonValue tipine bir referanstır - referans olması gerekir çünkü veriyi JSON dökümanı dışına taşımış oluruz. Ayrıca bir tür dizi olduğunu bildiğimiz için members() bize &JsonValue üzerinde çalışan dolu bir döngüleyici dönecektir.

Ya eğer "payload"'ın "features" diye bir anahtarı olmasaydı? O zaman features bir Null olurdu, elimizde patlamazdı. Bu yaklaşım biraz serbestlik tanıyor ki bu JSON'un gevşek doğasına da pekâlâ uyuyor. Belgenin yapısını incelemek ve yapı uyuşmazsa hataları idare etmek size kalmış.

Bu yapıları düzenleyebilirsiniz. Eğer let mut doc diye tanımlasaydık pekâlâ bunu yapabilirdik:


#![allow(unused)]
fn main() {
    let features = &mut doc["payload"]["features"];
    features.push("cargo!").expect("couldn't push");
}

push başarısız olabilir çünkü features bir dizi olmayabilir, bundan ötürü Result<()> döner.

JSON kalıplarını kullanmak için gerçekten şık bir makromuz da var:


#![allow(unused)]
fn main() {
    let data = object!{
        "name"    => "John Doe",
        "age"     => 30,
        "numbers" => array![10,53,553]
    };
    assert_eq!(
        data.dump(),
        r#"{"name":"John Doe","age":30,"numbers":[10,53,553]}"#
    );
}

Bunun çalışabilmesi için makroları JSON sandığından açıkça içe aktarmanız gerekir:


#![allow(unused)]
fn main() {
#[macro_use]
extern crate json;
}

Bu sandığın kötü bir tarafı da var, o da JSON'un dengesiz ve dinamik tipli doğasını Rust'ın statik ve yapılandırmış doğası arasında uyumsuzluğu dengeleyememiş olması. ("Readme" dosyasında bu sürtüşmeden ("friction") söz eder.) Eğer JSON'u Rust'ın veri yapılarına çevirmek isterseniz en sonunda pek çok düzenleme yapmanız gerekir çünkü elde ettiğiniz veriyi yapılarınıza uyacağınızdan emin olamazsınız! Bunun üstesinden gelebilmek için serde_json kullanabilirsiniz, bununla Rust veri yapılarınızı JSON'a serileştirebilir/çevirebilir (serialize) ya da JSON'dan Rust'a serisizleştirebilirsiniz/geri çevirebilirsiniz. (deserialize)

Ç.N: "Muvaffakiyetsizleştiricileştiriveremeyebileceklerimizdenmişsinizcesine" gibi bir karmaşa yarattığımın farkındayım. Bundan dolayı bazı yerlerde serialize ve deserialize için çevirmek karşılığını kullanacağım.

Bunun için cargo new --bin test-serde-json ile çalıştırılabilir bir Cargo projesi başlatalım ve test-serde-json dizinine girip Cargo.toml'u değiştirelim. Buna benzer bir şey yapabiliriz:

[dependencies]
serde="0.9"
serde_derive="0.9"
serde_json="0.9"

src/main.rs'yi şu şekilde dolduralım:

#[macro_use]
extern crate serde_derive;
extern crate serde_json;

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    address: Address,
    phones: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Address {
    street: String,
    city: String,
}

fn main() {
    let data = r#" {
     "name": "John Doe", "age": 43,
     "address": {"street": "main", "city":"Downtown"},
     "phones":["27726550023"]
    } "#;
    let p: Person = serde_json::from_str(data).expect("deserialize error");
    println!("Please call {} at the number {}", p.name, p.phones[0]);

    println!("{:#?}",p);
}

derive özelliğini daha çok görünüz ancak serde_derive sandığı içerisinde Serialize ve Deserialize gibi önemli özellikleri içeren derivelar bulunmaktadır. Sonuç, oluşturulan Rust yapısını gösterecektir:

Please call John Doe at the number 27726550023
Person {
    name: "John Doe",
    age: 43,
    address: Address {
        street: "main",
        city: "Downtown"
    },
    phones: [
        "27726550023"
    ]
}

Eğer bunu json sandığı ile yapsaydınız pek çok satırda çoğu hata yönetimiyle ilişkili birkaç yüz tanecik çeviri kodu yazmanız gerekecekti. Bunaltıcı, batırması bir hataya bakar ve bunun için oturup uğraşmak anlamsız.

Eğer dışarıdan gelen iyi yapılandırılmış JSON'u işleyecekseniz (gerekirse alanları yeniden isimlendirebilirsiniz) ve başka programlarla ağ üzerinden veri paylaşacaksanız (Şu günlerde JSON'u herkes anlıyor) bu bariz en iyi çözümdür. serde ("SERialization DEserialization") hakkındaki ilgi çeken bir nokta diğer dosya türlerini de desteklemesidir, mesela Cargo'da kullanılan yapılandırması kolay ve popüler toml da bunlardan birisidir. Eğer programınızın .toml dosyalarını yapılara çevirmesi gerekiyorsa bu tıpkı .json dosyalarında yaptığınız gibi bu yapıları hazırlayabilirsiniz.

Serileştirme, Java ve Go'da da benzerleri bulunan önemli bir tekniktir - ancak büyük bir fark vardır. Bu dillerde verinin yapısı çalışma zamanında yansıma (reflection) kullanılarak bulunur. Ancak bu koşulda serileştirme işlemi çalışma zamanında oluşturulur - bu çok daha verimlidir.

Cargo, Rust ekosisteminin ağır toplarından birisidir çünkü bizim için çok fazla işi halleder. Eğer o olmasaydı Github'dan tek tek kütüphaneleri indirmek, statik kütüphaneler olarak inşa etmek ve programa ilişkilendirmemiz gerekirdi. Bunu C++ projelerinde yapmak çiledir ve aynı çile Cargo olmasaydı Rust projelerinde de olacaktır. C++'ın çektirdiği çilenin eşi benzeri de yoktur bu arada, o yüzden diğer dillerin paket yöneticileriyle kıyaslamamız daha doğru olacaktır. (JavaScript için) npm, (Python için) pip sizin için bağımlılıkları kontrol eder ve indirir ancak programı dağıtmak zordur çünkü kullanıcının da sizin yerine NodeJS ve Python kurmuş olması gerekir. Ancak Rust programın bağımlılıkları statik linklenmiştir, ek bir bağımlılık gerekmeksizin istediğiniz kişiye yollayabilirsiniz.

Vali Kebabı

Basit bir yazının dışında herhangi bir veriyi işleme alacaksanız düzenli ifadeler (regular expressions) hayatınızı kolaylaştıracaktır. Bunlar pek çok dilde vardır ve sizin temel regex kalıplarına aşina olduğunuzu varsayıyorum. regex sandığını kullanmak için Cargo.toml dosyasına "[dependencies]" altına 'regex = "0.2.1"' ifadesini koymanız yererlidir.

Terk eğik çizgilerin özel anlamlar yaratmaması için "çiğ/raw karakter dizilerini" kullanacağım. İnsanın anlayacağı şekilde anlatırsak aşağıdaki düzenli ifade " ':' karakterinden önce iki rakam, sonrasında da herhangi uzunluktaki bir rakamı alın" anlamına gelmektedir:


#![allow(unused)]
fn main() {
extern crate regex;
use regex::Regex;

let re = Regex::new(r"(\d{2}):(\d+)").unwrap();
println!("{:?}", re.captures("  10:230"));
println!("{:?}", re.captures("[22:2]"));
println!("{:?}", re.captures("10:x23"));
// Some(Captures({0: Some("10:230"), 1: Some("10"), 2: Some("230")}))
// Some(Captures({0: Some("22:2"), 1: Some("22"), 2: Some("2")}))
// None
}

Başarılı bir çıktı üç parçadan oluşur - bütün eşleşme ile iki parça olarak sayılar. Düzenli ifadeler genel varsayılan olarak mıhlanmamıştır (anchored), yani regex ifademiz ilk eşleşmeyi arayacak ve gerisine bakmayacaktır. (Eğer sadece "()" olarak bir düzenli ifade yazarsanız her şeyle eşleşecektir.)

Bu eşleşmeleri isimlendirebiliriz ve düzenli ifadeleri birden fazla satır hâlinde yazabiliriz, satırları da içine alacak şekilde. Mesela burada sonucu ilişkisel bir dizi olarak kullanabiliriz ve eşleşmeleri isme göre arayabiliriz.


#![allow(unused)]
fn main() {
let re = Regex::new(r"(?x)
(?P<year>\d{4})  # the year
-
(?P<month>\d{2}) # the month
-
(?P<day>\d{2})   # the day
").expect("bad regex");
let caps = re.captures("2010-03-14").expect("match failed");

assert_eq!("2010", &caps["year"]);
assert_eq!("03", &caps["month"]);
assert_eq!("14", &caps["day"]);
}

Düzenli ifadeler karakter dizilerini örüntü eşleştirmelere göre bölebilir ancak ne anlama geldiğini anlayamaz. Mesela ISO-tarzı bir sözdizimini belirtip eşleşeyebilirsiniz, ancak anlamsal olarak saçma sapan şeylere işaret edebilirler. Mesela ki "2014-24-52".

Bu koşulda ayrıca tarih-zaman işlemeye ihtiyacınız olabilir, bu bize chrono tarafından sunulur. Tarihleri üretirken zaman dilimini belirtmeniz de gerekebilir:

extern crate chrono;
use chrono::*;

fn main() {
    let date = Local.ymd(2010,3,14);
    println!("date was {}", date);
}
// date was 2010-03-14+02:00

Ancak bu tarz kullanımda kötü tarihler şuna sebep olabilir: panik! (Mesela sahte bir tarih kullanmayı deneyebilirsiniz.) Esas ihtiyacınız olan metot ymd_opt'dir ki size LocalResult<Date> döner.


#![allow(unused)]
fn main() {
    let date = Local.ymd_opt(2010,3,14);
    println!("date was {:?}", date);
    // date was Single(2010-03-14+02:00)

    let date = Local.ymd_opt(2014,24,52);
    println!("date was {:?}", date);
    // date was None
}

Doğrudan tarihi ve zamanı tarayabilirsiniz, standart UTC biçiminde ya da farklı biçimler'da olabilir. Bu hemen hemen aynı formatlar istediğiniz tarzda biçimlendirmenize yardımcı olur.

Bu iki sandıktan özellikle bahsettiğm çünkü normalde bunlar diğer dillerde standart kütüphanenin birer parçasıdır. Ve üstelik bu kütüphanelerin çok çok ilkel bir tipi aslında Rust'ın standart kütüphanesinin parçasıydı, ancak ayrıştırıldı. Bu bilinçli bir karardı, Rust takımı standart kütüphanenin kararlılığını oldukça ciddiye alıyor ve ekleyecekleri yeni özellikleri önce kararsız "nightly" yayınında sonra yarı kararlı "beta" yayınında test ederler ve en sonunda "stable" yayınına alırlar. Pek çok deneyim ve iyileştirme gereken kütüphaneleri bağımsız olarak geliştirmek ve Cargo ile erişilir kılmak çok daha iyidir. Sonuca bakarsak, bu iki sandık standarttır - tekrardan standart kütüphaneye alınmayacaktır ve ortadan kaybolmayacaklardır.

Standart Kütüphane Konteynırları

Belgeleri Anlamak

Bu kısımda kabaca size Rust'ın standart kütüphanesinin bilindik bazı kısımlarını tanıtacağım. Belgelendirme gayet iyi ancak bağlamı tanıtmak ve biraz örneğin kimseye zararı olmaz.

Hepsinden önce Rust belgelerini okumak biraz yorucu gelebilir, bundan dolayı bir örneği inceleyeceğiz ki bu örnek Vec olacak. Kullanışlı bir tavsiye verelim, "[-]" belgeleri açıp kapamaya yarar. (Eğer rustup component add rust-src ile belgeleri indirmişseniz yanında bir de "[src]" bağlantısını göreceksiniz. Metotların bir krokisine buradan ulaşabilirsiniz.

Dikkat etmeniz gereken ilk detay, bütün ilişkili metotların Vec'in kendisinde tanımlanmadığıdır. Bunlar (çoğunlukla) push gibi vektörü değiştiren metotlardır. Bazı metotlar ise sadece vektörlerin içinde tuttuğu tiplere göre değişkenlik gösterir. Mesela, dedup'ı (kopyaları kaldır) sadece eşitliği denetlenebilir tipler üzerinde çalışır. Vec tipinde kullanılan birden fazla impl bloğu vardır ki bunlar içinde bulunduğu tiplerin çeşitliliğine göre şekillenmiştir.

Vec<T> ile &[T] arasında da özel bir ilişki olduğunu biliyoruz. Dilimler üzerinde çlışan her bir metot vektörler üzerinde doğrudan çalışacaktır, fazladan as_slice gibi metotlar kullanmanıza hiç gerek yoktur. Bu ilişki Deref<Target=[T]> ile gösterilir. Ayrıca bir vektörü referans olarak göstermek onu bir dilime çevirir - tip dönüşümlerinin nadiren gerçekleştiği nadir yerlerden birisidir. İlk öğeyi geri dönen first gibi dilim metotları, ya da bunun tersini yapan last, vektörler için de kullanılabilir. Metotların pek ciddi bir kısmı karakter dizilerini çağrıştırabilir, mesela split_at dilimi belirli bir indekse göre ayırır, starts_with bir vektörün belirli bir veri silsilesi ile başlayıp başlamadığını belirtir, contains bir vektörün belirli bir veriyi içerip içermediğini belirtir.

Belirli bir verinin indeksini bulmak için Rust'ta search metotu yoktur. Şimdi size size esas olayı anlatayım; eğer konteynırda metotu bulamazsanız, döngüleyici metotlarına bakın:


#![allow(unused)]
fn main() {
    let v = vec![10,20,30,40,50];
    assert_eq!(v.iter().position(|&i| i == 30).unwrap(), 2);
}

( & kullanmamızın sebebi döngüleyicinin referanslar üzerinde çalışmasıdır - alternatif olarak kıyaslamak için *i == 30 kullanabilirsiniz.)

Benzer şekilde vektörler üzerinde map metotu yoktur çünkü iter().map(...).collect ile aynı pekâlâ işi yapabilirsiniz. Rust, gerekmedikçe bellek tahsis etmeyi sevmez - çoğu zaman hâlihazırda bellekte yer tutan map'ın bütün sonuçlarına ihtiyacınız olmaz.

Döngüleyici (iterator) metotlarına aşina olmanızı tavsiye ederim çünkü iç içe girmiş döngülerle boğuşmadığınız iyi bir Rust kodu yazmak için elzemdirler. Her zaman olduğu gibi, büyük bir program yazarken bir anda onlarla güreşmek yerine döngüleyici metotlarını keşfetmek için minik programlar yazın.

Vec<T> ve &[T] metotları birbirleriyle ortak özellikleri (trait) paylaşırlar: vektörler kendi hata ayıklama bilgilerinin nasıl gösteirlebilirler. (Eğer bütün öğeler Debug özelliğine sahipse.) Aynı şekilde, eğer bütün öğeleri klonlanabilirlerse kendileri de klonlanabilirler. Drop özelliğine sahiptirler, bir vektör düşürüldüğü zaman bellekteki yerleri boşaltılır ve tek tek bütün öğeleri de düşürülür.

Extend özelliği döngüleyicilerdeki değerlerin bir döngü içerisine herhangi bir döngü kurmadan eklenebileceğini ifade eder.


#![allow(unused)]
fn main() {
v.extend([60,70,80].iter());
let mut strings = vec!["hello".to_string(), "dolly".to_string()];
strings.extend(["you","are","fine"].iter().map(|s| s.to_string()));
}

Aynı zamanda FromIterator özelliği de vektörlerin döngüleyicilerden inşa edilebileceğini ifade eder. (Döngülerin collect metotu bunu kullanır.)

Her konteynır döngülenebilir olmalıdır. Üç tarz-ı döngüleyiciyi hatırlayın.


#![allow(unused)]
fn main() {
for x in v {...} // returns T, consumes v
for x in &v {...} // returns &T
for x in &mut v {...} // returns &mut T
}

for deyimi IntoIterator üzerinde iş yapar ve buna bağlı olarak üç farklı kullanımı vardır.

Bir de Index (Bir vektörden okurken çalışan) bir de IndexMut (Bir vektörü düzenlerken çalışan) ile kontrol edilen indekslememiz vardır. Pek çok şey yapabiliriz çünkü v[0..2] gibi ifadelerle dilimlere indeksleyebilir ve dönebiliriz ya da sadece v[0] ile ilk elemana referans alabiliriz.

From özelliğinin de birtakım kullanımları vardır. Mesela Vec::from("hello".to_string()) size karakter dizelerinin özündeki Vec<u8> tipindeki vektörü verecektir. Ancak şunu düşünebilirsiniz, zaten String tipi için into_bytes diye bir vektör varken bunun ne özelliği var? Bir işi yapmanın birden çok yolu olması saçma değil mi? Ancak bu, özelliklerin (traits) genellenen metotlar oluşturması için gerekliliktir.

Bazen Rust'ın tip sisteminin kısıtlamalarından illallah edebilirsiniz. Mesela PartialEq boyutu 32'den az olan diziler için ayrıca tanımlanmıştır. (Bunu iyileştirecekler.) Bu vektörlerle dizileri doğrudan rahatça kıyaslamanızı sağlar ancak boyut sınırına dikkat etmelisiniz.

Belgelendirmenin diplerinde bazı gizli hazinelerle karşılaşabilirsiniz. Tıpkı Karol Kuczmarski'nin dediği gibi; "Kimse bu kadar arayıp taramaz.". Bir döngüleyicideki hataları nasıl yönetmelisiniz? Mesela bir döngüleyici üzerinde map kullandığınızda bazı öğeler sorun çıkarabilir ve size Result dönebilirler, böyle bir döngüleyici ile çalışacağınızı düşünün:

fn main() {
    let nums = ["5","52","65"];
    let iter = nums.iter().map(|s| s.parse::<i32>());
    let converted: Vec<_> = iter.collect();
    println!("{:?}",converted);
}
//[Ok(5), Ok(52), Ok(65)]

Yeterince iyi, ama tek tek bütün hataları kontrol etmeniz gerekiyor - dikkatlice! Ancak Rust bu işin doğrusunu yapar, eğer vektörün Result içerisinde barındırılmasını isterseniz - hepsi bu, eğer bir hata varsa bütün vektörü hatalı kabul edebiliriz.


#![allow(unused)]
fn main() {
    let converted: Result<Vec<_>,_> = iter.collect();
//Ok([5, 52, 65])
}

Ya dönüşüm başarısız olursa? İlk hatada işi fazla uzatmadan hemen Err döner. collect'in nasıl da esnek olduğuna dair iyi bir örnek olduğunu düşünebiliriz. (Tip bildirimini tuhaf bulabilirsiniz. Vec<_> kabaca bu bir vektör, Result<Vec<_>,_> herhangi bir vektörün Result tipi demektir. Siz ne istediğini belirttikten sonra Rust sizin yerinize işi çözer.)

Belgelendirmede epeyce detay var ancak ne olursa olsun C++'ın std::vector hakkındaki bilgilendirmesinden çok daha anlaşılır ve net.

Öğelerin gerektiği gereksinimler konteynırın üzerinde yapılan işlemlere dayanır. Çoğunlukla elemanın tipinin karşılanması ve düşürülebilir olması (drop) yeterlidir ancak bazı fonksiyonların katı gereksinimleri vardır.

C++'da kendi başınızın çaresine bakmanız gerekir. Rust'ın ilk başta her şeyi aleni olarak beklemesi sizi ürkütebilir ancak kısıtlamaları anlarken herhangi bir Vec metotunun gereksinimlerini de anlayacaksınız.

Kaynak kodlarını rustup component add rust-src ile okumanızı tavsiye ederim, standart kütüphanenin kodları oldukça okunaklıdır ve metotların içeriği tanımlarından çok daha anlaşılırdır.

Sözlükler (Maps)

Sözlükler (HashMap) dilediğiniz veriye bir anahtar ile ulaşabilmenizi sağlar. Aman aman bir fikir değil ve dilerseniz aynı şeyi demet dizisi ile yapabilirsiniz:


#![allow(unused)]
fn main() {
    let entries = [("one","eins"),("two","zwei"),("three","drei")];

    if let Some(val) = entries.iter().find(|t| t.0 == "two") {
        assert_eq!(val.1,"zwei");
    }
}

Küçük sözlükler ve sadece anahtar denkliği gerektiren durumlar için üstteki örnek iş görür, ancak içerisinde bir şey aramanın süresi doğru orantıya tabidir - sözlüğün büyüklüğü ile doğru orantılı.

Pek çok anahtar/veri çifti gerektiği zaman bir HashMap ile çalışmak çok çok daha verimlidir.


#![allow(unused)]
fn main() {
use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("one","eins");
map.insert("two","zwei");
map.insert("three","drei");

assert_eq! (map.contains_key("two"), true);
assert_eq! (map.get("two"), Some(&"zwei"));
}

&"zwei" mı? get ile verinin kendisini değil de referansını döndüğü için böyle bir şey görüyoruz. Eğer verinin tipi &str ise pekâlâ &&str alabiliriz. Alacağımız verinin referans olması gerekir çünkü çoğu zaman sahipli tiplerin değerlerini taşımak istemeyiz.

get_mut tıpkı get gibi çalışır ancak değişebilir bir referans döner. Şimdi karakter dizilerini sayılara çeviren bir sözlüğü inceleyelim ve "two" değerini güncellemeye çalışalım.


#![allow(unused)]
fn main() {
let mut map = HashMap::new();
map.insert("one",1);
map.insert("two",2);
map.insert("three",3);

println!("before {}", map.get("two").unwrap());

{
    let mut mref = map.get_mut("two").unwrap();
    *mref = 20;
}

println!("after {}", map.get("two").unwrap());
// before 2
// after 20
}

Referansı farklı bir bloğa aldığımıza dikkat edin - aksi taktirde sonuna kadar değişebilir bir referansımız olurdu ve Rust map, map.get("two") ile hiçbir şeyi ödünç almamıza izin vermezdi; değişebilir bir referans varken değişmez referanslara izin verilmez. (Eğer izin verilseydi, değişmez referansların geçerliliği şaibeli olurdu.) Bundan dolayı değişebilir referansı erkenden aradan çıkararak işi çözmüş oluyoruz.

Elbette bunun çok zarif bir API olduğunu söyleyemeyiz ama hatalara karşı daha dikkatli davranırız. Python olsa ters bir durumda hemen ekrana hata mesajları dizer ve C++ ise bize varsayılan veri dönerdi. (Aslında güzel bir çözüm ancak bazı sorunları var. Mesela a_map["two"] 0 döndüğü zaman "bulunamadı" mesajı ile gerçek sıfırın arasındaki farkı anlayamayız. Üstüne de fazladan bir girdi atanmış olur.)

Kimse unwrap kullanmaz, örneklerde öyle değil tabii. Gördüğünüz çoğu Rust kodu da bağımsız örneklerden oluştuğu için yaygın olarak kullanıldığı kanısına kapılabilirsiniz. Ancak çoğu zaman bir eşleşmenin kullanılması daha olasıdır:


#![allow(unused)]
fn main() {
if let Some(v) = map.get("two") {
    let res = v + 1;
    assert_eq!(res, 3);
}
...
match map.get_mut("two") {
    Some(mref) => *mref = 20,
    None => panic!("_now_ we can panic!")
}
}

Dilenirse anahtar/veri ikilileri üzerinde döngü kurabilirsiniz ancak belli bir sırası yoktur.


#![allow(unused)]
fn main() {
for (k,v) in map.iter() {
    println!("key {} value {}", k,v);
}
// key one value eins
// key three value drei
// key two value zwei
}

Ek olarak keys ve values'un döngüleyici dönen metotları vardır ki bu değerlerden vektör kullanmayı epeyce kolaylaştırır.

Örnek: Kelimeleri saymak

Metinleri anlamak için yapabileceğiniz keyifli işlerden birisi bir metinde kaç farklı kelime olduğunu sayabilmektir. Bir metni kelimelere bölmek split_whitespace ile oldukça kolaydır ancak noktalama işaretlerine özen göstermemiz gerekir. Bundan dolayı kelimeler sadece alfabetik karakterden oluşacak şekilde bölünmelidir. Üstelik kelimeler işleme tamamen küçük harfli olarak alınmalıdır.

Bir sözlükte içeriği değiştirecek tarzdan bir şey aramak kolaydır ancak arama başarısız olduğu zaman ne yapacağını belirtmek biraz tuhaf kaçabilir. Neyse ki hata koşulunu kontrol etmek için gayet zarif bir çözümümüz var:


#![allow(unused)]
fn main() {
let mut map = HashMap::new();

for s in text.split(|c: char| !c.is_alphabetic()) {
    let word = s.to_lowercase();
    let mut count = map.entry(word).or_insert(0);
    *count += 1;
}
}

Eğer aradığımız kelime sözlükte yoksa sözlüğe sıfır içeren yeni bir girdi yaratıyoruz ve onu sözlüğe sokuyoruz (insert). C++'daki sözlükler de aynen böyle çalışır tek fark burada varsayılan veri kendiliğinden gelmez ve net bir şekilde belirtilir.

Bu kapamada (closure) net bir tip belirttik ve tip de char oluyor. Bunun nedeni split tarafından kullanılan karakter dizilerinin Pattern özelliğinin tuhaflığıdır. Ancak Rust burada sözlüğün anahtar tipinin String, sözlüğün veri tipinin de i32 olduğunu çıkarabilir.

Gutenberg projesinden Sherlock Holmes'un maceraları'nı (The Adventures of Sherlock Holmes) kullanarak bunu güzelce test edebiliriz. (map.len() ile) Öğreniyoruz ki birbirinden farklı toplam 8071 kelime kullanılmış.

Peki ya en çok kullanılan yirmi kelimeyi nasıl öğrenebiliriz? Öncelikle sözlüğümüzü bir (anahtar, veri) formatında demetlerle dolu bir vektöre çevirebiliriz. (Bu mapı yok edecektir, çünkü into_iter kullandık)


#![allow(unused)]
fn main() {
let mut entries: Vec<_> = map.into_iter().collect();
}

Sonra bunları azalacak şekilde dizelim. sort_by, cmp metotunun sonuçlarını bekleyecektir ki bu metot sayı tiplerinde bulunur.


#![allow(unused)]
fn main() {
    entries.sort_by(|a,b| b.1.cmp(&a.1));
}

Ve bu sayıları ilk yirmi çıktıyı ekrana yazdıralım:


#![allow(unused)]
fn main() {
    for e in entries.iter().take(20) {
        println!("{} {}", e.0, e.1);
    }
}

(Sadece 0..20 üzerinde bir döngü kurabilirdiniz - bu kabul edilebilir ancak Rust'ın kendisine özgü tarzının dışına çıkmış olurduk - üstelik büyük döngüler için daha maliyetli olurdu.)

 38765
the 5810
and 3088
i 3038
to 2823
of 2778
a 2701
in 1823
that 1767
it 1749
you 1572
he 1486
was 1411
his 1159
is 1150
my 1007
have 929
with 877
as 863
had 830

Listenin başında bir tuhaflık sezdiniz mi? O aslında boş bir kelime. split metotu tek karaktere göre parçaladığı için iki noktalama işaretinin arasındaki boşlukklar da kelimeden sayılmış oldu.

Kümeler (Sets/HashSets)

Kümeleri sadece anahtarlarını umursadığınız sözlükler olarak düşünebilirsiniz, anahtarların karşılığı yoktur. Bundan dolayı insert sadece tek bir veri alır ve dilerseniz contains kullabilirsiniz.

Ç.N: Teknik olarak doğru olsa da buradaki tanımı karmaşık buldum. Kümeleri basitçe her verisi özgün olan, aynı veriyi ikinci kez almayan sırasız bir vektör gibi düşünebilirsiniz.

Diğer konteynırlar gibi bir döngüleyiciden HashSet oluşturabilirsiniz. collect ile bu işi yapabilirsiniz, tipi bildirdiğiniz sürece.

// set1.rs
use std::collections::HashSet;

fn make_set(words: &str) -> HashSet<&str> {
    words.split_whitespace().collect()
}

fn main() {
    let fruit = make_set("apple orange pear orange");

    println!("{:?}", fruit);
}
// {"orange", "pear", "apple"}

Aynı anahtarın tekrar girmiş olmanız (beklenildiği gibi) hiçbir etki oluşturmaz ve bir verideki sıralaması önemli değildir.

Matematikteki setlerle yaptığınız işlemleri pekâlâ Rust ile de yapabilirsiniz:


#![allow(unused)]
fn main() {
let fruit = make_set("apple orange pear");
let colours = make_set("brown purple orange yellow");

for c in fruit.intersection(&colours) {
    println!("{:?}",c);
}
// "orange"
}

Bütün işlemler döngüleyici döner ve collect kullanarak onları tekrardan sete çevirebilirsiniz.

İşte bir kısayol, vektörleri nasıl kullanıyorsak aynı şekilde kullanabiliriz.


#![allow(unused)]
fn main() {
use std::hash::Hash;

trait ToSet<T> {
    fn to_set(self) -> HashSet<T>;
}

impl <T,I> ToSet<T> for I
where T: Eq + Hash, I: Iterator<Item=T> {

    fn to_set(self) -> HashSet<T> {
       self.collect()
    }
}

...

let intersect = fruit.intersection(&colours).to_set();
}

Bütün Rust jeneriklerinde olduğu gibi burada da tipleri özelliklerle kısıtlamanız gereklidir - yukarıdaki kod sadece eşitliği (Eq) ve "hash fonksiyonu" (Hash) bulunan tipler için çalışır. Iterator diye bir tip bulunmadığını ve I'nın Iterator özelliğine sahip bir tip olması gerektiğini belirtiyoruz.

Standart kütüphane tiplerinine kendi metotlarımızı eklemek gözünüze biraz abartılı görünebilir ancak unutmayın ki kurallar var. Bunu sadece kendi özelliklerimize (trait) uygulayabiliriz. Eğer özelliğin ve yapının (struct) ikisi de aynı sandıktan geliyorsa (Mesela ki standart kütüphaneyi sunan "stdlib") bu tarz bir kullanıma izin verilmeyecektir. Bu şekilde bir dikkat dağınıklığından kurtulabiliyoruz.

Kendimizi bu zekice ve uygun kısayolu bulduğumuz için övmeye başlamadan önce yaratabileceği sonuçlara dikkat etmelisiniz. Eğer make_set aşağıdaki gibi kullanılırsa, ki burada sahipli bir tip olan String'in kümesi vardır, intersect'in tipi sizi epeyce bir şaşırtabilir:


#![allow(unused)]
fn main() {
fn make_set(words: &str) -> HashSet<String> {
    words.split_whitespace().map(|s| s.to_string()).collect()
}
...
// intersect is HashSet<&String>!
let intersect = fruit.intersection(&colours).to_set();
}

Rust sahipli karakter dizilerinin kopyalarını oluşturmadığı için aksi olamaz. intersect'in içerisinde fruitten ödünç alınmış tek bir &String bulunmakta. Bunun daha sonra size zorluk çıkaracağına yemin edebilirim, mesela ki yaşam sürelerini belirtmeye başalrken. Daha iyi bir çözüm, döngüleyicinin cloned metotunu kullanarak kesişim için kendi sahipli tiplerinizi üretmenizdir.


#![allow(unused)]
fn main() {
// intersect is HashSet<String> - much better
let intersect = fruit.intersection(&colours).cloned().to_set();
}

to_set'in daha iyi bir tanımı, self.cloned().collect() ile hazırlanabilir ki bir de bunu böyle denemenizi tavsiye ediyorum.

Örnek: İnteraktif Olarak Komut İşleme

Bir programın interaktif bir oturumu olması oldukça kullanışlı olabilir. Her bir satır kendi başına işleme alınır ve içindeki kelimelere bölünür; komut ilk bölümde yer alır ve geri kalan kelimeler ise komutun argümanları olur.

Bunun en akla yatan çözümlerinden birisi komut isimlerinden kapamalara (closure) ulaşılabilen bir sözlük inşa etmek olur. Peki ya nasıl kapamaları bir yerde barındıracağız? Hepsinin farklı boyutları olduğunu düşününce kulağa daha zor geliyor. En uygun çözüm, onların kopyalarını heap'a kutulamaktır (box):

Hadi deneyelim:


#![allow(unused)]
fn main() {
    let mut v = Vec::new();
    v.push(Box::new(|x| x * x));
    v.push(Box::new(|x| x / 2.0));

    for f in v.iter() {
        let res = f(1.0);
        println!("res {}", res);
    }
}

İkinci push kullanımında çok net bir hata alacağız:

  = note: expected type `[closure@closure4.rs:4:21: 4:28]`
  = note:    found type `[closure@closure4.rs:5:21: 5:28]`
note: no two closures, even if identical, have the same type

Ç.N: Aynı görünseler bile iki kapama asla aynı tipte olmayacaktır.

rustc gereğinden fazla spesifik bir tip çıkarımında bulundu, bundan dolayı vektörün içindeki tipi kendimiz kutulanmış özellik tipi (boxed trait type) olarak belirtmeliyiz:


#![allow(unused)]
fn main() {
    let mut v: Vec<Box<Fn(f64)->f64>> = Vec::new();
}

Şimdi kutulanmış kapamaları HashMap (sözlük) tipi için de kullanabiliriz. Kapamalar bulundukları ortamlardan veri çekebildikleri için yaşam sürelerini takip etmeliyiz.

FnMutu kullanmayı düşünebilirsiniz - çünkü yakaladıkları her türlü değişkenleri düzenleyebilirler. Ancak bir kapamaya tekabül eden birden fazla komutumuz bulunacağı için tekrar tekrar değişebilir referanslar alamazsınız.

Böylece kapamalar argümanlara değişebilir referanslar olarak erişir, karakter dizilerinin dilimleri de (&[&str]) satırdaki argümanları alır. Tasarladığımız yapıda geri dönüşü Result ile paketleyeceğiz - hata olarak en önce String kullanacağız.

D boyutu belli olan herhangi bir tipi gösterir.


#![allow(unused)]
fn main() {
type CliResult = Result<String,String>;

struct Cli<'a,D> {
    data: D,
    callbacks: HashMap<String, Box<Fn(&mut D,&[&str])->CliResult + 'a>>
}

impl<'a,D: Sized> Cli<'a,D> {
    fn new(data: D) -> Cli<'a,D> {
        Cli{data: data, callbacks: HashMap::new()}
    }

    fn cmd<F>(&mut self, name: &str, callback: F)
    where F: Fn(&mut D, &[&str])->CliResult + 'a {
        self.callbacks.insert(name.to_string(),Box::new(callback));
    }
}

cmd imzaya göre bir isim ve bir kapama alır, kapama kutulanmış ve sözlüğe girmiş olmalıdır. Fn ise çevreden verileri ödünç alabilir ancak düzenleyemez demektir. Bu tarz genelleme metotları en kötüsüdür, imzasına bakarken kafanız karışık ancak içeriği pirüpak anlaşılırdır! Yaşam ömrünü belirtmeyi unutmak burada en sık yapılan hatalardandır - Rust, çevresine kısıtlanmış kapamaların yaşam ömürlerini unutmanızı hoş görmeyecektir!

Şimdi komutları inceleyelim ve çalıştıralım:


#![allow(unused)]
fn main() {
    fn process(&mut self,line: &str) -> CliResult {
        let parts: Vec<_> = line.split_whitespace().collect();
        if parts.len() == 0 {
            return Ok("".to_string());
        }
        match self.callbacks.get(parts[0]) {
            Some(callback) => callback(&mut self.data,&parts[1..]),
            None => Err("no such command".to_string())
        }
    }

    fn go(&mut self) {
        let mut buff = String::new();
        while io::stdin().read_line(&mut buff).expect("error") > 0 {
            {
                let line = buff.trim_left();
                let res = self.process(line);
                println!("{:?}", res);

            }
            buff.clear();
        }
    }
}

Gayet anlaşılır - satırları kelimelere ayırıp bir vektörde topluyoruz, ardından sözlükte ilk kelimeyi aratıyoruz ve sözlüğün döndüğü kapamayı değişebilir verilerimizle ve kelimenin geri kalanlarıyla çağırıyoruz. Boş satırlar görmezden gelinir ve hata olarak değerlendirilmez.

Şimdi kapamalarımızın olumlu ve olumsuz sonuçlar dönmesini kolaylaştırmak için yardımcı fonksiyonlar tanımlayalım. Burada zekice ufak bir detay var; tanımladığımız genellenen fonksiyonların çalıştığı tipleri "String"e çevirebilir.


#![allow(unused)]
fn main() {
fn ok<T: ToString>(s: T) -> CliResult {
   Ok(s.to_string())
}

fn err<T: ToString>(s: T) -> CliResult {
   Err(s.to_string())
}
}

İşte karşımızda ana programımız var. "ok(answer)"ın nasıl çalıştığına dikkat edin - çünkü sayılar kendilerini nasıl karakter dizilerine çevrileceğini iyi bilirler!

use std::error::Error;

fn main() {
    println!("Welcome to the Interactive Prompt! ");

    struct Data {
        answer: i32
    }

    let mut cli = Cli::new(Data{answer: 42});

    cli.cmd("go",|data,args| {
        if args.len() == 0 { return err("need 1 argument"); }
        data.answer = match args[0].parse::<i32>() {
            Ok(n) => n,
            Err(e) => return err(e.description())
        };
        println!("got {:?}", args);
        ok(data.answer)
    });

    cli.cmd("show",|data,_| {
        ok(data.answer)
    });

    cli.go();
}

Hataları biraz uyduruk bir yoldan ele aldık ve bu tarz durumlarda soru işareti operatörünün nasıl çalıştığının inceleyeceğiz. Basitçe std::num::ParseIntError hatası std::errror::Errır özelliğini (trait) içeriyor ki bu bulunduğumuz bloğa description metotunu getiriyor - Rust özellikler erişilebilir olmadan üzerinde işlem yapmamıza izin vermez.

Ve çalıştıralım:

Welcome to the Interactive Prompt!
go 32
got ["32"]
Ok("32")
show
Ok("32")
goop one two three
Err("no such command")
go 42 one two three
got ["42", "one", "two", "three"]
Ok("42")
go boo!
Err("invalid digit found in string")

Denemek isteyeceğiniz pek çok iyileştirme olabilir. Mesela cmd komutuna yardım satırını içeren üçüncü bir argüman ekleyebilir, help komutuna bu üçüncü argümanla cevap verebilirdik. Ya da Cargo ile rustyline sandığını kullanarak komut düzenleme ve geçmişe dönmek konularını daha akılcı bir yoldan halledebiliriz.

Hataları Yönetmek

Hataları Yönetmenin Temelleri

Eğer soru işareti operatörünü kullanmazsanız Rust'ta hata yönetimi epey sıkıcı olabilir. Ancak bunu yapabilmek için bazen herhangi bir hatayı barındırabilecek bir Result tipi oluşturabilmemiz gerekir. Bütün hatalar std::error::Error dönebildiğine göre herhangi bir hatayı Box<Error> ile gösterebiliriz.

Düşünün ki hem girdi/çıktı işlemlerinden gelen hatayı hem de karakter dizisini sayıya çevirmekten gelen hatayı aynı fonksiyon içinde dönmek istiyoruz:


#![allow(unused)]
fn main() {
// box-error.rs
use std::fs::File;
use std::io::prelude::*;
use std::error::Error;

fn run(file: &str) -> Result<i32,Box<Error>> {
    let mut file = File::open(file)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents.trim().parse()?)
}
}

Burada girdi/çıktı işlemleri için iki farklı soru işareti operatörü kullanılıyor (Ya dosya açılamazsa? Ya Stringe çevrilemezse?) ve bir de çeviri için ayrıca bir soru işareti operatörü kullanıyoruz. En sonunda da sonucu Ok ile dönüyoruz. Rust, parse üzerinden dönen i32 tipi ile çalışabilir.

Result tipi için bir kısayolu oluşturmak da oldukça olaydır:


#![allow(unused)]
fn main() {
type BoxResult<T> = Result<T,Box<Error>>;
}

Ancak bizim programımızın kendisine özgü hata tipleri olacağı için kendi hata tiplerimizi hazırlamamız gerekecek. Bunun için gereken malzemeler:

  • Tercihen Debug
  • Bir tutam Display
  • Ve son olarak, olmazsa olmazımız Error

Eğer bunlar olmazsa hata tipiniz kafası nasıl eserse öyle çalışabilir.


#![allow(unused)]
fn main() {
// error1.rs
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError {
    details: String
}

impl MyError {
    fn new(msg: &str) -> MyError {
        MyError{details: msg.to_string()}
    }
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,"{}",self.details)
    }
}

impl Error for MyError {
    fn description(&self) -> &str {
        &self.details
    }
}

// a test function that returns our error result
fn raises_my_error(yes: bool) -> Result<(),MyError> {
    if yes {
        Err(MyError::new("borked"))
    } else {
        Ok(())
    }
}

Sürekli sürekli Result<T, MyError> yazmak biraz yorucu olduğu için çeşitli Rust modüllerinin kendi Result tipleri vardır. Mesela Result<T,io::Error> yazmak yerine io::Result<T> yazabilirsiniz.

Sonraki örneğimizde çevrilemeyecek bir karakter dizisini ondalıklı sayıya çevirirken karşımıza çıkan hatayı kontrol etmemiz gerekecek.

Şimdiye kadar ? ile elimizdeki ifadenin bir hata dönüp dönemeyeceğine bakarak çalıştı. Bunu belirleyen özellik (trait), From özelliğidir. Box<Error> ise From'a sahip bütün Error tiplerini kabul eder.

Devam etmeden önce kendi yarattığınız BoxResult isimlendirmesini kullanabilir ve her şeyi tek elde toplayabilirsiniz, bu kendi yarattığımız hata tipini Box<Error>'a döndürecektir. Ufak uygulamalar için pekâlâ iyi bir tercih olabilir. Ancak ben size diğer hata türlerini kendi hata türümüze dâhil edebileceğiniz daha iyi bir örnek göstereceğim.

ParseFloatError, Error'u içerdiğinden dolayı kendi içinde description()'un da tanımlanmış olması gerek.


#![allow(unused)]
fn main() {
use std::num::ParseFloatError;

impl From<ParseFloatError> for MyError {
    fn from(err: ParseFloatError) -> Self {
        MyError::new(err.description())
    }
}

// and test!
fn parse_f64(s: &str, yes: bool) -> Result<f64,MyError> {
    raises_my_error(yes)?;
    let x: f64 = s.parse()?;
    Ok(x)
}
}

İlk ? için pek bir olay yok. (From ile her tip kendisine dönüştürülür.) İkinci ? ise ParseFloatError hatasını MyError'a çevirir.

Ve sonuç:

fn main() {
    println!(" {:?}", parse_f64("42",false));
    println!(" {:?}", parse_f64("42",true));
    println!(" {:?}", parse_f64("?42",false));
}
//  Ok(42)
//  Err(MyError { details: "borked" })
//  Err(MyError { details: "invalid float literal" })

Birazcık işi yokuşa sürse de hiç de karmaşık değil. İşin tadını kaçıran kısım yazdığımız From dönüşümleri yazdığımız hataların bizim MyError ile iyi anlaşması gerektiği - ya da bunları hiç düşünmeyin Box<Error> kullanın olsun bitsin. Yeni başlayanlar tek bir şeyi birden fazla yapabilmenin yolunu görünce kafaları karışır. Bir avakadoyu soymanın (ya da yeterince manyaksanız bir kediyi yüzmenin) her zaman başka bir yolu vardır. Bu esnekliğin bedeli birden çok seçeneğe sahip olmaktır. Hata kontrolü 200 satırlık bir program için büyük bir programdan daha basit olabilir. Ve eğer bu kıymetli kod atıklarınızı bir Cargo paketine dönüştürmek isterseniz hata işleme çok daha kıymetli bir hâle gelir.

Şu an için soru işareti operatörü yalnızca Result için çalışmakta, Option için değil, ve bu bir özelliktir, bir kısıtlama değil. Option tipinin ok_or_else isminde kendisini Result'a dönüştüren bir metotu vardır. Mesela, düşünün ki bir HashMap'ta aradığımız anahtar bulunmuyor:


#![allow(unused)]
fn main() {
let val = map.get("my_key").ok_or_else(|| MyError::new("my_key not defined"))?;
}

Şimdi hatamız gayet anlaşılır bir şekilde dönmüş oldu! (Bu form içerisinde bir kapama kullanılıyor, böylece buradaki hata ancak gerekirse yaratılırsa olacak.)

Çeviri Notu: Yazarın dediğine karşın, daha sonra soru işareti operatörü Option tipine dönüşebilir bir şekilde güncellendi. Yani artık şu kullanım geçerlidir:

let val = map.get("my_key")?

get metotundan dönen None değerini olduğu gibi ya da bir hata dönmek size kalmış. İki durumunda kendince artıları ve eksileri var. Yazarın bahsettiği gibi, Rust'ta bir şeyi yapmanın birden fazla yolu var. Daha fazla bilgi için Rust'ın referans kitabını ya da kutsal kitabı inceleyebilirsiniz.

error-chain ve hatalarla baş etme sanatı

Çeviri notu: Acı bir şekilde bahsetmeliyim ki, error-chain isimli sandık 2019 gibi tedavülden kaldırıldı. Bunun yerine failure isminde alternatif bir sandığa kişiler yönlendirilmiş ancak o da 2020 yılı gibi tedavülden kaldırılmış. Benzer konseptleri sağlayan iki sandık var:

  • Anyhow: Kabaca hataların tipi ne olursa olsun yönlendirebileceğiniz bir tip sunuyor.
  • thiserror: Bir yapıyı derive(Error) gibi basit bir şekilde hata tipine dönüştürmeye yarar.

Yazının gerisini hem orijinal metni korumak hem belli başlı konseptleri tanıtmak hem de hata yönetiminin geçmişini göstermek için çeviriyorum.

Önemsiz olmayan uygulamalar için error_chain sandığına göz atmalısınız. Minik bir makro bu kadar faydalı olabilir.

cargo new test-error-chain komutuyla çalıştırılabilir bir sandık oluşturun ve oluşturulan dizinin içine girin. Cargo.toml'un sonuna error-chain="0.8.1"'i ekleyin.

error-chain'in olayı elle tek tek yazmanız gereken hata tiplerinin tanımlarını sizin yerinize yazmaktır; yapılar oluşturmak ve Display, Debug, Error gibi bir hata tipi yaratmak için kullanılan özellikleri eklemek. Aynı zamanda öntanımlı olarak From özelliği de dâhil edilir ki normal karakter dizileri de hatalara dönüştürülebilirler.

İlk src/main.rs dosyamız alttakine benzeyecektir. Main içerisinden run fonksiyonu çağrılıyor, hataları satır satır yazıyor ve programı sıfır olmayan bir çıkış kodu ile program sonlandırılıyor. Hepsi bu. error_chain makrosu ile bütün gerekli tanımlar üretilmiş olacaktır, errors modülünü gerekirse daha büyük programlarda kendi dosyasına yerleştirebilirsiniz. errors modülünü global kapsama dağıtmamız gerekti çünkü kodumuzun üretilmiş özellikleri (trait) görebilmesi gerekliydi. Varsayılan olarak, bir adet Error yapısı ve bu hatayla birlikte Result tanımlanacaktır.

Burada foreign_links kullanarak std::io::Error'ün From kullanarak bizim istediğimiz hata tipine dönüşmesini sağlıyoruz

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }
    }
}
use errors::*;

fn run() -> Result<()> {
    use std::fs::File;

    File::open("file")?;

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error: {}", e);

        std::process::exit(1);
    }
}
// error: No such file or directory (os error 2)

foreign_links hayatımızı oldukça kolaylaştırdı zira soru işareti operatörü artık std::io::Error'u nasıl error::Error'a dönüştüreceğini biliyor. (Kaputun altında tam da gerektiği gibi makromuz Form<std::io::Error> dönüşümü tanımlıyor.)

Bütün olay run içerisinde dönüyor; şimdi programa ilk argüman olarak verilen dosyanın ilk on satırını yazdırmayı deneyelim. Ortada verilmiş herhangi bir argüman olmayabilir, bunu bilemeyiz. Tek gereken şey Option<String>'i bir Result<String>'e dönüştürebilmek. Bunu yapabilmek için iki Option metotumuz var ve ben en basit olanını seçtim. Error tipimiz &str için From'u içerdiğinden basitçe bir karakter dizisiyle yeni bir hata oluşturabiliriz.


#![allow(unused)]
fn main() {
fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("provide a file"))?;

    let f = File::open(&file)?;
    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}
}

bail! isminde hata "fırlatmak" için kullanılan küçük ama etkili makromuzu da görelim. Bunun yerine ok_or kullanabilirdiniz:


#![allow(unused)]
fn main() {
   let file = match args().skip(1).next() {
       Some(s) => s,
       None => bail!("provide a file")
   };
}

Tıpkı ? gibi fonksiyondan erken döner. (early return)

Dönen hata içeriğinde ErrorKind isimli bir enum barındırır, bu bizi farklı türlü hataları seçebilmemizi sağlar. (Error::from(str) şeklinde oluşturduğunuz) her hata Msg isimli varyantla eşleşir. Foreign_links ile tanımladığımız Io ise Girdi/Çıktı hatalarıyla eşleşir:

fn main() {
    if let Err(e) = run() {
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}",s),
            &ErrorKind::Io(ref s) => println!("io {}",s),
        }
        std::process::exit(1);
    }
}
// $ cargo run
// msg provide a file
// $ cargo run foo
// io No such file or directory (os error 2)

Yeni tür hatalar eklemek de oldukça basittir. error_chain içerisine errors isimli bir kısım ekleyin:


#![allow(unused)]
fn main() {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }

        errors {
            NoArgument(t: String) {
                display("no argument provided: '{}'", t)
            }
        }

    }
}

Bu oluşturduğumuz yeni tür hata için Display'ın nasıl çalışacağını tanımlar. Ve şimdi "argüman yok" tarzı hataları daha spesifik bir şekilde tanımlamış olduk, ErrorKind::NoArgument'e bir String değeri verebiliriz:


#![allow(unused)]
fn main() {
    let file = args().skip(1).next()
        .ok_or(ErrorKind::NoArgument("filename needed".to_string()))?;
}

Şimdi eşleştirmeniz gereken yeni bir ErrorKind daha var:

fn main() {
    if let Err(e) = run() {
        println!("error {}",e);
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}", s),
            &ErrorKind::Io(ref s) => println!("io {}", s),
            &ErrorKind::NoArgument(ref s) => println!("no argument {:?}", s),
        }
        std::process::exit(1);
    }
}
// cargo run
// error no argument provided: 'filename needed'
// no argument "filename needed"

Genellikle mümkün olduğunca hataları spesifikleştirmek daha kullanışlıdır, bilhassa bu bir kütüphane fonksiyonuysa! Bu türe göre eşleştirme tekniği geleneksel hata yönetimine oldukça benzer, sadece burada except ya da catch blokları yerine eşleştirme yöntemlerini kullanıyoruz.

Sonuç olarak, error-chain sizin yerinize bir Error tipi oluşturur ve Result<T>'i std::result::Result<T, Error> olarak tanımlar. Error ise içeriğinde ErrorKind isimli bir enum barındırır ve varsayılan olarak karakter dizilerinden oluşan hatalarla eşleşen Msg'i barındırır. Harici hataları da iki farklı iş yapan foregin_links ile tanımlayabilirsiniz. Birincisi, yeni bir ErrorKind varyantı oluşturabilirsiniz. İkincisi dış hataları kendi hatamıza çeviren From tanımlarını hazırlar. Böylece kolaylıkla çeşitli hata türleri eklenebilir hâle gelir. Böylece artık kalıplaşmış olan pek çok koddan kurtulmuş oluyoruz.

Hataları Zincirlemek

Bu sandığın esas güzelliği hataları zincirlemek.

Bir kütüphane kullanıcısı olarak sadece gelişigüzel bir girdi/çıktı almak biraz can sıkar. Tamam, bir dosyayı açamadık. Ama hangi dosya? En basitinden, benim için önemli olan nokta nedir?

error_chain (Tr: Hata zinciri) bu tarz aşırı genelleme sorununa karşı hata zincirleme çözümünü sunar. Dosyayı açmak istediğimiz zaman tembelce ? kullanma alışkanlığımıza devam edebilir ve io::Error'a dönüştürebiliriz, ya da hatayı zincirleyebiliriz.


#![allow(unused)]
fn main() {
// non-specific error
let f = File::open(&file)?;

// a specific chained error
let f = File::open(&file).chain_err(|| "unable to read the damn file")?;
}

Şimdi programımızın foreign_links kullanmadan yazılan yeni bir versiyonuna bakalım.

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
    }

}
use errors::*;

fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("filename needed"))?;

    ///////// chain explicitly! ///////////
    let f = File::open(&file).chain_err(|| "unable to read the damn file")?;

    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line.chain_err(|| "cannot read a line")?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error {}", e);

        /////// look at the chain of errors... ///////
        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        std::process::exit(1);
    }
}
// $ cargo run foo
// error unable to read the damn file
// caused by: No such file or directory (os error 2)

Görmüş olduğunuz üzere chain_err metotu orijinal hatayı alıyor ve orijinal hatayı barındıran yeni bir hata yaratıyor - bu böyle sonsuza kadar gider. İlgili kapamalar hataya dönüştürülebilen herhangi bir veri dönebilir.

Rust makroları sizi pek çok şey yazmaktan kurtarabilir. error-chain'in main yerine geçebilecek ayrı bir makrosu bile vardır:

quick_main!(run);

(Zaten run bütün olayın gerçekleştiği yerdir.)

Sistem Süreçleri, Ağlar and Paylaşım

Değişemez olanı Değiştirmek

Eğer biraz dik kafalıysanız (anladığım kadarıyla) ve ödünç alma kurallarını nasıl es geçebilmenin bir yolu olup olmadığını kara kara düşünüyor olabilirsiniz.

Bu minik programı bir inceleyin, derlenecek ve hiçbir hata vermeyecektir.

// cell.rs
use std::cell::Cell;

fn main() {
    let answer = Cell::new(42);

    assert_eq!(answer.get(), 42);

    answer.set(77);

    assert_eq!(answer.get(), 77);
}

Evet, answer değişkeni değişebilir olarak belirtilmemesine rağmen içeriği değişti!

Bu gayet emniyetli, çünkü içeriğindeki Cell içindeki veri yalnızca set veya get ile erişilebilir. Bunun adı iç değişebilirliktir (interior mutability): Eğer bir v isminde bir yapı (struct) tanımladıysam v isimli yapı değiştirilebilirse v.a da değiştirilebilir olur. Cell, biraz bu kuralı rahatlatıyor çünkü set aracılığıyla içeriğindeki değeri değiştirebiliyoruz.

Fakat, Cell sadece Copy özelliğine sahip tiplerle çalışır. (Mesela kullanıcının tanımadığı Copy özelliğine sahip tiplerle veyahut ilkel tiplerle)

Farklı türden verilerle çalışmak için referansa ihtiyaç duyarız, değişebilir ya da değişemez olması fark etmeksizin. İşte bize RefCell bize bu konuda yarımcı olur - içerideki veriye açıkça bir şekilde referanslarla erişirsiniz.

// refcell.rs
use std::cell::RefCell;

fn main() {
    let greeting = RefCell::new("hello".to_string());

    assert_eq!(*greeting.borrow(), "hello");
    assert_eq!(greeting.borrow().len(), 5);

    *greeting.borrow_mut() = "hola".to_string();

    assert_eq!(*greeting.borrow(), "hola");
}

Dikkat edin, greeting değişebilir bir değişken olarak bildirilmedi!

Rust'ın dereferans operatörü bir tık kafa karıştırıcı olabilir, çünkü çoğu zaman buna ihtiyaç duymazsınız - mesela greeting.borrow().len() diye çağırsaydık metot kendiliğinden dereferans edeceği için sorun olmazdı. Ancak içeriden gelen greeting.borrow() üzerinden gelen &String veya greeting.borrow_mut() üzerinden gelen &mut String ile çalışmaya devam edebilmek adına * eklemeniz gerekecektir.

RefCell kullanmak her zaman emniyetli değildir, hâlen daha geri dönen metotların temel kurallara riayet etmesi gerekmektedir.


#![allow(unused)]
fn main() {
    let mut gr = greeting.borrow_mut(); // gr is a mutable borrow
    *gr = "hola".to_string();

    assert_eq!(*greeting.borrow(), "hola"); // <== we blow up here!
....
thread 'main' panicked at 'already mutably borrowed: BorrowError'
}

Eğer değişebilir bir referansınız hâlihazırda varsa tekrardan değişebilir referans oluşturamazsınız. Fakat bu sefer bu kural ihlali derleme zamanında değil çalışma zamanında anlaşılabilir. Çözüm, her zaman olduğu gibi, değişebilir referansları mümkün olduğunca az sayıda tutmaktır - örneğimizdeki kod için değişebilir referansımız gr'ı kod blokları içerisinde tutmayı düşünebilirsiniz böylece referansımız tekrar ödünç almadan önce düşürülmüş olur.

Bu özellik iyi bir sebebiniz olmadan kullanmak isteyeceğiniz bir şey değil çünkü hatalarınızı derleme zamanında almayacaksınız. Bu tipler, olağan kuralların sizin işinizi engellediği ama doğrusunu ypatığınız durumlarda size dinamik ödünç alma sunmak için vardır.

Paylaşılan Referanslar

Şimdiye dek, değer ve ödünç alınmış referanslar derleme zamanında açıkça biliniyordu. Veri sahiptir, referanslar ise onsuz var olamaz. Fakat her şey bu düzgün desen tasarımına uygun değildir, mesela düşünelim ki Rol ve Oyuncu diye iki yapımız (struct) var. Oyuncu, Rol objelerine referanslar bulunan bir vektör tutuyor. Burada veriler arasında net bir şekilde birebir eşleşme yok ve rustc'yi buna ikna etmek işleri epeyce karmaşıklaştıracaktır.

Rc tıpkı Box gibi çalışır - heap belleği tahsis edilir ve veri bunun içine taşınır. Eğer bir Box klonlarsanız, içindeki klonlanmış veriyi de beraberinde tutan bir bellek alanı tahsis eder. Fakat bir Rc klonlamak bilgisayar için daha kolaydır, çünkü her klonlama yapacağınız zaman sadece verinin referans sayısını arttırır. Bu, bellek yönetimi için eski ve bilindik bir yöntemdir; mesela iOS ve MacOSlarda kullanılan Objective C'nin çalışma zamanında kullanılır. (Ç.N: Eğer ilgiliyseniz "Automatic Referance Counting" diye bir araştırma yapabilirsiniz.) Modern C++'da bu özellike str::shared_ptr aracılığıyla bulunabilir.

Bir Rcnin içinde bulunduğu kapsam sona erdiği zaman referans sayımı da bir azaltılır. Eğer bu sayı sıfır olursa bellek boşaltılır ve sahiplenilmiş olan veri de düşürülür.

// rc1.rs
use std::rc::Rc;

fn main() {
    let s = "hello dolly".to_string();
    let rs1 = Rc::new(s); // s moves to heap; ref count 1
    let rs2 = rs1.clone(); // ref count 2

    println!("len {}, {}", rs1.len(), rs2.len());
} // both rs1 and rs2 drop, string dies.

Orijinal veriye istediğiniz kadar çok referans alabilirsiniz - bu daha önce bahsettiğimiz dinamik ödünç almadır. T verisi ve onun referansları olan &T'yi dikkatlice takip etmek zorunda değilsiniz. Bunun karşılığında makine çalışma zamanında biraz daha yorulmuş olur, bu yüzden ilk seçiminiz bu olmamalıdır; fakat bu şekilde ödünç alma mekanizmasının sert kurallarına ters düşebilecek paylaşım tasarıları kurgulayabilirsiniz. Dikkat edin ki Rc size değiştirilemez referanslar sağlayacaktır, aksi taktirde en basit ödünç alma kuralını ihlal ediyor olurduk. Bilirsiniz, huylu huyundan vazgeçmez.

Oyuncu örneğinde, rollerimizi Vec<Rc<Rol>> olarak tutabiliriz ve böylece rol ekleyip çıkartabiliriz ancak oluşturulduktan sonra rolleri değiştiremeyiz.

Fakat, ya Oyuncu başka bir takıma referanslar bulunduruyorsa ve takım oyuncu referanslarını tek bir vektör içinde tutuyorsa? Her şey değiştirilemez olur çünkü Oyuncu verilerinin Rc olarak depolanması gerekir! Bu durumda RefCell gerekli olur. Bu sefer bütün takım Vec<Rc<RefCell<Oyuncu>>> içinde tutulabilir. Oyuncuyu borrow_mut, aynı anda birden çok değişebilir referans tutmayacağımızdan emin olarak değiştirebiliriz. Mesela oyuncuya özel bir şeyler olursa bütün takımın güçleneceğine dair güçlü bir kuralımız var:


#![allow(unused)]
fn main() {
    for p in &self.team {
        p.borrow_mut().make_stronger();
    }
}

Kod fena değil, ancak tipler biraz ürkünç görünebilir. Her zaman type ile onlara daha basit isimler atayabiliriz:


#![allow(unused)]
fn main() {
type PlayerRef = Rc<RefCell<Player>>;
}

Çoklu Sistem Süreçleri

Yaklaşık bir yirmi yıldır, saf işlem hızından çoklu çekirdeklere bir geçiş var. Son teknoloji bilgisayarlarımızdan tamamen verim almak için bütün çekirdekleri kullanmalıyız. Bunun en iyi yolu arkaplanda alt süreçler oluşturmaktır, tıpkı daha önceden gördüğümüz Command ancak bu sefer bir senkranizasyon sorunumuz var; bu alt süreçleri beklemeden onların tamamlanıp tamamlanmadığından emin değiliz.

Ayrı iş süreçlerine ihtiyaç duymamızın tek sebebi bu değil elbette, bütün programı sırf bir girdi almak için bekletemezsiniz, mesela.

Alt süreçler üretmek oldukça basit, sadece arkaplanda işletilecek spawn için bir kapama hazırlayın.

// thread1.rs
use std::thread;
use std::time;

fn main() {
    thread::spawn(|| println!("hello"));
    thread::spawn(|| println!("dolly"));

    println!("so fine");
    // wait a little bit
    thread::sleep(time::Duration::from_millis(100));
}
// so fine
// hello
// dolly

Kod satırında gördüğünüz "wait a little bit", "az bekle" demektir ve pek de mantıklı bir çözüme benzemiyor. Bize dönen nesnelerin üzerinde join çağırmak biraz daha mantıklı, ana süreç bu süreçlerin tamamlanmasını bekleyecektir.

// thread2.rs
use std::thread;

fn main() {
    let t = thread::spawn(|| {
        println!("hello");
    });
    println!("wait {:?}", t.join());
}
// hello
// wait Ok(())

İşte başka bir tür acayiplik: Alt süreci paniklemeye zorlayalım:


#![allow(unused)]
fn main() {
    let t = thread::spawn(|| {
        println!("hello");
        panic!("I give up!");
    });
    println!("wait {:?}", t.join());
}

Beklediğimiz gibi panikledi, ancak sadece panikleyen süreç öldü! Ekrana hata mesajını join ile yazabiliyoruz, yani evet her paniğin sonu programın kapatılması değildir; ancak süreçler bilgisayar için yorucu bir işlemdir ve bu süreçleri kontrol etmenin bir yolu olarak görülmelidir.

hello
thread '<unnamed>' panicked at 'I give up!', thread2.rs:7
note: Run with `RUST_BACKTRACE=1` for a backtrace.
wait Err(Any)

Dönen objeler çeşitli alt süreçlerini takip için etmek için kullanılabilir.

// thread4.rs
use std::thread;

fn main() {
    let mut threads = Vec::new();

    for i in 0..5 {
        let t = thread::spawn(move || {
            println!("hello {}", i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().expect("thread failed");
    }
}
// hello 0
// hello 2
// hello 4
// hello 3
// hello 1

Rust joinden dönen sonucu ele almamız için bize ısrar eder, mesela alt süreç panikleyebilir. (Bu gerçekleştiği zaman genelde programın tamamını durdurmazsınız, sadece hataları not edersiniz, yeniden denersiniz vs.)

Alt süreçlerin işleyişi için belirli bir sıra yoktur (Program her çalışmada farklı bir sıra verir), ve buna esas noktadır - onlar gerçekten bağımsız yürütme süreçleridir (independent threads of execution. Çoklu süreçler kolaydır, esas olay eşzamanlılıktır (concurrency) - birden çok sürecin süreci senkronize etmek ve yönetmek.

Süreçler Ödünç Almaz

Süreç kapamaları dışarıdan veri alabilir, ancak taşıyarak, ödünç alarak değil!

// thread3.rs
use std::thread;

fn main() {
    let name = "dolly".to_string();
    let t = thread::spawn(|| {
        println!("hello {}", name);
    });
    println!("wait {:?}", t.join());
}

Ve işte karşınızda size yardımcı olan hata mesajı:

error[E0373]: closure may outlive the current function, but it borrows `name`, which is owned by the current function
 --> thread3.rs:6:27
  |
6 |     let t = thread::spawn(|| {
  |                           ^^ may outlive borrowed value `name`
7 |         println!("hello {}", name);
  |                             ---- `name` is borrowed here
  |
help: to force the closure to take ownership of `name` (and any other referenced variables), use the `move` keyword, as shown:
  |     let t = thread::spawn(move || {

Anlaşılabilir! Bir fonksiyon içerisinde üretilen süreci düşünün - fonksiyon çağrısı sona erdikten sonra bile çalışmaya devam eder ve name dürüşürülebilir. Kapamamıza move eklemek bu sorunu çözecektir.

Fakat bu bir taşımadır, yani name sadece bir süreçte var olabilr! Referansları paylaşmanın mümkün olduğunu vurgulamak istiyorum, ancak static ömre sahip olmalıdırlar:


#![allow(unused)]
fn main() {
let name = "dolly";
let t1 = thread::spawn(move || {
    println!("hello {}", name);
});
let t2 = thread::spawn(move || {
    println!("goodbye {}", name);
});
}

name, bütün programın çalışma süresince var olacaktır (static), bu yüzden rustc kapama çalıştığı sürece name değerinin de varlığından emin olacaktır. Ancak, havalı referansların static ömürleri yoktur!

Alt süreçler ortak bir ortamı paylaşamazlar - bu Rust'ın kendi tarzıdır. Biraz detay girersek, olağan referansları paylaşamazlar çünkü kapamalar yakaladıkları verileri taşırlar.

Yine de Paylaşılan referanslar fena değil çünkü yaşam ömürleri "gerektiği kadardır" - ama bunun için Rc kullanamazsınız. Çünkü Rc alt süreçler arası emniyete sahip değildir (thread safe) - sadece süreçlerin olmadığı anlar için hızlı olmaya optimize edilmiştir. Neyse ki Rc kullanırsanız derleme zamanında bir hata alırsınız, derleyici sizin arkanızı kollar.

Alt süreçler için std::sync::Arc kullanmalısınız - "Arc"ın açılımı "Atomic Referance Counting" yani "Atomik Referans Sayımıdır". Hepsi bu, bu dost her şeyin tek bir işlemde değiştirileceğini garanti eder. Bu garantiyi sağlamak için de işlem gerçekleştirilirken kilitlenir ve o an sadece bir sürecin erişimine izin verilir. clone kullanmak, bir kopya üretmekten bilgisayar için daha zahmetsizdir. (Ç.N: Buradaki clone, baştan aşağıya bellekte veri kopyalayan std::clone değil, referans klonlayan std::sync::Arc::clone metotudur.)

// thread5.rs
use std::thread;
use std::sync::Arc;

struct MyString(String);

impl MyString {
    fn new(s: &str) -> MyString {
        MyString(s.to_string())
    }
}

fn main() {
    let mut threads = Vec::new();
    let name = Arc::new(MyString::new("dolly"));

    for i in 0..5 {
        let tname = name.clone();
        let t = thread::spawn(move || {
            println!("hello {} count {}", tname.0, i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().expect("thread failed");
    }
}

Bilinçli olarak String barındıran bir tip oluşturdum, ("newtype" ya da yenitür) çünkü MyString, Clone özelliğini taşımayacak. Fakat paylaşılan referanslar klonlanabilir!

name'e ait paylaşılan referanslar her yeni alt sürece clone ile oluşturulan yeni referanslar olarak iletilir ve kapama içerisine taşınır. Biraz fazla kod yazdırıyor, ancak bu emniyetli bir örüntüdür. Emniyet, eşzamanlılık için epey önemlidir çünkü sorunlar pek öngörülebilir değildir. Program sizin bilgisayarınızda düzgünce çalışsa bile bir sunucuda patlayabilir, mesela haftasonu keyif yaparken. Daha da kötüsü, problemi semptomları bir tanı koymanıza yardımcı olmayabilir.

Kanallar

Süreçler arasında veri paylaşmanın çeşitli yolları var. Rust içerisinde, bunlardan birisi kanalları kullanmaktır. std::sync::mpsc::channel(), alıcı kanal ve verici kanal olmak üzere bize iki veri tutan bir demet sunar. Her bir alt sürece verici clone ile yollanır, ve referans üzerinde send çağrılır. Ana süreç ise bu esnada alıcı üzerinde recvi çağırır.

MPSC'nin açılımı "Multiple Producer Single Consumer"dır, yani "Çoklu üretici, tek tüketici". Kanala veri yollamaya teşebbüs eden birden çok altsüreç oluşturacağız, ana sürecimiz de kanalı "tüketecek".

// thread9.rs
use std::thread;
use std::sync::mpsc;

fn main() {
    let nthreads = 5;
    let (tx, rx) = mpsc::channel();

    for i in 0..nthreads {
        let tx = tx.clone();
        thread::spawn(move || {
            let response = format!("hello {}", i);
            tx.send(response).unwrap();
        });
    }

    for _ in 0..nthreads {
        println!("got {:?}", rx.recv());
    }
}
// got Ok("hello 0")
// got Ok("hello 1")
// got Ok("hello 3")
// got Ok("hello 4")
// got Ok("hello 2")

Bu sefer join kullanmaya ihtiyacımız yok çünkü alt süreçler kendilerini sonlandırmasından sonra cevaplarını dönecektir, fakat bu her an gerçekleşebilir. recv süreci kilitleyecektir ve eğer yollayıcı kanal devredışı kalacaksa bir hata dönecektir. recv_timeout ise belli bir süre boyunca bloklayacaktır ve ek olarak bir de zamanaşımı hatası dönebilecektir.

send ise süreci kilitlemeyecektir, bu faydalıdır çünkü süreçler mesajın alınmasını beklemeye gerek duymaksızın bir veriyi kanala atıp devam edecektir. Ek olarak, kanalın bir belleği vardır, böylece birden çok send metodu çalışabilir ve alıcıya sırasıyla mesaj iletilecektir.

Fakat, sürecin engellenmemesi aynı zamanda Ok değerinin mesajın başarıyla iletildiği anlamına gelmediğini de işaret eder.

sync_channel ise kanala veri yollarken süreci kilitler. Argüman sıfır olursa, alıcı mesajı recv ile alana kadar süreç kilitlenir. Süreçler ya buluşmalıdır ya da randevulaşmalıdır. (Her zaman yabancı kökenli kelimeler kulağa bir tık daha teknik ve doğru gelir.)


#![allow(unused)]
fn main() {
    let (tx, rx) = mpsc::sync_channel(0);

    let t1 = thread::spawn(move || {
        for i in 0..5 {
            tx.send(i).unwrap();
        }
    });

    for _ in 0..5 {
        let res = rx.recv().unwrap();
        println!("{}",res);
    }
    t1.join().unwrap();
}

Burada send kullanılmamışken recv kullanarak bir hataya kolayca sebep olabilir, mesela döngüyü for i in 0..5 ile kurmak yerine for i in 0..4 kullanarak. Süreç sona erer, tx düşer ve recv başarısız olur. Bu aynı zamanda bir süreç paniklediği zaman da gerçekleşir, stack yavaşça çözülür ve bütün veriler düşürülür.

Eğer sync_channel sıfır olmayan bir argümanla oluşturulursa, buna n diyelim, bu sefer en fazla n değeri alan bir sıra (queue) gibi davranır, send sadece sırada bekleyen nden fazla değer varsa süreci kilitleyecektir.

Kanallar güçlü tip (strongly type) mantığına uygundur, bu örnekte kanalın tipi i32dir, ancak tip çıkarımı bunu biraz gizler. Eğer farklı türden verilere ihtiyacınız varsa, numaralandırmalar (enum) bunu ifade etmek için uygundur.

Senkronizasyon

Senkranizasyona bakalım. join oldukça basit, tek işi bir iş parçacığı bitene kadar beklemek. sync_channel ise iki kanalı birbirine senkronize ediyor - son örneğimizde üretilen alt süreç ve ana süreç tamamen birbirine kilitlenmişti.

Bariyer senkronizasyonu, bütün süreçlerin bir noktaya geldiği zaman diğer süreçlerin beklemesini içerir, sonra yollarına devam ederler. Bariyer, beklemesini istediğimiz süreçlerin toplam sayısıyla oluşturulur. Daha önce olduğu gibi Arc aracılığıyla bu bariyeri diğer altsüreçlerle paylaşabilirsiniz.

// thread7.rs
use std::thread;
use std::sync::Arc;
use std::sync::Barrier;

fn main() {
    let nthreads = 5;
    let mut threads = Vec::new();
    let barrier = Arc::new(Barrier::new(nthreads));

    for i in 0..nthreads {
        let barrier = barrier.clone();
        let t = thread::spawn(move || {
            println!("before wait {}", i);
            barrier.wait();
            println!("after wait {}", i);
        });
        threads.push(t);
    }

    for t in threads {
        t.join().unwrap();
    }
}
// before wait 2
// before wait 0
// before wait 1
// before wait 3
// before wait 4
// after wait 4
// after wait 2
// after wait 3
// after wait 0
// after wait 1

Süreçler yine yarı-rastgele çalışırken bir anda birleşiyorlar ve sonra tekrar devam ediyorlar. Bu, devam ettirilebilir bir join gibidir ve bütün süreçlerin belli bir işi yaptıktan sonra o işle devam etmesini istediğinizde kullanışlı olabilir.

Paylaşılmış Durumlar

Süreçler, kendi paylaşılmış durum bilgisini nasıl düzenler?

Aklınıza dinamik olarak paylaşılan değişebilir referans almak için kullandığımız Rc<RefCell<T>> stratejisini getirin. RefCellin süreçlerde kullanılan muadili ise Mutex - değişken referansı lock kullanarak alabilirsiniz. Referans var olduğu müddetçe diğer süreçler veriye erişemeyecektir. mutex'in açılımı "Mutual Exclusion" yani "Karşılıklı Hariciyet" - bir sürecin erişmesi için kodun ilgili kısmını kilitliyoruz ve ardından kilidini açıyoruz. lock ile kilitlersiniz ve referans düşünce de kilit kalkar.

// thread9.rs
use std::thread;
use std::sync::Arc;
use std::sync::Mutex;

fn main() {
    let answer = Arc::new(Mutex::new(42));

    let answer_ref = answer.clone();
    let t = thread::spawn(move || {
        let mut answer = answer_ref.lock().unwrap();
        *answer = 55;
    });

    t.join().unwrap();

    let ar = answer.lock().unwrap();
    assert_eq!(*ar, 55);

}

RefCell kadar kolay değil çünkü eğer kilidin olduğu bir süreç paniklerse mutex daima kilitli kalabilir. (Böyle bir durumda, dokümentasyon açıkça süreci unwrap ile terk etmeniz gerektiğini söyler çünkü bir şeyler çok yanlış gitmiştir.)

Bu sefer değişebilir referansları mümkün olduğunca az tutmak çok daha önemli; çünkü bu mutex kapalı kaldıkça diğer süreçler de bloklanacaktır. Bu, kilitleyip bilgisayar için zor hesaplamaların yapmanın yeri bu değil! Yani, muhtemelen kodunuz şuna benzeyecek:


#![allow(unused)]
fn main() {
// ... do something in the thread
// get a locked reference and use it briefly!
{
    let mut data = data_ref.lock().unwrap();
    // modify data
}
//... continue with the thread
}

Yüksek Seviyeli İşlem

Belki de süreçleri yönetmenin daha yüksek seviyeli bir yolunu bulmak, süreçleri tek tek elle kontrol etmekten daha iyidir. Bunun bir örneği hesaplamaları paralel olarak yaptırmak ve sonuçları toplamak olabilir. Epey enteresan bir sandık olarak pipeliner1 sandığına bakabilirsiniz ki çok anlaşılır bir API'ya sahiptir. Deneysel bir "Merhaba Dünya"ye ne dersiniz? - bize çıktılar veren bir döngüleyici kurgulayalım ve n adet işlemi paralel olarak çalıştıralım: 1: Görünüşe göre pipeliner sandığı Şubat 2020'den beri güncelleme almamış

extern crate pipeliner;
use pipeliner::Pipeline;

fn main() {
    for result in (0..10).with_threads(4).map(|x| x + 1) {
        println!("result: {}", result);
    }
}
// result: 1
// result: 2
// result: 5
// result: 3
// result: 6
// result: 7
// result: 8
// result: 9
// result: 10
// result: 4

Salakça bir örnek olduğunun farkındayız, çünkü ilgili operasyon zaten bilgisayar için zahmetsiz ancak paralel işlemler gerçekleştirmenin ne derece kolay olabileceğini göstermiş oldum.

Daha kullanışlı bir şey yapalım. Ağ işlemlerini paralel olarak gerçekleştirmek faydalı olabilir, çünkü genellikle uzun zaman alırlar ve işe başlamak için hepsinin tamamlanmasını beklemeyi istemezsiniz.

Örneğimiz biraz kötü (emin olun yapmanın çok daha iyi yolları var) ancak ne işe odaklanın. 4. Bölümde tanımladığımız shell fonksiyonunu belli bir aralıktaki IP4 adreslerine ping atmak için tekrar kullanacağız:

extern crate pipeliner;
use pipeliner::Pipeline;

use std::process::Command;

fn shell(cmd: &str) -> (String,bool) {
    let cmd = format!("{} 2>&1",cmd);
    let output = Command::new("/bin/sh")
        .arg("-c")
        .arg(&cmd)
        .output()
        .expect("no shell?");
    (
        String::from_utf8_lossy(&output.stdout).trim_right().to_string(),
        output.status.success()
    )
}

fn main() {
    let addresses: Vec<_> = (1..40).map(|n| format!("ping -c1 192.168.0.{}",n)).collect();
    let n = addresses.len();

    for result in addresses.with_threads(n).map(|s| shell(&s)) {
        if result.1 {
            println!("got: {}", result.0);
        }
    }

}

Kendi ev ağımda sonuç şöyle bir şey:

got: PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.
64 bytes from 192.168.0.1: icmp_seq=1 ttl=64 time=43.2 ms

--- 192.168.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 43.284/43.284/43.284/0.000 ms
got: PING 192.168.0.18 (192.168.0.18) 56(84) bytes of data.
64 bytes from 192.168.0.18: icmp_seq=1 ttl=64 time=0.029 ms

--- 192.168.0.18 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.029/0.029/0.029/0.000 ms
got: PING 192.168.0.3 (192.168.0.3) 56(84) bytes of data.
64 bytes from 192.168.0.3: icmp_seq=1 ttl=64 time=110 ms

--- 192.168.0.3 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 110.008/110.008/110.008/0.000 ms
got: PING 192.168.0.5 (192.168.0.5) 56(84) bytes of data.
64 bytes from 192.168.0.5: icmp_seq=1 ttl=64 time=207 ms
...

Aktif adresler hızlıca bize bir saniyenin yarısı kadar bir sürede bize cevap verebiliyor, gerisi de olumsuz cevapları beklemek oluyor. Eğer paralel olarak çalıştırmasaydık, yaklaşık bir dakika boyunca sonuçları beklemek zorunda kalırdık! Ping zamanı gibi çıktıdan analiz ederek bulmayı düşünebilirsiniz, ancak bu sadece Linux üzerinde işe yarabilirdi. ping her sistemde vardır ancak çıktı her platform için değişiklik gösterebilir. Eğer Rust ile çalışacak evrensel br ağ API hizmeti tasarlamak isterseniz, ağlar kısmına geçiş yapabiliriz.

Adresleri Çözümlemenin Daha İyi Bir Yolu

Eğer sadece neyin aktif olduğu bilmek istiyorsanız ve gelişmiş ping istatistikleri ilginizi çekmiyorsa, std::net::ToSocketAddrs özelliği sizin için DNS çözümlemesi yapacaktır.

use std::net::*;

fn main() {
    for res in "google.com:80".to_socket_addrs().expect("bad") {
        println!("got {:?}", res);
    }
}
// got V4(216.58.223.14:80)
// got V6([2c0f:fb50:4002:803::200e]:80)

Bu bir döngüleyicidir çünkü genellikle bir alan adına birden çok arayüz bağlıdır, ikisi de Google'un arayüzleridir; birisi IPv4 ve diğeri IPv6 olmak üzere.

Şimdi pipeliner örneğimizi masumca bu metotu yeniden yazmak için kullanabiliriz. Çoğu ağ protokolü hem bir adres hem de bir port kullanır:

extern crate pipeliner;
use pipeliner::Pipeline;

use std::net::*;

fn main() {
    let addresses: Vec<_> = (1..40).map(|n| format!("192.168.0.{}:0",n)).collect();
    let n = addresses.len();

    for result in addresses.with_threads(n).map(|s| s.to_socket_addrs()) {
        println!("got: {:?}", result);
    }
}
// got: Ok(IntoIter([V4(192.168.0.1:0)]))
// got: Ok(IntoIter([V4(192.168.0.39:0)]))
// got: Ok(IntoIter([V4(192.168.0.2:0)]))
// got: Ok(IntoIter([V4(192.168.0.3:0)]))
// got: Ok(IntoIter([V4(192.168.0.5:0)]))
// ....

Bu, ping yollamaktan çok daha hızlıdır çünkü sadece bir IP adresinin geçerli olup olmadığını ölçüyoruz, eğer bir gerçek alan adlarının bir listesini kullansaydık DNS araştırması epey vakit alabilirdi ve bu paralleliğin nasıl önemli olabileceğini bize gösteriyor.

İlginç bir şekilde, bu "çalıştır ve unut" mantığında işliyor. Standart kütüphanede bulunan ve Debug barındıran her şey, hata ayıklamak için de müthiş keşifler sunar. Dönngüleyici Result (hâliyle Ok) dönüyor ve bu Result, IPv4 veyahut IPv6 varyantları olan bir numalandırma olan SocketAddr barındıran bir IntoIter içeriyor. Peki neden IntoIter? Çünkü bir soketin birden çok adresi olabilir, (hem IPv4 hem de IPv6 adresi olması gibi.)


#![allow(unused)]
fn main() {
    for result in addresses.with_threads(n)
        .map(|s| s.to_socket_addrs().unwrap().next().unwrap())
    {
        println!("got: {:?}", result);
    }
// got: V4(192.168.0.1:0)
// got: V4(192.168.0.39:0)
// got: V4(192.168.0.3:0)
}

Bu da çalışıyor, ilginç olarak, bizim basit örneğimiz kadar işe yarıyor. İlk unwrap Result'tan kurtuluyor ve döngüleyiciden ilk çıkan değeri dışarı çıkartıyor. Result genellikle bu durumda anlamsız adreslerde tetiklenir. (Mesela portu olmayan adres isimleri gibi.)

TCP İstemci Sunucusu

Rust, en çok kullanılan ve en yaygın ağ protokolü için gayet makul bir atayüz de sunar; TCP. TCP, hatalara karşı oldukça dayanıklıdır ve ağlarla örülü dünyamızın temel taşıdır - paketler onaylanarak gönderilir ve onaylanarak alınır. Bunun tersi olarak UDP ise paketleri hiçbir onay olmadan yollar. Bunun hakkında şöyle garabet bir espri vardır: "Sana UDP hakkında bir fıkra anlatabilirim ama muhtemelen kafan almayacak." (Ağlar hakkındaki espriler komiktir, sizin komikten ne anladığınıza göre değişir tabii.)

Fakat, hata kontrolü ağlarla uğraşırken çok önemlidir çünkü her an her şey bir anlığına oluşabilir.

TCP, bir istemci/sunucu modeliyle çalışır; sunucu adresi belli bir ağ portundan dinler ve istemci de sunucuya bağlanır. Bağlantı oluşturulduğunda ise istemci ve sunucu bir soket üzerinden haberleşebilir.

TcpStream::connect, SoccetAddr'a dönüştürülebilecek her şeyi kabul eder, kullandığımız düz karakter dizilerini de.

Rust'ta basit bir TCP istemcisi yazmak oldukça kolaydır - TcpStream yapısı hem yazılabilir hem de okunabilirdir. Her zaman olduğu gibi, özellikleri kullanmak için, Read, Write ve diğer std::io özelliklerini kapsamda görünür kılmalıyız.

// client.rs
use std::net::TcpStream;
use std::io::prelude::*;

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8000").expect("connection failed");

    write!(stream,"hello from the client!\n").expect("write failed");
 }

Sunucumuz pek karmaşık değil, bir dinleyici kuruyoruz ve bağlantıları bekliyoruz. Eğer bir istemci bağlanırsa, sunucu tarafında TcpStream elde ederiz. Bu örnekte sunucuya gelen her şey bir karakter dizisine yazılmış olur.

// server.rs
use std::net::TcpListener;
use std::io::prelude::*;

fn main() {

    let listener = TcpListener::bind("127.0.0.1:8000").expect("could not start server");

    // accept connections and get a TcpStream
    for connection in listener.incoming() {
        match connection {
            Ok(mut stream) => {
                let mut text = String::new();
                stream.read_to_string(&mut text).expect("read failed");
                println!("got '{}'", text);
            }
            Err(e) => { println!("connection failed {}", e); }
        }
    }
}

Port numarasını aşağı yukarı rastgele geçtim, ancak pek çok port özel anlamlar barındırır.

İki tarafında protokol üzerinde uzlaştığına dikkat edin. İstemci akışa yazı yazabileceğini düşünür ve sunucu da akıştan yazıyı okuyabileceğini umar. Eğer oyunu aynı kurallarla oynamazlarsa, bir tarafın engellendiği ve hiç gelmeyecek bir cevabı beklediği durumlar oluşur.

Hataların kontrolü önemlidir - ağ girdi çıktıları çeşitli sebeplerden aksayabilir ve hatalar dolunayda dosya sisteminin kurtadama dönüşmesi gibi acayip sebeplerlerl de geçerkleşebilir. Birileri kablolarla ip atlayabilir, öbür tarafın bilgisayarı çökebilir, falan fistan. Yazdığımız ufak sunucu çok da sağlam değil, çünkü ilk hata çökecektir.

Hataları düzgünce ele alan güçlü bir sunucu aşağıda bulunmaktadır. Akıştan bir satırı bir io::BufRead üreten bir io::BufReader aracılığıyla okur ve böylece çıktı üzerinde read_line çağırabiliriz.

// server2.rs
use std::net::{TcpListener, TcpStream};
use std::io::prelude::*;
use std::io;

fn handle_connection(stream: TcpStream) -> io::Result<()>{
    let mut rdr = io::BufReader::new(stream);
    let mut text = String::new();
    rdr.read_line(&mut text)?;
    println!("got '{}'", text.trim_right());
    Ok(())
}

fn main() {

    let listener = TcpListener::bind("127.0.0.1:8000").expect("could not start server");

    // accept connections and get a TcpStream
    for connection in listener.incoming() {
        match connection {
            Ok(stream) => {
                if let Err(e) = handle_connection(stream) {
                    println!("error {:?}", e);
                }
            }
            Err(e) => { print!("connection failed {}\n", e); }
        }
    }
}

handle_connection içindeki read_line çökebilir ancak sonuçta hata emniyetli bir şekilde kontrol edilmiş olur.

Bu tarz tek yönlü iletişimler bazen kullanışlı olabilir - bu örnekte olduğu gibi. Ağda bulunan servislerin durumlarını, merkezi bir yerde toplamış oluyoruz. Fakat kibarca bir şekilde geri dönüş yapsak iyi olur, en azından "ok" deyip geçelim.

İşte bir "eko" sunucusu. İstemci, sonunda satır sonu işareti olan bir metni sunucuya yollar ve sunucuda ek bir satır sonu ekleyerek geri yollar - akış okunabilir ve yazılabilirdir.

// client_echo.rs
use std::io::prelude::*;
use std::net::TcpStream;

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:8000").expect("connection failed");
    let msg = "hello from the client!";

    write!(stream,"{}\n", msg).expect("write failed");

    let mut resp = String::new();
    stream.read_to_string(&mut resp).expect("read failed");
    let text = resp.trim_right();
    assert_eq!(msg,text);
}

Sunucu şimdi ilginç bir hâl aldı. Sadece handle_connectionu değiştirelim:


#![allow(unused)]
fn main() {
fn handle_connection(stream: TcpStream) -> io::Result<()>{
    let mut ostream = stream.try_clone()?;
    let mut rdr = io::BufReader::new(stream);
    let mut text = String::new();
    rdr.read_line(&mut text)?;
    ostream.write_all(text.as_bytes())?;
    Ok(())
}
}

Bu yaygın olarak kullanılan yaygın br çift taraflı soket iletişimidir; BufReader'a yollamak için bir satır istiyoruz - fakat "BufReader" akışı tüketiyor! Bu yüzden akışı klonlamalıyız ve aynı soketi işaret eden yeni bir yapı oluşturmalıyız. Böylece nihayet huzuru elde ediyoruz.

Rust ve Nesne Yönelimli Programlama

Her disiplinden birileri geliyor ve Nesne Yönelimli bir dilden gelmeniz olasılığınız oldukça yüksektir:

  • "Sınıf"lar nesne (kimi zaman örnek (instance) deriz) üreten fabrikalar gibi çalışırlar.
  • Sınılar diğer sınıfların (üst sınıf/ebeveyn) alanlarını (field) ve davranışlarını (metotlar) miras (inheritance) alabilir.
  • Eğer B, A'dan miras alıyorsa, o zaman B aynı zamanda A olarak kabul edilebilir. (alttip/subtyping)
  • Nesne kendi verilerini gizlemelidir (kapsülleme/encapsulation), sadece metotlarıyla etkileşime geçmelidir. Nesne yönelimli tasarım sınıfların (yani isimleri) ve yöntemlerin (yani sıfatları) tanımlandığı anlayıştır. Aynı zamanda nesne yönelimli tasarımda bunlar arasındaki ilişkiler de tanımlanır ve bu ilişkiler sahip olmak (has-a) ve onun türünden olmak (is-a) olarak tanımlanır.

Eski Star Trek serilerinde doktorun kaptana "Bu bir yaşam Jim, sadece bizim bildiğimiz yaşam değil" (It's Life, Jim, just not Life as we know it) dediği bir an vardır. Bu deyim aynen olduğu gibi Rust'taki nesne yönelimi anlayışını da açıklıyor: Önce "Bu ne ya?" diyorsunuz, çünkü Rust'taki veri yapıları (yapılar, numaralandırmalar ve demetler) pek de uçan kaçan tarzda değiller. Onlara metot tanımlayabilirsiniz, verinin kendisini gizleyebilirsiniz ve bütün kapsülleme yöntemlerini kullanabilirsiniz ancak her birisi birbiriyle alakasız tiplerdir. Alt tipler yoktur, miras alma yöntemi yoktur. (Sadece Deref zorlamaları miras olarak kabul edilebilir 1)

1

Çevirmenin yorumu: Nesne yönelimli programlamada miras alan tip, doğal olarak miras aldığı tip olduğu kabul edilir. Rust'ta bir literatür farklılığı vardır, o da tipin aslında o olmadığı ancak zorlanarak dönüştürüldüğü vurgusudur. Yani bu bir miras alma mıdır? Evet, belki de. Ancak Rust, "coercion" kelimesi ile Rust'ın bir tipi başka bir tipe zorla dönüştürdüğünü vurgulamaktadır.

Çeşitli tipler arasındaki ilişkiler özellikler (trait) ile inşa edilir. Rust öğrenme sürecinin büyük bir kısmı standart kütüphanede bulunan özelliklerin nasıl çalıştığını anlamaktan geçer çünkü bu bütün verileri birbiriyle çalışmasına izin veren ve onlara anlamlar yükleyen bir sistemdir.

Özellik sistemi ilginçtir çünkü ana akım programlama dillerinde birebir benzeri yoktur. Tabii, bu dinamiklik ya da statiklik arasındaki farklı düşünürsek. Dinamik olarak Java ve Go arayüzlerine (interface) benzerler.

Özellik Nesneleri

Özellikleri anlatırken kullandığımız ilk örneği düşünün:


#![allow(unused)]
fn main() {
trait Show {
fn show(&self) -> String;
}

impl Show for i32 {
fn show(&self) -> String {
format!("four-byte signed {}", self)
}
}

impl Show for f64 {
fn show(&self) -> String {
format!("eight-byte float {}", self)
}
}
}

Bu da büyük impl bloklarıyla beraber ufak bir program:

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

fn main() {
let answer = 42;
let maybe_pi = 3.14;
let v: Vec<&Show> = vec![&answer,&maybe_pi];
for d in v.iter() {
println!("show {}",d.show());
}
}
// show four-byte signed 42
// show eight-byte float 3.14

Burası Rust'ın tip bildirimi gerektirdiği bazı nadir noktalardan birisi - Show özelliğini içeren herhangi bir vektörü açıkça istemek durumundayım. i32 ve f64 arasında hiçbir şey bağlantı olmadığına dikkat edin, fakat ikisi de show metotuna sahip çünkü ikisi de aynı özelliğe sahip. Bu sanal (virtual) bir metottur, çünkü bu metot her tip için farklı bir kod çalıştırır ve çalışma zamanındaki duruma göre doğru yöntem çağrılır. Bu referanslara özellik nesneleri denir.

Ve bu farklı tipleri nasıl aynı vektöre koyabileceğinizin yoludur. Eğer Java veyahut Go temeliniz varsa, Show'u bir arayüz (interface) olarak düşünebilirsiniz.

Biraz daha kurcalayalım ve değerleri bir Box işaretçisi içine koyalım. Box, heapta tahsis edilmiş alana yerleştirilen verinin referansını referansını temsil eder ve bir ödünç alma gibi çalışır - bu bir akıllı işaretçidir (smart pointer). Box ortadan kalktığı zaman Drop devreye girer ve bellek serbest bırakır.


#![allow(unused)]
fn main() {
trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

let answer = Box::new(42);
let maybe_pi = Box::new(3.14);

let show_list: Vec<Box<Show>> = vec![answer,maybe_pi];
for d in &show_list {
println!("show {}",d.show());
}
// show four-byte signed 42
// show eight-byte float 3.14
}

Buradaki fark, bu şekilde bu vektörü alıp bir yere referans takibi yapmaksızın başka yerlere ödünç verebilirsiniz. Vektör düşürüldüğü zaman, Box nesneleri de düşürülür ve bellek yeniden tahsis edilebilir.

Hayvanat Bahçesi

Nesne yönelimli programlamadan ve miras alma işleminden bahsettiğimiz ilk andan itibaren hayvanlardan konuşmaya başlarız. Kulağa da fena gelmez, "Bak işte, kedi bir etoburdur ve etoburlar bir hayvandır". Ruby evreninden klasik bir sloganı analım: "Eğer vaklıyorsa, o bir ördektir". quack (vak!) metotuna sahip bütün nesneler ördek olarak tanımlanabilir, biraz kulağa tuhaf gelse de.


#![allow(unused)]

fn main() {
trait Quack {
fn quack(&self);
}

struct Duck ();

impl Quack for Duck {
fn quack(&self) {
println!("quack!");
}
}

struct RandomBird {
is_a_parrot: bool
}

impl Quack for RandomBird {
fn quack(&self) {
if !self.is_a_parrot {
println!("quack!");
} else {
println!("squawk!");
}
}
}

let duck1 = Duck();
let duck2 = RandomBird{is_a_parrot: false};
let parrot = RandomBird{is_a_parrot: true};

let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot];

for d in &ducks {
d.quack();
}
// quack!
// quack!
// squawk!
}

Elimizde iki farklı tip var (o kadar işlevsiz ki hiçbir veri tutmuyorlar), ve evet, hepsinin quack() metotu var. Bir tanesinin davranışı bir ördek için azıcık tuhaf, ancak yine de hepsi ortak bir metot ismini paylaşıyor ve Rust hepsini tip emniyetli bir şekilde bir araya getiriyor.

Tip emniyeti müthiş bir şey. Eğer tip emniyeti olmasaydı, ortalığı çalışma zamanında darmaduman edecek bir kediyi vakvak kardeşlerin arasına sokabilirdik.

Şimdi saçma bir şey yapalım:


#![allow(unused)]
fn main() {
// and why the hell not!
impl Quack for i32 {
fn quack(&self) {
for i in 0..*self {
print!("quack {} ",i);
}
println!("");
}
}

let int = 4;

let ducks: Vec<&Quack> = vec![&duck1,&duck2,&parrot,&int];
...
// quack!
// quack!
// squawk!
// quack 0 quack 1 quack 2 quack 3
}

Ne diyebilirim ki? Vaklıyorsa ördektir. İlginç olan şey, özellikleri herhangi bir şeye ekleyebilirsiniz, sadece "nesnelere" değil. (quack bir referans olarak iletildiği için, defererans operatörü ile sayıyı alabilirsiniz.)

Yine de bunu sadece kendi sandığınızda tanımladığınız tipler üzerinde veya o sandıkta tanımlanmış özelliklerle yapabilirsiniz. Yani standart kütüphaneyi yamalı bohçaya (monkey patch) çeviremezsini, ki bu da Ruby vatandaşlarının başka bir alışkanlığı. (Çok da bayılan bir şey değildir doğrusu.)

Şimdiye kadar Quack bir Java arayüzü gibi davrandı ve isterseniz modern Java tipleri arayüzleri gibi çeşitli uygulamaları eğer gereken metotları sağladıysanız tipinize ekleyebilirsiniz. (Iterator özelliğini hatırlayın.)

Şimdiye kadar, özellikler bir tip tanımının parçası değildi ve isterseniz özellikleri istediğiniz tipe ekleyebilirsiniz, ancak aynı sandık kısıtlamasını gözden kaçırmayın.

Quack özelliğine sahip bütün nesneleri referans olarak da görebilirsiniz:


#![allow(unused)]
fn main() {
fn quack_ref (q: &Quack) {
q.quack();
}

quack_ref(&d);
}

İşte bu Rust'ın anladığı şekilde alttiplemedir.

Burada "Programlama Dilleri Kapıştırmaca Dersleri" verdiğimiz için, Go'nun ilginç vaklama yaklaşımını da not etmek istiyorum. Eğer Go'da tanımlanmış bir Quack arayüzü varsa ve bir tipin quack metotu varsa, o tip Quack arayüzünü açık bir tanıma gerek duymadan kendisine implement eder. Bu Java'nın "Her şey tek tek tanımlanacak!" yaklaşımına ters düşer ve tip emnyetini biraz tehlikeye soksa da "ördek tiplemeye" izin verir. (Duck-typing)

Fakat ördek tipleme ile alakalı bir sorun var. Kötü bir nesne yönelimli programlamanın ilk işaretçisi çok fazla metotun run gibi her kalıba girebilecek isimlere sahip olmasıdır. "Eğer run() (Çalıştır) metotu varsa, o zaman Runnable (Çalıştırılabilir) olmalıdır" vaklıyorsa o ördektir kadar zarif gelmiyor. Yani istemeden bir Go arayüzü o tipte tanımlı olabilir. Rust için mesela, Debug ve Display aynı anda fmt metotunu barındırır, fakar iki özelliğin çok farklı anlamları vardır.

Yani, Rust özellikleri bildiğimiz anlamda çok biçimli nesne yönelimli programlamaya izin verir. Peki ya miras? İnsanlar genellikle "Miras alma"yı kastederken Rust genellikle "Arayüz mirasından" bahseder. extend yerine her zaman implements kullanmayı tercih eden bir Java programcısı gibi. Ve bu aslında Alan Jolub tarafından tavsiye edilen bir alışkanlıktır. Der ki:

James Gosling'in (Java'nın yaratıcısı) sunduğu bir Java kullanıcıları gurubu konferansında bir keresinde bulunmuştum. O akılda kalıcı soru - cevap kısmında birisi şöyle bir soru sordu: "Eğer Java'yı yeniden tasarlasaydınız, neyi değiştirirdiniz?". O da şöyle bir cevap verdi: "Sınıf anlayışını terk ederdim". İnsanların kahkahası sona erdikten sonra sorunun sınıflar olmadığını ancak "miras alma" işleminden ziyade (extends ilişkisi) olduğundan bahsetti. "Arayüz mirası" (implements ilişkisi) çok daha tercih edilesi. Miras almayı mümkün olduğunca az yapmanız faydanıza olur.

Java gibi bir dilde bile, çok fazla sınıf üretiyor olabilirsiniz!

Miras alma anlayışının çok ciddi sorunları var, fakat oldukça akla yatkın görünüyor. Aşırı geniş bir sınıf olan Animalı tasarlıyoruz ve içerisine çok faydalı şeyler ekliyoruz (tehlikeli olsa bile), ve bizim Cat sınıfımız da bu faydalı şeyleri kullanabiliyor. Hepsi bu, kodları tekrar kullanmanın bir yolu. Ancak kodun yeniden kullanımı aslında başka bir konudur.

Miras alma ve arayüz mirası arasındaki farkı anlamak Rust'ı anlarken oldukça önemlidir.

Özelliklerin size başka metotlar sağladığını unutmayın. Iterator özelliğini düşünün, next metotunu tanımladıktan sonra başka metotları zahmetsizce tipinize eklemiş olursunuz. Modern Java arayüzlerindeki "varsayılan" metotlar gibi. Alttaki örnekte name kısmını tanımlıyoruz ve upper_case bizim yerimize tanımlanmış oluyor. İstersek upper_case metotunu yeniden yazabiliriz, ama bu gerekli değildir.


#![allow(unused)]
fn main() {
trait Named {
fn name(&self) -> String;

fn upper_case(&self) -> String {
self.name().to_uppercase()
}
}

struct Boo();

impl Named for Boo {
fn name(&self) -> String {
"boo".to_string()
}
}

let f = Boo();

assert_eq!(f.name(),"boo".to_string());
assert_eq!(f.upper_case(),"BOO".to_string());
}

Bu da bir çeşit kodu tekrar kullanma yöntemi, evet, ama bunun verinin kendisinde geçerli olmadığını sadece arayüzde tanımlı olduğuna dikkat edin.

Ördekler ve Genellemeler

Burada Rustla yazılmış, genelleme kullanılarak yazılan "ördek" fonksiyonu gözünüze anlamsız gelmiş olabilir:


#![allow(unused)]
fn main() {
fn quack<Q> (q: &Q)
where Q: Quack {
q.quack();
}

let d = Duck();
quack(&d);
}

Tip parametresi, Quack özelliğini barındıran herhangi bir şeyi işaret eder. Buradaki quack ve önceki bölümde tanımladığımız quack_ref arasında önemli bir fark var. Fonksiyonun içeriği, çağıran her bir tip için ayrıca oluşturulur ve sanal metotlara ihtiyacımız yok, bu tarz fonksiyonlar tamamen "satır içi" (inline) olabilir. Burada Quack tipi genellenen tip üzerinde bir kısıtlama olarak kullanılır.

Bu da genellenen quack metotumuzun C++ muadili (consta dikkat edin):

template <class Q>
void quack(const Q& q) {
q.quack();
}

Tip parametresinin herhangi bir şeyle kısıtlanmadığına dikkat edin.

Bu daha çok derleme zamanında çalışan bir ördek tiplemeye benziyor - vaklamayan bir tipi iletirsek derleyici quack diye bir metot olmadığından bahsedecektir. En azından sonra derleme zamanında keşfediliyor. Go'da direkt bütün tipin Quackable arayüzüne sahip olması daha da kötü şeylere sebep olabilir. Daha karmaşık template fonksiyonları ve sınıfları berbat hata mesajlarına yol açacaktır çünkü bu sefer genellenen tipler üzerinde hiçbir kısıtlama olmayacaktır.

Vaklayan nesne üzerindeki işaretçilerle bir döngü tanımlamayı düşünebilirsiniz:

template <class It>
void quack_everyone (It start, It finish) {
    for (It i = start; i != finish; i++) {
        (*i)->quack();
    }
}

Bu, her It döngüleyici türü için çalışacaktır. Rust muadili az biraz daha ilginçtir:


#![allow(unused)]
fn main() {
fn quack_everyone <I> (iter: I)
where I: Iterator<Item=Box<Quack>> {
for d in iter {
d.quack();
}
}

let ducks: Vec<Box<Quack>> = vec![Box::new(duck1),Box::new(duck2),Box::new(parrot),Box::new(int)];

quack_everyone(ducks.into_iter());
}

Rust'taki döngüleyiciler ördek tipli değildir ancak ilişkili tipler Iterator özelliğini barındırmalıdır ve ilgili örneğimizde Box<Quack> için döngüleyici tanımlanıyor. Hangi tiple çalışacağı hakkında bir belirsizlik yoktur ve ilişkili veriler muhakkak Quack'ı tanımlamalıdır. Bazen bir Rust fonksiyonu yazarken fonksiyon imzası yazmaktan Rust'tan bıkabilirsiniz, bu yüzden standart kütüphanenin kaynak kodlarını dikkatlice okumanızı şiddetle öneririm - ancak fonksiyon yazmak fonksiyon yazmaktan daha kolaydır! Örneğimizdeki tek tip parametresi döngüleyici tipidir, yani bu sadece vektör döngüleyicisiyle değil, Box<Duck> dizisi olan her şeyle çalışacaktır.

Miras Alma

Nesne yönelimli tasarımlı ilişkili en yaygın sorun nesnelerin bir "onun türünden olma" (is-a) ilişkisiyle tanımlanmasıdır, sahip olma ilişkileri (has-a) genellikle ihmal edilir. GoF, "Design Patterns" (Dizayn örüntüleri) kitabında yirmi iki yıl önce "Birleşkeleri mirasa tercih edin" demiş.

İşte size iyi bir örnek: Bir şirketin çalışanlarını modellemek istiyorsunuz ve Calisan sizin için iyi bir sınıf ismi gibi görünüyor. Sonra, "Yönetici" bir çalışan türüdür (yani doğru) ve kendi hiyerarşimizi Yonetici'yi Calisanın alt sınıfı olarak inşa ediyoruz kurguluyoruz. Zekice görünse de değil. İsimleri belirlemeye kendimizi kaptırmışken çalışanların ve yöneticilerin aslında birbirinden farklı canlılar olduğunu düşündük aslında. Belki de Calisan sınıfı sadece bir Roller dizisine sahip olmalıdır ve yöneticiyi daha fazla yetkiye sahip bir çalışan olarak tanımlamalıyız?

Taşıtları düşünelim, bisikletten damperli kamyonlara kadar geniş bir skalası var. Araçları kategorize etmenin çok farklı yolları var, üzerinde gittiği yoldan (şehir içinde, tarlada, rayda), kullandığı enerji türüne (dizel, hibrit, elektrikli vs), insan mı yoksa yük taşımacılığında mı kullanıldığı gibi. Sınıfların sabit bir hiyerarşisi, tek bir bakış açısı dışında diğer bakış açılarının görmezden gelindiği anlamına gelir. Gördüğünüz gibi, araçları çok farklı şekillerde sınıflandırabilirsiniz!

Rust için birleşkeler çok daha önemlidir çünkü başka bir sınıftan işlevleri olduğu gibi devrealmak tembelce bir yöntemdir.

Ödünç alma denetimi açısından da birleşkeler önemlidir çünkü çeşitli yapıların (struct) hangi alanlarının kullandıldığının takibi yapılabilir. Bir alanın değişken referansı alınırken diğer alanın değişmez referası ödünç alınabilir; bu yüzden yapılar kullanım rahatlığı için kendi metotlarına sahip olmalıdır. (Yapının dışsal arayüzü ise özellikler üzerinden sağlanabilir.)

Ayrık referansların net bir örneği bunu daha anlaşılır kılacaktır. Kendi String alanları olan bir yapı tanımladık ve tek bir String'in değişken referansını aldık.


#![allow(unused)]
fn main() {
struct Foo {
    one: String,
    two: String
}

impl Foo {
    fn borrow_one_mut(&mut self) -> &mut String {
        &mut self.one
    }
    ....
}
}

(Rust'ın isimlendirme anlayışının da aynı zamanda bir öneğidir - bu tarz metot isimleri _mut ile sona ermelidir.)

Şimdi ilk metotu tekrar kullanarak iki String alanını da ödünç alan bir yapı tanımlayalım:


#![allow(unused)]
fn main() {
    fn borrow_both(&self) -> (&str,&str) {
        (self.borrow_one_mut(), &self.two)
    }
}

Çalışamaz! self'in değişmez referansını aldık ve aynı zamanda self'in değişebilir referansını almaya çalışıyoruz. Eğer Rust bu tarz durumlara izin verseydi değişmez referansın değişemeyeceğini garanti edemeyebilirdi.

Çözüm basit:


#![allow(unused)]
fn main() {
    fn borrow_both(&self) -> (&str,&str) {
        (&self.one, &self.two)
    }
}

Bunda bir sorun yok çünkü bunların ikisini ödünç alma denetçisi bunların ikisini de bağımsız ödünç almalar olarak tanımlar. Alanların gelişigüzel yapılar olduğunu düşünün, rastgele çağırdığınız metotlar bu alanlar üzerinde çalışırken bir hataya sebep olmayacaktır.

Miras almanın sınırlandırılmış ancak önemli bir türü Deref özelliğidir, ismi "Dereferans" operatörü olan * ile gelir. String tipi Deref<Target=str> özelliğine sahiptir ve &str tipi için çalışacak bütün metotlar aynı zamanda String ile de çalışabilir! Benzer şekilde, Foo ile çalışan bütün metotlar aynı Box<Foo> üzerinden de doğruca çalışabilir. Başta biraz... kontrolsüz bir şekilde pratik görünse de aslında epey kullanışlıdır. Rust'ın içerisinde basit bir mantık var, ancak kullanımı o kadar da akılcı görünmüyor. Sadece değişken bir türün ödünç alındığı zaman daha basit davranması gerketiği zaman kullanılmalıdır.

Rust'ta özellikler birbirini miras alabilir:


#![allow(unused)]
fn main() {
trait Show {
    fn show(&self) -> String;
}

trait Location {
    fn location(&self) -> String;
}

trait ShowTell: Show + Location {}
}

Son özellik iki ayrıksı özelliği de barındırıyor, istenirse kendi içine metotlar tanımlanabilir.

Şimdi alıştığımız şeye geri döndük:


#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Foo {
    name: String,
    location: String
}

impl Foo {
    fn new(name: &str, location: &str) -> Foo {
        Foo{
            name: name.to_string(),
            location: location.to_string()
        }
    }
}

impl Show for Foo {
    fn show(&self) -> String {
        self.name.clone()
    }
}

impl Location for Foo {
    fn location(&self) -> String {
        self.location.clone()
    }
}

impl ShowTell for Foo {}

}

Eğer Foo türünden foo diye bir değerim varsa, bu türün değişkeni &Show ve &Location'u karşıladığı gibi (ikisini de barındıran) &ShowTell'i de karşılayacak.

İşte işimize yaracak ufak bir makro:


#![allow(unused)]
fn main() {
macro_rules! dbg {
    ($x:expr) => {
        println!("{} = {:?}",stringify!($x),$x);
    }
}
}

($x ile gösterilen) tek bir argümanı alıyor ve bu argüman bir "ifade" olmalıdır. Bu değeri ekrana yazdırıyoruz ve veriyi ve metinin metinleştirilmiş hâlini ekrana yazdırıyoruz. C programcıları burada bıyık altından gülebilir, ancak eğer 1 + 2 (bir ifade) değerini verirsem stringify!(1 + 2) bize "1 + 2" şeklinde karakter dizisi verecektir. Bu, bize biraz kodları incelemek için yardımcı olacaktır:


#![allow(unused)]
fn main() {
let foo = Foo::new("Pete","bathroom");
dbg!(foo.show());
dbg!(foo.location());

let st: &ShowTell = &foo;

dbg!(st.show());
dbg!(st.location());

fn show_it_all(r: &ShowTell) {
    dbg!(r.show());
    dbg!(r.location());
}

let boo = Foo::new("Alice","cupboard");
show_it_all(&boo);

fn show(s: &Show) {
    dbg!(s.show());
}

show(&boo);

// foo.show() = "Pete"
// foo.location() = "bathroom"
// st.show() = "Pete"
// st.location() = "bathroom"
// r.show() = "Alice"
// r.location() = "cupboard"
// s.show() = "Alice"
}

İşte bu nesne yönelimli programlamadır, sadece alıştığınız türden değil.

Show referansına iletilen show değerinin dinamik olarak ShowTell olmayacağına dikkat edin! Daha dinamik sınıf sistemlerine sahip dillerin size objenin bir sınıfın nesnesi olup olmadığını denetleme imkanı verdiğine ve bu dinamik türe göre çağrı yapma izni verdiğine dikkat edin! Aslında bu pek de iyi bir fikir değildir ve Rust'ta bunu yapmanın bir yol yoktur çünkü Rust Show referansının aslında ShowTell referansı olduğunu unutmuştur bile.

Her zaman seçimleriniz vardır; özellik nesneleri aracılığıyla çok biçimlilik ya da özellik kısıtlamaları ile biçimlendirilmiş genelleme tanımlarıyla tek biçimlilik. Modern C++ ve Rust standart kütüphanesi genelleme yolunu tercih eder, ancak çok biçimlilik yolu da hâlen daha tercih edilebilir. Her zaman neyin karşılığında neyi aldığınızı bilmeniz gerekir - genellemeler daha hızlı kod üretir ve satır içi (inline) kullanılabilirler. Bunun neticesinde kodunuz şişebilir. (code bloat) Fakat her şeyin mümkün olduğunca hızlı olmasına da gerek yoktur - olağan bir program akışında birkaç defa bu gerçekleşebilir.

Sonuç olarak:

  • Sınıfların rolü veri ve özellikler (trait) arasında paylaştırılmıştır.
  • Yapılar ve numaralandırmalar veri gizlemesine ve metot tanımlanabilmesine rağmen fazla işleve sahip değildir.
  • Alttiplemenin sınırlandırılmış bir türü Deref özelliği ile sağlanabilir.
  • Özellikler bir veri tutmaz ancak her tipe uygunlanabilirler. (Yalnızca yapılara değil.)
  • Özellikler, başka özellikleri miras alabilir.
  • Özellikler metotlar sunabilir, kodların tekrar kullanımını mümkün kılarlar
  • Özellikler size sanal metotlar (çok biçimlilik) ve genelleme sınırlamaları (tek biçimlilik) sunabilirler.

Ördek: Windows API

Geleneksel nesne yönetimli programlamanın en çok kullanıldığı yerlerden birisi GUI (Ç.N: düğmeli menüyü arayüzler işte) kütüphaneleridir. EditControl ve ListWindow, Window türünden sınıflardır falan filan. Bu, GUI kütüphanelerine Rust bağlantıları yazmayı biraz daha zorlaştırır.

Rust'ta Win32 programlaması direkt yapılabilir, ve orijinal C'den daha az gariptir. C'den C++'a geçer geçmez daha sade bir şey yapmak istedim ve kendi nesne tabanlı kodlarımı yazdım.

Tipik bir Win32 API fonksiyonu ShowWindow'dur ve bir pencerenin görünüp görünmediğini denetlemek için kullanılır. Şimdi, EditControl'ün kendisine özgü nitelikleri bulunur fakat hepsi Win32'nin HWND ("pencere yönetimi") opak değeriyle yapılır. EditControl'ün aynı zamanda show metotu olmasını isteyebilirsiniz, genelde miras alma yöntemiyle halledilir. Fakat bu tipin her işlevi miras almasını istemeyebilirsiniz! Rust size güzel bir çözüm sunar, Window özelliğini düşünelim:


#![allow(unused)]
fn main() {
trait Window {
    // you need to define this!
    fn get_hwnd(&self) -> HWND;

    // and all these will be provided
    fn show(&self, visible: bool) {
        unsafe {
         user32_sys::ShowWindow(self.get_hwnd(), if visible {1} else {0})
        }
    }

    // ..... oodles of methods operating on Windows

}
}

Şimdi EditControl yapısı sadece bir HWHD'ye sahip olabilir ve Window'u eklemek tek bir metotu tanımlayarak mümkün olabilir. EditControl ise Window özelliğini miras alan bir yapıdır ve daha geniş bir arayüz tanımlar. ComboBox gibi düşünün - EditControl gibi davranır ve bir ListWindow da özellik mirası aracılığıyla eklenebilir.

Win32 API'sı ("32" artık "32-bit" anlamına gelmiyor) özünde nesne yönelimlidir, ancak daha eski bir şekilde, Alan Key'in tanımından etkilenmiş bir şekilde: nesneler gizli veriler tutar ve mesajlar üzerinden işlenir. Yani Windows uygulamalarının kalbi bir mesaj döngüsüdür ve çeşitli pencereler (pencere sınıfları da denir) kendi metotlarını kendi anahtar ifadeleriyle uygularlar. WM_SETTEXT diye bir mesaj vardır fakat bunun koda dökülüş hâli biraz daha farklıdır: bir etiketin yazısı değişebilir ya pencere başlığı değişir.

Burada daha anlaşılır ve minimal bir Windows GUI frameworkü görebilirsiniz. Bana göre çok fazla unwrap öğeleri var ve bunların çoğu hata bile değil. Bu, "NWG"nin mesajlaşmanın dinamik doğasına ters düşmesiyle alakalı. Daha tip güvenli bir arayüz sunmak için, hatalar derleme zamanında sunuluyor.

Rust programlama kitabında "Nesne yönelimli nedir?" üzerine güzel bir tartışma mevcuttur.

Çeviri notu: Bu bölümün sonlarına doğru bir dolu hata yapmış olabilirim, çünkü bu son kısımları yazarken saat gecenin üçüne yaklaşıyor ve kafa olarak biraz yorgundum. :)

Türk yazılımcısı çoğunlukla nesne yönelimli kavramını C# ve Java aracılığıyla öğrendiği için nesne yönelimi kavramı dilde class kelimesinin sunulmasıyla bağdaştırılıyor ve bu epeyce bir kafa karışıklığı yaratıyor. Nesne tabanlı programlama, kendi özünde verilerin soyutlanıp yazılımcıya daha çok anlam ifade eden "bütünlere" dönüştürülmesiyle alakalı bir tekniktir ve bu tekniğin uygulanması için verinin gizlenmesi, yalnızca metotlarla erişim gibi ilkeler vardır. class ile tanımlanan yaplar, bu ilkeleri otomatik olarak inşa etmeye yarar. Ortada class adı hiç geçmese bile nesne tabanlı programlama yapılabilir ve Lua, Nim, Nix gibi diller buna iyi bir örnek oluşturur. İşin özü, JavaScript da bu sınıfların listesindeydi ancak sonradan class kelimesi bazı prototip işlemlerini otomatikleştirmek için eklendi. Eklenene dek, JavaScript her zaman nesne tabanlıydı ancak "fonksiyonel" ve "nesne tabanlı" olmak arasında kalmış bir dil gibi düşünüldü; aslında JavaScript her zaman "prototip" üzerinden işleyen nesne yönelimli bir dildi.

Bir literatür hatası olarak, saf fonksiyonel diller aynı zamanda nesne yönelimli olamayacağı için fonksiyonel programlama ve nesne yönelimli programlama arasında bir zıtlık varmış gibi anlaşıldı. Sonradan Go, Rust gibi klasik nesne yönelimli diller sınıfına uymayan diller "fonksiyonel" diller olarak anlaşıldı. Rust'ta fonksiyonel esintiler bulunmasına rağmen Rust saf bir fonksiyonel dil değildir, nesne yönelimi ile Rust'ın alakası neyse fonksiyonel programlama ile Rust'ın alakası o seviyede diyebilirim. Ha, fonksiyonel programlama "işlevsel, işe yarayan programlama" demek değildir diye not edelim.

Peki, neden Rust'ta sınıflar yok? Bu bölümde bahsedildiği gibi sınıflar çok fazla işi bir anda halleder ve bu kesinlikle Rust'ın doğasına uymaz. Rust'ta her şey açık, belirgin ve bilindiktir. Rust, sınıfların yaptığı şeyleri yapmanıza izin verecek araçları size sunar ancak kolaylık olsun diye gizlice bir şeyler hazırlamaz. Bu yüzden, Rust nesne yönelimli bir dil gibi görünmese de aslında nesne yönelimini size sunar.

Eğer nesne yönelimli programlamaya karşı bakış açınızı gözden geçirmek ve nesne yönelimli programlamanın yaratıcısından da neyin ne olduğunu öğrenmek isterseniz, bu bağlantıya tıklayabilirsiniz.

Yazıları Nom ile Ayrıştırmak

Nom, (burada anlatıldığı şekilde) öğrenmeye değer bir metin ayrıştırma için kullanılan bir Rust kütüphanesidir.

Eğer CSV veya JSON gibi türü bilinen bir veri türünü ayrıştırmak istiyorsanız bu işin özelleşmiş kütüphanelerden birisi olan Rust CSV veya [Bölüm 4'te] bahsedilen JavaScript kütüphanelerinden birisine bakmak isteyebilirsiniz.

Aynı şekilde, ini veya toml. gibi yapılandırma dosyaları için onlara özel kendisine özgü kütüphanelere göz atabilirsiniz. (serde_json'den bildiğimiz Serde Frameworkü ile uyumlu çalıştığı için Toml için hazırlanan kütüphane ayrıca hoştur.)

Fakat belli bir standarda ait olmayan, keyfe keder bir şekilde hazırlanmış verilerı taramak karakter dizileriyle geçireceğiniz sıkıcı saatlere işaret ediyor da olabilir. İlk fikir regex olur, ancak regex bir yerden sonra alabildiğine mantıksız bir şeye dönüşebilir. Nom, metin ayrılmanın güçlü ve sadece basit araçları birleştirmekten ibaret olduğu güzel bir yol sunar. Aynı zamanda regexlerin bir sınırı vardır, mesela HTML taramak için regex kullanamazsınız., fakat Nom ile HTML ayrıştırabilirsiniz. Hatta kendi programlama dilinizi yazmayı düşündüyseniz, Nom öğrenmek bu zorlu yolculuğun ilk adımı olabilir.

Nom öğrenmek için müthiş rehberler var, ancak ben biraz sindirerek gitmek istediğim için en basit yerden başlamak istiyorum. Bilmeniz gereken ilk şey, Nom baştan aşağıya makrolardan oluşur, ikincisi Nom karakter dizileri yerine bayt dilimleriyle çalışmayı tercih eder. Birinci şey, Nom'u kullanırken dikkatli olmanız gerektiğine işaret eder çünkü hata mesajlarından hiçbir şey anlamayabilirsiniz. İkincisi Nom'u herhangi bir veri türüyle kullanabileceğinizi işaret eder, sadece "metin" ayıklamak için değil. Nom kullanmış kişiler ikili biçimleri deşifre etmek veya dosya başlıklarını anlamak için kullandı. "UTF-8" ile kodlanmamış metinlerle de çalışabilirsiniz.

Nom'un son versiyonları karakter dizileriyle de çalışabilmeyi başladı, fakat karakter dizileriyle çalışabilen makroların sonunda _s bulunur.

#[macro_use]
extern crate nom;

named!(get_greeting<&str,&str>,
    tag_s!("hi")
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done(" there", "hi")

named! isimli makro (varsayılan olarak &[u8] tipinden) girdi alıp ve sivri parantezlerin ikinci argümanının tipinden geri dönen fonksiyonlar oluşturur. tag_s! ise kendisine iletilen karakter dizisi ile eşleşir, ve değer genellikle verilenin türünden olur. (Eğer &[u8] ile çalışmak isterseniz, bunun yerine tag! kullanabilirsiniz.)

Tanımladığımız get_greeting ayrıştırıcısını bir &str ile çağırabiliriz ve bize IResult dönecektir, bir de elbette ki eşleşen veriyi.

Boşlukları görmezden gelmek isteyebiliriz, tag! makrosunu ws! ile sarmalarsak aradığımız "hi" kelimesini eşleştirirken bütün boşluklar görmezden gelinecektir:

named!(get_greeting<&str,&str>,
    ws!(tag_s!("hi"))
);

fn main() {
    let res = get_greeting("hi there");
    println!("{:?}",res);
}
// Done("there", "hi")

Sonuç daha önce olduğu gibi "hi" olacaktır, ardında kalan karakter dizisi boşlukları kaldırılmış bir şekilde "there" olacaktır!

Tamam, "hi" eşleşmesi tıkırında çalışıyor ama bir şey yaramıyor. Hadi sadece "hi" yerine hem "hi" hem de "bye" kısmını eşleştirelim. alt! makrosu ("alternatif") | ile ayrılmış ayrıştırıcılardan birisiyle eşleşir. Aynı şekilde burada boşlukları okunaklı olması için kullanabilirsiniz:


#![allow(unused)]
fn main() {
named!(get_greeting<&str>,
    ws!(alt!(tag_s!("hi") | tag_s!("bye")))
);
println!("{:?}", get_greeting(" hi "));
println!("{:?}", get_greeting(" bye "));
println!("{:?}", get_greeting("  hola "));
// Done("", "hi")
// Done("", "bye")
// Error(Alt)
}

Sonuncu hatalı, çünkü "hola" ile eşleşen bir metnimiz yok.

Doğrusu IResult tipini iyice anlamamız gerekiyor ki daha ileriye gidebilelim; fakat neden bunu bir "regex" ifadesiyle kıyaslamıyoruz?


#![allow(unused)]
fn main() {
    let greetings = Regex::new(r"\s*(hi|bye)\s*").expect("bad regex");
    let caps = greetings.captures(" hi ").expect("match failed");
    println!("{:?}",caps);
// Captures({0: Some(" hi "), 1: Some("hi")})
}

Doğrusu Regex göze daha sade görünüyor! Sadece parantez içine | koyduk ve bir tarafına "hi" diğer tarafına "bye" yerleştirdik. İlk sonuç girdi olarak aldığımız karakter dizisi, ikincisi de eşleşen ifade. (| regex için sözde "çeşitlilik (alternation)" operatörüdür, alt! makrosuna ilham vermiştir.)

Fakat bu basit bir regex olsa bile bir anda herkes karmaşıklaşabilir. İşin ilginci metinlerde sıkça kullanılan * ve ( gibi karakterlerden kaçınmanız gerekir ve (hi) veya (bye) ile eşleşen bir regex ifadesi yazmak isterseniz sevimli regeximix \s*((hi | bye))\s* gibi ucube bir hâl alacaktır. Bunun Nom muadili, gayet anlaşılır bir biçimde alt!(tag_s!("(hi)") | tag_s!("(bye)")) şeklindedir.

İşin kötüsü regex kütüphanesi ağır bir bağımlılıktır. Ananıza babanıza ancak verebileceğiniz bu i5 işlemcili laptota "Merhaba Dünya" seviyesi Nom örneklerinin derlenmesi sadece 0.55 saniye sürüyor. Fakat aynı şey regex için 0.90 saniye sürüyor. Aynı şekilde strip komutu uygulanmış ikili programın boyutu 0.3Mb tutarken (Statik linklenmiş bir Rust programının tutabileceği en küçük boyut) Regex örneği için 0.8Mb tutmaktadır. (Ç.N: Gözünüze bunlar anlamsız salt istatiksel veriler gibi görünebilir, ancak program büyüdükçe bu kütüphaneler kullanıldıkça bu farkın nasıl da katlanarak artacağını gözünüzde canlandırın.)

Nom Ayrıştırıcısı Bize Ne Döner?

IResult tipi standart Result tipinden daha çok şey döner. Üç ihtimal var:

  • Done - başarılı - sonucu ve geri kalan baytları alırsınız.
  • Error - ayrıştırma başarısız - bir hata alırsınız.
  • Imcomplete - (tamamlanmadı) daha fazla veriye ihtiyaç vardır.

Hata ayrışma çıktısını bize dönebilen herhangi bir veriyi argüman olarak alan genellenen bir dump fonksiyonu yazabiliriz. Bu örnek aynı zamanda bize bildiğimiz Result'u dönen to_result metodununu nasıl kullanılabileceğini de gösterir - bu metodu veriyi ya da hatayı istediğiniz durumların çoğunda sıkça kullanacaksınızdır.

#[macro_use]
extern crate nom;
use nom::IResult;
use std::str::from_utf8;
use std::fmt::Debug;

fn dump<T: Debug>(res: IResult<&str,T>) {
    match res {
      IResult::Done(rest, value) => {println!("Done {:?} {:?}",rest,value)},
      IResult::Error(err) => {println!("Err {:?}",err)},
      IResult::Incomplete(needed) => {println!("Needed {:?}",needed)}
    }
}


fn main() {
    named!(get_greeting<&str,&str>,
        ws!(
            alt!( tag_s!("hi") | tag_s!("bye"))
        )
    );

    dump(get_greeting(" hi "));
    dump(get_greeting(" bye hi"));
    dump(get_greeting("  hola "));

    println!("result {:?}", get_greeting(" bye  ").to_result());
}
// Done "" "hi"
// Done "hi" "bye"
// Err Alt
// result Ok("bye")

Ayrıştırıcılar bize ayrıştırılmamış verileri de dönüyor ve yeterince girdi almadıklarını da ortaya çıakrırlar, fakat genellikle to_result'u tercih edeceksiniz.

Ayrıştırıcıları Birleştirmek

Selamlama örneğimizle devam edelim ve "hi" veya "bye", artı bir isimden oluşan bir selamlama tasarlayalım. nom::alpha alfabetik karakter serileriyle eşleşecek pair! ise iki ayrıştırıcıyı tek bir demekte birleştirecektir.


#![allow(unused)]
fn main() {
    named!(full_greeting<&str,(&str,&str)>,
        pair!(
            get_greeting,
            nom::alpha
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
// result Ok(("hi", "Bob"))
}

Şimdi, selamlayıcımızın pek sosyal olduğunu veya kimsenin adını bilmediğini de hesaba katalım, ismi opsiyonel yapalım. Doğal olarak demetteki ikinci veri bir Option olacaktır.


#![allow(unused)]
fn main() {
    named!(full_greeting<&str, (&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(nom::alpha)
        )
    );

    println!("result {:?}", full_greeting(" hi Bob  ").to_result());
    println!("result {:?}", full_greeting(" bye ?").to_result());
// result Ok(("hi", Some("Bob")))
// result Ok(("bye", None))
}

Selamlama için kullandığımız ayrıştırıcı ile isimleri yakalayan ayrıştırıcıyı birleştirmenin ve isim yakalamayı opsiyonel yapmanın ne seviye kolay olduğuna dikkat edin. Bu Nom'un geldiği gücün kaynağıdır ve bu yüzden ona "ayrıştırıcıları birleştiren kütüphane" (parse combinator library) denir. Basit ayrıştırıcılardan birleşerek inşa olan karmaşık ayrıştırıcılar inşa edebilir ve bunları teker teker test edebilirsiniz. (Buna eşdeğer bir regex bir Perl programı gibi görünmeye başlardı: çünkü regexlerin birleşmesi pek hayra alamet değildir.)

Fakat, henüz istediğimiz noktaya varamadık! full_greeting(" bye ") bize bir Incomplete hatası olarak dönecektir. Nom için "bye"dan sonra isim gelmelidir ve bu yüzden bizden isim namına bir şeyler isteyecektir. Bu bir akış ayrıştırıcısının (streaming parser) çalışmasının nasıl çalışması gerektiğidir, bu sayede dosyaları parça parça iletebilirsiniz; ancak burada Nom'a girdinin yetersiz olacağını bildirmemiz gerekir.


#![allow(unused)]
fn main() {
    named!(full_greeting<&str,(&str,Option<&str>)>,
        pair!(
            get_greeting,
            opt!(complete!(nom::alpha))
        )
    );

    println!("result {:?}", full_greeting(" bye ").to_result());
// result Ok(("bye", None))
}

Numaraları Ayrıştırmak

Nom bir dizi rakam serisini taramaya yarayan digit fonksiyonuna sahiptir. map! kullanarak bir yazıyı bir sayıya dönüştürebilir ve bir Result tipi içinde geri dönebiliriz.


#![allow(unused)]
fn main() {
use nom::digit;
use std::str::FromStr;
use std::num::ParseIntError;

named!(int8 <&str, Result<i8,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

named!(int32 <&str, Result<i32,ParseIntError>>,
    map!(digit, FromStr::from_str)
);

println!("{:?}", int8("120"));
println!("{:?}", int8("1200"));
println!("{:?}", int8("x120"));
println!("{:?}", int32("1202"));

// Done("", Ok(120))
// Done("", Err(ParseIntError { kind: Overflow }))
// Error(Digit)
// Done("", Ok(1202))
}

Burada Result'a dönüşebilen bir IResult ayrıştırıcısı elde ederiz - ve elbette ki, burada mümkün olan birden çok hata vardır. Fonksiyonların içeriklerinin aynı olduğuna dikkat edin, esas dönüşüm fonksiyonun döndüğü tipe bağlıdır.

Sayıların işareti olabilir. Sayıları bir çift parça hâlinde yakabilirsiniz; önce bir işaret gelir ardından rakam gelir.

Mesela:


#![allow(unused)]
fn main() {
named!(signed_digits<&str, (Option<&str>,&str)>,
    pair!(
        opt!(alt!(tag_s!("+") | tag_s!("-"))),  // maybe sign?
        digit
    )
);

println!("signed {:?}", signed_digits("4"));
println!("signed {:?}", signed_digits("+12"));
// signed Done("", (None, "4"))
// signed Done("", (Some("+"), "12"))
}

Eğer hedefe odaklıysanız ve ara sonuçları atlamak istiyorsanız, recognize! istediğiniz şeyi verebilir.


#![allow(unused)]
fn main() {
named!(maybe_signed_digits<&str,&str>,
    recognize!(signed_digits)
);

println!("signed {:?}", maybe_signed_digits("+12"));
// signed Done("", "+12")
}

Bu teknikle noktalı sayıları da yakalayabiliriz. Bu eşleşmeler üzerinden bayt dilimlerinden karakter dizilerine ulaşıyoruz. tuple!, pair!'in oluşturulan demetle ilgilenmediğimiz türünden bir muadili. complete! ise "yarım kalan selamlama"da yaşadığımız sorunu çözmek için kullandığımız bir araç - "12", noktalı olmasa da aslında geçerli bir sayıdır.


#![allow(unused)]
fn main() {
named!(floating_point<&str,&str>,
    recognize!(
        tuple!(
            maybe_signed_digits,
            opt!(complete!(pair!(
                tag_s!("."),
                digit
            ))),
            opt!(complete!(pair!(
                alt!(tag_s!("e") | tag_s!("E")),
                maybe_signed_digits
            )))
        )
    )
);
}

Yardımcı olacak minik bir makro tanımlayarak bazı geçerli testler üretebilriz. Bu testler, floating _point verilen metinden sayı yakalayabildiyse geçerli sonuç verecektir.


#![allow(unused)]
fn main() {
macro_rules! nom_eq {
    ($p:expr,$e:expr) => (
        assert_eq!($p($e).to_result().unwrap(), $e)
    )
}

nom_eq!(floating_point, "+2343");
nom_eq!(floating_point, "-2343");
nom_eq!(floating_point, "2343");
nom_eq!(floating_point, "2343.23");
nom_eq!(floating_point, "2e20");
nom_eq!(floating_point, "2.0e-6");
}

(Makrolar kodu biraz kirletilmiş gösterse de, testlerinizi hazırlamak faydalı bir şeydir.)

Ve metinleri ayrıştırıp noktalı sayılara çevirebilirsiniz. Burada akışa odaklanacağım ve hatayı uzaklaştıracağım:


#![allow(unused)]
fn main() {
    named!(float64<f64>,
        map_res!(floating_point, FromStr::from_str)
    );
}

Lütfen birbirinden karmaşık testler ayrıştırıcılar oluşturmanın adım adım nasıl mümkün olduğuna dikkat edin, her bir parçayı ayrıca test edebilirsiniz. Bu, birleştirilmiş ayrıştırıcıların regexler üzerinde güçlü bir avantajıdır. Bu gayet klasik bir programlama taktiği olan "böl ve yönettir".

Çeşitli eşlemeler üzerinde işlemler

Sabit bir sayıda örüntüyü yakalayan ve bir Rust demeti dönen pairs! ve tuple! ile tanıştık.

Bir de many0 ve many1 var - ikisi de değişken sayıda örüntüyü bir vektör içerisinde tanımlar. İkisi artasındaki fark birisinin "sıfır veya daha fazla", diğerinin ise "bir veya daha fazla" şeyi yakalalıyor olmasıdır. (regexteki * ve + karakterini düşünün) Yani, many1!(ws(float64)), "1 2 3" şeklinde bir karakter dizisi bize vec![1.0, 2.0, 3.0] olarak dönmeyi tercih edecek ancak boş bir karakter dizisinde hata verecektir.

fold_many0 ise bir azaltma (reduce) işlemidir. Ayrıştırılan değerler tek bir değerde bir ikili operatör kullanılarak tek bir değerde toplanır. Mesela, Rust programcıları döngüleyicilerin içeriğini toplamak kullanmak için sum gelmeden önce ne yapıyorsa bu da ona benzer; aşağıdaki fold işleyici (accumulator) için bir başlangıç değerine (burada sıfır) sahiptir ve işleyicinin ne yapacağını bildirmesi için + operatörünü kullanır.


#![allow(unused)]
fn main() {
    let res = [1,2,3].iter().fold(0,|acc,v| acc + v);
    println!("{}",res);
    // 6
}

Nom muadili şöyledir:


#![allow(unused)]
fn main() {
    named!(fold_sum<&str,f64>,
        fold_many1!(
            ws!(float64),
            0.0,
            |acc, v| acc + v
        )
    );

    println!("fold {}", fold_sum("1 2 3").to_result().unwrap());
    //fold 6
}

Şimdiye dek bütün ifadeleri yakalamaya çalıştık veya eşleşen baytları recognize! ile aldık:


#![allow(unused)]
fn main() {
    named!(pointf<(f64,&[u8],f64)>,
        tuple!(
            float64,
            tag_s!(","),
            float64
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
 //got (20, ",", 52.2)
}

Karmaşık ifadeler için, ayrıştırıcıların bütün sonuçlarını almış olmak bizi dağınık bir çalışma prensibine sokar! Daha iyisini yapabiliriz.

do_parse! sadece ihtiyacınız olan değerlere erişmesinize izin verir. Yakalanan veriler >> ile ayrılır - ilginizi çeken verileri isim: ayrıştırıcı formatında işaretleyebilirsiniz. Son olarak, parantezler arasında kodunuzu belirtirsiniz.


#![allow(unused)]
fn main() {
    #[derive(Debug)]
    struct Point {
        x: f64,
        y: f64
    }

    named!(pointf<Point>,
        do_parse!(
            first: float64 >>
            tag_s!(",") >>
            second: float64
            >>
            (Point{x: first, y: second})
        )
    );

    println!("got {:?}", nom_res!(pointf,"20,52.2").unwrap());
// got Point { x: 20, y: 52.2 }
}

İlgilenmediğimiz değerleri (bu örnekte olduğu gibi virgül) bir isme bağlamıyoruz ve iki noktalı sayıyı bir yapı oluşturmak için geçici isimlere atıyoruz. Parantezler içinde kalan kısım ise bir Rust kodu olmalı.

Aritmatik İfadeleri Ayrıştırmak

Gerekli bilgiler sayesinde basit aritmatik ifadeleri ayrıştırabiliriz. İşte regexlerle yapamayacağınız şeylere güzel bir örnek.

Aşağıda yapmaya çalıştığımız şey ifadelerimizi ayıklayacak şeyi basitten karmaşığa doğru inşa etmektir. İfadeler eklenip çıkartılabilir terimlerden (term) oluşur. Terimler ise çarpılıp bölünebilir faktörlerden oluşur. Ve (şimdilik), faktörler sadece noktalı sayılardır:


#![allow(unused)]
fn main() {
    named!(factor<f64>,
        ws!(float64)
    );

    named!(term<&str,f64>, do_parse!(
        init: factor >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("*") | tag_s!("/")),
                factor
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "*" {acc * v.1} else {acc / v.1}
            }
        )
        >> (res)
    ));

    named!(expr<&str,f64>, do_parse!(
        init: term >>
        res: fold_many0!(
            tuple!(
                alt!(tag_s!("+") | tag_s!("-")),
                term
            ),
            init,
            |acc, v:(_,f64)| {
                if v.0 == "+" {acc + v.1} else {acc - v.1}
            }
        )
        >> (res)
    ));

}

İfadelerimiz daha net ifade edilmiş oldu - bir ifade bir terimden ve artılı eksili daha fazla terimden oluşur. Onları biriktirmiyoruz, fakat uygun operatör vasıtasıyla işliyoruz. (fold) (Bunun gibi durumlarda Rust, ifadenin türünü tam olarak anlayamadığından işin içinden çıkamaz ve bir ipucu ister). Bu sayede işlem önceliğini sağlamış oluruz - * her zaman + gibi şeyler.

Noktalı sayılar için test ifadelerine ihtiyacımız olacak, ve bunun için bir sandık var..

Cargo.toml dosyanıza approx=0.1.1 satırını ekleyin ve işimize bakalım:


#![allow(unused)]
fn main() {
#[macro_use]
extern crate approx;
...
    assert_relative_eq!(fold_sum("1 2 3").to_result().unwrap(), 6.0);
}

Bir küçük bir test makrosu yazalım. stringify!, ifadeyi bir karakter dizisi ifadesine dönüştürür ve bunu expr içerisine argüman olarak iletebiliriz, sonra da sonucu Rust'ın bulacağı ifadenin sonucu ile kıyaslayalım:


#![allow(unused)]
fn main() {
    macro_rules! expr_eq {
        ($e:expr) => (assert_relative_eq!(
            expr(stringify!($e).to_result().unwrap(),
            $e)
        )
    }

    expr_eq!(2.3);
    expr_eq!(2.0 + 3.0 - 4.0);
    expr_eq!(2.0*3.0 - 4.0);
}

Şükela - sadece birkaç satırla ifade işleyicisi tanımladık! Daha iyi olabilir. factor içindeki numaralara bir alternatif ekleyebiliriz - parantez içindeki ifadeler için:


#![allow(unused)]
fn main() {
    named!(factor<&str,f64>,
        alt!(
            ws!(float64) |
            ws!(delimited!( tag_s!("("), expr, tag_s!(")") ))
        )
    );

    expr_eq!(2.2*(1.1 + 4.5)/3.4);
    expr_eq!((1.0 + 2.0)*(3.0 + 4.0*(5.0 + 6.0)));
}

Şükelalık ifadenin artık terimler açısından özyinemeli (recursively) olarak çağrılmasıdır!

delimited! makrosunun özel sırrı parantezlerin iç içe olabilmesidir - Nom parantezlerin kapandığından emin olacaktır.

Regexin yapabileceklerinin çok çok ötesindeki ve strip uygulanmış ikili dosyamız sadece 0.5Mb, ki hâlen daha ekrana "Merhaba Dünya" yazdıran regex programımızın yarısı kadar ediyor bu.

Rust ve Çektirdiği Çile

Rust'ın öğrenmesi pek çok "ana akım" dillere kıyasla öğrenmesi zor dillerden olduğunu söylemek doğru olur. Bazı istisnası insanlar o kadar da zor bulmuyor, fakat "istisna" ne anlama gelir buna dikkat edin - onlar istinaidir. Çoğu insan başta zorlanır ama sonra başarır. Başlangıçta çektiğiniz çilelerle neticede elde edeceğiniz şeyler arasında bir bağlantı yoktur.

Hepimiz bir yerlerden geliyoruz ve bu Python gibi "dinamik" veya "C++" gibi "statik" ana akım dilleriyle muhatap olmuş olduğumuz anlamına gelir. Her koşulda, Rust programlama anlayışımızı yeniden inşa etmeye zorlayacak kadar farklıdır. Zeki ve tecrübeli insanlar bu işin içine girip kendi zekiliklerinin fayda etmediğini görünce bir hayal kırıklığına uğruyorlar; kendisine yeterince değer vermeyenler de düşündükleri kadar "zeki" olmadıklarını düşünüyorlar.

Dinamik programlama tecrübesi olanlar (buna Java'yı da dahil edebilirim) her şeyin bir referans ve referansların da varsayılan olarak değişebilir olduğunu düşünür. Çöp toplayıcıları da bellek açısından emniyetli programlama yapmamıza yardımcı olur. Daha fazla bellek kullanımı ve öngörülememezlik pahasına da olsa JVM'i daha hızlı hâle getirmek için çok şey yapıldı. Bazen buna değer diye düşünülür, hiç modası geçmeyen bir fikir programcı üretkenliğinin bilgisayar performansından daha önemli olduğudur.

Fakat dünyadaki pek çok bilgisayarın - mesela arabalardaki gaz kelebeğini kontrol etmek gibi gibi hayati önemde şeyler yapan minik bir bilgisayarın - en ucuz laptop kadar kaynağa sahip değildir ve olaylara eşzamanlı (realtime) olarak cevap vermesi gerekir. Aynı şekilde yazılım altyapılarının doğru, sağlam ve hızlı olması gerekir. (Eski bir mühendislik kuralı). Şimdiye dek bu tarz işler kendi yapısı gereği hiçbir emniyet bulundurmayan C ve C++ ile yapıldı - bu raddedeki emniyetsizliğin toplam maaliyeti dikkat edilmesi gereken genel şey oldu. Bir programı çalışır hâle getirmek kolaydır, ama esas olay buradan sonra başlar.

Sistem programlama dillerinde bir çöp toplayıcısı bulunamaz çünkü onlar her şeyin üzerine inşa edildiği bina temelidir. Kaynakların sizin ihtiyaç duyduğunuz ve uygun gördüğünüz şekilde kullanılmasını sağlarlar.

Eğer çöp toplayıcısı olmazsa bellek başka türde şekillerde yönetilir. Manuel bellek yönetiminde belleği alırsınız, kullanırsınız ve bunu geri verirsiniz - kulağa kolay gibi gelse de aslında oldukça zordur. Üretken ve facia yaratmaya elverişli bir C programcısı olmak sizin için bir hafta sürer - ancak ne yaptığını bilen, bütün muhtemel hataları kontrol eden iyi bir C programcısı olmak yıllar sürer.

Rust belleği modern C++ gibi yönetir, nesneler yok edildiği zaman arka kalan bellek tekrar kullanılabilir. Box ile heap üzerinde bellek tahsis edebilirsiniz, ancak bu fonksiyon bittiğinde Box "kapsam sonuna" ulaşmış olur. new diye bir şey var ancak delete diye bir şey yok. File oluşturabilirsiniz, (önemli bir kaynak olan) dosya iş bittiğinde kendiliğinden kapatılır. Buna Rust'ta düşürme (dropping) denir.

Kaynakları paylaşmanız gerekir, her şeyin kopyasını üretmek verimsizdir, işi ilginç kılar da budur. C++'da da referanslar vardır ve Rust referansları daha çok C işaretçilerine benzer - veriye ulaşmak için *r kullanmanız gerekir ve veri referansını iletmek için de & kullanırsınız.

Rust'ın ödünç alma denetçisi (borrow checker) esas veri yok edildikten sonra referansının var olma ihtimalini ortadan kaldırır.

Tip Çıkarımı

"Statik" ve "dinamik" arasındaki fark her şey değildir. Her şeyde olduğu gibi bu da çok katmanlı bir konudur. C, statik tipli (her değişkenin derleme zamanında tipi vardır) ancak zayıf tipli (weakly typed) (mesela void* her şeye işaret edebilir) bir dildir; Python ise dinamik tipli (Tip değişkende tanımlanmaz değil veriyle beraber gelir) fakat güçlü tipli (strongly typed) bir dildir. Java ise statik/güçlümtraktır (akla uygun görünen fakat tehlikeli olabilen veri dönüştürme (?) mekanizmasıyla) ve Rust ise statik/güçlüdür; çalışma zamanında gizlice veri dönüştürmez.

Java her bir tipin ne olduğunun detaylıca yazdırdığı yaygın olarak bilinirken Rust tipleri tahmin (infer) etmeyi sever. Bu genelde zekicedir ancak fakat bazen hangi tiple çalıştığınızı merak edersiniz. Mesela let n = 100 gibi bir ifade görürsünüz ve merak edersiniz - bu sayının değeri ne? Varsayılan olarak bu i32dir, dort bitlik işaretli sayı. Herkes C'nin belirsiz sayı tiplerinin (int ve long gibi) kötü bir fikir olduğundan emindir, açık olmak en iyisidir. Bazen tipi belirtebilirsiniz, mesela let n: u32 = 100 gibi ya da sayıyla beraber tipi de belirtebilirsiniz; let n = 100u32 gibi. Ama tip çıkarımı dediğimi şey bundan ibaret değil! Eğer let n = 100 yazarsanız rustc bunun bir çeşit sayı olduğunu bilir. n değişkenini bir yere iletirseniz ve fonksiyon u64 bekliyorsa n'in tipi u64 olacaktır!

Bu noktadan sonra eğer u32 bekleyen bir yerde n değişkenini kullanırsanız rustc buna izin vermeyecektir çünkü n'in tipi u64 olarak işaretlenmiştir ve bunu dönüştürmenin gizli ve kolay bir yolu olmayacaktır. İşte bu güçlü tiplemedir - "Interger overflow"dan muzdarip olana dek hayatınızı kolaylaştıracak ufak tip dönüşümleri Rust'ta yoktur. Açıkça n değerini n as u32 olarak iletmeniz gerekir - Rust'ta tipler böyle dönüştürülür. Neyse ki, rustc sorunları müdahale edilebilirken vermekte iyidir - derleyicinin tavsiyesiyle sorunu düzeltebilirsiniz.

Bu sayede Rust açık tip ifadeleri olmaksızın yazılabilir:


#![allow(unused)]
fn main() {
let mut v = Vec::new();
// v is deduced to have type Vec<i32>
v.push(10);
v.push(20);
v.push("hello") <--- Bunu yapma işte, yapma
}

Bir sayı vektörüne karakter dizisi iletememek bir sorun değil, özelliktir. Dinamik tipleme esnek olduğu kadar beladır da.

(Eğer bir vektör tipine hem sayı hem de karakter dizisi iletmeniz lazımsa, Rust'ın enum tipleri bu işi emniyetlice görür.)

Bazen ufak bir ipucu vermeniz gerekir. collect çok güzel bir döngüleyici metotudur, ama bazen ipucuna ihtiyaç duyar. Mesela char üzerinde çalışan bir döngüleyicim var. collect iki farklı biçimde çalışır.


#![allow(unused)]
fn main() {
// a vector of char ['h','e','l','l','o']
let v: Vec<_> = "hello".chars().collect();
// a string "doy"
let m: String = "dolly".chars().filter(|&c| c != 'l').collect();
}

Bazen değişken tipinden emin olamazsanız bunun da ufak bir yolu var, o da rustc'yi işleme alınan tipi bir hata mesajında yazdırmaktır:


#![allow(unused)]
fn main() {
let x: () = var;
}

rustc aşırı spesifik bir tip seçebilir. Burada farklı referansları &Debug vektörüne koyuyoruz ancak tipleri açıkça belirtmemiz gereklidir.


#![allow(unused)]
fn main() {
use std::fmt::Debug;

let answer = 42;
let message = "hello";
let float = 2.7212;

let display: Vec<&Debug> = vec![&message, &answer, &float];

for d in display {
    println!("got {:?}", d);
}
}

Değişebilir Referanslar

Kural basit: bir anda sadece değişken referans olabilir. Bunun sebebi değişimin her yerde gerçekleşmesinin önüne geçmek. Küçük programlar yazarken fark etmiyor olabilirsiniz ancak büyük kod yapılarında başınıza ciddi bela olabilir.

Bir diğer kural ise ortada değişemez referanslar varken değişebilir referans elde edemiyor oluşunuzdur. Aksi takdirde kimse bu değişemez referansların değişemeyeceğini garantezi edemezdi. C++ da değişemez referanslara sahiptir (mesela const string&) gibi ancak başka birisinin string& referansı alıp veriyi değiştiremeyeceğini garanti edemez.

Her referansın değişebilir olduğu dillerle çalışmaya alışıksanız bu size ters gelecektir. Emniyetsiz, "rahat" diller tamamen programcının kötü bir şey yapmayacağına ve kendi programını kesinlikle iyi bir şekilde anladığını düşünerek çalışır. Fakat bir kişiden daha fazla kişi tarafından geliştirilmiş büyük programlar bir kişinin programın ne olduğunu anlayıp anlamamasından çok daha ötededir.

İşin gıcık eden eden tarafı ödünç alma denetçisinin düşündüğümüz kadar zeki olmamasıdır.


#![allow(unused)]
fn main() {
let mut m = HashMap::new();
m.insert("one", 1);
m.insert("two", 2);

if let Some(r) = m.get_mut("one") { // <-- mutable borrow of m
    *r = 10;
} else {
    m.insert("one", 1); // can't borrow mutably again!
}
}

Eğer None elde etmişsek HashMap'ten hiçbir şey ödünç alınmıyor, yani burada gerçekten hiçbir şey ödünç alma kurallarını çiğnemiyor.

Hâliyle iş çirkinleşiyor:


#![allow(unused)]
fn main() {
let mut found = false;
if let Some(r) = m.get_mut("one") {
    *r = 10;
    found = true;
}
if !found {
    m.insert("one", 1);
}
}

Rezalet olsa da işe yarıyor çünkü sadece "if" ifadesinde bir adet referasımız tutulmuş oluyor.

Daha iyisi HashMap içindeki entry metotunu kullanmaktır.


#![allow(unused)]
fn main() {
use std::collections::hash_map::Entry;

match m.entry("one") {
    Entry::Occupied(e) => {
        *e.into_mut() = 10;
    },
    Entry::Vacant(e) => {
        e.insert(1);
    }
};
}

Ödünç alma mekanizmasının bu sıralar sözcüksel olmayan yaşam süreleri (non-lexical lifetimes) hakkında daha yumuşak olacağı konuşuluyor.

Yine de ödünç alma mekanizması bazı önemli vakaları anlıyor. Mesela bir yapınız varsa alanları bağımsız olarak ödünç alınabilir. Birleşke mantığı işinize yarayacaktır, büyük yapılar (struct) kendi metotları olan daha ufak yapıları barındırmalıdır. Değişken metotları büyük yapılar içinde tanımlamak, tek bir alan değiştirilmiş olsa bile bütün yapının değiştirilemeyeceği durumlara yol açacaktır.

Değişebilir verilerin söz konusu olduğu durumlarda verilerin parçalarının bağımsız olarak işlendiği özel durumlar vardır. Örneğin değişebilir bir diliminiz varsa, split_at_mut size değişebilir iki referans verecektir. Bu gayet emniyetlidir çünkü Rust bu iki dilimin kazara aynı verileri sahiplenmeyeceğinden emin olacaktır.

Referanslar ve Yaşam Süreleri

Rust, referasın veriden daha uzun süre yaşadığı durumlara izin vermez. Aksi taktirde ölü veriye işaret eden "ölü referanslarımız" bulurdu - segfault kaçınılmaz olurdu.

rustc genelde fonksiyonlardaki yaşam süreleri hakkında mantıklı çıkarımlar yapabilir:

fn pair(s: &str, ch: char) -> (&str, &str) {
    if let Some(idx) = s.find(ch) {
        (&s[0..idx], &s[idx+1..])
    } else {
        (s, "")
    }
}
fn main() {
    let p = pair("hello:dolly", ':');
    println!("{:?}", p);
}
// ("hello", "dolly")

Gayet emniyetlidir çünkü burada herhangi bir sınırlayıcı yoktur. rustc iki karakter dizisinin de fonksiyona aktarılan karakter dizisinin referansları olduğuna karar verir.

Aslında fonksiyon tanımı şu şekildedir:


#![allow(unused)]
fn main() {
fn pair<'a>(s: &'a str, ch: char) -> (&'a str, &'a str) {...}
}

Bu noktasyon çıktıdaki karakter dizilerinin en fazla girdideki karakter dizileri kadar yaşayacağına karar verir. Yaşam sürelerinin aynı olduğu anlamına gelmez bu, onları istediğimiz an düşürebiliriz, sadece "s"ten fazla yaşamayacağını belirtiriz.

Yani, rustc yaşam süresi saptaması (lifetime ellision/?) işimizi biraz daha rahatlatır.

Şimdi eğer fonksiyon iki karakter dizisi alırsa, yaşam sürelerini açıkça belirtmemiz gerekir ki hangi çıktının hangi girdiyi referans aldığını bilmiş olalım.

Bir referans tutan bir yapı tanımladığımız sırada neyin ne kadar yaşam ömrü olduğunu belirtmeliyiz:


#![allow(unused)]
fn main() {
struct Container<'a> {
    s: &'a str
}
}

Burada da yapının referanstan daha uzun süre yaşamayamayacağını vurgulamış oluyoruz. Hem yapılar hem de fonksiyonlar için yaşam süresinin <> içerisinde tıpkı tip belirtilir gibi belirtilmesi gereklidir.

Kapamalar gayet akılcı ve güçlü bir özelliktir - Rust kapamalarının gücü buradan gelir. Eğer onları bir yerde saklamak isterseniz onlara bir yaşam süresi belirtmeniz gerekir. Çünkü kapamalar kendi özünde çevresinden referanslar alan ve çağrılabilen bir yapıdır. Mesela m ve c şeklinde değişemez referanslar alabilen linear kapamasına bakalım.


#![allow(unused)]
fn main() {
let m = 2.0;
let c = 0.5;

let linear = |x| m*x + c;
let sc = |x| m*x.cos()
...
}

linear ve sc kapamalarının ikisi de Fn(x: f64) -> f64 özelliğine sahiptir ancak ikisi de aynı yaratık değildir - ikisinin de farklı tipleri ve boyutları vardır! Eğer saklamak isterseniz tiplerini Box<Fn(x: f64)->f64 + 'a> olarak belirtilmelilerdir.

JavaScript veya Lua'da kapamaların nasıl da su gibi aktığına görmüşseniz size biraz sinir bozucu gelecektir. Ancak C++ da Rust ile aynı şeyi yapar ve sanal çağrılar için ufak bir bedel karşılığında farklı türden kapamaları saklamak için std::function'a ihtiyaç duyar.

Karakter Dizileri

Rust'ta karakter dizilerinin başlangıçta sinir bozucu gelmesi olağandır. Onları üretmenin birden çok yolu vardır ve hepsi gereksiz detaylı gelir.


#![allow(unused)]
fn main() {
let s1 = "hello".to_string();
let s2 = String::from("dolly");
}

"hello" hâli hazırda bir karakter dizisi değil midir? Yani, bir şekilde. String sahipli bir karakter dizisidir ve heap üzerinde yer tutar. Bir karakter dizisi kalıbı olan &str (karakter dizisi dilimi) tipi ise (sabit olarak) çalıştırılabilir dosyanın içinde bekler ya da String'ten ödünç alınarak oluşturulur. Sistem programlama dillerinin bu ayrıma ihtiyacı vardır - ufak bir mikrokontrollerı düşünür, azıcık RAM ve biraz daha fazla RM'u bulunur. Karakter dizisi kalıpları daha az enerji harcayan ve ucuz olan ROM'da ("read-only/salt okunur") depolanır.

Fakat bu C++'da çok daha kolay (diyebilirsiniz):

std::string s = "hello";

Kısaca evet, fakat bir karakter dizisinin örtükçe nasıl oluşturulduğunu da gizlemektedir. Rust bellek tahsis etme konusunda açık olmayı tercih eder, hâliyle to_string gibi şeyler var. Öbür taraftan, C++ karakter dizilerini ödünç almak için c_str kullanmalısınız ve C'nin karakter dizileri çok kullanışsızdır.

Neyse ki, Rust'ta işler çok daha iyi işliyor - String ve &str tiplerinin ikisinin de gerekli olduğunu bir kere kabul ederseniz. String metotları çoğunlukla karakter dizisini değiştirmek içindir, mesela push bir karakter ekler (alttan alta Vec<u8> gibi çalışır). Fakat &str metotlarının tamamına da sahiptir. Deref mekanizması aracılığıyla bir String aynı zamanda &str olarak iletilebilir - bu yüzden nadiren fonksiyon tanımlarında &String görürsünüz.

Çeşitli özellikler (trait) aracılığıyla &str'den String elde etmenin pek çok yolu var. Rust özelliklerin tiplerinin genellenerek çalışmasına izin verir. Pratik bir kural olarak, Display özelliğine sahip her şey to_string'e sahiptir; 42.to_string() gibi.

Bazı operatörler beklediğiniz gibi davranmaz:


#![allow(unused)]
fn main() {
    let s1 = "hello".to_string();
    let s2 = s1.clone();
    assert!(s1 == s2);  // cool
    assert!(s1 == "hello"); // fine
    assert!(s1 == &s2); // WTF?
}

Hatırlayın ki String ve &String birbirinden farklı tiplerdir ve == bu tarz komibasyonlarda tanımlı değildir. C++ programlamaya alışık bir kişi değerlerin yerine referansların konulduğunu görmeyi beklediğinden şaşırabilir. Ek olarak &s2 kendiğinden &str olmayacaktır çünkü deref zorlaması bir &str değişkeni veya argümanı atadığınız zaman çalışacaktır. (s2.as_str() diye açıkça ifade etmek işinize yarayacaktır.)

Ancak bu harbiden bir "has***tir artık" denmeyi hakeder:


#![allow(unused)]
fn main() {
let s3 = s1 + s2;  // <--- no can do
}

İki String değerini birleştiremezsiniz, ancak bir String ile &str'i birleştirebilirsiniz. Fakat &str ile String'i birleştiremezsiniz. Bu yüzden pek çok insan + yerine format! makrosunu tercih eder, biraz daha tutarlıdır ancak o kadar da verimli değildir.

Bazı karakter dizisi işlemleri de mevcuttur ancak farklı çalışır. Mesela pek çok dilin split metotu vardır ki bu da bir karakter dizisini, karakter dizisi listesine dönüştürür. Rust'ta karakter dizisi metodu bir döngüleyici döner ve bunu "sonra" bir vektör içerisine toplayabilirsiniz.


#![allow(unused)]
fn main() {
let parts: Vec<_> = s.split(',').collect();
}

Eğer hızlıca bir vektör almak istiyorsanız biraz göze batabilir fakat yeni bir vektörü belelkte tahsis etmeden önce birkaç işlem yapabilirsiniz. Mesela parçalanmış bir metin içerisinden en uzun kelimeyi mi almak istiyorsunuz?


#![allow(unused)]
fn main() {
let max = s.split(',').map(|s| s.len()).max().unwrap();
}

(Eğer doş bir döngüleyici varsa maksimum değer de olmayacaktır ve bu durumu kontrol etmek için unwrap kullanıyoruz.)

collect metotu bize parçaların orijinal karakter dizisinden ödünç alındığı bir Vec<&str> döner - sadece referanslar için bellekte alan tahsis etmemiz gerekir. C++'da çalıştığı gibi bir metot yok fakat son zamana dek her alt diziye ayrıca alan tahsis edilmesi gerekiyordu. (C++ 17 ile beraber Rust'taki &str gibi çalışan std::string_view geldi.)

Noktalı virgüller hakkında bir not

Noktalı virgüller bu dilde de zorunludur, fakat C'de noktalı virgül konulmaması gereken yerlerde Rust'ta da konulmaz, mesela {} bloklarından sonra. Aynı zamanda enum ve struct içinde de onlara ihtiyaç yoktur. (Bu C'den gelen bir acayiplik) Fakat, eğer blok bir değer dönmeliyse noktalı virgüllere ihtiyaç kalmaz.


#![allow(unused)]
fn main() {
    let msg = if ok {"ok"} else {"error"};
}

Bütün bir let deyiminin ardından yine bir noktalı virgül koymak zorunda olduğumuza dikkat edin!

Eğer noktalı karakter dizisi kalıplarından sonra noktalı virgül koysaydık bize () dönerdi. (Nothing veya void gibi) Bu fonksiyondan değer dönerken karşılaşılan olağan bir hatadır.


#![allow(unused)]
fn main() {
fn sqr(x: f64) -> f64 {
    x * x;
}
}

rustc size bu durumda açıklayıcı bir hata mesajıyla geri dönüş yapacaktır.

Rust, Haskell ve Ruby gibi expression-based bir dildir ve bu kavramı "ifade odaklı" olarak düşünebilirsiniz. Bu tarz dillerde her ifadenin bir değeri vardır

C++ ile Alakalı Konular

Rust'ta Değer Semantikleri Farklıdır

C++'da ilkel tipler gibi davranacak ve kendisini kopyalayacak tipler tanımlamak mümkündür. Ek olarak, bir değerin geçici bir bağımdan başka bir bağlama nasıl taşınacağını belirlemek adına taşıma oluşturucusu kullanılır.

Rust'ta ilkel tipler beklendiği gibi davranır fakat Copy özelliği sadece kopyalanabilir türler içeriyorsa (yapılar, demetler veya numaralandırma) kullanıcının tanımladığı tiplere eklenebilir. Diğer tiplere Clone eklenebilir ancak bu sefer de verilerin clone metotunu çağırmanız gerekir. Rust, herhangi bir bellekte alan tahsis etme işleminin açıktan olmasını ister ve atama operatörlerini ya da kopyalama oluşturucularını gizlemez.

Yani, kopyalama ile taşıma her zaman birkaç biti hareket etmesi olarak tanımlanır ve geçersiz kılınamaz.

Eğer s1 Copy özelliğini içermeyen bir türse s2 = s1 bir taşımaya sebep olur ve bu s1'i tüketir! Eğer gerçek bir kopya üretmek istiyorsanız clone kullanın.

Ödünç alma çoğu zaman kopyalamadan daha iyidir ancak bu sefer de ödünç alma kurallarını takip etmelisiniz. Neyse ki, ödünç alma işlemi yeniden düzenlenebilir bir davranıştır. Mesela String, &str olarak ödünç alınabilir ve &str'nin değişmeyen metotlarını kullanabilir. Karakter dizisi dilimleri de C++'ın const char*dan farksız c_str anlayışındaki ödünç alma yöntemine kıyasla çok daha güçlüdür. &str sahiplenilmiş birkaç baytın işaretçisinden (veya bir karakter dizisi kalıbından) ve boyut bilgisinden oluşur. Bu, bellek açısından oldukça verimli örüntüler kurmamıza yardımcı olur. Mesela bütün karakter dizilerinin bir karakter dizisinden ödünç alındığı bir Vec<&str> oluşturulabilir - tek ihtiyacınız olan vektör için ek alan olacaktır:

Mesela, boşluklardan bölerken:


#![allow(unused)]
fn main() {
fn split_whitespace(s: &str) -> Vec<&str> {
    s.split_whitespace().collect()
}
}

Aynı şekilde, C++'daki s.substr(0,2) her zaman karakter dizisinin kopyasını oluşturur ancak dilim sadece ödünç alır: &[0..2]

Buna benzer ilişki Vec<T> ve &[T] arasında da bulunur.

Paylaşılan Referanslar

C++'da bulunduğu gibi Rust için de akıllı işaretçiler (smart pointers) bulunur - mesela std::unique_ptr muadili Boxtur. Bellek ve tahsis edilmiş diğer kaynaklar Box kapsam dışına çıktığı zaman geri iade edildiğinden delete kullanmaya ihtiyaç yoktur. (Rust RAII'yi epeyce benimsemiştir.)


#![allow(unused)]
fn main() {
let mut answer = Box::new("hello".to_string());
*answer = "world".to_string();
answer.push('!');
println!("{} {}", answer, answer.len());
}

İnsanlar başlangıçta to_string'i başlangıçta pek sevmez ancak işleri açıktan yapmayı sağlar.

Açık dereferans operatörü olan * önemli ancak metotlar üzerinde herhangi bir özel notasyon kullanmadığımıza dikkat edin. (Mesela burada (*answer).push('!') yok)

Ödünç almanın sadece orijinal içeriğin sahibi belli olduğu zaman işe yaradığı açıktır. Çoğu tasarımda bu mümkün değildir.

Bu C++'da std::shared_ptr'in kullanıldığı yerdir; kopyalama sadece veri üzerindeki referans sayısını attırır. Bunun da bir bedeli var, üstelik:

  • veri sadece salt okunabilir olsa bile, sürekli referans sayımının arttırılması önbelleğin doğrulanamamasına sebep olabilir.
  • std::shared_ptr süreçler arası emniyetlice paylaşılabilecek şekilde tasarlanmıştır ve kendi kilidini de beraberinde taşıması kaba maliyeti arttırmaktadır.

Rust'ta std::rc::Rc da aynı zamanda referans sayımı yapan paylaşılan akıllı işaretçi gibi davranır. Fakat, bu sadece değişemez referanslar içindir! Eğer süreç anlamında emniyetli bir türünü istiyorsanız, std::sync::Arc ("Atomik Rc") kullanabilirsiniz. Rust iki farklı tür sunduğu için biraz tuhaf görünebilir fakat süreçlerin işin içine girmediği işlemler için kaba maliyeti arttırmamış olursunuz.

Değişemez referanslar olmalarının sebebi bunun Rust'ın bellek modelinin esaslarıyla olmasıyla alakalıdır. Fakat yine de sıyrılmanın bir yolu vardır: std::cell:RefCell. Eğer paylaşılan referansınızı Rc<RefCell<T>> olarak tanımlarsanız borrow_mut ile değişebilir referans edinebilirsiniz. Bu sefer Rust kurallarını dinamik olarak uygularsınız - mesela zaten bir referans varken ek olarak borrow_mut kullanırsanız bir paniğe sebep olacaktır.

Yine de bu hâlen daha emniyetlidir. Panikler bellekte yanlış bir yere dokunulduğu andan önce gerçekleşir! Tıpkı fırlatılan hatalar gibi, çağrı sırası teker teker boşaltılır. Bu derece yapılandırılmış bir süreç için talihsiz bir kelime seçimi diyebiliriz - bu panikleyerek kapanmak yerine sıralı bir temizliktir.

Rc<RefCell<T>> tipi göze biraz biraz batıyor olabilir, ancak kullanılış şekli kesinlikle kötü değil. Burada, Rust (tekrardan) işlerin açıktan yürümesini tercih etmiş oluyor.

Ortak durumu eğer bellek açısından güvenli bir şekilde paylaşmak istiyorsanız Arc<T> tek emniyetli yoldur. Eğer değişebilir erişimlere ihtiyaçlarınız varsa Rc<RefCell<T>> yerine Arc<Mutex<T>> kullanırsınız. Mutex tanımlandığından biraz daha farklı çalışır, bu veri için bir kutu görevi görür. Veriyi lock ile alır ve düzenlersiniz.


#![allow(unused)]
fn main() {
let answer = Arc::new(Mutex::new(10));

// in another thread
..
{
  let mut answer_ref = answer.lock().unwrap();
  *answer_ref = 42;
}
}

Neden unwrap? Eğer kilidi elinde tutan süreç paniklerse lock hata verir. (Resmi dokümentasyon bu tarz durumlarda unwrap kullanmanın mantıklı olduğunu düşünür çünkü belli bir şeyler çok yanlış gitmiş. Panikler süreçlerin içerisinde yakalanabilir.)

Özel kilidi mümkün olduğu sürece kısa sürece tamamlayıp işi teslim etmek (Mutexlerde her zaman olduğu gibi) önemlidir. Onları sınırlı bir kapsam içerisinde tutmak yaygın bir tercihtir - değişebilir referans kapsam dışına çıktığı zaman kilit de sona ermiş olur.

C++'ın sadeliği ile bunu kıyaslayınca ("dostum sadece shared_ptr kullan") göze biraz acayip görünüyor. Fakat bu şekilde paylaşılan veriler arasındaki herhangi bir düzenleme daha kolay fark ediliyor ve Mutex kilidi örüntüsü süreç emniyetine yönlendiriyor.

Her şeyde olduğu gibi, paylaşılan referansları kullanırken dikkatli olun..

Döngüleyiciler

C++'da döngüleyiciler olağan bir yoldan yapılamaz; akıllı işaretçilere sahiptirler ve genellikle c.begin() ile başlar ve c.end() ile biterler. Döngüleyiciler üzerindeki operasyonlar yalnız başına şablon fonksiyonları olarak kullanılır, std::find_if gibi.

Rust döngüleyicileri ise Iterator özelliği ile tanımlanırlar; next bize bir Option döner ve Option artık bir None olduğu zaman işimiz biter.

Bilinen işlemler artık birer metot. Aşağıda find_if'in muadilini görebilirsiniz. Bize bir Option döner (çünkü hiç yoksa None cevabı alırız) ve if let deyimi aracılığıyla None olmayan durumda değere erişebiliriz:


#![allow(unused)]
fn main() {
let arr = [10, 2, 30, 5];
if let Some(res) = arr.find(|x| x == 2) {
    // res is 2
}
}

Emniyetsizlik ve Bağlı Listeler

Rust'ın standart kütüphanesinde unsafe (emniyetsiz kod bloğu) kullanıldığı bir sır değil. Bu ödünç alma kontrolünün muhafazakar anlayışını ihlal etmez. "emniyetsiz (unsafe)" kelimenin özel bir anlamı olduğuna dikkat edin - Rust'ın derleme zamanında anlayamadığı işlemler. Rust'ın perspektifinden C++ her an ve her zaman emniyetsiz modda çalışır! Büyük uygulamalarda birkaç düzine satırlık emniyetsiz kod gerekiyorsa, ki bu da olabilir, bir hata olduğu zaman bu satırların bir insan tarafından dikkatlice incelenmesi yeterli olacaktır. Bilirsiniz, insanoğlu 100 bin satır üstü kodları okumakta pek başarılı değildir.

Bundan bahsediyorum çünkü ortada bir örüntü var, tecrübeli bir C++ programcısı bir ağaç yapısını ya da bağlı liste oluşturduğu zaman kendisini biraz yılgın hisseder. Pekâlâ, çift bağlı liste üretmek emniyetli Rust için mümkündür; Rc akışı yönlendirir ve Weak referanslar aradan çekilir. Ancak standart kütüphane daha fazla performans elde etmek için... işaretçileri kullanır.