Язык программирования Rust
Скачать 7.02 Mb.
|
Делаем код понятнее с помощью адаптеров итераторов Мы также можем воспользоваться преимуществами итераторов в функции search в нашем проекте с операциями ввода-вывода, которая воспроизведена здесь в листинге 13-21 так же, как и в листинге 12-19: Файл: src/lib.rs impl Config { pub fn build ( mut args: impl Iterator >, ) -> Result > { args.next(); let query = match args.next() { Some (arg) => arg, None => return Err ( "Didn't get a query string" ), }; let file_path = match args.next() { Some (arg) => arg, None => return Err ( "Didn't get a file path" ), }; let ignore_case = env::var( "IGNORE_CASE" ).is_ok(); Ok (Config { query, file_path, ignore_case, }) } } Листинг 13-21: Реализация функции search из листинга 12-19 Мы можем написать этот код в более сжатом виде, используя методы адаптера итератора. Это также позволит нам избежать наличия изменяемого временного вектора results . Функциональный стиль программирования предпочитает минимизировать количество изменяемого состояния, чтобы сделать код более понятным. Удаление изменяемого состояния может позволить в будущем сделать поиск параллельным, поскольку нам не придётся управлять одновременным доступом к вектору results . В листинге 13-22 показано это изменение: Файл: src/lib.rs Листинг 13-22: Использование методов адаптера итератора в реализации функции search Напомним, что назначение функции search - вернуть все строки в contents , которые содержат query . Подобно примеру filter в листинге 13-16, этот код использует адаптер filter , чтобы сохранить только те строки, для которых line.contains(query) возвращает true . Затем мы собираем совпадающие строки в другой вектор с помощью collect . Так гораздо проще! Не стесняйтесь сделать такое же изменение для использования методов итератора в функции search_case_insensitive Выбор между циклами или итераторами Следующий логичный вопрос - какой стиль вы должны выбрать в своём коде и почему: оригинальную реализацию в листинге 13-21 или версию с использованием итераторов в листинге 13-22. Большинство программистов на языке Rust предпочитают использовать стиль итераторов. Сначала разобраться с ним немного сложно, но как только вы почувствуете, что такое различные адаптеры итераторов и что они делают, понять итераторы станет проще. Вместо того чтобы возиться с различными элементами цикла и pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { let mut results = Vec ::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } pub fn search < 'a >(query: & str , contents: & 'a str ) -> Vec <& 'a str > { contents .lines() .filter(|line| line.contains(query)) .collect() } создавать новые векторы, код фокусируется на высокоуровневой цели цикла. Это абстрагирует часть обычного кода, поэтому легче увидеть концепции, уникальные для этого кода, такие как условие фильтрации, которое должен пройти каждый элемент в итераторе. Но действительно ли эти две реализации эквивалентны? Интуитивно можно предположить, что более низкоуровневый цикл будет быстрее. Давайте поговорим о производительности. Сравнение производительности циклов и итераторов Чтобы определить, что лучше использовать циклы или итераторы, нужно знать, какая реализация быстрее: версия функции search с явным циклом for или версия с итераторами. Мы выполнили тест производительности, разместив всё содержимое книги (“The Adventures of Sherlock Holmes” by Sir Arthur Conan Doyle) в строку типа String и поискали слово the в её содержимом. Вот результаты теста функции search с использованием цикла for и с использованием итераторов: Версия с использованием итераторов была немного быстрее! Мы не будем приводить здесь непосредственно код теста, поскольку идея не в том, чтобы доказать, что решения в точности эквивалентны, а в том, чтобы получить общее представление о том, как эти две реализации близки по производительности. Для более исчерпывающего теста, вам нужно проверить различные тексты разных размеров в качестве содержимого для contents , разные слова и слова различной длины в качестве query и всевозможные другие варианты. Дело в том, что итераторы, будучи высокоуровневой абстракцией, компилируются примерно в тот же код, как если бы вы написали его низкоуровневый вариант самостоятельно. Итераторы - это одна из абстракций с нулевой стоимостью ( zero-cost abstractions ) в Rust, под которой мы подразумеваем, что использование абстракции не накладывает дополнительных расходов во время выполнения. Аналогично тому, как Бьёрн Страуструп, дизайнер и разработчик C++, определяет нулевые накладные расходы ( zero-overhead ) в книге “Foundations of C++” (2012): В целом, реализация C++ подчиняется принципу отсутствия накладных расходов: за то, чем вы не пользуетесь, платить не нужно. И далее: тот код, что вы используете, нельзя сделать ещё лучше. В качестве другого примера приведём код, взятый из аудио декодера. Алгоритм декодирования использует математическую операцию линейного предсказания для оценки будущих значений на основе линейной функции предыдущих выборок. Код использует комбинирование вызовов итератора для выполнения математических вычислений для трёх переменных в области видимости: срез данных buffer , массив из 12 коэффициентов coefficients и число для сдвига данных в переменной qlp_shift Переменные определены в примере, но не имеют начальных значений. Хотя этот код не имеет большого значения вне контекста, он является кратким, реальным примером того, как Rust переводит идеи высокого уровня в код низкого уровня. test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) Чтобы вычислить значение переменной prediction , этот код перебирает каждое из 12 значений в переменной coefficients и использует метод zip для объединения значений коэффициентов с предыдущими 12 значениями в переменной buffer . Затем, для каждой пары мы перемножаем значения, суммируем все результаты и у суммы сдвигаем биты вправо в переменную qlp_shift Для вычислений в таких приложениях, как аудио декодеры, часто требуется производительность. Здесь мы создаём итератор, используя два адаптера, впоследствии потребляющих значение. В какой ассемблерный код будет компилироваться этот код на Rust? На момент написания этой главы он компилируется в то же самое, что вы написали бы руками. Не существует цикла, соответствующего итерации по значениям в «коэффициентах» coefficients : Rust знает, что существует двенадцать итераций, поэтому он «разворачивает» цикл. Разворачивание - это оптимизация, которая устраняет издержки кода управления циклом и вместо этого генерирует повторяющийся код для каждой итерации цикла. Все коэффициенты сохраняются в регистрах, что означает очень быстрый доступ к значениям. Нет никаких проверок границ доступа к массиву во время выполнения. Все эти оптимизации, которые может применить Rust, делают полученный код чрезвычайно эффективным. Теперь, когда вы это знаете, используйте итераторы и замыкания без страха! Они представляют код в более высокоуровневом виде, но без потери производительности во время выполнения. Итоги Замыкания (closures) и итераторы (iterators) это возможности Rust, вдохновлённые идеями функциональных языков. Они позволяют Rust ясно выражать идеи высокого уровня с производительностью низкоуровневого кода. Реализации замыканий и итераторов таковы, что нет влияния на производительность выполнения кода. Это одна из целей Rust, направленных на обеспечение абстракций с нулевой стоимостью (zero- cost abstractions). let buffer: & mut [ i32 ]; let coefficients: [ i64 ; 12 ]; let qlp_shift: i16 ; for i in 12 ..buffer.len() { let prediction = coefficients.iter() .zip(&buffer[i - 12 ..i]) .map(|(&c, &s)| c * s as i64 ) .sum::< i64 >() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32 + delta; } Теперь, когда мы улучшили представление кода в нашем проекте, рассмотрим некоторые возможности, которые нам предоставляет cargo для публикации нашего кода в репозитории. Больше о Cargo и Crates.io До сих пор мы использовали только самые основные возможности Cargo для сборки, запуска и тестирования нашего кода, но он может гораздо больше. В этой главе мы обсудим некоторые другие, более продвинутые возможности, чтобы показать вам, как делать следующее: Настройка сборки с помощью релизных профилей Публикация библиотеки на crates.io Управление крупными проектами с помощью рабочих пространств Установка бинарных файлов с crates.io Расширение возможностей Cargo с помощью возможности добавления собственных команд Cargo может делать значительно больше того, что мы рассмотрим в этой главе, полное описание всех его функций см. в документации Настройка сборок с профилями релизов В Rust профили выпуска — это предопределённые и настраиваемые профили с различными конфигурациями, которые позволяют программисту лучше контролировать различные параметры компиляции кода. Каждый профиль настраивается независимо от других. Cargo имеет два основных профиля: профиль dev , используемый Cargo при запуске cargo build , и профиль release , используемый Cargo при запуске cargo build -- release . Профиль dev определён со значениями по умолчанию для разработки, а профиль release имеет значения по умолчанию для сборок в релиз. Эти имена профилей могут быть знакомы по результатам ваших сборок: dev и release — это разные профили, используемые компилятором. Cargo содержит настройки по умолчанию для каждого профиля, которые применяются, если вы явно не указали секции [profile.*] в файле проекта Cargo.toml. Добавляя секции [profile.*] для любого профиля, который вы хотите настроить, вы переопределяете любое подмножество параметров по умолчанию. Например, вот значения по умолчанию для параметра opt-level для профилей dev и release : Файл: Cargo.toml Параметр opt-level управляет количеством оптимизаций, которые Rust будет применять к вашему коду, в диапазоне от 0 до 3. Использование большего количества оптимизаций увеличивает время компиляции, поэтому если вы находитесь в процессе разработки и часто компилируете свой код, целесообразно использовать меньшее количество оптимизаций, чтобы компиляция происходила быстрее, даже если в результате код будет работать медленнее. Поэтому opt-level по умолчанию для dev установлен в 0 . Когда вы готовы опубликовать свой код, то лучше потратить больше времени на компиляцию. Вы скомпилируете программу в режиме релиза только один раз, но выполняться она будет многократно, так что использование режима релиза позволяет увеличить скорость выполнения кода за счёт времени компиляции. Вот почему по умолчанию opt-level для профиля release равен 3 $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0s $ cargo build --release Finished release [optimized] target(s) in 0.0s [profile.dev] opt-level = 0 [profile.release] opt-level = 3 Вы можете переопределить настройки по умолчанию, добавив другое значение для них в Cargo.toml. Например, если мы хотим использовать уровень оптимизации 1 в профиле разработки, мы можем добавить эти две строки в файл Cargo.toml нашего проекта: Файл: Cargo.toml Этот код переопределяет настройку по умолчанию 0 . Теперь, когда мы запустим cargo build , Cargo будет использовать значения по умолчанию для профиля dev плюс нашу настройку для opt-level . Поскольку мы установили для opt-level значение 1 , Cargo будет применять больше оптимизаций, чем было задано по умолчанию, но не так много, как при сборке релиза. Полный список параметров конфигурации и значений по умолчанию для каждого профиля вы можете найти в документации Cargo [profile.dev] opt-level = 1 Публикация библиотеки в Crates.io Мы использовали пакеты из crates.io в качестве зависимостей нашего проекта, но вы также можете поделиться своим кодом с другими людьми, опубликовав свои собственные пакеты. Реестр библиотек по адресу crates.io распространяет исходный код ваших пакетов, поэтому он в основном размещает код с открытым исходным кодом. В Rust и Cargo есть функции, которые облегчают поиск и использование опубликованного пакета. Далее мы поговорим о некоторых из этих функций, а затем объясним, как опубликовать пакет. Создание полезных комментариев к документации Аккуратное документирование ваших пакетов поможет другим пользователям знать, как и когда их использовать, поэтому стоит потратить время на написание документации. В главе 3 мы обсуждали, как комментировать код Rust, используя две косые черты, // . В Rust также есть особый вид комментариев к документации, который обычно называется комментарием к документации, который генерирует документацию HTML. HTML-код отображает содержимое комментариев к документации для публичных элементов API, предназначенных для программистов, заинтересованных в знании того, как использовать вашу библиотеку, в отличие от того, как она реализована. Комментарии к документации используют три слеша, /// вместо двух и поддерживают нотацию Markdown для форматирования текста. Размещайте комментарии к документации непосредственно перед элементом, который они документируют. В листинге 14-1 показаны комментарии к документации для функции add_one в библиотеке с именем my_crate : Файл: src/lib.rs Листинг 14-1: Комментарий к документации для функции /// Adds one to the number given. /// /// # Examples /// /// ``` /// let arg = 5; /// let answer = my_crate::add_one(arg); /// /// assert_eq!(6, answer); /// ``` pub fn add_one (x: i32 ) -> i32 { x + 1 } Здесь мы даём описание того, что делает функция add_one , начинаем раздел с заголовка Examples , а затем предоставляем код, который демонстрирует, как использовать функцию add_one . Мы можем сгенерировать документацию HTML из этого комментария к документации, запустив cargo doc . Эта команда запускает инструмент rustdoc , поставляемый с Rust, и помещает сгенерированную HTML-документацию в каталог target/doc. Для удобства, запустив cargo doc --open , мы создадим HTML для документации вашей текущей библиотеки (а также документацию для всех зависимостей вашей библиотеки) и откроем результат в веб-браузере. Перейдите к функции add_one и вы увидите, как отображается текст в комментариях к документации, что показано на рисунке 14-1: Рисунок 14-1: HTML документация для функции add_one Часто используемые разделы Мы использовали Markdown заголовок # Examples в листинге 14-1 для создания раздела в HTML с заголовком "Examples". Вот некоторые другие разделы, которые авторы библиотек обычно используют в своей документации: Panics: Сценарии, в которых документированная функция может вызывать панику. Вызывающие функцию, которые не хотят, чтобы их программы паниковали, должны убедиться, что они не вызывают функцию в этих ситуациях. Ошибки: Если функция возвращает Result , описание типов ошибок, которые могут произойти и какие условия могут привести к тому, что эти ошибки могут быть возвращены, может быть полезным для вызывающих, так что они могут написать код для обработки различных типов ошибок разными способами. Безопасность: Если функция является unsafe для вызова (мы обсуждаем безопасность в главе 19), должен быть раздел, объясняющий, почему функция небезопасна и охватывающий инварианты, которые функция ожидает от вызывающих сторон. В подавляющем большинстве случаев комментарии к документации не нуждаются во всех этих разделах, но это хорошая подсказка, напоминающая вам о тех аспектах вашего кода, о которых пользователям будет интересно узнать. Комментарии к документации как тесты Добавление примеров кода в комментарии к документации может помочь продемонстрировать, как использовать вашу библиотеку, и это даёт дополнительный бонус: запуск cargo test запустит примеры кода в вашей документации как тесты! Нет ничего лучше, чем документация с примерами. Но нет ничего хуже, чем примеры, которые не работают, потому что код изменился с момента написания документации. Если мы запустим cargo test с документацией для функции add_one из листинга 14-1, мы увидим раздел результатов теста, подобный этому: Теперь, если мы изменим либо функцию, либо пример, так что assert_eq! в примере паникует, и снова запустим cargo test , мы увидим, что тесты документации обнаруживают, что пример и код не синхронизированы друг с другом! Комментирование содержащихся элементов Стиль комментариев к документам //! добавляет документацию к элементу, содержащему комментарии, а не к элементам, следующим за комментариями. Обычно мы используем эти комментарии внутри корневого файла крейта (по соглашению src/lib.rs ) или внутри модуля для документирования крейта или модуля в целом. Например, чтобы добавить документацию, описывающую назначение my_crate , содержащего функцию add_one , мы добавляем комментарии к документации, начинающиеся с //! в начало файла src/lib.rs , как показано в листинге 14-2: Файл: src/lib.rs Doc-tests my_crate running 1 test test src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s Листинг 14-2: Документация для крейта my_crate в целом Обратите внимание, что после последней строки, начинающейся с //! , нет никакого кода. Поскольку мы начали комментарии с //! вместо /// , мы документируем элемент, который содержит этот комментарий, а не элемент, который следует за этим комментарием. В данном случае таким элементом является файл src/lib.rs, который является корнем crate. Эти комментарии описывают весь крейт. Когда мы запускаем cargo doc --open , эти комментарии будут отображаться на первой странице документации для my_crate над списком публичных элементов в библиотеке, как показано на рисунке 14-2: |