¿Qué es la propiedad?

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a ¿Qué es la propiedad?. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, aprenderás sobre la propiedad en Rust, un conjunto de reglas que gobiernan cómo un programa gestiona la memoria y cómo afecta al comportamiento y rendimiento del lenguaje.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100392{{"¿Qué es la propiedad?"}} rust/mutable_variables -.-> lab-100392{{"¿Qué es la propiedad?"}} rust/string_type -.-> lab-100392{{"¿Qué es la propiedad?"}} rust/function_syntax -.-> lab-100392{{"¿Qué es la propiedad?"}} rust/expressions_statements -.-> lab-100392{{"¿Qué es la propiedad?"}} rust/method_syntax -.-> lab-100392{{"¿Qué es la propiedad?"}} end

¿Qué es la propiedad?

La propiedad es un conjunto de reglas que gobiernan cómo un programa de Rust gestiona la memoria. Todos los programas deben manejar la forma en que utilizan la memoria de un computador mientras se ejecutan. Algunos lenguajes tienen recolección de basura que busca regularmente la memoria que ya no se está utilizando mientras el programa se ejecuta; en otros lenguajes, el programador debe asignar y liberar explícitamente la memoria. Rust utiliza un tercer enfoque: la memoria se gestiona a través de un sistema de propiedad con un conjunto de reglas que el compilador verifica. Si se viola alguna de las reglas, el programa no se compilará. Ninguna de las características de la propiedad ralentizará su programa mientras se está ejecutando.

Debido a que la propiedad es un concepto nuevo para muchos programadores,确实需要一些时间来适应。好消息是,你对 Rust 和所有权系统的规则越有经验,就会发现自然地编写安全高效的代码就越容易。坚持下去!

Cuando comprenda la propiedad, tendrá una base sólida para entender las características que hacen a Rust único. En este capítulo, aprenderá sobre la propiedad trabajando a través de algunos ejemplos que se centran en una estructura de datos muy común: las cadenas.

La pila y el montón

Muchos lenguajes de programación no requieren que pienses muy a menudo en la pila y el montón. Pero en un lenguaje de programación de sistemas como Rust, si un valor está en la pila o en el montón afecta a cómo se comporta el lenguaje y por qué debes tomar ciertas decisiones. Partes de la propiedad se describirán en relación con la pila y el montón más adelante en este capítulo, por lo que aquí hay una breve explicación como preparación.

Tanto la pila como el montón son partes de la memoria disponible para su código para utilizar en tiempo de ejecución, pero están estructurados de diferentes maneras. La pila almacena los valores en el orden en que los obtiene y elimina los valores en el orden inverso. Esto se conoce como último en, primer out. Piensa en una pila de platos: cuando agregas más platos, los pones en la cima de la pila, y cuando necesitas un plato, lo tomas de la cima. Agregar o quitar platos del medio o del fondo no funcionaría tan bien. Agregar datos se llama apilar en la pila, y quitar datos se llama desapilar de la pila. Todos los datos almacenados en la pila deben tener un tamaño conocido y fijo. Los datos con un tamaño desconocido en tiempo de compilación o un tamaño que puede cambiar deben almacenarse en el montón en su lugar.

El montón está menos organizado: cuando pones datos en el montón, solicitas una cierta cantidad de espacio. El asignador de memoria encuentra un lugar vacío en el montón que sea lo suficientemente grande, lo marca como en uso y devuelve un apuntador, que es la dirección de esa ubicación. Este proceso se llama asignar en el montón y a veces se abrevia simplemente como asignar (apilar valores en la pila no se considera asignar). Debido a que el apuntador al montón es de un tamaño conocido y fijo, puedes almacenar el apuntador en la pila, pero cuando quieres los datos reales, debes seguir el apuntador. Piensa en sentarte en un restaurante. Cuando entras, dices el número de personas en tu grupo, y el host encuentra una mesa vacía que cabe a todos y te lleva allí. Si alguien de tu grupo llega tarde, pueden preguntar donde te has sentado para encontrarte.

Apilar en la pila es más rápido que asignar en el montón porque el asignador nunca tiene que buscar un lugar para almacenar nuevos datos; esa ubicación siempre está en la cima de la pila. Comparativamente, asignar espacio en el montón requiere más trabajo porque el asignador debe primero encontrar un espacio lo suficientemente grande para almacenar los datos y luego realizar contabilidad para prepararse para la próxima asignación.

Acceder a datos en el montón es más lento que acceder a datos en la pila porque debes seguir un apuntador para llegar allí. Los procesadores modernos son más rápidos si saltan menos en la memoria. Continuando la analogía, considera a un servidor en un restaurante tomando pedidos de muchas mesas. Es más eficiente obtener todos los pedidos de una mesa antes de pasar a la siguiente. Tomar un pedido de la mesa A, luego un pedido de la mesa B, luego uno de A nuevamente y luego uno de B nuevamente sería un proceso mucho más lento. Del mismo modo, un procesador puede hacer su trabajo mejor si trabaja con datos que están cerca de otros datos (como en la pila) en lugar de más lejos (como puede ser en el montón).

Cuando su código llama a una función, los valores pasados a la función (incluyendo, potencialmente, apuntadores a datos en el montón) y las variables locales de la función se apilan en la pila. Cuando la función termina, esos valores se desapilan de la pila.

Mantener un seguimiento de qué partes del código están utilizando qué datos en el montón, minimizar la cantidad de datos duplicados en el montón y limpiar los datos no utilizados en el montón para que no se te acabe el espacio son todos problemas que la propiedad aborda. Una vez que comprenda la propiedad, no tendrá que pensar muy a menudo en la pila y el montón, pero saber que el propósito principal de la propiedad es administrar datos del montón puede ayudar a explicar por qué funciona de la manera en que lo hace.

Reglas de propiedad

Primero, echemos un vistazo a las reglas de propiedad. Ten en cuenta estas reglas mientras trabajamos con los ejemplos que las ilustran:

  • Cada valor en Rust tiene un dueño.
  • Solo puede haber un dueño a la vez.
  • Cuando el dueño sale del ámbito, el valor se eliminará.

Alcance de variables

Ahora que ya hemos pasado por la sintaxis básica de Rust, no incluiremos todo el código fn main() { en los ejemplos, así que si estás siguiendo, asegúrate de poner los siguientes ejemplos manualmente dentro de una función main. Como resultado, nuestros ejemplos serán un poco más concisos, lo que nos permitirá centrar nuestra atención en los detalles reales en lugar del código repetitivo.

Como primer ejemplo de propiedad, vamos a ver el alcance de algunas variables. Un alcance es el rango dentro de un programa para el cual un elemento es válido. Tomemos la siguiente variable:

let s = "hello";

La variable s se refiere a un literal de cadena, donde el valor de la cadena está codificado en el texto de nuestro programa. La variable es válida desde el momento en que se declara hasta el final del alcance actual. La Lista 4-1 muestra un programa con comentarios que indican dónde la variable s sería válida.

{                      // s no es válida aquí, ya que aún no está declarada
    let s = "hello";   // s es válida a partir de este momento en adelante

    // haz cosas con s
}                      // este alcance ha terminado, y s ya no es válida

Lista 4-1: Una variable y el alcance en el que es válida

En otras palabras, hay dos momentos importantes aquí:

  • Cuando s entra en alcance, es válida.
  • Sigue siendo válida hasta que sale del alcance.

En este momento, la relación entre los alcances y cuándo las variables son válidas es similar a la de otros lenguajes de programación. Ahora construiremos sobre esta comprensión al introducir el tipo String.

El tipo String

Para ilustrar las reglas de propiedad, necesitamos un tipo de datos más complejo que los que cubrimos en "Tipos de datos". Los tipos cubiertos anteriormente son de tamaño conocido, pueden almacenarse en la pila y desapilarse de la pila cuando su ámbito finaliza, y pueden copiarse rápidamente y sin complejidad para crear una nueva instancia independiente si otra parte del código necesita usar el mismo valor en un ámbito diferente. Pero queremos ver datos que se almacenan en el montón y explorar cómo Rust sabe cuándo limpiar esos datos, y el tipo String es un gran ejemplo.

Nos concentraremos en las partes de String que se relacionan con la propiedad. Estos aspectos también se aplican a otros tipos de datos complejos, ya sea que estén proporcionados por la biblioteca estándar o creados por ti. Discutiremos String con más detalle en el Capítulo 8.

Ya hemos visto literales de cadena, donde un valor de cadena está codificado en nuestro programa. Los literales de cadena son convenientes, pero no son adecuados para todas las situaciones en las que podamos querer usar texto. Una razón es que son inmutables. Otra es que no todos los valores de cadena pueden conocerse cuando escribimos nuestro código: por ejemplo, ¿qué pasa si queremos tomar la entrada del usuario y almacenarla? Para estas situaciones, Rust tiene un segundo tipo de cadena, String. Este tipo gestiona datos asignados en el montón y, por lo tanto, es capaz de almacenar una cantidad de texto que es desconocida para nosotros en tiempo de compilación. Puedes crear un String a partir de un literal de cadena usando la función from, así:

let s = String::from("hello");

El operador de dos puntos :: nos permite colocar este particular función from dentro del espacio de nombres del tipo String en lugar de usar algún tipo de nombre como string_from. Discutiremos esta sintaxis más en "Sintaxis de métodos", y cuando hablamos sobre el espacio de nombres con módulos en "Rutas para referirse a un elemento en el árbol de módulos".

Este tipo de cadena puede ser mutada:

let mut s = String::from("hello");

s.push_str(", world!"); // push_str() agrega un literal a una String

println!("{s}"); // Esto imprimirá `hello, world!`

Entonces, ¿cuál es la diferencia aquí? ¿Por qué String puede ser mutado pero los literales no? La diferencia está en cómo estos dos tipos manejan la memoria.

Memoria y asignación

En el caso de un literal de cadena, conocemos el contenido en tiempo de compilación, por lo que el texto se codifica directamente en el ejecutable final. Es por esto que los literales de cadena son rápidos y eficientes. Pero estas propiedades solo se derivan de la inmutabilidad del literal de cadena. Lamentablemente, no podemos poner un bloque de memoria en el binario para cada fragmento de texto cuyo tamaño es desconocido en tiempo de compilación y que puede cambiar mientras se ejecuta el programa.

Con el tipo String, para admitir un fragmento de texto mutable y creciente, necesitamos asignar una cantidad de memoria en el montón, desconocida en tiempo de compilación, para almacenar el contenido. Esto significa:

  • La memoria debe solicitarse al asignador de memoria en tiempo de ejecución.
  • Necesitamos una forma de devolver esta memoria al asignador cuando hayamos terminado con nuestro String.

La primera parte la hacemos nosotros: cuando llamamos a String::from, su implementación solicita la memoria que necesita. Esto es bastante común en los lenguajes de programación.

Sin embargo, la segunda parte es diferente. En los lenguajes con un recolector de basura (GC), el GC lleva un seguimiento y limpia la memoria que ya no se está utilizando, y no tenemos que preocuparnos por ello. En la mayoría de los lenguajes sin GC, es nuestra responsabilidad identificar cuándo la memoria ya no se está utilizando y llamar a código para liberarla explícitamente, al igual que lo hicimos para solicitarla. Hacer esto correctamente históricamente ha sido un problema de programación difícil. Si olvidamos, desperdiciamos memoria. Si lo hacemos demasiado temprano, tendremos una variable inválida. Si lo hacemos dos veces, eso también es un error. Necesitamos emparejar exactamente una asignación con exactamente una liberación.

Rust sigue un camino diferente: la memoria se devuelve automáticamente una vez que la variable que la posee sale del ámbito. Aquí hay una versión de nuestro ejemplo de ámbito de la Lista 4-1 que utiliza un String en lugar de un literal de cadena:

{
    let s = String::from("hello"); // s es válida a partir de este momento en adelante

    // haz cosas con s
}                                  // este ámbito ha terminado, y s ya no es
                                   // válida

Hay un punto natural en el que podemos devolver la memoria que necesita nuestro String al asignador: cuando s sale del ámbito. Cuando una variable sale del ámbito, Rust llama a una función especial para nosotros. Esta función se llama drop, y es donde el autor de String puede poner el código para devolver la memoria. Rust llama a drop automáticamente en el corchete cerrado.

Nota: En C++, este patrón de desasignar recursos al final de la vida útil de un elemento a veces se llama Resource Acquisition Is Initialization (RAII). La función drop en Rust te resultará familiar si has utilizado patrones RAII.

Este patrón tiene un impacto profundo en la forma en que se escribe el código de Rust. Puede parecer simple ahora, pero el comportamiento del código puede ser inesperado en situaciones más complicadas cuando queremos que múltiples variables usen los datos que hemos asignado en el montón. Ahora exploremos algunas de esas situaciones.

Variables y datos interactuando con Move

En Rust, múltiples variables pueden interactuar con los mismos datos de diferentes maneras. Echemos un vistazo a un ejemplo que utiliza un entero en la Lista 4-2.

let x = 5;
let y = x;

Lista 4-2: Asignando el valor entero de la variable x a y

Probablemente podemos adivinar lo que está sucediendo: "asocia el valor 5 con x; luego hace una copia del valor en x y lo asocia con y". Ahora tenemos dos variables, x e y, y ambas son iguales a 5. Esto es lo que realmente está sucediendo, porque los enteros son valores simples con un tamaño conocido y fijo, y estos dos valores 5 se empujan a la pila.

Ahora echemos un vistazo a la versión de String:

let s1 = String::from("hello");
let s2 = s1;

Esto parece muy similar, por lo que podríamos suponer que la forma en que funciona sería la misma: es decir, la segunda línea haría una copia del valor en s1 y lo asociaría con s2. Pero esto no es exactamente lo que pasa.

Echa un vistazo a la Figura 4-1 para ver lo que está sucediendo con String por debajo de los paneles. Un String está compuesto por tres partes, mostradas en la izquierda: un puntero a la memoria que contiene el contenido de la cadena, una longitud y una capacidad. Este grupo de datos se almacena en la pila. En la derecha está la memoria en el montón que contiene el contenido.

Figura 4-1: Representación en memoria de un String que contiene el valor "hello" asociado a s1

La longitud es la cantidad de memoria, en bytes, que el contenido del String está utilizando actualmente. La capacidad es la cantidad total de memoria, en bytes, que el String ha recibido del asignador. La diferencia entre la longitud y la capacidad es importante, pero no en este contexto, por lo que por ahora está bien ignorar la capacidad.

Cuando asignamos s1 a s2, los datos del String se copian, lo que significa que copiamos el puntero, la longitud y la capacidad que están en la pila. No copiamos los datos en el montón a los que apunta el puntero. En otras palabras, la representación de los datos en memoria se ve como en la Figura 4-2.

Figura 4-2: Representación en memoria de la variable s2 que tiene una copia del puntero, la longitud y la capacidad de s1

La representación no se ve como en la Figura 4-3, que es cómo se vería la memoria si Rust en lugar de eso copiara también los datos del montón. Si Rust hiciera esto, la operación s2 = s1 podría ser muy costosa en términos de rendimiento en tiempo de ejecución si los datos en el montón fueran grandes.

Figura 4-3: Otra posibilidad de lo que podría hacer s2 = s1 si Rust copiara también los datos del montón

Antes, dijimos que cuando una variable sale del ámbito, Rust llama automáticamente a la función drop y libera la memoria del montón para esa variable. Pero la Figura 4-2 muestra ambos punteros de datos apuntando a la misma ubicación. Este es un problema: cuando s2 y s1 salen del ámbito, ambos intentarán liberar la misma memoria. Esto se conoce como un error de doble liberación y es uno de los errores de seguridad de memoria que mencionamos anteriormente. Liberar la memoria dos veces puede causar una corrupción de memoria, lo que puede conllevar posibles vulnerabilidades de seguridad.

Para garantizar la seguridad de la memoria, después de la línea let s2 = s1;, Rust considera que s1 ya no es válido. Por lo tanto, Rust no necesita liberar nada cuando s1 sale del ámbito. Echa un vistazo a lo que sucede cuando intentas usar s1 después de crear s2; no funcionará:

let s1 = String::from("hello");
let s2 = s1;

println!("{s1}, world!");

Obtendrás un error como este porque Rust te impide usar la referencia invalidada:

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which
 does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{s1}, world!");
  |                ^^ value borrowed here after move

Si has escuchado los términos copia superficial y copia profunda mientras trabajabas con otros lenguajes, el concepto de copiar el puntero, la longitud y la capacidad sin copiar los datos probablemente suene como hacer una copia superficial. Pero debido a que Rust también invalida la primera variable, en lugar de ser llamada una copia superficial, se conoce como un move. En este ejemplo, diríamos que s1 fue movido a s2. Entonces, lo que realmente sucede se muestra en la Figura 4-4.

Figura 4-4: Representación en memoria después de que s1 ha sido invalidado

¡Eso resuelve nuestro problema! Con solo s2 válido, cuando sale del ámbito solo él liberará la memoria, y ya terminamos.

Además, hay una decisión de diseño que se implica con esto: Rust nunca creará automáticamente "copias profundas" de tus datos. Por lo tanto, se puede asumir que cualquier copia automática será barata en términos de rendimiento en tiempo de ejecución.

Variables y datos interactuando con Clone

Si queremos copiar en profundidad los datos del montón del String, no solo los datos de la pila, podemos usar un método común llamado clone. Discutiremos la sintaxis de los métodos en el Capítulo 5, pero debido a que los métodos son una característica común en muchos lenguajes de programación, es probable que los hayas visto antes.

Aquí hay un ejemplo de cómo funciona el método clone:

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {s1}, s2 = {s2}");

Esto funciona perfectamente y produce explícitamente el comportamiento mostrado en la Figura 4-3, donde los datos del montón se copian.

Cuando ves una llamada a clone, sabes que se está ejecutando algún código arbitrario y que ese código puede ser costoso. Es un indicador visual de que algo diferente está sucediendo.

Datos solo en la pila: Copy

Hay otro detalle que no hemos mencionado todavía. Este código que utiliza enteros (parte del cual se mostró en la Lista 4-2) funciona y es válido:

let x = 5;
let y = x;

println!("x = {x}, y = {y}");

Pero este código parece contradecir lo que acabamos de aprender: no tenemos una llamada a clone, pero x sigue siendo válido y no se movió a y.

La razón es que tipos como los enteros, que tienen un tamaño conocido en tiempo de compilación, se almacenan enteramente en la pila, por lo que las copias de los valores reales se realizan rápidamente. Eso significa que no hay razón para evitar que x sea válido después de crear la variable y. En otras palabras, no hay diferencia entre una copia profunda y una copia superficial aquí, por lo que llamar a clone no haría nada diferente a la copia superficial habitual, y podemos omitirlo.

Rust tiene una anotación especial llamada el trato Copy que podemos colocar en tipos que se almacenan en la pila, como los enteros (hablaremos más sobre los tratados en el Capítulo 10). Si un tipo implementa el trato Copy, las variables que lo usan no se mueven, sino que se copian trivialmente, lo que las hace todavía válidas después de la asignación a otra variable.

Rust no nos permitirá anotar un tipo con Copy si el tipo, o alguna de sus partes, ha implementado el trato Drop. Si el tipo necesita que algo especial suceda cuando el valor sale del ámbito y agregamos la anotación Copy a ese tipo, obtendremos un error en tiempo de compilación. Para aprender cómo agregar la anotación Copy a su tipo para implementar el trato, consulte "Tratados derivables".

Entonces, ¿qué tipos implementan el trato Copy? Puedes consultar la documentación del tipo dado para estar seguro, pero como regla general, cualquier grupo de valores escalares simples puede implementar Copy, y nada que requiera asignación o que sea alguna forma de recurso puede implementar Copy. Aquí hay algunos de los tipos que implementan Copy:

  • Todos los tipos enteros, como u32.
  • El tipo booleano, bool, con los valores true y false.
  • Todos los tipos de punto flotante, como f64.
  • El tipo de carácter, char.
  • Las tuplas, si solo contienen tipos que también implementan Copy. Por ejemplo, (i32, i32) implementa Copy, pero (i32, String) no.

Propiedad y funciones

La mecánica de pasar un valor a una función es similar a la de asignar un valor a una variable. Pasar una variable a una función hará que se mueva o se copie, al igual que en el caso de la asignación. La Lista 4-3 tiene un ejemplo con algunas anotaciones que muestran dónde entran y salen del ámbito las variables.

// src/main.rs
fn main() {
    let s = String::from("hello");  // s entra en el ámbito

    takes_ownership(s);             // el valor de s se mueve a la función...
                                    //... y por lo tanto ya no es válido aquí

    let x = 5;                      // x entra en el ámbito

    makes_copy(x);                  // x se movería a la función,
                                    // pero i32 implementa Copy, por lo que está bien
                                    // seguir usando x después

} // Aquí, x sale del ámbito, luego s. Sin embargo, debido a que el valor de s
  // fue movido, no pasa nada especial

fn takes_ownership(some_string: String) { // some_string entra en el ámbito
    println!("{some_string}");
} // Aquí, some_string sale del ámbito y se llama a `drop`. La memoria
  // subyacente se libera

fn makes_copy(some_integer: i32) { // some_integer entra en el ámbito
    println!("{some_integer}");
} // Aquí, some_integer sale del ámbito. No pasa nada especial

Lista 4-3: Funciones con propiedad y ámbito anotados

Si intentáramos usar s después de llamar a takes_ownership, Rust lanzaría un error en tiempo de compilación. Estas comprobaciones estáticas nos protegen de los errores. Intenta agregar código a main que use s y x para ver dónde se pueden usar y dónde las reglas de propiedad te impiden hacerlo.

Valores de retorno y ámbito

Devolver valores también puede transferir la propiedad. La Lista 4-4 muestra un ejemplo de una función que devuelve un valor, con anotaciones similares a las de la Lista 4-3.

// src/main.rs
fn main() {
    let s1 = gives_ownership();         // gives_ownership mueve su valor
                                        // de retorno a s1

    let s2 = String::from("hello");     // s2 entra en el ámbito

    let s3 = takes_and_gives_back(s2);  // s2 se mueve a
                                        // takes_and_gives_back, que también
                                        // mueve su valor de retorno a s3
} // Aquí, s3 sale del ámbito y se elimina. s2 fue movida, por lo que nada
  // sucede. s1 sale del ámbito y se elimina

fn gives_ownership() -> String {             // gives_ownership moverá su
                                             // valor de retorno a la función
                                             // que la llama

    let some_string = String::from("yours"); // some_string entra en el ámbito

    some_string                              // some_string se devuelve y
                                             // se mueve hacia la función
                                             // llamante
}

// Esta función toma una String y devuelve una String
fn takes_and_gives_back(a_string: String) -> String { // a_string entra en
                                                      // el ámbito

    a_string  // a_string se devuelve y se mueve hacia la función
              // llamante
}

Lista 4-4: Transferencia de la propiedad de los valores de retorno

La propiedad de una variable sigue el mismo patrón cada vez: asignar un valor a otra variable lo mueve. Cuando una variable que incluye datos en el montón sale del ámbito, el valor se limpiará con drop a menos que la propiedad de los datos haya sido transferida a otra variable.

Si bien esto funciona, tomar la propiedad y luego devolver la propiedad con cada función es un poco tedioso. ¿Qué pasa si queremos permitir que una función use un valor pero no tome la propiedad? Es bastante molesto que todo lo que pasamos también tenga que ser pasado de vuelta si queremos usarlo de nuevo, además de cualquier dato resultante del cuerpo de la función que podamos querer devolver también.

Rust nos permite devolver múltiples valores usando una tupla, como se muestra en la Lista 4-5.

Nombre de archivo: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{s2}' is {len}.");
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() devuelve la longitud de una String

    (s, length)
}

Lista 4-5: Devolución de la propiedad de los parámetros

Pero esto es demasiado formal y requiere mucho trabajo para un concepto que debería ser común. Por suerte para nosotros, Rust tiene una característica para usar un valor sin transferir la propiedad, llamada referencias.

Resumen

¡Felicitaciones! Has completado el laboratorio ¿Qué es la propiedad? Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.