¿Abuso de mutabilidad interior en el diseño de API?

Aug 19 2020

Mi experiencia en C ++ me incomoda acerca de la mutabilidad interior . El siguiente código es mi investigación sobre este tema.

Estoy de acuerdo en que, desde el punto de vista del verificador de préstamos, es imposible tratar con muchas referencias en cada estructura, cuyo estado interno podría modificarse tarde o temprano; ahí es claramente donde la mutabilidad interior puede ayudar.

Por otra parte, en el capítulo 15.5 "RefCell y el patrón Mutabilidad interior" de la roya Lenguaje de programación , el ejemplo de la Messengercaracterística y su aplicación en la MockMessengerestructura me hace pensar que se trata de un diseño de API común para preferir sistemáticamente &selfmás &mut selfaún si es bastante obvio que algún tipo de mutabilidad será obligatorio tarde o temprano. ¿Cómo podría una implementación Messengerno alterar su estado interno al enviar un mensaje? La excepción es simplemente imprimir el mensaje, lo cual es consistente con &self, pero el caso general probablemente consistiría en escribir en algún tipo de flujo interno, lo que podría implicar almacenamiento en búfer, actualización de indicadores de error ... Todo esto ciertamente requiere &mut self, como por ejemploimpl Write for File.

Depender de la mutabilidad interior para resolver este problema me suena como, en C ++, const_castabusar o abusar de los mutablemiembros solo porque en otras partes de la aplicación no fuimos consistentes sobre la constness (error común para los estudiantes de C ++).

Entonces, volviendo a mi código de ejemplo a continuación, debería:

  • uso &mut self(el compilador no se queja, incluso si no es obligatorio) de change_e()a change_i()para mantener la coherencia con el hecho de que modifico los valores de los enteros almacenados?
  • seguir usando &self, porque la mutabilidad interior lo permite, incluso si realmente modifico los valores de los enteros almacenados?

Esta decisión no solo es local a la estructura en sí, sino que tendrá una gran influencia en lo que podría expresarse en la aplicación utilizando esta estructura. La segunda solución ciertamente ayudará mucho, porque solo se incluyen referencias compartidas, pero es consistente con lo que se espera en Rust.

No puedo encontrar una respuesta a esta pregunta en las Pautas de la API de Rust . ¿Existe alguna otra documentación de Rust similar 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());
}

Respuestas

7 trentcl Aug 19 2020 at 15:59

Depender de la mutabilidad interior para resolver este problema me suena como, en C ++, const_castabusar o abusar de los mutablemiembros solo porque en otras partes de la aplicación no fuimos consistentes sobre la constness (error común para los estudiantes de C ++).

Este es un pensamiento completamente comprensible en el contexto de C ++. La razón por la que no es precisa es porque C ++ y Rust tienen diferentes conceptos de mutabilidad.

En cierto modo, la mutpalabra clave de Rust tiene dos significados. En un patrón significa "mutable" y en un tipo de referencia significa "exclusivo". La diferencia entre &selfy &mut selfno es realmente si selfse puede mutar o no, sino si se puede usar un alias .

En el Messengerejemplo, bueno, primero no nos lo tomemos demasiado en serio; está destinado a ilustrar las características del lenguaje, no necesariamente al diseño del sistema. Pero podemos imaginar por qué &selfpodría usarse: Messengerestá destinado a ser implementado por estructuras que son compartidas , por lo que diferentes piezas de código pueden contener referencias al mismo objeto y usarlo para sendalertas sin coordinarse entre sí. Si sendse tomara &mut self, sería inútil para este propósito porque solo puede existir una &mut selfreferencia a la vez. Sería imposible enviar mensajes a un compartido Messenger(sin agregar una capa externa de mutabilidad interior a través de Mutexo algo así).

Por otro lado, cada referencia y puntero de C ++ puede tener un alias.¹ Entonces, en términos de Rust, ¡ toda mutabilidad en C ++ es mutabilidad "interior"! Rust no tiene equivalente mutableen C ++ porque Rust no tiene constmiembros (el eslogan aquí es "la mutabilidad es una propiedad de la unión, no del tipo"). Rust hace tener un equivalente a const_cast, pero sólo para los punteros primas, porque es poco sólido para convertir una compartida &referencia en un exclusivo &mutreferencia. Por el contrario, C ++ no tiene nada como Cello RefCellporque cada valor está implícitamente detrás de un UnsafeCellya.

Entonces, volviendo a mi código de ejemplo a continuación, ¿debería [...]

Realmente depende de la semántica prevista de Thing. ¿Es la naturaleza de Thingser compartido, como un punto final de canal o un archivo? ¿Tiene sentido change_eque se le llame en una referencia compartida (con alias)? Si es así, utilice la mutabilidad interior para exponer un método &self. ¿Es Thingprincipalmente un contenedor de datos? ¿A veces tiene sentido que sea compartido y, a veces, exclusivo? Entonces Thingprobablemente no debería usar la mutabilidad interior y dejar que el usuario de la biblioteca decida cómo lidiar con la mutación compartida, si fuera necesario.

Ver también

  • ¿Cuál es la diferencia entre colocar "mut" antes de un nombre de variable y después de ":"?
  • ¿Por qué la mutabilidad de una variable no se refleja en su tipo de firma en Rust?
  • Necesita una explicación holística sobre los tipos contados de referencia y celda de Rust

¹ En realidad, C ++ hace tener una característica que hace que los punteros funcionan de manera similar a las referencias en Rust. Mas o menos. restrictes una extensión no estándar en C ++ pero es parte de C99. Las &referencias compartidas ( ) de Rust son como const *restrictpunteros, y las &mutreferencias exclusivas ( ) son como no const *restrictpunteros. Consulte ¿Qué significa la palabra clave restringir en C ++?

¿Cuándo fue la última vez que usó deliberadamente un puntero restrict(o __restrict, etc.) en C ++? No se moleste en pensar en ello; la respuesta es "nunca". restrictpermite optimizaciones más agresivas que los punteros normales, pero es muy difícil usarlo correctamente porque debe tener mucho cuidado con el aliasing y el compilador no ofrece ayuda. Básicamente es una pistola enorme y casi nadie la usa. Para que valga la pena usar de manera restrictgeneralizada la forma en que usa consten C ++, necesitaría poder anotar en las funciones qué punteros pueden usar alias de otros en qué momentos, establecer algunas reglas sobre cuándo es válido seguir los punteros, y tener un pase de compilador que compruebe si se siguen las reglas en cada función. Como una especie de ... corrector.