Детализация изменений в «Impl Traz» в ржавчине

Детализация изменений в «Impl Traz» в ржавчине

1 августа 2025 г.

По умолчаниюimpl TraitРабота в обратном положении меняется в Rust 2024. Эти изменения предназначены для упрощенияimpl TraitЧтобы лучше соответствовать тому, что люди хотят большую часть времени. Мы также добавляем гибкий синтаксис, который дает вам полный контроль, когда вам это нужно.

TL; DR

Начиная с Rust 2024, мы меняем правила, когда в скрытом типе возврата может использоваться общий параметрimpl Trait:

  • Новый дефолт, который скрытые типы для положения возвратаimpl Traitможет использоватьлюбойобщий параметр в области объема, а не только типы (применимо только в Rust 2024);
  • Синтаксис для явного объявления, какие типы могут быть использованы (можно использовать в любом издании).

Новый явный синтаксис называется «Использование связано»:impl Trait + use<'x, T>, например, указывает на то, что скрытый тип разрешено использовать'xиT(но не любые другие общие параметры в области объема).

Читайте дальше для деталей!

Предпосылки: Поставка возвратаimpl Trait

В этом сообщении касается касаетсяпоставка возвратаimpl Trait, например, следующий пример:

fn process_data(
    data: &[Datum]
) -> impl Iterator<Item = ProcessedDatum> {
    data
        .iter()
        .map(|datum| datum.process())
}

Использование-> impl IteratorВнешняя позиция здесь означает, что функция возвращает «какой -то итератор». Фактический тип будет определяться компилятором на основе тела функции. Он называется «скрытым типом», потому что вызывающие абоненты не знают точно, что это такое; они должны кодироваться противIteratorчерта. Однако во время генерации кода компилятор будет генерировать код на основе фактического точного типа, который гарантирует, что вызывающие абоненты полностью оптимизированы.

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

Текущие правила Руста заключается в том, что положением возвратаimpl Traitзначение может использовать ссылку только в том случае, если срок службы этой ссылки появляется вimpl Traitсам В этом примере,impl Iterator<Item = ProcessedDatum>не ссылается на какую -либо жизнь и, следовательно, захватываетdataнезаконно. Вы можете увидеть это для себяна детской площадкеПолем

Сообщение об ошибке («скрытый тип захватывает срок службы»), которое вы получаете в этом сценарии, не является самым интуитивным, но оно имеет полезное предложение о том, как его исправить:

help: to declare that
      `impl Iterator<Item = ProcessedDatum>`
      captures `'_`, you can add an
      explicit `'_` lifetime bound
  |
5 | ) -> impl Iterator<Item = ProcessedDatum> + '_ {
  |                                           ++++

Следуя немного более явной версии этого совета, подпись функции становится:

fn process_data<'d>(
    data: &'d [Datum]
) -> impl Iterator<Item = ProcessedDatum> + 'd {
    data
        .iter()
        .map(|datum| datum.process())
}

В этой версии жизнь'dданные явно ссылаются вimpl TraitТип, и поэтому его разрешают использовать. Это также сигнал для вызывающего абонента, что заимствование дляdataдолжен длиться до тех пор, пока используется итератор, что означает, что он (правильно) помечает ошибку в таком примере, как это (Попробуйте это на детской площадке):

let mut data: Vec<Datum> = vec![Datum::default()];
let iter = process_data(&data);
data.push(Datum::default()); // <-- Error!
iter.next();

Проблемы с удобством использования с этим дизайном

Правила того, какие общие параметры можно использовать вimpl Traitбыли решены на ранних этапах на основе ограниченного набора примеров. Со временем мы заметили ряд проблем с ними.

Не правильный дефолт

Обследования основных кодовых баз (как компилятора, так и ящиков на ящиках.

недостаточно гибкий

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

Но по умолчанию также неверно, потому что некоторые функции хотят явно указать, что они не используют параметры типа в типе возврата, и сейчас этого невозможно переопределить. Первоначальное намерение заключалось в том, чтоТип псевдонимарешит этот вариант использования, но это было бы очень неганомическим решением (и стабилизирующая черта типа псевдоним IMP занимает больше времени, чем предполагалось из-за других осложнений).

трудно объяснить

Поскольку по умолчанию неверны, эти ошибки сталкиваются с пользователями довольно регулярно, и все же их также тонкие и трудно объяснить (о чем свидетельствует этот пост!). Добавление подсказки компилятора, чтобы предложить+ '_Помогает, но это не здорово, что пользователи должны следить за намеком, который они не до конца понимают.

неверное предложение

Добавление а+ '_аргументimpl TraitМожет быть, сбивает с толку, но это не очень сложно. К сожалению, это часто неправильная аннотация, что приводит к ненужным ошибкам компилятора - иверноИсправление является либо сложным, либо иногда даже невозможным. Рассмотрим пример, подобный этому:

fn process<'c, T> {
    context: &'c Context,
    data: Vec<T>,
) -> impl Iterator<Item = ()> + 'c {
    data
        .into_iter()
        .map(|datum| context.process(datum))
}

ЗдесьprocessФункция применяетсяcontext.processк каждому из элементов вdata(типаT) Потому что возвращаемое значение используетcontext, это объявлено как+ 'cПолем Наша реальная цель здесь - разрешить использовать тип возврата'c; письмо+ 'cдостигает этой цели, потому что'cтеперь появляется в граничном списке. Однако во время написания+ 'cэто удобный способ сделать'cпоявляются в границах, также означает, что скрытый тип должен пережить'cПолем Это требование не требуется и фактически приведет к ошибке компиляции в этом примере (Попробуйте это на детской площадке)

Причина, по которой эта ошибка возникает, немного тонкая. Скрытый тип - это тип итератора, основанный на результатеdata.into_iter(), который будет включать типTПолем Из -за+ 'cсвязан, скрытый тип должен пережить'c, что, в свою очередь, означает, чтоTдолжен пережить'cПолем НоTявляется общим параметром, поэтому компилятор требуется, чтобы оказатьсяwhere T: 'cПолем Это, где означает «безопасно создать ссылку с жизнью с жизнью'cк типуT".

Но на самом деле мы не создаем такую ссылку, поэтому, где не должно потребоваться. Это необходимо только потому, что мы использовали удобный, но с неверным обходным пути добавления+ 'cк границам нашегоimpl TraitПолем

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

Мы обследовали ящики на ящиках.

несоответствия с другими частями ржавчины

Текущий дизайн также вводил несоответствия с другими частями ржавчины.

Асинхронизация Fn Desugaring

Ржавчина определяетasync fnкак одоль к нормальномуfnэто возвращается-> impl FutureПолем Поэтому вы можете ожидать, что такая функцияprocess:

async fn process(data: &Data) { .. }

... был бы (грубо) оттолкнут:

fn process(
    data: &Data
) -> impl Future<Output = ()> {
    async move {
        ..
    }
}

На практике, из -за проблем с правилами, вокруг которых можно использовать время жизни, это не фактическое рассуждение. Фактическое освобождение от особого видаimpl TraitЭто разрешено использовать всю жизнь. Но эта формаimpl Traitне подвергался воздействию конечных пользователей.

Черта IMP в чертах

По мере того, как мы преследовали дизайн для признаков IMP на чертах (RFC 3425), мы столкнулись с рядом проблем, связанных с захватом жизни.Чтобы получить симметрию, которые мы хотели работать(например, что можно написать-> impl FutureВ черте и IMP с ожидаемым эффектом) нам пришлось изменить правила, чтобы позволить скрытым типам использоватьвсеОбщие параметры (тип и срок службы) равномерно.

Rust 2024 Дизайн

Приведенные выше проблемы побудили нас принять новый подход в Rust 2024. Подход представляет собой комбинацию двух вещей:

  • Новый дефолт, который скрытые типы для положения возвратаimpl Traitможет использоватьлюбойобщий параметр в области объема, а не только типы (применимо только в Rust 2024);
  • Синтаксис для явного объявления, какие типы могут быть использованы (можно использовать в любом издании).

Новый явный синтаксис называется «Использование связано»:impl Trait + use<'x, T>, например, указывает на то, что скрытый тип разрешено использовать'xиT(но не любые другие общие параметры в области объема).

Время жизни теперь можно использовать по умолчанию

В Rust 2024 по умолчанию является то, что скрытый тип для положения возвратаimpl TraitЗначения используютлюбойобщий параметр, который находится в области, будь то тип или время жизни. Это означает, что первоначальный пример этого сообщения в блоге будет отлично скомпилироваться в Rust 2024 (Попробуйте сами, установив издание на детской площадке до 2024 года):

fn process_data(
    data: &[Datum]
) -> impl Iterator<Item = ProcessedDatum> {
    data
        .iter()
        .map(|datum| datum.process())
}

Ура!

Черты IMM могут включатьuse<>Обязательно точно указать, какие общие типы и время жизни они используют

В качестве побочного эффекта этого изменения, если вы перемещаете код в Rust 2024 вручную (безcargo fix), вы можете начать получать ошибки в абонентах функций сimpl TraitВозврат тип. Это потому, что этиimpl TraitВ настоящее время предполагается, что типы потенциально используют время жизни ввода, а не только типы.

Чтобы контролировать это, вы можете использовать новыйuse<>Связанный синтаксис, который явно заявляет, какие общие параметры могут использоваться скрытым типом. Наш опыт портирования компилятора предполагает, что очень редко нуждаются в изменениях - большая часть кода на самом деле работает лучше с новым дефолтом.

Исключение из вышеупомянутого - когда функция принимает эталонный параметр, который используется только для чтения значений и не включается в возвращаемое значение. Одним из таких примеров является следующая функцияindices(): Это требует кусочка типа&[T]Но единственное, что он делает, это читает длину, которая используется для создания итератора. Сам срез не нужен в возвратном значении:

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> {
    0 .. slice.len()
}

В Rust 2021 это декларация неявно говорит, чтоsliceне используется в типе возврата. Но в Rust 2024 по умолчанию наоборот. Это означает, что подобные вызывающие абоненты перестанут собирать в Rust 2024, поскольку теперь они предполагают, чтоdataзаимствована до завершения итерации:

fn main() {
    let mut data = vec![1, 2, 3];
    let i = indices(&data);
    data.push(4); // <-- Error!
    i.next(); // <-- assumed to access `&data`
}

Это может быть то, что вы хотите! Это означает, что вы можете изменить определениеindices()позже, чтобы это на самом деледелаетвключатьsliceв результате. По словам другого, новый дефолт продолжаетimpl TraitТрадиция удержания гибкости для функции изменить свою реализацию без разбивания абонентов.

Но что, если этонетчто вы хотите? Что если вы хотите гарантировать этоindices()не сохранит ссылку на свой аргументsliceв его возвратной стоимости? Теперь вы делаете это, включивuse<>Связаны в типе возврата, чтобы явно сказать, какие общие параметры могут быть включены в тип возврата.

В случаеindices(), тип возврата фактически используетниктоо дженериках, поэтому мы в идеале писалиuse<>:

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> + use<> {
    //                             -----
    //             Return type does not use `'s` or `T`
    0 .. slice.len()
}

Ограничение реализации.К сожалению, если вы на самом деле попробуете приведенный выше пример сегодня вечером, вы увидите, что он не компилируется (Попробуйте это сами) Это потому, чтоuse<>Границы были реализованы лишь частично: в настоящее время они всегда должны включать в себя, по крайней мере, параметры типа.

Это соответствует ограничениямimpl Traitв более ранних изданиях, которые всегдадолженПараметры типа захвата. В этом случае это означает, что мы можем написать следующее, что также избегает ошибки компиляции, но все еще более консервативно, чем необходимо (Попробуйте сами):

fn indices<T>(
    slice: &[T],
) -> impl Iterator<Item = usize> + use<T> {
    0 .. slice.len()
}

Это ограничение реализации является лишь временным и, надеюсь, скоро будет поднято! Вы можете следовать текущему статусу вОтслеживание выпуска № 130031Полем

Alternative:'staticграницы Для особого случая захватанетссылки вообще, также можно использовать'staticсвязан, как так (Попробуйте сами):

fn indices<'s, T>(
    slice: &'s [T],
) -> impl Iterator<Item = usize> + 'static {
    //                             -------
    //             Return type does not capture references.
    0 .. slice.len()
}

'staticграницы удобны в этом случае, особенно с учетом текущих ограничений реализации вокругuse<>границы, ноuse<>Связывание в целом более гибки, и поэтому мы ожидаем, что они будут использоваться чаще. (Например, компилятор имеет вариантindicesэто возвращает индексы Newtype'dIвместоusizeзначения, и поэтому включаетuse<I>декларация.)

Заключение

Этот пример демонстрирует способ, которым издания могут помочь нам удалить сложность из ржавчины. В Rust 2021 правила по умолчанию для параметров жизни могут использоваться вimpl Traitне очень хорошо. Они часто не выражали то, что нужно пользователям, и приводили к необходимым обходным пути. Они привели к другим несоответствиям, например, между-> impl Futureиasync fn, или между семантикой положения возвратаimpl TraitВ функциях верхнего уровня и функциях признаков.

Благодаря выпускам мы можем рассмотреть это, не нарушая существующего кода. С новыми правилами, поступающими в Rust 2024,

  • Большая часть кода будет «просто работать» в Rust 2024, избегая запутанных ошибок;
  • Для кода, где требуются аннотации, у нас теперь есть более мощный механизм аннотации, который может позволить вам точно сказать, что вам нужно сказать.
  • Точный захват был предложен вRFC #3617, который оставил неразрешенный вопрос, касающийся синтаксиса, и его проблема отслеживания была#123432Полем
  • Нерешенное синтаксическое вопрос был решен вВыпуск № 125836, который представил+ use<>обозначения, используемые в этом посте.
  • Ограничение внедрения отслеживается в#130031Полем

Нико Мацакис от имениЯзыковая команда

Также опубликованоздесь

ФотоАдриен ОликоннаНеспособный


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE