Невозможно вывести подходящее время жизни для замыкания, которое возвращает ссылку

Учитывая следующий код:

fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || &t)
}

Что я ожидаю:

  • Тип T имеет время жизни 'a.
  • Значение t живет до тех пор, пока T
  • t движется к закрытию, поэтому закрытие жить до тех пор, как t
  • Закрытие возвращает ссылку на t которая была перемещена в закрытие. Таким образом, ссылка действительна, пока существует замыкание.
  • Нет проблем на всю жизнь, код компилируется.

Что на самом деле происходит:

  • Код не компилируется:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
 --> src/lib.rs:2:22
  |
2 |     Box::new(move || &t)
  |                      ^^
  |
note: first, the lifetime cannot outlive the lifetime  as defined on the body at 2:14...
 --> src/lib.rs:2:14
  |
2 |     Box::new(move || &t)
  |              ^^^^^^^^^^
note: ...so that closure can access 't'
 --> src/lib.rs:2:22
  |
2 |     Box::new(move || &t)
  |                      ^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 1:8...
 --> src/lib.rs:1:8
  |
1 | fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
  |        ^^
  = note: ...so that the expression is assignable:
          expected std::boxed::Box<(dyn std::ops::Fn() -> &'a T + 'a)>
             found std::boxed::Box<dyn std::ops::Fn() -> &T>

Я не понимаю конфликта. Как я могу это исправить?

Ответы

Ответ 1

Очень интересный вопрос! Я думаю, что я понял проблему в игре здесь. Позвольте мне попытаться объяснить.

Замыкания tl; dr: move не могут возвращать ссылки на свое окружение, поскольку это будет ссылка на self. Такая ссылка не может быть возвращена, потому что черты Fn* не позволяют нам выразить это. Это в основном то же самое, что и проблема потокового итератора, и может быть исправлено с помощью GAT (общих связанных типов).


Реализуя это вручную

Как вы, вероятно, знаете, когда вы пишете замыкание, компилятор генерирует структуру и блоки impl для соответствующих признаков Fn, поэтому замыкания в основном являются синтаксическим сахаром. Давайте попробуем избежать всего этого сахара и создадим ваш тип вручную.

Вам нужен тип, который владеет другим типом и может возвращать ссылки на этот принадлежащий тип. И вы хотите иметь функцию, которая возвращает упакованный экземпляр указанного типа.

struct Baz<T>(T);

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

fn make_baz<T>(t: T) -> Box<Baz<T>> {
    Box::new(Baz(t))
}

Это довольно эквивалентно вашему закрытию в штучной упаковке. Давайте попробуем использовать это:

let outside = {
    let s = "hi".to_string();
    let baz = make_baz(s);
    println!("{}", baz.call()); // works

    baz
};

println!("{}", outside.call()); // works too

Это работает просто отлично. Строка s перемещается в тип Baz а этот экземпляр Baz перемещается в Box. Теперь s принадлежит baz а затем outside.

Это становится более интересным, когда мы добавляем один символ:

let outside = {
    let s = "hi".to_string();
    let baz = make_baz(&s);  // <-- NOW BORROWED!
    println!("{}", baz.call()); // works

    baz
};

println!("{}", outside.call()); // doesn't work!

Теперь мы не можем сделать время жизни baz больше, чем время жизни s, так как baz содержит ссылку на s которая будет висящей ссылкой на s выйдет из области видимости раньше, чем baz.

Смысл этого фрагмента, который я хотел бы подчеркнуть: нам не нужно было аннотировать время жизни типа Baz чтобы сделать это безопасным; Руст понял это сам и убедился, что baz живет не дольше, чем s. Это будет важно ниже.

Написание черты для этого

Пока мы только охватили основы. Давайте попробуем написать такую черту, как Fn чтобы приблизиться к исходной проблеме:

trait MyFn {
    type Output;
    fn call(&self) -> Self::Output;
}

В нашей характеристике нет никаких параметров функции, но в остальном она довольно идентична реальной характеристике Fn.

Давай реализуем это!

impl<T> MyFn for Baz<T> {
    type Output = ???;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

Теперь у нас есть проблема: что мы пишем вместо ??? ? Наивно можно было бы написать &T... но нам нужен параметр времени жизни для этой ссылки. Где взять? Какое время жизни у возвращаемого значения?

Давайте проверим функцию, которую мы реализовали ранее:

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

Поэтому здесь мы также используем &T без параметра времени жизни. Но это работает только из-за жизненного выбора. По сути, компилятор заполняет пробелы, так что fn call(&self) → &T эквивалентен:

fn call<'s>(& self) -> & T

Ага, так что время жизни возвращенной ссылки связано с временем жизни самого self ! (более опытные пользователи Rust могут уже чувствовать, куда это идет...).

(В качестве примечания: почему возвращаемая ссылка не зависит от времени жизни самого T? Если T ссылается на что-то non- 'static то это необходимо учитывать, верно? Да, но это уже учитывается! Помните, что ни одного случая Baz<T> может когда - нибудь жить дольше, чем вещь T может ссылаться. Так что self время жизни уже короче, чем то, что срок службы T может иметь. Таким образом, нам нужно только сосредоточиться на self всю жизнь)

Но как мы можем выразить это в чертах характера? Оказывается: мы не можем (пока). Эта проблема регулярно упоминается в контексте потоковых итераторов, то есть итераторов, которые возвращают элемент со временем жизни, привязанным к self жизни. В сегодняшнем Русте, к сожалению, это невозможно реализовать; система типов недостаточно сильна.

Как насчет будущего?

К счастью, существует RFC "Generic Associated Types", который был объединен некоторое время назад. Этот RFC расширяет систему типов Rust, позволяя ассоциированным типам черт быть универсальными (в течение других типов и времени жизни).

Давайте посмотрим, как мы можем заставить ваш пример (вроде) работать с GAT (согласно RFC; этот материал пока не работает ☹). Сначала мы должны изменить определение черты:

trait MyFn {
    type Output<'a>;   // <-- we added <'a> to make it generic
    fn call(&self) -> Self::Output;
}

Сигнатура функции в коде не изменилась, но обратите внимание на то, что время жизни возрастает! Вышеупомянутый fn call(&self) → Self::Output эквивалентен:

fn call<'s>(& self) -> Self::Output<'s>

Таким образом, время жизни ассоциированного типа связано с временем жизни самого self. Как мы и хотели! impl выглядит так:

impl<T> MyFn for Baz<T> {
    type Output<'a> = &'a T;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

Чтобы вернуть упакованный MyFn нам нужно написать это (в соответствии с этим разделом RFC:

fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
    Box::new(Baz(t))
}

А что, если мы хотим использовать реальную черту Fn? Насколько я понимаю, мы не можем, даже с GAT. Я думаю, что невозможно изменить существующую черту Fn для использования GAT обратно совместимым образом. Поэтому вполне вероятно, что стандартная библиотека сохранит менее мощную черту как есть. (примечание: как развивать стандартную библиотеку несовместимыми способами использования новых языковых функций - это то, о чем я уже несколько раз задумывался; пока я не слышал ни о каком реальном плане в этом отношении; я надеюсь, что команда Rust придет с чем-то...)


Резюме

То, что вы хотите, не является технически невозможным или небезопасным (мы реализовали его как простую структуру, и она работает). Однако, к сожалению, сейчас невозможно выразить то, что вы хотите, в виде замыканий/черт Fn в системе типов Rust. С этой же проблемой сталкиваются потоковые итераторы.

С запланированной функцией GAT можно выразить все это в системе типов. Тем не менее, стандартная библиотека должна была бы как-то наверстать упущенное, чтобы сделать возможным ваш точный код.

Ответ 2

Что я ожидаю:

  • Тип T имеет время жизни 'a.
  • Значение t равно T

Это не имеет никакого смысла. Значение не может "жить так долго", как тип, потому что тип не живет. " T имеет пожизненное 'a "- очень неточное утверждение, которое легко понять неправильно. То, что T: 'a действительно означает, что" экземпляры T должны оставаться действительными, по крайней мере, до тех пор, пока это время жизни 'a. Например, T не должно быть ссылкой с временем жизни короче, чем 'a или структурой, содержащей такую ссылку. что это не имеет никакого отношения к формированию ссылок на T, т.е. &T

Таким образом, значение t продолжаться до тех пор, пока его лексический масштаб (это параметр функции) говорит, что он делает, что не имеет никакого отношения к 'a вообще".

  • t движется к закрытию, поэтому замыкание продолжается до тех пор, пока t

Это также неверно. Закрытие продолжается до тех пор, пока закрытие происходит лексически. Он является временным в выражении результата и, следовательно, живет до конца выражения результата. t время жизни относится к закрытию совсем не так, поскольку у него есть собственная переменная T внутри, захват t. Поскольку захват является копией/перемещением t, это никоим образом не зависит от времени жизни t.

Временное закрытие затем перемещается в хранилище ящиков, но это новый объект с его собственным временем жизни. Время жизни этого замыкания связано с временем жизни поля, то есть оно является возвращаемым значением функции, а позже (если вы храните ящик вне функции) время жизни любой переменной, в которой вы храните ящик.

Все это означает, что замыкание, возвращающее ссылку на его собственное состояние захвата, должно привязывать время жизни этой ссылки к ее собственной ссылке. К сожалению, это невозможно.

Вот почему:

FnMut Fn подразумевает черту FnMut, которая, в свою очередь, подразумевает свойство FnOnce. То есть, каждый объект функции в Rust может быть вызван с помощью аргумента self -value. Это означает, что каждый объект функции должен быть все еще действительным, вызываемый с помощью аргумента self -value и возвращающего то же самое, что и всегда.

Другими словами, попытка написать замыкание, возвращающее ссылку на собственные захваты, расширяется примерно до этого кода:

struct Closure<T> {
    captured: T,
}
impl<T> FnOnce<()> for Closure<T> {
    type Output = &'??? T; // what do I put as lifetime here?
    fn call_once(self, _: ()) -> Self::Output {
        &self.captured // returning reference to local variable
                       // no matter what, the reference would be invalid once we return
    }
}

И именно поэтому то, что вы пытаетесь сделать, принципиально невозможно. Сделайте шаг назад, подумайте о том, что вы на самом деле пытаетесь выполнить с этим закрытием, и найдите другой способ его достижения.

Ответ 3

Вы ожидаете, что тип T имеет время жизни 'a, но t не является ссылкой на значение типа T Функция переходит в собственность переменной t путем передачи аргумента:

// t is moved here, t lifetime is the scope of the function
fn foo<'a, T: 'a>(t: T)

Ты должен сделать:

fn foo<'a, T: 'a>(t: &'a T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || t)
}

Ответ 4

Другие ответы носят первоклассный характер, но я хотел перезвонить с другой причиной, по которой ваш исходный код не мог работать. Большая проблема заключается в подписи:

fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>

Это говорит о том, что вызывающий может указать время жизни при вызове foo и код будет действительным и безопасным для памяти. Это не может быть правдой для этого кода. Это не имело бы смысла называть это с 'a наборе к 'static, но ничего об этой подписи бы предотвратить это.