English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Propiedad en Rust

Los programas informáticos deben gestionar los recursos de memoria que utilizan mientras se ejecutan.

La mayoría de los lenguajes de programación tienen la función de gestión de memoria:

C/C++ Este tipo de lenguaje generalmente gestiona la memoria de manera manual, y los desarrolladores necesitan solicitar y liberar recursos de memoria manualmente. Pero para mejorar la eficiencia de desarrollo, muchos desarrolladores no tienen la costumbre de liberar la memoria a tiempo, siempre y cuando no afecte la implementación de la función del programa. Por lo tanto, la gestión manual de la memoria a menudo causa un desperdicio de recursos.

Los programas escritos en Java se ejecutan en el entorno virtual (JVM), que tiene la función de reciclaje automático de recursos de memoria. Sin embargo, este método a menudo reduce la eficiencia de ejecución, por lo que el JVM intenta reciclar los recursos lo menos posible, lo que también hace que el programa ocupe una gran cantidad de recursos de memoria.

El concepto de propiedad es un concepto nuevo para la mayoría de los desarrolladores, es un mecanismo de sintaxis diseñado por el lenguaje Rust para usar la memoria de manera eficiente. El concepto de propiedad nació para que Rust pueda analizar más eficazmente los recursos de memoria en la fase de compilación para lograr la gestión de memoria.

Reglas de propiedad

La propiedad tiene las siguientes tres reglas:

  • Cada valor en Rust tiene una variable, llamada su propietario.

  • Solo puede haber un propietario a la vez.

  • Cuando el propietario no está en el alcance de ejecución del programa, ese valor será eliminado.

Estas tres reglas son la base del concepto de propiedad.

A continuación, se presentarán conceptos relacionados con el concepto de propiedad.

Alcance de la variable

Describimos el concepto de alcance de la variable con el siguiente programa:

{
    // Antes de la declaración, la variable s es inválida
    let s = "w3codebox";
    // Aquí es el alcance de la variable s
}
// El alcance de la variable s ha finalizado, la variable s es inválida

El alcance de la variable es una propiedad de la variable, que representa el dominio de aplicación de la variable, que por defecto es válida desde la declaración de la variable hasta el final del dominio de la variable.

Memoria y asignación

Si definimos una variable y le asignamos un valor, ese valor existe en la memoria. Esta situación es muy común. Pero si la longitud de los datos que queremos almacenar no es determinada (por ejemplo, una cadena de caracteres ingresada por el usuario), no podemos definir la longitud de los datos en el momento de la definición y tampoco podemos asignar un espacio de memoria fijo de longitud en la fase de compilación para su uso en el almacenamiento de datos. (Algunos dicen que asignar un espacio muy grande puede resolver el problema, pero este método es muy poco civilizado). Esto requiere proporcionar un mecanismo en tiempo de ejecución para que el programa solicite por sí mismo la memoria que utiliza. Este capítulo se refiere a todos los "recursos de memoria" como el espacio de memoria ocupado por la pila.

Hay asignación y liberación, el programa no puede ocupar recursos de memoria durante mucho tiempo. Por lo tanto, el factor clave para determinar si los recursos se desperdician es si se liberan a tiempo.

Escribimos el programa de ejemplo de cadena en un lenguaje equivalente a C:

{
    carácter *s = "w3codebox";
    free(s); // Liberar los recursos de s
}

Es obvio que en Rust no se llama a la función free para liberar los recursos de la cadena s (sé que esto es incorrecto en el lenguaje C, porque "w3La cadena "codebox" no está en la pila, aquí se asume que está en). Rust no muestra explícitamente los pasos de liberación porque el compilador de Rust agrega automáticamente la llamada a la función de liberación de recursos cuando finaliza el alcance de la variable.

Este mecanismo parece muy simple: no es más que ayudar a los programadores a agregar una llamada a la función de liberación de recursos en el lugar adecuado. Pero este mecanismo simple puede resolver eficazmente uno de los problemas de programación más molestos para los programadores en la historia.

变量与数据交互的方式

Las formas de interacción entre variables y datos

Las formas de interacción entre variables y datos principalmente son mover (Move) y clonar (Clone):

Mover

let x = 5;
Se pueden interactuar con los datos de manera diferente en Rust:

let y = x; 5 Este programa asigna el valor 5Asignar el valor a la variable x, luego copiar y asignar el valor de x a la variable y. Ahora habrá dos valores en la pila:

  • . En este caso, los datos son de tipos de datos "básicos", que no necesitan almacenarse en el montón, y la "movimiento" de los datos en la pila es una copia directa, lo que no lleva más tiempo ni más espacio de almacenamiento. Los tipos de datos "básicos" incluyen estos:32 Todos los tipos de enteros, por ejemplo, i32 u64 i

  • Y otros.

  • El tipo booleano bool, con valores true o false.32 Todos los tipos de punto flotante, f64Y f

  • .

  • El tipo de carácter char.

Sólo contiene tipos de datos como los anteriores en los tuplos (Tuples).

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

Pero si los datos de interacción ocurren en el montón, es otro caso:

El primer paso produce un objeto String con el valor "hello" en él. "hello" puede considerarse como datos de longitud indeterminada que necesitan almacenarse en el montón.La situación en el segundo paso es ligeramente diferente (Esto no es completamente cierto, solo se utiliza como referencia para comparar

):2 Como se muestra en la figura: hay dos objetos String en la pila, y cada objeto String tiene un puntero que apunta a la cadena "hello" en el montón. Al asignar s

asignar, solo se copian los datos de la pila, y la cadena en el montón sigue siendo la cadena original.1 y s2 antes de lo que dijimos, cuando una variable sale de su rango, Rust llama automáticamente a la función de liberación de recursos y limpia la memoria del montón de la variable. Pero cuando se asigna s2 asignar, si se liberan todos los datos en el montón, "hello" se liberará dos veces, lo que no está permitido por el sistema. Para asegurar la seguridad, en el s1 ya no es válido. No hay error, cuando se asigna s1 asignar el valor a s2 Después de s1 No se puede usar más. A continuación, se muestra el programa incorrecto:

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // ¡Error! s1 Inactivo

Por lo tanto, la situación real es:

s1 Nombre inexistente.

Clonar

Rust intenta reducir al mínimo el costo de ejecución del programa, por lo que, por defecto, los datos de gran longitud se almacenan en el montón y se utilizan movimientos para la interacción de datos. Pero si se necesita copiar los datos simplemente para su uso, se puede utilizar el segundo método de interacción de datos: clonar.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}, s2 = {}, s1, s2);
}

Resultado de ejecución:

s1 = hello, s2 = hello

Aquí realmente se ha copiado una instancia de "hello" del montón, por lo que s1 y s2 Se binden valores separados, y se liberarán como dos recursos cuando se liberan.

Por supuesto, la clonación se utiliza solo cuando es necesario copiar, después de todo, copiar datos lleva más tiempo.

Involucra el mecanismo de propiedad de la función

Para las variables, este es el caso más complejo.

¿Cómo manejar de manera segura la propiedad si se pasa una variable como parámetro a otra función?

El siguiente fragmento de código describe el principio de funcionamiento del mecanismo de propiedad en esta situación:

fn main() {
    let s = String::from("hello");
    // s se declara válida
    toma_propiedad(s);
    // El valor de s se pasa como parámetro a la función
    // Por lo tanto, se puede considerar que s ya se ha movido, desde aquí en adelante ya es inválida
    let x = 5;
    // x se declara válida
    crea_copia(x);
    // El valor de x se pasa como parámetro a la función
    // Pero x es de tipo básico, sigue siendo válido
    // Todavía se puede usar x aquí, pero no se puede usar s
} // La función termina, x inválida, luego es s. Pero s ya se ha movido, por lo que no se necesita liberarla
fn toma_propiedad(some_string: String) { 
    // Un parámetro de String some_string se pasa, válida
    println!("{}", some_string);
} // La función termina, el parámetro some_string se libera aquí
fn crea_copia(some_integer: i32) { 
    // Un i32 Se pasa el parámetro some_integer, válida
    println!("{}", some_integer);
} // La función termina, el parámetro some_integer es de tipo básico, no es necesario liberarlo

Si se pasa una variable como parámetro a una función, entonces es equivalente al efecto de mover.

Mecanismo de propiedad del valor de retorno de la función

fn main() {
    let s1 = propiedad_de_devolucion();
    // propiedad_de_devolucion mueve su valor de retorno a s1
    let s2 = String::from("hello");
    // s2 Se declara válida
    let s3 = toma_y_devuelve(s2);
    // s2 Se mueve como parámetro, s3 Se obtiene la propiedad del valor de retorno
} // s3 La propiedad inválida se libera, s2 Se mueve, s1 La propiedad inválida se libera.
fn propiedad_de_devolucion() -> String {
    let some_string = String::from("hello");
    // some_string se declara válida
    return some_string;
    // some_string se mueve fuera de la función como valor de retorno
}
fn toma_y_devuelve(a_string: String) -> String { 
    // La cadena "a_string" se declara válida
    a_string  // a_string se considera como valor de retorno de la función
}

la propiedad del variable que se considera como valor de retorno de la función se transferirá fuera de la función y se devolverá al lugar de la función de llamada, en lugar de liberarse directamente.

Referencia y préstamo

Referencia (Reference) es C++ concepto familiar para los desarrolladores.

si estás familiarizado con el concepto de puntero, puedes considerarlo como un puntero.

en realidad "referencia" es una forma de acceso indirecto a variables.

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    println!("s1 es {}, s2 es {}", s1, s2);
}

Resultado de ejecución:

s1 es hello, s2 es hello

el operador & puede obtener la "referencia" de una variable.

cuando un valor de una variable se refiere, la variable en sí no se considera inválida. Porque "referencia" no copia el valor de la variable en la pila:

la lógica de la transmisión de parámetros de función es similar:

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("La longitud de '{}' es {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}

Resultado de ejecución:

La longitud de 'hello' es 5.

la referencia no obtendrá la propiedad del valor.

la referencia solo puede prestar el derecho de propiedad de valores.

la referencia en sí también es un tipo y tiene un valor, este valor registra la ubicación de otros valores, pero la referencia no tiene la propiedad del valor al que apunta:

fn main() {
    let s1 = String::from("hello");
    let s2 = &s1;
    let s3 = s1;
    println!("{}", s)2);
}

este programa no es correcto: porque s2 el préstamo de s1 ya ha movido la propiedad a s3, por lo que s2 no se podrá continuar alquilando para usar s1 la propiedad. Si necesitas usar s2 para usar este valor, debes alquilar de nuevo:

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // volver a alquilar desde s3 alquiler de propiedad
    println!("{}", s)2);
}

este programa es correcto.

ya que la referencia no tiene propiedad, incluso si alquila la propiedad, solo tiene el derecho de uso (esto es lo mismo que alquilar una casa).

si intentas utilizar el derecho del préstamo para modificar los datos se bloqueará:

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s)2);
    s2.push_str("oob"); // error, se prohíbe modificar el valor del préstamo
    println!("{}", s)2);
}

en este programa s2 intenta modificar s1 su valor se impide, el propietario del préstamo no puede modificar el valor del propietario.

Claro, también hay una forma de alquiler mutable, como si alquilas una casa y el propietario especifica que puedes modificar la estructura de la casa, el propietario también declara en el contrato que te otorgas este derecho, puedes reformar la casa:

fn main() {
    let mut s1 = String::from("run");
    // s1 es mutable
    let s2 = &mut s1;
    // s2 es una referencia mutable
    s2.push_str("oob");
    println!("{}", s)2);
}

Este programa no tiene problemas. Usamos &mut para modificar el tipo de referencia mutable.

En comparación con las referencias inmutables, las referencias mutables no permiten múltiples referencias, pero las referencias inmutables pueden:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

Este programa no es correcto porque hay múltiples referencias mutables a s.

Este diseño de Rust con referencias mutables se debe principalmente a la consideración de colisiones en el acceso a datos en el estado concurrente, evitando este tipo de situaciones en la etapa de compilación.

Dado que una de las condiciones necesarias para que ocurra una colisión en el acceso a datos es que los datos se hayan escrito por al menos un usuario y al mismo tiempo leídos o escritos por al menos otro usuario, no se permite que un valor sea accesible por múltiples referencias cuando se realiza una referencia mutable.

Referencias colgantes (Dangling References)

Es un concepto que ha cambiado de nombre, y si se colocara en un lenguaje de programación con conceptos de punteros, se referiría a的那种没有实际指向一个真正可访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"referencia colgante"。

"Referencia colgante" no se permite en el lenguaje Rust, y si existe, el compilador la detectará.

A continuación, se muestra un caso典型 de referencia colgante:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

Es evidente que, con el final de la función dangle, los valores de las variables locales no se han utilizado como valor de retorno y se han liberado. Pero su referencia se ha devuelto, y el valor al que apunta esta referencia ya no existe, por lo que no se permite que aparezca.