Mais um texto sobre Rust, e novamente comparando com outras linguagens pra ter uma melhor didática. Achei interessante trazer Golang pela semelhança com Rust e Java por propagar e tratar da mesma forma que várias linguagens populares.
Escrevi esse texto, porque tratamentos de erros é uma parte muito importante no comportamento de um software.
Golang é conhecida por sua simplicidade e eficiência. No que diz respeito à propagação de erros, Golang utiliza um modelo de retorno de valores múltiplos para indicar erros. Isso significa que uma função pode retornar tanto o resultado desejado quanto um valor de erro, que é um valor especial que representa um erro. Um exemplo simples de código em Golang que ilustra esse conceito:
package main
import (
"errors"
"fmt"
)
// Função que realiza uma operação e retorna um erro, se ocorrer algum.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("Divisão por zero não é permitida")
}
return a / b, nil
}
func main() {
// Chamada da função divide com tratamento de erro.
resultado, err := divide(10, 0)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado:", resultado)
}
// Chamada bem-sucedida.
resultado, err = divide(10, 2)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado:", resultado)
}
}
Neste exemplo, a função divide retorna dois valores: o resultado da divisão e um erro, que é nil se a operação for bem-sucedida ou contém uma mensagem de erro se a divisão por zero for detectada.
A propagação de erros em Golang é normalmente realizada por meio de verificações de erros locais, onde a função que chama verifica explicitamente o erro retornado e decide como lidar com ele. Isso significa que o controle sobre como os erros são tratados está nas mãos do desenvolvedor, o que pode levar a um código mais claro e fácil de entender.
Rust é conhecida por seu foco em segurança e prevenção de erros em tempo de execução. Em Rust, a propagação de erros é tratada por meio do sistema de tipos do Rust e do uso extensivo de enums (tipos enumerados). O exemplo a seguir demonstra como a propagação de erros é feita em Rust:
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
return Err("Divisão por zero não é permitida".to_string());
}
Ok(a / b)
}
fn main() {
// Chamada da função divide com tratamento de erro.
match divide(10.0, 0.0) {
Ok(resultado) => println!("Resultado: {}", resultado),
Err(erro) => println!("Erro: {}", erro),
}
// Chamada bem-sucedida.
match divide(10.0, 2.0) {
Ok(resultado) => println!("Resultado: {}", resultado),
Err(erro) => println!("Erro: {}", erro),
}
}
Em Rust o código fica menor e aqui, a função divide retorna um Result, que pode ser Ok com o valor resultante ou Err com uma mensagem de erro. A verificação de erros em Rust é fortemente incentivada por meio do uso do operador match, que permite ao desenvolvedor lidar de maneira abrangente com os resultados, como mostra nesse trecho:
match divide(10.0, 2.0) {
Ok(resultado) => println!("Resultado: {}", resultado),
Err(erro) => println!("Erro: {}", erro),
}
Dessa forma, Rust impõe uma forte verificação de erros em tempo de compilação, garantindo que os desenvolvedores considerem todas as possíveis situações de erro.
E por fim, a propagação e tratamentos de erros em Java são feitos por meio do uso de exceções. Resolvi trazer um exemplo em Java, porque é bem próximo como é utilizado em outras linguagens mais comuns, como Javascript, Python, Ruby e etc.
É uma forma de mostrar como o tratamento em Rust e Golang se diferente das formas tradicionais por uso de exceção:
public class ExemploDivisao {
// Método que realiza uma operação de divisão e lança uma exceção em caso de erro.
public static double divide(double a, double b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Divisão por zero não é permitida");
}
return a / b;
}
public static void main(String[] args) {
try {
// Chamada do método divide com tratamento de exceção.
double resultado = divide(10.0, 0.0);
System.out.println("Resultado: " + resultado);
} catch (ArithmeticException e) {
System.out.println("Erro: " + e.getMessage());
}
try {
// Chamada bem-sucedida.
double resultado = divide(10.0, 2.0);
System.out.println("Resultado: " + resultado);
} catch (ArithmeticException e) {
System.out.println("Erro: " + e.getMessage());
}
}
}
Neste exemplo em Java, a função divide recebe dois números como entrada e lança uma exceção ArithmeticException caso a divisão por zero seja detectada.
No método main(), a função divide é chamada duas vezes: uma vez com uma divisão por zero intencional e outra vez com uma divisão válida.
O tratamento de exceções é feito por meio do uso de blocos try-catch, onde a exceção ArithmeticException é capturada e a mensagem de erro é impressa caso ocorra um erro. Caso contrário, o resultado da operação é impresso.
err. As verificações de erro geralmente envolvem a verificação se err é igual a nil, o que indica que não ocorreu erro.Result, que pode conter um valor bem-sucedido (Ok) ou um valor de erro (Err). As funções que podem gerar erros retornam Result.match, que permite aos desenvolvedores fazer correspondência de padrões nos valores Result para lidar com os casos de sucesso e erro de forma explícita.throw.try-catch. O código que pode gerar uma exceção é colocado dentro de um bloco try, e as exceções são capturadas e tratadas em blocos catch.Em resumo, Go utiliza valores de erro explícitos e a verificação de erros locais, Rust usa o tipo de retorno Result e requer o tratamento explícito usando match. Java, por outro lado, emprega exceções e blocos try-catch para lidar com erros.