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

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

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

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

Добавлен: 11.01.2024

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

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


У значения может быть только один владелец в один момент времени,
Когда владелец покидает область видимости, значение удаляется.
Область видимости переменной
Теперь, когда мы прошли базовый синтаксис Rust, мы не будем включать весь код fn main() {
в примеры. Поэтому, если вы будете следовать этому курсу, убедитесь, что следующие примеры помещены в функцию main вручную. В результате наши примеры будут более лаконичными, что позволит нам сосредоточиться на реальных деталях, а не на шаблонном коде.
В качестве первого примера владения мы рассмотрим область видимости некоторых переменных. Область видимости — это диапазон внутри программы, для которого допустим элемент. Возьмём следующую переменную:
Переменная s
относится к строковому литералу, где значение строки жёстко прописано в тексте нашей программы. Переменная действительна с момента её объявления до конца текущей области видимости. В листинге 4-1 показана программа с комментариями, указывающими, где допустима переменная s
Листинг 4-1: переменная и область действия, в которой она допустима
Другими словами, здесь есть два важных момента:
Когда переменная s
появляется в области видимости, она считается действительной,
Она остаётся действительной до момента выхода за границы этой области.
На этом этапе объяснения взаимосвязь между областями видимости и допустимостью переменных аналогична той, что существует в других языках программирования. Теперь мы будем опираться на это понимание, введя тип
String
Тип данных String
Чтобы проиллюстрировать правила владения, нам нужен тип данных более сложный,
чем те, которые мы рассмотрели в разделе
«Типы данных»
главы 3. Все рассмотренные let s =
"hello"
;
{
// s is not valid here, it’s not yet declared let s =
"hello"
;
// s is valid from this point forward
// do stuff with s
}
// this scope is now over, and s is no longer valid
ранее типы имеют известный размер, могут храниться в стеке и извлекаться из стека,
когда их область действия заканчивается. Также они могут быть быстро и легко скопированы для создания нового независимого экземпляра, если другая часть кода должна использовать то же значение в другой области видимости. Но мы хотим посмотреть на данные, хранящиеся в куче, и выяснить, как Rust узнает, когда нужно очистить эти данные, поэтому тип
String
— отличный пример.
Мы сконцентрируемся на тех частях
String
, которые связаны с владением. Эти аспекты также применимы к другим сложным типам данных, независимо от того, предоставлены они стандартной библиотекой или созданы вами. Более подробно мы обсудим
String в
главе 8
Мы уже видели строковые литералы, где строковое значение жёстко прописано в нашей программе. Строковые литералы удобны, но они подходят не для каждой ситуации, где мы можем хотеть использовать текст. Одна из причин заключается в том, что они неизменны. Кроме того, не каждое строковое значение может быть известно во время написания кода: что, если мы захотим принять и сохранить пользовательский ввод? Для таких ситуаций в Rust есть ещё один строковый тип —
String
. Этот тип управляет данными, выделенными в куче, и поэтому может хранить объём текста, который во время компиляции неизвестен. Также вы можете создать
String из строкового литерала,
используя функцию from
, например:
Оператор двойного двоеточия
::
позволяет нам использовать пространство имён функции from под типом
String
, вместо какого-то имени вроде string_from
. Мы обсудим этот синтаксис более подробно в разделе
«Синтаксис метода»
главы 5 и когда мы будем говорить о пространствах имён с модулями в
«Пути для обращения к элементу в дереве модулей»
в главе 7.
Строка такого типа может быть изменяема:
В чем здесь разница? Почему
String можно менять, а литерал — нельзя? Разница в том,
как эти два типа работают с памятью.
Память и способы её выделения
В случае строкового литерала мы знаем его содержимое во время компиляции, и оно жёстко прописано в итоговом исполняемом файле. Причина того, что строковые let s =
String
::from(
"hello"
); let mut s =
String
::from(
"hello"
); s.push_str(
", world!"
);
// push_str() appends a literal to a String println!
(
"{}"
, s);
// This will print `hello, world!`

литералы более быстрые и эффективные, в их неизменяемости. К сожалению, нельзя поместить неопределённый кусок памяти в выполняемый файл для текста, размер которого неизвестен при компиляции и может меняться во время выполнения программы.
Чтобы поддерживать изменяемый, увеличивающийся текст типа
String
, необходимо выделять память в куче для всего содержимого, объем которого неизвестен во время компиляции. Это означает, что:
Память должна запрашиваться у операционной системы во время выполнения программы,
Необходим способ возврата этой памяти операционной системе, когда мы закончили в программе работу со
String
Первая часть выполняется нами: когда мы вызываем
String::from
, его реализация запрашивает необходимую память. Это работает довольно похоже во всех языках программирования.
Однако вторая часть отличается. В языках со сборщиком мусора (GC), память, которая больше не используется, отслеживается и очищается с его помощью — нам не нужно об этом думать. В большинстве языков без сборщика мусора мы обязаны сами определять,
когда память больше не используется, и вызывать код для явного её освобождения,
точно так же, как мы делали это для её запроса. Правильное выполнение этого процесса исторически было сложной проблемой программирования. Если мы забудем освободить память, она будет потеряна. Если мы сделаем это слишком рано, у нас будет недопустимая переменная. Сделать это дважды тоже будет ошибкой. Нам нужно соединить ровно один allocate ровно с одним free
Rust выбирает другой путь: память автоматически возвращается, как только владеющая памятью переменная выходит из области видимости. Вот версия примера с областью видимости из листинга 4-1, в котором используется тип
String вместо строкового литерала:
Существует естественный момент, когда мы можем вернуть память, необходимую нашему
String
, обратно распределителю — когда s
выходит за пределы области видимости. Когда переменная выходит за пределы области видимости, Rust вызывает для нас специальную функцию. Эта функция называется drop
, и именно здесь автор
String может поместить код для возврата памяти. Rust автоматически вызывает drop после закрывающей фигурной скобки.
{ let s =
String
::from(
"hello"
);
// s is valid from this point forward
// do stuff with s
}
// this scope is now over, and s is no
// longer valid

Примечание: в C++ этот паттерн освобождения ресурсов в конце времени жизни элемента иногда называется «Получение ресурса есть инициализация» (англ. Resource
Acquisition Is Initialization (RAII)). Функция drop в Rust покажется вам знакомой, если вы использовали шаблоны RAII.
Этот шаблон оказывает глубокое влияние на способ написания кода в Rust. Сейчас это может казаться простым, но в более сложных ситуациях поведение кода может быть неожиданным, например когда хочется иметь несколько переменных, использующих данные, выделенные в куче. Изучим несколько таких ситуаций.
1   2   3   4   5   6   7   8   9   10   ...   62

Способы взаимодействия переменных и данных: перемещение
Несколько переменных могут по-разному взаимодействовать с одними и теми же данными в Rust. Давайте рассмотрим пример использования целого числа в листинге 4-
2.
Листинг 4-2: присвоение целочисленного значения переменной
x
к переменной
y
Мы можем догадаться, что делает этот код: «привязать значение
5
к x
; затем сделать копию значения в x
и привязать его к y
». Теперь у нас есть две переменные: x
и y
, и обе равны
5
. Это то, что происходит на самом деле, потому что целые числа — это простые значения с известным фиксированным размером, и эти два значения
5
помещаются в стек.
Теперь рассмотрим версию с типом
String
:
Это выглядит очень похоже, поэтому мы можем предположить, что происходит то же самое: вторая строка сделает копию значения в s1
и привяжет его к s2
. Но это не совсем так.
Взгляните на рисунок 4-1, чтобы увидеть, что происходит со
String под капотом.
String состоит из трёх частей, показанных слева: указатель на память, в которой хранится содержимое строки, длина и ёмкость. Эта группа данных хранится в стеке. Справа —
память в куче, которая хранит содержимое.
let x =
5
; let y = x; let s1 =
String
::from(
"hello"
); let s2 = s1;
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-1: представление в памяти
String
, содержащей значение
"hello"
, привязанное к
s1
Длина — это количество байт памяти, которое использует содержимое
String в данный момент. Ёмкость — это общее количество байт памяти, которое
String получил от операционной системы. Разница между длиной и ёмкостью имеет значение, но не в данном контексте — сейчас можно игнорировать ёмкость.
Когда мы присваиваем s1
значение s2
, данные
String копируются, то есть мы копируем указатель, длину и ёмкость, которые находятся в стеке. Мы не копируем данные в куче, на которую указывает указатель. Другими словами, представление данных в памяти выглядит так, как показано на рисунке 4-2.
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s2
name value ptr len
5
capacity
5
Рисунок 4-2: представление в памяти переменной
s2
, имеющей копию указателя, длины и ёмкости
s1
Представление не похоже на рисунок 4-3, как выглядела бы память, если бы вместо этого
Rust также скопировал данные кучи. Если бы Rust сделал это, операция s2 = s1
могла бы быть очень дорогой с точки зрения производительности во время выполнения, если бы данные в куче были большими.

s2
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o
Рисунок 4-3: другой вариант того, что может сделать
s2 = s1
, если Rust также скопирует данные кучи
Ранее мы сказали, что когда переменная выходит из области видимости, Rust автоматически вызывает функцию drop и очищает память в куче для данной переменной. Но картинка 4-2 показывает, что теперь оба указателя указывают на одно и тоже место. Это проблема: когда переменная s2
и переменная s1
выходят из области видимости, они обе будут пытаться освободить одну и ту же память в куче. Это известно как «ошибка двойного освобождения» (double free), и является одной из ошибок безопасности памяти, упоминаемых ранее. Освобождение памяти дважды может привести к повреждению памяти, что потенциально может привести к уязвимостям безопасности.
Чтобы обеспечить безопасность памяти, после строки let s2 = s1
Rust считает s1
более недействительным. Следовательно, Rust не нужно ничего освобождать, когда s1
выходит за пределы области видимости. Посмотрите, что происходит, когда вы пытаетесь использовать s1
после создания s2
; это не сработает:
Вы получите похожую ошибку, потому что Rust не даст использовать недействительную ссылку s1
:
let s1 =
String
::from(
"hello"
); let s2 = s1; println!
(
"{}, world!"
, s1);

Если вы слышали термины поверхностное копирование и глубокое копирование при работе с другими языками, концепция копирования указателя, длины и ёмкости без копирования данных, вероятно, звучит как создание поверхностной копии. Но поскольку
Rust также аннулирует первую переменную, вместо того, чтобы называть её
поверхностной копией, это называется перемещением. В этом примере мы бы сказали,
что s1
был перемещён в s2
. Что происходит на самом деле, показано на рисунке 4-4.
s1
name value ptr len
5
capacity
5
index value
0
h
1
e
2
l
3
l
4
o s2
name value ptr len
5
capacity
5
Рисунок 4-4: представление в памяти после того, как
s1
был признан недействительным
Это решает нашу проблему! Действительной остаётся только переменная s2
. Когда она выходит из области видимости, то она одна будет освобождать память в куче.
$
cargo run
Compiling ownership v0.1.0 (file:///projects/ownership) error[E0382]: borrow of moved value: `s1`
-->
src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`. error: could not compile `ownership` due to previous error


Дополнительно присутствует дизайнерский выбор, который подразумевает следующее:
Rust никогда не будет автоматически создавать «глубокие» копии ваших данных.
Следовательно любое такое автоматическое копирование можно считать недорогим с точки зрения производительности во время выполнения.
Способы взаимодействия переменных и данных: клонирование
Если мы хотим глубоко скопировать данные кучи
String
, а не только данные стека, мы можем использовать общий метод, называемый clone
. Мы обсудим синтаксис методов в главе 5, но поскольку методы являются общей чертой многих языков программирования, вы, вероятно, уже встречались с ними.
Вот пример работы метода clone
:
Код работает отлично и явно выполняет поведение, показанное на картинке 4-3, где данные в куче действительно скопированы.
Когда вы видите вызов clone
, вы знаете о выполнении некоторого кода, который может быть дорогим. В то же время использование clone является визуальным индикатором того, что тут происходит что-то нестандартное (глубокое копирование вместо обыденного перемещения).
Стековые данные: копирование
Это ещё одна особенность о которой мы ранее не говорили. Этот код, часть которого была показа ранее в листинге 4-2, использует целые числа. Он работает без ошибок:
Но это, кажется, противоречит тому, что мы только что изучили: тут не нужно вызывать clone
, но x
является все ещё действительной переменной и не перемещена в y
Причина в том, что такие типы, как целые числа, размер которых известен во время компиляции, полностью хранятся в стеке, поэтому копии фактических значений создаются быстро. Это означает, что нет причин, по которым мы хотели бы предотвратить допустимость x
после того, как создадим переменную y
. Другими словами, здесь нет разницы между глубоким и поверхностным копированием, поэтому вызов clone ничем не отличается от обычного поверхностного копирования, и мы можем его опустить.
let s1 =
String
::from(
"hello"
); let s2 = s1.clone(); println!
(
"s1 = {}, s2 = {}"
, s1, s2); let x =
5
; let y = x; println!
(
"x = {}, y = {}"
, x, y);

В Rust есть специальная аннотация, называемая типажом
Copy
, которую мы можем размещать на типах, хранящихся в стеке, как и целые числа (подробнее о типах мы поговорим в главе 10
). Если тип реализует типаж
Copy
, переменные, которые его используют, не перемещаются, а тривиально копируются, что делает их действительными после присвоения другой переменной.
Rust не позволит нам аннотировать тип с помощью
Copy
, если тип или любая из его частей реализует
Drop
. Если для типа нужно, чтобы произошло что-то особенное, когда значение выходит за пределы области видимости, и мы добавляем аннотацию
Copy к
этому типу, мы получим ошибку времени компиляции. Чтобы узнать, как добавить аннотацию
Copy к вашему типу для реализации типажа, смотрите раздел «Производные типажи»
в приложении С.
Но какие же типы имеют типаж
Copy
? Можно проверить документацию любого типа для уверенности, но как правило любая группа простых скалярных значений может быть с типажом
Copy
, и ничего из типов, которые требуют выделения памяти в куче или являются некоторой формой ресурсов, не имеет типажа
Copy
. Вот некоторые типы,
которые реализуют типаж
Copy
:
Все целочисленные типы, такие как u32
,
Логический тип данных bool
, возможные значения которого true и false
,
Все числа с плавающей запятой, такие как f64
,
Символьный тип char
,
Кортежи, но только если они содержат типы, которые также реализуют
Copy
Например,
(i32, i32)
будет с
Copy
, но кортеж
(i32, String)
уже нет.
Владение и функции
Механика передачи значения функции аналогична тому, что происходит при присвоении значения переменной. Передача переменной в функцию приведёт к перемещению или копированию, как и присваивание. В листинге 4-3 есть пример с некоторыми аннотациями, показывающими, где переменные входят в область видимости и выходят из неё.
Файл: src/main.rs