Мы рады объявить третью редакцию языка Rust — Rust 2021, которая выйдет в октябре. Rust 2021 содержит несколько небольших изменений, которые, тем не менее, значительно улучшат удобство использования Rust.


Что такое Редакция?


Релиз Rust 1.0 установил "стабильность без застоя" как основное правило Rust. Начиная с релиза 1.0, это правило звучало так: выпустив функцию в стабильной версии, мы обязуемся поддерживать её во всех будущих выпусках.


Однако есть случаи, когда возможность вносить небольшие изменения в язык бывает полезной — даже если у них нет обратной совместимости. Самый очевидный пример — введение нового ключевого слова, которое делает недействительными переменные с тем же именем. Например, в первой версии Rust не было ключевых слов async и await. Внезапное изменение этих слов на ключевые слова в более поздних версиях привело бы к тому, что, например код let async = 1; перестал работать.


Редакции — механизм, который мы используем для решения этой проблемы. Когда мы хотим выпустить функцию без обратной совместимости, мы делаем её частью новой редакции Rust. Редакции опциональны и должны прописываться явно, поэтому существующие пакеты не видят эти изменения, пока явно не перейдут на новую версию. Это означает, что даже последняя версия Rust по-прежнему не будет рассматривать async как ключевое слово, если не будет выбрана версия 2018 или более поздняя. Этот выбор делается для каждого пакета как части Cargo.toml. Новые пакеты, созданные cargo new, всегда настроены на использование последней стабильной редакции.


Редакции не разделяют экосистему


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


Требование функциональной совместимости подразумевает ограничения на типы изменений, которые мы можем внести в редакцию. В целом изменения, которые происходят в редакции, имеют тенденцию быть "поверхностными". Весь код Rust, независимо от редакции, компилируется в одно и то же внутреннее представление в компиляторе.


Миграция редакций проста и в значительной степени автоматизирована


Наша цель — упростить обновление пакетов. Когда мы выпускаем новую редакцию, мы также предоставляем инструменты для автоматизации миграции на неё. Они вносят незначительные изменения в код, необходимые для его совместимости. Например, при переходе на Rust 2018 они изменяют всё, что называлось async, чтобы использовать эквивалентный синтаксис необработанного идентификатора: r#async.


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


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


Какие изменения запланированы в Rust 2021?


За последние несколько месяцев рабочая группа Rust 2021 рассмотрела ряд предложений, что именно включить в новую редакцию. Мы рады объявить окончательный список изменений. Чтобы попасть в этот список, каждая функция должна соответствовать двум критериям. Во-первых, она должна быть одобрена соответствующей командой(ами) Rust. Во-вторых, для её реализации должно быть достаточно времени, чтобы мы могли быть уверены в том, что все этапы будут завершены вовремя.


Дополнения к прелюдии


Прелюдия стандартной библиотеки — это модуль, содержащий всё, что автоматически импортируется в каждый модуль. Она содержит часто используемые элементы, такие как Option, Vec, drop и Clone.


Компилятор Rust отдаёт приоритет любым элементам, импортированным вручную, перед элементами из прелюдии. Так мы уверены, что дополнения к прелюдии не нарушают какой-либо существующий код. Например, если у вас есть пакет или модуль с именем example, содержащий pub struct Option;, то use example::*; заставит Option однозначно ссылаться на example не из стандартной библиотеки.


Однако добавление типажа к прелюдии может незаметно сломать существующий код. Вызов x.try_into() с использованием MyTryInto может стать неоднозначным и не скомпилироваться, если TryInto из std также импортирован, поскольку он предоставляет метод с тем же именем. По этой причине мы пока не добавили TryInto в прелюдию — потому что много кода может сломаться.


В качестве решения Rust 2021 будет использовать новую прелюдию. Она идентична текущей, за исключением трёх новых дополнений:



Распознаватель функций Cargo по умолчанию


Начиная с Rust 1.51.0, Cargo поддерживает новый распознаватель функций который можно активировать с помощью resolver = "2" в Cargo.toml.


В Rust 2021 и всех дальнейших выпусках это будет значением по умолчанию. То есть запись edition = "2021" в Cargo.toml будет означать resolver = "2".


Новый распознаватель функций больше не объединяет все запрошенные функции для пакетов, от которых есть несколько зависимостей. Подробности смотрите в анонсе Rust 1.51.


IntoIterator для массивов


До Rust 1.53 только ссылки на массивы реализовывали IntoIterator. Иными словами, вы могли выполнять итерацию по &[1, 2, 3] и &mut [1, 2, 3], но не по [1, 2, 3] напрямую.


for &e in &[1, 2, 3] {} // Ok :)

for e in [1, 2, 3] {} // Ошибка :(

Это давняя проблема, но решение не такое простое, как кажется. Просто добавление реализации типажа нарушит существующий код. Сейчас array.into_iter() компилируется, потому что он неявно вызывает (&array).into_iter() из-за особенностей синтаксиса вызова метода. Добавление реализации типажа изменит смысл.


Обычно мы классифицируем этот тип изменений (добавление реализации типажа) как "незначительный" и приемлемый. Но в этом случае оно может сломать слишком много кода.


Много раз предлагалось "реализовать IntoIterator только для массивов в Rust 2021". Однако это попросту невозможно: у вас не может быть реализации типажа в одной редакции и не быть в другой, поскольку редакции могут быть смешанными.


Вместо этого мы решили добавить реализацию типажа во все редакции, начиная с Rust 1.53.0. В этом нам помог небольшой приём, чтобы избежать значительных изменений вплоть до Rust 2021. В коде Rust 2015 и 2018 компилятор по-прежнему будет преобразовывать array.into_iter() в (&array).into_iter(), будто реализации типажа не существует. Это относится только к синтаксису вызова метода .into_iter() и не влияет на другие синтаксисы, такие как for e in [1, 2, 3], iter.zip([1, 2, 3]). Они начнут работать во всех редакциях.


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


Непересекающийся захват в замыканиях


Замыкания автоматически захватывают все, на что вы ссылаетесь из их тела. Например, || a + 1 автоматически захватывает ссылку на a из окружающего контекста.


В настоящее время это относится ко всем структурам, даже если используется только одно поле. К примеру, || a.x + 1 заимствует ссылку на a а не только на a.x. В некоторых ситуациях это будет проблемой. Когда поле структуры уже заимствовано (изменяемо) или перемещено из неё, другие поля больше не могут использоваться в замыкании, так как замыкание будет пытаться захватить всю структуру, которая больше не доступна.


let a = SomeStruct::new();

drop(a.x); // удаляем одно поле структуры

println!("{}", a.y); // Окей: до сих пор используем только поле структуры

let c = || println!("{}", a.y); // Ошибка: попытка захвата всей структуры `a`
c();

Начиная с Rust 2021, замыкания будут захватывать только те поля, которые они используют. Приведённый выше пример отлично скомпилируется в Rust 2021.


Это поведение активируется только в новой редакции, поскольку оно может изменить порядок, в котором удаляются поля. Что же касается всех изменений редакций — для них доступна автоматическая миграция. Она обновит ваши замыкания, для которых это будет необходимо. Она может вставлять let _ = &a; внутрь замыкания, чтобы захватить всю структуру, как раньше.


Согласованность макросов паники


panic!() — один из самых известных макросов Rust. Однако в нем есть несколько тонких моментов, которые мы не можем просто взять и изменить — снова из-за обратной совместимости.


panic!("{}", 1); // Окей, паника с сообщением "1"
panic!("{}"); // Окей, паника без сообщения "{}"

Макрос panic!() использует форматирование строки только тогда, когда вызывается с более чем одним аргументом. С одним аргументом он на него просто не смотрит.


let a = "{";
println!(a); // Ошибка: первый аргумент должен быть форматной строкой
panic!(a); // Хорошо: Макрос panic не обрабатывает аргумент

(Он даже принимает нестроковые аргументы, такие как panic!(123), что, впрочем, редко бывает полезно).


Особенно это будет проблемой после стабилизации неявных аргументов форматной строки. Эта функция сделает println!("hello {name}") сокращением для println!("hello {}", name). Однако panic!("hello {name}") не будет работать должным образом, поскольку panic!() не обрабатывает единственный аргумент как строку.


Чтобы выйти из этой запутанной ситуации, в Rust 2021 есть более последовательный макрос — panic!(). Новый panic!() больше не принимает произвольные выражения в качестве единственного аргумента. Он — как и println!() — всегда обрабатывает первый аргумент как строку. Поскольку panic!() больше не будет принимать произвольные аргументы, panic_any() будет единственным способом вызвать панику с чем-то кроме форматированной строки.


Кроме того, core::panic!() и std::panic!() будут идентичны в Rust 2021. В настоящее время между ними есть некоторые исторические различия, которые могут быть заметны при включении или выключении #![no_std] .


Зарезервированный синтаксис


Чтобы освободить место для будущих изменений, мы решили зарезервировать синтаксис префиксных идентификаторов и литералов: prefix#identifier, prefix"string", prefix'c' и prefix#123, где prefix может быть любым идентификатором (за исключением тех, что уже имеют значение — например, b'...' и r"...").


Это критическое изменение, поскольку в настоящее время макросы могут принимать hello"world", которое они будут видеть как два отдельных токена: hello и "world". Хотя исправление (автоматическое) очень простое — просто вставьте пробел: hello "world".


Помимо превращения в ошибку токенизации, RFC пока не придаёт значения никакому префиксу. Присвоение значения конкретным префиксам остаётся на усмотрение будущих предложений, которые — благодаря резервированию этих префиксов сейчас — не повлекут за собой критических изменений.


Вот некоторые новые префиксы, которые вы увидите в будущем:


  • f"" как сокращение от форматной строки. Например, f"hello {name}" как сокращение для эквивалентного format_args!().
  • c"" или z"" для С-строк с завершающим нулём.
  • k#keyword, позволяющее писать ключевые слова, которых ещё нет в текущей редакции. Например, хотя async и не является ключевым словом в версии 2015, этот префикс позволил бы нам принять k#async в редакции 2015, не дожидаясь выхода редакции 2018, чтобы зарезервировать async в качестве ключевого слова.

Повышение двух предупреждений до серьёзных ошибок


Две существующие статические проверки станут серьёзными ошибками в Rust 2021. В старых версиях они останутся предупреждениями.



Шаблоны "или" в macro_rules


Начиная с Rust 1.53.0 паттерны расширены для поддержки |, которые могут быть вложены в шаблоне где угодно. Это позволяет писать Some(1 | 2) вместо Some(1) | Some(2). Поскольку раньше это было просто недопустимо, это не критическое изменение.


Однако это изменение также влияет на макросы macro_rules. Такие макросы могут принимать шаблоны с использованием спецификатора фрагмента :pat. В настоящее время :pat не соответствует |, поскольку до Rust 1.53 не все шаблоны (на всех вложенных уровнях) могли содержать |. Макросы, которые принимают шаблоны типа A | B, такие как matches!(), используют что-то вроде $($_:pat)|+. Поскольку мы не хотим нарушать макросы, которые уже существуют, мы не изменили значение :pat в Rust 1.53.0, чтобы включить |.


Вместо этого мы внесём это изменение как часть Rust 2021. В новой редакции спецификатор фрагмента :pat будет соответствовать A | B


Поскольку бывают случаи, когда кто-то всё ещё желает сопоставить один вариант шаблона без |, мы добавили спецификатор фрагмента :pat_param для сохранения старого поведения. Название относится к его основному варианту использования: шаблон в параметре замыкания.


Что будет дальше?


Мы планируем объединить и полностью протестировать эти изменения к сентябрю, чтобы убедиться в том, что редакция 2021 года войдёт в Rust версии 1.56.0. Затем Rust 1.56.0 будет находиться в стадии бета-тестирования в течение шести недель, после чего он будет выпущен как стабильный релиз 21 октября.


Однако обратите внимание, что Rust — это проект, движимый волонтёрами. Мы ставим личное благополучие каждого, кто работает над Rust, выше любых сроков и ожиданий, которые мы могли бы установить. Это может означать задержку выпуска версии, если это будет необходимо, или отказ от функции, которая оказывается слишком сложной для завершения к релизу.


Тем не менее, мы идём по графику, и многие сложные проблемы уже решены. Спасибо всем, кто внёс свой вклад в Rust 2021!




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


Мы будем публиковать более подробную информацию о процессе и об отклонённых предложениях в блоге "Inside Rust".


Если вы не можете ждать, то многие фичи уже доступны в Nightly, просто добавьте флаги -Zunstable-options --edition=2021.




От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели fan-tom, blandger, Belanchuk, TelegaOvoshey и andreevlex.