Photo by Chandler Cruttenden on Unsplash
Aprendendo Rust: Option vs Null
Descubra como lidar de maneira segura com tipos nuláveis
É muito comum na programação que tenhamos o problema de controlar quando um valor pode existir ou pode não existir, se você requisita o primeiro item de uma lista não vazia receberá um valor, entretanto, se você pedir o primeiro item de uma lista vazia não receberá nada, isso implica que qualquer valor sempre terá dois estados nulo e não-nulo.
Este post é um resumo traduzido dos conceitos que você pode encontrar no livro, que é gratuito: The Rust Programming Language
Em Rust, uma característica da linguagem é não adotar o tipo null
, isso se faz por conta da baixa confiabilidade sobre a implementação do tipo null
, como diz o próprio criador do tipo na palestra “Null References The Billion Dollar Mistake”. Sendo assim como a linguagem gerência a falta ou a presença de um mesmo valor?
Option
Uma implementação particular, onde para cada valor nulo ou não nulo eu tenho a representação deste em Option
, que recebe um tipo T
qualquer, e pode retornar Some(T)
para representar a presença de um dado tipo T
ou None
para representar a falta do mesmo dado.
enum Option<T> {
None,
Some(T),
}
Por ser uma abstração que pertence a linguagem você não precisa fazer a chamada padrão para um enum, podendo invocar diretamente os tipos Some
ou None
.
O tipo Some
consegue segurar dados de qualquer tipo e retorna um Option<T>
, onde T
é o tipo passado ao enum.
O tipo None
sempre que for declarado precisa da referência de qual tipo seria sua representação não-nula, enquanto o mesmo não acontece para Some
pois o compilador pode inferir seu tipo pelo valor usado.
Para tornar o Option
uma implementação mais segura e eficiente que o null
, existe um tratamento de tipos, por exemplo…
O tipo Some
pode declarar um valor x
e um tipo T
entretanto caso eu queira somar uma variável y
com o mesmo tipo T
meu código não irá compilar pois Option<T>
não é igual a T
,
let x: Option<i8> = Some(5);
let y: i8 = 5;
let sum = x + y;
// Não compila pois tenta somar Option<i8> com i8
você precisará converter Option<T>
para T
antes de realizar qualquer operação possível para T
.
let x: Option<i8> = Some(5);
let y: i8 = 5;
let sum = x.unwrap() + y;
// Compila pois transformou optional<i8> em i8
Isso nos ajuda a manter longe um dos principais problemas com null
: Assumir que um valor não é nulo quando ele na verdade é.
Como extrair valores de um Option
na definição de Option pude explicar como o compilador entende que o valor é nulo ou não nulo e como isso se faz coerente em comparação a nível de código, entretanto não listamos as maneiras de extrair T
ou None
para usarmos.
Pattern matching
Quando se recebe uma Option qualquer a maneira mais intuitiva de saber quais das opções esta representação me entregou é fazendo um match
fn is_odd(number: i8) -> Option<i8> {
if number % 2 == 0 {
None
} else {
Some(number)
}
}
let number = 17;
let result = is_odd(number);
match result {
Some(x) => println!("{} is odd", x),
None => println!("isn't odd"),
}
No exemplo acima usamos a função is_odd
para definirmos nossa Option
onde caso o número não fosse impar o retorno seria None
ou como seria em outras linguagens null
.
Métodos de Options
Existem métodos implementados em Rust onde esperam extrair um valor caso exista Some
e também disparam tipos de erros diferentes baseado nos métodos, você pode acessar a documentação completa mas vou listar alguns aqui abaixo.
expect
gerapanic
com uma mensagem personalizada fornecida.unwrap
gerapanics
com uma mensagem genérica.unwrap_or
retorna o valor default fornecido.unwrap_or_default
retorna o valor default do tipoT
.unwrap_or_else
retorna o resultado da avaliação da função fornecida.
Esse é um tema bem complexo mas você pode encontrar mais referências nos links indexados no texto e também, caso se interesse, pode olhar ainda mais sobre o fundamento do tipo Option nesses links
Vejo vocês no próximo post pessoal 😁