Document not found (404)
-This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -diff --git a/.gitignore b/.gitignore index 81650e309..2ab52d776 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,174 @@ *.sln *.sw? ошибки.txt +rustbook-ru/book/tomorrow-night.css +rustbook-ru/book/title-page.html +rustbook-ru/book/theme/2018-edition.css +rustbook-ru/book/searchindex.json +rustbook-ru/book/searchindex.js +rustbook-ru/book/searcher.js +rustbook-ru/book/print.html +rustbook-ru/book/mark.min.js +rustbook-ru/book/index.html +rustbook-ru/book/img/trpl20-01.png +rustbook-ru/book/img/trpl15-04.svg +rustbook-ru/book/img/trpl15-03.svg +rustbook-ru/book/img/trpl15-02.svg +rustbook-ru/book/img/trpl15-01.svg +rustbook-ru/book/img/trpl14-10.png +rustbook-ru/book/img/trpl14-07.png +rustbook-ru/book/img/trpl14-05.png +rustbook-ru/book/img/trpl14-04.png +rustbook-ru/book/img/trpl14-03.png +rustbook-ru/book/img/trpl14-02.png +rustbook-ru/book/img/trpl14-01.png +rustbook-ru/book/img/trpl04-06.svg +rustbook-ru/book/img/trpl04-05.svg +rustbook-ru/book/img/trpl04-04.svg +rustbook-ru/book/img/trpl04-03.svg +rustbook-ru/book/img/trpl04-02.svg +rustbook-ru/book/img/trpl04-01.svg +rustbook-ru/book/img/ferris/unsafe.svg +rustbook-ru/book/img/ferris/panics.svg +rustbook-ru/book/img/ferris/not_desired_behavior.svg +rustbook-ru/book/img/ferris/does_not_compile.svg +rustbook-ru/book/highlight.js +rustbook-ru/book/highlight.css +rustbook-ru/book/foreword.html +rustbook-ru/book/fonts/source-code-pro-v11-all-charsets-500.woff2 +rustbook-ru/book/fonts/SOURCE-CODE-PRO-LICENSE.txt +rustbook-ru/book/fonts/open-sans-v17-all-charsets-regular.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-italic.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-800italic.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-800.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-700italic.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-700.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-600italic.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-600.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-300italic.woff2 +rustbook-ru/book/fonts/open-sans-v17-all-charsets-300.woff2 +rustbook-ru/book/fonts/OPEN-SANS-LICENSE.txt +rustbook-ru/book/fonts/fonts.css +rustbook-ru/book/FontAwesome/fonts/FontAwesome.ttf +rustbook-ru/book/FontAwesome/fonts/fontawesome-webfont.woff2 +rustbook-ru/book/FontAwesome/fonts/fontawesome-webfont.woff +rustbook-ru/book/FontAwesome/fonts/fontawesome-webfont.ttf +rustbook-ru/book/FontAwesome/fonts/fontawesome-webfont.svg +rustbook-ru/book/FontAwesome/fonts/fontawesome-webfont.eot +rustbook-ru/book/FontAwesome/css/font-awesome.css +rustbook-ru/book/ferris.js +rustbook-ru/book/ferris.css +rustbook-ru/book/favicon.svg +rustbook-ru/book/favicon.png +rustbook-ru/book/elasticlunr.min.js +rustbook-ru/book/css/variables.css +rustbook-ru/book/css/print.css +rustbook-ru/book/css/general.css +rustbook-ru/book/css/chrome.css +rustbook-ru/book/clipboard.min.js +rustbook-ru/book/ch20-03-graceful-shutdown-and-cleanup.html +rustbook-ru/book/ch20-02-multithreaded.html +rustbook-ru/book/ch20-01-single-threaded.html +rustbook-ru/book/ch20-00-final-project-a-web-server.html +rustbook-ru/book/ch19-06-macros.html +rustbook-ru/book/ch19-05-advanced-functions-and-closures.html +rustbook-ru/book/ch19-04-advanced-types.html +rustbook-ru/book/ch19-03-advanced-traits.html +rustbook-ru/book/ch19-01-unsafe-rust.html +rustbook-ru/book/ch19-00-advanced-features.html +rustbook-ru/book/ch18-03-pattern-syntax.html +rustbook-ru/book/ch18-02-refutability.html +rustbook-ru/book/ch18-01-all-the-places-for-patterns.html +rustbook-ru/book/ch18-00-patterns.html +rustbook-ru/book/ch17-03-oo-design-patterns.html +rustbook-ru/book/ch17-02-trait-objects.html +rustbook-ru/book/ch17-01-what-is-oo.html +rustbook-ru/book/ch17-00-oop.html +rustbook-ru/book/ch16-04-extensible-concurrency-sync-and-send.html +rustbook-ru/book/ch16-03-shared-state.html +rustbook-ru/book/ch16-02-message-passing.html +rustbook-ru/book/ch16-01-threads.html +rustbook-ru/book/ch16-00-concurrency.html +rustbook-ru/book/ch15-06-reference-cycles.html +rustbook-ru/book/ch15-05-interior-mutability.html +rustbook-ru/book/ch15-04-rc.html +rustbook-ru/book/ch15-03-drop.html +rustbook-ru/book/ch15-02-deref.html +rustbook-ru/book/ch15-01-box.html +rustbook-ru/book/ch15-00-smart-pointers.html +rustbook-ru/book/ch14-05-extending-cargo.html +rustbook-ru/book/ch14-04-installing-binaries.html +rustbook-ru/book/ch14-03-cargo-workspaces.html +rustbook-ru/book/ch14-02-publishing-to-crates-io.html +rustbook-ru/book/ch14-01-release-profiles.html +rustbook-ru/book/ch14-00-more-about-cargo.html +rustbook-ru/book/ch13-04-performance.html +rustbook-ru/book/ch13-03-improving-our-io-project.html +rustbook-ru/book/ch13-02-iterators.html +rustbook-ru/book/ch13-01-closures.html +rustbook-ru/book/ch13-00-functional-features.html +rustbook-ru/book/ch12-06-writing-to-stderr-instead-of-stdout.html +rustbook-ru/book/ch12-05-working-with-environment-variables.html +rustbook-ru/book/ch12-04-testing-the-librarys-functionality.html +rustbook-ru/book/ch12-03-improving-error-handling-and-modularity.html +rustbook-ru/book/ch12-02-reading-a-file.html +rustbook-ru/book/ch12-01-accepting-command-line-arguments.html +rustbook-ru/book/ch12-00-an-io-project.html +rustbook-ru/book/ch11-03-test-organization.html +rustbook-ru/book/ch11-02-running-tests.html +rustbook-ru/book/ch11-01-writing-tests.html +rustbook-ru/book/ch11-00-testing.html +rustbook-ru/book/ch10-03-lifetime-syntax.html +rustbook-ru/book/ch10-02-traits.html +rustbook-ru/book/ch10-01-syntax.html +rustbook-ru/book/ch10-00-generics.html +rustbook-ru/book/ch09-03-to-panic-or-not-to-panic.html +rustbook-ru/book/ch09-02-recoverable-errors-with-result.html +rustbook-ru/book/ch09-01-unrecoverable-errors-with-panic.html +rustbook-ru/book/ch09-00-error-handling.html +rustbook-ru/book/ch08-03-hash-maps.html +rustbook-ru/book/ch08-02-strings.html +rustbook-ru/book/ch08-01-vectors.html +rustbook-ru/book/ch08-00-common-collections.html +rustbook-ru/book/ch07-05-separating-modules-into-different-files.html +rustbook-ru/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html +rustbook-ru/book/ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html +rustbook-ru/book/ch07-02-defining-modules-to-control-scope-and-privacy.html +rustbook-ru/book/ch07-01-packages-and-crates.html +rustbook-ru/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html +rustbook-ru/book/ch06-03-if-let.html +rustbook-ru/book/ch06-02-match.html +rustbook-ru/book/ch06-01-defining-an-enum.html +rustbook-ru/book/ch06-00-enums.html +rustbook-ru/book/ch05-03-method-syntax.html +rustbook-ru/book/ch05-02-example-structs.html +rustbook-ru/book/ch05-01-defining-structs.html +rustbook-ru/book/ch05-00-structs.html +rustbook-ru/book/ch04-03-slices.html +rustbook-ru/book/ch04-02-references-and-borrowing.html +rustbook-ru/book/ch04-01-what-is-ownership.html +rustbook-ru/book/ch04-00-understanding-ownership.html +rustbook-ru/book/ch03-05-control-flow.html +rustbook-ru/book/ch03-04-comments.html +rustbook-ru/book/ch03-03-how-functions-work.html +rustbook-ru/book/ch03-02-data-types.html +rustbook-ru/book/ch03-01-variables-and-mutability.html +rustbook-ru/book/ch03-00-common-programming-concepts.html +rustbook-ru/book/ch02-00-guessing-game-tutorial.html +rustbook-ru/book/ch01-03-hello-cargo.html +rustbook-ru/book/ch01-02-hello-world.html +rustbook-ru/book/ch01-01-installation.html +rustbook-ru/book/ch01-00-getting-started.html +rustbook-ru/book/ch00-00-introduction.html +rustbook-ru/book/book.js +rustbook-ru/book/ayu-highlight.css +rustbook-ru/book/appendix-07-nightly-rust.html +rustbook-ru/book/appendix-06-translation.html +rustbook-ru/book/appendix-05-editions.html +rustbook-ru/book/appendix-04-useful-development-tools.html +rustbook-ru/book/appendix-03-derivable-traits.html +rustbook-ru/book/appendix-02-operators.html +rustbook-ru/book/appendix-01-keywords.html +rustbook-ru/book/appendix-00.html +rustbook-ru/book/404.html +rustbook-ru/book/.nojekyll diff --git a/rustbook-ru/README.md b/rustbook-ru/README.md index e62988b09..ad4e6a864 100644 --- a/rustbook-ru/README.md +++ b/rustbook-ru/README.md @@ -1,14 +1,14 @@ -# Язык программирования Rust +# Язык программирования Ржавчина -Данный хранилище содержит перевод второго издания “Язык программирования Rust”. +Данный хранилище содержит перевод второго издания “Язык программирования Ржавчина”. Второе издание - это переработанная книга "The Ржавчина Programming Language", которая будет напечатана издательством "No Starch Press" примерно в мае 2018 года. Последнюю сведения о дате выхода книги и о способе ее заказа вы можете узнать на сайте самого издательства [No Starch Press][nostarch]. [nostarch]: https://nostarch.com/rust -Книгу можно [читать онлайн](https://rustycrate.ru/book). +Книгу можно [читать в сети](https://rustycrate.ru/book). -Подлинник книги вы можете прочесть [онлайн][html]; несколько последних глав еще не закончены, но готовая часть книги заметно улучшена по сравнению с первым изданием. Авторы изначальной книги советуют начать чтение со второго издания. +Подлинник книги вы можете прочесть [в сети][html]; несколько последних глав еще не закончены, но готовая часть книги заметно улучшена по сравнению с первым изданием. Составители изначальной книги советуют начать чтение со второго издания. [html]: http://rust-lang.github.io/book/ @@ -25,13 +25,13 @@ $ cargo install mdbook ## Сборка Для того, чтобы собрать книгу, перейдите в нужный папка с помощью приказы cd - first-edition для первого, либо second-edition для второго издания. -Далее введите следующую приказ: +Далее введите следующий приказ: ```bash $ mdbook build ``` -Итоги выполнения приказы появятся в подпапке `book`. Для проверки откройте книгу в браузере. +Итоги выполнения приказы появятся в подпапке `book`. Для проверки откройте книгу в обозревателе. _Firefox:_ ```bash @@ -67,7 +67,7 @@ $ mdbook test [![ruRust/rust_book_ru](http://issuestats.com/github/ruRust/rust_book_ru/badge/issue?style=flat)](http://issuestats.com/github/ruRust/rust_book_ru) -# Соавторам +# Сосоставителям ## С чего начать @@ -79,7 +79,7 @@ $ mdbook test [Правила перевода](https://github.com/ruRust/rust_book_ru/wiki/Правила). -## Ресурсы +## Источники * первое издание rustbook расположено [здесь][original] * перевод первого издания расположен [здесь][rustbook] diff --git a/rustbook-ru/book.toml b/rustbook-ru/book.toml index 2fd9c8267..cbc05b1c5 100644 --- a/rustbook-ru/book.toml +++ b/rustbook-ru/book.toml @@ -1,6 +1,6 @@ [book] -title = "Язык программирования Rust" -author = "Стив Клабник, Кэрол Николс и другие участники сообщества Rust" +title = "Язык программирования Ржавчина" +author = "Стив Клабник, Кэрол Николс и другие участники сообщества Ржавчины" language = "ru-RU" [output.html] diff --git a/rustbook-ru/book/.nojekyll b/rustbook-ru/book/.nojekyll deleted file mode 100644 index f17311098..000000000 --- a/rustbook-ru/book/.nojekyll +++ /dev/null @@ -1 +0,0 @@ -This file makes sure that Github Pages doesn't process mdBook's output. diff --git a/rustbook-ru/book/404.html b/rustbook-ru/book/404.html deleted file mode 100644 index c9ac91689..000000000 --- a/rustbook-ru/book/404.html +++ /dev/null @@ -1,221 +0,0 @@ - - -
- - -This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -Следующие разделы содержат справочные источники, которые могут оказаться полезными в вашем путешествии по Rust.
- -Следующий список содержит ключевые слова, зарезервированные для текущего или будущего использования в языке Rust. Как таковые их нельзя использовать в качестве определителей (за исключением сырых определителей, которые мы обсудим в разделе «Сырые определители»). определительы — это имена функций, переменных, свойств, полей устройств, звеньев, ящиков, постоянных значений, макросов, постоянных значений, свойств, видов, свойств или времён жизни.
-Ниже приведён список используемых в настоящее время ключевых слов с их описанием.
-as
— выполнить простое преобразование, уточнить определенную свойство, которую содержит предмет, или переименовать элемент в выражении use
async
— возврат Future
вместо блокировки текущего потокаawait
— остановка выполнения до готовности итога Future
break
— немедленный выход из циклаconst
— определение постоянного элемента или неизменяемого сырого указателяcontinue
— досрочный переход к следующей повторения циклаcrate
— ссылка на корень дополнения в пути к звенуdyn
— изменяемая отсылка к особенности предметаelse
— иные ветви для устройств управления потока if
и if let
enum
— определение перечисленийextern
— связывание внешней функции или переменнойfalse
— логический ложный записьfn
— определение функции или вида указателя на функциюfor
— замкнуто перебирать элементы из повторителя, выполнить признак или указывать время жизни с более высоким рейтингом.if
— ветвление на основе итога условного выраженияimpl
— выполнение встроенной возможности или возможности особенностиin
— часть правил написания цикла for
let
— объявление (связывание) переменнойloop
— безусловный циклmatch
— сопоставление значения с образцамиmod
— определение звенаmove
— перекладывание владения на замыкание всеми захваченными элементамиmut
— обозначение изменчивости в ссылках, сырах указателей и привязках к образцуpub
— изменитель открытой доступность полей устройств, разделов impl
и звеньевref
— привязка по ссылкеreturn
— возвращает итог из функцииSelf
— псевдоним для определяемого или исполняемого видаself
— предмет текущего способа или звенаstatic
— вездесущая переменная или время жизни, продолжающееся на протяжении всего выполнения программыstruct
— определение устройстваsuper
— родительский звено текущего звенаtrait
— определение особенностиtrue
— логический истинный записьtype
— определение псевдонима вида или связанного видаunion
- определить объединение; является ключевым словом только при использовании в объявлении объединенияunsafe
— обозначение небезопасного кода, функций, особенностей и их выполненийuse
— ввод имён в область видимостиwhere
— ограничение видаwhile
— условный цикл, основанный на итоге выраженияСледующие ключевые слова ещё не имеют никакой возможности, но зарезервированы Ржавчина для возможного использования в будущем.
-abstract
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
Сырые определители — это правила написания, позволяющий использовать ключевые слова там, где обычно они не могут быть. Для создания и использования сырого определителя к ключевому слову добавляется приставка r#
.
Например, ключевое слово match
. Если вы попытаетесь собрать следующую функцию, использующую в качестве имени match
:
Файл: src/main.rs
-fn match(needle: &str, haystack: &str) -> bool {
- haystack.contains(needle)
-}
-вы получите ошибку:
-error: expected identifier, found keyword `match`
- --> src/main.rs:4:4
- |
-4 | fn match(needle: &str, haystack: &str) -> bool {
- | ^^^^^ expected identifier, found keyword
-
-Ошибка говорит о том, что вы не можете использовать ключевое слово match
в качестве определителя функции. Чтобы получить возможность использования слова match
в качестве имени функции, нужно использовать правила написания «сырых определителей», например так:
Файл: src/main.rs
--fn r#match(needle: &str, haystack: &str) -> bool { - haystack.contains(needle) -} - -fn main() { - assert!(r#match("foo", "foobar")); -}
Этот код собирается без ошибок. Обратите внимание, что приставка r#
в определении имени функции указан так же, как он указан в месте её вызова в main
.
Сырые определители позволяют вам использовать любое слово, которое вы выберете, в качестве определителя, даже если это слово окажется зарезервированным ключевым словом. Это даёт нам больше свободы в выборе имён определителей, а также позволяет нам встраиваться с программами, написанными на языке, где эти слова не являются ключевыми. Кроме того, необработанные определители позволяют вам использовать библиотеки, написанные в исполнения Rust, отличной от используемой в вашем ящике. Например, try
не является ключевым словом в выпуске 2015 года, но является в выпуске 2018 года. Если вы зависите от библиотеки, написанной с использованием исполнения 2015 года и имеющей функцию try
, вам потребуется использовать правила написания сырого определителя, в данном случае r#try
, для вызова этой функции из кода исполнения 2018 года. См. Приложение E для получения дополнительной сведений о изданиех Rust.
Это дополнение содержит глоссарий правил написания Rust, включая операторы и другие обозначения, которые появляются сами по себе или в среде путей, обобщений, особенностей, макросов, свойств, примечаниев, упорядоченных рядов и скобок.
-Таблица Б-1 содержит операторы языка Rust, пример появления оператора, короткое объяснение, возможность перегрузки оператора. Если оператор можно перегрузить, то показан особенность, с помощью которого его можно перегрузить.
--
Оператор | Пример | Объяснение | Перегружаемость |
---|---|---|---|
! | ident!(...) , ident!{...} , ident![...] | Вызов макроса | |
! | !expr | Побитовое или логическое отрицание | Not |
!= | expr != expr | Сравнение "не равно" | PartialEq |
% | expr % expr | Остаток от деления | Rem |
%= | var %= expr | Остаток от деления и присваивание | RemAssign |
& | &expr , &mut expr | Заимствование | |
& | &type , &mut type , &'a type , &'a mut type | Указывает что данный вид заимствуется | |
& | expr & expr | Побитовое И | BitAnd |
&= | var &= expr | Побитовое И и присваивание | BitAndAssign |
&& | expr && expr | Логическое И | |
* | expr * expr | Арифметическое умножение | Mul |
*= | var *= expr | Арифметическое умножение и присваивание | MulAssign |
* | *expr | Разыменование ссылки | Deref |
* | *const type , *mut type | Указывает, что данный вид является сырым указателем | |
+ | trait + trait , 'a + trait | Соединение ограничений вида | |
+ | expr + expr | Арифметическое сложение | Add |
+= | var += expr | Арифметическое сложение и присваивание | AddAssign |
, | expr, expr | Разделитель переменных и элементов | |
- | - expr | Арифметическое отрицание | Neg |
- | expr - expr | Арифметическое вычитание | Sub |
- | var -= expr | Арифметическое вычитание и присваивание | SubAssign |
-> | fn(...) -> type , |...| -> type | ... | |
. | expr.ident | Доступ к элементу | |
.. | .. , expr.. , ..expr , expr..expr | Указывает на рядчисел, исключая правый | PartialOrd |
..= | ..=expr , expr..=expr | Указывает на рядчисел, включая правый | PartialOrd |
.. | ..expr | правила написания обновления устройства | |
.. | variant(x, ..) , struct_type { x, .. } | Привязка «И все остальное» | |
... | expr...expr | (Устарело, используйте новый правила написания ..= ) Используется при определении инклюзивного ряда | |
/ | expr / expr | Арифметическое деление | Div |
/= | var /= expr | Арифметическое деление и присваивание | DivAssign |
: | pat: type , ident: type | Ограничения видов | |
: | ident: expr | Объявление поля устройства | |
: | 'a: loop {...} | Метка цикла | |
; | expr; | Признак конца указания и элемента | |
; | [...; len] | Часть правил написания массива конечного размера | |
<< | expr << expr | Битовый сдвиг влево | Shl |
<<= | var <<= expr | Битовый сдвиг влево и присваивание | ShlAssign |
< | expr < expr | Сравнение "меньше чем" | PartialOrd |
<= | expr <= expr | Сравнение "меньше или равно" | PartialOrd |
= | var = expr , ident = type | Присваивание/эквивалентность | |
== | expr == expr | Сравнение "равно" | PartialEq |
=> | pat => expr | Часть правил написания устройства match | |
> | expr > expr | Сравнение "больше чем" | PartialOrd |
>= | expr >= expr | Сравнение "больше или равно" | PartialOrd |
>> | expr >> expr | Битовый сдвиг вправо | Shr |
>>= | var >>= expr | Битовый сдвиг вправо и присваивание | ShrAssign |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Побитовое исключающее ИЛИ | BitXor |
^= | var ^= expr | Побитовое исключающее ИЛИ и присваивание | BitXorAssign |
| | pat | pat | Иные образцы | |
| | expr | expr | Побитовое ИЛИ | BitOr |
|= | var |= expr | Побитовое ИЛИ и присваивание | BitOrAssign |
|| | expr || expr | Короткое логическое ИЛИ | |
? | expr? | Возврат ошибки |
Следующий список содержит все символы, которые не работают как операторы; то есть они не ведут себя как вызов функции или способа.
-Таблица Б-2 показывает символы, которые появляются сами по себе и допустимы в различных местах.
--
Обозначение | Объяснение |
---|---|
'ident | Именованное время жизни или метка цикла |
...u8 , ...i32 , ...f64 , ...usize , etc. | Числовой запись определённого вида |
"..." | Строковый запись |
r"..." , r#"..."# , r##"..."## , etc. | Необработанный строковый запись, в котором не обрабатываются escape-символы |
b"..." | Строковый запись байтов; создаёт массив байтов вместо строки |
br"..." , br#"..."# , br##"..."## , etc. | Необработанный строковый байтовый запись, сочетание необработанного и байтового записи |
'...' | Символьный запись |
b'...' | ASCII байтовый запись |
|...| expr | Замыкание |
! | Всегда пустой вид для расходящихся функций |
_ | «Пренебрегаемое» связывание образцов; также используется для читабельности целочисленных записей |
Таблица Б-3 показывает обозначения которые появляются в среде путей упорядочевания звеньев
--
Обозначение | Объяснение |
---|---|
ident::ident | Путь к пространству имён |
::path | Путь относительно корня ящика (т. е. явный абсолютный путь) |
self::path | Путь относительно текущего звена (т. е. явный относительный путь). |
super::path | Путь относительно родительского звена текущего звена |
type::ident , <type as trait>::ident | Сопряженные постоянные значения, функции и виды |
<type>::... | Сопряженный элемент для вида, который не может быть назван прямо (например <&T>::... , <[T]>::... , etc.) |
trait::method(...) | Устранение неоднозначности вызова способа путём именования особенности, который определяет его |
type::method(...) | Устранение неоднозначности путём вызова способа через имя вида, для которого он определён |
<type as trait>::method(...) | Устранение неоднозначности вызова способа путём именования особенности и вида |
Таблица Б-4 показывает обозначения которые появляются в среде использования обобщённых видов свойств
--
Обозначение | Объяснение |
---|---|
path<...> | Определяет свойства для обобщённых свойств в виде (e.g., Vec<u8> ) |
path::<...> , method::<...> | Определяет свойства для обобщённых свойств, функций, или способов в выражении. Часто называют turbofish (например "42".parse::<i32>() ) |
fn ident<...> ... | Определение обобщённой функции |
struct ident<...> ... | Определение обобщённой устройства |
enum ident<...> ... | Объявление обобщённого перечисления |
impl<...> ... | Определение обобщённой выполнения |
for<...> type | Высокоуровневое связывание времени жизни |
type<ident=type> | Обобщённый вид где один или более сопряженных видов имеют определённое присваивание (например Iterator<Item=T> ) |
Таблица Б-5 показывает обозначения которые появляются в среде использования обобщённых видов свойств с ограничениями видов
--
Обозначение | Объяснение |
---|---|
T: U | Обобщённый свойство T ограничивается до видов которые выполняют особенность U |
T: 'a | Обобщённый вид T должен существовать не меньше чем 'a (то есть вид не может иметь ссылки с временем жизни меньше чем 'a ) |
T: 'static | Обобщённый вид T не имеет заимствованных ссылок кроме имеющих время жизни 'static |
'b: 'a | Обобщённое время жизни 'b должно быть не меньше чем 'a |
T: ?Sized | Позволяет обобщённым видам свойства иметь изменяемый размер |
'a + trait , trait + trait | Соединение ограничений видов |
Таблица Б-6 показывает обозначения, которые появляются в среде вызова или определения макросов и указания свойств элемента.
--
Обозначение | Объяснение |
---|---|
#[meta] | Внешний свойство |
#![meta] | Внутренний свойство |
$ident | Подстановка в макросе |
$ident:kind | Захват макроса |
$(…)… | Повторение макроса |
ident!(...) , ident!{...} , ident![...] | Вызов макроса |
Таблица Б-7 показывает обозначения, которые создают примечания.
--
Обозначение | Объяснение |
---|---|
// | Однострочный примечание |
//! | Внутренний однострочный примечание документации |
/// | Внешний однострочный примечание документации |
/*...*/ | Многострочный примечание |
/*!...*/ | Внутренний многострочный примечание документации |
/**...*/ | Внешний многострочный примечание документации |
Таблица Б-8 показывает обозначения, которые появляются в среде использования упорядоченных рядов.
--
Обозначение | Объяснение |
---|---|
() | Пустой упорядоченный ряд, он же пустой вид. И запись и вид. |
(expr) | Выражение в скобках |
(expr,) | Упорядоченный ряд с одним элементом выражения |
(type,) | Упорядоченный ряд с одним элементом вида |
(expr, ...) | Выражение упорядоченного ряда |
(type, ...) | Вид упорядоченного ряда |
(type, ...) | Выражение вызова функции; также используется для объявления устройств-упорядоченных рядов и исходов-упорядоченных рядов перечисления |
expr.0 , expr.1 , etc. | Взятие элемента по порядковому указателю в упорядоченном ряде |
Таблица Б-9 показывает среды, в которых используются фигурные скобки.
--
Среда | Объяснение |
---|---|
{...} | Выражение раздела |
Type {...} | struct запись |
Таблица Б-10 показывает среды, в которых используются квадратные скобки.
--
Среда | Объяснение |
---|---|
[...] | Запись массива |
[expr; len] | Запись массива, содержащий len повторов expr |
[type; len] | Массив, содержащий len образцов вида type |
expr[expr] | Взятие по порядковому указателю в собрания. Возможна перегрузка (Index , IndexMut ) |
expr[..] , expr[a..] , expr[..b] , expr[a..b] | Взятие среза собрания по порядковому указателю, используется Range , RangeFrom , RangeTo , или RangeFull как "порядковый указатель" |
Во многих частях книги мы обсуждали свойство derive
, которые Вы могли применить к объявлению устройства или перечисления. Свойство derive
порождает код по умолчанию для выполнения особенности, который вы указали в derive
.
В этом дополнении, мы расскажем про все особенности, которые вы можете использовать в свойстве derive
. Каждая раздел содержит:
derive
Если Вам понадобилось поведение отличное от поведения при выполнения через derive
, обратитесь к документации по встроенной библиотеке чтобы узнать как вручную выполнить особенность.
Перечисленные здесь особенности являются единственными, определёнными встроенной библиотекой, которые могут быть выполнены в ваших видах с помощью derive
. Другие особенности, определённые в встроенной библиотеке, не имеют ощутимого поведения по умолчанию, поэтому вам решать, как выполнить их для достижения ваших целей.
Пример особенности, который нельзя выполнить через derive - Display
, который обрабатывает изменение
-для конечных пользователей. Вы всегда должны сами рассмотреть лучший способ для отображения вида конечному пользователю. Какие части вида должны быть разрешены для просмотра конечному пользователю? Какие части они найдут подходящими? Какой вид вывода для них будет самым подходящим? Сборщик Ржавчина не знает ответы на эти вопросы, поэтому он не может подобрать подходящее обычное поведение.
Список видов, выполняемых через derive, в этом дополнении не является исчерпывающим: библиотеки могут выполнить derive
для их собственных особенностей, составляя свои списки особенностей, которые Вы можете использовать с помощью derive
. Выполнение derive
включает в себя использование процедурных макросов, которые были рассмотрены в разделе "Макросы" главы 19.
Debug
для отладочного выводаОсобенность Debug
включает отладочное изменение
-в изменяемых строках, которые вы можете указать с помощью :?
внутри {}
фигурных скобок.
Особенность Debug
позволяет Вам напечатать предметы вида с целью отладки, поэтому Вы и другие программисты, использующие Ваш вид, смогут проверить предмет в определённой точке выполнения программы.
Особенность Debug
обязателен в некоторых случаях. Например, при использовании макроса assert_eq!
. Этот макрос печатает значения входных переменных, если они не совпадают. Это позволяет программистам увидеть, почему эти предметы не равны.
PartialEq
и Eq
для сравнения равенстваОсобенность PartialEq
позволяет Вам сравнить предметы одного вида на эквивалентность, и включает для них использование операторов ==
и !=
.
Использование PartialEq
выполняет способ eq
. Когда PartialEq
используют для устройства, два предмета равны если равны все поля предметов, и предметы не равны, если хотя бы одно поле отлично. Когда используется для перечислений, каждый исход равен себе, и не равен другим исходам.
Особенность PartialEq
обязателен в некоторых случаях. Например для макроса assert_eq!
, где необходимо сравнивать два предмета одного вида на эквивалентность.
Особенность Eq
не имеет способов. Он указывает что каждое значение определеного вида равно самому себе. Особенность Eq
может быть применён только для видов выполняющих особенность PartialEq
, хотя не все виды, которые выполняют PartialEq
могут выполнить Eq
. Примером являются числа с плавающей запятой: выполнение чисел с плавающей запятой говорит, что два образца со значениями не-число (NaN
) не равны друг другу.
Особенность Eq
необходим в некоторых случаях. Например, для ключей в HashMap<K, V>
. Поэтому HashMap<K, V>
может сказать, что два ключа являются одним и тем же.
PartialOrd
и Ord
для сравнения порядкаОсобенность PartialOrd
позволяет Вам сравнить предметы одного вида с помощью сортировки. Вид, выполняющий PartialOrd
может использоваться с операторами <
, >
, <=
, и >=
. Вы можете выполнить особенность PartialOrd
только для видов, выполняющих PartialEq
.
Использование PartialOrd
выполняет способ partial_cmp
, который возвращает Option<Ordering>
который является None
когда значения не выстраивают порядок. Примером значения, которое не может быть упорядочено, не являются числом (NaN
) значение с плавающей запятой. Вызов partial_cmp
с любым числом с плавающей запятой и значением NaN
вернёт None
.
Когда используется для устройств, PartialOrd
сравнивает два предмета путём сравнения значений каждого поля в порядке, в котором поля объявлены в устройстве. Когда используется для перечислений, то исходы перечисления объявленные ранее будут меньше чем исходы объявленные позже.
Например, особенность PartialOrd
может потребоваться для способа gen_range
из rand
ящика который порождает случайные значения в заданном ряде (который определён выражением ряда).
Особенность Ord
позволяет знать, для двух значений определеного вида всегда будет существовать валидный порядок. Особенность Ord
выполняет способ cmp
, который возвращает Ordering
а не Option<Ordering>
потому что валидный порядок всегда будет существовать. Вы можете применить особенность Ord
только для видов, выполняющих особенность PartialOrd
и Eq
(Eq
также требует PartialEq
). При использовании на устройствах или перечислениях, cmp
имеет такое же поведение, как и partial_cmp
вPartialOrd
.
Особенность Ord
необходим в некоторых случаях. Например, сохранение значений в BTreeSet<T>
, виде данных, который хранит сведения на основе порядка отсортированных данных.
Clone
и Copy
для повторения значенийОсобенность Clone
позволяет вам явно создать глубокую повтор значения, а также этап повторения может вызывать особый код и воспроизводить данные с кучи. Более подробно про Clone
смотрите в разделы "Способы взаимодействия переменных и данных: клонирование" в разделе 4.
Использование Clone
выполняет способ clone
, который в случае выполнения на всем виде, вызывает clone
для каждой части данных вида. Это подразумевает, что все поля или значения в виде также должны выполнить Clone
для использования Clone
.
Особенность Clone
необходим в некоторых случаях. Например, для вызова способа to_vec
для среза. Срез не владеет данными, содержащимися в нем, но вектор значений, возвращённый из to_vec
должен владеть этими предметами, поэтому to_vec
вызывает clone
для всех данных. Таким образом, вид хранящийся в срезе, должен выполнить Clone
.
Особенность Copy
позволяет повторять значения повторяя только данные, которые хранятся на обойме, произвольный код не требуется. Смотрите раздел "Из обоймы данные: Повторение" в разделе 4 для большей сведений о Copy
.
Особенность Copy
не содержит способов для предотвращения перегрузки этих способов программистами, иначе бы это нарушило соглашение, что никакой произвольный код не запускается. Таким образом все программисты могут предполагать, что повторение значений будет происходить быстро.
Вы можете вывести Copy
для любого вида все части которого выполняют Copy
. Вид который выполняет Copy
должен также выполнить Clone
, потому что вид выполняющий Copy
имеет обыкновенную выполнение Clone
который выполняет ту же задачу, что и Copy
.
Особенность Copy
нужен очень редко; виды, выполняющие Copy
имеют небольшую переработку, то есть для него не нужно вызывать способ clone
, который делает код более кратким.
Все, что вы делаете с Copy
можно также делать и с Clone
, но код может быть медленнее и требовать вызов способа clone
в некоторых местах.
Hash
для превращения значения в значение конечного размераОсобенность Hash
позволяет превратить значение произвольного размера в значение конечного размера с использованием хеш-функции. Использование Hash
выполняет способ hash
. При выполнения через derive, способ hash
сочетает итоги вызова hash
на каждой части данных вида, то есть все поля или значения должны выполнить Hash
для использования Hash
с помощью derive.
Особенность Hash
необходим в некоторых случаях. Например, для хранения ключей в HashMap<K, V>
, для их более эффективного хранения.
Default
для значений по умолчаниюОсобенность Default
позволяет создавать значение по умолчанию для вида. Использование Default
выполняет функцию default
. Обычная выполнение способа default
вызовет функцию default
на каждой части данных вида, то есть для использования Default
через derive, все поля и значения вида данных должны также выполнить Default
.
Функция Default::default
часто используется в сочетания с правилами написания обновления устройства, который мы обсуждали в разделы "Создание образца устройства из образца другой устройства с помощью правил написания обновления устройства" главы 5. Вы можете настроить несколько полей для устройства, а для остальных полей установить значения с помощью ..Default::default()
.
Особенность Default
необходим в некоторых случаях. Например, для способа unwrap_or_default
у вида Option<T>
. Если значение Option<T>
будет None
, способ unwrap_or_default
вернёт итог вызова функции Default::default
для вида T
, хранящегося в Option<T>
.
В этом дополнении мы расскажем про часто используемые средства разработки, предоставляемые Rust. Мы рассмотрим самостоятельное изменение -, быстрый путь исправления предупреждений, линтер, и встраивание с IDE.
-с rustfmt
Средство rustfmt
переделает ваш код в соответствии со исполнением кода сообщества. Многие совместные дела используют rustfmt
, чтобы предотвратить споры о том, какой исполнение использовать при написании Rust: все изменяют свой код с помощью этого средства.
Для установки rustfmt
, введите следующее:
$ rustup component add rustfmt
-
-Этот приказ установит rustfmt
и cargo-fmt
, также как Ржавчина даёт Вам одновременно rustc
и cargo
. Для изменения дела, использующего Cargo, введите следующее:
$ cargo fmt
-
-Этот приказ изменит весь код на языке Ржавчина в текущем ящике. Будет изменён только исполнение кода, смысл останется прежней. Для большей сведений о rustfmt
, смотрите документацию.
rustfix
Средство rustfix включён в установку Ржавчина и может самостоятельно исправлять предупреждения сборщика с очевидным способом исправления сбоев, скорее всего, подходящим вам. Вероятно, вы уже видели предупреждения сборщика. Например, рассмотрим этот код:
-Файл: src/main.rs
--fn do_something() {} - -fn main() { - for i in 0..100 { - do_something(); - } -}
Мы вызываем функцию do_something
100 раз, но никогда не используем переменную i
в теле цикла for
. Ржавчина предупреждает нас об этом:
$ cargo build
- Compiling myprogram v0.1.0 (file:///projects/myprogram)
-warning: unused variable: `i`
- --> src/main.rs:4:9
- |
-4 | for i in 0..100 {
- | ^ help: consider using `_i` instead
- |
- = note: #[warn(unused_variables)] on by default
-
- Finished dev [unoptimized + debuginfo] target(s) in 0.50s
-
-Предупреждение предлагает нам использовать _i
как имя переменной: нижнее подчёркивание в начале определителя предполагает, что мы его не используем. Мы можем самостоятельно применить это предположение с помощью rustfix
, запустив приказ cargo fix
:
$ cargo fix
- Checking myprogram v0.1.0 (file:///projects/myprogram)
- Fixing src/main.rs (1 fix)
- Finished dev [unoptimized + debuginfo] target(s) in 0.59s
-
-Когда посмотрим в src/main.rs снова, мы увидим что cargo fix
изменил наш код:
Файл: src/main.rs
--fn do_something() {} - -fn main() { - for _i in 0..100 { - do_something(); - } -}
Переменная цикла for
теперь носит имя _i
, и предупреждение больше не появляется.
Также Вы можете использовать приказ cargo fix
для перемещения вашего кода между различными изданиеми Rust. Издания будут рассмотрены в дополнении Д.
Средство Clippy является собранием проверок (lints) для анализа Вашего кода, поэтому Вы можете найти простые ошибки и улучшить ваш Ржавчина код.
-Для установки Clippy, введите следующее:
-$ rustup component add clippy
-
-Для запуска проверок Clippy’s для дела Cargo, введите следующее:
-$ cargo clippy
-
-Например, скажем что Вы хотите написать программу, в которой будет использоваться приближенная математическая постоянное значение, такая как число Пи, как в следующей программе:
-Файл: src/main.rs
--fn main() { - let x = 3.1415; - let r = 8.0; - println!("the area of the circle is {}", x * r * r); -}
Запуск cargo clippy
для этого дела вызовет следующую ошибку:
error: approximate value of `f{32, 64}::consts::PI` found
- --> src/main.rs:2:13
- |
-2 | let x = 3.1415;
- | ^^^^^^
- |
- = note: `#[deny(clippy::approx_constant)]` on by default
- = help: consider using the constant directly
- = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
-
-Эта ошибка сообщает вам, что в Ржавчина уже определена более точная постоянное значение PI
, и что ваша программа будет более правильной, если вы вместо неё будете использовать эту постоянное значение. Затем вы должны изменить свой код, чтобы использовать постоянное значение PI
. Следующий код не приводит к ошибкам или предупреждениям от Clippy:
Файл: src/main.rs
--fn main() { - let x = std::f64::consts::PI; - let r = 8.0; - println!("the area of the circle is {}", x * r * r); -}
Для большей сведений о Clippy смотрите документацию.
-rust-analyzer
Чтобы облегчить встраивание с IDE, сообщество Ржавчина советует использовать rust-analyzer
. Этот средство представляет собой набор направленных на сборщик утилит, которые используют Language Server Protocol, который является сводом требований для взаимодействия IDE и языков программирования друг с другом. Разные клиенты могут использовать rust-analyzer
, например подключаемый звено анализатора Ржавчина для Visual Studio Code.
Посетите домашнюю страницу дела rust-analyzer
для получения указаний по установке, затем установите поддержку языкового сервера в именно среде IDE. Ваша IDE получит такие возможности, как автозаполнение, переход к определению и встроенные ошибки.
В главе 1, можно увидеть, что приказ cargo new
добавляет некоторые мета-данные о издания языка в файл Cargo.toml. Данное приложение рассказывает, что они означают.
Язык Ржавчина и его сборщик имеют шестинедельный цикл выпуска, означающий, что пользователи постоянно получают новые функции. В других языках обычно выпускают большие обновления, но редко. Объединение Ржавчина выпускает меньшие обновления, но более часто. Через некоторое время все эти небольшие изменения накапливаются. Между исполнениями обычно сложно оглянуться назад и сказать "Ого, язык сильно изменился между исполнениями Ржавчина 1.10 и Ржавчина 1.31!"
-Каждые два или три года, объединение Ржавчина выпускает новую издание языка (Rust edition). Каждая издание объединяет все новые особенности, которые попали в язык с новыми дополнениями, с полной, обновлённой документацией и набором средств. Новые издания поставляются как часть шестинедельного этапа исполнений.
-Для разных людей издания служат разным целям:
-На мгновение написания доступны две издания Rust: Ржавчина 2015 и Ржавчина 2018. Данная книга написана с использованием идиом издания Ржавчина 2018.
-Ключ edition
в настроечном файле Cargo.toml отображает, какую издание сборщик должен использовать для вашего кода. Если ключа нет, то для обратной совместимости сборщик Ржавчина использует издание 2015
.
Любой дело может выбрать издание отличную от издания по умолчанию, которая равна 2015. Издания могут содержать несовместимые изменения, включая новые ключевые слова, которые могут враждовать с определителями в коде. Однако, пока вы не переключитесь на новую издание, ваш код будет продолжать собираться даже после обновления используемой исполнения сборщика.
-Все исполнения сборщика Ржавчина поддерживают любую издание, которая предшествовала выпуску текущей, и они могут линковать дополнения любой поддерживаемой издания. Изменения изданий действуют только на способ начального разбора сборщиком исходного кода. Поэтому, если вы используете 2015 издание, а одна из ваших зависимостей использует 2018, ваш дело будет собран и сможет пользоваться этой зависимостью. Обратная случаей, когда ваш дело использует Ржавчина 2018, а зависимость использует Ржавчина 2015, работает таким же образом.
-Внесём ясность: большая часть возможностей будет доступна во всех изданиях. Разработчики, использующие любую издание Rust, будут продолжать получать улучшения по мере выпуска новых исполнений. Однако в некоторых случаях, в основном, когда добавляются новые ключевые слова, некоторые новые возможности могут быть доступны только в последних изданиях. Нужно переключить издание, чтобы воспользоваться новыми возможностями.
-Для получения больше подробностей, есть полная книга Edition Guide про издания, в которой перечисляются различия между изданиями и объясняется, как самостоятельно обновить свой код на новую издание с помощью приказы cargo fix
.
Для ресурсов на языках, отличных от английского. Большинство из них все ещё в разработке; см. ярлык «Переводы», чтобы помочь или сообщить нам о новом переводе!
- - -Это дополнение рассказывает как создаётся Rust, и как это влияет на Вас как на разработчика.
-Как язык, Ржавчина много заботиться о безотказности Вашего кода. Мы хотим чтобы Ржавчина был прочным фундаментом, вашей опорой, и если бы все постоянно менялось, это было бы невозможно. В то же время, если мы не можем экспериментировать с различными возможностями, мы не можем обнаружить важные сбоев до исполнения, когда мы не можем их изменить.
-Нашим решением сбоев является “безотказность без стагнации”, и наш руководящий принцип: Вы никогда не должны бояться перехода на новую безотказную исполнение Rust. Каждое обновление должно быть безболезненным, но также должно добавлять новые функции, меньше дефектов и более быструю скорость сборки.
-Разработка языка Ржавчина работает по принципу расписания поездов. То есть, вся разработка совершается в ветке master
Ржавчина хранилища. Выпуски следуют подходы последовательного выпуска продукта (software release train), которая была использована Cisco IOS и другими программными продуктами. Есть три потока выпуска Rust:
Большинство Ржавчина разработчиков используют безотказную исполнение, но те кто хотят попробовать экспериментальные новые функции, должны использовать Nightly или Beta.
-Приведём пример, как работает этап разработки и выпуска новых исполнений. Давайте предположим, что объединение Ржавчина работает над исполнением Ржавчина 1.5. Его исполнение состоялся в декабре 2015 года, но это даст существующегостичность номера исполнения. Была добавлена новая возможность в Rust: новые изменения в ветку master
. Каждую ночь выпускается новая ночная исполнение Rust. Каждый день является днём выпуска ночной исполнения и эти выпуски создаются нашей устройством самостоятельно . По мере того как идёт время, наши выпуски выглядят так:
nightly: * - - * - - *
-
-Каждые шесть недель наступает время подготовки новой Beta исполнения! Ветка beta
Ржавчина хранилища ответвляется от ветки master
, используемой исполнением Nightly. Теперь мы имеем два выпуска:
nightly: * - - * - - *
- |
-beta: *
-
-Многие пользователи Ржавчина не используют активно бета-исполнение, но проверяют бета-исполнение в их системе CI для помощи Ржавчина обнаружить сбоев обратной совместимости. В это время каждую ночь выпускается новая исполнение Nightly:
-nightly: * - - * - - * - - * - - *
- |
-beta: *
-
-Предположим, что была найдена отступление. Хорошо, что мы можем проверять бета-исполнение перед тем как отступление попала в безотказную исполнение! Исправление отправляется в ветку master
, поэтому исполнение nightly исправлена и затем исправление также направляется в ветку beta
, и происходит новый выпуск бета-исполнения:
nightly: * - - * - - * - - * - - * - - *
- |
-beta: * - - - - - - - - *
-
-Через шесть недель после выпуска бета-исполнения, наступает время для выпуска безотказной исполнения! Ветка stable
создаётся из ветки beta
:
nightly: * - - * - - * - - * - - * - - * - * - *
- |
-beta: * - - - - - - - - *
- |
-stable: *
-
-Ура! Ржавчина 1.5 выпущена! Но мы также забыли про одну вещь: так как прошло шесть недель, мы должны выпустить бета-исполнение следующей исполнения Ржавчина 1.6. Поэтому после ответвления ветки stable
из ветки beta
, следующая исполнение beta
ответвляется снова от nightly
:
nightly: * - - * - - * - - * - - * - - * - * - *
- | |
-beta: * - - - - - - - - * *
- |
-stable: *
-
-Это называется “прообраз поезда” (train model), потому что каждые шесть недель выпуск “покидает станцию”, но ему все ещё нужно пройти поток beta, чтобы попасть в безотказную исполнение.
-Rust выпускается каждые шесть недель, как часы. Если вы знаете дату одного выпуска Rust, вы знаете дату выпуска следующего: это шесть недель позднее. Хорошим особенностью выпуска исполнений каждые шесть недель является то, что следующий поезд прибывает скоро. Если какая-то функция не попадает в исполнение, не надо волноваться: ещё один выпуск произойдёт очень скоро! Это помогает снизить давление в случае если функция возможно не отполирована к дате выпуска.
-Благодаря этому этапу, вы всегда можете посмотреть следующую исполнение Ржавчина и убедиться, что на неё легко будет перейти: если бета-выпуск будет работать не так как ожидалось, вы можете сообщить об этом разработчикам и он будет исправлен перед выпуском безотказной исполнения! Поломки в бета-исполнения случаются относительно редко, но rustc
все ещё является частью программного обеспечения, поэтому дефекты все ещё существуют.
У этой подходы выпуска есть ещё один плюс: ненадежные функции. Ржавчина использует технику называемую “флаги возможностей” (feature flags) для определения функций, которые были включены в выпуске. Если новая функция находится в активной разработке, она попадает в ветку master
, и поэтому попадает в ночную исполнение, но с флагом функции (feature flag). Если как пользователь, вы хотите попробовать работу такой функции, находящейся в разработке, вы должны использовать ночную исполнение Ржавчина и указать в вашем исходном коде определённый флаг.
Если вы используете бета или безотказную исполнение Rust, Вы не можете использовать флаги функций. Этот ключевой мгновение позволяет использовать в действительностиновые возможности перед их отладкой. Это может использоваться желающими идти в ногу со временем, а другие могут использовать безотказную исполнение и быть уверенными что их код не сломается. Безотказность без стагнации.
-Эта книга содержит сведения только о безотказных возможностях, так как разрабатываемые возможности продолжают меняться в этапе и несомненно они будут отличаться в зависимости от того, когда эта книга написана и когда эти возможности будут включены в безотказные сборки. Вы можете найти сведения о возможностях ночной исполнения в интернете.
-Rustup делает лёгким изменение между различными потоками Rust, на вездесущем или местном для дела уровне. По умолчанию устанавливается безотказная исполнение Rust. Для установки ночной исполнения выполните приказ:
-$ rustup toolchain install nightly
-
-Вы можете также увидеть все установленные средства разработчика (toolchains) (исполнения Ржавчина и сопряженные составляющие) с помощью rustup
. Это пример вывода у одного из авторов Ржавчина с компьютером на Windows:
> rustup toolchain list
-stable-x86_64-pc-windows-msvc (default)
-beta-x86_64-pc-windows-msvc
-nightly-x86_64-pc-windows-msvc
-
-Как видите, включенный набор средств (toolchain) используется по умолчанию. Большинство пользователей Ржавчина используют безотказные исполнения большую часть времени. Возможно, вы захотите использовать безотказную большую часть времени, но использовать каждую ночную исполнение в определенном деле, потому что заботитесь о передовых возможностях. Для этого вы можете использовать приказ rustup override
в папке этого дела, чтобы установить ночной набор средств, должна использоваться приказ rustup
, когда вы находитесь в этом папке:
$ cd ~/projects/needs-nightly
-$ rustup override set nightly
-
-Теперь каждый раз, когда вы вызываете rustc
или cargo
внутри ~/projects/needs-nightly, rustup
будет следить за тем, чтобы вы используете ночную исполнение Rust, а не безотказную по умолчанию. Это очень удобно, когда у вас есть множество Ржавчина дел!
Итак, как вы узнаете об этих новых возможностях? Прообраз разработки Ржавчина следует этапу запроса примечаниев (RFC - Request For Comments). Если хотите улучшить Rust, вы можете написать предложение, которое называется RFC.
-Любой может написать RFC для улучшения Rust, предложения рассматриваются и обсуждаются приказом Rust, которая состоит из множества тематических объединений и общин. На веб-сайте Rust есть полный список приказов, который включает приказы для каждой области дела: внешний вид языка, выполнение сборщика, инфраустройства, документация и многое другое. Соответствующая приказ читает предложение и примечания, пишет некоторые собственные примечания и в конечном итоге, приходит к согласию принять или отклонить эту возможность.
-Если новая возможность принята и кто-то может выполнить её, то задача открывается в хранилища Rust. Человек выполняющий её, вполне может не быть тем, кто предложил эту возможность! Когда выполнение готова, она попадает в master
ветвь с флагом функции, как мы обсуждали в разделе "Небезотказных функциях".
Через некоторое время, разработчики Ржавчина использующие ночные выпуски, смогут опробовать новую возможность, члены приказы обсудят её, как она работает в ночной исполнения и решат, должна ли она попасть в безотказную исполнение Ржавчина или нет. Если принимается решение двигать её вперёд, ограничение функции с помощью флага убирается и функция теперь считается безотказной! Она едет в новую безотказную исполнение Rust.
- ---Примечание. Это издание книги такое же, как и Язык программирования Rust, доступное в печатном и электронном виде от No Starch Press.
-
Добро пожаловать в The Ржавчина Programming Language, вводную книгу о Rust. Язык программирования Ржавчина помогает создавать быстрые, более надёжные приложения. Хорошая удобство и низкоуровневый управление часто являются противоречивыми требованиями для внешнего видаязыков программирования; Ржавчина бросает вызов этому вражде. Благодаря уравновешенности мощных технических возможностей c большим удобством разработки, Ржавчина предоставляет возможности управления низкоуровневыми элементами (например, использование памяти) без трудностей, привычно связанных с таким управлением.
-Rust наилучше подходит для многих людей по целому ряду причин. Давайте рассмотрим несколько наиболее важных объединений.
-Rust показал себя как производительный средство для совместной работы больших приказов разработчиков с разным уровнем знаний в области системного программирования. Низкоуровневый код подвержен различным трудноуловимым ошибкам, которые в большинстве других языков могут быть обнаружены только с помощью тщательного проверки и проверки кода опытными разработчиками. В Ржавчина сборщик играет значение привратника, отказываясь собирать код с этими неуловимыми ошибками, включая ошибки одновременности. Работая вместе с сборщиком, приказ может сосредоточиться на работе над логикой программы, а не над поиском ошибок.
-Rust также привносит современные средства разработчика в мир системного программирования:
-Благодаря применению этих и других средств в внутреннем устройстве Ржавчина разработчики способны производительно работать при написании кода системного уровня.
-Rust полезен для студентов и тех, кто увлечен в изучении системных подходов. Используя Rust, многие люди узнали о таких темах, как разработка операционных систем. Сообщество радушно и с удовольствием ответит на вопросы начинающих. Благодаря усилиям — таким, как эта книга — приказы Ржавчина хотят сделать подходы систем более доступными для большего числа людей, особенно для новичков в программировании.
-Сотни больших и малых предприятий используют Ржавчина в промышленных условиях для решения различных задач, включая средства приказной строки, веб-сервисы, средства DevOps, встраиваемые устройства, анализ и транскодирование аудио и видео, криптовалюты, биоинформатику, поисковые системы, приложения Интернета вещей, машинное обучение и даже основные части веб-браузера Firefox.
-Rust предназначен для людей, которые хотят развивать язык программирования Rust, сообщество, средства для разработчиков и библиотеки. Мы будем рады, если вы внесёте свой вклад в развитие языка Rust.
-Rust предназначен для любителей скорости и безотказности в языке. Под скоростью мы подразумеваем как быстродействие программы на Rust, так и быстроту, с которой Ржавчина позволяет писать программы. Проверки сборщика Ржавчина обеспечивают безотказность за счёт полезных дополнений и переработки кода. Это выгодно отличается от хрупкого унаследованного кода в языках без таких проверок, который разработчики часто боятся изменять. Благодаря обеспечению абстракций с нулевой стоимостью, высокоуровневых возможностей, собираемых в низкоуровневый код такой же быстрый, как и написанный вручную, Ржавчина стремится сделать безопасный код ещё и быстрым.
-Язык Ржавчина надеется поддержать и многих других пользователей; перечисленные здесь - лишь самые значимые увлеченные лица. В целом, главная цель Ржавчина - избавиться от соглашений, на которые программисты шли десятилетиями, обеспечив безопасность и производительность, скорость и удобство. Попробуйте Ржавчина и убедитесь, подойдут ли вам его решения.
-В этой книге предполагается, что вы писали код на другом языке программирования, но не оговаривается, на каком именно. Мы постарались сделать источник доступным для широкого круга людей с разным уровнем подготовки в области программирования. Мы не будем тратить время на обсуждение сути понятия программирования или как его понимать. Если вы совсем новичок в программировании, советуем прочитать книгу, посвящённую введению в программирование.
-В целом, книга предполагает, что вы будете читать последовательно от начала до конца. Более поздние главы опираются на подходы, изложенные в предыдущих главах, а предыдущие главы могут не углубляться в подробности именно темы, так как в последующих главах они будут рассматриваться более подробно.
-В этой книге вы найдёте два вида глав: главы о подходах и главы с делом. В главах о подходах вы узнаете о каком-либо особенности Rust. В главах дела мы будем вместе создавать небольшие программы, применяя то, что вы уже узнали. Главы 2, 12 и 20 - это главы дела; остальные - главы о подходах.
-Глава 1 объясняет, как установить Rust, как написать программу "Hello, world!" и как использовать Cargo, управленец дополнений и средство сборки Rust. Глава 2 - это опытное введение в написание программы на Rust, в которой вам предлагается создать игру для угадывания чисел. Здесь мы рассмотрим подходы на высоком уровне, а в последующих главах будет предоставлена дополнительная сведения. Если вы хотите сразу же приступить к работе, глава 2 - самое подходящее место для этого. В главе 3 рассматриваются возможности Rust, схожие с возможностями других языков программирования, а в главе 4 вы узнаете о системе владения Rust. Если вы особенно дотошный ученик и предпочитаете изучить каждую подробность, прежде чем переходить к следующей, возможно, вы захотите пропустить главу 2 и сразу перейти к главе 3, вернувшись к главе 2, когда захотите поработать над делом, применяя изученные подробности.
-Глава 5 описывает устройства и способы, а глава 6 охватывает перечисления, выражения match
и устройства управления потоком if let
. Вы будете использовать устройства и перечисления для создания пользовательских видов в Rust.
В главе 7 вы узнаете о системе звеньев Rust, о правилах согласования закрытости вашего кода и его открытом внешней оболочке прикладного программирования (API). В главе 8 обсуждаются некоторые распространённые устройства данных - собрания, которые предоставляет обычная библиотека, такие как векторы, строки и HashMaps. В главе 9 рассматриваются философия и способы обработки ошибок в Rust.
-В главе 10 рассматриваются образцовые виды данных, особенности и времена жизни, позволяющие написать код, который может использоваться разными видами. Глава 11 посвящена проверке, которое даже с заверениями безопасности в Ржавчина необходимо для обеспечения правильной логики вашей программы. В главе 12 мы создадим собственную выполнение подмножества возможности средства приказной строки grep
, предназначенного для поиска текста в файлах. Для этого мы будем использовать многие подходы, которые обсуждались в предыдущих главах.
В главе 13 рассматриваются замыкания и повторители: особенности Rust, пришедшие из полезных языков программирования. В главе 14 мы более подробно рассмотрим Cargo и поговорим о лучших способах распространения ваших библиотек среди других разработчиков. В главе 15 обсуждаются умные указатели, которые предоставляет обычная библиотека, и особенности, обеспечивающие их возможность.
-В главе 16 мы рассмотрим различные подходы одновременного программирования и поговорим о возможности Ржавчина для безбоязненного многопоточно программирования. В главе 17 рассматривается сравнение идиом Ржавчина с принципами предметно-направленного программирования, которые наверняка вам знакомы.
-Глава 18 - это справочник по образцам и сопоставлению с образцами, которые являются мощными способами выражения мыслей в программах на Rust. Глава 19 содержит множество важных дополнительных тем, включая небезопасный Rust, макросы и многое другое о времени жизни, особенностях, видах, функциях и замыканиях.
-В главе 20 мы завершим дело, в котором выполняем низкоуровневый многопоточный веб-сервер!
-Наконец, некоторые приложения содержат полезную сведения о языке в более справочном виде. В приложении A рассматриваются ключевые слова Rust, в приложении B — операторы и символы Rust, в приложении C — производные особенности, предоставляемые встроенной библиотекой, в приложении D — некоторые полезные средства разработки, а в приложении E — издания Rust. В приложении F вы найдёте переводы книги, а в приложении G мы расскажем о том, как создаётся Ржавчина и что такое nightly Rust.
-Нет неправильного способа читать эту книгу: если вы хотите пропустить главу - сделайте это! Возможно, вам придётся вернуться к предыдущим главам, если возникнет недопонимание. Делайте все, как вам удобно.
--
Важной частью этапа обучения Ржавчина является изучение того, как читать сообщения об ошибках, которые отображает сборщик: они приведут вас к работающему коду. Мы изучим много примеров, которые не собираются и отображают ошибки в сообщениях сборщика в разных случаейх. Знайте, что если вы введёте и запустите случайный пример, он может не собраться! Убедитесь, что вы прочитали окружающий текст, чтобы понять, не предназначен ли пример, который вы пытаетесь запустить, для отображения ошибки. Ferris также поможет вам различить код, который не предназначен для работы:
-Ferris | Пояснения |
---|---|
Этот код не собирается! | |
Этот код вызывает панику! | |
Этот код не приводит к желаемому поведению. |
В большинстве случаев мы приведём вас к правильной исполнения любого кода, который не собирается.
-Файлы с исходным кодом, используемым в этой книге, можно найти на GitHub.
- -Начнём наше путешествие в Rust! Нужно много всего изучить, но каждое путешествие с чего-то начинается. В этой главе мы обсудим:
-Hello, world!
,cargo
, управленца дополнений и системы сборки в одном лице для Rust.Первым шагом является установка Rust. Мы загрузим Rust, используя средство приказной строки rustup
, предназначенный для управлениями исполнениями Ржавчина и другими связанными с ним средствами. Вам понадобится интернет-соединение для его загрузки.
--Примечание: если вы по каким-то причинам предпочитаете не использовать rustup, пожалуйста, посетите страницу «Другие способы установки Rust» для получения дополнительных возможностей.
-
Следующие шаги устанавливают последнюю безотказную исполнение сборщика Rust. Благодаря заверениям безотказности Ржавчина все примеры в книге, которые собираются, будут собираться и в новых исполнениях Rust. Вывод может немного отличаться в разных исполнениях, поскольку Ржавчина часто улучшает сообщения об ошибках и предупреждения. Другими словами, любая новая, безотказная исполнение Rust, которую вы установите с помощью этих шагов, должна работать с содержимым этой книги так, как ожидается.
---Условные обозначения приказной строки
-В этой главе и во всей книге мы будем выполнять некоторые приказы, используемые в окне вызова. Строки, которые вы должны вводить в окне вызова, начинаются с
-$
. Вам не нужно вводить символ$
; это подсказка приказной строки, отображаемая для обозначения начала каждой приказы. Строки, которые не начинаются с$
, обычно показывают вывод предыдущей приказы. Кроме того, в примерах, своеобразных для PowerShell, будет использоваться>
, а не$
.
rustup
на Linux или macOSЕсли вы используете Linux или macOS, пожалуйста, выполните следующую приказ:
-$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
-
-Приказ загружает сценарий и запускает установку средства rustup
, который устанавливает последнюю безотказную исполнение Rust. Вам может быть предложено ввести пазначение. Если установка прошла успешно, появится следующая строка:
Rust is installed now. Great!
-
-Вам также понадобится составитель (linker) — программа, которую Ржавчина использует для объединения своих собранных выходных данных в один файл. Скорее всего, он у вас уже есть. При возникновении ошибок объединения, вам следует установить сборщик C, который обычно будет включать в себя и составитель. Сборщик C также полезен, потому что некоторые распространённые дополнения Ржавчина зависят от кода C и нуждаются в сборщике C.
-На macOS вы можете получить сборщик C, выполнив приказ:
-$ xcode-select --install
-
-Пользователи Linux, как правило, должны устанавливать GCC или Clang в соответствии с документацией их установочного набора. Например, при использовании Ubuntu можно установить дополнение build-essential
.
rustup
на WindowsНа Windows перейдите по адресу https://www.rust-lang.org/tools/install и следуйте указаниям по установке Rust. На определённом этапе установки вы получите сообщение, предупреждающее, что вам также понадобятся средства сборки MSVC для Visual Studio 2013 или более поздней исполнения.
-Чтобы получить средства сборки, вам потребуется установить Visual Studio 2022. На вопрос о том, какие составляющие необходимо установить, выберите:
-В остальной части этой книги используются приказы, которые работают как в cmd.exe, так и в PowerShell. При наличии отличительных различий мы объясним, что необходимо сделать в таких случаях.
-Чтобы проверить, правильно ли у вас установлен Rust, откройте оболочку и введите эту строку:
-$ rustc --version
-
-Вы должны увидеть номер исполнения, хэш определения и дату определения для последней безотказной исполнения, которая была выпущена, в следующем виде:
-rustc x.y.z (abcabcabc yyyy-mm-dd)
-
-Если вы видите эту сведения, вы успешно установили Rust! Если вы не видите эту сведения, убедитесь, что Ржавчина находится в вашей системной переменной %PATH%
следующим образом:
В Windows CMD:
-> echo %PATH%
-
-В PowerShell:
-> echo $env:Path
-
-В Linux и macOS:
-$ echo $PATH
-
-Если все было сделано правильно, но Ржавчина все ещё не работает, есть несколько мест, где вам могут помочь. Узнайте, как связаться с другими Rustaceans (так мы себя называем) на странице сообщества.
-После установки Ржавчина с помощью rustup
обновление до новой исполнения не составит труда. В приказной оболочке запустите следующий скрипт обновления:
$ rustup update
-
-Чтобы удалить Ржавчина и rustup
, выполните следующую приказ:
$ rustup self uninstall
-
-Установка Ржавчина также включает местную повтор документации, чтобы вы могли читать её в без доступа к мировой сети режиме. Выполните rustup doc
, чтобы открыть местную документацию в браузере.
Если обычная библиотека предоставляет вид или функцию, а вы не знаете, что она делает или как её использовать, воспользуйтесь документацией внешней оболочки прикладного программирования (API), чтобы это узнать!
- -Теперь, когда вы установили Rust, пришло время написать свою первую программу на Rust. Привычно при изучении нового языка принято писать небольшую программу, которая печатает на экране текст Привет, мир!
, поэтому мы сделаем то же самое!
--Примечание: Эта книга предполагает наличие достаточного навыка работы с приказной строкой. Ржавчина не предъявляет особых требований к тому, каким набором средств вы пользуетесь для изменения или хранения вашего кода, поэтому если вы предпочитаете использовать встроенную среду разработки (IDE) вместо приказной строки, смело используйте вашу любимую IDE. Многие IDE сейчас в той или иной степени поддерживают Rust; подробности можно узнать из документации к IDE. Объединение Ржавчина сосредоточилась на обеспечении отличной поддержки IDE с помощью
-rust-analyzer
. Более подробную сведения смотрите в Приложении D.
Прежде всего начнём с создания папки, в которой будем сохранять наш код на языке Rust. На самом деле не важно, где сохранять наш код. Однако, для упражнений и дел, обсуждаемых в данной книге, мы советуем создать папку projects в вашем домашнем папке, там же и хранить в будущем код программ из книги.
-Откройте окно вызова и введите следующие приказы для того, чтобы создать папку projects для хранения кода разных дел, и, внутри неё, папку hello_world для дела “Привет, мир!”.
-Для Linux, macOS и PowerShell на Windows, введите:
-$ mkdir ~/projects
-$ cd ~/projects
-$ mkdir hello_world
-$ cd hello_world
-
-Для Windows в CMD, введите:
-> mkdir "%USERPROFILE%\projects"
-> cd /d "%USERPROFILE%\projects"
-> mkdir hello_world
-> cd hello_world
-
-Затем создайте новый исходный файл и назовите его main.rs. Файлы Ржавчина всегда заканчиваются расширением .rs. Если вы используете более одного слова в имени файла, принято разделять их символом подчёркивания. Например, используйте hello_world.rs вместо helloworld.rs.
-Теперь откроем файл main.rs для изменения и введём следующие строки кода:
-Название файла: main.rs
--fn main() { - println!("Привет, мир!"); -}
-
Сохраните файл и вернитесь в окно окна вызова в папка ~/projects/hello_world. В Linux или macOS введите следующие приказы для сборки и запуска файла:
-$ rustc main.rs
-$ ./main
-Привет, мир!
-
-В Windows, введите приказ .\main.exe
вместо ./main
:
> rustc main.rs
-> .\main.exe
-Привет, мир!
-
-Независимо от вашей операционной системы, строка Привет, мир!
должна быть выведена на окно вызова. Если вы не видите такого вывода, обратитесь к разделу "Устранение неполадок ", чтобы узнать, как получить помощь.
Если напечаталось Привет, мир!
, то примите наши поздравления! Вы написали программу на Rust, что делает вас Ржавчина программистом — добро пожаловать!
Давайте рассмотрим «Привет, мир!» программу в подробностях. Вот первая часть головоломки:
--fn main() { - -}
Эти строки определяют функцию с именем main
. Функция main
особенная: это всегда первый код, который запускается в каждой исполняемой программе Rust. Первая строка объявляет функцию с именем main
, которая не имеет свойств и ничего не возвращает. Если бы были свойства, они бы заключались в круглые скобки ()
.
Тело функции заключено в {}
. Ржавчина требует фигурных скобок вокруг всех тел функций. Хороший исполнение — поместить открывающую фигурную скобку на ту же строку, что и объявление функции, добавив между ними один пробел.
--Примечание: Если хотите придерживаться принятого исполнения во всех делах Rust, вы можете использовать средство самостоятельного изменения под названием
-rustfmt
для изменения кода в определённом исполнении (подробнее оrustfmt
в Приложении D. Объединение Ржавчина включила этот средство в обычный установочный набор Rust, какrustc
, поэтому он уже должен быть установлен на вашем компьютере!
Тело функции main
содержит следующий код:
-#![allow(unused)] -fn main() { - println!("Привет, мир!"); -}
Эта строка делает всю работу в этой маленькой программе: печатает текст на экран. Можно заметить четыре важных подробности.
-Во-первых, исполнение Ржавчина предполагает отступ в четыре пробела, а не табуляцию.
-Во-вторых, println!
вызывается макрос Rust. Если бы вместо него была вызвана функция, она была бы набрана как println
(без !
). Более подробно мы обсудим макросы Ржавчина в главе 19. Пока достаточно знать, что использование !
подразумевает вызов макроса вместо обычной функции, и что макросы не всегда подчиняются тем же правилам как функции.
В-третьих, вы видите строку "Привет, мир!"
. Мы передаём её в качестве переменной макросу println!
, и она выводится на экран.
В-четвёртых, мы завершаем строку точкой с запятой (;
), которая указывает на окончание этого выражения и возможность начала следующего. Большинство строк кода Ржавчина заканчиваются точкой с запятой.
Вы только что запустили впервые созданную программу, поэтому давайте рассмотрим каждый шаг этого этапа.
-Перед запуском программы на Ржавчина вы должны собрать её с помощью сборщика Rust, введя приказ rustc
и передав ей имя вашего исходного файла, например:
$ rustc main.rs
-
-Если у вас есть опыт работы с C или C++, вы заметите, что это похоже на gcc
или clang
. После успешной сборки Ржавчина выводит двоичный исполняемый файл.
В Linux, macOS и PowerShell в Windows вы можете увидеть исполняемый файл, введя приказ ls
в оболочке:
$ ls
-main main.rs
-
-В Linux и macOS вы увидите два файла. При использовании PowerShell в Windows вы увидите такие же три файла, как и при использовании CMD. Используя CMD в Windows, введите следующее:
-> dir /B %= the /B option says to only show the file names =%
-main.exe
-main.pdb
-main.rs
-
-Это показывает исходный код файла с расширением .rs, исполняемый файл (main.exe на Windows, но main на всех других площадках) и, при использовании Windows, файл, содержащий отладочную сведения с расширением .pdb. Отсюда вы запускаете файлы main или main.exe, например:
-$ ./main # для Linux
-> .\main.exe # для Windows
-
-Если ваш main.rs — это ваша программа «Привет, мир!», эта строка выведет в окно вызова Привет, мир!
.
Если вы лучше знакомы с изменяемыми языками, такими как Ruby, Python или JavaScript, возможно, вы не привыкли собирать и запускать программу как отдельные шаги. Ржавчина — это предварительно собранный язык, то есть вы можете собрать программу и передать исполняемый файл кому-то другому, и он сможет запустить его даже без установленного Rust. Если вы даёте кому-то файл .rb , .py или .js, у него должна быть установлена выполнение Ruby, Python или JavaScript (соответственно). Но в этих языках вам нужна только одна приказ для сборки и запуска вашей программы. В внешнем виде языков программирования всё — соглашение.
-Сборка с помощью rustc
подходит для простых программ, но по мере роста вашего дела вы захотите управлять всеми свойствами и упростить передачу кода. Далее мы познакомим вас с средством Cargo, который поможет вам писать программы из существующего мира на Rust.
Cargo - это система сборки и управленец дополнений Rust. Большая часть разработчиков используют данный средство для управления делами, потому что Cargo выполняет за вас множество задач, таких как сборка кода, загрузка библиотек, от которых зависит ваш код, и создание этих библиотек. (Мы называем библиотеки, которые нужны вашему коду, зависимостями.)
-Самые простые программы на Rust, подобные той, которую мы написали, не имеют никаких зависимостей. Если бы мы сделали дело «Hello, world!» с Cargo, он бы использовал только ту часть Cargo, которая отвечает за сборку вашего кода. По мере написания более сложных программ на Ржавчина вы будете добавлять зависимости, а если вы начнёте дело с использованием Cargo, добавлять зависимости станет намного проще.
-Поскольку значительное число дел Ржавчина используют Cargo, оставшаяся часть книги подразумевает, что вы тоже используете Cargo. Cargo входит в состав поставки Rust, если вы использовали напрямую от разрабочиков программы установки, рассмотренные в разделе "Установка". Если вы установили Ржавчина другим способом, проверьте, установлен ли Cargo, введя в окне вызова следующее:
-$ cargo --version
-
-Если приказ выдал номер исполнения, то значит Cargo установлен. Если вы видите ошибку, вроде command not found
("приказ не найдена"), загляните в документацию для использованного вами способа установки, чтобы выполнить установку Cargo отдельно.
Давайте создадим новый дело с помощью Cargo и посмотрим, как он отличается от нашего начального дела "Hello, world!". Перейдите обратно в папку projects (или любую другую, где вы решили сохранять код). Затем, в любой операционной системе, запустите приказ:
-$ cargo new hello_cargo
-$ cd hello_cargo
-
-Первая приказ создаёт новый папка и дело с именем hello_cargo. Мы назвали наш дело hello_cargo, и Cargo создаёт свои файлы в папке с тем же именем.
-Перейдём в папка hello_cargo и посмотрим файлы. Увидим, что Cargo создал два файла и одну папку: файл Cargo.toml и папка src с файлом main.rs внутри.
-Кроме того, cargo объявлял новый хранилище Git вместе с файлом .gitignore. Файлы Git не будут созданы, если вы запустите cargo new
в существующем хранилища Git; вы можете изменить это поведение, используя cargo new --vcs=git
.
--Примечание. Git — это распространённая система управления исполнений. Вы можете изменить
-cargo new
, чтобы использовать другую систему управления исполнений или не использовать систему управления исполнений, используя флаг--vcs
. Запуститеcargo new --help
, чтобы увидеть доступные свойства.
Откройте файл Cargo.toml в любом текстовом редакторе. Он должен выглядеть как код в приложении 1-2.
-Файл: Cargo.toml
-[package]
-name = "hello_cargo"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-
--
Это файл в видеTOML (Tom’s Obvious, Minimal Language), который является видом настроек Cargo.
-Первая строка, [package]
, является заголовочной разделом, которая указывает что следующие указания настраивают дополнение. По мере добавления больше сведений в данный файл, будет добавляться больше разделов и указаний (строк).
Следующие три строки задают сведения о настройке, необходимую Cargo для сборки вашей программы: имя, исполнение и издание Rust, который будет использоваться. Мы поговорим о ключе edition
в Приложении E.
Последняя строка, [dependencies]
является началом разделы для списка любых зависимостей вашего дела. В Rust, это внешние дополнения кода, на которые ссылаются ключевым словом crate. Нам не нужны никакие зависимости в данном деле, но мы будем использовать их в первом деле главы 2, так что нам пригодится данная раздел зависимостей потом.
Откройте файл src/main.rs и загляните в него:
-Файл: src/main.rs
--fn main() { - println!("Hello, world!"); -}
Cargo создал для вас программу "Hello, world!", подобную той, которую мы написали в Приложении 1-1! Пока что различия между нашим предыдущим делом и делом, созданным при помощи Cargo, заключаются в том, что Cargo поместил исходный код в папка src, и у нас есть настроечный файл Cargo.toml в верхнем папке дела.
-Cargo ожидает, что ваши исходные файлы находятся внутри папки src. Папка верхнего уровня дела предназначен только для файлов README, сведений о лицензии, файлы настройке и чего то ещё не относящего к вашему коду. Использование Cargo помогает создавать дело. Есть место для всего и все находится на своём месте.
-Если вы начали дело без использования Cargo, как мы делали для "Hello, world!" дела, то можно преобразовывать его в дело с использованием Cargo. Переместите код в подпапка src и создайте соответствующий файл Cargo.toml в папке.
-Посмотрим, в чем разница при сборке и запуске программы "Hello, world!" с помощью Cargo. В папке hello_cargo соберите дело следующей приказом:
-$ cargo build
- Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
-
-Этот приказ создаёт исполняемый файл в target/debug/hello_cargo (или target\debug\hello_cargo.exe в Windows), а не в вашем текущем папке. Поскольку обычная сборка является отладочной, Cargo помещает двоичный файл в папка с именем debug. Вы можете запустить исполняемый файл с помощью этой приказы:
-$ ./target/debug/hello_cargo # or .\target\debug\hello_cargo.exe on Windows
-Hello, world!
-
-Если все хорошо, то Hello, world!
печатается в окне вызова. Запуск приказы cargo build
в первый раз также приводит к созданию нового файла Cargo.lock в папке верхнего уровня. Данный файл хранит точные исполнения зависимостей вашего дела. Так как у нас нет зависимостей, то файл пустой. Вы никогда не должны менять этот файл вручную: Cargo сам управляет его содержимым для вас.
Только что мы собрали дело приказом cargo build
и запустили его из ./target/debug/hello_cargo
. Но мы также можем при помощи приказы cargo run
сразу и собрать код, и затем запустить полученный исполняемый файл всего лишь одной приказом:
$ cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
- Running `target/debug/hello_cargo`
-Hello, world!
-
-Использование cargo run
более удобно, чем необходимость помнить и запускать cargo build
, а затем использовать весь путь к двоичному файлу, поэтому большинство разработчиков используют cargo run
.
Обратите внимание, что на этот раз мы не видели вывода, указывающего на то, что Cargo собирает hello_cargo
. Cargo выяснил, что файлы не изменились, поэтому не стал пересобирать, а просто запустил двоичный файл. Если бы вы изменили свой исходный код, Cargo пересобрал бы дело перед его запуском, и вы бы увидели этот вывод:
$ cargo run
- Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
- Running `target/debug/hello_cargo`
-Hello, world!
-
-Cargo также предоставляет приказ, называемую cargo check
. Этот приказ быстро проверяет ваш код, чтобы убедиться, что он собирается, но не создаёт исполняемый файл:
$ cargo check
- Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
- Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
-
-Почему вам не нужен исполняемый файл? Часто cargo check
выполняется намного быстрее, чем cargo build
, поскольку пропускает этап создания исполняемого файла. Если вы постоянно проверяете свою работу во время написания кода, использование cargo check
ускорит этап уведомления вас о том, что ваш дело всё ещё собирается! Таким образом, многие Rustacean периодически запускают cargo check
, когда пишут свои программы, чтобы убедиться, что она собирается. Затем они запускают cargo build
, когда готовы использовать исполняемый файл.
Давайте подытожим, что мы уже узнали о Cargo:
-cargo new
.cargo build
,cargo run
,cargo check
, не тратя время на кодосоздание исполняемого файла,Дополнительным преимуществом использования Cargo является то, что его приказы одинаковы для разных операционных систем. С этой точки зрения, мы больше не будем предоставлять отдельные указания для Linux, macOS или Windows.
-Когда дело, наконец, готов к исполнению, можно использовать приказ cargo build --release
для его сборки с переработкой. Данная приказ создаёт исполняемый файл в папке target/release в отличии от папки target/debug. Переработки делают так, что Ржавчина код работает быстрее, но их включение увеличивает время сборки. По этой причине есть два отдельных профиля: один для разработки, когда нужно осуществлять сборку быстро и часто, и другой, для сборки конечной программы, которую будете отдавать пользователям, которая готова к работе и будет выполняться сверх быстро. Если вы замеряете время выполнения вашего кода, убедитесь, что собрали дело с переработкой cargo build --release
и проверяете исполняемый файл из папки target/release.
В простых делах Cargo не даёт больших преимуществ по сравнению с использованием rustc
, но он проявит себя, когда ваши программы станут более сложными. Когда программы вырастают до нескольких файлов или нуждаются в зависимостях, гораздо проще позволить Cargo согласовывать сборку.
Не смотря на то, что дело hello_cargo
простой, теперь он использует большую часть существующего набора средств, который вы будете повседневно использовать в вашей развитии, связанной с Rust. Когда потребуется работать над делами размещёнными в сети, вы сможете просто использовать следующую последовательность приказов для получения кода с помощью Git, перехода в папка дела, сборку дела:
$ git clone example.org/someproject
-$ cd someproject
-$ cargo build
-
-Для получения дополнительной сведений о Cargo ознакомьтесь с его документацией .
-Теперь вы готовы начать своё Ржавчина путешествие! В данной главе вы изучили как:
-rustup
,rustc
,Это отличное время для создания более существенной программы, чтобы привыкнуть читать и писать код на языке Rust. Итак, в главе 2 мы построим программу для игры в угадай число. Если вы предпочитаете начать с изучения того, как работают общие подходы программирования в Rust, обратитесь к главе 3, а затем вернитесь к главе 2.
- -Давайте окунёмся в Rust, вместе поработав над опытным делом! В этой главе вы познакомитесь с несколькими общими подходами Rust, показав, как использовать их в существующей программе. Вы узнаете о let
, match
, способах, сопряженных функциях, внешних дополнениях и многом другом! В следующих главах мы рассмотрим эти мысли более подробно. В этой главе вы просто примените в основах.
Мы выполняем привычную для начинающих программистов задачу — игру в загадки. Вот как это работает: программа порождает случайное целое число в ряде от 1 до 100. Затем она предлагает игроку его угадать. После ввода числа программа укажет, меньше или больше было загаданное число. Если догадка верна, игра напечатает поздравительное сообщение и завершится.
-Для настройки нового дела перейдите в папка projects, который вы создали в главе 1, и создайте новый дело с использованием Cargo, как показано ниже:
-$ cargo new guessing_game
-$ cd guessing_game
-
-Первая приказ, cargo new
, принимает в качестве первого переменной имя дела (guessing_game
). Вторая приказ изменяет папка на новый папка дела.
Загляните в созданный файл Cargo.toml:
- -Файл: Cargo.toml
-[package]
-name = "guessing_game"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-
-Как вы уже видели в главе 1, cargo new
создаёт программу «Hello, world!». Посмотрите файл src/main.rs:
Файл: src/main.rs
--fn main() { - println!("Hello, world!"); -}
Теперь давайте соберем программу «Hello, world!» и сразу на этом же этапе запустим её с помощью приказы cargo run
:
$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.50s
- Running `target/debug/guessing_game`
-Hello, world!
-
-Приказ run
пригодится, когда необходимо ускоренно выполнить повторение дела. Именно так мы собираемся делать в этом деле, быстро проверяя каждую повторение, прежде чем перейти к следующей.
Снова откройте файл src/main.rs. Весь код вы будете писать в нем.
-Первая часть программы запрашивает ввод данных пользователем, обрабатывает их и проверяет, что они в ожидаемой виде. Начнём с того, что позволим игроку ввести догадку. Вставьте код из приложения 2-1 в src/main.rs.
-Файл: src/main.rs
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
--
Этот код содержит много сведений, поэтому давайте рассмотрим его построчно. Чтобы получить пользовательский ввод и затем вывести итог, нам нужно включить в область видимости библиотеку ввода/вывода io
. Библиотека io
является частью встроенной библиотеки, известной как std
:
use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-По умолчанию в Ржавчина есть набор элементов, определённых в встроенной библиотеке, которые он добавляет в область видимости каждой программы. Этот набор называется прелюдией, и вы можете изучить его содержание в документации встроенной библиотеки.
-Если вид, который требуется использовать, отсутствует в прелюдии, его нужно явно ввести в область видимости с помощью оператора use
. Использование библиотеки std::io
предоставляет ряд полезных полезных возможностей, включая способность принимать пользовательский ввод.
Как уже отмечалось в главе 1, функция main
является точкой входа в программу:
use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Ключевое слово fn
объявляет новую функцию, круглые скобки ()
показывают, что у функции нет входных свойств, фигурная скобка {
- обозначение начала тела функции.
Также в главе 1 упоминалось, что println!
— это макрос, который выводит строку на экран:
use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Этот код показывает сведения о ходе игры и запрашивает пользовательский ввод.
-Далее мы создаём переменную для хранения пользовательского ввода, как показано ниже:
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Вот теперь программа становится важнее! В этой маленькой строке на самом деле происходит очень многое. Для создания переменной мы используем оператор let
. Вот ещё один пример:
let apples = 5;
-Эта строка создаёт новую переменную с именем apples
и привязывает её к значению 5. В Ржавчина переменные неизменяемы по умолчанию, то есть как только мы присвоим переменной значение, оно не изменится. Мы подробно обсудим эту подход в разделе "Переменные и изменчивость". в главе 3. Чтобы сделать переменную изменяемой, мы добавляем mut
перед её именем:
let apples = 5; // неизменяемая
-let mut bananas = 5; // изменяемая
---Примечание: сочетание знаков
-//
начинает примечание, который продолжается до конца строки. Ржавчина пренебрегает всё, что находится в примечаниях. Мы обсудим примечания более подробно в Главе 3.
Возвращаясь к программе игры "Угадайка" — теперь вы знаете, что let mut guess
предоставит изменяемую переменную с именем guess
. Знак равенства (=
) сообщает Rust, что сейчас нужно связать что-то с этой переменной. Справа от знака равенства находится значение, связанное с guess
, которое является итогом вызова функции String::new
, возвращающей новый образец String
. String
— это вид строки, предоставляемый встроенной библиотекой, который является расширяемым отрывком текста в кодировке UTF-8.
правила написания ::
в строке ::new
указывает, что new
является сопряженной функцией вида String
. Сопряженная функция — это функция, выполненная для вида, в данном случае String
. Функция new
создаёт новую пустую строку. Функцию new
можно встретить во многих видах, это привычное название для функции, которая создаёт новое значение какого-либо вида.
В конечном итоге строка let mut guess = String::new();
создала изменяемую переменную, которая связывается с новым пустым образцом String
. Фух!
Напомним: мы подключили возможность ввода/вывода из встроенной библиотеки с помощью use std::io;
в первой строке программы. Теперь мы вызовем функцию stdin
из звена io
, которая позволит нам обрабатывать пользовательский ввод:
use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Если бы мы не подключили библиотеку io
с помощью use std::io
в начале программы, мы все равно могли бы использовать эту функцию, записав её вызов как std::io::stdin
. Функция stdin
возвращает образец std::io::Stdin
, который является видом, представляющим указатель принятого ввода для вашего окна вызова.
Далее строка .read_line(&mut guess)
вызывает способ read_line
на указателе принятого ввода для получения ввода от пользователя. Мы также передаём &mut guess
в качестве переменной read_line
, сообщая ему, в какой строке хранить пользовательский ввод. Главная задача read_line
— принять все, что пользователь вводит в обычный ввод, и сложить это в строку (не переписывая её содержимое), поэтому мы передаём эту строку в качестве переменной. Строковый переменная должен быть изменяемым, чтобы способ мог изменить содержимое строки.
Символ &
указывает, что этот переменная является ссылкой, которая предоставляет возможность нескольким частям вашего кода получить доступ к одному отрывку данных без необходимости воспроизводить эти данные в память несколько раз. Ссылки — это сложная полезная возможность, а одним из главных преимуществ Ржавчина является безопасность и простота использования ссылок. Чтобы дописать эту программу, вам не понадобится знать много таких подробностей. Пока вам достаточно знать, что ссылки, как и переменные, по умолчанию неизменяемы. Соответственно, чтобы сделать её изменяемой, нужно написать &mut guess
, а не &guess
. (В главе 4 ссылки будут описаны более подробно).
Result
Мы всё ещё работаем над этой строкой кода. Сейчас мы обсуждаем третью строку, но обратите внимание, что она по-прежнему является частью одной логической строки. Следующая часть — способ:
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Мы могли бы написать этот код так:
-io::stdin().read_line(&mut guess).expect("Failed to read line");
-Однако одну длинную строку трудно читать, поэтому лучше разделить её. При вызове способа с помощью правил написания .method_name()
часто целесообразно вводить новую строку и другие пробельные символы, чтобы разбить длинные строки. Теперь давайте обсудим, что делает эта строка.
Как упоминалось ранее, read_line
помещает всё, что вводит пользователь, в строку, которую мы ему передаём, но также возвращает значение Result
. Result
— это перечисление, часто называемое enum, то есть вид, который может находиться в одном из нескольких возможных состояний. Мы называем каждое такое состояние исходом.
В Главе 6 рассмотрим перечисления более подробно. Задачей видов Result
является кодирование сведений для обработки ошибок.
Исходами Result
являются Ok
и Err
. Исход Ok
указывает, что действие завершилась успешно, а внутри Ok
находится успешно созданное значение. Исход Err
означает, что действие не удалась, а Err
содержит сведения о причинах неудачи.
Значения вида Result
, как и значения любого вида, имеют определённые для них способы. У образца Result
есть способ expect
, который можно вызвать. Если этот образец Result
является значением Err
, expect
вызовет сбой программы и отобразит сообщение, которое вы передали в качестве переменной. Если способ read_line
возвращает Err
, то это, скорее всего, итог ошибки основной операционной системы. Если образец Result
является значением Ok
, expect
возьмёт возвращаемое значение, которое удерживает Ok
, и вернёт вам только это значение, чтобы вы могли его использовать далее. В данном случае это значение представляет собой количество байтов, введённых пользователем.
Если не вызвать expect
, программа собирается, но будет получено предупреждение:
$ cargo build
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
-warning: unused `Result` that must be used
- --> src/main.rs:10:5
- |
-10 | io::stdin().read_line(&mut guess);
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- |
- = note: this `Result` may be an `Err` variant, which should be handled
- = note: `#[warn(unused_must_use)]` on by default
-help: use `let _ = ...` to ignore the resulting value
- |
-10 | let _ = io::stdin().read_line(&mut guess);
- | +++++++
-
-warning: `guessing_game` (bin "guessing_game") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
-
-Rust предупреждает о неиспользованном значении Result
, возвращаемого из read_line
, показывая, что программа не учла возможность возникновения ошибки.
Правильный способ убрать предупреждение — это написать обработку ошибок, но в нашем случае мы просто хотим со сбоем завершить программу при возникновении сбоев, поэтому используем expect
. О способах восстановления после ошибок вы узнаете в главе 9.
println!
Кроме закрывающей фигурной скобки, в коде на данный мгновение есть ещё только одно место для обсуждения:
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
-}
-Этот код выводит строку, которая теперь содержит ввод пользователя. Набор фигурных скобок {}
является заполнителем: думайте о {}
как о маленьких клешнях краба, которые удерживают значение на месте. При печати значения переменной имя переменной может заключаться в фигурные скобки. При печати итога вычисления выражения поместите пустые фигурные скобки в строку вида, затем после строки вида укажите список выражений, разделённых запятыми, которые будут напечатаны в каждом заполнителе пустой фигурной скобки в том же порядке. Печать переменной и итога выражения одним вызовом println!
будет выглядеть так:
-#![allow(unused)] -fn main() { -let x = 5; -let y = 10; - -println!("x = {x} and y + 2 = {}", y + 2); -}
Этот код выведет x = 5 and y + 2 = 12
.
Давайте проверим первую часть игры. Запустите её используя cargo run
:
$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 6.44s
- Running `target/debug/guessing_game`
-Guess the number!
-Please input your guess.
-6
-You guessed: 6
-
-На данном этапе первая часть игры завершена: мы получаем ввод с клавиатуры и затем печатаем его.
-Далее нам нужно создать тайное число, которое пользователь попытается угадать. Тайное число должно быть каждый раз разным, чтобы в игру можно было играть несколько раз. Мы будем использовать случайное число в ряде от 1 до 100, чтобы игра не была слишком сложной. Ржавчина пока не включает возможность случайных чисел в свою обычную библиотеку. Однако приказ Ржавчина предоставляет [ящик rand
] с подобной возможностью.
Помните, что дополнение (crate) - это собрание файлов исходного кода Rust. Дело, создаваемый нами, представляет собой
двоичный дополнение (binary crate), который является исполняемым файлом. Дополнение rand
- это библиотечный дополнение (library crate), содержащий код, который предназначен для использования в других программах и поэтому не может исполняться сам по себе.
Согласование работы внешних дополнений является тем местом, где Cargo на самом деле блистает. Чтобы начать писать код, использующий rand
, необходимо изменить файл Cargo.toml, включив в него в качестве зависимости дополнение rand
. Итак, откройте этот файл и добавьте следующую строку внизу под заголовком разделы [dependencies]
, созданным для вас Cargo. Обязательно укажите rand
в точности так же, как здесь, с таким же номером исполнения, иначе примеры кода из этого урока могут не заработать.
Имя файла: Cargo.toml
-[dependencies]
-rand = "0.8.5"
-
-В файле Cargo.toml всё, что следует за заголовком, является частью этой разделы, которая продолжается до тех пор, пока не начнётся следующая. В [dependencies]
вы сообщаете Cargo, от каких внешних ящиков зависит ваш дело и какие исполнения этих ящиков вам нужны. В этом случае мы указываем ящик rand
со определетелем смысловой исполнения 0.8.5
. Cargo понимает смысловое управление исполнениями (иногда называемое SemVer), которое является исполнением для описания исполнений. Число 0.8.5
на самом деле является сокращением от ^0.8.5
, что означает любую исполнение не ниже 0.8.5
, но ниже 0.9.0
.
Cargo рассчитывает, что эти исполнения имеют общедоступное API, совместимое с исполнением 0.8.5
, и вы получите последние исполнения исправлений, которые по-прежнему будут собираться с кодом из этой главы. Не обеспечивается, что исполнение 0.9.0
или выше будет иметь тот же API, что и в следующих примерах.
Теперь, не меняя ничего в коде, давайте соберём дело, как показано в приложении 2-2.
- -$ cargo build
- Updating crates.io index
- Downloaded rand v0.8.5
- Downloaded libc v0.2.127
- Downloaded getrandom v0.2.7
- Downloaded cfg-if v1.0.0
- Downloaded ppv-lite86 v0.2.16
- Downloaded rand_chacha v0.3.1
- Downloaded rand_core v0.6.3
- Compiling libc v0.2.127
- Compiling getrandom v0.2.7
- Compiling cfg-if v1.0.0
- Compiling ppv-lite86 v0.2.16
- Compiling rand_core v0.6.3
- Compiling rand_chacha v0.3.1
- Compiling rand v0.8.5
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 2.53s
-
--
Вы можете увидеть другие номера исполнений (но все они будут совместимы с кодом благодаря SemVer), другие строки (в зависимости от операционной системы), а также строки могут быть расположены в другом порядке.
-Когда мы включаем внешнюю зависимость, Cargo берет последние исполнения всего, что нужно этой зависимости, из реестра (registry), который является повтором данных с Crates.io. Crates.io — это место, где участники внутреннего устройства Ржавчина размещают свои дела с открытым исходным кодом для использования другими.
-После обновления реестра Cargo проверяет раздел [dependencies]
и загружает все указанные в списке дополнения, которые ещё не были загружены. В нашем случае, хотя мы указали только rand
в качестве зависимости, Cargo также захватил другие дополнения, от которых зависит работа rand
. После загрузки дополнений Ржавчина собирает их, а затем собирает дело с имеющимися зависимостями.
Если сразу же запустить cargo build
снова, не внося никаких изменений, то кроме строки Finished
вы не получите никакого вывода. Cargo знает, что он уже загрузил и собрал зависимости, и вы не вносили никаких изменений в файл Cargo.toml. Cargo также знает, что вы ничего не изменили в своём коде, поэтому он не пересоберет и его. Если делать нечего, он просто завершает работу.
Если вы откроете файл src/main.rs, внесёте обыкновенное изменение, а затем сохраните его и снова соберёте, вы увидите только две строки вывода:
- -$ cargo build
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
-
-Эти строки показывают, что Cargo обновляет сборку только с вашим крошечным изменением в файле src/main.rs. Ваши зависимости не изменились, поэтому Cargo знает, что может повторно использовать то, что уже скачано и собрано для них.
-В Cargo есть рычаг, обеспечивающий возможность пересобрать всё тот же артефакт каждый раз, когда вы или кто-либо другой собирает ваш код. Пока вы не укажете обратное, Cargo будет использовать только те исполнения зависимостей, которые были заданы ранее. Например, допустим, что на следующей неделе выходит исполнение 0.8.6 дополнения rand
, и она содержит важное исправление ошибки, но также отступление, которая может сломать ваш код. Чтобы справиться с этим, Ржавчина создаёт файл Cargo.lock при первом запуске cargo build
, поэтому теперь он есть в папке guessing_game.
Когда вы создаёте дело в первый раз, Cargo определяет все исполнения зависимостей, которые соответствуют условиям, а затем записывает их в файл Cargo.lock. Когда вы будете собирать свой дело в будущем, Cargo увидит, что файл Cargo.lock существует, и будет использовать указанные там исполнения, а не выполнять всю работу по выяснению исполнений заново. Это позволяет самостоятельно создавать воспроизводимую сборку. Другими словами, ваш дело останется на 0.8.5
до тех пор, пока вы явно не обновите его благодаря файлу Cargo.lock. Поскольку файл Cargo.lock важен для воспроизводимых сборок, он часто хранится в системе управления исполнениями вместе с остальным кодом дела.
Если вы захотите обновить дополнение, Cargo предоставляет приказ update
, которая пренебрегает файл Cargo.lock и определяет последние исполнения, соответствующие вашим согласно принятых требованийм из файла Cargo.toml. После этого Cargo запишет эти исполнения в файл Cargo.lock. Иначе по умолчанию Cargo будет искать только исполнения больше 0.8.5, но при этом меньше 0.9.0. Если дополнение rand
имеет две новые исполнения — 0.8.6 и 0.9.0 — то при запуске cargo update
вы увидите следующее:
$ cargo update
- Updating crates.io index
- Updating rand v0.8.5 -> v0.8.6
-
-Cargo пренебрегает исполнение 0.9.0. В этот мгновение также появится изменение в файле Cargo.lock, указывающее на то, что исполнение rand
, которая теперь используется, равна 0.8.6. Чтобы использовать rand
исполнения 0.9.0 или любой другой исполнения из серии 0.9.x, необходимо обновить файл Cargo.toml следующим образом:
[dependencies]
-rand = "0.9.0"
-
-В следующий раз, при запуске cargo build
, Cargo обновит реестр доступных дополнений и пересмотрит ваши требования к rand
в соответствии с новой исполнением, которую вы указали.
Можно много рассказать про Cargo и его внутреннее устройство которые мы обсудим в главе 14, сейчас это все что вам нужно знать. Cargo позволяет очень легко повторно использовать библиотеки, поэтому Ржавчина разработчики имеют возможность писать меньшие дела, которые составлены из многих дополнений.
-Давайте начнём использовать rand
, чтобы создать число для угадывания. Следующим шагом будет обновление src/main.rs, как показано в приложении 2-3.
Файл: src/main.rs
-use std::io;
-use rand::Rng;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-}
--
Сначала мы добавляем строку use rand::Rng
. Особенность Rng
определяет способы, выполняющие породители случайных чисел, и этот особенность должен быть в области видимости, чтобы эти способы можно было использовать. В главе 10 мы рассмотрим особенности подробно.
Затем мы добавляем две строки посередине. В первой строке мы вызываем функцию rand::thread_rng
, дающую нам породитель случайных чисел, который мы собираемся использовать: тот самый, который является местным для текущего потока выполнения и запускается операционной системой. Затем мы вызываем его способ gen_range
. Этот способ определяется Rng
, который мы включили в область видимости с помощью оператора use rand::Rng
. Способ gen_range
принимает в качестве переменной выражение ряда и порождает случайное число в этом ряде. Вид используемого выражения ряда принимает разновидность start..=end
и включает нижнюю и верхнюю границы, поэтому, чтобы запросить число от 1 до 100, нам нужно указать 1..=100
.
--Примечание: непросто сразу разобраться, какие особенности использовать, какие способы и функции вызывать из дополнения, поэтому каждый дополнение имеет документацию с указаниями по его использованию. Ещё одной замечательной особенностью Cargo является выполнение приказы
-cargo doc --open
, которая местно собирает документацию, предоставляемую всеми вашими зависимостями, и открывает её в браузере. К примеру, если важна другая возможность из дополненияrand
, запуститеcargo doc --open
и нажмитеrand
в боковой панели слева.
Во второй новой строке мы увидим загаданное число. Во время разработки программы полезно иметь возможность её проверять, но в конечной исполнения мы это удалим. Конечно, ведь это совсем не похоже на игру, если программа печатает ответ сразу после запуска!
-Попробуйте запустить программу несколько раз:
- -$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 2.53s
- Running `target/debug/guessing_game`
-Guess the number!
-The secret number is: 7
-Please input your guess.
-4
-You guessed: 4
-
-$ cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.02s
- Running `target/debug/guessing_game`
-Guess the number!
-The secret number is: 83
-Please input your guess.
-5
-You guessed: 5
-
-Вы должны получить разные случайные числа, и все они должны быть числами в ряде от 1 до 100. Отличная работа!
-Теперь, когда у нас есть пользовательский ввод и случайное число, мы можем сравнить их. Этот шаг показан в приложении 2-4. Учтите, что этот код ещё не собирается, подробнее мы объясним дальше.
-Имя файла: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- // --snip--
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
--
Сначала добавим ещё один оператор use
, который вводит вид с именем std::cmp::Ordering
в область видимости из встроенной библиотеки. Вид Ordering
является ещё одним перечислением и имеет исходы Less
, Greater
и Equal
. Это три возможных исхода при сравнении двух величин.
После чего ниже добавляем пять новых строк, использующих вид Ordering
. Способ cmp
сравнивает два значения и может вызываться для всего, что можно сравнить. Он принимает ссылку на все, что требуется сравнить: здесь сравнивается guess
с secret_number
. В итоге возвращается исход перечисления Ordering
, которое мы ввели в область видимости с помощью оператора use
. Для принятия решения о том, что делать дальше, мы используем выражение match
, определяющее, какой исход Ordering
был возвращён из вызова cmp
со значениями guess
и secret_number
.
Выражение match
состоит из веток (arms). Ветка состоит из образца для сопоставления и кода, который будет запущен, если значение, переданное в match
, соответствует образцу этой ветки. Ржавчина принимает значение, заданное match
, и по очереди просматривает образец каждой ветки. Образцы и устройство match
— это мощные возможности Rust, позволяющие выразить множество случаев, с которыми может столкнуться ваш код, и обеспечить их обработку. Эти возможности будут подробно раскрыты в главе 6 и главе 18 соответственно.
Давайте рассмотрим пример с выражением match
, которое мы здесь используем. Скажем, пользователь угадал 50, а случайно созданное тайное число на этот раз — 38.
Когда код сравнивает 50 с 38, способ cmp
вернёт Ordering::Greater
, поскольку 50 больше, чем 38. Выражение match
получит значение Ordering::Greater
и начнёт проверять образец в каждой ветке. Он просмотрит образец первой ветки, Ordering::Less
, и увидит, что значение Ordering::Greater
не соответствует Ordering::Less
, поэтому пренебрегает код этой ветки и перейдёт к следующей. Образец следующей ветки — Ordering::Greater
, который соответствует Ordering::Greater
! Код этой ветки будет выполнен и напечатает Too big!
на экран. Выражение match
заканчивается после первого успешного совпадения, поэтому в этом сценарии оно не будет рассматривать последнюю ветку.
Однако код в приложении 2-4 всё ещё не собирается. Давайте попробуем:
- -$ cargo build
- Compiling libc v0.2.86
- Compiling getrandom v0.2.2
- Compiling cfg-if v1.0.0
- Compiling ppv-lite86 v0.2.10
- Compiling rand_core v0.6.2
- Compiling rand_chacha v0.3.0
- Compiling rand v0.8.5
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
-error[E0308]: mismatched types
- --> src/main.rs:22:21
- |
-22 | match guess.cmp(&secret_number) {
- | --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
- | |
- | arguments to this method are incorrect
- |
- = note: expected reference `&String`
- found reference `&{integer}`
-note: method defined here
- --> /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/cmp.rs:840:8
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
-
-Суть ошибки заключается в наличии несовпадающих видов. У Ржавчина строгая постоянная система видов. Однако в нем также есть рычаг вывода видов. Когда мы написали let mut guess = String::new()
, Ржавчина смог сделать вывод, что guess
должна быть String
и не заставил указывать вид. С другой стороны, secret_number
— это числовой вид. Несколько видов чисел в Ржавчина могут иметь значение от 1 до 100: i32
, 32-битное число; u32
, беззнаковое 32-битное число; i64
, 64-битное число, и так далее. Если не указано иное, Ржавчина по умолчанию использует i32
, который будет видом secret_number
, если вы не добавите сведения о виде где-то ещё, чтобы заставить Ржавчина вывести другой числовой вид. Причина ошибки заключается в том, что Ржавчина не может сравнить строку и числовой вид.
В конечном итоге необходимо преобразовать String
, считываемую программой в качестве входных данных, в существующий числовой вид, чтобы иметь возможность числового сравнения с загаданным числом. Для этого добавьте в тело функции main
следующую строку:
Имя файла: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- // --snip--
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
-Вот эта строка:
-let guess: u32 = guess.trim().parse().expect("Please type a number!");
-Мы создаём переменную с именем guess
. Но подождите, разве в программе уже нет переменной с этим именем guess
? Так и есть, но Ржавчина позволяет нам затенять предыдущее значение guess
новым. Затенение позволяет нам повторно использовать имя переменной guess
, чтобы избежать создания двух единственных переменных, таких как guess_str
и guess
, например. Мы рассмотрим это более подробно в главе 3, а пока знайте, что эта функция часто используется, когда необходимо преобразовать значение из одного вида в другой.
Мы связываем эту новую переменную с выражением guess.trim().parse()
. Переменная guess
в этом выражении относится к исходной переменной guess
, которая содержала входные данные в виде строки. Способ trim
на образце String
удалит любые пробельные символы в начале и конце строки для того, чтобы мы могли сопоставить строку с u32
, который содержит только числовые данные. Пользователь должен нажать enter, чтобы выполнить read_line
и ввести свою догадку, при этом в строку добавится символ новой строки. Например, если пользователь набирает 5 и нажимает enter, guess
будет выглядеть так: 5\n
. Символ \n
означает "новая строка". (В Windows нажатие enter сопровождается возвратом каретки и новой строкой, \r\n
). Способ trim
убирает \n
или \r\n
, оставляя только 5
.
Способ parse
строк преобразует строку в другой вид. Здесь мы используем его для преобразования строки в число. Нам нужно сообщить Ржавчина точный числовой вид, который мы хотим получить, используя let guess: u32
. Двоеточие ( :
) после guess
говорит Rust, что мы определяем вид переменной. В Ржавчина есть несколько встроенных числовых видов; u32
, показанный здесь, представляет собой 32-битное целое число без знака. Это хороший выбор по умолчанию для небольшого положительного числа. Вы узнаете о других видах чисел в главе 3.
Кроме того, изложение u32
в этом примере программы и сравнение с secret_number
означает, что Ржавчина сделает вывод, что secret_number
должен быть u32
. Итак, теперь сравнение будет между двумя значениями одного типа!
Способ parse
будет работать только с символами, которые логически могут быть преобразованы в числа, и поэтому легко может вызвать ошибки. Если, например, строка содержит A👍%
, преобразовать её в число невозможно. Так как способ parse
может потерпеть неудачу, он возвращает вид Result
— так же как и способ read_line
(обсуждалось ранее в разделе «Обработка возможной ошибки с помощью вида Result
»). Мы будем точно так же обрабатывать данный Result
, вновь используя способ expect
. Если parse
вернёт исход Result
Err
, так как не смог создать число из строки, вызов expect
со сбоем завершит игру и отобразит переданное ему сообщение. Если parse
сможет успешно преобразовать строку в число, он вернёт исход Result
Ok
, а expect
вернёт число, полученное из значения Ok
.
Давайте запустим программу теперь:
- -$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 0.43s
- Running `target/debug/guessing_game`
-Guess the number!
-The secret number is: 58
-Please input your guess.
- 76
-You guessed: 76
-Too big!
-
-Хорошо! Несмотря на то, что были добавлены пробелы в строке ввода, программа всё равно поняла, что пользователь имел в виду число 76. Запустите программу несколько раз, чтобы проверить разное поведение при различных видах ввода: задайте число правильно, задайте слишком большое число и задайте слишком маленькое число.
-Сейчас у нас работает большая часть игры, но пользователь может сделать только одну догадку. Давайте изменим это, добавив цикл!
-Ключевое слово loop
создаёт бесконечный цикл. Мы добавляем цикл, чтобы дать пользователям больше шансов угадать число:
Имя файла: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- // --snip--
-
- println!("The secret number is: {secret_number}");
-
- loop {
- println!("Please input your guess.");
-
- // --snip--
-
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
- }
-}
-Как видите, мы перемеисполнения всё, начиная с подсказки ввода догадки, в цикл. Не забудьте добавить ещё по четыре пробела на отступы строк внутри цикла и запустите программу снова. Теперь программа будет бесконечно запрашивать ещё одну догадку, что в действительности создаёт новую неполадку. Похоже, пользователь не сможет выйти из игры!
-Пользователь может прервать выполнение программы с помощью сочетания клавиш ctrl+c. Но есть и другой способ спастись от этого ненасытного монстра, о котором говорилось при обсуждении parse
в «Сравнение догадки с тайным числом»: если пользователь введёт нечисловой ответ, программа завершится со сбоем. Мы можем воспользоваться этим, чтобы позволить пользователю выйти из игры, как показано здесь:
$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 1.50s
- Running `target/debug/guessing_game`
-Guess the number!
-The secret number is: 59
-Please input your guess.
-45
-You guessed: 45
-Too small!
-Please input your guess.
-60
-You guessed: 60
-Too big!
-Please input your guess.
-59
-You guessed: 59
-You win!
-Please input your guess.
-quit
-thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Ввод quit
приведёт к выходу из игры, но, как вы заметите, так же будет и при любом другом нечисловом вводе. Однако это, мягко говоря, не разумно. Мы хотим, чтобы игра самостоятельно остановилась, когда будет угадано правильное число.
Давайте запрограммируем игру на выход при выигрыше пользователя, добавив оператор break
:
Файл: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- loop {
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- println!("You guessed: {guess}");
-
- // --snip--
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
-Добавление строки break
после You win!
заставляет программу выйти из цикла, когда пользователь правильно угадает тайное число. Выход из цикла также означает выход из программы, так как цикл является последней частью main
.
Чтобы улучшить поведение игры, вместо со сбоемго завершения программы, когда пользователь вводит не число, давайте заставим игру пренебрегать этотобстоятельство, позволяя пользователю продолжить угадывание. Для этого необходимо изменить строку, в которой guess
преобразуется из String
в u32
, как показано в приложении 2-5.
Файл: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- loop {
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- // --snip--
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- println!("You guessed: {guess}");
-
- // --snip--
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
--
Мы заменяем вызов expect
на выражение match
, чтобы перейти от со сбоемго завершения при ошибке к обработке ошибки. Помните, что parse
возвращает вид Result
, а Result
— это перечисление, которое имеет исходы Ok
и Err
. Здесь мы используем выражение match
, как и в случае с итогом Ordering
способа cmp
.
Если parse
успешно преобразует строку в число, он вернёт значение Ok
, содержащее полученное число. Это значение Ok
будет соответствовать образцу первой ветки, а выражение match
просто вернёт значение num
, которое parse
произвёл и поместил внутрь значения Ok
. Это число окажется в нужной нам переменной guess
, которую мы создали.
Если способ parse
не способен превратить строку в число, он вернёт значение Err
, которое содержит более подробную сведения об ошибке. Значение Err
не совпадает с образцом Ok(num)
в первой ветке match
, но совпадает с образцом Err(_)
второй ветки. Подчёркивание _
является всеохватывающим выражением. В этой ветке мы говорим, что хотим обработать совпадение всех значений Err
, независимо от того, какая сведения находится внутри. Поэтому программа выполнит код второй ветки, continue
, который сообщает программе перейти к следующей повторения loop
и запросить ещё одну догадку. В этом случае программа эффективно пренебрегает все ошибки, с которыми parse
может столкнуться!
Всё в программе теперь должно работать как положено. Давайте попробуем:
- -$ cargo run
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 4.45s
- Running `target/debug/guessing_game`
-Guess the number!
-The secret number is: 61
-Please input your guess.
-10
-You guessed: 10
-Too small!
-Please input your guess.
-99
-You guessed: 99
-Too big!
-Please input your guess.
-foo
-Please input your guess.
-61
-You guessed: 61
-You win!
-
-Потрясающе! С помощью одной маленькой последней правки мы закончим игру в угадывание. Напомним, что программа все ещё печатает тайное число. Это хорошо подходило для проверки, но это портит игру. Давайте удалим println!
, который выводит тайное число. В Приложении 2-6 показан окончательный исход кода.
Файл: src/main.rs
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- loop {
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
--
На данный мгновение вы успешно создали игру в загадки. Поздравляем!
-Этот дело — опытный способ познакомить вас со многими новыми подходами Rust: let
, match
, функции, использование внешних ящиков и многое другое. В следующих нескольких главах вы изучите эти подходы более подробно. Глава 3 охватывает понятия, которые есть в большинстве языков программирования, такие как переменные, виды данных и функции, и показывает, как использовать их в Rust. В главе 4 рассматривается владение — особенность, которая отличает Ржавчина от других языков. В главе 5 обсуждаются устройства и правила написания способов, а в главе 6 объясняется, как работают перечисления.
В этой главе рассматриваются подходы, присутствующие почти в каждом языке программирования, и то, как они работают в Rust. В основе большинства языков программирования есть много общего. Все подходы, представленные в этой главе, не являются единственными для Rust, но мы обсудим их в среде Ржавчина и разъясним правила использования этих подходов.
-В частности вы изучите переменные, основные виды, функции, примечания и поток управления. Эти фундаментальные понятия будут присутствовать в каждой программе на Rust, и их изучение на ранней стадии даст вам прочную основу для начала работы.
--- -Ключевые слова
-В языке Ржавчина как и в других языках есть набор ключевых слов, зарезервированных только для использования в языке. Помните, что нельзя использовать эти слова в качестве имён переменных или функций. Большинство этих ключевых слов имеют особые назначения, и вы будете использовать их для выполнения различных задач в своих программах на Rust. Некоторые из них сейчас не имеют функционального назначения, но зарезервированы для возможности, которая может быть добавлена в Ржавчина в будущем. Список ключевых слов вы можете найти в Приложении А.
-
Как упоминалось в разделе "Хранение значений с помощью переменных", по умолчанию переменные неизменяемы. Это один из многих стимулов Rust, позволяющий писать код с использованием преимущества безопасности и удобной состязательности (concurrency), предоставляемых Rust. Тем не менее, существует возможность сделать переменные изменяемыми. Давайте рассмотрим, как и почему Ржавчина побуждает предпочесть неизменяемость и почему иногда можно отказаться от этого.
-Если переменная является неизменяемой, то после привязки значения к имени изменить его будет нельзя. Чтобы показать это, создайте новый дело под названием variables в папке projects с помощью приказы cargo new variables
.
Далее, в новом папке variables откройте src/main.rs и замените в нем код на ниже приведённый, который пока не будет собираться:
-Имя файла: src/main.rs
-fn main() {
-let x = 5;
-println!("The value of x is: {}", x);
-x = 6;
-println!("The value of x is: {}", x);
-}
-Сохраните и запустите программу, используя cargo run
. Будет получено сообщение об ошибке относительно неизменяемости, как показано в этом выводе:
error[E0384]: cannot assign twice to immutable variable `x` --> src/main.rs:4:5 | 2 | let x = 5; | - first assignment to `x` 3 | println!("The value of x is: {}", x); 4 | x = 6; | ^^^^^ cannot assign twice to immutable variable
-
-В этом примере показано, как сборщик помогает находить ошибки в ваших программах. Ошибки сборщика могут расстраивать, но в действительности они означают, что программа пока не делает правильно то, что вы ожидаете; это не значит, что вы плохой программист! Даже опытные Rustaceans иногда сталкиваются с ошибками сборщика.
-Вы получили сообщение об ошибке cannot assign twice to immutable variable
x``, потому что попытались присвоить новое значение неизменяемой переменной x
.
Важно, чтобы при попытке изменить значение, объявленное неизменяемым, выдавались ошибки времени сборки, так как подобная случаей может привести к сбоям. Если одна часть нашего кода исполняется исходя из уверенности в неизменяемости значения, а другая часть изменяет это значение, то велика вероятность , что первая часть не выполнит своего предназначения. Причину такой ошибки бывает трудно отследить, особенно если вторая часть кода изменяет значение лишь изредка. Сборщик Ржавчина предоставляет заверение, что если объявить значение неизменяемым, то оно действительно не изменится, а значит, не нужно следить за этим самим. Таким образом, ваш код становится проще для понимания.
-Однако изменяемость может быть очень полезной и может сделать код более удобным для написания. Хотя переменные по умолчанию неизменяемы, их можно сделать изменяемыми, добавив mut
перед именем переменной, как это было сделано в Главе 2. Добавление mut
также передаёт будущим читателям кода намерение, обозначая, что другие части кода будут изменять значение этой переменной.
Например, изменим src/main.rs на следующий код:
-Имя файла: src/main.rs
--fn main() { - let mut x = 5; - println!("The value of x is: {x}"); - x = 6; - println!("The value of x is: {x}"); -}
Запустив программу, мы получим итог:
-$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
- Running `target/debug/variables`
-The value of x is: 5
-The value of x is: 6
-
-Нам разрешено изменить значение, связанное с x, с 5 на 6 при помощи mut. В конечном счёте, решение об использовании изменяемости остаётся за вами и зависит от вашего мнения о наилучшем исходе в данной именно случаи.
-Подобно неизменяемым переменным, постоянные значения — это значения, которые связаны с именем и не могут изменяться, но между постоянными значениями и переменными есть несколько различий.
-Во-первых, нельзя использовать mut
с постоянными значениями. Постоянного значения не просто неизменяемы по умолчанию — они неизменяемы всегда. Для объявления постоянных значенийиспользуется ключевое слово const
вместо let
, а также вид значения должен быть указан в изложении. Мы рассмотрим виды и изложении видов в следующем разделе «Виды данных»., так что не беспокойтесь о подробностях прямо сейчас. Просто знайте, что вы всегда должны определять вид.
Постоянного значения можно объявлять в любой области видимости, включая вездесущую, благодаря этому они полезны для значений, которые нужны во многих частях кода.
-Последнее отличие в том, что постоянные значения могут быть заданы только постоянным выражением, но не итогом вычисленного во время выполнения значения.
-Вот пример объявления постоянные значения:
--#![allow(unused)] -fn main() { -const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; -}
Имя постоянные значения - THREE_HOURS_IN_SECONDS
, а её значение устанавливается как итог умножения 60 (количество секунд в минуте) на 60 (количество минут в часе) на 3 (количество часов, которые нужно посчитать в этой программе). Соглашение Ржавчина для именования постоянных значенийтребует использования всех заглавных букв с подчёркиванием между словами. Сборщик может вычислять ограниченный набор действий во время сборки, позволяющий записать это значение более понятным и простым для проверки способом, чем установка этой постоянные значения в значение 10 800. Дополнительную сведения о том, какие действия можно использовать при объявлении постоянных значений, см. в разделе Раздел справки Ржавчина по вычислениям постоянных значений.
Постоянного значения существуют в течение всего времени работы программы в пределах области, в которой они были объявлены. Это свойство делает постоянные значения полезными для значений в домене вашего приложения, о которых могут знать несколько частей программы, например, наибольшее количество очков, которое может заработать любой игрок в игре, или скорость света.
-Обозначение жёстко закодированных значений, используемых в программе, как постоянные значения полезно для передачи смысла этого значения будущим сопровождающим кода. Это также позволяет иметь единственное место в коде, которое нужно будет изменить, если в будущем потребуется обновить значение.
-Как было показано в уроке по игре в Угадайка в главе 2, можно объявить новую переменную с тем же именем, как и у существующей переменной. Rustaceans говорят, что первая переменная затеняется второй, то есть вторая переменная - это то, что увидит сборщик, когда вы будете использовать имя переменной. По сути, вторая переменная затеняет первую, принимая любое использование имени переменной на себя до тех пор, пока либо она сама не станет тенью, либо не закончится область видимости. Мы можем затенять переменную, используя то же имя переменной и повторяя использование ключевого слова let
следующим образом:
Имя файла: src/main.rs
--fn main() { - let x = 5; - - let x = x + 1; - - { - let x = x * 2; - println!("The value of x in the inner scope is: {x}"); - } - - println!("The value of x is: {x}"); -}
Эта программа сначала привязывает x
к значению 5
. Затем она создаёт новую переменную x
, повторяя let x =
, беря исходное значение и добавляя 1
, чтобы значение x
стало равным 6
. Затем во внутренней области видимости, созданной с помощью фигурных скобок, третий оператор let
также затеняет x
и создаёт новую переменную, умножая предыдущее значение на 2
, чтобы дать x
значение 12
. Когда эта область заканчивается, внутреннее затенение заканчивается, и x
возвращается к значению 6
. Запустив эту программу, она выведет следующее:
$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/variables`
-The value of x in the inner scope is: 12
-The value of x is: 6
-
-Затенение отличается от объявления переменной с помощью mut
, так как мы получим ошибку сборки, если случайно попробуем переназначить значение без использования ключевого слова let
. Используя let
, можно выполнить несколько превращений над значением, при этом оставляя переменную неизменяемой, после того как все эти превращения завершены.
Другой разницей между mut
и затенением является то, что мы создаём совершенно новую переменную, когда снова используем слово let
(ещё одну). Мы можем даже изменить вид значения, но снова использовать прежнее имя. К примеру, наша программа спрашивает пользователя, сколько пробелов он хочет разместить между некоторым текстом, запрашивая символы пробела, но мы на самом деле хотим сохранить данный ввод как число:
-fn main() { - let spaces = " "; - let spaces = spaces.len(); -}
Первая переменная spaces
— является строковым видом, а вторая переменная spaces
— числовым видом. Таким образом, затенение избавляет нас от необходимости придумывать разные имена, такие как spaces_str
и spaces_num
. Вместо этого мы можем повторно использовать более простое имя spaces
. Однако, если мы попытаемся использовать для этого mut
, как показано далее, то получим ошибку времени сборки:
fn main() {
- let mut spaces = " ";
- spaces = spaces.len();
-}
-Ошибка говорит, что не разрешается менять вид переменной:
-$ cargo run
- Compiling variables v0.1.0 (file:///projects/variables)
-error[E0308]: mismatched types
- --> src/main.rs:3:14
- |
-2 | let mut spaces = " ";
- | ----- expected due to this value
-3 | spaces = spaces.len();
- | ^^^^^^^^^^^^ expected `&str`, found `usize`
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `variables` (bin "variables") due to 1 previous error
-
-Теперь, когда мы изучили, как работают переменные, давайте рассмотрим различные виды данных, которые они могут иметь.
- -Каждое значение в Ржавчина относится к определённому виду данных, который указывает на вид данных, что позволяет Ржавчина знать, как работать с этими данными. Мы рассмотрим два подмножества видов данных: одиночные и составные.
-Не забывайте, что Ржавчина является постоянно строго определенным (statically typed) языком. Это означает, что он должен знать виды всех переменных во время сборки. Обычно сборщик может предположить, какой вид используется (вывести его), основываясь на значении и на том, как мы с ним работаем. В случаях, когда может быть выведено несколько видов, необходимо добавлять изложение вида вручную. Например, когда мы преобразовали String
в число с помощью вызова parse
в разделе «Сравнение предположения с загаданным номером» главы 2, мы должны добавить такую изложение:
-#![allow(unused)] -fn main() { -let guess: u32 = "42".parse().expect("Not a number!"); -}
Если мы не добавим изложение вида : u32
, показанную в предыдущем коде, Ржавчина отобразит следующую ошибку, которая означает, что сборщику нужно от нас больше сведений, чтобы узнать, какой вид мы хотим использовать:
$ cargo build
- Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
-error[E0284]: type annotations needed
- --> src/main.rs:2:9
- |
-2 | let guess = "42".parse().expect("Not a number!");
- | ^^^^^ ----- type must be known at this point
- |
- = note: cannot satisfy `<_ as FromStr>::Err == _`
-help: consider giving `guess` an explicit type
- |
-2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
- | ++++++++++++
-
-For more information about this error, try `rustc --explain E0284`.
-error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
-
-В будущем вы увидите различные изложении для разных видов данных.
-Одиночный вид представляет собой единичное значение. В Ржавчина есть четыре основных одиночных вида: целочисленный, числа с плавающей точкой, логический и символы. Вы наверняка знакомы с этими видами по другим языкам программирования. Давайте разберёмся, как они работают в Rust.
-Целочисленный вид (integer) — это число без дробной части. В главе 2 мы использовали один целочисленный вид — вид u32
. Такое объявление вида указывает, что значение, с которым оно связано, должно быть целым числом без знака (виды целых чисел со знаком начинаются с i
вместо u
), которое занимает 32 бита памяти. В Таблице 3-1 показаны встроенные целочисленные виды в Rust. Мы можем использовать любой из этих исходов для объявления вида целочисленного значения.
-
Длина | Со знаком | Без знака |
---|---|---|
8 бит | i8 | u8 |
16 бит | i16 | u16 |
32 бита | i32 | u32 |
64 бита | i64 | u64 |
128 бит | i128 | u128 |
архитектурно-зависимая | isize | usize |
Каждый исход может быть как со знаком, так и без знака и имеет явный размер. Такая свойство вида как знаковый и беззнаковый определяет возможность числа быть отрицательным. Другими словами, должно ли число иметь знак (знаковое) или оно всегда будет только положительным и, следовательно, может быть представлено без знака (беззнаковое). Это похоже на написание чисел на бумаге: когда знак имеет значение, число отображается со знаком плюс или со знаком -; однако, когда можно с уверенностью предположить, что число положительное, оно отображается без знака. Числа со знаком хранятся с использованием дополнительного кода.
-Каждый исход со знаком может хранить числа от -(2 n - 1 ) до 2 n - 1 - 1 включительно, где n — количество битов, которые использует этот исход. Таким образом, i8
может хранить числа от -(2 7 ) до 2 7 - 1, что равно значениям от -128 до 127. Исходы без знака могут хранить числа от 0 до 2 n - 1, поэтому u8
может хранить числа от 0 до 2 8 - 1, что равно значениям от 0 до 255.
Кроме того, виды isize
и usize
зависят от архитектуры компьютера, на котором выполняется программа, и обозначаются в таблице как "arch": 64 бита, если используется 64-битная архитектура, и 32 бита, если используется 32-битная архитектура.
Вы можете записывать целочисленные записи в любой из разновидностей, показанных в таблице 3-2. Заметьте, что числовые записи, имеющие несколько числовых видов, допускают использование вставки вида, например 57u8
, для обозначения вида. Числовые записи также могут использовать _
в качестве визуального разделителя для облегчения чтения числа, например 1_000
, который будет иметь такое же значение, как если бы было задано 1000
.
-
Числовой запись | Пример |
---|---|
Десятичный | 98_222 |
Шестнадцатеричный | 0xff |
восьмеричный | 0o77 |
Двоичный | 0b1111_0000 |
Байт (только u8 ) | b'A' |
Как же узнать, какой вид целого числа использовать? Если вы не уверены, значения по умолчанию в Rust, как правило, подходят для начала: целочисленные виды по умолчанию i32
. Основной случай, в котором вы должны использовать isize
или usize
, — это упорядочевание какой-либо собрания.
---
Целочисленное переполнение Допустим, имеется переменная видаu8
, которая может хранить значения от 0 до 255. Если попытаться изменить переменную на значение вне этого ряда, например, 256, произойдёт целочисленное переполнение, что может привести к одному из двух исходов поведения. Если выполняется сборка в режиме отладки, Ржавчина включает проверку на целочисленное переполнение, приводящую вашу программу к панике во время выполнения, когда возникает такое поведение. Ржавчина использует понятие паника(panicking), когда программа завершается с ошибкой. Мы обсудим панику более подробно в разделе "Неустранимые ошибки сpanic!
" в главе 9. . При сборки в режиме release с флагом--release
, Ржавчина не включает проверки на целочисленное переполнение, которое вызывает панику. Вместо этого, в случае переполнения, Ржавчина выполняет обёртывание второго дополнения. Проще говоря, значения, превышающие наибольшее значение, которое может хранить вид, "оборачиваются" к наименьшему из значений, которые может хранить вид. В случаеu8
значение 256 становится 0, значение 257 становится 1, и так далее. Программа не запаникует, но переменная будет иметь значение, которое, вероятно, не будет соответствовать вашим ожиданиям. Полагаться на поведение обёртывания целочисленного переполнения считается ошибкой. Для явной обработки возможности переполнения существует семейство способов, предоставляемых встроенной библиотекой для простых числовых видов:-
-- Обёртывание во всех режимах с помощью способов
-wrapping_*
, таких какwrapping_add
.- Возврат значения
-None
при переполнении с помощью способовchecked_*
.- Возврат значения и логический индикатор, указывающий, произошло ли переполнение при использовании способов
-overflowing_*
.- Насыщение наименьшим или наибольшим значением с помощью способов
-saturating_*
.
Также в Ржавчина есть два простых вида для чисел с плавающей запятой, представляющих собой числа с десятичной точкой. Виды с плавающей точкой в Ржавчина - это f32 и f64, размер которых составляет 32 бита и 64 бита соответственно. По умолчанию используется вид f64, поскольку на современных процессорах он работает примерно с той же скоростью, как и f32, но обладает большей точностью. Все виды с плавающей запятой являются знаковыми.
-Вот пример, отображающий числа с плавающей запятой в действии:
-Файл: src/main.rs
--fn main() { - let x = 2.0; // f64 - - let y: f32 = 3.0; // f32 -}
Числа с плавающей запятой представлены в соответствии со исполнением IEEE-754. Вид f32
является плавающей запятой одинарной точности, а f64
- двойной точности.
Rust поддерживает основные математические действия, привычные для всех видов чисел: сложение, вычитание, умножение, деление и остаток. Целочисленное деление обрезает значение в направлении нуля до ближайшего целого числа. Следующий код показывает, как можно использовать каждую числовую действие в указания let
:
Файл: src/main.rs
--fn main() { - // addition - let sum = 5 + 10; - - // subtraction - let difference = 95.5 - 4.3; - - // multiplication - let product = 4 * 30; - - // division - let quotient = 56.7 / 32.2; - let truncated = -5 / 3; // Results in -1 - - // remainder - let remainder = 43 % 5; -}
Каждое выражение в этих указаниях использует математический оператор и вычисляется в одно значение, которое связывается с переменной. Приложении B содержит список всех операторов, которые предоставляет Rust.
-Как и в большинстве других языков программирования, логический вид в Ржавчина имеет два возможных значения: true
и false
. Значения логических видов имеют размер в один байт. Логический вид в Ржавчина задаётся с помощью bool
. Например:
Файл: src/main.rs
--fn main() { - let t = true; - - let f: bool = false; // with explicit type annotation -}
Основной способ использования логических значений - это использование условий, таких как выражение if
. Мы рассмотрим, как выражения if
работают в Ржавчина в разделе "Поток управления".
Вид char
в Ржавчина является самым простым алфавитным видом языка. Вот несколько примеров объявления значений char
:
Файл: src/main.rs
--fn main() { - let c = 'z'; - let z: char = 'ℤ'; // with explicit type annotation - let heart_eyed_cat = '😻'; -}
Заметьте, мы указываем записи char
с одинарными кавычками, в отличие от строковых записей, для которых используются двойные кавычки. Вид char
в Ржавчина имеет размер четыре байта и представляет собой одиночное значение Unicode, а значит, может представлять собой не только ASCII. Акцентированные буквы, китайские, японские и корейские символы, эмодзи и пробелы нулевой ширины - все это допустимые значения вида char
в Rust. Одиночные значения Unicode находятся в ряде от U+0000
до U+D7FF
и от U+E000
до U+10FFFF
включительно. Однако "символ" не является понятием в Unicode, поэтому ваше человеческое представление о том, что такое "символ", может не совпадать с тем, что такое char
в Rust. Мы подробно обсудим эту тему в главе 8 "Хранение текста в кодировке UTF-8 с помощью строк".
Составные виды могут объединять различные значения в один вид. В Ржавчина есть два простых составных вида: упорядоченные ряды и массивы.
-Упорядоченный ряд- это гибкий способ объединения нескольких значений с различными видами в один составной вид. Упорядоченные ряды имеют конечную длину: после объявления они не могут увеличиваться или уменьшаться в размерах.
-Мы создаём упорядоченный ряд, записывая список значений, разделённых запятыми, внутри круглых скобок. Каждая позиция в упорядоченном ряде имеет вид, причём виды различных значений в упорядоченном ряде не обязательно должны быть одинаковыми. В этом примере мы добавили необязательные изложении видов:
-Файл: src/main.rs
--fn main() { - let tup: (i32, f64, u8) = (500, 6.4, 1); -}
Переменная tup
связана со всем упорядоченным рядом, поскольку упорядоченный ряд является одним составным элементом. Чтобы получить отдельные значения из упорядоченного ряда, можно использовать сопоставление с образцом для разъединения значения упорядоченного ряда, например, так:
Файл: src/main.rs
--fn main() { - let tup = (500, 6.4, 1); - - let (x, y, z) = tup; - - println!("The value of y is: {y}"); -}
Эта программа сначала создаёт упорядоченный ряд и связывает его с переменной tup
. Затем с помощью образца let
берётся tup
и превращается в три отдельные переменные, x
, y
и z
. Это называется разъединением, поскольку разбивает единый упорядоченный ряд на три части. Наконец, программа печатает значение y
, которое равно 6.4
.
Мы также можем получить доступ к элементу упорядоченного ряда напрямую, используя точку (.
), за которой следует порядковый указательзначения, требуемого для доступа. Например:
Файл: src/main.rs
--fn main() { - let x: (i32, f64, u8) = (500, 6.4, 1); - - let five_hundred = x.0; - - let six_point_four = x.1; - - let one = x.2; -}
Эта программа создаёт упорядоченный ряд x
, а затем обращается к каждому элементу упорядоченного ряда, используя соответствующие порядковые указатели. Как и в большинстве языков программирования, первый порядковый указательв упорядоченном ряде равен 0.
Упорядоченный ряд, не имеющий значений, имеет особое имя единичный вид (unit). Это значение и соответствующий ему вид записываются как ()
и представляет собой пустое значение или пустой возвращаемый вид. Выражения неявно возвращают значение единичного вида, если не возвращают никакого другого значения.
Другим способом создания собрания из нескольких значений является массив array. В отличие от упорядоченного ряда, каждый элемент массива должен иметь один и тот же вид. В отличие от массивов в некоторых других языках, массивы в Ржавчина имеют конечную длину.
-Мы записываем значения в массиве в виде списка, разделённого запятыми, внутри квадратных скобок:
-Файл: src/main.rs
--fn main() { - let a = [1, 2, 3, 4, 5]; -}
Массивы удобно использовать, если данные необходимо разместить в обойме, а не в куче (мы подробнее обсудим обойма и кучу в Главе 4) или если требуется, чтобы количество элементов всегда было конечным. Однако массив не так гибок, как вектор. Вектор - это подобный вид собрания, предоставляемый встроенной библиотекой, который может увеличиваться или уменьшаться в размере. Если вы не уверены, что лучше использовать - массив или вектор, то, скорее всего, вам следует использовать вектор. Более подробно векторы рассматриваются в Главе 8.
-Однако массивы более полезны, когда вы знаете, что количество элементов не нужно будет изменять. Например, если бы вы использовали названия месяцев в программе, вы, вероятно, использовали бы массив, а не вектор, потому что вы знаете, что он всегда будет содержать 12 элементов:
--#![allow(unused)] -fn main() { -let months = ["January", "February", "March", "April", "May", "June", "July", - "August", "September", "October", "November", "December"]; -}
Вид массива записывается следующим образом: в квадратных скобках обозначается вид элементов массива, а затем, через точку с запятой, количество элементов. Например:
--#![allow(unused)] -fn main() { -let a: [i32; 5] = [1, 2, 3, 4, 5]; -}
Здесь i32
является видом каждого элемента массива. После точки с запятой указано число 5
, показывающее, что массив содержит 5 элементов.
Вы также можете объявить массив, содержащий одно и то же значение для каждого элемента, указав это значение вместо вида. Следом за этим так же следует точка с запятой, а затем — длина массива в квадратных скобках, как показано здесь:
--#![allow(unused)] -fn main() { -let a = [3; 5]; -}
Массив в переменной a
будет включать 5
элементов, значение которых будет равно 3
. Данная запись подобна коду let a = [3, 3, 3, 3, 3];
, но является более краткой.
Массив — это единый отрывок памяти известного конечного размера, который может быть размещён в обойме. Вы можете получить доступ к элементам массива с помощью упорядочевания, например:
-Файл: src/main.rs
--fn main() { - let a = [1, 2, 3, 4, 5]; - - let first = a[0]; - let second = a[1]; -}
В этом примере переменная с именем first получит значение 1, потому что это значение находится по порядковому указателю [0] в массиве. Переменная с именем second получит значение 2 по порядковому указателю [1] в массиве.
-Давайте посмотрим, что произойдёт, если попытаться получить доступ к элементу массива, находящемуся за его пределами. Допустим, вы запускаете данный код, похожий на игру в угадывание из Главы 2, чтобы получить от пользователя порядковый указательмассива:
-Файл: src/main.rs
-use std::io;
-
-fn main() {
- let a = [1, 2, 3, 4, 5];
-
- println!("Please enter an array index.");
-
- let mut index = String::new();
-
- io::stdin()
- .read_line(&mut index)
- .expect("Failed to read line");
-
- let index: usize = index
- .trim()
- .parse()
- .expect("Index entered was not a number");
-
- let element = a[index];
-
- println!("The value of the element at index {index} is: {element}");
-}
-Этот код успешно собирается. Если запустить этот код с помощью cargo run
и ввести 0
, 1
, 2
, 3
или 4
, программа напечатает соответствующее значение по данному порядковому указателю в массиве. Если вместо этого ввести число за пределами массива, например, 10
, то программа выведет следующее:
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Программа столкнулась с ошибкой во времени выполнения на этапе применения недопустимого значения в действия упорядочевания. Программа завершилась с сообщением об ошибке и не выполнила конечную указанию println!
. При попытке доступа к элементу с помощью упорядочевания Ржавчина проверяет, что указанный порядковый указательменьше длины массива. Если порядковый указательбольше или равен длине, Ржавчина паникует. Эта проверка должна происходить во время выполнения, особенно в данном случае, потому что сборщик не может знать, какое значение введёт пользователь при последующем выполнении кода.
Это пример принципов безопасности памяти Ржавчина в действии. Во многих низкоуровневых языках такая проверка не выполняется, и когда вы указываете неправильный порядковый указатель, доступ к памяти может быть неправильным. Ржавчина защищает вас от такого рода ошибок, немедленно закрываясь вместо того, чтобы разрешать доступ к памяти и продолжать работу. В главе 9 подробнее обсуждается обработка ошибок в Ржавчина и то, как вы можете написать читаемый, безопасный код, который не вызывает панику и не разрешает неправильный доступ к памяти.
- -Функции широко распространены в коде Rust. Вы уже познакомились с одной из самых важных функций в языке: функцией main
, которая является точкой входа большинства программ. Вы также видели ключевое слово fn
, позволяющее объявлять новые функции.
Код Ржавчина использует змеиный регистр (snake case) как основной исполнение для имён функций и переменных, в котором все буквы строчные, а символ подчёркивания разделяет слова. Вот программа, содержащая пример определения функции:
-Имя файла: src/main.rs
--fn main() { - println!("Hello, world!"); - - another_function(); -} - -fn another_function() { - println!("Another function."); -}
Для определения функции в Ржавчина необходимо указать fn
, за которым следует имя функции и набор круглых скобок. Фигурные скобки указывают сборщику, где начинается и заканчивается тело функции.
Мы можем вызвать любую функцию, которую мы определили ранее, введя её имя и набор скобок следом. Поскольку в программе определена another_function
, её можно вызвать из функции main
. Обратите внимание, что another_function
определена после функции main
в исходном коде; мы могли бы определить её и раньше. Ржавчина не важно, где вы определяете свои функции, главное, чтобы они были определены где-то в той области видимости, которую может видеть вызывающий их код.
Создадим новый двоичный дело с названием functions для дальнейшего изучения функций. Поместите пример another_function
в файл src/main.rs и запустите его. Вы должны увидеть следующий вывод:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
- Running `target/debug/functions`
-Hello, world!
-Another function.
-
-Строки выполняются в том порядке, в котором они расположены в функции main
. Сначала печатается сообщение "Hello, world!", а затем вызывается another_function
, которая также печатает сообщение.
Мы можем определить функции, имеющие свойства, которые представляют собой особые переменные, являющиеся частью ярлыки функции. Когда у функции есть свойства, необходимо предоставить ей определенные значения этих свойств. Технически определенные значения называются переменные, но в повседневном общении люди обычно используют слова свойство и переменная как взаимозаменяемые либо для переменных в определении функции, либо для определенных значений, передаваемых при вызове функции.
-В этой исполнения another_function
мы добавляем свойство:
Имя файла: src/main.rs
--fn main() { - another_function(5); -} - -fn another_function(x: i32) { - println!("The value of x is: {x}"); -}
Попробуйте запустить эту программу. Должны получить следующий итог:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
- Running `target/debug/functions`
-The value of x is: 5
-
-Объявление another_function
содержит один свойство с именем x
. Вид x
задан как i32
. Когда мы передаём 5
в another_function
, макрос println!
помещает 5
на место пары фигурных скобок, содержащих x
в строке вида.
В ярлыках функций вы обязаны указывать вид каждого свойства. Это намеренное решение в внешнем виде Rust: требование наставлений видов в определениях функций позволяет сборщику в дальнейшем избежать необходимости использовать их в других местах кода, чтобы определить, какой вид вы имеете в виду. Сборщик также может выдавать более полезные сообщения об ошибках, если он знает, какие виды ожидает функция.
-При определении нескольких свойств, разделяйте объявления свойств запятыми, как показано ниже:
-Имя файла: src/main.rs
--fn main() { - print_labeled_measurement(5, 'h'); -} - -fn print_labeled_measurement(value: i32, unit_label: char) { - println!("The measurement is: {value}{unit_label}"); -}
Этот пример создаёт функцию под именем print_labeled_measurement
с двумя свойствами. Первый свойство называется value
с видом i32
. Второй называется unit_label
и имеет вид char
. Затем функция печатает текст, содержащий value
и unit_label
.
Попробуем запустить этот код. Замените текущую программу дела functions в файле src/main.rs на предыдущий пример и запустите его с помощью cargo run
:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/functions`
-The measurement is: 5h
-
-Поскольку мы вызвали функцию с 5
в качестве значения для value
и 'h'
в качестве значения для unit_label
, вывод программы содержит эти значения.
Тела функций состоят из ряда указаний, необязательно заканчивающихся выражением. До сих пор функции, которые мы рассматривали, не включали завершающее выражение, но вы видели выражение как часть указания. Поскольку Ржавчина является языком, основанным на выражениях, это важное различие необходимо понимать. В других языках таких различий нет, поэтому давайте рассмотрим, что такое указания и выражения, и как их различия влияют на тела функций.
-На самом деле мы уже использовали указания и выражения. Создание переменной и присвоение ей значения с помощью ключевого слова let
является оператором. В Приложении 3-1, let y = 6;
— это указание.
Имя файла: src/main.rs
--fn main() { - let y = 6; -}
-
Определения функций также являются указанием. Весь предыдущий пример сам по себе является указанием.
-Указания не возвращают значения. Следовательно вы не можете присвоить let
указанию другой переменной, как это пытается сделать следующий код. Вы получите ошибку:
Имя файла: src/main.rs
-fn main() {
- let x = (let y = 6);
-}
-Если вы запустите эту программу, то ошибка будет выглядеть так:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
-error: expected expression, found `let` statement
- --> src/main.rs:2:14
- |
-2 | let x = (let y = 6);
- | ^^^
- |
- = note: only supported directly in conditions of `if` and `while` expressions
-
-warning: unnecessary parentheses around assigned value
- --> src/main.rs:2:13
- |
-2 | let x = (let y = 6);
- | ^ ^
- |
- = note: `#[warn(unused_parens)]` on by default
-help: remove these parentheses
- |
-2 - let x = (let y = 6);
-2 + let x = let y = 6;
- |
-
-warning: `functions` (bin "functions") generated 1 warning
-error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
-
-Указание let y = 6
не возвращает значение, поэтому не с чем связать переменную x
. Это отличается от поведения в других языках, таких как C и Ruby, где присваивание возвращает присвоенное значение. В таких языках можно писать код x = y = 6
и обе переменные x
и y
будут иметь значение 6
. Но в Ржавчина не так.
Выражения вычисляют значение и составляют большую часть остального кода, который вы напишете на Rust. Рассмотрим математическую действие, к примеру 5 + 6
, которая является выражением, вычисляющим значение 11
. Выражения могут быть частью указаний: в приложении 3-1 6
в указания let y = 6;
является выражением, которое вычисляется в значение 6
. Вызов функции — это выражение. Вызов макроса — это выражение. Новый разделобласти видимости, созданный с помощью фигурных скобок, представляет собой выражение, например:
Имя файла: src/main.rs
--fn main() { - let y = { - let x = 3; - x + 1 - }; - - println!("The value of y is: {y}"); -}
Это выражение:
-{
- let x = 3;
- x + 1
-}
-это блок, который в данном случае вычисляется в значение 4
. Это значение связывается с y
как часть указания let
. Обратите внимание, что строка x + 1
не имеет точки с запятой в конце, что отличается от большинства строк, которые вы видели до сих пор. Выражения не содержат завершающих точек с запятой. Если вы добавите точку с запятой в конец выражения, вы превратите его в указанию, и тогда она не будет возвращать значение. Помните об этом, когда будете изучать возвращаемые значения функций и выражения.
Функции могут возвращать значения коду, который их вызывает. Мы не называем возвращаемые значения, но мы должны объявить их вид после стрелки ( ->
). В Ржавчина возвращаемое значение функции является родственным значения конечного выражения в разделе тела функции. Вы можете раньше выйти из функции и вернуть значение, используя ключевое слово return
и указав значение, но большинство функций неявно возвращают последнее выражение. Вот пример такой функции:
Имя файла: src/main.rs
--fn five() -> i32 { - 5 -} - -fn main() { - let x = five(); - - println!("The value of x is: {x}"); -}
В коде функции five
нет вызовов функций, макросов или даже указаний let
— есть только одно число 5
. Это является абсолютно правильной функцией в Rust. Заметьте, что возвращаемый вид у данной функции определён как -> i32
. Попробуйте запустить этот код. Вывод будет таким:
$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
- Running `target/debug/functions`
-The value of x is: 5
-
-Значение 5
в five
является возвращаемым функцией значением, поэтому возвращаемый вид - i32
. Рассмотрим пример более подробно. Здесь есть два важных особенности: во-первых, строка let x = five();
показывает использование возвращаемого функцией значения для объявления переменной. Так как функция five
возвращает 5
, то эта строка эквивалентна следующей:
-#![allow(unused)] -fn main() { -let x = 5; -}
Во-вторых, у функции five
нет свойств и определён вид возвращаемого значения, но тело функции представляет собой одинокую 5
без точки с запятой, потому что это выражение, значение которого мы хотим вернуть.
Рассмотрим другой пример:
-Имя файла: src/main.rs
--fn main() { - let x = plus_one(5); - - println!("The value of x is: {x}"); -} - -fn plus_one(x: i32) -> i32 { - x + 1 -}
Запуск кода напечатает The value of x is: 6
. Но если поставить точку с запятой в конце строки, содержащей x + 1
, превратив её из выражения в указанию, мы получим ошибку:
Имя файла: src/main.rs
-fn main() {
- let x = plus_one(5);
-
- println!("The value of x is: {x}");
-}
-
-fn plus_one(x: i32) -> i32 {
- x + 1;
-}
-Сборка данного кода вызывает следующую ошибку:
-$ cargo run
- Compiling functions v0.1.0 (file:///projects/functions)
-error[E0308]: mismatched types
- --> src/main.rs:7:24
- |
-7 | fn plus_one(x: i32) -> i32 {
- | -------- ^^^ expected `i32`, found `()`
- | |
- | implicitly returns `()` as its body has no tail or `return` expression
-8 | x + 1;
- | - help: remove this semicolon to return this value
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `functions` (bin "functions") due to 1 previous error
-
-Основное сообщение об ошибке, несовпадение видов
, раскрывает ключевую неполадку этого кода. Определение функции plus_one
сообщает, что будет возвращено i32
, но указания не вычисляются в значение, что и выражается единичным видом ()
. Следовательно, ничего не возвращается, что противоречит определению функции и приводит к ошибке. В этом выводе Ржавчина выдаёт сообщение, которое, возможно, поможет исправить эту неполадку: он предлагает удалить точку с запятой для устранения ошибки.
Все программисты стремятся сделать свой код простым для понимания, но иногда требуется дополнительное объяснение. В таких случаях программисты оставляют в исходном коде примечания, которые сборщик пренебрегает, но люди, читающие исходный код, вероятно, сочтут их полезными.
-Пример простого примечания:
--#![allow(unused)] -fn main() { -// Hello, world. -}
В Ржавчина принят идиоматический исполнение примечаниев, который начинает примечание с двух косых черт, и примечание продолжается до конца строки. Для примечаниев, выходящих за пределы одной строки, необходимо включить //
в каждую строку, как показано ниже:
-#![allow(unused)] -fn main() { -// Итак, мы делаем что-то сложное, настолько длинное, что нам нужно -// несколько строк примечаниев, чтобы сделать это! Ух! Надеюсь, этот примечание -// объясняет, что происходит. -}
Примечания также можно размещать в конце строк, содержащих код:
-Имя файла: src/main.rs
--fn main() { - let lucky_number = 7; // I’m feeling lucky today -}
Но чаще всего они используются в таком виде: примечание располагается на отдельной строке над кодом, который он определяет:
-Имя файла: src/main.rs
--fn main() { - // I’m feeling lucky today - let lucky_number = 7; -}
В Ржавчина есть ещё один вид примечаниев - документационные примечания, которые мы обсудим в разделе "Обнародование дополнения на Crates.io" главы 14.
- -Возможности запуска некоторого кода в зависимости от некоторого условия, и замкнутого выполнения некоторого кода, являются основными элементами в большинстве языков программирования. Наиболее распространёнными устройствоми, позволяющими управлять потоком выполнения кода Rust, являются выражения if
и циклы.
if
Выражение if
позволяет выполнять части кода в зависимости от условий. Вы задаёте условие, а затем указываете: "Если это условие выполняется, выполните этот разделкода. Если условие не выполняется, не выполняйте этот разделкода".
Для изучения выражения if
создайте новый дело под названием branches в папке projects. В файл src/main.rs поместите следующий код:
Имя файла: src/main.rs
--fn main() { - let number = 3; - - if number < 5 { - println!("condition was true"); - } else { - println!("condition was false"); - } -}
Условие начинается с ключевого слова if
, за которым следует условное выражение. В данном случае условное выражение проверяет, имеет ли переменная number
значение меньше 5. Сразу после условного выражения внутри фигурных скобок мы помещаем разделкода, который будет выполняться, если итог равен true
. Блоки кода, связанные с условными выражениями, иногда называют ветками, как и ветки в выражениях match
, которые мы обсуждали в разделе "Сравнение догадки с тайным числом" главы 2.
Это необязательно, но мы также можем использовать ключевое слово else
, которое мы используем в данном примере, чтобы предоставить программе иной разделвыполнения кода, выполняющийся если итог вычисления будет ложным. Если не указать выражение else
и условие будет ложным, программа просто пропустит разделif
и перейдёт к следующему отрывку кода.
Попробуйте запустить этот код. Появится следующий итог:
-$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/branches`
-condition was true
-
-Попробуйте изменить значение number
на значение, которое делает условие false
и посмотрите, что произойдёт:
fn main() {
- let number = 7;
-
- if number < 5 {
- println!("condition was true");
- } else {
- println!("condition was false");
- }
-}
-Запустите программу снова и посмотрите на вывод:
-$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/branches`
-condition was false
-
-Также стоит отметить, что условие в этом коде должно быть логического вида bool
. Если условие не является bool
, возникнет ошибка. Например, попробуйте запустить следующий код:
Имя файла: src/main.rs
-fn main() {
- let number = 3;
-
- if number {
- println!("number was three");
- }
-}
-На этот раз условие if
вычисляется в значение 3
, и Ржавчина бросает ошибку:
$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
-error[E0308]: mismatched types
- --> src/main.rs:4:8
- |
-4 | if number {
- | ^^^^^^ expected `bool`, found integer
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `branches` (bin "branches") due to 1 previous error
-
-Ошибка говорит, что Ржавчина ожидал вид bool
, но получил значение целочисленного вида. В отличии от других языков вроде Ruby и JavaScript, Ржавчина не будет пытаться самостоятельно преобразовывать нелогические виды в логические. Необходимо явно и всегда использовать if
с логическим видом в качестве условия. Если нужно, чтобы разделкода if
запускался только, когда число не равно 0
, то, например, мы можем изменить выражение if
на следующее:
Имя файла: src/main.rs
--fn main() { - let number = 3; - - if number != 0 { - println!("number was something other than zero"); - } -}
Будет напечатана следующая строка number was something other than zero
.
else if
Можно использовать несколько условий, сочетая if
и else
в выражении else if
. Например:
Имя файла: src/main.rs
--fn main() { - let number = 6; - - if number % 4 == 0 { - println!("number is divisible by 4"); - } else if number % 3 == 0 { - println!("number is divisible by 3"); - } else if number % 2 == 0 { - println!("number is divisible by 2"); - } else { - println!("number is not divisible by 4, 3, or 2"); - } -}
У этой программы есть четыре возможных пути выполнения. После её запуска вы должны увидеть следующий итог:
-$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/branches`
-number is divisible by 3
-
-Во время выполнения этой программы по очереди проверяется каждое выражение if
и выполняется первый блок, для которого условие true
. Заметьте, что хотя 6 делится на 2, мы не видим ни вывода number is divisible by 2
, ни текста number is not divisible by 4, 3, or 2
из раздела else
. Так происходит потому, что Ржавчина выполняет разделтолько для первого истинного условия, а обнаружив его, даже не проверяет остальные.
Использование множества выражений else if
приводит к загромождению кода, поэтому при наличии более чем одного выражения, возможно, стоит провести переработка кода кода. В главе 6 описана мощная устройство ветвления Ржавчина для таких случаев, называемая match
.
if
в указания let
Поскольку if
является выражением, его можно использовать в правой части указания let
для присвоения итога переменной, как в приложении 3-2.
Имя файла: src/main.rs
--fn main() { - let condition = true; - let number = if condition { 5 } else { 6 }; - - println!("The value of number is: {number}"); -}
-
Переменная number
будет привязана к значению, которое является итогом выражения if
. Запустим код и посмотрим, что происходит:
$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
- Running `target/debug/branches`
-The value of number is: 5
-
-Вспомните, что разделы кода вычисляются последним выражением в них, а числа сами по себе также являются выражениями. В данном случае, значение всего выражения if
зависит от того, какой разделвыполняется. При этом значения, которые могут быть итогами каждого из ветвей if
, должны быть одного вида. В Приложении 3-2, итогами обеих ветвей if
и else
являются целочисленный вид i32
. Если виды не совпадают, как в следующем примере, мы получим ошибку:
Имя файла: src/main.rs
-fn main() {
- let condition = true;
-
- let number = if condition { 5 } else { "six" };
-
- println!("The value of number is: {number}");
-}
-При попытке сборки этого кода, мы получим ошибку. Ветви if
и else
представляют несовместимые виды значений, и Ржавчина точно указывает, где искать неполадку в программе:
$ cargo run
- Compiling branches v0.1.0 (file:///projects/branches)
-error[E0308]: `if` and `else` have incompatible types
- --> src/main.rs:4:44
- |
-4 | let number = if condition { 5 } else { "six" };
- | - ^^^^^ expected integer, found `&str`
- | |
- | expected because of this
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `branches` (bin "branches") due to 1 previous error
-
-Выражение в разделе if
вычисляется как целочисленное, а выражение в разделе else
вычисляется как строка. Это не сработает, потому что переменные должны иметь один вид, а Ржавчина должен знать во время сборки, какого вида переменная number
. Зная вид number
, сборщик может убедиться, что вид действителен везде, где мы используем number
. Ржавчина не смог бы этого сделать, если бы вид number
определялся только во время выполнения. Сборщик усложнился бы и давал бы меньше заверений в отношении кода, если бы ему приходилось отслеживать несколько гипотетических видов для любой переменной.
Часто бывает полезно выполнить раздел кода более одного раза. Для этой задачи Ржавчина предоставляет несколько устройств цикла, которые позволяют выполнить разделкода до конца, а затем сразу же вернуться в начало. Для экспериментов с циклами давайте создадим новый дело под названием loops.
-В Ржавчина есть три вида циклов: loop
, while
и for
. Давайте попробуем каждый из них.
loop
Ключевое слово loop
говорит Ржавчина выполнять разделкода снова и снова до бесконечности или пока не будет явно приказано остановиться.
В качестве примера, измените код файла src/main.rs в папке дела loops на код ниже:
-Имя файла: src/main.rs
-fn main() {
- loop {
- println!("again!");
- }
-}
-Когда запустим эту программу, увидим, как again!
печатается снова и снова, пока не остановить программу вручную. Большинство окно вызоваов поддерживают сочетание клавиш ctrl-c для прерывания программы, которая застряла в непрерывном цикле. Попробуйте:
$ cargo run
- Compiling loops v0.1.0 (file:///projects/loops)
- Finished dev [unoptimized + debuginfo] target(s) in 0.29s
- Running `target/debug/loops`
-again!
-again!
-again!
-again!
-^Cagain!
-
-Символ ^C
обозначает место, где было нажато ctrl-c . В зависимости от того, где находился код в цикле в мгновение получения звонка отпрерывания, вы можете увидеть или не увидеть слово again!
, напечатанное после ^C
.
К счастью, Ржавчина также предоставляет способ выйти из цикла с помощью кода. Ключевое слово break
нужно поместить в цикл, чтобы указать программе, когда следует прекратить выполнение цикла. Напоминаем, мы делали так в игре "Угадайка" в разделе "Выход после правильной догадки" Главы 2, чтобы выйти из программы, когда пользователь выиграл игру, угадав правильное число.
Мы также использовали continue
в игре "Угадайка", которое указывает программе в цикле пропустить весь оставшийся код в данной повторения цикла и перейти к следующей повторения.
Одно из применений loop
- это повторение действия, которая может закончиться неудачей, например, проверка успешности выполнения потоком своего задания. Также может понадобиться передать из цикла итог этой действия в остальную часть кода. Для этого можно добавить возвращаемое значение после выражения break
, которое используется для остановки цикла. Это значение будет возвращено из цикла, и его можно будет использовать, как показано здесь:
-fn main() { - let mut counter = 0; - - let result = loop { - counter += 1; - - if counter == 10 { - break counter * 2; - } - }; - - println!("The result is {result}"); -}
Перед циклом мы объявляем переменную с именем counter
и объявим её значением 0
. Затем мы объявляем переменную с именем result
для хранения значения, возвращаемого из цикла. На каждой повторения цикла мы добавляем 1
к переменной counter
, а затем проверяем, равняется ли 10
переменная counter
. Когда это происходит, мы используем ключевое слово break
со значением counter * 2
. После цикла мы ставим точку с запятой для завершения указания, присваивающей значение result
. Наконец, мы выводим значение в result
, равное в данном случае 20.
Если у вас есть циклы внутри циклов, break
и continue
применяются к самому внутреннему циклу в этой цепочке. При желании вы можете создать метку цикла, которую вы затем сможете использовать с break
или continue
для указания, что эти ключевые слова применяются к помеченному циклу, а не к самому внутреннему циклу. Метки цикла должны начинаться с одинарной кавычки. Вот пример с двумя вложенными циклами:
-fn main() { - let mut count = 0; - 'counting_up: loop { - println!("count = {count}"); - let mut remaining = 10; - - loop { - println!("remaining = {remaining}"); - if remaining == 9 { - break; - } - if count == 2 { - break 'counting_up; - } - remaining -= 1; - } - - count += 1; - } - println!("End count = {count}"); -}
Внешний цикл имеет метку 'counting_up
, и он будет считать от 0 до 2. Внутренний цикл без метки ведёт обратный отсчёт от 10 до 9. Первый break
, который не содержит метку, выйдет только из внутреннего цикла. Указание break 'counting_up;
завершит внешний цикл. Этот код напечатает:
$ cargo run
- Compiling loops v0.1.0 (file:///projects/loops)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
- Running `target/debug/loops`
-count = 0
-remaining = 10
-remaining = 9
-count = 1
-remaining = 10
-remaining = 9
-count = 2
-remaining = 10
-End count = 2
-
-while
В программе часто требуется проверить состояние условия в цикле. Пока условие истинно, цикл выполняется. Когда условие перестаёт быть истинным, программа вызывает break
, останавливая цикл. Такое поведение можно выполнить с помощью сочетания loop
, if
, else
и break
. При желании попробуйте сделать это в программе. Это настолько распространённый образец, что в Ржавчина выполнена встроенная языковая устройство для него, называемая цикл while
. В приложении 3-3 мы используем while
, чтобы выполнить три цикла программы, производя каждый раз обратный отсчёт, а затем, после завершения цикла, печатаем сообщение и выходим.
Имя файла: src/main.rs
--fn main() { - let mut number = 3; - - while number != 0 { - println!("{number}!"); - - number -= 1; - } - - println!("LIFTOFF!!!"); -}
-
Эта устройство устраняет множество вложений, которые потребовались бы при использовании loop
, if
, else
и break
, и она более понятна. Пока условие вычисляется в true
, код выполняется; в противном случае происходит выход из цикла.
for
Для перебора элементов собрания, например, массива, можно использовать устройство while
. Например, цикл в приложении 3-4 печатает каждый элемент массива a
.
Имя файла: src/main.rs
--fn main() { - let a = [10, 20, 30, 40, 50]; - let mut index = 0; - - while index < 5 { - println!("the value is: {}", a[index]); - - index += 1; - } -}
-
Этот код выполняет перебор элементов массива. Он начинается с порядкового указателя 0
, а затем замкнуто выполняется, пока не достигнет последнего порядкового указателя в массиве (то есть, когда index < 5
уже не является истиной). Выполнение этого кода напечатает каждый элемент массива:
$ cargo run
- Compiling loops v0.1.0 (file:///projects/loops)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
- Running `target/debug/loops`
-the value is: 10
-the value is: 20
-the value is: 30
-the value is: 40
-the value is: 50
-
-Все пять значений массива появляются в окне вызова, как и ожидалось. Поскольку index
в какой-то мгновение достигнет значения 5
, цикл прекратит выполнение перед попыткой извлечь шестое значение из массива.
Однако такой подход чреват ошибками; мы можем вызвать панику в программе, если значение порядкового указателя или условие проверки неверны. Например, если изменить определение массива a
на четыре элемента, но забыть обновить условие на while index < 4
, код вызовет панику. Также это медленно, поскольку сборщик добавляет код времени выполнения для обеспечения проверки нахождения порядкового указателя в границах массива на каждой повторения цикла.
В качестве более краткой иного решения можно использовать цикл for
и выполнять некоторый код для каждого элемента собрания. Цикл for
может выглядеть как код в приложении 3-5.
Имя файла: src/main.rs
--fn main() { - let a = [10, 20, 30, 40, 50]; - - for element in a { - println!("the value is: {element}"); - } -}
-
При выполнении этого кода мы увидим тот же итог, что и в приложении 3-4. Что важнее, теперь мы повысили безопасность кода и устранили вероятность ошибок, которые могут возникнуть в итоге выхода за пределы массива или недостаточно далёкого перехода и пропуска некоторых элементов.
-При использовании цикла for
не нужно помнить о внесении изменений в другой код, в случае изменения количества значений в массиве, как это было бы с способом, использованным в приложении 3-4.
Безопасность и краткость циклов for
делают их наиболее часто используемой устройством цикла в Rust. Даже в случаейх необходимости выполнения некоторого кода определённое количество раз, как в примере обратного отсчёта, в котором использовался цикл while
из Приложения 3-3, большинство Rustaceans использовали бы цикл for
. Для этого можно использовать Range
, предоставляемый встроенной библиотекой, который порождает последовательность всех чисел, начиная с первого числа и заканчивая вторым числом, но не включая его (т.е. (1..4)
эквивалентно [1, 2, 3]
или в общем случае (start..end)
эквивалентно [start, start+1, start+2, ... , end-2, end-1]
- прим.переводчика).
Вот как будет выглядеть обратный отсчёт с использованием цикла for
и другого способа, о котором мы ещё не говорили, rev
, для разворота ряда:
Имя файла: src/main.rs
--fn main() { - for number in (1..4).rev() { - println!("{number}!"); - } - println!("LIFTOFF!!!"); -}
Данный код выглядит лучше, не так ли?
-Вы справились! Это была объёмная глава: вы узнали о переменных, одиночных и составных видах данных, функциях, примечаниях, выражениях if
и циклах! Для опытов работы с подходами, обсуждаемыми в этой главе, попробуйте создать программы для выполнения следующих действий:
Когда вы будете готовы двигаться дальше, мы поговорим о подходы в Rust, которая не существует обычно в других языках программирования: владение.
- -Владение - это самая не имеет себе подобных особенность Rust, которая имеет глубокие последствия для всего языка. Это позволяет Ржавчина обеспечивать безопасность памяти без использования сборщика мусора, поэтому важно понимать, как работает владение. В этой главе мы поговорим о владении, а также о нескольких связанных с ним возможностях: заимствовании, срезах и о том, как Ржавчина раскладывает данные в памяти.
- -Владение — это набор правил, определяющих, как программа на языке Ржавчина управляет памятью. Все программы так или иначе должны управлять тем, как они используют память компьютера во время работы. Некоторые языки имеют сборщик мусора, постоянно отслеживающий неиспользуемую память во время работы программы; в других языках программист должен явно выделять и освобождать память. В Ржавчина используется третий подход: память управляется через систему владения с набором правил, которые проверяются сборщиком. При нарушении любого из правил программа не будет собрана. Ни одна из особенностей системы владения не замедлит работу вашей программы.
-Поскольку владение является новой подходом для многих программистов, требуется некоторое время, чтобы привыкнуть к ней. Хорошая новость заключается в том, что чем больше у вас будет опыта с Ржавчина и с правилами системы владения, тем легче вам будет естественным образом разрабатывать безопасный и эффективный код. Держитесь! Не сдавайтесь!
-Понимание подходы владения даст вам основу для понимания всех остальных особенностей, делающих Ржавчина единственным. В этой главе вы изучите владение на примерах, которые сосредоточены на наиболее часто используемой устройстве данных: строках.
---Обойма и куча
-Многие языки программирования не требуют, чтобы вы слишком часто думали о обойме и куче. Но в языках системного программирования, одним из которых является Rust, то, какое значение находится в обойме или в куче, влияет на поведение языка и на принятие вами определённых решений. Владение будет описано через призму обоймы и кучи позже в этой главе, а пока — краткое пояснение.
-И обойма, и куча — это части памяти, доступные вашему коду для использования во время выполнения. Однако они внутренне выстроенны
-
по-разному. Обойма хранит значения в порядке их получения, а удаляет — в обратном. Это называется «последним пришёл — первым ушёл». Подумайте о стопке тарелок: когда вы добавляете тарелки, вы кладёте их сверху стопки — когда вам нужна тарелка, вы берёте одну так же сверху. Добавление или удаление тарелок посередине или снизу не сработает! Добавление данных называется помещением в обойма, а удаление — извлечением из обоймы. Все данные, хранящиеся в обойме, должны иметь известный определенный размер. Данные, размер которых во время сборки неизвестен или может измениться, должны храниться в куче.
---Куча устроена менее согласованно: когда вы кладёте данные в кучу, вы запрашиваете определённый объём пространства. Операционная система находит в куче свободный участок памяти достаточного размера, помечает его как используемый и возвращает указатель, являющийся адресом этого участка памяти. Этот этап называется выделением памяти в куче и иногда сокращается до выделения памяти (помещение значений в обойма не считается выделением). Поскольку указатель на участок памяти в куче имеет определённый определенный размер, его можно расположить в обойме, однако когда вам понадобятся актуальные данные, вам придётся проследовать по указателю. Представьте, что вы сидите в ресторане. Когда вы входите, вы называете количество человек в вашей объединении, и человек находит свободный стол, которого хватит на всех, и ведёт вас туда. Если кто-то из вашей объединение опоздает, он может спросить, куда вас посадили, чтобы найти вас.
-Помещение в обойма происходит более быстро, чем выделение памяти в куче, потому что операционная система не должна искать место для размещения сведений — это место всегда на верхушке обоймы. Для сравнения, выделение памяти в куче требует больше работы, потому что операционная система сначала должна найти участок памяти достаточного размера, а затем произвести некоторые действия для подготовки к следующему выделению памяти.
-Доступ к данным в куче медленнее, чем доступ к данным в обойме, потому что вам нужно следовать по адресу указателя, чтобы добраться туда. Современные процессоры работают быстрее, если они меньше прыгают по памяти. Продолжая подобие, рассмотрим официанта в ресторане, принимающего заказы со многих столов. Наиболее эффективно будет получить все заказы за одним столом, прежде чем переходить к следующему столу. Получение заказа со стола А, затем со стола В, затем снова одного с А, а затем снова одного с В было бы гораздо более медленным делом. Точно так же процессор может выполнять свою работу лучше, если он работает с данными, которые находятся близко к другим данным (как в обойме), а не далеко (как это может быть в куче).
-Когда ваш код вызывает функцию, значения, переданные в неё (возможно включающие указатели на данные в куче), и местные переменные помещаются в обойма. Когда функция завершается, эти значения извлекаются из обоймы.
-Отслеживание того, какие части кода используют какие данные, уменьшение количества повторяющихся данных и очистка неиспользуемых данных в куче, чтобы не исчерпать пространство, — все эти сбоев решает владение. Как только вы поймёте, что такое владение, вам не нужно будет слишком часто думать о обойме и куче. Однако знание того, что основная цель владения — управление данными кучи, может помочь объяснить, почему оно работает именно так.
-
Во-первых, давайте взглянем на правила владения. Помните об этих правилах, пока мы работаем с примерами, которые их отображают:
-Теперь, когда мы прошли основной правила написания Rust, мы не будем включать весь код fn main() {
в примеры. Поэтому, если вы будете следовать этому курсу, убедитесь, что следующие примеры помещены в функцию main
вручную. В итоге наши примеры будут более краткими, что позволит нам сосредоточиться на существующих подробностях, а не на образцовом коде.
В качестве первого примера владения мы рассмотрим область видимости некоторых переменных. Область видимости — это рядвнутри программы, для которого допустим элемент. Возьмём следующую переменную:
--#![allow(unused)] -fn main() { -let s = "hello"; -}
Переменная s
относится к строковому записи, где значение строки жёстко прописано в тексте нашей программы. Переменная действительна с особенности её объявления до конца текущей области видимости. В приложении 4-1 показана программа с примечаниями, указывающими, где допустима переменная s
.
-fn main() { - { // s is not valid here, it’s not yet declared - let s = "hello"; // s is valid from this point forward - - // do stuff with s - } // this scope is now over, and s is no longer valid -}
-
Другими словами, здесь есть два важных особенности:
-s
появляется в области видимости, она считается действительной,На этом этапе объяснения взаимосвязь между областями видимости и допустимостью переменных подобна той, что существует в других языках программирования. Теперь мы будем опираться на это понимание, введя вид String
.
String
Для отображения правил владения нам требуется более сложный вид данных чем те, что мы обсуждали в части "Виды данных" Главы 3. Виды, рассмотренные ранее, имеют определённый размер, а значит могут быть размещены на обойме и извлечены из него, когда их область видимости закончится, и могут быть быстро и обыкновенно воспроизведены для создания новой, независимой повторы, если другой части кода нужно использовать то же самое значение в другой области видимости. Но мы хотим посмотреть на данные, хранящиеся в куче, и выяснить, как Ржавчина узнаёт, когда нужно очистить эти данные, поэтому вид String
— отличный пример.
Мы сосредоточимся на тех частях String
, которые связаны с владением. Эти особенности также применимы к другим сложным видам данных, независимо от того, предоставлены они встроенной библиотекой или созданы вами. Более подробно мы обсудим String
в главе 8.
Мы уже видели строковые записи, где строковое значение жёстко прописано в нашей программе. Строковые записи удобны, но они подходят не для каждой случаи, где мы можем хотеть использовать текст. Одна из причин заключается в том, что они неизменны. Кроме того, не каждое строковое значение может быть известно во время написания кода: что, если мы захотим принять и сохранить пользовательский ввод? Для таких случаев в Ржавчина есть ещё один строковый вид — String
. Этот вид управляет данными, выделенными в куче, и поэтому может хранить объём текста, который во время сборки неизвестен. Также вы можете создать String
из строкового записи, используя функцию from
, например:
-#![allow(unused)] -fn main() { -let s = String::from("hello"); -}
Оператор "Двойное двоеточие" ::
позволяет использовать пространство имён данной именно функции from
с видом String
, а не какое-то иное имя, такое как string_from
. Мы обсудим этот правила написания более подробно в разделе «Синтаксис способа». раздел Главы 5, и в ходе обсуждения пространств имён с звенами в «Пути для обращения к элементу в дереве звеньев» в главе 7.
Строка такого вида может быть изменяема:
--fn main() { - let mut s = String::from("hello"); - - s.push_str(", world!"); // push_str() appends a literal to a String - - println!("{s}"); // This will print `hello, world!` -}
В чем же тут разница? Почему строку String
можно изменить, а записи — нельзя? Разница заключается в том, как эти два вида работают с памятью.
В случае строкового записи мы знаем его содержимое во время сборки, и оно жёстко прописано в итоговом исполняемом файле. Причина того, что строковые записи более быстрые и эффективные, в их неизменяемости. К сожалению, нельзя поместить неопределённый кусок памяти в выполняемый файл для текста, размер которого неизвестен при сборки и может меняться во время выполнения программы.
-Чтобы поддерживать изменяемый, увеличивающийся текст вида String
, необходимо выделять память в куче для всего содержимого, размер которого неизвестен во время сборки. Это означает, что:
String
.Первая часть выполняется нами: когда мы вызываем String::from
, его выполнение запрашивает необходимую память. Это работает довольно похоже во всех языках программирования.
Однако вторая часть отличается. В языках со сборщиком мусора (GC), память, которая больше не используется, отслеживается и очищается с его помощью — нам не нужно об этом думать. В большинстве языков без сборщика мусора мы обязаны сами определять, когда память больше не используется, и вызывать код для явного её освобождения, точно так же, как мы делали это для её запроса. Правильное выполнение этого этапа исторически было сложной неполадкой программирования. Если мы забудем освободить память, она будет потеряна. Если мы сделаем это слишком рано, у нас будет недопустимая переменная. Сделать это дважды — тоже будет ошибкой. Нам нужно соединить ровно один allocate
ровно с одним free
.
Rust выбирает другой путь: память самостоятельно возвращается, как только владеющая памятью переменная выходит из области видимости. Вот исполнение примера с областью видимости из приложения 4-1, в котором используется вид String
вместо строкового записи:
-fn main() { - { - let s = String::from("hello"); // s is valid from this point forward - - // do stuff with s - } // this scope is now over, and s is no - // longer valid -}
Существует естественный мгновение, когда мы можем вернуть память, необходимую нашему String
, обратно распределителю — когда s
выходит за пределы области видимости. Когда переменная выходит за пределы области видимости, Ржавчина вызывает для нас особую функцию. Эта функция называется drop
, и именно здесь автор String
может поместить код для возврата памяти. Ржавчина самостоятельно вызывает drop
после закрывающей фигурной скобки.
--Примечание: в C++ этот образец освобождения ресурсов в конце времени жизни элемента иногда называется «Получение ресурса есть объявление» (англ. Resource Acquisition Is Initialization (RAII)). Функция
-drop
в Ржавчина покажется вам знакомой, если вы использовали образцы RAII.
Этот образец оказывает глубокое влияние на способ написания кода в Rust. Сейчас это может казаться простым, но в более сложных случаейх поведение кода может быть неожиданным, например когда хочется иметь несколько переменных, использующих данные, выделенные в куче. Изучим несколько таких случаев.
- - -Несколько переменных могут по-разному взаимодействовать с одними и теми же данными в Rust. Давайте рассмотрим пример использования целого числа в приложении 4-2.
--fn main() { - let x = 5; - let y = x; -}
-
Мы можем догадаться, что делает этот код: «привязать значение 5
к x
; затем сделать повтор значения в x
и привязать его к y
». Теперь у нас есть две переменные: x
и y
, и обе равны 5
. Это то, что происходит на самом деле, потому что целые числа — это простые значения с известным конечным размером, и эти два значения 5
помещаются в обойма.
Теперь рассмотрим исполнение с видом String
:
-fn main() { - let s1 = String::from("hello"); - let s2 = s1; -}
Это выглядит очень похоже, поэтому мы можем предположить, что происходит то же самое: вторая строка сделает повтор значения в s1
и привяжет его к s2
. Но это не совсем так.
Взгляните на рисунок 4-1, чтобы увидеть, что происходит со String
под капотом. String
состоит из трёх частей, показанных слева: указатель на память, в которой хранится содержимое строки, длина и ёмкость. Эта объединение данных хранится в обойме. Справа — память в куче, которая содержит содержимое.
-
Длина — это объём памяти в байтах, который в настоящее время использует содержимое String
. Ёмкость — это общий объём памяти в байтах, который String
получил от распределителя. Разница между длиной и ёмкостью имеет значение, но не в этом среде, поэтому на данный мгновение можно пренебрегать ёмкость.
Когда мы присваиваем s1
значению s2
, данные String
повторяются, то есть мы повторяем указатель, длину и ёмкость, которые находятся в обойме. Мы не повторяем данные в куче, на которые указывает указатель. Другими словами, представление данных в памяти выглядит так, как показано на рис. 4-2.
-
Представление не похоже на рисунок 4-3, как выглядела бы память, если бы вместо этого Ржавчина также воспроизвел данные кучи. Если бы Ржавчина сделал это, действие s2 = s1
могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими.
-
Ранее мы сказали, что когда переменная выходит за пределы области видимости, Ржавчина самостоятельно вызывает функцию drop
и очищает память в куче для данной переменной. Но на рис. 4.2 оба указателя данных указывают на одно и то же место. Это неполадка: когда переменные s2
и s1
выходят из области видимости, они обе будут пытаться освободить одну и ту же память в куче. Это известно как ошибка двойного освобождения (double free) и является одной из ошибок безопасности памяти, упоминаемых ранее. Освобождение памяти дважды может привести к повреждению памяти, что возможно может привести к уязвимостям безопасности.
Чтобы обеспечить безопасность памяти, после строки let s2 = s1;
, Ржавчина считает s1
более недействительным. Следовательно, Ржавчина не нужно ничего освобождать, когда s1
выходит за пределы области видимости. Посмотрите, что происходит, когда вы пытаетесь использовать s1
после создания s2
; это не сработает:
fn main() {
- let s1 = String::from("hello");
- let s2 = s1;
-
- println!("{s1}, world!");
-}
-Вы получите похожую ошибку, потому что Ржавчина не позволяет вам использовать недействительную ссылку:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0382]: borrow of moved value: `s1`
- --> src/main.rs:5:15
- |
-2 | let s1 = String::from("hello");
- | -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
-3 | let s2 = s1;
- | -- value moved here
-4 |
-5 | println!("{s1}, world!");
- | ^^^^ value borrowed here after move
- |
- = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
-help: consider cloning the value if the performance cost is acceptable
- |
-3 | let s2 = s1.clone();
- | ++++++++
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Если вы слышали понятия поверхностное повторение и глубокое повторение при работе с другими языками, подход повторения указателя, длины и ёмкости без повторения данных, вероятно, звучит как создание поверхностной повторы. Но поскольку Ржавчина также аннулирует первую переменную, вместо того, чтобы называть это поверхностным повторением, это называется перемещением. В этом примере мы бы сказали, что s1
был перемещён в s2
. Итак, что на самом деле происходит, показано на рисунке 4-4.
-
Это решает нашу неполадку! Действительной остаётся только переменная s2
. Когда она выходит из области видимости, то она одна будет освобождать память в куче.
Такой выбор внешнего вида языка даёт дополнительное преимущество: Ржавчина никогда не будет самостоятельно создавать «глубокие» повторы ваших данных. Следовательно любое такое самостоятельное повторение можно считать недорогим с точки зрения производительности во время выполнения.
- - -Если мы хотим глубоко воспроизвести данные кучи String
, а не только данные обоймы, мы можем использовать общий способ, называемый clone
. Мы обсудим правила написания способов в главе 5, но поскольку способы являются общей чертой многих языков программирования, вы, вероятно, уже встречались с ними.
Вот пример работы способа clone
:
-fn main() { - let s1 = String::from("hello"); - let s2 = s1.clone(); - - println!("s1 = {s1}, s2 = {s2}"); -}
Это отлично работает и очевидно приводит к поведению, представленному на рисунке 4-3, где данные кучи были воспроизведены.
-Когда вы видите вызов clone
, вы знаете о выполнении некоторого кода, который может быть дорогим. В то же время использование clone
является визуальным индикатором того, что тут происходит что-то необычное.
Это ещё одна особенность о которой мы ранее не говорили. Этот код, часть которого была показа ранее в приложении 4-2, использует целые числа. Он работает без ошибок:
--fn main() { - let x = 5; - let y = x; - - println!("x = {x}, y = {y}"); -}
Но этот код, кажется, противоречит тому, что мы только что узнали: у нас нет вызова clone
, но x
всё ещё действителен и не был перемещён в y
.
Причина в том, что такие виды, как целые числа, размер которых известен во время сборки, полностью хранятся в обойме, поэтому повторы действительных значений создаются быстро. Это означает, что нет причин, по которым мы хотели бы предотвратить доступность x
после того, как создадим переменную y
. Другими словами, здесь нет разницы между глубоким и поверхностным повторением, поэтому вызов clone
ничем не отличается от обычного поверхностного повторения, и мы можем его опустить.
В Ржавчина есть особая изложение, называемая особенностью Copy
, которую мы можем размещать на видах, хранящихся в обойме, как и целые числа (подробнее о видах мы поговорим в главе 10). Если вид выполняет особенность Copy
, переменные, которые его используют, не перемещаются, а обыкновенно повторяются, что делает их действительными после присвоения другой переменной.
Rust не позволит нам определять вид с помощью Copy
, если вид или любая из его частей выполняет Drop
. Если для вида нужно, чтобы произошло что-то особенное, когда значение выходит за пределы области видимости, и мы добавляем изложение Copy
к этому виду, мы получим ошибку времени сборки. Чтобы узнать, как добавить изложение Copy
к вашему виду для выполнения особенности, смотрите раздел «Производные особенности» в приложении С.
Но какие же виды выполняют особенность Copy
? Можно проверить документацию любого вида для уверенности, но как правило любая объединение простых одиночных значений может быть выполнить Copy
, и никакие виды, которые требуют выделения памяти в куче или являются некоторой способом ресурсов, не выполняют особенности Copy
. Вот некоторые виды, которые выполняют Copy
:
u32
,bool
, возможные значения которого true
и false
,f64
.char
,Copy
. Например, (i32, i32)
будет с Copy
, но упорядоченный ряд (i32, String)
уже нет.Механика передачи значения функции подобна тому, что происходит при присвоении значения переменной. Передача переменной в функцию приведёт к перемещению или воспроизведению, как и присваивание. В приложении 4-3 есть пример с некоторыми изложениями, показывающими, где переменные входят в область видимости и выходят из неё.
-Файл: src/main.rs
--fn main() { - let s = String::from("hello"); // s comes into scope - - takes_ownership(s); // s's value moves into the function... - // ... and so is no longer valid here - - let x = 5; // x comes into scope - - makes_copy(x); // x would move into the function, - // but i32 is Copy, so it's okay to still - // use x afterward - -} // Here, x goes out of scope, then s. But because s's value was moved, nothing - // special happens. - -fn takes_ownership(some_string: String) { // some_string comes into scope - println!("{some_string}"); -} // Here, some_string goes out of scope and `drop` is called. The backing - // memory is freed. - -fn makes_copy(some_integer: i32) { // some_integer comes into scope - println!("{some_integer}"); -} // Here, some_integer goes out of scope. Nothing special happens.
-
Если попытаться использовать s
после вызова takes_ownership
, Ржавчина выдаст ошибку времени сборки. Такие постоянные проверки защищают от ошибок. Попробуйте добавить код в main
, который использует переменную s
и x
, чтобы увидеть где их можно использовать и где правила владения предотвращают их использование.
Возвращаемые значения также могут передавать право владения. В приложении 4-4 показан пример функции, возвращающей некоторое значение, с такими же изложениями, как в приложении 4-3.
-Файл: src/main.rs
--fn main() { - let s1 = gives_ownership(); // gives_ownership moves its return - // value into s1 - - let s2 = String::from("hello"); // s2 comes into scope - - let s3 = takes_and_gives_back(s2); // s2 is moved into - // takes_and_gives_back, which also - // moves its return value into s3 -} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing - // happens. s1 goes out of scope and is dropped. - -fn gives_ownership() -> String { // gives_ownership will move its - // return value into the function - // that calls it - - let some_string = String::from("yours"); // some_string comes into scope - - some_string // some_string is returned and - // moves out to the calling - // function -} - -// This function takes a String and returns one -fn takes_and_gives_back(a_string: String) -> String { // a_string comes into - // scope - - a_string // a_string is returned and moves out to the calling function -}
-
Владение переменной каждый раз следует одному и тому же образцу: присваивание значения другой переменной перемещает его. Когда переменная, содержащая данные в куче, выходит из области видимости, содержимое в куче будет очищено функцией drop
, если только данные не были перемещены во владение другой переменной.
Хотя это работает, получение права владения, а затем возвращение владения каждой функцией немного утомительно. Что, если мы хотим, чтобы функция использовала значение, но не становилась владельцем? Очень раздражает, что всё, что мы передаём, также должно быть передано обратно, если мы хотим использовать это снова, в дополнение к любым данным, полученным из тела функции, которые мы также можем захотеть вернуть.
-Rust позволяет нам возвращать несколько значений с помощью упорядоченного ряда, как показано в приложении 4-5.
-Файл: src/main.rs
--fn main() { - let s1 = String::from("hello"); - - let (s2, len) = calculate_length(s1); - - println!("The length of '{s2}' is {len}."); -} - -fn calculate_length(s: String) -> (String, usize) { - let length = s.len(); // len() returns the length of a String - - (s, length) -}
-
Но это слишком высокопарно и многословно для подходы, которая должна быть общей. К счастью для нас, в Ржавчина есть возможность использовать значение без передачи права владения, называемая ссылками.
- -Неполадкас кодом упорядоченного ряда в приложении 4-5 заключается в том, что мы должны вернуть String
из вызванной функции, чтобы использовать String
после вызова calculate_length
, потому что String
была перемещена в calculate_length
. Вместо этого мы можем предоставить ссылку на значение String
. Ссылка похожа на указатель в том смысле, что это адрес, по которому мы можем проследовать, чтобы получить доступ к данным, хранящимся по этому адресу; эти данные принадлежат какой-то другой переменной. В отличие от указателя, ссылка обязательно указывает на допустимое значение определённого вида в течение всего срока существования этой ссылки.
Вот как вы могли бы определить и использовать функцию calculate_length
, имеющую ссылку на предмет в качестве свойства, вместо того, чтобы брать на себя ответственность за значение:
Файл: src/main.rs
--fn main() { - let s1 = String::from("hello"); - - let len = calculate_length(&s1); - - println!("The length of '{s1}' is {len}."); -} - -fn calculate_length(s: &String) -> usize { - s.len() -}
Во-первых, обратите внимание, что весь код упорядоченного ряда в объявлении переменной и возвращаемое значение функции исчезли. Во-вторых, обратите внимание, что мы передаём &s1
в calculate_length
и в его определении используем &String
, а не String
. Эти знаки представляют собой ссылки, и они позволяют вам ссылаться на некоторое значение, не принимая владение над ним. Рисунок 4-5 изображает эту подход.
-
--Примечание: противоположностью ссылки с использованием
-&
является разыменование, выполняемое с помощью оператора разыменования*
. Мы увидим некоторые исходы использования оператора разыменования в главе 8 и обсудим подробности разыменования в главе 15.
Давайте подробнее рассмотрим рычаг вызова функции:
--fn main() { - let s1 = String::from("hello"); - - let len = calculate_length(&s1); - - println!("The length of '{s1}' is {len}."); -} - -fn calculate_length(s: &String) -> usize { - s.len() -}
&s1
позволяет нам создать ссылку, которая ссылается на значение s1
, но не владеет им. Поскольку она не владеет им, значение, на которое она указывает, не будет удалено, когда ссылка перестанет использоваться.
Ярлык функции использует &
для индикации того, что вид свойства s
является ссылкой. Добавим объясняющие примечания:
-fn main() { - let s1 = String::from("hello"); - - let len = calculate_length(&s1); - - println!("The length of '{s1}' is {len}."); -} - -fn calculate_length(s: &String) -> usize { // s is a reference to a String - s.len() -} // Here, s goes out of scope. But because it does not have ownership of what - // it refers to, it is not dropped.
Область действия s
такая же, как и область действия любого свойства функции, но значение, на которое указывает ссылка, не удаляется, когда s
перестаёт использоваться, потому что s
не является его владельцем. Когда функции имеют ссылки в качестве свойств вместо действительных значений, нам не нужно возвращать значения, чтобы вернуть право владения, потому что мы никогда не владели ими.
Мы называем этап создания ссылки заимствованием. Как и в существующей жизни, если человек чем-то владеет, вы можете это у него позаимствовать. Когда вы закончите, вы должны вернуть это законному владельцу.
-А что произойдёт, если попытаться изменить то, что было позаимствовано? Попробуйте код приложения 4-6 Спойлер: этот код не сработает!
-Файл: src/main.rs
-fn main() {
- let s = String::from("hello");
-
- change(&s);
-}
-
-fn change(some_string: &String) {
- some_string.push_str(", world");
-}
--
Вот ошибка:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
- --> src/main.rs:8:5
- |
-8 | some_string.push_str(", world");
- | ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
- |
-help: consider changing this to be a mutable reference
- |
-7 | fn change(some_string: &mut String) {
- | +++
-
-For more information about this error, try `rustc --explain E0596`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Как переменные неизменяемы по умолчанию, так и ссылки. Нам не разрешено изменять то, на что у нас есть ссылка.
-Мы можем исправить код из приложения 4-6, чтобы позволить себе изменять заимствованное значение, с помощью нескольких небольших настроек, которые используют изменяемую ссылку:
-Файл: src/main.rs
--fn main() { - let mut s = String::from("hello"); - - change(&mut s); -} - -fn change(some_string: &mut String) { - some_string.push_str(", world"); -}
Сначала мы меняем s
на mut
. Затем мы создаём изменяемую ссылку с помощью &mut s
, у которой вызываем change
и обновляем ярлык функции, чтобы принять изменяемую ссылку с помощью some_string: &mut String
. Это даёт понять, что change
изменит значение, которое заимствует.
Изменяемые ссылки имеют одно большое ограничение: если у вас есть изменяемая ссылка на значение, у вас не может быть других ссылок на это же значение. Код, который пытается создать две изменяемые ссылки на s
, завершится ошибкой:
Файл: src/main.rs
-fn main() {
- let mut s = String::from("hello");
-
- let r1 = &mut s;
- let r2 = &mut s;
-
- println!("{}, {}", r1, r2);
-}
-Описание ошибки:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0499]: cannot borrow `s` as mutable more than once at a time
- --> src/main.rs:5:14
- |
-4 | let r1 = &mut s;
- | ------ first mutable borrow occurs here
-5 | let r2 = &mut s;
- | ^^^^^^ second mutable borrow occurs here
-6 |
-7 | println!("{}, {}", r1, r2);
- | -- first borrow later used here
-
-For more information about this error, try `rustc --explain E0499`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Эта ошибка говорит о том, что код недействителен, потому что мы не можем заимствовать s
как изменяемые более одного раза в один мгновение. Первое изменяемое заимствование находится в r1
и должно длиться до тех пор, пока оно не будет использовано в println!
, но между созданием этой изменяемой ссылки и её использованием мы попытались создать другую изменяемую ссылку в r2
, которая заимствует те же данные, что и r1
.
Ограничение, предотвращающее одновременное использование нескольких изменяемых ссылок на одни и те же данные, допускает изменение, но очень управляющим образом. Это то, с чем борются новые Rustaceans, потому что большинство языков позволяют изменять значение в любой мгновение. Преимущество этого ограничения заключается в том, что Ржавчина может предотвратить гонку данных во время сборки. Гонка данных похожа на состояние гонки и происходит, когда возникают следующие три сценария:
-Гонки данных вызывают неопределённое поведение, и их может быть сложно диагностировать и исправить, когда вы пытаетесь отследить их во время выполнения. Ржавчина предотвращает такую неполадку, отказываясь собирать код с гонками данных!
-Как всегда, мы можем использовать фигурные скобки для создания новой области видимости, позволяющей использовать несколько изменяемых ссылок, но не одновременно:
--fn main() { - let mut s = String::from("hello"); - - { - let r1 = &mut s; - } // r1 goes out of scope here, so we can make a new reference with no problems. - - let r2 = &mut s; -}
Rust применяет подобное правило для соединения изменяемых и неизменяемых ссылок. Этот код приводит к ошибке:
-fn main() {
- let mut s = String::from("hello");
-
- let r1 = &s; // no problem
- let r2 = &s; // no problem
- let r3 = &mut s; // BIG PROBLEM
-
- println!("{}, {}, and {}", r1, r2, r3);
-}
-Ошибка:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
- --> src/main.rs:6:14
- |
-4 | let r1 = &s; // no problem
- | -- immutable borrow occurs here
-5 | let r2 = &s; // no problem
-6 | let r3 = &mut s; // BIG PROBLEM
- | ^^^^^^ mutable borrow occurs here
-7 |
-8 | println!("{}, {}, and {}", r1, r2, r3);
- | -- immutable borrow later used here
-
-For more information about this error, try `rustc --explain E0502`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Вау! У нас также не может быть изменяемой ссылки, пока у нас есть неизменяемая ссылка на то же значение.
-Пользователи неизменяемой ссылки не ожидают, что значение внезапно изменится из-под них! Однако разрешены множественные неизменяемые ссылки, потому что никто, кто просто читает данные, не может повлиять на чтение данных кем-либо ещё.
-Обратите внимание, что область действия ссылки начинается с того места, где она была введена, и продолжается до последнего использования этой ссылки. Например, этот код будет собираться, потому что последнее использование неизменяемых ссылок println!
, происходит до того, как вводится изменяемая ссылка:
-fn main() { - let mut s = String::from("hello"); - - let r1 = &s; // no problem - let r2 = &s; // no problem - println!("{r1} and {r2}"); - // variables r1 and r2 will not be used after this point - - let r3 = &mut s; // no problem - println!("{r3}"); -}
Области неизменяемых ссылок r1
и r2
заканчиваются после println!
где они использовались в последний раз, то есть до создания изменяемой ссылки r3
. Эти области не перекрываются, поэтому этот код разрешён: сборщик может сказать, что ссылка больше не используется в точке перед концом области.
Несмотря на то, что ошибки заимствования могут иногда вызывать разочарование, помните, что сборщик Ржавчина заранее указывает на вероятную ошибку (во время сборки, а не во время выполнения) и точно показывает, в чем неполадка. Тогда вам не придётся выяснять, почему ваши данные оказались не такими, как вы ожидали.
-В языках с указателями весьма легко ошибочно создать недействительную (висячую) (dangling) ссылку. Ссылку указывающую на участок памяти, который мог быть передан кому-то другому, путём освобождения некоторой памяти при сохранении указателя на эту память. Ржавчина сборщик заверяет, что ссылки никогда не станут недействительными: если у вас есть ссылка на какие-то данные, сборщик обеспечит что эти данные не выйдут из области видимости прежде, чем из области видимости исчезнет ссылка.
-Давайте попробуем создать висячую ссылку, чтобы увидеть, как Ржавчина предотвращает их появление с помощью ошибки во время сборки:
-Файл: src/main.rs
-fn main() {
- let reference_to_nothing = dangle();
-}
-
-fn dangle() -> &String {
- let s = String::from("hello");
-
- &s
-}
-Здесь ошибка:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0106]: missing lifetime specifier
- --> src/main.rs:5:16
- |
-5 | fn dangle() -> &String {
- | ^ expected named lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
-help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
- |
-5 | fn dangle() -> &'static String {
- | +++++++
-help: instead, you are more likely to want to return an owned value
- |
-5 - fn dangle() -> &String {
-5 + fn dangle() -> String {
- |
-
-error[E0515]: cannot return reference to local variable `s`
- --> src/main.rs:8:5
- |
-8 | &s
- | ^^ returns a reference to data owned by the current function
-
-Some errors have detailed explanations: E0106, E0515.
-For more information about an error, try `rustc --explain E0106`.
-error: could not compile `ownership` (bin "ownership") due to 2 previous errors
-
-Это сообщение об ошибке относится к особенности языка, которую мы ещё не рассмотрели: времени жизни. Мы подробно обсудим времена жизни в главе 10. Но если вы не обращаете внимания на части, касающиеся времени жизни, сообщение будет содержать ключ к тому, почему этот код является неполадкой:
-this function's return type contains a borrowed value, but there is no value
-for it to be borrowed from
-
-Давайте подробнее рассмотрим, что именно происходит на каждом этапе нашего кода dangle
:
Файл: src/main.rs
-fn main() {
- let reference_to_nothing = dangle();
-}
-
-fn dangle() -> &String { // dangle returns a reference to a String
-
- let s = String::from("hello"); // s is a new String
-
- &s // we return a reference to the String, s
-} // Here, s goes out of scope, and is dropped. Its memory goes away.
- // Danger!
-Поскольку s
создаётся внутри dangle
, когда код dangle
будет завершён, s
будет освобождена. Но мы попытались вернуть ссылку на неё. Это означает, что эта ссылка будет указывать на недопустимую String
. Это нехорошо! Ржавчина не позволит нам сделать это.
Решением будет вернуть непосредственно String
:
-fn main() { - let string = no_dangle(); -} - -fn no_dangle() -> String { - let s = String::from("hello"); - - s -}
Это работает без неполадок. Владение перемещено, и ничего не освобождено.
-Давайте повторим все, что мы обсудили про ссылки:
-В следующей главе мы рассмотрим другой вид ссылок — срезы.
- -Срезы позволяют ссылаться на непрерывную последовательность элементов в собрания, а не на всю собрание. Срез является своего рода ссылкой, поэтому он не имеет права владения.
-Вот небольшая неполадка программирования: напишите функцию, которая принимает строку слов, разделённых пробелами, и возвращает первое слово, которое она находит в этой строке. Если функция не находит пробела в строке, вся строка должна состоять из одного слова, поэтому должна быть возвращена вся строка.
-Давайте рассмотрим, как бы мы написали ярлык этой функции без использования срезов, чтобы понять неполадку, которую решат срезы:
-fn first_word(s: &String) -> ?
-Функция first_word
имеет &String
в качестве свойства. Мы не хотим владения, так что всё в порядке. Но что мы должны вернуть? На самом деле у нас нет способа говорить о части строки. Однако мы могли бы вернуть порядковый указательконца слова, обозначенного пробелом. Давайте попробуем, как показано в Приложении 4-7.
Файл: src/main.rs
--fn first_word(s: &String) -> usize { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return i; - } - } - - s.len() -} - -fn main() {}
-
Поскольку нам нужно просмотреть String
поэлементно и проверить, является ли значение пробелом, мы преобразуем нашу String
в массив байтов с помощью способа as_bytes
.
fn first_word(s: &String) -> usize {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return i;
- }
- }
-
- s.len()
-}
-
-fn main() {}
-Далее, мы создаём повторитель по массиву байт используя способ iter
:
fn first_word(s: &String) -> usize {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return i;
- }
- }
-
- s.len()
-}
-
-fn main() {}
-Мы обсудим повторители более подробно в Главе 13. На данный мгновение знайте, что iter
— это способ, который возвращает каждый элемент в собрания, а enumerate
оборачивает итог iter
и вместо этого возвращает каждый элемент как часть упорядоченного ряда. Первый элемент упорядоченного ряда, возвращаемый из enumerate
, является порядковым указателем, а второй элемент — ссылкой на элемент. Это немного удобнее, чем вычислять порядковый указательсамостоятельно.
Поскольку способ enumerate
возвращает упорядоченный ряд, мы можем использовать образцы для разъединения этого упорядоченного ряда. Мы подробнее обсудим образцы в Главе 6.. В цикле for
мы указываем образец, имеющий i
для порядкового указателя в упорядоченном ряде и &item
для одного байта в упорядоченном ряде. Поскольку мы получаем ссылку на элемент из .iter().enumerate()
, мы используем &
в образце.
Внутри цикла for
мы ищем байт, представляющий пробел, используя правила написания байтового записи. Если мы находим пробел, мы возвращаем положение. В противном случае мы возвращаем длину строки с помощью s.len()
.
fn first_word(s: &String) -> usize {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return i;
- }
- }
-
- s.len()
-}
-
-fn main() {}
-Теперь у нас есть способ узнать порядковый указательбайта указывающего на конец первого слова в строке, но есть неполадка. Мы возвращаем сам usize
, но это число имеет значение только в среде &String
. Другими словами, поскольку это значение отдельное от String
, то нет заверения, что оно все ещё будет действительным в будущем. Рассмотрим программу из приложения 4-8, которая использует функцию first_word
приложения 4-7.
Файл: src/main.rs
--fn first_word(s: &String) -> usize { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return i; - } - } - - s.len() -} - -fn main() { - let mut s = String::from("hello world"); - - let word = first_word(&s); // word will get the value 5 - - s.clear(); // this empties the String, making it equal to "" - - // word still has the value 5 here, but there's no more string that - // we could meaningfully use the value 5 with. word is now totally invalid! -}
-
Данная программа собирается без ошибок и будет успешно работать, даже после того как мы воспользуемся переменной word
после вызова s.clear()
. Так как значение word
совсем не связано с состоянием переменной s
, то word
сохраняет своё значение 5
без изменений. Мы бы могли воспользоваться значением 5
чтобы получить первое слово из переменной s
, но это приведёт к ошибке, потому что содержимое s
изменилось после того как мы сохранили 5
в переменной word
(стало пустой строкой в вызове s.clear()
).
Необходимость беспокоиться о том, что порядковый указательв переменной word
не согласуется с данными в переменной s
является утомительной и подверженной ошибкам! Управление этими порядковыми указателями становится ещё более хрупким, если мы напишем функцию second_word
. Её ярлык могла бы выглядеть так:
fn second_word(s: &String) -> (usize, usize) {
-Теперь мы отслеживаем начальный и конечный порядковый указатель, и у нас есть ещё больше значений, которые были рассчитаны на основе данных в определённом состоянии, но вообще не привязаны к этому состоянию. У нас есть три несвязанные переменные, которые необходимо согласовать.
-К счастью в Ржавчина есть решение данной сбоев: строковые срезы.
-Строковый срез - это ссылка на часть строки String
и он выглядит следующим образом:
-fn main() { - let s = String::from("hello world"); - - let hello = &s[0..5]; - let world = &s[6..11]; -}
Вместо ссылки на всю String
hello
является ссылкой на часть String
, указанную в дополнительном куске кода [0..5]
. Мы создаём срезы, используя рядв квадратных скобках, указав [starting_index..ending_index]
, где starting_index
— это первая позиция, аending_index
конечный_порядковый указатель— это на единицу больше, чем последняя позиция в срезе. Внутри устройства данных среза хранит начальную положение и длину среза, что соответствует ending_index
- starting_index
. Итак, в случае let world = &s[6..11];
, world
будет срезом, содержащим указатель на байт с порядковым указателем 6 s
со значением длины 5
.
Рисунок 4-6 отображает это на диаграмме.
- --
С правилами написания Ржавчина ..
, если вы хотите начать с порядкового указателя 0, вы можете отбросить значение перед двумя точками. Другими словами, они равны:
-#![allow(unused)] -fn main() { -let s = String::from("hello"); - -let slice = &s[0..2]; -let slice = &s[..2]; -}
Таким же образом, если ваш срез включает последний байт String
, вы можете отбросить конечный номер. Это означает, что они равны:
-#![allow(unused)] -fn main() { -let s = String::from("hello"); - -let len = s.len(); - -let slice = &s[3..len]; -let slice = &s[3..]; -}
Вы также можете отбросить оба значения, чтобы получить часть всей строки. Итак, они равны:
--#![allow(unused)] -fn main() { -let s = String::from("hello"); - -let len = s.len(); - -let slice = &s[0..len]; -let slice = &s[..]; -}
--Примечание. Порядковые указатели ряда срезов строк должны располагаться на допустимых границах символов UTF-8. Если вы попытаетесь создать отрывок строки нарушая границы символа в котором больше одного байта, ваша программа завершится с ошибкой. В целях введения срезов строк мы предполагаем, что в этом разделе используется только ASCII; более подробное обсуждение обработки UTF-8 находится в разделе «Сохранение закодированного текста UTF-8 со строками». раздел главы 8.
-
Давайте используем полученную сведения и перепишем способ first_word
так, чтобы он возвращал срез. Для обозначения вида "срез строки" существует запись &str
:
Файл: src/main.rs
--fn first_word(s: &String) -> &str { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return &s[0..i]; - } - } - - &s[..] -} - -fn main() {}
Мы получаем порядковый указательконца слова так же, как в приложении 4.7, ища первое вхождение пробела. Когда мы находим пробел, мы возвращаем отрывок строки, используя начало строки и порядковый указательпробела в качестве начального и конечного порядковых указателей.
-Теперь, когда мы вызываем first_word
, мы возвращаем одно значение, привязанное к основным данным. Значение состоит из ссылки на начальную точку среза и количества элементов в срезе.
Подобным образом можно переписать и второй способ second_word
:
fn second_word(s: &String) -> &str {
-Теперь у нас есть простой API, который гораздо сложнее испортить, потому что сборщик заверяет, что ссылки в String
останутся действительными. Помните ошибку в программе в приложении 4-8, когда мы получили порядковый указательдо конца первого слова, но затем очиисполнения строку, так что наш порядковый указательстал недействительным? Этот код был логически неправильным, но не показывал немедленных ошибок. Неполадки проявятся позже, если мы попытаемся использовать порядковый указательпервого слова с пустой строкой. Срезы делают эту ошибку невозможной и сообщают нам о неполадке с нашим кодом гораздо раньше. Так, использование исполнения способа first_word
со срезом вернёт ошибку сборки:
Файл: src/main.rs
-fn first_word(s: &String) -> &str {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return &s[0..i];
- }
- }
-
- &s[..]
-}
-
-fn main() {
- let mut s = String::from("hello world");
-
- let word = first_word(&s);
-
- s.clear(); // error!
-
- println!("the first word is: {word}");
-}
-Ошибка сборки:
-$ cargo run
- Compiling ownership v0.1.0 (file:///projects/ownership)
-error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
- --> src/main.rs:18:5
- |
-16 | let word = first_word(&s);
- | -- immutable borrow occurs here
-17 |
-18 | s.clear(); // error!
- | ^^^^^^^^^ mutable borrow occurs here
-19 |
-20 | println!("the first word is: {word}");
- | ------ immutable borrow later used here
-
-For more information about this error, try `rustc --explain E0502`.
-error: could not compile `ownership` (bin "ownership") due to 1 previous error
-
-Напомним из правил заимствования, что если у нас есть неизменяемая ссылка на что-то, мы не можем также взять изменяемую ссылку. Поскольку для clear
необходимо обрезать String
, необходимо получить изменяемую ссылку. println!
после вызова clear
использует ссылку в word
, поэтому неизменяемая ссылка в этот мгновение всё ещё должна быть активной. Ржавчина запрещает одновременное существование изменяемой ссылки в видеclear
и неизменяемой ссылки в word
, и сборка завершается ошибкой. Ржавчина не только упростил использование нашего API, но и устранил целый класс ошибок во время сборки!
Напомним, что мы говорили о строковых записях, хранящихся внутри двоичного файла. Теперь, когда мы знаем чем являются срезы, мы правильно понимаем что такое строковые записи:
--#![allow(unused)] -fn main() { -let s = "Hello, world!"; -}
Вид s
здесь &str
: это срез, указывающий на эту определенную точку двоичного файла. Вот почему строковые записи неизменяемы; &str
— неизменяемая ссылка.
Знание того, что вы можете брать срезы записей и String
значений, приводит нас к ещё одному улучшению first_word
, и это его ярлык:
fn first_word(s: &String) -> &str {
-Более опытный пользователь Rustacean вместо этого написал бы ярлык, показанную в приложении 4.9, потому что это позволяет нам использовать одну и ту же функцию как для значений &String
, так и для значений &str
.
fn first_word(s: &str) -> &str {
- let bytes = s.as_bytes();
-
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return &s[0..i];
- }
- }
-
- &s[..]
-}
-
-fn main() {
- let my_string = String::from("hello world");
-
- // `first_word` works on slices of `String`s, whether partial or whole
- let word = first_word(&my_string[0..6]);
- let word = first_word(&my_string[..]);
- // `first_word` also works on references to `String`s, which are equivalent
- // to whole slices of `String`s
- let word = first_word(&my_string);
-
- let my_string_literal = "hello world";
-
- // `first_word` works on slices of string literals, whether partial or whole
- let word = first_word(&my_string_literal[0..6]);
- let word = first_word(&my_string_literal[..]);
-
- // Because string literals *are* string slices already,
- // this works too, without the slice syntax!
- let word = first_word(my_string_literal);
-}
--
Если у нас есть отрывок строки, мы можем передать его напрямую. Если у нас есть String
, мы можем передать часть String
или ссылку на String
. Эта гибкость использует преимущества приведения deref, функции, которую мы рассмотрим в разделе «Неявное приведение Deref с функциями и способами». раздел главы 15.
Определение функции для получения отрывка строки вместо ссылки на String
делает наш API более общим и полезным без потери какой-либо возможности:
Файл: src/main.rs
--fn first_word(s: &str) -> &str { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return &s[0..i]; - } - } - - &s[..] -} - -fn main() { - let my_string = String::from("hello world"); - - // `first_word` works on slices of `String`s, whether partial or whole - let word = first_word(&my_string[0..6]); - let word = first_word(&my_string[..]); - // `first_word` also works on references to `String`s, which are equivalent - // to whole slices of `String`s - let word = first_word(&my_string); - - let my_string_literal = "hello world"; - - // `first_word` works on slices of string literals, whether partial or whole - let word = first_word(&my_string_literal[0..6]); - let word = first_word(&my_string_literal[..]); - - // Because string literals *are* string slices already, - // this works too, without the slice syntax! - let word = first_word(my_string_literal); -}
Срезы строк, как вы можете себе представить, отличительны для строк. Но есть и более общий вид среза. Рассмотрим этот массив:
--#![allow(unused)] -fn main() { -let a = [1, 2, 3, 4, 5]; -}
Точно так же, как мы можем захотеть сослаться на часть строки, мы можем захотеть сослаться на часть массива. Мы бы сделали так:
--#![allow(unused)] -fn main() { -let a = [1, 2, 3, 4, 5]; - -let slice = &a[1..3]; - -assert_eq!(slice, &[2, 3]); -}
Этот срез имеет вид &[i32]
. Он работает так же, как и срезы строк, сохраняя ссылку на первый элемент и его длину. Вы будете использовать этот вид отрывка для всех видов других собраний. Мы подробно обсудим эти собрания, когда будем говорить о векторах в главе 8.
Подходы владения, заимствования и срезов обеспечивают безопасность памяти в программах на Ржавчина во время сборки. Язык Ржавчина даёт вам управление над использованием памяти так же, как и другие языки системного программирования, но то, что владелец данных самостоятельно очищает эти данные, когда владелец выходит за рамки, означает, что вам не нужно писать и отлаживать дополнительный код, чтобы получить этот управление.
-Владение влияет на множество других частей и подходов языка Rust. Мы будем говорить об этих подходах на протяжении оставшихся частей книги. Давайте перейдём к Главе 5 и рассмотрим объединение частей данных в устройства struct
.
связанных данных
-Устройства (struct) — это пользовательский вид данных, позволяющий назвать и упаковать вместе несколько связанных значений, составляющих значимую логическую объединение. Если вы знакомы с предметно-направленными языками, устройства похожа на свойства данных предмета. В этой главе мы сравним и сопоставим упорядоченные ряды со устройствами, чтобы опираться на то, что вы уже знаете, и отобразим, когда устройства являются лучшим способом объединения данных.
-Мы отобразим, как определять устройства и создавать их образцы. Мы обсудим, как определить сопряженные функции, особенно сопряженные функции, называемые способами, для указания поведения, сопряженного с видом устройства. Устройства и перечисления (обсуждаемые в главе 6) являются строительными разделами для создания новых видов в предметной области вашей программы. Они дают возможность в полной мере воспользоваться преимуществами проверки видов во время сборки Rust.
- -Устройства похожи на упорядоченные ряды, рассмотренные в разделе "Упорядоченные ряды", так как оба хранят несколько связанных значений. Как и упорядоченные ряды, части устройств могут быть разных видов. В отличие от упорядоченных рядов, в устройстве необходимо именовать каждую часть данных для понимания смысла значений. Добавление этих имён обеспечивает большую гибкость устройств по сравнению с упорядоченнымм рядами: не нужно полагаться на порядок данных для указания значений образца или доступа к ним.
-Для определения устройства указывается ключевое слово struct
и её название. Название должно описывать значение частей данных, объединенных вместе. Далее, в фигурных скобках для каждой новой части данных поочерёдно определяются имя части данных и её вид. Каждая пара имя: тип
называется полем. Приложение 5-1 описывает устройство для хранения сведений об учётной записи пользователя:
Имя файла: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn main() {}
-
После определения устройства можно создавать её образец, назначая определённое значение каждому полю с соответствующим видом данных. Чтобы создать образец, мы указываем имя устройства, затем добавляем фигурные скобки и включаем в них пары ключ: значение
(key: value), где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании устройства (но всё-таки желательно для удобства чтения). Другими словами, объявление устройства - это как образец нашего вида, в то время как образец устройства использует этот образец, заполняя его определёнными данными, для создания значений нашего вида. Например, можно объявить пользователя как в приложении 5-2:
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn main() { - let user1 = User { - active: true, - username: String::from("someusername123"), - email: String::from("someone@example.com"), - sign_in_count: 1, - }; -}
-
Чтобы получить определенное значение из устройства, мы используем запись через точку. Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы используем user1.email
. Если образец является изменяемым, мы можем поменять значение, используя точечную наставление и присвоение к определенному полю. В Приложении 5-3 показано, как изменить значение в поле email
изменяемого образца User
.
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn main() { - let mut user1 = User { - active: true, - username: String::from("someusername123"), - email: String::from("someone@example.com"), - sign_in_count: 1, - }; - - user1.email = String::from("anotheremail@example.com"); -}
-
Стоит отметить, что весь образец устройства должен быть изменяемым; Ржавчина не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания устройства в качестве последнего выражения тела функции для неявного возврата нового образца.
-На приложении 5-4 функция build_user
возвращает образец User
с указанным адресом и именем. Поле active
получает значение true
, а поле sign_in_count
получает значение 1
.
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn build_user(email: String, username: String) -> User { - User { - active: true, - username: username, - email: email, - sign_in_count: 1, - } -} - -fn main() { - let user1 = build_user( - String::from("someone@example.com"), - String::from("someusername123"), - ); -}
-
Имеет смысл называть свойства функции теми же именами, что и поля устройства, но необходимость повторять email
и username
для названий полей и переменных несколько утомительна. Если устройства имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!
Так как имена входных свойств функции и полей устройства являются полностью равноценными в приложении 5-4, возможно использовать правила написания сокращённой объявления поля, чтобы переписать build_user
так, чтобы он работал точно также, но не содержал повторений для username
и email
, как в приложении 5-5.
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn build_user(email: String, username: String) -> User { - User { - active: true, - username, - email, - sign_in_count: 1, - } -} - -fn main() { - let user1 = build_user( - String::from("someone@example.com"), - String::from("someusername123"), - ); -}
-
Здесь происходит создание нового образца устройства User
, которая имеет поле с именем email
. Мы хотим установить поле устройства email
значением входного свойства email
функции build_user
. Так как поле email
и входной свойство функции email
имеют одинаковое название, можно писать просто email
вместо кода email: email
.
Часто бывает полезно создать новый образец устройства, который включает большинство значений из другого образца, но некоторые из них изменяет. Это можно сделать с помощью правил написания обновления устройства.
-Сначала в приложении 5-6 показано, как обычно создаётся новый образец User
в user2
без правил написания обновления. Мы задаём новое значение для email
, но в остальном используем те же значения из user1
, которые были заданы в приложении 5-2.
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn main() { - // --snip-- - - let user1 = User { - email: String::from("someone@example.com"), - username: String::from("someusername123"), - active: true, - sign_in_count: 1, - }; - - let user2 = User { - active: user1.active, - username: user1.username, - email: String::from("another@example.com"), - sign_in_count: user1.sign_in_count, - }; -}
-
Используя правила написания обновления устройства, можно получить тот же эффект, используя меньше кода как показано в приложении 5-7. правила написания ..
указывает, что оставшиеся поля устанавливаются неявно и должны иметь значения из указанного образца.
Файл: src/main.rs
--struct User { - active: bool, - username: String, - email: String, - sign_in_count: u64, -} - -fn main() { - // --snip-- - - let user1 = User { - email: String::from("someone@example.com"), - username: String::from("someusername123"), - active: true, - sign_in_count: 1, - }; - - let user2 = User { - email: String::from("another@example.com"), - ..user1 - }; -}
-
Код в приложении 5-7 также создаёт образец в user2
, который имеет другое значение для email
, но с тем же значением для полей username
, active
и sign_in_count
из user1
. Оператор ..user1
должен стоять последним для указания на получение значений всех оставшихся полей из соответствующих полей в user1
, но можно указать значения для любого количества полей в любом порядке, независимо от порядка полей в определении устройства.
Стоит отметить, что правила написания обновления устройства использует =
как присваивание. Это связано с перемещением данных, как мы видели в разделе «Взаимодействие переменных и данных с помощью перемещения». В этом примере мы больше не можем использовать user1
после создания user2
, потому что String
в поле username
из user1
было перемещено в user2
. Если бы мы задали user2
новые значения String
для email
и username
, и таким образом, использовали только значения active
и sign_in_count
из user1
, то user1
всё ещё был бы действительным после создания user2
. Оба вида active
и sign_in_count
выполняют особенность Copy
, поэтому они ведут себя так, как мы обсуждали в разделе «Из обоймы данные: повторение».
Rust также поддерживает устройства, похожие на упорядоченные ряды, которые называются упорядоченные в ряд устройства. Упорядоченные в ряд устройства обладают дополнительным смыслом, который даёт имя устройства, но при этом не имеют имён, связанных с их полями. Скорее, они просто хранят виды полей. Упорядоченные в ряд устройства полезны, когда вы хотите дать имя всему упорядоченному ряду и сделать упорядоченный ряд отличным от других упорядоченных рядов, и когда именование каждого поля, как в обычной устройстве, было бы многословным или избыточным.
-Чтобы определить упорядоченную в ряд устройство, начните с ключевого слова struct
и имени устройства, за которым следуют виды в упорядоченном ряде. Например, здесь мы определяем и используем две упорядоченные в ряд устройства с именами Color
и Point
:
Файл: src/main.rs
--struct Color(i32, i32, i32); -struct Point(i32, i32, i32); - -fn main() { - let black = Color(0, 0, 0); - let origin = Point(0, 0, 0); -}
Обратите внимание, что значения black
и origin
— это разные виды, потому что они являются образцами разных упорядоченных в ряд устройств. Каждая определяемая вами устройства имеет собственный вид, даже если поля внутри устройства могут иметь одинаковые виды. Например, функция, принимающая свойство вида Color
, не может принимать Point
в качестве переменной, даже если оба вида состоят из трёх значений i32
. В остальном образцы упорядоченных в ряд устройств похожи на упорядоченные ряды в том смысле, что вы можете разъединять их на отдельные части и использовать .
, за которой следует порядковый указательдля доступа к отдельному значению.
Также можно определять устройства, не имеющие полей! Они называются единично-подобными устройствами, поскольку ведут себя подобно ()
, единичному виду, о котором мы говорили в разделе "Упорядоченные ряды". Единично-подобные устройства могут быть полезны, когда требуется выполнить особенность для некоторого вида, но у вас нет данных, которые нужно хранить в самом виде. Мы обсудим особенности в главе 10. Вот пример объявления и создание образца единичной устройства с именем AlwaysEqual
:
Файл: src/main.rs
--struct AlwaysEqual; - -fn main() { - let subject = AlwaysEqual; -}
Чтобы определить AlwaysEqual
, мы используем ключевое слово struct
, желаемое имя, а затем точку с запятой. Нет необходимости в фигурных или круглых скобках! Затем мы можем получить образец AlwaysEqual
в переменной subject
подобным образом: используя имя, которое мы определили, без фигурных и круглых скобок. Представим, что в дальнейшем мы выполняем поведение для этого вида таким образом, что каждый образец AlwaysEqual
всегда будет равен каждому образцу любого другого вида, возможно, с целью получения ожидаемого итога для проверки. Для выполнения такого поведения нам не нужны никакие данные! В главе 10 вы увидите, как определять черты и выполнить их для любого вида, включая единично-подобные устройства.
-- - -Владение данными устройства
-В определении устройства
-User
в приложении 5-1 мы использовали владеющий видString
вместо вида строковый срез&str
. Это осознанный выбор, поскольку мы хотим, чтобы каждый образец этой устройства владел всеми своими данными и чтобы эти данные были действительны до тех пор, пока действительна вся устройства.Устройства также могут хранить ссылки на данные, принадлежащие кому-то другому, но для этого необходимо использовать возможность Ржавчина время жизни, которую мы обсудим в главе 10. Время жизни заверяет, что данные, на которые ссылается устройства, будут действительны до тех пор, пока существует устройства. Допустим, если попытаться сохранить ссылку в устройстве без указания времени жизни, как в следующем примере; это не сработает:
-Файл: src/main.rs
- --struct User { - active: bool, - username: &str, - email: &str, - sign_in_count: u64, -} - -fn main() { - let user1 = User { - active: true, - username: "someusername123", - email: "someone@example.com", - sign_in_count: 1, - }; -}
Сборщик будет жаловаться на необходимость определения времени жизни ссылок:
--$ cargo run - Compiling structs v0.1.0 (file:///projects/structs) -error[E0106]: missing lifetime specifier - --> src/main.rs:3:15 - | -3 | username: &str, - | ^ expected named lifetime parameter - | -help: consider introducing a named lifetime parameter - | -1 ~ struct User<'a> { -2 | active: bool, -3 ~ username: &'a str, - | - -error[E0106]: missing lifetime specifier - --> src/main.rs:4:12 - | -4 | email: &str, - | ^ expected named lifetime parameter - | -help: consider introducing a named lifetime parameter - | -1 ~ struct User<'a> { -2 | active: bool, -3 | username: &str, -4 ~ email: &'a str, - | - -For more information about this error, try `rustc --explain E0106`. -error: could not compile `structs` due to 2 previous errors -
В главе 10 мы обсудим, как исправить эти ошибки, чтобы иметь возможность хранить ссылки в устройствах, а пока мы исправим подобные ошибки, используя владеющие виды вроде
-String
вместо ссылок&str
.
Чтобы понять, когда нам может понадобиться использование устройств, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования устройств.
-Давайте создадим новый дело программы при помощи Cargo и назовём его rectangles. Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Приложение 5-8 показывает один из коротких исходов кода, который позволит нам сделать именно то, что надо, в файле дела src/main.rs.
-Файл: src/main.rs
--fn main() { - let width1 = 30; - let height1 = 50; - - println!( - "The area of the rectangle is {} square pixels.", - area(width1, height1) - ); -} - -fn area(width: u32, height: u32) -> u32 { - width * height -}
-
Теперь запустим программу, используя cargo run
:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
- Running `target/debug/rectangles`
-The area of the rectangle is 1500 square pixels.
-
-Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area
с каждым измерением, но мы можем улучшить его ясность и читабельность.
Неполадкаданного способа очевидна из ярлыки area
:
fn main() {
- let width1 = 30;
- let height1 = 50;
-
- println!(
- "The area of the rectangle is {} square pixels.",
- area(width1, height1)
- );
-}
-
-fn area(width: u32, height: u32) -> u32 {
- width * height
-}
-Функция area
должна вычислять площадь одного прямоугольника, но функция, которую мы написали, имеет два свойства, и нигде в нашей программе не ясно, что эти свойства взаимосвязаны. Было бы более читабельным и управляемым собъединять ширину и высоту вместе. В разделе «Упорядоченные ряды» главы 3 мы уже обсуждали один из способов сделать это — использовать упорядоченные ряды.
Приложение 5-9 — это другая исполнение программы, использующая упорядоченные ряды.
-Файл: src/main.rs
--fn main() { - let rect1 = (30, 50); - - println!( - "The area of the rectangle is {} square pixels.", - area(rect1) - ); -} - -fn area(dimensions: (u32, u32)) -> u32 { - dimensions.0 * dimensions.1 -}
-
С одной стороны, эта программа лучше. Упорядоченные ряды позволяют добавить немного устройства, и теперь мы передаём только один переменная. Но с другой стороны, эта исполнение менее понятна: упорядоченные ряды не называют свои элементы, поэтому нам приходится упорядочивать части упорядоченного ряда, что делает наше вычисление менее очевидным.
-Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width
находится в упорядоченном ряде с порядковым указателем 0
, а высота height
— с порядковым указателем 1
. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.
Мы используем устройства, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый упорядоченный ряд в устройство с единым именем для сущности и частными названиями её частей, как показано в приложении 5-10.
-Файл: src/main.rs
--struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println!( - "The area of the rectangle is {} square pixels.", - area(&rect1) - ); -} - -fn area(rectangle: &Rectangle) -> u32 { - rectangle.width * rectangle.height -}
-
Здесь мы определили устройство и дали ей имя Rectangle
. Внутри фигурных скобок определили поля как width
и height
, оба — вида u32
. Затем в main
создали определенный образец Rectangle
с шириной в 30
и высотой в 50
единиц.
Наша функция area
теперь определена с одним свойствоом, названным rectangle
, чей вид является неизменяемым заимствованием устройства Rectangle
. Как упоминалось в главе 4, необходимо заимствовать устройство, а не передавать её во владение. Таким образом функция main
сохраняет rect1
в собственности и может использовать её дальше. По этой причине мы и используем &
в ярлыке и в месте вызова функции.
Функция area
получает доступ к полям width
и height
образца Rectangle
(обратите внимание, что доступ к полям заимствованного образца устройства не приводит к перемещению значений полей, поэтому вы часто видите заимствования устройств). Наша ярлык функции для area
теперь говорит именно то, что мы имеем в виду: вычислить площадь Rectangle
, используя его поля width
и height
. Это означает, что ширина и высота связаны друг с другом, и даёт описательные имена значениям, а не использует значения порядкового указателя упорядоченного ряда 0
и 1
. Это торжество ясности.
Было бы полезно иметь возможность печатать образец Rectangle
во время отладки программы и видеть значения всех полей. Приложение 5-11 использует макрос println!
, который мы уже использовали в предыдущих главах. Тем не менее, это не работает.
Файл: src/main.rs
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
-
- println!("rect1 is {}", rect1);
-}
--
При сборки этого кода мы получаем ошибку с сообщением:
-error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
-
-Макрос println!
умеет выполнять множество видов изменения, и по умолчанию фигурные скобки в println!
означают использование изменение
-, известное как особенность Display
. Его вывод предназначен для непосредственного использования конечным пользователем. Простые виды, изученные ранее, по умолчанию выполняют особенность Display
, потому что есть только один способ отобразить число 1
или любой другой простой вид. Но для устройств изменение
-println!
менее очевидно, потому что есть гораздо больше способов отображения: Вы хотите запятые или нет? Вы хотите печатать фигурные скобки? Должны ли отображаться все поля? Из-за этой неоднозначности Ржавчина не пытается угадать, что нам нужно, а устройства не имеют встроенной выполнения Display
для использования в println!
с заполнителем {}
.
Продолжив чтение текста ошибки, мы найдём полезное замечание:
- = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
- = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
-
-Давайте попробуем! Вызов макроса println!
теперь будет выглядеть так println!("rect1 is {:?}", rect1);
. Ввод определетеля :?
внутри фигурных скобок говорит макросу println!
, что мы хотим использовать другой вид вывода, известный как Debug
. Особенность Debug
позволяет печатать устройство способом, удобным для разработчиков, чтобы видеть значение во время отладки кода.
Соберем код с этими изменениями. Упс! Мы всё ещё получаем ошибку:
-error[E0277]: `Rectangle` doesn't implement `Debug`
-
-Снова сборщик даёт нам полезное замечание:
- = help: the trait `Debug` is not implemented for `Rectangle`
- = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
-
-Rust выполняет возможность для печати отладочной сведений, но не включает (не выводит) её по умолчанию. Мы должны явно включить эту возможность для нашей устройства. Чтобы это сделать, добавляем внешний свойство #[derive(Debug)]
сразу перед определением устройства, как показано в приложении 5-12.
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println!("rect1 is {rect1:?}"); -}
-
Теперь при запуске программы мы не получим ошибок и увидим следующий вывод:
-$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/rectangles`
-rect1 is Rectangle { width: 30, height: 50 }
-
-Отлично! Это не самый красивый вывод, но он показывает значения всех полей образца, которые определённо помогут при отладке. Когда у нас более крупные устройства, то полезно иметь более простой для чтения вывод; в таких случаях можно использовать код {:#?}
вместо {:?}
в строке макроса println!
. В этом примере использование исполнения {:#?}
приведёт к такому выводу:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/rectangles`
-rect1 is Rectangle {
- width: 30,
- height: 50,
-}
-
-Другой способ распечатать значение в видеDebug
— использовать макрос dbg!
, который становится владельцем выражения (в отличие от println!
, принимающего ссылку), печатает номер файла и строки, где происходит вызов макроса dbg!
, вместе с результирующим значением этого выражения и возвращает владение на значение.
--Примечание: при вызове макроса
-dbg!
выполняется печать в обычный поток ошибок (stderr
), в отличие отprintln!
, который использует обычный поток вывода в окно вывода (stdout
). Подробнее оstderr
иstdout
мы поговорим в разделе «Запись сообщений об ошибках в обычный вывод ошибок вместо принятого вывода» главы 12.
Вот пример, когда нас важно значение, которое присваивается полю width
, а также значение всей устройства в rect1
:
-#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let scale = 2; - let rect1 = Rectangle { - width: dbg!(30 * scale), - height: 50, - }; - - dbg!(&rect1); -}
Можем написать макрос dbg!
вокруг выражения 30 * scale
, потому что dbg!
возвращает владение значения выражения. Поле width
получит то же значение, как если бы у нас не было вызова dbg!
. Мы не хотим, чтобы макрос dbg!
становился владельцем rect1
, поэтому используем ссылку на rect1
в следующем вызове. Вот как выглядит вывод этого примера:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running `target/debug/rectangles`
-[src/main.rs:10:16] 30 * scale = 60
-[src/main.rs:14:5] &rect1 = Rectangle {
- width: 60,
- height: 50,
-}
-
-Мы можем увидеть, что первый отладочный вывод поступил из строки 10 src/main.rs, там, где мы отлаживаем выражение 30 * scale
, и его результирующее значение равно 60 (Debug
изменение
-, выполненное для целых чисел, заключается в печати только их значения). Вызов dbg!
в строке 14 src/main.rs выводит значение &rect1
, которое является устройством Rectangle
. В этом выводе используется красивое изменение
-Debug
вида Rectangle
. Макрос dbg!
может быть очень полезен, когда вы пытаетесь понять, что делает ваш код!
В дополнение к Debug
, Ржавчина предоставил нам ряд особенностей, которые мы можем использовать с свойством derive
для добавления полезного поведения к нашим пользовательским видам. Эти особенности и их поведение перечислены в приложении C. Мы расскажем, как выполнить эти особенности с пользовательским поведением, а также как создать свои собственные особенности в главе 10. Кроме того, есть много других свойств помимо derive
; для получения дополнительной сведений смотрите раздел “Свойства” справочника Rust.
Функция area
является довольно отличительной: она считает только площадь прямоугольников. Было бы полезно привязать данное поведение как можно ближе к устройстве Rectangle
, потому что наш отличительный код не будет работать с любым другим видом. Давайте рассмотрим, как можно улучшить наш код превращая функцию area
в способ area
, определённый для вида Rectangle
.
Способы похожи на функции: мы объявляем их с помощью ключевого слова fn
и имени, они могут иметь свойства и возвращаемое значение, и они содержат код, запускающийся в случае вызова способа. В отличие от функций, способы определяются в среде устройства (или предмета перечисления или особенности, которые мы рассмотрим в главе 6) и главе 17 соответственно), а их первым свойствоом всегда является self
, представляющий собой образец устройства, с которой вызывается этот способ.
Давайте изменим функцию area
так, чтобы она имела образец Rectangle
в качестве входного свойства и сделаем её способом area
, определённым для устройства Rectangle
, как показано в приложении 5-13:
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -impl Rectangle { - fn area(&self) -> u32 { - self.width * self.height - } -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - println!( - "The area of the rectangle is {} square pixels.", - rect1.area() - ); -}
-
Чтобы определить функцию в среде Rectangle
, мы создаём разделimpl
(implementation - выполнение) для Rectangle
. Всё в impl
будет связано с видом Rectangle
. Затем мы перемещаем функцию area
внутрь фигурных скобок impl
и меняем первый (и в данном случае единственный) свойство на self
в ярлыке и в теле. В main
, где мы вызвали функцию area
и передали rect1
в качестве переменной, теперь мы можем использовать правила написания способа для вызова способа area
нашего образца Rectangle
. правила написания способа идёт после образца: мы добавляем точку, за которой следует имя способа, круглые скобки и любые переменные.
В ярлыке area
мы используем &self
вместо rectangle: &Rectangle
. &self
на самом деле является сокращением от self: &Self
. Внутри раздела impl
вид Self
является псевдонимом вида, для которого выполнен разделimpl
. Способы обязаны иметь свойство с именем self
вида Self
, поэтому Ржавчина позволяет вам сокращать его, используя только имя self
на месте первого свойства. Обратите внимание, что нам по-прежнему нужно использовать &
перед сокращением self
, чтобы указать на то, что этот способ заимствует образец Self
, точно так же, как мы делали это в rectangle: &Rectangle
. Как и любой другой свойство, способы могут брать во владение self
, заимствовать неизменяемый self
, как мы поступили в данном случае, или заимствовать изменяемый self
.
Мы выбрали &self
здесь по той же причине, по которой использовали &Rectangle
в исполнения кода с функцией: мы не хотим брать устройство во владение, мы просто хотим прочитать данные в устройстве, а не писать в неё. Если бы мы хотели изменить образец, на котором мы вызывали способ силами самого способа, то мы бы использовали &mut self
в качестве первого свойства. Наличие способа, который берёт образец во владение, используя только self
в качестве первого свойства, является редким; эта техника обычно используется, когда способ превращает self
во что-то ещё, и вы хотите запретить вызывающей стороне использовать исходный образец после превращения.
Основная причина использования способов вместо функций, помимо правил написания способа, где нет необходимости повторять вид self
в ярлыке каждого способа, заключается в согласования кода. Мы помеисполнения все, что мы можем сделать с образцом вида, в один impl
вместо того, чтобы заставлять будущих пользователей нашего кода искать доступный возможности Rectangle
в разных местах предоставляемой нами библиотеки.
Обратите внимание, что мы можем дать способу то же имя, что и одному из полей устройства. Например, для Rectangle
мы можем определить способ, также названный width
:
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -impl Rectangle { - fn width(&self) -> bool { - self.width > 0 - } -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - - if rect1.width() { - println!("The rectangle has a nonzero width; it is {}", rect1.width); - } -}
Здесь мы определили, чтобы способ width
возвращал значение true
, если значение в поле width
образца больше 0
, и значение false
, если значение равно 0
, но мы можем использовать поле в способе с тем же именем для любых целей. В main
, когда мы ставим после rect1.width
круглые скобки, Ржавчина знает, что мы имеем в виду способ width
. Когда мы не используем круглые скобки, Ржавчина понимает, что мы имеем в виду поле width
.
Часто, но не всегда, когда мы создаём способы с тем же именем, что и у поля, мы хотим, чтобы он только возвращал значение одноимённого поля и больше ничего не делал. Подобные способы называются геттерами, и Ржавчина не выполняет их самостоятельно для полей устройства, как это делают некоторые другие языки. Геттеры полезны, поскольку вы можете сделать поле закрытым, а способ открытым и, таким образом, включить доступ только для чтения к этому полю как часть общедоступного API вида. Мы обсудим, что такое открытость и закрытость, и как обозначить поле или способ в качестве открытого или закрытого в главе 7.
---Где используется оператор
-->
?В языках C и C++, используются два различных оператора для вызова способов: используется
-.
, если вызывается способ непосредственно у образца устройства и используется->
, если вызывается способ для указателя на предмет. Другими словами, еслиobject
является указателем, то вызовы способаobject->something()
и(*object).something()
являются подобными.Ржавчина не имеет эквивалента оператора
-->
, наоборот, в Ржавчина есть возможность называемая самостоятельное обращение по ссылке и разыменование (automatic referencing and dereferencing). Вызов способов является одним из немногих мест в Rust, в котором есть такое поведение.Вот как это работает: когда вы вызываете способ
- -object.something()
, Ржавчина самостоятельно добавляет&
,&mut
или*
, таким образом, чтобыobject
соответствовал ярлыке способа. Другими словами, это то же самое:-#![allow(unused)] -fn main() { -#[derive(Debug,Copy,Clone)] -struct Point { - x: f64, - y: f64, -} - -impl Point { - fn distance(&self, other: &Point) -> f64 { - let x_squared = f64::powi(other.x - self.x, 2); - let y_squared = f64::powi(other.y - self.y, 2); - - f64::sqrt(x_squared + y_squared) - } -} -let p1 = Point { x: 0.0, y: 0.0 }; -let p2 = Point { x: 5.0, y: 6.5 }; -p1.distance(&p2); -(&p1).distance(&p2); -}
Первый пример выглядит намного понятнее. Самостоятельный вывод ссылки работает потому, что способы имеют понятного получателя - вид
-self
. Учитывая получателя и имя способа, Ржавчина может точно определить, что в данном случае делает код: читает ли способ (&self
), делает ли изменение (&mut self
) или поглощает (self
). Тотобстоятельство, что Ржавчина делает заимствование неявным для принимающего способа, в значительной степени способствует тому, чтобы сделать владение удобным на опыте.
Давайте применим в использовании способов, выполнив второй способ в устройстве Rectangle
. На этот раз мы хотим, чтобы образец Rectangle
брал другой образец Rectangle
и возвращал true
, если второй Rectangle
может полностью поместиться внутри self
(первый Rectangle
); в противном случае он должен вернуть false
. То есть, как только мы определим способ can_hold
, мы хотим иметь возможность написать программу, показанную в Приложении 5-14.
Файл: src/main.rs
-fn main() {
- let rect1 = Rectangle {
- width: 30,
- height: 50,
- };
- let rect2 = Rectangle {
- width: 10,
- height: 40,
- };
- let rect3 = Rectangle {
- width: 60,
- height: 45,
- };
-
- println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
- println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
-}
--
Ожидаемый итог будет выглядеть следующим образом, т.к. оба размера в образце rect2
меньше, чем размеры в образце rect1
, а rect3
шире, чем rect1
:
Can rect1 hold rect2? true
-Can rect1 hold rect3? false
-
-Мы знаем, что хотим определить способ, поэтому он будет находится в impl Rectangle
разделе. Имя способа будет can_hold
, и оно будет принимать неизменяемое заимствование на другой Rectangle
в качестве свойства. Мы можем сказать, какой это будет вид свойства, посмотрев на код вызывающего способа: способ rect1.can_hold(&rect2)
передаёт в него &rect2
, который является неизменяемым заимствованием образца rect2
вида Rectangle
. В этом есть смысл, потому что нам нужно только читать rect2
(а не писать, что означало бы, что нужно изменяемое заимствование), и мы хотим, чтобы main
сохранил право собственности на образец rect2
, чтобы мы могли использовать его снова после вызов способа can_hold
. Возвращаемое значение can_hold
имеет булевый вид, а выполнение проверяет, являются ли ширина и высота self
больше, чем ширина и высота другого Rectangle
соответственно. Давайте добавим новый способ can_hold
в impl
разделиз приложения 5-13, как показано в приложении 5-15.
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -impl Rectangle { - fn area(&self) -> u32 { - self.width * self.height - } - - fn can_hold(&self, other: &Rectangle) -> bool { - self.width > other.width && self.height > other.height - } -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - let rect2 = Rectangle { - width: 10, - height: 40, - }; - let rect3 = Rectangle { - width: 60, - height: 45, - }; - - println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); - println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); -}
-
Когда мы запустим код с функцией main
приложения 5-14, мы получим желаемый вывод. Способы могут принимать несколько свойств, которые мы добавляем в ярлык после первого свойства self
, и эти свойства работают так же, как свойства в функциях.
Все функции, определённые в разделе impl
, называются сопряженными функциями, потому что они сопряжены с видом, указанным после ключевого слова impl
. Мы можем определить сопряженные функции, которые не имеют self
в качестве первого свойства (и, следовательно, не являются способами), потому что им не нужен образец вида для работы. Мы уже использовали одну подобную функцию: функцию String::from
, определённую для вида String
.
Сопряженные функции, не являющиеся способами, часто используются для строителей, возвращающих новый образец устройства. Их часто называют new
, но new
не является особым именем и не встроена в язык. Например, мы можем предоставить сопряженную функцию с именем square
, которая будет иметь один свойство размера и использовать его как ширину и высоту, что упростит создание квадратного Rectangle
, вместо того, чтобы указывать одно и то же значение дважды:
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -impl Rectangle { - fn square(size: u32) -> Self { - Self { - width: size, - height: size, - } - } -} - -fn main() { - let sq = Rectangle::square(3); -}
Ключевые слова Self
в возвращаемом виде и в теле функции являются псевдонимами для вида, указанного после ключевого слова impl
, которым в данном случае является Rectangle
.
Чтобы вызвать эту связанную функцию, используется правила написания ::
с именем устройства; например let sq = Rectangle::square(3);
. Эта функция находится в пространстве имён устройства. правила написания ::
используется как для связанных функций, так и для пространств имён, созданных звенами. Мы обсудим звенья в главе 7.
impl
Каждая устройства может иметь несколько impl
. Например, Приложение 5-15 эквивалентен коду, показанному в приложении 5-16, в котором каждый способ находится в своём собственном разделе impl
.
-#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -impl Rectangle { - fn area(&self) -> u32 { - self.width * self.height - } -} - -impl Rectangle { - fn can_hold(&self, other: &Rectangle) -> bool { - self.width > other.width && self.height > other.height - } -} - -fn main() { - let rect1 = Rectangle { - width: 30, - height: 50, - }; - let rect2 = Rectangle { - width: 10, - height: 40, - }; - let rect3 = Rectangle { - width: 60, - height: 45, - }; - - println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); - println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); -}
-
Здесь нет причин разделять способы на несколько impl
, но это допустимый правила написания. Мы увидим случай, когда несколько impl
могут оказаться полезными, в Главе 10, рассматривающей обобщённые виды и свойства.
Устройства позволяют создавать собственные виды, которые имеют смысл в вашей предметной области. Используя устройства, вы храните сопряженные друг с другом отрывки данных и даёте название частям данных, чтобы ваш код был более понятным. Способы позволяют определить поведение, которое имеют образцы ваших устройств, а сопряженные функции позволяют привязать возможность к вашей устройстве, не обращаясь к её образцу.
-Но устройства — не единственный способ создавать собственные виды: давайте обратимся к перечислениям в Rust, чтобы добавить ещё один средство в свой арсенал.
- -В этой главе мы рассмотрим перечисления (enumerations), также называемые enums. Перечисления позволяют определить вид путём перечисления его возможных исходов . Сначала мы определим и используем перечисление, чтобы показать, как оно может объединить значения и данные. Далее мы рассмотрим особенно полезное перечисление под названием Option
, которое выражает, что значение может быть либо чем-то, либо ничем. Затем мы рассмотрим, как сопоставление с образцом в выражении match
позволяет легко запускать разный код для разных значений перечисления. Наконец, мы узнаем, насколько устройство if let
удобна и кратка для обработки перечислений в вашем коде.
Там, где устройства дают вам возможность объединять связанные поля и данные, например Rectangle
с его width
и height
, перечисления дают вам способ сказать, что значение является одним из возможных наборов значений. Например, мы можем захотеть сказать, что Rectangle
— это одна из множества возможных фигур, в которую также входят Circle
и Triangle
. Для этого Ржавчина позволяет нам закодировать эти возможности в виде перечисления.
Давайте рассмотрим случай, которую мы могли бы захотеть отразить в коде, и поймём, почему перечисления полезны и более уместны, чем устройства в этом случае. Допустим, нам нужно работать с IP-адресами. В настоящее время для обозначения IP-адресов используются два основных исполнения: четвёртая и шестая исполнения. Поскольку это единственно возможные исходы IP-адресов, с которыми может столкнуться наша программа, мы можем перечислить все возможные исходы, откуда перечисление и получило своё название.
-Любой IP-адрес может быть либо четвёртой, либо шестой исполнения, но не обеими одновременно. Эта особенность IP-адресов делает устройство данных enum подходящей, поскольку значение enum может представлять собой только один из его возможных исходов. Адреса как четвёртой, так и шестой исполнения по своей сути все равно являются IP-адресами, поэтому их следует рассматривать как один и тот же вид, когда в коде обрабатываются задачи, относящиеся к любому виду IP-адресов.
-Можно выразить эту подход в коде, определив перечисление IpAddrKind
и составив список возможных видов IP-адресов, V4
и V6
. Вот исходы перечислений:
-enum IpAddrKind { - V4, - V6, -} - -fn main() { - let four = IpAddrKind::V4; - let six = IpAddrKind::V6; - - route(IpAddrKind::V4); - route(IpAddrKind::V6); -} - -fn route(ip_kind: IpAddrKind) {}
IpAddrKind
теперь является пользовательским видом данных, который мы можем использовать в другом месте нашего кода.
Образцы каждого исхода перечисления IpAddrKind
можно создать следующим образом:
-enum IpAddrKind { - V4, - V6, -} - -fn main() { - let four = IpAddrKind::V4; - let six = IpAddrKind::V6; - - route(IpAddrKind::V4); - route(IpAddrKind::V6); -} - -fn route(ip_kind: IpAddrKind) {}
Обратите внимание, что исходы перечисления находятся в пространстве имён вместе с его определителем, а для их обособления мы используем двойное двоеточие. Это удобно тем, что теперь оба значения IpAddrKind::V4
и IpAddrKind::V6
относятся к одному виду: IpAddrKind
. Затем мы можем, например, определить функцию, которая принимает любой из исходов IpAddrKind
:
-enum IpAddrKind { - V4, - V6, -} - -fn main() { - let four = IpAddrKind::V4; - let six = IpAddrKind::V6; - - route(IpAddrKind::V4); - route(IpAddrKind::V6); -} - -fn route(ip_kind: IpAddrKind) {}
Можно вызвать эту функцию с любым из исходов:
--enum IpAddrKind { - V4, - V6, -} - -fn main() { - let four = IpAddrKind::V4; - let six = IpAddrKind::V6; - - route(IpAddrKind::V4); - route(IpAddrKind::V6); -} - -fn route(ip_kind: IpAddrKind) {}
Использование перечислений позволяет получить ещё больше преимуществ. Если подумать о нашем виде для IP-адреса, то выяснится, что на данный мгновение у нас нет возможности хранить собственно сам IP-адрес; мы будем знать только его вид. Учитывая, что недавно в главе 5 вы узнали о устройствах, у вас может возникнуть соблазн решить эту неполадку с помощью устройств, как показано в приложении 6-1.
--fn main() { - enum IpAddrKind { - V4, - V6, - } - - struct IpAddr { - kind: IpAddrKind, - address: String, - } - - let home = IpAddr { - kind: IpAddrKind::V4, - address: String::from("127.0.0.1"), - }; - - let loopback = IpAddr { - kind: IpAddrKind::V6, - address: String::from("::1"), - }; -}
-
Здесь мы определили устройство IpAddr
, у которой есть два поля: kind
вида IpAddrKind
(перечисление, которое мы определили ранее) и address
вида String
. У нас есть два образца этой устройства. Первый - home
, который является IpAddrKind::V4
в качестве значения kind
с соответствующим адресом 127.0.0.1
. Второй образец - loopback
. Он в качестве значения kind
имеет другой исход IpAddrKind
, V6
, и с ним сопряжен адрес ::1
. Мы использовали устройство для объединения значений kind
и address
вместе, таким образом вид вида адреса теперь сопряжен со значением.
Однако представление этой же подходы с помощью перечисления более кратко: вместо того, чтобы помещать перечисление в устройство, мы можем поместить данные непосредственно в любой из исходов перечисления. Это новое определение перечисления IpAddr
гласит, что оба исхода V4
и V6
будут иметь соответствующие значения String
:
-fn main() { - enum IpAddr { - V4(String), - V6(String), - } - - let home = IpAddr::V4(String::from("127.0.0.1")); - - let loopback = IpAddr::V6(String::from("::1")); -}
Мы прикрепляем данные к каждому исходу перечисления напрямую, поэтому нет необходимости в дополнительной устройстве. Здесь также легче увидеть ещё одну подробность того, как работают перечисления: имя каждого исхода перечисления, который мы определяем, также становится функцией, которая создаёт образец перечисления. То есть IpAddr::V4()
- это вызов функции, который принимает String
и возвращает образец вида IpAddr
. Мы самостоятельно получаем эту функцию-строитель, определяемую в итоге определения перечисления.
Ещё одно преимущество использования перечисления вместо устройства заключается в том, что каждый исход перечисления может иметь разное количество сопряженных данных представленных в разных видах. Исполнение 4 для IP адресов всегда будет содержать четыре цифровых составляющих, которые будут иметь значения между 0 и 255. При необходимости сохранить адреса вида V4
как четыре значения вида u8
, а также описать адреса вида V6
как единственное значение вида String
, мы не смогли бы с помощью устройства. Перечисления решают эту задачу легко:
-fn main() { - enum IpAddr { - V4(u8, u8, u8, u8), - V6(String), - } - - let home = IpAddr::V4(127, 0, 0, 1); - - let loopback = IpAddr::V6(String::from("::1")); -}
Мы показали несколько различных способов определения устройств данных для хранения IP-адресов четвёртой и шестой исполнений. Однако, как оказалось, желание хранить IP-адреса и указывать их вид настолько распространено, что в встроенной библиотеке есть определение, которое мы можем использовать! Давайте посмотрим, как обычная библиотека определяет IpAddr
: в ней есть точно такое же перечисление с исходами, которое мы определили и использовали, но она помещает данные об адресе внутрь этих исходов в виде двух различных устройств, которые имеют различные определения для каждого из исходов:
-#![allow(unused)] -fn main() { -struct Ipv4Addr { - // --snip-- -} - -struct Ipv6Addr { - // --snip-- -} - -enum IpAddr { - V4(Ipv4Addr), - V6(Ipv6Addr), -} -}
Этот код отображает что мы можем добавлять любой вид данных в значение перечисления: строку, число, устройство и пр. Вы даже можете включить в перечисление другие перечисления! Обычные виды данных не очень сложны, хотя, возможно, могут быть очень сложными (вложенность данных может быть очень глубокой).
-Обратите внимание, что хотя определение перечисления IpAddr
есть в встроенной библиотеке, мы смогли объявлять и использовать свою собственную выполнение с подобным названием без каких-либо несоответствий, потому что мы не добавили определение встроенной библиотеки в область видимости кода. Подробнее об этом поговорим в Главе 7.
Рассмотрим другой пример перечисления в приложении 6-2: в этом примере каждый элемент перечисления имеет свой особый вид данных внутри:
--enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(i32, i32, i32), -} - -fn main() {}
-
Это перечисление имеет 4 элемента:
-Quit
- пустой элемент без сопряженных данных,Move
имеет именованные поля, как и устройства.Write
- элемент с единственной строкой вида String
,ChangeColor
- упорядоченный ряд из трёх значений вида i32
.Определение перечисления с исходами, такими как в приложении 6-2, похоже на определение значений различных видов внутри устройств, за исключением того, что перечисление не использует ключевое слово struct
и все исходы объединены внутри вида Message
. Следующие устройства могут содержать те же данные, что и предыдущие исходы перечислений:
-struct QuitMessage; // unit struct -struct MoveMessage { - x: i32, - y: i32, -} -struct WriteMessage(String); // tuple struct -struct ChangeColorMessage(i32, i32, i32); // tuple struct - -fn main() {}
Но когда мы использовали различные устройства, каждая из которых имеет свои собственные виды, мы не могли легко определять функции, которые принимают любые виды сообщений, как это можно сделать с помощью перечисления вида Message
, объявленного в приложении 6-2, который является единым видом.
Есть ещё одно сходство между перечислениями и устройствами: так же, как мы можем определять способы для устройств с помощью impl
раздела, мы можем определять и способы для перечисления. Вот пример способа с именем call
, который мы могли бы определить в нашем перечислении Message
:
-fn main() { - enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(i32, i32, i32), - } - - impl Message { - fn call(&self) { - // method body would be defined here - } - } - - let m = Message::Write(String::from("hello")); - m.call(); -}
В теле способа будет использоваться self
для получения значение того предмета. у которого мы вызвали этот способ. В этом примере мы создали переменную m
, содержащую значение Message::Write(String::from("hello"))
, и именно это значение будет представлять self
в теле способа call
при выполнении m.call()
.
Теперь посмотрим на другое наиболее часто используемое перечисление из встроенной библиотеки, которое является очень распространённым и полезным: Option
.
Option
и его преимущества перед Null-значениямиВ этом разделе рассматривается пример использования Option
, ещё одного перечисления, определённого в встроенной библиотеке. Вид Option
кодирует очень распространённый сценарий, в котором значение может быть чем-то, а может быть ничем.
Например, если вы запросите первый элемент из непустого списка, вы получите значение. Если вы запросите первый элемент пустого списка, вы ничего не получите. Выражение этой подходы в понятиях системы видов означает, что сборщик может проверить, обработали ли вы все случаи, которые должны были обработать; эта возможность может предотвратить ошибки, которые чрезвычайно распространены в других языках программирования.
-Внешний вид языка программирования часто рассматривается с точки зрения того, какие функции вы включаете в него, но те функции, которые вы исключаете, также важны. Например в Ржавчина нет такого возможностей как null значения, однако он есть во многих других языках. Null значение - это значение, которое означает, что значения нет. В языках с null значением переменные всегда могут находиться в одном из двух состояний: нет значения (null) или есть значение (not-null).
-В своей презентации 2009 года «Null ссылки: ошибка в миллиард долларов» Тони Хоар (Tony Hoare), изобретатель null, сказал следующее:
---Я называю это своей ошибкой на миллиард долларов. В то время я разрабатывал первую комплексную систему видов для ссылок на предметно-направленном языке. Моя цель состояла в том, чтобы обеспечить, что любое использование ссылок должно быть абсолютно безопасным, с самостоятельной проверкой сборщиком. Но я не мог устоять перед соблазном вставить пустую ссылку просто потому, что это было так легко выполнить. Это привело к бесчисленным ошибкам, уязвимостям и системным сбоям, которые, вероятно, причинили боль и ущерб на миллиард долларов за последние сорок лет.
-
Неполадкас null значениями заключается в том, что если вы попытаетесь использовать null значение в качестве not-null значения, вы получите ошибку определённого рода. Поскольку свойство null или not-null распространено повсеместно, сделать такую ошибку очень просто.
-Тем не менее, подход, которую null пытается выразить, является полезной: null - это значение, которое в настоящее время по какой-то причине недействительно или отсутствует.
-Неполадкана самом деле не в подходы, а в именно выполнения. Таким образом, в Ржавчина нет значений null, но есть перечисление, которое может закодировать подход присутствия или отсутствия значения. Это перечисление Option<T>
, и оно определено встроенной библиотекой следующим образом:
-#![allow(unused)] -fn main() { -enum Option<T> { - None, - Some(T), -} -}
Перечисление Option<T>
настолько полезно, что оно даже включено в прелюдию; вам не нужно явно вводить его в область видимости. Его исходы также включены в прелюдию: вы можете использовать Some
и None
напрямую, без приставки Option::
. При всём при этом, Option<T>
является обычным перечислением, а Some(T)
и None
представляют собой его исходы.
<T>
- это особенность Rust, о которой мы ещё не говорили. Это свойство обобщённого вида, и мы рассмотрим его более подробно в главе 10. На данный мгновение всё, что вам нужно знать, это то, что <T>
означает, что исход Some
Option
может содержать один отрывок данных любого вида, и что каждый определенный вид, который используется вместо T
делает общий Option<T>
другим видом. Вот несколько примеров использования Option
для хранения числовых и строковых видов:
-fn main() { - let some_number = Some(5); - let some_char = Some('e'); - - let absent_number: Option<i32> = None; -}
Вид some_number
- Option<i32>
. Вид some_char
- Option<char>
, это другой вид. Ржавчина может вывести эти виды, потому что мы указали значение внутри исхода Some
. Для absent_number
Ржавчина требует, чтобы мы определяли общий вид для Option
: сборщик не может вывести вид, который будет в Some
, глядя только на значение None
. Здесь мы сообщаем Rust, что absent_number
должен иметь вид Option<i32>
.
Когда есть значение Some
, мы знаем, что значение присутствует и содержится внутри Some
. Когда есть значение None
, это означает то же самое, что и null в некотором смысле: у нас нет действительного значения. Так почему наличие Option<T>
лучше, чем null?
Вкратце, поскольку Option<T>
и T
(где T
может быть любым видом) относятся к разным видам, сборщик не позволит нам использовать значение Option<T>
даже если бы оно было определённо допустимым значением. Например, этот код не будет собираться, потому что он пытается добавить i8
к значению вида Option<i8>
:
fn main() {
- let x: i8 = 5;
- let y: Option<i8> = Some(5);
-
- let sum = x + y;
-}
-Если мы запустим этот код, то получим такое сообщение об ошибке:
-$ cargo run
- Compiling enums v0.1.0 (file:///projects/enums)
-error[E0277]: cannot add `Option<i8>` to `i8`
- --> src/main.rs:5:17
- |
-5 | let sum = x + y;
- | ^ no implementation for `i8 + Option<i8>`
- |
- = help: the trait `Add<Option<i8>>` is not implemented for `i8`
- = help: the following other types implement trait `Add<Rhs>`:
- <&'a i8 as Add<i8>>
- <&i8 as Add<&i8>>
- <i8 as Add<&i8>>
- <i8 as Add>
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `enums` (bin "enums") due to 1 previous error
-
-Сильно! В действительности, это сообщение об ошибке означает, что Ржавчина не понимает, как сложить i8
и Option<i8>
, потому что это разные виды. Когда у нас есть значение вида наподобие i8
, сборщик заверяет, что у нас всегда есть допустимое значение вида. Мы можем уверенно продолжать работу, не проверяя его на null перед использованием. Однако, когда у нас есть значение вида Option<T>
(где T
- это любое значение любого вида T
, упакованное в Option
, например значение вида i8
или String
), мы должны беспокоиться о том, что значение вида T возможно не имеет значения (является исходом None
), и сборщик позаботится о том, чтобы мы обработали такой случай, прежде чем мы бы попытались использовать None
значение.
Другими словами, вы должны преобразовать Option<T>
в T
прежде чем вы сможете выполнять действия с этим T
. Как правило, это помогает выявить одну из наиболее распространённых неполадок с null: предполагая, что что-то не равно null, когда оно на самом деле равно null.
Устранение риска ошибочного предположения касательно не-null значения помогает вам быть более уверенным в своём коде. Чтобы иметь значение, которое может быть null, вы должны явно описать вид этого значения с помощью Option<T>
. Затем, когда вы используете это значение, вы обязаны явно обрабатывать случай, когда значение равно null. Везде, где значение имеет вид, отличный от Option<T>
, вы можете смело рассчитывать на то, что значение не равно null. Это продуманное расчетное решение в Rust, ограничивающее распространение null и увеличивающее безопасность кода на Rust.
Итак, как же получить значение T
из исхода Some
, если у вас на руках есть только предмет Option<T>
, и как можно его, вообще, использовать? Перечисление Option<T>
имеет большое количество способов, полезных в различных случаейх; вы можете ознакомиться с ними в его документации. Знакомство с способами перечисления Option<T>
будет чрезвычайно полезным в вашем путешествии с Rust.
В общем случае, чтобы использовать значение Option<T>
, нужен код, который будет обрабатывать все исходы перечисления Option<T>
. Вам понадобится некоторый код, который будет работать только тогда, когда у вас есть значение Some(T)
, и этому коду разрешено использовать внутри T
. Также вам понадобится другой код, который будет работать, если у вас есть значение None
, и у этого кода не будет доступного значения T
. Выражение match
— это устройство управления потоком выполнения программы, которая делает именно это при работе с перечислениями: она запускает разный код в зависимости от того, какой исход перечисления имеется, и этот код может использовать данные, находящиеся внутри совпавшего исхода.
match
В Ржавчина есть чрезвычайно мощный рычаг управления потоком, именуемый match
, который позволяет сравнивать значение с различными образцами и затем выполнять код в зависимости от того, какой из образцов совпал. Образцы могут состоять из записанных значений, имён переменных, подстановочных знаков и многого другого; в главе 18 рассматриваются все различные виды образцов и то, что они делают. Сила match
заключается в выразительности образцов и в том, что сборщик проверяет, что все возможные случаи обработаны.
Думайте о выражении match
как о машине для сортировки монет: монеты скользят по дорожке с различными по размеру отверстиями, и каждая монета падает через первое попавшееся отверстие, в которое она поместилась. Таким же образом значения проходят через каждый образец в match
, и при первом же "подходящем" образце значение попадает в соответствующий раздел кода, который будет использоваться во время выполнения.
Говоря о монетах, давайте используем их в качестве примера, используя match
! Для этого мы напишем функцию, которая будет получать на вход неизвестную монету Соединённых Штатов и, подобно счётной машине, определять, какая это монета, и возвращать её стоимость в центах, как показано в приложении 6-3.
-enum Coin { - Penny, - Nickel, - Dime, - Quarter, -} - -fn value_in_cents(coin: Coin) -> u8 { - match coin { - Coin::Penny => 1, - Coin::Nickel => 5, - Coin::Dime => 10, - Coin::Quarter => 25, - } -} - -fn main() {}
-
Давайте разберём match
в функции value_in_cents
. Сначала пишется ключевое слово match
, затем следует выражение, которое в данном случае является значением coin
. Это выглядит очень похоже на условное выражение, используемое в if
, но есть большая разница: с if
выражение должно возвращать булево значение, а здесь это может быть любой вид. Вид coin
в этом примере — перечисление вида Coin
, объявленное в строке 1.
Далее идут ветки match
. Ветки состоят из двух частей: образец и некоторый код. Здесь первая ветка имеет образец, который является значением Coin::Penny
, затем идёт оператор =>
, который разделяет образец и код для выполнения. Код в этом случае - это просто значение 1
. Каждая ветка отделяется от последующей при помощи запятой.
Когда выполняется выражение match
, оно сравнивает полученное значение с образцом каждого ответвления по порядку. Если образец совпадает со значением, то выполняется код, связанный с этим образцом. Если этот образец не соответствует значению, то выполнение продолжается со следующей ветки, так же, как в автомате по сортировке монет. У нас может быть столько ответвлений, сколько нужно: в приложении 6-3 наш match
состоит из четырёх ответвлений.
Код, связанный с каждым ответвлением, является выражением, а полученное значение выражения в соответствующем ответвлении — это значение, которое возвращается для всего выражения match
.
Обычно фигурные скобки не используются, если код совпадающей ветви невелик, как в приложении 6-3, где каждая ветвь просто возвращает значение. Если вы хотите выполнить несколько строк кода в одной ветви, вы должны использовать фигурные скобки, а запятая после этой ветви необязательна. Например, следующий код печатает "Lucky penny!" каждый раз, когда способ вызывается с Coin::Penny
, но при этом он возвращает последнее значение раздела - 1
:
-enum Coin { - Penny, - Nickel, - Dime, - Quarter, -} - -fn value_in_cents(coin: Coin) -> u8 { - match coin { - Coin::Penny => { - println!("Lucky penny!"); - 1 - } - Coin::Nickel => 5, - Coin::Dime => 10, - Coin::Quarter => 25, - } -} - -fn main() {}
Есть ещё одно полезное качество у веток в выражении match
: они могут привязываться к частям тех значений, которые совпали с образцом. Благодаря этому можно извлекать значения из исходов перечисления.
В качестве примера, давайте изменим один из исходов перечисления так, чтобы он хранил в себе данные. С 1999 по 2008 год Соединённые Штаты чеканили 25 центов с различным внешнем видом на одной стороне для каждого из 50 штатов. Ни одна другая монета не получила внешнего видаштата, только четверть доллара имела эту дополнительную особенность. Мы можем добавить эту сведения в наш enum
путём изменения исхода Quarter
и включить в него значение UsState
, как сделано в приложении 6-4.
-#[derive(Debug)] // so we can inspect the state in a minute -enum UsState { - Alabama, - Alaska, - // --snip-- -} - -enum Coin { - Penny, - Nickel, - Dime, - Quarter(UsState), -} - -fn main() {}
-
Представьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по виду, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою собрание.
-В выражении match для этого кода мы добавляем переменную с именем state
в образец, который соответствует значениям исхода Coin::Quarter
. Когда Coin::Quarter
совпадёт с образцом, переменная state
будет привязана к значению штата этого четвертака. Затем мы сможем использовать state
в коде этой ветки, вот так:
-#[derive(Debug)] -enum UsState { - Alabama, - Alaska, - // --snip-- -} - -enum Coin { - Penny, - Nickel, - Dime, - Quarter(UsState), -} - -fn value_in_cents(coin: Coin) -> u8 { - match coin { - Coin::Penny => 1, - Coin::Nickel => 5, - Coin::Dime => 10, - Coin::Quarter(state) => { - println!("State quarter from {state:?}!"); - 25 - } - } -} - -fn main() { - value_in_cents(Coin::Quarter(UsState::Alaska)); -}
Если мы сделаем вызов функции value_in_cents(Coin::Quarter(UsState::Alaska))
, то coin
будет иметь значение Coin::Quarter(UsState::Alaska)
. Когда мы будем сравнивать это значение с каждой из веток, ни одна из них не будет совпадать, пока мы не достигнем исхода Coin::Quarter(state)
. В этот мгновение state
привяжется к значению UsState::Alaska
. Затем мы сможем использовать эту привязку в выражении println!
, получив таким образом внутреннее значение исхода Quarter
перечисления Coin
.
Option<T>
В предыдущем разделе мы хотели получить внутреннее значение T
для случая Some
при использовании Option<T>
; мы можем обработать вид Option<T>
используя match
, как уже делали с перечислением Coin
! Вместо сравнения монет мы будем сравнивать исходы Option<T>
, независимо от этого изменения рычаг работы выражения match
останется прежним.
Допустим, мы хотим написать функцию, которая принимает Option<i32>
и если есть значение внутри, то добавляет 1 к существующему значению. Если значения нет, то функция должна возвращать значение None
и не пытаться выполнить какие-либо действия.
Такую функцию довольно легко написать благодаря выражению match
, код будет выглядеть как в приложении 6-5.
-fn main() { - fn plus_one(x: Option<i32>) -> Option<i32> { - match x { - None => None, - Some(i) => Some(i + 1), - } - } - - let five = Some(5); - let six = plus_one(five); - let none = plus_one(None); -}
-
Давайте более подробно рассмотрим первое выполнение plus_one
. Когда мы вызываем plus_one(five)
, переменная x
в теле plus_one
будет иметь значение Some(5)
. Затем мы сравниваем это значение с каждой ветвью сопоставления:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-Значение Some(5)
не соответствует образцу None
, поэтому мы продолжаем со следующим ответвлением:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-Совпадает ли Some(5)
с образцом Some(i)
? Да, это так! У нас такой же исход. Тогда переменная i
привязывается к значению, содержащемуся внутри Some
, поэтому i
получает значение 5
. Затем выполняется код сопряженный для данного ответвления, поэтому мы добавляем 1 к значению i
и создаём новое значение Some
со значением 6
внутри.
Теперь давайте рассмотрим второй вызов plus_one
в приложении 6-5, где x
является None
. Мы входим в выражение match
и сравниваем значение с первым ответвлением:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- None => None,
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-Оно совпадает! Для данной ветки образец (None) не подразумевает наличие какого-то значения к которому можно было бы что-то добавить, поэтому программа останавливается и возвращает значение которое находится справа от =>
- т.е. None
. Так как образец первой ветки совпал, то никакие другие образцы веток не сравниваются.
Соединение match
и перечислений полезно во многих случаейх. Вы часто будете видеть подобную сочетание в коде на Rust: сделать сопоставление значений перечисления используя match
, привязать переменную к данным внутри значения, выполнить код на основе привязанных данных. Сначала это может показаться немного сложным, но как только вы привыкнете, то захотите чтобы такая возможность была бы во всех языках. Это неизменно любимый пользователями приём.
Есть ещё один особенность match
, который мы должны обсудить: образцы должны покрывать все возможные исходы. Рассмотрим эту исполнение нашей функции plus_one
, которая содержит ошибку и не собирается:
fn main() {
- fn plus_one(x: Option<i32>) -> Option<i32> {
- match x {
- Some(i) => Some(i + 1),
- }
- }
-
- let five = Some(5);
- let six = plus_one(five);
- let none = plus_one(None);
-}
-Мы не обработали исход None
, поэтому этот код вызовет изъян в программе. К счастью, Ржавчина знает и умеет ловить такой случай. Если мы попытаемся собрать такой код, мы получим ошибку сборки:
$ cargo run
- Compiling enums v0.1.0 (file:///projects/enums)
-error[E0004]: non-exhaustive patterns: `None` not covered
- --> src/main.rs:3:15
- |
-3 | match x {
- | ^ pattern `None` not covered
- |
-note: `Option<i32>` defined here
- --> /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:571:1
- ::: /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/core/src/option.rs:575:5
- |
- = note: not covered
- = note: the matched value is of type `Option<i32>`
-help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
- |
-4 ~ Some(i) => Some(i + 1),
-5 ~ None => todo!(),
- |
-
-For more information about this error, try `rustc --explain E0004`.
-error: could not compile `enums` (bin "enums") due to 1 previous error
-
-Rust знает, что мы не описали все возможные случаи, и даже знает, какой именно из образцов мы упуисполнения! Сопоставления в Ржавчина являются исчерпывающими: мы должны покрыть все возможные исходы, чтобы код был правильным. Особенно в случае Option<T>
, когда Ржавчина не даёт нам забыть обработать явным образом значение None
, тем самым он защищает нас от предположения, что у нас есть значение, в то время как у нас может быть и null, что делает невозможным совершить ошибку на миллиард долларов, о которой говорилось ранее.
_
Используя перечисления, мы также можем выполнять особые действия для нескольких определённых значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы выполняем игру, в которой при выпадении 3 игрок не двигается, а получает новую нового образца шляпу. Если выпадает 7, игрок теряет шляпу. При всех остальных значениях ваш игрок перемещается на столько-то мест на игровом поле. Вот match
, выполняющий эту логику, в котором итог броска костей жёстко закодирован, а не является случайным значением, а вся остальная логика представлена функциями без тел, поскольку их выполнение не входит в рамки данного примера:
-fn main() { - let dice_roll = 9; - match dice_roll { - 3 => add_fancy_hat(), - 7 => remove_fancy_hat(), - other => move_player(other), - } - - fn add_fancy_hat() {} - fn remove_fancy_hat() {} - fn move_player(num_spaces: u8) {} -}
Для первых двух веток образцами являются записанные значения 3 и 7. Для последней ветки, которая охватывает все остальные возможные значения, образцом является переменная, которую мы решили назвать other
. Код, выполняемый для ветки other
, использует эту переменную, передавая её в функцию move_player
.
Этот код собирается, даже если мы не перечислили все возможные значения u8
, потому что последний образец будет соответствовать всем значениям, не указанным в определенном списке. Этот гибкий образец удовлетворяет требованию, что соответствие должно быть исчерпывающим. Обратите внимание, что мы должны поместить ветку с гибким образцом последней, потому что образцы оцениваются по порядку. Ржавчина предупредит нас, если мы добавим ветки после гибкого образца, потому что эти последующие ветки никогда не будут выполняться!
В Ржавчина также есть образец, который можно использовать, когда мы не хотим использовать значение в гибком образце: _
, который является особым образцом, который соответствует любому значению и не привязывается к этому значению. Это говорит Rust, что мы не собираемся использовать это значение, поэтому Ржавчина не будет предупреждать нас о неиспользуемой переменной.
Давайте изменим правила игры так: если выпадает что-то, кроме 3 или 7, нужно бросить ещё раз. Нам не нужно использовать значение в этом случае, поэтому мы можем изменить наш код, чтобы использовать _
вместо переменной с именем other
:
-fn main() { - let dice_roll = 9; - match dice_roll { - 3 => add_fancy_hat(), - 7 => remove_fancy_hat(), - _ => reroll(), - } - - fn add_fancy_hat() {} - fn remove_fancy_hat() {} - fn reroll() {} -}
Этот пример также удовлетворяет требованию исчерпывающей полноты, поскольку мы явно пренебрегаем все остальные значения в последней ветке; мы ничего не забыли.
-Если мы изменим правила игры ещё раз, чтобы в ваш ход не происходило ничего другого, если вы бросаете не 3 или 7, мы можем выразить это, используя единичное значение (пустой вид упорядоченного ряда, о котором мы упоминали в разделе "Упорядоченные ряды") в качестве кода, который идёт вместе с веткой _
:
-fn main() { - let dice_roll = 9; - match dice_roll { - 3 => add_fancy_hat(), - 7 => remove_fancy_hat(), - _ => (), - } - - fn add_fancy_hat() {} - fn remove_fancy_hat() {} -}
Здесь мы явно говорим Rust, что не собираемся использовать никакое другое значение, которое не соответствует образцам в предыдущих ветках, и не хотим запускать никакой код в этом случае.
-Подробнее о образцах и совпадениях мы поговорим в Главе 18. Пока же мы перейдём к правилам написания if let
, который может быть полезен в случаейх, когда выражение match
слишком многословно.
if let
правила написания if let
позволяет ссоединенять if
и let
в менее многословную устройство, и затем обработать значения соответствующе только одному образцу, одновременно пренебрегая все остальные. Рассмотрим программу в приложении 6-6, которая обрабатывает сопоставление значения Option<u8>
в переменной config_max
, но хочет выполнить код только в том случае, если значение является исходом Some
.
-fn main() { - let config_max = Some(3u8); - match config_max { - Some(max) => println!("The maximum is configured to be {max}"), - _ => (), - } -}
-
Если значение равно Some
, мы распечатываем значение в исходе Some
, привязывая значение к переменной max
в образце. Мы не хотим ничего делать со значением None
. Чтобы удовлетворить выражение match
, мы должны добавить _ => ()
после обработки первой и единственной ветки, и добавление образцового кода раздражает.
Вместо этого, мы могли бы написать это более коротким способом, используя if let
. Следующий код ведёт себя так же, как выражение match
в приложении 6-6:
-fn main() { - let config_max = Some(3u8); - if let Some(max) = config_max { - println!("The maximum is configured to be {max}"); - } -}
правила написания if let
принимает образец и выражение, разделённые знаком равенства. Он работает так же, как match
, когда в него на вход передадут выражение и подходящим образцом для этого выражения окажется первая ветка. В данном случае образцом является Some(max)
, где max
привязывается к значению внутри Some
. Затем мы можем использовать max
в теле раздела if let
так же, как мы использовали max
в соответствующей ветке match
. Код в разделе if let
не запускается, если значение не соответствует образцу.
Используя if let
мы меньше печатаем, меньше делаем отступов и меньше получаем образцового кода. Тем не менее, мы теряем полную проверку всех исходов, предоставляемую выражением match
. Выбор между match
и if let
зависит от того, что вы делаете в вашем определенном случае и является ли получение краткости при потере полноты проверки подходящим соглашением.
Другими словами, вы можете думать о устройства if let
как о синтаксическом сахаре для match
, который выполнит код если входное значение будет соответствовать единственному образцу, и пренебрегает все остальные значения.
Можно добавлять else
к if let
. Разделкода, который находится внутри else
подобен по смыслу блоку кода ветки связанной с образцом _
выражения match
(которое эквивалентно сборной устройства if let
и else
). Вспомним объявление перечисления Coin
в приложении 6-4, где исход Quarter
также содержит внутри значение штата вида UsState
. Если бы мы хотели посчитать все монеты не являющиеся четвертями, а для четвертей печатать название штата, то мы могли бы сделать это с помощью выражения match
таким образом:
-#[derive(Debug)] -enum UsState { - Alabama, - Alaska, - // --snip-- -} - -enum Coin { - Penny, - Nickel, - Dime, - Quarter(UsState), -} - -fn main() { - let coin = Coin::Penny; - let mut count = 0; - match coin { - Coin::Quarter(state) => println!("State quarter from {state:?}!"), - _ => count += 1, - } -}
Или мы могли бы использовать выражение if let
и else
так:
-#[derive(Debug)] -enum UsState { - Alabama, - Alaska, - // --snip-- -} - -enum Coin { - Penny, - Nickel, - Dime, - Quarter(UsState), -} - -fn main() { - let coin = Coin::Penny; - let mut count = 0; - if let Coin::Quarter(state) = coin { - println!("State quarter from {state:?}!"); - } else { - count += 1; - } -}
Если у вас есть случаей в которой ваша программа имеет логику которая слишком многословна для того чтобы её выражать используя match
, помните, о том, что также в вашем наборе средств Ржавчина есть if let
.
Мы рассмотрели как использовать перечисления для создания пользовательских видов, которые могут быть одним из наборов перечисляемых значений. Мы показали, как вид Option<T>
из встроенной библиотеки помогает использовать систему видов для предотвращения ошибок. А когда значения перечисления имеют данные внутри них, можно использовать match
или if let
, чтобы извлечь и пользоваться значением, в зависимости от того, сколько случаев нужно обработать.
Теперь ваши программы на Ржавчина могут выражать подходы вашей предметной области, используя устройства и перечисления. Создание и использование пользовательских видов в API обеспечивает типобезопасность: сборщик позаботится о том, чтобы функции получали значения только того вида, который они ожидают.
-Чтобы предоставить вашим пользователям хорошо согласованный API, который прост в использовании и предоставляет только то, что нужно вашим пользователям, надо поговорить о звенах в Rust.
- -По мере роста кодовой хранилища ваших программ, создание дела будет иметь большое значение, ведь отслеживание всей программы в голове будет становиться всё более сложным. Объединенияя связанные функции и разделяя код по основным возможностям (фичам, feature), вы делаете более прозрачным понимание о том, где искать код выполняющий определённую функцию и где стоит вносить изменения для того чтобы изменить её поведение.
-Программы, которые мы писали до сих пор, были в одном файле одного звена. По мере роста дела, мы можем создавать код иначе, разделив его на несколько звеньев и несколько файлов. Дополнение может содержать несколько двоичных ящиков и дополнительно один ящик библиотеки. Дополнение может включать в себя много двоичных ящиков и дополнительно один библиотечный ящик. По мере роста дополнения вы можете извлекать части программы в отдельные ящики, которые затем станут внешними зависимостями для основного кода нашей программы. Эта глава охватывает все эти техники. В свою очередь для очень крупных дел, состоящих из набора взаимосвязанных дополнений развивающихся вместе, Cargo предоставляет рабочие пространства, workspaces, их мы рассмотрим за пределами данной главы, в разделе "Рабочие пространства Cargo" Главы 14.
-Мы также обсудим инкапсуляцию подробностей, которая позволяет использовать код снова на более высоком уровне: единожды выполнив какую-то действие, другой код может вызывать этот код через открытый внешняя оболочка, не зная как работает выполнение. То, как вы пишете код, определяет какие части общедоступны для использования другим кодом и какие части являются закрытыми деталями выполнения для которых вы оставляете право на изменения только за собой. Это ещё один способ ограничить количество подробностей, которые вы должны держать в голове.
-Связанное понятие - это область видимости: вложенный среда в котором написан код имеющий набор имён, которые определены «в текущей области видимости». При чтении, письме и сборки кода, программистам и сборщикам необходимо знать, относится ли определенное имя в определённом месте к переменной, к функции, к устройстве, к перечислению, к звену, к постоянных значенийе или другому элементу и что означает этот элемент. Можно создавать области видимости и изменять какие имена входят или выходят за их рамки. Нельзя иметь два элемента с тем же именем в одной области; есть доступные средства для разрешения несоответствий имён.
-Rust имеет ряд функций, которые позволяют управлять согласованием кода, в том числе управлять тем какие подробности открыты, какие подробности являются частными, какие имена есть в каждой области вашей программы. Эти функции иногда вместе именуемые состоящей из звеньев системой включают в себя:
-В этой главе мы рассмотрим все эти функции, обсудим как они взаимодействуют и объясним, как использовать их для управления областью видимости. К концу у вас должно появиться солидное понимание состоящей из звеньев системы и умение работать с областями видимости на уровне искуссника!
- -Первые части состоящей из звеньев системы, которые мы рассмотрим — это дополнения и ящики.
-Ящик — это наименьший размер кода, который сборщик Ржавчина рассматривает за раз. Даже если вы запустите rustc
вместо cargo
и передадите один файл с исходным кодом (как мы уже делали в разделе «Написание и запуск программы на Rust» Главы 1), сборщик считает этот файл ящиком. Ящики могут содержать звенья, и звенья могут быть определены в других файлах, которые собираются вместе с ящиком, как мы увидим в следующих разделах.
Ящик может быть одним из двух видов: двоичный ящик или библиотечный ящик. Бинарные ящики — это программы, которые вы можете собрать в исполняемые файлы, которые вы можете запускать, например программу приказной строки или сервер. У каждого двоичного ящика должна быть функция с именем main
, которая определяет, что происходит при запуске исполняемого файла. Все ящики, которые мы создали до сих пор, были двоичными ящиками.
Библиотечные ящики не имеют функции main
и не собираются в исполняемый файл. Вместо этого они определяют возможность, предназначенную для совместного использования другими делами. Например, ящик rand
, который мы использовали в Главе 2 обеспечивает возможность, которая порождает случайные числа. В большинстве случаев, когда Rustaceans говорят «ящик», они имеют в виду библиотечный ящик, и они используют «ящик» взаимозаменяемо с общей подходом программирования «библиотека».
Корневой звено ящика — это исходный файл, из которого сборщик Ржавчина начинает собирать корневой звено вашего ящика (мы подробно объясним звенья в разделе «Определение звеньев для управления видимости и закрытости»).
-Дополнение — это набор из одного или нескольких ящиков, предоставляющий набор возможности. Дополнение содержит файл Cargo.toml, в котором описывается, как собирать эти ящики. На самом деле Cargo — это дополнение, содержащий двоичный ящик для средства приказной строки, который вы использовали для создания своего кода. Дополнение Cargo также содержит библиотечный ящик, от которого зависит двоичный ящик. Другие дела тоже могут зависеть от библиотечного ящика Cargo, чтобы использовать ту же логику, что и средство приказной строки Cargo.
-Дополнение может содержать сколько угодно двоичных ящиков, но не более одного библиотечного ящика. Дополнение должен содержать хотя бы один ящик, библиотечный или двоичный.
-Давайте пройдёмся по тому, что происходит, когда мы создаём дополнение. Сначала введём приказ cargo new
:
$ cargo new my-project
- Created binary (application) `my-project` package
-$ ls my-project
-Cargo.toml
-src
-$ ls my-project/src
-main.rs
-
-После того, как мы запустили cargo new
, мы используем ls
, чтобы увидеть, что создал Cargo. В папке дела есть файл Cargo.toml, дающий нам дополнение. Также есть папка src, содержащий main.rs. Откройте Cargo.toml в текстовом редакторе и обратите внимание, что в нём нет упоминаний о src/main.rs. Cargo следует соглашению о том, что src/main.rs — это корневой звено двоичного ящика с тем же именем, что и у дополнения. Точно так же Cargo знает, что если папка дополнения содержит src/lib.rs, дополнение содержит библиотечный ящик с тем же именем, что и дополнение, а src/lib.rs является корневым звеном этого ящика. Cargo передаёт файлы корневого звена ящика в rustc
для сборки библиотечного или двоичного ящика.
Здесь у нас есть дополнение, который содержит только src/main.rs, что означает, что он содержит только двоичный ящик с именем my-project
. Если дополнение содержит src/main.rs и src/lib.rs, он имеет два ящика: двоичный и библиотечный, оба с тем же именем, что и дополнение. Дополнение может иметь несколько двоичных ящиков, помещая их файлы в папка src/bin: каждый файл будет отдельным двоичным ящиком.
В этом разделе мы поговорим о звенах и других частях системы звеньев, а именно: путях (paths), которые позволяют именовать элементы; ключевом слове use
, которое приносит путь в область видимости; ключевом слове pub
, которое делает элементы общедоступными. Мы также обсудим ключевое слово as
, внешние дополнения и оператор glob. А пока давайте сосредоточимся на звенах!
Во-первых, мы начнём со списка правил, чтобы вам было легче понять при согласования кода в будущем. Затем мы подробно объясним каждое из правил.
-Здесь мы даём краткий обзор того, как звенья, пути, ключевое слово use
и ключевое слово pub
работают в сборщике и как большинство разработчиков согласуют свой код. В этой главе мы рассмотрим примеры каждого из этих правил, и это удобный мгновение чтобы напомнить о том, как работают звенья.
mod garden;
. Сборщик будет искать код звена в следующих местах:
-mod garden
mod vegetables;
в src/garden.rs. Сборщик будет искать код подзвена в папке с именем родительского звена в следующих местах:
-mod vegetables
, между фигурных скобок, которые заменяют точку с запятойAsparagus
, в подзвене vegetables звена garden, будет найден по пути crate::garden::vegetables::Asparagus
.pub mod
вместо mod
. Чтобы сделать элементы общедоступного звена тоже общедоступными, используйте pub
перед их объявлением.use
: Внутри области видимости использование ключевого слова use
создаёт псевдонимы для элементов, чтобы уменьшить повторение длинных путей. В любой области видимости, в которой может обращаться к crate::garden::vegetables::Asparagus
, вы можете создать псевдоним use crate::garden::vegetables::Asparagus;
и после этого вам нужно просто писать Asparagus
, чтобы использовать этот вид в этой области видимости.Мы создали двоичный ящик backyard
, который отображает эти правила. Директория ящика, также названная как backyard
, содержит следующие файлы и папки:
backyard
-├── Cargo.lock
-├── Cargo.toml
-└── src
- ├── garden
- │ └── vegetables.rs
- ├── garden.rs
- └── main.rs
-
-Файл корневого звена ящика в нашем случае src/main.rs, и его содержимое:
-Файл: src/main.rs
-use crate::garden::vegetables::Asparagus;
-
-pub mod garden;
-
-fn main() {
- let plant = Asparagus {};
- println!("I'm growing {plant:?}!");
-}
-Строка pub mod garden;
говорит сборщику о подключении кода, найденном в src/garden.rs:
Файл: src/garden.rs
-pub mod vegetables;
-А здесь pub mod vegetables;
указывает на подключаемый код в src/garden/vegetables.rs. Этот код:
#[derive(Debug)]
-pub struct Asparagus {}
-Теперь давайте рассмотрим подробности этих правил и отобразим их в действии!
-Звенья позволяют упорядочивать код внутри ящика для удобочитаемости и лёгкого повторного использования. Звенья также позволяют нам управлять закрытостью элементов, поскольку код внутри звена по умолчанию является закрытым. Частные элементы — это внутренние подробности выполнения, недоступные для внешнего использования. Мы можем сделать звенья и элементы внутри них общедоступными, что позволит внешнему коду использовать их и зависеть от них.
-В качестве примера, давайте напишем библиотечный ящик предоставляющий возможность ресторана. Мы определим ярлыки функций, но оставим их тела пустыми, чтобы сосредоточиться на согласования кода, вместо выполнения кода для ресторана.
-В ресторанной индустрии некоторые части ресторана называются фронтом дома, а другие задней частью дома. Фронт дома это там где находятся клиенты; здесь размещаются места клиентов, официанты принимают заказы и оплаты, а бармены делают напитки. Задняя часть дома это где шеф-повара и повара работают на кухне, работают посудомоечные машины, а управленцы занимаются административной деятельностью.
-Чтобы внутренне выстроить
-ящик подобно тому, как работает настоящий ресторан, можно согласовать размещение функций во вложенных звенах. Создадим новую библиотеку (библиотечный ящик) с именем restaurant
выполнив приказ cargo new restaurant --lib
; затем вставим код из приложения 7-1 в src/lib.rs для определения некоторых звеньев и ярлыков функций. Это раздел фронта дома:
Файл: src/lib.rs
-mod front_of_house {
- mod hosting {
- fn add_to_waitlist() {}
-
- fn seat_at_table() {}
- }
-
- mod serving {
- fn take_order() {}
-
- fn serve_order() {}
-
- fn take_payment() {}
- }
-}
--
Мы определяем звено, начиная с ключевого слова mod
, затем определяем название звена (в данном случае front_of_house
) и размещаем фигурные скобки вокруг тела звена. Внутри звеньев, можно иметь другие звенья, как в случае с звенами hosting
и serving
. Звенья также могут содержать определения для других элементов, таких как устройства, перечисления, постоянные значения, особенности или — как в приложении 7-1 — функции.
Используя звенья, мы можем собъединять связанные определения вместе и сказать почему они являются связанными. Программистам будет легче найти необходимую возможность в объединенном коде, вместо того чтобы искать её в одном общем списке. Программисты, добавляющие новые функции в этот код, будут знать, где разместить код для поддержания порядка в программе.
-Как мы упоминали ранее, файлы src/main.rs и src/lib.rs называются корневыми звенами ящика. Причина такого именования в том, что содержимое любого из этих двух файлов образует звено с именем crate
в корне устройства звеньев ящика, известной как дерево звеньев.
В приложении 7-2 показано дерево звеньев для устройства звеньев, приведённой в коде приложения 7-1.
-crate
- └── front_of_house
- ├── hosting
- │ ├── add_to_waitlist
- │ └── seat_at_table
- └── serving
- ├── take_order
- ├── serve_order
- └── take_payment
-
--
Это дерево показывает, как некоторые из звеньев вкладываются друг в друга; например, hosting
находится внутри front_of_house
. Дерево также показывает, что некоторые звенья являются братьями (siblings) друг для друга, то есть они определены в одном звене; hosting
и serving
это братья которые определены внутри front_of_house
. Если звено A содержится внутри звена B, мы говорим, что звено A является потомком (child) звена B, а звено B является родителем (parent) звена A. Обратите внимание, что родителем всего дерева звеньев является неявный звено с именем crate
.
Дерево звеньев может напомнить вам дерево папок файловой системы на компьютере; это очень удачное сравнение! По подобию с папкими в файловой системе, мы используется звенья для согласования кода. И так же, как нам надо искать файлы в папких на компьютере, нам требуется способ поиска нужных звеньев.
- -Чтобы показать Rust, где найти элемент в дереве звеньев, мы используем путь так же, как мы используем путь при навигации по файловой системе. Чтобы вызвать функцию, нам нужно знать её путь.
-Пути бывают двух видов:
-crate
.self
, super
или определитель в текущем звене.Как абсолютные, так и относительные, пути состоят из одного или нескольких определителей, разделённых двойными двоеточиями (::
).
Вернёмся к приложению 7-1, скажем, мы хотим вызвать функцию add_to_waitlist
. Это то же самое, что спросить: какой путь у функции add_to_waitlist
? В приложении 7-3 мы немного упроисполнения код приложения 7-1, удалив некоторые звенья и функции.
Мы покажем два способа вызова функции add_to_waitlist
из новой функции eat_at_restaurant
, определённой в корневом звене ящика. Эти пути правильные, но остаётся ещё одна неполадка, которая не позволит этому примеру собраться как есть. Мы скоро объясним почему.
Функция eat_at_restaurant
является частью общедоступного API нашего библиотечного ящика, поэтому мы помечаем её ключевым словом pub
. В разделе "Раскрываем закрытые пути с помощью ключевого слова pub
" мы рассмотрим более подробно pub
.
Файл: src/lib.rs
-mod front_of_house {
- mod hosting {
- fn add_to_waitlist() {}
- }
-}
-
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
--
При первом вызове функции add_to_waitlist
из eat_at_restaurant
мы используем абсолютный путь. Функция add_to_waitlist
определена в том же ящике, что и eat_at_restaurant
, и это означает, что мы можем использовать ключевое слово crate
в начале абсолютного пути. Затем мы добавляем каждый из последующих дочерних звеньев, пока не составим путь до add_to_waitlist
. Вы можете представить себе файловую систему с такой же устройством: мы указываем путь /front_of_house/hosting/add_to_waitlist
для запуска программы add_to_waitlist
; использование имени crate
в качестве корневого звена ящика подобно использованию /
для указания корня файловой системы в вашей оболочке.
Второй раз, когда мы вызываем add_to_waitlist
из eat_at_restaurant
, мы используем относительный путь. Путь начинается с имени звена front_of_house
, определённого на том же уровне дерева звеньев, что и eat_at_restaurant
. Для эквивалентной файловой системы использовался бы путь front_of_house/hosting/add_to_waitlist
. Начало пути с имени звена означает, что путь является относительным.
Выбор, использовать относительный или абсолютный путь, является решением, которое вы примете на основании вашего дела. Решение должно зависеть от того, с какой вероятностью вы переместите объявление элемента отдельно от или вместе с кодом использующим этот элемент. Например, в случае перемещения звена front_of_house
и его функции eat_at_restaurant
в другой звено с именем customer_experience
, будет необходимо обновить абсолютный путь до add_to_waitlist
, но относительный путь всё равно будет действителен. Однако, если мы переместим отдельно функцию eat_at_restaurant
в звено с именем dining
, то абсолютный путь вызова add_to_waitlist
останется прежним, а относительный путь нужно будет обновить. Мы предпочитаем указывать абсолютные пути, потому что это позволяет проще перемещать определения кода и вызовы элементов независимо друг от друга.
Давайте попробуем собрать код из приложения 7-3 и выяснить, почему он ещё не собирается. Ошибка, которую мы получаем, показана в приложении 7-4.
-$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0603]: module `hosting` is private
- --> src/lib.rs:9:28
- |
-9 | crate::front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
- | |
- | private module
- |
-note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
-2 | mod hosting {
- | ^^^^^^^^^^^
-
-error[E0603]: module `hosting` is private
- --> src/lib.rs:12:21
- |
-12 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
- | |
- | private module
- |
-note: the module `hosting` is defined here
- --> src/lib.rs:2:5
- |
-2 | mod hosting {
- | ^^^^^^^^^^^
-
-For more information about this error, try `rustc --explain E0603`.
-error: could not compile `restaurant` (lib) due to 2 previous errors
-
--
Сообщения об ошибках говорят о том, что звено hosting
является закрытым. Другими словами, у нас есть правильные пути к звену hosting
и функции add_to_waitlist
, но Ржавчина не позволяет нам использовать их, потому что у него нет доступа к закрытым разделам. В Ржавчина все элементы (функции, способы, устройства, перечисления, звенья и постоянные значения) по умолчанию являются закрытыми для родительских звеньев. Если вы хотите сделать элемент, например функцию или устройство, закрытым, вы помещаете его в звено.
Элементы в родительском звене не могут использовать закрытые элементы внутри дочерних звеньев, но элементы в дочерних звенах могут использовать элементы у своих звенах-предках. Это связано с тем, что дочерние звенья оборачивают и скрывают подробности своей выполнения, но дочерние звенья могут видеть среда, в котором они определены. Продолжая нашу метафору, подумайте о правилах закрытости как о задней части ресторана: то, что там происходит, скрыто от клиентов ресторана, но офис-управленцы могут видеть и делать всё в ресторане, которым они управляют.
-В Ржавчина решили, что система звеньев должна исполняться таким образом, чтобы по умолчанию скрывать подробности выполнения. Таким образом, вы знаете, какие части внутреннего кода вы можете изменять не нарушая работы внешнего кода. Тем не менее, Ржавчина даёт нам возможность открывать внутренние части кода дочерних звеньев для внешних звеньев-предков, используя ключевое слово pub
, чтобы сделать элемент общедоступным.
pub
Давайте вернёмся к ошибке в приложении 7-4, которая говорит, что звено hosting
является закрытым. Мы хотим, чтобы функция eat_at_restaurant
из родительского звена имела доступ к функции add_to_waitlist
в дочернем звене, поэтому мы помечаем звено hosting
ключевым словом pub
, как показано в приложении 7-5.
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- fn add_to_waitlist() {}
- }
-}
-
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
--
К сожалению, код в приложении 7-5 всё ещё приводит к ошибке, как показано в приложении 7-6.
-$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0603]: function `add_to_waitlist` is private
- --> src/lib.rs:9:37
- |
-9 | crate::front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^^^^^^^^^ private function
- |
-note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
-3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
-error[E0603]: function `add_to_waitlist` is private
- --> src/lib.rs:12:30
- |
-12 | front_of_house::hosting::add_to_waitlist();
- | ^^^^^^^^^^^^^^^ private function
- |
-note: the function `add_to_waitlist` is defined here
- --> src/lib.rs:3:9
- |
-3 | fn add_to_waitlist() {}
- | ^^^^^^^^^^^^^^^^^^^^
-
-For more information about this error, try `rustc --explain E0603`.
-error: could not compile `restaurant` (lib) due to 2 previous errors
-
--
Что произошло? Добавление ключевого слова pub
перед mod hosting
сделало звено общедоступным. После этого изменения, если мы можем получить доступ к звену front_of_house
, то мы можем получить доступ к звену hosting
. Но содержимое звена hosting
всё ещё является закрытым: превращение звена в общедоступный звено не делает его содержимое общедоступным. Ключевое слово pub
позволяет внешнему коду в звенах-предках обращаться только к звену, без доступа ко внутреннему коду. Поскольку звенья являются дополнениями, мы мало что можем сделать, просто сделав звено общедоступным; нам нужно пойти дальше и сделать один или несколько элементов в звене общедоступными.
Ошибки в приложении 7-6 говорят, что функция add_to_waitlist
является закрытой. Правила закрытости применяются к устройствам, перечислениям, функциям и способам, также как и к звенам.
Давайте также сделаем функцию add_to_waitlist
общедоступной, добавив ключевое слово pub
перед её определением, как показано в приложении 7-7.
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-pub fn eat_at_restaurant() {
- // Absolute path
- crate::front_of_house::hosting::add_to_waitlist();
-
- // Relative path
- front_of_house::hosting::add_to_waitlist();
-}
--
Теперь код собирается! Чтобы понять, почему добавление ключевого слова pub
позволяет нам использовать эти пути для add_to_waitlist
в соответствии с правилами закрытости, давайте рассмотрим абсолютный и относительный пути.
В случае абсолютного пути мы начинаем с crate
, корня дерева звеньев нашего ящика. Звено front_of_house
определён в корневом звене ящика. Хотя front_of_house
не является общедоступным, но поскольку функция eat_at_restaurant
определена в том же звене, что и front_of_house
(то есть, eat_at_restaurant
и front_of_house
являются потомками одного родителя), мы можем ссылаться на front_of_house
из eat_at_restaurant
. Далее идёт звено hosting
, помеченный как pub
. Мы можем получить доступ к родительскому звену звена hosting
, поэтому мы можем получить доступ и к hosting
. Наконец, функция add_to_waitlist
помечена как pub
, и так как мы можем получить доступ к её родительскому звену, то вызов этой функции разрешён!
В случае относительного пути логика такая же как для абсолютного пути, за исключением первого шага: вместо того, чтобы начинать с корневого звена ящика, путь начинается с front_of_house
. Звено front_of_house
определён в том же звене, что и eat_at_restaurant
, поэтому относительный путь, начинающийся с звена, в котором определена eat_at_restaurant
тоже работает. Тогда, по причине того, что hosting
и add_to_waitlist
помечены как pub
, остальная часть пути работает и вызов этой функции разрешён!
Если вы собираетесь предоставить общий доступ к своему библиотечному ящику, чтобы другие дела могли использовать ваш код, ваш общедоступный API — это ваш договор с пользователями вашего ящика, определяющий, как они могут взаимодействовать с вашим кодом. Есть много соображений по поводу управления изменениями в вашем общедоступном API, чтобы сделать необременительным для людей зависимость от вашего ящика. Эти соображения выходят за рамки этой книги; если вам важна эта тема, см. The Ржавчина API Guidelines.
---Лучшие опытов для дополнений с двоичным и библиотечным ящиками
-Мы упоминали, что дополнение может содержать как корневой звено двоичного ящика src/main.rs, так и корневой звено библиотечного ящика src/lib.rs, и оба ящика будут по умолчанию иметь имя дополнения. Как правило, дополнения с таким образцом, содержащим как библиотечный, так и двоичный ящик, будут иметь достаточно кода в двоичном ящике, чтобы запустить исполняемый файл, который вызывает код из библиотечного ящика. Это позволяет другим делам извлечь выгоду из большей части возможности, предоставляемой дополнением, поскольку код библиотечного ящика можно использовать совместно.
-Дерево звеньев должно быть определено в src/lib.rs. Затем любые общедоступные элементы можно использовать в двоичном ящике, начав пути с имени дополнения. Двоичный ящик становится пользователем библиотечного ящика точно так же, как полностью внешний ящик использует библиотечный ящик: он может использовать только общедоступный API. Это поможет вам разработать хороший API; вы не только автор, но и пользователь!
-В Главе 12 мы эту опыт согласования кода с помощью окно выводаной программы, которая будет содержать как двоичный, так и библиотечный ящики.
-
super
Также можно построить относительные пути, которые начинаются в родительском звене, используя ключевое слово super
в начале пути. Это похоже на правила написания начала пути файловой системы ..
. Использование super
позволяет нам сослаться на элемент, который, как мы знаем, находится в родительском звене, что может упростить переупорядочение дерева звеньев, чем когда звено тесно связан с родителем, но родитель может когда-нибудь быть перемещён в другое место в дереве звеньев.
Рассмотрим код в приложении 7-8, где расчитывается случаей, в которой повар исправляет неправильный заказ и лично приносит его клиенту. Функция fix_incorrect_order
вызывает функцию deliver_order
, определённую в родительском звене, указывая путь к deliver_order
, начинающийся с super
:
Файл: src/lib.rs
-fn deliver_order() {}
-
-mod back_of_house {
- fn fix_incorrect_order() {
- cook_order();
- super::deliver_order();
- }
-
- fn cook_order() {}
-}
--
Функция fix_incorrect_order
находится в звене back_of_house
, поэтому мы можем использовать super
для перехода к родительскому звену звена back_of_house
, который в этом случае является crate
, корневым звеном. В этом звене мы ищем deliver_order
и находим его. Успех! Мы думаем, что звено back_of_house
и функция deliver_order
, скорее всего, останутся в тех же родственных отношениях друг с другом, и должны будут перемещены вместе, если мы решим ресогласовать дерево звеньев ящика. Поэтому мы использовали super
, чтобы в будущем у нас было меньше мест для обновления кода, если этот код будет перемещён в другой звено.
Мы также можем использовать pub
для обозначения устройств и перечислений как общедоступных, но есть несколько дополнительных подробностей использования pub
со устройствами и перечислениями. Если мы используем pub
перед определением устройства, мы делаем устройство общедоступной, но поля устройства по-прежнему остаются закрытыми. Мы можем сделать каждое поле общедоступным или нет в каждом определенном случае. В приложении 7-9 мы определили общедоступную устройство back_of_house::Breakfast
с общедоступным полем toast
и с закрытым полем seasonal_fruit
. Это расчитывает случай в ресторане, когда клиент может выбрать вид хлеба, который подаётся с едой, а шеф-повар решает какие фрукты сопровождают еду, исходя из того, что сезонно и что есть в наличии. Доступные фрукты быстро меняются, поэтому клиенты не могут выбирать фрукты или даже увидеть, какие фрукты они получат.
Файл: src/lib.rs
-mod back_of_house {
- pub struct Breakfast {
- pub toast: String,
- seasonal_fruit: String,
- }
-
- impl Breakfast {
- pub fn summer(toast: &str) -> Breakfast {
- Breakfast {
- toast: String::from(toast),
- seasonal_fruit: String::from("peaches"),
- }
- }
- }
-}
-
-pub fn eat_at_restaurant() {
- // Order a breakfast in the summer with Rye toast
- let mut meal = back_of_house::Breakfast::summer("Rye");
- // Change our mind about what bread we'd like
- meal.toast = String::from("Wheat");
- println!("I'd like {} toast please", meal.toast);
-
- // The next line won't compile if we uncomment it; we're not allowed
- // to see or modify the seasonal fruit that comes with the meal
- // meal.seasonal_fruit = String::from("blueberries");
-}
--
Поскольку поле toast
в устройстве back_of_house::Breakfast
является открытым, то в функции eat_at_restaurant
можно писать и читать поле toast
, используя точечную наставление. Обратите внимание, что мы не можем использовать поле seasonal_fruit
в eat_at_restaurant
, потому что seasonal_fruit
является закрытым. Попробуйте убрать примечания с последней строки для значения поля seasonal_fruit
, чтобы увидеть какую ошибку вы получите!
Также обратите внимание, что поскольку back_of_house::Breakfast
имеет закрытое поле, то устройства должна предоставить открытую сопряженную функцию, которая создаёт образец Breakfast
(мы назвали её summer
). Если Breakfast
не имел бы такой функции, мы бы не могли создать образец Breakfast
внутри eat_at_restaurant
, потому что мы не смогли бы установить значение закрытого поля seasonal_fruit
в функции eat_at_restaurant
.
В отличии от устройства, если мы сделаем общедоступным перечисление, то все его исходы будут общедоступными. Нужно только указать pub
перед ключевым словом enum
, как показано в приложении 7-10.
Файл: src/lib.rs
-mod back_of_house {
- pub enum Appetizer {
- Soup,
- Salad,
- }
-}
-
-pub fn eat_at_restaurant() {
- let order1 = back_of_house::Appetizer::Soup;
- let order2 = back_of_house::Appetizer::Salad;
-}
--
Поскольку мы сделали общедоступным перечисление Appetizer
, то можно использовать исходы Soup
и Salad
в функции eat_at_restaurant
.
Перечисления не очень полезны, если их исходы не являются общедоступными: было бы досадно каждый раз определять все исходы перечисления как pub
. По этой причине по умолчанию исходы перечислений являются общедоступными. Устройства часто полезны, если их поля не являются общедоступными, поэтому поля устройства следуют общему правилу, согласно которому, всё по умолчанию является закрытым, если не указано pub
.
Есть ещё одна случаей с pub
, которую мы не освещали, и это последняя особенность состоящей из звеньев системы: ключевое слово use
. Мы сначала опишем use
само по себе, а затем покажем как сочетать pub
и use
вместе.
use
Необходимость записывать пути к функциям вызова может показаться неудобной и повторяющейся. В приложении 7-7 независимо от того, выбирали ли мы абсолютный или относительный путь к функции add_to_waitlist
, каждый раз, когда мы хотели вызвать add_to_waitlist
, нам приходилось также указывать front_of_house
и hosting
. К счастью, есть способ упростить этот этап: мы можем один раз создать псевдоним на путь при помощи ключевого слова use
, а затем использовать более короткое имя везде в области видимости.
В приложении 7-11 мы подключили звено crate::front_of_house::hosting
в область действия функции eat_at_restaurant
, поэтому нам достаточно только указать hosting::add_to_waitlist
для вызова функции add_to_waitlist
внутри eat_at_restaurant
.
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
--
Добавление use
и пути в область видимости подобно созданию символической ссылки в файловой системе. С добавлением use crate::front_of_house::hosting
в корневой звено ящика, hosting
становится допустимым именем в этой области, как если бы звено hosting
был определён в корневом звене ящика. Пути, подключённые в область видимости с помощью use
, также проверяются на доступность, как и любые другие пути.
Обратите внимание, что use
создаёт псевдоним только для той именно области, в которой это объявление use
и находится. В приложении 7-12 функция eat_at_restaurant
перемещается в новый дочерний звено с именем customer
, область действия которого отличается от области действия указания use
, поэтому тело функции не будет собираться:
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting;
-
-mod customer {
- pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
- }
-}
--
Ошибка сборщика показывает, что данный псевдоним не может использоваться в звене customer
:
$ cargo build
- Compiling restaurant v0.1.0 (file:///projects/restaurant)
-error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
- --> src/lib.rs:11:9
- |
-11 | hosting::add_to_waitlist();
- | ^^^^^^^ use of undeclared crate or module `hosting`
- |
-help: consider importing this module through its public re-export
- |
-10 + use crate::hosting;
- |
-
-warning: unused import: `crate::front_of_house::hosting`
- --> src/lib.rs:7:5
- |
-7 | use crate::front_of_house::hosting;
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- |
- = note: `#[warn(unused_imports)]` on by default
-
-For more information about this error, try `rustc --explain E0433`.
-warning: `restaurant` (lib) generated 1 warning
-error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
-
-Обратите внимание, что есть также предупреждение о том, что use
не используется в своей области! Чтобы решить эту неполадку, можно переместить use
в звено customer
, или же можно сослаться на псевдоним в родительском звене с помощью super::hosting
в дочернем звене customer
.
use
В приложении 7-11 вы могли бы задаться вопросом, почему мы указали use crate::front_of_house::hosting
, а затем вызвали hosting::add_to_waitlist
внутри eat_at_restaurant
вместо указания в use
полного пути прямо до функции add_to_waitlist
для получения того же итога, что в приложении 7-13.
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-use crate::front_of_house::hosting::add_to_waitlist;
-
-pub fn eat_at_restaurant() {
- add_to_waitlist();
-}
--
Хотя приложениеи 7-11 и 7-13 выполняют одну и ту же задачу, приложение 7-11 является идиоматическим способом подключения функции в область видимости с помощью use
. Подключение родительского звена функции в область видимости при помощи use
означает, что мы должны указывать родительский звено при вызове функции. Указание родительского звена при вызове функции даёт понять, что функция не определена местно, но в то же время сводя к уменьшению повторение полного пути. В коде приложения 7-13 не ясно, где именно определена add_to_waitlist
.
С другой стороны, при подключении устройств, перечислений и других элементов используя use
, идиоматически правильным будет указывать полный путь. Приложение 7-14 показывает идиоматический способ подключения устройства встроенной библиотеки HashMap
в область видимости двоичного ящика.
Файл: src/main.rs
--use std::collections::HashMap; - -fn main() { - let mut map = HashMap::new(); - map.insert(1, 2); -}
-
За этой идиомой нет веской причины: это просто соглашение, которое появилось само собой. Люди привыкли читать и писать код на Ржавчина таким образом.
-Исключением из этой идиомы является случай, когда мы подключаем два элемента с одинаковыми именами в область видимости используя указанию use
— Ржавчина просто не позволяет этого сделать. Приложение 7-15 показывает, как подключить в область действия два вида с одинаковыми именами Result
, но из разных родительских звеньев и как на них ссылаться.
Файл: src/lib.rs
-use std::fmt;
-use std::io;
-
-fn function1() -> fmt::Result {
- // --snip--
- Ok(())
-}
-
-fn function2() -> io::Result<()> {
- // --snip--
- Ok(())
-}
--
Как видите, использование имени родительских звеньев позволяет различать два вида Result
. Если бы вместо этого мы указали use std::fmt::Result
и use std::io::Result
, мы бы имели два вида Result
в одной области видимости, и Ржавчина не смог бы понять какой из двух Result
мы имели в виду, когда нашёл бы их употребление в коде.
as
Есть другое решение сбоев добавления двух видов с одинаковыми именами в одну и ту же область видимости используя use
: после пути можно указать as
и новое местное имя (псевдоним) для вида. Приложение 7-16 показывает как по-другому написать код из приложения 7-15, путём переименования одного из двух видов Result
используя as
.
Файл: src/lib.rs
-use std::fmt::Result;
-use std::io::Result as IoResult;
-
-fn function1() -> Result {
- // --snip--
- Ok(())
-}
-
-fn function2() -> IoResult<()> {
- // --snip--
- Ok(())
-}
--
Во второй указания use
мы выбрали новое имя IoResult
для вида std::io::Result
, которое теперь не будет враждовать с видом Result
из std::fmt
, который также подключён в область видимости. Приложения 7-15 и 7-16 считаются идиоматичными, поэтому выбор за вами!
pub use
Когда мы подключаем имя в область видимости, используя ключевое слово use
, то имя, доступное в новой области видимости, является закрытым. Чтобы позволить коду, который вызывает наш код, ссылаться на это имя, как если бы оно было определено в области видимости данного кода, можно объединить pub
и use
. Этот способ называется реэкспортом (re-exporting), потому что мы подключаем элемент в область видимости, но также делаем этот элемент доступным для подключения в других областях видимости.
Приложение 7-17 показывает код из приложения 7-11, где use
в корневом звене заменено на pub use
.
Файл: src/lib.rs
-mod front_of_house {
- pub mod hosting {
- pub fn add_to_waitlist() {}
- }
-}
-
-pub use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
--
До этого изменения внешний код должен был вызывать функцию add_to_waitlist
, используя путь restaurant::front_of_house::hosting::add_to_waitlist()
. Теперь, когда это объявление pub use
повторно экспортировало звено hosting
из корневого звена, внешний код теперь может использовать вместо него путь restaurant::hosting::add_to_waitlist()
.
Реэкспорт полезен, когда внутренняя устройства вашего кода отличается от того, как программисты, вызывающие ваш код, думают о предметной области. Например, по подобию с рестораном люди, управляющие им, думают о «передней части дома» и «задней части дома». Но клиенты, посещающие ресторан, вероятно, не будут думать о частях ресторана в таких понятиях. Используя pub use
, мы можем написать наш код с одной устройством, но сделать общедоступной другую устройство. Благодаря этому наша библиотека хорошо согласована для программистов, работающих над библиотекой, и для программистов, вызывающих библиотеку. Мы рассмотрим ещё один пример pub use
и его влияние на документацию вашего ящика в разделе «Экспорт удобного общедоступного API с pub use
» Главы 14.
В Главе 2 мы запрограммировали игру угадывания числа, где использовался внешний дополнение с именем rand
для создания случайного числа. Чтобы использовать rand
в нашем деле, мы добавили эту строку в Cargo.toml:
Файл: Cargo.toml
-rand = "0.8.5"
-
-Добавление rand
в качестве зависимости в Cargo.toml указывает Cargo загрузить дополнение rand
и все его зависимости из crates.io и сделать rand
доступным для нашего дела.
Затем, чтобы подключить определения rand
в область видимости нашего дополнения, мы добавили строку use
начинающуюся с названия дополнения rand
и списка элементов, которые мы хотим подключить в область видимости. Напомним, что в разделе "Создание случайного числа" Главы 2, мы подключили особенность Rng
в область видимости и вызвали функцию rand::thread_rng
:
use std::io;
-use rand::Rng;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-}
-Члены сообщества Ржавчина сделали много дополнений доступными на ресурсе crates.io, и добавление любого из них в ваш дополнение включает в себя одни и те же шаги: добавить внешние дополнения в файл Cargo.toml вашего дополнения, использовать use
для подключения элементов внешних дополнений в область видимости.
Обратите внимание, что обычная библиотека std
также является ящиком, внешним по отношению к нашему дополнению. Поскольку обычная библиотека поставляется с языком Rust, нам не нужно изменять Cargo.toml для подключения std
. Но нам нужно ссылаться на неё при помощи use
, чтобы добавить элементы оттуда в область видимости нашего дополнения. Например, с HashMap
мы использовали бы эту строку:
-#![allow(unused)] -fn main() { -use std::collections::HashMap; -}
Это абсолютный путь, начинающийся с std
, имени ящика встроенной библиотеки.
use
Если мы используем несколько элементов, определённых в одном ящике или в том же звене, то перечисление каждого элемента в отдельной строке может занимать много вертикального пространства в файле. Например, эти две указания use
используются в программе угадывания числа (приложение 2-4) для подключения элементов из std
в область видимости:
Файл: src/main.rs
-use rand::Rng;
-// --snip--
-use std::cmp::Ordering;
-use std::io;
-// --snip--
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
-Вместо этого, мы можем использовать вложенные пути, чтобы добавить эти элементы в область видимости одной строкой. Мы делаем это, как показано в приложении 7-18, указывая общую часть пути, за которой следуют два двоеточия, а затем фигурные скобки вокруг списка тех частей продолжения пути, которые отличаются.
-Файл: src/main.rs
-use rand::Rng;
-// --snip--
-use std::{cmp::Ordering, io};
-// --snip--
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- println!("You guessed: {guess}");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
-}
--
В больших программах, подключение множества элементов из одного дополнения или звена с использованием вложенных путей может значительно сократить количество необходимых отдельных указаний use
!
Можно использовать вложенный путь на любом уровне, что полезно при объединении двух указаний use
, которые имеют общую часть пути. Например, в приложении 7-19 показаны две указания use
: одна подключает std::io
, а другая подключает std::io::Write
в область видимости.
Файл: src/lib.rs
-use std::io;
-use std::io::Write;
--
Общей частью этих двух путей является std::io
, и это полный первый путь. Чтобы объединить эти два пути в одной указания use
, мы можем использовать ключевое слово self
во вложенном пути, как показано в приложении 7-20.
Файл: src/lib.rs
-use std::io::{self, Write};
--
Эта строка подключает std::io
и std::io::Write
в область видимости.
Если мы хотим включить в область видимости все общедоступные элементы, определённые в пути, мы можем указать этот путь, за которым следует оператор *
:
-#![allow(unused)] -fn main() { -use std::collections::*; -}
Эта указание use
подключает все открытые элементы из звена std::collections
в текущую область видимости. Будьте осторожны при использовании оператора *
! Он может усложнить понимание, какие имена находятся в области видимости и где были определены имена, используемые в вашей программе.
Оператор *
часто используется при проверке для подключения всего что есть в звене tests
; мы поговорим об этом в разделе "Как писать проверки" Главы 11. Оператор *
также иногда используется как часть образца самостоятельного подключения (prelude): смотрите документацию по встроенной библиотеке для получения дополнительной сведений об этом образце.
До сих пор все примеры в этой главе определяли несколько звеньев в одном файле. Когда звенья становятся большими, вы можете захотеть переместить их определения в отдельные файлы, чтобы упростить навигацию по коду.
-Например, давайте начнём с кода из приложения 7-17, в котором было несколько звеньев ресторана. Мы будем извлекать звенья в файлы вместо того, чтобы определять все звенья в корневом звене ящика. В нашем случае корневой звено ящика - src/lib.rs, но это разделение также работает и с двоичными ящиками, у которых корневой звено ящика — src/main.rs.
-Сначала мы извлечём звено front_of_house
в свой собственный файл. Удалите код внутри фигурных скобок для звена front_of_house
, оставив только объявление mod front_of_house;
, так что теперь src/lib.rs содержит код, показанный в приложении 7-21. Обратите внимание, что этот исход не собирается, пока мы не создадим файл src/front_of_house.rs из приложении 7-22.
Файл: src/lib.rs
-mod front_of_house;
-
-pub use crate::front_of_house::hosting;
-
-pub fn eat_at_restaurant() {
- hosting::add_to_waitlist();
-}
--
Затем поместим код, который был в фигурных скобках, в новый файл с именем src/front_of_house.rs, как показано в приложении 7-22. Сборщик знает, что нужно искать в этом файле, потому что он наткнулся в корневом звене ящика на объявление звена с именем front_of_house
.
Файл: src/front_of_house.rs
-pub mod hosting {
- pub fn add_to_waitlist() {}
-}
--
Обратите внимание, что вам нужно только один раз загрузить файл с помощью объявления mod
в вашем дереве звеньев. Как только сборщик узнает, что файл является частью дела (и узнает, где в дереве звеньев находится код из-за того, куда вы помеисполнения указанию mod
), другие файлы в вашем деле должны ссылаться на код загруженного файла, используя путь к месту, где он был объявлен, как описано в разделе «Пути для ссылки на элемент в дереве звеньев». Другими словами, mod
— это не действие «включения», которую вы могли видеть в других языках программирования.
Далее мы извлечём звено hosting
в его собственный файл. Этап немного отличается, потому что hosting
является дочерним звеном для front_of_house
, а не корневого звена. Мы поместим файл для hosting
в новый папка, который будет назван по имени его предка в дереве звеньев, в данном случае это src/front_of_house/.
Чтобы начать перенос hosting
, мы меняем src/front_of_house.rs так, чтобы он содержал только объявление звена hosting
:
Файл: src/front_of_house.rs
-pub mod hosting;
-Затем мы создаём папка src/front_of_house и файл hosting.rs, в котором будут определения, сделанные в звене hosting
:
Файл: src/front_of_house/hosting.rs
-pub fn add_to_waitlist() {}
-Если вместо этого мы поместим hosting.rs в папка src, сборщик будет думать, что код в hosting.rs это звено hosting
, объявленный в корне ящика, а не объявленный как дочерний звено front_of_house
. Правила сборщика для проверки какие файлы содержат код каких звеньев предполагают, что папки и файлы точно соответствуют дереву звеньев.
--Иные пути к файлам
-До сих пор мы рассматривали наиболее идиоматические пути к файлам, используемые сборщиком Rust, но Ржавчина также поддерживает и старый исполнение пути к файлу. Для звена с именем
-front_of_house
, объявленного в корневом звене ящика, сборщик будет искать код звена в:-
-- src/front_of_house.rs (что мы рассматривали)
-- src/front_of_house/mod.rs (старый исполнение, всё ещё поддерживаемый путь)
-Для звена с именем
-hosting
, который является подзвеномfront_of_house
, сборщик будет искать код звена в:-
-- src/front_of_house/hosting.rs (что мы рассматривали)
-- src/front_of_house/hosting/mod.rs (старый исполнение, всё ещё поддерживаемый путь)
-Если вы используете оба исполнения для одного и того же звена, вы получите ошибку сборщика. Использование сочетания обоих исполнениий для разных звеньев в одном деле разрешено, но это может сбивать с толку людей, перемещающихся по вашему делу.
-Основным недостатком исполнения, в котором используются файлы с именами mod.rs, является то, что в вашем деле может оказаться много файлов с именами mod.rs, что может привести к путанице, если вы одновременно откроете их в редакторе.
-
Мы перенесли код каждого звена в отдельный файл, а дерево звеньев осталось прежним. Вызовы функций в eat_at_restaurant
будут работать без каких-либо изменений, несмотря на то, что определения находятся в разных файлах. Этот способ позволяет перемещать звенья в новые файлы по мере увеличения их размеров.
Обратите внимание, что указание pub use crate::front_of_house::hosting
в src/lib.rs также не изменилась, и use
не влияет на то, какие файлы собираются как часть ящика. Ключевое слово mod
объявляет звенья, и Ржавчина ищет в файле с тем же именем, что и у звена, код, который входит в этот звено.
Rust позволяет разбить дополнение на несколько ящиков и ящик - на звенья, так что вы можете ссылаться на элементы, определённые в одном звене, из другого звена. Это можно делать при помощи указания абсолютных или относительных путей. Эти пути можно добавить в область видимости указанием use
, поэтому вы можете пользоваться более короткими путями для многократного использования элементов в этой области видимости. Код звена по умолчанию является закрытым, но можно сделать определения общедоступными, добавив ключевое слово pub
.
В следующей главе мы рассмотрим некоторые собрания устройств данных из встроенной библиотеки, которые вы можете использовать в своём правильноно согласованном коде.
- -Обычная библиотека содержит несколько полезных устройств данных, которые называются собраниями. Большая часть других видов данных представляют собой хранение определенного значения, но особенностью собраний является хранение множества однотипных значений. В отличии от массива или упорядоченного ряда данные собраний хранятся в куче, а это значит, что размер собрания может быть неизвестен в мгновение сборки программы. Он может изменяться (увеличиваться, уменьшаться) во время работы программы. Каждый вид собраний имеет свои возможности и отличается по производительности, так что выбор именно собрания зависит от случаи и является умением разработчика, вырабатываемым со временем. В этой главе будет рассмотрено несколько собраний:
-String
ранее, но в данной главе мы поговорим о нем подробнее.Для того, чтобы узнать о других видах собраний предоставляемых встроенной библиотекой смотрите документацию.
-Мы обсудим как создавать и обновлять векторы, строки и хеш-таблицы, а также объясним что делает каждую из них особенной.
- -Первым видом собрания, который мы разберём, будет Vec<T>
, также известный как вектор (vector). Векторы позволяют хранить более одного значения в единой устройстве данных, хранящей элементы в памяти один за другим. Векторы могут хранить данные только одного вида. Их удобно использовать, когда нужно хранить список элементов, например, список текстовых строк из файла, или список цен товаров в корзине покупок.
Чтобы создать новый пустой вектор, мы вызываем функцию Vec::new
, как показано в приложении 8-1.
-fn main() { - let v: Vec<i32> = Vec::new(); -}
-
Обратите внимание, что здесь мы добавили изложение вида. Поскольку мы не вставляем никаких значений в этот вектор, Ржавчина не знает, какие элементы мы собираемся хранить. Это важный мгновение. Векторы выполнены с использованием обобщённых видов; мы рассмотрим, как использовать обобщённые виды с вашими собственными видами в Главе 10. А пока знайте, что вид Vec<T>
, предоставляемый встроенной библиотекой, может хранить любой вид. Когда мы создаём новый вектор для хранения определенного вида, мы можем указать этот вид в угловых скобках. В приложении 8-1 мы сообщили Rust, что Vec<T>
в v
будет хранить элементы вида i32
.
Чаще всего вы будете создавать Vec<T>
с начальными значениями и Ржавчина может определить вид сохраняемых вами значений, но иногда вам всё же придётся указывать изложение вида. Для удобства Ржавчина предоставляет макрос vec!
, который создаст новый вектор, содержащий заданные вами значения. В приложении 8-2 создаётся новый Vec<i32>
, который будет хранить значения 1
, 2
и 3
. Числовым видом является i32
, потому что это вид по умолчанию для целочисленных значений, о чём упоминалось в разделе “Виды данных” Главы 3.
-fn main() { - let v = vec![1, 2, 3]; -}
-
Поскольку мы указали начальные значения вида i32
, Ржавчина может сделать вывод, что вид переменной v
это Vec<i32>
и изложение вида здесь не нужна. Далее мы посмотрим как изменять вектор.
Чтобы создать вектор и затем добавить к нему элементы, можно использовать способ push
показанный в приложении 8-3.
-fn main() { - let mut v = Vec::new(); - - v.push(5); - v.push(6); - v.push(7); - v.push(8); -}
-
Как и с любой переменной, если мы хотим изменить её значение, нам нужно сделать её изменяемой с помощью ключевого слова mut
, что обсуждалось в Главе 3. Все числа которые мы помещаем в вектор имеют вид i32
по этому Ржавчина с лёгкостью выводит вид вектора, по этой причине нам не нужна здесь изложение вида вектора Vec<i32>
.
Есть два способа сослаться на значение, хранящееся в векторе: с помощью порядкового указателя или способа get
. В следующих примерах для большей ясности мы указали виды значений, возвращаемых этими функциями.
В приложении 8-4 показаны оба способа доступа к значению в векторе: либо с помощью правил написания упорядочевания и с помощью способа get
.
-fn main() { - let v = vec![1, 2, 3, 4, 5]; - - let third: &i32 = &v[2]; - println!("The third element is {third}"); - - let third: Option<&i32> = v.get(2); - match third { - Some(third) => println!("The third element is {third}"), - None => println!("There is no third element."), - } -}
-
Обратите внимание здесь на пару подробностей. Мы используем значение порядкового указателя 2
для получения третьего элемента: векторы упорядочеваются начиная с нуля. Указывая &
и []
мы получаем ссылку на элемент по указанному порядковому указателю. Когда мы используем способ get
содержащего порядковый указатель, переданный в качестве переменной, мы получаем вид Option<&T>
, который мы можем проверить с помощью match
.
Причина, по которой Ржавчина предоставляет два способа ссылки на элемент, заключается в том, что вы можете выбрать, как программа будет себя вести, когда вы попытаетесь использовать значение порядкового указателя за пределами ряда существующих элементов. В качестве примера давайте посмотрим, что происходит, когда у нас есть вектор из пяти элементов, а затем мы пытаемся получить доступ к элементу с порядковым указателем 100 с помощью каждого способа, как показано в приложении 8-5.
--fn main() { - let v = vec![1, 2, 3, 4, 5]; - - let does_not_exist = &v[100]; - let does_not_exist = v.get(100); -}
-
Когда мы запускаем этот код, первая строка с &v[100]
вызовет панику программы, потому что происходит попытка получить ссылку на несуществующий элемент. Такой подход лучше всего использовать, когда вы хотите, чтобы ваша программа со сбоем завершила работу при попытке доступа к элементу за пределами вектора.
Когда способу get
передаётся порядковый указатель, который находится за пределами вектора, он без паники возвращает None
. Вы могли бы использовать такой подход, если доступ к элементу за пределами рядавектора происходит время от времени при обычных обстоятельствах. Тогда ваш код будет иметь логику для обработки наличия Some(&element)
или None
, как обсуждалось в Главе 6. Например, порядковый указательможет исходить от человека, вводящего число. Если пользователь случайно введёт слишком большое число, то программа получит значение None
и у вас будет возможность сообщить пользователю, сколько элементов находится в текущем векторе, и дать ему возможность ввести допустимое значение. Такое поведение было бы более дружелюбным для пользователя, чем внезапный сбой программы из-за опечатки!
Когда у программы есть действительная ссылка, borrow checker (средство проверки заимствований), обеспечивает соблюдение правил владения и заимствования (описанные в Главе 4), чтобы обеспечить, что эта ссылка и любые другие ссылки на содержимое вектора остаются действительными. Вспомните правило, которое гласит, что у вас не может быть изменяемых и неизменяемых ссылок в одной и той же области. Это правило применяется в приложении 8-6, где мы храним неизменяемую ссылку на первый элемент вектора и затем пытаемся добавить элемент в конец вектора. Данная программа не будет работать, если мы также попробуем сослаться на данный элемент позже в функции:
-fn main() {
- let mut v = vec![1, 2, 3, 4, 5];
-
- let first = &v[0];
-
- v.push(6);
-
- println!("The first element is: {first}");
-}
--
Сборка этого кода приведёт к ошибке:
-$ cargo run
- Compiling collections v0.1.0 (file:///projects/collections)
-error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
- --> src/main.rs:6:5
- |
-4 | let first = &v[0];
- | - immutable borrow occurs here
-5 |
-6 | v.push(6);
- | ^^^^^^^^^ mutable borrow occurs here
-7 |
-8 | println!("The first element is: {first}");
- | ------- immutable borrow later used here
-
-For more information about this error, try `rustc --explain E0502`.
-error: could not compile `collections` (bin "collections") due to 1 previous error
-
-Код в приложении 8-6 может выглядеть так, как будто он должен работать. Почему ссылка на первый элемент должна заботиться об изменениях в конце вектора? Эта ошибка возникает из-за особенности того, как работают векторы: поскольку векторы размещают значения в памяти друг за другом, добавление нового элемента в конец вектора может потребовать выделения новой памяти и повторения старых элементов в новое пространство, если нет достаточного места, чтобы разместить все элементы друг за другом там, где в данный мгновение хранится вектор. В этом случае ссылка на первый элемент будет указывать на освобождённую память. Правила заимствования предотвращают попадание программ в такую случай.
---Примечание: Дополнительные сведения о выполнения вида
-Vec<T>
смотрите в разделе "The Rustonomicon".
Для доступа к каждому элементу вектора по очереди, мы повторяем все элементы, вместо использования порядковых указателей для доступа к одному за раз. В приложении 8-7 показано, как использовать цикл for
для получения неизменяемых ссылок на каждый элемент в векторе значений вида i32
и их вывода.
-fn main() { - let v = vec![100, 32, 57]; - for i in &v { - println!("{i}"); - } -}
-
Мы также можем повторять изменяемые ссылки на каждый элемент изменяемого вектора, чтобы вносить изменения во все элементы. Цикл for
в приложении 8-8 добавит 50
к каждому элементу.
-fn main() { - let mut v = vec![100, 32, 57]; - for i in &mut v { - *i += 50; - } -}
-
Чтобы изменить значение на которое ссылается изменяемая ссылка, мы должны использовать оператор разыменования ссылки *
для получения значения по ссылке в переменной i
прежде чем использовать оператор +=
. Мы поговорим подробнее об операторе разыменования в разделе “Следование по указателю к значению с помощью оператора разыменования” Главы 15.
Перебор вектора, будь то неизменяемый или изменяемый, безопасен из-за правил проверки заимствования. Если бы мы попытались вставить или удалить элементы в телах цикла for
в приложениях 8-7 и 8-8, мы бы получили ошибку сборщика, подобную той, которую мы получили с кодом в приложении 8-6. Ссылка на вектор, содержащийся в цикле for, предотвращает одновременную изменение всего вектора.
Векторы могут хранить значения только одинакового вида. Это может быть неудобно; определённо могут быть случаи когда надо хранить список элементов разных видов. К счастью, исходы перечисления определены для одного и того же вида перечисления, поэтому, когда нам нужен один вид для представления элементов разных видов, мы можем определить и использовать перечисление!
-Например, мы хотим получить значения из строки в электронной таблице где некоторые столбцы строки содержат целые числа, некоторые числа с плавающей точкой, а другие - строковые значения. Можно определить перечисление, исходы которого будут содержать разные виды значений и тогда все исходы перечисления будут считаться одним и тем же видом: видом самого перечисления. Затем мы можем создать вектор для хранения этого перечисления и, в конечном счёте, для хранения различных видов. Мы покажем это в приложении 8-9.
--fn main() { - enum SpreadsheetCell { - Int(i32), - Float(f64), - Text(String), - } - - let row = vec![ - SpreadsheetCell::Int(3), - SpreadsheetCell::Text(String::from("blue")), - SpreadsheetCell::Float(10.12), - ]; -}
-
Rust должен знать, какие виды будут в векторе во время сборки, чтобы точно знать сколько памяти в куче потребуется для хранения каждого элемента. Мы также должны чётко указать, какие виды разрешены в этом векторе. Если бы Ржавчина позволял вектору содержать любой вид, то был бы шанс что один или несколько видов вызовут ошибки при выполнении действий над элементами вектора. Использование перечисления вместе с выражением match
означает, что во время сборки Ржавчина заверяет, что все возможные случаи будут обработаны, как обсуждалось в главе 6.
Если вы не знаете исчерпывающий набор видов, которые программа получит во время выполнения для хранения в векторе, то техника использования перечисления не сработает. Вместо этого вы можете использовать особенность-предмет, который мы рассмотрим в главе 17.
-Теперь, когда мы обсудили некоторые из наиболее распространённых способов использования векторов, обязательно ознакомьтесь с документацией по API вектора, чтобы узнать о множестве полезных способов, определённых в Vec<T>
встроенной библиотеки. Например, в дополнение к способу push
, существует способ pop
, который удаляет и возвращает последний элемент.
Подобно устройствам struct
, вектор высвобождает свою память когда выходит из области видимости, что показано в приложении 8-10.
-fn main() { - { - let v = vec![1, 2, 3, 4]; - - // do stuff with v - } // <- v goes out of scope and is freed here -}
-
Когда вектор удаляется, всё его содержимое также удаляется: удаление вектора означает и удаление значений, которые он содержит. Средство проверки заимствования заверяет, что любые ссылки на содержимое вектора используются только тогда, когда сам вектор действителен.
-Давайте перейдём к следующему виду собрания: String
!
Мы говорили о строках в главе 4, но сейчас мы рассмотрим их более подробно. Новички в Ржавчина обычно застревают на строках из-за сочетания трёх причин: склонность Ржавчина сборщика к выявлению возможных ошибок, более сложная устройства данных чем считают многие программисты и UTF-8. Эти обстоятельства объединяются таким образом, что направление может показаться сложной, если вы пришли из других языков программирования.
-Полезно обсуждать строки в среде собраний, потому что строки выполнены в виде набора байтов, плюс некоторые способы для обеспечения полезной возможности, когда эти байты преобразуются как текст. В этом разделе мы поговорим об действиех над String
таких как создание, обновление и чтение, которые есть у каждого вида собраний. Мы также обсудим какими особенностями String
отличается от других собраний, а именно каким образом упорядочевание в String
осложняется различием между тем как люди и компьютеры преобразуют данные заключённые в String
.
Сначала мы определим, что мы подразумеваем под понятием строка (string). В Ржавчина есть только один строковый вид в ядре языка - срез строки str
, обычно используемый в заимствованном виде как &str
. В Главе 4 мы говорили о срезах строк, string slices, которые являются ссылками на некоторые строковые данные в кодировке UTF-8. Например, строковые записи хранятся в двоичном файле программы и поэтому являются срезами строк.
Вид String
предоставляемый встроенной библиотекой Rust, не встроен в ядро языка и является расширяемым, изменяемым, владеющим, строковым видом в UTF-8 кодировке. Когда Rustaceans говорят о "строках" то, они обычно имеют в виду виды String
или строковые срезы &str
, а не просто один из них. Хотя этот раздел в основном посвящён String
, оба вида усиленно используются в встроенной библиотеке Rust, оба, и String
и строковые срезы, кодируются в UTF-8.
Многие из тех же действий, которые доступны Vec<T>
, доступны также в String
, потому что String
в действительности выполнен как обёртка вокруг вектора байтов с некоторыми дополнительными заверениями, ограничениями и возможностями. Примером функции, которая одинаково работает с Vec<T>
и String
, является функция new
, создающая новый образец вида, и показана в Приложении 8-11.
-fn main() { - let mut s = String::new(); -}
-
Эта строка создаёт новую пустую строковую переменную с именем s
, в которую мы можем затем загрузить данные. Часто у нас есть некоторые начальные данные, которые мы хотим назначить строке. Для этого мы используем способ to_string
доступный для любого вида, который выполняет особенность Display
, как у строковых записей. Приложение 8-12 показывает два примера.
-fn main() { - let data = "initial contents"; - - let s = data.to_string(); - - // the method also works on a literal directly: - let s = "initial contents".to_string(); -}
-
Эти выражения создают строку с initial contents
.
Мы также можем использовать функцию String::from
для создания String
из строкового записи. Код приложения 8-13 является эквивалентным коду из приложения 8-12, который использует функцию to_string
:
-fn main() { - let s = String::from("initial contents"); -}
-
Поскольку строки используются для очень многих вещей, можно использовать множество API для строк, предоставляющих множество возможностей. Некоторые из них могут показаться избыточными, но все они занимаются своим делом! В данном случае String::from
и to_string
делают одно и тоже, поэтому выбор зависит от исполнения который вам больше импонирует.
Запомните, что строки хранятся в кодировке UTF-8, поэтому можно использовать любые правильно кодированные данные в них, как показано в приложении 8-14:
--fn main() { - let hello = String::from("السلام عليكم"); - let hello = String::from("Dobrý den"); - let hello = String::from("Hello"); - let hello = String::from("שלום"); - let hello = String::from("नमस्ते"); - let hello = String::from("こんにちは"); - let hello = String::from("안녕하세요"); - let hello = String::from("你好"); - let hello = String::from("Olá"); - let hello = String::from("Здравствуйте"); - let hello = String::from("Hola"); -}
-
Все это допустимые String
значения.
Строка String
может увеличиваться в размере, а её содержимое может меняться, по подобию как содержимое Vec<T>
при вставке в него большего количества данных. Кроме того, можно использовать оператор +
или макрос format!
для объединения значений String
.
push_str
и push
Мы можем нарастить String
используя способ push_str
который добавит в исходное значение новый строковый срез, как показано в приложении 8-15.
-fn main() { - let mut s = String::from("foo"); - s.push_str("bar"); -}
-
После этих двух строк кода s
будет содержать foobar
. Способ push_str
принимает строковый срез, потому что мы не всегда хотим владеть входным свойствоом. Например, код в приложении 8-16 показывает исход, когда будет не желательно поведение, при котором мы не сможем использовать s2
после его добавления к содержимому значения переменной s1
.
-fn main() { - let mut s1 = String::from("foo"); - let s2 = "bar"; - s1.push_str(s2); - println!("s2 is {s2}"); -}
-
Если способ push_str
стал бы владельцем переменнойs2
, мы не смогли бы напечатать его значение в последней строке. Однако этот код работает так, как мы ожидали!
Способ push
принимает один символ в качестве свойства и добавляет его к String
. В приложении 8-17 показан код, добавляющий букву “l” к String
используя способ push
.
-fn main() { - let mut s = String::from("lo"); - s.push('l'); -}
-
В итоге s
будет содержать lol
.
+
или макроса format!
Часто хочется объединять две существующие строки. Один из возможных способов — это использование оператора +
из приложения 8-18:
-fn main() { - let s1 = String::from("Hello, "); - let s2 = String::from("world!"); - let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used -}
-
Строка s3
будет содержать Hello, world!
. Причина того, что s1
после добавления больше недействительна и причина, по которой мы использовали ссылку на s2
имеют отношение к ярлыке вызываемого способа при использовании оператора +
. Оператор +
использует способ add
, чья ярлык выглядит примерно так:
fn add(self, s: &str) -> String {
-В встроенной библиотеке вы увидите способ add
определённым с использованием обобщённых и связанных видов. Здесь мы видим ярлык с определенными видами, заменяющими обобщённый, что происходит когда вызывается данный способ со значениями String
. Мы обсудим обобщённые виды в Главе 10. Эта ярлык даёт нам ключ для понимания особенностей оператора +
.
Во-первых, перед s2
мы видим &
, что означает что мы складываем ссылку на вторую строку с первой строкой. Это происходит из-за свойства s
в функции add
: мы можем добавить только &str
к String
; мы не можем сложить два значения String
. Но подождите — вид &s2
это &String
, а не &str
, как определён второй свойство в add
. Так почему код в приложении 8-18 собирается?
Причина, по которой мы можем использовать &s2
в вызове add
заключается в том, что сборщик может принудительно привести (coerce) переменная вида &String
к виду &str
. Когда мы вызываем способ add
в Ржавчина используется принудительное приведение (deref coercion), которое превращает &s2
в &s2[..]
. Мы подробно обсудим принудительное приведение в Главе 15. Так как add
не забирает во владение свойство s
, s2
по прежнему будет действительной строкой String
после применения действия.
Во-вторых, как можно видеть в ярлыке, add
забирает во владение self
, потому что self
не имеет &
. Это означает, что s1
в приложении 8-18 будет перемещён в вызов add
и больше не будет действителен после этого вызова. Не смотря на то, что код let s3 = s1 + &s2;
выглядит как будто он воспроизведет обе строки и создаёт новую, эта указание в действительности забирает во владение переменную s1
, присоединяет к ней повтор содержимого s2
, а затем возвращает владение итогом. Другими словами, это выглядит как будто код создаёт множество повторов, но это не так; данная выполнение более эффективна, чем повторение.
Если нужно объединить несколько строк, поведение оператора +
становится громоздким:
-fn main() { - let s1 = String::from("tic"); - let s2 = String::from("tac"); - let s3 = String::from("toe"); - - let s = s1 + "-" + &s2 + "-" + &s3; -}
Здесь переменная s
будет содержать tic-tac-toe
. С множеством символов +
и "
становится трудно понять, что происходит. Для более сложного соединения строк можно использовать макрос format!
:
-fn main() { - let s1 = String::from("tic"); - let s2 = String::from("tac"); - let s3 = String::from("toe"); - - let s = format!("{s1}-{s2}-{s3}"); -}
Этот код также устанавливает переменную s
в значение tic-tac-toe
. Макрос format!
работает тем же способом что макрос println!
, но вместо вывода на экран возвращает вид String
с содержимым. Исполнение кода с использованием format!
значительно легче читается, а также код, созданный макросом format!
, использует ссылки, а значит не забирает во владение ни один из его свойств.
Доступ к отдельным символам в строке, при помощи ссылки на них по порядковому указателю, является допустимой и распространённой действием во многих других языках программирования. Тем не менее, если вы попытаетесь получить доступ к частям String
, используя правила написания упорядочевания в Rust, то вы получите ошибку. Рассмотрим неверный код в приложении 8-19.
fn main() {
- let s1 = String::from("hello");
- let h = s1[0];
-}
--
Этот код приведёт к следующей ошибке:
-$ cargo run
- Compiling collections v0.1.0 (file:///projects/collections)
-error[E0277]: the type `str` cannot be indexed by `{integer}`
- --> src/main.rs:3:16
- |
-3 | let h = s1[0];
- | ^ string indices are ranges of `usize`
- |
- = help: the trait `SliceIndex<str>` is not implemented for `{integer}`, which is required by `String: Index<_>`
- = note: you can use `.chars().nth()` or `.bytes().nth()`
- for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
- = help: the trait `SliceIndex<[_]>` is implemented for `usize`
- = help: for that trait implementation, expected `[_]`, found `str`
- = note: required for `String` to implement `Index<{integer}>`
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `collections` (bin "collections") due to 1 previous error
-
-Ошибка и примечание говорит, что в Ржавчина строки не поддерживают упорядочевание. Но почему так? Чтобы ответить на этот вопрос, нужно обсудить то, как Ржавчина хранит строки в памяти.
-Вид String
является оболочкой над видом Vec<u8>
. Давайте посмотрим на несколько закодированных правильным образом в UTF-8 строк из примера приложения 8-14. Начнём с этой:
-fn main() { - let hello = String::from("السلام عليكم"); - let hello = String::from("Dobrý den"); - let hello = String::from("Hello"); - let hello = String::from("שלום"); - let hello = String::from("नमस्ते"); - let hello = String::from("こんにちは"); - let hello = String::from("안녕하세요"); - let hello = String::from("你好"); - let hello = String::from("Olá"); - let hello = String::from("Здравствуйте"); - let hello = String::from("Hola"); -}
В этом случае len
будет 4, что означает вектор, хранит строку "Hola" длиной 4 байта. Каждая из этих букв занимает 1 байт при кодировании в UTF-8. Но как насчёт следующей строки? (Обратите внимание, что эта строка начинается с заглавной кириллической "З", а не цифры 3.)
-fn main() { - let hello = String::from("السلام عليكم"); - let hello = String::from("Dobrý den"); - let hello = String::from("Hello"); - let hello = String::from("שלום"); - let hello = String::from("नमस्ते"); - let hello = String::from("こんにちは"); - let hello = String::from("안녕하세요"); - let hello = String::from("你好"); - let hello = String::from("Olá"); - let hello = String::from("Здравствуйте"); - let hello = String::from("Hola"); -}
Отвечая на вопрос, какова длина строки, вы можете ответить 12. Однако ответ Ржавчина - 24, что равно числу байт, необходимых для кодирования «Здравствуйте» в UTF-8, так происходит, потому что каждое одиночное значение Unicode символа в этой строке занимает 2 байта памяти. Следовательно, порядковый указательпо байтам строки не всегда бы соответствовал действительному одиночному Unicode значению. Для отображения рассмотрим этот недопустимый код Rust:
-let hello = "Здравствуйте";
-let answer = &hello[0];
-Каким должно быть значение переменной answer
? Должно ли оно быть значением первой буквы З
? При кодировке в UTF-8, первый байт значения З
равен 208
, а второй - 151
, поэтому значение в answer
на самом деле должно быть 208
, но само по себе 208
не является действительным символом. Возвращение 208
, скорее всего не то, что хотел бы получить пользователь: ведь он ожидает первую букву этой строки; тем не менее, это единственный байт данных, который в Ржавчина доступен по порядковому указателю 0. Пользователи обычно не хотят получить значение байта, даже если строка содержит только латинские буквы: если &"hello"[0]
было бы допустимым кодом, который вернул значение байта, то он вернул бы 104
, а не h
.
Таким образом, чтобы предотвратить возврат непредвиденного значения, вызывающего ошибки которые не могут быть сразу обнаружены, Ржавчина просто не собирает такой код и предотвращает недопонимание на ранних этапах этапа разработки.
-Ещё один мгновение, касающийся UTF-8, заключается в том, что на самом деле существует три способа рассмотрения строк с точки зрения Rust: как байты, как одиночные значения и как кластеры графем (самая близкая вещь к тому, что мы назвали бы буквами).
-Если посмотреть на слово языка хинди «नमस्ते», написанное в транскрипции Devanagari, то оно хранится как вектор значений u8
который выглядит следующим образом:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
-224, 165, 135]
-
-Эти 18 байт являются именно тем, как компьютеры в конечном итоге сохранят в памяти эту строку. Если мы посмотрим на 18 байт как на одиночные Unicode значения, которые являются Ржавчина видом char
, то байты будут выглядеть так:
['न', 'म', 'स', '्', 'त', 'े']
-
-Здесь есть шесть значений вида char
, но четвёртый и шестой являются не буквами: они диакритики, особые обозначения которые не имеют смысла сами по себе. Наконец, если мы посмотрим на байты как на кластеры графем, то получим то, что человек назвал бы словом на хинди состоящем из четырёх букв:
["न", "म", "स्", "ते"]
-
-Rust предоставляет различные способы преобразования необработанных строковых данных, которые компьютеры хранят так, чтобы каждой программе можно было выбрать необходимую преобразование, независимо от того, на каком человеческом языке представлены эти данные.
-Последняя причина, по которой Ржавчина не позволяет нам упорядочивать String
для получения символов является то, что программисты ожидают, что действия упорядочевания всегда имеют постоянное время (O(1)) выполнения. Но невозможно обеспечить такую производительность для String
, потому что Ржавчина понадобилось бы пройтись по содержимому от начала до порядкового указателя, чтобы определить, сколько было действительных символов.
Упорядочевание строк часто является плохой мыслью, потому что не ясно каким должен быть возвращаемый вид такой действия: байтовым значением, символом, кластером графем или срезом строки. Поэтому Ржавчина просит вас быть более определенным, если действительно требуется использовать порядковые указатели для создания срезов строк.
-Вместо упорядочевания с помощью числового порядкового указателя []
, вы можете использовать оператор ряда[]
при создании среза строки в котором содержится указание на то, срез каких байтов надо делать:
-#![allow(unused)] -fn main() { -let hello = "Здравствуйте"; - -let s = &hello[0..4]; -}
Здесь переменная s
будет вида &str
который содержит первые 4 байта строки. Ранее мы упоминали, что каждый из этих символов был по 2 байта, что означает, что s
будет содержать "Зд".
Что бы произошло, если бы мы использовали &hello[0..1]
? Ответ: Ржавчина бы запаниковал во время выполнения точно так же, как если бы обращались к недействительному порядковому указателю в векторе:
$ cargo run
- Compiling collections v0.1.0 (file:///projects/collections)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
- Running `target/debug/collections`
-thread 'main' panicked at src/main.rs:4:19:
-byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Вы должны использовать ряды для создания срезов строк с осторожностью, потому что это может привести к сбою вашей программы.
-Лучший способ работать с отрывками строк — чётко указать, нужны ли вам символы или байты. Для отдельных одиночных значений в Юникоде используйте способ chars
. Вызов chars
у "Зд" выделяет и возвращает два значения вида char
, и вы можете выполнить повторение по итогу для доступа к каждому элементу:
-#![allow(unused)] -fn main() { -for c in "Зд".chars() { - println!("{c}"); -} -}
Код напечатает следующее:
-З
-д
-
-Способ bytes
возвращает каждый байт, который может быть подходящим в другой предметной области:
-#![allow(unused)] -fn main() { -for b in "Зд".bytes() { - println!("{b}"); -} -}
Этот код выведет четыре байта, составляющих эту строку:
-208
-151
-208
-180
-
-Но делая так, обязательно помните, что валидные одиночные Unicode значения могут состоять более чем из одного байта.
-Извлечение кластеров графем из строк, как в случае с языком хинди, является сложным, поэтому эта возможность не предусмотрена встроенной библиотекой. На crates.io есть доступные библиотеки, если Вам нужен данный возможности.
-Подводя итог, становится ясно, что строки сложны. Различные языки программирования выполняют различные исходы того, как представить эту сложность для программиста. В Ржавчина решили сделать правильную обработку данных String
поведением по умолчанию для всех программ Rust, что означает, что программисты должны заранее продумать обработку UTF-8 данных. Этот соглашение раскрывает большую сложность строк, чем в других языках программирования, но это предотвращает от необходимости обрабатывать ошибки, связанные с не-ASCII символами которые могут появиться в ходе разработки позже.
Хорошая новость состоит в том что обычная библиотека предлагает множество полезных возможностей, построенных на основе видов String
и &str
, чтобы помочь правильно обрабатывать эти сложные случаи. Обязательно ознакомьтесь с документацией для полезных способов, таких как contains
для поиска в строке и replace
для замены частей строки другой строкой.
Давайте переключимся на что-то немного менее сложное: HashMap!
- -Последняя собрание, которую мы рассмотрим, будет hash map (хеш-карта). Вид HashMap<K, V>
хранит ключи вида K
на значения вида V
. Данная устройства согласует и хранит данные с помощью функции хеширования. Во множестве языков программирования выполнена данная устройства, но часто с разными наименованиями: такими как hash, map, object, hash table, dictionary или ассоциативный массив.
Хеш-карты полезны, когда нужно искать данные не используя порядковый указатель, как это например делается в векторах, а с помощью ключа, который может быть любого вида. Например, в игре вы можете отслеживать счёт каждой приказы в хеш-карте, в которой каждый ключ - это название приказы, а значение - счёт приказы. Имея имя приказы, вы можете получить её счёт из хеш-карты.
-В этом разделе мы рассмотрим основной API хеш-карт. Остальной набор полезных функций скрывается в объявлении вида HashMap<K, V>
. Как и прежде, советуем обратиться к документации по встроенной библиотеке для получения дополнительной сведений.
Создать пустую хеш-карту можно с помощью new
, а добавить в неё элементы - с помощью insert
. В приложении 8-20 мы отслеживаем счёт двух приказов, синей Blue и жёлтой Yellow. Синяя приказ набрала 10 очков, а жёлтая приказ - 50.
-fn main() { - use std::collections::HashMap; - - let mut scores = HashMap::new(); - - scores.insert(String::from("Blue"), 10); - scores.insert(String::from("Yellow"), 50); -}
-
Обратите внимание, что нужно сначала указать строку use std::collections::HashMap;
для её подключения из собраний встроенной библиотеки. Из трёх собраний данная является наименее используемой, поэтому она не подключается в область видимости функцией самостоятельного подключения (prelude). Хеш-карты также имеют меньшую поддержку со стороны встроенной библиотеки; например, нет встроенного макроса для их разработки.
Подобно векторам, хеш-карты хранят свои данные в куче. Здесь вид HashMap
имеет в качестве вида ключей String
, а в качестве вида значений вид i32
. Как и векторы, HashMap однородны: все ключи должны иметь одинаковый вид и все значения должны иметь тоже одинаковый вид.
Мы можем получить значение из HashMap по ключу, с помощью способа get
, как показано в приложении 8-21.
-fn main() { - use std::collections::HashMap; - - let mut scores = HashMap::new(); - - scores.insert(String::from("Blue"), 10); - scores.insert(String::from("Yellow"), 50); - - let team_name = String::from("Blue"); - let score = scores.get(&team_name).copied().unwrap_or(0); -}
-
Здесь score
будет иметь количество очков, связанное с приказом "Blue", итог будет 10
. Способ get
возвращает Option<&V>
; если для какого-то ключа нет значения в HashMap, get
вернёт None
. Из-за такого подхода программе следует обрабатывать Option
, вызывая copied
для получения Option<i32>
вместо Option<&i32>
, затем unwrap_or
для установки score
в ноль, если scores не содержит данных по этому ключу.
Мы можем перебирать каждую пару ключ/значение в HashMap таким же образом, как мы делали с векторами, используя цикл for
:
-fn main() { - use std::collections::HashMap; - - let mut scores = HashMap::new(); - - scores.insert(String::from("Blue"), 10); - scores.insert(String::from("Yellow"), 50); - - for (key, value) in &scores { - println!("{key}: {value}"); - } -}
Этот код будет печатать каждую пару в произвольном порядке:
-Yellow: 50
-Blue: 10
-
-Для видов, которые выполняют особенность Copy
, например i32
, значения повторяются в HashMap. Для значений со владением, таких как String
, значения будут перемещены в хеш-карту и она станет владельцем этих значений, как показано в приложении 8-22.
-fn main() { - use std::collections::HashMap; - - let field_name = String::from("Favorite color"); - let field_value = String::from("Blue"); - - let mut map = HashMap::new(); - map.insert(field_name, field_value); - // field_name and field_value are invalid at this point, try using them and - // see what compiler error you get! -}
-
Мы не можем использовать переменные field_name
и field_value
после того, как их значения были перемещены в HashMap вызовом способа insert
.
Если мы вставим в HashMap ссылки на значения, то они не будут перемещены в HashMap. Значения, на которые указывают ссылки, должны быть действительными хотя бы до тех пор, пока хеш-карта действительна. Мы поговорим подробнее об этих вопросах в разделе "Валидация ссылок при помощи времён жизни" главы 10.
-Хотя количество ключей и значений может увеличиваться в HashMap, каждый ключ может иметь только одно значение, связанное с ним в один мгновение времени (обратное утверждение неверно: приказы "Blue" и "Yellow" могут хранить в хеш-карте scores
одинаковое количество очков, например 10).
Когда вы хотите изменить данные в хеш-карте, необходимо решить, как обрабатывать случай, когда ключ уже имеет назначенное значение. Можно заменить старое значение новым, полностью пренебрегая старое. Можно сохранить старое значение и пренебрегать новое, или добавлять новое значение, если только ключ ещё не имел значения. Или можно было бы объединить старое значение и новое значение. Давайте посмотрим, как сделать каждый из исходов!
-Если мы вставим ключ и значение в HashMap, а затем вставим тот же ключ с новым значением, то старое значение связанное с этим ключом, будет заменено на новое. Даже несмотря на то, что код в приложении 8-23 вызывает insert
дважды, хеш-карта будет содержать только одну пару ключ/значение, потому что мы вставляем значения для одного и того же ключа - ключа приказы "Blue".
-fn main() { - use std::collections::HashMap; - - let mut scores = HashMap::new(); - - scores.insert(String::from("Blue"), 10); - scores.insert(String::from("Blue"), 25); - - println!("{scores:?}"); -}
-
Код напечатает {"Blue": 25}
. Начальное значение 10
было перезаписано.
Обычно проверяют, существует ли определенный ключ в хеш-карте со значением, а затем предпринимаются следующие действия: если ключ существует в хеш-карте, существующее значение должно оставаться таким, какое оно есть. Если ключ не существует, то вставляют его и значение для него.
-Хеш-карты имеют для этого особый API, называемый entry
, который принимает ключ для проверки в качестве входного свойства. Возвращаемое значение способа entry
- это перечисление Entry
, с двумя исходами: первый представляет значение, которое может существовать, а второй говорит о том, что значение отсутствует. Допустим, мы хотим проверить, имеется ли ключ и связанное с ним значение для приказы "Yellow". Если хеш-карта не имеет значения для такого ключа, то мы хотим вставить значение 50. То же самое мы хотим проделать и для приказы "Blue". Используем API entry
в коде приложения 8-24.
-fn main() { - use std::collections::HashMap; - - let mut scores = HashMap::new(); - scores.insert(String::from("Blue"), 10); - - scores.entry(String::from("Yellow")).or_insert(50); - scores.entry(String::from("Blue")).or_insert(50); - - println!("{scores:?}"); -}
-
Способ or_insert
определён в Entry
так, чтобы возвращать изменяемую ссылку на соответствующее значение ключа внутри исхода перечисления Entry
, когда этот ключ существует, а если его нет, то вставлять свойство в качестве нового значения этого ключа и возвращать изменяемую ссылку на новое значение. Эта техника намного чище, чем самостоятельное написание логики и, кроме того, она более безопасна и согласуется с правилами заимствования.
При выполнении кода приложения 8-24 будет напечатано {"Yellow": 50, "Blue": 10}
. Первый вызов способа entry
вставит ключ для приказы "Yellow" со значением 50, потому что для жёлтой приказы ещё не имеется значения в HashMap. Второй вызов entry
не изменит хеш-карту, потому что для ключа приказы "Blue" уже имеется значение 10.
Другим распространённым исходом использования хеш-карт является поиск значения по ключу, а затем обновление этого значения на основе старого значения. Например, в приложении 8-25 показан код, который подсчитывает, сколько раз определённое слово встречается в некотором тексте. Мы используем HashMap со словами в качестве ключей и увеличиваем соответствующее слову значение, чтобы отслеживать, сколько раз мы встретили это слово. Если мы впервые встретили слово, то сначала вставляем значение 0.
--fn main() { - use std::collections::HashMap; - - let text = "hello world wonderful world"; - - let mut map = HashMap::new(); - - for word in text.split_whitespace() { - let count = map.entry(word).or_insert(0); - *count += 1; - } - - println!("{map:?}"); -}
-
Этот код напечатает {"world": 2, "hello": 1, "wonderful": 1}
. Если вы увидите, что пары ключ/значение печатаются в другом порядке, то вспомните, что мы писали в разделы "Доступ к данным в HashMap", что повторение по хеш-карте происходит в произвольном порядке.
Способ split_whitespace
возвращает повторитель по срезам строки, разделённых пробелам, для строки text
. Способ or_insert
возвращает изменяемую ссылку (&mut V
) на значение ключа. Мы сохраняем изменяемую ссылку в переменной count
, для этого, чтобы присвоить переменной значение, необходимо произвести разыменование с помощью звёздочки (*). Изменяемая ссылка удаляется сразу же после выхода из области видимости цикла for
, поэтому все эти изменения безопасны и согласуются с правилами заимствования.
По умолчанию HashMap
использует функцию хеширования SipHash, которая может противостоять атакам класса отказ в обслуживании, Denial of Service (DoS) с использованием хеш-таблиц siphash. Это не самый быстрый из возможных алгоритмов хеширования, в данном случае производительность идёт на соглашение с обеспечением лучшей безопасности. Если после профилирования вашего кода окажется, что хеш-функция, используемая по умолчанию, очень медленная, вы можете заменить её используя другой hasher. Hasher - это вид, выполняющий особенность BuildHasher
. Подробнее о особенностях мы поговорим в Главе 10. Вам совсем не обязательно выполнить свою собственную функцию хеширования; crates.io имеет достаточное количество библиотек, предоставляющих разные выполнения hasher с множеством общих алгоритмов хеширования.
Векторы, строки и хеш-карты предоставят большое количество возможностей для программ, когда необходимо сохранять, получать доступ и изменять данные. Теперь вы готовы решить следующие учебные задания:
-Документация API встроенной библиотеки описывает способы у векторов, строк и HashMap. Советуем воспользоваться ей при решении упражнений.
-Потихоньку мы переходим к более сложным программам, в которых действия могут потерпеть неудачу. Наступило наилучшее время для обсуждения обработки ошибок.
- -Возникновение ошибок в ходе выполнения программ — это суровая действительность в жизни программного обеспечения, поэтому Ржавчина имеет ряд функций для обработки случаев, в которых что-то идёт не так. Во многих случаях Ржавчина требует, чтобы вы признали возможность ошибки и предприняли некоторые действия, прежде чем ваш код будет собран. Это требование делает вашу программу более надёжной, обеспечивая, что вы обнаружите ошибки и обработаете их надлежащим образом, прежде чем развернёте свой код в производственной среде!
-В Ржавчина ошибки объединяются на две основные разряды: исправимые (recoverable) и неисправимые (unrecoverable). В случае исправимой ошибки, такой как файл не найден, мы, скорее всего, просто хотим сообщить о неполадке пользователю и повторить действие. Неисправимые ошибки всегда являются симптомами изъянов в коде, например, попытка доступа к ячейке за пределами границ массива, и поэтому мы хотим немедленно остановить программу.
-Большинство языков не различают эти два вида ошибок и обрабатывают оба вида одинаково, используя такие рычаги, как исключения. В Ржавчина нет исключений. Вместо этого он имеет вид Result<T, E>
для обрабатываемых (исправимых) ошибок и макрос panic!
, который останавливает выполнение, когда программа встречает необрабатываемую (неисправимую) ошибку. Сначала эта глава расскажет про вызов panic!
, а потом расскажет о возврате значений Result<T, E>
. Кроме того, мы рассмотрим, что нужно учитывать при принятии решения о том, следует ли попытаться исправить ошибку или остановить выполнение.
panic!
Иногда в коде происходят плохие вещи, и вы ничего не можете с этим поделать. В этих случаях у Ржавчина есть макрос panic! В действительностисуществует два способа вызвать панику: путём выполнения действия, которое вызывает панику в нашем коде (например, обращение к массиву за пределами его размера) или путём явного вызова макроса panic!
. В обоих случаях мы вызываем панику в нашей программе. По умолчанию паника выводит сообщение об ошибке, раскручивает и очищает обойма вызовов, и завершают работу. С помощью переменной окружения вы также можете заставить Ржавчина отображать обойма вызовов при возникновении паники, чтобы было легче отследить источник паники.
--Раскручивать обойма или прерывать выполнение программы в ответ на панику?
-По умолчанию, когда происходит паника, программа начинает этап раскрутки обоймы, означающий в Ржавчина проход обратно по обойме вызовов и очистку данных для каждой обнаруженной функции. Тем не менее, этот обратный проход по обойме и очистка порождают много работы. Ржавчина как иное решение предоставляет вам возможность немедленного прерывания (aborting), которое завершает работу программы без очистки.
-Память, которую использовала программа, должна быть очищена операционной системой. Если в вашем деле нужно насколько это возможно сделать маленьким исполняемый файл, вы можете переключиться с исхода раскрутки обоймы на исход прерывания при панике, добавьте
-panic = 'abort'
в раздел [profile] вашегоCargo.toml
файла. Например, если вы хотите прервать панику в режиме исполнения, добавьте это:-[profile.release] -panic = 'abort' -
Давайте попробуем вызвать panic!
в простой программе:
Файл: src/main.rs
--fn main() { - panic!("crash and burn"); -}
При запуске программы, вы увидите что-то вроде этого:
-$ cargo run
- Compiling panic v0.1.0 (file:///projects/panic)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
- Running `target/debug/panic`
-thread 'main' panicked at src/main.rs:2:5:
-crash and burn
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-Выполнение макроса panic!
вызывает сообщение об ошибке, содержащееся в двух последних строках. Первая строка показывает сообщение паники и место в исходном коде, где возникла паника: src/main.rs:2:5 указывает, что это вторая строка, пятый символ внутри нашего файла src/main.rs
В этом случае указанная строка является частью нашего кода, и если мы перейдём к этой строке, мы увидим вызов макроса panic!
. В других случаях вызов panic!
мог бы произойти в стороннем коде, который вызывает наш код, тогда имя файла и номер строки для сообщения об ошибке будет из чужого кода, где макрос panic!
выполнен, а не из строк нашего кода, которые в конечном итоге привели к выполнению panic!
. Мы можем использовать обратную трассировку вызовов функций которые вызвали panic!
чтобы выяснить, какая часть нашего кода вызывает неполадку. Мы обсудим обратную трассировку более подробно далее.
panic!
Давайте посмотрим на другой пример, где, вызов panic!
происходит в сторонней библиотеке из-за ошибки в нашем коде (а не как в примере ранее, из-за вызова макроса нашим кодом напрямую). В приложении 9-1 приведён код, который пытается получить доступ по порядковому указателю в векторе за пределами допустимого рядазначений порядкового указателя.
Файл: src/main.rs
--fn main() { - let v = vec![1, 2, 3]; - - v[99]; -}
-
Здесь мы пытаемся получить доступ к 100-му элементу вектора (который находится по порядковому указателю 99, потому что упорядочевание начинается с нуля), но вектор имеет только 3 элемента. В этой случаи, Ржавчина будет вызывать панику. Использование []
должно возвращать элемент, но вы передаёте неверный порядковый указатель: не существует элемента, который Ржавчина мог бы вернуть.
В языке C, например, попытка прочесть за пределами конца устройства данных (в нашем случае векторе) приведёт к неопределённому поведению, undefined behavior, UB. Вы всё равно получите значение, которое находится в том месте памяти компьютера, которое соответствовало бы этому элементу в векторе, несмотря на то, что память по тому адресу совсем не принадлежит вектору (всё просто: C рассчитал бы место хранения элемента с порядковым указателем 99 и считал бы то, что там хранится, упс). Это называется чтением за пределом буфера, buffer overread, и может привести к уязвимостям безопасности. Если злоумышленник может управлять порядковым указателем таким образом, то у него появляется возможность читать данные, которые он не должен иметь возможности читать.
-Чтобы защитить вашу программу от такого рода уязвимостей при попытке прочитать элемент с порядковым указателем, которого не существует, Ржавчина остановит выполнение и откажется продолжить работу программы. Давайте попробуем так сделать и посмотрим на поведение Rust:
-$ cargo run
- Compiling panic v0.1.0 (file:///projects/panic)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
- Running `target/debug/panic`
-thread 'main' panicked at src/main.rs:4:6:
-index out of bounds: the len is 3 but the index is 99
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Следующая строка говорит, что мы можем установить переменную среды RUST_BACKTRACE
, чтобы получить обратную трассировку того, что именно стало причиной ошибки. Обратная трассировка создаёт список всех функций, которые были вызваны до какой-то определённой точки выполнения программы. Обратная трассировка в Ржавчина работает так же, как и в других языках. По этому предлагаем вам читать данные обратной трассировки как и везде - читать сверху вниз, пока не увидите сведения о файлах написанных вами. Это место, где возникла неполадка. Другие строки, которые выше над строками с упоминанием наших файлов, - это код, который вызывается нашим кодом; строки ниже являются кодом, который вызывает наш код. Эти строки могут включать основной код Rust, код встроенной библиотеки или используемые ящики. Давайте попробуем получить обратную трассировку с помощью установки переменной среды RUST_BACKTRACE
в любое значение, кроме 0. Приложение 9-2 показывает вывод, подобный тому, что вы увидите.
$ RUST_BACKTRACE=1 cargo run
-thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
-stack backtrace:
- 0: rust_begin_unwind
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
- 1: core::panicking::panic_fmt
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
- 2: core::panicking::panic_bounds_check
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
- 3: <usize as core::slice::index::SliceIndex<[T]>>::index
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
- 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
- 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
- 6: panic::main
- at ./src/main.rs:4:5
- 7: core::ops::function::FnOnce::call_once
- at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
-note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
-
--
Тут много вывода! Вывод, который вы увидите, может отличаться от представленного, в зависимости от вашей операционной системы и исполнения Rust. Для того, чтобы получить обратную трассировку с этой сведениями, должны быть включены символы отладки, debug symbols. Символы отладки включены по умолчанию при использовании cargo build
или cargo run
без флага --release
, как у нас в примере.
В выводе обратной трассировки приложения 9-2, строка #6 указывает на строку в нашем деле, которая вызывала неполадку: строка 4 из файла src/main.rs. Если мы не хотим, чтобы наша программа запаниковала, мы должны начать исследование с места, на которое указывает первая строка с упоминанием нашего файла. В приложении 9-1, где мы для отображения обратной трассировки сознательно написали код, который паникует, способ исправления паники состоит в том, чтобы не запрашивать элемент за пределами ряда значений порядковых указателей вектора. Когда ваш код запаникует в будущем, вам нужно будет выяснить, какое выполняющееся кодом действие, с какими значениями вызывает панику и что этот код должен делать вместо этого.
-Мы вернёмся к обсуждению макроса panic!
, и того когда нам следует и не следует использовать panic!
для обработки ошибок в разделе "panic!
или НЕ panic!
" этой главы. Далее мы рассмотрим, как восстановить выполнение программы после исправляемых ошибок, использующих вид Result
.
Result
Многие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная преобразование и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить случай и создать новый файл вместо остановки программы.
-Вспомните раздел ["Обработка возможного сбоя с помощью Result
"] главы 2: мы использовали там перечисление Result
, имеющее два исхода. Ok
и Err
для обработки сбоев. Само перечисление определено следующим образом:
-#![allow(unused)] -fn main() { -enum Result<T, E> { - Ok(T), - Err(E), -} -}
Виды T
и E
являются свойствами обобщённого вида: мы обсудим обобщённые виды более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что T
представляет вид значения, которое будет возвращено в случае успеха внутри исхода Ok
, а E
представляет вид ошибки, которая будет возвращена при сбое внутри исхода Err
. Так как вид Result
имеет эти обобщённые свойства (generic type parameters), мы можем использовать вид Result
и функции, которые определены для него, в разных случаейх, когда вид успешного значение и значения ошибки, которые мы хотим вернуть, отличаются.
Давайте вызовем функцию, которая возвращает значение Result
, потому что может потерпеть неудачу. В приложении 9-3 мы пытаемся открыть файл.
Файл: src/main.rs
--use std::fs::File; - -fn main() { - let greeting_file_result = File::open("hello.txt"); -}
-
File::open
возвращает значения вида Result<T, E>
. Гибкий вид T
в выполнения File::open
соответствует виду успешно полученного значения, std::fs::File
, а именно указателю файла. Вид E
, используемый для значения в случае возникновения ошибки, - std::io::Error
. Такой возвращаемый вид означает, что вызов File::open
может быть успешным и вернуть указатель файла, из которого мы можем читать или в который можем писать. Также вызов функции может завершиться неудачей: например, файл может не существовать, или у нас может не быть разрешения на доступ к файлу. Функция File::open
должна иметь способ сообщить нам об успехе или неудаче и в то же время дать нам либо указатель файла, либо сведения об ошибке. Эту возможность как раз и предоставляет перечисление Result
.
В случае успеха File::open
значением переменной greeting_file_result
будет образец Ok
, содержащий указатель файла. В случае неудачи значение в переменной greeting_file_result
будет образцом Err
, содержащим дополнительную сведения о том, какая именно ошибка произошла.
Необходимо дописать в код приложения 9-3 выполнение разных действий в зависимости от значения, которое вернёт вызов File::open
. Приложение 9-4 показывает один из способов обработки Result
- пользуясь основным средством языка, таким как выражение match
, рассмотренным в Главе 6.
Файл: src/main.rs
--use std::fs::File; - -fn main() { - let greeting_file_result = File::open("hello.txt"); - - let greeting_file = match greeting_file_result { - Ok(file) => file, - Err(error) => panic!("Problem opening the file: {error:?}"), - }; -}
-
Обратите внимание, что также как перечисление Option
, перечисление Result
и его исходы, входят в область видимости благодаря авто-подключения (prelude), поэтому не нужно указывать Result::
перед использованием исходов Ok
и Err
в ветках выражения match
.
Если итогом будет Ok
, этот код вернёт значение file
из исхода Ok
, а мы затем присвоим это значение файлового указателя переменной greeting_file
. После match
мы можем использовать указатель файла для чтения или записи.
Другая ветвь match
обрабатывает случай, где мы получаем значение Err
после вызова File::open
. В этом примере мы решили вызвать макрос panic!
. Если в нашей текущей папки нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic!
:
$ cargo run
- Compiling error-handling v0.1.0 (file:///projects/error-handling)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
- Running `target/debug/error-handling`
-thread 'main' panicked at src/main.rs:8:23:
-Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Как обычно, данное сообщение точно говорит, что пошло не так.
-Код в приложении 9-4 будет вызывать panic!
независимо от того, почему вызов File::open
не удался. Однако мы хотим предпринять различные действия для разных причин сбоя. Если открытие File::open
не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его указатель. Если вызов File::open
не удался по любой другой причине - например, потому что у нас не было прав на открытие файла, то все равно мы хотим вызвать panic!
как у нас сделано в приложении 9-4. Для этого мы добавляем выражение внутреннего match
, показанное в приложении 9-5.
Файл: src/main.rs
- -use std::fs::File;
-use std::io::ErrorKind;
-
-fn main() {
- let greeting_file_result = File::open("hello.txt");
-
- let greeting_file = match greeting_file_result {
- Ok(file) => file,
- Err(error) => match error.kind() {
- ErrorKind::NotFound => match File::create("hello.txt") {
- Ok(fc) => fc,
- Err(e) => panic!("Problem creating the file: {e:?}"),
- },
- other_error => {
- panic!("Problem opening the file: {other_error:?}");
- }
- },
- };
-}
--
Видом значения возвращаемого функцией File::open
внутри Err
исхода является io::Error
, устройства из встроенной библиотеки. Данная устройства имеет способ kind
, который можно вызвать для получения значения io::ErrorKind
. Перечисление io::ErrorKind
из встроенной библиотеки имеет исходы, представляющие различные виды ошибок, которые могут появиться при выполнении действий в io
. Исход, который мы хотим использовать, это ErrorKind::NotFound
, который даёт сведения, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление образца с переменной greeting_file_result
и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind()
ошибки.
Условие, которое мы хотим проверить во внутреннем match
, заключается в том, является ли значение, возвращаемое error.kind()
, исходом NotFound
перечисления ErrorKind
. Если это так, мы пытаемся создать файл с помощью функции File::create
. Однако, поскольку вызов File::create
тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки, теперь уже во внутреннем выражении match
. Заметьте: если файл не может быть создан, выводится другое, особое сообщение об ошибке. Вторая же ветка внешнего match
(который обрабатывает вызов error.kind()
), остаётся той же самой - в итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла.
--Иные использованию
-match
сResult<T, E>
Как много
-match
! Выражениеmatch
является очень полезным, но в то же время довольно простым. В главе 13 вы узнаете о замыканиях (closures), которые используются во многих способах видаResult<T, E>
. Эти способы помогают быть более кратким, чем использованиеmatch
при работе со значениямиResult<T, E>
в вашем коде.Например, вот другой способ написать ту же логику, что показана в Приложении 9-5, но с использованием замыканий и способа
- -unwrap_or_else
:-use std::fs::File; -use std::io::ErrorKind; - -fn main() { - let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { - if error.kind() == ErrorKind::NotFound { - File::create("hello.txt").unwrap_or_else(|error| { - panic!("Problem creating the file: {:?}", error); - }) - } else { - panic!("Problem opening the file: {:?}", error); - } - }); -}
Несмотря на то, что данный код имеет такое же поведение как в приложении 9-5, он не содержит ни одного выражения
-match
и проще для чтения. Советуем вам вернуться к примеру этого раздела после того как вы прочитаете Главу 13 и изучите способunwrap_or_else
по документации встроенной библиотеки. Многие из способов о которых вы узнаете в документации и Главе 13 могут очистить код от больших, вложенных выраженийmatch
при обработке ошибок.
unwrap
и expect
Использование match
работает достаточно хорошо, но может быть довольно многословным и не всегда хорошо передаёт смысл. Вид Result<T, E>
имеет множество вспомогательных способов для выполнения различных, более отличительных задач. Способ unwrap
- это способ быстрого доступа к значениям, выполненный так же, как и выражение match
, которое мы написали в Приложении 9-4. Если значение Result
является исходом Ok
, unwrap
возвращает значение внутри Ok
. Если Result
- исход Err
, то unwrap
вызовет для нас макрос panic!
. Вот пример unwrap
в действии:
Файл: src/main.rs
--use std::fs::File; - -fn main() { - let greeting_file = File::open("hello.txt").unwrap(); -}
Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic!
способа unwrap
:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
-code: 2, kind: NotFound, message: "No such file or directory" }',
-src/main.rs:4:49
-
-Другой способ, похожий на unwrap
, это expect
, позволяющий указать сообщение об ошибке для макроса panic!
. Использование expect
вместо unwrap
с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. правила написания способа expect
выглядит так:
Файл: src/main.rs
--use std::fs::File; - -fn main() { - let greeting_file = File::open("hello.txt") - .expect("hello.txt should be included in this project"); -}
expect
используется так же как и unwrap
: либо возвращается указатель файла либо вызывается макрос panic!
.
Наше сообщение об ошибке в expect
будет передано в panic!
и заменит обычное используемое сообщение.
Вот как это выглядит:
thread 'main' panicked at 'hello.txt should be included in this project: Os {
-code: 2, kind: NotFound, message: "No such file or directory" }',
-src/main.rs:5:10
-
-В рабочем коде, большинство выбирает expect
в угоду unwrap
и добавляет описание, почему действие должна закончиться успешно. Но даже если предположение оказалось неверным, сведений для отладки будет больше.
Когда вы пишете функцию, выполнение которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как распространение ошибки (propagating the error). Благодаря нему мы даём больше управления вызывающему коду, где может быть больше сведений или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки.
-Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.
-Файл: src/main.rs
- --#![allow(unused)] -fn main() { -use std::fs::File; -use std::io::{self, Read}; - -fn read_username_from_file() -> Result<String, io::Error> { - let username_file_result = File::open("hello.txt"); - - let mut username_file = match username_file_result { - Ok(file) => file, - Err(e) => return Err(e), - }; - - let mut username = String::new(); - - match username_file.read_to_string(&mut username) { - Ok(_) => Ok(username), - Err(e) => Err(e), - } -} -}
-
Эта функция может быть написана гораздо более коротким способом, но мы начнём с того, что многое сделаем вручную, чтобы изучить обработку ошибок; а в конце покажем более короткий способ. Давайте сначала рассмотрим вид возвращаемого значения: Result<String, io::Error>
. Здесь есть возвращаемое значение функции вида Result<T, E>
где образцовый свойство T
был заполнен определенным видом String
и образцовый свойство E
был заполнен определенным видом io::Error
.
Если эта функция выполнится без неполадок. то код, вызывающий эту функцию, получит значение Ok
, содержащее String
- имя пользователя, которое эта функция прочитала из файла. Если функция столкнётся с какими-либо неполадками, вызывающий код получит значение Err
, содержащее образец io::Error
, который включает дополнительную сведения о том, какие сбоев возникли. Мы выбрали io::Error
в качестве возвращаемого вида этой функции, потому что это вид значения ошибки, возвращаемого из обеих действий, которые мы вызываем в теле этой функции и которые могут завершиться неудачей: функция File::open
и способ read_to_string
.
Тело функции начинается с вызова File::open
. Затем мы обрабатываем значение Result
с помощью match
, подобно match
из приложения 9-4. Если File::open
завершается успешно, то указатель файла в переменной образца file
становится значением в изменяемой переменной username_file
и функция продолжит свою работу. В случае Err
, вместо вызова panic!
, мы используем ключевое слово return
для досрочного возврата из функции и передаём значение ошибки из File::open
, которое теперь находится в переменной образца e
, обратно в вызывающий код как значение ошибки этой функции.
Таким образом, если у нас есть файловый указатель в username_file
, функция создаёт новую String
в переменной username
и вызывает способ read_to_string
для файлового указателя в username_file
, чтобы прочитать содержимое файла в username
. Способ read_to_string
также возвращает Result
, потому что он может потерпеть неудачу, даже если File::open
завершился успешно. Поэтому нам нужен ещё один match
для обработки этого Result
: если read_to_string
завершится успешно, то наша функция сработала, и мы возвращаем имя пользователя из файла, которое теперь находится в username
, обёрнутое в Ok
. Если read_to_string
потерпит неудачу, мы возвращаем значение ошибки таким же образом, как мы возвращали значение ошибки в match
, который обрабатывал возвращаемое значение File::open
. Однако нам не нужно явно указывать return
, потому что это последнее выражение в функции.
Затем код, вызывающий этот, будет обрабатывать получение либо значения Ok
, содержащего имя пользователя, либо значения Err
, содержащего io::Error
. Вызывающий код должен решить, что делать с этими значениями. Если вызывающий код получает значение Err
, он может вызвать panic!
и завершить работу программы, использовать имя пользователя по умолчанию или найти имя пользователя, например, не в файле. У нас недостаточно сведений о том, что на самом деле пытается сделать вызывающий код, поэтому мы распространяем всю сведения об успехах или ошибках вверх, чтобы она могла обрабатываться соответствующим образом.
Эта схема передачи ошибок настолько распространена в Rust, что Ржавчина предоставляет оператор вопросительного знака ?
, чтобы облегчить эту задачу.
?
В приложении 9-7 показана выполнение read_username_from_file
, которая имеет ту же возможность, что и в приложении 9-6, но в этой выполнения используется оператор ?
.
Файл: src/main.rs
- --#![allow(unused)] -fn main() { -use std::fs::File; -use std::io::{self, Read}; - -fn read_username_from_file() -> Result<String, io::Error> { - let mut username_file = File::open("hello.txt")?; - let mut username = String::new(); - username_file.read_to_string(&mut username)?; - Ok(username) -} -}
-
Выражение ?
, расположенное после Result
, работает почти так же, как и те выражения match
, которые мы использовали для обработки значений Result
в приложении 9-6. Если в качестве значения Result
будет Ok
, то значение внутри Ok
будет возвращено из этого выражения, и программа продолжит работу. Если же значение представляет собой Err
, то Err
будет возвращено из всей функции, как если бы мы использовали ключевое слово return
, так что значение ошибки будет передано в вызывающий код.
Существует разница между тем, что делает выражение match
из приложения 9-6 и тем, что делает оператор ?
: значения ошибок, для которых вызван оператор ?
, проходят через функцию from
, определённую в особенности From
встроенной библиотеки, которая используется для преобразования значений из одного вида в другой. Когда оператор ?
вызывает функцию from
, полученный вид ошибки преобразуется в вид ошибки, определённый в возвращаемом виде текущей функции. Это полезно, когда функция возвращает только один вид ошибки, для описания всех возможных исходов сбоев, даже если её отдельные составляющие могут выходить из строя по разным причинам.
Например, мы могли бы изменить функцию read_username_from_file
в приложении 9-7, чтобы возвращать пользовательский вид ошибки с именем OurError
, который мы определим. Если мы также определим impl From<io::Error> for OurError
для создания образца OurError
из io::Error
, то оператор ?
, вызываемый в теле read_username_from_file
, вызовет from
и преобразует виды ошибок без необходимости добавления дополнительного кода в функцию.
В случае приложения 9-7 оператор ?
в конце вызова File::open
вернёт значение внутри Ok
в переменную username_file
. Если произойдёт ошибка, оператор ?
выполнит ранний возврат значения Err
вызывающему коду. То же самое относится к оператору ?
в конце вызова read_to_string
.
Оператор ?
позволяет избавиться от большого количества образцового кода и упростить выполнение этой функции. Мы могли бы даже ещё больше сократить этот код, если бы использовали цепочку вызовов способов сразу после ?
, как показано в приложении 9-8.
Файл: src/main.rs
- --#![allow(unused)] -fn main() { -use std::fs::File; -use std::io::{self, Read}; - -fn read_username_from_file() -> Result<String, io::Error> { - let mut username = String::new(); - - File::open("hello.txt")?.read_to_string(&mut username)?; - - Ok(username) -} -}
-
Мы перенесли создание новой String
в username
в начало функции; эта часть не изменилась. Вместо создания переменной username_file
мы соединили вызов read_to_string
непосредственно с итогом File::open("hello.txt")?
. У нас по-прежнему есть ?
в конце вызова read_to_string
, и мы по-прежнему возвращаем значение Ok
, содержащее username
, когда и File::open
и read_to_string
завершаются успешно, а не возвращают ошибки. Возможность снова такая же, как в Приложении 9-6 и Приложении 9-7; это просто другой, более удобный способ её написания.
Продолжая рассматривать разные способы записи данной функции, приложение 9-9 отображает способ сделать её ещё короче с помощью fs::read_to_string
.
Файл: src/main.rs
- --#![allow(unused)] -fn main() { -use std::fs; -use std::io; - -fn read_username_from_file() -> Result<String, io::Error> { - fs::read_to_string("hello.txt") -} -}
-
Чтение файла в строку довольно распространённая действие, так что обычная библиотека предоставляет удобную функцию fs::read_to_string
, которая открывает файл, создаёт новую String
, читает содержимое файла, размещает его в String
и возвращает её. Конечно, использование функции fs::read_to_string
не даёт возможности объяснить обработку всех ошибок, поэтому мы сначала изучили длинный способ.
?
Оператор ?
может использоваться только в функциях, вид возвращаемого значения которых совместим со значением, для которого используется ?
. Это потому, что оператор ?
определён для выполнения раннего возврата значения из функции таким же образом, как и выражение match
, которое мы определили в приложении 9-6. В приложении 9-6 match
использовало значение Result
, а ответвление с ранним возвратом вернуло значение Err(e)
. Вид возвращаемого значения функции должен быть Result
, чтобы он был совместим с этим return
.
В приложении 9-10 давайте посмотрим на ошибку, которую мы получим, если воспользуемся оператором ?
в функции main
с видом возвращаемого значения, несовместимым с видом значения, для которого мы используем ?
:
Файл: src/main.rs
-use std::fs::File;
-
-fn main() {
- let greeting_file = File::open("hello.txt")?;
-}
--
Этот код открывает файл, что может привести к сбою. ?
оператор следует за значением Result
, возвращаемым File::open
, но эта main
функция имеет возвращаемый вид ()
, а не Result
. Когда мы собираем этот код, мы получаем следующее сообщение об ошибке:
$ cargo run
- Compiling error-handling v0.1.0 (file:///projects/error-handling)
-error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
- --> src/main.rs:4:48
- |
-3 | fn main() {
- | --------- this function should return `Result` or `Option` to accept `?`
-4 | let greeting_file = File::open("hello.txt")?;
- | ^ cannot use the `?` operator in a function that returns `()`
- |
- = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
-
-Эта ошибка указывает на то, что оператор ?
разрешено использовать только в функции, которая возвращает Result
, Option
или другой вид, выполняющий FromResidual
.
Для исправления ошибки есть два исхода. Первый - изменить возвращаемый вид вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор ?
, если у вас нет ограничений, препятствующих этому. Другой способ - использовать match
или один из способов Result<T, E>
для обработки Result<T, E>
любым подходящим способом.
В сообщении об ошибке также упоминалось, что ?
можно использовать и со значениями Option<T>
. Как и при использовании ?
для Result
, вы можете использовать ?
только для Option
в функции, которая возвращает Option
. Поведение оператора ?
при вызове Option<T>
похоже на его поведение при вызове Result<T, E>
: если значение равно None
, то None
будет возвращено раньше из функции в этот мгновение. Если значение Some
, значение внутри Some
является результирующим значением выражения, и функция продолжает исполняться. В приложении 9-11 приведён пример функции, которая находит последний символ первой строки заданного текста:
-fn last_char_of_first_line(text: &str) -> Option<char> { - text.lines().next()?.chars().last() -} - -fn main() { - assert_eq!( - last_char_of_first_line("Hello, world\nHow are you today?"), - Some('d') - ); - - assert_eq!(last_char_of_first_line(""), None); - assert_eq!(last_char_of_first_line("\nhi"), None); -}
-
Эта функция возвращает Option<char>
, потому что возможно, что там есть символ, но также возможно, что его нет. Этот код принимает переменная среза text
строки и вызывает для него способ lines
, который возвращает повторитель для строк в строке. Поскольку эта функция хочет проверить первую строку, она вызывает next
у повторителя, чтобы получить первое значение от повторителя. Если text
является пустой строкой, этот вызов next
вернёт None
, и в этом случае мы используем ?
чтобы остановить и вернуть None
из last_char_of_first_line
. Если text
не является пустой строкой, next
вернёт значение Some
, содержащее отрывок строки первой строки в text
.
Символ ?
извлекает отрывок строки, и мы можем вызвать chars
для этого отрывка строки. чтобы получить повторитель символов. Нас важно последний символ в первой строке, поэтому мы вызываем last
, чтобы вернуть последний элемент в повторителе. Вернётся Option
, потому что возможно, что первая строка пустая - например, если text
начинается с пустой строки, но имеет символы в других строках, как в "\nhi"
. Однако, если в первой строке есть последний символ, он будет возвращён в исходе Some
. Оператор ?
в середине даёт нам краткий способ выразить эту логику, позволяя выполнить функцию в одной строке. Если бы мы не могли использовать оператор ?
в Option
, нам пришлось бы выполнить эту логику, используя больше вызовов способов или выражение match
.
Обратите внимание, что вы можете использовать оператор ?
Result
в функции, которая возвращает Result
, и вы можете использовать оператор ?
для Option
в функции, которая возвращает Option
, но вы не можете смешивать и сопоставлять. Оператор ?
не будет самостоятельно преобразовывать Result
в Option
или наоборот; в этих случаях вы можете использовать такие способы, как способ ok
для Result
или способ ok_or
для Option
, чтобы выполнить преобразование явно.
До сих пор все функции main
, которые мы использовали, возвращали ()
. Функция main
- особенная, потому что это точка входа и выхода исполняемых программ, и существуют ограничения на вид возвращаемого значения, чтобы программы вели себя так, как ожидается.
К счастью, main
также может возвращать Result<(), E>
. В приложении 9-12 используется код из приложения 9-10, но мы изменили возвращаемый вид main
на Result<(), Box<dyn Error>>
и добавили возвращаемое значение Ok(())
в конец. Теперь этот код будет собран:
use std::error::Error;
-use std::fs::File;
-
-fn main() -> Result<(), Box<dyn Error>> {
- let greeting_file = File::open("hello.txt")?;
-
- Ok(())
-}
--
Вид Box<dyn Error>
является особенность-предметом, о котором мы поговорим в разделе "Использование особенность-предметов, допускающих значения разных видов" в главе 17. Пока что вы можете считать, что Box<dyn Error>
означает "любой вид ошибки". Использование ?
для значения Result
в функции main
с видом ошибки Box<dyn Error>
разрешено, так как позволяет вернуть любое значение Err
раньше времени. Даже если тело этой функции main
будет возвращать только ошибки вида std::io::Error
, указав Box<dyn Error>
, эта ярлык останется правильной, даже если в тело main
будет добавлен код, возвращающий другие ошибки.
Когда main
функция возвращает Result<(), E>
, исполняемый файл завершится со значением 0
, если main
вернёт Ok(())
, и выйдет с ненулевым значением, если main
вернёт значение Err
. Исполняемые файлы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число 0
, а программы с ошибкой возвращают целое число, отличное от 0
. Ржавчина также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением.
Функция main
может возвращать любые виды, выполняющие особенность std::process::Termination
, в которых имеется функция report
, возвращающая ExitCode
. Обратитесь к документации встроенной библиотеки за дополнительной сведениями о порядке выполнения особенности Termination
для ваших собственных видов.
Теперь, когда мы обсудили подробности вызова panic!
или возврата Result
, давайте вернёмся к тому, как решить, какой из случаев подходит для какой случаи.
panic!
или не panic!
Итак, как принимается решение о том, когда следует вызывать panic!
, а когда вернуть Result
? При панике код не имеет возможности восстановить своё выполнение. Можно было бы вызывать panic!
для любой ошибочной случаи, независимо от того, имеется ли способ восстановления или нет, но с другой стороны, вы принимаете решение от имени вызывающего вас кода, что случаей необратима. Когда вы возвращаете значение Result
, вы делегируете принятие решения вызывающему коду. Вызывающий код может попытаться выполнить восстановление способом, который подходит в данной случаи, или же он может решить, что из ошибки в Err
нельзя восстановиться и вызовет panic!
, превратив вашу исправимую ошибку в неисправимую. Поэтому возвращение Result
является хорошим выбором по умолчанию для функции, которая может дать сбой.
В таких случаей как примеры, протовиды и проверки, более уместно писать код, который паникует вместо возвращения Result
. Давайте рассмотрим почему, а затем мы обсудим случаи, в которых сборщик не может доказать, что ошибка невозможна, но вы, как человек, можете это сделать. Глава будет заканчиваться некоторыми общими руководящими принципами о том, как решить, стоит ли паниковать в коде библиотеки.
Когда вы пишете пример, отображающий некоторую подход, наличие хорошего кода обработки ошибок может сделать пример менее понятным. Понятно, что в примерах вызов способа unwrap
, который может привести к панике, является лишь обозначением способа обработки ошибок в приложении, который может отличаться в зависимости от того, что делает остальная часть кода.
Точно так же способы unwrap
и expect
являются очень удобными при создании протовида, прежде чем вы будете готовы решить, как обрабатывать ошибки. Они оставляют чёткие отступыв коде до особенности, когда вы будете готовы сделать программу более надёжной.
Если в проверке происходит сбой при вызове способа, то вы бы хотели, чтобы весь проверка не прошёл, даже если этот способ не является проверяемой возможностью. Поскольку вызов panic!
это способ, которым проверка помечается как провалившийся, использование unwrap
или expect
- именно то, что нужно.
Также было бы целесообразно вызывать unwrap
или expect
когда у вас есть какая-то другая логика, которая заверяет, что Result
будет иметь значение Ok
, но вашу логику не понимает сборщик. У вас по-прежнему будет значение Result
которое нужно обработать: любая действие, которую вы вызываете, все ещё имеет возможность неудачи в целом, хотя это логически невозможно в вашей именно случаи. Если, проверяя код вручную, вы можете убедиться, что никогда не будет исход с Err
, то вполне допустимо вызывать unwrap
, а ещё лучше задокументировать причину, по которой, по вашему мнению, у вас никогда не будет исхода Err
в тексте expect
. Вот пример:
-fn main() { - use std::net::IpAddr; - - let home: IpAddr = "127.0.0.1" - .parse() - .expect("Hardcoded IP address should be valid"); -}
Мы создаём образец IpAddr
, анализируя жёстко закодированную строку. Можно увидеть, что 127.0.0.1
является действительным IP-адресом, поэтому здесь допустимо использование expect
. Однако наличие жёстко закодированной допустимой строки не меняет вид возвращаемого значения способа parse
: мы все ещё получаем значение Result
и сборщик все также заставляет нас обращаться с Result
так, будто возможен исход Err
, потому что сборщик недостаточно умён, чтобы увидеть, что эта строка всегда действительный IP-адрес. Если строка IP-адреса пришла от пользователя, то она не является жёстко запрограммированной в программе и, следовательно, может привести к ошибке, мы определённо хотели бы обработать Result
более надёжным способом. Упоминание предположения о том, что этот IP-адрес жёстко закодирован, побудит нас изменить expect
для лучшей обработки ошибок, если в будущем нам потребуется вместо этого получить IP-адрес из какого-либо другого источника.
Желательно, чтобы код паниковал, если он может оказаться в неправильном состоянии. В этом среде неправильное состояние это когда некоторое допущение, заверение, договор или неизменная величина были нарушены. Например, когда недопустимые, противоречивые или пропущенные значения передаются в ваш код - плюс один или несколько пунктов из следующего перечисленного в списке:
-Если кто-то вызывает ваш код и передаёт значения, которые не имеют смысла, лучше всего вернуть ошибку, если вы это можете, чтобы пользователь библиотеки мог решить, что он хочет делать в этом случае. Однако в тех случаях, когда продолжение выполнения программы может быть небезопасным или вредным, лучшим выбором будет вызов panic!
и оповещение пользователя, использующего вашу библиотеку, об ошибке в его коде, чтобы он мог исправить её во время разработки. Подобно panic!
подходит, если вы вызываете внешний, неподуправлениеный вам код, и он возвращает недопустимое состояние, которое вы не можете исправить.
Однако, когда ожидается сбой, лучше вернуть Result
, чем выполнить вызов panic!
. В качестве примера можно привести синтаксический анализатор, которому передали неправильно созданные данные, или HTTP-запрос, возвращающий значение указывающий на то, что вы достигли ограничения на частоту запросов. В этих случаях возврат Result
означает, что ошибка является ожидаемой и вызывающий код должен решить, как её обрабатывать.
Когда ваш код выполняет действие, которая может подвергнуть пользователя риску, если она вызывается с использованием недопустимых значений, ваш код должен сначала проверить допустимость значений и паниковать, если значения недопустимы. Так советуется делать в основном из соображений безопасности: попытка оперировать неправильными данными может привести к уязвимостям. Это основная причина, по которой обычная библиотека будет вызывать panic!
, если попытаться получить доступ к памяти вне границ массива: доступ к памяти, не относящейся к текущей устройстве данных, является известной неполадкой безопасности. Функции часто имеют договоры: их поведение обеспечивается, только если входные данные отвечают определённым требованиям. Паника при нарушении договора имеет смысл, потому что это всегда указывает на изъян со стороны вызывающего кода, и это не ошибка, которую вы хотели бы, чтобы вызывающий код явно обрабатывал. На самом деле, нет разумного способа для восстановления вызывающего кода; программисты, вызывающие ваш код, должны исправить свой. Договоры для функции, особенно когда нарушение вызывает панику, следует описать в документации по API функции.
Тем не менее, наличие множества проверок ошибок во всех ваших функциях было бы многословным и раздражительным. К счастью, можно использовать систему видов Ржавчина (следовательно и проверку видов сборщиком), чтобы она сделала множество проверок вместо вас. Если ваша функция имеет определённый вид в качестве свойства, вы можете продолжить работу с логикой кода зная, что сборщик уже обеспечил правильное значение. Например, если используется обычный вид, а не вид Option
, то ваша программа ожидает наличие чего-то вместо ничего. Ваш код не должен будет обрабатывать оба исхода Some
и None
: он будет иметь только один исход для определённого значения. Код, пытающийся ничего не передавать в функцию, не будет даже собираться, поэтому ваша функция не должна проверять такой случай во время выполнения. Другой пример - это использование целого вида без знака, такого как u32
, который заверяет, что свойство никогда не будет отрицательным.
Давайте разовьём мысль использования системы видов Ржавчина чтобы убедиться, что у нас есть правильное значение, и рассмотрим создание пользовательского вида для валидации. Вспомним игру угадывания числа из Главы 2, в которой наш код просил пользователя угадать число между 1 и 100. Мы никогда не проверяли, что предположение пользователя лежит между этими числами, перед сравнением предположения с загаданным нами числом; мы только проверяли, что оно положительно. В этом случае последствия были не очень страшными: наши сообщения «Слишком много» или «Слишком мало», выводимые в окно вывода, все равно были правильными. Но было бы лучше подталкивать пользователя к правильным догадкам и иметь различное поведение для случаев, когда пользователь предлагает число за пределами ряда, и когда пользователь вводит, например, буквы вместо цифр.
-Один из способов добиться этого - пытаться разобрать введённое значение как i32
, а не как u32
, чтобы разрешить возможно отрицательные числа, а затем добавить проверку для нахождение числа в ряде, например, так:
use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- loop {
- // --snip--
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: i32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- if guess < 1 || guess > 100 {
- println!("The secret number will be between 1 and 100.");
- continue;
- }
-
- match guess.cmp(&secret_number) {
- // --snip--
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
-Выражение if
проверяет, находится ли наше значение вне ряда, сообщает пользователю о неполадке и вызывает continue
, чтобы начать следующую повторение цикла и попросить ввести другое число. После выражения if
мы можем продолжить сравнение значения guess
с загаданным числом, зная, что guess
лежит в ряде от 1 до 100.
Однако это не наилучшее решение: если бы было чрезвычайно важно, чтобы программа работала только со значениями от 1 до 100, существовало бы много функций, требующих этого, то такая проверка в каждой функции была бы утомительной (и могла бы отрицательно повлиять на производительность).
-Вместо этого можно создать новый вид и поместить проверки в функцию создания образца этого вида, не повторяя их везде. Таким образом, функции могут использовать новый вид в своих ярлыках и быть уверены в значениях, которые им передают. Приложение 9-13 показывает один из способов, как определить вид Guess
, чтобы образец Guess
создавался только при условии, что функция new
получает значение от 1 до 100.
-#![allow(unused)] - -fn main() { -}
-
Сначала мы определяем устройство с именем Guess
, которая имеет поле с именем value
вида i32
, в котором будет храниться число.
Затем мы выполняем сопряженную функцию new
, создающую образцы значений вида Guess
. Функция new
имеет один свойство value
вида i32
, и возвращает Guess
. Код в теле функции new
проверяет, что значение value
находится между 1 и 100. Если value
не проходит эту проверку, мы вызываем panic!
, которая оповестит программиста, написавшего вызывающий код, что в его коде есть ошибка, которую необходимо исправить, поскольку попытка создания Guess
со значением value
вне заданного ряда нарушает договор, на который полагается Guess::new
. Условия, в которых Guess::new
паникует, должны быть описаны в документации к API; мы рассмотрим соглашения о документации, указывающие на возможность появления panic!
в документации API, которую вы создадите в Главе 14. Если value
проходит проверку, мы создаём новый образец Guess
, у которого значение поля value
равно значению свойства value
, и возвращаем Guess
.
Затем мы выполняем способ с названием value
, который заимствует self
, не имеет других свойств, и возвращает значение вида i32
. Этот способ иногда называют извлекатель (getter), потому что его цель состоит в том, чтобы извлечь данные из полей устройства и вернуть их. Этот открытый способ является необходимым, поскольку поле value
устройства Guess
является закрытым. Важно, чтобы поле value
было закрытым, чтобы код, использующий устройство Guess
, не мог устанавливать value
напрямую: код снаружи звена должен использовать функцию Guess::new
для создания образца Guess
, таким образом обеспечивая, что у Guess
нет возможности получить value
, не проверенное условиями в функции Guess::new
.
Функция, которая принимает или возвращает только числа от 1 до 100, может объявить в своей ярлыке, что она принимает или возвращает Guess
, вместо i32
, таким образом не будет необходимости делать дополнительные проверки в теле такой функции.
Функции обработки ошибок в Ржавчина призваны помочь написанию более надёжного кода. Макрос panic!
указывает , что ваша программа находится в состоянии, которое она не может обработать, и позволяет сказать этапу чтобы он прекратил своё выполнение, вместо попытки продолжить выполнение с неправильными или неверными значениями. Перечисление Result
использует систему видов Rust, чтобы сообщить, что действия могут завершиться неудачей, и ваш код мог восстановиться. Можно использовать Result
, чтобы сообщить вызывающему коду, что он должен обрабатывать вероятный успех или вероятную неудачу. Использование panic!
и Result
правильным образом сделает ваш код более надёжным перед лицом неизбежных неполадок.
Теперь, когда вы увидели полезные способы использования обобщённых видов Option
и Result
в встроенной библиотеке, мы поговорим о том, как работают обобщённые виды и как вы можете использовать их в своём коде.
Каждый язык программирования имеет в своём арсенале эффективные средства борьбы с повторением кода. В Ржавчина одним из таких средств являются обобщённые виды данных - generics. Это абстрактные подставные виды на место которых возможно поставить какой-либо определенный вид или другое свойство. Когда мы пишем код, мы можем выразить поведение обобщённых видов или их связь с другими обобщёнными видами, не зная какой вид будет использован на их месте при сборки и запуске кода.
-Функции могут принимать свойства некоторого "обобщённого" вида вместо привычных "определенных" видов, вроде i32
или String
. Подобно, функция принимает свойства с неизвестными заранее значениями, чтобы выполнять одинаковые действия над несколькими определенными значениями. На самом деле мы уже использовали обобщённые виды данных в Главе 6 (Option<T>
), в Главе 8 (Vec<T>
и HashMap<K, V>
) и в Главе 9 (Result<T, E>
). В этой главе вы узнаете, как определить собственные виды данных, функции и способы, используя возможности обобщённых видов.
Прежде всего, мы рассмотрим как для уменьшения повторения извлечь из кода некоторую общую возможность. Далее, мы будем использовать тот же рычаг для создания обобщённой функции из двух функций, которые отличаются только видом их свойств. Мы также объясним, как использовать обобщённые виды данных при определении устройств и перечислений.
-После этого мы изучим как использовать особенности (traits) для определения поведения в обобщённом виде. Можно соединенять особенности с обобщёнными видами, чтобы обобщённый вид мог принимать только такие виды, которые имеют определённое поведение, а не все подряд.
-В конце мы обсудим времена жизни (lifetimes), вариации обобщённых видов, которые дают сборщику сведения о том, как сроки жизни ссылок относятся друг к другу. Времена жизни позволяют нам указать дополнительную сведения об "одолженных" (borrowed) значениях, которая позволит сборщику удостовериться в соблюдения правил используемых ссылок в тех случаейх, когда сборщик не может сделать это самостоятельно .
-В обобщениях мы можем заменить определенный вид на "заполнитель" (placeholder), обозначающую несколько видов, что позволяет удалить повторяющийся код. Прежде чем углубляться в правила написания обобщённых видов, давайте сначала посмотрим, как удалить повторение, не задействуя гибкие виды, путём извлечения функции, которая заменяет определённые значения заполнителем, представляющим несколько значений. Затем мы применим ту же технику для извлечения гибкой функции! Изучив, как распознать повторяющийся код, который можно извлечь в функцию, вы начнёте распознавать повторяющийся код, который может использовать обобщённые виды.
-Начнём с короткой программы в приложении 10-1, которая находит наибольшее число в списке.
-Файл: src/main.rs
--fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let mut largest = &number_list[0]; - - for number in &number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {largest}"); - assert_eq!(*largest, 100); -}
-
Сохраним список целых чисел в переменной number_list
и поместим первое значение из списка в переменную largest
. Далее, переберём все элементы списка, и, если текущий элемент больше числа сохранённого в переменной largest
, заменим значение в этой переменной. Если текущий элемент меньше или равен "наибольшему", найденному ранее, значение переменной оставим прежним и перейдём к следующему элементу списка. После перебора всех элементов списка переменная largest
должна содержать наибольшее значение, которое в нашем случае будет равно 100.
Теперь перед нами стоит задача найти наибольшее число в двух разных списках. Для этого мы можем повторять код из приложения 10-1 и использовать ту же логику в двух разных местах программы, как показано в приложении 10-2.
-Файл: src/main.rs
--fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let mut largest = &number_list[0]; - - for number in &number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {largest}"); - - let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; - - let mut largest = &number_list[0]; - - for number in &number_list { - if number > largest { - largest = number; - } - } - - println!("The largest number is {largest}"); -}
-
Несмотря на то, что код программы работает, повторение кода утомительно и подвержено ошибкам. При внесении изменений мы должны не забыть обновить каждое место, где код повторяется.
-Для устранения повторения мы можем создать дополнительную абстракцию с помощью функции которая сможет работать с любым списком целых чисел переданным ей в качестве входного свойства и находить для этого списка наибольшее число. Данное решение делает код более ясным и позволяет абстрактным образом выполнить алгоритм поиска наибольшего числа в списке.
-В приложении 10-3 мы извлекаем код, который находит наибольшее число, в функцию с именем largest
. Затем мы вызываем функцию, чтобы найти наибольшее число в двух списках из приложения 10-2. Мы также можем использовать эту функцию для любого другого списка значений i32
, который может встретиться позже.
Файл: src/main.rs
--fn largest(list: &[i32]) -> &i32 { - let mut largest = &list[0]; - - for item in list { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest(&number_list); - println!("The largest number is {result}"); - assert_eq!(*result, 100); - - let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; - - let result = largest(&number_list); - println!("The largest number is {result}"); - assert_eq!(*result, 6000); -}
-
Функция largest
имеет свойство с именем list
, который представляет любой срез значений вида i32
, которые мы можем передать в неё. В итоге вызова функции, код выполнится с определенными, переданными в неё значениями.
Итак, вот шаги выполненные для изменения кода из приложения 10-2 в приложение 10-3:
-Далее, чтобы уменьшить повторение кода, мы воспользуемся теми же шагами для обобщённых видов. Обобщённые виды позволяют работать над абстрактными видами таким же образом, как тело функции может работать над абстрактным списком list
вместо определенных значений.
Например, у нас есть две функции: одна ищет наибольший элемент внутри среза значений вида i32
, а другая внутри среза значений вида char
. Как уменьшить такое повторение? Давайте выяснять!
Мы используем обобщённые виды данных для объявления функций или устройств, которые затем можно использовать с различными определенными видами данных. Давайте сначала посмотрим, как объявлять функции, устройства, перечисления и способы, используя обобщённые виды данных. Затем мы обсудим, как обобщённые виды данных влияют на производительность кода.
-Когда мы объявляем функцию с обобщёнными видами, мы размещаем обобщённые виды в ярлыке функции, где мы обычно указываем виды данных переменных и возвращаемого значения. Используя обобщённые виды, мы делаем код более гибким и предоставляем большую возможность при вызове нашей функции, предотвращая повторение кода.
-Рассмотрим пример с функцией largest
. Приложение 10-4 показывает две функции, каждая из которых находит самое большое значение в срезе своего вида. Позже мы объединим их в одну функцию, использующую обобщённые виды данных.
Файл: src/main.rs
--fn largest_i32(list: &[i32]) -> &i32 { - let mut largest = &list[0]; - - for item in list { - if item > largest { - largest = item; - } - } - - largest -} - -fn largest_char(list: &[char]) -> &char { - let mut largest = &list[0]; - - for item in list { - if item > largest { - largest = item; - } - } - - largest -} - -fn main() { - let number_list = vec![34, 50, 25, 100, 65]; - - let result = largest_i32(&number_list); - println!("The largest number is {result}"); - assert_eq!(*result, 100); - - let char_list = vec!['y', 'm', 'a', 'q']; - - let result = largest_char(&char_list); - println!("The largest char is {result}"); - assert_eq!(*result, 'y'); -}
-
Функция largest_i32
уже встречалась нам: мы извлекли её в приложении 10-3, когда боролись с повторением кода — она находит наибольшее значение вида i32
в срезе. Функция largest_char
находит самое большое значение вида char
в срезе. Тело у этих функций одинаковое, поэтому давайте избавимся от повторяемлшл кода, используя свойство обобщённого вида в одной функции.
Для свойствоизации видов данных в новой объявляемой функции нам нужно дать имя обобщённому виду — так же, как мы это делаем для переменных функций. Можно использовать любой определитель для имени свойства вида, но мы будем использовать T
, потому что по соглашению имена свойств в Ржавчина должны быть короткими (обычно длиной в один символ), а именование видов в Ржавчина делается в наставлении UpperCamelCase. Сокращение слова «type» до одной буквы T
является обычным выбором большинства программистов, использующих язык Rust.
Когда мы используем свойство в теле функции, мы должны объявить имя свойства в ярлыке, чтобы сборщик знал, что означает это имя. Подобно когда мы используем имя вида свойства в ярлыке функции, мы должны объявить это имя раньше, чем мы его используем. Чтобы определить обобщённую функцию largest
, поместим объявление имён свойств в треугольные скобки <>
между именем функции и списком свойств, как здесь:
fn largest<T>(list: &[T]) -> &T {
-Объявление читается так: функция largest
является обобщённой по виду T
. Эта функция имеет один свойство с именем list
, который является срезом значений с видом данных T
. Функция largest
возвращает значение этого же вида T
.
Приложение 10-5 показывает определение функции largest
с использованием обобщённых видов данных в её ярлыке. Приложение также показывает, как мы можем вызвать функцию со срезом данных вида i32
или char
. Данный код пока не будет собираться, но мы исправим это к концу раздела.
Файл: src/main.rs
-fn largest<T>(list: &[T]) -> &T {
- let mut largest = &list[0];
-
- for item in list {
- if item > largest {
- largest = item;
- }
- }
-
- largest
-}
-
-fn main() {
- let number_list = vec![34, 50, 25, 100, 65];
-
- let result = largest(&number_list);
- println!("The largest number is {result}");
-
- let char_list = vec!['y', 'm', 'a', 'q'];
-
- let result = largest(&char_list);
- println!("The largest char is {result}");
-}
--
Если мы соберем программу сейчас, мы получим следующую ошибку:
-$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0369]: binary operation `>` cannot be applied to type `&T`
- --> src/main.rs:5:17
- |
-5 | if item > largest {
- | ---- ^ ------- &T
- | |
- | &T
- |
-help: consider restricting type parameter `T`
- |
-1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
- | ++++++++++++++++++++++
-
-For more information about this error, try `rustc --explain E0369`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-В подсказке упоминается std::cmp::PartialOrd
, который является особенностью. Мы поговорим про особенности в следующем разделе. Сейчас ошибка в функции largest
указывает, что функция не будет работать для всех возможных видов T
. Так как мы хотим сравнивать значения вида T
в теле функции, мы можем использовать только те виды, данные которых можно упорядочить: можем упорядочить — значит, можем и сравнить. Чтобы можно было задействовать сравнения, обычная библиотека имеет особенность std::cmp::PartialOrd
, который вы можете выполнить для видов (смотрите дополнение С для большей сведений про данный особенность). Следуя совету в сообщении сборщика, ограничим вид T
теми исходами, которые поддерживают особенность PartialOrd
, и тогда пример успешно собирается, так как обычная библиотека выполняет PartialOrd
как для вида i32
, так и для вида char
.
Мы также можем определить устройства, использующие обобщённые виды в одном или нескольких своих полях, с помощью правил написания <>
. Приложение 10-6 показывает, как определить устройство Point<T>
, чтобы хранить поля координат x
и y
любого вида данных.
Файл: src/main.rs
--struct Point<T> { - x: T, - y: T, -} - -fn main() { - let integer = Point { x: 5, y: 10 }; - let float = Point { x: 1.0, y: 4.0 }; -}
-
правила написания использования обобщённых видов в определении устройства очень похож на правила написания в определении функции. Сначала мы объявляем имена видов свойств внутри треугольных скобок сразу после названия устройства. Затем мы можем использовать обобщённые виды в определении устройства в тех местах, где ранее мы указывали бы определенные виды.
-Так как мы используем только один обобщённый вид данных для определения устройства Point<T>
, это определение означает, что устройства Point<T>
является обобщённой с видом T
, и оба поля x
и y
имеют одинаковый вид, каким бы он не являлся. Если мы создадим образец устройства Point<T>
со значениями разных видов, как показано в приложении 10-7, наш код не собирается.
Файл: src/main.rs
-struct Point<T> {
- x: T,
- y: T,
-}
-
-fn main() {
- let wont_work = Point { x: 5, y: 4.0 };
-}
--
В этом примере, когда мы присваиваем целочисленное значение 5 переменной x
, мы сообщаем сборщику, что обобщённый вид T
будет целым числом для этого образца Point<T>
. Затем, когда мы указываем значение 4.0 (имеющее вид, отличный от целого числа) для y
, который по нашему определению должен иметь тот же вид, что и x
, мы получим ошибку несоответствия видов:
$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0308]: mismatched types
- --> src/main.rs:7:38
- |
-7 | let wont_work = Point { x: 5, y: 4.0 };
- | ^^^ expected integer, found floating-point number
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-Чтобы определить устройство Point
, где оба значения x
и y
являются обобщёнными, но различными видами, можно использовать несколько свойств обобщённого вида. Например, в приложении 10-8 мы изменим определение Point
таким образом, чтобы оно использовало обобщённые виды T
и U
, где x
имеет вид T
а y
имеет вид U
.
Файл: src/main.rs
--struct Point<T, U> { - x: T, - y: U, -} - -fn main() { - let both_integer = Point { x: 5, y: 10 }; - let both_float = Point { x: 1.0, y: 4.0 }; - let integer_and_float = Point { x: 5, y: 4.0 }; -}
-
Теперь разрешены все показанные образцы вида Point
! В объявлении можно использовать сколь угодно много свойств обобщённого вида, но если делать это в большом количестве, код будет тяжело читать. Если в вашем коде требуется много обобщённых видов, возможно, стоит разбить его на более мелкие части.
Как и устройства, перечисления также могут хранить обобщённые виды в своих исхода.. Давайте ещё раз посмотрим на перечисление Option<T>
, предоставленное встроенной библиотекой, которое мы использовали в главе 6:
-#![allow(unused)] -fn main() { -enum Option<T> { - Some(T), - None, -} -}
Это определение теперь должно быть вам более понятно. Как видите, перечисление Option<T>
является обобщённым по виду T
и имеет два исхода: исход Some
, который содержит одно значение вида T
, и исход None
, который не содержит никакого значения. Используя перечисление Option<T>
, можно выразить абстрактную подход необязательного значения — и так как Option<T>
является обобщённым, можно использовать эту абстракцию независимо от того, каким будет вид необязательного значения.
Перечисления также могут использовать несколько обобщённых видов. Определение перечисления Result
, которое мы упоминали в главе 9, является примером такого использования:
-#![allow(unused)] -fn main() { -enum Result<T, E> { - Ok(T), - Err(E), -} -}
Перечисление Result
имеет два обобщённых вида: T
и E
— и два исхода: Ok
, который содержит вид T
, и Err
, содержащий вид E
. С таким определением удобно использовать перечисление Result
везде, где действия могут быть выполнены успешно (возвращая значение вида T
) или неуспешно (возвращая ошибку вида E
). Это то, что мы делали при открытии файла в приложении 9-3, где T
заполнялось видом std::fs::File
, если файл был открыт успешно, либо E
заполнялось видом std::io::Error
, если при открытии файла возникали какие-либо сбоев.
Если вы встречаете в коде случаи, когда несколько определений устройств или перечислений отличаются только видами содержащихся в них значений, вы можете устранить повторение, используя обобщённые виды.
-Мы можем выполнить способы для устройств и перечислений (как мы делали в главе 5) и в определениях этих способов также использовать обобщённые виды. В приложении 10-9 показана устройства Point<T>
, которую мы определили в приложении 10-6, с добавленным для неё способом x
.
Файл: src/main.rs
--struct Point<T> { - x: T, - y: T, -} - -impl<T> Point<T> { - fn x(&self) -> &T { - &self.x - } -} - -fn main() { - let p = Point { x: 5, y: 10 }; - - println!("p.x = {}", p.x()); -}
-
Здесь мы определили способ с именем x
у устройства Point<T>
, который возвращает ссылку на данные в поле x
.
Обратите внимание, что мы должны объявить T
сразу после impl
. В этом случае мы можем использовать T
для указания на то, что выполняем способ для вида Point<T>
. Объявив T
гибким видом сразу после impl
, Ржавчина может определить, что вид в угловых скобках в Point
является гибким, а не определенным видом. Мы могли бы выбрать другое имя для этого обобщённого свойства, отличное от имени, использованного в определении устройства, но обычно используют одно и то же имя. Способы, написанные внутри раздела impl
, который использует обобщённый вид, будут определены для любого образца вида, независимо от того, какой определенный вид в конечном итоге будет подставлен вместо этого обобщённого.
Мы можем также указать ограничения, какие обобщённые виды разрешено использовать при определении способов. Например, мы могли бы выполнить способы только для образцов вида Point<f32>
, а не для образцов Point<T>
, в которых используется произвольный обобщённый вид. В приложении 10-10 мы используем определенный вид f32
, что означает, что мы не определяем никакие виды после impl
.
Файл: src/main.rs
--struct Point<T> { - x: T, - y: T, -} - -impl<T> Point<T> { - fn x(&self) -> &T { - &self.x - } -} - -impl Point<f32> { - fn distance_from_origin(&self) -> f32 { - (self.x.powi(2) + self.y.powi(2)).sqrt() - } -} - -fn main() { - let p = Point { x: 5, y: 10 }; - - println!("p.x = {}", p.x()); -}
-
Этот код означает, что вид Point<f32>
будет иметь способ с именем distance_from_origin
, а другие образцы Point<T>
, где T
имеет вид, отличный от f32
, не будут иметь этого способа. Способ вычисляет, насколько далеко наша точка находится от точки с координатами (0.0, 0.0), и использует математические действия, доступные только для видов с плавающей точкой.
Свойства обобщённого вида, которые мы используем в определении устройства, не всегда совпадают с подобиями, использующимися в ярлыках способов этой устройства. Чтобы пример был более очевидным, в приложении 10-11 используются обобщённые виды X1
и Y1
для определения устройства Point
и виды X2
Y2
для ярлыки способа mixup
. Способ создаёт новый образец устройства Point
, где значение x
берётся из self
Point
(имеющей вид X1
), а значение y
- из переданной устройства Point
(где эта переменная имеет вид Y2
).
Файл: src/main.rs
--struct Point<X1, Y1> { - x: X1, - y: Y1, -} - -impl<X1, Y1> Point<X1, Y1> { - fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> { - Point { - x: self.x, - y: other.y, - } - } -} - -fn main() { - let p1 = Point { x: 5, y: 10.4 }; - let p2 = Point { x: "Hello", y: 'c' }; - - let p3 = p1.mixup(p2); - - println!("p3.x = {}, p3.y = {}", p3.x, p3.y); -}
-
В функции main
мы определили вид Point
, который имеет вид i32
для x
(со значением 5
) и вид f64
для y
(со значением 10.4
). Переменная p2
является устройством Point
, которая имеет строковый срез для x
(со значением «Hello»
) и char
для y
(со значением c
). Вызов mixup
на p1
с переменнаяом p2
создаст для нас образец устройства p3
, который будет иметь вид i32
для x
(потому что x
взят из p1
). Переменная p3
будет иметь вид char
для y
(потому что y
взят из p2
). Вызов макроса println!
выведет p3.x = 5, p3.y = c
.
Цель этого примера — отобразить случай, в которой некоторые обобщённые свойства объявлены с помощью impl
, а некоторые объявлены в определении способа. Здесь обобщённые свойства X1
и Y1
объявляются после impl
, потому что они относятся к определению устройства. Обобщённые свойства X2
и Y2
объявляются после fn mixup
, так как они относятся только к способу.
Вы могли бы задаться вопросом, возникают ли какие-нибудь дополнительные издержки при использовании свойств обобщённого вида. Хорошая новость в том, что при использовании обобщённых видов ваша программа работает ничуть ни медленнее, чем если бы она работала с использованием определенных видов.
-В Ржавчина это достигается во время сборки при помощи мономорфизации кода, использующего обобщённые виды. Мономорфизация — это этап превращения обобщённого кода в определенный код путём подстановки определенных видов, использующихся при сборки. В этом этапе сборщик выполняет шаги, противоположные тем, которые мы использовали для создания обобщённой функции в приложении 10-5: он просматривает все места, где вызывается обобщённый код, и порождает код для определенных видов, использовавшихся для вызова в обобщённом.
-Давайте посмотрим, как это работает при использовании перечисления Option<T>
из встроенной библиотеки:
-#![allow(unused)] -fn main() { -let integer = Some(5); -let float = Some(5.0); -}
Когда Ржавчина собирает этот код, он выполняет мономорфизацию. Во время этого этапа сборщик считывает значения, которые были использованы в образцах Option<T>
, и определяет два вида Option<T>
: один для вида i32
, а другой — для f64
. Таким образом, он разворачивает обобщённое определение Option<T>
в два определения, именно для i32
и f64
, тем самым заменяя обобщённое определение определенными.
Мономорфизированная исполнение кода выглядит примерно так (сборщик использует имена, отличные от тех, которые мы используем здесь для отображения):
-Файл: src/main.rs
--enum Option_i32 { - Some(i32), - None, -} - -enum Option_f64 { - Some(f64), - None, -} - -fn main() { - let integer = Option_i32::Some(5); - let float = Option_f64::Some(5.0); -}
Обобщённое Option<T>
заменяется определенными определениями, созданными сборщиком. Поскольку Ржавчина собирает обобщённый код в код, определяющий вид в каждом образце, мы не платим за использование обобщённых видов во время выполнения. Когда код запускается, он работает точно так же, как если бы мы сделали повторение каждое определение вручную. Этап мономорфизации делает обобщённые виды Ржавчина чрезвычайно эффективными во время выполнения.
Особенность сообщает сборщику Ржавчина о возможности, которой обладает определённый вид и которой он может поделиться с другими видами. Можно использовать особенности, чтобы определять общее поведение абстрактным способом. Мы можем использовать ограничение особенности (trait bounds) чтобы указать, что общим видом может быть любой вид, который имеет определённое поведение.
---Примечание: Особенности похожи на возможность часто называемую внешней оболочкими в других языках программирования, хотя и с некоторыми отличиями.
-
Поведение вида определяется теми способами, которые мы можем вызвать у данного вида. Различные виды разделяют одинаковое поведение, если мы можем вызвать одни и те же способы у этих видов. Определение особенностей - это способ собъединять ярлыки способов вместе для того, чтобы описать общее поведение, необходимое для достижения определённой цели.
-Например, пусть есть несколько устройств, которые имеют различный вид и различный размер текста: устройства NewsArticle
, которая содержит новость, напечатанную в каком-то месте мира; устройства Tweet
, которая содержит 280 символьную строку твита и мета-данные, обозначающие является ли твит новым или ответом на другой твит.
Мы хотим создать ящик библиотеки медиа-агрегатора aggregator
, которая может отображать сводку данных сохранённых в образцах устройств NewsArticle
или Tweet
. Чтобы этого достичь, нам необходимо иметь возможность для каждой устройства получить короткую сводку на основе имеющихся данных, и для этого мы запросим сводку вызвав способ summarize
. Приложение 10-12 показывает определение особенности Summary
, который выражает это поведение.
Файл: src/lib.rs
-pub trait Summary {
- fn summarize(&self) -> String;
-}
--
Здесь мы объявляем особенность с использованием ключевого слова trait
, а затем его название, которым в нашем случае является Summary
. Также мы объявляем ящик как pub
что позволяет ящикам, зависящим от нашего ящика, тоже использовать наш ящик, что мы увидим в последующих примерах. Внутри фигурных скобок объявляются ярлыки способов, которые описывают поведения видов, выполняющих данный особенность, в данном случае поведение определяется только одной ярлыком способа fn summarize(&self) -> String
.
После ярлыки способа, вместо предоставления выполнения в фигурных в скобках, мы используем точку с запятой. Каждый вид, выполняющий данный особенность, должен предоставить своё собственное поведение для данного способа. Сборщик обеспечит, что любой вид содержащий особенность Summary
, будет также иметь и способ summarize
объявленный с точно такой же ярлыком.
Особенность может иметь несколько способов в описании его тела: ярлыки способов перечисляются по одной на каждой строке и должны закачиваться символом ;
.
Теперь, после того как мы определили желаемое поведение используя особенность Summary
, можно выполнить его у видов в нашем медиа-агрегаторе. Приложение 10-13 показывает выполнение особенности Summary
у устройства NewsArticle
, которая использует для создания сводки в способе summarize
заголовок, автора и место обнародования статьи. Для устройства Tweet
мы определяем выполнение summarize
используя имя пользователя и следующий за ним полный текст твита, полагая что содержание твита уже ограничено 280 символами.
Файл: src/lib.rs
-pub trait Summary {
- fn summarize(&self) -> String;
-}
-
-pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
-}
-
-impl Summary for NewsArticle {
- fn summarize(&self) -> String {
- format!("{}, by {} ({})", self.headline, self.author, self.location)
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
-}
--
Выполнение особенности у вида подобна выполнения обычных способов. Разница в том что после impl
мы ставим имя особенности, который мы хотим выполнить, затем используем ключевое слово for
, а затем указываем имя вида, для которого мы хотим сделать выполнение особенности. Внутри раздела impl
мы помещаем ярлык способа объявленную в особенности. Вместо добавления точки с запятой в конце, после каждой ярлыки используются фигурные скобки и тело способа заполняется определенным поведением, которое мы хотим получить у способов особенности для определенного вида.
Теперь когда библиотека выполнила особенность Summary
для NewsArticle
и Tweet
, программисты использующие ящик могут вызывать способы особенности у образцов видов NewsArticle
и Tweet
точно так же как если бы это были обычные способы. Единственное отличие состоит в том, что программист должен ввести особенность в область видимости точно так же как и виды. Здесь пример того как двоичный ящик может использовать наш aggregator
:
use aggregator::{Summary, Tweet};
-
-fn main() {
- let tweet = Tweet {
- username: String::from("horse_ebooks"),
- content: String::from(
- "of course, as you probably already know, people",
- ),
- reply: false,
- retweet: false,
- };
-
- println!("1 new tweet: {}", tweet.summarize());
-}
-Данный код напечатает: 1 new tweet: horse_ebooks: of course, as you probably already know, people
.
Другие ящики, которые зависят от aggregator
, тоже могу включить особенность Summary
в область видимости для выполнения Summary
в их собственных видах. Одно ограничение, на которое следует обратить внимание, заключается в том, что мы можем выполнить особенность для вида только в том случае, если хотя бы один из особенностей вида является местным для нашего ящика. Например, мы можем выполнить обычный библиотечный особенность Display
на собственном виде Tweet
как часть возможности нашего ящика aggregator
потому что вид Tweet
является местным для ящика aggregator
. Также мы можем выполнить Summary
для Vec<T>
в нашем ящике aggregator
, потому что особенность Summary
является местным для нашего ящика aggregator
.
Но мы не можем выполнить внешние особенности для внешних видов. Например, мы не можем выполнить особенность Display
для Vec<T>
внутри нашего ящика aggregator
, потому что Display
и Vec<T>
оба определены в встроенной библиотеке а не местно в нашем ящике aggregator
. Это ограничение является частью свойства называемого согласованность (coherence), а ещё точнее сиротское правило (orphan rule), которое называется так потому что не представлен родительский вид. Это правило заверяет, что код других людей не может сломать ваш код и наоборот. Без этого правила два ящика могли бы выполнить один особенность для одинакового вида и Ржавчина не сможет понять, какой выполнением нужно пользоваться.
Иногда полезно иметь поведение по умолчанию для некоторых или всех способов в особенности вместо того, чтобы требовать выполнения всех способов в каждом виде, выполняющим данный особенность. Затем, когда мы выполняем особенность для определённого вида, можно сохранить или переопределить поведение каждого способа по умолчанию уже внутри видов.
-В примере 10-14 показано, как указать строку по умолчанию для способа summarize
из особенности Summary
вместо определения только ярлыки способа, как мы сделали в примере 10-12.
Файл: src/lib.rs
-pub trait Summary {
- fn summarize(&self) -> String {
- String::from("(Read more...)")
- }
-}
-
-pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
-}
-
-impl Summary for NewsArticle {}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
-}
--
Для использования выполнения по умолчанию при создании сводки у образцов NewsArticle
вместо определения пользовательской выполнения, мы указываем пустой разделimpl
с impl Summary for NewsArticle {}
.
Хотя мы больше не определяем способ summarize
непосредственно в NewsArticle
, мы предоставили выполнение по умолчанию и указали, что NewsArticle
выполняет особенность Summary
. В итоге мы всё ещё можем вызвать способ summarize
у образца NewsArticle
, например так:
use aggregator::{self, NewsArticle, Summary};
-
-fn main() {
- let article = NewsArticle {
- headline: String::from("Penguins win the Stanley Cup Championship!"),
- location: String::from("Pittsburgh, PA, USA"),
- author: String::from("Iceburgh"),
- content: String::from(
- "The Pittsburgh Penguins once again are the best \
- hockey team in the NHL.",
- ),
- };
-
- println!("New article available! {}", article.summarize());
-}
-Этот код печатает New article available! (Read more...)
.
Создание выполнения по умолчанию не требует от нас изменений чего-либо в выполнения Summary
для Tweet
в приложении 10-13. Причина заключается в том, что правила написания для переопределения выполнения по умолчанию является таким же, как правила написания для выполнения способа особенности, который не имеет выполнения по умолчанию.
Выполнения по умолчанию могут вызывать другие способы в том же особенности, даже если эти другие способы не имеют выполнения по умолчанию. Таким образом, особенность может предоставить много полезной возможности и только требует от разработчиков указывать небольшую его часть. Например, мы могли бы определить особенность Summary
имеющий способ summarize_author
, выполнение которого требуется, а затем определить способ summarize
который имеет выполнение по умолчанию, которая внутри вызывает способ summarize_author
:
pub trait Summary {
- fn summarize_author(&self) -> String;
-
- fn summarize(&self) -> String {
- format!("(Read more from {}...)", self.summarize_author())
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize_author(&self) -> String {
- format!("@{}", self.username)
- }
-}
-Чтобы использовать такую исполнение особенности Summary
, нужно только определить способ summarize_author
, при выполнения особенности для вида:
pub trait Summary {
- fn summarize_author(&self) -> String;
-
- fn summarize(&self) -> String {
- format!("(Read more from {}...)", self.summarize_author())
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize_author(&self) -> String {
- format!("@{}", self.username)
- }
-}
-После того, как мы определим summarize_author
, можно вызвать summarize
для образцов устройства Tweet
и выполнение по умолчанию способа summarize
будет вызывать определение summarize_author
которое мы уже предоставили. Так как мы выполнили способ summarize_author
особенности Summary
, то особенность даёт нам поведение способа summarize
без необходимости писать код.
use aggregator::{self, Summary, Tweet};
-
-fn main() {
- let tweet = Tweet {
- username: String::from("horse_ebooks"),
- content: String::from(
- "of course, as you probably already know, people",
- ),
- reply: false,
- retweet: false,
- };
-
- println!("1 new tweet: {}", tweet.summarize());
-}
-Этот код печатает 1 new tweet: (Read more from @horse_ebooks...)
.
Обратите внимание, что невозможно вызвать выполнение по умолчанию из переопределённой выполнения того же способа.
-Теперь, когда вы знаете, как определять и выполнить особенности, можно изучить, как использовать особенности, чтобы определить функции, которые принимают много различных видов. Мы будем использовать особенность Summary
, выполненный для видов NewsArticle
и Tweet
в приложении 10-13, чтобы определить функцию notify
, которая вызывает способ summarize
для его свойства item
, который имеет некоторый вид, выполняющий особенность Summary
. Для этого мы используем правила написания impl Trait
примерно так:
pub trait Summary {
- fn summarize(&self) -> String;
-}
-
-pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
-}
-
-impl Summary for NewsArticle {
- fn summarize(&self) -> String {
- format!("{}, by {} ({})", self.headline, self.author, self.location)
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
-}
-
-pub fn notify(item: &impl Summary) {
- println!("Breaking news! {}", item.summarize());
-}
-Вместо определенного вида у свойства item
указывается ключевое слово impl
и имя особенности. Этот свойство принимает любой вид, который выполняет указанный особенность. В теле notify
мы можем вызывать любые способы у образца item
, которые приходят с особенностью Summary
, такие как способ summarize
. Мы можем вызвать notify
и передать в него любой образец NewsArticle
или Tweet
. Код, который вызывает данную функцию с любым другим видом, таким как String
или i32
, не будет собираться, потому что эти виды не выполняют особенность Summary
.
правила написания impl Trait
работает для простых случаев, но на самом деле является синтаксическим сахаром для более длинной видовы, которая называется ограничением особенности (trait bound); это выглядит так:
pub fn notify<T: Summary>(item: &T) {
- println!("Breaking news! {}", item.summarize());
-}
-Эта более длинная разновидность эквивалентна примеру в предыдущем разделе, но она более многословна. Мы помещаем объявление свойства обобщённого вида с ограничением особенности после двоеточия внутри угловых скобок.
-правила написания impl Trait
удобен и делает код более сжатым в простых случаях, в то время как более полный правила написания с ограничением особенности в других случаях может выразить большую сложность. Например, у нас может быть два свойства, которые выполняют особенность Summary
. Использование правил написания impl Trait
выглядит так:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
-Использовать impl Trait
удобнее если мы хотим разрешить функции иметь разные виды для item1
и item2
(но оба вида должны выполнить Summary
). Если же мы хотим заставить оба свойства иметь один и тот же вид, то мы должны использовать ограничение особенности так:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
-Обобщённый вид T
указан для видов свойств item1
и item2
и ограничивает функцию так, что определенные значения видов переданные переменнойми для item1
и item2
должны быть одинаковыми.
+
Также можно указать более одного ограничения особенности. Допустим, мы хотели бы чтобы notify
использовал как изменение
-вывода так и summarize
для свойства item
:
тогда мы указываем что в notify
свойство item
должен выполнить оба особенности Display
и Summary
. Мы можем сделать это используя правила написания +
:
pub fn notify(item: &(impl Summary + Display)) {
-правила написания +
также допустим с ограничениями особенности для обобщённых видов:
pub fn notify<T: Summary + Display>(item: &T) {
-При наличии двух ограничений особенности, тело способа notify
может вызывать summarize
и использовать {}
для изменения item
при его печати.
where
Использование слишком большого количества ограничений особенности имеет свои недостатки. Каждый обобщённый вид имеет свои границы особенности, поэтому функции с несколькими свойствами обобщённого вида могут содержать много сведений об ограничениях между названием функции и списком её свойств затрудняющих чтение ярлыки. По этой причине в Ржавчина есть иной правила написания для определения ограничений особенности внутри предложения where
после ярлыки функции. Поэтому вместо того, чтобы писать так:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
-можно использовать where
таким образом:
fn some_function<T, U>(t: &T, u: &U) -> i32
-where
- T: Display + Clone,
- U: Clone + Debug,
-{
- unimplemented!()
-}
-Ярлык этой функции менее загромождена: название функции, список свойств, и возвращаемый вид находятся рядом, а ярлык не содержит в себе множество ограничений особенности.
-Также можно использовать правила написания impl Trait
в возвращаемой позиции, чтобы вернуть значение некоторого вида выполняющего особенность, как показано здесь:
pub trait Summary {
- fn summarize(&self) -> String;
-}
-
-pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
-}
-
-impl Summary for NewsArticle {
- fn summarize(&self) -> String {
- format!("{}, by {} ({})", self.headline, self.author, self.location)
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
-}
-
-fn returns_summarizable() -> impl Summary {
- Tweet {
- username: String::from("horse_ebooks"),
- content: String::from(
- "of course, as you probably already know, people",
- ),
- reply: false,
- retweet: false,
- }
-}
-Используя impl Summary
для возвращаемого вида, мы указываем, что функция returns_summarizable
возвращает некоторый вид, который выполняет особенность Summary
без обозначения определенного вида. В этом случае returns_summarizable
возвращает Tweet
, но код, вызывающий эту функцию, этого не знает.
Возможность возвращать вид, который определяется только выполняемым им признаком, особенно полезна в среде замыканий и повторителей, которые мы рассмотрим в Главе 13. Замыкания и повторители создают виды, которые знает только сборщик или виды, которые очень долго указывать. правила написания impl Trait
позволяет кратко указать, что функция возвращает некоторый вид, который выполняет особенность Iterator
без необходимости писать очень длинный вид.
Однако, impl Trait
возможно использовать, если возвращаете только один вид. Например, данный код, который возвращает значения или вида NewsArticle
или вида Tweet
, но в качестве возвращаемого вида объявляет impl Summary
, не будет работать:
pub trait Summary {
- fn summarize(&self) -> String;
-}
-
-pub struct NewsArticle {
- pub headline: String,
- pub location: String,
- pub author: String,
- pub content: String,
-}
-
-impl Summary for NewsArticle {
- fn summarize(&self) -> String {
- format!("{}, by {} ({})", self.headline, self.author, self.location)
- }
-}
-
-pub struct Tweet {
- pub username: String,
- pub content: String,
- pub reply: bool,
- pub retweet: bool,
-}
-
-impl Summary for Tweet {
- fn summarize(&self) -> String {
- format!("{}: {}", self.username, self.content)
- }
-}
-
-fn returns_summarizable(switch: bool) -> impl Summary {
- if switch {
- NewsArticle {
- headline: String::from(
- "Penguins win the Stanley Cup Championship!",
- ),
- location: String::from("Pittsburgh, PA, USA"),
- author: String::from("Iceburgh"),
- content: String::from(
- "The Pittsburgh Penguins once again are the best \
- hockey team in the NHL.",
- ),
- }
- } else {
- Tweet {
- username: String::from("horse_ebooks"),
- content: String::from(
- "of course, as you probably already know, people",
- ),
- reply: false,
- retweet: false,
- }
- }
-}
-Возврат либо NewsArticle
либо Tweet
не допускается из-за ограничений того, как выполнен правила написания impl Trait
в сборщике. Мы рассмотрим, как написать функцию с таким поведением в разделе "Использование предметов особенностей, которые разрешены для значений или разных видов" Главы 17.
Используя ограничение особенности с разделом impl
, который использует свойства обобщённого вида, можно выполнить способы условно, для тех видов, которые выполняют указанный особенность. Например, вид Pair<T>
в приложении 10-15 всегда выполняет функцию new
для возврата нового образца Pair<T>
(вспомните раздел “Определение способов” Главы 5 где Self
является псевдонимом вида для вида раздела impl
, который в данном случае является Pair<T>
). Но в следующем разделе impl
вид Pair<T>
выполняет способ cmp_display
только если его внутренний вид T
выполняет особенность PartialOrd
(позволяющий сравнивать) и особенность Display
(позволяющий выводить на печать).
Файл: src/lib.rs
-use std::fmt::Display;
-
-struct Pair<T> {
- x: T,
- y: T,
-}
-
-impl<T> Pair<T> {
- fn new(x: T, y: T) -> Self {
- Self { x, y }
- }
-}
-
-impl<T: Display + PartialOrd> Pair<T> {
- fn cmp_display(&self) {
- if self.x >= self.y {
- println!("The largest member is x = {}", self.x);
- } else {
- println!("The largest member is y = {}", self.y);
- }
- }
-}
--
Мы также можем условно выполнить особенность для любого вида, который выполняет другой особенность. Выполнения особенности для любого вида, который удовлетворяет ограничениям особенности, называются общими выполнениеми и широко используются в встроенной библиотеке Rust. Например, обычная библиотека выполняет особенность ToString
для любого вида, который выполняет особенность Display
. Разделimpl
в встроенной библиотеке выглядит примерно так:
impl<T: Display> ToString for T {
- // --snip--
-}
-Поскольку обычная библиотека имеет эту общую выполнение, то можно вызвать способ to_string
определённый особенностью ToString
для любого вида, который выполняет особенность Display
. Например, мы можем превратить целые числа в их соответствующие String
значения, потому что целые числа выполняют особенность Display
:
-#![allow(unused)] -fn main() { -let s = 3.to_string(); -}
Общие выполнения приведены в документации к особенности в разделе "Implementors".
-Особенности и ограничения особенностей позволяют писать код, который использует свойства обобщённого вида для уменьшения повторения кода, а также указывая сборщику, что мы хотим обобщённый вид, чтобы иметь определённое поведение. Затем сборщик может использовать сведения про ограничения особенности, чтобы проверить, что все определенные виды, используемые с нашим кодом, обеспечивают правильное поведение. В изменяемых строго определенных языках мы получили бы ошибку во время выполнения, если бы вызвали способ для вида, который не выполняет вид определяемый способом. Но Ржавчина перемещает эти ошибки на время сборки, поэтому мы вынуждены исправить сбоев, прежде чем наш код начнёт работать. Кроме того, мы не должны писать код, который проверяет своё поведение во время выполнения, потому что это уже проверено во время сборки. Это повышает производительность без необходимости отказываться от гибкости обобщённых видов.
- -Сроки (времена) жизни - ещё один вид обобщений, с которыми мы уже встречались. Если раньше мы использовали обобщения, чтобы убедиться, что вид обладает нужным нам поведением, теперь мы будем использовать сроки жизни для того, чтобы быть уверенными, что ссылки действительны как самое меньшее столько времени в этапе исполнения программы, сколько нам требуется.
-В разделе "Ссылки и заимствование" главы 4, мы кое о чём умолчали: у каждой ссылки в Ржавчина есть своё время жизни — область кода, на протяжении которого данная ссылка действительна (valid). В большинстве случаев сроки жизни выводятся неявно — так же, как у видов (нам требуется явно объявлять виды лишь в тех случаях, когда при самостоятельном выведении вида возможны исходы). Точно так же мы должны явно объявлять сроки жизни тех ссылок, для которых времена жизни могут быть определены сборщиком по-разному. Ржавчина требует от нас объявлять взаимосвязи посредством обобщённых свойств сроков жизни, чтобы убедиться в том, что во время исполнения все действующие ссылки будут правильными.
-Определение времени жизни — это подход, отсутствующая в большинстве других языков программирования, так что она может показаться незнакомой. Хотя в этой главе мы не будем рассматривать времена жизни во всех подробностях, тем не менее, мы обсудим основные случаи, в которых вы можете столкнуться с правилами написания времени жизни, что позволит вам получше ознакомиться с этой подходом.
-Основное предназначение сроков жизни — предотвращать появление так называемых "повисших ссылок" (dangling references), из-за которых программа обращается не к тем данным, к которым она собиралась обратиться. Рассмотрим программу из приложения 10-16, имеющую внешнюю и внутреннюю области видимости.
-fn main() {
- let r;
-
- {
- let x = 5;
- r = &x;
- }
-
- println!("r: {r}");
-}
--
--Примечание: примеры в приложениях 10-16, 10-17 и 10-23 объявляют переменные без указания их начального значения, поэтому имя переменной существует во внешней области видимости. На первый взгляд может показаться, что это противоречит отсутствию в Ржавчина нулевых (null) значений. Однако, если мы попытаемся использовать переменную, прежде чем присвоить ей значение, мы получим ошибку сборки, которая показывает, что Ржавчина действительно не разрешает нулевые (null) значения.
-
Внешняя область видимости объявляет переменную с именем r
без начального значения, а внутренняя область объявляет переменную с именем x
с начальным значением 5
. Во внутренней области мы пытаемся установить значение r
как ссылку на x
. Затем внутренняя область видимости заканчивается и мы пытаемся напечатать значение из r
. Этот код не будет собран, потому что значение на которое ссылается r
исчезает из области видимости, прежде чем мы попробуем использовать его. Вот сообщение об ошибке:
$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0597]: `x` does not live long enough
- --> src/main.rs:6:13
- |
-5 | let x = 5;
- | - binding `x` declared here
-6 | r = &x;
- | ^^ borrowed value does not live long enough
-7 | }
- | - `x` dropped here while still borrowed
-8 |
-9 | println!("r: {r}");
- | --- borrow later used here
-
-For more information about this error, try `rustc --explain E0597`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-Переменная x
«не живёт достаточно долго». Причина в том, что x
выйдет из области видимости, когда эта внутренняя область закончится в строке 7. Но r
все ещё является действительной во внешней области видимости; поскольку её охват больше, мы говорим, что она «живёт дольше». Если бы Ржавчина позволил такому коду работать, то переменная r
смогла бы ссылаться на память, которая уже была освобождена (в тот мгновение, когда x
вышла из внутренней области видимости), и всё что мы попытались бы сделать с r
работало бы неправильно. Как же Ржавчина определяет, что этот код неправилен? Он использует для этого анализатор заимствований (borrow checker).
Сборщик Ржавчина имеет в своём составе анализатор заимствований, который сравнивает области видимости для определения, являются ли все заимствования действительными. В приложении 10-17 показан тот же код, что и в приложении 10-16, но с изложениями, показывающими времена жизни переменных.
-fn main() {
- let r; // ---------+-- 'a
- // |
- { // |
- let x = 5; // -+-- 'b |
- r = &x; // | |
- } // -+ |
- // |
- println!("r: {r}"); // |
-} // ---------+
--
Здесь мы описали время жизни для r
с помощью 'a
и время жизни x
с помощью 'b
. Как видите, время жизни 'b
внутреннего раздела гораздо меньше, чем время жизни 'a
внешнего раздела. Во время сборки Ржавчина сравнивает продолжительность двух времён жизни и видит, что r
имеет время жизни 'a
, но ссылается на память со временем жизни 'b
. Программа отклоняется, потому что 'b
короче, чем 'a
: предмет ссылки не живёт так же долго, как сама ссылка.
Приложение 10-18 исправляет код, чтобы в нём не было повисшей ссылки, и собирается без ошибок.
--fn main() { - let x = 5; // ----------+-- 'b - // | - let r = &x; // --+-- 'a | - // | | - println!("r: {r}"); // | | - // --+ | -} // ----------+
-
Здесь переменная x
имеет время жизни 'b
, которое больше, чем время жизни 'a
. Это означает, что переменная r
может ссылаться на переменную x
потому что Ржавчина знает, что ссылка в переменной r
будет всегда действительной до тех пор, пока переменная x
является валидной.
После того, как мы на примерах рассмотрели времена жизни ссылок и обсудили как Ржавчина их анализирует, давайте поговорим об обобщённых временах жизни входных свойств и возвращаемых значений функций.
-Напишем функцию, которая возвращает более длинный из двух срезов строки. Эта функция принимает два среза строки и возвращает один срез строки. После того как мы выполнили функцию longest
, код в приложении 10-19 должен вывести The longest string is abcd
.
Файл: src/main.rs
-fn main() {
- let string1 = String::from("abcd");
- let string2 = "xyz";
-
- let result = longest(string1.as_str(), string2);
- println!("The longest string is {result}");
-}
--
Обратите внимание, что мы хотим чтобы функция принимала строковые срезы, которые являются ссылками, а не строки, потому что мы не хотим, чтобы функция longest
забирала во владение свои свойства. Обратитесь к разделу "Строковые срезы как свойства" Главы 4 для более подробного обсуждения того, почему свойства используемые в приложении 10-19 выбраны именно таким образом.
Если мы попробуем выполнить функцию longest
так, как это показано в приложении 10-20, программа не собирается:
Файл: src/main.rs
-fn main() {
- let string1 = String::from("abcd");
- let string2 = "xyz";
-
- let result = longest(string1.as_str(), string2);
- println!("The longest string is {result}");
-}
-
-fn longest(x: &str, y: &str) -> &str {
- if x.len() > y.len() {
- x
- } else {
- y
- }
-}
--
Вместо этого мы получим следующую ошибку, говорящую о временах жизни:
-$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0106]: missing lifetime specifier
- --> src/main.rs:9:33
- |
-9 | fn longest(x: &str, y: &str) -> &str {
- | ---- ---- ^ expected named lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
-help: consider introducing a named lifetime parameter
- |
-9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
- | ++++ ++ ++ ++
-
-For more information about this error, try `rustc --explain E0106`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-Текст ошибки показывает, что возвращаемому виду нужен обобщённый свойство времени жизни, потому что Ржавчина не может определить, относится ли возвращаемая ссылка к x
или к y
. На самом деле, мы тоже не знаем, потому что разделif
в теле функции возвращает ссылку на x
, а разделelse
возвращает ссылку на y
!
Когда мы определяем эту функцию, мы не знаем определенных значений, которые будут в неё передаваться. Поэтому мы не знаем какая из ветвей оператора if
или else
будет выполнена. Мы также не знаем определенных времён жизни ссылок, которые будут переданы в функцию, поэтому мы не можем посмотреть на их области видимости, как мы делали в примерах 10-17 и 10-18, чтобы определить, будет ли возвращаемая нами ссылка правильной во всех случаях. Анализатор заимствований также не может этого определить, потому что он не знает как времена жизни переменных x
и y
соотносятся с временем жизни возвращаемого значения. Чтобы исправить эту ошибку, мы добавим обобщённый свойство времени жизни, который определит отношения между ссылками таким образом, чтобы анализатор заимствований мог провести свой анализ.
Изложения времени жизни не меняют срок, как долго живёт та или иная ссылка. Они скорее описывают, как соотносятся между собой времена жизни нескольких ссылок, не влияя на само время жизни. Точно так же, как функции могут принимать любой вид, когда в ярлыке указан свойство обобщённого вида, функции могут принимать ссылки с любым временем жизни, указанным с помощью свойства обобщённого времени жизни.
-Изложения времени жизни имеют немного необычный правила написания: имена свойств времени жизни должны начинаться с апострофа ('
), пишутся маленькими буквами, и обычно очень короткие, как и имена обобщённых видов. Большинство людей использует имя 'a
в качестве первой изложении времени жизни. Изложения свойств времени жизни следуют после символа &
и отделяются пробелом от названия ссылочного вида.
Приведём несколько примеров: у нас есть ссылка на i32
без указания времени жизни, ссылка на i32
, с временем жизни имеющим имя 'a
и изменяемая ссылка на i32
, которая также имеет время жизни 'a
.
&i32 // a reference
-&'a i32 // a reference with an explicit lifetime
-&'a mut i32 // a mutable reference with an explicit lifetime
-Одна изложение времени жизни сама по себе не имеет большого значения, поскольку изложении предназначены для того, чтобы уведомить Ржавчина о том, как времена жизни нескольких ссылок соотносятся между собой. Давайте рассмотрим, как изложении времени жизни связаны друг с другом в среде функции longest
.
Чтобы использовать изложении времени жизни в ярлыках функций, нам нужно объявить свойства обобщённого времени жизни внутри угловых скобок между именем функции и списком свойств, как мы это делали с свойствами обобщённого вида .
-Мы хотим, чтобы ярлык отражала следующее ограничение: возвращаемая ссылка будет действительна до тех пор, пока валидны оба свойства. Это связь между временами жизни свойств и возвращаемого значения. Мы назовём это время жизни 'a
, а затем добавим его к каждой ссылке, как показано в приложении 10-21.
Файл: src/main.rs
--fn main() { - let string1 = String::from("abcd"); - let string2 = "xyz"; - - let result = longest(string1.as_str(), string2); - println!("The longest string is {result}"); -} - -fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { - if x.len() > y.len() { - x - } else { - y - } -}
-
Этот код должен собираться и давать желаемый итог, когда мы вызовем его в функции main
приложения 10-19.
Ярлык функции теперь сообщает Rust, что для некоторого времени жизни 'a
функция принимает два свойства, оба из которых являются срезами строк, которые живут не меньше, чем время жизни 'a
. Ярлык функции также сообщает Rust, что срез строки, возвращаемый функцией, будет жить как самое меньшее столько, сколько длится время жизни 'a
. В действительностиэто означает, что время жизни ссылки, возвращаемой функцией longest
, равно меньшему времени жизни передаваемых в неё ссылок. Мы хотим, чтобы Ржавчина использовал именно такие отношения при анализе этого кода.
Помните, когда мы указываем свойства времени жизни в этой ярлыке функции, мы не меняем время жизни каких-либо переданных или возвращённых значений. Скорее, мы указываем, что анализатор заимствований должен отклонять любые значения, которые не соответствуют этим ограничениям. Обратите внимание, что самой функции longest
не нужно точно знать, как долго будут жить x
и y
, достаточно того, что некоторая область может быть заменена на 'a
, которая будет удовлетворять этой ярлыке.
При определении времён жизни функций, изложении помещаются в ярлык функции, а не в тело функции. Изложения времени жизни становятся частью договора функции, как и виды в ярлыке. Наличие ярлыков функций, содержащих договор времени жизни, означает, что анализ который выполняет сборщик Rust, может быть проще. Если есть неполадка с тем, как функция определяется или как она вызывается, ошибки сборщика могут указать на часть нашего кода и ограничения более точно. Если бы вместо этого сборщик Ржавчина сделал больше предположений о том, какие отношения времён жизни мы хотели получить, сборщик смог бы указать только на использование нашего кода за много шагов от источника сбоев.
-Когда мы передаём определенные ссылки в функцию longest
, определенным временем жизни, которое будет заменено на 'a
, является часть области видимости x
, которая пересекается с областью видимости y
. Другими словами, обобщённое время жизни 'a
получит определенное время жизни, равное меньшему из времён жизни x
и y
. Так как мы определяли возвращаемую ссылку тем же свойствоом времени жизни 'a
, то возвращённая ссылка также будет действительна на протяжении меньшего из времён жизни x
и y
.
Давайте посмотрим, как изложении времени жизни ограничивают функцию longest
путём передачи в неё ссылок, которые имеют разные определенные времена жизни. Приложение 10-22 является очевидным примером.
Файл: src/main.rs
--fn main() { - let string1 = String::from("long string is long"); - - { - let string2 = String::from("xyz"); - let result = longest(string1.as_str(), string2.as_str()); - println!("The longest string is {result}"); - } -} - -fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { - if x.len() > y.len() { - x - } else { - y - } -}
-
В этом примере переменная string1
действительна до конца внешней области, string2
действует до конца внутренней области видимости и result
ссылается на что-то, что является действительным до конца внутренней области видимости. Запустите этот код, и вы увидите что анализатор заимствований разрешает такой код; он собирает и напечатает The longest string is long string is long
.
Теперь, давайте попробуем пример, который показывает, что время жизни ссылки result
должно быть меньшим временем жизни одного из двух переменных. Мы переместим объявление переменной result
за пределы внутренней области видимости, но оставим присвоение значения переменной result
в области видимости string2
. Затем мы переместим println!
, который использует result
за пределы внутренней области видимости, после того как внутренняя область видимости закончилась. Код в приложении 10-23 не собирается.
Файл: src/main.rs
-fn main() {
- let string1 = String::from("long string is long");
- let result;
- {
- let string2 = String::from("xyz");
- result = longest(string1.as_str(), string2.as_str());
- }
- println!("The longest string is {result}");
-}
-
-fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
- if x.len() > y.len() {
- x
- } else {
- y
- }
-}
--
При попытке собрать этот код, мы получим такую ошибку:
-$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0597]: `string2` does not live long enough
- --> src/main.rs:6:44
- |
-5 | let string2 = String::from("xyz");
- | ------- binding `string2` declared here
-6 | result = longest(string1.as_str(), string2.as_str());
- | ^^^^^^^ borrowed value does not live long enough
-7 | }
- | - `string2` dropped here while still borrowed
-8 | println!("The longest string is {result}");
- | -------- borrow later used here
-
-For more information about this error, try `rustc --explain E0597`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-Эта ошибка говорит о том, что если мы хотим использовать result
в указания println!
, переменная string2
должна бы быть действительной до конца внешней области видимости. Ржавчина знает об этом, потому что мы определяли свойства функции и её возвращаемое значение одинаковым временем жизни 'a
.
Будучи людьми, мы можем посмотреть на этот код и увидеть, что string1
длиннее, чем string2
и, следовательно, result
будет содержать ссылку на string1
. Поскольку string1
ещё не вышла из области видимости, ссылка на string1
будет все ещё действительной в указания println!
. Однако сборщик не видит, что ссылка в этом случае валидна. Мы сказали Rust, что время жизни ссылки, возвращаемой из функции longest
, равняется меньшему из времён жизни переданных в неё ссылок. Таким образом, анализатор заимствований запрещает код в приложении 10-23, как возможно имеющий недействительную ссылку.
Попробуйте провести больше экспериментов с различными значениями и временами жизни ссылок, передаваемых в функцию longest
, а также с тем, как используется возвращаемое значение Перед сборкой делайте предположения о том, пройдёт ли ваш код анализ заимствований, а затем проверяйте, насколько вы были правы.
В зависимости от того, что делает ваша функция, следует использовать разные способы указания свойств времени жизни. Например, если мы изменим выполнение функции longest
таким образом, чтобы она всегда возвращала свой первый переменная вместо самого длинного среза строки, то время жизни для свойства y
можно совсем не указывать. Этот код собирается:
Файл: src/main.rs
--fn main() { - let string1 = String::from("abcd"); - let string2 = "efghijklmnopqrstuvwxyz"; - - let result = longest(string1.as_str(), string2); - println!("The longest string is {result}"); -} - -fn longest<'a>(x: &'a str, y: &str) -> &'a str { - x -}
Мы указали свойство времени жизни 'a
для свойства x
и возвращаемого значения, но не для свойства y
, поскольку время жизни свойства y
никак не соотносится с временем жизни свойства x
или возвращаемого значения.
При возврате ссылки из функции, свойство времени жизни для возвращаемого вида должен соответствовать свойству времени жизни одного из переменных. Если возвращаемая ссылка не ссылается на один из свойств, она должна ссылаться на значение, созданное внутри функции. Однако, это приведёт к недействительной ссылке, поскольку значение, на которое она ссылается, выйдет из области видимости в конце функции. Посмотрите на попытку выполнения функции longest
, которая не собирается:
Файл: src/main.rs
-fn main() {
- let string1 = String::from("abcd");
- let string2 = "xyz";
-
- let result = longest(string1.as_str(), string2);
- println!("The longest string is {result}");
-}
-
-fn longest<'a>(x: &str, y: &str) -> &'a str {
- let result = String::from("really long string");
- result.as_str()
-}
-Здесь, несмотря на то, что мы указали свойство времени жизни 'a
для возвращаемого вида, выполнение не будет собрана, потому что время жизни возвращаемого значения никак не связано с временем жизни свойств. Получаем сообщение об ошибке:
$ cargo run
- Compiling chapter10 v0.1.0 (file:///projects/chapter10)
-error[E0515]: cannot return value referencing local variable `result`
- --> src/main.rs:11:5
- |
-11 | result.as_str()
- | ------^^^^^^^^^
- | |
- | returns a value referencing data owned by the current function
- | `result` is borrowed here
-
-For more information about this error, try `rustc --explain E0515`.
-error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
-
-Неполадказаключается в том, что result
выходит за область видимости и очищается в конце функции longest
. Мы также пытаемся вернуть ссылку на result
из функции. Мы не можем указать свойства времени жизни, которые могли бы изменить недействительную ссылку, а Ржавчина не позволит нам создать недействительную ссылку. В этом случае лучшим решением будет вернуть владеющий вид данных, а не ссылку: в этом случае вызывающая функция будет нести ответственность за очистку полученного ею значения.
В конечном итоге, правила написания времён жизни выполняет связывание времён жизни различных переменных и возвращаемых значений функций. Описывая времена жизни, мы даём Ржавчина достаточно сведений, чтобы разрешить безопасные действия с памятью и запретить действия, которые могли бы создать недействительные ссылки или иным способом нарушить безопасность памяти.
-До сих пор мы объявляли устройства, которые всегда содержали владеющие виды данных. Устройства могут содержать и ссылки, но при этом необходимо добавить изложение времени жизни для каждой ссылки в определении устройства. Приложение 10-24 описывает устройство ImportantExcerpt
, содержащую срез строки:
Файл: src/main.rs
--struct ImportantExcerpt<'a> { - part: &'a str, -} - -fn main() { - let novel = String::from("Call me Ishmael. Some years ago..."); - let first_sentence = novel.split('.').next().unwrap(); - let i = ImportantExcerpt { - part: first_sentence, - }; -}
-
У устройства имеется одно поле part
, хранящее срез строки, который сам по себе является ссылкой. Как и в случае с обобщёнными видами данных, мы объявляем имя обобщённого свойства времени жизни внутри угловых скобок после имени устройства, чтобы иметь возможность использовать его внутри определения устройства. Данная изложение означает, что образец ImportantExcerpt
не может пережить ссылку, которую он содержит в своём поле part
.
Функция main
здесь создаёт образец устройства ImportantExcerpt
, который содержит ссылку на первое предложение вида String
принадлежащее переменной novel
. Данные в novel
существуют до создания образца ImportantExcerpt
. Кроме того, novel
не выходит из области видимости до тех пор, пока ImportantExcerpt
не выйдет за область видимости, поэтому ссылка в внутри образца ImportantExcerpt
является действительной.
Вы изучили, что у каждой ссылки есть время жизни и что нужно указывать свойства времени жизни для функций или устройств, которые используют ссылки. Однако в Главе 4 у нас была функция в приложении 4-9, которая затем снова показана в приложении 10-25, в которой код собрался без наставлений времени жизни.
-Файл: src/lib.rs
--fn first_word(s: &str) -> &str { - let bytes = s.as_bytes(); - - for (i, &item) in bytes.iter().enumerate() { - if item == b' ' { - return &s[0..i]; - } - } - - &s[..] -} - -fn main() { - let my_string = String::from("hello world"); - - // first_word works on slices of `String`s - let word = first_word(&my_string[..]); - - let my_string_literal = "hello world"; - - // first_word works on slices of string literals - let word = first_word(&my_string_literal[..]); - - // Because string literals *are* string slices already, - // this works too, without the slice syntax! - let word = first_word(my_string_literal); -}
-
Причина, по которой этот код собирается — историческая. В ранних (до-1.0) исполнениях Ржавчина этот код не собрался бы, поскольку каждой ссылке нужно было явно назначать время жизни. В те времена, ярлык функции была бы написана примерно так:
-fn first_word<'a>(s: &'a str) -> &'a str {
-После написания большого количества кода на Ржавчина разработчики языка обнаружили, что в определённых случаейх программисты описывают одни и те же изложении времён жизни снова и снова. Эти случаи были предсказуемы и следовали нескольким определенным образцовым моделям. Объединение Ржавчина решила запрограммировать эти образцы в код сборщика Rust, чтобы анализатор заимствований мог вывести времена жизни в таких случаейх без необходимости явного указания наставлений программистами.
-Мы упоминаем этот отрывок истории Rust, потому что возможно, что в будущем появится больше образцов для самостоятельного выведения времён жизни, которые будут добавлены в сборщик. Таким образом, в будущем может понадобится ещё меньшее количество наставлений.
-Образцы, запрограммированные в анализаторе ссылок языка Rust, называются правилами неявного выведения времени жизни. Это не правила, которым должны следовать программисты; а набор частных случаев, которые рассмотрит сборщик, и, если ваш код попадает в эти случаи, вам не нужно будет указывать время жизни явно.
-Правила выведения не предоставляют полного заключения. Если Ржавчина определенно применяет правила, но некоторая неясность относительно времён жизни ссылок все ещё остаётся, сборщик не будет догадываться, какими должны быть времена жизни оставшихся ссылок. В этом случае, вместо угадывания сборщик выдаст ошибку, которую вы можете устранить, добавив изложении времени жизни.
-Времена жизни свойств функции или способа называются временем жизни ввода, а времена жизни возвращаемых значений называются временем жизни вывода.
-Сборщик использует три правила, чтобы выяснить времена жизни ссылок при отсутствии явных наставлений. Первое правило относится ко времени жизни ввода, второе и третье правила применяются ко временам жизни вывода. Если сборщик доходит до конца проверки трёх правил и всё ещё есть ссылки, для которых он не может выяснить время жизни, сборщик остановится с ошибкой. Эти правила применяются к объявлениям fn
, а также к разделам impl
.
Первое правило заключается в том, что каждый свойство являющийся ссылкой, получает свой собственный свойство времени жизни. Другими словами, функция с одним свойствоом получит один свойство времени жизни: fn foo<'a>(x: &'a i32)
; функция с двумя переменнойми получит два отдельных свойства времени жизни: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
, и так далее.
Второе правило говорит, что если есть ровно один входной свойство времени жизни, то его время жизни назначается всем выходным свойствам: fn foo<'a>(x: &'a i32) -> &'a i32
.
Третье правило о том, что если есть множество входных свойств времени жизни, но один из них является ссылкой &self
или &mut self
, так как эта функция является способом, то время жизни self
назначается временем жизни всем выходным свойствам. Это третье правило делает способы намного приятнее для чтения и записи, потому что требуется меньше символов.
Представим, что мы сборщик и применим эти правила, чтобы вывести времена жизни ссылок в ярлыке функции first_word
приложения 10-25. Ярлык этой функции начинается без объявления времён жизни ссылок:
fn first_word(s: &str) -> &str {
-Теперь мы (в качестве сборщика) применим первое правило, утверждающее, что каждый свойство функции получает своё собственное время жизни. Как обычно, назовём его 'a
и теперь ярлык выглядит так:
fn first_word<'a>(s: &'a str) -> &str {
-Далее применяем второе правило, поскольку в функции указан только один входной свойство времени жизни. Второе правило гласит, что время жизни единственного входного свойства назначается выходным свойствам, поэтому ярлык теперь преобразуется таким образом:
-fn first_word<'a>(s: &'a str) -> &'a str {
-Теперь все ссылки в этой функции имеют свойства времени жизни и сборщик может продолжить свой анализ без необходимости просить у программиста указать изложении времён жизни в ярлыке этой функции.
-Давайте рассмотрим ещё один пример: на этот раз функцию longest
, в которой не было свойств времени жизни, когда мы начали с ней работать в приложении 10-20:
fn longest(x: &str, y: &str) -> &str {
-Применим первое правило: каждому свойству назначается собственное время жизни. На этот раз у функции есть два свойства, поэтому есть два времени жизни:
-fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
-Можно заметить, что второе правило здесь не применимо, так как в ярлыке указано больше одного входного свойства времени жизни. Третье правило также не применимо, так как longest
— функция, а не способ, следовательно, в ней нет свойства self
. Итак, мы прошли все три правила, но так и не смогли вычислить время жизни выходного свойства. Поэтому мы и получили ошибку при попытке собрать код приложения 10-20: сборщик работал по правилам неявного выведения времён жизни, но не мог выяснить все времена жизни ссылок в ярлыке.
Так как третье правило применяется только к способам, далее мы рассмотрим времена жизни в этом среде, чтобы понять, почему нам часто не требуется определять времена жизни в ярлыках способов.
-Когда мы выполняем способы для устройств с временами жизни, мы используем тот же правила написания, который применялся для наставлений обобщённых видов данных на приложении 10-11. Место, где мы объявляем и используем времена жизни, зависит от того, с чем они связаны — с полями устройства, либо с переменнойми способов и возвращаемыми значениями.
-Имена свойств времени жизни для полей устройств всегда описываются после ключевого слова impl
и затем используются после имени устройства, поскольку эти времена жизни являются частью вида устройства.
В ярлыках способов внутри раздела impl
ссылки могут быть привязаны ко времени жизни ссылок в полях устройства, либо могут быть независимыми. Вдобавок, правила неявного выведения времён жизни часто делают так, что изложении переменных времён жизни являются необязательными в ярлыках способов. Рассмотрим несколько примеров, использующих устройство с названием ImportantExcerpt
, которую мы определили в приложении 10-24.
Сначала, воспользуемся способом level
, чей единственный свойство является ссылкой на self
, а возвращаемое значение i32
, не является ссылкой ни на что:
-struct ImportantExcerpt<'a> { - part: &'a str, -} - -impl<'a> ImportantExcerpt<'a> { - fn level(&self) -> i32 { - 3 - } -} - -impl<'a> ImportantExcerpt<'a> { - fn announce_and_return_part(&self, announcement: &str) -> &str { - println!("Attention please: {announcement}"); - self.part - } -} - -fn main() { - let novel = String::from("Call me Ishmael. Some years ago..."); - let first_sentence = novel.split('.').next().unwrap(); - let i = ImportantExcerpt { - part: first_sentence, - }; -}
Объявление свойства времени жизни после impl
и его использование после имени вида является обязательным, но нам не нужно определять время жизни ссылки на self
, благодаря первому правилу неявного выведения времён жизни.
Вот пример, где применяется третье правило неявного выведения времён жизни:
--struct ImportantExcerpt<'a> { - part: &'a str, -} - -impl<'a> ImportantExcerpt<'a> { - fn level(&self) -> i32 { - 3 - } -} - -impl<'a> ImportantExcerpt<'a> { - fn announce_and_return_part(&self, announcement: &str) -> &str { - println!("Attention please: {announcement}"); - self.part - } -} - -fn main() { - let novel = String::from("Call me Ishmael. Some years ago..."); - let first_sentence = novel.split('.').next().unwrap(); - let i = ImportantExcerpt { - part: first_sentence, - }; -}
В этом способе имеется два входных свойства, поэтому Ржавчина применит первое правило и назначит обоим свойствам &self
и announcement
собственные времена жизни. Далее, поскольку один из свойств является &self
, то возвращаемое значение получает время жизни переменой &self
и все времена жизни теперь выведены.
Одно особенное время жизни, которое мы должны обсудить, называется 'static
. Оно означает, что данная ссылка может жить всю продолжительность работы программы. Все строковые записи по умолчанию имеют время жизни 'static
, но мы можем указать его явным образом:
-#![allow(unused)] -fn main() { -let s: &'static str = "I have a static lifetime."; -}
Содержание этой строки сохраняется внутри двоичного файл программы и всегда доступно для использования. Следовательно, время жизни всех строковых записей равно 'static
.
Сообщения сборщика об ошибках в качестве решения сбоев могут предлагать вам использовать время жизни 'static
. Но прежде чем указывать 'static
как время жизни для ссылки, подумайте, на самом ли деле данная ссылка будет доступна во всё время работы программы. В большинстве случаев, сообщения об ошибках, предлагающие использовать время жизни 'static
появляются при попытках создания недействительных ссылок или несовпадения имеющихся времён жизни. В таких случаях, решение заключается в исправлении таких неполадок. а не в указании постоянного времени жизни 'static
.
Давайте кратко рассмотрим правила написания задания свойств обобщённых видов, ограничений особенности и времён жизни совместно в одной функции:
--fn main() { - let string1 = String::from("abcd"); - let string2 = "xyz"; - - let result = longest_with_an_announcement( - string1.as_str(), - string2, - "Today is someone's birthday!", - ); - println!("The longest string is {result}"); -} - -use std::fmt::Display; - -fn longest_with_an_announcement<'a, T>( - x: &'a str, - y: &'a str, - ann: T, -) -> &'a str -where - T: Display, -{ - println!("Announcement! {ann}"); - if x.len() > y.len() { - x - } else { - y - } -}
Это функция longest
из приложения 10-21, которая возвращает наибольший из двух срезов строки. Но теперь у неё есть дополнительный свойство с именем ann
обобщённого вида T
, который может быть представлен любым видом, выполняющим особенность Display
, как указано в предложении where
. Этот дополнительный свойство будет напечатан с использованием {}
, поэтому ограничение особенности Display
необходимо. Поскольку время жизни является обобщённым видом, то объявления свойства времени жизни 'a
и свойства обобщённого вида T
помещаются в один список внутри угловых скобок после имени функции.
В этой главе мы рассмотрели много всего! Теперь вы знакомы с свойствами обобщённого вида, особенностями и ограничениями особенности, обобщёнными свойствами времени жизни, вы готовы писать код без повторений, который будет работать во множестве различных случаев. Свойства обобщённого вида позволяют использовать код для различных видов данных. Особенности и ограничения особенности помогают убедиться, что, хотя виды и обобщённые, они будут вести себя, как этого требует ваш код. Вы изучили, как использовать изложении времени жизни чтобы убедиться, что этот гибкий код не будет порождать никаких повисших ссылок. И весь этот анализ происходит в мгновение сборки и не влияет на производительность программы во время работы!
-Верите или нет, но в рамках этой темы всё есть ещё чему поучиться: в Главе 17 обсуждаются особенности-предметы, которые являются ещё одним способом использования особенностей. Существуют также более сложные сценарии с изложениями времени жизни, которые вам понадобятся только в очень сложных случаях; для этого вам следует прочитать Rust Reference. Далее вы узнаете, как писать проверки на Rust, чтобы убедиться, что ваш код работает так, как задумано.
- -В своём эссе 1972 года “The Humble Programmer,” Edsger W. Dijkstra сказал, что «Проверка программы может быть очень эффективным способом показать наличие ошибок, но это безнадёжно неадекватно для показа их отсутствия». Это не значит, что мы не должны пытаться проверять столько, сколько мы можем!
-Соблюдение правил программы считается то, в какой степени наш код выполняет именно то, что мы задумывали. Ржавчина разработан с учётом большой озабоченности соблюдением правил программ, но соблюдение правил сложна и нелегко доказуема. Система определения Ржавчина берет на себя огромную часть этого бремени, но она не может уловить абсолютно все сбоев. Поэтому в Ржавчина предусмотрена возможность написания автопроверок.
-Допустим, мы пишем функцию add_two
, которая прибавляет 2 к любому переданному ей числу. Ярлык этой функции принимает целое число в качестве свойства и возвращает целое число в качестве итога. Когда мы выполняем и собираем эту функцию, Ржавчина выполняет всю проверку видов и проверку заимствований, которую вы уже изучили, чтобы убедиться, что, например, мы не передаём значение String
или недопустимую ссылку в эту функцию. Но Ржавчина не способен проверить, что эта функция сделает именно то, что мы задумали, то есть вернёт свойство плюс 2, а не, скажем, свойство плюс 10 или свойство - 50! Вот тут-то и приходят на помощь проверки.
Мы можем написать проверки, которые утверждают, например, что когда мы передаём 3
в функцию add_two
, возвращаемое значение будет 5
. Мы можем запускать эти проверки всякий раз, когда мы вносим изменения в наш код, чтобы убедиться, что любое существующее правильное поведение не изменилось.
Проверка - сложный навык: мы не сможем охватить все подробности написания хороших проверок в одной главе, но мы обсудим основные подходы к проверке в Rust. Мы поговорим об изложениех и макросах, доступных вам для написания проверок, о поведении по умолчанию и свойствах, предусмотренных для запуска проверок, а также о том, как согласовать проверки в состоящие из звеньев проверки и встроенные проверки.
- -Проверки - это функции Rust, которые проверяют, что не проверочный код работает ожидаемым образом. Содержимое проверочных функций обычно выполняет следующие три действия:
-Давайте рассмотрим функции предоставляемые в Ржавчина целенаправленно для написания проверок, которые выполнят все эти действия, включая свойство test
, несколько макросов и свойство should_panic
.
В простейшем случае в Ржавчина проверка - это функция, определенная свойством test
. Свойства представляют собой метаданные о отрывках кода Rust; один из примеров свойство derive
, который мы использовали со устройствами в главе 5. Чтобы превратить функцию в проверяющую функцию добавьте #[test]
в строку перед fn
. Когда вы запускаете проверки приказом cargo test
, Ржавчина создаёт двоичный звено выполняющий функции определеные свойством test и сообщающий о том, успешно или нет прошла каждая проверяющая функция.
Когда мы создаём новый дело библиотеки с помощью Cargo, то в нём самостоятельно порождается проверочный звено с проверку-функцией для нас. Этот звено даст вам образец для написания ваших проверок, так что вам не нужно искать точную устройство и правила написания проверочных функций каждый раз, когда вы начинаете новый дело. Вы можете добавить столько дополнительных проверочных функций и столько проверочных звеньев, сколько захотите!
-Мы исследуем некоторые особенности работы проверок, экспериментируя с образцовым проверкой созданным для нас, без существующего проверки любого кода. Затем мы напишем некоторые существующие проверки, которые вызывают некоторый написанный код и убедимся в его правильном поведении. Мы рассмотрим некоторые особенности работы проверок, поэкспериментируем с образцовым проверкой, прежде чем приступать к действительному проверке любого кода. Затем мы напишем несколько существующих проверок, которые вызывают некоторый написанный нами код и проверяют, что его поведение правильное.
-Давайте создадим новый библиотечный дело под названием adder
, который складывает два числа:
$ cargo new adder --lib
- Created library `adder` project
-$ cd adder
-
-Содержимое файла src/lib.rs вашей библиотеки adder
должно выглядеть как в приложении 11-1.
Файл: src/lib.rs
- -pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-}
--
Сейчас давайте пренебрегаем первые две строчки кода и сосредоточимся на функции. Обратите внимание на правила написания изложении #[test]
: этот свойство указывает, что это проверочная функция, поэтому запускающий проверка знает, что эту функцию следует рассматривать как проверочную. У нас также могут быть не проверяемые функции в звене tests
, которые помогут настроить общие сценарии или выполнить общие действия, поэтому нам всегда нужно указывать, какие функции являются проверкими.
В теле функции проверки используется макрос assert_eq!
, чтобы утверждать, что result
, который содержит итог сложения 2 и 2, равен 4. Это утверждение служит примером вида для типичного проверки. Давайте запустим его, чтобы убедиться, что этот проверка пройден.
Приказ cargo test
выполнит все проверки в выбранном деле и сообщит о итогах как в приложении 11-2:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
--
Cargo собрал и выполнил проверку. Мы видим строку running 1 test
. Следующая строка показывает имя созданной проверочной функции, называемой it_works
, и итог запуска этого проверки равный ok
. Текст test result: ok.
означает, что все проверки пройдены успешно и часть вывода 1 passed; 0 failed
сообщает общее количество проверок, которые прошли или были ошибочными.
Можно пометить проверка как пренебрегаемый, чтобы он не выполнялся в определенном случае; мы рассмотрим это в разделе “Пренебрежение некоторых проверок, если их целенаправленно не запрашивать” позже в этой главе. Поскольку в данный мгновение мы этого не сделали, в сводке показано, что 0 ignored
. Мы также можем передать переменная приказу cargo test
для запуска только тех проверок, имя которых соответствует строке; это называется выборкой, и мы рассмотрим это в разделе “Запуск подмножества проверок по имени”. Мы также не фильтровали выполняемые проверки, поэтому в конце сводки показано, что 0 filtered out
.
Исчисление 0 measured
предназначена для проверок производительности. На мгновение написания этой статьи такие проверки доступны только в ночной сборке Rust. Посмотрите документацию о проверках производительности, чтобы узнать больше.
Следующая часть вывода проверок начинается с Doc-tests adder
- это сведения о проверках в документации. У нас пока нет проверок документации, но Ржавчина может собирать любые примеры кода, которые находятся в API документации. Такая возможность помогает поддерживать документацию и код в согласованном состоянии. Мы поговорим о написании проверок документации в разделы "Примечания документации как проверки" Главы 14. Пока просто пренебрегаем часть Doc-tests
вывода.
Давайте начнём настраивать проверка в соответствии с нашими собственными потребностями. Сначала поменяем название нашего проверки it_works
на exploration
, вот так:
Файл: src/lib.rs
-pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn exploration() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-}
-Снова выполним приказ cargo test
. Вывод показывает наименование нашей проверку-функции - exploration
вместо it_works
:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::exploration ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Добавим ещё один проверка, но в этот раз целенаправленно сделаем так, чтобы этот новый проверка не отработал! Проверка терпит неудачу, когда что-то паникует в проверяемой функции. Каждый проверка запускается в новом потоке и когда главный поток видит, что проверочный поток упал, то помечает проверка как завершившийся со сбоем. Мы говорили о простейшем способе вызвать панику в главе 9, используя для этого известный макрос panic!
. Введём код проверку-функции another
, как в файле src/lib.rs из приложения 11-3.
Файл: src/lib.rs
-pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn exploration() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-
- #[test]
- fn another() {
- panic!("Make this test fail");
- }
-}
--
Запустим приказ cargo test
. Вывод итогов показан в приложении 11-4, который сообщает, что проверка exploration
пройден, а another
нет:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 2 tests
-test tests::another ... FAILED
-test tests::exploration ... ok
-
-failures:
-
----- tests::another stdout ----
-thread 'tests::another' panicked at src/lib.rs:17:9:
-Make this test fail
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::another
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
--
Вместо ok
, строка test tests::another
сообщает FAILED
. Две новые разделы появились между отдельными итогами и сводкой: в первом отображается подробная причина каждого сбоя проверки. В данном случае проверка another
не сработал, потому что panicked at 'Make this test fail'
, произошло в строке 10 файла src/lib.rs. В следующем разделе перечисляют имена всех не пройденных проверок, что удобно, когда есть много проверок и много подробных итогов неудачных проверок. Мы можем использовать имя не пройденного проверки для его дальнейшей отладки; мы больше поговорим о способах запуска проверок в разделе "Управление хода выполнения проверок".
Итоговая строка отображается в конце: общий итог нашего проверки FAILED
. У нас один проверка пройден и один проверка завершён со сбоем.
Теперь, когда вы увидели, как выглядят итоги проверки при разных сценариях, давайте рассмотрим другие макросы полезные в проверках, кроме panic!
.
assert!
Макрос assert!
доступен из встроенной библиотеки и является удобным, когда вы хотите проверить что некоторое условие в проверке вычисляется в значение true
. Мы передаём в макрос assert!
переменная, который вычисляется в логическое значение. Если оно true
, то ничего не происходит и проверка считается пройденным. Если же значение вычисляется в false
, то макрос assert!
вызывает макрос panic!
, чтобы вызвать сбой проверки. Использование макроса assert!
помогает проверить, что код исполняется как ожидалось.
В главе 5, приложении 5-15, мы использовали устройство Rectangle
и способ can_hold
, который повторён в приложении 11-5. Давайте поместим этот код в файл src/lib.rs и напишем несколько проверок для него используя макрос assert!
.
Файл: src/lib.rs
-#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width > other.width && self.height > other.height
- }
-}
--
Способ can_hold
возвращает логическое значение, что означает, что он является наилучшим исходом использования в макросе assert!
. В приложении 11-6 мы пишем проверка, который выполняет способ can_hold
путём создания образца Rectangle
шириной 8 и высотой 7 и убеждаемся, что он может содержать другой образец Rectangle
имеющий ширину 5 и высоту 1.
Файл: src/lib.rs
-#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width > other.width && self.height > other.height
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn larger_can_hold_smaller() {
- let larger = Rectangle {
- width: 8,
- height: 7,
- };
- let smaller = Rectangle {
- width: 5,
- height: 1,
- };
-
- assert!(larger.can_hold(&smaller));
- }
-}
--
Также, в звене tests
обратите внимание на новую добавленную строку use super::*;
. Звено tests
является обычным и подчиняется тем же правилам видимости, которые мы обсуждали в главе 7 "Пути для ссылки на элементы внутри дерева звена". Так как этот звено tests
является внутренним, нужно подключить проверяемый код из внешнего звена в область видимости внутреннего звена с проверкими. Для этого используется вездесущеее подключение, так что все что определено во внешнем звене становится доступным внутри tests
звена.
Мы назвали наш проверка larger_can_hold_smaller
и создали два нужных образца Rectangle
. Затем вызвали макрос assert!
и передали итог вызова larger.can_hold(&smaller)
в него. Это выражение должно возвращать true
, поэтому наш проверка должен пройти. Давайте выясним!
$ cargo test
- Compiling rectangle v0.1.0 (file:///projects/rectangle)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
- Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
-
-running 1 test
-test tests::larger_can_hold_smaller ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests rectangle
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Проверка проходит. Теперь добавим другой проверка, в этот раз мы попытаемся убедиться, что меньший прямоугольник не может содержать больший прямоугольник:
-Файл: src/lib.rs
-#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-impl Rectangle {
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width > other.width && self.height > other.height
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn larger_can_hold_smaller() {
- // --snip--
- let larger = Rectangle {
- width: 8,
- height: 7,
- };
- let smaller = Rectangle {
- width: 5,
- height: 1,
- };
-
- assert!(larger.can_hold(&smaller));
- }
-
- #[test]
- fn smaller_cannot_hold_larger() {
- let larger = Rectangle {
- width: 8,
- height: 7,
- };
- let smaller = Rectangle {
- width: 5,
- height: 1,
- };
-
- assert!(!smaller.can_hold(&larger));
- }
-}
-Поскольку правильный итог функции can_hold
в этом случае false
, то мы должны инвертировать этот итог, прежде чем передадим его в assert!
макро. Как итог, наш проверка пройдёт, если can_hold
вернёт false
:
$ cargo test
- Compiling rectangle v0.1.0 (file:///projects/rectangle)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
- Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
-
-running 2 tests
-test tests::larger_can_hold_smaller ... ok
-test tests::smaller_cannot_hold_larger ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests rectangle
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Два проверки работают. Теперь проверим, как отреагируют проверки, если мы добавим ошибку в код. Давайте изменим выполнение способа can_hold
заменив одно из логических выражений знак сравнения с "больше чем" на противоположный "меньше чем" при сравнении ширины:
#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-// --snip--
-impl Rectangle {
- fn can_hold(&self, other: &Rectangle) -> bool {
- self.width < other.width && self.height > other.height
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn larger_can_hold_smaller() {
- let larger = Rectangle {
- width: 8,
- height: 7,
- };
- let smaller = Rectangle {
- width: 5,
- height: 1,
- };
-
- assert!(larger.can_hold(&smaller));
- }
-
- #[test]
- fn smaller_cannot_hold_larger() {
- let larger = Rectangle {
- width: 8,
- height: 7,
- };
- let smaller = Rectangle {
- width: 5,
- height: 1,
- };
-
- assert!(!smaller.can_hold(&larger));
- }
-}
-Запуск проверок теперь производит следующее:
-$ cargo test
- Compiling rectangle v0.1.0 (file:///projects/rectangle)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
- Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
-
-running 2 tests
-test tests::larger_can_hold_smaller ... FAILED
-test tests::smaller_cannot_hold_larger ... ok
-
-failures:
-
----- tests::larger_can_hold_smaller stdout ----
-thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
-assertion failed: larger.can_hold(&smaller)
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::larger_can_hold_smaller
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Наши проверки нашли ошибку! Так как в проверке larger.width
равно 8 и smaller.width
равно 5 сравнение ширины в способе can_hold
возвращает итог false
, поскольку число 8 не меньше чем 5.
assert_eq!
и assert_ne!
Общим способом проверки возможности является использование сравнения итога проверяемого кода и ожидаемого значения, чтобы убедиться в их равенстве. Для этого можно использовать макрос assert!
, передавая ему выражение с использованием оператора ==
. Важно также знать, что кроме этого обычная библиотека предлагает пару макросов assert_eq!
и assert_ne!
, чтобы сделать проверка более удобным. Эти макросы сравнивают два переменной на равенство или неравенство соответственно. Макросы также печатают два значения входных свойств, если проверка завершился ошибкой, что позволяет легче увидеть почему проверка ошибочен. Противоположно этому, макрос assert!
может только отобразить, что он вычислил значение false
для выражения ==
, но не значения, которые привели к итогу false
.
В приложении 11-7, мы напишем функцию add_two
, которая прибавляет к входному свойству 2
и возвращает значение. Затем, проверим эту функцию с помощью макроса assert_eq!
:
Файл: src/lib.rs
-pub fn add_two(a: usize) -> usize {
- a + 2
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_adds_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
- }
-}
--
Проверим, что проверки проходят!
-$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Первый переменная, который мы передаём в макрос assert_eq!
число 4
чей итог вызова равен add_two(2)
. Строка для этого проверки - test tests::it_adds_two ... ok
, а текст ok
означает, что наш проверка пройден!
Давайте введём ошибку в код, чтобы увидеть, как она выглядит, когда проверка, который использует assert_eq!
завершается ошибкой. Измените выполнение функции add_two
, чтобы добавлять 3
:
pub fn add_two(a: usize) -> usize {
- a + 3
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_adds_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
- }
-}
-Попробуем выполнить данный проверка ещё раз:
-$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::it_adds_two ... FAILED
-
-failures:
-
----- tests::it_adds_two stdout ----
-thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
-assertion `left == right` failed
- left: 5
- right: 4
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::it_adds_two
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Наш проверка нашёл ошибку! Проверка it_adds_two
не выполнился, отображается сообщение assertion failed:
(left == right)`` и показывает, что left
было 4
, а right
было 5
. Это сообщение полезно и помогает начать отладку: это означает left
переменная assert_eq!
имел значение 4
, но right
переменная для вызова add_two(2)
был со значением 5
.
Обратите внимание, что в некоторых языках (таких как Java) в библиотеках кода для проверки принято именовать входные свойства проверочных функций как "ожидаемое" (expected
) и "действительное" (actual
). В Ржавчина приняты следующие обозначения left
и right
соответственно, а порядок в котором определяются ожидаемое значение и производимое проверяемым кодом значение не имеют значения. Мы могли бы написать выражение в проверке как assert_eq!(add_two(2), 4)
, что приведёт к отображаемому сообщению об ошибке assertion failed:
(left == right)``, слева left
было бы 5
, а справа right
было бы 4
.
Макрос assert_ne!
сработает успешно, если входные свойства не равны друг другу и завершится с ошибкой, если значения равны. Этот макрос наиболее полезен в тех случаях, когда мы не знаем заранее, каким значение будет, но знаем точно, каким оно не может быть. К примеру, если проверяется функция, которая обязательно изменяет входные данные определённым образом, но способ изменения входного свойства зависит от дня недели, в который запускаются проверки, что лучший способ проверить правильность работы такой функции - это сравнить и убедиться, что выходное значение функции не должно быть равным входному значению.
В своей работе макросы assert_eq!
и assert_ne!
неявным образом используют операторы ==
и !=
соответственно. Когда проверка не срабатывает, макросы печатают значения переменных с помощью отладочного изменения и это означает, что значения сравниваемых переменных должны выполнить особенности PartialEq
и Debug
. Все простые и большая часть видов встроенной библиотеки Ржавчина выполняют эти особенности. Для устройств и перечислений, которые вы выполняете сами будет необходимо выполнить особенность PartialEq
для сравнения значений на равенство или неравенство. Для печати отладочной сведений в виде сообщений в строку вывода окне вывода необходимо выполнить особенность Debug
. Так как оба особенности являются выводимыми особенностями, как упоминалось в приложении 5-12 главы 5, то эти особенности можно выполнить добавив изложение #[derive(PartialEq, Debug)]
к определению устройства или перечисления. Смотрите больше подробностей в Appendix C "Выводимые особенности" про эти и другие выводимые особенности.
Также можно добавить пользовательское сообщение как дополнительный переменная макросов для печати в сообщении об ошибке проверки assert!
, assert_eq!
, и assert_ne!
. Любые переменные, указанные после обязательных переменных, далее передаются в макрос format!
(он обсуждается в разделе "Сцепление с помощью оператора +
или макроса format!"), так что вы можете передать измененную строку, которая содержит {}
для заполнителей и значения, заменяющие эти заполнители. Пользовательские сообщения полезны для пояснения того, что означает утверждение (assertion); когда проверка завершается неудачей, у вас будет лучшее представление о том, в чем неполадка с кодом.
Например, есть функция, которая приветствует человека по имени и мы хотим проверять эту функцию. Мы хотим чтобы передаваемое ей имя выводилось в окно вывода:
-Файл: src/lib.rs
-pub fn greeting(name: &str) -> String {
- format!("Hello {name}!")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn greeting_contains_name() {
- let result = greeting("Carol");
- assert!(result.contains("Carol"));
- }
-}
-Требования к этой программе ещё не были согласованы и мы вполне уверены, что текст Hello
в начале приветствия ещё изменится. Мы решили, что не хотим обновлять проверка при изменении требований, поэтому вместо проверки на точное равенство со значением возвращённым из greeting
, мы просто будем проверять, что вывод содержит текст из входного свойства.
Давайте внесём ошибку в этот код, изменив greeting
так, чтобы оно не включало name
и увидим, как выглядит сбой этого проверки:
pub fn greeting(name: &str) -> String {
- String::from("Hello!")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn greeting_contains_name() {
- let result = greeting("Carol");
- assert!(result.contains("Carol"));
- }
-}
-Запуск этого проверки выводит следующее:
-$ cargo test
- Compiling greeter v0.1.0 (file:///projects/greeter)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
- Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
-
-running 1 test
-test tests::greeting_contains_name ... FAILED
-
-failures:
-
----- tests::greeting_contains_name stdout ----
-thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
-assertion failed: result.contains("Carol")
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::greeting_contains_name
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Сообщение содержит лишь сведения о том что сравнение не было успешным и в какой строке это произошло. В данном случае, более полезный текст сообщения был бы, если бы также выводилось значение из функции greeting
. Изменим проверяющую функцию так, чтобы выводились пользовательское сообщение измененное строкой с заменителем и действительными данными из кода greeting
:
pub fn greeting(name: &str) -> String {
- String::from("Hello!")
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn greeting_contains_name() {
- let result = greeting("Carol");
- assert!(
- result.contains("Carol"),
- "Greeting did not contain name, value was `{result}`"
- );
- }
-}
-После того, как выполним проверка ещё раз мы получим подробное сообщение об ошибке:
-$ cargo test
- Compiling greeter v0.1.0 (file:///projects/greeter)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
- Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
-
-running 1 test
-test tests::greeting_contains_name ... FAILED
-
-failures:
-
----- tests::greeting_contains_name stdout ----
-thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
-Greeting did not contain name, value was `Hello!`
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::greeting_contains_name
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Мы можем увидеть значение, которое мы на самом деле получили в проверочном выводе, что поможет нам отлаживать произошедшее, а не то, что мы ожидали.
-should_panic
В дополнение к проверке того, что наш код возвращает правильные, ожидаемые значения, важным также является проверить, что наш код обрабатывает ошибки, которые мы ожидаем. Например, рассмотрим вид Guess
который мы создали в главе 9, приложения 9-10. Другой код, который использует Guess
зависит от заверения того, что Guess
образцы будут содержать значения только от 1 до 100. Мы можем написать проверка, который заверяет, что попытка создать образец Guess
со значением вне этого ряда вызывает панику.
Выполняем это с помощью другого свойства проверку-функции #[should_panic]
. Этот свойство сообщает системе проверки, что проверка проходит, когда способ порождает ошибку. Если ошибка не порождается - проверка считается не пройденным.
Приложение 11-8 показывает проверка, который проверяет, что условия ошибки Guess::new
произойдут, когда мы их ожидаем их.
Файл: src/lib.rs
-pub struct Guess {
- value: i32,
-}
-
-impl Guess {
- pub fn new(value: i32) -> Guess {
- if value < 1 || value > 100 {
- panic!("Guess value must be between 1 and 100, got {value}.");
- }
-
- Guess { value }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- #[should_panic]
- fn greater_than_100() {
- Guess::new(200);
- }
-}
--
Свойство #[should_panic]
следует после #[test]
и до объявления проверочной функции. Посмотрим на вывод итога, когда проверка проходит:
$ cargo test
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
- Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
-
-running 1 test
-test tests::greater_than_100 - should panic ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests guessing_game
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Выглядит хорошо! Теперь давайте внесём ошибку в наш код, убрав условие о том, что функция new
будет паниковать если значение больше 100:
pub struct Guess {
- value: i32,
-}
-
-// --snip--
-impl Guess {
- pub fn new(value: i32) -> Guess {
- if value < 1 {
- panic!("Guess value must be between 1 and 100, got {value}.");
- }
-
- Guess { value }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- #[should_panic]
- fn greater_than_100() {
- Guess::new(200);
- }
-}
-Когда мы запустим проверка в приложении 11-8, он потерпит неудачу:
-$ cargo test
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
- Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
-
-running 1 test
-test tests::greater_than_100 - should panic ... FAILED
-
-failures:
-
----- tests::greater_than_100 stdout ----
-note: test did not panic as expected
-
-failures:
- tests::greater_than_100
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Мы получаем не очень полезное сообщение в этом случае, но когда мы смотрим на проверяющую функцию, мы видим, что она #[should_panic]
. Со сбоеме выполнение, которое мы получили означает, что код в проверяющей функции не вызвал паники.
Проверки, которые используют should_panic
могут быть неточными, потому что они только указывают, что код вызвал панику. Проверка с свойством should_panic
пройдёт, даже если проверка паникует по причине, отличной от той, которую мы ожидали. Чтобы сделать проверки с should_panic
более точными, мы можем добавить необязательный свойство expected
для свойства should_panic
. Такая подробностизация проверки позволит удостовериться, что сообщение об ошибке содержит предоставленный текст. Например, рассмотрим измененный код для Guess
в приложении 11-9, где new
функция паникует с различными сообщениями в зависимости от того, является ли значение слишком маленьким или слишком большим.
Файл: src/lib.rs
-pub struct Guess {
- value: i32,
-}
-
-// --snip--
-
-impl Guess {
- pub fn new(value: i32) -> Guess {
- if value < 1 {
- panic!(
- "Guess value must be greater than or equal to 1, got {value}."
- );
- } else if value > 100 {
- panic!(
- "Guess value must be less than or equal to 100, got {value}."
- );
- }
-
- Guess { value }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- #[should_panic(expected = "less than or equal to 100")]
- fn greater_than_100() {
- Guess::new(200);
- }
-}
--
Этот проверка пройдёт, потому что значение, которое мы помеисполнения для should_panic
в свойство свойства expected
является подстрокой сообщения, с которым функция Guess::new
вызывает панику. Мы могли бы указать полное, ожидаемое сообщение для паники, в этом случае это будет Guess value must be less than or equal to 100, got 200
. То что вы выберите для указания как ожидаемого свойства у should_panic
зависит от того, какая часть сообщения о панике неповторима или динамична, насколько вы хотите, чтобы ваш проверка был точным. В этом случае достаточно подстроки из сообщения паники, чтобы обеспечить выполнение кода в проверочной функции else if value > 100
.
Чтобы увидеть, что происходит, когда проверка should_panic
неуспешно завершается с сообщением expected
, давайте снова внесём ошибку в наш код, поменяв местами if value < 1
и else if value > 100
блоки:
pub struct Guess {
- value: i32,
-}
-
-impl Guess {
- pub fn new(value: i32) -> Guess {
- if value < 1 {
- panic!(
- "Guess value must be less than or equal to 100, got {value}."
- );
- } else if value > 100 {
- panic!(
- "Guess value must be greater than or equal to 1, got {value}."
- );
- }
-
- Guess { value }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- #[should_panic(expected = "less than or equal to 100")]
- fn greater_than_100() {
- Guess::new(200);
- }
-}
-На этот раз, когда мы выполним should_panic
проверка, он потерпит неудачу:
$ cargo test
- Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
- Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
-
-running 1 test
-test tests::greater_than_100 - should panic ... FAILED
-
-failures:
-
----- tests::greater_than_100 stdout ----
-thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
-Guess value must be greater than or equal to 1, got 200.
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-note: panic did not contain expected string
- panic message: `"Guess value must be greater than or equal to 1, got 200."`,
- expected substring: `"less than or equal to 100"`
-
-failures:
- tests::greater_than_100
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Сообщение об ошибке указывает, что этот проверка действительно вызвал панику, как мы и ожидали, но сообщение о панике не включено ожидаемую строку 'Guess value must be less than or equal to 100'
. Сообщение о панике, которое мы получили в этом случае, было Guess value must be greater than or equal to 1, got 200.
Теперь мы можем начать выяснение, где ошибка!
Result<T, E>
в проверкахПока что мы написали проверки, которые паникуют, когда терпят неудачу. Мы также можем написать проверки которые используют Result<T, E>
! Вот проверка из приложения 11-1, переписанный с использованием Result<T, E>
и возвращающий Err
вместо паники:
pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- // ANCHOR: here
- #[test]
- fn it_works() -> Result<(), String> {
- let result = add(2, 2);
-
- if result == 4 {
- Ok(())
- } else {
- Err(String::from("two plus two does not equal four"))
- }
- }
- // ANCHOR_END: here
-}
-Функция it_works
теперь имеет возвращаемый вид Result<(), String>
. В теле функции, вместо вызова макроса assert_eq!
, мы возвращаем Ok(())
когда проверка успешно выполнен и Err
со String
внутри, когда проверка не проходит.
Написание проверок так, чтобы они возвращали Result<T, E>
позволяет использовать оператор "вопросительный знак" в теле проверок, который может быть удобным способом писать проверки, которые должны выполниться не успешно, если какая-либо действие внутри них возвращает исход ошибки Err
.
Вы не можете использовать изложение #[should_panic]
в проверках, использующих Result<T, E>
. Чтобы утверждать, что действие возвращает исход Err
, не используйте оператор вопросительного знака для значения Result<T, E>
. Вместо этого используйте assert!(value.is_err())
.
Теперь, когда вы знаете несколько способов написания проверок, давайте взглянем на то, что происходит при запуске проверок и исследуем разные возможности используемые с приказом cargo test
.
Подобно тому, как cargo run
выполняет сборку вашего кода, а затем запускает полученный двоичный файл, cargo test
собирает ваш код в режиме проверки и запускает полученный двоичный файл с проверкими. Двоичный файл, создаваемый cargo test
, по умолчанию запускает все проверки одновременно и перехватывает вывод, порождаемый во время выполнения проверок, предотвращая их вывод на экран для облегчения чтения вывода, относящегося к итогам проверки. Однако вы можете указать свойства приказной строки, чтобы изменить это поведение по умолчанию.
Часть свойств приказной строки передаётся в cargo test
, а часть - в итоговый двоичный файл с проверкими. Чтобы разделить эти два вида переменных, нужно сначала указать переменные, которые идут в cargo test
, затем использовать разделитель --
, а потом те, которые попадут в двоичный файл проверки. Выполнение cargo test --help
выводит возможности, которые вы можете использовать с cargo test
, а выполнение cargo test -- --help
выводит возможности, которые вы можете использовать за разделителем.
Когда вы запускаете несколько проверок, по умолчанию они выполняются одновременно с использованием потоков, что означает, что они завершатся быстрее, и вы быстрее получите итоги. Поскольку проверки выполняются одновременно, вы должны убедиться, что ваши проверки не зависят друг от друга или от какого-либо общего состояния, включая общее окружение, например, текущий рабочий папка или переменные окружения.
-Например, допустим, каждый из ваших проверок запускает код, который создаёт файл на диске с именем test-output.txt и записывает некоторые данные в этот файл. Затем каждый проверка считывает данные из этого файла и утверждает, что файл содержит определённое значение, которое в каждом проверке разное. Поскольку все проверки выполняются одновременно, один из проверок может перезаписать файл в промежутке между записью и чтением файла другим проверкой. Тогда второй проверка потерпит неудачу, но не потому, что код неверен, а потому, что эти проверки мешали друг другу при одновременном выполнении. Одно из решений - убедиться, что каждый проверка пишет в свой отдельный файл; другое решение - запускать проверки по одному.
-Если вы не хотите запускать проверки одновременно или хотите более подробный управление над количеством используемых потоков, можно установить флаг --test-threads
и то количество потоков, которое вы хотите использовать для проверки. Взгляните на следующий пример:
$ cargo test -- --test-threads=1
-
-Мы устанавливаем количество проверочных потоков равным 1
, указывая программе не использовать одновременность. Выполнение проверок с использованием одного потока займёт больше времени, чем их одновременное выполнение, но проверки не будут мешать друг другу, если они совместно используют состояние.
По умолчанию, если проверка пройден, система управления запуска проверок блокирует вывод на печать, т.е. если вы вызовете макрос println!
внутри кода проверки и проверка будет пройден, вы не увидите вывода на окно вывода итогов вызова println!
. Если же проверка не был пройден, все несущие сведения сообщения, а также описание ошибки будут выведены на окно вывода.
Например, в коде (11-10) функция выводит значение свойства с поясняющим текстовым сообщением, а также возвращает целочисленное постоянных значенийное значение 10
. Далее следует проверка, который имеет правильный входной свойство и проверка, который имеет ошибочный входной свойство:
Файл: src/lib.rs
-fn prints_and_returns_10(a: i32) -> i32 {
- println!("I got the value {a}");
- 10
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn this_test_will_pass() {
- let value = prints_and_returns_10(4);
- assert_eq!(value, 10);
- }
-
- #[test]
- fn this_test_will_fail() {
- let value = prints_and_returns_10(8);
- assert_eq!(value, 5);
- }
-}
--
Итог вывода на окно вывода приказы cargo test
:
$ cargo test
- Compiling silly-function v0.1.0 (file:///projects/silly-function)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
- Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
-
-running 2 tests
-test tests::this_test_will_fail ... FAILED
-test tests::this_test_will_pass ... ok
-
-failures:
-
----- tests::this_test_will_fail stdout ----
-I got the value 8
-thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
-assertion `left == right` failed
- left: 10
- right: 5
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::this_test_will_fail
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Обратите внимание, что нигде в этом выводе мы не видим сообщения I got the value 4
, которое печатается при выполнении пройденного проверки. Этот вывод был записан. Итог неудачного проверки, I got the value 8
, появляется в разделе итоговых итогов проверки, который также показывает причину неудачного проверки.
Если мы хотим видеть напечатанные итоги прохождения проверок, мы можем сказать Rust, чтобы он также показывал итоги успешных проверок с помощью --show-output
.
$ cargo test -- --show-output
-
-Когда мы снова запускаем проверки из Приложения 11-10 с флагом --show-output
, мы видим следующий итог:
$ cargo test -- --show-output
- Compiling silly-function v0.1.0 (file:///projects/silly-function)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
-
-running 2 tests
-test tests::this_test_will_fail ... FAILED
-test tests::this_test_will_pass ... ok
-
-successes:
-
----- tests::this_test_will_pass stdout ----
-I got the value 4
-
-
-successes:
- tests::this_test_will_pass
-
-failures:
-
----- tests::this_test_will_fail stdout ----
-I got the value 8
-thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
-assertion `left == right` failed
- left: 5
- right: 10
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::this_test_will_fail
-
-test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Бывают случаи, когда в запуске всех проверок нет необходимости и нужно запустить только несколько проверок. Если вы работаете над функцией и хотите запустить проверки, которые исследуют её работу - это было бы удобно. Вы можете это сделать, используя приказ cargo test
, передав в качестве переменной имена проверок.
Для отображения, как запустить объединение проверок, мы создадим объединение проверок для функции add_two
function, как показано в Приложении 11-11, и постараемся выбрать какие из них запускать.
Файл: src/lib.rs
-pub fn add_two(a: usize) -> usize {
- a + 2
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn add_two_and_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
- }
-
- #[test]
- fn add_three_and_two() {
- let result = add_two(3);
- assert_eq!(result, 5);
- }
-
- #[test]
- fn one_hundred() {
- let result = add_two(100);
- assert_eq!(result, 102);
- }
-}
--
Если вы выполните приказ cargo test
без уточняющих переменных, все проверки выполнятся одновременно:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 3 tests
-test tests::add_three_and_two ... ok
-test tests::add_two_and_two ... ok
-test tests::one_hundred ... ok
-
-test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Мы можем запустить один проверка с помощью указания его имени в приказу cargo test
:
$ cargo test one_hundred
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::one_hundred ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
-
-
-Был запущен только проверка с названием one_hundred
; два других проверки не соответствовали этому названию. Итоги проверки с помощью вывода 2 filtered out
дают нам понять, что у нас было больше проверок, но они не были запущены.
Таким образом мы не можем указать имена нескольких проверок; будет использоваться только первое значение, указанное для cargo test
. Но есть способ запустить несколько проверок.
Мы можем указать часть имени проверки, и будет запущен любой проверка, имя которого соответствует этому значению. Например, поскольку имена двух наших проверок содержат add
, мы можем запустить эти два, запустив cargo test add
:
$ cargo test add
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 2 tests
-test tests::add_three_and_two ... ok
-test tests::add_two_and_two ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
-
-
-Этот приказ запускала все проверки с add
в имени и отфильтровывала проверка с именем one_hundred
. Также обратите внимание, что звено, в котором появляется проверка, становится частью имени проверки, поэтому мы можем запускать все проверки в звене, фильтруя имя звена.
Бывает, что некоторые проверки требуют продолжительного времени для своего исполнения, и вы хотите исключить их из исполнения при запуске cargo test
. Вместо перечисления в приказной строке всех проверок, которые вы хотите запускать, вы можете определять проверки, требующие много времени для прогона, свойством ignore
, чтобы исключить их, как показано здесь:
Файл: src/lib.rs
-pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-// ANCHOR: here
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-
- #[test]
- #[ignore]
- fn expensive_test() {
- // code that takes an hour to run
- }
-}
-// ANCHOR_END: here
-После #[test]
мы добавляем строку #[ignore]
в проверка, который хотим исключить. Теперь, когда мы запускаем наши проверки, it_works
запускается, а expensive_test
пренебрегается:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 2 tests
-test tests::expensive_test ... ignored
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Функция expensive_test
помечена как ignored
. Если вы хотите выполнить только пренебреженные проверки, вы можете воспользоваться приказом cargo test -- --ignored
:
$ cargo test -- --ignored
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test expensive_test ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Управляя тем, какие проверки запускать, вы можете быть уверены, что итоги вашего cargo test
будут быстрыми. Когда вы дойдёте до особенности, где имеет смысл проверить итоги проверок ignored
, и у вас есть время дождаться их итогов, вы можете запустить их с помощью cargo test -- --ignored
. Если вы хотите запустить все проверки независимо от того, пренебрегаются они или нет, выполните cargo test -- --include-ignored
.
Как упоминалось в начале главы, проверка является сложной пунктом и разные люди используют разную совокупность понятий и устройство. Сообщество Ржавчина думает о проверках с точки зрения двух основных разрядов: состоящие из звеньев проверки и встроенные проверки. Состоящие из звеньев проверки это небольшие и более сосредоточенные на проверке одного звена в отдельности или могут проверяться закрытые внешние оболочки. Встраиваемые проверки являются полностью внешними по отношению к вашей библиотеке и используют код библиотеки так же, как любой другой внешний код, используя только общедоступные внешние оболочки и возможно выполняя проверка нескольких звеньев в одном проверке.
-Написание обоих видов проверок важно для обеспечения того, чтобы кусочки вашей библиотеки по отдельности и вместе делали то, что вы ожидаете.
-Целью состоящих из звеньев проверок является проверка каждого раздела кода, изолированное от остального возможностей, чтобы можно было быстро понять, что работает неправильно или не так как ожидается. Мы разместим состоящие из звеньев проверки в папке src, в каждый проверяемый файл. Но в Ржавчина принято создавать проверяемый звено tests
и код проверки сохранять в файлы с таким же именем, как составляющие которые предстоит проверять. Также необходимо добавить изложение cfg(test)
к этому звену.
#[cfg(test)]
Изложение #[cfg(test)]
у звена с проверкими указывает Ржавчина собирать и запускать только код проверок, когда выполняется приказ cargo test
, а не когда запускается cargo build
. Это уменьшает время сборки, если вы только хотите собрать библиотеку и уменьшить место для результирующих собранных артефактов, потому что проверки не будут включены. Вы увидите что, по причине того, что встроенные проверки помещаются в другой папка им не нужна изложение #[cfg(test)]
. Тем не менее, так как состоящие из звеньев проверки идут в тех же файлах что и основной код, вы будете использовать #[cfg(test)]
чтобы указать, что они не должны быть включены в собранный итог.
Напомним, что когда мы порождали новый дело adder
в первом разделе этой главы, то Cargo создал для нас код ниже:
Файл: src/lib.rs
-pub fn add(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- let result = add(2, 2);
- assert_eq!(result, 4);
- }
-}
-Этот код является самостоятельно созданным проверочным звеном. Свойство cfg
предназначен для настройке и говорит Rust, что следующий элемент должен быть включён только учитывая определённую возможность настройке. В этом случае возможностью настройке является test
, который предоставлен в Ржавчина для сборки и запуска текущих проверок. Используя свойство cfg
, Cargo собирает только проверочный код при активном запуске проверок приказом cargo test
. Это включает в себя любые вспомогательные функции, которые могут быть в этом звене в дополнение к функциям помеченным #[test]
.
Сообщество программистов не имеет однозначного мнения по поводу проверять или нет закрытые функции. В некоторых языках весьма сложно или даже невозможно проверять такие функции. Независимо от того, какой технологии проверки вы придерживаетесь, в Ржавчина закрытые функции можно проверять. Рассмотрим приложение 11-12 с закрытой функцией internal_adder
.
Файл: src/lib.rs
-pub fn add_two(a: usize) -> usize {
- internal_adder(a, 2)
-}
-
-fn internal_adder(left: usize, right: usize) -> usize {
- left + right
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn internal() {
- let result = internal_adder(2, 2);
- assert_eq!(result, 4);
- }
-}
--
Обратите внимание, что функция internal_adder
не помечена как pub
. Проверки — это просто Ржавчина код, а звено tests
— это ещё один звено. Как мы обсуждали в разделе “Пути для ссылки на элемент в дереве звеньев“, элементы в дочерних звенах могут использовать элементы из своих родительских звеньев. В этом проверке мы помещаем все элементы родительского звена test
в область видимости с помощью use super::*
и затем проверка может вызывать internal_adder
. Если вы считаете, что закрытые функции не нужно проверять, то Ржавчина не заставит вас это сделать.
В Ржавчина встроенные проверки являются полностью внешними по отношению к вашей библиотеке. Они используют вашу библиотеку так же, как любой другой код, что означает, что они могут вызывать только функции, которые являются частью открытого API библиотеки. Их целью является проверка, много ли частей вашей библиотеки работают вместе правильно. У звеньев кода правильно работающих самостоятельно, могут возникнуть сбоев при встраивани, поэтому проверочное покрытие встроенного кода также важно. Для создания встроенных проверок сначала нужен папка tests .
-Мы создаём папку tests в корневой папке вашего дела, рядом с папкой src. Cargo знает, что искать файлы с встроенными проверкими нужно в этой папки. После этого мы можем создать столько проверочных файлов, сколько захотим, и Cargo собирает каждый из файлов в отдельный ящик.
-Давайте создадим встроенный проверку. Рядом с кодом из приложения 11-12, который всё ещё в файле src/lib.rs, создайте папка tests, создайте новый файл с именем tests/integration_test.rs. Устройства папок должна выглядеть так:
-adder
-├── Cargo.lock
-├── Cargo.toml
-├── src
-│ └── lib.rs
-└── tests
- └── integration_test.rs
-
-Введите код из приложения 11-13 в файл tests/integration_test.rs file:
-Файл: tests/integration_test.rs
-use adder::add_two;
-
-#[test]
-fn it_adds_two() {
- let result = add_two(2);
- assert_eq!(result, 4);
-}
--
Каждый файл в папке tests
представляет собой отдельный ящик, поэтому нам нужно подключить нашу библиотеку в область видимости каждого проверочного ящика. По этой причине мы добавляем use adder
в верхней части кода, что не нужно нам делать в состоящих из звеньев проверках.
Нам не нужно вносить примечания в код в tests/integration_test.rs с помощью #[cfg(test)]
. Cargo особым образом обрабатывает папка tests
и собирает файлы в этом папке только тогда, когда мы запускаем приказ cargo test
. Запустите cargo test
сейчас:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
- Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
-
-running 1 test
-test tests::internal ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Выходные данные представлены тремя разделами: состоящие из звеньев проверки, встроенные проверки и проверки документации. Обратите внимание, что если какой-нибудь проверка в одной из разделов не пройдёт, последующие разделы выполняться не будут. Например, если состоящий из звеньев проверка провалился, не будет выведено итогов встроенных и документационных проверок, потому что эти проверки будут выполняться только в том случае, если все состоящие из звеньев проверки завершатся успешно.
-Первый раздел для состоящих из звеньев проверок такой же, как мы видели: одна строка для каждого состоящего из звеньев проверки (один с именем internal
, который мы добавили в приложении 11-12), а затем сводная строка для состоящих из звеньев проверок.
Раздел встроенных проверок начинается со строки Running tests/integration_test.rs
. Далее идёт строка для каждой проверочной функции в этом встроенном проверке и итоговая строка для итогов встроенного проверки непосредственно перед началом раздела Doc-tests adder
.
Каждый файл встроенного проверки имеет свой собственный раздел, поэтому, если мы добавим больше файлов в папка tests, то здесь будет больше разделов встроенного проверки.
-Мы всё ещё можем запустить определённую функцию в встроенных проверках, указав имя проверка функции в качестве переменной в cargo test
. Чтобы запустить все проверки в определенном файле встроенных проверок, используйте переменная --test
сопровождаемый именем файла у приказы cargo test
:
$ cargo test --test integration_test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
- Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Этот приказ запускает только проверки в файле tests/integration_test.rs.
-По мере добавления большего количества встроенных проверок, можно создать более одного файла в папке tests, чтобы легче создавать их; например, вы можете собъединять функции проверки по возможности, которую они проверяют. Как упоминалось ранее, каждый файл в папке tests собран как отдельный ящик, что полезно для создания отдельных областей видимости, чтобы более точно создавать видимость то, как конечные пользователи будут использовать ваш ящик. Однако это означает, что файлы в папке tests ведут себя не так, как файлы в src, как вы узнали в Главе 7 относительно того как разделить код на звенья и файлы.
-Различное поведение файлов в папке tests наиболее заметно, когда у вас есть набор вспомогательных функций, которые будут полезны в нескольких встроенных проверочных файлах. Представим, что вы пытаетесь выполнить действия, описанные в разделе «Разделение звеньев в разные файлы» главы 7, чтобы извлечь их в общий звено. Например, вы создали файл tests/common.rs и помеисполнения в него функцию setup
, содержащую некоторый код, который вы будете вызывать из разных проверочных функций в нескольких проверочных файлах
Файл: tests/common.rs
-pub fn setup() {
- // setup code specific to your library's tests would go here
-}
-Когда мы снова запустим проверки, мы увидим новый раздел в итогах проверок для файла common.rs, хотя этот файл не содержит никаких проверочных функций, более того, мы даже не вызывали функцию setup
откуда либо:
$ cargo test
- Compiling adder v0.1.0 (file:///projects/adder)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
- Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
-
-running 1 test
-test tests::internal ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
-
-running 1 test
-test it_adds_two ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests adder
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Упоминание файла common
и появление в итогах выполнения проверок сообщения вида running 0 tests
- это не то, чего мы хотели. Мы только хотели выделить некоторый общий код, который будет использоваться другими файлами встроенных проверок.
Чтобы звено common
больше не появлялся в итогах выполнения проверок, вместо файла tests/common.rs мы создадим файл tests/common/mod.rs. Директория дела теперь выглядит следующим образом:
├── Cargo.lock
-├── Cargo.toml
-├── src
-│ └── lib.rs
-└── tests
- ├── common
- │ └── mod.rs
- └── integration_test.rs
-
-Здесь используется более раннее соглашение об именовании файлов, которое Ржавчина также понимает. Мы говорили об этом в разделе “Иные пути к файлам” главы 7. Именование файла таким образом говорит, что Ржавчина не должен рассматривать звено common
как файл встроенных проверок. Когда мы перемещаем код функции setup
в файл tests/common/mod.rs и удаляем файл tests/common.rs, дополнительный раздел больше не будет отображаться в итогах проверок. Файлы в подпапких папки tests не собираются как отдельные ящики или не появляются в итогах выполнения проверок.
После того, как мы создали файл tests/common/mod.rs, мы можем использовать его в любых файлах встроенных проверок как обычный звено. Вот пример вызова функции setup
из проверки it_adds_two
в файле tests/integration_test.rs:
Файл: tests/integration_test.rs
-use adder::add_two;
-
-mod common;
-
-#[test]
-fn it_adds_two() {
- common::setup();
-
- let result = add_two(2);
- assert_eq!(result, 4);
-}
-Обратите внимание, что объявление mod common;
совпадает с объявлением звена, которое отображено в приложении 7-21. Затем в проверочной функции мы можем вызвать функцию common::setup()
.
Если наш дело является двоичным ящиком, который содержит только src/main.rs и не содержит src/lib.rs, мы не сможем создать встроенные проверки в папке tests и подключить функции определённые в файле src/main.rs в область видимости с помощью указания use
. Только библиотечные ящики могут предоставлять функции, которые можно использовать в других ящиках; двоичные ящики предназначены только для самостоятельного запуска.
Это одна из причин, почему дела на Rust, которые порождают исполняемые звенья, обычно имеют простой файл src/main.rs, который в свою очередь вызывает логику, которая находится в файле src/lib.rs. Используя такую устройство, встроенные проверки могут проверить библиотечный ящик, используя оператор use
для подключения важного возможностей. Если этот важный возможности работает, то и небольшое количество кода в файле src/main.rs также будет работать, а значит этот небольшой объём кода не нуждается в проверке.
Средства проверки языка Ржавчина предоставляют способ задать ожидаемое поведение кода, чтобы убедиться, что он всё ещё соответствует вашим ожиданиям даже после внесения изменений. Состоящие из звеньев проверки проверяют различные части библиотеки по отдельности и могут проверять закрытые подробности выполнения. Встраиваемые проверки проверяют, что части библиотеки работают правильно сообща. Эти проверки используют для проверки кода открытый API библиотеки, таким же образом, как его будет использовать внешний код. Хотя система видов Ржавчина и правила владения помогают предотвратить некоторые виды ошибок, проверки по-прежнему важны для уменьшения количества логических ошибок, связанных с поведением вашего кода.
-Давайте объединим знания, полученные в этой и предыдущей главах, чтобы поработать над делом!
- -В этой главе вы примените многие знания, полученные ранее, а также познакомитесь с ещё неизученными API встроенной библиотеки. Мы создадим окно выводаное приложение, которое будет взаимодействовать с файлом и с окно выводаным вводом / выводом, чтобы применить в некоторых подходах Rust, с которыми вы уже знакомы.
-Скорость, безопасность, сборка в один исполняемый файл и кроссплатформенность делают Ржавчина наилучшим языком для создания окно выводаных средств, так что в нашем деле мы создадим свою собственную исполнение обычной утилиты поиска grep
, что расшифровывается, как "вездесущеее средство поиска и печати" (globally search a regular expression and print). В простейшем случае grep
используется для поиска в выбранном файле указанного текста. Для этого утилита grep
получает имя файла и текст в качестве переменных. Далее она читает файл, находит и выводит строки, содержащие искомый текст.
Попутно мы покажем, как сделать так, чтобы наше окно выводаное приложение использовало возможности окна вызова, которые используются многими другими окно выводаными средствами. Мы будем читать значение переменной окружения, чтобы позволить пользователю настроить поведение нашего средства. Мы также будем печатать сообщения об ошибках в обычный окно выводаный поток ошибок ( stderr
) вместо принятого вывода ( stdout
), чтобы, к примеру, пользователь мог перенаправить успешный вывод в файл, в то время, как сообщения об ошибках останутся на экране.
Один из участников Rust-сообщества, Andrew Gallant, уже выполнил полновозможный, очень быстрый подобие программы grep
и назвал его ripgrep
. По сравнению с ним, наша исполнение будет довольно простой, но эта глава даст вам знания, которые нужны для понимания существующих дел, таких как ripgrep
.
Наш дело grep
будет использовать ранее изученные подходы:
Мы также кратко представим замыкания, повторители и предметы особенности, которые будут объяснены подробно в главах 13 и 17.
- -Создадим новый дело с окном вывода приложения как обычно с помощью приказы cargo new
. Мы назовём дело minigrep
, чтобы различать наше приложение от grep
, которое возможно уже есть в вашей системе.
$ cargo new minigrep
- Created binary (application) `minigrep` project
-$ cd minigrep
-
-Первая задача - заставить minigrep
принимать два переменной приказной строки: путь к файлу и строку для поиска. То есть мы хотим иметь возможность запускать нашу программу через cargo run
, с использованием двойного дефиса, чтобы указать, что следующие переменные предназначены для нашей программы, а не для cargo
, строки для поиска и пути к файлу в котором нужно искать, как описано ниже:
$ cargo run -- searchstring example-filename.txt
-
-В данный мгновение программа созданная cargo new
не может обрабатывать переменные, которые мы ей передаём. Некоторые существующие библиотеки на crates.io могут помочь с написанием программы, которая принимает переменные приказной строки, но так как вы просто изучаете эту подход, давайте выполняем эту возможность сами.
Чтобы minigrep
мог воспринимать значения переменных приказной строки, которые мы ему передаём, нам понадобится функция std::env::args
, входящая в обычную библиотеку Rust. Эта функция возвращает повторитель переменных приказной строки, переданных в minigrep
. Мы подробно рассмотрим повторители в главе 13. Пока вам достаточно знать две вещи об повторителях: повторители порождают серию значений, и мы можем вызвать способ collect
у повторителя, чтобы создать из него собрание, например вектор, который будет содержать все элементы, произведённые повторителем.
Код представленный в Приложении 12-1 позволяет вашей программе minigrep
читать любые переданные ей переменные приказной строки, а затем собирать значения в вектор.
Файл: src/main.rs
--use std::env; - -fn main() { - let args: Vec<String> = env::args().collect(); - dbg!(args); -}
-
Сначала мы вводим звено std::env
в область видимости с помощью указания use
, чтобы мы могли использовать его функцию args
. Обратите внимание, что функция std::env::args
вложена в два уровня звеньев. Как мы обсуждали в главе 7, в случаях, когда нужная функция оказывается вложенной в более чем один звено, советуется выносить в область видимости родительский звено, а не функцию. Таким образом, мы можем легко использовать другие функции из std::env
. Это менее двусмысленно, чем добавление use std::env::args
и последующий вызов функции только с args
, потому что args
может быть легко принят за функцию, определённую в текущем звене.
--Функция
-args
и недействительный Юникод символ (Unicode)Обратите внимание, что
-std::env::args
вызовет панику, если какой-либо переменная содержит недопустимый символ Юникода. Если вашей программе необходимо принимать переменные, содержащие недопустимые символы Unicode, используйте вместо этогоstd::env::args_os
. Эта функция возвращает повторитель , который выдаёт значенияOsString
вместо значенийString
. Мы решили использоватьstd::env::args
здесь для простоты, потому что значенияOsString
отличаются для каждой площадки и с ними сложнее работать, чем со значениямиString
.
В первой строке кода функции main
мы вызываем env::args
и сразу используем способ collect
, чтобы превратить повторитель в вектор содержащий все полученные значения. Мы можем использовать функцию collect
для создания многих видов собраний, поэтому мы явно определяем вид args
чтобы указать, что мы хотим вектор строк. Хотя нам очень редко нужно определять виды в Rust, collect
- это одна из функций, с которой вам часто нужна изложение вида, потому что Ржавчина не может сам вывести какую собрание вы хотите.
И в заключение мы печатаем вектор с помощью отладочного макроса. Попробуем запустить код сначала без переменных, а затем с двумя переменнойми:
-$ cargo run
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
- Running `target/debug/minigrep`
-[src/main.rs:5:5] args = [
- "target/debug/minigrep",
-]
-
-$ cargo run -- needle haystack
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
- Running `target/debug/minigrep needle haystack`
-[src/main.rs:5:5] args = [
- "target/debug/minigrep",
- "needle",
- "haystack",
-]
-
-Обратите внимание, что первое значение в векторе "target/debug/minigrep"
является названием нашего двоичного файла. Это соответствует поведению списка переменных в Си, позволяя программам использовать название с которым они были вызваны при выполнении. Часто бывает удобно иметь доступ к имени программы, если вы хотите распечатать его в сообщениях или изменить поведение программы в зависимости от того, какой псевдоним приказной строки был использован для вызова программы. Но для целей этой главы, мы пренебрегаем его и сохраним только два переменной, которые нам нужны.
На текущий мгновение программа может получить доступ к значениям, указанным в качестве переменных приказной строки. Теперь нам требуется сохранять значения этих двух переменных в переменных, чтобы мы могли использовать их в остальных частях программы. Мы сделаем это в приложении 12-2.
-Файл: src/main.rs
-use std::env;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let query = &args[1];
- let file_path = &args[2];
-
- println!("Searching for {query}");
- println!("In file {file_path}");
-}
--
Как видно из распечатки вектора, имя программы занимает первое значение в векторе по адресу args[0]
, значит, переменные начинаются с порядкового указателя 1
. Первый переменная minigrep
- это строка, которую мы ищем, поэтому мы помещаем ссылку на первый переменная в переменную query
. Вторым переменнаяом является путь к файлу, поэтому мы помещаем ссылку на второй переменная в переменную file_path
.
Для проверки соблюдения правил работы нашей программы, значения переменных выводятся в окно вывода. Далее, запустим нашу программу со следующими переменнойми: test
и sample.txt
:
$ cargo run -- test sample.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep test sample.txt`
-Searching for test
-In file sample.txt
-
-Отлично, программа работает! Нам нужно чтобы значения переменных были сохранены в правильных переменных. Позже мы добавим обработку ошибок с некоторыми вероятными ошибочными случаейми, например, когда пользователь не предоставляет переменные; сейчас мы пренебрегаем эту случай и поработаем над добавлением возможности чтения файла.
- -Теперь добавим возможность чтения файла, указанного как переменная приказной строки file_path
. Во-первых, нам нужен пример файла для проверки: мы будем использовать файл с небольшим объёмом текста в несколько строк с несколькими повторяющимися словами. В приложении 12-3 представлено стихотворение Эмили Дикинсон, которое будет хорошо работать! Создайте файл с именем poem.txt в корне вашего дела и введите стихотворение "I’m nobody! Who are you?"
Файл: poem.txt
-I'm nobody! Who are you?
-Are you nobody, too?
-Then there's a pair of us - don't tell!
-They'd banish us, you know.
-
-How dreary to be somebody!
-How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
-
--
Текст на месте, изменените src/main.rs и добавьте код для чтения файла, как показано в приложении 12-4.
-Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- // --snip--
- let args: Vec<String> = env::args().collect();
-
- let query = &args[1];
- let file_path = &args[2];
-
- println!("Searching for {query}");
- println!("In file {file_path}");
-
- let contents = fs::read_to_string(file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
--
Во-первых, мы добавляем ещё одну указанию use
чтобы подключить соответствующую часть встроенной библиотеки: нам нужен std::fs
для обработки файлов.
В main
мы добавили новую указанию: функция fs::read_to_string
принимает file_path
, открывает этот файл и возвращает содержимое файла как std::io::Result<String>
.
После этого, мы снова добавили временную указанию println!
для печати значения contents
после чтения файла, таким образом мы можем проверить, что программа отрабатывает до этого места.
Давайте запустим этот код с любой строкой в качестве первого переменной приказной строки (потому что мы ещё не выполнили поисковую часть) и файл poem.txt как второй переменная:
-$ cargo run -- the poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep the poem.txt`
-Searching for the
-In file poem.txt
-With text:
-I'm nobody! Who are you?
-Are you nobody, too?
-Then there's a pair of us - don't tell!
-They'd banish us, you know.
-
-How dreary to be somebody!
-How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
-
-
-Отлично! Этот код прочитал и затем напечатал содержимое файла. Но у программы есть несколько недостатков. Прежде всего, функция main
решает слишком много задач: как правило функция понятнее и проще в обслуживании если она воплощает только одну мысль. Другая неполадка заключается в том, что мы не обрабатываем ошибки так хорошо, как могли бы. Пока наша программа небольшая, то эти недостатки не являются большой неполадкой, но по мере роста программы эти недостатки будет всё труднее исправлять. Хорошей опытом является начинать переработка кода на ранней стадии разработки программы, потому что гораздо проще перерабатывать код меньшие объёмы кода. Мы сделаем это далее.
Для улучшения программы мы исправим 4 имеющихся сбоев, связанных со устройством программы и тем как обрабатываются вероятные ошибки. Во-первых, функция main
на данный мгновение решает две задачи: анализирует переменные приказной строки и читает файлы. По мере роста программы количество отдельных задач, которые обрабатывает функция main
, будет увеличиваться. Поскольку эта функция получает больше обязанностей, то становится все труднее понимать её, труднее проверять и труднее изменять, не сломав одну из её частей. Лучше всего разделить возможность, чтобы каждая функция отвечала за одну задачу.
Эта неполадка также связана со второй неполадкой: хотя переменные query
и file_path
являются переменными настройке нашей программы, переменные вида contents
используются для выполнения логики программы. Чем длиннее становится main
, тем больше переменных нам нужно будет добавить в область видимости; чем больше у нас переменных в области видимости, тем сложнее будет отслеживать назначение каждой переменной. Лучше всего собъединять переменные настройке в одну устройство, чтобы сделать их назначение понятным.
Третья неполадка заключается в том, что мы используем expect
для вывода сведений об ошибке при неполадке с чтением файла, но сообщение об ошибке просто выведет текстShould have been able to read the file
. Чтение файла может не сработать по разным причинам, например: файл не найден или у нас может не быть разрешения на его чтение. Сейчас же, независимо от случаи, мы напечатаем одно и то же сообщение об ошибке, что не даст пользователю никакой сведений!
В-четвёртых, мы используем expect
неоднократно для обработки различных ошибок и если пользователь запускает нашу программу без указания достаточного количества переменных он получит ошибку index out of bounds
из Rust, что не совсем понятно описывает неполадку. Было бы лучше, если бы весь код обработки ошибок находился в одном месте, чтобы тем, кто будет поддерживать наш код в дальнейшем, нужно было бы вносить изменения только здесь, если потребуется изменить логику обработки ошибок. Наличие всего кода обработки ошибок в одном месте заверяет, что мы напечатаем сообщения, которые будут иметь смысл для наших конечных пользователей.
Давайте решим эти четыре сбоев путём переработки кода нашего дела.
-Внутренняя неполадка распределения ответственности за выполнение нескольких задач функции main
является общей для многих двоичных дел. В итоге Ржавчина сообщество разработало этап для использования в качестве руководства по разделению ответственности двоичной программы, когда код в main
начинает увеличиваться. Этап имеет следующие шаги:
Полезные обязанности, которые остаются в функции main
после этого этапа должно быть ограничено следующим:
run
в lib.rsrun
возвращает ошибкуЭтот образец о разделении ответственности: main.rs занимается запуском программы, а lib.rs обрабатывает всю логику задачи. Поскольку нельзя проверить функцию main
напрямую, то такая устройства позволяет проверить всю логику программы путём перемещения её в функции внутри lib.rs. Единственный код, который остаётся в main.rs будет достаточно маленьким, чтобы проверить его соблюдение правил прочитав код. Давайте переработаем нашу программу, следуя этому этапу.
Мы извлечём возможность для разбора переменных в функцию, которую вызовет main
для подготовки к перемещению логики разбора приказной строки в файл src/lib.rs. Приложение 12-5 показывает новый запуск main
, который вызывает новую функцию parse_config
, которую мы определим сначала в src/main.rs.
Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let (query, file_path) = parse_config(&args);
-
- // --snip--
-
- println!("Searching for {query}");
- println!("In file {file_path}");
-
- let contents = fs::read_to_string(file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-
-fn parse_config(args: &[String]) -> (&str, &str) {
- let query = &args[1];
- let file_path = &args[2];
-
- (query, file_path)
-}
--
Мы все ещё собираем переменные приказной строки в вектор, но вместо присваивания значение переменной с порядковым указателем 1 переменной query
и значение переменной с порядковым указателем 2 переменной с именем file_path
в функции main
, мы передаём весь вектор в функцию parse_config
. Функция parse_config
затем содержит логику, которая определяет, какой переменная идёт в какую переменную и передаёт значения обратно в main
. Мы все ещё создаём переменные query
и file_path
в main
, но main
больше не несёт ответственности за определение соответствия переменной приказной строки и соответствующей переменной.
Эта доработка может показаться излишней для нашей маленькой программы, но мы проводим переработка кода небольшими, постепенными шагами. После внесения этого изменения снова запустите программу и убедитесь, что анализ переменных все ещё работает. Также хорошо часто проверять все этапы, чтобы помочь определить причину неполадок. когда они возникают.
-Мы можем сделать ещё один маленький шаг для улучшения функции parse_config
. На данный мгновение мы возвращаем упорядоченный ряд, но затем мы немедленно разделяем его снова на отдельные части. Это признак того, что, возможно, пока у нас нет правильной абстракции.
Ещё один индикатор, который показывает, что есть место для улучшения, это часть config
из parse_config
, что подразумевает, что два значения, которые мы возвращаем, связаны друг с другом и оба являются частью одного настроечного значения. В настоящее время мы не отражаем этого смысла в устройстве данных, кроме объединения двух значений в упорядоченный ряд; мы могли бы поместить оба значения в одну устройство и дать каждому из полей устройства понятное имя. Это облегчит будущую поддержку этого кода, чтобы понять, как различные значения относятся друг к другу и какое их назначение.
В приложении 12-6 показаны улучшения функции parse_config
.
Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = parse_config(&args);
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- // --snip--
-
- println!("With text:\n{contents}");
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-fn parse_config(args: &[String]) -> Config {
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Config { query, file_path }
-}
--
Мы добавили устройство с именем Config
объявленную с полями назваными как query
и file_path
. Ярлык parse_config
теперь указывает, что она возвращает значение Config
. В теле parse_config
, где мы возвращали срезы строк, которые ссылаются на значения String
в args
, теперь мы определяем Config
как содержащие собственные String
значения. Переменная args
в main
является владельцем значений переменной и позволяют функции parse_config
только одалживать их, что означает, что мы бы нарушили правила заимствования Rust, если бы Config
попытался бы взять во владение значения в args
.
Мы можем управлять данными String
разным количеством способов, но самый простой, хотя и отчасти неэффективный это вызвать способ clone
у значений. Он сделает полную повтор данных для образца Config
для владения, что занимает больше времени и памяти, чем сохранение ссылки на строку данных. Однако клонирование данных также делает наш код очень простым, потому что нам не нужно управлять временем жизни ссылок; в этом обстоятельстве, отказ от небольшой производительности, чтобы получить простоту, стоит небольших соглашениеа.
---
К при использовании способа Существует тенденция в среде программистов Ржавчина избегать использованияclone
clone
, т.к. это понижает эффективность работы кода. В Главе 13, вы изучите более эффективные способы, которые могут подойти в подобной случаи. Но сейчас можно воспроизводить несколько строк, чтобы продолжить работу, потому что вы сделаете эти повторы только один раз, а ваше имя файла и строка запроса будут очень маленькими. Лучше иметь работающую программу, которая немного неэффективна, чем пытаться заранее перерабатывать код при первом написании. По мере приобретения опыта работы с Ржавчина вам будет проще начать с наиболее эффективного решения, но сейчас вполне приемлемо вызватьclone
.
Мы обновили код в main
поэтому он помещает образец Config
возвращённый из parse_config
в переменную с именем config
, и мы обновили код, в котором ранее использовались отдельные переменные query
и file_path
, так что теперь он использует вместо этого поля в устройстве Config
.
Теперь наш код более чётко передаёт то, что query
и file_path
связаны и что цель из использования состоит в том, чтобы настроить, как программа будет работать. Любой код, который использует эти значения знает, что может найти их в именованных полях образца config
по их назначению.
Config
Пока что мы извлекли логику, отвечающую за синтаксический анализ переменных приказной строки из main
и помеисполнения его в функцию parse_config
. Это помогло нам увидеть, что значения query
и file_path
были связаны и что их отношения должны быть отражены в нашем коде. Затем мы добавили устройство Config
в качестве названия связанных общей целью query
и file_path
и чтобы иметь возможность вернуть именованные значения как имена полей устройства из функции parse_config
.
Итак, теперь целью функции parse_config
является создание образца Config
, мы можем изменить parse_config
из простой функции на функцию названную new
, которая связана со устройством Config
. Выполняя это изменение мы сделаем код более идиоматичным. Можно создавать образцы видов в встроенной библиотеке, такие как String
с помощью вызова String::new
. Точно так же изменив название parse_config
на название функции new
, связанную с Config
, мы будем уметь создавать образцы Config
, вызывая Config::new
. Приложение 12-7 показывает изменения, которые мы должны сделать.
Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::new(&args);
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-
- // --snip--
-}
-
-// --snip--
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn new(args: &[String]) -> Config {
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Config { query, file_path }
- }
-}
--
Мы обновили main
где вызывали parse_config
, чтобы вместо этого вызывалась Config::new
. Мы изменили имя parse_config
на new
и перенесли его внутрь раздела impl
, который связывает функцию new
с Config
. Попробуйте снова собрать код, чтобы убедиться, что он работает.
Теперь мы поработаем над исправлением обработки ошибок. Напомним, что попытки получить доступ к значениям в векторе args
с порядковым указателем 1 или порядковым указателем 2 приведут к панике, если вектор содержит менее трёх элементов. Попробуйте запустить программу без каких-либо переменных; это будет выглядеть так:
$ cargo run
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep`
-thread 'main' panicked at src/main.rs:27:21:
-index out of bounds: the len is 1 but the index is 1
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Строка index out of bounds: the len is 1 but the index is 1
является сообщением об ошибке предназначенной для программистов. Она не поможет нашим конечным пользователям понять, что случилось и что они должны сделать вместо этого. Давайте исправим это сейчас.
В приложении 12-8 мы добавляем проверку в функцию new
, которая будет проверять, что срез достаточно длинный, перед попыткой доступа по порядковым указателям 1 и 2. Если срез не достаточно длинный, программа паникует и отображает улучшенное сообщение об ошибке.
Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::new(&args);
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- // --snip--
- fn new(args: &[String]) -> Config {
- if args.len() < 3 {
- panic!("not enough arguments");
- }
- // --snip--
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Config { query, file_path }
- }
-}
--
Этот код похож на функцию Guess::new
написанную в приложении 9-13, где мы вызывали panic!
, когда value
переменной вышло за пределы допустимых значений. Здесь вместо проверки на рядзначений, мы проверяем, что длина args
не менее 3 и остальная часть функции может работать при условии, что это условие было выполнено. Если в args
меньше трёх элементов, это условие будет истинным и мы вызываем макрос panic!
для немедленного завершения программы.
Имея нескольких лишних строк кода в new
, давайте запустим программу снова без переменных, чтобы увидеть, как выглядит ошибка:
$ cargo run
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep`
-thread 'main' panicked at src/main.rs:26:13:
-not enough arguments
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-Этот вывод лучше: у нас теперь есть разумное сообщение об ошибке. Тем не менее, мы также имеем постороннюю сведения, которую мы не хотим предоставлять нашим пользователям. Возможно, использованная техника, которую мы использовали в приложении 9-13, не является лучшей для использования: вызов panic!
больше подходит для программирования сбоев, чем решения сбоев, как обсуждалось в главе 9. Вместо этого мы можем использовать другую технику, о которой вы узнали в главе 9 [возвращая Result
], которая указывает либо на успех, либо на ошибку.
Result
вместо вызова panic!
Мы можем вернуть значение Result
, которое будет содержать образец Config
в успешном случае и опишет неполадку в случае ошибки. Мы так же изменим функцию new
на build
потому что многие программисты ожидают что new
никогда не завершится неудачей. Когда Config::build
взаимодействует с main
, мы можем использовать вид Result
как сигнал возникновения сбоев. Затем мы можем изменить main
, чтобы преобразовать исход Err
в более применимую ошибку для наших пользователей без окружающего текста вроде thread 'main'
и RUST_BACKTRACE
, что происходит при вызове panic!
.
Приложение 12-9 показывает изменения, которые нужно внести в возвращаемое значения функции Config::build
, и в тело функции, необходимые для возврата вида Result
. Заметьте, что этот код не собирается, пока мы не обновим main
, что мы и сделаем в следующем приложении.
Файл: src/main.rs
-use std::env;
-use std::fs;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::new(&args);
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
--
Наша функция build
теперь возвращает Result
с образцом Config
в случае успеха и &'static str
в случае ошибки. Значения ошибок всегда будут строковыми записями, которые имеют время жизни 'static
.
Мы внесли два изменения в тело функции build
: вместо вызова panic!
, когда пользователь не передаёт достаточно переменных, мы теперь возвращаем Err
значение и мы завернули возвращаемое значение Config
в Ok
. Эти изменения заставят функцию соответствовать своей новой ярлыке вида.
Возвращение значения Err
из Config::build
позволяет функции main
обработать значение Result
возвращённое из функции build
и выйти из этапа более чисто в случае ошибки.
Config::build
и обработка ошибокЧтобы обработать ошибку и вывести более дружественное сообщение об ошибке, нам нужно обновить код main
для обработки Result
, возвращаемого из Config::build
как показано в приложении 12-10. Мы также возьмём на себя ответственность за выход из программы приказной строки с ненулевым кодом ошибки panic!
и выполняем это вручную. Не нулевой значение выхода - это соглашение, которое указывает этапу, который вызывает нашу программу, что программа завершилась с ошибкой.
Файл: src/main.rs
-use std::env;
-use std::fs;
-use std::process;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- // --snip--
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
--
В этом приложении мы использовали способ, который мы ещё не рассматривали подробно: unwrap_or_else
, который в встроенной библиотеке определён как Result<T, E>
. Использование unwrap_or_else
позволяет нам определить некоторые пользовательские ошибки обработки, не содержащие panic!
. Если Result
является значением Ok
, поведение этого способа подобно unwrap
: возвращает внутреннее значение из обёртки Ok
. Однако, если значение является значением Err
, то этот способ вызывает код замыкания, которое является анонимной функцией, определённой заранее и передаваемую в качестве переменной в unwrap_or_else
. Мы рассмотрим замыкания более подробно в главе 13. В данный мгновение, вам просто нужно знать, что unwrap_or_else
передаст внутреннее значение Err
, которое в этом случае является постоянной строкой not enough arguments
, которое мы добавили в приложении 12-9, в наше замыкание как переменная err
указанное между вертикальными линиями. Код в замыкании может затем использовать значение err
при выполнении.
Мы добавили новую строку use
, чтобы подключить process
из встроенной библиотеки в область видимости. Код в замыкании, который будет запущен в случае ошибки содержит только две строчки: мы печатаем значение err
и затем вызываем process::exit
. Функция process::exit
немедленно остановит программу и вернёт номер, который был передан в качестве кода состояния выхода. Это похоже на обработку с помощью макроса panic!
, которую мы использовали в приложении 12-8, но мы больше не получаем весь дополнительный вывод. Давай попробуем:
$ cargo run
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/minigrep`
-Problem parsing arguments: not enough arguments
-
-Замечательно! Этот вывод намного дружелюбнее для наших пользователей.
-main
Теперь, когда мы закончили переработка кода разбора настройке, давайте обратимся к логике программы. Как мы указали в разделе «Разделение ответственности в двоичных делах», мы извлечём функцию с именем run
, которая будет содержать всю логику, присутствующую в настоящее время в функции main
и которая не связана с настройкой настройке или обработкой ошибок. Когда мы закончим, то main
будет краткой, легко проверяемой и мы сможем написать проверки для всей остальной логики.
Код 12-11 отображает извлечённую логику в функцию run
. Мы делаем маленькое, инкрементальное приближение к извлечению функции. Код всё ещё сосредоточен в файле src/main.rs:
Файл: src/main.rs
-use std::env;
-use std::fs;
-use std::process;
-
-fn main() {
- // --snip--
-
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- run(config);
-}
-
-fn run(config: Config) {
- let contents = fs::read_to_string(config.file_path)
- .expect("Should have been able to read the file");
-
- println!("With text:\n{contents}");
-}
-
-// --snip--
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
--
Функция run
теперь содержит всю оставшуюся логику из main
, начиная от чтения файла. Функция run
принимает образец Config
как переменная.
run
Оставшаяся логика программы выделена в функцию run
, где мы можем улучшить обработку ошибок как мы уже делали с Config::build
в приложении 12-9. Вместо того, чтобы позволить программе паниковать с помощью вызова expect
, функция run
вернёт Result<T, E>
, если что-то пойдёт не так. Это позволит далее окне выводадировать логику обработки ошибок в main
удобным способом. Приложение 12-12 показывает изменения, которые мы должны внести в ярлык и тело run
.
Файл: src/main.rs
-use std::env;
-use std::fs;
-use std::process;
-use std::error::Error;
-
-// --snip--
-
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- run(config);
-}
-
-fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- println!("With text:\n{contents}");
-
- Ok(())
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
--
Здесь мы сделали три значительных изменения. Во-первых, мы изменили вид возвращаемого значения функции run
на Result<(), Box<dyn Error>>
. Эта функция ранее возвращала вид ()
и мы сохраняли его как значение, возвращаемое в случае Ok
.
Для вида ошибки мы использовали предмет особенность Box<dyn Error>
(и вверху мы подключили вид std::error::Error
в область видимости с помощью указания use
). Мы рассмотрим особенности предметов в главе 17. Сейчас просто знайте, что Box<dyn Error>
означает, что функция будет возвращать вид выполняющий особенность Error
, но не нужно указывать, какой именно будет вид возвращаемого значения. Это даёт возможность возвращать значения ошибок, которые могут быть разных видов в разных случаях. Ключевое слово dyn
сокращение для слова «изменяемый».
Во-вторых, мы убрали вызов expect
в пользу использования оператора ?
, как мы обсудили в главе 9. Скорее, чем вызывать panic!
в случае ошибки, оператор ?
вернёт значение ошибки из текущей функции для вызывающего, чтобы он её обработал.
В-третьих, функция run
теперь возвращает значение Ok
в случае успеха. В ярлыке функции run
успешный вид объявлен как ()
, который означает, что нам нужно обернуть значение единичного вида в значение Ok
. Данный правила написания Ok(())
поначалу может показаться немного странным, но использование ()
выглядит как идиоматический способ указать, что мы вызываем run
для его побочных эффектов; он не возвращает значение, которое нам нужно.
Когда вы запустите этот код, он собирается, но отобразит предупреждение:
-$ cargo run -- the poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
-warning: unused `Result` that must be used
- --> src/main.rs:19:5
- |
-19 | run(config);
- | ^^^^^^^^^^^
- |
- = note: this `Result` may be an `Err` variant, which should be handled
- = note: `#[warn(unused_must_use)]` on by default
-help: use `let _ = ...` to ignore the resulting value
- |
-19 | let _ = run(config);
- | +++++++
-
-warning: `minigrep` (bin "minigrep") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
- Running `target/debug/minigrep the poem.txt`
-Searching for the
-In file poem.txt
-With text:
-I'm nobody! Who are you?
-Are you nobody, too?
-Then there's a pair of us - don't tell!
-They'd banish us, you know.
-
-How dreary to be somebody!
-How public, like a frog
-To tell your name the livelong day
-To an admiring bog!
-
-
-Rust говорит, что наш код пренебрег Result
значение и значение Result
может указывать на то, что произошла ошибка. Но мы не проверяем, была ли ошибка и сборщик напоминает нам, что мы, вероятно, хотели здесь выполнить некоторый код обработки ошибок! Давайте исправим эту неполадку сейчас.
run
в main
Мы будем проверять и обрабатывать ошибки используя способику, подобную той, которую мы использовали для Config::build
в приложении 12-10, но с небольшой разницей:
Файл: src/main.rs
-use std::env;
-use std::error::Error;
-use std::fs;
-use std::process;
-
-fn main() {
- // --snip--
-
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- if let Err(e) = run(config) {
- println!("Application error: {e}");
- process::exit(1);
- }
-}
-
-fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- println!("With text:\n{contents}");
-
- Ok(())
-}
-
-struct Config {
- query: String,
- file_path: String,
-}
-
-impl Config {
- fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-Мы используем if let
вместо unwrap_or_else
чтобы проверить, возвращает ли run
значение Err
и вызывается process::exit(1)
, если это так. Функция run
не возвращает значение, которое мы хотим развернуть способом unwrap
, таким же образом как Config::build
возвращает образец Config
. Так как run
возвращает ()
в случае успеха и мы заботимся только об обнаружении ошибки, то нам не нужно вызывать unwrap_or_else
, чтобы вернуть развёрнутое значение, потому что оно будет только ()
.
Тело функций if let
и unwrap_or_else
одинаковы в обоих случаях: мы печатаем ошибку и выходим.
Наш дело minigrep
пока выглядит хорошо! Теперь мы разделим файл src/main.rs и поместим некоторый код в файл src/lib.rs. Таким образом мы сможем его проверять и чтобы в файле src/main.rs было меньшее количество полезных обязанностей.
Давайте перенесём весь код не относящийся к функции main
из файла src/main.rs в новый файл src/lib.rs:
run
use
Config
Config::build
Содержимое src/lib.rs должно иметь ярлыки, показанные в приложении 12-13 (мы опуисполнения тела функций для краткости). Обратите внимание, что код не будет собираться пока мы не изменим src/main.rs в приложении 12-14.
-Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- // --snip--
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- // --snip--
- let contents = fs::read_to_string(config.file_path)?;
-
- println!("With text:\n{contents}");
-
- Ok(())
-}
--
Мы добавили определетель доступа pub
к устройстве Config
, а также её полям, к способу build
и функции run
. Теперь у нас есть библиотечный ящик, который содержит открытый API, который мы можем проверять!
Теперь нам нужно подключить код, который мы перемеисполнения в src/lib.rs, в область видимости двоичного ящика внутри src/main.rs, как показано в приложении 12-14.
-Файл: src/main.rs
-use std::env;
-use std::process;
-
-use minigrep::Config;
-
-fn main() {
- // --snip--
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- println!("Searching for {}", config.query);
- println!("In file {}", config.file_path);
-
- if let Err(e) = minigrep::run(config) {
- // --snip--
- println!("Application error: {e}");
- process::exit(1);
- }
-}
--
Мы добавляем use minigrep::Config
для подключения вида Config
из ящика библиотеки в область видимости двоичного ящика и добавляем к имени функции run
приставка нашего ящика. Теперь все функции должны быть подключены и должны работать. Запустите программу с cargo run
и убедитесь, что все работает правильно.
Уф! Было много работы, но мы настроены на будущий успех. Теперь проще обрабатывать ошибки и мы сделали код более состоящим из звеньев. С этого особенности почти вся наша работа будет выполняться внутри src/lib.rs.
-Давайте воспользуемся этой новой выделения на звенья, сделав что-то, что было бы трудно со старым кодом, но легко с новым кодом: мы напишем несколько проверок!
- -Теперь, когда мы извлекли логику в src/lib.rs и оставили разбор переменных приказной строки и обработку ошибок в src/main.rs, стало гораздо проще писать проверки для основной возможности нашего кода. Мы можем вызывать функции напрямую с различными переменнойми и проверить возвращаемые значения без необходимости вызова нашего двоичного файла из приказной строки.
-В этом разделе в программу minigrep
мы добавим логику поиска с использованием этапа разработки через проверка (TDD), который следует этим шагам:
Хотя это всего лишь один из многих способов написания программного обеспечения, TDD может помочь в разработке кода. Написание проверки перед написанием кода, обеспечивающего прохождение проверки, помогает поддерживать высокое покрытие проверкими на протяжении всего этапа разработки.
-Мы проверим выполнение возможности, которая делает поиск строки запроса в содержимом файла и создание списка строк, соответствующих запросу. Мы добавим эту возможность в функцию под названием search
.
Поскольку они нам больше не нужны, давайте удалим указания с println!
, которые мы использовали для проверки поведения программы в src/lib.rs и src/main.rs. Затем в src/lib.rs мы добавим звено tests
с проверочной функцией, как делали это в главе 11. Проверочная функция определяет поведение, которое мы хотим проверить в функции search
: она должна принимать запрос и текст для поиска, а возвращать только те строки из текста, которые содержат запрос. В приложении 12-15 показан этот проверка, который пока не собирается.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
Этот проверка ищет строку "duct"
. Текст, в котором мы ищем, состоит из трёх строк, только одна из которых содержит "duct"
(обратите внимание, что обратная косая черта после открывающей двойной кавычки говорит Ржавчина не помещать символ новой строки в начало содержимого этого строкового записи). Мы проверяем, что значение, возвращаемое функцией search
, содержит только ожидаемую нами строку.
Мы не можем запустить этот проверка и увидеть сбой, потому что проверка даже не собирается: функции search
ещё не существует! В соответствии с принципами TDD мы добавим ровно столько кода, чтобы проверка собирался и запускался, добавив определение функции search
, которая всегда возвращает пустой вектор, как показано в приложении 12-16. Потом проверка должен собраться и потерпеть неудачу при запуске, потому что пустой вектор не равен вектору, содержащему строку "safe, fast, productive."
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- vec![]
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
Заметьте, что в ярлыке search
нужно явно указать время жизни 'a
для переменной contents
и возвращаемого значения. Напомним из Главы 10, что свойства времени жизни указывают с временем жизни какого переменной связано время жизни возвращаемого значения. В данном случае мы говорим, что возвращаемый вектор должен содержать срезы строк, ссылающиеся на содержимое переменной contents
(а не переменной query
).
Другими словами, мы говорим Rust, что данные, возвращаемые функцией search
, будут жить до тех пор, пока живут данные, переданные в функцию search
через переменная contents
. Это важно! Чтобы ссылки были действительными, данные, на которые ссылаются с помощью срезов тоже должны быть действительными; если сборщик предполагает, что мы делаем строковые срезы переменной query
, а не переменной contents
, он неправильно выполнит проверку безопасности.
Если мы забудем изложении времени жизни и попробуем собрать эту функцию, то получим следующую ошибку:
-$ cargo build
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
-error[E0106]: missing lifetime specifier
- --> src/lib.rs:28:51
- |
-28 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
- | ---- ---- ^ expected named lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
-help: consider introducing a named lifetime parameter
- |
-28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
- | ++++ ++ ++ ++
-
-For more information about this error, try `rustc --explain E0106`.
-error: could not compile `minigrep` (lib) due to 1 previous error
-
-Rust не может понять, какой из двух переменных нам нужен, поэтому нужно сказать ему об этом. Так как contents
является тем переменнаяом, который содержит весь наш текст, и мы хотим вернуть части этого текста, которые совпали при поиске, мы понимаем, что contents
является переменнаяом, который должен быть связан с возвращаемым значением временем жизни.
Другие языки программирования не требуют от вас связывания в ярлыке переменных с возвращаемыми значениями, но после определённой опытов вам станет проще. Можете сравнить этот пример с разделом «Проверка ссылок с временами жизни» главы 10.
-Запустим проверку:
-$ cargo test
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.97s
- Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 1 test
-test tests::one_result ... FAILED
-
-failures:
-
----- tests::one_result stdout ----
-thread 'tests::one_result' panicked at src/lib.rs:44:9:
-assertion `left == right` failed
- left: ["safe, fast, productive."]
- right: []
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::one_result
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Отлично. Наш проверка не сработал, как мы и ожидали. Давайте сделаем так, чтобы он срабатывал!
-Сейчас наш проверка не проходит, потому что мы всегда возвращаем пустой вектор. Чтобы исправить это и выполнить search
, наша программа должна выполнить следующие шаги:
Давайте проработаем каждый шаг, начиная с перебора строк.
-lines
В Ржавчина есть полезный способ для построчной повторения строк, удобно названный lines
, как показано в приложении 12-17. Обратите внимание, код пока не собирается.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- for line in contents.lines() {
- // do something with line
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
Способ lines
возвращает повторитель . Мы подробно поговорим об повторителях в Главе 13, но вспомните, что вы видели этот способ использования повторителя в Приложении 3-5, где мы использовали цикл for
с повторителем, чтобы выполнить некоторый код для каждого элемента в собрания.
Далее мы проверяем, содержит ли текущая строка нашу искомую строку. К счастью, у строк есть полезный способ contains
, который именно это и делает! Добавьте вызов способа contains
в функции search
, как показано в приложении 12-18. Обратите внимание, что это все ещё не собирается.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- for line in contents.lines() {
- if line.contains(query) {
- // do something with line
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
На данный мгновение мы наращиваем возможность. Чтобы заставить это собираться, нам нужно вернуть значение из тела функции, как мы указали в ярлыке функции.
-Чтобы завершить эту функцию, нам нужен способ сохранить совпадающие строки, которые мы хотим вернуть. Для этого мы можем создать изменяемый вектор перед циклом for
и вызывать способ push
для сохранения line
в векторе. После цикла for
мы возвращаем вектор, как показано в приложении 12-19.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-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
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
Теперь функция search
должна возвратить только строки, содержащие query
, и проверка должен пройти. Запустим его:
$ cargo test
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
- Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 1 test
-test tests::one_result ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests minigrep
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Наш проверка пройден, значит он работает!
-На этом этапе мы могли бы рассмотреть возможности изменения выполнения функции поиска, сохраняя прохождение проверок и поддерживая имеющуюся возможность. Код в функции поиска не так уж плох, но он не использует некоторые полезные функции повторителей. Вернёмся к этому примеру в главе 13, где будем исследовать повторители подробно, и посмотрим как его улучшить.
-search
в функции run
Теперь, когда функция search
работает и проверена, нужно вызвать search
из нашей функции run
. Нам нужно передать значение config.query
и contents
, которые run
читает из файла, в функцию search
. Тогда run
напечатает каждую строку, возвращаемую из search
:
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- for line in search(&config.query, &contents) {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
-Мы по-прежнему используем цикл for
для возврата каждой строки из функции search
и её печати.
Теперь вся программа должна работать! Давайте попробуем сначала запустить её со словом «frog», которое должно вернуть только одну строчку из стихотворения Эмили Дикинсон:
-$ cargo run -- frog poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
- Running `target/debug/minigrep frog poem.txt`
-How public, like a frog
-
-Здорово! Теперь давайте попробуем слово, которое будет соответствовать нескольким строкам, например «body»:
-$ cargo run -- body poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep body poem.txt`
-I'm nobody! Who are you?
-Are you nobody, too?
-How dreary to be somebody!
-
-И наконец, давайте удостоверимся, что мы не получаем никаких строк, когда ищем слово, отсутствующее в стихотворении, например «monomorphization»:
-$ cargo run -- monomorphization poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep monomorphization poem.txt`
-
-Отлично! Мы создали собственную простое-исполнение обычного средства и научились тому, как внутренне выстроить
-приложения. Мы также немного узнали о файловом вводе и выводе, временах жизни, проверке и разборе переменных приказной строки.
-Чтобы завершить этот дело, мы кратко выполним пару вещей: как работать с переменными окружения и как печатать в обычный поток ошибок, обе из которых полезны при написании окно выводаных программ.
- -Мы улучшим minigrep
, добавив дополнительную функцию: возможность для поиска без учёта регистра, которую пользователь может включить с помощью переменной среды окружения. Мы могли бы сделать эту функцию свойствоом приказной строки и потребовать, чтобы пользователи вводили бы её каждый раз при её применении, но вместо этого мы будем использовать переменную среды окружения, что позволит нашим пользователям устанавливать переменную среды один раз и все поиски будут не чувствительны к регистру в этом окно вызоваьном сеансе.
search
с учётом регистраМы, во-первых, добавим новую функцию search_case_insensitive
, которую мы будем вызывать, когда переменная окружения содержит значение. Мы продолжим следовать этапу TDD, поэтому первый шаг - это снова написать не проходящий проверку. Мы добавим новый проверка для новой функции search_case_insensitive
и переименуем наш старый проверка из one_result
в case_sensitive
, чтобы прояснить различия между двумя проверкими, как показано в приложении 12-20.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- for line in search(&config.query, &contents) {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Обратите внимание, что мы также отредактировали содержимое переменной contents
из старого проверки. Мы добавили новую строку с текстом "Duct tape."
, используя заглавную D, которая не должна соответствовать запросу "duct"
при поиске с учётом регистра. Такое изменение старого проверки помогает избежать случайного нарушения возможности поиска чувствительного к регистру, который мы уже выполнили. Этот проверка должен пройти сейчас и должен продолжать выполняться успешно, пока мы работаем над поиском без учёта регистра.
Новый проверка для поиска нечувствительного к регистру использует "rUsT"
качестве строки запроса. В функции search_case_insensitive
, которую мы собираемся выполнить, запрос "rUsT"
должен соответствовать строке содержащей "Rust:"
с большой буквы R и соответствовать строке "Trust me."
, хотя обе имеют разные регистры из запроса. Это наш не проходящий проверка, он не собирается, потому что мы ещё не определили функцию search_case_insensitive
. Не стесняйтесь добавлять скелет выполнение, которая всегда возвращает пустой вектор, подобно тому, как мы это делали для функции search
в приложении 12-16, чтобы увидеть сборку проверки и его сбой.
search_case_insensitive
Функция search_case_insensitive
, показанная в приложении 12-21, будет почти такая же, как функция search
. Разница лишь в том, что текст будет в нижнем регистре для query
и для каждой line
, так что для любого регистра входных переменных это будет тот же случай, когда мы проверяем, содержит ли строка запрос.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- for line in search(&config.query, &contents) {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Сначала преобразуем в нижний регистр строку query
и сохраняем её в затенённой переменной с тем же именем. Вызов to_lowercase
для строки запроса необходим, так что независимо от того, будет ли пользовательский запрос "rust"
, "RUST"
, "Rust"
или "rUsT"
, мы будем преобразовывать запрос к "rust"
и делать значение нечувствительным к регистру. Хотя to_lowercase
будет обрабатывать Unicode, он не будет точным на 100%. Если бы мы писали существующее приложение, мы бы хотели проделать здесь немного больше работы, но этот раздел посвящён переменным среды, а не Unicode, поэтому мы оставим это здесь.
Обратите внимание, что query
теперь имеет вид String
, а не срез строки, потому что вызов to_lowercase
создаёт новые данные, а не ссылается на существующие. К примеру, запрос: "rUsT"
это срез строки не содержащий строчных букв u
или t
, которые мы можем использовать, поэтому мы должны выделить новую String
, содержащую «rust»
. Когда мы передаём запрос query
в качестве переменной способа contains
, нам нужно добавить знак, поскольку ярлык contains
, определена для приёмы среза строки.
Затем мы добавляем вызов to_lowercase
для каждой строки line
для преобразования к нижнему регистру всех символов. Теперь, когда мы преобразовали line
и query
в нижний регистр, мы найдём совпадения независимо от того, в каком регистре находится переменная с запросом.
Давайте посмотрим, проходит ли эта выполнение проверки:
-$ cargo test
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
- Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 2 tests
-test tests::case_insensitive ... ok
-test tests::case_sensitive ... ok
-
-test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests minigrep
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-
-Отлично! Проверки прошли. Теперь давайте вызовем новую функцию search_case_insensitive
из функции run
. Во-первых, мы добавим свойство настройке в устройство Config
для переключения между поиском с учётом регистра и без учёта регистра. Добавление этого поля приведёт к ошибкам сборщика, потому что мы ещё нигде не объявим это поле:
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
-Мы добавили поле ignore_case
, которое содержит логическое значение. Далее нам нужна функция run
, чтобы проверить значение поля ignore_case
и использовать его, чтобы решить, вызывать ли функцию search
или функцию search_case_insensitive
, как показано в приложении 12-22. Этот код все ещё не собирается.
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Наконец, нам нужно проверить переменную среды. Функции для работы с переменными среды находятся в звене env
встроенной библиотеки, поэтому мы хотим подключить этот звено в область видимости в верхней части src/lib.rs. Затем мы будем использовать функцию var
из звена env
для проверки установлено ли любое значение в переменной среды с именем IGNORE_CASE
, как показано в приложении 12-23.
Файл: src/lib.rs
-use std::env;
-// --snip--
-
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- let ignore_case = env::var("IGNORE_CASE").is_ok();
-
- Ok(Config {
- query,
- file_path,
- ignore_case,
- })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Здесь мы создаём новую переменную ignore_case
. Чтобы установить её значение, мы вызываем функцию env::var
и передаём ей имя переменной окружения IGNORE_CASE
. Функция env::var
возвращает Result
, который будет успешным исходом Ok
содержащий значение переменной среды, если переменная среды установлена. Он вернёт исход Err
, если переменная окружения не установлена.
Мы используем способ is_ok
у Result
, чтобы проверить установлена ли переменная окружения, что будет означать, что программа должна выполнить поиск без учёта регистра. Если переменная среды IGNORE_CASE
не содержит любого значения, то is_ok
вернёт значение false и программа выполнит поиск c учётом регистра. Мы не заботимся о значении переменной среды, нас важно только установлена она или нет, поэтому мы проверяем is_ok
, а не используем unwrap
, expect
или любой другой способ, который мы видели у Result
.
Мы передаём значение переменной ignore_case
образцу Config
, чтобы функция run
могла прочитать это значение и решить, следует ли вызывать search
или search_case_insensitive
, как мы выполнили в приложении 12-22.
Давайте попробуем! Во-первых, мы запустим нашу программу без установленной переменной среды и с помощью значения запроса to
, который должен соответствовать любой строке, содержащей слово «to» в нижнем регистре:
$ cargo run -- to poem.txt
- Compiling minigrep v0.1.0 (file:///projects/minigrep)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/minigrep to poem.txt`
-Are you nobody, too?
-How dreary to be somebody!
-
-Похоже, все ещё работает! Теперь давайте запустим программу с IGNORE_CASE
, установленным в 1
, но с тем же значением запроса to
.
$ IGNORE_CASE=1 cargo run -- to poem.txt
-
-Если вы используете PowerShell, вам нужно установить переменную среды и запустить программу двумя приказми, а не одной:
-PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
-
-Это заставит переменную окружения IGNORE_CASE
сохраниться до конца сеанса работы окне вывода. Переменную можно отключить с помощью приказы Remove-Item
:
PS> Remove-Item Env:IGNORE_CASE
-
-Мы должны получить строки, содержащие «to», которые могут иметь заглавные буквы:
- -Are you nobody, too?
-How dreary to be somebody!
-To tell your name the livelong day
-To an admiring bog!
-
-Отлично, мы также получили строки, содержащие «To»! Наша программа minigrep
теперь может выполнять поиск без учёта регистра, управляемая переменной среды. Теперь вы знаете, как управлять свойствами, заданными с помощью переменных приказной строки или переменных среды.
Некоторые программы допускают использование переменных и переменных среды для одной и той же настройке. В таких случаях программы решают, что из них имеет больший приоритет. Для другого самостоятельного упражнения попробуйте управлять чувствительностью к регистру с помощью переменной приказной строки или переменной окружения. Решите, переменная приказной строки или переменная среды будет иметь приоритет, если программа выполняется со значениями "учитывать регистр" в одном случае, и "пренебрегать регистр" в другом.
-Звено std::env
содержит много других полезных функций для работы с переменными среды: ознакомьтесь с его документацией, чтобы узнать доступные.
В данный мгновение мы записываем весь наш вывод в окно вызова, используя функцию println!
. В большинстве окно вызоваов предоставлено два вида вывода: обычный поток вывода ( stdout
) для общей сведений и обычный поток ошибок ( stderr
) для сообщений об ошибках. Это различие позволяет пользователям выбирать, направлять ли успешный вывод программы в файл, но при этом выводить сообщения об ошибках на экран.
Функция println!
может печатать только в обычный вывод, поэтому мы должны использовать что-то ещё для печати в обычный поток ошибок.
Во-первых, давайте посмотрим, как содержимое, напечатанное из minigrep
в настоящее время записывается в обычный вывод, включая любые сообщения об ошибках, которые мы хотим вместо этого записать в обычный поток ошибок. Мы сделаем это, перенаправив обычный поток вывода в файл и намеренно вызовем ошибку. Мы не будем перенаправлять обычный поток ошибок, поэтому любой содержание, отправленный в поток принятых ошибок будет продолжать отображаться на экране.
Ожидается, что программы приказной строки будут отправлять сообщения об ошибках в обычный поток ошибок, поэтому мы все равно можем видеть сообщения об ошибках на экране, даже если мы перенаправляем обычный поток вывода в файл. Наша программа в настоящее время не ведёт себя правильно: мы увидим, что она сохраняет вывод сообщения об ошибке в файл!
-Чтобы отобразить это поведение, мы запустим программу с помощью >
и именем файла output.txt в который мы хотим перенаправить обычный поток вывода. Мы не будем передавать никаких переменных, что должно вызвать ошибку:
$ cargo run > output.txt
-
-правила написания >
указывает оболочке записывать содержимое принятого вывода в output.txt вместо экрана. Мы не увидели сообщение об ошибке, которое мы ожидали увидеть на экране, так что это означает, что оно должно быть в файле. Вот что содержит output.txt:
Problem parsing arguments: not enough arguments
-
-Да, наше сообщение об ошибке выводится в обычный вывод. Гораздо более полезнее, чтобы подобные сообщения об ошибках печатались в встроенной поток ошибок, поэтому в файл попадают только данные из успешного запуска. Мы поменяем это.
-Мы будем использовать код в приложении 12-24, чтобы изменить способ вывода сообщений об ошибках. Из-за переработки кода, который мы делали ранее в этой главе, весь код, который печатает сообщения об ошибках, находится в одной функции: main
. Обычная библиотека предоставляет макрос eprintln!
который печатает в обычный поток ошибок, поэтому давайте изменим два места, где мы вызывали println!
для печати ошибок, чтобы использовать eprintln!
вместо этого.
Файл: src/main.rs
-use std::env;
-use std::process;
-
-use minigrep::Config;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- if let Err(e) = minigrep::run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
-}
--
Давайте снова запустим программу таким же образом, без каких-либо переменных и перенаправим обычный вывод с помощью >
:
$ cargo run > output.txt
-Problem parsing arguments: not enough arguments
-
-Теперь мы видим ошибку на экране и output.txt не содержит ничего, что мы ожидаем от программы приказной строки.
-Давайте снова запустим программу с переменнойми, которые не вызывают ошибку, но все же перенаправляют обычный вывод в файл, например так:
-$ cargo run -- to poem.txt > output.txt
-
-Мы не увидим никакого вывода в окно вызова, а output.txt будет содержать наши итоги:
-Файл: output.txt
-Are you nobody, too?
-How dreary to be somebody!
-
-Это отображает, что в зависимости от случаи мы теперь используем обычный поток вывода для успешного текста и обычный поток ошибок для вывода ошибок.
-В этой главе были повторены некоторые основные подходы, которые вы изучили до сих пор и было рассказано, как выполнять обычные действия ввода-вывода в Rust. Используя переменные приказной строки, файлы, переменные среды и макросeprintln!
для печати ошибок и вы теперь готовы писать приложения приказной строки. В сочетании с подходами из предыдущих главах, ваш код будет хорошо согласован, будет эффективно хранить данные в соответствующих устройствах, хорошо обрабатывать ошибки и хорошо проверяться.
Далее мы рассмотрим некоторые возможности Rust, на которые повлияли полезные языки: замыкания и повторители.
- -Внешний вид языка Ржавчина черпал вдохновение из многих других языков и техник, среди которых значительное влияние оказало функциональное программирование. Программирование в функциональном исполнении подразумевает использование функций взначении предметов, передавая их в качестве переменных, возвращая их из других функций, присваивая их переменным для последующего выполнения и так далее.
-В этой главе мы не будем рассуждать о том, что из себя представляет функциональное программирование, а обсудим возможности Rust, присущие многим языкам, которые принято называть функциональными.
-Более подробно мы поговорим про:
-Мы уже рассмотрели другие возможности Rust, такие как сопоставление с образцом и перечисления, которые также появились под влиянием функционального исполнения. Поскольку освоение замыканий и повторителей — важная часть написания идиоматичного, быстрого кода на Rust, мы посвятим им всю эту главу.
- -Замыкания в Ржавчина - это анонимные функции, которые можно сохранять в переменных или передавать в качестве переменных другим функциям. Вы можете создать замыкание в одном месте, а затем вызвать его в каком-нибудь другом, чтобы выполнить обработку в ином среде. В отличие от функций, замыкания могут использовать значения из области видимости в которой они были определены. Мы выполним, как эти функции замыканий открывают возможности для повторного использования кода и изменения его поведения.
- - -Сначала мы рассмотрим, как с помощью замыканий можно использовать предметы из области, в которой они вместе были определены, для их последующего использования. Вот сценарий: Время от времени наша предприятие по производству футболок в качестве акции дарит эксклюзивные футболки, выпущенные ограниченным тиражом, каким-нибудь пользователям из нашего списка рассылки. Люди из списка рассылки при желании могут выбрать любимый цвет в своём профиле. Если человек, выбранный для получения бесплатной футболки, указал свой любимый цвет, он получает футболку этого цвета. Если человек не указал свой любимый цвет, он получит рубашку того цвета, которых у предприятия на данный мгновение больше всего.
-Существует множество способов выполнить это. В данном примере мы будем использовать перечисление ShirtColor
, которое может быть двух исходов Red
и Blue
(для простоты ограничим количество доступных цветов этими двумя). Запасы предприятия мы представим устройством Inventory
, которая состоит из поля shirts
, содержащего Vec<ShirtColor>
, в котором перечислены рубашки тех цветов, которые есть в наличии. Способ giveaway
, определённый в Inventory
, принимает необязательный свойство - цвет, предпочитаемый пользователем, выбранным для получения бесплатной рубашки, и возвращает тот цвет рубашки, который он получит в действительности. Эта схема показана в приложении 13-1:
Имя файла: src/main.rs
-#[derive(Debug, PartialEq, Copy, Clone)]
-enum ShirtColor {
- Red,
- Blue,
-}
-
-struct Inventory {
- shirts: Vec<ShirtColor>,
-}
-
-impl Inventory {
- fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
- user_preference.unwrap_or_else(|| self.most_stocked())
- }
-
- fn most_stocked(&self) -> ShirtColor {
- let mut num_red = 0;
- let mut num_blue = 0;
-
- for color in &self.shirts {
- match color {
- ShirtColor::Red => num_red += 1,
- ShirtColor::Blue => num_blue += 1,
- }
- }
- if num_red > num_blue {
- ShirtColor::Red
- } else {
- ShirtColor::Blue
- }
- }
-}
-
-fn main() {
- let store = Inventory {
- shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
- };
-
- let user_pref1 = Some(ShirtColor::Red);
- let giveaway1 = store.giveaway(user_pref1);
- println!(
- "The user with preference {:?} gets {:?}",
- user_pref1, giveaway1
- );
-
- let user_pref2 = None;
- let giveaway2 = store.giveaway(user_pref2);
- println!(
- "The user with preference {:?} gets {:?}",
- user_pref2, giveaway2
- );
-}
--
В магазине store
, определённом в main
, осталось две синие и одна красная рубашки для этой ограниченной акции. Мы вызываем способ giveaway
для пользователя предпочитающего красную рубашку и для пользователя без каких-либо предпочтений.
Опять же, этот код мог быть выполнен множеством способов, но в данном случае, чтобы сосредоточиться на замыканиях, мы придерживались изученных ранее подходов, за исключением тела способа giveaway
, в котором используется замыкание. В способе giveaway
мы получаем пользовательское предпочтение цвета как свойство вида Option<ShirtColor>
и вызываем способ unwrap_or_else
на user_preference
. Способ unwrap_or_else
перечисления Option<T>
определён встроенной библиотекой. Он принимает один переменная: замыкание без переменных, которое возвращает значение T
(преобразуется в вид значения, которое окажется в исходе Some
перечисления Option<T>
, в нашем случае ShirtColor
). Если Option<T>
окажется исходом Some
, unwrap_or_else
вернёт значение из Some
. А если Option<T>
будет является исходом None
, unwrap_or_else
вызовет замыкание и вернёт значение, возвращённое замыканием.
В качестве переменной unwrap_or_else
мы передаём замыкание || self.most_stocked()
. Это замыкание, которое не принимает никаких свойств (если бы у замыкания были свойства, они были бы перечислены между двумя вертикальными полосами). В теле замыкания вызывается self.most_stocked()
. Здесь мы определили замыкание, а выполнение unwrap_or_else
такова, что выполнится оно позднее, когда потребуется получить итог.
Выполнение этого кода выводит:
-$ cargo run
- Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
- Running `target/debug/shirt-company`
-The user with preference Some(Red) gets Red
-The user with preference None gets Blue
-
-Важной особенностью здесь является то, что мы передали замыкание, которое вызывает self.most_stocked()
текущего образца Inventory
. Обычной библиотеке не нужно знать ничего о видах Inventory
или ShirtColor
, которые мы определили, или о логике, которую мы хотим использовать в этом сценарии. Замыкание определяет неизменяемую ссылку на self
Inventory
и передаёт её с указанным нами кодом в способ unwrap_or_else
. А вот функции не могут определять своё окружение таким образом.
Есть и другие различия между функциями и замыканиями. Замыкания обычно не требуют определенния видов входных свойств или возвращаемого значения, как это делается в функциях fn
. Изложения видов требуются для функций, потому что виды являются частью явного внешней оболочки, предоставляемого пользователям. Жёсткое определение таких внешних оболочек важно для того, чтобы все были согласованы в том, какие виды значений использует и возвращает функция. А вот замыкания, напротив, не употребляются взначении подобных открытых внешних оболочек: они хранятся в переменных, используются не имея имени и незримо для пользователей нашей библиотеки.
Замыкания, как правило, небольшие и уместны в каком-то узконаправленном среде, а не в произвольных случаях. В этих ограниченных средах сборщик может вывести виды свойств и возвращаемого вида, подобно тому, как он может вывести виды большинства переменных (есть редкие случаи, когда сборщику также нужны изложении видов замыканий).
-Как и в случае с переменными, мы можем добавить изложении видов, если хотим повысить ясность и чёткость описания ценой увеличения многословности, большей чем это необходимо. Определение видов для замыкания будет выглядеть как определение, показанное в приложении 13-2. В этом примере мы определяем замыкание и храним его в переменной, а не определяем замыкание в том месте, куда мы передаём его в качестве переменной, как это было в приложении 13-1.
-Имя файла: src/main.rs
--use std::thread; -use std::time::Duration; - -fn generate_workout(intensity: u32, random_number: u32) { - let expensive_closure = |num: u32| -> u32 { - println!("calculating slowly..."); - thread::sleep(Duration::from_secs(2)); - num - }; - - if intensity < 25 { - println!("Today, do {} pushups!", expensive_closure(intensity)); - println!("Next, do {} situps!", expensive_closure(intensity)); - } else { - if random_number == 3 { - println!("Take a break today! Remember to stay hydrated!"); - } else { - println!( - "Today, run for {} minutes!", - expensive_closure(intensity) - ); - } - } -} - -fn main() { - let simulated_user_specified_value = 10; - let simulated_random_number = 7; - - generate_workout(simulated_user_specified_value, simulated_random_number); -}
-
С добавлением наставлений видов правила написания замыканий выглядит более похожим на правила написания функций. Здесь мы, для сравнения, определяем функцию, которая добавляет 1 к своему свойству, и замыкание, которое имеет такое же поведение. Мы добавили несколько пробелов, чтобы выровнять соответствующие части. Это показывает, что правила написания замыкания похож на правила написания функции, за исключением использования труб (вертикальная черта) и количества необязательного правил написания:
-fn add_one_v1 (x: u32) -> u32 { x + 1 }
-let add_one_v2 = |x: u32| -> u32 { x + 1 };
-let add_one_v3 = |x| { x + 1 };
-let add_one_v4 = |x| x + 1 ;
-В первой строке показано определение функции, а во второй - полностью определенное определение замыкания. В третьей строке мы удаляем изложении видов из определения замыкания. В четвёртой строке мы убираем скобки, которые являются необязательными, поскольку тело замыкания содержит только одну действие. Это всё правильные определения, которые будут иметь одинаковое поведение при вызове. Строки add_one_v3
и add_one_v4
требуют, чтобы замыкания были вычислены до сборки, поскольку виды будут выведены из их использования. Это похоже на let v = Vec::new();
, когда в Vec
необходимо вставить либо изложении видов, либо значения некоторого вида, чтобы Ржавчина смог вывести вид.
Для определений замыкания сборщик выводит определенные виды для каждого из свойств и возвращаемого значения. Например, в приложении 13-3 показано определение короткого замыкания, которое просто возвращает значение, полученное в качестве свойства. Это замыкание не очень полезно, кроме как для целей данного примера. Обратите внимание, что мы не добавили в определение никаких наставлений видов. Поскольку наставлений видов нет, мы можем вызвать замыкание для любого вида, что мы и сделали в первый раз с String
. Если затем мы попытаемся вызвать example_closure
для целого числа, мы получим ошибку.
Имя файла: src/main.rs
-fn main() {
- let example_closure = |x| x;
-
- let s = example_closure(String::from("hello"));
- let n = example_closure(5);
-}
--
Сборщик вернёт нам вот такую ошибку:
-$ cargo run
- Compiling closure-example v0.1.0 (file:///projects/closure-example)
-error[E0308]: mismatched types
- --> src/main.rs:5:29
- |
-5 | let n = example_closure(5);
- | --------------- ^- help: try using a conversion method: `.to_string()`
- | | |
- | | expected `String`, found integer
- | arguments to this function are incorrect
- |
-note: expected because the closure was earlier called with an argument of type `String`
- --> src/main.rs:4:29
- |
-4 | let s = example_closure(String::from("hello"));
- | --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
- | |
- | in this closure call
-note: closure parameter defined here
- --> src/main.rs:2:28
- |
-2 | let example_closure = |x| x;
- | ^
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
-
-При первом вызове example_closure
со значением String
сборщик определяет вид x
и возвращаемый вид замыкания как String
. Эти виды затем определятся в замыкании в example_closure
, и мы получаем ошибку вида при следующей попытке использовать другой вид с тем же замыканием.
Замыкания могут захватывать значения из своего окружения тремя способами, которые соответствуют тем же трём способам, которыми функция может принимать свойства: заимствование неизменяемых, заимствование изменяемых и получение владения. Замыкание самостоятельно определяет, какой из этих способов использовать, исходя из того, что тело функции делает с полученными значениями.
-В приложении 13-4 мы определяем замыкание, которое захватывает неизменяемую ссылку на вектор с именем list
, поскольку неизменяемой ссылки достаточно для печати значения:
Имя файла: src/main.rs
--fn main() { - let list = vec![1, 2, 3]; - println!("Before defining closure: {list:?}"); - - let only_borrows = || println!("From closure: {list:?}"); - - println!("Before calling closure: {list:?}"); - only_borrows(); - println!("After calling closure: {list:?}"); -}
-
Этот пример также отображает, то что переменная может быть привязана к определению замыкания, и в дальнейшем мы можем вызвать замыкание, используя имя переменной и круглые скобки, как если бы имя переменной было именем функции.
-Поскольку мы можем иметь несколько неизменяемых ссылок на list
одновременно, list
остаётся доступным из кода до определения замыкания, после определения замыкания, а также до вызова замыкания и после. Этот код собирается, выполняется и печатает:
$ cargo run
- Locking 1 package to latest compatible version
- Adding closure-example v0.1.0 (/Users/carolnichols/rust/book/tmp/listings/ch13-functional-features/listing-13-04)
- Compiling closure-example v0.1.0 (file:///projects/closure-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
- Running `target/debug/closure-example`
-Before defining closure: [1, 2, 3]
-Before calling closure: [1, 2, 3]
-From closure: [1, 2, 3]
-After calling closure: [1, 2, 3]
-
-В следующем приложении 13-5 мы изменили тело замыкания так, чтобы оно добавляло элемент в вектор list
. Теперь замыкание захватывает изменяемую ссылку:
Имя файла: src/main.rs
--fn main() { - let mut list = vec![1, 2, 3]; - println!("Before defining closure: {list:?}"); - - let mut borrows_mutably = || list.push(7); - - borrows_mutably(); - println!("After calling closure: {list:?}"); -}
-
Этот код собирается, запускается и печатает:
-$ cargo run
- Locking 1 package to latest compatible version
- Adding closure-example v0.1.0 (/Users/carolnichols/rust/book/tmp/listings/ch13-functional-features/listing-13-05)
- Compiling closure-example v0.1.0 (file:///projects/closure-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
- Running `target/debug/closure-example`
-Before defining closure: [1, 2, 3]
-After calling closure: [1, 2, 3, 7]
-
-Обратите внимание, что между определением и вызовом замыкания borrows_mutably
больше нет println!
: когда определяется borrows_mutably
, оно захватывает изменяемую ссылку на list
. После вызова замыкания мы больше не используем его, поэтому изменяемое заимствование заканчивается. Между определением замыкания и вызовом замыкания неизменяемое заимствование для печати недоступно, потому что при наличии изменяемого заимствования никакие другие заимствования недопустимы. Попробуйте добавить туда println!
и посмотрите, какое сообщение об ошибке вы получите!
Если вы хотите заставить замыкание принять владение значениями, которые оно использует в окружении, даже если в теле замыкания нет кода, требующего владения, вы можете использовать ключевое слово move
перед списком свойств.
Эта техника в основном полезна при передаче замыкания новому потоку, чтобы переместить данные так, чтобы они принадлежали новому потоку. Мы подробно обсудим потоки и то, зачем их использовать, в главе 16, когда будем говорить о одновременности, а пока давайте вкратце рассмотрим порождение нового потока с помощью замыкания, в котором используется ключевое слово move
. В приложении 13-6 показан код из приложения 13-4, измененный для печати вектора в новом потоке, а не в основном потоке:
Файл: src/main.rs
--use std::thread; - -fn main() { - let list = vec![1, 2, 3]; - println!("Before defining closure: {list:?}"); - - thread::spawn(move || println!("From thread: {list:?}")) - .join() - .unwrap(); -}
-
Мы порождаем новый поток, передавая ему в качестве переменной замыкание для выполнения. Тело замыкания распечатывает список. В приложении 13-4 замыкание захватило list
только с помощью неизменяемой ссылки, потому что это наименьше необходимый доступ к list
для его печати. В этом примере, несмотря на то, что тело замыкания по-прежнему требует только неизменяемой ссылки, нам нужно указать, что list
должен быть перемещён в замыкание, поместив ключевое слово move
в начало определения замыкания. Новый поток может завершиться раньше, чем завершится основной поток, или основной поток может завершиться первым. Если основной поток сохранил владение list
, но завершился раньше нового потока и удалил list
, то неизменяемая ссылка в потоке будет недействительной. Поэтому сборщик требует, чтобы list
был перемещён в замыкание, переданное новому потоку, чтобы ссылка была действительной. Попробуйте убрать ключевое слово move
или использовать list
в основном потоке после определения замыкания и посмотрите, какие ошибки сборщика вы получите!
Fn
После того, как замыкание захватило ссылку или владение значением из среды, в которой оно определено (тем самым влияя на то, что перемещается в замыкание), код в теле замыкания определяет, что происходит со ссылками или значениями, в мгновение последующего выполнения замыкания (тем самым влияя на то, что перемещается из замыкания). Тело замыкания может делать любое из следующих действий: перемещать захваченное значение из замыкания, изменять захваченное значение, не перемещать и не изменять значение или вообще ничего не захватывать из среды.
-То, как замыкание получает и обрабатывает значения из своего окружения, указывает на то, какие особенности выполняет замыкание, а с помощью особенностей функции и устройства могут определять, какие виды замыканий они могут использовать. Замыканиям самостоятельно присваивается выполнение одного, двух или всех трёх из нижеперечисленных особенностей Fn
, аддитивным образом, в зависимости от того, как тело замыкания обрабатывает значения:
FnOnce
применяется к замыканиям, которые могут быть вызваны один раз. Все замыкания выполняют по крайней мере этот особенность, потому что все замыкания могут быть вызваны. Замыкание, которое перемещает захваченные значения из своего тела, выполняет только FnOnce
и ни один из других признаков Fn
, потому что оно может быть вызвано только один раз.FnMut
применяется к замыканиям, которые не перемещают захваченные значения из своего тела, но могут изменять захваченные значения. Такие замыкания могут вызываться более одного раза.Fn
применяется к замыканиям, которые не перемещают захваченные значения из своего тела и не изменяют захваченные значения, а также к замыканиям, которые ничего не захватывают из своего окружения. Такие замыкания могут выполняться более одного раза и не меняют ничего в своём окружении, что важно в таких случаях, как одновременный вызов замыкания несколько раз.Давайте рассмотрим определение способа unwrap_or_else
у Option<T>
, который мы использовали в приложении 13-1:
impl<T> Option<T> {
- pub fn unwrap_or_else<F>(self, f: F) -> T
- where
- F: FnOnce() -> T
- {
- match self {
- Some(x) => x,
- None => f(),
- }
- }
-}
-Напомним, что T
- это гибкий вид, отображающий вид значения в Some
исходе Option
. Этот вид T
также является возвращаемым видом функции unwrap_or_else
: например, код, вызывающий unwrap_or_else
у Option<String>
, получит String
.
Далее, обратите внимание, что функция unwrap_or_else
имеет дополнительный свойство гибкого вида F
. Здесь F
- это вид входного свойства f
, который является замыканием, заданным нами при вызове unwrap_or_else
.
Ограничением особенности, заданным для обобщённого вида F
, является FnOnce() -> T
, что означает, что F
должен вызываться один раз, не принимать никаких переменных и возвращать T
. Использование FnOnce
в ограничении особенности говорит о том, что unwrap_or_else
должен вызывать f
не более одного раза. В теле unwrap_or_else
мы видим, что если Option
будет равен Some
, то f
не будет вызван. Если же значение Option
будет равным None
, то f
будет вызван один раз. Поскольку все замыкания выполняют FnOnce
, unwrap_or_else
принимает самые разные виды замыканий и является настолько гибким, насколько это возможно.
--Примечание: Функции также могут выполнить все три особенности
-Fn
. Если то, что мы хотим сделать, не требует захвата значения из среды, мы можем передавать имя какой-либо функции, а не замыкания, когда нам нужно что-то, выполняющее один из особенностейFn
. Например, для значенияOption<Vec<T>>
мы можем вызватьunwrap_or_else(Vec::new)
, чтобы получить новый пустой вектор, если значение окажетсяNone
.
Теперь рассмотрим способ встроенной библиотеки sort_by_key
, определённый у срезов, чтобы увидеть, чем он отличается от unwrap_or_else
и почему sort_by_key
использует FnMut
вместо FnOnce
для ограничения особенности. Замыкание принимает единственный переменная в виде ссылки на текущий элемент в рассматриваемом срезе и возвращает значение вида K
, к которому применима сортировка. Эта функция полезна, когда вы хотите отсортировать срез по определённому свойству каждого элемента. В приложении 13-7 у нас есть список образцов Rectangle
, и мы используем sort_by_key
, чтобы упорядочить их по свойству width
от меньшего к большему:
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let mut list = [ - Rectangle { width: 10, height: 1 }, - Rectangle { width: 3, height: 5 }, - Rectangle { width: 7, height: 12 }, - ]; - - list.sort_by_key(|r| r.width); - println!("{list:#?}"); -}
-
Этот код печатает:
-$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
- Running `target/debug/rectangles`
-[
- Rectangle {
- width: 3,
- height: 5,
- },
- Rectangle {
- width: 7,
- height: 12,
- },
- Rectangle {
- width: 10,
- height: 1,
- },
-]
-
-Причина, по которой sort_by_key
определена как принимающая замыкание FnMut
, заключается в том, что она вызывает замыкание несколько раз: по одному разу для каждого элемента в срезе. Замыкание |r| r.width
не захватывает, не изменяет и не перемещает ничего из своего окружения, поэтому оно удовлетворяет требованиям связанности признаков.
И наоборот, в приложении 13-8 показан пример замыкания, которое выполняет только признак FnOnce
, потому что оно перемещает значение из среды. Сборщик не позволит нам использовать это замыкание с sort_by_key
:
Файл: src/main.rs
-#[derive(Debug)]
-struct Rectangle {
- width: u32,
- height: u32,
-}
-
-fn main() {
- let mut list = [
- Rectangle { width: 10, height: 1 },
- Rectangle { width: 3, height: 5 },
- Rectangle { width: 7, height: 12 },
- ];
-
- let mut sort_operations = vec![];
- let value = String::from("closure called");
-
- list.sort_by_key(|r| {
- sort_operations.push(value);
- r.width
- });
- println!("{list:#?}");
-}
--
Это надуманный, замысловатый способ (который не работает) подсчёта количества вызовов sort_by_key
при сортировке list
. Этот код пытается выполнить подсчёт, перемещая value
- String
из окружения замыкания - в вектор sort_operations
. Замыкание захватывает value
, затем перемещает value
из замыкания, передавая владение на value
вектору sort_operations
. Это замыкание можно вызвать один раз; попытка вызвать его второй раз не сработает, потому что value
уже не будет находиться в той среде, из которой его можно будет снова поместить в sort_operations
! Поэтому это замыкание выполняет только FnOnce
. Когда мы попытаемся собрать этот код, мы получим ошибку сообщающую о том что value
не может быть перемещено из замыкания, потому что замыкание должно выполнить FnMut
:
$ cargo run
- Compiling rectangles v0.1.0 (file:///projects/rectangles)
-error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
- --> src/main.rs:18:30
- |
-15 | let value = String::from("closure called");
- | ----- captured outer variable
-16 |
-17 | list.sort_by_key(|r| {
- | --- captured by this `FnMut` closure
-18 | sort_operations.push(value);
- | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait
- |
-help: consider cloning the value if the performance cost is acceptable
- |
-18 | sort_operations.push(value.clone());
- | ++++++++
-
-For more information about this error, try `rustc --explain E0507`.
-error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
-
-Ошибка указывает на строку в теле замыкания, которая перемещает value
из окружения. Чтобы исправить это, нужно изменить тело замыкания так, чтобы оно не перемещало значения из окружения. Для подсчёта количества вызовов sort_by_key
более простым способом является хранение счётчика в окружении и увеличение его значения в теле замыкания. Замыкание в приложении 13-9 работает с sort_by_key
, поскольку оно определяет только изменяемую ссылку на счётчик num_sort_operations
и поэтому может быть вызвано более одного раза:
Файл: src/main.rs
--#[derive(Debug)] -struct Rectangle { - width: u32, - height: u32, -} - -fn main() { - let mut list = [ - Rectangle { width: 10, height: 1 }, - Rectangle { width: 3, height: 5 }, - Rectangle { width: 7, height: 12 }, - ]; - - let mut num_sort_operations = 0; - list.sort_by_key(|r| { - num_sort_operations += 1; - r.width - }); - println!("{list:#?}, sorted in {num_sort_operations} operations"); -}
-
Особенности Fn
важны при определении или использовании функций или видов, использующих замыкания. В следующем разделе мы обсудим повторители. Многие способы повторителей принимают переменные в виде замыканий, поэтому не забывайте об этих подробностях, пока мы продвигаемся дальше!
Использование образца Повторитель помогает при необходимости поочерёдного выполнения какой-либо действия над элементами последовательности. Повторитель отвечает за логику перебора элементов и определение особенности завершения последовательности. Используя повторители, вам не нужно самостоятельно выполнить всю эту логику.
-В Ржавчина повторители ленивые (lazy), то есть они не делают ничего, пока вы не вызовете особые способы, потребляющие повторитель , чтобы задействовать его. Например, код в приложении 13-10 создаёт повторитель элементов вектора v1
, вызывая способ iter
, определённый у Vec<T>
. Сам по себе этот код не делает ничего полезного.
-fn main() { - let v1 = vec![1, 2, 3]; - - let v1_iter = v1.iter(); -}
-
Повторитель хранится в переменной v1_iter
. Создав повторитель , мы можем использовать его различными способами. В приложении 3-5 главы 3 мы совершали обход элементов массива используя цикл for
для выполнения какого-то кода над каждым из его элементов. Под капотом это неявно создавало, а затем потребляло повторитель , но до сих пор мы не касались того, как именно это работает.
В примере из приложения 13-11 мы отделили создание повторителя от его использования в цикле for. В цикле for, использующем повторитель в v1_iter, каждый элемент повторителя участвует только в одной повторения цикла, в ходе которой выводится на экран его значение.
--fn main() { - let v1 = vec![1, 2, 3]; - - let v1_iter = v1.iter(); - - for val in v1_iter { - println!("Got: {val}"); - } -}
-
В языках, обычные библиотеки которых не предоставляют повторители, вы, скорее всего, напишите эту же возможность так: создадите переменную со значением 0 затем, в цикле, использовав её для получения элемента вектора по порядковому указателю, будете увеличивать её значение, и так, пока оно не достигнет числа равного количеству элементов в векторе.
-Повторители выполняют всю эту логику за вас, сокращая количество повторяющегося кода, который возможно может быть написан неправильно. Повторители дают вам гибкость, позволяя использовать одинаковые принципы работы с различными видами последовательностей, а не только со устройствами данных, которые можно упорядочивать, например, векторами. Давайте рассмотрим, как повторители это делают.
-Iterator
и способ next
Все повторители выполняют особенность Iterator
, который определён в встроенной библиотеке. Его определение выглядит так:
-#![allow(unused)] -fn main() { -pub trait Iterator { - type Item; - - fn next(&mut self) -> Option<Self::Item>; - - // methods with default implementations elided -} -}
Обратите внимание данное объявление использует новый правила написания: type Item
и Self::Item
, которые определяют сопряженный вид (associated type) с этим особенностью. Мы подробнее поговорим о сопряженных видах в главе 19. Сейчас вам нужно знать, что этот код требует от выполнений особенности Iterator
определить требуемый им вид Item
и данный вид Item
используется в способе next
. Другими словами, вид Item
будет являться видом элемента, который возвращает повторитель .
Особенность Iterator
требует, чтобы разработчики определяли только один способ: способ next
, который возвращает один элемент повторителя за раз обёрнутый в исход Some
и когда повторение завершена, возвращает None
.
Мы можем вызывать способ next
у повторителей напрямую; в приложении 13-12 показано, какие значения возвращаются при повторных вызовах next
у повторителя, созданного из вектора.
Файл: src/lib.rs
-#[cfg(test)]
-mod tests {
- #[test]
- fn iterator_demonstration() {
- let v1 = vec![1, 2, 3];
-
- let mut v1_iter = v1.iter();
-
- assert_eq!(v1_iter.next(), Some(&1));
- assert_eq!(v1_iter.next(), Some(&2));
- assert_eq!(v1_iter.next(), Some(&3));
- assert_eq!(v1_iter.next(), None);
- }
-}
--
Обратите внимание, что нам нужно сделать переменную v1_iter
изменяемой: вызов способа next
повторителя изменяет внутреннее состояние повторителя, которое повторитель использует для отслеживания того, где он находится в последовательности. Другими словами, этот код потребляет (consume) или использует повторитель . Каждый вызов next
потребляет элемент из повторителя. Нам не нужно было делать изменяемой v1_iter
при использовании цикла for
, потому что цикл забрал во владение v1_iter
и сделал её изменяемой неявно для нас.
Заметьте также, что значения, которые мы получаем при вызовах next
являются неизменяемыми ссылками на значения в векторе. Способ iter
создаёт повторитель по неизменяемым ссылкам. Если мы хотим создать повторитель , который становится владельцем v1
и возвращает принадлежащие ему значения, мы можем вызвать into_iter
вместо iter
. Точно так же, если мы хотим перебирать изменяемые ссылки, мы можем вызвать iter_mut
вместо iter
.
У особенности Iterator
есть несколько способов, выполнение которых по умолчанию предоставляется встроенной библиотекой; вы можете узнать об этих способах, просмотрев документацию API встроенной библиотеки для Iterator
. Некоторые из этих способов вызывают next
в своём определении, поэтому вам необходимо выполнить способ next
при выполнения особенности Iterator
.
Способы, вызывающие next
, называются потребляющими переходниками, поскольку их вызов потребляет повторитель . Примером может служить способ sum
, который забирает во владение повторитель и перебирает элементы, многократно вызывая next
, тем самым потребляя повторитель . В этапе повторения он добавляет каждый элемент к текущей сумме и возвращает итоговое значение по завершении повторения. В приложении 13-13 приведён проверка, отображающий использование способа sum
:
Файл: src/lib.rs
-#[cfg(test)]
-mod tests {
- #[test]
- fn iterator_sum() {
- let v1 = vec![1, 2, 3];
-
- let v1_iter = v1.iter();
-
- let total: i32 = v1_iter.sum();
-
- assert_eq!(total, 6);
- }
-}
--
Мы не можем использовать v1_iter
после вызова способа sum
, потому что sum
забирает во владение повторитель у которого вызван способ.
Переходники повторителей - это способы, определённые для особенности Iterator
, которые не потребляют повторитель . Вместо этого они создают различные повторители, изменяя некоторые особенности исходного повторителя.
В приложении 13-14 показан пример вызова способа переходника повторителя map
, который принимает замыкание и вызывает его для каждого элемента по мере повторения элементов. Способ map
возвращает новый повторитель , который создаёт изменённые элементы. Замыкание здесь создаёт новый повторитель , в котором каждый элемент из вектора будет увеличен на 1:
Файл: src/main.rs
--fn main() { - let v1: Vec<i32> = vec![1, 2, 3]; - - v1.iter().map(|x| x + 1); -}
-
Однако этот код выдаёт предупреждение:
-$ cargo run
- Compiling iterators v0.1.0 (file:///projects/iterators)
-warning: unused `Map` that must be used
- --> src/main.rs:4:5
- |
-4 | v1.iter().map(|x| x + 1);
- | ^^^^^^^^^^^^^^^^^^^^^^^^
- |
- = note: iterators are lazy and do nothing unless consumed
- = note: `#[warn(unused_must_use)]` on by default
-help: use `let _ = ...` to ignore the resulting value
- |
-4 | let _ = v1.iter().map(|x| x + 1);
- | +++++++
-
-warning: `iterators` (bin "iterators") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
- Running `target/debug/iterators`
-
-Код в приложении 13-14 ничего не делает; указанное нами замыкание никогда не вызывается. Предупреждение напоминает нам, почему: переходники повторителей ленивы, и здесь нам нужно потребить повторитель .
-Чтобы устранить это предупреждение и потребить повторитель , мы воспользуемся способом collect
, который мы использовали в главе 12 с env::args
в приложении 12-1. Этот способ потребляет повторитель и собирает полученные значения в собрание указанного вида.
В приложении 13-15 мы собираем в вектор итоги перебора повторителя, который возвращается в итоге вызова map
. Этот вектор в итоге будет содержать каждый элемент исходного вектора, увеличенный на 1.
Файл: src/main.rs
--fn main() { - let v1: Vec<i32> = vec![1, 2, 3]; - - let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); - - assert_eq!(v2, vec![2, 3, 4]); -}
-
Поскольку map
принимает замыкание, мы можем указать любую действие, которую хотим выполнить над каждым элементом. Это отличный пример того, как замыкания позволяют задавать желаемое поведение, используя при этом особенности повторения, которые обеспечивает особенность Iterator
.
Вы можете выстроить цепочку из нескольких вызовов переходников повторителя для выполнения сложных действий в удобочитаемом виде. Но поскольку все повторители являются "ленивыми", для получения итогов вызовов переходников повторителя необходимо вызвать один из способов потребляющего переходника.
-Многие переходники повторителей принимают замыкания в качестве переменных, и обычно замыкания, которые мы будем указывать в качестве переменных переходникам повторителей, это замыкания, которые определяют (захватывают) своё окружение.
-В этом примере мы будем использовать способ filter
, который принимает замыкание. Замыкание получает элемент из повторителя и возвращает bool
. Если замыкание возвращает true
, значение будет включено в повторение, создаваемую filter
. Если замыкание возвращает false
, значение не будет включено.
В приложении 13-16 мы используем filter
с замыканием, которое захватывает переменную shoe_size
из своего окружения для повторения по собрания образцов устройства Shoe
. Он будет возвращать обувь только указанного размера.
Файл: src/lib.rs
-#[derive(PartialEq, Debug)]
-struct Shoe {
- size: u32,
- style: String,
-}
-
-fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
- shoes.into_iter().filter(|s| s.size == shoe_size).collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn filters_by_size() {
- let shoes = vec![
- Shoe {
- size: 10,
- style: String::from("sneaker"),
- },
- Shoe {
- size: 13,
- style: String::from("sandal"),
- },
- Shoe {
- size: 10,
- style: String::from("boot"),
- },
- ];
-
- let in_my_size = shoes_in_size(shoes, 10);
-
- assert_eq!(
- in_my_size,
- vec![
- Shoe {
- size: 10,
- style: String::from("sneaker")
- },
- Shoe {
- size: 10,
- style: String::from("boot")
- },
- ]
- );
- }
-}
--
Функция shoes_in_size
принимает в качестве свойств вектор с образцами обуви и размер обуви, а возвращает вектор, содержащий только обувь указанного размера.
В теле shoes_in_my_size
мы вызываем into_iter
чтобы создать повторитель , который становится владельцем вектора. Затем мы вызываем filter
, чтобы превратить этот повторитель в другой, который содержит только элементы, для которых замыкание возвращает true
.
Замыкание захватывает свойство shoe_size
из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect
собирает значения, возвращаемые приспособленным повторителем, в вектор, возвращаемый функцией.
Проверка показывает, что когда мы вызываем shoes_in_my_size
, мы возвращаем только туфли, размер которых совпадает с указанным нами значением.
Вооружившись полученными знаниями об повторителях, мы можем улучшить выполнение работы с вводом/выводом в деле главы 12, применяя повторители для того, чтобы сделать некоторые места в коде более понятными и краткими. Давайте рассмотрим, как повторители могут улучшить нашу выполнение функции Config::build
и функции search
.
clone
, используем повторительВ приложении 12-6 мы добавили код, который принимает срез значений String
и создаёт образец устройства Config
путём упорядочевания среза и клонирования значений, позволяя устройстве Config
владеть этими значениями. В приложении 13-17 мы воспроизвели выполнение функции Config::build
, как это было в приложении 12-23:
Файл: src/lib.rs
-use std::env;
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- let ignore_case = env::var("IGNORE_CASE").is_ok();
-
- Ok(Config {
- query,
- file_path,
- ignore_case,
- })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Ранее мы говорили, что не стоит беспокоиться о неэффективных вызовах clone
, потому что мы удалим их в будущем. Ну что же, время пришло!
Нам понадобился здесь clone
, потому что в свойстве args
у нас срез с элементами String
, но функция build
не владеет args
. Чтобы образец Config
владел значениями, нам пришлось клонировать их из args
в переменные query
и file_path
.
Благодаря нашим новым знаниям об повторителях мы можем изменить функцию build
, чтобы вместо заимствования среза она принимала в качестве переменной повторитель . Мы будем использовать возможность повторителя вместо кода, который проверяет длину среза и обращается по порядковому указателю к определённым значениям. Это позволит лучше понять, что делает функция Config::build
, поскольку повторитель будет обращаться к значениям.
Как только Config::build
получит в своё распоряжение повторитель и перестанет использовать действия упорядочевания с заимствованием, мы сможем переместить значения String
из повторителя в Config
вместо того, чтобы вызывать clone
и создавать новое выделение памяти.
Откройте файл src/main.rs дела ввода-вывода, который должен выглядеть следующим образом:
-Файл: src/main.rs
-use std::env;
-use std::process;
-
-use minigrep::Config;
-
-fn main() {
- let args: Vec<String> = env::args().collect();
-
- let config = Config::build(&args).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- // --snip--
-
- if let Err(e) = minigrep::run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
-}
-Сначала мы изменим начало функции main
, которая была в приложении 12-24, на код в приложении 13-18, который теперь использует повторитель . Это не будет собираться, пока мы не обновим Config::build
.
Файл: src/main.rs
-use std::env;
-use std::process;
-
-use minigrep::Config;
-
-fn main() {
- let config = Config::build(env::args()).unwrap_or_else(|err| {
- eprintln!("Problem parsing arguments: {err}");
- process::exit(1);
- });
-
- // --snip--
-
- if let Err(e) = minigrep::run(config) {
- eprintln!("Application error: {e}");
- process::exit(1);
- }
-}
--
Функция env::args
возвращает повторитель ! Вместо того чтобы собирать значения повторителя в вектор и затем передавать срез в Config::build
, теперь мы передаём владение повторителем, возвращённым из env::args
в Config::build
напрямую.
Далее нам нужно обновить определение Config::build
. В файле src/lib.rs вашего дела ввода-вывода изменим ярлык Config::build
так, чтобы она выглядела как в приложении 13-19. Это все ещё не собирается, потому что нам нужно обновить тело функции.
Файл: src/lib.rs
-use std::env;
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(
- mut args: impl Iterator<Item = String>,
- ) -> Result<Config, &'static str> {
- // --snip--
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- let ignore_case = env::var("IGNORE_CASE").is_ok();
-
- Ok(Config {
- query,
- file_path,
- ignore_case,
- })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Документация встроенной библиотеки для функции env::args
показывает, что вид возвращаемого ею повторителя - std::env::Args
, и этот вид выполняет признак Iterator
и возвращает значения String
.
Мы обновили ярлык функции Config::build
, чтобы свойство args
имел гибкий вид ограниченный особенностью impl Iterator<Item = String>
вместо &[String]
. Такое использование правил написания impl Trait
, который мы обсуждали в разделе " Особенности как свойства" главы 10, означает, что args
может быть любым видом, выполняющим вид Iterator
и возвращающим элементы String
.
Поскольку мы владеем args
и будем изменять args
в этапе повторения над ним, мы можем добавить ключевое слово mut
в свод требований свойства args
, чтобы сделать его изменяемым.
Iterator
вместо порядковых указателейДалее мы подправим содержимое Config::build
. Поскольку args
выполняет признак Iterator
, мы знаем, что можем вызвать у него способ next
! В приложении 13-20 код из приложения 12-23 обновлён для использования способа next
:
Файл: src/lib.rs
-use std::env;
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(
- mut args: impl Iterator<Item = String>,
- ) -> Result<Config, &'static str> {
- 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,
- })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-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_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Помните, что первое значение в возвращаемых данных env::args
- это имя программы. Мы хотим пренебрегать его и перейти к следующему значению, поэтому сперва мы вызываем next
и ничего не делаем с возвращаемым значением. Затем мы вызываем next
, чтобы получить значение, которое мы хотим поместить в поле query
в Config
. Если next
возвращает Some
, мы используем match
для извлечения значения. Если возвращается None
, это означает, что было задано недостаточно переменных, и мы досрочно возвращаем значение Err
. То же самое мы делаем для значения file_path
.
Мы также можем воспользоваться преимуществами повторителей в функции search
в нашем деле с действиеми ввода-вывода, которая воспроизведена здесь в приложении 13-21 так же, как и в приложении 12-19:
Файл: src/lib.rs
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
-}
-
-impl Config {
- pub fn build(args: &[String]) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("not enough arguments");
- }
-
- let query = args[1].clone();
- let file_path = args[2].clone();
-
- Ok(Config { query, file_path })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- Ok(())
-}
-
-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
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn one_result() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-}
--
Мы можем написать этот код в более сжатом виде, используя способы переходника повторителя. Это также позволит нам избежать наличия изменяемого временного вектора results
. Функциональный исполнение программирования предпочитает уменьшить количество изменяемого состояния, чтобы сделать код более понятным. Удаление изменяемого состояния может позволить в будущем сделать поиск одновременным, поскольку нам не придётся управлять одновременным доступом к вектору results
. В приложении 13-22 показано это изменение:
Файл: src/lib.rs
-use std::env;
-use std::error::Error;
-use std::fs;
-
-pub struct Config {
- pub query: String,
- pub file_path: String,
- pub ignore_case: bool,
-}
-
-impl Config {
- pub fn build(
- mut args: impl Iterator<Item = String>,
- ) -> Result<Config, &'static str> {
- 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,
- })
- }
-}
-
-pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let contents = fs::read_to_string(config.file_path)?;
-
- let results = if config.ignore_case {
- search_case_insensitive(&config.query, &contents)
- } else {
- search(&config.query, &contents)
- };
-
- for line in results {
- println!("{line}");
- }
-
- Ok(())
-}
-
-pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
- contents
- .lines()
- .filter(|line| line.contains(query))
- .collect()
-}
-
-pub fn search_case_insensitive<'a>(
- query: &str,
- contents: &'a str,
-) -> Vec<&'a str> {
- let query = query.to_lowercase();
- let mut results = Vec::new();
-
- for line in contents.lines() {
- if line.to_lowercase().contains(&query) {
- results.push(line);
- }
- }
-
- results
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn case_sensitive() {
- let query = "duct";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Duct tape.";
-
- assert_eq!(vec!["safe, fast, productive."], search(query, contents));
- }
-
- #[test]
- fn case_insensitive() {
- let query = "rUsT";
- let contents = "\
-Rust:
-safe, fast, productive.
-Pick three.
-Trust me.";
-
- assert_eq!(
- vec!["Rust:", "Trust me."],
- search_case_insensitive(query, contents)
- );
- }
-}
--
Напомним, что назначение функции search
- вернуть все строки в contents
, которые содержат query
. Подобно примеру filter
в приложении 13-16, этот код использует переходник filter
, чтобы сохранить только те строки, для которых line.contains(query)
возвращает true
. Затем мы собираем совпадающие строки в другой вектор с помощью collect
. Так гораздо проще! Не стесняйтесь сделать такое же изменение для использования способов повторителя в функции search_case_insensitive
.
Следующий логичный вопрос - какой исполнение вы должны выбрать в своём коде и почему: подлинную выполнение в приложении 13-21 или исполнение с использованием повторителей в приложении 13-22. Большинство программистов на языке Ржавчина предпочитают использовать исполнение повторителей. Сначала разобраться с ним немного сложно, но как только вы почувствуете, что такое различные переходники повторителей и что они делают, понять повторители станет проще. Вместо того чтобы возиться с различными элементами цикла и создавать новые векторы, код сосредотачивается на высокоуровневой цели цикла. Это абстрагирует часть обычного кода, поэтому легче увидеть подходы, единственные для этого кода, такие как условие выборки, которое должен пройти каждый элемент в повторителе.
-Но действительно ли эти две выполнения эквивалентны? Интуитивно можно предположить, что более низкоуровневый цикл будет быстрее. Давайте поговорим о производительности.
- -Чтобы определить, что лучше использовать циклы или повторители, нужно знать, какая выполнение быстрее: исполнение функции search
с явным циклом for
или исполнение с повторителями.
Мы выполнили проверка производительности, разместив всё содержимое книги (“The Adventures of Sherlock Holmes” by Sir Arthur Conan Doyle) в строку вида String
и поискали слово the в её содержимом. Вот итоги проверки функции search
с использованием цикла for
и с использованием повторителей:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
-test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
-
-Исполнение с использованием повторителей была немного быстрее! Мы не будем приводить здесь непосредственно код проверки, поскольку мысль не в том, чтобы доказать, что решения в точности эквивалентны, а в том, чтобы получить общее представление о том, как эти две выполнения близки по производительности.
-Для более исчерпывающего проверки, вам нужно проверить различные тексты разных размеров в качестве содержимого для contents
, разные слова и слова различной длины в качестве query
и всевозможные другие исходы. Дело в том, что повторители, будучи высокоуровневой абстракцией, собираются примерно в тот же код, как если бы вы написали его низкоуровневый исход самостоятельно. Повторители - это одна из абстракций с нулевой стоимостью ( zero-cost abstractions ) в Rust, под которой мы подразумеваем, что использование абстракции не накладывает дополнительных расходов во время выполнения. Подобно тому, как Бьёрн Страуструп, внешнем видер и разработчик C++, определяет нулевые накладные расходы ( zero-overhead ) в книге “Foundations of C++” (2012):
--В целом, выполнение C++ подчиняется принципу отсутствия накладных расходов: за то, чем вы не пользуетесь, платить не нужно. И далее: тот код, что вы используете, нельзя сделать ещё лучше.
-
В качестве другого примера приведём код, взятый из аудио декодера. Алгоритм декодирования использует математическую действие линейного предсказания для оценки будущих значений на основе линейной функции предыдущих выборок. Код использует соединение вызовов повторителя для выполнения математических вычислений для трёх переменных в области видимости: срез данных buffer
, массив из 12 коэффициентов coefficients
и число для сдвига данных в переменной qlp_shift
. Переменные определены в примере, но не имеют начальных значений. Хотя этот код не имеет большого значения вне среды, он является кратким, существующим примером того, как Ржавчина переводит мысли высокого уровня в код низкого уровня.
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;
-}
-Чтобы вычислить значение переменной prediction
, этот код перебирает каждое из 12 значений в переменной coefficients
и использует способ zip
для объединения значений коэффициентов с предыдущими 12 значениями в переменной buffer
. Затем, для каждой пары мы перемножаем значения, суммируем все итоги и у суммы сдвигаем биты вправо в переменную qlp_shift
.
Для вычислений в таких приложениях, как аудио декодеры, часто требуется производительность. Здесь мы создаём повторитель , используя два переходника, впоследствии потребляющих значение. В какой ассемблерный код будет собираться этот код на Rust? На мгновение написания этой главы он собирается в то же самое, что вы написали бы руками. Не существует цикла, соответствующего повторения по значениям в «коэффициентах»coefficients
: Ржавчина знает, что существует двенадцать повторений, поэтому он «разворачивает» цикл. Разворачивание - это улучшение, которая устраняет издержки кода управления циклом и вместо этого порождает повторяющийся код для каждой повторения цикла.
Все коэффициенты сохраняются в регистрах, что означает очень быстрый доступ к значениям. Нет никаких проверок границ доступа к массиву во время выполнения. Все эти переработки, которые может применить Rust, делают полученный код чрезвычайно эффективным. Теперь, когда вы это знаете, используйте повторители и замыкания без страха! Они представляют код в более высокоуровневом виде, но без потери производительности во время выполнения.
-Замыкания (closures) и повторители (iterators) это возможности Rust, вдохновлённые мыслями полезных языков. Они позволяют Ржавчина ясно выражать мысли высокого уровня с производительностью низкоуровневого кода. Выполнения замыканий и повторителей таковы, что нет влияния на производительность выполнения кода. Это одна из целей Rust, направленных на обеспечение абстракций с нулевой стоимостью (zero-cost abstractions).
-Теперь, когда мы улучшили представление кода в нашем деле, рассмотрим некоторые возможности, которые нам предоставляет cargo
для обнародования нашего кода в хранилища.
До сих пор мы использовали только самые основные возможности Cargo для сборки, запуска и проверки нашего кода, но он может гораздо больше. В этой главе мы обсудим некоторые другие, более продвинутые возможности, чтобы показать вам, как делать следующее:
-Cargo может делать значительно больше того, что мы рассмотрим в этой главе, полное описание всех его функций см. в документации.
- -В Ржавчина профили выпуска — это предопределённые и настраиваемые профили с различными настройками, которые позволяют программисту лучше управлять различные свойства сборки кода. Каждый профиль настраивается независимо от других.
-Cargo имеет два основных профиля: профиль dev
, используемый Cargo при запуске cargo build
, и профиль release
, используемый Cargo при запуске cargo build --release
. Профиль dev
определён со значениями по умолчанию для разработки, а профиль release
имеет значения по умолчанию для сборок в исполнение.
Эти имена профилей могут быть знакомы по итогам ваших сборок:
- -$ cargo build
- Finished dev [unoptimized + debuginfo] target(s) in 0.0s
-$ cargo build --release
- Finished release [optimized] target(s) in 0.0s
-
-dev
и release
— это разные профили, используемые сборщиком.
Cargo содержит настройки по умолчанию для каждого профиля, которые применяются, если вы явно не указали разделы [profile.*]
в файле дела Cargo.toml. Добавляя разделы [profile.*]
для любого профиля, который вы хотите настроить, вы переопределяете любое подмножество свойств по умолчанию. Например, вот значения по умолчанию для свойства opt-level
для профилей dev
и release
:
Файл: Cargo.toml
-[profile.dev]
-opt-level = 0
-
-[profile.release]
-opt-level = 3
-
-Свойство opt-level
управляет количеством переработок, которые Ржавчина будет применять к вашему коду, в ряде от 0 до 3. Использование большего количества переработок увеличивает время сборки, поэтому если вы находитесь в этапе разработки и часто собираете свой код, целесообразно использовать меньшее количество переработок, чтобы сборка происходила быстрее, даже если в итоге код будет работать медленнее. Поэтому opt-level
по умолчанию для dev
установлен в 0
. Когда вы готовы обнародовать свой код, то лучше потратить больше времени на сборку. Вы собираете программу в режиме исполнения только один раз, но выполняться она будет многократно, так что использование режима исполнения позволяет увеличить скорость выполнения кода за счёт времени сборки. Вот почему по умолчанию opt-level
для профиля release
равен 3
.
Вы можете переопределить настройки по умолчанию, добавив другое значение для них в Cargo.toml. Например, если мы хотим использовать уровень переработки 1 в профиле разработки, мы можем добавить эти две строки в файл Cargo.toml нашего дела:
-Файл: Cargo.toml
-[profile.dev]
-opt-level = 1
-
-Этот код переопределяет настройку по умолчанию 0
. Теперь, когда мы запустим cargo build
, Cargo будет использовать значения по умолчанию для профиля dev
плюс нашу настройку для opt-level
. Поскольку мы установили для opt-level
значение 1
, Cargo будет применять больше переработок, чем было задано по умолчанию, но не так много, как при сборке исполнения.
Полный список свойств настройке и значений по умолчанию для каждого профиля вы можете найти в документации Cargo.
- -Мы использовали дополнения из crates.io в качестве зависимостей нашего дела, но вы также можете поделиться своим кодом с другими людьми, обнародовав свои собственные дополнения. Реестр библиотек по адресу crates.io распространяет исходный код ваших дополнений, поэтому он в основном размещает код с открытым исходным кодом.
-В Ржавчина и Cargo есть функции, которые облегчают поиск и использование обнародованного дополнения. Далее мы поговорим о некоторых из этих функций, а затем объясним, как обнародовать дополнение.
-Правильноное документирование ваших дополнений поможет другим пользователям знать, как и когда их использовать, поэтому стоит потратить время на написание документации. В главе 3 мы обсуждали, как вносить примечания в код Rust, используя две косые черты, //
. В Ржавчина также есть особый вид примечаниев к документации, который обычно называется примечанием к документации, который порождает документацию HTML. HTML-код отображает содержимое примечаниев к документации для открытых элементов API, предназначенных для программистов, увлеченных в знании того, как использовать вашу библиотеку, в отличие от того, как она выполнена.
Примечания к документации используют три слеша, ///
вместо двух и поддерживают наставление Markdown для изменения текста. Размещайте примечания к документации непосредственно перед элементом, который они документируют. В приложении 14-1 показаны примечания к документации для функции add_one
в библиотеке с именем my_crate
:
Файл: src/lib.rs
-/// 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:
-
Мы использовали Markdown заголовок # Examples
в приложении 14-1 для создания раздела в HTML с заголовком "Examples". Вот некоторые другие разделы, которые авторы библиотек обычно используют в своей документации:
Result
, описание видов ошибок, которые могут произойти и какие условия могут привести к тому, что эти ошибки могут быть возвращены, может быть полезным для вызывающих, так что они могут написать код для обработки различных видов ошибок разными способами.unsafe
для вызова (мы обсуждаем безопасность в главе 19), должен быть раздел, объясняющий, почему функция небезопасна и охватывающий неизменные величины, которые функция ожидает от вызывающих сторон.В подавляющем большинстве случаев примечания к документации не нуждаются во всех этих разделах, но это хорошая подсказка, напоминающая вам о тех особенностях вашего кода, о которых пользователям будет важно узнать.
-Добавление примеров кода в примечания к документации может помочь отобразить, как использовать вашу библиотеку, и это даёт дополнительный бонус: запуск cargo test
запустит примеры кода в вашей документации как проверки! Нет ничего лучше, чем документация с примерами. Но нет ничего хуже, чем примеры, которые не работают, потому что код изменился с особенности написания документации. Если мы запустим cargo test
с документацией для функции add_one
из приложения 14-1, мы увидим раздел итогов проверки, подобный этому:
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
-
-Теперь, если мы изменим либо функцию, либо пример, так что assert_eq!
в примере паникует, и снова запустим cargo test
, мы увидим, что проверки документации обнаруживают, что пример и код не согласованы друг с другом!
Исполнение примечаниев к документам //!
добавляет документацию к элементу, содержащему примечания, а не к элементам, следующим за примечаниями. Обычно мы используем эти примечания внутри корневого файла ящика (по соглашению src/lib.rs ) или внутри звена для документирования ящика или звена в целом.
Например, чтобы добавить документацию, описывающую назначение my_crate
, содержащего функцию add_one
, мы добавляем примечания к документации, начинающиеся с //!
в начало файла src/lib.rs , как показано в приложении 14-2:
Файл: src/lib.rs
-//! # My Crate
-//!
-//! `my_crate` is a collection of utilities to make performing certain
-//! calculations more convenient.
-
-/// Adds one to the number given.
-// --snip--
-///
-/// # Examples
-///
-/// ```
-/// let arg = 5;
-/// let answer = my_crate::add_one(arg);
-///
-/// assert_eq!(6, answer);
-/// ```
-pub fn add_one(x: i32) -> i32 {
- x + 1
-}
--
Обратите внимание, что после последней строки, начинающейся с //!
, нет никакого кода. Поскольку мы начали примечания с //!
вместо ///
, мы документируем элемент, который содержит этот примечание, а не элемент, который следует за этим примечанием. В данном случае таким элементом является файл src/lib.rs, который является корнем crate. Эти примечания описывают весь ящик.
Когда мы запускаем cargo doc --open
, эти примечания будут отображаться на первой странице документации для my_crate
над списком открытых элементов в библиотеке, как показано на рисунке 14-2:
-
Примечания к документации внутри элементов полезны для описания ящиков и звеньев особенно. Используйте их, чтобы объяснить общую цель дополнения, чтобы помочь вашим пользователям понять устройство ящика.
-pub use
Устройства вашего открытого API является основным обстоятельством при обнародования ящика. Люди, которые используют вашу библиотеку, менее знакомы со устройством, чем вы и могут столкнуться с трудностями при поиске частей, которые они хотят использовать, если ваша библиотека имеет большую упорядочевание звеньев.
-В главе 7 мы рассмотрели, как сделать элементы общедоступными с помощью ключевого слова pub
и ввести элементы в область видимости с помощью ключевого слова use
. Однако устройства, которая имеет смысл для вас при разработке ящика, может быть не очень удобной для пользователей. Вы можете согласовать устройство в виде упорядочевания с несколькими уровнями, но тогда люди, желающие использовать вид, который вы определили в глубине упорядочевания, могут столкнуться с неполадкой его поиска. Их также может раздражать необходимость вводить use
my_crate::some_module::another_module::UsefulType;
вместо use
my_crate::UsefulType;
.
Хорошей новостью является то, что если устройства не удобна для использования другими из другой библиотеки, вам не нужно перестраивать внутреннюю устройство: вместо этого вы можете реэкспортировать элементы, чтобы сделать открытую устройство, отличную от вашей внутренней устройства, используя pub use
. Реэкспорт берет открытый элемент в одном месте и делает его открытым в другом месте, как если бы он был определён в другом месте.
Например, скажем, мы создали библиотеку с именем art
для расчетов художественных подходов. Внутри этой библиотеки есть два звена: звено kinds
содержащий два перечисления с именами PrimaryColor
и SecondaryColor
и звено utils
, содержащий функцию с именем mix
, как показано в приложении 14-3:
Файл: src/lib.rs
-//! # Art
-//!
-//! A library for modeling artistic concepts.
-
-pub mod kinds {
- /// The primary colors according to the RYB color model.
- pub enum PrimaryColor {
- Red,
- Yellow,
- Blue,
- }
-
- /// The secondary colors according to the RYB color model.
- pub enum SecondaryColor {
- Orange,
- Green,
- Purple,
- }
-}
-
-pub mod utils {
- use crate::kinds::*;
-
- /// Combines two primary colors in equal amounts to create
- /// a secondary color.
- pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
- // --snip--
- unimplemented!();
- }
-}
--
На рисунке 14-3 показано, как будет выглядеть титульная страница документации для этого ящика, созданный cargo doc
:
-
Обратите внимание, что виды PrimaryColor
и SecondaryColor
не указаны на главной странице, равно как и функция mix
. Мы должны нажать kinds
и utils
, чтобы увидеть их.
В другой библиотеке, которая зависит от этой библиотеки, потребуются операторы use
, которые подключают элементы из art
в область видимости, определяя устройство звена, которая определена в данный мгновение. В приложении 14-4 показан пример ящика, в котором используются элементы PrimaryColor
и mix
из ящика art
:
Файл: src/main.rs
-use art::kinds::PrimaryColor;
-use art::utils::mix;
-
-fn main() {
- let red = PrimaryColor::Red;
- let yellow = PrimaryColor::Yellow;
- mix(red, yellow);
-}
--
Автору кода в приложении 14-4, который использует ящик art
, пришлось выяснить, что PrimaryColor
находится в звене kinds
, а mix
- в звене utils
. Устройства звена art
ящика больше подходит для разработчиков, работающих над art
ящиком, чем для тех, кто его использует. Внутренняя устройства не содержит никакой полезной сведений для того, кто пытается понять, как использовать ящик art
, а скорее вызывает путаницу, поскольку разработчики, использующие его, должны понять, где искать, и должны указывать имена звеньев в выражениях use
.
Чтобы удалить внутреннюю устройство из общедоступного API, мы можем изменить код ящика art
в приложении 14-3, чтобы добавить операторы pub use
для повторного реэкспорта элементов на верхнем уровне, как показано в приложении 14-5:
Файл: src/lib.rs
-//! # Art
-//!
-//! A library for modeling artistic concepts.
-
-pub use self::kinds::PrimaryColor;
-pub use self::kinds::SecondaryColor;
-pub use self::utils::mix;
-
-pub mod kinds {
- // --snip--
- /// The primary colors according to the RYB color model.
- pub enum PrimaryColor {
- Red,
- Yellow,
- Blue,
- }
-
- /// The secondary colors according to the RYB color model.
- pub enum SecondaryColor {
- Orange,
- Green,
- Purple,
- }
-}
-
-pub mod utils {
- // --snip--
- use crate::kinds::*;
-
- /// Combines two primary colors in equal amounts to create
- /// a secondary color.
- pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
- SecondaryColor::Orange
- }
-}
--
Документация API, которую cargo doc
порождает для этой библиотеки, теперь будет перечислять и связывать реэкспорты на главной странице, как показано на рисунке 14-4, упрощая поиск видов PrimaryColor
, SecondaryColor
и функции mix
.
-
Пользователи ящика art
могут по-прежнему видеть и использовать внутреннюю устройство из приложения 14-3, как показано в приложении 14-4, или они могут использовать более удобную устройство в приложении 14-5, как показано в приложении 14-6:
Файл: src/main.rs
-use art::mix;
-use art::PrimaryColor;
-
-fn main() {
- // --snip--
- let red = PrimaryColor::Red;
- let yellow = PrimaryColor::Yellow;
- mix(red, yellow);
-}
--
В случаях, когда имеется много вложенных звеньев, реэкспорт видов на верхнем уровне с помощью pub use
может существенно повысить удобство работы для людей, использующих ящик. Ещё одно распространённое использование pub use
- это реэкспорт определений зависимого звена в текущем ящике, чтобы сделать определения этого ящика частью открытого API вашего ящика.
Создание полезной открытой устройства API - это больше искусство чем наука, и вы можете повторять, чтобы найти API, который лучше всего подойдёт вашим пользователям. Использование pub use
даёт вам гибкость в том, как вы внутренне выстраиваете
свою библиотеку внутри и отделяете эту внутреннюю устройство от того, что вы предоставляете пользователям. Посмотрите на код некоторых установленных ящиков, чтобы увидеть отличается ли их внутренняя устройства от их открытого API.
-Прежде чем вы сможете обнародовать любые библиотеки, вам необходимо создать учётную запись на crates.io и получить API токен. Для этого зайдите на домашнюю страницу crates.io и войдите в систему через учётную запись GitHub. (В настоящее время требуется наличие учётной записи GitHub, но сайт может поддерживать другие способы создания учётной записи в будущем.) Сразу после входа в систему перейдите в настройки своей учётной записи по адресу https://crates.io/me/ и получите свой ключ API. Затем выполните приказ cargo login
с вашим ключом API, например:
$ cargo login abcdefghijklmnopqrstuvwxyz012345
-
-Этот приказ сообщит Cargo о вашем API token и сохранит его местно в ~/.cargo/credentials. Обратите внимание, что этот токен является тайным: не делитесь им ни с кем другим. Если вы по какой-либо причине поделитесь им с кем-либо, вы должны отозвать его и создать новый токен на crates.io.
-Допустим, у вас есть ящик, который вы хотите обнародовать. Перед обнародованием вам нужно добавить некоторые метаданные в раздел [package]
файла Cargo.toml ящика.
Вашему ящику понадобится не повторяющееся имя. Пока вы работаете над ящиком местно, вы можете назвать его как угодно. Однако названия ящиков на crates.io определятся в мгновение первой обнародования. Как только ящику присвоено название, никто другой не сможет обнародовать ящик с таким же именем. Перед тем как обнародовать ящик, поищите название, которое вы хотите использовать. Если такое имя уже используется, вам придётся подобрать другое и отредактировать поле name
в файле Cargo.toml в разделе [package]
, чтобы использовать новое имя в качестве размещаяемого, например, так:
Файл: Cargo.toml
-[package]
-name = "guessing_game"
-
-Даже если вы выбрали не повторяющееся имя, когда вы запустите cargo publish
чтобы обнародовать ящик, вы получите предупреждение, а затем ошибку:
$ cargo publish
- Updating crates.io index
-warning: manifest has no description, license, license-file, documentation, homepage or repository.
-See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
---snip--
-error: failed to publish to registry at https://crates.io
-
-Caused by:
- the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata
-
-Это ошибка, потому что вам не хватает важной сведений: необходимы описание и лицензия, чтобы люди знали, что делает ваш ящик и на каких условиях они могут его использовать. В поле Cargo.toml добавьте описание, состоящее из одного-двух предложений, поскольку оно будет появляться вместе с вашим ящиком в итогах поиска. Для поля license
нужно указать значение определителя лицензии. В Linux Foundation's Software Package Data Exchange (SPDX) перечислены определители, которые можно использовать для этого значения. Например, чтобы указать, что вы лицензировали свой crate, используя лицензию MIT, добавьте определитель MIT
:
Файл: Cargo.toml
-[package]
-name = "guessing_game"
-license = "MIT"
-
-Если вы хотите использовать лицензию, которая отсутствует в SPDX, вам нужно поместить текст этой лицензии в файл, включите файл в свой дело, а затем используйте license-file
, чтобы указать имя этого файла вместо использования ключа license
.
Руководство по выбору лицензии для вашего дела выходит за рамки этой книги. Многие люди в сообществе Ржавчина лицензируют свои дела так же, как и Rust, используя двойную лицензию MIT OR Apache 2.0
. Эта применение отображает, что вы также можете указать несколько определителей лицензий, разделённых OR
, чтобы иметь несколько лицензий для вашего дела.
С добавлением единственного имени, исполнения, вашего описания и лицензии, файл Cargo.toml для дела, который готов к обнародования может выглядеть следующим образом:
-Файл: Cargo.toml
-[package]
-name = "guessing_game"
-version = "0.1.0"
-edition = "2021"
-description = "A fun game where you guess what number the computer has chosen."
-license = "MIT OR Apache-2.0"
-
-[dependencies]
-
-Документация Cargo описывает другие метаданные, которые вы можете указать, чтобы другие могли легче находить и использовать ваш ящик.
-Теперь, когда вы создали учётную запись, сохранили свой токен API, выбрали имя для своего ящика и указали необходимые метаданные, вы готовы к обнародования! Обнародование библиотеки загружает определённую исполнение в crates.io для использования другими.
-Будьте осторожны, потому что обнародование является перманентной действием. Исполнение никогда не сможет быть перезаписана, а код не подлежит удалению. Одна из основных целей crates.io - служить постоянным архивом кода, чтобы сборки всех дел, зависящих от crates из crates.io продолжали работать. Предоставление возможности удаления исполнений сделало бы выполнение этой цели невозможным. При этом количество исполнений ящиков, которые вы можете обнародовать, не ограничено.
-Запустите приказ cargo publish
ещё раз. Сейчас эта приказ должна выполниться успешно:
$ cargo publish
- Updating crates.io index
- Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
- Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
- Compiling guessing_game v0.1.0
-(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
- Finished dev [unoptimized + debuginfo] target(s) in 0.19s
- Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
-
-Поздравляем! Теперь вы поделились своим кодом с сообществом Ржавчина и любой может легко добавить вашу библиотеку в качестве зависимости их дела.
-Когда вы внесли изменения в свой ящик и готовы выпустить новую исполнение, измените значение version
, указанное в вашем файле Cargo.toml и повторите размещение. Воспользуйтесь Semantic Versioning rules, чтобы решить, какой номер следующей исполнения подходит для ваших изменений. Затем запустите cargo publish
, чтобы загрузить новую исполнение.
cargo yank
Хотя вы не можете удалить предыдущие исполнения ящика, вы можете помешать любым будущим делам добавлять его в качестве новой зависимости. Это полезно, когда исполнение ящика сломана по той или иной причине. В таких случаейх Cargo поддерживает выламывание (yanking) исполнения ящика.
-Вычёркивание исполнения не позволяет новым делам зависеть от этой исполнения, но при этом позволяет всем существующим делам, зависящим от неё, продолжать работу. По сути, исключение означает, что все дела с Cargo.lock не сломаются, а любые файлы Cargo.lock, которые будут порождаться в будущем, не смогут использовать исключённую исполнение.
-Чтобы вычеркнуть исполнение ящика, в папки ящика, который вы обнародовали ранее, выполните приказ cargo yank
и укажите, какую исполнение вы хотите вычеркнуть. Например, если мы обнародовали ящик под названием guessing_game
исполнения 1.0.1 и хотим вычеркнуть её, в папке дела для guessing_game
мы выполним:
$ cargo yank --vers 1.0.1
- Updating crates.io index
- Yank guessing_game@1.0.1
-
-Добавив в приказ --undo
, вы также можете отменить выламывание и разрешить делам начать зависеть от исполнения снова:
$ cargo yank --vers 1.0.1 --undo
- Updating crates.io index
- Unyank guessing_game@1.0.1
-
-Вычёркивание не удаляет код. Оно не может, например, удалить случайно загруженные пароли. Если это произойдёт, вы должны немедленно сбросить эти пароли.
- -В главе 12 мы создали дополнение, который включал в себя двоичный и библиотечный ящики. По мере развития вашего дела может возникнуть случаей, когда библиотечный ящик будет становиться все больше, и вы захотите разделить ваш дополнение на несколько библиотечных ящиков. Cargo предоставляет возможность под названием workspaces, которая помогает управлять несколькими взаимосвязанными дополнениями, которые разрабатываются в тандеме.
-Workspace - это набор дополнений, которые используют один и тот же Cargo.lock и папку для хранения итогов сборки. Давайте создадим дело с использованием workspace - мы будем использовать обыкновенный код, чтобы сосредоточиться на устройстве рабочего пространства. Существует несколько способов внутренне выстроить
-рабочую область, но мы покажем только один из них. У нас будет рабочая область, содержащая двоичный файл и две библиотеки. Двоичный файл, который обеспечивает основную возможность, будет зависеть от двух библиотек. Одна библиотека предоставит функцию add_one
, а вторая - add_two
. Эти три ящика будут частью одного workspace. Начнём с создания папки для рабочего окружения:
$ mkdir add
-$ cd add
-
-Далее в папке add мы создадим файл Cargo.toml, который будет определять настройку всего рабочего окружения. В этом файле не будет разделы [package]
. Вместо этого он будет начинаться с разделы [workspace]
, которая позволит нам добавить звенья в рабочее пространство, указав путь к дополнению с нашим двоичным ящиком; в данном случае этот путь - adder:
Файл: Cargo.toml
-[workspace]
-
-members = [
- "adder",
-]
-
-Затем мы создадим исполняемый ящик adder
, запустив приказ cargo new
в папке add:
$ cargo new adder
- Created binary (application) `adder` package
-
-На этом этапе мы можем создать рабочее пространство, запустив приказ cargo build
. Файлы в папке add должны выглядеть следующим образом:
├── Cargo.lock
-├── Cargo.toml
-├── adder
-│ ├── Cargo.toml
-│ └── src
-│ └── main.rs
-└── target
-
-Рабочая область содержит на верхнем уровне один папка target, в который будут помещены собранные артефакты; дополнение adder
не имеет собственного папки target. Даже если мы запустим cargo build
из папки adder, собранные артефакты все равно окажутся в add/target, а не в add/adder/target. Cargo так определил папку target в рабочем пространстве, потому что ящики в рабочем пространстве должны зависеть друг от друга. Если бы каждый ящик имел свой собственный папка target, каждому ящику пришлось бы пересобирать каждый из других ящиков в рабочем пространстве, чтобы поместить артефакты в свой собственный папка target. Благодаря совместному использованию единого папки target ящики могут избежать ненужной пересборки.
Далее давайте создадим ещё одного участника дополнения в рабочей области и назовём его add_one
. Внесите изменения в Cargo.toml верхнего уровня так, чтобы указать путь add_one в списке members
:
Файл: Cargo.toml
-[workspace]
-
-members = [
- "adder",
- "add_one",
-]
-
-Затем создайте новый ящик библиотеки с именем add_one
:
$ cargo new add_one --lib
- Created library `add_one` package
-
-Ваш папка add должен теперь иметь следующие папки и файлы:
-├── Cargo.lock
-├── Cargo.toml
-├── add_one
-│ ├── Cargo.toml
-│ └── src
-│ └── lib.rs
-├── adder
-│ ├── Cargo.toml
-│ └── src
-│ └── main.rs
-└── target
-
-В файле add_one/src/lib.rs добавим функцию add_one
:
Файл: add_one/src/lib.rs
-pub fn add_one(x: i32) -> i32 {
- x + 1
-}
-Теперь мы можем сделать так, чтобы дополнение adder
с нашим исполняемым файлом зависел от дополнения add_one
, содержащего нашу библиотеку. Сначала нам нужно добавить зависимость пути от add_one
в adder/Cargo.toml.
Файл: adder/Cargo.toml
-[dependencies]
-add_one = { path = "../add_one" }
-
-Cargo не исходит из того, что ящики в рабочем пространстве могут зависеть друг от друга, поэтому нам необходимо явно указать отношения зависимости.
-Далее, давайте используем функцию add_one
(из ящика add_one
) в ящике adder
. Откройте файл adder/src/main.rs и добавьте строку use
в верхней части, чтобы ввести в область видимости новый библиотечный ящик add_one
. Затем измените функцию main
для вызова функции add_one
, как показано в приложении 14-7.
Файл: adder/src/main.rs
-use add_one;
-
-fn main() {
- let num = 10;
- println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
-}
--
Давайте соберём рабочее пространство, запустив приказ cargo build
в папке верхнего уровня add!
$ cargo build
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished dev [unoptimized + debuginfo] target(s) in 0.68s
-
-Чтобы запустить двоичный ящик из папки add, нам нужно указать какой дополнение из рабочей области мы хотим использовать с помощью переменной -p
и названия дополнения в приказу cargo run
:
$ cargo run -p adder
- Finished dev [unoptimized + debuginfo] target(s) in 0.0s
- Running `target/debug/adder`
-Hello, world! 10 plus one is 11!
-
-Запуск кода из adder/src/main.rs, который зависит от add_one
.
Обратите внимание, что рабочая область имеет один единственный файл Cargo.lock на верхнем уровне, а не содержит Cargo.lock в папке каждого ящика. Это заверяет, что все ящики используют одну и ту же исполнение всех зависимостей. Если мы добавим дополнение rand
в файлы adder/Cargo.toml и add_one/Cargo.toml, Cargo сведёт их оба к одной исполнения rand
и запишет её в один Cargo.lock. Если заставить все ящики в рабочей области использовать одни и те же зависимости, то это будет означать, что ящики всегда будут совместимы друг с другом. Давайте добавим ящик rand
в раздел [dependencies]
в файле add_one/Cargo.toml, чтобы мы могли использовать ящик rand
в ящике add_one
:
Файл: add_one/Cargo.toml
-[dependencies]
-rand = "0.8.5"
-
-Теперь мы можем добавить use rand;
в файл add_one/src/lib.rs и сделать сборку рабочего пространства, запустив cargo build
в папке add, что загрузит и собирает rand
ящик:
$ cargo build
- Updating crates.io index
- Downloaded rand v0.8.5
- --snip--
- Compiling rand v0.8.5
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
-warning: unused import: `rand`
- --> add_one/src/lib.rs:1:5
- |
-1 | use rand;
- | ^^^^
- |
- = note: `#[warn(unused_imports)]` on by default
-
-warning: `add_one` (lib) generated 1 warning
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished dev [unoptimized + debuginfo] target(s) in 10.18s
-
-Файл Cargo.lock верхнего уровня теперь содержит сведения о зависимости add_one
к ящику rand
. Тем не менее, не смотря на то что rand
использован где-то в рабочем пространстве, мы не можем использовать его в других ящиках рабочего пространства, пока не добавим ящик rand
в отдельные Cargo.toml файлы. Например, если мы добавим use rand;
в файл adder/src/main.rs ящика adder
, то получим ошибку:
$ cargo build
- --snip--
- Compiling adder v0.1.0 (file:///projects/add/adder)
-error[E0432]: unresolved import `rand`
- --> adder/src/main.rs:2:5
- |
-2 | use rand;
- | ^^^^ no external crate `rand`
-
-Чтобы исправить это, изменените файл Cargo.toml для дополнения adder
и укажите, что rand
также является его зависимостью. При сборке дополнения adder
rand
будет добавлен в список зависимостей для adder
в Cargo.lock, но никаких дополнительных повторов rand
загружено не будет. Cargo позаботился о том, чтобы все ящики во всех дополнениях рабочей области, использующих дополнение rand
, использовали одну и ту же исполнение, экономя нам место и обеспечивая, что все ящики в рабочей области будут совместимы друг с другом.
В качестве ещё одного улучшения давайте добавим проверка функции add_one::add_one
в add_one
:
Файл: add_one/src/lib.rs
-pub fn add_one(x: i32) -> i32 {
- x + 1
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn it_works() {
- assert_eq!(3, add_one(2));
- }
-}
-Теперь запустите cargo test
в папке верхнего уровня add. Запуск cargo test
в рабочем пространстве, внутренне выстроеном
подобно этому, запустит проверки для всех ящиков в рабочем пространстве:
- -$ cargo test
- Compiling add_one v0.1.0 (file:///projects/add/add_one)
- Compiling adder v0.1.0 (file:///projects/add/adder)
- Finished test [unoptimized + debuginfo] target(s) in 0.27s
- Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841)
-
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e)
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests add_one
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-Первая раздел вывода показывает, что проверка it_works
в ящике add_one
прошёл. Следующая раздел показывает, что в ящике adder
не было обнаружено ни одного проверки, а последняя раздел показывает, что в ящике add_one
не было найдено ни одного проверки документации.
Мы также можем запустить проверки для одного определенного ящика в рабочем пространстве из папка верхнего уровня с помощью флага -p
и указанием имени ящика для которого мы хотим запустить проверки:
$ cargo test -p add_one
- Finished test [unoptimized + debuginfo] target(s) in 0.00s
- Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74)
-
-running 1 test
-test tests::it_works ... ok
-
-test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
- Doc-tests add_one
-
-running 0 tests
-
-test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-Эти выходные данные показывают, что выполнение cargo test
запускает только проверки для ящика add-one
и не запускает проверки ящика adder
.
Если вы соберётесь обнародовать ящики из рабочего пространства на crates.io, каждый ящик будет необходимо будет обнародовать отдельно. Подобно cargo test
, мы можем обнародовать определенный ящик из нашей рабочей области, используя флаг -p
и указав имя ящика, который мы хотим обнародовать.
Для дополнительной опытов добавьте ящик add_two
в данное рабочее пространство подобным способом, как делали с ящик add_one
!
По мере роста дела рассмотрите возможность использования рабочих областей: легче понять небольшие, отдельные составляющие, чем один большой кусок кода. Кроме того, хранение ящиков в рабочем пространстве может облегчить согласование между ящиками, если они часто изменяются одновременно.
- -cargo install
Приказ cargo install
позволяет местно устанавливать и использовать исполняемые ящики. Она не предназначена для замены системных дополнений; она используется как удобный способ Ржавчина разработчикам устанавливать средства, которыми другие разработчики поделились на сайте crates.io. Заметьте, можно устанавливать только дополнения, имеющие исполняемые целевые ящики. Исполняемой целью (binary target) является запускаемая программа, созданная и имеющая в составе ящика файл src/main.rs или другой файл, указанный как исполняемый, в отличии от библиотечных ящиков, которые не могут запускаться сами по себе, но подходят для включения в другие программы. Обычно ящик содержит сведения в файле README, является ли он библиотекой, исполняемым файлом или обоими вместе.
Все исполняемые файлы установленные приказом cargo install
сохранены в корневой установочной папке bin. Если вы установили Ржавчина с помощью rustup.rs и у вас его нет в пользовательских настройках, то этим папкой будет $HOME/.cargo/bin. Он заверяет, что папка находится в вашем окружении $PATH
, чтобы вы имели возможность запускать программы, которые вы установили приказом cargo install
.
Так, например, в главе 12 мы упоминали, что для поиска файлов существует выполнение утилиты grep
на Ржавчина под названием ripgrep
. Чтобы установить ripgrep
, мы можем выполнить следующее:
$ cargo install ripgrep
- Updating crates.io index
- Downloaded ripgrep v13.0.0
- Downloaded 1 crate (243.3 KB) in 0.88s
- Installing ripgrep v13.0.0
---snip--
- Compiling ripgrep v13.0.0
- Finished release [optimized + debuginfo] target(s) in 3m 10s
- Installing ~/.cargo/bin/rg
- Installed package `ripgrep v13.0.0` (executable `rg`)
-
-Последняя строка вывода показывает местоположение и название установленного исполняемого файла, который в случае ripgrep
называется rg
. Если вашей установочной папкой является $PATH
, как уже упоминалось ранее, вы можете запустить rg --help
и начать использовать более быстрый и грубый средство для поиска файлов!
Cargo расчитан так, что вы можете расширять его новыми субприказми без необходимости изменения самого Cargo. Если исполняемый файл доступен через переменную окружения $PATH
и назван по образцу cargo-something
, то его можно запускать как субприказ Cargo cargo something
. Пользовательские приказы подобные этой также перечисляются в списке доступных через cargo --list
. Возможность использовать cargo install
для установки расширений и затем запускать их так же, как встроенные в Cargo средства, это очень удобное следствие продуманного внешнего вида Cargo!
Совместное использование кода с Cargo и crates.io является частью того, что делает внутреннее устройство Ржавчина полезной для множества различных задач. Обычная библиотека Ржавчина небольшая и безотказная, но ящики легко распространять, использовать и улучшать независимо от самого языка. Не стесняйтесь делиться кодом, который был вам полезен, через crates.io; скорее всего, он будет полезен и кому-то ещё!
- -Указатель — это общая подход для переменной, которая содержит адрес участка памяти. Этот адрес «относится к», или «указывает на» некоторые другие данные. Наиболее общая разновидность указателя в Ржавчина — это ссылка, о которой вы узнали из главы 4. Ссылки обозначаются символом &
и заимствуют значение, на которое указывают. Они не имеют каких-либо особых возможностей, кроме как ссылаться на данные, и не имеют никаких накладных расходов.
Умные указатели, с другой стороны, являются устройствами данных, которые не только действуют как указатель, но также имеют дополнительные метаданные и возможности. Подход умных указателей не неповторима для Rust: умные указатели возникли в C++ и существуют в других языках. В Ржавчина есть разные умные указатели, определённые в встроенной библиотеке, которые обеспечивают возможность, выходящую за рамки ссылок. Одним из примеров, который мы рассмотрим в этой главе, является вид умного указателя reference counting (подсчёт ссылок). Этот указатель позволяет иметь несколько владельцев с помощью отслеживания количества владельцев и, когда владельцев не остаётся, очищает данные.
-Rust с его подходом владения и заимствования имеет дополнительное различие между ссылками и умными указателями: в то время, как ссылки только заимствуют данные, умные указатели часто владеют данными, на которые указывают.
-Ранее мы уже сталкивались с умными указателями в этой книге, хотя и не называли их так, например String
и Vec<T>
в главе 8. Оба этих вида считаются умными указателями, потому что они владеют некоторой областью памяти и позволяют ею управлять. У них также есть метаданные и дополнительные возможности или заверения. String
, например, хранит свой размер в виде метаданных и заверяет, что содержимое строки всегда будет в кодировке UTF-8.
Умные указатели обычно выполняются с помощью устройств. Присущей чертой, которая отличает умный указатель от обычной устройства, является то, что для умных указателей выполнены особенности Deref
и Drop
. Особенность Deref
позволяет образцу умного указателя вести себя как ссылка, так что вы можете написать код, работающий с ним как со ссылкой, так и как с умным указателем. Особенность Drop
позволяет написать код, который будет запускаться когда образец умного указателя выйдет из области видимости. В этой главе мы обсудим оба особенности и выясним, почему они важны для умных указателей.
Учитывая, что образец умного указателя является общим образцом разработки, часто используемым в Rust, эта глава не описывает все существующие умные указатели. Множество библиотек имеют свои умные указатели, и вы также можете написать свои. Мы охватим наиболее распространённые умные указатели из встроенной библиотеки:
-Box<T>
для распределения значений в куче (памяти)Rc<T>
вид счётчика ссылок, который допускает множественное владениеRef<T>
и RefMut<T>
, доступ к которым осуществляется через вид RefCell<T>
, который обеспечивает правила заимствования во время выполнения вместо времени сборкиДополнительно мы рассмотрим образец внутренней изменчивости (interior mutability), где неизменяемый вид предоставляет API для изменения своего внутреннего значения. Мы также обсудим ссылочные зацикленности (reference cycles): как они могут приводить к утечке памяти и как это предотвратить.
-Приступим!
- -Box<T>
для ссылки на данные в кучеНаиболее простой умный указатель - это box, чей вид записывается как Box<T>
. Такие переменные позволяют хранить данные в куче, а не в обойме. То, что остаётся в обойме, является указателем на данные в куче. Обратитесь к Главе 4, чтобы рассмотреть разницу между обоймой и кучей.
У Box нет неполадок с производительностью, кроме хранения данных в куче вместо обоймы. Но он также и не имеет множества дополнительных возможностей. Вы будете использовать его чаще всего в следующих случаейх:
-Мы выясним первую случай в разделе "Выполнение рекурсивных видов с помощью Box". Во втором случае, передача владения на большой размер данных может занять много времени, потому что данные повторяются через обойма. Для повышения производительности в этой случаи, мы можем хранить большое количество данных в куче с помощью Box. Затем только небольшое количество данных указателя воспроизводится в обойме, в то время как данные, на которые он ссылается, остаются в одном месте кучи. Третий случай известен как особенность предмет (trait object) и глава 17 посвящает целый раздел "Использование особенность предметов, которые допускают значения разных видов" только этой теме. Итак, то, что вы узнаете здесь, вы примените снова в Главе 17!
-Box<T>
для хранения данных в кучеПрежде чем мы обсудим этот исход использования Box<T>
, мы рассмотрим правила написания и то, как взаимодействовать со значениями, хранящимися в Box<T>
.
В приложении 15-1 показано, как использовать поле для хранения значения i32
в куче:
Файл: src/main.rs
--fn main() { - let b = Box::new(5); - println!("b = {b}"); -}
-
Мы объявляем переменную b
со значением Box
, указывающим на число 5
, размещённое в куче. Эта программа выведет b = 5
; в этом случае мы получаем доступ к данным в box так же, как если бы эти данные находились в обойме. Как и любое другое значение, когда box выйдет из области видимости, как b
в конце main
, он будет удалён. Деаллокация происходит как для box ( хранящегося в обойме), так и для данных, на которые он указывает (хранящихся в куче).
Размещать одиночные значения в куче не слишком целесообразно, поэтому вряд ли вы будете часто использовать box'ы таким образом. В большинстве случаев более уместно размещать такие значения, как i32
, в обойме, где они и сохраняются по умолчанию. Давайте рассмотрим случай, когда box позволяет нам определить виды, которые мы не могли бы иметь, если бы у нас не было box.
Значение рекурсивного вида может иметь другое значение такого же вида как свой составляющая. Рекурсивные виды представляют собой неполадку, поскольку во время сборки Ржавчина должен знать, сколько места занимает вид. Однако вложенность значений рекурсивных видов предположительно может продолжаться бесконечно, поэтому Ржавчина не может определить, сколько места потребуется. Поскольку box имеет известный размер, мы можем включить рекурсивные виды, добавив box в определение рекурсивного вида.
-В качестве примера рекурсивного вида рассмотрим cons list. Это вид данных, часто встречающийся в полезных языках программирования. Вид cons list, который мы определим, достаточно прост, за исключением наличия рекурсии; поэтому подходы, заложенные в примере, с которым мы будем работать, пригодятся вам в любой более сложной случаи, связанной с рекурсивными видами.
-cons list - это устройства данных из языка программирования Lisp и его диалектов, представляющая собой набор вложенных пар и являющаяся Lisp-исполнением связного списка. Его название происходит от функции cons
(сокращение от "construct function") в Lisp, которая создает пару из двух своих переменных. Вызывая cons
для пары, которая состоит из некоторого значения и другой пары, мы можем выстраивать списки cons, состоящие из рекурсивных пар.
Вот, пример cons list в виде псевдокода, содержащий список 1, 2, 3, где каждая пара заключена в круглые скобки:
-(1, (2, (3, Nil)))
-
-Каждый элемент в cons списке содержит два элемента: значение текущего элемента и следующий элемент. Последний элемент в списке содержит только значение называемое Nil
без следующего элемента. Cons список создаётся путём рекурсивного вызова функции cons
. Каноничное имя для обозначения основного случая рекурсии - Nil
. Обратите внимание, что это не то же самое, что понятие “null” или “nil” из главы 6, которая является недействительным или отсутствующим значением.
Cons list не является часто используемой устройством данных в Rust. В большинстве случаев, когда вам нужен список элементов при использовании Rust, лучше использовать Vec<T>
. Другие, более сложные рекурсивные виды данных полезны в определённых случаейх, но благодаря тому, что в этой главе мы начнём с cons list, мы сможем выяснить, как box позволяет нам определить рекурсивный вид данных без особого напряжения.
Приложение 15-2 содержит объявление перечисления cons списка. Обратите внимание, что этот код не будет собираться, потому что вид List
не имеет известного размера, что мы и выясним.
Файл: src/main.rs
-enum List {
- Cons(i32, List),
- Nil,
-}
-
-fn main() {}
--
--Примечание: В данном примере мы выполняем cons list, который содержит только значения
-i32
. Мы могли бы выполнить его с помощью generics, о которых мы говорили в главе 10, чтобы определить вид cons list, который мог бы хранить значения любого вида.
Использование вида List
для хранения списка 1, 2, 3
будет выглядеть как код в приложении 15-3:
Файл: src/main.rs
-enum List {
- Cons(i32, List),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-
-fn main() {
- let list = Cons(1, Cons(2, Cons(3, Nil)));
-}
--
Первое значение Cons
содержит 1
и другой List
. Это значение List
является следующим значением Cons
, которое содержит 2
и другой List
. Это значение List
является ещё один значением Cons
, которое содержит 3
и значение List
, которое наконец является Nil
, не рекурсивным исходом, сигнализирующим об окончании списка.
Если мы попытаемся собрать код в приложении 15-3, мы получим ошибку, показанную в приложении 15-4:
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
-error[E0072]: recursive type `List` has infinite size
- --> src/main.rs:1:1
- |
-1 | enum List {
- | ^^^^^^^^^
-2 | Cons(i32, List),
- | ---- recursive without indirection
- |
-help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
- |
-2 | Cons(i32, Box<List>),
- | ++++ +
-
-error[E0391]: cycle detected when computing when `List` needs drop
- --> src/main.rs:1:1
- |
-1 | enum List {
- | ^^^^^^^^^
- |
- = note: ...which immediately requires computing when `List` needs drop again
- = note: cycle used when computing whether `List` needs drop
- = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
-
-Some errors have detailed explanations: E0072, E0391.
-For more information about an error, try `rustc --explain E0072`.
-error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
-
--
Ошибка говорит о том, что этот вид "имеет бесконечный размер". Причина в том, что мы определили List
в виде, которая является рекурсивной: она непосредственно хранит другое значение своего собственного вида. В итоге Ржавчина не может определить, сколько места ему нужно для хранения значения List
. Давайте разберёмся, почему мы получаем эту ошибку. Сначала мы рассмотрим, как Ржавчина решает, сколько места ему нужно для хранения значения нерекурсивного вида.
Вспомните перечисление Message
определённое в приложении 6-2, когда обсуждали объявление enum в главе 6:
-enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(i32, i32, i32), -} - -fn main() {}
Чтобы определить, сколько памяти выделять под значение Message
, Ржавчина проходит каждый из исходов, чтобы увидеть, какой исход требует наибольшее количество памяти. Ржавчина видит, что для Message::Quit
не требуется места, Message::Move
хватает места для хранения двух значений i32
и т.д. Так как будет использоваться только один исход, то наибольшее пространство, которое потребуется для значения Message
, это пространство, которое потребуется для хранения самого большого из исходов перечисления.
Сравните это с тем, что происходит, когда Ржавчина пытается определить, сколько места необходимо рекурсивному виду, такому как перечисление List
в приложении 15-2. Сборщик смотрит на исход Cons
, который содержит значение вида i32
и значение вида List
. Следовательно, Cons
нужно пространство, равное размеру i32
плюс размер List
. Чтобы выяснить, сколько памяти необходимо виду List
, сборщик смотрит на исходы, начиная с Cons
. Исход Cons
содержит значение вида i32
и значение вида List
, и этот этап продолжается бесконечно, как показано на рисунке 15-1.
-
Box<T>
для получения рекурсивного вида с известным размеромПоскольку Ржавчина не может определить, сколько места нужно выделить для видов с рекурсивным определением, сборщик выдаёт ошибку с этим полезным предложением:
- -help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable
- |
-2 | Cons(i32, Box<List>),
- | ++++ +
-
-В данном предложении "перенаправление" означает, что вместо того, чтобы непосредственно хранить само значение, мы должны изменить устройство данных, так чтобы хранить его косвенно - хранить указатель на это значение.
-Поскольку Box<T>
является указателем, Ржавчина всегда знает, сколько места нужно Box<T>
: размер указателя не меняется в зависимости от объёма данных, на которые он указывает. Это означает, что мы можем поместить Box<T>
внутрь образца Cons
вместо значения List
напрямую. Box<T>
будет указывать на значение очередного List
, который будет находиться в куче, а не внутри образца Cons
. Мировозренческо у нас все ещё есть список, созданный из списков, содержащих другие списки, но эта выполнение теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга.
Мы можем изменить определение перечисления List
в приложении 15-2 и использование List
в приложении 15-3 на код из приложения 15-5, который будет собираться:
Файл: src/main.rs
--enum List { - Cons(i32, Box<List>), - Nil, -} - -use crate::List::{Cons, Nil}; - -fn main() { - let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); -}
-
Cons
требуется объём i32
плюс место для хранения данных указателя box. Nil
не хранит никаких значений, поэтому ему нужно меньше места, чем Cons
. Теперь мы знаем, что любое значение List
займёт размер i32
плюс размер данных указателя box. Используя box, мы разорвали бесконечную рекурсивную цепочку, поэтому сборщик может определить размер, необходимый для хранения значения List
. На рисунке 15-2 показано, как теперь выглядит Cons
.
-
Box-ы обеспечивают только перенаправление и выделение в куче; у них нет никаких других особых возможностей, подобных тем, которые мы увидим у других видов умных указателей. У них также нет накладных расходов на производительность, которые несут эти особые возможности, поэтому они могут быть полезны в таких случаях, как cons list, где перенаправление - единственная функция, которая нам нужна. В главе 17 мы также рассмотрим другие случаи использования box.
-Вид Box<T>
является умным указателем, поскольку он выполняет особенность Deref
, который позволяет обрабатывать значения Box<T>
как ссылки. Когда значение Box<T>
выходит из области видимости, данные кучи, на которые указывает box, также очищаются благодаря выполнения особенности Drop
. Эти два особенности будут ещё более значимыми для возможности, предоставляемой другими видами умных указателей, которые мы обсудим в оставшейся части этой главы. Давайте рассмотрим эти два особенности более подробно.
Deref
особенностиИспользуя особенность Deref
, вы можете изменить поведение оператора разыменования *
(не путать с операторами умножения или вездесущего подключения). Выполнив Deref
таким образом, что умный указатель может рассматриваться как обычная ссылка, вы можете писать код, оперирующий ссылками, а также использовать этот код с умными указателями.
Давайте сначала посмотрим, как работает оператор разыменования с обычными ссылками. Затем мы попытаемся определить пользовательский вид, который ведёт себя как Box<T>
и посмотрим, почему оператор разыменования не работает как ссылка для нового объявленного вида. Мы рассмотрим, как выполнение особенности Deref
делает возможным работу умных указателей подобно ссылкам. Затем посмотрим на разыменованное приведение (deref coercion) в Ржавчина и как оно позволяет работать с любыми ссылками или умными указателями.
-- - -Примечание: есть одна большая разница между видом
-MyBox<T>
, который мы собираемся создать и существующимBox<T>
: наша исполнение не будет хранить свои данные в куче. В примере мы сосредоточимся на особенностиDeref
, поэтому менее важно то, где данные хранятся, чем поведение подобное указателю.
Обычная ссылка - это разновидность указателя, а указатель можно рассматривать как своеобразную стрелочку направляющую к значению, хранящемуся в другом месте. В приложении 15-6 мы создаём ссылку на значение i32
, а затем используем оператор разыменования для перехода от ссылки к значению:
Файл: src/main.rs
--fn main() { - let x = 5; - let y = &x; - - assert_eq!(5, x); - assert_eq!(5, *y); -}
-
Переменной x
присвоено значение5
вида i32
. Мы установили в качестве значения y
ссылку на x
. Мы можем утверждать, что значение x
равно 5
. Однако, если мы хотим сделать утверждение о значении в y
, мы должны использовать *y
, чтобы перейти по ссылке к значению, на которое она указывает (таким образом, происходит разыменование), для того чтобы сборщик при сравнении мог использовать действительное значение. Как только мы разыменуем y
, мы получим доступ к целочисленному значению, на которое указывает y
, которое и будем сравнивать с 5
.
Если бы мы попытались написать assert_eq!(5, y);
, то получили ошибку сборки:
$ cargo run
- Compiling deref-example v0.1.0 (file:///projects/deref-example)
-error[E0277]: can't compare `{integer}` with `&{integer}`
- --> src/main.rs:6:5
- |
-6 | assert_eq!(5, y);
- | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
- |
- = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
- = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
-
-Сравнение числа и ссылки на число не допускается, потому что они различных видов. Мы должны использовать оператор разыменования, чтобы перейти по ссылке на значение, на которое она указывает.
-Box<T>
как ссылкуМы можем переписать код в приложении 15-6, чтобы использовать Box<T>
вместо ссылки; оператор разыменования, используемый для Box<T>
в приложении 15-7, работает так же, как оператор разыменования, используемый для ссылки в приложении 15-6:
Файл: src/main.rs
--fn main() { - let x = 5; - let y = Box::new(x); - - assert_eq!(5, x); - assert_eq!(5, *y); -}
-
Основное различие между приложением 15-7 и приложением 15-6 заключается в том, что здесь мы устанавливаем y
как образец Box<T>
, указывающий на воспроизведенное значение x
, а не как ссылку, указывающую на значение x
. В последнем утверждении мы можем использовать оператор разыменования, чтобы проследовать за указателем Box<T>
так же, как мы это делали, когда y
был ссылкой. Далее мы рассмотрим, что особенного в Box<T>
, что позволяет нам использовать оператор разыменования, определяя наш собственный вид.
Давайте создадим умный указатель, похожий на вид Box<T>
предоставляемый встроенной библиотекой, чтобы понять как поведение умных указателей отличается от поведения обычной ссылки. Затем мы рассмотрим вопрос, как добавить возможность использовать оператор разыменования.
Вид Box<T>
в конечном итоге определяется как устройства упорядоченного ряда с одним элементом, поэтому в приложении 15-8 подобным образом определяется MyBox<T>
. Мы также определим функцию new
, чтобы она соответствовала функции new
, определённой в Box<T>
.
Файл: src/main.rs
--struct MyBox<T>(T); - -impl<T> MyBox<T> { - fn new(x: T) -> MyBox<T> { - MyBox(x) - } -} - -fn main() {}
-
Мы определяем устройство с именем MyBox
и объявляем обобщённый свойство T
, потому что мы хотим, чтобы наш вид хранил значения любого вида. Вид MyBox
является устройством упорядоченного ряда с одним элементом вида T
. Функция MyBox::new
принимает один свойство вида T
и возвращает образец MyBox
, который содержит переданное значение.
Давайте попробуем добавить функцию main
из приложения 15-7 в приложение 15-8 и изменим её на использование вида MyBox<T>
, который мы определили вместо Box<T>
. Код в приложении 15-9 не будет собираться, потому что Ржавчина не знает, как разыменовывать MyBox
.
Файл: src/main.rs
-struct MyBox<T>(T);
-
-impl<T> MyBox<T> {
- fn new(x: T) -> MyBox<T> {
- MyBox(x)
- }
-}
-
-fn main() {
- let x = 5;
- let y = MyBox::new(x);
-
- assert_eq!(5, x);
- assert_eq!(5, *y);
-}
--
Вот итог ошибки сборки:
-$ cargo run
- Compiling deref-example v0.1.0 (file:///projects/deref-example)
-error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
- --> src/main.rs:14:19
- |
-14 | assert_eq!(5, *y);
- | ^^
-
-For more information about this error, try `rustc --explain E0614`.
-error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
-
-Наш вид MyBox<T>
не может быть разыменован, потому что мы не выполнили эту возможность. Чтобы включить разыменование с помощью оператора *
, мы выполняем особенность Deref
.
Deref
Как обсуждалось в разделе “Выполнение особенности для типа” Главы 10, для выполнения особенности нужно предоставить выполнения требуемых способов особенности. Особенность Deref
, предоставляемый встроенной библиотекой требует от нас выполнения одного способа с именем deref
, который заимствует self
и возвращает ссылку на внутренние данные. Приложение 15-10 содержит выполнение Deref
добавленную к определению MyBox
:
Файл: src/main.rs
--use std::ops::Deref; - -impl<T> Deref for MyBox<T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -struct MyBox<T>(T); - -impl<T> MyBox<T> { - fn new(x: T) -> MyBox<T> { - MyBox(x) - } -} - -fn main() { - let x = 5; - let y = MyBox::new(x); - - assert_eq!(5, x); - assert_eq!(5, *y); -}
-
правила написания type Target = T;
определяет связанный вид для использования у особенности Deref
. Связанные виды - это немного другой способ объявления обобщённого свойства, но пока вам не нужно о них беспокоиться; мы рассмотрим их более подробно в главе 19.
Мы заполним тело способа deref
оператором &self.0
, чтобы deref
вернул ссылку на значение, к которому мы хотим получить доступ с помощью оператора *
; вспомним из раздела "Using Tuple Structs without Named Fields to Create Different Types" главы 5, что .0
получает доступ к первому значению в упорядоченной в ряд устройстве. Функция main
в приложении 15-9, которая вызывает *
для значения MyBox<T>
, теперь собирается, и проверки проходят!
Без особенности Deref
сборщик может только разыменовывать &
ссылки. Способ deref
даёт сборщику возможность принимать значение любого вида, выполняющего Deref
и вызывать способ deref
чтобы получить ссылку &
, которую он знает, как разыменовывать.
Когда мы ввели *y
в приложении 15-9, Ржавчина в действительности выполнил за кулисами такой код:
*(y.deref())
-Rust заменяет оператор *
вызовом способа deref
и затем простое разыменование, поэтому нам не нужно думать о том, нужно ли нам вызывать способ deref
. Эта функция Ржавчина позволяет писать код, который исполняется одинаково, независимо от того, есть ли у нас обычная ссылка или вид, выполняющий особенность Deref
.
Причина, по которой способ deref
возвращает ссылку на значение, и что простое разыменование вне круглых скобок в *(y.deref())
все ещё необходимо, связана с системой владения. Если бы способ deref
возвращал значение напрямую, а не ссылку на него, значение переместилось бы из self
. Мы не хотим передавать владение внутренним значением внутри MyBox<T>
в этом случае и в большинстве случаев, когда мы используем оператор разыменования.
Обратите внимание, что оператор *
заменён вызовом способа deref
, а затем вызовом оператора *
только один раз, каждый раз, когда мы используем *
в коде. Поскольку замена оператора *
не повторяется бесконечно, мы получаем данные вида i32
, которые соответствуют 5
в assert_eq!
приложения 15-9.
Разыменованное приведение преобразует ссылку на вид, который выполняет признак Deref
, в ссылку на другой вид. Например, deref coercion может преобразовать &String
в &str
, потому что String
выполняет признак Deref
, который возвращает &str
. Deref coercion - это удобный рычаг, который Ржавчина использует для переменных функций и способов, и работает только для видов, выполняющих признак Deref
. Это происходит самостоятельно , когда мы передаём в качестве переменной функции или способа ссылку на значение определённого вида, которое не соответствует виду свойства в определении функции или способа. В итоге серии вызовов способа deref
вид, который мы передали, преобразуется в вид, необходимый для свойства.
Разыменованное приведение было добавлено в Rust, так что программистам, пишущим вызовы функций и способов, не нужно добавлять множество явных ссылок и разыменований с помощью использования &
и *
. Возможность разыменованного приведения также позволяет писать больше кода, который может работать как с ссылками, так и с умными указателями.
Чтобы увидеть разыменованное приведение в действии, давайте воспользуемся видом MyBox<T>
определённым в приложении 15-8, а также выполнение Deref
добавленную в приложении 15-10. Приложение 15-11 показывает определение функции, у которой есть свойство вида срез строки:
Файл: src/main.rs
--fn hello(name: &str) { - println!("Hello, {name}!"); -} - -fn main() {}
-
Можно вызвать функцию hello
со срезом строки в качестве переменной, например hello("Rust");
. Разыменованное приведение делает возможным вызов hello
со ссылкой на значение вида MyBox<String>
, как показано в приложении 15-12.
Файл: src/main.rs
--use std::ops::Deref; - -impl<T> Deref for MyBox<T> { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -struct MyBox<T>(T); - -impl<T> MyBox<T> { - fn new(x: T) -> MyBox<T> { - MyBox(x) - } -} - -fn hello(name: &str) { - println!("Hello, {name}!"); -} - -fn main() { - let m = MyBox::new(String::from("Rust")); - hello(&m); -}
-
Здесь мы вызываем функцию hello
с переменнаяом &m
, который является ссылкой на значение MyBox<String>
. Поскольку мы выполнили особенность Deref
для MyBox<T>
в приложении 15-10, то Ржавчина может преобразовать &MyBox<String>
в &String
вызывая deref
. Обычная библиотека предоставляет выполнение особенности Deref
для вида String
, которая возвращает срез строки, это описано в документации API особенности Deref
. Ржавчина снова вызывает deref
, чтобы превратить &String
в &str
, что соответствует определению функции hello
.
Если бы Ржавчина не выполнил разыменованное приведение, мы должны были бы написать код в приложении 15-13 вместо кода в приложении 15-12 для вызова способа hello
со значением вида &MyBox<String>
.
Файл: src/main.rs
--use std::ops::Deref; - -impl<T> Deref for MyBox<T> { - type Target = T; - - fn deref(&self) -> &T { - &self.0 - } -} - -struct MyBox<T>(T); - -impl<T> MyBox<T> { - fn new(x: T) -> MyBox<T> { - MyBox(x) - } -} - -fn hello(name: &str) { - println!("Hello, {name}!"); -} - -fn main() { - let m = MyBox::new(String::from("Rust")); - hello(&(*m)[..]); -}
-
Код (*m)
разыменовывает MyBox<String>
в String
. Затем &
и [..]
принимают строковый срез String
, равный всей строке, чтобы соответствовать ярлыке hello
. Код без разыменованного приведения сложнее читать, писать и понимать со всеми этими символами. Разыменованное приведение позволяет Ржавчина обрабатывать эти преобразования для нас самостоятельно .
Когда особенность Deref
определён для задействованных видов, Ржавчина проанализирует виды и будет использовать Deref::deref
столько раз, сколько необходимо, чтобы получить ссылку, соответствующую виду свойства. Количество раз, которое нужно вставить Deref::deref
определяется во время сборки, поэтому использование разыменованного приведения не имеет накладных расходов во время выполнения!
Подобно тому, как вы используете особенность Deref
для переопределения оператора *
у неизменяемых ссылок, вы можете использовать особенность DerefMut
для переопределения оператора *
у изменяемых ссылок.
Rust выполняет разыменованное приведение, когда находит виды и выполнения особенностей в трёх случаях:
-&T
в вид &U
когда верно T: Deref<Target=U>
&mut T
в вид &mut U
когда верно T: DerefMut<Target=U>
&mut T
в вид &U
когда верно T: Deref<Target=U>
Первые два случая равноценны друг другу, за исключением того, что второй выполняет изменяемость. В первом случае говорится, что если у вас есть &T
, а T
выполняет Deref
для некоторого вида U
, вы сможете прозрачно получить &U
. Во втором случае говорится, что такое же разыменованное приведение происходит и для изменяемых ссылок.
Третий случай хитрее: Ржавчина также приводит изменяемую ссылку к неизменяемой. Но обратное не представляется возможным: неизменяемые ссылки никогда не приводятся к изменяемым ссылкам. Из-за правил заимствования, если у вас есть изменяемая ссылка, эта изменяемая ссылка должна быть единственной ссылкой на данные (в противном случае программа не будет собираться). Преобразование одной изменяемой ссылки в неизменяемую ссылку никогда не нарушит правила заимствования. Преобразование неизменяемой ссылки в изменяемую ссылку потребует наличия только одной неизменяемой ссылки на эти данные, и правила заимствования не заверяют этого. Следовательно, Ржавчина не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую ссылку возможно.
- -Drop
Вторым важным особенностью умного указателя является Drop, который позволяет управлять, что происходит, когда значение вот-вот выйдет из области видимости. Вы можете выполнить особенность Drop для любого вида, а также использовать этот код для высвобождения ресурсов, таких как файлы или сетевые соединения.
-Мы рассматриваем Drop
в среде умных указателей, потому что возможность свойства Drop
по сути всегда используется при выполнения умного указателя. Например, при сбросе Box<T>
происходит деаллокация пространства на куче, на которое указывает box.
В некоторых языках для некоторых видов программист должен вызывать код для освобождения памяти или ресурсов каждый раз, когда он завершает использование образцов этих видов. Примерами могут служить указатели файлов, сокеты или блокировки. Если забыть об этом, система окажется перегруженной и может упасть. В Ржавчина вы можете указать, что определённый отрывок кода должен выполняться всякий раз, когда значение выходит из области видимости, и сборщик самостоятельно будет его вставлять. Как следствие, вам не нужно заботиться о размещении кода очистки везде в программе, где завершается работа образца определённого вида - утечки ресурсов все равно не будет!
-Вы можете задать определённую логику, которая будет выполняться, когда значение выходит за пределы области видимости, выполнив признак Drop
. Особенность Drop
требует от вас выполнения одного способа drop
, который принимает изменяемую ссылку на self
. Чтобы увидеть, когда Ржавчина вызывает drop
, давайте выполняем drop
с помощью указаний println!
.
В приложении 15-14 показана устройства CustomSmartPointer
, единственной не имеющей себе подобных возможностью которой является печать Dropping CustomSmartPointer!
, когда образец выходит из области видимости, чтобы показать, когда Ржавчина выполняет функцию drop
.
Файл: src/main.rs
--struct CustomSmartPointer { - data: String, -} - -impl Drop for CustomSmartPointer { - fn drop(&mut self) { - println!("Dropping CustomSmartPointer with data `{}`!", self.data); - } -} - -fn main() { - let c = CustomSmartPointer { - data: String::from("my stuff"), - }; - let d = CustomSmartPointer { - data: String::from("other stuff"), - }; - println!("CustomSmartPointers created."); -}
-
Особенность Drop
включён в прелюдию, поэтому нам не нужно вводить его в область видимости. Мы выполняем особенность Drop
для CustomSmartPointer
и выполняем способ drop
, который будет вызывать println!
. Тело функции drop
- это место, где должна располагаться вся логика, которую вы захотите выполнять, когда образец вашего вида выйдет из области видимости. Мы печатаем здесь текст, чтобы наглядно отобразить, когда Ржавчина вызовет drop
.
В main
мы создаём два образца CustomSmartPointer
и затем печатаем CustomSmartPointers created
. В конце main
наши образцы CustomSmartPointer
выйдут из области видимости и Ржавчина вызовет код, который мы добавили в способ drop
, который и напечатает наше окончательное сообщение. Обратите внимание, что нам не нужно вызывать способ drop
явно.
Когда мы запустим эту программу, мы увидим следующий вывод:
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
- Running `target/debug/drop-example`
-CustomSmartPointers created.
-Dropping CustomSmartPointer with data `other stuff`!
-Dropping CustomSmartPointer with data `my stuff`!
-
-Rust самостоятельно вызывал drop
в мгновение выхода наших образцов из области видимости, тем самым выполнив заданный нами код. Переменные удаляются в обратном порядке их создания, поэтому d
была удалена до c
. Цель этого примера — дать вам наглядное представление о том, как работает способ drop
; в типичных случаях вы будете задавать код очистки, который должен выполнить ваш вид, а не печатать сообщение.
std::mem::drop
К сожалению, отключение функции самостоятельного удаления с помощью drop
является не простым. Отключение drop
обычно не требуется; весь смысл особенности Drop
в том, чтобы о функции позаботились самостоятельно . Иногда, однако, вы можете захотеть очистить значение рано. Одним из примеров является использование умных указателей, которые управляют блокировками: вы могли бы потребовать принудительный вызов способа drop
который снимает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Ржавчина не позволяет вызвать способ особенности Drop
вручную; вместо этого вы должны вызвать функцию std::mem::drop
предоставляемую встроенной библиотекой, если хотите принудительно удалить значение до конца области видимости.
Если попытаться вызвать способ drop
особенности Drop
вручную, изменяя функцию main
приложения 15-14 так, как показано в приложении 15-15, мы получим ошибку сборщика:
Файл: src/main.rs
-struct CustomSmartPointer {
- data: String,
-}
-
-impl Drop for CustomSmartPointer {
- fn drop(&mut self) {
- println!("Dropping CustomSmartPointer with data `{}`!", self.data);
- }
-}
-
-fn main() {
- let c = CustomSmartPointer {
- data: String::from("some data"),
- };
- println!("CustomSmartPointer created.");
- c.drop();
- println!("CustomSmartPointer dropped before the end of main.");
-}
--
Когда мы попытаемся собрать этот код, мы получим ошибку:
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
-error[E0040]: explicit use of destructor method
- --> src/main.rs:16:7
- |
-16 | c.drop();
- | ^^^^ explicit destructor calls not allowed
- |
-help: consider using `drop` function
- |
-16 | drop(c);
- | +++++ ~
-
-For more information about this error, try `rustc --explain E0040`.
-error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
-
-Это сообщение об ошибке говорит, что мы не можем явно вызывать drop
. В сообщении об ошибке используется понятие деструктор (destructor), который является общим понятием программирования для функции, которая очищает образец. Деструктор подобен строителю, который создаёт образец. Функция drop
в Ржавчина является определённым деструктором.
Rust не позволяет обращаться к drop
напрямую, потому что он все равно самостоятельно вызовет drop
в конце main
. Это вызвало бы ошибку double free, потому что в этом случае Ржавчина попытался бы дважды очистить одно и то же значение.
Невозможно отключить самостоятельную подстановку вызова drop
, когда значение выходит из области видимости, и нельзя вызвать способ drop
напрямую. Поэтому, если нам нужно принудительно избавиться от значения раньше времени, следует использовать функцию std::mem::drop
.
Функция std::mem::drop
отличается от способа drop
особенности Drop
. Мы вызываем её, передавая в качестве переменной значение, которое хотим принудительно уничтожить. Функция находится в прелюдии, поэтому мы можем изменить main
в приложении 15-15 так, чтобы вызвать функцию drop
, как показано в приложении 15-16:
Файл: src/main.rs
--struct CustomSmartPointer { - data: String, -} - -impl Drop for CustomSmartPointer { - fn drop(&mut self) { - println!("Dropping CustomSmartPointer with data `{}`!", self.data); - } -} - -fn main() { - let c = CustomSmartPointer { - data: String::from("some data"), - }; - println!("CustomSmartPointer created."); - drop(c); - println!("CustomSmartPointer dropped before the end of main."); -}
-
Выполнение данного кода выведет следующий итог::
-$ cargo run
- Compiling drop-example v0.1.0 (file:///projects/drop-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
- Running `target/debug/drop-example`
-CustomSmartPointer created.
-Dropping CustomSmartPointer with data `some data`!
-CustomSmartPointer dropped before the end of main.
-
-Текст Dropping CustomSmartPointer with data
some data!
, напечатанный между CustomSmartPointer created.
и текстом CustomSmartPointer dropped before the end of main.
, показывает, что код способа drop
вызывается для удаления c
в этой точке.
Вы можете использовать код, указанный в выполнения особенности Drop
, чтобы сделать очистку удобной и безопасной: например, вы можете использовать её для создания своего собственного управленца памяти! С помощью особенности Drop
и системы владения Ржавчина не нужно целенаправленно заботиться о том, чтобы освобождать ресурсы, потому что Ржавчина делает это самостоятельно .
Также не нужно беспокоиться о неполадках, возникающих в итоге случайной очистки значений, которые всё ещё используются: система владения, которая заверяет, что ссылки всегда действительны, также заверяет, что drop
вызывается только один раз, когда значение больше не используется.
После того, как мы познакомились с Box<T>
и свойствами умных указателей, познакомимся с другими умными указателями, определёнными в встроенной библиотеке.
Rc<T>
, умный указатель с подсчётом ссылокВ большинстве случаев владение является однозначным: вы точно знаете, какая переменная владеет данным значением. Однако бывают случаи, когда у одного значения может быть несколько владельцев. Например, в Графовых устройствах может быть несколько рёбер, указывающих на один и тот же узел — таким образом, этот узел становится в действительности собственностью всех этих рёбер. Узел не подлежит удалению, за исключением тех случаев, когда на него не указывает ни одно ребро и, соответственно, у него нет владельцев.
-Вы должны включить множественное владение явно, используя вид Ржавчина Rc<T>
, который является аббревиатурой для подсчёта ссылок. Вид Rc<T>
отслеживает количество ссылок на значение, чтобы определить, используется ли оно ещё. Если ссылок на значение нет, значение может быть очищено и при этом ни одна ссылка не станет недействительной.
Представьте себе Rc<T>
как телевизор в гостиной. Когда один человек входит, чтобы смотреть телевизор, он включает его. Другие могут войти в комнату и посмотреть телевизор. Когда последний человек покидает комнату, он выключает телевизор, потому что он больше не используется. Если кто-то выключит телевизор во время его просмотра другими, то оставшиеся телезрители устроят шум!
Вид Rc<T>
используется, когда мы хотим разместить в куче некоторые данные для чтения несколькими частями нашей программы и не можем определить во время сборки, какая из частей завершит использование данных последней. Если бы мы знали, какая часть завершит использование последней то, мы могли бы сделать эту часть владельцем данных и вступили бы в силу обычные правила владения, применяемые во время сборки.
Обратите внимание, что Rc<T>
используется только в однопоточных сценариях. Когда мы обсудим состязательность в главе 16, мы рассмотрим, как выполнять подсчёт ссылок во многопоточных программах.
Rc<T>
для совместного использования данныхДавайте вернёмся к нашему примеру с cons списком в приложении 15-5. Напомним, что мы определили его с помощью вида Box<T>
. В этот раз мы создадим два списка, оба из которых будут владеть третьим списком. Мировозренческо это похоже на рисунок 15-3:
-
Мы создадим список a
, содержащий 5 и затем 10. Затем мы создадим ещё два списка: b
начинающийся с 3 и c
начинающийся с 4. Оба списка b
и c
затем продолжать первый список a
, содержащий 5 и 10. Другими словами, оба списка будут разделять первый список, содержащий 5 и 10.
Попытка выполнить этот сценарий, используя определение List
с видом Box<T>
не будет работать, как показано в приложении 15-17:
Файл: src/main.rs
-enum List {
- Cons(i32, Box<List>),
- Nil,
-}
-
-use crate::List::{Cons, Nil};
-
-fn main() {
- let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
- let b = Cons(3, Box::new(a));
- let c = Cons(4, Box::new(a));
-}
--
При сборки этого кода, мы получаем эту ошибку:
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
-error[E0382]: use of moved value: `a`
- --> src/main.rs:11:30
- |
-9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
- | - move occurs because `a` has type `List`, which does not implement the `Copy` trait
-10 | let b = Cons(3, Box::new(a));
- | - value moved here
-11 | let c = Cons(4, Box::new(a));
- | ^ value used here after move
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
-
-Исходы Cons
владеют данными, которые они содержат, поэтому, когда мы создаём список b
, то a
перемещается в b
, а b
становится владельцем a
. Затем, мы пытаемся использовать a
снова при создании c
, но нам не разрешают, потому что a
был перемещён.
Мы могли бы изменить определение Cons
, чтобы вместо этого хранить ссылки, но тогда нам пришлось бы указывать свойства времени жизни. Указывая свойства времени жизни, мы бы указали, что каждый элемент в списке будет жить как самое меньшее столько же, сколько и весь список. Это относится к элементам и спискам в приложении 15.17, но не во всех сценариях.
Вместо этого мы изменим наше определение вида List
так, чтобы использовать Rc<T>
вместо Box<T>
, как показано в приложении 15-18. Каждый исход Cons
теперь будет содержать значение и вид Rc<T>
, указывающий на List
. Когда мы создадим b
то, вместо того чтобы стал владельцем a
, мы будем клонировать Rc<List>
который содержит a
, тем самым увеличивая количество ссылок с единицы до двойки и позволяя переменным a
и b
разделять владение на данные в виде Rc<List>
. Мы также клонируем a
при создании c
, увеличивая количество ссылок с двух до трёх. Каждый раз, когда мы вызываем Rc::clone
, счётчик ссылок на данные внутри Rc<List>
будет увеличиваться и данные не будут очищены, если на них нет нулевых ссылок.
Файл: src/main.rs
--enum List { - Cons(i32, Rc<List>), - Nil, -} - -use crate::List::{Cons, Nil}; -use std::rc::Rc; - -fn main() { - let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); - let b = Cons(3, Rc::clone(&a)); - let c = Cons(4, Rc::clone(&a)); -}
-
Нам нужно добавить указанию use
, чтобы подключить вид Rc<T>
в область видимости, потому что он не входит в список самостоятельного подключения прелюдии. В main
, мы создаём список владеющий 5 и 10, сохраняем его в новом Rc<List>
переменной a
. Затем при создании b
и c
, мы называем функцию Rc::clone
и передаём ей ссылку на Rc<List>
как переменная a
.
Мы могли бы вызвать a.clone()
, а не Rc::clone(&a)
, но в Ржавчина принято использовать Rc::clone
в таком случае. Внутренняя выполнение Rc::clone
не делает глубокого повторения всех данных, как это происходит в видах большинства выполнений clone
. Вызов Rc::clone
только увеличивает счётчик ссылок, что не занимает много времени. Глубокое повторение данных может занимать много времени. Используя Rc::clone
для подсчёта ссылок, можно визуально различать виды клонирования с глубоким повторением и клонирования, которые увеличивают количество ссылок. При поиске в коде неполадок с производительностью нужно рассмотреть только клонирование с глубоким повторением и пренебрегать вызовы Rc::clone
.
Rc<T>
увеличивает количество ссылокДавайте изменим рабочий пример в приложении 15-18, чтобы увидеть как изменяется число ссылок при создании и удалении ссылок на Rc<List>
внутри переменной a
.
В приложении 15-19 мы изменим main
так, чтобы она имела внутреннюю область видимости вокруг списка c
; тогда мы сможем увидеть, как меняется счётчик ссылок при выходе c
из внутренней области видимости.
Файл: src/main.rs
--enum List { - Cons(i32, Rc<List>), - Nil, -} - -use crate::List::{Cons, Nil}; -use std::rc::Rc; - -fn main() { - let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); - println!("count after creating a = {}", Rc::strong_count(&a)); - let b = Cons(3, Rc::clone(&a)); - println!("count after creating b = {}", Rc::strong_count(&a)); - { - let c = Cons(4, Rc::clone(&a)); - println!("count after creating c = {}", Rc::strong_count(&a)); - } - println!("count after c goes out of scope = {}", Rc::strong_count(&a)); -}
-
В каждой части программы, где количество ссылок меняется, мы выводим количество ссылок, которое получаем, вызывая функцию Rc::strong_count
. Эта функция названа strong_count
, а не count
, потому что вид Rc<T>
также имеет weak_count
; мы увидим, для чего используется weak_count
в разделе "Предотвращение замкнутых ссылок: Превращение Rc<T>
в Weak<T>
".
Код выводит в окно вывода:
-$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
- Running `target/debug/cons-list`
-count after creating a = 1
-count after creating b = 2
-count after creating c = 3
-count after c goes out of scope = 2
-
-Можно увидеть, что Rc<List>
в переменной a
имеет начальный счётчик ссылок равный 1; затем каждый раз при вызове clone
счётчик увеличивается на 1. Когда c
выходит из области видимости, счётчик уменьшается на 1. Нам не нужно вызывать функцию уменьшения счётчика ссылок, как при вызове Rc::clone
для увеличения счётчика ссылок: выполнение Drop
самостоятельно уменьшает счётчик ссылок, когда значение Rc<T>
выходит из области видимости.
В этом примере мы не наблюдаем того, что когда b
, а затем a
выходят из области видимости в конце main
, счётчик становится равным 0, и Rc<List>
полностью очищается. Использование Rc<T>
позволяет одному значению иметь несколько владельцев, а счётчик заверяет, что значение остаётся действительным до тех пор, пока любой из владельцев ещё существует.
С помощью неизменяемых ссылок, вид Rc<T>
позволяет обмениваться данными между несколькими частями вашей программы только для чтения данных. Если вид Rc<T>
позволял бы иметь несколько изменяемых ссылок, вы могли бы нарушить одно из правил заимствования, описанных в главе 4: множественные изменяемые заимствования в одном и том же месте могут вызвать гонки данных (data races) и несогласованность данных. Но возможность изменять данные очень полезна! В следующем разделе мы обсудим образец внутренней изменчивости и вид RefCell<T>
, который можно использовать вместе с Rc<T>
для работы с этим ограничением.
RefCell<T>
и образец внутренней изменяемостиВнутренняя изменяемость - это образец разработки Rust, который позволяет вам изменять данные даже при наличии неизменяемых ссылок на эти данные; обычно такое действие запрещено правилами заимствования. Для изменения данных образец использует unsafe
код внутри устройства данных, чтобы обойти обычные правила Rust, управляющие изменяемость и заимствование. Небезопасный (unsafe) код даёт понять сборщику, что мы самостоятельно следим за соблюдением этих правил, а не полагаемся на то, что сборщик будет делать это для нас; подробнее о небезопасном коде мы поговорим в главе 19.
Мы можем использовать виды, в которых применяется образец внутренней изменяемости, только если мы можем обеспечить, что правила заимствования будут соблюдаться во время выполнения, несмотря на то, что сборщик не сможет этого обеспечить. В этом случае небезопасный
код оборачивается безопасным API, и внешне вид остаётся неизменяемым.
Давайте изучим данную подход с помощью вида данных RefCell<T>
, который выполняет этот образец.
RefCell<T>
В отличие от Rc<T>
вид RefCell<T>
предоставляет единоличное владение данными, которые он содержит. В чем же отличие вида RefCell<T>
от Box<T>
? Давайте вспомним правила заимствования из Главы 4:
С помощью ссылок и вида Box<T>
неизменные величины правил заимствования применяются на этапе сборки. С помощью RefCell<T>
они применяются во время работы программы. Если вы нарушите эти правила, работая с ссылками, то будет ошибка сборки. Если вы работаете с RefCell<T>
и нарушите эти правила, то программа вызовет панику и завершится.
Преимущества проверки правил заимствования во время сборки заключаются в том, что ошибки будут обнаруживаться раньше - ещё в этапе разработки, а производительность во время выполнения не пострадает, поскольку весь анализ завершён заранее. По этим причинам проверка правил заимствования во время сборки является лучшим выбором в большинстве случаев, и именно поэтому она используется в Ржавчина по умолчанию.
-Преимущество проверки правил заимствования во время выполнения заключается в том, что определённые сценарии, безопасные для памяти, разрешаются там, где они были бы запрещены проверкой во время сборки. Постоянной анализ, как и сборщик Rust, по своей сути устоявшийся. Некоторые свойства кода невозможно обнаружить, анализируя код: самый известный пример - неполадка остановки, которая выходит за рамки этой книги, но является важной темой для исследования.
-Поскольку некоторый анализ невозможен, то если сборщик Ржавчина не может быть уверен, что код соответствует правилам владения, он может отклонить правильную программу; таким образом он является консервативным. Если Ржавчина принял неправильную программу, то пользователи не смогут доверять заверениям, которые даёт Rust. Однако, если Ржавчина отклонит правильную программу, то программист будет испытывать неудобства, но ничего катастрофического не произойдёт. Вид RefCell<T>
полезен, когда вы уверены, что ваш код соответствует правилам заимствования, но сборщик не может понять и обеспечить этого.
Подобно виду Rc<T>
, вид RefCell<T>
предназначен только для использования в однопоточных сценариях и выдаст ошибку времени сборки, если вы попытаетесь использовать его в многопоточном среде. Мы поговорим о том, как получить возможность RefCell<T>
во многопоточной программе в главе 16.
Вот список причин выбора видов Box<T>
, Rc<T>
или RefCell<T>
:
Rc<T>
разрешает множественное владение одними и теми же данными; виды Box<T>
и RefCell<T>
разрешают иметь единственных владельцев.Box<T>
разрешает неизменяемые или изменяемые владения, проверенные при сборки; вид Rc<T>
разрешает только неизменяемые владения, проверенные при сборки; вид RefCell<T>
разрешает неизменяемые или изменяемые владения, проверенные во время выполнения.RefCell<T>
разрешает изменяемые заимствования, проверенные во время выполнения, можно изменять значение внутри RefCell<T>
даже если RefCell<T>
является неизменным.Изменение значения внутри неизменного значения является образцом внутренней изменяемости (interior mutability). Давайте посмотрим на случай, в которой внутренняя изменяемость полезна и рассмотрим, как это возможно.
-Следствием правил заимствования является то, что когда у вас есть неизменяемое значение, вы не можете заимствовать его с изменением. Например, этот код не будет собираться:
-fn main() {
- let x = 5;
- let y = &mut x;
-}
-Если вы попытаетесь собрать этот код, вы получите следующую ошибку:
-$ cargo run
- Compiling borrowing v0.1.0 (file:///projects/borrowing)
-error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
- --> src/main.rs:3:13
- |
-3 | let y = &mut x;
- | ^^^^^^ cannot borrow as mutable
- |
-help: consider changing this to be mutable
- |
-2 | let mut x = 5;
- | +++
-
-For more information about this error, try `rustc --explain E0596`.
-error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
-
-Однако бывают случаи, в которых было бы полезно, чтобы предмет мог изменять себя при помощи своих способов, но казался неизменным для прочего кода. Код вне способов этого предмета не должен иметь возможности изменять его содержимое. Использование RefCell<T>
- один из способов получить возможность внутренней изменяемости, но при этом RefCell<T>
не позволяет полностью обойти правила заимствования: средство проверки правил заимствования в сборщике позволяет эту внутреннюю изменяемость, однако правила заимствования проверяются во время выполнения. Если вы нарушите правила, то вместо ошибки сборки вы получите panic!
.
Давайте разберём опытный пример, в котором мы можем использовать RefCell<T>
для изменения неизменяемого значения и посмотрим, почему это полезно.
Иногда во время проверки программист использует один вид вместо другого для того, чтобы проверить определённое поведение и убедиться, что оно выполнено правильно. Такой вид-заместитель называется проверочным повторителем. Воспринимайте его как «каскадёра» в кинематографе, когда повторитель заменяет актёра для выполнения определённой сложной сцены. Проверочные повторители заменяют другие виды при выполнении проверок. Инсценировочные (mock) предметы — это особый вид проверочных повторителей, которые сохраняют данные происходящих во время проверки действий тем самым позволяя вам убедиться впоследствии, что все действия были выполнены правильно.
-В Ржавчина нет предметов в том же смысле, в каком они есть в других языках и в Ржавчина нет возможности мок предметов, встроенных в обычную библиотеку, как в некоторых других языках. Однако вы определённо можете создать устройство, которая будет служить тем же целям, что и мок предмет.
-Вот сценарий, который мы будем проверять: мы создадим библиотеку, которая отслеживает значение по отношению к заранее определённому наивысшему значению и отправляет сообщения в зависимости от того, насколько текущее значение находится близко к такому наивысшему значению. Эта библиотека может использоваться, например, для отслеживания квоты количества вызовов API пользователя, которые ему разрешено делать.
-Наша библиотека будет предоставлять только функции отслеживания того, насколько близко к наивысшему значению находится значение и какие сообщения должны быть внутри в этот мгновение. Ожидается, что приложения, использующие нашу библиотеку, предоставят рычаг для отправки сообщений: приложение может поместить сообщение в приложение, отправить электронное письмо, отправить текстовое сообщение или что-то ещё. Библиотеке не нужно знать эту подробность. Все что ему нужно - это что-то, что выполняет особенность, который мы предоставим с названием Messenger
. Приложение 15-20 показывает код библиотеки:
Файл: src/lib.rs
-pub trait Messenger {
- fn send(&self, msg: &str);
-}
-
-pub struct LimitTracker<'a, T: Messenger> {
- messenger: &'a T,
- value: usize,
- max: usize,
-}
-
-impl<'a, T> LimitTracker<'a, T>
-where
- T: Messenger,
-{
- pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
- LimitTracker {
- messenger,
- value: 0,
- max,
- }
- }
-
- pub fn set_value(&mut self, value: usize) {
- self.value = value;
-
- let percentage_of_max = self.value as f64 / self.max as f64;
-
- if percentage_of_max >= 1.0 {
- self.messenger.send("Error: You are over your quota!");
- } else if percentage_of_max >= 0.9 {
- self.messenger
- .send("Urgent warning: You've used up over 90% of your quota!");
- } else if percentage_of_max >= 0.75 {
- self.messenger
- .send("Warning: You've used up over 75% of your quota!");
- }
- }
-}
--
Одна важная часть этого кода состоит в том, что особенность Messenger
имеет один способ send
, принимающий переменнойми неизменяемую ссылку на self
и текст сообщения. Он является внешней оболочкой, который должен иметь наш мок предмет. Другой важной частью является то, что мы хотим проверить поведение способа set_value
у вида LimitTracker
. Мы можем изменить значение, которое передаём свойствоом value
, но set_value
ничего не возвращает и нет основания, чтобы мы могли бы проверить утверждения о выполнении способа. Мы хотим иметь возможность сказать, что если мы создаём LimitTracker
с чем-то, что выполняет особенность Messenger
и с определённым значением для max
, то когда мы передаём разные числа в переменной value
образец self.messenger отправляет соответствующие сообщения.
Нам нужен мок предмет, который вместо отправки электронного письма или текстового сообщения будет отслеживать сообщения, которые были ему поручены для отправки через send
. Мы можем создать новый образец мок предмета. создать LimitTracker
с использованием мок предмет для него, вызвать способ set_value
у образца LimitTracker
, а затем проверить, что мок предмет имеет ожидаемое сообщение. В приложении 15-21 показана попытка выполнить мок предмет, чтобы сделать именно то что хотим, но анализатор заимствований не разрешит такой код:
Файл: src/lib.rs
-pub trait Messenger {
- fn send(&self, msg: &str);
-}
-
-pub struct LimitTracker<'a, T: Messenger> {
- messenger: &'a T,
- value: usize,
- max: usize,
-}
-
-impl<'a, T> LimitTracker<'a, T>
-where
- T: Messenger,
-{
- pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
- LimitTracker {
- messenger,
- value: 0,
- max,
- }
- }
-
- pub fn set_value(&mut self, value: usize) {
- self.value = value;
-
- let percentage_of_max = self.value as f64 / self.max as f64;
-
- if percentage_of_max >= 1.0 {
- self.messenger.send("Error: You are over your quota!");
- } else if percentage_of_max >= 0.9 {
- self.messenger
- .send("Urgent warning: You've used up over 90% of your quota!");
- } else if percentage_of_max >= 0.75 {
- self.messenger
- .send("Warning: You've used up over 75% of your quota!");
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- struct MockMessenger {
- sent_messages: Vec<String>,
- }
-
- impl MockMessenger {
- fn new() -> MockMessenger {
- MockMessenger {
- sent_messages: vec![],
- }
- }
- }
-
- impl Messenger for MockMessenger {
- fn send(&self, message: &str) {
- self.sent_messages.push(String::from(message));
- }
- }
-
- #[test]
- fn it_sends_an_over_75_percent_warning_message() {
- let mock_messenger = MockMessenger::new();
- let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
-
- limit_tracker.set_value(80);
-
- assert_eq!(mock_messenger.sent_messages.len(), 1);
- }
-}
--
Этот проверочный код определяет устройство MockMessenger
, в которой есть поле sent_messages
со значениями вида Vec
из String
для отслеживания сообщений, которые поручены устройстве для отправки. Мы также определяем сопряженную функцию new
, чтобы было удобно создавать новые образцы MockMessenger
, которые создаются с пустым списком сообщений. Затем мы выполняем особенность Messenger
для вида MockMessenger
, чтобы передать MockMessenger
в LimitTracker
. В ярлыке способа send
мы принимаем сообщение для передачи в качестве свойства и сохраняем его в MockMessenger
внутри списка sent_messages
.
В этом проверке мы проверяем, что происходит, когда LimitTracker
сказано установить value
в значение, превышающее 75 процентов от значения max
. Сначала мы создаём новый MockMessenger
, который будет иметь пустой список сообщений. Затем мы создаём новый LimitTracker
и передаём ему ссылку на новый MockMessenger
и max
значение равное 100. Мы вызываем способ set_value
у LimitTracker
со значением 80, что составляет более 75 процентов от 100. Затем мы с помощью утверждения проверяем, что MockMessenger
должен содержать одно сообщение из списка внутренних сообщений.
Однако с этим проверкой есть одна неполадка, показанная ниже:
-$ cargo test
- Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
-error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
- --> src/lib.rs:58:13
- |
-58 | self.sent_messages.push(String::from(message));
- | ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
- |
-help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
- |
-2 ~ fn send(&mut self, msg: &str);
-3 | }
- ...
-56 | impl Messenger for MockMessenger {
-57 ~ fn send(&mut self, message: &str) {
- |
-
-For more information about this error, try `rustc --explain E0596`.
-error: could not compile `limit-tracker` (lib test) due to 1 previous error
-
-Мы не можем изменять MockMessenger
для отслеживания сообщений, потому что способ send
принимает неизменяемую ссылку на self
. Мы также не можем принять предложение из текста ошибки, чтобы использовать &mut self
, потому что тогда ярлык send
не будет соответствовать ярлыке в определении особенности Messenger
(не стесняйтесь попробовать и посмотреть, какое сообщение об ошибке получите вы).
Это случаей, в которой внутренняя изменяемость может помочь! Мы сохраним sent_messages
внутри вида RefCell<T>
, а затем в способе send
сообщение сможет изменить список sent_messages
для хранения сообщений, которые мы видели. Приложение 15-22 показывает, как это выглядит:
Файл: src/lib.rs
-pub trait Messenger {
- fn send(&self, msg: &str);
-}
-
-pub struct LimitTracker<'a, T: Messenger> {
- messenger: &'a T,
- value: usize,
- max: usize,
-}
-
-impl<'a, T> LimitTracker<'a, T>
-where
- T: Messenger,
-{
- pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
- LimitTracker {
- messenger,
- value: 0,
- max,
- }
- }
-
- pub fn set_value(&mut self, value: usize) {
- self.value = value;
-
- let percentage_of_max = self.value as f64 / self.max as f64;
-
- if percentage_of_max >= 1.0 {
- self.messenger.send("Error: You are over your quota!");
- } else if percentage_of_max >= 0.9 {
- self.messenger
- .send("Urgent warning: You've used up over 90% of your quota!");
- } else if percentage_of_max >= 0.75 {
- self.messenger
- .send("Warning: You've used up over 75% of your quota!");
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::cell::RefCell;
-
- struct MockMessenger {
- sent_messages: RefCell<Vec<String>>,
- }
-
- impl MockMessenger {
- fn new() -> MockMessenger {
- MockMessenger {
- sent_messages: RefCell::new(vec![]),
- }
- }
- }
-
- impl Messenger for MockMessenger {
- fn send(&self, message: &str) {
- self.sent_messages.borrow_mut().push(String::from(message));
- }
- }
-
- #[test]
- fn it_sends_an_over_75_percent_warning_message() {
- // --snip--
- let mock_messenger = MockMessenger::new();
- let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
-
- limit_tracker.set_value(80);
-
- assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
- }
-}
--
Поле sent_messages
теперь имеет вид RefCell<Vec<String>>
вместо Vec<String>
. В функции new
мы создаём новый образец RefCell<Vec<String>>
для пустого вектора.
Для выполнения способа send
первый свойство по-прежнему является неизменяемым для заимствования self
, которое соответствует определению особенности. Мы вызываем borrow_mut
для RefCell<Vec<String>>
в self.sent_messages
, чтобы получить изменяемую ссылку на значение внутри RefCell<Vec<String>>
, которое является вектором. Затем мы можем вызвать push
у изменяемой ссылки на вектор, чтобы отслеживать сообщения, отправленные во время проверки.
Последнее изменение, которое мы должны сделать, заключается в утверждении для проверки: чтобы увидеть, сколько элементов находится во внутреннем векторе, мы вызываем способ borrow
у RefCell<Vec<String>>
, чтобы получить неизменяемую ссылку на внутренний вектор сообщений.
Теперь, когда вы увидели как использовать RefCell<T>
, давайте изучим как он работает!
RefCell<T>
При создании неизменных и изменяемых ссылок мы используем правила написания &
и &mut
соответственно. У вида RefCell<T>
, мы используем способы borrow
и borrow_mut
, которые являются частью безопасного API, который принадлежит RefCell<T>
. Способ borrow
возвращает вид умного указателя Ref<T>
, способ borrow_mut
возвращает вид умного указателя RefMut<T>
. Оба вида выполняют особенность Deref
, поэтому мы можем рассматривать их как обычные ссылки.
Вид RefCell<T>
отслеживает сколько умных указателей Ref<T>
и RefMut<T>
активны в данное время. Каждый раз, когда мы вызываем borrow
, вид RefCell<T>
увеличивает количество активных заимствований. Когда значение Ref<T>
выходит из области видимости, то количество неизменяемых заимствований уменьшается на единицу. Как и с правилами заимствования во время сборки, RefCell<T>
позволяет иметь много неизменяемых заимствований или одно изменяемое заимствование в любой мгновение времени.
Если попытаться нарушить эти правила, то вместо получения ошибки сборщика, как это было бы со ссылками, выполнение RefCell<T>
будет вызывать панику во время выполнения. В приложении 15-23 показана изменение выполнения send
из приложения 15-22. Мы намеренно пытаемся создать два изменяемых заимствования активных для одной и той же области видимости, чтобы показать как RefCell<T>
не позволяет нам делать так во время выполнения.
Файл: src/lib.rs
-pub trait Messenger {
- fn send(&self, msg: &str);
-}
-
-pub struct LimitTracker<'a, T: Messenger> {
- messenger: &'a T,
- value: usize,
- max: usize,
-}
-
-impl<'a, T> LimitTracker<'a, T>
-where
- T: Messenger,
-{
- pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
- LimitTracker {
- messenger,
- value: 0,
- max,
- }
- }
-
- pub fn set_value(&mut self, value: usize) {
- self.value = value;
-
- let percentage_of_max = self.value as f64 / self.max as f64;
-
- if percentage_of_max >= 1.0 {
- self.messenger.send("Error: You are over your quota!");
- } else if percentage_of_max >= 0.9 {
- self.messenger
- .send("Urgent warning: You've used up over 90% of your quota!");
- } else if percentage_of_max >= 0.75 {
- self.messenger
- .send("Warning: You've used up over 75% of your quota!");
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use std::cell::RefCell;
-
- struct MockMessenger {
- sent_messages: RefCell<Vec<String>>,
- }
-
- impl MockMessenger {
- fn new() -> MockMessenger {
- MockMessenger {
- sent_messages: RefCell::new(vec![]),
- }
- }
- }
-
- impl Messenger for MockMessenger {
- fn send(&self, message: &str) {
- let mut one_borrow = self.sent_messages.borrow_mut();
- let mut two_borrow = self.sent_messages.borrow_mut();
-
- one_borrow.push(String::from(message));
- two_borrow.push(String::from(message));
- }
- }
-
- #[test]
- fn it_sends_an_over_75_percent_warning_message() {
- let mock_messenger = MockMessenger::new();
- let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
-
- limit_tracker.set_value(80);
-
- assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
- }
-}
--
Мы создаём переменную one_borrow
для умного указателя RefMut<T>
возвращаемого из способа borrow_mut
. Затем мы создаём другое изменяемое заимствование таким же образом в переменной two_borrow
. Это создаёт две изменяемые ссылки в одной области видимости, что недопустимо. Когда мы запускаем проверки для нашей библиотеки, код в приложении 15-23 собирается без ошибок, но проверка завершится неудачно:
$ cargo test
- Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
- Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
- Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
-
-running 1 test
-test tests::it_sends_an_over_75_percent_warning_message ... FAILED
-
-failures:
-
----- tests::it_sends_an_over_75_percent_warning_message stdout ----
-thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
-already borrowed: BorrowMutError
-note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
-
-
-failures:
- tests::it_sends_an_over_75_percent_warning_message
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
-error: test failed, to rerun pass `--lib`
-
-Обратите внимание, что код вызвал панику с сообщением already borrowed: BorrowMutError
. Вот так вид RefCell<T>
обрабатывает нарушения правил заимствования во время выполнения.
Решение отлавливать ошибки заимствования во время выполнения, а не во время сборки, как мы сделали здесь, означает, что вы возможно будете находить ошибки в своём коде на более поздних этапах разработки: возможно, не раньше, чем ваш код будет развернут в рабочем окружении. Кроме того, ваш код будет иметь небольшие потери производительности в этапе работы, поскольку заимствования будут отслеживаться во время выполнения, а не во время сборки. Однако использование RefCell<T>
позволяет написать предмет-имитатор, который способен изменять себя, чтобы сохранять сведения о тех значениях, которые он получал, пока вы использовали его в среде, где разрешены только неизменяемые значения. Вы можете использовать RefCell<T>
, несмотря на его недостатки, чтобы получить больше возможности, чем дают обычные ссылки.
Rc<T>
и RefCell<T>
Обычный способ использования RefCell<T>
заключается в его сочетании с видом Rc<T>
. Напомним, что вид Rc<T>
позволяет иметь нескольких владельцев некоторых данных, но даёт только неизменяемый доступ к этим данным. Если у вас есть Rc<T>
, который внутри содержит вид RefCell<T>
, вы можете получить значение, которое может иметь несколько владельцев и которое можно изменять!
Например, вспомните пример cons списка приложения 15-18, где мы использовали Rc<T>
, чтобы несколько списков могли совместно владеть другим списком. Поскольку Rc<T>
содержит только неизменяемые значения, мы не можем изменить ни одно из значений в списке после того, как мы их создали. Давайте добавим вид RefCell<T>
, чтобы получить возможность изменять значения в списках. В приложении 15-24 показано использование RefCell<T>
в определении Cons
так, что мы можем изменить значение хранящееся во всех списках:
Файл: src/main.rs
--#[derive(Debug)] -enum List { - Cons(Rc<RefCell<i32>>, Rc<List>), - Nil, -} - -use crate::List::{Cons, Nil}; -use std::cell::RefCell; -use std::rc::Rc; - -fn main() { - let value = Rc::new(RefCell::new(5)); - - let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); - - let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); - let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); - - *value.borrow_mut() += 10; - - println!("a after = {a:?}"); - println!("b after = {b:?}"); - println!("c after = {c:?}"); -}
-
Мы создаём значение, которое является образцом Rc<RefCell<i32>>
и сохраняем его в переменной с именем value
, чтобы получить к ней прямой доступ позже. Затем мы создаём List
в переменной a
с исходом Cons
, который содержит value
. Нам нужно вызвать клонирование value
, так как обе переменные a
и value
владеют внутренним значением 5
, а не передают владение из value
в переменную a
или не выполняют заимствование с помощью a
переменной value
.
Мы оборачиваем список у переменной a
в вид Rc<T>
, поэтому при создании списков в переменные b
и c
они оба могут ссылаться на a
, что мы и сделали в приложении 15-18.
После создания списков a
, b
и c
мы хотим добавить 10 к значению в value
. Для этого вызовем borrow_mut
у value
, который использует функцию самостоятельного разыменования, о которой мы говорили в главе 5 (см. раздел "Где находится оператор ->
?") во внутреннее значение RefCell<T>
. Способ borrow_mut
возвращает умный указатель RefMut<T>
, и мы используя оператор разыменования, изменяем внутреннее значение.
Когда мы печатаем a
, b
и c
то видим, что все они имеют изменённое значение равное 15, а не 5:
$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
- Running `target/debug/cons-list`
-a after = Cons(RefCell { value: 15 }, Nil)
-b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
-c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
-
-Эта техника довольно изящна! Используя RefCell<T>
, мы получаем внешне неизменяемое значение List
. Но мы можем использовать способы RefCell<T>
, которые предоставляют доступ к его внутренностям, чтобы мы могли изменять наши данные, когда это необходимо. Проверка правил заимствования во время выполнения защищает нас от гонок данных, и иногда стоит немного пожертвовать производительностью ради такой гибкости наших устройств данных. Обратите внимание, что RefCell<T>
не работает для многопоточного кода! Mutex<T>
- это thread-safe исполнение RefCell<T>
, а Mutex<T>
мы обсудим в главе 16.
Заверения безопасности памяти в Ржавчина затрудняют, но не делают невозможным случайное выделение памяти, которое никогда не очищается (известное как утечка памяти ). Полное предотвращение утечек памяти не является одной из заверений Rust, а это означает, что утечки памяти безопасны в Rust. Мы видим, что Ржавчина допускает утечку памяти с помощью Rc<T>
и RefCell<T>
: можно создавать ссылки, в которых элементы ссылаются друг на друга в цикле. Это создаёт утечки памяти, потому что счётчик ссылок каждого элемента в цикле никогда не достигнет 0, а значения никогда не будут удалены.
Давайте посмотрим, как может произойти случаей ссылочного замыкания и как её предотвратить, начиная с определения перечисления List
и способа tail
в приложении 15-25:
Файл: src/main.rs
--use crate::List::{Cons, Nil}; -use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Debug)] -enum List { - Cons(i32, RefCell<Rc<List>>), - Nil, -} - -impl List { - fn tail(&self) -> Option<&RefCell<Rc<List>>> { - match self { - Cons(_, item) => Some(item), - Nil => None, - } - } -} - -fn main() {}
-
Мы используем другую вариацию определения List
из приложения 15-5. Второй элемент в исходе Cons
теперь RefCell<Rc<List>>
, что означает, что вместо возможности менять значение i32
, как мы делали в приложении 15-24, мы хотим менять значение List
, на которое указывает исход Cons
. Мы также добавляем способ tail
, чтобы нам было удобно обращаться ко второму элементу, если у нас есть исход Cons
.
В приложении 15-26 мы добавляем main
функцию, которая использует определения приложения 15-25. Этот код создаёт список в переменной a
и список b
, который указывает на список a
. Затем он изменяет список внутри a
так, чтобы он указывал на b
, создавая ссылочное замыкание. В коде есть указания println!
, чтобы показать значения счётчиков ссылок в различных точках этого этапа.
Файл: src/main.rs
--use crate::List::{Cons, Nil}; -use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Debug)] -enum List { - Cons(i32, RefCell<Rc<List>>), - Nil, -} - -impl List { - fn tail(&self) -> Option<&RefCell<Rc<List>>> { - match self { - Cons(_, item) => Some(item), - Nil => None, - } - } -} - -fn main() { - let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); - - println!("a initial rc count = {}", Rc::strong_count(&a)); - println!("a next item = {:?}", a.tail()); - - let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); - - println!("a rc count after b creation = {}", Rc::strong_count(&a)); - println!("b initial rc count = {}", Rc::strong_count(&b)); - println!("b next item = {:?}", b.tail()); - - if let Some(link) = a.tail() { - *link.borrow_mut() = Rc::clone(&b); - } - - println!("b rc count after changing a = {}", Rc::strong_count(&b)); - println!("a rc count after changing a = {}", Rc::strong_count(&a)); - - // Uncomment the next line to see that we have a cycle; - // it will overflow the stack - // println!("a next item = {:?}", a.tail()); -}
-
Мы создаём образец Rc<List>
содержащий значение List
в переменной a
с начальным списком 5, Nil
. Затем мы создаём образец Rc<List>
содержащий другое значение List
в переменной b
, которое содержит значение 10 и указывает на список в a
.
Мы меняем a
так, чтобы он указывал на b
вместо Nil
, создавая зацикленность. Мы делаем это с помощью способа tail
, чтобы получить ссылку на RefCell<Rc<List>>
из переменной a
, которую мы помещаем в переменную link
. Затем мы используем способ borrow_mut
из вида RefCell<Rc<List>>
, чтобы изменить внутреннее значение вида Rc<List>
, содержащего начальное значение Nil
на значение вида Rc<List>
взятое из переменной b
.
Когда мы запускаем этот код, оставив последний println!
с примечаниями в данный мгновение, мы получим вывод:
$ cargo run
- Compiling cons-list v0.1.0 (file:///projects/cons-list)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
- Running `target/debug/cons-list`
-a initial rc count = 1
-a next item = Some(RefCell { value: Nil })
-a rc count after b creation = 2
-b initial rc count = 1
-b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
-b rc count after changing a = 2
-a rc count after changing a = 2
-
-Количество ссылок на образцы Rc<List>
как в a
, так и в b
равно 2 после того, как мы заменили список в a
на ссылку на b
. В конце main
Ржавчина уничтожает переменную b
, что уменьшает количество ссылок на Rc<List>
из b
с 2 до 1. Память, которую Rc<List>
занимает в куче, не будет освобождена в этот мгновение, потому что количество ссылок на неё равно 1, а не 0. Затем Ржавчина удаляет a
, что уменьшает количество ссылок образца Rc<List>
в a
с 2 до 1. Память этого образца также не может быть освобождена, поскольку другой образец Rc<List>
по-прежнему ссылается на него. Таким образом, память, выделенная для списка не будет освобождена никогда. Чтобы наглядно представить этот цикл ссылок, мы создали диаграмму на рисунке 15-4.
-
Если вы удалите последний примечание с println!
и запустите программу, Ржавчина будет пытаться печатать зацикленность в a
, указывающей на b
, указывающей на a
и так далее, пока не переполниться обойма.
По сравнению с существующей программой, последствия создания цикла ссылок в этом примере не так страшны: сразу после создания цикла ссылок программа завершается. Однако если более сложная программа выделит много памяти в цикле и будет удерживать её в течение длительного времени, программа будет потреблять больше памяти, чем ей нужно, и может перенапрячь систему, что приведёт к исчерпанию доступной памяти.
-Вызвать образование ссылочной зацикленности не просто, но и не невозможно. Если у вас есть значения RefCell<T>
которые содержат значения Rc<T>
или подобные вложенные сочетания видов с внутренней изменчивостью и подсчётом ссылок, вы должны убедиться, что вы не создаёте зацикленность; Вы не можете полагаться на то, что Ржавчина их обнаружит. Создание ссылочной зацикленности являлось бы логической ошибкой в программе, для которой вы должны использовать самостоятельно е проверки, проверку кода и другие опытов разработки программного обеспечения для её уменьшения.
Другое решение для избежания ссылочной зацикленности - это ресоздание ваших устройств данных, чтобы некоторые ссылки выражали владение, а другие - отсутствие владения. В итоге можно иметь циклы, построенные на некоторых отношениях владения и некоторые не основанные на отношениях владения, тогда только отношения владения влияют на то, можно ли удалить значение. В приложении 15-25 мы всегда хотим, чтобы исходы Cons
владели своим списком, поэтому ресоздание устройства данных невозможна. Давайте рассмотрим пример с использованием графов, состоящих из родительских и дочерних узлов, чтобы увидеть, когда отношения владения не являются подходящим способом предотвращения ссылочной зацикленности.
Rc<T>
на Weak<T>
До сих пор мы выясняли, что вызов Rc::clone
увеличивает strong_count
образца Rc<T>
, а образец Rc<T>
удаляется, только если его strong_count
равен 0. Вы также можете создать слабую ссылку на значение внутри образца Rc<T>
, вызвав Rc::downgrade
и передав ссылку на Rc<T>
. Сильные ссылки - это то с помощью чего вы можете поделиться владением образца Rc<T>
. Слабые ссылки не отражают связи владения, и их подсчёт не влияет на то, когда образец Rc<T>
будет очищен. Они не приведут к ссылочному циклу, потому что любой цикл, включающий несколько слабых ссылок, будет разорван, как только количество сильных ссылок для задействованных значений станет равным 0.
Когда вы вызываете Rc::downgrade
, вы получаете умный указатель вида Weak<T>
. Вместо того чтобы увеличить strong_count
в образце Rc<T>
на 1, вызов Rc::downgrade
увеличивает weak_count
на 1. Вид Rc<T>
использует weak_count
для отслеживания количества существующих ссылок Weak<T>
, подобно strong_count
. Разница в том, что weak_count
не должен быть равен 0, чтобы образец Rc<T>
мог быть удалён.
Поскольку значение, на которое ссылается Weak<T>
могло быть удалено, то необходимо убедиться, что это значение все ещё существует, чтобы сделать что-либо со значением на которое указывает Weak<T>
. Делайте это вызывая способ upgrade
у образца вида Weak<T>
, который вернёт Option<Rc<T>>
. Вы получите итог Some
, если значение Rc<T>
ещё не было удалено и итог None
, если значение Rc<T>
было удалено. Поскольку upgrade
возвращает вид Option<T>
, Ржавчина обеспечит обработку обоих случаев Some
и None
и не будет неправильного указателя.
В качестве примера, вместо того чтобы использовать список чей элемент знает только о следующем элементе, мы создадим дерево, чьи элементы знают о своих дочерних элементах и о своих родительских элементах.
-Node
с дочерними узламиДля начала мы построим дерево с узлами, которые знают о своих дочерних узлах. Мы создадим устройство с именем Node
, которая будет содержать собственное значение i32
, а также ссылки на его дочерние значения Node
:
Файл: src/main.rs
--use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Debug)] -struct Node { - value: i32, - children: RefCell<Vec<Rc<Node>>>, -} - -fn main() { - let leaf = Rc::new(Node { - value: 3, - children: RefCell::new(vec![]), - }); - - let branch = Rc::new(Node { - value: 5, - children: RefCell::new(vec![Rc::clone(&leaf)]), - }); -}
Мы хотим, чтобы Node
владел своими дочерними узлами и мы хотим поделиться этим владением с переменными так, чтобы мы могли напрямую обращаться к каждому Node
в дереве. Для этого мы определяем внутренние элементы вида Vec<T>
как значения вида Rc<Node>
. Мы также хотим изменять те узлы, которые являются дочерними по отношению к другому узлу, поэтому у нас есть вид RefCell<T>
в поле children
оборачивающий вид Vec<Rc<Node>>
.
Далее мы будем использовать наше определение устройства и создадим один образец Node
с именем leaf
со значением 3 и без дочерних элементов, а другой образец с именем branch
со значением 5 и leaf
в качестве одного из его дочерних элементов, как показано в приложении 15-27:
Файл: src/main.rs
--use std::cell::RefCell; -use std::rc::Rc; - -#[derive(Debug)] -struct Node { - value: i32, - children: RefCell<Vec<Rc<Node>>>, -} - -fn main() { - let leaf = Rc::new(Node { - value: 3, - children: RefCell::new(vec![]), - }); - - let branch = Rc::new(Node { - value: 5, - children: RefCell::new(vec![Rc::clone(&leaf)]), - }); -}
-
Мы клонируем содержимое Rc<Node>
из переменной leaf
и сохраняем его в переменной branch
, что означает, что Node
в leaf
теперь имеет двух владельцев: leaf
и branch
. Мы можем получить доступ из branch
к leaf
через обращение branch.children
, но нет способа добраться из leaf
к branch
. Причина в том, что leaf
не имеет ссылки на branch
и не знает, что они связаны. Мы хотим, чтобы leaf
знал, что branch
является его родителем. Мы сделаем это далее.
Для того, чтобы дочерний узел знал о своём родительском узле нужно добавить поле parent
в наше определение устройства Node
. Неполадкав том, чтобы решить, каким должен быть вид parent
. Мы знаем, что он не может содержать Rc<T>
, потому что это создаст ссылочную зацикленность с leaf.parent
указывающей на branch
и branch.children
, указывающей на leaf
, что приведёт к тому, что их значения strong_count
никогда не будут равны 0.
Подумаем об этих отношениях по-другому, родительский узел должен владеть своими потомками: если родительский узел удаляется, его дочерние узлы также должны быть удалены. Однако дочерний элемент не должен владеть своим родителем: если мы удаляем дочерний узел то родительский элемент все равно должен существовать. Это случай для использования слабых ссылок!
-Поэтому вместо Rc<T>
мы сделаем так, чтобы поле parent
использовало вид Weak<T>
, а именно RefCell<Weak<Node>>
. Теперь наше определение устройства Node
выглядит так:
Файл: src/main.rs
--use std::cell::RefCell; -use std::rc::{Rc, Weak}; - -#[derive(Debug)] -struct Node { - value: i32, - parent: RefCell<Weak<Node>>, - children: RefCell<Vec<Rc<Node>>>, -} - -fn main() { - let leaf = Rc::new(Node { - value: 3, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![]), - }); - - println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); - - let branch = Rc::new(Node { - value: 5, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![Rc::clone(&leaf)]), - }); - - *leaf.parent.borrow_mut() = Rc::downgrade(&branch); - - println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); -}
Узел сможет ссылаться на свой родительский узел, но не владеет своим родителем. В приложении 15-28 мы обновляем main
на использование нового определения так, чтобы у узла leaf
был бы способ ссылаться на его родительский узел branch
:
Файл: src/main.rs
--use std::cell::RefCell; -use std::rc::{Rc, Weak}; - -#[derive(Debug)] -struct Node { - value: i32, - parent: RefCell<Weak<Node>>, - children: RefCell<Vec<Rc<Node>>>, -} - -fn main() { - let leaf = Rc::new(Node { - value: 3, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![]), - }); - - println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); - - let branch = Rc::new(Node { - value: 5, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![Rc::clone(&leaf)]), - }); - - *leaf.parent.borrow_mut() = Rc::downgrade(&branch); - - println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); -}
-
Создание узла leaf
выглядит подобно примеру из Приложения 15-27, за исключением поля parent
: leaf
изначально не имеет родителя, поэтому мы создаём новый, пустой образец ссылки Weak<Node>
.
На этом этапе, когда мы пытаемся получить ссылку на родительский узел у узла leaf
с помощью способа upgrade
, мы получаем значение None
. Мы видим это в выводе первой указания println!
:
leaf parent = None
-
-Когда мы создаём узел branch
у него также будет новая ссылка вида Weak<Node>
в поле parent
, потому что узел branch
не имеет своего родительского узла. У нас все ещё есть leaf
как один из потомков узла branch
. Когда мы получили образец Node
в переменной branch
, мы можем изменить переменную leaf
чтобы дать ей Weak<Node>
ссылку на её родителя. Мы используем способ borrow_mut
у вида RefCell<Weak<Node>>
поля parent
у leaf
, а затем используем функцию Rc::downgrade
для создания Weak<Node>
ссылки на branch
из Rc<Node>
в branch
.
Когда мы снова напечатаем родителя leaf
то в этот раз мы получим исход Some
содержащий branch
, теперь leaf
может получить доступ к своему родителю! Когда мы печатаем leaf
, мы также избегаем цикла, который в конечном итоге заканчивался переполнением обоймы, как в приложении 15-26; ссылки вида Weak<Node>
печатаются как (Weak)
:
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
-children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
-children: RefCell { value: [] } }] } })
-
-Отсутствие бесконечного вывода означает, что этот код не создал ссылочной зацикленности. Мы также можем сказать это, посмотрев на значения, которые мы получаем при вызове Rc::strong_count
и Rc::weak_count
.
strong_count
и weak_count
Давайте посмотрим, как изменяются значения strong_count
и weak_count
образцов вида Rc<Node>
с помощью создания новой внутренней области видимости и перемещая создания образца branch
в эту область. Таким образом можно увидеть, что происходит, когда branch
создаётся и затем удаляется при выходе из области видимости. Изменения показаны в приложении 15-29:
Файл: src/main.rs
--use std::cell::RefCell; -use std::rc::{Rc, Weak}; - -#[derive(Debug)] -struct Node { - value: i32, - parent: RefCell<Weak<Node>>, - children: RefCell<Vec<Rc<Node>>>, -} - -fn main() { - let leaf = Rc::new(Node { - value: 3, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![]), - }); - - println!( - "leaf strong = {}, weak = {}", - Rc::strong_count(&leaf), - Rc::weak_count(&leaf), - ); - - { - let branch = Rc::new(Node { - value: 5, - parent: RefCell::new(Weak::new()), - children: RefCell::new(vec![Rc::clone(&leaf)]), - }); - - *leaf.parent.borrow_mut() = Rc::downgrade(&branch); - - println!( - "branch strong = {}, weak = {}", - Rc::strong_count(&branch), - Rc::weak_count(&branch), - ); - - println!( - "leaf strong = {}, weak = {}", - Rc::strong_count(&leaf), - Rc::weak_count(&leaf), - ); - } - - println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); - println!( - "leaf strong = {}, weak = {}", - Rc::strong_count(&leaf), - Rc::weak_count(&leaf), - ); -}
-
После того, как leaf
создан его Rc<Node>
имеет значения strong count равное 1 и weak count равное 0. Во внутренней области мы создаём branch
и связываем её с leaf
, после чего при печати значений счётчиков Rc<Node>
в branch
они будет иметь strong count 1 и weak count 1 (для leaf.parent
указывающего на branch
с Weak<Node>
). Когда мы распечатаем счётчики из leaf
, мы увидим, что они будут иметь strong count 2, потому что branch
теперь имеет клон Rc<Node>
переменной leaf
хранящийся в branch.children
, но все равно будет иметь weak count 0.
Когда заканчивается внутренняя область видимости, branch
выходит из области видимости и strong count Rc<Node>
уменьшается до 0, поэтому его Node
удаляется. Weak count 1 из leaf.parent
не имеет никакого отношения к тому, был ли Node
удалён, поэтому не будет никаких утечек памяти!
Если мы попытаемся получить доступ к родителю переменной leaf
после окончания области видимости, мы снова получим значение None
. В конце программы Rc<Node>
внутри leaf
имеет strong count 1 и weak count 0 потому что переменная leaf
снова является единственной ссылкой на Rc<Node>
.
Вся логика, которая управляет счётчиками и сбросом их значений, встроена внутри Rc<T>
и Weak<T>
и их выполнений особенности Drop
. Указав, что отношение из дочернего к родительскому элементу должно быть ссылкой вида Weak<T>
в определении Node
, делает возможным иметь родительские узлы, указывающие на дочерние узлы и наоборот, не создавая ссылочной зацикленности и утечек памяти.
В этой главе рассказано как использовать умные указатели для обеспечения различных заверений и соглашений по сравнению с обычными ссылками, которые Ржавчина использует по умолчанию. Вид Box<T>
имеет известный размер и указывает на данные размещённые в куче. Вид Rc<T>
отслеживает количество ссылок на данные в куче, поэтому данные могут иметь несколько владельцев. Вид RefCell<T>
с его внутренней изменяемостью предоставляет вид, который можно использовать при необходимости неизменного вида, но необходимости изменить внутреннее значение этого типа; он также обеспечивает соблюдение правил заимствования во время выполнения, а не во время сборки.
Мы обсудили также особенности Deref
и Drop
, которые обеспечивают большую возможность умных указателей. Мы исследовали ссылочную зацикленность, которая может вызывать утечки памяти и как это предотвратить с помощью вида Weak<T>
.
Если эта глава вызвала у вас влечение и вы хотите выполнить свои собственные умные указатели, обратитесь к "The Rustonomicon" за более полезной сведениями.
-Далее мы поговорим о одновременности в Rust. Вы даже узнаете о нескольких новых умных указателях.
- -Безопасное и эффективное управление многопоточным программированием — ещё одна из основных целей Rust. Многопоточное программирование, когда разные части программы выполняются независимо, и одновременное программирование, когда разные части программы выполняются одновременно, становятся всё более важными, поскольку всё больше компьютеров используют преимущества нескольких процессоров. Исторически программирование в этих условиях было сложным и подверженным ошибкам: Ржавчина надеется изменить это.
-Первоначально приказ Ржавчина считала, что обеспечение безопасности памяти и предотвращение неполадок многопоточности — это две отдельные сбоев, которые необходимо решать различными способами. Со временем приказ обнаружила, что системы владения и система видов являются мощным набором средств, помогающих управлять безопасностью памяти и неполадками многопоточного одновременности! Используя владение и проверку видов, многие ошибки многопоточности являются ошибками времени сборки в Rust, а не ошибками времени выполнения. Поэтому вместо того, чтобы тратить много времени на попытки воспроизвести точные обстоятельства, при которых возникает ошибка многопоточности во время выполнения, неправильный код будет отклонён с ошибкой. В итоге вы можете исправить свой код во время работы над ним, а не после развёртывания на рабочем сервере. Мы назвали этот особенность Ржавчина бесстрашной многопоточностью. Бесстрашная многопоточность позволяет вам писать код, который не содержит скрытых ошибок и легко ресогласуется без внесения новых.
---Примечание: для простоты мы будем называть многие сбоев многопоточными, хотя более точный понятие здесь — многопоточные и/или одновременные. Если бы эта книга была о многопоточности и/или одновременности, мы были бы более определены. В этой главе, пожалуйста, всякий раз, когда мы используем понятие «многопоточный», мысленно замените на понятие «многопоточный и/или одновременный».
-
Многие языки предлагают довольно устоявшиеся решения неполадок многопоточности. Например, Erlang обладает элегантной возможностью для многопоточности при передаче сообщений, но не определяет ясных способов совместного использования состояния между потоками. Поддержка только подмножества возможных решений является разумной подходом для языков более высокого уровня, поскольку язык более высокого уровня обещает выгоду при отказе от некоторого управления над получением абстракций. Однако ожидается, что языки низкого уровня обеспечат решение с наилучшей производительностью в любой именно случаи и будут иметь меньше абстракций по сравнению с аппаратным обеспечением. Поэтому Ржавчина предлагает множество средств для расчетов неполадок любым способом, который подходит для вашей случаи и требований.
-Вот темы, которые мы рассмотрим в этой главе:
-Sync
и Send
, которые расширяют заверения многопоточности в Ржавчина для пользовательских видов, а также видов, предоставляемых встроенной библиотекойВ большинстве современных операционных систем программный код выполняется в виде этапа, причём операционная система способна управлять несколькими этапами сразу. Программа, в свою очередь, может состоять из нескольких независимых частей, выполняемых одновременно. Устройство, благодаря которой эти независимые части выполняются, называется потоком. Например, веб-сервер может иметь несколько потоков для того, чтобы он мог обрабатывать больше одного запроса за раз.
-Разбиение вычислений на несколько потоков может повысить производительность программы, поскольку программа выполняет несколько задач одновременно, но такое разбиение также добавляет сложности. Поскольку потоки могут работать одновременно, нет чёткой заверения, определяющей порядок выполнения частей вашего кода в разных потоках. Это может привести к таким неполадкам, как:
-Rust пытается смягчить отрицательные последствия использования потоков, но программирование в многопоточном среде все ещё требует тщательного обдумывания устройства кода, которая отличается от устройства кода программ, работающих в одном потоке.
-Языки программирования выполняют потоки несколькими различными способами, и многие операционные системы предоставляют API, который язык может вызывать для создания новых потоков. Обычная библиотека Ржавчина использует прообраз выполнения потоков 1:1, при которой одному потоку операционной системы соответствует ровно один "языковой" поток. Существуют ящики, в которых выполнены другие подходы многопоточности, отличающиеся от подходы 1:1.
-spawn
Чтобы создать новый поток, мы вызываем функцию thread::spawn
и передаём ей замыкание (мы говорили о замыканиях в главе 13), содержащее код, который мы хотим запустить в новом потоке. Пример в приложении 16-1 печатает некоторый текст из основного потока, а также другой текст из нового потока:
Файл: src/main.rs
--use std::thread; -use std::time::Duration; - -fn main() { - thread::spawn(|| { - for i in 1..10 { - println!("hi number {i} from the spawned thread!"); - thread::sleep(Duration::from_millis(1)); - } - }); - - for i in 1..5 { - println!("hi number {i} from the main thread!"); - thread::sleep(Duration::from_millis(1)); - } -}
-
Обратите внимание, что когда основной поток программы на Ржавчина завершается, все порождённые потоки закрываются, независимо от того, завершили они работу или нет. Вывод этой программы может каждый раз немного отличаться, но он будет выглядеть примерно так:
- -hi number 1 from the main thread!
-hi number 1 from the spawned thread!
-hi number 2 from the main thread!
-hi number 2 from the spawned thread!
-hi number 3 from the main thread!
-hi number 3 from the spawned thread!
-hi number 4 from the main thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-
-Вызовы thread::sleep
заставляют поток на короткое время останавливать своё выполнение, позволяя выполняться другим потокам. Очерёдность выполнения потоков вероятно будет меняться, но это не обязательно: это зависит от того, как ваша операционная система расчитывает потоки. В этом цикле основной поток печатает первым, несмотря на то, что указание печати из порождённого потока появляется раньше в коде. И даже несмотря на то, что мы указали порождённый поток печатать до тех пор, пока значение i
не достигнет числа 9, оно успело дойти только до 5, когда основной поток завершился.
Если вы запустите этот код и увидите вывод только из основного потока или не увидите печати из других потоков, попробуйте увеличить числа в рядах, чтобы дать операционной системе больше возможностей для переключения между потоками.
-join
Код в приложении 16-1 преждевременно останавливает порождённый поток в большинстве случаев, из-за завершения основного потока. Более того, так как порядок выполнения потоков чётко не определён, этот код не даёт заверения, что порождённый поток вообще начнёт исполняться!
-Мы можем исправить неполадку, когда созданный поток не запускается или завершается преждевременно, сохранив возвращаемое значение thread::spawn
в какой-либо переменной. Вид возвращаемого значения thread::spawn
— JoinHandle
. JoinHandle
— это владеющее значение, которое, при вызове способа join
, будет ждать завершения своего потока. Приложение 16-2 отображает, как использовать JoinHandle
потока, созданного в приложении 16-1, и вызывать функцию join
, для того, чтобы убедиться, что порождённый поток завершится раньше, чем поток main
:
Файл: src/main.rs
--use std::thread; -use std::time::Duration; - -fn main() { - let handle = thread::spawn(|| { - for i in 1..10 { - println!("hi number {i} from the spawned thread!"); - thread::sleep(Duration::from_millis(1)); - } - }); - - for i in 1..5 { - println!("hi number {i} from the main thread!"); - thread::sleep(Duration::from_millis(1)); - } - - handle.join().unwrap(); -}
-
Вызов join
у указателя блокирует текущий поток, пока поток, представленный указателем не завершится. Блокировка потока означает, что потоку запрещено выполнять работу или выходить из него. Поскольку мы помеисполнения вызов join
после цикла for
основного потока, выполнение приложения 16-2 должно привести к выводу, подобному следующему:
hi number 1 from the main thread!
-hi number 2 from the main thread!
-hi number 1 from the spawned thread!
-hi number 3 from the main thread!
-hi number 2 from the spawned thread!
-hi number 4 from the main thread!
-hi number 3 from the spawned thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-hi number 6 from the spawned thread!
-hi number 7 from the spawned thread!
-hi number 8 from the spawned thread!
-hi number 9 from the spawned thread!
-
-Два потока продолжают чередоваться, но основной поток находится в ожидании из-за вызова handle.join()
и не завершается до тех пор, пока не завершится запущенный поток.
Но давайте посмотрим, что произойдёт, если мы вместо этого переместим handle.join()
перед циклом for
в main
, например так:
Файл: src/main.rs
--use std::thread; -use std::time::Duration; - -fn main() { - let handle = thread::spawn(|| { - for i in 1..10 { - println!("hi number {i} from the spawned thread!"); - thread::sleep(Duration::from_millis(1)); - } - }); - - handle.join().unwrap(); - - for i in 1..5 { - println!("hi number {i} from the main thread!"); - thread::sleep(Duration::from_millis(1)); - } -}
Основной поток будет ждать завершения порождённого потока, а затем запустит свой цикл for
, поэтому выходные данные больше не будут чередоваться, как показано ниже:
hi number 1 from the spawned thread!
-hi number 2 from the spawned thread!
-hi number 3 from the spawned thread!
-hi number 4 from the spawned thread!
-hi number 5 from the spawned thread!
-hi number 6 from the spawned thread!
-hi number 7 from the spawned thread!
-hi number 8 from the spawned thread!
-hi number 9 from the spawned thread!
-hi number 1 from the main thread!
-hi number 2 from the main thread!
-hi number 3 from the main thread!
-hi number 4 from the main thread!
-
-Небольшие подробности, такие как место вызова join
, могут повлиять на то, выполняются ли ваши потоки одновременно.
move
-замыканий в потокахМы часто используем ключевое слово move
с замыканиями, переданными в thread::spawn
, потому что в этом случае замыкание получает из окружения права владения на используемые им значения, таким образом передавая права владения этими значениями от одного потока к другому. В разделе "Захват ссылок или перемещение прав владения" главы 13 мы обсудили move
в среде замыканий. Теперь мы сосредоточимся на взаимодействии между move
и thread::spawn
.
Обратите внимание, что в приложении 16-1 замыкание, которое мы передаём в thread::spawn
не принимает переменных: мы не используем никаких данных из основного потока в коде порождённого потока. Чтобы использовать данные из основного потока в порождённом потоке, замыкание порождённого потока должно захватывать значения, которые ему необходимы. Приложение 16-3 показывает попытку создать вектор в главном потоке и использовать его в порождённом потоке. Тем не менее, это не будет работать, как вы увидите через мгновение.
Файл: src/main.rs
-use std::thread;
-
-fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(|| {
- println!("Here's a vector: {v:?}");
- });
-
- handle.join().unwrap();
-}
--
Замыкание использует переменную v
, поэтому оно захватит v
и сделает его частью окружения замыкания. Поскольку thread::spawn
запускает это замыкание в новом потоке, мы должны иметь доступ к v
внутри этого нового потока. Но при сборки этого примера, мы получаем следующую ошибку:
$ cargo run
- Compiling threads v0.1.0 (file:///projects/threads)
-error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
- --> src/main.rs:6:32
- |
-6 | let handle = thread::spawn(|| {
- | ^^ may outlive borrowed value `v`
-7 | println!("Here's a vector: {v:?}");
- | - `v` is borrowed here
- |
-note: function requires argument type to outlive `'static`
- --> src/main.rs:6:18
- |
-6 | let handle = thread::spawn(|| {
- | __________________^
-7 | | println!("Here's a vector: {v:?}");
-8 | | });
- | |______^
-help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
- |
-6 | let handle = thread::spawn(move || {
- | ++++
-
-For more information about this error, try `rustc --explain E0373`.
-error: could not compile `threads` (bin "threads") due to 1 previous error
-
-Rust выводит как захватить v
и так как в println!
нужна только ссылка на v
, то замыкание пытается заимствовать v
. Однако есть неполадка: Ржавчина не может определить, как долго будет работать порождённый поток, поэтому он не знает, будет ли всегда действительной ссылка на v
.
В приложении 16-4 приведён сценарий, который с большей вероятностью будет иметь ссылку на v
, что будет недопустимо:
Файл: src/main.rs
-use std::thread;
-
-fn main() {
- let v = vec![1, 2, 3];
-
- let handle = thread::spawn(|| {
- println!("Here's a vector: {v:?}");
- });
-
- drop(v); // oh no!
-
- handle.join().unwrap();
-}
--
Если бы Ржавчина позволил нам запустить этот код, есть вероятность, что порождённый поток был бы немедленно переведён в фоновый режим, не выполнив ничего. Порождённый поток имеет ссылку на v
, но основной поток немедленно удаляет v
, используя функцию drop
, которую мы обсуждали в главе 15. Затем, когда порождённый поток начинает выполняться, v
уже не существует, поэтому ссылка на него также будет недействительной. О, нет!
Чтобы исправить ошибку сборщика в приложении 16-3, мы можем использовать совет из сообщения об ошибке:
- -help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
- |
-6 | let handle = thread::spawn(move || {
- | ++++
-
-Добавляя ключевое слово move
перед замыканием, мы заставляем замыкание забирать используемые значения во владение, вместо того, чтобы позволить Ржавчина вывести необходимость заимствования значения. Изменение Приложения 16-3, показанная в Приложении 16-5, будет собрана и запущена так, как мы ожидаем:
Файл: src/main.rs
--use std::thread; - -fn main() { - let v = vec![1, 2, 3]; - - let handle = thread::spawn(move || { - println!("Here's a vector: {v:?}"); - }); - - handle.join().unwrap(); -}
-
У нас может возникнуть соблазн попробовать то же самое, чтобы исправить код в приложении 16.4, где основной поток вызывал drop
с помощью замыкания move
. Однако это исправление не сработает, потому что то, что пытается сделать приложение 16.4, запрещено по другой причине. Если мы добавим move
к замыканию, мы переместим v
в окружение замыкания и больше не сможем вызывать для него drop
в основном потоке. Вместо этого мы получим эту ошибку сборщика:
$ cargo run
- Compiling threads v0.1.0 (file:///projects/threads)
-error[E0382]: use of moved value: `v`
- --> src/main.rs:10:10
- |
-4 | let v = vec![1, 2, 3];
- | - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
-5 |
-6 | let handle = thread::spawn(move || {
- | ------- value moved into closure here
-7 | println!("Here's a vector: {v:?}");
- | - variable moved due to use in closure
-...
-10 | drop(v); // oh no!
- | ^ value used here after move
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `threads` (bin "threads") due to 1 previous error
-
-Правила владения Ржавчина снова нас спасли! Мы получили ошибку кода из приложения 16-3, потому что Ржавчина был устоявшийся и заимствовал v
только для потока, что означало, что основной поток предположительно может сделать недействительной ссылку на порождённый поток. Сообщив Ржавчина о передаче владения v
в порождаемый поток, мы заверяем Rust, что основной поток больше не будет использовать v
. Если мы изменим Приложение 16-4 таким же образом, то мы нарушаем правила владения при попытке использовать v
в главном потоке. Ключевое слово move
отменяет основное устоявшееся поведение Ржавчина по заимствованию, что не позволяет нам нарушать правила владения.
Имея достаточное понимание потоков и API потоков, давайте посмотрим, что мы можем делать с помощью потоков.
- -Всё большую распространенность для обеспечения безопасной многопоточности набирает способ, называемый передача сообщений. В этом случае потоки или акторы взаимодействуют друг с другом путём отправки сообщений с данными. Мысль этого подхода выражена в слогане из документации языка Go таким образом: «Не стоит передавать сведения с помощью разделяемой памяти; лучше делитесь памятью, передавая сведения».
-Для обеспечения отправки многопоточных сообщений в встроенной библиотеке языка Ржавчина выполнены потоки. Поток в программировании - это общепринятый рычаг, с помощью которого данные из одного потока отправляются другому потоку.
-Вы можете представить поток в программировании как направленное движение воды, например как ручей или реку. Если вы поместите какую-нибудь вещь на воду, например резиновую уточку, она будет плыть вниз по течению до тех пор, пока это течение не кончится.
-Поток состоит из двух половин: передатчика и приёмника. Передатчик — это место вверх по течению, где вы опускаете резиновых уточек в реку, а приёмник — это место, где резиновые уточки оказываются в конце пути. Одна часть вашего кода вызывает способы передатчика с данными, которые вы хотите отправить, а другая часть проверяет принимающую сторону на наличие поступающих сообщений. Поток считается закрытым , если либо передающая, либо принимающая его половина уничтожена.
-Давайте создадим программу, в которой один поток будет порождать значения и отправлять их в поток, а другой поток будет получать значения и распечатывать их. Мы будем отправлять между потоками простые значения, используя поток, чтобы изобразить эту функцию. После того, как вы ознакомитесь с этим способом, вы сможете использовать потоки с любыми потоками, которым необходимо взаимодействовать друг с другом. Это может быть например система чата или система, в которой несколько вычислительных потоков выполняют свою часть расчёта, а затем отправляют эту часть в отдельный поток, который уже агрегирует полученные итоги.
-Сначала в приложении 16-6 мы создадим поток, но не будем ничего с ним делать. Обратите внимание, что этот код ещё не собирается, потому что Ржавчина не может сказать, какой вид значений мы хотим отправить через поток.
-Файл: src/main.rs
-use std::sync::mpsc;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-}
--
Мы создаём новый поток, используя функцию mpsc::channel
; mpsc
означает несколько производителей, один потребитель (multiple producer, single consumer). Коротко, способ которым обычная библиотека Ржавчина выполняет потоки, означает, что поток может иметь несколько отправляющих источников порождающих значения, но только одну принимающую сторону, которая потребляет эти значения. Представьте, что несколько ручьёв втекают в одну большую реку: всё, что плывёт вниз по любому из ручьёв, в конце концов окажется в одной реке. Сейчас мы пока начнём с одного производителя, а когда пример заработает, добавим ещё несколько.
Функция mpsc::channel
возвращает упорядоченный ряд, первый элемент которого является отправляющей стороной (передатчиком), а вторым элементом является принимающая сторона (получатель). Аббревиатуры tx
и rx
привычно используются во многих полях для передатчика и приёмника соответственно, поэтому мы называем соответствующие переменные именно так. Мы используем указанию let
с образцом, который разъединяет упорядоченные ряды; мы обсудим использование образцов в указаниях let
и разъединение в главе 18. А пока знайте, что описанное использование указания let
является удобным способом извлечения частей упорядоченного ряда, возвращаемых mpsc::channel
.
Давайте переместим передающую часть в порождённый поток так, чтобы он отправлял одну строку и чтобы таким образом, порождённый поток связывался с основным потоком, как показано в приложении 16-7. Это похоже на то, как если бы вы помеисполнения резиновую утку в реку вверх по течению или отправили сообщение чата из одного потока в другой.
-Файл: src/main.rs
--use std::sync::mpsc; -use std::thread; - -fn main() { - let (tx, rx) = mpsc::channel(); - - thread::spawn(move || { - let val = String::from("hi"); - tx.send(val).unwrap(); - }); -}
-
Опять же, мы используем thread::spawn
для создания нового потока, а затем используем move
для перемещения tx
в замыкание, чтобы порождённый поток владел tx
. Порождённый поток должен владеть передатчиком, чтобы иметь возможность отправлять сообщения через поток. Передатчик имеет способ send
, который принимает значение, которое мы хотим отправить. Способ send
возвращает вид Result<T, E>
, поэтому, если получатель уже удалён и отправить значение некуда, действие отправки вернёт ошибку. В этом примере мы вызываем unwrap
для паники в случае ошибки. В существующем приложении мы обработали бы эту случай более правильно: вернитесь к главе 9, если хотите ещё раз разобрать стратегии правильной обработки ошибок.
В приложении 16-8 мы получим значение от приёмника в основном потоке. Это похоже на извлечение резиновой уточки из воды в конце реки или получение сообщения в чате.
-Файл: src/main.rs
--use std::sync::mpsc; -use std::thread; - -fn main() { - let (tx, rx) = mpsc::channel(); - - thread::spawn(move || { - let val = String::from("hi"); - tx.send(val).unwrap(); - }); - - let received = rx.recv().unwrap(); - println!("Got: {received}"); -}
-
Получатель имеет два важных способа: recv
и try_recv
. Мы используем recv
, что является сокращением от receive, который блокирует выполнение основного потока и ждёт, пока данные не будут переданы по потоку. Как только значение будет получено, recv
вернёт его в виде Result<T, E>
. Когда поток закроется, recv
вернёт ошибку, чтобы дать понять, что больше никаких сообщений не поступит.
В свою очередь, способ try_recv не блокирует, а сразу возвращает итог Result<T, E>
: значение Ok, содержащее сообщение, если оно доступно или значение Err, если никаких сообщений не поступило. Использование try_recv полезно, если у этого потока есть и другая работа в то время, пока происходит ожидание сообщений: так, мы можем написать цикл, который вызывает try_recv время от времени, обрабатывает сообщение, если оно доступно, а в промежутке выполняет другую работу до того особенности, как вновь будет произведена проверка.
Мы использовали recv
в этом примере для простоты; у нас нет никакой другой работы для основного потока, кроме как ждать сообщений, поэтому блокировка основного потока уместна.
При запуске кода приложения 16-8, мы увидим значение, напечатанное из основного потока:
- -Got: hi
-
-Отлично!
-Правила владения играют жизненно важную значение в отправке сообщений, потому что они помогают писать безопасный многопоточный код. Предотвращение ошибок в многопоточном программировании является преимуществом для размышлений о владении во всех ваших Ржавчина программах. Давайте проведём эксперимент, чтобы показать как потоки и владение действуют совместно для предотвращения неполадок. мы попытаемся использовать значение val
в порождённом потоке после того как отправим его в поток. Попробуйте собрать код в приложении 16-9, чтобы понять, почему этот код не разрешён:
Файл: src/main.rs
-use std::sync::mpsc;
-use std::thread;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- println!("val is {val}");
- });
-
- let received = rx.recv().unwrap();
- println!("Got: {received}");
-}
--
Здесь мы пытаемся напечатать значение val
после того, как отправили его в поток вызвав tx.send
. Разрешить это было бы плохой мыслью: после того, как значение было отправлено в другой поток, текущий поток мог бы изменить или удалить значение, прежде чем мы попытались бы использовать значение снова. Вероятно изменения в другом потоке могут привести к ошибкам или не ожидаемым итогам из-за противоречивых или несуществующих данных. Однако Ржавчина выдаёт нам ошибку, если мы пытаемся собрать код в приложении 16-9:
$ cargo run
- Compiling message-passing v0.1.0 (file:///projects/message-passing)
-error[E0382]: borrow of moved value: `val`
- --> src/main.rs:10:26
- |
-8 | let val = String::from("hi");
- | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
-9 | tx.send(val).unwrap();
- | --- value moved here
-10 | println!("val is {val}");
- | ^^^^^ value borrowed here after move
- |
- = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
-
-Наша ошибка для многопоточности привела к ошибке сборки. Функция send
вступает во владение своим свойствоом и когда значение перемещается, получатель становится владельцем этого свойства. Это останавливает нас от случайного использования значения снова после его отправки; анализатор заимствования проверяет, что все в порядке.
Код в приложении 16-8 собирается и выполняется, но в нем неясно показано то, что два отдельных потока общаются друг с другом через поток. В приложении 16-10 мы внесли некоторые изменения, которые докажут, что код в приложении 16-8 работает одновременно: порождённый поток теперь будет отправлять несколько сообщений и делать паузу на секунду между каждым сообщением.
-Файл: src/main.rs
-use std::sync::mpsc;
-use std::thread;
-use std::time::Duration;
-
-fn main() {
- let (tx, rx) = mpsc::channel();
-
- thread::spawn(move || {
- let vals = vec![
- String::from("hi"),
- String::from("from"),
- String::from("the"),
- String::from("thread"),
- ];
-
- for val in vals {
- tx.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {received}");
- }
-}
--
На этот раз порождённый поток имеет вектор строк, которые мы хотим отправить основному потоку. Мы перебираем их, отправляя каждую строку по отдельности и делаем паузу между ними, вызывая функцию thread::sleep
со значением Duration
равным 1 секунде.
В основном потоке мы больше не вызываем функцию recv
явно: вместо этого мы используем rx
как повторитель . Для каждого полученного значения мы печатаем его. Когда поток будет закрыт, повторение закончится.
При выполнении кода в приложении 16-10 вы должны увидеть следующий вывод с паузой в 1 секунду между каждой строкой:
- -Got: hi
-Got: from
-Got: the
-Got: thread
-
-Поскольку у нас нет кода, который приостанавливает или задерживает цикл for
в основном потоке, мы можем сказать, что основной поток ожидает получения значений из порождённого потока.
Ранее мы упоминали, что mpsc
— это аббревиатура от множество поставщиков, один потребитель . Давайте используем mpsc
в полной мере и расширим код в приложении 16.10, создав несколько потоков, которые отправляют значения одному и тому же получателю. Мы можем сделать это, клонировав передатчик, как показано в приложении 16.11:
Файл: src/main.rs
-use std::sync::mpsc;
-use std::thread;
-use std::time::Duration;
-
-fn main() {
- // --snip--
-
- let (tx, rx) = mpsc::channel();
-
- let tx1 = tx.clone();
- thread::spawn(move || {
- let vals = vec![
- String::from("hi"),
- String::from("from"),
- String::from("the"),
- String::from("thread"),
- ];
-
- for val in vals {
- tx1.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- thread::spawn(move || {
- let vals = vec![
- String::from("more"),
- String::from("messages"),
- String::from("for"),
- String::from("you"),
- ];
-
- for val in vals {
- tx.send(val).unwrap();
- thread::sleep(Duration::from_secs(1));
- }
- });
-
- for received in rx {
- println!("Got: {received}");
- }
-
- // --snip--
-}
--
На этот раз, прежде чем мы создадим первый порождённый поток, мы вызовем функцию clone
на передатчике. В итоге мы получим новый передатчик, который мы сможем передать первому порождённому потоку. Исходный передатчик мы передадим второму порождённому потоку. Это даст нам два потока, каждый из которых отправляет разные сообщения одному получателю.
Когда вы запустите код, вывод должен выглядеть примерно так:
- -Got: hi
-Got: more
-Got: from
-Got: messages
-Got: for
-Got: the
-Got: thread
-Got: you
-
-Вы можете увидеть значения в другом порядке, в зависимости от вашей системы. Именно такое поведение делает одновременность как важным, так и сложным. Если вы поэкспериментируете с thread::sleep
, задавая различные значения переменной в разных потоках, каждый запуск будет более неопределенным и каждый раз будут выводиться разные данные.
Теперь, когда мы посмотрели, как работают потоки, давайте рассмотрим другой способ многопоточности.
- -Передача сообщений — прекрасный способ обработки одновременности, но не единственный. Другим способом может быть доступ нескольких потоков к одним и тем же общим данным. Рассмотрим ещё раз часть слогана из документации по языку Go: «Не стоит передавать сведения с помощью разделяемой памяти».
-Как бы выглядело общение, используя разделяемую память? Кроме того, почему энтузиасты передачи сообщений предостерегают от его использования?
-В каком-то смысле потоки в любом языке программирования похожи на единоличное владение, потому что после передачи значения по потоку вам больше не следует использовать отправленное значение. Многопоточная, совместно используемая память подобна множественному владению: несколько потоков могут одновременно обращаться к одной и той же области памяти. Как вы видели в главе 15, где умные указатели сделали возможным множественное владение, множественное владение может добавить сложность, потому что нужно управлять этими разными владельцами. Система видов Ржавчина и правила владения очень помогают в их правильном управлении. Для примера давайте рассмотрим мьютексы, один из наиболее распространённых многопоточных простейших для разделяемой памяти.
-Mutex - это сокращение от взаимное исключение (mutual exclusion), так как мьютекс позволяет только одному потоку получать доступ к некоторым данным в любой мгновение времени. Для того, чтобы получить доступ к данным в мьютексе, поток должен сначала подать сигнал, что он хочет получить доступ запрашивая блокировку (lock) мьютекса. Блокировка - это устройства данных, являющаяся частью мьютекса, которая отслеживает кто в настоящее время имеет эксклюзивный доступ к данным. Поэтому мьютекс описывается как предмет защищающий данные, которые он хранит через систему блокировки.
-Мьютексы имеют репутацию трудных в использовании, потому что вы должны помнить два правила:
-Для понимания мьютекса, представьте пример из жизни как объединениевое обсуждение на конференции с одним микрофоном. Прежде чем участник дискуссии сможет говорить, он должен спросить или дать сигнал, что он хочет использовать микрофон. Когда он получает микрофон, то может говорить столько, сколько хочет, а затем передаёт микрофон следующему участнику, который попросит дать ему выступить. Если участник дискуссии забудет освободить микрофон, когда закончит с ним, то никто больше не сможет говорить. Если управление общим микрофоном идёт не правильно, то конференция не будет работать как было расчитано наперед!
-Правильное управление мьютексами может быть невероятно сложным и именно поэтому многие люди с энтузиазмом относятся к потокам. Однако, благодаря системе видов и правилам владения в Rust, вы не можете использовать блокировку и разблокировку неправильным образом.
-Mutex<T>
APIДавайте рассмотрим пример использования мьютекса в приложении 16-12 без использования нескольких потоков:
-Файл: src/main.rs
--use std::sync::Mutex; - -fn main() { - let m = Mutex::new(5); - - { - let mut num = m.lock().unwrap(); - *num = 6; - } - - println!("m = {m:?}"); -}
-
Как и во многих других видах, мы создаём Mutex<T>
с помощью сопутствующей функции new
. Чтобы получить доступ к данным внутри мьютекса, мы используем способ lock
для получения блокировки. Этот вызов блокирует выполнение текущего потока, так что он не сможет выполнять никакие действия, до тех пор пока не наступит наша очередь получить блокировку.
Вызов lock
потерпит неудачу, если другой поток, удерживающий блокировку, запаникует. В таком случае никто не сможет получить блокировку, поэтому мы предпочли использовать unwrap
и заставить этот поток паниковать, если мы окажемся в такой случаи.
После получения блокировки мы можем воспринимать возвращённое значение, названное в данном случае num
, как изменяемую ссылку на содержащиеся внутри данные. Система видов заверяет, что мы получим блокировку перед использованием значения в m
. Вид m
- Mutex<i32>
, а не i32
, поэтому мы должны вызвать lock
, чтобы иметь возможность использовать значение i32
. Мы не должны об этом забывать, тем более что в иных случаях система видов и не даст нам доступ к внутреннему значению i32
.
Как вы наверное подозреваете, Mutex<T>
является умным указателем. Точнее, вызов lock
возвращает умный указатель, называемый MutexGuard
, обёрнутый в LockResult
, который мы обработали с помощью вызова unwrap
. Умный указатель вида MutexGuard
выполняет особенность Deref
для указания на внутренние данные; умный указатель также имеет выполнение особенности Drop
, самостоятельно снимающего блокировку, когда MutexGuard
выходит из области видимости, что происходит в конце внутренней области видимости. В итоге у нас нет риска забыть снять блокировку и оставить мьютекс в заблокированном состоянии, препятствуя его использованию другими потоками (снятие блокировки происходит самостоятельно ).
После снятия блокировки можно напечатать значение мьютекса и увидеть, что мы смогли изменить внутреннее i32
на 6.
Mutex<T>
между множеством потоковТеперь давайте попробуем с помощью Mutex<T>
совместно использовать значение между несколькими потоками. Мы стартуем 10 потоков и каждый из них увеличивает значение счётчика на 1, поэтому счётчик изменяется от 0 до 10. Обратите внимание, что в следующих нескольких примерах будут ошибки сборщика и мы будем использовать эти ошибки, чтобы узнать больше об использовании вида Mutex<T>
и как Ржавчина помогает нам правильно его использовать. Приложение 16-13 содержит наш начальный пример:
Файл: src/main.rs
-use std::sync::Mutex;
-use std::thread;
-
-fn main() {
- let counter = Mutex::new(0);
- let mut handles = vec![];
-
- for _ in 0..10 {
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
-}
--
Мы создаём переменную-счётчик counter
для хранения i32
значения внутри Mutex<T>
, как мы это делали в приложении 16-12. Затем мы создаём 10 потоков, перебирая рядчисел. Мы используем thread::spawn
и передаём всем этим потокам одинаковое замыкание, которое перемещает счётчик в поток, запрашивает блокировку на Mutex<T>
, вызывая способ lock
, а затем добавляет 1 к значению в мьютексе. Когда поток завершит выполнение своего замыкания, num
выйдет из области видимости и освободит блокировку, чтобы её мог получить другой поток.
В основном потоке мы собираем все указатели в переменную handles. Затем, как мы это делали в приложении 16-2, вызываем join
для каждого указателя, чтобы убедиться в завершении всех потоков. В этот мгновение основной поток получит доступ к блокировке и тоже напечатает итог программы.
Сборщик намекнул, что этот пример не собирается. Давайте выясним почему!
-$ cargo run
- Compiling shared-state v0.1.0 (file:///projects/shared-state)
-error[E0382]: borrow of moved value: `counter`
- --> src/main.rs:21:29
- |
-5 | let counter = Mutex::new(0);
- | ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
-...
-8 | for _ in 0..10 {
- | -------------- inside of this loop
-9 | let handle = thread::spawn(move || {
- | ------- value moved into closure here, in previous iteration of loop
-...
-21 | println!("Result: {}", *counter.lock().unwrap());
- | ^^^^^^^ value borrowed here after move
- |
-help: consider moving the expression out of the loop so it is only moved once
- |
-8 ~ let mut value = counter.lock();
-9 ~ for _ in 0..10 {
-10 | let handle = thread::spawn(move || {
-11 ~ let mut num = value.unwrap();
- |
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
-
-Сообщение об ошибке указывает, что значение counter
было перемещёно в замыкание на предыдущей повторения цикла. Ржавчина говорит нам, что мы не можем передать counter
во владение нескольким потокам. Давайте исправим ошибку сборщика с помощью способа множественного владения, который мы обсуждали в главе 15.
В главе 15 мы давали значение нескольким владельцам, используя умный указатель Rc<T>
для создания значения подсчитанных ссылок. Давайте сделаем то же самое здесь и посмотрим, что произойдёт. Мы завернём Mutex<T>
в Rc<T>
в приложении 16-14 и клонируем Rc<T>
перед передачей владения в поток. Теперь, когда мы увидели ошибки, мы также вернёмся к использованию цикла for
и сохраним ключевое слово move
у замыкания.
Файл: src/main.rs
-use std::rc::Rc;
-use std::sync::Mutex;
-use std::thread;
-
-fn main() {
- let counter = Rc::new(Mutex::new(0));
- let mut handles = vec![];
-
- for _ in 0..10 {
- let counter = Rc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
-
- *num += 1;
- });
- handles.push(handle);
- }
-
- for handle in handles {
- handle.join().unwrap();
- }
-
- println!("Result: {}", *counter.lock().unwrap());
-}
--
Ещё раз, мы собираем и получаем ... другие ошибки! Сборщик учит нас.
-$ cargo run
- Compiling shared-state v0.1.0 (file:///projects/shared-state)
-error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
- --> src/main.rs:11:36
- |
-11 | let handle = thread::spawn(move || {
- | ------------- ^------
- | | |
- | ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
- | | |
- | | required by a bound introduced by this call
-12 | | let mut num = counter.lock().unwrap();
-13 | |
-14 | | *num += 1;
-15 | | });
- | |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
- |
- = help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`, which is required by `{closure@src/main.rs:11:36: 11:43}: Send`
-note: required because it's used within this closure
- --> src/main.rs:11:36
- |
-11 | let handle = thread::spawn(move || {
- | ^^^^^^^
-note: required by a bound in `spawn`
- --> /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/thread/mod.rs:691:1
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
-
-Ничего себе, это сообщение об ошибке очень многословно! Вот важная часть, на которой следует сосредоточиться: ``Rc<Mutex cannot be sent between threads safely
. Сборщик также сообщает нам причину: the trait
Sendis not implemented for
Rc<Mutex
. Мы поговорим о Send
в следующем разделе: это один из особенностей, который заверяет, что виды которые мы используем с потоками, предназначены для использования в многопоточном коде.
К сожалению, Rc<T>
небезопасен для совместного использования между потоками. Когда Rc<T>
управляет счётчиком ссылок, он добавляется значение к счётчику для каждого вызова clone
и вычитается значение из счётчика, когда каждое клонированное значение удаляется при выходе из области видимости. Но он не использует простейшие многопоточности, чтобы обеспечить, что изменения в подсчёте не могут быть прерваны другим потоком. Это может привести к неправильным подсчётам - незначительным ошибкам, которые в свою очередь, могут привести к утечкам памяти или удалению значения до того, как мы отработали с ним. Нам нужен вид точно такой же как Rc<T>
, но который позволяет изменять счётчик ссылок безопасно из разных потоков.
Arc<T>
К счастью, Arc<T>
является видом подобным виду Rc<T>
, который безопасен для использования в случаейх многопоточности. Буква А означает атомарное, что означает вид ссылка подсчитываемая атомарно. Atomics - это дополнительный вид простейших для многопоточности, который мы не будем здесь подробно описывать: дополнительную сведения смотрите в документации встроенной библиотеки для std::sync::atomic
. На данный мгновение вам просто нужно знать, что atomics работают как простые виды, но безопасны для совместного использования между потоками.
Вы можете спросить, почему все простые виды не являются атомарными и почему обычные виды библиотек не выполнены для использования вместе с видом Arc<T>
по умолчанию. Причина в том, что безопасность потоков сопровождается снижением производительности, которое вы хотите платить только тогда, когда вам это действительно нужно. Если вы просто выполняете действия со значениями в одном потоке, то ваш код может работать быстрее, если он не должен обеспечивать заверения предоставляемые atomics.
Давайте вернёмся к нашему примеру: виды Arc<T>
и Rc<T>
имеют одинаковый API, поэтому мы исправляем нашу программу, заменяя вид в строках use
, вызове new
и вызове clone
. Код в приложении 16-15, наконец собирается и запустится:
Файл: src/main.rs
--use std::sync::{Arc, Mutex}; -use std::thread; - -fn main() { - let counter = Arc::new(Mutex::new(0)); - let mut handles = vec![]; - - for _ in 0..10 { - let counter = Arc::clone(&counter); - let handle = thread::spawn(move || { - let mut num = counter.lock().unwrap(); - - *num += 1; - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - println!("Result: {}", *counter.lock().unwrap()); -}
-
Код напечатает следующее:
- -Result: 10
-
-Мы сделали это! Мы посчитали от 0 до 10, что может показаться не очень впечатляющим, но это позволило больше узнать про Mutex<T>
и безопасность потоков. Вы также можете использовать устройство этой программы для выполнения более сложных действий, чем просто увеличение счётчика. Используя эту стратегию, вы можете разделить вычисления на независимые части, разделить эти части на потоки, а затем использовать Mutex<T>
, чтобы каждый поток обновлял конечный итог своей частью кода.
Обратите внимание, что если вы выполняете простые числовые действия, то существуют виды более простые, чем Mutex<T>
, которые предоставляет звено std::sync::atomic
встроенной библиотеки. Эти виды обеспечивают безопасный, одновременный, атомарный доступ к простым видам. Мы решили использовать Mutex<T>
с простым видом в этом примере, чтобы подробнее рассмотреть, как работает Mutex<T>
.
RefCell<T>
/ Rc<T>
и Mutex<T>
/ Arc<T>
Вы могли заметить, что counter
сам по себе не изменяемый (immutable), но мы можем получить изменяемую ссылку на значение внутри него; это означает, что Mutex<T>
обеспечивает внутреннюю изменяемость, также как и семейство Cell
видов. Мы использовали RefCell<T>
в главе 15, чтобы получить возможность изменять содержимое внутри Rc<T>
, теперь подобным образом мы используем Mutex<T>
для изменения содержимого внутри Arc<T>
.
Ещё одна подробность, на которую стоит обратить внимание: Ржавчина не может защитить вас от всевозможных логических ошибок при использовании Mutex<T>
. Вспомните в главе 15, что использование Rc<T>
сопряжено с риском создания ссылочной зацикленности, где два значения Rc<T>
ссылаются друг на друга, что приводит к утечкам памяти. Подобным образом, Mutex<T>
сопряжён с риском создания взаимных блокировок (deadlocks). Это происходит, когда действия необходимо заблокировать два ресурса и каждый из двух потоков получил одну из блокировок, заставляя оба потока ждать друг друга вечно. Если вам важна направление взаимных блокировок, попробуйте создать программу Rust, которая её содержит; затем исследуйте стратегии устранения взаимных блокировок для мьютексов на любом языке и попробуйте выполнить их в Rust. Документация встроенной библиотеки для Mutex<T>
и MutexGuard
предлагает полезную сведения.
Мы завершим эту главу, рассказав о особенностях Send
и Sync
и о том, как мы можем использовать их с пользовательскими видами.
Sync
и Send
Важно, что сам язык Ржавчина имеет очень мало возможностей для многопоточности. Почти все функции многопоточности о которых мы говорили в этой главе, были частью встроенной библиотеки, а не языка. Ваши исходы работы с многопоточностью не ограничиваются языком или встроенной библиотекой; Вы можете написать свой собственный многопоточный возможности или использовать возможности написанные другими.
-Тем не менее, в язык встроены две подходы многопоточности: std::marker
особенности Sync
и Send
.
Send
Маркерный особенность Send
указывает, что владение видом выполняющим Send
, может передаваться между потоками. Почти каждый вид Ржавчина является видом Send
, но есть некоторые исключения, вроде Rc<T>
: он не может быть Send
, потому что если вы клонировали значение Rc<T>
и попытались передать владение клоном в другой поток, оба потока могут обновить счётчик ссылок одновременно. По этой причине Rc<T>
выполнен для использования в однопоточных случаейх, когда вы не хотите платить за снижение производительности.
Следовательно, система видов Ржавчина и ограничений особенности заверяют, что вы никогда не сможете случайно небезопасно отправлять значение Rc<T>
между потоками. Когда мы попытались сделать это в приложении 16-14, мы получили ошибку, the trait Send is not implemented for Rc<Mutex<i32>>
. Когда мы переключились на Arc<T>
, который является видом Send
, то код собрался.
Любой вид полностью состоящий из видов Send
самостоятельно помечается как Send
. Почти все простые виды являются Send
, кроме сырых указателей, которые мы обсудим в главе 19.
Sync
Маркерный особенность Sync
указывает, что на вид выполняющий Sync
можно безопасно ссылаться из нескольких потоков. Другими словами, любой вид T
является видом Sync
, если &T
(ссылка на T
) является видом Send
, что означает что ссылку можно безопасно отправить в другой поток. Подобно Send
, простые виды являются видом Sync
, а виды полностью объединенные из видов Sync
, также являются Sync
видом.
Умный указатель Rc<T>
не является Sync
видом по тем же причинам, по которым он не является Send
. Вид RefCell<T>
(о котором мы говорили в главе 15) и семейство связанных видов Cell<T>
не являются Sync
. Выполнение проверки заимствования, которую делает вид RefCell<T>
во время выполнения программы не является поточно-безопасной. Умный указатель Mutex<T>
является видом Sync
и может использоваться для совместного доступа из нескольких потоков, как вы уже видели в разделе «Совместное использование Mutex<T>
между несколькими потоками» .
Send
и Sync
вручную небезопаснаПоскольку виды созданные из особенностей Send
и Sync
самостоятельно также являются видами Send
и Sync
, мы не должны выполнить эти особенности вручную. Являясь маркерными особенностями у них нет никаких способов для выполнения. Они просто полезны для выполнения неизменных величин, связанных с многопоточностью.
Ручная выполнение этих особенностей включает в себя выполнение небезопасного кода Rust. Мы поговорим об использовании небезопасного кода Ржавчина в главе 19; на данный мгновение важная сведения заключается в том, что для создания новых многопоточных видов, не состоящих из частей Send
и Sync
необходимо тщательно продумать заверения безопасности. В Rustonomicon есть больше сведений об этих заверениях и о том как их соблюдать.
Это не последний случай, когда вы увидите многопоточность в этой книге: дело в главе 20 будет использовать подходы этой главы для более существующегостичного случая, чем небольшие примеры обсуждаемые здесь.
-Как упоминалось ранее, поскольку в языке Ржавчина очень мало того, с помощью чего можно управлять многопоточностью, многие решения выполнены в виде ящиков. Они развиваются быстрее, чем обычная библиотека, поэтому обязательно поищите в Интернете текущие современные ящики.
-Обычная библиотека Ржавчина предоставляет потоки для передачи сообщений и виды умных указателей, такие как Mutex<T>
и Arc<T>
, которые можно безопасно использовать в многопоточных средах. Система видов и анализатор заимствований заверяют, что код использующий эти решения не будет содержать гонки данных или недействительные ссылки. Получив собирающийся код, вы можете быть уверены, что он будет успешно работать в нескольких потоках без ошибок, которые трудно обнаружить в других языках. Многопоточное программирование больше не является подходом, которую стоит опасаться: иди вперёд и сделай свои программы многопоточными безбоязненно!
Далее мы поговорим об идиоматичных способах расчетов неполадок и внутреннего выстраивания -решений по мере усложнения ваших программ на Rust. Кроме того, мы обсудим как идиомы Ржавчина связаны с теми, с которыми вы, возможно, знакомы по предметно-направленному программированию.
- -Предметно-направленное программирование (ООП) — это способ построения программ. Предметы, как программная подход, были введены в язык программирования Simula в 1960-х годах. Эти предметы повлияли на архитектуру программирования Алана Кея, в которой предметы передают сообщения друг другу. Чтобы описать эту архитектуру, он ввёл понятие предметно-направленное программирование в 1967 году. Есть много состязающихся определений ООП, и по некоторым из этих определений Ржавчина является предметно-направленным, а по другим — нет. В этой главе мы рассмотрим некоторые свойства, которые обычно считаются предметно-направленными, и то, как эти свойства транслируются в идиомы языка Rust. Затем мы покажем, как выполнить образец предметно-направленного разработки в Rust, и обсудим соглашения между этим исходом и решением, использующим вместо этого некоторые сильные стороны Rust.
- -В сообществе программистов нет единого мнения о том, какими свойствами должен обладать язык, чтобы считаться предметно-направленным. На Ржавчина повлияли многие парадигмы программирования, включая ООП - например, в главе 13 мы изучали особенности, пришедшие из функционального программирования. Однозначно можно утверждать, что ООП-языкам присущи следующие присущие особенности: предметы, инкапсуляция и наследование. Давайте рассмотрим, что каждая из них означает и поддерживает ли их Rust.
-Книга Приёмы предметно-направленного разработки. Образцы разработки Erich Gamma, Richard Helm, Ralph Johnson, и John Vlissides (Addison-Wesley Professional, 1994), в просторечии называемая Книга банды четырёх, представляет собой сборник примеров предметно-направленного разработки. В ней даётся следующее определение ООП:
---Предметно-направленные программы состоят из предметов. Предмет представляет собой сущность, своего рода дополнение, с данными и процедурами, которые работают с этими данными. Процедуры обычно называются способами или действиеми.
-
В соответствии с этим определением, Ржавчина является предметно-направленным языком - в устройствах и перечислениях содержатся данные, а в х impl
определяются способы для них. Хотя устройства и перечисления, имеющие способы, не называются предметами, они обеспечивают возможность, соответствующую определению предметов в книге банды четырёх.
Другим особенностью, обычно связанным с предметно-направленным программированием, является мысль инкапсуляции: подробности выполнения предмета недоступны для кода, использующего этот предмет. Единственный способ взаимодействия с предметом — через его открытый внешняя оболочка; код, использующий этот предмет, не должен иметь возможности взаимодействовать с внутренними свойствами предметами напрямую изменять его данные или поведение. Инкапсуляция позволяет изменять и ресоздавать внутренние свойства предмета без необходимости изменять код, который использует предмет.
-В главе 7 мы уже говорили о том, как управлять инкапсуляцией: мы можем использовать ключевое слово pub
, чтобы определить, какие звенья, виды, функции и способы в нашем коде будут открытыми, а всё остальное по умолчанию будет закрытыми. Например, мы можем определить устройство AveragedCollection
, в которой есть поле, содержащее вектор значений i32
. Также, устройства будет иметь поле, содержащее среднее арифметическое чисел этого вектора, таким образом, среднее не нужно будет вычислять каждый раз, когда оно кому-то понадобится. Другими словами, AveragedCollection
будет кэшировать вычисленное среднее для нас. В приложении 17-1 приведено определение устройства AveragedCollection
:
Файл: src/lib.rs
-pub struct AveragedCollection {
- list: Vec<i32>,
- average: f64,
-}
--
Обратите внимание, что устройства помечена ключевым словом pub
, что позволяет другому коду её использовать, однако, поля устройства остаются недоступными. Это важно, потому что мы хотим обеспечить обновление среднего значения при добавлении или удалении элемента из списка. Мы можем получить нужное поведение, определив в устройстве способы add
, remove
и average
, как показано в примере 17-2:
Файл: src/lib.rs
-pub struct AveragedCollection {
- list: Vec<i32>,
- average: f64,
-}
-
-impl AveragedCollection {
- pub fn add(&mut self, value: i32) {
- self.list.push(value);
- self.update_average();
- }
-
- pub fn remove(&mut self) -> Option<i32> {
- let result = self.list.pop();
- match result {
- Some(value) => {
- self.update_average();
- Some(value)
- }
- None => None,
- }
- }
-
- pub fn average(&self) -> f64 {
- self.average
- }
-
- fn update_average(&mut self) {
- let total: i32 = self.list.iter().sum();
- self.average = total as f64 / self.list.len() as f64;
- }
-}
--
Открытые способы add
, remove
и average
являются единственным способом получить или изменить данные в образце AveragedCollection
. Когда элемент добавляется в list
способом add
, или удаляется с помощью способа remove
, код выполнения каждого из этих способов вызывает закрытый способ update_average
, который позаботится об обновлении поля average
.
Мы оставляем поля list
и average
закрытыми, чтобы внешний код не мог добавлять или удалять элементы непосредственно в поле list
; в противном случае поле average
может оказаться не согласовано при подобном вмешательстве. Способ average
возвращает значение в поле average
, что позволяет внешнему коду читать значение average
, но не изменять его.
Поскольку мы инкапсулировали подробности выполнения устройства AveragedCollection
, мы можем легко изменить такие особенности, как устройства данных, в будущем. Например, мы могли бы использовать HashSet<i32>
вместо Vec<i32>
для поля list
. Благодаря тому, что ярлыки открытых способов add
, remove
и average
остаются неизменными, код, использующий AveragedCollection
, также не будет нуждаться в изменении. У нас бы не получилось этого достичь, если бы мы сделали поле list
доступным внешнему коду: HashSet<i32>
иVec<i32>
имеют разные способы для добавления и удаления элементов, поэтому внешний код, вероятно, должен измениться, если он изменяет list
напрямую.
Если инкапсуляция является обязательным особенностью для определения языка как предметно-направленного, то Ржавчина соответствует этому требованию. Возможность использовать или не использовать изменитель доступа pub
для различных частей кода позволяет скрыть подробности выполнения.
Наследование — это рычаг, с помощью которого предмет может унаследовать элементы из определения другого предмета. то есть получить данные и поведение родительского предмета без необходимости повторно их определять.
-Если язык должен иметь наследование, чтобы быть предметно-направленным, то Ржавчина таким не является. Здесь нет способа определить устройство, наследующую поля и выполнения способов родительской устройства, без использования макроса.
-Однако, если вы привыкли иметь наследование в своём наборе средств для программирования, вы можете использовать другие решения в Rust, в зависимости от того, по какой причине вы изначально хотите использовать наследование.
-Вы могли бы выбрать наследование по двум основным причинам. Одна из них - возможность повторного использования кода: вы можете выполнить определённое поведение для одного вида, а наследование позволит вам повторно использовать эту выполнение для другого вида. В Ржавчина для этого есть ограниченный способ, использующий выполнение способа особенности по умолчанию, который вы видели в приложении 10-14, когда мы добавили выполнение по умолчанию в способе summarize
особенности Summary
. Любой вид, выполняющий свойство Summary
будет иметь доступный способ summarize
без дополнительного кода. Это похоже на то, как родительский класс имеет выполнение способа, и класс-наследник тоже имеет выполнение способа. Мы также можем переопределить выполнение по умолчанию для способа summarize
, когда выполняем особенность Summary
, что похоже на дочерний класс, переопределяющий выполнение способа, унаследованного от родительского класса.
Вторая причина использования наследования относится к системе видов: чтобы иметь возможность использовать дочерний вид в тех же места, что и родительский. Эта возможность также называется полиморфизм и означает возможность подменять предметы во время исполнения, если они имеют одинаковые свойства.
---Полиморфизм
-Для многих людей полиморфизм является родственным наследования. Но на самом деле это более общая подход, относящаяся к коду, который может работать с данными нескольких видов. Обычно такими видами выступают подклассы при наследовании.
-Вместо этого Ржавчина использует обобщённые виды для абстрагирования от видов, и ограничения особенностей (trait bounds) для указания того, какие возможности эти виды должны предоставлять. Это иногда называют ограниченным свойствоическим полиморфизмом.
-
Наследование, как подход к разработке, в последнее время утратило распространенность во многих языках программирования, поскольку часто существует риск, что мы будем наследовать код чаще, чем это необходимо. Подклассы не всегда должны обладать всеми свойствами родительского класса, но при использовании наследования другого исхода нет. Это может сделать внешний вид программы менее гибким. Кроме этого, появляется возможность вызова у подклассов способов, которые не имеют смысла или вызывают ошибки, потому что эти способы неприменимы к подклассу. Кроме того, в некоторых языках разрешается только одиночное наследование (т.е. подкласс может наследоваться только от одного класса), что ещё больше ограничивает гибкость разработки программы.
-По этим причинам в Ржавчина применяется иной подход, с использованием особенностей-предметов вместо наследования. Давайте посмотрим как особенности-предметы выполняют полиморфизм в Rust.
- -В главе 8 мы упоминали, что одним из ограничений векторов является то, что они могут хранить элементы только одного вида. Мы создали обходное решение в приложении 8-9, где мы определили перечисление SpreadsheetCell
в котором были исходы для хранения целых чисел, чисел с плавающей точкой и текста. Это означало, что мы могли хранить разные виды данных в каждой ячейке и при этом иметь вектор, представляющий строку из ячеек. Это очень хорошее решение, когда наши взаимозаменяемые элементы вектора являются видами с конечным набором, известным при сборки кода.
Однако иногда мы хотим, чтобы пользователь нашей библиотеки мог расширить набор видов, которые допустимы в именно случаи. Чтобы показать как этого добиться, мы создадим пример средства с графическим внешней оболочкой пользователя (GUI), который просматривает список элементов, вызывает способ draw
для каждого из них, чтобы нарисовать его на экране - это обычная техника для средств GUI. Мы создадим библиотечный ящик с именем gui
, содержащий устройство библиотеки GUI. Этот ящик мог бы включать некоторые готовые виды для использования, такие как Button
или TextField
. Кроме того, пользователи такого ящика gui
захотят создавать свои собственные виды, которые могут быть нарисованы: например, кто-то мог бы добавить вид Image
, а кто-то другой добавить вид SelectBox
.
Мы не будем выполнить полноценную библиотеку GUI для этого примера, но покажем, как её части будут подходить друг к другу. На мгновение написания библиотеки мы не можем знать и определить все виды, которые могут захотеть создать другие программисты. Но мы знаем, что gui
должен отслеживать множество значений различных видов и ему нужно вызывать способ draw
для каждого из этих значений различного вида. Ему не нужно точно знать, что произойдёт, когда вызывается способ draw
, просто у значения будет доступен такой способ для вызова.
Чтобы сделать это на языке с наследованием, можно определить класс с именем Component
у которого есть способ с названием draw
. Другие классы, такие как Button
, Image
и SelectBox
наследуются от Component
и следовательно, наследуют способ draw
. Каждый из них может переопределить выполнение способа draw
, чтобы определить своё пользовательское поведение, но площадка может обрабатывать все виды, как если бы они были образцами Component
и вызывать draw
у них. Но поскольку в Ржавчина нет наследования, нам нужен другой способ внутренне выстроить
gui
библиотеку, чтобы позволить пользователям расширять её новыми видами.
Чтобы выполнить поведение, которое мы хотим иметь в gui
, определим особенность с именем Draw
, который будет содержать один способ с названием draw
. Затем мы можем определить вектор, который принимает особенность-предмет. Особенность-предмет указывает как на образец вида, выполняющего указанный особенность, так и на внутреннюю таблицу, используемую для поиска способов особенности указанного вида во время выполнения. Мы создаём особенность-предмет в таком порядке: используем какой-нибудь вид указателя, например ссылку &
или умный указатель Box<T>
, затем ключевое слово dyn
, а затем указываем соответствующий особенность. (Мы будем говорить о причине того, что особенность-предметы должны использовать указатель в разделе "Виды изменяемого размера и особенность Sized
" главы 19). Мы можем использовать особенность-предметы вместо гибкого или определенного вида. Везде, где мы используем особенность-предмет, система видов Ржавчина проверит во время сборки, что любое значение, используемое в этом среде, будет выполнить нужный особенность у особенность-предмета. Следовательно, нам не нужно знать все возможные виды во время сборки.
Мы упоминали, что в Ржавчина мы воздерживаемся называть устройства и перечисления «предметами», чтобы отличать их от предметов в других языках. В устройстве или перечислении данные в полях устройства и поведение в разделах impl
разделены, тогда как в других языках данные и поведение объединены в одну подход, часто обозначающуюся как предмет. Тем не менее, особенность-предметы являются более похожими на предметы на других языках, в том смысле, что они сочетают в себе данные и поведение. Но особенность-предметы отличаются от привычных предметов тем, что не позволяют добавлять данные к особенность-предмету. Особенность-предметы обычно не настолько полезны, как предметы в других языках: их определенная цель - обеспечить абстракцию через общее поведение.
В приложении 17.3 показано, как определить особенность с именем Draw
с помощью одного способа с именем draw
:
Файл: src/lib.rs
-pub trait Draw {
- fn draw(&self);
-}
--
Этот правила написания должен выглядеть знакомым из наших дискуссий о том, как определять особенности в главе 10. Далее следует новый правила написания: в приложении 17.4 определена устройства с именем Screen
, которая содержит вектор с именем components
. Этот вектор имеет вид Box<dyn Draw>
, который и является особенность-предметом; это замена для любого вида внутри Box
который выполняет особенность Draw
.
Файл: src/lib.rs
-pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
--
В устройстве Screen
, мы определим способ run
, который будет вызывать способ draw
каждого элемента вектора components
, как показано в приложении 17-5:
Файл: src/lib.rs
-pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
-
-impl Screen {
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
--
Это работает иначе, чем определение устройства, которая использует свойство общего вида с ограничениями особенности. Обобщённый свойство вида может быть заменён только одним определенным видом, тогда как особенность-предметы позволяют нескольким определенным видам замещать особенность-предмет во время выполнения. Например, мы могли бы определить устройство Screen
используя общий вид и ограничение особенности, как показано в приложении 17-6:
Файл: src/lib.rs
-pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen<T: Draw> {
- pub components: Vec<T>,
-}
-
-impl<T> Screen<T>
-where
- T: Draw,
-{
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
--
Это исход ограничивает нас образцом Screen
, который имеет список составляющих всех видов Button
или всех видов TextField
. Если у вас когда-либо будут только однородные собрания, использование обобщений и ограничений особенности является предпочтительным, поскольку определения будут мономорфизированы во время сборки для использования с определенными видами.
С другой стороны, с помощью способа, использующего особенность-предметы, один образец Screen
может содержать Vec<T>
который содержит Box<Button>
, также как и Box<TextField>
. Давайте посмотрим как это работает, а затем поговорим о влиянии на производительность во время выполнения.
Теперь мы добавим несколько видов, выполняющих особенность Draw
. Мы объявим вид Button
. Опять же, действительная выполнение библиотеки GUI выходит за рамки этой книги, поэтому тело способа draw
не будет иметь никакой полезной выполнения. Чтобы представить, как может выглядеть такая выполнение, устройства Button
может иметь поля для width
, height
и label
, как показано в приложении 17-7:
Файл: src/lib.rs
-pub trait Draw {
- fn draw(&self);
-}
-
-pub struct Screen {
- pub components: Vec<Box<dyn Draw>>,
-}
-
-impl Screen {
- pub fn run(&self) {
- for component in self.components.iter() {
- component.draw();
- }
- }
-}
-
-pub struct Button {
- pub width: u32,
- pub height: u32,
- pub label: String,
-}
-
-impl Draw for Button {
- fn draw(&self) {
- // code to actually draw a button
- }
-}
--
Поля width
, height
и label
устройства Button
будут отличаться от, например, полей других составляющих вроде вида TextField
, которая могла бы иметь те же поля плюс поле placeholder
. Каждый из видов, который мы хотим нарисовать на экране будет выполнить особенность Draw
, но будет использовать отличающийся код способа draw
для определения как именно рисовать определенный вид, например Button
в этом примере (без действительного кода GUI, который выходит за рамки этой главы). Например, вид Button
может иметь дополнительный разделimpl
, содержащий способы, относящиеся к тому, что происходит, когда пользователь нажимает кнопку. Эти исходы способов не будут применяться к видам вроде TextField
.
Если кто-то использующий нашу библиотеку решает выполнить устройство SelectBox
, которая имеет width
, height
и поля options
, он выполняет также и особенность Draw
для вида SelectBox
, как показано в приложении 17-8:
Файл: src/main.rs
-use gui::Draw;
-
-struct SelectBox {
- width: u32,
- height: u32,
- options: Vec<String>,
-}
-
-impl Draw for SelectBox {
- fn draw(&self) {
- // code to actually draw a select box
- }
-}
-
-fn main() {}
--
Пользователь нашей библиотеки теперь может написать свою функцию main
для создания образца Screen
. К образцу Screen
он может добавить SelectBox
и Button
, поместив каждый из них в Box<T>
, чтобы он стал особенность-предметом. Затем он может вызвать способ run
у образца Screen
, который вызовет draw
для каждого из составляющих. Приложение 17-9 показывает эту выполнение:
Файл: src/main.rs
-use gui::Draw;
-
-struct SelectBox {
- width: u32,
- height: u32,
- options: Vec<String>,
-}
-
-impl Draw for SelectBox {
- fn draw(&self) {
- // code to actually draw a select box
- }
-}
-
-use gui::{Button, Screen};
-
-fn main() {
- let screen = Screen {
- components: vec![
- Box::new(SelectBox {
- width: 75,
- height: 10,
- options: vec![
- String::from("Yes"),
- String::from("Maybe"),
- String::from("No"),
- ],
- }),
- Box::new(Button {
- width: 50,
- height: 10,
- label: String::from("OK"),
- }),
- ],
- };
-
- screen.run();
-}
--
Когда мы писали библиотеку, мы не знали, что кто-то может добавить вид SelectBox
, но наша выполнение Screen
могла работать с новым видом и рисовать его, потому что SelectBox
выполняет особенность Draw
, что означает, что он выполняет способ draw
.
Эта подход, касающаяся только сообщений, на которые значение отвечает, в отличие от определенного вида у значения, подобна подходы duck typing в изменяемых строго определенных языках: если что-то ходит как утка и крякает как утка, то она должна быть утка! В выполнения способа run
у Screen
в приложении 17-5, run
не нужно знать каким будет определенный вид каждого составляющих. Он не проверяет, является ли составляющая образцом Button
или SelectBox
, он просто вызывает способ draw
составляющих. Указав Box<dyn Draw>
в качестве вида значений в векторе components
, мы определили Screen
для значений у которых мы можем вызвать способ draw
.
Преимущество использования особенность-предметов и системы видов Ржавчина для написания кода, похожего на код с использованием подходы duck typing состоит в том, что нам не нужно во время выполнения проверять выполняет ли значение в векторе определенный способ или беспокоиться о получении ошибок, если значение не выполняет способ, мы все равно вызываем способ. Ржавчина не собирает наш код, если значения не выполняют особенность, который нужен особенность-предмета..
-Например, в приложении 17-10 показано, что произойдёт, если мы попытаемся создать Screen
с String
в качестве его составляющих:
Файл: src/main.rs
-use gui::Screen;
-
-fn main() {
- let screen = Screen {
- components: vec![Box::new(String::from("Hi"))],
- };
-
- screen.run();
-}
--
Мы получим ошибку, потому что String
не выполняет особенность Draw
:
$ cargo run
- Compiling gui v0.1.0 (file:///projects/gui)
-error[E0277]: the trait bound `String: Draw` is not satisfied
- --> src/main.rs:5:26
- |
-5 | components: vec![Box::new(String::from("Hi"))],
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
- |
- = help: the trait `Draw` is implemented for `Button`
- = note: required for the cast from `Box<String>` to `Box<dyn Draw>`
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `gui` (bin "gui") due to 1 previous error
-
-Эта ошибка даёт понять, что либо мы передаём в составляющая Screen
что-то, что мы не собирались передавать и мы тогда должны передать другой вид, либо мы должны выполнить особенность Draw
у вида String
, чтобы Screen
мог вызывать draw
у него.
Вспомните, в разделе «Производительность кода, использующего обобщённые виды» в главе 10 наше обсуждение этапа мономорфизации, выполняемого сборщиком, когда мы используем ограничения особенностей для обобщённых видов: сборщик порождает частные выполнения функций и способов для каждого определенного вида, который мы применяем для свойства обобщённого вида. Код, который получается в итоге мономорфизации, выполняет постоянную управление , то есть когда сборщик знает, какой способ вы вызываете во время сборки. Это противоположно изменяемой управления, когда сборщик не может определить во время сборки, какой способ вы вызываете. В случае изменяемой управления сборщик создает код, который во время выполнения определит, какой способ нужно вызвать.
-Когда мы используем особенность-предметы, Ржавчина должен использовать изменяемую управление. Сборщик не знает всех видов, которые могут быть использованы с кодом, использующим особенность-предметы, поэтому он не знает, какой способ выполнен для какого вида при вызове. Вместо этого, во время выполнения, Ржавчина использует указатели внутри особенность-предмета. чтобы узнать какой способ вызвать. Такой поиск вызывает дополнительные затраты во время исполнения, которые не требуются при постоянной управления. Изменяемая управление также не позволяет сборщику выбрать встраивание кода способа, что в свою очередь делает невозможными некоторые переработки. Однако мы получили дополнительную гибкость в коде, который мы написали в приложении 17-5, и которую смогли поддержать в приложении 17-9, поэтому все "за" и "против" нужно рассматривать в совокупности.
- -Образец "Состояние" — это предметно-направленный образец разработки. Суть образца заключается в том, что мы определяем набор состояний, которые может иметь внутреннее значение. Состояния представлены набором предметов состояния, а поведение элемента изменяется в зависимости от его состояния. Мы рассмотрим пример устройства записи в блоге, в которой есть поле для хранения состояния, которое будет предметом состояния из набора «черновик», «обзор» или «обнародовано».
-Предметы состояния имеют общую возможность: конечно в Ржавчина мы используем устройства и особенности, а не предметы и наследование. Каждый предмет состояния отвечает за своё поведение и сам определяет, когда он должен перейти в другое состояние. Элемент, который содержит предмет состояния, ничего не знает о различиях в поведении состояний или о том, когда одно состояние должно перейти в другое.
-Преимуществом образца "Состояние" является то, что при изменении требований заказчика программы не требуется изменять код элемента, содержащего состояние, или код, использующий такой элемент. Нам нужно только обновить код внутри одного из предметов состояния, чтобы изменить его порядок действий, либо, возможно, добавить больше предметов состояния.
-Для начала выполняем образец "Состояние" более привычным предметно-направленным способом, а затем воспользуемся подходом, более естественным для Rust. Давайте шаг за шагом выполняем поток действий для записи в блоге, использующий образец "Состояние".
-Окончательный возможности будет выглядеть так:
-Любые другие изменения, сделанные в записи, не должны иметь никакого эффекта. Например, если мы попытаемся подтвердить черновик записи в блоге до того, как запросим проверку, запись должна остаться необнародованным черновиком.
-В приложении 17-11 показан этот поток действий в виде кода: это пример использования API, который мы собираемся выполнить в библиотеке (ящике) с именем blog
. Он пока не собирается, потому что ящик blog
ещё не создан.
Файл: src/main.rs
-use blog::Post;
-
-fn main() {
- let mut post = Post::new();
-
- post.add_text("I ate a salad for lunch today");
- assert_eq!("", post.content());
-
- post.request_review();
- assert_eq!("", post.content());
-
- post.approve();
- assert_eq!("I ate a salad for lunch today", post.content());
-}
--
Мы хотим, чтобы пользователь мог создать новый черновик записи в блоге с помощью Post::new
. Затем мы хотим разрешить добавление текста в запись блога. Если мы попытаемся получить содержимое записи сразу, до её проверки, мы не должны получить никакого текста на выходе, потому что запись все ещё является черновиком. Мы добавили утверждение (assert_eq!
) в коде для опытных целей. Утверждение (assertion), что черновик записи блога должен возвращать пустую строку из способа content
было бы отличным состоящим из звеньев проверкой, но мы не собираемся писать проверки для этого примера.
Далее мы хотим разрешить сделать запрос на проверку записи и хотим, чтобы content
возвращал пустую строку, пока проверки не завершена. Когда запись пройдёт проверку, она должна быть обнародована, то есть при вызове content
будет возвращён текст записи.
Обратите внимание, что единственный вид из ящика, с которым мы взаимодействуем - это вид Post
. Этот вид будет использовать образец "Состояние" и будет содержать значение, которое будет являться одним из трёх предметов состояний, представляющих различные состояния, в которых может находиться запись: "черновик", "ожидание проверки" или "обнародовано". Управление переходом из одного состояния в другое будет осуществляться внутренней логикой вида Post
. Состояния будут переключаться в итоге реакции на вызов способов образца Post
пользователями нашей библиотеки, но пользователи не должны управлять изменениями состояния напрямую. Кроме того, пользователи не должны иметь возможность ошибиться с состояниями, например, обнародовать сообщение до его проверки.
Post
и создание нового образца в состоянии черновикаПриступим к выполнения библиотеки! Мы знаем, что нам нужна открытая устройства Post
, хранящая некоторое содержимое, поэтому мы начнём с определения устройства и связанной с ней открытой функцией new
для создания образца Post
, как показано в приложении 17-12. Мы также сделаем закрытый особенность State
, который будет определять поведение, которое должны будут иметь все предметы состояний устройства Post
.
Затем Post
будет содержать особенность-предмет Box<dyn State>
внутри Option<T>
в закрытом поле state
для хранения предмета состояния. Чуть позже вы поймёте, зачем нужно использовать Option<T>
.
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-}
-
-trait State {}
-
-struct Draft {}
-
-impl State for Draft {}
--
Особенность State
определяет поведение, совместно используемое различными состояниями поста. Все предметы состояний (Draft
- "черновик", PendingReview
- "ожидание проверки" и Published
- "обнародовано") будут выполнить особенность State
. Пока у этого особенности нет никаких способов, и мы начнём с определения состояния Draft
, просто потому, что это первое состояние, с которого, как мы хотим, обнародование будет начинать свой путь.
Когда мы создаём новый образец Post
, мы устанавливаем его поле state
в значение Some
, содержащее Box
. Этот Box
указывает на новый образец устройства Draft
. Это заверяет, что всякий раз, когда мы создаём новый образец Post
, он появляется как черновик. Поскольку поле state
в устройстве Post
является закрытым, нет никакого способа создать Post
в каком-либо другом состоянии! В функции Post::new
мы объявим поле content
новой пустой строкой вида String
.
В приложении 17-11 показано, что мы хотим иметь возможность вызывать способ add_text
и передать ему &str
, которое добавляется к текстовому содержимому записи блога. Мы выполняем эту возможность как способ, а не делаем поле content
открыто доступным, используя pub
. Это означает, что позже мы сможем написать способ, который будет управлять, как именно читаются данные из поля content
. Способ add_text
довольно прост, поэтому давайте добавим его выполнение в разделimpl Post
приложения 17-13:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- // --snip--
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-}
-
-trait State {}
-
-struct Draft {}
-
-impl State for Draft {}
--
Способ add_text
принимает изменяемую ссылку на self
, потому что мы меняем образец Post
, для которого вызываем add_text
. Затем мы вызываем push_str
для String
у поля content
и передаём text
переменнаяом для добавления к сохранённому content
. Это поведение не зависит от состояния, в котором находится запись, таким образом оно не является частью образца "Состояние". Способ add_text
вообще не взаимодействует с полем state
, но это часть поведения, которое мы хотим поддерживать.
Даже после того, как мы вызвали add_text
и добавили некоторый содержание в нашу запись, мы хотим, чтобы способ content
возвращал пустой отрывок строки, так как запись всё ещё находится в черновом состоянии, как это показано в строке 7 приложения 17-11. А пока давайте выполняем способ content
наиболее простым способом, который будет удовлетворять этому требованию: будем всегда возвращать пустой отрывок строки. Мы изменим код позже, как только выполняем возможность изменить состояние записи, чтобы она могла бы быть обнародована. Пока что записи могут находиться только в черновом состоянии, поэтому содержимое записи всегда должно быть пустым. Приложение 17-14 показывает такую выполнение-заглушку:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- // --snip--
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn content(&self) -> &str {
- ""
- }
-}
-
-trait State {}
-
-struct Draft {}
-
-impl State for Draft {}
--
С добавленным таким образом способом content
всё в приложении 17-11 работает, как задумано, вплоть до строки 7.
Далее нам нужно добавить возможность для запроса проверки записи, который должен изменить её состояние с Draft
на PendingReview
. Приложение 17-15 показывает такой код:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- // --snip--
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn content(&self) -> &str {
- ""
- }
-
- pub fn request_review(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.request_review())
- }
- }
-}
-
-trait State {
- fn request_review(self: Box<Self>) -> Box<dyn State>;
-}
-
-struct Draft {}
-
-impl State for Draft {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- Box::new(PendingReview {})
- }
-}
-
-struct PendingReview {}
-
-impl State for PendingReview {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
--
Мы добавляем в Post
открытый способ с именем request_review
("запросить проверку"), который будет принимать изменяемую ссылку на self
. Затем мы вызываем внутренний способ request_review
для текущего состояния Post
, и этот второй способ request_review
поглощает текущее состояние и возвращает новое состояние.
Мы добавляем способ request_review
в особенность State
; все виды, выполняющие этот особенность, теперь должны будут выполнить способ request_review
. Обратите внимание, что вместо self
, &self
или &mut self
в качестве первого свойства способа у нас указан self: Box<Self>
. Этот правила написания означает, что способ действителен только при его вызове с обёрткой Box
, содержащей наш вид. Этот правила написания становится владельцем Box<Self>
, делая старое состояние недействительным, поэтому значение состояния Post
может быть преобразовано в новое состояние.
Чтобы поглотить старое состояние, способ request_review
должен стать владельцем значения состояния. Это место, где приходит на помощь вид Option
поля state
записи Post
: мы вызываем способ take
, чтобы забрать значение Some
из поля state
и оставить вместо него значение None
, потому что Ржавчина не позволяет иметь необъявленные поля в устройствах. Это позволяет перемещать значение state
из Post
, а не заимствовать его. Затем мы установим новое значение state
как итог этой действия.
Нам нужно временно установить state
в None
, вместо того, чтобы установить его напрямую с помощью кода вроде self.state = self.state.request_review();
. Нам нужно завладеть значением поля state
. Это даст нам заверение, что Post
не сможет использовать старое значение state
после того, как мы преобразовали его в новое состояние.
Способ request_review
в Draft
должен вернуть новый образец новой устройства PendingReview
, обёрнутый в Box. Эта устройства будет представлять состояние, в котором запись ожидает проверки. Устройства PendingReview
также выполняет способ request_review
, но не выполняет никаких преобразований. Она возвращает сама себя, потому что, когда мы запрашиваем проверку записи, уже находящейся в состоянии PendingReview
, она всё так же должна продолжать оставаться в состоянии PendingReview
.
Теперь мы начинаем видеть преимущества образца "Состояние": способ request_review
для Post
одинаков, он не зависит от значения state
. Каждое состояние само несёт ответственность за свои действия.
Оставим способ content
у Post
таким как есть, возвращающим пустой отрывок строки. Теперь мы можем иметь Post
как в состоянии PendingReview
, так и в состоянии Draft
, но мы хотим получить такое же поведение в состоянии PendingReview
. Приложение 17-11 теперь работает до строки 10!
approve
для изменения поведения content
Способ approve
("одобрить") будет подобен способу request_review
: он будет устанавливать у state
значение, которое должна иметь запись при её одобрении, как показано в приложении 17-16:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- // --snip--
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn content(&self) -> &str {
- ""
- }
-
- pub fn request_review(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.request_review())
- }
- }
-
- pub fn approve(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.approve())
- }
- }
-}
-
-trait State {
- fn request_review(self: Box<Self>) -> Box<dyn State>;
- fn approve(self: Box<Self>) -> Box<dyn State>;
-}
-
-struct Draft {}
-
-impl State for Draft {
- // --snip--
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- Box::new(PendingReview {})
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
-
-struct PendingReview {}
-
-impl State for PendingReview {
- // --snip--
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- Box::new(Published {})
- }
-}
-
-struct Published {}
-
-impl State for Published {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
--
Мы добавляем способ approve
в особенность State
, добавляем новую устройство, которая выполняет этот особенность State
и устройство для состояния Published
.
Подобно тому, как работает request_review
для PendingReview
, если мы вызовем способ approve
для Draft
, он не будет иметь никакого эффекта, потому что approve
вернёт self
. Когда мы вызываем для PendingReview
способ approve
, то он возвращает новый упакованный образец устройства Published
. Устройства Published
выполняет особенность State
, и как для способа request_review
, так и для способа approve
она возвращает себя, потому что в этих случаях запись должна оставаться в состоянии Published
.
Теперь нам нужно обновить способ content
для Post
. Мы хотим, чтобы значение, возвращаемое из content
, зависело от текущего состояния Post
, поэтому мы собираемся перенести часть возможности Post
в способ content
, заданный для state
, как показано в приложении 17.17:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- // --snip--
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn content(&self) -> &str {
- self.state.as_ref().unwrap().content(self)
- }
- // --snip--
-
- pub fn request_review(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.request_review())
- }
- }
-
- pub fn approve(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.approve())
- }
- }
-}
-
-trait State {
- fn request_review(self: Box<Self>) -> Box<dyn State>;
- fn approve(self: Box<Self>) -> Box<dyn State>;
-}
-
-struct Draft {}
-
-impl State for Draft {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- Box::new(PendingReview {})
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
-
-struct PendingReview {}
-
-impl State for PendingReview {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- Box::new(Published {})
- }
-}
-
-struct Published {}
-
-impl State for Published {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
--
Поскольку наша цель состоит в том, чтобы сохранить все эти действия внутри устройств, выполняющих особенность State
, мы вызываем способ content
у значения в поле state
и передаём образец обнародования (то есть self
) в качестве переменной. Затем мы возвращаем значение, которое нам выдаёт вызов способа content
поля state
.
Мы вызываем способ as_ref
у Option
, потому что нам нужна ссылка на значение внутри Option
, а не владение значением. Поскольку state
является видом Option<Box<dyn State>>
, то при вызове способа as_ref
возвращается Option<&Box<dyn State>>
. Если бы мы не вызывали as_ref
, мы бы получили ошибку, потому что мы не можем переместить state
из заимствованного свойства &self
функции.
Затем мы вызываем способ unwrap
. Мы знаем, что этот способ здесь никогда не приведёт к со сбоемму завершению программы, так все способы Post
устроены таким образом, что после их выполнения, в поле state
всегда содержится значение Some
. Это один из случаев, про которых мы говорили в разделе "Случаи, когда у вас больше сведений, чем у сборщика" главы 9 - случай, когда мы знаем, что значение None
никогда не встретится, даже если сборщик не может этого понять.
Теперь, когда мы вызываем content
у вида &Box<dyn State>
, в действие вступает принудительное приведение (deref coercion) для &
и Box
, поэтому в конечном итоге способ content
будет вызван для вида, который выполняет особенность State
. Это означает, что нам нужно добавить способ content
в определение особенности State
, и именно там мы поместим логику для определения того, какое содержимое возвращать, в зависимости от текущего состояния, как показано в приложении 17-18:
Файл: src/lib.rs
-pub struct Post {
- state: Option<Box<dyn State>>,
- content: String,
-}
-
-impl Post {
- pub fn new() -> Post {
- Post {
- state: Some(Box::new(Draft {})),
- content: String::new(),
- }
- }
-
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn content(&self) -> &str {
- self.state.as_ref().unwrap().content(self)
- }
-
- pub fn request_review(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.request_review())
- }
- }
-
- pub fn approve(&mut self) {
- if let Some(s) = self.state.take() {
- self.state = Some(s.approve())
- }
- }
-}
-
-trait State {
- // --snip--
- fn request_review(self: Box<Self>) -> Box<dyn State>;
- fn approve(self: Box<Self>) -> Box<dyn State>;
-
- fn content<'a>(&self, post: &'a Post) -> &'a str {
- ""
- }
-}
-
-// --snip--
-
-struct Draft {}
-
-impl State for Draft {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- Box::new(PendingReview {})
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-}
-
-struct PendingReview {}
-
-impl State for PendingReview {
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- Box::new(Published {})
- }
-}
-
-struct Published {}
-
-impl State for Published {
- // --snip--
- fn request_review(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn approve(self: Box<Self>) -> Box<dyn State> {
- self
- }
-
- fn content<'a>(&self, post: &'a Post) -> &'a str {
- &post.content
- }
-}
--
Мы добавляем выполнение по умолчанию способа content
, который возвращает пустой отрывок строки. Это означает, что нам не придётся выполнить content
в устройствах Draft
и PendingReview
. Устройства Published
будет переопределять способ content
и вернёт значение из post.content
.
Обратите внимание, что для этого способа нам нужны изложении времени жизни, как мы обсуждали в главе 10. Мы берём ссылку на post
в качестве переменной и возвращаем ссылку на часть этого post
, поэтому время жизни возвращённой ссылки связано с временем жизни переменной post
.
И вот, мы закончили - теперь всё из приложения 17-11 работает! Мы выполнили образец "Состояние", определяющий правила этапа работы с записью в блоге. Логика, связанная с этими правилами, находится в предмета. состояний, а не разбросана по всей устройстве Post
.
--Почему не перечисление?
-Возможно, вам было важно, почему мы не использовали
-enum
с различными возможными состояниями записи в качестве исходов. Это, безусловно, одно из возможных решений. Попробуйте его выполнить и сравните конечные итоги, чтобы выбрать, какой из исходов вам больше нравится! Одним из недостатков использования перечисления является то, что в каждом месте, где проверяется значение перечисления, потребуется выражениеmatch
или что-то подобное для обработки всех возможных исходов. Возможно в этом случае нам придётся повторять больше кода, чем это было в решении с особенность-предметом.
Мы показали, что Ржавчина способен выполнить предметно-направленный образец "Состояние" для инкапсуляции различных видов поведения, которые должна иметь запись в каждом состоянии. Способы в Post
ничего не знают о различных видах поведения. При такой согласования кода, нам достаточно взглянуть только на один его участок, чтобы узнать отличия в поведении обнародованной обнародования: в выполнение особенности State
у устройства Published
.
Если бы мы захотели создать иную выполнение, не использующую образец состояния, мы могли бы вместо этого использовать выражения match
в способах Post
или даже в main
, которые бы проверяли состояние записи и изменяли поведение в этих местах. Это приведёт к тому, что нам придётся в нескольких местах исследовать все следствия того, что пост перешёл в состояние "обнародовано"! И эта нагрузка будет только увеличиваться по мере добавления новых состояний: для каждого из этих выражений match
потребуются дополнительные ответвления.
С помощью образца "Состояние" способы Post
и участки, где мы используем Post
, не требуют использования выражений match
, а для добавления нового состояния нужно только добавить новую устройство и выполнить способы особенности у одной этой устройства.
Выполнение с использованием образца "Состояние" легко расширить для добавления новой возможности. Чтобы увидеть, как легко поддерживать код, использующий данный образец, попробуйте выполнить некоторые из предложений ниже:
-reject
, который изменяет состояние обнародования с PendingReview
обратно на Draft
.approve
, прежде чем переводить состояние в Published
.Draft
. Подсказка: пусть предмет состояния решает, можно ли менять содержимое, но не отвечает за изменение Post
.Одним из недостатков образца "Состояние" является то, что поскольку состояния сами выполняют переходы между собой, некоторые из состояний получаются связанными друг с другом. Если мы добавим другое состояние между PendingReview
и Published
, например Scheduled
("расчитано наперед"), то придётся изменить код в PendingReview
, чтобы оно теперь переходило в Scheduled
. Если бы не нужно было менять PendingReview
при добавлении нового состояния, было бы меньше работы, но это означало бы, что мы переходим на другой образец разработки.
Другим недостатком является то, что мы сделали повторение некоторую логику. Чтобы устранить некоторое повторение, мы могли бы попытаться сделать выполнения по умолчанию для способов request_review
и approve
особенности State
, которые возвращают self
; однако это нарушило бы безопасность предмета. потому что особенность не знает, каким определенно будет self
. Мы хотим иметь возможность использовать State
в качестве особенность-предмета. поэтому нам нужно, чтобы его способы были предметно-безопасными.
Другое повторение включает в себя схожие выполнения способов request_review
и approve
у Post
. Оба способа делегируют выполнения одного и того же способа значению поля state
вида Option
и устанавливают итогом новое значение поля state
. Если бы у Post
было много способов, которые следовали этому образцу, мы могли бы рассмотреть определение макроса для устранения повторения (смотри раздел "Макросы" в главе 19).
Выполняя образец "Состояние" точно так, как он определён для предметно-направленных языков, мы не настолько полно используем преимущества Rust, как могли бы. Давайте посмотрим на некоторые изменения, которые мы можем внести в ящик blog
, чтобы недопустимые состояния и переходы превратить в ошибки времени сборки.
Мы покажем вам, как переосмыслить образец "Состояние", чтобы получить другой набор соглашений. Вместо того, чтобы полностью инкапсулировать состояния и переходы, так, чтобы внешний код не знал о них, мы будем кодировать состояния с помощью разных видов. Следовательно, система проверки видов Ржавчина предотвратит попытки использовать черновые обнародования, там где разрешены только обнародованные обнародования, вызывая ошибки сборки.
-Давайте рассмотрим первую часть main
в приложении 17-11:
Файл: src/main.rs
-use blog::Post;
-
-fn main() {
- let mut post = Post::new();
-
- post.add_text("I ate a salad for lunch today");
- assert_eq!("", post.content());
-
- post.request_review();
- assert_eq!("", post.content());
-
- post.approve();
- assert_eq!("I ate a salad for lunch today", post.content());
-}
-Мы по-прежнему поддерживаем создание новых сообщений в состоянии "черновика" с помощью способа Post::new
и возможность добавлять текст к содержимому обнародования. Но вместо способа content
у чернового сообщения, возвращающего пустую строку, мы сделаем так, что у черновых сообщений вообще не будет способа content
. Таким образом, если мы попытаемся получить содержимое черновика, мы получим ошибку сборщика, сообщающую, что способ не существует. В итоге мы не сможем случайно отобразить черновик содержимого записи в работающей программе, потому что этот код даже не собирается. В приложении 17-19 показано определение устройств Post
и DraftPost
, а также способов для каждой из них:
Файл: src/lib.rs
-pub struct Post {
- content: String,
-}
-
-pub struct DraftPost {
- content: String,
-}
-
-impl Post {
- pub fn new() -> DraftPost {
- DraftPost {
- content: String::new(),
- }
- }
-
- pub fn content(&self) -> &str {
- &self.content
- }
-}
-
-impl DraftPost {
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-}
--
Обе устройства, Post
и DraftPost
, имеют закрытое поле content
, в котором хранится текст сообщения блога. Устройства больше не содержат поле state
, потому что мы перемещаем кодирование состояния в виды устройств. Устройства Post
будет представлять обнародованную размещение, и у неё есть способ content
, который возвращает content
.
У нас все ещё есть функция Post::new
, но вместо возврата образца Post
она возвращает образец DraftPost
. Поскольку поле content
является закрытым и нет никаких функций, которые возвращают Post
, просто так создать образец Post
уже невозможно.
Устройства DraftPost
имеет способ add_text
, поэтому мы можем добавлять текст к content
как и раньше, но учтите, что в DraftPost
не определён способ content
! Так что теперь программа заверяет, что все записи начинаются как черновики, а черновики размещений не имеют своего содержания для отображения. Любая попытка обойти эти ограничения приведёт к ошибке сборщика.
Так как же получить обнародованный пост? Мы хотим обеспечить соблюдение правила, согласно которому черновик записи должен быть рассмотрен и утверждён до того, как он будет обнародован. Запись, находящаяся в состоянии проверки, по-прежнему не должна отображать содержимое. Давайте выполняем эти ограничения, добавив ещё одну устройство, PendingReviewPost
, определив способ request_review
у DraftPost
, возвращающий PendingReviewPost
, и определив способ approve
у PendingReviewPost
, возвращающий Post
, как показано в приложении 17-20:
Файл: src/lib.rs
-pub struct Post {
- content: String,
-}
-
-pub struct DraftPost {
- content: String,
-}
-
-impl Post {
- pub fn new() -> DraftPost {
- DraftPost {
- content: String::new(),
- }
- }
-
- pub fn content(&self) -> &str {
- &self.content
- }
-}
-
-impl DraftPost {
- // --snip--
- pub fn add_text(&mut self, text: &str) {
- self.content.push_str(text);
- }
-
- pub fn request_review(self) -> PendingReviewPost {
- PendingReviewPost {
- content: self.content,
- }
- }
-}
-
-pub struct PendingReviewPost {
- content: String,
-}
-
-impl PendingReviewPost {
- pub fn approve(self) -> Post {
- Post {
- content: self.content,
- }
- }
-}
--
Способы request_review
и approve
забирают во владение self
, таким образом поглощая образцы DraftPost
и PendingReviewPost
, которые потом преобразуются в PendingReviewPost
и обнародованную Post
, соответственно. Таким образом, у нас не будет никаких долгоживущих образцов DraftPost
, после того, как мы вызвали у них request_review
и так далее. В устройстве PendingReviewPost
не определён способ content
, поэтому попытка прочитать его содержимое приводит к ошибке сборщика, также как и в случае с DraftPost
. Так как единственным способом получить обнародованный образец Post
, у которого действительно есть объявленный способ content
, является вызов способа approve
у образца PendingReviewPost
, а единственный способ получить PendingReviewPost
- это вызвать способ request_review
у образца DraftPost
, теперь мы закодировали этап смены состояний записи блога с помощью системы видов.
Кроме этого, нужно внести небольшие изменения в main
. Так как способы request_review
и approve
теперь возвращают предметы, а не преобразуют устройство от которой были вызваны, нам нужно добавить больше затеняющих присваиваний let post =
, чтобы сохранять возвращаемые предметы. Также, теперь мы не можем использовать утверждения (assertions) для проверки того является ли содержимое черновиков и записей, находящихся на рассмотрении, пустыми строками, да они нам и не нужны - теперь стало невозможным собрать код, который бы пытался использовать содержимое записей, находящихся в этих состояниях. Обновлённый код в main
показан в приложении 17-21:
Файл: src/main.rs
-use blog::Post;
-
-fn main() {
- let mut post = Post::new();
-
- post.add_text("I ate a salad for lunch today");
-
- let post = post.request_review();
-
- let post = post.approve();
-
- assert_eq!("I ate a salad for lunch today", post.content());
-}
--
Изменения, которые нам нужно было внести в main
, чтобы переназначить post
означают, что эта выполнение теперь не совсем соответствует предметно-направленному образцу "Состояние": преобразования между состояниями больше не инкапсулированы внутри выполнения Post
полностью. Тем не менее, мы получили большую выгоду в том, что недопустимые состояния теперь невозможны из-за системы видов и проверки видов, которая происходит во время сборки! У нас есть заверенияия, что некоторые ошибки, такие как отображение содержимого необнародованной обнародования, будут обнаружены до того, как они дойдут до пользователей.
Попробуйте выполнить задачи, предложенные в начале этого раздела, в исполнения ящика blog
, каким он стал после приложения 17-20, чтобы создать своё мнение о внешнем виде этой исполнения кода. Обратите внимание, что некоторые задачи в этом исходе могут быть уже выполнены.
Мы увидели, что хотя Ржавчина и способен выполнить предметно-направленные образцы разработки, в нём также доступны и другие образцы, такие как кодирование состояния с помощью системы видов. Эти подходы имеют различные соглашения. Хотя вы, возможно, очень хорошо знакомы с предметно-направленными образцами, переосмысление неполадок для использования преимуществ и возможностей Ржавчина может дать такие выгоды, как предотвращение некоторых ошибок во время сборки. Предметно-направленные образцы не всегда будут лучшим решением в Ржавчина из-за наличия определённых возможностей, таких как владение, которого нет у предметно-направленных языков.
-Независимо от того, что вы думаете о принадлежности Ржавчина к предметно-направленным языкам после прочтения этой главы, теперь вы знаете, что можете использовать особенность-предметы, чтобы выполнить некоторые предметно-направленные свойства в Rust. Изменяемая управление может дать вашему коду некоторую гибкость в обмен на небольшое ухудшение производительности во время выполнения. Вы можете использовать эту гибкость для выполнения предметно-направленных образцов, которые могут улучшить сопровождаемость вашего кода. В Ржавчина также есть другие особенности, такие как владение, которых нет у предметно-направленных языков. Предметно-направленный образец не всегда будет лучшим способом использовать преимущества Rust, но является доступной возможностью.
-Далее мы рассмотрим образцы, которые являются ещё одной особенностью Rust, обеспечивающей высокую гибкость. Мы бегло рассказывали о них на протяжении всей книги, но ещё не видели всех их возможностей. Вперёд!
- -Образцы - это особый правила написания в Ржавчина для сопоставления со устройством видов, как сложных, так и простых. Использование образцов в сочетании с выражениями match
и другими устройствоми даёт вам больший управление над потоком управления программы. Образец состоит из некоторой сочетания следующего:
Некоторые примеры образцов включают x
, (a, 3)
и Some(Color::Red)
. В средах, в которых допустимы образцы, эти составляющие описывают разновидность данных. Затем наша программа сопоставляет значения с образцами, чтобы определить, имеет ли значение правильную разновидность данных для продолжения выполнения определённого отрывка кода.
Чтобы использовать образец, мы сравниваем его с некоторым значением. Если образец соответствует значению, мы используем части значения в нашем дальнейшем коде. Вспомните выражения match
главы 6, в которых использовались образцы, например, описание машины для сортировки монет. Если значение в памяти соответствует виде образца, мы можем использовать именованные части образца. Если этого не произойдёт, то не выполнится код, связанный с образцом.
Эта глава - справочник по всем особенностим, связанным с образцами. Мы расскажем о допустимых местах использования образцов, разнице между опровержимыми и неопровержимыми образцами и про различные виды правил написания образцов, которые вы можете увидеть. К концу главы вы узнаете, как использовать образцы для ясного выражения многих понятий.
- -В этапе использования языка Ржавчина вы часто используете образцы, даже не осознавая этого! В этом разделе обсуждаются все случаи, где использование образцов является правильным.
-match
Как обсуждалось в главе 6, мы используем образцы в ветках выражений match
. Условновыражения match
определяется как ключевое слово match
, значение используемое для сопоставления, одна или несколько веток, которые состоят из образца и выражения для выполнения, если значение соответствует образцу этой ветки, как здесь:
match VALUE {
- PATTERN => EXPRESSION,
- PATTERN => EXPRESSION,
- PATTERN => EXPRESSION,
-}
-
-Например, вот выражение match
из приложения 6-5, которое соответствует значению Option<i32>
в переменной x
:
match x {
- None => None,
- Some(i) => Some(i + 1),
-}
-Образцами в этом выражении match
являются None
и Some(i)
слева от каждой стрелки.
Одно из требований к выражениям match
состоит в том, что они должны быть исчерпывающими (exhaustive) в том смысле, что они должны учитывать все возможности для значения в выражении match
. Один из способов убедиться, что вы рассмотрели каждую возможность - это иметь образец перехвата всех исходов в последней ветке выражения: например, имя переменной, совпадающее с любым значением, никогда не может потерпеть неудачу и таким образом, охватывает каждый оставшийся случай.
Особый образец _
будет соответствовать чему угодно, но он никогда не привязывается к переменной, поэтому он часто используется в последней ветке. Образец _
может быть полезен, если вы, например, хотите пренебрегать любое не указанное значение. Мы рассмотрим образец _
более подробно в разделе "Пренебрежение значений в образце позже в этой главе.
if let
В главе 6 мы обсуждали, как использовать выражения if let
как правило в качестве более короткого способа записи эквивалента match
, которое обрабатывает только один случай. Дополнительно if let
может иметь соответствующий else
, содержащий код для выполнения, если образец выражения if let
не совпадает.
В приложении 18-1 показано, что можно также смешивать и сопоставлять выражения if let
, else if
и else if let
. Это даёт больше гибкости, чем match
выражение, в котором можно выразить только одно значение для сравнения с образцами. Кроме того, условия в серии if let
, else if
, else if let
не обязаны соотноситься друг с другом.
Код в приложении 18-1 показывает последовательность проверок нескольких условий, определяющих каким должен быть цвет фона. В данном примере мы создали переменные с предопределёнными значениями, которые в существующей программе могли бы быть получены из пользовательского ввода.
-Файл: src/main.rs
--fn main() { - let favorite_color: Option<&str> = None; - let is_tuesday = false; - let age: Result<u8, _> = "34".parse(); - - if let Some(color) = favorite_color { - println!("Using your favorite color, {color}, as the background"); - } else if is_tuesday { - println!("Tuesday is green day!"); - } else if let Ok(age) = age { - if age > 30 { - println!("Using purple as the background color"); - } else { - println!("Using orange as the background color"); - } - } else { - println!("Using blue as the background color"); - } -}
-
Если пользователь указывает любимый цвет, то этот цвет используется в качестве цвета фона. Если любимый цвет не указан, и сегодня вторник, то цвет фона - зелёный. Иначе, если пользователь указывает свой возраст в виде строки, и мы можем успешно проанализировать её и представить в виде числа, то цвет будет либо фиолетовым, либо оранжевым, в зависимости от значения числа. Если ни одно из этих условий не выполняется, то цвет фона будет синим.
-Эта условная устройства позволяет поддерживать сложные требования. С жёстко закодированными значениями, которые у нас здесь есть, этот пример напечатает Using purple as the background color
.
Можно увидеть, что if let
может также вводить затенённые переменные, как это можно сделать в match
ветках: строка if let Ok(age) = age
вводит новую затенённую переменную age
, которая содержит значение внутри исхода Ok
. Это означает, что нам нужно поместить условие if age > 30
внутри этого блок: мы не можем объединить эти два условия в if let Ok(age) = age && age > 30
. Затенённый age
, который мы хотим сравнить с 30, не является действительным, пока не начнётся новая область видимости с фигурной скобки.
Недостатком использования if let
выражений является то, что сборщик не проверяет полноту (exhaustiveness) всех исходов, в то время как с помощью выражения match
это происходит. Если мы не напишем последний разделelse
и, благодаря этому, пропустим обработку некоторых случаев, сборщик не предупредит нас о возможной логической ошибке.
while let
Подобно устройства if let
, устройство условного цикла while let
позволяет повторять цикл while
до тех пор, пока образец продолжает совпадать. Пример в приложении 18-2 отображает цикл while let
, который использует вектор в качестве обоймы и печатает значения вектора в порядке, обратном тому, в котором они были помещены.
-fn main() { - let mut stack = Vec::new(); - - stack.push(1); - stack.push(2); - stack.push(3); - - while let Some(top) = stack.pop() { - println!("{top}"); - } -}
-
В этом примере выводится 3, 2, а затем 1. Способ pop
извлекает последний элемент из вектора и возвращает Some(value)
. Если вектор пуст, то pop
возвращает None
. Цикл while
продолжает выполнение кода в своём разделе, пока pop
возвращает Some
. Когда pop
возвращает None
, цикл останавливается. Мы можем использовать while let
для удаления каждого элемента из обоймы.
for
В цикле for
значение, которое следует непосредственно за ключевым словом for
, является образцом. Например, в for x in y
выражение x
является образцом. В приложении 18-3 показано, как использовать образец в цикле for
, чтобы разъединять или разбить упорядоченный ряд как часть цикла for
.
-fn main() { - let v = vec!['a', 'b', 'c']; - - for (index, value) in v.iter().enumerate() { - println!("{value} is at index {index}"); - } -}
-
Код в приложении 18-3 выведет следующее:
-$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
- Running `target/debug/patterns`
-a is at index 0
-b is at index 1
-c is at index 2
-
-Мы приспособимтируем повторитель с помощью способа enumerate
, чтобы он порождал упорядоченный ряд, состоящий из значения и порядкового указателя этого значения. Первым созданным значением будет упорядоченный ряд (0, 'a')
. Когда это значение сопоставляется с образцом (index, value)
, index
будет равен 0
, а value
будет равно 'a'
и будет напечатана первая строка выходных данных.
let
До этой главы мы подробно обсуждали только использование образцов с match
и if let
, но на самом деле, мы использовали образцы и в других местах, в том числе в указаниях let
. Например, рассмотрим следующее простое назначение переменной с помощью let
:
-#![allow(unused)] -fn main() { -let x = 5; -}
Каждый раз, когда вы использовали подобным образом указанию let
, вы использовали образцы, хотя могли и не осознавать этого! Более условноуказание let
выглядит так:
let PATTERN = EXPRESSION;
-
-В указаниях вида let x = 5;
с именем переменной в слоте PATTERN
, имя переменной является просто отдельной, простой способом образца. Ржавчина сравнивает выражение с образцом и присваивает любые имена, которые он находит. Так что в примере let x = 5;
, x
- это образец, который означает "привязать то, что соответствует здесь, переменной x
". Поскольку имя x
является полностью образцом, этот образец в действительности означает "привязать все к переменной x
независимо от значения".
Чтобы более чётко увидеть особенность сопоставления с образцом let
, рассмотрим приложение 18-4, в котором используется образец с let
для разъединения упорядоченного ряда.
-fn main() { - let (x, y, z) = (1, 2, 3); -}
-
Здесь мы сопоставляем упорядоченный ряд с образцом. Ржавчина сравнивает значение (1, 2, 3)
с образцом (x, y, z)
и видит, что значение соответствует образцу, поэтому Ржавчина связывает 1
с x
, 2
с y
и 3
с z
. Вы можете думать об этом образце упорядоченного ряда как о вложении в него трёх отдельных образцов переменных.
Если количество элементов в образце не совпадает с количеством элементов в упорядоченном ряде, то весь вид не будет совпадать и мы получим ошибку сборщика. Например, в приложении 18-5 показана попытка разъединять упорядоченный ряд с тремя элементами в две переменные, что не будет работать.
-fn main() {
- let (x, y) = (1, 2, 3);
-}
--
Попытка собрать этот код приводит к ошибке:
-$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-error[E0308]: mismatched types
- --> src/main.rs:2:9
- |
-2 | let (x, y) = (1, 2, 3);
- | ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
- | |
- | expected a tuple with 3 elements, found one with 2 elements
- |
- = note: expected tuple `({integer}, {integer}, {integer})`
- found tuple `(_, _)`
-
-For more information about this error, try `rustc --explain E0308`.
-error: could not compile `patterns` (bin "patterns") due to 1 previous error
-
-Чтобы исправить ошибку, мы могли бы пренебрегать одно или несколько значений в упорядоченном ряде, используя _
или ..
, как вы увидите в разделе “Пренебрежение значений в Образце” . Если образец содержит слишком много переменных в образце, можно решить неполадку, сделав виды совпадающими, удалив некоторые переменные таким образом, чтобы число переменных равнялось числу элементов в упорядоченном ряде.
Свойства функции также могут быть образцами. Код в приложении 18-6 объявляет функцию с именем foo
, которая принимает один свойство с именем x
вида i32
, к настоящему времени это должно выглядеть знакомым.
-fn foo(x: i32) { - // code goes here -} - -fn main() {}
-
x
это часть образца! Как и в случае с let
, мы можем сопоставить упорядоченный ряд в переменных функции с образцом. Приложение 18-7 разделяет значения в упорядоченном ряде при его передачи в функцию.
Файл: src/main.rs
--fn print_coordinates(&(x, y): &(i32, i32)) { - println!("Current location: ({x}, {y})"); -} - -fn main() { - let point = (3, 5); - print_coordinates(&point); -}
-
Этот код печатает Current location: (3, 5)
. Значения &(3, 5)
соответствуют образцу &(x, y)
, поэтому x
- это значение 3
, а y
- это значение 5
.
Добавляя к вышесказанному, мы можем использовать образцы в списках свойств замыкания таким же образом, как и в списках свойств функции, потому что, как обсуждалось в главе 13, замыкания похожи на функции.
-На данный мгновение вы видели несколько способов использования образцов, но образцы работают не одинаково во всех местах, где их можно использовать. В некоторых местах образцы должны быть неопровержимыми; в других обстоятельствах они могут быть опровергнуты. Мы обсудим эти две подходы далее.
- -Образцы бывают двух видов: опровержимые и неопровержимые. Образцы, которые будут соответствовать любому возможному переданному значению, являются неопровержимыми (irrefutable). Примером может быть x
в указания let x = 5;
, потому что x
соответствует чему-либо и, следовательно, не может не совпадать. Образцы, которые могут не соответствовать некоторому возможному значению, являются опровержимыми (refutable). Примером может быть Some(x)
в выражении if let Some(x) = a_value
, потому что если значение в переменной a_value
равно None
, а не Some
, то образец Some(x)
не будет совпадать.
Свойства функций, указания let
и циклы for
могут принимать только неопровержимые образцы, поскольку программа не может сделать ничего значимого, если значения не совпадают. А выражения if let
и while let
принимают опровержимые и неопровержимые образцы, но сборщик предостерегает от неопровержимых образцов, поскольку по определению они предназначены для обработки возможного сбоя: возможность условного выражения заключается в его способности выполнять разный код в зависимости от успеха или неудачи.
В общем случае, вам не нужно беспокоиться о разнице между опровержимыми (refutable) и неопровержимыми (irrefutable) образцами; тем не менее, вам необходимо ознакомиться с подходом возможности опровержения, чтобы вы могли отреагировать на неё, увидев в сообщении об ошибке. В таких случаях вам потребуется изменить либо образец, либо устройство, с которой вы используете образец, в зависимости от предполагаемого поведения кода.
-Давайте посмотрим на пример того, что происходит, когда мы пытаемся использовать опровержимый образец, где Ржавчина требует неопровержимый образец, и наоборот. В приложении 18-8 показана указание let
, но для образца мы указали Some(x)
, являющийся образцом, который можно опровергнуть. Как и следовало ожидать, этот код не будет собираться.
fn main() {
- let some_option_value: Option<i32> = None;
- let Some(x) = some_option_value;
-}
--
Если some_option_value
было бы значением None
, то оно не соответствовало бы образцу Some(x)
, что означает, что образец является опровержимым. Тем не менее, указание let
может принимать только неопровержимый образец, потому что нет правильного кода, который может что-то сделать со значением None
. Во время сборки Ржавчина будет жаловаться на то, что мы пытались использовать опровержимый образец, для которого требуется неопровержимый образец:
$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-error[E0005]: refutable pattern in local binding
- --> src/main.rs:3:9
- |
-3 | let Some(x) = some_option_value;
- | ^^^^^^^ pattern `None` not covered
- |
- = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
- = note: for more information, visit https://doc.rust-lang.org/book/ch18-02-refutability.html
- = note: the matched value is of type `Option<i32>`
-help: you might want to use `let else` to handle the variant that isn't matched
- |
-3 | let Some(x) = some_option_value else { todo!() };
- | ++++++++++++++++
-
-For more information about this error, try `rustc --explain E0005`.
-error: could not compile `patterns` (bin "patterns") due to 1 previous error
-
-Поскольку мы не покрыли (и не могли покрыть!) каждое допустимое значение с помощью образца Some(x)
, то Ржавчина выдаёт ошибку сборки.
Чтобы исправить неполадку наличия опровержимого образца, там, где нужен неопровержимый образец, можно изменить код, использующий образец: вместо использования let
, можно использовать if let
. Затем, если образец не совпадает, выполнение кода внутри фигурных скобок будет пропущено, что даст возможность продолжить правильное выполнение. В приложении 18-9 показано, как исправить код из приложения 18-8.
-fn main() { - let some_option_value: Option<i32> = None; - if let Some(x) = some_option_value { - println!("{x}"); - } -}
-
Код исправлен! Этот код совершенно правильный, хотя это означает, что мы не можем использовать неопровержимый образец без получения ошибки. Если мы используем образец if let
, который всегда будет совпадать, то для примера x
, показанного в приложении 18-10, сборщик выдаст предупреждение.
-fn main() { - if let x = 5 { - println!("{x}"); - }; -}
-
Rust жалуется, что не имеет смысла использовать if let
с неопровержимым образцом:
$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-warning: irrefutable `if let` pattern
- --> src/main.rs:2:8
- |
-2 | if let x = 5 {
- | ^^^^^^^^^
- |
- = note: this pattern will always match, so the `if let` is useless
- = help: consider replacing the `if let` with a `let`
- = note: `#[warn(irrefutable_let_patterns)]` on by default
-
-warning: `patterns` (bin "patterns") generated 1 warning
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
- Running `target/debug/patterns`
-5
-
-По этой причине совпадающие ветки выражений должны использовать опровержимые образцы, за исключением последнего, который должен сопоставлять любые оставшиеся значения с неопровержимым образцом. Ржавчина позволяет нам использовать неопровержимый образец в match
только с одной веткой, но этот правила написания не особенно полезен и может быть заменён более простой указанием let
.
Теперь, когда вы знаете, где использовать образцы и разницу между опровержимыми и неопровержимыми образцами, давайте рассмотрим весь правила написания, который мы можем использовать для создания образцов.
- -В этом разделе мы рассмотрим все виды допустимого правил написания в образцах и расскажем, когда и для чего вам может понадобиться каждый из них.
-Как мы уже видели в главе 6, можно сопоставлять образцы с записями напрямую. В следующем коде есть несколько примеров:
--fn main() { - let x = 1; - - match x { - 1 => println!("one"), - 2 => println!("two"), - 3 => println!("three"), - _ => println!("anything"), - } -}
Этот код печатает one
, потому что значение в x
равно 1. Данный правила написания полезен, когда вы хотите, чтобы ваш код предпринял действие, если он получает определенное значение.
Именованные переменные - это неопровержимые (irrefutable) образцы, которые соответствуют любому значению и мы использовали их много раз в книге. Однако при использовании именованных переменных в выражениях match
возникает сложность. Поскольку match
начинает новую область видимости, то переменные, объявленные как часть образца внутри выражения match
, будут затенять переменные с тем же именем вне устройства match
как и в случае со всеми переменными. В приложении 18-11 мы объявляем переменную с именем x
со значением Some(5)
и переменную y
со значением 10
. Затем мы создаём выражение match
для значения x
. Посмотрите на образцы в ветках, println!
в конце и попытайтесь выяснить, какой код будет напечатан прежде чем запускать его или читать дальше.
Файл: src/main.rs
--fn main() { - let x = Some(5); - let y = 10; - - match x { - Some(50) => println!("Got 50"), - Some(y) => println!("Matched, y = {y}"), - _ => println!("Default case, x = {x:?}"), - } - - println!("at the end: x = {x:?}, y = {y}"); -}
-
Давайте рассмотрим, что происходит, когда выполняется выражение match
. Образец в первой ветке не соответствует определённому значению x
, поэтому выполнение продолжается.
Образец во второй ветке вводит новую переменную с именем y
, которая будет соответствовать любому значению в Some
. Поскольку мы находимся в новой области видимости внутри выражения match
, это новая переменная y
, а не y
которую мы объявили в начале со значением 10. Эта новая привязка y
будет соответствовать любому значению из Some
, которое находится в x
. Следовательно, эта новая y
связывается с внутренним значением Some
из переменной x
. Этим значением является 5
, поэтому выражение для этой ветки выполняется и печатает Matched, y = 5
.
Если бы x
было значением None
вместо Some(5)
, то образцы в первых двух ветках не совпали бы, поэтому значение соответствовало бы подчёркиванию. Мы не ввели переменную x
в образце ветки со знаком подчёркивания, поэтому x
в выражении все ещё является внешней переменной x
, которая не была затенена. В этом гипотетическом случае совпадение match
выведет Default case, x = None
.
Когда выражение match
завершается, заканчивается его область видимости как и область действия внутренней переменной y
. Последний println!
печатает at the end: x = Some(5), y = 10
.
Чтобы создать выражение match
, которое сравнивает значения внешних x
и y
, вместо введения затенённой переменной нужно использовать условие в сопоставлении образца. Мы поговорим про условие в сопоставлении образца позже в разделе “Дополнительные условия в сопоставлении образца”.
В выражениях match
можно сравнивать сразу с несколькими образцами, используя правила написания |
, который является оператором образца or. Например, в следующем примере мы сопоставляем значение x
с ветвями match, первая из которых содержит оператор or, так что если значение x
совпадёт с любым из значений в этой ветви, то будет выполнен её код:
-fn main() { - let x = 1; - - match x { - 1 | 2 => println!("one or two"), - 3 => println!("three"), - _ => println!("anything"), - } -}
Будет напечатано one or two
.
..=
правила написания ..=
позволяет нам выполнять сравнение с рядом значений. В следующем коде, когда в образце найдётся совпадение с любым из значений заданного ряда, будет выполнена эта ветка:
-fn main() { - let x = 5; - - match x { - 1..=5 => println!("one through five"), - _ => println!("something else"), - } -}
Если x
равен 1, 2, 3, 4 или 5, то совпадение будет достигнуто в первой ветке. Этот правила написания более удобен при указании нескольких значений для сравнения, чем использование оператора |
для определения этой же мысли; если бы мы решили использовать |
, нам пришлось бы написать 1 | 2 | 3 | 4 | 5
. Указание ряда намного короче, особенно если мы хотим подобрать, скажем, любое число от 1 до 1 000!
Сборщик проверяет, что рядне является пустым во время сборки, и поскольку единственными видами, для которых Ржавчина может определить, пуст рядили нет, являются char
и числовые значения, ряды допускаются только с числовыми или char
значениями.
Вот пример использования рядов значений char
:
-fn main() { - let x = 'c'; - - match x { - 'a'..='j' => println!("early ASCII letter"), - 'k'..='z' => println!("late ASCII letter"), - _ => println!("something else"), - } -}
Rust может сообщить, что 'c'
находится в ряде первого образца и напечатать early ASCII letter
.
Мы также можем использовать образцы для разъединения устройств, перечислений и упорядоченных рядов, чтобы использовать разные части этих значений. Давайте пройдёмся по каждому исходу.
-В приложении 18-12 показана устройства Point
с двумя полями x
и y
, которые мы можем разделить, используя образец с указанием let
.
Файл: src/main.rs
--struct Point { - x: i32, - y: i32, -} - -fn main() { - let p = Point { x: 0, y: 7 }; - - let Point { x: a, y: b } = p; - assert_eq!(0, a); - assert_eq!(7, b); -}
-
Этот код создаёт переменные a
и b
, которые сопоставляются значениям полей x
и y
устройства p
. Этот пример показывает, что имена переменных в образце не обязательно должны совпадать с именами полей устройства. Однако обычно имена переменных сопоставляются с именами полей, чтобы было легче запомнить, какие переменные взяты из каких полей. Из-за этого, а также из-за того, что строчка let Point { x: x, y: y } = p;
содержит много повторения, в Ржавчина ввели особое сокращение для образцов, соответствующих полям устройства: вам нужно только указать имя поля устройства, и тогда переменные, созданные из образца, будут иметь те же имена. Код в приложении 18-13 подобен коду в Приложении 18-12, но в образце let
создаются переменные x
и y
, вместо a
и b
.
Файл: src/main.rs
--struct Point { - x: i32, - y: i32, -} - -fn main() { - let p = Point { x: 0, y: 7 }; - - let Point { x, y } = p; - assert_eq!(0, x); - assert_eq!(7, y); -}
-
Этот код создаёт переменные x
и y
, которые соответствуют полям x
и y
из переменной p
. В итоге переменные x
и y
содержат значения из устройства p
.
А ещё, используя записанные значения в образце, мы можем разъединять, не создавая переменные для всех полей. Это даёт возможность, проверяя одни поля на соответствие определенным значениям, создавать переменные для разъединения других.
-В приложении 18-14 показано выражение match
, которое разделяет значения Point
на три случая: точки, которые лежат непосредственно на оси x
(что верно, когда y = 0
), на оси y
(x = 0
) или ни то, ни другое.
Файл: src/main.rs
--struct Point { - x: i32, - y: i32, -} - -fn main() { - let p = Point { x: 0, y: 7 }; - - match p { - Point { x, y: 0 } => println!("On the x axis at {x}"), - Point { x: 0, y } => println!("On the y axis at {y}"), - Point { x, y } => { - println!("On neither axis: ({x}, {y})"); - } - } -}
-
Первая ветвь будет соответствовать любой точке, лежащей на оси x
, если значение поля y
будет соответствовать записи 0
. Образец по-прежнему создаёт переменную x
, которую мы сможем использовать в коде этой ветви.
Подобно, вторая ветвь совпадёт с любой точкой на оси y
, в случае, если значение поля x
будет равно 0
, а для значения поля y
будет создана переменная y
. Третья ветвь не содержит никаких записей, поэтому она соответствует любому другому Point
и создаёт переменные как для поля x
, так и для поля y
.
В этом примере значение p
совпадает по второй ветке, так как x
содержит значение 0, поэтому этот код будет печатать On the y axis at 7
.
Помните, что выражение match
перестаёт проверять следующие ветви, как только оно находит первый совпадающий образец, поэтому, даже если Point { x: 0, y: 0}
находится на оси x
и оси y
, этот код будет печатать только On the x axis at 0
.
Мы уже разъединили перечисления в книге (см., например, приложение 6-5 главы 6), но
не обсуждали явно, что образец для разъединения перечисления должен соответствовать способу объявления данных, хранящихся в перечислении. Например, в приложении 18-15 мы используем перечисление Message
из приложения 6-2 и пишем match
с образцами, которые будут разъединять каждое внутреннее значение.
Файл: src/main.rs
--enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(i32, i32, i32), -} - -fn main() { - let msg = Message::ChangeColor(0, 160, 255); - - match msg { - Message::Quit => { - println!("The Quit variant has no data to destructure."); - } - Message::Move { x, y } => { - println!("Move in the x direction {x} and in the y direction {y}"); - } - Message::Write(text) => { - println!("Text message: {text}"); - } - Message::ChangeColor(r, g, b) => { - println!("Change the color to red {r}, green {g}, and blue {b}") - } - } -}
-
Этот код напечатает Change the color to red 0, green 160, and blue 255
. Попробуйте изменить значение переменной msg
, чтобы увидеть выполнение кода в других ветках.
Для исходов перечисления без каких-либо данных, вроде Message::Quit
, мы не можем разъединять значение, которого нет. Мы можем сопоставить только буквальное значение Message::Quit
в этом образце, но без переменных.
Для исходов перечисления похожих на устройства, таких как Message::Move
, можно использовать образец, подобный образцу, который мы указываем для сопоставления устройств. После имени исхода мы помещаем фигурные скобки и затем перечисляем поля именами переменных. Таким образом мы разделяем отрывки, которые будут использоваться в коде этой ветки. Здесь мы используем сокращённую разновидность, как в приложении 18-13.
Для исходов перечисления, подобных упорядоченному ряду, вроде Message::Write
, который содержит упорядоченный ряд с одним элементом и Message::ChangeColor
, содержащему упорядоченный ряд с тремя элементами, образец подобен тому, который мы указываем для сопоставления упорядоченных рядов. Количество переменных в образце должно соответствовать количеству элементов в исходе, который мы сопоставляем.
До сих пор все наши примеры сопоставляли устройства или перечисления на один уровень глубины, но сопоставление может работать и с вложенными элементами! Например, мы можем ресогласовать код в приложении 18-15 для поддержки цветов RGB и HSV в сообщении ChangeColor
, как показано в приложении 18-16.
-enum Color { - Rgb(i32, i32, i32), - Hsv(i32, i32, i32), -} - -enum Message { - Quit, - Move { x: i32, y: i32 }, - Write(String), - ChangeColor(Color), -} - -fn main() { - let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); - - match msg { - Message::ChangeColor(Color::Rgb(r, g, b)) => { - println!("Change color to red {r}, green {g}, and blue {b}"); - } - Message::ChangeColor(Color::Hsv(h, s, v)) => { - println!("Change color to hue {h}, saturation {s}, value {v}") - } - _ => (), - } -}
-
Образец первой ветки в выражении match
соответствует исходу перечисления Message::ChangeColor
, который содержит исход Color::Rgb
; затем образец привязывается к трём внутренними значениями i32
. Образец второй ветки также соответствует исходу перечисления Message::ChangeColor
, но внутреннее перечисление соответствует исходу Color::Hsv
. Мы можем указать эти сложные условия в одном выражении match
, даже если задействованы два перечисления.
Можно смешивать, сопоставлять и вкладывать образцы разъединения ещё более сложными способами. В следующем примере показана сложная разъединение, где мы вкладываем устройства и упорядоченные ряды внутрь упорядоченного ряда и разъединим из него все простые значения:
--fn main() { - struct Point { - x: i32, - y: i32, - } - - let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); -}
Этот код позволяет нам разбивать сложные виды на составные части, чтобы мы могли использовать нужным нас значения по отдельности.
-Разъединение с помощью образцов - это удобный способ использования отрывков значений, таких как как значение из каждого поля в устройстве, по отдельности друг от друга.
-Вы видели, что иногда полезно пренебрегать значения в образце, например в последней ветке match
, чтобы получить ветку, обрабатывающую любые значения, которая на самом деле ничего не делает, но учитывает все оставшиеся возможные значения. Есть несколько способов пренебрегать целые значения или части значений в образце: используя образец _
(который вы видели), используя образец _
внутри другого образца, используя имя, начинающееся с подчёркивания, либо используя ..
, чтобы пренебрегать оставшиеся части значения. Давайте рассмотрим, как и зачем использовать каждый из этих образцов.
_
Мы использовали подчёркивание (_
) в качестве образца подстановочного знака (wildcard), который будет сопоставляться с любом значением, но не будет привязываться к этому значению. Это особенно удобно в последней ветке выражения match
, но мы также можем использовать его в любом образце, в том числе в свойствах функции, как показано в приложении 18-17.
Файл: src/main.rs
--fn foo(_: i32, y: i32) { - println!("This code only uses the y parameter: {y}"); -} - -fn main() { - foo(3, 4); -}
-
Этот код полностью пренебрегает значение 3
, переданное в качестве первого переменной, и выведет на печать This code only uses the y parameter: 4
.
В большинстве случаев, когда вам больше не нужен какой-то из свойств функции, вы можете изменить её ярлык, убрав неиспользуемый свойство. Пренебрежение свойства функции может быть особенно полезно в случаях когда, например, вы выполняете особенность с определённой ярлыком, но тело функции в вашей выполнения не нуждается в одном из свойств. В таком случае сборщик не будет выдавать предупреждения о неиспользуемых свойствах функции, как это было бы, если бы вы указали имя свойства.
-_
Также, _
можно использовать внутри образцов, чтобы пренебрегать какую-то часть значения, например, когда мы хотим проверить только определённую подробность, а остальные свойства нам не понадобятся в коде, который нужно выполнить. В приложении 18-18 показан код, ответственный за управление значениями настроек. Согласно бизнес-требованиям, пользователь не может изменить установленное значение свойства, но может удалить его и задать ему новое значение, если на данный мгновение оно отсутствует.
-fn main() { - let mut setting_value = Some(5); - let new_setting_value = Some(10); - - match (setting_value, new_setting_value) { - (Some(_), Some(_)) => { - println!("Can't overwrite an existing customized value"); - } - _ => { - setting_value = new_setting_value; - } - } - - println!("setting is {setting_value:?}"); -}
-
Этот код будет печатать Can't overwrite an existing customized value
, а затем setting is Some(5)
. В первой ветке нам не нужно сопоставлять или использовать значения внутри исхода Some
, но нам нужно проверить случай, когда setting_value
и new_setting_value
являются исходом Some
. В этом случае мы печатаем причину, почему мы не меняем значение setting_value
и оно не меняется.
Во всех других случаях (если либо setting_value
, либо new_setting_value
являются исходом None
), выраженных образцом _
во второй ветке, мы хотим, чтобы new_setting_value
стало равно setting_value
.
Мы также можем использовать подчёркивание в нескольких местах в одном образце, чтобы пренебрегать определенные значения. Приложение 18-19 показывает пример пренебрежения второго и четвёртого значения в упорядоченном ряде из пяти элементов.
--fn main() { - let numbers = (2, 4, 8, 16, 32); - - match numbers { - (first, _, third, _, fifth) => { - println!("Some numbers: {first}, {third}, {fifth}") - } - } -}
-
Этот код напечатает Some numbers: 2, 8, 32
, а значения 4 и 16 будут пренебрежены.
_
в имениЕсли вы создаёте переменную, но нигде её не используете, Ржавчина обычно выдаёт предупреждение, потому что неиспользуемая переменная может быть ошибкой. Но иногда полезно создать переменную, которую вы пока не используете, например, когда вы создаёте протовид или только начинаете дело. В этой случаи вы можете сказать Ржавчина не предупреждать вас о неиспользуемой переменной, начав имя переменной с подчёркивания. В приложении 18-20 мы создаём две неиспользуемые переменные, но когда мы собираем такой код, мы должны получить предупреждение только об одной из них.
-Файл: src/main.rs
--fn main() { - let _x = 5; - let y = 10; -}
-
Здесь мы получаем предупреждение о том, что не используем переменную y
, но мы не получаем предупреждения о неиспользовании переменной_x
.
Обратите внимание, что есть небольшая разница между использованием только _
и использованием имени, начинающегося с подчёркивания. правила написания _x
по-прежнему привязывает значение к переменной, тогда как _
не привязывает ничего. В приложении 18-21 представлена ошибка, показывающая, в каком случае это различие имеет значение.
fn main() {
- let s = Some(String::from("Hello!"));
-
- if let Some(_s) = s {
- println!("found a string");
- }
-
- println!("{s:?}");
-}
--
Мы получим ошибку, поскольку значение s
все равно будет перемещено в _s
, что не позволит нам больше воспользоваться s
. Однако использование подчёркивания само по себе никогда не приводит к привязке к значению. Приложение 18-22 собирается без ошибок, поскольку s
не будет перемещён в _
.
-fn main() { - let s = Some(String::from("Hello!")); - - if let Some(_) = s { - println!("found a string"); - } - - println!("{s:?}"); -}
-
Этот код работает правильно, потому что мы никогда не привязываем s
к чему либо; оно не перемещается.
..
Со значениями, которые имеют много частей, можно использовать правила написания ..
, чтобы использовать только некоторые части и пренебрегать остальные, избегая необходимости перечислять подчёркивания для каждого пренебрегаемого значения. Образец ..
пренебрегает любые части значения, которые мы явно не сопоставили в остальной частью образца. В приложении 18-23 мы имеем устройство Point
, которая содержит координату в трёхмерном пространстве. В выражении match
мы хотим работать только с координатой x
и пренебрегать значения полей y
и z
.
-fn main() { - struct Point { - x: i32, - y: i32, - z: i32, - } - - let origin = Point { x: 0, y: 0, z: 0 }; - - match origin { - Point { x, .. } => println!("x is {x}"), - } -}
-
Мы перечисляем значение x
и затем просто включаем образец ..
. Это быстрее, чем перечислять y: _
и z: _
, особенно когда мы работаем со устройствами, которые имеют много полей, в случаейх, когда только одно или два поля представляют для нас влечение.
правила написания ..
раскроется до необходимого количества значений. В приложении 18-24 показано, как использовать ..
с упорядоченным рядом.
Файл: src/main.rs
--fn main() { - let numbers = (2, 4, 8, 16, 32); - - match numbers { - (first, .., last) => { - println!("Some numbers: {first}, {last}"); - } - } -}
-
В этом коде первое и последнее значение соответствуют first
и last
. Устройство ..
будет соответствовать и пренебрегать всё, что находится между ними.
Однако использование ..
должно быть однозначным. Если неясно, какие значения предназначены для сопоставления, а какие следует пренебрегать, Ржавчина выдаст ошибку. В приложении 18-25 показан пример неоднозначного использования ..
, поэтому он не будет собираться.
Файл: src/main.rs
-fn main() {
- let numbers = (2, 4, 8, 16, 32);
-
- match numbers {
- (.., second, ..) => {
- println!("Some numbers: {second}")
- },
- }
-}
--
При сборки примера, мы получаем эту ошибку:
-$ cargo run
- Compiling patterns v0.1.0 (file:///projects/patterns)
-error: `..` can only be used once per tuple pattern
- --> src/main.rs:5:22
- |
-5 | (.., second, ..) => {
- | -- ^^ can only be used once per tuple pattern
- | |
- | previously used here
-
-error: could not compile `patterns` (bin "patterns") due to 1 previous error
-
-Rust не может определить, сколько значений в упорядоченном ряде нужно пренебрегать, прежде чем сопоставить значение с second
, и сколько следующих значений пренебрегать после этого. Этот код может означать, что мы хотим пренебрегать 2
, связать second
с 4
, а затем пренебрегать 8
, 16
и 32
; или что мы хотим пренебрегать 2
и 4
, связать second
с 8
, а затем пренебрегать 16
и 32
; и так далее. Имя переменной second
не означает ничего особенного для Rust, поэтому мы получаем ошибку сборщика, так как использование ..
в двух местах как здесь, является неоднозначным.
Условие сопоставления (match guard) является дополнительным условием if
, указанным после образца в ветке match
, которое также должно быть выполнено, чтобы ветка была выбрана. Условия сопоставления полезны для выражения более сложных мыслей, чем позволяет только образец.
Условие может использовать переменные, созданные в образце. В приложении 18-26 показан match
, в котором первая ветка имеет образец Some(x)
, а также имеет условие сопоставления, if x % 2 == 0
(которое будет истинным, если число чётное).
-fn main() { - let num = Some(4); - - match num { - Some(x) if x % 2 == 0 => println!("The number {x} is even"), - Some(x) => println!("The number {x} is odd"), - None => (), - } -}
-
В этом примере будет напечатано The number 4 is even
. Когда num
сравнивается с образцом в первой ветке, он совпадает, потому что Some(4)
соответствует Some(x)
. Затем условие сопоставления проверяет, равен ли 0 остаток от деления x
на 2 и если это так, то выбирается первая ветка.
Если бы num
вместо этого было Some(5)
, условие в сопоставлении первой ветки было бы ложным, потому что остаток от 5 делённый на 2, равен 1, что не равно 0. Ржавчина тогда перешёл бы ко второй ветке, которое совпадает, потому что вторая ветка не имеет условия сопоставления и, следовательно, соответствует любому исходу Some
.
Невозможно выразить условие if x % 2 == 0
внутри образца, поэтому условие в сопоставлении даёт нам возможность выразить эту логику. Недостатком этой дополнительной выразительности является то, что сборщик не пытается проверять полноту, когда задействованы выражения с условием в сопоставлении.
В приложении 18-11 мы упомянули, что можно использовать условия сопоставления для решения нашей сбоев затенения образца. Напомним, что внутри образца в выражении match
была создана новая переменная, вместо использования внешней к match
переменной. Эта новая переменная означала, что мы не могли выполнить сравнение с помощью значения внешней переменной. В приложении 18-27 показано, как мы можем использовать условие сопоставления для решения этой сбоев.
Файл: src/main.rs
--fn main() { - let x = Some(5); - let y = 10; - - match x { - Some(50) => println!("Got 50"), - Some(n) if n == y => println!("Matched, n = {n}"), - _ => println!("Default case, x = {x:?}"), - } - - println!("at the end: x = {x:?}, y = {y}"); -}
-
Этот код теперь напечатает Default case, x = Some(5)
. Образец во второй ветке не вводит новую переменную y
, которая будет затенять внешнюю y
, это означает, что теперь можно использовать внешнюю переменную y
в условии сопоставления. Вместо указания образца как Some(y)
, который бы затенял бы внешнюю y
, мы указываем Some(n)
. Это создаёт новую переменную n
, которая ничего не затеняет, так как переменной n
нет вне устройства match
.
Условие сопоставления if n == y
не является образцом и следовательно, не вводит новые переменные. Переменная y
и есть внешняя y
, а не новая затенённая y
, и теперь мы можем искать элемент, который будет иметь то же значение, что и внешняя y
, путём сравнения n
и y
.
Вы также можете использовать оператор или |
в условии сопоставления, чтобы указать несколько образцов; условие сопоставления будет применяться ко всем образцам. В приложении 18-28 показан приоритет соединения условия сопоставления с образцом, который использует |
. Важной частью этого примера является то, что условие сопоставления if y
применяется к 4
, 5
, и к 6
, хотя это может выглядеть как будто if y
относится только к 6
.
-fn main() { - let x = 4; - let y = false; - - match x { - 4 | 5 | 6 if y => println!("yes"), - _ => println!("no"), - } -}
-
Условие сопоставления гласит, что ветка совпадает, только если значение x
равно 4
, 5
или 6
, и если y
равно true
. Когда этот код выполняется, образец первой ветки совпадает, потому что x
равно 4
, но условие сопоставления if y
равно false, поэтому первая ветка не выбрана. Код переходит ко второй ветке, которая совпадает, и эта программа печатает no
. Причина в том, что условие if
применяется ко всему образцу 4 | 5 | 6
, а не только к последнему значению 6
. Другими словами, приоритет условия сопоставления по отношению к образцу ведёт себя так:
(4 | 5 | 6) if y => ...
-
-а не так:
-4 | 5 | (6 if y) => ...
-
-После запуска кода, старшинство в поведении становится очевидным: если условие сопоставления применялось бы только к конечному значению в списке, указанном с помощью оператора |
, то ветка бы совпала и программа напечатала бы yes
.
@
Оператор at (@
) позволяет создать переменную, которая содержит значение, одновременно с тем, как мы проверяем, соответствует ли это значение образцу. В приложении 18-29 показан пример, в котором мы хотим проверить, что перечисление Message::Hello
со значением поля id
находится в ряде 3..=7
. Но мы также хотим привязать такое значение к переменной id_variable
, чтобы использовать его внутри кода данной ветки. Мы могли бы назвать эту переменную id
, так же как поле, но для этого примера мы будем использовать другое имя.
-fn main() { - enum Message { - Hello { id: i32 }, - } - - let msg = Message::Hello { id: 5 }; - - match msg { - Message::Hello { - id: id_variable @ 3..=7, - } => println!("Found an id in range: {id_variable}"), - Message::Hello { id: 10..=12 } => { - println!("Found an id in another range") - } - Message::Hello { id } => println!("Found some other id: {id}"), - } -}
-
В этом примере будет напечатано Found an id in range: 5
. Указывая id_variable @
перед рядом 3..=7
, мы захватываем любое значение, попадающее в ряд, одновременно проверяя, что это значение соответствует ряду в образце.
Во второй ветке, где у нас в образце указан только ряд, код этой ветки не имеет переменной, которая содержит действительное значение поля id
. Значение поля id
могло бы быть 10, 11 или 12, но код, соответствующий этому образцу, не знает, чему оно равно. Код образца не может использовать значение из поля id
, потому что мы не сохранили значение id
в переменной.
В последней ветке, где мы указали переменную без ряда, у нас есть значение, доступное для использования в коде ветки, в переменной с именем id
. Причина в том, что мы использовали упрощённый правила написания полей устройства. Но мы не применяли никакого сравнения со значением в поле id
в этой ветке, как мы это делали в первых двух ветках: любое значение будет соответствовать этому образцу.
Использование @
позволяет проверять значение и сохранять его в переменной в пределах одного образца.
Образцы Ржавчина очень помогают различать разные виды данных. При использовании их в выражениях match
, Ржавчина заверяет, что ваши образцы охватывают все возможные значения, потому что иначе ваша программа не собирается. Образцы в указаниях let
и свойствах функций делают такие устройства более полезными, позволяя разбивать элементы на более мелкие части, одновременно присваивая их значения переменным. Мы можем создавать простые или сложные образцы в соответствии с нашими потребностями.
Далее, в предпоследней главе книги, мы рассмотрим некоторые продвинутые особенности различных возможностей Rust.
- -На данный мгновение вы изучили все наиболее используемые части языка программирования Rust. Прежде чем мы выполним ещё один дело в главе 20, мы рассмотрим несколько особенностей языка, с которыми вы можете сталкиваться время от времени, но не использовать каждый день. Вы можете использовать эту главу в качестве справочника, когда столкнётесь с какими-либо незнакомыми вещами. Рассмотренные здесь функции будут полезны в очень отличительных случаейх. Хотя вы, возможно, не будете часто пользоваться ими, мы хотим убедиться, что вы знаете все возможности языка Rust.
-В этой главе мы рассмотрим:
-Это набор возможностей Ржавчина для всех! Давайте погрузимся в него!
- -Во всех предыдущих главах этой книги мы обсуждали код на Rust, безопасность памяти в котором обеспечивается во время сборки. Однако внутри Ржавчина скрывается другой язык - небезопасный Rust, который не обеспечивает безопасной работы с памятью. Этот язык называется unsafe Rust и работает также как и первый, но предоставляет вам дополнительные возможности.
-Небезопасный Ржавчина существует потому что по своей природе постоянной анализ довольно устоявшийся. Когда сборщик пытается определить, соответствует ли код заверениям, то он скорее отвергнет несколько допустимых программ, чем пропустит несколько недопустимых. Не смотря на то, что код может быть в порядке, если сборщик Ржавчина не будет располагать достаточной сведениями, чтобы убедиться в этом, он отвергнет код. В таких случаях вы можете использовать небезопасный код, чтобы сказать сборщику: "Поверь мне, я знаю, что делаю". Однако имейте в виду, что вы используете небезопасный Ржавчина на свой страх и риск: если вы неправильно используете небезопасный код, могут возникнуть сбоев, связанные с нарушением безопасности памяти, например, разыменование нулевого указателя.
-Другая причина, по которой у Ржавчина есть небезопасное альтер эго, заключается в том, что по существу аппаратное обеспечение компьютера небезопасно. Если Ржавчина не позволял бы вам выполнять небезопасные действия, вы не могли бы выполнять определённые задачи. Ржавчина должен позволить вам использовать системное, низкоуровневое программирование, такое как прямое взаимодействие с операционной системой, или даже написание вашей собственной операционной системы. Возможность написания низкоуровневого, системного кода является одной из целей языка. Давайте рассмотрим, что и как можно делать с небезопасным Rust.
-Чтобы переключиться на небезопасный Rust, используйте ключевое слово unsafe
, а затем начните новый блок, содержащий небезопасный код. В небезопасном Ржавчина можно выполнять пять действий, которые недоступны в безопасном Rust, которые мы называем небезопасными супер силами. Эти супер силы включают в себя следующее:
union
Важно понимать, что unsafe
не отключает проверку заимствования или любые другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она всё равно будет проверена. Единственное, что делает ключевое слово unsafe
- даёт вам доступ к этим пяти возможностям, безопасность работы с памятью в которых не проверяет сборщик. Вы по-прежнему получаете некоторую степень безопасности внутри небезопасного раздела.
Кроме того, unsafe
не означает, что код внутри этого раздела является неизбежно опасным или он точно будет иметь сбоев с безопасностью памяти: цель состоит в том, что вы, как программист, заверяете, что код внутри раздела unsafe
будет обращаться к действительной памяти правильным образом.
Люди подвержены ошибкам и ошибки будут происходить, но требуя размещение этих четырёх небезопасных действия внутри разделов, помеченных как unsafe
, вы будете знать, что любые ошибки, связанные с безопасностью памяти, будут находиться внутри unsafe
разделов. Делайте unsafe
разделы маленькими; вы будете благодарны себе за это позже, при исследовании ошибок с памятью.
Чтобы наиболее изолировать небезопасный код, советуется заключить небезопасный код в безопасную абстракцию и предоставить безопасный API, который мы обсудим позже, когда будем обсуждать небезопасные функции и способы. Части встроенной библиотеки выполнены как проверенные, безопасные абстракции над небезопасным кодом. Оборачивание небезопасного кода в безопасную абстракцию предотвращает возможную утечку использования unsafe
кода во всех местах, где вы или ваши пользователи могли бы захотеть напрямую использовать возможность, выполненную unsafe
кодом, потому что использование безопасной абстракции само безопасно.
Давайте поговорим о каждой из четырёх небезопасных сверх способностей, и по ходу дела рассмотрим некоторые абстракции, которые обеспечивают безопасный внешняя оболочка для небезопасного кода.
-В главе 4 раздела "Недействительные ссылки" мы упоминали, что сборщик заверяет, что ссылки всегда действительны. Небезопасный Ржавчина имеет два новых вида, называемых сырыми указателями (raw pointers), которые похожи на ссылки. Как и в случае ссылок, сырые указатели могут быть неизменяемыми или изменяемыми и записываться как *const T
и *mut T
соответственно. Звёздочка не является оператором разыменования; это часть имени вида. В среде сырых указателей неизменяемый (immutable) означает, что указателю нельзя напрямую присвоить что-то после того как он разыменован.
В отличие от ссылок и умных указателей, сырые указатели:
-Отказавшись от этих заверений, вы можете обменять безопасность на большую производительность или возможность взаимодействия с другим языком или оборудованием, где заверения Ржавчина не применяются.
-В приложении 19-1 показано, как создать неизменяемый и изменяемый сырой указатель из ссылок.
--fn main() { - let mut num = 5; - - let r1 = &num as *const i32; - let r2 = &mut num as *mut i32; -}
-
Обратите внимание, что мы не используем ключевое слово unsafe
в этом коде. Можно создавать сырые указатели в безопасном коде; мы просто не можем разыменовывать сырые указатели за пределами небезопасного раздела, как вы увидите чуть позже.
Мы создали сырые указатели, используя as
для приведения неизменяемой и изменяемой ссылки к соответствующим им видам сырых указателей. Поскольку мы создали их непосредственно из ссылок, которые обязательно являются действительными, мы знаем, что эти определенные сырые указатели являются действительными, но мы не можем делать такое же предположение о любом сыром указателе.
Чтобы отобразить это, создадим сырой указатель, в достоверности которого мы не можем быть так уверены. В приложении 19-2 показано, как создать необработанный указатель на произвольное место в памяти. Попытка использовать произвольную память является непредсказуемой: по этому адресу могут быть данные, а могут и не быть, сборщик может перерабатывать код так, что доступа к памяти не будет, или программа может завершиться с ошибкой сегментации. Обычно нет веских причин писать такой код, но это возможно.
--fn main() { - let address = 0x012345usize; - let r = address as *const i32; -}
-
Напомним, что можно создавать сырые указатели в безопасном коде, но нельзя разыменовывать сырые указатели и читать данные, на которые они указывают. В приложении 19-3 мы используем оператор разыменования *
для сырого указателя, который требует unsafe
раздела.
-fn main() { - let mut num = 5; - - let r1 = &num as *const i32; - let r2 = &mut num as *mut i32; - - unsafe { - println!("r1 is: {}", *r1); - println!("r2 is: {}", *r2); - } -}
-
Создание указателей безопасно. Только при попытке доступа к предмету по адресу в указателе мы можем получить недопустимое значение.
-Также обратите внимание, что в примерах кода 19-1 и 19-3 мы создали *const i32
и *mut i32
, которые ссылаются на одну и ту же область памяти, где хранится num
. Если мы попытаемся создать неизменяемую и изменяемую ссылку на num
вместо сырых указателей, такой код не собирается, т.к. будут нарушены правила заимствования, запрещающие наличие изменяемой ссылки одновременно с неизменяемыми ссылками. С помощью сырых указателей мы можем создать изменяемый указатель и неизменяемый указатель на одну и ту же область памяти и изменять данные с помощью изменяемого указателя, возможно создавая эффект гонки данных. Будьте осторожны!
С учётом всех этих опасностей, зачем тогда использовать сырые указатели? Одним из основных применений является взаимодействие с кодом C, как вы увидите в следующем разделе "Вызов небезопасной функции или способа". Другой случай это создание безопасных абстракций, которые не понимает анализатор заимствований. Мы введём понятие небезопасных функций и затем рассмотрим пример безопасной абстракции, которая использует небезопасный код.
-Второй вид действий, которые можно выполнять в небезопасном разделе - это вызов небезопасных функций. Небезопасные функции и способы выглядят точно так же, как обычные функции и способы, но перед остальным определением у них есть дополнительное unsafe
. Ключевое слово unsafe
в данном среде указывает на то, что к функции предъявляются требования, которые мы должны соблюдать при вызове этой функции, поскольку Ржавчина не может обеспечить, что мы их выполняем. Вызывая небезопасную функцию внутри раздела unsafe
, мы говорим, что прочитали документацию к этой функции и берём на себя ответственность за соблюдение её условий.
Вот небезопасная функция с именем dangerous
которая ничего не делает в своём теле:
-fn main() { - unsafe fn dangerous() {} - - unsafe { - dangerous(); - } -}
Мы должны вызвать функцию dangerous
в отдельном unsafe
разделе. Если мы попробуем вызвать dangerous
без unsafe
раздела, мы получим ошибку:
$ cargo run
- Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
-error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or block
- --> src/main.rs:4:5
- |
-4 | dangerous();
- | ^^^^^^^^^^^ call to unsafe function
- |
- = note: consult the function's documentation for information on how to avoid undefined behavior
-
-For more information about this error, try `rustc --explain E0133`.
-error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
-
-С помощью раздела unsafe
мы сообщаем Rust, что прочитали документацию к функции, поняли, как правильно её использовать, и убедились, что выполняем договор функции.
Тела небезопасных функций являются в действительности unsafe
разделами, поэтому для выполнения других небезопасных действий внутри небезопасной функции не нужно добавлять ещё один unsafe
блок.
То, что функция содержит небезопасный код, не означает, что мы должны пометить всю функцию как небезопасную. На самом деле, обёртывание небезопасного кода в безопасную функцию - это обычная абстракция. В качестве примера рассмотрим функцию split_at_mut
из встроенной библиотеки, которая требует некоторого небезопасного кода. Рассмотрим, как мы могли бы её выполнить. Этот безопасный способ определён для изменяемых срезов: он берет один срез и превращает его в два, разделяя срез по порядковому указателю, указанному в качестве переменной. В приложении 19-4 показано, как использовать split_at_mut
.
-fn main() { - let mut v = vec![1, 2, 3, 4, 5, 6]; - - let r = &mut v[..]; - - let (a, b) = r.split_at_mut(3); - - assert_eq!(a, &mut [1, 2, 3]); - assert_eq!(b, &mut [4, 5, 6]); -}
-
Эту функцию нельзя выполнить, используя только безопасный Rust. Попытка выполнения могла бы выглядеть примерно как в приложении 19-5, который не собирается. Для простоты мы выполняем split_at_mut
как функцию, а не как способ, и только для значений вида i32
, а не обобщённого вида T
.
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- let len = values.len();
-
- assert!(mid <= len);
-
- (&mut values[..mid], &mut values[mid..])
-}
-
-fn main() {
- let mut vector = vec![1, 2, 3, 4, 5, 6];
- let (left, right) = split_at_mut(&mut vector, 3);
-}
--
Эта функция сначала получает общую длину среза. Затем она проверяет (assert), что порядковый указатель, переданный в качестве свойства, находится в границах среза, сравнивая его с длиной. Assert означает, что если мы передадим порядковый указатель, который больше, чем длина среза, функция запаникует ещё до попытки использования этого порядкового указателя.
-Затем мы возвращаем два изменяемых отрывка в упорядоченном ряде: один от начала исходного отрывка до mid
порядкового указателя (не включая сам mid), а другой - от mid
(включая сам mid) до конца отрывка.
При попытке собрать код в приложении 19-5, мы получим ошибку.
-$ cargo run
- Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
-error[E0499]: cannot borrow `*values` as mutable more than once at a time
- --> src/main.rs:6:31
- |
-1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
- | - let's call the lifetime of this reference `'1`
-...
-6 | (&mut values[..mid], &mut values[mid..])
- | --------------------------^^^^^^--------
- | | | |
- | | | second mutable borrow occurs here
- | | first mutable borrow occurs here
- | returning this value requires that `*values` is borrowed for `'1`
- |
- = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
-
-For more information about this error, try `rustc --explain E0499`.
-error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
-
-Анализатор заимствований Ржавчина не может понять, что мы заимствуем различные части среза, он понимает лишь, что мы хотим осуществить заимствование частей одного среза дважды. Заимствование различных частей среза в принципе в порядке вещей, потому что они не перекрываются, но Ржавчина недостаточно умён, чтобы это понять. Когда мы знаем, что код верный, но Ржавчина этого не понимает, значит пришло время прибегнуть к небезопасному коду.
-Приложение 19-6 отображает, как можно использовать unsafe
блок, сырой указатель и вызовы небезопасных функций чтобы split_at_mut
заработала:
-use std::slice; - -fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { - let len = values.len(); - let ptr = values.as_mut_ptr(); - - assert!(mid <= len); - - unsafe { - ( - slice::from_raw_parts_mut(ptr, mid), - slice::from_raw_parts_mut(ptr.add(mid), len - mid), - ) - } -} - -fn main() { - let mut vector = vec![1, 2, 3, 4, 5, 6]; - let (left, right) = split_at_mut(&mut vector, 3); -}
-
Напомним, из раздела "Вид срез" главы 4, что срезы состоят из указателя на некоторые данные и длины. Мы используем способ len
для получения длины среза и способ as_mut_ptr
для доступа к сырому указателю среза. Поскольку у нас есть изменяемый срез на значения вида i32
, функция as_mut_ptr
возвращает сырой указатель вида *mut i32
, который мы сохранили в переменной ptr
.
Далее проверяем, что порядковый указательmid
находится в границах среза. Затем мы обращаемся к небезопасному коду: функция slice::from_raw_parts_mut
принимает сырой указатель, длину и создаёт срез. Мы используем эту функцию для создания среза, начинающегося с ptr
и имеющего длину в mid
элементов. Затем мы вызываем способ add
у ptr
с mid
в качестве переменной, чтобы получить сырой указатель, который начинается с mid
, и создаём срез, используя этот указатель и оставшееся количество элементов после mid
в качестве длины.
Функция slice::from_raw_parts_mut
является небезопасной, потому что она принимает необработанный указатель и должна полагаться на то, что этот указатель действителен. Способ add
для необработанных указателей также небезопасен, поскольку он должен считать, что местоположение смещения также является действительным указателем. Поэтому мы были вынуждены разместить unsafe
разделвокруг наших вызовов slice::from_raw_parts_mut
и add
, чтобы иметь возможность вызвать их. Посмотрев на код и добавив утверждение, что mid
должен быть меньше или равен len
, мы можем сказать, что все необработанные указатели, используемые в разделе unsafe
, будут правильными указателями на данные внутри среза. Это приемлемое и уместное использование unsafe
.
Обратите внимание, что нам не нужно помечать результирующую функцию split_at_mut
как unsafe
, и мы можем вызвать эту функцию из безопасного Rust. Мы создали безопасную абстракцию для небезопасного кода с помощью выполнения функции, которая использует код unsafe
раздела безопасным образом, поскольку она создаёт только допустимые указатели из данных, к которым эта функция имеет доступ.
Напротив, использование slice::from_raw_parts_mut
в приложении 19-7 приведёт к вероятному сбою при использовании среза. Этот код использует произвольный адрес памяти и создаёт срез из 10000 элементов.
-fn main() { - use std::slice; - - let address = 0x01234usize; - let r = address as *mut i32; - - let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; -}
-
Мы не владеем памятью в этом произвольном месте, и нет никакой заверения, что созданный этим кодом отрывок содержит допустимые значения i32
. Попытка использовать values
так, как будто это допустимый срез, приводит к неопределённому поведению.
extern
функций для вызова внешнего кодаИногда вашему коду на языке Ржавчина может потребоваться взаимодействие с кодом, написанным на другом языке. Для этого в Ржавчина есть ключевое слово extern
, которое облегчает создание и использование внешней оболочки внешних функций (Foreign Function Interface - FFI). FFI - это способ для языка программирования определить функции и позволить другому (внешнему) языку программирования вызывать эти функции.
Приложение 19-8 отображает, как настроить встраивание с функцией abs
из встроенной библиотеки C. Функции, объявленные внутри разделов extern
, всегда небезопасны для вызова из кода Rust. Причина в том, что другие языки не обеспечивают соблюдение правил и заверений Rust, Ржавчина также не может проверить заверения, поэтому ответственность за безопасность ложится на программиста.
Имя файла: src/main.rs
--extern "C" { - fn abs(input: i32) -> i32; -} - -fn main() { - unsafe { - println!("Absolute value of -3 according to C: {}", abs(-3)); - } -}
-
Внутри раздела extern "C"
мы перечисляем имена и ярлыки внешних функций из другого языка, которые мы хотим вызвать. Часть "C"
определяет какой application binary interface (ABI - двоичный внешняя оболочка приложений) использует внешняя функция. Внешнюю оболочку ABI определяет как вызвать функцию на уровне ассемблера. Использование ABI "C"
является наиболее часто используемым и следует правилам ABI внешней оболочки языка Си.
--Вызов функций Ржавчина из других языков
-Также можно использовать
-extern
для создания внешней оболочки, позволяющего другим языкам вызывать функции Rust. Вместо того чтобы создавать целый разделextern
, мы добавляем ключевое словоextern
и указываем ABI для использования непосредственно перед ключевым словомfn
для необходимой функции. Нам также нужно добавить изложение#[no_mangle]
, чтобы сказать сборщику Ржавчина не искажать имя этой функции. Искажение - это когда сборщик меняет имя, которое мы дали функции, на другое имя, которое содержит больше сведений для других частей этапа сборки, но менее читабельно для человека. Сборщик каждого языка программирования искажает имена по-разному, поэтому, чтобы функция Ржавчина могла быть использована другими языками, мы должны отключить искажение имён в сборщике Rust.В следующем примере мы делаем функцию
-call_from_c
доступной из кода на C, после того как она будет собрана в разделяемую библиотеку и прилинкована с C:-#![allow(unused)] -fn main() { -#[no_mangle] -pub extern "C" fn call_from_c() { - println!("Just called a Ржавчина function from C!"); -} -}
Такое использование
-extern
не требуетunsafe
.
В этой книге мы ещё не говорили о вездесущих переменных, которые Ржавчина поддерживает, но с которыми могут возникнуть сбоев из-за действующих в Ржавчина правил владения. Если два потока обращаются к одной и той же изменяемой вездесущей переменной, это может привести к гонке данных.
-Вездесущие переменные в Ржавчина называют постоянными (static). Приложение 19-9 отображает пример объявления и использования в качестве значения постоянной переменной, имеющей вид строкового среза:
-Имя файла: src/main.rs
--static HELLO_WORLD: &str = "Hello, world!"; - -fn main() { - println!("name is: {HELLO_WORLD}"); -}
-
Постоянные переменные похожи на постоянные значения, которые мы обсуждали в разделе “Различия между переменными и постоянными значениями” главы 3. Имена постоянных переменных по общему соглашению пишутся в наставлении SCREAMING_SNAKE_CASE
, и мы должны указывать вид переменной, которым в данном случае является &'static str
. Постоянные переменные могут хранить только ссылки со временем жизни 'static
, это означает что сборщик Ржавчина может вывести время жизни и нам не нужно прописывать его явно. Доступ к неизменяемой постоянной переменной является безопасным.
Тонкое различие между постоянными значениями и неизменяемыми постоянными переменными заключается в том, что значения в постоянной переменной имеют определенный адрес в памяти. При использовании значения всегда будут доступны одни и те же данные. Постоянного значения, с другой стороны, могут повторять свои данные при каждом использовании. Ещё одно отличие заключается в том, что постоянные переменные могут быть изменяемыми. Обращение к изменяемым постоянном переменным и их изменение является небезопасным. В приложении 19-10 показано, как объявить, получить доступ и изменять изменяемую постоянную переменную с именем COUNTER
.
Имя файла: src/main.rs
--static mut COUNTER: u32 = 0; - -fn add_to_count(inc: u32) { - unsafe { - COUNTER += inc; - } -} - -fn main() { - add_to_count(3); - - unsafe { - println!("COUNTER: {COUNTER}"); - } -}
-
Как и с обычными переменными, мы определяем изменяемость с помощью ключевого слова mut
. Любой код, который читает из или пишет в переменную COUNTER
должен находиться в unsafe
разделе. Этот код собирается и печатает COUNTER: 3
, как и следовало ожидать, потому что выполняется в одном потоке. Наличие нескольких потоков с доступом к COUNTER
приведёт к случаи гонки данных.
Наличие изменяемых данных, которые доступны вездесуще, делает трудным выполнение заверения отсутствия гонок данных, поэтому Ржавчина считает изменяемые постоянные переменные небезопасными. Там, где это возможно, предпочтительно использовать техники многопоточности и умные указатели, направленные на многопоточное исполнение, которые мы обсуждали в главе 16. Таким образом, сборщик сможет проверить, что обращение к данным, доступным из разных потоков, выполняется безопасно.
-Мы можем использовать unsafe
для выполнения небезопасного особенности. Особенность является небезопасным, если хотя бы один из его способов имеет некоторый неизменная величина, который сборщик не может проверить. Мы объявляем особенности unsafe
, добавляя ключевое слово unsafe
перед trait
и помечая выполнение особенности как unsafe
, как показано в приложении 19-11.
-unsafe trait Foo { - // methods go here -} - -unsafe impl Foo for i32 { - // method implementations go here -} - -fn main() {}
-
Используя unsafe impl
, мы даём обещание поддерживать неизменные величины, которые сборщик не может проверить.
Для примера вспомним маркерные особенности Sync
и Send
, которые мы обсуждали в разделе "Расширяемый одновременность с помощью особенностей Sync
и Send
" главы 16: сборщик выполняет эти особенности самостоятельно , если наши виды полностью состоят из видов Send
и Sync
. Если мы создадим вид, который содержит вид, не являющийся Send
или Sync
, такой, как сырой указатель, и мы хотим пометить этот вид как Send
или Sync
, мы должны использовать unsafe
блок. Ржавчина не может проверить, что наш вид поддерживает заверения того, что он может быть безопасно передан между потоками или доступен из нескольких потоков; поэтому нам нужно добавить эти проверки вручную и указать это с помощью unsafe
.
Последнее действие, которое работает только с unsafe
- это доступ к полям union. union
похож на struct
, но в каждом определенном образце одновременно может использоваться только одно объявленное поле. Объединения в основном используются для взаимодействия с объединениями в коде на языке Си. Доступ к полям объединений небезопасен, поскольку Ржавчина не может обязательно определить вид данных, которые в данный мгновение хранятся в образце объединения. Подробнее об объединениях вы можете узнать в the Ржавчина Reference.
Использование unsafe
для выполнения одного из пяти действий (супер способностей), которые только что обсуждались, не является ошибочным или не одобренным. Но получить правильный unsafe
код сложнее, потому что сборщик не может помочь в обеспечении безопасности памяти. Если у вас есть причина использовать unsafe
код, вы можете делать это, а наличие явной unsafe
изложении облегчает отслеживание источника неполадок. если они возникают.
Мы познакомились с особенностями в разделе "Особенности: Определение общего поведения" в главе 10, но там мы не обсуждали более сложные подробности. Теперь, когда вы больше знаете о Rust, мы можем перейти к более подробному рассмотрению.
-Сопряженные виды связывают вид-заполнитель с особенностью таким образом, что определения способов особенности могут использовать эти виды-заполнители в своих ярлыках. Для именно выполнения особенности вместо типа-заполнителя указывается определенный вид, который будет использоваться. Таким образом, мы можем определить особенности, использующие некоторые виды, без необходимости точно знать, что это за виды, пока особенности не будут выполнены.
-Мы назвали большинство продвинутых возможностей в этой главе редко востребованными. Сопряженные виды находятся где-то посередине: они используются реже чем возможности описанные в остальной части книги, но чаще чем многие другие возможности обсуждаемые в этой главе.
-Одним из примеров особенности с сопряженным видом является особенность Iterator
из встроенной библиотеки. Сопряженный вид называется Item
и символизирует вид значений, по которым повторяется вид, выполняющий особенность Iterator
. Определение особенности Iterator
показано в приложении 19-12.
pub trait Iterator {
- type Item;
-
- fn next(&mut self) -> Option<Self::Item>;
-}
--
Вид Item
является заполнителем и определение способа next
показывает, что он будет возвращать значения вида Option<Self::Item>
. Разработчики особенности Iterator
определят определенный вид для Item
, а способ next
вернёт Option
содержащий значение этого определенного вида.
Сопряженные виды могут показаться подходом похожей на обобщения, поскольку последние позволяют нам определять функцию, не указывая, какие виды она может обрабатывать. Чтобы изучить разницу между этими двумя подходами, мы рассмотрим выполнение особенности Iterator
для вида с именем Counter
, который указывает, что вид Item
равен u32
:
Файл: src/lib.rs
-struct Counter {
- count: u32,
-}
-
-impl Counter {
- fn new() -> Counter {
- Counter { count: 0 }
- }
-}
-
-impl Iterator for Counter {
- type Item = u32;
-
- fn next(&mut self) -> Option<Self::Item> {
- // --snip--
- if self.count < 5 {
- self.count += 1;
- Some(self.count)
- } else {
- None
- }
- }
-}
-Этот правила написания весьма напоминает обобщённые виды. Так почему же особенность Iterator
не определён обобщённым видом, как показано в приложении 19-13?
pub trait Iterator<T> {
- fn next(&mut self) -> Option<T>;
-}
--
Разница в том, что при использовании обобщений, как показано в приложении 19-13, мы должны определять виды в каждой выполнения; потому что мы также можем выполнить Iterator<String> for Counter
или любого другого вида, мы могли бы иметь несколько выполнения Iterator
для Counter
. Другими словами, когда особенность имеет обобщённый свойство, он может быть выполнен для вида несколько раз, каждый раз меняя определенные виды свойств обобщённого вида. Когда мы используем способ next
у Counter
, нам пришлось бы предоставить изложении вида, указывая какую выполнение Iterator
мы хотим использовать.
С сопряженными видами не нужно определять виды, потому что мы не можем выполнить особенность у вида несколько раз. В приложении 19-12 с определением, использующим сопряженные виды можно выбрать только один вид Item
, потому что может быть только одно объявление impl Iterator for Counter
. Нам не нужно указывать, что нужен повторитель значений вида u32
везде, где мы вызываем next
у Counter
.
Сопряженные виды также становятся частью договора особенности: разработчики особенности должны предоставить вид, который заменит сопряженный заполнитель вида. Связанные виды часто имеют имя, описывающее то, как будет использоваться вид, и хорошей опытом является документирование связанного вида в документации по API.
-Когда мы используем свойства обобщённого вида, мы можем указать определенный вид по умолчанию для обобщённого вида. Это устраняет необходимость разработчикам указывать определенный вид, если работает вид по умолчанию. Вид по умолчанию указывается при объявлении обобщённого вида с помощью правил написания <PlaceholderType=ConcreteType>
.
Отличным примером, когда этот способ полезен, является перегрузка оператора (operator overloading), когда вы настраиваете поведение оператора (например, +
) для определённых случаев.
Rust не позволяет создавать собственные операторы или перегружать произвольные операторы. Но можно перегрузить перечисленные действия и соответствующие им особенности из std::ops
путём выполнения особенностей, связанных с этими операторами. Например, в приложении 19-14 мы перегружаем оператор +
, чтобы складывать два образца Point
. Мы делаем это выполняя особенность Add
для устройства Point
:
Файл: src/main.rs
--use std::ops::Add; - -#[derive(Debug, Copy, Clone, PartialEq)] -struct Point { - x: i32, - y: i32, -} - -impl Add for Point { - type Output = Point; - - fn add(self, other: Point) -> Point { - Point { - x: self.x + other.x, - y: self.y + other.y, - } - } -} - -fn main() { - assert_eq!( - Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, - Point { x: 3, y: 3 } - ); -}
-
Способ add
складывает значения x
двух образцов Point
и значения y
у Point
для создания нового образца Point
. Особенность Add
имеет сопряженный вид с именем Output
, который определяет вид, возвращаемый из способа add
.
Обобщённый вид по умолчанию в этом коде находится в особенности Add
. Вот его определение:
-#![allow(unused)] -fn main() { -trait Add<Rhs = Self> { - type Output; - - fn add(self, rhs: Rhs) -> Self::Output; -} -}
Этот код должен выглядеть знакомым: особенность с одним способом и сопряженным видом. Новый правила написания это RHS=Self
. Такой правила написания называется свойства вида по умолчанию (default type parameters). Свойство обобщённого вида RHS
(сокращённо “right hand side”) определяет вид свойства rhs
в способе add
. Если мы не укажем определенный вид для RHS
при выполнения особенности Add
, то видом для RHS
по умолчанию будет Self
, который будет видом для которого выполняется особенность Add
.
Когда мы выполнили Add
для устройства Point
, мы использовали обычное значение для RHS
, потому что хотели сложить два образца Point
. Давайте посмотрим на пример выполнения особенности Add
, где мы хотим пользовательский вид RHS
вместо использования вида по умолчанию.
У нас есть две разные устройства Millimeters
и Meters
, хранящие значения в разных единицах измерения. Это тонкое обёртывание существующего вида в другую устройство известно как образец newtype, который мы более подробно опишем в разделе "Образец Newtype для выполнение внешних особенностей у внешних видов" . Мы хотим добавить значения в миллиметрах к значениям в метрах и хотим иметь выполнение особенности Add
, которая делает правильное преобразование единиц. Можно выполнить Add
для Millimeters
с видом Meters
в качестве Rhs
, как показано в приложении 19-15.
Файл: src/lib.rs
-use std::ops::Add;
-
-struct Millimeters(u32);
-struct Meters(u32);
-
-impl Add<Meters> for Millimeters {
- type Output = Millimeters;
-
- fn add(self, other: Meters) -> Millimeters {
- Millimeters(self.0 + (other.0 * 1000))
- }
-}
--
Чтобы сложить Millimeters
и Meters
, мы указываем impl Add<Meters>
, чтобы указать значение свойства вида RHS
(Meters) вместо использования значения по умолчанию Self
(Millimeters).
Свойства вида по умолчанию используются в двух основных случаях:
-Особенность Add
из встроенной библиотеки является примером второй цели: обычно вы складываете два одинаковых вида, но особенность Add
позволяет сделать больше. Использование свойства вида по умолчанию в объявлении особенности Add
означает, что не нужно указывать дополнительный свойство большую часть времени. Другими словами, большая часть кода выполнения не нужна, что делает использование особенности проще.
Первая цель похожа на вторую, но используется наоборот: если вы хотите добавить свойство вида к существующему особенности, можно дать ему значение по умолчанию, чтобы разрешить расширение возможности особенности без нарушения кода существующей выполнения.
-В Ржавчина ничего не мешает особенности иметь способ с одинаковым именем, таким же как способ другого особенности и Ржавчина не мешает выполнить оба таких особенности у одного вида. Также возможно выполнить способ с таким же именем непосредственно у вида, такой как и способы у особенностей.
-При вызове способов с одинаковыми именами в Ржавчина нужно указать, какой из трёх возможных вы хотите использовать. Рассмотрим код в приложении 19-16, где мы определили два особенности: Pilot
и Wizard
, у обоих есть способ fly
. Затем мы выполняем оба особенности у вида Human
в котором уже выполнен способ с именем fly
. Каждый способ fly
делает что-то своё.
Файл: src/main.rs
--trait Pilot { - fn fly(&self); -} - -trait Wizard { - fn fly(&self); -} - -struct Human; - -impl Pilot for Human { - fn fly(&self) { - println!("This is your captain speaking."); - } -} - -impl Wizard for Human { - fn fly(&self) { - println!("Up!"); - } -} - -impl Human { - fn fly(&self) { - println!("*waving arms furiously*"); - } -} - -fn main() {}
-
Когда мы вызываем fly
у образца Human
, то сборщик по умолчанию вызывает способ, который непосредственно выполнен для вида, как показано в приложении 19-17.
Файл: src/main.rs
--trait Pilot { - fn fly(&self); -} - -trait Wizard { - fn fly(&self); -} - -struct Human; - -impl Pilot for Human { - fn fly(&self) { - println!("This is your captain speaking."); - } -} - -impl Wizard for Human { - fn fly(&self) { - println!("Up!"); - } -} - -impl Human { - fn fly(&self) { - println!("*waving arms furiously*"); - } -} - -fn main() { - let person = Human; - person.fly(); -}
-
Запуск этого кода напечатает *waving arms furiously*
, показывая, что Ржавчина называется способ fly
выполненный непосредственно у Human
.
Чтобы вызвать способы fly
у особенности Pilot
или особенности Wizard
нужно использовать более явный правила написания, указывая какой способ fly
мы имеем в виду. Приложение 19-18 отображает такой правила написания.
Файл: src/main.rs
--trait Pilot { - fn fly(&self); -} - -trait Wizard { - fn fly(&self); -} - -struct Human; - -impl Pilot for Human { - fn fly(&self) { - println!("This is your captain speaking."); - } -} - -impl Wizard for Human { - fn fly(&self) { - println!("Up!"); - } -} - -impl Human { - fn fly(&self) { - println!("*waving arms furiously*"); - } -} - -fn main() { - let person = Human; - Pilot::fly(&person); - Wizard::fly(&person); - person.fly(); -}
-
Указание имени особенности перед именем способа проясняет сборщику Rust, какую именно выполнение fly
мы хотим вызвать. Мы могли бы также написать Human::fly(&person)
, что эквивалентно используемому нами person.fly()
в приложении 19-18, но это писание немного длиннее, когда нужна неоднозначность.
Выполнение этого кода выводит следующее:
-$ cargo run
- Compiling traits-example v0.1.0 (file:///projects/traits-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
- Running `target/debug/traits-example`
-This is your captain speaking.
-Up!
-*waving arms furiously*
-
-Поскольку способ fly
принимает свойство self
, если у нас было два вида оба выполняющих один особенность, то Ржавчина может понять, какую выполнение особенности использовать в зависимости от вида self
.
Однако, сопряженные функции, не являющиеся способами, не имеют свойства self
. Когда существует несколько видов или особенностей, определяющих функции, не являющиеся способами, с одним и тем же именем функции, Ржавчина не всегда знает, какой вид вы имеете в виду, если только вы не используете полный правила написания. Например, в приложении 19-19 мы создаём особенность для приюта животных, который хочет назвать всех маленьких собак Spot. Мы создаём особенность Animal
со связанной с ним функцией baby_name
, не являющейся способом. Особенность Animal
выполнен для устройства Dog
, для которой мы также напрямую предоставляем связанную функцию baby_name
, не являющуюся способом.
Файл: src/main.rs
--trait Animal { - fn baby_name() -> String; -} - -struct Dog; - -impl Dog { - fn baby_name() -> String { - String::from("Spot") - } -} - -impl Animal for Dog { - fn baby_name() -> String { - String::from("puppy") - } -} - -fn main() { - println!("A baby dog is called a {}", Dog::baby_name()); -}
-
Мы выполнили код для приюта для животных, который хочет назвать всех щенков именем Spot, в сопряженной функции baby_name
, которая определена для Dog
. Вид Dog
также выполняет особенность Animal
, который описывает свойства, которые есть у всех животных. Маленьких собак называют щенками, и это выражается в выполнения Animal
у Dog
в функции baby_name
сопряженной с особенностью Animal
.
В main
мы вызываем функцию Dog::baby_name
, которая вызывает сопряженную функцию определённую напрямую у Dog
. Этот код печатает следующее:
$ cargo run
- Compiling traits-example v0.1.0 (file:///projects/traits-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
- Running `target/debug/traits-example`
-A baby dog is called a Spot
-
-Этот вывод не является тем, что мы хотели бы получить. Мы хотим вызвать функцию baby_name
, которая является частью особенности Animal
выполненного у Dog
, так чтобы код печатал A baby dog is called a puppy
. Техника указания имени особенности использованная в приложении 19-18 здесь не помогает; если мы изменим main
код как в приложении 19-20, мы получим ошибку сборки.
Файл: src/main.rs
-trait Animal {
- fn baby_name() -> String;
-}
-
-struct Dog;
-
-impl Dog {
- fn baby_name() -> String {
- String::from("Spot")
- }
-}
-
-impl Animal for Dog {
- fn baby_name() -> String {
- String::from("puppy")
- }
-}
-
-fn main() {
- println!("A baby dog is called a {}", Animal::baby_name());
-}
--
Поскольку Animal::baby_name
не имеет свойства self
, и могут быть другие виды, выполняющие особенность Animal
, Ржавчина не может понять, какую выполнение Animal::baby_name
мы хотим использовать. Мы получим эту ошибку сборщика:
$ cargo run
- Compiling traits-example v0.1.0 (file:///projects/traits-example)
-error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
- --> src/main.rs:20:43
- |
-2 | fn baby_name() -> String;
- | ------------------------- `Animal::baby_name` defined here
-...
-20 | println!("A baby dog is called a {}", Animal::baby_name());
- | ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
- |
-help: use the fully-qualified path to the only available implementation
- |
-20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
- | +++++++ +
-
-For more information about this error, try `rustc --explain E0790`.
-error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
-
-Чтобы устранить неоднозначность и сказать Rust, что мы хотим использовать выполнение Animal
для Dog
, нужно использовать полный правила написания. Приложение 19-21 отображает, как использовать полный правила написания.
Файл: src/main.rs
--trait Animal { - fn baby_name() -> String; -} - -struct Dog; - -impl Dog { - fn baby_name() -> String { - String::from("Spot") - } -} - -impl Animal for Dog { - fn baby_name() -> String { - String::from("puppy") - } -} - -fn main() { - println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); -}
-
Мы указываем изложение вида в угловых скобках, которая указывает на то что мы хотим вызвать способ baby_name
из особенности Animal
выполненный в Dog
, также указывая что мы хотим рассматривать вид Dog
в качестве Animal
для вызова этой функции. Этот код теперь напечатает то, что мы хотим:
$ cargo run
- Compiling traits-example v0.1.0 (file:///projects/traits-example)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
- Running `target/debug/traits-example`
-A baby dog is called a puppy
-
-В общем, полный правила написания определяется следующим образом:
-<Type as Trait>::function(receiver_if_method, next_arg, ...);
-Для сопряженных функций, которые не являются способами, будет отсутствовать receiver
(предмет приёмника): будет только список переменных. Вы можете использовать полный правила написания везде, где вызываете функции или способы. Тем не менее, разрешается опустить любую часть этого правил написания, которую Ржавчина может понять из другой сведений в программе. Вам нужно использовать более подробный правила написания только в тех случаях, когда существует несколько выполнений, использующих одно и то же название, и Ржавчина нужно помочь определить, какую выполнение вы хотите вызвать.
Иногда вы можете написать определение особенности, которое зависит от другого особенности: для вида, выполняющего первый особенность, вы хотите потребовать, чтобы этот вид также выполнил второй особенность. Вы должны сделать это, чтобы ваше определение особенности могло использовать связанные элементы второго особенности. Особенность, на который опирается ваше определение особенности, называется supertrait вашего особенности.
-Например, мы хотим создать особенность OutlinePrint
с способом outline_print
, который будет печатать значение обрамлённое звёздочками. Мы хотим чтобы устройства Point
, выполняющая особенность встроенной библиотеки Display
, вывела на печать (x, y)
при вызове outline_print
у образца Point
, который имеет значение 1
для x
и значение 3
для y
. Она должна напечатать следующее:
**********
-* *
-* (1, 3) *
-* *
-**********
-
-В выполнения outline_print
мы хотим использовать возможность особенности Display
. Поэтому нам нужно указать, что особенность OutlinePrint
будет работать только для видов, которые также выполняют Display
и предоставляют возможность, которая нужна в OutlinePrint
. Мы можем сделать это в объявлении особенности, указав OutlinePrint: Display
. Этот способ похож на добавление ограничения в особенность. В приложении 19-22 показана выполнение особенности OutlinePrint
.
Файл: src/main.rs
--use std::fmt; - -trait OutlinePrint: fmt::Display { - fn outline_print(&self) { - let output = self.to_string(); - let len = output.len(); - println!("{}", "*".repeat(len + 4)); - println!("*{}*", " ".repeat(len + 2)); - println!("* {output} *"); - println!("*{}*", " ".repeat(len + 2)); - println!("{}", "*".repeat(len + 4)); - } -} - -fn main() {}
-
Поскольку мы указали, что особенность OutlinePrint
требует особенности Display
, мы можем использовать функцию to_string
, которая самостоятельно выполнена для любого вида выполняющего Display
. Если бы мы попытались использовать to_string
не добавляя двоеточие и не указывая особенность Display
после имени особенности, мы получили бы сообщение о том, что способ с именем to_string
не был найден у вида &Self
в текущей области видимости.
Давайте посмотрим что происходит, если мы пытаемся выполнить особенность OutlinePrint
для вида, который не выполняет Display
, например устройства Point
:
Файл: src/main.rs
-use std::fmt;
-
-trait OutlinePrint: fmt::Display {
- fn outline_print(&self) {
- let output = self.to_string();
- let len = output.len();
- println!("{}", "*".repeat(len + 4));
- println!("*{}*", " ".repeat(len + 2));
- println!("* {output} *");
- println!("*{}*", " ".repeat(len + 2));
- println!("{}", "*".repeat(len + 4));
- }
-}
-
-struct Point {
- x: i32,
- y: i32,
-}
-
-impl OutlinePrint for Point {}
-
-fn main() {
- let p = Point { x: 1, y: 3 };
- p.outline_print();
-}
-Мы получаем сообщение о том, что требуется выполнение Display
, но её нет:
$ cargo run
- Compiling traits-example v0.1.0 (file:///projects/traits-example)
-error[E0277]: `Point` doesn't implement `std::fmt::Display`
- --> src/main.rs:20:23
- |
-20 | impl OutlinePrint for Point {}
- | ^^^^^ `Point` cannot be formatted with the default formatter
- |
- = help: the trait `std::fmt::Display` is not implemented for `Point`
- = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
-note: required by a bound in `OutlinePrint`
- --> src/main.rs:3:21
- |
-3 | trait OutlinePrint: fmt::Display {
- | ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
-
-error[E0277]: `Point` doesn't implement `std::fmt::Display`
- --> src/main.rs:24:7
- |
-24 | p.outline_print();
- | ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
- |
- = help: the trait `std::fmt::Display` is not implemented for `Point`
- = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
-note: required by a bound in `OutlinePrint::outline_print`
- --> src/main.rs:3:21
- |
-3 | trait OutlinePrint: fmt::Display {
- | ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
-4 | fn outline_print(&self) {
- | ------------- required by a bound in this associated function
-
-For more information about this error, try `rustc --explain E0277`.
-error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
-
-Чтобы исправить, мы выполняем Display
у устройства Point
и выполняем требуемое ограничение OutlinePrint
, вот так:
Файл: src/main.rs
--trait OutlinePrint: fmt::Display { - fn outline_print(&self) { - let output = self.to_string(); - let len = output.len(); - println!("{}", "*".repeat(len + 4)); - println!("*{}*", " ".repeat(len + 2)); - println!("* {output} *"); - println!("*{}*", " ".repeat(len + 2)); - println!("{}", "*".repeat(len + 4)); - } -} - -struct Point { - x: i32, - y: i32, -} - -impl OutlinePrint for Point {} - -use std::fmt; - -impl fmt::Display for Point { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "({}, {})", self.x, self.y) - } -} - -fn main() { - let p = Point { x: 1, y: 3 }; - p.outline_print(); -}
Тогда выполнение особенности OutlinePrint
для устройства Point
будет собрана успешно и мы можем вызвать outline_print
у образца Point
для отображения значения обрамлённое звёздочками.
В разделе "Выполнение особенности у типа" главы 10, мы упоминали "правило сироты" (orphan rule), которое гласит, что разрешается выполнить особенность у вида, если либо особенность, либо вид являются местными для нашего ящика. Можно обойти это ограничение, используя образец нового вида (newtype pattern), который включает в себя создание нового вида в упорядоченной в ряд устройстве. (Мы рассмотрели упорядоченные в ряд устройства в разделе "Использование устройств упорядоченных рядов без именованных полей для создания различных видов" главы 5.) Устройства упорядоченного ряда будет иметь одно поле и будет тонкой оболочкой для вида которому мы хотим выполнить особенность. Тогда вид оболочки является местным для нашего ящика и мы можем выполнить особенность для местной обёртки. Newtype это понятие, который происходит от языка программирования Haskell. В нем нет ухудшения производительности времени выполнения при использовании этого образца и вид оболочки исключается во время сборки.
-В качестве примера, мы хотим выполнить особенность Display
для вида Vec<T>
, где "правило сироты" (orphan rule) не позволяет нам этого делать напрямую, потому что особенность Display
и вид Vec<T>
объявлены вне нашего ящика. Мы можем сделать устройство Wrapper
, которая содержит образец Vec<T>
; тогда мы можем выполнить Display
у устройства Wrapper
и использовать значение Vec<T>
как показано в приложении 19-23.
Файл: src/main.rs
--use std::fmt; - -struct Wrapper(Vec<String>); - -impl fmt::Display for Wrapper { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "[{}]", self.0.join(", ")) - } -} - -fn main() { - let w = Wrapper(vec![String::from("hello"), String::from("world")]); - println!("w = {w}"); -}
-
Выполнение Display
использует self.0
для доступа к внутреннему Vec<T>
, потому что Wrapper
это устройства упорядоченного ряда, а Vec<T>
это элемент с порядковым указателем 0 в упорядоченном ряде. Затем мы можем использовать полезные возможности вида Display
у Wrapper
.
Недостатком использования этой техники является то, что Wrapper
является новым видом, поэтому он не имеет способов для значения, которое он держит в себе. Мы должны были бы выполнить все способы для Vec<T>
непосредственно во Wrapper
, так чтобы эти способы делегировались внутреннему self.0
, что позволило бы нам обращаться с Wrapper
точно так же, как с Vec<T>
. Если бы мы хотели, чтобы новый вид имел каждый способ имеющийся у внутреннего вида, выполняя особенность Deref
(обсуждается в разделе "Работа с умными указателями как с обычными ссылками с помощью Deref
особенности" главы 15) у Wrapper
для возвращения внутреннего вида, то это было бы решением. Если мы не хотим, чтобы вид Wrapper
имел все способы внутреннего вида, например, для ограничения поведения вида Wrapper
, то пришлось бы вручную выполнить только те способы, которые нам нужны.
Этот образец newtype также полезен, даже когда особенности не задействованы. Давайте переключим внимание и рассмотрим некоторые продвинутые способы взаимодействия с системой видов Rust.
- -Система видов Ржавчина имеет некоторые особенности, о которых мы уже упоминали, но ещё не обсуждали. Мы начнём с общего обзора newtypes, а затем разберёмся, чем они могут пригодиться в качестве видов. Далее мы перейдём к псевдонимам видов - возможности, похожей на newtypes, но с несколько иной смыслом. Мы также обсудим вид !
и виды с изменяемым размером.
--Примечание: В этом разделе предполагается, что вы прочитали предыдущий раздел "Использование образца Newtype для выполнения внешних особенностей для внешних видов."
-
Образец newtype полезен и для других задач, помимо тех, которые мы обсуждали до сих пор, в частности, для постоянного обеспечения того, чтобы значения никогда не путались, а также для указания единиц измерения значения. Пример использования newtypes для указания единиц измерения вы видели в приложении 19-15: вспомните, как устройства Millimeters
и Meters
обернули значения u32
в newtype. Если бы мы написали функцию с свойствоом вида Millimeters
, мы не смогли бы собрать программу, которая случайно попыталась бы вызвать эту функцию со значением вида Meters
или обычным u32
.
Мы также можем использовать образец newtype для абстрагирования от некоторых подробностей выполнения вида: новый вид может предоставлять открытый API, который отличается от API скрытого внутри вида.
-Newtypes также позволяют скрыть внутреннюю выполнение. Например, мы можем создать вид People
, который обернёт HashMap<i32, String>
, хранящий ID человека, связанный с его именем. Код, использующий People
, будет взаимодействовать только с открытым API, который мы предоставляем, например, способ добавления имени в собрание People
; этому коду не нужно будет знать, что внутри мы присваиваем i32
ID именам. Образец newtype - это лёгкий способ достижения инкапсуляции для скрытия подробностей выполнения, который мы обсуждали в разделе "Инкапсуляция, скрывающая подробности выполнения" главы 17.
Rust предоставляет возможность объявить псевдоним вида чтобы дать существующему виду другое имя. Для этого мы используем ключевое слово type
. Например, мы можем создать псевдоним вида Kilometers
для i32
следующим образом:
-fn main() { - type Kilometers = i32; - - let x: i32 = 5; - let y: Kilometers = 5; - - println!("x + y = {}", x + y); -}
Теперь псевдоним Kilometers
является родственным для i32
; в отличие от видов Millimeters
и Meters
, которые мы создали в приложении 19-15, Kilometers
не является отдельным, новым видом. Значения, имеющие вид Kilometers
, будут обрабатываться так же, как и значения вида i32
:
-fn main() { - type Kilometers = i32; - - let x: i32 = 5; - let y: Kilometers = 5; - - println!("x + y = {}", x + y); -}
Поскольку Kilometers
и i32
являются одним и тем же видом, мы можем добавлять значения обоих видов и передавать значения Kilometers
функциям, принимающим свойства i32
. Однако, используя этот способ, мы не получаем тех преимуществ проверки видов, которые мы получаем от образца newtype, рассмотренного ранее. Другими словами, если мы где-то перепутаем значения Kilometers
и i32
, сборщик не выдаст нам ошибку.
Родственные в основном используются для сокращения повторений. Например, у нас может быть такой многословный тип:
-Box<dyn Fn() + Send + 'static>
-Написание таких длинных видов в ярлыках функций и в виде наставлений видов по всему коду может быть утомительным и чреватым ошибками. Представьте себе дело, наполненный таким кодом, как в приложении 19-24.
--fn main() { - let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi")); - - fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { - // --snip-- - } - - fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { - // --snip-- - Box::new(|| ()) - } -}
-
Псевдоним вида делает этот код более удобным для работы, сокращая количество повторений. В приложении 19-25 мы ввели псевдоним Thunk
для вида verbose и можем заменить все использования этого вида более коротким псевдонимом Thunk
.
-fn main() { - type Thunk = Box<dyn Fn() + Send + 'static>; - - let f: Thunk = Box::new(|| println!("hi")); - - fn takes_long_type(f: Thunk) { - // --snip-- - } - - fn returns_long_type() -> Thunk { - // --snip-- - Box::new(|| ()) - } -}
-
Такой код гораздо легче читать и писать! Выбор осмысленного имени для псевдонима вида также может помочь прояснить ваши намерения (thunk - название для кода, который будет вычисляться позднее, поэтому это подходящее имя для сохраняемого замыкания).
-Псевдонимы видов также часто используются с видом Result<T, E>
для сокращения повторений. Рассмотрим звено std::io
в встроенной библиотеке. Действия ввода-вывода часто возвращают Result<T, E>
для обработки случаев, когда эти действия не удаются. В данной библиотеке есть устройства std::io::Error
, которая отражает все возможные ошибки ввода/вывода. Многие функции в std::io
будут возвращать Result<T, E>
, где E
- это std::io::Error
, например, эти функции в особенности Write
:
use std::fmt;
-use std::io::Error;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
- fn flush(&mut self) -> Result<(), Error>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
-}
-Result<..., Error>
часто повторяется. Поэтому std::io
содержит такое объявление псевдонима вида:
use std::fmt;
-
-type Result<T> = std::result::Result<T, std::io::Error>;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize>;
- fn flush(&mut self) -> Result<()>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<()>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
-}
-Поскольку это объявление находится в звене std::io
, мы можем использовать полный псевдоним std::io::Result<T>
; это и есть Result<T, E>
, где в качестве E
выступает std::io::Error
. Ярлыки функций особенности Write
в итоге выглядят следующим образом:
use std::fmt;
-
-type Result<T> = std::result::Result<T, std::io::Error>;
-
-pub trait Write {
- fn write(&mut self, buf: &[u8]) -> Result<usize>;
- fn flush(&mut self) -> Result<()>;
-
- fn write_all(&mut self, buf: &[u8]) -> Result<()>;
- fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
-}
-Псевдоним вида помогает двумя способами: он облегчает написание кода и даёт нам согласованный внешняя оболочка для всего из std::io
. Поскольку это псевдоним, то это просто ещё один вид Result<T, E>
, что означает, что с ним мы можем использовать любые способы, которые работают с Result<T, E>
, а также особый правила написания вроде ?
оператора.
В Ржавчина есть особый вид !
, который на жаргоне теории видов известен как empty type (пустой вид), потому что он не содержит никаких значений. Мы предпочитаем называть его never type (никакой вид), потому что он используется в качестве возвращаемого вида, когда функция ничего не возвращает. Вот пример:
fn bar() -> ! {
- // --snip--
- panic!();
-}
-Этот код читается как "функция bar
ничего не возвращает". Функции, которые ничего не возвращают, называются рассеивающими функциями (diverging functions). Мы не можем производить значения вида !
, поэтому bar
никогда ничего не вернёт.
Но для чего нужен вид, для которого вы никогда не сможете создать значения? Напомним код из приложения 2-5, отрывка "игры в загадки"; мы воспроизвели его часть здесь в приложении 19-26.
-use rand::Rng;
-use std::cmp::Ordering;
-use std::io;
-
-fn main() {
- println!("Guess the number!");
-
- let secret_number = rand::thread_rng().gen_range(1..=100);
-
- println!("The secret number is: {secret_number}");
-
- loop {
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- // --snip--
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
-
- println!("You guessed: {guess}");
-
- // --snip--
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- }
-}
--
В то время мы опуисполнения некоторые подробности в этом коде. В главе 6 раздела "Оператор управления потоком match
" мы обсуждали, что все ветви match
должны возвращать одинаковый вид. Например, следующий код не работает:
fn main() {
- let guess = "3";
- let guess = match guess.trim().parse() {
- Ok(_) => 5,
- Err(_) => "hello",
- };
-}
-Вид guess
в этом коде должен быть целым и строкой, а Ржавчина требует, чтобы guess
имел только один вид. Так что же возвращает continue
? Как нам позволили вернуть u32
из одной ветви и при этом иметь другую ветвь, которая оканчивается continue
в приложении 19-26?
Как вы уже возможно догадались, continue
имеет значение !
. То есть, когда Ржавчина вычисляет вид guess
, он смотрит на обе сопоставляемые ветки, первая со значением u32
и последняя со значением !
. Так как !
никогда не может иметь значение, то Ржавчина решает что видом guess
является вид u32
.
Условный подход к описанию такого поведения заключается в том, что выражения вида !
могут быть преобразованы в любой другой вид. Нам позволяется завершить этот match
с помощью continue
, потому что continue
не возвращает никакого значения; вместо этого он передаёт управление обратно в начало цикла, поэтому в случае Err
мы никогда не присваиваем значение guess
.
Вид never полезен также для макроса panic!
. Вспомните функцию unwrap
, которую мы вызываем для значений Option<T>
, чтобы создать значение или вызвать панику с этим определением:
enum Option<T> {
- Some(T),
- None,
-}
-
-use crate::Option::*;
-
-impl<T> Option<T> {
- pub fn unwrap(self) -> T {
- match self {
- Some(val) => val,
- None => panic!("called `Option::unwrap()` on a `None` value"),
- }
- }
-}
-В этом коде происходит то же самое, что и в match
в приложении 19-26: Ржавчина видит, что val
имеет вид T
, а panic!
имеет вид !
, поэтому итогом общего выражения match
является T
. Этот код работает, потому что panic!
не производит никакого значения; он завершает программу. В случае None
мы не будем возвращать значение из unwrap
, поэтому этот код работает.
Последнее выражение, которое имеет вид !
это loop
:
fn main() {
- print!("forever ");
-
- loop {
- print!("and ever ");
- }
-}
-В данном случае цикл никогда не завершится, поэтому !
является значением выражения. Но это не будет так, если мы добавим break
, так как цикл завершит свою работу, когда дойдёт до break
.
Sized
Rust необходимо знать некоторые подробности о видах, например, сколько места нужно выделить для значения определённого вида. Из-за этого один из особенностей системы видов поначалу вызывает некоторое недоумение: подход видов с изменяемым размером. Иногда называемые DST или безразмерные виды, эти виды позволяют нам писать код, используя значения, размер которых мы можем узнать только во время выполнения.
-Давайте углубимся в подробности изменяемого вида str
, который мы использовали на протяжении всей книги. Все верно, не вида &str
, а вида str
самого по себе, который является DST. Мы не можем знать, какой длины строка до особенности времени выполнения, то есть мы не можем создать переменную вида str
и не можем принять переменная вида str
. Рассмотрим следующий код, который не работает:
fn main() {
- let s1: str = "Hello there!";
- let s2: str = "How's it going?";
-}
-Rust должен знать, сколько памяти выделить для любого значения определенного вида и все значения вида должны использовать одинаковый размер памяти. Если Ржавчина позволил бы нам написать такой код, то эти два значения str
должны были бы занимать одинаковое количество памяти. Но они имеют разную длину: s1
нужно 12 байтов памяти, а для s2
нужно 15. Вот почему невозможно создать переменную имеющую вид изменяемого размера.
Так что же нам делать? В этом случае вы уже знаете ответ: мы преобразуем виды s1
и s2
в &str
, а не в str
. Вспомните из раздела "Строковые срезы" главы 4, что устройства данных среза просто хранит начальную положение и длину среза. Так, в отличие от &T
, который содержит только одно значение - адрес памяти, где находится T
, в &str
хранятся два значения - адрес str
и его длина. Таким образом, мы можем узнать размер значения &str
во время сборки: он вдвое больше длины usize
. То есть, мы всегда знаем размер &str
, независимо от длины строки, на которую оно ссылается. В целом, именно так в Ржавчина используются виды изменяемого размера: они содержат дополнительный бит метаданных, который хранит размер изменяемой сведений. Золотое правило изменяемых размерных видов заключается в том, что мы всегда должны помещать значения таких видов за каким-либо указателем.
Мы можем соединенять str
со всеми видами указателей: например, Box<str>
или Rc<str>
. На самом деле, вы уже видели это раньше, но с другим изменяемым размерным видом: особенностями. Каждый особенность - это изменяемый размерный вид, на который мы можем ссылаться, используя имя особенности. В главе 17 в разделе "Использование особенность-предметов, допускающих значения разных видов" мы упоминали, что для использования особенностей в качестве особенность-предметов мы должны поместить их за указателем, например &dyn Trait
или Box<dyn Trait>
(Rc<dyn Trait>
тоже подойдёт).
Для работы с DST Ржавчина использует особенность Sized
чтобы решить, будет ли размер вида известен на стадии сборки. Этот особенность самостоятельно выполняется для всего, чей размер известен к времени сборки. Кроме того, Ржавчина неявно добавляет ограничение на Sized
к каждой гибкой функции. То есть, определение гибкой функции, такое как:
fn generic<T>(t: T) {
- // --snip--
-}
-на самом деле рассматривается как если бы мы написали её в виде:
-fn generic<T: Sized>(t: T) {
- // --snip--
-}
-По умолчанию обобщённые функции будут работать только с видами чей размер известен во время сборки. Тем не менее, можно использовать следующий особый правила написания, чтобы ослабить это ограничение:
-fn generic<T: ?Sized>(t: &T) {
- // --snip--
-}
-Ограничение особенности ?Sized
означает «T
может или не может быть Sized
», эта наставление отменяет обычное правило, согласно которому гибкие виды должны иметь известный размер во время сборки. Использовать правила написания ?Trait
в таком качестве можно только для Sized
, и ни для каких других особенностей.
Также обратите внимание, что мы поменяли вид свойства t
с T
на &T
. Поскольку вид мог бы не быть Sized
, мы должны использовать его за каким-либо указателем. В данном случае мы выбрали ссылку.
Далее мы поговорим о функциях и замыканиях!
- -В этом разделе рассматриваются некоторые продвинутые возможности, относящиеся к функциям и замыканиям, такие как указатели функций и возвращаемые замыкания.
-Мы уже обсуждали, как передавать замыкания в функции; но также можно передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать ранее созданную функцию, а не определять новое замыкание. Функции соответствуют виду fn
(со строчной буквой f), не путать с особенностью замыкания Fn
. Вид fn
называется указателем функции. Передача функций с помощью указателей функций позволяет использовать функции в качестве переменных других функций.
Для указания того, что свойство является указателем на функцию, используется правила написания, такой же, как и для замыканий, что отображается в приложении 19-27, где мы определили функцию add_one
, которая добавляет единицу к переданному ей свойству. Функция do_twice
принимает два свойства: указатель на любую функцию, принимающую свойство i32
и возвращающую i32
, и число вида i32
. Функция do_twice
дважды вызывает функцию f
, передавая ей значение arg
, а затем складывает полученные итоги. Функция main
вызывает функцию do_twice
с переменнойми add_one
и 5
.
Файл: src/main.rs
--fn add_one(x: i32) -> i32 { - x + 1 -} - -fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { - f(arg) + f(arg) -} - -fn main() { - let answer = do_twice(add_one, 5); - - println!("The answer is: {answer}"); -}
-
Этот код выводит Ответ: 12
. Мы указали, что свойство f
в do_twice
является fn
, которая принимает на вход единственный свойство вида i32
и возвращает i32
. Затем мы можем вызвать f
в теле do_twice
. В main
мы можем передать имя функции add_one
в качестве первого переменной в do_twice
.
В отличие от замыканий, fn
является видом, а не особенностью, поэтому мы указываем fn
непосредственно в качестве вида свойства, а не объявляем свойство гибкого вида с одним из особенностей Fn
в качестве связанного.
Указатели функций выполняют все три особенности замыканий (Fn
, FnMut
и FnOnce
), то есть вы всегда можете передать указатель функции в качестве переменной функции, которая ожидает замыкание. Лучше всего для описания функции использовать гибкий вид и один из особенностей замыканий, чтобы ваши функции могли принимать как функции, так и замыкания.
Однако, одним из примеров, когда вы бы хотели принимать только fn
, но не замыкания, является взаимодействие с внешним кодом, который не имеет замыканий: функции языка C могут принимать функции в качестве переменных, однако замыканий в языке C нет.
В качестве примера того, где можно использовать либо замыкание, определяемое непосредственно в месте передачи, либо именованную функцию, рассмотрим использование способа map
, предоставляемого особенностью Iterator
в встроенной библиотеке. Чтобы использовать функцию map
для преобразования вектора чисел в вектор строк, мы можем использовать замыкание, например, так:
-fn main() { - let list_of_numbers = vec![1, 2, 3]; - let list_of_strings: Vec<String> = - list_of_numbers.iter().map(|i| i.to_string()).collect(); -}
Или мы можем использовать функцию в качестве переменной map
вместо замыкания, например, так:
-fn main() { - let list_of_numbers = vec![1, 2, 3]; - let list_of_strings: Vec<String> = - list_of_numbers.iter().map(ToString::to_string).collect(); -}
Обратите внимание, что мы должны использовать полный правила написания, о котором мы говорили ранее в разделе "Продвинутые особенности", потому что доступно несколько функций с именем to_string
. Здесь мы используем функцию to_string
определённую в особенности ToString
, который выполнен в встроенной библиотеке для любого вида выполняющего особенность Display
.
Вспомните из раздела "Значения перечислений" главы 6, что имя каждого определённого нами исхода перечисления также становится функцией-объявителем. Мы можем использовать эти объявители в качестве указателей на функции, выполняющих особенности замыканий, что означает, что мы можем использовать объявители в качестве переменных для способов, принимающих замыкания, например, так:
--fn main() { - enum Status { - Value(u32), - Stop, - } - - let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); -}
Здесь мы создаём образцы Status::Value
, используя каждое значение u32
в ряде (0..20), с которым вызывается map
с помощью функции объявителя Status::Value
. Некоторые люди предпочитают этот исполнение, а некоторые предпочитают использовать замыкания. Оба исхода собирается в один и тот же код, поэтому используйте любой исполнение, который вам понятнее.
Замыкания представлены особенностями, что означает, что вы не можете возвращать замыкания из функций. В большинстве случаев, когда вам захочется вернуть особенность, вы можете использовать определенный вид, выполняющий этот особенность, в качестве возвращаемого значения функции. Однако вы не можете сделать подобного с замыканиями, поскольку у них не может быть определенного вида, который можно было бы вернуть; например, вы не можете использовать указатель на функцию fn
в качестве возвращаемого вида.
Следующий код пытается напрямую вернуть замыкание, но он не собирается:
-fn returns_closure() -> dyn Fn(i32) -> i32 {
- |x| x + 1
-}
-Ошибка сборщика выглядит следующим образом:
-$ cargo build
- Compiling functions-example v0.1.0 (file:///projects/functions-example)
-error[E0746]: return type cannot have an unboxed trait object
- --> src/lib.rs:1:25
- |
-1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
- | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
- |
-help: return an `impl Trait` instead of a `dyn Trait`, if all returned values are the same type
- |
-1 | fn returns_closure() -> impl Fn(i32) -> i32 {
- | ~~~~
-help: box the return type, and wrap all of the returned values in `Box::new`
- |
-1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
-2 ~ Box::new(|x| x + 1)
- |
-
-For more information about this error, try `rustc --explain E0746`.
-error: could not compile `functions-example` (lib) due to 1 previous error
-
-Ошибка снова ссылается на особенность Sized
! Ржавчина не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой сбоев ранее. Мы можем использовать особенность-предмет:
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
- Box::new(|x| x + 1)
-}
-Этот код просто отлично собирается. Для получения дополнительной сведений об особенность-предмета. обратитесь к разделу "Использование особенность-предметов которые допускают значения разных видов" главы 17.
-Далее давайте посмотрим на макросы!
- -Мы использовали макросы, такие как println!
на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Понятие макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules!
и три вида процедурных (procedural) макросов:
#[derive]
макросы, которые указывают код, добавленный с помощью свойства derive
, используемые для устройств и перечисленийМы поговорим о каждом из них по очереди, но сначала давайте рассмотрим, зачем вообще нужны макросы, если есть функции.
-По сути, макросы являются способом написания кода, который записывает другой код, что известно как мета программирование. В Приложении C мы обсуждаем свойство derive
, который порождает за вас выполнение различных особенностей. Мы также использовали макросы println!
и vec!
на протяжении книги. Все эти макросы раскрываются для создания большего количества кода, чем исходный код написанный вами вручную.
Мета программирование полезно для уменьшения объёма кода, который вы должны написать и поддерживать, что также является одним из предназначений функций. Однако макросы имеют некоторые дополнительные возможности, которых функции не имеют.
-Ярлык функции должна объявлять некоторое количество и вид этих свойств имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число свойств: мы можем вызвать println!("hello")
с одним переменнаяом или println!("hello {}", name)
с двумя переменнойми. Также макросы раскрываются до того как сборщик преобразует смысл кода, поэтому макрос может, например, выполнить особенность заданного вида. Функция этого не может, потому что она вызывается во время выполнения и особенность должен быть выполнен во время сборки.
Обратной стороной выполнения макроса вместо функции является то, что определения макросов являются более сложными, чем определения функций, потому что вы создаёте Ржавчина код, который записывает другой Ржавчина код. Из-за этой косвенности, объявления макросов, как правило, труднее читать, понимать и поддерживать, чем объявления функций.
-Другое важное различие между макросами и функциями заключается в том, что вы должны объявить макросы или добавить их в область видимости прежде чем можете вызывать их в файле, в отличии от функций, которые вы можете объявить где угодно и вызывать из любого места.
-macro_rules!
для общего мета программированияНаиболее широко используемой способом макросов в Ржавчина являются декларативные макросы. Они также иногда упоминаются как "макросы на примере", "macro_rules!
макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match
в Rust. Как обсуждалось в главе 6, match
выражения являются управляющими устройствами, которые принимают некоторое выражение, итог значения выражения сопоставляют с образцами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с образцами, которые связаны с определенным кодом: в этой случаи значение является записью исходного кода Rust, переданным в макрос. Образцы сравниваются со устройствами этого исходного кода и при совпадении код, связанный с каждым образцом, заменяет код переданный макросу. Все это происходит во время сборки.
Для определения макроса используется устройство macro_rules!
. Давайте рассмотрим, как использовать macro_rules!
глядя на то, как объявлен макрос vec!
. В главе 8 рассказано, как можно использовать макрос vec!
для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор, содержащий три целых числа:
-#![allow(unused)] -fn main() { -let v: Vec<u32> = vec![1, 2, 3]; -}
Мы также могли использовать макрос vec!
для создания вектора из двух целых чисел или вектора из пяти строковых срезов. Мы не смогли бы использовать функцию, чтобы сделать то же самое, потому что мы не знали бы заранее количество или вид значений.
В приложении 19-28 приведено несколько упрощённое определение макроса vec!
.
Файл: src/lib.rs
-#[macro_export]
-macro_rules! vec {
- ( $( $x:expr ),* ) => {
- {
- let mut temp_vec = Vec::new();
- $(
- temp_vec.push($x);
- )*
- temp_vec
- }
- };
-}
--
--Примечание: действительное определение макроса
-vec!
в встроенной библиотеке содержит код для предварительного выделения правильного объёма памяти. Этот код является переработкой, которую мы здесь не используем, чтобы сделать пример проще.
Изложение #[macro_export]
указывает, что данный макрос должен быть доступен всякий раз, когда ящик с объявленным макросом, добавлен в область видимости. Без этой изложении макрос нельзя добавить в область видимости.
Затем мы начинаем объявление макроса с помощью macro_rules!
и имени макроса, который объявляется без восклицательного знака. Название, в данном случае vec
, после которого следуют фигурные скобки, указывающие тело определения макроса.
Устройства в теле макроса vec!
похожа на устройство match
выражения. Здесь у нас есть одна ветвь с образцом ( $( $x:expr ),* )
, затем следует ветвь =>
и раздел кода, связанный с этим образцом. Если образец сопоставлен успешно, то соответствующий раздел кода будет создан. Учитывая, что данный код является единственным образцом в этом макросе, существует только один действительный способ сопоставления, любой другой образец приведёт к ошибке. Более сложные макросы будут иметь более одной ветви.
Допустимый правила написания образца в определениях макросов отличается от правил написания образца рассмотренного в главе 18, потому что образцы макроса сопоставляются со устройствами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части образца в приложении 19-28 что означают; полный правила написания образцов макроса можно найти в Справочнике по Rust.
-Во-первых, мы используем набор скобок, чтобы охватить весь образец. Мы используем знак доллара ( $
) для объявления переменной в системе макросов, которая будет содержать код на Rust, соответствующий образцу. Знак доллара показывает, что это макропеременная, а не обычная переменная Rust. Далее следует набор скобок, в котором определятся значения, соответствующие образцу в скобках, для использования в коде замены. Внутри $()
находится $x:expr
, которое соответствует любому выражению Ржавчина и даёт выражению имя $x
.
Запятая, следующая за $()
указывает на то, что буквенный символ-разделитель запятая может дополнительно появиться после кода, который соответствует коду в $()
. Звёздочка *
указывает, что образец соответствует ноль или больше раз тому, что предшествует *
.
Когда вызывается этот макрос с помощью vec![1, 2, 3];
образец $x
соответствует три раза всем трём выражениям 1
, 2
и 3
.
Теперь давайте посмотрим на образец в теле кода, связанного с этой ветвью: temp_vec.push()
внутри $()*
порождается для каждой части, которая соответствует символу $()
в образце ноль или более раз в зависимости от того, сколько раз образец сопоставлен. Символ $x
заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3];
, созданный код, заменяющий этот вызов макроса будет следующим:
{
- let mut temp_vec = Vec::new();
- temp_vec.push(1);
- temp_vec.push(2);
- temp_vec.push(3);
- temp_vec
-}
-Мы определили макрос, который может принимать любое количество переменных любого вида и может порождать код для создания вектора, содержащего указанные элементы.
-Чтобы узнать больше о том, как писать макросы, обратитесь к онлайн-документации или другим ресурсам, таким как «Маленькая книга макросов Rust» , начатая Дэниелом Кипом и продолженная Лукасом Виртом.
-Вторая разновидность макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются видом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с образцами и замену кода другим кодом, как это делают декларативные макросы. Процедурные макросы могут быть трёх видов: "пользовательского вывода" (custom-derive), "похожие на свойство" (attribute-like) и "похожие на функцию" (function-like), все они работают схожим образом.
-При создании процедурных макросов объявления должны находиться в собственном ящике целенаправленного вида. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. В приложении 19-29 показано, как задать процедурный макрос, где some_attribute
является заполнителем для использования целенаправленного макроса.
Файл: src/lib.rs
-use proc_macro;
-
-#[some_attribute]
-pub fn some_name(input: TokenStream) -> TokenStream {
-}
--
Функция, которая определяет процедурный макрос, принимает TokenStream
в качестве входных данных и создаёт TokenStream
в качестве вывода. Вид TokenStream
объявлен ящиком proc_macro
, включённым в Ржавчина и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным TokenStream
, а код создаваемый макросом является выходным TokenStream
. К функции имеет также прикреплённый свойство, определяющий какой вид процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же ящике.
Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия, делающие другие разновидности отличающимися.
-derive
макросДавайте создадим ящик с именем hello_macro
, который определяет особенность с именем HelloMacro
и имеет одну с ним сопряженную функцию с именем hello_macro
. Вместо того, чтобы пользователи нашего ящика самостоятельно выполнили особенность HelloMacro
для каждого из своих видов, мы предоставим им процедурный макрос, чтобы они могли определять свой вид с помощью свойства #[derive(HelloMacro)]
и получили выполнение по умолчанию для функции hello_macro
. Выполнение по умолчанию выведет Hello, Macro! My name is TypeName!
, где TypeName
- это имя вида, для которого был определён этот особенность. Другими словами, мы напишем ящик, использование которого позволит другому программисту писать код показанный в приложении 19-30.
Файл: src/main.rs
-use hello_macro::HelloMacro;
-use hello_macro_derive::HelloMacro;
-
-#[derive(HelloMacro)]
-struct Pancakes;
-
-fn main() {
- Pancakes::hello_macro();
-}
--
Этот код напечатает Hello, Macro! My name is Pancakes!
, когда мы закончим. Первый шаг - создать новый, библиотечный ящик так:
$ cargo new hello_macro --lib
-
-Далее, мы определим особенность HelloMacro
и сопряженную с ним функцию:
Файл: src/lib.rs
-pub trait HelloMacro {
- fn hello_macro();
-}
-У нас есть особенность и его функция. На этом этапе пользователь ящика может выполнить особенность для достижения желаемой возможности, так:
-use hello_macro::HelloMacro;
-
-struct Pancakes;
-
-impl HelloMacro for Pancakes {
- fn hello_macro() {
- println!("Hello, Macro! My name is Pancakes!");
- }
-}
-
-fn main() {
- Pancakes::hello_macro();
-}
-Тем не менее, ему придётся написать разделвыполнения для каждого вида, который он хотел использовать вместе с hello_macro
; а мы хотим избавить их от необходимости делать эту работу.
Кроме того, мы пока не можем предоставить функцию hello_macro
с выполнением по умолчанию, которая будет печатать имя вида, для которого выполнен особенность: Ржавчина не имеет возможностей рефлексии (reflection), поэтому он не может выполнить поиск имени вида во время выполнения кода. Нам нужен макрос для создания кода во время сборки.
Следующим шагом является определение процедурного макроса. На мгновение написания этой статьи процедурные макросы должны быть в собственном ящике. Со временем это ограничение может быть отменено. Соглашение о внутреннем выстраивании
-ящиков и макросов является следующим: для ящика с именем foo
, его пользовательский, ящик с выводимым процедурным макросом называется foo_derive
. Давайте начнём с создания нового ящика с именем hello_macro_derive
внутри дела hello_macro
:
$ cargo new hello_macro_derive --lib
-
-Наши два ящика тесно связаны, поэтому мы создаём процедурный макрос-ящик в папке ящика hello_macro
. Если мы изменим определение особенности в hello_macro
, то нам придётся также изменить выполнение процедурного макроса в hello_macro_derive
. Два ящика нужно будет обнародовать отдельно и программисты, использующие эти ящики, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что ящик hello_macro
использует hello_macro_derive
как зависимость и реэкспортирует код процедурного макроса. Однако то, как мы внутренне выстраивали
дело, делает возможным программистам использовать hello_macro
даже если они не хотят derive
возможность.
Нам нужно объявить ящик hello_macro_derive
как процедурный макрос-ящик. Также понадобятся возможности из ящиков syn
и quote
, как вы увидите через мгновение, поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml для hello_macro_derive
:
Файл: hello_macro_derive/Cargo.toml
-[lib]
-proc-macro = true
-
-[dependencies]
-syn = "2.0"
-quote = "1.0"
-
-Чтобы начать определение процедурного макроса, поместите код приложения 19-31 в ваш файл src/lib.rs ящика hello_macro_derive
. Обратите внимание, что этот код не собирается пока мы не добавим определение для функции impl_hello_macro
.
Файл: hello_macro_derive/src/lib.rs
-use proc_macro::TokenStream;
-use quote::quote;
-
-#[proc_macro_derive(HelloMacro)]
-pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
- // Construct a representation of Ржавчина code as a syntax tree
- // that we can manipulate
- let ast = syn::parse(input).unwrap();
-
- // Build the trait implementation
- impl_hello_macro(&ast)
-}
--
Обратите внимание, что мы разделили код на функцию hello_macro_derive
, которая отвечает за синтаксический анализ TokenStream
и функцию impl_hello_macro
, которая отвечает за преобразование синтаксического дерева: это делает написание процедурного макроса удобнее. Код во внешней функции ( hello_macro_derive
в данном случае) будет одинаковым для почти любого процедурного макрос ящика, который вы видите или создаёте. Код, который вы указываете в теле внутренней функции (в данном случае impl_hello_macro
) будет отличаться в зависимости от цели вашего процедурного макроса.
Мы представили три новых ящика: proc_macro
syn
и quote
. Макрос proc_macro
поставляется с Rust, поэтому нам не нужно было добавлять его в зависимости внутри Cargo.toml. Макрос proc_macro
- это API сборщика, который позволяет нам читать и управлять Ржавчина кодом из нашего кода.
Ящик syn
разбирает Ржавчина код из строки в устройство данных над которой мы может выполнять действия. Ящик quote
превращает устройства данных syn
обратно в код Rust. Эти ящики упрощают разбор любого вида Ржавчина кода, который мы хотели бы обрабатывать: написание полного синтаксического анализатора для кода Ржавчина не является простой задачей.
Функция hello_macro_derive
будет вызываться, когда пользователь нашей библиотеки указывает своему виду #[derive(HelloMacro)]
. Это возможно, потому что мы определяли функцию hello_macro_derive
с помощью proc_macro_derive
и указали имя HelloMacro
, которое соответствует имени нашего особенности; это соглашение, которому следует большинство процедурных макросов.
Функция hello_macro_derive
сначала преобразует input
из TokenStream
в устройство данных, которую мы можем затем преобразовать и над которой выполнять действия. Здесь ящик syn
вступает в игру. Функция parse
в syn
принимает TokenStream
и возвращает устройство DeriveInput
, представляющую разобранный код Rust. Приложение 19-32 показывает соответствующие части устройства DeriveInput
, которые мы получаем при разборе строки struct Pancakes;
:
DeriveInput {
- // --snip--
-
- ident: Ident {
- ident: "Pancakes",
- span: #0 bytes(95..103)
- },
- data: Struct(
- DataStruct {
- struct_token: Struct,
- fields: Unit,
- semi_token: Some(
- Semi
- )
- }
- )
-}
--
Поля этой устройства показывают, что код Rust, который мы разобрали, является разделустройства с ident
(определителем, означающим имя) Pancakes
. В этой устройстве есть больше полей для описания всех видов кода Rust; проверьте документацию syn
о устройстве DeriveInput
для получения дополнительной сведений.
Вскоре мы определим функцию impl_hello_macro
, в которой построим новый, дополнительный код Rust. Но прежде чем мы это сделаем, обратите внимание, что выводом для нашего выводимого (derive) макроса также является TokenStream
. Возвращаемый TokenStream
добавляется в код, написанный пользователями макроса, поэтому, когда они соберут свой ящик, они получат дополнительную возможность, которую мы предоставляем в изменённом TokenStream
.
Возможно, вы заметили, что мы вызываем unwrap
чтобы выполнить панику в функции hello_macro_derive
, если вызов функции syn::parse
потерпит неудачу. Наш процедурный макрос должен паниковать при ошибках, потому что функции proc_macro_derive
должны возвращать TokenStream
, а не вид Result
для соответствия API процедурного макроса. Мы упроисполнения этот пример с помощью unwrap
, но в рабочем коде вы должны предоставить более определенные сообщения об ошибках, если что-то пошло не правильно, используя panic!
или expect
.
Теперь, когда у нас есть код для преобразования определеного Ржавчина кода из TokenStream
в образец DeriveInput
, давайте создадим код выполняющий особенность HelloMacro
у определеного вида, как показано в приложении 19-33.
Файл: hello_macro_derive/src/lib.rs
-use proc_macro::TokenStream;
-use quote::quote;
-
-#[proc_macro_derive(HelloMacro)]
-pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
- // Construct a representation of Ржавчина code as a syntax tree
- // that we can manipulate
- let ast = syn::parse(input).unwrap();
-
- // Build the trait implementation
- impl_hello_macro(&ast)
-}
-
-fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
- let name = &ast.ident;
- let gen = quote! {
- impl HelloMacro for #name {
- fn hello_macro() {
- println!("Hello, Macro! My name is {}!", stringify!(#name));
- }
- }
- };
- gen.into()
-}
--
Мы получаем образец устройства Ident
содержащий имя (определитель) определеного вида с использованием ast.ident
. Устройства в приложении 19-32 показывает, что когда мы запускаем функцию impl_hello_macro
для кода из приложения 19-30, то получаемый ident
будет иметь поле ident
со значением "Pancakes"
. Таким образом, переменная name
в приложении 19-33 будет содержать образец устройства Ident
, что при печати выдаст строку "Pancakes"
, что является именем устройства в приложении 19-30.
Макрос quote!
позволяет определить код Rust, который мы хотим вернуть. Сборщик ожидает что-то отличное от прямого итога выполнения макроса quote!
, поэтому нужно преобразовать его в TokenStream
. Мы делаем это путём вызова способа into
, который использует промежуточное представление и возвращает значение требуемого вида TokenStream
.
Макрос quote!
также предоставляет очень полезную механику образцов: мы можем ввести #name
и quote!
заменит его значением из переменной name
. Вы можете даже сделать некоторое повторение, подобное тому, как работают обычные макросы. Проверьте документацию ящика quote
для подробного введения.
Мы хотим, чтобы наш процедурный макрос порождал выполнение нашего особенности HelloMacro
для вида, который определял пользователь, который мы можем получить, используя #name
. Выполнение особенности имеет одну функцию hello_macro
, тело которой содержит возможность, которую мы хотим предоставить: напечатать Hello, Macro! My name is
с именем определеного вида.
Макрос stringify!
используемый здесь, встроен в Rust. Он принимает Ржавчина выражение, такое как 1 + 2
и во время сборки сборщик превращает выражение в строковый запись, такой как "1 + 2"
. Он отличается от макросов format!
или println!
, которые вычисляют выражение, а затем превращают итог в виде вида String
. Существует возможность того, что введённый #name
может оказаться выражением для печати буквально как есть, поэтому здесь мы используем stringify!
. Использование stringify!
также уменьшает выделение памяти путём преобразования #name
в строковый запись во время сборки.
На этом этапе приказ cargo build
должна завершиться успешно для обоих hello_macro
и hello_macro_derive
. Давайте подключим эти ящики к коду в приложении 19-30, чтобы увидеть процедурный макрос в действии! Создайте новый двоичный дело в папке ваших дел с использованием приказы cargo new pancakes
. Нам нужно добавить hello_macro
и hello_macro_derive
в качестве зависимостей для ящика pancakes
в файл Cargo.toml. Если вы размещаете свои исполнения hello_macro
и hello_macro_derive
на сайт crates.io, они будут обычными зависимостями; если нет, вы можете указать их как path
зависимости следующим образом:
hello_macro = { path = "../hello_macro" }
-hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
-
-Поместите код в приложении 19-30 в src/main.rs и выполните cargo run
: он должен вывести Hello, Macro! My name is Pancakes!
. Выполнение особенности HelloMacro
из процедурного макроса была включена без необходимости его выполнения ящиком pancakes
; #[derive(HelloMacro)]
добавил выполнение особенности.
Далее давайте рассмотрим, как другие виды процедурных макросов отличаются от пользовательских выводимых макросов.
-Подобные свойствам макросы похожи на пользовательские выводимые макросы, но вместо создания кода для derive
свойства, они позволяют создавать новые свойства. Они являются также более гибкими: derive
работает только для устройств и перечислений; свойство-подобные могут применяться и к другим элементам, таким как функции. Вот пример использования имеющего свойство макроса: допустим, у вас есть свойство именованный route
который определяет функции при использовании фреймворка для веб-приложений:
#[route(GET, "/")]
-fn index() {
-Данный свойство #[route]
будет определён платспособом как процедурный макрос. Ярлык функции определения макроса будет выглядеть так:
#[proc_macro_attribute]
-pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
-Здесь есть два свойства вида TokenStream
. Первый для содержимого свойства: часть GET, "/"
. Второй это тело элемента, к которому прикреплён свойство: в данном случае fn index() {}
и остальная часть тела функции.
Кроме того, имеющие свойства макросы работают так же как и пользовательские выводимые макросы: вы создаёте ящик с видом proc-macro
и выполняете функцию, которая порождает код, который хотите!
Макросы, похожие на функции, выглядят подобно вызову функций. Подобно макросам macro_rules!
они являются более гибкими, чем функции; например, они могут принимать неизвестное количество переменных. Тем не менее, макросы macro_rules!
можно объявлять только с использованием правил написания подобного сопоставлению, который мы обсуждали ранее в разделе "Декларативные макросы macro_rules!
для общего мета программирования". Макросы, похожие на функции, принимают свойство TokenStream
и их определение управляет этим TokenStream
, используя код Rust, как это делают два других вида процедурных макроса. Примером подобного возможностей макроса является макрос sql!
, который можно вызвать так:
let sql = sql!(SELECT * FROM posts WHERE id=1);
-Этот макрос будет разбирать SQL указанию внутри него и проверять, что она синтаксически правильная, что является гораздо более сложной обработкой, чем то что может сделать макрос macro_rules!
. Макрос sql!
мог бы быть определён так:
#[proc_macro]
-pub fn sql(input: TokenStream) -> TokenStream {
-Это определение похоже на ярлык пользовательского выводимого макроса: мы получаем токены, которые находятся внутри скобок и возвращаем код, который мы хотели создать.
-Фух! Теперь у вас в распоряжении есть некоторые возможности Rust, которые вы не будете часто использовать, но вы будете знать, что они доступны в особых обстоятельствах. Мы представили несколько сложных тем, чтобы при появлении сообщения с предложением исправить ошибку или в коде других людей, вы могли бы распознать эти подходы и правила написания. Используйте эту главу как справочник, который поможет вам найти решение.
-Далее мы применим в действительностивсе, что обсуждали на протяжении всей книги, и выполним ещё один дело!
- -Это был долгий путь, но мы дошли до конца книги. В этой главе мы сделаем ещё один дело, чтобы закрепить несколько тем из последних глав и резюмировать то, что прошли в самом начале.
-В качестве нашего конечного дела мы напишем веб-сервер, который выводит надпись “hello” в веб-браузере, как на рисунке 20-1.
- --
Для создания веб-сервера нам понадобится:
-Прежде чем мы начнём, заметим: способ, который мы будем использовать - не лучшим способ создания веб-сервера на Rust. Члены сообщества уже обнародовали на crates.io несколько готовых к использованию ящиков, которые предоставляют более полные выполнения веб-сервера и объединения потоков, чем те, которые мы создадим. Однако наша цель в этой главе — научиться новому, а не идти по лёгкому пути. Поскольку Ржавчина — это язык системного программирования, мы можем выбирать тот уровень абстракции, который нам подходит, и можем переходить на более низкий уровень, что может быть невозможно или неприменимо в других языках. Поэтому мы напишем основной HTTP-сервер и объединениепотоков вручную, чтобы вы могли изучить общие мысли и способы, лежащие в основе ящиков, которые, возможно, вы будете использовать в будущем.
- -Начнём с однопоточного веб-сервера. Перед тем, как начать, давайте сделаем краткий обзор протоколов, задействованных при создании веб-серверов. Детальное описание этих протоколов выходит за рамки этой книги, но краткий обзор даст вам необходимую сведения.
-Двумя основными протоколами, используемыми в веб-серверах, являются протокол передачи гипертекста (HTTP - Hypertext Transfer Protocol) и Протокол управления передачей (TCP - Transmission Control Protocol). Оба протокола являются протоколами вида запрос-ответ (request-response), то есть клиент объявляет запросы, а сервер слушает эти запросы и предоставляет ответ клиенту. Содержимое этих запросов и ответов определяется протоколами.
-TCP - это протокол нижнего уровня, который описывает подробности того, как сведения передаётся от одного сервера к другому, но не определяет, что это за сведения. HTTP строится поверх TCP, определяя содержимое запросов и ответов. Технически возможно использовать HTTP с другими протоколами, но в подавляющем большинстве случаев HTTP отправляет свои данные поверх TCP. Мы будем работать с необработанными байтами в TCP и запросами и ответами в HTTP.
-Нашему веб-серверу необходимо прослушивать TCP-соединение, так что это первая часть, над которой мы будем работать. Обычная библиотека предлагает для этого звено std::net
. Сделаем новый дело обычным способом:
$ cargo new hello
- Created binary (application) `hello` project
-$ cd hello
-
-Дл начала добавьте код из приложения 20-1 в файл src/main.rs. Этот код будет прослушивать входящие TCP потоки по адресу 127.0.0.1:7878
. Когда сервер примет входящий поток, он напечатает Connection established!
("Соединение установлено!").
Файл: src/main.rs
--use std::net::TcpListener; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - println!("Connection established!"); - } -}
-
Используя TcpListener
мы можем слушать TCP соединения к адресу 127.0.0.1:7878
. В адресе, в его части перед двоеточием, сначала идёт IP-адрес, относящийся к вашему компьютеру (он одинаковый на каждом компьютере и не представляет определенный компьютер автора), а часть 7878
является портом. Мы выбрали этот порт по двум причинам: HTTP обычно не используется на этом порту, поэтому маловероятно, что наш сервер будет враждовать с каким-нибудь другим сервером, который может выполняться на вашей машине, и ещё 7878 - это слово rust, набранное на телефоне.
Функция bind
в этом сценарии работает так же, как функция new
, поскольку она возвращает новый образец TcpListener
. Причина, по которой функция называется bind
заключается в том, что в сетевой совокупности понятий подключение к порту для прослушивания называется «привязка к порту» (“binding to a port”).
Функция bind
возвращает Result<T, E>
, а это значит, что привязка может не состояться. Так, например, подключение к порту 80 предполагает наличие привилегий администратора (прочие пользователи могут прослушивать порты только от 1023-го и выше), поэтому если мы попытаемся подключиться к порту 80, не будучи администратором, привязка не сработает. Привязка также не выполнится, например, если мы запустим два образца нашей программы, прослушивающие один и тот же порт. Поскольку мы пишем простейший сервер в учебных целях, мы не будем беспокоиться об обработке подобных ошибок; вместо этого мы используем unwrap
для прекращения работы программы в случае возникновения ошибок.
Способ incoming
в TcpListener
возвращает повторитель , который даёт нам последовательность потоков (определеннее, потоков вида TcpStream
). Один поток представляет собой открытое соединение между клиентом и сервером. Соединением называется полный этап запроса и ответа, в котором клиент подключается к серверу, сервер порождает ответ, и сервер закрывает соединение. Таким образом, мы будем читать из потока TcpStream
то, что отправил клиент, а затем записывать наш ответ в поток, для отправки его обратно клиенту. В целом, цикл for
будет обрабатывать каждое соединение по очереди и создавать серию потоков, которые мы будем обрабатывать.
На текущий мгновение наша обработка потока состоит из вызова unwrap
для завершения программы, если в потоке возникли ошибки, если же таковых не обнаружится, программа выведет сообщение. В следующем приложении мы добавим больше возможности для успешного сценария. Причиной того, что мы можем получать ошибки от способа incoming
, когда клиент подключается к серверу, является то, что на самом деле мы не перебираем подключения. На самом деле мы перебираем попытки подключения. Подключение может не состояться по ряду причин, многие из которых зависят от операционной системы. Например, многие операционные системы имеют ограничение на количество одновременно открытых соединений, которые они могут поддерживать; при превышении этого предела новые попытки установить соединение будут приводить к ошибке, пока какие-либо из уже открытых соединений не будут закрыты.
Попробуем запустить этот код! Вызовите cargo run
в окне вызова, а затем загрузите 127.0.0.1:7878 в веб-браузере. В браузере должно отображаться сообщение об ошибке, например «Connection reset», поскольку сервер в настоящее время не отправляет обратно никаких данных. Но когда вы посмотрите на свой окно вызова, вы должны увидеть несколько сообщений, которые были напечатаны, когда браузер подключался к серверу!
Running `target/debug/hello`
- Connection established!
- Connection established!
- Connection established!
-
-Иногда вы видите несколько сообщений, напечатанных для одного запроса браузера; Причина может заключаться в том, что браузер выполняет запрос страницы, а также других ресурсов, таких как значок favicon.ico, который отображается на вкладке браузера.
-Также может быть, что браузер пытается подключиться к серверу несколько раз, потому что сервер не отвечает. Когда stream
выходит из области видимости и отбрасывается в конце цикла, соединение закрывается как часть выполнения drop
. Браузеры иногда обрабатывают закрытые соединения, повторяя попытки, потому что неполадка может быть временной. Важным обстоятельством является то, что мы успешно получили указатель TCP-соединения!
Не забудьте остановить программу, нажав ctrl-c, когда вы закончите выполнение определённой исполнения кода. Затем перезапустите программу, вызвав приказ cargo run
, после того, как вы внесли какой-либо набор изменений, чтобы убедиться, что выполняется самая свежая исполнение кода.
Выполняем возможности чтения запроса из браузера! Чтобы разделить части, связанные с получением соединения и последующим действием с ним, мы запустим новую функцию для обработки соединения. В этой новой функции handle_connection
мы будем читать данные из потока TCP и распечатывать их, чтобы мы могли видеть данные, отправленные из браузера. Измените код, чтобы он выглядел как в приложении 20-2.
Файл: src/main.rs
--use std::{ - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let http_request: Vec<_> = buf_reader - .lines() - .map(|result| result.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - println!("Request: {http_request:#?}"); -}
-
Мы добавляем std::io::prelude
и std::io::BufReader
в область видимости, чтобы получить доступ к особенностям и видам, которые позволяют нам читать и писать в поток. В цикле for
функции main
вместо вывода сообщения о том, что мы установили соединение, мы теперь вызываем новую функцию handle_connection
и передаём ей stream
.
В функции handle_connection
мы создаём новый образец BufReader
, который оборачивает изменяемую ссылку на stream
. BufReader
добавляет буферизацию, управляя вызовами способов особенности std::io::Read
за нас.
Мы создаём переменную http_request
для сбора строк запроса, который браузер отправляет на наш сервер. Мы указываем, что хотим собрать эти строки в вектор, добавляя изложение вида Vec<_>
.
BufReader
выполняет особенность std::io::BufRead
, который выполняет способ lines
. Способ lines
возвращает повторитель Result<String, std::io::Error>
, разделяющий поток данных на части всякий раз, когда ему попадается байт новой строки. Чтобы получить все строки String
, мы с помощью map вызываем unwrap
у каждого Result
. Значение Result
может быть ошибкой, если данные не соответствуют исполнению UTF-8 или если возникли сбоев с чтением из потока. Опять же, программа в промышленном исполнении должна обрабатывать эти ошибки более изящно, но мы для простоты решили прекращать работу программы в случае ошибки.
Браузер указывает об окончании HTTP-запроса, отправляя два символа перевода строки подряд, поэтому, чтобы получить один запрос из потока, мы забираем строки, пока не получим строку, которая является пустой строкой. После того, как мы собрали строки в вектор, мы распечатываем их, используя красивое отладочное изменение -, чтобы мы могли взглянуть на указания, которые веб-браузер отправляет на наш сервер.
-Попробуем этот код! Запустите программу и снова сделайте запрос в веб-браузере. Обратите внимание, что мы по-прежнему будем получать в браузере страницу с ошибкой, но вывод нашей программы в окне вызова теперь будет выглядеть примерно так:
-$ cargo run
- Compiling hello v0.1.0 (file:///projects/hello)
- Finished dev [unoptimized + debuginfo] target(s) in 0.42s
- Running `target/debug/hello`
-Request: [
- "GET / HTTP/1.1",
- "Host: 127.0.0.1:7878",
- "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
- "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
- "Accept-Language: en-US,en;q=0.5",
- "Accept-Encoding: gzip, deflate, br",
- "DNT: 1",
- "Connection: keep-alive",
- "Upgrade-Insecure-Requests: 1",
- "Sec-Fetch-Dest: document",
- "Sec-Fetch-Mode: navigate",
- "Sec-Fetch-Site: none",
- "Sec-Fetch-User: ?1",
- "Cache-Control: max-age=0",
-]
-
-В зависимости от вашего браузера итог может немного отличаться. Теперь, когда мы печатаем данные запроса, мы можем понять, почему мы получаем несколько подключений из одного запроса браузера, посмотрев на путь после GET
в первой строке запроса. Если все повторяющиеся соединения запрашивают / , мы знаем, что браузер пытается получить / повторно, потому что он не получает ответа от нашей программы.
Давайте разберём эти данные запроса, чтобы понять, что браузер запрашивает у нашей программы.
-HTTP - это текстовый протокол и запрос имеет следующий вид:
-Method Request-URI HTTP-Version CRLF
-headers CRLF
-message-body
-
-Первая строка - это строка запроса , содержащая сведения о том, что запрашивает клиент. Первая часть строки запроса указывает используемый способ , например GET
или POST
, который описывает, как клиент выполняет этот запрос. Наш клиент использовал запрос GET
, что означает, что он просит нас предоставить сведения.
Следующая часть строки запроса - это /, которая указывает унифицированный определитель ресурса (URI), который запрашивает клиент: URI почти, но не совсем то же самое, что и унифицированный указатель ресурса (URL). Разница между URI и URL-адресами не важна для наших целей в этой главе, но согласно принятых требований HTTP использует понятие URI, поэтому мы можем просто мысленно заменить URL-адрес здесь.
-Последняя часть - это исполнение HTTP, которую использует клиент, а затем строка запроса заканчивается последовательностью CRLF . (CRLF обозначает возврат каретки и перевод строки , что является понятием из дней пишущих машинок!) Последовательность CRLF также может быть записана как \r\n
, где \r
- возврат каретки, а \n
- перевод строки. Последовательность CRLF отделяет строку запроса от остальных данных запроса. Обратите внимание, что при печати CRLF мы видим начало новой строки, а не \r\n
.
Глядя на данные строки запроса, которые мы получили от запуска нашей программы, мы видим, что GET
- это способ, / - это URI запроса, а HTTP/1.1
- это исполнение.
После строки запроса оставшиеся строки, начиная с Host:
далее, являются заголовками. GET
запросы не имеют тела.
Попробуйте сделать запрос из другого браузера или запросить другой адрес, например 127.0.0.1:7878/test , чтобы увидеть, как изменяются данные запроса.
-Теперь, когда мы знаем, что запрашивает браузер, давайте отправим обратно в ответ некоторые данные!
-Теперь выполняем отправку данных в ответ на запрос клиента. Ответы имеют следующий вид:
-HTTP-Version Status-Code Reason-Phrase CRLF
-headers CRLF
-message-body
-
-Первая строка - это строка состояния, которая содержит исполнение HTTP, используемую в ответе, числовой код состояния, который суммирует итог запроса, и фразу причины, которая предоставляет текстовое описание кода состояния. После последовательности CRLF идут любые заголовки, другая последовательность CRLF и тело ответа.
-Вот пример ответа, который использует HTTP исполнения 1.1, имеет код состояния 200, фразу причины OK, без заголовков и без тела:
-HTTP/1.1 200 OK\r\n\r\n
-
-Код состояния 200 - это обычный успешный ответ. Текст представляет собой крошечный успешный HTTP-ответ. Давайте запишем это в поток как наш ответ на успешный запрос! Из функции handle_connection
удалите println!
который печатал данные запроса и заменял их кодом из Приложения 20-3.
Файл: src/main.rs
--use std::{ - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let http_request: Vec<_> = buf_reader - .lines() - .map(|result| result.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - let response = "HTTP/1.1 200 OK\r\n\r\n"; - - stream.write_all(response.as_bytes()).unwrap(); -}
-
Первый перевод строки определяет переменную response
, которая содержит данные сообщения об успешном выполнении. Затем мы вызываем as_bytes
в нашем response
, чтобы преобразовать строковые данные в байты. Способ write_all
в stream
принимает вид &[u8]
и отправляет эти байты непосредственно получателю. Поскольку действие write_all
может завершиться с ошибкой, мы, как и ранее, используем unwrap
на любом возможно ошибочном итоге. И опять, в существующем приложении здесь вам нужно было бы добавить обработку ошибок.
После этих изменений давайте запустим наш код и сделаем запрос. Мы больше не печатаем никаких данных в окно вызова, поэтому мы не увидим никакого вывода, кроме сообщений от Cargo. Когда вы загрузите 127.0.0.1:7878 в веб-браузере, вы должны получить пустую страницу вместо ошибки. Вы только что вручную написали код получения HTTP-запроса и отправки ответа на него!
-Давайте выполняем возможности чего-нибудь большего, чем просто пустой страницы. Создайте новый файл hello.html в корне папки вашего дела, а не в папке src . Вы можете ввести любой HTML-код, который вам заблагорассудится; В приложении 20-4 показан один из исходов.
-Файл: hello.html
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Hello!</title>
- </head>
- <body>
- <h1>Hello!</h1>
- <p>Hi from Rust</p>
- </body>
-</html>
-
--
Это простейший HTML5-документ с заголовком и каким-то текстом. Чтобы сервер возвращал его в ответ на полученный запрос, мы изменим handle_connection
, как показано в приложении 20-5, чтобы считать HTML-файл, добавить его в ответ в качестве тела и отправить.
Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; -// --snip-- - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let http_request: Vec<_> = buf_reader - .lines() - .map(|result| result.unwrap()) - .take_while(|line| !line.is_empty()) - .collect(); - - let status_line = "HTTP/1.1 200 OK"; - let contents = fs::read_to_string("hello.html").unwrap(); - let length = contents.len(); - - let response = - format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(response.as_bytes()).unwrap(); -}
-
Мы добавили элемент fs
в указанию use
, чтобы включить в область видимости звено файловой системы встроенной библиотеки. Код для чтения содержимого файла в строку должен выглядеть знакомым для вас; мы использовали его в главе 12, когда читали содержимое файла для нашего дела ввода-вывода в приложении 12-4.
Далее мы используем format!
чтобы добавить содержимое файла в качестве тела ответа об успешном завершении. Чтобы обеспечить действительный HTTP-ответ, мы добавляем заголовок Content-Length
который имеет размер тела нашего ответа, в данном случае размер hello.html
.
Запустите этот код приказом cargo run
и загрузите 127.0.0.1:7878 в браузере; вы должны увидеть выведенный HTML в браузере!
В настоящее время мы пренебрегаем данные запроса в переменной http_request
и в любом случае просто отправляем обратно содержимое HTML-файла. Это означает, что если вы попытаетесь запросить адрес 127.0.0.1:7878/something-else в своём браузере, вы все равно получите тот же самый HTML-ответ. Пока что наш сервер очень ограничен, и не умеет делать то, что делает большинство веб-серверов. Мы хотим настроить наши ответы в зависимости от запроса и отправлять обратно HTML-файл только для правильно созданного запроса к пути / .
Сейчас наш веб-сервер возвращает HTML из файла независимо от того, что определенно запросил клиент. Давайте добавим проверку того, что браузер запрашивает /, прежде чем вернуть HTML-файл, и будем возвращать ошибку, если браузер запрашивает что-то постороннее. Для этого нам нужно изменять handle_connection
, как показано в приложении 20-6. Новый код проверяет соответствует ли требуемый запросом ресурс с определителем /, и содержит разделы if
и else
, чтобы иначе обрабатывать другие запросы.
Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} -// --snip-- - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let request_line = buf_reader.lines().next().unwrap().unwrap(); - - if request_line == "GET / HTTP/1.1" { - let status_line = "HTTP/1.1 200 OK"; - let contents = fs::read_to_string("hello.html").unwrap(); - let length = contents.len(); - - let response = format!( - "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" - ); - - stream.write_all(response.as_bytes()).unwrap(); - } else { - // some other request - } -}
-
Мы будем рассматривать только первую строку HTTP-запроса, поэтому вместо того, чтобы читать весь запрос в вектор, мы вызываем next
, чтобы получить первый элемент из повторителя. Первый вызов unwrap
заботится об обработке Option
и останавливает программу, если в повторителе нет элементов. Второй unwrap
обрабатывает Result
и имеет тот же эффект, что и unwrap
, который был в map
, добавленном в приложении 20-2.
Затем мы проверяем переменную request_line
, чтобы увидеть, равна ли она строке запроса, соответствующей запросу GET для пути / . Если это так, разделif
возвращает содержимое нашего HTML-файла.
Если request_line
не равна запросу GET для пути /, это означает, что мы получили какой-то другой запрос. Мы скоро добавим код в разделelse
, чтобы ответить на все остальные запросы.
Запустите этот код сейчас и запросите 127.0.0.1:7878 ; вы должны получить HTML в hello.html . Если вы сделаете любой другой запрос, например 127.0.0.1:7878/something-else , вы получите ошибку соединения, подобную той, которую вы видели при запуске кода из Приложения 20-1 и Приложения 20-2.
-Теперь давайте добавим код из приложения 20-7 в разделelse
чтобы вернуть ответ с кодом состояния 404, который указывает о том, что содержание для запроса не найден. Мы также вернём HTML-код для страницы, отображаемой в браузере, с указанием ответа конечному пользователю.
Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let request_line = buf_reader.lines().next().unwrap().unwrap(); - - if request_line == "GET / HTTP/1.1" { - let status_line = "HTTP/1.1 200 OK"; - let contents = fs::read_to_string("hello.html").unwrap(); - let length = contents.len(); - - let response = format!( - "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" - ); - - stream.write_all(response.as_bytes()).unwrap(); - // --snip-- - } else { - let status_line = "HTTP/1.1 404 NOT FOUND"; - let contents = fs::read_to_string("404.html").unwrap(); - let length = contents.len(); - - let response = format!( - "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" - ); - - stream.write_all(response.as_bytes()).unwrap(); - } -}
-
Здесь ответ имеет строку состояния с кодом 404 и фразу причины NOT FOUND
. Тело ответа будет HTML из файла 404.html. Вам нужно создать файл 404.html рядом с hello.html для этой страницы ошибки; снова не стесняйтесь использовать любой HTML код или пример HTML кода в приложении 20-8.
Файл: 404.html
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>Hello!</title>
- </head>
- <body>
- <h1>Oops!</h1>
- <p>Sorry, I don't know what you're asking for.</p>
- </body>
-</html>
-
--
С этими изменениями снова запустите сервер. Запрос на 127.0.0.1:7878 должен возвращать содержимое hello.html, и любой другой запрос, как 127.0.0.1:7878/foo, должен возвращать сообщение об ошибке HTML от 404.html.
-На текущий мгновение разделы if
и else
во многом повторяются: они оба читают файлы и записывают содержимое файлов в поток. Разница лишь в строке состояния и имени файла. Давайте сделаем код более кратким, вынеся эти отличия в отдельные разделы if
и else
, в которых переменным будут присвоены значения строки состояния и имени файла; далее эти переменные мы сможем использовать в коде для чтения файла и создания ответа. В приложении 20-9 показан код после изменения объёмных разделов if
и else
.
Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} -// --snip-- - -fn handle_connection(mut stream: TcpStream) { - // --snip-- - let buf_reader = BufReader::new(&mut stream); - let request_line = buf_reader.lines().next().unwrap().unwrap(); - - let (status_line, filename) = if request_line == "GET / HTTP/1.1" { - ("HTTP/1.1 200 OK", "hello.html") - } else { - ("HTTP/1.1 404 NOT FOUND", "404.html") - }; - - let contents = fs::read_to_string(filename).unwrap(); - let length = contents.len(); - - let response = - format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(response.as_bytes()).unwrap(); -}
-
Теперь разделы if
и else
возвращают только соответствующие значения для строки состояния и имени файла в упорядоченном ряде. Затем мы используем разъединение, чтобы присвоить эти два значения status_line
и filename
используя образец в указания let
, как обсуждалось в главе 18.
Ранее повторяющийся код теперь находится вне разделов if
и else
и использует переменные status_line
и filename
. Это позволяет легче увидеть разницу между этими двумя случаями и означает, что у нас есть только одно место для обновления кода, если захотим изменить работу чтения файлов и записи ответов. Поведение кода в приложении 20-9 будет таким же, как и в 20-8.
Потрясающие! Теперь у нас есть простой веб-сервер примерно на 40 строках кода Rust, который отвечает на один запрос страницей с содержанием и отвечает на все остальные запросы ответом 404.
-В настоящее время наш сервер работает в одном потоке, что означает, что он может обслуживать только один запрос за раз. Давайте разберёмся, почему это может быть неполадкой, сымитировав несколько медленных запросов. Затем мы исправим случай так, чтобы наш сервер мог обрабатывать несколько запросов одновременно.
- -В текущей выполнения сервер обрабатывает каждый запрос по очереди, то есть, он не начнёт обрабатывать второе соединение, пока не завершит обработку первого. При росте числа запросов к серверу, такое последовательное выполнение было бы все менее и менее разумным. Если сервер получает какой-то запрос, обработка которого занимает достаточно много времени, последующим запросам придётся ждать завершения обработки длительного запроса, даже если эти новые запросы сами по себе могут быть обработаны быстро. Нам нужно это исправить, но сначала рассмотрим неполадку в действии.
-Мы посмотрим, как запрос с медленной обработкой может повлиять на другие запросы, сделанные к серверу в текущей выполнения. В приложении 20-10 выполнена обработка запроса к ресурсу /sleep с эмуляцией медленного ответа, при которой сервер будет ждать 5 секунд перед тем, как ответить.
-Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; -// --snip-- - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - handle_connection(stream); - } -} - -fn handle_connection(mut stream: TcpStream) { - // --snip-- - - let buf_reader = BufReader::new(&mut stream); - let request_line = buf_reader.lines().next().unwrap().unwrap(); - - let (status_line, filename) = match &request_line[..] { - "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), - "GET /sleep HTTP/1.1" => { - thread::sleep(Duration::from_secs(5)); - ("HTTP/1.1 200 OK", "hello.html") - } - _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), - }; - - // --snip-- - - let contents = fs::read_to_string(filename).unwrap(); - let length = contents.len(); - - let response = - format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(response.as_bytes()).unwrap(); -}
-
Мы переключились с if
на match
, так как теперь у нас есть три случая. Нам придётся явно сопоставить срез от request_line
для проверки совпадения образца со строковыми записями; match
не делает самостоятельно е ссылки и разыменования, как это делает способ равенства.
Первая ветка совпадает с разделом if
из приложения 20-9. Вторая ветка соответствует запросу /sleep . Когда этот запрос получен, сервер заснёт на 5 секунд, прежде чем отдать успешную HTML-страницу. Третья ветка совпадает с разделом else
из приложения 20-9.
Можно увидеть, насколько прост наш сервер: в существующих библиотеках распознавание разных запросов осуществлялось бы гораздо менее многословно!
-Запустите сервер приказом cargo run
. Затем откройте два окна браузера: одно с адресом http://127.0.0.1:7878/, другое с http://127.0.0.1:7878/sleep. Если вы несколько раз обратитесь к URI /, то как и раньше увидите, что сервер быстро ответит. Но если вы введёте URI /sleep, а затем загрузите URI /, то увидите что / ждёт, пока /sleep
не отработает полные 5 секунд перед загрузкой страницы.
Есть несколько способов, которые можно использовать, чтобы избавиться от подтормаживания запросов после одного медленного запроса; способ, который мы выполняем, называется объединением потоков.
-Объединение потоков является объединением заранее порождённых потоков, ожидающих в объединении и готовых выполнить задачу. Когда программа получает новую задачу, она назначает эту задачу одному из потоков в объединении, и тогда задача будет обработана этим потоком. Остальные потоки в объединении доступны для обработки любых других задач, поступающих в то время, пока первый поток занят. Когда первый поток завершает обработку своей задачи, он возвращается в объединениесвободных потоков, готовых приступить к новой задаче. Объединение потоков позволяет обрабатывать соединения одновременно, увеличивая пропускную способность вашего сервера.
-Мы ограничим число потоков в объединении небольшим числом, чтобы защитить нас от атак вида «отказ в обслуживании» (DoS - Denial of Service); если бы наша программа создавала новый поток в мгновение поступления каждого запроса, то кто-то сделавший 10 миллионов запросов к серверу, мог бы создать хаос, использовать все ресурсы нашего сервера и остановить обработку запросов.
-Вместо порождения неограниченного количества потоков, у нас будет определенное количество потоков, ожидающих в объединении. Поступающие запросы будут отправляться в объединениедля обработки. Объединение будет иметь очередь входящих запросов. Каждый из потоков в объединении будет извлекать запрос из этой очереди, обрабатывать запрос и затем запрашивать в очереди следующий запрос. При таком внешнем виде мы можем обрабатывать N
запросов одновременно, где N
- количество потоков. Если каждый поток отвечает на длительный запрос, последующие запросы могут по-прежнему задержаться в очереди, но теперь мы увеличили количество "длинных" запросов, которые мы можем обработать, перед тем, как эта случаей снова возникнет.
Этот подход - лишь один из многих способов улучшить пропускную способность веб-сервера. Другими исходами, на которые возможно стоило бы обратить внимание, являются: прообраз fork/join, прообраз однопоточного не согласованного ввода-вывода или прообраз многопоточного не согласованного ввода-вывода. Если вам важна эта тема, вы можете почитать больше сведений о других решениях и попробовать выполнить их самостоятельно. С таким низкоуровневым языком как Rust, любой из этих исходов осуществим.
-Прежде чем приступить к выполнения объединения потоков, давайте поговорим о том, как должно выглядеть использование объединения . Когда вы пытаетесь создать код, сначала необходимо написать клиентский внешнюю оболочку. Напишите API кода, чтобы он был внутренне выстроен так, как вы хотите его вызывать, затем выполните возможность данной устройства, вместо подхода выполнить возможности. а затем разрабатывать общедоступный API.
-Подобно тому, как мы использовали разработку через проверка (test-driven) в деле главы 12, мы будем использовать здесь разработку, управляемую сборщиком (compiler-driven). Мы напишем код, вызывающий нужные нам функции, а затем посмотрим на ошибки сборщика, чтобы определить, что мы должны изменить дальше, чтобы заставить код работать. Однако перед этим, в качестве отправной точки, мы рассмотрим технику, которую мы не будем применять в дальнейшем.
- - -Сначала давайте рассмотрим, как мог бы выглядеть код, если бы он создавал бы новый поток для каждого соединения. Как упоминалось ранее, мы не собираемся использовать этот способ в окончательной выполнения, из-за возможных неполадок при возможно неограниченном числе порождённых потоков. Это лишь отправная точка, с которой начнёт работу наш многопоточный сервер. Затем мы улучшим код, добавив объединениепотоков, и тогда разницу между этими двумя решениями будет легче заметить. В приложении 20-11 показаны изменения, которые нужно внести в код main
, чтобы порождать новый поток для обработки каждого входящего соединения внутри цикла for
.
Файл: src/main.rs
--use std::{ - fs, - io::{prelude::*, BufReader}, - net::{TcpListener, TcpStream}, - thread, - time::Duration, -}; - -fn main() { - let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); - - for stream in listener.incoming() { - let stream = stream.unwrap(); - - thread::spawn(|| { - handle_connection(stream); - }); - } -} - -fn handle_connection(mut stream: TcpStream) { - let buf_reader = BufReader::new(&mut stream); - let request_line = buf_reader.lines().next().unwrap().unwrap(); - - let (status_line, filename) = match &request_line[..] { - "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), - "GET /sleep HTTP/1.1" => { - thread::sleep(Duration::from_secs(5)); - ("HTTP/1.1 200 OK", "hello.html") - } - _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), - }; - - let contents = fs::read_to_string(filename).unwrap(); - let length = contents.len(); - - let response = - format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); - - stream.write_all(response.as_bytes()).unwrap(); -}
-
Как вы изучили в главе 16, функция thread::spawn
создаст новый поток и затем запустит код замыкания в этом новом потоке. Если вы запустите этот код и загрузите /sleep в своём браузере, а затем загрузите / в двух других вкладках браузера, вы действительно увидите, что запросам к / не приходится ждать завершения /sleep. Но, как мы уже упоминали, это в какой-то мгновение приведёт к сильному снижению производительности системы, так как вы будете создавать новые потоки без каких-либо ограничений.
Мы хотим, чтобы наш объединениепотоков работал подобным, знакомым образом, чтобы переключение с потоков на объединениепотоков не требовало больших изменений в коде использующем наш API. В приложении 20-12 показан гипотетический внешняя оболочка для устройства ThreadPool
, который мы хотим использовать вместо thread::spawn
.
Файл: src/main.rs
-use std::{
- fs,
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- thread,
- time::Duration,
-};
-
-fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- let pool = ThreadPool::new(4);
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- pool.execute(|| {
- handle_connection(stream);
- });
- }
-}
-
-fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let request_line = buf_reader.lines().next().unwrap().unwrap();
-
- let (status_line, filename) = match &request_line[..] {
- "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
- "GET /sleep HTTP/1.1" => {
- thread::sleep(Duration::from_secs(5));
- ("HTTP/1.1 200 OK", "hello.html")
- }
- _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
- };
-
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
-
- let response =
- format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
-
- stream.write_all(response.as_bytes()).unwrap();
-}
--
Мы используем ThreadPool::new
, чтобы создать новый объединениепотоков с конфигурируемым числом потоков, в данном случае четырьмя. Затем в цикле for
функция pool.execute
имеет внешнюю оболочку, похожий на thread::spawn
, в том смысле, что он так же принимает замыкание, код которого объединениедолжен выполнить для каждого соединения. Нам нужно выполнить pool.execute
, чтобы он принимал замыкание и передавал его потоку из объединения для выполнения. Этот код пока не собирается, но мы постараемся, чтобы сборщик помог нам это исправить.
ThreadPool
с помощью разработки, управляемой сборщикомВнесите изменения приложения 20-12 в файл src/main.rs, а затем давайте воспользуемся ошибками сборщика из приказы cargo check
для управления нашей разработкой. Вот первая ошибка, которую мы получаем:
$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
- --> src/main.rs:11:16
- |
-11 | let pool = ThreadPool::new(4);
- | ^^^^^^^^^^ use of undeclared type `ThreadPool`
-
-For more information about this error, try `rustc --explain E0433`.
-error: could not compile `hello` (bin "hello") due to 1 previous error
-
-Замечательно! Ошибка говорит о том, что нам нужен вид или звено ThreadPool
, поэтому мы сейчас его создадим. Наша выполнение ThreadPool
не будет зависеть от того, что делает наш веб-сервер. Итак, давайте переделаем ящик hello
из двоичного в библиотечный, чтобы хранить там нашу выполнение ThreadPool
. После того, как мы переключимся в библиотечный ящик, мы также сможем использовать отдельную библиотеку объединения потоков для любой подходящей работы, а не только для обслуживания веб-запросов.
Создайте файл src/lib.rs, который содержит следующий код, который является простейшим определением устройства ThreadPool
, которое мы можем иметь на данный мгновение:
Файл: src/lib.rs
-pub struct ThreadPool;
-Затем изменените файл main.rs, чтобы внести ThreadPool
из библиотечного ящика в текущую область видимости, добавив следующий код в начало src/main.rs:
Файл: src/main.rs
-use hello::ThreadPool;
-use std::{
- fs,
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- thread,
- time::Duration,
-};
-
-fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- let pool = ThreadPool::new(4);
-
- for stream in listener.incoming() {
- let stream = stream.unwrap();
-
- pool.execute(|| {
- handle_connection(stream);
- });
- }
-}
-
-fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let request_line = buf_reader.lines().next().unwrap().unwrap();
-
- let (status_line, filename) = match &request_line[..] {
- "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
- "GET /sleep HTTP/1.1" => {
- thread::sleep(Duration::from_secs(5));
- ("HTTP/1.1 200 OK", "hello.html")
- }
- _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
- };
-
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
-
- let response =
- format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
-
- stream.write_all(response.as_bytes()).unwrap();
-}
-Этот код по-прежнему не будет работать, но давайте проверим его ещё раз, чтобы получить следующую ошибку, которую нам нужно устранить:
-$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
- --> src/main.rs:12:28
- |
-12 | let pool = ThreadPool::new(4);
- | ^^^ function or associated item not found in `ThreadPool`
-
-For more information about this error, try `rustc --explain E0599`.
-error: could not compile `hello` (bin "hello") due to 1 previous error
-
-Эта ошибка указывает, что далее нам нужно создать сопряженную функцию с именем new
для ThreadPool
. Мы также знаем, что new
должна иметь один свойство, который может принимать 4
в качестве переменной и должен возвращать образец ThreadPool
. Давайте выполняем простейшую функцию new
, которая будет иметь эти свойства:
Файл: src/lib.rs
-pub struct ThreadPool;
-
-impl ThreadPool {
- pub fn new(size: usize) -> ThreadPool {
- ThreadPool
- }
-}
-Мы выбираем usize
в качестве вида свойства size
, потому что мы знаем, что отрицательное число потоков не имеет никакого смысла. Мы также знаем, что мы будем использовать число 4 в качестве количества элементов в собрания потоков, для чего предназначен вид usize
, как обсуждалось в разделе "Целочисленные виды" главы 3.
Давайте проверим код ещё раз:
-$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
- --> src/main.rs:17:14
- |
-17 | pool.execute(|| {
- | -----^^^^^^^ method not found in `ThreadPool`
-
-For more information about this error, try `rustc --explain E0599`.
-error: could not compile `hello` (bin "hello") due to 1 previous error
-
-Теперь мы ошибка возникает из-за того, что у нас нет способа execute
в устройстве ThreadPool
. Вспомните раздел "Создание конечного числа потоков", в котором мы решили, что наш объединениепотоков должен иметь внешнюю оболочку, похожий на thread::spawn
. Кроме того, мы выполняем функцию execute
, чтобы она принимала замыкание и передавала его свободному потоку из объединения для запуска.
Мы определим способ execute
у ThreadPool
, принимающий замыкание в качестве свойства. Вспомните из раздела "Перемещение захваченных значений из замыканий и особенности Fn
" главы 13 сведения о том, что мы можем принимать замыкания в качестве свойств тремя различными особенностями: Fn
, FnMut
и FnOnce
. Нам нужно решить, какой вид замыкания использовать здесь. Мы знаем, что в конечном счёте мы сделаем что-то похожее на выполнение встроенной библиотеки thread::spawn
, поэтому мы можем посмотреть, какие ограничения накладывает на свой свойство ярлык функции thread::spawn
. Документация показывает следующее:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
- where
- F: FnOnce() -> T,
- F: Send + 'static,
- T: Send + 'static,
-Свойство вида F
- это как раз то, что нас важно; свойство вида T
относится к возвращаемому значению и нам он не важен. Можно увидеть, что spawn
использует FnOnce
в качестве ограничения особенности у F
. Возможно это как раз то, чего мы хотим, так как в конечном итоге мы передадим полученный в execute
переменная в функцию spawn
. Дополнительную уверенность в том, что FnOnce
- это именно тот особенность, который мы хотим использовать, нам даётобстоятельство, что поток для выполнения запроса будет выполнять замыкание этого запроса только один раз, что соответствует части Once
("единожды") в названии особенности FnOnce
.
Свойство вида F
также имеет ограничение особенности Send
и ограничение времени жизни 'static
, которые полезны в нашей случаи: нам нужен Send
для передачи замыкания из одного потока в другой и 'static
, потому что мы не знаем, сколько времени поток будет выполняться. Давайте создадим способ execute
для ThreadPool
, который будет принимать обобщённый свойство вида F
со следующими ограничениями:
Файл: src/lib.rs
-pub struct ThreadPool;
-
-impl ThreadPool {
- // --snip--
- pub fn new(size: usize) -> ThreadPool {
- ThreadPool
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
-Мы по-прежнему используем ()
после FnOnce
потому что особенность FnOnce
представляет замыкание, которое не принимает свойств и возвращает единичный вид ()
. Также как и при определении функций, вид возвращаемого значения в ярлыке может быть опущен, но даже если у нас нет свойств, нам все равно нужны скобки.
Опять же, это самая простая выполнение способа execute
: она ничего не делает, мы просто пытаемся сделать код собираемым. Давайте проверим снова:
$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
- Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
-
-Сейчас мы получаем только предупреждения, что означает, что код собирается! Но обратите внимание, если вы попробуете cargo run
и сделаете запрос в браузере, вы увидите ошибки в браузере, которые мы видели в начале главы. Наша библиотека на самом деле ещё не вызывает замыкание, переданное в execute
!
--Примечание: вы возможно слышали высказывание о языках со строгими сборщиками, таких как Haskell и Rust, которое звучит так: «Если код собирается, то он работает». Но это высказывание не всегда верно. Наш дело собирается, но абсолютно ничего не делает! Если бы мы создавали существующий, законченный дело, это был бы хороший мгновение начать писать состоящие из звеньев проверки, чтобы проверять, что код собирается и имеет желаемое поведение.
-
new
Мы ничего не делаем с свойствами new
и execute
. Давайте выполняем тела этих функций с нужным нам поведением. Для начала давайте подумаем о new
. Ранее мы выбрали беззнаковый вид для свойства size
, потому что объединениес отрицательным числом потоков не имеет смысла. Объединение с нулём потоков также не имеет смысла, однако ноль - это вполне допустимое значение usize
. Мы добавим код для проверки того, что size
больше нуля, прежде чем вернуть образец ThreadPool
, и заставим программу паниковать, если она получит ноль, используя макрос assert!
, как показано в приложении 20-13.
Файл: src/lib.rs
-pub struct ThreadPool;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- ThreadPool
- }
-
- // --snip--
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
--
Мы добавили немного документации для нашей устройства ThreadPool
с помощью примечаниев. Обратите внимание, что мы следовали хорошим применением документирования, добавив раздел, в котором указывается случаей, при которой функция может со сбоем завершаться, как это обсуждалось в главе 14. Попробуйте запустить cargo doc --open
и кликнуть на устройство ThreadPool
, чтобы увидеть как выглядит созданная документация для new
!
Вместо добавления макроса assert!
, как мы здесь сделали, мы могли бы преобразовать функцию new
в функцию build
таким образом, чтобы она возвращала Result
, подобно тому, как мы делали в функции Config::new
дела ввода/вывода в приложении 12-9. Но в данном случае мы решили, что попытка создания объединения потоков без указания хотя бы одного потока должна быть непоправимой ошибкой. Если вы чувствуете такое стремление, попробуйте написать функцию build
с ярлыком ниже, для сравнения с функцией new
:
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
-Теперь, имея возможность удостовериться, что количество потоков для хранения в объединении соответствует требованиям, мы можем создавать эти потоки и сохранять их в устройстве ThreadPool
перед тем как возвратить её. Но как мы "сохраним" поток? Давайте ещё раз посмотрим на ярлык thread::spawn
:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
- where
- F: FnOnce() -> T,
- F: Send + 'static,
- T: Send + 'static,
-Функция spawn
возвращает вид JoinHandle<T>
, где T
является видом, который возвращает замыкание. Давайте попробуем использовать JoinHandle
и посмотрим, что произойдёт. В нашем случае замыкания, которые мы передаём объединению потоков, будут обрабатывать соединение и не будут возвращать ничего, поэтому T
будет единичным (unit) видом ()
.
Код в приложении 20-14 собирается, но пока не создаст ни одного потока. Мы изменили определение ThreadPool
так, чтобы он содержал вектор образцов thread::JoinHandle<()>
, объявляли вектор ёмкостью size
, установили цикл for
, который будет выполнять некоторый код для создания потоков, и вернули образец ThreadPool
, содержащий их.
Файл: src/lib.rs
-use std::thread;
-
-pub struct ThreadPool {
- threads: Vec<thread::JoinHandle<()>>,
-}
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let mut threads = Vec::with_capacity(size);
-
- for _ in 0..size {
- // create some threads and store them in the vector
- }
-
- ThreadPool { threads }
- }
- // --snip--
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
--
Мы включили std::thread
в область видимости библиотечного ящика, потому что мы используем thread::JoinHandle
в качестве вида элементов вектора в ThreadPool
.
После получения правильного значения size, наш ThreadPool
создаёт новый вектор, который может содержать size
элементов. Функция with_capacity
выполняет ту же задачу, что и Vec::new
, но с важным отличием: она заранее выделяет необходимый объём памяти в векторе. Поскольку мы знаем, что нам нужно хранить size
элементов в векторе, предварительное выделение памяти для этих элементов будет немного более эффективным, чем использование Vec::new
, при котором размер вектора будет увеличиваться по мере вставки элементов.
Если вы снова запустите приказ cargo check
, она должна завершиться успешно.
Worker
, ответственная за отправку кода из ThreadPool
в потокМы целенаправленно оставили примечание в цикле for
в Приложении 20-14 по поводу создания потоков. Сейчас мы разберёмся, как на самом деле создаются потоки. Обычная библиотека предоставляет thread::spawn
для создания потоков, причём thread::spawn
ожидает получить некоторый код, который поток должен выполнить, как только он будет создан. Однако в нашем случае мы хотим создавать потоки и заставлять их ожидать код, который мы будем передавать им позже. Выполнение потоков в встроенной библиотеке не предоставляет никакого способа сделать это, мы должны выполнить это вручную.
Мы будем выполнить это поведение, добавив новую устройство данных между ThreadPool
и потоками, которая будет управлять этим новым поведением. Мы назовём эту устройство Worker
("работник"), это общепринятое имя в выполнения объединений. Работник берёт код, который нужно выполнить, и запускает этот код внутри рабочего потока. Представьте людей, работающих на кухне ресторана: работники ожидают, пока не поступят заказы от клиентов, а затем они несут ответственность за принятие этих заказов и их выполнение.
Вместо того чтобы хранить вектор образцов JoinHandle<()>
в объединении потоков, мы будем хранить образцы устройства Worker
. Каждый Worker
будет хранить один образец JoinHandle<()>
. Затем мы выполняем способ у Worker
, который будет принимать замыкание и отправлять его в существующий поток для выполнения. Для того чтобы мы могли различать работники в объединении при логировании или отладке, мы также присвоим каждому работнику id
.
Вот как выглядит новая последовательность действий, которые будут происходить при создании ThreadPool
. Мы выполняем код, который будет отправлять замыкание в поток, после того, как у нас будет Worker
, заданный следующим образом:
Worker
, которая содержит id
и JoinHandle<()>
.ThreadPool
, чтобы он содержал вектор образцов Worker
.Worker::new
, которая принимает номер id
и возвращает образец Worker
, который содержит id
и поток, порождённый с пустым замыканием.ThreadPool::new
используем счётчик цикла for
для создания id
, создаём новый Worker
с этим id
и сохраняем образец "работника" в вектор.Если вы готовы принять вызов, попробуйте выполнить эти изменения самостоятельно, не глядя на код в приложении 20-15.
-Готовы? Вот приложение 20-15 с одним из способов сделать указанные ранее изменения.
-Файл: src/lib.rs
-use std::thread;
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
-}
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id));
- }
-
- ThreadPool { workers }
- }
- // --snip--
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize) -> Worker {
- let thread = thread::spawn(|| {});
-
- Worker { id, thread }
- }
-}
--
Мы изменили название поля в ThreadPool
с threads
на workers
, поскольку теперь оно содержит образцы Worker
вместо образцов JoinHandle<()>
. Мы используем счётчик в цикле for
для передачи цифрового определителя в качестве переменной Worker::new
, и сохраняем каждый новый Worker
в векторе с именем workers
.
Внешний код (вроде нашего сервера в src/bin/main.rs) не обязательно должен знать подробности выполнения, касающиеся использования устройства Worker
внутри ThreadPool
, поэтому мы делаем устройство Worker
и её функцию new
закрытыми. Функция Worker::new
использует заданный нами id
и сохраняет образец JoinHandle<()>
, который создаётся при порождении нового потока с пустым замыканием.
--Примечание: Если операционная система не может создать поток из-за нехватки системных ресурсов,
-thread::spawn
со сбоем завершится. Это приведёт к со сбоемму завершению нашего сервера целиком, даже если некоторые потоки были созданы успешно. Для простоты будем считать, что нас устраивает такое поведение, но в существующей выполнения объединения потоков вы, вероятно, захотите использоватьstd::thread::Builder
и его способspawn
, который вместо этого возвращаетResult
.
Этот код собирается и будет хранить количество образцов Worker
, которое мы указали в качестве переменной функции ThreadPool::new
. Но мы всё ещё не обрабатываем замыкание, которое мы получаем в способе execute
. Давайте посмотрим, как это сделать далее.
Следующая неполадка, с которой мы будем бороться, заключается в том, что замыкания, переданные в thread::spawn
абсолютно ничего не делают. Сейчас мы получаем замыкание, которое хотим выполнить, в способе execute
. Но мы должны передать какое-то замыкание в способ thread::spawn
, при создании каждого Worker
во время создания ThreadPool
.
Мы хотим, чтобы вновь созданные устройства Worker
извлекали код для запуска из очереди, хранящейся в ThreadPool
и отправляли этот код в свой поток для выполнения.
потоки (channels), простой способ связи между двумя потоками, с которыми мы познакомились в главе 16, кажется наилучше подойдут для этого сценария. Мы будем использовать поток в качестве очереди заданий, а приказ execute
отправит задание из ThreadPool
образцам Worker
, которые будут отправлять задание в свой поток. Расчет таков:
ThreadPool
создаст поток и будет хранить отправитель.Worker
будет хранить приёмник.Job
, которая будет хранить замыкания, которые мы хотим отправить в поток.execute
отправит задание, которое он хочет выполнить, в отправляющую сторону потока.Worker
будет замкнуто опрашивать принимающую сторону потока и выполнять замыкание любого задания, которое он получит.Давайте начнём с создания потока в ThreadPool::new
и удержания отправляющей стороны в образце ThreadPool
, как показано в приложении 20-16. В устройстве Job
сейчас ничего не содержится, но это будет вид элемента, который мы отправляем в поток.
Файл: src/lib.rs
-use std::{sync::mpsc, thread};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-struct Job;
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id));
- }
-
- ThreadPool { workers, sender }
- }
- // --snip--
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize) -> Worker {
- let thread = thread::spawn(|| {});
-
- Worker { id, thread }
- }
-}
--
В ThreadPool::new
мы создаём наш новый поток и сохраняем в объединении его отправляющую сторону. Код успешно собирается.
Давайте попробуем передавать принимающую сторону потока каждому "работнику" (устройстве Worker), когда объединениепотоков создаёт поток. Мы знаем, что хотим использовать получающую часть потока в потоке, порождаемым "работником", поэтому мы будем ссылаться на свойство receiver
в замыкании. Код 20-17 пока не собирается.
Файл: src/lib.rs
-use std::{sync::mpsc, thread};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-struct Job;
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, receiver));
- }
-
- ThreadPool { workers, sender }
- }
- // --snip--
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
-
-// --snip--
-
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
- let thread = thread::spawn(|| {
- receiver;
- });
-
- Worker { id, thread }
- }
-}
--
Мы внесли несколько небольших и простых изменений: мы передаём принимающую часть потока в Worker::new
, а затем используем его внутри замыкания.
При попытке проверить код, мы получаем ошибку:
-$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0382]: use of moved value: `receiver`
- --> src/lib.rs:26:42
- |
-21 | let (sender, receiver) = mpsc::channel();
- | -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
-...
-25 | for id in 0..size {
- | ----------------- inside of this loop
-26 | workers.push(Worker::new(id, receiver));
- | ^^^^^^^^ value moved here, in previous iteration of loop
- |
-note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
- --> src/lib.rs:47:33
- |
-47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
- | --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
-help: consider moving the expression out of the loop so it is only moved once
- |
-25 ~ let mut value = Worker::new(id, receiver);
-26 ~ for id in 0..size {
-27 ~ workers.push(value);
- |
-
-For more information about this error, try `rustc --explain E0382`.
-error: could not compile `hello` (lib) due to 1 previous error
-
-Код пытается передать receiver
нескольким образцам Worker
. Это не сработает, поскольку, как вы можете помнить из главы 16: выполнение потока, которую предоставляет Ржавчина - несколько производителей, один потребитель. Это означает, что мы не можем просто клонировать принимающую сторону потока, чтобы исправить этот код. Кроме этого, мы не хотим отправлять одно и то же сообщение нескольким потребителям, поэтому нам нужен единый список сообщений для множества обработчиков, чтобы каждое сообщение обрабатывалось лишь один раз.
Кроме того, удаление задачи из очереди потока включает изменение receiver
, поэтому потокам необходим безопасный способ делиться и изменять receiver
, в противном случае мы можем получить условия гонки (как описано в главе 16).
Вспомните умные указатели, которые обсуждались в главе 16: чтобы делиться владением между несколькими потоками и разрешать потокам изменять значение, нам нужно использовать вид Arc<Mutex<T>>
. Вид Arc
позволит нескольким "работникам" владеть получателем (receiver), а Mutex
заверяет что только один "работник" сможет получить задание (job) от получателя за раз. Приложение 20-18 показывает изменения, которые мы должны сделать.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-// --snip--
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-struct Job;
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- // --snip--
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- }
-}
-
-// --snip--
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- // --snip--
- let thread = thread::spawn(|| {
- receiver;
- });
-
- Worker { id, thread }
- }
-}
--
В ThreadPool::new
мы помещаем принимающую сторону потока внутрь Arc
и Mutex
. Для каждого нового "работника" мы клонируем Arc
, чтобы увеличить счётчик ссылок так, что "работники" могут разделять владение принимающей стороной потока.
С этими изменениями код собирается! Мы подбираемся к цели!
-execute
Давайте выполняем наконец способ execute
у устройства ThreadPool
. Мы также изменим вид Job
со устройства на псевдоним вида для особенность-предмета. который будет содержать вид замыкания, принимаемый способом execute
. Как описано в разделе "Создание родственных вида с помощью псевдонимов типа" главы 19, псевдонимы видов позволяют делать длинные виды короче, облегчая их использование. Посмотрите на приложение 20-19.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-// --snip--
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- // --snip--
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-// --snip--
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(|| {
- receiver;
- });
-
- Worker { id, thread }
- }
-}
--
После создания нового образца Job
с замыканием, полученным в execute
, мы посылаем его через отправляющий конец потока. На тот случай, если отправка не удастся, вызываем unwrap
у send
. Это может произойти, например, если мы остановим выполнение всех наших потоков, что означает, что принимающая сторона прекратила получать новые сообщения. На данный мгновение мы не можем остановить выполнение наших потоков: наши потоки будут исполняться до тех пор, пока существует объединение Причина, по которой мы используем unwrap
, заключается в том, что, хотя мы знаем, что сбой не произойдёт, сборщик этого не знает.
Но мы ещё не закончили! В "работнике" (worker) наше замыкание, переданное в thread::spawn
все ещё ссылается только на принимающую сторону потока. Вместо этого нам нужно, чтобы замыкание работало в бесконечном цикле, запрашивая задание у принимающей части потока и выполняя задание, когда оно принято. Давайте внесём изменения, показанные в приложении 20-20 внутри Worker::new
.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-// --snip--
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker { id, thread }
- }
-}
--
Здесь мы сначала вызываем lock
у receiver
, чтобы получить мьютекс, а затем вызываем unwrap
, чтобы со сбоем завершить работу при любых ошибках. Захват блокировки может завершиться неудачей, если мьютекс находится в отравленном состоянии (poisoned state), что может произойти, если какой-то другой поток завершился со сбоем, удерживая блокировку, вместо снятия блокировки. В этой случаи вызвать unwrap
для со сбоемго завершения потока вполне оправдано. Не стесняйтесь заменить unwrap
на expect
с сообщением об ошибке, которое имеет для вас значение.
Если мы получили блокировку мьютекса, мы вызываем recv
, чтобы получить Job
из потока. Последний вызов unwrap
позволяет миновать любые ошибки, которые могут возникнуть, если поток, управляющий отправитель, прекратил исполняться, подобно тому, как способ send
возвращает Err
, если получатель не принимает сообщение.
Вызов recv
- блокирующий, поэтому пока задач нет, текущий поток будет ждать, пока задача не появится. Mutex<T>
заверяет, что только один поток Worker
за раз попытается запросить задачу.
Наш объединениепотоков теперь находится в рабочем состоянии! Выполните cargo run
и сделайте несколько запросов:
$ cargo run
- Compiling hello v0.1.0 (file:///projects/hello)
-warning: field is never read: `workers`
- --> src/lib.rs:7:5
- |
-7 | workers: Vec<Worker>,
- | ^^^^^^^^^^^^^^^^^^^^
- |
- = note: `#[warn(dead_code)]` on by default
-
-warning: field is never read: `id`
- --> src/lib.rs:48:5
- |
-48 | id: usize,
- | ^^^^^^^^^
-
-warning: field is never read: `thread`
- --> src/lib.rs:49:5
- |
-49 | thread: thread::JoinHandle<()>,
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-warning: `hello` (lib) generated 3 warnings
- Finished dev [unoptimized + debuginfo] target(s) in 1.40s
- Running `target/debug/hello`
-Worker 0 got a job; executing.
-Worker 2 got a job; executing.
-Worker 1 got a job; executing.
-Worker 3 got a job; executing.
-Worker 0 got a job; executing.
-Worker 2 got a job; executing.
-Worker 1 got a job; executing.
-Worker 3 got a job; executing.
-Worker 0 got a job; executing.
-Worker 2 got a job; executing.
-
-Успех! Теперь у нас есть объединениепотоков, который обрабатывает соединения не согласованно. Никогда не создаётся более четырёх потоков, поэтому наша система не будет перегружена, если сервер получит много запросов. Если мы отправим запрос ресурса /sleep, сервер сможет обслуживать другие запросы, обрабатывая их в другом потоке.
---Примечание: если вы запросите /sleep в нескольких окнах браузера одновременно, они могут загружаться по одному, с интервалами в 5 секунд. Некоторые веб-браузеры выполняют несколько образцов одного и того же запроса последовательно из-за кэширования. Такое ограничение не связано с работой нашего веб-сервера.
-
После изучения цикла while let
в главе 18 вы можете удивиться, почему мы не написали код рабочего потока (worker thread), как показано в приложении 20-22.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-// --snip--
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || {
- while let Ok(job) = receiver.lock().unwrap().recv() {
- println!("Worker {id} got a job; executing.");
-
- job();
- }
- });
-
- Worker { id, thread }
- }
-}
--
Этот код собирается и запускается, но не даёт желаемого поведения: медленный запрос всё равно приведёт к тому, что другие запросы будут ждать обработки. Причина здесь несколько тоньше: устройства Mutex
не имеет открытого способа unlock
, так как владение блокировкой основано на времени жизни MutexGuard<T>
внутри LockResult<MutexGuard<T>>
, которое возвращает способ lock
. Во время сборки анализатор заимствований может проследить за выполнением правила, согласно которому к ресурсу, охраняемому Mutex
, нельзя получить доступ пока мы удерживаем блокировку. Однако в этой выполнение мы также можем получить случай, когда блокировка будет удерживаться дольше, чем предполагалось, если мы не будем внимательно учитывать время жизни MutexGuard<T>
.
Код в приложении 20-20, использующий let job = receiver.lock().unwrap().recv().unwrap();
работает, потому что при использовании let
любые промежуточные значения, используемые в выражении справа от знака равенства, немедленно уничтожаются после завершения указания let
. Однако while let
(и if let
и match
) не удаляет временные значения до конца связанного раздела. Таким образом, в приложении 20-21 блокировка не снимается в течение всего времени вызова job()
, что означает, что другие работники не могут получать задания.
Приложение 20-20 не согласованно отвечает на запросы с помощью использования объединения потоков, как мы и хотели. Мы получаем некоторые предупреждения про workers
, id
и поля thread
, которые мы не используем напрямую, что напоминает нам о том, что мы не освобождаем все ресурсы. Когда мы используем менее элегантный способ остановки основного потока клавишной сочетанием ctrl-c, все остальные потоки также немедленно останавливаются, даже если они находятся в середине обработки запроса.
Далее, выполняем особенность Drop
для вызова join
у каждого потока в объединении, чтобы они могли завершить запросы, над которыми они работают, перед закрытием. Затем мы выполняем способ сообщить потокам, что они должны перестать принимать новые запросы и завершить работу. Чтобы увидеть этот код в действии, мы изменим наш сервер так, чтобы он принимал только два запроса, после чего правильно завершал работу объединения потоков.
Drop
для ThreadPool
Давайте начнём с выполнения Drop
у нашего объединения потоков. Когда объединениеудаляется, все наши потоки должны объединиться (join), чтобы убедиться, что они завершают свою работу. В приложении 20-22 показана первая попытка выполнения Drop
, код пока не будет работать.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- worker.thread.join().unwrap();
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: thread::JoinHandle<()>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker { id, thread }
- }
-}
--
Сначала мы пройдёмся по каждому worker
из объединения потоков. Для этого мы используем &mut
с self
, потому что нам нужно иметь возможность изменять worker
. Для каждого обработчика мы выводим сообщение о том, что он завершает работу, а затем вызываем join
у потока этого обработчика. Для случаев, когда вызов join
не удался, мы используем unwrap
, чтобы заставить Ржавчина запаниковать и перейти в режим грубого завершения работы.
Ошибка получаемая при сборки этого кода:
-$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
- --> src/lib.rs:52:13
- |
-52 | worker.thread.join().unwrap();
- | ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
- | |
- | move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
- |
-note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
- --> /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/thread/mod.rs:1718:17
-
-For more information about this error, try `rustc --explain E0507`.
-error: could not compile `hello` (lib) due to 1 previous error
-
-Ошибка говорит нам, что мы не можем вызвать join
, потому что у нас есть только изменяемое заимствование каждого worker
, а join
забирает во владение свой переменная. Чтобы решить эту неполадку, нам нужно извлечь поток из образца Worker
, который владеет thread
, чтобы join
мог его использовать. Мы сделали это в приложении 17-15: теперь, когда Worker
хранит в себе Option<thread::JoinHandle<()>>
, мы можем воспользоваться способом take
у Option
, чтобы извлечь значение из исхода Some
, тем самым оставляя на его месте None
. Другими словами, в рабочем состоянии Worker
будет использовать исход Some
содержащий thread
, а когда мы захотим завершить Worker
, мы заменим Some
на None
, чтобы у Worker
не было потока для работы.
Итак, мы хотим обновить объявление Worker
следующим образом:
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- worker.thread.join().unwrap();
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker { id, thread }
- }
-}
-Теперь давайте опираться на сборщик, чтобы найти другие места, которые нужно изменить. Проверяя код, мы получаем две ошибки:
-$ cargo check
- Checking hello v0.1.0 (file:///projects/hello)
-error[E0599]: no method named `join` found for enum `Option` in the current scope
- --> src/lib.rs:52:27
- |
-52 | worker.thread.join().unwrap();
- | ^^^^ method not found in `Option<JoinHandle<()>>`
- |
-note: the method `join` exists on the type `JoinHandle<()>`
- --> /rustc/129f3b9964af4d4a709d1383930ade12dfe7c081/library/std/src/thread/mod.rs:1718:5
-help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None`
- |
-52 | worker.thread.expect("REASON").join().unwrap();
- | +++++++++++++++++
-
-error[E0308]: mismatched types
- --> src/lib.rs:72:22
- |
-72 | Worker { id, thread }
- | ^^^^^^ expected `Option<JoinHandle<()>>`, found `JoinHandle<_>`
- |
- = note: expected enum `Option<JoinHandle<()>>`
- found struct `JoinHandle<_>`
-help: try wrapping the expression in `Some`
- |
-72 | Worker { id, thread: Some(thread) }
- | +++++++++++++ +
-
-Some errors have detailed explanations: E0308, E0599.
-For more information about an error, try `rustc --explain E0308`.
-error: could not compile `hello` (lib) due to 2 previous errors
-
-Давайте обратимся ко второй ошибке, которая указывает на код в конце Worker::new
; нам нужно обернуть значение thread
в исход Some
при создании нового Worker
. Внесите следующие изменения, чтобы исправить эту ошибку:
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- worker.thread.join().unwrap();
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- // --snip--
-
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker {
- id,
- thread: Some(thread),
- }
- }
-}
-Первая ошибка находится в нашей выполнения Drop
. Ранее мы упоминали, что намеревались вызвать take
для свойства Option
, чтобы забрать thread
из этапа worker
. Следующие изменения делают это:
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: mpsc::Sender<Job>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool { workers, sender }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- if let Some(thread) = worker.thread.take() {
- thread.join().unwrap();
- }
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker {
- id,
- thread: Some(thread),
- }
- }
-}
-Как уже говорилось в главе 17, способ take
у вида Option
забирает значение из исхода Some
и оставляет исход None
в этом месте. Мы используем if let
, чтобы разъединять Some
и получить поток; затем вызываем join
у потока. Если поток "работника" уже None
, мы знаем, что этот "работник" уже очистил свой поток, поэтому в этом случае ничего не происходит.
Теперь, после всех внесённых нами изменений, код собирается без каких-либо предупреждений. Но плохая новость в том, что этот код всё ещё не работает так, как мы этого хотим. Причина заключается в логике замыканий, запускаемых потоками образцов Worker: в данный мгновение мы вызываем join, но это не приводит к завершению потоков, так как они находятся в бесконечном цикле, ожидая новую задачу. Если мы попытаемся удалить ThreadPool в текущей выполнения drop, основной поток навсегда заблокируется в ожидании завершения первого потока из объединения .
-Чтобы решить эту неполадку, нам нужно будет изменить выполнение drop
в ThreadPool
, а затем внести изменения в цикл Worker
.
Во-первых, изменим выполнение drop
ThreadPool
таким образом, чтобы явно удалять sender
перед тем, как начнём ожидать завершения потоков. В приложении 20-23 показаны изменения в ThreadPool
для явного удаления sender
. Мы используем ту же технику Option
и take
, что и с потоком, чтобы переместить sender
из ThreadPool
:
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: Option<mpsc::Sender<Job>>,
-}
-// --snip--
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- // --snip--
-
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool {
- workers,
- sender: Some(sender),
- }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.as_ref().unwrap().send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- drop(self.sender.take());
-
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- if let Some(thread) = worker.thread.take() {
- thread.join().unwrap();
- }
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let job = receiver.lock().unwrap().recv().unwrap();
-
- println!("Worker {id} got a job; executing.");
-
- job();
- });
-
- Worker {
- id,
- thread: Some(thread),
- }
- }
-}
--
Удаление sender
закрывает поток, что указывает на то, что сообщения больше не будут отправляться. Когда это произойдёт, все вызовы recv
, выполняемые рабочими этапами в бесконечном цикле, вернут ошибку. В приложении 20-24 мы меняем цикл Worker
для правильного выхода из него в этом случае, что означает, что потоки завершатся, когда выполнение drop
ThreadPool
вызовет для них join
.
Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: Option<mpsc::Sender<Job>>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool {
- workers,
- sender: Some(sender),
- }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.as_ref().unwrap().send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- drop(self.sender.take());
-
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- if let Some(thread) = worker.thread.take() {
- thread.join().unwrap();
- }
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let message = receiver.lock().unwrap().recv();
-
- match message {
- Ok(job) => {
- println!("Worker {id} got a job; executing.");
-
- job();
- }
- Err(_) => {
- println!("Worker {id} disconnected; shutting down.");
- break;
- }
- }
- });
-
- Worker {
- id,
- thread: Some(thread),
- }
- }
-}
--
Чтобы увидеть этот код в действии, давайте изменим main
, чтобы принимать только два запроса, прежде чем правильно завершить работу сервера как показано в приложении 20-25.
Файл: src/main.rs
-use hello::ThreadPool;
-use std::{
- fs,
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- thread,
- time::Duration,
-};
-
-fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- let pool = ThreadPool::new(4);
-
- for stream in listener.incoming().take(2) {
- let stream = stream.unwrap();
-
- pool.execute(|| {
- handle_connection(stream);
- });
- }
-
- println!("Shutting down.");
-}
-
-fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let request_line = buf_reader.lines().next().unwrap().unwrap();
-
- let (status_line, filename) = match &request_line[..] {
- "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
- "GET /sleep HTTP/1.1" => {
- thread::sleep(Duration::from_secs(5));
- ("HTTP/1.1 200 OK", "hello.html")
- }
- _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
- };
-
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
-
- let response =
- format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
-
- stream.write_all(response.as_bytes()).unwrap();
-}
--
Вы бы не хотели, чтобы существующий веб-сервер отключался после обслуживания только двух запросов. Этот код всего лишь отображает, что правильное завершение работы и освобождение ресурсов находятся в рабочем состоянии.
-Способ take
определён в особенности Iterator
и ограничивает повторение самое большее первыми двумя элементами. ThreadPool
выйдет из области видимости в конце main
и будет запущена его выполнение drop
.
Запустите сервер с cargo run
и сделайте три запроса. Третий запрос должен выдать ошибку и в окне вызова вы должны увидеть вывод, подобный следующему:
$ cargo run
- Compiling hello v0.1.0 (file:///projects/hello)
- Finished dev [unoptimized + debuginfo] target(s) in 1.0s
- Running `target/debug/hello`
-Worker 0 got a job; executing.
-Shutting down.
-Shutting down worker 0
-Worker 3 got a job; executing.
-Worker 1 disconnected; shutting down.
-Worker 2 disconnected; shutting down.
-Worker 3 disconnected; shutting down.
-Worker 0 disconnected; shutting down.
-Shutting down worker 1
-Shutting down worker 2
-Shutting down worker 3
-
-Вы возможно увидите другой порядок рабочих потоков и напечатанных сообщений. Мы можем увидеть, как этот код работает по сообщениям: "работники" номер 0 и 3 получили первые два запроса. Сервер прекратил принимать соединения после второго подключения, а выполнение Drop
для ThreadPool
начинает выполняется ещё тогда, когда как работник 3 даже не приступил к выполнению своей работы. Удаление sender
отключает все рабочие потоки от потока и просит их завершить работу. Каждый рабочий поток при отключении печатает сообщение, а затем объединениепотоков вызывает join
, чтобы дождаться, пока каждый из рабочих потоков завершится.
Обратите внимание на один важная особенность этого определенного запуска: ThreadPool удалил sender
, и прежде чем какой-либо из работников получил ошибку, мы попытались присоединить (join) рабочий поток с номером 0. Рабочий поток 0 ещё не получил ошибку от recv
, поэтому основной поток заблокировался, ожидания завершения потока работника 0. Тем временем, работник 3 получил задание, а затем каждый из рабочих потоков получил ошибку. Когда рабочий поток 0 завершился, основной поток ждал окончания завершения выполнения остальных рабочих потоков. В этот мгновение все они вышли из своих циклов и остановились.
Примите поздравления! Теперь мы завершили дело; у нас есть основной веб-сервер, использующий объединениепотоков для не согласованных ответов. Мы можем выполнить правильное завершение работы сервера, очистив все потоки в объединении.
-Вот полный код для справки:
-Файл: src/main.rs
-use hello::ThreadPool;
-use std::{
- fs,
- io::{prelude::*, BufReader},
- net::{TcpListener, TcpStream},
- thread,
- time::Duration,
-};
-
-fn main() {
- let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
- let pool = ThreadPool::new(4);
-
- for stream in listener.incoming().take(2) {
- let stream = stream.unwrap();
-
- pool.execute(|| {
- handle_connection(stream);
- });
- }
-
- println!("Shutting down.");
-}
-
-fn handle_connection(mut stream: TcpStream) {
- let buf_reader = BufReader::new(&mut stream);
- let request_line = buf_reader.lines().next().unwrap().unwrap();
-
- let (status_line, filename) = match &request_line[..] {
- "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
- "GET /sleep HTTP/1.1" => {
- thread::sleep(Duration::from_secs(5));
- ("HTTP/1.1 200 OK", "hello.html")
- }
- _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
- };
-
- let contents = fs::read_to_string(filename).unwrap();
- let length = contents.len();
-
- let response =
- format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
-
- stream.write_all(response.as_bytes()).unwrap();
-}
-Файл: src/lib.rs
-use std::{
- sync::{mpsc, Arc, Mutex},
- thread,
-};
-
-pub struct ThreadPool {
- workers: Vec<Worker>,
- sender: Option<mpsc::Sender<Job>>,
-}
-
-type Job = Box<dyn FnOnce() + Send + 'static>;
-
-impl ThreadPool {
- /// Create a new ThreadPool.
- ///
- /// The size is the number of threads in the pool.
- ///
- /// # Panics
- ///
- /// The `new` function will panic if the size is zero.
- pub fn new(size: usize) -> ThreadPool {
- assert!(size > 0);
-
- let (sender, receiver) = mpsc::channel();
-
- let receiver = Arc::new(Mutex::new(receiver));
-
- let mut workers = Vec::with_capacity(size);
-
- for id in 0..size {
- workers.push(Worker::new(id, Arc::clone(&receiver)));
- }
-
- ThreadPool {
- workers,
- sender: Some(sender),
- }
- }
-
- pub fn execute<F>(&self, f: F)
- where
- F: FnOnce() + Send + 'static,
- {
- let job = Box::new(f);
-
- self.sender.as_ref().unwrap().send(job).unwrap();
- }
-}
-
-impl Drop for ThreadPool {
- fn drop(&mut self) {
- drop(self.sender.take());
-
- for worker in &mut self.workers {
- println!("Shutting down worker {}", worker.id);
-
- if let Some(thread) = worker.thread.take() {
- thread.join().unwrap();
- }
- }
- }
-}
-
-struct Worker {
- id: usize,
- thread: Option<thread::JoinHandle<()>>,
-}
-
-impl Worker {
- fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
- let thread = thread::spawn(move || loop {
- let message = receiver.lock().unwrap().recv();
-
- match message {
- Ok(job) => {
- println!("Worker {id} got a job; executing.");
-
- job();
- }
- Err(_) => {
- println!("Worker {id} disconnected; shutting down.");
- break;
- }
- }
- });
-
- Worker {
- id,
- thread: Some(thread),
- }
- }
-}
-Мы могли бы сделать ещё больше! Если вы хотите продолжить совершенствование этого дела, вот несколько мыслей:
-ThreadPool
и его открытые способы.unwrap
на более устойчивую обработку ошибок.ThreadPool
для выполнения некоторых других задач, помимо обслуживания веб-запросов.Отличная работа! Вы сделали это к концу книги! Мы хотим поблагодарить вас за то, что присоединились к нам в этом путешествии по языку Rust. Теперь вы готовы выполнить свои собственные дела на Ржавчина и помочь с делами другим людям. Имейте в виду, что сообщество Ржавчина разработчиков довольно гостеприимно, они с удовольствием постараются помочь вам с любыми трудностями, с которыми вы можете столкнуться в своём путешествии по Rust.
- -Не всегда было ясно, но язык программирования Ржавчина в основном посвящён расширению возможностей: независимо от того, какой код вы пишете сейчас, Ржавчина позволяет вам достичь большего, чтобы программировать уверенно в более широком ряде областей, чем вы делали раньше.
-Возьмём, к примеру, работу «системного уровня», которая касается низкоуровневых подробностей управления памятью, представления данных и многопоточности. Привычно эта область программирования считается загадочной, доступной лишь немногим избранным, посвятившим долгие годы изучению всех её печально известных подводных камней. И даже те, кто опытют это, делают всё с осторожностью, чтобы их код не был уязвим для уязвимостей, сбоев или повреждений.
-Rust разрушает эти преграды, устраняя старые подводные камни и предоставляя дружелюбный, отполированный набор средств, который поможет вам на этом пути. Программисты, которым необходимо «погрузиться» в низкоуровневое управление, могут сделать это с помощью Rust, не беря на себя привычный риск сбоев или дыр в безопасности и не изучая тонкости изменчивых наборов средств. Более того, язык предназначен для того, чтобы легко вести вас к надёжному коду, который эффективен с точки зрения скорости и использования памяти.
-Программисты, которые уже работают с низкоуровневым кодом, могут использовать Ржавчина для повышения своих чувства собственной значимости. Например, внедрение одновременности в Ржавчина является действием с относительно низким риском: сборщик поймает для вас привычные ошибки. И вы можете заняться более враждебной переработкой в своём коде с уверенностью, что не будете случайно добавлять в код сбои или уязвимости.
-Но Ржавчина не ограничивается низкоуровневым системным программированием. Он достаточно выразителен и удобен, чтобы приложения CLI (Command Line Interface – окно выводаные программы), веб-серверы и многие другие виды кода были довольно приятными для написания — позже вы найдёте простые примеры того и другого в книге. Работа с Ржавчина позволяет вырабатывать навыки, которые переносятся из одной предметной области в другую; вы можете изучить Rust, написав веб-приложение, а затем применить те же навыки для Raspberry Pi.
-Эта книга полностью раскрывает возможности Ржавчина для расширения возможностей его пользователей. Это дружелюбный и доступный источник, призванный помочь вам повысить уровень не только ваших знаний о Rust, но и ваших возможностей и уверенности как программиста в целом. Так что погружайтесь, готовьтесь учиться и добро пожаловать в сообщество Rust!
-— Nicholas Matsakis и Aaron Turon
- -От Стива Клабника и Кэрол Николс, при поддержке других участников сообщества Rust
-В этой исполнения учебника предполагается, что вы используете Ржавчина 1.67.1 (выпущен 09.02.2023) или новее. См. раздел «Установка» главы 1 для установки или обновления Rust.
-HTML-исполнение книги доступна онлайн по адресам https://doc.rust-lang.org/stable/book/(англ.) и https://doc.rust-lang.ru/book(рус.) и офлайн. При установке Ржавчина с помощью rustup
: просто запустите rustup docs --book
, чтобы её открыть.
Также доступны несколько переводов от сообщества.
-Этот источник доступен в виде печатной книги в мягкой обложке и в виде электронной книги от No Starch Press .
--- -🚨 Предпочитаете более увлекательный этап обучения? Попробуйте другую исполнение Ржавчина Book, в которой есть: проверочные вопросы, цветовое выделение, наглядные визуализации и многое другое: https://rust-book.cs.brown.edu
-