Olá queridos leitores! Em meu artigo anterior, How to Switch from Java to Rust Easily, compartilhei com vocês dicas para mudar para o Rust e reduzir a quantidade de “sangue derramado” ao longo do caminho. Mas o que fazer, depois de já ter mudado para o Rust, para que seu código pelo menos compile e funcione? Hoje quero compartilhar com você algumas ideias sobre como escrever código idiomático em Rust, especialmente se você estiver acostumado com outras linguagens de programação.
É Preciso Pensar e Programar no Estilo das Expressões
Uma das principais características do Rust é a ênfase no uso de expressões. Isso significa que quase tudo em Rust é uma expressão que retorna um valor. É importante entender e usar isso em seu código.
Em outras linguagens de programação, costumamos usar declarações que realizam alguma ação, mas não retornam um valor. Em Rust, tentamos evitar declarações e escrever o código de forma que cada parte retorne algum valor.
Aqui está um exemplo de como você pode escrever código no estilo expressões:
let x = 10;
let y = if x > 5 {
100
} else {
50
};
Este código é mais idiomático que o código a seguir:
let x = 10;
let y;
if x > 5 {
y = 100;
} else {
y = 50;
}
O segundo código é menos idiomático porque utiliza operadores de atribuição para calcular dinamicamente o valor de uma variável.
Outro exemplo de código no estilo de expressões idiomáticas:
fn factorial(n: u32) -> u32 {
if n == 0 {
1
} else {
n * factorial(n - 1)
}
}
Este código usa a recursão para calcular o fatorial de um número. A recursão é outra forma de pensar e programar no estilo de Expressões.
É importante lembrar que quase tudo em Rust pode ser usado como uma expressão, o que torna o código mais expressivo e compacto.
Escreva Iteradores, Não Loops
Outra importante característica idiomática no Rust é o uso de iteradores em vez de loops explícitos. Isso torna o código mais limpo e funcional.
Em vez de escrever loops clássicos como for ou while, usamos métodos iteradores para processar coleções de dados.
Por exemplo, para iterar sobre um vetor, seria melhor escrever:
let v = vec![1, 2, 3];
for x in v {
println!("{}", x);
}
Melhor ainda, use métodos iteradores:
let v = vec![1, 2, 3];
v.iter().for_each(|x| println!("{}", x));
Outros métodos iteradores úteis são map, filter, fold, etc. Eles permitem que você escreva um código Rust mais idiomático e expressivo.
// Uso de um iterador para filtrar elementos em um vetor
let filtered_names: Vec<_> = names.iter().filter(|x| x.starts_with("A")).collect();
Construtores
Outro aspecto importante do código Rust idiomático é o uso de construtores. Construtores são funções que recebem valores de parâmetros e retornam um objeto com esses valores.
Os construtores são úteis para criar objetos complexos que possuem muitos parâmetros. Eles também ajudam a garantir que os tipos e valores dos parâmetros sejam consistentes.
Aqui está um exemplo de uso do construtor para criar um carro:
struct Car {
name: String,
hp: u32,
}
impl Car {
pub fn new(name: String) -> Self {
Car {
name,
hp: 100,
}
}
pub fn hp(mut self, hp: u32) -> Self {
self.hp = hp;
self
}
pub fn build(self) -> Car {
self
}
}
let car = Car::new("Model T").hp(20).build();
Os construtores permitem evitar a criação de objetos em um estado inválido. O método build() garante que Car seja totalmente inicializado.
Os construtores também permitem personalizar objetos de uma forma mais flexível. Só podemos chamar .hp() e .wheels() para criar um carro parcialmente inicializado.
Em geral, o Rust incentiva a resolução “correta” de problemas com expressões idiomáticas como estados impossíveis e construtores. Ao aprender essas expressões, comecei a escrever códigos Rust mais idiomáticos e seguros.
Panic
Um panic é um tipo especial de erro que pode fazer com que um processo seja encerrado imediatamente. No Rust, os panics servem para indicar erros que não podem ser corrigidos ou contornados.
Por exemplo, se você tentar acessar um campo não inicializado, o Rust entrará em panic. Isso ocorre porque o erro não pode ser corrigido sem alterar o programa.
Em outras linguagens de programação, como Python e Java, os erros geralmente são tratados com exceções. No entanto, o Rust não usa exceções por padrão.
Isso ocorre porque as exceções podem causar vazamentos de memória e outros problemas. Além disso, as exceções podem ser difíceis de entender e depurar.
Em vez de exceções, o Rust usa dois tipos de erros:
- Option — Retorna um valor do tipo T, se disponível, ou None (nenhum), se não estiver disponível.
- Result< T, E> — retorna um valor do tipo T ou um erro do tipo E.
Option e Result< T, E> são usados para tratar erros de maneira mais segura e transparente do que exceções.
Vejamos um exemplo:
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divisão por zero não é permitida!"); // Panic na divisão por zero
}
a / b
}
Neste exemplo, se b for zero, a função entrará em panic, indicando um erro de programação. Porém, se você conseguir antecipar situações de erro e quiser tratá-las sem encerrar o programa, é melhor usar Result:
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
if b == 0 {
Err("A divisão por zero não é permitida!") // Retornamos o erro como um Result
} else {
Ok(a / b)
}
}
Essa abordagem torna seu código mais previsível e seguro.
Genéricos
Os genéricos são uma forma de escrever código que pode funcionar com qualquer tipo de dados. O Rust usa genéricos para criar tipos como Option e Result< T, E>.
As generalizações podem ser uma ferramenta poderosa, mas devem ser usadas com cuidado. Genéricos complexos podem dificultar a compreensão e a depuração do código.
Aqui está um exemplo de uma generalização simples:
fn print_value<T>(value: T) {
println!("{}", value);
}
Esta função pode pegar qualquer valor do tipo T e registrá-lo no console.
Aqui está um exemplo de uma generalização complexa:
fn compare_values<T: PartialEq>(value1: T, value2: T) -> bool {
value1 == value2
}
Esta função compara dois valores do tipo T que implementam a interface PartialEq.
Se você não tiver certeza se deve usar uma generalização, é melhor evitá-la. Na maioria dos casos, você pode usar um tipo mais específico, o que tornará seu código mais claro e fácil de depurar.
Separação de implementações
Os genéricos permitem escrever um código que pode ser usado com diferentes tipos de dados. Essa é uma ferramenta muito poderosa, mas também pode ser mal utilizada.
Uma maneira de fazer mau uso dos genéricos é tentar escrever um código que funcione para todos os tipos de dados. Isso pode levar a um código difícil de manter e estender.
A melhor maneira de usar genéricos é separar as implementações em módulos distintos. Por exemplo, suponha que você tenha uma estrutura Point que pode conter coordenadas x e y de qualquer tipo. Você pode escrever uma implementação genérica de Point que conterá métodos genéricos como getX() e getY(). Você pode então escrever implementações separadas de Point para tipos de dados específicos, como Point e Point.
Aqui está um exemplo de código que demonstra como funciona:
// Implementação Geral de Point
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
pub fn get_x(&self) -> &T {
&self.x
}
pub fn get_y(&self) -> &T {
&self.y
}
}
// Implementação de Point para f32
impl Point<f32> {
pub fn distance_to(&self, other: &Point<f32>) -> f32 {
let dx = self.x - other.x;
let dy = self.y - other.y;
return (dx * dx + dy * dy).sqrt();
}
}
// Implementação de para i32
impl Point<i32> {
pub fn distance_to(&self, other: &Point<i32>) -> i32 {
let dx = self.x - other.x;
let dy = self.y - other.y;
return (dx * dx + dy * dy).sqrt();
}
}
Este código nos permite escrever um código que funcione tanto para coordenadas de ponto flutuante quanto para coordenadas de inteiros. Se quisermos adicionar uma nova implementação de Point para um tipo de dados diferente, só precisamos adicionar um novo módulo.
Evite unsafe {}
O Rust é conhecido por seu foco na segurança. Ele fornece recursos poderosos para trabalhar com memória de baixo nível, mas, às vezes, os iniciantes podem ficar tentados a usar blocos unsafe {} para contornar o sistema de tipos e a segurança. No entanto, muitas vezes existem alternativas mais seguras e rápidas que não requerem o uso de unsafe.
Vejamos um exemplo. Suponha que temos um vetor de números e queremos obter a soma de todos os elementos. Poderíamos fazer isso usando unsafe {}:
fn unsafe_sum(numbers: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..numbers.len() {
unsafe {
sum += *numbers.get_unchecked(i);
}
}
sum
}
Mas uma maneira mais segura e idiomática de fazer isso é:
fn safe_sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
Concluindo, mudar para Rust pode ser complicado, especialmente se você vier de linguagens imperativas como Java ou Python. Mas, se você dedicar um tempo para aprender as expressões e práticas recomendadas do Rust, logo começará a gostar de escrever códigos seguros e expressivos na linguagem.
Lembre-se que o Rust não é apenas uma ferramenta, mas toda uma filosofia de programação focada em confiabilidade e desempenho. Quanto mais você pensar e escrever códigos em um estilo funcional usando expressões, iteradores e genéricos, mais natural o Rust parecerá para você.
Espero que este artigo tenha ajudado você a entender algumas das principais expressões do Rust e como escrever código mais idiomático nesta linguagem maravilhosa. Boa sorte no aprendizado com Rust e com a construção de aplicativos confiáveis e seguros!
Esse artigo foi escrito por Evgeny Igumnov e traduzido por Isabela Curado Nehme. Seu original pode ser lido aqui.
Latest comments (0)