rust - resueltos - ¿Cómo puede esta instancia sobrevivir a su propia vida útil de parámetros?
la duracion maxima de patente para un nuevo medicamento es 17 años (2)
A pesar de sus mejores intenciones, su función de hint
puede no tener el efecto que espera. Pero tenemos mucho terreno que cubrir antes de que podamos entender lo que está pasando.
Vamos a empezar con esto:
fn ensure_equal<''z>(a: &''z (), b: &''z ()) {}
fn main() {
let a = ();
let b = ();
ensure_equal(&a, &b);
}
OK, entonces en main
, definimos dos variables, b
. Tienen vidas distintas, en virtud de ser introducidas por distintas declaraciones let
. ensure_equal
requiere dos referencias con la misma vida útil . Y sin embargo, este código compila. ¿Por qué?
Esto se debe a que, dada ''a: ''b
(lea: ''a
sobrevive ''b
), &''a T
es un variance de &''b T
Digamos que la vida útil de a
es ''a
y la vida útil de b
es ''b
. Es un hecho que ''a: ''b
, porque a
se introduce primero. En la llamada a ensure_equal
, los argumentos se escriben &''a ()
y &''b ()
, respectivamente 1 . Hay una falta de coincidencia de tipos aquí, porque ''a
y ''b
no son la misma vida. ¡Pero el compilador no se rinde todavía! Sabe que &''a ()
es un subtipo de &''b ()
. En otras palabras, a &''a ()
es a &''b ()
. Por lo tanto, el compilador obligará a la expresión &a
a escribir &''b ()
, de modo que se escriban ambos argumentos &''b ()
. Esto resuelve la falta de coincidencia de tipo.
Si está confundido por la aplicación de "subtipos" con tiempos de vida, permítame reformular este ejemplo en términos de Java. Reemplazemos &''a ()
con Programmer
y &''b ()
con Person
. Ahora digamos que el Programmer
se deriva de Person
: Programmer
es, por lo tanto, un subtipo de Person
. Eso significa que podemos tomar una variable de tipo Programmer
y pasarla como un argumento a una función que espera un parámetro de tipo Person
. Es por eso que el siguiente código se compilará exitosamente: el compilador resolverá T
como Person
para la llamada main
.
class Person {}
class Programmer extends Person {}
class Main {
private static <T> void ensureSameType(T a, T b) {}
public static void main(String[] args) {
Programmer a = null;
Person b = null;
ensureSameType(a, b);
}
}
Quizás el aspecto no intuitivo de esta relación de subtipo es que la vida más larga es un subtipo de la vida más corta. Pero piénselo de esta manera: en Java, es seguro fingir que un Programmer
es una Person
, pero no puede asumir que una Person
es un Programmer
. Del mismo modo, es seguro fingir que una variable tiene una vida útil más corta , pero no puede suponer que una variable con una vida útil conocida tenga una vida útil más larga . Después de todo, el punto central de la vida útil de Rust es garantizar que no acceda a los objetos más allá de su vida real.
Ahora, hablemos de la variance . ¿Que es eso?
La varianza es una propiedad que los constructores de tipo tienen con respecto a sus argumentos. Un constructor de tipos en Rust es un tipo genérico con argumentos no vinculados. Por ejemplo,
Vec
es un constructor de tipos que toma unaT
y devuelve unVec<T>
.&
y&mut
son constructores de tipo que toman dos entradas: una vida útil y un tipo al que apuntar.
Normalmente, se esperaría que todos los elementos de un Vec<T>
tengan el mismo tipo (y aquí no estamos hablando de objetos de rasgos). Pero la varianza nos deja engañar con eso.
&''a T
es covariante sobre ''a
y T
Eso significa que dondequiera que veamos &''a T
en un argumento de tipo, podemos sustituirlo por un subtipo de &''a T
Veamos cómo funciona:
fn main() {
let a = ();
let b = ();
let v = vec![&a, &b];
}
Ya hemos establecido que b
tienen diferentes tiempos de vida, y que las expresiones &a
y &b
no tienen el mismo tipo 1 . Entonces, ¿por qué podemos hacer un Vec
de estos? El razonamiento es el mismo que el anterior, así que resumiré: &a
está obligado a &''b ()
, por lo que el tipo de v
es Vec<&''b ()>
.
fn(T)
es un caso especial en Rust cuando se trata de variación. fn(T)
es contravariante sobre T
¡Construyamos un Vec
de funciones!
fn foo(_: &''static ()) {}
fn bar<''a>(_: &''a ()) {}
fn quux<''a>() {
let v = vec![
foo as fn(&''static ()),
bar as fn(&''a ()),
];
}
fn main() {
quux();
}
Esto compila. Pero, ¿cuál es el tipo de v
en quux
? ¿Es Vec<fn(&''static ())>
o Vec<fn(&''a ())>
?
Te daré una pista:
fn foo(_: &''static ()) {}
fn bar<''a>(_: &''a ()) {}
fn quux<''a>(a: &''a ()) {
let v = vec![
foo as fn(&''static ()),
bar as fn(&''a ()),
];
v[0](a);
}
fn main() {
quux(&());
}
Esto no se compila. Aquí están los mensajes del compilador:
error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
--> <anon>:5:13
|
5 | let v = vec![
| _____________^ starting here...
6 | | foo as fn(&''static ()),
7 | | bar as fn(&''a ()),
8 | | ];
| |_____^ ...ending here
|
note: first, the lifetime cannot outlive the lifetime ''a as defined on the body at 4:23...
--> <anon>:4:24
|
4 | fn quux<''a>(a: &''a ()) {
| ________________________^ starting here...
5 | | let v = vec![
6 | | foo as fn(&''static ()),
7 | | bar as fn(&''a ()),
8 | | ];
9 | | v[0](a);
10| | }
| |_^ ...ending here
note: ...so that reference does not outlive borrowed content
--> <anon>:9:10
|
9 | v[0](a);
| ^
= note: but, the lifetime must be valid for the static lifetime...
note: ...so that types are compatible (expected fn(&()), found fn(&''static ()))
--> <anon>:5:13
|
5 | let v = vec![
| _____________^ starting here...
6 | | foo as fn(&''static ()),
7 | | bar as fn(&''a ()),
8 | | ];
| |_____^ ...ending here
= note: this error originates in a macro outside of the current crate
error: aborting due to previous error
Estamos tratando de llamar a una de las funciones en el vector con un argumento &''a ()
. Pero v[0]
espera un &''static ()
, y no hay garantía de que ''a
''static
, por lo que esto no es válido. Por lo tanto, podemos concluir que el tipo de v
es Vec<fn(&''static ())>
. Como puede ver, la contravarianza es lo opuesto a la covarianza: podemos reemplazar una vida útil corta por una más larga .
Vaya, ahora de nuevo a tu pregunta. Primero, veamos qué hace el compilador de la llamada para hint
. hint
tiene la siguiente firma:
fn hint<''a, Arg>(_: &''a Arg) -> Foo<''a>
Foo
es contravariante sobre ''a
porque Foo
envuelve un fn
(o más bien, pretende , gracias a PhantomData
, pero eso no hace una diferencia cuando hablamos de varianza; ambos tienen el mismo efecto), fn(T)
es contravariante. sobre T
y que T
aquí es &''a ()
.
Cuando el compilador intenta resolver la llamada a hint
, solo considera el tiempo de vida de shortlived
. Por lo tanto, la hint
devuelve un Foo
con vida útil shortlived
. Pero cuando tratamos de asignarle eso a la variable foo
, tenemos un problema: un parámetro de vida en un tipo siempre sobrevive al tipo en sí, y la vida útil de shortlived no sobrevive a la vida útil de foo
, tan claramente, no podemos usa ese tipo para foo
. Si Foo
fuera covariante sobre ''a
, ese sería el final y obtendrías un error. Pero Foo
es contravariante en ''a
, por lo que podemos reemplazar la vida útil más shortlived
con una vida más larga. Esa vida puede ser cualquier vida que sobreviva a la vida de foo
. Tenga en cuenta que "sobrevive" no es lo mismo que "sobrevive estrictamente": la diferencia es que ''a: ''a
( ''a
sobrevive ''a
) es verdadera, pero ''a
sobrevive estrictamente ''a
es falsa (es decir, se dice que toda una vida sobreviven a sí mismos, pero no sobreviven estrictamente a sí mismos). Por lo tanto, podríamos terminar con foo
teniendo el tipo Foo<''a>
donde ''a
es exactamente la vida útil de foo
.
Ahora veamos el check(&foo, &outlived);
(ese es el segundo). Este compila porque se &outlived
para que la vida útil se acorte para que coincida con la vida de foo
. Eso es válido porque el tiempo de vida de foo
es más largo que foo
, y el segundo argumento de check
es covariante sobre ''a
porque es una referencia.
¿Por qué no check(&foo, &shortlived);
¿compilar? foo
tiene una vida más larga que la de &shortlived
. el segundo argumento de check
es covariante sobre ''a
, pero su primer argumento es contravariante sobre ''a
, porque Foo<''a>
es contravariante. Es decir, ambos argumentos están tratando de tirar ''a
en direcciones opuestas para esta llamada: &foo
está tratando de ampliar &shortlived
la vida útil (lo que es ilegal), mientras que &shortlived
está intentando acortar la vida útil de &foo
(que también es ilegal). No hay un tiempo de vida que unifique estas dos variables, por lo tanto, la llamada no es válida.
1 Eso podría ser en realidad una simplificación. Creo que el parámetro de duración de una referencia en realidad representa la región en la que el préstamo está activo, en lugar de la duración de la referencia. En este ejemplo, ambos préstamos estarían activos para la declaración que contiene la llamada a ensure_equal
, por lo que tendrían el mismo tipo. Pero si divide los préstamos para separar declaraciones, el código todavía funciona, por lo que la explicación sigue siendo válida. Dicho esto, para que un préstamo sea válido, el referente debe sobrevivir a la región del préstamo, así que cuando estoy pensando en los parámetros de por vida, solo me preocupo por la vida del referente y considero los préstamos por separado.
Antes de encontrar el código a continuación, estaba convencido de que una vida útil en el parámetro de vida útil de un tipo siempre sobreviviría a sus propias instancias. En otras palabras, dado un foo: Foo<''a>
, entonces ''a
siempre sobrevivirá a foo
. Luego fui introducido a este código de contra-argumento por @Luc Danton ( Playground ):
#[derive(Debug)]
struct Foo<''a>(std::marker::PhantomData<fn(&''a ())>);
fn hint<''a, Arg>(_: &''a Arg) -> Foo<''a> {
Foo(std::marker::PhantomData)
}
fn check<''a>(_: &Foo<''a>, _: &''a ()) {}
fn main() {
let outlived = ();
let foo;
{
let shortlived = ();
foo = hint(&shortlived);
// error: `shortlived` does not live long enough
//check(&foo, &shortlived);
}
check(&foo, &outlived);
}
A pesar de que el foo
creado por la hint
parece considerar una vida útil que no dura tanto como ella misma, y una referencia a ella se pasa a una función en un ámbito más amplio, el código compila exactamente como es. Si no se comenta la línea indicada en el código, se genera un error de compilación. Alternativamente, cambiar Foo
a la tupla de estructura (PhantomData<&''a ()>)
también hace que el código ya no se compile con el mismo tipo de error ( Playground ).
¿Cómo es válido el código Rust? ¿Cuál es el razonamiento del compilador aquí?
Otra forma de explicar esto es notar que Foo
realidad no hace referencia a nada con una vida de ''a
. Más bien, tiene una función que acepta una referencia con tiempo ''a
vida ''a
.
Puede construir este mismo comportamiento con una función real en lugar de PhantomData
. E incluso puedes llamar a esa función:
struct Foo<''a>(fn(&''a ()));
fn hint<''a, Arg>(_: &''a Arg) -> Foo<''a> {
fn bar<''a, T: Debug>(value: &''a T) {
println!("The value is {:?}", value);
}
Foo(bar)
}
fn main() {
let outlived = ();
let foo;
{
let shortlived = ();
// &shortlived is borrowed by hint() but NOT stored in foo
foo = hint(&shortlived);
}
foo.0(&outlived);
}
Como explicó Francis en su excelente respuesta, el tipo de outlived
es un subtipo del tipo de vida shortlived
porque su vida útil es más larga. Por lo tanto, la función dentro de foo
puede aceptarlo porque puede ser forzada a una vida más corta (más corta).