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).
// 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.
// 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
}
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.
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.
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.
| Kavram | C karşılığı | Rust |
|---|---|---|
| Etiket | ayrı enum tag alanı | gizli discriminant |
| Payload | union | varyant alanları (overlap) |
| Boyut | elle hesap | max(varyant) + tag |
| Güvenlik | programcı sorumlu | derleyici 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.
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
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.
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.
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.
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"
}
};
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;
breakgerekmez. - 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.
| Pattern | Anlamı | Örnek |
|---|---|---|
| Literal | Sabit değere eşitlik | 42, "hi", true |
| Değişken | Değeri bir isme bağla | x |
| Wildcard | Eşleş, yoksay | _ |
| Çoklu (OR) | Herhangi biri | 1 | 2 | 3 |
| Aralık | Kapalı aralık | 1..=5, 'a'..='z' |
| Binding (@) | Sına ve değeri yakala | n @ 1..=5 |
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.
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}"),
}
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 | bbirden 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.
// 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.
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}"),
}
.. 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.
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.
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
}
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 lettek varyantı zarifçe ele alır,elseopsiyoneldir.if letbağlamaları yalnızca blok içinde yaşar.let elsebaşarıda değeri akışta bırakır.let else'inelsedalı 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.
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.
// 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}");
}
Ç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()/ iteratornext()tüketimi için idealdir. - C'deki "while (p = next())" sentinel desenini tipli kılar.
- Basit yineleme için
fordaha 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.
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.
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
}
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
ifkoş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.
// 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.
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}");
}
? 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
OptionveResultsıradan iki varyantlı enum'lardır.- Yokluk ve hata, tip-dışı kanallar yerine tipe kodlanır.
match/if letile 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.
// 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.
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 }
}
}
}
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.