
Детализация изменений в «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Полем
Нико Мацакис от имени
Также опубликованоздесь
ФотоАдриен ОликоннаНеспособный
Оригинал