Abuso di mutabilità interna nella progettazione API?
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 Messenger
tratto e la sua implementazione sulla MockMessenger
struttura mi fa pensare che sia un progetto API comune da preferire sistematicamente &self
rispetto a &mut self
anche se è abbastanza ovvio che una sorta di mutabilità sarà obbligatoria presto o tardi. Come potrebbe un'implementazione di Messenger
non 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_cast
ing o abusare dei mutable
membri solo perché altrove nell'applicazione non eravamo coerenti sulla const
ness (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) dalchange_e()
alchange_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
Affidarsi alla mutevolezza interiore per risolvere questo problema mi suona come, in C ++,
const_cast
ing o abusare deimutable
membri solo perché altrove nell'applicazione non eravamo coerenti sullaconst
ness (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 mut
parola chiave di Rust ha in realtà due significati. In un pattern significa "mutabile" e in un tipo di riferimento significa "esclusivo". La differenza tra &self
e &mut self
non è realmente se self
può essere mutato o meno, ma se può essere alias .
Nel Messenger
esempio, 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 &self
per 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 send
avvisi senza coordinarsi tra loro. Se send
dovesse prendere &mut self
, sarebbe inutile per questo scopo perché può esistere un solo &mut self
riferimento alla volta. Sarebbe impossibile inviare messaggi a un condiviso Messenger
(senza aggiungere uno strato esterno di mutabilità interna tramite Mutex
o 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 mutable
in C ++ perché Rust non ha const
membri (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 &mut
riferimento. Al contrario, C ++ non ha nulla come Cell
o RefCell
perché ogni valore è implicitamente dietro a UnsafeCell
già.
Quindi, tornando al mio codice di esempio qui sotto, dovrei [...]
Dipende davvero dalla semantica prevista di Thing
. È la natura dell'essere Thing
condiviso, come un endpoint del canale o un file? Ha senso change_e
essere chiamato su un riferimento condiviso (con alias)? Se è così, usa la mutabilità interiore per esporre un metodo &self
. È Thing
principalmente un contenitore di dati? A volte ha senso che sia condiviso e talvolta esclusivo? Quindi Thing
probabilmente 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 *restrict
puntatori, e i &mut
riferimenti exclusive ( ) sono come non const
*restrict
puntatori. 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". restrict
consente 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 restrict
pervasivo il modo in cui usi const
in 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.