Abus de mutabilité intérieure dans la conception d'API?

Aug 19 2020

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 Messengertrait et sa mise en œuvre sur le MockMessengerstruct me fait penser qu'il est une conception API commune à préférer systématiquement &selfsur &mut selfmême si son tout à fait évident que une certaine mutabilité sera obligatoire tôt ou tard. Comment une implémentation de Messengerne 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_castingérer ou abuser des mutablemembres simplement parce qu'ailleurs dans l'application, nous n'étions pas cohérents à propos de constness (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) de change_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

7 trentcl Aug 19 2020 at 15:59

S'appuyer sur la mutabilité intérieure pour résoudre ce problème me semble comme, en C ++, const_castingérer ou abuser des mutablemembres simplement parce qu'ailleurs dans l'application, nous n'étions pas cohérents à propos de constness (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 mutmot - 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 &selfet &mut selfn'est pas vraiment de savoir s'il selfpeut être muté ou non, mais s'il peut être aliasé .

Dans l' Messengerexemple, 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 &selfpourrait être utilisé: Messengerest 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 sendalertes sans se coordonner. Si sendc'était à prendre &mut self, cela ne servirait à rien à cette fin car il ne peut y avoir qu'une seule &mut selfréférence à la fois. Il serait impossible d'envoyer des messages à un partage Messenger(sans ajouter une couche externe de mutabilité intérieure via Mutexou 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 mutableen C ++ car Rust n'a pas de constmembres (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&&mutCellRefCellUnsafeCell

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_ed'ê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 Thingavant tout un conteneur de données? Est-il parfois judicieux qu'il soit partagé et parfois exclusif? Alors Thingne 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. restrictest une extension non standard en C ++ mais elle fait partie de C99. Les &références shared ( ) de Rust sont comme des const *restrictpointeurs, et les &mutréférences exclusives ( ) sont comme des non- const *restrictpointeurs. 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». restrictpermet 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 restrictgénéralisée de la façon dont vous utilisez consten 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.