Penyalahgunaan mutabilitas interior dalam desain API?

Aug 19 2020

Latar belakang saya di C ++ membuat saya tidak nyaman tentang mutabilitas interior . Kode di bawah ini adalah investigasi saya seputar topik ini.

Saya setuju bahwa, dari sudut pandang pemeriksa peminjam, berurusan dengan banyak referensi pada setiap struct yang keadaan internalnya dapat diubah cepat atau lambat tidak mungkin; itu jelas di mana mutabilitas interior dapat membantu.

Selain itu, dalam bab 15,5 "RefCell dan Pola Interior berubah-ubah" dari The Rust Bahasa Pemrograman , contoh tentang Messengersifat dan implementasinya pada MockMessengerstruct membuat saya berpikir bahwa itu adalah desain API umum untuk secara sistematis memilih &selflebih &mut selfbahkan jika cukup jelas bahwa semacam mutabilitas akan menjadi wajib segera atau nanti. Bagaimana implementasi Messengertidak mengubah keadaan internalnya saat mengirim pesan? Pengecualiannya hanyalah mencetak pesan, yang konsisten dengan &self, tetapi kasus umum mungkin terdiri dari penulisan ke beberapa jenis aliran dalam, yang dapat menyiratkan buffering, memperbarui tanda kesalahan ... Semua ini tentu saja membutuhkan &mut self, sebagai contohimpl Write for File.

Mengandalkan mutabilitas interior untuk menyelesaikan masalah ini terdengar seperti, di C ++, const_casting atau penyalahgunaan mutableanggota hanya karena di tempat lain dalam aplikasi kami tidak konsisten tentang constness (kesalahan umum untuk pelajar C ++).

Jadi, kembali ke contoh kode saya di bawah ini, haruskah saya:

  • gunakan &mut self(kompilator tidak mengeluh, meskipun tidak wajib) dari change_e()ke change_i()agar tetap konsisten dengan fakta bahwa saya mengubah nilai bilangan bulat yang disimpan?
  • tetap menggunakan &self, karena mutabilitas interior memungkinkannya, bahkan jika saya benar-benar mengubah nilai integer yang disimpan?

Keputusan ini tidak hanya bersifat lokal untuk struct itu sendiri tetapi akan memiliki pengaruh besar pada apa yang dapat diekspresikan dalam aplikasi yang menggunakan struct ini. Solusi kedua pasti akan banyak membantu, karena hanya referensi bersama yang dilibatkan, tetapi apakah itu konsisten dengan apa yang diharapkan di Rust.

Saya tidak dapat menemukan jawaban untuk pertanyaan ini di Rust API Guidelines . Apakah ada dokumentasi Rust lain yang mirip dengan 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());
}

Jawaban

7 trentcl Aug 19 2020 at 15:59

Mengandalkan mutabilitas interior untuk menyelesaikan masalah ini terdengar seperti, di C ++, const_casting atau penyalahgunaan mutableanggota hanya karena di tempat lain dalam aplikasi kami tidak konsisten tentang constness (kesalahan umum untuk pelajar C ++).

Ini adalah pemikiran yang sepenuhnya dapat dimengerti dalam konteks C ++. Alasan tidak akurat adalah karena C ++ dan Rust memiliki konsep mutabilitas yang berbeda.

Bisa dibilang, mutkata kunci Rust sebenarnya memiliki dua arti. Dalam pola itu berarti "bisa berubah" dan dalam jenis referensi itu berarti "eksklusif". Perbedaan antara &selfdan &mut selfbukanlah benar-benar selfbisa dimutasi atau tidak, tapi apakah bisa alias .

Dalam Messengercontoh, pertama jangan menganggapnya terlalu serius; ini dimaksudkan untuk mengilustrasikan fitur bahasa, tidak harus desain sistem. Tetapi kita dapat membayangkan mengapa &selfmungkin digunakan: Messengerdimaksudkan untuk diimplementasikan oleh struktur yang digunakan bersama , sehingga potongan kode yang berbeda dapat menyimpan referensi ke objek yang sama dan menggunakannya untuk sendmemberi peringatan tanpa berkoordinasi satu sama lain. Jika senddiambil &mut self, itu tidak akan berguna untuk tujuan ini karena hanya ada satu &mut selfreferensi yang ada pada satu waktu. Tidak mungkin untuk mengirim pesan ke yang dibagikan Messenger(tanpa menambahkan lapisan eksternal mutabilitas interior melalui Mutexatau sesuatu).

Di sisi lain, setiap referensi dan penunjuk C ++ dapat dialiasi.¹ Jadi dalam istilah Rust, semua mutabilitas di C ++ adalah mutabilitas "interior"! Rust tidak memiliki padanan dengan mutabledi C ++ karena Rust tidak memiliki constanggota (slogannya di sini adalah "mutabilitas adalah properti pengikatan, bukan tipe"). Rust memang memiliki padanan dengan const_cast, tetapi hanya untuk petunjuk mentah, karena tidak masuk akal untuk mengubah &referensi bersama menjadi &mutreferensi eksklusif . Sebaliknya, C ++ tidak memiliki like Cellatau RefCellkarena setiap nilai secara implisit berada di belakang UnsafeCellsudah.

Jadi, kembali ke contoh kode saya di bawah ini, haruskah saya [...]

Itu sangat tergantung pada semantik yang dimaksudkan dari Thing. Apakah itu sifat Thinguntuk dibagikan, seperti titik akhir saluran atau file? Apakah masuk akal untuk change_edipanggil dengan referensi bersama (alias)? Jika demikian, maka gunakan mutabilitas interior untuk mengekspos metode &self. Apakah pada Thingdasarnya merupakan wadah untuk data? Apakah terkadang masuk akal untuk dibagikan dan terkadang eksklusif? Maka Thingmungkin sebaiknya tidak menggunakan mutabilitas interior dan membiarkan pengguna perpustakaan memutuskan bagaimana menangani mutasi bersama, jika perlu.

Lihat juga

  • Apa perbedaan antara menempatkan "mut" sebelum nama variabel dan setelah ":"?
  • Mengapa mutabilitas variabel tidak tercermin dalam tanda tangan tipenya di Rust?
  • Perlu penjelasan holistik tentang sel Rust dan jenis referensi dihitung

¹ Sebenarnya, C ++ tidak memiliki fitur yang membuat pointer bekerja sama dengan referensi di Rust. Agak. restrictadalah ekstensi non-standar di C ++ tetapi merupakan bagian dari C99. &Referensi bersama ( ) Rust seperti const *restrictpointer, dan &mutreferensi eksklusif ( ) seperti non- const *restrictpointer. Lihat Apa arti kata kunci batasi di C ++?

Kapan terakhir kali Anda dengan sengaja menggunakan penunjuk restrict(atau __restrict, dll.) Di C ++? Jangan repot-repot memikirkannya; jawabannya adalah "tidak pernah". restrictmengaktifkan pengoptimalan yang lebih agresif daripada pointer biasa, tetapi sangat sulit untuk menggunakannya dengan benar karena Anda harus sangat berhati-hati tentang aliasing, dan kompilator tidak menawarkan bantuan. Ini pada dasarnya adalah pistol besar dan hampir tidak ada yang menggunakannya. Untuk membuatnya bermanfaat untuk digunakan secara restrictluas seperti yang Anda gunakan constdalam C ++, Anda harus dapat membuat anotasi ke fungsi yang diizinkan untuk alias yang lain pada saat itu, buat beberapa aturan tentang kapan pointer valid untuk diikuti, dan memiliki compiler pass yang memeriksa apakah aturan diikuti di setiap fungsi. Seperti semacam ... pemeriksa.