Ortak Davranış Üzerinden Soyutlamak İçin Trait Nesnelerini Kullanmak
- bölümde vektörlerin bir sınırlamasından söz etmiştik: yalnızca tek bir türün öğelerini tutabilirler. Bunu aşmak için, tamsayı, ondalıklı sayı ve metin tutabilen varyantlara sahip bir enum tanımlamıştık. Bu çözüm, değiş tokuş edilebilir türlerin kümesi derleme zamanında sabitse gayet iyidir.
Ama bazen kütüphanemizi kullanan kişinin bu tür kümesini genişletebilmesini
isteriz. Bunu göstermek için, ekrandaki öğelerin listesini dolaşıp her biri
için ciz metodunu çağıran küçük bir grafik arayüz aracı hayal edelim. Bunun
için arayuz adında bir kütüphane crate’i yazacağız. Bu crate Dugme gibi
bazı türler sunabilir. Fakat kullanıcılar kendi türlerini de tanımlamak
isteyecektir; örneğin biri SecimKutusu, bir başkası farklı bir bileşen ekler.
Kütüphaneyi yazarken, ileride herkesin hangi türleri tanımlayacağını bilemeyiz.
Ama şunu biliyoruz: arayuz, farklı türlerden pek çok değeri izleyebilmeli ve
her biri üzerinde ciz metodunu çağırabilmeli. Bize önemli olan, somut türün
ne olduğu değil; bu metodun var olup olmadığıdır.
Kalıtımlı bir dilde bunu yapmak için Component gibi bir üst sınıf tanımlayıp
ona draw metodu verebilir, Dugme, Resim ve SecimKutusu gibi türleri bu
sınıftan türetebilirdik. Rust’ta kalıtım olmadığından, bunu başka bir yolla
kurmamız gerekir.
Ortak Davranış İçin Bir Trait Tanımlamak
Önce Ciz adında, tek metodu ciz olan bir trait tanımlayacağız. Sonra trait
nesnesi alan bir vektör tanımlayacağız. Trait nesnesi, belirli bir trait’i
uygulayan bir türün örneğine ve o tür için trait metodlarını çalışma zamanında
bulmaya yarayan tabloya birlikte işaret eder. Trait nesnesi oluşturmak için
referans veya Box<T> gibi bir işaretçi, ardından dyn anahtar sözcüğü ve
ilgili trait yazılır.
Trait nesnelerini jenerik veya somut tür yerine kullanabiliriz. Nerede trait nesnesi kullanıyorsak, Rust tür sistemi o bağlamda kullanılacak her değerin bu trait’i uyguladığını derleme zamanında garanti eder. Böylece bütün olası türleri önceden bilmemiz gerekmez.
18-3 numaralı liste Ciz trait’ini tanımlar.
pub trait Ciz {
fn ciz(&self);
}
Ciz trait’inin tanımıŞimdi Ekran adında, içinde bilesenler vektörü tutan bir struct
tanımlayalım. Bu vektörün türü Vec<Box<dyn Ciz>> olur.
pub trait Ciz {
fn ciz(&self);
}
pub struct Ekran {
pub bilesenler: Vec<Box<dyn Ciz>>,
}
bilesenler alanı Ciz trait’ini uygulayan trait nesneleri tutan Ekran struct’ıEkran üzerinde de her bileşen için ciz metodunu çağıran calistir metodunu
tanımlarız.
pub trait Ciz {
fn ciz(&self);
}
pub struct Ekran {
pub bilesenler: Vec<Box<dyn Ciz>>,
}
impl Ekran {
pub fn calistir(&self) {
for bilesen in self.bilesenler.iter() {
bilesen.ciz();
}
}
}
ciz metodunu çağıran calistir metoduBu yaklaşım, trait sınırı kullanan jenerik bir struct tanımlamaktan farklıdır. Jenerik kullanırsanız aynı anda yalnızca tek somut türle çalışırsınız. Örneğin 18-6 numaralı listedeki gibi yazsaydık:
pub trait Ciz {
fn ciz(&self);
}
pub struct Ekran<T: Ciz> {
pub bilesenler: Vec<T>,
}
impl<T> Ekran<T>
where
T: Ciz,
{
pub fn calistir(&self) {
for bilesen in self.bilesenler.iter() {
bilesen.ciz();
}
}
}
Ekran ve calistir için jenerik ve trait sınırı kullanan alternatif yaklaşımBu durumda bir Ekran örneğindeki bütün bileşenler ya Dugme olurdu ya da tek
başka bir tür olurdu. Yalnızca homojen koleksiyonlarınız varsa jenerikler daha
iyi seçimdir; çünkü derleyici somut türler için monomorfizasyon yapar.
Trait nesneleri kullandığımızdaysa tek bir Ekran, içinde hem Dugme hem de
SecimKutusu gibi farklı türleri aynı anda taşıyabilir.
Trait’i Uygulamak
Şimdi Ciz trait’ini uygulayan türler ekleyelim. Önce Dugme türünü
tanımlayacağız.
pub trait Ciz {
fn ciz(&self);
}
pub struct Ekran {
pub bilesenler: Vec<Box<dyn Ciz>>,
}
impl Ekran {
pub fn calistir(&self) {
for bilesen in self.bilesenler.iter() {
bilesen.ciz();
}
}
}
pub struct Dugme {
pub genislik: u32,
pub yukseklik: u32,
pub etiket: String,
}
impl Ciz for Dugme {
fn ciz(&self) {
// bir dugmeyi gercekten cizmek icin kod
}
}
Ciz trait’ini uygulayan Dugme struct’ıDugme içindeki genislik, yukseklik ve etiket alanları, başka
bileşenlerin alanlarından farklı olabilir. Örneğin SecimKutusu da genişlik ve
yükseklik taşırken ek olarak secenekler alanına sahip olabilir. Ekranda
çizilmesini istediğimiz her tür Ciz trait’ini uygular, ama ciz metodu içinde
her tür kendine özgü davranışı tanımlar.
Kütüphanemizi kullanan biri de 18-8 numaralı listedeki gibi kendi
SecimKutusu türünü yazıp Ciz trait’ini uygulayabilir.
use arayuz::Ciz;
struct SecimKutusu {
genislik: u32,
yukseklik: u32,
secenekler: Vec<String>,
}
impl Ciz for SecimKutusu {
fn ciz(&self) {
// bir secim kutusunu gercekten cizmek icin kod
}
}
fn main() {}
arayuz crate’ini kullanan başka bir crate’in SecimKutusu için Ciz trait’ini uygulamasıArtık kütüphaneyi kullanan kişi main içinde bir Ekran örneği oluşturabilir.
Bu ekrana hem SecimKutusu hem Dugme ekleyip calistir metodunu çağırdığında,
Ekran her bileşen üzerinde ciz metodunu çağıracaktır.
use arayuz::Ciz;
struct SecimKutusu {
genislik: u32,
yukseklik: u32,
secenekler: Vec<String>,
}
impl Ciz for SecimKutusu {
fn ciz(&self) {
// bir secim kutusunu gercekten cizmek icin kod
}
}
use arayuz::{Dugme, Ekran};
fn main() {
let ekran = Ekran {
bilesenler: vec![
Box::new(SecimKutusu {
genislik: 75,
yukseklik: 10,
secenekler: vec![
String::from("Evet"),
String::from("Belki"),
String::from("Hayir"),
],
}),
Box::new(Dugme {
genislik: 50,
yukseklik: 10,
etiket: String::from("Tamam"),
}),
],
};
ekran.calistir();
}
Kütüphaneyi yazarken birilerinin SecimKutusu ekleyeceğini bilmiyorduk. Ama
SecimKutusu, Ciz trait’ini uyguladığı için Ekran onu da problemsiz şekilde
çalıştırabiliyor.
Bu yaklaşım, dinamik dillerdeki ördek tiplemesi fikrine biraz benzer: ördek
gibi yürüyüp ördek gibi ses çıkarıyorsa ördektir. Ekran içindeki calistir
metodu, bileşenin somut türünün Dugme mi SecimKutusu mu olduğunu bilmek
zorunda değildir. Onun için önemli olan, ciz metodunun çağrılabilir olmasıdır.
Trait nesneleri ve Rust’ın tür sistemi sayesinde bunu güvenli biçimde yaparız. Belirli bir metodun var olup olmadığını çalışma zamanında ayrıca sınamamız gerekmez. Eğer değer ilgili trait’i uygulamıyorsa, kod zaten derlenmez.
Örneğin 18-10 numaralı listedeki gibi Ekran içine String koymaya
kalkarsak, String Ciz trait’ini uygulamadığı için derleyici hata verir:
use arayuz::Ekran;
fn main() {
let ekran = Ekran {
bilesenler: vec![Box::new(String::from("Merhaba"))],
};
ekran.calistir();
}
error[E0277]: the trait bound `String: Ciz` is not satisfied
--> src/main.rs:5:21
|
5 | bilesenler: vec![Box::new(String::from("Merhaba"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Ciz` is not implemented for `String`
|
= help: the following other types implement trait `Ciz`:
Dugme
= note: required for the cast from `Box<String>` to `Box<dyn Ciz>`
Bu hata bize ya yanlış tür verdiğimizi ya da gerçekten istiyorsak String için
Ciz trait’ini uygulamamız gerektiğini söyler.
Dinamik Dağıtım Yapmak
- bölümde, jenerik kullanan kodların performansını anlatırken monomorfizasyon sürecinden söz etmiştik. Derleyici, jenerik fonksiyonlar ve metotlar için kullandığınız her somut tür adına somut sürümler üretir. Bu şekilde oluşan kod statik dağıtım (static dispatch) yapar; yani hangi metodun çağrılacağı derleme zamanında bellidir.
Buna karşılık dinamik dağıtım (dynamic dispatch) durumunda derleyici, hangi metodun çağrılacağını derleme zamanında bilemez. Gerekli kodu üretir; ama hangi metodun seçileceği çalışma zamanında belli olur.
Trait nesneleri kullandığımızda Rust dinamik dağıtım yapmak zorundadır. Çünkü o kodu hangi türlerin kullanacağını baştan bilemez. Bu yüzden trait nesnesinin içindeki işaretçiler ve tablolar yardımıyla çalışma zamanında doğru metod bulunur.
Bunun bir çalışma zamanı maliyeti vardır. Statik dağıtımda mümkün olan bazı iyileştirmeler dinamik dağıtımda yapılamaz. Yani burada biraz performans karşılığında ek esneklik kazanırız. 18-5’te yazdığımız kod ve 18-9’da sağlayabildiğimiz genişleyebilirlik, bu ödünleşimin ne kazandırdığını gösterir.