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);
}
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>>,
}
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();
}
}
}
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();
}
}
}
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
}
}
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() {}
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();
}
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();
}
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ş.