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

Gelişmiş Trait’ler

Trait’leri ilk olarak Bölüm 10’daki “Trait’lerle Ortak Davranış Tanımlama” bölümünde ele almıştık; ancak daha ileri düzey ayrıntılara girmemiştik. Artık Rust hakkında çok daha fazla şey bildiğinize göre işin inceliklerine inebiliriz.

İlişkili Türlerle Trait Tanımlamak

İlişkili türler (associated types), bir tür yer tutucusunu bir trait ile ilişkilendirir; böylece trait metot tanımları kendi imzalarında bu yer tutucu türleri kullanabilir. Bir trait’i uygulayan taraf, söz konusu uygulama için yer tutucu türün yerine kullanılacak somut türü belirler. Bu sayede, trait uygulanana kadar bu türlerin tam olarak ne olduğunu bilmemize gerek kalmadan bazı türleri kullanan bir trait tanımlayabiliriz.

Bu bölümde ele aldığımız gelişmiş özelliklerin çoğunu nadiren ihtiyaç duyulan yapılar olarak tanımladık. İlişkili türler ise ikisinin ortasında bir yerde durur: Kitabın geri kalanında açıklanan özelliklere kıyasla daha seyrek, ancak bu bölümde tartışılan diğer pek çok özelliğe kıyasla daha yaygın biçimde kullanılırlar.

İlişkili türe sahip bir trait’e örnek olarak standart kütüphanenin sunduğu Iterator trait’ini verebiliriz. Buradaki ilişkili türün adı Item’dır ve Iterator trait’ini uygulayan türün üzerinde ilerlediği değerlerin türünü temsil eder. Iterator trait’inin tanımı Liste 20-13’te gösterilmektedir.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Liste 20-13: Item adlı ilişkili türe sahip Iterator trait’inin tanımı

Buradaki Item türü bir yer tutucudur ve next metodunun tanımı, Option<Self::Item> türünde değerler döndüreceğini gösterir. Iterator trait’ini uygulayanlar Item için somut türü belirler ve next metodu da bu somut türden bir değer içeren bir Option döndürür.

İlişkili türler, hangi türleri işleyebileceğini baştan belirtmeden bir fonksiyon tanımlamamıza olanak tanıyan jeneriklere benzer bir kavram gibi görünebilir. İki kavram arasındaki farkı incelemek için, Item türünü u32 olarak belirten Sayac adlı bir tür üzerindeki Iterator trait uygulamasına bakalım:

Dosya adı: src/lib.rs
struct Sayac {
    count: u32,
}

impl Sayac {
    fn new() -> Sayac {
        Sayac { count: 0 }
    }
}

impl Iterator for Sayac {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Bu sözdizimi jeneriklerinkine oldukça benzer görünüyor. Öyleyse Iterator trait’ini neden Liste 20-14’te gösterildiği gibi doğrudan jeneriklerle tanımlamayalım?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Liste 20-14: Iterator trait’inin jenerikler kullanılarak yazılmış varsayımsal bir tanımı

Aradaki fark şudur: Liste 20-14’te olduğu gibi jenerikleri kullandığımızda, her uygulamada türleri açıkça belirtmemiz gerekir; çünkü Sayac için Iterator<String> ya da başka herhangi bir türle de bir uygulama yazabilir, böylece Sayac için birden fazla Iterator uygulamasına sahip olabiliriz. Başka bir deyişle, bir trait jenerik bir parametreye sahip olduğunda, jenerik tür parametrelerinin somut türleri her seferinde değiştirilerek tek bir tür için birden fazla kez uygulanabilir. Sayac üzerinde next metodunu kullandığımızda ise hangi Iterator uygulamasını kullanmak istediğimizi belirtmek için tür açıklamaları eklememiz gerekirdi.

İlişkili türlerle ise tür açıklamaları eklememize gerek kalmaz; çünkü bir trait’i bir tür üzerinde birden fazla kez uygulayamayız. İlişkili türleri kullanan Liste 20-13’teki tanımda, Sayac için yalnızca tek bir impl Iterator for Sayac olabileceğinden Item türünün ne olacağını yalnızca bir kez seçebiliriz. Sayac üzerinde next çağırdığımız her yerde u32 değerlerinden oluşan bir iteratör istediğimizi belirtmek zorunda kalmayız.

İlişkili türler aynı zamanda trait’in sözleşmesinin bir parçası hâline gelir: Trait’i uygulayanlar, ilişkili tür yer tutucusunun yerine geçecek bir tür sağlamak zorundadır. İlişkili türler genellikle türün nasıl kullanılacağını açıklayan bir ada sahip olur ve API dokümantasyonunda ilişkili türü belgelendirmek iyi bir pratiktir.

Not: Kendi trait’inizi tasarlarken ilişkili tür ile jenerik tür parametresi arasında seçim yapmak için şu pratik ölçütü kullanabilirsiniz:

  • Trait’i uygulayan her tür için yalnızca tek bir doğal çıktı türü olması mantıklıysa ilişkili tür seçin (Iterator::Item, Future::Output veya Deref::Target gibi). Böylece trait’i kullanan kodu her çağrıda tür açıklaması yazma yükünden kurtarırsınız.
  • Aynı türün bu trait’i farklı girdi veya dönüşüm türleriyle birden fazla kez uygulayabilmesi gerekiyorsa jenerik tür parametresi seçin (From<T> ile bir türün hem &str hem i32 değerlerinden dönüştürülebilmesi veya birazdan göreceğimiz Add<Rhs> ile farklı sağ taraf türleriyle toplanabilmesi gibi).

Varsayılan Jenerik Parametreler ve Operatör Aşırı Yükleme

Jenerik tür parametreleri kullandığımızda, jenerik tür için varsayılan bir somut tür belirtebiliriz. Bu, varsayılan tür iş görüyorsa trait’i uygulayanların somut bir tür belirtme zorunluluğunu ortadan kaldırır. Jenerik bir türü bildirirken varsayılan türü <YerTutucuTur=SomutTur> sözdizimiyle belirtirsiniz.

Bu tekniğin yararlı olduğu harika bir durum, belirli koşullarda bir operatörün (+ gibi) davranışını özelleştirdiğiniz operatör aşırı yüklemesidir (operator overloading).

Rust kendi operatörlerinizi oluşturmanıza veya rastgele operatörleri aşırı yüklemenize izin vermez. Ancak std::ops modülünde listelenen işlemleri ve bunlara karşılık gelen trait’leri, operatörle ilişkili trait’i uygulayarak aşırı yükleyebilirsiniz. Örneğin Liste 20-15’te, iki Nokta örneğini birbiriyle toplamak için + operatörünü aşırı yüklüyoruz. Bunu bir Nokta struct’ı üzerinde Add trait’ini uygulayarak yapıyoruz.

Dosya adı: src/main.rs
use std::ops::Add;

#[derive(Debug, Copy, Clone, PartialEq)]
struct Nokta {
    x: i32,
    y: i32,
}

impl Add for Nokta {
    type Output = Nokta;

    fn add(self, other: Nokta) -> Nokta {
        Nokta {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Nokta { x: 1, y: 0 } + Nokta { x: 2, y: 3 },
        Nokta { x: 3, y: 3 }
    );
}
Liste 20-15: Nokta örnekleri için + operatörünü aşırı yüklemek üzere Add trait’ini uygulamak

add metodu, yeni bir Nokta oluşturmak için iki Nokta örneğinin x ve y değerlerini kendi aralarında toplar. Add trait’i, add metodundan döndürülecek türü belirleyen Output adında bir ilişkili türe sahiptir.

Not: Add trait’indeki add metodu self ve rhs parametrelerini referans olarak değil, değer olarak alır; yani a + b ifadesi her iki işlenenin de sahipliğini tüketir. Liste 20-15’te Nokta için Copy türetmemizin nedeni budur: Böylece p1 + p2 yazdıktan sonra p1 ve p2 kullanılmaya devam edebilir. Copy olmayan bir tür için + operatörünü değerleri taşımadan kullanmak isterseniz Add trait’ini &Nokta gibi referans türleri üzerinde de uygulamanız gerekir; standart kütüphane String için tam da bunu yapar (String + &str ifadesinde sol taraf tüketilir, sağ taraf yalnızca ödünç alınır).

Bu koddaki varsayılan jenerik tür, Add trait’inin içinde yer alır. Tanımı şöyledir:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Bu kod genel hatlarıyla tanıdık gelmelidir: Tek bir metoda ve bir ilişkili türe sahip bir trait. Yeni olan kısım Rhs=Self ifadesidir: Bu sözdizimine varsayılan tür parametreleri (default type parameters) denir. Rhs jenerik tür parametresi (“sağ taraf” anlamındaki right-hand side ifadesinin kısaltmasıdır), add metodundaki rhs parametresinin türünü tanımlar. Add trait’ini uygularken Rhs için somut bir tür belirtmezsek, Rhs parametresinin türü varsayılan olarak Self, yani üzerinde Add uyguladığımız tür olur.

Nokta için Add uyguladığımızda iki Nokta örneğini toplamak istediğimizden Rhs için varsayılan türü kullandık. Şimdi Add trait’ini uygularken varsayılanı kullanmak yerine Rhs türünü özelleştirmek istediğimiz bir örneğe bakalım.

Farklı birimlerde değerler tutan Milimetreler ve Metreler adında iki struct’ımız var. Mevcut bir türün başka bir struct içinde bu şekilde ince bir katmanla sarmalanması newtype deseni olarak bilinir; bunu “Dış Trait’leri Newtype Deseniyle Uygulamak” bölümünde daha ayrıntılı olarak ele alacağız. Milimetre cinsinden değerleri metre cinsinden değerlerle toplamak ve Add uygulamasının birim dönüşümünü doğru biçimde yapmasını istiyoruz. Liste 20-16’da gösterildiği gibi Rhs olarak Metreler türünü vererek Milimetreler için Add uygulayabiliriz.

Dosya adı: src/lib.rs
use std::ops::Add;

struct Milimetreler(u32);
struct Metreler(u32);

impl Add<Metreler> for Milimetreler {
    type Output = Milimetreler;

    fn add(self, other: Metreler) -> Milimetreler {
        Milimetreler(self.0 + (other.0 * 1000))
    }
}
Liste 20-16: Milimetreler ile Metreler değerlerini toplamak için Milimetreler üzerinde Add trait’ini uygulamak

Milimetreler ve Metreler değerlerini toplamak için, Self varsayılanını kullanmak yerine Rhs tür parametresinin değerini belirlemek üzere impl Add<Metreler> yazarız. Bu uygulama yalnızca Milimetreler + Metreler sıralamasını geçerli kılar; Metreler + Milimetreler yazabilmek için Metreler üzerinde ayrı bir Add<Milimetreler> uygulaması gerekir.

Varsayılan tür parametrelerini başlıca iki şekilde kullanırsınız:

  1. Mevcut kodu bozmadan bir türü genişletmek için
  2. Çoğu kullanıcının ihtiyaç duymayacağı özel durumlarda özelleştirmeye olanak tanımak için

Standart kütüphanenin Add trait’i ikinci amaca örnektir: Genellikle aynı türden iki değeri toplarsınız, ancak Add trait’i bunun ötesinde özelleştirme yapabilme olanağı sunar. Add trait tanımında varsayılan bir tür parametresi kullanmak, çoğu zaman fazladan parametreyi belirtmek zorunda kalmamanız anlamına gelir. Başka bir deyişle, uygulama sırasında tekrarlayan kalıp kodlara biraz daha az ihtiyaç duyulur ve bu da trait’in kullanımını kolaylaştırır.

İlk amaç ikinciye benzer, ancak ters yönde işler: Mevcut bir trait’e bir tür parametresi eklemek isterseniz, mevcut uygulama kodlarını bozmadan trait’in işlevselliğinin genişletilmesine olanak tanımak için bu parametreye bir varsayılan tür verebilirsiniz.

Aynı Adlı Metotlar Arasında Ayrım Yapmak

Rust’ta hiçbir kural bir trait’in başka bir trait’in metoduyla aynı ada sahip bir metot barındırmasını engellemez; her iki trait’i tek bir tür üzerinde uygulamanıza da engel olmaz. Ayrıca trait’lerdeki metotlarla aynı ada sahip bir metodu doğrudan türün kendisi üzerinde uygulamak da mümkündür.

Aynı ada sahip metotları çağırırken Rust’a hangisini kullanmak istediğinizi söylemeniz gerekir. Her ikisi de fly adında bir metoda sahip Pilot ve Buyucu adlı iki trait tanımladığımız Liste 20-17’deki kodu ele alalım. Ardından her iki trait’i, üzerinde hâlihazırda fly adlı bir metot uygulanmış olan Insan türü üzerinde uyguluyoruz. Her bir fly metodu farklı bir iş yapıyor.

Dosya adı: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Buyucu {
    fn fly(&self);
}

struct Insan;

impl Pilot for Insan {
    fn fly(&self) {
        println!("Kaptanınız konuşuyor.");
    }
}

impl Buyucu for Insan {
    fn fly(&self) {
        println!("Yukarı!");
    }
}

impl Insan {
    fn fly(&self) {
        println!("*çılgınca kollarını çırpıyor*");
    }
}

fn main() {}
Liste 20-17: fly metoduna sahip olacak şekilde tanımlanıp Insan türü üzerinde uygulanan iki trait ve doğrudan Insan üzerinde uygulanan bir fly metodu

Bir Insan örneği üzerinde fly metodunu çağırdığımızda derleyici, Liste 20-18’de gösterildiği gibi varsayılan olarak doğrudan türün üzerinde uygulanmış olan metodu çağırır.

Dosya adı: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Buyucu {
    fn fly(&self);
}

struct Insan;

impl Pilot for Insan {
    fn fly(&self) {
        println!("Kaptanınız konuşuyor.");
    }
}

impl Buyucu for Insan {
    fn fly(&self) {
        println!("Yukarı!");
    }
}

impl Insan {
    fn fly(&self) {
        println!("*çılgınca kollarını çırpıyor*");
    }
}

fn main() {
    let person = Insan;
    person.fly();
}
Liste 20-18: Bir Insan örneği üzerinde fly metodunu çağırmak

Bu kodu çalıştırmak ekrana *çılgınca kollarını çırpıyor* yazdıracak ve Rust’ın doğrudan Insan üzerinde uygulanmış fly metodunu çağırdığını gösterecektir.

Gerek Pilot trait’inden gerekse Buyucu trait’inden gelen fly metotlarını çağırmak için hangi fly metodunu kastettiğimizi belirten daha açık bir sözdizimi kullanmamız gerekir. Liste 20-19 bu sözdizimini göstermektedir.

Dosya adı: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Buyucu {
    fn fly(&self);
}

struct Insan;

impl Pilot for Insan {
    fn fly(&self) {
        println!("Kaptanınız konuşuyor.");
    }
}

impl Buyucu for Insan {
    fn fly(&self) {
        println!("Yukarı!");
    }
}

impl Insan {
    fn fly(&self) {
        println!("*çılgınca kollarını çırpıyor*");
    }
}

fn main() {
    let person = Insan;
    Pilot::fly(&person);
    Buyucu::fly(&person);
    person.fly();
}
Liste 20-19: Hangi trait’in fly metodunu çağırmak istediğimizi belirtmek

Metot adından önce trait adını belirtmek, Rust’a hangi fly uygulamasını çağırmak istediğimizi netleştirir. Liste 20-19’da kullandığımız person.fly() ifadesine eşdeğer olan Insan::fly(&person) biçimini de yazabilirdik; ancak belirsizliği gidermemiz gerekmiyorsa bunu yazmak biraz daha uzundur.

Bu kodu çalıştırmak ekrana aşağıdakileri yazdırır:

Kaptanınız konuşuyor.
Yukarı!
*çılgınca kollarını çırpıyor*

fly metodu bir self parametresi aldığından, tek bir trait’i uygulayan iki farklı türümüz olsaydı, Rust self parametresinin türüne bakarak trait’in hangi uygulamasını kullanacağını çıkarabilirdi.

Ancak metot olmayan ilişkili fonksiyonların self parametresi yoktur. Metot olmayan ve aynı adı taşıyan fonksiyonlar tanımlayan birden fazla tür veya trait bulunduğunda, tam nitelikli sözdizimi (fully qualified syntax) kullanmadığınız sürece Rust hangi türü kastettiğinizi her zaman bilemez. Örneğin Liste 20-20’de, tüm yavru köpeklere Karabaş adını vermek isteyen bir hayvan barınağı için bir trait oluşturuyoruz. Metot olmayan ilişkili bir baby_name fonksiyonuna sahip Hayvan adında bir trait tanımlıyoruz. Hayvan trait’i, üzerinde doğrudan metot olmayan ilişkili bir baby_name fonksiyonu da sunduğumuz Kopek struct’ı için uygulanıyor.

Dosya adı: src/main.rs
trait Hayvan {
    fn baby_name() -> String;
}

struct Kopek;

impl Kopek {
    fn baby_name() -> String {
        String::from("Karabaş")
    }
}

impl Hayvan for Kopek {
    fn baby_name() -> String {
        String::from("yavru köpek")
    }
}

fn main() {
    println!("Bir yavru köpeğe {} denir", Kopek::baby_name());
}
Liste 20-20: İlişkili fonksiyona sahip bir trait ile aynı adda ilişkili fonksiyona sahip olan ve aynı zamanda bu trait’i uygulayan bir tür

Tüm yavru köpeklere Karabaş adını verme kodunu, Kopek üzerinde tanımlanan baby_name ilişkili fonksiyonunda uyguluyoruz. Kopek türü ayrıca tüm hayvanların sahip olduğu özellikleri tanımlayan Hayvan trait’ini de uygular. Köpek yavrularına yavru köpek denir ve bu durum, Kopek üzerindeki Hayvan trait uygulamasında Hayvan trait’iyle ilişkili baby_name fonksiyonu içinde ifade edilmiştir.

main içinde Kopek::baby_name ifadesini çağırıyoruz; bu çağrı doğrudan Kopek üzerinde tanımlanmış ilişkili fonksiyonu çalıştırır. Bu kod ekrana şunu yazdırır:

Bir yavru köpeğe Karabaş denir

Bu çıktı bizim istediğimiz sonuç değildir. Biz Kopek üzerinde uyguladığımız Hayvan trait’inin bir parçası olan baby_name fonksiyonunu çağırmak istiyoruz; böylece kod Bir yavru köpeğe yavru köpek denir çıktısını üretsin. Liste 20-19’da kullandığımız trait adını belirtme tekniği burada işe yaramaz; main fonksiyonunu Liste 20-21’deki gibi değiştirirsek bir derleme hatası alırız.

Dosya adı: src/main.rs
trait Hayvan {
    fn baby_name() -> String;
}

struct Kopek;

impl Kopek {
    fn baby_name() -> String {
        String::from("Karabaş")
    }
}

impl Hayvan for Kopek {
    fn baby_name() -> String {
        String::from("yavru köpek")
    }
}

fn main() {
    println!("Bir yavru köpeğe {} denir", Hayvan::baby_name());
}
Liste 20-21: Hayvan trait’inden baby_name fonksiyonunu çağırmayı denemek; ancak Rust hangi uygulamayı kullanacağını bilemez

Hayvan::baby_name bir self parametresine sahip olmadığından ve Hayvan trait’ini uygulayan başka türler de bulunabileceğinden, Rust Hayvan::baby_name fonksiyonunun hangi uygulamasını istediğimizi çıkaramaz. Şu derleyici hatasını alırız:

$ cargo run
   Compiling trait-ornekleri v0.1.0 (file:///projects/trait-ornekleri)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Hayvan::baby_name` defined here
...
20 |     println!("Bir yavru köpeğe {} denir", Hayvan::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("Bir yavru köpeğe {} denir", <Kopek as Hayvan>::baby_name());
   |                                           ++++++++++      +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `trait-ornekleri` (bin "trait-ornekleri") due to 1 previous error

Belirsizliği gidermek ve Rust’a başka bir tür için olan Hayvan uygulaması yerine Kopek için olan Hayvan uygulamasını kullanmak istediğimizi söylemek için tam nitelikli sözdizimini kullanmamız gerekir. Liste 20-22 tam nitelikli sözdiziminin nasıl kullanılacağını göstermektedir.

Dosya adı: src/main.rs
trait Hayvan {
    fn baby_name() -> String;
}

struct Kopek;

impl Kopek {
    fn baby_name() -> String {
        String::from("Karabaş")
    }
}

impl Hayvan for Kopek {
    fn baby_name() -> String {
        String::from("yavru köpek")
    }
}

fn main() {
    println!("Bir yavru köpeğe {} denir", <Kopek as Hayvan>::baby_name());
}
Liste 20-22: Kopek üzerinde uygulandığı hâliyle Hayvan trait’indeki baby_name fonksiyonunu çağırmak istediğimizi belirtmek için tam nitelikli sözdizimi kullanmak

Açılı parantezler içinde Rust’a bir tür açıklaması sağlıyoruz: <Kopek as Hayvan> yazarak bu fonksiyon çağrısı için Kopek türünü bir Hayvan olarak ele almak istediğimizi söylüyoruz. Böylece Kopek üzerinde uygulandığı hâliyle Hayvan trait’indeki baby_name fonksiyonu çağrılır. Bu kod artık istediğimiz çıktıyı yazdıracaktır:

Bir yavru köpeğe yavru köpek denir

Genel olarak tam nitelikli sözdizimi şöyle tanımlanır:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Metot olmayan ilişkili fonksiyonlarda bir receiver (alıcı, yani self) bulunmaz: Yalnızca diğer argümanların listesi yer alır. Fonksiyon veya metot çağırdığınız her yerde tam nitelikli sözdizimini kullanabilirsiniz. Ancak Rust’ın programdaki diğer bilgilerden çıkarabildiği her kısmı bu sözdiziminden atlayabilirsiniz. Bu daha ayrıntılı sözdizimini yalnızca aynı adı kullanan birden fazla uygulamanın bulunduğu ve Rust’ın hangi uygulamayı çağırmak istediğinizi belirlemek için yardıma ihtiyaç duyduğu durumlarda kullanmanız gerekir.

Üst Trait’leri Kullanmak

Bazen başka bir trait’e bağımlı olan bir trait tanımı yazabilirsiniz: Bir türün ilk trait’i uygulayabilmesi için ikinci trait’i de uygulamasını zorunlu kılmak istersiniz. Bunu, trait tanımınızın ikinci trait’in ilişkili öğelerinden yararlanabilmesi için yaparsınız. Trait tanımınızın dayandığı bu trait’e, trait’inizin üst trait’i (supertrait) denir.

Örneğin, verilen bir değeri yıldız işaretleriyle çerçevelenmiş biçimde yazdıracak outline_print metoduna sahip bir CerceveliYazdir trait’i yapmak istediğimizi varsayalım. Yani standart kütüphanedeki Display trait’ini (x, y) sonucu üretecek biçimde uygulayan bir Nokta struct’ı verildiğinde, x için 1 ve y için 3 değerine sahip bir Nokta örneği üzerinde outline_print çağırdığımızda metodun şunu yazdırması gerekir:

**********
*        *
* (1, 3) *
*        *
**********

outline_print metodunun uygulamasında Display trait’inin işlevselliğini kullanmak istiyoruz. Bu nedenle, CerceveliYazdir trait’inin yalnızca Display trait’ini de uygulayan ve CerceveliYazdir’ın ihtiyaç duyduğu işlevselliği sağlayan türler için çalışacağını belirtmemiz gerekir. Bunu trait tanımında CerceveliYazdir: Display belirterek yapabiliriz. Bu teknik, trait’e bir trait sınırı eklemeye benzer. Liste 20-23, CerceveliYazdir trait’inin bir uygulamasını göstermektedir.

Dosya adı: src/main.rs
use std::fmt;

trait CerceveliYazdir: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}
Liste 20-23: Display’den gelen işlevselliği gerektiren CerceveliYazdir trait’ini uygulamak

CerceveliYazdir trait’inin Display trait’ini gerektirdiğini belirttiğimiz için, Display uygulayan her tür üzerinde otomatik olarak uygulanan to_string fonksiyonunu kullanabiliriz. Trait adından sonra iki nokta üst üste ekleyip Display trait’ini belirtmeden to_string kullanmayı deneseydik, geçerli kapsamda &Self türü için to_string adında bir metot bulunamadığını söyleyen bir hata alırdık.

Nokta struct’ı gibi Display uygulamayan bir tür üzerinde CerceveliYazdir uygulamaya çalıştığımızda ne olacağını görelim:

Dosya adı: src/main.rs
use std::fmt;

trait CerceveliYazdir: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Nokta {
    x: i32,
    y: i32,
}

impl CerceveliYazdir for Nokta {}

fn main() {
    let p = Nokta { x: 1, y: 3 };
    p.outline_print();
}

Display’in gerekli olduğunu ancak uygulanmadığını belirten bir hata alırız:

$ cargo run
   Compiling trait-ornekleri v0.1.0 (file:///projects/trait-ornekleri)
error[E0277]: `Nokta` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:26
   |
20 | impl CerceveliYazdir for Nokta {}
   |                          ^^^^^ unsatisfied trait bound
   |
help: the trait `std::fmt::Display` is not implemented for `Nokta`
  --> src/main.rs:15:1
   |
15 | struct Nokta {
   | ^^^^^^^^^^^^
note: required by a bound in `CerceveliYazdir`
  --> src/main.rs:3:24
   |
 3 | trait CerceveliYazdir: fmt::Display {
   |                        ^^^^^^^^^^^^ required by this bound in `CerceveliYazdir`

error[E0277]: `Nokta` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ unsatisfied trait bound
   |
help: the trait `std::fmt::Display` is not implemented for `Nokta`
  --> src/main.rs:15:1
   |
15 | struct Nokta {
   | ^^^^^^^^^^^^
note: required by a bound in `CerceveliYazdir::outline_print`
  --> src/main.rs:3:24
   |
 3 | trait CerceveliYazdir: fmt::Display {
   |                        ^^^^^^^^^^^^ required by this bound in `CerceveliYazdir::outline_print`
 4 |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

For more information about this error, try `rustc --explain E0277`.
error: could not compile `trait-ornekleri` (bin "trait-ornekleri") due to 2 previous errors

Bunu düzeltmek için Nokta üzerinde Display uygular ve CerceveliYazdir’ın gerektirdiği kısıtı şu şekilde karşılarız:

Dosya adı: src/main.rs
trait CerceveliYazdir: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Nokta {
    x: i32,
    y: i32,
}

impl CerceveliYazdir for Nokta {}

use std::fmt;

impl fmt::Display for Nokta {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Nokta { x: 1, y: 3 };
    p.outline_print();
}

Ardından Nokta üzerinde CerceveliYazdir trait’ini uygulamak başarıyla derlenecek ve bir Nokta örneğini yıldızlardan oluşan bir çerçeve içinde görüntülemek için üzerinde outline_print metodunu çağırabileceğiz.

Dış Trait’leri Newtype Deseniyle Uygulamak

Bölüm 10’daki “Bir Tür Üzerinde Trait Uygulama” bölümünde, bir trait’i bir tür üzerinde yalnızca trait’in ya da türün veya her ikisinin birden kendi crate’imize yerel olması durumunda uygulayabileceğimizi belirten yetim kuralından (orphan rule) bahsetmiştik. Bir demet struct’ı içinde yeni bir tür oluşturmayı içeren newtype desenini kullanarak bu kısıtlamanın etrafından dolaşmak mümkündür. (Demet struct’larını Bölüm 5’teki “Demet Struct’ları ile Farklı Türler Oluşturma” bölümünde ele almıştık.) Bu demet struct’ı tek bir alana sahip olur ve kendisi için trait uygulamak istediğimiz türün etrafında ince bir sarmalayıcı görevi görür. Böylece sarmalayıcı tür kendi crate’imize yerel olur ve trait’i bu sarmalayıcı üzerinde uygulayabiliriz. Newtype, kökeni Haskell programlama diline dayanan bir terimdir. Bu deseni kullanmanın çalışma zamanında hiçbir performans maliyeti yoktur ve sarmalayıcı tür derleme zamanında ortadan kaldırılır.

Örnek olarak Vec<T> üzerinde Display uygulamak istediğimizi düşünelim; hem Display trait’i hem de Vec<T> türü crate’imizin dışında tanımlandığı için yetim kuralı bunu doğrudan yapmamızı engeller. Bir Vec<T> örneğini tutan bir Sarmalayici struct’ı oluşturabiliriz; ardından Liste 20-24’te gösterildiği gibi Sarmalayici üzerinde Display uygulayabilir ve Vec<T> değerini kullanabiliriz.

Dosya adı: src/main.rs
use std::fmt;

struct Sarmalayici(Vec<String>);

impl fmt::Display for Sarmalayici {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Sarmalayici(vec![String::from("merhaba"), String::from("dünya")]);
    println!("w = {w}");
}
Liste 20-24: Display uygulamak için Vec<String> etrafında bir Sarmalayici türü oluşturmak

Sarmalayici bir demet struct’ı olduğu ve Vec<T> demetteki 0 indeksli öğe olduğu için Display uygulaması içteki Vec<T> değerine erişmek üzere self.0 ifadesini kullanır. Ardından Sarmalayici üzerinde Display trait’inin işlevselliğini kullanabiliriz.

Bu tekniği kullanmanın dezavantajı, Sarmalayici’nın yeni bir tür olması ve dolayısıyla tuttuğu değerin metotlarına sahip olmamasıdır. Sarmalayici’yı tıpkı bir Vec<T> gibi kullanabilmek için Vec<T>’nin tüm metotlarını doğrudan Sarmalayici üzerinde, işlevi self.0’a devredecek şekilde uygulamamız gerekirdi. Yeni türün içteki türün sahip olduğu her metoda sahip olmasını isteseydik, içteki türü döndürecek şekilde Sarmalayici üzerinde Deref trait’ini uygulamak bir çözüm olurdu (Deref trait’ini uygulamayı Bölüm 15’teki “Akıllı İşaretçilere Normal Referanslar Gibi Davranma” bölümünde ele almıştık). Eğer Sarmalayici türünün içteki türün tüm metotlarına sahip olmasını istemeseydik —örneğin Sarmalayici türünün davranışını kısıtlamak amacıyla— yalnızca gerçekten istediğimiz metotları elle uygulamamız gerekirdi.

Bu newtype deseni, işin içinde trait’ler olmadığında bile kullanışlıdır. Şimdi odağımızı değiştirelim ve Rust’ın tür sistemiyle etkileşim kurmanın bazı gelişmiş yollarına bakalım.