wandres.dev
OWNERSHIP · move semantics

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Ver que pasar por valor mueve (o copia) exactamente igual que una asignación.
  • Entender que return transfiere 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]
💡
Tomar posesión también es una herramienta

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.

ℹ️
Devolver por valor es el patrón normal de construcción

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.

ℹ️
Por eso los métodos toman &self

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.

El borrowing nace de un dolor concreto, no de un capricho de diseño

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.

📝
Mover, devolver, prestar

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.

⚔️ Del move al préstamo
  1. Escribe una función que reciba un String por valor, úsala y comprueba que la variable original queda movida.
  2. Reescríbela para que devuelva el String en una tupla junto a un resultado, y observa lo ruidosa que queda la firma.
  3. Reescríbela una tercera vez tomando &String: confirma que ahora conservas la propiedad tras la llamada.
  4. Enuncia con tus palabras la distinción entre poseer y acceder a un valor, y por qué separarlas hace usable al ownership.
  5. Convierte una firma que toma String por valor en una que toma &mut String, y comprueba que puedes modificar el original sin perderlo.