Добрый день!
Тут Вы найдёте заметки о технологиях, с которыми я имею дело.
Возможно, они пригодятся и Вам!
Обновления
05.04.2026
Миграция сайта на новый шаблон дизайна, с новыми возможностями. В частности, появился переключатель цветовой темы Light/Dark. В процессе я освоил первый свой характерный для Rust паттерн newtype работы с типами переменных. Все эти типы данных должны давать какие-то бонусы!
03.04.2026
Эти пару дней у меня небольшой заряд сахара в синтаксисе: дополнил статью управления потоками темой if let, полезным сокращением кода.
Помимо этого, тема ИИ не могла обойти стороной мои заметки:
удобный промт для суммаризации новостей в Телеге, с количество которых можно скоро повеситься;
заметки по настроке и “обвесу” Claude Code с прицелом на сокращение потребления токенов при сохранении точности;
02.04.2026
Вчера я ловил ошибки в коде. Оказалось, универсальный молоток anyhow даёт осечки - не всегда тип ошибки подпадает под то, что он ожидает. И тогда нужно преобразовывать с помощью функции map_err, материалом о которой я дополнил свой конспект.
31.03.2026
Моё изучение дошло до концепции Type-Driven Development (TDD, не путать с Test-Driven). Добавил статьи со ссылками
25.03.2026
Один из вариантов оптимизации сборки - это собирать не весь код, а лишь часть, в зависимости от условий, флагов. Добавил в статью о сборке тему условная компиляция.
24.03.2026
По ходу курса обучения у меня возникла домашка: проект контроллера умного дома, а также стимул сделать пример этот максимально “Ынтерпрайзно”. Начал я с шаблона проекта: негоже с чистого листа серьёзные проекты делать! Сначала сделать шаблон с заложенным логгированием, трассировками, модулями, а потом его импортировать. Для этого я освоил подходящий инструмент cargo generate
Далее по toolchain: cargo wizard для оптимизации сборки, cargo cache для оптимизации/очистки кеша сборки.
06.03.2026
Некоторое время назад я читал про магический “Mold, который ускоряет сборку кода Rust”. Чем продолжать жить со слухами, я исследовал тему сборки с Cargo и её ускорения. Вместо магии теперь у меня дельный набор советов по диагностике и оптимизации!
03.03.2026
Аналогичная история при работе с файлами. С помощью AsRef можно передавать имя файла как объект PathBuf, либо как просто строку.
02.03.2026
Возврат к строкам в Rust с точки зрения передачи параметров в API: нужно обеспечить возможность передавать разные виды строк, не ограничивать. Для этого хорошо подходит trait AsRef для конверсии на лету вида ссылка-ссылка. Я добавил найденный пример в конце статьи.
29.01.2026
Эти несколько дней настал черёд пересмотреть работу с файлами. Интересное отличие от Python и т.д., что режим записи/дополнения файла включаются через отдельный модуль, а не в составе одной функции file::open().
12.01.2026
Ещё одна итерация по статье про ошибки и работу с ними в Rust. Продолжаю повторные итерации по изученным ранее темам, добавляю подробности.
08.01.2026
Первое обновление в новом году! Ещё один заход в строки на Rust: методы разделения и конкатенации строк, взятия срезов и форматирования теперь я разобрал весьма подробно, с учётом типа владения String и &str.
28.12.2025
Когда авторы статей про Rust хотят напугать новичков “ядрёностью” языка, то обычно пишут “там есть lifetimes! Лайфтаймы - это же-е-есть!”. На деле явное употребление lifetimes нужно, по ощущениям бывалых, в 5% случаях особо критичного к скорости кода (системы реального времении, embedded-устройства и т.д.). В остальное время можно этой темы не касаться. Однако, иметь понимание стоит, потому я аккуратно подкрадываюсь к меткам времени жизни в новой статье.
25.12.2025
Сегодня я навёл порядок в инструментах, окружающих Rust. Информацию о них я собрал в отдельный раздел. Плюс я добавил отдельную статью про bacon по мере того как распробовал удобство его использования для отслеживания и прогона сразу и сборки, и тестов - всё с автозапуском по триггеру.
23.12.2025
Первый случай, когда обновление портала УМЕНЬШИЛО, а не увеличило количество статей. Весь раздел касательно Kubernetes и его безопасности уехал в новый самостоятельный портал Kuberbez.ru. Другие статьи по контейнерам, по мере доработки, последуют за ним. Такая вот специализация!
18.12.2025
Пару дней я заносил изменения в тему работы со строками, вывод строк, в том числе, вывод с форматированием чисел.
16.12.2025
Очень много мелких изменений залетело в портал за эти несколько дней. Уточнения-примеры. Подытожу их я появлением статьи про множества уникальных элементов HashSet и BTreeSet.
12.12.2025
Новый подход к строкам в Rust. Тема настолько большая, что уже переросла из статьи в подраздел, и количество интересных конструкций и методов в моей копилке всё растёт.
10.12.2025
В статью про словари приехали B-деревья с упорядоченными ключами.
Также я немного исследовал запуск Python-скриптов с помощью Rust. Самый простой и надёжный способ - передавать скрипту Python shell-команду на запуск. Варианты через спец-библиотеки типа pyo3 я пока решил не трогать, так как при компиляции с ними на простых примерах я получил сразу кучу ошибок.
И у меня второй подход к теме замера скорости кода: можно строить графики отображения суммарного времени работы функций, flamegraph. Его установкой и настройкой я дополнил соответствующую статью.
09.12.2025
Сегодня мне попалась задачка с подсчётом битов в двоичном представлении чисел - с этим я в Rust ещё не сталкивался, будет новая статья по теме.
05.12.2025
В описании к Rust всегда пишут, что это быстрый язык. Но насколько? Для выяснения я сделал первый подход к теме замера скорости кода и добавил статью
04.12.2025
Ещё немного про итераторы: команда итерирования по коллекции бывает 3-х видов => iter(), into_iter(), iter_mut(), сравнение их между собой я также добавил в свою копилочку.
27.11.2025
Прошло уже несколько лет как я набегами пробую свои силы в Rust, “переезд” с Python. Процесс непростой, мозги кипят постоянно, но и тут я подмечаю ходовые структуры данных и инструменты. Среди них в Rust постоянно применяются замыкания и итераторы - в копилку-статью про последние я занёс ещё немного интересного про повторяющиеся символы (repeat, repeat_n).
26.11.2025
В Rust есть модная концепция “перепеши на Rust”, когда народ берёт обыденные инструменты и переписывает. Формально с целью сделать их безопаснее, быстрее, но на самом деле, мне кажется, для практики с языком. Надо же сделать что-то интересное-полезное, а всё кругом уже сделали, новое придумывать лень - так что объявим эти готовые инструменты ненадёжными и перепишем! Один из таких инструментов - эмулятор терминала. Шикарный проект - WezTerm, быстрый, функциональный и, в отличие от iTerm, кроссплатформенный. А в пару к нему ещё один крутой проект - Nushell, кроссплатформенный интерпретатор и полноценный скриптовый язык взамен bash.
22.11.2025
Ещё один подход к замыканиям - теперь к конкретному замыканию fold(): с его помощью можно заменить сразу много разных функций: sum(), product(), filter(), map() и так далее, либо скорее они его заменяют. Но самое главное - возможность использовать этот мощный инструмент для сложных сценариев.
21.11.2025
Одна из непростых тем в Rust - работа замыканий. Второй подход к снаряду - я переделал статью про Closures. Это всё ещё лишь верхушка айсберга, так как я решил разобраться в практике применения, без углубления в детали реализации.
03.10.2025
Сегодня у меня разбор давно намеченных тем из области “как грамотно начать большой проект?” в Rust:
28.01.2025
Для разминки я решил быстро написать игру “угадай число” из 2-ой главы книги Rust Book, и вдруг оказалось, что библиотеку rand() переделали в новой её версии. Я внёс новые имена вызовов в статью.
14.01.2025
Я в несколько заходов пару дней возился, чтоб настроить возможность коммитить с 1 системы сразу в github и gitlab с помощью двух разных ssh-сертов. Оказалось, это не самое тривиальное дело, потому как у git нет ключа-указателя на нужный сертификат и имя пользователя. По итогам у меня получилась важная добавка к статье про git
09.01.2025
Новый год я начинаю постепенно, с замены инструмента отслеживания изменений в коде и запуска перекомпиляции. Ранее с задачей справлялся cargo watch, но автор проекта написал, что более не будет развивать его. Поэтому я опробовал и переехал на bacon, и добавил заметку о его использовании.
git config --global user.name "spider_net"# username init, seen in commitsgit config --global user.email "test@gmail.com"# Init of user e-mail
SSH одновременно с GitHub и Gitlab
Для работы с одной системы с тем и другим репозиторием по SSH нужно создать ключи SSH под оба, а также прописать в файле конфигурации профили подключения:
git init # local repo initgit status # check statusgit branch -m master main # change master branch to main, for GitLabgit add * # add all files to repo, .gitignore file lets you hide junk files, password files from syncinggit commit -m "Hello world"git config --global user.email "Ivan@example.com"git config --global user.name "Ivan the Terrible"git log
git remote add origin git@gitlab.com:<name of account>/<project name>.git
git push origin main
Cloning Remote Repo on Amazon
Certificate file needed in .ssh folder. Also a config file .ssh/config is needed:
Host git-codecommit.us-east-1.amazonaws.comUser Your-SSH-Key-ID, such as APKAEIBAERJR2EXAMPLEIdentityFile Your-Private-Key-File, such as ~/.ssh/codecommit_rsa or ~/.ssh/id_rsa
Create new branch: git branch new-branch
Pointer does not automatically switch to new branch, it is left on main branch, so you need to switch manually: git checkout new-branch
Merging Branches
Switch to branch for merging
Merge
Check pointers point to one place OR merge the other branch with the current one
git commit
git checkout main
git commit
git merge new-branch
Change of branches to make it look as if the functionality was developed step-by-step, not in parallel.
git rebase master
git checkout master
git rebase bugFix
Pointer
HEAD is the pointer to current project state. By default HEAD points to current branch.
You can go back by commits or by direct commit hash:
git checkout C1
You can use syntax “^K” for going up 1 level (where K is the route number to choose if there are more than 1 parent. By default, K=1) and “~N” for N steps up:
git checkout master^
# ORgit checkout bugFix^^
# ORgit checkout bugFix~2
# same route on schema from picture# or build a sequencegit checkout bugFix^~1
Chaning main branch to several commits backwards:
git branch -f master HEAD~3
# -f means force - direct remap of branch for commit
Undo Actions
For 1-person local repo do reset.
For multi-person remote repo, do revert.
git reset HEAD~1
# ORgit revert HEAD
In c2’ state are the changes, which cancel state C2
Moving commits
Chosen commits are moved upon current commit using cherry-pick command. Used when it is known what specific commits to move: git cherry-pick C2 C4git rebase -i HEAD~4 --aboveAll
Small commit modifications
You can modify a commit, dividing it into two with same parent:
git commit --amend
Tagging commits
Marking milestones is done with tags. They block the commit from changes, moving etc.:
git tag v1 <hash> # if hash is not provided, then current commit is tagged
Tags serve as anchors in tree of commits. To define your position against the nearest anchor, use command:
git describe <ref> # if <ref> is not provided, current commit will be described
Naming
Remote branches have a naming convention: <remote name>/<branch name>
Main remote i called origin. So master branch is origin/main. When doing commit in local branch, the system is put into detached HEAD mode:
git checkout origin/master
git commit
Fetch data from remote repository
When data is downloaded from remote repo, the origin/main branch is updated to reflect the new commits:
git fetch
Only commits non-present locally are downloaded. No local state of files is changed after download. The local main status is unchanged. To change, a merge must be done:
git fetch + git merge = git pullgit pull
Publishing local repository to remote
Publishing syncs commits at remote repo and local repo (main and origin/main point to the same commit):
git push
If the remote repo has changed by someone by the time you need to push there, it means that your feature is based on an old commit. So Git will not let you push. Before push, a fetch must be made to sync the changes, than a rebase or merge to update the main branch, and then a push:
You can change that another branch will be main for the remote repo:
`git checkout -b side origin/main
Push arguments
You can specify what branch to push:
git push origin main
origin = remote repo,
main = branch to take commit from.
It does not matter where the HEAD is at this moment. You can specify where to push commits using git push origin <source>:<destination> notation:
git push origin side^2:master
If push is made to a non-existent branch on remote repo, Git will create this branch:
git push origin main:newBranch
Fetch arguments
Same as push, but the other wat around. Go to foo branch on remote repo, download commits from there to local branch origin/foo:
git fetch origin foo
# Go to any Terminal emulatorcd ~/.config/
mkdir wezterm
cd wezterm
which nu # check nushell location for configvim wezterm.lua
Вставить конфиг (путь до nushell проверить и заменить):
-- Load the wezterm module
localwezterm= require 'wezterm'-- Initialize configuration object
localconfig= wezterm.config_builder and wezterm.config_builder() or {}-- Use WebGPU for better performance
config.front_end ="WebGpu"config.webgpu_power_preference ="HighPerformance"-- Set Nushell as the default shell
-- This path is standard for Mac with Apple Silicon
-- To find the path on your system, run `which nu` in Terminal.app
config.default_prog ={'/usr/local/bin/nu'}-- Get the home directory (works on both macOS and Windows)localhome= os.getenv("HOME") or os.getenv("USERPROFILE") or ""-- Set XDG_CONFIG_HOME environment variable for Nushell
config.set_environment_variables ={XDG_CONFIG_HOME= home .. "/.config",
}localquick_select_patterns={ -- Nushell error paths (like ╭─[/path/to/file.nu:1946:63])"─\\[(.*\\:\\d+\\:\\d+)\\]",
-- Table patterns
-- $env.config.table.mode ="default" -- $env.config.table.header_on_separator =true -- $env.config.footer_mode ="always""(?<=─|╭|┬)([a-zA-Z0-9 _%.-]+?)(?=─|╮|┬)", -- Headers
"(?<=│ )([a-zA-Z0-9 _.-]+?)(?= │)", -- Column values
-- File paths (stops at ~, allows dots in path but stops before dot+space)"/[^/\\s│~]+(?:/[^/\\s│~]+)*(?:\\.(?!\\s)[a-zA-Z0-9]+)?",
}config.quick_select_patterns = quick_select_patterns
return config
Переменные среды
Настроим переменную среды для VSCode - (для macOS):
# Для ZSH:cat << EOF >> ~/.zprofile
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
EOF
# Для Bash:cat << EOF >> ~/.bash_profile
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
EOF
Переменные для nushell - запустить config nu команду и добавить кониг с путями. Как вариант, запустив nu, можно узнать локацию файла с помощью команды $nu.config-path.
$env.PATH =($env.PATH
| split row (char esep)| append ["/opt/homebrew/bin""/usr/local/bin""/usr/bin""/bin""/Applications/Visual Studio Code.app/Contents/Resources/app/bin"# ... последняя ссылка на VSCode, + можно добавить ещё разные пути]| str trim
| where {|i|$i| path exists }| uniq
)$env.config.history.file_format ="Sqlite"$env.config.history.max_size = 5_000_000
$env.config.show_banner =false# ALT+SHIFT+R to see all history commands$env.config.menus ++=[{# List all unique successful commands name: working_dirs_cd_menu
only_buffer_difference: true marker: "? " type: { layout: list
page_size: 23} style: { text: green
selected_text: green_reverse
} source: {|buffer, position| open $nu.history-path
| query db "SELECT DISTINCT(cwd) FROM history ORDER BY id DESC"| get CWD
| into string
| where $it=~ $buffer| compact --empty
| each {if($in has ' '){$'"($in)"'}else{}|{value: $in}}}}]$env.config.keybindings ++=[{ name: "working_dirs_cd_menu" modifier: alt_shift
keycode: char_r
mode: emacs
event: { send: menu name: working_dirs_cd_menu}}]# переключение между папками по частям их названий в стиле zsh$env.config.completions.algorithm ="Fuzzy"# FZF дополнение по CTRL+T$env.config.keybindings ++=[{ name: fzf_files
modifier: control
keycode: char_t
mode: [emacs, vi_normal, vi_insert] event: [{ send: executehostcommand
cmd: "
let fzf_ctrl_t_command = \"fd --type=file | fzf --preview 'bat --color=always --style=full --line-range=:500 {}'\";
let result = nu -c $fzf_ctrl_t_command;
commandline edit --append $result;
commandline set-cursor --end
"}]}]# для Quick Selection с CTRL+SHIFT+SPACE:$env.config.table.header_on_separator =true$env.config.footer_mode ="always"
Проба Quick Selection
Вывести список файлов в терминале nu с ls
Видим табличный вывод
Нажимаем Ctrl+Shift+Space для активации quick select mode. WezTerm подсветит:
Column headers (name, size, modified)
Individual cell values in each column
Any file paths
У каждого поля будет буква или две с цифрой, нажание соответствующих букв скопирует поле в буфер обмена.
Настроить среду
Настроить выход из Терминала сразу:
Повысить скорость печати повторяющихся символов в macOS (требует перезагрузки):
Можно поймать панику во всём тесте с помощью ключа #[should_panic]:
#[test]#[should_panic(expected = "assertion failed")]fntest_example(){letroom_nums=[1,2,3,4];letmax_length=room_nums.len();letindex_out_of_bounds=room_nums.get(max_length+1);assert!(index_out_of_bounds.is_some());// паника будет тут
}
❗Если паники по факту не будет, тест провалится.
Можно перехватить панику в конкретной команде с помощью std::panic::catch_unwind():
В библиотеках требуется получать на выходе конкретный тип ошибки, поэтому там применяется thiserror.
Установка
cargo add anyhow
Использование
Anyhow создаёт алиас наподобие Result<T> = Result<T, Box<dyn Error>>, чтобы скрыть тип ошибок и сделать его универсальным.
// ---- Без Anyhow
fnstring_error()-> Result<(),String>{Ok(())}fnio_error()-> Result<(),std::io::Error>{Ok(())}fnany_error()-> Result<(),Box<dynError>>{string_error()?;io_error()?;Ok(())}// ---- С Anyhow:
useanyhow::Result;fnstring_error()-> Result<()>{Ok(())}fnio_error()-> Result<()>{Ok(())}fnany_error()-> Result<()>{string_error()?;io_error()?;Ok(())}
Пример неудачного чтения файла:
useanyhow::{Context,Result};fnread_config_file(path: &str)-> Result<String>{std::fs::read_to_string(path).with_context(||format!("Failed to read file {}",path))}fnmain()-> Result<()>{letconfig_content=read_config_file("conf.txt")?;println!("Config content:\n{:?}",config_content);Ok(())}
Result<T> становится алиасом к Result<T, anyhow::Error>;
Context, with_context() позволяет добавить подробности к ошибке, в случае неуспеха функции чтения read_to_string();
Оператор ? выносит ошибку вверх, при этом авто-конвертирует её тип в anyhow::Error.
Замена Box<dyn> с контекстом
Возьмём пример, в котором чтение файла std::fs::read_to_string() (может быть неудачным), далее дешифровка его контента с помощью base64 decode() (может не получиться) в цепочку байт, из которой формируется строка String::from_utf8() (может не получиться). Все эти три потенциальных ошибки имеют разный тип.
Один способ все три их принять на одну функцию, это с помощью Box<dyn std::error::Error>>, потому что все 3 ошибки применяют std::error::Error.
Подход рабочий, но при срабатывании одной из трёх ошибок, судить о происхождении проблемы можно будет лишь по сообщению внутри ошибки.
В случае применения Anyhow, можно заменить им Box<dyn>, при этом сразу добавить контекстные сообщения, которое поможет понять место:
useanyhow::Context;usebase64::{self,engine,Engine};fndecode()-> Result<(),anyhow::Error>{letinput=std::fs::read_to_string("input").context("Failed to read file")?;// контекст 1
forlineininput.lines(){letbytes=engine::general_purpose::STANDARD.decode(line).context("Failed to decode the input")?;// контекст 2
println!("{}",String::from_utf8(bytes).context("Failed to cenvert bytes")?// контекст 3
);}Ok(())
Замена map_err()
map_err() - это универсальный метод из стандартной библиотеки, который работает с любым Result. context() - это метод из anyhow, специально созданный для удобного добавления контекста к ошибкам.
// map_err — нужно явно создавать anyhow::Error
.map_err(|e|anyhow::anyhow!("ошибка: {}",e))// context — просто добавляет пояснение
.context("ошибка")?
context() принимает замыкание (closure), которое выполняется только при ошибке. Это важно для ресурсоемких операций:
// context с замыканием — форматирование только при ошибке
letcontent=std::fs::read_to_string(path).with_context(||format!("не удалось прочитать файл {}",path))?;// map_err — форматирование всегда, даже при успехе
letcontent=std::fs::read_to_string(path).map_err(|e|anyhow::anyhow!("не удалось прочитать файл {}",path))?;
with_context() - вариант context() с ленивым вычислением, идеален для дорогих операций вроде форматирования строк.
При запуске данная программа требует 2 аргумента, притом второй обязательно числом.
Добавление описаний
Имя программы и версия вносятся отдельным признаком. Доп. поля описания вносятся с помощью спец. комментариев ///:
useclap::Parser;#[derive(Parser, Debug)]#[command(author = "Author Name", version, about)]/// A very simple CLI parser
structArgs{/// Text argument option
arg1: String,/// Number argument option
arg2: usize,}fnmain(){letargs=Args::parse();println!("{:?}",args)}
Добавка флагов
Флаги добавляем с помощью аннотации #[arg(short, long)] для короткого и длинного именования флага. Если у 2-х флагов одинаковая первая буква, можно указать вручную их короткую версию. Короткая версия не может быть String, можно только 1 char.
<..>structArgs{#[arg(short = 'a', long)]/// Text argument option
arg1: String,#[arg(short = 'A', long)]/// Number argument option
arg2: usize,}<..>
Необязательные флаги
Для отметки аргумента как необязательного достаточно указать его тип как Option<тип> и в скобках исходный тип данных:
structArgs{#[arg(short = 'a', long)]/// Text argument option
arg1: String,#[arg(short = 'A', long)]/// Number argument option
arg2: Option<usize>,}
Такой подход потребует обработать ситуацию, когда в arg2 ничего нет. Вместо так делать, можно указать значение по умолчанию:
structArgs{#[arg(short = 'a', long)]/// Text argument option
arg1: String,#[arg(default_value_t=usize::MAX, short = 'A', long)]/// Number argument option
arg2: usize,}
Теперь arg2 по умолчанию будет равен максимальному числу usize, если не указано иное.
Валидация введённых значений
В случае аргумента-строки есть возможность ввести пустую строку из пробелов " ". Для исключения таких вариантов, вводится функция валидации и её вызов:
useclap::Parser;#[derive(Parser, Debug)]#[command(author = "Author Name", version, about)]/// A very simple CLI parser
structArgs{#[arg(value_parser = validate_argument_name, short = 'a', long)]/// Text argument option
arg1: String,#[arg(default_value_t=usize::MAX, short = 'A', long)]/// Number argument option
arg2: usize,}fnvalidate_argument_name(name: &str)-> Result<String,String>{ifname.trim().len()!=name.len(){Err(String::from("строка не должна начинаться или заканчиваться пробелами",))}else{Ok(name.to_string())}}fnmain(){letargs=Args::parse();println!("{:?}",args)}
Теперь при попытке вызвать программу tiny-clapper -- -a " " будет показана ошибка валидации.
❗Ограничение - можно вызывать только существующие объекты, нельзя добавлять свой текст.
spawn - гибкий ввод
Самый гибкий вариант, позволяющий делать свой ввод, а не только существующие команды и файлы-папки, это через spawn:
letmutchild=Command::new("cat")// команда
.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?;letstdin=child.stdin.as_mut()?;stdin.write_all(b"Hello Rust!\n")?;// текст к команде, /n обязателен
letoutput=child.wait_with_output()?;foriinoutput.stdout.iter(){// цикл на случай многострочного вывода
print!("{}",*iaschar);}Ok(())
❗Ограничение - можно подавать на вход текст лишь тем командам, которые требуют сразу указать вводный текст. При этом ряд команд делают паузу перед потреблением текста на вход, с такими свой ввод работать не будет это относится и к фильтрации через pipe = | grep <...> и аналоги.
Pipe (nightly) - полный ввод (не проверенный способ)
#![feature(anonymous_pipe)]// только в Rust Nightly
usestd::pipelettext="| grep file".as_bytes();// Запускаем саму команду
letchild=Command::new("ls").arg("/Users/test").stdin({// Нельзя отправить просто строку в команду
// Нужно создать файловый дескриптор (как в обычном stdin "pipe")
// Поэтому создаём пару pipes тут
let(reader,mutwriter)=std::pipe::pipe().unwrap();// Пишем строку в одну pipe
writer.write_all(text).unwrap();// далее превращаем вторую для передачи в команду сразу при spawn.
Stdio::from(reader)}).spawn()?;
Sorting() a string of letters (with rev() - reverse order)
useitertools::Itertools;lettext="Hello world";lettext_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
useitertools::Itertools;letnumber_list=[1,12,3,1,5,2,7,8,7,8,2,3,12,7,7];letmode=number_list.iter().counts();// Itertools::counts()
// возвращает HashmapHashMap<char, usize>,
// где ключи взяты из массива, значения - частота
for(key,value)in&mode{println!("Число {key} встречается {value} раз");}
По сути counts() создаёт HashMap, заменяя собой конструкцию или конструкцию на базе fold():
userand::Rng;fnmain(){letsecret_of_type=rand::rng().random::<u32>();letsecret=rand::rng().random_range(1..=100);println!("Random nuber of type u32: {secret_of_type}");println!("Random nuber from 1 to 100: {}",secret);}
В старой версии библиотеки применялся признак gen(), который переименовали в связи с добавлением gen() в Rust 2024.
. any character except new line (includes new line with s flag)
\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 ])
Совмещения символов
xy concatenation (x followed by y)
x|y alternation (x or y, prefer x)
Повторы символов
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
Пустые символы
^ the beginning of text (or start-of-line with multi-line mode)
$ the end of text (or end-of-line with multi-line mode)
\A only the beginning of text (even with multi-line mode enabled)
\z only the end of text (even with multi-line mode enabled)
\b a Unicode word boundary (\w on one side and \W, \A, or \z on other)
\B not a Unicode word boundary
Группировка и флаги
(exp) numbered capture group (indexed by opening parenthesis)
(?P<name>exp) named (also numbered) capture group (names must be alpha-numeric)
(?<name>exp) named (also numbered) capture group (names must be alpha-numeric)
(?:exp) non-capturing group
(?flags) set flags within current group
(?flags:exp) set flags for exp (non-capturing)
Спец-символы
\* literal *, works for any punctuation character: \.+*?()|[]{}^$
\a bell (\x07)
\f form feed (\x0C)
\t horizontal tab
\n new line
\r carriage return
\v vertical tab (\x0B)
\123 octal character code (up to three digits) (when enabled)
\x7F hex character code (exactly two digits)
\x{10FFFF} any hex character code corresponding to a Unicode code point
\u007F hex character code (exactly four digits)
\u{7F} any hex character code corresponding to a Unicode code point
\U0000007F hex character code (exactly eight digits)
\U{7F} any hex character code corresponding to a Unicode code point
Первое совпадение будет иметь тип Option<match>, а в случае отсутствия совпадений = None.
Поиск всех совпадений
letpattern=regex::Regex::new(r"hello, (world|universe)!")?;letinput="hello, world! hello, universe!";letmatches: Vec<_>=pattern.find_iter(input).collect();// find_iter()
matches.iter().for_each(|i|println!("{}",i.as_str()));// matches = Vec<match> и содержит все совпадения
Add serde framework with Derive feature to use it in structures and functions. Also add a separate serde_json lib for converting into specifically JSON:
cargo add serde -F derive
cargo add serde_json
Usage
Add serde, then mark the structures with Serialise, Deserialise traits and use serde_json for serialising:
useserde::{Deserialize,Serialize};#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]pubenumLoginRole{Admin,User,}#[derive(Debug, Clone, Serialize, Deserialize)]pubstructUser{pubusername: String,pubpassword: String,pubrole: LoginRole,}pubfnget_default_users()-> HashMap<String,User>{letmutusers=HashMap::new();users.insert("admin".to_string(),User::new("admin","password",LoginRole::Admin),);users.insert("bob".to_string(),User::new("bob","password",LoginRole::User),);users}pubfnget_users()-> HashMap<String,User>{letusers_path=Path::new("users.json");ifusers_path.exists(){// Load the file!
letusers_json=std::fs::read_to_string(users_path).unwrap();letusers: HashMap<String,User>=serde_json::from_str(&users_json).unwrap();users}else{// Create a file and return it
letusers=get_default_users();letusers_json=serde_json::to_string(&users).unwrap();std::fs::write(users_path,users_json).unwrap();users}
Flatten
Если при сериализации в JSON вы хотите, чтобы вложенные структуры или enum выглядели как часть общего объекта, используйте атрибут #[serde(flatten)]
Он убирает лишние уровни вложенности и делает JSON более читаемым и удобным:
usestd::process;// номер процесса PID вывести
usesysinfo::{RefreshKind,System};fnmain(){let_sys=System::new_with_specifics(RefreshKind::nothing());letpid=process::id();println!("System name: {:?}",System::name());println!("System kernel version: {:?}",System::kernel_version());println!("System OS version: {:?}",System::os_version());println!("System host name: {:?}",System::host_name());println!("Process ID: {}",pid);}
С помощью RefreshKind::nothing() отключаются все динамические вещи, такие как остаток свободной памяти, загрузка ЦПУ, сеть и так далее, что драматически ускоряет опрос системы.
Можно частично опрашивать разные системные ресурсы, по заданным интервалам.
usethiserror::Error;#[derive(Error, Debug)]pubenumMyLibError{#[error("Network failed: {0}")]Network(String),#[error("Invalid user ID: {id}")]InvalidUserId{id: u32},#[error("File not found")]NotFound,#[error("IO error: {0}")]Io(#[from]std::io::Error),}
#[from] в Io означает ? превращает std::io::Error в MyLibError::Io(...).
Все функции библиотеки возвращают Result с ошибкой в MyLibError:
pubfnget_user(id: u32)-> Result<String,MyLibError>{ifid==0{returnErr(MyLibError::InvalidUserId{id});}std::fs::read_to_string("config.txt")?;// автоконвертирует тип ошибки
Ok(format!("User {}",id))}
Особенность применения:
Для работы с ошибками в БИБЛИОТЕКАХ (libs)!
В приложениях проще унифицировать тип ошибки, поэтому там применяется anyhow.
Пользователи библиотеки могут проверять по конкретным ошибкам:
useyour_lib::{get_user,MyLibError};matchget_user(0){Ok(name)=>println!("Got {name}"),Err(MyLibError::InvalidUserId{id})=>{println!("You gave ID {id}, but that's invalid!");// обработать конкретно
}Err(MyLibError::NotFound)=>{println!("File missing, creating new one...");// создать файл
}Err(e)=>{println!("Other error: {e}");}}
В случае с anyhow, пользователи получили бы единый тип anyhow::Error и вынуждены были бы парсить строки (плохой код).
Пример применения:
// Библиотека (image_loader.rs)
#[derive(thiserror::Error, Debug)]pubenumImageError{#[error("File too large: {size} bytes")]TooLarge{size: u64},#[error("Unsupported format: {format}")]BadFormat{format: String},#[error("Corrupted data")]Corrupted,}pubfnload_image(path: &str)-> Result<Vec<u8>,ImageError>{// ...
}// Кто-то используем библиотеку:
matchimage_loader::load_image("photo.png"){Ok(data)=>render(data),Err(ImageError::TooLarge{size})=>println!("Your image is {size} bytes, max is 10MB"),Err(ImageError::BadFormat{format})=>println!("Sorry, we don't support {format} files"),Err(ImageError::Corrupted)=>println!("The image is broken"),}
Наблюдаемость позволяет понять систему извне, задавая вопросы о ней, при этом не зная ее внутреннего устройства. Кроме того, она позволяет легко устранять неполадки и решать новые проблемы, то есть «неизвестные неизвестные». Она также поможет вам ответить на вопрос «почему что-то происходит?».
Чтобы задать эти вопросы о вашей системе, приложение должно содержать полный набор инструментов. То есть код приложения должен отправлять сигналы, такие как трассировки, метрики и журналы. Приложение правильно инструментировано, когда разработчикам не нужно добавлять дополнительные инструменты для устранения неполадок, потому что у них есть вся необходимая информация.
Терминология
Событие журнала (Log event/message) - событие, произошедшее в конкретный момент времени;
Промежуток (Span record) - запись потока исполнения в системе за период времени. Он также выполняет функции контекста для событий журнала и родителя для под-промежутков;
Трасса (trace) - полная запись потока исполнения в системе от получения запроса до отправки ответа. Это по сути промежуток-родитель, или корневой промежуток;
Подписчик (subscriber) - реализует способ сбора данных трассы, например, запись их в стандартный вывод;
Контекст трассировки (Tracing Context): набор значений, которые будут передаваться между службами
usetracing::info;fnmain(){// Установка глобального сборщика по конфигурации
tracing_subscriber::fmt::init();letnumber_of_yaks=3;// новое событие, вне промежутков
info!(number_of_yaks,"preparing to shave yaks");}
Ручная инициализация свойств подписчика для форматирования лога:
fnsetup_tracing(){letsubscriber=tracing_subscriber::fmt().json()// нужно cargo add tracing-subscriber -F json
.with_max_level(tracing::Level::TRACE)// МАХ уровень логирования
.compact()// компактный лог
.with_file(true)// показывать файл-исходник
.with_line_number(true)// показать номера строк кода
.with_thread_ids(true)// показать ID потока с событием
.with_target(false)// не показывать цель (модуль) события
.finish();tracing::subscriber::set_global_default(subscriber).unwrap();tracing::info!("Starting up");tracing::warn!("Are you sure this is a good idea?");tracing::error!("This is an error!");}
Макрос #[instrument] автоматически создаёт промежутки (spans) для функций, а подписчик (subscriber) настроен выводить промежутки в stdout.
Трассировка потоков в асинхронном режиме
usetracing_subscriber::fmt::format::FmtSpan;#[tracing::instrument]// инструмент следит за временем работы
asyncfnhello(){tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;}#[tokio::main]// cargo add tokio -F time,macros,rt-multi-thread
asyncfnmain()-> anyhow::Result<()>{letsubscriber=tracing_subscriber::fmt().json().compact().with_file(true).with_line_number(true).with_thread_ids(true).with_target(false).with_span_events(FmtSpan::ENTER|FmtSpan::CLOSE)// вход и выход
.finish();// потока отслеживать
tracing::subscriber::set_global_default(subscriber).unwrap();hello().await;Ok(())}
В итоге получаем лог работы потока с временем задержки:
2024-12-24T14:30:17.378906Z INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"}{}2024-12-24T14:30:18.383596Z INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"}{}2024-12-24T14:30:18.383653Z INFO ThreadId(01) hello: src/main.rs:3: {"message":"enter"}{}2024-12-24T14:30:18.383675Z INFO ThreadId(01) hello: src/main.rs:3: {"message":"close","time.busy":"179µs","time.idle":"1.00s"}{}
Для записи журнала применяется отдельный модуль tracing-appender:
cargo add tracing-appender
У него много функций не только записи в облачные службы типа Datadog, но и создание журнала с дозаписью (минутной, часовой, дневной), а также запись как неблокирующее действие во время многопоточного исполнения.
Пример инициализации неблокирующей записи журнала в файл (лучше всего как JSON), одновременно вывод на экран и организация часового журнала с дозаписью (rolling):
usetracing::{instrument,warn};// тянем std::io::Write признак в режиме неблокирования
usetracing_subscriber::fmt::writer::MakeWriterExt;fnsetup_tracing(){// инициализация файла с дозаписью
letlogfile=tracing_appender::rolling::hourly("/some/directory","app-log");// уровень записи при логировании = INFO
letstdout=std::io::stdout.with_max_level(tracing::Level::INFO);letsubscriber=tracing_subscriber::fmt().with_max_level(tracing::Level::TRACE).json().compact().with_file(true).with_line_number(true).with_thread_ids(true).with_target(false).with_writer(stdout.and(logfile))// обязательно указать запись тут
.finish();// в файл и в stdout консоль
tracing::subscriber::set_global_default(subscriber).unwrap();}#[instrument]fnsync_tracing(){warn!("event 1");sync_tracing_sub();}#[instrument]fnsync_tracing_sub(){warn!("event 2");}fnmain(){setup_tracing();sync_tracing();}
Инициализация единого трассировщика на проекте
В составе Workspace трассировщик следует инициализировать единожды и далее использовать во всём проекте.
Для этого на верхнем уровне Workspace в Cargo.toml вписываем зависимость с применением по всему Workspace:
usecommon_log::setup_tracing;usetracing::{debug,error,info,warn};fnmain(){setup_tracing();// инициализация логгера из библиотеки common-log
info!("Logger initialized. App started.");// вызов логгера
}
Лог будет сохранён в папке /log/ в бинарном крейте, так как из него делается вызов инициализации.
Использование в тестах
#[cfg(test)]modtests{usesuper::*;usecommon_logging::setup_tracking;#[test]fntest_something(){init_test_logger();// ваш тест
}}
Настройка через переменные окружения
# Установка уровня логированияRUST_LOG=debug cargo run
RUST_LOG=my_library=debug,info cargo run
# Для конкретного крейтаRUST_LOG=my_library=debug cargo run
command - команда и параметры
watch - отслеживаемая папка
need_stdout - stdout вывод кода показывать к терминале
Далее запуск секции:
bacon check-examples
Интерактивный перезапуск компиляции
This will compile+build the code in examples folder, file “variables.rs”. Very convenient to try test different stuff. For live development do:
bacon run -- -q # сборка и запуск текущего проектbacon run -- -q --example <файл> # сборка и запуск файла в папке examplesbacon test# запуск unit-тестов (например, определённых для lib.rs)
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 runcargo run # build & run projectcargo check # fast compilingcargo build --release # slow build with optimizations for speed of release version
Documentation of methods & traits used in code can be compiled an opened in browser with Cargo command:
cargo doc --open
Зависимости кода в cargo.toml
[dependencies] — Package library dependencies - видно отовсюду, из тестов;
[dev-dependencies] — Dependencies for examples, tests, and benchmarks. При сборке ПО видны только для тестов (в рамках #[cfg(test)] mod tests {});
[build-dependencies] — Dependencies for build scripts.
Сборка бинарных файлов
Можно в src папке создать подпапку bin, и там сложить вариации бинарных файлов. После чего указывать при сборке cargo run --bin <название файла в папке bin>
Сборка 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
❗Примеры в Examples папке наследуют features и прочие параметры из общего Cargo.toml. В случае условной компиляции (см, далее) команда будет выглядеть: cargo run --example variables --features mock
Условная компиляция
Позволяет исключить часть кода в зависимости от условий.
#[cfg(feature = "debug")]// код сработает только с флагом debug
fnget_data()-> String{"мулька".to_string()}#[cfg(feature = "mock", feature = "debug")]// код сработает только с флагами mock И debug
fnget_data()-> String{"хитрая мулька".to_string()}#[cfg(not(feature = "debug"))]// код сработает без флага debug
fnget_data()-> String{"реальные данные".to_string()}fnmain(){println!("{}",get_data());}
Features нужно специально включать при сборке командой cargo run --features debug. Либо можно прописать флаг включения default = ["debug"] в файле Cargo.toml:
[features]default=["debug"]# по-умолчанию флаг = ВКЛdebug=[]
Отключить работу флага default можно командой cargo run --no-default-features.
Условная компиляция, встроенная в Cargo, позволяет подключать зависимости опционально. Можно проверять фичи в коде:
Пример с подключением библиотеки rand только для создания случайного значения в “mock-варианте” кода:
[features]default=["mock"]# включить флаг mock по-умолчаниюmock=["rand"]# использовать рандимозатор только при включенном флагеmock-persistent=["rand"][dependencies]rand={version="0.10.0",optional=true}# опциональный рандомизатор
Диагностика сборки
cargo-bloat для поиска проблем
cargo install cargo-bloat
cargo bloat --release
Определяет, какие зависимости задерживают сборку.
cargo build –timings
Создаёт отчёт в HTML с указанием трат времени:
cargo build --timings
# далее открыть target/cargo-timings/cargo-timing.html
cargo-llvm-lines
Показывает, какие generic-функции создают больше всего нагрузки для LLVM:
Generics создают мономорфизм - каждый новый тип создаёт новый код при сборке.
Ускорение сборки
Профили Cargo
Можно управлять ходом сборки проекта с помощью профилей. В том числе это сильно виляет на скорость сборки. Примеры профилей, которые можно создать в cargo.toml файле:
[profile.dev]opt-level=0# No optimization (fastest compilation LLVM)debug=1# Line info only (not full debug symbols)codegen-units=256# More parallelism (max is 256)incremental=true# Recompile only changed code[profile.release]opt-level=3# Maximum runtime performancelto="thin"# Faster than "fat" LTO, still good optimizationcodegen-units=1# Better optimization at cost of compile time
Для debug сборок, opt-level = 0 и высокое число codegen-units даю самую высокую скорость сборки (разница в скорости в 108 раз!).
Кеширование сборки зависимостей
С помощью sccache можно кешировать собранные артефакты, в том числе таскать кеш между системами.
Использовать все ядра, кроме 2 (чтобы ОС не повисла):
cargo build -j $(nproc --ignore=2)
Либо указать в .cargo/config.toml:
[build]jobs=8
Panic Response
В ответ на панику, по умолчанию программа разматывает стек (unwinding) - проходит по стеку и вычищает данные всех функций. Для уменьшения размера можно просто отключать программу без очистки - abort. Для этого в файле Cargo.toml надо добавить в разделы [profile]:
[profile.release]panic='abort'
Mold (только для Linux)
Проект для Linux (в macOS есть коммерческий аналог Sold, который ненамного лучше сборщику в XCode 15+, потому использование не оправдано).
Настройка
Ubuntu: sudo apt-get install mold clang
Fedora: sudo dnf install mold clang
Arch Linux: sudo pacman -S mold clang
Создать в проекте папку и файл .cargo/config.toml (либо для всех проектов сразу - папка и файл ~/.cargo/config.toml) и вписать:
Далее, в код бинарной программы включить функции из библиотеки:
useexamplelib::function01;// фукнкция должна быть публичной (pub fn)
Единая инициализация и сборка библиотек зависимостей в Workspace
Создаём проект типа workspace и прописываем в его файле Cargo.toml верхнего уровня все библиотеки с версиями (в примере anyhow) в спец разделе [workspace.dependencies]:
При этом в отдельном разделе [dependencies] указываем, что библиотеки будут распространяться на весь проект. Далее, создаём модуль внутри workspace (в примере = greeter) и в его файле Cargo.toml прописываем, что библиотека берётся из зависимостей workspace:
[dependencies]anyhow={workspace=true}
Можно добавлять features к библиотеке из workspace на этапе описания вложенных модулей:
Замыкания - анонимные функции. Их можно присвоить переменным и вызывать. Это также функция, которая ссылается на свободные переменные в своей области видимости. Базовое использование:
letadd_one=|x: i32|-> i32{x+1};println!("Add one to 5 = {}",add_one(5));// Add one to 5 = 6
letadd_two=|x|x+2;println!("Add two to 5 = {}",add_two(5));// Add two to 5 = 7
letadd=|a,b|a+b;println!("Sum of 5 and 6 = {}",add(5,6));// Sum of 5 and 6 = 11
letjust_number=||42;println!("Answer to all: {}",just_number());// Answer to all: 42
В отличие от функций, замыкания могут использовать переменные вне своего блока:
(0..3).for_each(|x|{println!("map i = {}",x*2);});letfactor=2;letmultiplier=|x|x*factor;println!("{}",multiplier(5));// 10
println!("Factor: {factor}");// factor ещё доступен тут
Это нужно для того, чтобы замыкание получило владение данным и пережило scope, в котором оно было объявлено.
Работа с итераторами
.map(<closure>) передаёт владение элементами итератора замыканию, чтобы их можно было трансформировать в другие элементы, которые далее возвращает замыкание.
.filter(<closure>) возвращает оригинальные элементы, когда предикат замыкания возвращает true. Таким образом, отдавать владение элементами замыканию нельзя, и нужно передавать по ссылке.
Можно вернуть элементы, когда предикат значения true, и сразу же их трансформировать. На вход он принимает замыкание, возвращающее Option<T>:
Если на выходе Some(value), значение включается в результат;
Если замыкание возвращает None, элемент исключается фильтром.
letnumbers=vec!["1","2","abc","4"];// Раздельно: filter() и потом map()
letresult: Vec<i32>=numbers.iter().filter(|s|s.parse::<i32>().is_ok())// фильтровать цифры
.map(|s|s.parse::<i32>().unwrap())// Перевести их в int
.collect();// Result: [1, 2, 4]
// Вместе: filter_map()
letresult: Vec<i32>=numbers.iter().filter_map(|s|s.parse::<i32>().ok())// фильтр и перевод в 1 шаг
.collect();// Result: [1, 2, 4]
Вложенные замыкания map()
leta=(0..=3).map(|x|x*2).map(|y|y-1);// первая итерация map(): 2, 4, 6
// вторая итерация map(): 1, 3, 5
foriina{println!("{i}");}
All
Замыкание all возвращает True, если все элементы в замыкании соответствуют условию.
leta: Vec<i32>=vec![1,2,3,4];print!("{}\n",a.into_iter().all(|x|x>1));// false
Для пустого вектора замыкание all вернёт True:
leta: Vec<i32>=vec![];print!("{}\n",a.into_iter().all(|x|x>1));// true
Цикл через замыкание vs for
usestd::collections::HashMap;pubfnmain(){letnum_vec=vec![1,2,1,3,5,2,1,4,6];letmutnumber_count: HashMap<i32,i32>=HashMap::new();forkeyinnum_vec{*number_count.entry(key).or_default()+=1;}/* for (k, v) in number_count {
print!("{} -> {}; ", k, v);
} */number_count.iter().for_each(|(k,v)|{print!("{} -> {}; ",k,v);});//цикл через замыкание итератора
}
letnumbers=vec![1,2,3,4,5];letproduct=numbers.iter().fold(1,|acc,&x|acc*x);println!("Product: {}",product);// 120
// код выше можно заменить на product()
От fold() отличается тем, что прерывает выполнение и возвращает Result(Err):
letstrings=["1","2","3","4","5"];// перевести строки в числа и суммировать их
letsum: Result<i32,_>=strings.iter().try_fold(0,|acc,&s|matchs.parse::<i32>(){Ok(num)=>Ok(acc+num),Err(e)=>Err(e),});matchsum{Ok(total)=>println!("Total: {}",total),// 15
Err(e)=>println!("Parse error: {}",e),}
Это изменяемая структура словарь (“dictionary” в Python), которая хранит пары “ключ->значение”. В Rust Prelude она не входит, макроса создания не имеет. Поэтому нужно указывать библиотеку явно и явно создавать структуру.
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 ссылку на это значение.
Записывать значение, если ключа нет. Если же у ключа есть значение, модифицировать его:
usestd::collections::HashMap;letmutmap: 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):
Упорядоченная по ключам K структура данных на основе B-дерева.
Ключевые отличия от HashMap:
Свойство
HashMap
BTreeMap
Внутренняя структура
Хэш таблица
Дерево поиска
Упорядоченность
Не гарантированная
Ключи всегда упорядочены
Скорость
O(1)
O(log n)
Требования
Hash + Eq
Ord
Потребление памяти
Выше
Ниже
Применение
Когда нужно сразу сортировать данные:
letmutscores=BTreeMap::new();scores.insert("Charlie",85);scores.insert("Alice",92);scores.insert("Bob",78);// Итерация сразу в отсортированном порядке по ключам
for(name,score)in&scores{println!("{}: {}",name,score);// Alice, Bob, Charlie
}// Получить 1ый и последний элементы
println!("{:?}",scores.first_key_value());// ("Alice", 92)
println!("{:?}",scores.last_key_value());// ("Charlie", 85)
Работа с диапазонами значений:
// Диапазоны заданы кортежами (нижняя_граница, верхняя_граница)
letmutprice_ranges=BTreeMap::new();price_ranges.insert((0,10),"Budget");price_ranges.insert((11,50),"Standard");price_ranges.insert((51,100),"Premium");price_ranges.insert((101,1000),"Luxury");letquery_price=42;// Поиск по диапазонам (линейный в данном случае)
for((low,high),category)in&price_ranges{ifquery_price>=*low&&query_price<=*high{println!("Price ${} is in category: {} (range: {}-{})",query_price,category,low,high);break;}}
useserde_jsonletmutconfig=BTreeMap::new();config.insert("zebra","animal");config.insert("apple","fruit");config.insert("banana","fruit");// сериализация в JSON с порядком элементов
letjson=serde_json::to_string_pretty(&config).unwrap();println!("{}",json);// ключи будут в порядке: apple, banana, zebra
Для небольших коллекций (< 1000 элементов) или когда требуется отсортированный порядок, BTreeMap часто предпочтительнее. Для больших коллекций, где нужна максимальная скорость, а порядок не имеет значения, HashMap обычно лучше.
Оба типа представляют множества для хранения уникальных элементов.
Отличия HashSet и BTreeSet
Множество
HashSet
BTreeSet
Сортировка
Случайный порядок
Да
Сложность алгоритма
O(1) в среднем
O(log n)
Потребление памяти
Выше (hash-таблица)
Ниже
Запросы диапазонов значений
Нет
Да
Типажи
T: Hash + Eq
T: Ord
Работа с HashSet
usestd::collections::HashSet;fnmain(){// Создание
letmutcolors=HashSet::new();// Добавление элеметов
colors.insert("red");colors.insert("green");colors.insert("blue");colors.insert("red");// дубликат - не будет добавлен!
println!("HashSet: {:?}",colors);// порядок произвольный
// Проверка существования элемента
ifcolors.contains("green"){println!("Contains green!");}// Итерация (порядок не гарантирован)
forcolorin&colors{println!("Color: {}",color);}// Удаление элемента
colors.remove("blue");// Объединение двух HashSet
letmutwarm_colors=HashSet::new();warm_colors.insert("red");warm_colors.insert("yellow");warm_colors.insert("orange");letall_colors: HashSet<_>=colors.union(&warm_colors).collect();println!("All colors: {:?}",all_colors);}
Работа с BTreeSet
usestd::collections::BTreeSet;fnmain(){// Создание BTreeSet
letmutnumbers=BTreeSet::new();// Вставка элементов (авто-сортировка!)
numbers.insert(5);numbers.insert(2);numbers.insert(8);numbers.insert(1);numbers.insert(5);// дубликат - не будет добавлен!
println!("BTreeSet: {:?}",numbers);// сортировка всегда: {1, 2, 5, 8}
// Проверка существования элемента
ifnumbers.contains(&2){println!("Contains 2!");}// Итерация по порядку
fornumin&numbers{println!("Number: {}",num);// 1, 2, 5, 8
}// Получить первый и последний элементы
ifletSome(first)=numbers.first(){println!("First element: {}",first);// 1
}ifletSome(last)=numbers.last(){println!("Last element: {}",last);// 8
}// Запрос диапазона значений
letrange: Vec<_>=numbers.range(2..=5).collect();println!("Numbers between 2 and 5: {:?}",range);// [2, 5]
// Удаление элемента
numbers.remove(&5);}
HashSet -> Vector
Простой способ:
usestd::collections::HashSet;letset: HashSet<char>=['a','b','c','d'].into_iter().collect();// Перевод в Vec<char> - расстановка элементов произвольная
letvec: Vec<char>=set.into_iter().collect();println!("Vec: {:?}",vec);// Example: ['c', 'a', 'd', 'b']
Вектор - множество данных одного типа, количество которых можно изменять: добавлять и удалять элементы. Нужен, когда:
требуется собрать набор элементов для обработки в других местах;
нужно выставить элементы в определённом порядке, с добавлением новых элементов в конец;
нужно реализовать стэк;
нужен массив изменяемой величины и расположенный в куче.
Методы
// Задание пустого вектора:
// let mut a test_vector: Vec<i32> = Vec::new();
// Задание вектора со значениями через макрос:
letmuttest_vector=vec![1,2,3,4];test_vector.push(42);// добавить число 42 в конец mut вектора
letSome(last)=test_vector.pop();// удаляет и возвращает последний элемент (возвращает Option<T>)
test_vector.remove(0);// удалить первый элемент =1
foriin&muttest_vector{// пройти вектор как итератор для вывода
*i+=1;// изменять значения при их получении требует делать '*' dereference
println!("{i}");}println!("Vector length: {}",test_vector.len());// количество элементов
Элемент можно получить либо с помощью индекса, либо с помощью безопасного метода get:
letmuttest_vector=vec![1,2,3,4,5];println!("Third element of vector is: {}",&test_vector[2]);// индекс
letthird: Option<&i32>=test_vector.get(2);// безопасный метод get
matchthird{Some(third)=>println!("Third element of vector is: {}",third),None=>println!("There is no third element")}
Разница в способах в реакции на попытку взять несуществующий элемент за пределами вектора. Взятие через индекс приведёт к панике и остановке программы. Взятие с помощью get сопровождается проверкой и обработкой ошибки.
Удаление элемента
Метод .remove(index):
letmutnumbers=vec![1,2,3,4];numbers.remove(1);// удаляет элемент с индексом 1
println!("{:?}",numbers);// [1, 3, 4]
.remove() сдвигает все последующие элементы, что может быть дорого для больших векторов (O(n));
Возвращает удалённый элемент;
Требует mut, так как изменяет вектор;
Индекс должен быть в пределах длины, иначе паника.
Хранение элементов разных типов в векторе
Rust нужно заранее знать при компиляции, сколько нужно выделять памяти под каждый элемент. Если известны заранее все типы для хранения, то можно использовать промежуточный enum:
Смена элементов при сравнении, метод .sort_by() принимает замыкание (closure) для пользовательской сортировки:
number_vector.sort_by(|a,b|b.cmp(a));
|a, b| — это замыкание;
b.cmp(a) возвращает порядок: Ordering::Less, Equal или Greater. Инверсия (b.cmp(a) вместо a.cmp(b)) даёт убывающий порядок.
Альтернатива: .sort_by_key() для сортировки по вычисляемому ключу:
letmutnumbers=vec![3,1,4,1,5];numbers.sort_by_key(|&x|-x);// по убыванию через отрицание
println!("{:?}",numbers);// [5, 4, 3, 1, 1]
Если вернуть Reverse со ссылкой и без *, это приведёт к проблеме с временем жизни.
Сортировка вектора по ключу
usestd::collections::HashSet;fnmain(){letmutvowels=HashSet::new();vowels.insert('e');vowels.insert('a');vowels.insert('i');vowels.insert('o');vowels.insert('u');// Конвертация в Vec и сортировка
letmutvowel_vec: Vec<char>=vowels.into_iter().collect();// Свой порядок сортировки: a, e, i, o, u
letvowel_order=|c: &char|matchc{'a'=>0,'e'=>1,'i'=>2,'o'=>3,'u'=>4,_=>5,};vowel_vec.sort_by_key(vowel_order);println!("Sorted vowels: {:?}",vowel_vec);// ['a', 'e', 'i', 'o', 'u']
}
Дедупликация вектора
Удаление одинаковых элементов в векторе, похоже на работу с HashSet.
lets="aabbccdddeeeeffffeee";letmutchars: Vec<char>=s.chars().collect();// Сначала отсортировать, чтобы собрать одинаковые элементы вместе
chars.sort_unstable();// dedup() удаляет на месте одинаковые СТОЯЩИЕ РЯДОМ в векторе элементы
chars.dedup();// собрать назад в String:
letunique_s: String=chars.into_iter().collect();
Сделать новый проект, добавить в него библиотеку cargo add current_platform. Далее создаём и запускаем код проверки среды компиляции и исполнения:
usecurrent_platform::{COMPILED_ON,CURRENT_PLATFORM};fnmain(){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
Запуск кода на компиляцию под ОС Windows: cross run --target x86_64-pc-windows-gnu => в папке target/x86_64-pc-windows-gnu/debug получаем EXE-файл с результатом.
❗Компиляция проходит через WINE.
Проверка среды компиляции
Cross поддерживает тестирование других платформ. Добавка проверки:
Запустить проверку локально: cargo test
Запустить проверку с кросс-компиляцией: cross test --target x86_64-pc-windows-gnu
На Linux/macOS проверка пройдёт, а вот при компиляции под Windows - нет:
`test tests::test_compiled_on_equals_current_platform … FAILED
Добавка платформенно-специфичного кода
Можно вписать код, который запустится только на определённой ОС, например, только на Windows:
usecurrent_platform::{COMPILED_ON,CURRENT_PLATFORM};#[cfg(target_os="windows")]fnwindows_only(){println!("This will only print on Windows!");}fnmain(){println!("Run from {}! Compiled on {}.",CURRENT_PLATFORM,COMPILED_ON);#[cfg(target_os="windows")]{windows_only();}}
Перечисление — это тип данных, который позволяет определить набор именованных значений (вариантов). Каждый вариант может быть просто именем или содержать дополнительные данные.
enumMoney{Rub,Kop}
Здесь мы определили перечисление Money с двумя вариантами: Rub, и Kop. Эти варианты не содержат дополнительных данных — они просто имена, которые представляют возможные состояния. В терминах Rust такие варианты без данных часто называют “unit-like” (похожими на Unit), но это не совсем то же самое, что массив или указатели.
Unit
“Unit” в Rust — это специальный тип (), который имеет только одно значение, тоже обозначаемое как (). Это что-то вроде “пустого значения”, которое часто используется, когда функция ничего не возвращает. Например:
fn do_nothing() {
// Ничего не возвращаем, implicitly возвращается ()
}
В случае с Money каждый вариант (Rub и Kop) сам по себе не является типом (), но его можно рассматривать как “unit-like”, потому что он не несёт дополнительных данных. Это просто маркер, который говорит: “Я одно из двух состояний”.
Enum в памяти
Внутри памяти Money представлен как небольшое целое число (обычно 1 байт для простых перечислений вроде этого), называемое “дискриминантом”. Этот дискриминант указывает, какой вариант сейчас используется:
Money::Rub → 0
Money::Kop → 1
Но это внутренняя реализация. Для программиста это просто разные состояния.
match
Аналог switch в других языках, однако, круче: его проверка не сводится к bool, а также реакция на каждое действие может быть блоком:
n - привязка (binding). Когда ты пишешь n if n > 0, то говоришь: “возьми значение number и назови его n для этой ветки. Затем проверь условие if n > 0. Если оно истинно, выполни действие”. Обычно match используется с точными значениями (например, 1 => ..., 2 => ...), но добавление if позволяет проверять более сложные условия, как в этом примере (положительное, отрицательное или ноль). Это называется guards (охрана) в Rust.
Ещё пример:
fnmain(){letm=Money::Kop;println!("Я нашёл кошелёк, а там {}p",match_value_in_kop(m));}fnmatch_value_in_kop(money: Money)-> u8{matchmoney{Money::Rub=>100,Money::Kop=>{println!("Счастливая копейка!");1}}}
match как выражение
fnmain(){letnumber=3;letresult=matchnumber{1=>"один",2=>"два",_=>"другое",};println!("Результат: {}",result);// Вывод: Результат: другое
}
Проверка условия и запуск соответствующего метода:
structState{color: (u8,u8,u8),position: Point,quit: bool,}implState{fnchange_color(&mutself,color: (u8,u8,u8)){self.color=color;}fnquit(&mutself){self.quit=true;}fnecho(&self,s: String){println!("{}",s);}fnmove_position(&mutself,p: Point){self.position=p;}fnprocess(&mutself,message: Message){matchmessage{// проверка и запуск одного из методов
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),}}}
Использование Option как enum
fndivide(a: i32,b: i32)-> Option{ifb==0{None}else{Some(a/b)}}fnmain(){matchdivide(10,2){Some(result)=>println!("Result: {}",result),// Result: 5
None=>println!("Division by zero!"),}}
if let
В случае, когда выбор сводится к тому, что мы сравниваем 1 вариант с заданным паттерном и далее запускаем код при успехе, а в случае неравенства ничего не делаем, можно вместо match применять более короткую конструкцию if-let:
letconfig_max=Some(3u8);matchconfig_max{Some(max)=>println!("The maximum is configured to be {}",max),_=>(),// другие варианты ничего не возвращают
}
Превращается в:
letconfig_max=Some(3u8);ifletSome(max)=config_max{println!("The maximum is configured to be {}",max);}
Применение if-let - это синтаксический сахар, укорачивает код, однако, лишает нас проверки наличия обработчиков на все варианты возвращаемых значений как в конструкции match.
Сравнение величин
Для сравнения значений в переменных есть метод std::cmp, который возвращает объект типа enum Ordering с вариантами:
usestd::cmp::Ordering;usestd:io;fnmain(){letsecret_number=42;letmutguess=String::new();io::stdin().read_line(&guess).expect("Read line failed!");letguess:i32=guess.trim().parse().expect("Error! Non-number value entered.");matchguess.cmp(&secret_number){Ordering::Greater=>println!("Number too big"),Ordering::Less=>println!("Number too small"),Ordering::Equal=>println("Found exact number!")}}
Option — это перечисление (enum), которое используется, когда значение может быть либо “чем-то” (Some), либо “ничем” (None). Это замена привычным null или nil из других языков, но с важным отличием: в Rust вы обязаны явно обработать возможность отсутствия значения. Определение Option в стандартной библиотеке:
enumOption<T>{Some(T),None,}
где:
T — это любой тип данных (например, i32, String и т.д.).
Some(T) — значение есть, и оно равно T.
None — значения нет.
Result
Result — это перечисление, которое используется для операций, которые могут завершиться успехом (Ok) или неудачей (Err):
enumResult<T,E>{Ok(T),// Успех с результатом типа T
Err(E),// Ошибка с типом E
}
где:
T — тип возвращаемого значения при успехе.
E — тип ошибки при неудаче.
Обработка Option и Result
Использовать match и обработать все варианты поведения;
unwrap — “дай мне значение или паника” (если None или Err, программа крашится);
unwrap_or — “дай мне значение или что-то другое” (если None или Err, возвращает запасное значение, которое ты указал);
expect — “дай мне значение или паника с моим текстом” (как unwrap, но с твоим сообщением при краше);
is_some — “скажи, есть ли там значение?” (возвращает true, если Some в Option, и false, если None);
is_none — “скажи, пусто ли там?” (возвращает true, если None в Option, и false, если Some).
Если коротко: unwrap, unwrap_or и expect пытаются достать значение и что-то с ним сделать, а is_some и is_none просто проверяют, что внутри, не трогая само значение.
Обработка ошибки с map_err()
.map_err() — это метод, который позволяет преобразовать тип ошибки, не трогая успешное значение.
result.map_err(|err|{// преобразуем старую ошибку err в новую
new_error})
Работа map_err() с цепочками операций:
usestd::fs::File;usestd::io::Read;usestd::num::ParseIntError;// Создаём свой тип ошибки для понятного описания
#[derive(Debug)]enumMyError{IoError(String),ParseError(String),}fnread_and_parse()-> Result<i32,MyError>{// Пробуем открыть файл
letmutfile=File::open("number.txt").map_err(|e|MyError::IoError(format!("Ошибка открытия: {}",e)))?;// Пробуем прочитать содержимое
letmutcontents=String::new();file.read_to_string(&mutcontents).map_err(|e|MyError::IoError(format!("Ошибка чтения: {}",e)))?;// Пробуем распарсить число
letnumber: i32=contents.trim().parse().map_err(|e|MyError::ParseError(format!("Ошибка парсинга: {}",e)))?;Ok(number)}fnmain(){matchread_and_parse(){Ok(num)=>println!("Число: {}",num),Err(MyError::IoError(msg))=>println!("IO Ошибка: {}",msg),Err(MyError::ParseError(msg))=>println!("Ошибка парсинга: {}",msg),}}
В широком использовании удобнее использовать context из библиотеки anyhow. Однако, map_err нужен для преобразования в другой тип ошибки:
enumMyError{IoError(std::io::Error),ParseError(String),}fnparse_config()-> Result<(),MyError>{letcontent=std::fs::read_to_string("config.txt").map_err(MyError::IoError)?;// Преобразуем в наш тип
// ... обработка
Ok(())}
Ошибки, не реализующие std::error::Error:
// Некоторый API возвращает Result<..., ()>
fnold_api()-> Result<i32,()>{Ok(42)}// context() не работает, потому что () не реализует Error
letvalue=old_api().map_err(|_|anyhow::anyhow!("старое API вернуло ошибку"))?;
В коде, использующем anyhow, лучше context() и with_context() перед map_err(|e| anyhow!(...)). А map_err() для случаев, когда нужно преобразовать ошибку в другой тип или реализовать нестандартную логику обработки.
Оператор ‘?’
Оператор ? используется в функциях, возвращающих Result. Он автоматически возвращает Err из функции, если результат — Err, или извлекает значение из Ok:
fndivide(a: i32,b: i32)-> Result<i32,String>{ifb==0{Err(String::from("Деление на ноль!"))}else{Ok(a/b)}}fnsafe_division(a: i32,b: i32)-> Result<i32,String>{letresult=divide(a,b)?;Ok(result*2)// Удваиваем результат
}fnmain(){println!("{:?}",safe_division(10,0));// Err("Деление на ноль!")
println!("{:?}",safe_division(10,2));// Ok(10)
}
Ошибки без восстановления
Ряд ошибок приводит к вылету приложения. Также можно вручную вызвать вылет командой panic!:
fnmain(){panic!("Battery critically low! Shutting down to prevent data loss.");}
При этом некоторое время тратится на закрытие приложения, очистку стека и данных. Можно переложить это на ОС, введя настройку в Cargo.toml:
[profile.release]panic='abort'
Пользовательские ошибки с enum
Иногда стандартных типов ошибок (например, String) недостаточно. Вы можете создать свои собственные ошибки с помощью enum:
enumMathError{// введём свой enum с типами ошибок
DivisionByZero,NegativeNumber,}fndivide_with_custom_error(a: i32,b: i32)-> Result<i32,MathError>{ifb==0{Err(MathError::DivisionByZero)}elseifa<0||b<0{Err(MathError::NegativeNumber)}else{Ok(a/b)}}fnmain(){matchdivide_with_custom_error(10,0){Ok(value)=>println!("Результат: {}",value),Err(MathError::DivisionByZero)=>println!("Ошибка: деление на ноль"),Err(MathError::NegativeNumber)=>println!("Ошибка: отрицательное число"),}}
Преимущества:
Чёткое определение всех возможных ошибок.
Легко расширять (добавьте новый вариант в enum).
Стратегии работы с ошибками
Подготовка примера
Допустим, мы берём вектор из строк-чисел, складываем их и возвращаем сумму как строку:
fnsum_str_vec(strs: Vec<String>)-> String{letmutaccum=0i32;forsinstrs{accum+=to_int(&s);// to_int = заготовка, см. ниже реализацию
}returnaccum.to_string();}fnmain(){letv=vec![String::from("3"),String::from("4")];// Правильный ввод
lettotal=sum_str_vec(v);println!("Total equals: {:?}",total);letv=vec![String::from("3"),String::from("abc")];// Неправильный ввод
lettotal=sum_str_vec(v);println!("Total equals: {:?}",total);}
Для конвертации строки в числа, нужно реализовать функцию to_int в соответствии со стратегиями обработки ошибочного ввода. Конвертацию мы делаем функцией parse(), которая возвращает тип Result<T,E>, где T - значение, E - код ошибки.
Стратегия 1 - паника
В случае неверного ввода, программа полностью останавливается в панике. Метод unwrap() у типа Result<T,E> убирает проверки на ошибки и есть договор с компилятором о том, что ошибки в этом месте быть не может. Если она есть, программа падает с паникой:
fnto_int(s: &str)-> i32{s.parse().unwrap()}
Стратегия 2 - паника с указанием причины
В случае неверного ввода, программа сообщает фразу, заданную автором, далее полностью останавливается в панике. Метод expect() аналогичен unwrap(), но выводит сообщение:
fnto_int(s: &str)-> i32{s.parse().expect("Error converting from string")}
Стратегия 3 - обработать то, что возможно обработать
Можно сконвертировать и прибавить к результату те строки, которые позволяют это сделать, проигнорировать остальные. Метод unwrap_or() позволяет указать возвращаемое значение в случае ошибки:
Более предпочтительный вариант использовать закрытие unwrap_or_else(), так как метод unwrap_or() будет вызван ДО того как будет отработана основная команда, ВНЕ ЗАВИСИМОСТИ от того, будет ли её результат Some или None. Это потеря производительности, а также потенциальные глюки при использовании внутри unwrap_or() сложных выражений. Закрытие unwrap_or_else() будет вызвано только в случае None, иначе же эта ветка не обрабатывается:
Стратегия 5 - в случае проблем, передать всё в основную программу
Вместо передачи значения из функции, в случае каких-либо проблем, мы возвращаем None:
fnsum_str_vec(strs: Vec<String>)-> Option<String>{letmutaccum=0i32;forsinstrs{accum+=to_int(&s)?;// в случае None, ? передаёт его далее на выход
}Some(accum.to_string())// на выход пойдёт значение или None
}
Стратегия 6 - передать всё в основную программу с объяснением ошибки
Мы возвращаем проблему в основную программу с объясением проблемы. Для этого заводим структуру под ошибку, и передаём уже не объект Option<T>, а объект Result<T,E>, где E = SummationError. Для такого объекта есть метод ok_or(), который либо передаёт значение, либо передаёт ошибку нужного типа:
Вместо выдумывать свой собственный тип и конвертировать вывод метода parse() из Result<T,E> в Option<T>, а потом обратно, можно сразу протащить ошибку в объекте Result<T,E> в главную программу:
usestd::num::ParseIntError;// тип ошибки берём из библиотеки
fnto_int(s: &str)-> Result<i32,ParseIntError>{s.parse()// parse возвращает просто Result<T,E>
}fnsum_str_vec(strs: Vec<String>)-> Result<String,ParseIntError>{letmutaccum=0i32;forsinstrs{accum+=to_int(&s)?;}// ? передаёт ошибку нужного типа далее
Ok(accum.to_string())}
Однако, мы хотим скрыть подробности работы и ошибки от главной программы и передать ей ошибку в понятном виде, без разъяснения деталей её возникновения. Для этого можно сделать трансляцию ошибки из библиотечной в собственный тип, и далее передать методом map_err():
Оператор ? можно использовать только в функциях для возврата совместимых значений типа Result<T,E>, Option<T> или иных данных со свойством FromResidual. Для работы такого возврата в заголовке функции должен быть прописан возврат нужного типа данных. При использовании ? на выражении типа Result<T,E> или Option<T>, ошибка Err(e) или None будет возвращена рано из функции, а в случае успеха - выражение вернёт результат, и функция продолжит работу.
Пример функции, которая возвращает последний символ 1ой строки текста:
fnlast_char_of_first_line(text: &str)-> Option<char>{text.lines().next()?.chars().last()}// lines() возвращает итератор на текст
// next() берёт первую строку текста. Если текст пустой - сразу возвращаем None
Ввод-вывод построен вокруг модуля стандартной библиотеки std::io, который предоставляет инструменты для работы с потоками данных, а также модуля std::fs, предназначенного для операций с файловой системой.
Чтение файлов
Чтение текста из файлов в строку
Для чтения файлов, нужно сначала добавить несколько методов из библиотек: FIle - открытие файлов и Read - чтение.
usestd::fs::File;// импорт File
usestd::io::Read;// импорт Read
fnmain()-> std::io::Result<()>{// отлов ошибок ввода/вывода
letfilename="test.txt".to_string();// путь до файла в корне проекта
letmutfiletext=String::new();letmutfilehandle=File::open(filename)?;filehandle.read_to_string(&mutfiletext)?;// чтение в строку
println!("{filetext}");Ok(())}
Чтение в вектор из байтов (vector of bytes)
Чтение файла в память целиком как вектора байтов - для бинарных файлов, либо для частого обращения к содержимому:
usestd::io::Read;usestd::{env,fs,io,str};fnmain()-> io::Result<()>{letmutfile=fs::File::open("test_file.txt")?;letmutcontents=Vec::new();file.read_to_end(&mutcontents);println!("File contents: {:?}",contents);// вывод байт
lettext=matchstr::from_utf8(&contents){// перевод в строку UTF8
Ok(v)=>v,Err(e)=>panic!("Invalid UTF-8: {e}"),};println!("Result: {text}");// вывод строкой
Ok(())}
Запись в файлы
usestd::fs::File;usestd::io::Write;fnmain()-> io::Result<()>{// создать новый файл или обнулить имеющийся:
letmutfile=File::create("report.log")?;file.write_all(b"report.log");// запись байтов в файл
Ok(())}
Буферизация
Чтение текста через буфер
Нужно добавить к библиотеке File также библиотеку организации буфера чтения, а также обработать ошибки открытия файла и чтения.
usestd::fs::File;usestd::io::{BufReader,Read};fnmain()-> std::io::Result<()>{letfile=File::open("sportinv.csv")?;// обернуть File в буферизированный читатель (блоки по 8kb by default):
letmutreader=BufReader::new(file);letmuttext=String::new();reader.read_to_string(&muttext)?;println!("{text}");Ok(())}
Чтение текста из больших файлов в буфер по строчкам
usestd::fs::File;usestd::io::{BufWriter,Write};fnmain()-> std::io::Result<()>{letfile=File::create("output.txt")?;letmutwriter=BufWriter::new(file);writer.write_all(b"Test output!")?;writer.flush()?;// Сброс буфера в файл
Ok(())}
Запись и дополнение файла через буфер по строчкам
usestd::fs::File;usestd::io::{BufWriter,Write};fnmain()-> std::io::Result<()>{letfile=File::create("output.txt")?;letmutwriter=BufWriter::new(file);writeln!(writer,"Первая строка")?;// макрос записи в поток
writeln!(writer,"Вторая строка")?;writer.flush()?;Ok(())}
File::create всегда обнуляет файл, если он уже есть. Чтобы дописать файл, нужно использовать OpenOptions и указать режим append:
usestd::fs::OpenOptions;// OpenOptions вместо File
usestd::io::{BufWriter,Write};fnmain()-> std::io::Result<()>{letfile=OpenOptions::new().append(true)// +режим дозаписи
.create(true)// создать, если файла нет
.open("output.txt")?;// открыть файл
//let file = File::create("output.txt")?;
letmutwriter=BufWriter::new(file);writeln!(writer,"Первая строка")?;writeln!(writer,"Вторая строка")?;writer.flush()?;Ok(())}
Копирование файлов
Функция std::fs::copy принимает исходный и целевой пути и возвращает Result<u64, std::io::Error>, где u64 — количество скопированных байт.
Если целевой файл уже существует, он будет перезаписан.
Перемещение файлов
Функция std::fs::rename принимает исходный и целевой пути и возвращает Result<(), std::io::Error> (при успехе ничего не возвращает (()), при ошибке — описание проблемы):
Если целевой файл существует, он будет перезаписан (на той же файловой системе);
Перемещение между разными файловыми системами может не сработать, надо сначала скопировать, а затем удалить исходный файл вручную.
Удаление файлов
Функция std::fs::remove_file принимает путь к файлу и возвращает результат типа Result<(), std::io::Error>, что позволяет обработать возможные ошибки (например, если файла не существует):
Пути в Rust обрабатываются через структуры Path и PathBuf, где Path — это неизменяемый срез пути, аналогичный str:
usestd::path::Path;fnmain(){letpath=Path::new("/home/alex/example.txt");// ссылка на путь
println!("Extension: {:?}",path.extension());// Some("txt")
println!("File name: {:?}",path.file_name());// Some("example.txt")
println!("File exists? {}",path.exists());// false
}
Методы вроде extension() и file_name() возвращают Option, т.к. путь может не содержать этих элементов.
PathBuf — это изменяемая версия пути, аналогичная String:
usestd::path::PathBuf;fnmain(){letmutpath=PathBuf::from("/home/alex/");// push добавляет компонент к пути с учетом разделителей ОС:
path.push("test.txt");println!("Path: {:?}",path);// /home/alex/test.txt
}
Используйте Path для проверки существующих путей, а PathBuf — для построения новых.
Чтение файла по имени из строки или PathBuf
Объект PathBuf реализует AsRef trait, это даёт возможность универсально передавать его:
usestd::fs;usestd::path::Path;// Функция забирает как строку, так и Path объект
fnread_file<P: AsRef<Path>>(path: P)-> String{// .as_ref() переводит полученное в &Path
fs::read_to_string(path.as_ref()).unwrap()}fnmain(){// оба работают:
letcontent1=read_file("hello.txt");// &str
letcontent2=read_file(Path::new("world.txt"));// &Path
}
В Rust есть управление потоком программы через конструкции IF, ELSE IF, ELSE:
lettest_number=6;iftest_number%4==0{println!("Divisible by 4");}elseiftest_number%3==0{// Проверка останавливается на первом
println!("Divisible by 3");// выполнимом условии, дальнейшие проверки
}elseiftest_number%2==0{// пропускаются.
println!("Divisible by 2");}else{println!("Number is not divisible by 4, 3 or 2");}
Конструкция IF является выражением (expression) и возвращает значение:
letcondition=true;letnumber=ifcondition{"aa"}else{"ab"};// присваивание результата IF
println!("Number is {number}");
Используйте if как выражение для компактности. Для сложных случаев лучше переходить к match.
IF LET
В отличие от match, в котором нужно обязательно перебрать все переданные варианты, if let позволяет обработать лишь один вариант, отбросив все остальные (не реагируя на них). В то время как match можно сравнить с вендинговым автоматом, if let - это фильтр.
Применение - проверить, что в Option есть значение:
letusername: Option<String>=Some("cool_teen".to_string());// длинный путь с match:
matchusername{Some(name)=>println!("Hello {}!",name),None=>{},// ненужный пустой блок 🤮
}// короткий путь:
ifletSome(name)=username{println!("Hello {}!",name);// работает только если Some существует
}// Не надо обрабатыватьNone!
Применение - проверить, что Result успешен:
letfile_result: Result<File,std::io::Error>=File::open("config.txt");// нас волнует только, если сработало:
ifletOk(file)=file_result{println!("File opened successfully!");// далее использовать 'file'
}
if let работает с любым паттерном:
// проверить точное значение:
iflet42=answer{println!("The meaning of life!");}// сравнить 1ый элемент кортежа
letcoordinates=(10,20);iflet(x,20)=coordinates{println!("Y is 20, X is {}",x);}// проверить несколько условий с помощью `|` (or)
enumStatus{Active,Pending,Inactive,}letstate=Status::Pending;ifletStatus::Active|Status::Pending=state{println!("User can log in");}
❗Эффективно применять if let тогда, когда условие “все остальные случаи” _ => {} в конструкции match пустое.
IF LET ELSE
Можно комбинировать if let и else для обработки “противоположного” результата:
ifletSome(score)=high_score{println!("New high score: {}!",score);}else{println!("No high score yet. Play a game!");}
LOOPS
Три варианта организовать цикл: через операторы loop, while, for.
Loop
loop - организация вечных циклов. Конструкция loop является выражением (expression), поэтому возвращает значение.
letmutcounter=0;letresult=loop{counter+=1;ifcounter==10{breakcounter*2;// выход из вечного цикла
}};// ";" нужно, т.к. было выражение
println!("The result is {result}");
Если делать вложенные циклы, то можно помечать их меткой, чтобы выходить с break на нужный уровень.
for - цикл по множествам элементов. В том числе можно задать подмножество чисел.
foriin(1..10).rev(){// .rev() - выдача подмножества в обратную сторону
println!("Value: {i}");}println!("ЗАПУСК!");
Функции
Вызов функции: указатель на начало функции кладётся на стек (под каждую функцию выделяется место на стеке - stack frame). Стек имеет ограничения по размеру. Можно это проверить, написав пример:
fnmain(){a();}fna(){println!("Calling B!");b();}fnb(){println!("Calling C!");c();}fnc(){println!("Calling A!");a();// бесконечный цикл вызовов
}
При запуске программы она бесконечно вызывает функции, пока не исчерпает место в stack frame функции main, и тогда программа падает с ошибкой:
thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Abort trap: 6
Передача функций как аргументов
Тип функции — её сигнатура.
// Функция apply принимает 2 параметра:
// 1. x типа i32 (32-битное целое число)
// 2. f - функцию с сигнатурой fn(i32) -> i32,
// т.е. которая принимает i32 и возвращает i32
// Сама apply возвращает i32
fnapply(x: i32,f: fn(i32)-> i32)-> i32{// Применяем переданную функцию f к аргументу x
// и возвращаем результат
f(x)}// Функция принимает x типа i32 и возвращает x * 2 тоже типа i32
fndouble(x: i32)-> i32{x*2// удваивает входное число
}fnmain(){// Вызываем apply с двумя аргументами:
// 1. Число 5
// 2. Функция double
// double будет применена к 5, то есть 5 * 2 = 10
letresult=apply(5,double);println!("Удвоенное: {}",result);// Вывод: Удвоенное: 10
}
Exit
В Rust для “нормального” завершения программы используется функция std::process::exit из стандартной библиотеки. Она завершает программу немедленно с заданным кодом возврата, не вызывая панику.
usestd::process;fnmain(){println!("До выхода");process::exit(0);// Завершаем программу с кодом 0 (успех)
println!("Это не выведется");}
process::exit(code: i32) принимает код возврата (i32), который передаётся операционной системе.
Код 0 обычно означает “успешное завершение”, а ненулевые значения (напримр, 1) — “ошибка”.
После вызова exit программа завершается мгновенно — никакой код ниже не выполняется, даже если есть незавершённые операции.
Return
Можно использовать return в main. Это не то же самое, что exit (не мгновенно прерывает), но подходит для естественного завершения:
fnmain(){println!("Работаем...");return;// Завершаем с кодом 0
println!("Это не выведется");}
Result
Можно возвращать Result из main, чтобы указать успех или ошибку:
fnmain()-> Result<(),i32>{println!("Работаем...");Ok(());// Успех, код 0
// или Err(1) для ошибки с кодом 1
}
Если нужно мгновенное завершение с кодом возврата — используй std::process::exit;
Если надо завершить программу “правильно” в конце main — просто return или Result;
panic! - для случаев, когда программа должна “упасть” из-за ошибки.
Generics нужны для того, чтобы не повторять один и тот-же код несколько раз для разных типов данных. Это один из трёх основных способов экономии на повторении кода наравне с макросами и интерфейсами traits.
Пример: нужно сортировать 2 набора данных - массив чисел, массив символов.
Вместо того, чтобы писать 2 почти идентичные функции под 2 типа данных, можно объединить оба типа в 1 функцию, указав “неопределённый тип”.
Note
Нужно учесть, что не все типы данных имеют возможность сравнения объектов между собой, возможность выстроить их в порядок (Order). Поэтому в примере ниже надо дополнить тип в заголовке интерфейсом-свойством (trait) порядка.
fnlargest_universal<T: std::cmp::PartialOrd>(list: &[T])-> &T{// <T> = неопределённый тип, со свойством упорядочивания PartialOrd
letmutlargest=&list[0];foriteminlist{iflargest<item{largest=item;}}largest}fnmain(){letnum_list=vec![11,6,33,56,13];//упорядочить числа
println!("Largest number: {}",largest_universal(&num_list));letchar_list=vec!['y','m','a','q'];//упорядочить символы в той же функции
println!("Largest char: {}",largest_universal(&char_list));}
Структуры с неопределёнными типами
Можно создавать структуры, тип данных которых заранее неопределён. Причём в одной структуре можно сделать несколько разных типов.
structPoint<T,U>{// <T> и <U> - 2 разных типа
x: T,y: U,}letinteger=Point{x:5,y:6};// в оба поля пишем числа типа i32
letfloat_int=Point{x:1,y:4.2};// в поля пишем разные типы i32 и f32
Аналогично неопределённые типы можно делать в перечислениях:
enumOption<T>{Some(T),// <T> - возможное значение любого типа, которое может быть или нет
None,}enumResult<T,E>{Ok(T),// T - тип результата успешной операции
Err(E),// Е - тип ошибки
}
Методы со структурами с неопределёнными типами
Можно прописать методы над данными неопределённого типа. Важно в заголовке метода указывать при этом неопределённый тип, чтобы помочь компилятору отделить его от обычного типа данных. Можно при этом сделать реализацию для конкретных типов данных, например, расчёт расстояния до точки от центра координат будет работать только для чисел с плавающей точкой. Для других типов данных метод расчёта работать не будет:
structPoint<T>{x: T,y: T,}impl<T>Point<T>{// указываем неопределённый тип в заголовке
fnshow_x(&self)-> &T{// метод возвращает поле данных
&self.x}}implPoint<f32>{// указываем конкретный тип float в методе,
// чтобы только для него реализовать расчёт. Для не-float метод не будет работать
fndistance_from_origin(&self)-> f32{(self.x.powi(2)+self.y.powi(2)).sqrt()}}fnmain(){letp=Point{x:5,y:6};println!("P.x = {}",p.show_x());// вызов метода для экземпляра p
}
Tip
Код с неопределёнными типами не замедляет производительность, потому что компилятор проходит и подставляет конкретные типы везде, где видит generics (“monomorphization” of code).
Итераторы позволяют выполнять действия по очереди над цепочкой данных. Итератор берёт каждый объект цепочки и проверяет, не последний или он. Итераторы в Rust - ленивые, т.е не отрабатывают, пока не будет вызван метод, который их поглощает.
iter()
Возвращает Iterator<Item = &T>, забирает на себя массив без изменений (immutable), поэтому исходный массив далее доступен для других действий. Применять для задач чтения.
letv1=[1,2,3];letv1_iter=v1.iter();forvalinv1_iter{// iterator consume by for cycle
println!("Got: {val}");}println!("{v1:?}");// v1 доступен после итератора
into_iter()
Возвращает Iterator<Item = T>, делает move массиву, после применения массив использовать нельзя. Применять для задач изменений с исходным массивом.
letv1=[1,2,3];fornuminv1.into_iter(){println!("{}",num);}// num is i32 (owned value)
// println!("{v1:?}"); // ❌ ошибка, v1 перемещён
Когда ты пишешь for x in коллекция, Rust автоматически вызывает into_iter(), потому что это “по умолчанию” забирает коллекцию. Если хочешь оставить коллекцию, явно используй for x in коллекция.iter().
iter_mut()
Возвращает Iterator<Item = &mut T>, забирает на себя массив с возможностью его менять прямо на месте. Исходный массив далее доступен для других действий.
letmutvec=vec![1,2,3];// Создает iterator по mutable rссылкам (&mut T)
fornuminvec.iter_mut(){*num*=2;}// можно менять элементы
println!("Modified: {:?}",vec);// [2, 4, 6]
Цикл for с iter(), into_iter(), iter_mut()
Синтаксический сахар:
letvec=vec![1,2,3];forxinvec.iter(){/* x: &i32 */}forxin&vec{/* x: &i32 */}// равно iter()
forxinvec.into_iter(){/* x: i32 */}forxinvec{/* x: i32 */}// равно into_iter()
forxinvec.iter_mut(){/* x: &mut i32 */}forxin&mutvec{/* x: &mut i32 */}// равно iter_mut()
Характеристика
iter
into_iter
iter_mut
Что возвращает
Ссылки (<T>)
Сами значения (T)
Изменяемые ссылки (<mut T>)
Можно ли менять элементы
Нет, только смотреть
Да, но коллекция уже твоя
Да, через ссылки
Что с коллекцией
Остаётся живой
“Исчезает” (перемещается)
Остаётся живой
Тип итератора
Итератор по ссылкам
Итератор по значениям
Итератор по изменяемым ссылкам
Когда использовать
Хочу посмотреть элементы
Хочу забрать элементы
Хочу изменить элементы на месте
Требуется ли mut для коллекции
Нет
Нет (но коллекция уходит)
Да, коллекция должна быть mut
Пример кода
for x in vec.iter()
for x in vec.into_iter()
for x in vec.iter_mut()
Пример результата
x — ссылка, vec жив
x — значение, vec мёртв
x — изменяемая ссылка, vec жив
repeat, repeat_n, take, skip
repeat - создаёт строку, повторяя заданный символ N раз (N as usize).
Например, вывести квадрат из символов “+” размера n (через клонирование):
skip(n) создаёт новый итератор, в котором пропускает заданное число n элементов исходного итератора, а остальные возвращает. С помощью него удобно обрабатывать данные, пропустив заголовок, либо параметры введённой команды с пропуском имени самой команды:
usestd::env;fnmain(){// env::args() = итератор на Strings с аргументами.
// Вызываем skip(1) для пропуска пути к команде.
letarguments=env::args().skip(1);// Клонируем clone() для проверки количества аргументом
// сохраняя исходный итератор
ifarguments.clone().count()==0{println!("No arguments provided other than the program name.");}else{println!("Processing arguments:");forarginarguments{println!(" Argument: {}",arg);}}}
Вместе take() и skip() комбинируются, чтобы получить средние значения в массиве:
letdata=[10,20,30,40,50,60,70,80];letiter=data.iter();letmiddle_elements: Vec<_>=iter.skip(2)// пропуск 2 элементов
.take(3)// взять 3 элемента = 30,40,50
.collect();// поместить результат в Vec.
println!("Middle elements: {:?}",middle_elements);
Отличие .map() и .flat_map()
Обе функции раскрывают итератор вектора, однако, с разным результатом:
peekable() превращает обычный итератор в итератор, который позволяет посмотреть на следующий элемент, не продвигая итератор вперед.
letmutnumbers=vec![1,2,3].into_iter().peekable();// peek() - смотрим на следующий элемент, но не двигаем итератор
println!("Смотрим: {:?}",numbers.peek());// Some(1)
println!("Смотрим ещё раз: {:?}",numbers.peek());// Все ещё Some(1)
println!("Берём: {:?}",numbers.next());// Some(1)
println!("Теперь смотрим: {:?}",numbers.peek());// Some(2)
println!("Берём: {:?}",numbers.next());// Some(2)
println!("Берём: {:?}",numbers.next());// Some(3)
println!("Теперь смотрим: {:?}",numbers.peek());// None
next_if() - взять элемент только если условие выполняется
letmutnums=vec![1,3,5,2,4].into_iter().peekable();// Берём все нечетные числа подряд
whileletSome(num)=nums.next_if(|x|*x%2==1){println!("{}",num);}// 1, 3, 5
Пример
Взятие символов в строке так, чтобы разбивать строку на пары из двух символов. Если строка содержит нечетное количество символов, то недостающий второй символ последней пары должен быть заменен на символ подчеркивания (’_’).
fnsolution(s: &str)-> Vec<String>{letmutresult=Vec::new();letmutchars=s.chars().peekable();// Делаем итератор peekable
whileletSome(c1)=chars.next(){// Берём первый символ пары
letc2=chars.next().unwrap_or('_');// Второй или '_' если нет
result.push(format!("{}{}",c1,c2));// Собираем пару
}result}
Метки времени жизни сообщают компилятору, как долго ссылка действительна.
fnfoo<'input>(bar: &'astr){// ...
}
Имя меток может быть абсолютно любым.
Правила элизии (неявного использования) меток времени жизни
Каждая входная ссылка на функцию получает отдельное время жизни;
Если есть ровно одно входное время жизни, оно применяется ко всем выходным ссылкам;
Если есть несколько входных времен жизни, но одно из них — &self или &mut self, то время жизни self применяется ко всем выходным ссылкам.
Это означает, что вам нужно явно указывать времена жизни только в том случае, если у вас более одного входного времени жизни и ни одно из них не является &self или &mut self.
Заражение кода явными метками времени жизни
Если добавить метки времени жизни в структуру, например,
structFoo{bar: String}
оптимизировать в &str для предотвращения выделения памяти и превратить в:
structFoo<'a>{bar: &'astr}
то теперь нужно добавлять метки во все методы, которые используют эту структуру:
В данном коде компилятор не может предсказать, что данные, на которые указывают ссылки x и y, будут живыми к моменту выхода из функции (выдаст ошибку). При этом та или иная ссылка нужны, в зависимости от выполнения условия (та, где данные длиннее), а понять какая можно будет лишь во время запуска приложения. Поэтому необходимо уверить компилятор, что обе ссылки останутся рабочими до конца работы функции:
Обход применения явных меток времени с помощью умных указателей Rc и Arc
Можно использовать умные указатели, Rc (подсчет ссылок) или Arc (атомарный подсчет ссылок), чтобы разделить владение данными. Таким образом, не нужно беспокоиться о явных временах жизни, сохраняя при этом затраты на клонирование данных близкими к нулю:
// Платим за выделение памяти лишь 1 раз
lethello=Rc::new("Hello".to_string());// Дешёвая операция
lethello2=hello.clone();
Пример с x и y можно переписать с применением Rc для примера:
Причины для явного использования меток времени жизни
Причин в современном Rust всего две:
Существует узкое место в коде по производительности: Вы обнаружили медленный участок часто используемого кода, профилировали его и определили, что узкое место действительно вызвано выделением памяти. В этом случае имеет смысл использовать время жизни, чтобы избежать выделения памяти. (Альтернатива — реорганизовать ваш код, чтобы использовать лучший алгоритм и избежать «горячего пути» в первую очередь.)
Код библиотек, от которого зависит код Вашего приложения, требует аннотаций времени жизни (пример - html5ever). Здесь мало что можно сделать, кроме как искать альтернативы, которые не требуют меток времени жизни.
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 и других языков программирования.
Для примера, возьмём библиотеку, обеспечивающую работу ресторана. Ресторан делится на части front - обслуживание посетителей, и back - кухня, мойка, бухгалтерия.
Объявление модуля публичным с помощью pub не делает его функции публичными. Нужно указывать публичность каждой функции по отдельности:
modfront_of_house{pubmodhosting{// модуль публичен, чтобы к нему обращаться
pubfnadd_to_waitlist(){}// функция явно публична
// несмотря на публичность модуля, к функции обратиться нельзя
// если она непублична
}}pubfneat_at_restaurant(){// Абсолютный путь через корень - ключевое слово crate
crate::front_of_house::hosting::add_to_waitlist();// Относительный путь
front_of_house::hosting::add_to_waitlist();}
Обращние к функции выше уровнем
Относительный вызов функции можно сделать через super (аналог “..” в файловой системе):
fndeliver_order(){}modback_of_house{fnfix_incorrect_order(){cook_order();super::deliver_order();// вызов функции в родительском модуле
}fncook_order(){}}
Обращение к структурам и перечислениям
Поля структур приватны по умолчанию. Обозначение структуры публичной с pub не делает её поля публичными - каждое поле нужно делать публичным по отдельности.
modback_of_house{pubstructBreakfast{// структура обозначена как публичная
pubtoast: String,// поле обозначено публичным
seasonal_fruit: String,// поле осталось приватным
}implBreakfast{pubfnsummer(toast: &str)-> Breakfast{Breakfast{toast: String::from(toast),seasonal_fruit: String::from("peaches"),}}}}pubfneat_at_restaurant(){// Обращение к функции. Без функции к структуре с приватным полем
// не получится обратиться:
letmutmeal=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, чтобы видеть все его поля.
С помощью as можно создать ярлык на объект в строке с use. Особенно это удобно в случае, когда нужно обратиться к одинаковым по имени объектам в разных модулях:
usestd::fmt::Result;usestd::io::ResultasIoResult;// IoResult - ярлык на тип Result в модуле io
fnfunction1()-> Result{// ... }
fnfunction2()-> IoResult<()>{// ... }
Ре-экспорт объектов
При обращении к объекту с помощью use, сам ярлык этот становится приватным - через него могут обращаться только функции текущего scope. Для того, чтобы из других модулей функции могли тоже обратиться через этот ярлык, нужно сделать его публичным:
modfront_of_house{pubmodhosting{pubfnadd_to_waitlist(){}}}pubusecrate::front_of_house::hosting;// ре-экспорт объекта
pubfneat_at_restaurant(){hosting::add_to_waitlist();// обращение к функции через ярлык
}
Работа с внешними библиотеками
Внешние библиотеки включаются в файл Cargo.toml. Далее, публичные объекты из них заносятся в scope с помощью use.
Если нужно внести несколько объектов из одной библиотеки, то можно сократить количество use:
//use std::cmp::Ordering;
//use std::io;
usestd::{cmp::Ordering,io};// список объектов от общего корня
//use std::io;
//use std::io::Write;
usestd::io{self,Write};// включение самого общего корня в scope
usestd::collections::*;// включение всех публичных объектов по пути
Warning
Следует быть осторожным с оператором glob - *, так как про внесённые с его помощью объекты сложно сказать, где именно они были определены.
Объявленная переменная, обеспеченная памятью кучи (heap) - общей памятью (не стека!) всегда имеет владельца. При передаче такой переменной в другую переменную, либо в функцию, происходит перемещение указателя на переменную = смена владельца. После перемещения, нельзя обращаться к исходной переменной.
lets1=String::from("hello");// строка в куче создана из литералов в стеке
lets2=s1;// перемещение
println!("{}, world!",s1);// ошибка! Вызов перемещённой переменной
Решения
Можно сделать явный клон переменной со значением;
lets1=String::from("hello");lets2=s1.clone();// полный клон. Медленно и затратно,
println!("s1 = {}, s2 = {}",s1,s2);// но нет передачи владения
Передавать ссылку на указатель. Ссылка на указатель - ‘&’, раскрыть ссылку на указатель - ‘*’.
fnmain(){lets1=String::from("hello");letlen=calculate_length(&s1);// передача ссылки на указатель
println!("The length of '{}' is {}.",s1,len);}fncalculate_length(s: &String)-> usize{// приём ссылки на указатель
s.len()}
Ссылки (References)
Для внесения изменений по ссылке на указатель, нужно указать это явно через ‘mut’.
fnmain(){letmuts=String::with_capacity(32);// объявить размер блока данных заранее, чтобы потом не довыделять при закидывании данных в строку = быстрее
change(&muts);// передача изменяемой ссылки
}fnchange(some_string: &mutString){// приём изменяемой ссылки на указатель
some_string.push_str("hello, world");}
Tip
Правила:
В области жизни может быть лишь одна изменяемая ссылка на указатель (нельзя одновременно нескольким потокам писать в одну область памяти);
Если есть изменяемая ссылка на указатель переменной, не может быть неизменяемых ссылок на указатель этой же переменной (иначе можно перезаписать данные в процессе их же чтения);
Если ссылка на указатель переменной неизменяемая, можно делать сколько угодно неизменяемых ссылок на указатель (можно вместе читать одни и те же данные);
Конец жизни ссылки определяется её последним использованием. Можно объявлять новую ссылку на указатель, если последняя изменяемая ссылка по ходу программы более не вызывается.
Кратко правила зовутся Aliasing XOR Mutability:
&T-> &sharedT&mutT-> &uniqueT
letmuts=String::from("hello");{letr1=&muts;}// r1 вышла из области жизни, поэтому можно объявить новую ссылку на указатель.
letr2=&muts;
Отображение ссылок
Для показа адресов ссылок можно использовать форматирование {:p}:
fnmain(){leta=10;letb=&a;letc=&b;// двойная ссылка. Разыменовывание через **b
letd=b;println!("{:p}\n{:p}\n{:p}\n{:p}\n",&a,b,c,d);// для отображения указателя на само число a, нужно указать &a
}
. 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. При этом если строка начинается с заглавной буквы, то первое слово новой строки тоже с неё начинается:
Задача разбить переход от нижнего регистра к верхнему в camelCase пробелом:
useregex::Regex;fnsolution(s: &str)-> String{letre=Regex::new(r"[A-Z]").unwrap();// находим заглавную букву
re.replace_all(s," $0").to_string()// добавляем перед ней пробел
// $0 означает весь найденный текст
}// camelCasingTest -> camel Casing Test
// альтернативный вариант:
fnsolution(s: &str)-> String{Regex::new(r"([a-z])([A-Z])").unwrap().replace_all(s,"$1 $2").to_string()}// '$1 и $2' - найденные группы символов
Существует две основные категории программных бенчмарков: микро-бенчмарки и макро-бенчмарки. Микро-бенчмарки работают на уровне, аналогичном модульным тестам. Макро-бенчмарки работают на уровне, аналогичном интеграционным тестам.
В целом, лучше всего тестировать на максимально низком уровне абстракции. В случае бенчмарков это делает их более простыми в поддержке и помогает уменьшить количество шума в измерениях. Однако, как и наличие некоторых сквозных тестов может быть очень полезным для проверки того, что вся система работает как ожидается, так и наличие макро-бенчмарков может быть очень полезным для обеспечения того, чтобы критические пути в вашем программном обеспечении оставались производительными.
std::time для быстрого замера
usestd::time::Instant;fnarray_diff<T: PartialEq>(a: Vec<T>,b: Vec<T>)-> Vec<T>{a.into_iter().filter(|x|!b.contains(x)).collect()}fnarray_diff2<T: PartialEq>(muta: Vec<T>,b: Vec<T>)-> Vec<T>{a.retain(|x|!b.contains(x));a}fnmain(){letstart=Instant::now();// начало замера 1
println!("{:?}",array_diff(vec![1,2,2],vec![1]));letduration_a=start.elapsed();// конец
letstart=Instant::now();// начало замера 2
println!("{:?}",array_diff2(vec![1,2,2],vec![1]));letduration_b=start.elapsed();// конец
println!("Замеры: 1) {:?}; 2) {:?}",duration_a,duration_b);}
Для более точного замера следует собирать код с cargo run --release ключом.
Далее в проекте создать папку и файл benches\my_benchmark.rs:
usecriterion::{Criterion,criterion_group,criterion_main};fnarray_diff<T: PartialEq>(a: Vec<T>,b: Vec<T>)-> Vec<T>{a.into_iter().filter(|x|!b.contains(x)).collect()}// функция 1 для проверки
fnarray_diff2<T: PartialEq>(muta: Vec<T>,b: Vec<T>)-> Vec<T>{a.retain(|x|!b.contains(x));a}// функция 2 для проверки
fnbenchmark_functions(c: &mutCriterion){letmutgroup=c.benchmark_group("Function Comparison");group.bench_function("array_diff",|b|{b.iter(||{std::hint::black_box(array_diff(vec![1,2,2],vec![1]));})// black_box блокирует оптимизации компилятора
});group.bench_function("array_diff2",|b|{b.iter(||{std::hint::black_box(array_diff2(vec![1,2,2],vec![1]));})});group.finish();}criterion_group!(benches,benchmark_functions);criterion_main!(benches);
Для запуска тестов необходима команда cargo bench. Результаты замеров сохраняются и сравниваются с новыми результатами после внесения изменений.
flamegraph - визуализация стека вызовов
flamegraph — это инструмент для создания интерактивных SVG-диаграмм, которые визуализируют стек вызовов и время, затраченное в каждой функции. Он строится на основе данных от perf (xctrace в macOS) и помогает быстро понять, где сосредоточена нагрузка.
Установка:
cargo install flamegraph
Для того, чтобы видеть заголовки запускаемых функций (не их хэши), нужно в файле cargo.toml добавить профиль разработки. При этом профилирование скорости нужно проводить в режиме release со всеми оптимизациями, поэтому следует сделать отдельный профиль под release, но с debug-символами:
[profile.perfmon]inherits="release"debug=true
Использование:
# принудительно указать свой профиль perfmon (по умолчанию --release)cargo flamegraph --profile perfmon
# данные по выбранной функцииcargo flamegraph --profile perfmon --flamechart-opts "--grep functionName"# Профилирование unit-тестов# Разделитель `--` нужен, если `--unit-test` последний флаг.cargo flamegraph --unit-test -- test::in::package::with::single::crate
cargo flamegraph --unit-test crate_name -- test::in::package::with::multiple:crate
# Профилирование интеграционных тестовcargo flamegraph --test test_name
# Профилирование примера из папки examples в workspacecargo flamegraph --example some_example --features some_features
String printing, splitting, joining and formatting.
В Rust строки хранятся в формате UTF-8, где каждый символ может занимать от 1 до 4 байт. Поэтому индексация идёт не по символам напрямую, а по байтам или с учётом корректных границ символов (Unicode scalar values).
Пример строкового литерала:
lets="Hello, Rust!";// Обычная строка, строковый литерал
letraw=r#"Сырой текст с "кавычками""#;// Сырая строка без экранирования
String
Тип данных с владельцем. Имеет изменяемый размер, неизвестный в момент компиляции. Представляет собой векторную структуру:
pubstructString{vec: Vec<u8>;}// для ASCII символов
Поскольку структура содержит Vec, это значит, что есть указатель на массив памяти, размер строки size структуры и ёмкость capacity (сколько можно добавить к строке перед дополнительным выделением памяти под строку).
Работа с String: если у вас String, нужно сначала получить срез &str с помощью &
&str
Ссылка на часть, slice от String (куча), str (стек) или статической константы. Не имеет владельца, размер фиксирован, известен в момент компиляции.
&String можно неявно превращать в &str;
&str нельзя неявно превращать в &String.
fnmain(){lets="hello_world";letmutmut_string=String::from("hello");success(&mutable_string);fail(s);}fnsuccess(data: &str){// неявный перевод &String -> &str
println!("{}",data);}fnfail(data: &String){// ОШИБКА - expected &String, but found &str
println!("{}",data);}
Warning
Пока существует &str её в области жизни нельзя менять содержимое памяти, на которое она ссылается, даже владельцем строки.
&String
Ссылка на String. Не имеет владельца, размер фиксирован, известен в момент компиляции.
fnchange(mystring: &mutString){if!mystring.ends_with("s"){mystring.push_str("s");// добавляем "s" в конец исходной строки
}
str
Набор символов (литералов), размещённых на стеке. Не имеет владельца, размер фиксирован, известен в момент компиляции. Можно превращать str в String через признак from:
Если структуре надо владеть своими данными - использовать String. Если нет, можно использовать &str, но нужно указать время жизни (lifetime), чтобы структура не пережила взятую ей строку:
structOwned{bla: String,}structBorrowed<'a>{bla: &'astr,}fnmain(){leto=Owned{bla: String::from("bla"),};letb=create_something(&o.bla);}fncreate_something(other_bla: &str)-> Borrowed{letb=Borrowed{bla: other_bla};b// при возврате Borrowed, переменная всё ещё в области действия!
}
Разница между to_lowercase(), to_ascii_lowercase(), make_ascii_lowercase() и upper-аналогов
Функция
to_uppercase()
to_ascii_uppercase()
make_ascii_uppercase()
Возвращает
Новая String
Новая String
() (меняет входную строку)
Текст
Unicode
только ASCII
только ASCII
Память
Новая строка
Новая строка
Не выделяет память
Speed
Медленно
Быстро
Быстрее всех
Особенности
Обработка спец-символов (ß→SS)
Нет, пропускает спец-символы
Нет, пропускает спец-символы
Передача любых строк в функцию с переводом по ссылке
Можно воспользоваться встроенным типажом AsRef для перевода ссылок строки всех видов (String, &str, Box<str> и т.д.) в &str:
// AsRef<str> is a trait that allows cheap reference-to-reference conversion — here it guarantees we can get a `&str` from a value of type `N`:
fnhello<N: AsRef<str>>(name: N){println!("Hello, {}!",name.as_ref());// .as_ref() is from the `AsRef` trait — it converts `&name` into a `&str`.
}fnmain(){letstrings: Vec<String>=vec!["Alice".into(),"Bob".into()];// "Alice".into() равен String::from("Alice")
strings.into_iter().for_each(hello);hello("Charlie");hello(&String::from("Debbie"));hello(String::from("Evan").as_str());}
println!("{}",'a'asu8);// перевод символа в код ASCII
println!("{}",97aschar);// число как символ
// перевод кода UTF-8 в символ (не все символы можно перевести):
println!("{:?}",std::char::from_u32(98).unwrap());
Первая и последняя буква в строке
Чтобы проверить или изменить 1-ую букву в строке (в том числе иероглиф или иной вариант алфавита), нужно строку переделать в вектор из букв:
Есть функции sort() и sort_unstable() (работает быстрее, но равные элементы может перемешивать) для вектора символов Vec<char>. Обе функции делают сортировку на месте, возвращают ().
letinput="zxyabc";// сортировка Unicode тоже работает
letmutchar2vec_iter=input.chars().collect::<Vec<char>>();char2vec_iter.sort_unstable();// сортировка на месте
// char2vec_iter.sort_by(|a,b| b.cmp(a)); // реверс-сортировка
// собираем назад String из Vec<char>
letsorted_string: String=char2vec_iter.into_iter().collect();println!("{}",sorted_string);// "abcxyz"
chunks() и chunks_exact() разбиение строк на части
Аналогично массивам, строки можно разбить на части:
lettext="ПриветМир";letchars=text.chars().collect::<Vec<char>>();println!("Разбиваем строку на части по 3 символа:");forchunkinchars.chunks(3){lets: String=chunk.iter().collect();println!("{}",s);// При
// вет
// Мир
Строковые срезы
Срезы позволяют взять часть строки, указав диапазон байтовых индексов.
Синтаксис: &string[start..end] (где start — начало, end — конец, не включительно).
Особенность: нужно вручную следить за границами байтов, иначе будет паника при попытке разрезать строку посреди многобайтового символа.
lettext="Hello, world!";// тип &str
lettext=String::from("Hello, world!");// String
// Если у вас String, а не &str, нужно взять срез с помощью &:
letfirst_three=&text[0..3];// первые 3 символа
letlast_five=&text[text.len()-5..];// последние 5 символов
println!("{}",first_three);// "Hel"
println!("{}",last_five);// "orld!"
Отрицательные индексы в Rust не поддерживаются, поэтому нужно вычислять вручную. Если указать только начало ([start..]), берётся всё до конца:
Если строка содержит многобайтовые символы (например, кириллицу или эмодзи), простые байтовые срезы могут вызвать панику. Для работы с символами (Unicode scalar values) используйте метод .chars():
lettext="Привет, мир!";letchars: Vec=text.chars().collect();// преобразуем в вектор символов
letprivet: String=chars[0..6].iter().collect();// собираем первые 6 символов
println!("{}",privet);// "Привет"
Метод .chars() возвращает итератор по символам, а .collect() собирает их в нужный тип (например, String).
.get(start..end) для безопасного извлечения
Если нет уверенности в границах, и надо избежать паники, подойдёт метод .get() вместо прямого среза. Он возвращает Option<&str>:
Преимущество: проще для новичков, не нужно беспокоиться о байтовых границах.
Недостаток: добавляет внешнюю зависимость.
Примеры
Разворот букв в словах
Дана строка с пробелами между словами. Необходимо развернуть слова в строке наоборот, при этом сохранить пробелы.
fnreverse_words_split(str: &str)-> String{str.to_string().split(" ")// при разделении split() множественные пробелы сохраняются
.map(|x|x.chars().rev().collect::<String>())// разворот слов+сбор в строку
.collect::<Vec<String>>().// сбор всего в вектор
.join(" ")// превращение вектора в строку
}fnmain(){letword: &str="The quick brown fox jumps over the lazy dog.";println!("{}",reverse_words_split(&word));}// ehT kciuq nworb xof spmuj revo eht yzal .god
lets=String::from("Hello World!");letword=first_word(&s);println!("The first word is: {}",word);}fnfirst_word(s: &String)-> &str{// передача строки по ссылке
letword_count=s.as_bytes();for(i,&item)inword_count.iter().enumerate(){ifitem==b' '{return&s[..i];// возврат части строки как &str
}}&s[..]// обязательно указать возвращаемое значение, если условие в цикле выше ничего не вернёт (например, строка не содержит пробелов = вернуть всю строку)
‘Проход’ по строке итератором
Можно пройти по строке итератором chars() и его методами взятия N-го символа nth() спереди или nth_back() сзади:
letperson_name=String::from("Alice");println!("The last character of string is: {}",matchperson_name.chars().nth_back(0){// ищем 1-ый символ с конца строки
Some(i)=>i.to_string(),// если находим - превращаем в строку
None=>"Nothing found!".to_string(),// не находим = сообщаем
});
matches() и rmatches(),
Возвращают итератор с теми частями строки, которые совпадают с заданным шаблоном:
Возвращает Option<байт индекс 1го символа в строке слева-направо>, совпадающий с шаблоном. Либо возвращает None, если символ отсутствует в строке. rfind
fnduplicate_encode2(word: &str)-> String{lets=String::from(word).to_lowercase();s.chars().map(|c|ifs.find(c)==s.rfind(c){'('}else{')'}).collect()}// если у символа есть дубли => замена на '(',
// иначе на ')'
fnmain(){println!("{}",duplicate_encode("rEcede"));}
Use split(' '), filter out empty entries then re-join by space:
s.trim().split(' ').filter(|s|!s.is_empty()).collect::<Vec<_>>().join(" ")// Using itertools:
useitertools::Itertools;s.trim().split(' ').filter(|s|!s.is_empty()).join(" ")// Using split_whitespace, allocating a vector & string
pubfntrim_whitespace_v1(s: &str)-> String{letwords: Vec<_>=s.split_whitespace().collect();words.join(" ")}
Озаглавить каждое слово в предложении
В заданной фразе озаглавить каждое слово. Если результат больше 140 символов или пустой, вернуть None:
fncapitalize_first_letter(s: &str)-> Option<String>{letres=s.split_whitespace().map(capital)// каждое слово передать в функцию capital()
.collect::<Vec<String>>()// собрать в вектор
.join(" ");// потому что вектор можно собрать в string с join()
ifres.len()<141||!res.is_empty(){// проверка длины
Some(res)}else{None}}fncapital(word: &str)-> String{letmutlword=word.to_ascii_lowercase();// изменить НА МЕСТЕ - прямо в этой строке (быстрее всего):
lword[0..1].make_ascii_uppercase();lword// вернуть итоговую строку
}
Макрос println! позволяет вывести строку в поток stdout;
// println!("Hello there!\n");
// раскрывается в такой код:
usestd::io::{self,Write};io::stdout().lock().write_all(b"Hello there!\n").unwrap();
Макрос dbg!() позволяет вывести переменные и структуры в поток stdout;
split()
Метод split: разбивает строку на части по указанному разделителю и возвращает итератор. Разделитель может быть символом, строкой или даже пробелом. В том числе можно делить по нескольким символам разом:
lettext=String::from("the_stealth-warrior");letparts=text.split(['-','_']).collect::<Vec<&str>>();// collect собирает в коллекцию типа вектор
forpartinparts{println!("{}",part);
Другие методы разбивки
split_whitespace()
Разбивает по любым пробельным символам (пробелы, табы, переносы строк).
Нахождение закономерностей в структурах со строками
В примере мы передаём вектор из строк. Далее, анализируем его по частям:
fnlikes(names: &[&str])-> String{matchnames{[]=>"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()),}}
Struct - комплексный изменяемый тип данных, размещается в куче (heap), содержит внутри себя разные типы данных. Он похож на кортеж (tuple), однако типы данных должны иметь явные именования.
structUser{active: bool,username: String,email: String,sign_in_count: u64,// запятая в конце обязательна
}
Можно передать struct в функцию или вернуть из функции:
fnmain(){// создаём изменяяемый объект по структуре данных выше
letmutuser1=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);}fncreate_user(email: String,username: String)-> User{// возврат из функции
User{active: true,username,email,// имена полей имеют с входными переменными, и можно не писать username: username, email: email.
sign_in_count: 1,}// return заменяется отсутствием знака ";"" как обычно
}
Updating Structs
Если нужно создать новую структуру по подобию старой, и большая часть полей у них похожи, то можно использовать синтаксический сахар:
letuser2=User{email: String::from("another@example.com"),// задать новое значение поля
..user1// взять остальные атрибуты из user1. Идёт последней записью
};
Tuple structs
Структуры такого вида похожи на кортежи, но имеют имя структуры и тип. Нужны, когда нужно выделить кортеж отдельным типом, либо когда надо дать общее имя кортежу. При этом отдельные поля не имеют имён.
Переменные red и origin разных типов. Функции, которые берут Color как параметр, не возьмут Point, несмотря на одинаковые типы внутри. Каждая структура = свой собственный тип. Разбор таких структур на элементы аналогичен кортежам.
let(x,y,z)=(origin.0,origin.1,origin.2);
Unit-like structs
Структуры без полей аналогичны кортежам без полей, только с именем.
structTestTrait;fnmain(){test=TestTrait;}
Такие структуры нужны для задания признаков (traits), когда в самой структуре данные хранить не нужно. Их смысл заключается в том, чтобы представлять типы или маркеры, которые используются для передачи информации о структуре программы или её логике на уровне типов, а не для хранения данных.
Иногда нужно реализовать трейт (интерфейс) для типа, который не требует хранения данных. Юнит-структуры идеально подходят для этого:
Здесь Empty не хранит данных, но реализует интерфейс Display.
Структурные признаки
Можно выводить информацию о содержимом полей структуры для анализа кода. Для этого нужно добавить над структурой пометку debug:
#[derive(Debug)]structRectangle{width: u32,height: u32,}fnmain(){letscale=2;letrect=Rectangle{width: dbg!(20*scale),// вывод поля структуры. dbg! возвращает назад
height: 10,// взятое значение, с ним далее можно работать
};println!("Rectangle content: {:?}",rect);// вывод содержимого структуры
dbg!(&rect);// ещё вариант вывода - в поток stderr. Функция dbg!
// забирает владение структурой, поэтому передача по ссылке
}
Структурные методы
Можно добавлять функции как методы, привязанные к структурам. Это позволяет организовать код более чётко - по объектам и действиям над ними.
Для внесения изменений в объект структуры, в блоке методов можно объявить &mut self, а для перемещения владения - просто self. Это нужно изредка при превращении self в другой вид объекта, с целью запретить вызов предыдущей версии объекта. Внутри impl используется &self, чтобы метод мог обращаться к полям текущего экземпляра структуры, не передавая его явно как аргумент. Блоков impl может быть несколько.
Асоциированные функции
В блоке методов impl можно добавлять функции, которые первым параметром не берут саму структуру self. Обычно используются как конструкторы (например, для создания нового экземпляра структуры) или для других операций, не требующих доступа к данным экземпляра. Они вызываются через синтаксис :: (например, Rectangle::square), а не через точку (.), как методы:
implRectangle{fnsquare(size: u32)-> Rectangle{Rectangle{width: size,height: size}}// Используется блок `impl Rectangle` — это реализация для структуры `Rectangle` (предполагается, что она определена где-то выше, например, как `struct Rectangle { width: u32, height: u32 }`).
}fnmain(){letsq=Rectangle::square(5);println!("Square area: {}",sq.area());// Square area: 25
}
Создание типа данных с проверками
Вместо проверять введённые данные на корректность внутри функций, можно объявить собственный тип данных, содержащий в себе все необходимые проверки. Например, объявим число от 1 до 100 для игры, где надо угадать число:
pubstructGuess{// объявили тип данных (публичный)
value: i32,// внутри число (приватное)
}implGuess{pubfnnew(value: i32)-> Guess{// метод new проверяет значение
ifvalue<1||value>100{// на заданные границы 1-100
panic!("Guess value must be between 1 and 100, got {}.",value);}Guess{value}// возврат нового типа данных
}.// метод getter для получения значения value:
pubfnvalue(&self)-> i32{self.value// он нужен, тк напрямую видеть value нельзя
}// Это приватная переменная в структуре.
}
Представьте игру в LEGO:
Без типоориентированной разработки: мы начинаем строить случайные стены и крышу из того, что под руку попало, и надеемся, что в конечном итоге соберём дом.
С типоориентированной разработкой: мы сначала проектируем и определяем конкретные кирпичики LEGO, которые понадобятся: их формы, размеры и точки соединения.
Ход мышления: «Мне нужен кирпичик, который является дверью, кирпичик, который гарантирует, что он может держать крышу, и кирпичик, который обязательно должен иметь окно». Как только у нас определены нужные кирпичики, сборка становится простой, а компилятор выступает в роли помощника, гарантирует, что каждый выбранный кирпичик идеально встанет на свое место.
В Rust типы — это не просто данные для хранения. Это инструмент для обеспечения корректности, безопасности и бизнес-логики во время компиляции.
❗Философия такова: «Сделайте недействительные состояния непредставимыми».
Если логика программы гласит, что пользователь не может существовать без адреса электронной почты, то тип Rust для User даже не должен позволять вам создать его без электронной почты.
Как начать
На примере системы учёта сдачи в аренду книг в библиотеке:
Шаг 1: Понять предметную область и определить «существительные»:
Прежде чем писать какой-либо код, подумайте о реальных концепциях в вашем проекте. Каковы основные «вещи»? Примеры:
Книга:Вещь с названием, автором и уникальным идентификатором. Пользователь:Человек, который может брать книги. Выдача:Действие, связывающее Пользователя и Книгу на определенный период.
Шаг 2: Моделирование «существительных» как структур
Начните с определения основных структур данных. На этом этапе важно определить данные, которые они содержат, а не поведение.
// Простые типы для начала:
structBook{id: u32,title: String,author: String,}structUser{id: u32,name: String,// Активен ли пользователь? Определим позднее.
}structCheckout{book_id: u32,user_id: u32,checkout_date: String,// Слишком размыто, нужно уточнение
}
Шаг 3: Кодирование ограничений с помощью новых типов
Первая проблема: id: u32 слишком гибок. Может ли Book быть создан с user_id по ошибке? - Да. Можете ли вы случайно передать ID Book там, где ожидается ID User? - Да.
На помощь идиома New Type: мы оборачиваем примитивные типы, чтобы придать им семантическое значение.
// Создать обёртки для различия ID между собой
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]// типовые traits для IDs
structBookId(u32);#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]structUserId(u32);// намного более чёткие структуры
structBook{id: BookId,// чёткий тип BookId
title: String,author: String,}structUser{id: UserId,// говорящий тип UserId
name: String,is_active: bool,// добавляем теперь активность пользователя
}structCheckout{book_id: BookId,// Компилятор теперь не даст перепутать IDs
user_id: UserId,checkout_date: String,// Всё ещё проблема
}
Компилятор теперь не позволяет писать checkout.user_id = BookId(5). Вы закодировали ограничение “ID пользователя должен быть UserId” непосредственно в систему типов.
Шаг 4: Используйте enum для моделирования состояний и альтернатив
Наш Checkout имеет String для даты. Это рискованно: что, если она пустая? Что, если она недействительная? Кроме того, книги имеют состояния. Книга может быть Available или CheckedOut. Ставим enum-типы: они позволяют определить набор возможных состояний.
// Тип для обозначения даты
usechrono::NaiveDate;// моделируем состояния книги
enumBookStatus{Available,CheckedOut{checkout: Checkout,// у взятой книги ЕСТЬ запись в реестре
},Lost,}// Book включает теперь status
structBook{id: BookId,title: String,author: String,status: BookStatus,// статус как чать типа
}// Checkout создаётся только при выписывании книги.
// Дата checkout_date теперь корректного типа, НЕ String.
structCheckout{user_id: UserId,checkout_date: NaiveDate,due_date: NaiveDate,}
С BookStatus невозможно иметь книгу, которая одновременно CheckedOut и Available. Система типов гарантирует, что книга находится в одном из трех определенных состояний.
Шаг 5: Используйте enum для результатов (тип Result)
Ваши функции должны использовать тип Result для кодирования возможности сбоя. Это заставляет вызывающую сторону обрабатывать ошибки.
Возвращаемый тип Result<Checkout, CheckoutError> говорит любому, кто использует эту функцию: “Эта операция может завершиться неудачей, и вот точные способы, которыми она может завершиться неудачей”. Компилятор заставляет их обрабатывать как случаи Ok, так и Err.
Шаг 6: Используйте трейты для общего поведения
Трейты определяют, что могут делать типы. Когда у вас есть свои типы, вы можете обнаружить, что они имеют общее поведение. Например, и Book, и User имеют идентификаторы. Вы могли бы определить трейт для этого:
// trait определяет общее поведение
traitHasId{fnid(&self)-> u32;// Но наши IDs = BookId/UserId, а не u32. Ещё лучше!
}// Или, с нашими newtypes:
traitHasTypedId<T>{fnid(&self)-> &T;}implHasTypedId<BookId>forBook{fnid(&self)-> &BookId{&self.id}}implHasTypedId<UserId>forUser{fnid(&self)-> &UserId{&self.id}}
Необязательно планировать все характеристики заранее, но обдумывание общих моделей поведения помогает разрабатывать гибкие, повторно используемые компоненты.
Короткий чеклист
Identify Nouns: List the core concepts (Book, User, Checkout).
Start with structs: Define the data these concepts hold. Don’t worry about behavior yet.
Replace Primitives with New Types: Wrap u32, String, etc., in struct wrappers (BookId, EmailAddress) to make the compiler enforce distinctions and constraints.
Model State with enums: If a concept can be in multiple, mutually exclusive states (Available vs. CheckedOut), use an enum. Aim to make invalid states unrepresentable.
Define Fallible Functions with Result: Before writing a function’s body, decide what its success and error types will be. This forces you to consider edge cases upfront.
Abstract with traits: Once types are stable, identify shared behaviors and define traits.
Придание дополнительного смысла типу переменной. Например, строка String может быть использована для чего угодно, а нам нужно конкретизировать:
// Обычный String
letname=String::from("john");// Newtypes - каждый тип уникален
structUsername(String);structEmail(String);structPassword(String);letuser=Username(String::from("john"));// Не перепутать типы
letemail=Email(String::from("john@example.com"));// Они разные!
У данного паттерна НЕТ стоимости в скорости или ресурсах приложения.
Применение
Защита от смешивания типов:
structMeters(f32);structKilometers(f32);fnrace_distance(distance: Meters){println!("Race is {} meters!",distance.0);}letm=Meters(100.0);letkm=Kilometers(0.1);race_distance(m);// ✅ Работает!
race_distance(km);// ❌ ОШИБКА! Нельзя передать КМ туда, где ждут М
pubstructCreditCardNumber(String);// клиенты видят этот метод
implCreditCardNumber{// Отдаём только безопасные методы
pubfnlast_four(&self)-> String{self.0[self.0.len()-4..].to_string()}// Внутренняя валидация происходит тут:
pubfnnew(number: String)-> Result<Self,String>{ifnumber.len()==16{Ok(CreditCardNumber(number))}else{Err("Must be 16 digits".to_string())}}}// Нельзя извне обратиться к сырой строке!
letcard=CreditCardNumber::new("1234567890123456".to_string()).unwrap();println!("{}",card.last_four());// "3456" ✅
// println!("{}", card.0); ❌ ОШИБКА! Приватное поле!
Когда использовать паттерн Newtype
Когда есть два значения одного типа, но означают разные вещи (IDs, измерения);
Когда надо добавить методы к типу, которым мы не владеем (например, обернуть u32, чтобы добавить is_even());
Когда надо применить правила валидации.
Когда НЕ использовать паттерн Newtype
Когда действительно нужны все методы исходного типа (вместо этого используйте псевдоним типа: type Age = u8).
Эта философия превращает подверженные ошибкам проверки в runtime в гарантии во время компиляции. То есть, входные данные в программе нельзя тащить дальше в код. Нужно их на входе проверять и отсеивать. Т.е. вопросы: “а вдруг там ничего нет void/none, а вдруг пользователь ввёл некорректные данные?” - надо решать сразу на входной функции чтения данных, и не тащить это по всей программе, везде делая реверанс в стиле “а если там в начале ничего не было, то… "
В Rust это можно и нужно зашивать в типы данных, которые гарантируют наличие контента.
Пример: непустой массив
Допустим, я создаю тип для дома, в котором будет массив комнат. Массив комнат в доме априори не может быть пустым - хотя бы одна комната должны быть!
Вариант 1 - валидация в конструкторе
pubstructHouse{rooms: Vec<Room>,}implHouse{/// Конструктор, возвращающий Result - не может создать пустой дом
pubfnnew(rooms: Vec<Room>)-> Result<Self,&'staticstr>{ifrooms.is_empty(){Err("House must have at least one room")}else{Ok(House{rooms})}}
usenonempty::NonEmpty;pubstructHouse{rooms: NonEmpty<Room>,// Гарантированно не пустой список
}implHouse{/// Конструктор, принимающий как минимум одну комнату
pubfnnew(first_room: Room,rest_rooms: Vec<Room>)-> Self{letmutrooms=NonEmpty::new(first_room);rooms.extend(rest_rooms);House{rooms}}
Вариант 3 - собственный тип
Создаём собственный тип данных, аналог NonEmpty:
usestd::ops::{Index,IndexMut};/// Вектор, который гарантированно содержит хотя бы один элемент
#[derive(Debug, Clone)]pubstructNonEmptyVec<T>{first: T,rest: Vec<T>,}impl<T>NonEmptyVec<T>{/// Создает новый NonEmptyVec с одним элементом
pubfnnew(first: T)-> Self{NonEmptyVec{first,rest: Vec::new(),}}/// Создает NonEmptyVec из Vec, возвращая None если вектор пуст
pubfnfrom_vec(mutvec: Vec<T>)-> Option<Self>{ifvec.is_empty(){None}else{letfirst=vec.remove(0);Some(NonEmptyVec{first,rest: vec,})}}/// Создает NonEmptyVec из Vec, паникуя если вектор пуст
pubfnfrom_vec_unchecked(vec: Vec<T>)-> Self{assert!(!vec.is_empty(),"Cannot create NonEmptyVec from empty vector");letmutvec=vec;letfirst=vec.remove(0);NonEmptyVec{first,rest: vec,}}/// Создает NonEmptyVec из итератора, возвращая None если итератор пуст
pubfnfrom_iter<I: IntoIterator<Item=T>>(iter: I)-> Option<Self>{letmutiter=iter.into_iter();letfirst=iter.next()?;letrest: Vec<T>=iter.collect();Some(NonEmptyVec{first,rest})}/// Возвращает количество элементов
pubfnlen(&self)-> usize{1+self.rest.len()}/// Всегда возвращает false, так как NonEmptyVec никогда не пуст
pubfnis_empty(&self)-> bool{false}/// Получает ссылку на элемент по индексу
pubfnget(&self,index: usize)-> Option<&T>{ifindex==0{Some(&self.first)}else{self.rest.get(index-1)}}/// Получает мутабельную ссылку на элемент по индексу
pubfnget_mut(&mutself,index: usize)-> Option<&mutT>{ifindex==0{Some(&mutself.first)}else{self.rest.get_mut(index-1)}}/// Возвращает ссылку на первый элемент
pubfnfirst(&self)-> &T{&self.first}/// Возвращает мутабельную ссылку на первый элемент
pubfnfirst_mut(&mutself)-> &mutT{&mutself.first}/// Возвращает ссылку на последний элемент
pubfnlast(&self)-> &T{self.rest.last().unwrap_or(&self.first)}/// Возвращает мутабельную ссылку на последний элемент
pubfnlast_mut(&mutself)-> &mutT{ifself.rest.is_empty(){&mutself.first}else{self.rest.last_mut().unwrap()}}/// Добавляет элемент в конец
pubfnpush(&mutself,value: T){self.rest.push(value);}/// Удаляет последний элемент, возвращая его
/// Гарантированно возвращает Some, так как всегда есть хотя бы один элемент
pubfnpop(&mutself)-> Option<T>{self.rest.pop().or_else(||{// Не можем удалить последний элемент, так как это сделает коллекцию пустой
// Вместо этого возвращаем None, сигнализируя, что удаление невозможно
None})}/// Удаляет последний элемент, паникуя если это был последний элемент
pubfnpop_unchecked(&mutself)-> T{self.rest.pop().expect("Cannot pop the last element of NonEmptyVec")}/// Вставляет элемент на указанную позицию
pubfninsert(&mutself,index: usize,value: T){ifindex==0{letold_first=std::mem::replace(&mutself.first,value);self.rest.insert(0,old_first);}else{self.rest.insert(index-1,value);}}/// Удаляет элемент по индексу, возвращая его
pubfnremove(&mutself,index: usize)-> Option<T>{ifindex==0{ifself.rest.is_empty(){// Не можем удалить последний элемент
None}else{letold_first=std::mem::replace(&mutself.first,self.rest.remove(0));Some(old_first)}}else{self.rest.remove(index-1).into()}}/// Создает итератор
pubfniter(&self)-> implIterator<Item=&T>{std::iter::once(&self.first).chain(self.rest.iter())}/// Создает мутабельный итератор
pubfniter_mut(&mutself)-> implIterator<Item=&mutT>{std::iter::once(&mutself.first).chain(self.rest.iter_mut())}/// Преобразует в Vec
pubfninto_vec(mutself)-> Vec<T>{letmutvec=Vec::with_capacity(self.len());vec.push(self.first);vec.append(&mutself.rest);vec}/// Применяет функцию ко всем элементам
pubfnmap<U,F>(self,mutf: F)-> NonEmptyVec<U>whereF: FnMut(T)-> U,{NonEmptyVec{first: f(self.first),rest: self.rest.into_iter().map(f).collect(),}}}// Реализация Index для удобного доступа по индексу
impl<T>Index<usize>forNonEmptyVec<T>{typeOutput=T;fnindex(&self,index: usize)-> &Self::Output{self.get(index).expect("Index out of bounds")}}// Реализация IndexMut для мутабельного доступа по индексу
impl<T>IndexMut<usize>forNonEmptyVec<T>{fnindex_mut(&mutself,index: usize)-> &mutSelf::Output{self.get_mut(index).expect("Index out of bounds")}}// Реализация IntoIterator для использования в циклах
impl<T>IntoIteratorforNonEmptyVec<T>{typeItem=T;typeIntoIter=std::vec::IntoIter<T>;fninto_iter(self)-> Self::IntoIter{self.into_vec().into_iter()}}// Реализация FromIterator для создания из итератора
impl<T>FromIterator<T>forNonEmptyVec<T>{fnfrom_iter<I: IntoIterator<Item=T>>(iter: I)-> Self{NonEmptyVec::from_iter(iter).expect("Cannot create NonEmptyVec from empty iterator")}}
Потоки - объект ОС, способ разделения работы ПО. На обработку потоков могут назначаться разные ядра, а могут и не назначаться. Потоки выгодно использовать тогда, когда они дают выигрыш во времени больше, чем время на их создание (на x86 процессорах = ~9000 наносек, на ARM процессорах = ~27000 наносек). Обычно, это интенсивные по вычислениям приложения. Для интенсивным по вводу-выводу приложений следует использовать async/await вместо потоков.
Пример создания:
fnhello_thread(){println!("Hello from thread")}fnmain(){println!("Hello from the MAIN thread");letthread_handle=std::thread::spawn(hello_thread);thread_handle.join().unwrap();// нужно соединить новый поток с главным
// потоком программы, иначе он может не успеть вернуть данные до
// завершения главного потока программы
}
Потоки являются владельцами данных, поэтому нужно передавать им данные перемещением, чтобы они были живы к моменту запуска потока:
fndo_math(i: u32)-> u32{letmutn=i+1;for_in0..10{n*=2;}n}fnmain(){println!("Hello from the MAIN thread");letmutthread_handles=Vec::new();// вектор указателей потоков
foriin0..10{letthread_handle=std::thread::spawn(move||do_math(i));thread_handles.push(thread_handle);// добавить поток к вектор
}forhinthread_handles.into_iter(){println!("{}",h.join().unwrap());// соединение потоков с главным.
}// и вывод результата каждого потока.
Разделение задачи на потоки
Простой вариант - поделить вектор со значениями на куски (chunks), и под обработку каждого куска сделать отдельный поток, после чего собрать потоки вместе:
fnmain(){constN_THREADS: usize=8;letto_add=(0..5000).collect::<Vec<u32>>();// вектор от 0 до 4999
letmutthread_handles=Vec::new();// вектор указателей потоков
letchunks=to_add.chunks(N_THREADS);// размер кусков разбиения
forchunkinchunks{letmy_chunk=chunk.to_owned();// обход borrow checker/lifetime
thread_handles.push(std::thread::spawn(move||my_chunk.iter().sum::<u32>()));// создание потоков с принадлежащими им данными
}// суммирование потоков-кусков в одно число
letmutsum: u32=0;forhandleinthread_handles{sum+=handle.join().unwrap()}println!("Sum is {sum}");}
Trait или типаж - это способ определения общего поведения для типов. Трейты позволяют абстрагироваться от конкретных типов и сосредоточиться на том, что эти типы умеют делать.
traitPrintable{// объявление трейта
fnprint(&self)-> String;// определяем, что Trait делает
}structPoint{x: i32,y: i32,}implPrintableforPoint{// применяем Trait для типа данных
fnprint(&self)-> String{format!("Point: ({}, {})",self.x,self.y)// пишем сам код
}}fnmain(){letp=Point{x: 3,y: 4};println!("{}",p.print());// Вывод: Point: (3, 4)
}
Инициализация типажа
При объявлении типажа можно оставить обязательную реализацию на потом, либо вписать реализацию функций в типаже по-умолчанию:
// типаж задаёт метод и ограничения по входным/выходным типам
traitLandVehicle{fnLandDrive(&self)-> String;}// типаж задаёт методы плюс их реализация по умолчанию
traitWaterVehicle{fnWaterDrive(&self){println!("Default float");}}
Применение типажей к структурам данных
Во время применения, если реализация по умолчанию была задана, то можно её переделать под конкретную структуру, либо использовать эту реализацию:
structSedan{}structRocketship{}// типаж LandVehicle не имеет реализации по умолчанию, реализуем тут
implLandVehicleforSedan{fnLandDrive(&self)-> String{format!("Car zoom-zoom!")}}// типаж WaterVehicle имеет выше реализацию по умолчанию, используем её
implWaterVehicleforRocketship{}
Наследование и объединение типажей
Типажи могут наследовать другие типажи с помощью синтаксиса trait NewTrait: OldTrait:
Любой тип, реализующий Displayable, обязан также реализовать Printable.
При объединении типажей, создаётся ярлык (alias). При этом каждый входящий в него типаж нужно отдельно применить к структуре данных. При этом можно также использовать реализацию определённых в типаже методов по умолчанию, либо написать свою.
// создание ярлыка
traitAmphibiousVehicle: LandVehicle+WaterVehicle{}// применение типажей к структуре
implAmphibiousVehicleforCarrier{}implLandVehicleforCarrier{fnLandDrive(&self)-> String{format!("Use air thrust to travel on land")}}implWaterVehicleforCarrier{}
Вызов методов экземпляра структуры определённого типажа
Типажи определяют, какие возможности есть у generic типа T:
fnmy_function<T>(value: T)-> TwhereT: SomeTrait+AnotherTrait{/* function body */}
Eq (Equality)
Проверяет равенство (==) или неравенство (!=);
Включает PartialEq (проверяет только равенство) + гарантирует тождество;
// Без Eq типажа:
// ❌ не будет компилироваться ==
fnfind_value<T>(items: &[T],target: T)-> bool{items.iter().any(|x|x==&target)// Error
}// Добавим Eq trait:
fnfind_value<T>(items: &[T],target: T)-> boolwhereT: Eq// теперь можно использовать ==
{items.iter().any(|x|x==&target)// ✅ работает!
}
std::hash::Hash
Позволяет конвертировать значения в хэш;
Требуется для хранения в HashMap, HashSet, или других коллекций с хэшом;
Тип с Hash может быть ключом или значением словаря:
usestd::collections::HashSet;// Без Hash:
// ❌ не будет компилироваться - HashSet требует Hash
fncreate_set<T>(items: Vec<T>)-> HashSet<T>{items.into_iter().collect()// Error: T doesn't implement Hash
}// С Hash:
fncreate_set<T>(items: Vec<T>)-> HashSet<T>whereT: Eq+std::hash::Hash// требуется для HashSet
{items.into_iter().collect()// ✅ работает!
}
Clone
Позволяет дублировать значение;
Разрешает метод .clone();
Нужно для копирования значений без взятия владения.
// Без Clone:
fnduplicate_first<T>(items: &[T])-> Option<T>{items.first().map(|x|x)// Error: can't return T from &T
}// Включаем Clone:
fnduplicate_first<T>(items: &[T])-> Option<T>whereT: Clone// можно клонировать
{items.first().cloned()// ✅ работает! - создаёт копию
}
letletter='a';println!("{}",letterasu32-96);// = 97-96 = 1
leti=97u8;// только с u8 разрешено делать 'as char'
println!("Value: {}",iaschar);
Boolean type
bool type can be true or false. Non-integer - do NOT try to use arithmetic on these. But you can cast them:
trueasu8;falseasu8;
Mutability
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.
letx=5;// immutable variable, type i32 guessed by Rust as default for numbers.
letmutx=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:
letx=5;letx=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
constONE_DAY_IN_SECONDS: u32=24*60*60;// type u32 MUST be defined
letphrase="Hello World";println!("Before: {phrase}");// Before: Hello World
letphrase=phrase.len();println!("After: {phrase}");// After: 11
Составные переменные
Кортеж / Tuple
Кортеж — это структура с фиксированным размером, которая может содержать элементы разных типов. Синтаксис: (T1, T2, ...).
lettup: (u32,f32,i32)=(10,1.2,-32,"hello");// Деструктуризация позволяет "разобрать" кортеж на отдельные переменные:
let(x,y,z,u)=tup;//Доступ осуществляется через точку и индекс (начиная с 0):
leta1=tup.0;leta2=tup.1;
Если нужен только один элемент, остальные можно игнорировать с помощью _:
Кортежи полезны для возврата нескольких значений из функции;
Как и массивы, они хранятся в стеке и имеют фиксированный размер;
Используйте кортежи, когда нужно объединить разнородные данные в одну сущность.
Массив / Array
Массив в Rust — это структура данных с фиксированным размером, которая хранит элементы одного типа. Его синтаксис: [T; N], где:
T — тип элементов (например, i32 для целых чисел),
N — количество элементов, известное на этапе компиляции.
fnmain(){// Простой массив из трёх чисел
letnumbers: [i32;3]=[1,2,3];// Массив с повторяющимся значением (5 элементов, все равны 0)
letzeros: [i32;5]=[0;5];println!("Numbers: {:?}",numbers);// Вывод: [1, 2, 3]
println!("Zeros: {:?}",zeros);// Вывод: [0, 0, 0, 0, 0]
// Доступ к элементам массива = по индексу (начиная с 0)
letfirst=numbers[0];letsecond=numbers[1];// accessing array elements
}
❗Попытка обратиться к несуществующему индексу (например, arr[3] для массива из трёх элементов) вызовет панику!
Особенности:
Размер массива фиксирован и не может измениться после создания;
Массивы хранятся в стеке (stack), что делает их быстрыми, но менее гибкими;
Используйте массивы, когда размер известен заранее и не будет меняться.
chunks(usize) - разбиение массива на куски с остатком
Можно разбивать срез на части заданного размера. Последний кусок может быть меньше указанного размера, если элементов не хватает.
letnumbers=[1,2,3,4,5,6,7];letchunks=numbers.chunks(3);// разбить на куски по 3
forchunkinchunks{println!("Чанк: {:?}",chunk);}// Вывод:
// Чанк: [1, 2, 3]
// Чанк: [4, 5, 6]
// Чанк: [7] <-- меньше размера 3!
chunks_exact(usize) - точное разбиение
Разбивает срез на части строго заданного размера. Остаток (если есть) доступен отдельно через метод remainder().
Естественное удаление при выходе из области видимости
Переменная выходит из области видимости (закрывающая фигурная скобка }), её память освобождается автоматически, если она владеет данными (например,String, Vec).
fnmain(){lettext=String::from("Hello");println!("{}",text);// "Hello"
// Здесь text всё ещё существует
}// text выходит из области видимости и память освобождается
// println!("{}", text); // Ошибка: text больше не существует
Очистка содержимого c clear()
fnmain(){letmuttext=String::from("Hello");text.clear();// очищает содержимое, но переменная остаётся
println!("{}",text);// "" (пустая строка)
}
Использование std::mem::drop
Для явного “удаления” переменной (освобождения её памяти) до конца области видимости можно использовать функцию std::mem::drop:
fnmain(){letmuttext=String::from("Hello");println!("{}",text);// "Hello"
std::mem::drop(text);// text "удаляется" (перестаёт существовать)
// println!("{}", text); // Ошибка: text больше не существует
}
drop принимает владение переменной и немедленно освобождает её память. После этого переменная становится недоступной.
Ansible will be the latest version supporting current Python in OS. So to get latest Ansible , Python must be updated as well!
Warning
Do not update the default Python in OS - it is used by system services, which may break!
Build Python:
sudo apt install build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev curl libbz2-dev
wget https://www.python.org/ftp/python/3.9.15/Python-3.9.15.tgz
tar -xf Python-3.9.15.tgz
cd Python-3.9.15
./configure --enable-optimizations
make -j 8
Installing Python:
Best way is to do checkinstall, create a .deb file to share with the team.
Use altinstall parameter to install Python in an alternative path. This is simpler & better than using Python virtualenv.
sudo make altinstall # now Python 3.9x is installed on separate path, while Python 3.7 in OS is unchangedsudo python3.9 -m pip install --upgrade pip
sudo python3.9 -m pip install ansible
ansible --version # now ansible is the latest version
---- name:front-end playhosts:allgather_facts:yesbecome:yestasks:- name:include role apacheinclude_role:name:apache- name:include role phpinclude_role:name:php...
apache / tasks / main.yml - tasks example:
---- name:install apacheapt:name:apache2- name:Enable service apache2ansible.builtin.systemd:name:apache2enabled:yesmasked:no- name:Make sure apache2 is runningansible.builtin.systemd:state:startedname:apache2...
Role structure
Directories inside a role:
- defaults - variable values by default
-- main.yml
- vars - variables defined by role (for other roles)
-- main.yml
- tasks - jobs to be completed
-- main.yml
- handlers - actions to be taken after checks
-- main.yml
- files - static files to be copied into client machine
- templates
ansible-doc -l # all installed modules listansible-doc <module-name> # module manansible-doc -s <module-name> # playbook snippets code examples on module
Module examples
Copy module
Copies a file from the local box to a remote system - useful for copying config files to remote system
can do backups
can do remote validation
Fetch module
Copy a file from remote system to local box
validate file using md5 checksum
Setup module
Gather info and facts on remote system
Inventory analysis of the system
ansible -i inventory web1 -m setup # gather all available system infoansible -i inventory web1 -m setup -a "filter=ansible_eth*"# gather info on NICsansible -i inventory all -m setup --tree ./setup # form an inventory of files in /setup/ folder with info on targeted systems
ansible webservers -i inventory -m yum -a "name=httpd state=present" -u vagrant --sudo
# name - name of package (Apache)# present - if package is not there, install. If it is there - do nothing and report "changed: false" (idempotence test)
Service module
Start/stop/restart services
Set autostart option for services
ansible webservers -i inventory -m service -a "name=httpd state=started enabled=yes" -u vagrant --sudo
# name - name of service (Apache)# state = started/stopped - make idempotence test and change if necessary# enabled = yes/no - autostart on system boot
# Tasks in a playbook are executed top down. Tasks use modules.tasks:- name:Name the task for readabilitymodule:parameters=go_here# Example:- name:Deploy Apache Configuration Filecopy:src=./ansible/files/configuration/httpd.confdest=/etc/httpd/conf/
---# -------- Global play declaration- hosts:webservers ## ----- Variables per playvars:git_repo:https://github.com/repo.githttp_port:8081db_name:wordpress## ------------------------### ---- Declare user to run taskssudo:yessudo_user:wordpress_user### ------------------------------gather_facts:no# dont't gather facts with SETUP module (default gathers facts - expensive in time)remote_user:roottasks:# --------------------------------- name:Install Apacheyum:name=httpd state=present- name:Start Apacheservice:name=httpd state=started
Including files
Use “- include” and “- include_vars” directives to include playbook files:
Check output of previous task as condition to run next task:
tasks:- name:Stop iptables nowservice:name=iptables state=stoppedregister:resultignore_errors:yes# supress default stop on error- debug:msg="Failure!"when:result|failed # Debug message will only be shown if task has failed# other conditions are "result|success", "result|skipped"
Checking variables with WHEN condition
Bring variable check to BOOL check:
- name:"test"hosts:localvars_prompt:- name:"os_type"prompt:"What OS? (centos or ubuntu)"default:"centos"private:novars:- is_ubuntu:"{{os_type == 'ubuntu'}}"- is_debian:"{{os_type == 'debian'}}"tasks:- debug:msg="this shows the conditional if variable equals ubuntu"when:is_ubuntu|bool- debug:msg="this shows the conditional if variable equals centos"when:is_centos|bool
Early approach: periodic scanning of the file-system and comparing the expected state with the actual state. Limitations:
can only be used to detect modifications and not reads to files;
unreliable because a modification can go undetected if the file is modified, then returned back to its original state before the scanning occurs:
A quick attacker can read/modify target file, and clean up their tracks before the periodic scan.
Later approach: in-kernel inotify:
addresses unreliability - executed inline with the operation;
no way to associate or filter operations using the execution context (e.g., pid or cgroup) of the process doing the operation -> no way to filter events based on which Kubernetes workload performed the file access;
lack of flexibility in the actions taken when a file is accessed:
When a monitored file is accessed, it will send an event to user-space and it’s up to the user-space agent to do the rest:
Example: when monitoring the directory /private/data, the sequence of operations would be:
Agent adds /private into the directories to be watched
Application creates /private/data directory
inotify sends an event to the agent that a directory /private/data was created
Agent adds /private/data to the directories to be watched
If a file was created and/or accessed in /private/data/* between steps 2 and 4, there will be noinotify event for the access prior to it being added to the watch list.
Possible to modify the kernel to add execution context to inotify events:
long process, might take years until a new kernel reaches production.
eBPF allows FIM implementation to correlate file access events with execution context such as process information (e.g., credentials) and its cloud native identity (e.g., k8s workload), perform inline updates to its internal state to avoid races, as well as implement inline enforcement on file operations.
Path-based FIM with eBPF
Install eBPF hooks (kprobes, specifically) to track file operations and implement File Integrity Monitoring (FIM).
install these hooks into system calls: the open system call to determine when a file is opened and for what access (read or write):
Hooking into system calls, however, might lead to time-of-check to time-of-use (TOCTOU) issues: the memory location with the pathname to be accessed belongs to user-space, and user-space can change it after the hook runs, but before the pathname is used to perform the actual open in-kernel operation:
Hooking into a (kernel) function that happens after the path is copied from user-space to kernel-space avoids this problem since the hook operates on memory that the user-space application cannot change. Hence, instead of a system call we will install a hook into a security_function. Specifically, we will hook into the security_file_permission function which is called on every file access (there is also security_file_open which is executed whenever a file is opened). Information about the process and its parent such as binary, arguments, credentials, and others. In cloud-native environments, the events also contain information about the container and the pod that this process belongs to.
If no filtering is applied, we get a file-all policy: generate events for every file access:
Many file accesses happen in a system at any point in time, and monitoring all of them is not a good practice because generating an event for each one incurs significant overhead;
file-all policy does not inform users about what file was actually accessed;
We create a second version of the policy where sensitive ssh server private keys are monitored:
Filtering in-kernel & deciding at the eBPF hook whether the event is of interest to the user or not, means that no pointless events will be generated and processed by the agent;
The alternative is to do filtering in user-space tends to induce significant overhead for events that happen very frequently in a system (such as file access). For more details, see Tetragon’s 1.0 release blog post.
* security_file_permission - the eBPF hook is called on every file access in the system
* Use security_file_open and have the eBPF hook be executed whenever a file is opened. However, it means that if a file is already opened before the hook is installed, the hook will not be called and certain accesses may be missed
* Hooks into other functions such as security_file_truncate or security_file_ioctl for other operations;
eBPF lets you do observability and do inline enforcement by stopping an operation from happening by modifying the request.
It is impossible to do proper enforcement without in-kernel filtering, because by the time the event has reached user-space it is already too late if the operation has already executed.
Path FIM limitations
Paths are taken from from struct file arguments of functions such as security_file_open;
The same file can have multiple names in a Linux system:
If a policy monitors /etc/ssh/ssh_host_rsa_key but the same underlying file is accessed via a different name, the access will go unnoticed;
Same file can have multiple names are hard links, bind mounts, and chroot.
Hard link to the file /etc/ssh/ssh_host_rsa_key named, for example, /mykey accesses via /mykey will not be caught by policies such as file-ssh-keys:
Creating hard links requires appropriate permissions (when fs.protected_hardlinks is set to 1, creating a link requires certain permissions on the target file);
bind mount requires CAP_SYS_ADMIN;
chroot requires CAP_CHROOT.
We need the ability to monitor file accesses regardless of the name with which the file is accessed.
inode-based FIM with eBPF
An inode number uniquely identifies an underlying file in a single filesystem.
Example:
# stat /etc/ssh/ssh_host_ecdsa_key | grep InodeDevice: 259,2 Inode: 36176340 Links: 1# ln /etc/ssh/ssh_host_ecdsa_key /key# stat /key | grep InodeDevice: 259,2 Inode: 36176340 Links: 2# touch /key2# mount --bind /etc/ssh/ssh_host_ecdsa_key /key2# stat /key2 | grep InodeDevice: 259,2 Inode: 36176340 Links: 2
Диаграмма работы сканера
sequenceDiagram
autonumber
actor U as User
participant A as Агент
participant S as Сканнер
participant F as Файл
participant B as Программа eBPF
participant I as Карта inodes
U->>A: политика
activate A
activate U
A->>S: шаблон
S->>F: получить inode
activate F
activate S
F-->>S: inode
S->>I: обновить список inodes
deactivate S
activate I
F->>B: событие
deactivate F
activate B
loop Синк в ядре
B->>I: запрос списка inodes
I-->>B: список inodes
end
deactivate I
B-->>A: событие
deactivate B
A-->>U: уведомление
deactivate A
deactivate U
Tracing policies can be added to Tetragon through YAML configuration files that extend Tetragon’s base execution tracing capabilities. These policies perform filtering in kernel to ensure only interesting events are published to userspace from the BPF programs running in kernel. This ensures overhead remains low even on busy systems.
For directories it’s possible to exclude one or more directories using the --exclude-dir parameter. For example, this will exclude the dirs dir1/, dir2/ and all of them matching *.dst/:
Image manifest указывает на расположение конфига и набора слоёв для образа контейнера на конкретной ОС и архитектуре. Поле size указывает общий размер объекта. Теперь можно исследовать далее:
Распакуем базовый первый слой из архива и изучим его:
$ mkdir rootfs
$ tar -C rootfs -xf mysterious-image/blobs/sha256/0503825856099e6adb39c8297af09547f69684b7016b7f3680ed801aa310baaa
$ tree -L 1 rootfs/
rootfs/
├── bin
├── dev
├── etc
├── home
├── lib
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin
├── srv
├── sys
├── tmp
├── usr
└── var
Это файловая система ОС! Можно изучить версию дистрибутива
$ cat rootfs/etc/issue
Welcome to Alpine Linux 3.10
Kernel \r on an \m(\l)$ cat rootfs/etc/os-release
NAME="Alpine Linux"ID=alpine
VERSION_ID=3.10.1
PRETTY_NAME="Alpine Linux v3.10"HOME_URL="https://alpinelinux.org/"BUG_REPORT_URL="https://bugs.alpinelinux.org/"
Распакуем следующий слой и изучим его:
$ tar -C layer -xf mysterious-image/blobs/sha256/6d8c9f2df98ba6c290b652ac57151eab8bcd6fb7574902fbd16ad9e2912a6753
$ tree -L 1 layer/
layer/
└── my-file
1 directory, 1 file
Тут лежим доп файл, созданный командой "/bin/sh -c touch my-file" - это можно увидеть в секции history. По сути исходный Dockerfile выглядел так:
FROMalpine:latestRUNecho HelloRUN touch my-file
Buildah
В 2017 году Red Hat разработали инструмент для создания образов контейнеров по стандарту OCI - как аналог docker build.
Создадим Dockerfile vim Dockerfile и впишем в него:
FROMalpine:latestRUNecho HelloRUN touch my-file
Запустим сборку контейнера на базе этого файла - buildah bud
Buildah поддерживает много команд:
buildah images # список образовbuildah rmi # удалить все образыbuildah ps # показать запущенные контейнеры
Почему вдруг buildah ps показ запущенных контейнеров, когда это инструмент для их СОЗДАНИЯ? А потому что в процессе создания как в buildah, так и в docker идёт запуск промежуточных контейнеров, их модификация в runtime. Каждый шаг модификации создаёт записи в history. Это потенциальная проблема ИБ: можно влезть в контейнер, пока он собирается (и запущен), если там что-то большое, и модифицировать его.
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
05f4aa4aa95c 54da47293a0b "/bin/sh -c 'apt-get…"11 seconds ago Up 11 seconds interesting_heyrovsky
> docker exec -it 05f4aa4aa95c sh
# echo "Hello world" >> my-file
После сборки можно удостовериться, что созданный файл на месте:
> docker run clang cat my-file
Hello world
Создание контейнеров без Dockerfile
У buildah есть императивные команды к любой команде из Dockerfile, типа RUN или COPY. Это даёт огромную гибкость, т.к вместо огромных Dockerfile можно делить процесс создания контейнеров на части, между ними запускать любые вспомогательные инструменты UNIX.
Создадим базовый контейнер с Alpine Linux и посмотрим как он работает:
buildah from alpine:latest
buildah ps
Можно запускать команды в контейнере, а также создать в нёс файл:
По-умолчанию, buildah не делает записи history в контейнер, это значит порядок команд и частота их вызова не влияют на итоговые слои. Можно поменять это поведение ключом --add-history или переменной ENV BUILDAH_HISTORY=true.
Сделаем коммит нового контейнера в образ для финализации процесса:
buildah commit alpine-working-container my-image
buildah images # новый образ теперь в локальном реестре
Можно выпустить образ в реестр Docker, либо на локальный диск в формате OCI:
buildah unshare создаёт новое пространство имён, что позволяет подключить файловую систему как текущий не-root пользователь;
--mount автоматически подключает, путь кладём в переменную среды MOUNT;
Далее мы делаем commit на изменения, и mount на автомате убирается при покидании buildah unshare сессии.
Мы успешно модифицировали файловую систему контейнера через локальный mount. Проверим наличие файла:
> buildah run -t alpine-working-container cat test-from-mount
it-works
Вложенные контейнеры
У buildah нет даемона, значит, не нужно подключать docker.sock в контейнер для работы с Docker CLI. Это даёт гибкость и возможность делать вложенные схемы: установим buildah в контейнер, созданный buildah:
> buildah from opensuse/tumbleweed
tumbleweed-working-container
> buildah run -t tumbleweed-working-container bash
# zypper in -y buildah
Теперь вложенный buildah готов к использованию. Нужно указать драйвер хранения VFS в контейнере, чтобы получить рабочий стек файловой системы:
# buildah --storage-driver=vfs from alpine:latest# buildah --storage-driver=vfs commit alpine-working-container my-image
Получили вложенный контейнер. Заложим его в хранилище на локальной машине -> для начала заложим образ в /my-image:
ВАЖНОЕ ЗАМЕЧАНИЕ: все действия с buildah не потребовали sudo. Buildah создаёт всё необходимое для каждого пользователя в папках:
~/.config/containers, конфигурация
~/.local/share/containers, хранилища контейнеров
Декомпозиция Dockerfile в несколько разных с помощью CPP макросов.
podman
Инструмент для замены Docker. podman использует buildah как API для создания Dockerfile с помощью podman build. Это значит, что они разделяют одно хранилище под капотом. А это значит, что podman может запускать созданные buildah контейнеры:
chroot - утилита, которая предназначена для изоляции файловой среды приложения. Создана в Minix 1.7. Для процессов и ОЗУ не подходит, но вдохновила создание Namespaces в Linux позднее.
Пример работы
Для работы bash в новой среде chroot необходимо внести его копию в папку jail:
Либо, гораздо проще перенести разом все библиотеки:
cp -a /usr jail/
cp -a /lib jail/
cp -a /lib64 jail/
Далее, заход в окружение:
sudo chroot $HOME/jail /bin/bash
bash-5.0# ls
bin lib lib64 usr
#### Побег из chroot```c
#include <sys/stat.h>#include <unistd.h>int main(void){ mkdir(".out", 0755); // нужны права root в контейнере
chroot(".out"); chdir("../../../../../"); // относительный путь за пределы корня
chroot(".");return execl("/bin/bash", "-i", NULL);}
Only privileged processes with the capability CAP_SYS_CHROOT are able to call chroot.
Modern systems use pivot_mount (calling process must have the CAP_SYS_ADMIN capability). - has the benefit of putting the old mounts into a separate directory on calling.
Linux Namespaces
Задача: обернуть системные ресурсы в уровень абстракции;
Introduced in Linux 2.4.19 (2002), became “container ready” in 3.8 in 2013 with the introduction of the user namespace;
Seven distinct namespaces implemented: mnt, pid, net, ipc, uts, user, cgroup; time and syslog introduced in 2016;
функция clone . Создаёт дочерние процессы. Unlike fork(2), the clone(2) API allows the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal handlers. You can pass different namespace flags to clone(2)to create new namespaces for the child process.
unshare(2) - отсоединение частей контекста выполнения процесса.
setns(2) позволяет запрашивающему процессу присоединяться в разные namespaces.
proc - Besides the available syscalls, the proc filesystem populates additional namespace related files. Since Linux 3.8, each file in /proc/$PID/ns is a “magic“ link which can be used as a handle for performing operations (like setns(2)) to the referenced namespace.
> ls -Gg /proc/self/ns/
total 0lrwxrwxrwx 10 Feb 6 18:32 cgroup -> 'cgroup:[4026531835]'lrwxrwxrwx 10 Feb 6 18:32 ipc -> 'ipc:[4026531839]'lrwxrwxrwx 10 Feb 6 18:32 mnt -> 'mnt:[4026531840]'lrwxrwxrwx 10 Feb 6 18:32 net -> 'net:[4026532008]'lrwxrwxrwx 10 Feb 6 18:32 pid -> 'pid:[4026531836]'lrwxrwxrwx 10 Feb 6 18:32 pid_for_children -> 'pid:[4026531836]'lrwxrwxrwx 10 Feb 6 18:32 user -> 'user:[4026531837]'lrwxrwxrwx 10 Feb 6 18:32 uts -> 'uts:[4026531838]'
mnt namespace
Ввели в 2002 первым, ещё не знали, что понадобится много разных, потому обозвали флаг клонирования CLONE_NEWNS, что не соответствует флагам других namespaces.
С помощью mnt в Linux можно изолировать группу точек монтирования для групп процессов.
We have a successfully mounted tmpfs, which is not available on the host system level:
> ls mount-dir
> grep mount-dir /proc/mounts
The actual memory being used for the mount point is laying in an abstraction layer called Virtual File System (VFS), which is part of the kernel and where every other filesystem is based on.
Можно создавать на лету гибкие файловые системы. Mounts can have different flavors (shared, slave, private, unbindable), which is best explained within the shared subtree documentation of the Linux kernel.
uts namespace (UNIX Time-sharing System)
Ввели в 2006 в Linux 2.6.19. Можно отсоединить домен и имя хоста от системы.
And if we look at the system level nothing has changed, hooray:
exit> hostname
nb
ipc namespace
Ввели в 2006 в Linux 2.6.19. Можно изолировать связи между процессами. Например, общую память (shared memory = SHM) между процессами. Два процесса будут использовать 1 идентификатор для общей памяти, но при этом писать в 2 разных региона памяти.
pid namespace (Process ID)
Ввели в 2008 в Linux 2.6.24. Возможность для процессов иметь одинаковые PID в разных namespace. У одного процесса могут быть 2 PID: один внутри namespace, а второй вовне его - на хост системе. Можно делать вложенные namespace, и тогда PID у 1 процесса будет больше.
Первый процесс в namespace получается PID=1 и привилегии init-процесса.
> sudo unshare -fp --mount-proc
# ps aux
Флаг --mount-proc нужен чтобы переподключить proc filesystem из нового namespace. Иначе PID в namespace будут не видны.
net namespace (Network)
Ввели в 2009 в Linux 2.6.29 для виртуализации сетей. Каждая сеть имеет свои свойства в разделе /proc/net. При создании нового namespace он содержит только loopback интерфейсы. Создадим:
> sudo unshare -n
# ip l# ip a
Каждый интерфейс (физ или вирт) присутствует единожды в каждом namespace. Интерфейсы можно перемещать между namespace;
Каждый namespace имеет свой набор ip, таблицу маршрутизации, список сокетов, таблицу отслеживания соединений, МЭ и т.д. ресурсы;
Удаление net namespace разрушает все вирт интерфейсы и перемещает оттуда все физические.
Применение: создание SDN через пары виртуальных интерфейсов. Один конец пары подключается к bridge, а другой конец - к целевому контейнеру. Так работают CNI типа Flannel.
Создадим новый net namepsace:
> sudo ip netns add mynet
> sudo ip netns list
mynet
Когда команда ip создаёт network namespace, она создаёт it will create a bind mount for it under /var/run/netns too. This allows the namespace to persist even when no processes are running within it.
> sudo ip netns exec mynet ip l
> sudo ip netns exec mynet ping 127.0.0.1
The network seems down, let’s bring it up:
> sudo ip netns exec mynet ip link set dev lo up
> sudo ip netns exec mynet ping 127.0.0.1
Let’s create a veth pair which should allow communication later on:
> sudo ip link add veth0 type veth peer name veth1
> sudo ip link show type veth
Both interfaces are automatically connected, which means that packets sent to veth0 will be received by veth1 and vice versa. Now we associate one end of the veth pair to our network namespace:
> sudo ip link set veth1 netns mynet
> ip link show type veth
Добавляем адреса ip:
> sudo ip netns exec mynet ip addr add 172.2.0.1/24 dev veth1
> sudo ip netns exec mynet ip link set dev veth1 up
> sudo ip addr add 172.2.0.2/24 dev veth0
> sudo ip link set dev veth0 up
It works, but we wouldn’t have any internet access from the network namespace. We would need a network bridge or something similar for that and a default route from the namespace.
user namespace
Ввели в 2012-2013 в Linux 3.5-3.8 для изоляции пользователей, групп пользователей.
Пользователь получает разные ID внутри и вовне namespace, а также разные привилегии.
> id -u
1000> unshare -U
> whoami
nobody
После создания namespace, файлы /proc/$PID/{u,g}id_map раскрывают соответствия user+groupID и PID. Эти файлы пишутся лишь единожды для определения соответствий.
> cat /proc/$PID/uid_map
010001
cgroups
Ввели в 2008 в Linux 2.6.24 для квотирования и далее переделали капитально в 2016 в Linux 4.6 - ввели cgroups namespace.
Система выдаёт список ограничений. Поменяем ограничения памяти для этой cgroup. Также отключим swap, чтобы реализация сработала:
unshare -c # unshare cgroupns in some cgroupcat /proc/self/cgroup
sudo mkdir /sys/fs/cgroup/demo
cd /sys/fs/cgroup/demo/
sudo su
echo100000000 > memory.max
echo0 > memory.swap.max
cat /proc/self/cgroup
echo0 > cgroup.procs
cat /proc/self/cgroup
После того как установлено ограничение в 100Mb памяти ОЗУ, напишем приложение, которое забирает память больше чем положенные 100Mb (в случае отсутствия ограничений приложение закрывается при занятии 200Mb):
fnmain(){letmutvec=vec![];letmax_switch: usize=20;// запасное ограничение =200Mb
letmutmemcount: usize;loop{vec.extend_from_slice(&[1u8;10_000_000]);memcount=vec.len()/10_000_000;println!("{}0 MB",memcount);ifmemcount>max_switch{break;}}println!("Program terminated by MAX MEM = {}0 Mb",memcount);}
Если его запустить, то увидим, что PID будет убит из-за ограничений памяти:
Можно составлять пространства имён вместе, чтобы они делили 1 сетевой интерфейс. Так работают k8s Pods. Создадим новое пространство имён с изолированным PID:
> sudo unshare -fp --mount-proc
# ps auxUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.6 186886904 pts/0 S 23:36 0:00 -bash
root 39 0.0 0.1 354801836 pts/0 R+ 23:36 0:00 ps aux
Вызов ядра setns с приложением-обёрткой nsenter теперь можно использовать для присоединения к пространству имён. Для этого нужно понять, в какое пространство мы хотим присоединиться:
> exportPID=$(pgrep -u root bash)> sudo ls -l /proc/$PID/ns
В 2007 году Google сделали проект Let Me Contain That For You (LMCTFY), в 2008 году появился Linux Containers (LXC). Для управления LXC в 2013 году появился инструмент Docker. Далее в 2015, команда Docker разработали проект libcontainer на языке Go. Также, в 2015 вышел Kubernetes 1.0. В 2015 собрали Open Container Initiative (OCI), которые стали разрабатывать стандарты на метаданные (манифесты-спецификации), образы контейнеров, методы управления ими. В том числе, в рамках OCI создали инструмент запуска и работы с контейнерами runc.
runc
sudo apt install runc
runc spec
cat config.json
В спецификации от runc можно увидеть всё необходимое для создания и запуска контейнера: environment variables, user + group IDs, mount points, Linux namespaces. Не хватает только файловой системы (rootfs), базового образа контейнера:
В ней можно увидеть обычные поля из runc, а доп заполненные annotations:
"annotations":{"org.opencontainers.image.title":"openSUSE Tumbleweed Base Container","org.opencontainers.image.url":"https://www.opensuse.org/","org.opencontainers.image.vendor":"openSUSE Project","org.opencontainers.image.version":"20190517.6.190",
Чтобы создать контейнер с runc, нужно его зацепить на терминал ввода команд TTY:
sudo runc create -b bundle container
ERRO[0000] runc create failed: cannot allocate tty if runc will detach without setting console socket
На существующий TTY зацепить контейнер нельзя (потому что окно удалённого xTerm не поддерживает такое), нужно создать новый виртуальный TTY и указать его сокет. Для этого надо установить Golang, скачать приложение rectty, создать с его помощью виртуальный терминал, после чего В ДРУГОМ ОКНЕ терминала создать контейнер и зацепить его на создвнный TTY:
В ДРУГОМ ОКНЕ терминала создать контейнер и зацепить его на создвнный TTY:
sudo runc create -b bundle --console-socket $(pwd)/tty.sock container
sudo runc list # контейнер в статуса created, не запущенsudo runc ps container # посмотрим что внутри негоUID PID PPID C STIME TTY TIME CMD
root 2977210 10:35 ? 00:00:00 runc init
runc init создаёт новую среду со всеми namespaces. /bin/bash ещё не запущен в контейнере, но уже можно запускать в нём свои процессы, полезно чтоб настроить сеть:
sudo runc start container
sudo runc list
sudo runc ps container
UID PID PPID C STIME TTY TIME CMD
root 652165110 14:25 pts/0 00:00:00 /bin/bash
Исходный runc init пропал, теперь только /bin/bash существует в контейнере. На ПЕРВОМ ОКНЕ терминала появилась консоль контейнера:
$ ps aux
ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 51564504 pts/0 Ss 10:28 0:00 /bin/bash
root 29 0.0 0.0 65283372 pts/0 R+ 10:32 0:00 ps aux
Можно проверить управление: заморозим контейнер. Во ВТОРОМ ОКНЕ терминала выполним:
sudo runc pause container
# в первом окне ввод команд прервётсяsudo runc resume container
# Рассмотрим события контейнера:sudo runc events container
#{...}
Для остановки контейнера достаточно выйти из rectty-сессии, после чего удалить контейнер. Остановленный контейнер нельзя перезапустить, можно лишь пересоздать в новом состоянии:
> sudo runc list
ID PID STATUS BUNDLE CREATED OWNER
container 0 stopped /bundle 2019-05-21T10:28:32.765888075Z root
> sudo runc delete container
> sudo runc list
ID PID STATUS BUNDLE CREATED OWNER
Можно модифицировать спецификацию в контейнере (bundle/config.json):
> sudo apt install moreutils, jq # инструмент jq для работы с JSON> cd bundle
> jq '.process.args = ["echo", "Hello, world!"]' config.json | sponge config.json
> sudo runc run container
> Hello, world!
Можно удалить разделение PID namespace процессов в контейнере с хостом:
> jq '.process.args = ["ps", "a"] | del(.linux.namespaces[0])' config.json | sponge config.json
> sudo runc run container
16583 ? S+ 0:00 sudo runc run container
16584 ? Sl+ 0:00 runc run container
16594 pts/0 Rs+ 0:00 ps a
[output truncated]
runc очень низкоуровневый и позволяет серьёзно нарушить работу и безопасность контейнеров.
Однако, их намного удобнее использовать на уровне управления выше
Для защиты также можно запускать контейнеры в режиме rootless из runc
С помощью runc нужно руками настраивать сетевые интерфейсы, очень трудоемко
CRI-O
Инструмент CRI-O разработан в 2016 при участии OCI в рамках проекта Kubernetes. Философия UNIX, максимально лёгкий аналог Docker/containerd. Он НЕ предназначен как инструмент для приёма команд от разработчиков. Задача - принимать команды от K8s. Внутри себя CRI-O использует runc как backend, и принимает команды по gRPC API как frontend.
Попробуем CRI-O с помощью спец-контейнера с crictl:
sudo apt install podman
sudo vim /etc/containers/registries.conf
# нужно задать репозиторий для скачивания:# unqualified-search-registries=["docker.io"] sudo podman run --privileged -h crio-playground -it saschagrunert/crio-playground
hugo new site ./
git init
git add *
git commit -m "Initial commit"cd themes/
git submodule add https://github.com/halogenica/beautifulhugo.git themes/beautifulhugo
git status
git add *
git commit -m "Theme added"
Some themes (Relearn) have to be downloaded as zip. So we have a git repo inside a git repo now. Copy contents exampleSite of theme:
cp -r themes/hugo-theme-relearn/exampleSite/* ./
Launch local web-server:
hugo server
Visit local site and check it.
Gitlab sync
Create a Gitlab account, make SSH Key exchange. After that, create project by CLI from hugo site folder:
git branch -m master main # change master to main branch for gitlabgit push --set-upstream git@gitlab.com:aagern/hugotest.git main
Where hugotest.git is the project name.
Click Setup CI/CD button.
Use Hugo template (should be in Gitlab):
---
# All available Hugo versions are listed here:# https://gitlab.com/pages/hugo/container_registry# image: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest"image:
name: "${CI_TEMPLATE_REGISTRY_HOST}/pages/hugo:latest" entrypoint: ["/bin/sh", "-c"]variables:
GIT_SUBMODULE_STRATEGY: recursive
test:
script:
- hugo
except:
variables:
- $CI_COMMIT_BRANCH==$CI_DEFAULT_BRANCHpages:
script:
- hugo
artifacts:
paths:
- public
only:
variables:
- $CI_COMMIT_BRANCH==$CI_DEFAULT_BRANCH environment: production
Change visibility: Settings -> General -> Pages visibility = Everyone.
Timeline resolution - the resolution in which the project will be edited. This is NOT the resolution, in which the project will be rendered. So you can edit the whole project in FullHD or 2048x1080DCI faster mode and then render it in 4K UHD or 4096x2160 DCI - DaVinci Resolve will mathematically scale all masks & effects to match the higher resolution on final render. Blackmagic does warn that such scale may cause some approximation;
Timeline resolution - may be changed in process of work. Convenient to choose before work;
Timeline frame rate - cannot be changed in process of work. Need to choose wisely! For example, transitions correction layers may be only made for specific frame rate (29.97 FPS);
Playback frame rate the same as timeline frame rate, but is the playback rate of the connected reference monitor;
Video Monitoring section contains parameters for the connected reference monitor;
Optimized media resolution - may be changed later, but better to change right away to save performance and space on disk = Half/Quarter from original;
Optimized Media Format / Render Cache Format - fastest codec on Win = DNxHR LB, on macos = ProRes LT/Proxy;
Enable all checkboxes in this section to save performance the most;
Cache files location - insert folder Cache made earlier;
Gallery stills location - insert folder Gallery made earlier;
Save current settings as a preset: go to Presets tab in Project Settings, choose Save As.. After saving the preset, choose it with a right mouse click and choose Save As User Default Config.
Image Scaling
Mismatched resolution files = Scale full frame with crop, this setting greatly helps in conform process, if material was re-scaled before color correction.
Color Management
Broadcast Safe: turn ON to help monitor colors to be safe for TV broadcast.
-20 - 120 - most lenient option
0 - 100 - most strict option
Check current colors in Color room go in menu View → Display Safe Broadcast Exceptions
Get the colors back to safe with either using HUE vs SAT instrument, or Gamut Mapping OpenFX plugin (Gamut mapping method = Saturation Compression, lower Saturation Max level).
Global Preferences
These preferences work for the whole system and all projects.
System - Memory & GPU
Resolve Memory usage - give all available memory to DaVinci, if no games played or apps used same time as DaVinci;
Fusion Memory Cache - needed for Fusion compositing rendering and playback in real time;
GPU - leave Auto in case no problems. For macOS choose = Metal, for PC Intel/AMD card = OpenCL, for NVIDIA choose CUDA. GPU Selection Mode needed for several GPU cards on one computer: one can be used for interface, the other for rendering etc. Are 2 better than 1, should you connect both? Actually this depends on type of cards - does not give significant speed in notebook setup with external videocard, if both the external card and the inner card of the notebook are connected: this gives a few bonus seconds faster render but makes the notebook unusable during render process. Also inner notebook card gets connected to interface and previews, which makes them work slower.
System - Media Storage
Create shortcuts for all folders actively used in the project;
System - Decode Options
Needed for work with RAW. Enable all checkboxes - for the GPU to use hardware Debayer on RAW material.
System - Audio Plugins
The folder containing 3rd party VST plugins for Fairlight, in case DaVinci did not find them itself.
User - Color
Always perform copy and paste on selected nodes. Should be turned ON to do copy-pasting in DaVinci.
User - Project Save and Load
Live Save checkbox. This makes DaVinci save any change, like FCPX;
Configure Project backups: every 6 min, every 1 hour, every 1 day.
Choose Backup location folder, created previously.
EDITSHIFT+SCROLLTRACKPAD - scale in/out track on timeline (or SHIFT+SCROLLBUTTON)
EDIT - SHIFT+Z - Zoom to fit timeline
EDIT - ALT+ARROWUP/ARROWDOWN - bring selected clip up/down a track on timeline
EDIT - SHIFT+~ - Reveal Transform border over selected clip in viewer window
EDIT - CTRL+SHIFT+V (done after CTRL+C) - Paste selected attributes on selected clip / paste clip with no overwrite - moves adjacent clips to the right
EDIT - CTRL+SHIFT+X - Remove attributes from selected clip
EDIT - P - Open clip attributes windows
EDIT - F - turn snapping ON/OFF (also use N)
EDIT - ALT+F - find selected clip from timeline in Media Pool
EDIT - CTRL+SHIFT+F - find & show selected clip from Media Pool in Finder
Compound Clips
EDIT - CTRL+E - create a Compound clip from selected clips
EDIT - CTRL+SHIFT+E - enter inside Compound clip
EDIT - ALT+SHIFT+E - decompose a Compound clip
Clip Timeline Properties / Retime
EDIT - G - turn linked selection ON/OFF
EDIT - C - unlink/link video+audio of selected clip
EDIT - R - Open clip speed window (+Ripple timeline in config will move adjacent clips without overwirting them)
EDIT - SHIFT+R - clip speed overlay on selected clip
EDIT - CTRL+R** - clip speed menu for clip
EDIT - CTRL+SHIFT+R - show retime curve: ALT+click on curve to add speed points fast
EDIT - CTRL+ALT+R - reset retime controls to 100%
EDIT - SHIFT+C - Curve Editor on selected clip
EDIT - CTRL+SHIFT+C - Keyframe Editor on selected clip
EDIT - L - Loop mode ON/OFF
EDIT - SHIFT+SPACEBAR - play the loop (use In/Out points to define the loop interval)
Y - select clip & press to select all other clips to the right in the track from the current selected one
CTRL+Y - clip & press to select all other clips to the left in the track from the current selected one
EDITH - show current Timeline you are working in at Media Pool
Connecting output of one node to the output of another node will automatically create a Merge node;
Tips
Editing Color Curves color correction and other nodes splines in Spline Editor
Put a Color Curves node;
Open Spline Editor. In its’ menu …, choose Show only Selected Tool, then click Zoom to Fit button (diagonal arrows in a square).
Copy perspective view to camera view
Connect camera node to Merge node;
While Merge node is selected in graph, in perspective windows rotate the view, the Mouse Right-Click → Camera → Copy PoV To → Choose the connected camera.
Choose a large map picture, throw it in DaVinci Resolve Timeline;
Make the still image video 10 seconds in length;
Open the map image video in Fusion Room;
Add a Background node and Merge it with the MediaIn node;
Add a Polygon node and connect it to Background node;
Choose the Polygon node, then in the Viewer draw line segments from point A to B;
In Polygon node → Controls → Border Width = 0.006 add border width to see the line. Choose Polygon → Controls → Border Style to make line ends round or square;
Change Background node Background → Color to make the line colored appropriately;
Animate the Polygon node → Controls → Length from 0 and frame 0 to 1.0 at last frame;
In Spline Editor choose both keyframes, make them smooth (hotkey S). Then use hotkey T to select Ease In = 45 and Ease Out = 35;
Create map points: add a new Background node, Merge it with the previous Merge node;
Add an Ellipse node, drag the border of the ellipse to make it a small point;
Animate Ellipse → Controls → Border Width from small negative number to 0 to make the point pop out when travel line starts from it;
Go to Spline Editor, choose the Ellipse keyframes and press S to make them smooth;
Ctrl+C copy the Ellipse, then Ctrl+V paste it to make other points for the travel line. Move them to appropriate positions;
Use the Keyframes editor to move the keyframes of ellipse copies in the appropriate frames of time.
Make Pseudo-3D Effects
Add Drop Shadow node, connect it after the Polygon + Background nodes, before the Merge node;
Reduce Drop Shadow → Controls → Blur parameter, reduce the Drop Shadow → Controls → Drop Distance parameter to make the shadow closer;
Add the DVE node just before MediaOut node;
Adjust DVE → Controls → Z Move = 0.3. Adjust DVE → Controls → Rotation → X and Y to make cool 3D-like effect;
Make the Viewer into Dual-View, and put result of DVE in one viewer and Merge node before it in second Viewer windows;
Adjust DVE → Controls →Pivot → X slightly to see the large X in the viewers. Grab the X and move it to the first point of the line;
Animate the Pivot by dragging it to points following the travel line. The use the Spline editor to choose all keyframes and make the animation smooth (S);
Go to Polygon node and edit Polygon → Settings → Motion Blur = ON, Quality = 9.
Room - Deliver
General Web Export
Preset = H.264
Quality restrict to:
10000 - 40000 Kb/s - for 1080p
20000 - 80000 Kb/s - for 4K UHD/DCI
6000 - 9000 Kb/s - for Instagram, 720p. More is not needed since video will be re-encoded by their codec.
SHIFT+D - Grade Bypass (Disable). Disable all nodes to see the original image;
CTRL+D - Grade Bypass (Disable) on current node;
SHIFT+H - see node mask, or qualifier mask etc. in monitor windows;
CTRL+F - See preview window in Full screen.
Create nodes
ALT+S - New Corrector (Serial) AFTER selected node;
SHIFT+S - New Corrector (Serial) BEFORE selected node;
ALT+L - Add Layer lower than current node;
ALT+O - New Outside node (Includes everything NOT selected in previous node)
ALT+P - New Parallel node.
Tracker
CTRL+T - Track mask forward;
ALT+T - Track mask backward.
Versions
CTRL+Y - add new color grade version;
CTRL+N - switch to next version;
CTRL+B - switch to previous version.
PowerGrades
Create a PowerGrade folder to store color grading across all projects.
Captured stills can be put into this folder → go into the DaVinci Database and are shared across all projects. A convenient way to store ready-made Color Space Transforms for different camera colorspace/gamma types. Use Append Node Graph to apply the grade, do NOT use Apply Grade since this changes the original ISO of the footage, which may differ in the specific scene.
Tracker
Cloud tracker - tracks using a whole mask with all its’ points.
Point tracker - needs control points on contrast areas, which it uses to track.
Stills
Right-click on footage → Grab Still: this saves the footage with currently applied grade. After any changes to the image, you can find the still in Gallery, and:
double-clickon still to make a A|B compare picture. CTRL+W to see changes on full screen (disable still);
right-click → Apply Grade to apply to what was saved. Stills can be applied to any footage to copy correction or try interesting results.
Закат - много красного, оранжевого в средних и в светлых тонах, синий цвет в тенях противопоставляется;
Рассвет - более холодный чем закат, больше зелёного чем красного/оранжевого;
Ночь - в кино обычно синяя ночь, чем чёрная. Небо сохраняет светлость;
Унылый серый город - снизить насыщенность (Saturation), чтобы это подчеркнуть.
Цвет в портрете
Хакрактер рисуется контрастом. Обычно грейдинг начинается с опусканием вниз теней. Дальше отделение от фона.
Работа с ЧБ портретом
Человек не воспринимает ЧБ как именно ЧБ, он ищет и выдумывает немного цвета. Поэтому ЧБ не делают полностью лишённым цвета, его чуть-чуть подкрашивают для придания настроения: для тёплого настроения подкрашивают в коричневый, для “глянцево-журнального” настроения - в синий. Количество подкраса совсем маленькое, совсем чуть-чуть, как петрушка в супе.
Teal & Orange
Start by grading the image into nice blue using all color wheels (Lift, Gamma, Gain), and distribute the wheels in a clockwise fashion between each other!
All colors in the image turn blue, but shadows have to stay neutral: user the Log wheels to fine tune: Shadow, Midtone, Highlight wheels between Low Range (default lower than 33% = shadows) and High Range (default higher than 66% = highlights). Turn the Shadow wheel to blue counterpart to make shadows neutral. Lower the Low Range (around 10%) so that midtones are not affected;
THEN mask out skin and other “warm elements” in the picture (in parallel node) and grade them into the opposite color - yellow-orange;
Use the Log wheels to balance the colors: make a soft bridge between warm skin and cold background by turning Shadow wheel to green a bit, Highlight to magenta;
Balance the footage colors, so the color work together, and not appear as isolated elements: one cheater method to do this is to radically darken the background. Since with teal&orange we get the maximum color contrast, it is better to make the light contrast stronger as well. Another method is to make subtle changes - bring the overcolored ares back by lowering saturation, for example.
Color Wheels → Color Boost (number in the below section) - increases/decreases the saturation of the MOST SATURATED colors in the image. Helps if you have some over-saturated colors which you want to level down, without touching the rest if the image.
Add Saturation to high values, then subtract Color Boost to remove color spills. Example: vegetables in a white glass bowl will pass their color to the bowl. To keep it white, subtract Color Boost.
Цветокоррекция и покрас плёночного кино
В плёночном кино грейдинг являлся этапом PRE-Production. Это связано с необходимостью подбора правильной плёнки, дающей нужную гамму цветов. Для проявки плёнки проявочные машины (пример - российская машина МПМ—16—3М), которые работали по заданному СТАНДАРТУ.
Videocard sends a signal to the monitor, divided in 2 components: COLORSPACE (color) and GAMMA (light)
COLORSPACE
Initial colorspace is a triangle-like model of all colors which are discriminated by the human eye (mathematical model):
Different color spaces encompass different sets of colors as compared to this model:
rec709 - came with digital TV, P3 - used for digital cinema theater, rec2020 - UHDTV / new 4K video standard.
GAMMA (Dynamic Range)
Digital cameras see the world in a linear fashion. If we have a room with 1 lamp and we bring in a second lamp, camera will register 2x increase of light. Human eye will register around +20% increase of light.
Transfer function - math function to compress the light data of an image and then use this file to restore the whole dynamic range seen by the human eye.
Human eye dynamic range: 10-13 stops active range, 24 stops total range
Film dynamic range: 13 stops
RED Weapon Camera: 16.5+ (incl HDRX-mode) stops
HLG: 12 stops
REC709: 6 stops
REC709 GAMMA 2.4 is the native format for DaVinci Resolve, Adobe Premiere etc. If you throw a different colorspace file in DaVinci, you must tell it what it is and convert: Color room → Library → ResolveFX Color → Color Space Transform.
Fir SONY SLog3 shoot with +2 or +3 F-stops higher to quell noise, then choose:
Always use the curved mask: it is the only one that has soft transitions, which can be changed individually for different sides:
Neural Masks
Davinci Resolve Studio 18+ brings Object Mask to select parts of a person.
Draw lines with dropper+ over face & other parts of a person to highlight them. Use dropper- to leave other objects out of the mask. Use Better quality with Refine Edge BEFORE calculating the mask.
You can see the mask using Corrector node.
Use Key Mixer node to combine several masks (you can input more than 2, links are added in Key Mixer node automatically upon connection), invert them using button in Key Mixer config.
Use the wheels: lower Gain, then create node and restore contrast;
Use the Curves: lower the curve in the highlight area, make “S” form to restore contrast;
In the Curves instrument, use Soft Clip section: Low / High restores shadows / highlighs, while Low Soft / High Soft makes a smooth transition - ideal for blown sky areas.
Controlled Restoring of Blown Highlights
Create a Parallel Node after the input together with the denoise node
Use Luminance Qualifier to select smoothly the bright part of the image;
Turn down Highlights to get the details back.
Parasite color in highlights
Typically such things appear when shooting on 8bit cameras. Use Lum Vs Sat Curve to fix.
Isolate highlights by luminosity;
Bring saturation level in highlights down to remove color.
global denoise to reach pristine look for selection
node for primaries
node for curves
basic secondaries + additional secondaries nodes
vignette node, then Add Node → Outside Node, then finally the global look
Color Grading Structure
Color Grading structure contents:
3 global corrector nodes: color balance, global exposition, misc global node (noise reduction?)
3+ local exposition corrector nodes: use masks for areas and add light, like lighting up the face
Parallel mixer with 3 inputs
3+ local color corrector nodes: use masks for areas and change saturation, like de-saturating items with bright colors, which may drag unwanted attention
Parallel mixer with 3 inputs
1 corrector node with a combination of LUTs to produce a special look (color grading for drama / atmosphere / mood goes here)
Parallel Nodes
Changes in parallel nodes are made to the image with the same intersection, as if the nodes were in sequence (Serial type). The difference is that all parallel nodes read data from one initial node, while in serial sequence the next node reads data from previous node with all corrections applied on the previous step.
Node Layers
Layers are parallel nodes, but work in opposite direction: lowest layer has the highest precedence. Layers can be composited using connecting node settings.
Node Sequence
Nodes sequence matters
Create 2 nodes: in the first darken image using a LIFT wheel. In the second node make it red with the LIFT wheel and raise Saturation to 100%;
Make a second version with nodes in reverse sequence;
See the difference: if darken is first, then more areas become red using the second node than vice-versa.
Tip
Instruments sequence also matters: in the Color Wheels section, Lift/Gamma/Gain wheels are applied BEFORE the general Offset wheel.
Contrast sequence matters
When you use a node to make stronger contrast on a footage, you basically stretch the image info between Dark & Light boundaries. If you create a second node after the “contrast creation” one, you will be changing the resulting stretched image data, and the changes will be more subtle and soft.
Хорошо работают при неоднородном фоне. На однородном они слишком очевидны
Делают видео более объёмным, отделяют передний и средний планы от заднего
Способ создания 1
OpenFX → ResolveFX Stylize → Vignette
Способ создания 2
Обычно виньетки применяются на уровне Timeline
Добавить новую ноду (ALT+S), добавить к ней круговую маску
Добавить outside node (ALT+O), с помощью колец Color Wheels опустить Gain и Saturation, в разделе Key снизить Key Output → Gain до 0.8-0.9, чтобы смешать с оригинальной картинкой
Способ 2 лучше чем 1, т.к при способе 1 по углам создаются чёрные области с растушёвкой, в то время как при способе 2 происходит “multiply” видеопотока на себя с коррекцией = такой способ даёт менее “грязный” результат.
Способ создания 3
Создать ноду Vignette;
Создать круговую маску на субъекте, с высокой мягкостью (Soft = 25);
Зайти в Curves, основную кривую опустить примерно по центру вниз;
Добавить outside node (ALT+O),
В этой ноде зайти в Curves, основную кривую немного поднять (либо создать обратную S-кривую, поднять лишь тени), чтобы высветлить субъекта.
Собрать видео и экспорировать из родной программы в 360VR-ProRes 422 формат. Поставить галочки FlowState, Lock Direction (для видео с рук или с дрона, для видео со штатива не надо).
Очистить видео от артефактов и увеличить до 8K. С помощью Topaz Video AI сделать 2 прохода:
Первый проход с алгоритмом Artemis DeHalo, размер 100%, добавить Grain (Size=1);
Второй проход с алгоритмом Gaia High Quality, размер 133,33%.
Подготовка проекта
Разрешение:
3840x1920 - для соц сетей, интерактивного просмотра с моб телефонов подойдёт
5760х2880 - для YouTube 6K показа
7680x3840 - для Youtube 8K показа
Проксирование (Optimized Media and Render Cache):
Proxy media resolution = Quarter
Кодек = на Apple это Prores 422
Указать папку для складывания видео Proxy.
Просмотр VR360
Положить видео VR360 на timeline. Далее перейти в комнату Fusion, нажать правой кнопкой на видео в окне и выбрать 360 View -> LatLog. После этого можно панорамировать с помощью ALT+средняя кнопка мышки.
Когда закончил панормирование, можно нажать правой кнопкой на видео в окне и выбрать Settings -> Reset Defaults
Для ускорения просмотра выбрать режим отображения Proxy и отключить высокое качество.
Также в главном меню выбрать Playback -> Timeline Proxy Resolution -> Half.
Смена направления на север и завал горизонта
Для начальной ориентации камеры нужно добавить ноду PanoMap. Далее перевести отображение обоих окон с видео на эту ноду. Выбрать просмотр в одном из окон 360 View -> LatLog. С помощью настроек PanoMap поменять ориентацию по осям.
При съёмке с дрона может быть завал горизонта: исправить можно путём анимации осей. XYZ в настройках PanoMap.
Редактировать - убрать штатив/дрона
Добавить в Fusion ноду Lat Long Patcher. По оси X направить чётко вниз/вверх, чтобы было видо основание штатива или дрон.
Добавить ноду Paint. Выбрать в окне видео Stroke, в настройках ноды Paint выбрать режим Clone, размер кисти большой и повысить гладкость. С помощью ALT обозначить точку для клонирования и закрасить крестовину штатива или дрон.
Скопировать ноду Lat Long Patcher после Paint, поменять у неё в настройках режим Mode=Apply. После этого добавить ноду Merge и соединить в неё вторую ноды MediaIn, Lat Long Patcher и результат вывести на Media Out. Объект должен пропасть в кадре.
Стабилизация VR360
В команте Fusion добавить ноду Spherical Stabilizer. Stabilize Strength = 1, Smooting = 1. Произвести трекинг всего видео.
У ноды Shape 3D установить Shape = Sphere и соединить в неё Media In.
Подключить вывод видео в окно просмотра в ноде Merge 3D;
Можно панорамировать в окне просмотра с помощью ALT+средняя кнопка мышки;
У ноды Shape 3D установить Radius = 10, чтобы “войти в сферу”, увеличить Base Subdivisions = 60 и выше, чтобы убрать полигональные артефакты;
Выделить ноду Render 3D, включить отображение видео в 1 окно. Обратить внимание, что точка обзора поменялась. Нажать CTRL+G, чтобы отобразить перекрестье и направляющие;
У ноды Spherical Camera во вкладке Transform по оси Y подвинуть ось камеры, чтобы установить её в нужном направлении;
Проверить, что включено низкое качество показа, Proxy, в главном меню выбрать Playback -> Timeline Proxy Resolution -> Quarter для ускорения Renderer 3D;
Добавить ноду Text 3D, написать текст, подключить к ноде Merge 3D;
Использовать направляющие перемещения и поворота для правильной ориентации текста по отношению к камере;
Перейти в настройках Text 3D во вкладку Transform, добавить ключи анимации по осям XYZ;
Включить в интерфейсе Fusion вкладку Spline, включить в ней все оси анимации, нажать CTRL+F для масштабирования графиков анимации в экране;
Сделать анимацию по времени, нажать на ключевые кадры и усиками Безье придать плавность анимации;
Соединить выход Renderer 3D с Media Out;
Отображение 2D текста и графики в 3D
Добавить ноды Text, Background, соединить их через ноду Merge, после чего внести в 3D через ноду Image Plane 3D;
В настройках Image Plane 3D -> Transform регулировать масштаб Scale = 3;
Можно таким же образом перестаскивать из Media Pool обычные видеоролики и подключать через Image Plane 3D.
Экспорт видео
Для 6К - 8К видео выбирать кодек H.265.
Битрейт Quality = 100000 Kb/s для большей плавности картинки.
Warning
Для шлема Oculus Quest 1 нужно указывать не более чем Quality = 60000 Kb/s.
Включить Advance Settings -> Force Sizing to highest quality, Force debayer to highest quality.
DaVinci Resolve не делает инъекции метаданных в видео, поэтому для публикации на Youtube нужно это сделать отдельно (для Facebook этого делать не нужно - можно просто закачать видео, и оно будет VR360).
Полезен «красно-зелёный TDD»: то есть стоит не просто «сначала написать все тесты», а действовать по такому циклу:
Новый тест сначала должен падать (иначе что он проверяет?)
Затем требуется сделать минимальное изменение кода, чтобы тест начал проходить.
А дальше надо рефакторить, превращая код из «просто срабатывающего» в «полноценный».
Пример промта:
Build a Python function to extract headers from a markdown string. Use red/green TDD.
❗ИИ нельзя позволять менять тесты (если считает это необходимым, то должен объяснить причину). А на случай, если это всё-таки произойдёт, в CI нужен hook, который блокирует PR с такими изменениями.
Spec-Driven Development (SDD)
Всё значимое требуется фиксировать. При этом можно быстро проверять гипотезы на практике и подстраиваться на ходу: “Сделай тут тремя разными способами, а мы сравним результаты “вживую” и выберем”.
Экономия контекста и токенов при чтениях
Краткий и точный контекст - это самое важное. Нужно давать агенту ровно ту часть системы, которая нужна для конкретного изменения.
Формулировать задачу узко: измени только валидацию в этом методе, работай в пределах этих двух файлов;
Структура проекта понятна и предсказуема: предсказуемые названия файлов, понятное разделение по слоям, внятные README.md;
Карта проекта и ограничения: где находится нужный модуль, что считается точкой входа, где лежит бизнес-логика, а где тесты или вспомогательные части. И можно сразу уточнять, какая часть карты актуальна для текущей задачи: какой модуль трогать, какие директории релевантны, а где не нужно ничего менять;
Фиксировать архитектурные правила отдельно: важные инварианты вроде «этот слой не ходит напрямую в БД», «этот модуль не импортирует этот пакет» или «новую логику добавляем только через такой паттерн» агенту лучше знать заранее;
Разделять поиск, понимание и изменение: сначала дать агенту задачу поиска релевантных файлов и посмотреть, не “увело” ли его. Затем задачу понимания: что именно в них отвечает за нужное поведение. “Если агент плохо справляется с задачей - попробуй её декомпозировать”.
Выбор стека: типизация и “стандартизация”
Cтатическая типизация позволяет поймать ряд ошибок ИИ.
Для LLM “удобнее”, когда что-то часто встречалось в обучающем датасете. Если на GitHub есть тысячи “Тетрисов” на JavaScript, то агенту будет легко сделать ещё один. Хуже обстоят дела с малоизвестными вещами и с совсем новыми, вышедшими уже после даты «knowledge cutoff» ИИ-модели.
Подход к Git
Общий совет: коммитить как можно чаще. Это как сохранения в видеоигре: никогда не знаешь, когда ИИ вдруг пойдёт вразнос, и чем свежее последний коммит, тем меньше прогресса можешь потерять. Плюс по commit messages, ИИ в будущих диалогах сможет понимать по ним много полезного контекста.
При этом ИИ очень помогает со «сложными командами». Даже опытным разработчикам непросто целиком уместить в голове логику и команды Git.
Риск-менеджмент
Когда ИИ быстро генерирует много кода, человеческое ревью становится одновременно “важным местом” и “узким местом”. От его тщательности зависят и качество проекта, и скорость разработки.
При работе с ИИ-кодом требуются:
Компетенции и прилежность тщательно проверить всё, что требуется;
Душевный покой принимать без излишних проверок то, где они не требуются;
Мудрость отличать одно от другого.
Понимание работы ИИ
Знание принцпов как работает ИИ: чем различаются модели? Как измеряют их эффективность? Какие формы обучения к ним сейчас применяют? Как работает кэширование токенов? Как устроен «режим планирования»?
Agent Skills лучше MCP, потому что не загружаются в контекст сразу, а только по необходимости;
для верного выполнения задачи машине нужен весь релевантный контекст;
при этом окно контекста не бесконечное, да и то в середине “проседает”;
поэтому не стоит «закидывать в ИИ всё возможное», надо “не мусорить”;
фича «compaction» может «сжимать имеющийся диалог до главного»;
но модель сама выбирает «а что главное», и может упустить важное.
Понимание программирования
Все абстракции “протекают”. С ИИ, если хочется делать масштабные вещи, необходимость понимать «уровни ниже» совершенно не исчезла. Наоборот, к ним добавился ещё один. Значит, чтобы добиться от ИИ профессионального кода, надо самому быть профессионалом.
Обучение программированию
Главная проблема с LLM — они могут сразу давать правильный ответ, пропуская путь к нему. Значит, обучение должно быть выстроено так, чтобы не пропускать.
ИИ может быть полезен в объяснении «обычного» программирования.
Файл с инструкциями для ИИ глобального уровня или уровня проекта.
Размер: минимальное число строк. При большом размере (>300 строк текста) ИИ начинает игнорировать весь текст.
Команды
Shortcuts
ESCx2 - очистить промпт
Shift+Tab - выбрать режим (с подтверждением действий, planning mode, auto-edit)
ALT+Enter - добавить новую строку, не отправляя промпт на исполнение
Полезные команды
/model - выбор модели (Sonnet, Opus, Haiku)
/clear - очистить контекст
/context - посмотреть на текущий контекст
/compact - запустить процесс суммаризации контекста (Claude делает это и сам на автомате)
/resume - переоткрыть старый контекст, если случайно закрыл сессию с Claude
/mcp - отобразить текущие MCP. Нужно стараться ограничивать их число, чтобы не взрывать контекст лишней инфой
/help - справка по командам
/permissions - список разрешений для Claude Code агента
/chrome - команда для сходить в браузер, сделать скриншот и найти что-то (без API)
Расширение
Skills
Skills = Commands.
Скиллы - это краткое описание для ИИ, как решить задачу.
Технически это .md-файл с текстом, который вызывается также как команда по /
Хорошая привычка: всегда просить Claude самостоятельно обновлять и управлять Skills, не менять их вручную.
Rust Token Compressor (RTK) - https://github.com/rtk-ai/rtk сокращает количество используемых токенов для bash-операций, особенно с git. Подменяет с помощью global hook обращения ИИ и сокращает вывод;
I'm designing a CLI tool that helps developers track TODO comments across their codebase. Create an agent team to explore this from different angles: one teammate on UX will use Haiku model, one on technical architecture using Sonnet, one playing devil's advocate will use Opus.
Настройки teams хранятся:
Team config: ~/.claude/teams/{team-name}/config.json
Задачи информационной безопасности (ИБ) - обеспечить конфиденциальность, целостность, доступность (Confidentiality, Integrity, Availability - CIA triad) в информационной системе (ИС);
Угрозы ИБ (threat) - потенциальные опасности ИС, если нарушитель использует её уязвимости для атак. Угрозы бывают трёх видов: нарушение конфиденциальности, целостности, доступности;
Уязвимости (vulnerability) - недостатки ПО, оборудования или в мерах по обеспечению безопасности с точки зрения человеческого фактора, которые дают возможность злоумышленнику проникнуть в ИС и совершать там противоправные действия (реализация угрозы);
Источник угрозы (threat agent) - хакер, недобросовестный или ошибшийся сотрудник, через которого произошла реализация угрозы, повреждение ИС, утечка данных.
Каждая уязвимость получает ранг на основе факторов:
Какие системы затронуты;
Какие данные в опасности;
Какие бизнес-функции находятся под угрозой;
Насколько легко реализовать атаку и добиться компрометации ИС;
graph LR %% TD = Top->Down, LR = Left->Right etc.
S[Start] --> A;
A(Enter your EMail) --> E{Existing user?};
E -->|No| Create(Enter name)
E -->|Yes| Send(Send a letter to address)
Create --> EULA{Accept conditions}
EULA -->|Yes| Send
EULA -->|No|A
Result:
graph LR
S[Start] --> A;
A(Enter your EMail) --> E{Existing user?};
E -->|No| Create(Enter name)
E -->|Yes| Send(Send a letter to address)
Create --> EULA{Accept conditions}
EULA -->|Yes| Send
EULA -->|No|A
Sequence Diagrams
Example:
sequenceDiagram
autonumber %% action numbers placed on every arrow
actor C as Client
Note left of C: User %% [ right of | left of | over ] supported
participant I as Identity Provider
participant S as Service Provider
Note right of S: Blitz Identity
C->>S: Resource request
activate C
activate S
S-->>C: Redirect to Identity Provider
deactivate S
loop every hour %% loop start
C->>I: Request Access Token
activate C
activate I
I-->>C: Access Token
deactivate C
deactivate I
end %% loop end
C->>S: Access granted
Note over C,S: Browser access
deactivate C
Result:
sequenceDiagram
autonumber
actor C as Client
Note left of C: User
participant I as Identity Provider
participant S as Service Provider
Note right of S: Blitz Identity
C->>S: Resource request
activate C
activate S
S-->>C: Redirect to Identity Provider
deactivate S
loop every hour
C->>I: Request Access Token
activate C
activate I
loop
I->>I: Kerberos cert
end
I-->>C: Access Token
deactivate C
deactivate I
end
C->>S: Access granted
Note over C,S: Browser access
deactivate C
Чтобы немного упростить процесс ведения дневника, можно поставить плагин календаря (Calendar), периодических заметок (Periodic notes) и шаблонов (Templater). Логика думаю тут довольно проста – календарь позволяет лучше ориентироваться во времени, периодические заметки - организуют заметки, а шаблоны упрощают создание самих дневниковых заметок.
Сначала мы быстро читаем какой-то отрывок, главу. Не отвлекаемся, не тормозим, не ходим по ссылкам.
Далее читаем заново и оставляем метки, по которым сформируем впоследствии конспект.
Пишем конспект:
Сначала пишем просто изложение (значит своими словами). Не думаем о ссылках, не думаем о том, как атомизировать. Просто пишем четкое последовательное изложение по прочитанному.
Расширяем и дополняем конспект своими мыслями и наблюдениями, вставляем ссылки и источники.
(при необходимости формируем какой-то сопроводительный текст)
Начинаем атомизировать наш конспект на отдельные заметки.
Связываем заметки с другими, если, конечно, в голову приходят эти связи.
Медитируем
Okular, потому что эта программа открывает pdf и djvu (и многие другие), а также в ней прям очень легко и быстро можно аннотировать текст.
Zotero
Это ультимативная, бесплатная программа для работы с источниками информации
Obsidian to Anki.
Плагины
ReadItLater - способ быстро добавлять статьи и видео в заметки;
Timestamp Notes - смотреть видео и делать заметки по времени;
Кварц позволяет открыть свои markdown-страницы для других людей через веб-сайт.
Есть даже целая концепция learn in public, когда люди открывают аудитории свои незавершённые наработки с целью эффективного обучения. Закон Каннингема гласит:
Лучшим способом получить правильный ответ в Интернете будет не задавать вопрос, а разместить ложный ответ
Кстати, статьи, которые я пишу для своего телеграм канала тоже написаны в Обсидиан и опубликованы через Quartz.
Для того, чтобы их можно было удобно читать, не выходя из приложения, я написал шаблон для telegram instant view.
Top Plugins
Calendar
Позволяет создать календарь и писать в него быстрые заметки по дням.
DataView
Позволяет делать выборки из файлов с SQL Syntax и отображать результат в виде таблицы
Outliner
Создание и управление структурированными списками.
Novel Word Count
Отображение количества страниц и текста в папке.
Style Settings
Изменить стиль Obsidian, сделать Softpaper стиль.
Установить стиль (Theme) = AnuPpuccin
Зайти в настройки плагина Style Settings, выбрать конфиг для AnuPpuccin темы -> Import… и ввести:
The JSON document below is a message history from a Telegram group chat.
I need you to summarize this chat history and yield 5 primary conversation topics.
Each conversation topic mentioned should be accompanied by one-sentence summaries of 2-3 most representative dialogs (not single messages) from the conversation on the given topic including usernames.
For each dialog summary provide the exact keywords with which the message can be found in the history using text search.
IMPORTANT: The output should be provided in the language which prevails in the messages text.
Here's an example of desired output in Russian language (follow the exact structure):
1. <b>Изменения в политике открытия счетов в Испании для россиян по паспорту гражданина РФ без получения ВНЖ. Обсуждаются новые ограничения, введенные в начале 2024</b>.
Примеры сообщений:
- <b>Bolzhedor рассказывает</b> о неудачной попытке открытия счета по паспорту РФ в банке Caixa. Ключевые слова: "<i>завернули с паспортом</i>", "<i>больше никому не открывают :(</i>".
- <b>Александр Сергеевич</b> отмечает, что единственный банк, до сих пор открывающий счета россиянам по паспорту - это BBVA. Ключевые слова: "<i>BBVA пока разрешает</i>", "<i>главное дружить с хестором</i>".
2. <b>Изменения в политике налогообложения России и Испании в 2024 году</b>.
Примеры сообщений:
- <b>Себастьян Перейро</b> и <b>Max</b> обсуждают изменения в налоговом законодательстве и влияние налогового резидентства на обязательства. Ключевые слова: "<i>нерезидентам сейчас хуже всего</i>", "<i>кто попался на непредоставлении?</i>", "<i>зачем вообще об этом сообщать/<i>".
- <b>Akakij M</b> и <b>Олег</b> делятся опытом и советами по вопросам налогообложения и требованиям налоговых органов. Ключевые слова: "<i>Будут спрашивать - скажете</i>", "<i>у меня пока ничего не просили</i>".
3. <b>Судебные приставы и исполнение налоговых требований: пользователи делились опытом взаимодействия с судебными приставами и налоговыми органами, включая случаи неправомерного списания средств</b>.
Примеры сообщений:
- <b>Маша К</b> рассказывает о своем опыте с неправомерным списанием средств и последующим взысканием через суд. Ключевые слова: "<i>по судам затаскают</i>".
- <b>Любитель Бокса</b> упоминает о списании штрафов с нескольких счетов одновременно. Ключевые слова: "<i>уж не знаю как, но нашли</i>".
4. <b>Вопросы по открытию и пополнению счета для получения студенческой визы</b>.
Примеры сообщений:
- <b>Родион Раскольников</b> ищет информацию о том, как показать на счете 1337€ для студенческой визы, учитывая ограничения на пополнение счета в Nickel. Ключевые слова: "<i>студенческая виза</i>", "<i>leet</i>", "<i>1337€</i>".
- <b>Kusswurm</b> предлагает пополнение через Bank для обхода лимитов Nickel. Ключевые слова: "<i>пополнение через Bank</i>", "<i>обход лимитов Nickel</i>".
5. <b>Обсуждение возможности использования банковских услуг для нерезидентов и резидентов с TIE</b>.
Примеры сообщений:
- <b>Александр</b> спрашивает о переводе средств из РФ в Испанию, будучи нерезидентом без резиденции. Ключевые слова: "<i>вывод средств</i>", "<i>РФ в Испанию</i>", "<i>нерезидент</i>".
- <b>Жулик Обманщик</b> предлагает привезти наличные, а также упоминает о наличии людей, заинтересованных в обмене рублей на евро. Ключевые слова: "<i>привезти наличные</i>", "<i>рубли на евро</i>".
Here's the JSON document:
{text_to_summarize}
Habr Channel:
The JSON document attached is a message history from a Telegram channel chat.
I need you to summarize this channel history and for all days yield 4 primary topics for each day connected to DevOps Kubernetes and Linux commands, only Rust language programming from programming languages, DIY topics, network and VPN, Ai LLM, HR and psychology, Data Security, Sysadmin tools topics. Every day must have this summarization separately.
For posts named "Новости к этому часу" provide each news line inside this post as is with an accompanying URL link.
IMPORTANT: For each summary provide the href URL link (in clickable format) and the date connected to the channel post which was summarized. The output should be provided in the language which prevails in the post message text.
Other Channel:
The JSON document attached is a message history from a Telegram channel chat.
I need you to summarize this channel history and for all days primary topics for each day. Every day must have this summarization done separately.
IMPORTANT: If a post has a URL link, then add it to the summarization (in clickable format) and also add the date connected to the channel post which was summarized. The output should be provided in the language which prevails in the post message text.
Метаобучение. Дорожная карта предмета изучения. Как использовать свои текущие навыки, чтобы легче освоить новый. Перед изучением темы, разобраться с предметом изучения:
* Зачем учить, что учить, как учить? Написать в таблице;
* Интервью с экспертом: найти спеца, кто достиг высот в теме, которую планируешь изучать, и пообщаться - насколько ему нравится, помогают знания эти, как он учился;
* Выделить, что надо понимать, что просто запомнить (факты), что надо отработать;
* Закон убывающей ценности: поиск источников знаний - это процесс, ценность начальных источников будет убывать, надо будет искать новые.
* Эталонный сопоставительный анализ: взять программу курса (например, программу ВУЗа), и по ней следовать как по дорожной карте;
* Метод подчеркивания/исключения: учить тему, постоянно взвешивая её ценность. Например, изучение программирования для создания проекта - это учить конкретные языки, но не теорию вычислений;
* Сколько исследовать тему проекта перед его началом - 10% от времени на проект.
Фокус: выделение времени на интенсивное изучение. Способность концентрироваться, бороться с прокрастинацией:
* Определить причину прокрастинации: сопротивление конкретному делу или более привлекательные альтернативы?
* Поток от Михай Чиксентмихайи не годится (согласно Андерсону Эриксону) для обучения сложным навыкам. Потому как в потоке нет места для осознанной рефлексии. Также обучение - это уровень сложности более высокий, чем годится для потока. Человек к потоке занимается обычно делом, которое хорошо знает;
* Техника “я ещё 5 минут поделаю и пойду отдыхать” для начала борьбы с прокрастинацией, далее техника помидорок для концентрации.
Целенаправленность - идти вперёд, не заменять обучение другими проектами попроще. Фокус на практической значимости изучаемого навыка:
* Делать pet-project;
* Погружение в среду (языковую);
* Работа с симулятором (например, у лётчиков);
* Не бояться ставить высокие сложные задачи.
Упражнения. Атаковать свои слабые места безжалостно. Разбить большие задачи на мелкие, освоить их и собрать вместе. В процессе разбора на части и изучения подзадач могут найтись доселе неизвестные вспомогательные навыки. Например, задача изучения языка включает задачи скоростного и эффективного использования словарей, переводчиков. Среди эти под-задач могут быть “бутылочные горлышки”, из-за которых в целом изучение навыка тормозит. На них унужно сфокусироваться до их улучшения;
Самоконтроль. Вспоминать изученное активно через задачи и тесты, а не пассивно, заглянув в источник. Активно вспоминать, в том числа тестировать себя ДО изучения темы;
Обратная связь. Принимать критику на себя, не избегать её (но фильтровать полезную, остальную игнорировать). В том числе тренер/наставник даёт 1-1 обратную свзяь, отчего занятия с ними так эффекктивны;
Запоминание. Учиться помнить инфу навсегда. Метод интервального повторения;
Интуиция. Углубление знаний через игру. Усвоить как работает понимание, не прибегать к дешёвым трюкам запоминания;
Экспериментирование. Исследовать вне своей зоны комфорта.
Прошлые события
30.12.2024
Для анализа данных о системе из прошлой статьи нужно отфильтровать всё лишнее с помощью регулярных выражений.
26.12.2024
В качестве пробного приложения я пробую container runtime. Такое приложение сразу порождает кучу практичных вопросов. Например, вопрос подходящего ядра Linux. Чтобы узнать его версию, нужно быстро опросить систему, для этого я добавил кусочек библиотеки sysinfo. Есть и другой способ узнать версию ядра и тип ОС: uname -a && cat /etc/issue. Для этих целей можно запустить команду в STDIN поток ОС.
25.12.2024
Сегодня день посвящён работе с ошибками в Rust. У меня ранее уже была статья об этом, пора в её неё добавить деталей, а также разобрать специализированную библиотеку Anyhow для упрощения работы с ошибками.
24.12.2024
Механизм логгирования и трассировки позволяет отслеживать работу приложения максимально плотно. Есть несколько инструментов для сбора логов, но наиболее эффективный из них tokio tracing, так как корректно обрабатывает трассировку потоков многопоточных и многоканальных приложений. Разбор работы с ним теперь в статье про tracing. Общий посыл в том, что можно писать лог с функций любым логгером, и это вполне подходит - до тех пор, пока не начать писать многопоточное, многоканальное приложение. Как только появляется вязанка потоков, каждый из которых создаёт события, типовой логгер начинает сбоить, т.к лог события с одного потока перекрывает лог с другого потока. И вот, чтобы корректно с этим работать, логгер уже не пишет просто события, а создаёт “промежуток потоковой трассы событий”, т.е набор событий в контексте работы каждого потока, при этом запись этих событий должна быть как неблокирующее поток вывода действие, и отправляться оно должно необязательно в файл, а есть поддержка сразу отправки в SIEM, анализатор типа Datadog и т.д. в режиме дозаписи (rolling). Т.е логгер одновременно делает трассировку, отдачу телеметрии - с заворачиванием в стандарт OpenTelemetry по желанию, и многопоточную передачу этих данных в один или несколько приёмников: одновременно в файл с ротацией, SIEM, облако и т.д. Круто же!
23.12.2024
Долго я не понимал прелести работы библиотеки Rust clap по работе с аргументами командной строки. А потом как понял! Всему виной громоздкие документация и примеры. Начинать надо с малого, что я и сделал в своей статье про clap.
21.12.2024
Наконец-то я залезаю в многопоточность приложений. Тема непростая, интересная. Я добавил статью про работу с процессорными потоками в Rust, буду дополнять её далее.
Полезная утилита tokei для подсчёта строк кода на Rust и других языках
02.12.2024
Наконец-то, массовый апдейт сайта! После обновления движка Hugo у меня были проблемы с занесением сюда новых заметок, но теперь это в прошлом.
06.08.2024
Я делаю новый подход к снаряду с Kubernetes. Попутно я заметил, что не сделал заметок по пространствам имён - исправляю этот недочёт.
24.04.2024
Недавно я разбирался с хабом для умного дома на базе OpenWRT и озадачился кросс-компиляцией утилит под него. А следом у меня возник вопрос о кросс-компиляции проектов Rust. Оказалось, для этого есть модуль Cross, который через контейнер Docker/Podman +Wine способен скомпилировать код на моём макбуке под Windows. Заметку об этом я добавил по ссылке.
30.03.2024
Множество мелких добавок-правок не попадают в новости, но что-то я периодически дописываю на портале. Из недавнего-интересного - чтение файлов в Rust. Чтение в строку, через буфер, в вектор из байт для бинарных файлов. В перспективе я думаю разобрать парсинг спец-форматов: XML, JSON, YAML, TOML. 03.02.2024
Ещё один элемент стиля Rust - повсеместное использование замыканий в самых разных хитрых комбинациях. Это сокращает количество кода, при этом сохраняет скорость выполнения. Прицепом я добавил в тему словарей инициализацию со значениями. В Python аналог defaultdict был полезной штукой, местный аналог в Rust тоже нужно было записать.
01.02.2024
В процессе решения задачек я потихоньку прихожу к пониманию, что такое “смена мышления” при работе с Rust относительно других языков: это упор на типы данных и конструкции языка заместо привычных общепринятых. Например, использовать перечисления (enum) и match всегда и везде заместо if-else. Попутно я дополнил статью о векторах темой перевода туда-обратно в строки. Для составных типов данных это наиболее актуальный вопрос: как собрать в такой тип данные, а потом отдать данные назад в правильном формате.
28.01.2024
Этот год я решил вернуться к Rust после перерыва и научиться решать задачки с ним в codewars. В процессе я потихоньку осваиваю расширенную библиотеку, например, модуль itertools тут есть, как и в Python, даёт возможность делать полезные преобразования и сортировку строк.
11.11.2023
Неожиданное открытие на днях: давно я искал RAW-редактор фото а-ля Lightroom, но при этом бесплатный-открытый, и чтобы при этом умел работать с масками, а вдобавок делать много проб под кривые коррекции в LAB. И такой оказался у меня прямо под носом - Darktable. Я иногда им пользовался, но совершенно проморгал наличие там мощного механизма работы с масками, а вдобавок мощнейших функций шумодава и микроконтраста. Навёрстываю упущенное, создал раздел в заметках и конспект по шумодаву с масками
09.11.2023
Подборка полезных/интересных фреймворков для Rust:
20.10.2023
Формат “стены” - небольших новостей-блогов - мне показался более интересным для главной страницы сайта. В связи с этим я буду тут писать небольшие заметки, которые далее перемещать в новый раздел новостей сайта.
Позавчера мы обсудили с коллегами от Kaspersky Labs, VK Cloud и Positive Technologies перспективные технологии на рынке ИБ. Особый упор на ePBF и WebAssembly. Ссылку на стрим я добавил в список своих видео выступлений