Подтвердить что ты не робот

Как этот экземпляр, казалось бы, переживает свой собственный ресурс?

Прежде чем я наткнулся на приведенный ниже код, я был убежден, что срок жизни в параметре lifetime всегда будет переживать свои собственные экземпляры. Другими словами, при a foo: Foo<'a>, тогда 'a всегда будет переживать foo. Затем я познакомился с этим кодом встречного аргумента @Luc Danton (Игровая площадка):

#[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 на struct tuple (PhantomData<&'a ()>) также заставляет код больше не компилироваться с такой же ошибкой (Playground).

Как это действует Код ржавчины? Что такое аргумент компилятора здесь?

4b9b3361

Ответ 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 и возвращает a 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] ожидает 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, код все еще работает, поэтому объяснение остается в силе. Тем не менее, для того, чтобы заимствование было действительным, референт должен переживать область заимствования, поэтому, когда я думаю о параметрах жизни, я забочусь только о жизни референта, и я считаю, что он занимает отдельно.

Ответ 2

Еще один способ объяснить это - заметить, что 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 (короче) времени жизни.