Nesta aula, vamos aprofundar nosso conhecimento sobre lifetimes em Rust, um dos conceitos mais importantes para garantir a segurança de memória sem a necessidade de um coletor de lixo. Você já aprendeu o básico sobre lifetimes, como a elisão (elision) e a sintaxe de anotação. Agora, vamos explorar cenários mais avançados onde o controle fino dos lifetimes se torna essencial: structs que contêm referências, o lifetime estático 'static, e como usar lifetimes como parâmetros de tipo para impor restrições.

Dominar esses tópicos é fundamental para escrever código Rust idiomático e seguro, especialmente em bibliotecas e sistemas complexos. Vamos começar entendendo como os lifetimes se manifestam em structs, onde a anotação é muitas vezes obrigatória.

Lifetimes em structs

Em Rust, uma struct pode conter referências, mas para isso, cada referência precisa ter um lifetime explícito. Isso ocorre porque o compilador precisa saber por quanto tempo a referência é válida para garantir que a struct não sobreviva aos dados referenciados. A sintaxe é semelhante à de funções, mas a anotação é feita na definição da struct.

Por exemplo, considere uma struct que armazena uma referência a uma string:

struct Exemplo<'a> {
    texto: &'a str,
}

fn main() {
    let frase = String::from("Olá");
    let exemplo = Exemplo { texto: &frase };
    println!("{}", exemplo.texto);
}

O lifetime 'a é um parâmetro genérico que indica que a referência texto é válida enquanto o lifetime 'a durar. Isso permite que o compilador verifique as relações de validade entre a struct e os dados referenciados.

Quando você implementa métodos para uma struct com referências, também precisa anotar os lifetimes nos métodos. Por exemplo:

impl<'a> Exemplo<'a> {
    fn get_texto(&self) -> &'a str {
        self.texto
    }
}

fn main() {
    let frase = String::from("Mundo");
    let exemplo = Exemplo { texto: &frase };
    println!("{}", exemplo.get_texto());
}

Observe que o método get_texto retorna uma referência com o mesmo lifetime 'a da struct. Isso garante que a referência retornada não pode ser usada após a struct ser invalidada.

'static

O lifetime especial 'static indica que a referência é válida por toda a execução do programa. Isso é comum para literais de string, que são embutidos no binário e vivem para sempre. Por exemplo:

let saudacao: &'static str = "Olá, mundo!";

fn main() {
    println!("{}", saudacao);
}

O tipo &'static str é frequentemente usado em constantes e em dados que nunca devem ser liberados. Mas cuidado: nem toda referência pode ser 'static. Se você tentar retornar uma referência a um dado local como 'static, o compilador rejeitará, pois o dado não vive o suficiente.

Um caso comum é usar 'static em threads, onde a closure precisa ter lifetime 'static para ser movida para outra thread. Por exemplo:

use std::thread;

fn main() {
    let handle = thread::spawn(|| {
        println!("Thread executando");
    });
    handle.join().unwrap();
}

Nesse caso, a closure não captura nenhuma referência, então ela é 'static automaticamente. Se precisar capturar uma referência, você terá que garantir que ela seja 'static ou usar Arc e Mutex.

Lifetime bounds

Assim como podemos usar traits como bounds em genéricos, também podemos usar lifetimes como bounds. Isso é útil para restringir que um lifetime seja pelo menos tão longo quanto outro. A sintaxe é 'a: 'b, que significa "'a vive pelo menos tanto quanto 'b".

Por exemplo, em uma função que recebe duas referências, podemos garantir que a referência retornada tenha o lifetime mais curto:

fn menor<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'b str {
    if x.len() < y.len() { x } else { y }
}

fn main() {
    let a = String::from("abc");
    let b = String::from("defgh");
    let resultado = menor(&a, &b);
    println!("{}", resultado);
}

Aqui, a assinatura 'a: 'b garante que 'a é pelo menos tão longo quanto 'b, permitindo retornar uma referência com lifetime 'b que pode ser uma das duas entradas.

Lifetime bounds também são usados em structs e traits para impor relações entre diferentes lifetimes. Por exemplo:

struct Par<'a, 'b: 'a> {
    primeiro: &'a str,
    segundo: &'b str,
}

fn main() {
    let a = String::from("primeiro");
    let b = String::from("segundo");
    let par = Par { primeiro: &a, segundo: &b };
    println!("{} {}", par.primeiro, par.segundo);
}

Aqui, 'b: 'a garante que 'b vive pelo menos tanto quanto 'a, o que é necessário para que a struct seja válida.

Casos complexos

Em cenários mais complexos, podemos ter múltiplos lifetimes, lifetimes em closures, e interações com trait objects. Vamos explorar alguns casos:

Múltiplos lifetimes: Uma função pode ter vários parâmetros de lifetime independentes. Por exemplo:

fn escolher<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}

fn main() {
    let a = String::from("a");
    let b = String::from("b");
    let escolhido = escolher(&a, &b);
    println!("{}", escolhido);
}

Aqui, a função sempre retorna a referência x com lifetime 'a, independente do lifetime de y.

Lifetimes em closures: Closures podem capturar referências, e seus lifetimes são inferidos. Por exemplo:

fn main() {
    let x = String::from("olá");
    let closure = || println!("{}", x);
    closure();
}

Nesse caso, a closure captura a referência a x por borrow, e o lifetime é inferido automaticamente.

Trait objects com lifetimes: Quando você usa trait objects, como Box<dyn Trait>, pode ser necessário especificar lifetimes. Por exemplo:

trait Imprimivel {
    fn imprimir(&self);
}

struct Texto<'a> {
    conteudo: &'a str,
}

impl<'a> Imprimivel for Texto<'a> {
    fn imprimir(&self) {
        println!("{}", self.conteudo);
    }
}

fn main() {
    let texto = Texto { conteudo: "Olá" };
    let objeto: Box<dyn Imprimivel> = Box::new(texto);
    objeto.imprimir();
}

Nesse caso, o trait object não precisa de anotação de lifetime explícita, pois o compilador infere que ele pode viver por qualquer lifetime necessário.

Em resumo, os lifetimes avançados permitem expressar relações complexas de validade de referências, garantindo segurança sem sacrificar performance.

Boas práticas

  • Prefira usar elisão de lifetimes quando possível, para simplificar o código.
  • Evite usar 'static desnecessariamente; ele pode impedir o uso de dados não estáticos.
  • Em structs, considere usar String em vez de &str se você precisar de ownership, para evitar lifetimes.
  • Use lifetime bounds para documentar relações entre parâmetros de lifetime.

Exercícios

  1. Crie uma struct Livro que contém uma referência a uma string (título) e implemente um método titulo que retorna a referência. Use lifetimes adequados.
  2. ✓ Resposta:
    struct Livro<'a> {
        titulo: &'a str,
    }
    
    impl<'a> Livro<'a> {
        fn titulo(&self) -> &'a str {
            self.titulo
        }
    }
    
    fn main() {
        let titulo = String::from("O Senhor dos Anéis");
        let livro = Livro { titulo: &titulo };
        println!("{}", livro.titulo());
    }
  3. Escreva uma função que recebe duas referências a strings e retorna a mais longa, usando lifetime bounds para garantir que a referência retornada viva pelo menos tanto quanto a mais curta.
  4. ✓ Resposta:
    fn mais_longa<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'b str {
        if x.len() > y.len() { x } else { y }
    }
    
    fn main() {
        let a = String::from("curto");
        let b = String::from("uma string mais longa");
        let resultado = mais_longa(&a, &b);
        println!("{}", resultado);
    }
  5. Explique por que o código abaixo não compila e como corrigi-lo usando lifetimes ou ownership.
  6. fn criar_referencia() -> &'static str {
        let s = String::from("Olá");
        &s
    }

    ✓ Resposta: O código não compila porque a função tenta retornar uma referência a uma variável local s que será destruída quando a função terminar. Para corrigir, você pode retornar um literal de string (&'static str), ou usar Box::leak para criar uma referência estática, ou mudar o retorno para String.
    fn criar_referencia() -> &'static str {
        "Olá" // literal de string é 'static
    }
    
    // Ou, se precisar de String:
    fn criar_string() -> String {
        String::from("Olá")
    }
  7. Implemente uma struct Par que contém duas referências a strings, e um método primeiro que retorna a primeira referência. Use lifetimes distintos e um lifetime bound para garantir que o primeiro viva pelo menos tanto quanto o segundo.
  8. ✓ Resposta:
    struct Par<'a, 'b: 'a> {
        primeiro: &'a str,
        segundo: &'b str,
    }
    
    impl<'a, 'b: 'a> Par<'a, 'b> {
        fn primeiro(&self) -> &'a str {
            self.primeiro
        }
    }
    
    fn main() {
        let a = String::from("primeiro");
        let b = String::from("segundo");
        let par = Par { primeiro: &a, segundo: &b };
        println!("{}", par.primeiro());
    }
  9. Crie uma função que recebe um vetor de referências a strings e retorna a primeira referência que contém a letra 'a'. Use lifetimes para garantir que a referência retornada seja válida.
  10. ✓ Resposta:
    fn primeira_com_a<'a>(itens: &[&'a str]) -> Option<&'a str> {
        for item in itens {
            if item.contains('a') {
                return Some(item);
            }
        }
        None
    }
    
    fn main() {
        let palavras = vec!["casa", "carro", "moto"];
        if let Some(resultado) = primeira_com_a(&palavras) {
            println!("{}", resultado);
        }
    }

Referências