Skip to content

Código Rust Limpo - 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 projeto.

Notifications You must be signed in to change notification settings

fonluc/codigo-limpo-rust

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 

Repository files navigation

Código Rust Limpo

comic

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.

Prefácio: Por que Escrever Código Limpo?

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.

Sumário

Introdução ao 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.

Desenvolvimento Orientado a Testes

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:

  1. Escrever (ou executar) um teste
  2. Se o teste falhar, fazer com que ele passe
  3. Refatorar seu código conforme necessário
  4. 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.

Convenções de Nomenclatura

Comentários

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.

Nomeação de Funções em Rust

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("")
}

Nomeação de Variáveis em Rust

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
}

Limpeza de Funções em Rust

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")))
}

Assinaturas de Função em Rust

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,
        }
    }
}

Escopo de Variáveis em Rust

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.

Declaração de Variáveis em Rust

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.

Rust Limpo

Valores Retornados

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).

Retornando Erros Dinâmicos

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.

Valores None

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 e Option 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 usar Option 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

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.

Mutabilidade e Ponteiros

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.

Evitando Efeitos Colaterais Indesejados

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);
}

Fechamentos em Rust

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.

Interfaces em Go x Traits em Rust

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 {}

Interfaces Vazias em Go vs. Tipos Dinâmicos e Genéricos em Rust

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)
    }
}

About

Código Rust Limpo - 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 projeto.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages