Ownership
Domina la característica más única de Rust: reglas de ownership, semántica de movimiento, el trait Copy y Clone
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
}Operación | Datos de Stack | Datos de Heap | Costo |
|---|---|---|---|
Movimiento | Copiados | No copiados (dueño cambia) | Tamaño del puntero |
Clone | Copiados | Copiados profundamente | Asignación en heap + copia |
Copy | Copiados | N/A (sin datos en heap) | Trivial |
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?