Rust'ta generic (jenerik) tip, bir fonksiyonu, struct'ı, enum'u ya da metodu belirli bir veri tipine bağlamadan yazmanızı sağlayan tip parametresidir. fn en_buyuk<T: PartialOrd>(liste: &[T]) -> &T gibi bir imzada T yer tutucudur; fonksiyonu i32 dilimiyle çağırdığınızda T yerine i32, char dilimiyle çağırdığınızda char geçer. Derleyici her somut tip için ayrı bir kopya üretir (monomorphization), bu yüzden generic kod çalışma anında elle yazılmış koddan yavaş değildir.

Bu yazıda generic fonksiyonları, birden fazla tip parametresi taşıyan struct ve enum'ları, impl<T> bloklarını, yalnızca belirli bir tipe özel metotları, const generic parametreleri, turbofish sözdizimini ve en sık karşılaşılan derleme hatalarını çalışan örneklerle göreceksiniz. Tip parametresine "şu davranışı desteklemeli" kısıtını koyan trait bound konusu burada yalnızca gerektiği kadar geçiyor; ayrıntısı trait tanımlama ve impl yazısında. Serinin tamamı için Rust öğrenme rehberi sayfasına bakabilirsiniz.

Neden generic? Tekrarlanan kodu tek tanıma indirmek

Neden generic? Tekrarlanan kodu tek tanıma indirmek

The Rust Book bu konuyu çok tanıdık bir sorunla açar: bir dilimdeki en büyük i32 değerini bulan fonksiyon ile en büyük char değerini bulan fonksiyonun gövdesi harfi harfine aynıdır, yalnızca imzadaki tipler farklıdır. Aynı mantığı iki kez yazmak hem bakım yükü getirir hem de bir düzeltmenin bir kopyada unutulmasına yol açar. Generic, bu iki fonksiyonu tek bir tanıma indirir. Tip parametresini fonksiyon adı ile parametre listesi arasına, açılı parantez içinde bildirirsiniz; ardından parametrelerde ve dönüş tipinde bu adı kullanırsınız.

Ad seçiminde serbestsiniz, ancak topluluk geleneği kısa ve UpperCamelCase adlardır; "type" kelimesinin kısaltması olan T en yaygın tercihtir. İkinci ve üçüncü parametre için genellikle U, hata tipi için E, anahtar ve değer için K ve V görürsünüz. İlk denemede çoğu kişi aşağıdaki gibi bir kod yazar ve derleyiciden hemen bir hata alır:

rust
fn en_buyuk<T>(liste: &[T]) -> &T {
    let mut en_buyuk = &liste[0];
    for oge in liste {
        if oge > en_buyuk {
            en_buyuk = oge;
        }
    }
    en_buyuk
}

fn main() {
    let sayilar = vec![34, 50, 25, 100, 65];
    println!("{}", en_buyuk(&sayilar));
}
// error[E0369]: binary operation `>` cannot be applied to type `&T`

Hata mesajı çok öğreticidir: derleyici T hakkında hiçbir şey bilmediği için, T yerine geçebilecek her tipin > ile karşılaştırılabileceğini varsayamaz. Hata, fonksiyon gövdesinin T için olası tüm tiplerle çalışmayacağını söyler; yani denetim, fonksiyonu hangi tiple çağırdığınıza bakılmadan gövdenin kendisi üzerinde yapılır. Çözüm, T için sıralanabilir olma koşulunu yazmaktır. Derleyicinin help satırında önerdiği gibi std::cmp::PartialOrd trait'ini bound olarak ekleriz. Standart kütüphane bu trait'i hem i32 hem char için uyguladığı için aynı fonksiyon iki tiple de çalışır:

rust
fn en_buyuk<T: PartialOrd>(liste: &[T]) -> &T {
    let mut en_buyuk = &liste[0];
    for oge in liste {
        if oge > en_buyuk {
            en_buyuk = oge;
        }
    }
    en_buyuk
}

fn main() {
    let sayilar = vec![34, 50, 25, 100, 65];
    println!("En büyük sayı: {}", en_buyuk(&sayilar));

    let harfler = vec!['ç', 'a', 'z', 'ş'];
    println!("En büyük harf: {}", en_buyuk(&harfler));

    let kelimeler = ["elma", "armut", "üzüm"];
    println!("En büyük kelime: {}", en_buyuk(&kelimeler));
}
// çıktı: En büyük sayı: 100
// çıktı: En büyük harf: ş
// çıktı: En büyük kelime: üzüm

Çıktıdaki harf ve kelime sonuçları alfabetik Türkçe sıralamayı değil, Unicode kod noktası sırasını yansıtır: char karşılaştırması kod noktası değerine, &str karşılaştırması ise bayt bayt UTF-8 içeriğine bakar. "ş" (U+015F) "z" harfinden büyük çıkar. Türkçe alfabeye göre sıralama gerekiyorsa kendi karşılaştırma fonksiyonunuzu yazmanız gerekir. Fonksiyonun boş dilimle çağrılması da ayrı bir tuzaktır: &liste[0] boş dilimde panic üretir. Gerçek kodda dönüş tipini Option<&T> yapmak daha güvenlidir; enum ve Option yazısı bu deseni anlatır.

Generic struct: tek ve çok tip parametresi

Struct tanımında tip parametresini struct adından hemen sonra bildirirsiniz. struct Nokta<T> { x: T, y: T } tanımı, x ve y alanlarının aynı tipte olmasını şart koşar; tip ne olursa olsun ikisi de o tiptir. Bu yüzden bir alana tam sayı, diğerine ondalıklı sayı vermek derleme hatasıdır:

rust
struct Nokta<T> {
    x: T,
    y: T,
}

fn main() {
    let olmaz = Nokta { x: 5, y: 4.0 };
}
// error[E0308]: mismatched types

Derleyici x: 5 değerini görünce bu örnek için T tipinin bir tam sayı tipi olduğuna karar verir; ardından gelen 4.0 bu kararla çelişir. İki alanın farklı tipte olabilmesini istiyorsanız iki ayrı tip parametresi tanımlayın: struct Nokta<T, U>. İstediğiniz kadar parametre ekleyebilirsiniz, ancak Book'un uyarısı yerindedir: çok sayıda tip parametresine ihtiyaç duyuyorsanız bu, kodun daha küçük parçalara bölünmesi gerektiğine işaret olabilir.

impl bloklarında generic ve tipe özel metotlar

impl bloklarında generic ve tipe özel metotlar

Generic bir struct için metot yazarken impl anahtar kelimesinden hemen sonra da tip parametresini bildirmeniz gerekir: impl<T> Nokta<T>. Buradaki ilk <T> "bu blokta T adında bir tip parametresi var" der, ikincisi ise metotların hangi tip üzerine yazıldığını belirtir. impl sonrasında bildirim yapılmazsa Rust, Nokta<T> içindeki T harfini T adında somut bir tip olarak arar. Bu blokta yazılan her metot, T yerine hangi tip gelirse gelsin her Nokta örneğinde kullanılabilir.

Bazen bir metot yalnızca belirli bir tip için anlamlıdır. Örneğin orijine uzaklığı hesaplamak karekök ve üs alma gerektirir; bu işlemler yalnızca ondalıklı tiplerde vardır. Bu durumda impl sonrasına hiçbir tip parametresi yazmadan somut tipi kullanırsınız: impl Nokta<f64>. Bu blokta tanımlanan metot yalnızca Nokta<f64> için vardır; Nokta<i32> üzerinde çağırmaya çalışırsanız metot bulunamaz. Metotların kendi tip parametreleri de olabilir; aşağıdaki karistir metodu struct'ın parametrelerinden (X1, Y1) bağımsız olarak X2 ve Y2 parametrelerini kendisi bildirir:

rust
#[derive(Debug)]
struct Nokta<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Nokta<X1, Y1> {
    fn x(&self) -> &X1 {
        &self.x
    }

    // X2 ve Y2 yalnızca bu metoda ait
    fn karistir<X2, Y2>(self, diger: Nokta<X2, Y2>) -> Nokta<X1, Y2> {
        Nokta { x: self.x, y: diger.y }
    }
}

// Yalnızca iki alanı da f64 olan noktalar için
impl Nokta<f64, f64> {
    fn orijine_uzaklik(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p1 = Nokta { x: 5, y: 10.4 };
    let p2 = Nokta { x: "Merhaba", y: 'ç' };
    println!("p1.x = {}", p1.x());

    let p3 = p1.karistir(p2);
    println!("{:?}", p3);

    let p4 = Nokta { x: 3.0, y: 4.0 };
    println!("uzaklık = {}", p4.orijine_uzaklik());
}
// çıktı: p1.x = 5
// çıktı: Nokta { x: 5, y: 'ç' }
// çıktı: uzaklık = 5

Örnekte p3 değişkeninin tipi Nokta<i32, char> olur: x alanı p1 değişkeninden, y alanı p2 değişkeninden gelir. karistir metodu self değerini sahiplenerek aldığı için çağrıdan sonra p1 ve p2 artık kullanılamaz; bu, generic'le değil sahiplikle ilgili bir kuraldır ve ownership yazısında anlatılır. #[derive(Debug)] satırı da derleyicinin {:?} ile yazdırma kodunu üretmesini sağlar; alan tipleri Debug uygulamadığında bu türetme o tip parametreleri için çalışmaz. Ayrıntı derive yazısında.

Generic enum: Option ve Result zaten generic

Standart kütüphanenin en çok kullanılan iki tipi generic enum'dur. Option<T> ya bir T değeri taşıyan Some(T) ya da hiçbir değer taşımayan None varyantıdır. Result<T, E> iki tip parametresi taşır: başarıda Ok(T), hatada Err(E). Dosya açma örneğinde T yerine std::fs::File, E yerine std::io::Error geçer. Kendi enum'unuzu da aynı sözdizimiyle yazarsınız: varyant verilerinde tip parametresini kullanmanız yeterlidir. Book'un pratik önerisi şudur: yalnızca tuttukları değerin tipi farklı olan birden çok struct veya enum tanımı görüyorsanız, generic tip kullanarak tekrarı kaldırabilirsiniz. Hata yönetimi tarafı için Result yazısına bakın.

Const generic: sabit değer parametresi

Tip parametrelerine ek olarak Rust, öğeleri sabit bir değer üzerinden de parametreleştirebilir. En yaygın kullanım dizi uzunluğudur: [i32; 3] ile [i32; 5] farklı tiplerdir ve her uzunluk için ayrı fonksiyon yazmak yerine const N: usize parametresi tanımlarsınız. Reference'a göre const parametresinin tipi yalnızca u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, char veya bool olabilir. Parametreler sırasında önce lifetime'lar, ardından tip ve const parametreleri karışık sırada gelir.

rust
struct Tampon<const N: usize> {
    veri: [u8; N],
}

impl<const N: usize> Tampon<N> {
    fn yeni() -> Self {
        Tampon { veri: [0; N] }
    }

    fn kapasite(&self) -> usize {
        N
    }
}

fn toplam<const N: usize>(dizi: [i32; N]) -> i32 {
    let mut t = 0;
    for x in dizi {
        t += x;
    }
    t
}

fn main() {
    println!("{}", toplam([1, 2, 3]));
    println!("{}", toplam([10, 20, 30, 40, 50]));

    let t: Tampon<16> = Tampon::yeni();
    println!("kapasite = {}, ilk bayt = {}", t.kapasite(), t.veri[0]);
}
// çıktı: 6
// çıktı: 150
// çıktı: kapasite = 16, ilk bayt = 0

Const parametrelerinin bir sınırı vardır: bir tip veya dizi tekrar ifadesi içinde yalnızca tek başına kullanılabilirler. [u8; N] geçerlidir, ancak [u8; {N + 1}] gibi aritmetik içeren bir ifade kararlı Rust'ta reddedilir. Ayrıca fonksiyon gövdesinde tanımlanan iç öğeler (iç const, static, iç fonksiyon, iç struct) dıştaki generic parametreleri göremez. Çağrı yerinde değeri açıkça vermek isterseniz literal (f::<1>()), tek parçalı bir sabit adı ya da süslü parantez içinde bir sabit ifade (f::<{ 1 + 2 }>()) yazarsınız.

Turbofish: tip parametresini çağrıda açıkça vermek

Derleyici tip parametresini çoğu zaman argümanlardan veya sonucun atandığı değişkenden çıkarır. Çıkaramadığı durumda tipi iki yoldan birini kullanarak bildirirsiniz: değişkene tip notu yazmak (let n: i32 = ...) ya da çağrıda ::<> sözdizimini, topluluğun deyişiyle "turbofish"i kullanmak. Argüman almayan ve yalnızca dönüş tipinde T geçen fonksiyonlar bu durumun klasik örneğidir; parse ve collect da aynı nedenle sık sık turbofish ister:

rust
fn varsayilan<T: Default>() -> T {
    T::default()
}

fn main() {
    let a = varsayilan::<i32>();
    let b: String = varsayilan();
    let c = "42".parse::<u8>();
    let d = Vec::<u16>::with_capacity(4);

    println!("{a} {b:?} {c:?} {}", d.capacity());
}
// çıktı: 0 "" Ok(42) 4

varsayilan::<i32>() ve let b: String = varsayilan(); aynı işi farklı yerden bildirir. İkisini de yazmazsanız derleyici T için bir tip seçemez ve tip notu isteyen bir hata verir. Reference önemli bir ayrıntıya da dikkat çeker: parametre konumunda impl Trait kullanan bir fonksiyonda tip anonim olduğu için çağıran turbofish ile tip veremez. Bir fonksiyonu <T: Trait> biçiminden impl Trait biçimine (veya tersine) çevirmek, generic argüman sayısını değiştirdiği için kütüphane kullanıcıları açısından uyumluluğu bozan bir değişiklik olabilir.

Performans: monomorphization ne yapar?

Performans: monomorphization ne yapar? Fonksiyon, Struct, Enum, impl bloğu

Generic kullanmanın çalışma anında bir bedeli var mı sorusunun cevabı hayırdır. Rust derleme sırasında monomorphization uygular: generic kodun çağrıldığı her yere bakar ve kullanılan her somut tip için ayrı, generic olmayan bir kopya üretir. Some(5) ve Some(5.0) kullanan bir program derlendiğinde derleyici kavramsal olarak Option enum'unun biri i32, diğeri f64 için iki ayrı tanımını oluşturur. Çalışan kod, her tanımı elle kopyalamışsınız gibi davranır; çağrılar derleme anında çözüldüğü için buna statik dağıtım (static dispatch) denir.

Book, aynı yöntemi şöyle de özetler: derleyici, ilk örnekte elle yaptığımız genelleştirmenin tersini yapar ve generic tanımı yeniden somut tanımlara açar. Aynı koleksiyonda farklı tipleri bir arada tutmanız gerektiğinde ise generic tek başına yetmez, çünkü bir generic parametre aynı anda yalnızca tek bir somut tiple doldurulabilir. Bu durumda çalışma anında dağıtım yapan trait object'lere geçersiniz; iki yaklaşımın karşılaştırması dyn Trait yazısında.

Kullanım yeriSözdizimiNot
Fonksiyonfn f<T>(x: T) -> TParametre adı fonksiyon adından sonra
Structstruct Nokta<T> { x: T }Alanlar aynı T ise aynı tipte olmalı
Enumenum Option<T> { Some(T), None }Varyant verisinde kullanılır
impl bloğuimpl<T> Nokta<T> { }T, impl sonrasında bildirilir
Tipe özel implimpl Nokta<f64> { }Metot yalnızca o tipte vardır
Metot parametresifn karistir<X2, Y2>(self, ...)Struct parametrelerinden bağımsız
Const genericfn f<const N: usize>(a: [i32; N])Değer üzerinden parametre
Turbofishf::<i32>()Tip çıkarılamadığında açıkça verilir

Sık yapılan hatalar

  • Bound yazmadan işlem yapmak. T üzerinde >, +, {} ile yazdırma veya clone() kullanmak için ilgili trait'i bound olarak eklemeniz gerekir (PartialOrd, Add, Display, Clone). Derleyicinin help satırı genellikle doğru trait'i önerir.
  • impl sonrasında parametreyi unutmak. impl Nokta<T> yazarsanız derleyici T adında somut bir tip arar ve bulamadığını söyler. Doğrusu impl<T> Nokta<T>.
  • Tek parametreyle farklı alan tipleri beklemek. Nokta<T> içindeki tüm T alanları aynı tiptedir; farklı tip için ikinci parametre ekleyin.
  • Tip çıkarımına fazla güvenmek. Argümanda geçmeyen tip parametreleri için tip notu ya da turbofish gerekir.
  • Farklı tipleri tek Vec<T> içinde toplamaya çalışmak. Generic parametre bir anda tek tipe karşılık gelir; karışık koleksiyon için enum veya Box<dyn Trait> kullanın.
  • Const parametresiyle aritmetik yapmak. [T; N + 1] gibi tipler kararlı Rust'ta yazılamaz; tasarımı N tek başına kullanılacak şekilde kurun.

Sık Sorulan Sorular

Generic ile trait arasındaki fark nedir?

Generic, "burada bir tip olacak" diyen yer tutucudur; trait ise "bu tip şu metotlara sahip olmalı" diyen davranış sözleşmesidir. İkisi birlikte kullanılır: T: PartialOrd yazdığınızda generic parametreye trait bound eklemiş olursunuz.

Generic kod programı yavaşlatır mı?

Hayır. Monomorphization sayesinde her somut tip için ayrı kod üretilir ve çağrılar derleme anında çözülür. The Rust Book'a göre çalışan kod, her tanımı elle kopyalamışsınız gibi performans gösterir.

Tip parametresi adı mutlaka T mi olmalı?

Hayır, geçerli herhangi bir tanımlayıcı kullanılabilir. Gelenek kısa UpperCamelCase adlardır: T, U, E, K, V. Book, parametrenin tip adlandırma kuralına (UpperCamelCase) uyduğunu ve T harfinin çoğu Rust programcısının varsayılan tercihi olduğunu belirtir.

Lifetime parametreleri de generic mi?

Evet. <'a, T> gibi listelerde lifetime parametreleri de generic parametredir ve her zaman önce yazılır. Ayrıntılar lifetime yazısında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Rust öğrenme rehberi · Rust nedir · Rust trait tanımlama · Rust dyn Trait · Rust lifetime · Rust enum ve Option · E0277 ve E0308 tip hataları