Ответ 1
Несмотря на ваши лучшие намерения, ваша функция 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 &'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
, но вы не можете предположить, что a Person
является Programmer
. Точно так же безопасно притворяться, что переменная имеет более короткий срок службы, но вы не можете предположить, что переменная с некоторым известным временем жизни на самом деле имеет более длительный срок службы. В конце концов, вся точка жизни в Rust состоит в том, чтобы гарантировать, что вы не получаете доступ к объектам, находящимся за пределами их фактического срока службы.
Теперь поговорим о variance. Что это?
Отклонение - это свойство, которое конструкторы типа имеют в отношении своих аргументов. Конструктор типа в Rust - это общий тип с несвязанными аргументами. Например,
Vec
- это конструктор типа, который принимаетT
и возвращает aVec<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]
ожидает a &'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
, у нас есть проблема: параметр lifetime в типе всегда переживает сам тип, а 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
, код все еще работает, поэтому объяснение остается в силе. Тем не менее, для того, чтобы заимствование было действительным, референт должен переживать область заимствования, поэтому, когда я думаю о параметрах жизни, я забочусь только о жизни референта, и я считаю, что он занимает отдельно.