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

Trait & Generics
Kalıtımsız polimorfizm

Trait'ler (interface), generics ve trait bound'lar; static ve dynamic dispatch — C++ virtual/template ve abstract class'larla karşılaştırmalı, kalıtımın neden yok olduğu.

00 Kalıtımdan trait'e

C++'ın abstract class + virtual + multiple inheritance üçlüsünü Rust kaldırdı. Yerine iki dik kavram koydu: davranış için trait, durum paylaşımı için composition. Sınıf kalıtımı diye bir şey yok.

C++'ta polimorfizm tipik olarak bir taban sınıftan türeyerek kurulur. Bir Shape abstract class'ı, saf sanal area() ve birkaç ortak veri alanı içerir; Circle ondan türer, hem arayüzü hem state'i miras alır. Bu tek mekanizma üç ayrı işi birden yapar: arayüz sözleşmesi, kod yeniden kullanımı ve alt-tip ilişkisi (subtyping).

Rust bu üç işi ayırır. Arayüz sözleşmesini trait üstlenir — saf bir davranış kümesi, state taşımaz. Kod yeniden kullanımını trait'lerdeki default metotlar ile generic blanket impl'ler sağlar. State paylaşımı ise düpedüz kompozisyondur: ortak alanları bir alana koyar, ihtiyacı olan tipe gömersin.

C++ class hierarchy        Rust ayrışması
─────────────────          ─────────────────
abstract class       →     trait        (yalnız davranış)
  + virtual fns      →     trait fns
  + data members     →     composition  (struct alanı)
  + subtyping        →     dyn Trait    (gerektiğinde)

Diamond problemi neden yok

C++'ta D : B, C ve hem B hem C aynı A'dan türerse, virtual kalıtım olmadan A iki kez gömülür — elmas (diamond) belirsizliği. Rust'ta bir tipin state'i tek bir struct'tadır; trait'ler state taşımadığı için aynı trait'i iki yoldan miras almak diye bir durum oluşmaz. Bir tip aynı trait'i yalnızca bir kez impl edebilir; çatışma matematiksel olarak imkânsızdır.

NOT

Rust'ta "is-a" ilişkisi runtime düzeyinde yalnızca dyn Trait ile vardır ve o da tam anlamıyla alt-tipleme değildir. Veri modelinde sınıf ağacı kurmaya çalışmayın — bu refleksi bırakmak Rust'a geçişin en zor adımıdır.

Bu bölümde

  • C++ abstract class tek mekanizmayla arayüz + state + subtyping yapar
  • Rust bunları trait (davranış) ve composition (state) olarak ayırır
  • Sınıf kalıtımı yoktur; diamond problemi tasarımdan dolayı oluşamaz
  • "is-a" yerine "has-a" ve "can-do" düşünmeye alışmak gerekir

01 Trait tanımı ve impl

Bir trait, C++'taki saf sanal metotlardan oluşan bir interface gibidir; ama tanım ile implementasyon fiziksel olarak ayrıdır. Tipi yazan kişi ile trait'i ekleyen kişi farklı olabilir.

Trait, metot imzalarının kümesidir. &self parametresi C++'taki this'in karşılığıdır; &mut self ise non-const metoda denk gelir. Bir tip için trait'i ayrı bir impl ... for ... bloğunda gerçeklersin.

main.rs
trait Animal {
    // C++: virtual std::string sound() const = 0;
    fn sound(&self) -> String;
    fn name(&self) -> &str;
}

struct Dog { ad: String }

// Tip tanımından ayrı: davranışı sonradan iliştiriyoruz
impl Animal for Dog {
    fn sound(&self) -> String { "hav".to_string() }
    fn name(&self) -> &str { &self.ad }
}

fn main() {
    let d = Dog { ad: "Karabaş".to_string() };
    println!("{}: {}", d.name(), d.sound());
}
&selfSalt-okunur alıcı — C++ const metot karşılığı.
&mut selfMutating alıcı — non-const metot karşılığı.
selfSahipliği tüketir — nesneyi alıp yok eden metot (örn. into).

Orphan rule

Bir impl Trait for Tip yazabilmen için trait ya da tip senin crate'inde tanımlı olmalı. İkisi de yabancıysa (örn. başka crate'in Display'ini başka crate'in Vec'ine impl etmek) derleyici reddeder. Bu "orphan rule" kuralı, iki ayrı bağımlılığın aynı çift için çakışan impl yazıp birbirini bozmasını engeller — C++'ta böyle bir koruma yoktur (ODR ihlalleri linker'a kalır).

Bu bölümde

  • Trait = saf sanal metotlardan oluşan interface; state taşımaz
  • Tip tanımı ile impl bloğu fiziksel olarak ayrıdır
  • &self / &mut self / self alıcıları const-lik ve sahipliği belirler
  • Orphan rule: trait veya tip mutlaka yerel crate'te olmalı

02 Default metotlar

Trait metodu gövde de içerebilir. Bu, C++'ta abstract class'a koyduğun ortak (non-pure) virtual metoda denktir — fark, implementörün override etmesi tamamen isteğe bağlıdır.

Default metot, trait'in başka (default'suz) metotları üzerine kurulabilir. Bir tip yalnız minimum çekirdek metodu impl eder, gerisini bedavaya alır. İstersen default'u override edersin.

main.rs
trait Greet {
    fn name(&self) -> String;        // gerekli

    // Default gövde — diğer metoda dayanıyor
    fn hello(&self) -> String {
        return format!("Merhaba, {}!", self.name());
    }
}

struct Tr;
impl Greet for Tr {
    fn name(&self) -> String { "Emirhan".to_string() }
    // hello() override edilmedi → default kullanılır
}

struct En;
impl Greet for En {
    fn name(&self) -> String { "Em".to_string() }
    fn hello(&self) -> String {     // override
        format!("Hi, {}!", self.name())
    }
}
NOT

Default metot trait'in sadece public arayüzünü görür — implementör tipin private alanlarına erişemez. C++'ta protected üye üzerinden veriye uzanan template-method deseni Rust'ta doğrudan mümkün değildir; veriye gereksinim varsa onu trait metodu olarak imzalarsın.

Bu bölümde

  • Trait metotları gövde içerebilir; override isteğe bağlıdır
  • Default'lar diğer trait metotları üzerine kurulur (template method)
  • C++'ın non-pure virtual'ına benzer; ama state'e erişemez
  • Minimum çekirdeği impl et, türev davranışı bedavaya al

03 Generics — fonksiyonlar

Generic fonksiyon, C++ template fonksiyonunun birebir karşılığıdır: tek imza yaz, derleyici her somut tip için ayrı bir kopya üretir. Buna monomorphization denir.

Tip parametresi <T> ile gelir. Kullandığın her farklı T için derleyici fonksiyonu yeniden örnekler — max::<i32>, max::<f64> gibi ayrı makine kodları. Çağrı yerinde virtual indirection yoktur; her çağrı statik olarak bağlanır ve inline edilebilir.

main.rs
// C++: template<class T> T max(T a, T b) { ... }
fn max<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    // Tek imza, iki ayrı monomorphized kopya:
    println!("{}", max(3, 9));        // max::<i32>
    println!("{}", max(2.5, 1.0));    // max::<f64>
}

C++ template'inden tek kritik fark şudur: Rust'ta T: PartialOrd bound'unu yazmak zorundasın. Yazmazsan a > b satırı derlenmez. C++'ta ise bound'suz template gövdesi yazılır, hata ancak örnekleme anında ve genellikle anlaşılmaz mesajlarla patlar. Rust hatayı tanım yerinde verir.

DİKKAT

Monomorphization hızlıdır ama bedeli kod şişmesidir (code bloat): 20 farklı tiple çağrılan büyük bir generic fonksiyon ikili dosyada 20 kez yer kaplar. C++ template'leriyle aynı takas. Şişme bir sorunsa s07'deki dyn dinamik dispatch'e bakın.

Bu bölümde

  • Generic fonksiyon = C++ template fonksiyonu, monomorphization ile
  • Her somut tip için ayrı, inline'lanabilir makine kodu üretilir
  • Bound (T: PartialOrd) tanım yerinde zorunlu — hata erkene çekilir
  • Takas: hız vs ikili boyut (code bloat)

04 Generics — struct & enum

Tip parametreleri veri yapılarına da takılır. struct Wrapper<T> ve Option<T>, C++'taki template<class T> struct ...'in karşılığıdır; impl<T> blokları ise metotları taşır.

main.rs
struct Wrapper<T> { value: T }

// impl bloğu da T üzerinden generic'tir
impl<T> Wrapper<T> {
    fn new(v: T) -> Self { Self { value: v } }
    fn get(&self) -> &T { &self.value }
}

// Std'deki Option<T> tam olarak böyle bir generic enum'dur:
enum Maybe<T> {
    Some(T),
    None,
}

fn main() {
    let w = Wrapper::new(42i64);
    println!("{}", w.get());
    let m: Maybe<&str> = Maybe::Some("x");
    match m { Maybe::Some(s) => println!("{}", s), Maybe::None => {} }
}

İki ayrıntı C++ geliştiricisini şaşırtır. Birincisi: impl<T> Wrapper<T> satırında T iki kez geçer — soldaki <T> parametreyi tanıtır, sağdaki Wrapper<T> onu kullanır. İkincisi: belirli bir tip için ek metot eklemek istersen ayrı bir somut blok yazarsın.

main.rs
// Sadece Wrapper<i32> için ekstra metot (C++ template specialization gibi)
impl Wrapper<i32> {
    fn double(&self) -> i32 { self.value * 2 }
}

Bu bölümde

  • struct/enum tip parametresi alır: Wrapper<T>, Option<T>
  • impl<T> Wrapper<T> — soldaki T tanıtır, sağdaki kullanır
  • impl Wrapper<i32> ile somut tipe özel metot (specialization benzeri)
  • Std'deki Option/Result aynı mekanizmanın ürünüdür

05 Trait bound'lar

Bound, bir generic'in kabul ettiği tipleri kısıtlar: T: Display + Clone demek "T hem yazdırılabilir hem klonlanabilir olmalı". Bu, C++20 concept'lerinin doğrudan karşılığıdır — ama Rust'ta concept opsiyonel değil, dilin temelidir.

Bound, fonksiyonun gövdesinde hangi metotları/operatörleri kullanabileceğini belirler. T: Display yazmadan {} ile yazdıramaz, T: Clone yazmadan .clone() çağıramazsın. Birden çok bound + ile birleşir.

main.rs
use std::fmt::Display;

// T: yazdırılabilir VE klonlanabilir olmalı
fn duyur<T: Display + Clone>(x: T) {
    let kopya = x.clone();      // Clone sayesinde
    println!("deger = {}", kopya); // Display sayesinde
}

fn main() {
    duyur("selam");   // &str: Display + Clone ✓
    duyur(7);          // i32:  Display + Clone ✓
    // duyur(vec![1,2]); // Vec: Display değil → DERLENMEZ
}

Duck typing'e karşı açık sözleşme

C++'ın klasik (pre-concept) template'i "duck typing" yapar: gövdede x.clone() çağırırsın, tip o metoda sahip değilse hata örnekleme anında, çağrı zincirinin derinlerinde patlar. Rust'ta bound bir sözleşmedir: derleyici hem fonksiyonun gövdesini bound'a göre, hem her çağrıyı tipin bound'u sağlayıp sağlamadığına göre ayrı ayrı doğrular. Hata daima tanım veya çağrı yerindedir, asla şablon iç organlarında değil.

MekanizmaSözleşmeHata yeri
C++ template (klasik)örtük (duck typing)örnekleme, derinde
C++20 conceptaçık, opsiyonelçağrı yeri
Rust trait boundaçık, zorunlutanım + çağrı yeri

Bu bölümde

  • Bound, generic'in kabul ettiği tip kümesini daraltır
  • Gövdede kullanılan her metot bir bound gerektirir (zorunlu sözleşme)
  • C++20 concept'e denk; ama Rust'ta opsiyonel değil temeldir
  • Hatalar tanım/çağrı yerinde; template iç organlarında değil

06 where cümleleri

where, imzaya sıkışan karmaşık bound'ları aşağı alıp okunur kılar. impl Trait ise tip parametresini hiç adlandırmadan "bu trait'i sağlayan bir şey" demenin kısayoludur — argüman ve dönüş konumunda.

Bound sayısı arttıkça fn f<T: A + B, U: C + D>(...) imzası okunmaz hale gelir. where aynı kısıtları gövdeden önce, satır satır listeler.

main.rs
use std::fmt::Display;
use std::fmt::Debug;

// Inline bound — kalabalık
fn f1<T: Display + Clone, U: Debug + Default>(t: T, u: U) {}

// where ile — aynı anlam, temiz imza
fn f2<T, U>(t: T, u: U)
where
    T: Display + Clone,
    U: Debug + Default,
{}

impl Trait konumları

Argüman konumunda impl Trait, anonim bir generic parametredir — fn yaz(x: impl Display) tam olarak fn yaz<T: Display>(x: T) demektir, ama T'yi adlandırmazsın. Dönüş konumunda ise impl Trait "somut tipini gizlediğim, şu trait'i sağlayan bir şey döndürüyorum" anlamına gelir — closure ve iterator döndürürken vazgeçilmezdir, çünkü onların adlandırılamayan tipleri vardır.

main.rs
use std::fmt::Display;

// Argüman: anonim generic
fn yaz(x: impl Display) { println!("{}", x); }

// Dönüş: somut tip gizli (closure'un adı yazılamaz)
fn sayac() -> impl Iterator<Item = i32> {
    (0..3).map(|n| n * 10)
}

fn main() {
    yaz(99);
    for v in sayac() { println!("{}", v); }
}
NOT

Dönüşteki impl Trait hâlâ static dispatch'tir: derleyici tek somut tipi bilir, sadece seni ondan saklar. Çağrı başına farklı somut tip döndürmek istersen (örn. koşula göre iki ayrı closure) Box<dyn Trait> gerekir — bu s07'nin konusu.

Bu bölümde

  • where, karmaşık bound'ları imzadan ayırıp okunur kılar
  • Argümandaki impl Trait = adsız generic parametre
  • Dönüşteki impl Trait = somut tipi gizleyen static dispatch
  • Closure/iterator döndürürken impl Trait pratikte zorunludur

07 Static vs dynamic dispatch

İki yol var. Generic = static dispatch: monomorphize edilir, inline'lanır, hızlı ama kod şişer. dyn Trait = dynamic dispatch: vtable üzerinden çağrı, tek kod, runtime maliyet. Bu C++'taki template vs virtual ayrımının birebir aynısıdır.

BoyutGeneric (static)dyn Trait (dynamic)
C++ karşılığıtemplatevirtual + pointer
Çözümlemederleme anıruntime (vtable)
Çağrı maliyetisıfır, inline1 indirect jump
İkili boyuttip başına kopyatek kopya
Heterojen koleksiyonhayırevet

Static yol bildiğimiz generic. Dynamic yolda tipi dyn Trait arkasına saklarsın; ama dyn Trait'in boyutu derleme anında bilinmediği için onu daima bir pointer ardında tutman gerekir: &dyn Trait (ödünç) ya da Box<dyn Trait> (sahipli, heap).

main.rs
trait Shape { fn area(&self) -> f64; }

struct Circle { r: f64 }
struct Square { s: f64 }
impl Shape for Circle { fn area(&self) -> f64 { 3.14159 * self.r * self.r } }
impl Shape for Square { fn area(&self) -> f64 { self.s * self.s } }

// STATIC: monomorphize, inline — tek tip
fn alan_static<T: Shape>(s: &T) -> f64 { s.area() }

// DYNAMIC: vtable üzerinden — herhangi bir Shape
fn alan_dyn(s: &dyn Shape) -> f64 { s.area() }

fn main() {
    // Heterojen koleksiyon SADECE dyn ile mümkün:
    let sekiller: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { r: 1.0 }),
        Box::new(Square { s: 2.0 }),
    ];
    let mut toplam = 0.0;
    for sk in &sekiller {
        toplam += sk.area();    // her çağrı vtable lookup
    }
    println!("toplam alan = {}", toplam);
}

Karar kuralı sıcak yolda nettir. C++'ta da aynısını yaparsın: küçük, çok çağrılan, tip-tekil işlevler için generic/template (inline, sıfır maliyet). Çalışma anında farklı tipleri tek kapta tutman gerekiyorsa veya kod şişmesini bastırmak istiyorsan dyn/virtual.

DİKKAT

Her trait dyn olamaz; trait'in object-safe olması gerekir. Generic metotlar (fn f<U>(...)) ya da Self döndüren metotlar trait'i object-safe olmaktan çıkarır — çünkü vtable'da sabit bir imza tutulamaz. C++'taki "template virtual method olamaz" kuralının tam karşılığı.

Bu bölümde

  • Generic = static dispatch (template): inline, hızlı, kod şişer
  • dyn Trait = dynamic dispatch (virtual): vtable, tek kod, runtime maliyet
  • dyn boyutsuzdur → &dyn Trait veya Box<dyn Trait> ardında tutulur
  • Heterojen koleksiyon yalnız dyn ile; trait object-safe olmalı

08 Supertrait & composition

Kalıtım yerine kompozisyon Rust'ta slogan değil pratik. Bir tip birden çok trait impl ederek davranışları birleştirir; supertrait ("A için B gerekir") trait'ler arası bağ kurar; blanket impl ise bir trait'i sağlayan her tipe bir başka trait'i toptan ekler.

Supertrait, trait Foo: Bar sözdizimiyle "Foo impl eden tip Bar'ı da impl etmek zorunda" der ve Foo'nun default metotları Bar'ın metotlarını kullanabilir. C++'taki interface kalıtımına benzer ama yine state taşınmaz.

main.rs
trait Named { fn name(&self) -> String; }

// Supertrait: Greet'i impl eden Named'i de impl etmeli
trait Greet: Named {
    fn hello(&self) -> String {
        format!("Merhaba, {}", self.name()) // Named'den
    }
}

struct User;
impl Named for User { fn name(&self) -> String { "x3".into() } }
impl Greet for User {}   // default yeterli

Davranışı birleştirme (kalıtım değil)

C++'ta "uçabilen ve yüzebilen bir şey" için class Duck : public Flyer, public Swimmer yazardın — multiple inheritance, diamond riski. Rust'ta bunu iki ayrı trait impl ederek yaparsın; çatışma yok, çünkü state tek struct'ta.

main.rs
trait Flyer { fn fly(&self) -> &str { "uçuyor" } }
trait Swimmer { fn swim(&self) -> &str { "yüzüyor" } }

struct Duck;
impl Flyer for Duck {}      // davranış 1
impl Swimmer for Duck {}    // davranış 2

// BLANKET IMPL: Display olan HER tip otomatik Loglanabilir
use std::fmt::Display;
trait Loggable { fn log(&self); }
impl<T: Display> Loggable for T {
    fn log(&self) { println!("[log] {}", self); }
}

Yukarıdaki impl<T: Display> Loggable for T bir blanket impl'dir: artık i32, &str, kısaca Display sağlayan her tip ücretsiz .log() kazanır. Std'deki ToString, tam olarak Display üzerine kurulu bir blanket impl'dir. Bu güç C++'ta doğrudan yoktur.

Bu bölümde

  • Supertrait (trait Foo: Bar): Foo, Bar'ın metotlarına dayanabilir
  • Çok trait impl ederek davranış birleştir — multiple inheritance'sız
  • State tek struct'ta olduğu için diamond çatışması oluşmaz
  • Blanket impl: bir bound'u sağlayan her tipe toplu davranış ekler

09 Standart trait'ler + gerçek örnek

Std'nin trait sözlüğü dilin yarısıdır: Debug/Display yazdırma, From/Into dönüşüm, PartialEq/Eq/Ord karşılaştırma, Default başlangıç, Iterator dolaşma, Clone/Copy kopyalama. Çoğu #[derive(...)] ile tek satırda otomatik üretilir.

TraitİşC++ karşılığı
Debug / Display{:?} / {} ile yazmaoperator<<
From / Intotip dönüşümüconverting ctor
PartialEq / Eq== eşitlikoperator==
PartialOrd / Ord< > sıralamaoperator< / <=>
Defaultvarsayılan değerdefault ctor
Clone / Copyderin / bit kopyacopy ctor
Iteratordolaşma protokolübegin()/end()

#[derive], alan alan giderek bu trait'leri otomatik üretir — C++'taki = default'un çok daha kapsamlısı. From impl edersen Into'yu bedavaya alırsın (blanket impl sağ olsun).

main.rs
// Tek satırda 4 trait otomatik üretildi:
#[derive(Debug, Clone, PartialEq, Default)]
struct Point { x: i32, y: i32 }

// From impl → Into bedavaya gelir
impl From<(i32, i32)> for Point {
    fn from(t: (i32, i32)) -> Self {
        Point { x: t.0, y: t.1 }
    }
}

fn main() {
    let p: Point = (3, 4).into();   // Into, From'dan türedi
    let q = p.clone();             // Clone derive'dan
    println!("{:?} == {:?}: {}", p, q, p == q); // Debug + PartialEq
}

Kapanış: iki dünya bir arada

Gerçek kod ikisini birden kullanır: heterojen veriyi Vec<Box<dyn Shape>> ile (dynamic dispatch) tutar, üzerinde işlemi generic bir fonksiyonla (static dispatch) yaparsın. Aşağıda topla generic'tir ama herhangi bir &[Box<dyn Shape>] dilimini kabul eder.

main.rs
trait Shape {
    fn area(&self) -> f64;
    fn ad(&self) -> &str;
}

struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }

impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r }
    fn ad(&self) -> &str { "daire" }
}
impl Shape for Rect {
    fn area(&self) -> f64 { self.w * self.h }
    fn ad(&self) -> &str { "dikdörtgen" }
}

// GENERIC fonksiyon (static) — DYN koleksiyon (dynamic) üstünde
fn topla<S: AsRef<[Box<dyn Shape>]>>(sekiller: S) -> f64 {
    sekiller.as_ref().iter().map(|s| s.area()).sum()
}

fn main() {
    let v: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { r: 1.0 }),
        Box::new(Rect { w: 2.0, h: 3.0 }),
    ];
    for s in &v {
        println!("{}: {:.2}", s.ad(), s.area()); // vtable
    }
    println!("toplam = {:.2}", topla(v));    // generic
}
NOT

Tasarım refleksi: önce generic + bound (static, sıfır maliyet) ile başla. Yalnızca farklı somut tipleri tek kapta tutmak gerektiğinde dyn'e geç. C++'tan gelen "her şey için taban sınıf hiyerarşisi" alışkanlığını burada bırak — trait'ler ve composition fazlasıyla yeter.

Bu bölümde

  • Std trait'leri (Debug, From, Ord, Default, Iterator, Clone) dilin temeli
  • #[derive(...)] çoğunu tek satırda üretir; From verince Into bedava
  • Gerçek kod: Vec<Box<dyn Shape>> (dynamic) + generic fonksiyon (static)
  • Reflex: önce generic/static, gerekince dyn/dynamic; kalıtım refleksini bırak