¿Qué es la propiedad?

Completado

Rust incluye un sistema de propiedad para administrar la memoria. En tiempo de compilación, el sistema de propiedad comprueba un conjunto de reglas para asegurarse de que las características de propiedad permiten que el programa se ejecute sin ralentizarse.

Para comprender la propiedad, echemos primero un vistazo a las reglas de ámbito y a la semántica de transferencia de recursos de Rust.

Reglas de ámbito

En Rust, como en la mayoría de los demás lenguajes de programación, las variables solo son válidas dentro de un ámbito determinado. En Rust, los ámbitos normalmente se indican con llaves {}. Los ámbitos comunes incluyen cuerpos de función y ramas if, else y match.

Nota:

En Rust, las "variables" se suelen denominar "enlaces". Esto se debe a que las "variables" de Rust no son muy variables: no cambian con frecuencia, ya que son inmutables de manera predeterminada. Por el contrario, a menudo pensamos que los nombres están "enlazados" a los datos, de ahí el nombre "enlace". En este módulo, usaremos los términos "variable" y "enlace" indistintamente.

Imagine que tenemos una variable mascot que es una cadena definida dentro de un ámbito:

// `mascot` is not valid and cannot be used here, because it's not yet declared.
{
    let mascot = String::from("ferris");   // `mascot` is valid from this point forward.
    // do stuff with `mascot`.
}
// this scope is now over, so `mascot` is no longer valid and cannot be used.

Si se intenta usar mascot más allá de su ámbito, se produce un error como el de este ejemplo:

{
    let mascot = String::from("ferris");
}
println!("{}", mascot);
    error[E0425]: cannot find value `mascot` in this scope
     --> src/main.rs:5:20
      |
    5 |     println!("{}", mascot);
      |                    ^^^^^^ not found in this scope

Puede intentar ejecutar este ejemplo en el área de juegos de Rust.

La variable es válida desde el punto en el que se declara hasta el final de ese ámbito.

Propiedad y anulación

Rust supone una vuelta de tuerca para el concepto de ámbitos. Cada vez que un objeto sale del ámbito, se "descarta". El descarte de una variable libera todo los recursos asociados a ella. En el caso de las variables de archivos, el archivo termina cerrado. En el caso de las variables que tienen asignada memoria asociada a ellas, se libera la memoria.

En Rust, de los enlaces que tienen elementos "asociados" que se van a liberar cuando se anule el enlace se dice que "poseen" esos elementos.

En el ejemplo anterior, la variable mascot posee los datos de cadena asociados a ella. String posee la memoria asignada del montón que contiene los caracteres de esa cadena. Al final del ámbito, mascot se "anula", se anula el elemento String que posee y, por último, se libera la memoria que String posee.

{
    let mascot = String::from("ferris");
}
// mascot is dropped here. The string data memory will be freed here.

Semántica de transferencia de recursos

A veces, no se quiere que los elementos asociados a una variable se anulen al final del ámbito. sino que se quiere transferir la propiedad de un elemento de un enlace a otro.

El ejemplo más sencillo es cuando se declara un nuevo enlace:

{
    let mascot = String::from("ferris");
    // transfer ownership of mascot to the variable ferris.
    let ferris = mascot;
}
// ferris is dropped here. The string data memory will be freed here.

Una cuestión clave que se debe comprender es que, una vez transferida la propiedad, la variable antigua ya no es válida. En el ejemplo anterior, después de transferir la propiedad de String de mascot a ferris, ya no se puede usar la variable mascot.

En Rust, "transferir la propiedad" se conoce como "mover". Es decir, la propiedad del valor String se ha movido de mascot a ferris.

Si se intenta usar mascot después de que se haya movido String de mascot a ferris, el compilador no compilará el código:

{
    let mascot = String::from("ferris");
    let ferris = mascot;
    println!("{}", mascot) // We'll try to use mascot after we've moved ownership of the string data from mascot to ferris.
}
error[E0382]: borrow of moved value: `mascot`
 --> src/main.rs:4:20
  |
2 |     let mascot = String::from("ferris");
  |         ------ move occurs because `mascot` has type `String`, which does not implement the `Copy` trait
3 |     let ferris = mascot;
  |                  ------ value moved here
4 |     println!("{}", mascot);
  |                    ^^^^^^ value borrowed here after move

Este resultado se conoce como un error de compilación de "uso después de movimiento".

Importante

En Rust, solo un elemento puede poseer un fragmento de datos a la vez.

Propiedad en las funciones

Ahora se examinará un ejemplo de una cadena que se pasa a una función como argumento. Si se pasa algo como un argumento a una función, se mueve ese elemento a la función.

fn process(input: String) {}

fn caller() {
    let s = String::from("Hello, world!");
    process(s); // Ownership of the string in `s` moved into `process`
    process(s); // Error! ownership already moved.
}

El compilador notifica que el valor s se ha transferido.

    error[E0382]: use of moved value: `s`
     --> src/main.rs:6:13
      |
    4 |     let s = String::from("Hello, world!");
      |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
    5 |     process(s); // Transfers ownership of `s` to `process`
      |             - value moved here
    6 |     process(s); // Error! ownership already transferred.
      |             ^ value used here after move

Como puede ver en el fragmento de código anterior, la primera llamada a process transfiere la propiedad de la variable s. El compilador realiza un seguimiento de la propiedad, por lo que la segunda llamada a process produce un error. Una vez que se mueven los recursos, ya no se puede usar el propietario anterior.

Este patrón tiene un efecto considerable en la forma de escribir el código de Rust. Es fundamental para la promesa de seguridad de la memoria que propone Rust.

En otros lenguajes de programación, el valor String de la variable s puede copiarse implícitamente antes de pasarse a nuestra función. Pero en Rust, esta acción no se produce.

En Rust, la transferencia de propiedad (es decir, la transferencia) es el comportamiento predeterminado.

Copia en lugar de movimiento

En el ejemplo anterior, es posible que haya observado la mención del rasgo Copy en los mensajes de error del compilador (bastante informativos). Todavía no se han presentado de rasgos, pero un valor que implementa el rasgo Copy no se mueve, sino que se copia.

Echemos un vistazo a un valor que implementa el rasgo Copy: u32. En el código siguiente se refleja el código interrumpido, pero se compila sin ningún problema.

fn process(input: u32) {}

fn caller() {
    let n = 1u32;
    process(n); // Ownership of the number in `n` copied into `process`
    process(n); // `n` can be used again because it wasn't moved, it was copied.
}

Los tipos simples como los número son tipos de copia. Implementan el rasgo Copy, lo que significa que se copian en lugar de moverse. La misma acción se produce para la mayoría de los tipos simples. La copia de números es muy económica, por lo que tiene sentido que estos valores se copien. La copia de cadenas o vectores, u otros tipos complejos, puede ser costosa, por lo que no implementan el rasgo Copy y, en su lugar, se mueven.

Copia de tipos que no implementan Copy

Una manera de solucionar los errores que se han visto en el ejemplo anterior consiste en copiar explícitamente los tipos antes de moverlos: lo que se conoce como clonación en Rust. Una llamada a .clone duplica la memoria y genera un nuevo valor. El nuevo valor se mueve, lo que significa que todavía se puede usar el valor anterior.

fn process(s: String) {}

fn main() {
    let s = String::from("Hello, world!");
    process(s.clone()); // Passing another value, cloned from `s`.
    process(s); // s was never moved and so it can still be used.
}

Este enfoque puede resultar útil, aunque puede ralentizar el código, ya que cada llamada a clone realiza una copia completa de los datos. Este método a menudo incluye asignaciones de memoria u otras operaciones costosas. Estos costos se pueden evitar si los valores "se toman prestados" mediante referencias. Aprenderá a usar las referencias en la unidad siguiente.