球魚

你己經被幸運螃蟹造訪了

OOP 定義

Object-oriented programs are made up of objects.

An object packages both data and the procedures that operate on that data.

The procedures are typically called methods or operations.

Rust is object oriented

  • structs and enums have  data
  • impl blocks provides methods
struct Circle {
    radius: f64;
}
enum Number {
    Int(i32),
    Float(f32).
}
trait Shape {
    fn area(self: &Self) -> f64;
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        self.radius * self.radius * 3.14
    }
}

Encapsulation

  • pub 關鍵字 - 第七章

Inheritance

  • Rust 沒有繼承的機制
  • 但你有其他的方式達成繼承的好處

繼承的好處與用 Rust 達成

  • reuse of code - 透過 impl trait 達成
trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

impl Summary for NewsArticle {}

let article = NewsArticle {
    headline: String::from("Wow!"),
    content: String::from("The is an apple."),
};

println!("New article available! {}", article.summarize());

// -> New article available! (Read more...)

繼承的好處與用 Rust 達成

  • 實現 polymorphism
  • 一種實現方式是利用泛型 (static dispatch)
  • 另一種是 trait object (dynamic dispatch)

Trait Object

Trait

  • 屬於 DST 類型
  • 大小在編譯階段不是固定的,因此無法作為參數類型或返回類型
trait Shape {
    fn area(self: &Self) -> f64;
}
fn test (arg: Shape) {} // => X

fn test() =-> Shape {} // => X

Trait Object

  • 是指向 trait 的指針,由於指向 DST 類型,所以是胖指針
  • trait object 的 function 呼叫,是用在編譯階段建立虛函數表,然後執行階段查表呼叫
  • EX: &dyn Shape、&mut dyn Shape、Box<dyn Shape>、
    * const dyn Shape、* mut dyn Shape、Rc<dyn Shape>
trait Shape {
    fn area(self: &Self) -> f64;
}

GUI 範例

假設今天要製作 GUI 相關的 library,預期會有像 Button、Image、Text 之類的 component,所有的 component 都有 function draw

GUI 範例 - 使用泛型實現

首先寫出 Draw trait

trait Draw {
    fn draw(&self);
}

再來,有一個 Screen 的 struct 紀錄要被打印的 component

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

Screen 打印 component 的 function

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

GUI 範例 - 使用泛型實現

  • 屬於 static dispatch
  • 在編譯期間就確定了 T 的型態,並且在編譯階段把 function 自動展開
  • 但會變成一個 Screen 的所有 Component 都必須是同一種型態
  • 變成要這樣使用 Screen<Button>、Screen<Image>

GUI 範例 - 使用Trait Object實現

首先寫出 Draw trait

trait Draw {
    fn draw(&self);
}

再來,有一個 Screen 的 struct 紀錄要被打印的 component


struct Screen {
    components: Vec<Box<dyn Draw>>,
}
// Box<dyn Draw> 就是 trait object

Screen 打印 component 的 function

impl Screen {
    fn run(&self) {
        for component in self.components.iter() {
            component.draw(); // 有自動 deref
        }
    }
}

GUI 範例 - 使用Trait Object實現

  • 屬於 dynamic dispatch
  • 在 runtime 期間才確定型態,查虛函數表並呼叫
  • 可以解決 static dispatch 的缺點

Object safe 與 Trait Object 限制

有些 trait 不是 object safe,不是 object safe 的 trait 就無法使用 trait object

以下列舉幾種可能會在編譯期間報錯的情況

  1. 當 trait 有  Self: Sized 時
  2. 當 function 中有 Self 作為參數或返回類型時
  3. 當 function 的第一個參數不是 self 時
  4. 當 function 有泛型參數時

1. 當 trait 有  Self: Sized 時

  • Sized 是 Rust 裡特別的 trait
  • 我們不能 impl Sized 這個 trait
  • Sized 這個 trait 完全是由編譯器自行推導
  • 被冠上 Sized trait 的 function,會從虛函數表中被剔除
  • 能在編譯期間就確定空間大小的型態,都滿足 Sized trait,反之,trait 是 runtime 期間決定空間大小,就不滿足 Sized trait
  • 由於 trait 不滿足 Sized trait,若冠上 Self: Sized 的 trait 的 trait object 被呼叫了,就會在編譯期間被阻擋下來

1. 當 trait 有  Self: Sized 時

第一個例子 Sized 作用在 Foo trait 本身上,當建構 trait object 時,Sized 需要在編譯期間確定空間大小, 因無法確定 Foo trait 的大小而編譯失敗

trait Foo where Self: Sized {
    fn foo(&self);
}

impl Foo for i32 {
    fn foo(&self) { println!("{}", self); }
}

fn main () {
    let x = 1_i32;
    x.foo();
    let p = &x as &dyn Foo; // 這邊會編譯失敗
    p.foo();
}

1. 當 trait 有  Self: Sized 時

  • 第二個例子 Sized 作用在 foo function 上
  • 我們建構一個 trait object 的實體,但當用 trait object 呼叫 foo function 時,因為 Sized 無法在編譯期間確定 Self 空間大小,而編譯失敗
trait Foo {
    fn foo(&self) where Self: Sized;
}

impl Foo for i32 {
    fn foo(&self) { println!("{}", self); }
}

fn main () {
    let x = 1_i32;
    x.foo();
    let p = &x as &dyn Foo;
    p.foo(); // 這邊會編譯失敗
}

2. 當 function 中有 Self 作為參數或返回類型時

  • 當 trait 的 function 中,有任一 function 的回傳值或參數類型是 Self 時,這個 trait 就不是 object safe
  • 一個實際的例子是 Clone trait,Clone trait 的 clone function,會回傳 Self 類型
  • 由於 trait object 不會在編譯期間知道型態,編譯器無法確定 Self 類型,對編譯器來說就是使用者「可能去做一些他達不到的任務」他就會在一開始阻止使用者使用不是 object safe 的 trait object
fn main() {
    let s = String::new();
    let p: &dyn Clone = &s as &dyn Clone(); // 這邊會報錯
}

// 同理剛剛的 Screen 例子
struct Screen {
    pub components: Vec<Box<dyn Clone>>, // 這邊會報錯
}

2. 當 function 中有 Self 作為參數或返回類型時

  • 但有的時候,一個 trait 裡不是所有 function,我們都會用到,然而卻因為某些 function 導致整個 trait 被列入不是 object safe,以至於無法使用 trait object,有點因小失大,因此還是有些方式可以繞過編議器的檢查
  • 上一條說「冠上 Self: Sized 的 function 會在編譯階段從虛函數表中剔除」,如果參數或回傳類型是 Self 的 function 冠上了 Self: Sized,這個 trait 就會變成 object safe
  • 其實可以這樣想,被冠上 Self: Sized 的 function 基本上你就不能呼叫他了 (因為呼叫的話編譯就會不過),那編譯器就可以確定那些會造成不確定因素的 function 被排除了,就會把 trait 變成 object safe
trait Foo {
    fn new() -> Self where Self: Sized;
    fn foo(&self);
}
// Foo is object safe

3. 當 function 的第一個參數不是 self 時

  • 其實就是當 trait 裡有靜態方法時,這個 trait 就不是 object safe,而無法使用 trait object
  • 同理第二條,我們可以在靜態方法冠上 Self: Sized,把靜態方法從虛函數表中排除,使 trait 變成 object safe

4. 當 function 有泛型參數時

  • Rust 禁止使用 trait object 呼叫泛型方法
  • 因為 trait object,使用虛函數表查表呼叫,然而泛型是在編譯期間被靜態展開的,本質上就有衝突
trait Foo {
    fn foo<T>(&self, value: T);
}

fn fail(x: &dyn Foo) {
    x.foo("bar"); // T = &str
    x.foo(1_u8); // T = u8
    // 編譯器表示:黑人問號??
}

結論

  • 冠上 Self: Sized 的 trait 的 function 就無法用 trait object 呼叫
  • 不是 object safe 就無法使用 trait object
    • function 中有參數或回傳值是 Self
    • function 中有泛型或靜態方法
  • 用 Self: Sized 可以迴避 Self 問題與靜態方法問題

工商時間

關於讀書會之後
可以幹嘛?

話說……

我原本是想要開個黑客松之類的活動

所以有跟一個 Rust 大大,要了他開的課程的免費邀請碼兩組

原本是要當獎品的,要都要了,我還是想把他當獎品

課程是這個:https://www.packtpub.com/application-development/building-reusable-code-rust-video

感謝大家的聆聽

有沒有什麼問題

Object-Oriented and Rust

By 球魚

Object-Oriented and Rust

  • 1,115