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

#[non_exhaustive] และฟิลด์แบบ private เพื่อการขยายต่อในอนาคต

คำอธิบาย

มีสถานการณ์จำนวนหนึ่งที่ผู้พัฒนาไลบรารีต้องการเพิ่มฟิลด์แบบ public ให้แก่ struct สาธารณะ หรือเพิ่ม variant ใหม่ๆ ให้แก่ enum ในอนาคต โดยไม่ทำให้โค้ดของผู้เรียกใช้งานเดิมเกิดข้อผิดพลาดขึ้น (รักษาสภาพความเข้ากันได้ย้อนหลัง หรือ Backwards Compatibility)

ภาษา Rust มีทางออกสำหรับโจทย์ข้อนี้ 2 แนวทาง:

  • กำหนดแอตทริบิวต์ #[non_exhaustive] บน struct, enum หรือบนแต่ละ variant ของ enum (สามารถอ่านเอกสารฉบับละเอียดเกี่ยวกับทุกจุดที่รองรับ #[non_exhaustive] ได้ที่ เอกสารประกอบ)

  • เพิ่มฟิลด์แบบ private (Private field) ไว้ใน struct เพื่อป้องกันไม่ให้ผู้เรียกใช้งานสร้างอินสแตนซ์ผ่าน struct literal โดยตรง หรือเขียน Pattern Matching ครอบคลุมทุกฟิลด์ (ดูรายละเอียดในหัวข้อทางเลือก)

ตัวอย่าง

#![allow(unused)]
fn main() {
mod a {
    // Public struct.
    #[non_exhaustive]
    pub struct S {
        pub foo: i32,
    }

    #[non_exhaustive]
    pub enum AdmitMoreVariants {
        VariantA,
        VariantB,
        #[non_exhaustive]
        VariantC {
            a: String,
        },
    }
}

fn print_matched_variants(s: a::S) {
    // Because S is `#[non_exhaustive]`, it cannot be named here and
    // we must use `..` in the pattern.
    let a::S { foo: _, .. } = s;

    let some_enum = a::AdmitMoreVariants::VariantA;
    match some_enum {
        a::AdmitMoreVariants::VariantA => println!("it's an A"),
        a::AdmitMoreVariants::VariantB => println!("it's a b"),

        // .. required because this variant is non-exhaustive as well
        a::AdmitMoreVariants::VariantC { a, .. } => println!("it's a c"),

        // The wildcard match is required because more variants may be
        // added in the future
        _ => println!("it's a new variant"),
    }
}
}

ทางเลือก: Private fields สำหรับ struct

#[non_exhaustive] จะทำงานเฉพาะการข้ามขอบเขตระหว่าง crate เท่านั้น ส่วนการทำงานภายใน crate เดียวกัน คุณสามารถใช้วิธีเพิ่มฟิลด์แบบ private เข้ามาช่วยแทนได้

ตามปกติแล้ว การเพิ่มฟิลด์ใหม่เข้าไปใน struct ถือเป็นการเปลี่ยนแปลงที่เข้ากันได้ย้อนหลังเป็นส่วนใหญ่ อย่างไรก็ดี หากผู้เรียกใช้งานเขียนโค้ด Pattern Matching แบบแยกส่วนประกอบ (Destructure) โดยระบุชื่อฟิลด์ไว้ครบทุกฟิลด์ การที่เราเพิ่มฟิลด์ใหม่เข้าไปจะส่งผลให้โค้ดของพวกเขาคอมไพล์ไม่ผ่านทันที แต่หากผู้เรียกใช้งานระบุชื่อเฉพาะฟิลด์ที่สนใจและใส่ .. ต่อท้าย การเพิ่มฟิลด์ใหม่ก็จะไม่กระทบต่อโค้ดเดิมเลย ดังนั้น การกำหนดให้มีฟิลด์แบบ private อย่างน้อยหนึ่งฟิลด์ใน struct จะบีบให้ผู้เรียกใช้งานต้องใช้รูปแบบ .. เสมอ ซึ่งช่วยรับประกันว่า struct ของเราจะรองรับการเปลี่ยนแปลงในอนาคตได้อย่างปลอดภัย

ข้อเสียของแนวทางนี้คือ คุณอาจจำเป็นต้องเพิ่มฟิลด์ที่ไม่ได้ใช้งานจริงเข้ามาใน struct แต่คุณสามารถเลือกใช้ Unit Type () เพื่อไม่ให้มีภาระด้านหน่วยความจำตอนรันไทม์ (Zero runtime overhead) และใส่เครื่องหมาย _ นำหน้าชื่อฟิลด์เพื่อปิดคำเตือนตัวแปรไม่ได้ใช้งานของคอมไพเลอร์ได้

#![allow(unused)]
fn main() {
pub struct S {
    pub a: i32,
    // Because `b` is private, you cannot match on `S` without using `..` and `S`
    //  cannot be directly instantiated or matched against
    _b: (),
}
}

เจาะลึกรายละเอียด

สำหรับ struct แล้ว การใส่ #[non_exhaustive] ช่วยให้เราสามารถเพิ่มฟิลด์ใหม่ๆ ในอนาคตได้อย่างปลอดภัยโดยไม่ทำลายความเข้ากันได้ย้อนหลัง และยังช่วยป้องกันไม่ให้ผู้เรียกใช้งานสร้างออบเจกต์ผ่าน struct literal ตรงๆ แม้ว่าทุกฟิลด์จะเป็น public ก็ตาม สิ่งนี้อาจมีประโยชน์ แต่ก็ควรพิจารณาให้รอบคอบว่าคุณต้องการให้ผู้ใช้พบข้อผิดพลาดจากคอมไพเลอร์ทันทีเมื่อมีการเพิ่มฟิลด์ใหม่ หรือต้องการให้โค้ดทำงานผ่านไปอย่างเงียบๆ

นอกจากนี้ เรายังสามารถใส่ #[non_exhaustive] ให้กับแต่ละ variant ของ enum ได้เช่นกัน ซึ่งพฤติกรรมของ variant ดังกล่าวจะเหมือนกับ struct ที่เป็น #[non_exhaustive] ทุกประการ

ขอแนะนำให้ใช้เครื่องมือนี้อย่างตั้งใจและระมัดระวัง: บ่อยครั้งที่การปรับเพิ่มเลขเวอร์ชันหลัก (Major Version) เมื่อมีการเพิ่มฟิลด์หรือ variant ใหม่ มักจะเป็นทางเลือกที่ชัดเจนและตรงไปตรงมากว่า #[non_exhaustive] อาจเหมาะสมอย่างยิ่งในสถานการณ์ที่คุณกำลังจำลองข้อมูลจากแหล่งภายนอกที่อาจมีการเปลี่ยนแปลงตลอดเวลาโดยไม่ขึ้นกับวงรอบการปล่อยไลบรารีของคุณ แต่มันไม่ได้ถูกออกแบบมาเพื่อเป็นเครื่องมืออเนกประสงค์สำหรับทุกกรณี

ข้อเสีย

การใช้ #[non_exhaustive] อาจทำให้โค้ดฝั่งผู้เรียกใช้งานมีความสะดวกสบาย (Ergonomics) ลดลงอย่างมาก โดยเฉพาะเมื่อถูกบีบให้ต้องเขียนโค้ดดักจับ enum variant ที่ตนเองไม่รู้จัก ดังนั้นจึงควรเลือกใช้เฉพาะเมื่อคุณมีความจำเป็นอย่างยิ่งที่จะต้องพัฒนาฟีเจอร์เพิ่มเติมโดยไม่ต้องการปรับเพิ่มเลขเวอร์ชันหลักเท่านั้น

เมื่อนำ #[non_exhaustive] ไปใช้กับ enum มันจะบังคับให้ผู้เรียกใช้งานทุกคนต้องเขียนกรณี Wildcard (_ => ...) เสมอ และหากในทางปฏิบัติไม่มีการกระทำใดที่สมเหตุสมผลสำหรับกรณีที่ไม่รู้จักนั้น ก็อาจนำไปสู่การเขียนโค้ดที่อึดอัด หรือโค้ดที่ไม่มีวันถูกเรียกใช้งานจริง และหากผู้ใช้เลือกที่จะใส่ panic!() ดักไว้ในกรณีนั้น การแจ้งเตือนเป็น Compile Error ตั้งแต่แรกก็น่าจะเป็นทางเลือกที่ดีกว่าอย่างแน่นอน

ดูเพิ่มเติม