Tratando Ponteiros Inteligentes como ReferĂȘncias Normais com a Trait Deref

Implementar a trait Deref nos permite personalizar o comportamento do operador de desreferĂȘncia (dereference operator), * (que Ă© diferente do operador de multiplicação ou de glob). Implementando a Deref de tal modo que o ponteiro inteligente possa ser tratado como uma referĂȘncia normal, podemos escrever cĂłdigo que opere sobre referĂȘncias e usar esse cĂłdigo com ponteiros inteligentes tambĂ©m.

Primeiro vamos ver como o * funciona com referĂȘncias normais, e entĂŁo vamos tentar definir nosso prĂłprio tipo a la Box<T> e ver por que o * nĂŁo funciona como uma referĂȘncia no nosso tipo recĂ©m-criado. Vamos explorar como a trait Deref torna possĂ­vel aos ponteiros inteligentes funcionarem de um jeito similar a referĂȘncias. E entĂŁo iremos dar uma olhada na funcionalidade de coerção de desreferĂȘncia (deref coercion) e como ela nos permite trabalhar tanto com referĂȘncias quanto com ponteiros inteligentes.

Seguindo o Ponteiro até o Valor com *

Uma referĂȘncia normal Ă© um tipo de ponteiro, e um jeito de pensar sobre um ponteiro Ă© como uma seta atĂ© um valor armazenado em outro lugar. Na Listagem 15-6, nĂłs criamos uma referĂȘncia a um valor i32 e em seguida usamos o operador de desreferĂȘncia para seguir a referĂȘncia atĂ© o dado:

Arquivo: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listagem 15-6: Usando o operador de desreferĂȘncia para seguir uma referĂȘncia a um valor i32

A variĂĄvel x contĂ©m um valor i32, 5. NĂłs setamos y igual a uma referĂȘncia a x. Podemos conferir (coloquialmente, "assertar") que x Ă© igual a 5. Contudo, se queremos fazer uma asserção sobre o valor em y, temos que usar *y para seguir a referĂȘncia atĂ© o valor ao qual y aponta (por isso "desreferĂȘncia"). Uma vez que desreferenciamos y, temos acesso ao valor inteiro ao qual y aponta para podermos comparĂĄ-lo com 5.

Se em vez disso tentåssemos escrever assert_eq!(5, y);, receberíamos este erro de compilação:

erro[E0277]: a trait bound `{integer}: std::cmp::PartialEq<&{integer}>` nĂŁo foi
satisfeita
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^^ nĂŁo posso comparar `{integer}` com `&{integer}`
  |
  = ajuda: a trait `std::cmp::PartialEq<&{integer}>` nĂŁo estĂĄ implementada para
  `{integer}`

Comparar um nĂșmero com uma referĂȘncia a um nĂșmero nĂŁo Ă© permitido porque eles sĂŁo de tipos diferentes. Devemos usar * para seguir a referĂȘncia atĂ© o valor ao qual ela estĂĄ apontando.

Usando Box<T> como uma ReferĂȘncia

Podemos reescrever o cĂłdigo na Listagem 15-6 para usar um Box<T> em vez de uma referĂȘncia, e o operador de desreferĂȘncia vai funcionar do mesmo jeito que na Listagem 15-7:

Arquivo: src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listagem 15-7: Usando o operador de desreferĂȘncia em um Box<i32>

A Ășnica diferença entre a Listagem 15-7 e a Listagem 15-6 Ă© que aqui nĂłs setamos y para ser uma instĂąncia de um box apontando para o valor em x em vez de uma referĂȘncia apontando para o valor de x. Na Ășltima asserção, podemos usar o operador de desreferĂȘncia para seguir o ponteiro do box do mesmo jeito que fizemos quando y era uma referĂȘncia. A seguir, vamos explorar o que tem de especial no Box<T> que nos permite usar o operador de desreferĂȘncia, criando nosso prĂłprio tipo box.

Definindo Nosso PrĂłprio Ponteiro Inteligente

Vamos construir um smart pointer parecido com o tipo Box<T> fornecido pela biblioteca padrĂŁo para vermos como ponteiros inteligentes, por padrĂŁo, se comportam diferente de referĂȘncias. Em seguida, veremos como adicionar a habilidade de usar o operador de desreferĂȘncia.

O tipo Box<T> no fim das contas é definido como uma struct-tupla (tuple struct) de um elemento, então a Listagem 15-8 define um tipo MeuBox<T> da mesma forma. Também vamos definir uma função new como a definida no Box<T>:

Arquivo: src/main.rs


# #![allow(unused_variables)]
#fn main() {
struct MeuBox<T>(T);

impl<T> MeuBox<T> {
    fn new(x: T) -> MeuBox<T> {
        MeuBox(x)
    }
}
#}

Listagem 15-8: Definindo um tipo MeuBox<T>

Definimos um struct chamado MeuBox e declaramos um parùmetro genérico T, porque queremos que nosso tipo contenha valores de qualquer tipo. O tipo MeuBox é uma struct-tupla de um elemento do tipo T. A função MeuBox::new recebe um argumento do tipo T e retorna uma instùncia de MeuBox que contém o valor passado.

Vamos tentar adicionar a função main da Listagem 15-7 à Listagem 15-8 e alterå-la para usar o tipo MeuBox<T> que definimos em vez de Box<T>. O código na Listagem 15-9 não irå compilar porque o Rust não sabe como desreferenciar MeuBox:

Arquivo: src/main.rs

fn main() {
    let x = 5;
    let y = MeuBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Listagem 15-9: Tentando usar o MeuBox<T> do mesmo jeito que usamos referĂȘncias e o Box<T>

Aqui estå o erro de compilação resultante:

erro[E0614]: tipo `MeuBox<{integer}>` nĂŁo pode ser desreferenciado
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

Nosso tipo MeuBox<T> nĂŁo pode ser desreferenciado porque nĂŁo implementamos essa habilidade nele. Para habilitar desreferenciamento com o operador *, temos que implementar a trait Deref.

Implementando a Trait Deref para Tratar um Tipo como uma ReferĂȘncia

Conforme discutimos no CapĂ­tulo 10, para implementar uma trait, precisamos prover implementaçÔes para os mĂ©todos exigidos por ela. A trait Deref, disponibilizada pela biblioteca padrĂŁo, requer que implementemos um mĂ©todo chamado deref que pega emprestado self e retorna uma referĂȘncia para os dados internos. A Listagem 15-10 contĂ©m uma implementação de Deref que agrega Ă  definição de MeuBox:

Arquivo: src/main.rs


# #![allow(unused_variables)]
#fn main() {
use std::ops::Deref;

# struct MeuBox<T>(T);
impl<T> Deref for MeuBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}
#}

Listagem 15-10: Implementando Deref no MeuBox<T>

A sintaxe type Target = T; define um tipo associado para a trait Deref usar. Tipos associados sĂŁo um jeito ligeiramente diferente de declarar um parĂąmetro genĂ©rico, mas vocĂȘ nĂŁo precisa se preocupar com eles por ora; iremos cobri-los em mais detalhe no CapĂ­tulo 19.

NĂłs preenchemos o corpo do mĂ©todo deref com &self.0 para que deref retorne uma referĂȘncia ao valor que queremos acessar com o operador *. A função main na Listagem 15-9 que chama * no valor MeuBox<T> agora compila e as asserçÔes passam!

Sem a trait Deref, o compilador sĂł consegue desreferenciar referĂȘncias &. O mĂ©todo deref dĂĄ ao compilador a habilidade de tomar um valor de qualquer tipo que implemente Deref e chamar o mĂ©todo deref para pegar uma referĂȘncia &, que ele sabe como desreferenciar.

Quando entramos *y na Listagem 15-9, por trĂĄs dos panos o Rust na verdade rodou este cĂłdigo:

*(y.deref())

O Rust substitui o operador * com uma chamada ao mĂ©todo deref e em seguida uma desreferĂȘncia comum, de modo que nĂłs programadores nĂŁo precisamos pensar sobre se temos ou nĂŁo que chamar o mĂ©todo deref. Essa funcionalidade do Rust nos permite escrever cĂłdigo que funcione identicamente quando temos uma referĂȘncia comum ou um tipo que implementa Deref.

O fato de o mĂ©todo deref retornar uma referĂȘncia ao valor, e a desreferĂȘncia comum fora dos parĂȘnteses em *(y.deref()) ainda ser necessĂĄria, Ă© devido ao sistema de posse (ownership). Se o mĂ©todo deref retornasse o valor diretamente em vez de uma referĂȘncia ao valor, o valor seria movido para fora do self. NĂłs nĂŁo queremos tomar posse do valor interno do MeuBox<T> neste e na maioria dos casos em que usamos o operador de desreferĂȘncia.

Note que o * é substituído por uma chamada ao método deref e então uma chamada ao * apenas uma vez, cada vez que digitamos um * no nosso código. Como a substituição do * não entra em recursão infinita, nós terminamos com o dado do tipo i32, que corresponde ao 5 em assert_eq! na Listagem 15-9.

CoerçÔes de DesreferĂȘncia ImplĂ­citas com FunçÔes e MĂ©todos

Coerção de desreferĂȘncia (deref coercion) Ă© uma conveniĂȘncia que o Rust aplica a argumentos de funçÔes e mĂ©todos. A coerção de desreferĂȘncia converte uma referĂȘncia a um tipo que implementa Deref em uma referĂȘncia a um tipo ao qual a Deref pode converter o tipo original. A coerção de desreferĂȘncia acontece automaticamente quando passamos uma referĂȘncia ao valor de um tipo especĂ­fico como argumento a uma função ou mĂ©todo e esse tipo nĂŁo corresponde ao tipo do parĂąmetro na definição da função ou mĂ©todo. Uma sequĂȘncia de chamadas ao mĂ©todo deref converte o tipo que providenciamos no tipo que o parĂąmetro exige.

A coerção de desreferĂȘncia foi adicionada ao Rust para que programadores escrevendo chamadas a mĂ©todos e funçÔes nĂŁo precisassem adicionar tantas referĂȘncias e desreferĂȘncias explĂ­citas com & e *. A funcionalidade de coerção de desreferĂȘncia tambĂ©m nos permite escrever mais cĂłdigo que funcione tanto com referĂȘncias quanto com ponteiros inteligentes.

Para ver a coerção de desreferĂȘncia em ação, vamos usar o tipo MeuBox<T> que definimos na Listagem 15-8 e tambĂ©m a implementação de Deref que adicionamos na Listagem 15-10. A Listagem 15-11 mostra a definição de uma função que tem um parĂąmetro do tipo string slice:

Arquivo: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn ola(nome: &str) {
    println!("OlĂĄ, {}!", nome);
}
#}

Listagem 15-11: Uma função ola que tem um parùmetro nome do tipo &str

Podemos chamar a função ola passando uma string slice como argumento, por exemplo ola("Rust");. A coerção de desreferĂȘncia torna possĂ­vel chamar ola com uma referĂȘncia a um valor do tipo MeuBox<String>, como mostra a Listagem 15-12:

Arquivo: src/main.rs

# use std::ops::Deref;
#
# struct MeuBox<T>(T);
#
# impl<T> MeuBox<T> {
#     fn new(x: T) -> MeuBox<T> {
#         MeuBox(x)
#     }
# }
#
# impl<T> Deref for MeuBox<T> {
#     type Target = T;
#
#     fn deref(&self) -> &T {
#         &self.0
#     }
# }
#
# fn ola(name: &str) {
#     println!("OlĂĄ, {}!", name);
# }
#
fn main() {
    let m = MeuBox::new(String::from("Rust"));
    ola(&m);
}

Listagem 15-12: Chamando ola com uma referĂȘncia a um valor MeuBox<String>, o que sĂł funciona por causa da coerção de desreferĂȘncia

Aqui estamos chamando a função ola com o argumento &m, que Ă© uma referĂȘncia a um valor MeuBox<String>. Como implementamos a trait Deref em MeuBox<T> na Listagem 15-10, o Rust pode transformar &MeuBox<String> em &String chamando deref. A biblioteca padrĂŁo provĂȘ uma implementação de Deref para String que retorna uma string slice, documentada na API de Deref. O Rust chama deref de novo para transformar o &String em &str, que corresponde Ă  definição da função ola.

Se o Rust nĂŁo implementasse coerção de desreferĂȘncia, terĂ­amos que escrever o cĂłdigo na Listagem 15-13 em vez do cĂłdigo na Listagem 15-12 para chamar ola com um valor do tipo &MeuBox<String>:

Arquivo: src/main.rs

# use std::ops::Deref;
#
# struct MeuBox<T>(T);
#
# impl<T> MeuBox<T> {
#     fn new(x: T) -> MeuBox<T> {
#         MeuBox(x)
#     }
# }
#
# impl<T> Deref for MeuBox<T> {
#     type Target = T;
#
#     fn deref(&self) -> &T {
#         &self.0
#     }
# }
#
# fn ola(name: &str) {
#     println!("OlĂĄ, {}!", name);
# }
#
fn main() {
    let m = MeuBox::new(String::from("Rust"));
    ola(&(*m)[..]);
}

Listagem 15-13: O cĂłdigo que terĂ­amos que escrever se o Rust nĂŁo tivesse coerção de desreferĂȘncia

O (*m) desreferencia o MeuBox<String> em uma String. EntĂŁo o & e o [..] obtĂȘm uma string slice da String que Ă© igual Ă  string inteira para corresponder Ă  assinatura de ola. O cĂłdigo sem coerção de desreferĂȘncia Ă© mais difĂ­cil de ler, escrever e entender com todos esses sĂ­mbolos envolvidos. A coerção de desreferĂȘncia permite que o Rust lide com essas conversĂ”es automaticamente para nĂłs.

Quando a trait Deref estĂĄ definida para os tipos envolvidos, o Rust analisa os tipos e usa Deref::deref tantas vezes quanto necessĂĄrio para chegar a uma referĂȘncia que corresponda ao tipo do parĂąmetro. O nĂșmero de vezes que Deref::deref precisa ser inserida Ă© resolvido em tempo de compilação, entĂŁo nĂŁo existe nenhuma penalidade em tempo de execução para tomar vantagem da coerção de desreferĂȘncia.

Como a Coerção de DesreferĂȘncia Interage com a Mutabilidade

De modo semelhante a como usamos a trait Deref para redefinir * em referĂȘncias imutĂĄveis, o Rust provĂȘ uma trait DerefMut para redefinir * em referĂȘncias mutĂĄveis.

O Rust faz coerção de desreferĂȘncia quando ele encontra tipos e implementaçÔes de traits em trĂȘs casos:

  • De &T para &U quando T: Deref<Target=U>;
  • De &mut T para &mut U quando T: DerefMut<Target=U>;
  • De &mut T para &U quando T: Deref<Target=U>.

Os primeiros dois casos sĂŁo o mesmo exceto pela mutabilidade. O primeiro caso afirma que se vocĂȘ tem uma &T, e T implementa Deref para algum tipo U, vocĂȘ pode obter um &U de maneira transparente. O segundo caso afirma que a mesma coerção de desreferĂȘncia acontece para referĂȘncias mutĂĄveis.

O terceiro caso Ă© mais complicado: o Rust tambĂ©m irĂĄ coagir uma referĂȘncia mutĂĄvel a uma imutĂĄvel. Mas o contrĂĄrio nĂŁo Ă© possĂ­vel: referĂȘncias imutĂĄveis nunca serĂŁo coagidas a referĂȘncias mutĂĄveis. Por causa das regras de emprĂ©stimo, se vocĂȘ tem uma referĂȘncia mutĂĄvel, ela deve ser a Ășnica referĂȘncia Ă queles dados (caso contrĂĄrio, o programa nĂŁo compila). Converter uma referĂȘncia mutĂĄvel a uma imutĂĄvel nunca quebrarĂĄ as regras de emprĂ©stimo. Converter uma referĂȘncia imutĂĄvel a uma mutĂĄvel exigiria que houvesse apenas uma referĂȘncia imutĂĄvel Ă queles dados, e as regras de emprĂ©stimo nĂŁo garantem isso. Portanto, o Rust nĂŁo pode assumir que converter uma referĂȘncia imutĂĄvel a uma mutĂĄvel seja possĂ­vel.