การปฏิบัติต่อพอยน์เตอร์อัจฉริยะเสมือนการอ้างอิงทั่วไป
การประยุกต์ใช้เทรต Deref ช่วยให้คุณสามารถปรับแต่งพฤติกรรมของตัวดำเนินการลดระดับการอ้างอิง (dereference operator) * ได้ (ไม่สับสนกับตัวดำเนินการคูณหรือตัวดำเนินการระดับสัญลักษณ์แบบไวด์การ์ด) การประยุกต์ใช้ Deref ในลักษณะที่ช่วยให้พอยน์เตอร์อัจฉริยะถูกปฏิบัติตัวเหมือนการอ้างอิงทั่วไป ทำให้คุณสามารถเขียนโค้ดที่ทำงานกับการอ้างอิงและนำโค้ดนั้นไปใช้กับพอยน์เตอร์อัจฉริยะได้ด้วยเช่นกัน
ก่อนอื่นมาดูกันว่าตัวดำเนินการลดระดับการอ้างอิงทำงานอย่างไรกับการอ้างอิงทั่วไป จากนั้น เราจะลองนิยามชนิดข้อมูลแบบกำหนดเองที่ทำตัวเหมือน Box<T> และดูว่าเหตุใดตัวดำเนินการลดระดับการอ้างอิงจึงไม่ทำงานเหมือนการอ้างอิงกับชนิดข้อมูลที่นิยามขึ้นใหม่ของเรา เราจะสำรวจว่าการประยุกต์ใช้เทรต Deref ช่วยให้พอยน์เตอร์อัจฉริยะทำงานในลักษณะที่คล้ายกับการอ้างอิงได้อย่างไร จากนั้น เราจะดูฟีเจอร์การบังคับชนิดข้อมูลของ deref (deref coercion) ของ Rust และวิธีที่มันช่วยให้เราทำงานได้ทั้งกับการอ้างอิงหรือพอยน์เตอร์อัจฉริยะ
การปฏิบัติตามการอ้างอิงไปยังค่า
การอ้างอิงทั่วไปเป็นชนิดพอยน์เตอร์แบบหนึ่ง และวิธีหนึ่งในการมองพอยน์เตอร์คือการมองเป็นลูกศรที่ชี้ไปยังค่าที่เก็บไว้ ณ ตำแหน่งอื่น ในโค้ดตัวอย่างที่ 15-6 เราสร้างการอ้างอิงไปยังค่า i32 จากนั้นใช้ตัวดำเนินการลดระดับการอ้างอิงเพื่อปฏิบัติตามการอ้างอิงไปยังค่านั้น
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
ตัวแปร x บรรจุค่า i32 เท่ากับ 5 เราตั้งค่า y ให้เท่ากับการอ้างอิงไปยัง x เราสามารถยืนยัน (assert) ได้ว่า x เท่ากับ 5 อย่างไรก็ตาม หากเราต้องการทำการยืนยันเกี่ยวกับค่าใน y เราต้องใช้ *y เพื่อปฏิบัติตามการอ้างอิงไปยังค่าที่มันชี้ไป (ดังนั้นจึงเรียกว่า การลดระดับการอ้างอิง หรือ dereference) เพื่อให้คอมไพเลอร์สามารถเปรียบเทียบค่าที่แท้จริงได้ เมื่อเราทำการลดระดับการอ้างอิง y แล้ว เราจะสามารถเข้าถึงค่าจำนวนเต็มที่ y ชี้ไปเพื่อนำมาเปรียบเทียบกับ 5 ได้
หากเราลองเขียน assert_eq!(5, y); แทน เราจะได้ข้อผิดพลาดในการคอมไพล์ดังนี้:
$ 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}`
= help: the following other types implement trait `PartialEq<Rhs>`:
f128
f16
f32
f64
i128
i16
i32
i64
and 8 others
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
การเปรียบเทียบตัวเลขกับการอ้างอิงไปยังตัวเลขนั้นไม่ได้รับอนุญาตเนื่องจากพวกมันเป็นคนละชนิดข้อมูลกัน เราต้องใช้ตัวดำเนินการลดระดับการอ้างอิงเพื่อปฏิบัติตามการอ้างอิงไปยังค่าที่มันชี้ไป
การใช้ Box<T> เสมือนการอ้างอิง
เราสามารถเขียนโค้ดในโค้ดตัวอย่างที่ 15-6 ใหม่โดยใช้ Box<T> แทนการอ้างอิงได้ ตัวดำเนินการลดระดับการอ้างอิงที่ใช้กับ Box<T> ในโค้ดตัวอย่างที่ 15-7 ทำงานในลักษณะเดียวกับตัวดำเนินการลดระดับการอ้างอิงที่ใช้กับการอ้างอิงในโค้ดตัวอย่างที่ 15-6
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
ความแตกต่างหลักระหว่างโค้ดตัวอย่างที่ 15-7 และโค้ดตัวอย่างที่ 15-6 คือในที่นี้เรากำหนดให้ y เป็นอินสแตนซ์ของ box ที่ชี้ไปยังค่าสำเนาของ x แทนที่จะเป็นการอ้างอิงที่ชี้ไปยังค่าของ x ในการยืนยันบรรทัดสุดท้าย เราสามารถใช้ตัวดำเนินการลดระดับการอ้างอิงเพื่อปฏิบัติตามพอยน์เตอร์ของ box ในลักษณะเดียวกับที่เราทำเมื่อ y เป็นการอ้างอิงได้ ถัดไป เราจะสำรวจสิ่งพิเศษเกี่ยวกับ Box<T> ที่ช่วยให้เราสามารถใช้ตัวดำเนินการลดระดับการอ้างอิงได้โดยการนิยามชนิดข้อมูล box ของเราเอง
การนิยามพอยน์เตอร์อัจฉริยะของเราเอง
มาสร้างชนิดข้อมูลครอบ (wrapper type) ที่คล้ายกับชนิดข้อมูล Box<T> ที่จัดเตรียมโดยไลบรารีมาตรฐาน เพื่อสัมผัสประสบการณ์ว่าชนิดพอยน์เตอร์อัจฉริยะมีพฤติกรรมแตกต่างจากการอ้างอิงโดยเริ่มต้นอย่างไร จากนั้น เราจะดูวิธีเพิ่มความสามารถในการใช้ตัวดำเนินการลดระดับการอ้างอิง
หมายเหตุ: มีความแตกต่างใหญ่ประการหนึ่งระหว่างชนิดข้อมูล
MyBox<T>ที่เรากำลังจะสร้างกับBox<T>ที่แท้จริง: เวอร์ชันของเราจะไม่เก็บข้อมูลไว้บนฮีป เรากำลังเน้นตัวอย่างนี้ไปที่เรื่องDerefดังนั้นตำแหน่งที่เก็บข้อมูลที่แท้จริงจึงมีความสำคัญน้อยกว่าพฤติกรรมในลักษณะพอยน์เตอร์
ชนิดข้อมูล Box<T> ถูกนิยามเป็น tuple struct ที่มีสมาชิกหนึ่งตัว ดังนั้นโค้ดตัวอย่างที่ 15-8 จึงนิยามชนิดข้อมูล MyBox<T> ในลักษณะเดียวกัน เราจะนิยามฟังก์ชัน new เพื่อให้ตรงกับฟังก์ชัน new ที่นิยามไว้บน Box<T> ด้วย
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
เรานิยาม struct ชื่อ MyBox และประกาศพารามิเตอร์ทั่วไป (generic parameter) T เพราะเราต้องการให้ชนิดข้อมูลของเราถือครองค่าของชนิดข้อมูลใดก็ได้ ชนิดข้อมูล MyBox เป็น tuple struct ที่มีสมาชิกหนึ่งตัวที่เป็นชนิด T ฟังก์ชัน MyBox::new รับพารามิเตอร์หนึ่งตัวที่เป็นชนิด T และคืนค่าอินสแตนซ์ของ MyBox ที่ถือครองค่าที่ส่งเข้ามา
ลองเพิ่มฟังก์ชัน main ในโค้ดตัวอย่างที่ 15-7 ลงในโค้ดตัวอย่างที่ 15-8 และเปลี่ยนมาใช้ชนิดข้อมูล MyBox<T> ที่เรานิยามไว้แทน Box<T> โค้ดในโค้ดตัวอย่างที่ 15-9 จะยังไม่สามารถคอมไพล์ได้เนื่องจาก Rust ไม่รู้วิธีการลดระดับการอ้างอิง 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);
}
นี่คือข้อผิดพลาดในการคอมไพล์ที่เกิดขึ้น:
$ 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
ชนิดข้อมูล MyBox<T> ของเราไม่สามารถถูกลดระดับการอ้างอิงได้เพราะเรายังไม่ได้ประยุกต์ใช้ความสามารถนั้นบนชนิดข้อมูลของเรา ในการเปิดใช้งานการลดระดับการอ้างอิงด้วยตัวดำเนินการ * เราต้องประยุกต์ใช้เทรต Deref
การประยุกต์ใช้เทรต Deref
ตามที่ได้หารือในหัวข้อ “การประยุกต์ใช้เทรตบนชนิดข้อมูล” ในบทที่ 10 ในการประยุกต์ใช้เทรต เราจำเป็นต้องจัดเตรียมการประยุกต์ใช้งานสำหรับเมธอดที่เทรตต้องการ เทรต Deref ซึ่งจัดเตรียมโดยไลบรารีมาตรฐาน กำหนดให้เราต้องประยุกต์ใช้หนึ่งเมธอดชื่อ deref ที่ทำการยืม self และคืนค่าการอ้างอิงไปยังข้อมูลภายใน โค้ดตัวอย่างที่ 15-10 บรรจุการประยุกต์ใช้ 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);
}
ไวยากรณ์ type Target = T; นิยามชนิดข้อมูลที่เชื่อมโยง (associated type) สำหรับให้เทรต Deref ใช้งาน ชนิดข้อมูลที่เชื่อมโยงเป็นวิธีประกาศพารามิเตอร์ทั่วไปที่แตกต่างกันเล็กน้อย แต่คุณไม่จำเป็นต้องกังวลเกี่ยวกับพวกมันในตอนนี้ เราจะครอบคลุมรายละเอียดเพิ่มเติมในบทที่ 20
เราเติมเนื้อหาของเมธอด deref ด้วย &self.0 เพื่อให้ deref คืนค่าการอ้างอิงไปยังค่าที่เราต้องการเข้าถึงด้วยตัวดำเนินการ * โปรดจำจากหัวข้อ “การสร้างชนิดข้อมูลที่แตกต่างกันด้วย Tuple Structs” ในบทที่ 5 ว่า .0 จะเข้าถึงค่าแรกใน tuple struct ตอนนี้ฟังก์ชัน main ในโค้ดตัวอย่างที่ 15-9 ที่เรียกใช้ * บนค่า MyBox<T> สามารถคอมไพล์ได้แล้ว และการยืนยันผ่านการทดสอบ!
หากไม่มีเทรต Deref คอมไพเลอร์จะสามารถลดระดับการอ้างอิงได้เฉพาะการอ้างอิง & เท่านั้น เมธอด deref ช่วยให้คอมไพเลอร์มีความสามารถในการนำค่าของชนิดข้อมูลใดก็ตามที่ประยุกต์ใช้ Deref มาเรียกใช้เมธอด deref เพื่อให้ได้การอ้างอิงที่มันรู้วิธีการลดระดับการอ้างอิง
เมื่อเราพิมพ์ *y ในโค้ดตัวอย่างที่ 15-9 เบื้องหลังแล้ว Rust รันโค้ดนี้จริง ๆ:
*(y.deref())
Rust จะแทนที่ตัวดำเนินการ * ด้วยการเรียกใช้เมธอด deref แล้วตามด้วยการลดระดับการอ้างอิงแบบธรรมดา เพื่อให้เราไม่ต้องคิดว่าจำเป็นต้องเรียกใช้เมธอด deref หรือไม่ ฟีเจอร์นี้ของ Rust ช่วยให้เราเขียนโค้ดที่ทำงานได้เหมือนกันทุกประการ ไม่ว่าเราจะมี การอ้างอิงทั่วไปหรือชนิดข้อมูลที่ประยุกต์ใช้ Deref
เหตุผลที่เมธอด deref คืนค่าเป็นการอ้างอิงไปยังค่า และการลดระดับการอ้างอิงธรรมดานอกวงเล็บใน *(y.deref()) ยังคงมีความจำเป็น มีความเกี่ยวข้องกับระบบความเป็นเจ้าของ (ownership system) หากเมธอด deref คืนค่าเป็นค่านั้นโดยตรงแทนที่จะเป็น การอ้างอิงไปยังค่า ค่านั้นจะถูกย้ายออกไปจาก self เราไม่ได้ต้องการครอบครองความเป็นเจ้าของของค่าภายใน MyBox<T> ในกรณีนี้ หรือในกรณีส่วนใหญ่ที่เราใช้ตัวดำเนินการลดระดับการอ้างอิง
สังเกตว่าตัวดำเนินการ * ถูกแทนที่ด้วยการเรียกเมธอด deref แล้วตามด้วยการเรียกตัวดำเนินการ * เพียงครั้งเดียว ในแต่ละครั้งที่เราใช้ * ในโค้ดของเรา เนื่องจากการแทนที่ตัวดำเนินการ * ไม่ได้ทำงานแบบซ้ำเป็นอนันต์ เราจึงจบลงด้วยข้อมูลชนิด i32 ซึ่งตรงกับ 5 ใน assert_eq! ในโค้ดตัวอย่างที่ 15-9
การใช้ Deref Coercion ในฟังก์ชันและเมธอด
Deref coercion จะแปลงการอ้างอิงไปยังชนิดข้อมูลที่ประยุกต์ใช้เทรต Deref ให้กลายเป็นการอ้างอิงไปยังอีกชนิดข้อมูลหนึ่ง ตัวอย่างเช่น deref coercion สามารถแปลง &String เป็น &str ได้ เพราะ String ประยุกต์ใช้เทรต Deref ในลักษณะที่มันคืนค่าเป็น &str deref coercion เป็นความสะดวกสบายที่ Rust ปฏิบัติต่ออาร์กิวเมนต์ที่ส่งให้ฟังก์ชันและเมธอด และจะทำงานเฉพาะกับชนิดข้อมูลที่ประยุกต์ใช้เทรต Deref เท่านั้น สิ่งนี้จะเกิดขึ้นโดยอัตโนมัติเมื่อเราส่งการอ้างอิงไปยังค่าของชนิดข้อมูลเฉพาะเป็นอาร์กิวเมนต์ให้กับฟังก์ชันหรือเมธอดที่ไม่ตรงกับชนิดพารามิเตอร์ในการนิยามฟังก์ชันหรือเมธอดนั้น ลำดับของการเรียกใช้เมธอด deref จะแปลงชนิดข้อมูลที่เราจัดเตรียมไว้ให้กลายเป็นชนิดข้อมูลที่พารามิเตอร์ต้องการ
Deref coercion ถูกเพิ่มเข้ามาใน Rust เพื่อให้โปรแกรมเมอร์ที่เขียนการเรียกฟังก์ชันและเมธอดไม่ต้องเพิ่มการอ้างอิงและการลดระดับการอ้างอิงอย่างชัดเจนด้วย & และ * มากจนเกินไป ฟีเจอร์ deref coercion ยังช่วยให้เราเขียนโค้ดที่สามารถทำงานได้ทั้งสำหรับการอ้างอิงหรือพอยน์เตอร์อัจฉริยะได้มากขึ้น
ในการดู deref coercion ในการทำงานจริง ให้เราใช้ชนิดข้อมูล MyBox<T> ที่เรานิยามไว้ในโค้ดตัวอย่างที่ 15-8 รวมถึงการประยุกต์ใช้ Deref ที่เราเพิ่มลงไปในโค้ดตัวอย่างที่ 15-10 โค้ดตัวอย่างที่ 15-11 แสดงการนิยามฟังก์ชันที่มีพารามิเตอร์สไลซ์ข้อความ (string slice)
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
เราสามารถเรียกใช้ฟังก์ชัน hello โดยใช้อาร์กิวเมนต์สไลซ์ข้อความได้ เช่น hello("Rust"); เป็นต้น Deref coercion ช่วยให้สามารถเรียกใช้ hello ด้วยการอ้างอิงไปยังค่าชนิด MyBox<String> ได้ ดังแสดงในโค้ดตัวอย่างที่ 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 ด้วยอาร์กิวเมนต์ &m ซึ่งเป็นการอ้างอิงไปยังค่า MyBox<String> เนื่องจากเราได้ประยุกต์ใช้เทรต Deref บน MyBox<T> ในโค้ดตัวอย่างที่ 15-10 Rust จึงสามารถเปลี่ยน &MyBox<String> เป็น &String ได้โดยการเรียกใช้ deref ไลบรารีมาตรฐานได้จัดเตรียมการประยุกต์ใช้ Deref บน String ที่คืนค่าสไลซ์ข้อความไว้ให้ ซึ่งมีอยู่ในเอกสาร API สำหรับ Deref Rust จะเรียก deref อีกครั้งเพื่อเปลี่ยน &String ให้เป็น &str ซึ่งตรงกับการนิยามของฟังก์ชัน hello
หาก Rust ไม่ได้ประยุกต์ใช้ deref coercion เราจะต้องเขียนโค้ดในโค้ดตัวอย่างที่ 15-13 แทนโค้ดในโค้ดตัวอย่างที่ 15-12 เพื่อเรียกใช้ hello ด้วยค่าชนิด &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) จะลดระดับการอ้างอิง MyBox<String> ให้กลายเป็น String จากนั้น & และ [..] จะรับสไลซ์ข้อความของ String นั้นซึ่งเท่ากับสตริงทั้งหมดเพื่อความตรงกับลายเซ็นของ hello โค้ดนี้หากไม่มี deref coercion จะอ่าน เขียน และทำความเข้าใจได้ยากกว่าเนื่องจากมีสัญลักษณ์ทั้งหมดเข้ามาเกี่ยวข้อง Deref coercion ช่วยให้ Rust จัดการการแปลงเหล่านี้ให้เราโดยอัตโนมัติ
เมื่อเทรต Deref ถูกนิยามขึ้นสำหรับชนิดข้อมูลที่เกี่ยวข้อง Rust จะวิเคราะห์ชนิดข้อมูลและใช้ Deref::deref บ่อยเท่าที่จำเป็นเพื่อให้ได้การอ้างอิงที่ตรงกับชนิดของพารามิเตอร์ จำนวนครั้งที่ต้องแทรก Deref::deref จะถูกตัดสินใจในขณะคอมไพล์ ดังนั้นจึงไม่มีบทลงโทษด้านเวลาทำงาน (runtime penalty) สำหรับการใช้ประโยชน์จาก deref coercion!
การจัดการ Deref Coercion กับการอ้างอิงแบบแก้ไขได้
คล้ายกับวิธีที่คุณใช้เทรต Deref เพื่อเขียนทับตัวดำเนินการ * บนการอ้างอิงแบบแก้ไขไม่ได้ คุณสามารถใช้เทรต DerefMut เพื่อเขียนทับตัวดำเนินการ * บนการอ้างอิงแบบแก้ไขได้ (mutable references) ได้
Rust จะทำ deref coercion เมื่อพบชนิดข้อมูลและการประยุกต์ใช้เทรตใน 3 กรณี:
- จาก
&Tเป็น&Uเมื่อT: Deref<Target=U> - จาก
&mut Tเป็น&mut Uเมื่อT: DerefMut<Target=U> - จาก
&mut Tเป็น&Uเมื่อT: Deref<Target=U>
สองกรณีแรกเหมือนกันยกเว้นว่ากรณีที่สองจะประยุกต์ใช้ความสามารถในการแก้ไขได้ กรณีแรกระบุว่าหากคุณมี &T และ T ประยุกต์ใช้ Deref ไปยังชนิดข้อมูล U บางชนิด คุณจะสามารถรับ &U ได้อย่างโปร่งใส กรณีที่สองระบุว่า deref coercion เดียวกันจะเกิดขึ้นสำหรับการอ้างอิงแบบแก้ไขได้
กรณีที่สามจะซับซ้อนกว่า: Rust จะบังคับชนิดข้อมูลจากการอ้างอิงแบบแก้ไขได้ไปเป็นการอ้างอิงแบบแก้ไขไม่ได้ด้วย แต่ทิศทางตรงกันข้ามจะไม่สามารถทำได้: การอ้างอิงแบบแก้ไขไม่ได้จะไม่มีวันบังคับชนิดข้อมูลไปเป็น การอ้างอิงแบบแก้ไขได้ เนื่องจากกฎการยืม (borrowing rules) หากคุณมีการอ้างอิงแบบแก้ไขได้ การอ้างอิงแบบแก้ไขได้นั้นจะต้องเป็น การอ้างอิงเดียวที่ชี้ไปยังข้อมูลนั้น (มิฉะนั้นโปรแกรมจะไม่คอมไพล์) การแปลงการอ้างอิงแบบแก้ไขได้หนึ่งอันไปเป็นการอ้างอิงแบบแก้ไขไม่ได้หนึ่งอันจะไม่มีวันละเมิดกฎการยืม แต่การแปลงการอ้างอิงแบบแก้ไขไม่ได้ไปเป็น การอ้างอิงแบบแก้ไขได้ จำเป็นต้องให้การอ้างอิงแบบแก้ไขไม่ได้เริ่มต้นเป็นเพียงการอ้างอิงเดียวที่ชี้ไปยังข้อมูลนั้น ซึ่งกฎการยืมไม่สามารถรับประกันสิ่งนั้นได้ ดังนั้น Rust จึงไม่สามารถตั้งสมมติฐานได้ว่าการแปลงการอ้างอิงแบบแก้ไขไม่ได้ไปเป็น การอ้างอิงแบบแก้ไขได้ นั้นเป็นไปได้