Как этот экземпляр, по-видимому, переживает свой собственный срок жизни параметра?
Прежде чем я наткнулся на приведенный ниже код, я был убежден, что время жизни в параметре времени жизни типа всегда будет переживать его собственные экземпляры. Другими словами, учитывая foo: Foo<'a>
, затем 'a
всегда переживет foo
, Затем я познакомился с этим контраргументным кодом @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);
}
Хотя foo
создано hint
кажется, рассматривает время жизни, которое не длится так долго, как само по себе, и ссылка на него передается функции в более широкой области видимости, код компилируется точно так, как есть. Раскомментирование строки, указанной в коде, вызывает ошибку компиляции. В качестве альтернативы, изменение Foo
в структурный кортеж (PhantomData<&'a ()>)
также делает код больше не компилируется с такой же ошибкой ( Playground).
Как действителен код Rust? В чем причина компилятора здесь?
2 ответа
Несмотря на ваши лучшие намерения, ваш hint
функция может не иметь ожидаемого эффекта. Но у нас есть немало оснований, чтобы понять, прежде чем мы сможем понять, что происходит.
Давайте начнем с этого:
fn ensure_equal<'z>(a: &'z (), b: &'z ()) {}
fn main() {
let a = ();
let b = ();
ensure_equal(&a, &b);
}
Ок, так в main
мы определяем две переменные, a
а также b
, Они имеют разные жизни, благодаря тому, что let
заявления. ensure_equal
требуется две ссылки с одинаковым временем жизни. И все же этот код компилируется. Зачем?
Это потому, что, учитывая 'a: 'b
(читать: 'a
переживет 'b
), &'a T
это подтип &'b T
,
Допустим, жизнь a
является 'a
и время жизни b
является 'b
, Это факт, что 'a: 'b
, так как a
вводится первым. На призыв к ensure_equal
аргументы набираются &'a ()
а также &'b ()
соответственно 1. Здесь есть несоответствие типов, потому что 'a
а также 'b
не то же самое время жизни. Но компилятор еще не сдается! Это знает что &'a ()
это подтип &'b ()
, Другими словами, &'a ()
это &'b ()
, Поэтому компилятор будет приводить выражение &a
печатать &'b ()
так, чтобы оба аргумента были набраны &'b ()
, Это устраняет несоответствие типов.
Если вас смущает применение "подтипов" к временам жизни, позвольте мне перефразировать этот пример в терминах Java. Давай заменим &'a ()
с Programmer
а также &'b ()
с Person
, Теперь давайте скажем, что Programmer
происходит от Person
: Programmer
поэтому является подтипом Person
, Это означает, что мы можем взять переменную типа Programmer
и передать его в качестве аргумента функции, которая ожидает параметр типа Person
, Вот почему следующий код будет успешно скомпилирован: компилятор разрешит T
как Person
для вызова в 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);
}
}
Возможно, неинтуитивный аспект этого отношения подтипов состоит в том, что более длительное время жизни является подтипом более короткого времени жизни. Но подумайте об этом так: в Java безопасно делать вид, что Programmer
это Person
, но вы не можете предположить, что Person
это Programmer
, Аналогично, можно смело делать вид, что переменная имеет более короткое время жизни, но нельзя предполагать, что переменная с некоторым известным временем жизни на самом деле имеет более длинное время жизни. В конце концов, весь смысл жизни в Rust состоит в том, чтобы гарантировать, что вы не получите доступ к объектам после их фактического времени жизни.
Теперь поговорим о дисперсии. Что это такое?
Дисперсия - это свойство, которое конструкторы типов имеют в отношении своих аргументов. Конструктор типов в Rust - это универсальный тип с несвязанными аргументами. Например
Vec
это конструктор типа, который принимаетT
и возвращаетVec<T>
,&
а также&mut
являются конструкторами типов, которые принимают два входа: время жизни и тип для указания.
Обычно вы ожидаете, что все элементы Vec<T>
иметь тот же тип (и мы не говорим об объектах черт здесь). Но дисперсия позволяет нам обманывать это.
&'a T
ковариантен над 'a
а также T
, Это означает, что везде, где мы видим &'a T
в аргументе типа мы можем заменить его подтипом &'a T
, Давайте посмотрим, как это работает:
fn main() {
let a = ();
let b = ();
let v = vec![&a, &b];
}
Мы уже установили, что a
а также b
имеют разные времена жизни, и что выражения &a
а также &b
не имеют тот же тип 1. Так почему мы можем сделать Vec
из этих? Аргументация та же, что и выше, поэтому я подведу итог: &a
принужден к &'b ()
, так что тип v
является Vec<&'b ()>
,
fn(T)
это особый случай в Rust, когда дело доходит до дисперсии. fn(T)
противоречиво над T
, Давайте построим Vec
функций!
fn foo(_: &'static ()) {}
fn bar<'a>(_: &'a ()) {}
fn quux<'a>() {
let v = vec![
foo as fn(&'static ()),
bar as fn(&'a ()),
];
}
fn main() {
quux();
}
Это компилируется. Но какой тип v
в quux
? Это Vec<fn(&'static ())>
или же Vec<fn(&'a ())>
?
Я дам вам подсказку:
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(&());
}
Это не компилируется. Вот сообщения компилятора:
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
Мы пытаемся вызвать одну из функций в векторе с &'a ()
аргумент. Но v[0]
ожидает &'static ()
и нет никакой гарантии, что 'a
является 'static
, так что это неверно. Поэтому можно сделать вывод, что тип v
является Vec<fn(&'static ())>
, Как видите, контравариантность является противоположностью ковариации: мы можем заменить короткое время жизни более длинным.
Вот так, теперь вернемся к вашему вопросу. Во-первых, давайте посмотрим, что делает компилятор из вызова hint
, hint
имеет следующую подпись:
fn hint<'a, Arg>(_: &'a Arg) -> Foo<'a>
Foo
противоречиво над 'a
так как Foo
оборачивает fn
(точнее, притворяется, благодаря PhantomData
, но это не имеет значения, когда мы говорим о дисперсии; оба имеют одинаковый эффект), fn(T)
противоречиво над T
и это T
здесь &'a ()
,
Когда компилятор пытается разрешить вызов hint
, это только считает shortlived
Пожизненная Следовательно, hint
возвращает Foo
с shortlived
Пожизненная Но когда мы пытаемся присвоить это переменной foo
, у нас есть проблема: параметр времени жизни для типа всегда переживает сам тип, и shortlived
жизнь не выживает foo
так ясно, что мы не можем использовать этот тип для foo
, Если Foo
был ковариантным по 'a
, это будет конец, и вы получите ошибку. Но Foo
противоречиво над 'a
так что мы можем заменить shortlived
жизнь с большей продолжительностью жизни. Та жизнь может быть любой жизнью, которая переживает foo
Пожизненная Обратите внимание, что "переживает" не то же самое, что "строго переживает": разница в том, что 'a: 'a
('a
переживет 'a
) верно, но 'a
строго переживает 'a
является ложным (то есть говорят, что жизнь переживает себя, но она не живет сама по себе). Поэтому мы могли бы в конечном итоге foo
имеющий тип Foo<'a>
где 'a
это точно время жизни foo
сам.
Теперь давайте посмотрим на check(&foo, &outlived);
(это второй). Этот компилируется потому что &outlived
принудительно, так что время жизни сокращается, чтобы соответствовать foo
Пожизненная Это верно, потому что outlived
имеет более длительный срок службы, чем foo
, а также check
Второй аргумент ковариантен над 'a
потому что это ссылка.
Почему не check(&foo, &shortlived);
компилировать? foo
имеет более длительный срок службы, чем &shortlived
, check
Второй аргумент ковариантен над 'a
, но его первый аргумент противоречив 'a
, так как Foo<'a>
противоречиво То есть оба аргумента пытаются вытащить 'a
в противоположных направлениях для этого вызова: &foo
пытается увеличить &shortlived
время жизни (что является незаконным), в то время как &shortlived
пытается сократить &foo
время жизни (что тоже незаконно). Нет времени жизни, которое объединит эти две переменные, поэтому вызов недопустим.
1 Это может быть упрощением. Я полагаю, что параметр времени жизни ссылки на самом деле представляет регион, в котором заем активен, а не время жизни ссылки. В этом примере оба заимствования будут активны для оператора, который содержит вызов ensure_equal
, чтобы они были одного типа. Но если вы разделите кредиты на отдельные let
заявления, код все еще работает, поэтому объяснение по-прежнему в силе. Тем не менее, чтобы заем был действительным, референт должен пережить регион заимствования, поэтому, когда я думаю о параметрах времени жизни, меня волнует только время жизни референта, и я рассматриваю заимствования отдельно.
Другой способ объяснить это состоит в том, чтобы заметить, что Foo
на самом деле не имеет ссылки на что-либо с жизнью 'a
, Скорее, он содержит функцию, которая принимает ссылку с временем жизни 'a
,
Вы можете создать такое же поведение с реальной функцией вместо PhantomData
, И вы даже можете вызвать эту функцию:
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);
}
Как объяснил Фрэнсис в своем превосходном ответе, тип outlived
это подтип типа shortlived
потому что его жизнь дольше. Поэтому функция внутри foo
может принять это, потому что он может быть принужден к shortlived
(короче) время жизни.