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 10. bölümdeki “Trait’lerle Ortak Davranış Tanımlamak” kısmında ele almıştık; ancak daha ileri ayrıntılara girmemiştik. Artık Rust hakkında daha fazla şey bildiğinize göre, biraz daha derine inebiliriz.

İlişkili Türlerle Trait Tanımlamak

İlişkili türler (associated types), bir trait içindeki yer tutucu türü trait ile bağlar; böylece trait metodlarının imzalarında bu yer tutucu türler kullanılabilir. Trait’i uygulayan taraf, o uygulama için yer tutucu yerine kullanılacak somut türü belirtir. Böylece, bir trait’in bazı türleri kullanacağını söyleyebilir ama trait uygulanana kadar bu türlerin tam olarak ne olacağını bilmek zorunda kalmayız.

Bu bölümde anlattığımız gelişmiş özelliklerin çoğunu “nadiren gerekli” diye tanımladık. İlişkili türler ise ortada bir yerdedir: kitabın geri kalanındaki ana özelliklerden daha az kullanılırlar ama bu bölümdeki bazı diğer özelliklerden daha yaygındırlar.

İlişkili tür kullanan trait’e örnek olarak standart kütüphanedeki Iterator trait’ini verebiliriz. İlişkili türün adı Item’dır ve Iterator trait’ini uygulayan türün yinelediği değerlerin türünü temsil eder. Iterator trait’inin tanımı Liste 20-13’te gösteriliyor.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-13: Item adlı ilişkili tür içeren Iterator trait’inin tanımı

Buradaki Item bir yer tutucudur. next metodunun tanımı da Option<Self::Item> döndüreceğini söyler. Iterator trait’ini uygulayan türler Item için somut türü belirler; next de bu somut türden bir değer içeren Option döndürür.

İlişkili türler ilk bakışta jeneriklere benziyormuş gibi görünebilir. Çünkü jenerikler de bir fonksiyonun hangi türlerle çalışacağını belirtmeden tanımlanmasını sağlar. Aradaki farkı görmek için Sayac adlı bir tür üzerinde Iterator uygulamasına bakalım; burada Item türü u32 olarak belirtilmiştir:

Filename: 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 jeneriklere benzer görünüyor. O hâlde neden Iterator trait’ini doğrudan jeneriklerle tanımlamayalım? Liste 20-14’te bunun varsayımsal bir sürümünü görebilirsiniz.

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

Fark şu: jenerik kullandığımızda, Liste 20-14’te olduğu gibi, her uygulamada türleri ayrıca açıklamamız gerekir. Çünkü Sayac için istersek Iterator<String>, istersek başka bir Iterator<T> uygulaması daha yazabiliriz. Yani trait jenerik parametre alıyorsa, aynı trait aynı tür için birden çok kez uygulanabilir; her seferinde jenerik parametrelerin somut türleri değişebilir. Böyle olsaydı, Sayac üzerinde next çağırırken hangi Iterator uygulamasını kastettiğimizi ayrıca belirtmemiz gerekirdi.

İlişkili türlerde buna gerek kalmaz; çünkü aynı trait’i aynı tür için birden fazla kez uygulayamayız. Liste 20-13’te ilişkili tür kullanan tanımda Item türünü yalnızca bir kez seçebiliriz; çünkü Sayac için ancak tek bir impl Iterator yazılabilir. Böylece Sayac üzerinde next çağırdığımız her yerde bunun u32 değerleri döndüren yineleyici olduğunu tekrar tekrar belirtmemiz gerekmez.

İlişkili türler, trait’in sözleşmesinin de bir parçasıdır: trait’i uygulayanlar ilişkili tür yer tutucusuna karşılık gelecek bir tür vermek zorundadır. Bu yüzden ilişkili türlere genellikle kullanımını anlatan anlamlı adlar verilir; API belgelerinde bunları ayrıca açıklamak iyi bir pratiktir.

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

Jenerik tür parametreleri kullandığımızda, o jenerik tür için varsayılan bir somut tür de belirtebiliriz. Böylece varsayılan tür iş görüyorsa trait’i uygulayanların ayrıca tür belirtmesine gerek kalmaz. Varsayılan türü <YerTutucuTur=SomutTur> sözdizimiyle tanımlarız.

Bu tekniğin faydalı olduğu güzel örneklerden biri operatör aşırı yükleme dir; yani belirli durumlarda bir operatörün (+ gibi) davranışını özelleştirmek.

Rust kendi operatörlerinizi tanımlamanıza ya da rastgele operatörleri aşırı yüklemenize izin vermez. Ancak std::ops içindeki işlemleri ve bunlara karşılık gelen trait’leri uygulayarak özelleştirebilirsiniz. Örneğin Liste 20-15’te iki Nokta örneğini toplamak için + operatörünü aşırı yüklüyoruz. Bunu Nokta struct’ı üzerinde Add trait’ini uygulayarak yapıyoruz.

Filename: 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 }
    );
}
Listing 20-15: Nokta örnekleri için + operatörünü aşırı yüklemek üzere Add trait’ini uygulamak

add metodu iki Nokta örneğinin x değerlerini ve y değerlerini toplayıp yeni bir Nokta üretir. Add trait’inde Output adlı ilişkili tür vardır; add metodunun döndüreceği türü bu belirler.

Bu örnekteki varsayılan jenerik tür, Add trait’inin tanımı içindedir:

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

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

Bu kod size tanıdık gelmiş olmalı: tek metodlu, ilişkili tür içeren bir trait. Yeni kısım Rhs=Self ifadesidir. Buna varsayılan tür parametresi denir. Rhs jenerik tür parametresi (“sağ taraf” anlamındaki right-hand side’ın kısaltmasıdır), add metodundaki rhs parametresinin türünü belirler. Add trait’ini uygularken Rhs için somut tür vermezsek Self kullanılır; yani Add’i uyguladığımız türün kendisi.

Nokta için Add uygularken Rhs için varsayılanı kullandık; çünkü iki Nokta toplamak istiyorduk. Şimdi Rhs türünü özelleştirdiğimiz bir örneğe bakalım.

Elimizde farklı birimlerde değer tutan Milimetreler ve Metreler yapıları olduğunu düşünelim. Var olan bir türü başka bir struct ile ince biçimde sarmalama yaklaşımına newtype deseni denir; bunu birazdan daha ayrıntılı ele alacağız. Diyelim ki milimetre cinsinden değerlerle metre cinsinden değerleri toplamak istiyoruz ve Add uygulaması dönüşümü doğru yapsın istiyoruz. Bunun için Milimetreler üzerinde, Rhs türü Metreler olacak şekilde Add uygulayabiliriz; Liste 20-16 bunu gösteriyor.

Filename: 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))
    }
}
Listing 20-16: Milimetreler üzerinde Add uygulayarak Milimetreler ile Metreler toplamak

Milimetreler ile Metreler toplayabilmek için impl Add<Metreler> yazarız; böylece Rhs parametresi Self yerine Metreler olur.

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

  1. Var olan kodu bozmadan bir türü ya da trait’i genişletmek
  2. Çoğu kullanıcının ihtiyaç duymayacağı belirli durumlarda özelleştirme imkânı sunmak

Standart kütüphanedeki Add trait’i ikinci amaca güzel örnektir: çoğu zaman aynı türden iki değeri toplarsınız; ama Add gerektiğinde bundan fazlasını da yapabilmenizi sağlar. Add tanımındaki varsayılan tür parametresi sayesinde, çoğu durumda bu ek parametreyi yazmanız gerekmez. Yani biraz fazladan tekrarlayan koddan kurtulmuş olursunuz.

İlk amaç da buna benzer, ama ters yönde işler: var olan bir trait’e yeni tür parametresi eklemek isterseniz buna varsayılan değer vererek trait’in işlevini genişletebilir, mevcut uygulama kodlarını bozmamış olursunuz.

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

Rust’ta bir trait’in, başka bir trait’teki metodla aynı adda metod tanımlamasını engelleyen bir kural yoktur. Hatta aynı tür üzerinde bu iki trait’in ikisini birden uygulayabilirsiniz. Ayrıca türün kendisi üzerinde, trait metodlarıyla aynı ada sahip bir metod da tanımlayabilirsiniz.

Aynı isimli metodlar çağrılırken Rust’a hangisini kullanmak istediğinizi söylemeniz gerekir. Liste 20-17’de bunun örneğini görüyoruz: Pilot ve Buyucu adlı iki trait tanımlıyoruz; ikisinde de fly adlı metod var. Sonra her iki trait’i de zaten fly metodu olan Insan türü üzerinde uyguluyoruz. Her fly farklı bir şey yapıyor.

Filename: 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!("*çok selamlı kol çırpıyor*");
    }
}

fn main() {}
Listing 20-17: Hem Pilot hem Buyucu içinde fly metodu tanımlanması, bunların Insan üzerinde uygulanması ve Insan üzerinde ayrıca doğrudan fly metodunun bulunması

Bir Insan örneğinde fly çağırdığımızda, derleyici varsayılan olarak türün kendisi üzerinde tanımlı olan metodu seçer. Bunu Liste 20-18’de görebilirsiniz.

Filename: 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!("*çok selamlı kol çırpıyor*");
    }
}

fn main() {
    let person = Insan;
    person.fly();
}
Listing 20-18: Bir Insan örneğinde fly çağırmak

Bu kod çalıştırıldığında *waving arms furiously* yazdırır; yani Rust, doğrudan Insan üzerinde uygulanmış fly metodunu çağırır.

Pilot veya Buyucu trait’lerindeki fly metodlarını çağırmak için daha açık bir sözdizimi kullanmamız gerekir. Liste 20-19 bunu gösteriyor.

Filename: 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!("*çok selamlı kol çırpıyor*");
    }
}

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

Metod adından önce trait adını yazmak, Rust’a hangi fly uygulamasını istediğimizi açıkça söyler. İstersek Insan::fly(&person) da yazabilirdik; bu, person.fly() ile eşdeğerdir. Ama ayrım yapmaya ihtiyacımız yoksa daha uzundur.

Bu kodun çıktısı şöyledir:

Kaptanınız konuşuyor.
Yukarı!
*çok selamlı kol çırpıyor*

fly metodu self parametresi aldığı için, aynı trait’i uygulayan iki farklı tür olsa bile Rust self türüne bakarak hangi uygulamanın kullanılacağını anlayabilir.

Ama metod olmayan ilişkili fonksiyonlarda self parametresi yoktur. Aynı adlı metod olmayan fonksiyonları birden fazla tür ya da trait tanımladığında, tam nitelikli sözdizimi kullanmazsanız Rust her zaman neyi kastettiğinizi anlayamaz. Örneğin Liste 20-20’de, bir hayvan barınağının bütün yavru köpeklere Karabaş adını vermek istediğini düşünelim. Hayvan adlı bir trait oluşturuyoruz; burada metod olmayan ilişkili fonksiyon baby_name var. Kopek struct’ı bu trait’i uyguluyor; ayrıca Kopek üzerinde doğrudan yine baby_name adlı bir ilişkili fonksiyon tanımlıyoruz.

Filename: 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 {}", Kopek::baby_name());
}
Listing 20-20: Aynı adlı ilişkili fonksiyona sahip bir trait ve aynı adlı ilişkili fonksiyona sahip, bu trait’i de uygulayan bir tür

Tüm yavru köpeklere Karabaş adını verme kodunu, Kopek üzerinde tanımlı baby_name ilişkili fonksiyonunda yazıyoruz. Kopek ayrıca bütün hayvanların ortak özelliklerini anlatan Hayvan trait’ini de uygular. Yavru köpeklerin “yavru köpek” diye anılması da, Hayvan trait’inin Kopek üzerindeki baby_name uygulamasında ifade edilir.

main içinde Kopek::baby_name çağırdığımızda, doğrudan Kopek üzerinde tanımlı ilişkili fonksiyon çağrılır. Bu kod şu çıktıyı verir:

Bir yavru köpeğe Karabaş

Ama istediğimiz bu değildir. Biz Kopek için uyguladığımız Hayvan trait’inin baby_name fonksiyonunu çağırmak istiyoruz; böylece kod Bir yavru köpeğe yavru köpek denir anlamına gelen çıktıyı üretsin. Liste 20-19’da kullandığımız teknik burada işe yaramaz. main fonksiyonunu Liste 20-21’deki gibi değiştirirsek derleme hatası alırız.

Filename: 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 {}", Hayvan::baby_name());
}
Listing 20-21: Hayvan trait’indeki baby_name fonksiyonunu çağırmaya çalışmak; ama Rust hangi uygulamanın kullanılacağını bilemez

Hayvan::baby_name bir self parametresi almadığı ve Hayvan trait’ini uygulayan başka türler de olabileceği için, Rust hangi uygulamayı kastettiğimizi anlayamaz. Derleyiciden şu hatayı 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:19:39
   |
2  |     fn baby_name() -> String;
   |     ------------------------- `Hayvan::baby_name` defined here
...
19 |     println!("Bir yavru köpeğe {}", Hayvan::baby_name());
   |                                     ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
19 |     println!("Bir yavru köpeğe {}", <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

Rust’a özellikle Kopek için uygulanmış Hayvan sürümünü kullanmak istediğimizi söylemek için tam nitelikli sözdizimi (fully qualified syntax) kullanmamız gerekir. Liste 20-22 bunu gösteriyor.

Filename: 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 {}", <Kopek as Hayvan>::baby_name());
}
Listing 20-22: Kopek üzerinde uygulanmış Hayvan trait’indeki baby_name fonksiyonunu çağırmak istediğimizi tam nitelikli sözdizimiyle belirtmek

Açılı ayraçlar içinde Rust’a bir tür açıklaması veriyoruz; yani bu fonksiyon çağrısı için Kopek türüne Hayvan gibi davranılmasını istediğimizi söylüyoruz. Böylece tam istediğimiz çıktı elde edilir:

Bir yavru köpeğe yavru köpek

Genel biçim şöyledir:

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

Metod olmayan ilişkili fonksiyonlarda receiver kısmı bulunmaz; yalnızca diğer argümanlar kalır. Aslında fonksiyon ve metod çağırdığınız her yerde bu sözdizimini kullanabilirsiniz. Ama Rust programdaki diğer bilgilerden neyi kastettiğinizi çıkarabiliyorsa bazı kısımları yazmadan geçmenize izin verir. Bu daha uzun sözdizimine yalnızca aynı adı kullanan birden fazla uygulama olduğunda ihtiyaç duyarsınız.

Üst Trait’leri Kullanmak

Bazen bir trait tanımınız başka bir trait’e dayanır. Yani bir türün ilk trait’i uygulayabilmesi için ikinci trait’i de uygulamasını şart koşmak istersiniz. Bunu, trait tanımınızın ikinci trait’teki öğeleri kullanabilmesi için yaparsınız. Trait’inizin dayandığı bu trait’e üst trait denir.

Örneğin CerceveliYazdir adlı bir trait oluşturmak istediğimizi varsayalım. Bunun outline_print metodu, verilen değeri yıldızlarla çerçeveleyerek yazdırsın. Diyelim ki (x, y) biçiminde çıktı üreten Display uygulamasına sahip bir Nokta yapımız var. x = 1 ve y = 3 olan bir Nokta üzerinde outline_print çağrıldığında şu sonucu görmek istiyoruz:

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

outline_print içinde Display trait’inin sunduğu işlevselliği kullanmak istiyoruz. Bu yüzden CerceveliYazdir trait’inin yalnızca Display uygulayan türlerde çalışacağını belirtmeliyiz. Bunu trait tanımında CerceveliYazdir: Display diyerek yaparız. Bu teknik, trait’e trait sınırı eklemeye benzer. Liste 20-23, CerceveliYazdir uygulamasını gösteriyor.

Filename: 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() {}
Listing 20-23: Display işlevselliğine ihtiyaç duyan CerceveliYazdir trait’ini uygulamak

CerceveliYazdir trait’inin Display gerektirdiğini belirttiğimiz için, Display uygulayan her tür için otomatik olarak gelen to_string fonksiyonunu kullanabiliyoruz. Eğer trait adından sonra : Display yazmasaydık, &Self türü için geçerli kapsamda to_string adlı metod bulunamadığına dair hata alırdık.

Şimdi Display uygulamayan bir tür, örneğin Nokta, üzerinde CerceveliYazdir uygulamaya çalıştığımızda ne olacağına bakalım:

Filename: 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();
}

Derleyici bize Display gerektiğini ama uygulanmadığını söyler:

$ cargo run
   Compiling trait-ornekleri v0.1.0 (file:///projects/trait-ornekleri)
error[E0277]: `Nokta` doesn't implement `std::fmt::Display`
  --> src/main.rs:19:25
   |
19 | impl CerceveliYazdir for Nokta {}
   |                         ^^^^^ `Nokta` cannot be formatted with the default formatter
   |
   = help: the trait `std::fmt::Display` is not implemented for `Nokta`
note: required by a bound in `CerceveliYazdir`
  --> src/main.rs:3:24
   |
3  | trait CerceveliYazdir: fmt::Display {
   |                        ^^^^^^^^^^^^ required by this bound in `CerceveliYazdir`

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

Bunu düzeltmek için Nokta üzerinde Display uygular, böylece CerceveliYazdir’in istediği koşulu karşılarız:

Filename: 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();
}

Bundan sonra Nokta üzerinde CerceveliYazdir uygulamak sorunsuz derlenir; outline_print çağrısıyla değeri yıldızlı çerçeve içinde yazdırabiliriz.

Dış Trait’leri Newtype Deseniyle Uygulamak

  1. bölümdeki “Bir Trait’i Bir Türe Uygulamak” kısmında yetim kuralından söz etmiştik: bir trait’i bir tür üzerinde ancak trait ya da türden en az biri kendi crate’imize aitse uygulayabiliriz. Bu kısıtlamayı aşmanın yollarından biri newtype desenidir. Bunun için bir demet struct içinde yeni bir tür oluştururuz. (Demet struct’ları 5. bölümdeki “Demet Struct’larla Farklı Türler Oluşturmak” kısmında görmüştük.) Bu demet struct tek alanlı, ince bir sarmalayıcı olur. Böylece sarmalayıcı tür bizim crate’imize ait olduğundan, trait’i onun üzerinde uygulayabiliriz. Newtype terimi Haskell’den gelir. Bu desenin çalışma zamanında ek bir maliyeti yoktur; sarmalayıcı tür derleme zamanında ortadan kaldırılır.

Örneğin, Vec<T> üzerinde Display uygulamak istediğimizi düşünelim. Yetim kuralı buna doğrudan izin vermez; çünkü hem Display hem de Vec<T> bizim crate’imizin dışında tanımlanmıştır. Bunun yerine Vec<T> örneğini tutan Sarmalayici adlı bir struct oluşturabiliriz. Sonra Display trait’ini Sarmalayici üzerinde uygular ve içteki Vec<T> değerini kullanırız. Liste 20-24 bunu gösteriyor.

Filename: 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("dunya"), String::from("Rust")]);
    println!("w = {w}");
}
Listing 20-24: Display uygulayabilmek için Vec<String> etrafında Sarmalayici türü oluşturmak

Display uygulaması, Sarmalayici bir demet struct olduğu ve Vec<T> de demetin 0 numaralı öğesi olduğu için içteki Vec<T>’ye self.0 ile erişir. Böylece Sarmalayici üzerinde Display işlevselliğini kullanabiliriz.

Bu tekniğin dezavantajı, Sarmalayici’ın yeni bir tür olmasıdır; dolayısıyla sardığı değerin metodlarına otomatik olarak sahip değildir. Sarmalayici’ın tam olarak Vec<T> gibi davranmasını istersek, Vec<T> metodlarını Sarmalayici üzerinde tek tek yazar ve bunları self.0’a yönlendiririz. Yeni türün iç türdeki bütün metodlara sahip olmasını istersek Deref trait’ini uygulayıp iç türü döndürmek çözüm olabilir. Bunu 15. bölümdeki “Akıllı İşaretçileri Normal Referanslar Gibi Ele Almak” kısmında incelemiştik. Buna karşılık, Sarmalayici’ın iç türün bütün metodlarını görmesini istemiyorsak, yalnızca istediğimiz metodları elle uygularız.

Newtype deseni yalnızca trait’lerle sınırlı değildir. Trait’lerle devam etmeden önce Rust’ın tür sistemiyle etkileşmenin başka gelişmiş yollarına bakalım.