Ownership y funciones: mover, devolver y el porqué del borrowing
Pasar un valor a una función lo mueve o lo copia; devolverlo transfiere la propiedad de vuelta. El incómodo patrón de devolver para seguir usando que motiva el borrowing del nivel 9.
Las funciones no tienen reglas de ownership propias: obedecen las mismas tres que ya conoces. Un parámetro es otra variable que toma posesión; pasar un argumento es una asignación disfrazada; devolver es mover hacia fuera. Al llevar el ownership a las llamadas descubrimos un problema práctico —cómo seguir usando un valor después de prestárselo a una función— cuya solución elegante, el borrowing, es el tema del nivel 9.
- Ver que pasar por valor mueve (o copia) exactamente igual que una asignación.
- Entender que
returntransfiere la propiedad hacia el llamador. - Reconocer el patrón de “devolver el valor para poder seguir usándolo”.
- Motivar el borrowing como la salida limpia a ese patrón.
Pasar por valor mueve
Cuando pasas un valor no-Copy a una función, lo mueves dentro de ella. El parámetro se convierte en el nuevo dueño, y tu variable original deja de ser válida. Al terminar la función, el parámetro sale de su scope y el valor se libera allí:
fn main() {
let s = String::from("dato");
consume(s); // s se MUEVE a la función
// println!("{s}"); // ERROR E0382: s ya no es válido aquí
}
fn consume(texto: String) {
println!("{texto}");
} // `texto` sale de scope: el buffer se libera aquí
Con un tipo Copy la historia cambia: el argumento se copia y tu original sigue vivo. Por eso puedes pasar un i32 a mil funciones sin perderlo, pero un String desaparece de tu poder en la primera llamada por valor.
fn duplica(n: i32) -> i32 { n * 2 }
fn main() {
let x = 10;
let y = duplica(x); // x se COPIA al pasarse: sigue válido
println!("{x} y {y}"); // 10 y 20, ambos usables
}
flowchart LR dueno[main posee s] -->|pasa por valor| fn[consume toma la posesion] fn -->|fin de la funcion| drop[s se libera dentro de consume]
Prestar es lo habitual, pero a veces quieres consumir de verdad: un constructor que absorbe sus partes, un into_iter que se queda el contenedor, un builder que se transforma paso a paso. Tomar self por valor en la firma comunica “esto consume el valor” y a menudo habilita optimizaciones. La regla no es “nunca muevas a una función”, sino “muévela cuando la semántica sea consumir, préstala cuando sea mirar”.
Devolver transfiere la propiedad
El movimiento también fluye hacia fuera. Una función puede crear un valor y cederte la propiedad devolviéndolo. El dueño pasa a ser la variable que recibe el resultado:
fn produce() -> String {
let s = String::from("nuevo"); // creado dentro de la función
s // se mueve HACIA FUERA, al llamador
}
fn main() {
let mio = produce(); // `mio` es ahora el dueño del String
println!("{mio}");
}
En la práctica, el compilador construye el valor directamente en el hueco del llamador y elide el move, así que devolver por valor no cuesta una copia del buffer. La propiedad viaja; los bytes del heap, no.
No temas devolver String, Vec<T> o cualquier tipo que posea heap desde una función: es idiomático y eficiente. Toda la biblioteca estándar lo hace —String::from, Vec::new, format!— porque el ownership fluye limpio hacia el llamador y la elisión evita la copia. “Devolver por valor” no es un lujo caro en Rust, es la forma por defecto de entregar un valor recién creado.
El patrón incómodo: devolver para seguir usando
Aquí choca el ownership con la ergonomía. Si una función necesita leer un valor por valor, se lo tienes que ceder, y si luego lo quieres de vuelta, la función se ve obligada a devolvértelo. Con datos y resultados, esto degenera en tuplas:
fn longitud(s: String) -> (String, usize) {
let n = s.len();
(s, n) // hay que devolver `s` o el llamador la pierde
}
fn main() {
let s = String::from("hola");
let (s, n) = longitud(s); // recuperamos `s` junto al resultado
println!("{s} mide {n}");
}
Funciona, pero es horrible: cada función que solo quiere mirar un dato tendría que ir devolviéndolo, y las firmas se llenan de valores de ida y vuelta que no aportan nada. Escala fatal. Tiene que haber una forma de dejar que una función acceda a un valor sin arrebatarte la propiedad.
Mira cualquier método de la biblioteca estándar: s.len(), v.contains(&x), s.to_uppercase(). Casi todos toman &self o &mut self, nunca self por valor, salvo cuando de verdad consumen. La razón es exactamente este patrón: si len se apropiara del String, no podrías volver a usarlo, y sería absurdo. El borrowing es lo que permite que una API sea a la vez segura y cómoda.
La salida: prestar en vez de ceder
La hay, y se llama borrowing. En lugar de mover el valor, le pasas una referencia con &: la función obtiene acceso temporal para leer, pero tú sigues siendo el dueño. No hay move, así que tu variable sigue válida después de la llamada.
fn longitud(s: &String) -> usize { // toma prestado, no posee
s.len()
}
fn main() {
let s = String::from("hola");
let n = longitud(&s); // le PRESTAMOS s, no se la damos
println!("{s} mide {n}"); // seguimos siendo dueños: s sigue viva
}
Mover · ceder la propiedad
Pasar por valor. La función se queda el dato; tú lo pierdes salvo que te lo devuelva.
Prestar · dar acceso temporal
Pasar &valor. La función lo usa un rato; tú sigues siendo el dueño y lo conservas.
Y si la función necesita modificar el valor sin apropiárselo, se presta de forma mutable con &mut:
fn agranda(s: &mut String) {
s.push_str(" y más"); // modifica en el sitio, sin poseer
}
fn main() {
let mut s = String::from("texto");
agranda(&mut s); // préstamo mutable temporal
println!("{s}"); // seguimos siendo dueños: "texto y más"
}
El nivel 9 formaliza las dos formas de préstamo —compartido (&, muchos lectores a la vez) y exclusivo (&mut, un único escritor)— y las reglas que impiden que choquen. Aquí basta con ver que existen y que disuelven el patrón incómodo de la sección anterior: nadie tiene que devolver un valor solo para que su dueño lo recupere.
Con esto cierras el mapa del ownership básico: mover cede la propiedad, devolver la entrega, prestar la comparte sin soltarla. Todo lo que viene después —lifetimes, slices, smart pointers— son refinamientos sobre estas tres operaciones: quién posee un dato, quién puede tocarlo y durante cuánto tiempo.
Es tentador ver las referencias de Rust como una complicación más, pero acabas de presenciar su justificación exacta. El ownership, solo con moves, es completo y seguro —pero insoportable de usar—: cualquier función que quiera leer un valor tendría que apropiárselo y devolverlo, y programar así es un castigo. El borrowing no debilita el ownership; lo hace usable. La idea central es separar dos capacidades que en otros lenguajes van siempre juntas: poseer un valor (ser responsable de liberarlo) y acceder a un valor (leerlo o modificarlo). Mover transfiere la posesión; prestar concede solo el acceso, por un tiempo acotado, sin tocar la posesión. Con esa separación, el dueño se queda quieto y seguro mientras las funciones desfilan pidiendo prestado lo que necesitan. Todo Rust idiomático se apoya en esto: se mueve cuando de verdad se cede la propiedad, y se presta —que es la enorme mayoría de las veces— cuando solo hace falta mirar. El nivel 9 formaliza las reglas del préstamo: cuántos lectores, un solo escritor, y por cuánto tiempo. Pero el porqué ya lo tienes: sin borrowing, el ownership sería correcto e inservible.
Tres movimientos gobiernan el ownership a través de funciones: pasar por valor mueve (o copia, si es Copy); devolver transfiere la propiedad al llamador; y prestar con & o &mut concede acceso sin ceder la posesión. Los dos primeros ya conocías; el tercero, que resuelve el patrón de “devolver para seguir usando”, es la puerta al nivel 9.
- Escribe una función que reciba un
Stringpor valor, úsala y comprueba que la variable original queda movida. - Reescríbela para que devuelva el
Stringen una tupla junto a un resultado, y observa lo ruidosa que queda la firma. - Reescríbela una tercera vez tomando
&String: confirma que ahora conservas la propiedad tras la llamada. - Enuncia con tus palabras la distinción entre poseer y acceder a un valor, y por qué separarlas hace usable al ownership.
- Convierte una firma que toma
Stringpor valor en una que toma&mut String, y comprueba que puedes modificar el original sin perderlo.