Rust'ta enum, bir değerin birkaç olası varyanttan yalnızca biri olabileceğini ifade eden tiptir ve her varyant kendi verisini taşıyabilir. Standart kütüphanedeki Option<T> da sıradan bir enum'dur: Some(T) bir değerin var olduğunu, None olmadığını söyler. Rust'ta null yoktur; değerin eksik olabileceği her yer tipte Option olarak görünür ve derleyici None durumunu ele almanızı ister.

Bu yazıda enum tanımlamayı, veri taşıyan varyantları, enum'lara metot eklemeyi, ayırıcı (discriminant) değerlerini, Option'ın null'a göre neden daha güvenli olduğunu ve günlük kodda en sık kullanılan Option metotlarını çalışan örneklerle inceliyoruz. Enum değerlerini match ile işlemenin ayrıntıları Rust match ve if let yazısında, hata döndürmek için kullanılan Result ise Rust Result ve hata yönetimi yazısında.

Enum tanımlamak ve varyantlar

Struct'lar alanları bir araya getirir, enum'lar ise bir değerin olası seçeneklerinden birini temsil eder. The Book'un IP adresi örneği bunu iyi gösterir: bir adres ya dördüncü sürüm ya da altıncı sürümdür, ikisi birden olamaz. Enum'un en güçlü yanı, her varyantın farklı tipte ve miktarda veri taşıyabilmesidir; aynı şeyi struct ile yapmak için ayrı bir tür alanı ve her durum için ayrı veri alanları gerekirdi.

rust
enum IpAdres {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn yaz(ip: &IpAdres) -> String {
    match ip {
        IpAdres::V4(a, b, c, d) => format!("{a}.{b}.{c}.{d}"),
        IpAdres::V6(s) => s.clone(),
    }
}

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

impl Mesaj {
    fn aciklama(&self) -> String {
        match self {
            Mesaj::Cikis => String::from("çıkış"),
            Mesaj::Tasi { x, y } => format!("taşı: {x},{y}"),
            Mesaj::Yaz(metin) => format!("yaz: {metin}"),
            Mesaj::RenkDegistir(r, g, b) => format!("renk: {r} {g} {b}"),
        }
    }
}

fn main() {
    let ev = IpAdres::V4(127, 0, 0, 1);
    let geri = IpAdres::V6(String::from("::1"));
    println!("{} {}", yaz(&ev), yaz(&geri));

    let mesajlar = [
        Mesaj::Cikis,
        Mesaj::Tasi { x: 3, y: 4 },
        Mesaj::Yaz(String::from("merhaba")),
        Mesaj::RenkDegistir(0, 160, 255),
    ];
    for m in &mesajlar {
        println!("{}", m.aciklama());
    }
}
// çıktı: 127.0.0.1 ::1
// çıktı: çıkış
// çıktı: taşı: 3,4
// çıktı: yaz: merhaba
// çıktı: renk: 0 160 255

Mesaj enum'unda dört farklı varyant biçimi bir arada: veri taşımayan Cikis, struct gibi adlandırılmış alanları olan Tasi, tek bir String taşıyan Yaz ve üç sayı taşıyan RenkDegistir. Bunları dört ayrı struct ile modelleseydiniz her biri ayrı bir tip olurdu ve hepsini kabul eden tek bir fonksiyon yazmak zorlaşırdı. Enum ise tek bir tiptir; impl bloğuyla struct'lardaki gibi metot da ekleyebilirsiniz.

Standart kütüphane de IP adresleri için aynı fikri kullanır: std::net::IpAddr varyantlarının içinde Ipv4Addr ve Ipv6Addr adlı struct'lar taşır. Kendi IpAdres tipinizi tanımlamanız buna engel olmaz, çünkü standart tanımı kapsamınıza almadıkça ad çakışması yaşanmaz.

Enum mı, struct mı?

Enum mı, struct mı? Değer ne içerir?, Yeni seçenek eklemek, Veriye erişim, Metot tanımlama

İkisi de kendi tipinizi tanımlamanın yoludur ama farklı soruları yanıtlar. Struct "bu değerin şu alanların hepsi vardır" der; enum ise "bu değer şu seçeneklerden tam olarak biridir" der. Bir siparişin müşteri adı, tutarı ve tarihi aynı anda bulunur, bu yüzden struct uygundur. Siparişin durumu ise beklemede, kargoda ya da teslim edildi durumlarından yalnızca biridir ve her durumun ayrı verisi olabilir; burada enum doğal seçimdir. İkisi sık sık birlikte kullanılır: bir struct alanı enum olabilir, bir enum varyantı da struct taşıyabilir.

SoruStructEnum
Değer ne içerir?Tüm alanları birlikteVaryantlardan yalnızca birini
Yeni seçenek eklemekYeni alan eklenirYeni varyant eklenir, match ifadeleri güncellenmeli
Veriye erişimAlan adıyla: p.xDesen eşleştirmeyle: match, if let
Metot tanımlamaimpl bloğuimpl bloğu

Enum'a yeni bir varyant eklediğinizde, bu enum üzerinde kapsayıcı match yazan her yerde derleyici eksik durumu bildirir. Bu, büyüyen kod tabanlarında unutulan durumları bulmanın güvenilir bir yoludur.

Varyant adı bir kurucu fonksiyondur

The Book'un vurguladığı ayrıntı şudur: veri taşıyan her varyantın adı, aynı zamanda o varyantı oluşturan bir fonksiyon gibi davranır. IpAdres::V6 bir String alıp IpAdres döndüren bir fonksiyondur. Bu yüzden varyant adını doğrudan map gibi fonksiyon bekleyen yerlere verebilirsiniz:

rust
fn main() {
    let sayilar = vec![1, 2, 3];
    let secenekler: Vec<Option<i32>> = sayilar.into_iter().map(Some).collect();
    println!("{secenekler:?}");
}
// çıktı: [Some(1), Some(2), Some(3)]

Ayırıcı (discriminant) değerleri

Rust Reference'a göre her enum değerinin, hangi varyantı tuttuğunu belirleyen bir tamsayı ayırıcısı vardır. Hiçbir varyantı veri taşımayan (unit-only) enum'larda bu değeri açıkça verebilir ve as ile sayıya dönüştürebilirsiniz. Açıkça verilmeyen ayırıcı bir önceki varyantınkinin bir fazlasıdır; ilk varyant belirtilmemişse sıfırdan başlar.

rust
#[derive(Debug)]
enum Gun {
    Pazartesi = 1,
    Sali,
    Carsamba,
    Pazar = 7,
}

fn main() {
    println!("{}", Gun::Pazartesi as i32);
    println!("{}", Gun::Sali as i32);
    println!("{}", Gun::Carsamba as i32);
    println!("{:?} = {}", Gun::Pazar, Gun::Pazar as u8);
}
// çıktı: 1
// çıktı: 2
// çıktı: 3
// çıktı: Pazar = 7

Veri taşıyan varyantları olan bir enum'a açık ayırıcı vermek ise ancak #[repr(u8)] gibi ilkel bir temsil seçildiğinde mümkündür. Günlük kodda varyantlara sayısal anlam yüklemek gerekmedikçe ayırıcıyla uğraşmanız gerekmez.

Option&lt;T&gt;: null yerine tip

Pek çok dilde her değişken ya bir değer ya da null olabilir; null'ı gerçek bir değer gibi kullanmaya çalışmak da çok yaygın bir hata kaynağıdır. The Book, null'ın mucidi Tony Hoare'un onu "milyar dolarlık hatam" olarak nitelediği 2009 konuşmasını aktarır. Rust kavramı reddetmez, uygulamayı değiştirir: değerin olmayabileceği durum Option<T> ile ifade edilir. Bu enum o kadar sık kullanılır ki prelude'a dahildir; Some ve None'ı Option:: öneki olmadan yazabilirsiniz.

Asıl güvenlik, Option<T> ile T'nin farklı tipler olmasından gelir. Derleyici bir Option<i8> değerini kesin var olan bir i8 gibi kullanmanıza izin vermez:

rust
fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);
    let toplam = x + y;
}
// error[E0277]: cannot add `Option<i8>` to `i8`

Sonuç olarak tipi Option olmayan her değerin null olmadığını güvenle varsayabilirsiniz. Tipi Option olan bir değeri kullanmadan önce ise None durumunu açıkça ele almak zorundasınız. None yazdığınız yerde derleyici iç tipi çıkaramazsa tipi belirtmeniz gerekir: let yok: Option<i32> = None;.

rust
fn bol(pay: f64, payda: f64) -> Option<f64> {
    if payda == 0.0 { None } else { Some(pay / payda) }
}

fn main() {
    for payda in [4.0, 0.0] {
        match bol(10.0, payda) {
            Some(sonuc) => println!("sonuç: {sonuc}"),
            None => println!("sıfıra bölünemez"),
        }
    }
}
// çıktı: sonuç: 2.5
// çıktı: sıfıra bölünemez

Değeri çıkarmak: unwrap, expect ve güvenli alternatifler

Değeri çıkarmak: unwrap, expect ve güvenli alternatifler: expect(mesaj), unwrap(), unwrap_or(v), unwrap_or_else(f)

Standart kütüphane belgesi, Some içindeki değeri çıkaran metotları None durumunda ne yaptıklarına göre gruplar. unwrap ve expect panikler; geri kalanlar bir varsayılan üretir:

MetotNone olduğunda
expect(mesaj)Verilen mesajla panikler
unwrap()Genel bir mesajla panikler
unwrap_or(v)Verilen v değerini döndürür
unwrap_or_else(f)f fonksiyonunu çalıştırıp sonucunu döndürür
unwrap_or_default()T tipinin Default değerini döndürür
unwrap_unchecked()Tanımsız davranış; unsafe gerektirir
rust
fn main() {
    let yok: Option<i32> = None;
    println!("{}", yok.unwrap_or(0));
    println!("{}", yok.unwrap_or_else(|| 2 * 21));
    println!("{}", yok.unwrap_or_default());

    let ad: Option<String> = None;
    println!("{:?}", ad.unwrap_or_default());
}
// çıktı: 0
// çıktı: 42
// çıktı: 0
// çıktı: ""

unwrap_or argümanını her durumda hesaplar; varsayılan değer pahalı bir işlemle üretiliyorsa yalnızca gerektiğinde çalışan unwrap_or_else tercih edilir. unwrap çağrısı None üzerinde çalışırsa program şu mesajla panikler:

rust
fn main() {
    let yok: Option<i32> = None;
    let _x = yok.unwrap();
}
// panik: called `Option::unwrap()` on a `None` value

expect aynı işi yapar ama panik mesajını siz belirlersiniz; yok.expect("ayar bulunamadı") çağrısında panik satırı doğrudan ayar bulunamadı olur. Hata ayıklarken hangi varsayımın bozulduğunu görmek için unwrap yerine anlamlı bir mesajla expect yazmak iyi bir alışkanlıktır. Panik ile kurtarılabilir hata ayrımı için Result ve hata yönetimi yazısına bakın.

Dönüştürme ve zincirleme metotları

Option üzerinde çalışırken her seferinde match yazmak yerine, belgedeki dönüştürme metotlarıyla işlemleri zincirleyebilirsiniz. Bu metotlar None değerini değiştirmeden aktarır, yalnızca Some içindeki değeri işler:

rust
fn main() {
    let metin = Some("42");
    let sayi: Option<i32> = metin.and_then(|s| s.parse().ok());
    println!("{sayi:?}");

    let iki_kati = sayi.map(|n| n * 2);
    println!("{iki_kati:?}");

    let tek_mi = sayi.filter(|n| n % 2 == 1);
    println!("{tek_mi:?}");

    println!("{}", sayi.is_some_and(|n| n > 40));
    println!("{}", sayi.map_or(0, |n| n + 1));

    let sonuc: Result<i32, &str> = None.ok_or("değer yok");
    println!("{sonuc:?}");
}
// çıktı: Some(42)
// çıktı: Some(84)
// çıktı: None
// çıktı: true
// çıktı: 43
// çıktı: Err("değer yok")
  • map: Option<T> değerini Option<U> değerine çevirir; fonksiyon yalnızca Some içinde çalışır.
  • and_then: Fonksiyonun kendisi Option döndürüyorsa kullanılır; iç içe Option<Option<U>> oluşmasını önler.
  • filter: Koşulu sağlamayan Some değerini None yapar.
  • is_some_and: Değer varsa ve koşulu sağlıyorsa true döner; None için fonksiyon hiç çalışmaz.
  • map_or: None için varsayılanı, Some için fonksiyonun sonucunu döndürür.
  • ok_or: Option değerini Result'a çevirir; None verilen hata değeri olur.

Option üzerinde yineleme

Belgeye göre Option, en fazla bir eleman üreten bir yineleyici gibi de kullanılabilir: Some(v) tek bir değer, None hiç değer üretmez. chain gibi başka bir yineleyici bekleyen metotlar Option değerini doğrudan kabul eder. Bu, koşullu olarak bir eleman eklemek istediğinizde kullanışlıdır:

rust
fn main() {
    let ek = Some(42);
    let yok: Option<i32> = None;
    let a: Vec<i32> = (1..3).chain(ek).collect();
    let b: Vec<i32> = (1..3).chain(yok).collect();
    println!("{a:?} {b:?}");
}
// çıktı: [1, 2, 42] [1, 2]

? operatörü ve Option toplama

Option döndüren bir fonksiyonun içinde ? operatörünü kullanabilirsiniz: ifade Some ise içindeki değeri verir, None ise fonksiyon hemen None döndürür. Belgedeki örnek, bir yığından iki değer çekip toplamayı tek satıra indirir. Ayrıca Option değerlerinden oluşan bir yineleyiciyi collect ile tek bir Option<Vec<T>> değerine dönüştürebilirsiniz; elemanlardan biri bile None ise sonuç None olur.

rust
fn son_iki_toplam(yigin: &mut Vec<i32>) -> Option<i32> {
    Some(yigin.pop()? + yigin.pop()?)
}

fn main() {
    let mut y = vec![1, 2, 3];
    println!("{:?}", son_iki_toplam(&mut y));
    println!("{:?}", son_iki_toplam(&mut y));

    let tam: Option<Vec<i32>> = [Some(2), Some(4)].into_iter().collect();
    let eksik: Option<Vec<i32>> = [Some(2), None].into_iter().collect();
    println!("{tam:?} {eksik:?}");

    println!("{}", None < Some(0));
}
// çıktı: Some(5)
// çıktı: None
// çıktı: Some([2, 4]) None
// çıktı: true

İkinci çağrıda yığında tek eleman kaldığı için ikinci pop None döndürür ve fonksiyon erken çıkar. Son satır da belgede tanımlı bir sıralama kuralını gösterir: T karşılaştırılabiliyorsa None her Some değerinden küçük sayılır.

Option içeriğini yerinde değiştirmek ve referanslarla çalışmak

Bir Option alanının içindeki değeri dışarı almak, yerine yenisini koymak ya da yoksa oluşturmak için belgede ayrı bir metot grubu vardır. Ayrıca as_ref ve as_deref, sahipliği taşımadan içerideki değere referans almanızı sağlar; bu, özellikle struct alanlarındaki Option<String> değerlerini okurken işe yarar:

rust
fn main() {
    let mut onbellek: Option<String> = None;
    let deger = onbellek.get_or_insert_with(|| String::from("hesaplandı"));
    deger.push('!');
    println!("{onbellek:?}");

    let ad: Option<&str> = onbellek.as_deref(); // Option<String> değil Option<&str>
    println!("{ad:?}");

    let eski = onbellek.replace(String::from("yeni"));
    println!("{eski:?} {onbellek:?}");

    let alinan = onbellek.take(); // yerine None bırakır
    println!("{alinan:?} {onbellek:?}");
}
// çıktı: Some("hesaplandı!")
// çıktı: Some("hesaplandı!")
// çıktı: Some("hesaplandı!") Some("yeni")
// çıktı: Some("yeni") None

take, sahiplik yazısında anlattığımız std::mem::take'in Option'a özel karşılığıdır: değeri taşır ve geride geçerli bir None bırakır. Ayrıntı için Rust ownership kuralları yazısına bakabilirsiniz.

Option ek bellek harcar mı?

Standart kütüphane, belirli tipler için Option<T>'ın T ile aynı boyutta olmasını garanti eder. Bu tipler arasında Box<U>, &U, &mut U, fonksiyon işaretçileri ve NonZero sayı tipleri vardır. Bunlar hiçbir zaman sıfır olamadığı için sıfır değeri None olarak kullanılır; buna null pointer optimization denir:

rust
fn main() {
    println!("{}", size_of::<Box<i32>>() == size_of::<Option<Box<i32>>>());
    println!("{}", size_of::<&u8>() == size_of::<Option<&u8>>());
}
// çıktı: true
// çıktı: true

Yani isteğe bağlı bir referans ya da kutu için Option kullanmanın bellek maliyeti yoktur. Listede olmayan tipler için böyle bir garanti verilmez.

Sık yapılan hatalar

Sık yapılan hatalar: Her yerde unwrap kullanmak, Option<Option<T>> üretmek, Alanı okurken taşımak, None için tip belirtmemek
  • Her yerde unwrap kullanmak: Örnek kodda pratiktir ama gerçek programda None geldiğinde panik demektir. ?, unwrap_or ya da match kullanın.
  • Option<Option<T>> üretmek: Option döndüren bir fonksiyonu map ile çağırırsanız iç içe sonuç alırsınız; and_then kullanın ya da flatten ile bir seviyeyi kaldırın.
  • Alanı okurken taşımak: &self içinden self.ad.unwrap() çağırmak değeri taşımaya çalışır ve derlenmez; önce as_ref() veya as_deref() ile referansa çevirin.
  • None için tip belirtmemek: Tek başına let x = None; derleyicinin iç tipi bilmesini engeller; tip belirtin ya da değişkeni sonradan bir Some ile kullanın.
  • Varsayılanı gereksiz yere hesaplamak: unwrap_or(pahali()) fonksiyonu her zaman çalıştırır; unwrap_or_else(pahali) yalnızca None durumunda çalıştırır.

Sık Sorulan Sorular

Rust enum ile C enum arasındaki fark nedir?

Rust'ta veri taşımayan bir enum C'deki gibi sayısal ayırıcılarla kullanılabilir. Fark, Rust enum varyantlarının veri de taşıyabilmesidir; her varyant farklı tipte ve sayıda alan içerebilir ve match tüm varyantları ele almanızı zorunlu kılar.

Option ne zaman, Result ne zaman kullanılır?

Değerin yokluğu normal bir durumsa ve nedenini anlatmanız gerekmiyorsa Option; işlemin neden başarısız olduğunu çağırana bildirmeniz gerekiyorsa Result kullanılır. İkisi arasında ok_or ve ok() ile dönüşüm yapılabilir.

Enum içinde başka bir enum taşıyabilir miyim?

Evet. Varyantlar string, sayı, struct ve başka enum dahil her tipte veri taşıyabilir. İç içe enum'ları tek bir match deseninde açmak için Rust pattern sözdizimi yazısına bakın.

Option değerini yazdırmak için ne gerekir?

{:?} biçimi için iç tipin Debug uygulaması yeterlidir; Option kendisi Display uygulamaz. Kendi enum'larınızda #[derive(Debug)] eklemek için derive rehberi yazısına bakabilirsiniz.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · match ve if let · Pattern sözdizimi · Result ve hata yönetimi · Struct ve metotlar · Ownership · Rust nedir?