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

Rust 所有权

Les programmes informatiques doivent gérer les ressources mémoire qu'ils utilisent pendant l'exécution.

La plupart des langages de programmation possèdent la fonction de gestion de la mémoire :

C/C++ Ces langages gèrent généralement la mémoire de manière manuelle, et les développeurs doivent demander et libérer manuellement les ressources mémoire. Cependant, pour améliorer l'efficacité du développement, tant que cela n'affecte pas l'implémentation des fonctionnalités du programme, de nombreux développeurs n'ont pas l'habitude de libérer la mémoire en temps opportun. Par conséquent, la gestion manuelle de la mémoire peut souvent entraîner un gaspillage de ressources.

Les programmes écrits en Java s'exécutent dans le moteur de virtualisation (JVM), et le JVM dispose de la fonctionnalité de recyclage automatique des ressources mémoire. Cependant, ce mode de fonctionnement réduit souvent l'efficacité de l'exécution, donc le JVM essaye de recycler le moins de ressources possible, ce qui peut également faire que le programme occupe une plus grande quantité de ressources mémoire.

La notion de propriété est un concept nouveau pour la plupart des développeurs, c'est un mécanisme grammatical conçu par le langage Rust pour utiliser efficacement la mémoire. La notion de propriété est née pour que Rust puisse analyser plus efficacement les ressources mémoire au stade de la compilation pour réaliser la gestion de la mémoire.

Règles de propriété

La propriété suit les trois règles suivantes :

  • Dans Rust, chaque valeur a une variable, appelée son propriétaire.

  • Il ne peut y avoir qu'un seul propriétaire à la fois.

  • Quand le propriétaire n'est pas dans la portée de l'exécution du programme, cette valeur sera supprimée.

Ces trois règles sont la base de la notion de propriété.

Nous allons présenter les concepts liés à la notion de propriété à suivre.

Portée de la variable

Nous utilisons le programme suivant pour décrire le concept de portée de la variable :

{
    // Avant la déclaration, la variable s est invalide
    let s = "w3codebox";
    // Voici la portée de la variable s
}
// La portée de la variable s a déjà pris fin, la variable s est invalide

La portée de la variable est une propriété de la variable, qui représente le domaine d'application de la variable, par défaut, elle est effective à partir de la déclaration de la variable jusqu'à la fin de la portée de la variable.

Mémoire et allocation

Si nous définissons une variable et lui assignons une valeur, cette valeur de variable existe en mémoire. Ce type de situation est très courant. Mais si la longueur des données à stocker n'est pas déterminée (par exemple, une chaîne de caractères saisie par l'utilisateur), nous ne pouvons pas définir explicitement la longueur des données au moment de la définition, ni allouer un espace mémoire de longueur fixe pour le stockage des données au stade de la compilation. (Quelqu'un dit que l'allocation d'un espace aussi grand que possible peut résoudre le problème, mais cette méthode est très peu élégante). Cela nécessite un mécanisme qui permet au programme de demander lui-même l'utilisation de la mémoire pendant l'exécution du programme - la pile. Tous les "ressources mémoire" mentionnés dans ce chapitre se réfèrent à l'espace mémoire occupé par la pile.

Il y a allocation et libération, le programme ne peut pas occuper une ressource mémoire pendant toujours. Par conséquent, le facteur clé déterminant si une ressource est gaspillée est la libération opportune de la ressource.

Nous écrivons l'exemple de programme de chaîne en langage C équivalent :

{
    char *s = "w3codebox";
    free(s); // Libération des ressources de s
}

Il est évident que dans Rust, il n'y a pas d'appel de la fonction free pour libérer les ressources de la chaîne s (je sais que c'est une mauvaise écriture en langage C, car "w3La chaîne "codebox" n'est pas dans la pile, ici nous supposons qu'elle est). Le compilateur Rust n'indique pas explicitement les étapes de libération car, à la fin de la portée de la variable, le compilateur Rust ajoute automatiquement les appels de la fonction de libération des ressources.

Ce mécanisme semble très simple : il ne s'agit que d'aider les programmeurs à ajouter une appellation de fonction de libération des ressources à l'endroit approprié. Mais ce mécanisme simple peut résoudre efficacement un problème de programmation historique qui a rendu les programmeurs très énervés.

Les modes d'interaction entre les variables et les données

Les modes d'interaction entre les variables et les données principaux sont le déplacement (Move) et le clonage (Clone) :

Déplacement

Plusieurs variables peuvent interagir avec les mêmes données de différentes manières en Rust :

let x = 5;
let y = x;

Cette programme va valeur 5 Ce programme copie la valeur 5Lié à la variable x, puis copie et assigne la valeur de x à la variable y. Maintenant, il y aura deux valeurs dans la pile

  • . Dans ce cas, les données sont des données de "types de données de base", qui ne nécessitent pas d'être stockées dans le tas, et le "déplacement" des données dans la pile est directement copié, ce qui ne coûte pas plus de temps ni d'espace de stockage. Les "types de données de base" incluent ces :32 Tous les types de nombres entiers, par exemple i32 、 u64 、 i

  • Et autres.

  • Le type booléen bool, dont la valeur est true ou false .32 Tous les types de nombres flottants, f64Et f

  • .

  • Le type de caractères char.

Ne contient que les types de données mentionnés ci-dessus (Tuples).

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

Mais si les données échangées se trouvent dans le tas, c'est une autre affaire :

La première étape crée un objet String avec la valeur "hello". Dans ce cas, "hello" peut être considéré comme une données de longueur indéterminée, nécessitant un stockage dans le tas.La situation de la deuxième étape est légèrement différente (Ce n'est pas tout à fait vrai, il est utilisé uniquement pour la comparaison de référence

):2 Comme illustré : deux objets String dans la pile, chaque objet String a un pointeur vers la chaîne de caractères "hello" dans le tas. Lors de l'assignment de s

L'assignment, seule la copie des données de la pile est effectuée, et la chaîne de caractères dans le tas reste la même.1 et s2 Nous avons dit auparavant que lorsque une variable dépasse sa portée, Rust appelle automatiquement la fonction de libération des ressources et nettoie la mémoire du tas de la variable. Mais lors de l'assignment de s2 si toutes les données sont libérées, "hello" dans la zone de tas est libéré deux fois, ce qui n'est pas autorisé par le système. Pour assurer la sécurité, avant de donner s1 devient inefficace. Pas de doute, en assignant s1 de la valeur assignée à s2 Ensuite s1 Ne pourra plus être utilisé. Le programme suivant est incorrect :

let s1 = String::from("hello");
let s2 = s1; 
println!("{}, world!", s1); // Erreur ! s1 Défectueux

Donc, dans la pratique :

s1 Nom sans substance.

Clonage

Rust cherche à réduire au maximum les coûts d'exécution du programme, donc par défaut, les données de grande longueur sont stockées dans le tas et les échanges de données se font par déplacement. Mais si l'on doit copier simplement des données pour un autre usage, on peut utiliser le deuxième mode d'échange des données - la clonage.

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

Résultat de l'exécution :

s1 = hello, s2 = hello

Ici, on a effectivement fait une copie de "hello" dans le tas, donc s1 et s2 Chacun est lié à une valeur, et est libéré en tant que deux ressources lors de la libération.

Bien sûr, la clonage n'est utilisé que lorsque cela est nécessaire, car la copie des données prend plus de temps.

Concernant le mécanisme de propriété des fonctions

C'est l'un des cas les plus complexes pour une variable.

Si vous passez une variable en tant que paramètre à une autre fonction, comment traiter en toute sécurité la propriété ?

Le programme suivant décrit le principe de fonctionnement du mécanisme de propriété dans cette situation :

fn main() {
    let s = String::from("hello");
    // s est déclaré comme valable
    takes_ownership(s);
    // La valeur de s est passée en tant que paramètre à la fonction
    // Donc, on peut supposer que s a été déplacé, et qu'il est devenu invalide ici
    let x = 5;
    // x est déclaré comme valable
    makes_copy(x);
    // La valeur de x est passée en tant que paramètre à la fonction
    // Mais x est de type de base, toujours valable
    // Ici, on peut toujours utiliser x mais pas s
} // La fonction se termine, x est invalide, puis s. Mais s a été déplacé, donc pas besoin d'être libéré
fn takes_ownership(some_string: String) { 
    // Un paramètre String some_string est passé, valable
    println!("{}", some_string);
} // La fonction se termine, le paramètre some_string est ici libéré
fn makes_copy(some_integer: i32) { 
    // Un i32 Le paramètre some_integer est passé, valable
    println!("{}", some_integer);
} // La fonction se termine, le paramètre some_integer est de type de base, pas besoin de libérer

Si vous passez une variable en tant que paramètre à une fonction, les effets sont les mêmes que le déplacement.

Mécanisme de propriété du retour des fonctions

fn main() {
    let s1 = gives_ownership();
    // gives_ownership déplace son retour à s1
    let s2 = String::from("hello");
    // s2 Déclaré comme valable
    let s3 = takes_and_gives_back(s2);
    // s2 Déplacé en tant que paramètre, s3 Obtention de la propriété de retour
} // s3 L'invalidité est libérée, s2 Déplacé, s1 L'invalidité est libérée.
fn gives_ownership() -> String {
    let some_string = String::from("hello");
    // some_string est déclaré comme valable
    return some_string;
    // some_string est déplacée hors de la fonction en tant que valeur de retour
}
fn takes_and_gives_back(a_string: String) -> String { 
    // a_string est déclaré comme valable
    a_string  // a_string est utilisé comme retour de valeur hors de la fonction
}

Les variables whose ownership is passed as a function return value will be moved out of the function and returned to the calling function, rather than being directly invalidated.

Référence et prêt

Référence (Reference) est C++ concept plus familier aux développeurs.

si vous êtes familier du concept de pointeur, vous pouvez le considérer comme un pointeur.

en réalité "référence" est un mode d'accès indirect aux variables.

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

Résultat de l'exécution :

s1 est hello, s2 est hello

l'opérateur & peut prendre la "référence" de la variable.

quand la valeur d'une variable est référencée, la variable elle-même n'est pas reconnue comme invalide. Parce que "référence" ne copie pas la valeur de la variable dans la pile :

Les raisons de la transmission des paramètres de la fonction sont similaires :

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

Résultat de l'exécution :

La longueur de 'hello' est 5.

la référence ne reçoit pas la propriété des valeurs.

la référence ne peut prêter que la propriété des valeurs.

la référence elle-même est également un type et a une valeur, cette valeur enregistre l'emplacement d'autres valeurs, mais la référence n'a pas la propriété de la valeur pointée :

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

Ce programme n'est pas correct : parce que s2 le prêt de s1 a déplacé la propriété vers s3par conséquent s2 ne pourra plus être prêté pour usage1 la propriété. Si vous devez utiliser s2 Pour utiliser cette valeur, vous devez réemprunter :

fn main() {
    let s1 = String::from("hello");
    let mut s2 = &s1;
    let s3 = s2;
    s2 = &s3; // reprendre à partir de s3 prêt de propriété
    println!("{}", s2);
}

Ce programme est correct.

puisque la référence n'a pas de propriété, même si elle prête la propriété, elle ne possède que l'utilisation (c'est la même chose que de louer une maison).

Si vous essayez d'utiliser les droits du prêt pour modifier les données, cela sera bloqué :

fn main() {
    let s1 = String::from("run");
    let s2 = &s1; 
    println!("{}", s2);
    s2.push_str("oob"); // erreur, modification interdite de la valeur du prêt
    println!("{}", s2);
}

dans ce programme s2 tentative de modification de s1 sa valeur est bloquée, le propriétaire du prêt ne peut pas modifier la valeur du propriétaire.

Bien sûr, il existe également un mode de prêt mutable, comme si vous louiez une maison, si le propriétaire autorise le propriétaire à modifier la structure de la maison, le propriétaire déclare également dans le contrat que vous avez ce droit, vous pouvez réaménager la maison :

fn main() {
    let mut s1 = String::from("run");
    // s1 est mutable
    let s2 = &mut s1;
    // s2 est une référence mutable
    s2.push_str("oob");
    println!("{}", s2);
}

这段程序就没有问题了。我们用 &mut 修饰可变的引用类型。

可变引用与不可变引用相比除了权限不同以外,可变引用不允许多重引用,但不可变引用可以:

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

这段程序不正确,因为多重可变引用了 s。

Rust 对可变引用的这种设计主要出于对并发状态下发生数据访问碰撞的考虑,在编译阶段就避免了这种事情的发生。

由于发生数据访问碰撞的必要条件之一是数据被至少一个使用者写且同时被至少一个其他使用者读或写,所以在一个值被可变引用时不允许再次被任何引用。

垂悬引用(Dangling References)

这是一个换了个名字的概念,如果放在有指针概念的编程语言里它就指的是那种没有实际指向一个真正能访问的数据的指针(注意,不一定是空指针,还有可能是已经释放的资源)。它们就像失去悬挂物体的绳子,所以叫"垂悬引用"。

"垂悬引用"在 Rust 语言里不允许出现,如果有,编译器会发现它。

下面是一个垂悬的典型案例:

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

Il est évident que, avec la fin de la fonction dangle, la valeur de la variable locale elle-même n'a pas été utilisée comme valeur de retour, et a été libérée. Mais son référence a été retournée, et la valeur pointée par cette référence ne peut plus être déterminée, donc elle n'est pas autorisée à apparaître.