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 ขั้นสูง

เราครอบคลุม trait ครั้งแรกในส่วน “นิยามพฤติกรรมที่แชร์ด้วย Trait” ในบทที่ 10 แต่เราไม่ได้พูดถึงรายละเอียด ขั้นสูง ตอนนี้คุณรู้มากขึ้นเกี่ยวกับ Rust เราสามารถเข้าไปในส่วนเล็ก ๆ น้อย ๆ

นิยาม Trait ด้วย Associated Type

Associated type เชื่อม type placeholder กับ trait เพื่อให้นิยามเมธอด trait ใช้ placeholder type เหล่านี้ใน signature ของพวกมัน Implementor ของ trait จะระบุ type concrete ที่จะใช้แทน placeholder type สำหรับ implementation เฉพาะ ด้วยวิธีนั้น เรานิยาม trait ที่ใช้ type บางอย่าง โดยไม่ต้องรู้ว่า type เหล่านั้นเป็นอะไรแน่ ๆ จนกระทั่ง trait ถูก implement ได้

เราอธิบายฟีเจอร์ขั้นสูงส่วนใหญ่ในบทนี้ว่าจำเป็นแทบไม่ Associated type อยู่ที่ไหนสักแห่งตรงกลาง — พวกมันถูกใช้แทบไม่กว่าฟีเจอร์ที่อธิบายในส่วน อื่นของหนังสือ แต่ปกติมากกว่าหลายฟีเจอร์อื่นที่พูดถึงในบทนี้

ตัวอย่างของ trait ที่มี associated type คือ trait Iterator ที่ standard library ให้ Associated type ถูก named Item และยืนแทน type ของค่าที่ type ที่ implement trait Iterator กำลัง iterate บน นิยาม ของ trait Iterator เป็นดังที่แสดงใน Listing 20-13

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-13: นิยามของ trait Iterator ที่มี associated type Item

type Item คือ placeholder และนิยามของเมธอด next แสดงว่ามันจะ return ค่าของ type Option<Self::Item> Implementor ของ trait Iterator จะ ระบุ type concrete สำหรับ Item และเมธอด next จะ return Option ที่ บรรจุค่าของ type concrete นั้น

Associated type อาจดูเหมือนแนวคิดคล้ายกับ generic ที่อย่างหลังอนุญาตให้ เรานิยามฟังก์ชันโดยไม่ระบุว่ามันจัดการ type อะไรได้ เพื่อตรวจสอบความ แตกต่างระหว่างสองแนวคิด เราจะดู implementation ของ trait Iterator บน type ชื่อ Counter ที่ระบุว่า type Item คือ u32:

Filename: src/lib.rs
struct Counter {
    count: u32,
}

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

impl Iterator for Counter {
    type Item = u32;

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

Syntax นี้ดูเทียบเคียงได้กับของ generic แล้วทำไมไม่นิยาม trait Iterator ด้วย generic ดังที่แสดงใน Listing 20-14?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listing 20-14: นิยามสมมติของ trait Iterator โดยใช้ generic

ความแตกต่างคือเมื่อใช้ generic เช่นใน Listing 20-14 เราต้อง annotate type ในแต่ละ implementation — เพราะเราสามารถ implement Iterator<String> for Counter หรือ type อื่นใดด้วย เราจะมีหลาย implementation ของ Iterator สำหรับ Counter ได้ ในคำพูดอื่น เมื่อ trait มี generic parameter มันถูก implement สำหรับ type ได้หลายครั้ง เปลี่ยน type concrete ของ generic type parameter แต่ละครั้ง เมื่อเราใช้ เมธอด next บน Counter เราจะต้องให้ type annotation เพื่อบ่งบอกว่า implementation ใดของ Iterator ที่เราต้องการใช้

ด้วย associated type เราไม่ต้อง annotate type เพราะเราไม่สามารถ implement trait บน type ได้หลายครั้ง ใน Listing 20-13 กับนิยามที่ใช้ associated type เราเลือกว่า type ของ Item จะเป็นอะไรได้เพียงครั้ง เดียวเพราะมีได้เพียงหนึ่ง impl Iterator for Counter เราไม่ต้องระบุว่า เราต้องการ iterator ของค่า u32 ทุกที่ที่เราเรียก next บน Counter

Associated type ก็กลายเป็นส่วนของ contract ของ trait — Implementor ของ trait ต้องให้ type เพื่อยืนแทน associated type placeholder Associated type มักมีชื่อที่อธิบายว่า type จะถูกใช้ยังไง และ document associated type ใน documentation API คือ practice ดี

ใช้ Default Generic Parameter และ Operator Overloading

เมื่อเราใช้ generic type parameter เราระบุ default concrete type สำหรับ generic type ได้ นี่ขจัดความต้องการให้ implementor ของ trait ระบุ concrete type ถ้า default type ทำงาน คุณระบุ default type เมื่อ ประกาศ generic type ด้วย syntax <PlaceholderType=ConcreteType>

ตัวอย่างที่ดีของสถานการณ์ที่เทคนิคนี้มีประโยชน์คือกับ operator overloading ซึ่งคุณ customize พฤติกรรมของ operator (เช่น +) ใน สถานการณ์เฉพาะ

Rust ไม่อนุญาตให้คุณสร้าง operator ของตัวเองหรือ overload operator ใด ๆ แต่คุณ overload operation และ trait ที่ตรงกันที่ list ใน std::ops ได้โดย implement trait ที่ associate กับ operator ตัวอย่าง เช่น ใน Listing 20-15 เรา overload operator + เพื่อเพิ่มสอง instance Point ด้วยกัน เราทำสิ่งนี้โดย implement trait Add บน struct Point

Filename: src/main.rs
use std::ops::Add;

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

impl Add for Point {
    type Output = Point;

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

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
Listing 20-15: Implement trait Add เพื่อ overload operator + สำหรับ instance Point

เมธอด add เพิ่มค่า x ของสอง instance Point และค่า y ของสอง instance Point เพื่อสร้าง Point ใหม่ trait Add มี associated type ชื่อ Output ที่ตัดสิน type ที่ return จากเมธอด add

Default generic type ในโค้ดนี้อยู่ภายใน trait Add นี่คือนิยามของมัน:

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

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

โค้ดนี้ควรดูคุ้นเคยโดยทั่วไป — trait ที่มีหนึ่งเมธอดและ associated type ส่วนใหม่คือ Rhs=Self — syntax นี้ถูกเรียก default type parameter generic type parameter Rhs (ย่อมาจาก “right-hand side”) นิยาม type ของ parameter rhs ในเมธอด add ถ้าเราไม่ระบุ concrete type สำหรับ Rhs เมื่อเรา implement trait Add type ของ Rhs จะ default เป็น Self ซึ่งจะเป็น type ที่เรากำลัง implement Add บน

เมื่อเรา implement Add สำหรับ Point เราใช้ default สำหรับ Rhs เพราะเราต้องการเพิ่มสอง instance Point มาดูตัวอย่างของการ implement trait Add ที่เราต้องการ customize type Rhs แทนการใช้ default

เรามีสอง struct, Millimeters และ Meters ที่บรรจุค่าในหน่วยต่างกัน การห่อบางของ type ที่มีใน struct อื่นนี้เรียก newtype pattern ซึ่ง เราอธิบายในรายละเอียดมากขึ้นในส่วน “Implement External Trait ด้วย Newtype Pattern” เราต้องการเพิ่มค่าใน millimeter ให้ค่าใน meter และมี implementation ของ Add ทำ conversion ถูก เรา implement Add สำหรับ Millimeters ด้วย Meters เป็น Rhs ได้ ดังที่แสดงใน Listing 20-16

Filename: src/lib.rs
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
Listing 20-16: Implement trait Add บน Millimeters เพื่อเพิ่ม Millimeters และ Meters

เพื่อเพิ่ม Millimeters และ Meters เราระบุ impl Add<Meters> เพื่อ ตั้งค่าของ type parameter Rhs แทนการใช้ default ของ Self

คุณจะใช้ default type parameter ในสองวิธีหลัก:

  1. เพื่อขยาย type โดยไม่ break โค้ดที่มี
  2. เพื่ออนุญาต customization ในกรณีเฉพาะที่ user ส่วนใหญ่ไม่ต้องการ

trait Add ของ standard library คือตัวอย่างของจุดประสงค์ที่สอง — ปกติ คุณจะเพิ่มสอง type ที่เหมือนกัน แต่ trait Add ให้ความสามารถใน การ customize เกินกว่านั้น ใช้ default type parameter ในนิยามของ trait Add หมายความว่าคุณไม่ต้องระบุ parameter เพิ่มส่วนใหญ่ของเวลา ในคำพูด อื่น boilerplate implementation เล็กน้อยไม่จำเป็น ทำให้ง่ายขึ้นที่จะ ใช้ trait

จุดประสงค์แรกคล้ายกับที่สองแต่กลับกัน — ถ้าคุณต้องการเพิ่ม type parameter ให้ trait ที่มี คุณให้ default มันเพื่ออนุญาตการขยายของ functionality ของ trait โดยไม่ break โค้ด implementation ที่มี

Disambiguate ระหว่างเมธอดที่ตั้งชื่อเหมือนกัน

ไม่มีอะไรใน Rust ป้องกัน trait จากการมีเมธอดที่มีชื่อเดียวกับเมธอดของ trait อื่น และ Rust ไม่ป้องกันคุณจากการ implement ทั้งสอง trait บนหนึ่ง type มันเป็นไปได้ที่จะ implement เมธอดโดยตรงบน type ที่มีชื่อเดียวกับ เมธอดจาก trait

เมื่อเรียกเมธอดที่มีชื่อเดียวกัน คุณจะต้องบอก Rust ว่าอันไหนที่คุณ ต้องการใช้ พิจารณาโค้ดใน Listing 20-17 ที่เรานิยามสอง trait, Pilot และ Wizard ที่ทั้งสองมีเมธอดเรียก fly แล้วเรา implement ทั้งสอง trait บน type Human ที่มีเมธอดชื่อ fly ที่ implement บนมันแล้ว เมธอด fly แต่ละทำสิ่งที่ต่างกัน

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

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}
Listing 20-17: สอง trait ถูกนิยามให้มีเมธอด fly และถูก implement บน type Human และเมธอด fly ถูก implement บน Human โดยตรง

เมื่อเราเรียก fly บน instance ของ Human, compiler default ไปเรียก เมธอดที่ถูก implement บน type โดยตรง ดังที่แสดงใน Listing 20-18

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

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
Listing 20-18: เรียก fly บน instance ของ Human

รันโค้ดนี้จะ print *waving arms furiously* แสดงว่า Rust เรียกเมธอด fly ที่ implement บน Human โดยตรง

เพื่อเรียกเมธอด fly จาก trait Pilot หรือ trait Wizard เราต้องใช้ syntax ที่ชัดเจนกว่าเพื่อระบุว่าเมธอด fly ใดที่เราหมายถึง Listing 20-19 สาธิต syntax นี้

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

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
Listing 20-19: ระบุว่าเมธอด fly ของ trait ใดที่เราต้องการเรียก

ระบุชื่อ trait ก่อนชื่อเมธอดทำให้ชัดเจนกับ Rust ว่า implementation ใด ของ fly ที่เราต้องการเรียก เรายังเขียน Human::fly(&person) ได้ ซึ่งเทียบเท่ากับ person.fly() ที่เราใช้ใน Listing 20-19 แต่นี่ยาวกว่า เล็กน้อยถ้าเราไม่ต้อง disambiguate

รันโค้ดนี้ print ต่อไปนี้:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

เพราะเมธอด fly รับ parameter self ถ้าเรามีสอง type ที่ทั้งสอง implement หนึ่ง trait, Rust คำนวณว่า implementation ใดของ trait ที่ จะใช้ตาม type ของ self ได้

อย่างไรก็ตาม associated function ที่ไม่ใช่เมธอดไม่มี parameter self เมื่อมีหลาย type หรือ trait ที่นิยามฟังก์ชันไม่ใช่เมธอดที่มีชื่อ ฟังก์ชันเดียวกัน Rust ไม่รู้เสมอว่า type ใดที่คุณหมายถึงเว้นแต่คุณใช้ fully qualified syntax ตัวอย่างเช่น ใน Listing 20-20 เราสร้าง trait สำหรับ shelter สัตว์ที่ต้องการ name ลูก dog ทั้งหมด Spot เราทำ trait Animal กับ associated non-method function baby_name trait Animal ถูก implement สำหรับ struct Dog ที่เราให้ associated non-method function baby_name โดยตรงด้วย

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 20-20: trait ที่มี associated function และ type ที่มี associated function ของชื่อเดียวกันที่ implement trait ด้วย

เรา implement โค้ดสำหรับ naming puppy ทั้งหมด Spot ใน associated function baby_name ที่นิยามบน Dog type Dog ก็ implement trait Animal ซึ่งอธิบายลักษณะที่สัตว์ทั้งหมดมี ลูก dog ถูกเรียก puppy และ นั่นถูกแสดงใน implementation ของ trait Animal บน Dog ในฟังก์ชัน baby_name ที่ associate กับ trait Animal

ใน main เราเรียกฟังก์ชัน Dog::baby_name ซึ่งเรียก associated function ที่นิยามบน Dog โดยตรง โค้ดนี้ print ต่อไปนี้:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

Output นี้ไม่ใช่สิ่งที่เราต้องการ เราต้องการเรียกฟังก์ชัน baby_name ที่เป็นส่วนของ trait Animal ที่เรา implement บน Dog เพื่อให้โค้ด print A baby dog is called a puppy เทคนิคของระบุชื่อ trait ที่เราใช้ ใน Listing 20-19 ไม่ช่วยที่นี่ — ถ้าเราเปลี่ยน main เป็นโค้ดใน Listing 20-21 เราจะได้ error compilation

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 20-21: พยายามเรียกฟังก์ชัน baby_name จาก trait Animal แต่ Rust ไม่รู้ว่า implementation ใดที่จะใช้

เพราะ Animal::baby_name ไม่มี parameter self และอาจมี type อื่นที่ implement trait Animal, Rust ไม่สามารถคำนวณว่า implementation ใดของ Animal::baby_name ที่เราต้องการ เราจะได้ error compiler นี้:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

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

เพื่อ disambiguate และบอก Rust ว่าเราต้องการใช้ implementation ของ Animal สำหรับ Dog ตรงข้ามกับ implementation ของ Animal สำหรับ type อื่น เราต้องใช้ fully qualified syntax Listing 20-22 สาธิตวิธีใช้ fully qualified syntax

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listing 20-22: ใช้ fully qualified syntax เพื่อระบุว่าเราต้องการเรียกฟังก์ชัน baby_name จาก trait Animal ตามที่ implement บน Dog

เรากำลังให้ Rust type annotation ภายใน angle bracket ซึ่งบ่งบอกว่าเรา ต้องการเรียกเมธอด baby_name จาก trait Animal ตามที่ implement บน Dog โดยบอกว่าเราต้องการ treat type Dog เป็น Animal สำหรับการ เรียกฟังก์ชันนี้ โค้ดนี้ตอนนี้จะ print สิ่งที่เราต้องการ:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

โดยทั่วไป fully qualified syntax ถูกนิยามแบบนี้:

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

สำหรับ associated function ที่ไม่ใช่เมธอด จะไม่มี receiver — จะมี เพียง list ของ argument อื่น คุณใช้ fully qualified syntax ทุกที่ที่ คุณเรียกฟังก์ชันหรือเมธอดได้ อย่างไรก็ตาม คุณถูกอนุญาตให้ละส่วนใดของ syntax นี้ที่ Rust คำนวณจากข้อมูลอื่นในโปรแกรมได้ คุณเพียงต้องใช้ syntax verbose มากขึ้นนี้ในกรณีที่มีหลาย implementation ที่ใช้ชื่อ เดียวกันและ Rust ต้องการความช่วยเหลือในการระบุ implementation ใดที่ คุณต้องการเรียก

ใช้ Supertrait

บางครั้งคุณอาจเขียนนิยาม trait ที่ขึ้นกับ trait อื่น — สำหรับ type ที่ จะ implement trait แรก คุณต้องการให้ type นั้น implement trait ที่สองด้วย คุณจะทำสิ่งนี้เพื่อให้นิยาม trait ของคุณใช้ประโยชน์จาก associated item ของ trait ที่สองได้ trait ที่นิยาม trait ของคุณพึ่งพา ถูกเรียก supertrait ของ trait ของคุณ

ตัวอย่างเช่น สมมุติเราต้องการทำ trait OutlinePrint กับเมธอด outline_print ที่จะ print ค่าที่ให้ formatted เพื่อให้มันถูก frame ใน asterisk นั่นคือ ให้ struct Point ที่ implement trait Display ของ standard library เพื่อส่งผลเป็น (x, y) เมื่อเราเรียก outline_print บน instance Point ที่มี 1 สำหรับ x และ 3 สำหรับ y มันควร print ต่อไปนี้:

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

ใน implementation ของเมธอด outline_print เราต้องการใช้ functionality ของ trait Display ดังนั้น เราต้องระบุว่า trait OutlinePrint จะ ทำงานเพียงสำหรับ type ที่ implement Display ด้วยและให้ functionality ที่ OutlinePrint ต้องการ เราทำนั้นได้ในนิยาม trait โดยระบุ OutlinePrint: Display เทคนิคนี้คล้ายกับการเพิ่ม trait bound ให้ trait Listing 20-23 แสดง implementation ของ trait OutlinePrint

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: 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: Implement trait OutlinePrint ที่ต้องการ functionality จาก Display

เพราะเราระบุว่า OutlinePrint ต้องการ trait Display เราใช้ฟังก์ชัน to_string ที่ถูก implement อัตโนมัติสำหรับ type ใดที่ implement Display ได้ ถ้าเราลองใช้ to_string โดยไม่เพิ่ม colon และระบุ trait Display หลังชื่อ trait เราจะได้ error บอกว่าไม่มีเมธอดชื่อ to_string พบสำหรับ type &Self ใน scope ปัจจุบัน

มาดูสิ่งที่เกิดเมื่อเราพยายาม implement OutlinePrint บน type ที่ไม่ implement Display เช่น struct Point:

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: 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 Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

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

เราได้ error บอกว่า Display ถูกต้องการแต่ไม่ถูก implement:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::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 `traits-example` (bin "traits-example") due to 2 previous errors

เพื่อ fix สิ่งนี้ เรา implement Display บน Point และ satisfy constraint ที่ OutlinePrint ต้องการ แบบนี้:

Filename: src/main.rs
trait OutlinePrint: 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 Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

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

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

แล้ว implement trait OutlinePrint บน Point จะ compile สำเร็จ และเรา เรียก outline_print บน instance Point เพื่อแสดงมันภายใน outline ของ asterisk ได้

Implement External Trait ด้วย Newtype Pattern

ในส่วน “Implement Trait บน Type” ในบทที่ 10 เรากล่าวถึง orphan rule ที่บอกว่าเราถูกอนุญาต เพียงให้ implement trait บน type ถ้าทั้ง trait หรือ type หรือทั้งสอง อยู่ใน crate ของเรา มันเป็นไปได้ที่จะอ้อม restriction นี้โดยใช้ newtype pattern ซึ่งเกี่ยวกับการสร้าง type ใหม่ใน tuple struct (เราครอบคลุม tuple struct ในส่วน “สร้าง Type ต่างกันด้วย Tuple Struct” ในบทที่ 5) tuple struct จะมี หนึ่ง field และเป็นการห่อบางรอบ type ที่เราต้องการ implement trait แล้ว wrapper type อยู่ใน crate ของเรา และเราสามารถ implement trait บน wrapper Newtype คือ term ที่มาจากภาษาโปรแกรม Haskell ไม่มี penalty performance runtime สำหรับการใช้ pattern นี้ และ wrapper type ถูก elide ที่ compile time

เป็นตัวอย่าง สมมุติเราต้องการ implement Display บน Vec<T> ซึ่ง orphan rule ป้องกันเราจากการทำโดยตรงเพราะ trait Display และ type Vec<T> ถูกนิยามนอก crate ของเรา เราทำ struct Wrapper ที่บรรจุ instance ของ Vec<T> ได้ แล้ว เราสามารถ implement Display บน Wrapper และใช้ค่า Vec<T> ได้ ดังที่แสดงใน Listing 20-24

Filename: src/main.rs
use std::fmt;

struct Wrapper(Vec<String>);

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

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}
Listing 20-24: สร้าง type Wrapper รอบ Vec<String> เพื่อ implement Display

Implementation ของ Display ใช้ self.0 เพื่อเข้าถึง Vec<T> ด้านใน เพราะ Wrapper เป็น tuple struct และ Vec<T> คือ item ที่ index 0 ใน tuple แล้ว เราใช้ functionality ของ trait Display บน Wrapper ได้

ข้อเสียของการใช้เทคนิคนี้คือ Wrapper คือ type ใหม่ ดังนั้นมันไม่มี เมธอดของค่าที่มันบรรจุ เราจะต้อง implement เมธอดทั้งหมดของ Vec<T> โดยตรงบน Wrapper เพื่อให้เมธอด delegate ให้ self.0 ซึ่งจะอนุญาต ให้เรา treat Wrapper เป๊ะแบบ Vec<T> ถ้าเราต้องการให้ type ใหม่มี ทุกเมธอดที่ type ด้านในมี implement trait Deref บน Wrapper เพื่อ return type ด้านในจะเป็นวิธีแก้ (เราพูดถึงการ implement trait Deref ในส่วน “Treat Smart Pointer เหมือน Reference ปกติ” ในบทที่ 15) ถ้าเราไม่ ต้องการให้ type Wrapper มีเมธอดทั้งหมดของ type ด้านใน — ตัวอย่างเช่น เพื่อ restrict พฤติกรรมของ type Wrapper — เราจะต้อง implement เพียง เมธอดที่เราต้องการโดยมือ

newtype pattern นี้ก็มีประโยชน์แม้เมื่อ trait ไม่เกี่ยวข้อง มา switch focus และดูวิธีขั้นสูงในการ interact กับระบบ type ของ Rust