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

ใช้ trait ที่กำหนดขึ้นเองเพื่อหลีกเลี่ยงข้อกำหนดชนิดที่ซับซ้อน (Use custom traits to avoid complex type bounds)

คำอธิบาย

ข้อกำหนดคุณสมบัติของชนิดข้อมูล (trait bounds) อาจมีความยาวและซับซ้อนจนอ่านยาก โดยเฉพาะอย่างยิ่งเมื่อต้องเกี่ยวข้องกับ trait ในตระกูล Fn1 ควบคู่ไปกับเงื่อนไขเฉพาะเจาะจงของชนิดข้อมูลผลลัพธ์ ในสถานการณ์เช่นนี้ การประกาศ trait ตัวใหม่ขึ้นมาโดยเฉพาะจะช่วยลดความเยิ่นเย้อของโค้ด ช่วยกำจัดพารามิเตอร์ของชนิดข้อมูลบางตัว และช่วยเพิ่มความชัดเจนในการสื่อความหมาย (expressiveness) ได้เป็นอย่างดี โดย trait ใหม่นี้สามารถมาพร้อมกับบล็อก impl แบบ generic สำหรับทุกๆ ชนิดข้อมูลที่สอดคล้องตามเงื่อนไขเดิม

ตัวอย่าง

ลองจินตนาการถึงระบบตรวจสอบสถานะหรือระบบเก็บรวบรวมข้อมูล โดยระบบจะต้องดึงข้อมูลหลากหลายประเภทมาจากแหล่งที่มาที่แตกต่างกัน และนำข้อมูลเหล่านั้นมาประเมินสถานะเพื่อตรวจหาความผิดปกติ เช่น ปริมาณหน่วยความจำว่างทั้งหมดจะต้องสูงกว่าค่าขีดจำกัดที่กำหนดไว้ และผู้ใช้ที่มี id หมายเลข 0 จะต้องมีชื่อว่า “root” เสมอ

เพื่อความสะดวกในการบริหารจัดการระบบ เราอาจต้องการใช้ type erasure (การซ่อนชนิดข้อมูลที่แท้จริง) ในระดับบนสุด ทว่าในขณะเดียวกัน เราก็ยังจำเป็นต้องรองรับการประเมินสถานะแบบเฉพาะเจาะจง (ที่ผู้ใช้ปรับแต่งเองได้) สำหรับแหล่งข้อมูลแต่ละประเภท (เช่น ค่าเกณฑ์และช่วงตัวเลขสำหรับข้อมูลเชิงปริมาณ) และเนื่องจากแหล่งที่มาของข้อมูลมีความหลากหลาย เราจึงอาจเลือกออกแบบให้ส่งแหล่งข้อมูลเข้ามาในรูปของ closure ที่ส่งคืนค่าเมื่อถูกเรียกใช้ และเนื่องจากข้อมูลเหล่านี้น่าจะถูกดึงมาจากระบบปฏิบัติการ การทำงานจึงมีโอกาสเกิดข้อผิดพลาดขึ้นได้เสมอ

ด้วยเหตุนี้ เราจึงอาจเริ่มต้นออกแบบโครงสร้างของชนิดข้อมูลและ trait สำหรับจัดการค่าต่างๆ ไว้ดังนี้:

#![allow(unused)]
fn main() {
use std::fmt::Display;

struct Value<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> {
    value: Option<T>,
    getter: G,
    status: S,
}

impl<G: FnMut() -> Result<T, Error>, S: Fn(&T) -> Status, T: Display> Value<G, S, T> {
    pub fn update(&mut self) -> Result<(), Error> {
        (self.getter)().map(|v| self.value = Some(v))
    }

    pub fn value(&self) -> Option<&T> {
        self.value.as_ref()
    }

    pub fn status(&self) -> Option<Status> {
        self.value().map(&self.status)
    }
}

// ...

enum Status {
    // ...
}

struct Error {
    // ...
}
}

เมื่อใช้โครงสร้างเช่นนี้ เราจะต้องเขียน trait bounds ซ้ำๆ สำหรับ G ในหลายจุด ซึ่งส่งผลกระทบต่อความสามารถในการอ่านโค้ดอย่างมาก ส่วนหนึ่งเป็นเพราะ getter มีการคืนค่าเป็น Result การสร้างข้อกำหนด trait ใหม่สำหรับ “getter” โดยเฉพาะ จะช่วยให้ trait bound สื่อความหมายได้ชัดเจนยิ่งขึ้น และช่วยตัดพารามิเตอร์ของชนิดข้อมูลออกไปได้หนึ่งตัว:

#![allow(unused)]
fn main() {
use std::fmt::Display;
trait Getter {
    type Output: Display;

    fn get_value(&mut self) -> Result<Self::Output, Error>;
}

impl<F: FnMut() -> Result<T, Error>, T: Display> Getter for F {
    type Output = T;

    fn get_value(&mut self) -> Result<Self::Output, Error> {
        self()
    }
}

struct Value<G: Getter, S: Fn(&G::Output) -> Status> {
    value: Option<G::Output>,
    getter: G,
    status: S,
}

// ...
enum Status {}
struct Error;
}

ข้อดี

การสร้าง trait ใหม่ขึ้นมาจะช่วยลดความซับซ้อนของ trait bounds ลงได้อย่างเห็นได้ชัด โดยเฉพาะการตัดพารามิเตอร์ของชนิดข้อมูลออกไป นอกจากนี้ การตั้งชื่อ trait ที่สื่อความหมายยังช่วยให้อ่านโค้ดเข้าใจได้ง่ายขึ้น และในฐานะที่เป็น abstraction ตัว trait ใหม่ยังเปิดโอกาสให้ต่อยอดประโยชน์อื่นๆ เพิ่มเติมได้อีก เช่น:

  • สามารถสร้างชนิดข้อมูลเฉพาะทางอื่นๆ ที่นำ trait นี้ไปใช้งาน (เช่น ชนิดข้อมูลที่แทนค่าคงที่เฉพาะบางอย่าง) ตลอดจนการนำ trait พื้นฐานที่มีประโยชน์ เช่น Default มาใช้งานร่วมด้วย
  • สามารถเพิ่มเมท็อดช่วยเหลืออื่นๆ เพิ่มเติมได้ ตราบใดที่เมท็อดเหล่านั้นสามารถใช้งานร่วมกับทุกชนิดข้อมูลที่เกี่ยวข้องได้

ข้อเสีย

การประกาศองค์ประกอบใหม่อย่าง trait จำเป็นต้องมีการตั้งชื่อและการจัดวางตำแหน่งในโมดูลที่เหมาะสม นอกจากนี้ยังเป็นการเพิ่มสิ่งที่ผู้ใช้งานฟังก์ชันเดิมจะต้องศึกษาทำความเข้าใจเพิ่มเติมอีกหนึ่งอย่าง2 และขึ้นอยู่กับรูปแบบการนำเสนอ ผู้ใช้อาจมองไม่ออกในทันทีว่า closure ธรรมดาก็สามารถนำมาส่งให้กับ Getter ในตัวอย่างข้างต้นได้


  1. กล่าวคือ trait Fn, FnOnce และ FnMut ↩

  2. หมายความว่าผู้ใช้อาจจำเป็นต้องอ่านเอกสารประกอบเพิ่มเติม ↩