J'ai une valeur et je veux stocker cette valeur et une référence à quelque chose à l'intérieur de cette valeur dans mon propre type:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
Parfois, j'ai une valeur et je veux stocker cette valeur et une référence à cette valeur dans la même structure:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
Parfois, je ne prends même pas une référence de la valeur et j'obtiens la même erreur:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
Dans chacun de ces cas, j'obtiens une erreur indiquant que l'une des valeurs "ne vit pas assez longtemps". Que signifie cette erreur?
lifetime
borrow-checker
rust
Shepmaster
la source
la source
Parent
etChild
pourrait aider ...Réponses:
Regardons une implémentation simple de ceci :
Cela échouera avec l'erreur:
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:Que devrait-il arriver
child
? Si la valeur était simplement déplacée commeparent
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: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 dechild
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 revenirchild
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:Cela utilise l' élision à vie pour éviter d'écrire explicitement paramètres de durée de vie génériques . Il équivaut à:
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 deself
. Autrement dit, l'Child
instance contient une référence à celuiParent
qui l'a créée et ne peut donc pas vivre plus longtemps que cetteParent
instance.Cela nous permet également de reconnaître que quelque chose ne va vraiment pas avec notre fonction de création:
Bien que vous soyez plus susceptible de voir cela écrit sous une forme différente:
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 vie
Combined
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
ouArc
.Plus d'information
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:
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: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:
Et alors
Pin
?Pin
, stabilisé dans Rust 1.33, a ceci dans la documentation du module :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):
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:
la source
Combined
possède leChild
qui possède leParent
. Cela peut ou non avoir du sens selon les types réels que vous avez. Le renvoi de références à vos propres données internes est assez typique.Pin
est surtout un moyen de connaître la sécurité d'une structure contenant un pointeur auto-référentiel . La possibilité d'utiliser un pointeur brut dans le même but existe depuis Rust 1.0.Un problème légèrement différent qui provoque des messages de compilateur très similaires est la dépendance de la durée de vie des objets, plutôt que de stocker une référence explicite. Un exemple de cela est la bibliothèque ssh2 . Lorsque vous développez quelque chose de plus grand qu'un projet de test, il est tentant d'essayer de mettre côte à côte le
Session
etChannel
obtenu à partir de cette session, en cachant les détails de l'implémentation à l'utilisateur. Cependant, notez que laChannel
définition a la'sess
durée de vie dans son annotation de type, alorsSession
que non.Cela provoque des erreurs de compilation similaires liées aux durées de vie.
Une façon de le résoudre de manière très simple consiste à déclarer l'
Session
extérieur dans l'appelant, puis à annoter la référence dans la structure avec une durée de vie, similaire à la réponse dans ce post du forum de l'utilisateur de Rust parlant du même problème tout en encapsulant SFTP . Cela n'aura pas l'air élégant et peut ne pas toujours s'appliquer - parce que maintenant vous avez deux entités à traiter, plutôt qu'une que vous vouliez!Il s'avère que la caisse de location ou la caisse owning_ref de l'autre réponse sont également les solutions à ce problème. Considérons le owning_ref, qui a l'objet spécial à cette fin précise:
OwningHandle
. Pour éviter le déplacement de l'objet sous-jacent, nous l'allouons sur le tas à l'aide de aBox
, ce qui nous donne la solution possible suivante:Le résultat de ce code est que nous ne pouvons plus l'utiliser
Session
, mais il est stocké avec celuiChannel
que nous utiliserons. Parce que l'OwningHandle
objet fait référence àBox
, auquel fait référenceChannel
, lors de son stockage dans une structure, nous le nommons comme tel. REMARQUE: c'est juste ma compréhension. Je soupçonne que cela n'est peut-être pas correct, car cela semble être assez proche d'une discussion sur l'OwningHandle
insécurité .Un détail curieux ici est que
Session
logiquement a une relation similaire avecTcpStream
asChannel
to toSession
, mais sa propriété n'est pas prise et il n'y a pas d'annotations de type autour de cela. Au lieu de cela, c'est à l'utilisateur de s'en occuper, comme le dit la documentation de la méthode de prise de contact :Donc, avec l'
TcpStream
utilisation, il appartient entièrement au programmeur d'assurer l'exactitude du code. Avec leOwningHandle
, l'attention sur l'endroit où se produit la «magie dangereuse» est attirée à l'aide duunsafe {}
bloc.Une discussion plus approfondie et plus approfondie de ce problème se trouve dans ce fil de discussion du forum de l'utilisateur de Rust - qui comprend un exemple différent et sa solution utilisant la caisse de location, qui ne contient pas de blocs dangereux.
la source