Regardons une implémentation simple de ceci :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
Cela échouera avec l'erreur:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
Pour bien comprendre cette erreur, vous devez penser à la façon dont les valeurs sont représentées en mémoire et à ce qui se passe lorsque vous déplacez
ces valeurs. Annotons Combined::new
avec quelques adresses mémoire hypothétiques qui montrent où se trouvent les valeurs:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
Que devrait-il arriver child
? Si la valeur était simplement déplacée comme parent
c'était le cas, cela ferait référence à la mémoire qui n'est plus garantie d'avoir une valeur valide. Tout autre morceau de code est autorisé à stocker des valeurs à l'adresse mémoire 0x1000. Accéder à cette mémoire en supposant qu'il s'agit d'un entier peut entraîner des plantages et / ou des bogues de sécurité, et est l'une des principales catégories d'erreurs que Rust empêche.
C'est exactement le problème que les vies empêchent. Une durée de vie est un peu de métadonnées qui vous permet, ainsi qu'au compilateur, de savoir combien de temps une valeur sera valide à son emplacement de mémoire actuel . C'est une distinction importante, car c'est une erreur courante que font les nouveaux arrivants de Rust. La durée de vie de la rouille n'est pas la période de temps entre la création d'un objet et sa destruction!
Par analogie, pensez-y de cette façon: au cours de la vie d'une personne, elle résidera dans de nombreux endroits différents, chacun avec une adresse distincte. Une vie à Rust se préoccupe de l'adresse où vous résidez actuellement , pas du moment où vous mourrez à l'avenir (bien que la mort change également votre adresse). Chaque fois que vous déménagez, c'est pertinent car votre adresse n'est plus valide.
Il est également important de noter que les durées de vie ne modifient pas votre code; votre code contrôle les durées de vie, vos durées de vie ne contrôlent pas le code. Le dicton lapidaire est "les durées de vie sont descriptives, pas normatives".
Annotons Combined::new
avec quelques numéros de ligne que nous utiliserons pour mettre en évidence les durées de vie:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
La durée de vie concrète de parent
est de 1 à 4, inclus (que je représenterai comme [1,4]
). La durée de vie concrète de child
est [2,4]
et la durée de vie concrète de la valeur de retour est [4,5]
. Il est possible d'avoir des durées de vie concrètes qui commencent à zéro - qui représenteraient la durée de vie d'un paramètre pour une fonction ou quelque chose qui existait en dehors du bloc.
Notez que la durée de vie de child
lui-même est [2,4]
, mais qu'il se réfère à une valeur avec une durée de vie de [1,4]
. C'est très bien tant que la valeur de référence devient invalide avant la valeur de référence. Le problème se produit lorsque nous essayons de revenir child
du bloc. Cela "prolongerait" la durée de vie au-delà de sa longueur naturelle.
Cette nouvelle connaissance devrait expliquer les deux premiers exemples. Le troisième nécessite d'examiner la mise en œuvre de Parent::child
. Il y a de fortes chances que cela ressemble à ceci:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
Cela utilise l' élision à vie pour éviter d'écrire explicitement paramètres de durée de vie génériques . Il équivaut à:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
Dans les deux cas, la méthode indique qu'une Child
structure sera retournée qui a été paramétrée avec la durée de vie concrète de
self
. Autrement dit, l' Child
instance contient une référence à celui Parent
qui l'a créée et ne peut donc pas vivre plus longtemps que cette
Parent
instance.
Cela nous permet également de reconnaître que quelque chose ne va vraiment pas avec notre fonction de création:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
Bien que vous soyez plus susceptible de voir cela écrit sous une forme différente:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
Dans les deux cas, aucun paramètre de durée de vie n'est fourni via un argument. Cela signifie que la durée de vieCombined
sera paramétrée avec n'est limitée par rien - elle peut être ce que l'appelant veut qu'il soit. Cela n'a pas de sens, car l'appelant pourrait spécifier la 'static
durée de vie et il n'y a aucun moyen de remplir cette condition.
Comment je le répare?
La solution la plus simple et la plus recommandée consiste à ne pas tenter de regrouper ces éléments dans la même structure. En faisant cela, l'imbrication de votre structure imitera la durée de vie de votre code. Placez ensemble les types qui possèdent des données dans une structure, puis fournissez des méthodes qui vous permettent d'obtenir des références ou des objets contenant des références selon vos besoins.
Il existe un cas particulier où le suivi à vie est trop zélé: lorsque vous avez quelque chose placé sur le tas. Cela se produit lorsque vous utilisez un
Box<T>
, par exemple. Dans ce cas, la structure déplacée contient un pointeur dans le tas. La valeur pointée restera stable, mais l'adresse du pointeur lui-même se déplacera. En pratique, cela n'a pas d'importance, car vous suivez toujours le pointeur.
La caisse de location (NON PLUS MAINTENUE OU SOUTENUE) ou la caisse owning_ref sont des moyens de représenter ce cas, mais elles nécessitent que l'adresse de base ne bouge jamais . Cela exclut les vecteurs mutants, ce qui peut provoquer une réallocation et un déplacement des valeurs allouées en tas.
Exemples de problèmes résolus avec la location:
Dans d'autres cas, vous souhaiterez peut-être passer à un certain type de comptage de références, comme en utilisant Rc
ou Arc
.
Plus d'information
Après être passé parent
dans la structure, pourquoi le compilateur n'est-il pas en mesure d'obtenir une nouvelle référence parent
et de l'affecter à child
la structure?
Bien qu'il soit théoriquement possible de le faire, cela entraînerait une grande quantité de complexité et de surcharge. Chaque fois que l'objet est déplacé, le compilateur devra insérer du code pour "corriger" la référence. Cela signifierait que la copie d'une structure n'est plus une opération très bon marché qui ne fait que déplacer quelques bits. Cela pourrait même signifier qu'un code comme celui-ci coûte cher, selon la qualité d'un optimiseur hypothétique:
let a = Object::new();
let b = a;
let c = b;
Au lieu de forcer cela à chaque mouvement, le programmeur choisit quand cela se produira en créant des méthodes qui prendront les références appropriées uniquement lorsque vous les appellerez.
Un type avec une référence à lui-même
Il y a un cas spécifique où vous pouvez créer un type avec une référence à lui-même. Vous devez cependant utiliser quelque chose comme Option
pour le faire en deux étapes:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
Cela fonctionne, dans un certain sens, mais la valeur créée est très limitée - elle ne peut jamais être déplacée. Notamment, cela signifie qu'il ne peut pas être renvoyé d'une fonction ou transmis par valeur à quoi que ce soit. Une fonction constructeur présente le même problème avec les durées de vie que ci-dessus:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
Et alors Pin
?
Pin
, stabilisé dans Rust 1.33, a ceci dans la documentation du module :
Un excellent exemple d'un tel scénario serait la construction de structures auto-référentielles, car le déplacement d'un objet avec des pointeurs vers lui-même les invalidera, ce qui pourrait provoquer un comportement indéfini.
Il est important de noter que «autoréférentiel» ne signifie pas nécessairement utiliser une référence . En effet, l' exemple d'une structure auto-référentielle dit précisément (je souligne):
Nous ne pouvons pas en informer le compilateur avec une référence normale, car ce modèle ne peut pas être décrit avec les règles d'emprunt habituelles. Au lieu de cela, nous utilisons un pointeur brut , bien qu'il soit connu pour ne pas être nul, car nous savons qu'il pointe vers la chaîne.
La possibilité d'utiliser un pointeur brut pour ce comportement existe depuis Rust 1.0. En effet, la possession-ref et la location utilisent des pointeurs bruts sous le capot.
La seule chose qui Pin
ajoute à la table est une façon courante de déclarer qu'une valeur donnée est garantie de ne pas bouger.
Voir également:
Parent
etChild
pourrait aider ...