Rust: propiedad, préstamos y un primer programa CLI
En esta página
Rust comprueba en tiempo de compilación las reglas de uso de memoria mediante la propiedad y la comprobación de tipos. En lugar de que un recolector de basura siga la alcanzabilidad de los objetos como en Java y Kotlin, el código expresa quién posee un valor y durante cuánto tiempo se presta. Así, los errores del compilador pueden leerse como contratos de uso de datos y no como simples restricciones sintácticas.
Esta introducción está dirigida a desarrolladores que ya han escrito funciones en otro lenguaje. Los ejemplos se ejecutaron con Rust 1.100.0, edición 2024, en macOS arm64. La edición selecciona las reglas de sintaxis y compatibilidad independientemente de la versión del compilador. Los ejemplos solo usan la biblioteca estándar y no realizan comunicaciones externas.
¿Para qué problemas puede servir Rust?
Rust se compila a código nativo y su gestión de memoria predeterminada no requiere un recolector de basura de rastreo. Es una opción que conviene estudiar para herramientas CLI, componentes de sistema y programas con presupuestos exigentes de memoria o latencia. Su introducción oficial también presenta el rendimiento y la seguridad de memoria como objetivos. La ausencia de GC por sí sola no demuestra que Rust sea más rápido ni tenga menor latencia que Java. Este artículo no mide el rendimiento comparativo.
| Criterio | Java/Kotlin en la JVM | Rust |
|---|---|---|
| Gestión habitual de memoria de objetos | El GC rastrea los objetos que puede recuperar | Los recursos se liberan mediante la propiedad y Drop |
| Transferencia de datos | Varias partes pueden conservar referencias a objetos | Los valores se mueven o copian según su tipo y pueden prestarse por referencia |
| Comunicación de errores | Excepciones, tipos de resultado y otros mecanismos | Las API suelen expresar éxito o error con Result<T, E> |
| Costes de adopción que revisar | Bibliotecas JVM y experiencia del equipo existentes | Aprendizaje de propiedad y adecuación de bibliotecas y herramientas de compilación y operación |
Si el cuello de botella de un servicio existente es la espera de base de datos o de una API externa, mide primero esas rutas antes de cambiar de lenguaje. Para una primera adopción, empieza por una CLI pequeña con límites claros y evalúa los costes de desarrollo y despliegue.
¿En qué se diferencian la propiedad y los préstamos?
Primero instala rustup y Rust siguiendo las instrucciones oficiales para tu sistema operativo y luego ejecuta los ejemplos. En macOS instala también las Command Line Tools necesarias para el enlace nativo.
Un String posee los datos de su cadena. Guarda el siguiente código como move_error.rs: fallará intencionadamente al compilar.
fn main() {
let name = String::from("Rust");
let moved = name;
println!("{name}, {moved}");
}
rustc --edition 2024 move_error.rs
Como let moved = name transfiere la propiedad, el intento posterior de imprimir name produce E0382: borrow of moved value. No toda asignación mueve un valor: los tipos que implementan Copy, como u64, se copian al asignarse. String::clone() duplica los datos de la cadena, así que úsalo solo cuando sea necesario. Consulta la explicación oficial de propiedad para conocer las reglas.
Si solo necesitas leer un valor, puedes tomarlo prestado con & en vez de transferir su propiedad. Guarda el siguiente código en un archivo independiente borrow_ok.rs.
fn main() {
let mut name = String::from("Rust");
let borrowed = &name;
println!("{borrowed}");
name.push_str(" language");
println!("{name}");
}
rustc --edition 2024 borrow_ok.rs -o borrow_ok
./borrow_ok
La salida real es:
Rust
Rust language
Después del primer println!, borrowed no vuelve a usarse, por lo que termina el préstamo compartido. Esto permite modificar name después dentro del mismo bloque. Si vuelves a imprimir borrowed después de push_str, el periodo necesario de esa referencia se solapa con el préstamo exclusivo para modificar y la compilación falla.
Una regla útil para empezar es varias referencias compartidas &T o una referencia exclusiva &mut T a los mismos datos. Esto no significa que &T prohíba toda mutabilidad interior. La documentación oficial de préstamos detalla las condiciones. Un lifetime expresa relaciones entre los periodos durante los que las referencias deben ser válidas; no mantiene viva la memoria durante más tiempo.
Crear una CLI que suma importes con Cargo
El programa imprime la cantidad y la suma de importes enteros. Es un ejemplo explicativo, no una implementación de pagos de un servicio concreto, y supone que todos los importes usan la misma unidad. No acepta decimales ni negativos; cada importe y el total deben caber en u64.
Estos comandos utilizan un shell de macOS o Linux. Prepara la versión del ejercicio en un entorno con las herramientas de Rust instaladas.
rustup toolchain install 1.100.0 --profile minimal --component rustfmt --component clippy
cargo +1.100.0 new expense-summary --bin --edition 2024
cd expense-summary
Prepara los tres archivos siguientes. rust-toolchain.toml fija la versión de las herramientas para esta carpeta y Cargo.toml contiene la configuración del paquete. La introducción oficial a Cargo también explica sus comandos de compilación y comprobación.
Cargo.toml
[package]
name = "expense-summary"
version = "0.1.0"
edition = "2024"
[dependencies]
rust-toolchain.toml
[toolchain]
channel = "1.100.0"
profile = "minimal"
components = ["rustfmt", "clippy"]
El diagrama muestra el bucle y las devoluciones anticipadas de summarize. La línea que vuelve a comprobar el siguiente valor por la izquierda representa la repetición tras acumular correctamente; todas las rutas de error de la derecha confluyen en la rama Err de main.
src/main.rs
use std::{env, process::ExitCode};
struct Summary {
count: usize,
total: u64,
}
#[derive(Debug)]
enum InputError {
EmptyInput,
InvalidAmount,
TotalOverflow,
}
fn summarize(inputs: &[String]) -> Result<Summary, InputError> {
if inputs.is_empty() {
return Err(InputError::EmptyInput);
}
let mut total = 0_u64;
for input in inputs {
let amount = input
.parse::<u64>()
.map_err(|_| InputError::InvalidAmount)?;
total = total.checked_add(amount).ok_or(InputError::TotalOverflow)?;
}
Ok(Summary {
count: inputs.len(),
total,
})
}
fn main() -> ExitCode {
let inputs: Vec<String> = env::args().skip(1).collect();
match summarize(&inputs) {
Ok(summary) => {
println!("count={}, total={}", summary.count, summary.total);
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("error: {error:?}");
ExitCode::FAILURE
}
}
}
inputs es un Vec<String> del que main es propietario. summarize(&inputs) toma prestadas sus cadenas como una porción &[String], sin clonar todos los datos ni transferir su propiedad. for input in inputs también lee las cadenas por referencia. El préstamo evita copiar las cadenas de entrada en este ejemplo, pero no elimina las asignaciones al recopilar los argumentos de línea de comandos.
Summary es una estructura que agrupa el resultado correcto e InputError es una enumeración de posibles errores. Result<Summary, InputError> es el contrato que devuelve uno de esos dos resultados. ? devuelve anticipadamente desde la función actual al encontrar Err y extrae el valor interno en caso de éxito. No lanza una excepción ni termina el programa de inmediato. La explicación oficial de Result describe la propagación de errores.
parse::<u64>() comprueba el formato y rango de cada número. Si la suma de dos valores supera el rango, checked_add devuelve None y ok_or lo convierte en el error TotalOverflow. Así, aunque cada entrada sea válida, la suma puede fallar. main usa match para gestionar ambos resultados y {:?} imprime el nombre del error con el formato Debug.
¿Qué puedes comprobar al ejecutarlo?
rustc --version
cargo check --offline
cargo fmt --check
cargo clippy --offline -- -D warnings
cargo run --offline --quiet -- 12000 8000 0
La salida estándar de esa ejecución aparece a continuación. Los valores después de -- se pasan al programa del ejemplo, no a Cargo.
count=3, total=20000
Estos son resultados representativos observados en ejecuciones reales de la CLI. Si hay un error no se imprime una suma parcial; los errores se envían a la salida de error estándar y terminan con el código 1.
Argumentos después de cargo run --offline --quiet -- |
Salida | Código de salida |
|---|---|---|
12000 8000 0 |
salida estándar count=3, total=20000 |
0 |
0 |
salida estándar count=1, total=0 |
0 |
| Sin argumentos | error estándar error: EmptyInput |
1 |
12000 abc |
error estándar error: InvalidAmount |
1 |
-1 o 1.5 |
error estándar error: InvalidAmount |
1 |
18446744073709551616 |
error estándar error: InvalidAmount |
1 |
18446744073709551615 0 |
salida estándar count=2, total=18446744073709551615 |
0 |
18446744073709551615 1 |
error estándar error: TotalOverflow |
1 |
cargo check comprueba los tipos y otras propiedades sin ejecutar el código. cargo fmt --check revisa el formato y Clippy ofrece diagnósticos estáticos. Aunque estos comandos tengan éxito, ejecuta el programa por separado para comprobar las reglas de cálculo. Aquí se compararon la salida, los errores y los códigos de salida en ejecuciones debug y release; no se midieron el rendimiento ni la carga de producción.
¿Qué garantiza Rust?
Las comprobaciones de tipos y propiedad de Safe Rust ayudan a prevenir el uso incorrecto de referencias y las carreras de datos. La posibilidad de mover valores entre hilos y compartir referencias también depende de contratos de tipo como Send y Sync. Esto no elimina todas las condiciones de carrera, bloqueos mutuos, fugas de memoria ni errores de lógica de negocio.
unsafe hace que quien escribe el código sea responsable de las condiciones de seguridad de ciertas operaciones. No es un interruptor que desactive todo el comprobador de préstamos; una implementación incorrecta de unsafe o los límites de FFI pueden afectar a la seguridad. El diseño de servidores, ejecución asíncrona y estado compartido no se incluyó en la validación de esta CLI.
Como siguiente ejercicio, amplía el programa para leer los importes de un archivo. Decide cómo distinguir los errores de E/S del archivo del InputError existente para entender con más detalle cómo la propiedad y los tipos de error expresan los límites del programa.
Comentarios