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

Прежде чем я наткнулся на приведенный ниже код, я был убежден, что время жизни в параметре времени жизни типа всегда будет переживать его собственные экземпляры. Другими словами, учитывая 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(короче) время жизни.

Другие вопросы по тегам