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

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.
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:
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ı?

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.
| Özellik | Generic (T: Trait, impl Trait) | Trait object (dyn Trait) |
|---|---|---|
| Dağıtım | Statik, derleme anında | Dinamik, çalışma anında vtable ile |
| Kod üretimi | Her somut tip için ayrı kopya | Tek kopya |
| Karışık tip koleksiyonu | Hayır, bir anda tek tip | Evet, Vec<Box<dyn Trait>> |
| Inline ve optimizasyon | Mümkün | Dolaylı çağrı inline engeller |
| Boyut | Somut tipin boyutu | İşaretçi arkasında, işaretçi iki kat |
| Trait üzerinde koşul | Her trait kullanılabilir | Trait 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:
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ş kargodaDyn 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:
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:
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ı = 9Toplamdaki 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:
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: TekirTers 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.
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 tipKod 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

dyn Traitdeğerini işaretçisiz kullanmak.let x: dyn Ciz = ...derlenmez, çünkü boyut bilinmez.&dyn,Box<dyn>,Rc<dyn>veyaArc<dyn>kullanın.- Generic metotlu veya
Selfdöndüren trait'i trait object yapmak. E0038 alırsınız; ilgili metodawhere Self: Sizedekleyin ya da metodu ayrı bir trait'e taşıyın. - Homojen koleksiyonda gereksiz yere
dynkullanmak. Tek tip varsa generic daha uygundur; dinamik dağıtımın bedelini boşuna ödemeyin. - İki normal trait'i
+ile birleştirmek.dyn A + ByalnızcaBbir 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>üzerindetype_id()çağırmak. Kutunun tipini verir; önce&*kutuyapı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ı