Язык программирования Rust
Скачать 7.02 Mb.
|
Cargo как Конвенция В простых проектах Cargo не даёт больших преимуществ по сравнению с использованием rustc , но он проявит себя, когда ваши программы станут более сложными. Когда программы вырастают до нескольких файлов или нуждаются в зависимостях, гораздо проще позволить Cargo координировать сборку. Не смотря на то, что проект hello_cargo простой, теперь он использует большую часть реального инструментария, который вы будете повседневно использовать в вашей карьере, связанной с Rust. Когда потребуется работать над проектами размещёнными в сети, вы сможете просто использовать следующую последовательность команд для получения кода с помощью Git, перехода в каталог проекта, сборку проекта: Для получения дополнительной информации о Cargo ознакомьтесь с его документацией Итоги Теперь вы готовы начать своё Rust путешествие! В данной главе вы изучили как: установить последнюю стабильную версию Rust, используя rustup , обновить Rust до последней версии, открыть локально установленную документацию, написать и запустить программу типа "Hello, world!", используя напрямую компилятор rustc , создать и запустить новый проект, используя соглашения и команды Cargo. $ git clone example.org/someproject $ cd someproject $ cargo build Это отличное время для создания более существенной программы, чтобы привыкнуть читать и писать код на языке Rust. Итак, в главе 2 мы построим программу для игры в угадай число. Если вы предпочитаете начать с изучения того, как работают общие концепции программирования в Rust, обратитесь к главе 3, а затем вернитесь к главе 2. Программируем игру Угадайка Давайте погрузимся в Rust, вместе выполнив практический проект! Эта глава познакомит с несколькими распространёнными концепциями Rust, показав, как использовать их в реальной программе. Вы узнаете о let , match , методах, ассоциированных функциях, использовании внешних пакетов и многом другом! В следующих главах рассмотрим эти идеи более подробно. В этой главе вы на практике познакомитесь с основами. Мы реализуем классическую для начинающих программистов задачу: игру в угадывание. Вот как это работает: программа генерирует случайное целое число в диапазоне от 1 до 100. Затем она предлагает игроку ввести отгадку. После ввода отгадки программа укажет, является ли отгадка слишком заниженной или слишком завышенной. Если отгадка верна, игра напечатает поздравительное сообщение и завершится. Настройка нового проекта Для настройки нового проекта перейдите в каталог projects, который вы создали в главе 1, и создайте новый проект с использованием Cargo, как показано ниже: Первая команда, cargo new , принимает в качестве первого аргумента имя проекта ( guessing_game ). Вторая команда изменяет каталог на новый каталог проекта. Загляните в созданный файл Cargo.toml: Файл: Cargo.toml Как вы уже видели в Главе 1, cargo new создаёт программу "Hello, world!". Посмотрите файл src/main.rs: Файл: src/main.rs $ cargo new guessing_game $ cd guessing_game [package] name = "guessing_game" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust- lang.org/cargo/reference/manifest.html [dependencies] Теперь давайте скомпилируем программу "Hello, world!" и сразу на этом же этапе запустим её с помощью команды cargo run : Команда run пригодится, когда необходимо ускоренно выполнить итерацию проекта, мы так собираемся сделать в этом проекте, быстро тестируя каждую итерацию, прежде чем перейти к следующей. Снова откройте файл src/main.rs. Весь код вы будете писать в этом файле. Обработка отгадки Первая часть программы игры угадывания запрашивает ввод данных пользователем, обрабатывает их и проверяет, что вводимые данные имеют ожидаемую форму. Для начала мы позволим игроку ввести отгадку. Введите код из Листинга 2-1 в src/main.rs. Файл: src/main.rs Листинг 2-1: Код, который получает отгадку от пользователя и печатает её Этот код содержит много информации, поэтому давайте рассмотрим его построчно. Чтобы получить пользовательский ввод и затем вывести результат в качестве вывода, fn main () { println! ( "Hello, world!" ); } $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.50s Running `target/debug/guessing_game` Hello, world! use std::io; fn main () { println! ( "Guess the number!" ); println! ( "Please input your guess." ); let mut guess = String ::new(); io::stdin() .read_line(& mut guess) .expect( "Failed to read line" ); println! ( "You guessed: {guess}" ); } нам нужно включить в область видимости библиотеку ввода/вывода io . Библиотека io является частью стандартной библиотеки, известной как std : По умолчанию в Rust есть набор элементов, определённых в стандартной библиотеке, которые он добавляет в область видимости каждой программы. Этот набор называется прелюдией, и вы можете изучить его содержание в документации стандартной библиотеки Если тип, который требуется использовать, отсутствует в прелюдии, его нужно явно ввести в область видимости с помощью оператора use . Использование библиотеки std::io предоставляет ряд полезных функциональных возможностей, включая способность принимать пользовательский ввод. Как уже отмечалось в главе 1, функция main является точкой входа в программу: Синтаксис fn объявляет новую функцию, круглые скобки () указывают на отсутствие параметров, а фигурная скобка { обозначает начало тела функции. Также в главе 1 упоминалось, что println! - это макрос, который печатает строку на экран: Этот код печатает подсказку об игре и запрашивает пользовательский ввод. Хранение значений с помощью переменных Далее мы создаём переменную для хранения пользовательского ввода, как показано ниже: Вот теперь программа становится интересней! Очень многое происходит в этой маленькой строке. Для создания переменной мы используем оператор let . Вот ещё один пример: Эта строка создаёт новую переменную с именем apples и привязывает её к значению 5. В Rust переменные неизменяемы по умолчанию, то есть, как только мы присвоим use std::io; fn main () { println! ( "Guess the number!" ); println! ( "Please input your guess." ); let mut guess = String ::new(); let apples = 5 ; переменной значение, значение не изменится. Мы подробно обсудим эту концепцию в разделе «Переменные и изменчивость». в Главе 3. Чтобы сделать переменную изменяемой, мы добавляем mut перед её именем: Примечание: Синтаксис // означает начало комментария, который продолжается до конца строки. Rust игнорирует все содержимое комментариев. Подробнее о комментариях мы поговорим в главе 3 Возвращаясь к программе игры Угадайка, теперь вы знаете, что let mut guess предоставит изменяемую переменную с именем guess . Знак равенства ( = ) сообщает Rust, что сейчас нужно связать что-то с этой переменной. Справа от знака равенства находится значение, связанное с guess , которое является результатом вызова функции String::new , возвращающей новый экземпляр String String - это тип строки, предоставляемый стандартной библиотекой, который является расширяемым фрагментом текста в кодировке UTF-8. Синтаксис :: в строке ::new указывает, что new является ассоциированной функцией String типа. Ассоциированная функция - это функция, реализованная для типа, в данном случае String . Функция new создаёт новую, пустую строку. Функцию new можно встретить во многих типах, это типичное название для функции, которая создаёт новое значение какого-либо типа. В целом, строка let mut guess = String::new(); создала изменяемую переменную, которая связывается с новым, пустым экземпляром String . Фух! Получение пользовательского ввода Напомним, мы подключили функциональность ввода/вывода из стандартной библиотеки с помощью use std::io; в первой строке программы. Теперь мы вызовем функцию stdin из модуля io , которая позволит нам обрабатывать пользовательский ввод: Если бы мы не импортировали библиотеку io с помощью use std::io в начале программы, мы все равно могли бы использовать эту функцию, записав вызов этой функции как std::io::stdin . Функция stdin возвращает экземпляр std::io::Stdin , который является типом, представляющим дескриптор стандартного ввода для вашего терминала. let apples = 5 ; // неизменяемая let mut bananas = 5 ; // изменяемая io::stdin() .read_line(& mut guess) Далее строка .read_line(&mut guess) вызывает метод read_line на дескрипторе стандартного ввода для получения ввода от пользователя. Мы также передаём &mut guess в качестве аргумента read_line , сообщая ему, в какой строке хранить пользовательский ввод. Главная задача read_line - принять все, что пользователь вводит в стандартный ввод, и сложить это в строку (не переписывая её содержимое), поэтому мы передаём эту строку в качестве аргумента. Строковый аргумент должен быть изменяемым, чтобы метод мог изменить содержимое строки. Символ & указывает, что этот аргумент является ссылкой, который предоставляет возможность нескольким частям вашего кода получить доступ к одному фрагменту данных без необходимости копировать эти данные в память несколько раз. Ссылки - это сложная функциональная возможность, а одним из главных преимуществ Rust является безопасность и простота использования ссылок. Чтобы дописать эту программу, вам не понадобится знать много таких подробностей. Пока вам достаточно знать, что ссылки, как и переменные, по умолчанию неизменяемы. Соответственно, чтобы сделать её изменяемой, нужно написать &mut guess , а не &guess . (В главе 4 ссылки будут описаны более подробно). Обработка потенциального сбоя с помощью типа Result Мы все ещё работаем над этой строкой кода. Сейчас мы обсуждаем третью строку, но обратите внимание, что она по-прежнему является частью одной логической строки кода. Следующая часть - метод: Мы могли бы написать этот код так: Однако одну длинную строку трудно читать, поэтому лучше разделить её. При вызове метода с помощью синтаксиса .method_name() часто целесообразно вводить новую строку и другие пробельные символы, чтобы разбить длинные строки. Теперь давайте обсудим, что делает эта строка. Как упоминалось ранее, read_line помещает всё, что вводит пользователь, в строку, которую мы ему передаём, но также возвращает значение Result Result это перечисление , часто называемый enum, тип, который может находиться в одном из нескольких возможных состояний. Мы называем каждое такое состояние вариантом. В главе 6 перечисления будут рассмотрены более подробно. Назначение всех типов Result заключается в передаче информации для обработки ошибок. Варианты Result : Ok и Err . Вариант Ok указывает на то, что операция прошла успешно, а внутри Ok находится успешно сгенерированное значение. Вариант Err .expect( "Failed to read line" ); io::stdin().read_line(& mut guess).expect( "Failed to read line" ); означает, что операция не удалась, а Err содержит информацию о том, как и почему операция не удалась. Значения типа Result , как и значения любого типа, имеют определённые для них методы. Экземпляр Result имеет expect метод , который можно вызвать. Если этот экземпляр Result является значением Err , expect вызовет сбой программы и отобразит сообщение, которое вы передали в качестве аргумента. Если метод read_line возвращает Err , это, скорее всего, результат ошибки базовой операционной системы. Если экземпляр Result является значением Ok , expect возьмёт возвращаемое значение, которое Ok удерживает, и вернёт вам только это значение, чтобы вы могли его использовать. В данном случае это значение представляет собой количество байтов, введённых пользователем. Если не вызвать expect , программа скомпилируется, но будет получено предупреждение: Rust предупреждает о не использовании значения Result , возвращаемого из read_line , показывая, что программа не учла возможность возникновения ошибки. Правильный способ убрать предупреждение - это написать обработку ошибок, но в нашем случае мы просто хотим аварийно завершить программу при возникновении проблемы, поэтому используем expect . О способах восстановления после ошибок вы узнаете в главе 9 Напечатать значений с помощью заполнителей println! Кроме закрывающей фигурной скобки, в коде на данный момент есть ещё только одна строка для обсуждения: Эта строка печатает строку, которая теперь содержит ввод пользователя. Набор фигурных скобок {} является заполнителем: думайте о {} как о маленьких крабовых $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) warning: unused `Result` that must be used --> src/main.rs:10:5 | 10 | io::stdin().read_line(&mut guess); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled warning: `guessing_game` (bin "guessing_game") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.59s println! ( "You guessed: {guess}" ); клешнях, удерживающих значение на месте. С помощью фигурных скобок можно вывести более одного значения: первый набор фигурных скобок содержит первое значение, указанное после форматирующей строки, второй набор - второе значение и так далее. Печать нескольких значений за один вызов println! будет выглядеть следующим образом: Этот код напечатает x = 5 and y = 10 Тестирование первой части Давайте протестирует первую часть игры. Запустите её используя cargo run : На данном этапе первая часть игры завершена: мы получаем ввод с клавиатуры и затем печатаем его. Генерация секретного числа Далее нам нужно сгенерировать секретное число, которое пользователь попытается угадать. Секретное число должно быть каждый раз разным, чтобы в игру можно было играть несколько раз. Мы будем использовать случайное число в диапазоне от 1 до 100, чтобы игра не была слишком сложной. Rust пока не включает функциональность случайных чисел в свою стандартную библиотеку. Однако команда Rust предоставляет rand crate с подобной функциональностью. Использование пакета для получения дополнительной функциональности Запомните, что крейт — это набор файлов с исходным кодом Rust. Проект, который мы создавали, представляет собой двоичный крейт, являющийся исполняемым файлом. let x = 5 ; let y = 10 ; println! ( "x = {} and y = {}" , x, y); $ cargo run Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 6.44s Running `target/debug/guessing_game` Guess the number! Please input your guess. 6 You guessed: 6 rand крейт — это библиотечный крейт, содержащий код, предназначенный для использования в других программах, и не может быть выполнен сам по себе. Координация работы внешних пакетов является тем местом, где Cargo действительно блистает. Чтобы начать писать код, использующий rand , необходимо изменить файл Cargo.toml, включив в него в качестве зависимости пакет rand . Итак, откройте этот файл и добавьте следующую строку внизу под заголовком секции [dependencies] , созданным для вас Cargo. Обязательно укажите rand в точности как здесь, с таким же номером версии, иначе примеры кода из этого урока могут не заработать. Имя файла: Cargo.toml В файле Cargo.toml всё, что следует за заголовком, является частью этой секции, которая продолжается до тех пор, пока не начнётся следующая. В [dependencies] вы сообщаете Cargo, от каких внешних крейтов зависит ваш проект и какие версии этих крейтов вам нужны. В этом случае мы указываем крейт rand со спецификатором семантической версии 0.8.3 . Cargo понимает семантическое версионирование (иногда называемое SemVer), которое является стандартом для описания версий. Число 0.8.3 на самом деле является сокращением от ^0.8.3 , что означает любую версию не ниже 0.8.3 , но ниже 0.9.0 Cargo рассчитывает, что эти версии имеют общедоступное API, совместимое с версией 0.8.3 , и вы получите последние версии исправлений, которые по-прежнему будут компилироваться с кодом из этой главы. Не гарантируется, что версия 0.9.0 или выше будет иметь тот же API, что и в следующих примерах. Теперь, ничего не меняя в коде, давайте создадим проект, как показано в Листинге 2-2. Листинг 2-2: Результат выполнения cargo build после добавления крейта rand в качестве зависимости rand = "0.8.3" $ cargo build Updating crates.io index Downloaded rand v0.8.3 Downloaded libc v0.2.86 Downloaded getrandom v0.2.2 Downloaded cfg-if v1.0.0 Downloaded ppv-lite86 v0.2.10 Downloaded rand_chacha v0.3.0 Downloaded rand_core v0.6.2 Compiling rand_core v0.6.2 Compiling libc v0.2.86 Compiling getrandom v0.2.2 Compiling cfg-if v1.0.0 Compiling ppv-lite86 v0.2.10 Compiling rand_chacha v0.3.0 Compiling rand v0.8.3 Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 2.53s Вы можете увидеть другие номера версий (но все они будут совместимы с кодом, благодаря SemVer!), другие строки (в зависимости от операционной системы), а также строки могут быть расположены в другом порядке. Когда мы включаем внешнюю зависимость, Cargo берет последние версии всего, что нужно этой зависимости, из реестра (registry), который является копией данных с Crates.io . Crates.io - это место, где участники экосистемы Rust размещают свои проекты Rust с открытым исходным кодом для использования другими. После обновления реестра Cargo проверяет раздел [dependencies] и загружает все указанные в списке пакеты, которые ещё не были загружены. В нашем случае, хотя мы указали только rand в качестве зависимости, Cargo также захватил другие пакеты, от которых зависит работа rand . После загрузки пакетов Rust компилирует их, а затем компилирует проект с имеющимися зависимостями. Если вы немедленно снова запустите cargo build без внесения каких-либо изменений, вы не получите никакого вывода, кроме строки Finished . Cargo знает, что он уже выгрузил и скомпилировал зависимости, и вы ничего не изменили в файле Cargo.toml. Cargo также знает, что вы ничего не меняли в своём коде, поэтому он также не станет перекомпилировать его. Ввиду отсутствия задач он просто выходит. Если открыть файл src/main.rs, внести незначительные изменения, а затем сохранить его и снова произвести сборку, то вы увидите только две строки вывода: Эти строки показывают, что Cargo обновляет сборку только на основании вашего крошечного изменения в файле src/main.rs. Поскольку зависимости не изменились, Cargo знает, что может повторно использовать ранее загруженные и скомпилированные зависимости. |