Rust: Lidando com erro ao misturar borrow imutável e mutável

Posted by Vanius on 1 November 2020

Ao trabalhar com Rust cedo ou tarde você irá se deparar com o "error[E0502]: cannot borrow ... as immutable because it is also borrowed as mutable". As vezes acontece quando vamos tentar fazer algo simples, coisa que fazemos de maneira corriqueira em outra linguagem. Vamos conferir algumas soluções para contornar esse problema.

Entendendo o problema

Vamos supor uma aplicação que contenha dois objetos:


    pub struct Token {
        user_name: String,
        hash: Option<String>,
    }

    impl Token {
        fn new(user_name: String) -> Token {
            Token {
                user_name,
                hash: None,
            }
        }

        fn get_user_name(&self) -> &str {
            &self.user_name
        }

        fn get_hash(&mut self) -> &str {
            self.hash.get_or_insert_with(|| "ff0b14ba".to_string())
        }
    }

    pub struct Session<'a> {
        user_name: &'a str,
        hash: &'a str,
    }

    impl<'a> Session<'a> {
        fn login(user_name: &'a str, hash: &'a str) -> Session<'a> {
            Session { user_name, hash }
        }
    }

O objeto Token possui dois atributos, user_name e hash. A intenção nesse exemplo é supormos que para obter o hash é uma operação que requer algum processamento, então hash é obtido pela função get_hash, que internamente possui um artifício de lazy load, por isso é necessário ser mutável.

Para criar o objeto Session requer user_name e hash, que devemos obter através do objeto Token. Vamos tentar fazer isso:


    // Código omitido

    fn main() {
        let mut token = Token::new("user".to_string());

        let user_name = token.get_user_name();
        let hash = token.get_hash();

        let session = Session::login(user_name, hash);

        println!("user_name: {} hash: {}", session.user_name, session.hash);
    }

Mas ao compilar obtemos o seguinte erro:


    error[E0502]: cannot borrow `token` as mutable because it is also borrowed as immutable
    --> src/main.rs:42:16
    |
 41 |     let user_name = token.get_user_name();
    |                     ----- immutable borrow occurs here
 42 |     let hash = token.get_hash();
    |                ^^^^^^^^^^^^^^^^ mutable borrow occurs here
 43 | 
 44 |     let session = Session::login(user_name, hash);
    |                                  --------- immutable borrow later used here

Em Rust temos que ter atenção quando acessamos referências (borrowed &T) de um objeto mutável, pois acabamos por "bloquear" o objeto. No erro acima mesmo acessando user_name através de uma outra variável o compilador considera o objeto token como sendo referenciado. Isso por que o nosso método get_hash é mutável, mas também estamos acessando user_name que é imutável.

"Ok, não posso misturar referências mutáveis e imutáveis, vou alterar para tudo ser mutável". Se fazer isso o compilar exibe outro erro:


    Error[E0499]: cannot borrow `token` as mutable more than once at a time

O erro "Error[E0499]: cannot borrow ... as mutable more than once at a time" tem a mesma origem, devido a referência mutável user_name.

Não conseguimos evoluir, então teremos quer tentar outras coisas.

Clonagem

A solução mais fácil é fazer operações de clone e trabalhar com variáveis owned ou invés de borrowed.


    pub struct Token {
        user_name: String,
        hash: Option<String>,
    }

    impl Token {
        fn new(user_name: String) -> Token {
            Token {
                user_name,
                hash: None,
            }
        }

        fn get_user_name(&mut self) -> String {
            self.user_name.clone()
        }

        fn get_hash(&mut self) -> String {
            self.hash
                .get_or_insert_with(|| "ff0b14ba".to_string())
                .clone()
        }
    }

    pub struct Session {
        user_name: String,
        hash: String,
    }

    impl Session {
        fn login(user_name: String, hash: String) -> Session {
            Session { user_name, hash }
        }
    }

    pub fn main() {
        let mut token = Token::new("user".to_string());

        let user_name = token.get_user_name();
        let hash = token.get_hash();

        let session = Session::login(user_name, hash);

        println!("user_name: {} hash: {}", session.user_name, session.hash);
    }

Devemos ficar atentos que o clone pode causar um overhead, dependendo do tamanho do objeto ou complexidade do código.

Retorno através de Rc

Quanto queremos ter o equivalente um contador de referência, onde temos vários "ponteiros" apontando para a mesma área de dados, usamos o Rc. Ao fazermos Rc::clone ocorre a clonagem somente do ponteiro - o dado referênciado não é clonado.


    use std::rc::Rc;

    pub struct Token {
        user_name: Rc<String>,
        hash: Option<Rc<String>>,
    }

    impl Token {
        fn new(user_name: String) -> Token {
            Token {
                user_name: Rc::new(user_name),
                hash: None,
            }
        }
        
        fn get_user_name(&mut self) -> Rc<String> {
            Rc::clone(&self.user_name)
        }

        fn get_hash(&mut self) -> Rc<String> {
            let hash = self.hash.get_or_insert_with(|| Rc::new("ff0b14ba".to_string()));
            Rc::clone(hash)
        }

    }

    pub struct Session<'a> {
        user_name: &'a str,
        hash: &'a str,
    }

    impl <'a> Session<'a> {
        fn login(user_name: &'a str, key: &'a str) -> Session<'a> {
            Session {
                user_name,
                hash: key,
            }
        }
    }

    pub fn main() {

        let mut token = Token::new("user".to_string());    

        let user_name = token.get_user_name();
        let hash = token.get_hash();

        let session = Session::login(&user_name, &hash);

        println!("user_name: {} hash: {}", session.user_name, session.hash);

    }

Perceba que no método Session::login os parâmetros continuam com assinatura &'a T, porém para passar os valores para o parâmetro funciona a coerção a partir de Rc<T>.

Função única que acessa todos campos necessários

Outra alternativa é criar uma função que acessa os campos necessários, porém retornando algum objeto que contém as referências necessárias.


    pub struct Token {
        user_name: String,
        hash: Option<String>,
    }

    impl Token {
        fn new(user_name: String) -> Token {
            Token {
                user_name,
                hash: None,
            }
        }

        fn get_session(&mut self) -> Session {
            let hash = self.hash.get_or_insert_with(|| "ff0b14ba".to_string());
            Session {
                user_name: &self.user_name,
                hash,
            }
        }
    }

    pub struct Session<'a> {
        user_name: &'a str,
        hash: &'a str,
    }

    pub fn main() {
        let mut token = Token::new("user".to_string());

        let session = token.get_session();

        println!(
            "session user_name {} hash {}",
            session.user_name, session.hash
        );
    }

Conclusão

Vimos algumas possibilidade para resolver o problema de acessar variável borrowed mutável. Para objetos pequenos, fazer clone pode ser uma boa alternativa. A smart pointer Rc pode funcionar como um "botão de emergência", quando não vemos mais saída podemos recorrer a ele. Ele também é útil quando enfrentamos dificuldade com timelifes <'a>.