Pular para o conteúdo

Rust para desenvolvedores C#: guia para quem vem do .NET

Se você trabalha com C#, tenho uma boa notícia: muita coisa do Rust vai parecer familiar. O Rust tem genéricos com restrições, async/await, um pattern matching (casamento de padrões) poderoso, cadeias de iteradores que lembram muito o LINQ, e uma excelente ferramenta de linha de comando (Cargo) que faz o papel do dotnet. Você já sabe pensar em tipos, interfaces e lambdas. Esse conhecimento vem junto com você.

A grande mudança está na memória e nos erros. O Rust não tem garbage collector (coletor de lixo). Em vez disso, cada valor tem um único dono, e o compilador verifica o ownership (posse) e o borrowing (empréstimo) antes de o programa rodar. O Rust também não tem null (você usa Option) e não tem exceções (você usa Result). É nessas três mudanças que vai a maior parte do seu tempo de estudo. Esta página mostra o equivalente em Rust do que você já conhece do C# e do .NET, para você gastar esse tempo nas partes que são realmente novas.

Se você prefere uma comparação geral com C++, Go e Python, veja Rust vs outras linguagens.

Use esta tabela como um guia de conversação. A última coluna leva à lição deste site que explica o lado do Rust.

C# Rust Saiba mais
class com campos e métodos struct para os dados + um bloco impl para os métodos Structs
interface trait Traits
Classe abstrata, herança Não existe herança. Use composição (uma struct dentro de outra) + traits com métodos padrão Traits, Padrões de projeto
List<IShape> com classes diferentes Vec<Box<dyn Shape>> (um trait object) Trait objects
Extension methods Um trait seu, implementado para um tipo que você não escreveu (como String) Traits
Genéricos com where T : IComparable<T> Genéricos com trait bounds: T: Ord ou where T: Ord Genéricos
null, Nullable<int>, nullable reference types Option<T>: ou Some(valor) ou None Option
?? e ?. .unwrap_or(...), .map(...), .and_then(...) e ? Option
Exceções, throw, try/catch Result<T, E> + o operador ? Result, Erros personalizados
Uma exceção para um bug de verdade (como InvalidOperationException) panic! (encerra a thread) Panic
IDisposable + using Drop, que roda sozinho quando o dono sai de escopo (RAII) Escopo, Ponteiros inteligentes
Garbage collector Ownership e borrowing, verificados em tempo de compilação Ownership, Borrowing
record struct com #[derive(Debug, Clone, PartialEq)] Structs, Traits comuns
Expressão with em um record Sintaxe de atualização de struct: Order { total: 5, ..old } Structs
enum (só inteiros com nome) enum em que cada variante pode carregar os próprios dados Enums
Expressões switch, padrões is match (o compilador confere se você tratou todos os casos) Match, Pattern matching
Tuplas (int, string) Tuplas (i32, String) Tuplas
Parâmetros ref / out &mut T, ou devolva uma tupla ou um Option Borrowing
Lambdas, Func<T>, Action<T>, delegates Closures, descritas pelos traits Fn, FnMut, FnOnce Closures, Fn, FnMut e FnOnce
event Um canal (channel), ou uma lista de closures em Box (callbacks) Canais (channels), Fn, FnMut e FnOnce
LINQ: Where, Select, Sum, OrderBy, ToList Adaptadores de iterador: filter, map, sum, sort_by (em um Vec), collect Iteradores, Ordenação e busca
IEnumerable<T> e yield return impl Iterator<Item = T>, ou um tipo seu que implementa Iterator Iteradores personalizados
async Task<T> / await async fn devolve um Future; você precisa de um runtime como o tokio para executá-lo Async/Await
Task.Run, Thread, lock std::thread::spawn ou tokio::spawn; o Mutex<T> envolve os dados que protege Concorrência, Estado compartilhado
Propriedades (get; set;) Não existem propriedades. Use campos pub, ou métodos getter e setter Structs
string e StringBuilder &str (uma visão emprestada de um texto) e String (com dono, pode crescer) Strings
Interpolação de strings $"Oi {name}" format!("Oi {name}") e println!("Oi {name}") Saída em Rust
var let (imutável por padrão; escreva let mut para permitir mudanças) Variáveis
const, readonly const, e o let simples (que já é somente leitura) Variáveis
Campo static readonly static, ou LazyLock para valores criados na inicialização Variáveis
namespace e diretivas using Módulos (mod) e use Módulos
Assembly / projeto Crate (uma biblioteca ou um binário) Cargo e crates
Atributos como [Serializable] Atributos como #[derive(Serialize)] e macros derive Macros, JSON com Serde
Reflection Quase nada em tempo de execução. Macros e derive geram código em tempo de compilação Macros
unsafe e ponteiros Blocos unsafe e ponteiros brutos (raw pointers) Unsafe em Rust
Span<T>, ReadOnlySpan<char> Slices: &[T] e &str Slices
Sobrescrever ToString(), IFormattable Implemente Display (e o .to_string() vem de graça) Traits comuns
IEquatable<T>, IComparable<T>, GetHashCode PartialEq/Eq, PartialOrd/Ord, Hash (normalmente com derive) Traits comuns
Operadores de conversão implicit / explicit From/Into (e TryFrom quando a conversão pode falhar) Conversões
Sobrecarga de operadores (operator +) Implemente traits de std::ops, como Add Traits avançados
Aritmética checked / unchecked checked_add, wrapping_add, saturating_add Overflow de inteiros
decimal Não existe um tipo decimal embutido; use o crate rust_decimal, ou guarde centavos em um inteiro Tipos de dados

A biblioteca padrão cobre quase tudo o que existe em System.Collections.Generic. Os nomes mudam, as ideias não.

C# Rust Saiba mais
List<T> Vec<T> Vetores
T[] (tamanho fixo) [T; N] quando o tamanho é conhecido em tempo de compilação; senão, Vec<T> Arrays
Dictionary<K, V> HashMap<K, V> HashMap
SortedDictionary<K, V> BTreeMap<K, V> (as chaves ficam ordenadas) HashMap
HashSet<T> HashSet<T> HashSet
SortedSet<T> BTreeSet<T> HashSet
Queue<T> VecDeque<T> (push_back / pop_front) VecDeque e BinaryHeap
Stack<T> Vec<T> com push / pop Vetores
PriorityQueue<TElement, TPriority> BinaryHeap<T> (o maior primeiro; envolva em Reverse para o menor primeiro) VecDeque e BinaryHeap
LinkedList<T> LinkedList<T> existe, mas você raramente precisa dela; o VecDeque costuma ser mais rápido VecDeque e BinaryHeap
ConcurrentDictionary<K, V> Mutex<HashMap<K, V>>, ou o crate dashmap Estado compartilhado

Um cuidado: assim como o Dictionary, um HashMap do Rust não tem ordem fixa. Se for imprimir o conteúdo, ordene antes ou use um BTreeMap.

Esta é a parte de que a maioria dos desenvolvedores C# gosta logo de cara. O Cargo faz o trabalho da CLI do dotnet, do MSBuild e do NuGet numa ferramenta só.

.NET Rust Observações
CLI do dotnet cargo Compila, executa, testa, adiciona pacotes, publica. Veja Cargo e crates
.csproj Cargo.toml Curto e fácil de editar à mão
packages.lock.json Cargo.lock Criado para você
.sln com vários projetos Um workspace do Cargo Um Cargo.toml no topo lista os crates membros
NuGet / nuget.org crates.io A documentação da API de todo crate publicado fica no docs.rs
dotnet add package Serilog cargo add tracing
dotnet new console cargo new my_app (cargo new --lib para uma biblioteca)
dotnet run / build / test cargo run / cargo build / cargo test
-c Release --release Builds de debug são bem mais lentos na execução; faça benchmarks em release
dotnet watch cargo watch ou bacon (instalados com cargo install)
dotnet tool install -g cargo install
global.json (versão do SDK) rustup e um arquivo rust-toolchain.toml
Analisadores Roslyn cargo clippy Centenas de lints úteis, oficial
dotnet format cargo fmt Um estilo padrão para todo mundo
Visual Studio / Rider VS Code + rust-analyzer, ou JetBrains RustRover O rust-analyzer oferece autocompletar, ir para a definição e erros na linha
Comentários de documentação XML /// Comentários de documentação /// em Markdown + cargo doc Os exemplos de código na documentação rodam como testes. Veja Documentação
xUnit / NUnit / MSTest #[test] embutido e cargo test (o cargo nextest é um executor popular e mais rápido) Veja Testes em Rust
Moq mockall, ou fakes escritos à mão que implementam o seu trait
Coverlet (cobertura de código) cargo llvm-cov ou cargo tarpaulin
BenchmarkDotNet criterion Veja Desempenho
dotnet-trace, profilers cargo flamegraph, perf, ou qualquer profiler nativo

Aqui está um Cargo.toml pequeno, para você comparar com um .csproj:

[package]
name = "orders-api"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Bibliotecas e frameworks: “O que usamos no lugar de…?”

Seção intitulada “Bibliotecas e frameworks: “O que usamos no lugar de…?””

Essa é a primeira pergunta de todo time .NET. “A gente usa Entity Framework. O que tem no Rust?” Aqui estão os equivalentes mais próximos. No Rust, não existe uma empresa por trás de uma “stack padrão”, como a Microsoft no .NET, então muitas vezes você escolhe entre dois ou três bons crates da comunidade.

.NET Rust Observações
Entity Framework Core Diesel: ORM e query builder; suas consultas têm os tipos verificados em tempo de compilação. Migrations com diesel_cli Maduro (2.x), síncrono por padrão (existe o diesel-async)
Entity Framework Core (async, no estilo de entidades) SeaORM: ORM assíncrono com entidades, relacionamentos e migrations (sea-orm-cli) É o que mais lembra o EF; construído em cima do sqlx
Dapper sqlx: você escreve SQL puro, e a macro query! confere a consulta com o seu banco de dados real em tempo de compilação. Migrations com sqlx-cli Assíncrono, muito popular, não é um ORM
ADO.NET sqlx, tokio-postgres, rusqlite (SQLite) Drivers de mais baixo nível
StackExchange.Redis redis

Se você gostava do EF, experimente primeiro o SeaORM. Se você gostava do Dapper e de escrever o próprio SQL, experimente o sqlx. Se você quer as garantias mais fortes em tempo de compilação, experimente o Diesel. Bancos de dados em Rust mostra os três com código que funciona.

.NET Rust Observações
ASP.NET Core Web API axum (construímos uma API com ele em Projeto: API web com Axum), actix-web, Rocket axum e actix-web são muito usados em produção
Minimal APIs axum: as rotas são handlers async fn simples O estilo é muito parecido
Pipeline de middleware Camadas do tower e o crate tower-http (CORS, compressão, tracing, timeouts) O axum usa o tower diretamente
Injeção de dependência embutida State no axum, construtores simples e traits Veja a linha sobre DI mais abaixo
Swashbuckle / OpenAPI utoipa
SignalR WebSockets no axum (a feature ws), ou tokio-tungstenite Não há uma camada de protocolo no estilo do SignalR embutida
gRPC (Grpc.AspNetCore) tonic (com prost para protobuf)
HttpClient reqwest Assíncrono por padrão, com um cliente bloqueante opcional
Autenticação JWT bearer jsonwebtoken
.NET Rust Observações
System.Text.Json / Newtonsoft.Json serde + serde_json O padrão; também lida com TOML, YAML e mais. Veja JSON com Serde
ILogger / Serilog tracing (+ tracing-subscriber), ou log + env_logger Logging estruturado com spans. Veja Logging
IConfiguration / appsettings.json config ou figment Combine arquivos e variáveis de ambiente em camadas e depois desserialize com o serde
Contêiner de DI (IServiceCollection) Normalmente nada: passe as dependências nos construtores e use traits para o que você quer trocar nos testes O shaku existe, mas poucos projetos usam um contêiner
AutoMapper Escreva implementações de From / Into Explícito e verificado pelo compilador. Veja Conversões
FluentValidation / DataAnnotations validator ou garde (validação com derive)
Polly (novas tentativas) backon, ou o middleware de retry do tower
MediatR Sem equivalente direto Traits simples, ou canais entre tarefas
Hangfire / jobs em segundo plano tokio::spawn para tarefas dentro do processo; apalis para filas de jobs persistentes
CancellationToken Descarte (drop) o future, ou use o CancellationToken do tokio-util
.NET Rust Observações
Blazor WebAssembly Leptos, Yew, Dioxus Todos antes da versão 1.0; as APIs ainda mudam. Veja WebAssembly
WPF / WinForms / MAUI Tauri (interface web + backend em Rust), Slint, egui, Iced Tauri 2 e Slint 1.x são os mais estáveis. Não existe um toolkit de interface gráfica oficial
Unity Bevy, ou Rust dentro do Godot com o crate godot O Bevy está antes da 1.0 e quebra a API entre versões
ML.NET candle, burn, linfa Bem mais jovens que as ferramentas de ML do Python
Azure SDK for .NET O Azure SDK oficial para Rust: azure_core, azure_identity, azure_storage_blob, … Cobre menos serviços que o SDK do .NET

Vamos ver código de verdade. Aqui está uma pequena conta bancária em C#:

var account = new BankAccount("Ada", 100);
account.Deposit(50);
Console.WriteLine($"{account.Owner} tem {account.Balance}");
public class BankAccount
{
public string Owner { get; }
private decimal balance;
public BankAccount(string owner, decimal balance)
{
Owner = owner;
this.balance = balance;
}
public void Deposit(decimal amount) => balance += amount;
public decimal Balance => balance;
}

E em Rust:

struct BankAccount {
owner: String,
balance: i64,
}
impl BankAccount {
// `new` é só uma convenção, não uma palavra-chave.
fn new(owner: &str, balance: i64) -> Self {
BankAccount { owner: owner.to_string(), balance }
}
// `&mut self`: este método altera a conta.
fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
// `&self`: este método só lê.
fn balance(&self) -> i64 {
self.balance
}
}
fn main() {
let mut account = BankAccount::new("Ada", 100);
account.deposit(50);
println!("{} tem {}", account.owner, account.balance());
}

Saída:

Ada tem 150
▶ Experimente você mesmo

Os dados e o comportamento ficam em dois blocos separados. Não existem construtores, só funções que devolvem Self. Cada método diz logo de cara se lê (&self) ou altera (&mut self) o valor. E como deposit altera a conta, a variável precisa ser let mut. No C#, qualquer método pode alterar qualquer campo; no Rust, as alterações aparecem na assinatura.

Em C#, você poderia ter uma interface IShape com duas classes e uma List<IShape>:

var shapes = new List<IShape> { new Circle { Radius = 1 }, new Rectangle { Width = 2, Height = 3 } };
foreach (var s in shapes) Console.WriteLine($"{s.Area():F2}");
public interface IShape { double Area(); }
public class Circle : IShape
{
public double Radius { get; init; }
public double Area() => Math.PI * Radius * Radius;
}
public class Rectangle : IShape
{
public double Width { get; init; }
public double Height { get; init; }
public double Area() => Width * Height;
}

Em Rust, a interface é um trait, e cada struct o implementa no próprio bloco impl:

trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
// Box<dyn Shape> faz o papel de "uma referência para algum IShape".
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Rectangle { width: 2.0, height: 3.0 }),
];
for shape in &shapes {
println!("{:.2}", shape.area());
}
}

Saída:

3.14
6.00
▶ Experimente você mesmo

No C#, toda chamada por interface passa por uma chamada virtual. No Rust, você escolhe: Box<dyn Shape> usa uma chamada virtual (veja Trait objects), enquanto uma função genérica como fn print_area<T: Shape>(s: &T) é compilada separadamente para cada tipo, sem chamada virtual nenhuma. Um bônus: você pode implementar um trait seu para tipos que você não escreveu, até String ou i32. É assim que o Rust faz o que os extension methods fazem no C#.

O C# com nullable reference types avisa você sobre null, mas o null continua existindo em tempo de execução:

string? FindEmail(int userId) => userId == 1 ? "[email protected]" : null;
var email = FindEmail(2);
if (email is not null) Console.WriteLine(email.ToUpper());
else Console.WriteLine("nenhum e-mail cadastrado");
Console.WriteLine(FindEmail(2) ?? "desconhecido");

Em Rust, um valor que pode estar faltando tem o tipo Option<T>. Você não consegue usá-lo como um valor comum antes de tratar o caso None:

fn find_email(user_id: u32) -> Option<String> {
if user_id == 1 {
Some("[email protected]".to_string())
} else {
None
}
}
fn main() {
match find_email(1) {
Some(email) => println!("{}", email.to_uppercase()),
None => println!("nenhum e-mail cadastrado"),
}
// `if let` é a forma curta quando só um caso interessa.
if let Some(email) = find_email(2) {
println!("encontrado: {email}");
} else {
println!("nenhum e-mail cadastrado");
}
// unwrap_or funciona como o operador ?? do C#.
let email = find_email(2).unwrap_or("desconhecido".to_string());
println!("{email}");
}

Saída:

nenhum e-mail cadastrado
desconhecido
▶ Experimente você mesmo

Não existe NullReferenceException no Rust, porque não existe null para desreferenciar. O compilador simplesmente não deixa você chamar .to_uppercase() em um Option<String>. Aprenda os métodos auxiliares na página de Option, e as formas curtas em If Let / While Let.

Em C#, o parse pode lançar uma exceção, e você a captura em algum lugar mais acima na pilha de chamadas:

static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try
{
Console.WriteLine(TotalSeats("12", "30"));
Console.WriteLine(TotalSeats("12", "trinta"));
}
catch (FormatException e)
{
Console.WriteLine($"Erro: {e.Message}");
}

Em Rust, uma função que pode falhar diz isso no tipo de retorno. O operador ? significa “se isto falhou, devolva o erro para quem me chamou agora mesmo”. Ele faz o mesmo trabalho de deixar uma exceção subir, mas você consegue vê-lo no código:

use std::num::ParseIntError;
fn total_seats(a: &str, b: &str) -> Result<i32, ParseIntError> {
let first: i32 = a.parse()?;
let second: i32 = b.parse()?;
Ok(first + second)
}
fn main() {
for (a, b) in [("12", "30"), ("12", "trinta")] {
match total_seats(a, b) {
Ok(total) => println!("Total: {total}"),
Err(e) => println!("Erro: {e}"),
}
}
}

Saída:

Total: 42
Erro: invalid digit found in string
▶ Experimente você mesmo

A mensagem “invalid digit found in string” (dígito inválido encontrado na string) continua em inglês porque ela vem da biblioteca padrão do Rust. A grande diferença: no C#, qualquer método pode lançar uma exceção, e a assinatura não conta isso para você. No Rust, o Result faz parte da assinatura, então quem chama não tem como esquecer de tratá-lo. Para criar os seus próprios tipos de erro, veja Erros personalizados. Em aplicações, o crate anyhow oferece algo parecido com “capturar qualquer exceção” (veja anyhow e thiserror).

Uma consulta LINQ típica:

var orders = new List<Order> { new("Ada", 120), new("Grace", 80), new("Linus", 200), new("Ken", 150) };
var bigSpenders = orders
.Where(o => o.Total > 100)
.OrderByDescending(o => o.Total)
.Select(o => o.Customer)
.ToList();
Console.WriteLine(string.Join(", ", bigSpenders));
Console.WriteLine($"Faturamento: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);

A versão em Rust usa adaptadores de iterador e closures:

struct Order {
customer: String,
total: u32,
}
fn main() {
let orders = vec![
Order { customer: "Ada".to_string(), total: 120 },
Order { customer: "Grace".to_string(), total: 80 },
Order { customer: "Linus".to_string(), total: 200 },
Order { customer: "Ken".to_string(), total: 150 },
];
// Where(...).ToList()
let mut big: Vec<&Order> = orders.iter().filter(|o| o.total > 100).collect();
// OrderByDescending(...): a ordenação acontece num Vec, não no iterador.
big.sort_by(|a, b| b.total.cmp(&a.total));
// Select(...)
let names: Vec<&str> = big.iter().map(|o| o.customer.as_str()).collect();
println!("{}", names.join(", "));
// Sum(...)
let revenue: u32 = orders.iter().map(|o| o.total).sum();
println!("Faturamento: {revenue}");
}

Saída:

Linus, Ken, Ada
Faturamento: 550
▶ Experimente você mesmo

Os iteradores do Rust são preguiçosos (lazy), igual à execução adiada do LINQ: nada roda até você chamar algo como collect(), sum() ou um laço for. O collect() é o seu ToList(). Diferente do LINQ, as cadeias de iteradores normalmente são compiladas para o mesmo código de máquina de um laço escrito à mão, então você não paga nada pelo estilo bonito. A ordenação é explicada na página Ordenação e busca.

Em C#, você precisa lembrar do using para liberar um recurso na hora certa:

using (var conn = new Connection("orders-db"))
{
Console.WriteLine($"consultando {conn.Name}");
}
Console.WriteLine("depois do bloco");
class Connection : IDisposable
{
public string Name { get; }
public Connection(string name) => Name = name;
public void Dispose() => Console.WriteLine($"fechando a conexão com {Name}");
}

Em Rust, a limpeza é automática. Quando o dono de um valor sai de escopo, o Rust chama a implementação de Drop dele. Não tem nada para esquecer:

struct Connection {
name: String,
}
impl Drop for Connection {
fn drop(&mut self) {
println!("fechando a conexão com {}", self.name);
}
}
fn main() {
{
let conn = Connection { name: "orders-db".to_string() };
println!("consultando {}", conn.name);
} // `conn` sai de escopo aqui, então drop() roda.
println!("depois do bloco");
}

Saída:

consultando orders-db
fechando a conexão com orders-db
depois do bloco
▶ Experimente você mesmo

Isso se chama RAII, e é assim que o Rust cuida de tudo: arquivos, sockets, locks e a própria memória. Um arquivo é fechado quando o valor File dele é descartado (dropped). Um Mutex é destravado quando o guard dele é descartado. Não existe finalizer nem pausa do GC; a limpeza acontece num momento previsível.

Todo mundo que vem do C# passa pelos mesmos buracos. Conhecer todos com antecedência ajuda.

  • Atribuir move o valor, não copia uma referência. No C#, var b = a; com uma classe dá duas referências para um mesmo objeto. No Rust, let b = a; move o ownership de uma String ou de um Vec para b, e a não pode mais ser usado:

    fn main() {
    let owner = String::from("Ada");
    let copy = owner;
    println!("{owner} e {copy}");
    }
    error[E0382]: borrow of moved value: `owner`
    --> src/main.rs:4:16
    |
    2 | let owner = String::from("Ada");
    | ----- move occurs because `owner` has type `String`, which does not implement the `Copy` trait
    3 | let copy = owner;
    | ----- value moved here
    4 | println!("{owner} e {copy}");
    | ^^^^^ value borrowed here after move

    O compilador diz (em inglês) que owner foi movido na linha 3 (“value moved here”) e depois usado na linha 4 (“value borrowed here after move”). A correção é pegar emprestado (let copy = &owner;) ou clonar de propósito (owner.clone()). Leia primeiro Ownership (posse); essa página explica essa regra.

  • O borrow checker. Você pode ter muitos leitores (&T) ou um único escritor (&mut T) ao mesmo tempo, nunca os dois. Padrões normais no C#, como objetos que apontam de volta para o pai, ou muitos objetos alterando um mesmo objeto compartilhado, precisam de smart pointers (ponteiros inteligentes) ou de outro design. Prepare-se para algumas semanas frustrantes. Depois fica bem mais fácil.

  • String vs &str. O C# tem um único tipo string. O Rust tem dois: String é dona do texto, &str pega o texto emprestado. Funções normalmente recebem &str e structs normalmente guardam String. Veja Strings.

  • Sem herança. Você não consegue escrever uma classe base com campos compartilhados e métodos sobrescritos. Use traits (com métodos padrão) para comportamento compartilhado, e coloque os dados compartilhados numa struct que outras structs contêm.

  • Sem exceções. Erros são valores. Você vai escrever ? o tempo todo, e o seu main também pode devolver Result. Chamar .unwrap() é tranquilo enquanto você aprende, mas é a versão Rust de “ignorar a exceção e deixar o programa quebrar”.

  • Sem null. No começo, Option em todo lugar parece trabalho extra. Com o tempo, você percebe que parou de ter bugs de referência nula.

  • Clones explícitos. O Rust nunca copia um valor do heap escondido de você. Se você vê .clone(), uma cópia de verdade é feita. Isso é uma vantagem: o custo fica visível.

  • Tempo de compilação. Um build limpo de um projeto com muitos crates demora mais que um build do .NET. Builds incrementais e o cargo check (que pula a geração de código) são bem mais rápidos; use o cargo check enquanto edita.

  • Async precisa de um runtime. No .NET, o runtime e o thread pool estão sempre lá. No Rust, uma async fn não faz nada até que um executor a rode, então você adiciona o tokio e coloca #[tokio::main] no main. Veja Async/Await.

Você não precisa ler este site na ordem. Como desenvolvedor C#, este caminho pula o que você já sabe e investe tempo onde o Rust é diferente:

  1. Começando com Rust: instale o Rust e rode cargo new.
  2. Variáveis e Tipos de dados: passe os olhos, principalmente para aprender let mut e os tipos numéricos.
  3. Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
  4. Strings: String vs &str.
  5. Structs e Enums, depois Match e Pattern matching.
  6. Option e Result, depois Erros personalizados.
  7. Traits, Genéricos, Traits comuns e Trait objects.
  8. Closures e Iteradores: o seu LINQ.
  9. Vetores, HashMap e VecDeque e BinaryHeap.
  10. Módulos, Cargo e crates e Testes em Rust.
  11. Lifetimes e Ponteiros inteligentes.
  12. Concorrência, Estado compartilhado e Async/Await.
  13. JSON com Serde e Logging, e depois construa o projeto de API web.

Os dois exercícios trazem um trecho de C#. Converta para Rust.

Converta este código C#. Ele deve imprimir os nomes dos produtos que estão em estoque e custam menos de 20. Dica: crie uma struct com #[derive(Debug, Clone, PartialEq)] e depois use filter, map e collect para um Vec<&str>. O Vec<&str> tem um método .join(", ").

var products = new List<Product>
{
new("Caderno", 5, true),
new("Fone de ouvido", 45, true),
new("Caneta", 2, false),
new("Caneca", 12, true),
};
var cheap = products.Where(p => p.InStock && p.Price < 20).Select(p => p.Name);
Console.WriteLine(string.Join(", ", cheap));
record Product(string Name, int Price, bool InStock);
Ver solução
#[derive(Debug, Clone, PartialEq)]
struct Product {
name: String,
price: u32,
in_stock: bool,
}
fn product(name: &str, price: u32, in_stock: bool) -> Product {
Product { name: name.to_string(), price, in_stock }
}
fn main() {
let products = vec![
product("Caderno", 5, true),
product("Fone de ouvido", 45, true),
product("Caneta", 2, false),
product("Caneca", 12, true),
];
let cheap: Vec<&str> = products
.iter()
.filter(|p| p.in_stock && p.price < 20)
.map(|p| p.name.as_str())
.collect();
println!("{}", cheap.join(", "));
}

Saída:

Caderno, Caneca
▶ Experimente você mesmo

Uma pequena função auxiliar faz o papel do construtor posicional do record. Os derives dão o que um record do C# dá de graça: imprimir com {:?}, copiar com .clone() e comparar com ==.

Este código C# tira um item de um dicionário de estoque e lança uma exceção quando algo dá errado. Converta para que take devolva um Result<u32, String> em vez de lançar exceções. Dica: stock.get_mut(item) devolve um Option<&mut u32>. Transforme em um Result com .ok_or(...) e depois use ?.

var stock = new Dictionary<string, int> { ["maçãs"] = 3, ["peras"] = 0 };
int Take(string item)
{
if (!stock.TryGetValue(item, out var count))
throw new KeyNotFoundException($"nenhum item chamado {item}");
if (count == 0)
throw new InvalidOperationException($"sem estoque de {item}");
stock[item] = count - 1;
return count - 1;
}
foreach (var item in new[] { "maçãs", "peras", "kiwis" })
{
try { Console.WriteLine($"retirei uma unidade de {item}, restam {Take(item)}"); }
catch (Exception e) { Console.WriteLine($"erro: {e.Message}"); }
}
Ver solução
use std::collections::HashMap;
fn take(stock: &mut HashMap<String, u32>, item: &str) -> Result<u32, String> {
let count = stock
.get_mut(item)
.ok_or(format!("nenhum item chamado {item}"))?;
if *count == 0 {
return Err(format!("sem estoque de {item}"));
}
*count -= 1;
Ok(*count)
}
fn main() {
let mut stock = HashMap::new();
stock.insert("maçãs".to_string(), 3);
stock.insert("peras".to_string(), 0);
for item in ["maçãs", "peras", "kiwis"] {
match take(&mut stock, item) {
Ok(left) => println!("retirei uma unidade de {item}, restam {left}"),
Err(e) => println!("erro: {e}"),
}
}
}

Saída:

retirei uma unidade de maçãs, restam 2
erro: sem estoque de peras
erro: nenhum item chamado kiwis
▶ Experimente você mesmo

O TryGetValue com um parâmetro out vira get_mut, que devolve um Option. Os dois throw viram valores Err, e o try/catch vira um match. O &mut stock na chamada deixa claro que take altera o dicionário.

  • Muita coisa do C# vem junto: genéricos, async/await, lambdas, pattern matching e iteradores no estilo do LINQ têm equivalentes bem próximos no Rust.
  • As grandes mudanças são ownership no lugar de um garbage collector, Option no lugar de null, e Result + ? no lugar de exceções.
  • Classes viram uma struct + impl, interfaces viram traits, e herança vira composição mais traits. using/IDisposable vira o Drop automático.
  • O Cargo substitui a CLI do dotnet, o MSBuild e o NuGet; o clippy e o rustfmt substituem os analisadores e o dotnet format; os testes já vêm embutidos.
  • No lugar do EF Core, veja o SeaORM ou o Diesel; no lugar do Dapper, o sqlx; e no lugar do ASP.NET Core, o axum ou o actix-web. As opções para interfaces gráficas, jogos e ML são mais jovens.

Teste o que aprendeu

1. O que o Rust usa no lugar de null?

2. O seu time usa Dapper e gosta de escrever SQL à mão. Qual crate do Rust é o mais parecido?

3. Qual é o equivalente mais próximo em Rust do bloco using com IDisposable do C#?

4. No C#, var b = a; com uma classe copia uma referência. O que let b = a; faz com uma String no Rust?

Pontuação: 0 / 4