Abuso de mutabilidade interior no design de API?

Aug 19 2020

Minha experiência em C ++ me deixa desconfortável com a mutabilidade interior . O código abaixo é minha investigação sobre este tópico.

Concordo que, do ponto de vista do verificador de empréstimo, é impossível lidar com muitas referências em cada estrutura única cujo estado interno poderia ser alterado mais cedo ou mais tarde; é claramente aí que a mutabilidade interior pode ajudar.

Além disso, no capítulo 15.5 "RefCell eo Padrão Interior mutabilidade" do The Rust Linguagem de programação , o exemplo sobre o Messengertraço e sua implementação no MockMessengerstruct me faz pensar que é um desenho comum API para sistematicamente preferem &selfmais &mut self, mesmo se o seu bastante óbvio que algum tipo de mutabilidade será obrigatória mais cedo ou mais tarde. Como poderia uma implementação de Messengernão alterar seu estado interno ao enviar uma mensagem? A exceção é apenas imprimir a mensagem, o que é consistente com &self, mas o caso geral provavelmente consistiria em escrever em algum tipo de fluxo interno, o que poderia implicar em buffer, atualização de sinalizadores de erro ... Tudo isso certamente requer &mut self, como por exemploimpl Write for File.

Depender da mutabilidade interior para resolver esse problema me parece, em C ++, const_castingerir ou abusar de mutablemembros apenas porque em outras partes do aplicativo não éramos consistentes sobre o constness (erro comum para alunos de C ++).

Portanto, de volta ao meu código de exemplo abaixo, devo:

  • usar &mut self(o compilador não reclama, mesmo que não seja obrigatório) de change_e()para change_i()para manter a consistência com o fato de eu alterar os valores dos inteiros armazenados?
  • continuar usando &self, porque a mutabilidade interior permite, mesmo se eu realmente alterar os valores dos inteiros armazenados?

Essa decisão não é apenas local para a própria estrutura, mas terá uma grande influência sobre o que pode ser expresso no aplicativo usando essa estrutura. A segunda solução certamente ajudará muito, pois apenas referências compartilhadas estão envolvidas, mas é consistente com o que se espera do Rust.

Não consigo encontrar uma resposta para esta pergunta nas Diretrizes da API Rust . Existe alguma outra documentação do Rust semelhante ao 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());
}

Respostas

7 trentcl Aug 19 2020 at 15:59

Depender da mutabilidade interior para resolver esse problema me parece, em C ++, const_castingerir ou abusar de mutablemembros apenas porque em outras partes do aplicativo não éramos consistentes sobre o constness (erro comum para alunos de C ++).

Este é um pensamento completamente compreensível no contexto de C ++. O motivo pelo qual não é preciso é porque C ++ e Rust têm conceitos diferentes de mutabilidade.

De certa forma, a mutpalavra-chave de Rust tem dois significados. Em um padrão, significa "mutável" e em um tipo de referência significa "exclusivo". A diferença entre &selfe &mut selfnão é realmente se selfpode ser mutado ou não, mas se pode ter um alias .

No Messengerexemplo, bem, primeiro não vamos levar isso muito a sério; destina-se a ilustrar os recursos da linguagem, não necessariamente o design do sistema. Mas podemos imaginar por que &selfpode ser usado: Messengerdestina-se a ser implementado por estruturas que são compartilhadas , de forma que diferentes partes do código possam conter referências ao mesmo objeto e usá-lo para sendalertas sem coordenação entre si. Se sendtomasse &mut self, seria inútil para esse propósito, porque só pode haver uma &mut selfreferência de cada vez. Seria impossível enviar mensagens para um compartilhado Messenger(sem adicionar uma camada externa de mutabilidade interior via Mutexou algo assim).

Por outro lado, cada referência e ponteiro de C ++ podem ter um alias.¹ Portanto, em termos de Rust, toda mutabilidade em C ++ é mutabilidade "interior"! Rust não tem equivalente mutableem C ++ porque Rust não tem constmembros (a frase de efeito aqui é "mutabilidade é uma propriedade da ligação, não do tipo"). Rust faz tem um equivalente a const_cast, mas apenas para ponteiros crus, porque é insalubre para transformar um compartilhada &referência a um exclusivo &mutde referência. Por outro lado, C ++ não tem nada como Cellou RefCellporque cada valor está implicitamente atrás de um UnsafeCelljá.

Então, de volta ao meu código de exemplo abaixo, devo [...]

Realmente depende da semântica pretendida de Thing. É a natureza de Thingser compartilhado, como um endpoint de canal ou um arquivo? Faz sentido change_eser chamado em uma referência compartilhada (alias)? Nesse caso, use a mutabilidade interior para expor um método &self. É Thingprincipalmente um contêiner para dados? Às vezes faz sentido que seja compartilhado e às vezes exclusivo? Então Thingprovavelmente não deve usar mutabilidade interior e deixar o usuário da biblioteca decidir como lidar com a mutação compartilhada, caso seja necessário.

Veja também

  • Qual é a diferença entre colocar "mut" antes do nome de uma variável e depois de ":"?
  • Por que a mutabilidade de uma variável não é refletida em sua assinatura de tipo em Rust?
  • Precisa de uma explicação holística sobre a célula de Rust e tipos de referência contados

¹ Na verdade, C ++ faz tem uma característica que torna ponteiros trabalho semelhantes às referências em Rust. Mais ou menos. restricté uma extensão não padrão em C ++, mas faz parte do C99. As &referências shared ( ) de Rust são como const *restrictponteiros e &mutreferências exclusivas ( ) são como não const *restrictponteiros. Consulte O que significa a palavra-chave restringir em C ++?

Quando foi a última vez que você usou deliberadamente um ponteiro restrict(ou __restrictetc.) em C ++? Não se preocupe em pensar nisso; a resposta é "nunca". restrictpermite otimizações mais agressivas do que os ponteiros regulares, mas é muito difícil usá-lo corretamente porque você deve ser extremamente cuidadoso com o aliasing e o compilador não oferece assistência. É basicamente uma metralhadora enorme e quase ninguém a usa. A fim de fazer valer a pena usar restrictamplamente a maneira como você usa constem C ++, você precisa ser capaz de anotar nas funções quais ponteiros têm permissão para apelidar de outros em que momentos, estabelecer algumas regras sobre quando ponteiros são válidos para seguir, e ter uma passagem de compilador que verifica se as regras estão sendo seguidas em cada função. Como algum tipo de ... verificador.