การใช้ความไม่เหมาะสมภายในการออกแบบ API

Aug 19 2020

พื้นหลังของฉันใน C ++ ทำให้ฉันไม่สบายใจเกี่ยวกับความไม่แน่นอนภายใน รหัสด้านล่างคือการตรวจสอบของฉันเกี่ยวกับหัวข้อนี้

ฉันยอมรับว่าจากมุมมองของผู้ตรวจสอบการยืมการจัดการกับการอ้างอิงจำนวนมากในทุกโครงสร้างเดียวซึ่งสถานะภายในจะเปลี่ยนแปลงไม่ช้าก็เร็วเป็นไปไม่ได้ ชัดเจนว่าการเปลี่ยนแปลงภายในสามารถช่วยได้

ยิ่งไปกว่านั้นในบทที่15.5 "RefCell and the Interior Mutability Pattern"ของThe Rust Programming Languageตัวอย่างเกี่ยวกับMessengerลักษณะและการนำไปใช้งานในโครงสร้างMockMessengerทำให้ฉันคิดว่าเป็นการออกแบบ API ทั่วไปที่ชอบ&selfมากกว่าอย่างเป็นระบบ&mut selfแม้ว่าจะค่อนข้างชัดเจนว่า ความไม่แน่นอนบางอย่างจะมีผลบังคับไม่ช้าก็เร็ว การนำไปใช้Messengerไม่เปลี่ยนแปลงสถานะภายในเมื่อส่งข้อความได้อย่างไร ข้อยกเว้นคือเพียงแค่พิมพ์ข้อความซึ่งสอดคล้องกับ&selfแต่กรณีทั่วไปอาจประกอบด้วยการเขียนถึงสตรีมภายในบางประเภทซึ่งอาจหมายถึงการบัฟเฟอร์การอัปเดตแฟล็กข้อผิดพลาด ... ทั้งหมดนี้ต้องใช้อย่างแน่นอน&mut selfเช่นimpl Write for File.

การอาศัยความไม่แน่นอนภายในเพื่อแก้ปัญหานี้ทำให้ฉันรู้สึกเหมือนใน C ++ การconst_castใช้หรือการเหยียดหยามmutableสมาชิกเพียงเพราะที่อื่นในแอปพลิเคชันเราไม่สอดคล้องกันเกี่ยวกับconstness (ข้อผิดพลาดทั่วไปสำหรับผู้เรียน C ++)

ดังนั้นกลับไปที่โค้ดตัวอย่างของฉันด้านล่างฉันควร:

  • ใช้&mut self(คอมไพเลอร์ไม่บ่นแม้ว่าจะไม่ได้บังคับก็ตาม) จากchange_e()ถึงchange_i()เพื่อให้สอดคล้องกับความจริงที่ว่าฉันเปลี่ยนค่าของจำนวนเต็มที่เก็บไว้?
  • ใช้ต่อ&selfไปเพราะความสามารถในการเปลี่ยนแปลงภายในช่วยให้แม้ว่าฉันจะเปลี่ยนค่าของจำนวนเต็มที่เก็บไว้จริง ๆ

การตัดสินใจนี้ไม่เพียง แต่เฉพาะกับโครงสร้างเท่านั้น แต่จะมีอิทธิพลอย่างมากต่อสิ่งที่สามารถแสดงออกได้ในแอปพลิเคชันโดยใช้โครงสร้างนี้ วิธีที่สองจะช่วยได้มากอย่างแน่นอนเนื่องจากมีเพียงการอ้างอิงที่ใช้ร่วมกันเท่านั้นที่เกี่ยวข้อง แต่สอดคล้องกับสิ่งที่คาดไว้ใน Rust หรือไม่

ฉันไม่สามารถหาคำตอบให้กับคำถามนี้ในแนวทางสนิม API มีเอกสาร Rust อื่น ๆ ที่คล้ายกับC ++ CoreGuidelinesหรือไม่?

/*
    $ rustc int_mut.rs && ./int_mut
     initial:   1   2   3   4   5   6   7   8   9
    change_a:  11   2   3   4   5   6   7   8   9
    change_b:  11  22   3   4   5   6   7   8   9
    change_c:  11  22  33   4   5   6   7   8   9
    change_d:  11  22  33  44   5   6   7   8   9
    change_e:  11  22  33  44  55   6   7   8   9
    change_f:  11  22  33  44  55  66   7   8   9
    change_g:  11  22  33  44  55  66  77   8   9
    change_h:  11  22  33  44  55  66  77  88   9
    change_i:  11  22  33  44  55  66  77  88  99
*/

struct Thing {
    a: i32,
    b: std::boxed::Box<i32>,
    c: std::rc::Rc<i32>,
    d: std::sync::Arc<i32>,
    e: std::sync::Mutex<i32>,
    f: std::sync::RwLock<i32>,
    g: std::cell::UnsafeCell<i32>,
    h: std::cell::Cell<i32>,
    i: std::cell::RefCell<i32>,
}

impl Thing {
    fn new() -> Self {
        Self {
            a: 1,
            b: std::boxed::Box::new(2),
            c: std::rc::Rc::new(3),
            d: std::sync::Arc::new(4),
            e: std::sync::Mutex::new(5),
            f: std::sync::RwLock::new(6),
            g: std::cell::UnsafeCell::new(7),
            h: std::cell::Cell::new(8),
            i: std::cell::RefCell::new(9),
        }
    }

    fn show(&self) -> String // & is enough (read-only)
    {
        format!(
            "{:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3} {:3}",
            self.a,
            self.b,
            self.c,
            self.d,
            self.e.lock().unwrap(),
            self.f.read().unwrap(),
            unsafe { *self.g.get() },
            self.h.get(),
            self.i.borrow(),
        )
    }

    fn change_a(&mut self) // &mut is mandatory
    {
        let target = &mut self.a;
        *target += 10;
    }

    fn change_b(&mut self) // &mut is mandatory
    {
        let target = self.b.as_mut();
        *target += 20;
    }

    fn change_c(&mut self) // &mut is mandatory
    {
        let target = std::rc::Rc::get_mut(&mut self.c).unwrap();
        *target += 30;
    }

    fn change_d(&mut self) // &mut is mandatory
    {
        let target = std::sync::Arc::get_mut(&mut self.d).unwrap();
        *target += 40;
    }

    fn change_e(&self) // !!! no &mut here !!!
    {
        // With C++, a std::mutex protecting a separate integer (e)
        // would have been used as two data members of the structure.
        // As our intent is to alter the integer (e), and because
        // std::mutex::lock() is _NOT_ const (but it's an internal
        // that could have been hidden behind the mutable keyword),
        // this member function would _NOT_ be const in C++.
        // But here, &self (equivalent of a const member function)
        // is accepted although we actually change the internal
        // state of the structure (the protected integer).
        let mut target = self.e.lock().unwrap();
        *target += 50;
    }

    fn change_f(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e)
        let mut target = self.f.write().unwrap();
        *target += 60;
    }

    fn change_g(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f)
        let target = self.g.get();
        unsafe { *target += 70 };
    }

    fn change_h(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g)
        self.h.set(self.h.get() + 80);
    }

    fn change_i(&self) // !!! no &mut here !!!
    {
        // actually alters the integer (as with e, f, g, h)
        let mut target = self.i.borrow_mut();
        *target += 90;
    }
}

fn main() {
    let mut t = Thing::new();
    println!(" initial: {}", t.show());
    t.change_a();
    println!("change_a: {}", t.show());
    t.change_b();
    println!("change_b: {}", t.show());
    t.change_c();
    println!("change_c: {}", t.show());
    t.change_d();
    println!("change_d: {}", t.show());
    t.change_e();
    println!("change_e: {}", t.show());
    t.change_f();
    println!("change_f: {}", t.show());
    t.change_g();
    println!("change_g: {}", t.show());
    t.change_h();
    println!("change_h: {}", t.show());
    t.change_i();
    println!("change_i: {}", t.show());
}

คำตอบ

7 trentcl Aug 19 2020 at 15:59

การอาศัยความไม่แน่นอนภายในเพื่อแก้ปัญหานี้ทำให้ฉันรู้สึกเหมือนใน C ++ การconst_castใช้หรือการเหยียดหยามmutableสมาชิกเพียงเพราะที่อื่นในแอปพลิเคชันเราไม่สอดคล้องกันเกี่ยวกับconstness (ข้อผิดพลาดทั่วไปสำหรับผู้เรียน C ++)

นี่เป็นความคิดที่เข้าใจได้อย่างสมบูรณ์ในบริบทของ C ++ สาเหตุที่ไม่ถูกต้องเนื่องจาก C ++ และ Rust มีแนวคิดเกี่ยวกับความผันแปรที่แตกต่างกัน

ในทางหนึ่งmutคำหลักของ Rust มีสองความหมาย ในรูปแบบนั้นมีความหมายว่า "mutable" และในประเภทการอ้างอิงจะหมายถึง "เอกสิทธิ์" ความแตกต่างระหว่าง&selfและ&mut selfไม่ได้จริงๆว่าselfสามารถกลายพันธุ์หรือไม่ แต่ไม่ว่าจะสามารถaliased

ในMessengerตัวอย่างก่อนอื่นอย่าเอาจริงเอาจังเกินไป หมายถึงการแสดงคุณลักษณะของภาษาไม่จำเป็นต้องออกแบบระบบ แต่เราสามารถจินตนาการได้ว่าเหตุใดจึง&selfอาจถูกนำมาใช้: Messengerหมายถึงการใช้งานโดยโครงสร้างที่ใช้ร่วมกันดังนั้นโค้ดที่แตกต่างกันจึงสามารถเก็บข้อมูลอ้างอิงไปยังวัตถุเดียวกันและใช้เพื่อsendแจ้งเตือนโดยไม่ประสานกัน หากsendจะใช้&mut selfก็จะไม่มีประโยชน์สำหรับจุดประสงค์นี้เพราะสามารถมี&mut selfการอ้างอิงได้ครั้งละหนึ่งรายการเท่านั้น เป็นไปไม่ได้ที่จะส่งข้อความไปยังผู้แชร์Messenger(โดยไม่ต้องเพิ่มเลเยอร์ภายนอกของความสามารถในการเปลี่ยนแปลงภายในผ่านMutexหรือบางสิ่งบางอย่าง)

ในทางกลับกันการอ้างอิงและตัวชี้ C ++ ทุกตัวสามารถใช้นามแฝงได้¹ดังนั้นในแง่สนิมการเปลี่ยนแปลงทั้งหมดใน C ++ คือความไม่แน่นอน "ภายใน"! Rust ไม่มีความเทียบเท่ากับmutableใน C ++ เนื่องจาก Rust ไม่มีconstสมาชิก (บทกลอนในที่นี้คือ "ความสามารถในการกลายพันธุ์เป็นคุณสมบัติของการผูกไม่ใช่ประเภท") สนิมจะมีความคิดเป็นconst_castแต่สำหรับตัวชี้ดิบเพราะมันไม่ปลอดภัยที่จะเปิดใช้ร่วมกัน&อ้างอิงเป็นพิเศษ&mutอ้างอิง ในทางกลับกัน C ++ ไม่มีอะไรเหมือนCellหรือRefCellเพราะทุกค่าโดยปริยายอยู่ข้างหลังUnsafeCell.

ดังนั้นกลับไปที่โค้ดตัวอย่างของฉันด้านล่างฉันควร [... ]

จริงๆมันขึ้นอยู่กับความตั้งใจความหมายThingของ ลักษณะของThingการแชร์เช่นปลายทางของช่องหรือไฟล์หรือไม่ การchange_eเรียกใช้การอ้างอิงที่ใช้ร่วมกัน (นามแฝง) เหมาะสมหรือไม่ &selfถ้าเป็นเช่นนั้นแล้วใช้ความผันแปรภายในที่จะเปิดเผยวิธีการในการ คือThingหลักภาชนะสำหรับข้อมูลหรือไม่? บางครั้งมันสมเหตุสมผลหรือไม่ที่จะแบ่งปันและบางครั้งก็เป็นเอกสิทธิ์เฉพาะบุคคล จากนั้นThingไม่ควรใช้ความสามารถในการเปลี่ยนแปลงภายในและปล่อยให้ผู้ใช้ห้องสมุดตัดสินใจว่าจะจัดการกับการกลายพันธุ์ที่ใช้ร่วมกันได้อย่างไรหากจำเป็น

ดูสิ่งนี้ด้วย

  • อะไรคือความแตกต่างระหว่างการวาง "mut" ก่อนชื่อตัวแปรและหลัง ":"
  • เหตุใดความผันแปรของตัวแปรจึงไม่แสดงในลายเซ็นประเภทใน Rust
  • ต้องการคำอธิบายแบบองค์รวมเกี่ยวกับเซลล์ของ Rust และประเภทการนับอ้างอิง

¹จริง, C ++ จะมีคุณลักษณะที่ทำให้ตัวชี้ทำงานคล้ายกับการอ้างอิงในสนิม ชนิดของ. restrictเป็นส่วนขยายที่ไม่ได้มาตรฐานใน C ++ แต่เป็นส่วนหนึ่งของ C99 &การอ้างอิงที่ใช้ร่วมกัน ( ) ของ Rust เป็นเหมือนconst *restrictตัวชี้และ&mutการอ้างอิงแบบ Exclusive ( ) เป็นเหมือนconst *restrictตัวชี้ ดูคำสำคัญที่ จำกัด ใน C ++ หมายถึงอะไร?

ครั้งสุดท้ายที่คุณจงใจใช้ตัวชี้restrict(หรือ__restrictฯลฯ ) ใน C ++ คือเมื่อใด อย่ากังวลกับมัน คำตอบคือ "ไม่เคย" restrictเปิดใช้งานการเพิ่มประสิทธิภาพเชิงรุกมากกว่าตัวชี้ทั่วไป แต่ยากมากที่จะใช้อย่างถูกต้องเนื่องจากคุณต้องระมัดระวังอย่างมากเกี่ยวกับการใส่นามแฝงและคอมไพเลอร์ไม่ให้ความช่วยเหลือ มันเป็นปืนพกขนาดใหญ่และแทบไม่มีใครใช้มัน เพื่อให้คุ้มค่าที่จะใช้restrictวิธีที่คุณใช้constใน C ++ อย่างแพร่หลายคุณจะต้องสามารถใส่คำอธิบายประกอบลงในฟังก์ชันที่พอยน์เตอร์ได้รับอนุญาตให้ใช้นามแฝงอื่น ๆ ในเวลานั้นให้สร้างกฎบางอย่างเกี่ยวกับเวลาที่พอยน์เตอร์สามารถปฏิบัติตามได้ และมีคอมไพเลอร์พาสที่ตรวจสอบว่ามีการปฏิบัติตามกฎในแต่ละฟังก์ชันหรือไม่ ชอบ ...