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

ปฏิบัติต่อ 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 ไปยัง ค่า

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: ใช้ dereference operator เพื่อตาม reference ไปยังค่า 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

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: ใช้ dereference operator บน 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>

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}
Listing 15-8: นิยาม type 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

Filename: src/main.rs
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);
}
Listing 15-9: พยายามใช้ 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>

Filename: src/main.rs
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);
}
Listing 15-10: Implement 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

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: ฟังก์ชัน hello ที่มี parameter name ของ type &str

เราเรียกฟังก์ชัน hello ด้วย string slice เป็นอาร์กิวเมนต์ได้ เช่น hello("Rust"); ตัวอย่างเช่น Deref coercion ทำให้เป็นไปได้ที่จะ เรียก hello ด้วย reference ของค่าของ type MyBox<String> ดังที่ แสดงใน Listing 15-12

Filename: src/main.rs
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);
}
Listing 15-12: เรียก 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>

Filename: src/main.rs
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)[..]);
}
Listing 15-13: โค้ดที่เราต้องเขียนถ้า Rust ไม่มี deref coercion

(*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 ในสามกรณี:

  1. จาก &T เป็น &U เมื่อ T: Deref<Target=U>
  2. จาก &mut T เป็น &mut U เมื่อ T: DerefMut<Target=U>
  3. จาก &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 เป็นไปได้