Numaralandırılmış Yapı Tanımlamak

Yapıların size width ve height üyeleri olan bir Rectangle gibi ilgili üyeleri ve verileri gruplandırmanın bir yolunu verdiği yerde, numaralandırmalar size bir değerin olası bir değer kümesinden biri olduğunu söylemenin bir yolunu verir. Örneğin, Rectangle'ın Circle ve Triangle'ı da içeren olası şekillerden biri olduğunu söylemek isteyebiliriz. Bunu yapmak için Rust, bu olasılıkları bir enum olarak kodlamamıza izin verir.

Kodda ifade etmek isteyebileceğimiz bir duruma bakalım ve bu durumda numaralandırmaların neden yapılardan daha yararlı ve daha uygun olduğunu görelim. IP adresleriyle çalışmamız gerektiğini farz edin. Şu anda IP adresleri için iki ana standart kullanılmaktadır: V4 ve V6. Programımızın karşılaşacağı bir IP adresi için tek olasılık bunlar olduğundan, tüm olası değişkenleri sıralayabiliriz, bu da numaralandırmanın adını aldığı yerdir.

Herhangi bir IP adresi, V4 veya V6 adresi olabilir, ancak ikisi aynı anda olamaz. IP adreslerinin bu özelliği, enum veri yapısını uygun hale getirir, çünkü bir enum değeri onun türevlerinden yalnızca biri olabilir. Hem V4 hem de V6 adresleri hala temelde IP adresleridir, bu nedenle kod herhangi bir IP adresi için geçerli olan durumları işlerken aynı tür olarak ele alınmalıdır.

Bu kavramı bir IpAddrKind numaralandırması tanımlayarak ve bir IP adresinin olabileceği olası türleri V4 ve V6 olarak listeleyerek kodda ifade edebiliriz. Bunlar, numaralandırmanın varyantlarıdır:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

IpAddrKind artık kodumuzun başka bir yerinde kullanabileceğimiz özel bir veri türüdür.

enum Değerleri

IpAddrKind'in iki varyantının her birinin örneklerini şu şekilde oluşturabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Numaralandırmanın türevlerinin, tanımlayıcısının altında ad alanlı olduğuna ve ikisini ayırmak için çift iki nokta üst üste (:) kullandığımıza dikkat edin. Bu kullanışlıdır çünkü artık her iki IpAddrKind::V4 ve IpAddrKind::V6 değeri aynı türdedir: IpAddrKind. Daha sonra, örneğin, herhangi bir IpAddrKind alan bir fonksiyon tanımlayabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Ve bu fonksiyonu her iki değişkenle de çağırabiliriz:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

Numaralandırma kullanmanın daha da fazla avantajı vardır. IP adresi türümüz hakkında daha fazla düşünürsek, şu anda gerçek IP adresi verilerini saklamanın bir yolu yok; sadece ne tür olduğunu biliyoruz. Bölüm 5'te yapılar hakkında yeni öğrendiğinize göre, bu sorunu Liste 6-1'de gösterildiği gibi yapılarla çözmeye cazip gelebilirsiniz.

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

    struct IpAddr {
        kind: IpAddrKind,
        address: String,
    }

    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    };

    let loopback = IpAddr {
        kind: IpAddrKind::V6,
        address: String::from("::1"),
    };
}

Liste 6-1: Bir IP adresinin verilerinin ve IpAddrKind değişkeninin struct kullanılarak saklanması

Burada, iki üyesi olan bir IpAddr yapısı tanımladık: IpAddrKind türünde bir tür üyesi (daha önce tanımladığımız numaralandırma) ve String türünde bir adres üyesi. Bu yapının iki örneğine sahibiz. Birincisi home'dır ve 127.0.0.1 ilişkili adres verileriyle kendi türünde IpAddrKind::V4 değerine sahiptir. İkinci örnek geri döngüdür. Tür değeri V6 olarak IpAddrKind'in diğer türevine sahiptir ve onunla ilişkili ::1 adresine sahiptir. Tür ve adres değerlerini bir araya toplamak için bir yapı kullandık, bu yüzden şimdi değişken değerle ilişkilendirildi.

Bununla birlikte, aynı kavramı yalnızca bir numaralandırma kullanarak temsil etmek daha özlüdür: bir yapı içindeki bir numaralandırma yerine, verileri doğrudan her bir numaralandırma değişkenine koyabiliriz. IpAddr enum'un bu yeni tanımı, hem V4 hem de V6 varyantlarının ilişkili String değerlerine sahip olacağını söylüyor:

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

    let home = IpAddr::V4(String::from("127.0.0.1"));

    let loopback = IpAddr::V6(String::from("::1"));
}

enum'un her türevine doğrudan veri ekliyoruz, bu nedenle ekstra bir yapıya gerek yok. Burada ayrıca numaralandırmaların nasıl çalıştığına dair başka bir ayrıntıyı görmek daha kolaydır: tanımladığımız her bir numaralandırma değişkeninin adı aynı zamanda numaralandırmanın bir örneğini oluşturan bir fonksiyon haline gelir. Diğer bir deyişle, IpAddr::V4(), bir String bağımsız değişkeni alan ve IpAddr türünün bir örneğini döndüren bir fonksiyon çağrısıdır. enum'u tanımlamanın bir sonucu olarak bu yapıcı fonksiyonu otomatik olarak tanımlarız.

Bir yapı yerine bir numaralandırma kullanmanın başka bir avantajı daha vardır: her değişken, farklı türde ve miktarda ilişkili veriye sahip olabilir. V4 tip IP adresleri her zaman 0 ile 255 arasında değerlere sahip dört sayısal bileşene sahip olacaktır:

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

    let home = IpAddr::V4(127, 0, 0, 1);

    let loopback = IpAddr::V6(String::from("::1"));
}

V4 ve V6 IP adreslerini depolamak için veri yapılarını tanımlamanın birkaç farklı yolunu gösterdik. Ancak, ortaya çıktığı gibi, IP adreslerini saklamak ve hangi tür olduklarını kodlamak istemek o kadar yaygın ki, standart kütüphanenin kullanabileceğimiz bir tanımı var! Standart kütüphanenin IpAddr'yi nasıl tanımladığına bakalım: tanımladığımız ve kullandığımız tam enum ve varyantlara sahiptir, ancak adres verilerini varyantların içine, her varyant için farklı tanımlanmış iki farklı yapı şeklinde gömer:


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

struct Ipv6Addr {
    // --snip--
}

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

Bu kod, herhangi bir türde veriyi bir numaralandırma değişkeninin içine koyabileceğinizi gösterir: örneğin dizgiler, sayısal türler veya yapılar. Hatta başka bir numaralandırma ekleyebilirsiniz! Ayrıca, standart kütüphane türleri genellikle bulabileceklerinizden çok daha karmaşık değildir.

Standart kütüphanenin IpAddr için bir tanım içermesine rağmen, standart kütüphanenin tanımını kapsamımıza almadığımız için kendi tanımımızı çakışmadan oluşturup kullanabileceğimize dikkat edin. Bölüm 7'de türleri kapsama almak hakkında daha fazla konuşacağız.

Liste 6-2'deki başka bir numaralandırma örneğine bakalım: bu, türevlerinde gömülü çok çeşitli türlere sahiptir.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

Liste 6-2: Varyantlarının her biri farklı miktar ve türde değerleri saklayan bir Message numaralandırması

Bu numaralandırmanın farklı türlerde dört çeşidi vardır:

  • Quit onunla ilişkili hiçbir veriye sahip değil.
  • Move bir yapının yaptığı gibi alanları adlandırmıştır.
  • Write tek bir String'i dahil eder.
  • ChangeColor üç tane i32 değerini dahil eder.

Liste 6-2'dekiler gibi değişkenlerle bir numaralandırma tanımlamak, farklı türde yapı tanımları tanımlamaya benzer, ancak numaralandırmanın struct anahtar sözcüğünü kullanmaması ve tüm değişkenlerin Message türü altında gruplandırılması dışında. Aşağıdaki yapılar, önceki numaralandırma değişkenlerinin sahip olduğu aynı verileri tutabilir:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

Ancak, her biri kendi tipine sahip olan farklı yapıları kullanırsak, bu tür mesajların herhangi birini almak için, tek bir mesaj olan Liste 6-2'de tanımlanan Message enum'u ile yapabileceğimiz kadar kolay bir fonksiyon tanımlayamazdık.

Numaralandırmalar ve yapılar arasında bir benzerlik daha vardır: impl kullanarak yapılar üzerinde yöntemleri tanımlayabildiğimiz gibi, enum'lar üzerinde de yöntemler tanımlayabiliriz. İşte Message enum'umuzda tanımlayabileceğimiz call adında bir metod:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

Metodun gövdesi, metodu çağırdığımız değeri almak için self'i kullanır. Bu örnekte, m adında Message::Write(String::from("hello"))'yu tutan bir değişken oluşturduk ve m.call() çalıştığında self call metodunun gövdesinde olacak.

Standart kütüphanedeki çok yaygın ve kullanışlı olan başka bir numaralandırmaya bakalım: Option.

Option Numaralandırması ve Null Değerlerine Göre Avantajları

Bu bölüm, standart kütüphane tarafından tanımlanan başka bir numaralandırma olan Option'ın örnek olay incelemesini incelemektedir. Option türü, bir değerin bir şey olabileceği veya hiçbir şey olamayacağı çok yaygın senaryoyu kodlar.

Örneğin, öğeleri içeren bir listenin ilkini talep ederseniz, bir değer alırsınız. Boş bir listenin ilk maddesini talep ederseniz, hiçbir şey alamazsınız. Bu kavramı tür sistemi cinsinden 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ı önleyebilir.

Programlama dili tasarımı genellikle hangi özellikleri eklediğinize göre düşünülür, ancak hariç tuttuğunuz özellikler de önemlidir. Rust, diğer birçok dilde bulunan null özelliğine sahip değildir. null, hiçbir değer olmadığı anlamına gelen bir değerdir. null olan dillerde, değişkenler her zaman iki durumdan birinde olabilir: null veya null değil.

null'un mucidi Tony Hoare, “Null References: The Billion Dollar Mistake,” adlı 2009 sunumunda şunları söylüyor:

Ben buna milyar dolarlık hatam diyorum. O zaman, nesne yönelimli bir dilde referanslar için ilk kapsamlı tip sistemini tasarlıyordum. Amacım, derleyici tarafından otomatik olarak gerçekleştirilen kontrol ile referansların tüm kullanımının kesinlikle güvenli olmasını sağlamaktı. Ancak, uygulanması çok kolay olduğu için boş bir referans koymanın cazibesine karşı koyamadım. Bu, son kırk yılda muhtemelen milyarlarca dolarlık acıya ve hasara neden olan sayısız hataya, güvenlik açığına ve sistem çökmesine neden oldu.

Boş değerlerle ilgili sorun, boş olmayan bir değer olarak boş bir değer kullanmaya çalışırsanız, bir tür hata almanızdır. Bu boş veya boş olmayan özellik yaygın olduğundan, bu tür bir hatayı yapmak son derece kolaydır.

Bununla birlikte, null'un ifade etmeye çalıştığı kavram hala kullanışlıdır: null, şu anda geçersiz olan veya herhangi bir nedenle mevcut olmayan bir değerdir.

Sorun gerçekten konseptte değil, uygulanmasındadır. Bu nedenle, Rust'ın boş değerleri yoktur, ancak var olan veya olmayan bir değer kavramını kodlayabilen bir numaralandırmaya sahiptir. Bu numaralandırma Option<T>'dir ve standart kütüphane tarafından aşağıdaki gibi tanımlanır:


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

Option<T> enum'u o kadar kullanışlıdır ki, girişe bile dahil edilmiştir; Bunu açıkça kapsama sokmanız gerekmez. Varyantları da başlangıç bölümüne dahil edilmiştir: Some ve None'ı doğrudan Option:: ön eki olmadan kullanabilirsiniz. Option<T> numaralandırma hala normal bir numaralandırmadı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 bir tür parametresidir ve yaygınları Bölüm 10'da daha ayrıntılı olarak ele alacağız. Şimdilik, bilmeniz gereken tek şey <T>'nin Option enum'unun Some varyantının herhangi bir türden tek bir veri parçasını tutabileceği anlamına geldiğidir. Sayı türlerini ve dize türlerini tutmak için Option değerlerini kullanmanın bazı örnekleri:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

some_number türü Option<i32> şeklindedir. some_char türü, farklı bir tür olan Option<char>'dır. Rust, Some varyantı içinde bir değer belirttiğimiz için bu türlerin çıkarımını yapabilir. absent_number için Rust, genel Otpion türüne açıklama eklememizi gerektirir: derleyici, yalnızca None değerine bakarak karşılık gelen Some varyantının tutacağı türü çıkaramaz. Burada, absent_number için Option<i32> türünde olmasını kastettiğimizi Rust'a söylüyoruz.

Bir Some değerine sahip olduğumuzda, bir değerin mevcut olduğunu ve değerin Some içinde tutulduğunu biliriz. None değerine sahip olduğumuzda, bir anlamda null ile aynı anlama gelir: geçerli bir değerimiz yoktur. Öyleyse neden Option<T> seçeneğine sahip olmak boş değere sahip olmaktan daha iyidir?

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

    let sum = x + y;
}

Bu kodu çalıştırırsak şöyle bir hata mesajı alırız:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`

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

Aslında bu hata mesajı, farklı türler oldukları için Rust'ın bir i8 ve bir Option<i8> nasıl ekleneceğini anlamadığı anlamına gelir. Rust'ta i8 gibi bir değerimiz olduğunda, derleyici her zaman geçerli bir değere sahip olmamızı sağlayacaktır. Bu değeri kullanmadan önce null değerini kontrol etmek zorunda kalmadan güvenle ilerleyebiliriz. Yalnızca bir Option<i8> olduğunda (veya hangi tür değerle çalışırsak çalışalım), muhtemelen bir değere sahip olmama konusunda endişelenmemiz gerekir ve derleyici, değeri kullanmadan önce bu durumu ele aldığımızdan emin olacaktır.

Başka bir deyişle, onunla T işlemleri gerçekleştirmeden önce Option<T> öğesini T'ye dönüştürmeniz gerekir. Genellikle bu, null ile ilgili en yaygın sorunlardan birini yakalamaya yardımcı olur: bir şeyin gerçekte boş olmadığını varsaymak.

Yanlış bir şekilde boş olmayan bir değer varsayma riskini ortadan kaldırmak, kodunuza daha fazla güvenmenize yardımcı olur. Muhtemelen null olabilecek bir değere sahip olmak için, bu değerin türünü Option<T> yaparak açıkça seçmelisiniz. Ardından, bu değeri kullandığınızda, değer boş olduğunda durumu açıkça ele almanız gerekir. Bir değerin Option<T> olmayan bir türü olduğu her yerde, değerin boş olmadığını güvenle varsayabilirsiniz. Bu, Rust'ın null'ın yaygınlığını sınırlamak ve Rust kodunun güvenliğini artırmak için kasıtlı bir tasarım kararıydı.

Öyleyse, Option<T> türünde bir değeriniz olduğunda, bu değeri kullanabilmeniz için Some varyantından T değerini nasıl alırsınız? Option<T> enum'u, çeşitli durumlarda yararlı olan çok sayıda yönteme sahiptir; dokümantasyonundan kontrol edebilirsiniz. Option<T> üzerindeki metodlara aşina olmak, Rust ile olan yolculuğunuzda son derece yararlı olacaktır.

Genel olarak, bir Option<T> değeri kullanmak için her bir değişkeni işleyecek bir koda sahip olmak istersiniz. Yalnızca bir Some(T) değerine sahip olduğunuzda çalışacak bir kod istiyorsunuz ve bu kodun iç T'yi kullanmasına izin veriliyor. None değeriniz varsa ve bu kodda başka bir kodun çalıştırılmasını istiyorsunuz. bir T değeri mevcuttur. match ifadesi, numaralandırmalarla kullanıldığında tam da bunu yapan bir kontrol akışı yapısıdır: sahip olduğu numaralandırmanın hangi türevine bağlı olarak farklı kod çalıştırır ve bu kod, eşleşen değerin içindeki verileri kullanabilir.