Structs, enums, traits, generics
Os primitivos do capítulo 2 (i32, string, bool...) são um bom começo. Para modelar seus próprios dados — um usuário, uma requisição HTTP, um nó de árvore — você vai precisar de structs e enums. Para compartilhar comportamento entre tipos, use traits. Para escrever código uma vez que funcione em muitos tipos, generics. Este capítulo é o kit de ferramentas completo de modelagem.
Structs: pacotes de campos
Uma struct é um registro fixo de campos nomeados, cada um com seu próprio tipo.
struct User {
id: i64,
name: string,
admin: bool,
}
Construa um valor com a sintaxe de literal de struct — todos os campos precisam ser definidos:
let u: User = User {
id: 42,
name: "alice",
admin: false,
};
Leia os campos com .campo:
println!(u.name);
if u.admin { println!("is admin"); }
Métodos com impl
Quer uma função que receba um User? Você poderia escrever uma função avulsa — mas é mais elegante anexá-la diretamente ao tipo:
impl User {
fn display_name(self: User) -> string {
if self.admin {
return "@".concat(self.name).concat(" (admin)");
}
return "@".concat(self.name);
}
}
fn main() -> i32 {
let u: User = User { id: 1, name: "alice", admin: true };
println!(u.display_name()); // @alice (admin)
return 0;
}
Alguns detalhes:
impl User { ... }abre um bloco de métodos emUser.- O primeiro parâmetro é
self— o receptor. Seu tipo diz ao compilador se o método assume a posse (self: User), faz um empréstimo compartilhado (self: &User), ou faz um empréstimo mutável (self: &mut User). - No ponto de chamada,
u.display_name()parece um acesso a atributo, mas na verdade invoca o método, passandoucomoself.
Funções estáticas / associadas
Funções que pertencem a um tipo mas não recebem um receptor (sem self) — úteis para construtores:
impl User {
fn new(id: i64, name: string) -> User {
return User { id: id, name: name, admin: false };
}
}
let u: User = User::new(1, "alice");
Chame-as com Tipo::função(...). Por convenção, toda struct que aloca na heap tem um construtor Tipo::new(...).
Enums: um-entre-vários
Uma struct empacota todos os seus campos. Um enum diz "o valor é exatamente um destes variantes nomeados":
enum Shape {
Circle,
Square,
Triangle,
}
let s: Shape = Shape::Square;
Use match para fazer algo diferente para cada variante — e o compilador vai reclamar se você esquecer alguma:
fn sides(s: Shape) -> i32 {
return match s {
Shape::Circle => { return 0; },
Shape::Square => { return 4; },
Shape::Triangle => { return 3; },
};
}
Variantes com dados
O verdadeiro poder dos enums está em cada variante poder carregar dados diferentes:
enum Event {
Login(string), // um nome de usuário
Logout,
Message(string, string), // remetente, corpo
}
fn describe(e: Event) -> string {
return match e {
Event::Login(u) => { return "login: ".concat(u); },
Event::Logout => { return "logout"; },
Event::Message(f, b) => { return f.concat(": ").concat(b); },
};
}
Cada braço do match desestrutura o conteúdo do variante em bindings locais. Login(u) faz o binding de u ao nome de usuário quando esse braço é executado.
?T e !T são enums
Os tipos do capítulo 4 (?T e !T) são basicamente enums por baixo dos panos. ?T tem dois variantes: "tem valor" e "nenhum." !T tem dois: "ok" e "erro." Eles recebem sintaxe especial por serem tão comuns, mas o modelo mental é o mesmo de um enum normal.
Traits: comportamento compartilhado
Uma trait é um contrato: "qualquer tipo que tenha estes métodos pode ser usado aqui." Você a declara como uma classe sem implementação, e então diz quais tipos a satisfazem.
trait Greetable {
fn greet(self: &Self) -> string;
}
Self é o marcador de posição para "seja lá qual for o tipo que implementa esta trait." Quando User implementa Greetable, Self passa a ser User em todo lugar na trait.
impl Greetable for User {
fn greet(self: &User) -> string {
return "hello, ".concat(self.name);
}
}
Agora você pode chamar .greet() em qualquer User:
let u: User = User::new(1, "alice");
println!(u.greet()); // hello, alice
A mesma trait pode ser implementada para múltiplos tipos de forma independente:
struct Dog { name: string, }
impl Greetable for Dog {
fn greet(self: &Dog) -> string {
return "woof, ".concat(self.name);
}
}
Agora tanto User quanto Dog são Greetable.
Métodos padrão
Uma trait pode fornecer uma implementação padrão que os tipos herdam a menos que a sobrescrevam:
trait Greetable {
fn name(self: &Self) -> string;
fn greet(self: &Self) -> string {
return "hello, ".concat(self.name());
}
}
Agora qualquer tipo que implemente name() ganha greet() automaticamente, de graça.
Restrições de trait em funções
Uma vez que Greetable existe, você pode escrever uma função que funciona com qualquer tipo que a implemente:
fn say_hi<T: Greetable>(thing: &T) {
println!(thing.greet());
}
fn main() -> i32 {
let u: User = User::new(1, "alice");
let d: Dog = Dog { name: "rex" };
say_hi(&u); // hello, alice
say_hi(&d); // woof, rex
return 0;
}
Leia <T: Greetable> como "T é qualquer tipo, com a restrição de que ele implementa Greetable." O compilador gera uma versão especializada para cada tipo concreto em tempo de compilação — o mesmo mecanismo da monomorphization do Rust, ou dos templates do C++.
Despacho dinâmico com *dyn Trait
Às vezes você quer uma coleção em runtime com tipos mistos — digamos, um Vector de "qualquer Greetable, não importa qual." Para isso, use *dyn Trait. Você converte um ponteiro de heap (*User, *Dog) — não um empréstimo — para o tipo da trait:
let things: *Vector<*dyn Greetable> = Vector::new();
let u: *User = new User { id: 1, name: "alice", admin: false };
let d: *Dog = new Dog { name: "rex" };
things.push(u as *dyn Greetable);
things.push(d as *dyn Greetable);
for t in things {
println!(t.greet());
}
*dyn Greetable é um par de ponteiros por baixo dos panos (um para os dados, outro para a tabela de métodos do tipo). O custo: uma chamada de método passa por uma consulta em runtime. O ganho: uma única coleção pode conter tipos heterogêneos. (new T { ... } aloca um ponteiro de heap bruto — veja o modelo de memória — que é o que você armazena em uma coleção de longa duração.)
Generics: código que funciona em muitos tipos
Você já viu generics em Vector<T> e HashMap<V> — esses são pré-construídos. Você pode escrever os seus próprios:
struct Pair<A, B> {
first: A,
second: B,
}
impl<A, B> Pair<A, B> {
fn new(a: A, b: B) -> Pair<A, B> {
return Pair { first: a, second: b };
}
}
fn main() -> i32 {
let p: Pair<i32, string> = Pair::new(42, "answer");
println!(p.second);
return 0;
}
<A, B> são parâmetros de tipo — marcadores de posição que são preenchidos quando alguém usa a struct. O compilador gera uma versão especializada para cada combinação concreta (Pair<i32, string>, Pair<bool, f64>, etc.).
Restrições em generics
Frequentemente você vai querer restringir o parâmetro de tipo — "qualquer T, desde que implemente alguma trait." A restrição é o que permite chamar os métodos dessa trait dentro da função genérica:
trait Measured {
fn size(self: &Self) -> i32;
}
fn largest<T: Measured>(items: &*Vector<*T>) -> *T {
let mut best: *T = items.get(0);
for x in items {
if x.size() > best.size() { best = x; } // .size() é permitido pela restrição
}
return best;
}
<T: Measured> diz "T é qualquer coisa, desde que implemente Measured." Sem essa restrição o compilador não permitiria chamar x.size() — ele não tem como saber que o método existe. As restrições são o que torna o código genérico ao mesmo tempo flexível e type-safe.
Você pode exigir múltiplas traits com +:
trait Named { fn label(self: &Self) -> string; }
fn announce<T: Measured + Named>(item: *T) {
println!(item.label(), "has size", item.size()); // as duas restrições em jogo
}
Juntando tudo
Um exemplo prático — uma pequena biblioteca de "formas" que une todos os conceitos:
trait Area {
fn area(self: &Self) -> f64;
}
struct Circle { radius: f64, }
struct Square { side: f64, }
impl Area for Circle {
fn area(self: &Circle) -> f64 {
return 3.14159 * self.radius * self.radius;
}
}
impl Area for Square {
fn area(self: &Square) -> f64 {
return self.side * self.side;
}
}
fn total_area<T: Area>(shapes: &*Vector<*T>) -> f64 {
let mut sum: f64 = 0.0;
for s in shapes { sum = sum + s.area(); }
return sum;
}
fn main() -> i32 {
let circles: *Vector<*Circle> = Vector::new();
circles.push(&Circle { radius: 1.0 });
circles.push(&Circle { radius: 2.0 });
println!("circles total:", total_area(&circles));
return 0;
}
Seis conceitos: trait, struct, impl, for, função genérica com restrição de trait, genérica sobre um ponteiro de heap. Em menos de 30 linhas.
Para onde ir agora
Você já sabe modelar dados com structs e enums, compartilhar comportamento com traits e escrever funções reutilizáveis com generics. O próximo capítulo — Concorrência — mostra como fazer várias coisas acontecerem ao mesmo tempo.