Adaptado de: https://github.com/fonluc/codigo-limpo-golang
Este trabalho tem como objetivo estabelecer o primeiro guia de clean code em Rust, oferecendo uma base sólida para boas práticas na linguagem. No entanto, como todo trabalho inicial, pode conter erros de código e de gramática. Portanto, é fundamental que outros desenvolvedores se envolvam e contribuam para aprimorar este repositório. Isso não só enriquecerá o conteúdo, mas também refletirá a qualidade e o compromisso dos desenvolvedores brasileiros, sejam eles especialistas em backend, Golang, Rust, web3 ou blockchain. A colaboração de todos é essencial para elevar o padrão do desenvolvimento de software e promover a excelência na nossa comunidade.
Este documento é uma referência para a comunidade Rust que visa ajudar desenvolvedores a escreverem código mais limpo. Seja trabalhando em um projeto pessoal ou como parte de uma equipe maior, escrever código limpo é uma habilidade importante. Estabelecer bons paradigmas e padrões consistentes e acessíveis para escrever código limpo pode ajudar a prevenir que desenvolvedores percam horas tentando entender seu próprio trabalho (ou o de outros).
"Nós não lemos código, nós o decodificamos." – Peter Seibel
Como desenvolvedores, às vezes somos tentados a escrever código de uma maneira conveniente no momento, sem considerar as melhores práticas; isso torna as revisões de código e testes mais difíceis. De certa forma, estamos "codificando" - e, ao fazer isso, tornando mais difícil para outros decodificarem nosso trabalho. Mas queremos que nosso código seja utilizável, legível e manutenível. E isso requer codificar da maneira correta, não da maneira fácil.
Este documento começa com uma introdução simples e curta aos fundamentos da escrita de código limpo. Mais tarde, discutiremos exemplos concretos de refatoração específicos para Rust.
- Código Rust Limpo
- Prefácio: Por que Escrever Código Limpo?
- Sumário
- Introdução ao Código Limpo
- Desenvolvimento Orientado a Testes
- Convenções de Nomenclatura
- Comentários
- Nomeação de Funções em Rust
- Nomeação de Variáveis em Rust
- Limpeza de Funções em Rust
- Assinaturas de Função em Rust
- Escopo de Variáveis em Rust
- Declaração de Variáveis em Rust
- Rust Limpo
- Retornando Erros Dinâmicos
- Valores
None
- Ponteiros em Rust
- Mutabilidade e Ponteiros
- Evitando Efeitos Colaterais Indesejados
- Fechamentos em Rust
- Interfaces em Go x Traits em Rust
- Interfaces Vazias em Go vs. Tipos Dinâmicos e Genéricos em Rust
- Prefácio: Por que Escrever Código Limpo?
Código limpo é o conceito pragmático de promover software legível e manutenível. Código limpo estabelece confiança na base de código e ajuda a minimizar as chances de bugs descuidados serem introduzidos. Também ajuda os desenvolvedores a manter sua agilidade, que normalmente despenca à medida que a base de código se expande devido ao aumento do risco de introduzir bugs.
O desenvolvimento orientado a testes é a prática de testar seu código frequentemente ao longo de ciclos de desenvolvimento curtos ou sprints. Isso contribui para a limpeza do código ao convidar os desenvolvedores a questionar a funcionalidade e o propósito de seu código. Para facilitar os testes, os desenvolvedores são incentivados a escrever funções curtas que fazem apenas uma coisa. Por exemplo, é consideravelmente mais fácil testar (e entender) uma função de 4 linhas do que uma de 40.
O desenvolvimento orientado a testes consiste no seguinte ciclo:
- Escrever (ou executar) um teste
- Se o teste falhar, fazer com que ele passe
- Refatorar seu código conforme necessário
- Repetir
Testar e refatorar estão entrelaçados nesse processo. À medida que você refatora seu código para torná-lo mais compreensível ou manutenível, é necessário testar suas mudanças minuciosamente para garantir que você não alterou o comportamento de suas funções. Isso pode ser extremamente útil à medida que a base de código cresce.
Comentários são uma prática essencial na programação, mas frequentemente mal aplicada. Comentários desnecessários podem indicar problemas no código subjacente, como convenções de nomenclatura ruins. A necessidade de um comentário específico é subjetiva e depende da legibilidade do código. Mesmo um código bem escrito pode ter lógica complexa que requer um comentário explicativo.
Em Rust, a ferramenta rustfmt
ajuda a manter um estilo consistente, mas não dita regras específicas para documentação como o gofmt
faz para Go. No entanto, Rust tem uma forte cultura de documentação através de comentários de documentação (doc comments).
É importante distinguir entre comentários de documentação (que começam com ///
ou //!
em Rust) e outros tipos de comentários. Comentários de documentação devem ser escritos em um alto nível de abstração, focando mais na interface pública e menos nos detalhes de implementação.
Outras formas de explicar o código incluem escrever de maneira clara e expressiva. Código confuso não deve ser "consertado" com comentários explicativos, pois isso não resolve o problema fundamental. A maioria dos desenvolvedores tende a ignorar comentários extensos, e revisar código pouco claro cheio de comentários pode ser frustrante.
Vejamos um exemplo de como não comentar seu código em Rust:
// Iterar sobre o intervalo de 0 a 9
// e invocar a função `do_something`
// para cada iteração
for i in 0..10 {
do_something(i);
}
Este é um "comentário tutorial", útil para iniciantes, mas desnecessário em código de produção. Como programadores experientes, devemos entender estruturas básicas como loops sem necessidade de explicação.
Seguindo o princípio "Documente o porquê, não o como", podemos melhorar:
// Instanciar 10 threads para lidar com a carga de trabalho futura
for i in 0..10 {
do_something(i);
}
Isso explica o propósito, mas ainda não é ideal. Podemos expressar essa intenção diretamente no código:
for worker_id in 0..10 {
spawn_thread(worker_id);
}
Com nomes mais significativos, explicamos a intenção diretamente no código, tornando-o mais claro e eliminando a necessidade do comentário.
Em Rust, podemos aproveitar as características da linguagem para tornar o código ainda mais expressivo:
let num_workers = 10;
(0..num_workers).for_each(|worker_id| {
spawn_thread(worker_id);
});
Escrever código claro e expressivo torna-se mais desafiador à medida que a complexidade aumenta. Praticar essa mentalidade de evitar explicar "o que" o código faz e focar em "por que" ele é necessário resultará em um código mais limpo e manutenível.
Em Rust, use comentários de documentação (///
) para documentar funções e módulos públicos, aproveitando o sistema de documentação integrado da linguagem. Reserve comentários regulares (//
) para explicações cruciais que não podem ser expressas diretamente no código.
A regra geral para nomear funções em Rust é similar: quanto mais específica a função, mais geral deve ser seu nome. Começamos com nomes de funções amplos e curtos, como run
ou parse
, que descrevem a funcionalidade geral. Imaginemos que estamos criando um analisador de configuração em Rust:
fn main() {
let config_path = std::env::args().nth(1).expect("Informe o caminho do arquivo de configuração");
let config = configuration::parse(&config_path).expect("Falha ao analisar a configuração");
// ...
}
Focando na função parse
, vemos que, apesar de curto e geral, o nome é claro quanto ao seu propósito.
Um nível mais profundo, a nomeação se torna um pouco mais específica:
fn parse(filepath: &str) -> Result<Config, Box<dyn Error>> {
match file_extension(filepath) {
"json" => parse_json(filepath),
"yaml" => parse_yaml(filepath),
"toml" => parse_toml(filepath),
_ => Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Extensão de arquivo desconhecida")))
}
}
As funções aninhadas são claramente distinguidas de seu pai, permitindo que cada uma faça sentido por si só e no contexto do pai.
A função file_extension
é um pouco mais específica devido à sua natureza:
fn file_extension(filepath: &str) -> &str {
filepath.rsplit('.').next().unwrap_or("")
}
Em Rust, assim como em Go, as variáveis devem ser nomeadas de forma mais específica à medida que nos aprofundamos em escopos aninhados. Em escopos menores, nomes mais curtos são aceitáveis:
fn print_brands_in_list(brands: &[BeerBrand]) {
for b in brands {
println!("{}", b);
}
}
Em funções com escopo maior, a distinção se torna mais evidente:
fn beer_brand_list_to_beer_list(beer_brands: &[BeerBrand]) -> Vec<Beer> {
let mut beer_list = Vec::new();
for brand in beer_brands {
for beer in brand {
beer_list.push(beer.clone());
}
}
beer_list
}
Em Rust, assim como em Go, mantemos nossas funções curtas para melhorar a compreensão do código. Vejamos um exemplo:
use std::error::Error;
#[derive(Default)]
struct Item;
fn get_item(ctx: &Context, json: &[u8]) -> Result<Item, Box<dyn Error>> {
let order = Item::from_json(json)?;
if !get_user_from_context(ctx).is_admin() {
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "Usuário não tem privilégios suficientes")));
}
db::get_item(order.item_id())
}
Evitamos o "inferno da indentação" em Rust usando o operador ?
para propagação de erros e retornando erros cedo. Para refatorar funções complexas, podemos dividi-las em funções menores:
fn get_item(extension: &str) -> Result<Item, Box<dyn Error>> {
let reference = get_reference(extension)?;
get_item_by_reference(&reference)
}
fn get_reference(extension: &str) -> Result<String, Box<dyn Error>> {
db::reference_cache::get(extension)
.ok_or_else(|| Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "Referência não encontrada no cache")))
}
fn get_item_by_reference(reference: &str) -> Result<Item, Box<dyn Error>> {
let item = get_item_from_cache(reference)?;
if !item.is_active() {
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "Item não está ativo")));
}
Ok(item)
}
fn get_item_from_cache(reference: &str) -> Result<Item, Box<dyn Error>> {
db::item_cache::get(reference)
.ok_or_else(|| Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "Item não encontrado no cache")))
}
Criar uma boa estrutura de nomeação de função torna mais fácil ler e entender a intenção do código. Assim como em Go, ter funções com poucos parâmetros de entrada melhora a clareza. Em Rust, uma abordagem recomendada para funções que possuem muitos parâmetros é usar estruturas para agrupar esses parâmetros.
Vamos imaginar uma função create_queue
que precisa de muitos parâmetros:
fn create_queue(name: &str, durable: bool, delete_on_exit: bool, exclusive: bool, no_wait: bool, arguments: Option<&[(&str, &str)]>) -> Result<(), Box<dyn std::error::Error>> {
// Implementação da função
Ok(())
}
Aqui, a função create_queue
possui muitos parâmetros. Em vez disso, podemos usar uma estrutura para representar as opções:
struct QueueOptions<'a> {
name: &'a str,
durable: bool,
delete_on_exit: bool,
exclusive: bool,
no_wait: bool,
arguments: Option<&'a [(&'a str, &'a str)]>,
}
fn create_queue(options: QueueOptions) -> Result<(), Box<dyn std::error::Error>> {
// Implementação da função
Ok(())
}
Essa abordagem melhora a legibilidade e reduz a possibilidade de erros, pois os parâmetros são claramente nomeados e organizados. Podemos até fornecer valores padrão para algumas dessas opções se necessário:
impl Default for QueueOptions<'_> {
fn default() -> Self {
QueueOptions {
name: "default",
durable: false,
delete_on_exit: false,
exclusive: false,
no_wait: false,
arguments: None,
}
}
}
Escrever funções menores ajuda a evitar problemas com variáveis mutáveis e o escopo global. Em Rust, como em Go, variáveis globais e de escopo amplo podem levar a confusões e erros difíceis de depurar.
Considere o exemplo a seguir, que usa uma variável com escopo maior:
fn do_complex() -> Result<String, Box<dyn std::error::Error>> {
Ok("Success".to_string())
}
fn main() {
let mut val = String::new();
let num = 32;
match num {
16 => {},
32 => {
let result = do_complex()?;
val = result;
},
64 => {},
}
println!("{}", val);
}
Neste exemplo, val
é modificado dentro do match
, o que pode levar a problemas de compreensão e manutenção. Uma refatoração para limitar o escopo de val
pode ser feita assim:
fn get_string_result(num: i32) -> Result<String, Box<dyn std::error::Error>> {
match num {
16 => Ok(String::new()),
32 => do_complex(),
64 => Ok(String::new()),
_ => Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid number"))),
}
}
fn main() {
let val = get_string_result(32)?;
println!("{}", val);
}
Neste caso, val
é retornado pela função get_string_result
, e o escopo de val
é reduzido.
Declarar variáveis o mais próximo possível de seu uso melhora a legibilidade do código. Em Rust, você pode usar o mesmo conceito de declarar variáveis imediatamente antes de usá-las.
Vamos considerar o seguinte exemplo:
fn main() {
let items = get_items();
let sender = std::sync::mpsc::channel();
let receiver = std::sync::mpsc::channel();
for item in items {
// ...
}
}
Podemos melhorar a clareza criando funções auxiliares para encapsular a lógica relacionada às variáveis:
fn create_sender() -> std::sync::mpsc::Sender<Item> {
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for item in receiver {
// Processo item
}
});
sender
}
fn main() {
let sender = create_sender();
// Use o sender
}
Ao encapsular a criação do canal em uma função, a declaração e uso da variável sender
ficam mais claros e isolados.
Além disso, você pode criar structs para encapsular variáveis e fornecer um nível adicional de encapsulamento e segurança:
struct Sender {
sender: std::sync::mpsc::Sender<Item>,
}
impl Sender {
fn new() -> Self {
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for item in receiver {
// Processo item
}
});
Sender { sender }
}
fn send(&self, item: Item) {
self.sender.send(item).expect("Falha ao enviar item");
}
}
fn main() {
let sender = Sender::new();
// Use o sender
}
Neste exemplo, a variável sender
está encapsulada em uma struct Sender
, e seu uso é controlado por métodos da struct.
Retornando Erros Definidos
Em Rust, a abordagem para retornar erros é um pouco diferente da de Go, mas segue um princípio similar de manter a legibilidade, testabilidade e manutenção do código. Vamos ver como adaptar o conceito de retornar erros definidos para Rust.
Em Go, a prática de usar variáveis para erros permite um código mais robusto e fácil de manter. Em Rust, usamos o enum Result
e a enumeração Error
para alcançar um efeito similar.
Vamos considerar um exemplo simples de uma função que retorna um item de um Store
:
Em Go:
package clean
import "errors"
var (
NullItem = Item{}
ErrItemNotFound = errors.New("item could not be found in the store")
)
func (store *Store) GetItem(id string) (Item, error) {
store.mtx.Lock()
defer store.mtx.Unlock()
item, ok := store.items[id]
if !ok {
return NullItem, ErrItemNotFound
}
return item, nil
}
Em Rust:
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone)]
pub enum StoreError {
ItemNotFound,
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
StoreError::ItemNotFound => write!(f, "Item could not be found in the store"),
}
}
}
pub struct Store {
items: HashMap<String, Item>,
// outros campos, se houver
}
impl Store {
pub fn get_item(&self, id: &str) -> Result<Item, StoreError> {
match self.items.get(id) {
Some(item) => Ok(item.clone()),
None => Err(StoreError::ItemNotFound),
}
}
}
Neste exemplo, StoreError
é uma enumeração que define erros possíveis, e a função get_item
retorna um Result<Item, StoreError>
. Utilizamos Result
em Rust para retornar valores que podem ser um sucesso (Ok
) ou uma falha (Err
).
Para erros que precisam de informações dinâmicas, Rust usa Box<dyn std::error::Error>
para encapsular erros que podem ter diferentes tipos. Aqui está como você pode adaptar isso:
Em Go:
func (store *Store) GetItem(id string) (Item, error) {
store.mtx.Lock()
defer store.mtx.Unlock()
item, ok := store.items[id]
if !ok {
return NullItem, fmt.Errorf("Could not find item with ID: %s", id)
}
return item, nil
}
Em Rust:
use std::error::Error;
use std::fmt;
#[derive(Debug, Clone)]
pub struct DetailedError {
message: String,
}
impl fmt::Display for DetailedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl Error for DetailedError {}
pub struct Store {
items: HashMap<String, Item>,
// outros campos, se houver
}
impl Store {
pub fn get_item(&self, id: &str) -> Result<Item, Box<dyn Error>> {
match self.items.get(id) {
Some(item) => Ok(item.clone()),
None => Err(Box::new(DetailedError {
message: format!("Could not find item with ID: {}", id),
})),
}
}
}
Aqui, DetailedError
é uma estrutura que implementa a trait Error
, permitindo mensagens de erro dinâmicas. A função get_item
retorna um Result<Item, Box<dyn Error>>
, o que permite encapsular qualquer tipo de erro.
Em Rust, o conceito de None
é representado pelo tipo Option
. O tipo Option
é usado para valores que podem estar ausentes e é uma maneira segura de lidar com a ausência de valor.
Em Go:
type App struct {
Cache *KVCache
}
func (app *App) Cache() *KVCache {
if app.Cache == nil {
app.Cache = NewKVCache()
}
return app.Cache
}
Em Rust:
pub struct App {
cache: Option<KVCache>,
}
impl App {
pub fn new() -> Self {
App { cache: None }
}
pub fn cache(&mut self) -> &KVCache {
if self.cache.is_none() {
self.cache = Some(KVCache::new());
}
self.cache.as_ref().unwrap()
}
}
Neste exemplo, cache
é um Option<KVCache>
. O método cache
inicializa o KVCache
se ainda não estiver presente e retorna uma referência ao KVCache
inicializado.
- Em Rust, o uso de
Result
eOption
permite um controle mais explícito sobre valores de erro e ausência de valores. - A abordagem de retornar erros definidos e dinamicamente detalhados pode ser implementada usando enums e tipos personalizados, o que melhora a clareza e manutenção do código.
- Evitar valores
None
e usarOption
ajuda a garantir que o código não acabe com valores nulos não tratados, o que pode prevenir muitos problemas em tempo de execução.
Ponteiros em Rust são tratados de maneira diferente do que em Go, e a abordagem do Rust para gerenciamento de memória e segurança é mais rigorosa. Rust não possui o conceito de ponteiros nulos como em Go, e a segurança de memória é garantida através do sistema de tipos e regras de empréstimo.
Em Rust, a mutabilidade é controlada explicitamente através do uso de referências mutáveis e imutáveis. Referências mutáveis (&mut T
) permitem modificar o valor referenciado, enquanto referências imutáveis (&T
) garantem que o valor não pode ser alterado. Essa abordagem evita problemas comuns de mutabilidade encontrados em outras linguagens.
Considere um exemplo em Rust que reflete o uso de ponteiros e mutabilidade em Go:
struct User {
id: i64,
password: String,
}
struct UserStore {
users: std::collections::HashMap<i64, User>,
}
impl UserStore {
fn insert(&mut self, user: User) -> Result<(), &'static str> {
if self.users.contains_key(&user.id) {
return Err("Item already exists");
}
self.users.insert(user.id, user);
Ok(())
}
fn get(&self, id: i64) -> Result<&User, &'static str> {
self.users.get(&id).ok_or("User not found")
}
}
fn main() {
let mut store = UserStore {
users: std::collections::HashMap::new(),
};
let user = User {
id: 123,
password: "secure_password".to_string(),
};
match store.insert(user) {
Ok(_) => println!("User inserted successfully"),
Err(err) => println!("Failed to insert user: {}", err),
}
match store.get(123) {
Ok(user) => println!("Found user with id 123"),
Err(err) => println!("Failed to find user: {}", err),
}
}
Aqui, a função insert
aceita o User
por valor e o adiciona ao HashMap
. Como o User
é movido para o HashMap
, a função get
pode retornar uma referência ao User
armazenado sem problemas de escopo.
Como Rust não permite a modificação de dados através de referências imutáveis e impõe regras rigorosas de empréstimo, os problemas comuns de efeitos colaterais indesejados com ponteiros são mitigados.
Ao passar valores por referência mutável, Rust garante que apenas uma referência mutável para um valor existe ao mesmo tempo, evitando conflitos:
fn update_user_password(user: &mut User, new_password: &str) {
user.password = new_password.to_string();
}
fn main() {
let mut user = User {
id: 123,
password: "old_password".to_string(),
};
update_user_password(&mut user, "new_secure_password");
println!("Updated password: {}", user.password);
}
Rust também possui fechamentos, que são bastante semelhantes aos encontrados em Go, mas com uma diferença fundamental: eles capturam o ambiente de maneira segura e eficiente. Fechamentos podem capturar variáveis do ambiente de várias maneiras (por valor, por referência, ou mutável), e o Rust garante que essa captura seja segura.
Aqui está um exemplo de como você pode usar fechamentos em Rust:
fn apply_operation<F>(data: &str, operation: F) -> String
where
F: Fn(&str) -> String,
{
operation(data)
}
fn main() {
let to_uppercase = |s: &str| s.to_uppercase();
let result = apply_operation("hello", to_uppercase);
println!("Result: {}", result);
}
No exemplo acima, apply_operation
recebe um fechamento que transforma a string de entrada em maiúsculas. O fechamento captura o ambiente onde é definido e é passado como argumento para apply_operation
.
Para adaptar os conceitos de interfaces em Go para Rust, vamos explorar como Rust lida com traços (traits) e o que podemos aprender com a abordagem do Go, incluindo as boas práticas e os problemas associados.
1. Implementação Implícita vs. Explícita
Em Go, uma interface é implementada implicitamente; se um tipo fornece todos os métodos de uma interface, ele implementa a interface sem necessidade de uma declaração explícita. Em Rust, a implementação de um trait também é implícita, mas a diferença é que o Rust permite a implementação explícita, se desejado, o que pode ajudar na clareza do código.
Exemplo em Rust:
trait Writer {
fn write(&self, data: &[u8]) -> Result<usize, std::io::Error>;
}
struct NullWriter;
impl Writer for NullWriter {
fn write(&self, data: &[u8]) -> Result<usize, std::io::Error> {
Ok(data.len())
}
}
fn new_null_writer() -> impl Writer {
NullWriter
}
Aqui, NullWriter
implementa o trait Writer
implicitamente porque fornece uma implementação para o método write
.
2. Verificação de Implementação de Trait
Enquanto Go utiliza um padrão de teste para verificar a implementação de uma interface, Rust faz isso de forma mais direta através de erros de compilação se a implementação do trait não estiver correta.
Exemplo em Rust:
trait Writer {
fn write(&self, data: &[u8]) -> Result<usize, std::io::Error>;
}
struct NullWriter;
impl Writer for NullWriter {
fn write(&self, data: &[u8]) -> Result<usize, std::io::Error> {
Ok(data.len())
}
}
fn assert_writer_impl() {
let _writer: Box<dyn Writer> = Box::new(NullWriter);
}
Se NullWriter
não implementar o trait Writer
corretamente, o código não compila.
3. Traits e Herança
Rust não tem herança como em linguagens orientadas a objetos tradicionais, mas você pode utilizar traits para compor comportamentos. Traits podem ser usados para construir uma abstração similar à herança, mas de forma mais controlada e explícita.
Exemplo em Rust:
trait Read {
fn read(&self) -> String;
}
trait Write {
fn write(&self, data: &str);
}
struct Document;
impl Read for Document {
fn read(&self) -> String {
"Document content".to_string()
}
}
impl Write for Document {
fn write(&self, data: &str) {
println!("Writing: {}", data);
}
}
4. Métodos de Traits e Implementações
Diferente de Go, onde a implementação de métodos pode resultar em comportamentos inesperados, Rust exige que todos os métodos definidos em um trait sejam implementados. A implementação parcial não é permitida, o que reduz a possibilidade de comportamento confuso.
Exemplo em Rust:
trait MyReadCloser: std::io::Read + std::io::Close {}
struct MyReadCloserImpl;
impl std::io::Read for MyReadCloserImpl {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
Ok(0)
}
}
impl std::io::Close for MyReadCloserImpl {
fn close(&mut self) -> Result<(), std::io::Error> {
Ok(())
}
}
impl MyReadCloser for MyReadCloserImpl {}
1. Interface Vazias
Em Go, interface{}
pode armazenar qualquer tipo, mas frequentemente resulta em código mais difícil de manter e propenso a erros. Rust não possui um equivalente direto, mas oferece enums e traits para criar abstrações mais seguras e menos propensas a erros.
Exemplo de Enum em Rust:
enum MyEnum {
Variant1(i32),
Variant2(String),
}
fn process_enum(value: MyEnum) {
match value {
MyEnum::Variant1(n) => println!("Number: {}", n),
MyEnum::Variant2(s) => println!("String: {}", s),
}
}
2. Genéricos em Rust
Rust tem suporte robusto para genéricos, permitindo a criação de tipos e funções genéricas de forma segura e eficiente.
Exemplo em Rust:
struct HashMap<K, V> {
store: std::collections::HashMap<K, V>,
}
impl<K, V> HashMap<K, V> {
fn new() -> Self {
HashMap {
store: std::collections::HashMap::new(),
}
}
fn insert(&mut self, key: K, value: V) {
self.store.insert(key, value);
}
fn get(&self, key: &K) -> Option<&V> {
self.store.get(key)
}
}