Compartilhe comportamentos com traits

RustBeginner
Pratique Agora

Introdução

Às vezes, tipos de dados não relacionados precisam oferecer o mesmo comportamento. Um sensor de estufa e uma remessa têm campos diferentes, mas ambos podem descrever seu status atual. As traits do Rust dão nome a esse recurso compartilhado sem obrigar os tipos a terem a mesma estrutura de dados.

Neste laboratório, você definirá uma trait pequena, implementará essa trait para duas estruturas preparadas e passará os dois valores para uma única função genérica de relatório. As estruturas maiores e os dados de exemplo já foram criados para que cada edição permaneça focada.

Defina um contrato de status compartilhado

Nesta etapa, você definirá uma trait que nomeia um comportamento compartilhado por tipos que, de outra forma, não têm relação entre si.

Entre no pacote preparado e examine todo o esqueleto do código. O comando sed -n exibe as linhas solicitadas sem editá-las:

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

Sensor e Shipment têm campos diferentes. Os campos de texto preparados usam &'static str, ou seja, literais de string emprestados que permanecem válidos durante todo o programa. A anotação de lifetime 'static não faz parte desta lição sobre traits e foi deixada para depois de propósito; você não precisa editá-la nem analisá-la aqui.

Abaixo dos placeholders, show_status já é genérica. Leia T: Status em três partes: T representa um tipo concreto, os dois-pontos significam “deve atender a”, e Status é a trait exigida. Em linguagem simples: “para qualquer tipo T, desde que T implemente Status”. Essa garantia permite que a função chame item.status() com segurança.

A trait precisa existir antes que o Rust possa entender o bound. Abra o código-fonte:

nano src/main.rs

Substitua // STATUS_TRAIT por:

trait Status {
    fn status(&self) -> String;
}

A linha do método termina com ponto e vírgula porque a trait declara um método obrigatório, mas não escolhe uma implementação. &self permite que o método examine um valor sem assumir sua propriedade, e String permite que cada tipo crie seu próprio texto de status. Cada implementação posterior deverá repetir exatamente esse nome de método, receptor e tipo de retorno, fornecendo então seu próprio corpo.

Salve com Ctrl+O, pressione Enter e saia com Ctrl+X. Em seguida, verifique o pacote:

cargo check

Você poderá ver avisos informando que a trait e os itens preparados ainda não são usados. A linha final Finished é a evidência importante: o contrato compartilhado e o bound genérico são válidos. As próximas duas etapas adicionarão as implementações e as chamadas.

Implemente Status para Sensor

Nesta etapa, você atenderá ao contrato da trait para Sensor e enviará um sensor pelo gerador de relatórios compartilhado.

Um bloco impl Trait for Type conecta uma trait existente a um tipo concreto. A assinatura do método deve corresponder à da trait, mas o corpo pode usar os próprios campos desse tipo.

Abra o código-fonte:

nano src/main.rs

Substitua // SENSOR_IMPLEMENTATION por esta implementação curta:

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format! cria e retorna uma String; ele não imprime nada por conta própria. A implementação pode usar location e celsius porque self é um Sensor emprestado.

Em seguida, substitua // SENSOR_REPORT dentro de main por:

    show_status("Sensor", &sensor);

O argumento &sensor corresponde ao parâmetro emprestado &T do gerador de relatórios, portanto sensor continua pertencendo a main. Salve e saia do Nano. Depois, execute o programa:

cargo run --quiet
Sensor: Greenhouse: 24°C

Essa linha comprova que Sensor atende a Status e pode passar pelo gerador de relatórios genérico com bound. A remessa ainda não foi conectada.

Implemente Status para Shipment

Nesta etapa, você implementará a mesma trait para Shipment e comprovará que um único gerador de relatórios aceita os dois tipos concretos.

Abra o código-fonte novamente:

nano src/main.rs

Substitua // SHIPMENT_IMPLEMENTATION por:

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

Este método segue o mesmo contrato da trait, mas usa os próprios campos id e stage da remessa. Em seguida, substitua // SHIPMENT_REPORT por:

    show_status("Shipment", &shipment);

Salve e saia do Nano. Execute o pacote concluído:

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

As duas linhas vêm de show_status<T: Status>. O bound descreve o comportamento de que a função precisa, enquanto cada implementação controla como seu próprio valor produz esse comportamento.

Resumo

Você definiu um comportamento compartilhado com uma trait, implementou-o de forma independente para duas estruturas e usou um trait bound básico para que um único gerador de relatórios genérico aceitasse os dois tipos.