Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tratando Ponteiros Inteligentes como Referências Comuns

Implementar a trait Deref permite personalizar o comportamento do operador de desreferência * (não confundir com o operador de multiplicação ou glob). Ao implementar Deref de modo que um ponteiro inteligente possa ser tratado como uma referência comum, você pode escrever código que opera sobre referências e usar esse código também com ponteiros inteligentes.

Vamos primeiro ver como o operador de desreferência funciona com referências comuns. Depois, tentaremos definir um tipo personalizado que se comporta como Box<T> e veremos por que o operador de desreferência não funciona como uma referência no nosso tipo recém-definido. Exploraremos como implementar a trait Deref torna possível que ponteiros inteligentes funcionem de maneiras semelhantes a referências. Então veremos o recurso de coerção de desreferência de Rust e como ele nos permite trabalhar tanto com referências quanto com ponteiros inteligentes.

Seguindo a Referência até o Valor

Uma referência comum é um tipo de ponteiro, e uma forma de pensar em um ponteiro é como uma seta para um valor armazenado em outro lugar. Na Listagem 15-6, criamos uma referência para um valor i32 e então usamos o operador de desreferência para seguir a referência até o valor.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: Usando o operador de desreferência para seguir uma referência até um valor i32

A variável x armazena um valor i32, 5. Definimos y como uma referência a x. Podemos verificar que x é igual a 5. No entanto, se quisermos fazer uma asserção sobre o valor em y, precisamos usar *y para seguir a referência até o valor para o qual ela aponta (daí desreferenciar), para que o compilador possa comparar o valor real. Depois de desreferenciar y, temos acesso ao valor inteiro para o qual y aponta, que podemos comparar com 5.

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

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Comparar um número com uma referência para um número não é permitido, porque eles são tipos diferentes. Precisamos usar o operador de desreferência para seguir a referência até o valor para o qual ela aponta.

Usando Box<T> como uma Referência

Podemos reescrever o código da Listagem 15-6 para usar um Box<T> em vez de uma referência; o operador de desreferência usado no Box<T> da Listagem 15-7 funciona da mesma forma que o operador de desreferência usado na referência da Listagem 15-6.

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: Usando o operador de desreferência em um Box<i32>

A principal diferença entre a Listagem 15-7 e a Listagem 15-6 é que aqui definimos y como uma instância de um box que aponta para uma cópia do valor de x, em vez de uma referência que aponta para o valor de x. Na última asserção, podemos usar o operador de desreferência para seguir o ponteiro do box da mesma forma que fizemos quando y era uma referência. A seguir, exploraremos o que há de especial em Box<T> que nos permite usar o operador de desreferência, definindo nosso próprio tipo de box.

Definindo Nosso Próprio Ponteiro Inteligente

Vamos construir um tipo wrapper semelhante ao tipo Box<T> fornecido pela biblioteca padrão para experimentar como tipos de ponteiros inteligentes se comportam de forma diferente de referências por padrão. Depois, veremos como adicionar a capacidade de usar o operador de desreferência.

Observação: há uma grande diferença entre o tipo MyBox<T> que estamos prestes a construir e o Box<T> real: nossa versão não armazenará seus dados no heap. Estamos focando este exemplo em Deref, então o local real onde os dados são armazenados é menos importante que o comportamento semelhante ao de ponteiro.

No fim das contas, o tipo Box<T> é definido como uma tuple struct com um elemento, então a Listagem 15-8 define um tipo MyBox<T> da mesma forma. Também definiremos uma função new para corresponder à função new definida em Box<T>.

Filename: src/main.rs
struct MyBox<T>(T);

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

fn main() {}
Listing 15-8: Definindo um tipo MyBox<T>

Definimos uma struct chamada MyBox e declaramos um parâmetro genérico T porque queremos que nosso tipo armazene valores de qualquer tipo. O tipo MyBox é uma tuple struct com um elemento do tipo T. A função MyBox::new recebe um parâmetro do tipo T e retorna uma instância de MyBox que armazena o valor passado.

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

Filename: src/main.rs
struct MyBox<T>(T);

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

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-9: Tentando usar MyBox<T> da mesma forma que usamos referências e Box<T>

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

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^ can't be dereferenced

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

Nosso tipo MyBox<T> não pode ser desreferenciado porque não implementamos essa capacidade no tipo. Para habilitar a desreferência com o operador *, implementamos a trait Deref.

Implementando a Trait Deref

Como discutimos em “Implementando uma Trait em um Tipo” no Capítulo 10, para implementar uma trait precisamos fornecer implementações para os métodos exigidos por ela. A trait Deref, fornecida pela biblioteca padrão, exige que implementemos um método chamado deref, que pega self emprestado e retorna uma referência para os dados internos. A Listagem 15-10 contém uma implementação de Deref para adicionar à definição de MyBox<T>.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

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

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

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-10: Implementando Deref em MyBox<T>

A sintaxe type Target = T; define um tipo associado para a trait Deref usar. Tipos associados são uma forma um pouco diferente de declarar um parâmetro genérico, mas você não precisa se preocupar com eles por enquanto; vamos cobri-los em mais detalhes no Capítulo 20.

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 *; lembre-se de “Criando Diferentes Tipos com Tuple Structs” no Capítulo 5 que .0 acessa o primeiro valor em uma tuple struct. A função main da Listagem 15-9, que chama * no valor MyBox<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 capacidade de pegar um valor de qualquer tipo que implemente Deref e chamar o método deref para obter uma referência que ele sabe desreferenciar.

Quando escrevemos *y na Listagem 15-9, por trás dos panos Rust executou, na verdade, este código:

*(y.deref())

Rust substitui o operador * por uma chamada ao método deref e depois por uma desreferência comum, para que não precisemos pensar se devemos ou não chamar o método deref. Esse recurso de Rust nos permite escrever código que funciona de forma idêntica quando temos uma referência comum ou um tipo que implementa Deref.

O motivo pelo qual o método deref retorna uma referência a um valor, e pelo qual a desreferência comum fora dos parênteses em *(y.deref()) ainda é necessária, tem a ver com o sistema de ownership. Se o método deref retornasse o valor diretamente em vez de uma referência ao valor, o valor seria movido para fora de self. Não queremos tomar ownership do valor interno dentro de MyBox<T> neste caso nem na maioria dos casos em que usamos o operador de desreferência.

Observe que o operador * é substituído por uma chamada ao método deref e então por uma chamada ao operador * apenas uma vez, cada vez que usamos * em nosso código. Como a substituição do operador * não recorre infinitamente, acabamos com dados do tipo i32, que correspondem ao 5 em assert_eq! na Listagem 15-9.

Usando Coerção de Desreferência em Funções e Métodos

Coerção de desreferência (deref coercion) converte uma referência para um tipo que implementa a trait Deref em uma referência para outro tipo. Por exemplo, a coerção de desreferência pode converter &String em &str porque String implementa a trait Deref de modo que retorna &str. A coerção de desreferência é uma conveniência que Rust aplica a argumentos de funções e métodos, e funciona apenas em tipos que implementam a trait Deref. Ela acontece automaticamente quando passamos uma referência para o valor de um tipo específico como argumento para uma função ou método que não corresponde ao tipo do parâmetro na definição da função ou do método. Uma sequência de chamadas ao método deref converte o tipo que fornecemos no tipo de que o parâmetro precisa.

A coerção de desreferência foi adicionada a Rust para que programadores, ao escrever chamadas de funções e métodos, não precisassem adicionar tantas referências e desreferências explícitas com & e *. O recurso 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 MyBox<T> que definimos na Listagem 15-8, bem como 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 string slice.

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: Uma função hello que tem o parâmetro name do tipo &str

Podemos chamar a função hello com um string slice como argumento, como hello("Rust");, por exemplo. A coerção de desreferência torna possível chamar hello com uma referência para um valor do tipo MyBox<String>, como mostrado na Listagem 15-12.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

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

struct MyBox<T>(T);

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

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}
Listing 15-12: Chamando hello com uma referência para um valor MyBox<String>, o que funciona por causa da coerção de desreferência

Aqui chamamos a função hello com o argumento &m, que é uma referência para um valor MyBox<String>. Como implementamos a trait Deref em MyBox<T> na Listagem 15-10, Rust pode transformar &MyBox<String> em &String chamando deref. A biblioteca padrão fornece uma implementação de Deref em String que retorna um string slice, e isso está documentado na API de Deref. Rust chama deref novamente para transformar &String em &str, que corresponde à definição da função hello.

Se Rust não implementasse coerção de desreferência, teríamos que escrever o código da Listagem 15-13 em vez do código da Listagem 15-12 para chamar hello com um valor do tipo &MyBox<String>.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

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

struct MyBox<T>(T);

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

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}
Listing 15-13: O código que teríamos que escrever se Rust não tivesse coerção de desreferência

O (*m) desreferencia o MyBox<String> em uma String. Então, & e [..] obtêm um string slice da String que é igual à string inteira para corresponder à assinatura de hello. Esse código sem coerções 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 Rust lide com essas conversões automaticamente para nós.

Quando a trait Deref está definida para os tipos envolvidos, Rust analisará os tipos e usará Deref::deref tantas vezes quanto necessário para obter uma referência que corresponda ao tipo do parâmetro. A quantidade de vezes que Deref::deref precisa ser inserida é resolvida em tempo de compilação, então não há penalidade em tempo de execução por aproveitar a coerção de desreferência!

Lidando com Coerção de Desreferência em Referências Mutáveis

De forma semelhante a como você usa a trait Deref para sobrescrever o operador * em referências imutáveis, você pode usar a trait DerefMut para sobrescrever o operador * em referências mutáveis.

Rust faz coerção de desreferência quando encontra tipos e implementações de traits em três casos:

  1. De &T para &U quando T: Deref<Target=U>
  2. De &mut T para &mut U quando T: DerefMut<Target=U>
  3. De &mut T para &U quando T: Deref<Target=U>

Os dois primeiros casos são iguais, exceto que o segundo implementa mutabilidade. O primeiro caso afirma que, se você tem uma &T e T implementa Deref para algum tipo U, você pode obter uma &U de forma transparente. O segundo caso afirma que a mesma coerção de desreferência acontece para referências mutáveis.

O terceiro caso é mais sutil: Rust também fará coerção de uma referência mutável para uma imutável. Mas o inverso não é possível: referências imutáveis nunca serão coagidas para referências mutáveis. Por causa das regras de borrowing, se você tem uma referência mutável, essa referência mutável deve ser a única referência para aqueles dados (caso contrário, o programa não compilaria). Converter uma referência mutável em uma referência imutável nunca quebrará as regras de borrowing. Converter uma referência imutável em uma referência mutável exigiria que a referência imutável inicial fosse a única referência imutável para aqueles dados, mas as regras de borrowing não garantem isso. Portanto, Rust não pode assumir que converter uma referência imutável em uma referência mutável é possível.