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

Как синтаксис <> отличается от регулярной привязки к жизни?

Рассмотрим следующий код:

trait Trait<T> {}

fn foo<'a>(_b: Box<dyn Trait<&'a usize>>) {}
fn bar(_b: Box<dyn for<'a> Trait<&'a usize>>) {}

Обе функции foo и bar, кажется, принимают Box<Trait<&'a usize>>, хотя foo делает это более кратко, чем bar. В чем разница между ними?

Кроме того, в каких ситуациях мне понадобится синтаксис for<>, подобный приведенному выше? Я знаю, что стандартная библиотека Rust использует его внутренне (часто это связано с замыканиями), но зачем моему коду это может понадобиться?

4b9b3361

Ответ 1

for<> синтаксис называется границей с более высоким рангом (HRTB), и он действительно был введен главным образом из-за замыканий.

Короче говоря, разница между foo и bar заключается в том, что в foo() время жизни для внутренней ссылки usize предоставляется вызывающей функцией, а в bar() предусмотрено одно и то же время жизни самой функцией. И это различие очень важно для реализации foo/bar.

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

trait Trait<T> {
    fn do_something(&self, value: T);
}

Помните, что параметры времени жизни очень похожи на параметры типового типа. Когда вы используете общую функцию, вы всегда указываете все ее параметры типа, предоставляя конкретные типы, а компилятор мономорфизует эту функцию. То же самое происходит с параметрами жизни: когда вы вызываете функцию с параметром lifetime, вы указываете время жизни, хотя и неявно:

// imaginary explicit syntax
// also assume that there is TraitImpl::new::<T>() -> TraitImpl<T>,
// and TraitImpl<T>: Trait<T>

'a: {
    foo::<'a>(Box::new(TraitImpl::new::<&'a usize>()));
}

И теперь есть ограничение на то, что foo() может сделать с этим значением, то есть с каким аргументом он может называть do_something(). Например, это не скомпилируется:

fn foo<'a>(b: Box<Trait<&'a usize>>) {
    let x: usize = 10;
    b.do_something(&x);
}

Это не скомпилируется, потому что локальные переменные имеют времена жизни, которые строго меньше сроков жизни, заданных параметрами жизненного цикла (я думаю, это понятно, почему это так), поэтому вы не можете вызвать b.do_something(&x), потому что для этого требуется его аргумент иметь время жизни 'a, которое строго больше, чем x.

Однако вы можете сделать это с помощью bar:

fn bar(b: Box<for<'a> Trait<&'a usize>>) {
    let x: usize = 10;
    b.do_something(&x);
}

Это работает, потому что теперь bar может выбрать необходимое время жизни вместо вызывающего bar.

Это имеет значение, когда вы используете закрытие, которое принимает ссылки. Например, предположим, что вы хотите написать метод filter() на Option<T>:

impl<T> Option<T> {
    fn filter<F>(self, f: F) -> Option<T> where F: FnOnce(&T) -> bool {
        match self {
            Some(value) => if f(&value) { Some(value) } else { None }
            None => None
        }
    }
}

Закрытие здесь должно принимать ссылку на T, потому что иначе было бы невозможно вернуть значение, содержащееся в опции (это то же самое, что и с filter() на итераторах).

Но какое время жизни должно быть &T в FnOnce(&T) -> bool? Помните, что мы не указываем времена жизни в сигнатурах функций только потому, что существует пожизненное исключение; на самом деле компилятор вводит параметр lifetime для каждой ссылки внутри сигнатуры функции. Должно быть некоторое время жизни, связанное с &T в FnOnce(&T) -> bool. Таким образом, наиболее "очевидным" способом расширения сигнатуры выше было бы следующее:

fn filter<'a, F>(self, f: F) -> Option<T> where F: FnOnce(&'a T) -> bool

Однако это не сработает. Как и в примере с Trait выше, время жизни 'a строго превышает время жизни любой локальной переменной в этой функции, включая value внутри оператора соответствия. Поэтому невозможно применить f к &value из-за несоответствия времени жизни. Вышеупомянутая функция, написанная с такой сигнатурой, не будет компилироваться.

С другой стороны, если мы разложим сигнатуру filter(), как это (и на самом деле это то, как пожизненное исключение для закрытий работает в Rust сейчас):

fn filter<F>(self, f: F) -> Option<T> where F: for<'a> FnOnce(&'a T) -> bool

тогда вызов f с &value в качестве аргумента совершенно верен: теперь мы можем выбрать время жизни, поэтому использование времени жизни локальной переменной абсолютно нормально. И то, что важны HRTB: вы не сможете выразить много полезных шаблонов без них.

Вы также можете прочитать другое объяснение HRTB в Nomicon.