Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ortak Davranış Üzerinden Soyutlamak İçin Trait Nesnelerini Kullanmak

  1. 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.

Filename: src/lib.rs
pub trait Ciz {
    fn ciz(&self);
}
Listing 18-3: 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.

Filename: src/lib.rs
pub trait Ciz {
    fn ciz(&self);
}

pub struct Ekran {
    pub bilesenler: Vec<Box<dyn Ciz>>,
}
Listing 18-4: 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.

Filename: src/lib.rs
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();
        }
    }
}
Listing 18-5: Her bileşen için ciz metodunu çağıran calistir metodu

Bu 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:

Filename: src/lib.rs
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();
        }
    }
}
Listing 18-6: Ekran ve calistir için jenerik ve trait sınırı kullanan alternatif yaklaşım

Bu 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.

Filename: src/lib.rs
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
    }
}
Listing 18-7: 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.

Filename: src/main.rs
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() {}
Listing 18-8: 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.

Filename: src/main.rs
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();
}
Listing 18-9: Aynı trait’i uygulayan farklı türlerdeki değerleri trait nesneleriyle saklamak

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:

Filename: src/main.rs
use arayuz::Ekran;

fn main() {
    let ekran = Ekran {
        bilesenler: vec![Box::new(String::from("Merhaba"))],
    };

    ekran.calistir();
}
Listing 18-10: Trait nesnesinin trait’ini uygulamayan bir tür kullanmaya çalışmak
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

  1. 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.