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

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

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

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

Добавлен: 11.01.2024

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

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


1   ...   41   42   43   44   45   46   47   48   ...   62

Листинг 16-15: Использование типа
Arc
для обёртывания
Mutex
, теперь несколько потоков могут
совместно владеть мьютексом
Код напечатает следующее:
Мы сделали это! Мы посчитали от 0 до 10, что может показаться не очень впечатляющим, но это позволило больше узнать про
Mutex
и безопасность потоков.
Вы также можете использовать структуру этой программы для выполнения более сложных операций, чем просто увеличение счётчика. Используя эту стратегию, вы можете разделить вычисления на независимые части, разделить эти части на потоки, а затем использовать
Mutex
, чтобы каждый поток обновлял конечный результат своей частью кода.
Обратите внимание, что если вы выполняете простые числовые операции, существуют более простые типы, чем
Mutex
, предоставляемые модулем std::sync::atomic стандартной библиотеки
. Эти типы обеспечивают безопасный многопоточный атомарный доступ для примитивных типов. В этом примере мы решили использовать
Mutex
с примитивным типом, чтобы сосредоточиться на том, как работает
Mutex
Сходства RefCell / Rc и Mutex / Arc
Вы могли заметить, что counter сам по себе не изменяемый (immutable), но мы можем получить изменяемую ссылку на значение внутри него; это означает, что
Mutex
use std::sync::{Arc, Mutex}; use std::thread; fn main
() { let counter = Arc::new(Mutex::new(
0
)); let mut handles = vec!
[]; for
_ in
0 10
{ let counter = Arc::clone(&counter); let handle = thread::spawn(
move
|| { let mut num = counter.lock().unwrap();
*num +=
1
;
}); handles.push(handle);
} for handle in handles { handle.join().unwrap();
} println!
(
"Result: {}"
, *counter.lock().unwrap());
}
Result: 10
обеспечивает внутреннюю изменяемость, также как и семейство
Cell типов. Мы использовали
RefCell
в главе 15, чтобы получить возможность изменять содержимое внутри
Rc
, теперь аналогичным образом мы используем
Mutex
для изменения содержимого внутри
Arc
Ещё одна деталь, на которую стоит обратить внимание: Rust не может защитить вас от всевозможных логических ошибок при использовании
Mutex
. Вспомните в главе 15,
что использование
Rc
сопряжено с риском создания ссылочной зацикленности, где два значения
Rc
ссылаются друг на друга, что приводит к утечкам памяти.
Аналогичным образом,
Mutex
сопряжён с риском создания взаимных блокировок
(deadlocks). Это происходит, когда операции необходимо заблокировать два ресурса и каждый из двух потоков получил одну из блокировок, заставляя оба потока ждать друг друга вечно. Если вам интересна тема взаимных блокировок, попробуйте создать программу Rust, которая её содержит; затем исследуйте стратегии устранения взаимных блокировок для мьютексов на любом языке и попробуйте реализовать их в Rust.
Документация стандартной библиотеки для
Mutex
и
MutexGuard предлагает полезную информацию.
Мы завершим эту главу, рассказав о типажах
Send и
Sync и о том, как мы можем использовать их с пользовательскими типами.

Расширенная многопоточность с помощью типажей
Sync
и Send
Интересно, что сам язык Rust имеет очень мало возможностей для многопоточности.
Почти все функции многопоточности о которых мы говорили в этой главе, были частью стандартной библиотеки, а не языка. Ваши варианты работы с многопоточностью не ограничиваются языком или стандартной библиотекой; Вы можете написать свой собственный многопоточный функционал или использовать возможности написанные другими.
Тем не менее, в язык встроены две концепции многопоточности: std::marker типажи
Sync и
Send
Разрешение передачи во владение между потоками с помощью Send
Маркерный типаж
Send указывает, что владение типом реализующим
Send
, может передаваться между потоками. Почти каждый тип Rust является типом
Send
, но есть некоторые исключения, вроде
Rc
: он не может быть
Send
, потому что если вы клонировали значение
Rc
и попытались передать владение клоном в другой поток,
оба потока могут обновить счётчик ссылок одновременно. По этой причине
Rc
реализован для использования в однопоточных ситуациях, когда вы не хотите платить за снижение производительности.
Следовательно, система типов Rust и ограничений типажа гарантируют, что вы никогда не сможете случайно небезопасно отправлять значение
Rc
между потоками. Когда мы попытались сделать это в листинге 16-14, мы получили ошибку, the trait Send is not implemented for Rc>
. Когда мы переключились на
Arc
, который является типом
Send
, то код скомпилировался.
Любой тип полностью состоящий из типов
Send автоматически помечается как
Send
Почти все примитивные типы являются
Send
, кроме сырых указателей, которые мы обсудим в главе 19.
Разрешение доступа из нескольких потоков с Sync
Маркерный типаж
Sync указывает, что на тип реализующий
Sync можно безопасно ссылаться из нескольких потоков. Другими словами, любой тип
T
является типом
Sync
,
если
&T
(ссылка на
T
) является типом
Send
, что означает что ссылку можно безопасно отправить в другой поток. Подобно
Send
, примитивные типы являются типом
Sync
, а типы полностью скомбинированные из типов
Sync
, также являются
Sync типом.
Умный указатель
Rc
не является
Sync типом по тем же причинам, по которым он не является
Send
. Тип
RefCell
(о котором мы говорили в главе 15) и семейство
связанных типов
Cell
не являются
Sync
. Реализация проверки заимствования,
которую делает тип
RefCell
во время выполнения программы не является поточно- безопасной. Умный указатель
Mutex
является типом
Sync и может использоваться для совместного доступа из нескольких потоков, как вы уже видели в разделе
«Совместное использование
Mutex
между несколькими потоками»
Реализация Send и Sync вручную небезопасна
Поскольку типы созданные из типажей
Send и
Sync автоматически также являются типами
Send и
Sync
, мы не должны реализовывать эти типажи вручную. Являясь маркерными типажами у них нет никаких методов для реализации. Они просто полезны для реализации инвариантов, связанных с многопоточностью.
Ручная реализация этих типажей включает в себя реализацию небезопасного кода Rust.
Мы поговорим об использовании небезопасного кода Rust в главе 19; на данный момент важная информация заключается в том, что для создания новых многопоточных типов,
не состоящих из частей
Send и
Sync необходимо тщательно продумать гарантии безопасности. В
Rustonomicon есть больше информации об этих гарантиях и о том как их соблюдать.
Итоги
Это не последний случай, когда вы увидите многопоточность в этой книге: проект в главе
20 будет использовать концепции этой главы для более реалистичного случая, чем небольшие примеры обсуждаемые здесь.
Как упоминалось ранее, поскольку в языке Rust очень мало того, с помощью чего можно управлять многопоточностью, многие решения реализованы в виде крейтов. Они развиваются быстрее, чем стандартная библиотека, поэтому обязательно поищите в
Интернете текущие современные крейты.
Стандартная библиотека Rust предоставляет каналы для передачи сообщений и типы умных указателей, такие как
Mutex
и
Arc
, которые можно безопасно использовать в многопоточных контекстах. Система типов и анализатор заимствований гарантируют, что код использующий эти решения не будет содержать гонки данных или недействительные ссылки. Получив компилирующийся код, вы можете быть уверены,
что он будет успешно работать в нескольких потоках без ошибок, которые трудно обнаружить в других языках. Многопоточное программирование больше не является концепцией, которую стоит опасаться: иди вперёд и сделай свои программы многопоточными безбоязненно!
Далее мы поговорим об идиоматичных способах моделирования проблем и структурирования решений по мере усложнения ваших программ на Rust. Кроме того,
мы обсудим как идиомы Rust связаны с теми, с которыми вы, возможно, знакомы по объектно-ориентированному программированию.

Возможности объектно-
ориентированного программирования в
Rust
Объектно-ориентированное программирование (ООП) — это способ построения программ. Объекты, как программная концепция, были введены в язык программирования Simula в 1960-х годах. Эти объекты повлияли на архитектуру программирования Алана Кея, в которой объекты передают сообщения друг другу.
Чтобы описать эту архитектуру, он ввёл термин объектно-ориентированное
программирование в 1967 году. Есть много конкурирующих определений ООП, и по некоторым из этих определений Rust является объектно-ориентированным, а по другим
— нет. В этой главе мы рассмотрим некоторые характеристики, которые обычно считаются объектно-ориентированными, и то, как эти характеристики транслируются в идиомы языка Rust. Затем мы покажем, как реализовать шаблон объектно- ориентированного проектирования в Rust, и обсудим компромиссы между этим вариантом и решением, использующим вместо этого некоторые сильные стороны Rust.

Характеристики объектно-ориентированных языков
В сообществе разработчиков нет согласия относительно того, какие особенности языка делают его объектно-ориентированным. На Rust повлияли многие парадигмы программирования, включая ООП; например, в главе 13 мы изучили вещи, пришедшие из функционального программирования. С некоторыми оговорками, ООП языки обладают некоторыми общими характеристики, а именно объектами, инкапсуляцией и наследованием. Давайте посмотрим, что означает каждая из этих характеристик, и поддерживает ли её Rust.
Объекты содержат данные и поведение
Книга «Приёмы объектно-ориентированного проектирования. Шаблоны проектирования» (1994), называемая также «книгой банды четырёх», является каталогом объектно-ориентированных шаблонов проектирования. Объектно-ориентированные программы определяются в ней следующим образом:
Объектно-ориентированные программы состоят из объектов. Объект объединяет данные и процедуры, которые работают с этими данными. Эти процедуры обычно называются методами или операциями.
В соответствии с этим определением, Rust является объектно-ориентированным языком:
в структурах и перечислениях содержатся данные, а в блоках impl определяются методы для них. Хотя структуры и перечисления, имеющие методы, не называются объектами,
они обеспечивают функциональность, соответствующую определению объектов в книге банды четырёх.
Инкапсуляция, скрывающая детали реализации
Другим аспектом, обычно связанным с объектно-ориентированным программированием, является идея инкапсуляции: детали реализации объекта недоступны для кода, использующего этот объект. Единственный способ взаимодействия с объектом — через его публичный интерфейс; код, использующий этот объект, не должен иметь возможности взаимодействовать с внутренними свойствами объекта и напрямую изменять его данные или поведение. Инкапсуляция позволяет изменять и реорганизовывать внутренние свойства объекта без необходимости изменять код,
который использует объект.
Как мы обсудили в главе 7, мы можем использовать ключевое слово pub чтобы решить,
какие модули, типы, функции и методы в нашем коде должны быть публичными; по умолчанию все остальное является приватным. Например, мы можем определить структуру
AveragedCollection
, которая имеет поле, содержащее вектор значений типа
i32
. Структура также может иметь поле содержащее среднее значение в векторе, так что всякий раз, когда кто-либо захочет получить среднее значение элементов вектора, нам не нужно вычислять его заново, Другими словами,
AveragedCollection будет кэшировать рассчитанное среднее значение для нас. В примере 17-1 приведено определение структуры
AveragedCollection
:
Файл: src/lib.rs
Листинг 17-1: структура
AveragedCollection
содержит список целых чисел и среднее значение элементов в
коллекции.
Обратите внимание, что структура помечена ключевым словом pub
, что позволяет другому коду её использовать, однако, поля внутри структуры остаются недоступными.
Это важно, потому что мы хотим гарантировать обновление среднего значения при добавлении или удалении элемента из списка. Мы можем получить нужное поведение,
определив в структуре методы add
, remove и average
, как показано в примере 17-2:
Файл: src/lib.rs
Листинг 17-2: Реализация публичных методов
add
,
remove
и
average
структуры
AveragedCollection pub struct
AveragedCollection
{ list:
Vec
<
i32
>, average: f64
,
} impl
AveragedCollection { pub fn add
(&
mut self
, value: i32
) { self
.list.push(value); self
.update_average();
} pub fn remove
(&
mut self
) ->
Option
<
i32
> { let result = self
.list.pop(); match result {
Some
(value) => { self
.update_average();
Some
(value)
}
None
=>
None
,
}
} pub fn average
(&
self
) -> f64
{ self
.average
} fn update_average
(&
mut self
) { let total: i32
= self
.list.iter().sum(); self
.average = total as f64
/ self
.list.len() as f64
;
}
}

Публичные методы add
, remove и average являются единственным способом получить или изменить данные в экземпляре
AveragedCollection
. Когда элемент добавляется в list методом add
, или удаляется с помощью метода remove
, код реализации каждого из этих методов вызывает приватный метод update_average
, который позаботится об обновлении поля average
Мы оставляем поля list и average приватными, чтобы внешний код не мог добавлять или удалять элементы непосредственно в поле list
; в противном случае поле average может оказаться не синхронизировано при изменении list
. Метод average возвращает значение в поле average
, что позволяет внешнему коду читать значение average
, но не изменять его.
Поскольку мы инкапсулировали детали реализации структуры
AveragedCollection
, мы можем легко изменить такие аспекты, как структура данных, в будущем. Например, мы могли бы использовать
HashSet
вместо
Vec
для поля list
. Благодаря тому,
что сигнатуры публичных методов add
, remove и average остаются неизменными, код,
использующий
AveragedCollection
, также не будет нуждаться в изменении. У нас бы не получилось этого достичь, если бы мы сделали поле list доступным внешнему коду:
HashSet
и
Vec
имеют разные методы для добавления и удаления элементов,
поэтому внешний код, вероятно, должен измениться, если он модифицирует list напрямую.
Если инкапсуляция является обязательным аспектом для определения языка как объектно-ориентированного, то Rust соответствует этому требованию. Возможность использовать или не использовать модификатор доступа pub для различных частей кода позволяет скрыть детали реализации.
Наследование как система типов и способ совместного использования
кода
Наследование — это механизм, с помощью которого объект может быть унаследовать элементы из определения другого объекта, то есть получить данные и поведение родительского объекта без необходимости повторно их определять.
Если язык должен иметь наследование, чтобы быть объектно-ориентированным, то Rust таким не является. Здесь нет способа определить структуру, наследующую поля и реализации методов родительской структуры, без использования макроса.
Однако, если вы привыкли иметь наследование в своём наборе инструментов для программирования, вы можете использовать другие решения в Rust, в зависимости от того, по какой причине вы изначально хотите использовать наследование.
Вы могли бы выбрать наследование по двум основным причинам. Одна из них - возможность повторного использования кода: вы можете реализовать определённое поведение для одного типа, а наследование позволит вам повторно использовать эту
реализацию для другого типа. В Rust для этого есть ограниченный способ,
использующий реализацию метода типажа по умолчанию, который вы видели в листинге 10-14, когда мы добавили реализацию по умолчанию в методе summarize типажа
Summary
. Любой тип, реализующий свойство
Summary будет иметь доступный метод summarize без дополнительного кода. Это похоже на то, как родительский класс имеет реализацию метода, и класс-наследник тоже имеет реализацию метода. Мы также можем переопределить реализацию по умолчанию для метода summarize
, когда реализуем типаж
Summary
, что похоже на дочерний класс, переопределяющий реализацию метода, унаследованного от родительского класса.
Вторая причина использования наследования относится к системе типов: чтобы иметь возможность использовать дочерний тип в тех же места, что и родительский. Эта возможность также называется полиморфизм и означает возможность подменять объекты во время исполнения, если они имеют одинаковые характеристики.
Полиморфизм
Для многих людей полиморфизм является синонимом наследования. Но на самом деле это более общая концепция, которая относится к коду, который может работать с данными разных типов. Для наследования эти типы обычно являются подклассами. Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным
параметрическим полиморфизмом.
Вместо этого Rust использует обобщённые типы для абстрагирования от типов, и ограничения типажей (trait bounds) для указания того, какие возможности эти типы должны предоставлять. Это иногда называют ограниченным параметрическим
полиморфизмом.
Наследование, как подход к разработке, в последнее время утратило популярность во многих языках программирования, поскольку часто существует риск, что мы будем наследовать код чаще, чем это необходимо. Подклассы не всегда должны обладать всеми характеристиками родительского класса, но при использовании наследования другого варианта нет. Это может сделать дизайн программы менее гибким. Кроме этого,
появляется возможность вызова у подклассов методов, которые не имеют смысла или вызывают ошибки, потому что эти методы неприменимы к подклассу. Кроме того, в некоторых языках разрешается только одиночное наследование (т.е. подкласс может наследоваться только от одного класса), что ещё больше ограничивает гибкость разработки программы.
По этим причинам в Rust применяется альтернативный подход, с использованием типажей-объектов вместо наследования. Давайте посмотрим как типажи-объекты
реализуют полиморфизм в Rust.

1   ...   42   43   44   45   46   47   48   49   ...   62

Использование типаж-объектов, допускающих
значения разных типов
В главе 8 мы упоминали, что одним из ограничений векторов является то, что они могут хранить элементы только одного типа. Мы создали обходное решение в листинге 8-9, где мы определили перечисление
SpreadsheetCell в котором были варианты для хранения целых чисел, чисел с плавающей точкой и текста. Это означало, что мы могли хранить разные типы данных в каждой ячейке и при этом иметь вектор, представляющий строку из ячеек. Это очень хорошее решение, когда наши взаимозаменяемые элементы вектора являются типами с фиксированным набором, известным при компиляции кода.
Однако иногда мы хотим, чтобы пользователь нашей библиотеки мог расширить набор типов, которые допустимы в конкретной ситуации. Чтобы показать как этого добиться,
мы создадим пример инструмента с графическим интерфейсом пользователя (GUI),
который просматривает список элементов, вызывает метод draw для каждого из них,
чтобы нарисовать его на экране - это обычная техника для инструментов GUI. Мы создадим библиотечный крейт с именем gui
, содержащий структуру библиотеки GUI.
Этот крейт мог бы включать некоторые готовые типы для использования, такие как
Button или
TextField
. Кроме того, пользователи такого крейта gui захотят создавать свои собственные типы, которые могут быть нарисованы: например, кто-то мог бы добавить тип
Image
, а кто-то другой добавить тип
SelectBox
Мы не будем реализовывать полноценную библиотеку GUI для этого примера, но покажем, как её части будут подходить друг к другу. На момент написания библиотеки мы не можем знать и определить все типы, которые могут захотеть создать другие программисты. Но мы знаем, что gui должен отслеживать множество значений различных типов и ему нужно вызывать метод draw для каждого из этих значений различного типа. Ему не нужно точно знать, что произойдёт, когда вызывается метод draw
, просто у значения будет доступен такой метод для вызова.
Чтобы сделать это на языке с наследованием, можно определить класс с именем
Component у которого есть метод с названием draw
. Другие классы, такие как
Button
,
Image и
SelectBox наследуются от
Component и следовательно, наследуют метод draw
Каждый из них может переопределить реализацию метода draw
, чтобы определить своё
пользовательское поведение, но платформа может обрабатывать все типы, как если бы они были экземплярами
Component и вызывать draw у них. Но поскольку в Rust нет наследования, нам нужен другой способ структурировать gui библиотеку, чтобы позволить пользователям расширять её новыми типами.

Определение типажа для общего поведения
Чтобы реализовать поведение, которое мы хотим иметь в gui
, определим типаж с именем
Draw
, который будет содержать один метод с названием draw
. Затем мы можем
определить вектор, который принимает типаж-объект. Типаж-объект указывает как на экземпляр типа, реализующего указанный типаж, так и на внутреннюю таблицу,
используемую для поиска методов типажа указанного типа во время выполнения. Мы создаём типаж-объект в таком порядке: используем какой-нибудь вид указателя,
например ссылку
&
или умный указатель
Box
, затем ключевое слово dyn
, а затем указываем соответствующий типаж. (Мы будем говорить о причине того, что типаж- объекты должны использовать указатель в разделе "Типы динамического размера и типаж
Sized
"
главы 19). Мы можем использовать типаж-объекты вместо универсального или конкретного типа. Везде, где мы используем типаж-объект, система типов Rust проверит во время компиляции, что любое значение, используемое в этом контексте, будет реализовывать нужный типаж у типаж-объекта. Следовательно, нам не нужно знать все возможные типы во время компиляции.
Мы упоминали, что в Rust мы воздерживаемся называть структуры и перечисления
«объектами», чтобы отличать их от объектов в других языках. В структуре или перечислении данные в полях структуры и поведение в блоках impl разделены, тогда как в других языках данные и поведение объединены в одну концепцию, часто обозначающуюся как объект. Тем не менее, типаж-объекты являются более похожими на объекты на других языках, в том смысле, что они сочетают в себе данные и поведение. Но типаж-объекты отличаются от традиционных объектов тем, что не позволяют добавлять данные к типаж-объекту. Типаж-объекты обычно не настолько полезны, как объекты в других языках: их конкретная цель - обеспечить абстракцию через общее поведение.
В листинге 17.3 показано, как определить типаж с именем
Draw с помощью одного метода с именем draw
:
Файл: src/lib.rs
Листинг 17-3: Определение типажа
Draw
Этот синтаксис должен выглядеть знакомым из наших дискуссий о том, как определять типажи в главе 10. Далее следует новый синтаксис: в листинге 17.4 определена структура с именем
Screen
, которая содержит вектор с именем components
. Этот вектор имеет тип
Box
, который и является типаж-объектом; это замена для любого типа внутри
Box который реализует типаж
Draw
Файл: src/lib.rs pub trait
Draw
{ fn draw
(&
self
);
} pub struct
Screen
{ pub components:
Vec
<
Box
<
dyn
Draw>>,
}


Листинг 17-4: Определение структуры
Screen
с полем
components
, которое является вектором типаж-
объектов, которые реализуют типаж
Draw
В структуре
Screen
, мы определим метод run
, который будет вызывать метод draw каждого элемента вектора components
, как показано в листинге 17-5:
Файл: src/lib.rs
Листинг 17-5: Реализация метода
run
у структуры
Screen
, который вызывает метод
draw
каждого
компонента из вектора
Это работает иначе, чем определение структуры, которая использует параметр общего типа с ограничениями типажа. Обобщённый параметр типа может быть заменён только одним конкретным типом, тогда как типаж-объекты позволяют нескольким конкретным типам замещать типаж-объект во время выполнения. Например, мы могли бы определить структуру
Screen используя общий тип и ограничение типажа, как показано в листинге 17-6:
Файл: src/lib.rs
Листинг 17-6: Альтернативная реализация структуры
Screen
и метода
run
, используя обобщённый тип и
ограничения типажа
Это вариант ограничивает нас экземпляром
Screen
, который имеет список компонентов всех типов
Button или всех типов
TextField
. Если у вас когда-либо будут только однородные коллекции, использование обобщений и ограничений типажа является impl
Screen { pub fn run
(&
self
) { for component in self
.components.iter() { component.draw();
}
}
} pub struct
Screen
{ pub components:
Vec
,
} impl
Screen where
T: Draw,
{ pub fn run
(&
self
) { for component in self
.components.iter() { component.draw();
}
}
}
предпочтительным, поскольку определения будут мономорфизированы во время компиляции для использования с конкретными типами.
С другой стороны, с помощью метода, использующего типаж-объекты, один экземпляр
Screen может содержать
Vec
который содержит
Box