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

Traits Avançadas

Abordamos traits pela primeira vez em “Definindo comportamento compartilhado com traits” no Capítulo 10, mas não discutimos os detalhes mais avançados. Agora que você sabe mais sobre Rust, podemos entrar no âmago da questão.

Definindo Traits com Tipos Associados

Tipos associados conectam um espaço reservado de tipo a uma trait, de modo que as definições de métodos da trait possam usar esses tipos de espaço reservado em suas assinaturas. O implementador de uma trait especificará o tipo concreto a ser usado no lugar do espaço reservado para a implementação específica. Dessa forma, podemos definir uma trait que usa alguns tipos sem precisar saber exatamente quais são esses tipos até que a trait seja implementada.

Descrevemos a maioria dos recursos avançados deste capítulo como raramente necessários. Os tipos associados ficam em algum lugar no meio: são usados com menos frequência do que os recursos explicados no restante do livro, mas com mais frequência do que muitos dos outros recursos discutidos aqui.

Um exemplo de trait com tipo associado é a trait Iterator, fornecida pela biblioteca padrão. O tipo associado chama-se Item e representa o tipo dos valores sobre os quais o tipo que implementa a trait Iterator está iterando. A definição da trait Iterator é mostrada na Listagem 20-13.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}
Listing 20-13: A definição da trait Iterator, que possui um tipo associado Item

O tipo Item é um espaço reservado, e a definição do método next mostra que ele retornará valores do tipo Option<Self::Item>. Implementadores da trait Iterator especificam o tipo concreto de Item, e o método next retorna um Option contendo um valor desse tipo concreto.

Os tipos associados podem parecer semelhantes aos genéricos, na medida em que estes últimos também nos permitem definir comportamento sem especificar, de antemão, todos os tipos concretos envolvidos. Para examinar a diferença entre os dois conceitos, veremos uma implementação da trait Iterator em um tipo chamado Counter, que especifica o tipo Item como u32:

Filename: src/lib.rs
struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Essa sintaxe parece comparável à dos genéricos. Então, por que não definir a trait Iterator com genéricos, como mostrado na Listagem 20-14?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}
Listing 20-14: Uma definição hipotética da trait Iterator usando genéricos

A diferença é que, ao usar genéricos, como na Listagem 20-14, precisamos anotar os tipos em cada implementação; como também poderíamos implementar Iterator<String> for Counter, ou qualquer outra variação, poderíamos ter várias implementações de Iterator para Counter. Em outras palavras, quando uma trait tem um parâmetro genérico, ela pode ser implementada para um mesmo tipo várias vezes, alterando os tipos concretos dos parâmetros genéricos a cada implementação. Quando usássemos o método next em Counter, precisaríamos fornecer anotações de tipo para indicar qual implementação de Iterator queremos usar.

Com tipos associados, não precisamos anotar tipos, porque não podemos implementar a mesma trait várias vezes para um tipo. Na definição da Listagem 20-13, que usa tipos associados, só podemos escolher o tipo de Item uma única vez, porque só pode existir um impl Iterator for Counter. Não precisamos especificar, em todo lugar onde chamamos next em Counter, que queremos um iterador de valores u32.

Os tipos associados também passam a fazer parte do contrato da trait: implementadores da trait precisam fornecer um tipo para substituir o espaço reservado do tipo associado. Os tipos associados geralmente têm um nome que descreve como o tipo será usado, e documentar o tipo associado na documentação da API é uma boa prática.

Usando Parâmetros Genéricos Padrão e Sobrecarga de Operadores

Quando usamos parâmetros de tipo genérico, podemos especificar um tipo concreto padrão para o tipo genérico. Isso elimina a necessidade de implementadores da trait especificarem um tipo concreto se o tipo padrão já servir. Você define um tipo padrão ao declarar um tipo genérico com a sintaxe <PlaceholderType=ConcreteType>.

Um ótimo exemplo de situação em que essa técnica é útil é a sobrecarga de operadores (operator overloading), em que você personaliza o comportamento de um operador, como +, em situações específicas.

Rust não permite que você crie seus próprios operadores nem que sobrecarregue operadores arbitrariamente. Mas você pode sobrecarregar as operações e as traits correspondentes listadas em std::ops implementando a trait associada ao operador. Por exemplo, na Listagem 20-15, sobrecarregamos o operador + para somar duas instâncias de Point. Fazemos isso implementando a trait Add para a struct Point.

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

#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -> Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(
        Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
        Point { x: 3, y: 3 }
    );
}
Listing 20-15: Implementando a trait Add para sobrecarregar o operador + para instâncias de Point

O método add soma os valores x de duas instâncias de Point e os valores y dessas duas instâncias para criar um novo Point. A trait Add possui um tipo associado chamado Output, que determina o tipo retornado pelo método add.

O tipo genérico padrão neste código está na trait Add. Eis sua definição:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Esse código deve parecer familiar em termos gerais: uma trait com um método e um tipo associado. A parte nova é Rhs=Self: essa sintaxe é chamada de default type parameters. O parâmetro de tipo genérico Rhs, abreviação de “right-hand side”, define o tipo do parâmetro rhs no método add. Se não especificarmos um tipo concreto para Rhs ao implementar a trait Add, o tipo de Rhs terá como padrão Self, que será o tipo sobre o qual estamos implementando Add.

Quando implementamos Add para Point, usamos o valor padrão de Rhs porque queríamos somar duas instâncias de Point. Vejamos agora um exemplo de implementação da trait Add em que queremos personalizar o tipo Rhs, em vez de usar o padrão.

Temos duas structs, Millimeters e Meters, contendo valores em unidades diferentes. Esse empacotamento fino de um tipo existente em outra struct é conhecido como newtype pattern, que descrevemos em mais detalhes na seção “Implementando traits externas com o padrão newtype”. Queremos somar valores em milímetros a valores em metros e fazer com que a implementação de Add realize a conversão corretamente. Podemos implementar Add para Millimeters, usando Meters como Rhs, como mostra a Listagem 20-16.

Filename: src/lib.rs
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add<Meters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -> Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
Listing 20-16: Implementando a trait Add em Millimeters para somar Millimeters e Meters

Para somar Millimeters e Meters, especificamos impl Add<Meters> para definir o valor do parâmetro de tipo Rhs, em vez de usar o padrão Self.

Você usará parâmetros de tipo padrão de duas maneiras principais:

  1. Para estender um tipo sem quebrar código existente
  2. Para permitir personalização em casos específicos dos quais a maioria dos usuários não precisará

A trait Add da biblioteca padrão é um exemplo do segundo propósito: normalmente, você somará dois tipos iguais, mas a trait Add oferece a capacidade de ir além disso. Usar um parâmetro de tipo padrão na definição de Add significa que você não precisa especificar o parâmetro extra na maior parte do tempo. Em outras palavras, evita-se um pouco de boilerplate de implementação, o que torna a trait mais fácil de usar.

O primeiro propósito é parecido com o segundo, mas no sentido inverso: se você quiser adicionar um parâmetro de tipo a uma trait existente, pode fornecer um valor padrão para permitir a extensão da funcionalidade da trait sem quebrar o código de implementação já existente.

Desambiguando entre Métodos com o Mesmo Nome

Nada em Rust impede que uma trait tenha um método com o mesmo nome que o método de outra trait, nem impede que você implemente ambas as traits em um mesmo tipo. Também é possível implementar diretamente no tipo um método com o mesmo nome dos métodos dessas traits.

Ao chamar métodos com o mesmo nome, você precisará informar ao Rust qual deles deseja usar. Considere o código da Listagem 20-17, em que definimos duas traits, Pilot e Wizard, ambas com um método chamado fly. Em seguida, implementamos as duas traits para um tipo Human, que já possui um método chamado fly implementado diretamente. Cada método fly faz algo diferente.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {}
Listing 20-17: Duas traits são definidas com um método fly e implementadas no tipo Human, e um método fly também é implementado diretamente em Human

Quando chamamos fly em uma instância de Human, o compilador, por padrão, chama o método implementado diretamente no tipo, como mostrado na Listagem 20-18.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}
Listing 20-18: Chamando fly em uma instância de Human

A execução desse código imprimirá *waving arms furiously*, mostrando que Rust chamou o método fly implementado diretamente em Human.

Para chamar os métodos fly da trait Pilot ou da trait Wizard, precisamos usar uma sintaxe mais explícita para especificar a qual método fly nos referimos. A Listagem 20-19 demonstra essa sintaxe.

Filename: src/main.rs
trait Pilot {
    fn fly(&self);
}

trait Wizard {
    fn fly(&self);
}

struct Human;

impl Pilot for Human {
    fn fly(&self) {
        println!("This is your captain speaking.");
    }
}

impl Wizard for Human {
    fn fly(&self) {
        println!("Up!");
    }
}

impl Human {
    fn fly(&self) {
        println!("*waving arms furiously*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}
Listing 20-19: Especificando qual método fly de trait queremos chamar

Especificar o nome da trait antes do nome do método deixa claro para Rust qual implementação de fly queremos chamar. Também poderíamos escrever Human::fly(&person), o que é equivalente a person.fly(), usado na Listagem 20-19, mas isso é um pouco mais longo quando não precisamos desambiguar.

A execução deste código imprime o seguinte:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Como o método fly usa um parâmetro self, se tivéssemos dois tipos que implementassem a mesma trait, Rust poderia descobrir qual implementação usar com base no tipo de self.

No entanto, funções associadas que não são métodos não possuem parâmetro self. Quando existem vários tipos ou traits que definem funções associadas com o mesmo nome, Rust nem sempre sabe a qual tipo você está se referindo, a menos que use sintaxe totalmente qualificada. Por exemplo, na Listagem 20-20, criamos uma trait para um abrigo de animais que quer chamar todos os filhotes de cachorro de Spot. Criamos uma trait Animal com uma função associada chamada baby_name. A trait Animal é implementada para a struct Dog, na qual também fornecemos diretamente uma função associada baby_name.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 20-20: Uma trait com uma função associada e um tipo com uma função associada de mesmo nome que também implementa a trait

Implementamos em Dog o código para nomear todos os filhotes como Spot, na função associada baby_name. O tipo Dog também implementa a trait Animal, que descreve características compartilhadas por todos os animais. Filhotes de cachorro são chamados de puppy, e isso é expresso na implementação da trait Animal para Dog, na função associada baby_name dessa trait.

Em main, chamamos a função Dog::baby_name, que invoca a função definida diretamente em Dog. Esse código imprime o seguinte:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

Essa saída não é o que queríamos. Queremos chamar a função baby_name que faz parte da trait Animal implementada em Dog, para que o código imprima A baby dog is called a puppy. A técnica de especificar o nome da trait, que usamos na Listagem 20-19, não ajuda aqui; se mudarmos main para o código da Listagem 20-21, obteremos um erro de compilação.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 20-21: Tentando chamar a função baby_name da trait Animal, mas o Rust não sabe qual implementação usar

Como Animal::baby_name não possui um parâmetro self e pode haver outros tipos que implementem a trait Animal, Rust não consegue descobrir qual implementação de Animal::baby_name queremos. Obteremos este erro do compilador:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
 2 |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
   |                                           +++++++       +

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

Para desambiguar e dizer ao Rust que queremos usar a implementação de Animal para Dog, e não a implementação de Animal para algum outro tipo, precisamos usar sintaxe totalmente qualificada. A Listagem 20-22 mostra como usar essa sintaxe.

Filename: src/main.rs
trait Animal {
    fn baby_name() -> String;
}

struct Dog;

impl Dog {
    fn baby_name() -> String {
        String::from("Spot")
    }
}

impl Animal for Dog {
    fn baby_name() -> String {
        String::from("puppy")
    }
}

fn main() {
    println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
Listing 20-22: Usando sintaxe totalmente qualificada para especificar que queremos chamar a função baby_name da trait Animal como implementada em Dog

Estamos fornecendo ao Rust uma anotação de tipo entre sinais de menor e maior, que indica que queremos chamar a função baby_name da trait Animal como implementada em Dog, ou seja, queremos tratar o tipo Dog como um Animal nessa chamada de função. Esse código agora imprimirá o que queremos:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

Em geral, a sintaxe totalmente qualificada é definida da seguinte forma:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Para funções associadas que não são métodos, não haveria receiver: haveria apenas a lista dos outros argumentos. Você poderia usar sintaxe totalmente qualificada em todos os lugares em que chama funções ou métodos. No entanto, pode omitir qualquer parte dessa sintaxe que Rust consiga deduzir a partir de outras informações do programa. Você só precisa usar essa forma mais detalhada quando existem várias implementações com o mesmo nome e Rust precisa de ajuda para identificar qual delas você quer chamar.

Usando Supertraits

Às vezes você pode escrever uma definição de trait que depende de outra trait. Para um tipo implementar a primeira trait, você quer exigir que esse tipo também implemente a segunda. Você faz isso para que a definição da sua trait possa usar os itens associados da segunda. A trait da qual a sua definição depende é chamada de supertrait da sua trait.

Por exemplo, digamos que queremos criar uma trait OutlinePrint com um método outline_print que imprima um determinado valor formatado de modo a ficar emoldurado por asteriscos. Ou seja, dada uma struct Point que implementa a trait Display da biblioteca padrão e produz (x, y), quando chamamos outline_print em uma instância de Point que tenha 1 em x e 3 em y, ela deve imprimir o seguinte:

**********
*        *
* (1, 3) *
*        *
**********

Na implementação do método outline_print, queremos usar a funcionalidade da trait Display. Portanto, precisamos especificar que a trait OutlinePrint funcionará apenas para tipos que também implementem Display e forneçam a funcionalidade de que OutlinePrint precisa. Podemos fazer isso na definição da trait, especificando OutlinePrint: Display. Essa técnica é semelhante a adicionar uma trait bound. A Listagem 20-23 mostra uma implementação da trait OutlinePrint.

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

fn main() {}
Listing 20-23: Implementando a trait OutlinePrint, que exige a funcionalidade de Display

Como especificamos que OutlinePrint exige a trait Display, podemos usar a função to_string, implementada automaticamente para qualquer tipo que implemente Display. Se tentássemos usar to_string sem adicionar os dois-pontos e especificar a trait Display após o nome da trait, obteríamos um erro dizendo que nenhum método chamado to_string foi encontrado para o tipo &Self no escopo atual.

Vamos ver o que acontece quando tentamos implementar OutlinePrint em um tipo que não implementa Display, como a struct Point:

Filename: src/main.rs
use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Recebemos um erro dizendo que a trait Display é exigida, mas não foi implementada:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:20:23
   |
20 | impl OutlinePrint for Point {}
   |                       ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint`

error[E0277]: `Point` doesn't implement `std::fmt::Display`
  --> src/main.rs:24:7
   |
24 |     p.outline_print();
   |       ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
   |
note: required by a bound in `OutlinePrint::outline_print`
  --> src/main.rs:3:21
   |
 3 | trait OutlinePrint: fmt::Display {
   |                     ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
 4 |     fn outline_print(&self) {
   |        ------------- required by a bound in this associated function

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

Para corrigir isso, implementamos Display para Point e satisfazemos a restrição exigida por OutlinePrint, assim:

Filename: src/main.rs
trait OutlinePrint: fmt::Display {
    fn outline_print(&self) {
        let output = self.to_string();
        let len = output.len();
        println!("{}", "*".repeat(len + 4));
        println!("*{}*", " ".repeat(len + 2));
        println!("* {output} *");
        println!("*{}*", " ".repeat(len + 2));
        println!("{}", "*".repeat(len + 4));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Então, implementar a trait OutlinePrint para Point compilará com sucesso, e poderemos chamar outline_print em uma instância de Point para exibi-la dentro de um contorno de asteriscos.

Implementando Traits Externas com o Padrão Newtype

Na seção “Implementando uma trait em um tipo” do Capítulo 10, mencionamos a regra órfã, que afirma que só podemos implementar uma trait para um tipo se a trait, o tipo, ou ambos, forem locais ao nosso crate. É possível contornar essa restrição usando o padrão newtype, que envolve criar um novo tipo em uma tuple struct. (Cobrimos tuple structs na seção “Criando diferentes tipos com tuple structs”, no Capítulo 5.) A tuple struct terá um campo e será um wrapper fino em torno do tipo para o qual queremos implementar uma trait. Assim, o tipo wrapper é local ao nosso crate, e podemos implementar a trait nele. Newtype é um termo que vem da linguagem de programação Haskell. Não há penalidade de desempenho em tempo de execução ao usar esse padrão, e o tipo wrapper é eliminado em tempo de compilação.

Por exemplo, digamos que queremos implementar Display para Vec<T>, algo que a regra órfã nos impede de fazer diretamente porque tanto a trait Display quanto o tipo Vec<T> são definidos fora do nosso crate. Podemos criar uma struct Wrapper que contenha uma instância de Vec<T>; então, podemos implementar Display para Wrapper e usar o valor Vec<T>, como mostrado na Listagem 20-24.

Filename: src/main.rs
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {w}");
}
Listing 20-24: Criando um tipo Wrapper em torno de Vec<String> para implementar Display

A implementação de Display usa self.0 para acessar o Vec<T> interno porque Wrapper é uma tuple struct e Vec<T> é o item no índice 0 da tupla. Assim, podemos usar a funcionalidade da trait Display em Wrapper.

A desvantagem de usar essa técnica é que Wrapper é um novo tipo e, portanto, não tem os métodos do valor que está encapsulando. Teríamos de implementar todos os métodos de Vec<T> diretamente em Wrapper de modo que eles delegassem para self.0, o que nos permitiria tratar Wrapper exatamente como um Vec<T>. Se quiséssemos que o novo tipo tivesse todos os métodos do tipo interno, implementar a trait Deref para Wrapper, retornando o tipo interno, seria uma solução (discutimos a implementação de Deref na seção “Tratando smart pointers como referências normais” do Capítulo 15). Se não quiséssemos que Wrapper tivesse todos os métodos do tipo interno, por exemplo, para restringir seu comportamento, teríamos de implementar manualmente apenas os métodos desejados.

Esse padrão newtype também é útil mesmo quando traits não estão envolvidas. Vamos mudar o foco e ver algumas maneiras avançadas de interagir com o sistema de tipos de Rust.