Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1.26.0. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода.
Если у вас установлена предыдущая версия Rust с помощью rustup, то для обновления Rust до версии 1.26.0 вам достаточно выполнить:
$ 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
! Эта функциональность уже долгое время была очень востребована, ибо она обеспечивает возможность, известную как "экзистенциальные типы". Однако это только звучит страшно, суть идеи проста:
fn foo() -> impl Trait {
// ...
}
Данная сигнатура типа говорит: "foo
— это функция, которая не принимает аргументов и возвращает тип, реализующий типаж Trait
." То есть мы не указываем, какой именно тип возврата у foo
на самом деле, а указываем только то, что он реализует определенный типаж. Вы можете спросить, чем это отличается от использования типажей-объектов:
fn foo() -> Box<Trait> {
// ...
}
Это корректный код и такой способ тоже работает, но он хорош не для всех ситуаций. Допустим, у нас есть типаж Trait
, который реализован как для i32
, так и для f32
:
trait Trait {
fn method(&self);
}
impl Trait for i32 {
// тут реализация
}
impl Trait for f32 {
// тут реализация
}
Рассмотрим функцию:
fn foo() -> ? {
5
}
Мы хотим указать некий тип результата. Раньше был возможен только вариант с типажом-объектом:
fn foo() -> Box<Trait> {
Box::new(5) as Box<Trait>
}
Но тут используется Box
, что влечет выделение памяти в куче. На самом деле мы не хотим возвращать какие-то динамически определяемые данные, поэтому динамическая диспетчеризация тут только вредит. Вместо этого в Rust 1.26 вы можете написать так:
fn foo() -> impl Trait {
5
}
Это не создает типажа-объекта и больше похоже на то, как если бы мы написали -> i32
, но только с упоминанием части, относящейся к Trait
. Мы получаем статическую диспетчеризацию, но с возможностью скрыть реальный тип.
Чем это полезно? Одним из хороших применений являются замыкания. Не забывайте, что замыкания в Rust всегда имеют уникальный, незаписываемый тип, который реализует типаж Fn
. Это значит, что если ваша функция возвращает замыкание, вы можете сделать так:
// было
fn foo() -> Box<Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
// стало
fn foo() -> impl Fn(i32) -> i32 {
|x| x + 1
}
Никакой упаковки и никакой динамической диспетчеризации. Похожая ситуация возникает и при возврате итераторов. Мало того, что итераторы часто включают замыкания, они еще могут вкладываться друг в друга, в результате чего получаются довольно глубоко вложенные типы. Например:
fn foo() {
vec![1, 2, 3]
.into_iter()
.map(|x| x + 1)
.filter(|x| x % 2 == 0)
}
при компиляции выдаст ошибку:
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') — огромный, потому что каждый адаптер в цепочке добавляет новый тип. Кроме того, у нас тут есть еще и замыкание. Раньше нам приходилось использовать типажи-объекты в подобных случаях, но теперь мы можем просто написать
fn foo() -> impl Iterator<Item = i32> {
vec![1, 2, 3]
.into_iter()
.map(|x| x + 1)
.filter(|x| x % 2 == 0)
}
и дело сделано. Работать с futures можно так же.
Важно отметить, что иногда типажи-объекты все же нужны. Вы можете использовать impl Trait
только если ваша функция возвращает один тип; если вы хотите вернуть несколько, то вам потребуется динамическая диспетчеризация. Например:
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
также и в аргументах. То есть:
// было
fn foo<T: Trait>(x: T) {
// стало
fn foo(x: impl Trait) {
может улучшить вид коротких сигнатур.
Примечание для тех, кто разбирается в теории типов: тут не экзистенциальный, а универсальный тип. Другими словами,
impl Trait
— универсальный на входе в функцию, но экзистенциальный на выходе.
Улучшены сопоставления в match
Вы когда-нибудь пытались использовать match
для ссылки на Option
? Например, в подобном коде:
fn hello(arg: &Option<String>) {
match arg {
Some(name) => println!("Hello {}!", name),
None => println!("I don't know who you are."),
}
}
Если вы попытаетесь его скомпилировать в Rust 1.25, то вы получите такую ошибку:
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`
Да, конечно. Давайте изменим код:
fn hello(arg: &Option<String>) {
match arg {
&Some(name) => println!("Hello {}!", name),
&None => println!("I don't know who you are."),
}
}
Мы добавили &
, как требовал компилятор. Попробуем скомпилировать снова:
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
Да, конечно. Давайте усмирим-таки компилятор, последовав его совету:
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
. Поэтому, когда мы говорим
match arg {
Some(name) => println!("Hello {}!", name),
компилятор автоматически обратится к Some
по ссылке, и поскольку это будет заимствование, name
свяжется со значением как ref name
, тоже автоматически. Если бы мы изменяли значение:
fn hello(arg: &mut Option<String>) {
match arg {
Some(name) => name.push_str(", world"),
None => (),
}
}
компилятор бы автоматически выполнил изменяемое заимствование, и name
оказалось бы связано со значением как ref mut
.
Мы думаем, что это избавит от особо болезненного рутинного кода как новичков, так и заслуженных растоманов. Компилятор просто возьмет на себя эту работу, больше не требуя писать подобный рутинный код.
main
может возвращать Result
К слову о досадном рутинном коде: поскольку Rust использует тип Result
для возврата ошибок и ?
для упрощения их обработки, общей болевой точкой новичков в Rust становится попытка использовать ?
в main
:
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
". Которая многих людей вынуждает писать подобный код:
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
:
use std::fs::File;
fn main() -> Result<(), std::io::Error> {
let f = File::open("bar.txt")?;
Ok(())
}
Теперь это работает как надо! Если main
вернет ошибку, это приведет к завершению с кодом ошибки и печати отладочной информации об ошибке.
Закрытые диапазоны с ..=
Еще задолго до Rust 1.0, вы могли создавать полуоткрытые диапазоны с ..
, например:
for i in 1..3 {
println!("i: {}", i);
}
Этот код напечатает i: 1
, а затем i: 2
. В Rust 1.26 теперь вы можете создать закрытый диапазон, например:
for i in 1..=3 {
println!("i: {}", i);
}
Этот код напечатает i: 1
, затем i: 2
, как предыдущий, но также и i: 3
; три — тоже включится в диапазон. Закрытые диапазоны особенно полезны для перебора всех возможных значений. Например, вот удивительная программа на Rust:
fn takes_u8(x: u8) {
// ...
}
fn main() {
for i in 0..256 {
println!("i: {}", i);
takes_u8(i);
}
}
Что делает эта программа? Ответ: ничего. Предупреждение, которое мы получаем при компиляции, подсказывает почему:
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
, поэтому цикл выполняется ноль раз.
Однако с закрытыми диапазонами можно это исправить:
fn takes_u8(x: u8) {
// ...
}
fn main() {
for i in 0..=255 {
println!("i: {}", i);
takes_u8(i);
}
}
Этот код выведет 256 строк, которые вы ожидали.
Базовые образцы срезов
Еще одно долгожданное нововведение — "образцы срезов" (slice patterns). Оно позволяет вам сопоставлять с образцом срезы подобно тому, как вы сопоставляете с образцом другие типы данных. Например:
let arr = [1, 2, 3];
match arr {
[1, _, _] => "начинается с единицы",
[a, b, c] => "начинается с чего-то другого",
}
В данном случае мы знаем, что arr
имеет длину три, и поэтому нам нужны три элемента внутри []
. Мы также можем сопоставлять, когда мы не знаем длину:
fn foo(s: &[u8]) {
match s {
[a, b] => (),
[a, b, c] => (),
_ => (),
}
}
Здесь мы не знаем, какой длины s
, поэтому мы можем написать первые два образца,
каждый из которых рассчитан на разную длину. Также нам обязательно нужен вариант
_
, поскольку мы не покрываем все возможные случаи длины, но мы и не можем этого!
Увеличение скорости
Мы продолжаем улучшать скоростью работы компилятора. Мы обнаружили, что глубоко
вложенные типы в некоторых случаях становились нелинейными, что было исправлено. После этого исправления, вместе с которым было выпущено и много других небольших исправлений, мы наблюдали сокращение времени компиляции вплоть до 12%. В будущем улучшим ещё!
128-разрядные целые числа
И наконец, одно очень простое улучшение: теперь у Rust'а есть 128-разрядные целые числа!
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
для простого чтения в память всего файла сразу:
use std::fs;
use std::net::SocketAddr;
let foo: SocketAddr = fs::read_to_string("address.txt")?.parse()?;
Теперь вы можете форматировать вывод шестнадцатеричных чисел с Debug:
assert!(format!("{:02x?}", b"Foo") == "[46, 6f, 6f, 00]")
Завершающие запятые теперь поддерживаются всеми макросами в стандартной библиотеке.
Подробности смотрите в примечаниях к выпуску.
Улучшения в Cargo
В этом выпуске Cargo не получил значительных изменений функциональности, но получил ряд улучшений стабильности и производительности. Cargo теперь должен обрабатывать зависимости из lock-файлов еще быстрее и интеллектуальнее, а также требовать меньше ручных вызовов cargo update
. Исполняемый файл Cargo теперь имеет ту же версию, что и rustc.
Подробности смотрите в примечаниях к выпуску.
Разработчики 1.26.0
Множество людей участвовало в разработке Rust 1.26. Мы не смогли бы завершить работу без участия каждого из вас.
Авторы перевода: freecoder_xx и ozkriff.
Автор: Александр Мещеряков