Rust
Articles in Section
External links:
- Rustup toolchain installer - https://rustup.rs/
- Rustup add-ons needed for formatting and as dependencies:
rustup component add rustfmt
rustup component add rust-src
- Install Cargo-watch to rerun code upon save:
cargo install cargo-watch
git clone https://github.com/AstroNvim/AstroNvim ~/.config/nvim
nvim +PackerSync
# After install use commands:
:LspInstall rust -> rust-analyzer
:TSInstall rust
- Neovide GUI upgrade on Astro Vim
git clone https://github.com/neovide/neovide
cd neovide
cargo build --release
cargo install evcxr_repl
cargo install irust
cargo install --locked bacon
bacon test # run from project folder
Cargo
Use Cargo for managing projects.
cargo new test_project // create project with binary file src/main.rs
// OR
cargo new test_project --lib // create project with library file src/lib.rs
cd test_project
Source code is in src folder.
cargo build # build project for debug, do not run
cargo run # build & run project
cargo check # fast compiling
cargo build --release # slow build with optimizations for speed of release version
Cargo rerun upon saving code:
cargo watch -q -c -x 'run -q'
-x - rerun upon code change
-c - clear terminal each time
-q - quiet mode
Documentation of methods & traits used in code can be compiled an opened in browser with Cargo command:
Cargo Examples
Create “examples” folder beside the “src” folder. Create a file named, for example, “variables.rs” and place some test code in the folder. Then in the project root folder run:
cargo run --example variables
This will compile+build the code in examples folder, file “variables.rs”. Very convenient to try test different stuff. For live development do:
cargo watch -q -c -x 'run -q --example variables'
Panic Response
В ответ на панику, по умолчанию программа разматывает стек (unwinding) - проходит по стеку и вычищает данные всех функций. Для уменьшения размера можно просто отключать программу без очистки - abort. Для этого в файле Cargo.toml надо добавить в разделы [profile]
:
[profile.release]
panic = 'abort'
Cargo Clippy linter
Example of Clippy config:
cargo clippy --fix -- \
-W clippy::pedantic \
-W clippy::nursery \
-W clippy::unwrap_used \
-W clippy::expect_used
Clippy has a markdown book:
cargo install mdbook
# Run from top level of your rust-clippy directory:
mdbook serve book --open
# Goto http://localhost:3000 to see the book
Important Cargo libs
- Tokio - async runtime
- Eyre & color-eyre - type signatures, error report handling
- Tracing - collect diagnostic info
- Reqwest - HTTP requests library, async
- Rayon - data parallelism library, without data races
- Clap - commandline passing library
- SQLX - compile-checked SQL, async
- Chrono - time/date library
- EGUI - web-gui @60FPS, runs in Webassembly
- Yew.rs - web-library like React.js
Cargo OFFLINE
Для создания локального сервера можно скачать все пакеты Cargo с помощью проекта Panamax.
cargo install --locked panamax
panamax init ~/test/my-mirror
Нужно зайти в папку my-mirror, проверить настройки в файле mirror.toml. И далее запустить синхронизацию:
panamax sync ~/test/my-mirror
Далее, можно публиковать зеркало по веб через встроенный сервер (по порту 8080):
panamax serve ~/test/my-mirror
На индекс странице сервера будет справка по подключению Rust клиентов к зеркалу. В частности, посредством создания файла настроек ~/.cargo/config :
[source.my-mirror]
registry = "http://panamax.local/crates.io-index"
[source.crates-io]
replace-with = "my-mirror"
Rust Prelude
Rust has a Prelude - a set of libraries included in every project.
See current libs included in Prelude
Allow Unused
Turn off noise about unused objects while learning with a crate attribute:
#![allow(unused)] // silence unused warnings while learning
fn main() {}
std::io::stdin library is used to get user input from standard input stream. Not included in Prelude:
use std:io
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to load");
.expect handles Err variant of read_line function, crashing the program with the defined error message.
Subsections of Rust
Libraries
Articles in Section
Subsections of Libraries
External link: https://docs.rs/itertools/latest/itertools/
Working with iterators
Sorting() a string of letters (with rev() - reverse order)
use itertools::Itertools;
let text = "Hello world";
let text_sorted = text.chars().sorted().rev().collect::<String>();
// rev() - Iterate the iterable in reverse
println!("Text: {}, Sorted Text: {}", text, text_sorted);
// Text: Hello world, Sorted Text: wroollledH
Counts() подсчёт количества одинаковых элементов в Array
use itertools::Itertools;
let number_list = [1,12,3,1,5,2,7,8,7,8,2,3,12,7,7];
let mode = number_list.iter().counts(); // Itertools::counts()
// возвращает Hashmap, где ключи взяты из массива, значения - частота
for (key, value) in &mode {
println!("Число {key} встречается {value} раз");
}
rand
Generating random numbers
External links:
Using rand lib example:
use rand::Rng;
fn main() {
let secret_of_type = rand::thread_rng().gen::<u32>();
let secret = rand::thread_rng().gen_range(1..=100);
println!("Random nuber of type u32: {secret_of_type}");
println!("Random nuber from 1 to 100: {}", secret); }
CLI Arguments
ARGS
Аргументы командной строки можно захватить с помощью методов args()+collect() библиотеки env. Нужно поместить их в строковый вектор.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
for arg in args.iter() {
println!("{}", arg);
} }
Первым параметров всегда идёт имя самой программы.
Closures
Closure
- Замыкания можно присвоить переменным и вызывать;
- В отличие от функций, замыкания могут использовать переменные вне своего блока.
Map и Filter
.map(<closure>)
передаёт владение элементами итератора замыканию, чтобы их можно было трансформировать в другие элементы, которые далее возвращает замыкание.
.filter(<closure>)
возвращает оригинальные элементы, когда предикат замыкания возвращает true. Таким образом, отдавать владение элементами замыканию нельзя, и нужно передавать по ссылке.
let a = (0..3).map(|x| x * 2);
for i in a {
println!("map i = {}", i);
}
let a = (0..3).filter(|&x| x % 2 == 0);
for i in a {
println!("filter i = {}", i);
}
Fold
Каждая итерация fold
принимает 2 аргумента:
- Исходное значение счётчика;
- Замыкание, которое тоже принимает 2 аргумента: счётчик и объект. Замыкание возвращает счётчик.
fold
используется для превращения множества в 1 значение. Пример для получения суммы всех чётных чисел:
pub fn main() {
let even_sum = (1..=10).fold(0, |acc, num| if num % 2 == 0 { acc + num } else { acc });
println!("{even_sum:?}");
}
fold
можно часто заменить другими методами. Например, код выше можно заменить на отбор всех чётных числе с помощью filter
и их сложение с помощью sum
:
(0..=10).filter(|n| *n % 2 == 0).sum()
Наравне с sum
также популярны product
и collect
.
Счётчик у fold
необязательно числовой, можно использовать, например, String:
pub fn giant_grunts(initial: char) -> String {
["Bee", "Fee", "Gee", "Fi", "Hi", "Fo", "Mo", "Fum", "Tum"].iter().fold(
String::new(),
|acc, grunt| if grunt.starts_with(initial) { acc + grunt } else { acc },
)}
pub fn main() {
let song = giant_grunts('F');
println!("{song:?}"); // "FeeFiFoFum"
}
All
Замыкание all
возвращает True
, если все элементы в замыкании соответствуют условию.
let a: Vec<i32> = vec![1, 2, 3, 4];
print!("{}\n", a.into_iter().all(|x| x > 1)); // false
Для пустого вектора замыкание all
вернёт True
:
let a: Vec<i32> = vec![];
print!("{}\n", a.into_iter().all(|x| x > 1)); // true
Constants
Константы
Это глобальные по области действия значения, неизменяемые. При объявлении нужно всегда сразу указывать их тип.
const MAX_LIMIT: u8 = 15; // тип u8 обязательно надо указать
fn main() {
for i in 1..MAX_LIMIT {
println!("{}", i);
} }
Cross-compiling
Проверка системы
Сделать новый проект, добавить в него библиотеку cargo add current_platform
. Далее создаём и запускаем код проверки среды компиляции и исполнения:
use current_platform::{COMPILED_ON, CURRENT_PLATFORM};
fn main() {
println!("Run from {}! Compiled on {}.", CURRENT_PLATFORM, COMPILED_ON);
}
- Посмотреть текущую ОС компиляции:
rustc -vV
- Посмотреть список ОС для кросс-компиляции:
rustc --print target-list
Формат списка имеет поля <arch><sub>-<vendor>-<sys>-<env>
, например, x86_64-apple-darwin
=> macOS на чипе Intel.
Настройка кросс-компилятора
Нужно установить cross: cargo install cross
установит его по пути $HOME/.cargo/bin
Далее, нужно установить вариант Docker в системе:
- На macOS => Docker Desktop;
- На Linux => Podman
Запуск кода на компиляцию под ОС Windows: cross run --target x86_64-pc-windows-gnu
=> в папке target/x86_64-pc-windows-gnu/debug
получаем EXE-файл с результатом.
❗Компиляция проходит через WINE.
Проверка среды компиляции
Cross поддерживает тестирование других платформ. Добавка проверки:
mod tests {
use current_platform::{COMPILED_ON, CURRENT_PLATFORM};
#[test]
fn test_compiled_on_equals_current_platform() {
assert_eq!(COMPILED_ON, CURRENT_PLATFORM);
} }
Запустить проверку локально: cargo test
Запустить проверку с кросс-компиляцией: cross test --target x86_64-pc-windows-gnu
На Linux/macOS проверка пройдёт, а вот при компиляции под Windows - нет:
`test tests::test_compiled_on_equals_current_platform … FAILED
Добавка платформенно-специфичного кода
Можно вписать код, который запустится только на определённой ОС, например, только на Windows:
use current_platform::{COMPILED_ON, CURRENT_PLATFORM};
#[cfg(target_os="windows")]
fn windows_only() {
println!("This will only print on Windows!");
}
fn main() {
println!("Run from {}! Compiled on {}.", CURRENT_PLATFORM, COMPILED_ON);
#[cfg(target_os="windows")]
{
windows_only();
}
}
Enums
Перечисления
Тип перечислений позволяет организовать выбор из нескольких вариантов в пределах логического множества.
match
Аналог switch в других языках, однако, круче: его проверка не сводится к bool, а также реакция на каждое действие может быть блоком:
fn main() {
let m = Money::Kop;
println!("Я нашёл кошелёк, а там {}p",match_value_in_kop(m));
}
fn match_value_in_kop(money: Money) -> u8 {
match money {
Money::Rub => 100,
Money::Kop => {
println!("Счастливая копейка!");
1
} } }
Проверка условия и запуск соответствующего метода:
struct State {
color: (u8, u8, u8),
position: Point,
quit: bool,
}
impl State {
fn change_color(&mut self, color: (u8, u8, u8)) {
self.color = color; }
fn quit(&mut self) {
self.quit = true; }
fn echo(&self, s: String) {
println!("{}", s); }
fn move_position(&mut self, p: Point) {
self.position = p; }
fn process(&mut self, message: Message) {
match message { // проверка и запуск одного из методов
Message::Quit => self.quit(),
Message::Echo(x) => self.echo(x),
Message::ChangeColor(x, y, z) => self.change_color((x, y, z)),
Message::Move(x) => self.move_position(x),
} } }
if let
В случае, когда выбор сводится к тому, что мы сравниваем 1 вариант с заданным паттерном и далее запускаем код при успехе, а в случае неравенства ничего не делаем, можно вместо match применять более короткую конструкцию if-let:
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {}", max),
_ => (), // другие варианты ничего не возвращают
}
Превращается в:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {}", max);
}
Применение if-let - это синтаксический сахар, укорачивает код, однако, лишает нас проверки наличия обработчиков на все варианты возвращаемых значений как в конструкции match.
Сравнение величин
Для сравнения значений в переменных есть метод std::cmp, который возвращает объект типа enum Ordering с вариантами:
use std::cmp::Ordering;
use std:io;
fn main() {
let secret_number = 42;
let mut guess = String::new();
io::stdin()
.read_line(&guess)
.expect("Read line failed!");
let guess :i32 = guess.trim().parse()
.expect("Error! Non-number value entered.");
match guess.cmp(&secret_number) {
Ordering::Greater => println!("Number too big"),
Ordering::Less => println!("Number too small"),
Ordering::Equal => println("Found exact number!")
}
}
Error Handling
External link: https://youtu.be/f82wn-1DPas
Подготовка примера
Допустим, мы берём вектор из строк-чисел, складываем их и возвращаем сумму как строку:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s);
}
return accum.to_string();
}
fn main() {
let v = vec![String::from("3"), String::from("4")]; // Правильный ввод
let total = sum_str_vec(v);
println!("Total equals: {:?}", total);
let v = vec![String::from("3"), String::from("abc")]; // Неправильный ввод
let total = sum_str_vec(v);
println!("Total equals: {:?}", total);
}
Для конвертации строки в числа, нужно реализовать функцию to_int
в соответствии со стратегиями обработки ошибочного ввода. Конвертацию мы делаем функцией parse()
, которая возвращает тип Result<T,E>
, где T - значение, E - код ошибки.
Стратегия 1 - паника
В случае неверного ввода, программа полностью останавливается в панике. Метод unwrap()
у типа Result<T,E>
убирает проверки на ошибки и есть договор с компилятором о том, что ошибки в этом месте быть не может. Если она есть, программа падает с паникой:
fn to_int(s: &str) -> i32 {
s.parse().unwrap() }
Стратегия 2 - паника с указанием причины
В случае неверного ввода, программа сообщает фразу, заданную автором, далее полностью останавливается в панике. Метод expect()
аналогичен unwrap()
, но выводит сообщение:
fn to_int(s: &str) -> i32 {
s.parse().expect("Error converting from string") }
Стратегия 3 - обработать то, что возможно обработать
Можно сконвертировать и прибавить к результату те строки, которые позволяют это сделать, проигнорировать остальные. Метод unwrap_or()
позволяет указать возвращаемое значение в случае ошибки:
fn to_int(s: &str) -> i32 {
s.parse().unwrap_or(0) } // при вводе "abc" вернётся 0, сумма = "3"
Более предпочтительный вариант использовать закрытие unwrap_or_else()
, так как метод unwrap_or()
будет вызван ДО того как будет отработана основная команда, ВНЕ ЗАВИСИМОСТИ от того, будет ли её результат Some или None. Это потеря производительности, а также потенциальные глюки при использовании внутри unwrap_or() сложных выражений. Закрытие unwrap_or_else()
будет вызвано только в случае None, иначе же эта ветка не обрабатывается:
fn to_int(s: &str) -> i32 {
s.parse().unwrap_or_else(|_| 0) }
Стратегия 4 - решение принимает вызывающая функция
Вместо возврата числа, возвращаем тип Option<число>
- в зависимости от успешности функции, в нём будет либо само число, либо None
:
fn to_int(s: &str) -> Option<i32> {
s.parse().ok() // ok() конвертирует Result<T,E> в Option<T>
И тогда вызывающая функция должна обработать результат:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += match to_int(&s) {
Some(v) => v,
None => {
println!("Error converting a value, skipped");
0 // вернётся 0 +в лог пойдёт запись о пропуске
}, } }
return accum.to_string();
}
Более короткий вариант записи через if let
:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
if let Some(val) = to_int(&s) {
accum += val;
} else { println!("Error converting a value, skipped"); }
}
return accum.to_string();
}
Тип Option<T>
также имеет метод unwarp_or()
, отсюда ещё вариант записи:
fn sum_str_vec (strs: Vec<String>) -> String {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).unwrap_or(0); // раскрываем Option<T>
}
return accum.to_string();
}
Стратегия 5 - в случае проблем, передать всё в основную программу
Вместо передачи значения из функции, в случае каких-либо проблем, мы возвращаем None
:
fn sum_str_vec (strs: Vec<String>) -> Option<String> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s)?; // в случае None, ? передаёт его далее на выход
}
Some(accum.to_string()) // на выход пойдёт значение или None
}
Стратегия 6 - передать всё в основную программу с объяснением ошибки
Мы возвращаем проблему в основную программу с объясением проблемы. Для этого заводим структуру под ошибку, и передаём уже не объект Option<T>
, а объект Result<T,E>
, где E = SummationError. Для такого объекта есть метод ok_or()
, который либо передаёт значение, либо передаёт ошибку нужного типа:
#[derive(Debug)]
struct SummationError;
fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).ok_or(SummationError)?;
}
Ok(accum.to_string())
}
Вместо выдумывать свой собственный тип и конверстировать вывод метода parse()
из Result<T,E>
в Option<T>
, а потом обратно, можно сразу протащить ошибку в объекте Result<T,E>
в главную программу:
use std::num::ParseIntError; // тип ошибки берём из библиотеки
fn to_int(s: &str) -> Result<i32, ParseIntError> {
s.parse() // parse возвращает просто Result<T,E>
}
fn sum_str_vec (strs: Vec<String>) -> Result<String, ParseIntError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s)?; } // ? передаёт ошибку нужного типа далее
Ok(accum.to_string())
}
Однако, мы хотим скрыть подробности работы и ошиби от главной програмы и передать ей ошибку в понятном виде, без разъяснения деталей её возникновения. Для этого можно сделать трансляцию ошибки из библиотечной в собственный типа, и далее передать методом map_err():
use std::num::ParseIntError;
#[derive(Debug)]
struct SummationError;
fn to_int(s: &str) -> Result<i32, ParseIntError> {
s.parse()
}
fn sum_str_vec (strs: Vec<String>) -> Result<String, SummationError> {
let mut accum = 0i32;
for s in strs {
accum += to_int(&s).map_err(|_| SummationError)?; // конвертация ошибки
} // перед передачей
Ok(accum.to_string())
}
Где можно использовать ператор ?
Оператор ?
можно использовать только в функциях для возврата совместимых значений типа Result<T,E>
, Option<T>
или иных данных со свойством FromResidual
. Для работы такого возврата в заголовке функции должен быть прописан возврат нужного типа данных.
При использовании ?
на выражении типа Result<T,E>
или Option<T>
, ошибка Err(e)
или None
будет возвращена рано из функции, а в случае успеха - выражение вернёт результат, и функция продолжит работу.
Пример функции, которая возвращает последний символ 1ой строки текста:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
} // lines() возвращает итератор на текст
// next() берёт первую строку текста. Если текст пустой - сразу возвращаем None
Files
Чтение текста из файлов в строку
Для чтения файлов, нужно сначала добавить несколько методов из библиотек: FIle - открытие файлов и Read - чтение, который входит в prelude.
se std::fs::File; // импорт File
use std::io::prelude::*; // импорт Read
fn main() {
// читаемый файл находится в корне проекта.
let mut file = File::open("access.log.10").expect("File not opened!");
let mut contents = String::new(); // закачать файл в переменную-строку
file.read_to_string(&mut contents).expect("Cannot read file!");
println!("File contents: \n\n{}",contents);
}
Чтение в вектор из байтов (vector of bytes)
Чтение файла в память целиком как вектора байтов - для бинарных файлов, либо для частого обращения к содержимому:
use std::io::Read;
use std::{env, fs, io, str};
fn main() -> io::Result<()> {
let mut file = fs::File::open("test_file.txt")?;
let mut contents = Vec::new();
file.read_to_end(&mut contents);
println!("File contents: {:?}", contents); // вывод байт
let text = match str::from_utf8(&contents) { // перевод в строку UTF8
Ok(v) => v,
Err(e) => panic!("Invalid UTF-8: {e}"),
};
println!("Result: {text}"); // вывод строкой
Ok(())
}
Чтение текста из файла в буфер по строчкам
Нужно добавить к библиотеке File также библиотеку организации буфера чтения, а также обработать ошибки открытия файла и чтения.
use std::io::{BufRead, BufReader};
use std::{env, fs, io};
fn main() -> io::Result<()> {
let file = fs::File::open("textfile.txt")?;
let reader = BufReader::new(file);
for line in reader.lines() { // io::BufRead::lines()
let line = line?;
println!("{line}");
}
Ok(())
Запись в файлы
use std::fs::File;
use std::io::prelude::*;
fn main() {
let mut file = File::create("report.log").expect("File not created!");
file.write_all(b"report.log");
}
Flow Control
IF-ELSE
В Rust есть управление потоком программы через конструкции IF, ELSE IF, ELSE:
let test_number = 6;
if test_number % 4 == 0 {
println!("Divisible by 4");
} else if test_number % 3 == 0 { // Проверка останавливается на первом
println!("Divisible by 3"); // выполнимом условии, дальнейшие проверки
} else if test_number % 2 == 0 { // пропускаются.
println!("Divisible by 2");
} else {
println!("Number is not divisible by 4, 3 or 2");
}
Конструкция IF является выражением (expression), поэтому можно делать присваивание:
let condition = true;
let number = if condition { "aa" } else { "ab" }; // присваивание результата IF
println!("Number is {number}");
LOOPS
Три варианта организовать цикл: через операторы loop, while, for.
LOOP - организация вечных циклов. Конструкция LOOP является выражением (expression), поэтому можно делать присваивание.
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // выход из вечного цикла
}
}; // ";" нужно, т.к. было выражение
println!("The result is {result}");
Если делать вложенные циклы, то можно помечать их меткой, чтобы выходить с break на нужный уровень.
let mut count = 0;
'counting_up: loop { // метка внешнего цикла
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up; // goto метка
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
WHILE - цикл с условием. Можно реализовать через loop, но с while сильно короче.
let mut number = 10;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("ЗАПУСК!");
FOR - цикл по множествам элементов. В том числе можно задать подмножество чисел.
for i in (1..10).rev() { // .rev() - выдача подмножества в обратную сторону
println!("Value: {i}");
}
println!("ЗАПУСК!");
Generics
Usage
Generics нужны для того, чтобы не повторять один и тот-же код несколько раз для разных типов данных. Это один из трёх основных способов экономии на повторении кода наравне с макросами и интерфейсами traits.
Пример: нужно сортировать 2 набора данных - массив чисел, массив символов.
fn largest_i32(n_list: &[i32]) -> &i32 {
let mut largest_num = &n_list[0];
for i in n_list {
if largest_num < i {
largest_num = i;
}
}
largest_num
}
fn largest_char(n_list: &[char]) -> &char {
let mut largest_char = &n_list[0];
for i in n_list {
if largest_char < i {
largest_char = i;
}
}
largest_char
Вместо того, чтобы писать 2 почти идентичные функции под 2 типа данных, можно объединить оба типа в 1 функцию, указав “неопределённый тип”.
Note
Нужно учесть, что не все типы данных имеют возможность сравнения объектов между собой, возможность выстроить их в порядок (Order). Поэтому в примере ниже надо дополнить тип в заголовке интерфейсом-свойством (trait) порядка.
fn largest_universal<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
// <T> = неопределённый тип, со свойством упорядочивания PartialOrd
let mut largest = &list[0];
for item in list {
if largest < item {
largest = item;
}
}
largest
}
fn main() {
let num_list = vec![11, 6, 33, 56, 13];//упорядочить числа
println!("Largest number: {}", largest_universal(&num_list));
let char_list = vec!['y','m','a','q'];//упорядочить символы в той же функции
println!("Largest char: {}", largest_universal(&char_list));
}
Структуры с неопределёнными типами
Можно создавать структуры, тип данных которых заранее неопределён. Причём в одной структуре можно сделать несколько разных типов.
struct Point<T,U> { // <T> и <U> - 2 разных типа
x: T,
y: U,
}
let integer = Point{x:5,y:6}; // в оба поля пишем числа типа i32
let float_int = Point{x:1,y:4.2}; // в поля пишем разные типы i32 и f32
Аналогично неопределённые типы можно делать в перечислениях:
enum Option<T> {
Some(T), // <T> - возможное значение любого типа, которое может быть или нет
None,
}
enum Result<T, E> {
Ok(T), // T - тип результата успешной операции
Err(E), // Е - тип ошибки
}
Методы со структурами с неопределёнными типами
Можно прописать методы над данными неопределённого типа. Важно в заголовке метода указывать при этом неопределённый тип, чтобы помочь компилятору отделить его от обычного типа данных. Можно при этом сделать реализацию для конкретных типов данных, например, расчёт расстояния до точки от центра координат будет работать только для чисел с плавающей точкой. Для других типов данных метод расчёта работать не будет:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> { // указываем неопределённый тип в заголовке
fn show_x(&self) -> &T { // метод возвращает поле данных
&self.x
} }
impl Point<f32> { // указываем конкретный тип float в методе,
// чтобы только для него реализовать расчёт. Для не-float метод не будет работать
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
} }
fn main() {
let p = Point{x:5,y:6};
println!("P.x = {}",p.show_x()); // вызов метода для экземпляра p
}
Tip
Код с неопределёнными типами не замедляет производительность, потому что компилятор проходит и подставляет конкретные типы везде, где видит generics (“monomorphization” of code).
Hashmaps
Hashmap<K, V>
Это изменяемая структура словарь (“dictionary” в Python), которая хранит пары “ключ->значение”. В Rust Prelude она не входит, макроса создания не имеет. Поэтому нужно указывать библиотеку явно и явно создавать структуру.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Alpha"),1);
scores.insert(String::from("Beta"),2);
scores.insert(String::from("Gamma"),3);
}
Все ключи Hashmap должны быть уникальны и одного типа, все значения должны быть одного типа.
Warning
Значения с кучи типа String перемещаются (move) в Hashmap, который становится их владельцем.
Взятие значения по ключу из Hashmap с помощью get
нужно сопровождать проверкой - есть ли в памяти запрашиваемые ключ и значение:
let name = String::from("Gamma");
if let Some(letter_num) = scores.get(&name) {
println!("{}",letter_num);
} else { println!("Value not in HashMap!"); }
Итерация по Hashmap похожа на итерацию по вектору:
for (key, value) in &scores {
println!("{key} -> {value}"); }
Обновление Hashmap
Есть ряд стратегий обновления значений в Hashmap:
scores.insert(String::from("Gamma"),3); // вставка дважды значений по одному
scores.insert(String::from("Gamma"),6); // ключу сохранит последнее значение
- Записывать значение, если у ключа его нет:
scores.entry(String::from("Delta")).or_insert(4); // entry проверяет наличие
// значения, or_insert возвращает mut ссылку на него, либо записывает новое
// значение и возвращает mut ссылку на это значение.
- Записывать значение, если ключа нет. Если же у ключа есть значение, модифицировать его:
use std::collections::HashMap;
let mut map: HashMap<&str, u32> = HashMap::new();
map.entry("poneyland") // первое добавление
.and_modify(|e| { *e += 1 })
.or_insert(42); // добавит ключ "poneyland: 42"
assert_eq!(map["poneyland"], 42);
map.entry("poneyland") // второе добавление найдёт ключ со значением
.and_modify(|e| { *e += 1 }) // и модифицирует его
.or_insert(42);
assert_eq!(map["poneyland"], 43);
- Записывать значение, если ключа нет, в виде результата функции. Эта функция получает ссылку на значение ключа key, который передаётся в .entry(key):
use std::collections::HashMap;
let mut map: HashMap<&str, usize> = HashMap::new();
map
.entry("poneyland")
.or_insert_with_key(|key| key.chars().count());
assert_eq!(map["poneyland"], 9);
- Поднимать старое значение ключа, проверять его перед перезаписью:
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}",map); // {"wonderful": 1, "hello": 1, "world": 2}
Инициализация HashMap со значениями по-умолчанию
Поведение аналогично типу defaultdict
в Python. Заполнять ключами HashMap:
- в случае отсутствия ключа, создавать его со значением по-умолчанию (0);
- в случае присутствия ключа, добавлять к значению +1.
use std::collections::HashMap;
pub fn main() {
let num_vec = vec![1, 2, 1, 3, 5, 2, 1, 4, 6];
let mut number_count: HashMap<i32, i32> = HashMap::new();
for key in num_vec {
*number_count.entry(key).or_default() += 1;
}
for (k, v) in number_count {
print!("{} -> {}; ", k, v);
}
}
// 4 -> 1; 1 -> 3; 2 -> 2; 6 -> 1; 5 -> 1; 3 -> 1;
Modules Structure
External link: https://doc.rust-lang.org/stable/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
Правила работы с модулями
- Crate Root. При компиляции crate, компилятор ищет корень: src/lib.rs для библиотеки, либо src/main.rs для запускаемого файла (binary);
- Модули. При декларировании модуля в crate root, например,
mod test
, компилятор будет искать его код в одном из мест:
- Сразу после объявления в том же файле (inline);
- В файле src/test.rs;
- В файл src/test/mod.rs - старый стиль, поддерживается, но не рекомендуется.
- Подмодули. В любом файле, кроме crate root, можно объявить подмодуль, например,
mod automa
. Компилятор будет искать код подмодуля в одном из мест:
- Сразу после объявления в том же файле (inline);
- В файле src/test/automa.rs;
- В файле src/test/automa/mod.rs - *старый стиль, поддерживается, но не рекомендуется.
- Путь до кода. Когда модуль часть crate, можно обращаться к его коду из любого места этой crate в соответствии с правилами privacy и указывая путь до кода. Например, путь до типа robot в подмодуле automa будет
crate::test::automa::robot
;
- Private/public. Код модуля скрыт от родительских модулей по умолчанию. Для его публикации нужно объявлять его с
pub mod
вместо mod
;
- Use. Ключевое слово
use
нужно для сокращения написания пути до кода: use crate::test::automa::robot
позволяет далее писать просто robot
для обращения к данным этого типа.
Note
Нужно лишь единожды внести внешний файл с помощью mod
в дереве модулей, чтобы он стал частью проекта, и чтобы другие файлы могли на него ссылаться. В каждом файле с помощью mod
не надо ссылаться, mod
- это НЕ include
из Python и других языков программирования.
Пример структуры
my_crate
├── Cargo.lock
├── Cargo.toml
└── src
├── test
│ └── automa.rs
├── test.rs
└── main.rs
Вложенные модули
Для примера, возьмём библиотеку, обеспечивающую работу ресторана. Ресторан делится на части front - обслуживание посетителей, и back - кухня, мойка, бухгалтерия.
mod front {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
Пример обращения к функции вложенного модуля
Объявление модуля публичным с помощью pub
не делает его функции публичными. Нужно указывать публичность каждой функции по отдельности:
mod front_of_house {
pub mod hosting { // модуль публичен, чтобы к нему обращаться
pub fn add_to_waitlist() {} // функция явно публична
// несмотря на публичность модуля, к функции обратиться нельзя
// если она непублична
}
}
pub fn eat_at_restaurant() {
// Абсолютный путь через корень - ключевое слово crate
crate::front_of_house::hosting::add_to_waitlist();
// Относительный путь
front_of_house::hosting::add_to_waitlist();
}
Обращние к функции выше уровнем
Относительный вызов функции можно сделать через super
(аналог “..” в файловой системе):
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order(); // вызов функции в родительском модуле
}
fn cook_order() {}
}
Обращение к структурам и перечислениям
Поля структур приватны по умолчанию. Обозначение структуры публичной с pub
не делает её поля публичными - каждое поле нужно делать публичным по отдельности.
mod back_of_house {
pub struct Breakfast { // структура обозначена как публичная
pub toast: String, // поле обозначено публичным
seasonal_fruit: String, // поле осталось приватным
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
} } } }
pub fn eat_at_restaurant() {
// Обращение к функции. Без функции к структуре с приватным полем
// не получится обратиться:
let mut meal = back_of_house::Breakfast::summer("Rye");
// запись в публичное поле:
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// если раскомменировать строку далее, будет ошибка компиляции:
// meal.seasonal_fruit = String::from("blueberries");
}
Поля перечислений публичны по умолчанию. Достатоно сделать само перечисление публичным pub enum
, чтобы видеть все его поля.
mod back_of_house {
pub enum Appetizer { // обозначаем перечисление публичным
Soup,
Salad,
} }
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Обращение к объектам под другим именем
С помощью as
можно создать ярлык на объект в строке с use
. Особенно это удобно в случае, когда нужно обратиться к одинаковым по имени объектам в разных модулях:
use std::fmt::Result;
use std::io::Result as IoResult; // IoResult - ярлык на тип Result в модуле io
fn function1() -> Result {
// ... }
fn function2() -> IoResult<()> {
// ... }
Ре-экспорт объектов
При обращении к объекту с помощью use
, сам ярлык этот становится приватным - через него могут обращаться только функции текущего scope. Для того, чтобы из других модулей функции могли тоже обратиться через этот ярлык, нужно сделать его публичным:
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(); // обращение к функции через ярлык
}
Работа с внешними библиотеками
Внешние библиотеки включаются в файл Cargo.toml. Далее, публичные объекты из них заносятся в scope с помощью use
.
# Cargo.toml
rand = "0.8.5"
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Если нужно внести несколько объектов из одной библиотеки, то можно сократить количество use
:
//use std::cmp::Ordering;
//use std::io;
use std::{cmp::Ordering, io}; // список объектов от общего корня
//use std::io;
//use std::io::Write;
use std::io{self, Write}; // включение самого общего корня в scope
use std::collections::*; // включение всех публичных объектов по пути
Warning
Следует быть осторожным с оператором glob - *
, так как про внесённые с его помощью объекты сложно сказать, где именно они были определены.
Ownership and References
Ownership
Объявленная переменная, обеспеченная памятью кучи (heap) - общей памятью (не стека!) всегда имеет владельца. При передаче такой переменной в другую переменную, либо в функцию, происходит перемещение указателя на переменную = смена владельца. После перемещения, нельзя обращаться к исходной переменной.
let s1 = String::from("hello"); // строка в куче создана из литералов в стеке
let s2 = s1; // перемещение
println!("{}, world!", s1); // ошибка! Вызов перемещённой переменной
Решения:
- Можно сделать явный клон переменной со значением;
let s1 = String::from("hello");
let s2 = s1.clone(); // полный клон. Медленно и затратно,
println!("s1 = {}, s2 = {}", s1, s2); // но нет передачи владения
- Передавать ссылку на указатель. Ссылка на указатель - ‘&’, раскрыть ссылку на указатель - ‘*’.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // передача ссылки на указатель
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize { // приём ссылки на указатель
s.len()
}
References
Для внесения изменений по ссылке на указатель, нужно указать это явно через ‘mut’.
fn main() {
let mut s = String::with_capacity(32); // объявить размер блока данных заранее, чтобы потом не довыделять при закидывании данных в строку = быстрее
change(&mut s); // передача изменяемой ссылки
}
fn change(some_string: &mut String) { // приём изменяемой ссылки на указатель
some_string.push_str("hello, world");
}
Tip
Правила:
- В области жизни может быть лишь одна изменяемая ссылка на указатель (нельзя одновременно нескольким потокам писать в одну область памяти);
- Если есть изменяемая ссылка на указатель переменной, не может быть неизменяемых ссылок на указатель этой же переменной (иначе можно перезаписать данные в процессе их же чтения);
- Если ссылка на указатель переменной неизменяемая, можно делать сколько угодно неизменяемых ссылок на указатель (можно вместе читать одни и те же данные);
- Конец жизни ссылки определяется её последним использованием. Можно объявлять новую ссылку на указатель, если последняя изменяемая ссылка по ходу программы более не вызывается.
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 вышла из области жизни, поэтому можно объявить новую ссылку на указатель.
let r2 = &mut s;
Regular Expressions
Внешняя ссылка: https://docs.rs/regex/latest/regex/
Справка по символам
1 символ
. any character except new line (includes new line with s flag)
[0-9] any ASCII digit
\d digit (\p{Nd})
\D not digit
\pX Unicode character class identified by a one-letter name
\p{Greek} Unicode character class (general category or script)
\PX Negated Unicode character class identified by a one-letter name
\P{Greek} negated Unicode character class (general category or script)
Классы символов
[xyz] A character class matching either x, y or z (union).
[^xyz] A character class matching any character except x, y and z.
[a-z] A character class matching any character in range a-z.
[[:alpha:]] ASCII character class ([A-Za-z])
[[:^alpha:]] Negated ASCII character class ([^A-Za-z])
[x[^xyz]] Nested/grouping character class (matching any character except y and z)
[a-y&&xyz] Intersection (matching x or y)
[0-9&&[^4]] Subtraction using intersection and negation (matching 0-9 except 4)
[0-9--4] Direct subtraction (matching 0-9 except 4)
[a-g~~b-h] Symmetric difference (matching `a` and `h` only)
[\[\]] Escaping in character classes (matching [ or ])
[a&&b] An empty character class matching nothing
Повторы символов
x* zero or more of x (greedy)
x+ one or more of x (greedy)
x? zero or one of x (greedy)
x*? zero or more of x (ungreedy/lazy)
x+? one or more of x (ungreedy/lazy)
x?? zero or one of x (ungreedy/lazy)
x{n,m} at least n x and at most m x (greedy)
x{n,} at least n x (greedy)
x{n} exactly n x
x{n,m}? at least n x and at most m x (ungreedy/lazy)
x{n,}? at least n x (ungreedy/lazy)
x{n}? exactly n x
Примеры
Замена всех совпадений в строке
Задача заменить строки, разделённые - или _ на CamelCase. При этом если строка начинается с заглавной буквы, то первое слово новой строки тоже с неё начинается:
"the-stealth-warrior" => "theStealthWarrior"
"The_Stealth_Warrior" => "TheStealthWarrior"
"The_Stealth-Warrior" => "TheStealthWarrior"
Решение:
use regex::Regex;
fn to_camel_case(text: &str) -> String {
let before = text;
let re = Regex::new(r"(-|_)(?P<neme>[A-z])").unwrap();
let after = re.replace_all(before,|captures: ®ex::Captures| {
captures[2].to_uppercase()
});
return after.to_string();
}
Strings
Статья по ссылкам на память в Rust
String
Тип данных с владельцем. Имеет изменяемый размер, неизвестный в момент компиляции. Представляет собой векторную структуру:
pub struct String {
vec: Vec<u8>;
}
Поскольку структура содержит Vec, это значит, что есть указатель на массив памяти, размер строки size структуры и ёмкость capacity (сколько можно добавить к строке перед дополнительным выделением памяти под строку).
&String
Ссылка на String. Не имеет владельца, размер фиксирован, известен в момент компиляции.
fn change(mystring: &mut String) {
if !mystring.ends_with("s") {
mystring.push_str("s"); // добавляем "s" в конец исходной строки
}
str
Набор символов (литералов), размещённых на стеке. Не имеет владельца, размер фиксирован, известен в момент компиляции. Можно превращать str в String через признак from:
let text = String::from("TEST"); // "TEST" :str
&str
Ссылка на часть, slice от String (куча), str (стек) или статической константы. Не имеет владельца, размер фиксирован, известен в момент компиляции.
- &String можно неявно превращать в &str;
- &str нельзя неявно превращать в &String.
fn main() {
let s = "hello_world";
let mut mut_string = String::from("hello");
success(&mutable_string);
fail(s);
}
fn success(data: &str) { // неявный перевод &String -> &str
println!("{}", data);
}
fn fail(data: &String) { // ОШИБКА - expected &String, but found &str
println!("{}", data);
}
Warning
Пока существует &str её в области жизни нельзя менять содержимое памяти, на которое она ссылается, даже владельцем строки.
Примеры применения
Строковые константы
const CONST_STRING: &'static str = "a constant string";
Изменение строк
При наличии String, нужно передать ссылку &mut String для изменения:
fn main() {
let mut mutable_string = String::from("hello ");
do_mutation(&mut mutable_string);
println!("{}", mutables_string); // hello world!
}
fn do_mutation(input: &mut String) {
input.push_str("world!");
}
Строки с владением
Получение String с передачей владения нужно при получении строки от функции, передача в поток (thread):
fn main() {
let s = "hello_world";
println!("{}", do_upper(s)); // HELLO_WORLD
}
fn do_upper(input: &str) -> String { // возврат String
input.to_ascii_uppercase()
}
Отображение части строки
Передавать владельца не нужно, передаём в &str:
let s = String::from("Hello World!");
let word = first_word(&s);
println!("The first word is: {}", word);
}
fn first_word(s: &String) -> &str { // передача строки по ссылке
let word_count = s.as_bytes();
for (i, &item) in word_count.iter().enumerate() {
if item == b' ' {
return &s[..i]; // возврат части строки как &str
}
}
&s[..] // обязательно указать возвращаемое значение, если условие в цикле выше ничего не вернёт (например, строка не содержит пробелов = вернуть всю строку)
Можно пройти по строке итератором chars() и его методами взятия N-го символа nth() спереди или nth_back() сзади:
let person_name = String::from("Alice");
println!("The last character of string is: {}", match person_name.chars().nth_back(0) { // ищем 1-ый символ с конца строки
Some(i) => i.to_string(), // если находим - превращаем в строку
None => "Nothing found!".to_string(), // не находим = сообщаем
});
Вывод строк
- Макрос println! позволяет вывести строку в поток stdout;
// println!("Hello there!\n");
// раскрывается в такой код:
use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();
- Макрос format! позволяет сформировать строку и вернуть из функции;
- Метод len() выдаёт длину строки;
- Метод is_empty() проверят, что строка непустая;
- Метод contains() ищет одну строку в другой строке;
- Метод replace(from,to) заменяет часть строки на другую и выдаёт результат;
- Метод splt_whitespace() позволяет делить строку на части по пробелам;
- Метод push_str() позволяет добавить текст к строке (строка должна быть mut).
fn main() {
let mut a = String::from("Wonderful RUST World");
println!("Hello{}!", output_string(&a)); // вывод строки
println!("String is empty? {}", a.is_empty());
println!("String length: {}", a.len());
println!("Does string contain 'Hello'? {}", a.contains("Hello"));
println!("{}",a.replace("RUST","Python")); // Wonderful Python World
for i in a.split_whitespace() {
println!("{}", i);
}
a.push_str(" And let's go!");
println!("{}",a);
}
fn output_string(t: &String) -> String {
format!(", {}",t) // возврат сформированной строки
}
Повтор части строки n раз
Новый подход использует std::repeat
fn main() {
let repeated = "Repeat".repeat(4);
println!("{}", repeated); // RepeatRepeatRepeatRepeat
}
Старый вариант через итератор - позволяет бесконечно отдавать любое значение (как generic):
use std::iter;
fn main() {
let repeated: String = iter::repeat("Repeat").take(4).collect();
println!("{}", repeated);
}
Структуры
Если структуре надо владеть своими данными - использовать String. Если нет, можно использовать &str, но нужно указать время жизни (lifetime), чтобы структура не пережила взятую ей строку:
struct Owned {
bla: String,
}
struct Borrowed<'a> {
bla: &'a str,
}
fn main() {
let o = Owned {
bla: String::from("bla"),
};
let b = create_something(&o.bla);
}
fn create_something(other_bla: &str) -> Borrowed {
let b = Borrowed { bla: other_bla };
b // при возврате Borrowed, переменная всё ещё в области действия!
}
Разделение строки на подстроки
Можно делить с помощью метода split(). В том числе можно делить по нескольким символам разом:
let text = String::from("the_stealth-warrior");
let parts = text2.split(['-', '_']);
for part in parts {
println!("{}", part);
Первая буква в строке
Чтобы проверить или изменить 1-ую букву в строке (в том числе иероглиф или иной вариант алфавита), нужно строку переделать в вектор из букв:
let char_vec: Vec<char> = text.chars().collect();
if char_vec[0].is_lowercase() { .. }
Гласные / согласные буквы
Проверку нужно написать в виде функции:
fn is_vowel(c: char) -> bool {
c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' }
let text = String::from("Aria");
Разворот слов
Дана строка с пробелами между словами. Необходимо развернуть слова в строке наоборот, при этом сохранить пробелы.
fn reverse_words_split(str: &str) -> String {
str.to_string()
.split(" ") // при разделении split() множественные пробелы сохраняются
.map(|x| x.chars().rev().collect::<String>()) // разворот слов+сбор в строку
.collect::<Vec<String>>(). // сбор всего в вектор
.join(" ") // превращение вектора в строку
}
fn main() {
let word: &str = "The quick brown fox jumps over the lazy dog.";
println!("{}",reverse_words_split(&word));
}
// ehT kciuq nworb xof spmuj revo eht yzal .god
Нахождение закономерностей в структурах со строками
В примере мы передаём вектор из строк. Далее, анализируем его по частям:
fn likes(names: &[&str]) -> String {
match names {
[] => "no one likes this".to_string(),
[a] => format!("{} likes this", a),
[a, b] => format!("{} and {} like this", a, b),
[a, b, c] => format!("{}, {} and {} like this", a, b, c),
[a, b, other @ ..] => format!("{}, {} and {} others like this", a, b, other.len()),
}
}
Удаление пробелов в строке String
Use split(' ')
, filter out empty entries then re-join by space:
s.trim()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ")
// Using itertools:
use itertools::Itertools;
s.trim().split(' ').filter(|s| !s.is_empty()).join(" ")
// Using split_whitespace, allocating a vector & string
pub fn trim_whitespace_v1(s: &str) -> String {
let words: Vec<_> = s.split_whitespace().collect();
words.join(" ")
}
Популярные строковые методы
Structures
Struct Data Type
Struct - комплексный изменяемый тип данных, размещается в куче (heap), содержит внутри себя разные типы данных. Он похож на кортеж (tuple), однако типы данных должны иметь явные именования.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64, // запятая в конце обязательна
}
Можно передать struct в функцию или вернуть из функции:
fn main() {
// создаём изменяяемый объект по структуре данных выше
let mut user1 = create_user(String::from("john@doe.com"), String::from("testuser"));
println!("User Email is {}", user1.email);
user1.email = String::from("Parker@doe.com");
println!("User Email is {}", user1.email);
}
fn create_user(email: String, username: String) -> User { // возврат из функции
User {
active: true,
username,
email,
// имена полей имеют с входными переменными, и можно не писать username: username, email: email.
sign_in_count: 1,
} // return заменяется отсутствием знака ";"" как обычно
}
Updating Structs
Если нужно создать новую структуру по подобию старой, и большая часть полей у них похожи, то можно использовать синтаксический сахар:
let user2 = User {
email: String::from("another@example.com"), // задать новое значение поля
..user1 // взять остальные атрибуты из user1. Идёт последней записью
};
Tuple structs
Структуры такого вида похожи на кортежи, но имеют имя структуры и тип. Нужны, когда нужно выделить кортеж отдельным типом, либо когда надо дать общее имя кортежу. При этом отдельные поля не имеют имён.
struct Color (i32, i32, i32);
struct Point (i32, i32, i32);
fn main() {
let red = Color(255,0,0);
let origin = Point(0, 0, 0);
Переменные red и origin разных типов. Функции, которые берут Color как параметр, не возьмут Point, несмотря на одинаковые типы внутри. Каждая структура = свой собственный тип. Разбор таких структур на элементы аналогичен кортежам.
let (x,y,z) = (origin.0,origin.1,origin.2);
Unit-like structs
Структуры без полей аналогичны кортежам без полей, только с именем.
struct TestTrait;
fn main() {
test = TestTrait;
}
Такие структуры нужны для задания признаков (traits), когда в самой структуре данные хранить не нужно.
Структурные признаки
Можно выводить информацию о содержимом полей структуры для анализа кода. Для этого нужно добавить над структурой пометку debug:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect = Rectangle {
width: dbg!(20 * scale), // вывод поля структуры. dbg! возвращает назад
height: 10, // взятое значение, с ним далее можно работать
};
println!("Rectangle content: {:?}",rect); // вывод содержимого структуры
dbg!(&rect); // ещё вариант вывода - в поток stderr. Функция dbg!
// забирает владение структурой, поэтому передача по ссылке
}
Структурные методы
Можно добавлять функции как методы, привязанные к структурам. Это позволяет организовать код более чётко - по объектам и действиям над ними.
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle { // impl определяет блок методов структуры Rectangle
fn area(&self, scale) -> u32 { // 1-ый параметр всегда self = структура
self.width * self.height * scale // тело функции и возврат значения
} }
fn main() {
let rect = Rectangle {
width: 20,
height: 10,
};
println!("Rectangle area is {}", rect.area(2)); // вызов метода
}
Как и везде, для внесения изменений в объект структуры, в блоке методов можно объявить &mut self
, а для перемещения владения - просто self
. Это нужно изредка при превращении self в другой вид объекта, с целью запретить вызов предыдущей версии объекта. Блоков impl
может быть несколько.
Асоциированные функции
В блоке методов impl
можно добавлять функции, которые первым параметром не берут саму структуру self
. Такие функции не являются методами и часто служат для создания новых версий объекта.
fn square(side: u32) -> Self { // Self - алиас для типа данных Rectangle
Self {
width: side,
height: side,
} } }
fn main() {
let sq = Rectangle::square(10); // вызов асоциированной функции через ::
println!("Square created from {:?}",sq);
}
Создание типа данных с проверками
Вместо проверять введённые данные на корректность внутри функций, можно объявить собственный тип данных, содержащий в себе все необходимые проверки. Например, объявим число от 1 до 100 для игры, где надо угадать число:
pub struct Guess { // объявили тип данных (публичный)
value: i32, // внутри число (приватное)
}
impl Guess {
pub fn new(value: i32) -> Guess { // метод new проверяет значение
if value < 1 || value > 100 { // на заданные границы 1-100
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess { value } // возврат нового типа данных
}
pub fn value(&self) -> i32 { // метод getter для получения значения value
self.value // он нужен, тк напрямую видеть value нельзя
} // Это приватная переменная в структуре.
}
Traits
Инициализация типажа
Типаж нужен для организации интерфейса: он задаёт ограничения-особенности поведения для переменных или структур с неопределёнными (generic) переменными. Мы отделяем объявление типажа от его реализации. При объявлении типажа можно оставить обязательную реализацию на потом, либо вписать реализацию функций в типаже по-умолчанию:
// типаж задаёт метод и ограничения по входным/выходным типам
trait LandVehicle {
fn LandDrive(&self) -> String; }
// типаж задаёт методы плюс их реализация по умолчанию
trait WaterVehicle {
fn WaterDrive(&self) { println!("Default float"); }
}
Применение типажей к структурам данных
Во время применения, если реализация по умолчанию была задана, то можно её переделать под конкретную структуру, либо использовать эту реализацию:
struct Sedan {}
struct Rocketship {}
// типаж LandVehicle не имеет реализации по умолчанию, реализуем тут
impl LandVehicle for Sedan {
fn LandDrive(&self) -> String { format!("Car zoom-zoom!") } }
// типаж WaterVehicle имеет выше реализацию по умолчанию, используем её
impl WaterVehicle for Rocketship {}
Объединение типажей
При объединении, создаётся ярлык (alias). При этом каждый входящий в него типаж нужно отдельно применить к структуре данных. При этом можно также использовать реализацию определённых в типаже методов по умолчанию, либо написать свою.
// создание ярлыка
trait AmphibiousVehicle: LandVehicle + WaterVehicle {}
// применение типажей к структуре
impl AmphibiousVehicle for Carrier {}
impl LandVehicle for Carrier {
fn LandDrive(&self) -> String { format!("Use air thrust to travel on land") }
}
impl WaterVehicle for Carrier {}
Вызов методов экземпляра структуры определённого типажа
fn main() {
let toyota_camry = Sedan {};
println!("{}",toyota_camry.LandDrive());
let rs = Rocketship {};
rs.WaterDrive();
let project_x = Carrier {};
println!("{}",project_x.LandDrive());
project_x.WaterDrive();
}
Variables and constants
Note
When in doubt on variable type: just use i32
for everything! i32
is the default in Rust, and the fastest, even on x64 architecture.
Scalar Types
Unsigned |
Signed |
u8 |
i8 |
u16 |
i16 |
u32 |
i32 |
u64 |
i64 |
u128 |
i128 |
usize |
isize |
Floating point vars: f32, f64. |
|
You can declare number variables with _
sign in any place for convenience:
let x: i32 = 1_000_000;
let (mut missiles: i32, ready: u32) = (8, 5); // 2 vars tuple assign in 1 line
You can also add the var type to the number (convenient with _
when using generics):
let x = 1000_u32;
let y = 3.14_f32;
Converting
String2Int:
let n_str = "123456789".to_string();
let n_int = n_str.parse::<i32>().unwrap();
Char2Int:
let letter = 'a';
println!("{}", letter as u32 - 96); // = 97-96 = 1
let i = 97u8; // только с u8 разрешено делать 'as char'
println!("Value: {}", i as char);
Boolean type
bool type can be true
or false
. Non-integer - do NOT try to use arithmetic on these. But you can cast them:
Mutablitity
By default, variables in Rust are immutable. To make a variable mutable, special “mut” identifier must be placed. Rust compiler may infer the variable type from the type of value.
let x = 5; // immutable variable, type i32 guessed by Rust as default for numbers.
let mut x = 5; // mutable variable
Shadowing
Variable names can be reused. This is not mutability, because shadowing always re-creates the variable from scratch. Previous variable value may be used:
let x = 5;
let x = x + 1; // new variable created with value = 6
Constants
Constant values are always immutable and available in the scope they were created in throughout the whole program. Type of constant must always be defined. Constants may contain results of operations. They are evaluated by Rust compiler. List of evaluations: https://doc.rust-lang.org/stable/reference/const_eval.html
const ONE_DAY_IN_SECONDS: u32 = 24 * 60 * 60; // type u32 MUST be defined
let phrase = "Hello World";
println!("Before: {phrase}"); // Before: Hello World
let phrase = phrase.len();
println!("After: {phrase}"); // After: 11
Compound variables
Tuple
Compound type, immutable, consists of different types.
let tup: (u32, f32, i32) = (10, 1.2, -32);
let (x,y,z) = tup; // tuple deconstructing into variables
let a1 = tup.0;
let a2 = tup.1; // another way to deconstruct values
Deconstructing tuples is very useful when a function returns a tuple:
let (left, right) = slice.split_at(middle);
let (_, right) = slice.split_at(middle); // use '_' to throw away part of return
Array
Compound type, mutable, all values of the same type.
let ar: [i32;5] = [1,2,3,4,5];
// array data is allocated on the stack rather than the heap
// [i32;5] - type of values and number of elements
let first = ar[0];
let second = ar[1]; // accessing array elements
Подсчёт одинаковых элементов в массиве с помощью itertools…
Vectors
Vectors
Вектор - множество данных одного типа, количество которых можно изменять: добавлять и удалять элементы. Нужен, когда:
- требуется собрать набор элементов для обработки в других местах;
- нужно выставить элементы в определённом порядке, с добавлением новых элементов в конец;
- нужно реализовать стэк;
- нужен массив изменяемой величины и расположенный в куче.
Методы
// Задание пустого вектора:
// let mut a test_vector: Vec<i32> = Vec::new();
// Задание вектора со значениями через макрос:
let mut test_vector = vec![1, 2, 3, 4];
test_vector.push(42); // добавить число 42 в конец mut вектора
test_vector.remove(0); // удалить первый элемент =1
for i in &mut test_vector { // пройти вектор как итератор для вывода
*i += 1; // изменять значения при их получении требует делать '*' dereference
println!("{i}"); }
println!("Vector length: {}", test_vector.len()); // количество элементов
Получение элемента вектора
Элемент можно получить с помощью индекса, либо с помощью метода get
:
let mut test_vector = vec![1,2,3,4,5];
println!("Third element of vector is: {}", &test_vector[2]); // индекс
let third: Option<&i32> = test_vector.get(2); // метод get
match third {
Some(third) => println!("Third element of vector is: {}", third),
None => println!("There is no third element")
}
Разница в способах в реакции на попытку взять несуществующий элемент за пределами вектора. Взятие через индекс приведёт к панике и остановке программы. Взятие с помощью get
сопровождается проверкой и обработкой ошибки.
Хранение элементов разных типов в векторе
Rust нужно заранее знать при компиляции, сколько нужно выделять памяти под каждый элемент. Если известны заранее все типы для хранения, то можно использовать промежуточный enum:
#[derive(Debug)]
enum SpreadSheet {
Int(i32),
Float(f64),
Text(String)
}
fn main() {
let row = vec![
SpreadSheet::Int(42),
SpreadSheet::Float(3.14),
SpreadSheet::Text(String::from("red"))
];
for i in row {
println!("{:?}",i);
} }
Vector со строками String
let mut v: Vec<String> = Vec::new();
Пустой вектор с нулевыми строками можно создать через Default размером до 32 элементов (Rust 1.47):
let v: [String; 32] = Default::default();
Вектор большего размера можно создать через контейнер Vec:
let mut v: Vec<String> = vec![String::new(); 100];
Вектор с заданными строками можно инициализировать либо с помощью метода to_string(), либо через определение макроса:
macro_rules! vec_of_strings {
($($x:expr),*) => (vec![$($x.to_string()),*]);
}
fn main()
{
let a = vec_of_strings!["a", "b", "c"];
let b = vec!["a".to_string(), "b".to_string(), "c".to_string()];
assert!(a==b); // True
}
Соединение вектора со строками в строку (Join):
result_vec.join(" "); // указывается разделитель для соединения
// в старых версиях Rust <1.3 применяют метод .connect();
Сортировка
let number_vector = vec!(1,12,3,1,5);
number_vector.sort(); // 1,1,3,5,12
Способы реверс-сортировки
Смена элементов при сравнении:
number_vector.sort_by(|a,b| b.cmp(a));
Сортировка, потом реверс:
number_vector.sort();
number_vector.reverse();
Обёртка Reverse с экземпляром Ord:
use std::cmp::Reverse;
number_vector.sort_by_key(|w| Reverse(*w));
Если вернуть Reverse со ссылкой и без *
, это приведёт к проблеме с временем жизни.
Получение вектора из итератора
let collected_iterator: Vec<i32> = (0..10).collect();
println!("Collected (0..10) into: {:?}", collected_iterator);
// Collected (0..10) into: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Конвертация
Конвертация из массива array в vector:
let number_list = [1,12,3,1,5,2];
let number_vector = number_list.to_vec(); // перевод array[i32] -> vector<i32>
Вариант через итератор:
let a = [10, 20, 30, 40];
let v: Vec<i32> = a.iter().map(|&e| e as i32).collect();
Вектор из байтов vector of bytes в строку String:
use std::str;
fn main() {
let buf = &[0x41u8, 0x41u8, 0x42u8]; // vector of bytes
let s = match str::from_utf8(buf) {
Ok(v) => v,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};
println!("result: {}", s);
}