Ownership
Domina la característica más única de Rust: reglas de ownership, semántica de movimiento, el trait Copy y Clone
Ownership
Ownership es la característica más distintiva de Rust. Permite seguridad de memoria sin un recolector de basura al imponer reglas estrictas en tiempo de compilación.
Las Reglas de Ownership
- Cada valor tiene exactamente un dueño.
- Solo puede haber un dueño a la vez.
- Cuando el dueño sale de ámbito, el valor se libera.
fn main() {
{ // s no es válido aquí
let s = String::from("hola"); // s se vuelve válido
// usar s
} // s sale de ámbito, memoria liberada
}Cuando s sale de ámbito, Rust llama drop() automáticamente — como C++ RAII pero sin la gestión manual de destructores.
Semántica de Movimiento
Cuando asignas o pasas un valor, la ownership se mueve:
fn main() {
let s1 = String::from("hola");
let s2 = s1; // s1 es MOVIDO a s2
// println!("{s1}"); // ERROR: préstamo de valor movido
println!("{s2}"); // OK: s2 posee la cadena
}Después del movimiento, s1 es invalidado. El compilador previene uso-después-de-movimiento.
Los movimientos son baratos — solo copian un puntero, longitud y capacidad. Ningún dato del heap se copia. El antiguo dueño simplemente se marca como inválido en tiempo de compilación.
Qué Sucede en la Memoria
Antes del movimiento:
s1 -> { ptr: "hola", len: 4, cap: 4 }
|
v
[h][o][l][a] (heap)
Después del movimiento:
s1 -> INVALID (impuesto por el compilador)
s2 -> { ptr: "hola", len: 4, cap: 4 }
|
v
[h][o][l][a] (misma memoria del heap)
Movimiento en Funciones
fn toma_ownership(s: String) { // s toma ownership
println!("{s}");
} // s liberado aquí, heap liberado
fn hace_copia(i: i32) { // i es copiado (trait Copy)
println!("{i}");
} // i sale de ámbito, nada especial
fn main() {
let s = String::from("hola");
toma_ownership(s);
// println!("{s}"); // ERROR: movido
let x = 42;
hace_copia(x);
println!("{x}"); // OK: x es Copy
}Retornando Ownership
fn da_ownership() -> String {
let s = String::from("hola");
s // Ownership se mueve al llamante
}
fn toma_y_da(s: String) -> String {
s // Ownership se mueve nuevamente
}
fn main() {
let s1 = da_ownership();
let s2 = toma_y_da(s1);
// s1 ahora es inválido
println!("{s2}");
}El Trait Copy
Los tipos simples almacenados enteramente en la stack implementan Copy. La asignación copia en lugar de mover:
fn main() {
// Tipos Copy: asignación = copia
let x = 5;
let y = x; // x sigue siendo válido — i32 es Copy
println!("{x} {y}"); // Ambos funcionan
let a = true;
let b = a; // bool es Copy
println!("{a} {b}"); // Ambos funcionan
}Tipos Que Son Copy
| Tipo | Ejemplos |
|---|---|
| Enteros | i32, u64, i8, etc. |
| Floats | f32, f64 |
| Booleano | bool |
| Carácter | char |
| Tuplas de Copy | (i32, i32), (bool, char) |
| Referencias | &T, &mut T (siempre Copy) |
Tipos Que NO Son Copy
| Tipo | Razón |
|---|---|
String | Posee memoria en el heap |
Vec<T> | Posee memoria en el heap |
&mut T | Acceso único (movido, no Copy) |
Box<T> | Posee memoria en el heap |
Si un tipo o cualquiera de sus campos implementa Drop, no puede implementar Copy. Esto previene errores de doble liberación.
Clone — Copia Profunda Explícita
Cuando quieres una copia profunda de datos en el heap, llama .clone():
fn main() {
let s1 = String::from("hola");
let s2 = s1.clone(); // Copia profunda: datos del heap son duplicados
println!("s1: {s1}"); // Sigue siendo válido
println!("s2: {s2}"); // Copia separada
// Con tipos Copy, clone no es necesario
let x = 5;
let y = x.clone(); // Funciona, pero redundante para tipos Copy
}Ownership y Ámbito
fn main() {
let s = String::from("fuera");
{
let interno = String::from("dentro");
println!("{interno}"); // OK
}
// interno fue liberado aquí
println!("{s}"); // OK
// s es liberado aquí
}Orden de Liberación
Las variables se liberan en orden inverso de declaración:
fn main() {
let a = String::from("a");
let b = String::from("b");
// b liberado primero, luego a
}Movimientos Parciales
Los structs pueden ser parcialmente movidos:
struct Persona {
nombre: String,
edad: u8,
}
fn main() {
let persona = Persona {
nombre: String::from("Alice"),
edad: 30,
};
let nombre = persona.nombre; // Mover nombre fuera
// println!("{}", persona.nombre); // ERROR: parcialmente movido
println!("{}", persona.edad); // OK: edad es Copy, aún accesible
}Ownership en la Práctica
// Malo: ownership movida, pero queremos usarla después
fn mal_longitud(s: String) -> usize {
s.len()
} // s liberado
// Bueno: retornar ownership
fn ok_longitud(s: String) -> (String, usize) {
let len = s.len();
(s, len)
}
// Mejor: tomar prestado en lugar de tomar ownership (siguiente lección)
fn mejor_longitud(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hola");
let len = mal_longitud(s);
// println!("{s}"); // ERROR
let s = String::from("hola");
let (s, len) = ok_longitud(s);
println!("{s} tiene {len} caracteres"); // OK
let s = String::from("hola");
let len = mejor_longitud(&s);
println!("{s} tiene {len} caracteres"); // OK
}Ownership con Tipos Personalizados
#[derive(Debug)]
struct Archivo {
nombre: String,
datos: Vec<u8>,
}
fn main() {
let archivo = Archivo {
nombre: String::from("datos.txt"),
datos: vec![1, 2, 3],
};
let archivo2 = archivo; // ¡Movimiento! File NO es Copy
// println!("{:?}", archivo); // ERROR
// Usa clone si quieres ambos
let archivo3 = archivo2.clone();
println!("{:?}", archivo2);
println!("{:?}", archivo3);
}Ownership es la base de las garantías de seguridad de Rust. Cuando haga clic, entenderás cómo Rust elimina categorías enteras de bugs (use-after-free, doble liberación, punteros colgantes) en tiempo de compilación.
Preguntas de Práctica
- ¿Cuáles son las tres reglas de ownership?
- ¿Qué sucede con la memoria cuando el dueño sale de ámbito?
- ¿Cuál es la diferencia entre movimiento y copia?
- ¿Qué tipos implementan el trait
Copy? - ¿Qué hace
.clone()? - ¿Por qué un tipo con
Dropno puede implementarCopy? - ¿Qué sucede si intentas usar un valor después de moverlo?
- ¿Qué determina el orden en que se liberan las variables?
- ¿Qué es un movimiento parcial?
- ¿Cómo retornar ownership de una función mientras también retornas datos computados?