Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1.30.0. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода.
Если у вас установлена предыдущая версия Rust с помощью rustup
, то для обновления Rust до версии 1.30.0 вам достаточно выполнить:
$ rustup update stable
Если у вас еще не установлен rustup
, вы можете установить его с соответствующей страницы нашего веб-сайта. С подробными примечаниями к выпуску Rust 1.30.0 можно ознакомиться на GitHub.
Что вошло в стабильную версию 1.30.0
Rust 1.30 — выдающийся выпуск с рядом важных нововведений. Но уже в понедельник в официальном блоге будет опубликована просьба проверить бета-версию Rust 1.31, которая станет первым релизом "Rust 2018". Дополнительную информацию об этом вы найдете в нашей предыдущей публикации "What is Rust 2018".
Процедурные макросы
Еще в Rust 1.15 мы добавили возможность определять "пользовательские derive-макросы". Например, с помощью serde_derive
, вы можете объявить:
#[derive(Serialize, Deserialize, Debug)]
struct Pet {
name: String,
}
И конвертировать Pet
в JSON и обратно в структуру, используя serde_json
. Это возможно благодаря автоматическому выводу типажей Serialize
и Deserialize
с помощью процедурных макросов в serde_derive
.
Rust 1.30 расширяет функционал процедурных макросов, добавляя возможность определять еще два других типа макросов: "атрибутные процедурные макросы" и "функциональные процедурные макросы".
Атрибутные макросы подобны derive-макросам для автоматического вывода, но вместо генерации кода только для атрибута #[derive]
, они позволяют пользователям создавать собственные новые атрибуты. Это делает их более гибкими: derive-макросы работают только для структур и перечислений, но атрибуты могут применяться и к другим объектам, таким как функции. Например, атрибутные макросы позволят вам при использовании веб-фреймворка делать следующее:
#[route(GET, "/")]
fn index() {
Этот атрибут #[route]
будет определен в самом фреймворке как процедурный макрос. Его сигнатура будет выглядеть так:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Здесь у нас имеется два входных параметра типа TokenStream
: первый — для содержимого самого атрибута, то есть это параметры GET, "/"
. Второй — это тело того объекта, к которому применен атрибут. В нашем случае — это fn index() {}
и остальная часть тела функции.
Функциональные макросы определяют такие макросы, использование которых выглядит как вызов функции. Например, макрос sql!
:
let sql = sql!(SELECT * FROM posts WHERE id=1);
Этот макрос внутри себя будет разбирать SQL-выражения и проверять их на синтаксическую корректность. Подобный макрос должен быть объявлен следующим образом:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
Это похоже на сигнатуру derive-макроса: мы получаем токены, которые находятся внутри скобок, и возвращаем сгенерированный по ним код.
Макросы и use
Теперь можно импортировать макросы в область видимости с помощью ключевого слова use. Например, для использования макроса json
из пакета serde-json
, раньше использовалась запись:
#[macro_use]
extern crate serde_json;
let john = json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
});
А теперь вы должны будете написать:
extern crate serde_json;
use serde_json::json;
let john = json!({
"name": "John Doe",
"age": 43,
"phones": [
"+44 1234567",
"+44 2345678"
]
});
Здесь макрос импортируется также, как и другие элементы, так что нет необходимости в использовании аннотации macro_use
.
Наконец, стабилизирован пакет proc_macro, который дает API, необходимый для написания процедурных макросов. В нем также значительно улучшили API для обработки ошибок, и такие пакеты, как syn
и quote
уже используют его. Например, раньше:
#[derive(Serialize)]
struct Demo {
ok: String,
bad: std::thread::Thread,
}
приводило к такой ошибке:
error[E0277]: the trait bound `std::thread::Thread: _IMPL_SERIALIZE_FOR_Demo::_serde::Serialize` is not satisfied
--> src/main.rs:3:10
|
3 | #[derive(Serialize)]
| ^^^^^^^^^ the trait `_IMPL_SERIALIZE_FOR_Demo::_serde::Serialize` is not implemented for `std::thread::Thread`
Теперь же будет выдано:
error[E0277]: the trait bound `std::thread::Thread: serde::Serialize` is not satisfied
--> src/main.rs:7:5
|
7 | bad: std::thread::Thread,
| ^^^ the trait `serde::Serialize` is not implemented for `std::thread::Thread`
Улучшение системы модулей
Система модулей долгое время становилась больным местом для новичков в Rust'е; некоторые из ее правил оказывались неудобными на практике. Настоящие изменения являются первым шагом, который мы предпринимаем на пути упрощения системы модулей.
В дополнении к вышеупомянутому изменению для макросов, есть два новых улучшения в использовании use
. Во-первых, внешние пакеты теперь добавляются в prelude, то есть:
// было
let json = ::serde_json::from_str("...");
// стало
let json = serde_json::from_str("...");
Подвох в том, что старый стиль не всегда был нужен из-за особенностей работы системы модулей Rust:
extern crate serde_json;
fn main() {
// это прекрасно работает; мы находимся в корне пакета, поэтому `serde_json`
// здесь в области видимости
let json = serde_json::from_str("...");
}
mod foo {
fn bar() {
// это не работает; мы внутри пространства имен `foo`, и `serde_json`
// здесь не объявлен
let json = serde_json::from_str("...");
}
// одно решение - это импортировать его внутрь модуля с помощью `use`
use serde_json;
fn baz() {
// другое решение - это использовать `::serde_json`, когда указывается
// абсолютный путь, вместо относительного
let json = ::serde_json::from_str("...");
}
}
Было неприятно получать сломанный код, просто перемещая функцию в подмодуль. Теперь же будет проверяться первая часть пути, и если она соответствует некоторому extern crate
, то он будет использоваться независимо от положения вызова в иерархии модулей.
mod foo {
pub fn bar() {
// ...
}
}
// было
use ::foo::bar;
// или
use foo::bar;
// стало
use crate::foo::bar;
Ключевое слово crate
в начале пути указывает, что путь будет начинаться от корня пакета. Раньше пути, указанные в строке импорта use
, всегда указывались относительно корня пакета, но пути в остальном коде, напрямую ссылающиеся на элементы, указывались относительно текущего модуля, что приводило к противоречивому поведению путей:
mod foo {
pub fn bar() {
// ...
}
}
mod baz {
pub fn qux() {
// было
::foo::bar();
// не работает, в отличии от `use`:
// foo::bar();
// стало
crate::foo::bar();
}
}
Как только новый стиль станет широко использоваться, то он, мы надеемся, сделает абсолютные пути более ясными, без необходимости использовать уродливый префикс ::
.
Все эти изменения в совокупности упрощают понимание того, как разрешаются пути. В любом месте, где вы видите путь a::b::c
, кроме оператора use
, вы можете спросить:
- Является ли
a
именем пакета? Тогда нужно искатьb::c
внутри него. - Является ли
a
ключевым словомcrate
? Тогда нужно искатьb::c
от корня текущего пакета. - В противном случае, нужно искать
a::b::c
от текущего положения в иерархии модулей.
Старое поведение путей в use
, всегда начинающихся от корня пакета, по-прежнему применимо. Но при единовременном переходе на новый стиль, данные правила будут применяться к путям повсюду единообразно, и вам придется гораздо меньше заботиться об импортах при перемещении кода.
Сырые идентификаторы
Вы можете теперь использовать ключевые слова как идентификаторы, используя следующий новый синтаксис:
// определение локальной переменной с именем `for`
let r#for = true;
// определение функции с именем `for`
fn r#for() {
// ...
}
// вызов той функции
r#for();
Пока не так много случаев, когда вам это пригодится. Но однажды вы попытаетесь использовать пакет для Rust 2015 в проекте для Rust 2018 или наоборот, тогда набор ключевых слов у них будет разным. Мы расскажем об этом подробнее в предстоящем анонсе Rust 2018.
Приложения без стандартной библиотеки
Еще в Rust 1.6 мы объявили о стабилизации "no_std" и libcore для создания проектов без стандартной библиотеки. Однако, с одним уточнением: можно было создавать только библиотеки, но не приложения.
В Rust 1.30 можно использовать атрибут #[panic_handler]
для самостоятельной реализации паники. Это означает, что теперь можно создавать приложения, а не только библиотеки, которые не используют стандартную библиотеку.
Другое
И последнее: в макросах теперь можно сопоставлять модификаторы области видимости, такие как pub
, с помощью спецификатора vis
. Дополнительно, "инструментальные атрибуты", такие как #[rustfmt::skip]
, теперь стабилизированы. Правда для использования с инструментами статического анализа, наподобие #[allow(clippy::something)]
, они еще не стабильны.
Подробности смотрите в примечаниях к выпуску.
Стабилизация стандартной библиотеки
В этом выпуске были стабилизированы следующие API:
Ipv4Addr::{BROADCAST, LOCALHOST, UNSPECIFIED}
Ipv6Addr::{BROADCAST, LOCALHOST, UNSPECIFIED}
Iterator::find_map
Кроме того, стандартная библиотека уже давно имеет функции для удаления пробелов с одной стороны некоторого текста, такие как trim_left
. Однако, для RTL-языков значение "справа" и "слева" тут приводят к путанице. Поэтому мы вводим новые имена для этих функций:
trim_left
->trim_start
trim_right
->trim_end
trim_left_matches
->trim_start_matches
trim_right_matches
->trim_end_matches
Мы планируем объявить устаревшими старые имена (но не удалить, конечно) в Rust 1.33.
Подробности смотрите в примечаниях к выпуску.
Улучшения в Cargo
Самое большое улучшение Cargo в этом выпуске заключается в том, что теперь у нас есть индикатор выполнения!
Подробности смотрите в примечаниях к выпуску.
Разработчики 1.30.0
Множество людей совместно создавало Rust 1.30. Мы не смогли бы завершить работу без участия каждого из вас. Спасибо!
От переводчика: выражаю отдельную благодарность участникам сообщества Rustycrate и лично vitvakatu и Virtuos86 за помощь с переводом и вычиткой.
Комментарии (45)
Laney1
29.10.2018 11:56-2[route(GET, "/")]
повбивав бы за такое. Уж сколько раз твердили миру, что все эти DSL-и выглядят красиво только в теории, а на практике они приводят к неотлаживаемому и неподдерживаемому коду. Но похоже, что этот урок пока усвоили только авторы Go.
freecoder_xx Автор
29.10.2018 12:13+2А в чем, собственно, проблема? Атрибуты в Rust реализованы не через строки, как в Go, они вполне себе парсятся и проверяются во время компиляции.
Laney1
29.10.2018 12:57-7в синтаксическом сахаре и бессмысленном усложнении кода. Если вы не видите в этом проблемы, значит вы плохой программист.
Zanak
29.10.2018 13:25+3На сколько я понимаю, аналогом данной конструкции в python являются декораторы. Если так, то вы безусловно не правы. При правильном применении, декораторы способны сделать ваш код проще, главное не перебарщивать с их использованием.
Laney1
29.10.2018 14:30-3разработчики вообще много чего напридумывали. Но я грешным делом думал, что системный язык программирования, ориентированный на безопасность и применение во всяких там эмбедах — не место для подобных экспериментов.
Интересно, сколько из минусующих являются системными программистами, и сколько — обычными веб-макаками, не разбирающимися в теме?
andreymal
29.10.2018 14:35Давайте предположим, что здесь кто-нибудь не разбирается в теме — расскажите пожалуйста, чем это небезопасно?
Zanak
29.10.2018 14:56+1Когда кажется, что «весь мир сошел с ума, и только я нормальный» — это имеет свое название… :) (это к вопросу о макаках, не разбирающихся в программировании)
Что же касается самого шаблона «декоратор», я прекрасно знаю о его плюсах и минусах, но готов выслушать ваше мнение. Или вы больше против экспериментов, как таковых, в языках программирования?
humbug
29.10.2018 15:39+1Но я грешным делом думал, что системный язык программирования, ориентированный на безопасность и применение во всяких там эмбедах — не место для подобных экспериментов.
Ну так это же user(library)-defined правила, а не вшитые в язык. Не хотите — не пользуйтесь, генерируйте роуты ручками.
Сравните подход go vs rust. Только угоротый подтвердит, что в go аннотирование структур через строки (которые проверяются только в рантайме) сделаны удачно.
JekaMas
29.10.2018 16:00Оооккк. Где записываться в "угоротые"?
*Мне кажется, вы тоже несколько слишком категоричны.
Hellpain
29.10.2018 16:30+2Если вы системным программированием зовете скудность языка C, то это исключительно ваши проблемы восприятия мира системного программирования.
freecoder_xx Автор
29.10.2018 16:45+1Несмотря на то, что Rust позиционируется как системный язык программирования, он также является языком программирования общего назначения. Что плохого в том, что как системный ЯП Rust имеет развитую систему типов и довольно высокоуровневые абстракции с нулевой стоимостью, а как прикладной ЯП — высокую производительность?
Sabubu
29.10.2018 14:46Вообще, что касается правил роутинга, их удобнее иметь в отдельном файле — чтобы, например, сразу видеть конфликты -а не раскидывать по коду.
freecoder_xx Автор
29.10.2018 16:52Ну вот посмотрите, как это делается в Rocket:
#[get("/<name>/<age>")] fn hello(name: String, age: u8) -> String { format!("Hello, {} year old named {}!", age, name) } fn main() { rocket::ignite().mount("/hello", routes![hello]).launch(); }
То есть атрибуту на откуп отдается указание метода и параметров запроса, а к корневой части пути разные обработчики монтируются в одном месте.
Zanak
29.10.2018 21:42Вы написали:
а теперь представим, что в другом месте вы определили маршрут#[get("/<name>/<age>")]
что при этом произойдет?#[get("/<model>/<year_of_product>")]
mayorovp
29.10.2018 22:12+3Если монтировать их на разные конечные точки — ничего особенного не произойдет…
KIVan
30.10.2018 02:36-1«представим, что в другом месте» — а если эти два маршрута определить в одном месте, проблема ведь никуда не денется, так? Её только заметят с большей вероятностью. Проблема тут возникает из-за использования сегментов пути для передачи параметров. Это в принципе плохо и проблемы с таким подходом будут всегда, вне зависимости от технической реализации.
У URL есть разные специализированные части, en.wikipedia.org/wiki/URL#Syntax. Для передачи агрументов запроса служит «query», а в этом примере мы пытаемся передавать их через «path». «Path» предназначен для описания иерархичного пути (изначально соответствовал пути файла в ФС), к примеру, Application/Controller/Method. Такой путь практически гарантированно будет уникальным (1 путь = 0..1 файлов / методов). А в примере мы передаем в сегментах аргументы, для которых уникальность не является условием, отсюда и проблема с определением правильного маршрута.
anonymous
30.10.2018 06:26Как практик, отвечаю, в Rocket для этого есть аттрибут «rank».
Ничего хорошего в нём нет, но приходится делать, то, что приходится :)
Sabubu
30.10.2018 14:04Не всегда получится сделать так, что у роутов будут разные префиксы. И даже если они разные, все равно, в реальном коде (не с функциями на 3 строчки как у вас), эти роуты будут далеко друг от друга раскиданы.
ozkriff
30.10.2018 14:12в других языках
Как бы там ни было с правилами роутинга, речь же просто о дизайне одной из библиотек, а не всего языка, у rocket.rs вполне себе есть живые ржавые альтернативы.
freecoder_xx Автор
30.10.2018 14:18Ну и что с того? У вас же монтироваться они будут в одном месте — переходите к указанной функции-обработчику и все. Наоборот, удобно, что та часть запроса, которая должна соответствовать сигнатуре обработчика, указывается рядом с этой сигнатурой. Более того, так как она задается в процедурном макросе, мы можем проверять во время компиляции это соответствие.
a-tk
30.10.2018 18:01Вы про .htaccess сейчас?
Sabubu
31.10.2018 14:17Я про файл с конфигурацией роутинга, вроде routing.yml из Симфони.
freecoder_xx Автор
31.10.2018 14:54+1Кстати, чтение, разбор и генерацию кода по такому файлу тоже можно сделать на этапе компиляции с помощью процедурных макросов.
anonymous
30.10.2018 06:23Частично соглашусь.
Эти маршруты действительно красиво выглядят в документации (и в подходящих проектах, наверное), но у нас в итоге всё свелось к фактически одному маршруту на всё подряд (ну, к 4-ем, GET/POST/PUT/DELETE), а дальше мы запрос сами разбираем… (тут возникает справедливый вопрос, зачем нам Rocket, но просто пока времени нет всё на hyper переделать).
Но всё же для разных задач — разные инструменты. Охотно допускаю, что где-то такие маршруты (и Rocket) могут быть удобны. Для более фиксированных API (наша проблема в том, что API очень уж динамичные).
kvark
30.10.2018 03:44Те ещё пляски с бубном вокруг модулей, да методов именований. Лучше бы о массивах фиксмрованной длинны подумали.
AngReload
30.10.2018 06:50Помню, не мог разобраться как макросы импортировать, мне нравятся сегодняшние изменения.
А массивы, что с ними не так?naething
30.10.2018 09:28А массивы, что с ними не так?
Проблема 1: Массив фиксированной длины нельзя ициниализировать в цикле или, скажем, из среза или итератора. Например, если вы хотите заполнить массив длины N (где N — констана) числами от 0 до N-1, вам придется написать что-то вроде
let a = [0i32; N]; for i in 0..N { a[i] = i; }
Здесь память переписывается дважды: первый раз нулями при инициализации массива, второй раз при заполнении числами. Нет никакой возможности этого избежать.
Проблема 2: Длину фиксированного массива нельзя сделать параметром шаблона. В C++ вы можете написать
В текущей версии Rust числа не могут быть параметрами шаблона, поэтому так сделать нельзя. Из этого вытекают многие другие проблемы. Например, нельзя реализовать какое-либо свойство (trait) для массивов произвольной длины. Если вы откроете страницу документации о массивах, то увидите, что реализации стандандартных свойств генерируются с помощью макросов для каждого N от 1 до 32: doc.rust-lang.org/std/primitive.array.html (в самом конце страницы). То есть, например, PartialEq реализовано для [u8; 32], но не для [u8; 33].template<size_t N> struct Foo { int x[N]; };
freecoder_xx Автор
30.10.2018 13:24+1В unsafe Rust первая проблема решается:
let a = unsafe { let mut array: [i32; N] = std::mem::uninitialized(); for i in 0..N { std::ptr::write(&mut array[i], i); } array };
red75prim
30.10.2018 17:56+2Во многих случаях первая проблема и оптимизатором решается. godbolt.org/z/3HHGYt
Но при длине массива 52 и больше оптимизатор почему-то сдаётся. Так что с unsafe всё-таки надёжнее.
freecoder_xx Автор
30.10.2018 13:11Ну сейчас устаканивается Rust 2018, поэтому в основном завершается стабилизация уже разработанного ранее. Константы в обобщенных типах еще требуют серьезной разработки и широкого тестирования, так что в нынешнем году мы их точно не увидим в стабильном Rust.
n0dwis
30.10.2018 09:53Небольшой оффтопик, извините.
Вопрос специалистам по Rust — пытаюсь построить дерево DOM, есть ли какие-то стандартные подходы, просто для обучения? Все библиотеки, что находил, основываются на cell и трёхэтажных шаблонах. Нет ли чего-нибудь попроще, пусть и не настолько универсального.freecoder_xx Автор
30.10.2018 11:31+1Если вам не обязательно хранить ссылку на родителя в дочернем элементе, то реализация будет тривиальна. В противном случае вам придется:
- Использовать
Rc
,RefCell
и прочиеWeak
; или - Использовать сырые указатели и управлять освобождением памяти вручную; или
- Хранить узлы во внешнем контейнере и ссылаться на них по индексу; или
- Использовать готовые библиотеки, которые скрывают всю кухню за своим API.
Вообще, для лучшего понимания, рекомендую изучить реализацию
rust-forest
. Там представлено несколько подходов и они довольно неплохо прокомментированы.n0dwis
30.10.2018 12:02Не совсем тривиальна. Нужно ведь хранить стек предков + дерево узлов. И в той и в той структуре должна быть ссылка на один и тот же узел. И поскольку всё это в цикле — компилятор не может определить время жизни и ругается (насколько я понял).
Можно, правда, добавлять узел в дерево только после обхода всех его потомков, это снимает проблему, но, боюсь, такое подойдёт не во всех случаях.
Вообще, для лучшего понимания, рекомендую изучить реализацию rust-forest. Там представлено несколько подходов и они довольно неплохо прокомментированы.
Спасибо большое! Изучу.
- Использовать
freecoder_xx Автор
30.10.2018 12:17+2В Cell-ах и трехэтажных шаблонах все же лучше сразу разобраться, чтобы понимать, как реализовывать некоторые, привычные по другим языкам программирования вещи в Rust.
Допустим у вас есть некий объект и вы хотите хранить ссылки на него в нескольких местах одновременно. Но тут возникает сразу две проблемы:
- Объект должен быть доступен из разных мест по ссылке, и удален только тогда, когда на него нет ссылок. Если есть по крайней мере одна ссылка, то объект должен жить.
- Правила Rust позволяют иметь больше одной ссылки только на неизменяемый объект. Ссылка на изменяемый объект может быть только одна, без каких либо других ссылок на этот объект.
Первая проблема решается подсчетом ссылок (reference counting) и в других популярных прикладных ЯП она обычно скрыта от пользователя за реализацией. То есть любая "ссылка" на объект в таких ЯП всегда есть умный указатель со счетчиком ссылок. В Rust же такого типа ссылку нужно создавать вручную, если она вам нужна. Для этого используются типы
Rc
иArc
. Кроме того, нужно как-то решать вопрос образования циклических ссылок (Weak
).
Вторая проблема решается введением совместно используемых изменяемых контейнеров — это типы
Cell
,RefCell
,Mutex
,RwLock
.
Я рекомендую прочитать вот эту страницу официальной документации и разобрать приведенный там пример с гаджетами для того, чтобы понять, для чего и как использовать
Rc
,RefCell
иWeak
вместе. Также рекомендую ознакомиться с документацией по cell.
freecoder_xx Автор
30.10.2018 22:59+1Обещанный блог-пост таки вышел, слегка с опозданием: Help test Rust 2018
ozkriff
Новый прогрессбар так себе помещается в узкие вертикальные консоли по 90-100 символов, но не могу найти опций cargo для его отключения. Никто не натыкался?
freecoder_xx Автор
Похоже, что не отключается. Придется терпеть )
Хотя у меня нормально отображается и в узкой консоли:
ozkriff
Да, походу в совсем узкой консоли имена компилируемых пакетов не показываются и все окей. А вот если сделать 90-100 шириной, то справа от индексов компилируемых пакетов показываются их имена и они уже не влезают в границу строки нормально. :(