ปฏิบัติต่อ Smart Pointer เหมือน Reference ปกติ
การ implement trait Deref อนุญาตให้คุณกำหนดพฤติกรรมของ
dereference operator * (ไม่สับสนกับ operator คูณหรือ glob)
โดยการ implement Deref ในแบบที่ smart pointer ถูกปฏิบัติเหมือน
reference ปกติ คุณเขียนโค้ดที่ทำงานบน reference และใช้โค้ดนั้นกับ
smart pointer ด้วยได้
มาดูก่อนว่า dereference operator ทำงานกับ reference ปกติยังไง จาก
นั้น เราจะพยายามนิยาม type กำหนดเองที่ทำตัวเหมือน Box<T> และดู
ทำไม dereference operator ไม่ทำงานเหมือน reference บน type ที่
เราเพิ่งนิยาม เราจะสำรวจว่าการ implement trait Deref ทำให้เป็นไป
ได้ที่ smart pointer ทำงานในแบบคล้ายกับ reference ได้ จากนั้น เรา
จะดูฟีเจอร์ deref coercion ของ Rust และวิธีที่มันให้เราทำงานกับ
reference หรือ smart pointer
ตาม Reference ไปยังค่า
reference ปกติเป็นประเภทของ pointer และวิธีหนึ่งที่จะคิดถึง pointer
คือเป็นลูกศรไปยังค่าที่เก็บที่อื่น ใน Listing 15-6 เราสร้าง reference
ของค่า i32 แล้วใช้ dereference operator เพื่อตาม reference ไปยัง
ค่า
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
i32ตัวแปร x เก็บค่า i32 5 เราตั้ง y เท่ากับ reference ของ x
เรา assert ว่า x เท่ากับ 5 ได้ อย่างไรก็ตาม ถ้าเราต้องการทำ
assertion เกี่ยวกับค่าใน y เราต้องใช้ *y เพื่อตาม reference
ไปยังค่าที่มันชี้ (ดังนั้น dereference) เพื่อให้ compiler
เปรียบเทียบค่าจริงได้ เมื่อเรา dereference y เรามีสิทธิ์เข้าถึงค่า
integer ที่ y กำลังชี้ที่เรา compare กับ 5 ได้
ถ้าเราพยายามเขียน assert_eq!(5, y); แทน เราจะได้ error การคอมไพล์
นี้:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
การ compare ตัวเลขและ reference ของตัวเลขไม่ได้รับอนุญาตเพราะพวก มันเป็น type ต่างกัน เราต้องใช้ dereference operator เพื่อตาม reference ไปยังค่าที่มันกำลังชี้
ใช้ Box<T> เหมือน Reference
เราเขียนโค้ดใน Listing 15-6 ใหม่เพื่อใช้ Box<T> แทน reference ได้
— dereference operator ที่ใช้บน Box<T> ใน Listing 15-7 ทำงานในแบบ
เดียวกับ dereference operator ที่ใช้บน reference ใน Listing 15-6
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32>ความแตกต่างหลักระหว่าง Listing 15-7 และ Listing 15-6 คือที่นี่เรา
ตั้ง y เป็น instance ของ box ที่ชี้ไปยังค่าที่ copy ของ x แทน
reference ที่ชี้ไปยังค่าของ x ใน assertion สุดท้าย เราใช้
dereference operator เพื่อตาม pointer ของ box ในแบบเดียวกับที่เรา
ทำเมื่อ y เป็น reference ได้ ถัดไป เราจะสำรวจว่าอะไรพิเศษเกี่ยวกับ
Box<T> ที่ทำให้เราใช้ dereference operator ได้ โดยนิยาม box type
ของเราเอง
นิยาม Smart Pointer ของเราเอง
มา build wrapper type คล้ายกับ type Box<T> ที่ standard library
ให้มา เพื่อสัมผัสว่าประเภท smart pointer ทำตัวต่างจาก reference ตาม
ค่าเริ่มต้นยังไง จากนั้น เราจะดูวิธีเพิ่มความสามารถที่จะใช้
dereference operator
สังเกต — มีความแตกต่างใหญ่หนึ่งระหว่าง type
MyBox<T>ที่เรากำลัง จะ build และBox<T>จริง — เวอร์ชันของเราจะไม่เก็บข้อมูลของมัน บน heap เรากำลังโฟกัสตัวอย่างนี้ที่Derefดังนั้นที่ข้อมูลถูก เก็บจริงสำคัญน้อยกว่าพฤติกรรมเหมือน pointer
type Box<T> ในที่สุดถูกนิยามเป็น tuple struct ที่มีหนึ่ง element
ดังนั้น Listing 15-8 นิยาม type MyBox<T> ในแบบเดียวกัน เรายังจะ
นิยามฟังก์ชัน new เพื่อตรงกับฟังก์ชัน new ที่นิยามบน Box<T>
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox<T>เรานิยาม struct ชื่อ MyBox และประกาศ generic parameter T เพราะ
เราต้องการให้ type ของเราเก็บค่าของ type ใดก็ได้ type MyBox คือ
tuple struct ที่มีหนึ่ง element ของ type T ฟังก์ชัน MyBox::new
รับหนึ่ง parameter ของ type T และ return instance MyBox ที่เก็บ
ค่าที่ส่งเข้า
ลองเพิ่มฟังก์ชัน main ใน Listing 15-7 ให้ Listing 15-8 และเปลี่ยน
มันให้ใช้ type MyBox<T> ที่เรานิยามแทน Box<T> โค้ดใน Listing
15-9 จะไม่คอมไพล์ เพราะ Rust ไม่รู้วิธี dereference MyBox
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
MyBox<T> ในแบบเดียวกับที่เราใช้ reference และ Box<T>นี่คือ error การคอมไพล์ที่ได้:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
type MyBox<T> ของเรา dereference ไม่ได้เพราะเรายังไม่ได้
implement ความสามารถนั้นบน type ของเรา เพื่อเปิดใช้ dereferencing
ด้วย operator * เรา implement trait Deref
Implement Trait Deref
ดังที่พูดใน “Implement Trait บน Type”
ในบทที่ 10 เพื่อ implement trait เราต้องให้ implementation สำหรับ
เมธอดที่ต้องการของ trait trait Deref ที่ standard library ให้มา
ต้องให้เรา implement หนึ่งเมธอดชื่อ deref ที่ borrow self และ
return reference ของข้อมูลภายใน Listing 15-10 บรรจุ implementation
ของ Deref ที่จะเพิ่มในนิยามของ MyBox<T>
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Deref บน MyBox<T>syntax type Target = T; นิยาม associated type สำหรับ trait Deref
ใช้ associated type เป็นวิธีต่างเล็กน้อยในการประกาศ generic
parameter แต่คุณไม่ต้องกังวลเกี่ยวกับพวกมันตอนนี้ — เราจะครอบคลุม
พวกมันในรายละเอียดมากขึ้นในบทที่ 20
เราเติม body ของเมธอด deref ด้วย &self.0 เพื่อให้ deref return
reference ของค่าที่เราต้องการเข้าถึงด้วย operator * — จำได้จาก
“สร้าง Type ต่างด้วย Tuple Struct”
ในบทที่ 5 ว่า .0 เข้าถึงค่าแรกใน tuple struct ฟังก์ชัน main
ใน Listing 15-9 ที่เรียก * บนค่า MyBox<T> ตอนนี้คอมไพล์ได้ และ
assertion ผ่าน!
โดยไม่มี trait Deref compiler dereference ได้เฉพาะ reference &
เมธอด deref ให้ compiler ความสามารถที่จะรับค่าของ type ใดก็ตามที่
implement Deref และเรียกเมธอด deref เพื่อรับ reference ที่มัน
รู้วิธี dereference
เมื่อเราใส่ *y ใน Listing 15-9 เบื้องหลัง Rust จริง ๆ รันโค้ดนี้:
*(y.deref())
Rust แทนที่ operator * ด้วยการเรียกเมธอด deref แล้ว
dereference ธรรมดา เพื่อให้เราไม่ต้องคิดว่าเราต้องเรียกเมธอด deref
หรือไม่ ฟีเจอร์ Rust นี้ให้เราเขียนโค้ดที่ทำงานเหมือนกันไม่ว่าเรามี
reference ปกติหรือ type ที่ implement Deref
เหตุผลที่เมธอด deref return reference ของค่า และว่า dereference
ธรรมดาภายนอกวงเล็บใน *(y.deref()) ยังจำเป็น เกี่ยวข้องกับระบบ
ownership ถ้าเมธอด deref return ค่าโดยตรงแทน reference ของค่า ค่า
จะถูกย้ายออกจาก self เราไม่ต้องการรับ ownership ของค่าภายในภายใน
MyBox<T> ในกรณีนี้หรือในกรณีส่วนใหญ่ที่เราใช้ dereference
operator
สังเกตว่า operator * ถูกแทนที่ด้วยการเรียกเมธอด deref แล้วการ
เรียก operator * เพียงครั้งเดียว แต่ละครั้งที่เราใช้ * ใน
โค้ดของเรา เพราะการแทนที่ของ operator * ไม่ recursive infinitely
เราลงเอยที่ข้อมูลของ type i32 ซึ่งตรงกับ 5 ใน assert_eq! ใน
Listing 15-9
ใช้ Deref Coercion ในฟังก์ชันและเมธอด
Deref coercion แปลง reference ของ type ที่ implement trait
Deref เป็น reference ของอีก type ตัวอย่างเช่น deref coercion แปลง
&String เป็น &str ได้เพราะ String implement trait Deref แบบ
ที่มัน return &str Deref coercion เป็นความสะดวกที่ Rust ทำกับ
อาร์กิวเมนต์ของฟังก์ชันและเมธอด และมันทำงานเฉพาะบน type ที่
implement trait Deref มันเกิดอัตโนมัติเมื่อเราส่ง reference ของ
ค่าของ type เฉพาะเป็นอาร์กิวเมนต์ให้ฟังก์ชันหรือเมธอดที่ไม่ตรงกับ
type ของ parameter ในนิยามของฟังก์ชันหรือเมธอด ลำดับของการเรียก
เมธอด deref แปลง type ที่เราให้เป็น type ที่ parameter ต้องการ
Deref coercion ถูกเพิ่มใน Rust เพื่อให้ programmer ที่เขียนการเรียก
ฟังก์ชันและเมธอดไม่ต้องเพิ่ม reference ชัดเจนและ dereference ด้วย
& และ * มาก ฟีเจอร์ deref coercion ยังให้เราเขียนโค้ดมากขึ้นที่
ทำงานได้สำหรับ reference หรือ smart pointer
เพื่อเห็น deref coercion ในการกระทำ มาใช้ type MyBox<T> ที่เรา
นิยามใน Listing 15-8 รวมถึง implementation ของ Deref ที่เราเพิ่ม
ใน Listing 15-10 Listing 15-11 แสดงนิยามของฟังก์ชันที่มี parameter
เป็น string slice
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
hello ที่มี parameter name ของ type &strเราเรียกฟังก์ชัน hello ด้วย string slice เป็นอาร์กิวเมนต์ได้ เช่น
hello("Rust"); ตัวอย่างเช่น Deref coercion ทำให้เป็นไปได้ที่จะ
เรียก hello ด้วย reference ของค่าของ type MyBox<String> ดังที่
แสดงใน Listing 15-12
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
hello ด้วย reference ของค่า MyBox<String> ซึ่งทำงานได้เพราะ deref coercionที่นี่เรากำลังเรียกฟังก์ชัน hello ด้วยอาร์กิวเมนต์ &m ซึ่งเป็น
reference ของค่า MyBox<String> เพราะเรา implement trait Deref
บน MyBox<T> ใน Listing 15-10 Rust เปลี่ยน &MyBox<String> เป็น
&String โดยเรียก deref standard library ให้ implementation ของ
Deref บน String ที่ return string slice และนี่อยู่ใน API
documentation สำหรับ Deref Rust เรียก deref อีกครั้งเพื่อ
เปลี่ยน &String เป็น &str ซึ่งตรงกับนิยามของฟังก์ชัน hello
ถ้า Rust ไม่ implement deref coercion เราจะต้องเขียนโค้ดใน Listing
15-13 แทนโค้ดใน Listing 15-12 เพื่อเรียก hello ด้วยค่าของ type
&MyBox<String>
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m) dereference MyBox<String> เป็น String จากนั้น & และ
[..] รับ string slice ของ String ที่เท่ากับ string ทั้งหมดเพื่อ
ตรง signature ของ hello โค้ดนี้โดยไม่มี deref coercion อ่าน
ยากกว่า เขียนยากกว่า และเข้าใจยากกว่าด้วยสัญลักษณ์เหล่านี้ทั้งหมด
ที่เกี่ยวข้อง Deref coercion อนุญาตให้ Rust จัดการการแปลงเหล่านี้
ให้เราอัตโนมัติ
เมื่อ trait Deref ถูกนิยามสำหรับ type ที่เกี่ยวข้อง Rust จะ
วิเคราะห์ type และใช้ Deref::deref มากเท่าที่จำเป็นเพื่อรับ
reference ที่ตรงกับ type ของ parameter จำนวนครั้งที่ Deref::deref
ต้องถูก insert ถูก resolve ที่ compile time ดังนั้นไม่มีบทลงโทษ
runtime สำหรับการใช้ประโยชน์ของ deref coercion!
จัดการ Deref Coercion กับ Mutable Reference
คล้ายกับวิธีที่คุณใช้ trait Deref เพื่อ override operator * บน
immutable reference คุณใช้ trait DerefMut เพื่อ override operator
* บน mutable reference ได้
Rust ทำ deref coercion เมื่อมันพบ type และ implementation trait ในสามกรณี:
- จาก
&Tเป็น&Uเมื่อT: Deref<Target=U> - จาก
&mut Tเป็น&mut Uเมื่อT: DerefMut<Target=U> - จาก
&mut Tเป็น&Uเมื่อT: Deref<Target=U>
กรณีสองแรกเหมือนกันยกเว้นที่กรณีที่สอง implement mutability กรณีแรก
ระบุว่าถ้าคุณมี &T และ T implement Deref ไปยัง type U
บางตัว คุณรับ &U ได้แบบโปร่งใส กรณีที่สองระบุว่า deref coercion
เดียวกันเกิดสำหรับ mutable reference
กรณีที่สามยุ่งยากกว่า — Rust จะยัง coerce mutable reference เป็น immutable ด้วย แต่ตรงข้าม_ไม่_ เป็นไปได้ — immutable reference จะ ไม่ coerce เป็น mutable reference เพราะกฎ borrowing ถ้าคุณมี mutable reference mutable reference นั้นต้องเป็น reference เดียว ของข้อมูลนั้น (มิฉะนั้น โปรแกรมจะไม่คอมไพล์) การแปลงหนึ่ง mutable reference เป็นหนึ่ง immutable reference จะไม่ทำลายกฎ borrowing การแปลง immutable reference เป็น mutable reference จะต้องการให้ immutable reference เริ่มต้นเป็น immutable reference เดียวของข้อมูล นั้น แต่กฎ borrowing ไม่รับประกันสิ่งนั้น ดังนั้น Rust ไม่ทำ สมมุติฐานว่าการแปลง immutable reference เป็น mutable reference เป็นไปได้