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

ใช้ Trait Object เพื่อนามธรรมพฤติกรรมร่วม

ในบทที่ 8 เรากล่าวว่าข้อจำกัดหนึ่งของ vector คือพวกมันเก็บ element ของเพียงหนึ่ง type ได้ เราสร้างวิธีแก้ใน Listing 8-9 ที่เรานิยาม enum SpreadsheetCell ที่มี variant เพื่อเก็บ integer, float และ text นี่หมายความว่าเราเก็บ type ต่างกันของข้อมูลในแต่ละ cell และยัง มี vector ที่แทน row ของ cell ได้ นี่เป็นวิธีแก้ดีสมบูรณ์เมื่อ item แลกเปลี่ยนได้ของเราคือชุด type คงที่ที่เรารู้เมื่อโค้ดของเราถูก คอมไพล์

อย่างไรก็ตาม บางครั้งเราต้องการให้ user ของ library ของเราขยายชุด type ที่ valid ในสถานการณ์เฉพาะ เพื่อแสดงว่าเราบรรลุนี้ยังไงได้ เรา จะสร้างเครื่องมือ GUI ตัวอย่างที่ iterate ผ่าน list ของ item เรียก เมธอด draw บนแต่ละตัวเพื่อ draw มันบนหน้าจอ — เทคนิคทั่วไปสำหรับ เครื่องมือ GUI เราจะสร้าง library crate ชื่อ gui ที่บรรจุโครงสร้าง ของ library GUI crate นี้อาจรวม type บางตัวให้คนใช้ เช่น Button หรือ TextField นอกจากนี้ user gui จะต้องการสร้าง type ของพวก เขาเองที่ draw ได้ — ตัวอย่างเช่น programmer หนึ่งอาจเพิ่ม Image และอีกคนอาจเพิ่ม SelectBox

ตอนเขียน library เรารู้และนิยาม type ทั้งหมดที่ programmer อื่นอาจ ต้องการสร้างไม่ได้ แต่เรารู้ว่า gui ต้องตามค่าหลายค่าของ type ต่างกัน และมันต้องเรียกเมธอด draw บนแต่ละค่าที่ type ต่างเหล่านี้ มันไม่ต้องรู้แน่นอนว่าจะเกิดอะไรเมื่อเราเรียกเมธอด draw เพียงว่า ค่าจะมีเมธอดนั้นใช้ได้สำหรับเราเรียก

เพื่อทำสิ่งนี้ในภาษากับ inheritance เราอาจนิยาม class ชื่อ Component ที่มีเมธอดชื่อ draw บนมัน class อื่น เช่น Button, Image และ SelectBox จะสืบทอดจาก Component และดังนั้นสืบทอด เมธอด draw พวกมันแต่ละตัว override เมธอด draw เพื่อนิยามพฤติกรรม custom ของพวกมันได้ แต่ framework จะปฏิบัติกับ type ทั้งหมดเหมือน พวกมันเป็น instance Component และเรียก draw บนพวกมัน แต่เพราะ Rust ไม่มี inheritance เราต้องการอีกวิธีในการจัดโครงสร้าง library gui เพื่ออนุญาตให้ user สร้าง type ใหม่ที่เข้ากันได้กับ library

นิยาม Trait สำหรับพฤติกรรมร่วม

เพื่อ implement พฤติกรรมที่เราต้องการให้ gui มี เราจะนิยาม trait ชื่อ Draw ที่จะมีหนึ่งเมธอดชื่อ draw จากนั้น เรานิยาม vector ที่รับ trait object ได้ trait object ชี้ไปยังทั้ง instance ของ type ที่ implement trait ที่เราระบุและ table ที่ใช้ค้นหาเมธอด trait บน type นั้นที่ runtime เราสร้าง trait object โดยระบุ pointer บางประเภท เช่น reference หรือ smart pointer Box<T> แล้ว keyword dyn แล้วระบุ trait ที่เกี่ยวข้อง (เราจะพูดถึงเหตุผล ที่ trait object ต้องใช้ pointer ใน “Type ขนาด Dynamic และ Trait Sized ในบทที่ 20) เราใช้ trait object ในที่ของ generic หรือ type คอนกรีต ที่ใดที่เราใช้ trait object ระบบ type ของ Rust จะรับ ประกันที่ compile time ว่าค่าใดที่ใช้ใน context นั้นจะ implement trait ของ trait object ผลคือ เราไม่ต้องรู้ type ทั้งหมดที่เป็นไป ได้ที่ compile time

เรากล่าวว่าใน Rust เราละเว้นจากการเรียก struct และ enum “object” เพื่อแยกพวกมันจาก object ของภาษาอื่น ใน struct หรือ enum ข้อมูล ใน field struct และพฤติกรรมใน block impl ถูกแยก ในขณะที่ในภาษา อื่น ข้อมูลและพฤติกรรมที่รวมเป็นแนวคิดเดียวมักถูก label เป็น object Trait object ต่างจาก object ในภาษาอื่นในที่เราเพิ่มข้อมูลให้ trait object ไม่ได้ Trait object ไม่มีประโยชน์ทั่วไปเท่า object ในภาษา อื่น — จุดประสงค์เฉพาะของพวกมันคือการอนุญาต abstraction ข้ามพฤติกรรม ร่วม

Listing 18-3 แสดงวิธีนิยาม trait ชื่อ Draw ที่มีหนึ่งเมธอดชื่อ draw

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}
Listing 18-3: นิยามของ trait Draw

syntax นี้ควรดูคุ้นเคยจากการพูดคุยของเราเกี่ยวกับวิธีนิยาม trait ในบทที่ 10 ถัดมาคือ syntax ใหม่ — Listing 18-4 นิยาม struct ชื่อ Screen ที่เก็บ vector ชื่อ components vector นี้เป็น type Box<dyn Draw> ซึ่งคือ trait object — มันเป็นตัวยืนแทนสำหรับ type ใดภายใน Box ที่ implement trait Draw

Filename: src/lib.rs
pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}
Listing 18-4: นิยามของ struct Screen ที่มี field components เก็บ vector ของ trait object ที่ implement trait Draw

บน struct Screen เราจะนิยามเมธอดชื่อ run ที่จะเรียกเมธอด draw บนแต่ละ components ของมัน ดังที่แสดงใน Listing 18-5

Filename: 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();
        }
    }
}
Listing 18-5: เมธอด run บน Screen ที่เรียกเมธอด draw บนแต่ละ component

นี่ทำงานต่างจากการนิยาม struct ที่ใช้ generic type parameter กับ trait bound generic type parameter ถูกทดแทนด้วยเพียงหนึ่ง type คอนกรีตในเวลาเดียวกัน ในขณะที่ trait object อนุญาตให้หลาย type คอนกรีตเติมแทน trait object ที่ runtime ตัวอย่างเช่น เรานิยาม struct Screen โดยใช้ generic type และ trait bound ดังใน Listing 18-6 ได้

Filename: 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();
        }
    }
}
Listing 18-6: Implementation ทางเลือกของ struct Screen และเมธอด run ของมันโดยใช้ generic และ trait bound

นี่จำกัดเราที่ instance Screen ที่มี list ของ component ทั้งหมด ของ type Button หรือทั้งหมดของ type TextField ถ้าคุณจะมี collection homogeneous เท่านั้น การใช้ generic และ trait bound ดีกว่าเพราะนิยามจะถูก monomorphize ที่ compile time เพื่อใช้ type คอนกรีต

ในทางตรงกันข้าม ด้วยวิธีที่ใช้ trait object instance Screen หนึ่ง เก็บ Vec<T> ที่บรรจุ Box<Button> รวมถึง Box<TextField> ได้ มาดูว่านี่ทำงานยังไง แล้วเราจะพูดถึงผลกระทบ performance ที่ runtime

Implement Trait

ตอนนี้เราจะเพิ่ม type บางตัวที่ implement trait Draw เราจะให้ type Button อีกครั้ง การ implement library GUI จริง ๆ อยู่นอก scope ของหนังสือเล่มนี้ ดังนั้นเมธอด draw จะไม่มี implementation ที่ มีประโยชน์ใน body ของมัน เพื่อจินตนาการว่า implementation อาจดู ยังไง struct Button อาจมี field สำหรับ width, height และ label ดังที่แสดงใน Listing 18-7

Filename: 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
    }
}
Listing 18-7: struct Button ที่ implement trait Draw

field width, height และ label บน Button จะต่างจาก field บน component อื่น — ตัวอย่างเช่น type TextField อาจมี field เดียวกันเหล่านั้นบวก field placeholder แต่ละ type ที่เราต้องการ draw บนหน้าจอจะ implement trait Draw แต่จะใช้โค้ดต่างกันในเมธอด draw เพื่อนิยามว่าจะ draw type นั้นยังไง เช่นที่ Button มีที่ นี่ (โดยไม่มีโค้ด GUI จริง ดังที่กล่าว) type Button ตัวอย่างเช่น อาจมี block impl เพิ่มที่บรรจุเมธอดที่เกี่ยวกับสิ่งที่เกิดเมื่อ user click ปุ่ม เมธอดประเภทเหล่านี้จะไม่ใช้กับ type เช่น TextField

ถ้าใครที่ใช้ library ของเราตัดสินใจ implement struct SelectBox ที่มี field width, height และ options พวกเขาจะ implement trait Draw บน type SelectBox ด้วย ดังที่แสดงใน Listing 18-8

Filename: 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() {}
Listing 18-8: อีก crate ที่ใช้ gui และ implement trait Draw บน struct SelectBox

user ของ library เรา ตอนนี้เขียนฟังก์ชัน main ของพวกเขาเพื่อสร้าง instance Screen ได้ ให้ instance Screen พวกเขาเพิ่ม SelectBox และ Button โดยใส่แต่ละตัวใน Box<T> เพื่อกลายเป็น trait object ได้ พวกเขาจากนั้นเรียกเมธอด run บน instance Screen ซึ่งจะเรียก draw บนแต่ละ component Listing 18-9 แสดง implementation นี้

Filename: 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();
}
Listing 18-9: ใช้ trait object เพื่อเก็บค่าของ type ต่างกันที่ implement trait เดียวกัน

เมื่อเราเขียน library เราไม่รู้ว่าใครอาจเพิ่ม type SelectBox แต่ implementation Screen ของเราสามารถ operate บน type ใหม่และ draw มันได้เพราะ SelectBox implement trait Draw ซึ่งหมายความว่ามัน implement เมธอด draw

แนวคิดนี้ — ของการเป็นกังวลเพียงกับข้อความที่ค่าตอบสนองไม่ใช่ type คอนกรีตของค่า — คล้ายกับแนวคิดของ duck typing ในภาษาที่ type แบบ dynamic — ถ้ามันเดินเหมือนเป็ดและร้องก๊าบเหมือนเป็ด มันต้องเป็น เป็ด! ใน implementation ของ run บน Screen ใน Listing 18-5 run ไม่ต้องรู้ว่า type คอนกรีตของแต่ละ component คืออะไร มันไม่ ตรวจสอบว่า component เป็น instance ของ Button หรือ SelectBox มันเพียงเรียกเมธอด draw บน component โดยระบุ Box<dyn Draw> เป็น type ของค่าใน vector components เรานิยาม Screen ให้ต้องการค่า ที่เราเรียกเมธอด draw บนได้

ข้อดีของการใช้ trait object และระบบ type ของ Rust ในการเขียนโค้ด คล้ายกับโค้ดที่ใช้ duck typing คือเราไม่เคยต้องตรวจสอบว่าค่า implement เมธอดเฉพาะที่ runtime หรือกังวลเกี่ยวกับการได้ error ถ้าค่าไม่ implement เมธอดแต่เราเรียกมันต่อ Rust จะไม่คอมไพล์โค้ด ของเราถ้าค่าไม่ implement trait ที่ trait object ต้องการ

ตัวอย่างเช่น Listing 18-10 แสดงสิ่งที่เกิดถ้าเราพยายามสร้าง Screen กับ String เป็น component

Filename: src/main.rs
use gui::Screen;

fn main() {
    let screen = Screen {
        components: vec![Box::new(String::from("Hi"))],
    };

    screen.run();
}
Listing 18-10: พยายามใช้ type ที่ไม่ implement trait ของ trait object

เราจะได้ error นี้เพราะ String ไม่ implement trait Draw:

$ 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`
  |
  = help: the trait `Draw` is implemented for `Button`
  = note: required for the cast from `Box<String>` to `Box<dyn Draw>`

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

error นี้ให้เรารู้ว่าเรากำลังส่งอะไรให้ Screen ที่เราไม่ได้ตั้งใจ ส่ง และดังนั้นควรส่ง type ต่างกัน หรือเราควร implement Draw บน String เพื่อให้ Screen เรียก draw บนมันได้

ทำ Dynamic Dispatch

จำได้ใน “Performance ของโค้ดที่ใช้ Generic” ในบทที่ 10 การพูดคุยของเราเกี่ยวกับกระบวนการ monomorphization ที่ ทำบน generic โดย compiler — compiler สร้าง implementation ที่ไม่ใช่ generic ของฟังก์ชันและเมธอดสำหรับแต่ละ type คอนกรีตที่เราใช้แทน generic type parameter โค้ดที่ได้จาก monomorphization กำลังทำ static dispatch ซึ่งคือเมื่อ compiler รู้ว่าเมธอดไหนที่คุณกำลัง เรียกที่ compile time นี่ตรงข้ามกับ dynamic dispatch ซึ่งคือเมื่อ compiler บอกที่ compile time ไม่ได้ว่าเมธอดไหนที่คุณกำลังเรียก ใน กรณี dynamic dispatch compiler ปล่อยโค้ดที่ที่ runtime จะรู้ว่า เมธอดไหนที่จะเรียก

เมื่อเราใช้ trait object Rust ต้องใช้ dynamic dispatch compiler ไม่รู้ type ทั้งหมดที่อาจถูกใช้กับโค้ดที่ใช้ trait object ดังนั้น มันไม่รู้ว่าเมธอดที่ implement บน type ไหนที่จะเรียก แทน ที่ runtime Rust ใช้ pointer ภายใน trait object เพื่อรู้ว่าเมธอดไหน ที่จะเรียก การค้นหานี้มีค่าใช้จ่ายที่ runtime ที่ไม่เกิดกับ static dispatch Dynamic dispatch ยังป้องกัน compiler จากการเลือก inline โค้ดของ เมธอด ซึ่งในทางกลับป้องกัน optimization บางอย่าง และ Rust มีกฎบาง ตัวเกี่ยวกับที่ที่คุณใช้ dynamic dispatch ได้และใช้ไม่ได้ เรียก dyn compatibility กฎเหล่านั้นอยู่นอก scope ของการพูดคุยนี้ แต่ คุณอ่านเพิ่มเกี่ยวกับพวกมันได้ ใน reference อย่างไรก็ตาม เรา ได้ความยืดหยุ่นเพิ่มในโค้ดที่เราเขียนใน Listing 18-5 และสามารถ สนับสนุนใน Listing 18-9 ดังนั้นมันเป็น trade-off ที่จะพิจารณา