Команда Rust рада сообщить о выпуске новой версии — 1.51.0. Rust — это язык программирования, позволяющий каждому создавать надёжное и эффективное программное обеспечение.
Если вы установили предыдущую версию Rust средствами rustup
, то для обновления до версии 1.51.0 вам достаточно выполнить следующую команду:
rustup update stable
Если у вас ещё не установлен rustup
, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.
Что было стабилизировано в 1.51.0
Этот выпуск представляет одно из наиболее крупных дополнений языка Rust и Cargo за долгое время, включающее в себя стабилизацию константных обобщений в минимально полезном варианте и новый распознаватель функциональности в Cargo. Давайте посмотрим подробнее!
Константные обобщения (Const Generics MVP)
До этого выпуска Rust позволял параметризовать типы по времени жизни или по типам. Например, если бы мы хотели получить struct
, которая является обобщённой для массива, мы бы написали следующее:
struct FixedArray<T> {
// ^^^ Определение обобщённого типа.
list: [T; 32]
// ^ Где мы использовали его.
}
Если затем мы используем FixedArray<u8>
, компилятор создаст мономорфизированную версию FixedArray
, которая выглядит так:
struct FixedArray<u8> {
list: [u8; 32]
}
Этот полезный функционал позволяет писать повторно используемый код без дополнительных затрат во время выполнения. Однако до этого выпуска у нас не было возможности легко объединять значения таких типов. Это наиболее заметно в массивах, где длина указывается в определении типа ([T; N]
). Теперь в версии 1.51.0 вы можете писать код, который будет обобщённым для значений любого числа, типа bool
или char
! (Использование значений struct
и enum
по-прежнему не стабилизировано.)
Это изменение позволяет создать массив, обобщённый по типу и длине. Давайте посмотрим на пример определения и то, как его можно использовать.
struct Array<T, const LENGTH: usize> {
// ^^^^^^^^^^^^^^^^^^^ Определение константного обобщения.
list: [T; LENGTH]
// ^^^^^^ Мы использовали его здесь.
}
Теперь если мы используем Array<u8, 32>
, компилятор создаст мономорфизированную версию Array
, которая выглядит так:
struct Array<u8, 32> {
list: [u8; 32]
}
Константные обобщения добавляют важный новый инструмент для разработчиков библиотек, чтобы создавать новые, мощные и безопасных API во время компиляции. Если вы хотите узнать больше о константных обобщениях, можете почитать статью в блоге «Const Generics MVP Hits Beta» для получения дополнительной информации об этой функции и её текущих ограничениях. Нам не терпится увидеть, какие новые библиотеки и API вы создадите!
Стабилизация array::IntoIter
Как часть стабилизации константных обобщений, мы также стабилизировали использующее их новое API — std::array::IntoIter
. IntoIter
позволяет вам создать поверх массива итератор по значению. Ранее не было удобного способа итерироваться по самим значениям, только по ссылкам.
fn main() {
let array = [1, 2, 3, 4, 5];
// Раньше
for item in array.iter().copied() {
println!("{}", item);
}
// Теперь
for item in std::array::IntoIter::new(array) {
println!("{}", item);
}
}
Обратите внимание, что это было добавлено вместо .into_iter()
как отдельный метод, так как сейчас оно ломает текущее соглашение о том, что .into_iter()
относится к срезам по ссылочному итератору. Мы изучаем возможности в будущем сделать такой код более эргономичным.
Новый распознаватель функциональности Cargo
Управление зависимостями — сложная задача, и одна из самых сложных её частей — выбор версии зависимости, когда от неё зависят два разных пакета. Здесь учитывается не только номер версии, но и то, какая функциональность была включена или выключена для пакета. По умолчанию Cargo объединяет функциональные флаги (features) для одного пакета, если он встречается в графе зависимостей несколько раз.
Например, у вас есть зависимость foo
с функциональными флагами A и B, которые используются пакетами bar
and baz
, но bar
зависит от foo+A
, а baz
— от foo+B
. Cargo объединит оба флага и соберёт foo
как foo+AB
. Выгода здесь в том, что foo
будет собран только один раз и далее будет использован и для bar
, и для baz
.
Но у этого решения есть и обратная сторона. Что, если функциональный флаг подключён как build-зависимость, но не совместим с конечной целью сборки?
Общим примером этого из экосистемы может служить опциональная функциональность std
во многих #![no_std]
пакетах, которая позволяет этим пакетам предоставить дополнительную функциональность, если она включена. Теперь представим, что вы хотите использовать #![no_std]
версию foo
в вашей #![no_std]
программе и использовать foo
во время сборки в build.rs
. Так как во время сборки вы зависите от foo+std
, то и ваша программа тоже зависит от foo+std
, а значит более не может быть скомпилирована, так как std
не доступна для вашей целевой платформы.
Это была давняя проблема в Cargo, и с этим выпуском появилась новая опция resolver
в вашем Cargo.toml
, где вы можете установить resolver="2"
, чтобы попробовать новый подход к разрешению функциональных флагов. Вы можете ознакомиться с RFC 2957 для получения подробного описания поведения, которое можно резюмировать следующим образом.
- Dev dependencies — когда пакет используется совместно как обычная зависимость и dev, возможности dev-зависимости включаются только в том случае, если текущая сборка включает dev-зависимости.
- Host Dependencies — когда пакет совместно используется как обычная зависимость и зависимость сборки или процедурный макрос, features для нормальной зависимости сохраняются независимо от зависимости сборки или процедурного макроса.
- Target dependencies — когда у пакета включены зависимые от платформы features, и он присутствует в графе сборки несколько раз, будут включены только features, подходящие текущей платформе сборки.
Хотя это может привести к компиляции некоторых пакетов более одного раза, это должно обеспечить гораздо более интуитивный опыт разработки при использовании функций с Cargo. Если вы хотите узнать больше, вы также можете прочитать раздел «Feature Resolver» в Cargo Book для получения дополнительной информации. Мы хотели бы поблагодарить команду Cargo и всех участников за их тяжёлую работу по разработке и внедрению нового механизма!
[package]
resolver = "2"
# Или если вы используете workspace
[workspace]
resolver = "2"
Разделение отладочной информации
Хоть это и нечасто освещается в релизах, команда Rust постоянно работает над сокращением времени компиляции. В этом выпуске вносится самое крупное улучшение за долгое время для Rust на macOS. Отладочная информация исходного кода содержится в собранном бинарнике, и за счет этого программа может дать больше информации о том, что происходит во время исполнения. Раньше в macOS отладочная информация собиралась в единую директорию .dSYM
при помощи утилиты dsymutil
, что могло занимать много времени и дискового пространства.
Сбор всей отладочной информации в эту директорию помогал найти её во время выполнения, особенно если бинарник перемещался. Однако у такого решения есть и обратная сторона: если вы сделали небольшое изменение в вашей программе, то dsymutil
необходимо запустить над всем собранным бинарником, чтобы собрать директорию .dSYM
. Иногда это могло сильно увеличить время сборки, особенно для крупных проектов, поскольку надо перебирать все зависимости, но это важный шаг, без которого стандартная библиотека Rust не знает, как загружать отладочную информацию на macOS.
Недавно обратная трассировка в Rust была переключена на другой бэкенд, который поддерживает загрузку отладочной информации без запуска dsymutil
. Она была стабилизирована, чтобы убрать запуск dsymutil
. Это может значительно ускорить сборки, которые включают отладочную информацию, и уменьшить размер занимаемого пространства. Мы не проводили обширных тестов, но у нас есть множество отчётов сборок, которые стали с таким поведением на macOS намного быстрее.
Вы можете включить новое поведение, установив флаг -Csplit-debuginfo=unpacked
при запуске rustc
или задав опцию split-debuginfo
в unpacked
раздела [profile]
в Cargo. С опцией "unpacked" rustc будет оставлять объектные файлы (.o
) в директории сборки вместо их удаления и пропустит запуск dsymutil
. Поддержка бэктрейсов Rust достаточно умна, чтобы понять, как найти эти .o
файлы. Такие инструменты, как lldb, также знают, как это делается. Это должно работать до тех пор, пока вам не понадобится переместить бинарники в другое место и сохранить отладочную информацию.
[profile.dev]
split-debuginfo = "unpacked"
Стабилизированные API
Итого: в этом выпуске было стабилизировано 18 новых методов для разных типов, например slice
и Peekable
. Одним из примечательных дополнений является стабилизация ptr::addr_of!
и ptr::addr_of_mut!
, которая позволяет вам создавать сырые указатели для полей без выравнивания. Ранее это было невозможно, так как Rust требовал, чтобы &/&mut
были выровнены и указывали на инициализированные данные. Из-за этого преобразование &addr as *const _
приводило к неопределённому поведению, так как &addr
должно быть выровнено. Теперь эти два макроса позволяют вам безопасно создать невыровненные указатели.
use std::ptr;
#[repr(packed)]
struct Packed {
f1: u8,
f2: u16,
}
let packed = Packed { f1: 1, f2: 2 };
// `&packed.f2` будет создана ссылка на невыровненную память, таким образом это неопределённое поведение!
let raw_f2 = ptr::addr_of!(packed.f2);
assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);
Следующие методы были стабилизированы:
Arc::decrement_strong_count
Arc::increment_strong_count
Once::call_once_force
Peekable::next_if_eq
Peekable::next_if
Seek::stream_position
array::IntoIter
panic::panic_any
ptr::addr_of!
ptr::addr_of_mut!
slice::fill_with
slice::split_inclusive_mut
slice::split_inclusive
slice::strip_prefix
slice::strip_suffix
str::split_inclusive
sync::OnceState
task::Wake
Другие изменения
Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.
Участники 1.51.0
Множество людей собрались вместе, чтобы создать Rust 1.51.0. Мы не смогли бы сделать это без всех вас. Спасибо!
От переводчиков
С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.
Данную статью совместными усилиями перевели andreevlex, TelegaOvoshey, blandger, nlinker и funkill.
snuk182
Еее, константные дженерики! Теперь можно не колхозить доступ к многомерным массивам.
tbl
дженерики становятся все мощнее, следующий шаг — HKT?
snuk182
Я проголосую за поля в типажах. Это уберет мне большую часть ансейфов в проекте.
red75prim
Хм. А для чего там unsafe? Это же можно реализовать с помощью методов get_field(&self) -> &Field, get_field_mut(&mut self) -> &mut Field.
Для того, чтобы заимствовать отдельные поля без блокировки всего объекта? По-моему, с текущей моделью stacked borrows это — UB. MIRI на такой код не ругается?
snuk182
Для наследования. В данный момент оно реализовано вложенными структурами, как в си.
AnthonyMikh
Вы так и не сказали, для чего вам наследование и чем не подходят геттеры-сеттеры
snuk182
Мне нужен вид зависимости сущности "is-a", вместо "has-a". Тогда я могу обобщать сущности разных конечных типов в контейнеры. При этом мне нужно хранить состояние, общее для всех типов, а таскать его по конечным типам, вызывая через геттер в типаже, можно, но костыльно.