İçeriğe geç
Educora
Orta20 dk8 / 10

Trait'ler, generic'ler ve yaşam süreleri

Trait'lerle ortak davranış tanımla, trait sınırlarıyla generic fonksiyonlar yaz, `impl Trait` ve `#[derive]` kullan ve yaşam süresi açıklamalarının temellerini kavra.

Kendini test et
Bu derste öğreneceklerin
  • Bir trait bildirmek ve onu farklı türler için uygulamak
  • Trait sınırlı generic fonksiyonlar yazmak, impl Trait ve where kullanmak
  • Debug, Clone ve PartialEq'yu #[derive] ile elde etmek
  • E0106 ve E0597 hatalarını yaşam süreleriyle açıklamak

Telefon, dizüstü bilgisayar ve kulaklık birbirinden tamamen farklı cihazlardır ama hepsi aynı USB-C kablosuyla şarj olur. Kablo onların içini bilmez; onun için önemli olan yalnızca “şarj olabilme” yeteneğidir. Rust'ta bu ortak yeteneğe trait denir. Trait'ler, generic türlerle birlikte bir kez yazılan kodun birçok farklı türle çalışmasını sağlar.

Trait'ler

Trait, bir türün sahip olması gereken metotların listesidir. impl TraitAdı for Tür bloğu bu metotları somut bir tür için yazar. Trait, bir metodun varsayılan gövdesini de verebilir: tür onu olduğu gibi bırakabilir ya da kendi sürümüyle değiştirebilir. Display, Debug ve Clone, standart kütüphanenin trait'leridir.

Rust
trait Describe {
    fn name(&self) -> String;

    fn describe(&self) -> String {
        format!("This is {}", self.name())
    }
}

struct Cat;

struct Car {
    model: String,
}

impl Describe for Cat {
    fn name(&self) -> String {
        String::from("a cat")
    }
}

impl Describe for Car {
    fn name(&self) -> String {
        format!("a car ({})", self.model)
    }

    fn describe(&self) -> String {
        format!("Beep! {}", self.name())
    }
}

fn main() {
    let car = Car { model: String::from("sedan") };
    println!("{}", Cat.describe());
    println!("{}", car.describe());
}
Beklenen çıktı
This is a cat
Beep! a car (sedan)
Cat, varsayılan describe metodunu olduğu gibi bıraktı; Car ise onu kendi sürümüyle değiştirdi. struct Cat; alanı olmayan bir yapıdır.

Generic türler ve trait sınırları

Generic bir fonksiyon, somut bir tür yerine bir tür parametresi alır: fn largest<T>(...). Ama T hakkında hiçbir şey bilinmiyorsa onunla hiçbir şey yapılamaz, karşılaştırma bile. Trait sınırı (T: PartialOrd + Copy) şunu söyler: “T, karşılaştırılabilen ve kopyalanabilen herhangi bir türdür”. Derleyici her somut tür için fonksiyonun ayrı bir kopyasını oluşturur; bu yüzden generic kod çalışma sırasında yavaşlamaz.

Rust
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
    let mut max = items[0];
    for &item in items {
        if item > max {
            max = item;
        }
    }
    max
}

fn main() {
    println!("{}", largest(&[3, 9, 2]));
    println!("{}", largest(&[1.5, 0.5]));
    println!("{}", largest(&['a', 'z', 'm']));
}
Beklenen çıktı
9
1.5
z
E0369 hatası
fn largest<T>(items: &[T]) -> T {
    let mut max = items[0];
    for &item in items {
        if item > max {
            max = item;
        }
    }
    max
}
Sınırlarla doğru
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
    let mut max = items[0];
    for &item in items {
        if item > max {
            max = item;
        }
    }
    max
}
Solda derleyici binary operation > cannot be applied to type T yazar ve T: PartialOrd eklemeyi önerir. Copy ise elemanı listeden kopyalayarak almak için gereklidir.

Daha kısa yazmak için parametrede impl Trait kullanılabilir: item: &impl Display, “Display'i uygulayan herhangi bir tür” demektir. Sınırlar çok olduğunda onları imzadan sonra bir where bloğuna taşımak okunaklılığı artırır. impl Trait dönüş türü olarak da yazılabilir; örneğin uzun bir yineleyici türünün adını yazmamak için.

Rust
use std::fmt::Display;

fn announce(item: &impl Display) {
    println!("[{item}]");
}

fn show_pair<T, U>(a: T, b: U)
where
    T: Display,
    U: Display,
{
    println!("{a} & {b}");
}

fn evens(limit: u32) -> impl Iterator<Item = u32> {
    (1..=limit).filter(|n| n % 2 == 0)
}

fn main() {
    announce(&42);
    announce(&"hello");
    show_pair("Rust", 2015);
    let v: Vec<u32> = evens(10).collect();
    println!("{:?}", v);
}
Beklenen çıktı
[42]
[hello]
Rust & 2015
[2, 4, 6, 8, 10]

#[derive] ile hazır trait'ler

Birçok standart trait'i elle yazmak gerekmez: #[derive(...)] özniteliği onları derleyiciye otomatik olarak ürettirir. Bunun için yapının tüm alanlarının da bu trait'leri uygulaması gerekir.

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

fn main() {
    let a = Point { x: 1, y: 2 };
    let mut b = a.clone();
    println!("{:?} == {:?}: {}", a, b, a == b);
    b.x = 5;
    println!("{:?} == {:?}: {}", a, b, a == b);
}
Beklenen çıktı
Point { x: 1, y: 2 } == Point { x: 1, y: 2 }: true
Point { x: 1, y: 2 } == Point { x: 5, y: 2 }: false
TraitNe sağlar
Debug{:?} ve {:#?} ile yazdırma
Clone.clone() ile tam kopya
Copytaşıma yerine otomatik kopyalama (yalnızca basit alanlarla)
PartialEq, Eq== ve != ile karşılaştırma
PartialOrd, Ord<, > ve sıralama
HashHashMap anahtarı olarak kullanım
DefaultType::default() ile varsayılan değer

Yaşam sürelerinin temelleri

Her referansın bir yaşam süresi (lifetime) vardır: referansın geçerli olduğu kod bölümü. Çoğu zaman derleyici bunu kendisi çıkarır. Ama bir fonksiyon iki referans alıp birini döndürdüğünde derleyici sonucun hangisinden ödünç aldığını bilemez. 'a açıklaması ilişkiyi tanımlar: “sonuç, iki girdiden de uzun yaşamaz”. Açıklama değerlerin ömrünü değiştirmez, yalnızca derleyiciye açıklar.

E0106 hatası
fn longest(a: &str, b: &str) -> &str {
    if a.len() >= b.len() {
        a
    } else {
        b
    }
}
`'a` ile doğru
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() {
        a
    } else {
        b
    }
}
Solda: missing lifetime specifier — this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from a or b. Sağdaki imzayı derleyicinin kendisi önerir.
Rust
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() {
        a
    } else {
        b
    }
}

fn main() {
    let city = String::from("Lankaran");
    let result;
    {
        let other = String::from("Baku");
        result = longest(&city, &other);
        println!("longest: {result}");
    }
    println!("{}", longest("Quba", "Shaki"));
}
Beklenen çıktı
longest: Lankaran
Shaki

println!("longest: {result}") satırını iç bloğun dışına taşırsak kod artık derlenmez. Burada sonuç aslında city'dir; ama imzaya göre other'dan da ödünç almış olabilir ve other bloğun sonunda düşürülür:

Text
error[E0597]: `other` does not live long enough
  --> src/main.rs:14:33
   |
13 |         let other = String::from("Baku");
   |             ----- binding `other` declared here
14 |         result = longest(&city, &other);
   |                                 ^^^^^^ borrowed value does not live long enough
15 |     }
   |     - `other` dropped here while still borrowed
16 |     println!("longest: {result}");
   |                        -------- borrow later used here

Önemli noktalar

  • Trait ortak davranışı tanımlar; impl Trait for Tür onu somut bir tür için uygular, varsayılan metotlar ise değiştirilebilir.
  • Generic fonksiyon <T: Sınır> ile yazılır; sınır, T ile neler yapılabileceğini belirtir.
  • Derleyici her tür için ayrı kod üretir; bu yüzden generic kod hız kaybetmez.
  • #[derive(Debug, Clone, PartialEq)], sık gereken trait'leri otomatik olarak üretir.
  • 'a yaşam süresi açıklaması referanslar arasındaki ilişkiyi tanımlar, ömrü değiştirmez.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
Rust'ta trait nedir?