Файл: Язык программирования Rust.pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 11.01.2024

Просмотров: 1156

Скачиваний: 5

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

СОДЕРЖАНИЕ

Обновление и удалениеПосле установки Rust через rustup при выходе новой версии Rust обновление до последней версии не составит труда. В вашем терминале запустите следующий скрипт обновления:Чтобы удалить Rust и rustup, выполните следующую команду:Локальная документацияУстановка Rust также включает в себя локальную копию документации, поэтому вы можете читать её в оффлайн режиме. Запустите rustup doc, чтобы открыть локальную документацию в браузере.Каждый раз, когда есть какой-либо тип или какая-либо функция, предоставляемые стандартной библиотекой, а вы не знаете, что они делают и как их использовать,>echo %PATH% >echo$env:Path echo $PATH $ rustup update $ rustup self uninstall воспользуйтесь документацией по интерфейсу прикладного программирования (API) для поиска! Привет, мир!Итак, когда Rust уже установлен, можно приступать к написанию вашей первой программы. Общая традиция при изучении нового языка программирования - писать маленькую программу, которая печатает в строке вывода "Hello, world!". Давайте сделаем то же самое.Примечание: Эта книга предполагает наличие базового навыка работы с командной строкой. Rust не предъявляет особых требований к тому, каким инструментарием вы пользуетесь для редактирования или хранения вашего кода, поэтому если вы предпочитаете использовать интегрированную среду разработки (IDE) вместо командной строки, смело используйте вашу любимую IDE. Многие IDE сейчас в той или иной степени поддерживают Rust; подробности можно узнать из документации к IDE. Команда Rust сосредоточилась на обеспечении отличной поддержки IDE с помощью rust-analyzer. Более подробную информацию смотрите в ПриложенииD!Создание папки проектаПрежде всего начнём с создания директории, в которой будем сохранять наш код на языке Rust. На самом деле не важно, где сохранять наш код. Однако, для упражнений и проектов, обсуждаемых в данной книге, мы советуем создать директорию projects в вашем домашнем каталоге, там же и хранить в будущем код программ из книги.Откройте терминал и введите следующие команды для того, чтобы создать директориюprojects для хранения кода разных проектов, и, внутри неё, директорию hello_world для проекта “Hello, world!”.Для Linux, macOS и PowerShell на Windows, введите:Для Windows в CMD, введите:Написание и запуск первой Rust программы$ mkdir /projects $cd /projects $ mkdir hello_world $cd hello_world > mkdir "%USERPROFILE%\projects" > cd /d "%USERPROFILE%\projects" > mkdir hello_world > cd hello_world Затем создайте новый исходный файл и назовите его main.rs. Файлы Rust всегда заканчиваются расширением .rs. Если вы используете более одного слова в имени файла,принято разделять их символом подчёркивания. Например, используйте hello_world.rsвместо helloworld.rs.Теперь откроем файл main.rs для редактирования и введём следующие строки кода:Название файла: main.rsЛистинг 1-1: Программа которая печатает Hello, world!Сохраните файл и вернитесь в окно терминала в каталог /projects/hello_world. В Linux или macOS введите следующие команды для компиляции и запуска файла:В Windows, введите команду .\main.exe вместо ./main:Независимо от операционной системы, строка Hello, world! должна напечататься в окне вашего терминала. Если вы не увидели вывода, вернитесь в часть "Решение проблем" "Troubleshooting" раздела "Установка" для получения помощи.Если напечаталось Hello, world!, то примите наши поздравления! Вы написали программу на Rust, что делает вас Rust программистом — добро пожаловать!Анатомия программы на RustДавайте рассмотрим «Hello, world!» программу в деталях. Вот первая часть головоломки:Эти строки определяют функцию с именем main. Функция main особенная: это всегда первый код, который запускается в каждой исполняемой программе Rust. Первая строка объявляет функцию с именем main, которая не имеет параметров и ничего не возвращает. Если бы были параметры, они бы заключались в круглые скобки ()fn main() { println!("Hello, world!"); } $ rustc main.rs $ ./main Hello, world! > rustc main.rs > .\main.exe Hello, world! fn main() { } Тело функции заключено в {}. Rust требует фигурных скобок вокруг всех тел функций.Хороший стиль — поместить открывающую фигурную скобку на ту же строку, что и объявление функции, добавив между ними один пробел.Примечание. Если вы хотите придерживаться стандартного стиля в проектах наRust, вы можете использовать инструмент автоматического форматирования под названием rustfmt для форматирования кода в определённом стиле (подробнее о rustfmt в Приложении D ). ). Команда Rust включила этот инструмент в стандартный дистрибутив Rust, например, rustc , поэтому он уже должен быть установлен на вашем компьютере!Тело функции main содержит следующий код:Эта строка делает всю работу в этой маленькой программе: печатает текст на экран.Можно заметить четыре важных детали.Первая, не столь заметная, - в стиле Rust для отступа используются четыре пробела, а не знак табуляции.Во-вторых, println! вызывает макрос Rust. Если бы вместо этого была вызвана функция,она была бы введена как println (без !). Мы обсудим макросы Rust более подробно вГлаве 19. Сейчас вам просто нужно знать, что использование ! означает, что вы вызываете макрос вместо обычной функции, и что макросы не всегда следуют тем же правилам, что и функции.В-третьих, вы видите строку "Hello, world!". Мы передаём её в качестве аргумента макросу println!, и она выводится на экран.В-четвёртых, мы заканчиваем строку точкой с запятой (;), которая указывает на то, что это выражение закончилось и готово начаться следующее. Большинство строк кода наRust заканчиваются точкой с запятой.Компиляция и выполнение кода являются отдельными шагамиВы только что запустили только что созданную программу, так что давайте рассмотрим каждый шаг в этом процессе.Перед запуском программы на Rust вы должны скомпилировать её с помощью компилятора Rust, введя команду rustc и передав ей имя вашего исходного файла,например:println!("Hello, world!"); Если у вас есть опыт работы с C или C++, вы заметите, что это похоже на gcc или clangПосле успешной компиляции Rust выводит двоичный исполняемый файл.На Linux, macOS и с PowerShell на Windows вы можете увидеть исполняемый файл, введя команду ls в своём терминале. В Linux и macOS вы увидите два файла. С PowerShell вWindows вы увидите те же три файла, что и при использовании CMD.В CMD на Windows следует ввести следующие команды:Это показывает исходный код файла с расширением .rs, исполняемый файл (main.exe наWindows, но main на всех других платформах) и, при использовании Windows, файл,содержащий отладочную информацию с расширением .pdb. Отсюда вы запускаете файлыmain или main.exe, например:Если ваш main.rs — это ваша программа «Hello, world!», эта строка выведет в терминал Hello, world!Если вы лучше знакомы с динамическими языками, такими как Ruby, Python илиJavaScript, возможно, вы не привыкли компилировать и запускать программу как отдельные шаги. Rust — это предварительно скомпилированный язык, то есть вы можете скомпилировать программу и передать исполняемый файл кому-то другому, и он сможет запустить его даже без установленного Rust. Если вы даёте кому-то файл .rb , .py или .js, у него должна быть установлена реализация Ruby, Python или JavaScript (соответственно).Но в этих языках вам нужна только одна команда для компиляции и запуска вашей программы. В дизайне языков программирования всё — компромисс.Компиляция с помощью rustc подходит для простых программ, но по мере роста вашего проекта вы захотите управлять всеми параметрами и упростить передачу кода.Далее мы познакомим вас с инструментом Cargo, который поможет вам писать программы из реального мира на Rust.$ rustc main.rs $ ls main main.rs > dir /B %= the /B option says to only show the file names =% main.exe main.pdb main.rs $ ./main # для Linux> .\main.exe # для Windows Привет, Cargo!Cargo - это система сборки и менеджер пакетов Rust. Большая часть разработчиков используют данный инструмент для управления проектами, потому что Cargo выполняет за вас множество задач, таких как сборка кода, загрузка библиотек, от которых зависит ваш код, и создание этих библиотек. (Мы называем библиотеки, которые нужны вашему коду, зависимостями.)Самые простые программы на Rust, подобные той, которую мы написали, не имеют никаких зависимостей. Если бы мы сделали проект «Hello, world!» с Cargo, он бы использовал только ту часть Cargo, которая отвечает за компиляцию вашего кода. По мере написания более сложных программ на Rust вы будете добавлять зависимости, а если вы начнёте проект с использованием Cargo, добавлять зависимости станет намного проще.Так как большая часть проектов использует Cargo, то остальная часть книги подразумевает, что вы также используете Cargo. Cargo устанавливается вместе с Rust при использовании официальных установщиков обсуждаемых в разделе "Установка Rust"Если вы установили Rust другим способом, то проверьте, работает ли он, введя команду проверки версии Cargo в терминале:Если команда выдала номер версии, то значит Cargo установлен. Если вы видите ошибку,вроде command not found ("команда не найдена"), загляните в документацию для использованного вами способа установки, чтобы выполнить установку Cargo отдельно.Создание проекта с помощью CargoДавайте создадим новый проект с помощью Cargo и посмотрим, как он отличается от нашего начального проекта "Hello, world!". Перейдите обратно в папку projects (или любую другую, где вы решили сохранять код). Затем, в любой операционный системе,запустите команду:Первая команда создаёт новый каталог и проект с именем hello_cargo. Мы назвали наш проект hello_cargo, и Cargo создаёт свои файлы в каталоге с тем же именем.Перейдём в каталог hello_cargo и посмотрим файлы. Увидим, что Cargo сгенерировал два файла и одну директорию: файл Cargo.toml и каталог src с файлом main.rs внутри.Кроме того, cargo инициализировал новый репозиторий Git вместе с файлом .gitignore.Файлы Git не будут сгенерированы, если вы запустите cargo new в существующем$ cargo --version $ cargo new hello_cargo $cd hello_cargo репозитории Git; вы можете изменить это поведение, используя cargo new --vcs=gitПримечание: Git - это распространённая система контроля версий. Вы можете изменить cargo new на использование другой системы контроля версий или не использовать никакой системы контроля версий с помощью флага --vcsВыполните команду cargo new --help, чтобы увидеть доступные опции.Откройте файл Cargo.toml в любом текстовом редакторе. Он должен выглядеть как код в листинге 1-2.Файл: Cargo.tomlЛистинг 1-2: Содержимое файла Cargo.toml, сгенерированного командой cargo newЭто файл в формате TOML (Tom’s Obvious, Minimal Language), который является форматом конфигураций Cargo.Первая строка, [package], является заголовочной секцией, которая указывает что следующие инструкции настраивают пакет. По мере добавления больше информации в данный файл, будет добавляться больше секций и инструкций (строк).Следующие три строки задают информацию о конфигурации, необходимую Cargo для компиляции вашей программы: имя, версию и редакцию Rust, который будет использоваться. Мы поговорим о ключе edition в Приложении EПоследняя строка, [dependencies] является началом секции для списка любых зависимостей вашего проекта. В Rust, это внешние пакеты кода, на которые ссылаются ключевым словом crate. Нам не нужны никакие зависимости в данном проекте, но мы будем использовать их в первом проекте главы 2, так что нам пригодится данная секция зависимостей потом.Откройте файл src/main.rs и загляните в него:Файл : src/main.rs[package]name = "hello_cargo"version = "0.1.0"edition = "2021"[dependencies]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 в папке.Сборка и запуск Cargo проектаПосмотрим, в чем разница при сборке и запуске программы "Hello, world!" с помощьюCargo. В каталоге hello_cargo соберите проекта следующей командой:Эта команда создаёт исполняемый файл в target/debug/hello_cargo (илиtarget\debug\hello_cargo.exe в Windows), а не в вашем текущем каталоге. Поскольку стандартная сборка является отладочной, Cargo помещает двоичный файл в каталог с именем debug. Вы можете запустить исполняемый файл с помощью этой команды:Если все хорошо, то Hello, world! печатается в терминале. Запуск команды cargo build в первый раз также приводит к созданию нового файла Cargo.lock в папке верхнего уровня. Данный файл хранит точные версии зависимостей вашего проекта. Так как у нас нет зависимостей, то файл пустой. Вы никогда не должны менять этот файл вручную:Cargo сам управляет его содержимым для вас.Мы только что собрали проект командой cargo build и запустили его из ./target/debug/hello_cargo. Но мы также можем использовать команду cargo run для компиляции кода и затем его запуска одной командой:$ 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 # or .\target\debug\hello_cargo.exe on WindowsHello, world! $ 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 также предоставляет команду, называемую cargo check. Эта команда быстро проверяет ваш код, чтобы убедиться, что он компилируется, но не создаёт исполняемый файл:Почему вам не нужен исполняемый файл? Часто cargo check выполняется намного быстрее, чем cargo build, поскольку пропускает этап создания исполняемого файла.Если вы постоянно проверяете свою работу во время написания кода, использование cargo check ускорит процесс информирования вас о том, что ваш проект всё ещёкомпилируется! Таким образом, многие Rustacean периодически запускают cargo check,когда пишут свои программы, чтобы убедиться, что она компилируется. Затем они запускают cargo build, когда готовы использовать исполняемый файл.Давайте подытожим, что мы уже узнали о Cargo:Мы можем создать проект с помощью cargo new можно собирать проект, используя команду cargo build,можно одновременно собирать и запускать проект одной командой cargo run,можно собрать проект для проверки ошибок с помощью cargo check, не тратя время на кодогенерацию исполняемого файла,cargo сохраняет результаты сборки не в директорию с исходным кодом, а в отдельный каталог target/debug.Дополнительным преимуществом использования Cargo является то, что его команды одинаковы для разных операционных систем. С этой точки зрения, мы больше не будем предоставлять отдельные инструкции для Linux, macOS или Windows.$ 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 check Checking hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs Сборка финальной версии (Release)Когда проект, наконец, готов к релизу, можно использовать команду cargo build -- release для его компиляции с оптимизацией. Данная команда создаёт исполняемый файл в папке target/release в отличии от папки target/debug. Оптимизации делают так, чтоRust код работает быстрее, но их включение увеличивает время компиляции. По этой причине есть два отдельных профиля: один для разработки, когда нужно осуществлять сборку быстро и часто, и другой, для сборки финальной программы, которую будете отдавать пользователям, которая готова к работе и будет выполняться максимально быстро. Если вы замеряете время выполнения вашего кода, убедитесь, что собрали проект с оптимизацией cargo build --release и тестируете исполняемый файл из папкиtarget/release.1   2   3   4   5   6   7   8   9   ...   62

Недействительные ссылкиВ языках с указателями легко ошибочно создать висячий указатель — указатель,ссылающийся на место в памяти, которое могло быть передано кому-то другому после освобождения этой части памяти, сохраняя при этом указатель на неё. В Rust, напротив,компилятор гарантирует, что ссылки никогда не будут висячими: если у вас есть ссылка на какие-то данные, компилятор убедится, что данные не выйдут за пределы области видимости до того, как это сделает ссылка на них.Давайте попробуем создать висячую ссылку, чтобы увидеть, как Rust предотвращает их появление с помощью ошибки во время компиляции:Файл: src/main.rsЗдесь ошибка:Это сообщение об ошибке относится к особенности языка, которую мы ещё не рассмотрели: времени жизни. Мы подробно обсудим времена жизни в главе 10. Но если вы не обращаете внимания на части, касающиеся времени жизни, сообщение будет содержать ключ к тому, почему этот код является проблемой: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 | 5 | fn dangle() -> &'static String { |

Определение и инициализация структурСтруктуры похожи на кортежи, рассмотренные в разделе "Кортежи", так как оба хранят несколько связанных значений. Как и кортежи, части структур могут быть разных типов.В отличие от кортежей, в структуре необходимо именовать каждую часть данных для понимания смысла значений. Добавление этих имён обеспечивает большую гибкость структур по сравнению с кортежами: не нужно полагаться на порядок данных для указания значений экземпляра или доступа к ним.Для определения структуры указывается ключевое слово struct и её название.Название должно описывать значение частей данных, сгруппированных вместе. Далее, в фигурных скобках для каждой новой части данных поочерёдно определяются имя части данных и её тип. Каждая пара имя: тип называется полем. Листинг 5-1 описывает структуру для хранения информации об учётной записи пользователя:Листинг 5-1: Определение структуры UserПосле определения структуры можно создавать её экземпляр, назначая определённое значение каждому полю с соответствующим типом данных. Чтобы создать экземпляр, мы указываем имя структуры, затем добавляем фигурные скобки и включаем в них пары ключ: значение (key: value), где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании структуры (но всё-таки желательно для удобства чтения). Другими словами, объявление структуры - это как шаблон нашего типа, в то время как экземпляр структуры использует этот шаблон, заполняя его определёнными данными, для создания значений нашего типа. Например, можно объявить пользователя как в листинге 5-2:Листинг 5-2: Создание экземпляра структуры UserЧтобы получить конкретное значение из структуры, мы используем запись через точку.Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы structUser { active: bool, username: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; } используем user1.email. Если экземпляр является изменяемым, мы можем поменять значение, используя точечную нотацию и присвоение к конкретному полю. В Листинге5-3 показано, как изменить значение в поле email изменяемого экземпляра UserЛистинг 5-3: Изменение значения в поле email экземпляра UserЗаметим, что весь экземпляр структуры должен быть изменяемым; Rust не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания структуры в качестве последнего выражения тела функции для неявного возврата нового экземпляра.На листинге 5-4 функция build_user возвращает экземпляр User с указанным адресом и именем. Поле active получает значение true, а поле sign_in_count получает значение1Листинг 5-4: Функция build_user, которая принимает email и имя пользователя и возвращает экземпляр UserИмеет смысл называть параметры функции теми же именами, что и поля структуры, но необходимость повторять email и username для названий полей и переменных несколько утомительна. Если структура имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!Использование сокращённой инициализации поляТак как имена входных параметров функции и полей структуры являются полностью идентичными в листинге 5-4, возможно использовать синтаксис сокращённойfn main() { let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); } fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } инициализации поля, чтобы переписать build_user так, чтобы он работал точно также,но не содержал повторений для email и username, как в листинге 5-5.Листинг 5-5: Функция build_user, использующая сокращённую инициализацию поля, когда параметры email и username имеют те же имена, что и поля structЗдесь происходит создание нового экземпляра структуры User, которая имеет поле с именем email. Мы хотим установить поле структуры email значением входного параметра email функции build_user. Так как поле email и входной параметр функции email имеют одинаковое название, можно писать просто email вместо кода email: emailСоздание экземпляра структуры из экземпляра другой структуры спомощью синтаксиса обновления структурыЧасто бывает полезно создать новый экземпляр структуры, который включает большинство значений из другого экземпляра, но некоторые из них изменяет. Это можно сделать с помощью синтаксиса обновления структуры.Сначала в листинге 5-6 показано, как обычно создаётся новый экземпляр User в user2без синтаксиса обновления. Мы задаём новое значение для email, но в остальном используем те же значения из user1, которые были заданы в листинге 5-2.Листинг 5-6: Создание нового экземпляра User с использованием некоторых значений из экземпляра user1Используя синтаксис обновления структуры, можно получить тот же эффект, используя меньше кода как показано в листинге 5-7. Синтаксис указывает, что оставшиеся поля fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } fn main() { // --snip-- let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, }; } устанавливаются неявно и должны иметь значения из указанного экземпляра.Листинг 5-7: Использование синтаксиса обновления структуры для установки нового значения email дляэкземпляра User, но использование остальных значений из экземпляра 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:fn main() { // --snip-- let user2 = User { email: String::from("another@example.com"), ..user1 }; } Обратите внимание, что значения black и origin — это разные типы, потому что они являются экземплярами разных кортежных структур. Каждая определяемая вами структура имеет собственный тип, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, принимающая параметр типа Color, не может принимать Point в качестве аргумента, даже если оба типа состоят из трёх значений i32. В остальном экземпляры кортежных структур похожи на кортежи в том смысле, что вы можете деструктурировать их на отдельные части и использовать , за которой следует индекс для доступа к отдельному значению.Единично-подобные структуры: структуры без полейТакже можно определять структуры, не имеющие полей! Они называются единично-подобными структурами, поскольку ведут себя аналогично (), единичному типу, о котором мы говорили в разделе "Кортежи". Единично-подобные структуры могут быть полезны, когда требуется реализовать типаж для некоторого типа, но у вас нет данных,которые нужно хранить в самом типе. Мы обсудим типажи в главе 10. Вот пример объявления и создание экземпляра единичной структуры с именем AlwaysEqual:Чтобы определить AlwaysEqual, мы используем ключевое слово struct, желаемое имя,а затем точку с запятой. Нет необходимости в фигурных или круглых скобках! Затем мы можем получить экземпляр AlwaysEqual в переменной subject аналогичным образом:используя имя, которое мы определили, без фигурных и круглых скобок. Представим, что в дальнейшем мы реализуем поведение для этого типа таким образом, что каждый экземпляр AlwaysEqual всегда будет равен каждому экземпляру любого другого типа,возможно, с целью получения ожидаемого результата для тестирования. Для реализации такого поведения нам не нужны никакие данные! В главе 10 вы увидите, как определять черты и реализовывать их для любого типа, включая единично-подобные структуры.Владение данными структурыstructColor(i32, i32, i32); structPoint(i32, i32, i32); fn main() { let black = Color(0, 0, 0); let origin = Point(0, 0, 0); } structAlwaysEqual; fn main() { let subject = AlwaysEqual; } В определении структуры User в листинге 5-1 мы использовали владеющий тип String вместо типа строковой срез &str. Это осознанный выбор, поскольку мы хотим, чтобы каждый экземпляр этой структуры владел всеми своими данными и чтобы эти данные были действительны до тех пор, пока действительна вся структура.Структуры также могут хранить ссылки на данные, принадлежащие кому-то другому,но для этого необходимо использовать возможность Rust время жизни, которую мы обсудим в главе 10. Время жизни гарантирует, что данные, на которые ссылается структура, будут действительны до тех пор, пока существует структура. Допустим,если попытаться сохранить ссылку в структуре без указания времени жизни, как в следующем примере; это не сработает:Имя файла: src/main.rsКомпилятор будет жаловаться на необходимость определения времени жизни ссылок:structUser { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; } В главе 10 мы обсудим, как исправить эти ошибки, чтобы иметь возможность хранить ссылки в структурах, а пока мы исправим подобные ошибки, используя владеющие типы вроде String вместо ссылок &str$ 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 Пример использования структурЧтобы понять, когда нам может понадобиться использование структур, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.Давайте создадим новый проект программы при помощи Cargo и назовём его rectangles.Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Листинг 5-8 показывает один из коротких вариантов кода, который позволит нам сделать именно то, что надо, в файле проектаsrc/main.rs.Файл: src/main.rsЛистинг 5-8: вычисление площади прямоугольника, заданного отдельными переменными ширины ивысотыТеперь запустим программу, используя cargo run:Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area скаждым измерением, но мы можем улучшить его ясность и читабельность.Проблема данного метода очевидна из сигнатуры 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 } $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.42s Running `target/debug/rectangles` The area of the rectangle is 1500 square pixels. fn area(width: u32, height: u32) -> u32 { ширину и высоту вместе. В разделе «Кортежи» главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.Рефакторинг при помощи кортежейЛистинг 5-9 — это другая версия программы, использующая кортежи.Файл: src/main.rsЛистинг 5-9: определение ширины и высоты прямоугольника с помощью кортежаС одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже с индексом 0, а высота height — с индексом 1. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.Рефакторинг при помощи структур: добавим больше смыслаМы используем структуры, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в листинге 5-10.Файл: 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} Листинг 5-10: определение структуры RectangleЗдесь мы определили структуру и дали ей имя Rectangle. Внутри фигурных скобок определили поля как width и height, оба — типа u32. Затем в main создали конкретный экземпляр Rectangle с шириной в 30 и высотой в 50 единиц.Наша функция area теперь определена с одним параметром, названным rectangle, чей тип является неизменяемым заимствованием структуры Rectangle. Как упоминалось в главе 4, необходимо заимствовать структуру, а не передавать её во владение. Таким образом функция main сохраняет rect1 в собственности и может использовать еёдальше. По этой причине мы и используем & в сигнатуре и в месте вызова функции.Функция area получает доступ к полям width и height экземпляра Rectangle (обратите внимание, что доступ к полям заимствованного экземпляра структуры не приводит к перемещению значений полей, поэтому вы часто видите заимствования структур). Наша сигнатура функции для area теперь говорит именно то, что мы имеем в виду: вычислить площадь Rectangle, используя его поля width и height. Это означает, что ширина и высота связаны друг с другом, и даёт описательные имена значениям, а не использует значения индекса кортежа 0 и 1. Это торжество ясности.Добавление полезной функциональности при помощи выводимых1   ...   6   7   8   9   10   11   12   13   ...   62

Управляющая конструкция matchВ Rust есть чрезвычайно мощный механизм управления потоком, именуемый match,который позволяет сравнивать значение с различными шаблонами и затем выполнять код в зависимости от того, какой из шаблонов совпал. Шаблоны могут состоять из литеральных значений, имён переменных, подстановочных знаков и многого другого; в главе 18 рассматриваются все различные виды шаблонов и то, что они делают. Сила match заключается в выразительности шаблонов и в том, что компилятор проверяет, что все возможные случаи обработаны.Думайте о выражении match как о машине для сортировки монет: монеты скользят по дорожке с различными по размеру отверстиями, и каждая монета падает через первое попавшееся отверстие, в которое она поместилась. Таким же образом значения проходят через каждый шаблон в match, и при первом же "подходящем" шаблоне значение попадает в соответствующий блок кода, который будет использоваться во время выполнения.Говоря о монетах, давайте используем их в качестве примера, используя match! Для этого мы напишем функцию, которая будет получать на вход неизвестную монетуСоединённых Штатов и, подобно счётной машине, определять, какая это монета, и возвращать её стоимость в центах, как показано в листинге 6-3.Листинг 6-3: Перечисление и выражение match, использующее в качестве шаблонов его вариантыДавайте разберём match в функции value_in_cents. Сначала мы используем ключевое слово match, за которым следует выражение, которое в данном случае является значением coin. Это кажется очень похожим на выражение, используемое в if, но есть большая разница: в if выражение должно возвращать булево значение, а здесь оно может возвращать любой тип. Типом coin в данном примере является перечисление Coin, которое мы определили в первой строке.enumCoin { 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, } } Далее идут ветки match. Ветки состоят из двух частей: шаблон и некоторый код. Здесь первая ветка имеет шаблон, который является значением Coin::Penny, затем идёт оператор =>, который разделяет шаблон и код для выполнения. Код в этом случае - это просто значение 1. Каждая ветка отделяется от последующей при помощи запятой.Когда выполняется выражение match, оно сравнивает полученное значение с образцом каждой ветки по порядку. Если шаблон совпадает со значением, то выполняется код,связанный с этим шаблоном. Если этот шаблон не соответствует значению, то выполнение продолжается со следующей ветки, так же, как в автомате по сортировке монет. У нас может быть столько веток, сколько нужно: в листинге 6-3 наш match состоит из четырёх веток.Код, связанный с каждой веткой, является выражением, а полученное значение выражения в соответствующей ветке — это значение, которое возвращается для всего выражения matchОбычно фигурные скобки не используются, если код совпадающей ветви невелик, как в листинге 6-3, где каждая ветвь просто возвращает значение. Если вы хотите выполнить несколько строк кода в одной ветви, вы должны использовать фигурные скобки, а запятая после этой ветви необязательна. Например, следующий код печатает "Lucky penny!" каждый раз, когда метод вызывается с Coin::Penny, но при этом он возвращает последнее значение блока - 1:Шаблоны, которые привязывают значенияЕсть ещё одно полезное качество у веток в выражении match: они могут привязываться к частям тех значений, которые совпали с шаблоном. Благодаря этому можно извлекать значения из вариантов перечисления.В качестве примера, давайте изменим один из вариантов перечисления так, чтобы он хранил в себе данные. С 1999 по 2008 год Соединённые Штаты чеканили 25 центов с различным дизайном на одной стороне для каждого из 50 штатов. Ни одна другая монета не получила дизайна штата, только четверть доллара имела эту дополнительную особенность. Мы можем добавить эту информацию в наш enum путём изменения варианта Quarter и включить в него значение UsState, как сделано в листинге 6-4.fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } Листинг 6-4: Перечисление Coin, в котором вариант Quarter также сохраняет значение UsStateПредставьте, что ваш друг пытается собрать четвертаки всех 50 штатов. Сортируя монеты по типу, мы также будем сообщать название штата, к которому относится каждый четвертак, чтобы, если у нашего друга нет такой монеты, он мог добавить её в свою коллекцию.В выражении match для этого кода мы добавляем переменную с именем state вшаблон, который соответствует значениям варианта Coin::Quarter. Когда Coin::Quarter совпадёт с шаблоном, переменная state будет привязана к значению штата этого четвертака. Затем мы сможем использовать state в коде этой ветки, вот так:Если мы сделаем вызов функции value_in_cents(Coin::Quarter(UsState::Alaska)), то coin будет иметь значение Coin::Quarter(UsState::Alaska). Когда мы будем сравнивать это значение с каждой из веток, ни одна из них не будет совпадать, пока мы не достигнем варианта Coin::Quarter(state). В этот момент state привяжется к значению UsState::Alaska. Затем мы сможем использовать эту привязку в выражении println!, получив таким образом внутреннее значение варианта Quarter перечисления CoinСопоставление шаблона для Option#[derive(Debug)]// so we can inspect the state in a minute enumUsState { Alabama, Alaska, // --snip--} enumCoin { 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 } } } В предыдущем разделе мы хотели получить внутреннее значение T для случая Some при использовании Option; мы можем обработать тип Option используя match,как уже делали с перечислением Coin! Вместо сравнения монет мы будем сравнивать варианты Option, независимо от этого изменения механизм работы выражения match останется прежним.Допустим, мы хотим написать функцию, которая принимает Option и если есть значение внутри, то добавляет 1 к существующему значению. Если значения нет, то функция должна возвращать значение None и не пытаться выполнить какие-либо операции.Такую функцию довольно легко написать благодаря выражению match, код будет выглядеть как в листинге 6-5.Листинг 6-5: Функция, использующая выражение match для OptionДавайте более подробно рассмотрим первое выполнение plus_one. Когда мы вызываем plus_one(five), переменная x в теле plus_one будет иметь значение Some(5). Затем мы сравниваем это значение с каждой ветвью выражения match.Значение Some(5) не соответствует шаблону None, поэтому мы продолжаем со следующей ветки.Совпадает ли Some(5) с шаблоном Some(i)? Да, это так! У нас такой же вариант. Тогда переменная i привязывается к значению, содержащемуся внутри Some, поэтому iполучает значение 5. Затем выполняется код ассоциированный для данной ветки,поэтому мы добавляем 1 к значению i и создаём новое значение Some со значением 6внутри.Теперь давайте рассмотрим второй вызов plus_one в листинге 6-5, где x является NoneМы входим в выражение match и сравниваем значение с первой веткой.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, Some(i) => Some(i + 1), None => None, Оно совпадает! Для данной ветки шаблон (None) не подразумевает наличие какого-то значения к которому можно было бы что-то добавить, поэтому программа останавливается и возвращает значение которое находится справа от => - т.е. None. Так как шаблон первой ветки совпал, то никакие другие шаблоны веток не сравниваются.Комбинирование match и перечислений полезно во многих ситуациях. Вы часто будете видеть подобную комбинацию в коде на Rust: сделать сопоставление значений перечисления используя match, привязать переменную к данным внутри значения,выполнить код на основе привязанных данных. Сначала это может показаться немного сложным, но как только вы привыкнете, то захотите чтобы такая возможность была бы во всех языках. Это неизменно любимый пользователями приём.Match объемлет все варианты значенияЕсть ещё один аспект match, который мы должны обсудить: шаблоны должны покрывать все возможные варианты. Рассмотрим эту версию нашей функции plus_one, которая содержит ошибку и не компилируется:Мы не обработали вариант None, поэтому этот код вызовет дефект в программе. Ксчастью, Rust знает и умеет ловить такой случай. Если мы попытаемся скомпилировать такой код, мы получим ошибку компиляции:fn plus_one(x: Option<i32>) -> Option<i32> { match x { Some(i) => Some(i + 1), } } $ 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` defined here = note: the matched value is of type `Option` 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` due to previous error Rust знает, что мы не описали все возможные случаи, и даже знает, какой именно из шаблонов мы упустили! Сопоставления в Rust являются исчерпывающими: мы должны покрыть все возможные варианты, чтобы код был корректным. Особенно в случае Option, когда Rust не даёт нам забыть обработать явным образом значение None,тем самым он защищает нас от предположения, что у нас есть значение, в то время как у нас может быть и null, что делает невозможным совершить ошибку на миллиард долларов, о которой говорилось ранее.Универсальные шаблоны и заполнитель _Используя перечисления, мы также можем выполнять специальные действия для нескольких определённых значений, а для всех остальных значений выполнять одно действие по умолчанию. Представьте, что мы реализуем игру, в которой при выпадении3 игрок не двигается, а получает новую модную шляпу. Если выпадает 7, игрок теряет шляпу. При всех остальных значениях ваш игрок перемещается на столько-то мест на игровом поле. Вот match, реализующий эту логику, в котором результат броска костей жёстко закодирован, а не является случайным значением, а вся остальная логика представлена функциями без тел, поскольку их реализация не входит в рамки данного примера:Для первых двух веток шаблонами являются литеральные значения 3 и 7. Для последней ветки, которая охватывает все остальные возможные значения, шаблоном является переменная, которую мы решили назвать other. Код, выполняемый для другой ветки,использует эту переменную, передавая её в функцию move_player.Этот код компилируется, даже если мы не перечислили все возможные значения u8,потому что последний паттерн будет соответствовать всем значениям, не указанным в конкретном списке. Этот универсальный шаблон удовлетворяет требованию, что соответствие должно быть исчерпывающим. Обратите внимание, что мы должны поместить ветку с универсальным шаблоном последней, потому что шаблоны оцениваются по порядку. Rust предупредит нас, если мы добавим ветки после универсального шаблона, потому что эти последующие ветки никогда не будут выполняться!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) {} В Rust также есть шаблон, который можно использовать, когда мы не хотим использовать значение в универсальном шаблоне: _, который является специальным шаблоном, который соответствует любому значению и не привязывается к этому значению. Это говорит Rust, что мы не собираемся использовать это значение, поэтомуRust не будет предупреждать нас о неиспользуемой переменной.Давайте изменим правила игры так: если выпадает что-то, кроме 3 или 7, нужно бросить ещё раз. Нам не нужно использовать значение в этом случае, поэтому мы можем изменить наш код, чтобы использовать _ вместо переменной с именем other:Этот пример также удовлетворяет требованию исчерпывающей полноты, поскольку мы явно игнорируем все остальные значения в последней ветке; мы ничего не забыли.Если мы изменим правила игры ещё раз, чтобы в ваш ход не происходило ничего другого, если вы бросаете не 3 или 7, мы можем выразить это, используя единичное значение (пустой тип кортежа, о котором мы упоминали в разделе "Кортежи") в качестве кода, который идёт вместе с веткой _:Здесь мы явно говорим Rust, что не собираемся использовать никакое другое значение,которое не соответствует шаблонам в предыдущих ветках, и не хотим запускать никакой код в этом случае.Подробнее о шаблонах и совпадениях мы поговорим в Главе 18. Пока же мы перейдём к синтаксису if let, который может быть полезен в ситуациях, когда выражение match слишком многословно.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() {} let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {} Компактное управление потоком выполнения с if letСинтаксис if let позволяет скомбинировать if и let в менее многословную конструкцию, и затем обработать значения соответствующе только одному шаблону,одновременно игнорируя все остальные. Рассмотрим программу в листинге 6-6, которая обрабатывает сопоставление значения Option в переменной config_max, но хочет выполнить код только в том случае, если значение является вариантом SomeЛистинг 6-6. Выражение match, которое выполнит код только при значении равном SomeЕсли значение равно Some, мы распечатываем значение в варианте Some, привязывая значение к переменной max в шаблоне. Мы не хотим ничего делать со значением NoneЧтобы удовлетворить выражение match, мы должны добавить _ => () после обработки первой и единственной ветки, и добавление шаблонного кода раздражает.Вместо этого, мы могли бы написать это более коротким способом, используя if letСледующий код ведёт себя так же, как выражение match в листинге 6-6:Синтаксис if let принимает шаблон и выражение, разделённые знаком равенства. if let сработает так же, как match, когда в него на вход передадут выражение и подходящим шаблоном для этого выражения окажется первая ветка. В нашем случае шаблоном является Some(max), где max привязывается к значению внутри Some. Затем мы можем использовать max в теле блока if let так же, как мы использовали max всоответствующей ветке match. Код в блоке if let не запускается, если значение не соответствует шаблону.Используя if let мы меньше печатаем, меньше делаем отступов и меньше получаем шаблонного кода. Тем не менее, мы теряем полную проверку всех вариантов,предоставляемую выражением match. Выбор между match и if let зависит от того, что вы делаете в вашем конкретном случае и является ли получение краткости при потере полноты проверки подходящим компромиссом.Другими словами, вы можете думать о конструкции if let как о синтаксическом сахаредля match, который выполнит код если входное значение будет соответствовать единственному шаблону, и проигнорирует все остальные значения.let config_max = Some(3u8); match config_max { Some(max) => println!("The maximum is configured to be {}", max), _ => (), } let config_max = Some(3u8); if letSome(max) = config_max { println!("The maximum is configured to be {}", max); } Можно добавлять else к if let. Блок кода, который находится внутри else аналогичен по смыслу блоку кода ветки связанной с шаблоном _ выражения match (которое эквивалентно сборной конструкции if let и else). Вспомним объявление перечисления Coin в листинге 6-4, где вариант Quarter также содержит внутри значение штата типа UsState. Если бы мы хотели посчитать все монеты не являющиеся четвертями, а для четвертей печатать название штата, то мы могли бы сделать это с помощью выражения match таким образом:Или мы могли бы использовать выражение if let и else так:Если у вас есть ситуация в которой ваша программа имеет логику которая слишком многословна для того чтобы её выражать используя match, помните, о том, что также в вашем наборе инструментов Rust есть if letИтогиМы рассмотрели как использовать перечисления для создания пользовательских типов,которые могут быть одним из наборов перечисляемых значений. Мы показали, как тип Option из стандартной библиотеки помогает использовать систему типов для предотвращения ошибок. А когда значения перечисления имеют данные внутри них,можно использовать match или if let, чтобы извлечь и пользоваться значением, в зависимости от того, сколько случаев нужно обработать.Теперь ваши программы на Rust могут выражать концепции вашей предметной области используя структуры и перечисления. Создание и использование пользовательских типов в API обеспечивает типобезопасность: компилятор позаботится о том, чтобы функции получали значения только того типа, который они ожидают.Чтобы предоставить вашим пользователям хорошо организованный API, который прост в использовании и предоставляет только то, что нужно вашим пользователям, надо поговорить о модулях в Rust.let mut count = 0; match coin { Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, } let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } 1   ...   9   10   11   12   13   14   15   16   ...   62

Рисунок 14-2: Предоставленная документация для my_crate, включая комментарий, описывающие крейт вцеломКомментарии к документации внутри элементов полезны для описания крейтов и модулей особенно. Используйте их, чтобы объяснить общую цель контейнера, чтобы помочь вашим пользователям понять организацию крейта.Экспорт удобного общедоступного API с pub useСтруктура вашего публичного API является основным фактором при публикации крейта.Люди, которые используют вашу библиотеку, менее знакомы со структурой, чем вы и//! # My Crate//!//! `my_crate` is a collection of utilities to make performing certain//! calculations more convenient./// Adds one to the number given.// --snip-- могут столкнуться с трудностями при поиске частей, которые они хотят использовать,если ваша библиотека имеет большую иерархию модулей.В главе 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 Листинг 14-3: Библиотека art с элементами, организованными в модули kinds и utilsНа рисунке 14-3 показано, как будет выглядеть титульная страница документации для этого крейта, сгенерированный cargo doc://! # Art//!//! A library for modeling artistic concepts.pub mod kinds { /// The primary colors according to the RYB color model.pub enumPrimaryColor { Red, Yellow, Blue, } /// The secondary colors according to the RYB color model.pub enumSecondaryColor { 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-- } } Рисунок 14-3: Первая страница документации для art, в которой перечислены модули kinds и utilsОбратите внимание, что типы PrimaryColor и SecondaryColor не указаны на главной странице, равно как и функция mix. Мы должны нажать kinds и utils, чтобы увидеть их.В другой библиотеке, которая зависит от этой библиотеки, потребуются операторы use,которые подключают элементы из art в область видимости, определяя структуру модуля, которая определена в данный момент. В листинге 14-4 показан пример крейта, в котором используются элементы PrimaryColor и mix из крейта art:Файл: src/main.rsЛистинг 14-4: Крейт использующий элементы из крейта art с экспортированной внутренней структуройАвтору кода в листинге 14-4, который использует крейт art, пришлось выяснить, что PrimaryColor находится в модуле kinds, а mix - в модуле utils. Структура модуля art крейта больше подходит для разработчиков, работающих над art крейтом, чем для тех,кто его использует. Внутренняя структура не содержит никакой полезной информации для того, кто пытается понять, как использовать крейт art, а скорее вызывает путаницу,поскольку разработчики, использующие его, должны понять, где искать, и должны указывать имена модулей в выражениях useЧтобы удалить внутреннюю организацию из общедоступного API, мы можем изменить код крейта art в листинге 14-3, чтобы добавить операторы pub use для повторного реэкспорта элементов на верхнем уровне, как показано в листинге 14-5:Файл: src/lib.rs use art::kinds::PrimaryColor; use art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow); } Листинг 14-5: Добавление операторов pub use для реэкспорта элементовДокументация API, которую cargo doc генерирует для этой библиотеки, теперь будет перечислять и связывать реэкспорты на главной странице, как показано на рисунке 14-4,упрощая поиск типов PrimaryColor, SecondaryColor и функции mixРисунок 14-4: Первая страница документации для art, которая перечисляет реэкспортПользователи крейта art могут по-прежнему видеть и использовать внутреннюю структуру из листинга 14-3, как показано в листинге 14-4, или они могут использовать//! # 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--} pub mod utils { // --snip--} более удобную структуру в листинге 14-5, как показано в листинге 14-6:Файл: src/main.rsЛистинг 14-6: Программа, использующая реэкспортированные элементы из крейта artВ случаях, когда имеется много вложенных модулей, реэкспорт типов на верхнем уровне с помощью pub use может существенно повысить удобство работы для людей,использующих крейт. Ещё одно распространённое использование pub use - это реэкспорт определений зависимого модуля в текущем крейте, чтобы сделать определения этого крейта частью публичного API вашего крейта.Создание полезной публичной структуры API - это больше искусство чем наука, и вы можете повторять, чтобы найти API, который лучше всего подойдёт вашим пользователям. Использование pub use даёт вам гибкость в том, как вы структурируете свою библиотеку внутри и отделяете эту внутреннюю структуру от того, что вы предоставляете пользователям. Посмотрите на код некоторых установленных крейтов,чтобы увидеть отличается ли их внутренняя структура от их публичного API.Настройка учётной записи Crates.ioПрежде чем вы сможете опубликовать любые библиотеки, вам необходимо создать учётную запись на crates.io и получить API токен. Для этого зайдите на домашнюю страницу crates.io и войдите в систему через учётную запись GitHub. (В настоящее время требуется наличие учётной записи GitHub, но сайт может поддерживать другие способы создания учётной записи в будущем.) Сразу после входа в систему перейдите в настройки своей учётной записи по адресу https://crates.io/me/ и получите свой ключ API. Затем выполните команду cargo login с вашим ключом API, например:Эта команда сообщит Cargo о вашем API token и сохранит его локально в/.cargo/credentials. Обратите внимание, что этот токен является секретным: не делитесь им ни с кем другим. Если вы по какой-либо причине поделитесь им с кем-либо, вы должны отозвать его и сгенерировать новый токен на crates.ioДобавление метаданных в новую библиотекуuse art::mix; use art::PrimaryColor; fn main() { // --snip--} $ cargo login abcdefghijklmnopqrstuvwxyz012345 Допустим, у вас есть крейт, который вы хотите опубликовать. Перед публикацией вам нужно добавить некоторые метаданные в раздел [package] файла Cargo.toml крейта.Вашему крейту понадобится уникальное имя. Пока вы работаете над крейтом локально,вы можете назвать его как угодно. Однако названия крейтов на crates.io фиксируются в момент первой публикации. Как только крейту присвоено название, никто другой не сможет опубликовать крейт с таким же именем. Перед тем как опубликовать крейт,поищите название, которое вы хотите использовать. Если такое имя уже используется,вам придётся подобрать другое и отредактировать поле name в файле Cargo.toml в разделе [package], чтобы использовать новое имя в качестве публикуемого, например,так:Файл: Cargo.tomlДаже если вы выбрали уникальное имя, когда вы запустите cargo publish чтобы опубликовать крейт, вы получите предупреждение, а затем ошибку:Это ошибка, потому что вам не хватает важной информации: необходимы описание и лицензия, чтобы люди знали, что делает ваш крейт и на каких условиях они могут его использовать. В поле Cargo.toml добавьте описание, состоящее из одного-двух предложений, поскольку оно будет появляться вместе с вашим крейтом в результатах поиска. Для поля license нужно указать значение идентификатора лицензии. В LinuxFoundation's Software Package Data Exchange (SPDX) перечислены идентификаторы,которые можно использовать для этого значения. Например, чтобы указать, что вы лицензировали свой crate, используя лицензию MIT, добавьте идентификатор MIT:Файл: Cargo.toml[package]name = "guessing_game"$ 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 [package]name = "guessing_game"license = "MIT" Если вы хотите использовать лицензию, которая отсутствует в SPDX, вам нужно поместить текст этой лицензии в файл, включите файл в свой проект, а затем используйте license-file, чтобы указать имя этого файла вместо использования ключа licenseРуководство по выбору лицензии для вашего проекта выходит за рамки этой книги.Многие люди в сообществе Rust лицензируют свои проекты так же, как и Rust, используя двойную лицензию MIT OR Apache 2.0. Эта практика демонстрирует, что вы также можете указать несколько идентификаторов лицензий, разделённых OR, чтобы иметь несколько лицензий для вашего проекта.С добавлением уникального имени, версии, вашего описания и лицензии, файлCargo.toml для проекта, который готов к публикации может выглядеть следующим образом:Файл: Cargo.tomlДокументация Cargo описывает другие метаданные, которые вы можете указать, чтобы другие могли легче находить и использовать ваш крейт.Публикация на Crates.ioТеперь, когда вы создали учётную запись, сохранили свой токен API, выбрали имя для своего крейта и указали необходимые метаданные, вы готовы к публикации! Публикация библиотеки загружает определённую версию в crates.io для использования другими.Будьте осторожны, потому что публикация является перманентной операцией. Версия никогда не сможет быть перезаписана, а код не подлежит удалению. Одна из основных целей crates.io - служить постоянным архивом кода, чтобы сборки всех проектов,зависящих от crates из crates.io продолжали работать. Предоставление возможности удаления версий сделало бы выполнение этой цели невозможным. При этом количество версий крейтов, которые вы можете опубликовать, не ограничено.Запустите команду cargo publish ещё раз. Сейчас эта команда должна выполниться успешно:[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] Поздравляем! Теперь вы поделились своим кодом с сообществом Rust и любой может легко добавить вашу библиотеку в качестве зависимости их проекта.Публикация новой версии существующей библиотекиКогда вы внесли изменения в свой крейт и готовы выпустить новую версию, измените значение version, указанное в вашем файле Cargo.toml и повторите публикацию.Воспользуйтесь Semantic Versioning rules, чтобы решить, какой номер следующей версии подходит для ваших изменений. Затем запустите cargo publish, чтобы загрузить новую версию.Устранение устаревших версий с Crates.io с помощью cargo yankХотя вы не можете удалить предыдущие версии крейта, вы можете помешать любым будущим проектам добавлять его в качестве новой зависимости. Это полезно, когда версия крейта сломана по той или иной причине. В таких ситуациях Cargo поддерживаетвыламывание (yanking) версии крейта.Вычёркивание версии не позволяет новым проектам зависеть от этой версии, но при этом позволяет всем существующим проектам, зависящим от неё, продолжать работу. По сути, исключение означает, что все проекты с Cargo.lock не сломаются, а любые файлыCargo.lock, которые будут генерироваться в будущем, не смогут использовать исключённую версию.Чтобы вычеркнуть версию крейта, в директории крейта, который вы опубликовали ранее, выполните команду cargo yank и укажите, какую версию вы хотите вычеркнуть.Например, если мы опубликовали крейт под названием guessing_game версии 1.0.1 и хотим вычеркнуть её, в каталоге проекта для guessing_game мы выполним:Добавив в команду --undo, вы также можете отменить выламывание и разрешить проектам начать зависеть от версии снова:$ 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) $ cargo yank --vers 1.0.1 Updating crates.io index Yank guessing_game:1.0.1 Вычёркивание не удаляет код. Оно не может, например, удалить случайно загруженные пароли. Если это произойдёт, вы должны немедленно сбросить эти пароли.$ cargo yank --vers 1.0.1 --undo Updating crates.io index Unyank guessing_game_:1.0.1 Рабочие пространства CargoВ главе 12 мы создали пакет, который включал в себя бинарный и библиотечный крейты.По мере развития вашего проекта может возникнуть ситуация, когда библиотечный крейт будет становиться все больше, и вы захотите разделить ваш пакет на несколько библиотечных крейтов. Cargo предоставляет функциональность под названиемworkspaces, которая помогает управлять несколькими взаимосвязанными пакетами,которые разрабатываются в тандеме.Создание рабочего пространстваWorkspace - это набор пакетов, которые используют один и тот же Cargo.lock и директорию для хранения результатов компиляции. Давайте создадим проект с использованиемworkspace - мы будем использовать тривиальный код, чтобы сосредоточиться на структуре рабочего пространства. Существует несколько способов структурировать рабочую область, но мы покажем только один из них. У нас будет рабочая область,содержащая двоичный файл и две библиотеки. Двоичный файл, который обеспечивает основную функциональность, будет зависеть от двух библиотек. Одна библиотека предоставит функцию add_one, а вторая - add_two. Эти три крейта будут частью одногоworkspace. Начнём с создания каталога для рабочего окружения:Далее в каталоге add мы создадим файл Cargo.toml, который будет определять конфигурацию всего рабочего окружения. В этом файле не будет секции [package]Вместо этого он будет начинаться с секции [workspace], которая позволит нам добавить модули в рабочее пространство, указав путь к пакету с нашим бинарным крейтом; в данном случае этот путь - adder:Файл: Cargo.tomlЗатем мы создадим исполняемый крейт adder, запустив команду cargo new в каталогеadd:На этом этапе мы можем создать рабочее пространство, запустив команду cargo buildФайлы в каталоге add должны выглядеть следующим образом:$ mkdir add $cd add [workspace]members = [ "adder", ] $ cargo new adder Created binary (application) `adder` package Рабочая область содержит на верхнем уровне один каталог 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Затем сгенерируйте новый крейт библиотеки с именем add_one:Ваш каталог add должен теперь иметь следующие каталоги и файлы:├── Cargo.lock ├── Cargo.toml ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target [workspace]members = [ "adder", "add_one", ] $ cargo new add_one --lib Created library `add_one` package В файле add_one/src/lib.rs добавим функцию add_one:Файл: add_one/src/lib.rsТеперь мы можем сделать так, чтобы пакет adder с нашим исполняемым файлом зависел от пакета add_one, содержащего нашу библиотеку. Сначала нам нужно добавить зависимость пути от add_one в adder/Cargo.toml.Файл: adder/Cargo.tomlCargo не исходит из того, что крейты в рабочем пространстве могут зависеть друг от друга, поэтому нам необходимо явно указать отношения зависимости.Далее, давайте используем функцию add_one (из крейта add_one) в крейте adderОткройте файл adder/src/main.rs и добавьте строку use в верхней части, чтобы ввести в область видимости новый библиотечный крейт add_one. Затем измените функцию main для вызова функции add_one, как показано в листинге 14-7.Файл: adder/src/main.rs1   ...   33   34   35   36   37   38   39   40   ...   62

Листинг 14-7: Использование функционала библиотечного крейта add-one в крейте adderДавайте соберём рабочее пространство, запустив команду cargo build в каталоге верхнего уровня add!├── Cargo.lock ├── Cargo.toml ├── add_one │ ├── Cargo.toml │ └── src │ └── lib.rs ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target pub fn add_one(x: i32) -> i32 { x + 1} [dependencies]add_one = { path = "../add_one" } use add_one; fn main() { let num = 10; println!("Hello, world! {num} plus one is {}!", add_one::add_one(num)); } Чтобы запустить бинарный крейт из каталога add, нам нужно указать какой пакет из рабочей области мы хотим использовать с помощью аргумента -p и названия пакета в команде cargo run :Запуск кода из 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Теперь мы можем добавить use rand; в файл add_one/src/lib.rs и сделать сборку рабочего пространства, запустив cargo build в каталоге add, что загрузит и скомпилирует rand крейт:$ 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 $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/adder` Hello, world! 10 plus one is 11! [dependencies]rand = "0.8.3" Файл Cargo.lock верхнего уровня теперь содержит информацию о зависимости add_one ккрейту rand. Тем не менее, не смотря на то что rand использован где-то в рабочем пространстве, мы не можем использовать его в других крейтах рабочего пространства,пока не добавим крейт rand в отдельные Cargo.toml файлы. Например, если мы добавим use rand; в файл adder/src/main.rs крейта adder, то получим ошибку:Чтобы исправить это, отредактируйте файл Cargo.toml для пакета adder и укажите, что rand также является его зависимостью. При сборке пакета adder rand будет добавлен в список зависимостей для adder в Cargo.lock, но никаких дополнительных копий rand загружено не будет. Cargo позаботился о том, чтобы все крейты во всех пакетах рабочей области, использующих пакет rand, использовали одну и ту же версию, экономя нам место и гарантируя, что все крейты в рабочей области будут совместимы друг с другом.Добавление теста в рабочее пространствоВ качестве ещё одного улучшения давайте добавим тест функции add_one::add_one в add_one:Файл: add_one/src/lib.rs$ cargo build Updating crates.io index Downloaded rand v0.8.3 --snip-- Compiling rand v0.8.3 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: 1 warning emitted Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 10.18s $ 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 test в каталоге верхнего уровня add. Запуск cargo test врабочем пространстве, структурированном подобно этому, запустит тесты для всех крейтов в рабочем пространстве:Первая секция вывода показывает, что тест it_works в крейте add_one прошёл.Следующая секция показывает, что в крейте adder не было обнаружено ни одного теста,а последняя секция показывает, что в крейте add_one не было найдено ни одного теста документации.Мы также можем запустить тесты для одного конкретного крейта в рабочем пространстве из каталог верхнего уровня с помощью флага -p и указанием имени крейта для которого мы хотим запустить тесты: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 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 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 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 Эти выходные данные показывают, что выполнение cargo test запускает только тесты для крейта add-one и не запускает тесты крейта adderЕсли вы соберётесь опубликовать крейты из рабочего пространства на crates.io, каждый крейт будет необходимо будет опубликовать отдельно. Подобно cargo test, мы можем опубликовать конкретный крейт из нашей рабочей области, используя флаг -p и указав имя крейта, который мы хотим опубликовать.Для дополнительной практики добавьте крейт add_two в данное рабочее пространство аналогичным способом, как делали с крейт add_one !По мере роста проекта рассмотрите возможность использования рабочих областей:легче понять небольшие, отдельные компоненты, чем один большой кусок кода. Кроме того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются параллельно.$ cargo test -p add_one Finished test [unoptimized + debuginfo] target(s) in 0.00s Running 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 installКоманда cargo install позволяет локально устанавливать и использовать исполняемые крейты. Она не предназначена для замены системных пакетов; она используется как удобный способ Rust разработчикам устанавливать инструменты,которыми другие разработчики поделились на сайте crates.io. Заметьте, можно устанавливать только пакеты, имеющие исполняемые целевые крейты. Исполняемойцелью (binary target) является запускаемая программа, созданная и имеющая в составе крейта файл src/main.rs или другой файл, указанный как исполняемый, в отличии от библиотечных крейтов, которые не могут запускаться сами по себе, но подходят для включения в другие программы. Обычно крейт содержит информацию в файле README,является ли он библиотекой, исполняемым файлом или обоими вместе.Все исполняемые файлы установленные командой cargo install сохранены в корневой установочной папке bin. Если вы установили Rust с помощью rustup.rs и у вас его нет в пользовательских конфигурациях, то этим каталогом будет $HOME/.cargo/bin. Он гарантирует, что каталог находится в вашем окружении $PATH, чтобы вы имели возможность запускать программы, которые вы установили командой cargo installТак, например, в главе 12 мы упоминали, что для поиска файлов существует реализация утилиты grep на Rust под названием ripgrep. Чтобы установить ripgrep, мы можем выполнить следующее:Последняя строка вывода показывает местоположение и название установленного исполняемого файла, который в случае ripgrep называется rg. Если вашей установочной директорией является $PATH, как уже упоминалось ранее, вы можете запустить rg --help и начать использовать более быстрый и грубый инструмент для поиска файлов!$ cargo install ripgrep Updating crates.io index Downloaded ripgrep v11.0.2 Downloaded 1 crate (243.3 KB) in 0.88s Installing ripgrep v11.0.2 --snip-- Compiling ripgrep v11.0.2 Finished release [optimized + debuginfo] target(s) in 3m 10s Installing /.cargo/bin/rg Installed package `ripgrep v11.0.2` (executable `rg`) Расширение Cargo пользовательскими командамиCargo спроектирован так, что вы можете расширять его новыми субкомандами без необходимости изменения самого Cargo. Если исполняемый файл доступен через переменную окружения $PATH и назван по шаблону cargo-something, то его можно запускать как субкоманду Cargo cargo something. Пользовательские команды подобные этой также перечисляются в списке доступных через cargo --list. Возможность использовать cargo install для установки расширений и затем запускать их так же, как встроенные в Cargo инструменты, это очень удобное следствие продуманного дизайнаCargo!ИтогиСовместное использование кода с Cargo и crates.io является частью того, что делает экосистему Rust полезной для множества различных задач. Стандартная библиотека Rust небольшая и стабильная, но крейты легко распространять, использовать и улучшать независимо от самого языка. Не стесняйтесь делиться кодом, который был вам полезен,через crates.io; скорее всего, он будет полезен и кому-то ещё! Умные указателиУказатель — это общая концепция для переменной, которая содержит адрес участка памяти. Этот адрес «относится к», или «указывает на» некоторые другие данные.Наиболее общая разновидность указателя в Rust — это ссылка, о которой вы узнали из главы 4. Ссылки обозначаются символом & и заимствуют значение, на которое указывают. Они не имеют каких-либо специальных возможностей, кроме как ссылаться на данные, и не имеют никаких накладных расходов.Умные указатели, с другой стороны, являются структурами данных, которые не только действуют как указатель, но также имеют дополнительные метаданные и возможности.Концепция умных указателей не уникальна для Rust: умные указатели возникли в C++ и существуют в других языках. В Rust есть разные умные указатели, определённые в стандартной библиотеке, которые обеспечивают функциональность, выходящую за рамки ссылок. Одним из примеров, который мы рассмотрим в этой главе, является тип умного указателя reference counting (подсчёт ссылок). Этот указатель позволяет иметь несколько владельцев с помощью отслеживания количества владельцев и, когда владельцев не остаётся, очищает данные.Rust с его концепцией владения и заимствования имеет дополнительное различие между ссылками и умными указателями: в то время, как ссылки только заимствуют данные, умные указатели часто владеют данными, на которые указывают.Ранее мы уже сталкивались с умными указателями в этой книге, хотя и не называли их так, например String и Vec в главе 8. Оба этих типа считаются умными указателями,потому что они владеют некоторой областью памяти и позволяют ею манипулировать. Уних также есть метаданные и дополнительные возможности или гарантии. String,например, хранит свой размер в виде метаданных и гарантирует, что содержимое строки всегда будет в кодировке UTF-8.Умные указатели обычно реализуются с помощью структур. Характерной чертой, которая отличает умный указатель от обычной структуры, является то, что для умных указателей реализованы типажи Deref и Drop. Типаж Deref позволяет экземпляру умного указателя вести себя как ссылка, так что вы можете написать код, работающий с ним как со ссылкой, так и как с умным указателем. Типаж Drop позволяет написать код, который будет запускаться когда экземпляр умного указателя выйдет из области видимости. Вэтой главе мы обсудим оба типажа и продемонстрируем, почему они важны для умных указателей.Учитывая, что паттерн умного указателя является общим паттерном проектирования,часто используемым в Rust, эта глава не описывает все существующие умные указатели.Множество библиотек имеют свои умные указатели, и вы также можете написать свои.Мы охватим наиболее распространённые умные указатели из стандартной библиотеки:Box для распределения значений в куче (памяти) Rc тип счётчика ссылок, который допускает множественное владениеТипы Ref и RefMut, доступ к которым осуществляется через тип RefCell,который обеспечивает правила заимствования во время выполнения вместо времени компиляцииДополнительно мы рассмотрим паттерн внутренней изменчивости (interior mutability), где неизменяемый тип предоставляет API для изменения своего внутреннего значения. Мы также обсудим ссылочные зацикленности (reference cycles): как они могут приводить к утечке памяти и как это предотвратить.Приступим! Использование Box для ссылки на данные в кучеНаиболее простой умный указатель - это box, чей тип записывается как Box. Такие переменные позволяют хранить данные в куче, а не в стеке. То, что остаётся в стеке,является указателем на данные в куче. Обратитесь к Главе 4, чтобы рассмотреть разницу между стеком и кучей.У Box нет проблем с производительностью, кроме хранения данных в куче вместо стека.Но он также и не имеет множества дополнительных возможностей. Вы будете использовать его чаще всего в следующих ситуациях:Когда у вас есть тип, размер которого невозможно определить во время компиляции, а вы хотите использовать значение этого типа в контексте, требующем точного размера.Когда у вас есть большой объем данных и вы хотите передать владение, но при этом быть уверенным, что данные не будут скопированыКогда вы хотите получить значение во владение и вас интересует только то, что оно относится к типу, реализующему определённый трейт, а не то, является ли оно значением какого-то конкретного типаМы продемонстрируем первую ситуацию в разделе "Реализация рекурсивных типов с помощью Box". Во втором случае, передача владения на большой объем данных может занять много времени, потому что данные копируются через стек. Для повышения производительности в этой ситуации, мы можем хранить большое количество данных в куче с помощью Box. Затем только небольшое количество данных указателя копируется в стеке, в то время как данные, на которые он ссылается, остаются в одном месте кучи.Третий случай известен как типаж объект (trait object) и глава 17 посвящает целый раздел "Использование типаж объектов, которые допускают значения разных типов"только этой теме. Итак, то, что вы узнаете здесь, вы примените снова в Главе 17!Использование Box для хранения данных в кучеПрежде чем мы обсудим этот вариант использования Box, мы рассмотрим синтаксис и то, как взаимодействовать со значениями, хранящимися в BoxВ листинге 15-1 показано, как использовать поле для хранения значения i32 в куче:Файл: src/main.rsЛистинг 15-1: Сохранение значения i32 в куче с использованием boxfn main() { let b = Box::new(5); println!("b = {}", b); } Мы объявляем переменную b со значением Box, указывающим на число 5,размещённое в куче. Эта программа выведет b = 5; в этом случае мы получаем доступ к данным в box так же, как если бы эти данные находились в стеке. Как и любое другое значение, когда box выйдет из области видимости, как b в конце main, он будет удалён.Деаллокация происходит как для box ( хранящегося в стеке), так и для данных, на которые он указывает (хранящихся в куче).Размещать одиночные значения в куче не слишком целесообразно, поэтому вряд ли вы будете часто использовать box'ы таким образом. В большинстве ситуаций более уместно размещать такие значения, как i32, в стеке, где они и сохраняются по умолчанию.Давайте рассмотрим ситуацию, когда box позволяет нам определить типы, которые мы не могли бы иметь, если бы у нас не было box.Включение рекурсивных типов с помощью BoxesЗначение рекурсивного типа может иметь другое значение такого же типа как свой компонент. Рекурсивные типы представляют собой проблему, поскольку во время компиляции Rust должен знать, сколько места занимает тип. Однако вложенность значений рекурсивных типов теоретически может продолжаться бесконечно, поэтомуRust не может определить, сколько места потребуется. Поскольку box имеет известный размер, мы можем включить рекурсивные типы, добавив box в определение рекурсивного типа.В качестве примера рекурсивного типа рассмотрим cons list. Это тип данных, часто встречающийся в функциональных языках программирования. Тип cons list, который мы определим, достаточно прост, за исключением наличия рекурсии; поэтому концепции,заложенные в примере, с которым мы будем работать, пригодятся вам в любой более сложной ситуации, связанной с рекурсивными типами.Больше информации о cons списке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,которая является недействительным или отсутствующим значением.Сons list не является часто используемой структурой данных в Rust. В большинстве случаев, когда вам нужен список элементов при использовании Rust, лучше использовать Vec. Другие, более сложные рекурсивные типы данных полезны в определённых ситуациях, но благодаря тому, что в этой главе мы начнём с cons list, мы сможем выяснить, как box позволяет нам определить рекурсивный тип данных без особого напряжения.Листинг 15-2 содержит объявление перечисления cons списка. Обратите внимание, что этот код не будет компилироваться, потому что тип List не имеет известного размера,что мы и продемонстрируем.Файл: src/main.rs1   ...   34   35   36   37   38   39   40   41   ...   62

help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:14]`, which implements `Fn(i32) -> i32` | 1 | fn returns_closure() -> impl Fn(i32) -> i32 { |

Исправимые ошибки с Result
Многие ошибки являются не настолько критичными, чтобы останавливать выполнение программы. Иногда, когда в функции происходит сбой, необходима просто правильная интерпретация и обработка ошибки. К примеру, при попытке открыть файл может произойти ошибка из-за отсутствия файла. Вы, возможно, захотите исправить ситуацию и создать новый файл вместо остановки программы.
Вспомните раздел ["Обработка потенциального сбоя с помощью типа
Result
"] главы 2:
мы использовали там перечисление
Result
, имеющее два варианта,
Ok и
Err для обработки сбоев. Само перечисление определено следующим образом:
Типы
T
и
E
являются параметрами обобщённого типа: мы обсудим обобщённые типы более подробно в Главе 10. Все что вам нужно знать прямо сейчас - это то, что
T
представляет тип значения, которое будет возвращено в случае успеха внутри варианта
Ok
, а
E
представляет тип ошибки, которая будет возвращена при сбое внутри варианта
Err
. Так как тип
Result имеет эти обобщённые параметры (generic type parameters), мы можем использовать тип
Result и функции, которые определены для него, в разных ситуациях, когда тип успешного значение и значения ошибки, которые мы хотим вернуть, отличаются.
Давайте вызовем функцию, которая возвращает значение
Result
, потому что может потерпеть неудачу. В листинге 9-3 мы пытаемся открыть файл.
Файл: src/main.rs
Листинг 9-3: Открытие файла
File::open возвращает значения типа
Result
. Универсальный тип
T
в реализации
File::open соответствует типу успешно полученного значения, std::fs::File
, а именно дескриптору файла. Тип
E
, используемый для значения в случае возникновения ошибки, - std::io::Error
. Такой возвращаемый тип означает, что вызов
File::open может быть успешным и вернуть дескриптор файла, из которого мы можем читать или в который можем писать. Также вызов функции может завершиться неудачей: например, файл может не существовать, или у нас может не быть разрешения enum
Result
{
Ok
(T),
Err
(E),
} use std::fs::File; fn main
() { let greeting_file_result = File::open(
"hello.txt"
);
}
на доступ к файлу. Функция
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
Листинг 9-4: Использование выражения
match
для обработки возвращаемых вариантов типа
Result
Обратите внимание, что также как перечисление
Option
, перечисление
Result и его варианты, входят в область видимости благодаря авто-импорту (prelude), поэтому не нужно указывать
Result::
перед использованием вариантов
Ok и
Err в ветках выражения match
Если результатом будет
Ok
, этот код вернёт значение file из варианта
Ok
, а мы затем присвоим это значение файлового дескриптора переменной greeting_file
. После match мы можем использовать дескриптор файла для чтения или записи.
Другая ветвь match обрабатывает случай, где мы получаем значение
Err после вызова
File::open
. В этом примере мы решили вызвать макрос panic!
. Если в нашей текущей директории нет файла с именем hello.txt и мы выполним этот код, то мы увидим следующее сообщение от макроса panic!
:
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),
};
}

Как обычно, данное сообщение точно говорит, что пошло не так.
Обработка различных ошибок с помощью match
Код в листинге 9-4 будет вызывать panic!
независимо от того, почему вызов
File::open не удался. Однако мы хотим предпринять различные действия для разных причин сбоя.
Если открытие
File::open не удалось из-за отсутствия файла, мы хотим создать файл и вернуть его дескриптор. Если вызов
File::open не удался по любой другой причине - например, потому что у нас не было прав на открытие файла, то все равно мы хотим вызвать panic!
как у нас сделано в листинге 9-4. Для этого мы добавляем выражение внутреннего match
, показанное в листинге 9-5.
Файл: src/main.rs
Листинг 9-5: Обработка различных ошибок разными способами
Типом значения возвращаемого функцией
File::open внутри
Err варианта является io::Error
, структура из стандартной библиотеки. Данная структура имеет метод kind
,
который можно вызвать для получения значения io::ErrorKind
. Перечисление io::ErrorKind из стандартной библиотеки имеет варианты, представляющие различные типы ошибок, которые могут появиться при выполнении операций в io
. Вариант,
$
cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling` thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 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);
}
},
};
}
который мы хотим использовать, это
ErrorKind::NotFound
, который даёт информацию, о том, что файл который мы пытаемся открыть ещё не существует. Итак, во второй строке мы вызываем сопоставление шаблона с переменной greeting_file_result и попадаем в ветку с обработкой ошибки, но также у нас есть внутренняя проверка для сопоставления error.kind()
ошибки.
Условие, которое мы хотим проверить во внутреннем match
, заключается в том,
является ли значение, возвращаемое error.kind()
, вариантом
NotFound перечисления
ErrorKind
. Если это так, мы пытаемся создать файл с помощью функции
File::create
Однако, поскольку вызов
File::create тоже может завершиться ошибкой, нам нужна обработка ещё одной ошибки, теперь уже во внутреннем выражении match
. Заметьте:
если файл не может быть создан, выводится другое, специализированное сообщение об ошибке. Вторая же ветка внешнего match
(который обрабатывает вызов error.kind()
),
остаётся той же самой - в итоге программа паникует при любой ошибке, кроме ошибки отсутствия файла.
Альтернативы использованию match с Result
Как много match
! Выражение match является очень полезным, но в то же время довольно примитивным. В главе 13 вы узнаете о замыканиях (closures), которые используются во многих методах типа
Result
. Эти методы помогают быть более лаконичным, чем использование match при работе со значениями
ResultE>
в вашем коде.
Например, вот другой способ написать ту же логику, что показана в Листинге 9-5, но с использованием замыканий и метода unwrap_or_else
:
Хотя этот код ведёт себя так же, как и код из листинга 9-5, он не содержит никаких выражений match и его легче читать. После прочтения главы 13 и поищите метод 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);
}
});
}
подобных методов могут очистить огромные вложенные выражения match, когда вы имеете дело с ошибками.
Лаконичные способы обработки ошибок - unwrap и expect
Использование match работает достаточно хорошо, но может быть довольно многословным и не всегда хорошо передаёт смысл. Тип
Result
имеет множество вспомогательных методов для выполнения различных, более специфических задач.
Метод unwrap
- это метод быстрого доступа к значениям, реализованный так же, как и выражение match
, которое мы написали в Листинге 9-4. Если значение
Result является вариантом
Ok
, unwrap возвращает значение внутри
Ok
. Если
Result
- вариант
Err
, то unwrap вызовет для нас макрос panic!
. Вот пример unwrap в действии:
Файл: src/main.rs
Если мы запустим этот код при отсутствии файла hello.txt, то увидим сообщение об ошибке из вызова panic!
метода unwrap
:
Другой метод, похожий на unwrap
, это expect
, позволяющий указать сообщение об ошибке для макроса panic!
. Использование expect вместо unwrap с предоставлением хорошего сообщения об ошибке выражает ваше намерение и делает более простым отслеживание источника паники. Синтаксис метода expect выглядит так:
Файл: src/main.rs expect используется так же как и unwrap
: либо возвращается дескриптор файла либо вызывается макрос panic!
Наше сообщение об ошибке в expect будет передано в panic!
и заменит стандартное use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
).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 use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)
.expect(
"hello.txt should be included in this project"
);
}
используемое сообщение.
Вот как это выглядит:
В рабочем коде, большинство выбирает expect в угоду unwrap и добавляет описание,
почему операция должна закончиться успешно. Но даже если предположение оказалось неверным, информации для отладки будет больше.
Проброс ошибок
Когда вы пишете функцию, реализация которой вызывает что-то, что может завершиться ошибкой, вместо обработки ошибки в этой функции, вы можете вернуть ошибку в вызывающий код, чтобы он мог решить, что с ней делать. Такой приём известен как
распространение ошибки (propagating the error). Благодаря нему мы даём больше контроля вызывающему коду, где может быть больше информации или логики, которая диктует, как ошибка должна обрабатываться, чем было бы в месте появления этой ошибки.
Например, код программы 9-6 читает имя пользователя из файла. Если файл не существует или не может быть прочтён, то функция возвращает ошибку в код, который вызвал данную функцию.
Файл: src/main.rs
Листинг 9-6: Функция, которая возвращает ошибки в вызывающий код, используя оператор
match 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 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
. Здесь есть возвращаемое значение функции типа
ResultE>
где шаблонный параметр
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, что Rust предоставляет оператор вопросительного знака
?
, чтобы облегчить эту задачу.
Сокращение для проброса ошибок: оператор ?
В листинге 9-7 показана реализация read_username_from_file
, которая имеет ту же функциональность, что и в листинге 9-6, но в этой реализации используется оператор
?
Файл: src/main.rs
Листинг 9-7: Функция, возвращающая ошибки в вызывающий код с помощью оператора
?
Выражение
?
, расположенное после
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<:error> for OurError для создания экземпляра
OurError из io::Error
, то оператор
?
, вызываемый в теле read_username_from_file
, вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию.
use std::fs::File; use std::io; use std::io::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)
}

В случае листинга 9-7 оператор
?
в конце вызова
File::open вернёт значение внутри
Ok в переменную username_file
. Если произойдёт ошибка, оператор
?
выполнит ранний возврат значения
Err вызывающему коду. То же самое относится к оператору
?
в конце вызова read_to_string
Оператор
?
позволяет избавиться от большого количества шаблонного кода и упростить реализацию этой функции. Мы могли бы даже ещё больше сократить этот код,
если бы использовали цепочку вызовов методов сразу после
?
, как показано в листинге
9-8.
Файл: src/main.rs
1   ...   16   17   18   19   20   21   22   23   ...   62

Листинг 9-8: Цепочка вызовов методов после оператора
?
Мы перенесли создание новой
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
демонстрирует способ сделать её ещё короче.
Файл: src/main.rs
Листинг 9-9: Использование
fs::read_to_string
вместо открытия и последующего чтения файла
use std::fs::File; use std::io; use std::io::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)
} 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
Листинг 9-10: Попытка использовать
?
в
main
функции, которая возвращает
()
, не будет
компилироваться
Этот код открывает файл, что может привести к сбою.
?
оператор следует за значением
Result
, возвращаемым
File::open
, но эта main функция имеет возвращаемый тип
()
, а не
Result
. Когда мы компилируем этот код, мы получаем следующее сообщение об ошибке:
use std::fs::File; fn main
() { let greeting_file = File::open(
"hello.txt"
)?;
}


Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, реализующий
FromResidual
Для исправления ошибки есть два варианта. Первый - изменить возвращаемый тип вашей функции так, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Другой способ - использовать match или один из методов
Result
для обработки
Result
любым подходящим способом.
Эта ошибка указывает на то, что оператор
?
разрешено использовать только в функции,
которая возвращает
Result
,
Option или другой тип, который реализует
FromResidual
Чтобы исправить ошибку, есть два варианта. Первый - изменить тип возвращаемый из функции, чтобы он был совместим со значением, для которого вы используете оператор
?
, если у вас нет ограничений, препятствующих этому. Второй заключается в использовании match или одного из методов
Result
для обработки
Result
любым подходящим способом.
Listing 9-11: Using the
?
operator on an
Option
value
Эта функция возвращает
Option
, потому что возможно, что там есть символ, но также возможно, что его нет. Этот код принимает аргумент среза text строки и вызывает для него метод lines
, который возвращает итератор для строк в строке.
Поскольку эта функция хочет проверить первую строку, она вызывает next у итератора,
чтобы получить первое значение от итератора. Если text является пустой строкой, этот вызов next вернёт
None
, и в этом случае мы используем
?
чтобы остановить и вернуть
$
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() {
4 | | let greeting_file = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`. error: could not compile `error-handling` due to previous error fn last_char_of_first_line
(text: &
str
) ->
Option
<
char
> { text.lines().next()?.chars().last()
}

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
, чтобы выполнить преобразование явно.
Обратите внимание, что вы можете использовать оператор
?
на
Result в функции,
которая возвращает
Result
, и вы можете использовать оператор
?
на
Option в
функции, которая возвращает
Option
, но вы не можете смешивать и сочетать их.
Оператор
?
не будет автоматически преобразовывать
Result в
Option или наоборот; в этих случаях, вы можете использовать такие методы, как ok из
Result или ok_or из
Option для явного преобразования.
К счастью, main также может возвращать
Result<(), E>
. В листинге 9-12 используется код из листинга 9-10, но мы изменили возвращаемый тип main на
Result<(), BoxError>>
и добавили возвращаемое значение
Ok(())
в конец. Теперь этот код будет скомпилирован:
Листинг 9-12: Замена
main
на return
Result<(), E>
позволяет использовать оператор
?
оператор над
значениями
Result
Тип
Box
является трейт-объектом, о котором мы поговорим в разделе "Использование трейт-объектов, допускающих значения разных типов"
в главе 17. Пока use std::error::Error; use std::fs::File; fn main
() ->
Result
<(),
Box
<
dyn
Error>> { let greeting_file = File::open(
"hello.txt"
)?;
Ok
(())
}

что вы можете считать, что
Box
означает "любой вид ошибки".
Использование
?
для значения
Result в функции main с типом ошибки
Box
разрешено, так как позволяет вернуть любое значение
Err раньше времени. Даже если тело этой функции main будет возвращать только ошибки типа std::io::Error
, указав
Box
, эта сигнатура останется корректной, даже если в тело main будет добавлен код, возвращающий другие ошибки.
Когда main функция возвращает
Result<(), E>
, исполняемый файл завершится со значением
0
, если main вернёт
Ok(())
, и выйдет с ненулевым значением, если main вернёт значение
Err
. Исполняемые файлы, написанные на C, при выходе возвращают целые числа: успешно завершённые программы возвращают целое число
0
, а программы с ошибкой возвращают целое число, отличное от
0
. Rust также возвращает целые числа из исполняемых файлов, чтобы быть совместимым с этим соглашением.
Функция 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
. Вот пример:
Мы создаём экземпляр
IpAddr
, анализируя жёстко закодированную строку. Можно увидеть, что
127.0.0.1
является действительным IP-адресом, поэтому здесь допустимо использование expect
. Однако наличие жёстко закодированной допустимой строки не меняет тип возвращаемого значения метода parse
: мы все ещё получаем значение
Result и компилятор все также заставляет нас обращаться с
Result так, будто возможен вариант
Err
, потому что компилятор недостаточно умён, чтобы увидеть, что эта строка всегда действительный IP-адрес. Если строка IP-адреса пришла от пользователя, то она не является жёстко запрограммированной в программе и, следовательно, может
привести к ошибке, мы определённо хотели бы обработать
Result более надёжным способом. Упоминание предположения о том, что этот IP-адрес жёстко закодирован,
побудит нас изменить expect для лучшей обработки ошибок, если в будущем нам потребуется вместо этого получить IP-адрес из какого-либо другого источника.
Руководство по обработке ошибок
Желательно, чтобы код паниковал, если он может оказаться в некорректном состоянии. В
этом контексте некорректное состояние это когда некоторое допущение, гарантия,
контракт или инвариант были нарушены. Например, когда недопустимые,
противоречивые или пропущенные значения передаются в ваш код - плюс один или несколько пунктов из следующего перечисленного в списке:
Не корректное состояние — это что-то неожиданное, отличается от того, что может происходить время от времени, например, когда пользователь вводит данные в неправильном формате.
Ваш код после этой точки должен полагаться на то, что он не находится в не корректном состоянии, вместо проверок наличия проблемы на каждом этапе.
Нет хорошего способа закодировать данную информацию в типах, которые вы используете. Мы рассмотрим пример того, что мы имеем в виду в разделе
“Кодирование состояний и поведения на основе типов”
главы 17.
Если кто-то вызывает ваш код и передаёт значения, которые не имеют смысла, лучше всего вернуть ошибку, если вы это можете, чтобы пользователь библиотеки мог решить,
use std::net::IpAddr; let home: IpAddr =
"127.0.0.1"
.parse()
.expect(
"Hardcoded IP address should be valid"
);