Basics
Variables & Mutability
Las variables en Rust son inmutables por defecto — esta es una característica de seguridad central que previene mutación accidental. Usa 'mut' solo cuando genuinamente necesites cambiar un valor. 'const' requiere tipo explícito y se inserta en línea en tiempo de compilación, mientras que 'static' tiene una dirección de memoria fija con un lifetime 'static.
let x = 5; // immutable by default
let mut y = 10; // mutable with 'mut'
y += 1; // OK, y is mutable
const MAX: u32 = 100; // compile-time constant
static GREETING: &str = "Hi"; // global static
let z: i32 = -5; // explicit type annotationShadowing
El shadowing te permite reusar un nombre de variable mientras cambias su tipo o valor. A diferencia de 'mut', el shadowing crea un nuevo binding — útil para transformar datos (p. ej., analizar un string a int) sin inventar un nuevo nombre. El valor antiguo se descarta después del nuevo binding.
let x = 5;
let x = x * 2; // shadow previous x, now 10
let x = "text"; // can even change type!
println!("{}", x); // "text"
// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changesComments & Printing
Usa /// para documentación de items (mostrada por 'cargo doc') y //! para docs de módulo/crate. println! escribe a stdout, eprintln! a stderr. El placeholder {} soporta especificaciones de formato: > para alineación derecha, < para izquierda, ^ para centro, .N para precisión, #b/#o/#x para binario/octal/hex.
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment
let name = "Alice";
println!("Hello, {}!", name); // format string
println!("{0} {1} {0}", "a", "b"); // positional args
println!("{:>5}", 42); // right-align, width 5
println!("{:.2}", 3.14159); // 2 decimal places: 3.14
println!("{:#b}", 0b1010); // binary: 0b1010
eprintln!("Error to stderr"); // error outputData Types Overview
Rust no tiene conversión implícita de tipos — usa 'as' para casts. i32 es el tipo entero por defecto, f64 el float por defecto. usize se usa para indexación y tamaños. char es un valor escalar Unicode completo (no un byte), así que '🦀' es un solo char.
// Scalar types
let a: i32 = 42; // signed 32-bit integer
let b: u64 = 100; // unsigned 64-bit
let c: f64 = 3.14; // 64-bit float (default)
let d: bool = true;
let e: char = '🦀'; // 4-byte Unicode scalar
// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();
// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3]; // [0, 0, 0]Type Casting & Aliases
El cast 'as' es sin comprobar y puede perder datos (p. ej., 300u16 as u8 se convierte en 44). Para conversiones seguras usa los traits From/Into que están implementados para casts sin pérdida. Los alias de tipo mejoran la legibilidad sin crear nuevos tipos — usa 'struct NewType(i32)' para un patrón newtype distinto.
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64; // 42.0
let z: u8 = 300u16 as u8; // truncated: 44
// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();
// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;Strings
String vs &str
La distinción entre String (owned, heap) y &str (slice prestado) es fundamental en Rust. Usa String cuando necesitas poseer/modificar/hacer crecer el texto; usa &str cuando solo necesitas leerlo. &str puede apuntar tanto al buffer de un String como a un literal de cadena en el binario. Prefiere &str en parámetros de función para flexibilidad.
// &str: immutable string slice (borrowed)
let s1: &str = "literal"; // stored in binary
let s2: &str = &s3[..2]; // slice of a String
// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world"); // append
s3.push('!'); // append char
s3.insert(0, '>'); // insert at index
println!("{}", s3); // >hello, world!
// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");String Methods
Los métodos de String retornan nuevos Strings owned en lugar de modificar in place (excepto los métodos push_*/insert en Strings mutables). split() retorna un iterador, así que usa .collect() para materializar. Ten en cuenta que la indexación s[0] NO está permitida en strings porque los límites UTF-8 no se alinean con los índices de bytes — usa s.chars().nth(0) en su lugar.
let s = String::from("Hello, World");
// inspection
println!("len: {}", s.len()); // 12
println!("is_empty: {}", s.is_empty()); // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true
// transformation
let upper = s.to_uppercase(); // HELLO, WORLD
let replaced = s.replace("o", "0"); // Hell0, W0rld
let trimmed = " hi ".trim().to_string(); // hi
// splitting
for word in s.split(", ") {
println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();Formatting & Concatenation
El operador + toma ownership del String izquierdo y pide prestado el derecho (&str). Por eso s1 se vuelve inválido después de s1 + &s2. Para encadenar múltiples strings, prefiere format! que es más legible y no mueve ningún operando. concat! funciona solo con literales y produce un &'static str.
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);
// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2; // s1 moved, s2 borrowed
// s1 is now invalid!
let s4 = format!("{}{}", s2, s3); // neither moved
// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile timeIterating Characters & Bytes
Los strings de Rust están codificados en UTF-8, así que el índice de byte != índice de char. chars() itera valores escalares Unicode (O(n) para decodificar), bytes() itera bytes crudos. El slicing con [n..m] causa pánico si n o m caen en medio de un carácter multi-byte. Para acceso a nivel de byte, convierte a Vec<u8> vía as_bytes().
let s = "héllo"; // é is 2 bytes in UTF-8
// iterate over chars (Unicode scalar values)
for c in s.chars() {
print!("{} ", c); // h é l l o
}
// iterate over bytes
for b in s.bytes() {
print!("{} ", b); // 104 195 169 108 108 111
}
// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')
// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!Parsing & Conversion
parse() retorna Result porque el string podría no ser un número válido — siempre maneja el error. La sintaxis turbofish parse::<T>() te permite especificar el tipo inline. Convertir String a &str es gratis (solo un borrow), pero &str a String aloca memoria. collect() puede construir un String a partir de un iterador de chars.
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();
// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);
// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;
// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();Data Structures
Arrays & Slices
Los arrays [T; N] tienen un tamaño fijo conocido en tiempo de compilación y viven en el stack. Los slices &[T] son fat pointers (puntero + longitud) que piden prestado una secuencia contigua — permiten que las funciones acepten cualquier array o vector sin importar el tamaño. Usa slices en firmas de función para generalidad.
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());
// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3]; // [2, 3]
let full: &[i32] = &arr; // whole array
let first = &arr[..1]; // [1]
// iterating
for n in &arr {
println!("{}", n);
}Vectors (Vec<T>)
Vec<T> es el array crecible de Rust, respaldado por memoria heap con capacidad duplicante. push/pop son O(1) amortizado; insert/remove son O(n) porque los elementos se desplazan. Usa .get(i) en lugar de v[i] cuando quieres acceso seguro (retorna Option). into_iter() consume el vector, yielding valores owned.
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3]; // macro shorthand
v.push(4); // append
v.pop(); // remove last -> Option
v.insert(0, 0); // insert at index (O(n))
v.remove(0); // remove at index (O(n))
v.extend([5, 6]); // append multiple
// access
println!("{}", v[0]); // panics if out of bounds
println!("{:?}", v.get(0)); // Some(&4) — safe
// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; } // double each
let owned: Vec<i32> = v.into_iter().collect();HashMap & BTreeMap
HashMap usa hashing para acceso promedio O(1) pero no tiene orden. BTreeMap usa un B-tree para acceso O(log n) pero mantiene las claves ordenadas. Usa HashMap cuando necesitas lookups rápidos; usa BTreeMap cuando necesitas iteración ordenada o range queries. entry().or_insert() es la forma idiomática de 'upsert' — retorna una referencia mutable al valor, insertando un valor por defecto si está ausente.
use std::collections::HashMap;
use std::collections::BTreeMap;
// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)
// iterate (unordered)
for (name, score) in &scores {
println!("{}: {}", name, score);
}
// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebraHashSet & BTreeSet
HashSet almacena valores únicos con comprobaciones de pertenencia O(1). Las operaciones de conjunto (union, intersection, difference, symmetric_difference) retornan iteradores. Usa conjuntos para desduplicación, comprobación de pertenencia y operaciones matemáticas de conjuntos. BTreeSet es el equivalente ordenado respaldado por BTreeMap.
use std::collections::HashSet;
let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();
a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));
// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();Tuples & Destructuring
Las tuplas agrupan un número fijo de valores de tipos potencialmente diferentes. Accede a los campos con .0, .1, etc. El destructuring con patrones let es idiomático. El tipo unit () tiene un valor () y se usa como void en otros lenguajes. Las tuplas se usan comúnmente para retornar múltiples valores de funciones.
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");
// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);
// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);
// nested
let ((a, b), c) = ((1, 2), 3);
// unit tuple (empty)
let unit: () = ();
// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)Control Flow
If / Else If / Else
A diferencia de muchos lenguajes, 'if' en Rust es una expresión que retorna un valor. Esto elimina la necesidad de un operador ternario (condition ? a : b) — solo usa if/else. Ambas ramas deben retornar el mismo tipo. La condición NO necesita paréntesis, pero el cuerpo debe ser un bloque { }.
let score = 85;
// if is an expression — returns a value
let grade = if score >= 90 {
"A"
} else if score >= 80 {
"B"
} else if score >= 70 {
"C"
} else {
"F"
};
println!("Grade: {}", grade);
// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!Loops: loop, while, for
loop crea un bucle infinito — break lo sale y puede retornar un valor (break value). while comprueba una condición antes de cada iteración. for es el bucle más común, iterando sobre rangos, arrays, vectores, iteradores. Usa 'continue' para saltar a la siguiente iteración. Rangos: a..b es exclusivo, a..=b es inclusivo.
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
count += 1;
if count == 10 {
break count * 2; // break with value
}
};
println!("{}", result); // 20
// while: condition-checked loop
let mut n = 5;
while n > 0 {
println!("{}", n);
n -= 1;
}
// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); } // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }Match (Pattern Matching)
match es la poderosa construcción de pattern matching de Rust. Debe ser exhaustivo (todas las posibilidades cubiertas) — usa _ como catch-all. Los patrones soportan literales, rangos (..=), or-patterns (|), bindings y guards (if). match es una expresión y retorna un valor. Es la forma idiomática de manejar enums como Option y Result.
let coin = 25;
match coin {
25 => println!("quarter"),
10 => println!("dime"),
5 => println!("nickel"),
1 => println!("penny"),
_ => println!("unknown"), // catch-all (required)
}
// matching with bindings
let x = 3;
match x {
1 | 2 => println!("one or two"), // or-pattern
3..=9 => println!("single digit"), // range
n if n % 2 == 0 => println!("even: {}", n), // guard
_ => println!("other"),
}
// match is exhaustive — all cases must be coveredIf Let & While Let
if let es azúcar sintáctico para match cuando solo te importa una variante. Es menos verboso pero menos exhaustivo que match — úsalo cuando los otros casos no importan. while let hace bucle mientras el patrón coincida, comúnmente usado con iteradores (next() retorna Option). La rama else es opcional.
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);
// verbose: match
match maybe {
Some(x) => println!("got {}", x),
None => println!("nothing"),
}
// concise: if let
if let Some(x) = maybe {
println!("got {}", x);
} else {
println!("nothing");
}
// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
println!("{}", n);
}Break, Continue & Labels
Las labels (comilla simple + nombre) permiten romper o continuar bucles externos desde dentro de bucles anidados. Esto es esencial cuando necesitas salir de múltiples niveles de bucle a la vez. Sin labels, break/continue solo afectan el bucle más interno. Las labels son una alternativa limpia a las variables flag.
// continue: skip to next iteration
for i in 0..10 {
if i % 2 == 0 { continue; }
println!("{}", i); // prints odd numbers
}
// break: exit loop
for i in 0..10 {
if i == 5 { break; }
println!("{}", i); // prints 0..4
}
// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
for j in 0..3 {
if i == 1 && j == 1 {
break 'outer; // breaks the outer loop
}
println!("{} {}", i, j);
}
}Functions & Closures
Defining Functions
Las funciones usan la palabra clave 'fn'. La última expresión sin punto y coma es el valor de retorno (expresión). Añadir un punto y coma la convierte en una sentencia que retorna (). Usa 'return' explícito solo para salidas tempranas. El tipo de retorno ! marca funciones divergentes que nunca retornan (bucles infinitos, panics, salidas de proceso).
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = expression = return value
}
// statements (with semicolon) return ()
fn greet(name: &str) {
println!("Hi, {}", name);
// implicit return ()
}
// explicit return
fn abs(x: i32) -> i32 {
if x < 0 {
return -x; // early return needs 'return'
}
x // tail expression
}
// diverging function (never returns)
fn forever() -> ! {
loop {}
}Parameters & Arguments
Rust no tiene sobrecarga de funciones ni parámetros opcionales (usa genéricos o builders en su lugar). Elige los tipos de parámetro cuidadosamente: &T para acceso de lectura, &mut T para acceso de escritura, T para transferencia de ownership. Los slices (&[T]) son la forma idiomática de aceptar secuencias de longitud variable. Los argumentos por defecto no están soportados — usa el patrón builder o Option<T>.
// immutable borrow
fn len(s: &String) -> usize { s.len() }
// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }
// take ownership
fn consume(s: String) { println!("{}", s); }
// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }
// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10Closures
Las closures son funciones anónimas que pueden capturar su entorno. Se infieren por uso. Las closures capturan por referencia por defecto; 'move' fuerza la transferencia de ownership (esencial para threads). Las closures implementan los traits Fn (borrow), FnMut (mut borrow), o FnOnce (consume), permitiendo que se pasen como parámetros de función.
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3
// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };
// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15
// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();Higher-Order Functions & Iterators
Los iteradores de Rust son lazy — las operaciones no se ejecutan hasta que se llama a .collect() u otro método consumidor. Esto permite abstracción de coste cero: el compilador puede optimizar métodos de iterador encadenados en bucles eficientes. Métodos comunes: map (transformar), filter (seleccionar), fold (acumular), take (limitar), skip, enumerate, zip, flat_map.
let nums = vec![1, 2, 3, 4, 5];
// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// fold/reduce: accumulate
let sum: i32 = nums.iter().sum(); // 15
let product: i32 = nums.iter().product(); // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);
// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
.filter(|&&x| x > 1)
.map(|&x| x * x)
.collect(); // [4, 9, 16, 25]Function Pointers & Traits
fn (minúscula) es un tipo de puntero a función — coste cero, pero no puede capturar el entorno. Para closures que capturan, usa bounds genéricos <F: Fn(...)>. Fn pide prestado, FnMut pide prestado mutably, FnOnce consume. Los punteros a función son útiles para almacenar funciones en structs o pasar a C. Los genéricos con bounds Fn son más flexibles y aún de coste cero cuando se monomorfizan.
// function pointer type
type MathFn = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }
fn apply(f: MathFn, a: i32, b: i32) -> i32 {
f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12
// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7Ownership & Borrowing
Ownership Rules
El ownership es el sistema central de gestión de memoria de Rust — sin necesidad de garbage collector. Cuando asignas un valor heap (String, Vec), el ownership se MUEVE y la variable antigua se vuelve inválida. Los tipos de stack (i32, f64, bool, char, tuplas de tipos Copy) implementan Copy y se duplican en su lugar. Esto elimina los bugs use-after-free y double-free en tiempo de compilación.
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1; // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move
// Rule 2: When owner goes out of scope, value is dropped
{
let s = String::from("temp");
// s is valid here
} // s is automatically dropped (memory freed)
// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a; // a is copied, both valid
println!("{} {}", a, b); // OKBorrowing & References
El borrowing te permite usar un valor sin tomar ownership. &T crea una referencia inmutable — puedes tener muchas simultáneamente. &mut T crea una referencia mutable — pero solo UNA referencia mutable O cualquier número de referencias inmutables, nunca ambas. Esto previene data races en tiempo de compilación. Las referencias deben siempre apuntar a datos válidos (no dangling pointers).
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
s.len()
// s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s); // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid
// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, worldSlice References
Los slices son referencias a una porción contigua de una colección. Son 'fat pointers' que contienen un puntero y una longitud. Los string slices (&str) permiten que las funciones acepten tanto String como literales de cadena. Los array slices (&[T]) funcionan con cualquier secuencia contigua. Los slices piden prestado los datos subyacentes, previniendo que se modifiquen o descarten mientras el slice existe.
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5]; // "hello"
let world: &str = &s[6..]; // "world"
let full: &str = &s[..]; // "hello world"
// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]
// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[0..i];
}
}
&s[..]
}Lifetimes
Los lifetimes le dicen al compilador cuánto tiempo son válidas las referencias. La anotación 'a no cambia los lifetimes — describe relaciones. 'static es un lifetime especial que dura todo el programa (los literales de cadena lo tienen). La mayor parte del código usa lifetime elision (el compilador infiere). Necesitas lifetimes explícitos cuando: una función retorna una referencia, o un struct contiene una referencia.
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes
let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);
// struct holding references needs lifetime
struct Excerpt<'a> {
part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };Smart Pointers
Box<T> mueve datos al heap (único owner). Rc<T> habilita ownership compartida vía conteo de referencias (solo single-threaded). Arc<T> es la versión thread-safe usando atomics. RefCell<T> mueve el borrow checking al runtime, permitiendo mutación a través de referencias compartidas (interior mutability). Usa Box para tipos recursivos, Rc/Arc para estructuras tipo grafo, RefCell cuando necesitas mutar datos compartidos.
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;
// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b); // dereferenced automatically
// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2
// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);
// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtimeStructs, Enums & Traits
Defining Structs
Los structs agrupan campos relacionados. Los structs con campos nombrados son los más comunes. Los tuple structs son útiles cuando los nombres de campo no son significativos (Color, Point). Los unit structs no tienen datos y se usan para implementar traits. La sintaxis .. copia campos no especificados de otra instancia. Los structs se asignan en el stack a menos que contengan tipos heap (String, Vec, Box).
// named-field struct
struct User {
name: String,
age: u32,
active: bool,
}
let u = User {
name: String::from("Alice"),
age: 30,
active: true,
};
// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };
// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };
// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
// unit struct (no fields, useful for traits)
struct AlwaysEqual;Methods with impl
Los métodos van en bloques impl. &self pide prestado inmutablemente, &mut self pide prestado mutably, self toma ownership (consumiendo). Las funciones asociadas (sin parámetro self) son como métodos estáticos — llamadas con Type::function(). Self es un alias para el tipo. Se permiten múltiples bloques impl, útiles para dividir métodos por preocupación o compilación condicional.
struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
// associated function (constructor, no &self)
fn new(w: f64, h: f64) -> Self {
Rectangle { width: w, height: h }
}
// method (borrows self)
fn area(&self) -> f64 {
self.width * self.height
}
// mutable method
fn scale(&mut self, factor: f64) {
self.width *= factor;
self.height *= factor;
}
// consuming method (takes ownership)
fn into_square(self) -> Rectangle {
let side = (self.width + self.height) / 2.0;
Rectangle { width: side, height: side }
}
}
let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumedEnums & Pattern Matching
Los enums en Rust son tipos de datos algebraicos — cada variante puede llevar diferentes datos. Esto los hace mucho más poderosos que los enums de C. El pattern matching con match destructure las variantes y extrae sus datos. Usa enums cuando un valor puede ser una de varias formas distintas. La macro matches! es un atajo para matching de un solo patrón que retorna bool.
// enum with data (algebraic data type)
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields
Write(String), // tuple variant
ChangeColor(i32, i32, i32), // tuple variant
}
// pattern matching with destructuring
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
}
}
process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));
// enums can have methods too
impl Message {
fn is_quit(&self) -> bool {
matches!(self, Message::Quit)
}
}Option & Result
Option<T> reemplaza null — debes manejar explícitamente el caso None, eliminando bugs estilo NullPointerException. Result<T, E> es para operaciones que pueden fallar. Ambos tienen métodos ricos: map (transformar), and_then (encadenar), unwrap_or (por defecto), is_some/is_ok (comprobar). El operador ? en Result propaga errores automáticamente. Estos dos tipos son la columna vertebral del manejo de errores de Rust.
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
if id == 1 { Some(String::from("Alice")) }
else { None }
}
let user = find_user(1);
match user {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));
// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
match parse_num("42") {
Ok(n) => println!("Parsed: {}", n),
Err(e) => println!("Error: {}", e),
}Traits & Trait Bounds
Los traits definen comportamiento compartido (como interfaces en otros lenguajes). Los tipos implementan traits con 'impl Trait for Type'. Los traits pueden tener implementaciones de método por defecto. Los trait bounds (<T: Trait>) restringen los genéricos a tipos que implementan ciertos traits. La cláusula 'where' mejora la legibilidad para bounds complejos. Los traits habilitan polimorfismo vía tanto static dispatch (genéricos) como dynamic dispatch (trait objects &dyn Trait).
// define a trait (interface)
trait Summary {
fn summarize(&self) -> String;
// default method
fn author(&self) -> String {
String::from("Unknown")
}
}
struct Article { title: String, content: String }
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
// author() uses default implementation
}
let a = Article {
title: "Rust".into(),
content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown
// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}Derive Macros & Trait Objects
#[derive(...)] auto-implementa traits comunes: Debug (impresión debug), Clone (copia profunda), PartialEq/Eq (comparación ==), Hash (para claves de HashMap), Copy (copia de stack en lugar de move). Los trait objects (&dyn Trait o Box<dyn Trait>) habilitan polimorfismo en runtime vía vtable, con un pequeño coste de rendimiento. Usa genéricos para static dispatch (coste cero) cuando sea posible, trait objects cuando necesitas colecciones heterogéneas.
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone(); // Clone
println!("{:?}", p1); // Debug
println!("{}", p1 == p2); // PartialEq -> true
// trait object: dynamic dispatch
trait Animal {
fn sound(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }
// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog),
Box::new(Cat),
];
for a in &animals {
println!("{}", a.sound());
}Error Handling
Result & ? Operator
El operador ? es la forma idiomática de propagar errores. En Ok(v), lo desenvuelve a v. En Err(e), retorna Err(e) de la función inmediatamente. ? también convierte tipos de error vía el trait From, así las funciones que retornan Box<dyn Error> pueden usar ? con cualquier tipo de error. En Option, ? retorna None temprano. Esto hace el manejo de errores conciso sin sacrificar seguridad.
use std::fs;
use std::io;
use std::num::ParseIntError;
// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
let content = fs::read_to_string(path)?; // ? on io::Result
Ok(content.len() as i32)
}
// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // io::Error -> Box
let n: i32 = content.trim().parse()?; // ParseIntError -> Box
Ok(n)
}
// ? on Option too
fn first_char(s: &str) -> Option<char> {
s.lines().next()?.chars().next()
}Panic vs Result
Usa panic! para errores irrecuperables (bugs, invariantes violados) — indica un error de programación. Usa Result para fallos esperados y recuperables (entrada de usuario, file I/O, red). unwrap()/expect() causan pánico en error — aceptable en tests, prototipos o cuando puedes probar que el valor es válido. En código de producción, prefiere manejo de errores adecuado con ? y match.
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero!"); // unwinds stack
}
a / b
}
// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}
// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap(); // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg
// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)Custom Error Types
Los tipos de error personalizados te dan manejo de errores con seguridad de tipos y estructurado. Implementa Display (legible por humanos) y Error (para source chaining). Implementa From para cada error subyacente para que ? convierta automáticamente. Bibliotecas como thiserror (macro derive) o anyhow (cajas dinámicas de error) reducen el boilerplate. Usa thiserror para bibliotecas, anyhow para aplicaciones.
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
}
// implement Display (required by Error)
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(name) => write!(f, "Not found: {}", name),
}
}
}
// implement Error
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Parse(e) => Some(e),
_ => None,
}
}
}
// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}Matching & Combining Results
Los combinadores de Result permiten manejo de errores estilo funcional sin match. map transforma el valor Ok, map_err transforma el error, and_then encadena operaciones falibles (flatMap). unwrap_or proporciona un valor por defecto en error. is_ok/is_err comprueban sin consumir. Estos métodos hacen las cadenas de manejo de errores legibles y evitan anidamiento profundo.
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
Ok(n) => println!("Got: {}", n),
Err(e) => println!("Error: {}", e),
}
// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2); // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));
// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });
// unwrap_or family
let val = r1.unwrap_or(0); // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default
// check variants
println!("{}", r1.is_ok()); // true
println!("{}", r1.is_err()); // falseConverting Errors with From
El operador ? usa From para convertir errores. Box<dyn Error> implementa From para todos los errores estándar, haciéndolo un catch-all conveniente. El crate anyhow proporciona anyhow::Result que añade contexto (p. ej., .context("failed to read config")?). Para bibliotecas, define un enum de error específico con thiserror; para aplicaciones, usa anyhow por simplicidad.
use std::fs;
use std::num::ParseIntError;
// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
let content = fs::read_to_string(path)
.map_err(|e| e.to_string())?; // manual convert
let n: i32 = content.trim().parse()
.map_err(|e| e.to_string())?; // manual convert
Ok(n)
}
// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // auto-converts
let n: i32 = content.trim().parse()?; // auto-converts
Ok(n)
}
// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
// let n: i32 = something()?; // any error auto-converted
// Ok(n)
// }Modules & Crates
Module System
El sistema de módulos de Rust organiza el código. 'mod' declara un módulo (inline o vía archivo). 'pub' hace los items públicos (por defecto son privados). 'use' crea atajos a rutas. 'pub use' re-exporta items (útil para diseño de API). El sistema de archivos refleja el árbol de módulos: mod network puede ser src/network.rs o src/network/mod.rs. La raíz del crate es lib.rs (bibliotecas) o main.rs (binarios).
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
pub mod connection {
pub fn connect() -> bool { true }
fn disconnect() {} // private
}
}
// use: bring paths into scope
use network::connection::connect;
// calling with full path
fn main() {
network::connection::connect();
connect(); // after 'use'
}
// re-export with pub use
pub use network::connection::connect as open_connection;
// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;Privacy & Visibility
La privacidad en Rust tiene alcance de módulo. Los items son privados por defecto — solo accesibles dentro de su módulo definitorio y descendientes. 'pub' los hace públicos. 'pub(crate)' restringe al crate actual (útil para internos de biblioteca). 'pub(super)' restringe al módulo padre. Los campos de struct tienen visibilidad individual — un struct pub puede tener campos privados, requiriendo un constructor.
mod my_module {
// private by default
fn internal_helper() {}
// public: accessible from outside
pub fn public_api() {
internal_helper(); // can call private within module
}
// pub(crate): visible within this crate only
pub(crate) fn crate_wide() {}
// pub(super): visible to parent module
pub(super) fn parent_visible() {}
// struct fields are private even if struct is pub
pub struct Config {
pub name: String, // public field
secret: String, // private field
}
// enum variants inherit the enum's visibility
pub enum Status {
Active, // public because Status is public
Inactive,
}
}Use Statements & Aliasing
Las sentencias use traen items al scope, reduciendo la verbosidad de rutas. Las importaciones agrupadas (use std::io::{self, Read}) son más limpias que múltiples líneas. Los traits deben estar en scope para usar sus métodos — por esto a veces necesitas 'use std::io::Read' incluso si no referencias Read por nombre. Las importaciones globales (*) están desaconsejadas excepto para preludes.
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;
// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;
// aliasing with 'as'
use std::collections::HashMap as Map;
// glob import (use sparingly)
use std::prelude::v1::*;
// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait methodCargo & External Crates
Cargo es el gestor de paquetes y sistema de build de Rust. Las dependencias van en Cargo.toml bajo [dependencies]. Las features habilitan funcionalidad opcional (reduciendo tiempo de compilación/tamaño de binario). 'cargo add' auto-edita Cargo.toml. Los crates se publican en crates.io. La edition (2021) controla las características del lenguaje. Cargo maneja compilación, testing, documentación y publicación.
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
rand = "0.8"
# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;
#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }
fn main() {
let mut rng = rand::thread_rng();
let n: i32 = rng.gen_range(1..=100);
println!("{}", n);
}
# CLI commands:
# cargo new my_app # create new project
# cargo build # compile
# cargo run # compile + run
# cargo test # run tests
# cargo add serde # add dependency
# cargo update # update dependenciesTesting
Los tests usan el atributo #[test]. assert_eq!/assert_ne! comparan valores. #[should_panic] verifica que ocurra un pánico. Los tests pueden retornar Result para aserciones basadas en errores. #[cfg(test)] asegura que el módulo de tests solo se compile durante el testing. Los unit tests viven junto al código; los integration tests van en el directorio tests/. Ejecuta con 'cargo test'. Usa #[ignore] para saltar tests inestables.
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_ne!(add(2, 3), 6);
}
#[test]
fn test_with_message() {
let result = add(1, 1);
assert_eq!(result, 2, "Expected 2, got {}", result);
}
#[test]
#[should_panic(expected = "division by zero")]
fn test_panic() {
panic!("division by zero");
}
#[test]
fn test_result() -> Result<(), String> {
if add(2, 2) == 4 { Ok(()) }
else { Err(String::from("math is broken")) }
}
}
// Integration tests: tests/integration_test.rs
// Run: cargo testConcurrency & File I/O
Threads
std::thread::spawn crea threads del SO. La closure debe ser 'move' si captura variables, transfiriendo ownership al thread (previniendo use-after-free). join() bloquea hasta que el thread completa, retornando un Result. El sistema de ownership de Rust previene data races en tiempo de compilación — no puedes compartir datos mutables entre threads sin sincronización (Arc<Mutex<T>>).
use std::thread;
use std::time::Duration;
// spawn a thread
let handle = thread::spawn(|| {
for i in 0..5 {
println!("spawned thread: {}", i);
thread::sleep(Duration::from_millis(10));
}
});
// main thread continues
for i in 0..3 {
println!("main thread: {}", i);
}
// wait for spawned thread to finish
handle.join().unwrap();
// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();Channels (Message Passing)
Los canales habilitan comunicación entre threads vía message passing (el lema de Go: 'Comparte memoria comunicándose'). mpsc permite múltiples senders (tx.clone()) pero un receiver. send() retorna Result (Err si el receiver se ha dropeado). El receiver implementa Iterator, así los bucles for funcionan naturalmente. Para múltiples receivers, usa crossbeam-channel o canales async. El message passing evita la complejidad del estado mutable compartido.
use std::sync::mpsc;
use std::thread;
// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();
// clone transmitter for multiple producers
let tx2 = tx.clone();
thread::spawn(move || {
let vals = vec!["a", "b", "c"];
for v in vals {
tx.send(v).unwrap();
}
});
thread::spawn(move || {
tx2.send("from tx2").unwrap();
});
// receiver is an iterator
for received in rx {
println!("Got: {}", received);
}Mutex & Arc (Shared State)
Arc (Atomic Reference Counted) habilita ownership compartido entre threads (Rc thread-safe). Mutex proporciona acceso exclusivo — lock() bloquea hasta adquirirse, retorna un MutexGuard que libera el lock al hacer drop. RwLock permite múltiples readers o un writer. La combinación Arc<Mutex<T>> es el patrón estándar para estado mutable compartido. unwrap() en lock() maneja poison (un thread hizo panic mientras tenía el lock).
use std::sync::{Arc, Mutex};
use std::thread;
// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// lock() returns MutexGuard, auto-unlocks when dropped
let mut num = counter.lock().unwrap();
*num += 1;
}); // lock released here
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10
// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap(); // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 droppedFile I/O
fs::read_to_string es conveniente para archivos pequeños. Para archivos grandes, usa BufReader/BufWriter para reducir system calls. Los traits Read/Write proporcionan operaciones de bytes de bajo nivel. BufRead añade lines() y read_line() para texto. Siempre maneja los errores con ? (los archivos pueden faltar, permisos denegados, disco lleno). flush() asegura que los datos bufferizados lleguen al SO (aunque no necesariamente al disco).
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;
// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;
// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;
// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
println!("{}", line?);
}
// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to diskAsync/Await (Tokio)
Async/await habilita concurrencia eficiente sin threads del SO — las tareas se ejecutan en un thread pool y ceden en los puntos .await. tokio es el runtime async más popular. async fn retorna un Future que debe ser .awaited. tokio::join! ejecuta futures concurrentemente y espera a todos. tokio::select! hace racing de futures. Async es ideal para trabajo I/O-bound (red, archivos); usa threads para trabajo CPU-bound. El .await no bloquea el thread — cede el control de vuelta al runtime.
use tokio::{fs, task, time};
use std::time::Duration;
// async fn returns a Future
async fn fetch_data() -> String {
time::sleep(Duration::from_secs(1)).await;
String::from("data")
}
// spawn concurrent tasks
#[tokio::main]
async fn main() {
// run tasks concurrently
let (a, b, c) = tokio::join!(
fetch_data(),
fetch_data(),
fetch_data()
);
println!("{} {} {}", a, b, c);
// spawn a background task
let handle = tokio::spawn(async {
let data = fs::read_to_string("file.txt").await.unwrap();
println!("Read {} bytes", data.len());
});
handle.await.unwrap();
// select: first to complete wins
tokio::select! {
val = fetch_data() => println!("Got: {}", val),
_ = time::sleep(Duration::from_millis(500)) => {
println!("Timeout!");
}
}
}Lifetimes Deep Dive
Named Lifetimes in Functions
Los lifetimes son la forma que tiene el compilador de rastrear la validez de las referencias. El 'a es un parámetro de lifetime genérico — no cambia el comportamiento en runtime, solo la comprobación en compile time. Cuando una función toma múltiples referencias y retorna una, debes anotar lifetimes para que el compilador sepa que la referencia retornada no sobrevivirá a sus inputs. Esto previene dangling pointers en tiempo de compilación.
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y
let s1 = String::from("long string");
let result;
{
let s2 = String::from("hi");
result = longest(s1.as_str(), s2.as_str());
// result valid here
println!("{}", result);
}
// result NOT valid here — s2 is droppedLifetime Elision Rules
Las reglas de lifetime elision te permiten omitir anotaciones explícitas de lifetime en casos comunes. La regla 1 asigna un lifetime distinto a cada parámetro de referencia. La regla 2 asigna ese lifetime al output si hay exactamente una referencia de input. La regla 3 aplica a métodos — el output obtiene el lifetime de &self. Cuando ninguna de estas resuelve todas las referencias, debes escribir lifetimes explícitos. La mayor parte del código Rust idiomático rara vez necesita anotaciones explícitas de lifetime.
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime
// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' { return &s[0..i]; }
}
&s[..]
}
// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a strThe 'static Lifetime
'static es el lifetime más largo — dura toda la duración del programa. Todos los literales de cadena tienen este lifetime porque están embebidos en el binario. Cuando ves T: 'static como bound, no significa que T deba vivir para siempre — significa que T no debe contener referencias más cortas que 'static (p. ej., tipos owned como String, Vec, i64 siempre satisfacen esto). Los threads requieren 'static porque pueden sobrevivir a la función que los llama.
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str
// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);
// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
std::thread::spawn(move || {
drop(t);
});
}Lifetimes in Structs
Cuando un struct contiene una referencia (no un tipo owned), necesita un parámetro de lifetime para declarar cuánto tiempo es válida esa referencia. La instancia del struct no puede sobrevivir a los datos que pide prestados. Esto es común para parsers zero-copy, iteradores sobre datos prestados y views. El lifetime debe declararse tanto en el struct como en su bloque impl. Prefiere poseer los datos (String, Vec) a menos que tengas una razón específica para pedirlos prestados.
// Structs holding references need lifetime annotations
struct Parser<'a> {
text: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(text: &'a str) -> Self {
Parser { text, pos: 0 }
}
fn peek(&self) -> Option<char> {
self.text[self.pos..].chars().next()
}
fn advance(&mut self) {
self.pos += 1;
}
}
// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();Multiple Lifetimes & Subtyping
Las funciones con múltiples referencias que interactúan diferentemente necesitan múltiples parámetros de lifetime. El subtyping de lifetime significa que un lifetime más largo puede sustituir a uno más corto (covarianza) — &'static puede usarse donde se espera &'a. Por eso 'static es un subtipo de todos los lifetimes. Usa múltiples lifetimes cuando el lifetime del output depende solo de algunos inputs, dando al compilador más flexibilidad y al caller menos restricciones.
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
// returns something tied to src, not ctx
src
}
// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
"constant" // 'static coerces to 'a
}
// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long); // OK: 'static coerces to any 'shortSmart Pointers (Box, Rc, Arc, RefCell)
Box<T> — Heap Allocation
Box<T> es el smart pointer más simple de Rust — aloja un valor en el heap con ownership único. Úsalo para tipos recursivos (cuyo tamaño no puede conocerse en tiempo de compilación), valores grandes que no quieres mover por el stack, y trait objects (Box<dyn Trait>) para dynamic dispatch. Box se dereferencia a T así que se comporta como el valor interno. Tiene esencialmente coste cero más allá de la propia asignación del heap.
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b); // derefs automatically
// Recursive types NEED Box (size unknown at compile time)
enum List {
Cons(i32, Box<List>),
Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Box has zero runtime overhead when dereferencingRc<T> — Reference Counting (Single Thread)
Rc<T> (Reference Counted) habilita ownership múltiple para escenarios single-threaded. Rc::clone incrementa el conteo de referencias en lugar de copiar datos — copia de puntero barata. El valor se dropea cuando el último Rc se dropea. Usa Rc para nodos de grafos compartidos, árboles padre-hijo, o cualquier estructura donde múltiples partes necesitan poseer los mismos datos. Rc NO es thread-safe — usa Arc para multithreading. Rc es inmutable — no puedes mutar a través de él directamente.
use std::rc::Rc;
// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments count, doesn't copy
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
// Data dropped when count reaches 0
// Shared graph/tree structures
struct Node {
children: Vec<Rc<Node>>,
value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
children: vec![Rc::clone(&leaf)],
value: 2,
});Arc<T> — Atomic Reference Counting (Thread-Safe)
Arc<T> (Atomically Reference Counted) es la contraparte thread-safe de Rc. Usa operaciones atómicas para el conteo de referencias, haciéndolo seguro de compartir entre threads. El trade-off es ligeramente más overhead que Rc. Usa Arc siempre que necesites ownership compartido entre múltiples threads. Para mutar datos compartidos, combina Arc con Mutex (para acceso exclusivo) o RwLock (para acceso de lectura intensiva). Arc::clone es barato — solo incrementa un contador atómico.
use std::sync::Arc;
use std::thread;
// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let handles: Vec<_> = (0..3).map(|i| {
let data = Arc::clone(&data); // atomic increment
thread::spawn(move || {
println!("Thread {} sees: {:?}", i, *data);
})
}).collect();
for h in handles { h.join().unwrap(); }
// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threadsRefCell<T> — Interior Mutability
RefCell<T> proporciona interior mutability — puedes mutar a través de una referencia compartida, con las reglas de borrow aplicadas en runtime en lugar de compile time. borrow() retorna una referencia inmutable, borrow_mut() una mutable. Violar las reglas (p. ej., dos borrows mutables) causa un pánico en runtime. Usa RefCell cuando el compilador no puede probar la seguridad del borrow (p. ej., estructuras de grafo, mock objects en tests). Rc<RefCell<T>> es el patrón clásico para datos compartidos mutables single-threaded.
use std::cell::RefCell;
// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);
// Multiple immutable borrows OR one mutable borrow
{
let mut borrowed = cell.borrow_mut();
borrowed.push(4);
} // borrow released here
{
let r1 = cell.borrow(); // OK
let r2 = cell.borrow(); // OK — multiple immutable
println!("{:?} {:?}", r1, r2);
}
// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphsWeak<T> — Breaking Reference Cycles
Weak<T> es una referencia non-owning que no afecta al conteo de referencias fuertes. Esto es esencial para romper ciclos de referencias: si un padre posee hijos (Rc) y los hijos poseen al padre (Rc), ninguno se liberará nunca — un memory leak. La solución es hacer la back-reference Weak. upgrade() retorna Option<Rc<T>> — None si el valor ya se había dropeado. Usa Weak para enlaces hijo→padre, caches y patrones observer donde no quieres mantener los datos vivos.
use std::rc::{Rc, Weak, RefCell};
// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
parent: RefCell<Weak<Node>>, // weak: child → parent
children: RefCell<Vec<Rc<Node>>>, // strong: parent → children
}
let leaf = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
println!("parent exists");
}Trait Objects & Dynamic Dispatch
dyn Trait — Dynamic Dispatch
dyn Trait habilita dynamic dispatch — el tipo concreto se borra en tiempo de compilación y las llamadas a métodos van a través de una vtable en runtime. Esto te permite almacenar tipos heterogéneos en una sola colección (Vec<Box<dyn Animal>>). El trade-off: un pequeño coste en runtime (indirección de vtable, sin inlining) y el tipo no puede conocerse en tiempo de compilación. Usa trait objects cuando el conjunto de tipos concretos no se conoce en tiempo de compilación o cuando necesitas agrupar diferentes tipos.
trait Animal {
fn name(&self) -> &str;
fn sound(&self) -> String;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn sound(&self) -> String { "Meow".into() }
}
// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: "Rex".into() }),
Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
println!("{} says {}", a.name(), a.sound());
}Object Safety Rules
Un trait es object-safe solo si el compilador puede construirle una vtable. Dos reglas: (1) los métodos no deben retornar Self (el tipo concreto se borra, así que no puede conocerse), y (2) los métodos no deben tener parámetros de tipo genérico (la vtable necesitaría una entrada para cada tipo posible). Los traits con Sized como supertrait tampoco son object-safe. Si necesitas object safety, refactoriza los métodos que retornan Self para retornar Box<dyn Trait> o usa una función factory separada.
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
fn draw(&self); // OK: &self, no generics
}
// NOT object-safe:
trait Bad {
fn create() -> Self; // returns Self — needs known type
fn process<T>(&self, x: T); // generic method — vtable can't cover all T
const SIZE: usize; // associated const (sometimes OK)
}
// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
fn new_boxed() -> Box<dyn GoodFactory>;
}
// Sized bound makes trait non-object-safe
// trait Foo: Sized {} // NOT object-safeTrait Objects vs Generics
Los genéricos usan monomorfización — el compilador genera una copia separada de la función para cada tipo concreto, habilitando static dispatch y optimización completa (inlining). Esto tiene coste cero en runtime pero aumenta el tamaño del binario. Los trait objects (dyn) usan una sola función con lookup de vtable — binario más pequeño pero un pequeño coste en runtime por llamada. Elige genéricos cuando el rendimiento importa y el conjunto de tipos es pequeño/conocido; elige dyn cuando necesitas colecciones heterogéneas o no conoces todos los tipos upfront.
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.
// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
// can't return — don't know size at compile time
a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}
// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dynAny Trait & Downcasting
El trait Any te permite almacenar valores de cualquier tipo y recuperar el tipo concreto en runtime vía downcasting. downcast_ref::<T>() retorna Option<&T>, downcast::<T>() retorna Result<Box<T>, Box<dyn Any>>. Este es el escape hatch de Rust para cuando genuinamente no conoces el tipo en tiempo de compilación (sistemas de plugins, configs dinámicas). Sin embargo, prefiere enums cuando el conjunto de tipos posibles es conocido — son más seguros, más rápidos y más idiomáticos. Any confía en TypeId, que está implementado para todos los tipos 'static.
use std::any::Any;
// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);
// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
println!("It's an i32: {}", n);
}
// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
println!("Recovered: {}", s);
}
// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is knownDefault Trait Methods & Supertraits
Los traits pueden proporcionar implementaciones de método por defecto que los implementadores pueden sobrescribir o usar tal cual. Los supertraits (trait Named: Shape) requieren que el tipo implementador también implemente el supertrait — esto crea una jerarquía donde los tipos Named garantizan tener area() y describe(). Los métodos por defecto reducen boilerplate y habilitan el patrón 'extension method' donde añadir un método a un trait beneficia automáticamente a todos los implementadores existentes sin romperlos.
trait Shape {
fn area(&self) -> f64;
// Default method — can be overridden
fn describe(&self) -> String {
format!("Shape with area {:.2}", self.area())
}
}
// Supertrait: trait that requires another trait
trait Named: Shape {
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
fn name(&self) -> &str { "Circle" }
}
let c = Circle { radius: 2.0 };
println!("{}", c.describe()); // uses default methodMacros (Declarative & Procedural)
Declarative Macros (macro_rules!)
macro_rules! crea macros declarativas que se expanden en tiempo de compilación vía pattern matching. La sintaxis $(...),* es un repetition matcher — coincide cero o más expresiones separadas por comas. $x:expr significa 'coincide cualquier expresión y vincúlala a x'. Las macros se expanden antes del type checking, así que pueden generar código que funciona con cualquier tipo. Usa macros para reducir boilerplate que los genéricos no pueden manejar (p. ej., argumentos variádicos, extensión de sintaxis).
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$( v.push($x); )*
v
}};
}
let nums = vec_of!(1, 2, 3, 4);
// Recursive macro: build a HashMap
macro_rules! hashmap {
($($k:expr => $v:expr),*) => {{
let mut m = std::collections::HashMap::new();
$( m.insert($k, $v); )*
m
}};
}
let config = hashmap!("host" => "localhost", "port" => 8080);Macro Fragment Types
Los fragment specifiers determinan qué tipo de sintaxis coincide un argumento de macro. :ident coincide identificadores (nombres), :expr coincide expresiones (valores), :ty coincide tipos, :block coincide bloques delimitados por llaves, :stmt coincide sentencias, :literal coincide literales. Elegir el specifier correcto importa — :expr es el más común pero :ident se necesita cuando quieres crear un nombre de función/variable. El sistema de macros es higiénico: los identificadores introducidos por macros no colisionan con el código circundante.
// Common fragment specifiers:
macro_rules! build_fn {
// $name:ident — identifier (function/variable name)
// $body:block — a block { ... }
// $ty:ty — a type
// $expr:expr — an expression
// $stmt:stmt — a statement
// $lit:literal — a literal (string, number)
($name:ident, $ret:ty, $body:block) => {
fn $name() -> $ret $body
};
}
build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());
// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibilityBuilt-in Standard Macros
Rust viene con muchas macros integradas. println!/eprintln! imprimen a stdout/stderr. dbg! imprime el valor de una expresión con info de archivo/línea — genial para depurar (también retorna el valor). assert!/assert_eq!/assert_ne! son para tests e invariantes. todo!/unimplemented! marcan código incompleto con un panic. file!/line!/module! dan info de ubicación en compile time. env!/option_env! leen variables de entorno en tiempo de compilación — útiles para embeber info de versión.
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops"); // stderr
format!("{}-{}", "a", "b"); // returns String
// Debug helpers
dbg!(2 + 2); // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2); // panic if false
assert_eq!(2 + 2, 4); // panic if not equal
assert_ne!(1, 2); // panic if equal
// Code generation
todo!("not implemented yet"); // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");
// Environment & file info
println!("{}:{}", file!(), line!()); // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));Procedural Macros Overview
Las procedural macros (proc macros) son funciones de Rust que toman TokenStreams como input y producen TokenStreams como output — transformación completa de código a código. A diferencia de las macros declarativas, pueden hacer computación arbitraria. Tres tipos: derive macros (añaden implementaciones de trait vía #[derive]), attribute macros (anotan items), y function-like macros (sintaxis personalizada como sqlx::query!). Deben vivir en un crate separado con proc-macro = true. El crate syn analiza sintaxis de Rust, quote! genera código. Ejemplos populares: serde, tokio, thiserror.
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):
// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }
// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}
// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");
// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0" # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"Macro Hygiene & Common Patterns
Las macros de Rust son higiénicas — los identificadores creados dentro de una macro existen en un 'syntax context' separado y no capturarán ni harán shadow de variables en el scope de llamada accidentalmente. Esto previene bugs sutiles donde el nombre de variable interno de una macro colisiona con el del caller. stringify! convierte cualquier token stream en un literal de cadena en tiempo de compilación (útil para mensajes de error). cfg_debug! muestra un patrón común: compilar condicionalmente código basado en la configuración de build usando el sistema cfg!.
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
($e:expr) => {
let temp = $e; // this 'temp' is distinct from caller's
println!("{}", temp);
};
}
let temp = 10;
using_temp!(temp + 5); // no conflict — hygienic
// Conditional compilation macro
macro_rules! cfg_debug {
($($e:tt)*) => {
#[cfg(debug_assertions)]
{ $($e)* }
};
}
cfg_debug! {
println!("Debug mode on");
}
// stringify! converts tokens to a string literal
let s = stringify!(a + b * c); // "a + b * c"Unsafe Rust
Raw Pointers
Los raw pointers (*const T, *mut T) son el escape hatch de Rust del borrow checker. A diferencia de las referencias, pueden ser null, pueden alias (múltiples punteros a los mismos datos), y no rastrean lifetimes. Crearlos es seguro, pero desreferenciarlos requiere unsafe porque el compilador no puede garantizar validez. Usa raw pointers para FFI (interfaz con C), implementar estructuras de datos de bajo nivel (linked lists, vectores), y código crítico en rendimiento donde aseguras la seguridad manualmente. Documenta siempre por qué unsafe es sound.
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x; // coerce from reference
let r2: *mut i32 = x as *mut i32; // cast
// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();
// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
println!("r1 = {}", *r1);
}
// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); } // little-endian intUnsafe Blocks & Functions
unsafe no desactiva el borrow checker — te permite hacer cinco cosas específicas: (1) desreferenciar raw pointers, (2) llamar funciones unsafe, (3) implementar traits unsafe, (4) acceder/mutar static mut, (5) acceder a campos de union. Los bloques unsafe hacen las operaciones unsafe explícitas y localizadas. unsafe fn declara que llamar a la función requiere mantener invariantes que el compilador no puede comprobar. get_unchecked salta la comprobación de límites por rendimiento — solo seguro si has verificado el índice. Minimiza el área de superficie unsafe y encapsúlala detrás de una API safe.
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };
// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
*ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };
// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
let first = v.get_unchecked(0); // no bounds check
let last = v.get_unchecked(len - 1);
println!("{} {}", first, last);
}FFI — Calling C Functions
FFI (Foreign Function Interface) permite a Rust llamar funciones de C y viceversa. Los bloques extern "C" declaran funciones C externas — llamarlas es unsafe porque el compilador no puede verificar su comportamiento. #[no_mangle] evita que Rust renombre la función para que C pueda encontrarla por nombre. #[repr(C)] garantiza que el layout del struct coincide con el layout de memoria de C (Rust puede reordenar campos por defecto por eficiencia). Usa FFI para system calls, bibliotecas legacy y bindings críticos de rendimiento. El crate bindgen auto-genera declaraciones FFI a partir de cabeceras de C.
// extern "C" declares foreign functions
extern "C" {
fn abs(x: i32) -> i32;
}
fn main() {
let x = -5;
let positive = unsafe { abs(x) };
println!("{}", positive); // 5
}
// Exporting Rust functions to C
#[no_mangle] // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
a + b
}
// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }Implementing Unsafe Traits
Los traits unsafe (como Send, Sync) requieren que el implementador mantenga invariantes que el compilador no puede verificar. Send significa que un tipo puede moverse de forma segura entre threads; Sync significa que &T puede compartirse entre threads. El compilador auto-deriva estos para la mayoría de tipos, pero los raw pointers no son Send/Sync por defecto. Cuando los implementas manualmente, asumes la responsabilidad de la thread safety. static mut requiere acceso unsafe porque múltiples threads podrían hacer race en ello — prefiere atomics (AtomicU64) o Mutex. Documenta siempre la justificación de seguridad con un comentario SAFETY.
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;
// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);
// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}
// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally
// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; } // unsafe: data races possibleUnions & Inline Assembly
Los unions permiten que diferentes tipos compartan la misma ubicación de memoria — leer un campo que no fue el último escrito es undefined behavior, de ahí unsafe. Son principalmente para FFI con C. transmute reinterpret-castea el patrón de bits de un tipo a otro del mismo tamaño — extremadamente peligroso si los tamaños difieren o los tipos son incompatibles. Inline assembly (asm!) te permite embeber instrucciones de CPU directamente, útil para desarrollo de kernel y optimización extrema. Todas estas son herramientas afiladas: úsalas solo cuando no existe alternativa safe, y encapsúlalas detrás de una abstracción safe.
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
i: i32,
f: f32,
}
let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!
// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
std::arch::asm!("hlt", options(nostack));
}
// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB; // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };Iterators Deep Dive
Iterator Trait & Creation
El trait Iterator requiere solo un método next() que retorna Option<Item> — None señala agotamiento. Todo lo demás (map, filter, collect) se construye encima. iter() pide prestado elementos (&T), into_iter() consume la colección (yields T owned), iter_mut() yields &mut T. Los rangos (1..5, 1..=5) son iteradores directamente. Los strings iteran por chars (valores escalares Unicode) o bytes. Los iteradores son lazy — nada se ejecuta hasta que los consumes.
// The Iterator trait: one required method
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ... many provided methods (map, filter, etc.)
}
// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter(); // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut(); // mutably borrows: &mut i32
// Range is an iterator
for i in 1..=5 { print!("{} ", i); } // 1 2 3 4 5
// String iteration
for c in "héllo".chars() { print!("{} ", c); } // h é l l o
for b in "hi".bytes() { print!("{} ", b); } // 104 105Adapter Methods (Lazy)
Los métodos adaptadores transforman iteradores y retornan nuevos iteradores — son lazy, así que encadenar map().filter().map() crea cero colecciones intermedias. map aplica una función a cada elemento. filter mantiene los elementos donde el predicado retorna true. take(n) se detiene después de n elementos (útil para iteradores infinitos). skip(n) descarta los primeros n. flat_map mapea y aplana iteradores anidados. enumerate empareja cada elemento con su índice. Nada se ejecuta hasta que un consumidor (collect, sum, bucle for) conduce el iterador.
let nums = vec![1, 2, 3, 4, 5, 6];
// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();
// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();
// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect(); // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect(); // [3,4,5,6]
// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
.flat_map(|s| s.split(' '))
.collect(); // ["a","b","c"]
// enumerate: add index
for (i, v) in nums.iter().enumerate() {
println!("{}: {}", i, v);
}Consumer Methods
Los métodos consumidores conducen la cadena de iterador lazy para ejecutarse realmente. collect() recolecta resultados en cualquier colección que implemente FromIterator (Vec, HashMap, String, etc.). sum/product/count/fold reducen el iterador a un solo valor. find/any/all short-circuit — se detienen tan pronto como se conoce la respuesta, así que son eficientes en iteradores infinitos. min/max retornan Option (None para iteradores vacíos). La combinación de adaptadores lazy + un consumidor final significa que las cadenas de iterador son tan eficientes como bucles escritos a mano después de la optimización.
let nums = vec![1, 2, 3, 4, 5];
// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();
// sum, product, count
let total: i32 = nums.iter().sum(); // 15
let prod: i32 = nums.iter().product(); // 120
let count = nums.iter().count(); // 5
// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max); // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15
// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0); // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0); // false
let all_pos = nums.iter().all(|&x| *x > 0); // true
// min / max
nums.iter().copied().min(); // Some(1)
nums.iter().copied().max(); // Some(5)Custom Iterators
Para crear un iterador personalizado, implementa el trait Iterator con un método next(). Una vez que lo haces, obtienes gratis todos los más de 70 métodos adaptadores y consumidores. El iterador debe rastrear su propio estado (posición actual, etc.) y retornar None cuando se agote. Implementar IntoIterator para tu tipo de colección habilita la sintaxis de bucle for. Para acceso bidireccional o aleatorio, implementa también DoubleEndedIterator o ExactSizeIterator. Así es como Vec, HashMap, Range y todas las colecciones estándar proporcionan iteración.
// Build a custom iterator for a counter
struct Counter {
current: usize,
max: usize,
}
impl Counter {
fn new(max: usize) -> Self {
Counter { current: 0, max }
}
}
impl Iterator for Counter {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.max {
let val = self.current;
self.current += 1;
Some(val)
} else {
None
}
}
}
// Now all iterator methods work!
let sum: usize = Counter::new(5).sum(); // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();Infinite & Chained Iterators
Los iteradores de Rust pueden ser infinitos — (1..) genera números naturales para siempre, repeat(x) repite sin fin. Son seguros porque los adaptadores son lazy: take(n) limita el consumo. cycle() repite un iterador finito infinitamente. chain() concatena iteradores secuencialmente. zip() empareja elementos posicionalmente (se detiene en el más corto). peekable() te permite mirar el siguiente elemento sin consumirlo — útil para parsers. El diseño lazy significa que los iteradores infinitos no cuestan nada hasta que se consumen, y el compilador optimiza las cadenas en bucles ajustados.
use std::iter;
// Infinite iterators (use with take!)
let naturals = (1..).take(10); // 1..10
let zeros = iter::repeat(0).take(5); // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());
// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]
// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]
// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]
// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }Error Handling Deep Dive
Result & Option Combinators
Los combinadores te permiten encadenar operaciones falibles sin expresiones match anidadas. map transforma el valor Ok, map_err transforma el error. and_then encadena operaciones que ellas mismas retornan Result (flatmap para errores). ok_or convierte Option→Result. unwrap_or/unwrap_or_else/unwrap_or_default proporcionan valores de fallback. Estos componen elegantemente: parse().map().and_then().map_err() crea un pipeline donde cada paso puede fallar, y el primer fallo short-circuit. Prefiere combinadores sobre unwrap() en código de producción.
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse::<i32>().map(|n| n * 2)
}
// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate); // Ok(5)
// map_err: transform the error type
let r = "abc".parse::<i32>()
.map_err(|e| format!("Parse failed: {}", e));
// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value"); // Err("missing value")
// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);Custom Error Types
Los tipos de error personalizados te permiten representar fallos específicos del dominio. El patrón clave: implementa From para cada tipo de error subyacente para que el operador ? auto-convierta. Esto significa que puedes usar ? con std::io::Error, ParseIntError, etc. sin map_err explícito. Implementar Display hace el error user-friendly; Debug es para desarrolladores. Los errores basados en enum son idiomáticos en Rust — son exhaustivos (el compilador avisa de casos faltantes) y de coste cero (sin heap allocation). Esta es la fundación antes de usar thiserror.
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
NotFound(String),
Unauthorized,
}
// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
AppError::NotFound(s) => write!(f, "Not found: {}", s),
AppError::Unauthorized => write!(f, "Unauthorized"),
}
}
}
// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
let s = std::fs::read_to_string("config.txt")?; // io → AppError
let n: i32 = s.trim().parse()?; // parse → AppError
Ok(n)
}The Error Trait & Box<dyn Error>
std::error::Error es el trait de la biblioteca estándar para tipos de error (requiere Debug + Display). Box<dyn Error> es el tipo de error más simple — acepta cualquier error vía ? y es genial para prototipado o aplicaciones donde no necesitas manejar errores específicos programáticamente. El inconveniente: pierdes el tipo de error concreto, así que hacer match en variantes específicas requiere downcast_ref. Para bibliotecas, prefiere un tipo de error enum concreto (con thiserror). Para aplicaciones, anyhow es una mejor elección que Box<dyn Error> porque preserva backtraces y cadenas de error.
use std::error::Error;
// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
let f = std::fs::read_to_string("file.txt")?; // io::Error
let n: i32 = f.parse()?; // ParseIntError
println!("{}", n);
Ok(())
}
// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info
// downcast to recover specific error type
match do_something() {
Err(e) => {
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
println!("IO: {}", io_err);
}
}
_ => {}
}thiserror Crate (Library Errors)
thiserror es el crate estándar para tipos de error de biblioteca. La macro #[derive(Error)] genera Display (a partir de #[error("...")]) y From (a partir de #[from]) automáticamente. #[from] hace que ? convierta el error subyacente en tu variante de enum. Esto elimina boilerplate manteniendo un enum de error fuertemente tipado y exhaustivo. Usa thiserror para bibliotecas (donde los callers necesitan hacer match en errores específicos). El placeholder {0} inserta el Display del error interno; campos nombrados como {id} insertan campos del struct.
use thiserror::Error;
// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("parse failed: {0}")]
Parse(#[from] std::num::ParseIntError),
#[error("item {id} not found")]
NotFound { id: u32 },
#[error("invalid state: {msg}")]
Invalid { msg: String },
}
// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
let s = std::fs::read_to_string("data.txt")?; // auto-converts
let n: i32 = s.trim().parse()?;
Ok(n)
}anyhow Crate (Application Errors)
anyhow es el crate estándar para manejo de errores de aplicaciones/binarios. anyhow::Error envuelve cualquier error que implemente std::error::Error y añade contexto, backtraces y encadenamiento de errores. context() adjunta un mensaje legible por humanos a cada paso falible, creando una cadena como 'Failed to read config: IO error: No such file'. Esto hace la depuración mucho más fácil — ves exactamente qué paso falló y por qué. Usa anyhow para main() y código de aplicación donde solo necesitas reportar errores, no hacer match en ellos. Usa thiserror para bibliotecas donde los callers necesitan errores tipados.
use anyhow::{Context, Result, anyhow};
// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("Failed to read config.toml")?; // add context
Ok(content)
}
fn main() -> Result<()> {
let config = load_config()?;
if config.is_empty() {
// bail! / anyhow! create errors with format! syntax
return Err(anyhow!("config is empty"));
}
println!("{}", config);
Ok(())
}
// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtraceCargo & Crates Deep Dive
Cargo.toml Structure
Cargo.toml es el manifiesto para un proyecto de Rust. [package] describe los metadatos del crate. [dependencies] lista los crates externos — las cadenas de versión usan semver (^1.0 significa >=1.0, <2.0). Las features habilitan funcionalidad opcional (la feature derive de serde activa #[derive(Serialize)]). [dev-dependencies] son solo para tests/benchmarks. [features] definen flags de compilación condicional. [profile.release] controla los ajustes de optimización. El campo edition (2015/2018/2021) determina las características del lenguaje — usa siempre la última.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"
[dev-dependencies]
criterion = "0.5" # only for tests/benches
[features]
default = ["tokio"]
async-mode = ["dep:tokio"]
[[bin]]
name = "myapp"
path = "src/main.rs"
[profile.release]
opt-level = 3
lto = true
strip = trueDependencies & Feature Flags
Los feature flags habilitan compilación condicional. Cada dependencia puede exponer features (p. ej., derive de serde). default-features = false elimina las features por defecto para reducir el tamaño del binario. Las dependencias opcionales (optional = true) solo se compilan cuando una feature las habilita vía dep:name. Las features son aditivas — activan cosas, nunca las desactivan. Esto asegura la unificación de features: si dos dependencias habilitan diferentes features de serde, Cargo compila serde una vez con la unión de todas las features. Usa #[cfg(feature = "x")] para compilar código condicionalmente.
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0" # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false } # disable defaults
# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }
[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]
# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }Workspaces (Multi-Crate Projects)
Los workspaces agrupan múltiples crates relacionados que comparten un Cargo.lock y directorio target. Esto acelera los builds (caché de compilación compartida) y asegura que todos los crates usen las mismas versiones de dependencias. Los miembros pueden depender entre sí vía path = "../core". [workspace.dependencies] centraliza la gestión de versiones — los crates miembros las referencian con { workspace = true }. El resolver = "2" (por defecto en edition 2021) usa unificación de features por-target, evitando algunos problemas de build. Usa workspaces para monorepos, bibliotecas con múltiples componentes, o proyectos que dividen core/CLI/server.
# Root Cargo.toml — workspace manifest
[workspace]
members = [
"core",
"cli",
"server",
"utils",
]
resolver = "2"
# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"
# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" } # local path dependencyBuild Profiles & Optimization
Los profiles controlan cómo cargo construye tu proyecto. dev (por defecto para cargo build) prioriza la velocidad de compilación. release (cargo build --release) prioriza el rendimiento en runtime. Knobs clave: opt-level (0-3, 's' para tamaño, 'z' para tamaño mínimo), lto (optimización en tiempo de enlace a través de límites de crate), codegen-units (1 = mejor optimización pero compilación más lenta), strip (elimina símbolos para binarios más pequeños), panic = 'abort' (desactiva unwinding, binario más pequeño). Para producción, usa lto = true, codegen-units = 1, strip = true. Los profiles personalizados heredan de los existentes.
# Cargo.toml
[profile.dev]
opt-level = 0 # no optimization (fast compile)
debug = true # include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # max optimization
lto = "fat" # link-time optimization across crates
codegen-units = 1 # single codegen unit (better opt, slower compile)
strip = true # strip debug symbols from binary
panic = "abort" # smaller binary, no unwinding
[profile.release.package."*"]
opt-level = 2 # optimize dependencies less than your code
# Custom profile
[profile.bench]
inherits = "release"
debug = true # keep symbols for profiling
# Usage: cargo build --release --profile benchPublishing & Documentation
Publicar en crates.io es permanente — las versiones no pueden sobrescribirse ni eliminarse (solo yanked, lo que previene nuevos dependientes). Asegúrate de que name, version, description, license y repository estén establecidos. cargo package valida el manifiesto y muestra qué se publicaría. cargo doc genera documentación HTML a partir de comentarios doc /// — los doc tests (código en bloques ```) se compilan y ejecutan con cargo test. Los buenos comentarios doc con ejemplos son tanto documentación como tests. Usa #[doc(hidden)] para ocultar items internos.
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token> (token from crates.io)
# 2. Check the package
# $ cargo package # creates .crate file, checks metadata
# $ cargo publish --dry-run
# 3. Publish
# $ cargo publish # uploads to crates.io (irreversible!)
# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository
# Documentation:
# $ cargo doc # generate docs for your crate + deps
# $ cargo doc --open # generate and open in browser
# $ cargo doc --no-deps # only your crate
# Doc tests run automatically:
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }Trait Objects & Dynamic Dispatch
dyn Trait Basics
Los trait objects (dyn Trait) habilitan polimorfismo en runtime: una sola variable puede contener diferentes tipos concretos que implementan el mismo trait. El compilador genera una vtable (tabla de métodos virtuales) por tipo, y las llamadas a métodos van a través de la vtable (dynamic dispatch). Esto tiene un pequeño coste en runtime pero habilita colecciones heterogéneas. Usa trait objects cuando el tipo concreto se desconoce en tiempo de compilación o varía.
trait Draw {
fn draw(&self);
}
struct Circle { radius: f64 }
struct Square { side: f64 }
impl Draw for Circle {
fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
fn draw(&self) { println!("Square s={}", self.side); }
}
// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
Box::new(Circle { radius: 1.0 }),
Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }
// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }Object Safety
Un trait es object-safe (puede usarse como dyn Trait) solo si: no tiene métodos que retornen Self, no tiene métodos que tomen Self por valor, no tiene métodos genéricos, y todos los métodos son dispatchable. Clone, Default y From NO son object-safe. Las soluciones incluyen usar retornos Box<Self> (patrón box_clone), dividir traits, o usar static dispatch con enums. El compilador reporta claramente las violaciones de object safety.
// Object-safe trait (can be made into dyn)
trait Animal {
fn sound(&self) -> String;
fn name(&self) -> &str;
}
// NOT object-safe: returns Self
trait Clone {
fn clone(&self) -> Self; // Self is unknown for dyn
}
// NOT object-safe: takes Self by value
trait Add {
fn add(&self, other: Self) -> Self;
}
// NOT object-safe: generic method
trait From {
fn from<T>(t: T) -> Self;
}
// Fix: use where clauses or separate traits
trait AnimalSafe {
fn sound(&self) -> String;
fn box_clone(&self) -> Box<dyn AnimalSafe>;
}Static vs Dynamic Dispatch
Static dispatch (genéricos con trait bounds) monomorfiza: el compilador genera una versión especializada por tipo concreto, habilitando inlining y máximo rendimiento a costa del tamaño del binario. Dynamic dispatch (dyn Trait) usa lookups de vtable en runtime, binario más pequeño pero llamadas más lentas (impidiendo inlining). Prefiere static dispatch para código crítico en rendimiento; usa dynamic dispatch para colecciones heterogéneas y sistemas de plugins.
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.
// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
// a > b // Cannot use operators on dyn
false
}
// Trait bound (static)
fn process<T: Display>(item: &T) {
println!("{}", item);
}
// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
println!("{}", item);
}
// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
println!("{}", item);
}Trait Objects with Lifetimes
Los trait objects pueden llevar bounds de lifetime: Box<dyn Trait + 'a> significa que el trait object (y el tipo concreto detrás) debe vivir al menos 'a. Por defecto, Box<dyn Trait> implica 'static. Cuando almacenes trait objects que pueden contener referencias, añade el lifetime explícitamente. La sintaxis + combina trait bounds con lifetimes. Esto es común en sistemas de plugins y event handlers.
trait Parser {
fn parse(&self, input: &str) -> &str;
}
// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}
// Trait object holding references
struct Runner<'a> {
parsers: Vec<Box<dyn Parser + 'a>>,
}
impl<'a> Runner<'a> {
fn add(&mut self, p: Box<dyn Parser + 'a>) {
self.parsers.push(p);
}
}
// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
Box::new(MyParser)
}Downcasting & Any
El trait Any habilita comprobación de tipos en runtime y downcasting. Any se implementa automáticamente para todos los tipos 'static. downcast_ref y downcast_mut retornan Option, permitiendo recuperación segura de tipos. Esto es útil para sistemas de plugins, configuraciones dinámicas y contenedores heterogéneos. Usa Any con moderación — evita el sistema de tipos. Prefiere enums para alternativas conocidas y genéricos para polimorfismo con seguridad de tipos.
use std::any::Any;
// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);
// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
println!("Got i32: {}", n);
}
// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
Box::new(42_i32),
Box::new("hello".to_string()),
Box::new(3.14_f64),
];
for item in &bag {
if let Some(s) = item.downcast_ref::<String>() {
println!("String: {}", s);
} else if let Some(n) = item.downcast_ref::<i32>() {
println!("i32: {}", n);
}
}Declarative Macros
macro_rules! Basics
macro_rules! define macros declarativas que coinciden patrones y se expanden a código. $( $x:expr ),* coincide una lista de expresiones separadas por comas, repetida cero o más veces. El bloque $() ... * se repite para cada coincidencia. Las macros se expanden en tiempo de compilación antes del type checking. Son higiénicas: los identificadores introducidos por la macro no colisionan con el código circundante. Usa macros para reducir boilerplate (vec!, println!, format!).
macro_rules! vec_of {
( $( $x:expr ),* ) => {
{
let mut v = Vec::new();
$(
v.push($x);
)*
v
}
};
}
let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");
// Multiple patterns
macro_rules! greet {
() => { println!("Hello!") };
($name:expr) => { println!("Hello, {}!", $name) };
($name:expr, $greeting:expr) => {
println!("{}, {}!", $greeting, $name)
};
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");Fragment Types
Los fragmentos de macro tienen tipos específicos: expr (expresiones), stmt (sentencias), ty (tipos), pat (patrones), ident (identificadores), tt (token trees, el más flexible), literal (literales), y más. El tipo de fragmento determina qué acepta la macro y cómo lo parsea. tt es el más general — cualquier secuencia de tokens válida. Usa el tipo más específico posible para mejores mensajes de error. El parser sigue la regla Most-Recently-Added-Ambiguity.
macro_rules! items {
// $x:expr - expression (1 + 2, foo())
// $x:stmt - statement (let x = 5;)
// $x:ty - type (Vec<i32>, &str)
// $x:pat - pattern (Some(x), (a, b))
// $x:path - path (std::vec::Vec, Module::Type)
// $x:ident - identifier (foo, Bar)
// $x:literal - literal (42, "hello")
// $x:tt - token tree (anything)
// $x:block - block ({ ... })
// $x:item - item (fn, struct)
($e:expr, $t:ty, $i:ident) => {
let $i: $t = $e;
};
}
items!(42, i32, my_num);
items!("hi", &str, greeting);Repetition Patterns
Repetición en macros: $(...)* coincide cero o más, $(...)+ coincide uno o más, $(...)? coincide cero o uno. Los separadores como comas van entre coincidencias. Las repeticiones anidadas manejan datos multi-dimensionales (matrices, listas de listas). El bloque de expansión $() ... * se repite para cada coincidencia. Múltiples variables en la misma repetición deben coincidir el mismo número de veces. Usa reglas internas @prefix para patrones acumuladores.
macro_rules! sum {
// Zero or more: $(...)*
( $( $x:expr ),* ) => {
{
let mut total = 0;
$(
total += $x;
)*
total
}
};
// One or more: $(...)+
( first $(, $x:expr )+ ) => {
println!("First and more");
};
// With separator and count
( $( $x:expr ),+ $(; $sep:expr )? ) => {
println!("List with optional separator");
};
}
// Nested repetition
macro_rules! matrix {
( $( [ $( $x:expr ),* ] ),* ) => {
vec![ vec![ $( $x ),* ],* ]
};
}Hygiene & Exporting
La higiene de macros previene colisiones de identificadores: las variables introducidas por una macro viven en su propio scope y no capturan ni hacen shadow de variables del caller. Usa $crate para referirte a items en el crate donde se define la macro, asegurando que funcione después de re-export. La convención @prefix marca reglas helper internas que los usuarios no deberían llamar directamente. #[macro_export] publica la macro en la raíz del crate.
// Export at crate root
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {
// Hygienic: this T does not collide with caller's T
let _ts: std::time::Instant = std::time::Instant::now();
eprintln!("[{}] {}", "LOG", format!($($arg)*));
};
}
// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
($msg:expr) => {
$crate::Error::new($msg)
};
}
// Internal helper rule (convention: @ prefix)
macro_rules! count {
(@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
(@count $x:expr) => { 1 };
() => { 0 };
}Common Macro Patterns
Las macros destacan en construir DSLs y reducir boilerplate. Patrones comunes: DSLs de builder (html!, sql!), aserciones de test (assert_approx!), configuración (config!), y generación de código (macros tipo derive). Las macros son higiénicas y de compile time, así que no tienen coste en runtime. Limitaciones: sin profundidad de recursión más allá de 64, mensajes de error complejos, y dificultad con parsing no trivial. Para metaprogramación compleja, usa procedural macros.
// 1. Builder DSL
macro_rules! html {
($tag:ident { $($body:tt)* }) => {
format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
};
}
// 2. Test assertion
macro_rules! assert_approx {
($a:expr, $b:expr, $eps:expr) => {
assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
};
}
// 3. Configuration
macro_rules! config {
( $( $key:ident : $val:expr ),* ) => {
{
let mut m = std::collections::HashMap::new();
$(
m.insert(stringify!($key).to_string(), $val.to_string());
)*
m
}
};
}
let cfg = config! { host: "localhost", port: 8080 };Cargo & Workspaces
Workspace Setup
Los workspaces agrupan múltiples crates que comparten dependencias y un directorio target. Los miembros se listan explícitamente o vía globs. [workspace.package] define metadatos compartidos del paquete, heredados con .workspace = true. [workspace.dependencies] centraliza las versiones de dependencias, asegurando que todos los crates usen la misma versión. Esto previene conflictos de versión y acelera los builds (un solo Cargo.lock). Usa workspaces para proyectos multi-crate como CLI + biblioteca + server.
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
tokio.workspace = trueBuild Profiles
Los profiles controlan los ajustes de compilación. dev prioriza compilación rápida (opt-level 0, debug symbols). release maximiza rendimiento en runtime (opt-level 3, LTO, una sola codegen unit). LTO (Link-Time Optimization) habilita inlining cross-crate. panic = "abort" produce binarios más pequeños pero desactiva unwinding. Las sobrescrituras por-paquete (profile.dev.package."*") optimizan dependencias incluso en dev. Los profiles personalizados heredan de los existentes.
# Cargo.toml
[profile.dev]
opt-level = 0 # No optimization (fast compile)
debug = true # Include debug symbols
overflow-checks = true
[profile.release]
opt-level = 3 # Max optimization
debug = false
lto = "fat" # Link-time optimization
codegen-units = 1 # Single unit (better opt, slower compile)
panic = "abort" # Smaller binary, no unwinding
strip = true # Strip symbols
[profile.dev.package."*"]
opt-level = 2 # Optimize dependencies in dev
# Custom profile
[profile.bench]
inherits = "release"
debug = true
# Usage: cargo build --release, cargo build --profile=benchFeatures & Conditional Compilation
Las features habilitan compilación condicional. Las dependencias opcionales se convierten en features automáticamente. cfg(feature = "...") gatea código por feature. El conjunto de features por defecto se habilita a menos que se pase --no-default-features. Las features deberían ser aditivas (habilitar más, no menos). Usa features para reducir tamaño de binario, soportar múltiples backends, o gatear código experimental. Combina con cfg_attr para macros derive condicionales. Evita features mutuamente excluyentes.
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]
[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }
# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
serde_json::from_str(s)
}
#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
Err(Error::FeatureNotEnabled)
}Build Scripts (build.rs)
build.rs se ejecuta antes de la compilación, habilitando generación de código, embebido de entorno y linking de bibliotecas C. Las directivas cargo:rerun-if-* controlan cuándo se reejecuta el script. Genera código con macros println!, luego inclúyelo con include! en tu crate. Usos comunes: embeber info de versión, generar bindings (bindgen), compilar esquemas protobuf/SQL, y enlazar bibliotecas del sistema. Mantén los build scripts rápidos — se ejecutan en cada build.
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;
fn main() {
// Tell Cargo to rerun if env changes
println!("cargo:rerun-if-env-changed=DATABASE_URL");
let out_dir = env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("config.rs");
let db_url = env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite://default.db".to_string());
// Generate Rust code at build time
fs::write(&dest, format!(
"pub const DATABASE_URL: &str = \"{}\";",
db_url
)).unwrap();
// Link a C library
println!("cargo:rustc-link-lib=static=mylib");
println!("cargo:rustc-link-search=native=/usr/local/lib");
}
// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));Publishing & Documentation
cargo publish sube un crate a crates.io. Usa --dry-run para verificar antes de publicar. cargo doc genera documentación HTML a partir de comentarios doc (///). Los bloques de código en comentarios doc se prueban con cargo test --doc. Incluye secciones Examples, Panics y Errors. Los metadatos (description, repository, keywords) mejoran la discoverability. Una vez publicado, una versión no puede reusarse ni eliminarse — usa yank para evitar que nuevos proyectos dependan de ella.
# Publish to crates.io
cargo login <token> # One-time authentication
cargo publish # Publish current crate
# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package # Inspect the .crate file
# Documentation
cargo doc # Generate docs
cargo doc --open # Generate and open
cargo doc --no-deps # Only this crate
# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]Procedural Macros
Macro Types
Las procedural macros generan código de Rust en tiempo de compilación, operando sobre token streams. Tres tipos: function-like (custom!()), derive (#[derive(Custom)]), y attribute (#[custom]). Requieren un crate separado con proc-macro = true. El crate syn analiza sintaxis de Rust, quote genera código, y proc-macro2 habilita testing. Las proc-macros son poderosas pero complejas — úsalas para derive macros, DSLs, y generación de código que las macros declarativas no pueden manejar.
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]
// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true
// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> i32 { 42 }".parse().unwrap()
}
// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }Derive Macro
Las derive macros añaden implementaciones de trait a tipos anotados con #[derive(MyMacro)]. syn parsea el input en un AST DeriveInput. quote! genera código con interpolación # para variables. Los helper attributes (attributes(hello)) permiten personalización en campos o variantes. Derive macros comunes: Debug, Clone, Serialize, Deserialize. El código generado se añade al módulo, así que no puede modificar el tipo original.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
expanded.into()
}
// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello(); // "Hello from Pancakes!"
// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }Attribute Macro
Las attribute macros (#[my_attr]) transforman el item que anotan, potencialmente reemplazándolo por completo. Reciben tanto los argumentos del atributo como el item anotado. Usos comunes: logging, caching, async wrappers (#[tokio::main]), y routing (#[get("/path")]). Las attribute macros pueden cambiar la firma del item, añadir código, o generar items adicionales. Son más flexibles que las derive macros pero más difíciles de usar correctamente.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_block = &input_fn.block;
let expanded = quote! {
fn #fn_name() {
println!("Calling {}", stringify!(#fn_name));
let __result = (|| #fn_block)();
println!("Finished {}", stringify!(#fn_name));
__result
}
};
expanded.into()
}
// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }Function-Like Macro
Las procedural macros function-like (my_macro!()) aceptan token streams arbitrarios, habilitando DSLs personalizados. Implementa Parse para definir la sintaxis aceptada. La macro puede validar, transformar, o generar código basado en el input. Usos comunes: consultas SQL (sqlx), plantillas HTML (maud), y DSLs de configuración. A diferencia de las macros declarativas, las proc-macros pueden parsear sintaxis compleja y realizar computación arbitraria en tiempo de compilación. Mantenlas rápidas para evitar builds lentos.
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};
// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
query: String,
}
impl Parse for SqlQuery {
fn parse(input: ParseStream) -> syn::Result<Self> {
let query = input.to_string();
Ok(SqlQuery { query })
}
}
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let SqlQuery { query } = parse_macro_input!(input as SqlQuery);
let expanded = quote! {
{
static QUERY: &str = #query;
// Compile-time SQL validation could go here
QUERY
}
};
expanded.into()
}Testing & Debugging
Testing de proc-macros: trybuild ejecuta UI tests comparando la salida del compilador (éxito o mensajes de error) contra archivos esperados. Para derive macros, prueba que el código generado compile y se comporte correctamente. Depura con eprintln! (impreso durante la compilación) o cargo expand (muestra la salida de macro expandida). El desarrollo de macros es iterativo: escribe la macro, usa cargo expand para inspeccionar la salida, corrige problemas. Documenta claramente la sintaxis de la macro y las features soportadas.
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"
// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }
// tests/ui/my_macro.stderr - expected error
// error: ...
// Test runner
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/pass_*.rs");
t.compile_fail("tests/ui/fail_*.rs");
}
// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
eprintln!("Input tokens: {}", input);
let ast = syn::parse2(input.clone().into()).unwrap();
eprintln!("Parsed AST: {:#?}", ast);
TokenStream::new()
}Macros
macro_rules!
macro_rules! define macros declarativas. $x es una captura, expr coincide expresiones. $(...)* repite. Las macros se expanden en tiempo de compilación. Útil para reducir boilerplate. La macro estándar vec! funciona de forma similar.
macro_rules! vec_of {
($($x:expr),*) => {{
let mut v = Vec::new();
$(v.push($x);)*
v
}};
}
let nums = vec_of!(1, 2, 3);Procedural Macros
Las procedural macros generan código en tiempo de compilación. Tres tipos: derive (#[derive(Debug)]), attribute (#[my_attr]), function-like (my_macro!). Más poderosas que macro_rules! pero requieren un crate separado. Usadas por serde, tokio y diesel.
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Generate impl HelloMacro for the type
// Returns new TokenStream
}Common Macros
Macros integradas: println!/format! para output, vec! para vectores, assert!/assert_eq! para tests, dbg! para depurar, todo!/unreachable! para control flow. Todas están basadas en macro_rules!. dbg! retorna el valor para encadenamiento.
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable); // Debug print
todo!("Not implemented");
unreachable!();Macro Hygiene
Las macros de Rust son higiénicas: los identificadores introducidos por la macro no entran en conflicto con identificadores en el scope de llamada. Esto previene bugs sutiles. El temp dentro de la macro es diferente del temp externo. Las macros declarativas son siempre higiénicas.
macro_rules! swap {
($a:expr, $b:expr) => {
let temp = $a;
$a = $b;
$b = temp;
};
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x); // Works correctlyRepetition
Repetición en macros: $(...)* coincide cero o más, $(...)+ uno o más. El separador (coma) puede especificarse. $x captura cada valor. Útil para funciones tipo variádicas. La println! estándar usa esto para múltiples argumentos.
macro_rules! sum {
($($x:expr),*) => {
0 $(+ $x)*
};
}
let total = sum!(1, 2, 3, 4); // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing commaAsync Deep Dive
async/await
async fn retorna un Future. .await suspende hasta que el future esté listo. Los Futures son lazy: nada se ejecuta hasta que se awaited. El compilador transforma async fn en una state machine. Usa el runtime tokio o async-std para ejecutar.
async fn fetch_data() -> String {
// Simulate async work
String::from("data")
}
async fn process() {
let data = fetch_data().await;
println!("{}", data);
}Tokio Runtime
tokio::main habilita async main. spawn crea una tarea (como un green thread). join! espera múltiples futures concurrentemente. Tokio proporciona I/O, timers y scheduling. El runtime async más popular en Rust.
#[tokio::main]
async fn main() {
let task1 = tokio::spawn(async { work1().await });
let task2 = tokio::spawn(async { work2().await });
let (r1, r2) = tokio::join!(task1, task2);
}Channels (async)
Los canales mpsc (multi-producer, single-consumer) habilitan comunicación async. send/recv son async. El canal tiene un buffer (32 mensajes). Cuando todos los senders se dropean, recv retorna None. Útil para patrones producer-consumer.
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}Select
select! espera al primero de múltiples futures en completarse. Otros futures se dropean. Útil para timeouts y operaciones de racing. El patrón es común en servidores de red. Cada rama puede tener un guard pattern.
tokio::select! {
result = task1 => {
println!("Task1 done: {:?}", result);
}
result = task2 => {
println!("Task2 done: {:?}", result);
}
_ = tokio::time::sleep(Duration::from_secs(5)) => {
println!("Timeout");
}
}Stream
Stream es el equivalente async de Iterator. next().await obtiene el siguiente item. StreamExt proporciona map, filter, for_each. Útil para procesar chunks de datos de red o archivos. El crate async-stream simplifica la creación de streams.
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
println!("{}", x);
}).await;Cargo & Crates
Cargo.toml
Cargo.toml es el archivo manifiesto. [package] define metadatos. [dependencies] lista crates externos. Las features habilitan funcionalidad opcional. [dev-dependencies] son solo para tests. Edition 2021 es la última estable. Las versiones usan semver.
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
pretty_assertions = "1"Cargo Commands
cargo new crea un proyecto binario (--lib para biblioteca). build compila a target/. --release habilita optimizaciones. check es más rápido que build (sin codegen). clippy captura errores comunes. fmt formatea código. doc genera documentación HTML. add inserta una dependencia.
cargo new myapp # Create new project
cargo build # Compile
cargo build --release # Optimized build
cargo run # Build and run
cargo test # Run tests
cargo check # Fast type-check
cargo fmt # Format code
cargo clippy # Lint
cargo doc --open # Generate docs
cargo add serde # Add dependencyWorkspaces
Los workspaces agrupan múltiples crates que comparten un directorio target y Cargo.lock. Los miembros son crates individuales. [workspace.dependencies] centraliza las versiones de dependencias. Cada crate las referencia con workspace = true. Builds más rápidos debido a compilación compartida. Usado por proyectos grandes como rust-analyzer.
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]
# Shared dependencies
[workspace.dependencies]
serde = "1.0"
# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }Features
Las features habilitan compilación condicional. Las features por defecto se habilitan a menos que --no-default-features. cfg(feature = ...) gatea código. La sintaxis dep: en dependencias evita la unificación de features. Útil para funcionalidad opcional y código específico de plataforma. Los crates pueden exponer features a los consumidores.
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]
# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }Publishing
crates.io es el registro de paquetes de Rust. cargo login autentica. --dry-run captura problemas. Una vez publicado, una versión no puede republicarse (yank solo oculta de la búsqueda). Sigue semver: patch para fixes, minor para features, major para breaking changes. README y license son obligatorios.
# Login (one time)
cargo login <token>
# Check before publishing
cargo publish --dry-run
# Publish to crates.io
cargo publish
# Version bumping
cargo bump patch # 0.1.0 -> 0.1.1
cargo bump minor # 0.1.0 -> 0.2.0
cargo bump major # 0.1.0 -> 1.0.0Testing Rust
Unit Tests
Los tests viven en un módulo #[cfg(test)]. use super::* importa el padre. #[test] marca funciones de test. assert_eq! comprueba igualdad. #[should_panic] espera un pánico. Los tests se ejecutan con cargo test. Los unit tests se colocan junto al código. El atributo cfg(test) asegura que los tests no se compilen en release builds.
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic]
fn test_panic() {
panic!("expected");
}
}Integration Tests
Los integration tests viven en el directorio tests/. Cada archivo se compila como un crate separado. Solo pueden probar la API pública. Útiles para testing end-to-end. Ejecuta tests específicos con --test <name>. Los integration tests son más lentos de compilar pero prueban la interfaz real.
// tests/integration_test.rs
use myapp::add;
#[test]
fn test_add_integration() {
assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crateTest Organization
#[ignore] salta un test a menos que se pase --ignored. Filtra tests por patrón de nombre. --nocapture muestra la salida de println!. Los tests se ejecutan en paralelo por defecto. Usa --test-threads=1 para secuencial. Los harnesses personalizados pueden reemplazar el test runner por defecto. Útil para benchmarks y property tests.
#[test]
fn it_works() { /* ... */ }
// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }
// Run only ignored tests
cargo test -- --ignored
// Filter by name
cargo test test_add
// Show output
cargo test -- --nocaptureAssertions
assert! comprueba un booleano. assert_eq!/assert_ne! comparan valores con salida debug en caso de fallo. Los mensajes personalizados ayudan a depurar. Para coma flotante, usa el crate approx. Para igualdad parcial, implementa PartialEq. La salida debug muestra ambos valores cuando la aserción falla.
assert!(true); // Boolean
assert_eq!(2 + 2, 4); // Equality
assert_ne!(3, 4); // Inequality
assert!(x > 0, "x must be positive"); // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);Property Testing
proptest genera entradas aleatorias para encontrar casos falibles. Las Strategies (a in range) definen generadores de entrada. prop_assert! reporta fallos con contraejemplos mínimos. Shrinking encuentra la entrada falible más pequeña. Mejor que tests escritos a mano para casos límite. Similar a QuickCheck en Haskell.
// Cargo.toml: proptest = "1"
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
prop_assert_eq!(add(a, b), add(b, a));
}
#[test]
fn test_string_len(s in ".{0,100}") {
prop_assert!(s.len() <= 100);
}
}Fragmentos de Rust relacionados
Copy-paste ready code for common tasks.
Struct con Métodos
Definir un struct de Rust e implementar métodos con self y Self.
Ownership
Sistema de ownership de Rust.
Borrowing y Referencias
Referencias y borrows mutables.
Lifetimes
Anotaciones explícitas de lifetime.
Trait
Definir e implementar traits.
Genéricos
Funciones y structs genéricos.
Enum
Enums y Option.
Pattern Matching
match y destructuring.
Manejo de Errores
Result y el operador ?.
Iterador
Adaptadores y consumidores de iterador.
Closures
Closures y traits Fn.
Sistema de Módulos
Módulos, paths y visibilidad.
Programación Concurrente
Threads y channels.
Smart Pointers
Box, Rc, RefCell.
Macros
Macros declarativas y procedurales.
Unsafe Rust
Operaciones unsafe.
Was this helpful?