Rust
2.2K subscribers
148 photos
101 videos
2 files
201 links
Полезный контент по программированию на Rust
Download Telegram
Media is too big
VIEW IN TELEGRAM
Rust Tutorial Full Course

0:00 Intro
01:24 Create Project
02:43 TOML
02:54 Cargo.lock
03:45 Use / Libraries
04:30 Dependencies
06:00 Main
07:54 Mutable
09:28 Input
09:57 Expect
12:28 Variables
12:34 Constant
14:03 Shadowing
15:40 Data types
19:32 Math
23:03 Random
24:08 If
26:20 Ternary Operator
27:48 Match
32:39 Arrays
34:30 Loop
37:11 While
38:33 For
39:20 Tuples
41:27 Strings
50:25 Casting
51:52 Enums
55:55 Vectors
1:00:00 Functions
1:07:42 Generic
1:11:25 Ownership
1:20:09 HashMaps
1:24:50 Struct
1:27:54 Trait
1:34:14 Modules
1:45:36 Error Handling
1:47:14 File IO
1:48:11 Result
1:53:28 ErrorKind
1:55:40 Iterators
1:58:10 Closures
2:05:50 Smart Pointers
2:06:51 Box
2:15:03 Concurrency
2:17:10 Thread
2:25:26 Rc T
2:33:02 Installation

#RustTutorial #Rust #RustProgramming
👍5
🦀 Error Handling: Библиотеки против Приложений

Часто вижу в код-ревью кашу из способов обработки ошибок. Давайте раз и навсегда зафиксируем базу, чтобы ваш код был идиоматичным.

Есть два лагеря, и вам нужно быть в обоих, но в разное время:

1. Вы пишите Библиотеку?
Используйте thiserror.
Почему: Вашим пользователям важно матчить ошибки. Им нужно знать, что именно пошло не так (NetworkError, ParseError), чтобы программно на это отреагировать. Вы не должны навязывать им тяжелые трейты.

#[derive(thiserror::Error, Debug)]
pub enum MyLibError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid header")]
InvalidHeader,
}




2. Вы пишите Приложение (CLI, Backend)?
Используйте anyhow.
Почему: Вам чаще всего плевать на тип ошибки в глубине стека, вам важно прокинуть её наверх (main), залоггировать контекст и упасть (или отдать 500-ку).

use anyhow::{Context, Result};

fn main() -> Result<()> {
let config = std::fs::read_to_string("config.toml")
.context("Не удалось прочитать конфиг")?;
Ok(())
}



Итог: В библиотеках даем структуру (thiserror), в приложениях собираем контекст (anyhow). Не смешивайте.

#rust #tips #error_handling

👉 @rust_lib
👍22👎1🥰1💩1
👻 Сила пустоты: Zero-Sized Types (ZST)

Мы часто говорим про "zero-cost abstractions", но редко задумываемся, как это выглядит в памяти. ZST - это типы, которые занимают 0 байт. Компилятор знает о них, но в рантайме они исчезают.

Зачем это нужно, кроме экономии памяти? Для управления состоянием на уровне типов.

Представьте, что у вас есть стейт-машина. Вместо того чтобы хранить enum State и делать проверки в рантайме:


struct Socket<State> {
inner: FileDesc,
_marker: PhantomData<State>, // 0 байт
}

// ZST-маркеры
struct Connected;
struct Disconnected;

impl Socket<Disconnected> {
fn connect(self) -> Socket<Connected> {
// Логика подключения...
// Трансформация типа без оверхеда в памяти
unsafe { std::mem::transmute(self) }
}
}

impl Socket<Connected> {
fn send(&self, data: &[u8]) { ... }
}



В чем профит?

1. Вы физически не можете вызвать метод send у Disconnected сокета. Код просто не скомпилируется.
2. В скомпилированном бинарнике нет никаких флагов состояния, if state == connected и прочего мусора. Метод просто вызывается.

Используйте систему типов, чтобы делать некорректные состояния невыразимыми (Make invalid states unrepresentable).

#rust #advanced #architecture #zst

👉 @rust_lib
14🔥8
🛠 Cargo Expand: Загляни под капот макросам

Признайтесь, у вас бывало такое: навесили #[derive(Serialize)] или сложный макрос из sqlx, получили странную ошибку компиляции и сидите в ступоре?

Макросы это круто, но это черный ящик. Чтобы превратить его в прозрачный, поставьте cargo-expand.


cargo install cargo-expand



Запускаем:


cargo expand



И видите весь тот ужас (или красоту), который генерируется до того, как код попадет к компилятору. Это маст-хэв тулза при отладке async трейтов (привет, async-trait крейт) и понимании того, как работает "магия" фреймворков типа Actix или Rocket.

P.S. Только не пугайтесь, когда увидите, во что разворачивается println!.

#rust #tools #cargo #macro

👉 @rust_lib
👍141🔥1🥰1👻1
🐮 Cow: Ленивое клонирование

Все знают: лишний clone() - это грех. Но иногда нам нужно вернуть строку, которая может быть изменена, а может и нет.

Допустим, мы чистим инпут от спецсимволов.

1. Если спецсимволов нет - зачем аллоцировать новую строку? Можно вернуть ссылку на исходную.
2. Если есть - придется создать новую String.

Здесь на сцену выходит std::borrow::Cow (Clone on Write).


use std::borrow::Cow;

fn sanitize(input: &str) -> Cow<str> {
if input.contains('!') {
// Аллокация происходит только тут
Cow::Owned(input.replace('!', "?"))
} else {
// Тут мы просто возвращаем ссылку. Zero allocation.
Cow::Borrowed(input)
}
}



Это мощнейший инструмент для хот-пасов (hot paths), где 90% данных не требуют изменений. Не ленитесь использовать Cow там, где можно избежать лишних аллокаций.

#rust #performance #std

👉 @rust_lib
👍315🥰1😁1🤗1
🤠 Трюк Индианы Джонса: std::mem::take

Типичная ситуация: у вас есть &mut self, и вы хотите забрать поле (например, String или Vec), что-то с ним сделать, и, возможно, вернуть обратно или сохранить измененную версию.

Компилятор бьет по рукам: нельзя просто так переместить (move) значение из-за мутабельной ссылки. Новички часто делают .clone(), чтобы успокоить borrow checker. Но это лишняя аллокация!

Решение: std::mem::take.

Оно забирает значение из ссылки, оставляя на его месте Default::default().


struct Buffer {
data: Vec<u8>,
}

impl Buffer {
fn process(&mut self) {
// Мы "выкрадываем" вектор.
// В self.data теперь пустой Vec (без аллокаций).
let raw_data = std::mem::take(&mut self.data);

// Тяжелая обработка в другом потоке/функции,
// требующая владения (ownership)
let processed = heavy_transform(raw_data);

// Возвращаем результат
self.data = processed;
}
}



Это работает для всего, что реализует Default (Option, String, Vec). Для типов без Default есть std::mem::replace.

Это zero-cost, семантически верно и намного быстрее клонирования.

#rust #std #optimization #tips

👉 @rust_lib
🔥25👍86🥰1😁1🤗1
Почему Vec::new() это ловушка для производительности.

📈 Векторный рост: Избегаем реаллокаций

Мы часто пишем:


let mut vec = Vec::new();
for item in heavy_iter {
vec.push(item);
}



Что происходит под капотом?

1. Вектор создается пустым (0 байт).

2. Первый push: аллокация на 4 элемента (условно).

3. Пятый push: места нет.
• Создается новый буфер (размером x2).
• Все старые элементы копируются туда.
• Старый буфер освобождается.
• Новый элемент записывается.



Это называется Reallocation. Это дорого. Если у вас 10 миллионов элементов, вы сделаете десятки реаллокаций и скопируете гигабайты данных просто так.

Решение: Если вы хотя бы примерно знаете размер - подскажите компилятору.


// Идеально, если знаем точно
let mut vec = Vec::with_capacity(exact_count);

// Или используем итераторы, они часто сами знают свой размер
let vec: Vec<_> = heavy_iter.collect();



collect() умный. Он смотрит на size_hint итератора и сразу аллоцирует нужный буфер (если итератор "честный").

Правило: Vec::new() - только если вы правда не знаете, будет ли там хоть один элемент. В остальных случаях - with_capacity.

#rust #performance #vectors

👉 @rust_lib
👍285🥰2😢2🤗1
⚡️ Забудь про RefCell для примитивов

Вам нужно изменить поле внутри неизменяемой структуры (&self).
Рука тянется к RefCell? Стоп.

RefCell имеет оверхед:

1. Хранит счетчик заимствований (borrow count).
2. В рантайме проверяет правила (может паниковать!).

Если ваш тип реализует Copy (числа, bool, маленькие структуры) - используйте Cell.

В чем магия Cell?
У него нет никакого рантайм-оверхеда. Вообще. Он не проверяет заимствования.
Он работает так: "Я даю тебе копию значения" (get) или "Я заменяю значение целиком" (set). Вы не можете получить ссылку (&) на содержимое Cell, поэтому правила Rust не нарушаются.


use std::cell::Cell;

struct Metric {
count: Cell<u64>, // Zero overhead
}

impl Metric {
fn inc(&self) {
// Мы меняем значение по неизменяемой ссылке &self
let current = self.count.get();
self.count.set(current + 1);
}
}



Это атомарно? Нет. Это работает только в одном потоке. Но для однопоточного кода (или внутри thread_local) это самая быстрая мутабельность, которая только возможна.

#rust #std #optimization #interior_mutability

👉 @rust_lib
👍1361🥰1🤗1
🗝 Турбируем HashMap

Стандартный std::collections::HashMap в Rust использует алгоритм хэширования SipHash.
Почему? Он криптографически стоек к HashDoS атакам (когда злоумышленник подбирает ключи так, чтобы все они попадали в один бакет, превращая мапу в связный список и убивая CPU).

Но SipHash медленный. Если вы решаете алгоритмические задачи, пишете геймдев или у вас внутренний сервис без внешнего доступа - вам не нужна защита от DoS. Вам нужна скорость.

Используйте сторонние хэшеры. Самые популярные:

1. FxHash (rustc-hash) - используется внутри самого компилятора Rust и Firefox. Молниеносно быстрый для коротких ключей (integers).
2. AHash - современный, быстрый, использует аппаратные инструкции AES.

Пример с крейтом rustc-hash:


use rustc_hash::FxHashMap;

// Это та же HashMap, но с быстрым хэшером
let mut map: FxHashMap<u32, &str> = FxHashMap::default();



Прирост производительности на операциях вставки/поиска может достигать 2x-3x на простых ключах.

Итог:

• Backend наружу? Оставляем дефолтный SipHash.
• GameDev / Algo / Internal Data Processing? Ставим FxHash или AHash.

#rust #performance #hashmap #external_crates

👉 @rust_lib
👍175🥰2🏆1
🔮 Ты не пройдешь: Магия предсказателя ветвлений

Вы когда-нибудь задумывались, почему обработка отсортированного массива чисел происходит в разы быстрее, чем случайного, даже если логика одна и та же?

Всё дело в конвейере (pipeline). Процессор выполняет инструкции не по одной, а потоком: пока одна декодируется, другая уже выполняется. Но тут появляется if (ветвление).

Процессор не знает, пойдет код в then или в else, пока не вычислит условие. Но ждать он не может - конвейер встанет. Поэтому он угадывает.

• Угадал? Выполнение продолжается без задержек.

• Не угадал? Происходит Pipeline Flush. Процессор выбрасывает все инструкции, которые успел "набрать" по неверному пути, и начинает заново с правильного адреса. Это огромная потеря тактов (10-20 циклов CPU).

Пример на Rust:


// Если data отсортирован: T T T T T F F F F F (паттерн ясен)
// Если data случайный: T F T T F F T F (паттерн непредсказуем)
for &x in &data {
if x > 128 {
sum += x;
}
}



На случайных данных BPU ошибается в 50% случаев. На сортированных почти никогда.

Вывод: Чем предсказуемее ваши данные, тем быстрее работает ваш код. Иногда data.sort() перед обработкой окупается с лихвой.

#rust #cpu #lowlevel #performance #branch_prediction

👉 @rust_lib
👍16👎3