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

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

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

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

Добавлен: 11.01.2024

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

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

Листинг 19-15: Реализация типажа
Add
для структуры
Millimeters
, чтобы складывать
Millimeters
и
Meters
Чтобы сложить
Millimeters и
Meters
, мы указываем impl Add
, чтобы указать значение параметра типа
RHS
(Meters) вместо использования значения по умолчанию
Self
(Millimeters).
Параметры типа по умолчанию используются в двух основных случаях:
Чтобы расширить тип без внесения изменений ломающих существующий код
Чтобы позволить пользовательское поведение в специальных случаях, которые не нужны большинству пользователей
Типаж
Add из стандартной библиотеки является примером второй цели: обычно вы складываете два одинаковых типа, но типаж
Add позволяет сделать больше.
Использование параметра типа по умолчанию в объявлении типажа
Add означает, что не нужно указывать дополнительный параметр большую часть времени. Другими словами, большая часть кода реализации не нужна, что делает использование типажа проще.
use std::ops::Add; struct
Millimeters
(
u32
); struct
Meters
(
u32
); impl
Add for
Millimeters { type
Output
= Millimeters; fn add
(
self
, other: Meters) -> Millimeters {
Millimeters(
self
0
+ (other.
0
*
1000
))
}
}

Первая цель похожа на вторую, но используется наоборот: если вы хотите добавить параметр типа к существующему типажу, можно дать ему значение по умолчанию, чтобы разрешить расширение функциональности типажа без нарушения кода существующей реализации.
Полностью квалифицированный синтаксис для устранения
неоднозначности: вызов методов с одинаковым именем
В Rust ничего не мешает типажу иметь метод с одинаковым именем, таким же как метод другого типажа и Rust не мешает реализовывать оба таких типажа у одного типа. Также возможно реализовать метод с таким же именем непосредственно у типа, такой как и методы у типажей.
При вызове методов с одинаковыми именами в Rust нужно указать, какой из трёх возможных вы хотите использовать. Рассмотрим код в листинге 19-16, где мы определили два типажа:
Pilot и
Wizard
, у обоих есть метод fly
. Затем мы реализуем оба типажа у типа
Human в котором уже реализован метод с именем fly
. Каждый метод fly делает что-то своё.
Файл: src/main.rs trait
Pilot
{ fn fly
(&
self
);
} trait
Wizard
{ fn fly
(&
self
);
} struct
Human
; impl
Pilot for
Human { fn fly
(&
self
) { println!
(
"This is your captain speaking."
);
}
} impl
Wizard for
Human { fn fly
(&
self
) { println!
(
"Up!"
);
}
} impl
Human { fn fly
(&
self
) { println!
(
"*waving arms furiously*"
);
}
}

Листинг 19-16: Два типажа определены с методом
fly
и реализованы у типа
Human
, а также метод
fly
реализован непосредственно у
Human
Когда мы вызываем fly у экземпляра
Human
, то компилятор по умолчанию вызывает метод, который непосредственно реализован для типа, как показано в листинге 19-17.
Файл: src/main.rs
Листинг 19-17: Вызов
fly
у экземпляра
Human
Запуск этого кода напечатает
*waving arms furiously*
, показывая, что Rust называется метод fly реализованный непосредственно у
Human
Чтобы вызвать методы fly у типажа
Pilot или типажа
Wizard нужно использовать более явный синтаксис, указывая какой метод fly мы имеем в виду. Листинг 19-18
демонстрирует такой синтаксис.
Файл: src/main.rs
Листинг 19-18: Указание какой метода
fly
мы хотим вызвать
Указание имени типажа перед именем метода проясняет компилятору Rust, какую именно реализацию fly мы хотим вызвать. Мы могли бы также написать
Human::fly(&person)
, что эквивалентно используемому нами person.fly()
в листинге
19-18, но это писание немного длиннее, когда нужна неоднозначность.
Выполнение этого кода выводит следующее:
Поскольку метод fly принимает параметр self
, если у нас было два типа оба реализующих один типаж, то Rust может понять, какую реализацию типажа fn main
() { let person = Human; person.fly();
} fn main
() { let person = Human;
Pilot::fly(&person);
Wizard::fly(&person); person.fly();
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
использовать в зависимости от типа self
Однако ассоциированные функции являющиеся частью типажей не имеют self параметра. Когда два типа в одной области видимости реализуют такой типаж, Rust не может выяснить, какой тип вы имеете в виду если вы не используете полностью
квалифицированный синтаксис (fully qualified). Например, типаж
Animal в листинге 19-19
имеет: ассоциированную функцию baby_name
, реализацию типажа
Animal для структуры
Dog и ассоциированную функцию baby_name
, объявленную напрямую у структуры
Dog
Файл: src/main.rs
Листинг 19-19: Типаж с ассоциированной функцией и тип с ассоциированной функцией с тем же именем,
которая тоже реализует типаж
Этот код для приюта для животных, который хочет назвать всех щенков именем Spot, что реализовано в ассоциированной функции baby_name
, которая определена для
Dog
. Тип
Dog также реализует типаж
Animal
, который описывает характеристики, которые есть у всех животных. Маленьких собак называют щенками, и это выражается в реализации
Animal у
Dog в функции baby_name ассоциированной с типажом
Animal
В main мы вызываем функцию
Dog::baby_name
, которая вызывает ассоциированную функцию определённую напрямую у
Dog
. Этот код печатает следующее:
trait
Animal
{ fn baby_name
() ->
String
;
} struct
Dog
; impl
Dog { fn baby_name
() ->
String
{
String
::from(
"Spot"
)
}
} impl
Animal for
Dog { fn baby_name
() ->
String
{
String
::from(
"puppy"
)
}
} fn main
() { println!
(
"A baby dog is called a {}"
, Dog::baby_name());
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot

Этот вывод является не тем, что мы хотели получить. Мы хотим вызвать функцию baby_name
, которая является частью типажа
Animal реализованного у
Dog
, так чтобы код печатал
A baby dog is called a puppy
. Техника указания имени типажа использованная в листинге 19-18 здесь не помогает; если мы изменим main код как в листинге 19-20, мы получим ошибку компиляции.
Файл: src/main.rs
Листинг 19-20. Попытка вызвать функцию
baby_name
из типажа
Animal
, но Rust не знает какую реализацию
использовать
Так как
Animal::baby_name является ассоциированной функцией не имеющей self параметра в сигнатуре, а не методом, то Rust не может понять, какую реализацию
Animal::baby_name мы хотим вызвать. Мы получим эту ошибку компилятора:
Чтобы устранить неоднозначность и сказать Rust, что мы хотим использовать реализацию
Animal для
Dog
, нужно использовать полный синтаксис. Листинг 19-21
демонстрирует, как использовать полный синтаксис.
Файл: src/main.rs
Листинг 19-21: Использование полностью квалифицированного синтаксиса для указания, что мы мы хотим
вызвать функцию
baby_name
у типажа
Animal
реализованную в
Dog
Мы указываем аннотацию типа в угловых скобках, которая указывает на то что мы хотим вызвать метод baby_name из типажа
Animal реализованный в
Dog
, также указывая что мы хотим рассматривать тип
Dog в качестве
Animal для вызова этой функции. Этот код теперь напечатает то, что мы хотим:
fn main
() { println!
(
"A baby dog is called a {}"
, Animal::baby_name());
}
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0283]: type annotations needed
-->
src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer type
|
= note: cannot satisfy `_: Animal`
For more information about this error, try `rustc --explain E0283`. error: could not compile `traits-example` due to previous error fn main
() { println!
(
"A baby dog is called a {}"
, Animal>::baby_name());
}

В общем, полностью квалифицированный синтаксис определяется следующим образом:
Для ассоциированных функций при их вызове не будет receiver
(объекта приёмника), а будет только список аргументов. Вы можете использовать полностью квалифицированный синтаксис везде, где вызываете функции или методы. Тем не менее,
разрешается опустить любую часть этого синтаксиса, которую Rust может понять из другой информации в программе. Необходимость использования этого наиболее подробного синтаксиса возникает только в тех случаях, когда есть несколько реализаций, которые используют одинаковое имя и Rust нуждается в помощи для определения, какой вариант реализации вы хотите вызвать.
Использование супер типажей для требования функциональности
одного типажа в рамках другого типажа
Иногда вам может понадобиться, чтобы один типаж использовал функциональность другого типажа. В этом случае нужно полагаться на зависимый типаж, который также реализуется. Типаж на который вы полагаетесь, является супер типажом типажа,
который реализуете вы.
Например, мы хотим создать типаж
OutlinePrint с методом outline_print
, который будет печатать значение обрамлённое звёздочками. Мы хотим чтобы структура
Point реализующая типаж
Display вывела на печать
(x, y)
при вызове outline_print у
экземпляра
Point
, который имеет значение
1
для x
и значение
3
для y
. Она должна напечатать следующее:
В реализации outline_print мы хотим использовать функциональность типажа
Display
. Поэтому нам нужно указать, что типаж
OutlinePrint будет работать только для типов, которые также реализуют
Display и предоставляют функциональность, которая нужна в
OutlinePrint
. Мы можем сделать это в объявлении типажа, указав
OutlinePrint: Display
. Этот метод похож на добавление ограничения в типаж. В
листинге 19-22 показана реализация типажа
OutlinePrint
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
Trait>::function(receiver_if_method, next_arg, ...);
**********
* *
* (1, 3) *
* *
**********

Файл: src/main.rs
Листинг 19-22: Реализация типажа
OutlinePrint
которая требует функциональности типажа
Display
Поскольку мы указали, что типаж
OutlinePrint требует типажа
Display
, мы можем использовать функцию to_string
, которая автоматически реализована для любого типа реализующего
Display
. Если бы мы попытались использовать to_string не добавляя двоеточие и не указывая типаж
Display после имени типажа, мы получили бы сообщение о том, что метод с именем to_string не был найден у типа
&Self в текущей области видимости.
Давайте посмотрим что происходит, если мы пытаемся реализовать типаж
OutlinePrint для типа, который не реализует
Display
, например структура
Point
:
Файл: src/main.rs
Мы получаем сообщение о том, что требуется реализация
Display
, но её нет:
use std::fmt; trait
OutlinePrint
: fmt::Display { fn outline_print
(&
self
) { let output = self
.to_string(); let len = output.len(); println!
(
"{}"
,
"*"
.repeat(len +
4
)); println!
(
"*{}*"
,
" "
.repeat(len +
2
)); println!
(
"* {} *"
, output); println!
(
"*{}*"
,
" "
.repeat(len +
2
)); println!
(
"{}"
,
"*"
.repeat(len +
4
));
}
} struct
Point
{ x: i32
, y: i32
,
} impl
OutlinePrint for
Point {}

Чтобы исправить, мы реализуем
Display у структуры
Point и выполняем требуемое ограничение
OutlinePrint
, вот так:
Файл: src/main.rs
Тогда реализация типажа
OutlinePrint для структуры
Point будет скомпилирована успешно и мы можем вызвать outline_print у экземпляра
Point для отображения значения обрамлённое звёздочками.
Шаблон Newtype для реализация внешних типажей у внешних типов
В разделе "Реализация типажа у типа"
главы 10, мы упоминали "правило сироты" (orphan rule), которое гласит, что разрешается реализовать типаж у типа, если либо типаж, либо тип являются локальными для нашего крейта. Можно обойти это ограничение,
используя шаблон нового типа (newtype pattern), который включает в себя создание нового типа в кортежной структуре. (Мы рассмотрели кортежные структуры в разделе "Использование структур кортежей без именованных полей для создания различных типов"
главы 5.) Структура кортежа будет иметь одно поле и будет тонкой оболочкой для типа которому мы хотим реализовать типаж. Тогда тип оболочки является локальным для нашего крейта и мы можем реализовать типаж для локальной обёртки. Newtype это термин, который происходит от языка программирования Haskell. В нем нет ухудшения
$
cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0277]: `Point` doesn't implement `std::fmt::Display`
-->
src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty- print) instead note: required by a bound in `OutlinePrint`
-->
src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
For more information about this error, try `rustc --explain E0277`. error: could not compile `traits-example` due to previous error use std::fmt; impl fmt::Display for
Point { fn fmt
(&
self
, f: &
mut fmt::Formatter) -> fmt::
Result
{ write!
(f,
"({}, {})"
, self
.x, self
.y)
}
}
производительности времени выполнения при использовании этого шаблона и тип оболочки исключается во время компиляции.
В качестве примера, мы хотим реализовать типаж
Display для типа
Vec
, где "правило сироты" (orphan rule) не позволяет нам этого делать напрямую, потому что типаж
Display и тип
Vec
объявлены вне нашего крейта. Мы можем сделать структуру
Wrapper
, которая содержит экземпляр
Vec
; тогда мы можем реализовать
Display у структуры
Wrapper и использовать значение
Vec
как показано в листинге
19-23.
Файл: src/main.rs
Листинг 19-23. Создание типа
Wrapper
Vec
для реализации
Display
Реализация
Display использует self.0
для доступа к внутреннему
Vec
, потому что
Wrapper это структура кортежа, а
Vec
это элемент с индексом 0 в кортеже. Затем мы можем использовать функциональные возможности типа
Display у
Wrapper
Недостатком использования этой техники является то, что
Wrapper является новым типом, поэтому он не имеет методов для значения, которое он держит в себе. Мы должны были бы реализовать все методы для
Vec
непосредственно во
Wrapper
, так чтобы эти методы делегировались внутреннему self.0
, что позволило бы нам обращаться с
Wrapper точно так же, как с
Vec
. Если бы мы хотели, чтобы новый тип имел каждый метод имеющийся у внутреннего типа, реализуя типаж
Deref
(обсуждается в разделе "Работа с умными указателями как с обычными ссылками с помощью
Deref типажа"
главы 15) у
Wrapper для возвращения внутреннего типа, то это было бы решением. Если мы не хотим, чтобы тип
Wrapper имел все методы внутреннего типа,
например, для ограничения поведения типа
Wrapper
, то пришлось бы вручную реализовать только те методы, которые нам нужны.
Теперь вы знаете, как используется newtype шаблон по отношению к типажам; это также полезный шаблон, даже когда типажи не используются. Давайте переключимся и посмотрим на некоторые продвинутые способы взаимодействия с системой типов Rust.
use std::fmt; struct
Wrapper
(
Vec
<
String
>); impl fmt::Display for
Wrapper { fn fmt
(&
self
, f: &
mut fmt::Formatter) -> fmt::
Result
{ write!
(f,
"[{}]"
, self
0
.join(
", "
))
}
} fn main
() { let w = Wrapper(
vec!
[
String
::from(
"hello"
),
String
::from(
"world"
)]); println!
(
"w = {}"
, w);
}

Расширенные типы
Система типов Rust имеет некоторые возможности, которые мы упоминали в этой книге,
но ещё не обсуждали. Мы начнём с обсуждения новых типов (newtypes) в целом, по мере изучения того, почему новые типы полезны в качестве типов. Затем мы перейдём к псевдонимам, возможности похожей на новые типы (newtypes), но с немного другой семантикой. Мы также обсудим тип
!
и с динамическими типами (dynamically sized type).
Использование Newtype шаблона для безопасности типов и
реализации абстракций
Примечание. В следующем разделе предполагается, что вы прочитали предыдущий раздел "Использование шаблона Newtype для реализации внешних типажей у внешних типов"
Шаблон newtype полезен для задач помимо тех, которые мы обсуждали до сих пор,
включая статическое обеспечение того, чтобы значения никогда не путались и указывали единицы значения. Вы видели пример использования newtype для обозначения единиц в листинге 19-15. Вспомним, что структуры
Millimeters и
Meters содержат обёрнутые значения u32
в newtype. Если бы мы написали функцию с параметром типа
Millimeters
, мы не смогли бы скомпилировать программу, которая случайно пыталась вызвать эту функция со значением типа
Meters или обычным u32
Другое использование шаблона newtype - абстрагирование от некоторых деталей реализации типа: новый тип может предоставлять открытый API, отличный от API
приватного внутреннего типа, если мы напрямую использовали новый тип для ограничения доступного функционала, например.
Варианты шаблона (Newtypes) также могут скрывать внутреннюю реализацию.
Например, мы могли бы предоставить тип
People для оборачивания типа
HashMapString>
, которой хранит идентификатор человека связанного с его именем. Код использующий
People будет взаимодействовать только с предоставляемым нами открытым API, например метод добавления строки имени в коллекцию
People
; этому коду не понадобилось бы знать, что мы внутри присваиваем ID код типа i32
именам.
Шаблон newtype - это лёгкий способ добиться инкапсуляции, скрыть детали реализации,
которые мы обсуждали в разделе "Инкапсуляция, которая скрывает детали реализации"
главы 17.
Создание синонимов типа с помощью псевдонимов типа

Наряду с шаблоном newtype, Rust предоставляет возможность объявить псевдоним типа
чтобы дать существующему типу другое имя. Для этого мы используем ключевое слово type
. Например, мы можем создать псевдоним типа
Kilometers для i32
следующим образом:
Теперь псевдоним
Kilometers является синонимом для i32
; в отличие от типов
Millimeters и
Meters
, которые мы создали в листинге 19-15,
Kilometers не являются отдельными, новыми типами. Значения с типом
Kilometers будут обрабатываться так же, как значения типа i32
:
Поскольку
Kilometers и i32
являются одинаковым типом, мы можем сложить значения обоих типы и мы можем передать значения
Kilometers в функции, которые принимают параметры типа i32
. Однако, используя этот метод, мы не получаем преимуществ проверки типа, которые доступны в шаблоне newtype, обсуждавшемся ранее.
Синонимы в основном используются для уменьшения повторяемости. Например, у нас есть тип:
Запись этого длинного типа в сигнатурах функций и в виде аннотаций типов по всему коду может быть утомительной и приводить к ошибкам. Представьте, что у вас есть проект, полный кодом как в листинге 19-24.
Листинг 19-24: Использование длинного типа во многих местах
Псевдоним типа делает этот код более управляемым за счёт сокращения повторений. В
листинге 19-25 мы представили псевдоним
Thunk для "многословного" типа и теперь можем заменить все использования такого типа на более короткий псевдонимом
Thunk type
Kilometers
= i32
; type
Kilometers
= i32
; let x: i32
=
5
; let y: Kilometers =
5
; println!
(
"x + y = {}"
, x + y);
Box
<
dyn
Fn
() +
Send
+
'static
> let f:
Box
<
dyn
Fn
() +
Send
+
'static
> =
Box
::new(|| println!
(
"hi"
)); fn takes_long_type
(f:
Box
<
dyn
Fn
() +
Send
+
'static
>) {
// --snip--
} fn returns_long_type
() ->
Box
<
dyn
Fn
() +
Send
+
'static
> {
// --snip--
}

Листинг 19-25: Представление псевдонима
Thunk
для уменьшения количества повторений
Этот код намного легче читать и писать! Выбор значимого имени для псевдоним типа может также помочь сообщить о ваших намерениях (thunk является словом для кода,
который будет вычисляться позднее, так что это подходящее название для замыкания,
которое сохраняется).
Псевдоним типы также обычно используются с типом
Result
для сокращения повторения. Рассмотрим модуль std::io в стандартной библиотеке. Операции ввода/
вывода часто возвращают тип
Result
для обработки ситуаций, когда операция не выполняется из-за ошибки. Эта библиотека имеет структуру std::io::Error которая представляет все возможные ошибки ввода/вывода. Многие функции в библиотеке std::io будут возвращать
Result
, где
E
- это std::io::Error
, например, такие как функции в
Write типаже:
Тип
Result<..., Error>
многократно повторяется. Таким образом, std::io имеет этот тип как объявление псевдонима:
Поскольку это объявление находится в модуле std::io
, мы можем использовать полностью квалифицированный псевдоним std::io::Result
, что является
ResultE>
с типом
E
заполненным типом std::io::Error
. Сигнатуры функций типажа
Write в
конечном итоге выглядят как:
type
Thunk
=
Box
<
dyn
Fn
() +
Send
+
'static
>; let f: Thunk =
Box
::new(|| println!
(
"hi"
)); fn takes_long_type
(f: Thunk) {
// --snip--
} fn returns_long_type
() -> Thunk {
// --snip--
} use std::fmt; use std::io::Error; pub trait
Write
{ fn write
(&
mut self
, buf: &[
u8
]) ->
Result
<
usize
, Error>; fn flush
(&
mut self
) ->
Result
<(), Error>; fn write_all
(&
mut self
, buf: &[
u8
]) ->
Result
<(), Error>; fn write_fmt
(&
mut self
, fmt: fmt::Arguments) ->
Result
<(), Error>;
} type
Result
= std::result::
Result
;

Псевдоним типа помогает двумя способами: он облегчает написание кода и даёт нам согласованный интерфейс для всего из std::io
. Поскольку это псевдоним, то это просто ещё один тип
Result
, что означает, что с ним мы можем использовать любые методы, которые работают с
Result
, а также специальный синтаксис вроде
?
оператора.
Тип Never, который никогда не возвращается
Rust имеет специальный тип с названием
!
, который известен в теории типов как
пустой тип (empty type), потому что у него нет значений. Мы предпочитаем называть его тип никогда (never type), потому что он стоит на месте возвращаемого типа, такая функция никогда не возвращает управление. Вот пример:
Этот код читается как «функция bar никогда не возвращается». Функции, которые никогда не возвращаются называются расходящимися функциями (diverging functions).
Нельзя создавать значения типа
!
, так как bar никогда не может вернуться.
Но для чего нужен тип, для которого вы никогда не сможете создать значения?
Напомним код из листинга 2-5; мы воспроизвели его часть здесь в листинге 19-26.
Листинг 19-26: Сопоставление
match
с веткой, которая заканчивается
continue
В то время мы опустили некоторые детали в этом коде. В главе 6 раздела "Оператор управления потоком match
"
мы обсуждали, что все ветви match должны возвращать одинаковый тип. Например, следующий код не работает:
pub trait
Write
{ fn write
(&
mut self
, buf: &[
u8
]) ->
Result
<
usize
>; fn flush
(&
mut self
) ->
Result
<()>; fn write_all
(&
mut self
, buf: &[
u8
]) ->
Result
<()>; fn write_fmt
(&
mut self
, fmt: fmt::Arguments) ->
Result
<()>;
} fn bar
() -> ! {
// --snip--
} let guess: u32
= match guess.trim().parse() {
Ok
(num) => num,
Err
(_) => continue
,
}; let guess = match guess.trim().parse() {
Ok
(_) =>
5
,
Err
(_) =>
"hello"
,
};

Тип guess в этом коде должен быть целым числом и строкой и Rust требует, чтобы guess имел только один тип. Так что тогда возвращает код continue
? Как нам разрешили вернуть u32
из одной ветки и иметь другую ветку заканчивающуюся на continue в
листинге 19-26?
Как вы уже возможно догадались, continue имеет значение
!
. То есть, когда Rust вычисляет тип guess
, он смотрит на обе сопоставляемые ветки, первая со значением u32
и последняя со значением
!
. Так как
!
никогда не может иметь значение, то Rust решает что типом guess является тип u32
Формальным способом описания этого поведения является то, что выражения типа
!
могу быть приведены (coerced) к любому другому типу. Нам разрешено закончить сопоставление этой match ветки с помощью continue
, потому что continue не возвращает значение; вместо этого она передаёт контроль обратно в начало цикла,
поэтому в случае
Err мы никогда не присваиваем guess значение.
Never тип полезен также с макросом panic!
. Помните, функцию unwrap
, которую мы вызываем для значений
Option
, чтобы создать значение или вызвать панику? Вот её
определение:
В этом коде происходит то же самое, что и в выражении match из листинга 19-26: Rust видит, что val имеет тип
T
и panic!
имеет тип
!
, поэтому общим результатом match выражения является
T
. Этот код работает, потому что panic!
не производит значения;
он завершает выполнение программы. В случае
None
, мы не будем возвращать значение из unwrap
, поэтому этот код действительный.
Последнее выражение, которое имеет тип
!
это loop
:
Здесь цикл никогда не заканчивается, так что
!
(never type) является значением выражения. Тем не менее, это не будет правдой, если мы добавим в цикл break
, потому что цикл мог бы завершится, когда дело дойдёт до break impl

Option
{ pub fn unwrap
(
self
) -> T { match self
{
Some
(val) => val,
None
=> panic!
(
"called `Option::unwrap()` on a `None` value"
),
}
}
} print!
(
"forever "
); loop
{ print!
(
"and ever "
);
}

Динамические типы и Sized типаж
В связи с необходимостью Rust знать определённые детали, например, сколько места выделять для значения определённого типа, то существует краеугольный камень его системы типов, который может сбивать с толку. Это концепция динамических типов
(dynamically sized types). Иногда она упоминается как DST или безразмерные типы (unsized types), эти типы позволяют писать код, используя значения, чей размер известен только во время выполнения.
Давайте углубимся в детали динамического типа str
, который мы использовали на протяжении всей книги. Все верно, не типа
&str
, а типа str самого по себе, который является DST. Мы не можем знать, какой длины строка до момента времени выполнения,
то есть мы не можем создать переменную типа str и не можем принять аргумент типа str
. Рассмотрим следующий код, который не работает:
Rust должен знать, сколько памяти выделить для любого значения конкретного типа и все значения типа должны использовать одинаковый объем памяти. Если Rust позволил бы нам написать такой код, то эти два значения str должны были бы занимать одинаковое количество памяти. Но они имеют разную длину: s1
нужно 12 байтов памяти, а для s2
нужно 15. Вот почему невозможно создать переменную имеющую динамический тип.
Так что же нам делать? В этом случае вы уже знаете ответ: мы делаем типы s1
и s2
в виде типа
&str
, а не str
. Напомним, что в разделе "Строковые срезы"
главы 4, мы сказали, что структура данных срез хранит начальную позицию и длину среза.
Таким образом, хотя
&T
является единственным значением, которое хранит адрес памяти где находится тип
T
, тип
&str является двумя значениями: адресом str и его длиной. Таким образом, мы можем знать размер значения
&str во время компиляции:
это двойная длина от типа usize
. То есть мы всегда знаем размер
&str
, неважно какой длины является строка на которую она ссылается. В общем, это способ которым в Rust используются динамические типы: у них есть дополнительные метаданные в которых хранится размер динамической информации. Золотое правило динамических типов в том, что мы всегда должны ставить значения динамических типов позади некоторого указателя.
Можно комбинировать str со всеми видами указателей: например,
Box
или
Rc
. На самом деле, вы видели это раньше, но с другим динамическим типом:
типажом. Каждый типаж является динамическим типом к которому можно обратиться используя имя типажа. В разделе "Использование объектов-типажей, которые разрешаю использовать разные значения типов"
главы 17, мы упоминали, что для использования типажей в качестве объектов-типажей мы должны поместить их за указателем, например
&dyn Trait или
Box
(
Rc
тоже будет работать).
let s1: str
=
"Hello there!"
; let s2: str
=
"How's it going?"
;

Для работы с DST в Rust есть особый типаж, называемый
Sized для определения,
известен ли размер типа во время компиляции. Этот типаж автоматически реализуется для всех типов, чей размер известен во время компиляции. Кроме того, Rust неявно добавляет ограничение
Sized в каждую обобщённую функцию. То есть определение обобщённой функции написанное как:
на самом деле рассматривается как если бы мы написали её в виде:
По умолчанию обобщённые функции будут работать только с типами чей размер известен в время компиляции. Тем не менее, можно использовать следующий специальный синтаксис, чтобы ослабить это ограничение:
Ограничение на типаж
?Sized означает «
T
может или не может быть
Sized
», и это обозначение имеет приоритет по умолчанию. Общие типы должны иметь известный размер во время компиляции. Синтаксис
?Trait с таким значением доступен только для
Sized
, но не для любых других типажей.
Также обратите внимание, что мы поменяли тип параметра t
с
T
на
&T
. Поскольку тип мог бы не быть
Sized
, мы должны использовать его за каким-либо указателем. В в этом случае мы выбрали ссылку.
Далее мы поговорим о функциях и замыканиях!
fn generic
(t: T) {
// --snip--
} fn generic
Sized
>(t: T) {
// --snip--
} fn generic
Sized
>(t: &T) {
// --snip--
}

Продвинутые функции и замыкания
Наконец, мы рассмотрим некоторые дополнительные возможности, связанные с функциями и замыкания, которые включают указатели на функции и возврат замыканий.
Указатели функций
Мы говорили о том, как передавать замыкания в функции; но вы также можете передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать функцию, которую вы уже определили, а не объявлять новое замыкание. Указатель функции позволит использовать функции как аргументы к другим функциям. Функции приводятся (coerce) к типу fn
(с нижним регистром f), не к путать с типажом замыкания
Fn
. Тип fn называется указателем функции. Синтаксис для указания того, что параметр является указателем функции, похож на замыкание как показано в листинге 19-27.
Файл: src/main.rs
Листинг 19-27: Использование типа
fn
для принятия указателя функции в качестве аргумента
Этот код печатает
The answer is: 12
. Мы указываем, что параметр вызова f
для функции do_twice является fn
, которая принимает один параметр типа i32
и возвращает тип i32
. Затем мы можем вызвать f
в теле функции do_twice
. В main показано как можно передать имя функции add_one в качестве первого аргумента для функции do_twice
В отличие от замыканий, fn является типом, а не типажом, поэтому мы указываем fn как параметр типа напрямую, а не объявляем параметр обобщённого типа с одним из типажей
Fn в качестве ограничения типажа.
Указатели функций реализуют все три типажа замыканий (
Fn
,
FnMut и
FnOnce
), поэтому вы всегда можете передать указатель функции в качестве аргумента функции ожидающей замыкание. Лучше всего объявлять функции, используя обобщённый тип и fn add_one
(x: i32
) -> i32
{ x +
1
} fn do_twice
(f: fn
(
i32
) -> i32
, arg: i32
) -> i32
{ f(arg) + f(arg)
} fn main
() { let answer = do_twice(add_one,
5
); println!
(
"The answer is: {}"
, answer);
}
одним из типажей замыкания, так что ваши функции могут принимать либо функции,
либо замыкания.
Пример того, где вы хотели бы принимать только тип fn
, а не замыкания является взаимодействие с внешним кодом, который не имеет замыканий: функции в C могут принимать функции в качестве аргументов, но C не имеет замыканий.
Для примера того, где вы могли бы использовать либо замыкание, определённое как встроенное, либо именованную функцию, давайте посмотрим на использование map
Для использования функции map
, чтобы превратить вектор чисел в вектор строк, мы могли бы использовать замыкание, как здесь:
Или мы могли бы назвать функцию вместо замыкания в качестве аргумента при вызове map
, как здесь:
Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Расширенные типажи"
, потому что доступно несколько функций с именем to_string
. Здесь мы используем функцию to_string определённую в типаже
ToString
, который реализован в стандартной библиотеке для любого типа реализующего типаж
Display
У нас есть ещё один полезный шаблон, который использует детали реализации структур кортежей (tuple structs) и вариантов перечислений структур кортежей (tuple-struct enum).
Эти типы используют
()
в качестве синтаксиса инициализатора, который выглядит как вызов функции. Инициализаторы на самом деле реализованы как функции,
возвращающие экземпляр, который построен из их аргументов. Мы можем использовать эти функции инициализаторы как указатели на функции, которые реализуют типажи замыканий, что означает мы можем указать инициализирующие функции в качестве аргументов для методов, которые принимают замыкания, например:
Здесь мы создаём экземпляры
Status::Value
, используя каждое значение u32
в диапазоне (0..20), с которым вызывается map с помощью функции инициализатора
Status::Value
. Некоторые люди предпочитают этот стиль, а некоторые предпочитают let list_of_numbers = vec!
[
1
,
2
,
3
]; let list_of_strings:
Vec
<
String
> = list_of_numbers.iter().map(|i| i.to_string()).collect(); let list_of_numbers = vec!
[
1
,
2
,
3
]; let list_of_strings:
Vec
<
String
> = list_of_numbers.iter().map(
ToString
::to_string).collect(); enum
Status
{
Value(
u32
),
Stop,
} let list_of_statuses:
Vec
= (
0u32 20
).map(Status::Value).collect();
использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее.
Возврат замыканий
Замыкания представлены типажами, что означает невозможность напрямую вернуть замыкания. В большинстве случаев, когда вы возможно хотите вернуть типаж, вы вместо этого используете конкретный тип, который реализует типаж в качестве возвращаемого значения функции. Но вы не можете сделать этого с замыканиями, потому что у них нет конкретного типа, который можно вернуть; не разрешается использовать указатель функции fn в качестве возвращаемого типа, например.
Следующий код пытается напрямую вернуть замыкание, но он не компилируется:
Ошибка компилятора выглядит следующим образом:
Ошибка снова ссылается на типаж
Sized
! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект:
fn returns_closure
() -> dyn
Fn
(
i32
) -> i32
{
|x| x +
1
}
$
cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example) error[E0746]: return type cannot have an unboxed trait object
-->
src/lib.rs:1:25
|
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
|
= note: for information on `impl Trait`, see
1   ...   50   51   52   53   54   55   56   57   ...   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 {
|


For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error fn returns_closure
() ->
Box
<
dyn
Fn
(
i32
) -> i32
> {
Box
::new(|x| x +
1
)
}

Этот код просто отлично компилируется. Для получения дополнительной информации об типаж-объектах обратитесь к разделу "Использование типаж-объектов которые допускают значения разных типов"
главы 17.
Далее давайте посмотрим на макросы!

Макросы
Мы использовали макросы, такие как println!
на протяжении всей этой книги, но мы не изучили полностью, что такое макрос и как он работает. Термин макрос относится к семейству возможностей в Rust. Это декларативные (declarative) макросы с помощью macro_rules!
и три вида процедурных (procedural) макросов:
Пользовательские (выводимые)
#[derive]
макросы, которые указывают код добавленный с помощью derive атрибута, используемые для структур и перечислений
Макросы подобные атрибутам (attribute-like), которые определяют настраиваемые атрибуты, используемые для любого элемента языка
Функционально подобные (function-like) макросы, которые выглядят как вызовы функций, но работают с TokenStream
Мы поговорим о каждом из них по очереди, но сначала давайте рассмотрим, зачем вообще нужны макросы, если есть функции.
Разница между макросами и функциями
По сути, макросы являются способом написания кода, который записывает другой код,
что известно как мета программирование. В приложении C мы обсуждаем атрибут derive
, который генерирует за вас реализацию различных типажей. Вы также использовали макросы println!
и vec!
в книге. Все эти макросы раскрываются для генерации большего количества кода, чем исходный код написанный вами вручную.
Мета программирование полезно для уменьшения объёма кода, который вы должны написать и поддерживать, что также является одним из предназначений функций.
Однако макросы имеют некоторые дополнительные возможности, которых функции не имеют.
Сигнатура функции должна объявлять некоторое количество и тип этих параметров имеющихся у функции. Макросы, с другой стороны, могут принимать переменное число параметров: мы можем вызвать println!("hello")
с одним аргументом или println!
("hello {}", name)
с двумя аргументами. Также макросы раскрываются до того как компилятор интерпретирует смысл кода, поэтому макрос может, например, реализовать типаж заданного типа. Функция этого не может, потому что она вызывается во время выполнения и типаж должен быть реализован во время компиляции.
Обратной стороной реализации макроса вместо функции является то, что определения макросов являются более сложными, чем определения функций, потому что вы создаёте
Rust код, который записывает другой Rust код. Из-за этой косвенности, объявления макросов, как правило, труднее читать, понимать и поддерживать, чем объявления функций.


Другое важное различие между макросами и функциями заключается в том, что вы должны объявить макросы или добавить их в область видимости прежде чем можете вызывать их в файле, в отличии от функций, которые вы можете объявить где угодно и вызывать из любого места.
Декларативные макросы с macro_rules! для общего мета
программирования
Наиболее широко используемой формой макросов в Rust являются декларативные
макросы. Они также иногда упоминаются как "макросы на примере", "
macro_rules!
макрос" или просто "макросы". По своей сути декларативные макросы позволяют писать нечто похожее на выражение match в Rust. Как обсуждалось в главе 6, match выражения являются управляющими структурами, которые принимают некоторое выражение,
результат значения выражения сопоставляют с шаблонами, а затем запускают код для сопоставляемой ветки. Макросы также сравнивают значение с шаблонами, которые связаны с конкретным кодом: в этой ситуации значение является литералом исходного кода Rust, переданным в макрос. Шаблоны сравниваются со структурами этого исходного кода и при совпадении код, связанный с каждым шаблоном, заменяет код переданный макросу. Все это происходит во время компиляции.
Для определения макроса используется конструкция macro_rules!
. Давайте рассмотрим,
как использовать macro_rules!
глядя на то, как объявлен макрос vec!
. В главе 8
рассказано, как можно использовать макрос vec!
для создания нового вектора с определёнными значениями. Например, следующий макрос создаёт новый вектор,
содержащий три целых числа:
Мы также могли использовать макрос vec!
для создания вектора из двух целых чисел или вектора из пяти строковых срезов. Мы не смогли бы использовать функцию, чтобы сделать то же самое, потому что мы не знали бы заранее количество или тип значений.
В листинге 19-28 приведено несколько упрощённое определение макроса vec!
Файл: src/lib.rs let v:
Vec
<
u32
> = vec!
[
1
,
2
,
3
];

Листинг 19-28: Упрощённая версия определения макроса
vec!
Примечание: фактическое определение макроса vec!
в стандартной библиотеке включает сначала код для предварительного выделения правильного объёма памяти. Этот код является оптимизацией, которую мы здесь не включаем, чтобы сделать пример проще.
Аннотация
#[macro_export]
указывает, что данный макрос должен быть доступен всякий раз, когда крейт с объявленным макросом, добавлен в область видимости. Без этой аннотации макрос нельзя добавить в область видимости.
Затем мы начинаем объявление макроса с помощью macro_rules!
и имени макроса,
который объявляется без восклицательного знака. Название, в данном случае vec
, после которого следуют фигурные скобки, указывающие тело определения макроса.
Структура в теле макроса vec!
похожа на структуру match выражения. Здесь у нас есть одна ветвь с шаблоном
( $( $x:expr ),* )
, затем следует ветвь
=>
и блок кода,
связанный с этим шаблоном. Если шаблон сопоставлен успешно, то соответствующий блок кода будет сгенерирован. Учитывая, что данный код является единственным шаблоном в этом макросе, существует только один действительный способ сопоставления, любой другой шаблон приведёт к ошибке. Более сложные макросы будут иметь более чем одна ветвь.
Допустимый синтаксис шаблона в определениях макросов отличается от синтаксиса шаблона рассмотренного в главе 18, потому что шаблоны макроса сопоставляются со структурами кода Rust, а не со значениями. Давайте пройдёмся по тому, какие части шаблона в листинге 19-28 что означают; полный синтаксис макроса см. в ссылке
Во-первых, набор скобок охватывает весь шаблон. Далее идёт знак доллара (
$
), затем следует набор скобок, который захватывает значения, соответствующие шаблону в скобках для использования в коде замены. Внутри
$()
находится
$x:expr
, который соответствует любому выражению Rust и даёт выражению имя
$x
#[macro_export]
macro_rules!
vec {
( $( $x:expr ),* ) => {
{ let mut temp_vec =
Vec
::new();
$( temp_vec.push($x);
)* temp_vec
}
};
}


Запятая, следующая за
$()
указывает на то, что буквенный символ-разделитель запятой может дополнительно появиться после кода, который соответствует коду в
$()
Звёздочка
*
указывает, что шаблон соответствует ноль или больше раз тому, что предшествует
*
Когда вызывается этот макрос с помощью vec![1, 2, 3];
шаблон
$x соответствует три раза всем трём выражениям
1
,
2
и
3
Теперь давайте посмотрим на шаблон в теле кода, связанного с этой ветвью: temp_vec.push()
внутри
$()*
генерируется для каждой части, которая соответствует символу
$()
в шаблоне ноль или более раз в зависимости от того, сколько раз шаблон сопоставлен. Символ
$x заменяется на каждое совпадающее выражение. Когда мы вызываем этот макрос с vec![1, 2, 3];
, сгенерированный код, заменяющий этот вызов макроса будет следующим:
Мы определили макрос, который может принимать любое количество аргументов любого типа и может генерировать код для создания вектора, содержащего указанные элементы.
Есть несколько странных краевых случаев у макроса macro_rules!
. В будущем у Rust будет второй вид декларативного макроса, который будет работать аналогичным образом, но поправит некоторые из этих краевых случаев. После этого обновления macro_rules!
будет фактически устаревшим. Имея это в виду, а также тот факт, что большинство Rust программистов будут использовать макросы больше, чем сами писать
макросы, мы далее не будем обсуждать macro_rules!
. Чтобы узнать больше о том, как писать макросы, обратитесь к электронной документации или другим ресурсам, таким как
“The Little Book of Rust Macros”
Процедурные макросы для генерации кода из атрибутов
Вторая форма макросов - это процедурные макросы (procedural macros), которые действуют как функции (и являются типом процедуры). Процедурные макросы принимают некоторый код в качестве входных данных, работают над этим кодом и создают некоторый код в качестве вывода, а не выполняют сопоставления с шаблонами и замену кода другим кодом, как это делают декларативные макросы.
Все три вида процедурных макросов (пользовательские выводимые, похожие на атрибуты и похожие на функции) все работают аналогично.
{ let mut temp_vec =
Vec
::new(); temp_vec.push(
1
); temp_vec.push(
2
); temp_vec.push(
3
); temp_vec
}

При создании процедурных макросов объявления должны находиться в собственном крейте специального типа. Это из-за сложных технических причин, которые мы надеемся будут устранены в будущем. Использование процедурных макросов выглядит как код в листинге 19-29, где some_attribute является заполнителем для использования специального макроса.
Файл: src/lib.rs
Листинг 19-29: Пример использования процедурного макроса
Функция, которая определяет процедурный макрос, принимает
TokenStream в качестве входных данных и создаёт
TokenStream в качестве вывода. Тип
TokenStream объявлен крейтом proc_macro
, включённым в Rust и представляет собой последовательность токенов. Это ядро макроса: исходный код над которым работает макрос, является входным
TokenStream
, а код создаваемый макросом является выходным
TokenStream
. К
функции имеет также прикреплённый атрибут, определяющий какой тип процедурного макроса мы создаём. Можно иметь несколько видов процедурных макросов в одном и том же крейте.
Давайте посмотрим на различные виды процедурных макросов. Начнём с пользовательского, выводимого (derive) макроса и затем объясним небольшие различия,
делающие другие формы отличающимися.
Как написать пользовательский derive макрос
Давайте создадим крейт с именем hello_macro
, который определяет типаж с именем
HelloMacro и имеет одну с ним ассоциированную функцию с именем hello_macro
Вместо того, чтобы пользователи нашего крейта самостоятельно реализовывали типаж
HelloMacro для каждого из своих типов, мы предоставим им процедурный макрос, чтобы они могли аннотировать свой тип с помощью атрибута
#[diverve(HelloMacro)]
и получили реализацию по умолчанию для функции hello_macro
. Реализация по умолчанию выведет
Hello, Macro! My name is TypeName!
, где
TypeName
- это имя типа,
для которого был определён этот типаж. Другими словами, мы напишем крейт,
использование которого позволит другому программисту писать код показанный в листинге 19-30.
Файл: src/main.rs use proc_macro;
#[some_attribute]
pub fn some_name
(input: TokenStream) -> TokenStream {
}


Листинг 19-30: Код, который сможет писать пользователь нашего крейта при использовании нашего
процедурного макроса
Этот код напечатает
Hello, Macro! My name is Pancakes!
, когда мы закончим. Первый шаг - создать новый, библиотечный крейт так:
Далее, мы определим типаж
HelloMacro и ассоциированную с ним функцию:
Файл: src/lib.rs
У нас есть типаж и его функция. На этом этапе пользователь крейта может реализовать типаж для достижения желаемой функциональности, так:
Тем не менее, ему придётся написать блок реализации для каждого типа, который он хотел использовать вместе с hello_macro
; а мы хотим избавить их от необходимости делать эту работу.
Кроме того, мы пока не можем предоставить функцию hello_macro с реализацией по умолчанию, которая будет печатать имя типа, для которого реализован типаж: Rust не имеет возможностей рефлексии (reflection), поэтому он не может выполнить поиск имени use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct
Pancakes
; fn main
() {
Pancakes::hello_macro();
}
$
cargo new hello_macro --lib pub trait
HelloMacro
{ fn hello_macro
();
} use hello_macro::HelloMacro; struct
Pancakes
; impl
HelloMacro for
Pancakes { fn hello_macro
() { println!
(
"Hello, Macro! My name is Pancakes!"
);
}
} fn main
() {
Pancakes::hello_macro();
}
типа во время выполнения кода. Нам нужен макрос для генерации кода во время компиляции.
Следующим шагом является определение процедурного макроса. На момент написания этой статьи процедурные макросы должны быть в собственном крейте. Со временем это ограничение может быть отменено. Соглашение о структурировании крейтов и макросов является следующим: для крейта с именем foo
, его пользовательский, крейт с выводимым процедурным макросом называется foo_derive
. Давайте начнём с создания нового крейта с именем hello_macro_derive внутри проекта hello_macro
:
Наши два крейта тесно связаны, поэтому мы создаём процедурный макрос-крейт в каталоге крейта hello_macro
. Если мы изменим определение типажа в hello_macro
, то нам придётся также изменить реализацию процедурного макроса в hello_macro_derive
Два крейта нужно будет опубликованы отдельно и программисты, использующие эти крейты, должны будут добавить их как зависимости, а затем добавить их в область видимости. Мы могли вместо этого сделать так, что крейт hello_macro использует hello_macro_derive как зависимость и реэкспортирует код процедурного макроса.
Однако то, как мы структурировали проект, делает возможным программистам использовать hello_macro даже если они не хотят derive функциональность.
Нам нужно объявить крейт hello_macro_derive как процедурный макрос-крейт. Также понадобятся функционал из крейтов syn и quote
, как вы увидите через мгновение,
поэтому нам нужно добавить их как зависимости. Добавьте следующее в файл Cargo.toml
для hello_macro_derive
:
Файл: hello_macro_derive/Cargo.toml
Чтобы начать определение процедурного макроса, поместите код листинга 19-31 в ваш файл src/lib.rs крейта hello_macro_derive
. Обратите внимание, что этот код не скомпилируется пока мы не добавим определение для функции impl_hello_macro
Файл: hello_macro_derive/src/lib.rs
$
cargo new hello_macro_derive --lib
[lib]
proc-macro = true
[dependencies]
syn =
"1.0"
quote =
"1.0"