Introdução
Bem-vindo(a) ao RefCell
Neste laboratório, exploraremos o conceito de mutabilidade interior em Rust e como ele é implementado usando o tipo RefCell<T>.
This tutorial is from open-source community. Access the source code
Bem-vindo(a) ao RefCell
Neste laboratório, exploraremos o conceito de mutabilidade interior em Rust e como ele é implementado usando o tipo RefCell<T>.
<T> e o Padrão de Mutabilidade InteriorA mutabilidade interior (interior mutability) é um padrão de design em Rust que permite mutar dados mesmo quando existem referências imutáveis a esses dados; normalmente, essa ação é proibida pelas regras de empréstimo (borrowing rules). Para mutar dados, o padrão usa código unsafe dentro de uma estrutura de dados para contornar as regras usuais do Rust que governam a mutação e o empréstimo. Código unsafe indica ao compilador que estamos verificando as regras manualmente, em vez de confiar no compilador para verificá-las por nós; discutiremos o código unsafe com mais detalhes no Capítulo 19.
Podemos usar tipos que usam o padrão de mutabilidade interior somente quando podemos garantir que as regras de empréstimo serão seguidas em tempo de execução, mesmo que o compilador não possa garantir isso. O código unsafe envolvido é então encapsulado em uma API segura, e o tipo externo ainda é imutável.
Vamos explorar esse conceito analisando o tipo RefCell<T> que segue o padrão de mutabilidade interior.
<T>Ao contrário de Rc<T>, o tipo RefCell<T> representa propriedade única sobre os dados que ele contém. Então, o que torna RefCell<T> diferente de um tipo como Box<T>? Recorde as regras de empréstimo que você aprendeu no Capítulo 4:
Com referências e Box<T>, os invariantes das regras de empréstimo são aplicados em tempo de compilação. Com RefCell<T>, esses invariantes são aplicados em tempo de execução. Com referências, se você quebrar essas regras, você receberá um erro do compilador. Com RefCell<T>, se você quebrar essas regras, seu programa entrará em pânico e sairá.
As vantagens de verificar as regras de empréstimo em tempo de compilação são que os erros serão detectados mais cedo no processo de desenvolvimento, e não há impacto no desempenho em tempo de execução porque toda a análise é concluída antecipadamente. Por essas razões, verificar as regras de empréstimo em tempo de compilação é a melhor escolha na maioria dos casos, e é por isso que esta é a configuração padrão do Rust.
A vantagem de verificar as regras de empréstimo em tempo de execução é que certos cenários seguros para a memória são então permitidos, onde teriam sido proibidos pelas verificações em tempo de compilação. A análise estática, como o compilador Rust, é inerentemente conservadora. Algumas propriedades do código são impossíveis de detectar analisando o código: o exemplo mais famoso é o Problema da Parada (Halting Problem), que está além do escopo deste livro, mas é um tópico interessante para pesquisar.
Como alguma análise é impossível, se o compilador Rust não puder ter certeza de que o código está em conformidade com as regras de propriedade, ele poderá rejeitar um programa correto; dessa forma, ele é conservador. Se o Rust aceitasse um programa incorreto, os usuários não poderiam confiar nas garantias que o Rust faz. No entanto, se o Rust rejeitar um programa correto, o programador será inconveniente, mas nada catastrófico pode ocorrer. O tipo RefCell<T> é útil quando você tem certeza de que seu código segue as regras de empréstimo, mas o compilador não consegue entender e garantir isso.
Semelhante a Rc<T>, RefCell<T> é apenas para uso em cenários de thread único e fornecerá um erro em tempo de compilação se você tentar usá-lo em um contexto multithread. Falaremos sobre como obter a funcionalidade de RefCell<T> em um programa multithreaded no Capítulo 16.
Aqui está um resumo das razões para escolher Box<T>, Rc<T> ou RefCell<T>:
Rc<T> permite múltiplos proprietários dos mesmos dados; Box<T> e RefCell<T> têm proprietários únicos.Box<T> permite empréstimos imutáveis ou mutáveis verificados em tempo de compilação; Rc<T> permite apenas empréstimos imutáveis verificados em tempo de compilação; RefCell<T> permite empréstimos imutáveis ou mutáveis verificados em tempo de execução.RefCell<T> permite empréstimos mutáveis verificados em tempo de execução, você pode mutar o valor dentro do RefCell<T> mesmo quando o RefCell<T> é imutável.Mutar o valor dentro de um valor imutável é o padrão de mutabilidade interior (interior mutability). Vamos analisar uma situação em que a mutabilidade interior é útil e examinar como isso é possível.
Uma consequência das regras de empréstimo é que, quando você tem um valor imutável, você não pode emprestá-lo de forma mutável. Por exemplo, este código não compilará:
Nome do arquivo: src/main.rs
fn main() {
let x = 5;
let y = &mut x;
}
Se você tentasse compilar este código, você obteria o seguinte erro:
error[E0596]: cannot borrow `x` as mutable, as it is not declared
as mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
No entanto, existem situações em que seria útil para um valor se mutar em seus métodos, mas parecer imutável para outro código. O código fora dos métodos do valor não seria capaz de mutar o valor. Usar RefCell<T> é uma maneira de obter a capacidade de ter mutabilidade interior, mas RefCell<T> não contorna completamente as regras de empréstimo: o verificador de empréstimo (borrow checker) no compilador permite essa mutabilidade interior, e as regras de empréstimo são verificadas em tempo de execução em vez disso. Se você violar as regras, você obterá um panic! em vez de um erro do compilador.
Vamos analisar um exemplo prático onde podemos usar RefCell<T> para mutar um valor imutável e ver por que isso é útil.
Às vezes, durante os testes, um programador usará um tipo no lugar de outro tipo, a fim de observar um comportamento específico e afirmar que ele foi implementado corretamente. Esse tipo de espaço reservado é chamado de test double. Pense nisso no sentido de um dublê em cinematografia, onde uma pessoa entra e substitui um ator para fazer uma cena particularmente complicada. Os test doubles substituem outros tipos quando estamos executando testes. Objetos mock são tipos específicos de test doubles que registram o que acontece durante um teste para que você possa afirmar que as ações corretas foram tomadas.
Rust não tem objetos no mesmo sentido que outras linguagens têm objetos, e Rust não tem funcionalidade de objeto mock integrada na biblioteca padrão como algumas outras linguagens têm. No entanto, você pode definitivamente criar uma struct que servirá aos mesmos propósitos que um objeto mock.
Aqui está o cenário que testaremos: criaremos uma biblioteca que rastreia um valor em relação a um valor máximo e envia mensagens com base em quão próximo do valor máximo o valor atual está. Essa biblioteca pode ser usada para controlar a cota de um usuário para o número de chamadas de API que ele pode fazer, por exemplo.
Nossa biblioteca fornecerá apenas a funcionalidade de rastrear quão próximo do máximo um valor está e quais mensagens devem ser em quais momentos. Espera-se que os aplicativos que usam nossa biblioteca forneçam o mecanismo para enviar as mensagens: o aplicativo pode colocar uma mensagem no aplicativo, enviar um e-mail, enviar uma mensagem de texto ou fazer outra coisa. A biblioteca não precisa saber esse detalhe. Tudo o que ela precisa é algo que implemente uma trait que forneceremos chamada Messenger. A Listagem 15-20 mostra o código da biblioteca.
Nome do arquivo: src/lib.rs
pub trait Messenger {
1 fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(
messenger: &'a T,
max: usize
) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
2 pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max =
self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger
.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent: You're at 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You're at 75% of your quota!");
}
}
}
Listagem 15-20: Uma biblioteca para controlar quão próximo um valor está de um valor máximo e avisar quando o valor está em certos níveis
Uma parte importante deste código é que a trait Messenger tem um método chamado send que recebe uma referência imutável a self e o texto da mensagem [1]. Essa trait é a interface que nosso objeto mock precisa implementar para que o mock possa ser usado da mesma forma que um objeto real. A outra parte importante é que queremos testar o comportamento do método set_value no LimitTracker [2]. Podemos alterar o que passamos para o parâmetro value, mas set_value não retorna nada para que possamos fazer afirmações. Queremos ser capazes de dizer que, se criarmos um LimitTracker com algo que implementa a trait Messenger e um valor específico para max, quando passarmos números diferentes para value, o messenger é instruído a enviar as mensagens apropriadas.
Precisamos de um objeto mock que, em vez de enviar um e-mail ou mensagem de texto quando chamamos send, apenas acompanhe as mensagens que ele recebe para enviar. Podemos criar uma nova instância do objeto mock, criar um LimitTracker que usa o objeto mock, chamar o método set_value no LimitTracker e, em seguida, verificar se o objeto mock tem as mensagens que esperamos. A Listagem 15-21 mostra uma tentativa de implementar um objeto mock para fazer exatamente isso, mas o verificador de empréstimo não permitirá.
Nome do arquivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
1 struct MockMessenger {
2 sent_messages: Vec<String>,
}
impl MockMessenger {
3 fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
4 impl Messenger for MockMessenger {
fn send(&self, message: &str) {
5 self.sent_messages.push(String::from(message));
}
}
#[test]
6 fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(
&mock_messenger,
100
);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
Listagem 15-21: Uma tentativa de implementar um MockMessenger que não é permitida pelo verificador de empréstimo
Este código de teste define uma struct MockMessenger [1] que tem um campo sent_messages com um Vec de valores String [2] para acompanhar as mensagens que ele recebe para enviar. Também definimos uma função associada new [3] para tornar conveniente a criação de novos valores MockMessenger que começam com uma lista vazia de mensagens. Em seguida, implementamos a trait Messenger para MockMessenger [4] para que possamos dar um MockMessenger a um LimitTracker. Na definição do método send [5], pegamos a mensagem passada como um parâmetro e a armazenamos na lista sent_messages do MockMessenger.
No teste, estamos testando o que acontece quando o LimitTracker recebe a instrução de definir value para algo que é mais de 75 por cento do valor max [6]. Primeiro, criamos um novo MockMessenger, que começará com uma lista vazia de mensagens. Em seguida, criamos um novo LimitTracker e damos a ele uma referência ao novo MockMessenger e um valor max de 100. Chamamos o método set_value no LimitTracker com um valor de 80, que é mais de 75 por cento de 100. Em seguida, afirmamos que a lista de mensagens que o MockMessenger está acompanhando agora deve ter uma mensagem nela.
No entanto, há um problema com este teste, como mostrado aqui:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a
`&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference:
`&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a
`&` reference, so the data it refers to cannot be borrowed as mutable
Não podemos modificar o MockMessenger para acompanhar as mensagens porque o método send recebe uma referência imutável a self. Também não podemos aceitar a sugestão do texto do erro para usar &mut self em vez disso, porque então a assinatura de send não corresponderia à assinatura na definição da trait Messenger (sinta-se à vontade para tentar e ver qual mensagem de erro você recebe).
Esta é uma situação em que a mutabilidade interior pode ajudar! Armazenaremos as sent_messages dentro de um RefCell<T>, e então o método send poderá modificar sent_messages para armazenar as mensagens que vimos. A Listagem 15-22 mostra como isso se parece.
Nome do arquivo: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
1 sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
2 sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages
3 .borrow_mut()
.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
--snip--
assert_eq!(
4 mock_messenger.sent_messages.borrow().len(),
1
);
}
}
Listagem 15-22: Usando RefCell<T> para mutar um valor interno enquanto o valor externo é considerado imutável
O campo sent_messages agora é do tipo RefCell<Vec<String>> [1] em vez de Vec<String>. Na função new, criamos uma nova instância RefCell<Vec<String>> em torno do vetor vazio [2].
Para a implementação do método send, o primeiro parâmetro ainda é um empréstimo imutável de self, que corresponde à definição da trait. Chamamos borrow_mut no RefCell<Vec<String>> em self.sent_messages [3] para obter uma referência mutável ao valor dentro do RefCell<Vec<String>>, que é o vetor. Então, podemos chamar push na referência mutável ao vetor para acompanhar as mensagens enviadas durante o teste.
A última alteração que temos que fazer é na afirmação: para ver quantos itens estão no vetor interno, chamamos borrow no RefCell<Vec<String>> para obter uma referência imutável ao vetor [4].
Agora que você viu como usar RefCell<T>, vamos nos aprofundar em como ele funciona!
<T>Ao criar referências imutáveis e mutáveis, usamos a sintaxe & e &mut, respectivamente. Com RefCell<T>, usamos os métodos borrow e borrow_mut, que fazem parte da API segura que pertence a RefCell<T>. O método borrow retorna o tipo de ponteiro inteligente Ref<T>, e borrow_mut retorna o tipo de ponteiro inteligente RefMut<T>. Ambos os tipos implementam Deref, então podemos tratá-los como referências regulares.
O RefCell<T> acompanha quantos ponteiros inteligentes Ref<T> e RefMut<T> estão atualmente ativos. Toda vez que chamamos borrow, o RefCell<T> aumenta sua contagem de quantos empréstimos imutáveis estão ativos. Quando um valor Ref<T> sai do escopo, a contagem de empréstimos imutáveis diminui em 1. Assim como as regras de empréstimo em tempo de compilação, RefCell<T> nos permite ter muitos empréstimos imutáveis ou um empréstimo mutável em qualquer ponto no tempo.
Se tentarmos violar essas regras, em vez de obter um erro do compilador como faríamos com referências, a implementação de RefCell<T> entrará em pânico em tempo de execução. A Listagem 15-23 mostra uma modificação da implementação de send na Listagem 15-22. Estamos deliberadamente tentando criar dois empréstimos mutáveis ativos para o mesmo escopo para ilustrar que RefCell<T> nos impede de fazer isso em tempo de execução.
Nome do arquivo: src/lib.rs
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Listagem 15-23: Criando duas referências mutáveis no mesmo escopo para ver que RefCell<T> entrará em pânico
Criamos uma variável one_borrow para o ponteiro inteligente RefMut<T> retornado de borrow_mut. Em seguida, criamos outro empréstimo mutável da mesma forma na variável two_borrow. Isso cria duas referências mutáveis no mesmo escopo, o que não é permitido. Quando executamos os testes para nossa biblioteca, o código na Listagem 15-23 compilará sem nenhum erro, mas o teste falhará:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Observe que o código entrou em pânico com a mensagem already borrowed: BorrowMutError. É assim que RefCell<T> lida com violações das regras de empréstimo em tempo de execução.
Optar por detectar erros de empréstimo em tempo de execução em vez de tempo de compilação, como fizemos aqui, significa que você potencialmente estaria encontrando erros em seu código mais tarde no processo de desenvolvimento: possivelmente não até que seu código fosse implantado em produção. Além disso, seu código incorreria em uma pequena penalidade de desempenho em tempo de execução como resultado de acompanhar os empréstimos em tempo de execução em vez de tempo de compilação. No entanto, usar RefCell<T> torna possível escrever um objeto mock que pode se modificar para acompanhar as mensagens que viu enquanto você o está usando em um contexto onde apenas valores imutáveis são permitidos. Você pode usar RefCell<T> apesar de suas compensações para obter mais funcionalidade do que as referências regulares fornecem.
<T> e RefCell<T>Uma forma comum de usar RefCell<T> é em combinação com Rc<T>. Lembre-se que Rc<T> permite que você tenha múltiplos proprietários de alguns dados, mas ele só dá acesso imutável a esses dados. Se você tiver um Rc<T> que contém um RefCell<T>, você pode obter um valor que pode ter múltiplos proprietários e que você pode mutar!
Por exemplo, lembre-se do exemplo de lista cons na Listagem 15-18, onde usamos Rc<T> para permitir que múltiplas listas compartilhassem a propriedade de outra lista. Como Rc<T> contém apenas valores imutáveis, não podemos alterar nenhum dos valores na lista depois de criá-los. Vamos adicionar RefCell<T> por sua capacidade de alterar os valores nas listas. A Listagem 15-24 mostra que, usando um RefCell<T> na definição Cons, podemos modificar o valor armazenado em todas as listas.
Nome do arquivo: src/main.rs
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
1 let value = Rc::new(RefCell::new(5));
2 let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
3 *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
Listagem 15-24: Usando Rc<RefCell<i32>> para criar uma List que podemos mutar
Criamos um valor que é uma instância de Rc<RefCell<i32>> e o armazenamos em uma variável chamada value [1] para que possamos acessá-lo diretamente mais tarde. Em seguida, criamos uma List em a com uma variante Cons que contém value [2]. Precisamos clonar value para que tanto a quanto value tenham a propriedade do valor interno 5, em vez de transferir a propriedade de value para a ou fazer com que a empreste de value.
Envolvemos a lista a em um Rc<T> para que, quando criarmos as listas b e c, ambas possam se referir a a, que é o que fizemos na Listagem 15-18.
Depois de criarmos as listas em a, b e c, queremos adicionar 10 ao valor em value [3]. Fazemos isso chamando borrow_mut em value, que usa o recurso de desreferenciação automática que discutimos em "Onde está o operador ->?" para desreferenciar o Rc<T> para o valor interno RefCell<T>. O método borrow_mut retorna um ponteiro inteligente RefMut<T>, e usamos o operador de desreferenciação nele e alteramos o valor interno.
Quando imprimimos a, b e c, podemos ver que todos eles têm o valor modificado de 15 em vez de 5:
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Essa técnica é muito legal! Usando RefCell<T>, temos um valor List externamente imutável. Mas podemos usar os métodos em RefCell<T> que fornecem acesso à sua mutabilidade interior para que possamos modificar nossos dados quando precisarmos. As verificações em tempo de execução das regras de empréstimo nos protegem de condições de corrida de dados, e às vezes vale a pena trocar um pouco de velocidade por essa flexibilidade em nossas estruturas de dados. Observe que RefCell<T> não funciona para código multithread! Mutex<T> é a versão thread-safe de RefCell<T>, e discutiremos Mutex<T> no Capítulo 16.
Parabéns! Você concluiu o laboratório de RefCell