Farklı Türlerdeki Değerlere İzin Veren Tanım Nesnelerini Kullanma

Bölüm 8'de, vektörlerin bir sınırlamasının yalnızca tek bir türden elemanları saklayabilmeleri olduğundan bahsetmiştik. Liste 8-9'da tam sayıları, kayan değerleri ve metni tutmak için varyantları olan bir SpreadsheetCell enum'u tanımladığımız geçici bir çözüm oluşturduk. Bu, her hücrede farklı veri türlerini saklayabileceğimiz ve yine de bir hücre satırını temsil eden bir vektöre sahip olabileceğimiz anlamına geliyordu. Bu, değiştirilebilir öğelerimiz kodumuz derlendiğinde bildiğimiz sabit bir tür kümesi olduğunda mükemmel bir çözümdür.

Ancak, bazen kütüphane kullanıcımızın belirli bir durumda geçerli olan türler kümesini genişletebilmesini isteriz. Bunu nasıl başarabileceğimizi göstermek için, GUI araçları için yaygın bir teknik olan, bir öğe listesini yineleyerek her birini ekrana çizmek için bir draw yöntemini çağıran örnek bir grafik kullanıcı arayüzü (GUI) aracı oluşturacağız. GUI kütüphanesinin yapısını içeren gui adında bir kütüphane kasası oluşturacağız. Bu kasa insanların kullanması için Button veya TextField gibi bazı türler içerebilir. Buna ek olarak, gui kullanıcıları çizilebilecek kendi türlerini oluşturmak isteyeceklerdir: örneğin, bir programcı bir Image ekleyebilir ve bir diğeri bir SelectBox ekleyebilir.

Bu örnek için tam teşekküllü bir GUI kütüphanesi uygulamayacağız ancak parçaların birbirine nasıl uyacağını göstereceğiz. Kütüphaneyi yazarken, diğer programcıların oluşturmak isteyebileceği tüm türleri bilemeyiz ve tanımlayamayız. Ancak gui'nin farklı tiplerdeki birçok değeri takip etmesi gerektiğini ve bu farklı tipteki değerlerin her biri için bir draw metodu çağırması gerektiğini biliyoruz. draw metodunu çağırdığımızda tam olarak ne olacağını bilmesine gerek yoktur, sadece değerin çağırmamız için bu metoda sahip olması yeterlidir.

Bunu kalıtımın olduğu bir dilde yapmak için, üzerinde draw adında bir yöntem bulunan Component adında bir sınıf tanımlayabiliriz. Button, Image ve SelectBox gibi diğer sınıflar Component'ten miras alır ve böylece draw yöntemini miras alır. Her biri kendi özel davranışlarını tanımlamak için draw yöntemini geçersiz kılabilir, ancak çerçeve tüm türlere Component örneğiymiş gibi davranabilir ve draw yöntemini çağırabilir. Ancak Rust'ta kalıtım olmadığı için, kullanıcıların yeni türlerle genişletmesine izin vermek üzere gui kütüphanesini yapılandırmak için başka bir yola ihtiyacımız var.

Ortak Davranış için Bir Özellik Tanımlama

gui'nin sahip olmasını istediğimiz davranışı uygulamak için, draw adında bir metoda sahip olacak Draw adında bir trait tanımlayacağız. Daha sonra bir trait nesnesi alan bir vektör tanımlayabiliriz. Bir trait nesnesi, hem belirttiğimiz trait'i uygulayan bir türün örneğine hem de çalışma zamanında bu türdeki trait yöntemlerini aramak için kullanılan bir tabloya işaret eder. Bir & referansı veya Box<T> akıllı işaretçisi gibi bir tür işaretçi, ardından dyn anahtar sözcüğü ve ardından ilgili özelliği belirterek bir trait nesnesi oluştururuz. (Özellik nesnelerinin neden bir işaretçi kullanması gerektiğinden Bölüm 19'da “Dinamik Olarak Boyutlandırılmış Türler ve Sized Tanımı” bölümünde bahsedeceğiz). Tanım nesnelerini yaygın veya somut bir tip yerine kullanabiliriz. Bir trait nesnesi kullandığımız her yerde, Rust'ın tür sistemi derleme zamanında bu bağlamda kullanılan herhangi bir değerin trait nesnesinin özelliğini uygulamasını sağlayacaktır. Sonuç olarak, derleme zamanında tüm olası türleri bilmemize gerek yoktur.

Rust'ta struct ve enum'ları diğer dillerin nesnelerinden ayırmak için “nesne” olarak adlandırmaktan kaçındığımızdan bahsetmiştik. Bir struct veya enum'da, struct alanlarındaki veri ve impl bloklarındaki davranış birbirinden ayrılırken, diğer dillerde veri ve davranış tek bir kavramda birleştirilir ve genellikle bir nesne olarak etiketlenir. Bununla birlikte, tanım nesneleri, veri ve davranışı birleştirmeleri açısından diğer dillerdeki nesnelere daha çok benzemektedir. Ancak trait nesneleri, bir trait nesnesine veri ekleyemediğimiz için geleneksel nesnelerden farklıdır. Özellik nesneleri diğer dillerdeki nesneler kadar genel olarak kullanışlı değildir: özel amaçları ortak davranışlar arasında soyutlamaya izin vermektir.

Liste 17-3, draw adında bir yöntemle Draw adında bir özelliğin nasıl tanımlanacağını gösterir:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

Liste 17-3: Draw tanımının tanımı

This syntax should look familiar from our discussions on how to define traits in Chapter 10. Next comes some new syntax: Listing 17-4 defines a struct named Screen that holds a vector named components. This vector is of type Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a Box that implements the Draw trait.

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

Liste 17-4: Draw tanımını uygulayan tanım nesnelerinden oluşan bir vektörü tutan components alanına sahip Screen yapısının tanımı

Screen yapısında, Liste 17-5'te gösterildiği gibi, components'in her birinde draw yöntemini çağıracak run adında bir yöntem tanımlayacağız:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

Liste 17-5: Her bileşende draw yöntemini çağıran Screen üzerinde bir run yöntemi

Bu, özellik sınırları olan genel bir tür parametresi kullanan bir struct tanımlamaktan farklı çalışır. Bir yaygın tür parametresi bir seferde yalnızca bir somut tiple değiştirilebilirken, tanım nesneleri çalışma zamanında birden fazla somut tipin özellik nesnesinin yerini doldurmasına izin verir. Örneğin, Screen yapısını Liste 17-6'daki gibi bir yaygın tip ve bir tanım bağı kullanarak tanımlayabilirdik:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen<T: Draw> {
    pub components: Vec<T>,
}

impl<T> Screen<T>
where
    T: Draw,
{
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

Liste 17-6: Yaygınlar ve tanım sınırlarını kullanarak Screen yapısının ve run yönteminin alternatif bir uygulaması

Bu, bizi, tümü Button türünden veya tümü TextField türünden bileşenlerin bir listesini içeren bir Screen örneğiyle sınırlar. Yalnızca homojen koleksiyonlarınız olacaksa, yaygınlar ve özellik sınırlarının kullanılması tercih edilir, çünkü somut türleri kullanmak için tanımlar derleme zamanında monomorfize edilecektir.

Öte yandan, özellik nesnelerini kullanan yöntemle, bir Screen örneği, Box<Button> ve Box<TextField> içeren bir Vec<T> içerebilir. Bunun nasıl çalıştığına bakalım ve ardından çalışma zamanı performans sonuçları hakkında konuşacağız.

Tanımı Uygulamak

Şimdi Draw özelliğini uygulayan bazı türleri ekleyeceğiz. Button türünü sağlayacağız. Yine, aslında bir GUI kütüphanesini uygulamak bu kitabın kapsamı dışındadır, bu nedenle draw yönteminin gövdesinde herhangi bir çalışan süreklemesi olmayacaktır. Uygulamanın nasıl görünebileceğini hayal etmek için, bir Button yapısında Liste 17-7'de gösterildiği gibi width, height ve label alanları olabilir:

Dosya adı: src/lib.rs

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

impl Screen {
    pub fn run(&self) {
        for component in self.components.iter() {
            component.draw();
        }
    }
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // code to actually draw a button
    }
}

Liste 17-7: Draw tanımını uygulayan bir Button yapısı

Button'daki width, height ve label alanları diğer bileşenlerdeki alanlardan farklı olacaktır; örneğin, bir TextField türü aynı alanlara ve bir yer tutucu alana sahip olabilir. Ekranda çizmek istediğimiz türlerin her biri Draw tanımını uygular, ancak burada Button'da olduğu gibi (belirtildiği gibi gerçek GUI kodu olmadan) söz konusu türün nasıl çizileceğini tanımlamak için draw yönteminde farklı kod kullanır. Örneğin Button tipi, kullanıcı düğmeye tıkladığında ne olacağıyla ilgili metotları içeren ek bir impl bloğuna sahip olabilir. Bu tür yöntemler TextField gibi türler için geçerli olmayacaktır.

Kütüphanemizi kullanan biri width, height ve options alanları olan bir SelectBox yapısını uygulamaya karar verirse, Liste 17-8'de gösterildiği gibi SelectBox türüne Draw tanımını da uygular:

Dosya adı: src/main.rs

use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

fn main() {}

Liste 17-8: Bir SelectBox yapısı üzerinde gui kullanan ve Draw tanımını uygulayan başka bir kasa

Kütüphanemizin kullanıcısı artık bir Screen örneği oluşturmak için main fonksiyonunu yazabilir. Screen örneğine bir SelectBox ve bir Button ekleyebilir ve her birini bir Box<T> içine koyarak bir trait nesnesi haline getirebilirler. Daha sonra Screen örneğinde run metodunu çağırabilirler, bu da her bir bileşen üzerinde draw metodunu çağıracaktır. Liste 17-9 bu uygulamayı göstermektedir:

Dosya adı: src/main.rs

use gui::Draw;

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // code to actually draw a select box
    }
}

use gui::{Button, Screen};

fn main() {
    let screen = Screen {
        components: vec![
            Box::new(SelectBox {
                width: 75,
                height: 10,
                options: vec![
                    String::from("Yes"),
                    String::from("Maybe"),
                    String::from("No"),
                ],
            }),
            Box::new(Button {
                width: 50,
                height: 10,
                label: String::from("OK"),
            }),
        ],
    };

    screen.run();
}

Liste 17-9: Aynı özelliği uygulayan farklı türlerin değerlerini depolamak için tanım nesnelerini kullanma

Kütüphaneyi yazarken, birinin SelectBox türünü ekleyebileceğini bilmiyorduk, ancak Screen uygulamamız yeni tür üzerinde çalışabiliyor ve onu çizebiliyordu çünkü SelectBox Draw özelliğini uyguluyor, yani draw yöntemini uyguluyor.

Bu kavram - bir değerin somut türünden ziyade yalnızca değerin yanıt verdiği mesajlarla ilgilenmek - dinamik olarak yazılan dillerdeki ördek gibi yazma kavramına benzer: ördek gibi yürüyorsa ve ördek gibi vaklıyorsa, o zaman bir ördek olmalıdır! Liste 17-5'teki run on Screen uygulamasında, run'ın her bir bileşenin somut türünün ne olduğunu bilmesine gerek yoktur. Bir bileşenin Button ya da SelectBox örneği olup olmadığını kontrol etmez, sadece bileşen üzerindeki draw yöntemini çağırır. Bileşenler vektöründeki değerlerin türü olarak Box<dyn Draw> belirterek, Screen'i draw yöntemini çağırabileceğimiz değerlere ihtiyaç duyacak şekilde tanımladık.

Ördek tiplemesi kullanan kodlara benzer kod yazmak için trait nesnelerini ve Rust'ın tür sistemini kullanmanın avantajı, çalışma zamanında bir değerin belirli bir yöntemi uygulayıp uygulamadığını kontrol etmek zorunda kalmamamız veya bir değer bir yöntemi uygulamıyorsa ancak yine de çağırırsak hata alma konusunda endişelenmememizdir. Değerler, özellik nesnelerinin ihtiyaç duyduğu özellikleri uygulamıyorsa Rust kodumuzu derlemeyecektir.

Örneğin, Liste 17-10, bileşen olarak String içeren bir Screen oluşturmaya çalıştığımızda ne olacağını gösterir:

Dosya adı: src/main.rs

use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}

Liste 17-10: Tanım nesnesinin özelliğini uygulamayan bir tür kullanmaya çalışmak

String, Draw özelliğini uygulamadığı için bu hatayı alıyoruz:

$ cargo run
   Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
 --> src/main.rs:5:26
  |
5 |         components: vec![Box::new(String::from("Hi"))],
  |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
  |
  = note: required for the cast to the object type `dyn Draw`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` due to previous error

Bu hata bize ya Screen'e geçmek istemediğimiz bir şey geçirdiğimizi ve bu nedenle farklı bir tür geçirmemiz gerektiğini ya da Screen'in üzerinde draw çağrısı yapabilmesi için Draw on String'i uygulamamız gerektiğini bildirir.

Tanım Nesneleri Dinamik Gönderim Gerçekleştirir

Bölüm 10'daki “Yaygınları Kullanan Kodun Performansı” bölümünde, yaygınlarda özellik sınırları kullandığımızda derleyici tarafından gerçekleştirilen monomorfizasyon işlemi hakkındaki tartışmamızı hatırlayın: derleyici, yaygın tür parametresi yerine kullandığımız her somut tip için fonksiyonların ve metodların yaygın olmayan uygulamalarını üretir. Monomorfizasyondan kaynaklanan kod, derleyicinin derleme zamanında hangi yöntemi çağırdığınızı bildiği statik gönderim yapıyor. Bu, derleyicinin derleme sırasında hangi yöntemi çağırdığınızı bilemediği dinamik gönderime zıttır. Dinamik gönderim durumlarında, derleyici çalışma zamanında hangi yöntemin çağrılacağını belirleyecek kodu yayınlar.

Tanım nesnelerini kullandığımızda, Rust dinamik gönderim kullanacaktır. Derleyici, trait nesnelerini kullanan kodla kullanılabilecek tüm türleri bilmez, bu nedenle hangi türde hangi yöntemin uygulanacağını bilemez. Bunun yerine, çalışma zamanında Rust, hangi yöntemin çağrılacağını bilmek için trait nesnesinin içindeki işaretçileri kullanır. Bu arama, statik gönderim ile oluşmayan bir çalışma zamanı maliyetine neden olur. Dinamik gönderim ayrıca derleyicinin bir yöntemin kodunu satır içi yapmayı seçmesini engeller ve bu da bazı optimizasyonları önler. Ancak, Liste 17-5'te yazdığımız ve Liste 17-9'da destekleyebildiğimiz kodda ekstra esneklik elde ettik, bu nedenle dikkate alınması gereken bir değiş tokuş.