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>
.