Подробный разбор простого приложения на Rust
Начнём с самого простого и, при этом, самого важного вопроса…
Что мы будем разрабатывать?
Первый этап, практически, любой разработки — выработка требований. Нужно определиться, какую проблему будет решать наше приложения и какой набор возможностей для этого будет предоставлять.
С проблемой мы определились ещё в первом абзаце — хранить заметки. Можно обдумать некоторые детали, например:
Где хранить заметки?
Как выводить заметки?
Какой пользовательский интерфейс будет у приложения?
На эти вопросы можно придумать множество разных ответов:
Хранить заметки можно в: памяти, файле, базе данных…
Выводить заметки можно: в порядке добавления, в алфавитном порядке, по датам…
Варианты пользовательского интерфейса: в командной строке, графический, веб-интерфейс…
Остановимся на самых простых вариантах, чтобы приложение было максимально простым:
Храним заметки в памяти. Это означает, что наши заметки будут существовать, только пока запущено приложение.
Выводить их будем в порядке добавления, а значит, нам не потребуется дополнительная сортировка.
Пользовательский интерфейс организуем через командную строку, а следовательно, мы сможем обойтись без сторонних библиотек, так как функционал для работы с командной строкой (стандартный ввод/вывод), обычно, присутствует в стандартной библиотеке* большинства языков.
*стандартная библиотека — набор программных сущностей, доступных для вызова из любой программы, написанной на этом языке и присутствующих во всех реализациях языка.
Итак, у нас получился список возможностей нашего приложения. Теперь нам нужно свести каждую задачу к набору действий, которыми нас обеспечивает выбранный язык программирования, его стандартная библиотека, либо сторонние библиотеки. Этот процесс называют декомпозицией задачи.
Декомпозиция задачи
Для начала определимся со способом хранения каждой заметки: пусть это будет обычная Rust-строка (тип String
). На самом деле, Rust-строки не совсем обычные. Они хранят свои данные в UTF-8 кодировке, предоставляя возможность использовать в тексте не только любой язык, но и различные вспомогательные символы, например, эмодзи.
Далее, нам нужно найти способ хранить введённые пользователем заметки. Для хранения данных обычно используют структуры данных. Существует множество различных структур данных. Каждая предоставляет различные возможности по добавлению/удалению элементов и доступу к ним. Сейчас нам нужно уметь добавлять новые заметки в конец списка и выводить их все разом в порядке добавления. Для этого мы будем использовать одну из самых простых структур данных — вектор (тип Vec
). Он представляет собой непрерывную в памяти последовательность элементов, размер которой может расти по мере добавления элементов в конец последовательности. Как раз, то, что нужно.
Теперь опредилимся с выводом заметок. Мы решили использовать интерфейс коммандной строки для взаимодействия с пользователем. Для этого воспользуемся макросом println!
, который предоставляет Rust для передачи строк в стандартный вывод. Макросы в раст — очень мощный и довольно сложный инструмент. Разбор этого механизма выходит за рамки статьи. Остановимся на том, что использование макроса println!
позволяет передавать произвольное число параметров, которые будут подставлены в строку-шаблон для последующего вывода на экран. Пример:
println!("Меня зовут {}. Мне {} лет.", "Иван", 30);
// Вывод:
// Меня зовут Иван. Мне 30 лет.
//
// Фигурные скобки заменились на текстовую интерпретацию значений,
// перечисленных после строкового шаблона
Последнее, чего нам не хватает — это механизм, позволяющий читать пользовательский ввод в нашем интерфейсе командной строки. Для этого воспользуемся абстракцией над стандартным вводом, которую предоставляет стандартная библиотека — типом Stdin
. Данный тип позволяет получать данные из стандартного ввода, как в виде сырого потока байт, так и интерпретируя ввод в виде строк. Нам интересен последний вариант.
Теперь, имея инструментарий для всех необходимых нам действий, можно приступить к созданию поректа и написанию кода.
Установка инструментов разработки Rust
Все операции над исходным кодом Rust производит с помощью набора прилжений, установить которые можно воспользовавшись инструкцией с официального сайта.
Создание проекта
Теперь, когда все необходимые инструменты установлены, воспользуемся менеджером пакетов cargo для создния проекта:
cargo new notes_list
Данная команда создаст директорию с названием notes_list, в которую поместит следующие элементы:
Cargo.toml
файл, содержащий базовую конфигурацию проекта.Поддиректорию
src
с файломmain.rs
, содержащим исходный код hello world приложения.Пустой git репозиторий.
Перейдя в директорию notes_list
, можно убедиться, что созданный нами шаблонный проект собирается и запускается:
cargo run
В результате, мы должны увидеть информацию о сборке и запуске приложения:
Compiling notes_list v0.1.0 (/home/f3/work/otus/notes_list)
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/notes_list`
А также вывод приложения:
Hello, world!
Формируем алгоритм
Для начала, сформируем общий алгоритм работы программы:
Создаём вектор, в котором будут хранится заметки.
Выводим пользовательское меню в терминале.
Читаем ввод пользователя.
Если пользователь ввёл команду «show», отображаем все заметки из вектора.
Если пользователь ввёл команду «add», то читаем пользовательский ввод и добавляем новую заметку.
Если пользователь ввёл что-то другое, выходим из программы.
Нам нужно, чтобы пользователь мог добавлять/просматривать заметки многократно, поэтому пункты 2–6 будем выполнять циклично, повторяя их, пока не будет выполнено условие пункта 5 для выхода из программы.
Пишем код
Опишем в общем виде логику программы, согласно сформированному алгоритму:
fn main() {
// Создаём мутабельный вектор, в котором будут храниться заметки и
// сохраняем его в переменную `notes`.
let mut notes = Vec::new();
// Запускам цикл, который будет выполнять операции многократно,
// пока не дойдёт до операции `break` - выход из цикла.
loop {
// Вызываем функцию для вывода меню.
print_menu();
// Читаем строковую команду, введёную пользователем и
// сохраняем её в переменную `command`.
let command = read_input();
// Сравниваем команду с шаблонами и указываем действие для каждого:
match command.trim() {
// Если была введена команда для отображения заметок - отображаем.
"show" => show_notes(¬es),
// Если была введена команда для добавления заметок - добавляем.
"add" => add_note(&mut notes),
// Если пользователь ввёл что-нибудь другое - выходим из цикла.
_ => break,
}
}
}
Остановимся подробнее на некоторых моментах:
main()
— функция, с которой начнётся выполнение нашей программы. Программа завершиться, когда функция main закончит выполняться.let
— ключевое слово для объявления переменной.let mut
— объявление переменной, которую мы планируем менять.Vec::new()
— вызов метода new для типаVec
. Данный метод вернйт нам экземпляр типаVec
без содержимого.loop {...}
— цикл, выполняющийся бесконечно. Для выхода из него используется ключевое словоbreak
, либо возврат из функции (return
).match {...}
— операция соспоставления с шаблоном. После ключевого словаmatch
указывается то, что будем сопоствалять. В фигурных скобках, по очереди, указываются шаблоны (слева от=>
) и операции (справа от=>
), которые следует выполнять при совпадении с указанным шаблоном.command.trim()
— обрезаем служебные символы в конце пользовательского ввода. При вводе в терминал строки и нажатии Enter, к строке добавляется символ переноса строки. Он нам не нужен и команда trim () его отбросит.¬es
— ссылка на вектор с заметками. Ссылка позволяет читать данные из вектора.&mut notes
— мутабельная ссылка на вектор с заметками. В отличии от¬e
, позволяет менять данные вектора.break
позволяет выйти из цикла. В нашем случае из циклаloop
.После выхода из цикла завершится и функция
main
, а следовательно, и работа программы.
При попытке собрать приложение:
cargo build
Мы увидим ряд ошибок, в которых компилятор сообщает нам что не может найти функции:
print_menu()
,read_input()
,show_notes()
,add_note()
.
Исправим это, реализовав их.
fn print_menu() {
println!();
println!();
println!("**** PROGRAM MENU ****");
println!("Enter command:");
println!("'show' - show all notes");
println!("'add' - add new note");
println!("other - exit");
}
Функция print_menu()
не принимает аргументов и ничего не возвращает.
В каждой строке этой функции выводим на экран сообщения с помощью макроса println!
.
В начале выводи две пустые строки, чтобы отделить текст от предудущего.
fn read_input() -> String {
// Создаём мутабельную строку, в которую будем читать пользовательский ввод.
let mut buffer = String::new();
// Получаем объект типа `Stdin` из функции `stdin()` и вызываем метод
// `read_line()` для чтения пользовательского ввода.
// В метод передаём мутабельную ссылку на ранее созданый буфер,
// в который будут записаны данные.
std::io::stdin().read_line(&mut buffer).unwrap();
// Возвращаем буфер с пользовательским вводом из функции.
buffer
}
Функция read_input()
не принимает аргументов и возвращает строку.
Конструкция std::io::stdin()
интерпретируется как вызов функции stdin
из модуля io
находящегося в модуле std
(стандартная библиотека). Таким образом, запрашиваем у стандартной библиотеки объект, отвечающий за стандартный ввод. Далее, у этого объекта вызывается метод read_line()
, читающий строку из стандартного ввода.
fn show_notes(notes: &Vec) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note)
}
}
Цикл for
позволяет пройти по всем элементам вектора. На каждой итерации выводим заметку на экран.
fn add_note(notes: &mut Vec) {
// Сообщаем пользователю, что можно вводить заметку.
println!();
println!("Enter note:");
// Читаем пользовательский ввод.
let input = read_input();
// Получаем подстроку без служебных символов и преобразуем её в строку.
let note = input.trim().to_string();
// Добавлем заметку в конец вектора.
notes.push(note);
}
Пользуемся написанной ранее функцией read_input()
для чтения ввода пользователя.input.trim().to_string()
— Обрезаем с помощью метода trim()
служебные символы у строки, содержащей пользовательский ввод и преобразуем полученную подстроку в строку, с помощью метода to_string()
.
Теперь наше приложение будет работать и предоставлять все ожидаемые возможности. В этом можно убедиться запустив его (cargo run) и следуя его инструкциям.
Полный код:
fn main() {
// Создаём мутабельный вектор, в котором будут храниться заметки и
// сохраняем его в переменную `notes`.
let mut notes = Vec::new();
// Запускам цикл, который будет выполнять операции многократно,
// пока не дойдёт до операции `break` - выход из цикла.
loop {
// Вызываем функцию для вывода меню.
print_menu();
// Читаем строковую команду, введёную пользователем и
// сохраняем её в переменную `command`.
let command = read_input();
// Сравниваем команду с шаблонами и указываем действие для каждого:
match command.trim() {
// Если была введена команда для отображения заметок - отображаем.
"show" => show_notes(¬es),
// Если была введена команда для добавления заметок - добавляем.
"add" => add_note(&mut notes),
// Если пользователь ввёл что-нибудь другое - выходим из цикла.
_ => break,
}
}
}
fn print_menu() {
println!();
println!();
println!("**** PROGRAM MENU ****");
println!("Enter command:");
println!("'show' - show all notes");
println!("'add' - add new note");
println!("other - exit");
}
fn read_input() -> String {
// Создаём мутабельную строку, в которую будем читать пользовательский ввод.
let mut buffer = String::new();
// Получаем объект типа `Stdin` из функции `stdin()` и вызываем метод
// `read_line()` для чтения пользовательского ввода.
// В метод передаём мутабельную ссылку на ранее созданый буфер,
// в который будут записаны данные.
std::io::stdin().read_line(&mut buffer).unwrap();
// Возвращаем буфер с пользовательским вводом из функции.
buffer
}
fn show_notes(notes: &Vec) {
// Выводим пустую строку.
println!();
// Для каждой заметки в заметках ...
for note in notes {
// выводим её на экран.
println!("{}", note)
}
}
fn add_note(notes: &mut Vec) {
// Сообщаем пользователю, что можно вводить заметку.
println!();
println!("Enter note:");
// Читаем пользовательский ввод.
let input = read_input();
// Получаем подстроку без служебных символов и преобразуем её в строку.
let note = input.trim().to_string();
// Добавлем заметку в конец вектора.
notes.push(note);
}
Итог
Выполнив ряд шагов, мы прошли от идеи до реализации простого прилжения:
Формирование требований,
Декомпозиция задачи,
Формирование алгоритма,
Написание кода.
На курсе Rust developer мы рассматриваем все концепции языка, переходя от простых, описанных выше, к более сложным, таким как трейты, обобщённое программирование, умные указатели, многопоточность, архитектура и другие.