Estaba leyendo el capítulo de toda la vida del libro Rust, y me encontré con este ejemplo para una vida con nombre / explícita:
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
Es bastante claro para mí que el compilador previene el error es el uso después de liberar la referencia asignada a x
: después de que se hace el alcance interno, f
y por lo tanto se &f.x
vuelve inválido, y no debería haber sido asignado x
.
Mi problema es que el problema podría haberse analizado fácilmente sin usar la vida útil explícita 'a
, por ejemplo, al inferir una asignación ilegal de una referencia a un alcance más amplio ( x = &f.x;
).
¿En qué casos se necesitan vidas explícitas para evitar errores de uso libre (o alguna otra clase)?
reference
rust
static-analysis
lifetime
corazza
fuente
fuente
Respuestas:
Todas las otras respuestas tienen puntos sobresalientes ( ejemplo concreto de fjh donde se necesita una vida útil explícita ), pero les falta una cosa clave: ¿por qué se necesitan vidas explícitas cuando el compilador le dirá que se equivocó ?
Esta es realmente la misma pregunta que "por qué se necesitan tipos explícitos cuando el compilador puede inferirlos". Un ejemplo hipotético:
Por supuesto, el compilador puede ver que estoy devolviendo un
&'static str
, entonces ¿por qué el programador tiene que escribirlo?La razón principal es que, si bien el compilador puede ver lo que hace su código, no sabe cuál era su intención.
Las funciones son un límite natural para cortafuegos de los efectos del cambio de código. Si permitiéramos inspeccionar completamente las vidas desde el código, entonces un cambio de aspecto inocente podría afectar las vidas, lo que podría causar errores en una función muy lejana. Este no es un ejemplo hipotético. Según tengo entendido, Haskell tiene este problema cuando confía en la inferencia de tipos para las funciones de nivel superior. Rust cortó ese problema particular de raíz.
También hay un beneficio de eficiencia para el compilador: solo se deben analizar las firmas de funciones para verificar los tipos y la vida útil. Más importante aún, tiene un beneficio de eficiencia para el programador. Si no tuviéramos vidas explícitas, ¿qué hace esta función?
Es imposible saberlo sin inspeccionar la fuente, lo que iría en contra de una gran cantidad de mejores prácticas de codificación.
Los ámbitos son vidas, esencialmente. Un poco más claro, una vida útil
'a
es un parámetro genérico de vida útil que puede especializarse con un alcance específico en tiempo de compilación, en función del sitio de la llamada.De ningún modo. Tiempos de vida son necesarios para evitar errores, pero se necesitan tiempos de vida explícitas para proteger lo que tienen pocos programadores cordura.
fuente
f x = x + 1
sin una firma de tipo que está utilizando en otro módulo. Si luego cambia la definición af x = sqrt $ x + 1
, su tipo cambia deNum a => a -> a
aFloating a => a -> a
, lo que provocará errores de tipo en todos los sitios de llamada dondef
se llama, por ejemplo, con unInt
argumento. Tener una firma de tipo asegura que los errores ocurran localmente.sqrt $
, solo se habría producido un error local después del cambio, y no muchos errores en otros lugares (lo que sería mucho mejor si no lo hubiéramos hecho) ¿No quieres cambiar el tipo real)?Echemos un vistazo al siguiente ejemplo.
Aquí, las vidas explícitas son importantes. Esto se compila porque el resultado de
foo
tiene la misma vida útil que su primer argumento ('a
), por lo que puede sobrevivir a su segundo argumento. Esto se expresa mediante los nombres de por vida en la firma defoo
. Si cambiara los argumentos en la llamada alfoo
compilador, se quejaría de quey
no dura lo suficiente:fuente
La anotación de por vida en la siguiente estructura:
especifica que una
Foo
instancia no debería sobrevivir a la referencia que contiene (x
campo).El ejemplo que se encontró en el libro Rust no ilustra esto porque
f
yy
variables de salir de su ámbito, al mismo tiempo.Un mejor ejemplo sería este:
Ahora,
f
realmente sobrevive a la variable señalada porf.x
.fuente
Tenga en cuenta que no hay tiempos de vida explícitos en ese fragmento de código, excepto la definición de estructura. El compilador es perfectamente capaz de inferir vidas en
main()
.En las definiciones de tipo, sin embargo, las vidas explícitas son inevitables. Por ejemplo, hay una ambigüedad aquí:
¿Deberían ser vidas diferentes o deberían ser las mismas? Importa desde la perspectiva del uso,
struct RefPair<'a, 'b>(&'a u32, &'b u32)
es muy diferente destruct RefPair<'a>(&'a u32, &'a u32)
.Ahora, para casos simples, como el que proporcionó, el compilador teóricamente podría eludir vidas como lo hace en otros lugares, pero estos casos son muy limitados y no valen la pena de una complejidad adicional en el compilador, y esta ganancia en claridad sería Muy menos cuestionable.
fuente
'static
,'static
se puede utilizar en todas partes donde se pueden utilizar tiempos de vida locales, por lo tanto, en su ejemplop
tendrá su parámetro de tiempo de vida infiere que el curso de la vida local dely
.RefPair<'a>(&'a u32, &'a u32)
significa que'a
será la intersección de las dos vidas de entrada, es decir, en este caso la vida útil dey
.El caso del libro es muy simple por diseño. El tema de las vidas se considera complejo.
El compilador no puede inferir fácilmente la duración de una función con múltiples argumentos.
Además, mi propia caja opcional tiene un
OptionBool
tipo con unas_slice
método cuya firma en realidad es:No hay absolutamente ninguna manera de que el compilador haya podido resolverlo.
fuente
He encontrado otra gran explicación aquí: http://doc.rust-lang.org/0.12.0/guide-lifetimes.html#returning-references .
fuente
Si una función recibe dos referencias como argumentos y devuelve una referencia, la implementación de la función a veces puede devolver la primera referencia y, a veces, la segunda. Es imposible predecir qué referencia se devolverá para una llamada determinada. En este caso, es imposible inferir un tiempo de vida para la referencia devuelta, ya que cada referencia de argumento puede referirse a un enlace variable diferente con un tiempo de vida diferente. Las vidas explícitas ayudan a evitar o aclarar tal situación.
Del mismo modo, si una estructura contiene dos referencias (como dos campos miembros), una función miembro de la estructura a veces puede devolver la primera referencia y, a veces, la segunda. Una vez más, las vidas explícitas evitan tales ambigüedades.
En algunas situaciones simples, hay una elisión de por vida donde el compilador puede inferir vidas.
fuente
La razón por la cual su ejemplo no funciona es simplemente porque Rust solo tiene una duración local y una inferencia de tipos. Lo que está sugiriendo exige inferencia global. Siempre que tenga una referencia cuya vida útil no se pueda eludir, debe anotarse.
fuente
Como recién llegado a Rust, entiendo que las vidas explícitas tienen dos propósitos.
Poner una anotación explícita de por vida en una función restringe el tipo de código que puede aparecer dentro de esa función. Las vidas explícitas permiten que el compilador se asegure de que su programa esté haciendo lo que usted pretendía.
Si usted (el compilador) quiere comprobar si un código es válido, usted (el compilador) no tendrá que buscar de forma iterativa dentro de cada función llamada. Basta con echar un vistazo a las anotaciones de funciones que ese código llama directamente. Esto hace que su programa sea mucho más fácil de razonar para usted (el compilador) y hace que los tiempos de compilación sean manejables.
En el punto 1., considere el siguiente programa escrito en Python:
que imprimirá
Este tipo de comportamiento siempre me sorprende. Lo que está sucediendo es que
df
está compartiendo memoriaar
, por lo que cuando parte del contenido de losdf
cambios enwork
ese cambio también infectaar
. Sin embargo, en algunos casos esto puede ser exactamente lo que desea, por razones de eficiencia de memoria (sin copia). El verdadero problema en este código es que la funciónsecond_row
está devolviendo la primera fila en lugar de la segunda; buena suerte depurando eso.Considere en su lugar un programa similar escrito en Rust:
Compilando esto, obtienes
De hecho, obtienes dos errores, también hay uno con los roles de
'a
e'b
intercambiado. Alsecond_row
observar la anotación de , encontramos que la salida debería ser&mut &'b mut [i32]
, es decir, se supone que la salida es una referencia a una referencia con duración'b
(la duración de la segunda fila deArray
). Sin embargo, debido a que estamos devolviendo la primera fila (que tiene una vida útil'a
), el compilador se queja de una falta de coincidencia de por vida. En el lugar correcto En el momento adecuado. La depuración es muy fácil.fuente
Pienso en una anotación de por vida como un contrato sobre una referencia dada válida en el ámbito de recepción solo mientras siga siendo válida en el ámbito de origen. Declarar más referencias en el mismo tipo de vida fusiona los ámbitos, lo que significa que todas las referencias de origen deben cumplir con este contrato. Dicha anotación permite al compilador verificar el cumplimiento del contrato.
fuente