Хабрахабр

[Перевод] Выпуск Rust 1.26

26. Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода. 0.

26. Если у вас установлена предыдущая версия Rust с помощью rustup, то для обновления Rust до версии 1. 0 вам достаточно выполнить:

$ rustup update stable

С подробными примечаниями к выпуску Rust 1. Если у вас еще не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта. 0 можно ознакомиться на GitHub. 26.

Что вошло в стабильную версию 1.26.0

Тем не менее, мы продолжали работу над многими другими вещами и теперь они начинают выходить в стабильной версии. Последние несколько выпусков имели ряд относительно небольших улучшений. 26, возможно, самая богатая нововведениями со времен выпуска Rust 1. Версия 1. Давайте их рассмотрим! 0.

Второе издание книги "Язык программирования Rust"

С момента написания первой книги мы узнали много нового о том, как люди изучают Rust, так что новая версия книги теперь лучше во всех отношениях. Почти 18 месяцев Кэрол, Стив и другие работали над полной переработкой книги "Язык программирования Rust".

Теперь же в книгу вносятся небольшие финальные правки и она готовится к печати. Ранее черновик второго издания уже был опубликован на веб-сайте с заявлением о том, что это незавершенная версия. Вы можете найти его на doc.rust-lang.org или получить локально, выполнив rustup doc --book. Так что с этого выпуска мы рекомендуем читать второе издание вместо первого.

Содержимое идентично, но вы получите или настоящую физическую копию книги, чтобы поставить ее на полку, или отлично сверстанный PDF. Кстати, о печати: если вам не жалко деревьев, то вы можете заказать бумажную версию книги на NoStarch Press. Вся выручка пойдёт на благотворительность.

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, что влечет выделение памяти в куче. 26 вы можете написать так: Вместо этого в Rust 1.

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<>, [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."), }
}

25, то вы получите такую ошибку: Если вы попытаетесь его скомпилировать в Rust 1.

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.

26, первоначальный код без & и ref будет просто компилироваться и делать именно то, что вы ожидаете. Итак, начиная с Rust 1. Поэтому, когда мы говорим Короче говоря, компилятор будет автоматически ссылаться или разыменовывать ссылки в конструкции 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); }
}

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

26 вы теперь можете объявить main, который возвращает Result: В Rust 1.

use std::fs::File; fn main() -> Result<(), std::io::Error> { let f = File::open("bar.txt")?; Ok(())
}

Если main вернет ошибку, это приведет к завершению с кодом ошибки и печати отладочной информации об ошибке. Теперь это работает как надо!

Закрытые диапазоны с ..=

0, вы могли создавать полуоткрытые диапазоны с .., например: Еще задолго до Rust 1.

for i in 1..3 { println!("i: {}", i);
}

В Rust 1. Этот код напечатает i: 1, а затем i: 2. 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

0, поэтому цикл выполняется ноль раз. Это правильно, так как i типа u8, который переполняется, и это то же самое, что писать for i in 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,455
  • i128: −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\0") == "[46, 6f, 6f, 00]")

Завершающие запятые теперь поддерживаются всеми макросами в стандартной библиотеке.

Подробности смотрите в примечаниях к выпуску.

Улучшения в Cargo

Cargo теперь должен обрабатывать зависимости из lock-файлов еще быстрее и интеллектуальнее, а также требовать меньше ручных вызовов cargo update. В этом выпуске Cargo не получил значительных изменений функциональности, но получил ряд улучшений стабильности и производительности. Исполняемый файл Cargo теперь имеет ту же версию, что и rustc.

Подробности смотрите в примечаниях к выпуску.

Разработчики 1.26.0

26. Множество людей участвовало в разработке Rust 1. Мы не смогли бы завершить работу без участия каждого из вас.

Спасибо!

Авторы перевода: freecoder_xx и ozkriff.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть