Capítulo 09: Structs, Tuplas e Métodos com impl

Especialização em Sistemas com Rust • Rust 2021 Edition • Ownership, Concorrência Segura e Sistemas de Alta Performance


🗺️ Mapa Conceitual da Modelagem Orientada a Dados com Structs e Métodos

flowchart TD
    Modelagem["Modelagem de Domínio em Rust"] --> Variedades["Variedades de Structs"]
    Modelagem --> Implementacao["Comportamento com Blocos impl"]

    subgraph STRUCT_KINDS["As 3 Variedades de Structs"]
        Classic["Classic Struct: Campos Nomeados (struct Servidor { ip: String })"]
        TupleStruct["Tuple Struct: Newtype Pattern (struct Porta(u16))"]
        UnitStruct["Unit Struct: Zero-Sized Type Marcador (struct ProtocoloTLS)"]
    end

    subgraph IMPL_METHODS["Métodos e Funções Associadas"]
        AssociatedFn["Função Associada (Construtor): fn new() -> Self"]
        ReadMethod["Método de Leitura: fn status(&self) -> &str"]
        MutMethod["Método de Escrita: fn atualizar(&mut self)"]
        ConsumeMethod["Método de Consumo: fn encerrar(self) (Drop automático)"]
    end

    Variedades --- STRUCT_KINDS
    Implementacao --- IMPL_METHODS

    style Modelagem fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Variedades fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style Implementacao fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style IMPL_METHODS fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Modelo Orientado a Dados em Rust (Sem Herança)

Diferente de linguagens clássicas orientadas a objetos (como Java, C# ou C++), o Rust não possui herança de classes. Em vez disso, o design de software em Rust adota a Composição Pura e a Separação Rígida entre Dados e Comportamento:

  • Dados: São modelados estritamente através de structs e enums.
  • Comportamento: É anexado aos dados através de blocos de implementação impl ou via contratos de interface chamados traits.

Essa abordagem elimina os conhecidos problemas de herança profunda (como o Fragile Base Class Problem e herança múltipla ambígua), resultando em código modular, previsível e com layout de memória otimizado.


⚙️ 2. As Três Formas de Structs em Rust

2.1 Classic Structs (Campos Nomeados)

A forma mais comum para modelar entidades complexas de negócio:

#[derive(Debug, Clone)]
struct ConexaoBanco {
    host: String,
    porta: u16,
    pool_maximo: u32,
    ativo: bool,
}

2.2 Tuple Structs e o Newtype Pattern

Tuple structs nomeiam uma tupla inteira sem dar nomes aos campos individuais. São muito utilizadas para criar tipos nominais fortes (Newtype Pattern), impedindo que parâmetros do mesmo tipo primitivo sejam passados na ordem errada:

struct LarguraPixels(pub u32);
struct AlturaPixels(pub u32);

// O compilador impede passar Largura no lugar de Altura, embora ambos sejam u32!
fn redimensionar_canvas(w: LarguraPixels, h: AlturaPixels) {
    println!("Canvas: {}x{} px", w.0, h.0);
}

2.3 Unit Structs (Zero-Sized Types - ZST)

Structs sem nenhum campo. Não ocupam nenhum byte na memória (size_of == 0), sendo utilizadas pelo compilador como marcadores de estado em tempo de compilação (State Pattern estático) e traits de sincronização.


🔨 3. O Bloco impl: Métodos e Funções Associadas

Dentro de um bloco impl, você define tanto funções associadas quanto métodos:

struct ServidorCluster {
    identificador: String,
    carga_cpu_pct: f32,
}

impl ServidorCluster {
    // 1. Função Associada (Construtor Idiomático 'new')
    // Não recebe 'self'. É invocada via 'ServidorCluster::new(...)'.
    fn new(id: &str) -> Self {
        Self {
            identificador: String::from(id),
            carga_cpu_pct: 0.0,
        }
    }

    // 2. Método de Leitura Imutável (&self)
    // Permite que qualquer chamador inspecione o estado sem mutação.
    fn esta_sobrecarregado(&self) -> bool {
        self.carga_cpu_pct > 80.0
    }

    // 3. Método de Escrita Mutável (&mut self)
    // Exige empréstimo mutável exclusivo da instância.
    fn atualizar_telemetria(&mut self, nova_carga: f32) {
        self.carga_cpu_pct = nova_carga;
    }

    // 4. Método que Assume Ownership (self por valor)
    // Consome e destrói a instância ao finalizar a operação.
    fn descomissionar(self) {
        println!("Descomissionando servidor: {}", self.identificador);
        // 'self' sai de escopo e sua memória é desalocada via Drop!
    }
}

🧩 4. Struct Update Syntax (..instancia)

Ao instanciar uma nova struct aproveitando a maioria dos campos de outra já existente, utiliza-se a sintaxe funcional ..:

let config_padrao = ConexaoBanco {
    host: String::from("localhost"),
    porta: 5432,
    pool_maximo: 10,
    ativo: true,
};

// Cria uma nova conexão sobrescrevendo apenas o host
let config_producao = ConexaoBanco {
    host: String::from("db.producao.cloud.internal"),
    ..config_padrao // Copia os demais campos (porta, pool_maximo, ativo)
};

⚖️ 5. Matriz Comparativa dos Tipos de self em Métodos

Assinatura Empréstimo Permite Mutação? Efeito sobre o Chamador
&self Empréstimo Imutável Não (Apenas leitura) A instância continua disponível e intacta
&mut self Empréstimo Mutável SIM (Mutação in-place) A instância continua disponível com novo estado
self Move (Posse Total) Sim (se mut self) A instância é consumida e destruída

⚠️ 6. Armadilhas Comuns e Mensagens do Compilador

  1. Esquecer &mut self ao Modificar Campos:
    fn registrar(&self) {
        self.carga_cpu_pct = 50.0; // ERRO: cannot assign to `self.carga_cpu_pct`, which is behind a `&` reference
    }
    

    Correção: Altere a assinatura para &mut self e garanta que a variável instanciada pelo chamador seja mutável (let mut s = ...).

  2. Tentar Usar a Instância após um Método que Recebe self: Se um método receber self por valor, a instância é movida para dentro do método. Tentar acessá-la novamente depois gerará o erro use of moved value.

🛡️ 7. Boas Práticas e Recomendações de Engenharia

  • Implemente Sempre Debug: Anote #[derive(Debug)] em suas structs para viabilizar logging e depuração formatada com {:?}.
  • Utilize o Newtype Pattern: Não use primitivos brutos para grandezas físicas ou identificadores sensíveis (como UserID(u64) e ProductID(u64)). O compilador garantirá que um UserID nunca seja acidentalmente passado para um endpoint que espera ProductID.

📋 8. Cheat-Sheet: Resumo Executivo

struct Ponto { x: i32, y: i32 }

impl Ponto {
    fn new(x: i32, y: i32) -> Self { Self { x, y } }
    fn distancia_origem(&self) -> f64 { ((self.x*self.x + self.y*self.y) as f64).sqrt() }
    fn mover(&mut self, dx: i32, dy: i32) { self.x += dx; self.y += dy; }
}