Abuso di mutabilità interna nella progettazione API?

Aug 19 2020

Il mio background in C ++ mi mette a disagio riguardo alla mutevolezza interiore . Il codice seguente è la mia indagine su questo argomento.

Sono d'accordo che, dal punto di vista del controllo del prestito, trattare molti riferimenti su ogni singola struttura il cui stato interno potrebbe essere alterato presto o tardi è impossibile; è chiaramente dove la mutevolezza interiore può aiutare.

Inoltre, nel capitolo 15.5 "RefCell and the Interior Mutability Pattern" del Linguaggio di programmazione Rust , l'esempio sul Messengertratto e la sua implementazione sulla MockMessengerstruttura mi fa pensare che sia un progetto API comune da preferire sistematicamente &selfrispetto a &mut selfanche se è abbastanza ovvio che una sorta di mutabilità sarà obbligatoria presto o tardi. Come potrebbe un'implementazione di Messengernon alterare il suo stato interno quando invia un messaggio? L'eccezione è solo la stampa del messaggio, che è coerente con &self, ma il caso generale consisterebbe probabilmente nella scrittura di una sorta di flusso interno, che potrebbe implicare il buffering, l'aggiornamento dei flag di errore ... Tutto ciò richiede certamente &mut self, come ad esempioimpl Write for File.

Affidarsi alla mutevolezza interiore per risolvere questo problema mi suona come, in C ++, const_casting o abusare dei mutablemembri solo perché altrove nell'applicazione non eravamo coerenti sulla constness (errore comune per gli studenti di C ++).

Quindi, tornando al mio codice di esempio qui sotto, dovrei:

  • utilizzare &mut self(il compilatore non si lamenta, anche se non è obbligatorio) dal change_e()al change_i()per restare coerente con il fatto che alteri i valori degli interi memorizzati?
  • continuare a usare &self, perché la mutabilità interna lo consente, anche se in realtà altero i valori degli interi memorizzati?

Questa decisione non è solo locale per la struttura stessa, ma avrà una grande influenza su ciò che potrebbe essere espresso nell'applicazione utilizzando questa struttura. La seconda soluzione sicuramente aiuterà molto, perché sono coinvolti solo riferimenti condivisi, ma è coerente con quanto ci si aspetta da Rust.

Non riesco a trovare una risposta a questa domanda nelle linee guida dell'API Rust . Esiste altra documentazione Rust simile a 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());
}

Risposte

7 trentcl Aug 19 2020 at 15:59

Affidarsi alla mutevolezza interiore per risolvere questo problema mi suona come, in C ++, const_casting o abusare dei mutablemembri solo perché altrove nell'applicazione non eravamo coerenti sulla constness (errore comune per gli studenti di C ++).

Questo è un pensiero completamente comprensibile nel contesto di C ++. Il motivo per cui non è accurato è perché C ++ e Rust hanno concetti diversi di mutabilità.

In un certo senso, la mutparola chiave di Rust ha in realtà due significati. In un pattern significa "mutabile" e in un tipo di riferimento significa "esclusivo". La differenza tra &selfe &mut selfnon è realmente se selfpuò essere mutato o meno, ma se può essere alias .

Nel Messengeresempio, beh, prima cerchiamo di non prendere troppo sul serio; ha lo scopo di illustrare le caratteristiche del linguaggio, non necessariamente il design del sistema. Ma possiamo immaginare il motivo &selfper cui potrebbe essere utilizzato: Messengerè pensato per essere implementato da strutture condivise , quindi parti di codice diverse possono contenere riferimenti allo stesso oggetto e utilizzarlo per gli sendavvisi senza coordinarsi tra loro. Se senddovesse prendere &mut self, sarebbe inutile per questo scopo perché può esistere un solo &mut selfriferimento alla volta. Sarebbe impossibile inviare messaggi a un condiviso Messenger(senza aggiungere uno strato esterno di mutabilità interna tramite Mutexo qualcosa del genere).

D'altra parte, ogni riferimento e puntatore C ++ può essere alias.¹ Quindi, in termini di Rust, tutta la mutabilità in C ++ è mutabilità "interna"! Rust non ha equivalenti a mutablein C ++ perché Rust non ha constmembri (lo slogan qui è "la mutabilità è una proprietà del legame, non del tipo"). Ruggine ha avere un equivalente const_cast, ma solo per i puntatori prime, perché è infondata per trasformare un comune &di riferimento in un esclusivo &mutriferimento. Al contrario, C ++ non ha nulla come Cello RefCellperché ogni valore è implicitamente dietro a UnsafeCellgià.

Quindi, tornando al mio codice di esempio qui sotto, dovrei [...]

Dipende davvero dalla semantica prevista di Thing. È la natura dell'essere Thingcondiviso, come un endpoint del canale o un file? Ha senso change_eessere chiamato su un riferimento condiviso (con alias)? Se è così, usa la mutabilità interiore per esporre un metodo &self. È Thingprincipalmente un contenitore di dati? A volte ha senso che sia condiviso e talvolta esclusivo? Quindi Thingprobabilmente non dovresti usare la mutabilità interiore e lasciare che l'utente della libreria decida come affrontare la mutazione condivisa, se necessario.

Guarda anche

  • Qual è la differenza tra mettere "mut" prima del nome di una variabile e dopo ":"?
  • Perché la mutabilità di una variabile non si riflette nella sua firma del tipo in Rust?
  • Hai bisogno di una spiegazione olistica sulla cella di Rust e sui tipi conteggiati di riferimento

¹ In realtà, C ++ ha una caratteristica che rende puntatori lavoro simile a riferimenti a Rust. Tipo. restrictè un'estensione non standard in C ++ ma fa parte di C99. I &riferimenti shared ( ) di Rust sono come const *restrictpuntatori, e i &mutriferimenti exclusive ( ) sono come non const *restrictpuntatori. Vedi cosa significa la parola chiave limit in C ++

Quando è stata l'ultima volta che hai usato deliberatamente un puntatore restrict(o __restrict, ecc.) In C ++? Non preoccuparti di pensarci; la risposta è "mai". restrictconsente ottimizzazioni più aggressive rispetto ai normali puntatori, ma è molto difficile usarlo correttamente perché devi stare estremamente attento all'aliasing e il compilatore non offre assistenza. È fondamentalmente un enorme footgun e quasi nessuno lo usa. Per fare in modo che valga la pena di usare in modo restrictpervasivo il modo in cui usi constin C ++, dovresti essere in grado di annotare su funzioni quali puntatori sono autorizzati ad alias altri e in quali momenti, fare alcune regole su quando i puntatori sono validi da seguire, e avere un passaggio del compilatore che controlla se le regole vengono seguite in ogni funzione. Come una specie di ... controllore.