Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 7-13: Добавление функции add_to_waitlist в область видимости с use неидиоматическим способом Хотя листинги 7-11 и 7-13 выполняют одну и ту же задачу, листинг 7-11 является идиоматическим способом подключения функции в область видимости с помощью use Подключение родительского модуля функции в область видимости при помощи use означает, что мы должны указывать родительский модуль при вызове функции. Указание родительского модуля при вызове функции даёт понять, что функция не определена локально, но в то же время сводя к минимуму повторение полного пути. В коде листинга 7-13 не ясно, где именно определена add_to_waitlist С другой стороны, при подключении структур, перечислений и других элементов используя use , идиоматически правильным будет указывать полный путь. Листинг 7-14 показывает идиоматический способ подключения структуры стандартной библиотеки HashMap в область видимости бинарного крейта. Файл: src/main.rs Листинг 7-14. Включение HashMap в область видимости идиоматическим способом За этой идиомой нет веской причины: это просто соглашение, которое появилось само собой. Люди привыкли читать и писать код на Rust таким образом. Исключением из этой идиомы является случай, когда мы подключаем два элемента с одинаковыми именами в область видимости используя оператор use - Rust просто не позволяет этого сделать. Листинг 7-15 показывает, как подключить в область действия два типа с одинаковыми именами Result , но из разных родительских модулей и как на них ссылаться. mod front_of_house { pub mod hosting { pub fn add_to_waitlist () {} } } use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant () { add_to_waitlist(); } use std::collections::HashMap; fn main () { let mut map = HashMap::new(); map.insert( 1 , 2 ); } Файл: src/lib.rs Листинг 7-15. Для включения двух типов с одинаковыми именами в одну область видимости необходимо использовать их родительские модули. Как видите, использование имени родительских модулей позволяет различать два типа Result . Если бы вместо этого мы указали use std::fmt::Result и use std::io::Result , мы бы имели два типа Result в одной области видимости, и Rust не смог бы понять какой из двух Result мы имели в виду, когда нашёл бы их употребление в коде. Предоставление новых имён с помощью ключевого слова as Есть другое решение проблемы добавления двух типов с одинаковыми именами в одну и ту же область видимости используя use : после пути можно указать as и новое локальное имя (псевдоним) для типа. Листинг 7-16 показывает как по-другому написать код из листинга 7-15, путём переименования одного из двух типов Result используя as Файл: src/lib.rs Листинг 7-16: Переименование типа, когда он включён в область видимости с помощью ключевого слова as Во втором операторе use мы выбрали новое имя IoResult для типа std::io::Result , которое теперь не будет конфликтовать с типом Result из std::fmt , который также подключён в область видимости. Листинги 7-15 и 7-16 считаются идиоматичными, поэтому выбор за вами! use std::fmt; use std::io; fn function1 () -> fmt:: Result { // --snip-- } fn function2 () -> io:: Result <()> { // --snip-- } use std::fmt:: Result ; use std::io:: Result as IoResult; fn function1 () -> Result { // --snip-- } fn function2 () -> IoResult<()> { // --snip-- } Реэкспорт имён с pub use Когда мы подключаем имя в область видимости, используя ключевое слово use , то имя, доступное в новой области видимости, является приватным. Чтобы позволить коду, который вызывает наш код, ссылаться на это имя, как если бы оно было определено в области видимости данного кода, можно объединить pub и use . Этот метод называется реэкспортом (re-exporting), потому что мы подключаем элемент в область видимости, но также делаем этот элемент доступным для подключения в других областях видимости. Листинг 7-17 показывает код из листинга 7-11, где use в корневом модуле заменено на pub use Файл: src/lib.rs Листинг 7-17. Предоставление имени для использования любым кодом из новой области при помощи pub use До этого изменения внешний код должен был вызывать функцию add_to_waitlist , используя путь restaurant::front_of_house::hosting::add_to_waitlist() . Теперь, когда это объявление pub use повторно экспортировало модуль hosting из корневого модуля, внешний код теперь может использовать вместо него путь restaurant::hosting::add_to_waitlist() Реэкспорт полезен, когда внутренняя структура вашего кода отличается от того, как программисты, вызывающие ваш код, думают о предметной области. Например, по аналогии с рестораном люди, управляющие им, думают о «передней части дома» и «задней части дома». Но клиенты, посещающие ресторан, вероятно, не будут думать о частях ресторана в таких терминах. Используя pub use , мы можем написать наш код с одной структурой, но сделать общедоступной другую структуру. Благодаря этому наша библиотека хорошо организована для программистов, работающих над библиотекой, и для программистов, вызывающих библиотеку. Мы рассмотрим ещё один пример pub use и его влияние на документацию вашего крейта в разделе «Экспорт удобного общедоступного API с pub use » Главы 14. Использование внешних пакетов mod front_of_house { pub mod hosting { pub fn add_to_waitlist () {} } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant () { hosting::add_to_waitlist(); } В Главе 2 мы запрограммировали игру угадывания числа, где использовался внешний пакет с именем rand для генерации случайного числа. Чтобы использовать rand в нашем проекте, мы добавили эту строку в Cargo.toml: Файл: Cargo.toml Добавление rand в качестве зависимости в Cargo.toml указывает Cargo загрузить пакет rand и все его зависимости из crates.io и сделать rand доступным для нашего проекта. Затем, чтобы подключить определения rand в область видимости нашего пакета, мы добавили строку use начинающуюся с названия пакета rand и списка элементов, которые мы хотим подключить в область видимости. Напомним, что в разделе "Генерация случайного числа" Главы 2, мы подключили трейт Rng в область видимости и вызвали функцию rand::thread_rng : Члены сообщества Rust сделали много пакетов доступными на ресурсе crates.io , и добавление любого из них в ваш пакет включает в себя одни и те же шаги: добавить внешние пакеты в файл Cargo.toml вашего пакета, использовать use для подключения элементов внешних пакетов в область видимости. Обратите внимание, что стандартная библиотека std также является крейтом, внешним по отношению к нашему пакету. Поскольку стандартная библиотека поставляется с языком Rust, нам не нужно изменять Cargo.toml для подключения std . Но нам нужно ссылаться на неё при помощи use , чтобы добавить элементы оттуда в область видимости нашего пакета. Например, с HashMap мы использовали бы эту строку: Это абсолютный путь, начинающийся с std , имени крейта стандартной библиотеки. Использование вложенных путей для уменьшения длинных списков use Если мы используем несколько элементов, определённых в одном крейте или в том же модуле, то перечисление каждого элемента в отдельной строке может занимать много вертикального пространства в файле. Например, эти два объявления use используются rand = "0.8.3" use rand::Rng; fn main () { let secret_number = rand::thread_rng().gen_range( 1 ..= 100 ); } use std::collections::HashMap; в программе угадывания числа (листинг 2-4) для подключения элементов из std в область видимости: Файл: src/main.rs Вместо этого, мы можем использовать вложенные пути, чтобы добавить эти элементы в область видимости одной строкой. Мы делаем это, как показано в листинге 7-18, указывая общую часть пути, за которой следуют два двоеточия, а затем фигурные скобки вокруг списка тех частей продолжения пути, которые отличаются. Файл: src/main.rs Листинг 7-18. Указание вложенного пути для добавления нескольких элементов с одинаковым префиксом в область видимости В больших программах, подключение множества элементов из одного пакета или модуля с использованием вложенных путей может значительно сократить количество необходимых отдельных операторов use ! Можно использовать вложенный путь на любом уровне, что полезно при объединении двух операторов use , которые имеют общую часть пути. Например, в листинге 7-19 показаны два оператора use : один подключает std::io , а другой подключает std::io::Write в область видимости. Файл: src/lib.rs Листинг 7-19: Два оператора use , где один является частью другого Общей частью этих двух путей является std::io , и это полный первый путь. Чтобы объединить эти два пути в одном операторе use , мы можем использовать ключевое слово self во вложенном пути, как показано в листинге 7-20. Файл: src/lib.rs Листинг 7-20: Объединение путей из Листинга 7-19 в один оператор use // --snip-- use std::cmp::Ordering; use std::io; // --snip-- // --snip-- use std::{cmp::Ordering, io}; // --snip-- use std::io; use std::io::Write; use std::io::{ self , Write}; Эта строка подключает std::io и std::io::Write в область видимости. Оператор * (glob) Если мы хотим включить в область видимости все общедоступные элементы, определённые в пути, мы можем указать этот путь, за которым следует оператор * : Этот оператор use подключает все открытые элементы из модуля std::collections в текущую область видимости. Будьте осторожны при использовании оператора * ! Он может усложнить понимание, какие имена находятся в области видимости и где были определены имена, используемые в вашей программе. Оператор * часто используется при тестировании для подключения всего что есть в модуле tests ; мы поговорим об этом в разделе "Как писать тесты" Главы 11. Оператор * также иногда используется как часть шаблона автоматического импорта (prelude): смотрите документацию по стандартной библиотеке для получения дополнительной информации об этом шаблоне. use std::collections::*; Разделение модулей на разные файлы До сих пор все примеры в этой главе определяли несколько модулей в одном файле. Когда модули становятся большими, вы можете захотеть переместить их определения в отдельные файлы, чтобы упростить навигацию по коду. Например, давайте начнём с кода из листинга 7-17, в котором было несколько модулей ресторана. Мы будем извлекать модули в файлы вместо того, чтобы определять все модули в корневом модуле крейта. В нашем случае корневой модуль крейта - src/lib.rs, но это разделение также работает и с бинарными крейтами, у которых корневой модуль крейта — src/main.rs. Сначала мы извлечём модуль front_of_house в свой собственный файл. Удалите код внутри фигурных скобок для модуля front_of_house , оставив только объявление mod front_of_house; , так что теперь src/lib.rs содержит код, показанный в листинге 7-21. Обратите внимание, что этот вариант не скомпилируется, пока мы не создадим файл src/front_of_house.rs из листинге 7-22. Файл: src/lib.rs Листинг 7-21. Объявление модуля front_of_house , чьё содержимое будет в src/front_of_house.rs Затем поместим код, который был в фигурных скобках, в новый файл с именем src/front_of_house.rs, как показано в листинге 7-22. Компилятор знает, что нужно искать в этом файле, потому что он наткнулся в корневом модуле крейта на объявление модуля с именем front_of_house Файл: src/front_of_house.rs Листинг 7-22. Определение содержимого модуля front_of_house в файле src/front_of_house.rs Обратите внимание, что вам нужно только один раз загрузить файл с помощью объявления mod в вашем дереве модулей. Как только компилятор узнает, что файл является частью проекта (и узнает, где в дереве модулей находится код из-за того, куда вы поместили оператор mod ), другие файлы в вашем проекте должны ссылаться на код загруженного файла, используя путь к месту, где он был объявлен, как описано в разделе mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant () { hosting::add_to_waitlist(); } pub mod hosting { pub fn add_to_waitlist () {} } «Пути для ссылки на элемент в дереве модулей» . Другими словами, mod — это не операция «включения», которую вы могли видеть в других языках программирования. Далее мы извлечём модуль hosting в его собственный файл. Процесс немного отличается, потому что hosting является дочерним модулем для front_of_house , а не корневого модуля. Мы поместим файл для hosting в новый каталог, который будет назван по имени его предка в дереве модулей, в данном случае это src/front_of_house/. Чтобы начать перенос hosting , мы меняем src/front_of_house.rs так, чтобы он содержал только объявление модуля hosting : Файл: src/front_of_house.rs Затем мы создаём каталог src/front_of_house и файл hosting.rs, в котором будут определения, сделанные в модуле hosting : Файл: src/front_of_house/hosting.rs Если вместо этого мы поместим hosting.rs в каталог src, компилятор будет думать, что код в hosting.rs это модуль hosting , объявленный в корне крейта, а не объявленный как дочерний модуль front_of_house . Правила компилятора для проверки какие файлы содержат код каких модулей предполагают, что каталоги и файлы точно соответствуют дереву модулей. Альтернативные пути к файлам До сих пор мы рассматривали наиболее идиоматические пути к файлам, используемые компилятором Rust, но Rust также поддерживает и старый стиль пути к файлу. Для модуля с именем front_of_house , объявленного в корневом модуле крейта, компилятор будет искать код модуля в: src/front_of_house.rs (что мы рассматривали) src/front_of_house/mod.rs (старый стиль, всё ещё поддерживаемый путь) Для модуля с именем hosting , который является подмодулем front_of_house , компилятор будет искать код модуля в: src/front_of_house/hosting.rs (что мы рассматривали) src/front_of_house/hosting/mod.rs (старый стиль, всё ещё поддерживаемый путь) pub mod hosting; pub fn add_to_waitlist () {} Если вы используете оба стиля для одного и того же модуля, вы получите ошибку компилятора. Использование сочетания обоих стилей для разных модулей в одном проекте разрешено, но это может сбивать с толку людей, перемещающихся по вашему проекту. Основным недостатком стиля, в котором используются файлы с именами mod.rs, является то, что в вашем проекте может оказаться много файлов с именами mod.rs, что может привести к путанице, если вы одновременно откроете их в редакторе. Мы перенесли код каждого модуля в отдельный файл, а дерево модулей осталось прежним. Вызовы функций в eat_at_restaurant будут работать без каких-либо изменений, несмотря на то, что определения находятся в разных файлах. Этот метод позволяет перемещать модули в новые файлы по мере увеличения их размеров. Обратите внимание, что оператор pub use crate::front_of_house::hosting в src/lib.rs также не изменился, и use не влияет на то, какие файлы компилируются как часть крейта. Ключевое слово mod объявляет модули, и Rust ищет в файле с тем же именем, что и у модуля, код, который входит в этот модуль. Итог Rust позволяет разбить пакет на несколько крейтов и крейт - на модули, так что вы можете ссылаться на элементы, определённые в одном модуле, из другого модуля. Это можно делать при помощи указания абсолютных или относительных путей. Эти пути можно добавить в область видимости оператором use , поэтому вы можете пользоваться более короткими путями для многократного использования элементов в этой области видимости. Код модуля по умолчанию является приватным, но можно сделать определения общедоступными, добавив ключевое слово pub В следующей главе мы рассмотрим некоторые коллекции структур данных из стандартной библиотеки, которые вы можете использовать в своём аккуратно организованном коде. Коллекции стандартной библиотеки Стандартная библиотека содержит несколько полезных структур данных, которые называются коллекциями. Большая часть других типов данных представляют собой хранение конкретного значения, но особенностью коллекций является хранение множества однотипных значений. В отличии от массива или кортежа данные коллекций хранятся в куче, а это значит, что размер коллекции может быть неизвестен в момент компиляции программы. Он может изменяться (увеличиваться, уменьшаться) во время работы программы. Каждый вид коллекций имеет свои возможности и отличается по производительности, так что выбор конкретной коллекции зависит от ситуации и является умением разработчика, вырабатываемым со временем. В этой главе будет рассмотрено несколько коллекций: Вектор (vector) - позволяет нам сохранять различное количество последовательно хранящихся значений, Строка (string) - это последовательность символов. Мы же упоминали тип String ранее, но в данной главе мы поговорим о нем подробнее. Хеш таблица (hash map) - коллекция которая позволяет хранить перечень ассоциаций значения с ключом (перечень пар ключ:значение). Является конкретной реализацией более общей структуры данных называемой map. Для того, чтобы узнать о других видах коллекций предоставляемых стандартной библиотекой смотрите документацию Мы обсудим как создавать и обновлять вектора, строки и хеш таблицы, а также объясним что делает каждую из них особенной. Хранение списков значений в векторах Первым типом коллекции, который мы разберём, будет Vec , также известный как вектор (vector). Векторы позволяют хранить более одного значения в одной структуре данных, хранящей элементы в памяти один за другим. Векторы могут хранить данные только одного типа. Их удобно использовать, когда нужно хранить список элементов, например, список текстовых строк из файле, или список цен товаров в корзине покупок. |