Ponteiros inteligentes em Rust

(smart pointers)

(parte 1)

Clube do Livro #15

O que são ponteiros?

  • Um ponteiro é uma variável que contém um endereço de memória onde um dado está. Ele aponta para o dado.
  • Em rust, o ponteiro mais comum é a referência (&) (Capítulo 4)

Clube do Livro #15

O que são ponteiros inteligentes?

  • Smart Pointers não somente apontam, mas podem ter:
    • metadados
    • comportamento customizado
    • na maioria dos casos, são os donos do dado (especificamente em Rust)
  • Exemplo: String
    • é dona do dado
    • metadados: tamanho, capacidade
    • comportamento: permite apenas UTF-8 válido
  • Smart Pointers geralmente são implementados usando structs
    • Quase sempre implementam as traits Deref e Drop

Clube do Livro #15

O que são ponteiros inteligentes?

  • Smart pointers são um design pattern em Rust
  • A biblioteca-padrão provê vários tipos de ponteiros inteligentes, os mais comuns são:
    • Box<T>, para alocar valores na heap
    • Rc<T>, um contador de referências que permite múltiplos donos de um mesmo valor
    • Ref<T> e RefMut<T>, acessados por RefCell<T>, para utilizar as regras do borrow checker em tempo de execução ao invés de tempo de compilação, para casos especiais

Clube do Livro #15

O smart pointer Box<T>

  • Box permite guardar valores na heap
  • O que fica na stack é apenas um ponteiro para os dados na heap
  • Situações de uso:
    • Um tipo cujo tamanho não pode ser conhecido no momento da compilação e deseja usar um valor desse tipo em um contexto que requer um tamanho exato
    • Uma grande quantidade de dados e deseja transferir a propriedade, sem ter que copiá-los

    • Quando você deseja alterar a propriedade um valor e se preocupa apenas se ele implementa um trait específica ao invés de ser de um tipo específico

Clube do Livro #15

Clube do Livro #15

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

Um caso recursivo para Box<T>

  • Uma estrutura recursiva refere-se a si mesma
  • Rust precisa saber, em tempo de compilação, quanto um tipo ocupará na memória
  • Um tipo recursivo não permite saber o seu tamanho, pois a sua própria definição é recursiva, o que confunde o compilador
  • Como Box<T> tem um tamanho fixo, envolveremos os nossos dados nele, que os salvará na heap, e, ao mesmo tempo, terá um tamanho fixo, o que agradará o compilador
  • Vamos explicar isso com um exemplo: cons list

Clube do Livro #15

Cons list

  • Estrutura de dados comum em linguagens funcionais, como o LISP
  • Estrutura consite num par de valores: um guarda o valor atual e o outro aponta para o próximo valor. Estes pares formam uma lista. O último item contém apenas um valor, NIL.

Clube do Livro #15

Clube do Livro #15

fn main() {
    let list = Cons(42, Cons(69, Cons(613, Nil)));
}

Clube do Livro #15

Resolvendo o problema

  • Já que Box<T> tem um tamanho fixo, podemos usar ela para resolver esse problema e implementar a nossa estrutura recursiva.
  • O tamanho de um ponteiro não está definido em função do tamanho dos dados para os quais ele aponta
  • Isso significa que podemos colocar um Box <T> dentro da variante Cons em vez de outro valor List diretamente. O Box <T> apontará para o próximo valor de List que estará na heap, e não dentro da variante Cons.

  • Dessa forma o compilador determinará que a variante Cons precisará do tamanho de um i32 mais o espaço para armazenar os dados do ponteiro Box<List>.

Clube do Livro #15

Um sp e a trait Deref

  • Vamos implementar um sp
  • O nosso ponteiro irá implementar as traits Deref e Drop
  • De-refereciamento: pegar o valor que um ponteiro aponta
  • Para usar o "de-referenciamento" com nosso sp temos que implementar a trait Deref.

Clube do Livro #15

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

Clube do Livro #15

Coerção de de-referências

  • Funcionalidade de conveniência de Rust que opera sobre argumentos para funções e métodos ao converter um tipo de uma referência para outro tipo, aceito pela função
  • A coerção acontece automaticamente quando passamos uma referência para um tipo particular como argumento numa função ou método, sendo que o esse tipo não bate com a definição do argumento da função ou método.

  • Rust executa uma sequência de chamadas para o método deref para converter o tipo passado para o tipo que a função ou método precisa.

Clube do Livro #15

Exemplo de coereção

  • Estamos tentando chamar hello com o nosso tipo, mas hello espera um tipo &str
  • Como implementamos deref no nosso tipo, o compilador chamará essa função, que fará a conversão para String
  • String, por sua vez, possui uma implementação de Deref para converter seu valor para um slice de string (&str), Rust irá chamar o método deref dessa implementação
  • Nossa função receberá o slice de string necessário
  • Se Rust não tivesse coerção, teríamos que escrever:
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

Clube do Livro #15

Limpeza com a trait Drop

  • Trait drop: permite definir o que acontece quando um valor está para sair de escopo - permite definir a nossa própria limpeza de recursos automática
impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        ...
    }
}

Clube do Livro #15

Limpeza prematura com ​drop()

  • Existem situações em que precisamos liberar um recurso antes da liberação automática feita pelo Rust
  • Neste caso usamos std::mem::drop, que está no prelúdio, portanto apenas drop():
fn main() {
    let c = CustomSmartPointer {
        data: String::from("dados"),
    };
    println!("CustomSmartPointer criado.");
    drop(c);
    println!("CustomSmartPointer liberado antes.");
}

Obrigado, pessoal!

@felubra

twitter, github, telegram:

Referências

Clube do Livro #15

Made with Slides.com