Cargo: Gestión de Proyectos
Cargo es la herramienta principal para compilar código, gestionar dependencias y distribuir paquetes en Rust.
verificar_versiones() {
cargo --version
rustc --version
}
Para iniciar un proyecto nuevo, use el comando crear_proyecto. Esto genera la estructura de directorios y un repositorio Git.
cargo new gestor_tareas
La compilación del proyecto se ejecuta con construir. El binario resultante se deposita en el directorio target/debug.
cargo build
El comando ejecutar compilará el código si es necesario y luego correrá el binario.
cargo run
Para una verificación rápida del código sin generar un ejecutable, se puede usar comprobar. Es más veloz que una compilación completa.
cargo check
Cuando el proyecto esté listo para producción, se construye con optimizaciones usando la bandera de lanzamiento.
cargo build --release
Sistema de Variables y Mutabilidad
Las variables son inmutables por defecto en Rust. Para permitir la modificación de su valor, se utiliza el modificador mut.
fn main() {
let asignacion = 10;
let mut contador = 0;
contador += 1;
println!("Contador: {}", contador);
}
Las constantes se definen con const y requieern anotación de tipo explícita. Su valor debe ser conocido en tiempo de compilación.
const MAX_CONEXIONES: u32 = 1000;
Tipos de Datos Fundamentales
Rust categoriza sus tipos en escalares y compuestos.
Tipos Escalares
Representan un valor único. Los principales son: enteros, flotantes, booleanos y caracteres.
Enteros: Se distinguen por tamaño y signo. Ejemplos: i32 (32 bits con signo), u8 (8 bits sin signo).
Punto Flotante: f64 (doble precisión) es el tipo por defecto.
fn main() {
let temperatura: f32 = 36.5;
let pi = 3.14159265358979; // f64 por defecto
}
Booleanos y Caracteres: Los booleanos (bool) representan true o false. Los caracteres (char) representan un valor Unicode de 4 bytes, designado por comillas simples.
fn main() {
let activo = true;
let simbolo = 'Ω';
}
Tipos Compuestos
Agrupan múltiples valores en un solo tipo.
Tuplas: Fijas en longitud, pueden contener tipos diferentes. Se accede a sus elementos por índice.
fn main() {
let coordenada: (i64, f64, &str) = (100, 5.6, "norte");
let (_, altitud, _) = coordenada; // Desestructuración
println!("Altitud: {}", altitud);
}
Arreglos: Todos los elementos deben ser del mismo tipo y la longitud es fija. Residen en la pila.
fn main() {
let meses: [&str; 4] = ["Ene", "Feb", "Mar", "Abr"];
let primer_mes = meses[0];
println!("Primer mes: {}", primer_mes);
}
Funciones y Flujo de Control
Las funciones se definen con fn. Las sentencias realizan acciones sin devolver valor; las expresiones calculan y retornan uno.
fn calcular_area(base: u32, altura: u32) -> u32 {
base * altura // Expresión final es el valor de retorno
}
fn main() {
let area = calcular_area(10, 5);
println!("Área: {}", area);
}
Estructuras de control comunes:
fn main() {
let x = 7;
if x % 2 == 0 {
println!("Par");
} else {
println!("Impar");
}
let mensaje = if x > 5 { "Grande" } else { "Pequeño" };
let mut suma = 0;
let mut i = 1;
while i <= 100 {
suma += i;
i += 1;
}
for elem in [10, 20, 30].iter() {
println!("Valor: {}", elem);
}
}
Concepto de Propiedad
Rust gestiona la memoria mediante un sistema de propiedad, sin recolector de basura. Cada valor tiene un único dueño.
La asignación de valores en el montículo (heap) causa un movimiento de propiedad, invalidando la variable original. Para duplicar datos del montículo, se usa el método clone().
fn main() {
let original = String::from("texto");
let duplicado = original.clone(); // Copia profunda
println!("{} {}", original, duplicado); // Ambas válidas
let numero_original = 5;
let numero_copia = numero_original; // Copia, no movimiento (tipo con Copy)
println!("{} {}", numero_original, numero_copia); // Ambas válidas
}
Los tipos que implementan el trait Copy (como enteros y booleanos) se copian, no se mueven.
Préstamos y Referencias
Se puede acceder a un valor sin tomar propiedad mediante referencias (&). Esto se conoce como préstamo.
Reglas clave:
- Múltiples referencias inmutables (
&T) están permitidas simultáneamente. - Solo puede existir una referencia mutable (
&mut T) a la vez, y no puede coexistir con referencias inmutables.
fn calcular_longitud(s: &String) -> usize {
s.len()
}
fn main() {
let saludo = String::from("Hola");
let longitud = calcular_longitud(&saludo); // Préstamo inmutable
println!("Longitud: {}", longitud);
}
Los slices (&[T], &str) son referencias a una secuencia contigua de elementos en una colección.
fn main() {
let frase = String::from("el mundo es bello");
let primera_palabra = &frase[0..8]; // Slice de string
println!("{}", primera_palabra);
let numeros = [10, 20, 30, 40, 50];
let sub_conjunto = &numeros[1..3]; // Slice de arreglo
println!("{:?}", sub_conjunto);
}
Estructuras y Enumeraciones
Las structs agrupan campos relacionados bajo un nombre.
struct Empleado {
nombre: String,
departamento: String,
antigüedad: u32,
}
fn main() {
let mut emp1 = Empleado {
nombre: String::from("Ana"),
departamento: String::from("TI"),
antigüedad: 3,
};
emp1.antigüedad += 1;
}
Las enums definen un tipo que puede ser una de varias variantes, cada una opcionalmente con datos adjuntos.
enum Comando {
Salir,
Desplazar { x: i32, y: i32 },
Escribir(String),
}
fn procesar(cmd: Comando) {
match cmd {
Comando::Salir => println!("Saliendo..."),
Comando::Desplazar { x, y } => println!("Moviendo a ({}, {})", x, y),
Comando::Esribir(msg) => println!("{}", msg),
}
}
La enum Option<T> es central en Rust para modelar la ausencia de valor, evitando null.
fn main() {
let valor_posible: Option<i32> = Some(42);
let valor_ausente: Option<i32> = None;
match valor_posible {
Some(n) => println!("Valor: {}", n),
None => println!("Sin valor"),
}
}
Colecciones Estándar
Tres colecciones fundamentales se encuentran en la biblioteca estándar:
Vector (Vec<T>): Lista dinámica y homogénea.
use std::collections::HashMap;
fn main() {
let mut notas: Vec<u8> = Vec::new();
notas.push(95);
notas.push(87);
let nota_promedio: f32 = notas.iter().sum::<u8>() as f32 / notas.len() as f32;
}
String: Texto UTF-8 con propiedad y capacidad de modificación.
fn main() {
let mut saludo = String::from("Hola");
saludo.push(' ');
saludo.push_str("mundo");
println!("{}", saludo);
}
HashMap (HashMap<K, V>): Colección de pares clave-valor.
use std::collections::HashMap;
fn main() {
let mut puntuaciones = HashMap::new();
puntuaciones.insert("Jugador A", 100);
puntuaciones.insert("Jugador B", 85);
let puntuación_jugador_a = puntuaciones.get("Jugador A").copied().unwrap_or(0);
}
Gestión de Errores
Rust distingue entre errores recuperables (Result<T, E>) e irrecuperables (panic!).
El operador ? propaga errores de manera concisa dentro de funciones que retornan Result.
use std::fs;
use std::io;
fn leer_configuracion(ruta: &str) -> Result<String, io::Error> {
let contenido = fs::read_to_string(ruta)?; // Propagación con ?
Ok(contenido)
}
fn main() {
match leer_configuracion("config.txt") {
Ok(config) => println!("Configuración: {}", config),
Err(e) => println!("Error leyendo configuración: {}", e),
}
}
Genericidad, Traits y Ciclo de Vida
Los genéricos permiten escribir código abstracto para múltiples tipos.
fn mayor<T: PartialOrd>(lista: &[T]) -> &T {
let mut mayor_elem = &lista[0];
for elem in &lista[1..] {
if elem > mayor_elem {
mayor_elem = elem;
}
}
mayor_elem
}
Los traits definen comportamientos compartidos, similar a interfaces.
trait Describible {
fn descripcion(&self) -> String;
fn resumen(&self) -> String {
format!("Resumen: {}", self.descripcion()) // Implementación por defecto
}
}
struct Articulo { titulo: String }
impl Describible for Articulo {
fn descripcion(&self) -> String {
self.titulo.clone()
}
}
Los ciclos de vida ('a) anotan las relaciones entre las longitudes de vida de las referencias, garantizando seguridad de memoria.
fn texto_mas_largo<'a>(t1: &'a str, t2: &'a str) -> &'a str {
if t1.len() > t2.len() { t1 } else { t2 }
}
Programación Concurrente
Rust ofrece concurrecnia sin data races a través de:
Hilos (std::thread):
use std::thread;
fn main() {
let handler = thread::spawn(|| {
for i in 1..5 {
println!("Hilo hijo: {}", i);
thread::sleep(std::time::Duration::from_millis(10));
}
});
for i in 1..3 {
println!("Hilo principal: {}", i);
thread::sleep(std::time::Duration::from_millis(15));
}
handler.join().unwrap(); // Espera a que el hilo hijo termine
}
Canales (std::sync::mpsc): Comunicación entre hilos mediante envío de mensajes.
use std::sync::mpsc;
use std::thread;
fn main() {
let (transmisor, receptor) = mpsc::channel();
thread::spawn(move || {
let dato = String::from("mensaje");
transmisor.send(dato).unwrap();
});
let recibido = receptor.recv().unwrap();
println!("Recibido: {}", recibido);
}
Mutex (std::sync::Mutex): Exclusión mutua para acceso seguro a datos compartidos.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador_compartido = Arc::new(Mutex::new(0));
let mut handlers = vec![];
for _ in 0..5 {
let contador = Arc::clone(&contador_compartido);
let handler = thread::spawn(move || {
let mut num = contador.lock().unwrap();
*num += 1;
});
handlers.push(handler);
}
for handler in handlers {
handler.join().unwrap();
}
println!("Resultado: {}", *contador_compartido.lock().unwrap());
}