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

Bir Enum Tanımlama

Struct’ların (yapıların) size ilgili alanları ve verileri birlikte gruplandırmanın bir yolunu sunduğu gibi (örneğin genislik ve yukseklik değerlerine sahip bir Dikdortgen gibi), enum’lar da bir değerin olası değerler kümesinden biri olduğunu söylemenin bir yolunu sunar. Örneğin, Dikdortgen’in Daire ve Ucgen’i de içeren olası şekiller kümesinden biri olduğunu söylemek isteyebiliriz. Bunu yapmak için Rust, bu olasılıkları bir enum olarak kodlamamıza (encode) olanak tanır.

Kodda ifade etmek isteyebileceğimiz bir duruma bakalım ve enum’ların neden yararlı olduğunu ve bu durumda struct’lardan neden daha uygun olduğunu görelim. Diyelim ki IP adresleriyle çalışmamız gerekiyor. Şu anda IP adresleri için iki ana standart kullanılıyor: sürüm dört ve sürüm altı. Programımızın karşılaşacağı bir IP adresi için yalnızca bu olasılıklar söz konusu olduğundan, olası tüm varyantları numaralandırabiliriz (enumerate); numaralandırma da adını buradan alır.

Herhangi bir IP adresi sürüm dört veya sürüm altı adres olabilir, ancak aynı anda ikisi birden olamaz. IP adreslerinin bu özelliği enum veri yapısını uygun hale getirir çünkü bir enum değeri varyantlarından yalnızca biri olabilir. Hem sürüm dört hem de sürüm altı adresler temelde hala IP adresleridir, bu nedenle kod herhangi bir IP adresi türüne uygulanan durumları işlerken her ikisine de aynı tür olarak davranılmalıdır.

Bu kavramı, kodda bir IpAdresTuru (IpAddrKind) numaralandırması tanımlayıp bir IP adresinin olabileceği olası türleri olan V4 ve V6’yı listeleyerek ifade edebiliriz. Bunlar enum’ın varyantlarıdır:

enum IpAdresTuru {
    V4,
    V6,
}

fn main() {
    let dort = IpAdresTuru::V4;
    let alti = IpAdresTuru::V6;

    yonlendir(IpAdresTuru::V4);
    yonlendir(IpAdresTuru::V6);
}

fn yonlendir(ip_turu: IpAdresTuru) {}

IpAdresTuru, artık kodumuzun başka yerlerinde kullanabileceğimiz özel bir veri türüdür.

Enum Değerleri

IpAdresTuru’nün iki varyantının her birinin örneklerini şu şekilde oluşturabiliriz:

enum IpAdresTuru {
    V4,
    V6,
}

fn main() {
    let dort = IpAdresTuru::V4;
    let alti = IpAdresTuru::V6;

    yonlendir(IpAdresTuru::V4);
    yonlendir(IpAdresTuru::V6);
}

fn yonlendir(ip_turu: IpAdresTuru) {}

Enum varyantlarının kendi tanımlayıcısı altında adlandırıldığına ve ikisini ayırmak için çift iki nokta üst üste (::) kullandığımıza dikkat edin. Bu faydalıdır çünkü artık IpAdresTuru::V4 ve IpAdresTuru::V6 değerlerinin her ikisi de aynı türdendir: IpAdresTuru. Daha sonra, örneğin, herhangi bir IpAdresTuru alan bir fonksiyon tanımlayabiliriz:

enum IpAdresTuru {
    V4,
    V6,
}

fn main() {
    let dort = IpAdresTuru::V4;
    let alti = IpAdresTuru::V6;

    yonlendir(IpAdresTuru::V4);
    yonlendir(IpAdresTuru::V6);
}

fn yonlendir(ip_turu: IpAdresTuru) {}

Ve bu fonksiyonu her iki varyantla da çağırabiliriz:

enum IpAdresTuru {
    V4,
    V6,
}

fn main() {
    let dort = IpAdresTuru::V4;
    let alti = IpAdresTuru::V6;

    yonlendir(IpAdresTuru::V4);
    yonlendir(IpAdresTuru::V6);
}

fn yonlendir(ip_turu: IpAdresTuru) {}

Enum kullanmanın daha da fazla avantajı vardır. IP adresi türümüz hakkında daha fazla düşünürsek, şu anda asıl IP adresi verisini depolamak için bir yolumuz yok; sadece hangi tür olduğunu biliyoruz. Bölüm 5’te struct’ları yeni öğrendiğiniz için, Liste 6-1’de gösterildiği gibi bu sorunu struct’larla çözmek isteyebilirsiniz.

fn main() {
    enum IpAdresTuru {
        V4,
        V6,
    }

    struct IpAdres {
        tur: IpAdresTuru,
        adres: String,
    }

    let ev = IpAdres {
        tur: IpAdresTuru::V4,
        adres: String::from("127.0.0.1"),
    };

    let geridongu = IpAdres {
        tur: IpAdresTuru::V6,
        adres: String::from("::1"),
    };
}
Listing 6-1: Bir IP adresinin verilerini ve IpAdresTuru varyantını struct kullanarak saklama

Burada, iki alana sahip bir IpAdres (IpAddr) struct’ı tanımladık: IpAdresTuru (daha önce tanımladığımız enum) türünde bir tur (kind) alanı ve String türünde bir adres alanı. Bu struct’ın iki örneği var. İlki ev’dir ve tur değeri olarak IpAdresTuru::V4’e, ilişkili adres verisi olarak da 127.0.0.1 değerine sahiptir. İkinci örnek ise geridongu’dur (loopback). Bu örnek, tur değeri olarak IpAdresTuru’nün diğer varyantı olan V6’ya sahiptir ve bununla ilişkili ::1 adresini barındırır. tur ve adres değerlerini bir araya toplamak için bir struct kullandık, böylece artık varyant değerle ilişkilendirilmiş oldu.

Bununla birlikte, aynı kavramı sadece bir enum kullanarak ifade etmek daha kısa ve özlüdür: Bir struct’ın içine bir enum koymak yerine, verileri doğrudan her bir enum varyantının içine koyabiliriz. IpAdres enum’ının bu yeni tanımı, hem V4 hem de V6 varyantlarının ilişkili String değerlerine sahip olacağını belirtir:

fn main() {
    enum IpAdres {
        V4(String),
        V6(String),
    }

    let ev = IpAdres::V4(String::from("127.0.0.1"));

    let geridongu = IpAdres::V6(String::from("::1"));
}

Verileri doğrudan enum’ın her bir varyantına ekliyoruz, bu nedenle fazladan bir struct’a gerek kalmaz. Burada, enum’ların nasıl çalıştığına dair başka bir ayrıntıyı görmek de daha kolaydır: Tanımladığımız her enum varyantının adı, aynı zamanda o enum’ın bir örneğini oluşturan bir fonksiyon haline gelir. Yani, IpAdres::V4(), bir String argümanı alan ve IpAdres türünde bir örnek döndüren bir fonksiyon çağrısıdır. Enum’ı tanımladığımız için bu kurucu (constructor) fonksiyon otomatik olarak tanımlanmış olarak gelir.

Struct yerine enum kullanmanın başka bir avantajı daha vardır: Her varyant farklı türlerde ve miktarlarda ilişkili verilere sahip olabilir. Sürüm dört IP adresleri her zaman 0 ile 255 arasında değerlere sahip dört sayısal bileşene sahip olacaktır. V4 adreslerini dört u8 değeri olarak depolamak isteyip V6 adreslerini yine de tek bir String değeri olarak ifade etmek isteseydik, bunu bir struct ile yapamazdık. Enum’lar bu durumu kolaylıkla halleder:

fn main() {
    enum IpAdres {
        V4(u8, u8, u8, u8),
        V6(String),
    }

    let ev = IpAdres::V4(127, 0, 0, 1);

    let geridongu = IpAdres::V6(String::from("::1"));
}

Sürüm dört ve sürüm altı IP adreslerini depolamak üzere veri yapılarını tanımlamanın birkaç farklı yolunu gösterdik. Ancak ortaya çıktığı üzere, IP adreslerini depolamak ve hangi türde olduklarını kodlamak o kadar yaygındır ki, standart kütüphanede kullanabileceğimiz bir tanım vardır! Gelin standart kütüphanenin IpAddr’ı nasıl tanımladığına bakalım. Bizim tanımladığımız ve kullandığımız enum ve varyantların aynısına sahiptir, ancak adres verilerini varyantların içine her varyant için farklı şekilde tanımlanan iki farklı struct biçiminde gömer:

#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
}

Bu kod, bir enum varyantının içine her tür veriyi koyabileceğinizi göstermektedir: örneğin string’ler, sayısal türler veya struct’lar. Hatta içine başka bir enum bile ekleyebilirsiniz! Ayrıca, standart kütüphane türleri genellikle sizin bulacağınızdan çok daha karmaşık değildir.

Standart kütüphane IpAddr için bir tanım içerse de, standart kütüphanenin tanımını kapsamımıza dahil etmediğimiz için yine de kendi tanımımızı oluşturup çakışma olmadan kullanabileceğimizi unutmayın. Türleri kapsama getirme konusunu Bölüm 7’de daha ayrıntılı olarak ele alacağız.

Liste 6-2’de başka bir enum örneğine bakalım: Bunun varyantlarına gömülü çok çeşitli türleri vardır.

enum Mesaj {
    Cik,
    Tasi { x: i32, y: i32 },
    Yaz(String),
    RenkDegistir(i32, i32, i32),
}

fn main() {}
Listing 6-2: Varyantlarının her biri farklı miktarlarda ve türlerde değerler depolayan bir Mesaj enum’ı

Bu enum’ın farklı türlere sahip dört varyantı vardır:

  • Cik: Kendisiyle ilişkili hiçbir veriye sahip değildir.
  • Tasi: Tıpkı bir struct’ta olduğu gibi isimlendirilmiş alanlara sahiptir.
  • Yaz: Tek bir String içerir.
  • RenkDegistir: Üç adet i32 değeri içerir.

Liste 6-2’deki gibi varyantlara sahip bir enum tanımlamak, farklı türlerde struct tanımları tanımlamaya benzer, tek fark enum’ın struct anahtar kelimesini kullanmaması ve tüm varyantların Mesaj türü altında birlikte gruplandırılmasıdır. Aşağıdaki struct’lar önceki enum varyantlarının tuttuğu aynı verileri tutabilirdi:

struct CikMesaji; // birim (unit) struct
struct TasiMesaji {
    x: i32,
    y: i32,
}
struct YazMesaji(String); // demet (tuple) struct
struct RenkDegistirMesaji(i32, i32, i32); // demet (tuple) struct

fn main() {}

Ancak her biri kendi türüne sahip olan bu farklı struct’ları kullansaydık, bu mesaj türlerinden herhangi birini alacak bir fonksiyonu, tek bir tür olan Liste 6-2’deki Mesaj enum’ı ile tanımladığımız kadar kolay tanımlayamazdık.

Enum’lar ve struct’lar arasında bir benzerlik daha vardır: Tıpkı struct’larda impl kullanarak metotlar tanımlayabildiğimiz gibi, enum’larda da metotlar tanımlayabiliriz. İşte Mesaj enum’ımızda tanımlayabileceğimiz cagir adlı bir metot:

fn main() {
    enum Mesaj {
        Cik,
        Tasi { x: i32, y: i32 },
        Yaz(String),
        RenkDegistir(i32, i32, i32),
    }

    impl Mesaj {
        fn cagir(&self) {
            // metot gövdesi burada tanımlanabilir
        }
    }

    let m = Mesaj::Yaz(String::from("merhaba"));
    m.cagir();
}

Metodun gövdesi, metodu çağırdığımız değeri elde etmek için self kullanacaktır. Bu örnekte, Mesaj::Yaz(String::from("merhaba")) değerine sahip bir m değişkeni oluşturduk; m.cagir() çalıştığında cagir metodunun gövdesinde self’in değeri bu olacaktır.

Standart kütüphanede çok yaygın ve kullanışlı olan başka bir enum’a bakalım: Option.

Option Enum’ı

Bu bölüm, standart kütüphane tarafından tanımlanan başka bir enum olan Option için bir örnek olay incelemesi yapmaktadır. Option türü, bir değerin bir şey olabileceği veya hiçbir şey olamayacağı son derece yaygın senaryoyu kodlar.

Örneğin, boş olmayan bir listedeki ilk öğeyi isterseniz, bir değer alırsınız. Boş bir listedeki ilk öğeyi isterseniz, hiçbir şey alamazsınız. Bu kavramı tür sistemi açısından ifade etmek, derleyicinin ele almanız gereken tüm durumları ele alıp almadığınızı kontrol edebileceği anlamına gelir; bu işlevsellik, diğer programlama dillerinde son derece yaygın olan hataları (bug) önleyebilir.

Programlama dili tasarımı genellikle hangi özellikleri dahil ettiğiniz açısından düşünülür, ancak hariç tuttuğunuz özellikler de önemlidir. Rust, diğer birçok dilin sahip olduğu null (boş) özelliğine sahip değildir. Null, orada hiçbir değer olmadığı anlamına gelen bir değerdir. Null içeren dillerde değişkenler her zaman iki durumdan birinde olabilir: null veya null olmayan (not-null).

Null değerinin mucidi Tony Hoare, 2009’daki “Null Referanslar: Milyar Dolarlık Hata” (“Null References: The Billion Dollar Mistake”) adlı sunumunda şunları söylemiştir:

Buna benim milyar dolarlık hatam diyorum. O zamanlar nesne yönelimli (object-oriented) bir dilde referanslar için ilk kapsamlı tür sistemini tasarlıyordum. Amacım, derleyici tarafından otomatik olarak gerçekleştirilen kontrollerle, referansların tüm kullanımlarının kesinlikle güvenli olmasını sağlamaktı. Ancak, uygulaması çok kolay olduğu için null referans ekleme cazibesine karşı koyamadım. Bu, muhtemelen son kırk yılda bir milyar dolarlık acıya ve hasara neden olan sayısız hataya, güvenlik açığına ve sistem çökmesine yol açtı.

Null değerleriyle ilgili sorun, null değerini null olmayan bir değer olarak kullanmaya çalışırsanız, bir tür hata almanızdır. Bu null veya null olmama özelliği yaygın olduğundan (pervasive), bu tür bir hata yapmak son derece kolaydır.

Ancak null’un ifade etmeye çalıştığı kavram hala yararlıdır: Null, şu anda herhangi bir nedenle geçersiz olan veya olmayan (absent) bir değerdir.

Sorun aslında kavramda değil, belirli uygulamadadır. Bu nedenle Rust’ta null yoktur, ancak bir değerin var veya yok olması kavramını kodlayabilen bir enum’a sahiptir. Bu enum Option<T>’dir ve standart kütüphane tarafından şu şekilde tanımlanır:

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

Option<T> enum’ı o kadar faydalıdır ki prelude (başlangıç) kütüphanesine bile dahil edilmiştir; açıkça (explicitly) kapsama dahil etmenize gerek yoktur. Varyantları da prelude kütüphanesine dahildir: Some ve None’ı Option:: öneki olmadan doğrudan kullanabilirsiniz. Option<T> enum’ı hala sadece normal bir enum’dır ve Some(T) ve None hala Option<T> türünün varyantlarıdır.

<T> sözdizimi, Rust’ın henüz bahsetmediğimiz bir özelliğidir. Bu genel (generic) bir tür parametresidir ve Bölüm 10’da jenerikleri daha ayrıntılı olarak ele alacağız. Şimdilik bilmeniz gereken tek şey, <T>’nin Option enum’ının Some varyantının herhangi bir türden bir veri tutabileceği anlamına geldiği ve T yerine kullanılan her somut (concrete) türün genel Option<T> türünü farklı bir tür haline getirdiğidir. Sayı türlerini ve karakter (char) türlerini tutmak için Option değerlerinin kullanıldığı bazı örnekler:

fn main() {
    let bir_sayi = Some(5);
    let bir_karakter = Some('e');

    let olmayan_sayi: Option<i32> = None;
}

bir_sayi değişkeninin türü Option<i32>’dir. bir_karakter değişkeninin türü ise farklı bir tür olan Option<char>’dır. Some varyantının içinde bir değer belirttiğimiz için Rust bu türleri çıkarabilir. olmayan_sayi için Rust bizden genel Option türünü açıklamamızı ister: Derleyici, yalnızca bir None değerine bakarak karşılık gelen Some varyantının tutacağı türü çıkaramaz. Burada, Rust’a olmayan_sayi’nın Option<i32> türünde olmasını kastettiğimizi söyleriz.

Bir Some değerimiz olduğunda, bir değerin mevcut olduğunu ve değerin Some içinde tutulduğunu biliriz. Bir None değerimiz olduğunda ise bu, bazı açılardan null ile aynı anlama gelir: Geçerli bir değerimiz yoktur. Peki, Option<T>’ye sahip olmak null’a sahip olmaktan neden daha iyidir?

Kısaca, Option<T> ve T (burada T herhangi bir tür olabilir) farklı türler olduğundan, derleyici bir Option<T> değerini sanki kesinlikle geçerli bir değermiş gibi kullanmamıza izin vermez. Örneğin, aşağıdaki kod derlenmeyecektir, çünkü bir Option<i8>’e bir i8 eklemeye çalışmaktadır:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let toplam = x + y;
}

Eğer bu kodu çalıştırırsak, şuna benzer bir hata mesajı alırız:

$ cargo run
   Compiling enums v0.1.0 ($PROJE/listings/ch06-enums-and-pattern-matching/no-listing-07-cant-use-option-directly)
error[E0277]: cannot add `Option<i8>` to `i8`
   --> src/main.rs:6:20
    |
  6 |     let toplam = x + y;
    |                    ^ no implementation for `i8 + Option<i8>`
    |
    = help: the trait `Add<Option<i8>>` is not implemented for `i8`
help: the following other types implement trait `Add<Rhs>`
   --> $HOME/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ops/arith.rs:99:9
    |
 99 |         impl const Add for $t {
    |         ^^^^^^^^^^^^^^^^^^^^^ `i8` implements `Add`
...
114 | add_impl! { usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128 f16 f32 f64 f128 }
    | ---------------------------------------------------------------------------------- in this macro invocation
    |
   ::: $HOME/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/internal_macros.rs:22:9
    |
 22 |         impl const $imp<$u> for &$t {
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&i8` implements `Add<i8>`
...
 33 |         impl const $imp<&$u> for $t {
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ `i8` implements `Add<&i8>`
...
 44 |         impl const $imp<&$u> for &$t {
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `&i8` implements `Add`
    = note: this error originates in the macro `add_impl` (in Nightly builds, run with -Z macro-backtrace for more info)

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

Sert! Aslında bu hata mesajı, Rust’ın farklı türlerde oldukları için bir i8 ile bir Option<i8>’i nasıl toplayacağını anlamadığı anlamına gelir. Rust’ta i8 gibi bir türde değere sahip olduğumuzda, derleyici her zaman geçerli bir değere sahip olduğumuzdan emin olur. Bu değeri kullanmadan önce null olup olmadığını kontrol etmek zorunda kalmadan güvenle devam edebiliriz. Yalnızca bir Option<i8>’e (veya üzerinde çalıştığımız değerin türü her neyse) sahip olduğumuzda bir değere sahip olmama ihtimalinden endişe duymalıyız; derleyici bu değeri kullanmadan önce bu durumu ele aldığımızdan emin olacaktır.

Başka bir deyişle, bir Option<T> ile T işlemleri gerçekleştirebilmeniz için onu bir T’ye dönüştürmeniz gerekir. Genellikle bu, null ile ilgili en yaygın sorunlardan birini yakalamaya yardımcı olur: Bir şeyin aslında null olduğu halde null olmadığını varsaymak.

Null olmayan (not-null) bir değeri yanlış varsayma riskini ortadan kaldırmak, kodunuza daha fazla güvenmenize yardımcı olur. Null olabilme ihtimali olan bir değere sahip olmak için, o değerin türünü Option<T> yaparak bu durumu açıkça (explicitly) belirtmeniz (opt in) gerekir. Daha sonra, o değeri kullandığınızda, değerin null olduğu durumu açıkça ele almanız istenir. Değerin Option<T> olmayan bir türe sahip olduğu her yerde, değerin null olmadığını güvenle varsayabilirsiniz. Bu, Rust’ta null’un yaygınlığını sınırlamak ve Rust kodunun güvenliğini artırmak için bilinçli (deliberate) olarak verilmiş bir tasarım kararıydı.

Peki, bu değeri kullanabilmek için Option<T> türünde bir değere sahip olduğunuzda T değerini bir Some varyantından nasıl çıkarırsınız? Option<T> enum’ı, çeşitli durumlarda yararlı olan çok sayıda metoda sahiptir; bunları belgelerinden inceleyebilirsiniz. Option<T> üzerindeki metotlara aşina olmak, Rust yolculuğunuzda son derece yararlı olacaktır.

Genel olarak, bir Option<T> değerini kullanmak için, her bir varyantı ele alacak bir koda sahip olmak istersiniz. Yalnızca Some(T) değeriniz olduğunda çalışacak bir kod istersiniz ve bu kodun içteki (inner) T’yi kullanmasına izin verilir. Yalnızca None değeriniz varsa çalışacak başka bir kod istersiniz ve bu kodda kullanılabilecek bir T değeri yoktur. match ifadesi, enum’larla kullanıldığında tam olarak bunu yapan bir kontrol akışı yapısıdır: Sahip olduğu enum’ın hangi varyantına bağlı olarak farklı kodlar çalıştıracaktır ve bu kod eşleşen değerin içindeki verileri kullanabilir.