Rust'ta trait object, bir trait'i uygulayan herhangi bir tipin değerini, somut tipi derleme anında bilinmeden tutmanızı sağlayan dyn Trait tipidir. Her zaman bir işaretçinin arkasında kullanılır: &dyn Ciz, Box<dyn Ciz>, Rc<dyn Ciz> gibi. Böylece Vec<Box<dyn Ciz>> içinde aynı anda bir düğme, bir metin kutusu ve kütüphaneyi yazarken hiç bilmediğiniz bir kullanıcı tipini tutabilirsiniz. Metot çağrısı çalışma anında bir sanal metot tablosu (vtable) üzerinden çözülür; buna dinamik dağıtım denir.

Bu yazıda dyn Trait sözdizimini, trait object ile generic arasındaki seçimi (statik ve dinamik dağıtım), bir trait'in trait object olabilmesi için gereken dyn uyumluluğu kurallarını, impl Trait ile çözülemeyen "koşula göre farklı tip döndür" sorununu, supertrait'e yükseltmeyi (upcasting) ve Any ile somut tipe geri dönüşü çalışan örneklerle anlatıyoruz. Trait tanımlama temelleri trait yazısında, Box<T> akıllı işaretçisi Box yazısında. Serinin tamamı için Rust öğrenme rehberi.

Sorun: farklı tipleri tek koleksiyonda tutmak

Sorun: farklı tipleri tek koleksiyonda tutmak

Bir Vec<T> yalnızca tek tipte öğe tutar. Tip kümesi derleme anında bellisi ve sabitse çözüm bir enum'dur: her varyant bir tipi sarar ve match ile işlenir. Ancak bir kütüphane yazarken kullanıcının kendi tiplerini eklemesine izin vermek istediğinizde enum yetmez, çünkü enum'un varyant listesi sizin crate'inizde kapalıdır. The Rust Book bunu bir grafik arayüz kütüphanesi örneğiyle anlatır: kütüphane ekrandaki bileşenlerin listesini dolaşıp her birinin draw metodunu çağırmak ister, ama kullanıcının ileride Resim ya da SecimKutusu gibi hangi tipleri ekleyeceğini bilemez.

Kalıtım olan dillerde bu bir temel sınıf ve onu geçersiz kılan alt sınıflarla çözülür. Rust'ta kalıtım yoktur; bunun yerine ortak davranışı bir trait olarak tanımlar ve vektörü trait object tutacak şekilde kurarsınız. Book şu ayrımı da yapar: Rust'ta struct ve enum'lara "nesne" denmez, çünkü veri ve davranış ayrı tutulur. Trait object'in amacı daha dardır: ortak davranış üzerinden soyutlama yapmak. Trait object'e veri ekleyemezsiniz.

rust
trait Ciz {
    fn ciz(&self) -> String;
}

struct Dugme {
    etiket: String,
}

struct SecimKutusu {
    secenekler: Vec<String>,
}

impl Ciz for Dugme {
    fn ciz(&self) -> String {
        format!("[ {} ]", self.etiket)
    }
}

impl Ciz for SecimKutusu {
    fn ciz(&self) -> String {
        format!("<{}>", self.secenekler.join(" | "))
    }
}

struct Ekran {
    bilesenler: Vec<Box<dyn Ciz>>,
}

impl Ekran {
    fn calistir(&self) {
        for b in self.bilesenler.iter() {
            println!("{}", b.ciz());
        }
    }
}

fn main() {
    let ekran = Ekran {
        bilesenler: vec![
            Box::new(SecimKutusu {
                secenekler: vec![String::from("Evet"), String::from("Hayır")],
            }),
            Box::new(Dugme { etiket: String::from("Tamam") }),
        ],
    };
    ekran.calistir();
}
// çıktı: <Evet | Hayır>
// çıktı: [ Tamam ]

calistir metodu hangi bileşenin düğme, hangisinin seçim kutusu olduğunu kontrol etmez; yalnızca ciz metodunu çağırır. Book bunu dinamik dillerdeki "duck typing" fikrine benzetir, ancak önemli bir farkla: vektöre Ciz uygulamayan bir değer (örneğin bir String) koymaya çalışırsanız kod çalışma anında değil, derleme anında E0277 hatasıyla reddedilir. Yani bir değerin metodu olup olmadığını çalışma anında kontrol etmeniz gerekmez.

Trait object bellekte nasıl durur?

Reference'a göre trait object, somut tipi gizlenmiş bir değerdir ve bu yüzden boyutu derleme anında bilinmeyen (dynamically sized) bir tiptir. Bu nedenle tek başına bir değişkende tutulamaz, bir işaretçinin arkasında yaşar. Bir trait object işaretçisi iki parçadan oluşur: somut değere giden işaretçi ve vtable denen sanal metot tablosuna giden işaretçi. Vtable, trait'in ve supertrait'lerinin her metodu için o tipin uygulamasına giden bir fonksiyon işaretçisi içerir. Metot çağrısında tablodan fonksiyon işaretçisi okunur ve dolaylı olarak çağrılır. Bu iki parçalı yapıyı boyutlara bakarak görebilirsiniz:

rust
use std::mem::size_of;

trait Ciz {
    fn ciz(&self) -> String;
}

struct Dugme;

impl Ciz for Dugme {
    fn ciz(&self) -> String {
        String::from("düğme")
    }
}

fn main() {
    println!("&Dugme        : {} bayt", size_of::<&Dugme>());
    println!("&dyn Ciz      : {} bayt", size_of::<&dyn Ciz>());
    println!("Box<dyn Ciz>  : {} bayt", size_of::<Box<dyn Ciz>>());

    let d = Dugme;
    let r: &dyn Ciz = &d; // &Dugme, &dyn Ciz tipine dönüşür
    println!("{}", r.ciz());
}
// çıktı: &Dugme        : 8 bayt
// çıktı: &dyn Ciz      : 16 bayt
// çıktı: Box<dyn Ciz>  : 16 bayt
// çıktı: düğme

Çıktı 64 bit bir Windows sisteminde alındı; 32 bit hedeflerde değerler yarıya iner, ancak "trait object işaretçisi normal işaretçinin iki katı" ilişkisi aynı kalır. let r: &dyn Ciz = &d; satırı bir unsized coercion örneğidir: Reference, T tipinin U trait'ini uyguladığı ve U dyn uyumlu olduğu durumda T değerinin dyn U tipine kendiliğinden dönüşebileceğini söyler. Box::new(Dugme) değerini Box<dyn Ciz> bekleyen bir yere vermek de aynı kurala dayanır. 2021 edition ve sonrasında dyn anahtar kelimesi zorunludur; eski kodlarda gördüğünüz Box<Ciz> yazımı artık kabul edilmez.

Statik dağıtım mı, dinamik dağıtım mı?

Statik dağıtım mı, dinamik dağıtım mı? Dağıtım, Kod üretimi, Karışık tip koleksiyonu, Inline ve optimizasyon

Generic ve trait bound kullandığınızda derleyici monomorphization yapar: her somut tip için ayrı kod üretir ve hangi metodun çağrılacağını derleme anında bilir. Buna statik dağıtım denir. Trait object kullandığınızda derleyici olası bütün tipleri bilemez, bu yüzden çağrıyı çalışma anında vtable üzerinden çözen kod üretir; buna dinamik dağıtım denir. Book bunun bir çalışma anı maliyeti olduğunu ve ayrıca derleyicinin metodu satır içine alamamasına (inline), dolayısıyla bazı optimizasyonların engellenmesine yol açtığını belirtir. Karşılığında esneklik kazanırsınız.

Pratik kural Book'tan gelir: koleksiyonlarınız her zaman tek tipten oluşacaksa (homojen), generic ve trait bound tercih edilir. struct Ekran<T: Ciz> { bilesenler: Vec<T> } tanımı bir ekranın ya yalnızca düğmelerden ya yalnızca seçim kutularından oluşmasına izin verir. Aynı koleksiyonda farklı tipleri karıştırmanız gerekiyorsa trait object gerekir.

ÖzellikGeneric (T: Trait, impl Trait)Trait object (dyn Trait)
DağıtımStatik, derleme anındaDinamik, çalışma anında vtable ile
Kod üretimiHer somut tip için ayrı kopyaTek kopya
Karışık tip koleksiyonuHayır, bir anda tek tipEvet, Vec<Box<dyn Trait>>
Inline ve optimizasyonMümkünDolaylı çağrı inline engeller
BoyutSomut tipin boyutuİşaretçi arkasında, işaretçi iki kat
Trait üzerinde koşulHer trait kullanılabilirTrait dyn uyumlu olmalı

Koşula göre farklı tip döndürmek: Box<dyn Trait>

Dönüş konumundaki impl Trait tek bir somut tipe çözülmek zorundadır; if dalına göre iki farklı tip döndürmek "incompatible types" hatası verir (bkz. trait yazısı). Trait object bu sınırı kaldırır, çünkü iki dal da aynı tipi, yani Box<dyn Trait> döndürür. Reference, closure döndürmek için impl Fn sözdiziminden önce tek yolun Box<dyn Fn(i32) -> i32> olduğunu, bunun heap ayırma ve dinamik dağıtım bedeli taşıdığını not eder. Tek tip döndürüyorsanız impl Trait, gerçekten farklı tipler döndürüyorsanız Box<dyn Trait> kullanın:

rust
trait Bildirim {
    fn gonder(&self, mesaj: &str) -> String;
}

struct Eposta {
    adres: String,
}

struct Sms {
    numara: String,
}

impl Bildirim for Eposta {
    fn gonder(&self, mesaj: &str) -> String {
        format!("e-posta {} : {}", self.adres, mesaj)
    }
}

impl Bildirim for Sms {
    fn gonder(&self, mesaj: &str) -> String {
        format!("sms {} : {}", self.numara, mesaj)
    }
}

fn kanal_sec(tercih: &str) -> Box<dyn Bildirim> {
    if tercih == "sms" {
        Box::new(Sms { numara: String::from("0555 000 00 00") })
    } else {
        Box::new(Eposta { adres: String::from("ornek@example.com") })
    }
}

// &dyn parametre: tek fonksiyon gövdesi, her tip için ayrı kopya yok
fn duyur(k: &dyn Bildirim) {
    println!("{}", k.gonder("Sipariş kargoda"));
}

fn main() {
    for t in ["sms", "eposta"] {
        let k = kanal_sec(t);
        duyur(k.as_ref());
    }
}
// çıktı: sms 0555 000 00 00 : Sipariş kargoda
// çıktı: e-posta ornek@example.com : Sipariş kargoda

Dyn uyumluluğu: her trait trait object olamaz

Bir trait'in dyn ile kullanılabilmesi için dyn compatible olması gerekir; bu kavramın eski adı "object safety" idi. Reference kuralları şöyle sıralar: bütün supertrait'ler de dyn uyumlu olmalı, Sized supertrait olmamalı, trait ilişkili sabit (associated const) içermemeli ve generic ilişkili tip taşımamalıdır. Trait object üzerinden çağrılabilecek (dispatchable) her metot da şu koşulları sağlamalıdır: tip parametresi olmamalı (lifetime parametresi serbesttir), Self yalnızca alıcı (receiver) tipinde geçmeli, alıcı &self, &mut self, Box<Self>, Rc<Self>, Arc<Self> veya bunların Pin sarmalı olmalı, async fn olmamalı ve dönüş tipinde impl Trait kullanmamalıdır.

Mantığı basittir: trait object'in arkasındaki somut tip çalışma anına kadar bilinmez. Self döndüren bir metodun dönüş boyutu, generic bir metodun hangi kopyasının vtable'a konacağı derleme anında belirlenemez. En sık karşılaşılan örnek, Self döndüren bir kopyalama metodudur:

rust
trait Sekil {
    fn alan(&self) -> f64;
    fn kopyala(&self) -> Self;
}

fn main() {
    let _v: Vec<Box<dyn Sekil>> = Vec::new();
}
// error[E0038]: the trait `Sekil` is not dyn compatible

Çözüm, sorunlu metodu trait object üzerinden çağrılamaz olarak işaretlemektir. Reference buna "açıkça dağıtılamaz" (explicitly non-dispatchable) der ve where Self: Sized bound'unu kullanır. Bu bound'u taşıyan metot somut tipler üzerinde yine çağrılabilir, yalnızca dyn Sekil üzerinde çağrılamaz; trait'in geri kalanı trait object olarak kullanılabilir hale gelir:

rust
trait Sekil {
    fn alan(&self) -> f64;
    fn kopyala(&self) -> Self
    where
        Self: Sized;
}

#[derive(Clone)]
struct Kare(f64);

struct Daire(f64);

impl Sekil for Kare {
    fn alan(&self) -> f64 {
        self.0 * self.0
    }
    fn kopyala(&self) -> Self {
        self.clone()
    }
}

impl Sekil for Daire {
    fn alan(&self) -> f64 {
        3.14 * self.0 * self.0
    }
    fn kopyala(&self) -> Self {
        Daire(self.0)
    }
}

fn main() {
    let sekiller: Vec<Box<dyn Sekil>> = vec![Box::new(Kare(2.0)), Box::new(Daire(1.0))];
    let toplam: f64 = sekiller.iter().map(|s| s.alan()).sum();
    println!("toplam alan = {toplam}");

    let k2 = Kare(3.0).kopyala(); // somut tipte hâlâ çağrılabilir
    println!("kopya alanı = {}", k2.alan());
}
// çıktı: toplam alan = 7.140000000000001
// çıktı: kopya alanı = 9

Toplamdaki 7.140000000000001 değeri bir hata değil, ondalıklı sayı aritmetiğinin doğal sonucudur; 4 ile 3.14 toplamı ikili tabanda tam temsil edilemez. Biçimlendirme sırasında basamak sayısını sınırlamak için {:.2} kullanabilirsiniz; ayrıntılar biçimlendirme yazısında. Standart kütüphanenin Clone trait'i de Sized gerektirdiği için dyn Clone yazılamaz; bu yüzden yukarıdaki gibi kendi trait'inize özel bir yöntem eklemek yaygın bir yoldur.

Ek trait'ler, lifetime ve supertrait'e yükseltme

Bir trait object tek bir "temel" trait ve istediğiniz kadar auto trait içerebilir: dyn Ciz + Send, dyn Ciz + Send + Sync veya dyn Ciz + 'static geçerli tiplerdir. Reference, ikinci bir normal trait eklemeye izin vermez; dyn Ciz + Display yazılamaz. Böyle bir ihtiyaçta ikisini supertrait olarak içeren yeni bir trait tanımlarsınız. Trait object içinde referans tutulabileceği için bir lifetime sınırı da taşır; çoğu durumda bu sınır makul bir varsayılanla çıkarılır. Thread'ler arasında trait object taşırken Send eklemek gerekir; thread yazısına bakın.

Reference'taki tip dönüşümü kurallarından biri de trait upcasting'dir: dyn Alt değeri, Ust trait'i Alt trait'inin supertrait'i ise dyn Ust tipine dönüştürülebilir. Ayrıca belgeye göre trait object, temel trait'in supertrait'lerini de uygular; dyn Alt üzerinde Ust metotlarını doğrudan çağırabilirsiniz:

rust
trait Adli {
    fn ad(&self) -> String;
}

trait Hayvan: Adli {
    fn ses(&self) -> String;
}

struct Kedi;

impl Adli for Kedi {
    fn ad(&self) -> String {
        String::from("Tekir")
    }
}

impl Hayvan for Kedi {
    fn ses(&self) -> String {
        String::from("miyav")
    }
}

fn adi_yaz(a: &dyn Adli) {
    println!("ad: {}", a.ad());
}

fn main() {
    let h: Box<dyn Hayvan> = Box::new(Kedi);
    println!("{} diyor ki {}", h.ad(), h.ses()); // supertrait metodu

    let a: &dyn Adli = &*h; // dyn Hayvan, dyn Adli tipine yükseltilir
    adi_yaz(a);
}
// çıktı: Tekir diyor ki miyav
// çıktı: ad: Tekir

Ters yön: Any ile somut tipe dönmek

Trait object somut tipi gizler; geri almanın genel bir yolu yoktur. Standart kütüphanedeki std::any::Any trait'i bu konuda sınırlı bir çalışma anı yansıması sunar. &dyn Any üzerinde is::<T>() ile tipi test edebilir, downcast_ref::<T>() ile Option<&T> alabilirsiniz; Box<dyn Any> ise downcast ile Box<T> değerine dönmeyi dener. Belgenin iki uyarısı önemlidir: Any yalnızca belirli bir somut tip olup olmadığını test eder, bir tipin bir trait'i uygulayıp uygulamadığını test edemez. Ayrıca Box<dyn Any> üzerinde doğrudan type_id() çağırırsanız içteki değerin değil kutunun kendi tip kimliğini alırsınız; önce &*kutu ile &dyn Any referansına geçin.

rust
use std::any::Any;

fn tanimla(deger: &dyn Any) {
    if let Some(s) = deger.downcast_ref::<String>() {
        println!("String, {} karakter: {}", s.chars().count(), s);
    } else if let Some(n) = deger.downcast_ref::<i32>() {
        println!("i32: {}", n);
    } else {
        println!("bilinmeyen tip");
    }
}

fn main() {
    tanimla(&String::from("Işık"));
    tanimla(&42_i32);
    tanimla(&3.5_f64);
}
// çıktı: String, 4 karakter: Işık
// çıktı: i32: 42
// çıktı: bilinmeyen tip

Kod tabanınızda sık sık downcast yazıyorsanız bu, tasarımın aslında kapalı bir tip kümesiyle çalıştığına işaret olabilir. Böyle durumlarda bir enum ve match daha açık ve derleyici tarafından kapsayıcılığı denetlenen bir çözüm sunar; bkz. match yazısı.

Sık yapılan hatalar

Sık yapılan hatalar: dyn Trait değerini işaretçisiz kullanmak., Generic metotlu veya Self döndüren trait'i trait object yapmak., Homojen koleksiyonda gereksiz yere dyn kullanmak...
  • dyn Trait değerini işaretçisiz kullanmak. let x: dyn Ciz = ... derlenmez, çünkü boyut bilinmez. &dyn, Box<dyn>, Rc<dyn> veya Arc<dyn> kullanın.
  • Generic metotlu veya Self döndüren trait'i trait object yapmak. E0038 alırsınız; ilgili metoda where Self: Sized ekleyin ya da metodu ayrı bir trait'e taşıyın.
  • Homojen koleksiyonda gereksiz yere dyn kullanmak. Tek tip varsa generic daha uygundur; dinamik dağıtımın bedelini boşuna ödemeyin.
  • İki normal trait'i + ile birleştirmek. dyn A + B yalnızca B bir auto trait (Send, Sync gibi) ise geçerlidir.
  • Eski sözdizimi. Box<Trait> yazımı 2021 edition'dan beri kabul edilmez; Box<dyn Trait> yazın.
  • Box<dyn Any> üzerinde type_id() çağırmak. Kutunun tipini verir; önce &*kutu yapın.

Sık Sorulan Sorular

dyn Trait mi, enum mu kullanmalıyım?

Tip kümesi sabit ve sizin kontrolünüzdeyse enum daha basittir ve match kapsayıcılık denetimi verir. Kütüphane kullanıcılarının yeni tip eklemesi gerekiyorsa trait object kullanın.

&dyn Trait ile Box<dyn Trait> arasındaki fark nedir?

&dyn Trait başkasına ait bir değeri ödünç alır; Box<dyn Trait> değerin sahibidir ve heap üzerinde tutar. Fonksiyon parametresi için genellikle &dyn, koleksiyonda saklamak veya fonksiyondan döndürmek için Box<dyn> uygundur.

Object safety ile dyn compatibility aynı şey mi?

Evet. Reference, bu kavramın eskiden "object safety" adıyla bilindiğini belirtir; std belgelerindeki "Dyn Compatibility" bölümleri her trait için bu durumu yazar.

Trait object'te generic metot çağırabilir miyim?

Hayır. Tip parametreli metotlar dağıtılabilir değildir. Metodu where Self: Sized ile işaretlerseniz trait yine dyn uyumlu olur, ama o metodu yalnızca somut tipler üzerinde çağırabilirsiniz.

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 generics · Rust Box · Rust Rc ve RefCell · E0277 tip hataları