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>;
}
Iterator ที่มี associated type Itemtype 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:
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>;
}
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
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 }
);
}
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
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))
}
}
Add บน Millimeters เพื่อเพิ่ม Millimeters และ Metersเพื่อเพิ่ม Millimeters และ Meters เราระบุ impl Add<Meters> เพื่อ
ตั้งค่าของ type parameter Rhs แทนการใช้ default ของ Self
คุณจะใช้ default type parameter ในสองวิธีหลัก:
- เพื่อขยาย type โดยไม่ break โค้ดที่มี
- เพื่ออนุญาต 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 แต่ละทำสิ่งที่ต่างกัน
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() {}
fly และถูก implement บน type Human และเมธอด fly ถูก implement บน Human โดยตรงเมื่อเราเรียก fly บน instance ของ Human, compiler default ไปเรียก
เมธอดที่ถูก implement บน type โดยตรง ดังที่แสดงใน Listing 20-18
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();
}
fly บน instance ของ Humanรันโค้ดนี้จะ print *waving arms furiously* แสดงว่า Rust เรียกเมธอด
fly ที่ implement บน Human โดยตรง
เพื่อเรียกเมธอด fly จาก trait Pilot หรือ trait Wizard เราต้องใช้
syntax ที่ชัดเจนกว่าเพื่อระบุว่าเมธอด fly ใดที่เราหมายถึง Listing
20-19 สาธิต syntax นี้
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();
}
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 โดยตรงด้วย
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());
}
เรา 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
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());
}
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
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());
}
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
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() {}
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:
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 ต้องการ แบบนี้:
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
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}");
}
Wrapper รอบ Vec<String> เพื่อ implement DisplayImplementation ของ 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