Introducción
Bienvenido a Referencias y Prestamos. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.
En esta práctica, aprenderemos cómo usar referencias en Rust para prestar valores en lugar de tomar posesión, lo que nos permite pasar y manipular datos sin necesidad de devolver la posesión a la función llamante.
Referencias y Prestamos
El problema con el código de tupla en la Lista 4-5 es que tenemos que devolver la String a la función llamante para que aún podamos usar la String después de llamar a calculate_length, porque la String fue movida a calculate_length. En cambio, podemos proporcionar una referencia al valor de la String. Una referencia es como un puntero en el sentido de que es una dirección que podemos seguir para acceder a los datos almacenados en esa dirección; esos datos son propiedad de alguna otra variable. A diferencia de un puntero, se garantiza que una referencia apunte a un valor válido de un tipo particular durante la vida de esa referencia.
Aquí está cómo definir y usar una función calculate_length que tiene una referencia a un objeto como parámetro en lugar de tomar posesión del valor:
Nombre de archivo: src/main.rs
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Primero, observa que todo el código de tupla en la declaración de variable y el valor de retorno de la función desapareció. Segundo, observa que pasamos &s1 a calculate_length y, en su definición, tomamos &String en lugar de String. Estos signos de admiración representan referencias, y te permiten referirte a algún valor sin tomar posesión de él. La Figura 4-5 describe este concepto.
Figura 4-5: Un diagrama de &String s apuntando a String s1
Nota: Lo contrario de referenciar usando
&es desreferenciar, lo que se logra con el operador de desreferencia,*. Veremos algunos usos del operador de desreferencia en el Capítulo 8 y discutiremos los detalles de la desreferencia en el Capítulo 15.
Echemos un vistazo más detenido a la llamada de función aquí:
let s1 = String::from("hello");
let len = calculate_length(&s1);
La sintaxis &s1 nos permite crear una referencia que se refiere al valor de s1 pero no lo posee. Debido a que no lo posee, el valor al que apunta no se eliminará cuando la referencia deje de usarse.
Del mismo modo, la firma de la función usa & para indicar que el tipo del parámetro s es una referencia. Vamos a agregar algunas anotaciones explicativas:
fn calculate_length(s: &String) -> usize { // s es una referencia a una String
s.len()
} // Aquí, s sale del ámbito. Pero debido a que no tiene posesión de lo
// a lo que se refiere, la String no se elimina
El ámbito en el que la variable s es válida es el mismo que el ámbito de cualquier parámetro de función, pero el valor al que apunta la referencia no se elimina cuando s deja de usarse, porque s no tiene posesión. Cuando las funciones tienen referencias como parámetros en lugar de los valores reales, no necesitaremos devolver los valores para devolver la posesión, porque nunca tuvimos posesión.
Llamamos a la acción de crear una referencia prestar. Al igual que en la vida real, si una persona posee algo, puedes prestarlo de ellos. Cuando hayas terminado, debes devolverlo. No lo posees.
Entonces, ¿qué pasa si intentamos modificar algo que estamos prestando? Prueba el código en la Lista 4-6. Alerta: ¡no funciona!
Nombre de archivo: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
Lista 4-6: Intentando modificar un valor prestado
Aquí está el error:
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&`
reference
--> src/main.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- help: consider changing this to be a mutable
reference: `&mut String`
8 | some_string.push_str(", world");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so
the data it refers to cannot be borrowed as mutable
Al igual que las variables son inmutables por defecto, también lo son las referencias. No se nos permite modificar algo a lo que tenemos una referencia.
Referencias Mutables
Podemos corregir el código de la Lista 4-6 para permitirnos modificar un valor prestado con solo unos pequeños ajustes que usan, en cambio, una referencia mutable:
Nombre de archivo: src/main.rs
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
Primero cambiamos s a ser mut. Luego creamos una referencia mutable con &mut s donde llamamos a la función change, y actualizamos la firma de la función para aceptar una referencia mutable con some_string: &mut String. Esto hace muy claro que la función change mutará el valor que presta.
Las referencias mutables tienen una gran restricción: si tienes una referencia mutable a un valor, no puedes tener otras referencias a ese valor. Este código que intenta crear dos referencias mutables a s fallará:
Nombre de archivo: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
Aquí está el error:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
Este error dice que este código es inválido porque no podemos prestar s como mutable más de una vez a la vez. El primer préstamo mutable está en r1 y debe durar hasta que se use en el println!, pero entre la creación de esa referencia mutable y su uso, intentamos crear otra referencia mutable en r2 que presta los mismos datos que r1.
La restricción que impide múltiples referencias mutables al mismo dato al mismo tiempo permite la mutación pero de una manera muy controlada. Es algo con lo que los nuevos Rustaceans luchan porque la mayoría de los lenguajes te dejan mutar cuando quieras. El beneficio de tener esta restricción es que Rust puede prevenir las carreras de datos en tiempo de compilación. Una carrera de datos es similar a una condición de carrera y ocurre cuando ocurren estos tres comportamientos:
- Dos o más punteros acceden al mismo dato al mismo tiempo.
- Al menos uno de los punteros se está usando para escribir en los datos.
- No hay ningún mecanismo que se use para sincronizar el acceso a los datos.
Las carreras de datos causan un comportamiento no definido y pueden ser difíciles de diagnosticar y corregir cuando intentas localizarlas en tiempo de ejecución; Rust evita este problema al rechazar la compilación de código con carreras de datos.
Como siempre, podemos usar llaves para crear un nuevo ámbito, lo que permite múltiples referencias mutables, solo no simultáneas:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 sale del ámbito aquí, por lo que podemos crear una nueva referencia sin problemas
let r2 = &mut s;
Rust impone una regla similar para combinar referencias mutables e inmutables. Este código da como resultado un error:
let mut s = String::from("hello");
let r1 = &s; // no problema
let r2 = &s; // no problema
let r3 = &mut s; // GRAN PROBLEMA
println!("{r1}, {r2}, and {r3}");
Aquí está el error:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // GRAN PROBLEMA
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
¡Uy! Tampoco podemos tener una referencia mutable mientras tenemos una inmutable al mismo valor.
Los usuarios de una referencia inmutable no esperan que el valor cambie repentinamente por debajo de ellos. Sin embargo, se permiten múltiples referencias inmutables porque nadie que solo está leyendo los datos tiene la capacidad de afectar la lectura de los datos de nadie más.
Tenga en cuenta que el ámbito de una referencia comienza desde donde se introduce y continúa hasta la última vez que se usa esa referencia. Por ejemplo, este código se compilará porque el último uso de las referencias inmutables, el println!, ocurre antes de que se introduzca la referencia mutable:
let mut s = String::from("hello");
let r1 = &s; // no problema
let r2 = &s; // no problema
println!("{r1} and {r2}");
// variables r1 y r2 no se usarán después de este punto
let r3 = &mut s; // no problema
println!("{r3}");
Los ámbitos de las referencias inmutables r1 y r2 terminan después del println! donde se usan por última vez, lo que es antes de que se cree la referencia mutable r3. Estos ámbitos no se superponen, por lo que este código está permitido: el compilador puede decir que la referencia ya no se está usando en un punto antes del final del ámbito.
Aunque los errores de préstamo pueden ser frustrantes a veces, recuerde que es el compilador de Rust el que señala un posible error temprano (en tiempo de compilación en lugar de en tiempo de ejecución) y le muestra exactamente dónde está el problema. Entonces no tienes que investigar por qué tus datos no son lo que pensabas que eran.
Referencias colgantes
En los lenguajes con punteros, es fácil crear erróneamente un puntero colgante (un puntero que hace referencia a una ubicación en memoria que puede haber sido dada a alguien más) liberando alguna memoria mientras se conserva un puntero a esa memoria. En Rust, en cambio, el compilador garantiza que las referencias nunca serán referencias colgantes: si tienes una referencia a algunos datos, el compilador asegurará de que los datos no saldrán del ámbito antes de que la referencia a los datos lo haga.
Intentemos crear una referencia colgante para ver cómo Rust las previene con un error en tiempo de compilación:
Nombre de archivo: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
Aquí está el error:
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
5 | fn dangle() -> &'static String {
| ~~~~~~~~
Este mensaje de error se refiere a una característica que aún no hemos cubierto: los lifetimes. Discutiremos los lifetimes en detalle en el Capítulo 10. Pero, si ignores las partes sobre lifetimes, el mensaje contiene la clave de por qué este código es un problema:
this function's return type contains a borrowed value, but there
is no value for it to be borrowed from
Echemos un vistazo más detenido a exactamente lo que está sucediendo en cada etapa de nuestro código dangle:
// src/main.rs
fn dangle() -> &String { // dangle devuelve una referencia a una String
let s = String::from("hello"); // s es una nueva String
&s // devolvemos una referencia a la String, s
} // Aquí, s sale del ámbito y se elimina, por lo que su memoria desaparece
// ¡Peligro!
Debido a que s se crea dentro de dangle, cuando se termine el código de dangle, s se desasignará. Pero intentamos devolver una referencia a ella. Eso significa que esta referencia apuntaría a una String inválida. ¡Eso no es bueno! Rust no nos dejará hacer esto.
La solución aquí es devolver la String directamente:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
Esto funciona sin problemas. La posesión se mueve y nada se desasigna.
Las Reglas de las Referencias
Repasemos lo que hemos discutido sobre las referencias:
- En cualquier momento dado, puedes tener ya sea una referencia mutable o cualquier número de referencias inmutables.
- Las referencias deben siempre ser válidas.
A continuación, veremos un tipo diferente de referencia: los slices.
Resumen
¡Felicitaciones! Has completado el laboratorio de Referencias y Prestamos. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.