Tüm Rust rehberleri
TEKNİK REHBER TEMEL ENUM 2026

Enum & Pattern Matching
Sum type'lar ve exhaustive match

Veri taşıyan enum'lar (sum type), zorunlu exhaustive match, if let / while let / let else ve destructuring — C union/enum'larıyla karşılaştırmalı.

00 C union'dan sum type'a

C'de bir "etiketli birlik" (tagged union) elle yazılır; doğru kolu okumak tamamen programcının disiplinine kalmıştır. Rust enum'u bu kontratı tip sistemine taşır.

C'de enum aslında isimlendirilmiş bir int'tir — taşıdığı tek bilgi bir tamsayı sabitidir. Bir değer ve o değerin türünü birlikte taşımak istediğinizde, klasik C deseni bir struct içine bir enum etiket ile bir union koymaktır. Sorun şudur: etiket ile birliğin senkron kalacağına dair hiçbir garanti yoktur. Yanlış kolu okursanız, derleyici sizi uyarmaz; sonuç tanımsız davranıştır (UB).

tagged_union.c
// C: etiket + union'ı elle senkron tutmak zorundasın
enum Kind { K_INT, K_FLT };

struct Value {
    enum Kind tag;        // hangi kol geçerli?
    union { int i; float f; } data;
};

struct Value v = { .tag = K_INT, .data.i = 42 };

// Etiketi yok say, yanlış kolu oku -> derleyici susar, sonuç UB
float x = v.data.f;   // i32 bitleri float gibi yorumlanır

Rust'ta aynı kavram bir sum type olarak ifade edilir: enum varyantları doğrudan veri taşır ve etiket (discriminant) ile veriyi dil ayrılmaz biçimde birbirine bağlar. Değere yalnızca match üzerinden, doğru varyantı kanıtlayarak erişebilirsiniz. "Yanlış kolu okuma" diye bir kavram yoktur — derleyici buna izin vermez.

main.rs
// Rust: etiket ve veri tek bir tip-güvenli birimde
enum Value {
    Int(i32),
    Flt(f32),
}

let v = Value::Int(42);

match v {
    Value::Int(i) => println!("int: {i}"),
    Value::Flt(f) => println!("flt: {f}"),
    // Int'i Flt gibi okumanın yolu yok
}
DİKKAT

C'deki union bellek tasarrufu için aynı adresi paylaşır ama hangi alanın "canlı" olduğunu izlemez. Rust enum'u da en büyük varyant + discriminant kadar yer kaplar (overlap eder) fakat canlı varyantı discriminant ile kanıtlar. Aynı bellek modeli, sıfır maliyetli güvenlik.

C: tag + union (elle senkron, UB riski)  →  Rust: enum (dil senkron tutar, UB yok)

Bu bölümde

  • C enum = isimlendirilmiş int; veri taşımaz.
  • Elle yazılan tagged union'da yanlış kolu okumak UB'dir.
  • Rust enum = veri taşıyan, tip-güvenli sum type.
  • Veriye yalnızca doğru varyant kanıtlanarak (match ile) erişilir.

01 Enum tanımı ve veri taşıyan varyantlar

Bir enum varyantı üç biçimden biri olabilir: birim (veri yok), tuple-benzeri (konumsal alanlar) veya struct-benzeri (isimli alanlar). Hepsi tek bir tipte birleşir.

main.rs
enum Message {
    Quit,                       // birim varyant — veri yok
    Move { x: i32, y: i32 },    // struct-benzeri — isimli alanlar
    Write(String),              // tuple-benzeri — tek alan
    ChangeColor(i32, i32, i32), // tuple-benzeri — üç alan
}

let m = Message::Move { x: 10, y: 20 };

Bunu C'de yazmak için dört ayrı struct, bir union ve bir etiket enum gerekirdi. Rust tek bir bildirimle hepsini kapsar; her varyant kendi şekline (shape) sahiptir.

QuitBirim varyant. Yalnızca discriminant; payload yok.
Move { x, y }İsimli alanlar; anonim bir struct gibi davranır.
Write(String)Tek konumsal alan; bir newtype gibi sarmalar.
ChangeColor(..)Çoklu konumsal alan; bir tuple gibi.

Bellek temsili

Bir enum'un boyutu, en büyük varyantın boyutu artı discriminant'ı tutacak kadar yerdir (hizalama dahil). Varyantların payload'ları aynı bölgeyi paylaşır (C union gibi), ancak hangi varyantın canlı olduğunu discriminant belirler.

KavramC karşılığıRust
Etiketayrı enum tag alanıgizli discriminant
Payloadunionvaryant alanları (overlap)
Boyutelle hesapmax(varyant) + tag
Güvenlikprogramcı sorumluderleyici garanti

Niche optimization

Derleyici, bir tipte "kullanılmayan" bit desenleri (niche) varsa discriminant'ı ayrı yer ayırmadan oraya gömebilir. En bilinen örnek Option<&T>: bir referans asla null olamaz, dolayısıyla None değeri tüm-sıfır bit deseniyle kodlanır. Sonuç: Option<&T> bir ham pointer ile aynı boyuttadır — ekstra etiket baytı yoktur.

main.rs
use std::mem::size_of;

// Referans asla null olamaz; None = null pattern'i
assert_eq!(size_of::<&i32>(), size_of::<Option<&i32>>());

// C'de "nullable pointer + ayrı bool" iki ayrı alan olurdu
NOT

Niche optimization C'de elle yapacağınız "magic value" (örn. -1 = yok) hilesinin tip-güvenli, derleyici tarafından otomatik halidir. Option<Box<T>>, Option<NonNull<T>> da aynı sıfır-maliyet avantajından yararlanır.

Bu bölümde

  • Varyantlar birim, tuple-benzeri ve struct-benzeri olabilir.
  • Enum boyutu = en büyük varyant + discriminant.
  • Payload'lar C union gibi belleği paylaşır.
  • Niche optimization sayesinde Option<&T> pointer boyutundadır.

02 match temelleri ve exhaustiveness

match, C'nin switch'ine benzer ama iki temel farkla: tüm durumlar ele alınmak zorundadır ve fallthrough yoktur. Üstelik bir ifadedir — değer döndürür.

main.rs
enum Dir { North, South, East, West }

fn dx(d: Dir) -> i32 {
    // match bir ifadedir; sonucu doğrudan döndürürüz
    match d {
        Dir::East => 1,
        Dir::West => -1,
        Dir::North | Dir::South => 0, // çoklu pattern
    }
}

Yukarıdaki match tüm dört varyantı kapsar. Bir varyantı unutursanız derleyici reddeder — bu, yeni bir varyant eklediğinizde ilgili tüm match'lerin "burayı da güncelle" diye sizi uyarması demektir. C'de eklediğiniz yeni bir enum sabitini güncellemeyi unuttuğunuz switch sessizce yanlış davranır.

DİKKAT

C'de switch kolları break unutulursa bir sonrakine düşer (fallthrough). Rust'ta her kol bağımsızdır; düşme yoktur, break de gerekmez. Birden fazla deseni birlikte ele almak için a | b kullanın.

Catch-all: _ ve bağlama

Kalan tüm durumları tek kolda toplamak için _ (değeri yoksay) veya bir değişken adı (değeri yakala) kullanılır. _ kolu yalnızca gerçekten umursamadığınız durumlar için iyidir; aksi halde exhaustiveness'in koruyuculuğunu kaybedersiniz.

main.rs
let n = 7;
let s = match n {
    1 => "bir",
    2 => "iki",
    other => { // yakalanan değer 'other' olarak bağlanır
        println!("bilinmeyen: {other}");
        "diğer"
    }
};
NOT

match bir ifade olduğundan tüm kollar aynı tipte değer döndürmelidir. Yan etki için değer döndürmeyen bir kol istiyorsanız, kolun değeri () (unit) olur — yine tüm kollar tutarlı olmalıdır.

Bu bölümde

  • match tüm durumları kapsamak zorundadır (exhaustive).
  • Fallthrough yoktur; break gerekmez.
  • match bir ifadedir; kollar aynı tipte değer döndürür.
  • _ veya bir değişken ile catch-all yapılır.

03 Pattern türleri

Pattern, bir değerin şeklini sınamanın ve aynı anda parçalarını bağlamanın diliridir. match kolları, if let, let ve fonksiyon parametreleri hep pattern alır.

PatternAnlamıÖrnek
LiteralSabit değere eşitlik42, "hi", true
DeğişkenDeğeri bir isme bağlax
WildcardEşleş, yoksay_
Çoklu (OR)Herhangi biri1 | 2 | 3
AralıkKapalı aralık1..=5, 'a'..='z'
Binding (@)Sına ve değeri yakalan @ 1..=5
main.rs
let x = 5;

match x {
    0 => println!("sıfır"),              // literal
    1 | 2 | 3 => println!("küçük"),     // çoklu (OR)
    4..=9 => println!("orta"),          // aralık (kapalı)
    n => println!("büyük: {n}"),       // değişkene bağlama
}

@ binding: sına ve aynı anda yakala

Bir aralığa uyup uymadığını test ederken eşleşen değeri de bir isme bağlamak istersiniz. @ ikisini tek kolda birleştirir.

main.rs
enum Msg { Id(u32) }

match Msg::Id(5) {
    // 3..=7 aralığına uyarsa, değeri 'id' olarak da bağla
    Msg::Id(id @ 3..=7) => println!("geçerli aralık: {id}"),
    Msg::Id(id) => println!("aralık dışı: {id}"),
}
NOT

Aralık pattern'i yalnızca kapalı aralık (..=) için tam desteklidir; tamsayı ve char üzerinde çalışır. Yarı-açık .. aralık pattern'i kararsızdır, kullanmayın. a | b ile aralıklar birlikte de yazılabilir: 1..=3 | 7..=9.

Bu bölümde

  • Pattern'ler literal, değişken, _, OR, aralık ve @ biçiminde olur.
  • a | b birden fazla deseni tek kolda toplar.
  • Aralık pattern'i ..= ile tamsayı/char üzerinde çalışır.
  • @ sınamayı ve değeri yakalamayı birleştirir.

04 Destructuring

Pattern matching'in asıl gücü, bileşik bir değeri tek hamlede parçalayıp her parçasını bir isme bağlamaktır — C'de elle yapacağınız alan alan erişimin yerini alır.

main.rs
// Tuple destructuring
let (a, b, c) = (1, 2, 3);

// Struct destructuring — alan adlarıyla
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
let Point { x, y } = p;   // alan kısayolu: x, y otomatik bağlanır

let Point { x, y } = p; satırındaki "alan kısayolu" (field shorthand), x: x, y: y yazmaktan kurtarır. Yalnızca bazı alanları istiyorsanız geri kalanını .. ile atlayabilirsiniz.

İç içe (nested) destructuring

Pattern'ler iç içe geçer; bir enum varyantının içindeki struct'ın içindeki tuple'ı tek pattern ile sökebilirsiniz. Bu, "v.data.shape.color.r" gibi zincirleri tek satıra indirir ve aynı anda doğru varyantı kanıtlar.

main.rs
enum Shape {
    Circle { center: (i32, i32), r: u32 },
    Rect { w: u32, h: u32 },
}

let s = Shape::Circle { center: (3, 4), r: 10 };

match s {
    // varyant + iç tuple + alan tek pattern'de sökülür
    Shape::Circle { center: (cx, cy), r } =>
        println!("daire ({cx},{cy}) yarıçap {r}"),
    Shape::Rect { w, .. } =>        // h'yi atla
        println!("dikdörtgen, w={w}"),
}
NOT

.. bir struct veya tuple'ın "kalanını" atlar; her yapıda yalnızca bir kez kullanılabilir ve konumu net olmalıdır. Tuple'da (first, .., last) gibi baş/son bağlama için de kullanışlıdır.

Bu bölümde

  • Tuple, struct ve enum içerikleri pattern ile sökülür.
  • Alan kısayolu { x, y } tekrar yazımı önler.
  • .. ilgilenmediğiniz alanları atlar.
  • Nested pattern derin erişim zincirlerini tek satıra indirir.

05 if let ve let else

Yalnızca tek bir varyantı umursadığınızda tam bir match abartı kaçar. if let bu durumu zarifçe ele alır; let else ise erken çıkış (guard clause) için idealdir.

main.rs
let cfg: Option<u16> = Some(8080);

// Sadece Some kolu ilgilendiriyor
if let Some(port) = cfg {
    println!("port: {port}");
} else {
    println!("port tanımsız, varsayılan kullanılıyor");
}

if let, "şu pattern'e uyuyorsa içeri gir, parçalarını bağla" demektir; else dalı opsiyoneldir. Ancak if let içindeki bağlamalar yalnızca o blok içinde geçerlidir — bağlanan değeri blok sonrasında kullanmak istediğinizde sağ kenara doğru kayan (rightward drift) iç içe bloklar oluşur.

let else: başarısızlıkta erken çık

let ... else bu sorunu çözer: pattern uyarsa bağlanan değer normal akışta kalır; uymazsa else bloğu çalışır ve bu blok akışı sonlandırmak zorundadır (return, break, continue veya panic!). Bu, tam olarak C'deki "if (!x) return; ... x'i kullan" guard clause desenidir.

main.rs
fn parse_port(s: &str) -> u16 {
    // Uyarsa 'port' bu noktadan sonra erişilebilir
    let Ok(port) = s.parse::<u16>() else {
        return 8080; // else mutlaka akışı bitirmeli
    };
    // rightward drift yok — port düz akışta
    port
}
DİKKAT

let else bloğunun else dalı diverging olmalıdır (asla normal akışa dönmemeli). Eğer else içinde akışı bitirmezseniz derleyici hata verir — bu, guard clause kontratını derleme zamanında zorlar.

Bu bölümde

  • if let tek varyantı zarifçe ele alır, else opsiyoneldir.
  • if let bağlamaları yalnızca blok içinde yaşar.
  • let else başarıda değeri akışta bırakır.
  • let else'in else dalı diverging olmak zorundadır.

06 while let

Bir değer belirli bir pattern'e uyduğu sürece dönen döngüler için while let vardır. Klasik kullanım: bir Option None dönene dek tüketmek.

main.rs
let mut stack = vec![1, 2, 3];

// pop() Option döndürür; None gelene dek dön
while let Some(top) = stack.pop() {
    println!("çıkan: {top}");
}
// 3, 2, 1 yazılır; boşalınca pop() None döner, döngü biter

Bu desen C'de "while ((p = next()) != NULL)" döngüsünün doğrudan, tip-güvenli karşılığıdır. Sentinel değeri (NULL, -1) elle test etmek yerine, None varyantı pattern uymadığında döngüyü doğal olarak sonlandırır.

main.rs
// Iterator'ı elle next() ile tüketmek
let mut it = [10, 20, 30].into_iter();

while let Some(x) = it.next() {
    if x == 20 { break; } // koşullu erken çıkış
    println!("{x}");
}
NOT

Çoğu durumda doğrudan for x in it tercih edilir — daha okunaklı ve idiomatic'tir. while let, döngü içinde iterator/yapı üzerinde ek kontrol (kısmi tüketim, koşullu pop) gerektiğinde parlar.

Bu bölümde

  • while let, pattern uyduğu sürece döner.
  • Stack pop() / iterator next() tüketimi için idealdir.
  • C'deki "while (p = next())" sentinel desenini tipli kılar.
  • Basit yineleme için for daha idiomatic'tir.

07 Match guards

Bir match kolunun pattern'i uyduktan sonra ekstra bir koşul daha sınamak istediğinizde guard kullanılır: => öncesine eklenen bir if ifadesi.

main.rs
let x: Option<i32> = Some(-3);

match x {
    Some(n) if n > 0 => println!("pozitif: {n}"),
    Some(n) if n < 0 => println!("negatif: {n}"),
    Some(_) => println!("sıfır"),
    None => println!("yok"),
}

Guard, pattern'in ifade edemediği runtime koşullarını (aralık dışı karşılaştırma, iki bağlamanın eşitliği, harici durum) kola taşır. Pattern şekli sınar; guard değeri sınar.

Guard ile exhaustiveness ilişkisi

Kritik nokta: derleyici, guard koşulunun runtime'da ne döneceğini bilemez. Bu yüzden guard'lı bir kolu exhaustiveness sayımında "tam kapsadı" olarak kabul etmez. Yalnızca guard'lı kollarınız varsa, tüm değerleri kapsasanız bile derleyici "non-exhaustive" hatası verir; guard'sız bir catch-all kol eklemek zorunda kalırsınız.

main.rs
let n = 5;

// HATA OLURDU: yalnızca guard'lı kollar -> non-exhaustive
// match n { x if x > 0 => .., x if x <= 0 => .. }

// DOĞRU: guard'sız bir catch-all gerekir
match n {
    x if x > 0 => println!("pozitif"),
    _ => println!("diğer"), // guard'sız kapsama
}
DİKKAT

Guard koşulu, a | b ile yazılmış çoklu pattern'in tamamına uygulanır: Some(a) | Some(b) if cond ifadesinde cond her iki alternatif için de geçerlidir, yalnızca sonuncusu için değil.

Bu bölümde

  • Guard, pattern uyduktan sonra ek bir if koşulu sınar.
  • Pattern şekli, guard değeri kontrol eder.
  • Derleyici guard'lı kolu exhaustive saymaz.
  • Guard'lı match'ler genelde bir catch-all kol gerektirir.

08 Option ve Result enum olarak

Rust'ta "null yok", "exception yok" derken kastedilen şey aslında basittir: bu kavramlar standart kütüphanede sıradan birer enum olarak tanımlıdır. Hepsi aynı pattern matching ile açılır.

main.rs
// Std'de tam olarak böyle tanımlıdır:
enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Yani Option bir "nullable" sihirli tip değil; sadece iki varyantlı bir sum type'tır. C'de "değer var mı?" sorusunu NULL pointer veya özel bir sentinel ile, "hata oldu mu?" sorusunu errno veya negatif dönüş kodu ile yanıtlarsınız — ikisi de tip dışı kanallardır. Rust bunları tipin kendisine kodlar, dolayısıyla derleyici "hata kolunu ele almayı unuttun" diyebilir.

main.rs
fn bol(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        return Err("sıfıra bölme".to_string());
    }
    Ok(a / b)
}

match bol(10, 2) {
    Ok(v) => println!("sonuç: {v}"),
    Err(e) => println!("hata: {e}"),
}

// Tek kol yetiyorsa if let de olur
if let Ok(v) = bol(9, 3) {
    println!("bölüm: {v}");
}
NOT

? operatörü, Result/Option üzerinde "Err/None ise erken dön, değilse içteki değeri al" deseninin sözdizimsel kısayolu — temelinde yine bir match vardır. Yani match bütün bu ergonomik araçların altında yatan ilkel yapıdır.

Bu bölümde

  • Option ve Result sıradan iki varyantlı enum'lardır.
  • Yokluk ve hata, tip-dışı kanallar yerine tipe kodlanır.
  • match / if let ile güvenle açılırlar.
  • ? operatörü bu match desenlerinin kısayoludur.

09 Gerçek örnek: durum makinesi

Sum type'ların en güçlü olduğu yer durum makineleridir: geçersiz durumları temsil etmek imkânsız hale gelir, geçişler ise tek bir exhaustive match'te toplanır.

Bir trafik ışığını modelleyelim. C'de bunu bir int state ile yapsaydınız, hiçbir şey size 7 gibi geçersiz bir değer atamanızı engellemezdi. Rust enum'unda yalnızca tanımlı üç durum vardır — derleyici dördüncü bir durumu yazmanıza izin vermez.

main.rs
// Geçersiz durum tip sistemiyle imkânsız: yalnızca bu üçü
enum Light { Red, Green, Yellow }

impl Light {
    // Geçişler tek bir exhaustive match'te
    fn next(self) -> Light {
        match self {
            Light::Red    => Light::Green,
            Light::Green  => Light::Yellow,
            Light::Yellow => Light::Red,
        }
    }

    fn duration_s(&self) -> u32 {
        match self {
            Light::Red    => 30,
            Light::Green  => 25,
            Light::Yellow => 5,
        }
    }
}

Veri taşıyan varyantlarla bunu daha da ileri götürebiliriz. Aşağıdaki basit bir bağlantı protokolü: her durum yalnızca o aşamada anlamlı olan veriyi taşır — örneğin Connected bir oturum kimliği tutar, Disconnected hiçbir şey tutmaz. Böylece "bağlı değilken oturum kimliği okumak" gibi geçersiz erişimler derleme zamanında elenir.

main.rs
enum Conn {
    Disconnected,
    Connecting { retry: u8 },
    Connected { session: u32 },
}

fn step(c: Conn) -> Conn {
    match c {
        Conn::Disconnected => Conn::Connecting { retry: 0 },

        // guard + destructuring birlikte
        Conn::Connecting { retry } if retry < 3 =>
            Conn::Connecting { retry: retry + 1 },

        Conn::Connecting { .. } =>
            Conn::Connected { session: 0xABCD },

        // session yalnızca Connected içinde erişilebilir
        Conn::Connected { session } => {
            println!("oturum aktif: {session:#x}");
            Conn::Connected { session }
        }
    }
}
NOT

Bu "make illegal states unrepresentable" ilkesidir: durumu bir int + bir avuç ilgisiz alan yerine sum type ile modellersiniz; her durumun verisi yalnızca o durumda var olur. Geçersiz kombinasyonlar derleme zamanında ortadan kalkar — runtime kontrolüne ihtiyaç kalmaz.

Disconnected → Connecting{retry:0..3} → Connected{session} → (next)

Bu bölümde

  • Durum makineleri enum + match ile doğal modellenir.
  • Her durum yalnızca kendine ait veriyi taşır.
  • Exhaustive match, eklenen her yeni durumu zorla ele aldırır.
  • Geçersiz durumlar tip sistemiyle imkânsız kılınır.