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

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

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

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

Добавлен: 11.01.2024

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

Скачиваний: 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 { |

оператор or, так что если значение x
совпадёт с любым из значений в этой ветви, то будет выполнен её код:
Будет напечатано one or two
Сопоставление диапазонов с помощью ..=
Синтаксис
..=
позволяет нам выполнять сравнение с диапазоном значений. В
следующем коде, когда в шаблоне найдётся совпадение с любым из значений заданного диапазона, будет выполнена эта ветка:
Если x
равен 1, 2, 3, 4 или 5, то совпадение будет достигнуто в первой ветке. Этот синтаксис более удобен при указании нескольких значений для сравнения, чем использование оператора
|
для определения этой же идеи; если бы мы решили использовать
|
, нам пришлось бы написать
1 | 2 | 3 | 4 | 5
. Указание диапазона намного короче, особенно если мы хотим подобрать, скажем, любое число от 1 до 1 000!
Компилятор проверяет, что диапазон не является пустым во время компиляции, и поскольку единственными типами, для которых Rust может определить, пуст диапазон или нет, являются char и числовые значения, диапазоны допускаются только с числовыми или char значениями.
Вот пример использования диапазонов значений char
:
let x =
1
; match x {
1
|
2
=> println!
(
"one or two"
),
3
=> println!
(
"three"
),
_ => println!
(
"anything"
),
} let x =
5
; match x {
1
..=
5
=> println!
(
"one through five"
),
_ => println!
(
"something else"
),
} let x =
'c'
; match x {
'a'
..=
'j'
=> println!
(
"early ASCII letter"
),
'k'
..=
'z'
=> println!
(
"late ASCII letter"
),
_ => println!
(
"something else"
),
}

Rust может сообщить, что 'c'
находится в диапазоне первого шаблона и напечатать начальную букву ASCII
Деструктуризация для получения значений
Мы также можем использовать шаблоны для деструктуризации структур, перечислений и кортежей, чтобы использовать разные части этих значений. Давайте пройдёмся по каждому варианту.
Деструктуризация структуры
В листинге 18-12 показана структура
Point с двумя полями x
и y
, которые мы можем разделить, используя шаблон с выражением let
Файл: src/main.rs
Листинг 18-12: Разбиение полей структуры в отдельные переменные
Этот код создаёт переменные a
и b
, которые сопоставляются значениям полей x
и y
структуры p
. Этот пример показывает, что имена переменных в шаблоне не обязательно должны совпадать с именами полей структуры. Однако обычно имена переменных сопоставляются с именами полей, чтобы было легче запомнить, какие переменные взяты из каких полей. Из-за этого, а также из-за того, что строчка let Point
{ x: x, y: y } = p;
содержит много дублирования, в Rust ввели специальное сокращение для шаблонов, соответствующих полям структуры: вам нужно только указать имя поля структуры, и тогда переменные, созданные из шаблона, будут иметь те же имена. Код в листинге 18-13 аналогичен коду в Листинге 18-12, но в шаблоне let создаются переменные x
и y
, вместо a
и b
Файл: src/main.rs struct
Point
{ x: i32
, y: i32
,
} fn main
() { let p = Point { x:
0
, y:
7
}; let
Point { x: a, y: b } = p; assert_eq!
(
0
, a); assert_eq!
(
7
, b);
}


Листинг 18-13: Деструктуризация полей структуры с использованием сокращённой записи
Этот код создаёт переменные x
и y
, которые соответствуют полям x
и y
из переменной p
. В результате переменные x
и y
содержат значения из структуры p
Вместо создания переменных для всех полей мы также можем деструктурировать с помощью литеральных значений являющихся частью структуры. Это позволяет проверить некоторые поля на определённые значения при создании переменных для деструктуризации других полей.
В листинге 18-14 показано выражение match
, которое разделяет значения
Point на три случая: точки, которые лежат непосредственно на оси x
(что верно, когда y = 0
), на оси y
(
x = 0
) или ни то, ни другое.
Файл: src/main.rs
Листинг 18-14: Деструктуризация и сопоставление с литералами в одном шаблоне
Первая ветка будет соответствовать любой точке, которая лежит на оси x
, указанием того что поле y
совпадает, если его значение соответствует литералу равному
0
Шаблон все ещё создаёт переменную x
, которую мы можем использовать в коде для этой ветки.
Точно так же вторая ветка соответствует любой точке на оси y
, указанием того что поле x
совпадает, если его значение равно
0
и создаёт переменную y
для значения поля y
Третья ветка не указывает никаких литералов, поэтому он соответствует любой другой точке
Point и создаёт переменные для обоих полей x
и y
struct
Point
{ x: i32
, y: i32
,
} fn main
() { let p = Point { x:
0
, y:
7
}; let
Point { x, y } = p; assert_eq!
(
0
, x); assert_eq!
(
7
, y);
} fn main
() { let p = Point { x:
0
, y:
7
}; match p {
Point { x, y:
0
} => println!
(
"On the x axis at {}"
, x),
Point { x:
0
, y } => println!
(
"On the y axis at {}"
, y),
Point { x, y } => println!
(
"On neither axis: ({}, {})"
, x, y),
}
}

В этом примере значение p
совпадает по второй ветке, так как x
содержит значение 0,
поэтому этот код будет печатать
On the y axis at 7
Помните, что выражение match перестаёт проверять следующие ветви, как только оно находит первый совпадающий шаблон, поэтому, даже если
Point { x: 0, y: 0}
находится на оси x
и оси y
, этот код будет печатать только
On the x axis at 0
1   ...   46   47   48   49   50   51   52   53   ...   62

Деструктуризация перечислений
Мы уже деструктурировали перечисления в книге (см., например, листинг 6-5 главы 6), но не обсуждали явно, что шаблон для деструктуризации перечисления должен соответствовать способу объявления данных, хранящихся в перечислении. Например, в листинге 18-15 мы используем перечисление
Message из листинга 6-2 и пишем match с
шаблонами, которые будут деструктурировать каждое внутреннее значение.
Файл: src/main.rs
Листинг 18-15: Деструктуризация вариантов перечисления, содержащих разные виды значений
Этот код напечатает
Change the color to red 0, green 160, and blue 255
. Попробуйте изменить значение переменной msg
, чтобы увидеть выполнение кода в других ветках.
enum
Message
{
Quit,
Move { x: i32
, y: i32
},
Write(
String
),
ChangeColor(
i32
, i32
, i32
),
} fn main
() { let msg = Message::ChangeColor(
0
,
160
,
255
); match msg {
Message::Quit => { println!
(
"The Quit variant has no data to destructure."
)
}
Message::Move { x, y } => { println!
(
"Move in the x direction {} and in the y direction {}"
, x, y
);
}
Message::Write(text) => println!
(
"Text message: {}"
, text),
Message::ChangeColor(r, g, b) => println!
(
"Change the color to red {}, green {}, and blue {}"
, r, g, b
),
}
}

Для вариантов перечисления без каких-либо данных, вроде
Message::Quit
, мы не можем деструктурировать значение, которого нет. Мы можем сопоставить только буквальное значение
Message::Quit в этом шаблоне, но без переменных.
Для вариантов перечисления похожих на структуры, таких как
Message::Move
, можно использовать шаблон, подобный шаблону, который мы указываем для сопоставления структур. После имени варианта мы помещаем фигурные скобки и затем перечисляем поля именами переменных. Таким образом мы разделяем фрагменты, которые будут использоваться в коде этой ветки. Здесь мы используем сокращённую форму, как в листинге 18-13.
Для вариантов перечисления, подобных кортежу, вроде
Message::Write
, который содержит кортеж с одним элементом и
Message::ChangeColor
, содержащему кортеж с тремя элементами, шаблон аналогичен тому, который мы указываем для сопоставления кортежей. Количество переменных в шаблоне должно соответствовать количеству элементов в варианте, который мы сопоставляем.
Деструктуризация вложенных структур и перечислений
До сих пор все наши примеры сопоставляли структуры или перечисления на один уровень глубины, но сопоставление может работать и с вложенными элементами!
Например, мы можем реорганизовать код в листинге 18-15 для поддержки цветов RGB и
HSV в сообщении
ChangeColor
, как показано в листинге 18-16.
enum
Color
{
Rgb(
i32
, i32
, i32
),
Hsv(
i32
, i32
, i32
),
} enum
Message
{
Quit,
Move { x: i32
, y: i32
},
Write(
String
),
ChangeColor(Color),
} fn main
() { let msg = Message::ChangeColor(Color::Hsv(
0
,
160
,
255
)); match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => println!
(
"Change the color to red {}, green {}, and blue {}"
, r, g, b
),
Message::ChangeColor(Color::Hsv(h, s, v)) => println!
(
"Change the color to hue {}, saturation {}, and value {}"
, h, s, v
),
_ => (),
}
}


Листинг 18-16: Сопоставление со вложенными перечислениями
Шаблон первой ветки в выражении match соответствует варианту перечисления
Message::ChangeColor
, который содержит вариант
Color::Rgb
; затем шаблон привязывается к трём внутренними значениями i32
. Шаблон второй ветки также соответствует варианту перечисления
Message::ChangeColor
, но внутреннее перечисление соответствует варианту
Color::Hsv
. Мы можем указать эти сложные условия в одном выражении match
, даже если задействованы два перечисления.
Деструктуризация структур и кортежей
Можно смешивать, сопоставлять и вкладывать шаблоны деструктуризации ещё более сложными способами. В следующем примере показана сложная деструктуризация, где мы вкладываем структуры и кортежи внутрь кортежа и деструктурируем из него все примитивные значения:
Этот код позволяет нам разбивать сложные типы на составные части, чтобы мы могли использовать интересующие нас значения по отдельности.
Деструктуризация с помощью шаблонов - это удобный способ использования фрагментов значений, таких как как значение из каждого поля в структуре, по отдельности друг от друга.
Игнорирование значений в шаблоне
Вы видели, что иногда полезно игнорировать значения в шаблоне, например в последней ветке match
, чтобы получить ветку, обрабатывающую любые значения,
которая на самом деле ничего не делает, но учитывает все оставшиеся возможные значения. Есть несколько способов игнорировать целые значения или части значений в шаблоне: используя шаблон
_
(который вы видели), используя шаблон
_
внутри другого шаблона, используя имя, начинающееся с подчёркивания, либо используя
,
чтобы игнорировать оставшиеся части значения. Давайте рассмотрим, как и зачем использовать каждый из этих шаблонов.
Игнорирование всего значения с помощью шаблона _
Мы использовали подчёркивание (
_
) в качестве шаблона подстановочного знака
(wildcard), который будет сопоставляться с любом значением, но не будет привязываться к этому значению. Это особенно удобно в последней ветке выражения match
, но мы также можем использовать его в любом шаблоне, в том числе в параметрах функции, как показано в листинге 18-17.
let
((feet, inches), Point { x, y }) = ((
3
,
10
), Point { x:
3
, y: -
10
});


Файл: src/main.rs
Листинг 18-15: Использование
_
в сигнатуре функции
Этот код полностью игнорирует значение
3
, переданное в качестве первого аргумента,
и выведет на печать
This code only uses the y parameter: 4
В большинстве случаев, когда вам больше не нужен какой-то из параметров функции, вы можете изменить её сигнатуру, убрав неиспользуемый параметр. Игнорирование параметра функции может быть особенно полезно в случаях когда, например, вы реализуете типаж с определённой сигнатурой, но тело функции в вашей реализации не нуждается в одном из параметров. В таком случае компилятор не будет выдавать предупреждения о неиспользуемых параметрах функции, как это было бы, если бы вы указали имя параметра.
Игнорирование частей значения с помощью вложенного _
Также можно использовать
_
внутри другого шаблона, чтобы игнорировать только часть значения, например, когда мы хотим проверить только часть значения, не обращая внимания на другие его части, в соответствующем коде, который мы хотим выполнить. В листинге 18-18 показан код, отвечающий за управление значением настройки. Бизнес-требования заключаются в том, что пользователь не должен иметь права перезаписывать существующую настройку параметра, но может сбросить параметр и присвоить ему значение, если он в данный момент не установлен.
Листинг 18-18: Использование подчёркивания в шаблонах, соответствующих вариантам
Some
, когда нам
не нужно использовать значение внутри
Some fn foo
(_: i32
, y: i32
) { println!
(
"This code only uses the y parameter: {}"
, y);
} fn main
() { foo(
3
,
4
);
} let mut setting_value =
Some
(
5
); let new_setting_value =
Some
(
10
); match
(setting_value, new_setting_value) {
(
Some
(_),
Some
(_)) => { println!
(
"Can't overwrite an existing customized value"
);
}
_ => { setting_value = new_setting_value;
}
} println!
(
"setting is {:?}"
, setting_value);

Этот код будет печатать
Can't overwrite an existing customized value
, а затем setting is Some(5)
. В первой ветке нам не нужно сопоставлять или использовать значения внутри варианта
Some
, но нам нужно проверить случай, когда setting_value и new_setting_value являются вариантом
Some
. В этом случае мы печатаем причину,
почему мы не меняем значение setting_value и оно не меняется.
Во всех других случаях (если либо setting_value
, либо new_setting_value являются вариантом
None
), выраженных шаблоном
_
во второй ветке, мы хотим, чтобы new_setting_value стало равно setting_value
Мы также можем использовать подчёркивание в нескольких местах в одном шаблоне,
чтобы игнорировать конкретные значения. Листинг 18-19 показывает пример игнорирования второго и четвёртого значения в кортеже из пяти элементов.
Листинг 18-19: Игнорирование нескольких частей кортежа
Этот код напечатает
Some numbers: 2, 8, 32
, а значения 4 и 16 будут проигнорированы.
Игнорирование неиспользуемой переменной, начинающейся с символа _ в имени
Если вы создаёте переменную, но нигде её не используете, Rust обычно выдаёт предупреждение, потому что неиспользуемая переменная может быть ошибкой. Но иногда полезно создать переменную, которую вы пока не используете, например, когда вы создаёте прототип или только начинаете проект. В этой ситуации вы можете сказать
Rust не предупреждать вас о неиспользуемой переменной, начав имя переменной с подчёркивания. В листинге 18-20 мы создаём две неиспользуемые переменные, но когда мы компилируем такой код, мы должны получить предупреждение только об одной из них.
Файл: src/main.rs
Листинг 18-20: Начинаем имя переменной с подчёркивания, чтобы не получить предупреждения о
неиспользованных переменных
let numbers = (
2
,
4
,
8
,
16
,
32
); match numbers {
(first, _, third, _, fifth) => { println!
(
"Some numbers: {first}, {third}, {fifth}"
)
}
} fn main
() { let
_x =
5
; let y =
10
;
}