Dominar el sistema de propiedad de Rust: la guía completa para la seguridad de la memoria sin recolección de basura
1. Por qué es importante la propiedad: el problema de los 2 billones de dólares
Los errores de seguridad de la memoria le cuestan a la industria del software aproximadamente 2 billones de dólares al año. Microsoft informa que el 70% de sus vulnerabilidades de seguridad son problemas de seguridad de la memoria. El equipo de Chrome de Google encontró cifras similares. Estos errores incluyen:
- Uso después de la liberación: acceso a la memoria que ha sido desasignada
- Doble liberación: Liberar memoria dos veces, provocando corrupción
- Desbordamientos de búfer: Escritura más allá de la memoria asignada
- Carreras de datos: acceso simultáneo que provoca un comportamiento impredecible
- Pérdidas de memoria: Olvidar liberar la memoria asignada
La compensación tradicional
Históricamente, los lenguajes de programación han elegido uno de dos enfoques:
Enfoque 1: Gestión manual de la memoria (C/C++)
// Código C: propenso a errores
char* crear_mensaje() {
char* mensaje = malloc(100);
strcpy(msj, "Hola");
devolver mensaje; // ¡La persona que llama debe recordar liberar!
}
proceso nulo() {
char* m = crear_mensaje();
printf("%s", m);
// Olvidé liberar (m) - ¡pérdida de memoria!
}
Problemas: Requiere una disciplina perfecta, es fácil cometer errores y vulnerabilidades de seguridad.
Enfoque 2: Recolección de basura (Java/Go/JavaScript)
// Código Java: seguro pero con sobrecarga de tiempo de ejecución
Cadena crear mensaje() {
devolver "Hola"; // GC se limpiará eventualmente
}
// Seguro, pero las pausas de GC afectan el rendimiento
Problemas: pausas impredecibles, sobrecarga de memoria, menos control sobre el rendimiento.
La solución revolucionaria de Rust
Rust proporciona una tercera vía: seguridad de la memoria sin recolección de basura mediante verificación de propiedad en tiempo de compilación. Obtienes:
- ✅ Seguridad de la memoria garantizada en el momento de la compilación
- ✅ Sin sobrecarga de tiempo de ejecución (abstracciones de costo cero)
- ✅ Sin pausas del recolector de basura
- ✅ Simultaneidad intrépida (carreras de datos imposibles)
- ✅ Rendimiento predecible
¿El truco? Debes aprender el sistema de propiedad. Esta guía lo dejará muy claro.
2. Las tres reglas de oro de la propiedad
Cada programa Rust sigue estas tres reglas, que se aplican en el momento de la compilación:
Regla 1: cada valor tiene un único propietario
fn principal() {
let s = String::from("hola"); // s posee la cadena
// Sólo una variable puede poseer estos datos a la vez
} // s sale del alcance, la memoria se libera automáticamente
Regla 2: Cuando el propietario sale del alcance, el valor se elimina
fn principal() {
{
let s = String::from("hola"); // s es válido desde aquí
// hacer cosas con s
} // s sale del alcance y se elimina, se libera memoria
// println!("{}", s); // ERROR: s ya no existe
}
Regla 3: Puede tener una referencia mutable O varias referencias inmutables
fn principal() {
let mut s = String::from("hola");
// Múltiples referencias inmutables - OK
sea r1 = &s;
sea r2 = &s;
println!("{} y {}", r1, r2);
// Una referencia mutable - OK (después de r1, r2 ya no se usan)
sea r3 = &mut s;
r3.push_str("mundo");
println!("{}", r3);
}
¿Por qué estas reglas?
- Reglas 1 y 2: Previene pérdidas de memoria y errores de doble liberación
- Regla 3: Previene carreras de datos en tiempo de compilación
3. Semántica de movimiento: comprensión de la transferencia de propiedad
El problema: la copia ingenua es cara
fn principal() {
let s1 = String::from("hola");
sea s2 = s1; // ¿Qué pasa aquí?
// println!("{}", s1); // ERROR: valor movido a s2
}
Lo que realmente sucede:
Pila: Montón:
s1 -> [ptr, len, cap] -> datos de "hola"
|
| (mover)
v
s2 -> [ptr, len, cap] -> (mismos datos del montón)
Rust mueve la propiedad en lugar de copiar datos del montón. Después de "let s2 = s1", solo "s2" es válido. Esto previene:
- Copias profundas costosas por defecto
- Errores de doble liberación (sólo s2 liberará la memoria)
¿Cuándo Rust copia en lugar de mover?
Los tipos que implementan el rasgo "Copiar" se copian en lugar de moverse:
fn principal() {
// Enteros, flotantes, bools y caracteres implementan Copiar
sea x = 5;
sea y = x; // se copia x en y
println!("x = {}, y = {}", x, y); // ¡Ambos válidos!
// Las tuplas de tipos Copiar también son Copiar
sea el punto = (3, 4);
let punto2 = punto; // copiado
println!("{:?} y {:?}", punto, punto2); // ¡Ambos válidos!
}
Regla general: si un tipo almacena datos en el montón o posee recursos, no implementará "Copiar".
Clonación explícita cuando la necesitas
fn principal() {
let s1 = String::from("hola");
let s2 = s1.clone(); // Copia profunda explícita
println!("s1 = {}, s2 = {}", s1, s2); // Ambos válidos
}
Usar clon cuando:
- Necesitas copias independientes.
- El costo de rendimiento es aceptable.
- Deja clara la intención
Evitar clonar cuando:
- Puedes reestructurar para utilizar préstamos en su lugar.
- El rendimiento es fundamental
- Trabajar en bucles calientes.
4. Préstamo: referencias que no son propias
El préstamo le permite hacer referencia a datos sin asumir la propiedad.
Referencias inmutables (&T)
fn principal() {
let s = String::from("hola");
let len = calcular_longitud(&s); // pedir prestado
println!("La longitud de '{}' es {}", s, len); // ¡sigue siendo válido!
}
fn calcular_longitud(s: &String) -> usar tamaño {
en.len()
} // s sale del alcance, pero no es propietario de los datos, por lo que no sucede nada
Puntos clave:
&screa una referencia a s- Las referencias son inmutables por defecto.
- Se permiten múltiples referencias inmutables.
- El propietario original aún puede leer los datos.
Referencias mutables (&mut T)
fn principal() {
let mut s = String::from("hola");
cambiar(&mut s); // Tomar prestado mutablemente
println!("{}", s); // Imprime "hola, mundo"
}
fn cambio(s: &mut String) {
s.push_str(", mundo");
}
Restricción crítica: ¡Solo una referencia mutable a la vez!
fn principal() {
let mut s = String::from("hola");
sea r1 = &mut s;
sea r2 = &mut s; // ERROR: no se puede pedir prestado como mutable más de una vez
println!("{}, {}", r1, r2);
}
¿Por qué? Previene carreras de datos en tiempo de compilación:
// Esto es imposible en Rust (se compilaría en C++)
let mut datos = vec![1, 2, 3];
let ref1 = &mut datos;
let ref2 = &mut datos;
ref1.push(4); // Podría reasignarse
ref2.push(5); // ¡Podría causar uso después de la liberación!
El secreto del verificador de préstamos: vidas no léxicas (NLL)
Modern Rust (edición 2018+) es más inteligente acerca de cuándo terminan las referencias:
fn principal() {
let mut s = String::from("hola");
sea r1 = &s;
sea r2 = &s;
println!("{} y {}", r1, r2);
// r1 y r2 ya no se usan después de este punto
sea r3 = &mut s; // ¡DE ACUERDO! r1 y r2 están "muertos"
r3.push_str("mundo");
println!("{}", r3);
}
Antes de NLL (edición de 2015), esto no se compilaba. Ahora el compilador rastrea dónde realmente se usan las referencias, no solo su alcance léxico.
Patrón de préstamo común: lecturas múltiples, escritura única
fn principal() {
let mut datos = vec![1, 2, 3, 4, 5];
// Fase de lectura: múltiples préstamos inmutables
dejar primero = &datos[0];
let last = &data[data.len() - 1];
println!("primero: {}, último: {}", primero, último);
// Fase de escritura: préstamo mutable exclusivo
datos.push(6);
println!("Actualizado: {:?}", datos);
}
5. Lifetimes: Enseñar al compilador sobre las referencias
El problema que resuelven las vidas
fn más largo (x: &str, y: &str) -> &str {
si x.len() > y.len() {
x // ¿Qué vida útil debería tener la devolución?
} más {
y // ¿la vida útil de x o la vida útil de y?
}
}
El compilador no puede determinar si la referencia devuelta es válida:
fn principal() {
let string1 = String::from("cadena larga");
dejar resultado;
{
let string2 = String::from("corto");
resultado = más largo (&cadena1, &cadena2);
} // cadena2 caída aquí
// ¿Es válido el resultado? ¡Depende de qué entrada se devolvió!
}
Anotaciones de por vida: contratos explícitos
fn más largo<'a>(x: &'a str, y: &'a str) -> &'a str {
si x.len() > y.len() {
x
} más {
y
}
}
Leyendo esto: "La referencia devuelta será válida siempre que tanto x como y sean válidos".
fn principal() {
let string1 = String::from("cadena larga");
dejar resultado;
{
let string2 = String::from("corto");
resultado = más largo (&cadena1, &cadena2);
println!("{}", resultado); // OK: ambos siguen siendo válidos
} // cadena2 eliminada
// println!("{}", resultado); // ERROR: es posible que se haya devuelto la cadena2
}
Elisión de por vida: cuando no necesitas anotaciones
El compilador puede inferir vidas en patrones comunes:
// No se necesitan anotaciones: duración de entrada única
fn primera_palabra(s: &cadena) -> &cadena {
s.split_whitespace().next().unwrap_or("")
}
// El compilador ve esto como:
fn primera_palabra<'a>(s: &'a cadena) -> &'a cadena {
s.split_whitespace().next().unwrap_or("")
}
Reglas de elisión:
- Cada referencia de entrada tiene su propia vida útil.
- Si hay exactamente una vida útil de entrada, la salida obtiene esa vida útil.
- Si el método tiene "& self", la salida obtiene la duración de "self"
Vidas en estructuras
Cuando las estructuras contienen referencias, debes anotar las duraciones:
estructura Extracto importante<'a> {
parte: &'a str,
}
fn principal() {
let novel = String::from("Llámame Ismael. Hace algunos años...");
let first_sentence = novel.split('.').next().unwrap();
let extracto = Extracto importante {
parte: primera_frase,
};
println!("{}", extracto.parte);
} // extracto y novela eliminados, todo bien
Significado: Un ExtractoImportante no puede sobrevivir a los datos a los que hace referencia.
fn principal() {
dejar extracto;
{
let novel = String::from("Llámame Ismael.");
extracto = Extracto importante {
parte: &novela,
};
} // ERROR: novela eliminada, extracto.parte quedaría colgando
// println!("{}", extracto.parte);
}
La 'vida útil estática
'estático significa "vive durante toda la duración del programa":
// Los literales de cadena tienen una duración estática
let s: &'static str = "Estoy almacenado en el binario";
// variables estáticas
estático GLOBAL: &str = "También 'estático";
Error común: ¡No utilices "estático" sólo para que los errores desaparezcan!
// MALO: Forzar la compilación de 'static
fn bad_function() -> &'cadena estática {
let s = String::from("hola");
// &s // No puedo regresar - no vive lo suficiente
// ¡Perder memoria para obtener estática está mal!
}
// BUENO: devolver datos propios en su lugar
fn good_function() -> Cadena {
Cadena::de("hola")
}
6. Errores comunes y cómo solucionarlos
Error 1: No se puede pedir prestado como mutable porque ya está prestado
// ERROR
fn principal() {
let mut vec = vec![1, 2, 3];
dejar primero = &vec[0]; // Préstamo inmutable
vec.push(4); // ERROR: préstamo mutable
println!("{}", primero);
}
Por qué falla: push podría reasignarse, invalidando first.
Solución 1: Reestructurar para separar préstamos
fn principal() {
let mut vec = vec![1, 2, 3];
let primer_valor = vec[0]; // Copia el valor
vec.push(4); // OK: no hay préstamos pendientes
println!("{}", primer_valor);
}
Solución 2: Clonar los datos antes de mutar
fn principal() {
let mut vec = vec![1, 2, 3];
let first = vec.get(0).cloned(); // Opción<i32>, sin préstamo
vec.push(4);
si let Some(val) = primero {
println!("{}", valor);
}
}
Error 2: No se puede devolver la referencia a la variable local
// ERROR
fn create_string() -> &Cadena {
let s = String::from("hola");
&s // ERROR: devuelve una referencia a los datos propiedad de la función
} // s se elimina aquí, ¡devolvería un puntero colgante!
Solución: devolver datos propios
fn create_string() -> Cadena {
String::from("hola") // Propiedad transferida a la persona que llama
}
Error 3: No se puede salir del contenido prestado
// ERROR
fn principal() {
let vec = vec![String::from("a"), String::from("b")];
deja primero = &vec;
dejar tomado = vec[0]; // ERROR: no se puede salir del contenido indexado
}
Solución 1: clonar el valor
fn principal() {
let vec = vec![String::from("a"), String::from("b")];
dejar tomado = vec[0].clone();
println!("{}", tomado);
}
Solución 2: Utilice métodos que transfieran la propiedad
fn principal() {
let mut vec = vec![String::from("a"), String::from("b")];
dejar tomado = vec.swap_remove(0); // Toma posesión
println!("{}", tomado);
}
Error 4: Préstamos simultáneos mutables e inmutables
// ERROR
fn principal() {
let mut map = HashMap::new();
map.insert("clave", "valor");
let valor = map.get("clave");
map.insert("clave2", "valor2"); // ERROR: no se puede mutar mientras está prestado
println!("{:?}", valor);
}
Solución: Utilice la API de entrada
fn principal() {
let mut map = HashMap::new();
map.insert("clave", "valor");
map.entry("key2").or_insert("value2"); // No hay préstamos conflictivos
si let Some(valor) = map.get("clave") {
println!("{}", valor);
}
}
Error 5: Desajuste de duración en estructuras
// ERROR
contenedor de estructura {
datos: &str, // ERROR: falta anotación de duración
}
Solución: Agregar parámetro de duración
estructura contenedor<'a> {
datos: &'una cadena,
}
impl<'a> Contenedor<'a> {
fn nuevo (texto: &'a str) -> Self {
Contenedor {datos: texto}
}
fn get_data(&self) -> &str {
auto.datos
}
}
Error 6: Invalidación del iterador
// ERROR
fn principal() {
let mut vec = vec![1, 2, 3, 4, 5];
para yo en & vec {
si *yo % 2 == 0 {
vec.push(*i * 2); // ERROR: no se puede modificar durante la iteración
}
}
}
Solución: recopilar índices primero
fn principal() {
let mut vec = vec![1, 2, 3, 4, 5];
let to_add: Vec<i32> = vec.iter()
.filtro(|&&x| x % 2 == 0)
.mapa(|&x| x * 2)
.recoger();
vec.extend(to_add);
println!("{:?}", vec);
}
7. Patrones avanzados: más allá de la propiedad básica
Patrón 1: Mutabilidad interior con RefCell
A veces es necesario mutar datos con solo una referencia inmutable:
utilizar std::cell::RefCell;
Registrador de estructuras {
registros: RefCell<Vec<String>>, // Puede mutar a través de &self
}
registrador implícito {
fn nuevo() -> Uno mismo {
Registrador {
registros: RefCell::nuevo(Vec::nuevo()),
}
}
fn log(&self, mensaje: &str) { // Toma &self, no &mut self
self.logs.borrow_mut().push(message.to_string());
}
fn print_logs(&yo) {
para iniciar sesión self.logs.borrow().iter() {
println!("{}", iniciar sesión);
}
}
}
fn principal() {
let registrador = Registrador::nuevo();
logger.log("Primer mensaje");
logger.log("Segundo mensaje");
registrador.print_logs();
}
Cuándo utilizar:
- Implementación de cachés o registradores.
- Estructuras de gráficos o árboles con mutabilidad interior.
- Objetos simulados en pruebas.
Precaución: Comprobación de préstamos en tiempo de ejecución: ¡entra en pánico si se infringen las reglas!
let celda = RefCell::new(5);
dejar prestado1 = cell.borrow();
dejar prestado2 = cell.borrow_mut(); // PÁNICO: ¡ya prestado!
Patrón 2: Conteo de referencias con Rc
Compartir la propiedad de los datos con varios propietarios:
utilizar std::rc::Rc;
estructura nodo {
valor: i32,
hijos: Vec<Rc<Nodo>>,
}
fn principal() {
let hoja = Rc::nuevo(Nodo {
valor: 3,
niños: vec![],
});
let rama1 = Rc::nuevo(Nodo {
valor: 1,
niños: vec![Rc::clone(&leaf)], // Propiedad compartida
});
let rama2 = Rc::nuevo(Nodo {
valor: 2,
hijos: vec![Rc::clone(&leaf)], // Ambas ramas tienen su propia hoja
});
println!("Recuento de referencias de hojas: {}", Rc::strong_count(&leaf)); // 3
}
Puntos clave:
- No es seguro para subprocesos (use
Arcpara subprocesos) - El recuento de referencias tiene gastos generales.
- Crea ciclos si no se tiene cuidado (use "Débil" para romper ciclos)
Patrón 3: Combinando Rc y RefCell
Múltiples propietarios con mutación:
utilizar std::rc::Rc;
utilizar std::cell::RefCell;
#[derivar(Depurar)]
estructura contadorcompartido {
recuento: Rc<RefCell<i32>>,
}
implícito contador compartido {
fn nuevo() -> Uno mismo {
Contador compartido {
recuento: Rc::nuevo(RefCell::nuevo(0)),
}
}
fn incremento(&yo) {
*self.count.borrow_mut() += 1;
}
fn obtener(&self) -> i32 {
*auto.cuenta.pedir prestado()
}
}
fn principal() {
let contador1 = SharedCounter::new();
let contador2 = contadorcompartido {
contar: Rc::clon(&counter1.count),
};
contador1.incremento();
contador2.increment();
println!("Contar: {}", contador1.get()); // 2
}
Patrón 4: Patrón de constructor con propiedad
servidor de estructura {
anfitrión: cadena,
puerto: u16,
tiempo de espera: u64,
}
estructura ServerBuilder {
anfitrión: Opción<Cadena>,
puerto: Opción<u16>,
tiempo de espera: Opción<u64>,
}
implícito ServerBuilder {
fn nuevo() -> Uno mismo {
Constructor de servidores {
anfitrión: Ninguno,
puerto: Ninguno,
tiempo de espera: Ninguno,
}
}
fn host(mut self, host: impl Into<String>) -> Self {
self.host = Algunos(host.into());
self // Devolver la propiedad
}
puerto fn (mut self, puerto: u16) -> Self {
self.port = Algunos(puerto);
yo
}
fn tiempo de espera (mut self, tiempo de espera: u64) -> Self {
self.timeout = Algunos (tiempo de espera);
yo
}
fn build(self) -> Resultado<Servidor, &'cadena estática> {
Ok (servidor {
host: self.host.ok_or("Se requiere host")?,
puerto: self.port.unwrap_or(8080),
tiempo de espera: self.timeout.unwrap_or(30),
})
}
}
fn principal() {
dejar servidor = ServerBuilder::nuevo()
.host("localhost")
.puerto(3000)
.tiempo de espera(60)
.construir()
.desenvolver();
println!("Servidor: {}:{}", servidor.host, servidor.puerto);
}
Patrón 5: RAII (La adquisición de recursos es inicialización)
La propiedad permite la limpieza automática de recursos:
utilizar std::fs::Archivo;
utilizar std::io::{self, Write};
estructura archivo de registro {
archivo: archivo,
}
implica archivo de registro {
fn nuevo(ruta: &str) -> io::Resultado<Self> {
Ok (archivo de registro {
archivo: Archivo::crear(ruta)?,
})
}
fn write_log(&mut self, mensaje: &str) -> io::Resultado<()> {
writeln!(self.file, "{}", mensaje)
}
}
implica soltar para LogFile {
fn soltar (& mut yo) {
println!("Cerrando archivo de registro");
// El archivo se cierra automáticamente cuando se suelta
}
}
fn principal() -> io::Resultado<()> {
{
let mut log = LogFile::new("app.log")?;
log.write_log("Aplicación iniciada"?;
log.write_log("Procesando datos"?;
} // El archivo se cierra automáticamente aquí mediante Drop
println!("El archivo de registro se cierra automáticamente");
Ok(())
}
8. Estrategias de refactorización del mundo real
Escenario 1: pasar datos a funciones
Antes (luchando contra el verificador de préstamos):
estructura usuario {
nombre: cadena,
correo electrónico: cadena,
}
fn usuario_proceso(usuario: Usuario) {
println!("Procesando {}", nombre.usuario);
}
fn principal() {
dejar usuario = Usuario {
nombre: Cadena::de("Alicia"),
correo electrónico: Cadena::de("[email protected]"),
};
usuario_proceso(usuario);
// println!("{}", nombre.usuario); // ERROR: usuario movido
}
Después (pedir prestado en lugar de mudarse):
fn Process_user(usuario: &Usuario) { // Pedir prestado en su lugar
println!("Procesando {}", nombre.usuario);
}
fn principal() {
dejar usuario = Usuario {
nombre: Cadena::de("Alicia"),
correo electrónico: Cadena::de("[email protected]"),
};
usuario_proceso(&usuario);
println!("{}", nombre.usuario); // OK: el usuario aún es propietario
}
Escenario 2: Trabajar con colecciones
Antes:
fn get_first_name(usuarios: Vec<Usuario>) -> Opción<Cadena> {
usuarios.first().map(|u| u.name.clone()) // Clon innecesario
}
fn principal() {
dejar usuarios = vec![/* ... */];
let nombre = get_first_name(usuarios);
// Ya no puedo usar usuarios - movido
}
Después:
fn get_first_name(usuarios: &[Usuario]) -> Opción<&str> {
usuarios.first().map(|u| u.nombre.as_str())
}
fn principal() {
dejar usuarios = vec![/* ... */];
let nombre = get_first_name(&usuarios);
// usuarios todavía utilizables
}
Escenario 3: Estructura con múltiples campos de cadena
Antes (mucha clonación):
fn build_full_name(primero: Cadena, último: Cadena) -> Cadena {
formato!("{} {}", primero, último)
}
fn principal() {
let first = String::from("Juan");
let last = String::from("Doe");
let full = build_full_name(first.clone(), last.clone());
println!("Primero: {}, Último: {}", primero, último);
}
Después (use trozos de hilo):
fn build_full_name(primero: &cadena, último: &cadena) -> Cadena {
formato!("{} {}", primero, último)
}
fn principal() {
let first = String::from("Juan");
let last = String::from("Doe");
let full = build_full_name(&primero, &último);
println!("Primero: {}, Último: {}", primero, último);
}
Escenario 4: resultados de almacenamiento en caché
Problema: Necesita caché mutable con métodos inmutables
Solución: Mutabilidad interior
utilizar std::cell::RefCell;
utilizar std::collections::HashMap;
estructura CalculadoraCara {
caché: RefCell<HashMap<i32, i32>>,
}
impl CalculadoraCara {
fn nuevo() -> Uno mismo {
Calculadora cara {
caché: RefCell::new(HashMap::new()),
}
}
fn calcular(&self, entrada: i32) -> i32 { // &self, no &mut self
// Comprobar caché
si let Some(&cached) = self.cache.borrow().get(&input) {
devolver en caché;
}
// Cálculo caro
let resultado = entrada * entrada;
// Almacenar en caché
self.cache.borrow_mut().insert(entrada, resultado);
resultado
}
}
fn principal() {
let calc = CalculadoraCara::nueva();
println!("{}", calc.calcular(5)); // Calculado
println!("{}", calc.calcular(5)); // Desde el caché
}
Escenario 5: Estructuras de árboles
Desafío: Las relaciones entre padres e hijos crean conflictos de endeudamiento
Solución: Usar índices o Rc/Débil
utilizar std::rc::{Rc, Débil};
utilizar std::cell::RefCell;
estructura nodo {
valor: i32,
padre: RefCell<Débil<Nodo>>,
hijos: RefCell<Vec<Rc<Nodo>>>,
}
implicar nodo {
fn nuevo (valor: i32) -> Rc<Self> {
Rc::nuevo(Nodo {
valor,
padre: RefCell::nuevo(Débil::nuevo()),
niños: RefCell::new(vec![]),
})
}
fn add_child(padre: &Rc<Nodo>, hijo: Rc<Nodo>) {
*child.parent.borrow_mut() = Rc::downgrade(padre);
padre.niños.borrow_mut().push(niño);
}
}
fn principal() {
let root = Nodo::nuevo(1);
let niño1 = Nodo::nuevo(2);
let niño2 = Nodo::nuevo(3);
Nodo::add_child(&root, child1);
Nodo::add_child(&root, child2);
println!("La raíz tiene {} hijos", root.children.borrow().len());
}
9. Implicaciones de rendimiento: abstracciones de costo cero
La propiedad no tiene coste
El sistema de propiedad no tiene sobrecarga de tiempo de ejecución:
// Este código de Rust:
proceso fn (datos: Vec<i32>) -> i32 {
datos.iter().sum()
}
// Compila en el mismo ensamblado que:
// int proceso(int* datos, tamaño_t len) {
// int suma = 0;
// para (tamaño_t i = 0; i < len; i++) {
// suma += datos[i];
// }
// devolver suma;
// }
Prueba: Verifique el ensamblaje con cargo build --release y herramientas como cargo-asm.
Cuando la clonación tiene un coste
// Caro: copia profunda
let vec1 = vec![1, 2, 3, 4, 5];
let vec2 = vec1.clone(); // Asigna nueva memoria de montón, copia todos los elementos
// Gratis: Referencia
let vec1 = vec![1, 2, 3, 4, 5];
dejar vec2 = &vec1; // Sin asignación, sólo un puntero
Punto de referencia: clonar frente a pedir prestado
utilizar std::time::Instantáneo;
fn proceso_por_valor(datos: Vec<i32>) -> i32 {
datos.iter().sum()
}
fn proceso_por_referencia (datos: &[i32]) -> i32 {
datos.iter().sum()
}
fn principal() {
dejar datos: Vec<i32> = (0..1_000_000).collect();
// Clonar versión
let start = Instantáneo::ahora();
para _ en 0..1000 {
let resultado = proceso_por_valor(datos.clone()); // Clonar cada vez
}
println!("Clon: {:?}", inicio.elapsed());
// versión prestada
let start = Instantáneo::ahora();
para _ en 0..1000 {
let resultado = proceso_por_referencia(&datos); // Sin clon
}
println!("Préstamo: {:?}", start.elapsed());
}
// Resultados típicos:
// Clonar: 850 ms
// Préstamo: 120 ms
Puntero inteligente arriba
// Rc tiene una pequeña sobrecarga
utilizar std::rc::Rc;
utilizar std::time::Instantáneo;
fn with_rc(datos: Rc<Vec<i32>>) {
let _ = datos.len();
}
fn with_ref(datos: &Vec<i32>) {
let _ = datos.len();
}
// Rc son 2 palabras (puntero + recuento de referencias)
// La referencia es 1 palabra (solo puntero)
// Pero Rc permite la propiedad compartida donde las referencias no pueden
Cuando los gastos generales importan:
- Hot loops con millones de iteraciones.
- Sistemas en tiempo real
- Sistemas integrados con recursos limitados
Cuando los gastos generales no importan:
- La mayoría del código de aplicación
- Cuando permite un mejor diseño.
- Cuando la clonación sería más cara
10. Guía de Migración: Desde Otros Idiomas
Procedente de C++
Mentalidad C++:
std::cadena* crearCadena() {
devolver nuevo std::string("hola"); // La persona que llama debe eliminar
}
proceso nulo() {
std::cadena* s = crearCadena();
std::cout << *s;
eliminar s; // limpieza manual
}
Equivalente a óxido:
fn create_string() -> Cadena {
String::from("hola") // Propiedad transferida
}
proceso fn() {
let s = crear_cadena();
println!("{}", s);
} // Eliminado automáticamente
Diferencias clave:
- Sin manual
nuevo/eliminar - No hay punteros sin formato en el código seguro
- Las referencias tienen verificación de por vida.
- Mover semántica por defecto
Viniendo de Go
Vaya mentalidad:
proceso func(datos []int) {
data[0] = 100 // Muta el original
}
función principal() {
números := []int{1, 2, 3}
proceso (números)
fmt.Println(números) // [100, 2, 3]
}
El óxido requiere mutabilidad explícita:
fn proceso(datos: &mut [i32]) { // Explícito &mut
datos[0] = 100;
}
fn principal() {
let mut nums = vec![1, 2, 3]; // se requiere palabra clave mut
proceso(&mut nums); // Explícito y mut
println!("{:?}", números); // [100, 2, 3]
}
Diferencias clave:
- La mutabilidad debe ser explícita.
- No hay carreras de datos ocultos
- Las referencias son explícitas (
&vs valor)
Procedente de Python
Mentalidad de Python:
def modificar_lista (elementos):
items.append(4) # Muta el original
números = [1, 2, 3]
modificar_lista (números)
imprimir(números) # [1, 2, 3, 4]
Equivalente a óxido:
fn modificar_lista (elementos: &mut Vec<i32>) {
elementos.push(4);
}
fn principal() {
let mut nums = vec![1, 2, 3];
modificar_lista(&mut nums);
println!("{:?}", números); // [1, 2, 3, 4]
}
Diferencias clave:
- Todo en Python es una referencia; Rust distingue valores y referencias
- Python GC se encarga de la limpieza; Rust usa la propiedad
- Python permite la mutación libremente; Rust requiere "mut"
Procedente de JavaScript
Mentalidad de JavaScript:
función crearUsuario() {
return {nombre: "Alice", correo electrónico: "[email protected]" };
}
dejar usuario = crearUsuario();
dejar usuario2 = usuario; // copia superficial
usuario2.nombre = "Bob";
console.log(nombre.usuario); // "Bob" - ambos se refieren al mismo objeto
Comportamiento de oxidación:
#[derivar(Clonar)]
estructura usuario {
nombre: cadena,
correo electrónico: cadena,
}
fn create_user() -> Usuario {
Usuario {
nombre: Cadena::de("Alicia"),
correo electrónico: Cadena::de("[email protected]"),
}
}
fn principal() {
dejar usuario = crear_usuario();
dejar usuario2 = usuario; // Movido, no copiado
// println!("{}", nombre.usuario); // ERROR: valor movido
// Si quieres copiar comportamiento:
dejar usuario = crear_usuario();
dejar usuario2 = usuario.clon(); // Clon explícito
println!("{}", nombre.usuario); // OK
}
Diferencias clave:
- Los objetos JS se cuentan por referencia; Rust se mueve por defecto
- JS tiene GC; Rust tiene propiedad
- La mutación JS no tiene restricciones; Rust hace cumplir las reglas de préstamo
Conclusión: adopte el verificador de préstamos
El sistema de propiedad parece restrictivo al principio, pero en realidad es liberador:
✅ Sin pérdidas de memoria: si se compila, la memoria se administra correctamente ✅ Sin carreras de datos: los errores concurrentes se detectan en el momento de la compilación ✅ Sin uso después de la liberación: Imposible acceder a la memoria liberada ✅ Rendimiento predecible: sin pausas de GC, sin asignaciones ocultas ✅ Refactorización intrépida: el compilador detecta cambios importantes
Obtenga servicios profesionales →
La curva de aprendizaje
Semana 1-2: Frustración. El verificador de préstamos lo rechaza todo. Semana 3-4: Comprensión. Empiezas a pensar en la propiedad. Mes 2: Fluidez. Usted diseña API que funcionan con propiedad. Mes 3+: Maestría. Escribes Rust más rápido que tu antiguo idioma.
Consejos prácticos para el aprendizaje
- Lea atentamente los errores del compilador: son excelentes profesores
- Comience con programas pequeños: domine los conceptos básicos antes de construir sistemas grandes
- Utilice
clone()generosamente al principio - optimice más tarde - No luches contra el verificador de préstamos: rediseña si tienes dificultades
- Estudie el código de biblioteca estándar: vea cómo lo hacen los expertos
Próximos pasos
- Práctica: Resuelve problemas de Exercism, LeetCode o Advent of Code en Rust
- Leer: El libro de Rust, Rust con el ejemplo, Rustonomicon (Rust inseguro)
- Construir: los proyectos reales te obligan a encontrar y resolver problemas reales
- Contribuir: Los proyectos Rust de código abierto dan la bienvenida a los recién llegados
Recursos
- El lenguaje de programación Rust (El libro)
- Óxido con el ejemplo
- Rustlings - Pequeños ejercicios
- Foro de usuarios de Rust - Haga preguntas
- Esta semana en Rust - Manténgase actualizado