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.
Conceitos da linguagem: do C# para o Rust
Seção intitulada “Conceitos da linguagem: do C# para o Rust”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 |
Coleções
Seção intitulada “Coleções”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.
Ferramentas: do dotnet para o cargo
Seção intitulada “Ferramentas: do dotnet para o cargo”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.
Acesso a dados
Seção intitulada “Acesso a dados”| .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.
Web e rede
Seção intitulada “Web e rede”| .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 |
Blocos do dia a dia
Seção intitulada “Blocos do dia a dia”| .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 |
Desktop, jogos e mais
Seção intitulada “Desktop, jogos e mais”| .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 |
Lado a lado: uma classe vira struct + impl
Seção intitulada “Lado a lado: uma classe vira struct + impl”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 150Os 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.
Lado a lado: uma interface vira um trait
Seção intitulada “Lado a lado: uma interface vira um trait”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.146.00No 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#.
Lado a lado: null vira Option
Seção intitulada “Lado a lado: null vira Option”O C# com nullable reference types avisa você sobre null, mas o null continua existindo em tempo de execução:
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 { } 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 cadastradodesconhecidoNã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.
Lado a lado: try/catch vira Result e ?
Seção intitulada “Lado a lado: try/catch vira Result e ?”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: 42Erro: invalid digit found in stringA 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).
Lado a lado: LINQ vira uma cadeia de iteradores
Seção intitulada “Lado a lado: LINQ vira uma cadeia de iteradores”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, AdaFaturamento: 550Os 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.
Lado a lado: using/IDisposable vira Drop
Seção intitulada “Lado a lado: using/IDisposable vira Drop”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-dbfechando a conexão com orders-dbdepois do blocoIsso 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.
O que vai fazer você tropeçar vindo do C#
Seção intitulada “O que vai fazer você tropeçar vindo do C#”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 umaStringou de umVecparab, eanã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` trait3 | let copy = owner;| ----- value moved here4 | println!("{owner} e {copy}");| ^^^^^ value borrowed here after moveO compilador diz (em inglês) que
ownerfoi 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. -
Stringvs&str. O C# tem um único tipostring. O Rust tem dois:Stringé dona do texto,&strpega o texto emprestado. Funções normalmente recebem&stre structs normalmente guardamString. 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 seumaintambém pode devolverResult. 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,
Optionem 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 ocargo checkenquanto edita. -
Async precisa de um runtime. No .NET, o runtime e o thread pool estão sempre lá. No Rust, uma
async fnnão faz nada até que um executor a rode, então você adiciona o tokio e coloca#[tokio::main]nomain. Veja Async/Await.
Uma trilha de estudo para desenvolvedores C#
Seção intitulada “Uma trilha de estudo para desenvolvedores C#”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:
- Começando com Rust: instale o Rust e rode
cargo new. - Variáveis e Tipos de dados: passe os olhos, principalmente para aprender
let mute os tipos numéricos. - Ownership, depois Borrowing, depois Slices: as páginas mais importantes para você. Vá com calma.
- Strings:
Stringvs&str. - Structs e Enums, depois Match e Pattern matching.
- Option e Result, depois Erros personalizados.
- Traits, Genéricos, Traits comuns e Trait objects.
- Closures e Iteradores: o seu LINQ.
- Vetores, HashMap e VecDeque e BinaryHeap.
- Módulos, Cargo e crates e Testes em Rust.
- Lifetimes e Ponteiros inteligentes.
- Concorrência, Estado compartilhado e Async/Await.
- JSON com Serde e Logging, e depois construa o projeto de API web.
Prática
Seção intitulada “Prática”Os dois exercícios trazem um trecho de C#. Converta para Rust.
1. Converta um record e uma consulta LINQ
Seção intitulada “1. Converta um record e uma consulta LINQ”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, CanecaUma 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 ==.
2. Converta um Dictionary com exceções
Seção intitulada “2. Converta um Dictionary com exceções”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 2erro: sem estoque de peraserro: nenhum item chamado kiwisO 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,
Optionno lugar de null, eResult+?no lugar de exceções. - Classes viram uma
struct+impl, interfaces viram traits, e herança vira composição mais traits.using/IDisposablevira oDropautomático. - O Cargo substitui a CLI do
dotnet, o MSBuild e o NuGet; o clippy e o rustfmt substituem os analisadores e odotnet 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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Rust for C#/.NET Developers — Microsoft (em inglês): o guia da própria Microsoft, com muitos outros exemplos lado a lado em C# e Rust.
- The Rust Programming Language (em inglês): o livro oficial. Os capítulos 4 (ownership), 9 (erros) e 10 (genéricos e traits) são os que mais importam para quem vem do C#.
- Getting Started with Diesel e documentação do SeaORM (em inglês): os dois principais ORMs, se você está procurando algo como o Entity Framework.
- sqlx no docs.rs (em inglês): a escolha no estilo do Dapper, com SQL verificado em tempo de compilação.
- Are We Web Yet? e Are We GUI Yet? (em inglês): panoramas da comunidade sobre as bibliotecas de Rust para web e para interfaces gráficas de desktop.