Rust'ta #[derive(...)] özniteliği, bir struct, enum veya union için belirli trait'lerin uygulamasını derleyiciye otomatik ürettirir. #[derive(Debug, Clone, PartialEq)] yazdığınızda {:?} ile yazdırma, .clone() ile kopyalama ve == ile karşılaştırma elle tek satır kod yazmadan kullanılabilir hale gelir. Standart kütüphanede türetilebilen dokuz trait vardır: Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash ve Default.

Bu yazıda her birinin hangi operatörü veya metodu açtığını, türetilen kodun tam olarak ne yaptığını (alan sırası neden önemli, enum varyantları nasıl sıralanır), hangi koşullarda türetmenin reddedildiğini ve elle uygulamanın ne zaman daha doğru olduğunu çalışan örneklerle göreceksiniz. Trait kavramının kendisi için trait yazısına, kullanıcıya yönelik çıktı veren ve türetilemeyen Display için Display ve Debug yazısına bakın. Serinin tamamı Rust öğrenme rehberi sayfasında.

derive tam olarak ne üretir?

derive tam olarak ne üretir?

Reference'a göre derive özniteliği bir veya daha fazla derive makrosunu çağırır ve makro genişletme sırasında her biri sıfır veya daha fazla öğe üretir. Yalnızca struct, enum ve union üzerine yazılabilir; aynı öğe üzerinde birden çok derive satırı kullanılabilir ve hepsindeki makrolar çalışır. Yerleşik türetmeler ürettikleri impl bloklarına #[automatically_derived] işaretini koyar; bunun doğrudan bir etkisi yoktur, araçlar ve lint'ler otomatik üretilmiş kodu tanımak için kullanır.

Generic tiplerde önemli bir davranış vardır: türetilen uygulama, her tip parametresine ilgili trait bound'unu ekler. #[derive(PartialEq, Clone)] struct Kutu<T> { a: i32, b: T } için üretilen kod kavramsal olarak impl<T: PartialEq> PartialEq for Kutu<T> ve impl<T: Clone> Clone for Kutu<T> biçimindedir. Yani Kutu<T> yalnızca T klonlanabiliyorsa klonlanabilir. Clone belgesi bunun gereksiz olduğu bir durumu da gösterir: bir fonksiyon işaretçisi tutan generic struct'ta, işaretçinin kendisi her zaman kopyalanabilir olsa da türetme T: Clone şartı koyar. Böyle durumlarda elle uygulama yazılır.

Bu dokuz trait dışındaki standart trait'ler türetilemez, çünkü mantıklı bir varsayılan davranışları yoktur. Book'un örneği Display'dir: bir değeri son kullanıcıya nasıl göstereceğinize, hangi alanların görünmesi gerektiğine derleyici karar veremez. Kütüphaneler ise procedural macro ile kendi trait'leri için derive yazabildiği için kullanabileceğiniz derive listesi açık uçludur.

Debug, Clone ve PartialEq: en sık kullanılan üçlü

Debug programcıya yönelik çıktı içindir ve {:?} ya da girintili biçim olan {:#?} ile kullanılır. Türetilmiş hali struct adını, süslü parantezi ve her alanın adıyla Debug değerini; enum'larda varyant adını ve varsa parantez içindeki alanları yazar. assert_eq! makrosu başarısız olduğunda iki değeri yazdırdığı için hem Debug hem PartialEq ister. PartialEq türetildiğinde struct'ın iki örneği ancak bütün alanları eşitse eşittir; enum'larda her varyant yalnızca kendisine eşittir. Clone ise her alan için clone çağırarak derin kopya üretir:

rust
#[derive(Debug, Clone, PartialEq)]
struct Urun {
    ad: String,
    fiyat: f64,
    etiketler: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq)]
enum Durum {
    Stokta,
    Tukendi,
    Siparis(u32),
}

fn main() {
    let a = Urun {
        ad: String::from("Çaydanlık"),
        fiyat: 349.9,
        etiketler: vec![String::from("mutfak")],
    };
    let mut b = a.clone();
    println!("{}", a == b);
    b.etiketler.push(String::from("indirim"));
    println!("{}", a == b);
    println!("{:?}", a);
    println!("{:#?}", Durum::Siparis(3));

    let d = Durum::Stokta;
    let e = d; // Copy: d hâlâ kullanılabilir
    println!("{:?} {:?} {}", d, e, d == Durum::Tukendi);
}
// çıktı: true
// çıktı: false
// çıktı: Urun { ad: "Çaydanlık", fiyat: 349.9, etiketler: ["mutfak"] }
// çıktı: Siparis(
// çıktı:     3,
// çıktı: )
// çıktı: Stokta Stokta false

b kendi Vec kopyasına sahip olduğu için ona eklenen etiket a değerini etkilemez; bu, Clone türetmesinin her alanı ayrı ayrı klonlamasının sonucudur. Clone belgesi burada bir uyarı yapar: alanlardan biri Arc<Mutex<T>> gibi paylaşımlı bir akıllı işaretçiyse, klonlanan struct asıl değerle aynı değiştirilebilir durumu paylaşır, çünkü Arc klonu yalnızca referans sayacını artırır. Ayrıca Debug belgesine göre türetilmiş Debug çıktısının biçimi kararlı değildir ve ileriki Rust sürümlerinde değişebilir; testlerinizde bu çıktıyı birebir metin olarak karşılaştırmaktan kaçının.

Copy: yalnızca bitleri kopyalanabilen tipler

Copy: yalnızca bitleri kopyalanabilen tipler

Copy, bir değerin yalnızca yığındaki bitleri kopyalanarak çoğaltılabileceğini söyler. Metodu yoktur; Book bunun bilinçli bir tercih olduğunu, böylece kimsenin kopyalama sırasında keyfi kod çalıştıramayacağını ve kopyanın her zaman hızlı olacağını açıklar. Copy uygulayan tipin Clone da uygulaması gerekir; bu yüzden ikisi hep birlikte türetilir. Bir tipin Copy olabilmesi için bütün parçalarının Copy olması gerekir. String veya Vec gibi heap belleği yöneten bir alan varsa türetme reddedilir:

rust
#[derive(Clone, Copy)]
struct Kullanici {
    id: u32,
    ad: String,
}

fn main() {}
// error[E0204]: the trait `Copy` cannot be implemented for this type

Bu durumda iki seçeneğiniz var: Copy türetmesini kaldırıp gerektiğinde açıkça .clone() çağırmak ya da String alanını tipten çıkarmak. Book'un hatırlattığı gibi Copy ile yapılabilen her şey Clone ile de yapılabilir; fark, Copy tiplerde atamanın ve fonksiyona geçirmenin değeri taşımak (move) yerine kopyalamasıdır. Taşıma ve kopyalama farkı ownership yazısında anlatılıyor.

Eq ve Hash: HashMap anahtarı olmak

Eq trait'inin metodu yoktur; tek amacı "bu tipin her değeri kendisine eşittir" garantisini vermektir. Yalnızca PartialEq uygulayan tiplere uygulanabilir, ama her PartialEq tipi Eq olamaz. Klasik örnek ondalıklı sayılardır: iki NaN değeri birbirine eşit sayılmaz. Bu yüzden f32 veya f64 alanı içeren bir struct Eq türetemez. Hash, keyfi boyuttaki bir değeri sabit boyutlu bir değere eşleyen hash metodunu üretir; türetilmiş hali her alanın hash sonucunu birleştirir.

HashMap<K, V> anahtarlarının Eq ve Hash uygulaması gerekir. Hash belgesi kritik bir kuralı vurgular: iki anahtar eşitse hash değerleri de eşit olmalıdır. Bunu elle sağlamak hataya açıktır; #[derive(PartialEq, Eq, Hash)] üçünü birlikte türettiğinizde kural kendiliğinden korunur. Elle bir PartialEq yazıp Hash türetmek ise iki uygulamanın birbiriyle çelişmesine yol açabilir; belge bu tür ihlalleri mantık hatası olarak tanımlar.

rust
use std::collections::{HashMap, HashSet};

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum Sehir {
    Ankara,
    Izmir,
}

#[derive(Debug, PartialEq, Eq, Hash)]
struct Adres {
    sehir: Sehir,
    posta_kodu: u32,
}

fn main() {
    let mut stok = HashMap::new();
    stok.insert(Sehir::Ankara, 12);
    stok.insert(Sehir::Izmir, 7);
    println!("{:?}", stok.get(&Sehir::Izmir));

    let mut adresler = HashSet::new();
    adresler.insert(Adres { sehir: Sehir::Ankara, posta_kodu: 6420 });
    let yeni_mi = adresler.insert(Adres { sehir: Sehir::Ankara, posta_kodu: 6420 });
    println!("{} {}", yeni_mi, adresler.len());
}
// çıktı: Some(7)
// çıktı: false 1

İkinci insert çağrısı false döndürür, çünkü aynı alan değerlerine sahip adres kümede zaten vardır: türetilmiş PartialEq alanları tek tek karşılaştırır, türetilmiş Hash aynı alanlardan aynı hash değerini üretir. Haritadaki stok sayıları uydurma örnek değerlerdir. HashMap kullanımının ayrıntıları HashMap yazısında.

PartialOrd ve Ord: sıralama, alan sırası ve varyant sırası

PartialOrd türetmek <, >, <= ve >= operatörlerini açar ve Option<Ordering> döndüren partial_cmp metodunu üretir. Sonuç, sıralama tanımsız olduğunda None olur; ondalıklı bir sayıyı NaN ile karşılaştırmak bunun örneğidir. Ord ise her iki değer arasında geçerli bir sıra olduğunu garanti eder ve doğrudan Ordering döndüren cmp metodunu sağlar. Ord için PartialOrd ve Eq (dolayısıyla PartialEq) gerekir. sort(), BTreeMap anahtarları ve BTreeSet öğeleri Ord ister.

Türetilmiş sıralamanın kuralı belgelerde açıktır: struct'larda alanlar, tanımdaki yukarıdan aşağıya sıralarıyla sözlük sırası (lexicographic) mantığında karşılaştırılır. İlk alan farklıysa sonuç odur; eşitse ikinci alana geçilir. Enum'larda önce discriminant karşılaştırılır; varsayılan olarak üstte tanımlanan varyant daha küçüktür. Discriminant'ları elle verirseniz bu sıra değişir. Aynı varyantın iki değeri ise alanlarına göre karşılaştırılır. Bu, alan sırasını değiştirmenin programın davranışını değiştirebileceği anlamına gelir:

rust
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Oncelik {
    Dusuk,
    Orta,
    Yuksek,
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Gorev {
    oncelik: Oncelik, // önce bu alan karşılaştırılır
    ad: String,       // öncelik eşitse bu alan
}

fn main() {
    println!("{}", Oncelik::Dusuk < Oncelik::Yuksek);

    let mut liste = vec![
        Gorev { oncelik: Oncelik::Orta, ad: String::from("rapor") },
        Gorev { oncelik: Oncelik::Yuksek, ad: String::from("yedek") },
        Gorev { oncelik: Oncelik::Orta, ad: String::from("e-posta") },
    ];
    liste.sort();
    for g in &liste {
        println!("{:?} {}", g.oncelik, g.ad);
    }

    println!("{:?}", 1.0_f64.partial_cmp(&f64::NAN));
}
// çıktı: true
// çıktı: Orta e-posta
// çıktı: Orta rapor
// çıktı: Yuksek yedek
// çıktı: None

İki Orta görev, ikinci alan olan ad üzerinden sözlük sırasına konur. String karşılaştırması bayt değerlerine göre yapılır; Ord belgesi bunun her dilin alfabetik sırasıyla aynı olmadığını açıkça belirtir. Türkçe büyük-küçük harf ve "ç, ğ, ı, ö, ş, ü" içeren adlarda beklediğiniz sıralamayı almak için sort_by ile kendi karşılaştırma fonksiyonunuzu yazmanız gerekir. Ord belgesi bir uyarı daha yapar: Ord ve PartialOrd birbiriyle tutarlı olmalıdır. Birini türetip diğerini elle yazmak çelişki yaratmanın en kolay yoludur; türetecekseniz dördünü (PartialEq, Eq, PartialOrd, Ord) birlikte türetin, elle yazacaksanız dördünü de Ord üzerine kurun.

Default: varsayılan değer ve struct güncelleme sözdizimi

Default türetmek, her alan için o alanın tipinin default() değerini kullanan bir default fonksiyonu üretir: sayılar için 0, bool için false, String ve Vec için boş değer, Option için None. En yaygın kullanımı struct güncelleme sözdizimidir: birkaç alanı kendiniz verip geri kalanını ..Default::default() ile doldurursunuz. Enum'larda hangi varyantın varsayılan olacağını #[default] özniteliğiyle seçersiniz; belgeye göre bu öznitelik yalnızca veri taşımayan (unit) varyantlara konabilir ve Rust 1.62.0 ile kararlı hale gelmiştir. Option::unwrap_or_default gibi metotlar da Default ister.

rust
#[derive(Debug, Default)]
enum Tema {
    #[default]
    Acik,
    Koyu,
}

#[derive(Debug, Default)]
struct Ayarlar {
    dil: String,
    yazi_boyutu: u8,
    bildirim: bool,
    tema: Tema,
}

fn main() {
    let varsayilan = Ayarlar::default();
    println!("{:?}", varsayilan);

    let ozel = Ayarlar {
        dil: String::from("tr"),
        tema: Tema::Koyu,
        ..Default::default()
    };
    println!("{:?}", ozel);

    let bos: Option<u8> = None;
    println!("{}", bos.unwrap_or_default());
}
// çıktı: Ayarlar { dil: "", yazi_boyutu: 0, bildirim: false, tema: Acik }
// çıktı: Ayarlar { dil: "tr", yazi_boyutu: 0, bildirim: false, tema: Koyu }
// çıktı: 0

Varsayılan yazı boyutunun 0 olması gerçek bir uygulama için anlamsız olabilir. Alanlardan biri için mantıklı varsayılan tipin kendi varsayılanından farklıysa Default türetmek yerine impl Default for Ayarlar bloğunu elle yazın ve default() içinde istediğiniz değerleri döndürün.

Türetilemeyen durumlar ve tipik hata mesajları

Türetilemeyen durumlar ve tipik hata mesajları: Debug, Clone, Copy, PartialEq

Türetme, tipin bütün parçaları ilgili trait'i uyguluyorsa çalışır. Bir alan bu koşulu sağlamıyorsa hata türetme satırını gösterir. En sık görülen örnek ondalıklı alan içeren struct'a Eq türetmektir:

rust
#[derive(PartialEq, Eq)]
struct Olcum {
    deger: f64,
}

fn main() {}
// error[E0277]: the trait bound `f64: Eq` is not satisfied
TraitAçtığı özellikTüretme koşuluKim ister
Debug{:?} ve {:#?}Tüm alanlar Debugassert_eq!, dbg!
Clone.clone()Tüm alanlar Cloneto_vec, açık kopyalar
CopyAtamada kopyalamaTüm alanlar Copy, ayrıca CloneTaşımasız kullanım
PartialEq== ve !=Tüm alanlar PartialEqassert_eq!
EqTam eşitlik garantisiPartialEq ve tüm alanlar EqHashMap anahtarı
PartialOrd<, >, <=, >=PartialEq ve tüm alanlar PartialOrdKarşılaştırma
Ordcmp, toplam sıraPartialOrd, Eq ve tüm alanlar Ordsort, BTreeMap, BTreeSet
Hashhash metoduTüm alanlar HashHashMap, HashSet
DefaultTip::default()Tüm alanlar Default; enum için #[default]..Default::default()

Sık yapılan hatalar

  • Ondalıklı alanlı tipe Eq, Ord veya Hash türetmek. f64 bu trait'leri uygulamaz; ya alanı tam sayıya çevirin ya da yalnızca PartialEq ve PartialOrd türetin.
  • Alan sırasını önemsememek. Türetilmiş PartialOrd ve Ord alanları tanım sırasıyla karşılaştırır. Alanları yeniden düzenlemek sıralama sonucunu değiştirebilir.
  • Türetme ile elle uygulamayı karıştırmak. PartialEq elle, Hash türetilerek yazılırsa eşit değerlerin farklı hash üretmesi mümkün olur; bu, HashMap davranışını bozan bir mantık hatasıdır.
  • Copy türetip Clone unutmak. Copy için Clone şarttır; ikisini birlikte yazın.
  • Veri taşıyan varyanta #[default] koymak. Yalnızca unit varyantlarda geçerlidir.
  • Türetilmiş Debug çıktısına bağımlı olmak. Biçim kararlı değildir; kullanıcıya gösterilecek veya ayrıştırılacak metin için Display uygulayın.

Sık Sorulan Sorular

Display neden türetilemiyor?

Display kullanıcıya yönelik çıktıdır ve hangi alanların, hangi biçimde gösterileceği tipin anlamına bağlıdır. Derleyici bu bilgiye sahip olmadığı için mantıklı bir varsayılan üretemez; Display'i elle uygularsınız.

Copy türetmek her zaman iyi bir fikir mi?

Küçük, yalnızca sayı ve bool gibi alanlardan oluşan tiplerde kullanışlıdır. Book, Copy gerektiren durumların nadir olduğunu, sağladığı şeyin kodda clone çağrısı yazma ihtiyacını kaldırmak olduğunu belirtir.

Kendi trait'im için derive yazabilir miyim?

Evet, ama bunun için procedural macro gerekir ve ayrı bir proc-macro crate'i oluşturmanız gerekir. Kısa özeti makro yazısında. Hazır derive sunan üçüncü taraf crate'ler için https://docs.rs adresine bakabilirsiniz.

Generic struct'ta derive(Clone) neden T: Clone istiyor?

Yerleşik derive makroları her tip parametresine ilgili bound'u ekler. Struct'ınız T değerini doğrudan saklamıyorsa (örneğin yalnızca bir fonksiyon işaretçisi tutuyorsa) bu bound gereksiz olabilir; o zaman trait'i elle uygulayın.

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 Display ve Debug · Rust HashMap · Rust ownership · Rust struct ve metotlar