00 Zihniyet değişimi
Rust'ı C++ gibi yazmaya çalışırsan derleyici sürekli yolunu keser; doğru hamle borrow checker'ı bir düşman değil, bir tasarım ortağı olarak görmektir.
C/C++ dünyasından gelen herkesin ilk haftalarda yaşadığı duygu aynıdır: derleyiciyle güreşmek. Kafandaki algoritma nettir, C'de beş dakikada yazardın, ama Rust derleyicisi borrow of moved value, cannot borrow as mutable, does not live long enough diye art arda reddediyor. Bu noktada iki yol var: ya hataları susturmak için rastgele .clone(), Rc<RefCell> ve unsafe serpiştirirsin, ya da derleyicinin sana ne söylemeye çalıştığını dinlersin. İkinci yol acı verir ama tek doğru yoldur.
Zihniyet farkını tek cümleyle özetlemek gerekirse: C++'ta kod yazarken belleği düşünürsün; Rust'ta tasarım yaparken sahipliği düşünürsün. C++'ta bir nesneyi kim siler, kim sahiplenir, kim sadece bakar — bunları kafanda ya da yorum satırlarında tutarsın ve hata yaptığında runtime'da öğrenirsin. Rust'ta aynı soruların cevabını tip imzasına yazarsın: fn f(x: T) sahipliği alır, fn f(x: &T) ödünç bakar, fn f(x: &mut T) ödünç değiştirir. İmza bir sözleşmedir ve derleyici onu zorlar.
C++ refleksi Rust yaklaşımı ───────────── ───────────── kodu yaz → derle → sahipliği tasarla → crash'i debug et derleyici doğrular → çalışır
Borrow checker'ı bir linter ya da bir hata bulucu sanmak en yaygın yanılgıdır. O bir doğrulayıcıdır: sen veri akışını ve ömürleri doğru tasarladıysan onaylar, tasarlamadıysan reddeder. Yani borrow checker hatası genelde "şu satırı düzelt" değil, "bu verinin sahibi kim olmalı, ömrü neye bağlı olmalı" sorusunu yeniden sormanı söyler. Deneyimli Rust geliştiricileri bu yüzden borrow checker'la neredeyse hiç güreşmez — çünkü kodu yazmadan önce sahipliği kafalarında çözmüşlerdir.
İlk iki-üç hafta direnç hissetmek tamamen normaldir; bu C++ reflekslerinin Rust modeline çarpmasıdır, beceriksizlik değil. Bu rehberdeki tuzakların çoğu "C'de doğru olan alışkanlığın Rust'ta neden yanlış reflekse dönüştüğünü" anlatır. İlgili ownership rehberi bu modelin teorik temelini verir; burada odak geçiş psikolojisi ve pratik eşlemedir.
İlk haftalarda her derleyici hatasını .clone() veya .unwrap() ile susturma refleksine düşme. Bu kısa vadede ilerletir ama Rust'ı öğrenmez, sadece "çirkin C++" yazmış olursun. Hatayı sustur değil, anla.
Bu bölümde
- Geçişin özü "derleyiciyle güreşmek"ten "derleyiciyle tasarlamak"a kaymaktır.
- C++'ta belleği yazarken, Rust'ta sahipliği tasarlarken düşünürsün; cevaplar tip imzasına yazılır.
- Borrow checker bir hata bulucu değil, tasarım kararlarını doğrulayan bir araçtır.
- İlk haftalardaki direnç normaldir; hatayı susturmak değil anlamak öğretir.
01 Büyük kavram eşleme tablosu
Çoğu C/C++ kavramının Rust'ta doğrudan bir karşılığı vardır; ama eşleme birebir değil — bazı kavramlar tek bir Rust özelliğine, bazıları ise dile içkin bir varsayılana karşılık gelir.
Aşağıdaki tablo bu rehberin omurgasıdır. Bir kavramı ararken buraya dön; her satır sonraki bölümlerde ya da ilgili rehberlerde derinleştirilir. Önemli olan nüansı görmek: bazı C++ kavramları (örn. const, kopya semantiği) Rust'ta artık varsayılan davranış olduğu için "özel bir özellik" olmaktan çıkmıştır.
| C / C++ | Rust | Not |
|---|---|---|
T* (sahipsiz pointer) | &T / &mut T | Referans; ömrü derleyici tarafından izlenir. |
new T / unique_ptr<T> | Box<T> | Tekil sahipli heap tahsisi. İlgili smart pointer rehberi. |
malloc / free | ownership + Drop | Serbest bırakma kapsam sonunda otomatik. |
delete | (otomatik) / drop(x) | Elle free neredeyse hiç gerekmez. |
std::vector<T> | Vec<T> | Büyüyen dizi; sahiplik Vec'te. |
T* raw (bilinçli ham) | *const T / *mut T | Sadece unsafe içinde dereference edilir. |
class | struct + impl | Veri ve davranış ayrı bloklarda. |
virtual / saf sanal sınıf | trait + dyn Trait | Dinamik dağıtım açık biçimde belirtilir. |
template<typename T> | generic <T> + trait bound | Monomorfizasyon; kısıtlar imzada. İlgili trait rehberi. |
#define SABIT 42 | const SABIT: i32 = 42; | Tipli, kapsamlı; ön-işlemci yok. |
#define makro hilesi | macro_rules! / proc-macro | Hijyenik, AST düzeyinde makrolar. |
header (.h) / #include | mod + use | Header/impl ayrımı yok; modül sistemi. |
namespace | mod | İsim alanı ve modül aynı mekanizma. |
throw / try/catch | Result<T, E> + ? | Hata bir değer; tip imzasında görünür. |
errno / dönüş kodu | Result<T, E> | Yan kanal yok; hata dönüş değerinde. |
enum (C: sade tamsayı) | enum (data taşıyabilir) | Rust enum'ı = etiketli birleşim (tagged union). |
union | enum (güvenli) / union (unsafe) | Genelde istediğin şey enum'dur. |
NULL / nullptr | Option<T> (None) | "Yokluk" tip seviyesinde; null dereference imkânsız. |
const T | (varsayılan immutable) | Değişkenler varsayılan değişmez; mut ile aç. |
std::move(x) | (varsayılan move) | Atama/geçiş zaten move; std::move gibi bir araç gerekmez. |
İki satır özellikle dikkat ister. NULL → Option: Rust'ta "bir değer olmayabilir" durumu tipte kodlanır, dolayısıyla null pointer dereference diye bir hata kategorisi yoktur. const → varsayılan immutable: C++'ta değişkenleri korumak için const eklersin; Rust'ta tam tersi — her şey değişmezdir, değiştirmek istediğinde mut eklersin. Bu tek kelimelik tersine çevirme, kodun büyük kısmını otomatik olarak daha güvenli yapar.
Rust enum'ı C enum'undan çok daha güçlüdür: her varyant veri taşıyabilir, bu da onu C++'taki std::variant ya da elle yazılmış tagged union'ın yerine koyar. Option ve Result de aslında birer enum'dur. İlgili enum & pattern matching rehberi bunu derinleştirir.
Bu bölümde
- Çoğu C/C++ kavramının net bir Rust karşılığı var; ama eşleme birebir değil, nüanslı.
- Pointer'lar referans/
Box/ham pointer'a, sınıflar struct+impl'e, virtual'lar trait+dyn'e eşlenir. constve move artık özellik değil, dilin varsayılanı — refleksi tersine çevir.NULL → Optionveenum'ın veri taşıması bütün bir hata kategorisini ortadan kaldırır.
02 Bellek modeli farkı
C++'ta atama varsayılan olarak kopyalar (kopya ctor), Rust'ta atama varsayılan olarak taşır (move) — bu tek fark birçok refleksini tersine çevirir.
C++'ta bir nesneyi başka bir değişkene atadığında, aksini belirtmedikçe kopya yapıcı (copy constructor) çağrılır. Büyük bir vektörü farkında olmadan kopyalamak, C++'ta klasik performans tuzağıdır; bu yüzden std::move ve referansları öğrenirsin. Rust'ta varsayılan tersidir: atama taşır (move), kopyalamaz. Taşınan değer artık kullanılamaz — eski sahip "boşaltılır".
// C++: atama varsayılan olarak KOPYALAR
std::vector<int> a = {1, 2, 3};
std::vector<int> b = a; // derin kopya — a hâlâ geçerli, sessiz maliyet
a.push_back(4); // b etkilenmez
// Rust: atama varsayılan olarak TAŞIR (move)
let a = vec![1, 2, 3];
let b = a; // sahiplik b'ye taşındı — kopya YOK
// a.push(4); // HATA: borrow of moved value: `a`
let c = b.clone(); // kopya istiyorsan AÇIKÇA söyle
Buradaki kazanım şudur: Rust'ta hiçbir derin kopya sessizce oluşmaz. Kopyalamak istiyorsan .clone() yazarsın ve maliyet kodda görünür hale gelir. i32, bool, char gibi küçük, sabit boyutlu tipler Copy trait'ini uygular ve gerçekten kopyalanır — ama bunlar bit kopyası olduğu için ucuzdur. Heap tutan her şey (String, Vec, Box) move semantiğindedir.
| C / C++ | Rust | Not |
|---|---|---|
| kopya ctor (varsayılan) | move (varsayılan) | Atama eski değeri geçersiz kılar. |
std::move(x) | (zaten move) | Ekstra araca gerek yok. |
kopya = auto b = a; | let b = a.clone(); | Kopya artık açık ve aranabilir. |
| RAII (destructor, elle yaz) | Drop (otomatik üretilir) | Alanlar ters sırada otomatik drop edilir. |
| moved-from nesne (geçerli ama belirsiz) | moved-from = kullanılamaz | Derleyici erişimi engeller. |
RAII tarafında Rust C++'tan daha katıdır ve daha rahattır. C++'ta destructor'ı elle yazarsın ve "rule of three/five" gibi kuralları akılda tutarsın. Rust'ta Drop derleyici tarafından üretilir: bir değer kapsamdan çıktığında alanları otomatik olarak, tanımlanma sırasının tersinde drop edilir. Özel temizlik gerekirse impl Drop yazarsın — ama Drop uygulayan bir tip artık Copy olamaz, çünkü "iki kopya, iki temizlik" çelişkisi olurdu.
C/C++'ta sıkça yazdığın kendi kendine referans veren yapılar (bir struct'ın bir alanı aynı struct'ın başka alanına pointer tutar) Rust'ta doğrudan ifade edilemez. Çünkü değer taşındığında adres değişir ama içerideki pointer eskisini gösterir — bu tam olarak Rust'ın yasakladığı dangling durumudur. Çözüm: index kullan, Rc/arena tahsisi yap ya da gerçekten gerekiyorsa Pin ve unsafe'e in. Çoğu durumda tasarımı yeniden düşünmek doğru cevaptır.
Bu bölümde
- C++ varsayılanı kopya, Rust varsayılanı move — sessiz derin kopya Rust'ta oluşmaz.
- Kopya istiyorsan
.clone()ile açıkça belirtirsin; maliyet kodda görünür. Dropderleyici tarafından garanti edilir; destructor'ı elle yazmazsın.- Kendine referans veren yapılar move modeliyle çeliştiği için zordur; tasarımı yeniden düşün.
03 Sık refleks tuzakları — bellek/erişim
C'deki en sağlam alışkanlıkların bir kısmı Rust'ta ya gereksizdir ya da doğrudan borrow checker'a çarpar; bunları erken fark etmek aylarca zaman kazandırır.
İlk tuzak: index'li manuel döngü. C'de for (int i = 0; i < n; i++) refleksi kasına işlemiştir. Rust'ta bu hem deyimsiz (idiomatic değil) hem de bound-check maliyetli kalır. Iterator kullan — derleyici sınır kontrolünü çoğunlukla eler ve kod daha okunur olur.
// C++ refleksi: index'li manuel döngü
std::vector<int> v = {1, 2, 3};
int sum = 0;
for (size_t i = 0; i < v.size(); i++)
sum += v[i]; // her erişimde manuel index
// Rust deyimi: iterator
let v = vec![1, 2, 3];
let sum: i32 = v.iter().sum(); // niyet net, sınır kontrolü elenir
// ya da: for x in &v { ... }
İkinci tuzak: paylaşımlı değiştirilebilir erişim. C'de iki pointer aynı tampona yazabilir; bu da aliasing bug'larının kaynağıdır. Rust'ın kuralı kemik gibi sağlam: aynı anda ya çok sayıda &T (salt-okunur) ya da tek bir &mut T (değiştirilebilir) olabilir — ikisi bir arada olamaz. Bu yüzden C'de doğal gelen "elimde iterator varken aynı koleksiyonu değiştireyim" hamlesi Rust'ta derlenmez (ve iyi ki derlenmez — C++'ta iterator invalidation tam olarak budur).
Üçüncü tuzak: getter/setter refleksi. C++'ta her alana get_x()/set_x() yazmak yaygındır. Rust'ta alanları doğrudan açmak (pub) ya da değişmez yapı + builder kullanmak çoğu zaman daha deyimseldir; getter/setter sadece gerçekten invariant koruman gerektiğinde yazılır. Encapsulation modül seviyesinde de sağlanır.
Dördüncü tuzak: "yokluk için ham pointer arama". C'de "bulunamadı"yı NULL döndürerek anlatırsın. Rust'ta bu Option<&T>'dir; null kontrolünü unutamazsın çünkü Option'ı açmadan içindeki değere erişemezsin.
| C / C++ refleksi | Rust deyimi | Not |
|---|---|---|
for (i=0; i<n; i++) v[i] | v.iter() / for x in &v | Sınır kontrolü elenir, niyet açık. |
| iki pointer aynı tampona yazar | &mut tekliği | Aliasing derleme zamanında yasak. |
| döngüde koleksiyonu değiştir | topla/filtrele, sonra uygula | Iterator invalidation imkânsız. |
get_x()/set_x() her alana | pub alan / builder | Getter sadece invariant için. |
"bulunamadı" → NULL | Option<&T> | Kontrol atlanamaz. |
"Döngüde koleksiyonu değiştiremiyorum" engeline takılırsan deyim şudur: önce iter().filter().collect() ile değişecekleri topla, döngü bitince uygula. Borrow checker burada seni iterator invalidation'dan koruyor — direnme, kalıbı değiştir. İlgili koleksiyonlar & iterator rehberi bu kalıpları detaylandırır.
Bu bölümde
- Index'li manuel döngü yerine iterator kullan; daha hızlı ve daha okunur.
- Paylaşımlı değiştirilebilir erişim yasaktır: ya çok
&Tya tek&mut T. - Otomatik getter/setter refleksini bırak;
pubalan ya da builder çoğu zaman daha deyimsel. - "Yokluk için NULL" yerine
Option; null kontrolü atlanamaz hale gelir.
04 Araç ve build eşlemesi
Rust'ın en büyük günlük konforu entegre araç zinciridir: build, bağımlılık, test, formatlama ve lint tek bir komutun (cargo) altında toplanır.
C/C++'ta build sistemi seçmek başlı başına bir projedir: Makefile mi, CMake mi, Meson mi; bağımlılıkları nasıl çekeceğin (vcpkg, Conan, submodule) ayrı bir tartışma. Rust'ta bunların hepsi cargo ve Cargo.toml ile gelir. Bağımlılık eklemek tek satırdır, build/test/çalıştırma tek komuttur. Bu, geçişte hissedeceğin ilk somut rahatlamadır.
| C / C++ | Rust | Not |
|---|---|---|
make / cmake / Meson | cargo build | Build + bağımlılık + test tek araçta. |
| vcpkg / Conan / submodule | Cargo.toml + crates.io | Bağımlılık tek satır. İlgili cargo rehberi. |
gcc / clang | rustc (cargo çağırır) | rustc'yi elle çağırman nadir. |
gdb / lldb | aynısı + rust-gdb/rust-lldb | Sarmalayıcılar tipleri güzel basar. |
| Valgrind / ASan / UBSan | Miri + sanitizer'lar | Miri, unsafe'te UB'yi yakalar. |
clang-tidy / cppcheck | cargo clippy | 500+ lint; deyimsel kodu öğretir. |
clang-format | cargo fmt (rustfmt) | Tek standart stil; tartışma biter. |
| ayrı test framework (gtest) | cargo test (dahili) | #[test] ile, harici bağımlılık yok. |
| Doxygen | cargo doc (rustdoc) | Doc yorumları test bile edilir. |
Günlük döngüde en çok dokunacağın iki araç: cargo clippy ve cargo fmt. Clippy sadece bug değil, deyim de öğretir — "burada iter().sum() kullan", "bu match aslında if let olabilir" gibi öneriler verir. Geçiş döneminde clippy'yi sürekli çalıştırmak, C++ reflekslerini Rust deyimlerine çevirmenin en hızlı yoludur.
cargo fmt → cargo clippy → cargo test → cargo build --release
Hata ayıklarken rust-gdb ya da rust-lldb kullan: bunlar standart gdb/lldb'nin üstüne Rust tip yazıcıları (pretty-printer) ekler, böylece Vec ve String ham bellek yerine okunur biçimde görünür. unsafe kod yazıyorsan, çalışma zamanında UB'yi yakalamak için cargo +nightly miri test paha biçilmezdir — Valgrind'in soyut yorumlayıcı muadili.
Bu bölümde
cargobuild, bağımlılık, test, doc ve çalıştırmayı tek araçta birleştirir.- Debugger aynı (gdb/lldb) ama
rust-gdb/rust-lldbtipleri okunur basar. - Valgrind/ASan karşılığı Miri + sanitizer'lar; clang-tidy → clippy, clang-format → rustfmt.
- Clippy geçiş döneminde deyim öğreten en değerli araçtır; sürekli çalıştır.
05 Anti-pattern'ler ve deyimler
"C'de böyle yapardım" cümlesi geçiş sürecinin en tehlikeli cümlesidir; her C kalıbının Rust'ta daha temiz, daha güvenli bir deyimsel karşılığı vardır.
İlk anti-pattern: çıkış parametresi (output parameter). C'de bir fonksiyon hem başarı durumunu hem sonucu döndüremez, bu yüzden sonucu pointer ile dışarı yazarsın. Rust'ta birden çok değer döndürmek serbesttir — tuple ya da daha iyisi Result kullan.
// C: çıkış parametresi + hata kodu
int parse(const char *s, int *out) {
if (!valid(s)) return -1; // hata kodu
*out = atoi(s); // sonuç dışarı yazılır
return 0;
}
// Rust: sonuç ve hata tek dönüş değerinde
fn parse(s: &str) -> Result<i32, ParseError> {
if !valid(s) {
return Err(ParseError::Invalid);
}
Ok(s.parse()?) // başarı da hata da tipte görünür
}
İkinci anti-pattern: OOP kalıtım hiyerarşisi. C++'ta davranış paylaşmak için temel sınıftan türetirsin. Rust'ta kalıtım yoktur; davranışı trait ile soyutlar, kodu kompozisyonla paylaşırsın. Bu başta kısıtlayıcı gelir ama "elmas kalıtım", "kırılgan temel sınıf" gibi C++ dertlerini tamamen ortadan kaldırır.
C++ kalıtım Rust kompozisyon
───────── ──────────────
Base → Derived trait Davranış + struct { alanlar }
(is-a, sıkı bağ) (has-a / does, gevşek bağ)Üçüncü anti-pattern: paylaşımlı değiştirilebilir global. C'de global bir değişkeni her yerden okuyup yazmak yaygındır. Rust'ta değiştirilebilir global (static mut) unsafe'tir ve istenmez. Deyim: durumu parametre olarak geçir, ya da gerçekten global lazımsa OnceLock / LazyLock ile bir kez ilklenen, paylaşımlı (mümkünse immutable) bir değer kullan.
| "C'de böyle yapardım" | Rust deyimi | Not |
|---|---|---|
çıkış parametresi (int *out) | tuple / Result dönüşü | Birden çok değer dönmek serbest. |
| hata kodu döndür | Result<T, E> | Hata kullanılmadan göz ardı edilemez. |
| OOP kalıtım (Base/Derived) | trait + kompozisyon | Elmas/kırılgan temel sınıf yok. |
| paylaşımlı değiştirilebilir global | OnceLock / parametre geçişi | static mut = unsafe, kaçın. |
void* + cast | generic <T> / enum | Tip güvenli; cast cehennemi yok. |
Dördüncü anti-pattern: void*. C'de tip silmek için void* taşır, sonra cast edersin — derleyici seni hiç korumaz. Rust'ta iki temiz alternatif var: tip parametresi gerçekten serbestse generic kullan; sonlu bir tip kümesi taşıyorsan enum kullan. Gerçekten heterojen koleksiyon lazımsa Box<dyn Trait> ile tip güvenli dinamik dağıtım yaparsın.
Kalıtım refleksiyle her şeyi Box<dyn Trait> yapma. Çoğu zaman ihtiyacın generic + trait bound'dur (statik dağıtım, sıfır maliyet). dyn'ı sadece gerçekten heterojen bir koleksiyon ya da çalışma zamanı polimorfizmi gerektiğinde kullan. İlgili trait & generics rehberi bu seçimi ayrıntılandırır.
Bu bölümde
- Çıkış parametresi ve hata kodu yerine tuple/
Resultdönüşü. - Kalıtım yerine
trait+ kompozisyon; elmas/kırılgan temel sınıf dertleri biter. - Değiştirilebilir global yerine
OnceLockya da durumu parametre geçirme. void*yerine generic (serbest tip) ya daenum(sonlu küme); tip güvenliği korunur.
06 String dünyası
C/C++'ta char* ve std::string karmaşası Rust'ta net bir ikiliye oturur: sahipli String ve ödünç &str — ama UTF-8 ve FFI sınırında birkaç kuzen daha vardır.
C'de string'ler null-terminated char*'tır ve uzunluk, kodlama, sahiplik hep belirsizdir. C++ std::string sahipliği netleştirir ama hâlâ baytlar üzerinde çalışır. Rust string'leri iki temel tipte düşünür ve bu ayrım sahiplikle birebir örtüşür: String sahipli, büyüyebilir, heap'tedir (std::string gibi); &str ise bir string'in ödünç alınmış, değişmez bir dilimidir (bir const char* + uzunluk gibi, ama güvenli).
| C / C++ | Rust | Ne zaman |
|---|---|---|
std::string (sahipli) | String | Sahiplik/değişiklik/büyüme gerektiğinde. |
const char* + len | &str | Salt-okunur bakış; parametre tipi olarak ideal. |
string literal "x" | &'static str | Programla yaşayan sabit dilim. |
C API'ye geçen char* | CString / CStr | Null-terminated, FFI sınırı için. |
| OS yolu / ortam değişkeni | OsString / OsStr | Platforma özgü, UTF-8 garantisiz. |
En kritik kural: fonksiyon parametresi her zaman &str olmalı, String değil. Çünkü &str hem bir String'i hem bir literal'i hem de bir dilimi kabul eder (deref coercion sayesinde), &String ise yalnızca String'i. Sahipliğe gerçekten ihtiyacın yoksa &str al.
// C++: const ref alarak gereksiz kopyadan kaçın
void selam(const std::string& ad) {
std::cout << "Merhaba " << ad;
}
// Rust: &str al — String, literal ve dilim hepsi geçer
fn selam(ad: &str) {
println!("Merhaba {ad}");
}
selam("Ada"); // literal
selam(&String::from("Ada")); // String → &str (otomatik)
İkinci kritik fark: Rust string'leri UTF-8'dir, baytlar değil. Bu yüzden C'deki s[i] ile karakter indekslemek Rust'ta yoktur — çünkü bir "karakter" birden çok bayt olabilir. Bayt gezmek için .bytes(), Unicode skaler değer için .chars() kullanırsın. Bu başta sürpriz olur ama seni gizli kodlama bug'larından korur.
Geçişin en yaygın çirkinleştirici tuzağı: borrow checker'ı susturmak için her yere .to_string() ve .clone() serpmek. Bu kod çalışır ama gereksiz tahsis yapar ve "C++'ı Rust sentaksıyla yazmak"tır. Çözüm genelde sahipliği değil ödünç almayı geçirmektir: String yerine &str al. Eğer gerçekten clone gerekiyorsa sorun yok — ama önce "ödünç alabilir miyim?" diye sor.
Bu bölümde
- Temel ikili: sahipli
Stringvs ödünç&str; FFI/OS içinCString/OsStringkuzenleri. - Fonksiyon parametresi her zaman
&strolmalı — daha esnek ve kopyasız. - String'ler UTF-8'dir;
s[i]indeksleme yok,.chars()/.bytes()kullan. .to_string()/.clone()spam'ı bir tuzaktır; önce ödünç almayı dene.
07 Hata yönetimi zihniyeti
Rust'ta hata bir kontrol akışı kaçışı değil, bir değerdir; Result ve ? operatörü exception'ın okunabilirliğini, dönüş kodunun açıklığıyla birleştirir.
C'de hatalar dönüş koduyla (-1, errno) yayılır — açık ama gürültülü, ve kontrol etmeyi unutmak kolaydır. C++'ta exception'lar kodu temizler ama görünmez kontrol akışı yaratır: bir fonksiyonun fırlatıp fırlatmayacağını imzasından anlayamazsın. Rust üçüncü yolu seçer: hata bir değerdir (Result<T, E>) ve tip imzasında görünür, ama ? operatörü onu exception kadar zahmetsiz yayar.
// C++: görünmez kontrol akışı — imza fırlatabileceğini söylemez
Config yukle(const std::string& yol) {
auto ham = dosya_oku(yol); // throw edebilir
return ayristir(ham); // bu da throw edebilir
}
// Rust: hata imzada görünür, ? ile zahmetsiz yayılır
fn yukle(yol: &str) -> Result<Config, Error> {
let ham = dosya_oku(yol)?; // hata varsa erken döner
let cfg = ayristir(&ham)?; // zincir devam eder
Ok(cfg)
}
En önemli zihinsel düzeltme: panic! bir exception DEĞİLDİR. C++'ta exception'lar normal, beklenen hata akışıdır ve yakalanır. Rust'ta panic! "programcı hatası / kurtarılamaz durum" anlamına gelir; varsayılan olarak thread'i sonlandırır ve genelde yakalamak için yazılmaz. Beklenen hatalar (dosya yok, geçersiz girdi, ağ kesildi) her zaman Result ile ifade edilir. panic!'i bir try/catch mekanizması gibi kullanmak Rust'ta anti-pattern'dir.
| C / C++ | Rust | Not |
|---|---|---|
dönüş kodu / errno | Result<T, E> | Kontrol unutulamaz; tipte zorunlu. |
throw / try-catch | Result + ? | Görünür akış, exception okunabilirliği. |
throw (beklenen hata) | Err(...) | Beklenen hata panic değildir. |
abort() / assertion | panic! / assert! | Kurtarılamaz programcı hatası. |
boş catch(...) | .unwrap() / .expect() | Hatayı yutma; sadece prototip/test. |
Geçişin bir numaralı kötü alışkanlığı erken .unwrap() spam'ıdır: her Result'ı düşünmeden açıp programı potansiyel panic'lerle doldurmak. Prototipte tolere edilebilir, ama üretim kodunda her .unwrap() "burada çökebilirim" demektir. Bunun yerine hatayı ? ile yukarı yay, ya da en azından .expect("anlamlı mesaj") kullan. İlgili hata yönetimi rehberi ?, özel hata tipleri ve From dönüşümlerini ayrıntılı işler.
Bu bölümde
- Hata bir değerdir (
Result) ve imzada görünür;?onu zahmetsiz yayar. panic!exception değildir; beklenen hatalar her zamanResultile ifade edilir.- Görünmez kontrol akışı (throw) yerine açık, tip güvenli akış elde edersin.
- Erken
.unwrap()alışkanlığından kaçın;?ile yay ya da.expectkullan.
08 Eşzamanlılık zihniyeti
C/C++'ta eşzamanlılık "paylaş ve kilitle"dir; Rust'ta öncelik "sahiplik + mesajlaş"tır ve veri yarışları derleme zamanında imkânsızdır.
C/C++'ta çok thread'li kod yazmak, doğruluğu tamamen programcının disiplinine bırakan bir mayın tarlasıdır: hangi mutex hangi veriyi korur, kilit sırası nedir, bir alanı kilitsiz okumak güvenli mi? Bu soruların cevabı yorum satırlarında yaşar ve hata yaptığında veri yarışı (data race) ortaya çıkar — yeniden üretilmesi en zor bug sınıfı. Rust'ın iddiası radikaldir: güvenli kodda veri yarışı derleme zamanında imkânsızdır. Bu, ownership kurallarının ve iki işaretleyici trait'in (Send, Sync) doğal bir sonucudur.
&T ile bakmak güvenlidir.Rc tek-thread'liktir; derleyici onu thread sınırından geçirmez.Önce tercih edilen kalıp mesajlaşmadır: veriyi paylaşmak yerine sahipliğini bir kanal (channel) üzerinden gönderirsin. "Belleği paylaşarak iletişim kurma, iletişim kurarak belleği paylaş" felsefesi. Paylaşımlı durum gerçekten kaçınılmazsa Arc<Mutex<T>> kullanırsın — ama burada bile veriye mutex'i kilitlemeden erişemezsin, çünkü Mutex::lock() sana ancak kilitliyken geçerli bir &mut verir.
// C++: mutex ve veri AYRI — kilitlemeyi unutmak derlenir
std::mutex m;
int sayac = 0; // koruma sadece konvansiyon
sayac++; // mutex'i unuttun: veri yarışı, derleyici sessiz
// Rust: veri mutex'in İÇİNDE — kilitlemeden erişemezsin
use std::sync::{Arc, Mutex};
let sayac = Arc::new(Mutex::new(0));
{
let mut g = sayac.lock().unwrap(); // kilit alındı
*g += 1; // erişim ancak kilitliyken
} // g düşünce kilit otomatik bırakılır (Drop)
Farkı gör: C++'ta mutex ile koruduğu veri ayrı yaşar, ikisini birbirine bağlayan tek şey programcının niyetidir. Rust'ta veri mutex'in içine konur, dolayısıyla veriye erişmenin tek yolu kilidi almaktır. Üstelik kilit, kilit nesnesi (MutexGuard) kapsamdan çıkınca otomatik bırakılır — C++'taki "unutulan unlock" bug'ı yapısal olarak imkânsızdır.
| C / C++ | Rust | Ne zaman |
|---|---|---|
std::thread | std::thread::spawn | OS thread'i; benzer model. |
std::mutex + veri (ayrı) | Mutex<T> (veri içeride) | Kilitsiz erişim derlenmez. |
shared_ptr (atomik sayım) | Arc<T> | Thread'ler arası paylaşımlı sahiplik. |
| elle bırakılan kilit | RAII guard (Drop) | Unutulan unlock imkânsız. |
| concurrent queue + manuel senkron. | mpsc kanal | Mesajlaşma; paylaşımdan kaçın. |
Karar kuralı basit: veri akışı tek yönlüyse ya da iş parçaları arasında "sahiplik devri" mantıklıysa channel kullan; gerçekten paylaşımlı, her iki taraftan okunup yazılan bir durum varsa Arc<Mutex<T>> kullan. Rc'yi thread'ler arası kullanmaya çalışırsan derleyici !Send diye reddeder — bu bir engel değil, seni veri yarışından koruyan kontroldür. İlgili eşzamanlılık rehberi async ve thread havuzlarını da kapsar.
Bu bölümde
- Öncelik "sahiplik + mesajlaş"; paylaşımlı kilit son çaredir.
Send/Syncsayesinde veri yarışları derleme zamanında engellenir.Mutex<T>veriyi içine alır; kilitsiz erişim derlenmez, unlock unutulamaz.- Tek yönlü akış için channel, çift yönlü paylaşımlı durum için
Arc<Mutex>.
09 Öğrenme yol haritası
Doğru sırayla öğrenirsen geçiş haftalar, yanlış sırayla aylar sürer; önce ownership'i içselleştir, kestirme kaçışlardan uzak dur ve küçük gerçek bir proje yaz.
Önerilen öğrenme sırası, her adımın bir öncekine yaslanacak şekilde tasarlandı. Bu sırayı atlamak — örneğin ownership'i tam oturtmadan trait'lere geçmek — tam olarak "borrow checker'la güreşme" dönemini uzatır.
ownership/borrow → enum/Option/match → Result/? → trait/generics → koleksiyon/iterator → lifetimes → smart pointer (Box/Rc/Arc) → eşzamanlılık
| Sıra | Konu | Neden bu sırada |
|---|---|---|
| 1 | Ownership & borrowing | Her şeyin temeli; bunu atlarsan sonrası anlamsız. |
| 2 | enum, Option, match | NULL'dan kurtuluş; pattern matching refleksi. |
| 3 | Result & ? | Hata yönetimi deyimini erken oturt. |
| 4 | Trait & generics | Kalıtım refleksini kompozisyona çevir. |
| 5 | Koleksiyon & iterator | Manuel döngü refleksini sök. |
| 6 | Lifetimes | Borrow checker'ı tam anla; çoğu zaman örtük. |
| 7 | Smart pointer (Box/Rc/Arc) | Paylaşımlı sahipliği gerektiğinde. |
| 8 | Eşzamanlılık & async | En son; tüm modeli kullanır. |
Şimdi madalyonun diğer yüzü: erken kaçış tuzakları. Bunlar Rust'ı zorlaştıran değil, öğrenmeni geciktiren kestirmelerdir. Hepsi "borrow checker'a teslim olmamak için ödenen rüşvet"tir ve hepsi öğrenmeyi engeller.
Üç büyük erken kaçış: (1) her şeyi .clone()'lamak — borrow checker'ı düşünmek yerine kopyayla satın almak; (2) Rc<RefCell<T>>'e erken sığınmak — bu paylaşımlı değiştirilebilirliği runtime'a iter ve aslında C++ alışkanlığını taklit eder, gerçekten gerekene kadar kullanma; (3) unsafe'e ya da .unwrap() spam'ına kaçmak — derleyiciyle güreşmek yerine onu devre dışı bırakmak. Bu üçü kısa vadede ilerletir, uzun vadede Rust'ı öğretmez.
Teori kadar pratik şart: küçük, gerçek bir proje yaz. "Hello world"den sonra en iyi öğretici, somut bir araç yapmaktır. Önerilen ilk projeler: bir komut satırı aracı (örn. grep benzeri bir arama, log filtreleyici, dosya istatistikçisi), basit bir HTTP istemcisi, ya da bir JSON/CSV dönüştürücü. Bu projeler ownership, Result, iterator ve string tiplerinin hepsine aynı anda dokunur — yani bu rehberin tamamını pratiğe döker.
Kapanış: ilk haftalarda borrow checker'la geçirdiğin her saat, gelecekte üretimde oluşmayacak bir use-after-free, data race ya da null dereference'ın bedelidir — sadece zamanı öne çekiyorsun. Bir C/C++ programcısı olarak bu hataların maliyetini zaten biliyorsun; Rust onları derleyiciye fatura ediyor. Direnç geçicidir, sonra "bu nasıl derlendiyse doğrudur" rahatlığı kalıcıdır. Bu rehberdeki diğer Rust rehberleri her konuyu derinleştirir — sırayla üzerlerinden geç ve küçük bir proje yazmaya bugün başla.
Bu bölümde
- Sıra: ownership → enum/Option → Result → trait → iterator → lifetimes → smart pointer → eşzamanlılık.
- Erken kaçışlardan kaçın: her şeyi clone'lama,
Rc<RefCell>/unsafe'e erken sığınma,.unwrap()spam'ı. - Küçük gerçek bir proje (CLI aracı, dönüştürücü) tüm kavramları aynı anda pekiştirir.
- Kaynaklar: The Book, Rustlings, std docs, clippy; direnç geçicidir, garanti kalıcıdır.