Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

match Kontrol Akışı Yapısı

Rust, bir değeri bir dizi desenle (pattern) karşılaştırmanıza ve hangi desenin eşleştiğine bağlı olarak ilgili kodu çalıştırmanıza olanak tanıyan match adında son derece güçlü bir kontrol akışı yapısına sahiptir. Desenler; sabit değerlerden, değişken adlarından, joker desenlerden (wildcards) ve daha pek çok bileşenden oluşabilir; Bölüm 19 tüm farklı desen türlerini ve ne işe yaradıklarını ayrıntılı olarak ele alır. match yapısının gücü, desenlerin yüksek ifade yeteneğinden ve derleyicinin olası tüm durumların eksiksiz biçimde ele alındığını doğrulamasından gelir.

Bir match ifadesini bozuk para ayırma makinesi gibi düşünebilirsiniz: Madeni paralar, üzerinde farklı boyutlarda delikler sıralanan bir raydan aşağı kayar ve her para, karşısına çıkıp içine sığabildiği ilk delikten aşağı düşer. Aynı şekilde değerler de bir match ifadesindeki her bir desenden sırayla geçer ve değerin “uyduğu” ilk desende, çalıştırılmak üzere o desenle ilişkili kod bloğuna düşer.

Madeni paralardan söz etmişken gelin match kullanımı için onları örnek alalım! Liste 6-3’te gösterildiği gibi, türü bilinmeyen bir ABD madeni parasını alan ve tıpkı para sayma makinesi gibi hangi madeni para olduğunu belirleyip değerini kuruş (sent) cinsinden döndüren bir fonksiyon yazabiliriz.

enum MadeniPara {
    Kurus,
    BesKurus,
    OnKurus,
    YirmiBesKurus,
}

fn kurus_degeri(para: MadeniPara) -> u8 {
    match para {
        MadeniPara::Kurus => 1,
        MadeniPara::BesKurus => 5,
        MadeniPara::OnKurus => 10,
        MadeniPara::YirmiBesKurus => 25,
    }
}

fn main() {}
Liste 6-3: Bir enum ve bu enum’ın varyantlarını desen olarak kullanan bir match ifadesi

Gelin kurus_degeri fonksiyonundaki match ifadesini adım adım inceleyelim. İlk olarak match anahtar kelimesini ve ardından bir ifadeyi yazarız; bu örnekte söz konusu ifade para değeridir. Bu yapı if ile kullanılan koşul ifadesine çok benzer, ancak aralarında büyük bir fark vardır: if yapısında koşulun bir Boole (bool) değeri üretmesi gerekirken, burada ifade herhangi bir türde olabilir. Bu örnekteki para değerinin türü, ilk satırda tanımladığımız MadeniPara enum’ıdır.

Ardından match kolları (match arms) gelir. Bir kol iki bölümden oluşur: bir desen ve çalıştırılacak kod. Buradaki ilk kolun deseni MadeniPara::Kurus değeridir; hemen ardından deseni ve çalıştırılacak kodu birbirinden ayıran => operatörü gelir. Bu koldaki kod yalnızca 1 değeridir. Her kol bir sonrakinden virgülle ayrılır.

match ifadesi çalıştırıldığında, elde edilen değeri her bir kolun deseniyle sırasıyla karşılaştırır. Bir desen değerle eşleşirse o desenle ilişkili kod çalıştırılır. Desen değerle eşleşmezse, tıpkı bozuk para ayırma makinesinde olduğu gibi akış bir sonraki kola geçer. İhtiyaç duyduğumuz kadar kol tanımlayabiliriz: Liste 6-3’te match ifademizin dört kolu vardır.

Her kolla ilişkili kod bir ifadedir (expression) ve eşleşen koldaki ifadenin ürettiği değer, match ifadesinin tamamı için döndürülen değer olur.

Liste 6-3’te olduğu gibi her kolun yalnızca tek bir değer ürettiği kısa durumlarda match kollarında genellikle süslü parantez kullanmayız. Bir match kolunda birden fazla satır kod çalıştırmak istiyorsanız süslü parantez kullanmanız gerekir; bu durumda kolu takip eden virgül isteğe bağlıdır. Örneğin aşağıdaki kod, fonksiyon her MadeniPara::Kurus değeriyle çağrıldığında ekrana "Şanslı kuruş!" yazdırır, ancak yine de bloğun son ifadesi olan 1 değerini döndürür:

enum MadeniPara {
    Kurus,
    BesKurus,
    OnKurus,
    YirmiBesKurus,
}

fn kurus_degeri(para: MadeniPara) -> u8 {
    match para {
        MadeniPara::Kurus => {
            println!("Şanslı kuruş!");
            1
        }
        MadeniPara::BesKurus => 5,
        MadeniPara::OnKurus => 10,
        MadeniPara::YirmiBesKurus => 25,
    }
}

fn main() {}

Not: Tıpkı Bölüm 3’te gördüğümüz if gibi match de bir ifadedir. Dolayısıyla ürettiği sonucu doğrudan bir değişkene atayabilir (let tutar = match para { ... };) veya burada olduğu gibi fonksiyonun dönüş değeri olarak kullanabilirsiniz. Derleyicinin derleme zamanında tek ve kesin bir tür belirleyebilmesi için tüm match kollarının aynı türü üretmesi zorunludur; aksi hâlde E0308 (match arms have incompatible types — match kolları uyumsuz türlere sahip) hatası alırsınız. Özellikle süslü parantezli kollarda son satırdaki 1 ifadesinin sonuna yanlışlıkla noktalı virgül (;) koymamaya dikkat edin: 1; yazarsanız o kol birim türü (()) üretirken diğer kollar u8 üreteceği için kod derlenmeyecektir.

Değerlere Bağlanan Desenler

match kollarının bir diğer kullanışlı özelliği, desenle eşleşen değerlerin içindeki parçalara bağlanabilmeleridir. Enum varyantlarının barındırdığı değerleri bu sayede dışarı çıkarabiliriz.

Örnek olarak enum varyantlarımızdan birini içinde veri tutacak şekilde değiştirelim. Amerika Birleşik Devletleri, 1999’dan 2008’e kadar 50 eyaletin her biri için bir yüzünde farklı tasarımlar bulunan çeyreklikler (25 kuruşluk/sentlik madeni paralar) bastı. Başka hiçbir madeni parada eyalet tasarımı yer almadığından bu ek bilgi yalnızca çeyrekliklerde bulunur. Bu bilgiyi Liste 6-4’te yaptığımız gibi YirmiBesKurus varyantını içinde bir Eyalet değeri barındıracak şekilde değiştirerek enum tanımımıza ekleyebiliriz.

#[derive(Debug)] // birazdan eyaleti inceleyebilmek için
enum Eyalet {
    Alabama,
    Alaska,
    // --snip--
}

enum MadeniPara {
    Kurus,
    BesKurus,
    OnKurus,
    YirmiBesKurus(Eyalet),
}

fn main() {}
Liste 6-4: YirmiBesKurus varyantının ayrıca bir Eyalet değeri tuttuğu MadeniPara enum’ı

Bir arkadaşımızın 50 eyalet çeyrekliğinin tamamını biriktirmeye çalıştığını düşünelim. Bozuk paralarımızı türlerine göre ayırırken her çeyreklikle ilişkili eyaletin adını da yüksek sesle söyleyeceğiz; böylece arkadaşımızın koleksiyonunda henüz olmayan bir eyalet çıkarsa onu koleksiyonuna ekleyebilecek.

Aşağıdaki kodun match ifadesinde, MadeniPara::YirmiBesKurus varyantının değerleriyle eşleşen desene eyalet adında bir değişken ekliyoruz. Bir MadeniPara::YirmiBesKurus değeri eşleştiğinde eyalet değişkeni o çeyrekliğin eyalet değerine bağlanır. Ardından bu koldaki kod içinde eyalet değişkenini aşağıdaki gibi kullanabiliriz:

#[derive(Debug)]
enum Eyalet {
    Alabama,
    Alaska,
    // --snip--
}

enum MadeniPara {
    Kurus,
    BesKurus,
    OnKurus,
    YirmiBesKurus(Eyalet),
}

fn kurus_degeri(para: MadeniPara) -> u8 {
    match para {
        MadeniPara::Kurus => 1,
        MadeniPara::BesKurus => 5,
        MadeniPara::OnKurus => 10,
        MadeniPara::YirmiBesKurus(eyalet) => {
            println!("{eyalet:?} eyaletinden çeyreklik!");
            25
        }
    }
}

fn main() {
    kurus_degeri(MadeniPara::YirmiBesKurus(Eyalet::Alaska));
}

kurus_degeri(MadeniPara::YirmiBesKurus(Eyalet::Alaska)) çağrısı yaptığımızda para parametresi MadeniPara::YirmiBesKurus(Eyalet::Alaska) değerini alır. Bu değeri her bir match koluyla sırayla karşılaştırdığımızda MadeniPara::YirmiBesKurus(eyalet) koluna ulaşana kadar hiçbir desen eşleşmez. Bu noktada eyalet değişkeni Eyalet::Alaska değerine bağlanır. Böylece bu değişkeni println! ifadesinde kullanabilir ve MadeniPara enum’ının YirmiBesKurus varyantı içindeki eyalet verisini elde edebiliriz.

Not: Sahipliği sizde olan bir enum üzerinde doğrudan match para yaptığınızda, Eyalet veya String gibi Copy trait’ini uygulamayan iç veriler desendeki değişkene (eyalet) taşınır (move). Bu nedenle match bloğundan sonra para değişkenini tekrar kullanamazsınız. Eğer enum’ın sahipliğini tüketmeden yalnızca içindeki veriyi okumak istiyorsanız referans üzerinden eşleştirme (match &para) yapabilirsiniz. Rust’ın eşleştirme ergonomisi (match ergonomics) özelliği sayesinde match &para yazdığınızda deseni değiştirmeden yine MadeniPara::YirmiBesKurus(eyalet) şeklinde yazabilirsiniz; derleyici eyalet değişkenini veriyi taşımak yerine otomatik olarak &Eyalet referansı biçiminde bağlar.

Option<T> match Deseni

Önceki bölümde Option<T> kullanırken Some varyantının içindeki T değerini elde etmek istemiştik; tıpkı MadeniPara enum’ında yaptığımız gibi Option<T> türünü de match kullanarak işleyebiliriz! Bu kez madeni paraları karşılaştırmak yerine Option<T> varyantlarını karşılaştıracağız, ancak match ifadesinin çalışma mantığı birebir aynı kalır.

Diyelim ki parametre olarak bir Option<i32> alan ve içinde bir değer varsa bu değere 1 ekleyen bir fonksiyon yazmak istiyoruz. Eğer içinde bir değer yoksa fonksiyon None döndürmeli ve başka hiçbir işlem yapmaya çalışmamalıdır.

match sayesinde bu fonksiyonu yazmak son derece kolaydır ve Liste 6-5’teki gibi görünür.

fn main() {
    fn bir_ekle(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let bes = Some(5);
    let alti = bir_ekle(bes);
    let hicbiri = bir_ekle(None);
}
Liste 6-5: Bir Option<i32> üzerinde match ifadesi kullanan bir fonksiyon

bir_ekle fonksiyonunun ilk çağrılışını daha yakından inceleyelim. bir_ekle(bes) çağrısı yaptığımızda bir_ekle gövdesindeki x değişkeni Some(5) değerini alır. Ardından bu değeri her bir match koluyla sırayla karşılaştırırız:

fn main() {
    fn bir_ekle(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let bes = Some(5);
    let alti = bir_ekle(bes);
    let hicbiri = bir_ekle(None);
}

Some(5) değeri None deseniyle eşleşmez; bu yüzden bir sonraki kola geçeriz:

fn main() {
    fn bir_ekle(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let bes = Some(5);
    let alti = bir_ekle(bes);
    let hicbiri = bir_ekle(None);
}

Some(5) değeri Some(i) deseniyle eşleşir mi? Evet, eşleşir! İkisi de aynı varyanttır. i değişkeni Some içindeki değere bağlanır ve 5 değerini alır. Ardından bu match kolundaki kod çalıştırılır: i değerine 1 eklenir ve içinde toplam 6 değerini taşıyan yeni bir Some varyantı oluşturulup döndürülür.

Şimdi Liste 6-5’te x parametresinin None olduğu ikinci bir_ekle çağrısına bakalım. match ifadesine girer ve x değerini ilk kolla karşılaştırırız:

fn main() {
    fn bir_ekle(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let bes = Some(5);
    let alti = bir_ekle(bes);
    let hicbiri = bir_ekle(None);
}

Eşleşti! Eklenecek bir değer olmadığından akış burada tamamlanır ve => operatörünün sağ tarafındaki None değeri döndürülür. İlk kol eşleştiği için diğer kollar hiç kontrol edilmez.

match ile enum’ları bir arada kullanmak pek çok senaryoda son derece işlevseldir. Rust kodlarında şu yapıyı çok sık göreceksiniz: Bir enum üzerinde match uygula, içindeki veriyi bir değişkene bağla ve bu veriye dayanarak ilgili kodu çalıştır. İlk başta biraz yabancı gelebilir, ancak bir kez alıştığınızda bu yapının kullandığınız tüm programlama dillerinde olmasını isteyeceksiniz. Rust kullanıcılarının her zaman en sevdiği özelliklerin başında gelir.

Eşleşmeler Kapsamlıdır

match ifadelerinin ele almamız gereken önemli bir yönü daha vardır: Kolların desenleri olası tüm durumları kapsamak zorundadır. bir_ekle fonksiyonumuzun hata barındıran ve derlenmeyecek olan aşağıdaki sürümüne bakalım:

fn main() {
    fn bir_ekle(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let bes = Some(5);
    let alti = bir_ekle(bes);
    let hicbiri = bir_ekle(None);
}

Burada None durumunu ele almadık; dolayısıyla bu kod hataya yol açacaktır. Neyse ki bu, Rust derleyicisinin anında yakalayabildiği türden bir hatadır. Bu kodu derlemeye çalıştığımızda aşağıdaki hatayı alırız:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/option.rs:598:0
 ::: /rustc/58cf8f9c35b355199727627d07ae34a4e2846d67/library/core/src/option.rs:602:4
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
4 ~             Some(i) => Some(i + 1),
5 ~             None => todo!(),
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error

Not: Derleyicinin ürettiği error[E0004]: non-exhaustive patterns: `None` not covered mesajı, “kapsamlı olmayan desenler: None durumu kapsanmadı” anlamına gelir. Dikkat ederseniz help: satırında derleyici, eksik kalan kolu None => todo!(), şeklinde kodunuza nasıl ekleyebileceğinizi bile doğrudan gösterir.

Rust, olası her durumu kapsamadığımızı bilir; üstelik hangi deseni unuttuğumuzu bile tam olarak söyler! Rust’ta eşleşmeler kapsamlıdır (exhaustive): Kodun geçerli olabilmesi için olası her durumu sonuna kadar, eksiksiz biçimde ele almalıyız. Özellikle Option<T> söz konusu olduğunda, Rust None durumunu açıkça işlemeyi unutmamızı engelleyerek ortada bir değer yokken (null benzeri bir durumdayken) sanki geçerli bir değer varmış gibi varsayımda bulunmamızın önüne geçer; böylece önceki bölümde bahsettiğimiz milyar dolarlık hatayı derleme zamanında imkânsız hâle getirir.

Tümünü Yakalama Desenleri ve _ Yer Tutucusu

Enum’ları ve match yapısını kullanarak belirli birkaç değer için özel işlemler yapabilir, kalan diğer tüm değerler için ise tek bir varsayılan işlem uygulayabiliriz. Şöyle bir oyun geliştirdiğimizi düşünelim: Zar attığınızda 3 gelirse oyuncunuz hareket etmez, bunun yerine süslü yeni bir şapka kazanır. Eğer 7 gelirse oyuncunuz süslü şapkasını kaybeder. Diğer tüm zar değerlerinde ise oyuncunuz oyun tahtasında gelen sayı kadar kare ilerler. Aşağıda bu mantığı uygulayan bir match ifadesi yer alıyor; bu örnekte zar atışının sonucu rastgele üretilmek yerine doğrudan koda sabit olarak yazılmış ve diğer işlemlerin ayrıntıları konumuzun dışında olduğundan gövdesi boş fonksiyonlarla temsil edilmiştir:

fn main() {
    let zar_atisi = 9;
    match zar_atisi {
        3 => suslu_sapka_ekle(),
        7 => suslu_sapkayi_cikar(),
        diger => oyuncuyu_tasi(diger),
    }

    fn suslu_sapka_ekle() {}
    fn suslu_sapkayi_cikar() {}
    fn oyuncuyu_tasi(kare_sayisi: u8) {}
}

İlk iki kolun desenleri 3 ve 7 sabit değerleridir. Kalan olası tüm değerleri kapsayan son kolun deseni ise diger adını verdiğimiz değişkendir. diger kolunda çalışan kod, yakalanan bu değişkeni oyuncuyu_tasi fonksiyonuna argüman olarak aktararak kullanır.

Bir u8 türünün alabileceği olası tüm değerleri tek tek listelememiş olsak da bu kod sorunsuzca derlenir; çünkü son desen özel olarak listelenmeyen tüm değerlerle eşleşir. Bu tümünü yakalama deseni (catch-all pattern), match ifadesinin kapsamlı olma zorunluluğunu karşılar. Desenler sırayla değerlendirildiği için tümünü yakalayan kolu en sona koymamız gerektiğine dikkat edin. Eğer bu kolu daha üst bir sıraya koysaydık ondan sonraki kollar asla çalışmazdı; nitekim tümünü yakalayan bir koldan sonra başka kollar eklerseniz Rust ulaşılamaz desen konusunda sizi uyarır!

Rust ayrıca tümünü yakalayan bir kol istediğimizde ancak yakalanan değeri kullanmak istemediğimizde başvurabileceğimiz özel bir desene sahiptir: _, herhangi bir değerle eşleşen fakat o değere bağlanmayan özel bir yer tutucu desendir. Bu desen Rust’a söz konusu değeri kullanmayacağımızı bildirir; böylece Rust ne değeri taşır ne de kullanılmayan bir değişken hakkında uyarı verir.

Gelin oyunun kurallarını değiştirelim: Artık 3 veya 7 dışında bir sayı atarsanız tekrar zar atmanız gerekiyor. Yakalanan değere artık ihtiyacımız olmadığından kodumuzu diger adlı değişken yerine _ kullanacak şekilde güncelleyebiliriz:

fn main() {
    let zar_atisi = 9;
    match zar_atisi {
        3 => suslu_sapka_ekle(),
        7 => suslu_sapkayi_cikar(),
        _ => tekrar_zar_at(),
    }

    fn suslu_sapka_ekle() {}
    fn suslu_sapkayi_cikar() {}
    fn tekrar_zar_at() {}
}

Son kolda diğer tüm değerleri açıkça yok saydığımız için bu örnek de kapsamlılık şartını eksiksiz karşılar; unuttuğumuz hiçbir durum yoktur.

Son olarak oyunun kurallarını bir kez daha değiştirelim: 3 veya 7 dışında bir sayı attığınızda sıranızda başka hiçbir şey olmasın. Bunu, _ kolunda çalıştırılacak kod olarak birim değerini (“Demet Türü” bölümünde bahsettiğimiz boş demet türü ()) yazarak ifade edebiliriz:

fn main() {
    let zar_atisi = 9;
    match zar_atisi {
        3 => suslu_sapka_ekle(),
        7 => suslu_sapkayi_cikar(),
        _ => (),
    }

    fn suslu_sapka_ekle() {}
    fn suslu_sapkayi_cikar() {}
}

Burada Rust’a önceki kolların desenleriyle eşleşmeyen hiçbir değeri kullanmayacağımızı ve bu durumda herhangi bir kod çalıştırmak istemediğimizi açıkça belirtmiş oluyoruz.

Desenler ve eşleştirme hakkında Bölüm 19’da ele alacağımız daha pek çok ayrıntı var. Şimdilik, match ifadesinin biraz ayrıntılı ve uzun kalabildiği durumlarda oldukça pratik birer alternatif sunan if let ve let...else yapılarına geçeceğiz.