Abus de mutabilité intérieure dans la conception d'API?
Mon expérience en C ++ me met mal à l'aise avec la mutabilité intérieure . Le code ci-dessous est mon enquête sur ce sujet.
Je suis d'accord que, du point de vue du vérificateur d'emprunt, il est impossible de traiter de nombreuses références sur chaque structure dont l'état interne pourrait être modifié tôt ou tard; c'est clairement là que la mutabilité intérieure peut aider.
De plus, au chapitre 15.5 « RefCell et le modèle intérieur mutabilité » de Rust Langage de programmation , l'exemple sur le Messenger
trait et sa mise en œuvre sur le MockMessenger
struct me fait penser qu'il est une conception API commune à préférer systématiquement &self
sur &mut self
même si son tout à fait évident que une certaine mutabilité sera obligatoire tôt ou tard. Comment une implémentation de Messenger
ne pas modifier son état interne lors de l'envoi d'un message? L'exception est simplement l'impression du message, ce qui est cohérent avec &self
, mais le cas général consisterait probablement à écrire dans une sorte de flux interne, ce qui pourrait impliquer une mise en mémoire tampon, la mise à jour des indicateurs d'erreur ... Tout cela nécessite certainement &mut self
, comme par exempleimpl Write for File.
S'appuyer sur la mutabilité intérieure pour résoudre ce problème me semble comme, en C ++, const_cast
ingérer ou abuser des mutable
membres simplement parce qu'ailleurs dans l'application, nous n'étions pas cohérents à propos de const
ness (erreur courante pour les apprenants de C ++).
Donc, revenons à mon exemple de code ci-dessous, devrais-je:
- utiliser
&mut self
(le compilateur ne se plaint pas, même si ce n'est pas obligatoire) dechange_e()
àchange_i()
pour rester cohérent avec le fait que je modifie les valeurs des entiers stockés? - continuer à utiliser
&self
, parce que la mutabilité intérieure le permet, même si je modifie réellement les valeurs des entiers stockés?
Cette décision n'est pas seulement locale à la structure elle-même, mais aura une grande influence sur ce qui pourrait être exprimé dans l'application utilisant cette structure. La deuxième solution aidera certainement beaucoup, car seules les références partagées sont impliquées, mais est-elle cohérente avec ce qui est attendu dans Rust.
Je ne trouve pas de réponse à cette question dans les directives de l'API Rust . Existe-t-il une autre documentation Rust similaire à 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());
}
Réponses
S'appuyer sur la mutabilité intérieure pour résoudre ce problème me semble comme, en C ++,
const_cast
ingérer ou abuser desmutable
membres simplement parce qu'ailleurs dans l'application, nous n'étions pas cohérents à propos deconst
ness (erreur courante pour les apprenants de C ++).
C'est une pensée tout à fait compréhensible dans le contexte du C ++. La raison pour laquelle ce n'est pas précis est que C ++ et Rust ont des concepts différents de mutabilité.
D'une certaine manière, le mut
mot - clé de Rust a en fait deux significations. Dans un modèle, cela signifie «mutable» et dans un type de référence, cela signifie «exclusif». La différence entre &self
et &mut self
n'est pas vraiment de savoir s'il self
peut être muté ou non, mais s'il peut être aliasé .
Dans l' Messenger
exemple, eh bien, d'abord ne le prenons pas trop au sérieux; il est destiné à illustrer les fonctionnalités du langage, pas nécessairement la conception du système. Mais nous pouvons imaginer pourquoi cela &self
pourrait être utilisé: Messenger
est destiné à être implémenté par des structures partagées , de sorte que différents morceaux de code peuvent contenir des références au même objet et l'utiliser pour des send
alertes sans se coordonner. Si send
c'était à prendre &mut self
, cela ne servirait à rien à cette fin car il ne peut y avoir qu'une seule &mut self
référence à la fois. Il serait impossible d'envoyer des messages à un partage Messenger
(sans ajouter une couche externe de mutabilité intérieure via Mutex
ou quelque chose).
D'un autre côté, chaque référence et pointeur C ++ peut être aliasé. Donc, en termes Rust, toute mutabilité en C ++ est une mutabilité "intérieure"! Rust n'a pas d'équivalent mutable
en C ++ car Rust n'a pas de const
membres (le slogan ici est "la mutabilité est une propriété de la liaison, pas du type"). Rust n'ont un équivalent , mais seulement pour les pointeurs premières, parce qu'il est pas raisonnable de transformer une commune référence en une exclusivité référence. Inversement, C ++ n'a rien de tel ou parce que chaque valeur est implicitement derrière un .const_cast
&
&mut
Cell
RefCell
UnsafeCell
Donc, revenons à mon exemple de code ci-dessous, devrais-je [...]
Cela dépend vraiment de la sémantique prévue de Thing
. Est-ce la nature d' Thing
être partagé, comme un point de terminaison de canal ou un fichier? Est-il judicieux change_e
d'être appelé sur une référence partagée (aliasée)? Si tel est le cas, utilisez la mutabilité intérieure pour exposer une méthode &self
. Est-ce Thing
avant tout un conteneur de données? Est-il parfois judicieux qu'il soit partagé et parfois exclusif? Alors Thing
ne devrait probablement pas utiliser la mutabilité intérieure et laisser l'utilisateur de la bibliothèque décider comment gérer la mutation partagée, si cela est nécessaire.
Voir également
- Quelle est la différence entre placer "mut" avant un nom de variable et après le ":"?
- Pourquoi la mutabilité d'une variable n'est-elle pas reflétée dans sa signature de type dans Rust?
- Besoin d'une explication holistique sur la cellule de Rust et les types comptés de référence
¹ En fait, C ++ possède une fonctionnalité qui permet aux pointeurs de fonctionner de manière similaire aux références dans Rust. En quelque sorte. restrict
est une extension non standard en C ++ mais elle fait partie de C99. Les &
références shared ( ) de Rust sont comme des const *restrict
pointeurs, et les &mut
références exclusives ( ) sont comme des non- const
*restrict
pointeurs. Consultez Que signifie le mot clé restrict en C ++?
À quand remonte la dernière fois que vous avez délibérément utilisé un pointeur restrict
(ou __restrict
, etc.) en C ++? Ne vous embêtez pas à y penser; la réponse est «jamais». restrict
permet des optimisations plus agressives que les pointeurs classiques, mais il est très difficile de l'utiliser correctement car vous devez être extrêmement prudent sur l'aliasing, et le compilateur n'offre aucune assistance. C'est fondamentalement une arme à pied massive et presque personne ne l'utilise. Afin de rendre utile l'utilisation restrict
généralisée de la façon dont vous utilisez const
en C ++, vous devez être en mesure d'annoter sur les fonctions quels pointeurs sont autorisés à en aliaser d'autres, à quel moment, établissez des règles sur le moment où les pointeurs sont valides à suivre, et faites passer un compilateur qui vérifie si les règles sont suivies dans chaque fonction. Comme une sorte de ... vérificateur.