พอลิมอร์ฟิซึมผ่าน Deref
คำอธิบาย
การนำ trait Deref ไปใช้อย่างผิดวัตถุประสงค์เพื่อเลียนแบบการสืบทอดคุณสมบัติ (inheritance) ระหว่าง struct ด้วยเป้าหมายที่จะนำเมท็อดกลับมาใช้ซ้ำ
ตัวอย่าง
ในบางครั้ง เราอาจต้องการเลียนแบบแพตเทิร์นยอดนิยมจากภาษาเชิงวัตถุ (Object-Oriented) อย่าง Java ดังนี้:
class Foo {
void m() { ... }
}
class Bar extends Foo {}
public static void main(String[] args) {
Bar b = new Bar();
b.m();
}
เราสามารถใช้แอนติแพตเทิร์นการทำพอลิมอร์ฟิซึมผ่าน Deref เพื่อจำลองพฤติกรรมดังกล่าวได้ดังนี้:
use std::ops::Deref;
struct Foo {}
impl Foo {
fn m(&self) {
//..
}
}
struct Bar {
f: Foo,
}
impl Deref for Bar {
type Target = Foo;
fn deref(&self) -> &Foo {
&self.f
}
}
fn main() {
let b = Bar { f: Foo {} };
b.m();
}
ในภาษา Rust นั้นไม่มีระบบการสืบทอด (inheritance) สำหรับ struct เราจึงใช้วิธีประกอบออบเจกต์ (composition) โดยนำอินสแตนซ์ของ Foo มาบรรจุไว้ใน Bar แทน (เนื่องจากฟิลด์ดังกล่าวเก็บค่าโดยตรง จึงถูกจัดเก็บบนหน่วยความจำแบบอินไลน์ ดังนั้นหากมีฟิลด์ข้อมูล เลย์เอาต์ในหน่วยความจำก็จะเหมือนกับเวอร์ชันของ Java (โดยประมาณ ซึ่งหากต้องการความแน่นอน 100% ควรระบุ #[repr(C)] กำกับไว้))
และเพื่อให้การเรียกเมท็อดทำงานได้ เราจึงอิมพลีเมนต์ Deref ให้กับ Bar โดยกำหนดให้ Foo เป็น target (พร้อมคืนค่าฟิลด์ Foo ที่ฝังอยู่ภายใน) นั่นหมายความว่าเมื่อใดก็ตามที่เราทำ dereference กับตัวแปร Bar (เช่น การใช้โอเปอเรเตอร์ *) เราจะได้ค่า Foo กลับมา ซึ่งถือว่าแปลกประหลาดมาก เพราะโดยปกติแล้วการ dereference จะเป็นการดึงค่า T ออกมาจากเรฟเฟอเรนซ์ของ T แต่ในกรณีนี้เรากลับได้ชนิดข้อมูลสองชนิดที่ไม่เกี่ยวข้องกันเลย อย่างไรก็ดี เนื่องจากโอเปอเรเตอร์จุด (dot operator .) มีกลไกการทำ dereference โดยอัตโนมัติ (implicit dereferencing) จึงส่งผลให้การเรียกใช้เมท็อดผ่านจุดจะวิ่งไปค้นหาเมท็อดของ Foo นอกเหนือจากเมท็อดของ Bar ด้วย
ข้อดี
ช่วยลดโค้ด boilerplate ลงได้เล็กน้อย ตัวอย่างเช่น ไม่ต้องเขียนส่งต่อเมท็อดเองแบบนี้:
impl Bar {
fn m(&self) {
self.f.m()
}
}
ข้อเสีย
สิ่งที่สำคัญที่สุดคือ นี่เป็นแนวทางปฏิบัติที่สร้างความประหลาดใจ (surprising idiom) โปรแกรมเมอร์คนอื่นที่เข้ามาอ่านโค้ดนี้ในภายหลังจะคาดไม่ถึงว่าโค้ดทำงานเช่นนี้ เพราะเรากำลังใช้ trait Deref ผิดวัตถุประสงค์อย่างสิ้นเชิง แทนที่จะใช้ตามเจตนารมณ์ที่แท้จริง (และตามที่ระบุไว้ในคู่มือ) อีกทั้งกลไกการทำงานทั้งหมดยังเกิดขึ้นอย่างซ่อนเร้นโดยอัตโนมัติ (implicit)
นอกจากนี้ แพตเทิร์นนี้ไม่ได้สร้างความสัมพันธ์แบบชนิดข้อมูลย่อย (subtyping) ระหว่าง Foo กับ Bar เหมือนกับการสืบทอดใน Java หรือ C++ ยิ่งไปกว่านั้น trait ต่างๆ ที่ Foo อิมพลีเมนต์ไว้ ก็จะไม่ได้รับการอิมพลีเมนต์ให้กับ Bar โดยอัตโนมัติ ทำให้แพตเทิร์นนี้ทำงานร่วมกับการตรวจสอบขอบเขตชนิดข้อมูล (trait bounds) และการเขียนโปรแกรมเชิงเจเนอริก (generic programming) ได้แย่มาก
การใช้แพตเทิร์นนี้ยังให้ความหมายเชิงพฤติกรรมต่อ self ที่ผิดแผกไปจากภาษาเชิงวัตถุส่วนใหญ่อยู่เล็กน้อย เพราะตามปกติแล้ว self จะยังคงเป็นเรฟเฟอเรนซ์ที่ชี้ไปยังคลาสย่อย (sub-class) เสมอ แต่ในแพตเทิร์นนี้ self กลับกลายเป็น ‘คลาส’ ต้นทางที่เมท็อดนั้นถูกนิยามไว้แทน
ประการสุดท้าย แพตเทิร์นนี้รองรับเพียงการสืบทอดชั้นเดียว (single inheritance) เท่านั้น และไม่มีแนวคิดเรื่อง interface, การควบคุมระดับการเข้าถึงตามคลาส (class-based access control / privacy) หรือคุณสมบัติอื่นๆ ของการสืบทอดเลย จึงสร้างความสับสนและประหลาดใจให้กับโปรแกรมเมอร์ที่คุ้นเคยกับระบบ inheritance ใน Java เป็นอย่างมาก
เจาะลึกรายละเอียด
ในปัจจุบันยังไม่มีทางเลือกอื่นที่ดีเพียงทางเดียวอย่างสมบูรณ์ ทั้งนี้ขึ้นอยู่กับบริบทของงาน บางกรณีอาจเหมาะสมกว่าที่จะออกแบบใหม่โดยใช้ trait เข้ามาช่วย หรือเขียนเมท็อดครอบ (facade method) เพื่อส่งต่อการทำงานไปยัง Foo ด้วยตนเอง แม้ว่าทีมพัฒนา Rust จะมีความตั้งใจที่จะเพิ่มกลไกช่วยเรื่อง inheritance ในลักษณะนี้ แต่ก็คงต้องใช้เวลาอีกพอสมควรกว่าที่ฟีเจอร์ดังกล่าวจะเข้าสู่เวอร์ชัน stable สามารถอ่านรายละเอียดเพิ่มเติมได้จากบล็อก บทความเหล่านี้ และ RFC issue นี้
trait Deref ถูกออกแบบมาเพื่อการอิมพลีเมนต์สมาร์ตพอยน์เตอร์ที่กำหนดขึ้นเอง โดยมีเจตนารมณ์เพื่อแปลงจาก pointer-to-T ไปสู่ค่า T ไม่ใช่การแปลงข้ามชนิดข้อมูลที่ต่างกันอย่างสิ้นเชิง แต่น่าเสียดายที่ตัวนิยามของ trait ไม่สามารถบังคับกฎนี้ได้ (และอาจเป็นไปไม่ได้เลยที่จะบังคับในระดับภาษา)
ภาษา Rust พยายามรักษาสมดุลระหว่างกลไกแบบชัดเจน (explicit) และแบบอัตโนมัติ (implicit) โดยจะให้น้ำหนักกับการแปลงชนิดข้อมูลแบบชัดเจนเป็นหลัก การทำ dereference อัตโนมัติผ่านโอเปอเรเตอร์จุดเป็นกรณีพิเศษที่คำนึงถึงความสะดวกในการเขียนโค้ด (ergonomics) อย่างยิ่ง แต่เจตนารมณ์ดั้งเดิมก็จำกัดไว้เฉพาะการลดทอนระดับของการอ้างอิงทางอ้อม (indirection) เท่านั้น ไม่ใช่เพื่อแปลงชนิดข้อมูลใดๆ ตามอำเภอใจ
ดูเพิ่มเติม
- แนวปฏิบัติคอลเลกชันคือสมาร์ตพอยน์เตอร์
- crate ช่วยทำ delegation เพื่อลดโค้ด boilerplate เช่น delegate หรือ ambassador
- เอกสารประกอบของ trait
Deref