Trait Nâng Cao
Chúng ta đã đề cập đến trait trong "Trait: Định Nghĩa Hành Vi Chung" ở Chương 10, nhưng chúng ta chưa thảo luận về các chi tiết nâng cao hơn. Giờ đây khi bạn đã biết nhiều hơn về Rust, chúng ta có thể đi sâu vào chi tiết.
Kiểu Liên Kết (Associated Types)
Kiểu liên kết kết nối một placeholder kiểu với một trait để các định nghĩa phương thức của trait có thể sử dụng các placeholder kiểu này trong chữ ký của chúng. Người triển khai trait sẽ chỉ định kiểu cụ thể được sử dụng thay vì kiểu placeholder cho việc triển khai cụ thể. Bằng cách đó, chúng ta có thể định nghĩa một trait sử dụng một số kiểu mà không cần biết chính xác những kiểu đó là gì cho đến khi trait được triển khai.
Chúng ta đã mô tả hầu hết các tính năng nâng cao trong chương này là ít khi cần đến. Kiểu liên kết nằm đâu đó ở giữa: chúng được sử dụng ít thường xuyên hơn các tính năng được giải thích trong phần còn lại của cuốn sách nhưng phổ biến hơn nhiều tính năng khác được thảo luận trong chương này.
Một ví dụ về trait với kiểu liên kết là trait Iterator
mà thư viện chuẩn cung
cấp. Kiểu liên kết được đặt tên là Item
và đại diện cho kiểu của các giá trị
mà kiểu thực hiện trait Iterator
đang lặp qua. Định nghĩa của trait Iterator
được hiển thị trong Listing 20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Kiểu Item
là một placeholder, và định nghĩa của phương thức next
cho thấy nó
sẽ trả về các giá trị kiểu Option<Self::Item>
. Người triển khai trait
Iterator
sẽ chỉ định kiểu cụ thể cho Item
, và phương thức next
sẽ trả về
một Option
chứa giá trị của kiểu cụ thể đó.
Kiểu liên kết có thể giống với khái niệm generics, vì generics cũng cho phép
chúng ta định nghĩa một hàm mà không cần chỉ định kiểu nào nó có thể xử lý. Để
xem xét sự khác biệt giữa hai khái niệm này, chúng ta sẽ xem xét việc triển khai
trait Iterator
trên một kiểu có tên là Counter
chỉ định kiểu Item
là
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
}
}
}
Cú pháp này có vẻ tương đương với generics. Vậy tại sao không chỉ định nghĩa
trait Iterator
với generics, như được hiển thị trong Listing 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Sự khác biệt là khi sử dụng generics, như trong Listing 20-14, chúng ta phải chú
thích các kiểu trong mỗi triển khai; bởi vì chúng ta cũng có thể triển khai
Iterator<String> for Counter
hoặc bất kỳ kiểu nào khác, chúng ta có thể có
nhiều triển khai của Iterator
cho Counter
. Nói cách khác, khi một trait có
tham số generic, nó có thể được triển khai cho một kiểu nhiều lần, thay đổi kiểu
cụ thể của các tham số kiểu generic mỗi lần. Khi chúng ta sử dụng phương thức
next
trên Counter
, chúng ta sẽ phải cung cấp chú thích kiểu để chỉ ra triển
khai nào của Iterator
chúng ta muốn sử dụng.
Với kiểu liên kết, chúng ta không cần phải chú thích kiểu vì chúng ta không thể
triển khai một trait trên một kiểu nhiều lần. Trong Listing 20-13 với định nghĩa
sử dụng kiểu liên kết, chúng ta chỉ có thể chọn kiểu của Item
sẽ là gì một
lần, vì chỉ có thể có một impl Iterator for Counter
. Chúng ta không phải chỉ
định rằng chúng ta muốn một iterator của các giá trị u32
ở mọi nơi mà chúng ta
gọi next
trên Counter
.
Kiểu liên kết cũng trở thành một phần của hợp đồng trait: người triển khai trait phải cung cấp một kiểu để thay thế cho placeholder kiểu liên kết. Kiểu liên kết thường có tên mô tả cách kiểu sẽ được sử dụng, và việc ghi tài liệu cho kiểu liên kết trong tài liệu API là một thực hành tốt.
Tham Số Kiểu Generic Mặc Định và Nạp Chồng Toán Tử
Khi chúng ta sử dụng các tham số kiểu generic, chúng ta có thể chỉ định một kiểu
cụ thể mặc định cho kiểu generic. Điều này loại bỏ nhu cầu cho người triển khai
trait phải chỉ định một kiểu cụ thể nếu kiểu mặc định hoạt động. Bạn chỉ định
một kiểu mặc định khi khai báo một kiểu generic với cú pháp
<PlaceholderType=ConcreteType>
.
Một ví dụ tuyệt vời về tình huống mà kỹ thuật này hữu ích là với nạp chồng toán
tử, trong đó bạn tùy chỉnh hành vi của một toán tử (như +
) trong các tình
huống cụ thể.
Rust không cho phép bạn tạo các toán tử của riêng mình hoặc nạp chồng các toán
tử tùy ý. Nhưng bạn có thể nạp chồng các hoạt động và các trait tương ứng được
liệt kê trong std::ops
bằng cách triển khai các trait liên quan đến toán tử.
Ví dụ, trong Listing 20-15, chúng ta nạp chồng toán tử +
để cộng hai thể hiện
Point
với nhau. Chúng ta làm điều này bằng cách triển khai trait Add
trên
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 } ); }
Phương thức add
cộng các giá trị x
của hai thể hiện Point
và các giá trị
y
của hai thể hiện Point
để tạo ra một Point
mới. Trait Add
có một kiểu
liên kết có tên là Output
xác định kiểu trả về từ phương thức add
.
Kiểu generic mặc định trong mã này nằm trong trait Add
. Đây là định nghĩa của
nó:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
Mã này có vẻ quen thuộc: một trait với một phương thức và một kiểu liên kết.
Phần mới là Rhs=Self
: cú pháp này được gọi là tham số kiểu mặc định. Tham số
kiểu generic Rhs
(viết tắt của "right-hand side") định nghĩa kiểu của tham số
rhs
trong phương thức add
. Nếu chúng ta không chỉ định một kiểu cụ thể cho
Rhs
khi triển khai trait Add
, kiểu của Rhs
sẽ mặc định là Self
, là kiểu
mà chúng ta đang triển khai Add
trên đó.
Khi chúng ta triển khai Add
cho Point
, chúng ta đã sử dụng giá trị mặc định
cho Rhs
bởi vì chúng ta muốn cộng hai thể hiện Point
. Hãy xem xét một ví dụ
về việc triển khai trait Add
trong đó chúng ta muốn tùy chỉnh kiểu Rhs
thay
vì sử dụng giá trị mặc định.
Chúng ta có hai struct, Millimeters
và Meters
, chứa các giá trị trong các
đơn vị khác nhau. Việc bọc mỏng của một kiểu hiện có trong một struct khác được
gọi là mẫu newtype, mà chúng ta mô tả chi tiết hơn trong phần "Sử Dụng Mẫu
Newtype để Triển Khai Trait Bên Ngoài trên Kiểu Bên
Ngoài". Chúng ta muốn cộng các giá trị tính bằng
milimet với các giá trị tính bằng mét và có triển khai của Add
thực hiện
chuyển đổi một cách chính xác. Chúng ta có thể triển khai Add
cho
Millimeters
với Meters
là Rhs
, như hiển thị trong 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))
}
}
Để cộng Millimeters
và Meters
, chúng ta chỉ định impl Add<Meters>
để đặt
giá trị của tham số kiểu Rhs
thay vì sử dụng giá trị mặc định là Self
.
Bạn sẽ sử dụng các tham số kiểu mặc định theo hai cách chính:
- Để mở rộng một kiểu mà không phá vỡ mã hiện có
- Để cho phép tùy chỉnh trong các trường hợp cụ thể mà hầu hết người dùng không cần
Trait Add
của thư viện chuẩn là một ví dụ cho mục đích thứ hai: thông thường,
bạn sẽ cộng hai kiểu giống nhau, nhưng trait Add
cung cấp khả năng tùy chỉnh
vượt ra ngoài điều đó. Sử dụng một tham số kiểu mặc định trong định nghĩa trait
Add
có nghĩa là bạn không phải chỉ định tham số bổ sung trong hầu hết các
trường hợp. Nói cách khác, một chút mã soạn sẵn triển khai không cần thiết, làm
cho việc sử dụng trait dễ dàng hơn.
Mục đích đầu tiên tương tự như mục đích thứ hai nhưng ngược lại: nếu bạn muốn thêm một tham số kiểu vào một trait hiện có, bạn có thể cho nó một giá trị mặc định để cho phép mở rộng chức năng của trait mà không phá vỡ mã triển khai hiện có.
Phân Biệt Giữa Các Phương Thức Có Cùng Tên
Không có gì trong Rust ngăn cản một trait có một phương thức có cùng tên với phương thức của trait khác, Rust cũng không ngăn bạn triển khai cả hai trait trên một kiểu. Cũng có thể triển khai một phương thức trực tiếp trên kiểu với cùng tên như các phương thức từ các trait.
Khi gọi các phương thức có cùng tên, bạn cần phải nói cho Rust biết bạn muốn sử
dụng cái nào. Xem xét mã trong Listing 20-17 trong đó chúng ta đã định nghĩa hai
trait, Pilot
và Wizard
, cả hai đều có một phương thức được gọi là fly
. Sau
đó, chúng ta triển khai cả hai trait trên một kiểu Human
đã có một phương thức
tên là fly
được triển khai trên nó. Mỗi phương thức fly
làm một điều gì đó
khác nhau.
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() {}
Khi chúng ta gọi fly
trên một thể hiện của Human
, trình biên dịch mặc định
gọi phương thức được triển khai trực tiếp trên kiểu, như hiển thị trong 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(); }
Chạy mã này sẽ in ra *waving arms furiously*
, cho thấy Rust đã gọi phương thức
fly
được triển khai trực tiếp trên Human
.
Để gọi các phương thức fly
từ trait Pilot
hoặc trait Wizard
, chúng ta cần
sử dụng cú pháp rõ ràng hơn để chỉ định phương thức fly
nào chúng ta muốn.
Listing 20-19 minh họa cú pháp này.
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(); }
Chỉ định tên trait trước tên phương thức làm rõ cho Rust biết chúng ta muốn gọi
triển khai fly
nào. Chúng ta cũng có thể viết Human::fly(&person)
, tương
đương với person.fly()
mà chúng ta đã sử dụng trong Listing 20-19, nhưng điều
này dài hơn một chút nếu chúng ta không cần phân biệt.
Chạy mã này in ra như sau:
$ 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*
Bởi vì phương thức fly
nhận một tham số self
, nếu chúng ta có hai kiểu đều
triển khai một trait, Rust có thể xác định triển khai nào của trait sẽ được sử
dụng dựa trên kiểu của self
.
Tuy nhiên, các hàm liên kết không phải là phương thức không có tham số self
.
Khi có nhiều kiểu hoặc trait định nghĩa các hàm không phải phương thức với cùng
tên hàm, Rust không phải lúc nào cũng biết bạn muốn ý nghĩa kiểu nào trừ khi bạn
sử dụng cú pháp đầy đủ. Ví dụ, trong Listing 20-20, chúng ta tạo một trait cho
một nơi trú ẩn động vật muốn đặt tên cho tất cả chó con là Spot. Chúng ta tạo
một trait Animal
với một hàm liên kết không phải phương thức baby_name
.
Trait Animal
được triển khai cho struct Dog
, trên đó chúng ta cũng cung cấp
một hàm liên kết không phải phương thức baby_name
trực tiếp.
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()); }
Chúng ta triển khai mã để đặt tên cho tất cả chó con là Spot trong hàm liên kết
baby_name
được định nghĩa trên Dog
. Kiểu Dog
cũng triển khai trait
Animal
, mô tả các đặc điểm mà tất cả động vật có. Chó con được gọi là puppies,
và điều đó được thể hiện trong việc triển khai trait Animal
trên Dog
trong
hàm baby_name
liên kết với trait Animal
.
Trong main
, chúng ta gọi hàm Dog::baby_name
, hàm này gọi hàm liên kết được
định nghĩa trực tiếp trên Dog
. Mã này in ra:
$ 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
Đầu ra này không phải là những gì chúng ta muốn. Chúng ta muốn gọi hàm
baby_name
là một phần của trait Animal
mà chúng ta đã triển khai trên Dog
để mã in ra A baby dog is called a puppy
. Kỹ thuật chỉ định tên trait mà chúng
ta đã sử dụng trong Listing 20-19 không giúp ích ở đây; nếu chúng ta thay đổi
main
thành mã trong Listing 20-21, chúng ta sẽ nhận được lỗi biên dịch.
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());
}
Bởi vì Animal::baby_name
không có tham số self
, và có thể có các kiểu khác
triển khai trait Animal
, Rust không thể xác định triển khai
Animal::baby_name
nào chúng ta muốn. Chúng ta sẽ nhận được lỗi trình biên dịch
này:
$ 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
Để loại bỏ sự không rõ ràng và nói cho Rust biết rằng chúng ta muốn sử dụng
triển khai của Animal
cho Dog
thay vì triển khai của Animal
cho một số
kiểu khác, chúng ta cần sử dụng cú pháp đầy đủ. Listing 20-22 minh họa cách sử
dụng cú pháp đầy đủ.
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()); }
Chúng ta đang cung cấp cho Rust một chú thích kiểu trong dấu ngoặc nhọn, cho
biết chúng ta muốn gọi phương thức baby_name
từ trait Animal
như được triển
khai trên Dog
bằng cách nói rằng chúng ta muốn coi kiểu Dog
như một Animal
cho lệnh gọi hàm này. Mã này giờ đây sẽ in ra những gì chúng ta muốn:
$ 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
Nhìn chung, cú pháp đầy đủ được định nghĩa như sau:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
Đối với các hàm liên kết không phải phương thức, sẽ không có receiver
: sẽ chỉ
có danh sách các đối số khác. Bạn có thể sử dụng cú pháp đầy đủ ở mọi nơi mà bạn
gọi hàm hoặc phương thức. Tuy nhiên, bạn được phép bỏ qua bất kỳ phần nào của cú
pháp này mà Rust có thể tìm ra từ thông tin khác trong chương trình. Bạn chỉ cần
sử dụng cú pháp dài dòng hơn này trong các trường hợp có nhiều triển khai sử
dụng cùng một tên và Rust cần trợ giúp để xác định triển khai nào bạn muốn gọi.
Sử Dụng Supertraits
Đôi khi bạn có thể viết một định nghĩa trait phụ thuộc vào một trait khác: để một kiểu triển khai trait đầu tiên, bạn muốn yêu cầu kiểu đó cũng phải triển khai trait thứ hai. Bạn sẽ làm điều này để định nghĩa trait của bạn có thể sử dụng các phần tử liên kết của trait thứ hai. Trait mà định nghĩa trait của bạn dựa vào được gọi là supertrait của trait của bạn.
Ví dụ, giả sử chúng ta muốn tạo một trait OutlinePrint
với một phương thức
outline_print
sẽ in một giá trị đã cho được định dạng để nó được đóng khung
bằng dấu hoa thị. Nghĩa là, với một struct Point
triển khai trait Display
của thư viện chuẩn để có kết quả là (x, y)
, khi chúng ta gọi outline_print
trên một thể hiện Point
có 1
cho x
và 3
cho y
, nó sẽ in ra:
**********
* *
* (1, 3) *
* *
**********
Trong việc triển khai phương thức outline_print
, chúng ta muốn sử dụng chức
năng của trait Display
. Do đó, chúng ta cần chỉ định rằng trait OutlinePrint
sẽ chỉ hoạt động cho các kiểu cũng triển khai Display
và cung cấp chức năng mà
OutlinePrint
cần. Chúng ta có thể làm điều đó trong định nghĩa trait bằng cách
chỉ định OutlinePrint: Display
. Kỹ thuật này tương tự như việc thêm một ràng
buộc trait cho trait. Listing 20-23 cho thấy một triển khai của 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() {}
Bởi vì chúng ta đã chỉ định rằng OutlinePrint
yêu cầu trait Display
, chúng
ta có thể sử dụng hàm to_string
được tự động triển khai cho bất kỳ kiểu nào
triển khai Display
. Nếu chúng ta cố gắng sử dụng to_string
mà không thêm dấu
hai chấm và chỉ định trait Display
sau tên trait, chúng ta sẽ gặp lỗi nói rằng
không tìm thấy phương thức nào có tên là to_string
cho kiểu &Self
trong phạm
vi hiện tại.
Hãy xem điều gì xảy ra khi chúng ta cố gắng triển khai OutlinePrint
trên một
kiểu không triển khai Display
, chẳng hạn như 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();
}
Chúng ta nhận được lỗi nói rằng Display
là cần thiết nhưng không được triển
khai:
$ 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 {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
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();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
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
Để sửa lỗi này, chúng ta triển khai Display
trên Point
và thỏa mãn ràng buộc
mà OutlinePrint
yêu cầu, như sau:
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(); }
Sau đó, việc triển khai trait OutlinePrint
trên Point
sẽ biên dịch thành
công, và chúng ta có thể gọi outline_print
trên một thể hiện Point
để hiển
thị nó trong một đường viền dấu hoa thị.
Sử Dụng Mẫu Newtype để Triển Khai Trait Bên Ngoài trên Kiểu Bên Ngoài
Trong "Triển Khai một Trait trên một Kiểu" ở Chương 10, chúng ta đã đề cập đến quy tắc orphan nói rằng chúng ta chỉ được phép triển khai một trait trên một kiểu nếu một trong hai trait hoặc kiểu, hoặc cả hai, là cục bộ đối với crate của chúng ta. Có thể vượt qua hạn chế này bằng cách sử dụng mẫu newtype, liên quan đến việc tạo một kiểu mới trong một tuple struct. (Chúng ta đã đề cập đến tuple struct trong "Sử Dụng Tuple Structs Không Có Trường Đặt Tên để Tạo Các Kiểu Khác Nhau" ở Chương 5.) Tuple struct sẽ có một trường và là một bản bọc mỏng xung quanh kiểu mà chúng ta muốn triển khai trait. Sau đó, kiểu bọc sẽ là cục bộ đối với crate của chúng ta, và chúng ta có thể triển khai trait trên bản bọc. Newtype là một thuật ngữ có nguồn gốc từ ngôn ngữ lập trình Haskell. Không có mất mát hiệu suất thời gian chạy khi sử dụng mẫu này, và kiểu bọc được loại bỏ tại thời điểm biên dịch.
Ví dụ, giả sử chúng ta muốn triển khai Display
trên Vec<T>
, mà quy tắc
orphan ngăn chúng ta làm trực tiếp vì trait Display
và kiểu Vec<T>
được định
nghĩa bên ngoài crate của chúng ta. Chúng ta có thể tạo một struct Wrapper
chứa một thể hiện của Vec<T>
; sau đó chúng ta có thể triển khai Display
trên
Wrapper
và sử dụng giá trị Vec<T>
, như hiển thị trong 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}"); }
Triển khai của Display
sử dụng self.0
để truy cập Vec<T>
bên trong, vì
Wrapper
là một tuple struct và Vec<T>
là phần tử ở chỉ số 0 trong tuple. Sau
đó, chúng ta có thể sử dụng chức năng của trait Display
trên Wrapper
.
Nhược điểm của việc sử dụng kỹ thuật này là Wrapper
là một kiểu mới, vì vậy nó
không có các phương thức của giá trị mà nó đang giữ. Chúng ta sẽ phải triển khai
trực tiếp tất cả các phương thức của Vec<T>
trên Wrapper
sao cho các phương
thức ủy quyền cho self.0
, điều này sẽ cho phép chúng ta đối xử với Wrapper
chính xác như một Vec<T>
. Nếu chúng ta muốn kiểu mới có mọi phương thức mà
kiểu bên trong có, việc triển khai trait Deref
trên Wrapper
để trả về kiểu
bên trong sẽ là một giải pháp (chúng ta đã thảo luận về việc triển khai trait
Deref
trong "Đối Xử với Smart Pointers Như Tham Chiếu Thông Thường với Trait
Deref
" ở Chương 15). Nếu chúng ta không
muốn kiểu Wrapper
có tất cả các phương thức của kiểu bên trong — ví dụ, để hạn
chế hành vi của kiểu Wrapper
— chúng ta sẽ phải tự triển khai các phương thức
mà chúng ta muốn.
Mẫu newtype này cũng hữu ích ngay cả khi các trait không liên quan. Hãy chuyển sang xem xét một số cách tiên tiến để tương tác với hệ thống kiểu của Rust.