Tratando Ponteiros Inteligentes como Referências Regulares

Beginner

This tutorial is from open-source community. Access the source code

Introdução

Bem-vindo a Tratando Ponteiros Inteligentes como Referências Regulares com Deref. Este laboratório faz parte do Livro Rust. Você pode praticar suas habilidades em Rust no LabEx.

Neste laboratório, exploraremos como a implementação do trait Deref permite que ponteiros inteligentes sejam tratados como referências regulares e como o recurso de coerção deref do Rust possibilita trabalhar com referências ou ponteiros inteligentes.

Tratando Ponteiros Inteligentes como Referências Regulares com Deref

Implementar o trait Deref permite que você personalize o comportamento do operador de desreferenciação * (não deve ser confundido com o operador de multiplicação ou glob). Ao implementar Deref de tal forma que um ponteiro inteligente possa ser tratado como uma referência regular, você pode escrever código que opera em referências e usar esse código também com ponteiros inteligentes.

Vamos primeiro analisar como o operador de desreferenciação funciona com referências regulares. Em seguida, tentaremos definir um tipo personalizado que se comporte como Box<T> e veremos por que o operador de desreferenciação não funciona como uma referência em nosso tipo recém-definido. Exploraremos como a implementação do trait Deref torna possível que ponteiros inteligentes funcionem de maneira semelhante às referências. Em seguida, analisaremos o recurso de coerção deref do Rust e como ele nos permite trabalhar com referências ou ponteiros inteligentes.

Nota: 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 onde os dados são realmente armazenados é menos importante do que o comportamento semelhante a um ponteiro.

Seguindo o Ponteiro para o Valor

Uma referência regular é um tipo de ponteiro, e uma maneira de pensar em um ponteiro é como uma seta para um valor armazenado em outro lugar. Na Listagem 15-6, criamos uma referência a um valor i32 e, em seguida, usamos o operador de desreferenciação para seguir a referência ao valor.

Nome do arquivo: src/main.rs

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

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

Listagem 15-6: Usando o operador de desreferenciação para seguir uma referência a um valor i32

A variável x contém um valor i32 5 [1]. Definimos y igual a uma referência a x [2]. Podemos afirmar que x é igual a 5 [3]. No entanto, se quisermos fazer uma afirmação sobre o valor em y, devemos usar *y para seguir a referência ao valor para o qual ela está apontando (portanto, desreferenciação) para que o compilador possa comparar o valor real [4]. Depois de desreferenciar y, temos acesso ao valor inteiro para o qual y está apontando, que podemos comparar com 5.

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

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}`

Comparar um número e uma referência a um número não é permitido porque são tipos diferentes. Devemos usar o operador de desreferenciação para seguir a referência ao valor para o 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; o operador de desreferenciação usado no Box<T> na Listagem 15-7 funciona da mesma forma que o operador de desreferenciação usado na referência na Listagem 15-6.

Nome do arquivo: src/main.rs

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

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

Listagem 15-7: Usando o operador de desreferenciação 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 uma box apontando para um valor copiado de x, em vez de uma referência apontando para o valor de x [1]. Na última asserção [2], podemos usar o operador de desreferenciação para seguir o ponteiro da box da mesma forma que fizemos quando y era uma referência. Em seguida, exploraremos o que é especial sobre Box<T> que nos permite usar o operador de desreferenciação, definindo nosso próprio tipo de box.

Definindo Nosso Próprio Ponteiro Inteligente

Vamos construir um ponteiro inteligente semelhante ao tipo Box<T> fornecido pela biblioteca padrão para experimentar como os ponteiros inteligentes se comportam de forma diferente das referências por padrão. Em seguida, veremos como adicionar a capacidade de usar o operador de desreferenciação.

O tipo Box<T> é, em última análise, definido como uma struct de tupla com um elemento, então a Listagem 15-8 define um tipo MyBox<T> da mesma maneira. Também definiremos uma função new para corresponder à função new definida em Box<T>.

Nome do arquivo: src/main.rs

 1 struct MyBox<T>(T);

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

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

Definimos uma struct chamada MyBox e declaramos um parâmetro genérico T [1] porque queremos que nosso tipo contenha valores de qualquer tipo. O tipo MyBox é uma struct de tupla com um elemento do tipo T. A função MyBox::new recebe um parâmetro do tipo T [2] e retorna uma instância MyBox que contém o valor passado [3].

Vamos tentar adicionar a função main na 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 na Listagem 15-9 não compilará porque o Rust não sabe como desreferenciar MyBox.

Nome do arquivo: src/main.rs

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

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

Listagem 15-9: Tentando usar MyBox<T> da mesma forma que usamos referências e Box<T>

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

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

Nosso tipo MyBox<T> não pode ser desreferenciado porque não implementamos essa capacidade em nosso tipo. Para habilitar a desreferenciação com o operador *, implementamos o trait Deref.

Implementando o Trait Deref

Como discutido em "Implementando um Trait em um Tipo", para implementar um trait, precisamos fornecer implementações para os métodos exigidos pelo trait. O trait Deref, fornecido pela biblioteca padrão, exige que implementemos um método chamado deref que empresta self e retorna uma referência aos dados internos. A Listagem 15-10 contém uma implementação de Deref para adicionar à definição de MyBox``<T>.

Nome do arquivo: src/main.rs

use std::ops::Deref;

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

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

Listagem 15-10: Implementando Deref em MyBox<T>

A sintaxe type Target = T; [1] define um tipo associado para o trait Deref usar. Tipos associados são uma maneira ligeiramente diferente de declarar um parâmetro genérico, mas você não precisa se preocupar com eles por enquanto; abordaremos eles em mais detalhes no Capítulo 19.

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 * [2]; lembre-se de "Usando Structs de Tupla Sem Campos Nomeados para Criar Tipos Diferentes" que .0 acessa o primeiro valor em uma struct de tupla. A função main na Listagem 15-9 que chama * no valor MyBox<T> agora compila, e as asserções passam!

Sem o trait Deref, o compilador só pode 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 como desreferenciar.

Quando inserimos *y na Listagem 15-9, nos bastidores o Rust realmente executou este código:

*(y.deref())

Rust substitui o operador * por uma chamada ao método deref e, em seguida, uma desreferenciação simples para que não tenhamos que pensar se precisamos ou não chamar o método deref. Esse recurso do Rust nos permite escrever código que funciona de forma idêntica, quer tenhamos uma referência regular ou um tipo que implemente Deref.

A razão pela qual o método deref retorna uma referência a um valor, e que a desreferenciação simples fora dos parênteses em *(y.deref()) ainda é necessária, tem a ver com o sistema de propriedade. Se o método deref retornasse o valor diretamente em vez de uma referência ao valor, o valor seria movido de self. Não queremos assumir a propriedade do valor interno dentro de MyBox<T> neste caso ou na maioria dos casos em que usamos o operador de desreferenciação.

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

Coerções Implícitas de Deref com Funções e Métodos

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

A coerção de deref foi adicionada ao Rust para que os programadores que escrevem chamadas de funções e métodos não precisem adicionar tantas referências e desreferenciações explícitas com & e *. O recurso de coerção de deref também nos permite escrever mais código que pode funcionar para referências ou ponteiros inteligentes.

Para ver a coerção de deref 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 de fatia de string.

Nome do arquivo: src/main.rs

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

Listagem 15-11: Uma função hello que tem o parâmetro name do tipo &str

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

Nome do arquivo: src/main.rs

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

Listagem 15-12: Chamando hello com uma referência a um valor MyBox<String>, que funciona por causa da coerção de deref

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

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

Nome do arquivo: src/main.rs

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Listagem 15-13: O código que teríamos que escrever se o Rust não tivesse coerção de deref

O (*m) desreferencia o MyBox<String> em um String. Em seguida, o & e [..] pegam uma fatia de string do String que é igual à string inteira para corresponder à assinatura de hello. Este código sem coerções de deref é mais difícil de ler, escrever e entender com todos esses símbolos envolvidos. A coerção de deref permite que o Rust lide com essas conversões para nós automaticamente.

Quando o trait Deref é definido para os tipos envolvidos, o Rust analisará os tipos e usará Deref::deref quantas vezes forem necessárias para obter uma referência para corresponder ao tipo do parâmetro. O número de vezes que Deref::deref precisa ser inserido é resolvido em tempo de compilação, portanto, não há penalidade em tempo de execução por tirar proveito da coerção de deref!

Como a Coerção de Deref Interage com a Mutabilidade

Semelhante a como você usa o trait Deref para substituir o operador * em referências imutáveis, você pode usar o trait DerefMut para substituir o operador * em referências mutáveis.

O Rust faz a coerção de deref quando 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 dois primeiros casos são os mesmos, exceto que o segundo implementa a mutabilidade. O primeiro caso afirma que, se você tem um &T, e T implementa Deref para algum tipo U, você pode obter um &U de forma transparente. O segundo caso afirma que a mesma coerção de deref acontece para referências mutáveis.

O terceiro caso é mais complicado: o Rust também irá coagir 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 empréstimo (borrowing rules), se você tem uma referência mutável, essa referência mutável deve ser a única referência a esses 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 empréstimo. 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 a esses dados, mas as regras de empréstimo não garantem isso. Portanto, o Rust não pode fazer a suposição de que converter uma referência imutável em uma referência mutável é possível.

Resumo

Parabéns! Você concluiu o laboratório Tratando Ponteiros Inteligentes como Referências Regulares com Deref. Você pode praticar mais laboratórios no LabEx para aprimorar suas habilidades.