Rust para desarrolladores de C#
Si programas en C# para ganarte la vida, tenemos buenas noticias: mucho de Rust te va a resultar familiar. Rust tiene genéricos con restricciones, async/await, una potente coincidencia de patrones, cadenas de iteradores que se leen casi como LINQ y una excelente herramienta de línea de comandos (Cargo) que hace lo mismo que dotnet. Ya sabes pensar en tipos, interfaces y lambdas. Todo eso te sirve.
El gran cambio está en la memoria y los errores. Rust no tiene recolector de basura (garbage collector). En su lugar, cada valor tiene un único dueño, y el compilador comprueba el ownership (propiedad) y el borrowing (préstamo) antes de que tu programa se ejecute. Rust tampoco tiene null (usas Option) ni excepciones (usas Result). Estos tres cambios son los que más tiempo de aprendizaje te van a llevar. Esta página traduce a Rust lo que ya conoces de C# y .NET, para que dediques ese tiempo a lo que de verdad es nuevo.
Si prefieres una comparación general con C++, Go y Python, consulta Rust vs C++, Go y Python.
Conceptos del lenguaje: de C# a Rust
Sección titulada «Conceptos del lenguaje: de C# a Rust»Usa esta tabla como un diccionario de bolsillo. La última columna enlaza con la lección de este sitio que explica la parte de Rust.
| C# | Rust | Para saber más |
|---|---|---|
class con campos y métodos |
struct para los datos + un bloque impl para los métodos |
Structs |
interface |
trait |
Traits |
| Clase abstracta, herencia | No hay herencia. Usa composición (un struct dentro de otro) + traits con métodos por defecto | Traits, Patrones de diseño |
List<IShape> con clases distintas |
Vec<Box<dyn Shape>> (un trait object) |
Trait objects |
| Métodos de extensión | Tu propio trait, implementado para un tipo que no escribiste tú (como String) |
Traits |
Genéricos con where T : IComparable<T> |
Genéricos con trait bounds (restricciones): T: Ord o where T: Ord |
Genéricos |
null, Nullable<int>, tipos de referencia anulables |
Option<T>: o bien Some(value) o bien None |
Option |
?? y ?. |
.unwrap_or(...), .map(...), .and_then(...) y ? |
Option |
Excepciones, throw, try/catch |
Result<T, E> + el operador ? |
Result, Errores personalizados |
Una excepción para un bug de verdad (como InvalidOperationException) |
panic! (detiene el hilo) |
Panic |
IDisposable + using |
Drop, que se ejecuta automáticamente cuando el dueño sale del ámbito (RAII) |
Ámbito, Punteros inteligentes |
| Recolector de basura | Ownership y borrowing, comprobados al compilar | Ownership, Borrowing |
record |
struct con #[derive(Debug, Clone, PartialEq)] |
Structs, Traits comunes |
Expresión with en un record |
Sintaxis de actualización de structs: Order { total: 5, ..old } |
Structs |
enum (solo enteros con nombre) |
enum donde cada variante puede llevar sus propios datos |
Enums |
Expresiones switch, patrones is |
match (el compilador comprueba que cubriste todos los casos) |
Match, Coincidencia de patrones |
Tuplas (int, string) |
Tuplas (i32, String) |
Tuplas |
Parámetros ref / out |
&mut T, o devolver una tupla o un Option |
Borrowing |
Lambdas, Func<T>, Action<T>, delegados |
Closures, descritas por los traits Fn, FnMut, FnOnce |
Closures, Fn, FnMut y FnOnce |
event |
Un canal, o una lista de closures en un Box (callbacks) |
Canales, Fn, FnMut y FnOnce |
LINQ: Where, Select, Sum, OrderBy, ToList |
Adaptadores de iteradores: filter, map, sum, sort_by (en un Vec), collect |
Iteradores, Ordenar y buscar |
IEnumerable<T> y yield return |
impl Iterator<Item = T>, o tu propio tipo que implemente Iterator |
Iteradores personalizados |
async Task<T> / await |
async fn devuelve un Future; necesitas un runtime como tokio para ejecutarlo |
Async/Await |
Task.Run, Thread, lock |
std::thread::spawn o tokio::spawn; Mutex<T> envuelve los datos que protege |
Concurrencia, Estado compartido |
Propiedades (get; set;) |
No hay propiedades. Usa campos pub, o métodos getter y setter |
Structs |
string y StringBuilder |
&str (una vista prestada de un texto) y String (con dueño, puede crecer) |
Strings |
Interpolación de cadenas $"Hi {name}" |
format!("Hi {name}") y println!("Hi {name}") |
Salida en Rust |
var |
let (inmutable por defecto; escribe let mut para permitir cambios) |
Variables |
const, readonly |
const, y un let normal (ya es de solo lectura) |
Variables |
Campo static readonly |
static, o LazyLock para valores que se crean al arrancar |
Variables |
namespace y directivas using |
Módulos (mod) y use |
Módulos |
| Ensamblado / proyecto | Crate (una biblioteca o un binario) | Cargo y crates |
Atributos como [Serializable] |
Atributos como #[derive(Serialize)] y macros derive |
Macros, JSON con Serde |
| Reflexión | Casi nada en tiempo de ejecución. Las macros y derive generan código al compilar |
Macros |
unsafe y punteros |
Bloques unsafe y punteros crudos (raw pointers) |
Unsafe en Rust |
Span<T>, ReadOnlySpan<char> |
Slices: &[T] y &str |
Slices |
Sobrescribir ToString(), IFormattable |
Implementar Display (y .to_string() funciona gratis) |
Traits comunes |
IEquatable<T>, IComparable<T>, GetHashCode |
PartialEq/Eq, PartialOrd/Ord, Hash (normalmente con derive) |
Traits comunes |
Operadores de conversión implicit / explicit |
From/Into (y TryFrom cuando puede fallar) |
Conversiones |
Sobrecarga de operadores (operator +) |
Implementar traits de std::ops, como Add |
Traits avanzados |
Aritmética checked / unchecked |
checked_add, wrapping_add, saturating_add |
Desbordamiento de enteros |
decimal |
No hay un tipo decimal incorporado; usa el crate rust_decimal, o guarda céntimos en un entero |
Tipos de datos |
Colecciones
Sección titulada «Colecciones»La biblioteca estándar cubre casi todo lo que hay en System.Collections.Generic. Cambian los nombres, no las ideas.
| C# | Rust | Para saber más |
|---|---|---|
List<T> |
Vec<T> |
Vectores |
T[] (longitud fija) |
[T; N] cuando la longitud se conoce al compilar; si no, Vec<T> |
Arrays |
Dictionary<K, V> |
HashMap<K, V> |
HashMap |
SortedDictionary<K, V> |
BTreeMap<K, V> (las claves se mantienen ordenadas) |
HashMap |
HashSet<T> |
HashSet<T> |
HashSet |
SortedSet<T> |
BTreeSet<T> |
HashSet |
Queue<T> |
VecDeque<T> (push_back / pop_front) |
VecDeque y BinaryHeap |
Stack<T> |
Vec<T> con push / pop |
Vectores |
PriorityQueue<TElement, TPriority> |
BinaryHeap<T> (primero el mayor; envuélvelo en Reverse para que salga primero el menor) |
VecDeque y BinaryHeap |
LinkedList<T> |
LinkedList<T> existe, pero rara vez lo necesitas; VecDeque suele ser más rápido |
VecDeque y BinaryHeap |
ConcurrentDictionary<K, V> |
Mutex<HashMap<K, V>>, o el crate dashmap |
Estado compartido |
Un detalle a tener en cuenta: igual que Dictionary, un HashMap de Rust no tiene un orden fijo. Si imprimes su contenido, ordénalo antes o usa un BTreeMap.
Herramientas: de dotnet a cargo
Sección titulada «Herramientas: de dotnet a cargo»Esta es la parte que más le gusta desde el primer momento a quien viene de C#. Cargo hace el trabajo de la CLI de dotnet, de MSBuild y de NuGet en una sola herramienta.
| .NET | Rust | Notas |
|---|---|---|
CLI de dotnet |
cargo |
Compila, ejecuta, prueba, añade paquetes y publica. Consulta Cargo y crates |
.csproj |
Cargo.toml |
Corto y fácil de editar a mano |
packages.lock.json |
Cargo.lock |
Se crea solo |
.sln con varios proyectos |
Un workspace de Cargo | Un Cargo.toml en la raíz enumera los crates miembros |
| NuGet / nuget.org | crates.io | La documentación de la API de cada crate publicado está en docs.rs |
dotnet add package Serilog |
cargo add tracing |
|
dotnet new console |
cargo new my_app (cargo new --lib para una biblioteca) |
|
dotnet run / build / test |
cargo run / cargo build / cargo test |
|
-c Release |
--release |
Las compilaciones debug son mucho más lentas al ejecutarse; mide el rendimiento en release |
dotnet watch |
cargo watch o bacon (se instalan con cargo install) |
|
dotnet tool install -g |
cargo install |
|
global.json (versión del SDK) |
rustup y un archivo rust-toolchain.toml |
|
| Analizadores de Roslyn | cargo clippy |
Cientos de lints útiles, oficiales |
dotnet format |
cargo fmt |
Un único estilo estándar para todos |
| Visual Studio / Rider | VS Code + rust-analyzer, o JetBrains RustRover | rust-analyzer ofrece autocompletado, ir a la definición y errores en línea |
Comentarios de documentación XML /// |
Comentarios de documentación /// en Markdown + cargo doc |
Los ejemplos de código de la documentación se ejecutan como tests. Consulta Documentación en Rust |
| xUnit / NUnit / MSTest | #[test] y cargo test, incluidos de serie (cargo nextest es un ejecutor más rápido muy popular) |
Consulta Pruebas en Rust |
| Moq | mockall, o implementaciones falsas escritas a mano que implementan tu trait |
|
| Coverlet (cobertura de código) | cargo llvm-cov o cargo tarpaulin |
|
| BenchmarkDotNet | criterion |
Consulta Rendimiento en Rust |
| dotnet-trace, profilers | cargo flamegraph, perf o cualquier profiler nativo |
Aquí tienes un Cargo.toml pequeño, para que lo compares con un .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 y frameworks: “¿Qué usamos en lugar de…?”
Sección titulada «Bibliotecas y frameworks: “¿Qué usamos en lugar de…?”»Esta es la primera pregunta de cualquier equipo de .NET. “Usamos Entity Framework. ¿Qué tenemos en Rust?” Aquí tienes los equivalentes más cercanos. Detrás de Rust no hay una sola empresa que marque un “stack por defecto”, como hace Microsoft con .NET, así que a menudo eliges entre dos o tres buenos crates de la comunidad.
Acceso a datos
Sección titulada «Acceso a datos»| .NET | Rust | Notas |
|---|---|---|
| Entity Framework Core | Diesel: ORM y constructor de consultas; tus consultas se comprueban por tipos al compilar. Migraciones con diesel_cli |
Maduro (2.x), síncrono por defecto (existe diesel-async) |
| Entity Framework Core (asíncrono, estilo entidades) | SeaORM: ORM asíncrono con entidades, relaciones y migraciones (sea-orm-cli) |
Es el que más se parece a EF; está construido sobre sqlx |
| Dapper | sqlx: escribes SQL normal, y la macro query! lo comprueba contra tu base de datos real al compilar. Migraciones con sqlx-cli |
Asíncrono, muy popular, no es un ORM |
| ADO.NET | sqlx, tokio-postgres, rusqlite (SQLite) |
Drivers de más bajo nivel |
| StackExchange.Redis | redis |
Si te gustaba EF, prueba primero SeaORM. Si te gustaba Dapper y escribir tu propio SQL, prueba sqlx. Si quieres las garantías más fuertes al compilar, prueba Diesel. Bases de datos en Rust muestra los tres con código que funciona.
Web y redes
Sección titulada «Web y redes»| .NET | Rust | Notas |
|---|---|---|
| ASP.NET Core Web API | axum (construimos una en Proyecto: API web con Axum), actix-web, Rocket | axum y actix-web se usan mucho en producción |
| Minimal APIs | axum: las rutas son simples handlers async fn |
El estilo es muy parecido |
| Pipeline de middleware | Capas de tower y el crate tower-http (CORS, compresión, trazas, timeouts) |
axum usa tower directamente |
| Inyección de dependencias incorporada | State en axum, constructores normales y traits |
Mira la fila del contenedor de DI más abajo |
| Swashbuckle / OpenAPI | utoipa |
|
| SignalR | WebSockets en axum (la feature ws), o tokio-tungstenite |
No trae una capa de protocolo al estilo SignalR |
| gRPC (Grpc.AspNetCore) | tonic (con prost para protobuf) |
|
HttpClient |
reqwest |
Asíncrono por defecto, con un cliente bloqueante opcional |
| Autenticación JWT bearer | jsonwebtoken |
Piezas del día a día
Sección titulada «Piezas del día a día»| .NET | Rust | Notas |
|---|---|---|
| System.Text.Json / Newtonsoft.Json | serde + serde_json |
El estándar; también sirve para TOML, YAML y más. Consulta JSON con Serde |
ILogger / Serilog |
tracing (+ tracing-subscriber), o log + env_logger |
Logging estructurado con spans. Consulta Logging en Rust |
IConfiguration / appsettings.json |
config o figment |
Combina archivos y variables de entorno, y luego deserializa con serde |
Contenedor de DI (IServiceCollection) |
Normalmente nada: pasa las dependencias a los constructores y usa traits para lo que quieras cambiar en los tests | Existe shaku, pero pocos proyectos usan un contenedor |
| AutoMapper | Escribe implementaciones de From / Into |
Explícito y comprobado por el compilador. Consulta Conversiones |
| FluentValidation / DataAnnotations | validator o garde (validación basada en derive) |
|
| Polly (reintentos) | backon, o el middleware de reintentos de tower |
|
| MediatR | No hay un equivalente directo | Traits normales, o canales entre tareas |
| Hangfire / tareas en segundo plano | tokio::spawn para tareas dentro del proceso; apalis para colas de trabajos persistentes |
|
CancellationToken |
Descarta (drop) el future, o usa CancellationToken de tokio-util |
Escritorio, juegos y más
Sección titulada «Escritorio, juegos y más»| .NET | Rust | Notas |
|---|---|---|
| Blazor WebAssembly | Leptos, Yew, Dioxus |
Todos antes de la versión 1.0; sus APIs aún cambian. Consulta Rust y WebAssembly |
| WPF / WinForms / MAUI | Tauri (interfaz web + backend en Rust), Slint, egui, Iced |
Tauri 2 y Slint 1.x son los más estables. No hay un toolkit de GUI oficial |
| Unity | Bevy, o Rust dentro de Godot con el crate godot |
Bevy aún no llega a la 1.0 y cambia su API entre versiones |
| ML.NET | candle, burn, linfa |
Mucho más jóvenes que las herramientas de ML de Python |
| Azure SDK for .NET | El Azure SDK for Rust oficial: azure_core, azure_identity, azure_storage_blob, … |
Cubre menos servicios que el SDK de .NET |
Lado a lado: una clase se convierte en struct + impl
Sección titulada «Lado a lado: una clase se convierte en struct + impl»Veamos código real. Aquí tienes una pequeña cuenta bancaria en C#:
var account = new BankAccount("Ada", 100);account.Deposit(50);Console.WriteLine($"{account.Owner} tiene {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;}Y en Rust:
struct BankAccount { owner: String, balance: i64,}
impl BankAccount { // `new` es solo una convención, no una palabra clave. fn new(owner: &str, balance: i64) -> Self { BankAccount { owner: owner.to_string(), balance } }
// `&mut self`: este método modifica la cuenta. fn deposit(&mut self, amount: i64) { self.balance += amount; }
// `&self`: este método solo lee. fn balance(&self) -> i64 { self.balance }}
fn main() { let mut account = BankAccount::new("Ada", 100); account.deposit(50); println!("{} tiene {}", account.owner, account.balance());}Salida:
Ada tiene 150Los datos y el comportamiento viven en dos bloques separados. No hay constructores, solo funciones que devuelven Self. Cada método dice desde el principio si lee (&self) o modifica (&mut self) el valor. Y como deposit modifica la cuenta, la variable tiene que ser let mut. En C#, cualquier método puede cambiar cualquier campo; en Rust, los cambios se ven en la firma.
Lado a lado: una interfaz se convierte en trait
Sección titulada «Lado a lado: una interfaz se convierte en trait»En C# podrías tener una interfaz IShape con dos clases y una 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;}En Rust, la interfaz es un trait, y cada struct lo implementa en su propio bloque 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> hace el papel de "una referencia a algún 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()); }}Salida:
3.146.00En C#, cada llamada a través de una interfaz es una llamada virtual. En Rust eliges tú: Box<dyn Shape> usa una llamada virtual (consulta Trait objects), mientras que una función genérica como fn print_area<T: Shape>(s: &T) se compila por separado para cada tipo, sin ninguna llamada virtual. Un extra muy práctico: puedes implementar tu propio trait para tipos que no escribiste tú, incluso String o i32. Así es como Rust hace lo que en C# hacen los métodos de extensión.
Lado a lado: null se convierte en Option
Sección titulada «Lado a lado: null se convierte en Option»C# con tipos de referencia anulables te avisa sobre null, pero null sigue existiendo en tiempo de ejecución:
var email = FindEmail(2);if (email is not null) Console.WriteLine(email.ToUpper());else Console.WriteLine("no hay email registrado");
Console.WriteLine(FindEmail(2) ?? "desconocido");En Rust, un valor que puede faltar tiene el tipo Option<T>. No puedes usarlo como un valor normal hasta que hayas tratado el 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!("no hay email registrado"), }
// `if let` es la forma corta cuando solo te importa un caso. if let Some(email) = find_email(2) { println!("encontrado: {email}"); } else { println!("no hay email registrado"); }
// unwrap_or funciona como el operador ?? de C#. let email = find_email(2).unwrap_or("desconocido".to_string()); println!("{email}");}Salida:
no hay email registradodesconocidoEn Rust no existe la NullReferenceException, porque no hay ningún null que desreferenciar. El compilador simplemente no te deja llamar a .to_uppercase() sobre un Option<String>. Aprende los métodos auxiliares en la página de Option, y las formas cortas en If Let / While Let en Rust.
Lado a lado: try/catch se convierte en Result y ?
Sección titulada «Lado a lado: try/catch se convierte en Result y ?»En C#, al convertir texto en número se puede lanzar una excepción, y la capturas en algún punto más arriba de la pila de llamadas:
static int TotalSeats(string a, string b) => int.Parse(a) + int.Parse(b);
try{ Console.WriteLine(TotalSeats("12", "30")); Console.WriteLine(TotalSeats("12", "treinta"));}catch (FormatException e){ Console.WriteLine($"Error: {e.Message}");}En Rust, una función que puede fallar lo dice en su tipo de retorno. El operador ? significa “si esto falló, devuelve el error a quien me llamó, ahora mismo”. Hace el mismo trabajo que dejar que una excepción suba por la pila, pero lo ves en el 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", "treinta")] { match total_seats(a, b) { Ok(total) => println!("Total: {total}"), Err(e) => println!("Error: {e}"), } }}Salida:
Total: 42Error: invalid digit found in stringEl mensaje “invalid digit found in string” (“se encontró un dígito no válido en la cadena”) sale en inglés porque lo escribe la biblioteca estándar de Rust. La gran diferencia: en C#, cualquier método puede lanzar una excepción y la firma no te lo dice. En Rust, Result forma parte de la firma, así que quien llama no puede olvidarse de tratarlo. Para tus propios tipos de error, consulta Errores personalizados. En aplicaciones, el crate anyhow te da algo parecido a “capturar cualquier excepción” (consulta anyhow y thiserror).
Lado a lado: LINQ se convierte en una cadena de iteradores
Sección titulada «Lado a lado: LINQ se convierte en una cadena de iteradores»Una 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($"Ingresos: {orders.Sum(o => o.Total)}");
record Order(string Customer, int Total);La versión en Rust usa adaptadores de iteradores y 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(...): se ordena en un Vec, no en el 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!("Ingresos: {revenue}");}Salida:
Linus, Ken, AdaIngresos: 550Los iteradores de Rust son perezosos (lazy), igual que la ejecución diferida de LINQ: no se ejecuta nada hasta que llamas a algo como collect(), sum() o usas un bucle for. collect() es tu ToList(). A diferencia de LINQ, las cadenas de iteradores normalmente se compilan al mismo código máquina que un bucle escrito a mano, así que no pagas nada por el estilo elegante. Tienes más sobre ordenación en la página Ordenar y buscar.
Lado a lado: using/IDisposable se convierte en Drop
Sección titulada «Lado a lado: using/IDisposable se convierte en Drop»En C#, tienes que acordarte de usar using para liberar un recurso a tiempo:
using (var conn = new Connection("orders-db")){ Console.WriteLine($"consultando {conn.Name}");}Console.WriteLine("después del bloque");
class Connection : IDisposable{ public string Name { get; } public Connection(string name) => Name = name; public void Dispose() => Console.WriteLine($"cerrando la conexión con {Name}");}En Rust, la limpieza es automática. Cuando el dueño de un valor sale del ámbito (scope), Rust llama a su implementación de Drop. No hay nada que olvidar:
struct Connection { name: String,}
impl Drop for Connection { fn drop(&mut self) { println!("cerrando la conexión con {}", self.name); }}
fn main() { { let conn = Connection { name: "orders-db".to_string() }; println!("consultando {}", conn.name); } // `conn` sale del ámbito aquí, así que se ejecuta drop().
println!("después del bloque");}Salida:
consultando orders-dbcerrando la conexión con orders-dbdespués del bloqueEsto se llama RAII, y es la forma en que Rust lo gestiona todo: archivos, sockets, locks y la propia memoria. Un archivo se cierra cuando su valor File se descarta (drop). Un Mutex se desbloquea cuando se descarta su guard. No hay finalizadores ni pausas del recolector de basura; la limpieza ocurre en un momento predecible.
Lo que te va a hacer tropezar si vienes de C#
Sección titulada «Lo que te va a hacer tropezar si vienes de C#»Todo el que llega desde C# se encuentra con los mismos baches. Conocerlos de antemano ayuda.
-
Asignar mueve el valor, no copia una referencia. En C#,
var b = a;con una clase te da dos referencias al mismo objeto. En Rust,let b = a;mueve el ownership de unStringo unVecab, yaya no se puede usar:fn main() {let owner = String::from("Ada");let copy = owner;println!("{owner} y {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} y {copy}");| ^^^^^ value borrowed here after moveEl compilador te dice que
ownerse movió en la línea 3 (“value moved here”) y que después intentas usarlo en la línea 4. La solución es tomarlo prestado (let copy = &owner;) o clonarlo a propósito (owner.clone()). Lee primero Ownership; ahí se explica esta regla. -
El borrow checker (comprobador de préstamos). Puedes tener muchos lectores (
&T) o un solo escritor (&mut T) a la vez, nunca las dos cosas. Patrones normales en C#, como objetos que apuntan a su padre o muchos objetos que modifican un mismo objeto compartido, necesitan punteros inteligentes o un diseño distinto. Cuenta con unas semanas frustrantes. Luego se vuelve mucho más fácil. -
Stringfrente a&str. C# tiene un solo tipostring. Rust tiene dos:Stringes dueño de su texto y&strlo toma prestado. Las funciones suelen recibir&stry los structs suelen guardarString. Consulta Strings en Rust. -
No hay herencia. No puedes escribir una clase base con campos compartidos y métodos que se sobrescriben. Usa traits (con métodos por defecto) para el comportamiento compartido, y pon los datos compartidos en un struct que otros structs contengan.
-
No hay excepciones. Los errores son valores. Vas a escribir
?muchas veces, y tumaintambién puede devolverResult. Llamar a.unwrap()está bien mientras aprendes, pero es la versión de Rust de “ignora la excepción y deja que el programa se caiga”. -
No hay null. Al principio, tener
Optionpor todas partes parece trabajo extra. Con el tiempo te darás cuenta de que ya no tienes errores de referencias nulas. -
Clones explícitos. Rust nunca copia un valor del heap a tus espaldas. Si ves
.clone(), se hace una copia real. Es una ventaja: el coste se ve. -
Tiempos de compilación. Una compilación desde cero de un proyecto con muchos crates tarda más que una compilación de .NET. Las compilaciones incrementales y
cargo check(que se salta la generación de código) son mucho más rápidas; usacargo checkmientras editas. -
Async necesita un runtime. En .NET, el runtime y el pool de hilos siempre están ahí. En Rust, una
async fnno hace nada hasta que un ejecutor la ejecuta, así que añades tokio y pones#[tokio::main]enmain. Consulta Async/Await en Rust.
Una ruta de aprendizaje para desarrolladores de C#
Sección titulada «Una ruta de aprendizaje para desarrolladores de C#»No necesitas leer este sitio en orden. Si vienes de C#, esta ruta se salta lo que ya sabes y dedica el tiempo a lo que Rust hace distinto:
- Empezar con Rust: instala Rust y ejecuta
cargo new. - Variables y Tipos de datos: léelas por encima, sobre todo para aprender
let muty los tipos numéricos. - Ownership, luego Borrowing y luego Slices: las páginas más importantes para ti. Tómate tu tiempo.
- Strings:
Stringfrente a&str. - Structs y Enums, luego Match y Coincidencia de patrones.
- Option y Result, luego Errores personalizados.
- Traits, Genéricos, Traits comunes y Trait objects.
- Closures e Iteradores: tu LINQ.
- Vectores, HashMap y VecDeque y BinaryHeap.
- Módulos, Cargo y crates y Pruebas.
- Lifetimes y Punteros inteligentes.
- Concurrencia, Estado compartido y Async/Await.
- JSON con Serde y Logging, y después construye el proyecto de API web.
Práctica
Sección titulada «Práctica»Los dos ejercicios te dan un fragmento de C#. Pásalo a Rust.
1. Pasa a Rust un record y una consulta LINQ
Sección titulada «1. Pasa a Rust un record y una consulta LINQ»Pasa a Rust este código de C#. Debe imprimir los nombres de los productos que hay en stock y cuestan menos de 20. Pista: crea un struct con #[derive(Debug, Clone, PartialEq)] y luego usa filter, map y collect para obtener un Vec<&str>. Vec<&str> tiene un método .join(", ").
var products = new List<Product>{ new("Cuaderno", 5, true), new("Auriculares", 45, true), new("Bolígrafo", 2, false), new("Taza", 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 solución
#[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("Cuaderno", 5, true), product("Auriculares", 45, true), product("Bolígrafo", 2, false), product("Taza", 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(", "));}Salida:
Cuaderno, TazaUna pequeña función auxiliar hace el papel del constructor posicional del record. Los derives te dan lo que un record de C# te da gratis: imprimir con {:?}, copiar con .clone() y comparar con ==.
2. Pasa a Rust un Dictionary con excepciones
Sección titulada «2. Pasa a Rust un Dictionary con excepciones»Este código de C# saca una unidad de un artículo de un diccionario de stock y lanza una excepción cuando algo va mal. Pásalo a Rust de forma que take devuelva un Result<u32, String> en lugar de lanzar excepciones. Pista: stock.get_mut(item) devuelve un Option<&mut u32>. Conviértelo en un Result con .ok_or(...) y luego usa ?.
var stock = new Dictionary<string, int> { ["manzanas"] = 3, ["peras"] = 0 };
int Take(string item){ if (!stock.TryGetValue(item, out var count)) throw new KeyNotFoundException($"no hay ningún artículo llamado {item}"); if (count == 0) throw new InvalidOperationException($"no quedan {item}"); stock[item] = count - 1; return count - 1;}
foreach (var item in new[] { "manzanas", "peras", "kiwis" }){ try { Console.WriteLine($"se sacó una unidad de {item}, quedan {Take(item)}"); } catch (Exception e) { Console.WriteLine($"error: {e.Message}"); }}Ver solución
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!("no hay ningún artículo llamado {item}"))?;
if *count == 0 { return Err(format!("no quedan {item}")); }
*count -= 1; Ok(*count)}
fn main() { let mut stock = HashMap::new(); stock.insert("manzanas".to_string(), 3); stock.insert("peras".to_string(), 0);
for item in ["manzanas", "peras", "kiwis"] { match take(&mut stock, item) { Ok(left) => println!("se sacó una unidad de {item}, quedan {left}"), Err(e) => println!("error: {e}"), } }}Salida:
se sacó una unidad de manzanas, quedan 2error: no quedan peraserror: no hay ningún artículo llamado kiwisTryGetValue con un parámetro out se convierte en get_mut, que devuelve un Option. Los dos throw se convierten en valores Err, y el try/catch se convierte en un match. El &mut stock de la llamada deja claro que take modifica el diccionario.
Resumen
Sección titulada «Resumen»- Mucho de C# te sirve en Rust: los genéricos, async/await, las lambdas, la coincidencia de patrones y los iteradores al estilo LINQ tienen equivalentes muy parecidos.
- Los grandes cambios son el ownership en lugar de un recolector de basura,
Optionen lugar de null, yResult+?en lugar de excepciones. - Las clases se convierten en un
struct+impl, las interfaces en traits y la herencia en composición más traits.using/IDisposablese convierte en elDropautomático. - Cargo sustituye a la CLI de
dotnet, a MSBuild y a NuGet; clippy y rustfmt sustituyen a los analizadores y adotnet format; los tests vienen incluidos. - En lugar de EF Core, mira SeaORM o Diesel; en lugar de Dapper, sqlx; y en lugar de ASP.NET Core, axum o actix-web. Las opciones para GUI, juegos y ML son más jóvenes.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué usa Rust en lugar de null?
2. Tu equipo usa Dapper y le gusta escribir el SQL a mano. ¿Qué crate de Rust se le parece más?
3. ¿Cuál es el equivalente más cercano en Rust del bloque using de C# con IDisposable?
4. En C#, var b = a; con una clase copia una referencia. ¿Qué hace let b = a; con un String en Rust?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Rust for C#/.NET Developers — Microsoft (en inglés): la guía de la propia Microsoft, con muchos más ejemplos de C# y Rust lado a lado.
- El lenguaje de programación Rust: el libro oficial, en su traducción de la comunidad. Los capítulos 4 (ownership), 9 (errores) y 10 (genéricos y traits) son los más importantes si vienes de C#.
- Getting Started with Diesel y la documentación de SeaORM (en inglés): los dos ORM principales, si buscas algo como Entity Framework.
- sqlx en docs.rs (en inglés): la opción al estilo Dapper, con SQL comprobado al compilar.
- Are We Web Yet? y Are We GUI Yet? (en inglés): resúmenes de la comunidad sobre las bibliotecas de Rust para web y para interfaces gráficas de escritorio.