Выпуск Rust 1.26
Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1.26.0. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода.
Если у вас установлена предыдущая версия Rust с помощью rustup, то для обновления Rust до версии 1.26.0 вам достаточно выполнить:
1 | $ rustup update stable
|
Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта. С подробными примечаниями к выпуску Rust 1.26.0 можно ознакомиться на GitHub.
Что вошло в стабильную версию 1.26.0
Последние несколько выпусков имели ряд относительно небольших улучшений. Тем не менее, мы продолжали работу над многими другими вещами и теперь они начинают выходить в стабильной версии. Версия 1.26, возможно, самая богатая нововведениями со времён выпуска Rust 1.0. Давайте их рассмотрим!
Второе издание книги «Язык программирования Rust»
Почти 18 месяцев Кэрол, Стив и другие работали над полной переработкой книги «Язык программирования Rust». С момента написания первой книги мы узнали много нового о том, как люди изучают Rust, так что новая версия книги теперь лучше во всех отношениях.
Ранее черновик второго издания уже был опубликован на веб-сайте с заявлением о том,
что это незавершённая версия. Теперь же в книгу вносятся небольшие финальные
правки и она готовится к печати. Так что с этого выпуска мы рекомендуем
читать второе издание вместо первого. Вы можете найти его на
doc.rust-lang.org или получить локально,
выполнив rustup doc --book
.
Кстати, о печати: если вам не жалко деревьев, то вы можете заказать бумажную версию книги на NoStarch Press. Содержимое идентично, но вы получите или настоящую физическую копию книги, чтобы поставить её на полку, или отлично свёрстанный PDF. Вся выручка пойдёт на благотворительность.
impl Trait
Наконец-то у нас появился impl Trait
! Эта функциональность уже долгое время была очень
востребована, ибо она обеспечивает возможность, известную как «экзистенциальные типы».
Однако это только звучит страшно, суть идеи проста:
1 2 3 | fn foo() -> impl Trait { // ... } |
Данная сигнатура типа говорит: «foo
— это функция, которая не принимает аргументов
и возвращает тип, реализующий типаж Trait
.» То есть мы не указываем, какой
именно тип возврата у foo
на самом деле, а указываем только то, что он реализует
определённый типаж. Вы можете спросить, чем это отличается от использования
типажей-объектов:
1 2 3 | fn foo() -> Box<Trait> { // ... } |
Это корректный код и такой способ тоже работает, но он хорош не для
всех ситуаций. Допустим, у нас есть типаж Trait
, который реализован
как для i32
, так и для f32
:
1 2 3 4 5 6 7 8 9 10 11 | trait Trait { fn method(&self); } impl Trait for i32 { // тут реализация } impl Trait for f32 { // тут реализация } |
Рассмотрим функцию:
1 2 3 | fn foo() -> ? { 5 } |
Мы хотим указать некий тип результата. Раньше был возможен только вариант с типажом-объектом:
1 2 3 | fn foo() -> Box<Trait> { Box::new(5) as Box<Trait> } |
Но тут используется Box
, что влечёт выделение памяти в куче. На самом деле
мы не хотим возвращать какие-то динамически определяемые данные, поэтому
динамическая диспетчеризация тут только вредит. Вместо этого в Rust 1.26 вы
можете написать так:
1 2 3 | fn foo() -> impl Trait { 5 } |
Это не создаёт типажа-объекта и больше похоже на то, как если бы мы написали
-> i32
, но только с упоминанием части, относящейся к Trait
. Мы получаем
статическую диспетчеризацию, но с возможностью скрыть реальный тип.
Чем это полезно? Одним из хороших применений являются замыкания. Не забывайте,
что замыкания в Rust всегда имеют уникальный, незаписываемый тип, который реализует
типаж Fn
. Это значит, что если ваша функция возвращает замыкание, вы можете
сделать так:
1 2 3 4 5 6 7 8 9 | // было fn foo() -> Box<Fn(i32) -> i32> { Box::new(|x| x + 1) } // стало fn foo() -> impl Fn(i32) -> i32 { |x| x + 1 } |
Никакой упаковки и никакой динамической диспетчеризации. Похожая ситуация возникает и при возврате итераторов. Мало того, что итераторы часто включают замыкания, они ещё могут вкладываться друг в друга, в результате чего получаются довольно глубоко вложенные типы. Например:
1 2 3 4 5 6 | fn foo() { vec![1, 2, 3] .into_iter() .map(|x| x + 1) .filter(|x| x % 2 == 0) } |
при компиляции выдаст ошибку:
1 2 3 4 5 6 7 8 9 10 11 | error[E0308]: mismatched types --> src/main.rs:5:5 | 5 | / vec![1, 2, 3] 6 | | .into_iter() 7 | | .map(|x| x + 1) 8 | | .filter(|x| x % 2 == 0) | |_______________________________^ expected (), found struct `std::iter::Filter` | = note: expected type `()` found type `std::iter::Filter<std::iter::Map<std::vec::IntoIter<{integer}>, [closure@src/main.rs:7:14: 7:23]>, [closure@src/main.rs:8:17: 8:31]>` |
Этот ‘обнаруженный тип’ (‘found type’) — огромный, потому что каждый адаптер в цепочке добавляет новый тип. Кроме того, у нас тут есть ещё и замыкание. Раньше нам приходилось использовать типажи-объекты в подобных случаях, но теперь мы можем просто написать
1 2 3 4 5 6 | fn foo() -> impl Iterator<Item = i32> { vec![1, 2, 3] .into_iter() .map(|x| x + 1) .filter(|x| x % 2 == 0) } |
и дело сделано. Работать с futures можно так же.
Важно отметить, что иногда типажи-объекты все же нужны. Вы можете
использовать impl Trait
только если ваша функция возвращает один тип;
если вы хотите вернуть несколько, то вам потребуется динамическая
диспетчеризация. Например:
1 2 3 4 5 6 7 8 9 10 11 | fn foo(x: i32) -> Box<Iterator<Item = i32>> { let iter = vec![1, 2, 3] .into_iter() .map(|x| x + 1); if x % 2 == 0 { Box::new(iter.filter(|x| x % 2 == 0)) } else { Box::new(iter) } } |
Здесь итератор фильтра может быть возвращён, а может и нет. Есть два разных типа, которые могут быть возвращены, и поэтому мы должны использовать типаж-объект.
И напоследок: для синтаксической симметрии вы можете использовать
impl Trait
также и в аргументах. То есть:
1 2 3 4 5 | // было fn foo<T: Trait>(x: T) { // стало fn foo(x: impl Trait) { |
может улучшить вид коротких сигнатур.
Примечание для тех, кто разбирается в теории типов: тут не экзистенциальный, а универсальный тип. Другими словами,
impl Trait
— универсальный на входе в функцию, но экзистенциальный на выходе.
Улучшены сопоставления в match
Вы когда-нибудь пытались использовать match
для ссылки на Option
? Например,
в подобном коде:
1 2 3 4 5 6 | fn hello(arg: &Option<String>) { match arg { Some(name) => println!("Hello {}!", name), None => println!("I don't know who you are."), } } |
Если вы попытаетесь его скомпилировать в Rust 1.25, то вы получите такую ошибку:
1 2 3 4 5 6 7 8 9 10 11 | error[E0658]: non-reference pattern used to match a reference (see issue #42640) --> src/main.rs:6:9 | 6 | Some(name) => println!("Hello {}!", name), | ^^^^^^^^^^ help: consider using a reference: `&Some(name)` error[E0658]: non-reference pattern used to match a reference (see issue #42640) --> src/main.rs:7:9 | 7 | None => println!("I don't know who you are."), | ^^^^ help: consider using a reference: `&None` |
Да, конечно. Давайте изменим код:
1 2 3 4 5 6 | fn hello(arg: &Option<String>) { match arg { &Some(name) => println!("Hello {}!", name), &None => println!("I don't know who you are."), } } |
Мы добавили &
, как требовал компилятор. Попробуем скомпилировать снова:
1 2 3 4 5 6 7 8 | error[E0507]: cannot move out of borrowed content --> src/main.rs:6:9 | 6 | &Some(name) => println!("Hello {}!", name), | ^^^^^^----^ | | | | | hint: to prevent move, use `ref name` or `ref mut name` | cannot move out of borrowed content |
Да, конечно. Давайте усмирим-таки компилятор, последовав его совету:
1 2 3 4 5 6 | fn hello(arg: &Option<String>) { match arg { &Some(ref name) => println!("Hello {}!", name), &None => println!("I don't know who you are."), } } |
Теперь компиляция пройдёт успешно. Нам пришлось добавить два &
и один ref
.
Но что особенно важно, ничто из этого не было по-настоящему полезным нам,
как программистам. Конечно, сначала мы забыли &
, но имеет ли это значение? Нам
потребовалось добавить ref
чтобы получить ссылку на значение, сохранённое внутри
Option
, но мы и не могли сделать ничего другого, кроме как получить ссылку, так
как мы не можем переместить значение за &T
.
Итак, начиная с Rust 1.26, первоначальный код без &
и ref
будет просто
компилироваться и делать именно то, что вы ожидаете. Короче говоря, компилятор
будет автоматически ссылаться или разыменовывать ссылки в конструкции match
.
Поэтому, когда мы говорим
1 2 | match arg { Some(name) => println!("Hello {}!", name), |
компилятор автоматически обратится к Some
по ссылке, и поскольку это будет
заимствование, name
свяжется со значением как ref name
, тоже автоматически.
Если бы мы изменяли значение:
1 2 3 4 5 6 | fn hello(arg: &mut Option<String>) { match arg { Some(name) => name.push_str(", world"), None => (), } } |
компилятор бы автоматически выполнил изменяемое заимствование, и name
оказалось бы связано со значением как ref mut
.
Мы думаем, что это избавит от особо болезненного рутинного кода как новичков, так и опытных разработчиков. Компилятор просто возьмёт на себя эту работу, больше не требуя писать подобный рутинный код.
main
может возвращать Result
К слову о досадном рутинном коде: поскольку Rust использует тип
Result
для возврата ошибок и ?
для упрощения их обработки,
общей болевой точкой новичков в Rust становится попытка использовать
?
в main
:
1 2 3 4 5 | use std::fs::File; fn main() { let f = File::open("bar.txt")?; } |
Это порождает ошибку вроде «error[E0277]: the ?
operator can only be used
in a function that returns Result
». Которая многих людей вынуждает писать
подобный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 | fn run(config: Config) -> Result<(), Box<Error>> { // ... } fn main() { // ... if let Err(e) = run(config) { println!("Application error: {}", e); process::exit(1); } } |
Наша функция run
содержит всю реальную логику, а main
вызывает run
, проверяет, произошла ли ошибка и
завершает работу. Нам нужна эта вторая функция только
потому, что main
не может вернуть Result
, но мы бы
хотели использовать ?
в своей логике.
В Rust 1.26 вы теперь можете объявить main
, который возвращает Result
:
1 2 3 4 5 6 7 | use std::fs::File; fn main() -> Result<(), std::io::Error> { let f = File::open("bar.txt")?; Ok(()) } |
Теперь это работает как надо! Если main
вернёт ошибку, это
приведёт к завершению с кодом ошибки и печати отладочной информации
об ошибке.
Замкнутые диапазоны с ..=
Ещё задолго до Rust 1.0, вы могли создавать полуоткрытые диапазоны с ..
, например:
1 2 3 | for i in 1..3 { println!("i: {}", i); } |
Этот код напечатает i: 1
, а затем i: 2
. В Rust 1.26 теперь вы можете
создать замкнутый диапазон, например:
1 2 3 | for i in 1..=3 { println!("i: {}", i); } |
Этот код напечатает i: 1
, затем i: 2
, как предыдущий, но также и i: 3
;
три — тоже включится в диапазон. Замкнутые диапазоны особенно полезны для
перебора всех возможных значений. Например, вот удивительная программа на Rust:
1 2 3 4 5 6 7 8 9 10 | fn takes_u8(x: u8) { // ... } fn main() { for i in 0..256 { println!("i: {}", i); takes_u8(i); } } |
Что делает эта программа? Ответ: ничего. Предупреждение, которое мы получаем при компиляции, подсказывает почему:
1 2 3 4 5 6 7 | warning: literal out of range for u8 --> src/main.rs:6:17 | 6 | for i in 0..256 { | ^^^ | = note: #[warn(overflowing_literals)] on by default |
Это правильно, так как i
типа u8
, который переполняется, и это то же самое,
что писать for i in 0..0
, поэтому цикл выполняется ноль раз.
Однако с замкнутыми диапазонами можно это исправить:
1 2 3 4 5 6 7 8 9 10 | fn takes_u8(x: u8) { // ... } fn main() { for i in 0..=255 { println!("i: {}", i); takes_u8(i); } } |
Этот код выведет 256 строк, которые вы ожидали.
Базовые образцы срезов
Ещё одно долгожданное нововведение — «образцы срезов» (slice patterns). Оно позволяет вам сопоставлять с образцом срезы подобно тому, как вы сопоставляете с образцом другие типы данных. Например:
1 2 3 4 5 6 | let arr = [1, 2, 3]; match arr { [1, _, _] => "начинается с единицы", [a, b, c] => "начинается с чего-то другого", } |
В данном случае мы знаем, что arr
имеет длину три, и поэтому нам нужны
три элемента внутри []
. Мы также можем сопоставлять, когда мы не знаем длину:
1 2 3 4 5 6 7 | fn foo(s: &[u8]) { match s { [a, b] => (), [a, b, c] => (), _ => (), } } |
Здесь мы не знаем, какой длины s
, поэтому мы можем написать первые два образца,
каждый из которых рассчитан на разную длину. Также нам обязательно нужен вариант
_
, поскольку мы не покрываем все возможные случаи длины, но мы и не можем этого!
Увеличение скорости
Мы продолжаем улучшать скоростью работы компилятора. Мы обнаружили, что глубоко вложенные типы в некоторых случаях становились нелинейными, что [было исправлено] (https://github.com/rust-lang/rust/pull/48296). После этого исправления, вместе с которым было выпущено и много других небольших исправлений, мы наблюдали сокращение времени компиляции вплоть до 12%. В будущем улучшим ещё!
128-разрядные целые числа
И наконец, одно очень простое улучшение: теперь у Rust’а есть 128-разрядные целые числа!
1 2 | let x: i128 = 0; let y: u128 = 0; |
Они по размеру в два раза больше u64
и поэтому могут содержать большие значения.
А именно:
u128
: 0 — 340,282,366,920,938,463,463,374,607,431,768,211,455i128
: −170,141,183,460,469,231,731,687,303,715,884,105,728 — 170,141,183,460,469,231,731,687,303,715,884,105,727
Фух!
Подробности смотрите в примечаниях к выпуску.
Стабилизация библиотек
Мы стабилизировали fs::read_to_string
,
который удобнее, чем File::open
и io::Read::read_to_string
для простого чтения в память
всего файла сразу:
1 2 3 4 | use std::fs; use std::net::SocketAddr; let foo: SocketAddr = fs::read_to_string("address.txt")?.parse()?; |
Теперь вы можете форматировать вывод шестнадцатеричных
чисел с Debug
:
1 | assert!(format!("{:02x?}", b"Foo\0") == "[46, 6f, 6f, 00]") |
Завершающие запятые теперь поддерживаются всеми макросами в стандартной библиотеке.
Подробности смотрите в примечаниях к выпуску.
Улучшения в Cargo
В этом выпуске Cargo не получил значительных изменений функциональности, но
получил ряд улучшений стабильности и производительности. Cargo теперь должен
обрабатывать зависимости из lock-файлов ещё быстрее и интеллектуальнее, а также
требовать меньше ручных вызовов cargo update
. Исполняемый файл Cargo теперь
имеет ту же версию, что и rustc
.
Подробности смотрите в примечаниях к выпуску.
Разработчики 1.26.0
Множество людей участвовало в разработке Rust 1.26. Мы не смогли бы завершить работу без участия каждого из вас.