00 C dizilerinden Rust koleksiyonlarına
C'de bellek yönetimi el emeğidir, C++ STL bunu container'lara devreder; Rust ise aynı container'ları ownership ve borrow kurallarıyla derleme zamanında güvenli kılar.
C'de büyüyebilen bir tampon istediğinizde malloc ile başlar, dolunca realloc ile büyütür, işiniz bitince free edersiniz. Boyutu ayrı bir size_t'de taşırsınız, kapasiteyi başka bir değişkende. Bir hata — çift free, sınır taşması, asılı işaretçi (dangling pointer) — derleyiciye görünmez, çalışma zamanında patlar. C++ STL bu defter tutmayı std::vector, std::string, std::unordered_map gibi sınıflara taşır; ama iterator invalidation hâlâ sizin sorumluluğunuzdadır.
Rust'ın standart kütüphanesi aynı container ailesini sunar: Vec<T>, String, HashMap<K, V>, VecDeque, BTreeMap, HashSet. Fark, bellek defterinin değil, kuralların derleyiciye verilmiş olmasıdır. Her değerin tek bir sahibi (owner) vardır; sahip kapsamdan çıkınca Drop otomatik çağrılır — free'yi unutmak mümkün değildir. Ödünç alma (borrow) kuralı, aynı anda ya tek bir mutable referansa ya da çok sayıda immutable referansa izin verir.
Bu kuralın en çarpıcı sonucu iterator invalidation'ın derleme zamanında yakalanmasıdır. C++'ta bir vector'ü dolaşırken ona push_back yapmak realloc'u tetikleyip tüm iterator'ları geçersiz kılabilir — UB. Rust'ta aynı şey bir borrow ihlalidir ve derlenmez.
fn main() {
let mut v = vec![1, 2, 3];
// C++'ta UB: dolaşırken push_back realloc tetikleyebilir.
for x in &v { // v paylaşımlı olarak ödünç alındı
v.push(*x); // HATA: v zaten ödünç alınmışken mutable erişim
} // cannot borrow `v` as mutable ...
}
Rust'ta "container + boyut + kapasite" üçlüsü tek bir tip içinde paketlenir. Vec<T> tam olarak üç makine kelimesidir: heap işaretçisi, uzunluk (len) ve kapasite (capacity). C'deki üç ayrı değişkenin tek, ihlal edilemez bir nesneye dönüşmüş hâli.
Bu bölümde
- C'nin malloc/realloc/free el emeği yerine Rust koleksiyonları otomatik
Dropile çalışır. - STL container'larının karşılığı std koleksiyonlarıdır, ama kurallar derleyiciye devredilmiştir.
- Ownership tek-sahip; borrow ya bir mutable ya çok immutable referans verir.
- Iterator invalidation, çalışma zamanı UB'si değil, derleme zamanı borrow hatasıdır.
01 Vec<T>
Vec<T>, std::vector<T>'nin doğrudan karşılığıdır: bitişik (contiguous), büyüyebilen, heap üzerinde tek bir dizi.
Eleman eklemek/çıkarmak için push ve pop, araya girmek için insert(i, x) ve remove(i) vardır. Bunlar STL'deki push_back, pop_back, insert, erase ile birebir eşleşir. Kritik fark erişimdedir.
panic! — C++'taki at() gibi davranır ama operatörle.Option<&T> döner: geçerliyse Some(&x), değilse None. Panik yok.&[T]). C++'taki std::span'e benzer.fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(10);
v.push(20);
v.insert(1, 15); // [10, 15, 20]
let last = v.pop(); // Some(20); Vec'i daraltır
// İndeksleme panik riski taşır; get() güvenlidir.
let a = v[0]; // i32, panik olabilir
let b = v.get(99); // None — panik yok
// Slice: kopyasız, sahiplenmeyen görünüm.
let dilim: &[i32] = &v[0..2];
println!("{:?} {:?} {:?}", last, b, dilim);
}
Kapasite, STL'deki gibi ayrı yönetilir. Dolu bir Vec'e push yapmak kapasiteyi (genellikle iki katına) büyütür, yeni bir tampon ayırır ve elemanları taşır — amortize O(1). Eleman sayısını biliyorsanız Vec::with_capacity(n) ile tek seferde ayırın; bu reserve'in yapıcıdaki hâlidir.
let mut v = Vec::with_capacity(1000);
// len = 0, capacity = 1000: 1000 push'a kadar yeniden tahsis yok.
for i in 0..1000 {
v.push(i); // hiçbir realloc tetiklenmez
}
assert_eq!(v.len(), 1000);
assert!(v.capacity() >= 1000);
v[i] ile v.get(i) arasındaki seçim bir API sözleşmesidir. İndeksin tanım gereği geçerli olduğu yerde v[i] kullanın (bir bug varsa hızlı patlar). Dışarıdan gelen, güvenilmez indekslerde her zaman get tercih edin.
Bu bölümde
Vec<T>= bitişik, büyüyebilen heap dizisi;std::vector'ün karşılığı.push/pop/insert/removeSTL eşdeğerleriyle aynı semantik.- İndeksleme panikler,
getOption<&T>döner — güvenli erişim için ikincisi. - Kapasite ayrı yönetilir;
with_capacityile yeniden tahsisi önleyin.
02 String ve &str
Rust'ta metin her zaman geçerli UTF-8'dir: String sahip olan büyüyebilen tampon, &str ise ona (veya bir literale) ödünç verilen görünümdür.
İlişki Vec<T> ile &[T] ilişkisinin tıpkısıdır: String, içte Vec<u8>'dir (sahip + büyüyebilir); &str ise bir byte dilimine UTF-8 garantisi eklenmiş görünümdür. C++'ta bu ayrımın karşılığı kabaca std::string (sahip) ile std::string_view (ödünç) arasındadır. Önemli ayrım: &str bir kodlama sözleşmesi taşır, string_view taşımaz.
En çok şaşırtan nokta: s[0] yoktur. C++'ta std::string bir char (byte) dizisidir ve s[0] ilk byte'ı verir. Rust'ta byte ile karakter aynı şey değildir — bir Unicode skaler değeri (char) 1 ila 4 byte tutabilir. "ö" iki byte'tır. Tek bir s[i] indekslemesi byte mı yoksa karakter mi döndüreceği belirsiz olduğundan dilden çıkarılmıştır.
fn main() {
let mut s = String::from("merhaba");
s.push_str(" dünya"); // büyüyen tampona ekleme
s.push('!'); // tek char (UTF-8 olarak kodlanır)
// HATA: String byte ile indekslenemez.
// let c = s[0]; // derlenmez
// "ş" iki byte tutar: byte sayısı != karakter sayısı
let t = "şö";
assert_eq!(t.len(), 4); // byte uzunluğu
assert_eq!(t.chars().count(), 2); // karakter sayısı
}
Karaktere göre dolaşmak için .chars() (Unicode skalerleri), byte'a göre için .bytes() kullanılır. .len() her zaman byte sayısını verir; bu STL'de std::string::size()'in davranışına denktir ama Rust burada sizi karakter saydığınızı zannetmekten korur.
"şö" → bytes: 0xC5 0x9F 0xC3 0xB6 (4 byte) → chars: 'ş' 'ö' (2 char)
fn selamla(ad: &str) -> String {
// &str alıyoruz: String veya literal, ikisi de geçer (zero-copy).
format!("Merhaba, {ad}!")
}
fn main() {
let sahip = String::from("Emirhan");
// &String → &str otomatik (deref coercion)
println!("{}", selamla(&sahip));
println!("{}", selamla("literal")); // &'static str
}
API kuralı: parametre olarak her zaman &str alın, &String değil. &String alan bir fonksiyon literal kabul edemez; &str alan ise hem String'i hem literali zero-copy alır. C++'taki "const std::string& yerine std::string_view al" tavsiyesinin aynısıdır.
Bu bölümde
Stringsahip/büyüyebilir (içteVec<u8>),&strödünç görünüm —stringvsstring_view.s[0]yoktur çünkü byte ≠ karakter; UTF-8'de char 1–4 byte.- Dolaşma:
.chars()karakter,.bytes()byte;.len()byte sayar. - Fonksiyon parametresi için
&strtercih edin: hem String'i hem literali zero-copy alır.
03 HashMap<K, V>
HashMap<K, V>, std::unordered_map'in karşılığıdır: ortalama O(1) ekleme/erişim, sırasız iterasyon — ama ownership ve entry API'siyle.
Temel işlemler tanıdıktır: insert(k, v) ekler (eski değeri varsa Some(old) döndürür), get(&k) ise Option<&V> verir. Sahiplik kritiktir: insert hem anahtarı hem değeri map'e taşır (move). String bir anahtarsa, insert'ten sonra o String artık sizin değildir.
use std::collections::HashMap;
fn main() {
let mut skor: HashMap<String, i32> = HashMap::new();
skor.insert(String::from("ahmet"), 10);
skor.insert(String::from("zehra"), 20);
match skor.get("ahmet") { // &str ile sorgulanabilir
Some(p) => println!("puan: {p}"),
None => println!("yok"),
}
// Sırasız iterasyon: çalıştırmalar arası sıra değişir.
for (ad, puan) in &skor {
println!("{ad}: {puan}");
}
}
C++'ta "anahtar varsa güncelle, yoksa ekle" deyimi operator[] veya find + insert ile yazılır ve genellikle iki kez arama yapar. Rust'ın entry API'si bunu tek aramada, ergonomik biçimde çözer. or_insert(default) giriş yoksa varsayılanı yerleştirir ve her durumda değere mutable referans döndürür.
use std::collections::HashMap;
fn main() {
let metin = "bir iki bir üç iki bir";
let mut say: HashMap<&str, i32> = HashMap::new();
for kelime in metin.split_whitespace() {
// yoksa 0 ekle, sonra &mut i32 üzerinden artır — tek arama
*say.entry(kelime).or_insert(0) += 1;
}
// {"bir": 3, "iki": 2, "üç": 1}
println!("{say:?}");
}
Rust'ın varsayılan hash fonksiyonu (SipHash 1-3) süreç başına rastgele tohumlanır. Bu, kötü niyetli girdilerin kasıtlı çakışma üreterek O(n) bucket zincirleriyle map'i kilitlemesini (HashDoS) engeller. Bedeli, unordered_map'in tipik FNV/identity hash'inden biraz daha yavaş olmasıdır; saf hız gerektiğinde FxHashMap gibi alternatif hasher'lara geçilebilir.
Bu bölümde
HashMap<K, V>= ortalama O(1), sırasız —unordered_map'in karşılığı.insertanahtarı ve değeri map'e taşır (move); sahiplik el değiştirir.entry(k).or_insert(d)tek aramada "yoksa ekle, mutable ref döndür".- Varsayılan SipHash rastgele tohumlu — HashDoS'a dirençli, biraz daha yavaş.
04 Diğer koleksiyonlar
İhtiyaca göre Vec ve HashMap'in ötesinde dört yardımcı: çift uçlu kuyruk, sıralı ağaç tabanlı map ve iki küme tipi.
VecDeque<T>, halka tampon (ring buffer) üzerine kurulu çift uçlu kuyruktur: her iki uçtan da O(1) push_front/push_back ve pop_front/pop_back sağlar. C++'ta std::deque'nin yerini tutar — kuyruk (queue) veya çift uçlu yapı gerektiğinde Vec'in baştan ekleme O(n) maliyetinden kaçınmak için kullanılır.
BTreeMap<K, V> ve BTreeSet<T>, B-ağacı üzerinde anahtarları sıralı tutar; tıpkı std::map/std::set gibi O(log n) işlemler ve sıralı dolaşım sunar. Anahtarları her zaman sıralı gezmek veya aralık sorgusu (range) yapmak istiyorsanız bunları seçin. HashSet<T> ise HashMap üzerine kurulu, sırasız, ortalama O(1) üyelik kümesidir — std::unordered_set karşılığı.
| Rust | C++ STL | Sıra | Karmaşıklık | Ne zaman |
|---|---|---|---|---|
Vec<T> | vector | ekleme sırası | amortize O(1) sona ekleme | varsayılan liste, sıralı erişim |
VecDeque<T> | deque | ekleme sırası | O(1) iki uç | kuyruk / çift uçlu yapı |
HashMap<K,V> | unordered_map | sırasız | ort. O(1) | hızlı anahtar→değer |
BTreeMap<K,V> | map | anahtara göre sıralı | O(log n) | sıralı gezme, range sorgusu |
HashSet<T> | unordered_set | sırasız | ort. O(1) | hızlı üyelik testi |
BTreeSet<T> | set | sıralı | O(log n) | sıralı, tekil eleman kümesi |
use std::collections::{VecDeque, BTreeMap, HashSet};
fn main() {
let mut kuyruk: VecDeque<i32> = VecDeque::new();
kuyruk.push_back(1);
kuyruk.push_front(0); // O(1) baştan ekleme
assert_eq!(kuyruk.pop_front(), Some(0));
let mut sirali = BTreeMap::new();
sirali.insert(3, "c");
sirali.insert(1, "a");
// dolaşım her zaman anahtar sırasında: 1, 3
for (k, v) in &sirali { println!("{k}:{v}"); }
let benzersiz: HashSet<i32> = [1, 2, 2, 3].into_iter().collect();
assert_eq!(benzersiz.len(), 3); // tekrarlar elendi
}
Bu bölümde
VecDeque= halka tamponlu çift uçlu kuyruk; iki uçtan O(1),std::dequekarşılığı.BTreeMap/BTreeSet= sıralı, O(log n), range sorgusu —std::map/set.HashSet= sırasız, ortalama O(1) üyelik kümesi.- Seçim, sıralı erişim ile salt hız arasındaki gerçek ihtiyaca bağlıdır (tabloya bakın).
05 Iterator trait
Rust'ta tüm dolaşım tek bir soyutlamaya dayanır: Iterator trait'i ve onun tek zorunlu metodu next.
Trait'in özü minimaldir: bir ilişkili tip (Item) ve next metodu. Her next çağrısı ya bir sonraki elemanı (Some(item)) ya da bitişi (None) döndürür. C++ STL'deki "begin/end işaretçi çifti + operator++ + operator*" üçlüsünün yerine tek, daha güvenli bir arayüz geçer — sınır kontrolü Option'a kodlanmıştır.
// Standart kütüphanedeki tanımın özü:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... map, filter, collect gibi onlarca metod varsayılan olarak
// sadece next() üstüne tanımlıdır.
}
Bir koleksiyondan iterator elde etmenin üç yolu vardır ve farkları doğrudan ownership'tir. Bunları seçmek C++'taki const_iterator / iterator / move-iterator seçimine benzer ama derleyici zorlar.
&T verir — paylaşımlı, salt-okunur. Koleksiyon dokunulmadan kalır.&mut T verir — yerinde değiştirilebilir. Elemanlar güncellenir.T verir — koleksiyonu tüketir (move), elemanların sahipliğini devreder.fn main() {
let mut v = vec![1, 2, 3];
for x in v.iter() { // x: &i32 (salt-okunur) }
for x in v.iter_mut() { *x *= 2; } // x: &mut i32, yerinde değiştir
for x in v.into_iter() { // x: i32 (sahiplenir); v artık kullanılamaz }
}
for döngüsünün kendisi sözdizimsel şekerdir (syntactic sugar). for x in koleksiyon { ... } aslında into_iter() çağırır ve bir loop içinde next()'i None gelene dek döndürür. &v üzerinde dolaşmak v.iter()'a, &mut v ise v.iter_mut()'a çözülür.
// Bu döngü...
for x in &v { println!("{x}"); }
// ...derleyici tarafından kabaca şuna açılır:
let mut it = v.iter();
while let Some(x) = it.next() {
println!("{x}");
}
into_iter() koleksiyonu tüketir. Çağırdıktan sonra orijinal değişkene erişmek borrow/move hatasıdır. Koleksiyonu sonradan kullanacaksanız iter() veya iter_mut() ile referans üzerinden dolaşın.
Bu bölümde
Iterator=type Item+fn next(&mut self) -> Option<Item>; gerisi varsayılan metod.iter()→&T,iter_mut()→&mut T,into_iter()→T(tüketir).fordöngüsüinto_iter()+next()üstüne kurulu şekerdir.- Bitiş, ayrı bir "end" işaretçisiyle değil
Noneile kodlanır.
06 Lazy adapter'lar
Adapter'lar bir iterator'ı alıp yeni bir iterator döndürür; hepsi tembeldir (lazy) — bir tüketici çağrılana dek tek bir eleman bile işlenmez.
En sık kullanılanlar tanıdık gelecektir: map (dönüştür), filter (ele), enumerate (indeks ekle), zip (iki iterator'ı eşle), take/skip (ilk n'i al/atla), take_while (koşul bozulana dek al), rev (tersine çevir), flat_map (her elemandan iterator üret ve düzleştir). Her biri Vec üretmez; yalnızca bir sonraki next()'in ne yapacağını tarif eder.
fn main() {
// Bu satır HİÇBİR ŞEY hesaplamaz; sadece tarifi kurar.
let tarif = (1..=10)
.filter(|x| x % 2 == 0) // çiftler
.map(|x| x * x); // kareleri
// collect() — tüketici — çağrıldığında zincir CANLANIR:
let sonuc: Vec<i32> = tarif.collect();
assert_eq!(sonuc, vec![4, 16, 36, 64, 100]);
}
Tembellik, gereksiz işten kaçınmayı bedavaya getirir. take(3) ile sınırlanan bir zincir, kaynaktan yalnızca üç eleman çeker — sonsuz bir iterator bile güvenle kullanılabilir, çünkü hiçbir ara Vec tahsis edilmez.
fn main() {
let isimler = ["ada", "can", "deniz"];
let yaslar = [7, 12, 9];
// enumerate + zip + take birlikte, hepsi lazy:
for (i, (ad, yas)) in isimler.iter()
.zip(yaslar.iter()) // (&ad, &yas) çiftleri
.enumerate() // (indeks, çift)
.take(2) // yalnızca ilk 2 — gerisi çekilmez
{
println!("{i}: {ad} ({yas})");
}
// Sonsuz kaynak + take: yalnızca 3 eleman üretilir.
let ilk_uc: Vec<u64> = (0..).map(|x| x * 10).take(3).collect();
assert_eq!(ilk_uc, vec![0, 10, 20]);
}
Bu, C++20'nin std::ranges ve views (örn. views::filter, views::transform) yaklaşımının doğrudan karşılığıdır — view'lar da tembeldir ve | ile zincirlenir. Rust'ta zincirleme nokta (.) ile yapılır; semantik aynıdır: bir tüketici gelene dek hiçbir şey çalışmaz.
Bu bölümde
- Adapter'lar (
map,filter,enumerate,zip,take,skip,rev,flat_map) iterator → iterator. - Hepsi lazy: bir tüketici çağrılana dek tek eleman bile işlenmez.
- Tembellik gereksiz işi eler; sonsuz iterator'lar
takeile güvenli kullanılır. - C++20
ranges/views'ın birebir muadili; araVectahsisi yok.
07 Tüketiciler (consumers)
Tüketiciler tembel zinciri çalıştıran metodlardır; next()'i sonuna dek tüketip somut bir değer üretirler — <algorithm>'in yerini tutarlar.
En esnek tüketici collect'tir: iterator'ı bir koleksiyona toplar. Hedef tipi ya turbofish ile (.collect::<Vec<_>>()) ya da değişken tip açıklamasıyla belirtilir; Rust geri kalanı çıkarır. Aynı collect hedefe göre Vec, String, HashMap hatta Result üretebilir.
| Tüketici | Döner | C++ <algorithm> muadili |
|---|---|---|
sum() / product() | toplam / çarpım | accumulate |
fold(init, f) | genel katlama | accumulate (binary op) |
count() | eleman sayısı | distance |
any() / all() | bool | any_of / all_of |
find(p) | Option<T> | find_if |
position(p) | Option<usize> | find_if + distance |
max() / min() | Option<T> | max_element / min_element |
fn main() {
let v = vec![1, 2, 3, 4, 5];
let toplam: i32 = v.iter().sum(); // 15
let carpim: i32 = v.iter().product(); // 120
let cift_var = v.iter().any(|&x| x % 2 == 0); // true
let ilk_buyuk = v.iter().find(|&&x| x > 3); // Some(&4)
let en_buyuk = v.iter().max(); // Some(&5)
// fold: genel katlama (init + her elemanı işle)
let kareler_toplami = v.iter().fold(0, |acc, &x| acc + x * x);
assert_eq!(kareler_toplami, 55);
}
collect'in gücü hedef tipindedir. Aynı zincir String'e (karakterleri birleştir), HashMap'e (çiftleri topla) ya da Result<Vec<_>, E>'ye (ilk hatada dur) toplanabilir.
use std::collections::HashMap;
fn main() {
// → String
let s: String = ['a', 'b', 'c'].into_iter().collect();
// → HashMap (turbofish ile)
let m = [(1, "bir"), (2, "iki")]
.into_iter()
.collect::<HashMap<_, _>>();
// → Result<Vec, _>: tüm parse'lar başarılıysa Ok(Vec), ilk hatada Err
let r: Result<Vec<i32>, _> =
["1", "2", "3"].iter().map(|s| s.parse::<i32>()).collect();
assert_eq!(s, "abc");
assert_eq!(m.len(), 2);
assert_eq!(r, Ok(vec![1, 2, 3]));
}
collect::<Result<Vec<_>, E>>() deseni, "hepsi başarılı olmalı, yoksa ilk hata" mantığını tek satıra sığdırır. <algorithm>'de bunun temiz bir karşılığı yoktur; elle döngü + erken çıkış yazardınız.
Bu bölümde
- Tüketiciler tembel zinciri çalıştırır;
<algorithm>'in iterator karşılığı. collectturbofish veya tip açıklamasıyla hedefe göreVec/String/HashMapüretir.sum,fold,count,any/all,find,position,max/mintek geçişte sonuç verir.collect::<Result<Vec<_>, _>>()ile "hepsi-ya-da-ilk-hata" toplanır.
08 Zero-cost abstraction
Iterator zinciri, soyut görünmesine rağmen elle yazılmış bir for döngüsüyle aynı makine koduna derlenir — soyutlama bedavadır.
"Zero-cost abstraction" Rust'ın (ve C++'ın) çekirdek vaadidir: kullanmadığınız hiçbir şeyin bedelini ödemezsiniz, kullandığınız soyutlama ise elle yazacağınızdan daha pahalı değildir. Iterator zincirleri bunun en saf örneğidir. map/filter/sum birer fonksiyon çağrısı gibi görünür; ama LLVM optimizasyonundan sonra geriye yalın bir döngü kalır.
// Üst seviye soyutlama: niyet açık, kod kısa.
fn toplam_iter(v: &[i32]) -> i32 {
v.iter().filter(|&&x| x > 0).map(|&x| x * 2).sum()
}
// Elle yazılmış eşdeğer.
fn toplam_dongu(v: &[i32]) -> i32 {
let mut acc = 0;
for &x in v {
if x > 0 { acc += x * 2; }
}
acc
}
// --release ile: ikisi de bit bit AYNI assembly. Kapanışlar inline
// olur, ara nesneler kaybolur. Performans farkı ölçülemez.
Bunu mümkün kılan üç mekanizma var. Birincisi, adapter'lar somut tipler döndürür (örn. Map<Filter<...>>) — sanal çağrı (vtable) yok, hepsi statik dağıtım (static dispatch). İkincisi, kapanışlar (closures) çağrı yerine inline edilir. Üçüncüsü, bilinen aralıklarda derleyici bounds-check elemesi yapar: iterator garantili biçimde sınır içinde kaldığından çalışma zamanı sınır kontrolleri tamamen kaldırılır.
kaynak iter → filter (inline closure) → map (inline closure) → sum (akümülatör) ⇒ tek döngü, sıfır ara nesne
Bounds-check elemesi kavramsal olarak şudur: elle yazdığınız C döngüsünde v[i] erişimi sınır kontrolü içermez (sizin sorumluluğunuzdadır). Rust'ta her indeksleme normalde kontrol içerir, ama iterator zincirinde derleyici indeksin tanım gereği geçerli olduğunu kanıtlayabildiği için kontrolü siler. Sonuç: güvenlik garantisini koruyan ama C döngüsüyle aynı talimatları üreten bir döngü.
Pratik sonuç: idiyomatik, okunaklı iterator zincirlerini gönül rahatlığıyla yazın. "Daha hızlı olur" diye elle döngüye dönmek neredeyse her zaman gereksizdir — ürettikleri kod aynıdır, ama iterator versiyonunda off-by-one ve sınır taşması hataları yapısal olarak imkânsızdır.
Bu bölümde
- Iterator zinciri
--releaseile elle döngüyle aynı assembly'ye derlenir. - Adapter'lar static dispatch + inline closure: vtable veya ara nesne yok.
- Bounds-check elemesi sayesinde çalışma zamanı sınır kontrolleri kaybolur.
- C döngüsüyle performans denkliği + yapısal olarak imkânsız off-by-one hatası.
09 Gerçek örnek: veri işleme pipeline
Öğrendiğimiz her şeyi tek bir akışta birleştirelim: ham metni satırlara böl, parse et, filtrele, dönüştür ve özetle — hepsi tek iterator zinciriyle.
Senaryo: bir sıcaklık log'u var; her satır "sensör,sıcaklık" biçiminde. Geçersiz satırları (yanlış biçim, parse edilemeyen sayı) atlamak, eşiği aşan ölçümleri saymak ve ortalamayı hesaplamak istiyoruz. C++'ta bu std::getline döngüsü + std::stoi + try/catch karışımı olurdu; Rust'ta tek bir tembel zincir.
fn main() {
let log = "\
oda1,21.5
oda2,bozuk
oda1,30.0
oda3,18.2
eksik_satir
oda2,42.7";
// Satır → parse → geçersizleri ele → sıcaklıkları topla
let sicakliklar: Vec<f64> = log
.lines() // satırlara böl
.filter_map(|satir| { // parse + ayıkla
let (_ad, deger) = satir.split_once(',')?; // virgül yoksa None
deger.trim().parse::<f64>().ok() // sayı değilse None
})
.collect();
let sayi = sicakliklar.len();
let toplam: f64 = sicakliklar.iter().sum();
let ortalama = toplam / sayi as f64;
let sicak = sicakliklar.iter().filter(|&&t| t > 25.0).count();
println!("geçerli: {sayi}, ortalama: {ortalama:.1}, >25°: {sicak}");
// geçerli: 4, ortalama: 28.1, >25°: 2
}
Burada filter_map, "dönüştür ve aynı anda None olanları at" işini tek adımda yapar — ? operatörü ve .ok() ile hatalı satırlar sessizce elenir. Şimdi tersini isteyelim: bir satır bile geçersizse tüm işlemi iptal et. İşte collect::<Result<Vec<_>, _>>() deseninin parladığı yer.
fn kati_parse(csv: &str) -> Result<Vec<f64>, std::num::ParseFloatError> {
csv.lines()
.map(|satir| {
let deger = satir.split_once(',').map(|(_, v)| v).unwrap_or(satir);
deger.trim().parse::<f64>() // Result<f64, _>
})
// Vec<Result> DEĞİL: ilk Err'de durur, Result<Vec> döner.
.collect()
}
fn main() {
match kati_parse("21.5\n30.0\n18.2") {
Ok(v) => println!("tümü geçerli: {v:?}"),
Err(e) => eprintln!("hata: {e}"),
}
}
İki desen iki farklı hata politikasını temsil eder. filter_map(... .ok()) "hatalıyı atla, devam et" (best-effort) der; collect::<Result<Vec<_>, _>>() ise "hepsi geçerli olmalı, ilk hatada dur" (fail-fast) der. İhtiyaca göre birini seçmek, tüm döngü mantığını değil tek bir metodu değiştirmek demektir.
Kapanış olarak: bu rehberde C'nin manuel bellek emeğinden başlayıp Rust'ın güvenli koleksiyonlarına, oradan tembel iterator zincirlerine geçtik. Sonuç, C++ STL kadar ifade gücü yüksek ama derleyicinin ownership ile zorladığı bir model — ve hepsi zero-cost. İdiyomatik Rust yazmanın anlamı budur: kısa, okunaklı zincirler kurun; güvenlik ve performans derleyicinin işi olsun.
Bu bölümde
- Tek bir tembel zincir:
lines→filter_map(parse + ayıkla) →collect→ özetle. filter_map+?/.ok()= "hatalıyı atla, devam et" (best-effort).collect::<Result<Vec<_>, _>>()= "ilk hatada dur" (fail-fast).- İfade gücü STL düzeyinde, güvenlik derleyicide, maliyet sıfır — idiyomatik Rust.