Je lisais le chapitre sur les vies du livre Rust, et je suis tombé sur cet exemple pour une durée de vie nommée / explicite:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x; // -+ x goes into scope
// |
{ // |
let y = &5; // ---+ y goes into scope
let f = Foo { x: y }; // ---+ f goes into scope
x = &f.x; // | | error here
} // ---+ f and y go out of scope
// |
println!("{}", x); // |
} // -+ x goes out of scope
Il est tout à fait clair pour moi que l'erreur empêchée par le compilateur est l' utilisation après libération de la référence affectée à x
: une fois la portée interne terminée, f
et donc &f.x
invalide, et n'aurait pas dû être affectée à x
.
Mon problème est que le problème aurait pu être facilement analysé sans utiliser la durée de vie explicite 'a
, par exemple en inférant une affectation illégale d'une référence à une portée plus large ( x = &f.x;
).
Dans quels cas des durées de vie explicites sont-elles réellement nécessaires pour éviter les erreurs d'utilisation après la libération (ou d'une autre classe?)?
reference
rust
static-analysis
lifetime
corazza
la source
la source
Réponses:
Les autres réponses ont toutes des points saillants ( l'exemple concret de fjh où une durée de vie explicite est nécessaire ), mais il manque une chose clé: pourquoi des durées de vie explicites sont-elles nécessaires lorsque le compilateur vous dira que vous vous êtes trompé ?
Il s'agit en fait de la même question que «pourquoi des types explicites sont-ils nécessaires lorsque le compilateur peut les déduire». Un exemple hypothétique:
Bien sûr, le compilateur peut voir que je retourne un
&'static str
, alors pourquoi le programmeur doit-il le taper?La raison principale est que, bien que le compilateur puisse voir ce que fait votre code, il ne sait pas quelle était votre intention.
Les fonctions sont une frontière naturelle pour pare-feu les effets du changement de code. Si nous devions permettre que les durées de vie soient complètement inspectées à partir du code, un changement d'apparence innocente pourrait affecter les durées de vie, ce qui pourrait alors provoquer des erreurs dans une fonction éloignée. Ce n'est pas un exemple hypothétique. Si je comprends bien, Haskell a ce problème lorsque vous comptez sur l'inférence de type pour les fonctions de niveau supérieur. La rouille a étouffé ce problème particulier dans l'œuf.
Le compilateur présente également un avantage en termes d'efficacité: seules les signatures de fonction doivent être analysées afin de vérifier les types et les durées de vie. Plus important encore, il présente un avantage d'efficacité pour le programmeur. Si nous n'avions pas de durée de vie explicite, que fait cette fonction:
Il est impossible de le dire sans inspecter la source, ce qui irait à l'encontre d'un grand nombre de bonnes pratiques de codage.
Les portées sont essentiellement des durées de vie. Un peu plus clairement, une durée de vie
'a
est un paramètre de durée de vie générique qui peut être spécialisé avec une portée spécifique au moment de la compilation, en fonction du site d'appel.Pas du tout. Des durées de vie sont nécessaires pour éviter les erreurs, mais des durées de vie explicites sont nécessaires pour protéger le peu de programmeurs sains.
la source
f x = x + 1
sans signature de type que vous utilisez dans un autre module. Si vous modifiez ultérieurement la définition enf x = sqrt $ x + 1
, son type passe deNum a => a -> a
àFloating a => a -> a
, ce qui entraînera des erreurs de type sur tous les sites d'appel oùf
est appelé, par exemple avec unInt
argument. Avoir une signature de type garantit que les erreurs se produisent localement.sqrt $
, seule une erreur locale se serait produite après le changement, et pas beaucoup d'erreurs dans d'autres endroits (ce qui est beaucoup mieux si nous ne l'avons pas fait) ne veux pas changer le type réel)?Jetons un coup d'œil à l'exemple suivant.
Ici, les durées de vie explicites sont importantes. Cela se compile car le résultat de
foo
a la même durée de vie que son premier argument ('a
), il peut donc survivre à son deuxième argument. Ceci est exprimé par les noms à vie dans la signature defoo
. Si vous commutiez les arguments de l'appel versfoo
le compilateur, vous vous plaindriez dey
ne pas vivre assez longtemps:la source
L'annotation à vie dans la structure suivante:
spécifie qu'une
Foo
instance ne doit pas survivre à la référence qu'elle contient (x
champ).L'exemple que vous avez rencontré dans le livre Rust n'illustre pas cela car
f
et lesy
variables sortent du champ d'application en même temps.Un meilleur exemple serait ceci:
Maintenant,
f
survit vraiment la variable pointée parf.x
.la source
Notez qu'il n'y a pas de durée de vie explicite dans ce morceau de code, à l'exception de la définition de la structure. Le compilateur est parfaitement capable d'inférer des durées de vie dans
main()
.Dans les définitions de type, cependant, des durées de vie explicites sont inévitables. Par exemple, il y a une ambiguïté ici:
Ces durées devraient-elles être différentes ou devraient-elles être identiques? Il importe du point de vue de l'utilisation,
struct RefPair<'a, 'b>(&'a u32, &'b u32)
est très différent destruct RefPair<'a>(&'a u32, &'a u32)
.Maintenant, pour des cas simples, comme celui que vous avez fourni, le compilateur pourrait théoriquement éliminer des durées de vie comme il le fait dans d'autres endroits, mais ces cas sont très limités et ne valent pas une complexité supplémentaire dans le compilateur, et ce gain de clarté serait au très peu discutable.
la source
'static
, elles'static
peuvent être utilisées partout où les durées de vie locales peuvent être utilisées, par conséquent, dans votre exemplep
, son paramètre de durée de vie sera déduit comme la durée de vie locale dey
.RefPair<'a>(&'a u32, &'a u32)
signifie que ce'a
sera l'intersection des deux durées de vie d'entrée, c'est-à-dire dans ce cas la durée de vie dey
.Le boîtier du livre est très simple de par sa conception. Le sujet des vies est jugé complexe.
Le compilateur ne peut pas facilement déduire la durée de vie d'une fonction avec plusieurs arguments.
De plus, ma propre caisse optionnelle a un
OptionBool
type avec uneas_slice
méthode dont la signature est en fait:Il n'y a absolument aucun moyen que le compilateur ait pu comprendre cela.
la source
J'ai trouvé une autre grande explication ici: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .
la source
Si une fonction reçoit deux références en tant qu'arguments et renvoie une référence, l'implémentation de la fonction peut parfois renvoyer la première référence et parfois la seconde. Il est impossible de prédire quelle référence sera renvoyée pour un appel donné. Dans ce cas, il est impossible d'inférer une durée de vie pour la référence renvoyée, car chaque référence d'argument peut faire référence à une liaison de variable différente avec une durée de vie différente. Des durées de vie explicites aident à éviter ou à clarifier une telle situation.
De même, si une structure contient deux références (comme deux champs membres), une fonction membre de la structure peut parfois renvoyer la première référence et parfois la seconde. Encore une fois, des durées de vie explicites empêchent de telles ambiguïtés.
Dans quelques situations simples, il existe une élision de durée de vie où le compilateur peut déduire des durées de vie.
la source
La raison pour laquelle votre exemple ne fonctionne pas est simplement parce que Rust n'a qu'une durée de vie locale et une inférence de type. Ce que vous proposez exige une inférence globale. Chaque fois que vous avez une référence dont la durée de vie ne peut être éluée, elle doit être annotée.
la source
En tant que nouveau venu à Rust, je crois comprendre que les durées de vie explicites ont deux objectifs.
Mettre une annotation explicite à vie sur une fonction restreint le type de code qui peut apparaître à l'intérieur de cette fonction. Des durées de vie explicites permettent au compilateur de s'assurer que votre programme fait ce que vous vouliez.
Si vous (le compilateur) voulez (s) vérifier si un morceau de code est valide, vous (le compilateur) n'aurez pas à regarder itérativement à l'intérieur de chaque fonction appelée. Il suffit de regarder les annotations des fonctions qui sont directement appelées par ce morceau de code. Cela rend votre programme beaucoup plus facile à raisonner (le compilateur) et rend les temps de compilation gérables.
Au point 1, considérons le programme suivant écrit en Python:
qui imprimera
Ce type de comportement me surprend toujours. Ce qui se passe, c'est le
df
partage de la mémoire avecar
, donc quand une partie du contenu desdf
changementswork
, ce changement infectear
aussi. Cependant, dans certains cas, cela peut être exactement ce que vous voulez, pour des raisons d'efficacité de la mémoire (pas de copie). Le vrai problème dans ce code est que la fonctionsecond_row
renvoie la première ligne au lieu de la seconde; bonne chance pour déboguer ça.Considérez plutôt un programme similaire écrit en Rust:
Compiler cela, vous obtenez
En fait, vous obtenez deux erreurs, il y en a aussi une avec les rôles de
'a
et'b
échangés. En regardant l'annotation desecond_row
, nous constatons que la sortie doit être&mut &'b mut [i32]
, c'est-à-dire que la sortie est censée être une référence à une référence avec une durée de vie'b
(la durée de vie de la deuxième ligne deArray
). Cependant, comme nous renvoyons la première ligne (qui a une durée de vie'a
), le compilateur se plaint de la non-concordance de la durée de vie. Au bon endroit. Au bon moment. Le débogage est un jeu d'enfant.la source
Je pense qu'une annotation à vie comme un contrat sur une référence donnée n'a été valide dans la portée de réception que pendant qu'elle reste valide dans la portée source. La déclaration de plusieurs références dans le même type de durée de vie fusionne les étendues, ce qui signifie que toutes les références source doivent respecter ce contrat. Une telle annotation permet au compilateur de vérifier l'exécution du contrat.
la source