From 717aed59c8876a7f4b7c45befb1cd44fe42179c8 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 07:16:54 +0400 Subject: [PATCH 01/18] Translate Type-Driven Correctness book to Russian Translate all prose, headings, tables, diagram labels, code comments and user-facing strings in type-driven-correctness-book/src into Russian. Code identifiers, file names, links and code structure are unchanged; Rust compiler output quoted in comments is kept in English. Co-Authored-By: Claude Haiku 5.5 --- type-driven-correctness-book/src/SUMMARY.md | 48 +- .../src/ch00-introduction.md | 161 ++-- ...h01-the-philosophy-why-types-beat-tests.md | 115 ++- ...yped-command-interfaces-request-determi.md | 283 ++++--- ...ingle-use-types-cryptographic-guarantee.md | 263 +++---- ...apability-tokens-zero-cost-proof-of-aut.md | 244 +++--- ...rotocol-state-machines-type-state-for-r.md | 271 +++---- ...imensional-analysis-making-the-compiler.md | 197 +++-- ...alidated-boundaries-parse-dont-validate.md | 744 +++++++++--------- ...apability-mixins-compile-time-hardware-.md | 171 ++-- ...h09-phantom-types-for-resource-tracking.md | 211 +++-- ...utting-it-all-together-a-complete-diagn.md | 161 ++-- .../ch11-fourteen-tricks-from-the-trenches.md | 741 ++++++++--------- .../src/ch12-exercises.md | 378 +++++---- .../src/ch13-reference-card.md | 207 +++-- .../src/ch14-testing-type-level-guarantees.md | 196 +++-- ...onst-fn-compile-time-correctness-proofs.md | 510 ++++++------ ...nd-sync-compile-time-concurrency-proofs.md | 300 +++---- .../src/ch17-redfish-applied-walkthrough.md | 465 +++++------ .../src/ch18-redfish-server-walkthrough.md | 546 ++++++------- 20 files changed, 2931 insertions(+), 3281 deletions(-) diff --git a/type-driven-correctness-book/src/SUMMARY.md b/type-driven-correctness-book/src/SUMMARY.md index 2730326..a6c899a 100644 --- a/type-driven-correctness-book/src/SUMMARY.md +++ b/type-driven-correctness-book/src/SUMMARY.md @@ -1,35 +1,35 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I — Foundations +# Часть I — Основы -- [1. The Philosophy — Why Types Beat Tests](ch01-the-philosophy-why-types-beat-tests.md) +- [1. Философия: почему типы лучше тестов](ch01-the-philosophy-why-types-beat-tests.md) -# Part II — Core Patterns +# Часть II — Базовые паттерны -- [2. Typed Command Interfaces — Request Determines Response](ch02-typed-command-interfaces-request-determi.md) -- [3. Single-Use Types — Cryptographic Guarantees via Ownership](ch03-single-use-types-cryptographic-guarantee.md) -- [4. Capability Tokens — Zero-Cost Proof of Authority](ch04-capability-tokens-zero-cost-proof-of-aut.md) -- [5. Protocol State Machines — Type-State for Real Hardware](ch05-protocol-state-machines-type-state-for-r.md) -- [6. Dimensional Analysis — Making the Compiler Check Your Units](ch06-dimensional-analysis-making-the-compiler.md) -- [7. Validated Boundaries — Parse, Don't Validate](ch07-validated-boundaries-parse-dont-validate.md) -- [8. Capability Mixins — Compile-Time Hardware Contracts](ch08-capability-mixins-compile-time-hardware-.md) -- [9. Phantom Types for Resource Tracking](ch09-phantom-types-for-resource-tracking.md) -- [10. Const Fn — Compile-Time Correctness Proofs](ch15-const-fn-compile-time-correctness-proofs.md) -- [11. Send & Sync — Compile-Time Concurrency Proofs](ch16-send-sync-compile-time-concurrency-proofs.md) +- [2. Типизированные командные интерфейсы: запрос определяет ответ](ch02-typed-command-interfaces-request-determi.md) +- [3. Одноразовые типы: криптографические гарантии через владение](ch03-single-use-types-cryptographic-guarantee.md) +- [4. Capability-токены: доказательство полномочий без накладных расходов](ch04-capability-tokens-zero-cost-proof-of-aut.md) +- [5. Автоматы состояний протоколов: typestate для реального оборудования](ch05-protocol-state-machines-type-state-for-r.md) +- [6. Анализ размерностей: компилятор проверяет единицы измерения](ch06-dimensional-analysis-making-the-compiler.md) +- [7. Проверенные границы: parse, don't validate](ch07-validated-boundaries-parse-dont-validate.md) +- [8. Capability-миксины: контракты аппаратного обеспечения на этапе компиляции](ch08-capability-mixins-compile-time-hardware-.md) +- [9. Phantom-типы для отслеживания ресурсов](ch09-phantom-types-for-resource-tracking.md) +- [10. Const fn: доказательства корректности на этапе компиляции](ch15-const-fn-compile-time-correctness-proofs.md) +- [11. Send и Sync: доказательства корректности конкурентности на этапе компиляции](ch16-send-sync-compile-time-concurrency-proofs.md) -# Part III — Integration & Practice +# Часть III — Интеграция и практика -- [12. Putting It All Together — A Complete Diagnostic Platform](ch10-putting-it-all-together-a-complete-diagn.md) -- [13. Applied Walkthrough — Type-Safe Redfish Client](ch17-redfish-applied-walkthrough.md) -- [14. Applied Walkthrough — Type-Safe Redfish Server](ch18-redfish-server-walkthrough.md) -- [15. Fourteen Tricks from the Trenches](ch11-fourteen-tricks-from-the-trenches.md) -- [16. Exercises](ch12-exercises.md) +- [12. Собираем всё вместе: полная платформа диагностики](ch10-putting-it-all-together-a-complete-diagn.md) +- [13. Пошаговый разбор: типобезопасный клиент Redfish](ch17-redfish-applied-walkthrough.md) +- [14. Пошаговый разбор: типобезопасный сервер Redfish](ch18-redfish-server-walkthrough.md) +- [15. Четырнадцать проверенных приёмов](ch11-fourteen-tricks-from-the-trenches.md) +- [16. Упражнения](ch12-exercises.md) -# Part IV — Reference +# Часть IV — Справочник -- [17. Reference Card](ch13-reference-card.md) -- [18. Testing Type-Level Guarantees](ch14-testing-type-level-guarantees.md) +- [17. Краткая шпаргалка](ch13-reference-card.md) +- [18. Тестирование гарантий на уровне типов](ch14-testing-type-level-guarantees.md) diff --git a/type-driven-correctness-book/src/ch00-introduction.md b/type-driven-correctness-book/src/ch00-introduction.md index 8cfbd40..7b43f67 100644 --- a/type-driven-correctness-book/src/ch00-introduction.md +++ b/type-driven-correctness-book/src/ch00-introduction.md @@ -1,92 +1,91 @@ -# Type-Driven Correctness in Rust +# Корректность на уровне типов в Rust -## Speaker Intro +## Об авторе -- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team -- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems -- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since +- Главный архитектор по прошивкам в команде Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering). +- Инженер с многолетним опытом в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры процессоров и платформ, а также больших систем на C++. +- Пишет на Rust с 2017 года, со времен AWS EC2, и с тех пор остается убежденным сторонником этого языка. --- -A practical guide to using Rust's type system to make entire classes of bugs **impossible to compile**. While the companion [Rust Patterns](../rust-patterns-book/index.html) book covers the mechanics (traits, associated types, type-state), this guide shows how to **apply** those mechanics to real-world domains — hardware diagnostics, cryptography, protocol validation, and embedded systems. - -Every pattern here follows one principle: **push invariants from runtime checks into the type system so the compiler enforces them.** - -## How to Use This Book - -### Difficulty Legend - -| Symbol | Level | Audience | -|:------:|-------|----------| -| 🟢 | Introductory | Comfortable with ownership + traits | -| 🟡 | Intermediate | Familiar with generics + associated types | -| 🔴 | Advanced | Ready for type-state, phantom types, and session types | - -### Pacing Guide - -| Goal | Path | Time | -|------|------|------| -| **Quick overview** | ch01, ch13 (reference card) | 30 min | -| **IPMI / BMC developer** | ch02, ch05, ch07, ch10, ch17 | 2.5 hrs | -| **GPU / PCIe developer** | ch02, ch06, ch09, ch10, ch15 | 2.5 hrs | -| **Redfish implementer** | ch02, ch05, ch07, ch08, ch17, ch18 | 3 hrs | -| **Framework / infrastructure** | ch04, ch08, ch11, ch14, ch18 | 2.5 hrs | -| **New to correct-by-construction** | ch01 → ch10 in order, then ch12 exercises | 4 hrs | -| **Full deep dive** | All chapters sequentially | 7 hrs | - -### Annotated Table of Contents - -| Ch | Title | Difficulty | Key Idea | -|----|-------|:----------:|----------| -| 1 | The Philosophy — Why Types Beat Tests | 🟢 | Three levels of correctness; Curry-Howard intuition | -| 2 | Typed Command Interfaces | 🟡 | Associated types bind request → response | -| 3 | Single-Use Types | 🟡 | Move semantics as linear types for crypto | -| 4 | Capability Tokens | 🟡 | Zero-sized proof-of-authority tokens | -| 5 | Protocol State Machines | 🔴 | Type-state for IPMI sessions + PCIe LTSSM | -| 6 | Dimensional Analysis | 🟢 | Newtype wrappers prevent unit mix-ups | -| 7 | Validated Boundaries | 🟡 | Parse once at the edge, carry proof in types | -| 8 | Capability Mixins | 🟡 | Ingredient traits + blanket impls | -| 9 | Phantom Types | 🟡 | PhantomData for register width, DMA direction | -| 10 | Putting It All Together | 🟡 | All 7 patterns in one diagnostic platform | -| 11 | Fourteen Tricks from the Trenches | 🟡 | Sentinel→Option, sealed traits, builders, etc. | -| 12 | Exercises | 🟡 | Six capstone problems with solutions | -| 13 | Reference Card | — | Pattern catalogue + decision flowchart | -| 14 | Testing Type-Level Guarantees | 🟡 | trybuild, proptest, cargo-show-asm | -| 15 | Const Fn | 🟠 | Compile-time proofs for memory maps, registers, bitfields | -| 16 | Send & Sync | 🟠 | Compile-time concurrency proofs | -| 17 | Redfish Client Walkthrough | 🟡 | Eight patterns composed into a type-safe Redfish client | -| 18 | Redfish Server Walkthrough | 🟡 | Builder type-state, source tokens, health rollup, mixins | - -## Prerequisites - -| Concept | Where to learn it | -|---------|-------------------| -| Ownership and borrowing | [Rust Patterns](../rust-patterns-book/index.html), ch01 | -| Traits and associated types | [Rust Patterns](../rust-patterns-book/index.html), ch02 | -| Newtypes and type-state | [Rust Patterns](../rust-patterns-book/index.html), ch03 | -| PhantomData | [Rust Patterns](../rust-patterns-book/index.html), ch04 | -| Generics and trait bounds | [Rust Patterns](../rust-patterns-book/index.html), ch01 | - -## The Correct-by-Construction Spectrum +Это практическое руководство по использованию системы типов Rust, чтобы целые классы ошибок становились **невозможными для компиляции**. Сопутствующая книга [Паттерны Rust](../rust-patterns-book/index.html) объясняет механику (трейты, ассоциированные типы, typestate), а это руководство показывает, как **применять** эту механику к реальным задачам: диагностике аппаратного обеспечения, криптографии, валидации протоколов и встраиваемым системам. + +Все паттерны здесь следуют одному принципу: **переносите инварианты из проверок во время выполнения в систему типов, чтобы компилятор обеспечивал их соблюдение.** + +## Как работать с книгой + +### Уровни сложности + +| Символ | Уровень | Для кого | +|:------:|---------|----------| +| 🟢 | Вводный | Уверенно владеете владением и трейтами | +| 🟡 | Средний | Знакомы с обобщениями и ассоциированными типами | +| 🔴 | Продвинутый | Готовы к typestate, phantom-типам и сессионным типам | + +### Рекомендации по темпу + +| Цель | Маршрут | Время | +|------|---------|-------| +| **Быстрый обзор** | гл. 01, гл. 13 (шпаргалка) | 30 мин | +| **Разработчик IPMI / BMC** | гл. 02, 05, 07, 10, 17 | 2,5 ч | +| **Разработчик GPU / PCIe** | гл. 02, 06, 09, 10, 15 | 2,5 ч | +| **Реализация Redfish** | гл. 02, 05, 07, 08, 17, 18 | 3 ч | +| **Фреймворки и инфраструктура** | гл. 04, 08, 11, 14, 18 | 2,5 ч | +| **Новичок в корректности по построению** | гл. 01 → 10 по порядку, затем упражнения гл. 12 | 4 ч | +| **Полное погружение** | Все главы по порядку | 7 ч | + +### Аннотированное оглавление + +| Гл. | Название | Сложность | Ключевая идея | +|:---:|----------|:---------:|---------------| +| 1 | Философия: почему типы лучше тестов | 🟢 | Три уровня корректности; интуиция соответствия Карри — Говарда | +| 2 | Типизированные командные интерфейсы | 🟡 | Ассоциированные типы связывают запрос с ответом | +| 3 | Одноразовые типы | 🟡 | Семантика перемещения как линейные типы для криптографии | +| 4 | Capability-токены | 🟡 | Токены-доказательства полномочий нулевого размера | +| 5 | Автоматы состояний протоколов | 🔴 | Typestate для сессий IPMI и LTSSM в PCIe | +| 6 | Анализ размерностей | 🟢 | Обёртки newtype не дают перепутать единицы измерения | +| 7 | Проверенные границы | 🟡 | Разбор данных один раз на границе, доказательство хранится в типах | +| 8 | Capability-миксины | 🟡 | Трейты-ингредиенты и blanket impl | +| 9 | Phantom-типы | 🟡 | PhantomData для ширины регистров и направления DMA | +| 10 | Собираем всё вместе | 🟡 | Все 7 паттернов в одной платформе диагностики | +| 11 | Четырнадцать проверенных приёмов | 🟡 | Sentinel → Option, sealed-трейты, билдеры и др. | +| 12 | Упражнения | 🟡 | Шесть итоговых задач с решениями | +| 13 | Краткая шпаргалка | — | Каталог паттернов и блок-схема выбора | +| 14 | Тестирование гарантий на уровне типов | 🟡 | trybuild, proptest, cargo-show-asm | +| 15 | Const fn: доказательства корректности на этапе компиляции | 🟠 | Карты памяти, регистры и битовые поля | +| 16 | Send и Sync: доказательства корректности конкурентности | 🟠 | Проверки конкурентности на этапе компиляции | +| 17 | Пошаговый разбор: клиент Redfish | 🟡 | Восемь паттернов в типобезопасном клиенте Redfish | +| 18 | Пошаговый разбор: сервер Redfish | 🟡 | Typestate билдера, токены источников, агрегация состояния здоровья, миксины | + +## Что желательно знать заранее + +| Концепция | Где изучить | +|-----------|-------------| +| Владение и заимствование | [Паттерны Rust](../rust-patterns-book/index.html), гл. 01 | +| Трейты и ассоциированные типы | [Паттерны Rust](../rust-patterns-book/index.html), гл. 02 | +| Newtype и typestate | [Паттерны Rust](../rust-patterns-book/index.html), гл. 03 | +| PhantomData | [Паттерны Rust](../rust-patterns-book/index.html), гл. 04 | +| Обобщения и ограничения трейтов | [Паттерны Rust](../rust-patterns-book/index.html), гл. 01 | + +## Спектр корректности по построению ```text -← Less Safe More Safe → - -Runtime checks Unit tests Property tests Correct by Construction -───────────── ────────── ────────────── ────────────────────── - -if temp > 100 { #[test] proptest! { struct Celsius(f64); - panic!("too fn test_temp() { |t in 0..200| { // Can't confuse with Rpm - hot"); assert!( assert!(...) // at the type level -} check(42)); } - } } - Invalid program? -Invalid program? Invalid program? Invalid program? Won't compile. -Crashes in prod. Fails in CI. Fails in CI Never exists. - (probabilistic). +← Менее безопасно Более безопасно → + +Проверки в рантайме Модульные тесты Property-тесты Корректность по построению +─────────────────── ──────────────── ────────────── ────────────────────────── + +if temp > 100 { #[test] proptest! { struct Celsius(f64); + panic!("слишком fn test_temp() { |t in 0..200| { // Нельзя перепутать с Rpm + жарко"); assert!( assert!(...) // на уровне типов +} check(42)); } + } } + Некорректная программа? +Неверный код? Неверный код? Неверный код? Не скомпилируется. +Падает в проде. Падает в CI. Падает в CI Её не существует. + (вероятностно). ``` -This guide operates at the rightmost position — where bugs don't exist because the type system **cannot express them**. +Это руководство работает в самой правой позиции — там, где ошибок не бывает, потому что система типов **не может их выразить**. --- - diff --git a/type-driven-correctness-book/src/ch01-the-philosophy-why-types-beat-tests.md b/type-driven-correctness-book/src/ch01-the-philosophy-why-types-beat-tests.md index 4c45850..b2ca7b0 100644 --- a/type-driven-correctness-book/src/ch01-the-philosophy-why-types-beat-tests.md +++ b/type-driven-correctness-book/src/ch01-the-philosophy-why-types-beat-tests.md @@ -1,60 +1,60 @@ -# The Philosophy — Why Types Beat Tests 🟢 +# Философия: почему типы лучше тестов 🟢 -> **What you'll learn:** The three levels of compile-time correctness (value, state, protocol), the Curry-Howard intuition behind type-level proofs, and when correct-by-construction patterns are — and aren't — worth the investment. +> **Что вы узнаете:** три уровня корректности на этапе компиляции (значения, состояния, протоколы), интуицию соответствия Карри — Говарда, лежащую в основе доказательств на уровне типов, и то, когда паттерны корректности по построению оправданы, а когда — нет. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (typed commands), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (type-state), [ch13](ch13-reference-card.md) (reference card) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (типизированные команды), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (typestate), [гл. 13](ch13-reference-card.md) (шпаргалка) -## The Cost of Runtime Checking +## Цена проверок во время выполнения -Consider a typical runtime guard in a diagnostics codebase: +Рассмотрим типичную проверку во время выполнения в кодовой базе диагностики: ```rust,ignore fn read_sensor(sensor_type: &str, raw: &[u8]) -> f64 { match sensor_type { - "temperature" => raw[0] as i8 as f64, // signed byte + "temperature" => raw[0] as i8 as f64, // знаковый байт "fan_speed" => u16::from_le_bytes([raw[0], raw[1]]) as f64, "voltage" => u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0, - _ => panic!("unknown sensor type: {sensor_type}"), + _ => panic!("неизвестный тип датчика: {sensor_type}"), } } ``` -This function has **four failure modes** the compiler cannot catch: +У этой функции есть **четыре режима отказа**, которые компилятор не может поймать: -1. Typo: `"temperture"` → panic at runtime -2. Wrong `raw` length: `fan_speed` with 1 byte → panic at runtime -3. Caller uses the returned `f64` as RPM when it's actually °C → logic bug, silent -4. New sensor type added but this `match` not updated → panic at runtime +1. Опечатка: `"temperture"` → panic во время выполнения +2. Неверная длина `raw`: `fan_speed` с одним байтом → panic во время выполнения +3. Вызывающий код использует возвращённое `f64` как обороты (RPM), хотя это градусы Цельсия → логическая ошибка, которая никак не проявляется +4. Добавлен новый тип датчика, но этот `match` не обновлён → panic во время выполнения -Every failure mode is discovered **after deployment**. Tests help, but they only cover the cases someone thought to write. The type system covers **all** cases, including ones nobody imagined. +Каждый из этих режимов обнаруживается **уже после развёртывания**. Тесты помогают, но они покрывают только те случаи, о которых кто-то подумал. Система типов охватывает **все** случаи, включая те, о которых никто не подумал. -## Three Levels of Correctness +## Три уровня корректности -### Level 1 — Value Correctness -**Make invalid values unrepresentable.** +### Уровень 1 — Корректность значений +**Сделайте недопустимые значения непредставимыми.** ```rust,ignore -// ❌ Any u16 can be a "port" — 0 is invalid but compiles +// ❌ Любой u16 может быть «портом» — 0 недопустим, но код компилируется fn connect(port: u16) { /* ... */ } -// ✅ Only validated ports can exist -pub struct Port(u16); // private field +// ✅ Существуют только проверенные порты +pub struct Port(u16); // приватное поле impl TryFrom for Port { type Error = &'static str; fn try_from(v: u16) -> Result { - if v > 0 { Ok(Port(v)) } else { Err("port must be > 0") } + if v > 0 { Ok(Port(v)) } else { Err("порт должен быть больше 0") } } } fn connect(port: Port) { /* ... */ } -// Port(0) can never be constructed — invariant holds everywhere +// Port(0) никогда не может быть создан — инвариант выполняется везде ``` -**Hardware example:** `SensorId(u8)` — wraps a raw sensor number with validation that it's in the SDR range. +**Пример из аппаратного обеспечения:** `SensorId(u8)` — оборачивает номер датчика сырым значением и проверяет, что он находится в диапазоне SDR. -### Level 2 — State Correctness -**Make invalid transitions unrepresentable.** +### Уровень 2 — Корректность состояний +**Сделайте недопустимые переходы непредставимыми.** ```rust,ignore use std::marker::PhantomData; @@ -69,7 +69,7 @@ struct Socket { impl Socket { fn connect(self, addr: &str) -> Socket { - // ... connect logic ... + // ... логика подключения ... Socket { fd: self.fd, _state: PhantomData } } } @@ -81,13 +81,13 @@ impl Socket { } } -// Socket has no send() method — compile error if you try +// У Socket нет метода send() — если его вызвать, будет ошибка компиляции ``` -**Hardware example:** GPIO pin modes — `Pin` has `read()` but not `write()`. +**Пример из аппаратного обеспечения:** режимы пинов GPIO — у `Pin` есть `read()`, но нет `write()`. -### Level 3 — Protocol Correctness -**Make invalid interactions unrepresentable.** +### Уровень 3 — Корректность протоколов +**Сделайте недопустимые взаимодействия непредставимыми.** ```rust,ignore use std::io; @@ -97,8 +97,8 @@ trait IpmiCmd { fn parse_response(&self, raw: &[u8]) -> io::Result; } -// Simplified for illustration — see ch02 for the full trait with -// net_fn(), cmd_byte(), payload(), and parse_response(). +// Упрощено для наглядности — полный трейт с net_fn(), cmd_byte(), payload() и +// parse_response() см. в гл. 02. struct ReadTemp { sensor_id: u8 } impl IpmiCmd for ReadTemp { @@ -113,49 +113,48 @@ impl IpmiCmd for ReadTemp { fn execute(cmd: &C, raw: &[u8]) -> io::Result { cmd.parse_response(raw) } -// ReadTemp always returns Celsius — can't accidentally get Rpm +// ReadTemp всегда возвращает Celsius — нельзя случайно получить Rpm ``` -**Hardware example:** IPMI, Redfish, NVMe Admin commands — the request type determines the response type. +**Пример из аппаратного обеспечения:** IPMI, Redfish, административные команды NVMe — тип запроса определяет тип ответа. -## The Curry-Howard Connection (Simplified) +## Связь с соответствием Карри — Говарда (упрощённо) -In programming language theory, the **Curry-Howard correspondence** states that types are propositions and programs are proofs. When you write: +В теории языков программирования **соответствие Карри — Говарда** утверждает, что типы — это высказывания, а программы — их доказательства. Когда вы пишете: ```rust,ignore fn execute(cmd: &C) -> io::Result ``` -You're not just writing a function — you're stating a **theorem**: "for any command type `C` that implements `IpmiCmd`, executing it produces exactly `C::Response`." The compiler **proves** this theorem every time it compiles your code. If the proof fails, the program can't exist. +вы не просто пишете функцию, а формулируете **теорему**: «для любого типа команды `C`, реализующего `IpmiCmd`, выполнение даёт ровно `C::Response`». Компилятор **доказывает** эту теорему при каждой сборке вашего кода. Если доказательство не сходится, такой программы просто не может существовать. -You don't need to understand the theory to use the patterns. But it explains *why* Rust's type system is so powerful — it's not just catching mistakes, it's **proving correctness**. +Чтобы пользоваться паттернами, теорию знать не обязательно. Но она объясняет, *почему* система типов Rust так мощна: она не просто ловит ошибки, а **доказывает корректность**. -## When NOT to Use These Patterns +## Когда НЕ стоит использовать эти паттерны -Correct-by-construction is not always the right choice: +Корректность по построению не всегда оправдана: -| Situation | Recommendation | -|-----------|---------------| -| Safety-critical boundary (power sequencing, crypto) | ✅ Always — a bug here melts hardware or leaks secrets | -| Cross-module public API | ✅ Usually — misuse should be a compile error | -| State machine with 3+ states | ✅ Usually — type-state prevents wrong transitions | -| Internal helper within one 50-line function | ❌ Overkill — a simple `assert!` suffices | -| Prototyping / exploring unknown hardware | ❌ Raw types first — refine after behaviour is understood | -| User-facing CLI parsing | ⚠️ `clap` + `TryFrom` at the boundary, raw types inside is fine | +| Ситуация | Рекомендация | +|----------|--------------| +| Критичная к безопасности граница (управление питанием, криптография) | ✅ Всегда: ошибка здесь может расплавить железо или утечь секреты | +| Публичный API между модулями | ✅ Обычно: неправильное использование должно приводить к ошибке компиляции | +| Автомат состояний с тремя и более состояниями | ✅ Обычно: typestate предотвращает неверные переходы | +| Внутренний помощник в одной функции на 50 строк | ❌ Избыточно: достаточно простого `assert!` | +| Прототипирование и изучение неизвестного оборудования | ❌ Сначала сырые типы; уточняйте их, когда поведение станет понятно | +| Разбор пользовательского CLI | ⚠️ `clap` + `TryFrom` на границе, внутри допустимы сырые типы | -The key question: **"If this bug happens in production, how bad is it?"** +Ключевой вопрос: **«Если эта ошибка случится в продакшене, насколько она опасна?»** -- Fan stops → GPU melts → **use types** -- Wrong DER record → customer gets bad data → **use types** -- Debug log message slightly wrong → **use `assert!`** +- Вентилятор остановился → GPU выходит из строя → **используйте типы** +- Неверная запись DER → клиент получает некорректные данные → **используйте типы** +- Немного неверное debug-сообщение в логе → **используйте `assert!`** -## Key Takeaways +## Ключевые выводы -1. **Three levels of correctness** — value (newtypes), state (type-state), protocol (associated types) — each eliminates a broader class of bugs. -2. **Curry-Howard in practice** — every generic function signature is a theorem the compiler proves on each build. -3. **The cost question** — "if this bug ships, how bad is it?" determines whether types or tests are the right tool. -4. **Types complement tests** — they eliminate entire *categories*; tests cover specific *values* and edge cases. -5. **Know when to stop** — internal helpers and throwaway prototypes rarely need type-level enforcement. +1. **Три уровня корректности** — значения (newtype), состояния (typestate), протоколы (ассоциированные типы) — каждый уровень устраняет более широкий класс ошибок. +2. **Соответствие Карри — Говарда на практике** — каждая сигнатура обобщённой функции — это теорема, которую компилятор доказывает при каждой сборке. +3. **Вопрос о цене** — «если эта ошибка уйдёт в продакшен, насколько она опасна?» — определяет, что уместнее: типы или тесты. +4. **Типы дополняют тесты** — типы устраняют целые *категории* ошибок, а тесты проверяют конкретные *значения* и граничные случаи. +5. **Знайте, когда остановиться** — внутренние помощники и одноразовые прототипы редко нуждаются в принудительной проверке на уровне типов. --- - diff --git a/type-driven-correctness-book/src/ch02-typed-command-interfaces-request-determi.md b/type-driven-correctness-book/src/ch02-typed-command-interfaces-request-determi.md index 8bb1b12..7511c50 100644 --- a/type-driven-correctness-book/src/ch02-typed-command-interfaces-request-determi.md +++ b/type-driven-correctness-book/src/ch02-typed-command-interfaces-request-determi.md @@ -1,67 +1,65 @@ -# Typed Command Interfaces — Request Determines Response 🟡 +# Типизированные командные интерфейсы: запрос определяет ответ 🟡 -> **What you'll learn:** How associated types on a command trait create a compile-time binding between request and response, eliminating mismatched parsing, unit confusion, and silent type coercion across IPMI, Redfish, and NVMe protocols. +> **Что вы узнаете:** как ассоциированные типы в трейте команды создают связь между запросом и ответом на этапе компиляции, устраняя рассогласованный разбор данных, путаницу единиц измерения и тихое приведение типов в протоколах IPMI, Redfish и NVMe. > -> **Cross-references:** [ch01](ch01-the-philosophy-why-types-beat-tests.md) (philosophy), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional types), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (validated boundaries), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (integration) +> **Перекрёстные ссылки:** [гл. 01](ch01-the-philosophy-why-types-beat-tests.md) (философия), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (размерные типы), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (проверенные границы), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция) -## The Untyped Swamp +## Неструктурированное болото -Most hardware management stacks — IPMI, Redfish, NVMe Admin, PLDM — start life as -`raw bytes in → raw bytes out`. This creates a category of bugs that tests can only -partially find: +Большинство стеков управления аппаратным обеспечением — IPMI, Redfish, NVMe Admin, PLDM — начинаются с модели `сырые байты на входе → сырые байты на выходе`. Это порождает класс ошибок, которые тесты могут найти лишь частично: ```rust,ignore use std::io; -struct BmcRaw { /* ipmitool handle */ } +struct BmcRaw { /* дескриптор ipmitool */ } impl BmcRaw { fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result> { - // ... shells out to ipmitool ... - Ok(vec![0x00, 0x19, 0x00]) // stub + // ... вызывает ipmitool ... + Ok(vec![0x00, 0x19, 0x00]) // заглушка } } fn diagnose_thermal(bmc: &BmcRaw) -> io::Result<()> { let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?; - let cpu_temp = raw[0] as f64; // 🤞 is byte 0 the reading? + let cpu_temp = raw[0] as f64; // 🤞 а байт 0 — точно показание? let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?; - let fan_rpm = raw[0] as u32; // 🐛 fan speed is 2 bytes LE + let fan_rpm = raw[0] as u32; // 🐛 скорость вентилятора — 2 байта, little-endian let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?; - let voltage = raw[0] as f64; // 🐛 need to divide by 1000 + let voltage = raw[0] as f64; // 🐛 нужно поделить на 1000 - if cpu_temp > fan_rpm as f64 { // 🐛 comparing °C to RPM - println!("uh oh"); + if cpu_temp > fan_rpm as f64 { // 🐛 сравниваем °C с об/мин + println!("упс"); } - log_temp(voltage); // 🐛 passing Volts as temperature + log_temp(voltage); // 🐛 передаём вольты как температуру Ok(()) } -fn log_temp(t: f64) { println!("Temp: {t}°C"); } +fn log_temp(t: f64) { println!("Температура: {t}°C"); } ``` -| # | Bug | Discovered | -|---|-----|------------| -| 1 | Fan RPM parsed as 1 byte instead of 2 | Production, 3 AM | -| 2 | Voltage not scaled | Every PSU flagged as overvoltage | -| 3 | Comparing °C to RPM | Maybe never | -| 4 | Volts passed to temp logger | 6 months later, reading historical data | +| № | Ошибка | Когда обнаружена | +|---|--------|------------------| +| 1 | Скорость вентилятора разобрана как 1 байт вместо 2 | В продакшене, в 3 часа ночи | +| 2 | Напряжение не масштабировано | Каждый блок питания помечен как перенапряжение | +| 3 | Сравнение °C с об/мин | Возможно, никогда | +| 4 | Вольты переданы в логгер температуры | Через полгода, при чтении исторических данных | -**Root cause:** Everything is `Vec` → `f64` → pray. +**Первопричина:** всё превращено в `Vec` → `f64` → и молимся. -## The Typed Command Pattern +## Паттерн типизированной команды -### Step 1 — Domain newtypes +### Шаг 1 — Доменные newtype ```rust,ignore #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Celsius(pub f64); #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] -pub struct Rpm(pub u32); // u32: raw IPMI sensor value (integer RPM) +pub struct Rpm(pub u32); // u32: сырое целочисленное показание датчика IPMI (об/мин) #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Volts(pub f64); @@ -70,43 +68,31 @@ pub struct Volts(pub f64); pub struct Watts(pub f64); ``` -> **Note on `Rpm(u32)` vs `Rpm(f64)`:** In this chapter the inner type is `u32` -> because IPMI sensor readings are integer values. In ch06 (Dimensional Analysis), -> `Rpm` uses `f64` to support arithmetic operations (averaging, scaling). Both -> are valid — the newtype prevents cross-unit confusion regardless of inner type. +> **Примечание о `Rpm(u32)` и `Rpm(f64)`:** в этой главе внутренний тип — `u32`, потому что показания датчиков IPMI — целые значения. В гл. 06 (анализ размерностей) `Rpm` использует `f64`, чтобы поддерживать арифметические операции (усреднение, масштабирование). Оба варианта допустимы: newtype предотвращает путаницу между единицами независимо от внутреннего типа. -### Step 2 — The command trait (type-indexed dispatch) +### Шаг 2 — Трейт команды (диспетчеризация по индексу типа) -> **Background: GADTs in one paragraph.** -> In Haskell and similar languages, a *Generalised Algebraic Data Type* -> ([GADT](https://wiki.haskell.org/GADTs_for_dummies)) lets each constructor of a -> data type fix the type parameter to something specific. For example, -> `ReadTemp :: SensorId -> Cmd Celsius` says "constructing a `ReadTemp` value -> produces a `Cmd` whose type parameter is always `Celsius`." -> Rust achieves the same thing differently: an **associated type** on a trait -> lets each implementing struct pin the response type at compile time. -> You don't need to know Haskell to use this pattern — the Rust version is -> self-contained. +> **Справочно: GADT за один абзац.** +> В Haskell и похожих языках *обобщённый алгебраический тип данных* ([GADT](https://wiki.haskell.org/GADTs_for_dummies)) позволяет каждому конструктору зафиксировать параметр типа конкретным значением. Например, `ReadTemp :: SensorId -> Cmd Celsius` означает: «создание значения `ReadTemp` даёт `Cmd`, параметр типа которого всегда `Celsius`». +> В Rust то же самое достигается иначе: **ассоциированный тип** трейта позволяет каждой реализующей структуре зафиксировать тип ответа на этапе компиляции. Знать Haskell для использования этого паттерна не нужно — версия на Rust самодостаточна. -The associated type `Response` is the key — it binds each command struct to its -return type. Each implementing struct pins `Response` to a specific domain type, -so `execute()` always returns exactly the right type: +Ассоциированный тип `Response` — ключевой элемент: он связывает каждую структуру команды с её возвращаемым типом. Каждая реализующая структура фиксирует `Response` на конкретном доменном типе, поэтому `execute()` всегда возвращает ровно нужный тип: ```rust,ignore pub trait IpmiCmd { - /// The "type index" — determines what execute() returns. + /// «Индекс типа» — определяет, что возвращает execute(). type Response; fn net_fn(&self) -> u8; fn cmd_byte(&self) -> u8; fn payload(&self) -> Vec; - /// Parsing encapsulated here — each command knows its own byte layout. + /// Разбор инкапсулирован здесь — каждая команда знает свою раскладку байтов. fn parse_response(&self, raw: &[u8]) -> io::Result; } ``` -### Step 3 — One struct per command +### Шаг 3 — Одна структура на команду ```rust,ignore pub struct ReadTemp { pub sensor_id: u8 } @@ -117,15 +103,15 @@ impl IpmiCmd for ReadTemp { fn payload(&self) -> Vec { vec![self.sensor_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "empty response")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ")); } - // Note: ch01's untyped example uses `raw[0] as i8 as f64` (signed) - // because that function was demonstrating generic parsing without - // SDR metadata. Here we use unsigned (`as f64`) because the SDR - // linearization formula in IPMI spec §35.5 converts the unsigned - // raw reading to a calibrated value. In production, apply the - // full SDR formula: result = (M × raw + B) × 10^(R_exp). - Ok(Celsius(raw[0] as f64)) // unsigned raw byte, converted per SDR formula + // Примечание: нетипизированный пример из гл. 01 использует `raw[0] as i8 as f64` + // (со знаком), потому что та функция демонстрировала обобщённый разбор без + // метаданных SDR. Здесь мы используем беззнаковое значение (`as f64`), так как + // формула линеаризации SDR из раздела 35.5 спецификации IPMI преобразует + // беззнаковое сырое показание в калиброванное значение. В продакшене примените + // полную формулу SDR: result = (M × raw + B) × 10^(R_exp). + Ok(Celsius(raw[0] as f64)) // беззнаковый сырой байт, преобразуется по формуле SDR } } @@ -138,7 +124,7 @@ impl IpmiCmd for ReadFanSpeed { fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 2 { return Err(io::Error::new(io::ErrorKind::InvalidData, - format!("fan speed needs 2 bytes, got {}", raw.len()))); + format!("для скорости вентилятора нужно 2 байта, получено {}", raw.len()))); } Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32)) } @@ -153,14 +139,14 @@ impl IpmiCmd for ReadVoltage { fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 2 { return Err(io::Error::new(io::ErrorKind::InvalidData, - format!("voltage needs 2 bytes, got {}", raw.len()))); + format!("для напряжения нужно 2 байта, получено {}", raw.len()))); } Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0)) } } ``` -### Step 4 — The executor (zero `dyn`, monomorphised) +### Шаг 4 — Исполнитель (без `dyn`, мономорфизированный) ```rust,ignore pub struct BmcConnection { pub timeout_secs: u32 } @@ -172,12 +158,12 @@ impl BmcConnection { } fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result> { - Ok(vec![0x19, 0x00]) // stub + Ok(vec![0x19, 0x00]) // заглушка } } ``` -### Step 5 — All four bugs become compile errors +### Шаг 5 — Все четыре ошибки становятся ошибками компиляции ```rust,ignore fn diagnose_thermal_typed(bmc: &BmcConnection) -> io::Result<()> { @@ -185,30 +171,30 @@ fn diagnose_thermal_typed(bmc: &BmcConnection) -> io::Result<()> { let fan_rpm: Rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?; let voltage: Volts = bmc.execute(&ReadVoltage { rail: 0x40 })?; - // Bug #1 — IMPOSSIBLE: parsing lives in ReadFanSpeed::parse_response - // Bug #2 — IMPOSSIBLE: unit scaling lives in ReadVoltage::parse_response + // Ошибка №1 — НЕВОЗМОЖНА: разбор находится в ReadFanSpeed::parse_response + // Ошибка №2 — НЕВОЗМОЖНА: масштабирование единиц находится в ReadVoltage::parse_response - // Bug #3 — COMPILE ERROR: + // Ошибка №3 — ОШИБКА КОМПИЛЯЦИИ: // if cpu_temp > fan_rpm { } - // ^^^^^^^^ ^^^^^^^ Celsius vs Rpm → "mismatched types" ❌ + // ^^^^^^^^ ^^^^^^^ Celsius и Rpm → "mismatched types" ❌ - // Bug #4 — COMPILE ERROR: + // Ошибка №4 — ОШИБКА КОМПИЛЯЦИИ: // log_temperature(voltage); - // ^^^^^^^ Volts, expected Celsius ❌ + // ^^^^^^^ Volts, ожидается Celsius ❌ - if cpu_temp > Celsius(85.0) { println!("CPU overheating: {:?}", cpu_temp); } - if fan_rpm < Rpm(4000) { println!("Fan too slow: {:?}", fan_rpm); } + if cpu_temp > Celsius(85.0) { println!("Перегрев CPU: {:?}", cpu_temp); } + if fan_rpm < Rpm(4000) { println!("Вентилятор слишком медленный: {:?}", fan_rpm); } Ok(()) } -fn log_temperature(t: Celsius) { println!("Temp: {:?}", t); } -fn log_voltage(v: Volts) { println!("Voltage: {:?}", v); } +fn log_temperature(t: Celsius) { println!("Температура: {:?}", t); } +fn log_voltage(v: Volts) { println!("Напряжение: {:?}", v); } ``` -## IPMI: Sensor Reads That Can't Be Confused +## IPMI: показания датчиков, которые нельзя перепутать -Adding a new sensor is one struct + one impl — no scattered parsing: +Добавление нового датчика — это одна структура и одна реализация, без разбросанного по коду разбора: ```rust,ignore pub struct ReadPowerDraw { pub domain: u8 } @@ -220,17 +206,17 @@ impl IpmiCmd for ReadPowerDraw { fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 2 { return Err(io::Error::new(io::ErrorKind::InvalidData, - format!("power draw needs 2 bytes, got {}", raw.len()))); + format!("для потребляемой мощности нужно 2 байта, получено {}", raw.len()))); } Ok(Watts(u16::from_le_bytes([raw[0], raw[1]]) as f64)) } } -// Every caller that uses bmc.execute(&ReadPowerDraw { domain: 0 }) -// automatically gets Watts back — no parsing code elsewhere +// Любой вызов bmc.execute(&ReadPowerDraw { domain: 0 }) автоматически возвращает Watts — +// никакого кода разбора в других местах ``` -### Testing Each Command in Isolation +### Тестирование каждой команды изолированно ```rust,ignore #[cfg(test)] @@ -245,7 +231,7 @@ mod tests { fn execute(&self, cmd: &C) -> io::Result { let key = cmd.payload()[0]; let raw = self.responses.get(&key) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no stub"))?; + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "нет заглушки"))?; cmd.parse_response(raw) } } @@ -253,7 +239,7 @@ mod tests { #[test] fn read_temp_parses_raw_byte() { let bmc = StubBmc { - responses: [(0x20, vec![0x19])].into(), // 25 decimal = 0x19 + responses: [(0x20, vec![0x19])].into(), // 25 в десятичной = 0x19 }; let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap(); assert_eq!(temp, Celsius(25.0)); @@ -271,7 +257,7 @@ mod tests { #[test] fn read_voltage_scales_millivolts() { let bmc = StubBmc { - responses: [(0x40, vec![0xE8, 0x2E])].into(), // 0x2EE8 = 12008 mV + responses: [(0x40, vec![0xE8, 0x2E])].into(), // 0x2EE8 = 12008 мВ }; let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap(); assert!((v.0 - 12.008).abs() < 0.001); @@ -279,9 +265,9 @@ mod tests { } ``` -## Redfish: Schema-Typed REST Endpoints +## Redfish: REST-эндпоинты с типизированной схемой -Redfish is an even better fit — each endpoint returns a DMTF-defined JSON schema: +Redfish подходит ещё лучше: каждый эндпоинт возвращает JSON-схему, определённую DMTF: ```rust,ignore use serde::Deserialize; @@ -362,7 +348,7 @@ pub struct RedfishHealth { pub health: Option, } -/// Typed Redfish endpoint — each knows its response type. +/// Типизированный эндпоинт Redfish — каждый знает тип своего ответа. pub trait RedfishEndpoint { type Response: serde::de::DeserializeOwned; fn method(&self) -> &'static str; @@ -410,11 +396,11 @@ impl RedfishClient { } fn http_request(&self, _method: &str, _url: &str) -> io::Result> { - Ok(vec![]) // stub — real impl uses reqwest/hyper + Ok(vec![]) // заглушка — реальная реализация использует reqwest/hyper } } -// Usage — fully typed, self-documenting +// Использование — полностью типизировано, код говорит сам за себя fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> { let thermal: ThermalResponse = client.execute(&GetThermal { chassis_id: "1".into(), @@ -423,13 +409,13 @@ fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> { chassis_id: "1".into(), })?; - // ❌ Compile error — can't pass PowerResponse to a thermal check: + // ❌ Ошибка компиляции — нельзя передать PowerResponse в проверку температуры: // check_thermals(&power); → "expected ThermalResponse, found PowerResponse" for temp in &thermal.temperatures { if let Some(crit) = temp.critical_hi { if temp.reading > crit { - println!("CRITICAL: {} at {}°C (threshold: {}°C)", + println!("КРИТИЧНО: {} при {}°C (порог: {}°C)", temp.name, temp.reading, crit); } } @@ -438,16 +424,14 @@ fn redfish_pre_flight(client: &RedfishClient) -> io::Result<()> { } ``` -## NVMe Admin: Identify Doesn't Return Log Pages +## NVMe Admin: Identify не возвращает журнальные страницы -NVMe admin commands follow the same shape. The controller distinguishes command -opcodes, but in C the caller must know which struct to overlay on the 4 KB -completion buffer. The typed-command pattern makes this impossible to get wrong: +Команды NVMe Admin устроены так же. Контроллер различает коды команд (opcode), но в C вызывающий код должен знать, какую структуру наложить на 4-килобайтный буфер завершения. Паттерн типизированной команды делает такую ошибку невозможной: ```rust,ignore use std::io; -/// The NVMe Admin command trait — same shape as IpmiCmd. +/// Трейт команды NVMe Admin — той же формы, что IpmiCmd. pub trait NvmeAdminCmd { type Response; fn opcode(&self) -> u8; @@ -458,14 +442,14 @@ pub trait NvmeAdminCmd { #[derive(Debug, Clone)] pub struct IdentifyResponse { - pub model_number: String, // bytes 24–63 - pub serial_number: String, // bytes 4–23 - pub firmware_rev: String, // bytes 64–71 + pub model_number: String, // байты 24–63 + pub serial_number: String, // байты 4–23 + pub firmware_rev: String, // байты 64–71 pub total_capacity_gb: u64, } pub struct Identify { - pub nsid: u32, // 0 = controller, >0 = namespace + pub nsid: u32, // 0 = контроллер, >0 = пространство имён } impl NvmeAdminCmd for Identify { @@ -473,7 +457,7 @@ impl NvmeAdminCmd for Identify { fn opcode(&self) -> u8 { 0x06 } fn parse_completion(&self, data: &[u8]) -> io::Result { if data.len() < 4096 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "short identify")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "короткий ответ identify")); } Ok(IdentifyResponse { serial_number: String::from_utf8_lossy(&data[4..24]).trim().to_string(), @@ -505,7 +489,7 @@ impl NvmeAdminCmd for GetLogPage { fn opcode(&self) -> u8 { 0x02 } fn parse_completion(&self, data: &[u8]) -> io::Result { if data.len() < 512 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "short log page")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "короткая страница журнала")); } Ok(SmartLog { critical_warning: data[0], @@ -516,9 +500,9 @@ impl NvmeAdminCmd for GetLogPage { } } -// ── Executor ── +// ── Исполнитель ── -pub struct NvmeController { /* fd, BAR, etc. */ } +pub struct NvmeController { /* дескриптор, BAR и т. д. */ } impl NvmeController { pub fn admin_cmd(&self, cmd: &C) -> io::Result { @@ -527,20 +511,20 @@ impl NvmeController { } fn submit_and_wait(&self, _opcode: u8) -> io::Result> { - Ok(vec![0u8; 4096]) // stub — real impl issues doorbell + waits for CQ entry + Ok(vec![0u8; 4096]) // заглушка — реальная реализация записывает doorbell и ждёт запись в CQ } } -// ── Usage ── +// ── Использование ── fn nvme_health_check(ctrl: &NvmeController) -> io::Result<()> { let id: IdentifyResponse = ctrl.admin_cmd(&Identify { nsid: 0 })?; let smart: SmartLog = ctrl.admin_cmd(&GetLogPage { log_id: 0x02 })?; - // ❌ Compile error — Identify returns IdentifyResponse, not SmartLog: + // ❌ Ошибка компиляции: Identify возвращает IdentifyResponse, а не SmartLog: // let smart: SmartLog = ctrl.admin_cmd(&Identify { nsid: 0 })?; - println!("{} (FW {}): {}°C, {}% spare", + println!("{} (FW {}): {}°C, {}% резерва", id.model_number, id.firmware_rev, smart.temperature_kelvin.saturating_sub(273), smart.available_spare_pct); @@ -549,19 +533,18 @@ fn nvme_health_check(ctrl: &NvmeController) -> io::Result<()> { } ``` -The three-protocol progression now follows a **graduated arc** (the same technique -ch07 uses for validated boundaries): +Трёхпротокольная прогрессия теперь следует **постепенной дуге** (та же техника, что в гл. 07 для проверенных границ): -| Beat | Protocol | Complexity | What it adds | -|:----:|----------|-----------|--------------| -| 1 | IPMI | Simple: sensor ID → reading | Core pattern: `trait + associated type` | -| 2 | Redfish | REST: endpoint → typed JSON | Serde integration, schema-typed responses | -| 3 | NVMe | Binary: opcode → 4 KB struct overlay | Raw buffer parsing, multi-struct completion data | +| Этап | Протокол | Сложность | Что добавляет | +|:----:|----------|-----------|---------------| +| 1 | IPMI | Просто: ID датчика → показание | Базовый паттерн: `trait + associated type` | +| 2 | Redfish | REST: эндпоинт → типизированный JSON | Интеграция с serde, типизированные ответы по схеме | +| 3 | NVMe | Бинарный: код операции → наложение структуры на 4 КБ | Разбор сырого буфера, данные завершения из нескольких структур | -## Extension: Macro DSL for Command Scripts +## Расширение: макро-DSL для сценариев команд ```rust,ignore -/// Execute a series of typed IPMI commands, returning a tuple of results. +/// Выполняет серию типизированных команд IPMI и возвращает кортеж результатов. macro_rules! diag_script { ($bmc:expr; $($cmd:expr),+ $(,)?) => {{ ( $( $bmc.execute(&$cmd)?, )+ ) @@ -574,17 +557,17 @@ fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> { ReadFanSpeed { fan_id: 0x30 }, ReadVoltage { rail: 0x40 }, ); - // Type: (Celsius, Rpm, Volts) — fully inferred, swap = compile error - assert!(temp < Celsius(95.0), "CPU too hot"); - assert!(rpm > Rpm(3000), "Fan too slow"); - assert!(volts > Volts(11.4), "12V rail sagging"); + // Тип: (Celsius, Rpm, Volts) — выводится полностью; перестановка даёт ошибку компиляции + assert!(temp < Celsius(95.0), "ЦП слишком горячий"); + assert!(rpm > Rpm(3000), "Вентилятор слишком медленный"); + assert!(volts > Volts(11.4), "Линия 12 В просела"); Ok(()) } ``` -## Extension: Enum Dispatch for Dynamic Scripts +## Расширение: диспетчеризация через enum для динамических сценариев -When commands come from JSON config at runtime: +Когда команды приходят из JSON-конфигурации во время выполнения: ```rust,ignore pub enum AnyReading { @@ -617,29 +600,28 @@ fn run_dynamic_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result Cmd Celsius type Response; @@ -650,19 +632,19 @@ eval :: Cmd a -> IO a fn execute(&self, cmd: &C) -> io::Result ``` -Both guarantee: **the command determines the return type**. +Оба варианта гарантируют: **команда определяет тип возвращаемого значения**. -## Typed Command Flow +## Поток типизированных команд ```mermaid flowchart LR - subgraph "Compile Time" + subgraph "Этап компиляции" RT["ReadTemp"] -->|"type Response = Celsius"| C[Celsius] RF["ReadFanSpeed"] -->|"type Response = Rpm"| R[Rpm] RV["ReadVoltage"] -->|"type Response = Volts"| V[Volts] end - subgraph "Runtime" - E["bmc.execute(&cmd)"] -->|"monomorphised"| P["cmd.parse_response(raw)"] + subgraph "Рантайм" + E["bmc.execute(&cmd)"] -->|"мономорфизировано"| P["cmd.parse_response(raw)"] end style RT fill:#e1f5fe,color:#000 style RF fill:#e1f5fe,color:#000 @@ -674,16 +656,16 @@ flowchart LR style P fill:#fff3e0,color:#000 ``` -## Exercise: PLDM Typed Commands +## Упражнение: типизированные команды PLDM -Design a `PldmCmd` trait (same shape as `IpmiCmd`) for two PLDM commands: +Спроектируйте трейт `PldmCmd` (той же формы, что `IpmiCmd`) для двух команд PLDM: - `GetFwParams` → `FwParamsResponse { active_version: String, pending_version: Option }` - `QueryDeviceIds` → `DeviceIdResponse { descriptors: Vec }` -Requirements: static dispatch, `parse_response` returns `io::Result`. +Требования: статическая диспетчеризация, `parse_response` возвращает `io::Result`.
-Solution +Решение ```rust,ignore use std::io; @@ -704,12 +686,12 @@ pub struct FwParamsResponse { pub struct GetFwParams; impl PldmCmd for GetFwParams { type Response = FwParamsResponse; - fn pldm_type(&self) -> u8 { 0x05 } // Firmware Update + fn pldm_type(&self) -> u8 { 0x05 } // Обновление прошивки fn command_code(&self) -> u8 { 0x02 } fn parse_response(&self, raw: &[u8]) -> io::Result { - // Simplified — real impl decodes PLDM FW Update spec fields + // Упрощённо — реальная реализация декодирует поля спецификации PLDM FW Update if raw.len() < 4 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "too short")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко")); } Ok(FwParamsResponse { active_version: String::from_utf8_lossy(&raw[..4]).to_string(), @@ -730,21 +712,20 @@ impl PldmCmd for QueryDeviceIds { fn pldm_type(&self) -> u8 { 0x05 } fn command_code(&self) -> u8 { 0x04 } fn parse_response(&self, raw: &[u8]) -> io::Result { - Ok(DeviceIdResponse { descriptors: vec![] }) // stub + Ok(DeviceIdResponse { descriptors: vec![] }) // заглушка } } ```
-## Key Takeaways +## Ключевые выводы -1. **Associated type = compile-time contract** — `type Response` on the command trait locks each request to exactly one response type. -2. **Parsing is encapsulated** — byte-layout knowledge lives in `parse_response`, not scattered across callers. -3. **Zero-cost dispatch** — generic `execute` monomorphises to direct calls with no vtable. -4. **One pattern, many protocols** — IPMI, Redfish, NVMe, PLDM, MCTP all fit the same `trait Cmd { type Response; }` shape. -5. **Enum dispatch bridges static and dynamic** — wrap typed commands in an enum for runtime-driven scripts without losing type safety inside each arm. -6. **Graduated complexity strengthens intuition** — IPMI (sensor ID → reading), Redfish (endpoint → JSON schema), and NVMe (opcode → 4 KB struct overlay) all use the same trait shape, but each beat adds a layer of parsing complexity. +1. **Ассоциированный тип — это контракт на этапе компиляции** — `type Response` в трейте команды жёстко связывает каждый запрос ровно с одним типом ответа. +2. **Разбор инкапсулирован** — знание о раскладке байтов живёт в `parse_response`, а не разбросано по вызывающему коду. +3. **Диспетчеризация без накладных расходов** — обобщённая функция `execute` мономорфизируется в прямые вызовы без vtable. +4. **Один паттерн, много протоколов** — IPMI, Redfish, NVMe, PLDM и MCTP укладываются в одну и ту же форму `trait Cmd { type Response; }`. +5. **Диспетчеризация через enum связывает статику и динамику** — оборачивайте типизированные команды в enum для сценариев, управляемых во время выполнения, не теряя типобезопасности внутри каждой ветки. +6. **Постепенное усложнение укрепляет интуицию** — IPMI (ID датчика → показание), Redfish (эндпоинт → JSON-схема) и NVMe (код операции → наложение структуры на 4 КБ) используют одну и ту же форму трейта, но каждый этап добавляет ещё один слой сложности разбора. --- - diff --git a/type-driven-correctness-book/src/ch03-single-use-types-cryptographic-guarantee.md b/type-driven-correctness-book/src/ch03-single-use-types-cryptographic-guarantee.md index 9147928..c6715e5 100644 --- a/type-driven-correctness-book/src/ch03-single-use-types-cryptographic-guarantee.md +++ b/type-driven-correctness-book/src/ch03-single-use-types-cryptographic-guarantee.md @@ -1,107 +1,103 @@ -# Single-Use Types — Cryptographic Guarantees via Ownership 🟡 +# Одноразовые типы: криптографические гарантии через владение 🟡 -> **What you'll learn:** How Rust's move semantics act as a linear type system, making nonce reuse, double key-agreement, and accidental fuse re-programming impossible at compile time. +> **Что вы узнаете:** как семантика перемещения в Rust работает как система линейных типов, делая невозможными на этапе компиляции повторное использование nonce, двойное согласование ключа и случайное повторное программирование предохранителя. > -> **Cross-references:** [ch01](ch01-the-philosophy-why-types-beat-tests.md) (philosophy), [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (type-state), [ch14](ch14-testing-type-level-guarantees.md) (testing compile-fail) +> **Перекрёстные ссылки:** [гл. 01](ch01-the-philosophy-why-types-beat-tests.md) (философия), [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (typestate), [гл. 14](ch14-testing-type-level-guarantees.md) (тестирование compile-fail) -## The Nonce Reuse Catastrophe +## Катастрофа повторного использования nonce -In authenticated encryption (AES-GCM, ChaCha20-Poly1305), reusing a nonce with the -same key is **catastrophic** — it leaks the XOR of two plaintexts and often the -authentication key itself. This isn't a theoretical concern: +В аутентифицированном шифровании (AES-GCM, ChaCha20-Poly1305) повторное использование nonce с тем же ключом — **катастрофа**: оно раскрывает XOR двух открытых текстов и часто сам ключ аутентификации. Это не теоретическая проблема: -- **2016**: Forbidden Attack on AES-GCM in TLS — nonce reuse allowed plaintext recovery -- **2020**: Multiple IoT firmware update systems found reusing nonces due to poor RNG +- **2016**: Forbidden Attack на AES-GCM в TLS — повторное использование nonce позволяло восстановить открытый текст +- **2020**: в нескольких системах обновления прошивок IoT обнаружили повторное использование nonce из-за слабого генератора случайных чисел -In C/C++, a nonce is just a `uint8_t[12]`. Nothing prevents you from using it twice. +В C/C++ nonce — это просто `uint8_t[12]`. Ничто не мешает использовать его дважды. ```c -// C — nothing stops nonce reuse +// C — ничто не мешает повторно использовать nonce uint8_t nonce[12]; generate_nonce(nonce); -encrypt(key, nonce, msg1, out1); // ✅ first use -encrypt(key, nonce, msg2, out2); // 🐛 CATASTROPHIC: same nonce +encrypt(key, nonce, msg1, out1); // ✅ первое использование +encrypt(key, nonce, msg2, out2); // 🐛 КАТАСТРОФА: тот же nonce ``` -## Move Semantics as Linear Types +## Семантика перемещения как линейные типы -Rust's ownership system is effectively a **linear type system** — a value can be used -exactly once (moved) unless it implements `Copy`. The `ring` crate exploits this: +Система владения Rust по сути является **системой линейных типов**: значение можно использовать ровно один раз (переместив его), если только тип не реализует `Copy`. Крейт `ring` использует это: ```rust,ignore -// ring::aead::Nonce is: -// - NOT Clone -// - NOT Copy -// - Consumed by value when used -pub struct Nonce(/* private */); +// ring::aead::Nonce: +// - НЕ Clone +// - НЕ Copy +// - потребляется по значению при использовании +pub struct Nonce(/* приватное */); impl Nonce { pub fn try_assume_unique_for_key(value: &[u8]) -> Result { // ... } - // No Clone, no Copy — can only be used once + // Нет Clone, нет Copy — использовать можно только один раз } ``` -When you pass a `Nonce` to `seal_in_place()`, **it moves**: +Когда вы передаёте `Nonce` в `seal_in_place()`, он **перемещается**: ```rust,ignore -// Pseudocode mirroring ring's API shape +// Псевдокод, повторяющий форму API ring fn seal_in_place( key: &SealingKey, - nonce: Nonce, // ← moved, not borrowed + nonce: Nonce, // ← перемещается, а не заимствуется data: &mut Vec, ) -> Result<(), Error> { - // ... encrypt data in place ... - // nonce is consumed — cannot be used again + // ... шифруем данные на месте ... + // nonce потреблён — использовать его снова нельзя Ok(()) } ``` -Attempting to reuse it: +Попытка использовать его повторно: ```rust,ignore fn bad_encrypt(key: &SealingKey, data1: &mut Vec, data2: &mut Vec) { let nonce = Nonce::try_assume_unique_for_key(&[0u8; 12]).unwrap(); - seal_in_place(key, nonce, data1).unwrap(); // ✅ nonce moved here + seal_in_place(key, nonce, data1).unwrap(); // ✅ nonce перемещён здесь // seal_in_place(key, nonce, data2).unwrap(); - // ^^^^^ ERROR: use of moved value ❌ + // ^^^^^ ОШИБКА: использование перемещённого значения ❌ } ``` -The compiler **proves** that each nonce is used exactly once. No test required. +Компилятор **доказывает**, что каждый nonce используется ровно один раз. Тесты для этого не нужны. -## Case Study: ring's Nonce +## Разбор: nonce в ring -The `ring` crate goes further with `NonceSequence` — a trait that **generates** -nonces and is also non-cloneable: +Крейт `ring` идёт дальше с `NonceSequence` — трейтом, который **генерирует** nonce и тоже не может быть клонирован: ```rust,ignore -/// A sequence of unique nonces. -/// Not Clone — once bound to a key, cannot be duplicated. +/// Последовательность уникальных nonce. +/// Не Clone — после привязки к ключу не может быть продублирована. pub trait NonceSequence { fn advance(&mut self) -> Result; } -/// SealingKey wraps a NonceSequence — each seal() auto-advances. +/// SealingKey оборачивает NonceSequence — каждый seal() автоматически продвигает её. pub struct SealingKey { - key: UnboundKey, // consumed during construction + key: UnboundKey, // потребляется при создании nonce_seq: N, } impl SealingKey { pub fn new(key: UnboundKey, nonce_seq: N) -> Self { - // UnboundKey is moved — can't be used for both sealing AND opening + // UnboundKey перемещается — нельзя использовать и для шифрования, и для расшифровки SealingKey { key, nonce_seq } } pub fn seal_in_place_append_tag( - &mut self, // &mut — exclusive access + &mut self, // &mut — эксклюзивный доступ aad: Aad<&[u8]>, in_out: &mut Vec, ) -> Result<(), Unspecified> { - let nonce = self.nonce_seq.advance()?; // auto-generate unique nonce - // ... encrypt with nonce ... + let nonce = self.nonce_seq.advance()?; // автоматически генерируем уникальный nonce + // ... шифруем с nonce ... Ok(()) } } @@ -110,32 +106,31 @@ impl SealingKey { # pub struct Unspecified; ``` -The ownership chain prevents: -1. **Nonce reuse** — `Nonce` is not `Clone`, consumed on each call -2. **Key duplication** — `UnboundKey` is moved into `SealingKey`, can't also make an `OpeningKey` -3. **Sequence duplication** — `NonceSequence` is not `Clone`, so no two keys share a counter +Цепочка владения предотвращает: +1. **Повторное использование nonce** — `Nonce` не `Clone` и потребляется при каждом вызове +2. **Дублирование ключа** — `UnboundKey` перемещается в `SealingKey`, поэтому из него нельзя также создать `OpeningKey` +3. **Дублирование последовательности** — `NonceSequence` не `Clone`, поэтому два ключа не могут делить один счётчик -**None of these require runtime checks.** The compiler enforces all three. +**Ни одна из этих гарантий не требует проверок во время выполнения.** Компилятор обеспечивает все три. -## Case Study: Ephemeral Key Agreement +## Разбор: эфемерное согласование ключей -Ephemeral Diffie-Hellman keys must be used **exactly once** (that's what "ephemeral" means). -`ring` enforces this: +Эфемерные ключи Диффи — Хеллмана должны использоваться **ровно один раз** (именно это и означает «эфемерный»). `ring` обеспечивает это: ```rust,ignore -/// An ephemeral private key. Not Clone, not Copy. -/// Consumed by agree_ephemeral(). +/// Эфемерный приватный ключ. Не Clone, не Copy. +/// Потребляется в agree_ephemeral(). pub struct EphemeralPrivateKey { /* ... */ } -/// Compute shared secret — consumes the private key. +/// Вычисляет общий секрет — потребляет приватный ключ. pub fn agree_ephemeral( - my_private_key: EphemeralPrivateKey, // ← moved + my_private_key: EphemeralPrivateKey, // ← перемещается peer_public_key: &UnparsedPublicKey, error_value: Unspecified, kdf: impl FnOnce(&[u8]) -> Result, ) -> Result { - // ... DH computation ... - // my_private_key is consumed — can never be reused + // ... вычисление DH ... + // my_private_key потреблён — переиспользовать его невозможно # kdf(&[]) } # pub struct UnparsedPublicKey; @@ -143,46 +138,42 @@ pub fn agree_ephemeral( # pub struct Unspecified; ``` -After calling `agree_ephemeral()`, the private key **no longer exists in memory** -(it's been dropped). A C++ developer would need to remember to `memset(key, 0, len)` -and hope the compiler doesn't optimise it away. In Rust, the key is simply gone. +После вызова `agree_ephemeral()` приватный ключ **больше не существует в памяти** (он уничтожен). Разработчику на C++ пришлось бы не забыть вызвать `memset(key, 0, len)` и надеяться, что компилятор не оптимизирует этот вызов. В Rust ключ просто исчезает. -## Hardware Application: One-Time Fuse Programming +## Аппаратное применение: однократное программирование предохранителей -Server platforms have **OTP (one-time programmable) fuses** for security keys, -board serial numbers, and feature bits. Writing a fuse is irreversible — doing it -twice with different data bricks the board. This is a perfect fit for move semantics: +Серверные платформы содержат **OTP-предохранители (one-time programmable)** для ключей безопасности, серийных номеров плат и битов функций. Запись в предохранитель необратима: если сделать это дважды с разными данными, плата выходит из строя. Это идеально подходит для семантики перемещения: ```rust,ignore use std::io; -/// A fuse write payload. Not Clone, not Copy. -/// Consumed when the fuse is programmed. +/// Полезная нагрузка для записи в предохранитель. Не Clone, не Copy. +/// Потребляется при программировании предохранителя. pub struct FusePayload { address: u32, data: Vec, - // private constructor — only created via validated builder + // приватный конструктор — создаётся только через проверенный builder } -/// Proof that the fuse programmer is in the correct state. +/// Доказательство того, что программатор предохранителей находится в корректном состоянии. pub struct FuseController { - /* hardware handle */ + /* дескриптор аппаратуры */ } impl FuseController { - /// Program a fuse — consumes the payload, preventing double-write. + /// Программирует предохранитель — потребляет полезную нагрузку, исключая двойную запись. pub fn program( &mut self, - payload: FusePayload, // ← moved — can't be used twice + payload: FusePayload, // ← перемещается — нельзя использовать дважды ) -> io::Result<()> { - // ... write to OTP hardware ... - // payload is consumed — trying to program again with the same - // payload is a compile error + // ... запись в OTP-аппаратуру ... + // payload потреблён — повторное программирование с тем же payload + // является ошибкой компиляции Ok(()) } } -/// Builder with validation — only way to create a FusePayload. +/// Builder с валидацией — единственный способ создать FusePayload. pub struct FusePayloadBuilder { address: Option, data: Option>, @@ -204,14 +195,14 @@ impl FusePayloadBuilder { } pub fn build(self) -> Result { - let address = self.address.ok_or("address required")?; - let data = self.data.ok_or("data required")?; - if data.len() > 32 { return Err("fuse data too long"); } + let address = self.address.ok_or("требуется адрес")?; + let data = self.data.ok_or("требуются данные")?; + if data.len() > 32 { return Err("данные предохранителя слишком длинные"); } Ok(FusePayload { address, data }) } } -// Usage: +// Использование: fn program_board_serial(ctrl: &mut FuseController) -> io::Result<()> { let payload = FusePayloadBuilder::new() .address(0x100) @@ -219,22 +210,21 @@ fn program_board_serial(ctrl: &mut FuseController) -> io::Result<()> { .build() .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; - ctrl.program(payload)?; // ✅ payload consumed + ctrl.program(payload)?; // ✅ payload потреблён - // ctrl.program(payload); // ❌ ERROR: use of moved value - // ^^^^^^^ value used after move + // ctrl.program(payload); // ❌ ОШИБКА: использование перемещённого значения + // ^^^^^^^ значение используется после перемещения Ok(()) } ``` -## Hardware Application: Single-Use Calibration Token +## Аппаратное применение: одноразовый токен калибровки -Some sensors require a calibration step that must happen **exactly once** per power -cycle. A calibration token enforces this: +Некоторые датчики требуют шага калибровки, который должен произойти **ровно один раз** за цикл питания. Токен калибровки обеспечивает это: ```rust,ignore -/// Issued once at power-on. Not Clone, not Copy. +/// Выдаётся один раз при включении питания. Не Clone, не Copy. pub struct CalibrationToken { _private: (), } @@ -244,7 +234,7 @@ pub struct SensorController { } impl SensorController { - /// Called once at power-on — returns a calibration token. + /// Вызывается один раз при включении питания — возвращает токен калибровки. pub fn power_on() -> (Self, CalibrationToken) { ( SensorController { calibrated: false }, @@ -252,89 +242,89 @@ impl SensorController { ) } - /// Calibrate the sensor — consumes the token. + /// Калибрует датчик — потребляет токен. pub fn calibrate(&mut self, _token: CalibrationToken) -> io::Result<()> { - // ... run calibration sequence ... + // ... выполняем последовательность калибровки ... self.calibrated = true; Ok(()) } - /// Read a sensor — only meaningful after calibration. + /// Читает датчик — имеет смысл только после калибровки. /// - /// **Limitation:** The move-semantics guarantee is *partial*. The caller - /// can `drop(cal_token)` without calling `calibrate()` — the token will - /// be destroyed but calibration won't run. The `#[must_use]` annotation - /// (see below) generates a warning but not a hard error. + /// **Ограничение:** гарантия семантики перемещения здесь *частичная*. Вызывающий код + /// может сделать `drop(cal_token)`, не вызывая `calibrate()`: токен будет уничтожен, + /// но калибровка не выполнится. Аннотация `#[must_use]` (см. ниже) выдаёт предупреждение, + /// но не жёсткую ошибку. /// - /// The runtime `self.calibrated` check here is the **safety net** for - /// that gap. For a fully compile-time solution, see the type-state - /// pattern in ch05 where `send_command()` only exists on `IpmiSession`. + /// Проверка `self.calibrated` во время выполнения — это **страховочная сетка** для + /// этого пробела. Полностью решить задачу на этапе компиляции позволяет паттерн + /// typestate из гл. 05, где `send_command()` существует только у `IpmiSession`. pub fn read(&self) -> io::Result { if !self.calibrated { - return Err(io::Error::new(io::ErrorKind::Other, "not calibrated")); + return Err(io::Error::new(io::ErrorKind::Other, "датчик не откалиброван")); } - Ok(25.0) // stub + Ok(25.0) // заглушка } } fn sensor_workflow() -> io::Result<()> { let (mut ctrl, cal_token) = SensorController::power_on(); - // Must use cal_token somewhere — it's not Copy, so dropping it - // without consuming it generates a warning (or error with #[must_use]) + // Токен нужно где-то использовать: он не Copy, поэтому отбрасывание без потребления + // даёт предупреждение (или ошибку при #[must_use]) ctrl.calibrate(cal_token)?; - // Now reads work: + // Теперь чтение работает: let temp = ctrl.read()?; - println!("Temperature: {temp}°C"); + println!("Температура: {temp}°C"); - // Can't calibrate again — token was consumed: - // ctrl.calibrate(cal_token); // ❌ use of moved value + // Повторно калибровать нельзя — токен потреблён: + // ctrl.calibrate(cal_token); // ❌ использование перемещённого значения Ok(()) } ``` -### When to Use Single-Use Types +### Когда использовать одноразовые типы -| Scenario | Use single-use (move) semantics? | +| Сценарий | Использовать семантику одноразового (перемещаемого) значения? | |----------|:------:| -| Cryptographic nonces | ✅ Always — nonce reuse is catastrophic | -| Ephemeral keys (DH, ECDH) | ✅ Always — reuse weakens forward secrecy | -| OTP fuse writes | ✅ Always — double-write bricks hardware | -| License activation codes | ✅ Usually — prevent double-activation | -| Calibration tokens | ✅ Usually — enforce once-per-session | -| File write handles | ⚠️ Sometimes — depends on protocol | -| Database transaction handles | ⚠️ Sometimes — commit/rollback is single-use | -| General data buffers | ❌ These need reuse — use `&mut [u8]` | +| Криптографические nonce | ✅ Всегда: повторное использование nonce катастрофично | +| Эфемерные ключи (DH, ECDH) | ✅ Всегда: повторное использование ослабляет прямую секретность | +| Запись в OTP-предохранители | ✅ Всегда: двойная запись выводит аппаратуру из строя | +| Коды активации лицензий | ✅ Обычно: предотвращает двойную активацию | +| Токены калибровки | ✅ Обычно: обеспечивает однократность за сессию | +| Дескрипторы записи файлов | ⚠️ Иногда: зависит от протокола | +| Дескрипторы транзакций БД | ⚠️ Иногда: commit/rollback выполняются однократно | +| Общие буферы данных | ❌ Их нужно переиспользовать: используйте `&mut [u8]` | -## Single-Use Ownership Flow +## Поток одноразового владения ```mermaid flowchart LR - N["Nonce::new()"] -->|move| E["encrypt(nonce, msg)"] - E -->|consumed| X["❌ nonce gone"] - N -.->|"reuse attempt"| ERR["COMPILE ERROR:\nuse of moved value"] + N["Nonce::new()"] -->|перемещение| E["encrypt(nonce, msg)"] + E -->|потреблён| X["❌ nonce уничтожен"] + N -.->|"попытка повторного использования"| ERR["ОШИБКА КОМПИЛЯЦИИ:\nuse of moved value"] style N fill:#e1f5fe,color:#000 style E fill:#c8e6c9,color:#000 style X fill:#ffcdd2,color:#000 style ERR fill:#ffcdd2,color:#000 ``` -## Exercise: Single-Use Firmware Signing Token +## Упражнение: одноразовый токен подписи прошивки -Design a `SigningToken` that can be used exactly once to sign a firmware image: -- `SigningToken::issue(key_id: &str) -> SigningToken` (not Clone, not Copy) -- `sign(token: SigningToken, image: &[u8]) -> SignedImage` (consumes the token) -- Attempting to sign twice should be a compile error. +Спроектируйте `SigningToken`, который можно использовать ровно один раз для подписи образа прошивки: +- `SigningToken::issue(key_id: &str) -> SigningToken` (не Clone, не Copy) +- `sign(token: SigningToken, image: &[u8]) -> SignedImage` (потребляет токен) +- Попытка подписать дважды должна приводить к ошибке компиляции.
-Solution +Решение ```rust,ignore pub struct SigningToken { key_id: String, - // NOT Clone, NOT Copy + // НЕ Clone, НЕ Copy } pub struct SignedImage { @@ -349,30 +339,29 @@ impl SigningToken { } pub fn sign(token: SigningToken, _image: &[u8]) -> SignedImage { - // Token consumed by move — can't be reused + // Токен потреблён перемещением — переиспользовать его нельзя SignedImage { - signature: vec![0xDE, 0xAD], // stub + signature: vec![0xDE, 0xAD], // заглушка key_id: token.key_id, } } -// ✅ Compiles: +// ✅ Компилируется: // let tok = SigningToken::issue("release-key"); // let signed = sign(tok, &firmware_bytes); // -// ❌ Compile error: -// let signed2 = sign(tok, &other_bytes); // ERROR: use of moved value +// ❌ Ошибка компиляции: +// let signed2 = sign(tok, &other_bytes); // ОШИБКА: использование перемещённого значения ```
-## Key Takeaways +## Ключевые выводы -1. **Move = linear use** — a non-Clone, non-Copy type can be consumed exactly once; the compiler enforces this. -2. **Nonce reuse is catastrophic** — Rust's ownership system prevents it structurally, not by discipline. -3. **Pattern applies beyond crypto** — OTP fuses, calibration tokens, audit entries — anything that must happen at most once. -4. **Ephemeral keys get forward secrecy for free** — the key agreement value is moved into the derived secret and vanishes. -5. **When in doubt, remove `Clone`** — you can always add it later; removing it from a published API is a breaking change. +1. **Перемещение = линейное использование** — тип, который не является ни `Clone`, ни `Copy`, можно потребить ровно один раз; это обеспечивает компилятор. +2. **Повторное использование nonce катастрофично** — система владения Rust предотвращает его структурно, а не дисциплиной. +3. **Паттерн выходит за рамки криптографии** — OTP-предохранители, токены калибровки, записи аудита: всё, что должно произойти не более одного раза. +4. **Эфемерные ключи получают прямую секретность бесплатно** — значение ключевого соглашения перемещается в производный секрет и исчезает. +5. **Если сомневаетесь, уберите `Clone`** — его всегда можно добавить позже; удаление из опубликованного API — ломающее изменение. --- - diff --git a/type-driven-correctness-book/src/ch04-capability-tokens-zero-cost-proof-of-aut.md b/type-driven-correctness-book/src/ch04-capability-tokens-zero-cost-proof-of-aut.md index f59d8fd..49872c8 100644 --- a/type-driven-correctness-book/src/ch04-capability-tokens-zero-cost-proof-of-aut.md +++ b/type-driven-correctness-book/src/ch04-capability-tokens-zero-cost-proof-of-aut.md @@ -1,54 +1,51 @@ -# Capability Tokens — Zero-Cost Proof of Authority 🟡 +# Capability-токены: доказательство полномочий без накладных расходов 🟡 -> **What you'll learn:** How zero-sized types (ZSTs) act as compile-time proof tokens, enforcing privilege hierarchies, power sequencing, and revocable authority — all at zero runtime cost. +> **Что вы узнаете:** как типы нулевого размера (ZST) выступают токенами-доказательствами на этапе компиляции и обеспечивают иерархию привилегий, порядок подачи питания и отзываемые полномочия, причём без накладных расходов во время выполнения. > -> **Cross-references:** [ch03](ch03-single-use-types-cryptographic-guarantee.md) (single-use types), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (type-state), [ch08](ch08-capability-mixins-compile-time-hardware-.md) (mixins), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (integration) +> **Перекрёстные ссылки:** [гл. 03](ch03-single-use-types-cryptographic-guarantee.md) (одноразовые типы), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (typestate), [гл. 08](ch08-capability-mixins-compile-time-hardware-.md) (миксины), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция) -## The Problem: Who Is Allowed to Do What? +## Проблема: кому что разрешено? -In hardware diagnostics, some operations are **dangerous**: +В аппаратной диагностике некоторые операции **опасны**: -- Programming BMC firmware -- Resetting PCIe links -- Writing OTP fuses -- Enabling high-voltage test modes +- Прошивка BMC +- Сброс линков PCIe +- Запись OTP-предохранителей +- Включение тестовых режимов с высоким напряжением -In C/C++, these are guarded by runtime checks: +В C/C++ такие операции защищают проверками во время выполнения: ```c -// C — runtime permission check +// C — проверка прав во время выполнения int reset_pcie_link(bmc_handle_t bmc, int slot) { - if (!bmc->is_admin) { // runtime check + if (!bmc->is_admin) { // проверка во время выполнения return -EPERM; } - if (!bmc->link_trained) { // another runtime check + if (!bmc->link_trained) { // ещё одна проверка во время выполнения return -EINVAL; } - // ... do the dangerous thing ... + // ... выполняем опасное действие ... return 0; } ``` -Every function that does something dangerous must repeat these checks. Forget one, -and you have a privilege escalation bug. +Каждая функция, которая делает что-то опасное, должна повторять эти проверки. Забудьте одну — и у вас ошибка эскалации привилегий. -## Zero-Sized Types as Proof Tokens +## Типы нулевого размера как токены-доказательства -A **capability token** is a zero-sized type (ZST) that proves the caller has -the authority to perform an action. It costs **zero bytes** at runtime — it exists -only in the type system: +**Capability-токен** — это тип нулевого размера (ZST), который доказывает, что у вызывающего кода есть полномочия на действие. Во время выполнения он не занимает **ни одного байта**: он существует только в системе типов: ```rust,ignore use std::marker::PhantomData; -/// Proof that the caller has admin privileges. -/// Zero-sized — compiles away completely. -/// Not Clone, not Copy — must be explicitly passed. +/// Доказательство того, что вызывающий обладает правами администратора. +/// Нулевой размер — полностью исчезает при компиляции. +/// Не Clone, не Copy — должен передаваться явно. pub struct AdminToken { - _private: (), // prevents construction outside this module + _private: (), // запрещает создание вне этого модуля } -/// Proof that the PCIe link is trained and ready. +/// Доказательство того, что линк PCIe обучен и готов. pub struct LinkTrainedToken { _private: (), } @@ -56,78 +53,74 @@ pub struct LinkTrainedToken { pub struct BmcController { /* ... */ } impl BmcController { - /// Authenticate as admin — returns a capability token. - /// This is the ONLY way to create an AdminToken. + /// Аутентификация как администратор — возвращает capability-токен. + /// Это ЕДИНСТВЕННЫЙ способ создать AdminToken. pub fn authenticate_admin( &mut self, credentials: &[u8], ) -> Result { - // ... validate credentials ... + // ... проверяем учётные данные ... # let valid = true; if valid { Ok(AdminToken { _private: () }) } else { - Err("authentication failed") + Err("аутентификация не пройдена") } } - /// Train the PCIe link — returns proof that it's trained. + /// Обучает линк PCIe — возвращает доказательство того, что он обучен. pub fn train_link(&mut self) -> Result { - // ... perform link training ... + // ... выполняем обучение линка ... Ok(LinkTrainedToken { _private: () }) } - /// Reset a PCIe link — requires BOTH admin + link-trained proof. - /// No runtime checks needed — the tokens ARE the proof. + /// Сбрасывает линк PCIe — требует ОБА доказательства: администратора и обученного линка. + /// Никаких проверок во время выполнения не нужно: сами токены и есть доказательство. pub fn reset_pcie_link( &mut self, - _admin: &AdminToken, // zero-cost proof of authority - _trained: &LinkTrainedToken, // zero-cost proof of state + _admin: &AdminToken, // zero-cost доказательство полномочий + _trained: &LinkTrainedToken, // zero-cost доказательство состояния slot: u32, ) -> Result<(), &'static str> { - println!("Resetting PCIe link on slot {slot}"); + println!("Сброс линка PCIe в слоте {slot}"); Ok(()) } } ``` -Usage — the type system enforces the workflow: +Использование: система типов обеспечивает соблюдение порядка действий: ```rust,ignore fn maintenance_workflow(bmc: &mut BmcController) -> Result<(), &'static str> { - // Step 1: Authenticate — get admin proof + // Шаг 1: аутентификация — получаем доказательство администратора let admin = bmc.authenticate_admin(b"secret")?; - // Step 2: Train link — get trained proof + // Шаг 2: обучение линка — получаем доказательство обучения let trained = bmc.train_link()?; - // Step 3: Reset — compiler requires both tokens + // Шаг 3: сброс — компилятор требует оба токена bmc.reset_pcie_link(&admin, &trained, 0)?; Ok(()) } -// This WON'T compile: +// Это НЕ скомпилируется: fn unprivileged_attempt(bmc: &mut BmcController) -> Result<(), &'static str> { let trained = bmc.train_link()?; // bmc.reset_pcie_link(???, &trained, 0)?; - // ^^^ no AdminToken — can't call this + // ^^^ нет AdminToken — вызвать эту функцию нельзя Ok(()) } ``` -The `AdminToken` and `LinkTrainedToken` are **zero bytes** in the compiled binary. -They exist only during type-checking. The function signature `fn reset_pcie_link(&mut self, _admin: &AdminToken, ...)` is a **proof obligation** — "you may only -call this if you can produce an `AdminToken`" — and the only way to produce one is -through `authenticate_admin()`. +`AdminToken` и `LinkTrainedToken` занимают **ноль байт** в скомпилированном бинарнике. Они существуют только во время проверки типов. Сигнатура `fn reset_pcie_link(&mut self, _admin: &AdminToken, ...)` — это **обязательство доказательства** («вызвать эту функцию можно, только если вы можете получить `AdminToken`»), а получить его можно только через `authenticate_admin()`. -## Power Sequencing Authority +## Полномочия на управление питанием -Server power sequencing has strict ordering: standby → auxiliary → main → CPU. -Reversing the sequence can damage hardware. Capability tokens enforce ordering: +Последовательность подачи питания на сервере строго определена: дежурное питание → вспомогательное → основное → CPU. Нарушение порядка может повредить аппаратуру. Capability-токены обеспечивают соблюдение порядка: ```rust,ignore -/// State tokens — each one proves the previous step completed. +/// Токены состояния — каждый доказывает, что предыдущий шаг завершён. pub struct StandbyOn { _p: () } pub struct AuxiliaryOn { _p: () } pub struct MainOn { _p: () } @@ -136,36 +129,36 @@ pub struct CpuPowered { _p: () } pub struct PowerController { /* ... */ } impl PowerController { - /// Step 1: Enable standby power. No precondition. + /// Шаг 1: включить дежурное питание. Предусловий нет. pub fn enable_standby(&mut self) -> Result { - println!("Standby power ON"); + println!("Дежурное питание ВКЛ"); Ok(StandbyOn { _p: () }) } - /// Step 2: Enable auxiliary — requires standby proof. + /// Шаг 2: включить вспомогательное питание — требует доказательство дежурного. pub fn enable_auxiliary( &mut self, _standby: &StandbyOn, ) -> Result { - println!("Auxiliary power ON"); + println!("Вспомогательное питание ВКЛ"); Ok(AuxiliaryOn { _p: () }) } - /// Step 3: Enable main — requires auxiliary proof. + /// Шаг 3: включить основное питание — требует доказательство вспомогательного. pub fn enable_main( &mut self, _aux: &AuxiliaryOn, ) -> Result { - println!("Main power ON"); + println!("Основное питание ВКЛ"); Ok(MainOn { _p: () }) } - /// Step 4: Power CPU — requires main proof. + /// Шаг 4: подать питание на CPU — требует доказательство основного питания. pub fn power_cpu( &mut self, _main: &MainOn, ) -> Result { - println!("CPU powered ON"); + println!("Питание CPU ВКЛ"); Ok(CpuPowered { _p: () }) } } @@ -178,30 +171,29 @@ fn power_on_sequence(ctrl: &mut PowerController) -> Result u64; } -/// Operator can read sensors and run non-destructive diagnostics. +/// Оператор может читать датчики и запускать неразрушающую диагностику. pub trait Operator: Authenticated {} -/// Admin can do everything an operator can, plus destructive operations. +/// Администратор может всё, что оператор, плюс разрушающие операции. pub trait Admin: Operator {} -// Concrete tokens: +// Конкретные токены: pub struct UserToken { id: u64 } pub struct OperatorToken { id: u64 } pub struct AdminCapToken { id: u64 } @@ -216,35 +208,32 @@ impl Admin for AdminCapToken {} pub struct Bmc { /* ... */ } impl Bmc { - /// Anyone authenticated can read sensors. + /// Любой аутентифицированный может читать датчики. pub fn read_sensor(&self, _who: &impl Authenticated, id: u32) -> f64 { - 42.0 // stub + 42.0 // заглушка } - /// Only operators and above can run diagnostics. + /// Запускать диагностику могут только операторы и выше. pub fn run_diag(&mut self, _who: &impl Operator, test: &str) -> bool { - true // stub + true // заглушка } - /// Only admins can flash firmware. + /// Прошивать прошивку могут только администраторы. pub fn flash_firmware(&mut self, _who: &impl Admin, image: &[u8]) -> Result<(), &'static str> { - Ok(()) // stub + Ok(()) // заглушка } } ``` -An `AdminCapToken` can be passed to any function — it satisfies `Authenticated`, -`Operator`, and `Admin`. A `UserToken` can only call `read_sensor()`. The compiler -enforces the entire privilege model **at zero runtime cost**. +`AdminCapToken` можно передать в любую функцию: он удовлетворяет `Authenticated`, `Operator` и `Admin`. `UserToken` может вызывать только `read_sensor()`. Компилятор обеспечивает всю модель привилегий **без накладных расходов во время выполнения**. -## Lifetime-Bounded Capability Tokens +## Capability-токены, ограниченные временем жизни -Sometimes a capability should be **scoped** — valid only within a certain lifetime. -Rust's borrow checker handles this naturally: +Иногда полномочие должно быть **ограниченным по области действия**, то есть действительным только в пределах определённого времени жизни. Borrow checker решает эту задачу естественным образом: ```rust,ignore -/// A scoped admin session. The token borrows the session, -/// so it cannot outlive it. +/// Ограниченная административная сессия. Токен заимствует сессию, +/// поэтому он не может её пережить. pub struct AdminSession { _active: bool, } @@ -255,11 +244,11 @@ pub struct ScopedAdminToken<'session> { impl AdminSession { pub fn begin(credentials: &[u8]) -> Result { - // ... authenticate ... + // ... аутентификация ... Ok(AdminSession { _active: true }) } - /// Create a scoped token — lives only as long as the session. + /// Создаёт ограниченный токен — он живёт ровно столько, сколько сессия. pub fn token(&self) -> ScopedAdminToken<'_> { ScopedAdminToken { _session: self } } @@ -269,52 +258,52 @@ fn scoped_example() -> Result<(), &'static str> { let session = AdminSession::begin(b"credentials")?; let token = session.token(); - // Use token within this scope... - // When session drops, token is invalidated by the borrow checker. - // No need for runtime expiry checks. + // Используем токен в этой области видимости... + // Когда session уничтожается, токен становится недействительным благодаря borrow checker. + // Проверки истечения срока во время выполнения не нужны. // drop(session); - // ❌ ERROR: cannot move out of `session` because it is borrowed - // (by `token`, which holds &session) + // ❌ Ошибка компилятора: cannot move out of `session` because it is borrowed + // (by `token`, который хранит &session) // - // Even if we skip drop() and just try to use `token` after - // session goes out of scope — same error: lifetime mismatch. + // Даже если пропустить drop() и попытаться использовать `token` после того, как + // session выйдет из области видимости, — будет та же ошибка: несоответствие времён жизни. Ok(()) } ``` -### When to Use Capability Tokens +### Когда использовать capability-токены -| Scenario | Pattern | +| Сценарий | Паттерн | |----------|---------| -| Privileged hardware operations | ZST proof token (AdminToken) | -| Multi-step sequencing | Chain of state tokens (StandbyOn → AuxiliaryOn → ...) | -| Role-based access control | Trait hierarchy (Authenticated → Operator → Admin) | -| Time-limited privileges | Lifetime-bounded tokens (`ScopedAdminToken<'a>`) | -| Cross-module authority | Public token type, private constructor | +| Привилегированные операции с аппаратурой | ZST-токен-доказательство (AdminToken) | +| Многошаговая последовательность | Цепочка токенов состояния (StandbyOn → AuxiliaryOn → ...) | +| Управление доступом на основе ролей (RBAC) | Иерархия трейтов (Authenticated → Operator → Admin) | +| Привилегии с ограничением по времени | Токены с ограниченным временем жизни (`ScopedAdminToken<'a>`) | +| Полномочия между модулями | Публичный тип токена, приватный конструктор | -### Cost Summary +### Сводка по стоимости -| What | Runtime cost | -|------|:------:| -| ZST token in memory | 0 bytes | -| Token parameter passing | Optimised away by LLVM | -| Trait hierarchy dispatch | Static dispatch (monomorphised) | -| Lifetime enforcement | Compile-time only | +| Что | Стоимость во время выполнения | +|-----|:------:| +| ZST-токен в памяти | 0 байт | +| Передача токена как параметра | Оптимизируется LLVM | +| Диспетчеризация по иерархии трейтов | Статическая (мономорфизация) | +| Проверка времени жизни | Только на этапе компиляции | -**Total runtime overhead: zero.** The privilege model exists only in the type system. +**Итоговые накладные расходы во время выполнения: ноль.** Модель привилегий существует только в системе типов. -## Capability Token Hierarchy +## Иерархия capability-токенов ```mermaid flowchart TD - AUTH["authenticate(user, pass)"] -->|returns| AT["AdminToken"] + AUTH["authenticate(user, pass)"] -->|возвращает| AT["AdminToken"] AT -->|"&AdminToken"| FW["firmware_update()"] AT -->|"&AdminToken"| RST["reset_pcie_link()"] - AT -->|downgrade| OP["OperatorToken"] + AT -->|понижение| OP["OperatorToken"] OP -->|"&OperatorToken"| RD["read_sensors()"] - OP -.->|"attempt firmware_update"| ERR["❌ Compile Error"] + OP -.->|"попытка firmware_update"| ERR["❌ Ошибка компиляции"] style AUTH fill:#e1f5fe,color:#000 style AT fill:#c8e6c9,color:#000 style OP fill:#fff3e0,color:#000 @@ -324,24 +313,24 @@ flowchart TD style ERR fill:#ffcdd2,color:#000 ``` -## Exercise: Tiered Diagnostic Permissions +## Упражнение: многоуровневые диагностические полномочия -Design a three-tier capability system: `ViewerToken`, `TechToken`, `EngineerToken`. -- Viewers can call `read_status()` -- Techs can also call `run_quick_diag()` -- Engineers can also call `flash_firmware()` -- Higher tiers can do everything lower tiers can (use trait bounds or token conversion). +Спроектируйте трёхуровневую систему capability: `ViewerToken`, `TechToken`, `EngineerToken`. +- Наблюдатели могут вызывать `read_status()` +- Техники могут также вызывать `run_quick_diag()` +- Инженеры могут также вызывать `flash_firmware()` +- Более высокие уровни могут делать всё, что могут низшие (используйте ограничения трейтов или преобразование токенов).
-Solution +Решение ```rust,ignore -// Tokens — zero-sized, private constructors +// Токены — нулевого размера, с приватными конструкторами pub struct ViewerToken { _private: () } pub struct TechToken { _private: () } pub struct EngineerToken { _private: () } -// Capability traits — hierarchical +// Трейты capability — иерархические pub trait CanView {} pub trait CanDiag: CanView {} pub trait CanFlash: CanDiag {} @@ -354,27 +343,26 @@ impl CanDiag for EngineerToken {} impl CanFlash for EngineerToken {} pub fn read_status(_tok: &impl CanView) -> String { - "status: OK".into() + "статус: OK".into() } pub fn run_quick_diag(_tok: &impl CanDiag) -> String { - "diag: PASS".into() + "диагностика: PASS".into() } pub fn flash_firmware(_tok: &impl CanFlash, _image: &[u8]) { - // Only engineers reach here + // Сюда попадают только инженеры } ```
-## Key Takeaways +## Ключевые выводы -1. **ZST tokens cost zero bytes** — they exist only in the type system; LLVM optimises them away completely. -2. **Private constructors = unforgeable** — only your module's `authenticate()` can mint a token. -3. **Trait hierarchies model permission levels** — `CanFlash: CanDiag: CanView` mirrors real RBAC. -4. **Lifetime-bounded tokens revoke automatically** — `ScopedAdminToken<'session>` can't outlive the session. -5. **Combine with type-state (ch05)** for protocols that require authentication *and* sequenced operations. +1. **ZST-токены не занимают байтов** — они существуют только в системе типов; LLVM полностью их оптимизирует. +2. **Приватные конструкторы делают токены неподделываемыми** — токен может создать только `authenticate()` вашего модуля. +3. **Иерархии трейтов моделируют уровни разрешений** — `CanFlash: CanDiag: CanView` повторяет реальный RBAC. +4. **Токены с ограниченным временем жизни отзываются автоматически** — `ScopedAdminToken<'session>` не может пережить сессию. +5. **Сочетайте с typestate (гл. 05)** для протоколов, которые требуют одновременно аутентификации и упорядоченных операций. --- - diff --git a/type-driven-correctness-book/src/ch05-protocol-state-machines-type-state-for-r.md b/type-driven-correctness-book/src/ch05-protocol-state-machines-type-state-for-r.md index 779326c..6c1b485 100644 --- a/type-driven-correctness-book/src/ch05-protocol-state-machines-type-state-for-r.md +++ b/type-driven-correctness-book/src/ch05-protocol-state-machines-type-state-for-r.md @@ -1,17 +1,14 @@ -# Protocol State Machines — Type-State for Real Hardware 🔴 +# Автоматы состояний протоколов: typestate для реального оборудования 🔴 -> **What you'll learn:** How type-state encoding makes protocol violations (wrong-order commands, use-after-close) into compile errors, applied to IPMI session lifecycles and PCIe link training. +> **Что вы узнаете:** как кодирование через typestate превращает нарушения протокола (команды в неверном порядке, использование после закрытия) в ошибки компиляции. Применим к жизненному циклу сессий IPMI и обучению линка PCIe. > -> **Cross-references:** [ch01](ch01-the-philosophy-why-types-beat-tests.md) (level 2 — state correctness), [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (tokens), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types), [ch11](ch11-fourteen-tricks-from-the-trenches.md) (trick 4 — typestate builder, trick 8 — async type-state) +> **Перекрёстные ссылки:** [гл. 01](ch01-the-philosophy-why-types-beat-tests.md) (уровень 2: корректность состояний), [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (токены), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы), [гл. 11](ch11-fourteen-tricks-from-the-trenches.md) (приём 4: typestate-билдер, приём 8: async typestate) -## The Problem: Protocol Violations +## Проблема: нарушения протокола -Hardware protocols have **strict state machines**. An IPMI session has states: -Unauthenticated → Authenticated → Active → Closed. PCIe link training goes through -Detect → Polling → Configuration → L0. Sending a command in the wrong state -corrupts the session or hangs the bus. +У аппаратных протоколов **строгие автоматы состояний**. У сессии IPMI есть состояния: не аутентифицирована (`Idle`) → аутентифицирована (`Authenticated`) → активна (`Active`) → закрыта (`Closed`). Обучение линка PCIe проходит через фазы Detect → Polling → Configuration → L0. Отправка команды в неверном состоянии повреждает сессию или зависает шину. -**IPMI session state machine:** +**Автомат состояний сессии IPMI:** ```mermaid stateDiagram-v2 @@ -22,27 +19,27 @@ stateDiagram-v2 Active --> Closed : close() Closed --> [*] - note right of Active : send_command() only exists here - note right of Idle : send_command() → compile error + note right of Active : send_command() существует только здесь + note right of Idle : send_command() → ошибка компиляции ``` -**PCIe Link Training State Machine (LTSSM):** +**Автомат состояний обучения линка PCIe (LTSSM):** ```mermaid stateDiagram-v2 [*] --> Detect - Detect --> Polling : receiver detected - Polling --> Configuration : bit lock + symbol lock - Configuration --> L0 : link number + lane assigned + Detect --> Polling : приёмник обнаружен + Polling --> Configuration : блокировка битов + блокировка символов + Configuration --> L0 : номер линка + линии назначены L0 --> L0 : send_tlp() / receive_tlp() - L0 --> Recovery : error threshold - Recovery --> L0 : retrained - Recovery --> Detect : retraining failed + L0 --> Recovery : порог ошибок + Recovery --> L0 : переобучен + Recovery --> Detect : переобучение не удалось - note right of L0 : TLP transmit only in L0 + note right of L0 : передача TLP только в L0 ``` -In C/C++, state is tracked with an enum and runtime checks: +На C/C++ состояние отслеживают с помощью enum и проверок во время выполнения: ```c typedef enum { IDLE, AUTHENTICATED, ACTIVE, CLOSED } session_state_t; @@ -54,40 +51,38 @@ typedef struct { } ipmi_session_t; int ipmi_send_command(ipmi_session_t *s, uint8_t cmd, uint8_t *data, int len) { - if (s->state != ACTIVE) { // runtime check — easy to forget + if (s->state != ACTIVE) { // проверка во время выполнения — её легко забыть return -EINVAL; } - // ... send command ... + // ... отправляем команду ... return 0; } ``` -## Type-State Pattern +## Паттерн typestate -With type-state, each protocol state is a **distinct type**. Transitions are methods -that consume one state and return another. The compiler prevents calling methods in -the wrong state because **those methods don't exist on that type**. +С typestate каждое состояние протокола — это **отдельный тип**. Переходы — это методы, которые потребляют одно состояние и возвращают другое. Компилятор не даёт вызывать методы в неверном состоянии, потому что **этих методов у такого типа просто нет**. + +## Разбор: жизненный цикл сессии IPMI ```rust,ignore use std::marker::PhantomData; -// States — zero-sized marker types +// Состояния — маркерные типы нулевого размера pub struct Idle; -## Case Study: IPMI Session Lifecycle - pub struct Authenticated; pub struct Active; pub struct Closed; -/// IPMI session parameterised by its current state. -/// The state exists ONLY in the type system (PhantomData is zero-sized). +/// Сессия IPMI, параметризованная текущим состоянием. +/// Состояние существует ТОЛЬКО в системе типов (PhantomData занимает ноль байт). pub struct IpmiSession { - transport: String, // e.g., "192.168.1.100" + transport: String, // например, "192.168.1.100" session_id: Option, _state: PhantomData, } -// Transition: Idle → Authenticated +// Переход: Idle → Authenticated impl IpmiSession { pub fn new(host: &str) -> Self { IpmiSession { @@ -98,11 +93,11 @@ impl IpmiSession { } pub fn authenticate( - self, // ← consumes Idle session + self, // ← потребляет сессию в состоянии Idle user: &str, pass: &str, ) -> Result, String> { - println!("Authenticating {user} on {}", self.transport); + println!("Аутентификация {user} на {}", self.transport); Ok(IpmiSession { transport: self.transport, session_id: Some(42), @@ -111,10 +106,10 @@ impl IpmiSession { } } -// Transition: Authenticated → Active +// Переход: Authenticated → Active impl IpmiSession { pub fn activate(self) -> Result, String> { - println!("Activating session {}", self.session_id.unwrap()); + println!("Активация сессии {}", self.session_id.unwrap()); Ok(IpmiSession { transport: self.transport, session_id: self.session_id, @@ -123,15 +118,15 @@ impl IpmiSession { } } -// Operations available ONLY in Active state +// Операции доступны ТОЛЬКО в состоянии Active impl IpmiSession { pub fn send_command(&mut self, netfn: u8, cmd: u8, data: &[u8]) -> Vec { - println!("Sending cmd 0x{cmd:02X} on session {}", self.session_id.unwrap()); - vec![0x00] // stub: completion code OK + println!("Отправка команды 0x{cmd:02X} в сессии {}", self.session_id.unwrap()); + vec![0x00] // заглушка: код завершения OK } pub fn close(self) -> IpmiSession { - println!("Closing session {}", self.session_id.unwrap()); + println!("Закрытие сессии {}", self.session_id.unwrap()); IpmiSession { transport: self.transport, session_id: None, @@ -144,50 +139,49 @@ fn ipmi_workflow() -> Result<(), String> { let session = IpmiSession::new("192.168.1.100"); // session.send_command(0x04, 0x2D, &[]); - // ^^^^^^ ERROR: no method `send_command` on IpmiSession ❌ + // ^^^^^^ ОШИБКА: у IpmiSession нет метода send_command ❌ let session = session.authenticate("admin", "password")?; // session.send_command(0x04, 0x2D, &[]); - // ^^^^^^ ERROR: no method `send_command` on IpmiSession ❌ + // ^^^^^^ ОШИБКА: у IpmiSession нет метода send_command ❌ let mut session = session.activate()?; - // ✅ NOW send_command exists: + // ✅ ТЕПЕРЬ send_command доступна: let response = session.send_command(0x04, 0x2D, &[1]); let _closed = session.close(); // _closed.send_command(0x04, 0x2D, &[]); - // ^^^^^^ ERROR: no method `send_command` on IpmiSession ❌ + // ^^^^^^ ОШИБКА: у IpmiSession нет метода send_command ❌ Ok(()) } ``` -**No runtime state checks anywhere.** The compiler enforces: -- Authentication before activation -- Activation before sending commands -- No commands after close +**Нет ни одной проверки состояния во время выполнения.** Компилятор обеспечивает: +- аутентификацию перед активацией +- активацию перед отправкой команд +- отсутствие команд после закрытия -## PCIe Link Training State Machine +## Автомат состояний обучения линка PCIe -PCIe link training is a multi-phase protocol defined in the PCIe specification. -Type-state prevents sending data before the link is ready: +Обучение линка PCIe — многофазный протокол, описанный в спецификации PCIe. Typestate не даёт отправить данные до того, как линк готов: ```rust,ignore use std::marker::PhantomData; -// PCIe LTSSM states (simplified) +// Состояния LTSSM PCIe (упрощённо) pub struct Detect; pub struct Polling; pub struct Configuration; -pub struct L0; // fully operational +pub struct L0; // полностью работоспособен pub struct Recovery; pub struct PcieLink { slot: u32, - width: u8, // negotiated width (x1, x4, x8, x16) + width: u8, // согласованная ширина (x1, x4, x8, x16) speed: u8, // Gen1=1, Gen2=2, Gen3=3, Gen4=4, Gen5=5 _state: PhantomData, } @@ -201,7 +195,7 @@ impl PcieLink { } pub fn detect_receiver(self) -> Result, String> { - println!("Slot {}: receiver detected", self.slot); + println!("Слот {}: приёмник обнаружен", self.slot); Ok(PcieLink { slot: self.slot, width: 0, speed: 0, _state: PhantomData, @@ -211,7 +205,7 @@ impl PcieLink { impl PcieLink { pub fn poll_compliance(self) -> Result, String> { - println!("Slot {}: polling complete, entering configuration", self.slot); + println!("Слот {}: опрос завершён, переход к конфигурации", self.slot); Ok(PcieLink { slot: self.slot, width: 0, speed: 0, _state: PhantomData, @@ -221,7 +215,7 @@ impl PcieLink { impl PcieLink { pub fn negotiate(self, width: u8, speed: u8) -> Result, String> { - println!("Slot {}: negotiated x{width} Gen{speed}", self.slot); + println!("Слот {}: согласовано x{width} Gen{speed}", self.slot); Ok(PcieLink { slot: self.slot, width, speed, _state: PhantomData, @@ -230,13 +224,13 @@ impl PcieLink { } impl PcieLink { - /// Send a TLP — only possible when the link is fully trained (L0). + /// Отправить TLP — возможно только тогда, когда линк полностью обучен (L0). pub fn send_tlp(&mut self, tlp: &[u8]) -> Vec { - println!("Slot {}: sending {} byte TLP", self.slot, tlp.len()); - vec![0x00] // stub + println!("Слот {}: отправка TLP из {} байт", self.slot, tlp.len()); + vec![0x00] // заглушка } - /// Enter recovery — returns to Recovery state. + /// Войти в восстановление — возврат в состояние Recovery. pub fn enter_recovery(self) -> PcieLink { PcieLink { slot: self.slot, width: self.width, speed: self.speed, @@ -251,7 +245,7 @@ impl PcieLink { impl PcieLink { pub fn retrain(self, speed: u8) -> Result, String> { - println!("Slot {}: retrained at Gen{speed}", self.slot); + println!("Слот {}: переобучен на Gen{speed}", self.slot); Ok(PcieLink { slot: self.slot, width: self.width, speed, _state: PhantomData, @@ -262,29 +256,28 @@ impl PcieLink { fn pcie_workflow() -> Result<(), String> { let link = PcieLink::new(0); - // link.send_tlp(&[0x01]); // ❌ no method `send_tlp` on PcieLink + // link.send_tlp(&[0x01]); // ❌ нет метода send_tlp у PcieLink let link = link.detect_receiver()?; let link = link.poll_compliance()?; let mut link = link.negotiate(16, 5)?; // x16 Gen5 - // ✅ NOW we can send TLPs: + // ✅ ТЕПЕРЬ можно отправлять TLP: let _resp = link.send_tlp(&[0x00, 0x01, 0x02]); - println!("Link: {}", link.link_info()); + println!("Линк: {}", link.link_info()); - // Recovery and retrain: + // Восстановление и переобучение: let recovery = link.enter_recovery(); - let mut link = recovery.retrain(4)?; // downgrade to Gen4 + let mut link = recovery.retrain(4)?; // понижаем до Gen4 let _resp = link.send_tlp(&[0x03]); Ok(()) } ``` -## Combining Type-State with Capability Tokens +## Комбинирование typestate с capability-токенами -Type-state and capability tokens compose naturally. A diagnostic that requires -an active IPMI session AND admin privileges: +Typestate и capability-токены естественно сочетаются. Пример: диагностика, которой нужны одновременно активная сессия IPMI и права администратора: ```rust,ignore # use std::marker::PhantomData; @@ -295,35 +288,32 @@ an active IPMI session AND admin privileges: # pub fn send_command(&mut self, _nf: u8, _cmd: u8, _d: &[u8]) -> Vec { vec![] } # } -/// Run a firmware update — requires: -/// 1. Active IPMI session (type-state) -/// 2. Admin privileges (capability token) +/// Запускает обновление прошивки — требует: +/// 1. Активную сессию IPMI (typestate) +/// 2. Права администратора (capability-токен) pub fn firmware_update( - session: &mut IpmiSession, // proves session is active - _admin: &AdminToken, // proves caller is admin + session: &mut IpmiSession, // доказывает, что сессия активна + _admin: &AdminToken, // доказывает, что вызывающий — администратор image: &[u8], ) -> Result<(), String> { - // No runtime checks needed — the signature IS the check + // Никаких проверок во время выполнения не нужно — сама сигнатура и есть проверка session.send_command(0x2C, 0x01, image); Ok(()) } ``` -The caller must: -1. Create a session (`Idle`) -2. Authenticate it (`Authenticated`) -3. Activate it (`Active`) -4. Obtain an `AdminToken` -5. Then and only then call `firmware_update()` +Вызывающий код должен: +1. Создать сессию (`Idle`) +2. Аутентифицировать её (`Authenticated`) +3. Активировать её (`Active`) +4. Получить `AdminToken` +5. И только затем вызвать `firmware_update()` -All enforced at compile time, zero runtime cost. +Всё это проверяется на этапе компиляции, без накладных расходов во время выполнения. -## Beat 3: Firmware Update — Multi-Phase FSM with Composition +## Этап 3: обновление прошивки — многофазный FSM с композицией -A firmware update lifecycle has more states than a session and composition with -both capability tokens AND single-use types (ch03). This is the most complex -type-state example in the book — if you're comfortable with it, you've mastered -the pattern. +У жизненного цикла обновления прошивки больше состояний, чем у сессии, и он сочетает одновременно capability-токены и одноразовые типы (гл. 03). Это самый сложный пример typestate в книге: если вы в нём разобрались, паттерн освоен. ```mermaid stateDiagram-v2 @@ -333,18 +323,18 @@ stateDiagram-v2 Uploading --> Idle : abort() Verifying --> Verified : verify_ok() Verifying --> Idle : verify_fail() - Verified --> Applying : apply(single-use VerifiedImage token) + Verified --> Applying : apply(одноразовый токен VerifiedImage) Applying --> WaitingReboot : apply_complete() WaitingReboot --> [*] : reboot() - note right of Verified : VerifiedImage token consumed by apply() - note right of Uploading : abort() returns to Idle (safe) + note right of Verified : токен VerifiedImage потребляется в apply() + note right of Uploading : abort() возвращает в Idle (безопасно) ``` ```rust,ignore use std::marker::PhantomData; -// ── States ── +// ── Состояния ── pub struct Idle; pub struct Uploading; pub struct Verifying; @@ -352,13 +342,13 @@ pub struct Verified; pub struct Applying; pub struct WaitingReboot; -// ── Single-use proof that image passed verification (ch03) ── +// ── Одноразовое доказательство того, что образ прошёл проверку (гл. 03) ── pub struct VerifiedImage { _private: (), pub digest: [u8; 32], } -// ── Capability token: only admins can initiate (ch04) ── +// ── Capability-токен: инициировать обновление могут только администраторы (гл. 04) ── pub struct FirmwareAdminToken { _private: () } pub struct FwUpdate { @@ -371,34 +361,34 @@ impl FwUpdate { FwUpdate { version: String::new(), _state: PhantomData } } - /// Begin upload — requires admin privilege. + /// Начать загрузку — требует права администратора. pub fn begin_upload( self, _admin: &FirmwareAdminToken, version: &str, ) -> FwUpdate { - println!("Uploading firmware v{version}..."); + println!("Загрузка прошивки v{version}..."); FwUpdate { version: version.to_string(), _state: PhantomData } } } impl FwUpdate { pub fn finish_upload(self) -> FwUpdate { - println!("Upload complete, verifying v{}...", self.version); + println!("Загрузка завершена, проверяем v{}...", self.version); FwUpdate { version: self.version, _state: PhantomData } } - /// Abort returns to Idle — safe at any point during upload. + /// Прерывание возвращает в Idle — безопасно в любой момент загрузки. pub fn abort(self) -> FwUpdate { - println!("Upload aborted."); + println!("Загрузка прервана."); FwUpdate { version: String::new(), _state: PhantomData } } } impl FwUpdate { - /// On success, produces a single-use VerifiedImage token. + /// При успехе возвращает одноразовый токен VerifiedImage. pub fn verify_ok(self, digest: [u8; 32]) -> (FwUpdate, VerifiedImage) { - println!("Verification passed for v{}", self.version); + println!("Проверка v{} пройдена", self.version); ( FwUpdate { version: self.version, _state: PhantomData }, VerifiedImage { _private: (), digest }, @@ -406,84 +396,82 @@ impl FwUpdate { } pub fn verify_fail(self) -> FwUpdate { - println!("Verification failed — returning to idle."); + println!("Проверка не пройдена — возврат в состояние ожидания."); FwUpdate { version: String::new(), _state: PhantomData } } } impl FwUpdate { - /// Apply CONSUMES the VerifiedImage token — can't apply twice. + /// Apply ПОТРЕБЛЯЕТ токен VerifiedImage — применить дважды нельзя. pub fn apply(self, proof: VerifiedImage) -> FwUpdate { - println!("Applying v{} (digest: {:02x?})", self.version, &proof.digest[..4]); - // proof is moved — can't be reused + println!("Применяем v{} (дайджест: {:02x?})", self.version, &proof.digest[..4]); + // proof перемещён — переиспользовать его нельзя FwUpdate { version: self.version, _state: PhantomData } } } impl FwUpdate { pub fn apply_complete(self) -> FwUpdate { - println!("Apply complete — waiting for reboot."); + println!("Применение завершено — ожидаем перезагрузки."); FwUpdate { version: self.version, _state: PhantomData } } } impl FwUpdate { pub fn reboot(self) { - println!("Rebooting into v{}...", self.version); + println!("Перезагрузка в v{}...", self.version); } } -// ── Usage ── +// ── Использование ── fn firmware_workflow() { let fw = FwUpdate::new(); - // fw.finish_upload(); // ❌ no method `finish_upload` on FwUpdate + // fw.finish_upload(); // ❌ нет метода finish_upload у FwUpdate - let admin = FirmwareAdminToken { _private: () }; // from auth system + let admin = FirmwareAdminToken { _private: () }; // из системы аутентификации let fw = fw.begin_upload(&admin, "2.10.1"); let fw = fw.finish_upload(); - let digest = [0xAB; 32]; // computed during verification + let digest = [0xAB; 32]; // вычисляется во время проверки let (fw, token) = fw.verify_ok(digest); let fw = fw.apply(token); - // fw.apply(token); // ❌ use of moved value: `token` + // fw.apply(token); // ❌ ошибка компиляции: use of moved value: `token` let fw = fw.apply_complete(); fw.reboot(); } ``` -**What the three beats illustrate together:** +**Что вместе иллюстрируют три этапа:** -| Beat | Protocol | States | Composition | -|:----:|----------|:------:|-------------| -| 1 | IPMI session | 4 | Pure type-state | -| 2 | PCIe LTSSM | 5 | Type-state + recovery branch | -| 3 | Firmware update | 6 | Type-state + capability tokens (ch04) + single-use proof (ch03) | +| Этап | Протокол | Состояния | Композиция | +|:----:|----------|:---------:|------------| +| 1 | Сессия IPMI | 4 | Чистый typestate | +| 2 | LTSSM PCIe | 5 | Typestate + ветка восстановления | +| 3 | Обновление прошивки | 6 | Typestate + capability-токены (гл. 04) + одноразовое доказательство (гл. 03) | -Each beat adds a layer of complexity. By beat 3, the compiler enforces state -ordering, admin privilege, AND one-time application — three bug classes -eliminated in a single FSM. +С каждым этапом добавляется слой сложности. К этапу 3 компилятор обеспечивает порядок состояний, права администратора **и** однократное применение: три класса ошибок устранены в одном автомате состояний. -### When to Use Type-State +### Когда использовать typestate -| Protocol | Type-State worthwhile? | +| Протокол | Стоит ли использовать typestate? | |----------|:------:| -| IPMI session lifecycle | ✅ Yes — authenticate → activate → command → close | -| PCIe link training | ✅ Yes — detect → poll → configure → L0 | -| TLS handshake | ✅ Yes — ClientHello → ServerHello → Finished | -| USB enumeration | ✅ Yes — Attached → Powered → Default → Addressed → Configured | -| Simple request/response | ⚠️ Probably not — only 2 states | -| Fire-and-forget messages | ❌ No — no state to track | +| Жизненный цикл сессии IPMI | ✅ Да: аутентификация → активация → команда → закрытие | +| Обучение линка PCIe | ✅ Да: detect → poll → configure → L0 | +| TLS-рукопожатие | ✅ Да: ClientHello → ServerHello → Finished | +| Перечисление USB | ✅ Да: Attached → Powered → Default → Addressed → Configured | +| Простой запрос/ответ | ⚠️ Скорее нет: всего 2 состояния | +| Сообщения «отправил и забыл» | ❌ Нет: нет состояния, которое нужно отслеживать | -## Exercise: USB Device Enumeration Type-State +## Упражнение: typestate для перечисления USB-устройства -Model a USB device that must go through: `Attached` → `Powered` → `Default` → `Addressed` → `Configured`. Each transition should consume the previous state and produce the next. `send_data()` should only be available in `Configured`. +Смоделируйте USB-устройство, которое должно пройти путь: `Attached` → `Powered` → `Default` → `Addressed` → `Configured`. Каждый переход должен потреблять предыдущее состояние и возвращать следующее. Метод `send_data()` должен быть доступен только в `Configured`.
-Solution +Решение ```rust,ignore use std::marker::PhantomData; @@ -528,21 +516,20 @@ impl UsbDevice { impl UsbDevice { pub fn send_data(&self, _data: &[u8]) { - // Only available in Configured state + // Доступно только в состоянии Configured } } ```
-## Key Takeaways +## Ключевые выводы -1. **Type-state makes wrong-order calls impossible** — methods only exist on the state where they're valid. -2. **Each transition consumes `self`** — you can't hold onto an old state after transitioning. -3. **Combine with capability tokens** — `firmware_update()` requires *both* `Session` and `AdminToken`. -4. **Three beats, increasing complexity** — IPMI (pure FSM), PCIe LTSSM (recovery branches), and firmware update (FSM + tokens + single-use proofs) show the pattern scales from simple to richly composed. -5. **Don't over-apply** — two-state request/response protocols are simpler without type-state. -6. **The pattern extends to full Redfish workflows** — ch17 applies type-state to Redfish session lifecycles, and ch18 uses builder type-state for response construction. +1. **Typestate делает вызовы в неверном порядке невозможными** — методы существуют только в тех состояниях, где они допустимы. +2. **Каждый переход потребляет `self`** — нельзя сохранить старое состояние после перехода. +3. **Сочетайте с capability-токенами** — `firmware_update()` требует *и* `Session`, *и* `AdminToken`. +4. **Три этапа, нарастающая сложность** — IPMI (чистый FSM), LTSSM PCIe (ветки восстановления) и обновление прошивки (FSM + токены + одноразовые доказательства) показывают, что паттерн масштабируется от простого к сложно составленному. +5. **Не переусердствуйте** — протоколы «запрос/ответ» с двумя состояниями проще без typestate. +6. **Паттерн распространяется на полные сценарии Redfish** — гл. 17 применяет typestate к жизненному циклу сессий Redfish, а гл. 18 использует typestate билдера для построения ответов. --- - diff --git a/type-driven-correctness-book/src/ch06-dimensional-analysis-making-the-compiler.md b/type-driven-correctness-book/src/ch06-dimensional-analysis-making-the-compiler.md index f063a8f..35048f3 100644 --- a/type-driven-correctness-book/src/ch06-dimensional-analysis-making-the-compiler.md +++ b/type-driven-correctness-book/src/ch06-dimensional-analysis-making-the-compiler.md @@ -1,59 +1,55 @@ -# Dimensional Analysis — Making the Compiler Check Your Units 🟢 +# Анализ размерностей: заставляем компилятор проверять единицы измерения 🟢 -> **What you'll learn:** How newtype wrappers and the `uom` crate turn the compiler into a unit-checking engine, preventing the class of bug that destroyed a $328M spacecraft. +> **Что вы узнаете:** как newtype-обёртки и крейт `uom` превращают компилятор в движок проверки единиц измерения и предотвращают класс ошибок, из-за которого был потерян космический аппарат стоимостью 328 млн долларов. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (typed commands use these types), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (validated boundaries), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (integration) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (типизированные команды используют эти типы), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (проверенные границы), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция) -## The Mars Climate Orbiter +## Mars Climate Orbiter -In 1999, NASA's Mars Climate Orbiter was lost because one team sent thrust data in -**pound-force seconds** while the navigation team expected **newton-seconds**. The -spacecraft entered the atmosphere at 57 km instead of 226 km and disintegrated. -Cost: $327.6 million. +В 1999 году был потерян марсианский зонд NASA Mars Climate Orbiter: одна команда передавала данные о тяге в **фунт-силах на секунду**, а команда навигации ожидала **ньютон-секунды**. Аппарат вошёл в атмосферу на высоте 57 км вместо 226 км и разрушился. Стоимость: 327,6 млн долларов. -The root cause: **both values were `double`**. The compiler couldn't distinguish them. +Первопричина: **оба значения были `double`**. Компилятор не мог их различить. -This same class of bug lurks in every hardware diagnostic that deals with physical -quantities: +Тот же класс ошибок скрывается в любой аппаратной диагностике, которая работает с физическими величинами: ```c -// C — all doubles, no unit checking -double read_temperature(int sensor_id); // Celsius? Fahrenheit? Kelvin? -double read_voltage(int channel); // Volts? Millivolts? -double read_fan_speed(int fan_id); // RPM? Radians per second? +// C — все double, никакой проверки единиц +double read_temperature(int sensor_id); // Цельсий? Фаренгейт? Кельвин? +double read_voltage(int channel); // Вольты? Милливольты? +double read_fan_speed(int fan_id); // об/мин? радианы в секунду? -// Bug: comparing Celsius to Fahrenheit -if (read_temperature(0) > read_temperature(1)) { ... } // units might differ! +// Ошибка: сравнение Цельсия с Фаренгейтом +if (read_temperature(0) > read_temperature(1)) { ... } // единицы могут различаться! ``` -## Newtypes for Physical Quantities +## Newtype для физических величин -The simplest correct-by-construction approach: **wrap each unit in its own type**. +Самый простой корректный по построению подход: **обернуть каждую единицу измерения в отдельный тип**. ```rust,ignore use std::fmt; -/// Temperature in degrees Celsius. +/// Температура в градусах Цельсия. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Celsius(pub f64); -/// Temperature in degrees Fahrenheit. +/// Температура в градусах Фаренгейта. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Fahrenheit(pub f64); -/// Voltage in volts. +/// Напряжение в вольтах. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Volts(pub f64); -/// Voltage in millivolts. +/// Напряжение в милливольтах. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Millivolts(pub f64); -/// Fan speed in RPM. +/// Скорость вентилятора в об/мин. #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Rpm(pub f64); -// Conversions are explicit: +// Преобразования явные: impl From for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) @@ -86,12 +82,12 @@ impl fmt::Display for Celsius { impl fmt::Display for Rpm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:.0} RPM", self.0) + write!(f, "{:.0} об/мин", self.0) } } ``` -Now the compiler catches unit mismatches: +Теперь компилятор ловит несоответствия единиц: ```rust,ignore # #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -100,23 +96,22 @@ Now the compiler catches unit mismatches: # pub struct Volts(pub f64); fn check_thermal_limit(temp: Celsius, limit: Celsius) -> bool { - temp > limit // ✅ same units — compiles + temp > limit // ✅ одинаковые единицы — компилируется } // fn bad_comparison(temp: Celsius, voltage: Volts) -> bool { -// temp > voltage // ❌ ERROR: mismatched types — Celsius vs Volts +// temp > voltage // ❌ ОШИБКА: mismatched types — Celsius vs Volts // } ``` -**Zero runtime cost** — newtypes compile down to raw `f64` values. The wrapper is -purely a type-level concept. +**Нулевые накладные расходы во время выполнения:** newtype компилируются в обычные значения `f64`. Обёртка существует чисто как понятие системы типов. -## Newtype Macro for Hardware Quantities +## Макрос newtype для аппаратных величин -Writing newtypes by hand gets repetitive. A macro eliminates the boilerplate: +Писать newtype вручную утомительно. Макрос устраняет шаблонный код: ```rust,ignore -/// Generate a newtype for a physical quantity. +/// Генерирует newtype для физической величины. macro_rules! quantity { ($Name:ident, $unit:expr) => { #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -145,35 +140,26 @@ macro_rules! quantity { }; } -// Usage: +// Использование: quantity!(Celsius, "°C"); quantity!(Fahrenheit, "°F"); quantity!(Volts, "V"); -quantity!(Millivolts, "mV"); -quantity!(Rpm, "RPM"); -quantity!(Watts, "W"); -quantity!(Amperes, "A"); -quantity!(Pascals, "Pa"); -quantity!(Hertz, "Hz"); -quantity!(Bytes, "B"); +quantity!(Millivolts, "мВ"); +quantity!(Rpm, "об/мин"); +quantity!(Watts, "Вт"); +quantity!(Amperes, "А"); +quantity!(Pascals, "Па"); +quantity!(Hertz, "Гц"); +quantity!(Bytes, "Б"); ``` -Each line generates a complete type with Display, Add, Sub, and comparison operators. -**All at zero runtime cost.** +Каждая строка порождает полноценный тип с Display, Add, Sub и операторами сравнения. **Всё это без накладных расходов во время выполнения.** -> **Physics caveat:** The macro generates `Add` for *all* quantities, including -> `Celsius`. Adding absolute temperatures (`25°C + 30°C = 55°C`) is not -> physically meaningful — you'd need a separate `TemperatureDelta` type for -> differences. The `uom` crate (shown later) handles this correctly. For -> simple sensor diagnostics where you only compare and display, you can omit -> `Add`/`Sub` from temperature types and keep them for quantities where -> addition makes sense (Watts, Volts, Bytes). If you need delta arithmetic, -> define a `CelsiusDelta(f64)` newtype with `impl Add for Celsius`. +> **Физическая оговорка:** макрос генерирует `Add` для *всех* величин, включая `Celsius`. Сложение абсолютных температур (`25°C + 30°C = 55°C`) физически бессмысленно: для разностей температур нужен отдельный тип, например `TemperatureDelta`. Крейт `uom` (см. ниже) обрабатывает это корректно. Для простой диагностики датчиков, где температуры только сравниваются и выводятся, можно убрать `Add`/`Sub` из типов температур и оставить их там, где сложение имеет смысл (ватты, вольты, байты). Если нужна арифметика разностей, определите newtype `CelsiusDelta(f64)` с `impl Add for Celsius`. -## Applied Example: Sensor Pipeline +## Прикладной пример: конвейер датчиков -A typical diagnostic reads raw ADC values, converts them to physical units, and -compares against thresholds. With dimensional types, each step is type-checked: +Типичная диагностика считывает сырые значения АЦП, переводит их в физические единицы и сравнивает с порогами. С размерными типами каждый шаг проверяется системой типов: ```rust,ignore # macro_rules! quantity { @@ -193,19 +179,19 @@ compares against thresholds. With dimensional types, each step is type-checked: # } # quantity!(Celsius, "°C"); # quantity!(Volts, "V"); -# quantity!(Rpm, "RPM"); +# quantity!(Rpm, "об/мин"); -/// Raw ADC reading — not yet a physical quantity. +/// Сырое показание АЦП — ещё не физическая величина. #[derive(Debug, Clone, Copy)] pub struct AdcReading { pub channel: u8, - pub raw: u16, // 12-bit ADC value (0–4095) + pub raw: u16, // 12-битное значение АЦП (0–4095) } -/// Calibration coefficients for converting ADC → physical unit. +/// Коэффициенты калибровки для перевода АЦП → физическая единица. pub struct TemperatureCalibration { pub offset: f64, - pub scale: f64, // °C per ADC count + pub scale: f64, // °C на отсчёт АЦП } pub struct VoltageCalibration { @@ -214,20 +200,20 @@ pub struct VoltageCalibration { } impl TemperatureCalibration { - /// Convert raw ADC → Celsius. The return type guarantees the output is Celsius. + /// Перевод сырого АЦП → Celsius. Тип результата гарантирует, что выход — Celsius. pub fn convert(&self, adc: AdcReading) -> Celsius { Celsius::new(adc.raw as f64 * self.scale + self.offset) } } impl VoltageCalibration { - /// Convert raw ADC → Volts. The return type guarantees the output is Volts. + /// Перевод сырого АЦП → Volts. Тип результата гарантирует, что выход — Volts. pub fn convert(&self, adc: AdcReading) -> Volts { Volts::new(adc.raw as f64 * self.reference_mv / 4096.0 / self.divider_ratio / 1000.0) } } -/// Threshold check — only compiles if units match. +/// Проверка порогов — компилируется, только если единицы совпадают. pub struct Threshold { pub warning: T, pub critical: T, @@ -263,28 +249,26 @@ fn sensor_pipeline_example() { let temp: Celsius = temp_cal.convert(adc); let result = temp_threshold.check(&temp); - println!("Temperature: {temp}, Status: {result:?}"); + println!("Температура: {temp}, статус: {result:?}"); - // This won't compile — can't check a Celsius reading against a Volts threshold: + // Это не скомпилируется: нельзя сравнить показание Celsius с порогом в Volts: // let volt_threshold = Threshold { // warning: Volts::new(11.4), // critical: Volts::new(10.8), // }; - // volt_threshold.check(&temp); // ❌ ERROR: expected &Volts, found &Celsius + // volt_threshold.check(&temp); // ❌ ОШИБКА: expected &Volts, found &Celsius } ``` -The **entire pipeline** is statically type-checked: -- ADC readings are raw counts (not units) -- Calibration produces typed quantities (Celsius, Volts) -- Thresholds are generic over the quantity type -- Comparing Celsius against Volts is a **compile error** +**Весь конвейер** статически проверяется на соответствие типов: +- показания АЦП — это сырые отсчёты, а не единицы измерения +- калибровка даёт типизированные величины (Celsius, Volts) +- пороги обобщены по типу величины +- сравнение Celsius с Volts — **ошибка компиляции** -## The uom Crate +## Крейт uom -For production use, the [`uom`](https://crates.io/crates/uom) crate provides -a comprehensive dimensional analysis system with hundreds of units, automatic -conversion, and zero runtime overhead: +Для промышленного использования крейт [`uom`](https://crates.io/crates/uom) предоставляет полноценную систему анализа размерностей: сотни единиц, автоматическое преобразование и нулевые накладные расходы во время выполнения: ```rust,ignore // Cargo.toml: uom = { version = "0.36", features = ["f64"] } @@ -298,33 +282,31 @@ conversion, and zero runtime overhead: // let voltage = ElectricPotential::new::(12.0); // let power = Power::new::(250.0); // -// // temp + voltage; // ❌ compile error — can't add temperature to voltage -// // power > temp; // ❌ compile error — can't compare power to temperature +// // temp + voltage; // ❌ ошибка компиляции — нельзя сложить температуру и напряжение +// // power > temp; // ❌ ошибка компиляции — нельзя сравнить мощность с температурой ``` -Use `uom` when you need automatic derived-unit support (e.g., Watts = Volts × Amperes). -Use hand-rolled newtypes when you need only simple quantities without derived-unit -arithmetic. +Используйте `uom`, когда нужна автоматическая поддержка производных единиц (например, Ватты = Вольты × Амперы). Используйте самописные newtype, когда нужны только простые величины без арифметики производных единиц. -### When to Use Dimensional Types +### Когда использовать размерные типы -| Scenario | Recommendation | -|----------|---------------| -| Sensor readings (temp, voltage, fan) | ✅ Always — prevents unit confusion | -| Threshold comparisons | ✅ Always — generic `Threshold` | -| Cross-subsystem data exchange | ✅ Always — enforce contracts at API boundaries | -| Internal calculations (same unit throughout) | ⚠️ Optional — less bug-prone | -| String/display formatting | ❌ Use Display impl on the quantity type | +| Сценарий | Рекомендация | +|----------|--------------| +| Показания датчиков (температура, напряжение, вентилятор) | ✅ Всегда: предотвращает путаницу единиц | +| Сравнение с порогами | ✅ Всегда: обобщённый `Threshold` | +| Обмен данными между подсистемами | ✅ Всегда: закрепляйте контракты на границах API | +| Внутренние вычисления (одна единица во всём коде) | ⚠️ По желанию: меньше риска ошибок | +| Форматирование строк и вывод | ❌ Используйте реализацию Display у типа величины | -## Sensor Pipeline Type Flow +## Поток типов в конвейере датчиков ```mermaid flowchart LR - RAW["raw: &[u8]"] -->|parse| C["Celsius(f64)"] - RAW -->|parse| R["Rpm(u32)"] - RAW -->|parse| V["Volts(f64)"] - C -->|threshold check| TC["Threshold"] - R -->|threshold check| TR["Threshold"] + RAW["raw: &[u8]"] -->|разбор| C["Celsius(f64)"] + RAW -->|разбор| R["Rpm(u32)"] + RAW -->|разбор| V["Volts(f64)"] + C -->|проверка порога| TC["Threshold"] + R -->|проверка порога| TR["Threshold"] C -.->|"C + R"| ERR["❌ mismatched types"] style RAW fill:#e1f5fe,color:#000 style C fill:#c8e6c9,color:#000 @@ -335,15 +317,15 @@ flowchart LR style ERR fill:#ffcdd2,color:#000 ``` -## Exercise: Power Budget Calculator +## Упражнение: калькулятор энергетического бюджета -Create `Watts(f64)` and `Amperes(f64)` newtypes. Implement: +Создайте newtype `Watts(f64)` и `Amperes(f64)`. Реализуйте: - `Watts::from_vi(volts: Volts, amps: Amperes) -> Watts` (P = V × I) -- A `PowerBudget` that tracks total watts and rejects additions that exceed a configured limit. -- Attempting `Watts + Celsius` should be a compile error. +- `PowerBudget`, который суммирует ватты и отклоняет добавления, превышающие заданный лимит. +- Попытка `Watts + Celsius` должна приводить к ошибке компиляции.
-Solution +Решение ```rust,ignore #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -383,26 +365,25 @@ impl PowerBudget { pub fn add(&mut self, w: Watts) -> Result<(), String> { let new_total = Watts(self.total.0 + w.0); if new_total > self.limit { - return Err(format!("budget exceeded: {:?} > {:?}", new_total, self.limit)); + return Err(format!("бюджет превышен: {:?} > {:?}", new_total, self.limit)); } self.total = new_total; Ok(()) } } -// ❌ Compile error: Watts + Celsius → "mismatched types" +// ❌ Ошибка компиляции: Watts + Celsius → "mismatched types" // let bad = Watts(100.0) + Celsius(50.0); ```
-## Key Takeaways +## Ключевые выводы -1. **Newtypes prevent unit confusion at zero cost** — `Celsius` and `Rpm` are both `f64` inside, but the compiler treats them as different types. -2. **The Mars Climate Orbiter bug is impossible** — passing `Pounds` where `Newtons` is expected is a compile error. -3. **`quantity!` macro reduces boilerplate** — stamp out Display, arithmetic, and threshold logic for each unit. -4. **`uom` crate handles derived units** — use it when you need `Watts = Volts × Amperes` automatically. -5. **Threshold is generic over the quantity** — `Threshold` can't accidentally compare to `Threshold`. +1. **Newtype предотвращает путаницу единиц без накладных расходов** — `Celsius` и `Rpm` внутри оба `f64`, но компилятор считает их разными типами. +2. **Ошибка Mars Climate Orbiter становится невозможной** — передать фунт-силы там, где ожидаются ньютоны, — ошибка компиляции. +3. **Макрос `quantity!` уменьшает объём шаблонного кода** — он генерирует Display, арифметику и логику порогов для каждой единицы. +4. **Крейт `uom` обрабатывает производные единицы** — используйте его, когда нужно автоматически получать `Watts = Volts × Amperes`. +5. **Threshold обобщён по величине** — `Threshold` нельзя случайно сравнить с `Threshold`. --- - diff --git a/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md b/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md index ae2a615..4919522 100644 --- a/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md +++ b/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md @@ -1,50 +1,48 @@ -# Validated Boundaries — Parse, Don't Validate 🟡 +# Проверенные границы: parse, don't validate 🟡 -> **What you'll learn:** How to validate data exactly once at the system boundary, carry the proof of validity in a dedicated type, and never re-check — applied to IPMI FRU records (flat bytes), Redfish JSON (structured documents), and IPMI SEL records (polymorphic binary with nested dispatch), with a complete end-to-end walkthrough. +> **Что вы узнаете:** как проверить данные ровно один раз на границе системы, хранить доказательство их корректности в отдельном типе и никогда не проверять повторно. Применяем к записям IPMI FRU (плоские байты), JSON Redfish (структурированные документы) и записям IPMI SEL (полиморфные бинарные данные с вложенной диспетчеризацией) со сквозным пошаговым разбором. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (typed commands), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional types), [ch11](ch11-fourteen-tricks-from-the-trenches.md) (trick 2 — sealed traits, trick 3 — `#[non_exhaustive]`, trick 5 — FromStr), [ch14](ch14-testing-type-level-guarantees.md) (proptest) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (типизированные команды), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (размерные типы), [гл. 11](ch11-fourteen-tricks-from-the-trenches.md) (приём 2: sealed-трейты, приём 3: `#[non_exhaustive]`, приём 5: FromStr), [гл. 14](ch14-testing-type-level-guarantees.md) (proptest) -## The Problem: Shotgun Validation +## Проблема: разбросанная валидация -In typical code, validation is scattered everywhere. Every function that receives -data re-checks it "just in case": +В типичном коде проверки разбросаны повсюду. Каждая функция, которая получает данные, перепроверяет их «на всякий случай»: ```c -// C — validation scattered across the codebase +// C — проверки разбросаны по всей кодовой базе int process_fru_data(uint8_t *data, int len) { - if (data == NULL) return -1; // check: non-null - if (len < 8) return -1; // check: minimum length - if (data[0] != 0x01) return -1; // check: format version - if (checksum(data, len) != 0) return -1; // check: checksum + if (data == NULL) return -1; // проверка: не NULL + if (len < 8) return -1; // проверка: минимальная длина + if (data[0] != 0x01) return -1; // проверка: версия формата + if (checksum(data, len) != 0) return -1; // проверка: контрольная сумма - // ... 10 more functions that repeat the same checks ... + // ... ещё 10 функций, которые повторяют те же проверки ... } ``` -This pattern ("shotgun validation") has two problems: -1. **Redundancy** — the same checks appear in dozens of places -2. **Incompleteness** — forget one check in one function and you have a bug +Этот паттерн («дробовик» проверок) имеет две проблемы: +1. **Избыточность**: одни и те же проверки встречаются в десятках мест +2. **Неполнота**: забудете одну проверку в одной функции — и получите ошибку -## Parse, Don't Validate +## Parse, Don't Validate: разбирай, а не проверяй -The correct-by-construction approach: **validate once at the boundary, then carry -the proof of validity in the type**. +Корректный по построению подход: **проверить один раз на границе, а затем хранить доказательство корректности в типе**. ```rust,ignore -/// Raw bytes from the wire — not yet validated. +/// Сырые байты из линии — ещё не проверены. #[derive(Debug)] pub struct RawFruData(Vec); ``` -### Case Study: IPMI FRU Data +### Разбор: данные IPMI FRU ```rust,ignore # #[derive(Debug)] # pub struct RawFruData(Vec); -/// Validated IPMI FRU data. Can only be created via TryFrom, -/// which enforces all invariants. Once you have a ValidFru, -/// all data is guaranteed correct. +/// Проверенные данные IPMI FRU. Создать можно только через TryFrom, +/// который обеспечивает все инварианты. Имея ValidFru, вы гарантированно +/// имеете корректные данные. #[derive(Debug)] pub struct ValidFru { format_version: u8, @@ -67,13 +65,13 @@ impl std::fmt::Display for FruError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::TooShort { actual, minimum } => - write!(f, "FRU data too short: {actual} bytes (minimum {minimum})"), + write!(f, "данные FRU слишком короткие: {actual} байт (минимум {minimum})"), Self::BadFormatVersion(v) => - write!(f, "unsupported FRU format version: {v}"), + write!(f, "неподдерживаемая версия формата FRU: {v}"), Self::ChecksumMismatch { expected, actual } => - write!(f, "checksum mismatch: expected 0x{expected:02X}, got 0x{actual:02X}"), + write!(f, "контрольная сумма не совпадает: ожидалось 0x{expected:02X}, получено 0x{actual:02X}"), Self::InvalidAreaOffset { area, offset } => - write!(f, "invalid {area} area offset: {offset}"), + write!(f, "некорректное смещение области {area}: {offset}"), } } } @@ -84,7 +82,7 @@ impl TryFrom for ValidFru { fn try_from(raw: RawFruData) -> Result { let data = raw.0; - // 1. Length check + // 1. Проверка длины if data.len() < 8 { return Err(FruError::TooShort { actual: data.len(), @@ -92,12 +90,12 @@ impl TryFrom for ValidFru { }); } - // 2. Format version + // 2. Версия формата if data[0] != 0x01 { return Err(FruError::BadFormatVersion(data[0])); } - // 3. Checksum (header is first 8 bytes, checksum at byte 7) + // 3. Контрольная сумма (заголовок — первые 8 байт, контрольная сумма в байте 7) let checksum: u8 = data[..8].iter().fold(0u8, |acc, &b| acc.wrapping_add(b)); if checksum != 0 { return Err(FruError::ChecksumMismatch { @@ -106,7 +104,7 @@ impl TryFrom for ValidFru { }); } - // 4. Area offsets must be within bounds + // 4. Смещения областей должны быть в пределах данных for (name, idx) in [ ("internal", 1), ("chassis", 2), ("board", 3), ("product", 4), @@ -120,7 +118,7 @@ impl TryFrom for ValidFru { } } - // All checks passed — construct the validated type + // Все проверки пройдены — создаём проверенный тип Ok(ValidFru { format_version: data[0], internal_area_offset: data[1], @@ -133,13 +131,13 @@ impl TryFrom for ValidFru { } impl ValidFru { - /// No validation needed — the type guarantees correctness. + /// Проверка не нужна: тип гарантирует корректность. pub fn board_area(&self) -> Option<&[u8]> { if self.board_area_offset == 0 { return None; } let start = self.board_area_offset as usize * 8; - Some(&self.data[start..]) // safe — bounds checked during parsing + Some(&self.data[start..]) // безопасно: границы проверены при разборе } pub fn product_area(&self) -> Option<&[u8]> { @@ -156,7 +154,7 @@ impl ValidFru { } ``` -Any function that takes `&ValidFru` **knows** the data is well-formed. No re-checking: +Любая функция, которая принимает `&ValidFru`, **знает**, что данные корректны. Повторных проверок не нужно: ```rust,ignore # pub struct ValidFru { board_area_offset: u8, data: Vec } @@ -164,35 +162,34 @@ Any function that takes `&ValidFru` **knows** the data is well-formed. No re-che # pub fn board_area(&self) -> Option<&[u8]> { None } # } -/// This function does NOT need to validate the FRU data. -/// The type signature guarantees it's already valid. +/// Эта функция НЕ должна проверять данные FRU. +/// Сигнатура типа гарантирует, что они уже корректны. fn extract_board_serial(fru: &ValidFru) -> Option { let board = fru.board_area()?; - // ... parse serial from board area ... - // No bounds checks needed — ValidFru guarantees offsets are in range - Some("ABC123".to_string()) // stub + // ... разбираем серийный номер из области платы ... + // Проверки границ не нужны: ValidFru гарантирует, что смещения в пределах + Some("ABC123".to_string()) // заглушка } fn extract_board_manufacturer(fru: &ValidFru) -> Option { let board = fru.board_area()?; - // Still no validation needed — same guarantee - Some("Acme Corp".to_string()) // stub + // Проверка здесь тоже не нужна: та же гарантия + Some("Acme Corp".to_string()) // заглушка } ``` -## Validated Redfish JSON +## Проверенный JSON Redfish -The same pattern applies to Redfish API responses. Parse once, carry validity in -the type: +Тот же паттерн применим к ответам API Redfish. Разбираем один раз, а валидность храним в типе: ```rust,ignore use std::collections::HashMap; -/// Raw JSON string from a Redfish endpoint. +/// Сырая JSON-строка из эндпоинта Redfish. pub struct RawRedfishResponse(pub String); -/// A validated Redfish Thermal response. -/// All required fields are guaranteed present and within range. +/// Проверенный ответ Redfish Thermal. +/// Все обязательные поля гарантированно присутствуют и находятся в допустимом диапазоне. #[derive(Debug)] pub struct ValidThermalResponse { pub temperatures: Vec, @@ -202,7 +199,7 @@ pub struct ValidThermalResponse { #[derive(Debug)] pub struct ValidTemperatureReading { pub name: String, - pub reading_celsius: f64, // guaranteed non-NaN, within sensor range + pub reading_celsius: f64, // гарантированно не NaN, в пределах диапазона датчика pub upper_critical: f64, pub status: HealthStatus, } @@ -210,7 +207,7 @@ pub struct ValidTemperatureReading { #[derive(Debug)] pub struct ValidFanReading { pub name: String, - pub reading_rpm: u32, // guaranteed > 0 for present fans + pub reading_rpm: u32, // гарантированно > 0 для установленных вентиляторов pub status: HealthStatus, } @@ -231,18 +228,18 @@ pub enum RedfishValidationError { impl std::fmt::Display for RedfishValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::MissingField(name) => write!(f, "missing required field: {name}"), + Self::MissingField(name) => write!(f, "отсутствует обязательное поле: {name}"), Self::OutOfRange { field, value } => - write!(f, "field {field} out of range: {value}"), - Self::InvalidStatus(s) => write!(f, "invalid health status: {s}"), + write!(f, "поле {field} вне диапазона: {value}"), + Self::InvalidStatus(s) => write!(f, "некорректный статус здоровья: {s}"), } } } -// Once validated, downstream code never re-checks: +// Имея проверенные данные, нижележащий код никогда не проверяет их повторно: fn check_thermal_health(thermal: &ValidThermalResponse) -> bool { - // No need to check for missing fields or NaN values. - // ValidThermalResponse guarantees all readings are sensible. + // Не нужно проверять отсутствие полей и значения NaN. + // ValidThermalResponse гарантирует, что все показания разумны. thermal.temperatures.iter().all(|t| { t.reading_celsius < t.upper_critical && t.status != HealthStatus::Critical }) && thermal.fans.iter().all(|f| { @@ -251,72 +248,66 @@ fn check_thermal_health(thermal: &ValidThermalResponse) -> bool { } ``` -## Polymorphic Validation: IPMI SEL Records +## Полиморфная валидация: записи SEL IPMI -The first two case studies validated **flat** structures — a fixed byte layout (FRU) -and a known JSON schema (Redfish). Real-world data is often **polymorphic**: the -interpretation of later bytes depends on earlier bytes. IPMI System Event Log (SEL) -records are the canonical example. +В первых двух разобранных примерах проверялись **плоские** структуры: фиксированная раскладка байтов (FRU) и известная JSON-схема (Redfish). Реальные данные часто **полиморфны**: смысл последующих байтов зависит от предыдущих. Классический пример — записи журнала системных событий IPMI (System Event Log, SEL). -### The Shape of the Problem +### Суть проблемы -Every SEL record is exactly 16 bytes. But what those bytes *mean* depends on a -dispatch chain: +Каждая запись SEL занимает ровно 16 байт. Но то, что означают эти байты, определяется цепочкой диспетчеризации: ``` -Byte 2: Record Type - ├─ 0x02 → System Event - │ Byte 10[6:4]: Event Type - │ ├─ 0x01 → Threshold event (reading + threshold in data bytes 2-3) - │ ├─ 0x02-0x0C → Discrete event (bit in offset field) - │ └─ 0x6F → Sensor-specific (meaning depends on Sensor Type in byte 7) - │ Byte 7: Sensor Type - │ ├─ 0x01 → Temperature events - │ ├─ 0x02 → Voltage events - │ ├─ 0x04 → Fan events - │ ├─ 0x07 → Processor events - │ ├─ 0x0C → Memory events - │ ├─ 0x08 → Power Supply events - │ └─ ... → (42 sensor types in IPMI 2.0 Table 42-3) - ├─ 0xC0-0xDF → OEM Timestamped - └─ 0xE0-0xFF → OEM Non-Timestamped +Байт 2: Тип записи + ├─ 0x02 → Системное событие + │ Байт 10[6:4]: Тип события + │ ├─ 0x01 → Событие порога (показание + порог в байтах данных 2–3) + │ ├─ 0x02-0x0C → Дискретное событие (бит в поле смещения) + │ └─ 0x6F → Специфичное для датчика (смысл зависит от типа датчика в байте 7) + │ Байт 7: Тип датчика + │ ├─ 0x01 → События температуры + │ ├─ 0x02 → События напряжения + │ ├─ 0x04 → События вентилятора + │ ├─ 0x07 → События процессора + │ ├─ 0x0C → События памяти + │ ├─ 0x08 → События блока питания + │ └─ ... → (42 типа датчиков в IPMI 2.0, таблица 42-3) + ├─ 0xC0-0xDF → OEM с меткой времени + └─ 0xE0-0xFF → OEM без метки времени ``` -In C, this is a `switch` inside a `switch` inside a `switch`, with each level sharing -the same `uint8_t *data` pointer. Forget one level, misread the spec table, or index -the wrong byte — the bug is silent. +В C это `switch` внутри `switch` внутри `switch`, и каждый уровень использует один и тот же указатель `uint8_t *data`. Забыли один уровень, неверно прочитали таблицу спецификации или взяли не тот байт — ошибка проходит незамеченной. ```c -// C — the polymorphic parsing problem +// C — проблема полиморфного разбора void process_sel_entry(uint8_t *data, int len) { - if (data[2] == 0x02) { // system event + if (data[2] == 0x02) { // системное событие uint8_t event_type = (data[10] >> 4) & 0x07; - if (event_type == 0x01) { // threshold - uint8_t reading = data[11]; // 🐛 or is it data[13]? - uint8_t threshold = data[12]; // 🐛 spec says byte 12 is trigger, not threshold - printf("Temp: %d crossed %d\n", reading, threshold); - } else if (event_type == 0x6F) { // sensor-specific + if (event_type == 0x01) { // порог + uint8_t reading = data[11]; // 🐛 или всё-таки data[13]? + uint8_t threshold = data[12]; // 🐛 в спецификации байт 12 — это триггер, а не порог + printf("Температура: %d пересекла %d\n", reading, threshold); + } else if (event_type == 0x6F) { // специфичное для датчика uint8_t sensor_type = data[7]; - if (sensor_type == 0x0C) { // memory - // 🐛 forgot to check event data 1 offset bits - printf("Memory ECC error\n"); + if (sensor_type == 0x0C) { // память + // 🐛 забыли проверить биты смещения event data 1 + printf("Ошибка ECC памяти\n"); } - // 🐛 no else — silently drops 30+ other sensor types + // 🐛 нет else — молча отбрасываются 30+ других типов датчиков } } - // 🐛 OEM record types silently ignored + // 🐛 записи OEM молча игнорируются } ``` -### Step 1 — Parse the Outer Frame +### Шаг 1 — разбор внешнего уровня -The first `TryFrom` dispatches on record type — the outermost layer of the union: +Первый `TryFrom` диспетчеризует по типу записи — самому внешнему слою объединения: ```rust,ignore -/// Raw 16-byte SEL record, straight from `Get SEL Entry` (IPMI cmd 0x43). +/// Сырая 16-байтная запись SEL, прямо из `Get SEL Entry` (команда IPMI 0x43). pub struct RawSelRecord(pub [u8; 16]); -/// Validated SEL record — record type dispatched, all fields checked. +/// Проверенная запись SEL — тип записи продиспетчеризован, все поля проверены. pub enum ValidSelRecord { SystemEvent(SystemEventRecord), OemTimestamped(OemTimestampedRecord), @@ -348,10 +339,10 @@ pub enum SelParseError { impl std::fmt::Display for SelParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::UnknownRecordType(t) => write!(f, "unknown record type: 0x{t:02X}"), - Self::UnknownSensorType(t) => write!(f, "unknown sensor type: 0x{t:02X}"), - Self::UnknownEventType(t) => write!(f, "unknown event type: 0x{t:02X}"), - Self::InvalidEventData { reason } => write!(f, "invalid event data: {reason}"), + Self::UnknownRecordType(t) => write!(f, "неизвестный тип записи: 0x{t:02X}"), + Self::UnknownSensorType(t) => write!(f, "неизвестный тип датчика: 0x{t:02X}"), + Self::UnknownEventType(t) => write!(f, "неизвестный тип события: 0x{t:02X}"), + Self::InvalidEventData { reason } => write!(f, "некорректные данные события: {reason}"), } } } @@ -389,13 +380,11 @@ impl TryFrom for ValidSelRecord { } ``` -After this boundary, every consumer matches on the enum. The compiler enforces -handling all three record types — you can't "forget" OEM records. +После этой границы каждый потребитель выполняет сопоставление с enum. Компилятор требует обработать все три типа записей: вы не можете «забыть» OEM-записи. -### Step 2 — Parse the System Event: Sensor Type → Typed Event +### Шаг 2 — разбор системного события: тип датчика → типизированное событие -The inner dispatch turns the event data bytes into a sum type indexed by sensor -type. This is where the C `switch`-in-a-`switch` becomes a nested enum: +Внутренняя диспетчеризация превращает байты данных события в сумму типов, индексированную типом датчика. Вложенный `switch` из C здесь становится вложенным enum: ```rust,ignore #[derive(Debug)] @@ -406,7 +395,7 @@ pub struct SystemEventRecord { pub sensor_type: SensorType, pub sensor_number: u8, pub event_direction: EventDirection, - pub event: TypedEvent, // ← the key: event data is TYPED + pub event: TypedEvent, // ← ключевое: данные события ТИПИЗИРОВАНЫ } #[derive(Debug)] @@ -418,10 +407,10 @@ pub enum GeneratorId { #[derive(Debug, Clone, Copy, PartialEq)] pub enum EventDirection { Assertion, Deassertion } -// ──── The Sensor/Event Type Hierarchy ──── +// ──── Иерархия типов датчиков и событий ──── -/// Sensor types from IPMI Table 42-3. Non-exhaustive because future -/// IPMI revisions and OEM ranges will add variants (see ch11 trick 3). +/// Типы датчиков из таблицы 42-3 IPMI. Non-exhaustive, потому что будущие версии IPMI +/// и диапазоны OEM добавят варианты (см. приём 3 в гл. 11). #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq)] pub enum SensorType { @@ -437,7 +426,7 @@ pub enum SensorType { Watchdog2, // 0x23 } -/// The polymorphic payload — each variant carries its own typed data. +/// Полиморфная полезная нагрузка: каждый вариант несёт собственные типизированные данные. #[derive(Debug)] pub enum TypedEvent { Threshold(ThresholdEvent), @@ -445,9 +434,9 @@ pub enum TypedEvent { Discrete { offset: u8, event_data: [u8; 3] }, } -/// Threshold events carry the trigger reading and threshold value. -/// Both are raw sensor values (pre-linearization), kept as u8. -/// After SDR linearization, they become dimensional types (ch06). +/// События порога несут значение триггера и пороговое значение. +/// Оба — сырые значения датчика (до линеаризации), хранятся как u8. +/// После линеаризации SDR они становятся размерными типами (гл. 06). #[derive(Debug)] pub struct ThresholdEvent { pub crossing: ThresholdCrossing, @@ -471,8 +460,8 @@ pub enum ThresholdCrossing { UpperNonRecoverableHigh, } -/// Sensor-specific events — each sensor type gets its own variant -/// with an exhaustive enum of that sensor's defined events. +/// Специфичные для датчика события: каждый тип датчика получает свой вариант +/// с исчерпывающим enum событий этого датчика. #[derive(Debug)] pub enum SensorSpecificEvent { Temperature(TempEvent), @@ -485,7 +474,7 @@ pub enum SensorSpecificEvent { Watchdog(WatchdogEvent), } -// ──── Per-sensor-type event enums (from IPMI Table 42-3) ──── +// ──── Перечисления событий для каждого типа датчика (из таблицы 42-3 IPMI) ──── #[derive(Debug, Clone, Copy, PartialEq)] pub enum MemoryEvent { @@ -582,7 +571,7 @@ pub enum WatchdogEvent { } ``` -### Step 3 — The Parser Wiring +### Шаг 3 — связка парсера ```rust,ignore fn parse_system_event(record_id: u16, d: &[u8]) -> Result { @@ -611,7 +600,7 @@ fn parse_system_event(record_id: u16, d: &[u8]) -> Result { - // Threshold — event data byte 2 is trigger reading, byte 3 is threshold + // Порог — байт 2 данных события это показание триггера, байт 3 — порог let offset = event_data[0] & 0x0F; TypedEvent::Threshold(ThresholdEvent { crossing: parse_threshold_crossing(offset)?, @@ -620,13 +609,13 @@ fn parse_system_event(record_id: u16, d: &[u8]) -> Result { - // Sensor-specific — dispatch on sensor type + // Специфичное для датчика — диспетчеризация по типу датчика let offset = event_data[0] & 0x0F; let specific = parse_sensor_specific(&sensor_type, offset)?; TypedEvent::SensorSpecific(specific) } 0x02..=0x0C => { - // Generic discrete + // Дискретное общего вида TypedEvent::Discrete { offset: event_data[0] & 0x0F, event_data } } other => return Err(SelParseError::UnknownEventType(other)), @@ -674,7 +663,7 @@ fn parse_threshold_crossing(offset: u8) -> Result Ok(ThresholdCrossing::UpperNonRecoverableLow), 0x0B => Ok(ThresholdCrossing::UpperNonRecoverableHigh), _ => Err(SelParseError::InvalidEventData { - reason: "threshold offset out of range", + reason: "смещение порога вне диапазона", }), } } @@ -698,7 +687,7 @@ fn parse_sensor_specific( 0x09 => MemoryEvent::Throttled, 0x0A => MemoryEvent::CriticalOvertemperature, _ => return Err(SelParseError::InvalidEventData { - reason: "unknown memory event offset", + reason: "неизвестное смещение события памяти", }), }; Ok(SensorSpecificEvent::Memory(ev)) @@ -714,7 +703,7 @@ fn parse_sensor_specific( 0x06 => PowerSupplyEvent::ConfigurationError, 0x07 => PowerSupplyEvent::InactiveStandby, _ => return Err(SelParseError::InvalidEventData { - reason: "unknown power supply event offset", + reason: "неизвестное смещение события блока питания", }), }; Ok(SensorSpecificEvent::PowerSupply(ev)) @@ -733,33 +722,32 @@ fn parse_sensor_specific( 0x09 => ProcessorEvent::TerminatorPresenceDetected, 0x0A => ProcessorEvent::Throttled, _ => return Err(SelParseError::InvalidEventData { - reason: "unknown processor event offset", + reason: "неизвестное смещение события процессора", }), }; Ok(SensorSpecificEvent::Processor(ev)) } - // Pattern repeats for Temperature, Voltage, Fan, etc. - // Each sensor type maps its offsets to a dedicated enum. + // Аналогично для Temperature, Voltage, Fan и других типов. + // Каждый тип датчика сопоставляет свои смещения с отдельным enum. _ => Err(SelParseError::InvalidEventData { - reason: "sensor-specific dispatch not implemented for this sensor type", + reason: "диспетчеризация специфичных событий не реализована для этого типа датчика", }), } } ``` -### Step 4 — Consuming Typed SEL Records +### Шаг 4 — потребление типизированных записей SEL -Once parsed, downstream code pattern-matches on the nested enums. The compiler -enforces exhaustive handling — no silent fallthrough, no forgotten sensor type: +После разбора нижележащий код выполняет сопоставление с образцом по вложенным enum. Компилятор требует исчерпывающей обработки: никаких тихих проваливаний и забытых типов датчиков: ```rust,ignore -/// Determine whether a SEL event should trigger a hardware alert. -/// The compiler ensures every variant is handled. +/// Определяет, должно ли событие SEL вызывать аппаратное оповещение. +/// Компилятор гарантирует, что обрабатывается каждый вариант. fn should_alert(record: &ValidSelRecord) -> bool { match record { ValidSelRecord::SystemEvent(sys) => match &sys.event { TypedEvent::Threshold(t) => { - // Any critical or non-recoverable threshold crossing → alert + // Любое критическое или невосстанавливаемое пересечение порога → оповещение matches!(t.crossing, ThresholdCrossing::UpperCriticalLow | ThresholdCrossing::UpperCriticalHigh @@ -786,92 +774,91 @@ fn should_alert(record: &ValidSelRecord) -> bool { | ProcessorEvent::ThermalTrip | ProcessorEvent::UncorrectableMachineCheck ), - // New sensor type variant added in a future version? - // ❌ Compile error: non-exhaustive patterns + // Новый вариант типа датчика, добавленный в будущей версии? + // ❌ Ошибка компиляции: non-exhaustive patterns _ => false, }, TypedEvent::Discrete { .. } => false, }, - // OEM records are not alertable in this policy + // Записи OEM в этой политике не вызывают оповещений ValidSelRecord::OemTimestamped(_) => false, ValidSelRecord::OemNonTimestamped(_) => false, } } -/// Generate a human-readable description. -/// Every branch produces a specific message — no "unknown event" fallback. +/// Формирует понятное человеку описание. +/// Каждая ветка даёт конкретное сообщение — без запасного варианта «неизвестное событие». fn describe(record: &ValidSelRecord) -> String { match record { ValidSelRecord::SystemEvent(sys) => { - let sensor = format!("{:?} sensor #{}", sys.sensor_type, sys.sensor_number); + let sensor = format!("датчик {:?} #{}", sys.sensor_type, sys.sensor_number); let dir = match sys.event_direction { - EventDirection::Assertion => "asserted", - EventDirection::Deassertion => "deasserted", + EventDirection::Assertion => "установлено", + EventDirection::Deassertion => "снято", }; match &sys.event { TypedEvent::Threshold(t) => { - format!("{sensor}: {:?} {dir} (reading: 0x{:02X}, threshold: 0x{:02X})", + format!("{sensor}: {:?} {dir} (показание: 0x{:02X}, порог: 0x{:02X})", t.crossing, t.trigger_reading, t.threshold_value) } TypedEvent::SensorSpecific(ss) => { format!("{sensor}: {ss:?} {dir}") } TypedEvent::Discrete { offset, .. } => { - format!("{sensor}: discrete offset {offset:#x} {dir}") + format!("{sensor}: дискретное смещение {offset:#x} {dir}") } } } ValidSelRecord::OemTimestamped(oem) => - format!("OEM record 0x{:04X} (mfr {:02X}{:02X}{:02X})", + format!("OEM-запись 0x{:04X} (производитель {:02X}{:02X}{:02X})", oem.record_id, oem.manufacturer_id[0], oem.manufacturer_id[1], oem.manufacturer_id[2]), ValidSelRecord::OemNonTimestamped(oem) => - format!("OEM non-ts record 0x{:04X}", oem.record_id), + format!("OEM-запись без метки времени 0x{:04X}", oem.record_id), } } ``` -### Walkthrough: End-to-End SEL Processing +### Пошаговый разбор: сквозная обработка SEL -Here's a complete flow — from raw bytes off the wire to an alert decision — -showing every typed handoff: +Вот полный поток: от сырых байтов по линии до решения о тревоге, с каждой типизированной передачей: ```rust,ignore -/// Process all SEL entries from a BMC, producing typed alerts. +/// Обрабатывает все записи SEL от BMC, формируя типизированные тревоги. fn process_sel_log(raw_entries: &[[u8; 16]]) -> Vec { let mut alerts = Vec::new(); for (i, raw_bytes) in raw_entries.iter().enumerate() { - // ─── Boundary: raw bytes → validated record ─── + // ─── Граница: сырые байты → проверенная запись ─── let raw = RawSelRecord(*raw_bytes); let record = match ValidSelRecord::try_from(raw) { Ok(r) => r, Err(e) => { - eprintln!("SEL entry {i}: parse error: {e}"); + eprintln!("Запись SEL {i}: ошибка разбора: {e}"); continue; } }; - // ─── From here, everything is typed ─── + // ─── Отсюда всё типизировано ─── - // 1. Describe the event (exhaustive match — every variant covered) + // 1. Описываем событие (исчерпывающее сопоставление — покрыты все варианты) let description = describe(&record); println!("SEL[{i}]: {description}"); - // 2. Check alert policy (exhaustive match — compiler proves completeness) + // 2. Проверяем политику тревог (исчерпывающее сопоставление — компилятор доказывает полноту) if should_alert(&record) { alerts.push(description); } - // 3. Extract dimensional readings from threshold events + // 3. Извлекаем размерные показания из событий порога if let ValidSelRecord::SystemEvent(sys) = &record { if let TypedEvent::Threshold(t) = &sys.event { - // The compiler knows t.trigger_reading is a threshold event reading, - // not an arbitrary byte. After SDR linearization (ch06), this becomes: + // Компилятор знает, что t.trigger_reading — это показание события порога, + // а не произвольный байт. После линеаризации SDR (гл. 06) это станет: // let temp: Celsius = linearize(t.trigger_reading, &sdr); - // And then Celsius can't be compared with Rpm. + // А затем Celsius нельзя сравнить с Rpm. println!( - " → raw reading: 0x{:02X}, raw threshold: 0x{:02X}", + " → сырое показание: 0x{:02X}, сырой порог: 0x{:02X}", t.trigger_reading, t.threshold_value ); } @@ -882,102 +869,94 @@ fn process_sel_log(raw_entries: &[[u8; 16]]) -> Vec { } fn main() { - // Example: two SEL entries (fabricated for illustration) + // Пример: две записи SEL (сфабрикованы для иллюстрации) let sel_data: Vec<[u8; 16]> = vec![ - // Entry 1: System event, Memory sensor #3, sensor-specific, - // offset 0x00 = CorrectableEcc, assertion + // Запись 1: системное событие, датчик памяти #3, специфичное для датчика, + // смещение 0x00 = CorrectableEcc, установка [ - 0x01, 0x00, // record ID: 1 - 0x02, // record type: system event - 0x00, 0x00, 0x00, 0x00, // timestamp (stub) - 0x20, // generator: IPMB slave addr 0x20 - 0x00, // channel/lun - 0x04, // event message rev - 0x0C, // sensor type: Memory (0x0C) - 0x03, // sensor number: 3 - 0x6F, // event dir: assertion, event type: sensor-specific - 0x00, // event data 1: offset 0x00 = CorrectableEcc - 0x00, 0x00, // event data 2-3 + 0x01, 0x00, // ID записи: 1 + 0x02, // тип записи: системное событие + 0x00, 0x00, 0x00, 0x00, // метка времени (заглушка) + 0x20, // генератор: адрес ведомого IPMB 0x20 + 0x00, // канал/LUN + 0x04, // версия сообщения события + 0x0C, // тип датчика: Memory (0x0C) + 0x03, // номер датчика: 3 + 0x6F, // направление: установка, тип события: специфичное для датчика + 0x00, // данные события 1: смещение 0x00 = CorrectableEcc + 0x00, 0x00, // данные события 2–3 ], - // Entry 2: System event, Temperature sensor #1, threshold, - // offset 0x09 = UpperCriticalHigh, reading=95, threshold=90 + // Запись 2: системное событие, датчик температуры #1, порог, + // смещение 0x09 = UpperCriticalHigh, показание=95, порог=90 [ - 0x02, 0x00, // record ID: 2 - 0x02, // record type: system event - 0x00, 0x00, 0x00, 0x00, // timestamp (stub) - 0x20, // generator - 0x00, // channel/lun - 0x04, // event message rev - 0x01, // sensor type: Temperature (0x01) - 0x01, // sensor number: 1 - 0x01, // event dir: assertion, event type: threshold (0x01) - 0x09, // event data 1: offset 0x09 = UpperCriticalHigh - 0x5F, // event data 2: trigger reading (95 raw) - 0x5A, // event data 3: threshold value (90 raw) + 0x02, 0x00, // ID записи: 2 + 0x02, // тип записи: системное событие + 0x00, 0x00, 0x00, 0x00, // метка времени (заглушка) + 0x20, // генератор + 0x00, // канал/LUN + 0x04, // версия сообщения события + 0x01, // тип датчика: Temperature (0x01) + 0x01, // номер датчика: 1 + 0x01, // направление: установка, тип события: порог (0x01) + 0x09, // данные события 1: смещение 0x09 = UpperCriticalHigh + 0x5F, // данные события 2: значение триггера (95 в сыром виде) + 0x5A, // данные события 3: пороговое значение (90 в сыром виде) ], ]; let alerts = process_sel_log(&sel_data); - println!("\n=== ALERTS ({}) ===", alerts.len()); + println!("\n=== ТРЕВОГИ ({}) ===", alerts.len()); for alert in &alerts { println!(" 🚨 {alert}"); } } ``` -**Expected output:** +**Ожидаемый вывод:** ```text -SEL[0]: Memory sensor #3: Memory(CorrectableEcc) asserted -SEL[1]: Temperature sensor #1: UpperCriticalHigh asserted (reading: 0x5F, threshold: 0x5A) - → raw reading: 0x5F, raw threshold: 0x5A +SEL[0]: датчик Memory #3: Memory(CorrectableEcc) установлено +SEL[1]: датчик Temperature #1: UpperCriticalHigh установлено (показание: 0x5F, порог: 0x5A) + → сырое показание: 0x5F, сырой порог: 0x5A -=== ALERTS (1) === - 🚨 Temperature sensor #1: UpperCriticalHigh asserted (reading: 0x5F, threshold: 0x5A) +=== ТРЕВОГИ (1) === + 🚨 датчик Temperature #1: UpperCriticalHigh установлено (показание: 0x5F, порог: 0x5A) ``` -Entry 0 (correctable ECC) is logged but not alerted. Entry 1 (upper critical -temperature) triggers an alert. Both decisions are enforced by exhaustive pattern -matching — the compiler proves every sensor type and threshold crossing is handled. +Запись 0 (корректируемая ошибка ECC) регистрируется, но тревоги не вызывает. Запись 1 (верхний критический порог температуры) вызывает тревогу. Оба решения обеспечены исчерпывающим сопоставлением с образцом: компилятор доказывает, что каждый тип датчика и каждое пересечение порога обработаны. -### From Parsed Events to Redfish Health: The Consumer Pipeline +### От разобранных событий к здоровью Redfish: конвейер потребителя -The walkthrough above ends with alerts — but in a real BMC, parsed SEL records -flow into the Redfish health rollup ([ch18](ch18-redfish-server-walkthrough.md)). -The current handoff is a lossy `bool`: +Пошаговый разбор выше заканчивается тревогами, но в реальном BMC разобранные записи SEL попадают в агрегацию состояния здоровья Redfish ([гл. 18](ch18-redfish-server-walkthrough.md)). Текущая передача данных — это `bool`, который теряет информацию: ```rust,ignore -// ❌ Lossy — throws away per-subsystem detail +// ❌ С потерями: отбрасываем детали по подсистемам pub struct SelSummary { pub has_critical_events: bool, pub total_entries: u32, } ``` -This loses everything the type system just gave us: which subsystem is affected, -what severity level, and whether the reading carries dimensional data. Let's build -the full pipeline. +Это теряет всё, что дала система типов: какая подсистема затронута, уровень серьёзности и есть ли в показании размерные данные. Построим полный конвейер. -#### Step 1 — SDR Linearization: Raw Bytes → Dimensional Types (ch06) +#### Шаг 1 — линеаризация SDR: сырые байты → размерные типы (гл. 06) -Threshold SEL events carry raw sensor readings in event data bytes 2-3. The IPMI -SDR (Sensor Data Record) provides the linearization formula. After linearization, -the raw byte becomes a dimensional type: +События порога SEL несут сырые показания датчика в байтах данных 2–3. SDR (Sensor Data Record) IPMI задаёт формулу линеаризации. После линеаризации сырой байт становится размерным типом: ```rust,ignore -/// SDR linearization coefficients for a single sensor. -/// See IPMI spec section 36.3 for the full formula. +/// Коэффициенты линеаризации SDR для одного датчика. +/// См. раздел 36.3 спецификации IPMI для полной формулы. pub struct SdrLinearization { pub sensor_type: SensorType, - pub m: i16, // multiplier - pub b: i16, // offset - pub r_exp: i8, // result exponent (power-of-10) - pub b_exp: i8, // B exponent + pub m: i16, // множитель + pub b: i16, // смещение + pub r_exp: i8, // экспонента результата (степень 10) + pub b_exp: i8, // экспонента B } -/// A linearized sensor reading with its unit attached. -/// The return type depends on the sensor type — the compiler -/// enforces that temperature sensors produce Celsius, not Rpm. +/// Линеаризованное показание датчика с прикреплённой единицей измерения. +/// Возвращаемый тип зависит от типа датчика: компилятор гарантирует, что датчики +/// температуры возвращают Celsius, а не Rpm. #[derive(Debug, Clone)] pub enum LinearizedReading { Temperature(Celsius), @@ -991,9 +970,9 @@ pub enum LinearizedReading { pub struct Amps(pub f64); impl SdrLinearization { - /// Apply the IPMI linearization formula: + /// Применяет формулу линеаризации IPMI: /// y = (M × raw + B × 10^B_exp) × 10^R_exp - /// Returns a dimensional type based on the sensor type. + /// Возвращает размерный тип в зависимости от типа датчика. pub fn linearize(&self, raw: u8) -> LinearizedReading { let y = (self.m as f64 * raw as f64 + self.b as f64 * 10_f64.powi(self.b_exp as i32)) @@ -1005,23 +984,21 @@ impl SdrLinearization { SensorType::Fan => LinearizedReading::Fan(Rpm(y as u32)), SensorType::Current => LinearizedReading::Current(Amps(y)), SensorType::PowerSupply => LinearizedReading::Power(Watts(y)), - // Other sensor types — extend as needed + // Остальные типы датчиков — расширить при необходимости _ => LinearizedReading::Temperature(Celsius(y)), } } } ``` -With this, the raw byte `0x5F` (95 decimal) from our SEL walkthrough becomes -`Celsius(95.0)` — and the compiler prevents comparing it with `Rpm` or `Watts`. +Теперь сырой байт `0x5F` (95 в десятичной системе) из нашего пошагового разбора SEL становится `Celsius(95.0)`, и компилятор не даст сравнить его с `Rpm` или `Watts`. -#### Step 2 — Per-Subsystem Health Classification +#### Шаг 2 — классификация здоровья по подсистемам -Instead of collapsing everything into `has_critical_events: bool`, classify each -parsed SEL event into a per-subsystem health bucket: +Вместо свёртывания всего в `has_critical_events: bool` классифицируем каждое разобранное событие SEL по корзине здоровья подсистемы: ```rust,ignore -/// Health contribution from a single SEL event, classified by subsystem. +/// Вклад одного события SEL в здоровье, классифицированный по подсистемам. #[derive(Debug, Clone)] pub enum SubsystemHealth { Processor(HealthValue), @@ -1033,20 +1010,20 @@ pub enum SubsystemHealth { Security(HealthValue), } -/// Classify a typed SEL event into per-subsystem health. -/// Exhaustive matching ensures every sensor type contributes. +/// Классифицирует типизированное событие SEL по подсистемам. +/// Исчерпывающее сопоставление гарантирует, что каждый тип датчика вносит вклад. fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth { match &record.event { TypedEvent::Threshold(t) => { - // Threshold severity depends on the crossing level + // Серьёзность порога зависит от уровня пересечения let health = match t.crossing { - // Non-critical → Warning + // Некритичный → Warning ThresholdCrossing::UpperNonCriticalLow | ThresholdCrossing::UpperNonCriticalHigh | ThresholdCrossing::LowerNonCriticalLow | ThresholdCrossing::LowerNonCriticalHigh => HealthValue::Warning, - // Critical or Non-recoverable → Critical + // Критический или невосстанавливаемый → Critical ThresholdCrossing::UpperCriticalLow | ThresholdCrossing::UpperCriticalHigh | ThresholdCrossing::LowerCriticalLow @@ -1057,7 +1034,7 @@ fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth { | ThresholdCrossing::LowerNonRecoverableHigh => HealthValue::Critical, }; - // Route to the correct subsystem based on sensor type + // Направляем в нужную подсистему по типу датчика match record.sensor_type { SensorType::Temperature => SubsystemHealth::Thermal(health), SensorType::Voltage => SubsystemHealth::PowerSupply(health), @@ -1131,7 +1108,7 @@ fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth { SensorSpecificEvent::Watchdog(_) => SubsystemHealth::Processor(HealthValue::Warning), - // Temperature, Voltage, Fan sensor-specific events + // Специфичные для датчиков температуры, напряжения и вентилятора события SensorSpecificEvent::Temperature(_) => SubsystemHealth::Thermal(HealthValue::Warning), SensorSpecificEvent::Voltage(_) => @@ -1141,7 +1118,7 @@ fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth { }, TypedEvent::Discrete { .. } => { - // Generic discrete — classify by sensor type with Warning + // Дискретное общего вида: классифицируем по типу датчика с Warning match record.sensor_type { SensorType::Processor => SubsystemHealth::Processor(HealthValue::Warning), SensorType::Memory => SubsystemHealth::Memory(HealthValue::Warning), @@ -1152,21 +1129,17 @@ fn classify_event_health(record: &SystemEventRecord) -> SubsystemHealth { } ``` -Every `match` arm is exhaustive — add a new `MemoryEvent` variant and the compiler -forces you to decide its severity. Add a new `SensorSpecificEvent` variant and -every consumer must classify it. This is the payoff of the enum tree from the -parsing section. +Каждая ветка `match` исчерпывающая: добавьте новый вариант `MemoryEvent`, и компилятор заставит вас определить его серьёзность. Добавьте новый вариант `SensorSpecificEvent`, и каждый потребитель должен будет его классифицировать. Это и есть выигрыш от дерева enum из раздела о разборе. -#### Step 3 — Aggregate into a Typed SEL Summary +#### Шаг 3 — агрегация в типизированную сводку SEL -Replace the lossy `bool` with a structured summary that preserves per-subsystem -health: +Заменяем `bool`, который теряет информацию, структурированной сводкой, сохраняющей здоровье по подсистемам: ```rust,ignore use std::collections::HashMap; -/// Rich SEL summary — per-subsystem health derived from typed events. -/// This is what gets handed to the Redfish server (ch18) for health rollup. +/// Расширенная сводка SEL: здоровье по подсистемам, выведенное из типизированных событий. +/// Именно её получает сервер Redfish (гл. 18) для агрегации состояния здоровья. #[derive(Debug, Clone)] pub struct TypedSelSummary { pub total_entries: u32, @@ -1177,11 +1150,11 @@ pub struct TypedSelSummary { pub fan_health: HealthValue, pub storage_health: HealthValue, pub security_health: HealthValue, - /// Dimensional readings from threshold events (post-linearization). + /// Размерные показания из событий порога (после линеаризации). pub threshold_readings: Vec, } -/// A threshold event with linearized readings attached. +/// Событие порога с прикреплёнными линеаризованными показаниями. #[derive(Debug, Clone)] pub struct LinearizedThresholdEvent { pub sensor_type: SensorType, @@ -1191,8 +1164,8 @@ pub struct LinearizedThresholdEvent { pub threshold_value: LinearizedReading, } -/// Build a TypedSelSummary from parsed SEL records. -/// This is the consumer pipeline: parse (Step 0 above) → classify → aggregate. +/// Строит TypedSelSummary из разобранных записей SEL. +/// Это конвейер потребителя: разбор (шаг 0 выше) → классификация → агрегация. pub fn summarize_sel( records: &[ValidSelRecord], sdr_table: &HashMap, @@ -1211,10 +1184,10 @@ pub fn summarize_sel( count += 1; let ValidSelRecord::SystemEvent(sys) = record else { - continue; // OEM records don't contribute to health + continue; // записи OEM не влияют на здоровье }; - // ── Classify event → per-subsystem health ── + // ── Классифицируем событие → здоровье по подсистемам ── let health = classify_event_health(sys); match &health { SubsystemHealth::Processor(h) => processor = processor.max(*h), @@ -1226,7 +1199,7 @@ pub fn summarize_sel( SubsystemHealth::Security(h) => security = security.max(*h), } - // ── Linearize threshold readings if SDR is available ── + // ── Линеаризуем показания порога, если SDR доступен ── if let TypedEvent::Threshold(t) = &sys.event { if let Some(sdr) = sdr_table.get(&sys.sensor_number) { threshold_readings.push(LinearizedThresholdEvent { @@ -1254,23 +1227,22 @@ pub fn summarize_sel( } ``` -#### Step 4 — The Full Pipeline: Raw Bytes → Redfish Health +#### Шаг 4 — полный конвейер: сырые байты → здоровье Redfish -Here's the complete consumer pipeline, showing every typed handoff from raw SEL -bytes to Redfish-ready health values: +Вот полный конвейер потребителя, с каждой типизированной передачей от сырых байтов SEL до значений здоровья, готовых для Redfish: ```mermaid flowchart LR - RAW["Raw [u8; 16]\nSEL entries"] - PARSE["TryFrom:\nValidSelRecord\n(enum tree)"] - CLASSIFY["classify_event_health\n(exhaustive match)"] - LINEARIZE["SDR linearize\nraw → Celsius/Rpm/Watts"] - SUMMARY["TypedSelSummary\n(per-subsystem health\n+ dimensional readings)"] - REDFISH["ch18: health rollup\n→ Status.Health JSON"] - - RAW -->|"ch07 §Parse"| PARSE - PARSE -->|"typed events"| CLASSIFY - PARSE -->|"threshold bytes"| LINEARIZE + RAW["Сырые [u8; 16]\nзаписи SEL"] + PARSE["TryFrom:\nValidSelRecord\n(дерево enum)"] + CLASSIFY["classify_event_health\n(исчерпывающее match)"] + LINEARIZE["Линеаризация SDR\nсырое → Celsius/Rpm/Watts"] + SUMMARY["TypedSelSummary\n(здоровье по подсистемам\n+ размерные показания)"] + REDFISH["гл. 18: агрегация здоровья\n→ JSON Status.Health"] + + RAW -->|"гл. 07 §Разбор"| PARSE + PARSE -->|"типизированные события"| CLASSIFY + PARSE -->|"байты порога"| LINEARIZE CLASSIFY -->|"SubsystemHealth"| SUMMARY LINEARIZE -->|"LinearizedReading"| SUMMARY SUMMARY -->|"TypedSelSummary"| REDFISH @@ -1287,127 +1259,121 @@ flowchart LR use std::collections::HashMap; fn full_sel_pipeline() { - // ── Raw SEL data from BMC ── + // ── Сырые данные SEL от BMC ── let raw_entries: Vec<[u8; 16]> = vec![ - // Memory correctable ECC on sensor #3 + // Корректируемая ошибка ECC памяти на датчике #3 [0x01,0x00, 0x02, 0x00,0x00,0x00,0x00, 0x20,0x00, 0x04, 0x0C, 0x03, 0x6F, 0x00, 0x00,0x00], - // Temperature upper critical on sensor #1, reading=95, threshold=90 + // Верхний критический порог температуры на датчике #1, показание=95, порог=90 [0x02,0x00, 0x02, 0x00,0x00,0x00,0x00, 0x20,0x00, 0x04, 0x01, 0x01, 0x01, 0x09, 0x5F,0x5A], - // PSU failure on sensor #5 + // Отказ блока питания на датчике #5 [0x03,0x00, 0x02, 0x00,0x00,0x00,0x00, 0x20,0x00, 0x04, 0x08, 0x05, 0x6F, 0x01, 0x00,0x00], ]; - // ── Step 0: Parse at the boundary (ch07 TryFrom) ── + // ── Шаг 0: разбор на границе (TryFrom, гл. 07) ── let records: Vec = raw_entries.iter() .filter_map(|raw| ValidSelRecord::try_from(RawSelRecord(*raw)).ok()) .collect(); - // ── Step 1-3: Classify + linearize + aggregate ── + // ── Шаги 1–3: классификация, линеаризация, агрегация ── let mut sdr_table = HashMap::new(); sdr_table.insert(1u8, SdrLinearization { sensor_type: SensorType::Temperature, - m: 1, b: 0, r_exp: 0, b_exp: 0, // 1:1 mapping for this example + m: 1, b: 0, r_exp: 0, b_exp: 0, // отображение 1:1 для этого примера }); let summary = summarize_sel(&records, &sdr_table); - // ── Result: structured, typed, Redfish-ready ── - println!("SEL Summary:"); - println!(" Total entries: {}", summary.total_entries); - println!(" Processor: {:?}", summary.processor_health); // OK - println!(" Memory: {:?}", summary.memory_health); // OK (correctable → OK) - println!(" Power: {:?}", summary.power_health); // Critical (PSU failure) - println!(" Thermal: {:?}", summary.thermal_health); // Critical (upper critical) - println!(" Fan: {:?}", summary.fan_health); // OK - println!(" Security: {:?}", summary.security_health); // OK - - // Dimensional readings preserved from threshold events: + // ── Результат: структурированный, типизированный, готовый для Redfish ── + println!("Сводка SEL:"); + println!(" Всего записей: {}", summary.total_entries); + println!(" Процессор: {:?}", summary.processor_health); // OK + println!(" Память: {:?}", summary.memory_health); // OK (корректируемая → OK) + println!(" Питание: {:?}", summary.power_health); // Critical (отказ БП) + println!(" Температура: {:?}", summary.thermal_health); // Critical (верхний критический) + println!(" Вентилятор: {:?}", summary.fan_health); // OK + println!(" Безопасность: {:?}", summary.security_health); // OK + + // Размерные показания, сохранённые из событий порога: for r in &summary.threshold_readings { - println!(" Threshold: sensor {:?} #{} — {:?} crossed {:?}", + println!(" Порог: датчик {:?} #{} — {:?} пересёк {:?}", r.sensor_type, r.sensor_number, r.trigger_reading, r.crossing); - // trigger_reading is LinearizedReading::Temperature(Celsius(95.0)) - // — not a raw byte, not an untyped f64 + // trigger_reading — это LinearizedReading::Temperature(Celsius(95.0)), + // а не сырой байт и не нетипизированный f64 } - // ── This summary feeds directly into ch18's health rollup ── - // compute_system_health() can now use per-subsystem values - // instead of a single `has_critical_events: bool` + // ── Эта сводка напрямую поступает в агрегацию здоровья гл. 18 ── + // compute_system_health() теперь может использовать значения по подсистемам + // вместо одного `has_critical_events: bool` } ``` -**Expected output:** +**Ожидаемый вывод:** ```text -SEL Summary: - Total entries: 3 - Processor: OK - Memory: OK - Power: Critical - Thermal: Critical - Fan: OK - Security: OK - Threshold: sensor Temperature #1 — Temperature(Celsius(95.0)) crossed UpperCriticalHigh +Сводка SEL: + Всего записей: 3 + Процессор: OK + Память: OK + Питание: Critical + Температура: Critical + Вентилятор: OK + Безопасность: OK + Порог: датчик Temperature #1 — Temperature(Celsius(95.0)) пересёк UpperCriticalHigh ``` -#### What the Consumer Pipeline Proves +#### Что доказывает конвейер потребителя -| Stage | Pattern | What's Enforced | -|-------|---------|-----------------| -| Parse | Validated boundary (ch07) | Every consumer works with typed enums, never raw bytes | -| Classify | Exhaustive matching | Every sensor type and event variant maps to a health value — can't forget one | -| Linearize | Dimensional analysis (ch06) | Raw byte 0x5F becomes `Celsius(95.0)`, not `f64` — can't confuse with RPM | -| Aggregate | Typed fold | Per-subsystem health uses `HealthValue::max()` — `Ord` guarantees correctness | -| Handoff | Structured summary | ch18 receives `TypedSelSummary` with 7 subsystem health values, not a `bool` | +| Этап | Паттерн | Что обеспечивается | +|------|---------|--------------------| +| Разбор | Проверенная граница (гл. 07) | Каждый потребитель работает с типизированными enum, а не с сырыми байтами | +| Классификация | Исчерпывающее сопоставление | Каждый тип датчика и вариант события сопоставлен со значением здоровья: ничего не пропустить | +| Линеаризация | Анализ размерностей (гл. 06) | Сырой байт 0x5F становится `Celsius(95.0)`, а не `f64`: нельзя перепутать с об/мин | +| Агрегация | Типизированная свёртка | Здоровье по подсистемам использует `HealthValue::max()`: `Ord` гарантирует корректность | +| Передача | Структурированная сводка | гл. 18 получает `TypedSelSummary` с 7 значениями здоровья подсистем, а не `bool` | -Compare with the untyped C pipeline: +Сравним с нетипизированным конвейером на C: -| Step | C | Rust | -|------|---|------| -| Parse record type | `switch` with possible fallthrough | `match` on enum — exhaustive | -| Classify severity | manual `if` chain, forgot PSU | exhaustive `match` — compiler error on missing variant | -| Linearize reading | `double` — no unit | `Celsius` / `Rpm` / `Watts` — distinct types | -| Aggregate health | `bool has_critical` | 7 typed subsystem fields | -| Handoff to Redfish | untyped `json_object_set("Health", "OK")` | `TypedSelSummary` → typed health rollup (ch18) | +| Шаг | C | Rust | +|-----|---|------| +| Разбор типа записи | `switch` с возможным проваливанием | `match` по enum: исчерпывающий | +| Классификация серьёзности | ручная цепочка `if`, забыли БП | исчерпывающий `match`: ошибка компиляции при отсутствующем варианте | +| Линеаризация показания | `double`: без единиц | `Celsius` / `Rpm` / `Watts`: разные типы | +| Агрегация здоровья | `bool has_critical` | 7 типизированных полей подсистем | +| Передача в Redfish | нетипизированный `json_object_set("Health", "OK")` | `TypedSelSummary` → типизированная агрегация здоровья (гл. 18) | -The Rust pipeline doesn't just prevent more bugs — it **produces richer output**. -The C pipeline loses information at every stage (polymorphic → flat, dimensional → -untyped, per-subsystem → single bool). The Rust pipeline preserves it all, because -the type system makes it **easier to keep the structure than to throw it away**. +Конвейер на Rust не просто предотвращает больше ошибок: он **даёт более богатый результат**. Конвейер на C теряет информацию на каждом этапе (полиморфное → плоское, размерное → нетипизированное, по подсистемам → один `bool`). Конвейер на Rust сохраняет всё, потому что система типов делает **проще сохранить структуру, чем её выбросить**. -### What the Compiler Proves +### Что доказывает компилятор -| Bug in C | How Rust prevents it | -|----------|---------------------| -| Forgot to check record type | `match` on `ValidSelRecord` — must handle all three variants | -| Wrong byte index for trigger reading | Parsed once into `ThresholdEvent.trigger_reading` — consumers never touch raw bytes | -| Missing `case` for a sensor type | `SensorSpecificEvent` match is exhaustive — compiler error on missing variant | -| Silently dropped OEM records | Enum variant exists — must be handled or explicitly `_ =>` ignored | -| Compared threshold reading (°C) with fan offset | After SDR linearization, `Celsius` ≠ `Rpm` (ch06) | -| Added new sensor type, forgot alert logic | `#[non_exhaustive]` + exhaustive match → compiler error in downstream crates | -| Event data parsed differently in two code paths | Single `parse_system_event()` boundary — one source of truth | +| Ошибка в C | Как Rust её предотвращает | +|------------|---------------------------| +| Забыли проверить тип записи | `match` по `ValidSelRecord`: обязан обработать все три варианта | +| Неверный индекс байта для показания триггера | Разбирается один раз в `ThresholdEvent.trigger_reading`: потребители никогда не трогают сырые байты | +| Пропущен `case` для типа датчика | Сопоставление `SensorSpecificEvent` исчерпывающее: ошибка компиляции при отсутствующем варианте | +| Тихо отброшенные записи OEM | Вариант enum существует: его нужно обработать или явно проигнорировать через `_ =>` | +| Сравнили показание порога (°C) со смещением вентилятора | После линеаризации SDR `Celsius` ≠ `Rpm` (гл. 06) | +| Добавили новый тип датчика и забыли логику тревог | `#[non_exhaustive]` + исчерпывающее сопоставление → ошибка компиляции в зависимых крейтах | +| Событие разбирается по-разному в двух путях кода | Единая граница `parse_system_event()`: один источник истины | -### The Three-Beat Pattern +### Трёхэтапный паттерн -Looking back at this chapter's three case studies, notice the **graduated arc**: +Посмотрев на три кейс-стади этой главы, заметьте **постепенную дугу**: -| Case Study | Input Shape | Parsing Complexity | Key Technique | +| Кейс-стади | Форма входа | Сложность разбора | Ключевая техника | |---|---|---|---| -| **FRU** (bytes) | Flat, fixed layout | One `TryFrom`, check fields | Validated boundary type | -| **Redfish** (JSON) | Structured, known schema | One `TryFrom`, check fields + nesting | Same technique, different transport | -| **SEL** (polymorphic bytes) | Nested discriminated union | Dispatch chain: record type → event type → sensor type | Enum tree + exhaustive matching | +| **FRU** (байты) | Плоская, фиксированная раскладка | Один `TryFrom`, проверка полей | Тип проверенной границы | +| **Redfish** (JSON) | Структурированная, известная схема | Один `TryFrom`, проверка полей и вложенности | Та же техника, другой транспорт | +| **SEL** (полиморфные байты) | Вложенное тегированное объединение | Цепочка диспетчеризации: тип записи → тип события → тип датчика | Дерево enum + исчерпывающее сопоставление | -The principle is identical in all three: **validate once at the boundary, carry -the proof in the type, never re-check.** The SEL case study shows this principle -scales to arbitrarily complex polymorphic data — the type system handles nested -dispatch just as naturally as flat field validation. +Принцип одинаков во всех трёх случаях: **проверяйте один раз на границе, храните доказательство в типе, никогда не проверяйте повторно.** Кейс SEL показывает, что этот принцип масштабируется на сколь угодно сложные полиморфные данные: система типов обрабатывает вложенную диспетчеризацию так же естественно, как проверку плоских полей. -## Composing Validated Types +## Композиция проверенных типов -Validated types compose — a struct of validated fields is itself validated: +Проверенные типы компонуются: структура из проверенных полей сама является проверенной: ```rust,ignore # #[derive(Debug)] @@ -1415,50 +1381,49 @@ Validated types compose — a struct of validated fields is itself validated: # #[derive(Debug)] # pub struct ValidThermalResponse { } -/// A fully validated system snapshot. -/// Each field was validated independently; the composite is also valid. +/// Полностью проверенный снимок системы. +/// Каждое поле проверено независимо; составной объект тоже корректен. #[derive(Debug)] pub struct ValidSystemSnapshot { pub fru: ValidFru, pub thermal: ValidThermalResponse, - // Each field carries its own validity guarantee. - // No need for a "validate_snapshot()" function. + // Каждое поле несёт собственную гарантию корректности. + // Не нужна функция «validate_snapshot()». } -/// Because ValidSystemSnapshot is composed of validated parts, -/// any function that receives it can trust ALL the data. +/// Поскольку ValidSystemSnapshot составлен из проверенных частей, +/// любая функция, которая его получает, может доверять ВСЕМ данным. fn generate_health_report(snapshot: &ValidSystemSnapshot) { - println!("FRU version: {}", snapshot.fru.format_version); - // No validation needed — the type guarantees everything + println!("Версия FRU: {}", snapshot.fru.format_version); + // Проверка не нужна: тип гарантирует всё } ``` -### The Key Insight +### Ключевая идея -> **Validate at the boundary. Carry the proof in the type. Never re-check.** +> **Проверяйте на границе. Храните доказательство в типе. Никогда не проверяйте повторно.** -This eliminates an entire class of bugs: "forgot to validate in this one function." -If a function takes `&ValidFru`, the data IS valid. Period. +Это устраняет целый класс ошибок: «забыли проверить в этой одной функции». Если функция принимает `&ValidFru`, данные ЯВЛЯЮТСЯ корректными. Точка. -### When to Use Validated Boundary Types +### Когда использовать типы проверенных границ -| Data Source | Use validated boundary type? | -|------------|:------:| -| IPMI FRU data from BMC | ✅ Always — complex binary format | -| Redfish JSON responses | ✅ Always — many required fields | -| PCIe configuration space | ✅ Always — register layout is strict | -| SMBIOS tables | ✅ Always — versioned format with checksums | -| User-provided test parameters | ✅ Always — prevent injection | -| Internal function calls | ❌ Usually not — types already constrain | -| Log messages | ❌ No — best-effort, not safety-critical | +| Источник данных | Использовать тип проверенной границы? | +|-----------------|:------:| +| Данные IPMI FRU от BMC | ✅ Всегда: сложный бинарный формат | +| Ответы JSON Redfish | ✅ Всегда: много обязательных полей | +| Пространство конфигурации PCIe | ✅ Всегда: раскладка регистров строгая | +| Таблицы SMBIOS | ✅ Всегда: версионированный формат с контрольными суммами | +| Параметры тестов от пользователя | ✅ Всегда: предотвращает инъекции | +| Внутренние вызовы функций | ❌ Обычно нет: типы уже ограничивают | +| Сообщения журнала | ❌ Нет: best-effort, не критично для безопасности | -## Validation Boundary Flow +## Поток проверки на границе ```mermaid flowchart LR - RAW["Raw bytes / JSON"] -->|"TryFrom / serde"| V{"Valid?"} - V -->|Yes| VT["ValidFru / ValidRedfish"] - V -->|No| E["Err(ParseError)"] + RAW["Сырые байты / JSON"] -->|"TryFrom / serde"| V{"Корректно?"} + V -->|Да| VT["ValidFru / ValidRedfish"] + V -->|Нет| E["Err(ParseError)"] VT -->|"&ValidFru"| F1["fn process()"] & F2["fn report()"] & F3["fn store()"] style RAW fill:#fff3e0,color:#000 style V fill:#e1f5fe,color:#000 @@ -1469,15 +1434,15 @@ flowchart LR style F3 fill:#e8f5e9,color:#000 ``` -## Exercise: Validated SMBIOS Table +## Упражнение: проверенная таблица SMBIOS -Design a `ValidSmbiosType17` type for SMBIOS Type 17 (Memory Device) records: -- Raw input is `&[u8]`; minimum length 21 bytes, byte 0 must be 0x11. -- Fields: `handle: u16`, `size_mb: u16`, `speed_mhz: u16`. -- Use `TryFrom<&[u8]>` so that all downstream functions take `&ValidSmbiosType17`. +Спроектируйте тип `ValidSmbiosType17` для записей SMBIOS Type 17 (Memory Device): +- Вход: `&[u8]`; минимальная длина 21 байт, байт 0 должен быть 0x11. +- Поля: `handle: u16`, `size_mb: u16`, `speed_mhz: u16`. +- Используйте `TryFrom<&[u8]>`, чтобы все нижележащие функции принимали `&ValidSmbiosType17`.
-Solution +Решение ```rust,ignore #[derive(Debug)] @@ -1491,10 +1456,10 @@ impl TryFrom<&[u8]> for ValidSmbiosType17 { type Error = String; fn try_from(raw: &[u8]) -> Result { if raw.len() < 21 { - return Err(format!("too short: {} < 21", raw.len())); + return Err(format!("слишком короткий: {} < 21", raw.len())); } if raw[0] != 0x11 { - return Err(format!("wrong type: 0x{:02X} != 0x11", raw[0])); + return Err(format!("неверный тип: 0x{:02X} != 0x11", raw[0])); } Ok(ValidSmbiosType17 { handle: u16::from_le_bytes([raw[1], raw[2]]), @@ -1504,26 +1469,25 @@ impl TryFrom<&[u8]> for ValidSmbiosType17 { } } -// Downstream functions take the validated type — no re-checking +// Нижележащие функции принимают проверенный тип: повторных проверок нет pub fn report_dimm(dimm: &ValidSmbiosType17) -> String { - format!("DIMM handle 0x{:04X}: {}MB @ {}MHz", + format!("DIMM, handle 0x{:04X}: {} МБ @ {} МГц", dimm.handle, dimm.size_mb, dimm.speed_mhz) } ```
-## Key Takeaways +## Ключевые выводы -1. **Parse once at the boundary** — `TryFrom` validates raw data exactly once; all downstream code trusts the type. -2. **Eliminate shotgun validation** — if a function takes `&ValidFru`, the data IS valid. Period. -3. **The pattern scales from flat to polymorphic** — FRU (flat bytes), Redfish (structured JSON), and SEL (nested discriminated union) all use the same technique at increasing complexity. -4. **Exhaustive matching is validation** — for polymorphic data like SEL, the compiler's enum exhaustiveness check prevents the "forgot a sensor type" class of bugs with zero runtime cost. -5. **The consumer pipeline preserves structure** — parsing → classification → linearization → aggregation keeps per-subsystem health and dimensional readings intact, where C lossy-reduces to a single `bool`. The type system makes it easier to keep information than to throw it away. -6. **`serde` is a natural boundary** — `#[derive(Deserialize)]` with `#[serde(try_from)]` validates JSON at parse time. -7. **Compose validated types** — a `ValidServerHealth` can require `ValidFru` + `ValidThermal` + `ValidPower`. -8. **Pair with proptest (ch14)** — fuzz the `TryFrom` boundary to ensure no valid input is rejected and no invalid input sneaks through. -9. **These patterns compose into full Redfish workflows** — ch17 applies validated boundaries on the client side (parsing JSON responses into typed structs), while ch18 inverts the pattern on the server side (builder type-state ensures every required field is present before serialization). The SEL consumer pipeline built here feeds directly into ch18's `TypedSelSummary` health rollup. +1. **Разбирайте один раз на границе**: `TryFrom` проверяет сырые данные ровно один раз, а весь нижележащий код доверяет типу. +2. **Устраняйте разбросанную валидацию**: если функция принимает `&ValidFru`, данные ЯВЛЯЮТСЯ корректными. Точка. +3. **Паттерн масштабируется от плоского к полиморфному**: FRU (плоские байты), Redfish (структурированный JSON) и SEL (вложенное тегированное объединение) используют одну технику с возрастающей сложностью. +4. **Исчерпывающее сопоставление — это валидация**: для полиморфных данных вроде SEL проверка исчерпываемости enum компилятора предотвращает класс ошибок «забыли тип датчика» без накладных расходов во время выполнения. +5. **Конвейер потребителя сохраняет структуру**: разбор → классификация → линеаризация → агрегация сохраняют здоровье по подсистемам и размерные показания, тогда как C сводит всё к одному `bool`. Система типов делает проще сохранить информацию, чем её выбросить. +6. **`serde` — естественная граница**: `#[derive(Deserialize)]` с `#[serde(try_from)]` проверяет JSON на этапе разбора. +7. **Компонуйте проверенные типы**: `ValidServerHealth` может требовать `ValidFru` + `ValidThermal` + `ValidPower`. +8. **Дополняйте proptest (гл. 14)**: подавайте фаззинг на границу `TryFrom`, чтобы убедиться, что ни один корректный вход не отвергается и ни один некорректный не проходит. +9. **Эти паттерны складываются в полные сценарии Redfish**: гл. 17 применяет проверенные границы на стороне клиента (разбор JSON-ответов в типизированные структуры), а гл. 18 переворачивает паттерн на стороне сервера (typestate билдера гарантирует наличие каждого обязательного поля до сериализации). Конвейер потребителя SEL, построенный здесь, напрямую передаёт `TypedSelSummary` в агрегацию здоровья гл. 18. --- - diff --git a/type-driven-correctness-book/src/ch08-capability-mixins-compile-time-hardware-.md b/type-driven-correctness-book/src/ch08-capability-mixins-compile-time-hardware-.md index 9b73b3a..cf8f3f7 100644 --- a/type-driven-correctness-book/src/ch08-capability-mixins-compile-time-hardware-.md +++ b/type-driven-correctness-book/src/ch08-capability-mixins-compile-time-hardware-.md @@ -1,66 +1,60 @@ -# Capability Mixins — Compile-Time Hardware Contracts 🟡 +# Capability-миксины: контракты с аппаратурой на этапе компиляции 🟡 -> **What you'll learn:** How ingredient traits (bus capabilities) combined with mixin traits and blanket impls eliminate diagnostic code duplication while guaranteeing every hardware dependency is satisfied at compile time. +> **Что вы узнаете:** как трейты-ингредиенты (возможности шин) в сочетании с трейтами-миксинами и blanket impl устраняют дублирование диагностического кода и гарантируют на этапе компиляции, что каждая аппаратная зависимость выполнена. > -> **Cross-references:** [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (integration) +> **Перекрёстные ссылки:** [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция) -## The Problem: Diagnostic Code Duplication +## Проблема: дублирование диагностического кода -Server platforms share diagnostic patterns across subsystems. Fan diagnostics, -temperature monitoring, and power sequencing all follow similar workflows but -operate on different hardware buses. Without abstraction, you get copy-paste: +Серверные платформы используют общие диагностические паттерны в разных подсистемах. Диагностика вентиляторов, мониторинг температуры и последовательность подачи питания следуют похожим сценариям, но работают с разными аппаратными шинами. Без абстракций возникает copy-paste: ```c -// C — duplicated logic across subsystems +// C — дублирующаяся логика в разных подсистемах int run_fan_diag(spi_bus_t *spi, i2c_bus_t *i2c) { - // ... 50 lines of SPI sensor read ... - // ... 30 lines of I2C register check ... - // ... 20 lines of threshold comparison (same as CPU diag) ... + // ... 50 строк чтения датчика по SPI ... + // ... 30 строк проверки регистров I2C ... + // ... 20 строк сравнения с порогами (как в диагностике CPU) ... } int run_cpu_temp_diag(i2c_bus_t *i2c, gpio_t *gpio) { - // ... 30 lines of I2C register check (same as fan diag) ... - // ... 15 lines of GPIO alert check ... - // ... 20 lines of threshold comparison (same as fan diag) ... + // ... 30 строк проверки регистров I2C (как в диагностике вентиляторов) ... + // ... 15 строк проверки сигнала тревоги GPIO ... + // ... 20 строк сравнения с порогами (как в диагностике вентиляторов) ... } ``` -The threshold comparison logic is identical, but you can't extract it because the -bus types differ. With capability mixins, each hardware bus is an **ingredient -trait**, and diagnostic behaviors are automatically provided when the right -ingredients are present. +Логика сравнения с порогами одинакова, но вынести её нельзя, потому что типы шин различаются. С capability-миксинами каждая аппаратная шина становится **трейтом-ингредиентом**, а диагностические поведения предоставляются автоматически, когда есть нужные ингредиенты. -## Ingredient Traits (Hardware Capabilities) +## Трейты-ингредиенты (аппаратные возможности) -Each bus or peripheral is an associated type on a trait. A diagnostic controller -declares which buses it has: +Каждая шина или периферийное устройство — это ассоциированный тип трейта. Диагностический контроллер объявляет, какие шины у него есть: ```rust,ignore -/// SPI bus capability. +/// Возможность шины SPI. pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; } -/// I2C bus capability. +/// Возможность шины I2C. pub trait HasI2c { type I2c: I2cBus; fn i2c(&self) -> &Self::I2c; } -/// GPIO pin access capability. +/// Возможность доступа к пинам GPIO. pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; } -/// IPMI access capability. +/// Возможность доступа к IPMI. pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; } -// Bus trait definitions: +// Определения трейтов шин: pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec; } @@ -80,10 +74,9 @@ pub trait IpmiClient { } ``` -## Mixin Traits (Diagnostic Behaviors) +## Трейты-миксины (диагностические поведения) -A mixin provides behavior **automatically** to any type that has the required -capabilities: +Миксин предоставляет поведение **автоматически** любому типу, у которого есть нужные возможности: ```rust,ignore # pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec; } @@ -98,26 +91,26 @@ capabilities: # pub trait HasGpio { type Gpio: GpioController; fn gpio(&self) -> &Self::Gpio; } # pub trait HasIpmi { type Ipmi: IpmiClient; fn ipmi(&self) -> &Self::Ipmi; } -/// Fan diagnostic mixin — auto-implemented for anything with SPI + I2C. +/// Миксин диагностики вентиляторов — реализуется автоматически для любого типа с SPI + I2C. pub trait FanDiagMixin: HasSpi + HasI2c { fn read_fan_speed(&self, fan_id: u8) -> u32 { - // Read tachometer via SPI + // Считываем тахометр через SPI let cmd = [0x80 | fan_id, 0x00]; let response = self.spi().transfer(&cmd); u32::from_be_bytes([0, 0, response[0], response[1]]) } fn set_fan_pwm(&self, fan_id: u8, duty_percent: u8) { - // Set PWM via I2C controller + // Устанавливаем PWM через контроллер I2C self.i2c().write_register(0x2E, fan_id, duty_percent); } fn run_fan_diagnostic(&self) -> bool { - // Full diagnostic: read all fans, check thresholds + // Полная диагностика: читаем все вентиляторы, проверяем пороги for fan_id in 0..6 { let speed = self.read_fan_speed(fan_id); if speed < 1000 || speed > 20000 { - println!("Fan {fan_id}: FAIL ({speed} RPM)"); + println!("Вентилятор {fan_id}: ОШИБКА ({speed} об/мин)"); return false; } } @@ -125,14 +118,14 @@ pub trait FanDiagMixin: HasSpi + HasI2c { } } -// Blanket implementation — ANY type with SPI + I2C gets FanDiagMixin for free +// Blanket-реализация — ЛЮБОЙ тип с SPI + I2C получает FanDiagMixin бесплатно impl FanDiagMixin for T {} -/// Temperature monitoring mixin — requires I2C + GPIO. +/// Миксин мониторинга температуры — требует I2C + GPIO. pub trait TempMonitorMixin: HasI2c + HasGpio { fn read_temperature(&self, sensor_addr: u8) -> f64 { let raw = self.i2c().read_register(sensor_addr, 0x00); - raw as f64 * 0.5 // 0.5°C per LSB + raw as f64 * 0.5 // 0.5°C на младший разряд (LSB) } fn check_thermal_alert(&self, alert_pin: u32) -> bool { @@ -143,11 +136,11 @@ pub trait TempMonitorMixin: HasI2c + HasGpio { for addr in [0x48, 0x49, 0x4A] { let temp = self.read_temperature(addr); if temp > 95.0 { - println!("Sensor 0x{addr:02X}: CRITICAL ({temp}°C)"); + println!("Датчик 0x{addr:02X}: КРИТИЧНО ({temp}°C)"); return false; } if self.check_thermal_alert(addr as u32) { - println!("Sensor 0x{addr:02X}: ALERT pin asserted"); + println!("Датчик 0x{addr:02X}: сработал сигнал ALERT"); return false; } } @@ -157,11 +150,11 @@ pub trait TempMonitorMixin: HasI2c + HasGpio { impl TempMonitorMixin for T {} -/// Power sequencing mixin — requires I2C + IPMI. +/// Миксин последовательности подачи питания — требует I2C + IPMI. pub trait PowerSeqMixin: HasI2c + HasIpmi { fn read_voltage_rail(&self, rail: u8) -> f64 { let raw = self.i2c().read_register(0x40, rail); - raw as f64 * 0.01 // 10mV per LSB + raw as f64 * 0.01 // 10 мВ на младший разряд (LSB) } fn check_power_good(&self) -> bool { @@ -173,10 +166,9 @@ pub trait PowerSeqMixin: HasI2c + HasIpmi { impl PowerSeqMixin for T {} ``` -## Concrete Controller — Mix and Match +## Конкретный контроллер: комбинирование -A concrete diagnostic controller declares its capabilities, and **automatically -inherits** all matching mixins: +Конкретный диагностический контроллер объявляет свои возможности и **автоматически наследует** все подходящие миксины: ```rust,ignore # pub trait SpiBus { fn transfer(&self, data: &[u8]) -> Vec; } @@ -200,7 +192,7 @@ inherits** all matching mixins: # pub trait PowerSeqMixin: HasI2c + HasIpmi {} # impl PowerSeqMixin for T {} -// Concrete bus implementations (stubs for illustration) +// Конкретные реализации шин (заглушки для иллюстрации) pub struct LinuxSpi { bus: u8 } impl SpiBus for LinuxSpi { fn transfer(&self, data: &[u8]) -> Vec { vec![0; data.len()] } @@ -223,7 +215,7 @@ impl IpmiClient for IpmiToolClient { fn send_raw(&self, _netfn: u8, _cmd: u8, _data: &[u8]) -> Vec { vec![0x00] } } -/// BaseBoardController has ALL buses → gets ALL mixins. +/// BaseBoardController имеет ВСЕ шины → получает ВСЕ миксины. pub struct BaseBoardController { spi: LinuxSpi, i2c: LinuxI2c, @@ -251,52 +243,48 @@ impl HasIpmi for BaseBoardController { fn ipmi(&self) -> &IpmiToolClient { &self.ipmi } } -// BaseBoardController now automatically has: -// - FanDiagMixin (because it HasSpi + HasI2c) -// - TempMonitorMixin (because it HasI2c + HasGpio) -// - PowerSeqMixin (because it HasI2c + HasIpmi) -// No manual implementation needed — blanket impls do it all. +// BaseBoardController теперь автоматически имеет: +// - FanDiagMixin (потому что у него есть HasSpi + HasI2c) +// - TempMonitorMixin (потому что у него есть HasI2c + HasGpio) +// - PowerSeqMixin (потому что у него есть HasI2c + HasIpmi) +// Ручная реализация не нужна: blanket impl делает всё сам. ``` -## Correct-by-Construction Aspect +## Аспект корректности по построению -The mixin pattern is correct-by-construction because: +Паттерн миксинов корректен по построению, потому что: -1. **You can't call `read_fan_speed()` without SPI** — the method only exists on - types that implement `HasSpi + HasI2c` -2. **You can't forget a bus** — if you remove `HasSpi` from `BaseBoardController`, - `FanDiagMixin` methods disappear at compile time -3. **Mock testing is automatic** — replace `LinuxSpi` with `MockSpi` and - all mixin logic works with the mock -4. **New platforms just declare capabilities** — a GPU daughter card with only I2C - gets `TempMonitorMixin` (if it also has GPIO) but not `FanDiagMixin` (no SPI) +1. **Нельзя вызвать `read_fan_speed()` без SPI**: метод существует только у типов, которые реализуют `HasSpi + HasI2c` +2. **Нельзя забыть шину**: если убрать `HasSpi` у `BaseBoardController`, методы `FanDiagMixin` исчезнут на этапе компиляции +3. **Тестирование с моками автоматическое**: замените `LinuxSpi` на `MockSpi`, и вся логика миксинов будет работать с моком +4. **Новые платформы просто объявляют возможности**: дочерняя плата GPU только с I2C получит `TempMonitorMixin` (если есть ещё и GPIO), но не `FanDiagMixin` (нет SPI) -### When to Use Capability Mixins +### Когда использовать capability-миксины -| Scenario | Use mixins? | +| Сценарий | Использовать миксины? | |----------|:------:| -| Cross-cutting diagnostic behaviors | ✅ Yes — prevent copy-paste | -| Multi-bus hardware controllers | ✅ Yes — declare capabilities, get behaviors | -| Platform-specific test harnesses | ✅ Yes — mock capabilities for testing | -| Single-bus simple peripherals | ⚠️ Overhead may not be worth it | -| Pure business logic (no hardware) | ❌ Simpler patterns suffice | +| Сквозные диагностические поведения | ✅ Да: устраняют copy-paste | +| Контроллеры аппаратуры с несколькими шинами | ✅ Да: объявляете возможности, получаете поведения | +| Специализированные тестовые стенды для платформ | ✅ Да: мокайте возможности для тестов | +| Простые периферийные устройства с одной шиной | ⚠️ Накладные расходы могут не окупиться | +| Чистая бизнес-логика (без аппаратуры) | ❌ Простых паттернов достаточно | -## Mixin Trait Architecture +## Архитектура трейтов-миксинов ```mermaid flowchart TD - subgraph "Ingredient Traits" + subgraph "Трейты-ингредиенты" SPI["HasSpi"] I2C["HasI2c"] GPIO["HasGpio"] end - subgraph "Mixin Traits (blanket impls)" + subgraph "Трейты-миксины (blanket impl)" FAN["FanDiagMixin"] TEMP["TempMonitorMixin"] end - SPI & I2C -->|"requires both"| FAN - I2C & GPIO -->|"requires both"| TEMP - subgraph "Concrete Types" + SPI & I2C -->|"нужны оба"| FAN + I2C & GPIO -->|"нужны оба"| TEMP + subgraph "Конкретные типы" BBC["BaseBoardController"] end BBC -->|"impl HasSpi + HasI2c + HasGpio"| FAN & TEMP @@ -308,16 +296,16 @@ flowchart TD style BBC fill:#fff3e0,color:#000 ``` -## Exercise: Network Diagnostic Mixins +## Упражнение: сетевые диагностические миксины -Design a mixin system for network diagnostics: -- Ingredient traits: `HasEthernet`, `HasIpmi` -- Mixin: `LinkHealthMixin` (requires `HasEthernet`) with `check_link_status(&self)` -- Mixin: `RemoteDiagMixin` (requires `HasEthernet + HasIpmi`) with `remote_health_check(&self)` -- Concrete type: `NicController` that implements both ingredients. +Спроектируйте систему миксинов для сетевой диагностики: +- Трейты-ингредиенты: `HasEthernet`, `HasIpmi` +- Миксин: `LinkHealthMixin` (требует `HasEthernet`) с методом `check_link_status(&self)` +- Миксин: `RemoteDiagMixin` (требует `HasEthernet + HasIpmi`) с методом `remote_health_check(&self)` +- Конкретный тип: `NicController`, который реализует оба ингредиента.
-Solution +Решение ```rust,ignore pub trait HasEthernet { @@ -330,7 +318,7 @@ pub trait HasIpmi { pub trait LinkHealthMixin: HasEthernet { fn check_link_status(&self) -> &'static str { - if self.eth_link_up() { "link: UP" } else { "link: DOWN" } + if self.eth_link_up() { "канал: UP" } else { "канал: DOWN" } } } impl LinkHealthMixin for T {} @@ -338,9 +326,9 @@ impl LinkHealthMixin for T {} pub trait RemoteDiagMixin: HasEthernet + HasIpmi { fn remote_health_check(&self) -> &'static str { if self.eth_link_up() && self.ipmi_ping() { - "remote: HEALTHY" + "удалённо: HEALTHY" } else { - "remote: DEGRADED" + "удалённо: DEGRADED" } } } @@ -353,18 +341,17 @@ impl HasEthernet for NicController { impl HasIpmi for NicController { fn ipmi_ping(&self) -> bool { true } } -// NicController automatically gets both mixin methods +// NicController автоматически получает оба метода миксинов ```
-## Key Takeaways +## Ключевые выводы -1. **Ingredient traits declare hardware capabilities** — `HasSpi`, `HasI2c`, `HasGpio` are associated-type traits. -2. **Mixin traits provide behaviour via blanket impls** — `impl FanDiagMixin for T {}`. -3. **Adding a new platform = listing its capabilities** — the compiler provides all matching mixin methods. -4. **Removing a bus = compile errors everywhere it's used** — you can't forget to update downstream code. -5. **Mock testing is free** — swap `LinuxSpi` for `MockSpi`; all mixin logic works unchanged. +1. **Трейты-ингредиенты объявляют аппаратные возможности**: `HasSpi`, `HasI2c`, `HasGpio` — это трейты с ассоциированными типами. +2. **Трейты-миксины предоставляют поведение через blanket impl**: `impl FanDiagMixin for T {}`. +3. **Новая платформа = перечисление её возможностей**: компилятор предоставляет все подходящие методы миксинов. +4. **Удаление шины = ошибки компиляции везде, где она используется**: нельзя забыть обновить нижележащий код. +5. **Тестирование с моками бесплатно**: замените `LinuxSpi` на `MockSpi`, и вся логика миксинов работает без изменений. --- - diff --git a/type-driven-correctness-book/src/ch09-phantom-types-for-resource-tracking.md b/type-driven-correctness-book/src/ch09-phantom-types-for-resource-tracking.md index 894f7e3..e0df3e3 100644 --- a/type-driven-correctness-book/src/ch09-phantom-types-for-resource-tracking.md +++ b/type-driven-correctness-book/src/ch09-phantom-types-for-resource-tracking.md @@ -1,44 +1,43 @@ -# Phantom Types for Resource Tracking 🟡 +# Phantom-типы для отслеживания ресурсов 🟡 -> **What you'll learn:** How `PhantomData` markers encode register width, DMA direction, and file-descriptor state at the type level — preventing an entire class of resource-mismatch bugs at zero runtime cost. +> **Что вы узнаете:** как маркеры `PhantomData` кодируют ширину регистра, направление DMA и состояние файлового дескриптора на уровне типов, предотвращая целый класс ошибок рассогласования ресурсов без накладных расходов во время выполнения. > -> **Cross-references:** [ch05](ch05-protocol-state-machines-type-state-for-r.md) (type-state), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional types), [ch08](ch08-capability-mixins-compile-time-hardware-.md) (mixins), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (integration) +> **Перекрёстные ссылки:** [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (typestate), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (размерные типы), [гл. 08](ch08-capability-mixins-compile-time-hardware-.md) (миксины), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция) -## The Problem: Mixing Up Resources +## Проблема: путаница ресурсов -Hardware resources look alike in code but aren't interchangeable: +Аппаратные ресурсы в коде выглядят одинаково, но не взаимозаменяемы: -- A 32-bit register and a 16-bit register are both "registers" -- A DMA buffer for read and a DMA buffer for write both look like `*mut u8` -- An open file descriptor and a closed one are both `i32` +- 32-битный регистр и 16-битный регистр — оба «регистры» +- Буфер DMA для чтения и буфер DMA для записи оба выглядят как `*mut u8` +- Открытый файловый дескриптор и закрытый — оба `i32` -In C: +В C: ```c -// C — all registers look the same +// C — все регистры выглядят одинаково uint32_t read_reg32(volatile void *base, uint32_t offset); uint16_t read_reg16(volatile void *base, uint32_t offset); -// Bug: reading a 16-bit register with the 32-bit function -uint32_t status = read_reg32(pcie_bar, LINK_STATUS_REG); // should be reg16! +// Ошибка: чтение 16-битного регистра 32-битной функцией +uint32_t status = read_reg32(pcie_bar, LINK_STATUS_REG); // должен быть reg16! ``` -## Phantom Type Parameters +## Параметры phantom-типов -A **phantom type** is a type parameter that appears in the struct definition but -not in any field. It exists purely to carry type-level information: +**Phantom-тип** — это параметр типа, который присутствует в определении структуры, но не появляется ни в одном поле. Он существует исключительно для того, чтобы нести информацию на уровне типов: ```rust,ignore use std::marker::PhantomData; -// Register width markers — zero-sized +// Маркеры ширины регистра — нулевого размера pub struct Width8; pub struct Width16; pub struct Width32; pub struct Width64; -/// A register handle parameterised by its width. -/// PhantomData costs zero bytes — it's a compile-time-only marker. +/// Дескриптор регистра, параметризованный его шириной. +/// PhantomData не занимает места: это маркер только для этапа компиляции. pub struct Register { base: usize, offset: usize, @@ -47,35 +46,35 @@ pub struct Register { impl Register { pub fn read(&self) -> u8 { - // ... read 1 byte from base + offset ... - 0 // stub + // ... читаем 1 байт из base + offset ... + 0 // заглушка } pub fn write(&self, _value: u8) { - // ... write 1 byte ... + // ... пишем 1 байт ... } } impl Register { pub fn read(&self) -> u16 { - // ... read 2 bytes from base + offset ... - 0 // stub + // ... читаем 2 байта из base + offset ... + 0 // заглушка } pub fn write(&self, _value: u16) { - // ... write 2 bytes ... + // ... пишем 2 байта ... } } impl Register { pub fn read(&self) -> u32 { - // ... read 4 bytes from base + offset ... - 0 // stub + // ... читаем 4 байта из base + offset ... + 0 // заглушка } pub fn write(&self, _value: u32) { - // ... write 4 bytes ... + // ... пишем 4 байта ... } } -/// PCIe config space register definitions. +/// Определения регистров конфигурационного пространства PCIe. pub struct PcieConfig { base: usize, } @@ -105,76 +104,73 @@ impl PcieConfig { fn pcie_example() { let cfg = PcieConfig { base: 0xFE00_0000 }; - let vid: u16 = cfg.vendor_id().read(); // returns u16 ✅ - let bar: u32 = cfg.bar0().read(); // returns u32 ✅ + let vid: u16 = cfg.vendor_id().read(); // возвращает u16 ✅ + let bar: u32 = cfg.bar0().read(); // возвращает u32 ✅ - // Can't mix them up: - // let bad: u32 = cfg.vendor_id().read(); // ❌ ERROR: expected u16 - // cfg.bar0().write(0u16); // ❌ ERROR: expected u32 + // Нельзя перепутать: + // let bad: u32 = cfg.vendor_id().read(); // ❌ ОШИБКА: expected u16 + // cfg.bar0().write(0u16); // ❌ ОШИБКА: expected u32 } ``` -## DMA Buffer Access Control +## Контроль доступа к буферам DMA -DMA buffers have direction: some are for **device-to-host** (read), others for -**host-to-device** (write). Using the wrong direction corrupts data or causes -bus errors: +У буферов DMA есть направление: одни предназначены для передачи **от устройства к хосту** (чтение), другие — для передачи **от хоста к устройству** (запись). Использование неверного направления портит данные или вызывает ошибки шины: ```rust,ignore use std::marker::PhantomData; -// Direction markers -pub struct ToDevice; // host writes, device reads -pub struct FromDevice; // device writes, host reads +// Маркеры направления +pub struct ToDevice; // хост пишет, устройство читает +pub struct FromDevice; // устройство пишет, хост читает -/// A DMA buffer with direction enforcement. +/// Буфер DMA с контролем направления. pub struct DmaBuffer { ptr: *mut u8, len: usize, - dma_addr: u64, // physical address for the device + dma_addr: u64, // физический адрес для устройства _dir: PhantomData, } impl DmaBuffer { - /// Fill the buffer with data to send to the device. + /// Заполняет буфер данными для отправки устройству. pub fn write_data(&mut self, data: &[u8]) { assert!(data.len() <= self.len); unsafe { std::ptr::copy_nonoverlapping(data.as_ptr(), self.ptr, data.len()) } } - /// Get the DMA address for the device to read from. + /// Получает адрес DMA, из которого устройство будет читать. pub fn device_addr(&self) -> u64 { self.dma_addr } } impl DmaBuffer { - /// Read data that the device wrote into the buffer. + /// Читает данные, которые устройство записало в буфер. pub fn read_data(&self) -> &[u8] { unsafe { std::slice::from_raw_parts(self.ptr, self.len) } } - /// Get the DMA address for the device to write to. + /// Получает адрес DMA, в который устройство будет писать. pub fn device_addr(&self) -> u64 { self.dma_addr } } -// Can't write to a FromDevice buffer: +// Нельзя писать в буфер FromDevice: // fn oops(buf: &mut DmaBuffer) { -// buf.write_data(&[1, 2, 3]); // ❌ no method `write_data` on DmaBuffer +// buf.write_data(&[1, 2, 3]); // ❌ нет метода `write_data` у DmaBuffer // } -// Can't read from a ToDevice buffer: +// Нельзя читать из буфера ToDevice: // fn oops2(buf: &DmaBuffer) { -// let data = buf.read_data(); // ❌ no method `read_data` on DmaBuffer +// let data = buf.read_data(); // ❌ нет метода `read_data` у DmaBuffer // } ``` -## File Descriptor Ownership +## Владение файловым дескриптором -A common bug: using a file descriptor after it's been closed. Phantom types can -track open/closed state: +Распространённая ошибка: использование файлового дескриптора после его закрытия. Phantom-типы могут отслеживать состояние «открыт/закрыт»: ```rust,ignore use std::marker::PhantomData; @@ -182,7 +178,7 @@ use std::marker::PhantomData; pub struct Open; pub struct Closed; -/// A file descriptor with state tracking. +/// Файловый дескриптор с отслеживанием состояния. pub struct Fd { raw: i32, _state: PhantomData, @@ -190,31 +186,31 @@ pub struct Fd { impl Fd { pub fn open(path: &str) -> Result { - // ... open the file ... - Ok(Fd { raw: 3, _state: PhantomData }) // stub + // ... открываем файл ... + Ok(Fd { raw: 3, _state: PhantomData }) // заглушка } pub fn read(&self, buf: &mut [u8]) -> Result { - // ... read from fd ... - Ok(0) // stub + // ... читаем из fd ... + Ok(0) // заглушка } pub fn write(&self, data: &[u8]) -> Result { - // ... write to fd ... - Ok(data.len()) // stub + // ... пишем в fd ... + Ok(data.len()) // заглушка } - /// Close the fd — returns a Closed handle. - /// The Open handle is consumed, preventing use-after-close. + /// Закрывает fd — возвращает дескриптор в состоянии Closed. + /// Дескриптор Open потребляется, что исключает использование после закрытия. pub fn close(self) -> Fd { - // ... close the fd ... + // ... закрываем fd ... Fd { raw: self.raw, _state: PhantomData } } } impl Fd { - // No read() or write() methods — they don't exist on Fd. - // This makes use-after-close a compile error. + // Нет методов read() и write(): у Fd их просто нет. + // Поэтому использование после закрытия — ошибка компиляции. pub fn raw_fd(&self) -> i32 { self.raw @@ -228,16 +224,16 @@ fn fd_example() -> Result<(), String> { let closed = fd.close(); - // closed.read(&mut buf)?; // ❌ no method `read` on Fd - // closed.write(&[1])?; // ❌ no method `write` on Fd + // closed.read(&mut buf)?; // ❌ нет метода `read` у Fd + // closed.write(&[1])?; // ❌ нет метода `write` у Fd Ok(()) } ``` -## Combining Phantom Types with Earlier Patterns +## Комбинирование phantom-типов с предыдущими паттернами -Phantom types compose with everything we've seen: +Phantom-типы сочетаются со всем, что мы уже видели: ```rust,ignore # use std::marker::PhantomData; @@ -249,43 +245,43 @@ Phantom types compose with everything we've seen: # #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] # pub struct Celsius(pub f64); -/// Combine phantom types (register width) with dimensional types (Celsius). +/// Комбинирует phantom-типы (ширина регистра) с размерными типами (Celsius). fn read_temp_sensor(reg: &Register) -> Celsius { - let raw = reg.read(); // guaranteed u16 by phantom type - Celsius(raw as f64 * 0.0625) // guaranteed Celsius by return type + let raw = reg.read(); // тип u16 гарантирован phantom-типом + Celsius(raw as f64 * 0.0625) // тип Celsius гарантирован возвращаемым типом } -// The compiler enforces: -// 1. The register is 16-bit (phantom type) -// 2. The result is Celsius (newtype) -// Both at zero runtime cost. +// Компилятор обеспечивает: +// 1. Регистр 16-битный (phantom-тип) +// 2. Результат — Celsius (newtype) +// Оба — без накладных расходов во время выполнения. ``` -### When to Use Phantom Types +### Когда использовать phantom-типы -| Scenario | Use phantom parameter? | +| Сценарий | Использовать phantom-параметр? | |----------|:------:| -| Register width encoding | ✅ Always — prevents width mismatch | -| DMA buffer direction | ✅ Always — prevents data corruption | -| File descriptor state | ✅ Always — prevents use-after-close | -| Memory region permissions (R/W/X) | ✅ Always — enforces access control | -| Generic container (Vec, HashMap) | ❌ No — use concrete type parameters | -| Runtime-variable attributes | ❌ No — phantom types are compile-time only | +| Кодирование ширины регистра | ✅ Всегда: предотвращает несоответствие ширины | +| Направление буфера DMA | ✅ Всегда: предотвращает порчу данных | +| Состояние файлового дескриптора | ✅ Всегда: предотвращает использование после закрытия | +| Права доступа к области памяти (R/W/X) | ✅ Всегда: обеспечивает контроль доступа | +| Обобщённые контейнеры (Vec, HashMap) | ❌ Нет: используйте конкретные параметры типа | +| Атрибуты, меняющиеся во время выполнения | ❌ Нет: phantom-типы существуют только на этапе компиляции | -## Phantom Type Resource Matrix +## Матрица ресурсов с phantom-типами ```mermaid flowchart TD - subgraph "Width Markers" - W8["Width8"] + subgraph "Маркеры ширины" + W8["Width8"] W16["Width16"] W32["Width32"] end - subgraph "Direction Markers" + subgraph "Маркеры направления" RD["Read"] WR["Write"] end - subgraph "Typed Resources" + subgraph "Типизированные ресурсы" R1["Register"] R2["DmaBuffer"] R3["DmaBuffer"] @@ -293,7 +289,7 @@ flowchart TD W16 --> R1 RD --> R2 WR --> R3 - R2 -.->|"write attempt"| ERR["❌ Compile Error"] + R2 -.->|"попытка записи"| ERR["❌ Ошибка компиляции"] style W8 fill:#e1f5fe,color:#000 style W16 fill:#e1f5fe,color:#000 style W32 fill:#e1f5fe,color:#000 @@ -305,16 +301,16 @@ flowchart TD style ERR fill:#ffcdd2,color:#000 ``` -## Exercise: Memory Region Permissions +## Упражнение: права доступа к области памяти -Design phantom types for memory regions with read, write, and execute permissions: -- `MemRegion` has `fn read(&self, offset: usize) -> u8` -- `MemRegion` has both `read` and `write` -- `MemRegion` has `read` and `fn execute(&self)` -- Writing to `ReadOnly` or executing `ReadWrite` should not compile. +Спроектируйте phantom-типы для областей памяти с правами чтения, записи и выполнения: +- `MemRegion` имеет `fn read(&self, offset: usize) -> u8` +- `MemRegion` имеет и `read`, и `write` +- `MemRegion` имеет `read` и `fn execute(&self)` +- Запись в `ReadOnly` или выполнение `ReadWrite` не должны компилироваться.
-Solution +Решение ```rust,ignore use std::marker::PhantomData; @@ -329,7 +325,7 @@ pub struct MemRegion { _perm: PhantomData, } -// Read available on all permission types +// Чтение доступно для всех типов прав impl

MemRegion

{ pub fn read(&self, offset: usize) -> u8 { assert!(offset < self.len); @@ -346,23 +342,22 @@ impl MemRegion { impl MemRegion { pub fn execute(&self) { - // Jump to base address (conceptual) + // Переход по базовому адресу (концептуально) } } -// ❌ region_ro.write(0, 0xFF); // Compile error: no method `write` -// ❌ region_rw.execute(); // Compile error: no method `execute` +// ❌ region_ro.write(0, 0xFF); // Ошибка компиляции: нет метода `write` +// ❌ region_rw.execute(); // Ошибка компиляции: нет метода `execute` ```

-## Key Takeaways +## Ключевые выводы -1. **PhantomData carries type-level information at zero size** — the marker exists only for the compiler. -2. **Register width mismatches become compile errors** — `Register` returns `u16`, not `u32`. -3. **DMA direction is enforced structurally** — `DmaBuffer` has no `write()` method. -4. **Combine with dimensional types (ch06)** — `Register` can return `Celsius` via the parse step. -5. **Phantom types are compile-time only** — they don't work for runtime-variable attributes; use enums for those. +1. **PhantomData несёт информацию о типе при нулевом размере**: маркер существует только для компилятора. +2. **Несоответствие ширины регистра становится ошибкой компиляции**: `Register` возвращает `u16`, а не `u32`. +3. **Направление DMA обеспечивается структурно**: у `DmaBuffer` нет метода `write()`. +4. **Сочетайте с размерными типами (гл. 06)**: `Register` может возвращать `Celsius` через шаг разбора. +5. **Phantom-типы работают только на этапе компиляции**: они не подходят для атрибутов, которые меняются во время выполнения; для них используйте enum. --- - diff --git a/type-driven-correctness-book/src/ch10-putting-it-all-together-a-complete-diagn.md b/type-driven-correctness-book/src/ch10-putting-it-all-together-a-complete-diagn.md index d0f195e..901eda3 100644 --- a/type-driven-correctness-book/src/ch10-putting-it-all-together-a-complete-diagn.md +++ b/type-driven-correctness-book/src/ch10-putting-it-all-together-a-complete-diagn.md @@ -1,26 +1,25 @@ -# Putting It All Together — A Complete Diagnostic Platform 🟡 +# Собираем всё вместе: полная платформа диагностики 🟡 -> **What you'll learn:** How all seven core patterns (ch02–ch09) compose into a single diagnostic workflow — authentication, sessions, typed commands, audit tokens, dimensional results, validated data, and phantom-typed registers — with zero total runtime overhead. +> **Что вы узнаете:** как все семь основных паттернов (гл. 02–09) объединяются в единый диагностический сценарий: аутентификация, сессии, типизированные команды, токены аудита, размерные результаты, проверенные данные и регистры с phantom-типами. Суммарные накладные расходы во время выполнения при этом равны нулю. > -> **Cross-references:** Every core pattern chapter (ch02–ch09), [ch14](ch14-testing-type-level-guarantees.md) (testing these guarantees) +> **Перекрёстные ссылки:** каждая глава с основными паттернами (гл. 02–09), [гл. 14](ch14-testing-type-level-guarantees.md) (тестирование этих гарантий) -## Goal +## Цель -This chapter combines **seven patterns** from chapters 2–9 into a single, realistic -diagnostic workflow. We'll build a server health check that: +Эта глава объединяет **семь паттернов** из глав 2–9 в единый реалистичный диагностический сценарий. Мы построим проверку состояния сервера, которая: -1. **Authenticates** (capability token — ch04) -2. **Opens an IPMI session** (type-state — ch05) -3. **Sends typed commands** (typed commands — ch02) -4. **Uses single-use tokens** for audit logging (single-use types — ch03) -5. **Returns dimensional results** (dimensional analysis — ch06) -6. **Validates FRU data** (validated boundaries — ch07) -7. **Reads typed registers** (phantom types — ch09) +1. **Проходит аутентификацию** (capability-токен, гл. 04) +2. **Открывает сессию IPMI** (typestate, гл. 05) +3. **Отправляет типизированные команды** (типизированные команды, гл. 02) +4. **Использует одноразовые токены** для журнала аудита (одноразовые типы, гл. 03) +5. **Возвращает размерные результаты** (анализ размерностей, гл. 06) +6. **Проверяет данные FRU** (проверенные границы, гл. 07) +7. **Читает типизированные регистры** (phantom-типы, гл. 09) ```rust,ignore use std::marker::PhantomData; use std::io; -// ──── Pattern 1: Dimensional Types (ch06) ──── +// ──── Паттерн 1: размерные типы (гл. 06) ──── #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Celsius(pub f64); @@ -31,11 +30,11 @@ pub struct Rpm(pub f64); #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Volts(pub f64); -// ──── Pattern 2: Typed Commands (ch02) ──── +// ──── Паттерн 2: типизированные команды (гл. 02) ──── -/// Same trait shape as ch02, using methods (not associated constants) -/// for consistency. Associated constants (`const NETFN: u8`) are an -/// equally valid alternative when the value is truly fixed per type. +/// Та же форма трейта, что в гл. 02; здесь используются методы (а не ассоциированные +/// константы) для единообразия. Ассоциированные константы (`const NETFN: u8`) — равноценная +/// альтернатива, если значение действительно фиксировано для типа. pub trait IpmiCmd { type Response; fn net_fn(&self) -> u8; @@ -46,13 +45,13 @@ pub trait IpmiCmd { pub struct ReadTemp { pub sensor_id: u8 } impl IpmiCmd for ReadTemp { - type Response = Celsius; // ← dimensional type! + type Response = Celsius; // ← размерный тип! fn net_fn(&self) -> u8 { 0x04 } fn cmd_byte(&self) -> u8 { 0x2D } fn payload(&self) -> Vec { vec![self.sensor_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "empty")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ")); } Ok(Celsius(raw[0] as f64)) } @@ -66,13 +65,13 @@ impl IpmiCmd for ReadFanSpeed { fn payload(&self) -> Vec { vec![self.fan_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 2 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "need 2 bytes")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "нужно 2 байта")); } Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as f64)) } } -// ──── Pattern 3: Capability Token (ch04) ──── +// ──── Паттерн 3: capability-токен (гл. 04) ──── pub struct AdminToken { _private: () } @@ -80,11 +79,11 @@ pub fn authenticate(user: &str, pass: &str) -> Result if user == "admin" && pass == "secret" { Ok(AdminToken { _private: () }) } else { - Err("authentication failed") + Err("аутентификация не пройдена") } } -// ──── Pattern 4: Type-State Session (ch05) ──── +// ──── Паттерн 4: сессия с typestate (гл. 05) ──── pub struct Idle; pub struct Active; @@ -101,32 +100,32 @@ impl Session { pub fn activate( self, - _admin: &AdminToken, // ← requires capability token + _admin: &AdminToken, // ← требует capability-токен ) -> Result, String> { - println!("Session activated on {}", self.host); + println!("Сессия активирована на {}", self.host); Ok(Session { host: self.host, _state: PhantomData }) } } impl Session { - /// Execute a typed command — only available on Active sessions. - /// Returns io::Result to propagate transport errors (consistent with ch02). + /// Выполняет типизированную команду — доступно только для активных сессий. + /// Возвращает io::Result, чтобы пробрасывать ошибки транспорта (согласованно с гл. 02). pub fn execute(&mut self, cmd: &C) -> io::Result { let raw_response = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?; cmd.parse_response(&raw_response) } fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result> { - Ok(vec![42, 0x1E]) // stub: raw IPMI response + Ok(vec![42, 0x1E]) // заглушка: сырой ответ IPMI } - pub fn close(self) { println!("Session closed"); } + pub fn close(self) { println!("Сессия закрыта"); } } -// ──── Pattern 5: Single-Use Audit Token (ch03) ──── +// ──── Паттерн 5: одноразовый токен аудита (гл. 03) ──── -/// Each diagnostic run gets a unique audit token. -/// Not Clone, not Copy — ensures each audit entry is unique. +/// Каждый запуск диагностики получает уникальный токен аудита. +/// Не Clone, не Copy: гарантирует, что каждая запись аудита уникальна. pub struct AuditToken { run_id: u64, } @@ -136,16 +135,16 @@ impl AuditToken { AuditToken { run_id } } - /// Consume the token to write an audit log entry. + /// Потребляет токен для записи в журнал аудита. pub fn log(self, message: &str) { - println!("[AUDIT run_id={}] {}", self.run_id, message); - // token is consumed — can't log the same run_id twice + println!("[АУДИТ run_id={}] {}", self.run_id, message); + // токен потреблён — нельзя записать один и тот же run_id дважды } } -// ──── Pattern 6: Validated Boundary (ch07) ──── -// Simplified from ch07's full ValidFru — only the fields needed for this -// composite example. See ch07 for the complete TryFrom version. +// ──── Паттерн 6: проверенная граница (гл. 07) ──── +// Упрощено по сравнению с полным ValidFru из гл. 07: только поля, нужные для этого +// составного примера. Полную версию TryFrom см. в гл. 07. pub struct ValidFru { pub board_serial: String, @@ -154,22 +153,22 @@ pub struct ValidFru { impl ValidFru { pub fn parse(raw: &[u8]) -> Result { - if raw.len() < 8 { return Err("FRU too short"); } - if raw[0] != 0x01 { return Err("bad FRU version"); } + if raw.len() < 8 { return Err("FRU слишком короткий"); } + if raw[0] != 0x01 { return Err("неверная версия FRU"); } Ok(ValidFru { - board_serial: "SN12345".to_string(), // stub + board_serial: "SN12345".to_string(), // заглушка product_name: "ServerX".to_string(), }) } } -// ──── Pattern 7: Phantom-Typed Registers (ch09) ──── +// ──── Паттерн 7: регистры с phantom-типом (гл. 09) ──── pub struct Width16; pub struct Reg { offset: u16, _w: PhantomData } impl Reg { - pub fn read(&self) -> u16 { 0x8086 } // stub + pub fn read(&self) -> u16 { 0x8086 } // заглушка } pub struct PcieDev { @@ -186,82 +185,80 @@ impl PcieDev { } } -// ──── Composite Workflow ──── +// ──── Составной сценарий ──── fn full_diagnostic() -> Result<(), String> { - // 1. Authenticate → get capability token + // 1. Аутентификация → получаем capability-токен let admin = authenticate("admin", "secret") .map_err(|e| e.to_string())?; - // 2. Connect and activate session (type-state: Idle → Active) + // 2. Подключение и активация сессии (typestate: Idle → Active) let session = Session::connect("192.168.1.100"); - let mut session = session.activate(&admin)?; // requires AdminToken + let mut session = session.activate(&admin)?; // требует AdminToken - // 3. Send typed commands (response type matches command) + // 3. Отправляем типизированные команды (тип ответа соответствует команде) let temp: Celsius = session.execute(&ReadTemp { sensor_id: 0 }) .map_err(|e| e.to_string())?; let fan: Rpm = session.execute(&ReadFanSpeed { fan_id: 1 }) .map_err(|e| e.to_string())?; - // Type mismatch would be caught: + // Несоответствие типов будет поймано: // let wrong: Volts = session.execute(&ReadTemp { sensor_id: 0 })?; - // ❌ ERROR: expected Celsius, found Volts + // ❌ ОШИБКА: expected Celsius, found Volts - // 4. Read phantom-typed PCIe registers + // 4. Читаем регистры PCIe с phantom-типом let pcie = PcieDev::new(); - let vid: u16 = pcie.vendor_id.read(); // guaranteed u16 + let vid: u16 = pcie.vendor_id.read(); // гарантированно u16 - // 5. Validate FRU data at the boundary + // 5. Проверяем данные FRU на границе let raw_fru = vec![0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0xFD]; let fru = ValidFru::parse(&raw_fru) .map_err(|e| e.to_string())?; - // 6. Issue single-use audit token + // 6. Выдаём одноразовый токен аудита let audit = AuditToken::issue(1001); - // 7. Generate report (all data is typed and validated) + // 7. Формируем отчёт (все данные типизированы и проверены) let report = format!( - "Server: {} (SN: {}), VID: 0x{:04X}, CPU: {:?}, Fan: {:?}", + "Сервер: {} (SN: {}), VID: 0x{:04X}, CPU: {:?}, Вентилятор: {:?}", fru.product_name, fru.board_serial, vid, temp, fan, ); - // 8. Consume audit token — can't log twice + // 8. Потребляем токен аудита — записать дважды нельзя audit.log(&report); - // audit.log("oops"); // ❌ use of moved value + // audit.log("повтор"); // ❌ use of moved value - // 9. Close session (type-state: Active → dropped) + // 9. Закрываем сессию (typestate: Active → уничтожена) session.close(); Ok(()) } ``` -### What the Compiler Proves +### Что доказывает компилятор -| Bug class | How it's prevented | Pattern | -|-----------|-------------------|---------| -| Unauthenticated access | `activate()` requires `&AdminToken` | Capability token | -| Command in wrong session state | `execute()` only exists on `Session` | Type-state | -| Wrong response type | `ReadTemp::Response = Celsius`, fixed by trait | Typed commands | -| Unit confusion (°C vs RPM) | `Celsius` ≠ `Rpm` ≠ `Volts` | Dimensional types | -| Register width mismatch | `Reg` returns `u16` | Phantom types | -| Processing unvalidated data | Must call `ValidFru::parse()` first | Validated boundary | -| Duplicate audit entries | `AuditToken` is consumed on log | Single-use type | -| Out-of-order power sequencing | Each step requires previous token | Capability tokens (ch04) | +| Класс ошибки | Как это предотвращается | Паттерн | +|--------------|-------------------------|---------| +| Доступ без аутентификации | `activate()` требует `&AdminToken` | Capability-токен | +| Команда в неверном состоянии сессии | `execute()` существует только у `Session` | Typestate | +| Неверный тип ответа | `ReadTemp::Response = Celsius`, фиксируется трейтом | Типизированные команды | +| Путаница единиц (°C и об/мин) | `Celsius` ≠ `Rpm` ≠ `Volts` | Размерные типы | +| Несоответствие ширины регистра | `Reg` возвращает `u16` | Phantom-типы | +| Обработка непроверенных данных | Сначала нужно вызвать `ValidFru::parse()` | Проверенная граница | +| Дублирование записей аудита | `AuditToken` потребляется при записи в журнал | Одноразовый тип | +| Нарушение порядка подачи питания | Каждый шаг требует токен предыдущего | Capability-токены (гл. 04) | -**Total runtime overhead of ALL these guarantees: zero.** +**Суммарные накладные расходы во время выполнения от ВСЕХ этих гарантий: ноль.** -Every check happens at compile time. The generated assembly is identical to -hand-written C code with no checks at all — but **C can have bugs, this can't**. +Каждая проверка происходит на этапе компиляции. Сгенерированный ассемблер идентичен рукописному коду на C без единой проверки, но **в C могут быть ошибки, а здесь — нет**. -## Key Takeaways +## Ключевые выводы -1. **Seven patterns compose seamlessly** — capability tokens, type-state, typed commands, single-use types, dimensional types, validated boundaries, and phantom types all work together. -2. **The compiler proves eight bug classes impossible** — see the "What the Compiler Proves" table above. -3. **Zero total runtime overhead** — the generated assembly is identical to unchecked C code. -4. **Each pattern is independently useful** — you don't need all seven; adopt them incrementally. -5. **The integration chapter is a design template** — use it as a starting point for your own typed diagnostic workflows. -6. **From IPMI to Redfish at scale** — ch17 and ch18 apply these same seven patterns (plus capability mixins from ch08) to a full Redfish client and server. The IPMI workflow here is the foundation; the Redfish walkthroughs show how the composition scales to production systems with multiple data sources and schema-version constraints. +1. **Семь паттернов сочетаются без швов**: capability-токены, typestate, типизированные команды, одноразовые типы, размерные типы, проверенные границы и phantom-типы работают вместе. +2. **Компилятор доказывает невозможность восьми классов ошибок**: см. таблицу «Что доказывает компилятор» выше. +3. **Нулевые суммарные накладные расходы во время выполнения**: сгенерированный ассемблер идентичен коду на C без проверок. +4. **Каждый паттерн полезен сам по себе**: не обязательно использовать все семь; внедряйте их постепенно. +5. **Глава с интеграцией — это шаблон проектирования**: используйте её как отправную точку для собственных типизированных диагностических сценариев. +6. **От IPMI к Redfish в масштабе**: гл. 17 и 18 применяют эти же семь паттернов (плюс capability-миксины из гл. 08) к полному клиенту и серверу Redfish. Сценарий IPMI здесь — основа; пошаговые разборы Redfish показывают, как композиция масштабируется до производственных систем с несколькими источниками данных и ограничениями версий схем. --- - diff --git a/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md b/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md index c7819a1..85fdf07 100644 --- a/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md +++ b/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md @@ -1,51 +1,39 @@ -# Fourteen Tricks from the Trenches 🟡 +# Четырнадцать проверенных на практике приёмов 🟡 -> **What you'll learn:** Fourteen smaller correct-by-construction techniques — from sentinel elimination and sealed traits to session types, `Pin`, RAII, and `#[must_use]` — each eliminating a specific bug class for near-zero effort. +> **Что вы узнаете:** четырнадцать более мелких техник корректности по построению, от устранения sentinel-значений и sealed-трейтов до сессионных типов, `Pin`, RAII и `#[must_use]`. Каждая из них устраняет конкретный класс ошибок почти без затрат. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (sealed traits extend ch02), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (typestate builder extends ch05), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (FromStr extends ch07) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (sealed-трейты расширяют гл. 02), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (билдер с typestate расширяет гл. 05), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (FromStr расширяет гл. 07) -## Fourteen Tricks from the Trenches +## Четырнадцать проверенных на практике приёмов -The eight core patterns (ch02–ch09) cover the major correct-by-construction -techniques. This chapter collects fourteen **smaller but high-value tricks** that -show up repeatedly in production Rust code — each one eliminates a specific -class of bug for zero or near-zero effort. +Восемь основных паттернов (гл. 02–09) покрывают основные техники корректности по построению. Эта глава собирает четырнадцать **более мелких, но ценных приёмов**, которые регулярно встречаются в продакшн-коде на Rust. Каждый из них устраняет конкретный класс ошибок почти без затрат. -### Trick 1 — Sentinel → `Option` at the Boundary +### Приём 1 — Sentinel → `Option` на границе -Hardware protocols are full of sentinel values: IPMI uses `0xFF` for -"sensor not present," PCI uses `0xFFFF` for "no device," and SMBIOS uses -`0x00` for "unknown." If you carry these sentinels through your code as -plain integers, every consumer must remember to check for the magic value. -If even one comparison forgets, you get a phantom 255 °C reading or a -spurious vendor-ID match. +Аппаратные протоколы полны sentinel-значений: IPMI использует `0xFF` для «датчик отсутствует», PCI использует `0xFFFF` для «нет устройства», а SMBIOS использует `0x00` для «неизвестно». Если протаскивать такие значения через код как обычные целые числа, каждый потребитель должен помнить о проверке магического значения. Если хотя бы одно сравнение забыто, получаем призрачное показание 255 °C или ложное совпадение ID производителя. -**The rule:** Convert sentinels to `Option` at the very first parse boundary, -and convert *back* to the sentinel only at the serialization boundary. +**Правило:** преобразуйте sentinel-значения в `Option` на самой первой границе разбора и преобразуйте их *обратно* в sentinel только на границе сериализации. -#### The anti-pattern (from `pcie_tree/src/lspci.rs`) +#### Антипаттерн (из `pcie_tree/src/lspci.rs`) ```rust,ignore -// Sentinel carried internally — every comparison must remember +// Sentinel хранится внутри — каждое сравнение должно помнить о нём let mut current_vendor_id: u16 = 0xFFFF; let mut current_device_id: u16 = 0xFFFF; -// ... later, parsing fails silently ... +// ... позже разбор молча завершается ошибкой ... current_vendor_id = u16::from_str_radix(hex, 16) - .unwrap_or(0xFFFF); // sentinel hides the error + .unwrap_or(0xFFFF); // sentinel скрывает ошибку ``` -Every function that receives `current_vendor_id` must know that `0xFFFF` is -special. If someone writes `if vendor_id == target_id` without checking -for `0xFFFF` first, a missing device silently matches when the target also -happens to be parsed from bad input as `0xFFFF`. +Каждая функция, которая получает `current_vendor_id`, должна знать, что `0xFFFF` — особое значение. Если кто-то напишет `if vendor_id == target_id` и не проверит сначала `0xFFFF`, отсутствующее устройство тихо совпадёт, если и цель случайно разобрана из некорректных входных данных как `0xFFFF`. -#### The correct pattern (from `nic_sel/src/events.rs`) +#### Правильный паттерн (из `nic_sel/src/events.rs`) ```rust,ignore pub struct ThermalEvent { pub record_id: u16, - pub temperature: Option, // None if sensor reports 0xFF + pub temperature: Option, // None, если датчик сообщает 0xFF } impl ThermalEvent { @@ -62,23 +50,23 @@ impl ThermalEvent { } ``` -Now every consumer *must* handle the `None` case — the compiler forces it: +Теперь каждый потребитель *обязан* обработать случай `None`, и компилятор это обеспечивает: ```rust,ignore -// Safe — compiler ensures we handle missing temps +// Безопасно: компилятор гарантирует обработку отсутствующих температур fn is_overtemp(temp: Option, threshold: u8) -> bool { temp.map_or(false, |t| t > threshold) } -// Forgetting to handle None is a compile error: +// Забыть обработать None — ошибка компиляции: // fn bad_check(temp: Option, threshold: u8) -> bool { -// temp > threshold // ERROR: can't compare Option with u8 +// temp > threshold // ОШИБКА: нельзя сравнить Option с u8 // } ``` -#### Real-world impact +#### Практический эффект -`inventory/src/events.rs` uses the same pattern for GPU thermal alerts: +`inventory/src/events.rs` использует тот же паттерн для тревог о температуре GPU: ```rust,ignore temperature: if data[1] != 0xFF { Some(data[1] as i8) @@ -87,38 +75,30 @@ temperature: if data[1] != 0xFF { }, ``` -The refactoring for `pcie_tree/src/lspci.rs` is straightforward: change -`current_vendor_id: u16` to `current_vendor_id: Option`, replace -`0xFFFF` with `None`, and let the compiler find every site that needs -updating. +Рефакторинг для `pcie_tree/src/lspci.rs` несложен: замените `current_vendor_id: u16` на `current_vendor_id: Option`, замените `0xFFFF` на `None` и позвольте компилятору найти каждое место, которое нужно обновить. -| Before | After | -|--------|-------| +| До | После | +|----|-------| | `let mut vendor_id: u16 = 0xFFFF` | `let mut vendor_id: Option = None` | -| `.unwrap_or(0xFFFF)` | `.ok()` (already returns `Option`) | +| `.unwrap_or(0xFFFF)` | `.ok()` (уже возвращает `Option`) | | `if vendor_id != 0xFFFF { ... }` | `if let Some(vid) = vendor_id { ... }` | -| Serialization: `vendor_id` | `vendor_id.unwrap_or(0xFFFF)` | +| Сериализация: `vendor_id` | `vendor_id.unwrap_or(0xFFFF)` | *** -### Trick 2 — Sealed Traits +### Приём 2 — Sealed-трейты -Chapter 2 introduced `IpmiCmd` with an associated type that binds each command -to its response. But there's a loophole: if *any* code can implement `IpmiCmd`, -someone could write a `MaliciousCmd` whose `parse_response` returns the wrong -type or panics. The type safety of the entire system rests on every -implementation being correct. +Глава 2 ввела `IpmiCmd` с ассоциированным типом, который связывает каждую команду с её ответом. Но есть лазейка: если *любой* код может реализовать `IpmiCmd`, кто-то может написать `MaliciousCmd`, у которого `parse_response` возвращает неверный тип или паникует. Безопасность типов всей системы держится на том, что каждая реализация корректна. -A **sealed trait** closes this loophole. The idea is simple: make the trait -require a *private* supertrait that only your crate can implement. +**Sealed-трейт** закрывает эту лазейку. Идея проста: сделать так, чтобы трейт требовал *приватный* супертрейт, который может реализовать только ваш крейт. ```rust,ignore -// — Private module: not exported from the crate — +// — Приватный модуль: не экспортируется из крейта — mod private { pub trait Sealed {} } -// — Public trait: requires Sealed, which outsiders can't implement — +// — Публичный трейт: требует Sealed, который внешний код не может реализовать — pub trait IpmiCmd: private::Sealed { type Response; fn net_fn(&self) -> u8; @@ -128,7 +108,7 @@ pub trait IpmiCmd: private::Sealed { } ``` -Inside your crate, you implement `Sealed` for each approved command type: +Внутри крейта вы реализуете `Sealed` для каждого одобренного типа команды: ```rust,ignore pub struct ReadTemp { pub sensor_id: u8 } @@ -140,40 +120,40 @@ impl IpmiCmd for ReadTemp { fn cmd_byte(&self) -> u8 { 0x2D } fn payload(&self) -> Vec { vec![self.sensor_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { - if raw.is_empty() { return Err(io::Error::new(io::ErrorKind::InvalidData, "empty")); } + if raw.is_empty() { return Err(io::Error::new(io::ErrorKind::InvalidData, "пустой ответ")); } Ok(Celsius(raw[0] as f64)) } } ``` -External code sees `IpmiCmd` and can call `execute()`, but cannot implement it: +Внешний код видит `IpmiCmd` и может вызывать `execute()`, но не может его реализовать: ```rust,ignore -// In another crate: +// В другом крейте: struct EvilCmd; -// impl private::Sealed for EvilCmd {} // ERROR: module `private` is private -// impl IpmiCmd for EvilCmd { ... } // ERROR: `Sealed` is not satisfied +// impl private::Sealed for EvilCmd {} // ОШИБКА: модуль `private` приватный +// impl IpmiCmd for EvilCmd { ... } // ОШИБКА: требование `Sealed` не выполнено ``` -#### When to seal +#### Когда применять sealing -| Seal when… | Don't seal when… | -|-----------|-----------------| -| Safety depends on correct implementation (IpmiCmd, DiagModule) | Users should extend the system (custom report formatters) | -| Associated types must satisfy invariants | The trait is a simple capability marker (HasIpmi) | -| You own the canonical set of implementations | Third-party plugins are a design goal | +| Применяйте sealing, когда… | Не применяйте, когда… | +|----------------------------|----------------------| +| Безопасность зависит от корректности реализации (IpmiCmd, DiagModule) | Пользователи должны расширять систему (свои форматировщики отчётов) | +| Ассоциированные типы должны соблюдать инварианты | Трейт — простой маркер возможности (HasIpmi) | +| У вас есть эталонный набор реализаций | Сторонние плагины — цель дизайна | -#### Real-world candidates +#### Кандидаты в реальном коде -- `IpmiCmd` — incorrect parse could corrupt typed responses -- `DiagModule` — framework assumes `run()` returns valid DER records -- `SelEventFilter` — broken filter could swallow critical SEL events +- `IpmiCmd`: некорректный разбор может повредить типизированные ответы +- `DiagModule`: фреймворк предполагает, что `run()` возвращает корректные записи DER +- `SelEventFilter`: сломанный фильтр может поглотить критические события SEL *** -### Trick 3 — `#[non_exhaustive]` for Evolving Enums +### Приём 3 — `#[non_exhaustive]` для развивающихся enum -`SkuVariant` in `inventory/src/types.rs` today has five variants: +`SkuVariant` в `inventory/src/types.rs` сейчас имеет пять вариантов: ```rust,ignore pub enum SkuVariant { @@ -181,23 +161,14 @@ pub enum SkuVariant { } ``` -When the next generation ships and you add `S4001`, any external code that -matches on `SkuVariant` and doesn't have a wildcard arm will **silently fail -to compile** — which is the whole point. But what about internal code? Without -`#[non_exhaustive]`, your `match` in the *same crate* compiles without a -wildcard, and adding the new variant breaks your own build. +Когда выйдет следующее поколение и вы добавите `S4001`, любой внешний код, который сопоставляет с `SkuVariant` без ветки-заглушки `_`, **тихо перестанет компилироваться**. В этом и смысл. Но что с внутренним кодом? Без `#[non_exhaustive]` ваш `match` в *том же* крейте компилируется без `_`, и добавление нового варианта ломает вашу собственную сборку. -Marking the enum `#[non_exhaustive]` forces **external crates** that match on -it to include a wildcard arm. Within the defining crate, `#[non_exhaustive]` -has no effect — you can still write exhaustive matches. +Пометка enum как `#[non_exhaustive]` заставляет **внешние крейты**, которые сопоставляют с ним, включать ветку-заглушку. Внутри определяющего крейта `#[non_exhaustive]` не действует: исчерпывающие сопоставления по-прежнему возможны. -**Why this is useful:** When you publish `SkuVariant` from a library crate -(or a shared sub-crate in a workspace), downstream code is forced to handle -unknown future variants. When you add `S4001` next generation, downstream -code already compiles — they have a wildcard arm. +**Почему это полезно:** когда вы публикуете `SkuVariant` из библиотечного крейта (или общего подкрейта в рабочей области), нижележащий код вынужден обрабатывать неизвестные будущие варианты. Когда вы добавите `S4001` в следующем поколении, нижележащий код уже компилируется: у него есть ветка `_`. ```rust,ignore -// In gpu_sel crate (the defining crate): +// В крейте gpu_sel (определяющем крейте): #[non_exhaustive] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SkuVariant { @@ -206,11 +177,11 @@ pub enum SkuVariant { S2002, S2003, S3001, - // When the next SKU ships, add it here. - // External consumers already have a wildcard — zero breakage for them. + // Когда выйдет следующий SKU, добавьте его сюда. + // У внешних потребителей уже есть подстановка: для них ноль поломок. } -// Within gpu_sel itself — exhaustive match is allowed (no wildcard needed): +// Внутри gpu_sel — исчерпывающее сопоставление разрешено (подстановка не нужна): fn diag_path_internal(sku: SkuVariant) -> &'static str { match sku { SkuVariant::S1001 => "legacy_gen1", @@ -218,15 +189,15 @@ fn diag_path_internal(sku: SkuVariant) -> &'static str { SkuVariant::S2002 => "gen2_alt_diag", SkuVariant::S2003 => "gen2_alt_hf_diag", SkuVariant::S3001 => "gen3_accel_diag", - // No wildcard needed inside the defining crate. - // Adding S4001 here will cause a compile error at this match, - // which is exactly what you want — it forces you to update it. + // Подстановка внутри определяющего крейта не нужна. + // Добавление S4001 сюда вызовет ошибку компиляции в этом match, + // и это именно то, что нужно: это заставит обновить код. } } ``` ```rust,ignore -// In the binary crate (a downstream crate that depends on inventory): +// В бинарном крейте (нижележащем крейте, который зависит от inventory): fn diag_path_external(sku: inventory::SkuVariant) -> &'static str { match sku { inventory::SkuVariant::S1001 => "legacy_gen1", @@ -234,39 +205,34 @@ fn diag_path_external(sku: inventory::SkuVariant) -> &'static str { inventory::SkuVariant::S2002 => "gen2_alt_diag", inventory::SkuVariant::S2003 => "gen2_alt_hf_diag", inventory::SkuVariant::S3001 => "gen3_accel_diag", - _ => "generic_diag", // REQUIRED by #[non_exhaustive] for external crates + _ => "generic_diag", // ОБЯЗАТЕЛЬНО для внешних крейтов из-за #[non_exhaustive] } } ``` -> **Workspace tip:** If all your code is in a single crate, `#[non_exhaustive]` -> won't help — it only affects cross-crate boundaries. For the project's -> large workspace, place evolving enums in a shared crate (`core_lib` or -> `inventory`) so the attribute protects consumers in other workspace crates. +> **Совет по рабочей области:** если весь код находится в одном крейте, `#[non_exhaustive]` не поможет: он действует только на границах между крейтами. Для большой рабочей области проекта размещайте развивающиеся enum в общем крейте (`core_lib` или `inventory`), чтобы атрибут защищал потребителей в других крейтах рабочей области. -#### Candidates +#### Кандидаты -| Enum | Module | Why | -|------|--------|-----| -| `SkuVariant` | `inventory`, `net_inventory` | New SKUs every generation | -| `SensorType` | `protocol_lib` | IPMI spec reserves 0xC0–0xFF for OEM | -| `CompletionCode` | `protocol_lib` | Custom BMC vendors add codes | -| `Component` | `event_handler` | New hardware categories (NewSoC was recently added) | +| Enum | Модуль | Почему | +|------|--------|--------| +| `SkuVariant` | `inventory`, `net_inventory` | Новые SKU в каждом поколении | +| `SensorType` | `protocol_lib` | Спецификация IPMI резервирует 0xC0–0xFF для OEM | +| `CompletionCode` | `protocol_lib` | Производители BMC добавляют собственные коды | +| `Component` | `event_handler` | Новые категории оборудования (NewSoC был добавлен недавно) | *** -### Trick 4 — Typestate Builder +### Приём 4 — Билдер с typestate -Chapter 5 showed type-state for *protocols* (session lifecycles, link training). -The same idea applies to *builders* — structs whose `build()` / `finish()` -can only be called when all required fields have been set. +Глава 5 показала typestate для *протоколов* (жизненные циклы сессий, обучение линка). Та же идея применима к *билдерам*: структурам, у которых `build()` или `finish()` можно вызвать только тогда, когда установлены все обязательные поля. -#### The problem with fluent builders +#### Проблема текучих (fluent) билдеров -`DerBuilder` in `diag_framework/src/der.rs` today looks like this (simplified): +`DerBuilder` в `diag_framework/src/der.rs` сейчас выглядит так (упрощённо): ```rust,ignore -// Current fluent builder — finish() always available +// Текущий текучий билдер: finish() доступен всегда pub struct DerBuilder { der: Der, } @@ -275,18 +241,18 @@ impl DerBuilder { pub fn new(marker: &str, fault_code: u32) -> Self { ... } pub fn mnemonic(mut self, m: &str) -> Self { ... } pub fn fault_class(mut self, fc: &str) -> Self { ... } - pub fn finish(self) -> Der { self.der } // ← always callable! + pub fn finish(self) -> Der { self.der } // ← доступен всегда! } ``` -This compiles without error, but produces an incomplete DER record: +Это компилируется без ошибок, но создаёт неполную запись DER: ```rust,ignore let bad = DerBuilder::new("CSI_ERR", 62691) - .finish(); // oops — no mnemonic, no fault_class + .finish(); // упс: нет mnemonic и fault_class ``` -#### Typestate builder: `finish()` requires both fields +#### Билдер с typestate: `finish()` требует оба поля ```rust,ignore pub struct Missing; @@ -300,7 +266,7 @@ pub struct DerBuilder { description: Option, } -// Constructor: starts with both required fields Missing +// Конструктор: начинаем с обоими обязательными полями в состоянии Missing impl DerBuilder { pub fn new(marker: &str, fault_code: u32) -> Self { DerBuilder { @@ -313,7 +279,7 @@ impl DerBuilder { } } -// Set mnemonic (works regardless of fault_class's state) +// Устанавливаем mnemonic (работает независимо от состояния fault_class) impl DerBuilder { pub fn mnemonic(self, m: &str) -> DerBuilder, FC> { DerBuilder { @@ -325,7 +291,7 @@ impl DerBuilder { } } -// Set fault_class (works regardless of mnemonic's state) +// Устанавливаем fault_class (работает независимо от состояния mnemonic) impl DerBuilder { pub fn fault_class(self, fc: &str) -> DerBuilder> { DerBuilder { @@ -337,7 +303,7 @@ impl DerBuilder { } } -// Optional fields — available in ANY state +// Необязательные поля доступны в ЛЮБОМ состоянии impl DerBuilder { pub fn description(mut self, desc: &str) -> Self { self.description = Some(desc.to_string()); @@ -345,7 +311,7 @@ impl DerBuilder { } } -/// The fully-built DER record. +/// Полностью собранная запись DER. pub struct Der { pub marker: String, pub fault_code: u32, @@ -354,7 +320,7 @@ pub struct Der { pub description: Option, } -// finish() ONLY available when both required fields are Set +// finish() доступен ТОЛЬКО тогда, когда оба обязательных поля в состоянии Set impl DerBuilder, Set> { pub fn finish(self) -> Der { Der { @@ -368,52 +334,50 @@ impl DerBuilder, Set> { } ``` -Now the buggy call is a compile error: +Теперь ошибочный вызов становится ошибкой компиляции: ```rust,ignore -// ✅ Compiles — both required fields set (in any order) +// ✅ Компилируется: оба обязательных поля установлены (порядок не важен) let der = DerBuilder::new("CSI_ERR", 62691) - .fault_class("GPU Module") // order doesn't matter + .fault_class("GPU Module") // порядок не важен .mnemonic("ACCEL_CARD_ER691") .description("Thermal throttle") .finish(); -// ❌ Compile error — finish() doesn't exist on DerBuilder, Missing> +// ❌ Ошибка компиляции: finish() не существует у DerBuilder, Missing> let bad = DerBuilder::new("CSI_ERR", 62691) .mnemonic("ACCEL_CARD_ER691") - .finish(); // ERROR: method `finish` not found + .finish(); // ОШИБКА: метод `finish` не найден ``` -#### When to use typestate builders +#### Когда использовать билдеры с typestate -| Use when… | Don't bother when… | -|-----------|-------------------| -| Omitting a field causes silent bugs (DER missing mnemonic) | All fields have sensible defaults | -| The builder is part of a public API | The builder is test-only scaffolding | -| More than 2–3 required fields | Single required field (just take it in `new()`) | +| Используйте, когда… | Не стоит, когда… | +|---------------------|------------------| +| Пропуск поля приводит к тихим ошибкам (в DER пропущен mnemonic) | Все поля имеют разумные значения по умолчанию | +| Билдер — часть публичного API | Билдер — только тестовая обвязка | +| Более 2–3 обязательных полей | Одно обязательное поле (просто передайте его в `new()`) | *** -### Trick 5 — `FromStr` as a Validation Boundary +### Приём 5 — `FromStr` как граница валидации -Chapter 7 showed `TryFrom<&[u8]>` for binary data (FRU records, SEL entries). -For **string** inputs — config files, CLI arguments, JSON fields — the -analogous boundary is `FromStr`. +Глава 7 показала `TryFrom<&[u8]>` для бинарных данных (записи FRU, записи SEL). Для **строковых** входных данных (файлов конфигурации, аргументов CLI, полей JSON) аналогичной границей служит `FromStr`. -#### The problem +#### Проблема ```rust,ignore -// C++ / unvalidated Rust: silently falls through to a default +// C++ / Rust без валидации: тихо падает в значение по умолчанию fn route_diag(level: &str) -> DiagMode { if level == "quick" { ... } else if level == "standard" { ... } - else { QuickMode } // typo in config? ¯\_(ツ)_/¯ + else { QuickMode } // опечатка в конфиге? ¯\_(ツ)_/¯ } ``` -A config file with `"diag_level": "extendedd"` (typo) silently gets `QuickMode`. +Файл конфигурации с `"diag_level": "extendedd"` (опечатка) тихо получит `QuickMode`. -#### The pattern (from `config_loader/src/diag.rs`) +#### Паттерн (из `config_loader/src/diag.rs`) ```rust,ignore use std::str::FromStr; @@ -434,82 +398,78 @@ impl FromStr for DiagLevel { "standard" | "2" => Ok(DiagLevel::Standard), "extended" | "3" => Ok(DiagLevel::Extended), "stress" | "4" => Ok(DiagLevel::Stress), - other => Err(format!("unknown diag level: '{other}'")), + other => Err(format!("неизвестный уровень диагностики: '{other}'")), } } } ``` -Now a typo is caught immediately: +Теперь опечатка ловится сразу: ```rust,ignore let level: DiagLevel = "extendedd".parse()?; -// Err("unknown diag level: 'extendedd'") +// Err("неизвестный уровень диагностики: 'extendedd'") ``` -#### The three benefits +#### Три преимущества -1. **Fail-fast:** Bad input is caught at the parsing boundary, not three - layers deep in diagnostic logic. -2. **Aliases are explicit:** `"MEM"`, `"DIMM"`, and `"MEMORY"` all map to - `Component::Memory` — the match arms document the mapping. -3. **`.parse()` is ergonomic:** Because `FromStr` integrates with `str::parse()`, - you get clean one-liners: `let level: DiagLevel = config["level"].parse()?;` +1. **Быстрый отказ:** некорректные входные данные ловятся на границе разбора, а не на три уровня глубже в логике диагностики. +2. **Синонимы явные:** `"MEM"`, `"DIMM"` и `"MEMORY"` все отображаются в `Component::Memory`: ветки `match` документируют соответствие. +3. **`.parse()` удобен:** поскольку `FromStr` интегрируется с `str::parse()`, получаются лаконичные однострочники: `let level: DiagLevel = config["level"].parse()?;` -#### Real codebase usage +#### Использование в реальном коде -The project already has 8 `FromStr` implementations: +В проекте уже есть 8 реализаций `FromStr`: -| Type | Module | Notable aliases | -|------|--------|----------------| +| Тип | Модуль | Заметные синонимы | +|-----|--------|-------------------| | `DiagLevel` | `config_loader` | `"1"` = Quick, `"4"` = Stress | | `Component` | `event_handler` | `"MEM"` / `"DIMM"` = Memory, `"SSD"` / `"NVME"` = Disk | | `SkuVariant` | `net_inventory` | `"Accel-X1"` = S2001, `"Accel-M1"` = S2002, `"Accel-Z1"` = S3001 | -| `SkuVariant` | `inventory` | Same aliases (separate module, same pattern) | -| `FaultStatus` | `config_loader` | Fault lifecycle states | -| `DiagAction` | `config_loader` | Remediation action types | -| `ActionType` | `config_loader` | Action categories | -| `DiagMode` | `cluster_diag` | Multi-node test modes | +| `SkuVariant` | `inventory` | Те же синонимы (отдельный модуль, тот же паттерн) | +| `FaultStatus` | `config_loader` | Состояния жизненного цикла неисправности | +| `DiagAction` | `config_loader` | Типы действий по устранению | +| `ActionType` | `config_loader` | Категории действий | +| `DiagMode` | `cluster_diag` | Режимы тестирования нескольких узлов | -The contrast with `TryFrom`: +Сравнение с `TryFrom`: | | `TryFrom<&[u8]>` | `FromStr` | |---|---|---| -| Input | Raw bytes (binary protocols) | Strings (configs, CLI, JSON) | -| Typical source | IPMI, PCIe config space, FRU | JSON fields, env vars, user input | -| Chapter | ch07 | ch11 | -| Both use | `Result` — forcing the caller to handle invalid input | +| Вход | Сырые байты (бинарные протоколы) | Строки (конфиги, CLI, JSON) | +| Типичный источник | IPMI, пространство конфигурации PCIe, FRU | Поля JSON, переменные окружения, ввод пользователя | +| Глава | гл. 07 | гл. 11 | +| Оба используют | `Result`: заставляет вызывающего обрабатывать некорректный ввод | *** -### Trick 6 — Const Generics for Compile-Time Size Validation +### Приём 6 — const-обобщения для проверки размера на этапе компиляции -When hardware buffers, register banks, or protocol frames have fixed sizes, -const generics let the compiler enforce them: +Когда аппаратные буферы, банки регистров или кадры протокола имеют фиксированный размер, const-обобщения позволяют компилятору его проверить: ```rust,ignore -/// A fixed-size register bank. The size is part of the type. -/// `RegisterBank<256>` and `RegisterBank<4096>` are different types. +/// Банк регистров фиксированного размера. Размер — часть типа. +/// `RegisterBank<256>` и `RegisterBank<4096>` — разные типы. pub struct RegisterBank { data: [u8; N], } impl RegisterBank { - /// Read a register at the given offset. - /// Compile-time: N is known, so the array size is fixed. - /// Runtime: only the offset is checked. + /// Читает регистр по заданному смещению. + /// Время компиляции: N известно, поэтому размер массива фиксирован. + /// Время выполнения: проверяется только смещение. pub fn read(&self, offset: usize) -> Option { self.data.get(offset).copied() } } -// PCIe conventional config space: 256 bytes +// Конфигурационное пространство PCIe (обычное): 256 байт type PciConfigSpace = RegisterBank<256>; -// PCIe extended config space: 4096 bytes +// Расширенное конфигурационное пространство PCIe: 4096 байт type PcieExtConfigSpace = RegisterBank<4096>; -// These are different types — can't accidentally pass one for the other: +// Это разные типы: нельзя случайно передать один вместо другого: fn read_extended_cap(config: &PcieExtConfigSpace, offset: usize) -> Option { config.read(offset) } @@ -517,41 +477,36 @@ fn read_extended_cap(config: &PcieExtConfigSpace, offset: usize) -> Option { // ^^^^^^^^^^^ expected RegisterBank<4096>, found RegisterBank<256> ❌ ``` -**Compile-time assertions with const generics:** +**Утверждения на этапе компиляции с const-обобщениями:** ```rust,ignore -/// NVMe admin commands use 4096-byte buffers. Enforce at compile time. +/// Административные команды NVMe используют буферы по 4096 байт. Проверяем на этапе компиляции. pub struct NvmeBuffer { data: Box<[u8; N]>, } impl NvmeBuffer { pub fn new() -> Self { - // Runtime assertion: only 512 or 4096 allowed - assert!(N == 4096 || N == 512, "NVMe buffers must be 512 or 4096 bytes"); + // Проверка во время выполнения: разрешены только 512 или 4096 + assert!(N == 4096 || N == 512, "Буферы NVMe должны быть 512 или 4096 байт"); NvmeBuffer { data: Box::new([0u8; N]) } } } -// NvmeBuffer::<1024>::new(); // panics at runtime with this form -// For true compile-time enforcement, see Trick 9 (const assertions). +// NvmeBuffer::<1024>::new(); // в такой форме паникует во время выполнения +// Для настоящей проверки на этапе компиляции см. приём 9 (const-утверждения). ``` -> **When to use:** Fixed-size protocol buffers (NVMe, PCIe config space), -> DMA descriptors, hardware FIFO depths. Anywhere the size is a hardware -> constant that should never vary at runtime. +> **Когда использовать:** фиксированные протокольные буферы (NVMe, конфигурационное пространство PCIe), дескрипторы DMA, глубины аппаратных FIFO. Везде, где размер — аппаратная константа, которая никогда не должна меняться во время выполнения. *** -### Trick 7 — Safe Wrappers Around `unsafe` +### Приём 7 — Безопасные обёртки вокруг `unsafe` -The project currently has zero `unsafe` blocks. But when you -add MMIO register access, DMA, or FFI to accel-mgmt/accel-query, you'll need -`unsafe`. The correct-by-construction approach: **wrap every `unsafe` block -in a safe abstraction** so the unsafety is contained and auditable. +В проекте сейчас нет ни одного блока `unsafe`. Но когда вы добавите доступ к регистрам MMIO, DMA или FFI в accel-mgmt или accel-query, `unsafe` понадобится. Корректный по построению подход: **обернуть каждый блок `unsafe` в безопасную абстракцию**, чтобы небезопасность была локализована и поддавалась аудиту. ```rust,ignore -/// MMIO-mapped register. The pointer is valid for the lifetime of the mapping. -/// All unsafe is contained in this module — callers use safe methods. +/// Регистр, отображённый в MMIO. Указатель действителен, пока существует отображение. +/// Весь unsafe заключён в этом модуле: вызывающий код использует безопасные методы. pub struct MmioRegion { base: *mut u8, len: usize, @@ -559,26 +514,26 @@ pub struct MmioRegion { impl MmioRegion { /// # Safety - /// - `base` must be a valid pointer to an MMIO-mapped region - /// - The region must remain mapped for the lifetime of this struct - /// - No other code may alias this region + /// - `base` должен быть корректным указателем на область, отображённую в MMIO + /// - Область должна оставаться отображённой, пока существует эта структура + /// - Никакой другой код не должен алиасить эту область pub unsafe fn new(base: *mut u8, len: usize) -> Self { MmioRegion { base, len } } - /// Safe read — bounds checking prevents out-of-bounds MMIO access. + /// Безопасное чтение: проверка границ предотвращает выход за пределы MMIO. pub fn read_u32(&self, offset: usize) -> Option { if offset + 4 > self.len { return None; } - // SAFETY: offset is bounds-checked above, base is valid per new() contract + // SAFETY: offset проверен выше, base корректен согласно контракту new() Some(unsafe { core::ptr::read_volatile(self.base.add(offset) as *const u32) }) } - /// Safe write — bounds checking prevents out-of-bounds MMIO access. + /// Безопасная запись: проверка границ предотвращает выход за пределы MMIO. pub fn write_u32(&self, offset: usize, value: u32) -> bool { if offset + 4 > self.len { return false; } - // SAFETY: offset is bounds-checked above, base is valid per new() contract + // SAFETY: offset проверен выше, base корректен согласно контракту new() unsafe { core::ptr::write_volatile(self.base.add(offset) as *mut u32, value); } @@ -587,7 +542,7 @@ impl MmioRegion { } ``` -**Combine with phantom types (ch09) for typed MMIO:** +**Комбинируем с phantom-типами (гл. 09) для типизированного MMIO:** ```rust,ignore use std::marker::PhantomData; @@ -604,7 +559,7 @@ impl TypedMmio { pub fn read_u32(&self, offset: usize) -> Option { self.region.read_u32(offset) } - // No write method — compile error if you try to write to a ReadOnly region + // Нет метода записи: ошибка компиляции при попытке записи в область ReadOnly } impl TypedMmio { @@ -617,42 +572,39 @@ impl TypedMmio { } ``` -> **Guidelines for `unsafe` wrappers:** +> **Правила для обёрток над `unsafe`:** > -> | Rule | Why | -> |------|-----| -> | One `unsafe fn new()` with documented `# Safety` invariants | Caller takes responsibility once | -> | All other methods are safe | Callers can't trigger UB | -> | `# SAFETY:` comment on every `unsafe` block | Auditors can verify locally | -> | Wrap in a module with `#[deny(unsafe_op_in_unsafe_fn)]` | Even inside `unsafe fn`, individual ops need `unsafe` | -> | Run `cargo +nightly miri test` on the wrapper | Verify memory model compliance | +> | Правило | Зачем | +> |---------|-------| +> | Один `unsafe fn new()` с задокументированными инвариантами `# Safety` | Вызывающий берёт ответственность один раз | +> | Все остальные методы безопасны | Вызывающие не могут вызвать UB | +> | Комментарий `# SAFETY:` у каждого блока `unsafe` | Аудиторы могут проверить локально | +> | Обернуть в модуль с `#[deny(unsafe_op_in_unsafe_fn)]` | Даже внутри `unsafe fn` отдельные операции требуют `unsafe` | +> | Запустить `cargo +nightly miri test` на обёртке | Проверка соответствия модели памяти | --- -### ✅ Checkpoint: Tricks 1–7 +### ✅ Контрольная точка: приёмы 1–7 -You now have seven everyday tricks. Here's a quick scorecard: +Теперь у вас есть семь повседневных приёмов. Вот краткая сводка: -| Trick | Bug class eliminated | Effort to adopt | -|:-----:|----------------------|:---------------:| -| 1 | Sentinel confusion (0xFF) | Low — one `match` at the boundary | -| 2 | Unauthorized trait impls | Low — add `Sealed` supertrait | -| 3 | Broken consumers after enum growth | Low — one-line attribute | -| 4 | Missing builder fields | Medium — extra type parameters | -| 5 | Typos in string-typed config | Low — `impl FromStr` | -| 6 | Wrong buffer sizes | Low — const generic parameter | -| 7 | Unsafe scattered across codebase | Medium — wrapper module | +| Приём | Устраняемый класс ошибок | Усилия на внедрение | +|:-----:|--------------------------|:-------------------:| +| 1 | Путаница sentinel-значений (0xFF) | Низкие: один `match` на границе | +| 2 | Несанкционированные реализации трейтов | Низкие: добавить супертрейт `Sealed` | +| 3 | Поломка потребителей при росте enum | Низкие: одна строка-атрибут | +| 4 | Пропущенные поля билдера | Средние: дополнительные параметры типа | +| 5 | Опечатки в строковых конфигурациях | Низкие: `impl FromStr` | +| 6 | Неверные размеры буферов | Низкие: параметр const-обобщения | +| 7 | Небезопасный код, разбросанный по кодовой базе | Средние: модуль-обёртка | -Tricks 8–14 are **more advanced** — they touch async, const evaluation, session -types, `Pin`, and `Drop`. Take a break here if you need one; the techniques -above are already high-value, low-effort wins you can adopt tomorrow. +Приёмы 8–14 — **более продвинутые**: они затрагивают async, вычисления на этапе компиляции, сессионные типы, `Pin` и `Drop`. Сделайте паузу, если нужно: приёмы выше уже дают высокую отдачу при низких затратах, и их можно внедрить хоть завтра. *** -### Trick 8 — Async Type-State Machines +### Приём 8 — Автоматы состояний в async-коде -When hardware drivers use `async` (e.g., async BMC communication, async NVMe -I/O), type-state still works — but ownership across `.await` points needs care: +Когда драйверы аппаратуры используют `async` (например, асинхронная связь с BMC или асинхронный ввод-вывод NVMe), typestate по-прежнему работает, но владение через точки `.await` требует внимания: ```rust,ignore use std::marker::PhantomData; @@ -671,21 +623,21 @@ impl AsyncSession { AsyncSession { host: host.to_string(), _state: PhantomData } } - /// Transition Idle → Authenticating → Active. - /// The Session is consumed (moved into the future) across the .await. + /// Переход Idle → Authenticating → Active. + /// Сессия потребляется (перемещается в future) через .await. pub async fn authenticate(self, user: &str, pass: &str) -> Result, String> { - // Phase 1: send credentials (consumes Idle session) + // Фаза 1: отправляем учётные данные (потребляет сессию Idle) let pending: AsyncSession = AsyncSession { host: self.host, _state: PhantomData, }; - // Simulate async BMC authentication + // Имитация асинхронной аутентификации в BMC // tokio::time::sleep(Duration::from_secs(1)).await; - // Phase 2: return Active session + // Фаза 2: возвращаем активную сессию Ok(AsyncSession { host: pending.host, _state: PhantomData, @@ -695,50 +647,44 @@ impl AsyncSession { impl AsyncSession { pub async fn send_command(&mut self, cmd: &[u8]) -> Vec { - // async I/O here... + // асинхронный ввод-вывод здесь... vec![0x00] } } -// Usage: +// Использование: // let session = AsyncSession::new("192.168.1.100"); // let mut session = session.authenticate("admin", "pass").await?; // let resp = session.send_command(&[0x04, 0x2D]).await; ``` -**Key rules for async type-state:** +**Ключевые правила для async typestate:** -| Rule | Why | -|------|-----| -| Transition methods take `self` (by value), not `&mut self` | Ownership transfer works across `.await` | -| Return `Result` for recoverable errors | Caller can retry from the previous state | -| Don't split state across multiple futures | One future owns one session | -| Use `Send + 'static` bounds if using tokio::spawn | The session must be movable across threads | +| Правило | Зачем | +|---------|-------| +| Методы перехода принимают `self` (по значению), а не `&mut self` | Передача владения работает через `.await` | +| Возвращать `Result` для восстанимых ошибок | Вызывающий может повторить попытку с предыдущего состояния | +| Не разделять состояние между несколькими future | Один future владеет одной сессией | +| Использовать ограничения `Send + 'static`, если применяется `tokio::spawn` | Сессия должна перемещаться между потоками | -> **Caveat:** If you need the *previous* state back on error (to retry), -> return `Result, (Error, AsyncSession)>` so -> the caller gets ownership back. Without this, a failed `.await` drops the -> session permanently. +> **Оговорка:** если при ошибке нужно вернуть *предыдущее* состояние (чтобы повторить попытку), верните `Result, (Error, AsyncSession)>`, чтобы вызывающий получил владение обратно. Без этого неудачный `.await` навсегда уничтожает сессию. *** -### Trick 9 — Refinement Types via Const Assertions +### Приём 9 — Уточняющие типы через const-утверждения -When a numeric constraint is a compile-time invariant (not runtime data), -use `const` evaluation to enforce it. This differs from Trick 6 (which -provides type-level size distinctions) — here we *reject invalid values* -at compile time: +Когда числовое ограничение является инвариантом времени компиляции (а не данными времени выполнения), используйте вычисления на этапе компиляции (`const`), чтобы его обеспечить. Это отличается от приёма 6, который задаёт различия размеров на уровне типов: здесь мы *отвергаем недопустимые значения* на этапе компиляции: ```rust,ignore -/// A sensor ID that must be in the IPMI SDR range (0x01..=0xFE). -/// The constraint is checked at compile time when `N` is const. +/// ID датчика, который должен находиться в диапазоне SDR IPMI (0x01..=0xFE). +/// Ограничение проверяется на этапе компиляции, когда `N` — константа. pub struct SdrSensorId; impl SdrSensorId { - /// Compile-time validation: panics during compilation if N is out of range. + /// Проверка на этапе компиляции: паникует во время компиляции, если N вне диапазона. pub const fn validate() { - assert!(N >= 0x01, "Sensor ID must be >= 0x01"); - assert!(N <= 0xFE, "Sensor ID must be <= 0xFE (0xFF is reserved)"); + assert!(N >= 0x01, "ID датчика должен быть >= 0x01"); + assert!(N <= 0xFE, "ID датчика должен быть <= 0xFE (0xFF зарезервирован)"); } pub const VALIDATED: () = Self::validate(); @@ -746,25 +692,25 @@ impl SdrSensorId { pub const fn value() -> u8 { N } } -// Usage: +// Использование: fn read_sensor_const() -> f64 { - let _ = SdrSensorId::::VALIDATED; // compile-time check - // read sensor N... + let _ = SdrSensorId::::VALIDATED; // проверка на этапе компиляции + // читаем датчик N... 42.0 } -// read_sensor_const::<0x20>(); // ✅ compiles — 0x20 is valid -// read_sensor_const::<0x00>(); // ❌ compile error — "Sensor ID must be >= 0x01" -// read_sensor_const::<0xFF>(); // ❌ compile error — 0xFF is reserved +// read_sensor_const::<0x20>(); // ✅ компилируется: 0x20 допустим +// read_sensor_const::<0x00>(); // ❌ ошибка компиляции: "ID датчика должен быть >= 0x01" +// read_sensor_const::<0xFF>(); // ❌ ошибка компиляции: 0xFF зарезервирован ``` -**Simpler form — bounded fan IDs:** +**Упрощённый вариант: ограниченные ID вентиляторов:** ```rust,ignore pub struct BoundedFanId; impl BoundedFanId { - pub const VALIDATED: () = assert!(N < 8, "Server has at most 8 fans (0..7)"); + pub const VALIDATED: () = assert!(N < 8, "Сервер поддерживает не более 8 вентиляторов (0..7)"); pub const fn id() -> u8 { let _ = Self::VALIDATED; @@ -773,97 +719,88 @@ impl BoundedFanId { } // BoundedFanId::<3>::id(); // ✅ -// BoundedFanId::<10>::id(); // ❌ compile error +// BoundedFanId::<10>::id(); // ❌ ошибка компиляции ``` -> **When to use:** Hardware-defined fixed IDs (sensor IDs, fan slots, PCIe -> slot numbers) known at compile time. When the value comes from runtime data -> (config file, user input), use `TryFrom` / `FromStr` (ch07, Trick 5) instead. +> **Когда использовать:** аппаратно заданные фиксированные ID (ID датчиков, слоты вентиляторов, номера слотов PCIe), известные на этапе компиляции. Если значение приходит из данных времени выполнения (файл конфигурации, ввод пользователя), используйте `TryFrom` / `FromStr` (гл. 07, приём 5). *** -### Trick 10 — Session Types for Channel Communication +### Приём 10 — Сессионные типы для обмена по каналам -When two components communicate over a channel (e.g., diagnostic orchestrator ↔ -worker thread), **session types** encode the protocol in the type system: +Когда два компонента обмениваются сообщениями по каналу (например, оркестратор диагностики и рабочий поток), **сессионные типы** кодируют протокол в системе типов: ```rust,ignore use std::marker::PhantomData; -// Protocol: Client sends Request, Server sends Response, then done. +// Протокол: клиент отправляет Request, сервер отправляет Response, затем завершение. pub struct SendRequest; pub struct RecvResponse; pub struct Done; -/// A typed channel endpoint. `S` is the current protocol state. +/// Типизированный конец канала. `S` — текущее состояние протокола. pub struct Chan { - // In real code: wraps a mpsc::Sender/Receiver pair + // В реальном коде: оборачивает пару mpsc::Sender/Receiver _state: PhantomData, } impl Chan { - /// Send a request — transitions to RecvResponse state. + /// Отправить запрос: переход в состояние RecvResponse. pub fn send(self, request: DiagRequest) -> Chan { - // ... send on channel ... + // ... отправка в канал ... Chan { _state: PhantomData } } } impl Chan { - /// Receive a response — transitions to Done state. + /// Получить ответ: переход в состояние Done. pub fn recv(self) -> (DiagResponse, Chan) { - // ... recv from channel ... + // ... получение из канала ... (DiagResponse { passed: true }, Chan { _state: PhantomData }) } } impl Chan { - /// Closing the channel — only possible when the protocol is complete. + /// Закрытие канала: возможно только когда протокол завершён. pub fn close(self) { /* drop */ } } pub struct DiagRequest { pub test_name: String } pub struct DiagResponse { pub passed: bool } -// The protocol MUST be followed in order: +// Протокол ОБЯЗАН соблюдаться по порядку: fn orchestrator(chan: Chan) { let chan = chan.send(DiagRequest { test_name: "gpu_stress".into() }); let (response, chan) = chan.recv(); chan.close(); - println!("Result: {}", if response.passed { "PASS" } else { "FAIL" }); + println!("Результат: {}", if response.passed { "PASS" } else { "FAIL" }); } -// Can't recv before send: +// Нельзя получить ответ до отправки: // fn wrong_order(chan: Chan) { -// chan.recv(); // ❌ no method `recv` on Chan +// chan.recv(); // ❌ нет метода `recv` у Chan // } ``` -> **When to use:** Inter-thread diagnostic protocols, BMC command sequences, -> any request-response pattern where order matters. For complex multi-message -> protocols, consider the [`session-types`](https://crates.io/crates/session-types) -> or [`rumpsteak`](https://crates.io/crates/rumpsteak) crates. +> **Когда использовать:** межпоточные диагностические протоколы, последовательности команд BMC, любой паттерн «запрос — ответ», где важен порядок. Для сложных многосообщенческих протоколов рассмотрите крейты [`session-types`](https://crates.io/crates/session-types) или [`rumpsteak`](https://crates.io/crates/rumpsteak). *** -### Trick 11 — `Pin` for Self-Referential State Machines +### Приём 11 — `Pin` для самоссылочных автоматов состояний -Some type-state machines need to hold references into their own data (e.g., a -parser that tracks a position within its owned buffer). Rust normally forbids -this because moving the struct would invalidate the internal pointer. `Pin` -solves this by guaranteeing the value **will not be moved**: +Некоторым автоматам состояний нужно хранить ссылки на собственные данные (например, парсеру, который отслеживает позицию внутри собственного буфера). Rust обычно запрещает это, потому что перемещение структуры сделает внутренний указатель недействительным. `Pin` решает эту проблему, гарантируя, что значение **не будет перемещено**: ```rust,ignore use std::pin::Pin; use std::marker::PhantomPinned; -/// A streaming parser that holds a reference into its own buffer. -/// Once pinned, it cannot be moved — the internal reference stays valid. +/// Потоковый парсер, который хранит ссылку на собственный буфер. +/// После закрепления его нельзя переместить: внутренняя ссылка остаётся действительной. pub struct StreamParser { buffer: Vec, - /// Points into `buffer`. Only valid while pinned. + /// Указывает внутрь `buffer`. Действительна только пока парсер закреплён. cursor: *const u8, - _pin: PhantomPinned, // opts out of Unpin — prevents accidental unpinning + _pin: PhantomPinned, // отказываемся от Unpin: предотвращает случайное открепление } impl StreamParser { @@ -875,9 +812,9 @@ impl StreamParser { }; let mut boxed = Box::pin(parser); - // Set cursor to point into the pinned buffer + // Устанавливаем cursor на закреплённый буфер let cursor = boxed.buffer.as_ptr(); - // SAFETY: we have exclusive access and the parser is pinned + // SAFETY: у нас эксклюзивный доступ, и парсер закреплён unsafe { let mut_ref = Pin::as_mut(&mut boxed); Pin::get_unchecked_mut(mut_ref).cursor = cursor; @@ -886,52 +823,47 @@ impl StreamParser { boxed } - /// Read the next byte — only callable through Pin<&mut Self>. + /// Читает следующий байт: вызывается только через Pin<&mut Self>. pub fn next_byte(self: Pin<&mut Self>) -> Option { - // The parser can't be moved, so cursor remains valid + // Парсер нельзя переместить, поэтому cursor остаётся действительным if self.cursor.is_null() { return None; } - // ... advance cursor through buffer ... - Some(42) // stub + // ... продвигаем cursor по буферу ... + Some(42) // заглушка } } -// Usage: +// Использование: // let mut parser = StreamParser::new(vec![0x01, 0x02, 0x03]); // let byte = parser.as_mut().next_byte(); ``` -**Key insight:** `Pin` is the correct-by-construction solution to the -self-referential struct problem. Without it, you'd need `unsafe` and manual -lifetime tracking. With it, the compiler prevents moves and the internal -pointer invariant is maintained. +**Ключевая идея:** `Pin` — это корректное по построению решение проблемы самоссылочных структур. Без него понадобился бы `unsafe` и ручное отслеживание времён жизни. С ним компилятор запрещает перемещения, и инвариант внутреннего указателя сохраняется. -| Use `Pin` when… | Don't use `Pin` when… | -|-----------------|----------------------| -| State machine holds intra-struct references | All fields are independently owned | -| Async futures that borrow across `.await` | No self-referencing needed | -| DMA descriptors that must not relocate in memory | Data can be freely moved | -| Hardware ring buffers with internal cursor | Simple index-based iteration works | +| Используйте `Pin`, когда… | Не используйте `Pin`, когда… | +|---------------------------|------------------------------| +| Автомат состояний хранит ссылки внутри структуры | Все поля независимо принадлежат структуре | +| Async-future заимствуют данные через `.await` | Самоссылки не нужны | +| Дескрипторы DMA, которые не должны перемещаться в памяти | Данные можно свободно перемещать | +| Аппаратные кольцевые буферы с внутренним курсором | Простой итерации по индексу достаточно | *** -### Trick 12 — RAII / `Drop` as a Correctness Guarantee +### Приём 12 — RAII / `Drop` как гарантия корректности -Rust's `Drop` trait is a correct-by-construction mechanism: cleanup code **cannot -be forgotten** because the compiler inserts it automatically. This is especially -valuable for hardware resources that must be released exactly once. +Трейт `Drop` в Rust — это механизм корректности по построению: код очистки **нельзя забыть**, потому что компилятор вставляет его автоматически. Это особенно ценно для аппаратных ресурсов, которые нужно освободить ровно один раз. ```rust,ignore use std::io; -/// An IPMI session that MUST be closed when done. -/// The `Drop` impl guarantees cleanup even on panic or early `?` return. +/// Сессия IPMI, которую ОБЯЗАТЕЛЬНО нужно закрыть по завершении. +/// Реализация `Drop` гарантирует очистку даже при панике или раннем возврате через `?`. pub struct IpmiSession { handle: u32, } impl IpmiSession { pub fn open(host: &str) -> io::Result { - // ... negotiate IPMI session ... + // ... согласовываем сессию IPMI ... Ok(IpmiSession { handle: 42 }) } @@ -942,39 +874,38 @@ impl IpmiSession { impl Drop for IpmiSession { fn drop(&mut self) { - // Close Session command: always runs, even on panic/early-return. - // In C, forgetting CloseSession() leaks a BMC session slot. + // Команда Close Session: выполняется всегда, даже при панике или раннем возврате. + // В C забытый CloseSession() оставляет слот сессии BMC занятым. let _ = self.send_raw(&[0x06, 0x3C]); - eprintln!("[RAII] session {} closed", self.handle); + eprintln!("[RAII] сессия {} закрыта", self.handle); } } -// Usage: +// Использование: fn diagnose(host: &str) -> io::Result<()> { let session = IpmiSession::open(host)?; session.send_raw(&[0x04, 0x2D, 0x20])?; - // No explicit close needed — Drop runs here automatically + // Явное закрытие не нужно: Drop выполнится здесь автоматически Ok(()) - // Even if send_raw returns Err(...), the session is still closed. + // Даже если send_raw вернёт Err(...), сессия всё равно закроется. } ``` -**The C/C++ failure mode that RAII eliminates:** +**Сбой C/C++, который устраняет RAII:** ```text C: session = ipmi_open(host); ipmi_send(session, data); - if (error) return -1; // 🐛 leaked session — forgot close() + if (error) return -1; // 🐛 утечка сессии: забыли close() ipmi_close(session); Rust: let session = IpmiSession::open(host)?; - session.send_raw(data)?; // ✅ Drop runs on ? return - // Drop always runs — leak is impossible + session.send_raw(data)?; // ✅ Drop выполняется при раннем возврате через ? + // Drop всегда выполняется: утечка невозможна ``` -**Combine RAII with type-state (ch05) for ordered cleanup:** +**Комбинируем RAII с typestate (гл. 05) для упорядоченной очистки:** -You cannot specialize `Drop` on a generic parameter (Rust error E0366). -Instead, use **separate wrapper types** per state: +Нельзя специализировать `Drop` по параметру обобщённого типа (ошибка Rust E0366). Вместо этого используйте **отдельные типы-обёртки для каждого состояния**: ```rust,ignore use std::marker::PhantomData; @@ -989,64 +920,57 @@ pub struct GpuContext { impl GpuContext { pub fn lock_clocks(self) -> LockedGpu { - // ... lock GPU clocks for stable benchmarking ... + // ... блокируем частоты GPU для стабильных замеров ... LockedGpu { device_id: self.device_id } } } -/// Separate type for the locked state — has its own Drop. -/// We can't do `impl Drop for GpuContext` (E0366), -/// so we use a distinct wrapper that owns the locked resource. +/// Отдельный тип для заблокированного состояния: имеет собственный Drop. +/// Нельзя написать `impl Drop for GpuContext` (E0366), +/// поэтому используем отдельную обёртку, которая владеет заблокированным ресурсом. pub struct LockedGpu { device_id: u32, } impl LockedGpu { pub fn run_benchmark(&self) -> f64 { - // ... benchmark with locked clocks ... + // ... бенчмарк с заблокированными частотами ... 42.0 } } impl Drop for LockedGpu { fn drop(&mut self) { - // Unlock clocks on drop — only fires for the locked wrapper. - eprintln!("[RAII] GPU {} clocks unlocked", self.device_id); + // Разблокируем частоты при Drop: срабатывает только для заблокированной обёртки. + eprintln!("[RAII] частоты GPU {} разблокированы", self.device_id); } } -// GpuContext has no special Drop — no clocks to unlock. -// LockedGpu always unlocks on drop, even on panic or early return. +// GpuContext не имеет собственного Drop: разблокировать нечего. +// LockedGpu всегда разблокирует частоты при Drop, даже при панике или раннем возврате. ``` -> **Why not `impl Drop for GpuContext`?** Rust requires `Drop` impls -> to apply to *all* instantiations of a generic type. To get state-specific -> cleanup, use one of: +> **Почему не `impl Drop for GpuContext`?** Rust требует, чтобы реализации `Drop` применялись ко *всем* инстанциям обобщённого типа. Чтобы получить очистку, зависящую от состояния, используйте один из вариантов: > -> | Approach | Pros | Cons | -> |----------|------|------| -> | Separate wrapper type (above) | Clean, zero-cost | Extra type name | -> | Generic `Drop` + runtime `TypeId` check | Single type | Requires `'static`, runtime cost | -> | `enum` state with exhaustive match in `Drop` | Single generic type | Runtime dispatch, less type safety | +> | Подход | Плюсы | Минусы | +> |--------|-------|--------| +> | Отдельный тип-обёртка (выше) | Чисто, без накладных расходов | Дополнительное имя типа | +> | Обобщённый `Drop` и проверка `TypeId` во время выполнения | Один тип | Требует `'static`, накладные расходы во время выполнения | +> | Состояние в виде `enum` с исчерпывающим `match` в `Drop` | Один обобщённый тип | Диспетчеризация во время выполнения, меньше безопасности типов | -> **When to use:** BMC sessions, GPU clock locks, DMA buffer mappings, file -> handles, mutex guards, any resource with a mandatory release step. If you -> find yourself writing `fn close(&mut self)` or `fn cleanup()`, it should -> almost certainly be `Drop` instead. +> **Когда использовать:** сессии BMC, блокировки частот GPU, отображения буферов DMA, файловые дескрипторы, охранники мьютексов (mutex guards), любой ресурс с обязательным шагом освобождения. Если вы пишете `fn close(&mut self)` или `fn cleanup()`, почти наверняка это должен быть `Drop`. *** -### Trick 13 — Error Type Hierarchies as Correctness +### Приём 13 — Иерархии типов ошибок как инструмент корректности -Well-designed error types prevent silent error swallowing and ensure callers -handle each failure mode appropriately. Using `thiserror` for structured errors -is a correct-by-construction pattern: the compiler forces exhaustive matching. +Хорошо спроектированные типы ошибок предотвращают тихое проглатывание ошибок и заставляют вызывающий код корректно обрабатывать каждый режим отказа. Использование `thiserror` для структурированных ошибок — паттерн корректности по построению: компилятор требует исчерпывающего сопоставления. ```toml # Cargo.toml [dependencies] thiserror = "1" -# For application-level error handling (optional): +# Для обработки ошибок на уровне приложения (необязательно): # anyhow = "1" ``` @@ -1055,35 +979,35 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum DiagError { - #[error("IPMI communication failed: {0}")] + #[error("сбой связи по IPMI: {0}")] Ipmi(#[from] IpmiError), - #[error("sensor {sensor_id:#04x} reading out of range: {value}")] + #[error("показание датчика {sensor_id:#04x} вне диапазона: {value}")] SensorRange { sensor_id: u8, value: f64 }, - #[error("GPU {gpu_id} not responding")] + #[error("GPU {gpu_id} не отвечает")] GpuTimeout { gpu_id: u32 }, - #[error("configuration invalid: {0}")] + #[error("некорректная конфигурация: {0}")] Config(String), } #[derive(Debug, Error)] pub enum IpmiError { - #[error("session authentication failed")] + #[error("аутентификация сессии не пройдена")] AuthFailed, - #[error("command {net_fn:#04x}/{cmd:#04x} timed out")] + #[error("команда {net_fn:#04x}/{cmd:#04x}: истекло время ожидания")] Timeout { net_fn: u8, cmd: u8 }, - #[error("completion code {0:#04x}")] + #[error("код завершения {0:#04x}")] CompletionCode(u8), } -// Callers MUST handle each variant — no silent swallowing: +// Вызывающий ОБЯЗАН обработать каждый вариант: никакого тихого проглатывания fn run_thermal_check() -> Result<(), DiagError> { - // If this returns IpmiError, it's automatically converted to DiagError::Ipmi - // via the #[from] attribute. + // Если здесь возвращается IpmiError, он автоматически преобразуется в DiagError::Ipmi + // через атрибут #[from]. let temp = read_cpu_temp()?; if temp > 105.0 { return Err(DiagError::SensorRange { @@ -1097,54 +1021,49 @@ fn run_thermal_check() -> Result<(), DiagError> { # fn read_cpu_temp() -> Result { Ok(42.0) } ``` -**Why this is correct-by-construction:** +**Почему это корректно по построению:** -| Without structured errors | With `thiserror` enums | -|--------------------------|----------------------| +| Без структурированных ошибок | С enum из `thiserror` | +|------------------------------|----------------------| | `fn op() -> Result` | `fn op() -> Result` | -| Caller gets opaque string | Caller matches on specific variants | -| Can't distinguish auth failure from timeout | `DiagError::Ipmi(IpmiError::AuthFailed)` vs `Timeout` | -| Logging swallows the error | `match` forces handling each case | -| New error variant → nobody notices | New variant → compiler warns unmatched arms | +| Вызывающий получает непрозрачную строку | Вызывающий сопоставляет конкретные варианты | +| Нельзя отличить сбой аутентификации от тайм-аута | `DiagError::Ipmi(IpmiError::AuthFailed)` и `Timeout` | +| Логирование проглатывает ошибку | `match` заставляет обработать каждый случай | +| Новый вариант ошибки: никто не замечает | Новый вариант: компилятор предупреждает о непокрытых ветках | -**The `anyhow` vs `thiserror` decision:** +**Выбор между `anyhow` и `thiserror`:** -| Use `thiserror` when… | Use `anyhow` when… | -|-----------------------|-------------------| -| Writing a library/crate | Writing a binary/CLI | -| Callers need to match on error variants | Callers just log and exit | -| Error types are part of the public API | Internal error plumbing | -| `protocol_lib`, `accel_diag`, `thermal_diag` | `diag_tool` main binary | +| Используйте `thiserror`, когда… | Используйте `anyhow`, когда… | +|---------------------------------|------------------------------| +| Пишете библиотеку или крейт | Пишете бинарник или CLI | +| Вызывающим нужно сопоставлять варианты ошибок | Вызывающие просто логируют и завершаются | +| Типы ошибок — часть публичного API | Внутренняя обвязка ошибок | +| `protocol_lib`, `accel_diag`, `thermal_diag` | Главный бинарник `diag_tool` | -> **When to use:** Every crate in the workspace should define its own error -> enum with `thiserror`. The top-level binary crate can use `anyhow` to -> aggregate them. This gives library callers compile-time error handling -> guarantees while keeping the binary ergonomic. +> **Когда использовать:** каждый крейт в рабочей области должен определять собственный enum ошибок с `thiserror`. Верхнеуровневый бинарный крейт может использовать `anyhow`, чтобы объединить их. Это даёт вызывающим библиотеки гарантии обработки ошибок на этапе компиляции, сохраняя при этом удобство бинарника. *** -### Trick 14 — `#[must_use]` for Enforcing Consumption +### Приём 14 — `#[must_use]` для обязательного использования -The `#[must_use]` attribute turns ignored return values into compiler warnings. -This is a lightweight correct-by-construction tool that pairs with every pattern -in this guide: +Атрибут `#[must_use]` превращает проигнорированные возвращаемые значения в предупреждения компилятора. Это лёгкий инструмент корректности по построению, который сочетается с каждым паттерном этого руководства: ```rust,ignore -/// A calibration token that MUST be used — dropping it silently is a bug. -#[must_use = "calibration token must be passed to calibrate(), not dropped"] +/// Токен калибровки, который ОБЯЗАН быть использован: тихое отбрасывание — ошибка. +#[must_use = "токен калибровки нужно передать в calibrate(), а не отбрасывать"] pub struct CalibrationToken { _private: (), } -/// A diagnostic result that MUST be checked — ignoring failures is a bug. -#[must_use = "diagnostic result must be inspected for failures"] +/// Результат диагностики, который ОБЯЗАН быть проверен: игнорирование сбоев — ошибка. +#[must_use = "результат диагностики нужно проверить на наличие сбоев"] pub struct DiagResult { pub passed: bool, pub details: String, } -/// Functions that return important values should be marked too: -#[must_use = "the authenticated session must be used or explicitly closed"] +/// Функции, возвращающие важные значения, тоже стоит пометить: +#[must_use = "аутентифицированную сессию нужно использовать или явно закрыть"] pub fn authenticate(user: &str, pass: &str) -> Result { // ... # unimplemented!() @@ -1154,7 +1073,7 @@ pub fn authenticate(user: &str, pass: &str) -> Result { # pub struct AuthError; ``` -**What the compiler tells you:** +**Что сообщает компилятор:** ```text warning: unused `CalibrationToken` that must be used @@ -1163,31 +1082,27 @@ warning: unused `CalibrationToken` that must be used 5 | CalibrationToken { _private: () }; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | - = note: calibration token must be passed to calibrate(), not dropped + = note: токен калибровки нужно передать в calibrate(), а не отбрасывать ``` -**Apply `#[must_use]` to these patterns:** +**Применяйте `#[must_use]` к этим паттернам:** -| Pattern | What to annotate | Why | -|---------|-----------------|-----| -| Single-Use Tokens (ch03) | `CalibrationToken`, `FusePayload` | Dropping without use = logic bug | -| Capability Tokens (ch04) | `AdminToken` | Authenticating but ignoring the token | -| Type-State transitions | Return type of `authenticate()`, `activate()` | Session created but never used | -| Results | `DiagResult`, `SensorReading` | Silent failure swallowing | -| RAII handles (Trick 12) | `IpmiSession`, `LockedGpu` | Opening but not using a resource | +| Паттерн | Что аннотировать | Почему | +|---------|------------------|--------| +| Одноразовые токены (гл. 03) | `CalibrationToken`, `FusePayload` | Отбрасывание без использования: логическая ошибка | +| Capability-токены (гл. 04) | `AdminToken` | Аутентификация, но токен игнорируется | +| Переходы typestate | Возвращаемый тип `authenticate()`, `activate()` | Сессия создана, но не используется | +| Результаты | `DiagResult`, `SensorReading` | Тихое проглатывание сбоев | +| Обработчики RAII (приём 12) | `IpmiSession`, `LockedGpu` | Ресурс открыт, но не используется | -> **Rule of thumb:** If dropping a value without using it is always a bug, -> add `#[must_use]`. If it's sometimes intentional (e.g., a `Vec`), don't. -> The `_` prefix (`let _ = foo()`) explicitly acknowledges and silences the -> warning — this is fine when the drop is intentional. +> **Эмпирическое правило:** если отбрасывание значения без использования всегда является ошибкой, добавьте `#[must_use]`. Если это иногда намеренно (например, для `Vec`), не добавляйте. Префикс `_` (`let _ = foo()`) явно подтверждает отбрасывание и отключает предупреждение: это нормально, когда отбрасывание намеренное. -## Key Takeaways +## Ключевые выводы -1. **Sentinel → Option at the boundary** — convert magic values to `Option` on parse; the compiler forces callers to handle `None`. -2. **Sealed traits close the implementation loophole** — private supertrait means only your crate can implement the trait. -3. **`#[non_exhaustive]` + `#[must_use]` are one-line, high-value annotations** — add them to evolving enums and consumed tokens. -4. **Typestate builders enforce required fields** — `finish()` only exists when all required type parameters are `Set`. -5. **Each trick targets a specific bug class** — adopt them incrementally; no trick requires rewriting your architecture. +1. **Sentinel → Option на границе**: преобразуйте магические значения в `Option` при разборе; компилятор заставит вызывающих обрабатывать `None`. +2. **Sealed-трейты закрывают лазейку в реализациях**: приватный супертрейт означает, что реализовать трейт может только ваш крейт. +3. **`#[non_exhaustive]` и `#[must_use]` — однострочные аннотации с высокой отдачей**: добавляйте их к развивающимся enum и потребляемым токенам. +4. **Билдеры с typestate обеспечивают обязательные поля**: `finish()` существует только тогда, когда все обязательные параметры типа в состоянии `Set`. +5. **Каждый приём нацелен на конкретный класс ошибок**: внедряйте их постепенно; ни один приём не требует переписывать архитектуру. --- - diff --git a/type-driven-correctness-book/src/ch12-exercises.md b/type-driven-correctness-book/src/ch12-exercises.md index eecba52..cf72eae 100644 --- a/type-driven-correctness-book/src/ch12-exercises.md +++ b/type-driven-correctness-book/src/ch12-exercises.md @@ -1,28 +1,28 @@ -# Exercises 🟡 +# Упражнения 🟡 -> **What you'll learn:** Hands-on practice applying correct-by-construction patterns to realistic hardware scenarios — NVMe admin commands, firmware update state machines, sensor pipelines, PCIe phantom types, multi-protocol health checks, and session-typed diagnostic protocols. +> **Что вы узнаете:** практику применения паттернов корректности по построению к реалистичным сценариям аппаратного обеспечения: административные команды NVMe, автоматы состояний обновления прошивки, конвейеры обработки показаний датчиков, phantom-типы для PCIe, проверки здоровья по нескольким протоколам и диагностические протоколы с сессионными типами. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (exercise 1), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (exercise 2), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (exercise 3), [ch09](ch09-phantom-types-for-resource-tracking.md) (exercise 4), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (exercise 5) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (упражнение 1), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (упражнение 2), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (упражнение 3), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (упражнение 4), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (упражнение 5) -## Practice Problems +## Практические задачи -### Exercise 1: NVMe Admin Command (Typed Commands) +### Упражнение 1: команда администрирования NVMe (типизированные команды) -Design a typed command interface for NVMe admin commands: +Спроектируйте типизированный командный интерфейс для административных команд NVMe: -- `Identify` → `IdentifyResponse` (model number, serial, firmware rev) -- `GetLogPage` → `SmartLog` (temperature, available spare, data units read) -- `GetFeature` → feature-specific response +- `Identify` → `IdentifyResponse` (номер модели, серийный номер, версия прошивки) +- `GetLogPage` → `SmartLog` (температура, доступный резерв, количество прочитанных единиц данных) +- `GetFeature` → ответ, специфичный для функции -Requirements: -1. The command type determines the response type -2. No runtime dispatch — static dispatch only -3. Add a `NamespaceId` newtype that prevents mixing namespace IDs with other `u32`s +Требования: +1. Тип команды определяет тип ответа +2. Никакой диспетчеризации во время выполнения, только статическая +3. Добавьте newtype `NamespaceId`, который не даёт перепутать идентификаторы пространств имён с другими значениями `u32` -**Hint:** Follow the `IpmiCmd` trait pattern from ch02, but use NVMe-specific constants. +**Подсказка:** следуйте паттерну трейта `IpmiCmd` из гл. 02, но используйте константы, специфичные для NVMe.
-Sample Solution (Exercise 1) +Пример решения (упражнение 1) ```rust,ignore use std::io; @@ -51,7 +51,7 @@ pub struct ArbitrationFeature { pub low_priority_weight: u8, } -/// The core pattern: associated type pins each command's response. +/// Ключевой паттерн: ассоциированный тип фиксирует ответ каждой команды. pub trait NvmeAdminCmd { type Response; fn opcode(&self) -> u8; @@ -67,7 +67,7 @@ impl NvmeAdminCmd for Identify { fn nsid(&self) -> Option { Some(self.nsid) } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 12 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "too short")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко")); } Ok(IdentifyResponse { model: String::from_utf8_lossy(&raw[0..4]).trim().to_string(), @@ -85,7 +85,7 @@ impl NvmeAdminCmd for GetLogPage { fn nsid(&self) -> Option { None } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 11 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "too short")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко")); } Ok(SmartLog { temperature_kelvin: u16::from_le_bytes([raw[0], raw[1]]), @@ -103,7 +103,7 @@ impl NvmeAdminCmd for GetFeature { fn nsid(&self) -> Option { None } fn parse_response(&self, raw: &[u8]) -> io::Result { if raw.len() < 3 { - return Err(io::Error::new(io::ErrorKind::InvalidData, "too short")); + return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко")); } Ok(ArbitrationFeature { high_priority_weight: raw[0], @@ -113,34 +113,34 @@ impl NvmeAdminCmd for GetFeature { } } -/// Static dispatch — the compiler monomorphises per command type. +/// Статическая диспетчеризация: компилятор мономорфизирует код для каждого типа команды. pub struct NvmeController; impl NvmeController { pub fn execute(&self, cmd: &C) -> io::Result { - // Build SQE from cmd.opcode()/cmd.nsid(), - // submit to SQ, wait for CQ, then: + // Собираем SQE из cmd.opcode()/cmd.nsid(), + // отправляем в SQ, ждём CQ, затем: let raw = self.submit_and_read(cmd.opcode())?; cmd.parse_response(&raw) } fn submit_and_read(&self, _opcode: u8) -> io::Result> { - // Real implementation talks to /dev/nvme0 + // Реальная реализация обращается к /dev/nvme0 Ok(vec![0; 512]) } } ``` -**Key points:** -- `NamespaceId(u32)` prevents mixing namespace IDs with arbitrary `u32` values. -- `NvmeAdminCmd::Response` is the "type index" — `execute()` returns exactly `C::Response`. -- Fully static dispatch: no `Box`, no runtime downcasting. +**Ключевые моменты:** +- `NamespaceId(u32)` не даёт смешивать идентификаторы пространств имён с произвольными значениями `u32`. +- `NvmeAdminCmd::Response` — это «индекс типа»: `execute()` возвращает ровно `C::Response`. +- Полностью статическая диспетчеризация: никаких `Box` и никакого динамического приведения типов во время выполнения.
-### Exercise 2: Firmware Update State Machine (Type-State) +### Упражнение 2: автомат состояний обновления прошивки (typestate) -Model a BMC firmware update lifecycle: +Смоделируйте жизненный цикл обновления прошивки BMC: ```mermaid stateDiagram-v2 @@ -149,44 +149,44 @@ stateDiagram-v2 Uploading --> Uploading : send_chunk(data) Uploading --> Verifying : finish_upload() Uploading --> Idle : abort() - Verifying --> Applying : verify() ✅ + VerifiedImage token - Verifying --> Idle : verify() ❌ or abort() + Verifying --> Applying : verify() ✅ + токен VerifiedImage + Verifying --> Idle : verify() ❌ или abort() Applying --> Rebooting : apply(token) Rebooting --> Complete : reboot_complete() Complete --> [*] - note right of Applying : No abort() — irreversible - note right of Verifying : VerifiedImage is a proof token + note right of Applying : нет abort(): необратимо + note right of Verifying : VerifiedImage — токен-доказательство ``` -Requirements: -1. Each state is a distinct type -2. Upload can only begin from Idle -3. Verification requires upload to be complete -4. Apply can only happen after successful verification — take a `VerifiedImage` proof token -5. Reboot is the only option after applying -6. Add an `abort()` method available in Uploading and Verifying (but not Applying — too late) +Требования: +1. Каждое состояние — отдельный тип +2. Загрузка может начинаться только из `Idle` +3. Проверка требует завершённой загрузки +4. Применение возможно только после успешной проверки: примите токен-доказательство `VerifiedImage` +5. Перезагрузка — единственный вариант после применения +6. Добавьте метод `abort()`, доступный в `Uploading` и `Verifying` (но не в `Applying`: уже поздно) -**Hint:** Combine type-state (ch05) with capability tokens (ch04). +**Подсказка:** совместите typestate (гл. 05) с capability-токенами (гл. 04).
-Sample Solution (Exercise 2) +Пример решения (упражнение 2) ```rust,ignore -// --- State types --- -// Design choice: here we store state inline (`_state: S`) rather than using -// `PhantomData` (ch05's approach). This lets states carry data — -// e.g., `Uploading { bytes_sent: usize }` tracks progress. Use `PhantomData` -// when states are pure markers (zero-sized); use inline storage when -// states carry meaningful runtime data. +// --- Типы состояний --- +// Проектное решение: здесь состояние хранится непосредственно (`_state: S`), а не через +// `PhantomData` (подход гл. 05). Это позволяет состояниям нести данные: например, +// `Uploading { bytes_sent: usize }` отслеживает прогресс. Используйте `PhantomData`, +// когда состояния — чистые маркеры (нулевого размера); храните состояние в структуре, +// когда состояния несут значимые данные времени выполнения. pub struct Idle; -pub struct Uploading { bytes_sent: usize } // not ZST — carries progress data +pub struct Uploading { bytes_sent: usize } // не ZST: несёт данные о прогрессе pub struct Verifying; pub struct Applying; pub struct Rebooting; pub struct Complete; -/// Proof token: only constructed inside verify(). +/// Токен-доказательство: создаётся только внутри verify(). pub struct VerifiedImage { _private: () } pub struct FwUpdate { @@ -211,31 +211,31 @@ impl FwUpdate { pub fn finish_upload(self) -> FwUpdate { FwUpdate { bmc_addr: self.bmc_addr, _state: Verifying } } - /// Abort available during upload — returns to Idle. + /// Abort доступен во время загрузки: возврат в Idle. pub fn abort(self) -> FwUpdate { FwUpdate { bmc_addr: self.bmc_addr, _state: Idle } } } impl FwUpdate { - /// On success, returns the next state AND a VerifiedImage proof token. + /// При успехе возвращает следующее состояние И токен-доказательство VerifiedImage. pub fn verify(self) -> Result<(FwUpdate, VerifiedImage), FwUpdate> { - // Real: check CRC, signature, compatibility + // Реально: проверяем CRC, подпись, совместимость let token = VerifiedImage { _private: () }; Ok(( FwUpdate { bmc_addr: self.bmc_addr, _state: Applying }, token, )) } - /// Abort available during verification. + /// Abort доступен во время проверки. pub fn abort(self) -> FwUpdate { FwUpdate { bmc_addr: self.bmc_addr, _state: Idle } } } impl FwUpdate { - /// Consumes the VerifiedImage proof — can't apply without verification. - /// Note: NO abort() method here — once flashing starts, it's too dangerous. + /// Потребляет токен-доказательство VerifiedImage: без проверки применить нельзя. + /// Примечание: метода abort() здесь НЕТ, потому что после начала записи это слишком опасно. pub fn apply(self, _proof: VerifiedImage) -> FwUpdate { FwUpdate { bmc_addr: self.bmc_addr, _state: Rebooting } } @@ -251,39 +251,37 @@ impl FwUpdate { pub fn version(&self) -> &str { "2.1.0" } } -// Usage: +// Использование: // let fw = FwUpdate::new("192.168.1.100") // .begin_upload() // .send_chunk(b"image_data") // .finish_upload(); -// let (fw, proof) = fw.verify().map_err(|_| "verify failed")?; +// let (fw, proof) = fw.verify().map_err(|_| "проверка не пройдена")?; // let fw = fw.apply(proof).wait_for_reboot(); -// println!("New version: {}", fw.version()); +// println!("Новая версия: {}", fw.version()); ``` -**Key points:** -- `abort()` exists only on `FwUpdate` and `FwUpdate` — calling - it on `FwUpdate` is a **compile error**, not a runtime check. -- `VerifiedImage` has a private field, so only `verify()` can create one. -- `apply()` consumes the proof token — you can't skip verification. +**Ключевые моменты:** +- `abort()` существует только у `FwUpdate` и `FwUpdate`: вызов его у `FwUpdate` — **ошибка компиляции**, а не проверка во время выполнения. +- `VerifiedImage` имеет приватное поле, поэтому создать его может только `verify()`. +- `apply()` потребляет токен-доказательство: пропустить проверку нельзя.
-### Exercise 3: Sensor Reading Pipeline (Dimensional Analysis) +### Упражнение 3: конвейер показаний датчиков (анализ размерностей) -Build a complete sensor pipeline: +Постройте полный конвейер обработки показаний датчиков: -1. Define newtypes: `RawAdc`, `Celsius`, `Fahrenheit`, `Volts`, `Millivolts`, `Watts` -2. Implement `From for Fahrenheit` and vice versa -3. Create `impl Mul for Amperes` (P = V × I) -4. Build a `Threshold` generic checker -5. Write a pipeline: ADC → calibration → threshold check → result +1. Определите newtype: `RawAdc`, `Celsius`, `Fahrenheit`, `Volts`, `Millivolts`, `Watts` +2. Реализуйте `From for Fahrenheit` и наоборот +3. Создайте `impl Mul for Amperes` (P = V × I) +4. Постройте обобщённый проверщик порогов `Threshold` +5. Напишите конвейер: АЦП → калибровка → проверка порога → результат -The compiler should reject: comparing `Celsius` to `Volts`, adding `Watts` to `Rpm`, -passing `Millivolts` where `Volts` is expected. +Компилятор должен отвергать: сравнение `Celsius` с `Volts`, сложение `Watts` с `Rpm`, передачу `Millivolts` там, где ожидаются `Volts`.
-Sample Solution (Exercise 3) +Пример решения (упражнение 3) ```rust,ignore use std::ops::{Add, Sub, Mul}; @@ -309,7 +307,7 @@ pub struct Amperes(pub f64); #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Watts(pub f64); -// --- Safe conversions --- +// --- Безопасные преобразования --- impl From for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) } } @@ -323,10 +321,9 @@ impl From for Millivolts { fn from(v: Volts) -> Self { Millivolts(v.0 * 1000.0) } } -// --- Arithmetic on same-unit types --- -// NOTE: Adding absolute temperatures (25°C + 30°C) is physically -// questionable — see ch06's discussion of ΔT newtypes for a more -// rigorous approach. Here we keep it simple for the exercise. +// --- Арифметика для величин одной единицы --- +// ПРИМЕЧАНИЕ: сложение абсолютных температур (25°C + 30°C) физически сомнительно. +// См. в гл. 06 обсуждение newtype для ΔT: там подход строже. Здесь для упражнения оставим всё просто. impl Add for Celsius { type Output = Celsius; fn add(self, rhs: Self) -> Celsius { Celsius(self.0 + rhs.0) } @@ -336,16 +333,16 @@ impl Sub for Celsius { fn sub(self, rhs: Self) -> Celsius { Celsius(self.0 - rhs.0) } } -// P = V × I (cross-unit multiplication) +// P = V × I (умножение разных единиц) impl Mul for Volts { type Output = Watts; fn mul(self, rhs: Amperes) -> Watts { Watts(self.0 * rhs.0) } } -// --- Generic threshold checker --- -// Exercise 3 extends ch06's Threshold with a generic ThresholdResult -// that carries the triggering reading — an evolution of ch06's simpler -// ThresholdResult { Normal, Warning, Critical } enum. +// --- Обобщённый проверщик порогов --- +// Упражнение 3 расширяет Threshold из гл. 06 обобщённым ThresholdResult, +// который несёт показание, вызвавшее срабатывание. Это развитие более простого +// enum ThresholdResult { Normal, Warning, Critical } из гл. 06. pub enum ThresholdResult { Normal(T), Warning(T), @@ -357,7 +354,7 @@ pub struct Threshold { pub critical: T, } -// Generic impl — works for any unit type that supports PartialOrd. +// Обобщённая реализация: работает для любого типа единиц, поддерживающего PartialOrd. impl Threshold { pub fn check(&self, reading: T) -> ThresholdResult { if reading >= self.critical { @@ -369,12 +366,12 @@ impl Threshold { } } } -// Now `Threshold`, `Threshold`, etc. all work automatically. +// Теперь `Threshold`, `Threshold` и т. д. работают автоматически. -// --- Pipeline: ADC → calibration → threshold → result --- +// --- Конвейер: АЦП → калибровка → порог → результат --- pub struct CalibrationParams { - pub scale: f64, // ADC counts per °C - pub offset: f64, // °C at ADC 0 + pub scale: f64, // отсчётов АЦП на °C + pub offset: f64, // °C при АЦП 0 } pub fn calibrate(raw: RawAdc, params: &CalibrationParams) -> Celsius { @@ -390,51 +387,50 @@ pub fn sensor_pipeline( threshold.check(temp) } -// Compile-time safety — these would NOT compile: -// let _ = Celsius(25.0) + Volts(12.0); // ERROR: mismatched types -// let _: Millivolts = Volts(1.0); // ERROR: no implicit coercion -// let _ = Watts(100.0) + Rpm(3000); // ERROR: mismatched types +// Проверка на этапе компиляции: эти строки НЕ скомпилируются: +// let _ = Celsius(25.0) + Volts(12.0); // ОШИБКА: mismatched types +// let _: Millivolts = Volts(1.0); // ОШИБКА: no implicit coercion +// let _ = Watts(100.0) + Rpm(3000); // ОШИБКА: mismatched types ``` -**Key points:** -- Each physical unit is a distinct type — no accidental mixing. -- `Mul for Volts` yields `Watts`, encoding P = V × I in the type system. -- Explicit `From` conversions for related units (mV ↔ V, °C ↔ °F). -- `Threshold` only accepts `Celsius` — can't accidentally threshold-check RPM. +**Ключевые моменты:** +- Каждая физическая величина — отдельный тип: случайного смешивания не происходит. +- `Mul for Volts` даёт `Watts`, кодируя P = V × I в системе типов. +- Явные преобразования `From` для связанных единиц (мВ ↔ В, °C ↔ °F). +- `Threshold` принимает только `Celsius`: нельзя случайно проверить порог для об/мин.
-### Exercise 4: PCIe Capability Walk (Phantom Types + Validated Boundary) +### Упражнение 4: обход возможностей PCIe (phantom-типы + проверенная граница) -Model the PCIe capability linked list: +Смоделируйте связный список возможностей PCIe: -1. `RawCapability` — unvalidated bytes from config space -2. `ValidCapability` — parsed and validated (via TryFrom) -3. Each capability type (MSI, MSI-X, PCIe Express, Power Management) has its own - phantom-typed register layout -4. Walking the list returns an iterator of `ValidCapability` values +1. `RawCapability`: непроверенные байты из конфигурационного пространства +2. `ValidCapability`: разобранная и проверенная (через TryFrom) +3. Каждый тип возможности (MSI, MSI-X, PCI Express, Power Management) имеет собственную раскладку регистров с phantom-типом +4. Обход списка возвращает итератор значений `ValidCapability` -**Hint:** Combine validated boundaries (ch07) with phantom types (ch09). +**Подсказка:** совместите проверенные границы (гл. 07) с phantom-типами (гл. 09).
-Sample Solution (Exercise 4) +Пример решения (упражнение 4) ```rust,ignore use std::marker::PhantomData; -// --- Phantom markers for capability types --- +// --- Phantom-маркеры для типов возможностей --- pub struct Msi; pub struct MsiX; pub struct PciExpress; pub struct PowerMgmt; -// PCI capability IDs from the spec +// Идентификаторы возможностей PCI из спецификации const CAP_ID_PM: u8 = 0x01; const CAP_ID_MSI: u8 = 0x05; const CAP_ID_PCIE: u8 = 0x10; const CAP_ID_MSIX: u8 = 0x11; -/// Unvalidated bytes — may be garbage. +/// Непроверенные байты: могут быть мусором. #[derive(Debug)] pub struct RawCapability { pub id: u8, @@ -442,7 +438,7 @@ pub struct RawCapability { pub data: Vec, } -/// Validated and type-tagged capability. +/// Проверенная и помеченная типом возможность. #[derive(Debug)] pub struct ValidCapability { id: u8, @@ -451,12 +447,12 @@ pub struct ValidCapability { _kind: PhantomData, } -// --- TryFrom: parse-don't-validate boundary --- +// --- TryFrom: граница parse-don't-validate --- impl TryFrom for ValidCapability { type Error = &'static str; fn try_from(raw: RawCapability) -> Result { - if raw.id != CAP_ID_PM { return Err("not a PM capability"); } - if raw.data.len() < 2 { return Err("PM data too short"); } + if raw.id != CAP_ID_PM { return Err("не возможность PM"); } + if raw.data.len() < 2 { return Err("данные PM слишком короткие"); } Ok(ValidCapability { id: raw.id, next_ptr: raw.next_ptr, data: raw.data, _kind: PhantomData, @@ -467,8 +463,8 @@ impl TryFrom for ValidCapability { impl TryFrom for ValidCapability { type Error = &'static str; fn try_from(raw: RawCapability) -> Result { - if raw.id != CAP_ID_MSI { return Err("not an MSI capability"); } - if raw.data.len() < 6 { return Err("MSI data too short"); } + if raw.id != CAP_ID_MSI { return Err("не возможность MSI"); } + if raw.data.len() < 6 { return Err("данные MSI слишком короткие"); } Ok(ValidCapability { id: raw.id, next_ptr: raw.next_ptr, data: raw.data, _kind: PhantomData, @@ -476,9 +472,9 @@ impl TryFrom for ValidCapability { } } -// (Similar TryFrom impls for MsiX, PciExpress — omitted for brevity) +// (Аналогичные реализации TryFrom для MsiX и PciExpress опущены для краткости) -// --- Type-safe accessors: only available on the correct capability --- +// --- Типобезопасные методы доступа: доступны только для правильной возможности --- impl ValidCapability { pub fn pm_control(&self) -> u16 { u16::from_le_bytes([self.data[0], self.data[1]]) @@ -500,7 +496,7 @@ impl ValidCapability { } } -// --- Capability walker: iterates the linked list --- +// --- Обходчик возможностей: проходит по связному списку --- pub struct CapabilityWalker<'a> { config_space: &'a [u8], next_ptr: u8, @@ -508,7 +504,7 @@ pub struct CapabilityWalker<'a> { impl<'a> CapabilityWalker<'a> { pub fn new(config_space: &'a [u8]) -> Self { - // Capability pointer lives at offset 0x34 in PCI config space + // Указатель на первую возможность находится по смещению 0x34 в конфигурационном пространстве PCI let first_ptr = if config_space.len() > 0x34 { config_space[0x34] } else { 0 }; @@ -533,41 +529,39 @@ impl<'a> Iterator for CapabilityWalker<'a> { } } -// Usage: +// Использование: // for raw_cap in CapabilityWalker::new(&config_space) { // if let Ok(pm) = ValidCapability::::try_from(raw_cap) { -// println!("PM control: 0x{:04X}", pm.pm_control()); +// println!("Управление PM: 0x{:04X}", pm.pm_control()); // } // } ``` -**Key points:** -- `RawCapability` → `ValidCapability` is the parse-don't-validate boundary. -- `pm_control()` only exists on `ValidCapability` — calling it on an MSI - capability is a compile error. -- The `CapabilityWalker` iterator yields raw capabilities; the caller validates - the ones they care about with `TryFrom`. +**Ключевые моменты:** +- `RawCapability` → `ValidCapability` — граница parse-don't-validate. +- `pm_control()` существует только у `ValidCapability`: вызов его у возможности MSI — **ошибка компиляции**. +- Итератор `CapabilityWalker` выдаёт сырые возможности; вызывающий код проверяет нужные ему через `TryFrom`.
-### Exercise 5: Multi-Protocol Health Check (Capability Mixins) +### Упражнение 5: проверка здоровья по нескольким протоколам (capability-миксины) -Create a health-check framework: +Создайте фреймворк проверки здоровья: -1. Define ingredient traits: `HasIpmi`, `HasRedfish`, `HasNvmeCli`, `HasGpio` -2. Create mixin traits: - - `ThermalHealthMixin` (requires HasIpmi + HasGpio) — reads temps, checks alerts - - `StorageHealthMixin` (requires HasNvmeCli) — SMART data checks - - `BmcHealthMixin` (requires HasIpmi + HasRedfish) — cross-validates BMC data -3. Build a `FullPlatformController` that implements all ingredient traits -4. Build a `StorageOnlyController` that only implements `HasNvmeCli` -5. Verify that `StorageOnlyController` gets `StorageHealthMixin` but NOT the others +1. Определите трейты-ингредиенты: `HasIpmi`, `HasRedfish`, `HasNvmeCli`, `HasGpio` +2. Создайте трейты-миксины: + - `ThermalHealthMixin` (требует HasIpmi + HasGpio): читает температуры, проверяет сигналы тревоги + - `StorageHealthMixin` (требует HasNvmeCli): проверки данных SMART + - `BmcHealthMixin` (требует HasIpmi + HasRedfish): перекрёстная проверка данных BMC +3. Постройте `FullPlatformController`, который реализует все трейты-ингредиенты +4. Постройте `StorageOnlyController`, который реализует только `HasNvmeCli` +5. Убедитесь, что `StorageOnlyController` получает `StorageHealthMixin`, но НЕ остальные
-Sample Solution (Exercise 5) +Пример решения (упражнение 5) ```rust,ignore -// --- Ingredient traits --- +// --- Трейты-ингредиенты --- pub trait HasIpmi { fn ipmi_read_sensor(&self, id: u8) -> f64; } @@ -586,7 +580,7 @@ pub struct SmartData { pub spare_pct: u8, } -// --- Mixin traits with blanket impls --- +// --- Трейты-миксины с blanket impl --- pub trait ThermalHealthMixin: HasIpmi + HasGpio { fn thermal_check(&self) -> ThermalStatus { let temp = self.ipmi_read_sensor(0x01); @@ -620,7 +614,7 @@ pub struct ThermalStatus { pub temperature: f64, pub alert_active: bool } pub struct StorageStatus { pub temperature_ok: bool, pub spare_ok: bool } pub struct BmcStatus { pub ipmi_temp: f64, pub redfish_temp: String, pub consistent: bool } -// --- Full platform: all ingredients → all three mixins for free --- +// --- Полная платформа: все ингредиенты → все три миксина бесплатно --- pub struct FullPlatformController; impl HasIpmi for FullPlatformController { @@ -638,7 +632,7 @@ impl HasGpio for FullPlatformController { fn gpio_read_alert(&self, _pin: u8) -> bool { false } } -// --- Storage-only: only HasNvmeCli → only StorageHealthMixin --- +// --- Только хранилище: только HasNvmeCli → только StorageHealthMixin --- pub struct StorageOnlyController; impl HasNvmeCli for StorageOnlyController { @@ -647,43 +641,39 @@ impl HasNvmeCli for StorageOnlyController { } } -// StorageOnlyController automatically gets storage_check(). -// Calling thermal_check() or bmc_health() on it is a COMPILE ERROR. +// StorageOnlyController автоматически получает storage_check(). +// Вызов thermal_check() или bmc_health() у него — ОШИБКА КОМПИЛЯЦИИ. ``` -**Key points:** -- Blanket `impl ThermalHealthMixin for T {}` — any type that - implements both ingredients automatically gets the mixin. -- `StorageOnlyController` only implements `HasNvmeCli`, so the compiler grants it - `StorageHealthMixin` but rejects `thermal_check()` and `bmc_health()` — zero - runtime checks needed. -- Adding a new mixin (e.g., `NetworkHealthMixin: HasRedfish + HasGpio`) is one trait - + one blanket impl — existing controllers pick it up automatically if they qualify. +**Ключевые моменты:** +- Blanket `impl ThermalHealthMixin for T {}`: любой тип, реализующий оба ингредиента, автоматически получает миксин. +- `StorageOnlyController` реализует только `HasNvmeCli`, поэтому компилятор даёт ему `StorageHealthMixin`, но отвергает `thermal_check()` и `bmc_health()`: никаких проверок во время выполнения не нужно. +- Добавление нового миксина (например, `NetworkHealthMixin: HasRedfish + HasGpio`) — это один трейт и одна blanket impl: существующие контроллеры получат его автоматически, если подходят.
-### Exercise 6: Session-Typed Diagnostic Protocol (Single-Use + Type-State) +### Упражнение 6: диагностический протокол с сессионными типами (одноразовые типы + typestate) -Design a diagnostic session with single-use test execution tokens: +Спроектируйте диагностическую сессию с одноразовыми токенами выполнения тестов: -1. `DiagSession` starts in `Setup` state -2. Transition to `Running` state — issues `N` execution tokens (one per test case) -3. Each `TestToken` is consumed when the test runs — prevents running the same test twice -4. After all tokens are consumed, transition to `Complete` state -5. Generate a report (only in `Complete` state) +1. `DiagSession` начинает в состоянии `Setup` +2. Переход в состояние `Running` выдаёт `N` токенов выполнения (по одному на каждый тестовый случай) +3. Каждый `TestToken` потребляется при запуске теста, что исключает повторный запуск одного и того же теста +4. Когда все токены потреблены, переход в состояние `Complete` +5. Формирование отчёта (только в состоянии `Complete`) -**Advanced:** Use a const generic `N` to track how many tests remain at the type level. +**Продвинутый вариант:** используйте const-обобщение `N`, чтобы отслеживать на уровне типов, сколько тестов осталось.
-Sample Solution (Exercise 6) +Пример решения (упражнение 6) ```rust,ignore -// --- State types --- +// --- Типы состояний --- pub struct Setup; pub struct Running; pub struct Complete; -/// Single-use test token. NOT Clone, NOT Copy — consumed on use. +/// Одноразовый токен теста. НЕ Clone, НЕ Copy: потребляется при использовании. pub struct TestToken { test_name: String, } @@ -709,7 +699,7 @@ impl DiagSession { } } - /// Transition to Running — issues one token per test case. + /// Переход в Running: выдаёт по одному токену на каждый тестовый случай. pub fn start(self, test_names: &[&str]) -> (DiagSession, Vec) { let tokens = test_names.iter() .map(|n| TestToken { test_name: n.to_string() }) @@ -726,9 +716,9 @@ impl DiagSession { } impl DiagSession { - /// Consume a token to run one test. The move prevents double-running. + /// Потребляет токен для запуска одного теста. Перемещение исключает двойной запуск. pub fn run_test(mut self, token: TestToken) -> Self { - let passed = true; // real code runs actual diagnostics here + let passed = true; // реальный код здесь запускает настоящую диагностику self.results.push(TestResult { test_name: token.test_name, passed, @@ -736,14 +726,13 @@ impl DiagSession { self } - /// Transition to Complete. + /// Переход в Complete. /// - /// **Note:** This solution does NOT enforce that all tokens have been - /// consumed — `finish()` can be called with tokens still outstanding. - /// The tokens will simply be dropped (they're not `#[must_use]`). - /// For full compile-time enforcement, use the const-generic variant - /// described in the "Advanced" note below, where `finish()` is only - /// available on `DiagSession`. + /// **Примечание:** это решение НЕ обеспечивает, что все токены потреблены: + /// `finish()` можно вызвать, когда токены ещё остались. Такие токены просто + /// уничтожатся (они не помечены `#[must_use]`). Для полной проверки на этапе компиляции + /// используйте вариант с const-обобщением из примечания «Продвинутый» ниже, где + /// `finish()` доступен только у `DiagSession`. pub fn finish(self) -> DiagSession { DiagSession { name: self.name, @@ -754,46 +743,41 @@ impl DiagSession { } impl DiagSession { - /// Report is ONLY available in Complete state. + /// Отчёт доступен ТОЛЬКО в состоянии Complete. pub fn report(&self) -> String { let total = self.results.len(); let passed = self.results.iter().filter(|r| r.passed).count(); - format!("{}: {}/{} passed", self.name, passed, total) + format!("{}: пройдено {}/{}", self.name, passed, total) } } -// Usage: -// let session = DiagSession::new("GPU stress"); +// Использование: +// let session = DiagSession::new("стресс-тест GPU"); // let (mut session, tokens) = session.start(&["vram", "compute", "thermal"]); // for token in tokens { // session = session.run_test(token); // } // let session = session.finish(); -// println!("{}", session.report()); // "GPU stress: 3/3 passed" +// println!("{}", session.report()); // "стресс-тест GPU: пройдено 3/3" // -// // These would NOT compile: -// // session.run_test(used_token); → ERROR: use of moved value -// // running_session.report(); → ERROR: no method `report` on DiagSession +// // Это НЕ скомпилируется: +// // session.run_test(used_token); → ОШИБКА: use of moved value +// // running_session.report(); → ОШИБКА: no method `report` on DiagSession ``` -**Key points:** -- `TestToken` is not `Clone` or `Copy` — consuming it via `run_test(token)` moves it, - so re-running the same test is a compile error. -- `report()` only exists on `DiagSession` — calling it mid-run is impossible. -- The **Advanced** variant would use `DiagSession` with const generics - where `run_test` returns `DiagSession` and `finish` is only - available on `DiagSession` — that ensures *all* tokens are consumed - before finishing. +**Ключевые моменты:** +- `TestToken` не реализует `Clone` и `Copy`: потребление через `run_test(token)` перемещает его, поэтому повторный запуск того же теста — ошибка компиляции. +- `report()` существует только у `DiagSession`: вызвать его в процессе выполнения невозможно. +- **Продвинутый** вариант использовал бы `DiagSession` с const-обобщениями, где `run_test` возвращает `DiagSession`, а `finish` доступен только у `DiagSession`. Это гарантирует, что *все* токены потреблены до завершения.
-## Key Takeaways +## Ключевые выводы -1. **Practice with realistic protocols** — NVMe, firmware update, sensor pipelines, PCIe are all real-world targets for these patterns. -2. **Each exercise maps to a core chapter** — use the cross-references to review the pattern before attempting. -3. **Solutions use expandable details** — try each exercise before revealing the solution. -4. **Compose patterns in exercise 5** — multi-protocol health checks combine typed commands, dimensional types, and validated boundaries. -5. **Session types (exercise 6) are the frontier** — they enforce message ordering across channels, extending type-state to distributed systems. +1. **Практикуйтесь на реалистичных протоколах**: NVMe, обновление прошивки, конвейеры датчиков и PCIe — реальные цели для этих паттернов. +2. **Каждое упражнение соответствует основной главе**: используйте перекрёстные ссылки, чтобы повторить паттерн перед попыткой. +3. **Решения спрятаны в раскрывающихся блоках**: попробуйте каждое упражнение, прежде чем открывать решение. +4. **Сочетайте паттерны в упражнении 5**: проверки здоровья по нескольким протоколам объединяют типизированные команды, размерные типы и проверенные границы. +5. **Сессионные типы (упражнение 6) — передний край**: они обеспечивают порядок сообщений в каналах, распространяя typestate на распределённые системы. --- - diff --git a/type-driven-correctness-book/src/ch13-reference-card.md b/type-driven-correctness-book/src/ch13-reference-card.md index ed34be1..1632e82 100644 --- a/type-driven-correctness-book/src/ch13-reference-card.md +++ b/type-driven-correctness-book/src/ch13-reference-card.md @@ -1,119 +1,118 @@ -# Reference Card +# Краткая шпаргалка -> **Quick-reference for all 14+ correct-by-construction patterns** with selection flowchart, pattern catalogue, composition rules, crate mapping, and Curry-Howard cheat sheet. +> **Краткая справка по всем 14+ паттернам корректности по построению** с блок-схемой выбора, каталогом паттернов, правилами композиции, сопоставлением с крейтами и шпаргалкой по соответствию Карри — Говарда. > -> **Cross-references:** Every chapter — this is the lookup table for the entire book. +> **Перекрёстные ссылки:** все главы: это таблица для поиска по всей книге. -## Quick Reference: Correct-by-Construction Patterns +## Краткая справка: паттерны корректности по построению -### Pattern Selection Guide +### Руководство по выбору паттерна ```text -Is the bug catastrophic if missed? -├── Yes → Can it be encoded in types? -│ ├── Yes → USE CORRECT-BY-CONSTRUCTION -│ └── No → Runtime check + extensive testing -└── No → Runtime check is fine +Ошибка катастрофична, если её пропустить? +├── Да → Её можно закодировать в типах? +│ ├── Да → ИСПОЛЬЗУЙТЕ КОРРЕКТНОСТЬ ПО ПОСТРОЕНИЮ +│ └── Нет → Проверка во время выполнения + обширное тестирование +└── Нет → Проверка во время выполнения подойдёт ``` -### Pattern Catalogue - -| # | Pattern | Key Trait/Type | Prevents | Runtime Cost | Chapter | -|---|---------|---------------|----------|:------:|---------| -| 1 | Typed Commands | `trait IpmiCmd { type Response; }` | Wrong response type | Zero | ch02 | -| 2 | Single-Use Types | `struct Nonce` (not Clone/Copy) | Nonce/key reuse | Zero | ch03 | -| 3 | Capability Tokens | `struct AdminToken { _private: () }` | Unauthorised access | Zero | ch04 | -| 4 | Type-State | `Session` | Protocol violations | Zero | ch05 | -| 5 | Dimensional Types | `struct Celsius(f64)` | Unit confusion | Zero | ch06 | -| 6 | Validated Boundaries | `struct ValidFru` (via TryFrom) | Unvalidated data use | Parse once | ch07 | -| 7 | Capability Mixins | `trait FanDiagMixin: HasSpi + HasI2c` | Missing bus access | Zero | ch08 | -| 8 | Phantom Types | `Register` | Width/direction mismatch | Zero | ch09 | -| 9 | Sentinel → Option | `Option` (not `0xFF`) | Sentinel-as-value bugs | Zero | ch11 | -| 10 | Sealed Traits | `trait Cmd: private::Sealed` | Unsound external impls | Zero | ch11 | -| 11 | Non-Exhaustive Enums | `#[non_exhaustive] enum Sku` | Silent match fallthrough | Zero | ch11 | -| 12 | Typestate Builder | `DerBuilder` | Incomplete construction | Zero | ch11 | -| 13 | FromStr Validation | `impl FromStr for DiagLevel` | Unvalidated string input | Parse once | ch11 | -| 14 | Const-Generic Size | `RegisterBank` | Buffer size mismatch | Zero | ch11 | -| 15 | Safe `unsafe` Wrapper | `MmioRegion::read_u32()` | Unchecked MMIO/FFI | Zero | ch11 | -| 16 | Async Type-State | `AsyncSession` | Async protocol violations | Zero | ch11 | -| 17 | Const Assertions | `SdrSensorId` | Invalid compile-time IDs | Zero | ch11 | -| 18 | Session Types | `Chan` | Out-of-order channel ops | Zero | ch11 | -| 19 | Pin Self-Referential | `Pin>` | Dangling intra-struct pointer | Zero | ch11 | -| 20 | RAII / Drop | `impl Drop for Session` | Resource leak on any exit path | Zero | ch11 | -| 21 | Error Type Hierarchy | `#[derive(Error)] enum DiagError` | Silent error swallowing | Zero | ch11 | -| 22 | `#[must_use]` | `#[must_use] struct Token` | Silently dropped values | Zero | ch11 | - -### Composition Rules +### Каталог паттернов + +| № | Паттерн | Ключевой трейт/тип | Предотвращает | Стоимость во время выполнения | Глава | +|---|---------|--------------------|---------------|:-----------------------------:|-------| +| 1 | Типизированные команды | `trait IpmiCmd { type Response; }` | Неверный тип ответа | Нулевая | гл. 02 | +| 2 | Одноразовые типы | `struct Nonce` (не Clone/Copy) | Повторное использование nonce/ключа | Нулевая | гл. 03 | +| 3 | Capability-токены | `struct AdminToken { _private: () }` | Несанкционированный доступ | Нулевая | гл. 04 | +| 4 | Typestate | `Session` | Нарушения протокола | Нулевая | гл. 05 | +| 5 | Размерные типы | `struct Celsius(f64)` | Путаница единиц | Нулевая | гл. 06 | +| 6 | Проверенные границы | `struct ValidFru` (через TryFrom) | Использование непроверенных данных | Разбор один раз | гл. 07 | +| 7 | Capability-миксины | `trait FanDiagMixin: HasSpi + HasI2c` | Отсутствие доступа к шине | Нулевая | гл. 08 | +| 8 | Phantom-типы | `Register` | Несоответствие ширины/направления | Нулевая | гл. 09 | +| 9 | Sentinel → Option | `Option` (не `0xFF`) | Ошибки «sentinel как значение» | Нулевая | гл. 11 | +| 10 | Sealed-трейты | `trait Cmd: private::Sealed` | Небезопасные внешние реализации | Нулевая | гл. 11 | +| 11 | Non-exhaustive enum | `#[non_exhaustive] enum Sku` | Тихое проваливание в match | Нулевая | гл. 11 | +| 12 | Билдер с typestate | `DerBuilder` | Неполное создание объекта | Нулевая | гл. 11 | +| 13 | Валидация через FromStr | `impl FromStr for DiagLevel` | Непроверенный строковый ввод | Разбор один раз | гл. 11 | +| 14 | Размер через const-обобщения | `RegisterBank` | Несоответствие размера буфера | Нулевая | гл. 11 | +| 15 | Безопасная обёртка `unsafe` | `MmioRegion::read_u32()` | Непроверенный MMIO/FFI | Нулевая | гл. 11 | +| 16 | Async typestate | `AsyncSession` | Нарушения async-протокола | Нулевая | гл. 11 | +| 17 | Const-утверждения | `SdrSensorId` | Недопустимые ID на этапе компиляции | Нулевая | гл. 11 | +| 18 | Сессионные типы | `Chan` | Операции с каналом не по порядку | Нулевая | гл. 11 | +| 19 | Самоссылочный Pin | `Pin>` | Висячий внутренний указатель | Нулевая | гл. 11 | +| 20 | RAII / Drop | `impl Drop for Session` | Утечка ресурса при любом пути выхода | Нулевая | гл. 11 | +| 21 | Иерархия типов ошибок | `#[derive(Error)] enum DiagError` | Тихое проглатывание ошибок | Нулевая | гл. 11 | +| 22 | `#[must_use]` | `#[must_use] struct Token` | Тихо отброшенные значения | Нулевая | гл. 11 | + +### Правила композиции ```text -Capability Token + Type-State = Authorised state transitions -Typed Command + Dimensional Type = Physically-typed responses -Validated Boundary + Phantom Type = Typed register access on validated config -Capability Mixin + Typed Command = Bus-aware typed operations -Single-Use Type + Type-State = Consume-on-transition protocols -Sealed Trait + Typed Command = Closed, sound command set -Sentinel → Option + Validated Boundary = Clean parse-once pipeline -Typestate Builder + Capability Token = Proof-of-complete construction -FromStr + #[non_exhaustive] = Evolvable, fail-fast enum parsing -Const-Generic Size + Validated Boundary = Sized, validated protocol buffers -Safe unsafe Wrapper + Phantom Type = Typed, safe MMIO access -Async Type-State + Capability Token = Authorised async transitions -Session Types + Typed Command = Fully-typed request-response channels -Pin + Type-State = Self-referential state machines that can't move -RAII (Drop) + Type-State = State-dependent cleanup guarantees -Error Hierarchy + Validated Boundary = Typed parse errors with exhaustive handling -#[must_use] + Single-Use Type = Hard-to-ignore, hard-to-reuse tokens +Capability-токен + Typestate = Авторизованные переходы состояний +Типизированная команда + Размерный тип = Физически типизированные ответы +Проверенная граница + Phantom-тип = Типизированный доступ к регистрам по проверенной конфигурации +Capability-миксин + Типизированная команда = Типизированные операции с учётом шин +Одноразовый тип + Typestate = Протоколы «потребляется при переходе» +Sealed-трейт + Типизированная команда = Закрытый, корректный набор команд +Sentinel → Option + Проверенная граница = Чистый конвейер «разбор один раз» +Билдер с typestate + Capability-токен = Доказательство полного создания +FromStr + #[non_exhaustive] = Развиваемый разбор enum с быстрым отказом +Размер через const-обобщения + Проверенная граница = Размерные, проверенные протокольные буферы +Безопасная обёртка unsafe + Phantom-тип = Типизированный и безопасный доступ к MMIO +Async typestate + Capability-токен = Авторизованные async-переходы +Сессионные типы + Типизированная команда = Полностью типизированные каналы «запрос — ответ» +Pin + Typestate = Самоссылочные автоматы состояний, которые не могут переместиться +RAII (Drop) + Typestate = Гарантии очистки, зависящей от состояния +Иерархия ошибок + Проверенная граница = Типизированные ошибки разбора с исчерпывающей обработкой +#[must_use] + Одноразовый тип = Токены, которые трудно игнорировать и трудно переиспользовать ``` -### Anti-Patterns to Avoid - -| Anti-Pattern | Why It's Wrong | Correct Alternative | -|-------------|---------------|-------------------| -| `fn read_sensor() -> f64` | Unitless — could be °C, °F, or RPM | `fn read_sensor() -> Celsius` | -| `fn encrypt(nonce: &[u8; 12])` | Nonce can be reused (borrow) | `fn encrypt(nonce: Nonce)` (move) | -| `fn admin_op(is_admin: bool)` | Caller can lie (`true`) | `fn admin_op(_: &AdminToken)` | -| `fn send(session: &Session)` | No state guarantee | `fn send(session: &Session)` | -| `fn process(data: &[u8])` | Not validated | `fn process(data: &ValidFru)` | -| `Clone` on ephemeral keys | Defeats single-use guarantee | Don't derive Clone | -| `let vendor_id: u16 = 0xFFFF` | Sentinel carried internally | `let vendor_id: Option = None` | -| `fn route(level: &str)` with fallback | Typos silently default | `let level: DiagLevel = s.parse()?` | -| `Builder::new().finish()` without fields | Incomplete object constructed | Typestate builder: `finish()` gated on `Set` | -| `let buf: Vec` for fixed-size HW buffer | Size only checked at runtime | `RegisterBank<4096>` (const generic) | -| Raw `unsafe { ptr::read(...) }` scattered | UB risk, unauditable | `MmioRegion::read_u32()` safe wrapper | -| `async fn transition(&mut self)` | Mutable borrows don't enforce state | `async fn transition(self) -> NextState` | -| `fn cleanup()` called manually | Forgotten on early return / panic | `impl Drop` — compiler inserts call | -| `fn op() -> Result` | Opaque error, no variant matching | `fn op() -> Result` enum | - -### Mapping to a Diagnostics Codebase - -| Module | Applicable Pattern(s) | -|---------------------|----------------------| -| `protocol_lib` | Typed commands, type-state sessions | -| `thermal_diag` | Capability mixins, dimensional types | -| `accel_diag` | Validated boundaries, phantom registers | -| `network_diag` | Type-state (link training), capability tokens | -| `pci_topology` | Phantom types (register width), validated config, sentinel → Option | -| `event_handler` | Single-use audit tokens, capability tokens, FromStr (Component) | -| `event_log` | Validated boundaries (SEL record parsing) | -| `compute_diag` | Dimensional types (temperature, frequency) | -| `memory_diag` | Validated boundaries (SPD data), dimensional types | -| `switch_diag` | Type-state (port enumeration), phantom types | +### Антипаттерны, которых следует избегать + +| Антипаттерн | Почему это неверно | Правильная альтернатива | +|-------------|--------------------|-------------------------| +| `fn read_sensor() -> f64` | Без единиц: может быть °C, °F или об/мин | `fn read_sensor() -> Celsius` | +| `fn encrypt(nonce: &[u8; 12])` | Nonce можно переиспользовать (заимствование) | `fn encrypt(nonce: Nonce)` (перемещение) | +| `fn admin_op(is_admin: bool)` | Вызывающий может соврать (`true`) | `fn admin_op(_: &AdminToken)` | +| `fn send(session: &Session)` | Нет гарантии состояния | `fn send(session: &Session)` | +| `fn process(data: &[u8])` | Не проверено | `fn process(data: &ValidFru)` | +| `Clone` для эфемерных ключей | Сводит на нет гарантию одноразовости | Не выводите Clone | +| `let vendor_id: u16 = 0xFFFF` | Sentinel хранится внутри | `let vendor_id: Option = None` | +| `fn route(level: &str)` с запасным вариантом | Опечатки тихо заменяются значением по умолчанию | `let level: DiagLevel = s.parse()?` | +| `Builder::new().finish()` без полей | Создаётся неполный объект | Билдер с typestate: `finish()` открывается при `Set` | +| `let buf: Vec` для аппаратного буфера фиксированного размера | Размер проверяется только во время выполнения | `RegisterBank<4096>` (const-обобщение) | +| Разбросанный сырой `unsafe { ptr::read(...) }` | Риск UB, не поддаётся аудиту | `MmioRegion::read_u32()`: безопасная обёртка | +| `async fn transition(&mut self)` | Изменяемые заимствования не обеспечивают состояние | `async fn transition(self) -> NextState` | +| `fn cleanup()`, вызываемая вручную | Забывается при раннем возврате или панике | `impl Drop`: компилятор вставляет вызов | +| `fn op() -> Result` | Непрозрачная ошибка, нельзя сопоставить варианты | `fn op() -> Result`: enum | + +### Сопоставление с кодовой базой диагностики + +| Модуль | Применимые паттерны | +|--------|---------------------| +| `protocol_lib` | Типизированные команды, сессии с typestate | +| `thermal_diag` | Capability-миксины, размерные типы | +| `accel_diag` | Проверенные границы, phantom-регистры | +| `network_diag` | Typestate (обучение линка), capability-токены | +| `pci_topology` | Phantom-типы (ширина регистра), проверенная конфигурация, sentinel → Option | +| `event_handler` | Одноразовые токены аудита, capability-токены, FromStr (Component) | +| `event_log` | Проверенные границы (разбор записей SEL) | +| `compute_diag` | Размерные типы (температура, частота) | +| `memory_diag` | Проверенные границы (данные SPD), размерные типы | +| `switch_diag` | Typestate (перечисление портов), phantom-типы | | `config_loader` | FromStr (DiagLevel, FaultStatus, DiagAction) | -| `log_analyzer` | Validated boundaries (CompiledPatterns) | -| `diag_framework` | Typestate builder (DerBuilder), session types (orchestrator↔worker) | -| `topology_lib` | Const-generic register banks, safe MMIO wrappers | - -### Curry-Howard Cheat Sheet - -| Logic Concept | Rust Equivalent | Example | -|--------------|----------------|---------| -| Proposition | Type | `AdminToken` | -| Proof | Value of that type | `let tok = authenticate()?;` | -| Implication (A → B) | Function `fn(A) -> B` | `fn activate(AdminToken) -> Session` | -| Conjunction (A ∧ B) | Tuple `(A, B)` or multi-param | `fn op(a: &AdminToken, b: &LinkTrained)` | -| Disjunction (A ∨ B) | `enum { A(A), B(B) }` or `Result` | `Result, Error>` | -| True | `()` (unit type) | Always constructible | -| False | `!` (never type) or `enum Void {}` | Can never be constructed | +| `log_analyzer` | Проверенные границы (CompiledPatterns) | +| `diag_framework` | Билдер с typestate (DerBuilder), сессионные типы (оркестратор ↔ воркер) | +| `topology_lib` | Const-обобщённые банки регистров, безопасные обёртки MMIO | + +### Шпаргалка по соответствию Карри — Говарда + +| Логическое понятие | Эквивалент в Rust | Пример | +|--------------------|-------------------|--------| +| Высказывание | Тип | `AdminToken` | +| Доказательство | Значение этого типа | `let tok = authenticate()?;` | +| Импликация (A → B) | Функция `fn(A) -> B` | `fn activate(AdminToken) -> Session` | +| Конъюнкция (A ∧ B) | Кортеж `(A, B)` или несколько параметров | `fn op(a: &AdminToken, b: &LinkTrained)` | +| Дизъюнкция (A ∨ B) | `enum { A(A), B(B) }` или `Result` | `Result, Error>` | +| Истина | `()` (тип unit) | Всегда конструируется | +| Ложь | `!` (тип never) или `enum Void {}` | Никогда не может быть сконструирована | --- - diff --git a/type-driven-correctness-book/src/ch14-testing-type-level-guarantees.md b/type-driven-correctness-book/src/ch14-testing-type-level-guarantees.md index 4f7eff3..3ad3b3d 100644 --- a/type-driven-correctness-book/src/ch14-testing-type-level-guarantees.md +++ b/type-driven-correctness-book/src/ch14-testing-type-level-guarantees.md @@ -1,24 +1,18 @@ -# Testing Type-Level Guarantees 🟡 +# Тестирование гарантий на уровне типов 🟡 -> **What you'll learn:** How to test that invalid code *fails to compile* (trybuild), fuzz validated boundaries (proptest), verify RAII invariants, and prove zero-cost abstraction via `cargo-show-asm`. +> **Что вы узнаете:** как проверить, что некорректный код *не компилируется* (trybuild), как подвергнуть фаззингу проверенные границы (proptest), как проверить инварианты RAII и как доказать бесплатность абстракций с помощью `cargo-show-asm`. > -> **Cross-references:** [ch03](ch03-single-use-types-cryptographic-guarantee.md) (compile-fail for nonces), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (proptest for boundaries), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (RAII for sessions) +> **Перекрёстные ссылки:** [гл. 03](ch03-single-use-types-cryptographic-guarantee.md) (compile-fail для nonce), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (proptest для границ), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (RAII для сессий) -## Testing Type-Level Guarantees +## Тестирование гарантий на уровне типов -Correct-by-construction patterns shift bugs from runtime to compile time. But -how do you **test** that invalid code actually fails to compile? And how do you -ensure validated boundaries hold under fuzzing? This chapter covers the testing -tools that complement type-level correctness. +Паттерны корректности по построению переносят ошибки из времени выполнения на этап компиляции. Но как **проверить**, что некорректный код действительно не компилируется? И как убедиться, что проверенные границы выдерживают фаззинг? В этой главе рассматриваются инструменты тестирования, которые дополняют корректность на уровне типов. -### Compile-Fail Tests with `trybuild` +### Compile-fail тесты с `trybuild` -The [`trybuild`](https://crates.io/crates/trybuild) crate lets you assert that -certain code **should not compile**. This is essential for maintaining type-level -invariants across refactors — if someone accidentally adds `Clone` to your -single-use `Nonce`, the compile-fail test catches it. +Крейт [`trybuild`](https://crates.io/crates/trybuild) позволяет утверждать, что определённый код **не должен компилироваться**. Это важно для сохранения инвариантов уровня типов при рефакторинге: если кто-то случайно добавит `Clone` к вашему одноразовому `Nonce`, compile-fail тест это поймает. -**Setup:** +**Настройка:** ```toml # Cargo.toml @@ -26,7 +20,7 @@ single-use `Nonce`, the compile-fail test catches it. trybuild = "1" ``` -**Test file (`tests/compile_fail.rs`):** +**Файл теста (`tests/compile_fail.rs`):** ```rust,ignore #[test] @@ -36,7 +30,7 @@ fn type_safety_tests() { } ``` -**Test case: Nonce reuse must not compile (`tests/ui/nonce_reuse.rs`):** +**Тест: повторное использование nonce не должно компилироваться (`tests/ui/nonce_reuse.rs`):** ```rust,ignore // tests/ui/nonce_reuse.rs @@ -45,13 +39,13 @@ use my_crate::Nonce; fn main() { let nonce = Nonce::new(); encrypt(nonce); - encrypt(nonce); // should fail: use of moved value + encrypt(nonce); // должно быть ошибкой: использование перемещённого значения } fn encrypt(_n: Nonce) {} ``` -**Expected error (`tests/ui/nonce_reuse.stderr`):** +**Ожидаемая ошибка (`tests/ui/nonce_reuse.stderr`):** ```text error[E0382]: use of moved value: `nonce` @@ -61,35 +55,32 @@ error[E0382]: use of moved value: `nonce` | ----- move occurs because `nonce` has type `Nonce`, which does not implement the `Copy` trait 5 | encrypt(nonce); | ----- value moved here -6 | encrypt(nonce); // should fail: use of moved value +6 | encrypt(nonce); // должно быть ошибкой: использование перемещённого значения | ^^^^^ value used here after move ``` -**More compile-fail test cases per chapter:** +**Другие compile-fail тесты по главам:** -| Pattern (Chapter) | Test assertion | File | -|-------------------|---------------|------| -| Single-Use Nonce (ch03) | Can't use nonce twice | `nonce_reuse.rs` | -| Capability Token (ch04) | Can't call `admin_op()` without token | `missing_token.rs` | -| Type-State (ch05) | Can't `send_command()` on `Session` | `wrong_state.rs` | -| Dimensional (ch06) | Can't add `Celsius + Rpm` | `unit_mismatch.rs` | -| Sealed Trait (Trick 2) | External crate can't impl sealed trait | `unseal_attempt.rs` | -| Non-Exhaustive (Trick 3) | External match without wildcard fails | `missing_wildcard.rs` | +| Паттерн (глава) | Проверяемое утверждение | Файл | +|-----------------|-------------------------|------| +| Одноразовый nonce (гл. 03) | Нельзя использовать nonce дважды | `nonce_reuse.rs` | +| Capability-токен (гл. 04) | Нельзя вызвать `admin_op()` без токена | `missing_token.rs` | +| Typestate (гл. 05) | Нельзя вызвать `send_command()` у `Session` | `wrong_state.rs` | +| Размерные типы (гл. 06) | Нельзя сложить `Celsius + Rpm` | `unit_mismatch.rs` | +| Sealed-трейт (приём 2) | Внешний крейт не может реализовать sealed-трейт | `unseal_attempt.rs` | +| Non-exhaustive (приём 3) | Внешний match без подстановки не компилируется | `missing_wildcard.rs` | -**CI integration:** +**Интеграция с CI:** ```yaml # .github/workflows/ci.yml -- name: Run compile-fail tests +- name: Запуск compile-fail тестов run: cargo test --test compile_fail ``` -### Property-Based Testing of Validated Boundaries +### Property-based тестирование проверенных границ -Validated boundaries (ch07) parse data once and reject invalid input. But -how do you know your validation catches **all** invalid inputs? Property-based -testing with [`proptest`](https://crates.io/crates/proptest) generates -thousands of random inputs to stress the boundary: +Проверенные границы (гл. 07) разбирают данные один раз и отвергают некорректный ввод. Но откуда знать, что проверка ловит **все** некорректные входные данные? Property-based тестирование с [`proptest`](https://crates.io/crates/proptest) генерирует тысячи случайных входных данных, чтобы нагрузить границу: ```toml # Cargo.toml @@ -100,42 +91,42 @@ proptest = "1" ```rust,ignore use proptest::prelude::*; -/// From ch07: ValidFru wraps a spec-compliant FRU payload. -/// These tests use the full ch07 ValidFru with board_area(), -/// product_area(), and format_version() methods. -/// Note: ch07 defines TryFrom, so we wrap raw bytes first. +/// Из гл. 07: ValidFru оборачивает полезную нагрузку FRU, соответствующую спецификации. +/// В этих тестах используется полный ValidFru из гл. 07 с методами board_area(), +/// product_area() и format_version(). +/// Примечание: в гл. 07 определён TryFrom, поэтому сначала оборачиваем сырые байты. proptest! { - /// Any byte sequence that passes validation must be usable without panic. + /// Любая последовательность байтов, прошедшая проверку, должна использоваться без паники. #[test] fn valid_fru_never_panics(data in proptest::collection::vec(any::(), 0..1024)) { if let Ok(fru) = ValidFru::try_from(RawFruData(data)) { - // These must never panic on a validated FRU - // (methods from ch07's ValidFru impl): + // Эти методы не должны паниковать для проверенного FRU + // (методы из реализации ValidFru в гл. 07): let _ = fru.format_version(); let _ = fru.board_area(); let _ = fru.product_area(); } } - /// Round-trip: format_version is preserved through reparsing. + /// Круговой тест: format_version сохраняется при повторном разборе. #[test] fn fru_round_trip(data in valid_fru_strategy()) { let raw = RawFruData(data.clone()); let fru = ValidFru::try_from(raw).unwrap(); let version = fru.format_version(); - // Re-parse the same bytes — version must be identical + // Повторно разбираем те же байты: версия должна совпасть let reparsed = ValidFru::try_from(RawFruData(data)).unwrap(); prop_assert_eq!(version, reparsed.format_version()); } } -/// Custom strategy: generates byte vectors that satisfy the FRU spec header. -/// The header format matches ch07's `TryFrom` validation: -/// - Byte 0: version = 0x01 -/// - Bytes 1-6: area offsets (×8 = actual byte offset) -/// - Byte 7: checksum (sum of bytes 0-7 = 0 mod 256) -/// The body is random but large enough for the offsets to be in-bounds. +/// Произвольная стратегия: генерирует векторы байтов, удовлетворяющие заголовку FRU. +/// Формат заголовка соответствует проверке `TryFrom` из гл. 07: +/// - Байт 0: версия = 0x01 +/// - Байты 1–6: смещения областей (×8 = фактическое смещение в байтах) +/// - Байт 7: контрольная сумма (сумма байтов 0–7 = 0 по модулю 256) +/// Тело случайное, но достаточно большое, чтобы смещения оставались в пределах. fn valid_fru_strategy() -> impl Strategy> { let header = vec![0x01, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00]; proptest::collection::vec(any::(), 64..256) @@ -149,32 +140,31 @@ fn valid_fru_strategy() -> impl Strategy> { } ``` -**The testing pyramid for correct-by-construction code:** +**Пирамида тестирования для кода с корректностью по построению:** ```text ┌───────────────────────────────────┐ -│ Compile-Fail Tests (trybuild) │ ← "Invalid code must not compile" +│ Compile-fail тесты (trybuild) │ ← «Некорректный код не должен компилироваться» ├───────────────────────────────────┤ -│ Property Tests (proptest/quickcheck) │ ← "Valid inputs never panic" +│ Property-тесты (proptest/quickcheck) │ ← «Корректные входные данные никогда не вызывают панику» ├───────────────────────────────────┤ -│ Unit Tests (#[test]) │ ← "Specific inputs produce expected outputs" +│ Модульные тесты (#[test]) │ ← «Конкретные входные данные дают ожидаемый результат» ├───────────────────────────────────┤ -│ Type System (patterns ch02–13) │ ← "Entire classes of bugs can't exist" +│ Система типов (гл. 02–13) │ ← «Целые классы ошибок не могут существовать» └───────────────────────────────────┘ ``` -### RAII Verification +### Проверка RAII -RAII (Trick 12) guarantees cleanup. To test this, verify that the `Drop` impl -actually fires: +RAII (приём 12) гарантирует очистку. Чтобы это проверить, убедимся, что реализация `Drop` действительно срабатывает: ```rust,ignore use std::sync::atomic::{AtomicBool, Ordering}; -// NOTE: These tests use a global AtomicBool, so they must not run in -// parallel with each other. Use `#[serial_test::serial]` or run with -// `cargo test -- --test-threads=1`. Alternatively, use a per-test -// `Arc` passed via closure to avoid the global entirely. +// ПРИМЕЧАНИЕ: эти тесты используют глобальный AtomicBool, поэтому их нельзя запускать +// параллельно друг с другом. Используйте `#[serial_test::serial]` или запускайте с +// `cargo test -- --test-threads=1`. Либо используйте `Arc` для каждого теста, +// передаваемый через замыкание, чтобы полностью обойтись без глобального состояния. static DROPPED: AtomicBool = AtomicBool::new(false); struct TestSession; @@ -189,11 +179,11 @@ fn session_drops_on_early_return() { DROPPED.store(false, Ordering::SeqCst); let result: Result<(), &str> = (|| { let _session = TestSession; - Err("simulated failure")?; + Err("имитация сбоя")?; Ok(()) })(); assert!(result.is_err()); - assert!(DROPPED.load(Ordering::SeqCst), "Drop must fire on early return"); + assert!(DROPPED.load(Ordering::SeqCst), "Drop должен сработать при раннем возврате"); } #[test] @@ -201,42 +191,39 @@ fn session_drops_on_panic() { DROPPED.store(false, Ordering::SeqCst); let result = std::panic::catch_unwind(|| { let _session = TestSession; - panic!("simulated panic"); + panic!("имитация паники"); }); assert!(result.is_err()); - assert!(DROPPED.load(Ordering::SeqCst), "Drop must fire on panic"); + assert!(DROPPED.load(Ordering::SeqCst), "Drop должен сработать при панике"); } ``` -### Applying to Your Codebase +### Применение к вашей кодовой базе -Here's a prioritized plan for adding type-level tests to the -workspace: +Вот приоритизированный план добавления тестов на уровне типов в рабочую область: -| Crate | Test type | What to test | -|-------|-----------|-------------| -| `protocol_lib` | Compile-fail | `Session` can't `send_command()` | -| `protocol_lib` | Property | Any byte seq → `TryFrom` either succeeds or returns Err (no panic) | -| `thermal_diag` | Compile-fail | Can't construct `FanReading` without `HasSpi` mixin | -| `accel_diag` | Property | GPU sensor parsing: random bytes → validated-or-rejected | -| `config_loader` | Property | Random strings → `FromStr` for `DiagLevel` never panics | -| `pci_topology` | Compile-fail | `Register` can't be passed where `Width32` expected | -| `event_handler` | Compile-fail | Audit token can't be cloned | -| `diag_framework` | Compile-fail | `DerBuilder` can't call `finish()` | +| Крейт | Тип теста | Что тестировать | +|-------|-----------|-----------------| +| `protocol_lib` | Compile-fail | `Session` не может вызвать `send_command()` | +| `protocol_lib` | Property | Любая последовательность байтов → `TryFrom` либо успешно завершается, либо возвращает Err (без паники) | +| `thermal_diag` | Compile-fail | Нельзя создать `FanReading` без миксина `HasSpi` | +| `accel_diag` | Property | Разбор показаний GPU: случайные байты → либо проверены, либо отвергнуты | +| `config_loader` | Property | Случайные строки → `FromStr` для `DiagLevel` никогда не паникует | +| `pci_topology` | Compile-fail | `Register` нельзя передать туда, где ожидается `Width32` | +| `event_handler` | Compile-fail | Токен аудита нельзя клонировать | +| `diag_framework` | Compile-fail | `DerBuilder` не может вызвать `finish()` | -### Zero-Cost Abstraction: Proof by Assembly +### Бесплатные абстракции: доказательство через ассемблер -A common concern: "Do newtypes and phantom types add runtime overhead?" -The answer is **no** — they compile to identical assembly as raw primitives. -Here's how to verify: +Распространённый вопрос: «Добавляют ли newtype и phantom-типы накладные расходы во время выполнения?» Ответ: **нет**. Они компилируются в такой же ассемблер, как и голые примитивы. Вот как это проверить. -**Setup:** +**Настройка:** ```bash cargo install cargo-show-asm ``` -**Example: Newtype vs raw u32:** +**Пример: newtype против голого u32:** ```rust,ignore // src/lib.rs @@ -246,62 +233,57 @@ pub struct Rpm(pub u32); #[derive(Clone, Copy)] pub struct Celsius(pub f64); -// Newtype arithmetic +// Арифметика через newtype #[inline(never)] pub fn add_rpm(a: Rpm, b: Rpm) -> Rpm { Rpm(a.0 + b.0) } -// Raw arithmetic (for comparison) +// Голая арифметика (для сравнения) #[inline(never)] pub fn add_raw(a: u32, b: u32) -> u32 { a + b } ``` -**Run:** +**Запуск:** ```bash cargo asm my_crate::add_rpm cargo asm my_crate::add_raw ``` -**Result — identical assembly:** +**Результат: одинаковый ассемблер:** ```asm -; add_rpm (newtype) ; add_raw (raw u32) +; add_rpm (newtype) ; add_raw (голый u32) my_crate::add_rpm: my_crate::add_raw: lea eax, [rdi + rsi] lea eax, [rdi + rsi] ret ret ``` -The `Rpm` wrapper is completely erased at compile time. The same holds for -`PhantomData` (zero bytes), `ZST` tokens (zero bytes), and all other -type-level markers used throughout this guide. +Обёртка `Rpm` полностью стирается на этапе компиляции. То же верно для `PhantomData` (ноль байт), токенов `ZST` (ноль байт) и всех остальных маркеров уровня типов, которые используются в этом руководстве. -**Verify for your own types:** +**Проверка своих типов:** ```bash -# Show assembly for a specific function +# Показать ассемблер для конкретной функции cargo asm --lib ipmi_lib::session::execute -# Show that PhantomData adds zero bytes +# Показать, что PhantomData не добавляет байтов cargo asm --lib --rust ipmi_lib::session::IpmiSession ``` -> **Key takeaway:** Every pattern in this guide has **zero runtime cost**. -> The type system does all the work and is erased completely during compilation. -> You get the safety of Haskell with the performance of C. +> **Ключевой вывод:** каждый паттерн в этом руководстве имеет **нулевую стоимость во время выполнения**. Система типов делает всю работу и полностью стирается при компиляции. Вы получаете безопасность Haskell с производительностью C. -## Key Takeaways +## Ключевые выводы -1. **trybuild tests that invalid code won't compile** — essential for maintaining type-level invariants across refactors. -2. **proptest fuzzes validation boundaries** — generates thousands of random inputs to stress `TryFrom` implementations. -3. **RAII verification tests that Drop runs** — Arc counters or mock flags prove cleanup happened. -4. **cargo-show-asm proves zero-cost** — phantom types, ZSTs, and newtypes produce the same assembly as raw C. -5. **Add compile-fail tests for every "impossible" state** — if someone accidentally derives `Clone` on a single-use type, the test catches it. +1. **trybuild проверяет, что некорректный код не компилируется**: это важно для сохранения инвариантов уровня типов при рефакторинге. +2. **proptest подвергает фаззингу границы валидации**: генерирует тысячи случайных входных данных, чтобы нагрузить реализации `TryFrom`. +3. **Проверка RAII подтверждает, что Drop выполняется**: счётчики Arc или флаги-заглушки доказывают, что очистка произошла. +4. **cargo-show-asm доказывает нулевую стоимость**: phantom-типы, ZST и newtype дают тот же ассемблер, что и голый C. +5. **Добавляйте compile-fail тесты для каждого «невозможного» состояния**: если кто-то случайно выведет `Clone` для одноразового типа, тест это поймает. --- -*End of Type-Driven Correctness in Rust* - +*Конец книги «Корректность на уровне типов в Rust»* diff --git a/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md b/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md index 910f10f..5209297 100644 --- a/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md +++ b/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md @@ -1,15 +1,15 @@ -# Const Fn — Compile-Time Correctness Proofs 🟠 +# Const fn: доказательства корректности на этапе компиляции 🟠 -> **What you'll learn:** How `const fn` and `assert!` turn the compiler into a proof engine — verifying SRAM memory maps, register layouts, protocol frames, bitfield masks, clock trees, and lookup tables at compile time with zero runtime cost. +> **Что вы узнаете:** как `const fn` и `assert!` превращают компилятор в движок доказательств: проверка карт памяти SRAM, раскладки регистров, кадров протокола, масок битовых полей, деревьев тактирования и таблиц поиска на этапе компиляции, без затрат во время выполнения. > -> **Cross-references:** [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional analysis), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types) +> **Перекрёстные ссылки:** [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (анализ размерностей), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы) -## The Problem: Memory Maps That Lie +## Проблема: карты памяти, которые лгут -In embedded and systems programming, memory maps are the foundation of everything — they define where bootloaders, firmware, data sections, and stacks live. Get a boundary wrong, and two subsystems silently corrupt each other. In C, these maps are typically `#define` constants with no structural relationship: +Во встраиваемом и системном программировании карты памяти лежат в основе всего: они определяют, где находятся загрузчик, прошивка, секции данных и стеки. Ошибся с границей, и две подсистемы тихо портят друг друга. В C такие карты обычно задаются `#define`-константами без структурной связи: ```c -/* STM32F4 SRAM layout — 256 KB at 0x20000000 */ +/* Раскладка SRAM STM32F4: 256 KB по адресу 0x20000000 */ #define SRAM_BASE 0x20000000 #define SRAM_SIZE (256 * 1024) @@ -20,31 +20,31 @@ In embedded and systems programming, memory maps are the foundation of everythin #define FW_SIZE (128 * 1024) #define DATA_BASE 0x20024000 -#define DATA_SIZE (80 * 1024) /* Someone bumped this from 64K to 80K */ +#define DATA_SIZE (80 * 1024) /* Кто-то поднял это значение с 64K до 80K */ #define STACK_BASE 0x20038000 -#define STACK_SIZE (48 * 1024) /* 0x20038000 + 48K = 0x20044000 — past SRAM end! */ +#define STACK_SIZE (48 * 1024) /* 0x20038000 + 48K = 0x20044000 — за концом SRAM! */ ``` -The bug: `16 + 128 + 80 + 48 = 272 KB`, but SRAM is only 256 KB. The stack extends 16 KB past the end of physical memory. No compiler warning, no linker error, no runtime check — just silent corruption when the stack grows into unmapped space. +Ошибка: `16 + 128 + 80 + 48 = 272 KB`, а SRAM — всего 256 KB. Стек выходит за конец физической памяти на 16 KB. Ни предупреждения компилятора, ни ошибки линкера, ни проверки во время выполнения: только тихое повреждение, когда стек растёт в незанятое адресное пространство. -**Every failure mode is discovered after deployment** — potentially as a mysterious crash that only happens under heavy stack usage, weeks after the data section was resized. +**Каждый режим отказа обнаруживается только после развёртывания**: возможно, как загадочный сбой, который случается лишь при интенсивном использовании стека, через недели после того, как изменили размер секции данных. -## Const Fn: Turning the Compiler into a Proof Engine +## Const fn: превращаем компилятор в движок доказательств -Rust's `const fn` functions can run at compile time. When a `const fn` panics during compile-time evaluation, the panic becomes a **compile error**. Combined with `assert!`, this turns the compiler into a theorem prover for your invariants: +Функции `const fn` в Rust могут выполняться на этапе компиляции. Если `const fn` паникует во время такого вычисления, panic становится **ошибкой компиляции**. В сочетании с `assert!` это превращает компилятор в доказатель теорем для ваших инвариантов: ```rust pub const fn checked_add(a: u32, b: u32) -> u32 { let sum = a as u64 + b as u64; - assert!(sum <= u32::MAX as u64, "overflow"); + assert!(sum <= u32::MAX as u64, "переполнение"); sum as u32 } -// ✅ Compiles — 100 + 200 fits in u32 +// ✅ Компилируется: 100 + 200 помещается в u32 const X: u32 = checked_add(100, 200); -// ❌ Compile error: "overflow" +// ❌ Ошибка компиляции: "переполнение" // const Y: u32 = checked_add(u32::MAX, 1); fn main() { @@ -52,13 +52,13 @@ fn main() { } ``` -> **The key insight:** `const fn` + `assert!` = a proof obligation. Each assertion is a theorem that the compiler must verify. If the proof fails, the program does not compile. No test suite needed, no code review catch — the compiler itself is the auditor. +> **Ключевая идея:** `const fn` + `assert!` = обязательство доказательства. Каждое утверждение — теорема, которую компилятор должен проверить. Если доказательство не сходится, программа не компилируется. Не нужен набор тестов, и код-ревью не нужно: сам компилятор выступает аудитором. -## Building a Verified SRAM Memory Map +## Построение проверенной карты памяти SRAM -### The Region Type +### Тип Region -A `Region` represents a contiguous block of memory. Its constructor is a `const fn` that enforces basic validity: +`Region` представляет непрерывный блок памяти. Его конструктор — `const fn`, который обеспечивает базовую корректность: ```rust #[derive(Debug, Clone, Copy)] @@ -68,12 +68,12 @@ pub struct Region { } impl Region { - /// Create a region. Panics at compile time if invariants fail. + /// Создаёт регион. Паникует на этапе компиляции, если инварианты нарушены. pub const fn new(base: u32, size: u32) -> Self { - assert!(size > 0, "region size must be non-zero"); + assert!(size > 0, "размер региона должен быть ненулевым"); assert!( base as u64 + size as u64 <= u32::MAX as u64, - "region overflows 32-bit address space" + "регион выходит за пределы 32-битного адресного пространства" ); Self { base, size } } @@ -82,33 +82,33 @@ impl Region { self.base + self.size } - /// True if `inner` fits entirely within `self`. + /// Истина, если `inner` целиком помещается внутри `self`. pub const fn contains(&self, inner: &Region) -> bool { inner.base >= self.base && inner.end() <= self.end() } - /// True if two regions share any addresses. + /// Истина, если два региона имеют общие адреса. pub const fn overlaps(&self, other: &Region) -> bool { self.base < other.end() && other.base < self.end() } - /// True if `addr` falls within this region. + /// Истина, если `addr` попадает в этот регион. pub const fn contains_addr(&self, addr: u32) -> bool { addr >= self.base && addr < self.end() } } -// Every Region is born valid — you cannot construct an invalid one +// Регион рождается корректным: некорректный создать нельзя const R: Region = Region::new(0x2000_0000, 1024); fn main() { - println!("Region: {:#010X}..{:#010X}", R.base, R.end()); + println!("Регион: {:#010X}..{:#010X}", R.base, R.end()); } ``` -### The Verified Memory Map +### Проверенная карта памяти -Now we compose regions into a full SRAM map. The constructor proves six overlap-freedom invariants and four containment invariants — all at compile time: +Теперь объединяем регионы в полную карту SRAM. Конструктор доказывает шесть инвариантов непересечения и четыре инварианта вложенности, всё на этапе компиляции: ```rust # #[derive(Debug, Clone, Copy)] @@ -143,75 +143,75 @@ impl SramMap { data: Region, stack: Region, ) -> Self { - // ── Containment: every sub-region fits within total SRAM ── - assert!(total.contains(&bootloader), "bootloader exceeds SRAM"); - assert!(total.contains(&firmware), "firmware exceeds SRAM"); - assert!(total.contains(&data), "data section exceeds SRAM"); - assert!(total.contains(&stack), "stack exceeds SRAM"); - - // ── Overlap freedom: no pair of sub-regions shares an address ── - assert!(!bootloader.overlaps(&firmware), "bootloader/firmware overlap"); - assert!(!bootloader.overlaps(&data), "bootloader/data overlap"); - assert!(!bootloader.overlaps(&stack), "bootloader/stack overlap"); - assert!(!firmware.overlaps(&data), "firmware/data overlap"); - assert!(!firmware.overlaps(&stack), "firmware/stack overlap"); - assert!(!data.overlaps(&stack), "data/stack overlap"); + // ── Вложенность: каждый подрегион помещается в общий объём SRAM ── + assert!(total.contains(&bootloader), "загрузчик выходит за пределы SRAM"); + assert!(total.contains(&firmware), "прошивка выходит за пределы SRAM"); + assert!(total.contains(&data), "секция данных выходит за пределы SRAM"); + assert!(total.contains(&stack), "стек выходит за пределы SRAM"); + + // ── Непересечение: никакие два подрегиона не делят адрес ── + assert!(!bootloader.overlaps(&firmware), "загрузчик/прошивка пересекаются"); + assert!(!bootloader.overlaps(&data), "загрузчик/данные пересекаются"); + assert!(!bootloader.overlaps(&stack), "загрузчик/стек пересекаются"); + assert!(!firmware.overlaps(&data), "прошивка/данные пересекаются"); + assert!(!firmware.overlaps(&stack), "прошивка/стек пересекаются"); + assert!(!data.overlaps(&stack), "данные/стек пересекаются"); Self { total, bootloader, firmware, data, stack } } } -// ✅ All 10 invariants verified at compile time — zero runtime cost +// ✅ Все 10 инвариантов проверены на этапе компиляции — ноль затрат во время выполнения const SRAM: SramMap = SramMap::verified( - Region::new(0x2000_0000, 256 * 1024), // 256 KB total SRAM - Region::new(0x2000_0000, 16 * 1024), // bootloader: 16 KB - Region::new(0x2000_4000, 128 * 1024), // firmware: 128 KB - Region::new(0x2002_4000, 64 * 1024), // data: 64 KB - Region::new(0x2003_4000, 48 * 1024), // stack: 48 KB + Region::new(0x2000_0000, 256 * 1024), // 256 KB всего SRAM + Region::new(0x2000_0000, 16 * 1024), // загрузчик: 16 KB + Region::new(0x2000_4000, 128 * 1024), // прошивка: 128 KB + Region::new(0x2002_4000, 64 * 1024), // данные: 64 KB + Region::new(0x2003_4000, 48 * 1024), // стек: 48 KB ); fn main() { - println!("SRAM: {:#010X} — {} KB", SRAM.total.base, SRAM.total.size / 1024); - println!("Boot: {:#010X} — {} KB", SRAM.bootloader.base, SRAM.bootloader.size / 1024); - println!("FW: {:#010X} — {} KB", SRAM.firmware.base, SRAM.firmware.size / 1024); - println!("Data: {:#010X} — {} KB", SRAM.data.base, SRAM.data.size / 1024); - println!("Stack: {:#010X} — {} KB", SRAM.stack.base, SRAM.stack.size / 1024); + println!("SRAM: {:#010X} — {} KB", SRAM.total.base, SRAM.total.size / 1024); + println!("Загрузчик: {:#010X} — {} KB", SRAM.bootloader.base, SRAM.bootloader.size / 1024); + println!("Прошивка: {:#010X} — {} KB", SRAM.firmware.base, SRAM.firmware.size / 1024); + println!("Данные: {:#010X} — {} KB", SRAM.data.base, SRAM.data.size / 1024); + println!("Стек: {:#010X} — {} KB", SRAM.stack.base, SRAM.stack.size / 1024); } ``` -Ten compile-time checks, zero runtime instructions. The binary contains only the verified constants. +Десять проверок на этапе компиляции, ноль инструкций во время выполнения. Бинарный файл содержит только проверенные константы. -### Breaking the Map +### Нарушаем карту -Suppose someone increases the data section from 64 KB to 80 KB without adjusting anything else: +Допустим, кто-то увеличил секцию данных с 64 KB до 80 KB, ничего больше не меняя: ```rust,ignore -// ❌ Does not compile +// ❌ Не компилируется const BAD_SRAM: SramMap = SramMap::verified( Region::new(0x2000_0000, 256 * 1024), Region::new(0x2000_0000, 16 * 1024), Region::new(0x2000_4000, 128 * 1024), - Region::new(0x2002_4000, 80 * 1024), // 80 KB — 16 KB too large - Region::new(0x2003_8000, 48 * 1024), // stack pushed past SRAM end + Region::new(0x2002_4000, 80 * 1024), // 80 KB: на 16 KB больше, чем нужно + Region::new(0x2003_8000, 48 * 1024), // стек сдвинут за конец SRAM ); ``` -The compiler reports: +Компилятор выдаёт: ```text error[E0080]: evaluation of constant value failed --> src/main.rs:38:9 | -38 | assert!(total.contains(&stack), "stack exceeds SRAM"); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | the evaluated program panicked at 'stack exceeds SRAM' +38 | assert!(total.contains(&stack), "стек выходит за пределы SRAM"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | the evaluated program panicked at 'стек выходит за пределы SRAM' ``` -> **The bug that would have been a mysterious field failure is now a compile error.** No unit test needed, no code review catch — the compiler proves it impossible. Compare this to C, where the same bug would ship silently and surface as a stack corruption months later in the field. +> **Ошибка, которая стала бы загадочным сбоем в полевых условиях, теперь — ошибка компиляции.** Не нужен модульный тест и код-ревью: компилятор доказывает, что такого не может быть. Сравните с C, где та же ошибка тихо попала бы в продукт и через месяцы проявилась бы как повреждение стека в полевых условиях. -## Layering Access Control with Phantom Types +## Контроль доступа с phantom-типами -Combine `const fn` verification with phantom-typed access permissions ([ch09](ch09-phantom-types-for-resource-tracking.md)) to enforce read/write constraints at the type level: +Объедините проверку `const fn` с phantom-типами прав доступа ([гл. 09](ch09-phantom-types-for-resource-tracking.md)), чтобы обеспечивать ограничения чтения и записи на уровне типов: ```rust use std::marker::PhantomData; @@ -227,41 +227,41 @@ pub struct TypedRegion { impl TypedRegion { pub const fn new(base: u32, size: u32) -> Self { - assert!(size > 0, "region size must be non-zero"); + assert!(size > 0, "размер региона должен быть ненулевым"); Self { base, size, _access: PhantomData } } } -// Read is available for any access level +// Чтение доступно для любого уровня доступа fn read_word(region: &TypedRegion, offset: u32) -> u32 { - assert!(offset + 4 <= region.size, "read out of bounds"); - // In real firmware: unsafe { core::ptr::read_volatile((region.base + offset) as *const u32) } - 0 // stub + assert!(offset + 4 <= region.size, "чтение за пределами"); + // В реальной прошивке: unsafe { core::ptr::read_volatile((region.base + offset) as *const u32) } + 0 // заглушка } -// Write requires ReadWrite — the function signature enforces it +// Запись требует ReadWrite: сигнатура функции это обеспечивает fn write_word(region: &TypedRegion, offset: u32, value: u32) { - assert!(offset + 4 <= region.size, "write out of bounds"); - // In real firmware: unsafe { core::ptr::write_volatile(...) } - let _ = value; // stub + assert!(offset + 4 <= region.size, "запись за пределами"); + // В реальной прошивке: unsafe { core::ptr::write_volatile(...) } + let _ = value; // заглушка } const BOOTLOADER: TypedRegion = TypedRegion::new(0x2000_0000, 16 * 1024); const DATA: TypedRegion = TypedRegion::new(0x2002_4000, 64 * 1024); fn main() { - read_word(&BOOTLOADER, 0); // ✅ read from read-only region - read_word(&DATA, 0); // ✅ read from read-write region - write_word(&DATA, 0, 42); // ✅ write to read-write region - // write_word(&BOOTLOADER, 0, 42); // ❌ Compile error: expected ReadWrite, found ReadOnly + read_word(&BOOTLOADER, 0); // ✅ чтение из региона только для чтения + read_word(&DATA, 0); // ✅ чтение из региона чтения-записи + write_word(&DATA, 0, 42); // ✅ запись в регион чтения-записи + // write_word(&BOOTLOADER, 0, 42); // ❌ Ошибка компиляции: ожидается ReadWrite, найден ReadOnly } ``` -The bootloader region is physically writeable (it's SRAM), but the type system prevents accidental writes. This distinction between **hardware capability** and **software permission** is exactly what correct-by-construction means. +Регион загрузчика физически доступен для записи (это SRAM), но система типов предотвращает случайную запись. Именно это различие между **аппаратной возможностью** и **программным разрешением** и означает корректность по построению. -## Pointer Provenance: Proving Addresses Belong to Regions +## Происхождение указателя: доказываем, что адреса принадлежат регионам -Taking it further, we can create verified addresses — values that are statically proven to lie within a specific region: +Двигаясь дальше, можно создавать проверенные адреса: значения, которые статически доказаны как лежащие внутри конкретного региона: ```rust # #[derive(Debug, Clone, Copy)] @@ -277,15 +277,15 @@ Taking it further, we can create verified addresses — values that are statical # addr >= self.base && addr < self.end() # } # } -/// An address proven at compile time to lie within a Region. +/// Адрес, доказанно лежащий внутри Region на этапе компиляции. pub struct VerifiedAddr { - addr: u32, // private — can only be created through the checked constructor + addr: u32, // приватное: создаётся только через проверяющий конструктор } impl VerifiedAddr { - /// Panics at compile time if `addr` is outside `region`. + /// Паникует на этапе компиляции, если `addr` вне `region`. pub const fn new(region: &Region, addr: u32) -> Self { - assert!(region.contains_addr(addr), "address outside region"); + assert!(region.contains_addr(addr), "адрес вне региона"); Self { addr } } @@ -296,40 +296,40 @@ impl VerifiedAddr { const DATA: Region = Region::new(0x2002_4000, 64 * 1024); -// ✅ Proven at compile time to be inside the data region +// ✅ Доказано на этапе компиляции, что адрес внутри региона данных const STATUS_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_4000); const CONFIG_WORD: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2002_5000); -// ❌ Would not compile: address is in the bootloader region, not data +// ❌ Не скомпилируется: адрес лежит в регионе загрузчика, а не данных // const BAD_ADDR: VerifiedAddr = VerifiedAddr::new(&DATA, 0x2000_0000); fn main() { - println!("Status register at {:#010X}", STATUS_WORD.raw()); - println!("Config register at {:#010X}", CONFIG_WORD.raw()); + println!("Регистр статуса по адресу {:#010X}", STATUS_WORD.raw()); + println!("Регистр конфигурации по адресу {:#010X}", CONFIG_WORD.raw()); } ``` -**Provenance established at compile time** — no runtime bounds check needed when accessing these addresses. The constructor is private, so a `VerifiedAddr` can only exist if the compiler has proven it valid. +**Происхождение доказано на этапе компиляции**: при доступе к этим адресам не нужна проверка границ во время выполнения. Конструктор приватный, поэтому `VerifiedAddr` может существовать только тогда, когда компилятор доказал его корректность. -## Beyond Memory Maps +## За пределами карт памяти -The `const fn` proof pattern applies wherever you have **compile-time-known values with structural invariants**. The SRAM map above proved *inter-region* properties (containment, non-overlap). The same technique scales to increasingly fine-grained domains: +Паттерн доказательства с `const fn` применим везде, где есть **известные на этапе компиляции значения со структурными инвариантами**. Карта SRAM выше доказала свойства *между регионами* (вложенность, непересечение). Та же техника масштабируется на всё более мелкие области: ```mermaid flowchart TD - subgraph coarse["Coarse-Grained"] - MEM["Memory Maps
regions don't overlap"] - REG["Register Maps
offsets are aligned & disjoint"] + subgraph coarse["Крупнозернистые"] + MEM["Карты памяти
регионы не перекрываются"] + REG["Карты регистров
смещения выровнены и не пересекаются"] end - subgraph fine["Fine-Grained"] - BIT["Bitfield Layouts
masks are disjoint within a register"] - FRAME["Protocol Frames
fields are contiguous, total ≤ max"] + subgraph fine["Мелкозернистые"] + BIT["Раскладка битовых полей
маски не пересекаются внутри регистра"] + FRAME["Кадры протокола
поля идут подряд, суммарно ≤ максимума"] end - subgraph derived["Derived-Value Chains"] - PLL["Clock Trees / PLL
each intermediate freq in range"] - LUT["Lookup Tables
computed & verified at compile time"] + subgraph derived["Цепочки производных значений"] + PLL["Деревья тактирования / PLL
каждая промежуточная частота в диапазоне"] + LUT["Таблицы поиска
вычисляются и проверяются на этапе компиляции"] end MEM --> REG --> BIT @@ -345,11 +345,11 @@ flowchart TD style LUT fill:#fff3e0,color:#000 ``` -Each subsection below follows the same pattern: define a type with a `const fn` constructor that encodes the invariants, then use `const _: () = { ... }` or a `const` binding to trigger verification. +Каждый подраздел ниже следует одному шаблону: определяется тип с конструктором `const fn`, который кодирует инварианты, а затем используется `const _: () = { ... }` или `const`-привязка для запуска проверки. -### Register Maps +### Карты регистров -Hardware register blocks have fixed offsets and widths. A misaligned or overlapping register definition is always a bug: +Блоки аппаратных регистров имеют фиксированные смещения и ширины. Смещённое или перекрывающееся определение регистра всегда является ошибкой: ```rust #[derive(Debug, Clone, Copy)] @@ -362,9 +362,9 @@ impl Register { pub const fn new(offset: u32, width: u32) -> Self { assert!( width == 1 || width == 2 || width == 4, - "register width must be 1, 2, or 4 bytes" + "ширина регистра должна быть 1, 2 или 4 байта" ); - assert!(offset % width == 0, "register must be naturally aligned"); + assert!(offset % width == 0, "регистр должен быть естественно выровнен"); Self { offset, width } } @@ -377,13 +377,13 @@ const fn disjoint(a: &Register, b: &Register) -> bool { a.end() <= b.offset || b.end() <= a.offset } -// UART peripheral registers +// Регистры периферии UART const DATA: Register = Register::new(0x00, 4); const STATUS: Register = Register::new(0x04, 4); const CTRL: Register = Register::new(0x08, 4); const BAUD: Register = Register::new(0x0C, 4); -// Compile-time proof: no register overlaps another +// Доказательство на этапе компиляции: ни один регистр не перекрывает другой const _: () = { assert!(disjoint(&DATA, &STATUS)); assert!(disjoint(&DATA, &CTRL)); @@ -399,25 +399,25 @@ fn main() { } ``` -Note the `const _: () = { ... };` idiom — an unnamed constant whose only purpose is to run compile-time assertions. If any assertion fails, the constant can't be evaluated and compilation stops. +Обратите внимание на идиому `const _: () = { ... };`: безымянная константа, единственная цель которой — выполнить утверждения на этапе компиляции. Если хотя бы одно утверждение не выполняется, константа не может быть вычислена, и компиляция останавливается. -#### Mini-Exercise: SPI Register Bank +#### Мини-упражнение: банк регистров SPI -Given these SPI controller registers, add const fn assertions proving: -1. Every register is naturally aligned (offset % width == 0) -2. No two registers overlap -3. All registers fit within a 64-byte register block +Дан набор регистров SPI-контроллера. Добавьте `const fn`-утверждения, которые доказывают: +1. Каждый регистр естественно выровнен (offset % width == 0) +2. Никакие два регистра не перекрываются +3. Все регистры помещаются в блок регистров размером 64 байта
-Hint +Подсказка -Reuse the `Register` and `disjoint` functions from the UART example above. Define three or four `const Register` values (e.g., `CTRL` at offset 0x00 width 4, `STATUS` at 0x04 width 4, `TX_DATA` at 0x08 width 1, `RX_DATA` at 0x0C width 1) and assert the three properties. +Используйте повторно `Register` и `disjoint` из примера с UART выше. Определите три-четыре `const Register` (например, `CTRL` со смещением 0x00 и шириной 4, `STATUS` со смещением 0x04 и шириной 4, `TX_DATA` со смещением 0x08 и шириной 1, `RX_DATA` со смещением 0x0C и шириной 1) и проверьте три свойства.
-### Protocol Frame Layouts +### Раскладка кадров протокола -Network or bus protocol frames have fields at specific offsets. The `then()` method makes contiguity structural — gaps and overlaps are impossible by construction: +Кадры сетевых или шинных протоколов содержат поля по определённым смещениям. Метод `then()` делает непрерывность структурной: промежутки и перекрытия невозможны по построению: ```rust #[derive(Debug, Clone, Copy)] @@ -428,7 +428,7 @@ pub struct Field { impl Field { pub const fn new(offset: usize, size: usize) -> Self { - assert!(size > 0, "field size must be non-zero"); + assert!(size > 0, "размер поля должен быть ненулевым"); Self { offset, size } } @@ -436,7 +436,7 @@ impl Field { self.offset + self.size } - /// Create the next field immediately after this one. + /// Создаёт следующее поле сразу после этого. pub const fn then(&self, size: usize) -> Field { Field::new(self.end(), size) } @@ -449,42 +449,42 @@ const SEQ_NUM: Field = HEADER.then(2); const PAYLOAD: Field = SEQ_NUM.then(246); const CRC: Field = PAYLOAD.then(4); -// Compile-time proof: frame fits within maximum size -const _: () = assert!(CRC.end() <= MAX_FRAME, "frame exceeds maximum size"); +// Доказательство на этапе компиляции: кадр помещается в максимальный размер +const _: () = assert!(CRC.end() <= MAX_FRAME, "кадр превышает максимальный размер"); fn main() { - println!("Header: [{}..{})", HEADER.offset, HEADER.end()); - println!("SeqNum: [{}..{})", SEQ_NUM.offset, SEQ_NUM.end()); - println!("Payload: [{}..{})", PAYLOAD.offset, PAYLOAD.end()); - println!("CRC: [{}..{})", CRC.offset, CRC.end()); - println!("Total: {}/{} bytes", CRC.end(), MAX_FRAME); + println!("Заголовок: [{}..{})", HEADER.offset, HEADER.end()); + println!("Номер: [{}..{})", SEQ_NUM.offset, SEQ_NUM.end()); + println!("Полезные данные: [{}..{})", PAYLOAD.offset, PAYLOAD.end()); + println!("CRC: [{}..{})", CRC.offset, CRC.end()); + println!("Итого: {}/{} байт", CRC.end(), MAX_FRAME); } ``` -Fields are contiguous by construction — each starts exactly where the previous one ends. The final assertion proves the frame fits within the protocol's maximum size. +Поля идут непрерывно по построению: каждое начинается ровно там, где заканчивается предыдущее. Последнее утверждение доказывает, что кадр помещается в максимальный размер протокола. -### Inline Const Blocks for Generic Validation +### Встроенные const-блоки для проверки обобщений -Since Rust 1.79, `const { ... }` blocks let you validate const generic parameters at the point of use — perfect for DMA buffer size constraints or alignment requirements: +Начиная с Rust 1.79, блоки `const { ... }` позволяют проверять параметры const-обобщений в месте использования. Это идеально для ограничений размера DMA-буферов и требований к выравниванию: ```rust,ignore fn dma_transfer(buf: &[u8; N]) { - const { assert!(N % 4 == 0, "DMA buffer must be 4-byte aligned in size") }; - const { assert!(N <= 65536, "DMA transfer exceeds maximum size") }; - // ... initiate transfer ... + const { assert!(N % 4 == 0, "размер DMA-буфера должен быть кратен 4 байтам") }; + const { assert!(N <= 65536, "DMA-передача превышает максимальный размер") }; + // ... запускаем передачу ... } -dma_transfer(&[0u8; 1024]); // ✅ 1024 is divisible by 4 and ≤ 65536 -// dma_transfer(&[0u8; 1023]); // ❌ Compile error: not 4-byte aligned +dma_transfer(&[0u8; 1024]); // ✅ 1024 делится на 4 и ≤ 65536 +// dma_transfer(&[0u8; 1023]); // ❌ Ошибка компиляции: не выровнено по 4 байтам ``` -The assertions are evaluated when the function is monomorphized — each call site with a different `N` gets its own compile-time check. +Утверждения вычисляются при мономорфизации функции: каждый вызов с другим `N` получает собственную проверку на этапе компиляции. -### Bitfield Layouts Within a Register +### Раскладка битовых полей внутри регистра -Register maps prove that registers don't *overlap each other* — but what about the **bits within a single register**? Control registers pack multiple fields into one word. If two fields share a bit position, reads and writes silently corrupt each other. In C, this is typically caught (or not) by manual review of mask constants. +Карты регистров доказывают, что регистры не *перекрываются друг с другом*. Но что насчёт **битов внутри одного регистра**? Управляющие регистры упаковывают несколько полей в одно слово. Если два поля занимают один и тот же бит, операции чтения и записи тихо портят друг друга. В C это обычно ловят (или не ловят) вручную, проверяя константы масок. -A `const fn` can prove that every field's mask/shift pair is disjoint from every other field in the same register: +`const fn` может доказать, что пара маска/сдвиг каждого поля не пересекается с парами всех остальных полей того же регистра: ```rust #[derive(Debug, Clone, Copy)] @@ -495,9 +495,9 @@ pub struct BitField { impl BitField { pub const fn new(shift: u8, width: u8) -> Self { - assert!(width > 0, "bit field width must be non-zero"); - assert!(shift as u32 + width as u32 <= 32, "bit field exceeds 32-bit register"); - // Build mask: `width` ones starting at bit `shift` + assert!(width > 0, "ширина битового поля должна быть ненулевой"); + assert!(shift as u32 + width as u32 <= 32, "битовое поле выходит за пределы 32-битного регистра"); + // Строим маску: `width` единиц, начиная с бита `shift` let mask = ((1u64 << width as u64) - 1) as u32; Self { mask: mask << shift as u32, shift } } @@ -507,7 +507,7 @@ impl BitField { } pub const fn encode(&self, value: u32) -> u32 { - assert!(value & !( self.mask >> self.shift as u32 ) == 0, "value exceeds field width"); + assert!(value & !( self.mask >> self.shift as u32 ) == 0, "значение превышает ширину поля"); value << self.shift as u32 } } @@ -516,13 +516,13 @@ const fn fields_disjoint(a: &BitField, b: &BitField) -> bool { a.positioned_mask() & b.positioned_mask() == 0 } -// SPI Control Register fields: enable[0], mode[1:2], clock_div[4:7], irq_en[8] -const SPI_EN: BitField = BitField::new(0, 1); // bit 0 -const SPI_MODE: BitField = BitField::new(1, 2); // bits 1–2 -const SPI_CLKDIV: BitField = BitField::new(4, 4); // bits 4–7 -const SPI_IRQ: BitField = BitField::new(8, 1); // bit 8 +// Поля управляющего регистра SPI: enable[0], mode[1:2], clock_div[4:7], irq_en[8] +const SPI_EN: BitField = BitField::new(0, 1); // бит 0 +const SPI_MODE: BitField = BitField::new(1, 2); // биты 1–2 +const SPI_CLKDIV: BitField = BitField::new(4, 4); // биты 4–7 +const SPI_IRQ: BitField = BitField::new(8, 1); // бит 8 -// Compile-time proof: no field shares a bit position +// Доказательство на этапе компиляции: ни одно поле не занимает общий бит const _: () = { assert!(fields_disjoint(&SPI_EN, &SPI_MODE)); assert!(fields_disjoint(&SPI_EN, &SPI_CLKDIV)); @@ -541,41 +541,41 @@ fn main() { } ``` -This complements the register map pattern above — register maps prove *inter-register* disjointness while bitfield layouts prove *intra-register* disjointness. Together they provide full coverage from the register block down to individual bits. +Это дополняет паттерн карты регистров выше: карты регистров доказывают непересечение *между регистрами*, а раскладка битовых полей доказывает непересечение *внутри регистра*. Вместе они обеспечивают полное покрытие от блока регистров до отдельных битов. -### Clock Tree / PLL Configuration +### Конфигурация дерева тактирования / PLL -Microcontrollers derive peripheral clocks through multiplier/divider chains. A PLL produces `f_vco = f_in × N / M`, and the VCO frequency must stay within a hardware-specified range. Get any parameter wrong for a specific board, and the chip outputs garbage clocks or refuses to lock. These constraints are perfect for `const fn`: +Микроконтроллеры получают тактовые частоты периферии через цепочки умножителей и делителей. PLL выдаёт `f_vco = f_in × N / M`, и частота VCO должна оставаться в диапазоне, заданном аппаратурой. Ошибка в любом параметре для конкретной платы, и чип выдаёт некорректные тактовые сигналы или не может захватить частоту. Эти ограничения идеально подходят для `const fn`: ```rust #[derive(Debug, Clone, Copy)] pub struct PllConfig { - pub input_khz: u32, // external oscillator - pub m: u32, // input divider - pub n: u32, // VCO multiplier - pub p: u32, // system clock divider + pub input_khz: u32, // внешний генератор + pub m: u32, // входной делитель + pub n: u32, // множитель VCO + pub p: u32, // делитель системной частоты } impl PllConfig { pub const fn verified(input_khz: u32, m: u32, n: u32, p: u32) -> Self { - // Input divider produces the PLL input frequency + // Входной делитель даёт частоту на входе PLL let pll_input = input_khz / m; assert!(pll_input >= 1_000 && pll_input <= 2_000, - "PLL input must be 1–2 MHz"); + "вход PLL должен быть 1–2 МГц"); - // VCO frequency must be within hardware limits + // Частота VCO должна быть в пределах аппаратных ограничений let vco = pll_input as u64 * n as u64; assert!(vco >= 192_000 && vco <= 432_000, - "VCO must be 192–432 MHz"); + "VCO должен быть в диапазоне 192–432 МГц"); - // System clock divider must be even (hardware constraint) + // Делитель системной частоты должен быть чётным (аппаратное ограничение) assert!(p == 2 || p == 4 || p == 6 || p == 8, - "P must be 2, 4, 6, or 8"); + "P должен быть 2, 4, 6 или 8"); - // Final system clock + // Итоговая системная частота let sysclk = vco / p as u64; assert!(sysclk <= 168_000, - "system clock exceeds 168 MHz maximum"); + "системная частота превышает максимум 168 МГц"); Self { input_khz, m, n, p } } @@ -589,19 +589,19 @@ impl PllConfig { } } -// STM32F4 with 8 MHz HSE crystal → 168 MHz system clock +// STM32F4 с кварцем HSE 8 МГц → системная частота 168 МГц const PLL: PllConfig = PllConfig::verified(8_000, 8, 336, 2); -// ❌ Would not compile: VCO = 480 MHz exceeds 432 MHz limit +// ❌ Не скомпилируется: VCO = 480 МГц превышает предел 432 МГц // const BAD: PllConfig = PllConfig::verified(8_000, 8, 480, 2); fn main() { - println!("VCO: {} MHz", PLL.vco_khz() / 1_000); - println!("SYSCLK: {} MHz", PLL.sysclk_khz() / 1_000); + println!("VCO: {} МГц", PLL.vco_khz() / 1_000); + println!("SYSCLK: {} МГц", PLL.sysclk_khz() / 1_000); } ``` -Uncommenting the `BAD` constant produces a compile-time error that pinpoints the violated constraint: +Раскомментирование константы `BAD` даёт ошибку на этапе компиляции, которая точно указывает на нарушенное ограничение: ```text error[E0080]: evaluation of constant value failed @@ -609,18 +609,18 @@ error[E0080]: evaluation of constant value failed | 18 | assert!(vco >= 192_000 && vco <= 432_000, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | the evaluated program panicked at 'VCO must be 192–432 MHz' + | the evaluated program panicked at 'VCO должен быть в диапазоне 192–432 МГц' ``` -The compiler catches the constraint violation in the *middle* of the derivation chain — not at the end. If you had instead violated the system clock limit (`sysclk > 168 MHz`), the error message would point to that assertion instead. +Компилятор ловит нарушение ограничения в *середине* цепочки вычислений, а не в конце. Если бы вместо этого было нарушено ограничение системной частоты (`sysclk > 168 МГц`), сообщение об ошибке указывало бы на другое утверждение. -> **Derived-value constraint chains turn a single `const fn` into a multi-stage proof.** Each intermediate value has its own hardware-mandated range. Changing one parameter (e.g., swapping to a 25 MHz crystal) immediately surfaces any downstream violation. +> **Цепочки ограничений производных значений превращают один `const fn` в многоступенчатое доказательство.** У каждого промежуточного значения свой диапазон, заданный аппаратурой. Изменение одного параметра (например, переход на кварц 25 МГц) сразу выявляет любое нарушение ниже по цепочке. -**Derived-value constraint chains** — the VCO frequency depends on `input / m × n`, and the system clock depends on `vco / p`. Each intermediate value has its own hardware-mandated range. A single `const fn` verifies the entire chain, so changing one parameter (e.g., swapping to a 25 MHz crystal) immediately surfaces any downstream violation. +**Цепочки ограничений производных значений:** частота VCO зависит от `input / m × n`, а системная частота зависит от `vco / p`. У каждого промежуточного значения свой диапазон, заданный аппаратурой. Один `const fn` проверяет всю цепочку, поэтому изменение одного параметра (например, переход на кварц 25 МГц) сразу выявляет любое нарушение ниже по цепочке. -### Compile-Time Lookup Tables +### Таблицы поиска на этапе компиляции -`const fn` can compute entire lookup tables at compile time, placing them in `.rodata` with zero startup cost. This is especially valuable for CRC tables, trigonometry, encoding maps, and error-correction codes — anywhere you'd normally use a build script or code generation: +`const fn` может вычислить целые таблицы поиска на этапе компиляции и поместить их в `.rodata` без затрат на инициализацию при запуске. Это особенно полезно для таблиц CRC, тригонометрии, таблиц кодирования и кодов коррекции ошибок, то есть везде, где обычно использовали бы build-скрипт или генерацию кода: ```rust const fn crc32_table() -> [u32; 256] { @@ -631,7 +631,7 @@ const fn crc32_table() -> [u32; 256] { let mut j = 0; while j < 8 { if crc & 1 != 0 { - crc = (crc >> 1) ^ 0xEDB8_8320; // standard CRC-32 polynomial + crc = (crc >> 1) ^ 0xEDB8_8320; // стандартный полином CRC-32 } else { crc >>= 1; } @@ -643,10 +643,10 @@ const fn crc32_table() -> [u32; 256] { table } -/// Full CRC-32 table — computed at compile time, placed in .rodata +/// Полная таблица CRC-32: вычисляется на этапе компиляции и размещается в .rodata const CRC32_TABLE: [u32; 256] = crc32_table(); -/// Compute CRC-32 over a byte slice at runtime using the precomputed table. +/// Вычисляет CRC-32 для среза байтов во время выполнения, используя готовую таблицу. fn crc32(data: &[u8]) -> u32 { let mut crc: u32 = !0; for &byte in data { @@ -656,65 +656,65 @@ fn crc32(data: &[u8]) -> u32 { !crc } -// Smoke-test: well-known CRC-32 of "123456789" +// Проверка: известный CRC-32 для «123456789» const _: () = { - // Verify a single table entry at compile time + // Проверяем отдельные элементы таблицы на этапе компиляции assert!(CRC32_TABLE[0] == 0x0000_0000); assert!(CRC32_TABLE[1] == 0x7707_3096); }; fn main() { let check = crc32(b"123456789"); - // Known CRC-32 of "123456789" is 0xCBF43926 + // Известный CRC-32 для «123456789» — 0xCBF43926 assert_eq!(check, 0xCBF4_3926); - println!("CRC-32 of '123456789' = {:#010X} ✓", check); - println!("Table size: {} entries × 4 bytes = {} bytes in .rodata", + println!("CRC-32 для '123456789' = {:#010X} ✓", check); + println!("Размер таблицы: {} элементов × 4 байта = {} байт в .rodata", CRC32_TABLE.len(), CRC32_TABLE.len() * 4); } ``` -The `crc32_table()` function runs entirely during compilation. The resulting 1 KB table is baked into the binary's read-only data section — no allocator, no initialization code, no startup cost. Compare this with a C approach that either uses a code generator or computes the table at startup. The Rust version is provably correct (the `const _` assertions verify known values) and provably complete (the compiler will reject the program if the function fails to produce a valid table). +Функция `crc32_table()` выполняется целиком во время компиляции. Полученная таблица размером 1 KB записывается в секцию read-only данных бинарного файла: ни аллокатора, ни кода инициализации, ни затрат на запуск. Сравните с подходом C, где таблицу либо генерирует отдельный инструмент, либо вычисляют при запуске. Версия на Rust доказуемо корректна (утверждения `const _` проверяют известные значения) и доказуемо полна (компилятор отвергнет программу, если функция не сможет построить корректную таблицу). -## When to Use Const Fn Proofs +## Когда использовать доказательства на основе const fn -| Scenario | Recommendation | -|----------|:---:| -| Memory maps, register offsets, partition tables | ✅ Always | -| Protocol frame layouts with fixed fields | ✅ Always | -| Bitfield masks within a register | ✅ Always | -| Clock tree / PLL parameter chains | ✅ Always | -| Lookup tables (CRC, trig, encoding) | ✅ Always — zero startup cost | -| Constants with cross-value invariants (non-overlap, sum ≤ bound) | ✅ Always | -| Configuration values with domain constraints | ✅ When values are known at compile time | -| Values computed from user input or files | ❌ Use runtime validation | -| Highly dynamic structures (trees, graphs) | ❌ Use property-based testing | -| Single-value range checks | ⚠️ Consider newtype + `From` instead ([ch07](ch07-validated-boundaries-parse-dont-validate.md)) | +| Сценарий | Рекомендация | +|----------|:------------:| +| Карты памяти, смещения регистров, таблицы разделов | ✅ Всегда | +| Раскладка кадров протокола с фиксированными полями | ✅ Всегда | +| Битовые маски внутри регистра | ✅ Всегда | +| Дерево тактирования / параметры PLL | ✅ Всегда | +| Таблицы поиска (CRC, тригонометрия, кодирование) | ✅ Всегда: ноль затрат на запуск | +| Константы с перекрёстными инвариантами (непересечение, сумма ≤ границы) | ✅ Всегда | +| Значения конфигурации с ограничениями предметной области | ✅ Когда значения известны на этапе компиляции | +| Значения, вычисленные из пользовательского ввода или файлов | ❌ Используйте проверку во время выполнения | +| Сильно динамические структуры (деревья, графы) | ❌ Используйте property-based тестирование | +| Проверка диапазона одного значения | ⚠️ Лучше newtype + `From` ([гл. 07](ch07-validated-boundaries-parse-dont-validate.md)) | -### Cost Summary +### Сводка стоимости -| What | Runtime cost | -|------|:------:| -| `const fn` assertions (`assert!`, `panic!`) | Compile time only — 0 instructions | -| `const _: () = { ... }` validation blocks | Compile time only — not in binary | -| `Region`, `Register`, `Field` structs | Plain data — same layout as raw integers | -| Inline `const { }` generic validation | Monomorphised at compile time — 0 cost | -| Lookup tables (`crc32_table()`) | Computed at compile time — placed in `.rodata` | -| Phantom-typed access markers (`TypedRegion`) | Zero-sized — optimised away | +| Что | Стоимость во время выполнения | +|-----|:------------------------------:| +| Утверждения `const fn` (`assert!`, `panic!`) | Только время компиляции: 0 инструкций | +| Блоки проверки `const _: () = { ... }` | Только время компиляции: не попадают в бинарник | +| Структуры `Region`, `Register`, `Field` | Обычные данные: та же раскладка, что у голых целых | +| Встроенная проверка обобщений `const { }` | Мономорфизируется на этапе компиляции: 0 затрат | +| Таблицы поиска (`crc32_table()`) | Вычисляются на этапе компиляции: размещаются в `.rodata` | +| Маркеры доступа с phantom-типами (`TypedRegion`) | Нулевой размер: оптимизируются | -Every row is **zero runtime cost** — the proofs exist only during compilation. The resulting binary contains only the verified constants and lookup tables, with no assertion-checking code. +В каждой строке **ноль затрат во время выполнения**: доказательства существуют только во время компиляции. Итоговый бинарный файл содержит только проверенные константы и таблицы поиска, без кода проверки утверждений. -## Exercise: Flash Partition Map +## Упражнение: карта разделов флеш-памяти -Design a verified flash partition map for a 1 MB NOR flash starting at `0x0800_0000`. Requirements: +Спроектируйте проверенную карту разделов для NOR-флеш-памяти 1 MB, начинающейся с адреса `0x0800_0000`. Требования: -1. Four partitions: **bootloader** (64 KB), **application** (640 KB), **config** (64 KB), **OTA staging** (256 KB) -2. Every partition must be **4 KB aligned** (flash erase granularity): both base and size must be multiples of 4096 -3. No partition may overlap another -4. All partitions must fit within flash -5. Add a `const fn total_used()` that returns the sum of all partition sizes and assert it equals 1 MB +1. Четыре раздела: **загрузчик** (64 KB), **приложение** (640 KB), **конфигурация** (64 KB), **промежуточный буфер OTA** (256 KB) +2. Каждый раздел должен быть **выровнен по 4 KB** (гранулярность стирания флеш-памяти): и базовый адрес, и размер должны быть кратны 4096 +3. Ни один раздел не может пересекаться с другим +4. Все разделы должны помещаться во флеш-памяти +5. Добавьте `const fn total_used()`, которая возвращает сумму размеров всех разделов, и утверждение, что она равна 1 MB
-Solution +Пример решения ```rust #[derive(Debug, Clone, Copy)] @@ -725,12 +725,12 @@ pub struct FlashRegion { impl FlashRegion { pub const fn new(base: u32, size: u32) -> Self { - assert!(size > 0, "partition size must be non-zero"); - assert!(base % 4096 == 0, "partition base must be 4 KB aligned"); - assert!(size % 4096 == 0, "partition size must be 4 KB aligned"); + assert!(size > 0, "размер раздела должен быть ненулевым"); + assert!(base % 4096 == 0, "базовый адрес раздела должен быть выровнен по 4 KB"); + assert!(size % 4096 == 0, "размер раздела должен быть выровнен по 4 KB"); assert!( base as u64 + size as u64 <= u32::MAX as u64, - "partition overflows address space" + "раздел выходит за пределы адресного пространства" ); Self { base, size } } @@ -762,17 +762,17 @@ impl FlashMap { config: FlashRegion, ota: FlashRegion, ) -> Self { - assert!(total.contains(&boot), "bootloader exceeds flash"); - assert!(total.contains(&app), "application exceeds flash"); - assert!(total.contains(&config), "config exceeds flash"); - assert!(total.contains(&ota), "OTA staging exceeds flash"); - - assert!(!boot.overlaps(&app), "boot/app overlap"); - assert!(!boot.overlaps(&config), "boot/config overlap"); - assert!(!boot.overlaps(&ota), "boot/ota overlap"); - assert!(!app.overlaps(&config), "app/config overlap"); - assert!(!app.overlaps(&ota), "app/ota overlap"); - assert!(!config.overlaps(&ota), "config/ota overlap"); + assert!(total.contains(&boot), "загрузчик выходит за пределы флеш-памяти"); + assert!(total.contains(&app), "приложение выходит за пределы флеш-памяти"); + assert!(total.contains(&config), "конфигурация выходит за пределы флеш-памяти"); + assert!(total.contains(&ota), "промежуточный буфер OTA выходит за пределы флеш-памяти"); + + assert!(!boot.overlaps(&app), "загрузчик/приложение пересекаются"); + assert!(!boot.overlaps(&config), "загрузчик/конфигурация пересекаются"); + assert!(!boot.overlaps(&ota), "загрузчик/OTA пересекаются"); + assert!(!app.overlaps(&config), "приложение/конфигурация пересекаются"); + assert!(!app.overlaps(&ota), "приложение/OTA пересекаются"); + assert!(!config.overlaps(&ota), "конфигурация/OTA пересекаются"); Self { total, boot, app, config, ota } } @@ -783,21 +783,21 @@ impl FlashMap { } const FLASH: FlashMap = FlashMap::verified( - FlashRegion::new(0x0800_0000, 1024 * 1024), // 1 MB total - FlashRegion::new(0x0800_0000, 64 * 1024), // bootloader: 64 KB - FlashRegion::new(0x0801_0000, 640 * 1024), // application: 640 KB - FlashRegion::new(0x080B_0000, 64 * 1024), // config: 64 KB - FlashRegion::new(0x080C_0000, 256 * 1024), // OTA staging: 256 KB + FlashRegion::new(0x0800_0000, 1024 * 1024), // всего 1 MB + FlashRegion::new(0x0800_0000, 64 * 1024), // загрузчик: 64 KB + FlashRegion::new(0x0801_0000, 640 * 1024), // приложение: 640 KB + FlashRegion::new(0x080B_0000, 64 * 1024), // конфигурация: 64 KB + FlashRegion::new(0x080C_0000, 256 * 1024), // промежуточный буфер OTA: 256 KB ); -// Every byte of flash is accounted for +// Каждый байт флеш-памяти учтён const _: () = assert!( FLASH.total_used() == 1024 * 1024, - "partitions must exactly fill flash" + "разделы должны точно заполнять флеш-память" ); fn main() { - println!("Flash map: {} KB used / {} KB total", + println!("Карта флеш-памяти: {} KB используется / {} KB всего", FLASH.total_used() / 1024, FLASH.total.size / 1024); } @@ -807,16 +807,16 @@ fn main() { ```mermaid flowchart LR - subgraph compile["Compile Time — zero runtime cost"] + subgraph compile["Этап компиляции — ноль затрат во время выполнения"] direction TB - RGN["Region::new()
✅ size > 0
✅ no overflow"] - MAP["SramMap::verified()
✅ containment
✅ non-overlap"] - ACC["TypedRegion<RW>
✅ access control"] - PROV["VerifiedAddr::new()
✅ provenance"] + RGN["Region::new()
✅ размер > 0
✅ нет переполнения"] + MAP["SramMap::verified()
✅ вложенность
✅ без пересечений"] + ACC["TypedRegion<RW>
✅ контроль доступа"] + PROV["VerifiedAddr::new()
✅ происхождение"] end - subgraph runtime["Runtime"] - HW["Hardware access
No bounds checks
No permission checks"] + subgraph runtime["Рантайм"] + HW["Доступ к аппаратуре
Без проверки границ
Без проверки прав"] end RGN --> MAP --> ACC --> PROV --> HW @@ -828,20 +828,20 @@ flowchart LR style HW fill:#fff3e0,color:#000 ``` -## Key Takeaways +## Ключевые выводы -1. **`const fn` + `assert!` = compile-time proof obligation** — if the assertion fails during const evaluation, the program does not compile. No test needed, no code review catch — the compiler proves it. +1. **`const fn` + `assert!` = обязательство доказательства на этапе компиляции**: если утверждение не выполняется при вычислении константы, программа не компилируется. Не нужен тест и не нужен код-ревью: компилятор доказывает это сам. -2. **Memory maps are ideal candidates** — sub-region containment, overlap freedom, total-size bounds, and alignment constraints are all expressible as const fn assertions. The C `#define` approach offers none of these guarantees. +2. **Карты памяти — идеальные кандидаты**: вложенность подрегионов, непересечение, ограничения общего объёма и выравнивания выражаются утверждениями `const fn`. Подход с `#define` в C не даёт ни одной из этих гарантий. -3. **Phantom types layer on top** — combine const fn (value verification) with phantom-typed access markers (permission verification) for defense in depth at zero runtime cost. +3. **Phantom-типы добавляются сверху**: сочетайте const fn (проверку значений) с phantom-маркерами доступа (проверку прав), чтобы получить защиту в глубину без затрат во время выполнения. -4. **Provenance can be established at compile time** — `VerifiedAddr` proves at compile time that an address belongs to a specific region, eliminating runtime bounds checks on every access. +4. **Происхождение можно установить на этапе компиляции**: `VerifiedAddr` доказывает на этапе компиляции, что адрес принадлежит конкретному региону, устраняя проверки границ во время выполнения при каждом обращении. -5. **The pattern generalizes beyond memory** — register maps, bitfield masks, protocol frames, clock trees, DMA parameters — anywhere you have compile-time-known values with structural invariants. +5. **Паттерн обобщается за пределы памяти**: карты регистров, маски битовых полей, кадры протокола, деревья тактирования, параметры DMA. Всюду, где есть известные на этапе компиляции значения со структурными инвариантами. -6. **Bitfields and clock trees are ideal candidates** — intra-register bit disjointness and derived-value constraint chains (VCO range, divider limits) are exactly the kind of invariant that `const fn` proves effortlessly. +6. **Битовые поля и деревья тактирования — идеальные кандидаты**: непересечение битов внутри регистра и цепочки ограничений производных значений (диапазон VCO, пределы делителей) — ровно то, что `const fn` доказывает без усилий. -7. **`const fn` replaces code generators and build scripts for lookup tables** — CRC tables, trigonometry, encoding maps — computed at compile time, placed in `.rodata`, with zero startup cost and no external tooling. +7. **`const fn` заменяет генераторы кода и build-скрипты для таблиц поиска**: таблицы CRC, тригонометрия, таблицы кодирования вычисляются на этапе компиляции, размещаются в `.rodata` и не требуют затрат на запуск или внешних инструментов. -8. **Inline `const { }` blocks validate generic parameters** — since Rust 1.79, you can enforce constraints on const generics at the call site, catching misuse before any code runs. +8. **Встроенные блоки `const { }` проверяют параметры обобщений**: начиная с Rust 1.79, можно накладывать ограничения на const-обобщения прямо в месте вызова и ловить неправильное использование до запуска любого кода. diff --git a/type-driven-correctness-book/src/ch16-send-sync-compile-time-concurrency-proofs.md b/type-driven-correctness-book/src/ch16-send-sync-compile-time-concurrency-proofs.md index 954b686..8fe15dd 100644 --- a/type-driven-correctness-book/src/ch16-send-sync-compile-time-concurrency-proofs.md +++ b/type-driven-correctness-book/src/ch16-send-sync-compile-time-concurrency-proofs.md @@ -1,72 +1,72 @@ -# Send & Sync — Compile-Time Concurrency Proofs 🟠 +# Send и Sync: доказательства корректности конкурентности на этапе компиляции 🟠 -> **What you'll learn:** How Rust's `Send` and `Sync` auto-traits turn the compiler into a concurrency auditor — proving at compile time which types can cross thread boundaries and which can be shared, with zero runtime cost. +> **Что вы узнаете:** как автоматические трейты `Send` и `Sync` превращают компилятор в аудитора конкурентности: на этапе компиляции доказывается, какие типы могут переходить между потоками и какие можно разделять, без затрат во время выполнения. > -> **Cross-references:** [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types), [ch15](ch15-const-fn-compile-time-correctness-proofs.md) (const fn proofs) +> **Перекрёстные ссылки:** [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы), [гл. 15](ch15-const-fn-compile-time-correctness-proofs.md) (доказательства через const fn) -## The Problem: Concurrent Access Without a Safety Net +## Проблема: конкурентный доступ без страховочной сетки -In systems programming, peripherals, shared buffers, and global state are accessed from multiple contexts — main loops, interrupt handlers, DMA callbacks, and worker threads. In C, the compiler offers no enforcement whatsoever: +В системном программировании периферия, общие буферы и глобальное состояние используются из нескольких контекстов: главного цикла, обработчиков прерываний, колбэков DMA и рабочих потоков. В C компилятор никак этого не контролирует: ```c -/* Shared sensor buffer — accessed from main loop and ISR */ +/* Общий буфер датчика: доступ из главного цикла и из ISR */ volatile uint32_t sensor_buf[64]; volatile uint32_t buf_index = 0; void SENSOR_IRQHandler(void) { - sensor_buf[buf_index++] = read_sensor(); /* Race: buf_index read + write */ + sensor_buf[buf_index++] = read_sensor(); /* Гонка: чтение и запись buf_index */ } void process_sensors(void) { - for (uint32_t i = 0; i < buf_index; i++) { /* buf_index changes mid-loop */ - process(sensor_buf[i]); /* Data overwritten mid-read */ + for (uint32_t i = 0; i < buf_index; i++) { /* buf_index меняется посреди цикла */ + process(sensor_buf[i]); /* данные перезаписаны во время чтения */ } - buf_index = 0; /* ISR fires between these lines */ + buf_index = 0; /* ISR срабатывает между этими строками */ } ``` -The `volatile` keyword prevents the compiler from optimizing away the reads, but it does **nothing** about data races. Two contexts can read and write `buf_index` simultaneously, producing torn values, lost updates, or buffer overruns. The same problem appears with `pthread_mutex_t` — the compiler will happily let you forget to lock: +Ключевое слово `volatile` не даёт компилятору оптимизировать чтения, но **ничего не делает** с гонками данных. Два контекста могут одновременно читать и писать `buf_index`, давая рваные значения, потерянные обновления или переполнение буфера. Та же проблема возникает и с `pthread_mutex_t`: компилятор спокойно позволит забыть взять блокировку: ```c pthread_mutex_t lock; int shared_counter; void increment(void) { - shared_counter++; /* Oops — forgot pthread_mutex_lock(&lock) */ + shared_counter++; /* Упс: забыли pthread_mutex_lock(&lock) */ } ``` -**Every concurrent bug is discovered at runtime** — typically under load, in production, and intermittently. +**Каждая ошибка конкурентности обнаруживается во время выполнения**, обычно под нагрузкой, в продакшене и с перебоями. -## What Send and Sync Prove +## Что доказывают Send и Sync -Rust defines two marker traits that the compiler derives automatically: +Rust определяет два маркерных трейта, которые компилятор выводит автоматически: -| Trait | Proof | Informal meaning | -|-------|-------|-------------------| -| `Send` | A value of type `T` can be safely **moved** to another thread | "This can cross a thread boundary" | -| `Sync` | A **shared reference** `&T` can be safely used by multiple threads | "This can be read from multiple threads" | +| Трейт | Доказывает | Неформальный смысл | +|-------|-----------|--------------------| +| `Send` | Значение типа `T` можно безопасно **переместить** в другой поток | «Это может пересекать границу потока» | +| `Sync` | **Разделяемую ссылку** `&T` можно безопасно использовать из нескольких потоков | «Это можно читать из нескольких потоков» | -These are **auto-traits** — the compiler derives them by inspecting every field. A struct is `Send` if all its fields are `Send`. A struct is `Sync` if all its fields are `Sync`. If any field opts out, the entire struct opts out. No annotation needed, no runtime overhead — the proof is structural. +Это **авто-трейты**: компилятор выводит их, проверяя каждое поле. Структура является `Send`, если все её поля `Send`. Структура является `Sync`, если все её поля `Sync`. Если хотя бы одно поле отказывается от трейта, отказывается вся структура. Аннотация не нужна, накладных расходов во время выполнения нет: доказательство структурное. ```mermaid flowchart TD - STRUCT["Your struct"] - INSPECT["Compiler inspects
every field"] - ALL_SEND{"All fields
Send?"} - ALL_SYNC{"All fields
Sync?"} - SEND_YES["Send ✅
can cross thread boundaries"] - SEND_NO["!Send ❌
confined to one thread"] - SYNC_YES["Sync ✅
shareable across threads"] - SYNC_NO["!Sync ❌
no concurrent references"] + STRUCT["Ваша структура"] + INSPECT["Компилятор проверяет
каждое поле"] + ALL_SEND{"Все поля
Send?"} + ALL_SYNC{"Все поля
Sync?"} + SEND_YES["Send ✅
может пересекать границы потоков"] + SEND_NO["!Send ❌
ограничена одним потоком"] + SYNC_YES["Sync ✅
может разделяться между потоками"] + SYNC_NO["!Sync ❌
нет конкурентных ссылок"] STRUCT --> INSPECT INSPECT --> ALL_SEND INSPECT --> ALL_SYNC - ALL_SEND -->|Yes| SEND_YES - ALL_SEND -->|"Any field !Send
(e.g., Rc, *const T)"| SEND_NO - ALL_SYNC -->|Yes| SYNC_YES - ALL_SYNC -->|"Any field !Sync
(e.g., Cell, RefCell)"| SYNC_NO + ALL_SEND -->|Да| SEND_YES + ALL_SEND -->|"Любое поле !Send
(например, Rc, *const T)"| SEND_NO + ALL_SYNC -->|Да| SYNC_YES + ALL_SYNC -->|"Любое поле !Sync
(например, Cell, RefCell)"| SYNC_NO style SEND_YES fill:#c8e6c9,color:#000 style SYNC_YES fill:#c8e6c9,color:#000 @@ -74,36 +74,36 @@ flowchart TD style SYNC_NO fill:#ffcdd2,color:#000 ``` -> **The compiler is the auditor.** In C, thread-safety annotations live in comments and header documentation — advisory, never enforced. In Rust, `Send` and `Sync` are derived from the structure of the type itself. Adding a single `Cell` field automatically makes the containing struct `!Sync`. No programmer action required, no way to forget. +> **Компилятор — это аудитор.** В C аннотации потокобезопасности живут в комментариях и документации к заголовочным файлам: это рекомендации, которые никто не проверяет. В Rust `Send` и `Sync` выводятся из структуры самого типа. Добавление одного поля `Cell` автоматически делает содержащую структуру `!Sync`. Никаких действий программиста не требуется, и забыть невозможно. -The two traits are linked by a key identity: +Два трейта связаны ключевым тождеством: -> **`T` is `Sync` if and only if `&T` is `Send`.** +> **`T` является `Sync` тогда и только тогда, когда `&T` является `Send`.** -This makes intuitive sense: if a shared reference can be safely sent to another thread, then the underlying type is safe for concurrent reads. +Это интуитивно понятно: если разделяемую ссылку можно безопасно отправить в другой поток, то лежащий в её основе тип безопасен для конкурентного чтения. -### Types That Opt Out +### Типы, которые отказываются от трейтов -Certain types are deliberately `!Send` or `!Sync`: +Некоторые типы намеренно являются `!Send` или `!Sync`: -| Type | Send | Sync | Why | -|------|:----:|:----:|-----| -| `u32`, `String`, `Vec` | ✅ | ✅ | No interior mutability, no raw pointers | -| `Cell`, `RefCell` | ✅ | ❌ | Interior mutability without synchronization | -| `Rc` | ❌ | ❌ | Reference count is not atomic | -| `*const T`, `*mut T` | ❌ | ❌ | Raw pointers have no safety guarantees | -| `Arc` (where `T: Send + Sync`) | ✅ | ✅ | Atomic reference count | -| `Mutex` (where `T: Send`) | ✅ | ✅ | Lock serializes all access | +| Тип | Send | Sync | Почему | +|-----|:----:|:----:|--------| +| `u32`, `String`, `Vec` | ✅ | ✅ | Нет внутренней изменяемости и сырых указателей | +| `Cell`, `RefCell` | ✅ | ❌ | Внутренняя изменяемость без синхронизации | +| `Rc` | ❌ | ❌ | Счётчик ссылок не атомарный | +| `*const T`, `*mut T` | ❌ | ❌ | У сырых указателей нет гарантий безопасности | +| `Arc` (где `T: Send + Sync`) | ✅ | ✅ | Атомарный счётчик ссылок | +| `Mutex` (где `T: Send`) | ✅ | ✅ | Блокировка сериализует весь доступ | -Every ❌ in this table is a **compile-time invariant**. You cannot accidentally send an `Rc` to another thread — the compiler rejects it. +Каждая ❌ в этой таблице — **инвариант времени компиляции**. Случайно отправить `Rc` в другой поток нельзя: компилятор это отвергнет. -## !Send Peripheral Handles +## Дескрипторы периферии, которые не являются Send -In embedded systems, a peripheral register block lives at a fixed memory address and should only be accessed from a single execution context. Raw pointers are inherently `!Send` and `!Sync`, so wrapping one automatically opts the containing type out of both traits: +Во встраиваемых системах блок регистров периферии находится по фиксированному адресу памяти и должен использоваться только из одного контекста выполнения. Сырые указатели по своей природе `!Send` и `!Sync`, поэтому обёртка над ними автоматически лишает содержащий тип обоих трейтов: ```rust -/// A handle to a memory-mapped UART peripheral. -/// The raw pointer makes this automatically !Send and !Sync. +/// Дескриптор периферийного UART, отображённого в память. +/// Сырой указатель делает его автоматически !Send и !Sync. pub struct Uart { regs: *const u32, } @@ -114,23 +114,23 @@ impl Uart { } pub fn write_byte(&self, byte: u8) { - // In real firmware: unsafe { write_volatile(self.regs.add(DATA_OFFSET), byte as u32) } + // В реальной прошивке: unsafe { write_volatile(self.regs.add(DATA_OFFSET), byte as u32) } println!("UART TX: {:#04X}", byte); } } fn main() { let uart = Uart::new(0x4000_1000); - uart.write_byte(b'A'); // ✅ Use on the creating thread + uart.write_byte(b'A'); // ✅ Использование в потоке, который его создал - // ❌ Would not compile: Uart is !Send + // ❌ Не скомпилируется: Uart — !Send // std::thread::spawn(move || { // uart.write_byte(b'B'); // }); } ``` -The commented-out `thread::spawn` would produce: +Закомментированный `thread::spawn` дал бы: ```text error[E0277]: `*const u32` cannot be sent between threads safely @@ -140,13 +140,13 @@ error[E0277]: `*const u32` cannot be sent between threads safely | implemented for `*const u32` ``` -**No raw pointer? Use `PhantomData`.** Sometimes a type has no raw pointer but should still be confined to one thread — for example, a file descriptor index or a handle obtained from a C library: +**Нет сырого указателя? Используйте `PhantomData`.** Иногда у типа нет сырого указателя, но его всё равно нужно ограничить одним потоком, например индекс файлового дескриптора или дескриптор, полученный из библиотеки на C: ```rust use std::marker::PhantomData; -/// An opaque handle from a C library. PhantomData<*const ()> makes it -/// !Send + !Sync even though the inner fd is just a plain integer. +/// Непрозрачный дескриптор из библиотеки на C. PhantomData<*const ()> делает его +/// !Send + !Sync, хотя внутренний fd — это просто целое число. pub struct LibHandle { fd: i32, _not_send: PhantomData<*const ()>, @@ -165,34 +165,34 @@ fn main() { let handle = LibHandle::open("/dev/sensor0"); println!("fd = {}", handle.fd()); - // ❌ Would not compile: LibHandle is !Send + // ❌ Не скомпилируется: LibHandle — !Send // std::thread::spawn(move || { let _ = handle.fd(); }); } ``` -This is the compile-time equivalent of C's "please read the documentation that says this handle isn't thread-safe." In Rust, the compiler enforces it. +Это аналог на этапе компиляции той строки из документации C: «прочитайте, что этот дескриптор не потокобезопасен». В Rust это проверяет компилятор. -## Mutex Transforms !Sync into Sync +## Mutex превращает !Sync в Sync -`Cell` and `RefCell` provide interior mutability without any synchronization — so they're `!Sync`. But sometimes you genuinely need to share mutable state across threads. `Mutex` adds the missing synchronization, and the compiler recognizes this: +`Cell` и `RefCell` дают внутреннюю изменяемость без какой-либо синхронизации, поэтому они `!Sync`. Но иногда действительно нужно разделять изменяемое состояние между потоками. `Mutex` добавляет недостающую синхронизацию, и компилятор это учитывает: -> **If `T: Send`, then `Mutex: Send + Sync`.** +> **Если `T: Send`, то `Mutex: Send + Sync`.** -The lock serializes all access, so the `!Sync` inner type becomes safe to share. The compiler proves this structurally — no runtime check for "did the programmer remember to lock": +Блокировка сериализует весь доступ, поэтому внутренний `!Sync`-тип становится безопасным для разделения. Компилятор доказывает это структурно: никакой проверки во время выполнения «не забыл ли программист взять блокировку» не нужно: ```rust use std::sync::{Arc, Mutex}; use std::cell::Cell; -/// A sensor cache using Cell for interior mutability. -/// Cell is !Sync — can't be shared across threads directly. +/// Кэш показаний датчика с Cell для внутренней изменяемости. +/// Cell — !Sync: напрямую разделять между потоками его нельзя. struct SensorCache { last_reading: Cell, reading_count: Cell, } fn main() { - // Mutex makes SensorCache safe to share — compiler proves it + // Mutex делает SensorCache безопасным для разделения: компилятор это доказывает let cache = Arc::new(Mutex::new(SensorCache { last_reading: Cell::new(0), reading_count: Cell::new(0), @@ -201,7 +201,7 @@ fn main() { let handles: Vec<_> = (0..4).map(|i| { let c = Arc::clone(&cache); std::thread::spawn(move || { - let guard = c.lock().unwrap(); // Must lock before access + let guard = c.lock().unwrap(); // Перед доступом обязательно блокируем guard.last_reading.set(i * 10); guard.reading_count.set(guard.reading_count.get() + 1); }) @@ -210,18 +210,18 @@ fn main() { for h in handles { h.join().unwrap(); } let guard = cache.lock().unwrap(); - println!("Last reading: {}", guard.last_reading.get()); - println!("Total reads: {}", guard.reading_count.get()); + println!("Последнее показание: {}", guard.last_reading.get()); + println!("Всего чтений: {}", guard.reading_count.get()); } ``` -Compare to the C version: `pthread_mutex_lock` is a runtime call that the programmer can forget. Here, the type system makes it impossible to access `SensorCache` without going through the `Mutex`. The proof is structural — the only runtime cost is the lock itself. +Сравните с версией на C: `pthread_mutex_lock` — это вызов во время выполнения, который программист может забыть. Здесь система типов делает невозможным доступ к `SensorCache` в обход `Mutex`. Доказательство структурное, а единственная затрата во время выполнения — сама блокировка. -> **`Mutex` doesn't just synchronize — it proves synchronization.** `Mutex::lock()` returns a `MutexGuard` that `Deref`s to `&T`. There is no way to obtain a reference to the inner data without going through the lock. The API makes "forgot to lock" structurally unrepresentable. +> **`Mutex` не просто синхронизирует, а доказывает синхронизацию.** `Mutex::lock()` возвращает `MutexGuard`, который разыменовывается в `&T`. Получить ссылку на внутренние данные в обход блокировки невозможно. API делает «забыл взять блокировку» структурно невыразимым. -## Function Bounds as Theorems +## Ограничения функций как теоремы -`std::thread::spawn` has this signature: +`std::thread::spawn` имеет такую сигнатуру: ```rust,ignore pub fn spawn(f: F) -> JoinHandle @@ -230,17 +230,17 @@ where T: Send + 'static, ``` -The `Send + 'static` bound isn't just an implementation detail — it's a **theorem**: +Ограничение `Send + 'static` — не деталь реализации, а **теорема**: -> "Any closure and return value passed to `spawn` is proven at compile time to be safe to run on another thread, with no dangling references." +> «Любое замыкание и возвращаемое значение, переданные в `spawn`, доказано на этапе компиляции безопасными для запуска в другом потоке, без висячих ссылок.» -You can apply the same pattern to your own APIs: +Тот же паттерн можно применить к собственным API: ```rust use std::sync::mpsc; -/// Run a task on a background thread and return its result. -/// The bounds prove: the closure and its result are thread-safe. +/// Выполняет задачу в фоновом потоке и возвращает её результат. +/// Ограничения доказывают: замыкание и его результат потокобезопасны. fn run_on_background(task: F) -> T where F: FnOnce() -> T + Send + 'static, @@ -250,26 +250,26 @@ where std::thread::spawn(move || { let _ = tx.send(task()); }); - rx.recv().expect("background task panicked") + rx.recv().expect("фоновая задача завершилась паникой") } fn main() { - // ✅ u32 is Send, closure captures nothing non-Send + // ✅ u32 — Send, замыкание не захватывает ничего не-Send let result = run_on_background(|| 6 * 7); - println!("Result: {result}"); + println!("Результат: {result}"); - // ✅ String is Send - let greeting = run_on_background(|| String::from("hello from background")); + // ✅ String — Send + let greeting = run_on_background(|| String::from("привет из фонового потока")); println!("{greeting}"); - // ❌ Would not compile: Rc is !Send + // ❌ Не скомпилируется: Rc — !Send // use std::rc::Rc; // let data = Rc::new(42); // run_on_background(move || *data); } ``` -Uncommenting the `Rc` example produces a precise diagnostic: +Раскомментирование примера с `Rc` даёт точную диагностику: ```text error[E0277]: `Rc` cannot be sent between threads safely @@ -284,64 +284,64 @@ note: required by a bound in `run_on_background` | ^^^^ required by this bound ``` -The compiler traces the violation back to the exact bound — and tells the programmer *why*. Compare to C's `pthread_create`: +Компилятор возвращает нарушение к точному ограничению и сообщает программисту, *почему* это нарушение. Сравните с `pthread_create` из C: ```c int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); ``` -The `void *arg` accepts anything — thread-safe or not. The C compiler can't distinguish a non-atomic refcount from a plain integer. Rust's trait bounds make the distinction at the type level. +`void *arg` принимает что угодно: потокобезопасное или нет. Компилятор C не может отличить неатомарный счётчик ссылок от обычного целого. Ограничения трейтов в Rust проводят это различие на уровне типов. -## When to Use Send/Sync Proofs +## Когда использовать доказательства Send/Sync -| Scenario | Approach | -|----------|----------| -| Peripheral handle wrapping a raw pointer | Automatic `!Send + !Sync` — nothing to do | -| Handle from C library (integer fd/handle) | Add `PhantomData<*const ()>` for `!Send + !Sync` | -| Shared config behind a lock | `Arc>` — compiler proves access is safe | -| Cross-thread message passing | `mpsc::channel` — `Send` bound enforced automatically | -| Task spawner or thread pool API | Require `F: Send + 'static` in signature | -| Single-threaded resource (e.g., GPU context) | `PhantomData<*const ()>` to prevent sharing | -| Type should be `Send` but contains a raw pointer | `unsafe impl Send` with documented safety justification | +| Сценарий | Подход | +|----------|--------| +| Дескриптор периферии с сырым указателем | Автоматически `!Send + !Sync`: ничего делать не нужно | +| Дескриптор из библиотеки на C (целочисленный fd/handle) | Добавить `PhantomData<*const ()>` для `!Send + !Sync` | +| Общая конфигурация за блокировкой | `Arc>`: компилятор доказывает безопасность доступа | +| Передача сообщений между потоками | `mpsc::channel`: ограничение `Send` применяется автоматически | +| API планировщика задач или пула потоков | Требовать `F: Send + 'static` в сигнатуре | +| Однопоточный ресурс (например, контекст GPU) | `PhantomData<*const ()>`, чтобы предотвратить разделение | +| Тип должен быть `Send`, но содержит сырой указатель | `unsafe impl Send` с задокументированным обоснованием безопасности | -### Cost Summary +### Сводка стоимости -| What | Runtime cost | -|------|:------:| -| `Send` / `Sync` auto-derivation | Compile time only — 0 bytes | -| `PhantomData<*const ()>` field | Zero-sized — optimised away | -| `!Send` / `!Sync` enforcement | Compile time only — no runtime check | -| `F: Send + 'static` function bounds | Monomorphised — static dispatch, no boxing | -| `Mutex` lock | Runtime lock (unavoidable for shared mutation) | -| `Arc` reference counting | Atomic increment/decrement (unavoidable for shared ownership) | +| Что | Стоимость во время выполнения | +|-----|:-----------------------------:| +| Автоматический вывод `Send` / `Sync` | Только время компиляции: 0 байт | +| Поле `PhantomData<*const ()>` | Нулевой размер: оптимизируется | +| Проверка `!Send` / `!Sync` | Только время компиляции: без проверок во время выполнения | +| Ограничения функций `F: Send + 'static` | Мономорфизируются: статическая диспетчеризация, без боксинга | +| Блокировка `Mutex` | Блокировка во время выполнения (неизбежна для разделяемых изменений) | +| Подсчёт ссылок `Arc` | Атомарные инкременты и декременты (неизбежны для разделяемого владения) | -The first four rows are **zero-cost** — they exist only in the type system and vanish after compilation. `Mutex` and `Arc` carry unavoidable runtime costs, but those costs are the *minimum* any correct concurrent program must pay — Rust just makes sure you pay them. +Первые четыре строки имеют **нулевую стоимость**: они существуют только в системе типов и исчезают после компиляции. `Mutex` и `Arc` несут неизбежные затраты во время выполнения, но эти затраты — *минимум*, который должна платить любая корректная конкурентная программа. Rust лишь следит за тем, чтобы вы их заплатили. -## Exercise: DMA Transfer Guard +## Упражнение: защита DMA-передачи -Design a `DmaTransfer` that holds a buffer while a DMA transfer is in flight. Requirements: +Спроектируйте `DmaTransfer`, который удерживает буфер, пока выполняется DMA-передача. Требования: -1. `DmaTransfer` must be `!Send` — the DMA controller uses physical addresses tied to this core's memory bus -2. `DmaTransfer` must be `!Sync` — concurrent reads while DMA is writing would see torn data -3. Provide a `wait()` method that **consumes** the guard and returns the buffer — ownership proves the transfer is complete -4. The buffer type `T` must implement a `DmaSafe` marker trait +1. `DmaTransfer` должен быть `!Send`: контроллер DMA использует физические адреса, привязанные к шине памяти этого ядра +2. `DmaTransfer` должен быть `!Sync`: одновременное чтение во время записи DMA увидит рваные данные +3. Предоставьте метод `wait()`, который **потребляет** защиту и возвращает буфер: владение доказывает, что передача завершена +4. Тип буфера `T` должен реализовывать маркерный трейт `DmaSafe`
-Solution +Пример решения ```rust use std::marker::PhantomData; -/// Marker trait for types that can be used as DMA buffers. -/// In real firmware: type must be repr(C) with no padding. +/// Маркерный трейт для типов, которые можно использовать как DMA-буферы. +/// В реальной прошивке: тип должен быть repr(C) без паддинга. trait DmaSafe {} impl DmaSafe for [u8; 64] {} impl DmaSafe for [u8; 256] {} -/// A guard representing an in-flight DMA transfer. -/// !Send + !Sync: can't be sent to another thread or shared. +/// Защита, представляющая DMA-передачу в процессе выполнения. +/// !Send + !Sync: её нельзя передать в другой поток или разделить. pub struct DmaTransfer { buffer: T, channel: u8, @@ -349,10 +349,10 @@ pub struct DmaTransfer { } impl DmaTransfer { - /// Start a DMA transfer. The buffer is consumed — no one else can touch it. + /// Запускает DMA-передачу. Буфер потребляется: никто другой до него не дотронется. pub fn start(buffer: T, channel: u8) -> Self { - // In real firmware: configure DMA channel, set source/dest, start transfer - println!("DMA channel {} started", channel); + // В реальной прошивке: настроить канал DMA, задать источник/приёмник, запустить передачу + println!("Канал DMA {} запущен", channel); Self { buffer, channel, @@ -360,11 +360,11 @@ impl DmaTransfer { } } - /// Wait for the transfer to complete and return the buffer. - /// Consumes self — the guard no longer exists after this. + /// Ждёт завершения передачи и возвращает буфер. + /// Потребляет self: после этого защиты больше не существует. pub fn wait(self) -> T { - // In real firmware: poll DMA status register until complete - println!("DMA channel {} complete", self.channel); + // В реальной прошивке: опрашивать регистр статуса DMA до завершения + println!("Канал DMA {} завершён", self.channel); self.buffer } } @@ -372,18 +372,18 @@ impl DmaTransfer { fn main() { let buf = [0u8; 64]; - // Start transfer — buf is moved into the guard + // Запуск передачи: buf перемещается в защиту let transfer = DmaTransfer::start(buf, 2); - // ❌ buf is no longer accessible — ownership prevents use-during-DMA + // ❌ buf больше недоступен: владение не даёт использовать его во время DMA // println!("{:?}", buf); - // ❌ Would not compile: DmaTransfer is !Send + // ❌ Не скомпилируется: DmaTransfer — !Send // std::thread::spawn(move || { transfer.wait(); }); - // ✅ Wait on the original thread, get the buffer back + // ✅ Ждём в исходном потоке и получаем буфер обратно let buf = transfer.wait(); - println!("Buffer recovered: {} bytes", buf.len()); + println!("Буфер возвращён: {} байт", buf.len()); } ``` @@ -391,25 +391,25 @@ fn main() { ```mermaid flowchart TB - subgraph compiler["Compile Time — Auto-Derived Proofs"] + subgraph compiler["Этап компиляции — автоматически выводимые доказательства"] direction TB - SEND["Send
✅ safe to move across threads"] - SYNC["Sync
✅ safe to share references"] - NOTSEND["!Send
❌ confined to one thread"] - NOTSYNC["!Sync
❌ no concurrent sharing"] + SEND["Send
✅ безопасно перемещать между потоками"] + SYNC["Sync
✅ безопасно разделять ссылки"] + NOTSEND["!Send
❌ ограничен одним потоком"] + NOTSYNC["!Sync
❌ нет конкурентного разделения"] end - subgraph types["Type Taxonomy"] + subgraph types["Таксономия типов"] direction TB - PLAIN["Primitives, String, Vec
Send + Sync"] + PLAIN["Примитивы, String, Vec
Send + Sync"] CELL["Cell, RefCell
Send + !Sync"] - RC["Rc, raw pointers
!Send + !Sync"] - MUTEX["Mutex<T>
restores Sync"] - ARC["Arc<T>
shared ownership + Send"] + RC["Rc, сырые указатели
!Send + !Sync"] + MUTEX["Mutex<T>
восстанавливает Sync"] + ARC["Arc<T>
разделяемое владение + Send"] end - subgraph runtime["Runtime"] - SAFE["Thread-safe access
No data races
No forgotten locks"] + subgraph runtime["Рантайм"] + SAFE["Потокобезопасный доступ
Нет гонок данных
Нет забытых блокировок"] end SEND --> PLAIN @@ -431,16 +431,16 @@ flowchart TB style SAFE fill:#c8e6c9,color:#000 ``` -## Key Takeaways +## Ключевые выводы -1. **`Send` and `Sync` are compile-time proofs about concurrency safety** — the compiler derives them structurally by inspecting every field. No annotation, no runtime cost, no opt-in needed. +1. **`Send` и `Sync` — это доказательства времени компиляции о безопасности конкурентности**: компилятор выводит их структурно, проверяя каждое поле. Ни аннотаций, ни накладных расходов во время выполнения, ни явного согласия не нужно. -2. **Raw pointers automatically opt out** — any type containing `*const T` or `*mut T` becomes `!Send + !Sync`. This makes peripheral handles naturally thread-confined. +2. **Сырые указатели автоматически отказываются от трейтов**: любой тип, содержащий `*const T` или `*mut T`, становится `!Send + !Sync`. Поэтому дескрипторы периферии естественно ограничены одним потоком. -3. **`PhantomData<*const ()>` is the explicit opt-out** — when a type has no raw pointer but should still be thread-confined (C library handles, GPU contexts), a phantom field does the job. +3. **`PhantomData<*const ()>` — явный отказ**: когда у типа нет сырого указателя, но его всё равно нужно ограничить одним потоком (дескрипторы библиотек на C, контексты GPU), задачу решает phantom-поле. -4. **`Mutex` restores `Sync` with proof** — the compiler structurally proves that all access goes through the lock. Unlike C's `pthread_mutex_t`, you cannot forget to lock. +4. **`Mutex` восстанавливает `Sync` с доказательством**: компилятор структурно доказывает, что весь доступ проходит через блокировку. В отличие от `pthread_mutex_t` из C, забыть взять блокировку невозможно. -5. **Function bounds are theorems** — `F: Send + 'static` in a spawner's signature is a compile-time proof obligation: every call site must prove its closure is thread-safe. Compare to C's `void *arg` which accepts anything. +5. **Ограничения функций — это теоремы**: `F: Send + 'static` в сигнатуре планировщика — обязательство доказательства на этапе компиляции: каждый вызов должен доказать, что его замыкание потокобезопасно. Сравните с `void *arg` в C, который принимает что угодно. -6. **The pattern complements all other correctness techniques** — typestate proves protocol sequencing, phantom types prove permissions, `const fn` proves value invariants, and `Send`/`Sync` prove concurrency safety. Together they cover the full correctness surface. +6. **Паттерн дополняет все остальные техники корректности**: typestate доказывает порядок протокола, phantom-типы доказывают права доступа, `const fn` доказывает инварианты значений, а `Send`/`Sync` доказывают безопасность конкурентности. Вместе они покрывают всю поверхность корректности. diff --git a/type-driven-correctness-book/src/ch17-redfish-applied-walkthrough.md b/type-driven-correctness-book/src/ch17-redfish-applied-walkthrough.md index f13a380..3ff0f9c 100644 --- a/type-driven-correctness-book/src/ch17-redfish-applied-walkthrough.md +++ b/type-driven-correctness-book/src/ch17-redfish-applied-walkthrough.md @@ -1,31 +1,28 @@ -# Applied Walkthrough — Type-Safe Redfish Client 🟡 +# Пошаговый разбор: типобезопасный клиент Redfish 🟡 -> **What you'll learn:** How to compose type-state sessions, capability tokens, phantom-typed resource navigation, dimensional analysis, validated boundaries, builder type-state, and single-use types into a complete, zero-overhead Redfish client — where every protocol violation is a compile error. +> **Что вы узнаете:** как скомпоновать typestate сессий, capability-токены, навигацию по ресурсам с phantom-типами, анализ размерностей, проверенные границы, typestate билдера и одноразовые типы в полноценный клиент Redfish с нулевыми накладными расходами, где каждое нарушение протокола становится ошибкой компиляции. > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (typed commands), [ch03](ch03-single-use-types-cryptographic-guarantee.md) (single-use types), [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens), [ch05](ch05-protocol-state-machines-type-state-for-r.md) (type-state), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional types), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (validated boundaries), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types), [ch10](ch10-putting-it-all-together-a-complete-diagn.md) (IPMI integration), [ch11](ch11-fourteen-tricks-from-the-trenches.md) (trick 4 — builder type-state) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (типизированные команды), [гл. 03](ch03-single-use-types-cryptographic-guarantee.md) (одноразовые типы), [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены), [гл. 05](ch05-protocol-state-machines-type-state-for-r.md) (typestate), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (размерные типы), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (проверенные границы), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы), [гл. 10](ch10-putting-it-all-together-a-complete-diagn.md) (интеграция с IPMI), [гл. 11](ch11-fourteen-tricks-from-the-trenches.md) (приём 4: билдер с typestate) -## Why Redfish Deserves Its Own Chapter +## Почему Redfish заслуживает отдельной главы -Chapter 10 composes the core patterns around IPMI — a byte-level protocol. But -most BMC platforms now expose a **Redfish** REST API alongside (or instead of) -IPMI, and Redfish introduces its own category of correctness hazards: +Глава 10 строит основные паттерны вокруг IPMI, протокола на уровне байтов. Но большинство платформ BMC сейчас предоставляют REST API **Redfish** наряду с IPMI (или вместо него), и Redfish приносит собственную категорию опасностей для корректности: -| Hazard | Example | Consequence | -|--------|---------|-------------| -| Malformed URI | `GET /redfish/v1/Chassis/1/Processors` (wrong parent) | 404 or wrong data silently returned | -| Action on wrong power state | `Reset(ForceOff)` on an already-off system | BMC returns error, or worse, races with another operation | -| Missing privilege | Operator-level code calls `Manager.ResetToDefaults` | 403 in production, security audit finding | -| Incomplete PATCH | Omit a required BIOS attribute from a PATCH body | Silent no-op or partial config corruption | -| Unverified firmware apply | `SimpleUpdate` invoked before image integrity check | Bricked BMC | -| Schema version mismatch | Access `LastResetTime` on a v1.5 BMC (added in v1.13) | `null` field → runtime panic | -| Unit confusion in telemetry | Compare inlet temperature (°C) to power draw (W) | Nonsensical threshold decisions | +| Опасность | Пример | Последствие | +|-----------|--------|-------------| +| Некорректный URI | `GET /redfish/v1/Chassis/1/Processors` (неверный родитель) | Тихо возвращается 404 или неверные данные | +| Действие в неверном состоянии питания | `Reset(ForceOff)` для уже выключенной системы | BMC возвращает ошибку или, хуже, гонка с другой операцией | +| Отсутствие привилегии | Код уровня оператора вызывает `Manager.ResetToDefaults` | 403 в продакшене, замечание службы безопасности | +| Неполный PATCH | В теле PATCH пропущен обязательный атрибут BIOS | Тихий no-op или частичное повреждение конфигурации | +| Непроверенное применение прошивки | `SimpleUpdate` вызван до проверки целостности образа | Превращённый в кирпич BMC | +| Несоответствие версии схемы | Обращение к `LastResetTime` на BMC v1.5 (добавлено в v1.13) | Поле `null` → паника во время выполнения | +| Путаница единиц в телеметрии | Сравнение температуры на входе (°C) с потреблением мощности (Вт) | Бессмысленные пороговые решения | -In C, Python, or untyped Rust, every one of these is prevented by discipline and -testing alone. This chapter makes them **compile errors**. +В C, Python или нетипизированном Rust каждую из этих опасностей предотвращают только дисциплина и тестирование. Эта глава делает их **ошибками компиляции**. -## The Untyped Redfish Client +## Нетипизированный клиент Redfish -A typical Redfish client looks like this: +Типичный клиент Redfish выглядит так: ```rust,ignore use std::collections::HashMap; @@ -38,36 +35,36 @@ struct RedfishClient { impl RedfishClient { fn get(&self, path: &str) -> Result { // ... HTTP GET ... - Ok(serde_json::json!({})) // stub + Ok(serde_json::json!({})) // заглушка } fn patch(&self, path: &str, body: &serde_json::Value) -> Result<(), String> { // ... HTTP PATCH ... - Ok(()) // stub + Ok(()) // заглушка } fn post_action(&self, path: &str, body: &serde_json::Value) -> Result<(), String> { // ... HTTP POST ... - Ok(()) // stub + Ok(()) // заглушка } } fn check_thermal(client: &RedfishClient) -> Result<(), String> { let resp = client.get("/redfish/v1/Chassis/1/Thermal")?; - // 🐛 Is this field always present? What if the BMC returns null? + // 🐛 Это поле всегда присутствует? Что, если BMC вернёт null? let cpu_temp = resp["Temperatures"][0]["ReadingCelsius"] .as_f64().unwrap(); let fan_rpm = resp["Fans"][0]["Reading"] .as_f64().unwrap(); - // 🐛 Comparing °C to RPM — both are f64 + // 🐛 Сравнение °C с об/мин: оба — f64 if cpu_temp > fan_rpm { - println!("thermal issue"); + println!("проблема с температурой"); } - // 🐛 Is this the right path? No compile-time check. + // 🐛 Это правильный путь? Проверки на этапе компиляции нет. client.post_action( "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", &serde_json::json!({"ResetType": "ForceOff"}) @@ -77,15 +74,13 @@ fn check_thermal(client: &RedfishClient) -> Result<(), String> { } ``` -This "works" — until it doesn't. Every `unwrap()` is a potential panic, every -string path is an unchecked assumption, and unit confusion is invisible. +Это «работает», пока не перестаёт работать. Каждый `unwrap()` — потенциальная паника, каждый строковый путь — непроверенное допущение, а путаница единиц не видна. --- -## Section 1 — Session Lifecycle (Type-State, ch05) +## Раздел 1 — Жизненный цикл сессии (typestate, гл. 05) -A Redfish session has a strict lifecycle: connect → authenticate → use → close. -Encode each state as a distinct type. +У сессии Redfish строгий жизненный цикл: подключение → аутентификация → использование → закрытие. Каждое состояние кодируем отдельным типом. ```mermaid stateDiagram-v2 @@ -96,14 +91,14 @@ stateDiagram-v2 Authenticated --> Closed : logout() Closed --> [*] - note right of Authenticated : API calls only exist here - note right of Connected : get() → compile error + note right of Authenticated : вызовы API существуют только здесь + note right of Connected : get() → ошибка компиляции ``` ```rust,ignore use std::marker::PhantomData; -// ──── Session States ──── +// ──── Состояния сессии ──── pub struct Disconnected; pub struct Connected; @@ -124,11 +119,11 @@ impl RedfishSession { } } - /// Transition: Disconnected → Connected. - /// Verifies the service root is reachable. + /// Переход: Disconnected → Connected. + /// Проверяет, что корневой сервис доступен. pub fn connect(self) -> Result, RedfishError> { - // GET /redfish/v1 — verify service root - println!("Connecting to {}/redfish/v1", self.base_url); + // GET /redfish/v1 — проверяем корневой сервис + println!("Подключение к {}/redfish/v1", self.base_url); Ok(RedfishSession { base_url: self.base_url, auth_token: None, @@ -138,15 +133,15 @@ impl RedfishSession { } impl RedfishSession { - /// Transition: Connected → Authenticated. - /// Creates a session via POST /redfish/v1/SessionService/Sessions. + /// Переход: Connected → Authenticated. + /// Создаёт сессию через POST /redfish/v1/SessionService/Sessions. pub fn login( self, user: &str, _pass: &str, ) -> Result<(RedfishSession, LoginToken), RedfishError> { // POST /redfish/v1/SessionService/Sessions - println!("Authenticated as {}", user); + println!("Аутентификация как {}", user); let token = "X-Auth-Token-abc123".to_string(); Ok(( RedfishSession { @@ -160,11 +155,11 @@ impl RedfishSession { } impl RedfishSession { - /// Only available on Authenticated sessions. + /// Доступен только у аутентифицированных сессий. fn http_get(&self, path: &str) -> Result { let _url = format!("{}{}", self.base_url, path); - // ... HTTP GET with auth_token header ... - Ok(serde_json::json!({})) // stub + // ... HTTP GET с заголовком auth_token ... + Ok(serde_json::json!({})) // заглушка } fn http_patch( @@ -174,7 +169,7 @@ impl RedfishSession { ) -> Result { let _url = format!("{}{}", self.base_url, path); let _ = body; - Ok(serde_json::json!({})) // stub + Ok(serde_json::json!({})) // заглушка } fn http_post( @@ -184,22 +179,22 @@ impl RedfishSession { ) -> Result { let _url = format!("{}{}", self.base_url, path); let _ = body; - Ok(serde_json::json!({})) // stub + Ok(serde_json::json!({})) // заглушка } - /// Transition: Authenticated → Closed (session consumed). + /// Переход: Authenticated → Closed (сессия потребляется). pub fn logout(self) { // DELETE /redfish/v1/SessionService/Sessions/{id} - println!("Session closed"); - // self is consumed — can't use the session after logout + println!("Сессия закрыта"); + // self потреблён: после logout сессию использовать нельзя } } -// Attempting to call http_get on a non-Authenticated session: +// Попытка вызвать http_get у неаутентифицированной сессии: // // let session = RedfishSession::new("bmc01").connect()?; // session.http_get("/redfish/v1/Systems"); -// ❌ ERROR: method `http_get` not found for `RedfishSession` +// ❌ ОШИБКА: метод `http_get` не найден для `RedfishSession` #[derive(Debug)] pub enum RedfishError { @@ -212,45 +207,42 @@ pub enum RedfishError { impl std::fmt::Display for RedfishError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::ConnectionFailed(msg) => write!(f, "connection failed: {msg}"), - Self::AuthenticationFailed(msg) => write!(f, "auth failed: {msg}"), + Self::ConnectionFailed(msg) => write!(f, "ошибка подключения: {msg}"), + Self::AuthenticationFailed(msg) => write!(f, "ошибка аутентификации: {msg}"), Self::HttpError { status, message } => write!(f, "HTTP {status}: {message}"), - Self::ValidationError(msg) => write!(f, "validation: {msg}"), + Self::ValidationError(msg) => write!(f, "валидация: {msg}"), } } } ``` -**Bug class eliminated:** sending requests on a disconnected or unauthenticated -session. The method simply doesn't exist — no runtime check to forget. +**Устранённый класс ошибок:** отправка запросов в отключённой или неаутентифицированной сессии. Метод просто не существует, и нечего забыть проверить во время выполнения. --- -## Section 2 — Privilege Tokens (Capability Tokens, ch04) +## Раздел 2 — Токены привилегий (capability-токены, гл. 04) -Redfish defines four privilege levels: `Login`, `ConfigureComponents`, -`ConfigureManager`, `ConfigureSelf`. Rather than checking permissions at -runtime, encode them as zero-sized proof tokens. +Redfish определяет четыре уровня привилегий: `Login`, `ConfigureComponents`, `ConfigureManager`, `ConfigureSelf`. Вместо проверки прав во время выполнения кодируем их как токены-доказательства нулевого размера. ```rust,ignore -// ──── Privilege Tokens (zero-sized) ──── +// ──── Токены привилегий (нулевого размера) ──── -/// Proof the caller has Login privilege. -/// Returned by successful login — the only way to obtain one. +/// Доказательство, что у вызывающего есть привилегия Login. +/// Возвращается при успешном входе: это единственный способ его получить. pub struct LoginToken { _private: () } -/// Proof the caller has ConfigureComponents privilege. -/// Only obtainable by admin-level authentication. +/// Доказательство, что у вызывающего есть привилегия ConfigureComponents. +/// Получить его может только аутентификация на уровне администратора. pub struct ConfigureComponentsToken { _private: () } -/// Proof the caller has ConfigureManager privilege (firmware updates, etc.). +/// Доказательство, что у вызывающего есть привилегия ConfigureManager (обновления прошивки и т. д.). pub struct ConfigureManagerToken { _private: () } -// Extend login to return privilege tokens based on role: +// Расширяем вход так, чтобы он возвращал токены привилегий в зависимости от роли: impl RedfishSession { - /// Admin login — returns all privilege tokens. + /// Вход администратора: возвращает все токены привилегий. pub fn login_admin( self, user: &str, @@ -270,7 +262,7 @@ impl RedfishSession { )) } - /// Operator login — returns Login + ConfigureComponents only. + /// Вход оператора: возвращает только Login и ConfigureComponents. pub fn login_operator( self, user: &str, @@ -288,7 +280,7 @@ impl RedfishSession { )) } - /// Read-only login — returns Login token only. + /// Вход только для чтения: возвращает только токен Login. pub fn login_readonly( self, user: &str, @@ -299,7 +291,7 @@ impl RedfishSession { } ``` -Now privilege requirements are part of the function signature: +Теперь требования к привилегиям — часть сигнатуры функции: ```rust,ignore # use std::marker::PhantomData; @@ -310,16 +302,16 @@ Now privilege requirements are part of the function signature: # pub struct ConfigureManagerToken { _private: () } # #[derive(Debug)] pub enum RedfishError { HttpError { status: u16, message: String } } -/// Anyone with Login can read thermal data. +/// Любой с Login может читать данные о температуре. fn get_thermal( session: &RedfishSession, _proof: &LoginToken, ) -> Result { // GET /redfish/v1/Chassis/1/Thermal - Ok(serde_json::json!({})) // stub + Ok(serde_json::json!({})) // заглушка } -/// Changing boot order requires ConfigureComponents. +/// Изменение порядка загрузки требует ConfigureComponents. fn set_boot_order( session: &RedfishSession, _proof: &ConfigureComponentsToken, @@ -330,7 +322,7 @@ fn set_boot_order( Ok(()) } -/// Factory reset requires ConfigureManager. +/// Сброс к заводским настройкам требует ConfigureManager. fn reset_to_defaults( session: &RedfishSession, _proof: &ConfigureManagerToken, @@ -339,44 +331,41 @@ fn reset_to_defaults( Ok(()) } -// Operator code calling reset_to_defaults: +// Код оператора, вызывающий reset_to_defaults: // // let (session, login, configure) = session.login_operator("op", "pass")?; // reset_to_defaults(&session, &???); -// ❌ ERROR: no ConfigureManagerToken available — operator can't do this +// ❌ ОШИБКА: нет доступного ConfigureManagerToken, оператор не может этого сделать ``` -**Bug class eliminated:** privilege escalation. An operator-level login physically -cannot produce a `ConfigureManagerToken` — the compiler won't let the code reference -one. Zero runtime cost: for the compiled binary, these tokens don't exist. +**Устранённый класс ошибок:** эскалация привилегий. Вход на уровне оператора физически не может выдать `ConfigureManagerToken`: компилятор не даст коду на него сослаться. Нулевые накладные расходы: в скомпилированном бинарнике этих токенов не существует. --- -## Section 3 — Typed Resource Navigation (Phantom Types, ch09) +## Раздел 3 — Типизированная навигация по ресурсам (phantom-типы, гл. 09) -Redfish resources form a tree. Encoding the hierarchy as types prevents constructing -illegal URIs: +Ресурсы Redfish образуют дерево. Кодирование иерархии в типах не даёт построить недопустимые URI: ```mermaid graph TD - SR[ServiceRoot] --> Systems - SR --> Chassis - SR --> Managers - SR --> UpdateService - Systems --> CS[ComputerSystem] - CS --> Processors - CS --> Memory - CS --> Bios - Chassis --> Ch1[Chassis Instance] - Ch1 --> Thermal - Ch1 --> Power - Managers --> Mgr[Manager Instance] + SR["Корень службы (ServiceRoot)"] --> Systems["Системы"] + SR --> Chassis["Шасси"] + SR --> Managers["Менеджеры"] + SR --> UpdateService["Служба обновления"] + Systems --> CS["ComputerSystem"] + CS --> Processors["Процессоры"] + CS --> Memory["Память"] + CS --> Bios["BIOS"] + Chassis --> Ch1["Экземпляр шасси"] + Ch1 --> Thermal["Термо (Thermal)"] + Ch1 --> Power["Питание (Power)"] + Managers --> Mgr["Экземпляр менеджера"] ``` ```rust,ignore use std::marker::PhantomData; -// ──── Resource Type Markers ──── +// ──── Маркеры типов ресурсов ──── pub struct ServiceRoot; pub struct SystemsCollection; @@ -390,7 +379,7 @@ pub struct ManagersCollection; pub struct ManagerInstance; pub struct UpdateServiceResource; -// ──── Typed Resource Path ──── +// ──── Типизированный путь к ресурсу ──── pub struct RedfishPath { uri: String, @@ -492,44 +481,41 @@ impl RedfishPath { } } -// ── Usage ── +// ── Использование ── fn build_paths() { let root = RedfishPath::root(); - // ✅ Valid navigation + // ✅ Корректная навигация let thermal = root.chassis().instance("1").thermal(); assert_eq!(thermal.uri(), "/redfish/v1/Chassis/1/Thermal"); let bios = root.systems().system("1").bios(); assert_eq!(bios.uri(), "/redfish/v1/Systems/1/Bios"); - // ❌ Compile error: ServiceRoot has no .thermal() method + // ❌ Ошибка компиляции: у ServiceRoot нет метода .thermal() // root.thermal(); - // ❌ Compile error: SystemsCollection has no .bios() method + // ❌ Ошибка компиляции: у SystemsCollection нет метода .bios() // root.systems().bios(); - // ❌ Compile error: ChassisInstance has no .bios() method + // ❌ Ошибка компиляции: у ChassisInstance нет метода .bios() // root.chassis().instance("1").bios(); } ``` -**Bug class eliminated:** malformed URIs, navigating to a child resource that -doesn't exist under the given parent. The hierarchy is enforced structurally — -you can only reach `Thermal` through `Chassis → Instance → Thermal`. +**Устранённый класс ошибок:** некорректные URI, переход к дочернему ресурсу, которого нет под данным родителем. Иерархия обеспечивается структурно: до `Thermal` можно добраться только через `Chassis → Instance → Thermal`. --- -## Section 4 — Typed Telemetry Reads (Typed Commands + Dimensional Analysis, ch02 + ch06) +## Раздел 4 — Типизированное чтение телеметрии (типизированные команды + анализ размерностей, гл. 02 + гл. 06) -Combine typed resource paths with dimensional return types so the compiler knows -what unit every reading carries: +Соединяем типизированные пути к ресурсам с размерными типами возвращаемых значений, чтобы компилятор знал, какую единицу несёт каждое показание: ```rust,ignore use std::marker::PhantomData; -// ──── Dimensional Types (ch06) ──── +// ──── Размерные типы (гл. 06) ──── #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Celsius(pub f64); @@ -543,15 +529,15 @@ pub struct Watts(pub f64); #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] pub struct Volts(pub f64); -// ──── Typed Redfish GET (ch02 pattern applied to REST) ──── +// ──── Типизированный GET Redfish (паттерн гл. 02, применённый к REST) ──── -/// A Redfish resource type determines its parsed response. +/// Тип ресурса Redfish определяет его разобранный ответ. pub trait RedfishResource { type Response; fn parse(json: &serde_json::Value) -> Result; } -// ──── Validated Thermal Response (ch07) ──── +// ──── Проверенный ответ Thermal (гл. 07) ──── #[derive(Debug)] pub struct ValidThermalResponse { @@ -562,7 +548,7 @@ pub struct ValidThermalResponse { #[derive(Debug)] pub struct TemperatureReading { pub name: String, - pub reading: Celsius, // ← dimensional type, not f64 + pub reading: Celsius, // ← размерный тип, а не f64 pub upper_critical: Celsius, pub status: HealthStatus, } @@ -570,7 +556,7 @@ pub struct TemperatureReading { #[derive(Debug)] pub struct FanReading { pub name: String, - pub reading: Rpm, // ← dimensional type, not u32 + pub reading: Rpm, // ← размерный тип, а не u32 pub status: HealthStatus, } @@ -581,11 +567,11 @@ impl RedfishResource for ThermalResource { type Response = ValidThermalResponse; fn parse(json: &serde_json::Value) -> Result { - // Parse and validate in one pass — boundary validation (ch07) + // Разбор и валидация за один проход: валидация на границе (гл. 07) let temps = json["Temperatures"] .as_array() .ok_or_else(|| RedfishError::ValidationError( - "missing Temperatures array".into(), + "отсутствует массив Temperatures".into(), ))? .iter() .map(|t| { @@ -593,20 +579,20 @@ impl RedfishResource for ThermalResource { name: t["Name"] .as_str() .ok_or_else(|| RedfishError::ValidationError( - "missing Name".into(), + "отсутствует Name".into(), ))? .to_string(), reading: Celsius( t["ReadingCelsius"] .as_f64() .ok_or_else(|| RedfishError::ValidationError( - "missing ReadingCelsius".into(), + "отсутствует ReadingCelsius".into(), ))?, ), upper_critical: Celsius( t["UpperThresholdCritical"] .as_f64() - .unwrap_or(105.0), // safe default for missing threshold + .unwrap_or(105.0), // безопасное значение по умолчанию для отсутствующего порога ), status: parse_health( t["Status"]["Health"] @@ -620,7 +606,7 @@ impl RedfishResource for ThermalResource { let fans = json["Fans"] .as_array() .ok_or_else(|| RedfishError::ValidationError( - "missing Fans array".into(), + "отсутствует массив Fans".into(), ))? .iter() .map(|f| { @@ -628,14 +614,14 @@ impl RedfishResource for ThermalResource { name: f["Name"] .as_str() .ok_or_else(|| RedfishError::ValidationError( - "missing Name".into(), + "отсутствует Name".into(), ))? .to_string(), reading: Rpm( f["Reading"] .as_u64() .ok_or_else(|| RedfishError::ValidationError( - "missing Reading".into(), + "отсутствует Reading".into(), ))? as u32, ), status: parse_health( @@ -659,7 +645,7 @@ fn parse_health(s: &str) -> HealthStatus { } } -// ──── Typed GET on Authenticated Session ──── +// ──── Типизированный GET в аутентифицированной сессии ──── impl RedfishSession { pub fn get_resource( @@ -671,7 +657,7 @@ impl RedfishSession { } } -// ── Usage ── +// ── Использование ── fn read_thermal( session: &RedfishSession, @@ -679,19 +665,19 @@ fn read_thermal( ) -> Result<(), RedfishError> { let path = RedfishPath::root().chassis().instance("1").thermal(); - // Response type is inferred: ValidThermalResponse + // Тип ответа выводится: ValidThermalResponse let thermal = session.get_resource(&path)?; for t in &thermal.temperatures { - // t.reading is Celsius — can only compare with Celsius + // t.reading — Celsius: сравнивать можно только с Celsius if t.reading > t.upper_critical { - println!("CRITICAL: {} at {:?}", t.name, t.reading); + println!("КРИТИЧНО: {} при {:?}", t.name, t.reading); } - // ❌ Compile error: cannot compare Celsius with Rpm + // ❌ Ошибка компиляции: нельзя сравнить Celsius с Rpm // if t.reading > thermal.fans[0].reading { } - // ❌ Compile error: cannot compare Celsius with Watts + // ❌ Ошибка компиляции: нельзя сравнить Celsius с Watts // if t.reading > Watts(350.0) { } } @@ -699,29 +685,26 @@ fn read_thermal( } ``` -**Bug classes eliminated:** -- **Unit confusion:** `Celsius` ≠ `Rpm` ≠ `Watts` — the compiler rejects comparisons. -- **Missing field panics:** `parse()` validates at the boundary; `ValidThermalResponse` - guarantees all fields are present. -- **Wrong response type:** `get_resource(&thermal_path)` returns `ValidThermalResponse`, - not raw JSON. The resource type determines the response type at compile time. +**Устранённые классы ошибок:** +- **Путаница единиц:** `Celsius` ≠ `Rpm` ≠ `Watts`, компилятор отвергает сравнения. +- **Паника из-за отсутствующего поля:** `parse()` валидирует на границе; `ValidThermalResponse` гарантирует наличие всех полей. +- **Неверный тип ответа:** `get_resource(&thermal_path)` возвращает `ValidThermalResponse`, а не сырой JSON. Тип ресурса определяет тип ответа на этапе компиляции. --- -## Section 5 — PATCH with Builder Type-State (ch11, Trick 4) +## Раздел 5 — PATCH с билдером typestate (гл. 11, приём 4) -Redfish PATCH payloads must contain specific fields. A builder that gates -`.apply()` on required fields being set prevents incomplete or empty patches: +Полезные нагрузки PATCH в Redfish должны содержать определённые поля. Билдер, который открывает `.apply()` только при установке обязательных полей, не даёт отправить неполные или пустые патчи: ```rust,ignore use std::marker::PhantomData; -// ──── Type-level booleans for required fields ──── +// ──── Типовые булевы значения для обязательных полей ──── pub struct FieldUnset; pub struct FieldSet; -// ──── BIOS Settings PATCH Builder ──── +// ──── Билдер PATCH для настроек BIOS ──── pub struct BiosPatchBuilder { boot_order: Option>, @@ -740,7 +723,7 @@ impl BiosPatchBuilder { } impl BiosPatchBuilder { - /// Set boot order — transitions the BootOrder marker to FieldSet. + /// Задаёт порядок загрузки: переводит маркер BootOrder в FieldSet. pub fn boot_order(self, order: Vec) -> BiosPatchBuilder { BiosPatchBuilder { boot_order: Some(order), @@ -751,7 +734,7 @@ impl BiosPatchBuilder { } impl BiosPatchBuilder { - /// Set TPM state — transitions the TpmState marker to FieldSet. + /// Задаёт состояние TPM: переводит маркер TpmState в FieldSet. pub fn tpm_enabled(self, enabled: bool) -> BiosPatchBuilder { BiosPatchBuilder { boot_order: self.boot_order, @@ -762,7 +745,7 @@ impl BiosPatchBuilder { } impl BiosPatchBuilder { - /// .apply() only exists when ALL required fields are set. + /// .apply() существует только тогда, когда ВСЕ обязательные поля заданы. pub fn apply( self, session: &RedfishSession, @@ -785,7 +768,7 @@ impl BiosPatchBuilder { } } -// ── Usage ── +// ── Использование ── fn configure_bios( session: &RedfishSession, @@ -793,18 +776,18 @@ fn configure_bios( ) -> Result<(), RedfishError> { let system = RedfishPath::root().systems().system("1"); - // ✅ Both required fields set — .apply() is available + // ✅ Оба обязательных поля заданы: .apply() доступен BiosPatchBuilder::new() .boot_order(vec!["Pxe".into(), "Hdd".into()]) .tpm_enabled(true) .apply(session, configure, &system)?; - // ❌ Compile error: .apply() not found on BiosPatchBuilder + // ❌ Ошибка компиляции: .apply() не найден у BiosPatchBuilder // BiosPatchBuilder::new() // .boot_order(vec!["Pxe".into()]) // .apply(session, configure, &system)?; - // ❌ Compile error: .apply() not found on BiosPatchBuilder + // ❌ Ошибка компиляции: .apply() не найден у BiosPatchBuilder // BiosPatchBuilder::new() // .apply(session, configure, &system)?; @@ -812,38 +795,37 @@ fn configure_bios( } ``` -**Bug classes eliminated:** -- **Empty PATCH:** Can't call `.apply()` without setting every required field. -- **Missing privilege:** `.apply()` requires `&ConfigureComponentsToken`. -- **Wrong resource:** Takes a `&RedfishPath`, not a raw string. +**Устранённые классы ошибок:** +- **Пустой PATCH:** нельзя вызвать `.apply()`, не задав каждое обязательное поле. +- **Отсутствие привилегии:** `.apply()` требует `&ConfigureComponentsToken`. +- **Неверный ресурс:** принимает `&RedfishPath`, а не строку. --- -## Section 6 — Firmware Update Lifecycle (Single-Use + Type-State, ch03 + ch05) +## Раздел 6 — Жизненный цикл обновления прошивки (одноразовые типы + typestate, гл. 03 + гл. 05) -The Redfish `UpdateService` has a strict sequence: push image → verify → -apply → reboot. Each phase must happen exactly once, in order. +У `UpdateService` в Redfish строгая последовательность: загрузить образ → проверить → применить → перезагрузить. Каждая фаза должна произойти ровно один раз и в порядке очерёдности. ```mermaid stateDiagram-v2 [*] --> Idle Idle --> Uploading : push_image() - Uploading --> Uploaded : upload completes + Uploading --> Uploaded : загрузка завершена Uploaded --> Verified : verify() ✓ Uploaded --> Failed : verify() ✗ - Verified --> Applying : apply() — consumes Verified - Applying --> NeedsReboot : apply completes + Verified --> Applying : apply() — потребляет Verified + Applying --> NeedsReboot : применение завершено NeedsReboot --> [*] : reboot() Failed --> [*] - note right of Verified : apply() consumes this state — - note right of Verified : can't apply twice + note right of Verified : apply() потребляет это состояние — + note right of Verified : применить дважды нельзя ``` ```rust,ignore use std::marker::PhantomData; -// ──── Firmware Update States ──── +// ──── Состояния обновления прошивки ──── pub struct FwIdle; pub struct FwUploaded; @@ -864,9 +846,9 @@ impl FirmwareUpdate { image: &[u8], ) -> Result, RedfishError> { // POST /redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate - // or multipart push to /redfish/v1/UpdateService/upload + // или multipart-загрузка в /redfish/v1/UpdateService/upload let _ = image; - println!("Image uploaded ({} bytes)", image.len()); + println!("Образ загружен ({} байт)", image.len()); Ok(FirmwareUpdate { task_uri: "/redfish/v1/TaskService/Tasks/1".to_string(), image_hash: "sha256:abc123".to_string(), @@ -876,10 +858,10 @@ impl FirmwareUpdate { } impl FirmwareUpdate { - /// Verify image integrity. Returns FwVerified on success. + /// Проверяет целостность образа. При успехе возвращает FwVerified. pub fn verify(self) -> Result, RedfishError> { - // Poll task until verification complete - println!("Image verified: {}", self.image_hash); + // Опрашиваем задачу до завершения проверки + println!("Образ проверен: {}", self.image_hash); Ok(FirmwareUpdate { task_uri: self.task_uri, image_hash: self.image_hash, @@ -889,12 +871,12 @@ impl FirmwareUpdate { } impl FirmwareUpdate { - /// Apply the update. Consumes self — can't apply twice. - /// This is the single-use pattern from ch03. + /// Применяет обновление. Потребляет self: применить дважды нельзя. + /// Это одноразовый паттерн из гл. 03. pub fn apply(self) -> Result, RedfishError> { - // PATCH /redfish/v1/UpdateService — set ApplyTime - println!("Firmware applied from {}", self.task_uri); - // self is moved — calling apply() again is a compile error + // PATCH /redfish/v1/UpdateService: задаём ApplyTime + println!("Прошивка применена из {}", self.task_uri); + // self перемещён: повторный вызов apply() — ошибка компиляции Ok(FirmwareUpdate { task_uri: self.task_uri, image_hash: self.image_hash, @@ -904,7 +886,7 @@ impl FirmwareUpdate { } impl FirmwareUpdate { - /// Reboot to activate the new firmware. + /// Перезагрузка, чтобы активировать новую прошивку. pub fn reboot( self, session: &RedfishSession, @@ -912,84 +894,83 @@ impl FirmwareUpdate { ) -> Result<(), RedfishError> { // POST .../Actions/Manager.Reset {"ResetType": "GracefulRestart"} let _ = session; - println!("BMC rebooting to activate firmware"); + println!("BMC перезагружается для активации прошивки"); Ok(()) } } -// ── Usage ── +// ── Использование ── fn update_bmc_firmware( session: &RedfishSession, manager_proof: &ConfigureManagerToken, image: &[u8], ) -> Result<(), RedfishError> { - // Each step returns the next state — the old state is consumed + // Каждый шаг возвращает следующее состояние, предыдущее потребляется let uploaded = FirmwareUpdate::push_image(session, manager_proof, image)?; let verified = uploaded.verify()?; let needs_reboot = verified.apply()?; needs_reboot.reboot(session, manager_proof)?; - // ❌ Compile error: use of moved value `verified` + // ❌ Ошибка компиляции: использование перемещённого значения `verified` // verified.apply()?; - // ❌ Compile error: FirmwareUpdate has no .apply() method - // uploaded.apply()?; // must verify first! + // ❌ Ошибка компиляции: у FirmwareUpdate нет метода .apply() + // uploaded.apply()?; // сначала нужно проверить! - // ❌ Compile error: push_image requires &ConfigureManagerToken + // ❌ Ошибка компиляции: push_image требует &ConfigureManagerToken // FirmwareUpdate::push_image(session, &login_token, image)?; Ok(()) } ``` -**Bug classes eliminated:** -- **Applying unverified firmware:** `.apply()` only exists on `FwVerified`. -- **Double apply:** `apply()` consumes `self` — moved value can't be reused. -- **Skipping reboot:** `FwNeedsReboot` is a distinct type; you can't accidentally - continue normal operations while firmware is staged. -- **Unauthorized update:** `push_image()` requires `&ConfigureManagerToken`. +**Устранённые классы ошибок:** +- **Применение непроверенной прошивки:** `.apply()` существует только у `FwVerified`. +- **Двойное применение:** `apply()` потребляет `self`, перемещённое значение нельзя использовать повторно. +- **Пропуск перезагрузки:** `FwNeedsReboot` — отдельный тип; нельзя по ошибке продолжить обычную работу, пока прошивка подготовлена. +- **Несанкционированное обновление:** `push_image()` требует `&ConfigureManagerToken`. --- -## Section 7 — Putting It All Together +## Раздел 7 — Собираем всё вместе -Here's the full diagnostic workflow composing all six sections: +Вот полный диагностический сценарий, который объединяет все шесть разделов: ```rust,ignore fn full_redfish_diagnostic() -> Result<(), RedfishError> { - // ── 1. Session lifecycle (Section 1) ── + // ── 1. Жизненный цикл сессии (раздел 1) ── let session = RedfishSession::new("bmc01.lab.local"); let session = session.connect()?; - // ── 2. Privilege tokens (Section 2) ── - // Admin login — receives all capability tokens + // ── 2. Токены привилегий (раздел 2) ── + // Вход администратора: получаем все токены возможностей let (session, _login, configure, manager) = session.login_admin("admin", "p@ssw0rd")?; - // ── 3. Typed navigation (Section 3) ── + // ── 3. Типизированная навигация (раздел 3) ── let thermal_path = RedfishPath::root() .chassis() .instance("1") .thermal(); - // ── 4. Typed telemetry read (Section 4) ── + // ── 4. Типизированное чтение телеметрии (раздел 4) ── let thermal: ValidThermalResponse = session.get_resource(&thermal_path)?; for t in &thermal.temperatures { - // Celsius can only compare with Celsius — dimensional safety + // Celsius сравнивается только с Celsius: размерная безопасность if t.reading > t.upper_critical { - println!("🔥 {} is critical: {:?}", t.name, t.reading); + println!("🔥 {} в критическом состоянии: {:?}", t.name, t.reading); } } for f in &thermal.fans { if f.reading < Rpm(1000) { - println!("⚠ {} below threshold: {:?}", f.name, f.reading); + println!("⚠ {} ниже порога: {:?}", f.name, f.reading); } } - // ── 5. Type-safe PATCH (Section 5) ── + // ── 5. Типобезопасный PATCH (раздел 5) ── let system_path = RedfishPath::root().systems().system("1"); BiosPatchBuilder::new() @@ -997,13 +978,13 @@ fn full_redfish_diagnostic() -> Result<(), RedfishError> { .tpm_enabled(true) .apply(&session, &configure, &system_path)?; - // ── 6. Firmware update lifecycle (Section 6) ── + // ── 6. Жизненный цикл обновления прошивки (раздел 6) ── let firmware_image = include_bytes!("bmc_firmware.bin"); let uploaded = FirmwareUpdate::push_image(&session, &manager, firmware_image)?; let verified = uploaded.verify()?; let needs_reboot = verified.apply()?; - // ── 7. Clean shutdown ── + // ── 7. Чистое завершение ── needs_reboot.reboot(&session, &manager)?; session.logout(); @@ -1011,60 +992,50 @@ fn full_redfish_diagnostic() -> Result<(), RedfishError> { } ``` -### What the Compiler Proves +### Что доказывает компилятор -| # | Bug class | How it's prevented | Pattern (Section) | -|---|-----------|-------------------|-------------------| -| 1 | Request on unauthenticated session | `http_get()` only exists on `Session` | Type-state (§1) | -| 2 | Privilege escalation | `ConfigureManagerToken` not returned by operator login | Capability tokens (§2) | -| 3 | Malformed Redfish URI | Navigation methods enforce parent→child hierarchy | Phantom types (§3) | -| 4 | Unit confusion (°C vs RPM vs W) | `Celsius`, `Rpm`, `Watts` are distinct types | Dimensional analysis (§4) | -| 5 | Missing JSON field → panic | `ValidThermalResponse` validates at parse boundary | Validated boundaries (§4) | -| 6 | Wrong response type | `RedfishResource::Response` is fixed per resource | Typed commands (§4) | -| 7 | Incomplete PATCH payload | `.apply()` only exists when all fields are `FieldSet` | Builder type-state (§5) | -| 8 | Missing privilege for PATCH | `.apply()` requires `&ConfigureComponentsToken` | Capability tokens (§5) | -| 9 | Applying unverified firmware | `.apply()` only exists on `FwVerified` | Type-state (§6) | -| 10 | Double firmware apply | `apply()` consumes `self` — value is moved | Single-use types (§6) | -| 11 | Firmware update without authority | `push_image()` requires `&ConfigureManagerToken` | Capability tokens (§6) | -| 12 | Use-after-logout | `logout()` consumes the session | Ownership (§1) | +| № | Класс ошибки | Как это предотвращается | Паттерн (раздел) | +|---|--------------|-------------------------|------------------| +| 1 | Запрос в неаутентифицированной сессии | `http_get()` существует только у `Session` | Typestate (§1) | +| 2 | Эскалация привилегий | `ConfigureManagerToken` не выдаётся при входе оператора | Capability-токены (§2) | +| 3 | Некорректный URI Redfish | Методы навигации обеспечивают иерархию родитель → потомок | Phantom-типы (§3) | +| 4 | Путаница единиц (°C, об/мин, Вт) | `Celsius`, `Rpm`, `Watts` — разные типы | Анализ размерностей (§4) | +| 5 | Отсутствующее поле JSON → паника | `ValidThermalResponse` валидирует на границе разбора | Проверенные границы (§4) | +| 6 | Неверный тип ответа | `RedfishResource::Response` фиксирован для каждого ресурса | Типизированные команды (§4) | +| 7 | Неполная нагрузка PATCH | `.apply()` существует только при `FieldSet` у всех полей | Typestate билдера (§5) | +| 8 | Отсутствие привилегии для PATCH | `.apply()` требует `&ConfigureComponentsToken` | Capability-токены (§5) | +| 9 | Применение непроверенной прошивки | `.apply()` существует только у `FwVerified` | Typestate (§6) | +| 10 | Двойное применение прошивки | `apply()` потребляет `self`: значение перемещается | Одноразовые типы (§6) | +| 11 | Обновление прошивки без полномочий | `push_image()` требует `&ConfigureManagerToken` | Capability-токены (§6) | +| 12 | Использование после выхода | `logout()` потребляет сессию | Владение (§1) | -**Total runtime overhead of ALL twelve guarantees: zero.** +**Суммарные накладные расходы во время выполнения от ВСЕХ двенадцати гарантий: ноль.** -The generated binary makes the same HTTP calls as the untyped version — but the -untyped version can have 12 classes of bugs. This version can't. +Скомпилированный бинарник делает те же HTTP-вызовы, что и нетипизированная версия, но нетипизированная версия может содержать 12 классов ошибок. Эта версия не может. --- -## Comparison: IPMI Integration (ch10) vs. Redfish Integration - -| Dimension | ch10 (IPMI) | This chapter (Redfish) | -|-----------|-------------|----------------------| -| Transport | Raw bytes over KCS/LAN | JSON over HTTPS | -| Navigation | Flat command codes (NetFn/Cmd) | Hierarchical URI tree | -| Response binding | `IpmiCmd::Response` | `RedfishResource::Response` | -| Privilege model | Single `AdminToken` | Role-based multi-token | -| Payload construction | Byte arrays | Builder type-state for JSON | -| Update lifecycle | Not covered | Full type-state chain | -| Patterns exercised | 7 | 8 (adds builder type-state) | - -The two chapters are complementary: ch10 shows the patterns work at the byte level, -this chapter shows they work identically at the REST/JSON level. The type system -doesn't care about the transport — it proves correctness either way. - -## Key Takeaways - -1. **Eight patterns compose into one Redfish client** — session type-state, capability - tokens, phantom-typed URIs, typed commands, dimensional analysis, validated - boundaries, builder type-state, and single-use firmware apply. -2. **Twelve bug classes become compile errors** — see the table above. -3. **Zero runtime overhead** — every proof token, phantom type, and type-state - marker compiles away. The binary is identical to hand-rolled untyped code. -4. **REST APIs benefit as much as byte protocols** — the patterns from ch02–ch09 - apply equally to JSON-over-HTTPS (Redfish) and bytes-over-KCS (IPMI). -5. **Privilege enforcement is structural, not procedural** — the function signature - declares what's required; the compiler enforces it. -6. **This is a design template** — adapt the resource type markers, capability - tokens, and builder for your specific Redfish schema and organizational - role hierarchy. +## Сравнение: интеграция с IPMI (гл. 10) и с Redfish + +| Аспект | гл. 10 (IPMI) | Эта глава (Redfish) | +|--------|---------------|---------------------| +| Транспорт | Сырые байты через KCS/LAN | JSON через HTTPS | +| Навигация | Плоские коды команд (NetFn/Cmd) | Иерархическое дерево URI | +| Привязка ответа | `IpmiCmd::Response` | `RedfishResource::Response` | +| Модель привилегий | Один `AdminToken` | Многотокенная модель на основе ролей | +| Построение полезной нагрузки | Массивы байтов | Билдер с typestate для JSON | +| Жизненный цикл обновления | Не рассматривается | Полная цепочка typestate | +| Задействованные паттерны | 7 | 8 (добавляется typestate билдера) | + +Две главы дополняют друг друга: гл. 10 показывает, что паттерны работают на уровне байтов, а эта глава показывает, что они работают одинаково на уровне REST/JSON. Система типов не зависит от транспорта и доказывает корректность в обоих случаях. + +## Ключевые выводы + +1. **Восемь паттернов складываются в один клиент Redfish**: typestate сессии, capability-токены, URI с phantom-типами, типизированные команды, анализ размерностей, проверенные границы, typestate билдера и одноразовое применение прошивки. +2. **Двенадцать классов ошибок становятся ошибками компиляции**: см. таблицу выше. +3. **Нулевые накладные расходы во время выполнения**: каждый токен-доказательство, phantom-тип и маркер typestate исчезает при компиляции. Бинарный файл идентичен написанному вручную нетипизированному коду. +4. **REST API выигрывают не меньше, чем протоколы на уровне байтов**: паттерны из гл. 02–09 одинаково применимы к JSON-over-HTTPS (Redfish) и байтам-over-KCS (IPMI). +5. **Соблюдение привилегий структурно, а не процедурно**: сигнатура функции объявляет требования, а компилятор их обеспечивает. +6. **Это шаблон проектирования**: адаптируйте маркеры типов ресурсов, capability-токены и билдер под вашу конкретную схему Redfish и иерархию ролей в организации. --- diff --git a/type-driven-correctness-book/src/ch18-redfish-server-walkthrough.md b/type-driven-correctness-book/src/ch18-redfish-server-walkthrough.md index faa5a12..7da7840 100644 --- a/type-driven-correctness-book/src/ch18-redfish-server-walkthrough.md +++ b/type-driven-correctness-book/src/ch18-redfish-server-walkthrough.md @@ -1,63 +1,57 @@ -# Applied Walkthrough — Type-Safe Redfish Server 🟡 +# Пошаговый разбор: типобезопасный сервер Redfish 🟡 -> **What you'll learn:** How to compose response builder type-state, source-availability tokens, dimensional serialization, health rollup, schema versioning, and typed action dispatch into a Redfish server that **cannot produce a schema-non-compliant response** — the mirror of the client walkthrough in [ch17](ch17-redfish-applied-walkthrough.md). +> **Что вы узнаете:** как скомпоновать typestate билдера ответа, токены доступности источников, размерную сериализацию, агрегацию здоровья, версионирование схем и типизированную диспетчеризацию действий в сервер Redfish, который **не может выдать ответ, не соответствующий схеме**. Это зеркало пошагового разбора клиента из [гл. 17](ch17-redfish-applied-walkthrough.md). > -> **Cross-references:** [ch02](ch02-typed-command-interfaces-request-determi.md) (typed commands — inverted for action dispatch), [ch04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability tokens — source availability), [ch06](ch06-dimensional-analysis-making-the-compiler.md) (dimensional types — serialization side), [ch07](ch07-validated-boundaries-parse-dont-validate.md) (validated boundaries — inverted: "construct, don't serialize"), [ch09](ch09-phantom-types-for-resource-tracking.md) (phantom types — schema versioning), [ch11](ch11-fourteen-tricks-from-the-trenches.md) (trick 3 — `#[non_exhaustive]`, trick 4 — builder type-state), [ch17](ch17-redfish-applied-walkthrough.md) (client counterpart) +> **Перекрёстные ссылки:** [гл. 02](ch02-typed-command-interfaces-request-determi.md) (типизированные команды, перевёрнутые для диспетчеризации действий), [гл. 04](ch04-capability-tokens-zero-cost-proof-of-aut.md) (capability-токены для доступности источников), [гл. 06](ch06-dimensional-analysis-making-the-compiler.md) (размерные типы, сторона сериализации), [гл. 07](ch07-validated-boundaries-parse-dont-validate.md) (проверенные границы, перевёрнуто: «строй, а не сериализуй»), [гл. 09](ch09-phantom-types-for-resource-tracking.md) (phantom-типы, версионирование схем), [гл. 11](ch11-fourteen-tricks-from-the-trenches.md) (приём 3: `#[non_exhaustive]`, приём 4: билдер с typestate), [гл. 17](ch17-redfish-applied-walkthrough.md) (клиентская часть) -## The Mirror Problem +## Проблема зеркала -Chapter 17 asks: *"How do I consume Redfish correctly?"* This chapter asks the -mirror question: *"How do I produce Redfish correctly?"* +Глава 17 спрашивает: *«Как правильно потреблять Redfish?»* Эта глава задаёт зеркальный вопрос: *«Как правильно отдавать Redfish?»* -On the client side, the danger is **trusting** bad data. On the server side, the -danger is **emitting** bad data — and every client in the fleet trusts what you -send. +На стороне клиента опасность в том, чтобы **доверять** плохим данным. На стороне сервера опасность в том, чтобы **выдавать** плохие данные, а каждый клиент в парке доверяет тому, что вы отправляете. -A single `GET /redfish/v1/Systems/1` response must fuse data from many sources: +Один ответ `GET /redfish/v1/Systems/1` должен объединять данные из многих источников: ```mermaid flowchart LR - subgraph Sources - SMBIOS["SMBIOS\nType 1, Type 17"] - SDR["IPMI Sensors\n(SDR + readings)"] - SEL["IPMI SEL\n(critical events)"] - PCIe["PCIe Config\nSpace"] - FW["Firmware\nVersion Table"] - PWR["Power State\nRegister"] + subgraph Sources["Источники"] + SMBIOS["SMBIOS\nТип 1, Тип 17"] + SDR["Датчики IPMI\n(SDR + показания)"] + SEL["SEL IPMI\n(критические события)"] + PCIe["Конфигурационное\nпространство PCIe"] + FW["Таблица версий\nпрошивки"] + PWR["Регистр состояния\nпитания"] end - subgraph Server["Redfish Server"] - Handler["GET handler"] - Builder["ComputerSystem\nBuilder"] + subgraph Server["Сервер Redfish"] + Handler["Обработчик GET"] + Builder["Билдер\nComputerSystem"] end - SMBIOS -->|"Name, UUID, Serial"| Handler + SMBIOS -->|"Имя, UUID, серийный номер"| Handler SDR -->|"Temperatures, Fans"| Handler - SEL -->|"Health escalation"| Handler - PCIe -->|"Device links"| Handler - FW -->|"BIOS version"| Handler + SEL -->|"Эскалация здоровья"| Handler + PCIe -->|"Связи устройств"| Handler + FW -->|"Версия BIOS"| Handler PWR -->|"PowerState"| Handler Handler --> Builder - Builder -->|".build()"| JSON["Schema-compliant\nJSON response"] + Builder -->|".build()"| JSON["JSON, соответствующий\nсхеме"] style JSON fill:#c8e6c9,color:#000 style Builder fill:#e1f5fe,color:#000 ``` -In C, this is a 500-line handler that calls into six subsystems, manually builds -a JSON tree with `json_object_set()`, and hopes every required field was populated. -Forget one? The response violates the Redfish schema. Get the unit wrong? Every -client sees corrupted telemetry. +В C это обработчик на 500 строк, который обращается к шести подсистемам, вручную строит дерево JSON через `json_object_set()` и надеется, что каждое обязательное поле заполнено. Забыли одно? Ответ нарушает схему Redfish. Перепутали единицу? Каждый клиент получает искажённую телеметрию. ```c -// C — the assembly problem +// C — проблема сборки json_t *get_computer_system(const char *id) { json_t *obj = json_object(); json_object_set_new(obj, "@odata.type", json_string("#ComputerSystem.v1_13_0.ComputerSystem")); - // 🐛 Forgot to set "Name" — schema requires it - // 🐛 Forgot to set "UUID" — schema requires it + // 🐛 Забыли задать "Name": схема его требует + // 🐛 Забыли задать "UUID": схема его требует smbios_type1_t *t1 = smbios_get_type1(); if (t1) { @@ -66,53 +60,49 @@ json_t *get_computer_system(const char *id) { } json_object_set_new(obj, "PowerState", - json_string(get_power_state())); // at least this one is always available + json_string(get_power_state())); // хотя бы это всегда доступно - // 🐛 Reading is in raw ADC counts, not Celsius — no type to catch it + // 🐛 Показание в сырых отсчётах АЦП, а не в Цельсиях: нет типа, который это поймает double cpu_temp = read_sensor(SENSOR_CPU_TEMP); - // This number ends up in a Thermal response somewhere else... - // but nothing ties it to "Celsius" at the type level + // Это число где-то попадает в ответ Thermal... + // но ничто не связывает его с «Цельсием» на уровне типов - // 🐛 Health is manually computed — forgot to include PSU status + // 🐛 Здоровье вычисляется вручную: забыли учесть статус БП json_object_set_new(obj, "Status", - build_status("Enabled", "OK")); // should be "Critical" — PSU is failing + build_status("Enabled", "OK")); // должно быть "Critical": БП выходит из строя - return obj; // missing 2 required fields, wrong health, raw units + return obj; // не хватает 2 обязательных полей, неверное здоровье, сырые единицы } ``` -Four bugs in one handler. On the client side, each bug affects **one** client. -On the server side, each bug affects **every** client that queries this BMC. +Четыре ошибки в одном обработчике. На стороне клиента каждая ошибка затрагивает **одного** клиента. На стороне сервера каждая ошибка затрагивает **каждого** клиента, который запрашивает этот BMC. --- -## Section 1 — Response Builder Type-State: "Construct, Don't Serialize" (ch07 Inverted) +## Раздел 1 — Typestate билдера ответа: «строй, а не сериализуй» (гл. 07, перевёрнуто) -Chapter 7 teaches "parse, don't validate" — validate inbound data once, carry the -proof in a type. The server-side mirror is **"construct, don't serialize"** — build -the outbound response through a builder that gates `.build()` on all required fields -being present. +Глава 7 учит «parse, don't validate»: проверить входящие данные один раз и хранить доказательство в типе. Зеркальный приём на стороне сервера — **«construct, don't serialize»**: строить исходящий ответ через билдер, который открывает `.build()` только при наличии всех обязательных полей. ```rust,ignore use std::marker::PhantomData; -// ──── Type-level field tracking ──── +// ──── Отслеживание полей на уровне типов ──── pub struct HasField; pub struct MissingField; -// ──── Response Builder ──── +// ──── Билдер ответа ──── -/// Builder for a ComputerSystem Redfish resource. -/// Type parameters track which REQUIRED fields have been supplied. -/// Optional fields don't need type-level tracking. +/// Билдер ресурса ComputerSystem Redfish. +/// Параметры типа отслеживают, какие ОБЯЗАТЕЛЬНЫЕ поля уже заданы. +/// Необязательные поля не нужно отслеживать на уровне типов. pub struct ComputerSystemBuilder { - // Required fields — tracked at the type level + // Обязательные поля — отслеживаются на уровне типов name: Option, uuid: Option, power_state: Option, status: Option, - // Optional fields — not tracked (always settable) + // Необязательные поля — не отслеживаются (можно задать всегда) manufacturer: Option, model: Option, serial_number: Option, @@ -157,7 +147,7 @@ pub struct MemorySummary { pub status: ResourceStatus, } -// ──── Constructor: all fields start MissingField ──── +// ──── Конструктор: все поля начинаются как MissingField ──── impl ComputerSystemBuilder { pub fn new() -> Self { @@ -170,7 +160,7 @@ impl ComputerSystemBuilder ComputerSystemBuilder { pub fn name(self, name: String) -> ComputerSystemBuilder { @@ -228,7 +218,7 @@ impl ComputerSystemBuilder { } } -// ──── Optional field setters — available in any state ──── +// ──── Сеттеры необязательных полей — доступны в любом состоянии ──── impl ComputerSystemBuilder { pub fn manufacturer(mut self, m: String) -> Self { @@ -251,7 +241,7 @@ impl ComputerSystemBuilder { } } -// ──── .build() ONLY exists when all required fields are HasField ──── +// ──── .build() существует ТОЛЬКО когда все обязательные поля — HasField ──── impl ComputerSystemBuilder { pub fn build(self, id: &str) -> serde_json::Value { @@ -265,7 +255,7 @@ impl ComputerSystemBuilder { "Status": self.status.unwrap(), }); - // Optional fields — included only if present + // Необязательные поля — включаются, только если заданы if let Some(m) = self.manufacturer { obj["Manufacturer"] = serde_json::json!(m); } @@ -290,67 +280,60 @@ impl ComputerSystemBuilder { } // -// ── The Compiler Enforces Completeness ── +// ── Компилятор обеспечивает полноту ── // -// ✅ All required fields set — .build() is available: +// ✅ Все обязательные поля заданы — .build() доступен: // ComputerSystemBuilder::new() // .name("PowerEdge R750".into()) // .uuid("4c4c4544-...".into()) // .power_state(PowerStateValue::On) // .status(ResourceStatus { ... }) -// .manufacturer("Dell".into()) // optional — fine to include +// .manufacturer("Dell".into()) // необязательное — можно включать // .build("1") // -// ❌ Missing "Name" — compile error: +// ❌ Нет «Name» — ошибка компиляции: // ComputerSystemBuilder::new() // .uuid("4c4c4544-...".into()) // .power_state(PowerStateValue::On) // .status(ResourceStatus { ... }) // .build("1") -// ERROR: method `build` not found for +// ОШИБКА: метод `build` не найден для // `ComputerSystemBuilder` ``` -**Bug class eliminated:** schema-non-compliant responses. The handler physically -cannot serialize a `ComputerSystem` without supplying every required field. The -compiler error message even tells you *which* field is missing — it's right there -in the type parameter: `MissingField` in the `Name` position. +**Устранённый класс ошибок:** ответы, не соответствующие схеме. Обработчик физически не может сериализовать `ComputerSystem`, не задав каждое обязательное поле. Сообщение компилятора даже подсказывает, *какого* поля не хватает: это видно в параметре типа, где стоит `MissingField` на месте `Name`. --- -## Section 2 — Source-Availability Tokens (Capability Tokens, ch04 — New Twist) +## Раздел 2 — Токены доступности источников (capability-токены, гл. 04, новый поворот) -In ch04 and ch17, capability tokens prove **authorization** — "the caller is -allowed to do this." On the server side, the same pattern proves **availability** — -"this data source was successfully initialized." +В гл. 04 и гл. 17 capability-токены доказывают **авторизацию**: «вызывающему разрешено это делать». На стороне сервера тот же паттерн доказывает **доступность**: «этот источник данных успешно инициализирован». -Each subsystem the BMC queries can fail independently. SMBIOS tables might be -corrupt. The sensor subsystem might still be initializing. PCIe bus scan might -have timed out. Encode each as a proof token: +Каждая подсистема, которую опрашивает BMC, может отказать независимо. Таблицы SMBIOS могут быть повреждены. Подсистема датчиков может ещё инициализироваться. Сканирование шины PCIe может завершиться по тайм-ауту. Кодируем каждую как токен-доказательство: ```rust,ignore -/// Proof that SMBIOS tables were successfully parsed. -/// Only produced by the SMBIOS init function. +/// Доказательство, что таблицы SMBIOS успешно разобраны. +/// Создаётся только функцией инициализации SMBIOS. pub struct SmbiosReady { _private: (), } -/// Proof that IPMI sensor subsystem is responsive. +/// Доказательство, что подсистема датчиков IPMI отвечает. pub struct SensorsReady { _private: (), } -/// Proof that PCIe bus scan completed. +/// Доказательство, что сканирование шины PCIe завершено. pub struct PcieReady { _private: (), } -/// Proof that the SEL was successfully read. +/// Доказательство, что SEL успешно прочитан. pub struct SelReady { _private: (), } -// ──── Data source initialization ──── +// ──── Инициализация источников данных ──── pub struct SmbiosTables { pub product_name: String, @@ -366,9 +349,9 @@ pub struct SensorCache { pub psu_power: Vec<(String, Watts)>, } -/// Rich SEL summary — per-subsystem health derived from typed events. -/// Built by the consumer pipeline in ch07's SEL section. -/// Replaces the lossy `has_critical_events: bool` with typed granularity. +/// Расширенная сводка SEL — здоровье по подсистемам, выведенное из типизированных событий. +/// Строится конвейером потребителя из раздела SEL гл. 07. +/// Заменяет теряющий информацию `has_critical_events: bool` типизированной детализацией. pub struct TypedSelSummary { pub total_entries: u32, pub processor_health: HealthValue, @@ -381,8 +364,8 @@ pub struct TypedSelSummary { } pub fn init_smbios() -> Option<(SmbiosReady, SmbiosTables)> { - // Read SMBIOS entry point, parse tables... - // Returns None if tables are absent or corrupt + // Читаем точку входа SMBIOS, разбираем таблицы... + // Возвращает None, если таблицы отсутствуют или повреждены Some(( SmbiosReady { _private: () }, SmbiosTables { @@ -395,8 +378,8 @@ pub fn init_smbios() -> Option<(SmbiosReady, SmbiosTables)> { } pub fn init_sensors() -> Option<(SensorsReady, SensorCache)> { - // Initialize SDR repository, read all sensors... - // Returns None if IPMI subsystem is not responsive + // Инициализируем репозиторий SDR, читаем все датчики... + // Возвращает None, если подсистема IPMI не отвечает Some(( SensorsReady { _private: () }, SensorCache { @@ -415,8 +398,8 @@ pub fn init_sensors() -> Option<(SensorsReady, SensorCache)> { } pub fn init_sel() -> Option<(SelReady, TypedSelSummary)> { - // In production: read SEL entries, parse via ch07's TryFrom, - // classify via classify_event_health(), aggregate via summarize_sel(). + // В продакшене: читаем записи SEL, разбираем через TryFrom из гл. 07, + // классифицируем через classify_event_health(), агрегируем через summarize_sel(). Some(( SelReady { _private: () }, TypedSelSummary { @@ -433,11 +416,10 @@ pub fn init_sel() -> Option<(SelReady, TypedSelSummary)> { } ``` -Now, functions that populate builder fields from a data source **require the -corresponding proof token**: +Теперь функции, которые заполняют поля билдера из источника данных, **требуют соответствующий токен-доказательство**: ```rust,ignore -/// Populate SMBIOS-sourced fields. Requires proof SMBIOS is available. +/// Заполняет поля из SMBIOS. Требует доказательство, что SMBIOS доступен. fn populate_from_smbios( builder: ComputerSystemBuilder, _proof: &SmbiosReady, @@ -450,18 +432,18 @@ fn populate_from_smbios( .serial_number(tables.serial_number.clone()) } -/// Fallback when SMBIOS is unavailable — supplies required fields -/// with safe defaults. +/// Запасной вариант, когда SMBIOS недоступен: обязательные поля заполняются +/// безопасными значениями по умолчанию. fn populate_smbios_fallback( builder: ComputerSystemBuilder, ) -> ComputerSystemBuilder { builder - .name("Unknown System".into()) + .name("Неизвестная система".into()) .uuid("00000000-0000-0000-0000-000000000000".into()) } ``` -The handler chooses the path based on which tokens are available: +Обработчик выбирает путь в зависимости от того, какие токены доступны: ```rust,ignore fn build_computer_system( @@ -478,25 +460,20 @@ fn build_computer_system( None => populate_smbios_fallback(builder), }; - // Both paths produce HasField for Name and UUID. - // .build() is available either way. + // Оба пути дают HasField для Name и UUID. + // .build() доступен в любом случае. builder.build("1") } ``` -**Bug class eliminated:** calling into a subsystem that failed initialization. -If SMBIOS didn't parse, you don't have a `SmbiosReady` token — the compiler forces -you through the fallback path. No runtime `if (smbios != NULL)` to forget. +**Устранённый класс ошибок:** обращение к подсистеме, которая не смогла инициализироваться. Если SMBIOS не разобрался, токена `SmbiosReady` у вас нет: компилятор заставляет пройти через запасной путь. Никакой проверки `if (smbios != NULL)` во время выполнения, которую можно забыть. -### Combining Source Tokens with Capability Mixins (ch08) +### Комбинирование токенов источников с capability-миксинами (гл. 08) -With multiple Redfish resource types to serve (ComputerSystem, Chassis, Manager, -Thermal, Power), source-population logic repeats across handlers. The **mixin** -pattern from ch08 eliminates this duplication. Declare what sources a handler has, -and blanket impls provide the population methods automatically: +Когда нужно обслуживать несколько типов ресурсов Redfish (ComputerSystem, Chassis, Manager, Thermal, Power), логика заполнения из источников повторяется в обработчиках. Паттерн **миксинов** из гл. 08 устраняет это дублирование. Объявляете, какие источники есть у обработчика, и blanket impl сами предоставляют методы заполнения: ```rust,ignore -/// ── Ingredient Traits (ch08) for data sources ── +/// ── Трейты-ингредиенты (гл. 08) для источников данных ── pub trait HasSmbios { fn smbios(&self) -> &(SmbiosReady, SmbiosTables); @@ -510,7 +487,7 @@ pub trait HasSel { fn sel(&self) -> &(SelReady, TypedSelSummary); } -/// ── Mixin: any handler with SMBIOS + Sensors gets identity population ── +/// ── Миксин: любой обработчик с SMBIOS и датчиками получает заполнение идентификации ── pub trait IdentityMixin: HasSmbios { fn populate_identity( @@ -526,10 +503,10 @@ pub trait IdentityMixin: HasSmbios { } } -/// Auto-implement for any type that has SMBIOS capability. +/// Автоматическая реализация для любого типа с возможностью SMBIOS. impl IdentityMixin for T {} -/// ── Mixin: any handler with Sensors + SEL gets health rollup ── +/// ── Миксин: любой обработчик с датчиками и SEL получает агрегацию здоровья ── pub trait HealthMixin: HasSensors + HasSel { fn compute_health(&self) -> ResourceStatus { @@ -544,7 +521,7 @@ pub trait HealthMixin: HasSensors + HasSel { impl HealthMixin for T {} -/// ── Concrete handler owns available sources ── +/// ── Конкретный обработчик владеет доступными источниками ── struct FullPlatformHandler { smbios: (SmbiosReady, SmbiosTables), @@ -562,33 +539,27 @@ impl HasSel for FullPlatformHandler { fn sel(&self) -> &(SelReady, TypedSelSummary) { &self.sel } } -// FullPlatformHandler automatically gets: -// IdentityMixin::populate_identity() (via HasSmbios) -// HealthMixin::compute_health() (via HasSensors + HasSel) +// FullPlatformHandler автоматически получает: +// IdentityMixin::populate_identity() (через HasSmbios) +// HealthMixin::compute_health() (через HasSensors + HasSel) // -// A SensorsOnlyHandler that impls HasSensors but NOT HasSel -// would get IdentityMixin (if it has SMBIOS) but NOT HealthMixin. -// Calling .compute_health() on it → compile error. +// SensorsOnlyHandler, который реализует HasSensors, но НЕ HasSel, +// получит IdentityMixin (если есть SMBIOS), но НЕ HealthMixin. +// Вызов .compute_health() у него → ошибка компиляции. ``` -This directly mirrors ch08's `BaseBoardController` pattern: ingredient traits -declare what you have, mixin traits provide behavior via blanket impls, and -the compiler gates each mixin on its prerequisites. Adding a new data -source (e.g., `HasNvme`) plus a mixin (e.g., `StorageMixin: HasNvme + HasSel`) -gives health rollup for storage to every handler that has both — automatically. +Это напрямую повторяет паттерн `BaseBoardController` из гл. 08: трейты-ингредиенты объявляют, что у вас есть, трейты-миксины предоставляют поведение через blanket impl, а компилятор открывает каждый миксин только при выполнении его предусловий. Добавление нового источника данных (например, `HasNvme`) и миксина (например, `StorageMixin: HasNvme + HasSel`) даёт агрегацию здоровья для хранилища каждому обработчику, у которого есть оба, автоматически. --- -## Section 3 — Dimensional Types at the Serialization Boundary (ch06) +## Раздел 3 — Размерные типы на границе сериализации (гл. 06) -On the client side (ch17 §4), dimensional types prevent **reading** °C as RPM. -On the server side, they prevent **writing** RPM into a Celsius JSON field. This -is arguably more dangerous — a wrong value on the server propagates to every client. +На стороне клиента (гл. 17, §4) размерные типы не дают **прочитать** °C как об/мин. На стороне сервера они не дают **записать** об/мин в JSON-поле в градусах Цельсия. Это, пожалуй, опаснее: неверное значение на сервере доходит до каждого клиента. ```rust,ignore use serde::Serialize; -// ──── Dimensional types from ch06, with Serialize ──── +// ──── Размерные типы из гл. 06, с Serialize ──── #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)] pub struct Celsius(pub f64); @@ -599,15 +570,15 @@ pub struct Rpm(pub u32); #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)] pub struct Watts(pub f64); -// ──── Redfish Thermal response members ──── -// Field types enforce which unit belongs in which JSON property. +// ──── Элементы ответа Redfish Thermal ──── +// Типы полей задают, какая единица относится к какому JSON-свойству. #[derive(Serialize)] #[serde(rename_all = "PascalCase")] pub struct TemperatureMember { pub member_id: String, pub name: String, - pub reading_celsius: Celsius, // ← must be Celsius + pub reading_celsius: Celsius, // ← обязан быть Celsius #[serde(skip_serializing_if = "Option::is_none")] pub upper_threshold_critical: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -620,8 +591,8 @@ pub struct TemperatureMember { pub struct FanMember { pub member_id: String, pub name: String, - pub reading: Rpm, // ← must be Rpm - pub reading_units: &'static str, // always "RPM" + pub reading: Rpm, // ← обязан быть Rpm + pub reading_units: &'static str, // всегда "RPM" pub status: ResourceStatus, } @@ -630,13 +601,13 @@ pub struct FanMember { pub struct PowerControlMember { pub member_id: String, pub name: String, - pub power_consumed_watts: Watts, // ← must be Watts + pub power_consumed_watts: Watts, // ← обязан быть Watts #[serde(skip_serializing_if = "Option::is_none")] pub power_capacity_watts: Option, pub status: ResourceStatus, } -// ──── Building a Thermal response from sensor cache ──── +// ──── Построение ответа Thermal из кэша датчиков ──── fn build_thermal_response( _proof: &SensorsReady, @@ -645,7 +616,7 @@ fn build_thermal_response( let temps = vec![ TemperatureMember { member_id: "0".into(), - name: "CPU Temp".into(), + name: "Температура ЦП".into(), reading_celsius: cache.cpu_temp, // Celsius → Celsius ✅ upper_threshold_critical: Some(Celsius(95.0)), upper_threshold_fatal: Some(Celsius(105.0)), @@ -661,7 +632,7 @@ fn build_thermal_response( }, TemperatureMember { member_id: "1".into(), - name: "Inlet Temp".into(), + name: "Температура на входе".into(), reading_celsius: cache.inlet_temp, // Celsius → Celsius ✅ upper_threshold_critical: Some(Celsius(42.0)), upper_threshold_fatal: None, @@ -672,7 +643,7 @@ fn build_thermal_response( }, }, - // ❌ Compile error — can't put Rpm in a Celsius field: + // ❌ Ошибка компиляции — нельзя положить Rpm в поле Celsius: // TemperatureMember { // reading_celsius: cache.fan_readings[0].1, // Rpm ≠ Celsius // ... @@ -701,39 +672,33 @@ fn build_thermal_response( } ``` -**Bug class eliminated:** unit confusion at serialization. The Redfish schema says -`ReadingCelsius` is in °C. The Rust type system says `reading_celsius` must be -`Celsius`. If a developer accidentally passes `Rpm(8400)` or `Watts(285.0)`, the -compiler catches it before the value ever reaches JSON. +**Устранённый класс ошибок:** путаница единиц при сериализации. Схема Redfish говорит, что `ReadingCelsius` — в °C. Система типов Rust говорит, что `reading_celsius` обязан быть `Celsius`. Если разработчик по ошибке передаст `Rpm(8400)` или `Watts(285.0)`, компилятор поймает это до того, как значение попадёт в JSON. --- -## Section 4 — Health Rollup as a Typed Fold +## Раздел 4 — Агрегация здоровья как типизированная свёртка -Redfish `Status.Health` is a *rollup* — the worst health of all sub-components. -In C, this is typically a series of `if` checks that inevitably misses a source. -With typed enums and `Ord`, the rollup is a one-line fold — and the compiler -ensures every source contributes: +`Status.Health` в Redfish — это *агрегат*: худшее состояние среди всех подкомпонентов. В C это обычно серия проверок `if`, в которой неизбежно пропускается какой-нибудь источник. С типизированными enum и `Ord` агрегация становится однострочной свёрткой, а компилятор гарантирует, что каждый источник участвует: ```rust,ignore -/// Roll up health from multiple sources. -/// Ord on HealthValue: OK < Warning < Critical. -/// Returns the worst (max) value. +/// Агрегирует здоровье из нескольких источников. +/// Ord для HealthValue: OK < Warning < Critical. +/// Возвращает худшее (максимальное) значение. fn rollup(sources: &[HealthValue]) -> HealthValue { sources.iter().copied().max().unwrap_or(HealthValue::OK) } -/// Compute system-level health from all sub-components. -/// Takes explicit references to every source — the caller must provide ALL of them. +/// Вычисляет здоровье системы по всем подкомпонентам. +/// Принимает явные ссылки на каждый источник: вызывающий обязан передать ВСЕ. fn compute_system_health( sensors: Option<&(SensorsReady, SensorCache)>, sel: Option<&(SelReady, TypedSelSummary)>, ) -> ResourceStatus { let mut inputs = Vec::new(); - // ── Live sensor readings ── + // ── Текущие показания датчиков ── if let Some((_proof, cache)) = sensors { - // Temperature health (dimensional: Celsius comparison) + // Здоровье по температуре (размерное: сравнение Celsius) if cache.cpu_temp > Celsius(95.0) { inputs.push(HealthValue::Critical); } else if cache.cpu_temp > Celsius(85.0) { @@ -742,7 +707,7 @@ fn compute_system_health( inputs.push(HealthValue::OK); } - // Fan health (dimensional: Rpm comparison) + // Здоровье вентиляторов (размерное: сравнение Rpm) for (_name, rpm) in &cache.fan_readings { if *rpm < Rpm(500) { inputs.push(HealthValue::Critical); @@ -753,7 +718,7 @@ fn compute_system_health( } } - // PSU health (dimensional: Watts comparison) + // Здоровье БП (размерное: сравнение Watts) for (_name, watts) in &cache.psu_power { if *watts > Watts(800.0) { inputs.push(HealthValue::Critical); @@ -763,9 +728,9 @@ fn compute_system_health( } } - // ── SEL per-subsystem health (from ch07's TypedSelSummary) ── - // Each subsystem's health was derived by exhaustive matching over - // every sensor type and event variant. No information was lost. + // ── Здоровье по подсистемам из SEL (из TypedSelSummary гл. 07) ── + // Здоровье каждой подсистемы выведено исчерпывающим сопоставлением по + // каждому типу датчика и варианту события. Информация не потеряна. if let Some((_proof, sel_summary)) = sel { inputs.push(sel_summary.processor_health); inputs.push(sel_summary.memory_health); @@ -786,32 +751,23 @@ fn compute_system_health( } ``` -**Bug class eliminated:** incomplete health rollup. In C, forgetting to include PSU -status in the health calculation is a silent bug — the system reports "OK" while a -PSU is failing. Here, `compute_system_health` takes explicit references to every -data source. The SEL contribution is no longer a lossy `bool` — it's seven -per-subsystem `HealthValue` fields derived by exhaustive matching in ch07's consumer -pipeline. Adding a new SEL sensor type forces the classifier to handle it; adding a -new subsystem field forces the rollup to include it. +**Устранённый класс ошибок:** неполная агрегация здоровья. В C забытый учёт статуса БП в расчёте здоровья — тихая ошибка: система сообщает «OK», пока БП выходит из строя. Здесь `compute_system_health` принимает явные ссылки на каждый источник данных. Вклад SEL больше не `bool`, который теряет информацию: это семь полей `HealthValue` по подсистемам, выведенных исчерпывающим сопоставлением в конвейере потребителя из гл. 07. Добавление нового типа датчика SEL вынуждает классификатор его обработать; добавление нового поля подсистемы вынуждает агрегацию его учесть. --- -## Section 5 — Schema Versioning with Phantom Types (ch09) +## Раздел 5 — Версионирование схем с phantom-типами (гл. 09) -If the BMC advertises `ComputerSystem.v1_13_0`, the response **must** include -properties introduced in that schema version (`LastResetTime`, `BootProgress`). -Advertising v1.13 without those fields is a Redfish Interop Validator failure. -Phantom version markers make this a compile-time contract: +Если BMC объявляет `ComputerSystem.v1_13_0`, ответ **обязан** содержать свойства, введённые в этой версии схемы (`LastResetTime`, `BootProgress`). Объявить v1.13 без этих полей — провал Redfish Interop Validator. Phantom-маркеры версий превращают это в контракт на этапе компиляции: ```rust,ignore use std::marker::PhantomData; -// ──── Schema Version Markers ──── +// ──── Маркеры версий схемы ──── pub struct V1_5; pub struct V1_13; -// ──── Version-Aware Response ──── +// ──── Ответ с учётом версии ──── pub struct ComputerSystemResponse { pub base: ComputerSystemBase, @@ -829,7 +785,7 @@ pub struct ComputerSystemBase { pub bios_version: Option, } -// Methods available on ALL versions: +// Методы, доступные во ВСЕХ версиях: impl ComputerSystemResponse { pub fn base_json(&self) -> serde_json::Value { serde_json::json!({ @@ -842,27 +798,27 @@ impl ComputerSystemResponse { } } -// ──── v1.13-specific fields ──── +// ──── Поля, специфичные для v1.13 ──── -/// Date and time of the last system reset. +/// Дата и время последнего сброса системы. pub struct LastResetTime(pub String); -/// Boot progress information. +/// Информация о ходе загрузки. pub struct BootProgress { pub last_state: String, pub last_state_time: String, } impl ComputerSystemResponse { - /// LastResetTime — REQUIRED in v1.13+. - /// This method only exists on V1_13. If the BMC advertises v1.13 - /// and the handler doesn't call this, the field is missing. + /// LastResetTime — ОБЯЗАТЕЛЕН в v1.13 и выше. + /// Этот метод есть только у V1_13. Если BMC объявляет v1.13, + /// а обработчик не вызывает его, поле будет отсутствовать. pub fn last_reset_time(&self) -> LastResetTime { - // Read from RTC or boot timestamp register + // Читаем из RTC или регистра метки времени загрузки LastResetTime("2026-03-16T08:30:00Z".to_string()) } - /// BootProgress — REQUIRED in v1.13+. + /// BootProgress — ОБЯЗАТЕЛЕН в v1.13 и выше. pub fn boot_progress(&self) -> BootProgress { BootProgress { last_state: "OSRunning".to_string(), @@ -870,7 +826,7 @@ impl ComputerSystemResponse { } } - /// Build the full v1.13 JSON response, including version-specific fields. + /// Формирует полный JSON-ответ v1.13, включая поля, специфичные для версии. pub fn to_json(&self) -> serde_json::Value { let mut obj = self.base_json(); obj["@odata.type"] = @@ -890,7 +846,7 @@ impl ComputerSystemResponse { } impl ComputerSystemResponse { - /// v1.5 JSON — no LastResetTime, no BootProgress. + /// JSON версии v1.5: без LastResetTime и BootProgress. pub fn to_json(&self) -> serde_json::Value { let mut obj = self.base_json(); obj["@odata.type"] = @@ -898,40 +854,35 @@ impl ComputerSystemResponse { obj } - // last_reset_time() doesn't exist here. - // Calling it → compile error: + // last_reset_time() здесь не существует. + // Вызов → ошибка компиляции: // let resp: ComputerSystemResponse = ...; // resp.last_reset_time(); - // ❌ ERROR: method `last_reset_time` not found for + // ❌ ОШИБКА: метод `last_reset_time` не найден для // `ComputerSystemResponse` } ``` -**Bug class eliminated:** schema version mismatch. If the BMC is configured to -advertise v1.13, use `ComputerSystemResponse` and the compiler ensures -every v1.13-required field is produced. Downgrade to v1.5? Change the type -parameter — the v1.13 methods vanish, and no dead fields leak into the response. +**Устранённый класс ошибок:** несоответствие версии схемы. Если BMC настроен объявлять v1.13, используйте `ComputerSystemResponse`, и компилятор гарантирует, что каждое поле, обязательное для v1.13, будет сформировано. Понижаете до v1.5? Смените параметр типа: методы v1.13 исчезнут, и в ответ не просочатся лишние поля. --- -## Section 6 — Typed Action Dispatch (ch02 Inverted) +## Раздел 6 — Типизированная диспетчеризация действий (гл. 02, перевёрнуто) -In ch02, the typed command pattern binds `Request → Response` on the **client** -side. On the **server** side, the same pattern validates incoming action payloads -and dispatches them type-safely — the inverse direction. +В гл. 02 паттерн типизированной команды связывает `Request → Response` на стороне **клиента**. На стороне **сервера** тот же паттерн проверяет входящие полезные нагрузки действий и диспетчеризует их безопасно по типам: обратное направление. ```rust,ignore use serde::Deserialize; -// ──── Action Trait (mirror of ch02's IpmiCmd trait) ──── +// ──── Трейт действия (зеркало трейта IpmiCmd из гл. 02) ──── -/// A Redfish action: the framework deserializes Params from the POST body, -/// then calls execute(). If the JSON doesn't match Params, deserialization -/// fails — execute() is never called with bad input. +/// Действие Redfish: фреймворк десериализует Params из тела POST, +/// затем вызывает execute(). Если JSON не соответствует Params, +/// десериализация завершается ошибкой: execute() никогда не вызывается с некорректными данными. pub trait RedfishAction { - /// The expected JSON body structure. + /// Ожидаемая структура тела JSON. type Params: serde::de::DeserializeOwned; - /// The result of executing the action. + /// Результат выполнения действия. type Result: serde::Serialize; fn execute(&self, params: Self::Params) -> Result; @@ -971,32 +922,32 @@ impl RedfishAction for ComputerSystemReset { fn execute(&self, params: ResetParams) -> Result<(), RedfishError> { match params.reset_type { ResetType::GracefulShutdown => { - // Send ACPI shutdown to host - println!("Initiating ACPI shutdown"); + // Отправляем хосту ACPI-команду выключения + println!("Инициирован ACPI-выключение"); Ok(()) } ResetType::ForceOff => { - // Assert power-off to host - println!("Forcing power off"); + // Выставляем сигнал отключения питания хоста + println!("Принудительное выключение питания"); Ok(()) } ResetType::On | ResetType::ForceOn => { - println!("Powering on"); + println!("Включение питания"); Ok(()) } ResetType::GracefulRestart => { - println!("ACPI restart"); + println!("Перезапуск ACPI"); Ok(()) } ResetType::ForceRestart => { - println!("Forced restart"); + println!("Принудительный перезапуск"); Ok(()) } ResetType::PushPowerButton => { - println!("Simulating power button press"); + println!("Имитация нажатия кнопки питания"); Ok(()) } - // Exhaustive — compiler catches missing variants + // Исчерпывающе — компилятор поймает пропущенные варианты } } } @@ -1025,76 +976,71 @@ impl RedfishAction for ManagerResetToDefaults { fn execute(&self, params: ResetToDefaultsParams) -> Result<(), RedfishError> { match params.reset_to_defaults_type { ResetToDefaultsType::ResetAll => { - println!("Full factory reset"); + println!("Полный сброс к заводским настройкам"); Ok(()) } ResetToDefaultsType::PreserveNetworkAndUsers => { - println!("Reset preserving network + users"); + println!("Сброс с сохранением сети и пользователей"); Ok(()) } ResetToDefaultsType::PreserveNetwork => { - println!("Reset preserving network config"); + println!("Сброс с сохранением сетевой конфигурации"); Ok(()) } } } } -// ──── Generic Action Dispatcher ──── +// ──── Общий диспетчер действий ──── fn dispatch_action( action: &A, raw_body: &str, ) -> Result { - // Deserialization validates the payload structure. - // If the JSON doesn't match A::Params, this fails - // and execute() is never called. + // Десериализация проверяет структуру полезной нагрузки. + // Если JSON не соответствует A::Params, это завершается ошибкой, + // и execute() не вызывается. let params: A::Params = serde_json::from_str(raw_body) .map_err(|e| RedfishError::InvalidPayload(e.to_string()))?; action.execute(params) } -// ── Usage ── +// ── Использование ── fn handle_reset_action(body: &str) -> Result<(), RedfishError> { - // Type-safe: ResetParams is validated by serde before execute() + // Типобезопасно: ResetParams проверяется serde до вызова execute() dispatch_action(&ComputerSystemReset, body)?; Ok(()) - // Invalid JSON: {"ResetType": "Explode"} - // → serde error: "unknown variant `Explode`" - // → execute() never called + // Некорректный JSON: {"ResetType": "Explode"} + // → ошибка serde: "unknown variant `Explode`" + // → execute() не вызывается - // Missing field: {} - // → serde error: "missing field `ResetType`" - // → execute() never called + // Отсутствует поле: {} + // → ошибка serde: "missing field `ResetType`" + // → execute() не вызывается } ``` -**Bug classes eliminated:** -- **Invalid action payload:** serde rejects unknown enum variants and missing fields - before `execute()` is called. No manual `if (body["ResetType"] == ...)` chains. -- **Missing variant handling:** `match params.reset_type` is exhaustive — adding a - new `ResetType` variant forces every action handler to be updated. -- **Type confusion:** `ComputerSystemReset` expects `ResetParams`; - `ManagerResetToDefaults` expects `ResetToDefaultsParams`. The trait system prevents - passing one action's params to another action's handler. +**Устранённые классы ошибок:** +- **Некорректная полезная нагрузка действия:** serde отвергает неизвестные варианты enum и отсутствующие поля до вызова `execute()`. Никаких ручных цепочек `if (body["ResetType"] == ...)`. +- **Необработанный вариант:** `match params.reset_type` исчерпывающий: добавление нового `ResetType` вынуждает обновить каждый обработчик действия. +- **Путаница типов:** `ComputerSystemReset` ожидает `ResetParams`, а `ManagerResetToDefaults` ожидает `ResetToDefaultsParams`. Система трейтов не даёт передать параметры одного действия обработчику другого. --- -## Section 7 — Putting It All Together: The GET Handler +## Раздел 7 — Собираем всё вместе: обработчик GET -Here's the complete handler that composes all six sections into a single -schema-compliant response: +Вот полный обработчик, который соединяет все шесть разделов в один ответ, соответствующий схеме: ```rust,ignore -/// Complete GET /redfish/v1/Systems/1 handler. +/// Полный обработчик GET /redfish/v1/Systems/1. /// -/// Every required field is enforced by the builder type-state. -/// Every data source is gated by availability tokens. -/// Every unit is locked to its dimensional type. -/// Every health input feeds the typed rollup. +/// Каждое обязательное поле обеспечивается typestate билдера. +/// Каждый источник данных открывается токенами доступности. +/// Каждая единица измерения закреплена за своим размерным типом. +/// Каждый входной сигнал здоровья попадает в типизированную агрегацию. fn handle_get_computer_system( smbios: &Option<(SmbiosReady, SmbiosTables)>, sensors: &Option<(SensorsReady, SensorCache)>, @@ -1102,31 +1048,31 @@ fn handle_get_computer_system( power_state: PowerStateValue, bios_version: Option, ) -> serde_json::Value { - // ── 1. Health rollup (Section 4) ── - // Folds health from sensors + SEL into a single typed status + // ── 1. Агрегация здоровья (раздел 4) ── + // Сворачиваем здоровье датчиков и SEL в один типизированный статус let health = compute_system_health( sensors.as_ref(), sel.as_ref(), ); - // ── 2. Builder type-state (Section 1) ── + // ── 2. Typestate билдера (раздел 1) ── let builder = ComputerSystemBuilder::new() .power_state(power_state) .status(health); - // ── 3. Source-availability tokens (Section 2) ── + // ── 3. Токены доступности источников (раздел 2) ── let builder = match smbios { Some((proof, tables)) => { - // SMBIOS available — populate from hardware + // SMBIOS доступен — заполняем из аппаратуры populate_from_smbios(builder, proof, tables) } None => { - // SMBIOS unavailable — safe defaults + // SMBIOS недоступен — безопасные значения по умолчанию populate_smbios_fallback(builder) } }; - // ── 4. Optional enrichment from sensors (Section 3) ── + // ── 4. Необязательное дополнение из датчиков (раздел 3) ── let builder = if let Some((_proof, cache)) = sensors { builder .processor_summary(ProcessorSummary { @@ -1150,21 +1096,21 @@ fn handle_get_computer_system( None => builder, }; - // ── 5. Build (Section 1) ── - // .build() is available because both paths (SMBIOS present / absent) - // produce HasField for Name and UUID. The compiler verified this. + // ── 5. Сборка (раздел 1) ── + // .build() доступен, потому что оба пути (SMBIOS есть / нет) + // дают HasField для Name и UUID. Компилятор это проверил. builder.build("1") } -// ──── Server Startup ──── +// ──── Запуск сервера ──── fn main() { - // Initialize all data sources — each returns an availability token + // Инициализируем все источники данных: каждый возвращает токен доступности let smbios = init_smbios(); let sensors = init_sensors(); let sel = init_sel(); - // Simulate handler call + // Имитируем вызов обработчика let response = handle_get_computer_system( &smbios, &sensors, @@ -1177,7 +1123,7 @@ fn main() { } ``` -**Expected output:** +**Ожидаемый вывод:** ```json { @@ -1205,63 +1151,49 @@ fn main() { } ``` -### What the Compiler Proves (Server Side) +### Что доказывает компилятор (сторона сервера) -| # | Bug class | How it's prevented | Pattern (Section) | -|---|-----------|-------------------|-------------------| -| 1 | Missing required field in response | `.build()` requires all type-state markers to be `HasField` | Builder type-state (§1) | -| 2 | Calling into failed subsystem | Source-availability tokens gate data access | Capability tokens (§2) | -| 3 | No fallback for unavailable source | Both `match` arms (present/absent) must produce `HasField` | Type-state + exhaustive match (§2) | -| 4 | Wrong unit in JSON field | `reading_celsius: Celsius` ≠ `Rpm` ≠ `Watts` | Dimensional types (§3) | -| 5 | Incomplete health rollup | `compute_system_health` takes explicit source refs; SEL provides per-subsystem `HealthValue` via ch07's `TypedSelSummary` | Typed function signature + exhaustive matching (§4) | -| 6 | Schema version mismatch | `ComputerSystemResponse` has `last_reset_time()`; `V1_5` doesn't | Phantom types (§5) | -| 7 | Invalid action payload accepted | serde rejects unknown/missing fields before `execute()` | Typed action dispatch (§6) | -| 8 | Missing action variant handling | `match params.reset_type` is exhaustive | Enum exhaustiveness (§6) | -| 9 | Wrong action params to wrong handler | `RedfishAction::Params` is an associated type | Typed commands inverted (§6) | +| № | Класс ошибки | Как это предотвращается | Паттерн (раздел) | +|---|--------------|-------------------------|------------------| +| 1 | Отсутствует обязательное поле в ответе | `.build()` требует, чтобы все маркеры типа были `HasField` | Typestate билдера (§1) | +| 2 | Обращение к неисправной подсистеме | Токены доступности источников открывают доступ к данным | Capability-токены (§2) | +| 3 | Нет запасного пути для недоступного источника | Обе ветки `match` (есть / нет) обязаны давать `HasField` | Typestate + исчерпывающее match (§2) | +| 4 | Неверная единица в поле JSON | `reading_celsius: Celsius` ≠ `Rpm` ≠ `Watts` | Размерные типы (§3) | +| 5 | Неполная агрегация здоровья | `compute_system_health` принимает явные ссылки на источники; SEL даёт `HealthValue` по подсистемам через `TypedSelSummary` гл. 07 | Типизированная сигнатура функции + исчерпывающее сопоставление (§4) | +| 6 | Несоответствие версии схемы | `ComputerSystemResponse` имеет `last_reset_time()`, у `V1_5` его нет | Phantom-типы (§5) | +| 7 | Принимается некорректная полезная нагрузка действия | serde отвергает неизвестные и отсутствующие поля до `execute()` | Типизированная диспетчеризация действий (§6) | +| 8 | Отсутствует обработка варианта действия | `match params.reset_type` исчерпывающий | Исчерпываемость enum (§6) | +| 9 | Параметры не того действия переданы обработчику | `RedfishAction::Params` — ассоциированный тип | Типизированные команды, перевёрнутые (§6) | -**Total runtime overhead: zero.** The builder markers, availability tokens, phantom -version types, and dimensional newtypes all compile away. The JSON produced is -identical to the hand-rolled C version — minus nine classes of bugs. +**Суммарные накладные расходы во время выполнения: ноль.** Маркеры билдера, токены доступности, phantom-типы версий и размерные newtype исчезают при компиляции. Получаемый JSON идентичен версии на C, написанной вручную, за вычетом девяти классов ошибок. --- -## The Mirror: Client vs. Server Pattern Map - -| Concern | Client (ch17) | Server (this chapter) | -|---------|---------------|----------------------| -| **Boundary direction** | Inbound: JSON → typed values | Outbound: typed values → JSON | -| **Core principle** | "Parse, don't validate" | "Construct, don't serialize" | -| **Field completeness** | `TryFrom` validates required fields are present | Builder type-state gates `.build()` on required fields | -| **Unit safety** | `Celsius` ≠ `Rpm` when reading | `Celsius` ≠ `Rpm` when writing | -| **Privilege / availability** | Capability tokens gate requests | Availability tokens gate data source access | -| **Data sources** | Single source (BMC) | Multiple sources (SMBIOS, sensors, SEL, PCIe, ...) | -| **Schema version** | Phantom types prevent accessing unsupported fields | Phantom types enforce providing version-required fields | -| **Actions** | Client sends typed action POST | Server validates + dispatches via `RedfishAction` trait | -| **Health** | Read and trust `Status.Health` | Compute `Status.Health` via typed rollup | -| **Failure propagation** | One bad parse → one client error | One bad serialization → every client sees wrong data | - -The two chapters form a complete story. Ch17: *"Every response I consume is -type-checked."* This chapter: *"Every response I produce is type-checked."* The -same patterns flow in both directions — the type system doesn't know or care -which end of the wire you're on. - -## Key Takeaways - -1. **"Construct, don't serialize"** is the server-side mirror of "parse, don't - validate" — use builder type-state so `.build()` only exists when all required - fields are present. -2. **Source-availability tokens prove initialization** — the same capability token - pattern from ch04, repurposed to prove a data source is ready. -3. **Dimensional types protect producers and consumers** — putting `Rpm` in a - `ReadingCelsius` field is a compile error, not a customer-reported bug. -4. **Health rollup is a typed fold** — `Ord` on `HealthValue` plus explicit source - references mean the compiler catches "forgot to include PSU status." -5. **Schema versioning at the type level** — phantom type parameters make - version-specific fields appear and disappear at compile time. -6. **Action dispatch inverts ch02** — `serde` deserializes the payload into a - typed `Params` struct, and exhaustive matching on enum variants means adding a - new `ResetType` forces every handler to be updated. -7. **Server-side bugs propagate to every client** — that's why compile-time - correctness on the producer side is even more critical than on the consumer side. +## Зеркало: карта паттернов клиента и сервера + +| Аспект | Клиент (гл. 17) | Сервер (эта глава) | +|--------|-----------------|--------------------| +| **Направление границы** | Входящая: JSON → типизированные значения | Исходящая: типизированные значения → JSON | +| **Основной принцип** | «Parse, don't validate» | «Construct, don't serialize» | +| **Полнота полей** | `TryFrom` проверяет наличие обязательных полей | Typestate билдера открывает `.build()` при обязательных полях | +| **Безопасность единиц** | `Celsius` ≠ `Rpm` при чтении | `Celsius` ≠ `Rpm` при записи | +| **Привилегии / доступность** | Capability-токены открывают запросы | Токены доступности открывают доступ к источникам данных | +| **Источники данных** | Один источник (BMC) | Несколько источников (SMBIOS, датчики, SEL, PCIe, ...) | +| **Версия схемы** | Phantom-типы не дают обращаться к неподдерживаемым полям | Phantom-типы требуют заполнения полей, обязательных для версии | +| **Действия** | Клиент отправляет типизированный POST действия | Сервер проверяет и диспетчеризует через трейт `RedfishAction` | +| **Здоровье** | Читает и доверяет `Status.Health` | Вычисляет `Status.Health` через типизированную агрегацию | +| **Распространение сбоя** | Один плохой разбор → одна ошибка клиента | Одна плохая сериализация → каждый клиент видит неверные данные | + +Две главы образуют полную картину. Гл. 17: *«Каждый ответ, который я потребляю, проверен типами».* Эта глава: *«Каждый ответ, который я отдаю, проверен типами».* Одни и те же паттерны работают в обоих направлениях: система типов не знает и не заботится о том, с какой стороны провода вы находитесь. + +## Ключевые выводы + +1. **«Construct, don't serialize»** — серверное зеркало «parse, don't validate»: используйте typestate билдера, чтобы `.build()` существовал только тогда, когда заданы все обязательные поля. +2. **Токены доступности источников доказывают инициализацию**: тот же паттерн capability-токена из гл. 04, переиспользованный, чтобы доказать готовность источника данных. +3. **Размерные типы защищают и производителей, и потребителей**: положить `Rpm` в поле `ReadingCelsius` — это ошибка компиляции, а не баг, о котором сообщит заказчик. +4. **Агрегация здоровья — это типизированная свёртка**: `Ord` для `HealthValue` плюс явные ссылки на источники означают, что компилятор поймает «забыли учесть статус БП». +5. **Версионирование схемы на уровне типов**: параметры phantom-типов заставляют поля, специфичные для версии, появляться и исчезать на этапе компиляции. +6. **Диспетчеризация действий — обратная сторона гл. 02**: `serde` десериализует полезную нагрузку в типизированную структуру `Params`, а исчерпывающее сопоставление по вариантам enum означает, что добавление нового `ResetType` вынуждает обновить каждый обработчик. +7. **Ошибки на стороне сервера распространяются на каждого клиента**: поэтому корректность на этапе компиляции со стороны производителя ещё важнее, чем со стороны потребителя. --- From 3d0213ae2e0bca32be4b57d13ddac6ca54c79086 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 07:54:05 +0400 Subject: [PATCH 02/18] Translate Rust Patterns book to Russian Retranslate every chapter of rust-patterns-book/src from the English source. Prose, headings, tables, diagram labels, code comments and user-facing strings are in Russian; identifiers, code structure, file names and compiler output are unchanged. Also: - Align chapter headings and cross-references with the file numbering (chapters 9-16 were numbered one lower in the source), and add the functional/imperative chapter (8) to the table of contents and the pacing table. - Point internal links at the real chapter filenames. - Put toml/bash snippets in ch13 and ch15 inside proper code fences. - Translate the book title in book.toml and set language to ru. Co-Authored-By: Claude Haiku 5.5 --- rust-patterns-book/book.toml | 4 +- rust-patterns-book/src/SUMMARY.md | 50 +- rust-patterns-book/src/ch00-introduction.md | 196 ++-- .../src/ch01-generics-the-full-picture.md | 221 ++-- .../src/ch02-traits-in-depth.md | 989 ++++++++---------- ...h03-the-newtype-and-type-state-patterns.md | 518 ++++----- ...04-phantomdata-types-that-carry-no-data.md | 238 ++--- .../src/ch05-channels-and-message-passing.md | 163 ++- ...6-concurrency-vs-parallelism-vs-threads.md | 347 +++--- ...h07-closures-and-higher-order-functions.md | 271 +++-- ...tional-vs-imperative-when-elegance-wins.md | 417 ++++---- ...-smart-pointers-and-interior-mutability.md | 483 ++++----- .../src/ch10-error-handling-patterns.md | 162 ++- ...serialization-zero-copy-and-binary-data.md | 356 +++---- .../src/ch12-unsafe-rust-controlled-danger.md | 332 +++--- .../src/ch13-macros-code-that-writes-code.md | 261 +++-- .../ch14-testing-and-benchmarking-patterns.md | 127 ++- .../ch15-crate-architecture-and-api-design.md | 646 ++++++------ .../src/ch16-asyncawait-essentials.md | 156 ++- rust-patterns-book/src/ch17-exercises.md | 265 +++-- .../src/ch18-summary-and-reference-card.md | 175 ++-- .../src/ch19-capstone-project.md | 173 ++- 22 files changed, 3097 insertions(+), 3453 deletions(-) diff --git a/rust-patterns-book/book.toml b/rust-patterns-book/book.toml index 0ae7238..6870ed2 100644 --- a/rust-patterns-book/book.toml +++ b/rust-patterns-book/book.toml @@ -1,7 +1,7 @@ [book] -title = "Rust Patterns & Engineering How-Tos" +title = "Паттерны Rust и практические руководства по инженерии" authors = ["Rust Training Team"] -language = "en" +language = "ru" src = "src" [build] diff --git a/rust-patterns-book/src/SUMMARY.md b/rust-patterns-book/src/SUMMARY.md index 87f96b0..644688c 100644 --- a/rust-patterns-book/src/SUMMARY.md +++ b/rust-patterns-book/src/SUMMARY.md @@ -1,42 +1,42 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I: Type-Level Patterns +# Часть I: Паттерны на уровне типов -- [1. Generics — The Full Picture](ch01-generics-the-full-picture.md) -- [2. Traits In Depth](ch02-traits-in-depth.md) -- [3. The Newtype and Type-State Patterns](ch03-the-newtype-and-type-state-patterns.md) -- [4. PhantomData — Types That Carry No Data](ch04-phantomdata-types-that-carry-no-data.md) +- [1. Обобщённые типы — полная картина](ch01-generics-the-full-picture.md) +- [2. Трейты в деталях](ch02-traits-in-depth.md) +- [3. Паттерны newtype и type-state](ch03-the-newtype-and-type-state-patterns.md) +- [4. PhantomData — типы, которые не несут данных](ch04-phantomdata-types-that-carry-no-data.md) --- -# Part II: Concurrency & Runtime +# Часть II: Конкурентность и рантайм -- [5. Channels and Message Passing](ch05-channels-and-message-passing.md) -- [6. Concurrency vs Parallelism vs Threads](ch06-concurrency-vs-parallelism-vs-threads.md) -- [7. Closures and Higher-Order Functions](ch07-closures-and-higher-order-functions.md) -- [8. Functional vs. Imperative: When Elegance Wins](ch08-functional-vs-imperative-when-elegance-wins.md) -- [9. Smart Pointers and Interior Mutability](ch09-smart-pointers-and-interior-mutability.md) +- [5. Каналы и передача сообщений](ch05-channels-and-message-passing.md) +- [6. Конкурентность, параллелизм и потоки](ch06-concurrency-vs-parallelism-vs-threads.md) +- [7. Замыкания и функции высшего порядка](ch07-closures-and-higher-order-functions.md) +- [8. Функциональный и императивный стили: когда выигрывает изящество](ch08-functional-vs-imperative-when-elegance-wins.md) +- [9. Умные указатели и внутренняя изменяемость](ch09-smart-pointers-and-interior-mutability.md) --- -# Part III: Systems & Production +# Часть III: Системы и продакшн -- [10. Error Handling Patterns](ch10-error-handling-patterns.md) -- [11. Serialization, Zero-Copy, and Binary Data](ch11-serialization-zero-copy-and-binary-data.md) -- [12. Unsafe Rust — Controlled Danger](ch12-unsafe-rust-controlled-danger.md) -- [13. Macros — Code That Writes Code](ch13-macros-code-that-writes-code.md) -- [14. Testing and Benchmarking Patterns](ch14-testing-and-benchmarking-patterns.md) -- [15. Crate Architecture and API Design](ch15-crate-architecture-and-api-design.md) -- [16. Async/Await Essentials](ch16-asyncawait-essentials.md) -- [17. Exercises](ch17-exercises.md) +- [10. Паттерны обработки ошибок](ch10-error-handling-patterns.md) +- [11. Сериализация, zero-copy и бинарные данные](ch11-serialization-zero-copy-and-binary-data.md) +- [12. Unsafe Rust — контролируемая опасность](ch12-unsafe-rust-controlled-danger.md) +- [13. Макросы — код, который пишет код](ch13-macros-code-that-writes-code.md) +- [14. Паттерны тестирования и бенчмаркинга](ch14-testing-and-benchmarking-patterns.md) +- [15. Архитектура крейтов и дизайн API](ch15-crate-architecture-and-api-design.md) +- [16. Основы async/await](ch16-asyncawait-essentials.md) +- [17. Упражнения](ch17-exercises.md) --- -# Appendices +# Приложения -- [Summary and Reference Card](ch18-summary-and-reference-card.md) -- [Capstone Project: Type-Safe Task Scheduler](ch19-capstone-project.md) +- [Итоги и справочная карточка](ch18-summary-and-reference-card.md) +- [Итоговый проект: планировщик задач с безопасностью типов](ch19-capstone-project.md) diff --git a/rust-patterns-book/src/ch00-introduction.md b/rust-patterns-book/src/ch00-introduction.md index 663704d..6bce4c6 100644 --- a/rust-patterns-book/src/ch00-introduction.md +++ b/rust-patterns-book/src/ch00-introduction.md @@ -1,140 +1,146 @@ -# Rust Patterns & Engineering How-Tos +# Паттерны Rust и практические руководства по инженерии -## Speaker Intro +## Об авторе -- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team -- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems -- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since +- Ведущий архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft +- Опытный инженер с экспертизой в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре CPU и платформ, а также системах на C++ +- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык --- -A practical guide to intermediate-and-above Rust patterns that arise in real codebases. This is not a language tutorial — it assumes you can write basic Rust and want to level up. Each chapter isolates one concept, explains when and why to use it, and provides compilable examples with inline exercises. +Практическое руководство по паттернам Rust уровня «средний и выше», которые возникают в реальных кодовых базах. Это не учебник по языку: предполагается, что вы уже умеете писать простой код на Rust и хотите перейти на следующий уровень. Каждая глава посвящена одной концепции, объясняет, когда и зачем её применять, и содержит компилируемые примеры с упражнениями прямо в тексте. -## Who This Is For +## Для кого эта книга -- Developers who have finished *The Rust Programming Language* but struggle with "how do I actually design this?" -- C++/C# engineers translating production systems into Rust -- Anyone who has hit a wall with generics, trait bounds, or lifetime errors and wants a systematic toolkit +- Разработчики, которые прошли *The Rust Programming Language*, но не понимают, как на практике спроектировать решение +- Инженеры, переносящие промышленные системы с C++ и C# на Rust +- Все, кто упёрся в стену с обобщёнными типами, границами трейтов или ошибками времён жизни и хочет систематический набор инструментов -## Prerequisites +## Предварительные требования -Before starting, you should be comfortable with: -- Ownership, borrowing, and lifetimes (basic level) -- Enums, pattern matching, and `Option`/`Result` -- Structs, methods, and basic traits (`Display`, `Debug`, `Clone`) -- Cargo basics: `cargo build`, `cargo test`, `cargo run` +Прежде чем начать, вам стоит уверенно владеть: +- владением, заимствованием и временами жизни (базовый уровень) +- перечислениями, сопоставлением с образцом и `Option`/`Result` +- структурами, методами и базовыми трейтами (`Display`, `Debug`, `Clone`) +- основами Cargo: `cargo build`, `cargo test`, `cargo run` -## How to Use This Book +## Как пользоваться этой книгой -### Difficulty Legend +### Обозначения сложности -Each chapter is tagged with a difficulty level: +Каждая глава помечена уровнем сложности: -| Symbol | Level | Meaning | -|--------|-------|---------| -| 🟢 | Fundamentals | Core concepts every Rust developer needs | -| 🟡 | Intermediate | Patterns used in production codebases | -| 🔴 | Advanced | Deep language mechanics — revisit as needed | +| Символ | Уровень | Значение | +|--------|---------|----------| +| 🟢 | Основы | Базовые концепции, которые нужны каждому разработчику на Rust | +| 🟡 | Средний | Паттерны, которые применяются в промышленных кодовых базах | +| 🔴 | Продвинутый | Глубинные механизмы языка: возвращайтесь к ним по мере необходимости | -### Pacing Guide +### План изучения -| Chapters | Topic | Suggested Time | Checkpoint | -|----------|-------|----------------|------------| -| **Part I: Type-Level Patterns** | | | | -| 1. Generics 🟢 | Monomorphization, const generics, `const fn` | 1–2 hours | Can explain when `dyn Trait` beats generics | -| 2. Traits 🟡 | Associated types, GATs, blanket impls, vtables | 3–4 hours | Can design a trait with associated types | -| 3. Newtype & Type-State 🟡 | Zero-cost safety, compile-time FSMs | 2–3 hours | Can build a type-state builder pattern | -| 4. PhantomData 🔴 | Lifetime branding, variance, drop check | 2–3 hours | Can explain why `PhantomData` differs from `PhantomData` | -| **Part II: Concurrency & Runtime** | | | | -| 5. Channels 🟢 | `mpsc`, crossbeam, `select!`, actors | 1–2 hours | Can implement a channel-based worker pool | -| 6. Concurrency 🟡 | Threads, rayon, Mutex, RwLock, atomics | 2–3 hours | Can pick the right sync primitive for a scenario | -| 7. Closures 🟢 | `Fn`/`FnMut`/`FnOnce`, combinators | 1–2 hours | Can write a higher-order function that accepts closures | -| 8. Smart Pointers 🟡 | Box, Rc, Arc, RefCell, Cow, Pin | 2–3 hours | Can explain when to use each smart pointer | -| **Part III: Systems & Production** | | | | -| 9. Error Handling 🟢 | thiserror, anyhow, `?` operator | 1–2 hours | Can design an error type hierarchy | -| 10. Serialization 🟡 | serde, zero-copy, binary data | 2–3 hours | Can write a custom serde deserializer | -| 11. Unsafe 🔴 | Superpowers, FFI, UB pitfalls, allocators | 2–3 hours | Can wrap unsafe code in a sound safe API | -| 12. Macros 🟡 | `macro_rules!`, proc macros, `syn`/`quote` | 2–3 hours | Can write a declarative macro with `tt` munching | -| 13. Testing 🟢 | Unit/integration/doc tests, proptest, criterion | 1–2 hours | Can set up property-based tests | -| 14. API Design 🟡 | Module layout, ergonomic APIs, feature flags | 2–3 hours | Can apply the "parse, don't validate" pattern | -| 15. Async 🔴 | Futures, Tokio, common pitfalls | 1–2 hours | Can identify async anti-patterns | -| **Appendices** | | | | -| Reference Card | Quick-look trait bounds, lifetimes, patterns | As needed | — | -| Capstone Project | Type-safe task scheduler | 4–6 hours | Submit a working implementation | +| Главы | Тема | Рекомендуемое время | Контрольная точка | +|-------|------|---------------------|-------------------| +| **Часть I: Паттерны на уровне типов** | | | | +| 1. Обобщённые типы 🟢 | Мономорфизация, const-обобщения, `const fn` | 1–2 часа | Может объяснить, когда `dyn Trait` предпочтительнее обобщений | +| 2. Трейты 🟡 | Ассоциированные типы, GAT, blanket-реализации, vtable | 3–4 часа | Может спроектировать трейт с ассоциированными типами | +| 3. Newtype и type-state 🟡 | Безопасность типов без затрат во время выполнения, автоматы на этапе компиляции | 2–3 часа | Может реализовать builder-паттерн на основе type-state | +| 4. PhantomData 🔴 | Маркировка времён жизни, вариантность, проверка drop | 2–3 часа | Может объяснить, почему `PhantomData` отличается от `PhantomData` | +| **Часть II: Конкурентность и рантайм** | | | | +| 5. Каналы 🟢 | `mpsc`, crossbeam, `select!`, акторы | 1–2 часа | Может реализовать пул воркеров на каналах | +| 6. Конкурентность 🟡 | Потоки, rayon, Mutex, RwLock, атомарные типы | 2–3 часа | Может подобрать подходящий примитив синхронизации для задачи | +| 7. Замыкания 🟢 | `Fn`/`FnMut`/`FnOnce`, комбинаторы | 1–2 часа | Может написать функцию высшего порядка, принимающую замыкания | +| 8. Функциональный и императивный стили 🟡 | Цепочки итераторов, комбинаторы `Option` и `Result`, когда выбирать цикл | 2–3 часа | Может объяснить, когда комбинатор понятнее цикла, а когда нет | +| 9. Умные указатели 🟡 | Box, Rc, Arc, RefCell, Cow, Pin | 2–3 часа | Может объяснить, когда применять каждый умный указатель | +| **Часть III: Системы и продакшн** | | | | +| 10. Обработка ошибок 🟢 | thiserror, anyhow, оператор `?` | 1–2 часа | Может спроектировать иерархию типов ошибок | +| 11. Сериализация 🟡 | serde, zero-copy, бинарные данные | 2–3 часа | Может написать собственный десериализатор serde | +| 12. Unsafe 🔴 | Сверхспособности, FFI, подводные камни UB, аллокаторы | 2–3 часа | Может обернуть unsafe-код в корректный безопасный API | +| 13. Макросы 🟡 | `macro_rules!`, процедурные макросы, `syn`/`quote` | 2–3 часа | Может написать декларативный макрос с tt munching | +| 14. Тестирование 🟢 | Модульные, интеграционные и doc-тесты, proptest, criterion | 1–2 часа | Может настроить property-based тесты | +| 15. Дизайн API 🟡 | Структура модулей, эргономичные API, флаги функций | 2–3 часа | Может применить паттерн «parse, don't validate» | +| 16. Async 🔴 | Futures, Tokio, распространённые ошибки | 1–2 часа | Может находить анти-паттерны async | +| **Приложения** | | | | +| Справочная карточка | Краткая сводка по границам трейтов, временам жизни и паттернам | По необходимости | — | +| Итоговый проект | Планировщик задач с безопасностью типов | 4–6 часов | Сдайте работающую реализацию | -**Total estimated time**: 30–45 hours for thorough study with exercises. +**Общее расчётное время**: 30–45 часов для основательного изучения с упражнениями. -### Working Through Exercises +### Работа с упражнениями -Every chapter ends with a hands-on exercise. For maximum learning: +Каждая глава заканчивается практическим упражнением. Чтобы извлечь из книги максимум: -1. **Try it yourself first** — spend at least 15 minutes before opening the solution -2. **Type the code** — don't copy-paste; typing builds muscle memory -3. **Modify the solution** — add a feature, change a constraint, break something on purpose -4. **Check cross-references** — most exercises combine patterns from multiple chapters +1. **Сначала попробуйте сами**: потратьте минимум 15 минут, прежде чем открывать решение +2. **Набирайте код**: не копируйте и не вставляйте, набор текста вырабатывает мышечную память +3. **Меняйте решение**: добавьте функциональность, измените ограничение, намеренно что-нибудь сломайте +4. **Проверяйте перекрёстные ссылки**: большинство упражнений сочетают паттерны из нескольких глав -The capstone project (Appendix) ties together patterns from across the book into a single, production-quality system. +Итоговый проект (в приложении) объединяет паттерны из всей книги в одну систему промышленного качества. -## Table of Contents +## Оглавление -### Part I: Type-Level Patterns +### Часть I: Паттерны на уровне типов -**[1. Generics — The Full Picture](ch01-generics-the-full-picture.md)** 🟢 -Monomorphization, code bloat trade-offs, generics vs enums vs trait objects, const generics, `const fn`. +**[1. Обобщённые типы — полная картина](ch01-generics-the-full-picture.md)** 🟢 +Мономорфизация, компромиссы по размеру кода, обобщения против перечислений и трейт-объектов, const-обобщения, `const fn`. -**[2. Traits In Depth](ch02-traits-in-depth.md)** 🟡 -Associated types, GATs, blanket impls, marker traits, vtables, HRTBs, extension traits, enum dispatch. +**[2. Трейты в деталях](ch02-traits-in-depth.md)** 🟡 +Ассоциированные типы, GAT, blanket-реализации, маркерные трейты, vtable, HRTB, трейты-расширения, диспетчеризация через перечисления. -**[3. The Newtype and Type-State Patterns](ch03-the-newtype-and-type-state-patterns.md)** 🟡 -Zero-cost type safety, compile-time state machines, builder patterns, config traits. +**[3. Паттерны newtype и type-state](ch03-the-newtype-and-type-state-patterns.md)** 🟡 +Безопасность типов без затрат во время выполнения, автоматы состояний на этапе компиляции, builder-паттерны, конфигурационные трейты. -**[4. PhantomData — Types That Carry No Data](ch04-phantomdata-types-that-carry-no-data.md)** 🔴 -Lifetime branding, unit-of-measure pattern, drop check, variance. +**[4. PhantomData — типы, которые не несут данных](ch04-phantomdata-types-that-carry-no-data.md)** 🔴 +Маркировка времён жизни, паттерн «единицы измерения», проверка drop, вариантность. -### Part II: Concurrency & Runtime +### Часть II: Конкурентность и рантайм -**[5. Channels and Message Passing](ch05-channels-and-message-passing.md)** 🟢 -`std::sync::mpsc`, crossbeam, `select!`, backpressure, actor pattern. +**[5. Каналы и передача сообщений](ch05-channels-and-message-passing.md)** 🟢 +`std::sync::mpsc`, crossbeam, `select!`, обратное давление (backpressure), паттерн актора. -**[6. Concurrency vs Parallelism vs Threads](ch06-concurrency-vs-parallelism-vs-threads.md)** 🟡 -OS threads, scoped threads, rayon, Mutex/RwLock/Atomics, Condvar, OnceLock, lock-free patterns. +**[6. Конкурентность, параллелизм и потоки](ch06-concurrency-vs-parallelism-vs-threads.md)** 🟡 +Потоки ОС, scoped-потоки, rayon, Mutex/RwLock/атомарные типы, Condvar, OnceLock, паттерны без блокировок. -**[7. Closures and Higher-Order Functions](ch07-closures-and-higher-order-functions.md)** 🟢 -`Fn`/`FnMut`/`FnOnce`, closures as parameters/return values, combinators, higher-order APIs. +**[7. Замыкания и функции высшего порядка](ch07-closures-and-higher-order-functions.md)** 🟢 +`Fn`/`FnMut`/`FnOnce`, замыкания как параметры и возвращаемые значения, комбинаторы, API высшего порядка. -**[8. Smart Pointers and Interior Mutability](ch08-smart-pointers-and-interior-mutability.md)** 🟡 +**[8. Функциональный и императивный стили: когда выигрывает изящество](ch08-functional-vs-imperative-when-elegance-wins.md)** 🟡 +Комбинаторы `Option` и `Result`, цепочки итераторов против циклов, оператор `?`, построение коллекций, антипаттерны. + +**[9. Умные указатели и внутренняя изменяемость](ch09-smart-pointers-and-interior-mutability.md)** 🟡 Box, Rc, Arc, Weak, Cell/RefCell, Cow, Pin, ManuallyDrop. -### Part III: Systems & Production +### Часть III: Системы и продакшн -**[9. Error Handling Patterns](ch09-error-handling-patterns.md)** 🟢 -thiserror vs anyhow, `#[from]`, `.context()`, `?` operator, panics. +**[10. Паттерны обработки ошибок](ch10-error-handling-patterns.md)** 🟢 +thiserror против anyhow, `#[from]`, `.context()`, оператор `?`, паника. -**[10. Serialization, Zero-Copy, and Binary Data](ch10-serialization-zero-copy-and-binary-data.md)** 🟡 -serde fundamentals, enum representations, zero-copy deserialization, `repr(C)`, `bytes::Bytes`. +**[11. Сериализация, zero-copy и бинарные данные](ch11-serialization-zero-copy-and-binary-data.md)** 🟡 +Основы serde, представления перечислений, десериализация без копирования, `repr(C)`, `bytes::Bytes`. -**[11. Unsafe Rust — Controlled Danger](ch11-unsafe-rust-controlled-danger.md)** 🔴 -Five superpowers, sound abstractions, FFI, UB pitfalls, arena/slab allocators. +**[12. Unsafe Rust — контролируемая опасность](ch12-unsafe-rust-controlled-danger.md)** 🔴 +Пять сверхспособностей, корректные абстракции, FFI, подводные камни UB, аллокаторы на основе арен и slab-аллокаторы. -**[12. Macros — Code That Writes Code](ch12-macros-code-that-writes-code.md)** 🟡 -`macro_rules!`, when (not) to use macros, proc macros, derive macros, `syn`/`quote`. +**[13. Макросы — код, который пишет код](ch13-macros-code-that-writes-code.md)** 🟡 +`macro_rules!`, когда (не) стоит использовать макросы, процедурные макросы, derive-макросы, `syn`/`quote`. -**[13. Testing and Benchmarking Patterns](ch13-testing-and-benchmarking-patterns.md)** 🟢 -Unit/integration/doc tests, proptest, criterion, mocking strategies. +**[14. Паттерны тестирования и бенчмаркинга](ch14-testing-and-benchmarking-patterns.md)** 🟢 +Модульные, интеграционные и doc-тесты, proptest, criterion, стратегии моков. -**[14. Crate Architecture and API Design](ch14-crate-architecture-and-api-design.md)** 🟡 -Module layout, API design checklist, ergonomic parameters, feature flags, workspaces. +**[15. Архитектура крейтов и дизайн API](ch15-crate-architecture-and-api-design.md)** 🟡 +Структура модулей, чек-лист дизайна API, эргономичные параметры, флаги функций, воркспейсы. -**[15. Async/Await Essentials](ch15-asyncawait-essentials.md)** 🔴 -Futures, Tokio quick-start, common pitfalls. (For deep async coverage, see our Async Rust Training.) +**[16. Основы async/await](ch16-asyncawait-essentials.md)** 🔴 +Futures, быстрый старт с Tokio, распространённые ошибки. (Подробнее об async см. в нашем курсе по Async Rust.) -### Appendices +**[17. Упражнения](ch17-exercises.md)** +Сводный набор упражнений по главам книги. -**[Summary and Reference Card](ch17-summary-and-reference-card.md)** -Pattern decision guide, trait bounds cheat sheet, lifetime elision rules, further reading. +### Приложения -**[Capstone Project: Type-Safe Task Scheduler](ch18-capstone-project.md)** -Integrate generics, traits, typestate, channels, error handling, and testing into a complete system. +**[Итоги и справочная карточка](ch18-summary-and-reference-card.md)** +Руководство по выбору паттерна, шпаргалка по границам трейтов, правила elision времён жизни, дополнительная литература. -*** +**[Итоговый проект: планировщик задач с безопасностью типов](ch19-capstone-project.md)** +Объединение обобщённых типов, трейтов, typestate, каналов, обработки ошибок и тестирования в единую систему. +*** diff --git a/rust-patterns-book/src/ch01-generics-the-full-picture.md b/rust-patterns-book/src/ch01-generics-the-full-picture.md index bbff4cb..2b3fdf4 100644 --- a/rust-patterns-book/src/ch01-generics-the-full-picture.md +++ b/rust-patterns-book/src/ch01-generics-the-full-picture.md @@ -1,14 +1,14 @@ -# 1. Generics — The Full Picture 🟢 +# 1. Обобщённые типы — полная картина 🟢 -> **What you'll learn:** -> - How monomorphization gives zero-cost generics — and when it causes code bloat -> - The decision framework: generics vs enums vs trait objects -> - Const generics for compile-time array sizes and `const fn` for compile-time evaluation -> - When to trade static dispatch for dynamic dispatch on cold paths +> **Что вы узнаете:** +> - Как мономорфизация даёт обобщения без накладных расходов и когда она приводит к раздуванию кода +> - Схему выбора: обобщения, перечисления или трейт-объекты +> - Const-обобщения для размеров массивов на этапе компиляции и `const fn` для вычислений на этапе компиляции +> - Когда на холодных путях стоит заменить статическую диспетчеризацию динамической -## Monomorphization and Zero Cost +## Мономорфизация и нулевые затраты -Generics in Rust are **monomorphized** — the compiler generates a specialized copy of each generic function for every concrete type it's used with. This is the opposite of Java/C# where generics are erased at runtime. +Обобщения в Rust **мономорфизируются**: компилятор генерирует специализированную копию каждой обобщённой функции для каждого конкретного типа, с которым она используется. Это противоположность Java и C#, где обобщения стираются во время выполнения. ```rust fn max_of(a: T, b: T) -> T { @@ -16,99 +16,94 @@ fn max_of(a: T, b: T) -> T { } fn main() { - max_of(3_i32, 5_i32); // Compiler generates max_of_i32 - max_of(2.0_f64, 7.0_f64); // Compiler generates max_of_f64 - max_of("a", "z"); // Compiler generates max_of_str + max_of(3_i32, 5_i32); // Компилятор сгенерирует max_of_i32 + max_of(2.0_f64, 7.0_f64); // Компилятор сгенерирует max_of_f64 + max_of("a", "z"); // Компилятор сгенерирует max_of_str } ``` -**What the compiler actually produces** (conceptually): +**Что на самом деле создаёт компилятор** (концептуально): ```rust -// Three separate functions — no runtime dispatch, no vtable: +// Три отдельные функции — никакой динамической диспетчеризации и никакой vtable: fn max_of_i32(a: i32, b: i32) -> i32 { if a >= b { a } else { b } } fn max_of_f64(a: f64, b: f64) -> f64 { if a >= b { a } else { b } } fn max_of_str<'a>(a: &'a str, b: &'a str) -> &'a str { if a >= b { a } else { b } } ``` -> **Why does `max_of_str` need `<'a>` but `max_of_i32` doesn't?** `i32` and `f64` -> are `Copy` types — the function returns an owned value. But `&str` is a reference, -> so the compiler must know the returned reference's lifetime. The `<'a>` annotation -> says "the returned `&str` lives at least as long as both inputs." +> **Почему `max_of_str` требует `<'a>`, а `max_of_i32` — нет?** `i32` и `f64` — типы с `Copy`, функция возвращает копию значения, которая не зависит ни от каких заимствований. А `&str` — это ссылка, поэтому компилятор должен знать время жизни возвращаемой ссылки. Аннотация `<'a>` означает: «возвращаемый `&str` живёт не меньше, чем оба входных параметра». -**Advantages**: Zero runtime cost — identical to hand-written specialized code. The optimizer can inline, vectorize, and specialize each copy independently. +**Преимущества**: нулевые затраты во время выполнения — код совпадает с вручную написанной специализированной версией. Оптимизатор может независимо инлайнить, векторизовать и специализировать каждую копию. -**Comparison with C++**: Rust generics work like C++ templates but with one crucial difference — **bounds checking happens at definition, not instantiation**. In C++, a template compiles only when used with a specific type, leading to cryptic error messages deep in library code. In Rust, `T: PartialOrd` is checked when you define the function, so errors are caught early and messages are clear. +**Сравнение с C++**: обобщения в Rust работают как шаблоны C++, но есть одно принципиальное отличие — **проверка ограничений происходит в момент определения, а не инстанцирования**. В C++ шаблон компилируется только при использовании с конкретным типом, поэтому ошибки всплывают глубоко в библиотечном коде и выглядят запутанно. В Rust `T: PartialOrd` проверяется при определении функции, поэтому ошибки находятся раньше, а сообщения понятны. ```rust -// Rust: error at definition site — "T doesn't implement Display" +// Rust: ошибка уже в месте определения — "T doesn't implement Display" fn broken(val: T) { println!("{val}"); // ❌ Error: T doesn't implement Display } -// Fix: add the bound +// Исправление: добавляем ограничение fn fixed(val: T) { println!("{val}"); // ✅ } ``` -### When Generics Hurt: Code Bloat +### Когда обобщения вредят: раздувание кода -Monomorphization has a cost — binary size. Each unique instantiation duplicates the function body: +У мономорфизации есть цена — размер бинарного файла. Каждая уникальная инстанциация дублирует тело функции: ```rust -// This innocent function... +// Эта невинная функция... fn serialize(value: &T) -> Vec { serde_json::to_vec(value).unwrap() } -// ...used with 50 different types → 50 copies in the binary. +// ...используемая с 50 разными типами → 50 копий в бинарном файле. ``` -**Mitigation strategies**: +**Стратегии смягчения**: ```rust -// 1. Extract the non-generic core ("outline" pattern) +// 1. Вынести необобщённое ядро (паттерн «outline») fn serialize(value: &T) -> Result, serde_json::Error> { - // Generic part: only the serialization call + // Обобщённая часть: только вызов сериализации let json_value = serde_json::to_value(value)?; - // Non-generic part: extracted into a separate function + // Необобщённая часть: вынесена в отдельную функцию serialize_value(json_value) } fn serialize_value(value: serde_json::Value) -> Result, serde_json::Error> { - // This function exists only ONCE in the binary + // Эта функция существует в бинарном файле ТОЛЬКО ОДИН раз serde_json::to_vec(&value) } -// 2. Use trait objects (dynamic dispatch) when inlining isn't critical +// 2. Использовать трейт-объекты (динамическая диспетчеризация), когда инлайнинг не критичен fn log_item(item: &dyn std::fmt::Display) { - // One copy — uses vtable for dispatch + // Одна копия — диспетчеризация через vtable println!("[LOG] {item}"); } ``` -> **Rule of thumb**: Use generics for hot paths where inlining matters. -> Use `dyn Trait` for cold paths (error handling, logging, configuration) -> where a vtable call is negligible. +> **Практическое правило**: используйте обобщения на горячих путях, где важен инлайнинг. Используйте `dyn Trait` на холодных путях (обработка ошибок, логирование, конфигурация), где вызов через vtable несущественен. -### Generics vs Enums vs Trait Objects — Decision Guide +### Обобщения, перечисления и трейт-объекты: руководство по выбору -Three ways to handle "different types, same interface" in Rust: +В Rust есть три способа реализовать «разные типы с одинаковым интерфейсом»: -| Approach | Dispatch | Known at | Extensible? | Overhead | -|----------|----------|----------|-------------|----------| -| **Generics** (`impl Trait` / ``) | Static (monomorphized) | Compile time | ✅ (open set) | Zero — inlined | -| **Enum** | Match arm | Compile time | ❌ (closed set) | Zero — no vtable | -| **Trait object** (`dyn Trait`) | Dynamic (vtable) | Runtime | ✅ (open set) | Vtable pointer + indirect call | +| Подход | Диспетчеризация | Известен на этапе | Расширяемый? | Накладные расходы | +|--------|-----------------|-------------------|--------------|-------------------| +| **Обобщения** (`impl Trait` / ``) | Статическая (мономорфизация) | Компиляции | ✅ (открытое множество) | Нулевые — инлайнятся | +| **Перечисление** | Ветка `match` | Компиляции | ❌ (закрытое множество) | Нулевые — без vtable | +| **Трейт-объект** (`dyn Trait`) | Динамическая (vtable) | Выполнения | ✅ (открытое множество) | Указатель на vtable и косвенный вызов | ```rust -// --- GENERICS: Open set, zero cost, compile-time --- +// --- ОБОБЩЕНИЯ: открытое множество, нулевые затраты, решается на этапе компиляции --- fn process(handler: H, request: Request) -> Response { - handler.handle(request) // Monomorphized — one copy per H + handler.handle(request) // Мономорфизировано — по одной копии на каждый H } -// --- ENUM: Closed set, zero cost, exhaustive matching --- +// --- ПЕРЕЧИСЛЕНИЕ: закрытое множество, нулевые затраты, исчерпывающее сопоставление --- enum Shape { Circle(f64), Rect(f64, f64), @@ -127,33 +122,33 @@ impl Shape { } } } -// Adding a new variant forces updating ALL match arms — the compiler -// enforces exhaustiveness. Great for "I control all the variants." +// При добавлении нового варианта придётся обновить ВСЕ ветки match: компилятор +// требует исчерпывающего сопоставления. Это удобно, когда вы контролируете все варианты. -// --- TRAIT OBJECT: Open set, runtime cost, extensible --- +// --- ТРЕЙТ-ОБЪЕКТ: открытое множество, затраты во время выполнения, расширяемый --- fn log_all(items: &[Box]) { for item in items { - println!("{item}"); // vtable dispatch + println!("{item}"); // диспетчеризация через vtable } } ``` -**Decision flowchart**: +**Схема выбора**: ```mermaid flowchart TD - A["Do you know ALL
possible types at
compile time?"] - A -->|"Yes, small
closed set"| B["Enum"] - A -->|"Yes, but set
is open"| C["Generics
(monomorphized)"] - A -->|"No — types
determined at runtime"| D["dyn Trait"] + A["Знаете ли вы ВСЕ
возможные типы на
этапе компиляции?"] + A -->|"Да, небольшое
закрытое множество"| B["Перечисление"] + A -->|"Да, но множество
открытое"| C["Обобщения
(мономорфизированные)"] + A -->|"Нет — типы определяются
во время выполнения"| D["dyn Trait"] - C --> E{"Hot path?
(millions of calls)"} - E -->|Yes| F["Generics
(inlineable)"] - E -->|No| G["dyn Trait
is fine"] + C --> E{"Горячий путь?
(миллионы вызовов)"} + E -->|Да| F["Обобщения
(инлайнируемые)"] + E -->|Нет| G["dyn Trait
подойдёт"] - D --> H{"Need mixed types
in one collection?"} - H -->|Yes| I["Vec<Box<dyn Trait>>"] - H -->|No| C + D --> H{"Нужны разные типы
в одной коллекции?"} + H -->|Да| I["Vec<Box<dyn Trait>>"] + H -->|Нет| C style A fill:#e8f4f8,stroke:#2980b9,color:#000 style B fill:#d4efdf,stroke:#27ae60,color:#000 @@ -166,12 +161,12 @@ flowchart TD style H fill:#fef9e7,stroke:#f1c40f,color:#000 ``` -### Const Generics +### Const-обобщения -Since Rust 1.51, you can parameterize types and functions over *constant values*, not just types: +Начиная с Rust 1.51, типы и функции можно параметризовать *константными значениями*, а не только типами: ```rust -// Array wrapper parameterized over size +// Обёртка над массивом, параметризованная размерами struct Matrix { data: [[f64; COLS]; ROWS], } @@ -192,10 +187,10 @@ impl Matrix { } } -// The compiler enforces dimensional correctness: +// Компилятор следит за согласованностью размерностей: fn multiply( a: &Matrix, - b: &Matrix, // N must match! + b: &Matrix, // N должно совпадать! ) -> Matrix { let mut result = Matrix::::new(); for i in 0..M { @@ -208,7 +203,7 @@ fn multiply( result } -// Usage: +// Использование: let a = Matrix::<2, 3>::new(); // 2×3 let b = Matrix::<3, 4>::new(); // 3×4 let c = multiply(&a, &b); // 2×4 ✅ @@ -217,24 +212,22 @@ let c = multiply(&a, &b); // 2×4 ✅ // multiply(&a, &d); // ❌ Compile error: expected Matrix<3, _>, got Matrix<5, 5> ``` -> **C++ comparison**: This is similar to `template` in C++, but Rust -> const generics are type-checked eagerly and don't suffer from SFINAE complexity. +> **Сравнение с C++**: это похоже на `template` в C++, но const-обобщения в Rust проверяются сразу и не страдают от сложности SFINAE. -### Const Functions (const fn) +### Константные функции (const fn) -`const fn` marks a function as evaluable at compile time — Rust's equivalent -of C++ `constexpr`. The result can be used in `const` and `static` contexts: +`const fn` помечает функцию как вычислимую на этапе компиляции. Это аналог `constexpr` в C++. Результат можно использовать в контекстах `const` и `static`: ```rust -// Basic const fn — evaluated at compile time when used in const context +// Простая const fn — вычисляется на этапе компиляции в константном контексте const fn celsius_to_fahrenheit(c: f64) -> f64 { c * 9.0 / 5.0 + 32.0 } -const BOILING_F: f64 = celsius_to_fahrenheit(100.0); // Computed at compile time +const BOILING_F: f64 = celsius_to_fahrenheit(100.0); // Вычисляется на этапе компиляции const FREEZING_F: f64 = celsius_to_fahrenheit(0.0); // 32.0 -// Const constructors — create statics without lazy_static! +// Константные конструкторы — создаём статические значения без lazy_static! struct BitMask(u32); impl BitMask { @@ -251,83 +244,76 @@ impl BitMask { } } -// Static lookup table — no runtime cost, no lazy initialization +// Статическая таблица поиска — без затрат во время выполнения и без ленивой инициализации const GPIO_INPUT: BitMask = BitMask::new(0); const GPIO_OUTPUT: BitMask = BitMask::new(1); const GPIO_IRQ: BitMask = BitMask::new(2); const GPIO_IO: BitMask = GPIO_INPUT.or(GPIO_OUTPUT); -// Register maps as const arrays: +// Таблицы регистров как константные массивы: const SENSOR_THRESHOLDS: [u16; 4] = { let mut table = [0u16; 4]; - table[0] = 50; // Warning - table[1] = 70; // High - table[2] = 85; // Critical - table[3] = 100; // Shutdown + table[0] = 50; // Предупреждение + table[1] = 70; // Высокая температура + table[2] = 85; // Критическая + table[3] = 100; // Аварийное отключение table }; -// The entire table exists in the binary — no heap, no runtime init. +// Вся таблица находится в бинарном файле: без кучи и без инициализации во время выполнения. ``` -**What you CAN do in `const fn`** (as of Rust 1.79+): -- Arithmetic, bit operations, comparisons -- `if`/`else`, `match`, `loop`, `while` (control flow) -- Creating and modifying local variables (`let mut`) -- Calling other `const fn`s -- References (`&`, `&mut` — within the const context) -- `panic!()` (becomes a compile error if reached at compile time) +**Что МОЖНО делать в `const fn`** (начиная с Rust 1.79): +- арифметика, битовые операции, сравнения +- `if`/`else`, `match`, `loop`, `while` (управление потоком) +- создание и изменение локальных переменных (`let mut`) +- вызов других `const fn` +- ссылки (`&`, `&mut` в пределах константного контекста) +- `panic!()` (становится ошибкой компиляции, если достигается на этапе компиляции) -**What you CANNOT do** (yet): -- Heap allocation (`Box`, `Vec`, `String`) -- Trait method calls (only inherent methods) -- Floating-point in some contexts (stabilized for basic ops) -- I/O or side effects +**Чего НЕЛЬЗЯ** (пока): +- выделение памяти в куче (`Box`, `Vec`, `String`) +- вызов методов трейтов (только собственные методы типа) +- операции с плавающей точкой в некоторых контекстах (стабилизированы только базовые операции) +- ввод-вывод и побочные эффекты ```rust -// const fn with panic — becomes a compile-time error: +// const fn с panic — становится ошибкой компиляции: const fn checked_div(a: u32, b: u32) -> u32 { if b == 0 { - panic!("division by zero"); // Compile error if b is 0 at const time + panic!("деление на ноль"); // Ошибка компиляции, если b равно 0 на этапе const } a / b } const RESULT: u32 = checked_div(100, 4); // ✅ 25 -// const BAD: u32 = checked_div(100, 0); // ❌ Compile error: "division by zero" +// const BAD: u32 = checked_div(100, 0); // ❌ Ошибка компиляции: паника «деление на ноль» ``` -> **C++ comparison**: `const fn` is Rust's `constexpr`. The key difference: -> Rust's version is opt-in and the compiler rigorously verifies that only -> const-compatible operations are used. In C++, `constexpr` functions can -> silently fall back to runtime evaluation — in Rust, a `const` context -> *requires* compile-time evaluation or it's a hard error. +> **Сравнение с C++**: `const fn` — это аналог `constexpr` в Rust. Ключевое отличие: в Rust это явный выбор, и компилятор строго проверяет, что используются только операции, совместимые с const. Функции `constexpr` в C++ могут молча перейти к вычислению во время выполнения. В Rust контекст `const` *требует* вычисления на этапе компиляции, иначе это жёсткая ошибка. -> **Practical advice**: Make constructors and simple utility functions `const fn` -> whenever possible — it costs nothing and enables callers to use them in const -> contexts. For hardware diagnostic code, `const fn` is ideal for register -> definitions, bitmask construction, and threshold tables. +> **Практический совет**: делайте конструкторы и простые служебные функции `const fn`, когда это возможно. Это ничего не стоит, а вызывающий код сможет использовать их в константных контекстах. Для кода диагностики оборудования `const fn` идеально подходит для описания регистров, построения битовых масок и таблиц пороговых значений. -> **Key Takeaways — Generics** -> - Monomorphization gives zero-cost abstractions but can cause code bloat — use `dyn Trait` for cold paths -> - Const generics (`[T; N]`) replace C++ template tricks with compile-time–checked array sizes -> - `const fn` eliminates `lazy_static!` for compile-time–computable values +> **Ключевые выводы — обобщения** +> - Мономорфизация даёт абстракции без накладных расходов, но может раздувать код — на холодных путях используйте `dyn Trait` +> - Const-обобщения (`[T; N]`) заменяют шаблонные трюки C++ размерами массивов, проверяемыми на этапе компиляции +> - `const fn` избавляет от `lazy_static!` для значений, которые можно вычислить на этапе компиляции -> **See also:** [Ch 2 — Traits In Depth](ch02-traits-in-depth.md) for trait bounds, associated types, and trait objects. [Ch 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) for zero-sized generic markers. +> **См. также:** [гл. 2 — Трейты в деталях](ch02-traits-in-depth.md) — ограничения трейтов, ассоциированные типы и трейт-объекты. [гл. 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) — маркеры-обобщения нулевого размера. --- -### Exercise: Generic Cache with Eviction ★★ (~30 min) +### Упражнение: обобщённый кэш с вытеснением ★★ (~30 минут) -Build a generic `Cache` struct that stores key-value pairs with a configurable maximum capacity. When full, the oldest entry is evicted (FIFO). Requirements: +Реализуйте обобщённую структуру `Cache`, которая хранит пары «ключ — значение» с настраиваемой максимальной ёмкостью. Когда кэш заполнен, вытесняется самая старая запись (FIFO). Требования: - `fn new(capacity: usize) -> Self` -- `fn insert(&mut self, key: K, value: V)` — evicts the oldest if at capacity +- `fn insert(&mut self, key: K, value: V)` — вытесняет самую старую запись, если достигнута ёмкость - `fn get(&self, key: &K) -> Option<&V>` - `fn len(&self) -> usize` -- Constrain `K: Eq + Hash + Clone` +- Ограничьте `K: Eq + Hash + Clone`
-🔑 Solution +🔑 Решение ```rust use std::collections::{HashMap, VecDeque}; @@ -378,14 +364,13 @@ fn main() { cache.insert("c", 3); assert_eq!(cache.len(), 3); - cache.insert("d", 4); // Evicts "a" + cache.insert("d", 4); // Вытесняет "a" assert_eq!(cache.get(&"a"), None); assert_eq!(cache.get(&"d"), Some(&4)); - println!("Cache works! len = {}", cache.len()); + println!("Кэш работает! len = {}", cache.len()); } ```
*** - diff --git a/rust-patterns-book/src/ch02-traits-in-depth.md b/rust-patterns-book/src/ch02-traits-in-depth.md index a2ed0ca..9689625 100644 --- a/rust-patterns-book/src/ch02-traits-in-depth.md +++ b/rust-patterns-book/src/ch02-traits-in-depth.md @@ -1,28 +1,28 @@ -# 2. Traits In Depth 🟡 +# 2. Трейты в деталях 🟡 -> **What you'll learn:** -> - Associated types vs generic parameters — and when to use each -> - GATs, blanket impls, marker traits, and trait object safety rules -> - How vtables and fat pointers work under the hood -> - Extension traits, enum dispatch, and typed command patterns +> **Что вы узнаете:** +> - Ассоциированные типы и обобщённые параметры: когда что использовать +> - GAT, blanket-реализации, маркерные трейты и правила объектной безопасности +> - Как устроены vtable и «толстые» указатели +> - Трейты-расширения, диспетчеризация через перечисления и паттерны типизированных команд -## Associated Types vs Generic Parameters +## Ассоциированные типы и обобщённые параметры -Both let a trait work with different types, but they serve different purposes: +Оба механизма позволяют трейту работать с разными типами, но служат разным целям: ```rust -// --- ASSOCIATED TYPE: One implementation per type --- +// --- АССОЦИИРОВАННЫЙ ТИП: одна реализация на тип --- trait Iterator { - type Item; // Each iterator produces exactly ONE kind of item + type Item; // Каждый итератор выдаёт ровно ОДИН вид элементов fn next(&mut self) -> Option; } -// A custom iterator that always yields i32 — there's no choice +// Пользовательский итератор, который всегда возвращает i32 — выбора здесь нет struct Counter { max: i32, current: i32 } impl Iterator for Counter { - type Item = i32; // Exactly one Item type per implementation + type Item = i32; // Ровно один тип Item в каждой реализации fn next(&mut self) -> Option { if self.current < self.max { self.current += 1; @@ -33,12 +33,12 @@ impl Iterator for Counter { } } -// --- GENERIC PARAMETER: Multiple implementations per type --- +// --- ОБОБЩЁННЫЙ ПАРАМЕТР: несколько реализаций на тип --- trait Convert { fn convert(&self) -> T; } -// A single type can implement Convert for MANY target types: +// Один тип может реализовать Convert для МНОГИХ целевых типов: impl Convert for i32 { fn convert(&self) -> f64 { *self as f64 } } @@ -47,23 +47,23 @@ impl Convert for i32 { } ``` -**When to use which**: +**Когда что использовать**: -| Use | When | -|-----|------| -| **Associated type** | There's exactly ONE natural output/result per implementing type. `Iterator::Item`, `Deref::Target`, `Add::Output` | -| **Generic parameter** | A type can meaningfully implement the trait for MANY different types. `From`, `AsRef`, `PartialEq` | +| Случай | Когда | +|--------|-------| +| **Ассоциированный тип** | Для каждого реализующего типа есть ровно ОДИН естественный результат. `Iterator::Item`, `Deref::Target`, `Add::Output` | +| **Обобщённый параметр** | Тип может осмысленно реализовывать трейт для МНОГИХ разных типов. `From`, `AsRef`, `PartialEq` | -**Intuition**: If it makes sense to ask "what is the `Item` of this iterator?", use associated type. If it makes sense to ask "can this convert to `f64`? to `String`? to `bool`?", use a generic parameter. +**Интуиция**: если осмысленно спросить «каков `Item` у этого итератора?», используйте ассоциированный тип. Если осмысленно спросить «можно ли это преобразовать в `f64`? в `String`? в `bool`?», используйте обобщённый параметр. ```rust -// Real-world example: std::ops::Add +// Пример из реального мира: std::ops::Add trait Add { - type Output; // Associated type — addition has ONE result type + type Output; // Ассоциированный тип — у сложения ОДИН тип результата fn add(self, rhs: Rhs) -> Self::Output; } -// Rhs is a generic parameter — you can add different types to Meters: +// Rhs — обобщённый параметр: к Meters можно прибавлять разные типы: struct Meters(f64); struct Centimeters(f64); @@ -77,29 +77,27 @@ impl Add for Meters { } ``` -### Generic Associated Types (GATs) +### Обобщённые ассоциированные типы (GAT) -Since Rust 1.65, associated types can have generic parameters of their own. -This enables **lending iterators** — iterators that return references tied to -the iterator rather than to the underlying collection: +Начиная с Rust 1.65, ассоциированные типы могут иметь собственные обобщённые параметры. Это позволяет реализовать **lending-итераторы**: итераторы, которые возвращают ссылки, привязанные к самому итератору, а не к базовой коллекции. ```rust -// Without GATs — impossible to express a lending iterator: +// Без GAT выразить lending-итератор невозможно: // trait LendingIterator { -// type Item<'a>; // ← This was rejected before 1.65 +// type Item<'a>; // ← До 1.65 это отклонялось компилятором // } -// With GATs (Rust 1.65+): -// Note: This is a custom trait, distinct from std::iter::Iterator. -// In real code, name it `LendingIterator` to avoid confusion with the -// standard `Iterator` trait. +// С GAT (Rust 1.65+): +// Примечание: это пользовательский трейт, отличный от std::iter::Iterator. +// В реальном коде назовите его `LendingIterator`, чтобы не путать со +// стандартным трейтом `Iterator`. trait LendingIterator { type Item<'a> where Self: 'a; fn next(&mut self) -> Option>; } -// Example: an iterator that yields overlapping windows +// Пример: итератор, который выдаёт перекрывающиеся окна struct WindowIter<'data> { data: &'data [u8], pos: usize, @@ -121,13 +119,11 @@ impl<'data> LendingIterator for WindowIter<'data> { } ``` -> **When you need GATs**: Lending iterators, streaming parsers, or any trait -> where the associated type's lifetime depends on the `&self` borrow. -> For most code, plain associated types are sufficient. +> **Когда нужны GAT**: lending-итераторы, потоковые парсеры и любые трейты, где время жизни ассоциированного типа зависит от заимствования `&self`. Для большинства кода достаточно обычных ассоциированных типов. -### Supertraits and Trait Hierarchies +### Супертрейты и иерархии трейтов -Traits can require other traits as prerequisites, forming hierarchies: +Трейты могут требовать другие трейты как предварительное условие, формируя иерархии: ```mermaid graph BT @@ -160,20 +156,20 @@ graph BT style Ord fill:#fef9e7,stroke:#f1c40f,color:#000 ``` -> Arrows point from subtrait to supertrait: implementing `Error` requires `Display` + `Debug`. +> Стрелки направлены от субтрейта к супертрейту: реализация `Error` требует `Display` и `Debug`. -A trait can require that implementors also implement other traits: +Трейт может потребовать, чтобы его реализаторы реализовывали и другие трейты: ```rust use std::fmt; -// Display is a supertrait of Error +// Display — супертрейт для Error trait Error: fmt::Display + fmt::Debug { fn source(&self) -> Option<&(dyn Error + 'static)> { None } } -// Any type implementing Error MUST also implement Display and Debug +// Любой тип, реализующий Error, ОБЯЗАН также реализовать Display и Debug -// Build your own hierarchies: +// Строим собственные иерархии: trait Identifiable { fn id(&self) -> u64; } @@ -182,12 +178,12 @@ trait Timestamped { fn created_at(&self) -> chrono::DateTime; } -// Entity requires both: +// Entity требует оба: trait Entity: Identifiable + Timestamped { fn is_active(&self) -> bool; } -// Implementing Entity forces you to implement all three: +// Реализация Entity заставляет реализовать все три трейта: struct User { id: u64, name: String, created: chrono::DateTime } impl Identifiable for User { @@ -201,55 +197,53 @@ impl Entity for User { } ``` -### Blanket Implementations +### Blanket-реализации -Implement a trait for ALL types that satisfy some bound: +Реализуйте трейт сразу для ВСЕХ типов, которые удовлетворяют некоторому ограничению: ```rust -// std does this: any type that implements Display automatically gets ToString +// Так устроена std: любой тип с Display автоматически получает ToString impl ToString for T { fn to_string(&self) -> String { format!("{self}") } } -// Now i32, &str, your custom types — anything with Display — gets to_string() for free. +// Теперь i32, &str и ваши собственные типы, у которых есть Display, получают to_string() бесплатно. -// Your own blanket impl: +// Собственная blanket-реализация: trait Loggable { fn log(&self); } -// Every Debug type is automatically Loggable: +// Каждый тип с Debug автоматически становится Loggable: impl Loggable for T { fn log(&self) { eprintln!("[LOG] {self:?}"); } } -// Now ANY Debug type has .log(): +// Теперь ЛЮБОЙ тип с Debug имеет метод .log(): // 42.log(); // [LOG] 42 // "hello".log(); // [LOG] "hello" // vec![1, 2, 3].log(); // [LOG] [1, 2, 3] ``` -> **Caution**: Blanket impls are powerful but irreversible — you can't add a -> more specific impl for a type that's already covered by a blanket impl -> (orphan rules + coherence). Design them carefully. +> **Осторожно**: blanket-реализации мощны, но необратимы. Нельзя добавить более специфичную реализацию для типа, который уже покрыт blanket-реализацией (правила сирот и когерентность). Проектируйте их внимательно. -### Marker Traits +### Маркерные трейты -Traits with no methods — they mark a type as having some property: +Трейты без методов помечают тип как обладающий некоторым свойством: ```rust -// Standard library marker traits: -// Send — safe to transfer between threads -// Sync — safe to share (&T) between threads -// Unpin — safe to move after pinning -// Sized — has a known size at compile time -// Copy — can be duplicated with memcpy - -// Your own marker trait: -/// Marker: this sensor has been factory-calibrated +// Маркерные трейты стандартной библиотеки: +// Send — безопасно передавать между потоками +// Sync — безопасно разделять (&T) между потоками +// Unpin — безопасно перемещать после закрепления (pin) +// Sized — размер известен на этапе компиляции +// Copy — может дублироваться через memcpy + +// Собственный маркерный трейт: +/// Маркер: этот датчик прошёл заводскую калибровку trait Calibrated {} struct RawSensor { reading: f64 } @@ -257,108 +251,103 @@ struct CalibratedSensor { reading: f64 } impl Calibrated for CalibratedSensor {} -// Only calibrated sensors can be used in production: +// В продакшене используются только откалиброванные датчики: fn record_measurement(sensor: &S) { // ... } -// record_measurement(&RawSensor { reading: 0.0 }); // ❌ Compile error +// record_measurement(&RawSensor { reading: 0.0 }); // ❌ Ошибка компиляции // record_measurement(&CalibratedSensor { reading: 0.0 }); // ✅ ``` -This connects directly to the **type-state pattern** in Chapter 3. +Это напрямую связано с **паттерном type-state** из главы 3. -### Trait Object Safety Rules +### Правила объектной безопасности -Not every trait can be used as `dyn Trait`. A trait is **object-safe** only if: +Не каждый трейт можно использовать как `dyn Trait`. Трейт является **объектно-безопасным** только если: -1. **No `Self: Sized` bound** on the trait itself -2. **No generic type parameters** on methods -3. **No use of `Self` in return position** (except via indirection like `Box`) -4. **No associated functions** (methods must have `&self`, `&mut self`, or `self`) +1. **Нет ограничения `Self: Sized`** на сам трейт +2. **Нет обобщённых параметров типа** у методов +3. **Нет `Self` в возвращаемом типе** (кроме случаев через косвенность, например `Box`) +4. **Нет ассоциированных функций без приёмника**: методы должны принимать `&self`, `&mut self` или `self` ```rust -// ✅ Object-safe — can be used as dyn Drawable +// ✅ Объектно-безопасен — можно использовать как dyn Drawable trait Drawable { fn draw(&self); fn bounding_box(&self) -> (f64, f64, f64, f64); } -let shapes: Vec> = vec![/* ... */]; // ✅ Works +let shapes: Vec> = vec![/* ... */]; // ✅ Работает -// ❌ NOT object-safe — uses Self in return position +// ❌ НЕ объектно-безопасен — использует Self в возвращаемом типе trait Clonable { fn clone_self(&self) -> Self; - // ^^^^ Can't know the concrete size at runtime + // ^^^^ Во время выполнения нельзя узнать конкретный размер } -// let items: Vec> = ...; // ❌ Compile error +// let items: Vec> = ...; // ❌ Ошибка компиляции -// ❌ NOT object-safe — generic method +// ❌ НЕ объектно-безопасен — обобщённый метод trait Converter { fn convert(&self) -> T; - // ^^^ The vtable can't contain infinite monomorphizations + // ^^^ vtable не может содержать бесконечное число мономорфизаций } -// ❌ NOT object-safe — associated function (no self) +// ❌ НЕ объектно-безопасен — ассоциированная функция (без self) trait Factory { fn create() -> Self; - // No &self — how would you call this through a trait object? + // Нет &self — как вызвать это через трейт-объект? } ``` -**Workarounds**: +**Обходные пути**: ```rust -// Add `where Self: Sized` to exclude a method from the vtable: +// Добавьте `where Self: Sized`, чтобы исключить метод из vtable: trait MyTrait { - fn regular_method(&self); // Included in vtable + fn regular_method(&self); // Входит в vtable fn generic_method(&self) -> T where - Self: Sized; // Excluded from vtable — can't be called via dyn MyTrait + Self: Sized; // Исключён из vtable — нельзя вызвать через dyn MyTrait } -// Now dyn MyTrait is valid, but generic_method can only be called -// when the concrete type is known. +// Теперь dyn MyTrait допустим, но generic_method можно вызвать +// только тогда, когда конкретный тип известен. ``` -> **Rule of thumb**: If you plan to use `dyn Trait`, keep methods simple — -> no generics, no `Self` in return types, no `Sized` bounds. When in doubt, -> try `let _: Box;` and let the compiler tell you. +> **Практическое правило**: если вы собираетесь использовать `dyn Trait`, держите методы простыми: без обобщений, без `Self` в возвращаемых типах и без ограничений `Sized`. Если сомневаетесь, попробуйте `let _: Box;` и пусть компилятор подскажет. -### Trait Objects Under the Hood — vtables and Fat Pointers +### Трейт-объекты изнутри: vtable и «толстые» указатели -A `&dyn Trait` (or `Box`) is a **fat pointer** — two machine words: +`&dyn Trait` (или `Box`) — это **«толстый» указатель**, состоящий из двух машинных слов: ```text ┌──────────────────────────────────────────────────┐ -│ &dyn Drawable (on 64-bit: 16 bytes total) │ +│ &dyn Drawable (на 64-битной системе: 16 байт) │ ├──────────────┬───────────────────────────────────┤ │ data_ptr │ vtable_ptr │ -│ (8 bytes) │ (8 bytes) │ +│ (8 байт) │ (8 байт) │ │ ↓ │ ↓ │ │ ┌─────────┐ │ ┌──────────────────────────────┐ │ -│ │ Circle │ │ │ vtable for │ │ +│ │ Circle │ │ │ vtable для │ │ │ │ r: 5.0 │ │ │ │ │ │ │ } │ │ │ drop_in_place: 0x7f...a0 │ │ -│ └─────────┘ │ │ size: 8 │ │ -│ │ │ align: 8 │ │ -│ │ │ draw: 0x7f...b4 │ │ -│ │ │ bounding_box: 0x7f...c8 │ │ +│ └─────────┘ │ │ size: 8 │ │ +│ │ │ align: 8 │ │ +│ │ │ draw: 0x7f...b4 │ │ +│ │ │ bounding_box: 0x7f...c8 │ │ │ │ └──────────────────────────────┘ │ └──────────────┴───────────────────────────────────┘ ``` -**How a vtable call works** (e.g., `shape.draw()`): +**Как работает вызов через vtable** (например, `shape.draw()`): -1. Load `vtable_ptr` from the fat pointer (second word) -2. Index into the vtable to find the `draw` function pointer -3. Call it, passing `data_ptr` as the `self` argument +1. Загрузить `vtable_ptr` из «толстого» указателя (второе слово) +2. Найти в vtable указатель на функцию `draw` +3. Вызвать её, передав `data_ptr` в качестве аргумента `self` -This is similar to C++ virtual dispatch in cost (one pointer indirection -per call), but Rust stores the vtable pointer in the fat pointer rather -than inside the object — so a plain `Circle` on the stack carries no -vtable pointer at all. +По стоимости это похоже на виртуальную диспетчеризацию C++ (одно косвенное обращение на вызов), но Rust хранит указатель на vtable внутри «толстого» указателя, а не внутри самого объекта. Поэтому обычный `Circle` на стеке вообще не содержит указателя на vtable. ```rust trait Drawable { @@ -369,14 +358,14 @@ trait Drawable { struct Circle { radius: f64 } impl Drawable for Circle { - fn draw(&self) { println!("Drawing circle r={}", self.radius); } + fn draw(&self) { println!("Рисуем круг r={}", self.radius); } fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } struct Square { side: f64 } impl Drawable for Square { - fn draw(&self) { println!("Drawing square s={}", self.side); } + fn draw(&self) { println!("Рисуем квадрат s={}", self.side); } fn area(&self) -> f64 { self.side * self.side } } @@ -386,48 +375,45 @@ fn main() { Box::new(Square { side: 3.0 }), ]; - // Each element is a fat pointer: (data_ptr, vtable_ptr) - // The vtable for Circle and Square are DIFFERENT + // Каждый элемент — «толстый» указатель: (data_ptr, vtable_ptr) + // vtable для Circle и Square РАЗНЫЕ for shape in &shapes { - shape.draw(); // vtable dispatch → Circle::draw or Square::draw - println!(" area = {:.2}", shape.area()); + shape.draw(); // диспетчеризация через vtable → Circle::draw или Square::draw + println!(" площадь = {:.2}", shape.area()); } - // Size comparison: - println!("size_of::<&Circle>() = {}", std::mem::size_of::<&Circle>()); - // → 8 bytes (one pointer — the compiler knows the type) - println!("size_of::<&dyn Drawable>() = {}", std::mem::size_of::<&dyn Drawable>()); - // → 16 bytes (data_ptr + vtable_ptr) + // Сравнение размеров: + println!("Размер size_of::<&Circle>() = {}", std::mem::size_of::<&Circle>()); + // → 8 байт (один указатель — компилятор знает тип) + println!("Размер size_of::<&dyn Drawable>() = {}", std::mem::size_of::<&dyn Drawable>()); + // → 16 байт (data_ptr + vtable_ptr) } ``` -**Performance cost model**: +**Модель стоимости производительности**: -| Aspect | Static dispatch (`impl Trait` / generics) | Dynamic dispatch (`dyn Trait`) | -|--------|------------------------------------------|-------------------------------| -| Call overhead | Zero — inlined by LLVM | One pointer indirection per call | -| Inlining | ✅ Compiler can inline | ❌ Opaque function pointer | -| Binary size | Larger (one copy per type) | Smaller (one shared function) | -| Pointer size | Thin (1 word) | Fat (2 words) | -| Heterogeneous collections | ❌ | ✅ `Vec>` | +| Аспект | Статическая диспетчеризация (`impl Trait` / обобщения) | Динамическая диспетчеризация (`dyn Trait`) | +|--------|--------------------------------------------------------|---------------------------------------------| +| Накладные расходы на вызов | Нулевые: инлайнится LLVM | Одно косвенное обращение через указатель на каждый вызов | +| Инлайнинг | ✅ Компилятор может инлайнить | ❌ Непрозрачный указатель на функцию | +| Размер бинарного файла | Больше (по копии на каждый тип) | Меньше (одна общая функция) | +| Размер указателя | «Тонкий» (1 слово) | «Толстый» (2 слова) | +| Разнородные коллекции | ❌ | ✅ `Vec>` | -> **When vtable cost matters**: In tight loops calling a trait method millions -> of times, the indirection and inability to inline can be significant (2-10× -> slower). For cold paths, configuration, or plugin architectures, the -> flexibility of `dyn Trait` is worth the small cost. +> **Когда стоимость vtable имеет значение**: в узких циклах, которые миллионы раз вызывают метод трейта, косвенность и невозможность инлайнинга могут заметно замедлить код (в 2–10 раз). Для холодных путей, конфигурации и архитектур с плагинами гибкость `dyn Trait` стоит небольшой цены. -### Higher-Ranked Trait Bounds (HRTBs) +### Higher-Ranked Trait Bounds (HRTB) -Sometimes you need a function that works with references of *any* lifetime, not a specific one. This is where `for<'a>` syntax appears: +Иногда нужна функция, которая работает со ссылками с *любым* временем жизни, а не с каким-то конкретным. Здесь и появляется синтаксис `for<'a>`: ```rust -// Problem: this function needs a closure that can process -// references with ANY lifetime, not just one specific lifetime. +// Проблема: этой функции нужно замыкание, которое может обрабатывать +// ссылки с ЛЮБЫМ временем жизни, а не с каким-то одним конкретным. -// ❌ This is too restrictive — 'a is fixed by the caller: +// ❌ Это слишком ограничительно — 'a фиксируется вызывающей стороной: // fn apply<'a, F: Fn(&'a str) -> &'a str>(f: F, data: &'a str) -> &'a str -// ✅ HRTB: F must work for ALL possible lifetimes: +// ✅ HRTB: F должен работать для ВСЕХ возможных времён жизни: fn apply(f: F, data: &str) -> &str where F: for<'a> Fn(&'a str) -> &'a str, @@ -441,16 +427,16 @@ fn main() { } ``` -**When you encounter HRTBs**: -- `Fn(&T) -> &U` traits — the compiler infers `for<'a>` automatically in most cases -- Custom trait implementations that must work across different borrows -- Deserialization with `serde`: `for<'de> Deserialize<'de>` +**Где вы встретите HRTB**: +- Трейты `Fn(&T) -> &U`: в большинстве случаев компилятор выводит `for<'a>` автоматически +- Собственные реализации трейтов, которые должны работать с разными заимствованиями +- Десериализация с `serde`: `for<'de> Deserialize<'de>` ```rust,ignore -// serde's DeserializeOwned is defined as: +// DeserializeOwned в serde определён так: // trait DeserializeOwned: for<'de> Deserialize<'de> {} -// Meaning: "can be deserialized from data with ANY lifetime" -// (i.e., the result doesn't borrow from the input) +// Смысл: «может быть десериализован из данных с ЛЮБЫМ временем жизни» +// (то есть результат не заимствует данные из входа) use serde::de::DeserializeOwned; @@ -459,54 +445,51 @@ fn parse_json(input: &str) -> T { } ``` -> **Practical advice**: You'll rarely write `for<'a>` yourself. It mostly appears -> in trait bounds on closure parameters, where the compiler handles it implicitly. -> But recognizing it in error messages ("expected a `for<'a> Fn(&'a ...)` bound") -> helps you understand what the compiler is asking for. +> **Практический совет**: сами вы редко будете писать `for<'a>`. Чаще всего он появляется в ограничениях трейтов для параметров замыканий, и компилятор обрабатывает его неявно. Но если узнавать его в сообщениях об ошибках («expected a `for<'a> Fn(&'a ...)` bound»), проще понять, чего от вас хочет компилятор. -### `impl Trait` — Argument Position vs Return Position +### `impl Trait`: позиция аргумента и позиция возвращаемого значения -`impl Trait` appears in two positions with **different semantics**: +`impl Trait` встречается в двух позициях с **разной семантикой**: ```rust -// --- Argument-Position impl Trait (APIT) --- -// "Caller chooses the type" — syntactic sugar for a generic parameter +// --- impl Trait в позиции аргумента (APIT) --- +// «Вызывающая сторона выбирает тип» — синтаксический сахар для обобщённого параметра fn print_all(items: impl Iterator) { for item in items { println!("{item}"); } } -// Equivalent to: +// Эквивалентно: fn print_all_verbose>(items: I) { for item in items { println!("{item}"); } } -// Caller decides: print_all(vec![1,2,3].into_iter()) -// print_all(0..10) +// Решает вызывающая сторона: print_all(vec![1,2,3].into_iter()) +// print_all(0..10) -// --- Return-Position impl Trait (RPIT) --- -// "Callee chooses the type" — the function picks one concrete type +// --- impl Trait в позиции возвращаемого значения (RPIT) --- +// «Вызываемая функция выбирает тип» — функция сама определяет конкретный тип fn evens(limit: i32) -> impl Iterator { (0..limit).filter(|x| x % 2 == 0) - // The concrete type is Filter, Closure> - // but the caller only sees "some Iterator" + // Конкретный тип — Filter, Closure>, + // но вызывающая сторона видит лишь «какой-то Iterator» } ``` -**Key difference**: +**Ключевое отличие**: | | APIT (`fn foo(x: impl T)`) | RPIT (`fn foo() -> impl T`) | |---|---|---| -| Who picks the type? | Caller | Callee (function body) | -| Monomorphized? | Yes — one copy per type | Yes — one concrete type | -| Turbofish? | No (`foo::()` not allowed) | N/A | -| Equivalent to | `fn foo(x: X)` | Existential type | +| Кто выбирает тип? | Вызывающая сторона | Вызываемая функция (тело функции) | +| Мономорфизируется? | Да: по копии на каждый тип | Да: один конкретный тип | +| Turbofish? | Нет (`foo::()` недопустим) | Неприменимо | +| Эквивалентно | `fn foo(x: X)` | Экзистенциальный тип | -#### RPIT in Trait Definitions (RPITIT) +#### RPIT в определениях трейтов (RPITIT) -Since Rust 1.75, you can use `-> impl Trait` directly in trait definitions: +Начиная с Rust 1.75, можно использовать `-> impl Trait` прямо в определениях трейтов: ```rust trait Container { fn items(&self) -> impl Iterator; - // ^^^^ Each implementor returns its own concrete type + // ^^^^ Каждая реализация возвращает свой конкретный тип } struct CsvRow { @@ -528,53 +511,51 @@ impl Container for FixedFields { } ``` -> **Before Rust 1.75**, you had to use `Box` or an associated -> type to achieve this in traits. RPITIT removes the allocation. +> **До Rust 1.75** для этого в трейтах приходилось использовать `Box` или ассоциированный тип. RPITIT убирает необходимость в выделении памяти в куче. -#### `impl Trait` vs `dyn Trait` — Decision Guide +#### `impl Trait` или `dyn Trait`: руководство по выбору ```text -Do you know the concrete type at compile time? -├── YES → Use impl Trait or generics (zero cost, inlinable) -└── NO → Do you need a heterogeneous collection? - ├── YES → Use dyn Trait (Box, &dyn T) - └── NO → Do you need the SAME trait object across an API boundary? - ├── YES → Use dyn Trait - └── NO → Use generics / impl Trait +Известен ли конкретный тип на этапе компиляции? +├── ДА → Используйте impl Trait или обобщения (нулевые затраты, можно инлайнить) +└── НЕТ → Нужна ли разнородная коллекция? + ├── ДА → Используйте dyn Trait (Box, &dyn T) + └── НЕТ → Нужен ли ОДИН И ТОТ ЖЕ трейт-объект на границе API? + ├── ДА → Используйте dyn Trait + └── НЕТ → Используйте обобщения / impl Trait ``` -| Feature | `impl Trait` | `dyn Trait` | -|---------|-------------|------------| -| Dispatch | Static (monomorphized) | Dynamic (vtable) | -| Performance | Best — inlinable | One indirection per call | -| Heterogeneous collections | ❌ | ✅ | -| Binary size per type | One copy each | Shared code | -| Trait must be object-safe? | No | Yes | -| Works in trait definitions | ✅ (Rust 1.75+) | Always | +| Характеристика | `impl Trait` | `dyn Trait` | +|----------------|--------------|-------------| +| Диспетчеризация | Статическая (мономорфизация) | Динамическая (vtable) | +| Производительность | Лучшая: можно инлайнить | Одно косвенное обращение на вызов | +| Разнородные коллекции | ❌ | ✅ | +| Размер бинарного файла на тип | Своя копия для каждого | Общий код | +| Трейт должен быть объектно-безопасным? | Нет | Да | +| Работает в определениях трейтов | ✅ (Rust 1.75+) | Всегда | *** -## Type Erasure with `Any` and `TypeId` +## Стирание типов с помощью `Any` и `TypeId` -Sometimes you need to store values of *unknown* types and downcast them later — a pattern -familiar from `void*` in C or `object` in C#. Rust provides this through `std::any::Any`: +Иногда нужно хранить значения *неизвестных* типов и позже приводить их обратно к конкретному типу. Такой паттерн знаком по `void*` в C и `object` в C#. В Rust его даёт `std::any::Any`: ```rust use std::any::Any; -// Store heterogeneous values: +// Хранение разнородных значений: fn log_value(value: &dyn Any) { if let Some(s) = value.downcast_ref::() { println!("String: {s}"); } else if let Some(n) = value.downcast_ref::() { println!("i32: {n}"); } else { - // TypeId lets you inspect the type at runtime: - println!("Unknown type: {:?}", value.type_id()); + // TypeId позволяет узнать тип во время выполнения: + println!("Неизвестный тип: {:?}", value.type_id()); } } -// Useful for plugin systems, event buses, or ECS-style architectures: +// Полезно для систем плагинов, шин событий и архитектур в стиле ECS: struct AnyMap(std::collections::HashMap>); impl AnyMap { @@ -597,51 +578,44 @@ fn main() { assert_eq!(map.get::(), Some(&42)); assert_eq!(map.get::().map(|s| s.as_str()), Some("hello")); - assert_eq!(map.get::(), None); // Never inserted + assert_eq!(map.get::(), None); // Никогда не вставлялось } ``` -> **When to use `Any`**: Plugin/extension systems, type-indexed maps (`typemap`), -> error downcasting (`anyhow::Error::downcast_ref`). Prefer generics or trait -> objects when the set of types is known at compile time — `Any` is a last resort -> that trades compile-time safety for flexibility. +> **Когда использовать `Any`**: системы плагинов и расширений, карты с индексацией по типам (`typemap`), приведение ошибок к конкретному типу (`anyhow::Error::downcast_ref`). Если набор типов известен на этапе компиляции, предпочитайте обобщения или трейт-объекты: `Any` — это крайняя мера, которая обменивает безопасность на этапе компиляции на гибкость. *** -## Extension Traits — Adding Methods to Types You Don't Own +## Трейты-расширения: добавление методов к чужим типам -Rust's orphan rule prevents you from implementing a foreign trait on a foreign type. -Extension traits are the standard workaround: define a **new trait** in your crate whose -methods have a blanket implementation for any type that meets a bound. The caller imports -the trait and the new methods appear on existing types. +Правило сирот не позволяет реализовать чужой трейт для чужого типа. Стандартный обходной путь — трейты-расширения: вы определяете в своём крейте **новый трейт**, методы которого имеют blanket-реализацию для любого типа, удовлетворяющего некоторому ограничению. Вызывающий код импортирует трейт, и новые методы появляются у существующих типов. -This pattern is pervasive in the Rust ecosystem: `itertools::Itertools`, `futures::StreamExt`, -`tokio::io::AsyncReadExt`, `tower::ServiceExt`. +Этот паттерн широко распространён в экосистеме Rust: `itertools::Itertools`, `futures::StreamExt`, `tokio::io::AsyncReadExt`, `tower::ServiceExt`. -### The Problem +### Проблема ```rust -// We want to add a .mean() method to all iterators that yield f64. -// But Iterator is defined in std and f64 is a primitive — orphan rule prevents: +// Хотим добавить метод .mean() ко всем итераторам, которые выдают f64. +// Но Iterator определён в std, а f64 — примитивный тип, и правило сирот не позволяет: // -// impl> I { // ❌ Cannot add inherent methods to a foreign type +// impl> I { // ❌ Нельзя добавлять собственные методы к чужому типу // fn mean(self) -> f64 { ... } // } ``` -### The Solution: An Extension Trait +### Решение: трейт-расширение ```rust -/// Extension methods for iterators over numeric values. +/// Методы-расширения для итераторов по числовым значениям. pub trait IteratorExt: Iterator { - /// Computes the arithmetic mean. Returns `None` for empty iterators. + /// Вычисляет среднее арифметическое. Для пустых итераторов возвращает `None`. fn mean(self) -> Option where Self: Sized, Self::Item: Into; } -// Blanket implementation — automatically applies to ALL iterators +// Blanket-реализация — автоматически применяется ко ВСЕМ итераторам impl IteratorExt for I { fn mean(self) -> Option where @@ -658,19 +632,19 @@ impl IteratorExt for I { } } -// Usage — just import the trait: -use crate::IteratorExt; // One import and the method appears on all iterators +// Использование — достаточно импортировать трейт: +use crate::IteratorExt; // Один импорт — и метод появляется у всех итераторов fn analyze_temperatures(readings: &[f64]) -> Option { - readings.iter().copied().mean() // .mean() is now available! + readings.iter().copied().mean() // .mean() теперь доступен! } fn analyze_sensor_data(data: &[i32]) -> Option { - data.iter().copied().mean() // Works on i32 too (i32: Into) + data.iter().copied().mean() // Работает и для i32 (i32: Into) } ``` -### Real-World Example: Diagnostic Result Extensions +### Пример из практики: расширения для результатов диагностики ```rust use std::collections::HashMap; @@ -681,7 +655,7 @@ struct DiagResult { message: String, } -/// Extension trait for Vec — adds domain-specific analysis methods. +/// Трейт-расширение для Vec: добавляет методы предметного анализа. pub trait DiagResultsExt { fn passed_count(&self) -> usize; fn failed_count(&self) -> usize; @@ -711,49 +685,47 @@ impl DiagResultsExt for Vec { } } -// Now any Vec has these methods: +// Теперь у любого Vec есть эти методы: fn report(results: Vec) { if !results.overall_pass() { let failures = results.failures_by_component(); for (component, fails) in &failures { - eprintln!("{component}: {} failures", fails.len()); + eprintln!("{component}: отказов — {}", fails.len()); } } } ``` -### Naming Convention +### Соглашение об именах -The Rust ecosystem uses a consistent `Ext` suffix: +В экосистеме Rust принято единообразное суффиксное имя `Ext`: -| Crate | Extension Trait | Extends | -|-------|----------------|---------| +| Крейт | Трейт-расширение | Расширяет | +|-------|------------------|-----------| | `itertools` | `Itertools` | `Iterator` | | `futures` | `StreamExt`, `FutureExt` | `Stream`, `Future` | | `tokio` | `AsyncReadExt`, `AsyncWriteExt` | `AsyncRead`, `AsyncWrite` | | `tower` | `ServiceExt` | `Service` | -| `bytes` | `BufMut` (partial) | `&mut [u8]` | -| Your crate | `DiagResultsExt` | `Vec` | +| `bytes` | `BufMut` (частично) | `&mut [u8]` | +| Ваш крейт | `DiagResultsExt` | `Vec` | -### When to Use +### Когда использовать -| Situation | Use Extension Trait? | -|-----------|:---:| -| Adding convenience methods to a foreign type | ✅ | -| Grouping domain-specific logic on generic collections | ✅ | -| The method needs access to private fields | ❌ (use a wrapper/newtype) | -| The method logically belongs on a new type you control | ❌ (just add it to your type) | -| You want the method available without any import | ❌ (inherent methods only) | +| Ситуация | Нужен трейт-расширение? | +|----------|:---:| +| Добавить удобные методы к чужому типу | ✅ | +| Сгруппировать логику предметной области для обобщённых коллекций | ✅ | +| Методу нужен доступ к приватным полям | ❌ (используйте обёртку/newtype) | +| Метод логически принадлежит новому типу, которым вы управляете | ❌ (просто добавьте его в свой тип) | +| Нужно, чтобы метод был доступен без каких-либо импортов | ❌ (подойдут только собственные методы) | *** -## Enum Dispatch — Static Polymorphism Without `dyn` +## Диспетчеризация через перечисления: статический полиморфизм без `dyn` -When you have a **closed set** of types implementing a trait, you can replace `dyn Trait` -with an enum whose variants hold the concrete types. This eliminates the vtable indirection -and heap allocation while preserving the same caller-facing interface. +Если у вас есть **закрытое множество** типов, реализующих трейт, можно заменить `dyn Trait` перечислением, варианты которого хранят конкретные типы. Это устраняет косвенное обращение через vtable и выделение памяти в куче, сохраняя тот же интерфейс для вызывающего кода. -### The Problem with `dyn Trait` +### Проблема с `dyn Trait` ```rust trait Sensor { @@ -771,25 +743,25 @@ impl Sensor for Gps { } impl Sensor for Thermometer { fn read(&self) -> f64 { self.temp_c } - fn name(&self) -> &str { "Thermometer" } + fn name(&self) -> &str { "Термометр" } } impl Sensor for Accelerometer { fn read(&self) -> f64 { self.g_force } - fn name(&self) -> &str { "Accelerometer" } + fn name(&self) -> &str { "Акселерометр" } } -// Heterogeneous collection with dyn — works, but has costs: +// Разнородная коллекция через dyn — работает, но имеет свою цену: fn read_all_dyn(sensors: &[Box]) -> Vec { sensors.iter().map(|s| s.read()).collect() - // Each .read() goes through a vtable indirection - // Each Box allocates on the heap + // Каждый вызов .read() проходит через косвенное обращение к vtable + // Каждый Box выделяет память в куче } ``` -### The Enum Dispatch Solution +### Решение через диспетчеризацию по перечислению ```rust -// Replace the trait object with an enum: +// Заменяем трейт-объект перечислением: enum AnySensor { Gps(Gps), Thermometer(Thermometer), @@ -814,10 +786,10 @@ impl AnySensor { } } -// Now: no heap allocation, no vtable, stored inline +// Теперь: без выделения памяти в куче, без vtable, хранится inline fn read_all(sensors: &[AnySensor]) -> Vec { sensors.iter().map(|s| s.read()).collect() - // Each .read() is a match branch — compiler can inline everything + // Каждый .read() — это ветка match, компилятор может заинлайнить всё } fn main() { @@ -833,9 +805,9 @@ fn main() { } ``` -### Implement the Trait on the Enum +### Реализация трейта для перечисления -For interoperability, you can implement the original trait on the enum itself: +Для совместимости можно реализовать исходный трейт и для самого перечисления: ```rust impl Sensor for AnySensor { @@ -856,15 +828,15 @@ impl Sensor for AnySensor { } } -// Now AnySensor works anywhere a Sensor is expected via generics: +// Теперь AnySensor работает везде, где ожидается Sensor, через обобщения: fn report(s: &S) { println!("{}: {:.2}", s.name(), s.read()); } ``` -### Reducing Boilerplate with a Macro +### Уменьшение шаблонного кода с помощью макроса -The match-arm delegation is repetitive. A macro eliminates it: +Делегирование в каждой ветке `match` повторяется. Макрос убирает повторы: ```rust macro_rules! dispatch_sensor { @@ -883,7 +855,7 @@ impl Sensor for AnySensor { } ``` -For larger projects, the `enum_dispatch` crate automates this entirely: +Для больших проектов крейт `enum_dispatch` автоматизирует всё это полностью: ```rust use enum_dispatch::enum_dispatch; @@ -900,68 +872,62 @@ enum AnySensor { Thermometer, Accelerometer, } -// All delegation code is generated automatically. +// Весь код делегирования генерируется автоматически. ``` -### `dyn Trait` vs Enum Dispatch — Decision Guide +### `dyn Trait` или диспетчеризация через перечисление: руководство по выбору ```text -Is the set of types closed (known at compile time)? -├── YES → Prefer enum dispatch (faster, no heap allocation) -│ ├── Few variants (< ~20)? → Manual enum -│ └── Many variants or growing? → enum_dispatch crate -└── NO → Must use dyn Trait (plugins, user-provided types) +Известно ли закрытое множество типов (на этапе компиляции)? +├── ДА → Предпочтительна диспетчеризация через перечисление (быстрее, без выделения памяти в куче) +│ ├── Мало вариантов (< ~20)? → перечисление вручную +│ └── Много вариантов или они растут? → крейт enum_dispatch +└── НЕТ → Нужен dyn Trait (плагины, типы, предоставленные пользователем) ``` -| Property | `dyn Trait` | Enum Dispatch | -|----------|:-----------:|:-------------:| -| Dispatch cost | Vtable indirection (~2ns) | Branch prediction (~0.3ns) | -| Heap allocation | Usually (Box) | None (inline) | -| Cache-friendly | No (pointer chasing) | Yes (contiguous) | -| Open to new types | ✅ (anyone can impl) | ❌ (closed set) | -| Code size | Shared | One copy per variant | -| Trait must be object-safe | Yes | No | -| Adding a variant | No code changes | Update enum + match arms | - -### When to Use Enum Dispatch - -| Scenario | Recommendation | -|----------|---------------| -| Diagnostic test types (CPU, GPU, NIC, Memory, ...) | ✅ Enum dispatch — closed set, known at compile time | -| Bus protocols (SPI, I2C, UART, ...) | ✅ Enum dispatch or Config trait | -| Plugin system (user loads .so at runtime) | ❌ Use `dyn Trait` | -| 2-3 variants | ✅ Manual enum dispatch | -| 10+ variants with many methods | ✅ `enum_dispatch` crate | -| Performance-critical inner loop | ✅ Enum dispatch (eliminates vtable) | +| Свойство | `dyn Trait` | Диспетчеризация через перечисление | +|----------|:-----------:|:----------------------------------:| +| Стоимость диспетчеризации | Косвенное обращение через vtable (~2 нс) | Предсказание ветвлений (~0,3 нс) | +| Выделение памяти в куче | Обычно (Box) | Нет (хранится inline) | +| Дружелюбность к кэшу | Нет (переходы по указателям) | Да (непрерывное хранение) | +| Открытость для новых типов | ✅ (реализовать может любой) | ❌ (закрытое множество) | +| Размер кода | Общий | Одна копия на вариант | +| Трейт должен быть объектно-безопасным | Да | Нет | +| Добавление варианта | Изменения кода не нужны | Обновить перечисление и ветки match | + +### Когда использовать диспетчеризацию через перечисление + +| Сценарий | Рекомендация | +|----------|--------------| +| Типы диагностических тестов (CPU, GPU, NIC, память, ...) | ✅ Диспетчеризация через перечисление: закрытое множество, известное на этапе компиляции | +| Протоколы шин (SPI, I2C, UART, ...) | ✅ Диспетчеризация через перечисление или трейт-конфигурация | +| Система плагинов (пользователь загружает .so во время выполнения) | ❌ Используйте `dyn Trait` | +| 2–3 варианта | ✅ Диспетчеризация через перечисление вручную | +| 10+ вариантов со многими методами | ✅ Крейт `enum_dispatch` | +| Критичный к производительности внутренний цикл | ✅ Диспетчеризация через перечисление (устраняет vtable) | *** -## Capability Mixins — Associated Types as Zero-Cost Composition +## Примеси возможностей (capability mixins): ассоциированные типы как композиция без накладных расходов -Ruby developers compose behaviour with **mixins** — `include SomeModule` injects methods -into a class. Rust traits with **associated types + default methods + blanket impls** -produce the same result, except: +Разработчики на Ruby компонуют поведение с помощью **миксинов**: `include SomeModule` добавляет методы в класс. Трейты Rust с **ассоциированными типами, методами по умолчанию и blanket-реализациями** дают тот же результат, за исключением следующего: -* Everything resolves at **compile time** — no method-missing surprises -* Each associated type is a **knob** that changes what the default methods produce -* The compiler **monomorphises** each combination — zero vtable overhead +* Всё разрешается на **этапе компиляции**: никаких сюрпризов вида method_missing +* Каждый ассоциированный тип — это **ручка настройки**, которая меняет то, что порождают методы по умолчанию +* Компилятор **мономорфизирует** каждую комбинацию: никаких накладных расходов на vtable -### The Problem: Cross-Cutting Bus Dependencies +### Проблема: сквозные зависимости от шин -Hardware diagnostic routines share common operations — read an IPMI sensor, toggle a -GPIO rail, sample a temperature over SPI — but different diagnostics need different -combinations. Inheritance hierarchies don't exist in Rust. Passing every bus handle -as a function argument creates unwieldy signatures. We need a way to **mix in** bus -capabilities à la carte. +Процедуры аппаратной диагностики используют общие операции: чтение датчика IPMI, переключение линии питания GPIO, измерение температуры по SPI. Но разным диагностикам нужны разные комбинации. В Rust нет иерархий наследования. Передавать каждый дескриптор шины отдельным аргументом функции значит получить громоздкие сигнатуры. Нам нужен способ **подмешивать** возможности шин по выбору, как блюда из меню à la carte. -### Step 1 — Define "Ingredient" Traits +### Шаг 1: определите трейты-«ингредиенты» -Each ingredient provides one hardware capability via an associated type: +Каждый ингредиент предоставляет одну аппаратную возможность через ассоциированный тип: ```rust use std::io; -// ── Bus abstractions (traits the hardware team provides) ────────── +// ── Абстракции шин (трейты, которые предоставляет команда аппаратного обеспечения) ── pub trait SpiBus { fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> io::Result<()>; } @@ -982,7 +948,7 @@ pub trait IpmiBmc { fn read_sensor(&self, sensor_id: u8) -> io::Result; } -// ── Ingredient traits — one per bus, carries an associated type ─── +// ── Трейты-ингредиенты: по одному на шину, каждый содержит ассоциированный тип ── pub trait HasSpi { type Spi: SpiBus; fn spi(&self) -> &Self::Spi; @@ -1004,52 +970,51 @@ pub trait HasIpmi { } ``` -Each ingredient is tiny, generic, and testable in isolation. +Каждый ингредиент мал, обобщён и тестируется изолированно. -### Step 2 — Define "Mixin" Traits +### Шаг 2: определите трейты-«примеси» -A mixin trait declares its required ingredients as supertraits, then provides all -its methods via **defaults** — implementors get them for free: +Трейт-примесь объявляет необходимые ингредиенты как супертрейты, а затем предоставляет все свои методы через **реализации по умолчанию**. Реализаторы получают их бесплатно: ```rust -/// Mixin: fan diagnostics — needs I2C (tachometer) + GPIO (PWM enable) +/// Примесь: диагностика вентиляторов. Нужны I2C (тахометр) и GPIO (разрешение ШИМ) pub trait FanDiagMixin: HasI2c + HasGpio { - /// Read fan RPM from the tachometer IC over I2C. + /// Прочитать обороты вентилятора из микросхемы тахометра по I2C. fn read_fan_rpm(&self, fan_id: u8) -> io::Result { let mut buf = [0u8; 2]; self.i2c().i2c_read(0x48 + fan_id, 0x00, &mut buf)?; - Ok(u16::from_be_bytes(buf) as u32 * 60) // tach counts → RPM + Ok(u16::from_be_bytes(buf) as u32 * 60) // отсчёты тахометра → об/мин } - /// Enable or disable the fan PWM output via GPIO. + /// Включить или выключить ШИМ-выход вентилятора через GPIO. fn set_fan_pwm(&self, enable: bool) -> io::Result<()> { if enable { self.gpio().set_high() } else { self.gpio().set_low() } } - /// Full fan health check — read RPM + verify within threshold. + /// Полная проверка состояния вентилятора: чтение оборотов и проверка порога. fn check_fan_health(&self, fan_id: u8, min_rpm: u32) -> io::Result { let rpm = self.read_fan_rpm(fan_id)?; Ok(rpm >= min_rpm) } } -/// Mixin: temperature monitoring — needs SPI (thermocouple ADC) + IPMI (BMC sensors) +/// Примесь: мониторинг температуры. Нужны SPI (АЦП термопары) и IPMI (датчики BMC) pub trait TempMonitorMixin: HasSpi + HasIpmi { - /// Read a thermocouple via the SPI ADC (e.g. MAX31855). + /// Прочитать термопару через АЦП на SPI (например, MAX31855). fn read_thermocouple(&self) -> io::Result { let mut rx = [0u8; 4]; self.spi().spi_transfer(&[0x00; 4], &mut rx)?; - let raw = i32::from_be_bytes(rx) >> 18; // 14-bit signed + let raw = i32::from_be_bytes(rx) >> 18; // 14 бит, со знаком Ok(raw as f64 * 0.25) } - /// Read a BMC-managed temperature sensor via IPMI. + /// Прочитать датчик температуры, которым управляет BMC, через IPMI. fn read_bmc_temp(&self, sensor_id: u8) -> io::Result { self.ipmi().read_sensor(sensor_id) } - /// Cross-validate: thermocouple vs BMC must agree within delta. + /// Перекрёстная проверка: показания термопары и BMC должны совпадать в пределах delta. fn validate_temps(&self, sensor_id: u8, max_delta: f64) -> io::Result { let tc = self.read_thermocouple()?; let bmc = self.read_bmc_temp(sensor_id)?; @@ -1057,29 +1022,29 @@ pub trait TempMonitorMixin: HasSpi + HasIpmi { } } -/// Mixin: power sequencing — needs GPIO (rail enable) + IPMI (event logging) +/// Примесь: управление последовательностью питания. Нужны GPIO (включение линии) и IPMI (журнал событий) pub trait PowerSeqMixin: HasGpio + HasIpmi { - /// Assert the power-good GPIO and verify via IPMI sensor. + /// Установить сигнал power-good через GPIO и проверить его через датчик IPMI. fn enable_power_rail(&self, sensor_id: u8) -> io::Result { self.gpio().set_high()?; std::thread::sleep(std::time::Duration::from_millis(50)); let voltage = self.ipmi().read_sensor(sensor_id)?; - Ok(voltage > 0.8) // above 80% nominal = good + Ok(voltage > 0.8) // выше 80% номинала: норма } - /// De-assert power and log shutdown via IPMI OEM command. + /// Снять питание и записать отключение через OEM-команду IPMI. fn disable_power_rail(&self) -> io::Result<()> { self.gpio().set_low()?; - // Log OEM "power rail disabled" event to BMC + // Записать OEM-событие «линия питания отключена» в BMC self.ipmi().raw_command(0x2E, 0x01, &[0x00, 0x01])?; Ok(()) } } ``` -### Step 3 — Blanket Impls Make It Truly "Mixin" +### Шаг 3: blanket-реализации делают их настоящими «примесями» -The magic line — provide the ingredients, get the methods: +Ключевая строка: предоставьте ингредиенты, получите методы: ```rust impl FanDiagMixin for T {} @@ -1087,13 +1052,12 @@ impl TempMonitorMixin for T {} impl PowerSeqMixin for T {} ``` -Any struct that implements the right ingredient traits **automatically** gains every -mixin method — no boilerplate, no forwarding, no inheritance. +Любая структура, которая реализует нужные трейты-ингредиенты, **автоматически** получает все методы примесей: без шаблонного кода, без делегирования и без наследования. -### Step 4 — Wire Up Production +### Шаг 4: соберите продакшен-версию ```rust -// ── Concrete bus implementations (Linux platform) ──────────────── +// ── Конкретные реализации шин (платформа Linux) ──────────────── struct LinuxSpi { dev: String } struct LinuxI2c { dev: String } struct SysfsGpio { pin: u32 } @@ -1101,13 +1065,13 @@ struct IpmiTool { timeout_secs: u32 } impl SpiBus for LinuxSpi { fn spi_transfer(&self, _tx: &[u8], _rx: &mut [u8]) -> io::Result<()> { - // spidev ioctl — omitted for brevity + // ioctl spidev: опущено для краткости Ok(()) } } impl I2cBus for LinuxI2c { fn i2c_read(&self, _addr: u8, _reg: u8, _buf: &mut [u8]) -> io::Result<()> { - // i2c-dev ioctl — omitted for brevity + // ioctl i2c-dev: опущено для краткости Ok(()) } fn i2c_write(&self, _addr: u8, _reg: u8, _data: &[u8]) -> io::Result<()> { Ok(()) } @@ -1119,13 +1083,13 @@ impl GpioPin for SysfsGpio { } impl IpmiBmc for IpmiTool { fn raw_command(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result> { - // shells out to ipmitool — omitted for brevity + // вызывает ipmitool: опущено для краткости Ok(vec![]) } fn read_sensor(&self, _id: u8) -> io::Result { Ok(25.0) } } -// ── Production platform — all four buses ───────────────────────── +// ── Продакшен-платформа: все четыре шины ───────────────────────── struct DiagPlatform { spi: LinuxSpi, i2c: LinuxI2c, @@ -1138,17 +1102,17 @@ impl HasI2c for DiagPlatform { type I2c = LinuxI2c; fn i2c(&self) -> &LinuxI impl HasGpio for DiagPlatform { type Gpio = SysfsGpio; fn gpio(&self) -> &SysfsGpio { &self.gpio } } impl HasIpmi for DiagPlatform { type Ipmi = IpmiTool; fn ipmi(&self) -> &IpmiTool { &self.ipmi } } -// DiagPlatform now has ALL mixin methods: +// Теперь DiagPlatform имеет ВСЕ методы примесей: fn production_diagnostics(platform: &DiagPlatform) -> io::Result<()> { - let rpm = platform.read_fan_rpm(0)?; // from FanDiagMixin - let tc = platform.read_thermocouple()?; // from TempMonitorMixin - let ok = platform.enable_power_rail(42)?; // from PowerSeqMixin - println!("Fan: {rpm} RPM, Temp: {tc}°C, Power: {ok}"); + let rpm = platform.read_fan_rpm(0)?; // из FanDiagMixin + let tc = platform.read_thermocouple()?; // из TempMonitorMixin + let ok = platform.enable_power_rail(42)?; // из PowerSeqMixin + println!("Вентилятор: {rpm} об/мин, температура: {tc}°C, питание: {ok}"); Ok(()) } ``` -### Step 5 — Test With Mocks (No Hardware Required) +### Шаг 5: тестирование на моках (аппаратура не нужна) ```rust #[cfg(test)] @@ -1163,7 +1127,7 @@ mod tests { impl SpiBus for MockSpi { fn spi_transfer(&self, _tx: &[u8], rx: &mut [u8]) -> io::Result<()> { - // Encode mock temp as MAX31855 format + // Кодируем температуру мока в формате MAX31855 let raw = ((self.temp.get() / 0.25) as i32) << 18; rx.copy_from_slice(&raw.to_be_bytes()); Ok(()) @@ -1187,14 +1151,14 @@ mod tests { fn read_sensor(&self, _: u8) -> io::Result { Ok(self.sensor_val.get()) } } - // ── Partial platform: only fan-related buses ───────────────── + // ── Частичная платформа: только шины, нужные вентиляторам ───── struct FanTestRig { i2c: MockI2c, gpio: MockGpio, } impl HasI2c for FanTestRig { type I2c = MockI2c; fn i2c(&self) -> &MockI2c { &self.i2c } } impl HasGpio for FanTestRig { type Gpio = MockGpio; fn gpio(&self) -> &MockGpio { &self.gpio } } - // FanTestRig gets FanDiagMixin but NOT TempMonitorMixin or PowerSeqMixin + // FanTestRig получает FanDiagMixin, но НЕ TempMonitorMixin и НЕ PowerSeqMixin #[test] fn fan_health_check_passes_above_threshold() { @@ -1216,35 +1180,32 @@ mod tests { } ``` -Notice that `FanTestRig` only implements `HasI2c + HasGpio` — it gets `FanDiagMixin` -automatically, but the compiler **refuses** `rig.read_thermocouple()` because `HasSpi` -is not satisfied. This is mixin scoping enforced at compile time. +Обратите внимание: `FanTestRig` реализует только `HasI2c + HasGpio`. Он автоматически получает `FanDiagMixin`, но компилятор **не примет** `rig.read_thermocouple()`, потому что `HasSpi` не выполнен. Так область действия примесей обеспечивается на этапе компиляции. -### Conditional Methods — Beyond What Ruby Can Do +### Условные методы: то, чего не умеет Ruby -Add `where` bounds to individual default methods. The method only **exists** when -the associated type satisfies the extra bound: +Добавляйте границы `where` к отдельным методам по умолчанию. Метод **существует** только тогда, когда ассоциированный тип удовлетворяет дополнительному ограничению: ```rust -/// Marker trait for DMA-capable SPI controllers +/// Трейт-маркер для SPI-контроллеров с поддержкой DMA pub trait DmaCapable: SpiBus { fn dma_transfer(&self, tx: &[u8], rx: &mut [u8]) -> io::Result<()>; } -/// Marker trait for interrupt-capable GPIO pins +/// Трейт-маркер для GPIO-линий с поддержкой прерываний pub trait InterruptCapable: GpioPin { fn wait_for_edge(&self, timeout_ms: u32) -> io::Result; } pub trait AdvancedDiagMixin: HasSpi + HasGpio { - // Always available + // Доступен всегда fn basic_probe(&self) -> io::Result { let mut rx = [0u8; 1]; self.spi().spi_transfer(&[0xFF], &mut rx)?; Ok(rx[0] != 0x00) } - // Only exists when the SPI controller supports DMA + // Существует только тогда, когда SPI-контроллер поддерживает DMA fn bulk_sensor_read(&self, buf: &mut [u8]) -> io::Result<()> where Self::Spi: DmaCapable, @@ -1252,7 +1213,7 @@ pub trait AdvancedDiagMixin: HasSpi + HasGpio { self.spi().dma_transfer(&vec![0x00; buf.len()], buf) } - // Only exists when the GPIO pin supports interrupts + // Существует только тогда, когда GPIO-линия поддерживает прерывания fn wait_for_fault_signal(&self, timeout_ms: u32) -> io::Result where Self::Gpio: InterruptCapable, @@ -1264,13 +1225,11 @@ pub trait AdvancedDiagMixin: HasSpi + HasGpio { impl AdvancedDiagMixin for T {} ``` -If your platform's SPI doesn't support DMA, calling `bulk_sensor_read()` is a -**compile error**, not a runtime crash. Ruby's `respond_to?` check is the closest -equivalent — but it happens at deploy time, not compile time. +Если SPI вашей платформы не поддерживает DMA, вызов `bulk_sensor_read()` — это **ошибка компиляции**, а не падение во время выполнения. Ближайший аналог — проверка `respond_to?` в Ruby, но она выполняется во время развёртывания, а не компиляции. -### Composability: Stacking Mixins +### Компонуемость: наложение примесей -Multiple mixins can share the same ingredient — no diamond problem: +Несколько примесей могут использовать один и тот же ингредиент, и ромбовидной проблемы здесь нет: ```text ┌─────────────┐ ┌───────────┐ ┌──────────────┐ @@ -1285,60 +1244,51 @@ Multiple mixins can share the same ingredient — no diamond problem: └───────────────────────────┘ ``` -`DiagPlatform` implements `HasGpio` **once**, and both `FanDiagMixin` and -`PowerSeqMixin` use the same `self.gpio()`. In Ruby, this would be two modules -both calling `self.gpio_pin` — but if they expected different pin numbers, you'd -discover the conflict at runtime. In Rust, you can disambiguate at the type level. - -### Comparison: Ruby Mixins vs Rust Capability Mixins - -| Dimension | Ruby Mixins | Rust Capability Mixins | -|-----------|-------------|------------------------| -| Dispatch | Runtime (method table lookup) | Compile-time (monomorphised) | -| Safe composition | MRO linearisation hides conflicts | Compiler rejects ambiguity | -| Conditional methods | `respond_to?` at runtime | `where` bounds at compile time | -| Overhead | Method dispatch + GC | Zero-cost (inlined) | -| Testability | Stub/mock via metaprogramming | Generic over mock types | -| Adding new buses | `include` at runtime | Add ingredient trait, recompile | -| Runtime flexibility | `extend`, `prepend`, open classes | None (fully static) | - -### When to Use Capability Mixins - -| Scenario | Use Mixins? | -|----------|:-----------:| -| Multiple diagnostics share bus-reading logic | ✅ | -| Test harness needs different bus subsets | ✅ (partial ingredient structs) | -| Methods only valid for certain bus capabilities (DMA, IRQ) | ✅ (conditional `where` bounds) | -| You need runtime module loading (plugins) | ❌ (use `dyn Trait` or enum dispatch) | -| Single struct with one bus — no sharing needed | ❌ (keep it simple) | -| Cross-crate ingredients with coherence issues | ⚠️ (use newtype wrappers) | - -> **Key Takeaways — Capability Mixins** +`DiagPlatform` реализует `HasGpio` **один раз**, а `FanDiagMixin` и `PowerSeqMixin` используют один и тот же `self.gpio()`. В Ruby это были бы два модуля, оба обращающиеся к `self.gpio_pin`. Но если бы они ожидали разные номера пинов, конфликт обнаружился бы только во время выполнения. В Rust неоднозначность можно разрешить на уровне типов. + +### Сравнение: миксины Ruby и возможностные миксины Rust + +| Параметр | Миксины Ruby | Возможностные миксины Rust | +|----------|--------------|----------------------------| +| Диспетчеризация | Во время выполнения (поиск в таблице методов) | На этапе компиляции (мономорфизация) | +| Безопасная композиция | Линеаризация MRO скрывает конфликты | Компилятор отвергает неоднозначность | +| Условные методы | `respond_to?` во время выполнения | Границы `where` на этапе компиляции | +| Накладные расходы | Диспетчеризация методов и сборка мусора | Без накладных расходов (инлайнится) | +| Тестируемость | Заглушки и моки через метапрограммирование | Обобщения по типам-моков | +| Добавление новых шин | `include` во время выполнения | Добавить трейт-ингредиент и перекомпилировать | +| Гибкость во время выполнения | `extend`, `prepend`, открытые классы | Нет (полностью статично) | + +### Когда использовать возможностные миксины + +| Сценарий | Использовать миксины? | +|----------|:---------------------:| +| Несколько диагностик используют общую логику чтения шин | ✅ | +| Тестовому стенду нужны разные подмножества шин | ✅ (структуры с частичным набором ингредиентов) | +| Методы допустимы только для определённых возможностей шин (DMA, IRQ) | ✅ (условные границы `where`) | +| Нужна загрузка модулей во время выполнения (плагины) | ❌ (используйте `dyn Trait` или диспетчеризацию через перечисление) | +| Одна структура с одной шиной, разделять нечего | ❌ (оставьте всё простым) | +| Ингредиенты из разных крейтов с проблемами когерентности | ⚠️ (используйте обёртки newtype) | + +> **Ключевые выводы: возможностные миксины** > -> 1. **Ingredient trait** = associated type + accessor method (e.g., `HasSpi`) -> 2. **Mixin trait** = supertrait bounds on ingredients + default method bodies -> 3. **Blanket impl** = `impl Mixin for T {}` — auto-injects methods -> 4. **Conditional methods** = `where Self::Spi: DmaCapable` on individual defaults -> 5. **Partial platforms** = test structs that only impl the needed ingredients -> 6. **No runtime cost** — the compiler generates specialised code for each platform type +> 1. **Трейт-ингредиент** = ассоциированный тип + метод доступа (например, `HasSpi`) +> 2. **Трейт-примесь** = супертрейты-ингредиенты + тела методов по умолчанию +> 3. **Blanket-реализация** = `impl Mixin for T {}`: автоматически добавляет методы +> 4. **Условные методы** = `where Self::Spi: DmaCapable` у отдельных методов по умолчанию +> 5. **Частичные платформы** = тестовые структуры, которые реализуют только нужные ингредиенты +> 6. **Без затрат во время выполнения**: компилятор генерирует специализированный код для каждого типа платформы *** -## Typed Commands — GADT-Style Return Type Safety +## Типизированные команды: безопасность возвращаемых типов в стиле GADT -In Haskell, **Generalised Algebraic Data Types (GADTs)** let each constructor of a -data type refine the type parameter — so `Expr Int` and `Expr Bool` are enforced by -the type checker. Rust has no direct GADT syntax, but **traits with associated types** -achieve the same guarantee: the command type **determines** the response type, and -mixing them up is a compile error. +В Haskell **обобщённые алгебраические типы данных (GADT)** позволяют каждому конструктору типа уточнять параметр типа, поэтому `Expr Int` и `Expr Bool` различает проверка типов. В Rust нет прямого синтаксиса GADT, но **трейты с ассоциированными типами** дают ту же гарантию: тип команды **определяет** тип ответа, а перепутать их — ошибка компиляции. -This pattern is particularly powerful for hardware diagnostics, where IPMI commands, -register reads, and sensor queries each return different physical quantities that -should never be confused. +Этот паттерн особенно полезен в аппаратной диагностике: команды IPMI, чтение регистров и запросы к датчикам возвращают разные физические величины, которые нельзя путать. -### The Problem: The Untyped `Vec` Swamp +### Проблема: болото из нетипизированных `Vec` -Most C/C++ IPMI stacks — and naïve Rust ports — use raw bytes everywhere: +Большинство стеков IPMI на C/C++ и наивные порты на Rust используют сырые байты повсюду: ```rust use std::io; @@ -1347,53 +1297,52 @@ struct BmcConnectionUntyped { timeout_secs: u32 } impl BmcConnectionUntyped { fn raw_command(&self, net_fn: u8, cmd: u8, data: &[u8]) -> io::Result> { - // ... shells out to ipmitool ... - Ok(vec![0x00, 0x19, 0x00]) // stub + // ... вызывает ipmitool ... + Ok(vec![0x00, 0x19, 0x00]) // заглушка } } fn diagnose_thermal_untyped(bmc: &BmcConnectionUntyped) -> io::Result<()> { - // Read CPU temperature — sensor ID 0x20 + // Читаем температуру CPU — ID датчика 0x20 let raw = bmc.raw_command(0x04, 0x2D, &[0x20])?; - let cpu_temp = raw[0] as f64; // 🤞 hope byte 0 is the reading + let cpu_temp = raw[0] as f64; // 🤞 надеемся, что байт 0 — это показание - // Read fan speed — sensor ID 0x30 + // Читаем скорость вентилятора — ID датчика 0x30 let raw = bmc.raw_command(0x04, 0x2D, &[0x30])?; - let fan_rpm = raw[0] as u32; // 🐛 BUG: fan speed is 2 bytes LE + let fan_rpm = raw[0] as u32; // 🐛 ОШИБКА: скорость вентилятора — 2 байта, little-endian - // Read inlet voltage — sensor ID 0x40 + // Читаем входное напряжение — ID датчика 0x40 let raw = bmc.raw_command(0x04, 0x2D, &[0x40])?; - let voltage = raw[0] as f64; // 🐛 BUG: need to divide by 1000 + let voltage = raw[0] as f64; // 🐛 ОШИБКА: нужно делить на 1000 - // 🐛 Comparing °C to RPM — compiles, but nonsensical + // 🐛 Сравнение °C с об/мин — компилируется, но бессмысленно if cpu_temp > fan_rpm as f64 { - println!("uh oh"); + println!("Ой-ой"); } - // 🐛 Passing Volts as temperature — compiles fine + // 🐛 Передача вольт как температуры — компилируется без проблем log_temp_untyped(voltage); log_volts_untyped(cpu_temp); Ok(()) } -fn log_temp_untyped(t: f64) { println!("Temp: {t}°C"); } -fn log_volts_untyped(v: f64) { println!("Voltage: {v}V"); } +fn log_temp_untyped(t: f64) { println!("Температура: {t}°C"); } +fn log_volts_untyped(v: f64) { println!("Напряжение: {v}В"); } ``` -**Every reading is `f64`** — the compiler has no idea that one is a temperature, another -is RPM, another is voltage. Four distinct bugs compile without warning: +**Каждое показание — это `f64`**: компилятор не знает, что одно из них — температура, другое — обороты, а третье — напряжение. Четыре разные ошибки компилируются без единого предупреждения: -| # | Bug | Consequence | Discovered | -|---|-----|-------------|------------| -| 1 | Fan RPM parsed as 1 byte instead of 2 | Reads 25 RPM instead of 6400 | Production, 3 AM fan-failure flood | -| 2 | Voltage not divided by 1000 | 12000V instead of 12.0V | Threshold check flags every PSU | -| 3 | Comparing °C to RPM | Meaningless boolean | Possibly never | -| 4 | Voltage passed to `log_temp_untyped()` | Silent data corruption in logs | 6 months later, reading history | +| № | Ошибка | Последствие | Обнаружена | +|---|--------|-------------|------------| +| 1 | Обороты вентилятора разбираются как 1 байт вместо 2 | Читается 25 об/мин вместо 6400 | В продакшене, в 3 часа ночи, потоком сообщений об отказе вентиляторов | +| 2 | Напряжение не поделено на 1000 | 12000 В вместо 12,0 В | Проверка порога помечает каждый блок питания | +| 3 | Сравнение °C с об/мин | Бессмысленное логическое значение | Возможно, никогда | +| 4 | Напряжение передано в `log_temp_untyped()` | Тихое повреждение данных в логах | Через 6 месяцев, при чтении истории | -### The Solution: Typed Commands via Associated Types +### Решение: типизированные команды через ассоциированные типы -#### Step 1 — Domain newtypes +#### Шаг 1: доменные newtype ```rust #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] @@ -1409,59 +1358,59 @@ struct Volts(f64); struct Watts(f64); ``` -#### Step 2 — The command trait (the GADT equivalent) +#### Шаг 2: трейт команды (аналог GADT) -The associated type `Response` is the key — it binds each command to its return type: +Ключевой элемент здесь — ассоциированный тип `Response`: он связывает каждую команду с её типом результата. ```rust trait IpmiCmd { - /// The GADT "index" — determines what execute() returns. + /// «Индекс» GADT: определяет, что возвращает execute(). type Response; fn net_fn(&self) -> u8; fn cmd_byte(&self) -> u8; fn payload(&self) -> Vec; - /// Parsing is encapsulated HERE — each command knows its own byte layout. + /// Разбор ответа инкапсулирован ЗДЕСЬ: каждая команда знает свою раскладку байтов. fn parse_response(&self, raw: &[u8]) -> io::Result; } ``` -#### Step 3 — One struct per command, parsing written once +#### Шаг 3: одна структура на команду, разбор записан один раз ```rust struct ReadTemp { sensor_id: u8 } impl IpmiCmd for ReadTemp { - type Response = Celsius; // ← "this command returns a temperature" + type Response = Celsius; // ← «эта команда возвращает температуру» fn net_fn(&self) -> u8 { 0x04 } fn cmd_byte(&self) -> u8 { 0x2D } fn payload(&self) -> Vec { vec![self.sensor_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { - // Signed byte per IPMI SDR — written once, tested once + // Знаковый байт по IPMI SDR: написан один раз, протестирован один раз Ok(Celsius(raw[0] as i8 as f64)) } } struct ReadFanSpeed { fan_id: u8 } impl IpmiCmd for ReadFanSpeed { - type Response = Rpm; // ← "this command returns RPM" + type Response = Rpm; // ← «эта команда возвращает обороты» fn net_fn(&self) -> u8 { 0x04 } fn cmd_byte(&self) -> u8 { 0x2D } fn payload(&self) -> Vec { vec![self.fan_id] } fn parse_response(&self, raw: &[u8]) -> io::Result { - // 2-byte LE — the correct layout, encoded once + // 2 байта, little-endian: правильная раскладка, записана один раз Ok(Rpm(u16::from_le_bytes([raw[0], raw[1]]) as u32)) } } struct ReadVoltage { rail: u8 } impl IpmiCmd for ReadVoltage { - type Response = Volts; // ← "this command returns voltage" + type Response = Volts; // ← «эта команда возвращает напряжение» fn net_fn(&self) -> u8 { 0x04 } fn cmd_byte(&self) -> u8 { 0x2D } fn payload(&self) -> Vec { vec![self.rail] } fn parse_response(&self, raw: &[u8]) -> io::Result { - // Millivolts → Volts, always correct + // Милливольты → вольты, всегда корректно Ok(Volts(u16::from_le_bytes([raw[0], raw[1]]) as f64 / 1000.0)) } } @@ -1478,25 +1427,25 @@ impl IpmiCmd for ReadFru { } ``` -#### Step 4 — The executor (zero `dyn`, monomorphised) +#### Шаг 4: исполнитель (без `dyn`, мономорфизация) ```rust struct BmcConnection { timeout_secs: u32 } impl BmcConnection { - /// Generic over any command — compiler generates one version per command type. + /// Обобщённая по любой команде: компилятор создаёт одну версию на каждый тип команды. fn execute(&self, cmd: &C) -> io::Result { let raw = self.raw_send(cmd.net_fn(), cmd.cmd_byte(), &cmd.payload())?; cmd.parse_response(&raw) } fn raw_send(&self, _nf: u8, _cmd: u8, _data: &[u8]) -> io::Result> { - Ok(vec![0x19, 0x00]) // stub — real impl calls ipmitool + Ok(vec![0x19, 0x00]) // заглушка: настоящая реализация вызывает ipmitool } } ``` -#### Step 5 — Caller code: all four bugs become compile errors +#### Шаг 5: код вызывающей стороны — все четыре ошибки становятся ошибками компиляции ```rust fn diagnose_thermal(bmc: &BmcConnection) -> io::Result<()> { @@ -1504,41 +1453,40 @@ fn diagnose_thermal(bmc: &BmcConnection) -> io::Result<()> { let fan_rpm: Rpm = bmc.execute(&ReadFanSpeed { fan_id: 0x30 })?; let voltage: Volts = bmc.execute(&ReadVoltage { rail: 0x40 })?; - // Bug #1 — IMPOSSIBLE: parsing lives in ReadFanSpeed::parse_response - // Bug #2 — IMPOSSIBLE: scaling lives in ReadVoltage::parse_response + // Ошибка №1 — НЕВОЗМОЖНА: разбор находится в ReadFanSpeed::parse_response + // Ошибка №2 — НЕВОЗМОЖНА: масштабирование находится в ReadVoltage::parse_response - // Bug #3 — COMPILE ERROR: + // Ошибка №3 — ОШИБКА КОМПИЛЯЦИИ: // if cpu_temp > fan_rpm { } // ^^^^^^^^ ^^^^^^^ // Celsius Rpm → "mismatched types" ❌ - // Bug #4 — COMPILE ERROR: + // Ошибка №4 — ОШИБКА КОМПИЛЯЦИИ: // log_temperature(voltage); // ^^^^^^^ Volts, expected Celsius ❌ - // Only correct comparisons compile: + // Компилируются только корректные сравнения: if cpu_temp > Celsius(85.0) { - println!("CPU overheating: {:?}", cpu_temp); + println!("ЦП перегревается: {:?}", cpu_temp); } if fan_rpm < Rpm(4000) { - println!("Fan too slow: {:?}", fan_rpm); + println!("Вентилятор слишком медленный: {:?}", fan_rpm); } Ok(()) } -fn log_temperature(t: Celsius) { println!("Temp: {:?}", t); } -fn log_voltage(v: Volts) { println!("Voltage: {:?}", v); } +fn log_temperature(t: Celsius) { println!("Температура: {:?}", t); } +fn log_voltage(v: Volts) { println!("Напряжение: {:?}", v); } ``` -### Macro DSL for Diagnostic Scripts +### Макро-DSL для диагностических сценариев -For large diagnostic routines that run many commands in sequence, a macro gives -concise declarative syntax while preserving full type safety: +Для больших диагностических процедур, которые выполняют много команд подряд, макрос даёт краткий декларативный синтаксис и при этом сохраняет полную типобезопасность: ```rust -/// Execute a series of typed IPMI commands, returning a tuple of results. -/// Each element of the tuple has the command's own Response type. +/// Выполняет последовательность типизированных команд IPMI и возвращает кортеж результатов. +/// Каждый элемент кортежа имеет собственный тип Response команды. macro_rules! diag_script { ($bmc:expr; $($cmd:expr),+ $(,)?) => {{ ( $( $bmc.execute(&$cmd)?, )+ ) @@ -1546,7 +1494,7 @@ macro_rules! diag_script { } fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> { - // Expands to: (Celsius, Rpm, Volts, String) — every type tracked + // Раскрывается в: (Celsius, Rpm, Volts, String). Каждый тип отслеживается let (temp, rpm, volts, board_pn) = diag_script!(bmc; ReadTemp { sensor_id: 0x20 }, ReadFanSpeed { fan_id: 0x30 }, @@ -1554,26 +1502,23 @@ fn full_pre_flight(bmc: &BmcConnection) -> io::Result<()> { ReadFru { fru_id: 0x00 }, ); - println!("Board: {:?}", board_pn); - println!("CPU: {:?}, Fan: {:?}, 12V: {:?}", temp, rpm, volts); + println!("Плата: {:?}", board_pn); + println!("ЦП: {:?}, вентилятор: {:?}, 12 В: {:?}", temp, rpm, volts); - // Type-safe threshold checks: - assert!(temp < Celsius(95.0), "CPU too hot"); - assert!(rpm > Rpm(3000), "Fan too slow"); - assert!(volts > Volts(11.4), "12V rail sagging"); + // Проверки порогов с учётом типов: + assert!(temp < Celsius(95.0), "ЦП слишком горячий"); + assert!(rpm > Rpm(3000), "Вентилятор слишком медленный"); + assert!(volts > Volts(11.4), "Линия 12 В просела"); Ok(()) } ``` -The macro is just syntactic sugar — the tuple type `(Celsius, Rpm, Volts, String)` is -fully inferred by the compiler. Swap two commands and the destructuring breaks at -compile time, not at runtime. +Макрос — это всего лишь синтаксический сахар: тип кортежа `(Celsius, Rpm, Volts, String)` полностью выводится компилятором. Поменяйте местами две команды, и деструктуризация сломается на этапе компиляции, а не во время выполнения. -### Enum Dispatch for Heterogeneous Command Lists +### Диспетчеризация через перечисление для разнородных списков команд -When you need a `Vec` of mixed commands (e.g., a configurable script loaded from JSON), -use enum dispatch to stay `dyn`-free: +Когда нужен `Vec` из команд разных типов (например, настраиваемый сценарий, загруженный из JSON), используйте диспетчеризацию через перечисление, чтобы обойтись без `dyn`: ```rust enum AnyReading { @@ -1601,16 +1546,15 @@ impl AnyCmd { } } -/// Dynamic diagnostic script — commands loaded at runtime +/// Динамический диагностический сценарий: команды загружаются во время выполнения fn run_script(bmc: &BmcConnection, script: &[AnyCmd]) -> io::Result> { script.iter().map(|cmd| cmd.execute(bmc)).collect() } ``` -You lose per-element type tracking (everything is `AnyReading`), but you gain -runtime flexibility — and the parsing is still encapsulated in each `IpmiCmd` impl. +Вы теряете отслеживание типа для каждого отдельного элемента (всё становится `AnyReading`), но получаете гибкость во время выполнения, а разбор по-прежнему инкапсулирован в каждой реализации `IpmiCmd`. -### Testing Typed Commands +### Тестирование типизированных команд ```rust #[cfg(test)] @@ -1623,9 +1567,9 @@ mod tests { impl StubBmc { fn execute(&self, cmd: &C) -> io::Result { - let key = cmd.payload()[0]; // sensor ID as key + let key = cmd.payload()[0]; // ID датчика как ключ let raw = self.responses.get(&key) - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no stub"))?; + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "нет заглушки"))?; cmd.parse_response(raw) } } @@ -1633,7 +1577,7 @@ mod tests { #[test] fn read_temp_parses_signed_byte() { let bmc = StubBmc { - responses: [( 0x20, vec![0xE7] )].into() // -25 as i8 = 0xE7 + responses: [( 0x20, vec![0xE7] )].into() // -25 как i8 = 0xE7 }; let temp = bmc.execute(&ReadTemp { sensor_id: 0x20 }).unwrap(); assert_eq!(temp, Celsius(-25.0)); @@ -1651,7 +1595,7 @@ mod tests { #[test] fn read_voltage_scales_millivolts() { let bmc = StubBmc { - responses: [( 0x40, vec![0xE8, 0x2E] )].into() // 0x2EE8 = 12008 mV + responses: [( 0x40, vec![0xE8, 0x2E] )].into() // 0x2EE8 = 12008 мВ }; let v = bmc.execute(&ReadVoltage { rail: 0x40 }).unwrap(); assert!((v.0 - 12.008).abs() < 0.001); @@ -1659,14 +1603,12 @@ mod tests { } ``` -Each command's parsing is tested independently. If `ReadFanSpeed` changes from 2-byte -LE to 4-byte BE in a new IPMI spec revision, you update **one** `parse_response` and -the test catches regressions. +Разбор каждой команды тестируется независимо. Если `ReadFanSpeed` в новой редакции спецификации IPMI перейдёт с 2-байтового little-endian на 4-байтовый big-endian, вы обновите **один** `parse_response`, а тест поймает регрессии. -### How This Maps to Haskell GADTs +### Как это соотносится с GADT в Haskell ```text -Haskell GADT Rust Equivalent +GADT в Haskell Аналог в Rust ──────────────── ─────────────────────── data Cmd a where trait IpmiCmd { ReadTemp :: SensorId -> Cmd Temp type Response; @@ -1676,56 +1618,54 @@ data Cmd a where trait IpmiCmd { eval :: Cmd a -> IO a fn execute(&self, cmd: &C) -> io::Result -Type refinement in case branches Monomorphisation: compiler generates - execute::() → returns Celsius - execute::() → returns Rpm +Уточнение типа в ветках case Мономорфизация: компилятор генерирует + execute::() → возвращает Celsius + execute::() → возвращает Rpm ``` -Both guarantee: **the command determines the return type**. Rust achieves it through -generic monomorphisation instead of type-level case analysis — same safety, zero -runtime cost. - -### Before vs After Summary - -| Dimension | Untyped (`Vec`) | Typed Commands | -|-----------|:---:|:---:| -| Lines per sensor | ~3 (duplicated at every call site) | ~15 (written and tested once) | -| Parsing errors possible | At every call site | In one `parse_response` impl | -| Unit confusion bugs | Unlimited | Zero (compile error) | -| Adding a new sensor | Touch N files, copy-paste parsing | Add 1 struct + 1 impl | -| Runtime cost | — | Identical (monomorphised) | -| IDE autocomplete | `f64` everywhere | `Celsius`, `Rpm`, `Volts` — self-documenting | -| Code review burden | Must verify every raw byte parse | Verify one `parse_response` per sensor | -| Macro DSL | N/A | `diag_script!(bmc; ReadTemp{..}, ReadFan{..})` → `(Celsius, Rpm)` | -| Dynamic scripts | Manual dispatch | `AnyCmd` enum — still `dyn`-free | - -### When to Use Typed Commands - -| Scenario | Recommendation | -|----------|:--------------:| -| IPMI sensor reads with distinct physical units | ✅ Typed commands | -| Register map with different-width fields | ✅ Typed commands | -| Network protocol messages (request → response) | ✅ Typed commands | -| Single command type with one return format | ❌ Overkill — just return the type directly | -| Prototyping / exploring an unknown device | ❌ Raw bytes first, type later | -| Plugin system where commands aren't known at compile time | ⚠️ Use `AnyCmd` enum dispatch | - -> **Key Takeaways — Traits** -> - Associated types = one impl per type; generic parameters = many impls per type -> - GATs unlock lending iterators and async-in-traits patterns -> - Use enum dispatch for closed sets (fast); `dyn Trait` for open sets (flexible) -> - `Any` + `TypeId` is the escape hatch when compile-time types are unknown - -> **See also:** [Ch 1 — Generics](ch01-generics-the-full-picture.md) for monomorphization and when generics cause code bloat. [Ch 3 — Newtype & Type-State](ch03-the-newtype-and-type-state-patterns.md) for using traits with the config trait pattern. +Оба варианта гарантируют: **команда определяет тип результата**. Rust достигает этого через обобщённую мономорфизацию, а не через анализ случаев на уровне типов. Безопасность та же, а стоимость во время выполнения нулевая. + +### Сводка: до и после + +| Параметр | Нетипизированный (`Vec`) | Типизированные команды | +|----------|:---:|:---:| +| Строк на датчик | ~3 (дублируются в каждом месте вызова) | ~15 (написаны и протестированы один раз) | +| Возможные ошибки разбора | В каждом месте вызова | В одной реализации `parse_response` | +| Ошибки путаницы единиц | Без ограничений | Нет (ошибка компиляции) | +| Добавление нового датчика | Правка N файлов, копирование разбора | Добавить 1 структуру и 1 реализацию | +| Затраты во время выполнения | — | Такие же (мономорфизация) | +| Автодополнение в IDE | `f64` везде | `Celsius`, `Rpm`, `Volts`: код документирует себя | +| Нагрузка на код-ревью | Нужно проверить разбор каждого сырого байта | Проверить по одной `parse_response` на датчик | +| Макро-DSL | Неприменимо | `diag_script!(bmc; ReadTemp{..}, ReadFan{..})` → `(Celsius, Rpm)` | +| Динамические сценарии | Ручная диспетчеризация | Перечисление `AnyCmd`, по-прежнему без `dyn` | + +### Когда использовать типизированные команды + +| Сценарий | Рекомендация | +|----------|:------------:| +| Чтение датчиков IPMI с разными физическими единицами | ✅ Типизированные команды | +| Карта регистров с полями разной ширины | ✅ Типизированные команды | +| Сетевые сообщения протокола (запрос → ответ) | ✅ Типизированные команды | +| Один тип команды с одним форматом ответа | ❌ Избыточно: верните тип напрямую | +| Прототипирование или изучение неизвестного устройства | ❌ Сначала сырые байты, типы позже | +| Система плагинов, где команды неизвестны на этапе компиляции | ⚠️ Используйте диспетчеризацию через перечисление `AnyCmd` | + +> **Ключевые выводы: трейты** +> - Ассоциированные типы = одна реализация на тип; обобщённые параметры = много реализаций на тип +> - GAT открывают путь к lending-итераторам и паттернам async в трейтах +> - Для закрытых множеств используйте диспетчеризацию через перечисление (быстро), для открытых: `dyn Trait` (гибко) +> - `Any` + `TypeId` — запасной выход, когда типы неизвестны на этапе компиляции + +> **См. также:** [гл. 1 — Обобщённые типы](ch01-generics-the-full-picture.md) — мономорфизация и случаи, когда обобщения раздувают код. [гл. 3 — Newtype и type-state](ch03-the-newtype-and-type-state-patterns.md) — использование трейтов в паттерне конфигурационного трейта. --- -### Exercise: Repository with Associated Types ★★★ (~40 min) +### Упражнение: репозиторий с ассоциированными типами ★★★ (~40 минут) -Design a `Repository` trait with associated `Error`, `Id`, and `Item` types. Implement it for an in-memory store and demonstrate compile-time type safety. +Спроектируйте трейт `Repository` с ассоциированными типами `Error`, `Id` и `Item`. Реализуйте его для хранилища в памяти и продемонстрируйте безопасность типов на этапе компиляции.
-🔑 Solution +🔑 Решение ```rust use std::collections::HashMap; @@ -1784,9 +1724,9 @@ where R::Id: std::fmt::Debug, { let id = repo.insert(item)?; - println!("Inserted with id: {id:?}"); + println!("Вставлено с id: {id:?}"); let retrieved = repo.get(&id)?; - println!("Retrieved: {retrieved:?}"); + println!("Получено: {retrieved:?}"); Ok(()) } @@ -1802,4 +1742,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch03-the-newtype-and-type-state-patterns.md b/rust-patterns-book/src/ch03-the-newtype-and-type-state-patterns.md index 9c35585..4f45a07 100644 --- a/rust-patterns-book/src/ch03-the-newtype-and-type-state-patterns.md +++ b/rust-patterns-book/src/ch03-the-newtype-and-type-state-patterns.md @@ -1,22 +1,22 @@ -# 3. The Newtype and Type-State Patterns 🟡 +# 3. Паттерны newtype и type-state 🟡 -> **What you'll learn:** -> - The newtype pattern for zero-cost compile-time type safety -> - Type-state pattern: making illegal state transitions unrepresentable -> - Builder pattern with type states for compile-time–enforced construction -> - Config trait pattern for taming generic parameter explosion +> **Что вы узнаете:** +> - Паттерн newtype для безопасности типов на этапе компиляции без накладных расходов +> - Паттерн type-state: как сделать недопустимые переходы между состояниями невыразимыми +> - Builder-паттерн с type-state для конструирования, которое проверяет компилятор +> - Паттерн конфигурационного трейта для борьбы с разрастанием обобщённых параметров -## Newtype: Zero-Cost Type Safety +## Newtype: безопасность типов без накладных расходов -The newtype pattern wraps an existing type in a single-field tuple struct to create a distinct type with zero runtime overhead: +Паттерн newtype оборачивает существующий тип в кортежную структуру с одним полем, создавая отдельный тип без накладных расходов во время выполнения: ```rust -// Without newtypes — easy to mix up: +// Без newtype легко перепутать аргументы: fn create_user(name: String, email: String, age: u32, employee_id: u32) { } -// create_user(name, email, age, id); — but what if we swap age and id? -// create_user(name, email, id, age); — COMPILES FINE, BUG +// create_user(name, email, age, id); — а что, если поменять местами age и id? +// create_user(name, email, id, age); — КОМПИЛИРУЕТСЯ БЕЗ ОШИБОК, НО ЭТО БАГ -// With newtypes — the compiler catches mistakes: +// С newtype компилятор ловит ошибки: struct UserName(String); struct Email(String); struct Age(u32); @@ -27,10 +27,9 @@ fn create_user(name: UserName, email: Email, age: Age, id: EmployeeId) { } // ❌ Compile error: expected Age, got EmployeeId ``` -### `impl Deref` for Newtypes — Power and Pitfalls +### `impl Deref` для newtype: возможности и подводные камни -Implementing `Deref` on a newtype lets it auto-coerce to the inner type's -reference, giving you all of the inner type's methods "for free": +Реализация `Deref` для newtype позволяет автоматически приводить его к ссылке на внутренний тип и получать все методы внутреннего типа «бесплатно»: ```rust use std::ops::Deref; @@ -42,7 +41,7 @@ impl Email { if raw.contains('@') { Ok(Email(raw.to_string())) } else { - Err("invalid email: missing @") + Err("некорректный email: нет символа @") } } } @@ -52,40 +51,34 @@ impl Deref for Email { fn deref(&self) -> &str { &self.0 } } -// Now Email auto-derefs to &str: +// Теперь Email автоматически разыменовывается в &str: let email = Email::new("user@example.com").unwrap(); -println!("Length: {}", email.len()); // Uses str::len via Deref +println!("Длина: {}", email.len()); // Использует str::len через Deref ``` -This is convenient — but it effectively **punches a hole** through your -newtype's abstraction boundary because *every* method on the target type -becomes callable on your wrapper. +Это удобно, но фактически **пробивает брешь** в границе абстракции вашего newtype: каждый метод целевого типа становится доступен и у вашей обёртки. -#### When `Deref` IS appropriate +#### Когда `Deref` УМЕСТЕН -| Scenario | Example | Why it's fine | -|----------|---------|---------------| -| Smart-pointer wrappers | `Box`, `Arc`, `MutexGuard` | The wrapper's whole purpose is to behave like `T` | -| Transparent "thin" wrappers | `String` → `str`, `PathBuf` → `Path`, `Vec` → `[T]` | The wrapper IS-A superset of the target | -| Your newtype genuinely IS the inner type | `struct Hostname(String)` where you always want full string ops | Restricting the API would add no value | +| Сценарий | Пример | Почему это нормально | +|----------|--------|----------------------| +| Обёртки-умные указатели | `Box`, `Arc`, `MutexGuard` | Единственная задача обёртки — вести себя как `T` | +| Прозрачные «тонкие» обёртки | `String` → `str`, `PathBuf` → `Path`, `Vec` → `[T]` | Обёртка является расширенной версией целевого типа | +| Ваш newtype по сути и есть внутренний тип | `struct Hostname(String)`, где всегда нужны все строковые операции | Ограничение API не принесёт пользы | -#### When `Deref` is an anti-pattern +#### Когда `Deref` — антипаттерн -| Scenario | Problem | -|----------|---------| -| **Domain types with invariants** | `Email` derefs to `&str`, so callers can call `.split_at()`, `.trim()`, etc. — none of which preserve the "must contain @" invariant. If someone stores the trimmed `&str` and reconstructs, the invariant is lost. | -| **Types where you want a restricted API** | `struct Password(String)` with `Deref` leaks `.as_bytes()`, `.chars()`, `Debug` output — exactly what you're trying to hide. | -| **Fake inheritance** | Using `Deref` to make `ManagerWidget` auto-deref to `Widget` simulates OOP inheritance. This is explicitly discouraged — see the Rust API Guidelines (C-DEREF). | +| Сценарий | Проблема | +|----------|----------| +| **Доменные типы с инвариантами** | `Email` разыменовывается в `&str`, поэтому вызывающий код может вызвать `.split_at()`, `.trim()` и т. д. Ни один из них не сохраняет инвариант «должен содержать @». Если кто-то сохранит обрезанный `&str` и соберёт объект заново, инвариант будет потерян. | +| **Типы, для которых нужен ограниченный API** | `struct Password(String)` с `Deref` открывает `.as_bytes()`, `.chars()` и вывод `Debug`: ровно то, что вы пытаетесь скрыть. | +| **Имитация наследования** | Использование `Deref`, чтобы `ManagerWidget` автоматически разыменовывался в `Widget`, имитирует наследование из ООП. Это прямо не рекомендуется: см. Rust API Guidelines (C-DEREF). | -> **Rule of thumb**: If your newtype exists to *add type safety* or *restrict -> the API*, don't implement `Deref`. If it exists to *add capabilities* while -> keeping the inner type's full surface (like a smart pointer), `Deref` is -> the right choice. +> **Практическое правило**: если ваш newtype существует, чтобы *добавить безопасность типов* или *ограничить API*, не реализуйте `Deref`. Если же он нужен, чтобы *добавить возможности*, сохранив полный интерфейс внутреннего типа (как умный указатель), `Deref` будет правильным выбором. -#### `DerefMut` — doubles the risk +#### `DerefMut` удваивает риск -If you also implement `DerefMut`, callers can *mutate* the inner value -directly, bypassing any validation in your constructors: +Если вы также реализуете `DerefMut`, вызывающий код может *изменять* внутреннее значение напрямую, минуя любые проверки в конструкторах: ```rust use std::ops::{Deref, DerefMut}; @@ -102,14 +95,14 @@ impl DerefMut for PortNumber { } let mut port = PortNumber(443); -*port = 0; // Bypasses any validation — now an invalid port +*port = 0; // Обходит любую проверку — теперь порт некорректен ``` -Only implement `DerefMut` when the inner type has no invariants to protect. +Реализуйте `DerefMut` только тогда, когда у внутреннего типа нет инвариантов, которые нужно защищать. -#### Prefer explicit delegation instead +#### Лучше использовать явное делегирование -When you want only *some* of the inner type's methods, delegate explicitly: +Если нужны лишь *некоторые* методы внутреннего типа, делегируйте их явно: ```rust struct Email(String); @@ -117,44 +110,38 @@ struct Email(String); impl Email { fn new(raw: &str) -> Result { if raw.contains('@') { Ok(Email(raw.to_string())) } - else { Err("missing @") } + else { Err("нет символа @") } } - // Expose only what makes sense: + // Открываем только то, что имеет смысл: pub fn as_str(&self) -> &str { &self.0 } pub fn len(&self) -> usize { self.0.len() } pub fn domain(&self) -> &str { self.0.split('@').nth(1).unwrap_or("") } - // .split_at(), .trim(), .replace() — NOT exposed + // .split_at(), .trim(), .replace() — НЕ открыты } ``` -#### Clippy and the ecosystem +#### Clippy и экосистема -- **`clippy::wrong_self_convention`** can fire when `Deref` coercion - makes method resolution surprising (e.g., `is_empty()` resolving to the - inner type's version instead of one you intended to shadow). -- The **Rust API Guidelines** (C-DEREF) state: *"only smart pointers - should implement `Deref`."* Treat this as a strong default; deviate - only with clear justification. -- If you need trait compatibility (e.g., passing `Email` to functions - expecting `&str`), consider implementing `AsRef` and `Borrow` - instead — they're explicit conversions without auto-coercion surprises. +- **`clippy::wrong_self_convention`** может сработать, когда приведение через `Deref` делает разрешение методов неожиданным (например, `is_empty()` разрешается в версию внутреннего типа, а не в ту, которую вы хотели переопределить). +- **Rust API Guidelines** (C-DEREF) утверждают: *«только умные указатели должны реализовывать `Deref`»*. Считайте это сильным правилом по умолчанию и отступайте от него, только имея веское обоснование. +- Если нужна совместимость с трейтами (например, передать `Email` в функции, которые ожидают `&str`), рассмотрите реализацию `AsRef` и `Borrow`. Это явные преобразования без сюрпризов автоматического приведения. -#### Decision matrix +#### Матрица решений ```text -Do you want ALL methods of the inner type to be callable? - ├─ YES → Does your type enforce invariants or restrict the API? - │ ├─ NO → impl Deref ✅ (smart-pointer / transparent wrapper) - │ └─ YES → Don't impl Deref ❌ (invariant leaks) - └─ NO → Don't impl Deref ❌ (use AsRef / explicit delegation) +Нужны ли ВСЕ методы внутреннего типа? + ├─ ДА → Использует ли ваш тип инварианты или ограничивает API? + │ ├─ НЕТ → impl Deref ✅ (умный указатель / прозрачная обёртка) + │ └─ ДА → Не реализуйте Deref ❌ (инвариант протекает) + └─ НЕТ → Не реализуйте Deref ❌ (используйте AsRef / явное делегирование) ``` -### Type-State: Compile-Time Protocol Enforcement +### Type-State: принуждение к протоколу на этапе компиляции -The type-state pattern uses the type system to enforce that operations happen in the correct order. Invalid states become **unrepresentable**. +Паттерн type-state использует систему типов, чтобы гарантировать, что операции выполняются в правильном порядке. Недопустимые состояния становятся **невыразимыми**. ```mermaid stateDiagram-v2 @@ -164,32 +151,32 @@ stateDiagram-v2 Authenticated --> Authenticated: request() Authenticated --> [*]: drop - Disconnected --> Disconnected: ❌ request() won't compile - Connected --> Connected: ❌ request() won't compile + Disconnected --> Disconnected: ❌ request() не скомпилируется + Connected --> Connected: ❌ request() не скомпилируется ``` -> Each transition *consumes* `self` and returns a new type — the compiler enforces valid ordering. +> Каждый переход *поглощает* `self` и возвращает новый тип. Компилятор следит за правильным порядком. ```rust -// Problem: A network connection that must be: -// 1. Created -// 2. Connected -// 3. Authenticated -// 4. Then used for requests -// Calling request() before authenticate() should be a COMPILE error. - -// --- Type-state markers (zero-sized types) --- +// Проблема: сетевое соединение должно пройти шаги: +// 1. Создание +// 2. Подключение +// 3. Аутентификация +// 4. И только потом использование для запросов +// Вызов request() до authenticate() должен быть ОШИБКОЙ КОМПИЛЯЦИИ. + +// --- Маркеры состояний (типы нулевого размера) --- struct Disconnected; struct Connected; struct Authenticated; -// --- Connection parameterized by state --- +// --- Соединение, параметризованное состоянием --- struct Connection { address: String, _state: std::marker::PhantomData, } -// Only Disconnected connections can connect: +// Только отключённые соединения могут подключаться: impl Connection { fn new(address: &str) -> Self { Connection { @@ -199,7 +186,7 @@ impl Connection { } fn connect(self) -> Connection { - println!("Connecting to {}...", self.address); + println!("Подключение к {}...", self.address); Connection { address: self.address, _state: std::marker::PhantomData, @@ -207,10 +194,10 @@ impl Connection { } } -// Only Connected connections can authenticate: +// Только подключённые соединения могут аутентифицироваться: impl Connection { fn authenticate(self, _token: &str) -> Connection { - println!("Authenticating..."); + println!("Аутентификация..."); Connection { address: self.address, _state: std::marker::PhantomData, @@ -218,10 +205,10 @@ impl Connection { } } -// Only Authenticated connections can make requests: +// Только аутентифицированные соединения могут отправлять запросы: impl Connection { fn request(&self, path: &str) -> String { - format!("GET {} from {}", path, self.address) + format!("GET {} с {}", path, self.address) } } @@ -233,25 +220,23 @@ fn main() { // conn.request("/data"); // ❌ Compile error: no method `request` on Connection let conn = conn.authenticate("secret-token"); - let response = conn.request("/data"); // ✅ Only works after authentication + let response = conn.request("/data"); // ✅ Работает только после аутентификации println!("{response}"); } ``` -> **Key insight**: Each state transition *consumes* `self` and returns a new type. -> You can't use the old state after transitioning — the compiler enforces it. -> Zero runtime cost — `PhantomData` is zero-sized, states are erased at compile time. +> **Ключевая мысль**: каждый переход состояния *поглощает* `self` и возвращает новый тип. После перехода использовать старое состояние нельзя: это обеспечивает компилятор. Затрат во время выполнения нет: `PhantomData` имеет нулевой размер, а состояния стираются на этапе компиляции. -**Comparison with C++/C#**: In C++ or C#, you'd enforce this with runtime checks (`if (!authenticated) throw ...`). The Rust type-state pattern moves these checks to compile time — invalid states are literally unrepresentable in the type system. +**Сравнение с C++ и C#**: там это обеспечивают проверками во время выполнения (`if (!authenticated) throw ...`). Паттерн type-state переносит такие проверки на этап компиляции: недопустимые состояния буквально невыразимы в системе типов. -### Builder Pattern with Type States +### Builder-паттерн с type-state -A practical application — a builder that enforces required fields: +Практическое применение: builder, который требует заполнения обязательных полей. ```rust use std::marker::PhantomData; -// Marker types for required fields +// Маркерные типы для обязательных полей struct NeedsName; struct NeedsPort; struct Ready; @@ -259,7 +244,7 @@ struct Ready; struct ServerConfig { name: Option, port: Option, - max_connections: usize, // Optional, has default + max_connections: usize, // Необязательное поле, есть значение по умолчанию _state: PhantomData, } @@ -316,7 +301,7 @@ struct Server { } fn main() { - // Must provide name, then port, then can build: + // Сначала обязательно задаём имя, затем порт, и только после этого можно собирать: let server = ServerConfig::new() .name("my-server") .port(8080) @@ -330,9 +315,9 @@ fn main() { *** -## Case Study: Type-Safe Connection Pool +## Пример: типобезопасный пул соединений -Real-world systems need connection pools where connections move through well-defined states. Here's how the typestate pattern enforces correctness in a production pool: +Реальным системам нужны пулы соединений, в которых соединения проходят через чётко определённые состояния. Вот как паттерн typestate обеспечивает корректность в промышленном пуле: ```mermaid stateDiagram-v2 @@ -342,13 +327,13 @@ stateDiagram-v2 Active --> Idle: conn.commit() / conn.rollback() Idle --> [*]: pool.release(conn) - Active --> [*]: ❌ cannot release mid-transaction + Active --> [*]: ❌ нельзя освободить посреди транзакции ``` ```rust use std::marker::PhantomData; -// States +// Состояния struct Idle; struct InTransaction; @@ -366,13 +351,13 @@ impl Pool { fn acquire(&mut self) -> PooledConnection { self.next_id += 1; - println!("[pool] Acquired connection #{}", self.next_id); + println!("[pool] Получено соединение #{}", self.next_id); PooledConnection { id: self.next_id, _state: PhantomData } } - // Only idle connections can be released — prevents mid-transaction leaks + // Освобождать можно только простаивающие соединения: это предотвращает утечки посреди транзакции fn release(&self, conn: PooledConnection) { - println!("[pool] Released connection #{}", conn.id); + println!("[pool] Возвращено соединение #{}", conn.id); } } @@ -406,25 +391,22 @@ fn main() { let conn = conn.begin_transaction(); conn.execute("INSERT INTO users VALUES ('Alice')"); conn.execute("INSERT INTO orders VALUES (1, 42)"); - let conn = conn.commit(); // Back to Idle - pool.release(conn); // ✅ Only works on Idle connections + let conn = conn.commit(); // Снова Idle + pool.release(conn); // ✅ Работает только с соединениями в состоянии Idle // pool.release(conn_active); // ❌ Compile error: can't release InTransaction } ``` -**Why this matters in production**: A connection leaked mid-transaction holds database -locks indefinitely. The typestate pattern makes this impossible — you literally cannot -return a connection to the pool until the transaction is committed or rolled back. +**Почему это важно на практике**: соединение, утёкшее посреди транзакции, удерживает блокировки базы данных бесконечно. Паттерн typestate делает такую утечку невозможной: вернуть соединение в пул нельзя, пока транзакция не будет зафиксирована (commit) или откатана (rollback). *** -## Config Trait Pattern — Taming Generic Parameter Explosion +## Паттерн конфигурационного трейта: укрощаем разрастание обобщённых параметров -### The Problem +### Проблема -As a struct takes on more responsibilities, each backed by a trait-constrained generic, -the type signature grows unwieldy: +По мере того как структура получает всё больше обязанностей, каждая из которых опирается на обобщённый параметр с ограничением трейтом, сигнатура типа становится громоздкой: ```rust trait SpiBus { fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError>; } @@ -433,7 +415,7 @@ trait I3cBus { fn i3c_read(&self, addr: u8, buf: &mut [u8]) -> Result<(), BusE trait SmBus { fn smbus_read_byte(&self, addr: u8, cmd: u8) -> Result; } trait GpioBus { fn gpio_set(&self, pin: u32, high: bool); } -// ❌ Every new bus trait adds another generic parameter +// ❌ Каждый новый трейт шины добавляет ещё один обобщённый параметр struct DiagController { spi: S, com: C, @@ -441,18 +423,15 @@ struct DiagController { smbus: M, gpio: G, } -// impl blocks, function signatures, and callers all repeat the full list. -// Adding a 6th bus means editing every mention of DiagController. +// Блоки impl, сигнатуры функций и вызывающий код повторяют весь список. +// Добавление шестой шины потребует правки каждого упоминания DiagController. ``` -This is often called **"generic parameter explosion."** It compounds across `impl` blocks, -function parameters, and downstream consumers — each of which must repeat the full -parameter list. +Это часто называют **«взрывом обобщённых параметров»** (generic parameter explosion). Он усугубляется в блоках `impl`, параметрах функций и у потребителей, каждый из которых вынужден повторять полный список параметров. -### The Solution: A Config Trait +### Решение: конфигурационный трейт -Bundle all associated types into a single trait. The struct then has **one** generic -parameter regardless of how many component types it contains: +Объедините все ассоциированные типы в один трейт. Тогда у структуры будет **один** обобщённый параметр, сколько бы компонентов она ни содержала: ```rust #[derive(Debug)] @@ -462,7 +441,7 @@ enum BusError { HardwareFault(String), } -// --- Bus traits (unchanged) --- +// --- Трейты шин (без изменений) --- trait SpiBus { fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError>; fn spi_write(&self, data: &[u8]) -> Result<(), BusError>; @@ -478,14 +457,14 @@ trait I3cBus { fn i3c_write(&self, addr: u8, data: &[u8]) -> Result<(), BusError>; } -// --- The Config trait: one associated type per component --- +// --- Конфигурационный трейт: один ассоциированный тип на компонент --- trait BoardConfig { type Spi: SpiBus; type Com: ComPort; type I3c: I3cBus; } -// --- DiagController has exactly ONE generic parameter --- +// --- У DiagController ровно ОДИН обобщённый параметр --- struct DiagController { spi: Cfg::Spi, com: Cfg::Com, @@ -493,11 +472,9 @@ struct DiagController { } ``` -`DiagController` will never gain another generic parameter. -Adding a 4th bus means adding one associated type to `BoardConfig` and one field -to `DiagController` — no downstream signature changes. +`DiagController` больше никогда не получит новый обобщённый параметр. Добавление четвёртой шины означает добавление одного ассоциированного типа в `BoardConfig` и одного поля в `DiagController`, без изменения зависимых сигнатур. -### Implementing the Controller +### Реализация контроллера ```rust impl DiagController { @@ -550,10 +527,9 @@ struct DiagReport { } ``` -### Production Wiring - -One `impl BoardConfig` selects the concrete hardware drivers: +### Сборка для продакшена +Один `impl BoardConfig` выбирает конкретные драйверы оборудования: ```rust struct PlatformSpi { dev: String, speed_hz: u32 } struct UartCom { dev: String, baud: u32 } @@ -561,7 +537,7 @@ struct LinuxI3c { dev: String } impl SpiBus for PlatformSpi { fn spi_transfer(&self, tx: &[u8], rx: &mut [u8]) -> Result<(), BusError> { - // ioctl(SPI_IOC_MESSAGE) in production + // ioctl(SPI_IOC_MESSAGE) в продакшене rx[0..4].copy_from_slice(&[0xEF, 0x40, 0x18, 0x00]); Ok(()) } @@ -585,7 +561,7 @@ impl I3cBus for LinuxI3c { fn i3c_write(&self, _addr: u8, _data: &[u8]) -> Result<(), BusError> { Ok(()) } } -// ✅ One struct, one impl — all concrete types resolved here +// ✅ Одна структура, одна реализация: все конкретные типы определяются здесь struct ProductionBoard; impl BoardConfig for ProductionBoard { type Spi = PlatformSpi; @@ -604,9 +580,9 @@ fn main() { } ``` -### Test Wiring with Mocks +### Подключение тестов с моками -Swap the entire hardware layer by defining a different `BoardConfig`: +Замените весь аппаратный слой, определив другой `BoardConfig`: ```rust struct MockSpi { flash_id: [u8; 4] } @@ -687,94 +663,85 @@ mod tests { } ``` -### Adding a New Bus Later +### Добавление новой шины позже -When you need a 4th bus, only two things change — `BoardConfig` and `DiagController`. -**No downstream signature changes.** The generic parameter count stays at one: +Когда понадобится четвёртая шина, меняются только две вещи: `BoardConfig` и `DiagController`. **Зависимые сигнатуры не меняются.** Число обобщённых параметров остаётся равным одному: ```rust trait SmBus { fn smbus_read_byte(&self, addr: u8, cmd: u8) -> Result; } -// 1. Add one associated type: +// 1. Добавляем один ассоциированный тип: trait BoardConfig { type Spi: SpiBus; type Com: ComPort; type I3c: I3cBus; - type Smb: SmBus; // ← new + type Smb: SmBus; // ← новое } -// 2. Add one field: +// 2. Добавляем одно поле: struct DiagController { spi: Cfg::Spi, com: Cfg::Com, i3c: Cfg::I3c, - smb: Cfg::Smb, // ← new + smb: Cfg::Smb, // ← новое } -// 3. Provide the concrete type in each config impl: +// 3. Указываем конкретный тип в каждой реализации конфигурации: impl BoardConfig for ProductionBoard { type Spi = PlatformSpi; type Com = UartCom; type I3c = LinuxI3c; - type Smb = LinuxSmbus; // ← new + type Smb = LinuxSmbus; // ← новое } ``` -### When to Use This Pattern +### Когда использовать этот паттерн -| Situation | Use Config Trait? | Alternative | -|-----------|:-:|---| -| 3+ trait-constrained generics on a struct | ✅ Yes | — | -| Need to swap entire hardware/platform layer | ✅ Yes | — | -| Only 1-2 generics | ❌ Overkill | Direct generics | -| Need runtime polymorphism | ❌ | `dyn Trait` objects | -| Open-ended plugin system | ❌ | Type-map / `Any` | -| Component traits form a natural group (board, platform) | ✅ Yes | — | +| Ситуация | Использовать конфигурационный трейт? | Альтернатива | +|----------|:-:|---| +| У структуры 3 и более обобщённых параметров с ограничениями трейтами | ✅ Да | — | +| Нужно заменить весь аппаратный слой или платформу целиком | ✅ Да | — | +| Только 1–2 обобщённых параметра | ❌ Избыточно | Прямые обобщённые параметры | +| Нужен полиморфизм во время выполнения | ❌ | Трейт-объекты `dyn Trait` | +| Открытая система плагинов | ❌ | Карта типов / `Any` | +| Трейты компонентов естественно образуют группу (плата, платформа) | ✅ Да | — | -### Key Properties +### Ключевые свойства -- **One generic parameter forever** — `DiagController` never gains more `` -- **Fully static dispatch** — no vtables, no `dyn`, no heap allocation for trait objects -- **Clean test swapping** — define `TestBoard` with mock impls, zero conditional compilation -- **Compile-time safety** — forget an associated type → compile error, not runtime crash -- **Battle-tested** — this is the pattern used by Substrate/Polkadot's frame system - to manage 20+ associated types through a single `Config` trait +- **Один обобщённый параметр навсегда**: `DiagController` никогда не получит новых `` +- **Полностью статическая диспетчеризация**: никаких vtable, `dyn` и выделений памяти в куче для трейт-объектов +- **Чистая подмена в тестах**: определите `TestBoard` с моками, без условной компиляции +- **Безопасность на этапе компиляции**: забыли ассоциированный тип → ошибка компиляции, а не падение во время выполнения +- **Проверено на практике**: именно этот паттерн использует система frame в Substrate/Polkadot, чтобы управлять более чем 20 ассоциированными типами через один трейт `Config` -> **Key Takeaways — Newtype & Type-State** -> - Newtypes give compile-time type safety at zero runtime cost -> - Type-state makes illegal state transitions a compile error, not a runtime bug -> - Config traits tame generic parameter explosion in large systems +> **Ключевые выводы: newtype и type-state** +> - Newtype даёт безопасность типов на этапе компиляции без затрат во время выполнения +> - Type-state превращает недопустимые переходы состояний в ошибку компиляции, а не в ошибку времени выполнения +> - Конфигурационные трейты укрощают разрастание обобщённых параметров в больших системах -> **See also:** [Ch 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) for the zero-sized markers that power type-state. [Ch 2 — Traits In Depth](ch02-traits-in-depth.md) for associated types used in the config trait pattern. +> **См. также:** [гл. 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) — нулевые маркеры, на которых держится type-state. [гл. 2 — Трейты в деталях](ch02-traits-in-depth.md) — ассоциированные типы в паттерне конфигурационного трейта. --- -## Case Study: Dual-Axis Typestate — Vendor × Protocol State +## Пример: двухосевой typestate (поставщик × состояние протокола) -The patterns above handle one axis at a time: typestate enforces *protocol order*, -and trait abstraction handles *multiple vendors*. Real systems often need **both -simultaneously**: a wrapper `Handle` where available methods depend -on *which vendor* is plugged in **and** *which state* the handle is in. +Описанные выше паттерны работают по одной оси за раз: typestate обеспечивает *порядок протокола*, а абстракция через трейты справляется с *несколькими поставщиками*. Реальным системам часто нужны **обе вещи одновременно**: обёртка `Handle`, в которой доступные методы зависят от того, *какой поставщик* подключён, **и** от того, *в каком состоянии* находится дескриптор. -This section shows the **dual-axis conditional `impl`** pattern — where `impl` -blocks are gated on both a vendor trait bound and a state marker trait. +В этом разделе показан паттерн **условного `impl` по двум осям**: блоки `impl` ограничиваются одновременно границей трейта поставщика и маркерным трейтом состояния. -### The Two-Dimensional Problem +### Двумерная задача -Consider a debug probe interface (JTAG/SWD). Multiple vendors make probes, and -every probe must be unlocked before registers become accessible. Some vendors -additionally support direct memory reads — but only after an *extended unlock* -that configures the memory access port: +Рассмотрим интерфейс отладочного зонда (JTAG/SWD). Зонды выпускают разные поставщики, и каждый зонд нужно разблокировать, прежде чем станут доступны регистры. Некоторые поставщики дополнительно поддерживают прямое чтение памяти, но только после *расширенной разблокировки*, которая настраивает порт доступа к памяти: ```mermaid graph LR - subgraph "All vendors" - L["🔒 Locked"] -- "unlock()" --> U["🔓 Unlocked"] + subgraph "Все поставщики" + L["🔒 Заблокирован"] -- "unlock()" --> U["🔓 Разблокирован"] end - subgraph "Memory-capable vendors only" - U -- "extended_unlock()" --> E["🔓🧠 ExtendedUnlocked"] + subgraph "Только поставщики с поддержкой памяти" + U -- "extended_unlock()" --> E["🔓🧠 Расширенно разблокирован"] end U -. "read_reg() / write_reg()" .-> U @@ -786,15 +753,14 @@ graph LR style E fill:#eef,stroke:#33c ``` -The **capability matrix** — which methods exist for which (vendor, state) -combination — is two-dimensional: +**Матрица возможностей**, то есть какие методы существуют для каждой комбинации (поставщик, состояние), двумерна: ```mermaid block-beta columns 4 - space header1["Locked"] header2["Unlocked"] header3["ExtendedUnlocked"] - basic["Basic Vendor"]:1 b1["unlock()"] b2["read_reg()\nwrite_reg()"] b3["— unreachable —"] - memory["Memory Vendor"]:1 m1["unlock()"] m2["read_reg()\nwrite_reg()\nextended_unlock()"] m3["read_reg()\nwrite_reg()\nread_memory()\nwrite_memory()"] + space header1["Заблокирован"] header2["Разблокирован"] header3["Расширенно разблокирован"] + basic["Базовый поставщик"]:1 b1["unlock()"] b2["read_reg()\nwrite_reg()"] b3["— недостижимо —"] + memory["Поставщик с памятью"]:1 m1["unlock()"] m2["read_reg()\nwrite_reg()\nextended_unlock()"] m3["read_reg()\nwrite_reg()\nread_memory()\nwrite_memory()"] style b1 fill:#ffd,stroke:#aa0 style b2 fill:#efe,stroke:#3a3 @@ -804,23 +770,21 @@ block-beta style m3 fill:#eef,stroke:#33c ``` -The challenge: express this matrix **entirely at compile time**, with static -dispatch, so that calling `extended_unlock()` on a basic probe or -`read_memory()` on an unlocked-but-not-extended handle is a compile error. +Задача: выразить эту матрицу **полностью на этапе компиляции** со статической диспетчеризацией, чтобы вызов `extended_unlock()` у базового зонда или `read_memory()` у разблокированного, но не расширенно разблокированного дескриптора был ошибкой компиляции. -### The Solution: `Jtag` with Marker Traits +### Решение: `Jtag` с маркерными трейтами -**Step 1 — State tokens and capability markers:** +**Шаг 1: токены состояний и маркеры возможностей:** ```rust,ignore use std::marker::PhantomData; -// Zero-sized state tokens — no runtime cost +// Токены состояний нулевого размера: без затрат во время выполнения struct Locked; struct Unlocked; struct ExtendedUnlocked; -// Marker traits express which capabilities each state has +// Маркерные трейты описывают, какие возможности есть у каждого состояния trait HasRegAccess {} impl HasRegAccess for Unlocked {} impl HasRegAccess for ExtendedUnlocked {} @@ -829,24 +793,19 @@ trait HasMemAccess {} impl HasMemAccess for ExtendedUnlocked {} ``` -> **Why marker traits, not just concrete states?** -> Writing `impl Jtag` means `read_reg()` works in -> *any* state with register access — today that's `Unlocked` and `ExtendedUnlocked`, -> but if you add `DebugHalted` tomorrow, you just add one line: -> `impl HasRegAccess for DebugHalted {}`. Every register function works with -> it automatically — zero code changes. - -**Step 2 — Vendor traits (raw operations):** +> **Почему маркерные трейты, а не просто конкретные состояния?** +> Запись `impl Jtag` означает, что `read_reg()` работает в *любом* состоянии с доступом к регистрам. Сегодня это `Unlocked` и `ExtendedUnlocked`, но если завтра добавить `DebugHalted`, достаточно одной строки: `impl HasRegAccess for DebugHalted {}`. Каждая функция для регистров станет работать с ним автоматически, без изменений в коде. +**Шаг 2: трейты поставщиков (низкоуровневые операции):** ```rust,ignore -// Every probe vendor implements these +// Каждый поставщик зондов реализует эти методы trait JtagVendor { fn raw_unlock(&mut self); fn raw_read_reg(&self, addr: u32) -> u32; fn raw_write_reg(&mut self, addr: u32, val: u32); } -// Vendors with memory access also implement this super-trait +// Поставщики с доступом к памяти дополнительно реализуют этот супертрейт trait JtagMemoryVendor: JtagVendor { fn raw_extended_unlock(&mut self); fn raw_read_memory(&self, addr: u64, buf: &mut [u8]); @@ -854,7 +813,7 @@ trait JtagMemoryVendor: JtagVendor { } ``` -**Step 3 — The wrapper with conditional `impl` blocks:** +**Шаг 3: обёртка с условными блоками `impl`:** ```rust,ignore struct Jtag { @@ -862,7 +821,7 @@ struct Jtag { _state: PhantomData, } -// Construction — always starts Locked +// Конструирование: всегда начинается в состоянии Locked impl Jtag { fn new(vendor: V) -> Self { Jtag { vendor, _state: PhantomData } @@ -874,7 +833,7 @@ impl Jtag { } } -// Register I/O — any vendor, any state with HasRegAccess +// Ввод-вывод регистров: любой поставщик, любое состояние с HasRegAccess impl Jtag { fn read_reg(&self, addr: u32) -> u32 { self.vendor.raw_read_reg(addr) @@ -884,7 +843,7 @@ impl Jtag { } } -// Extended unlock — only memory-capable vendors, only from Unlocked +// Расширенная разблокировка: только поставщики с памятью, только из Unlocked impl Jtag { fn extended_unlock(mut self) -> Jtag { self.vendor.raw_extended_unlock(); @@ -892,7 +851,7 @@ impl Jtag { } } -// Memory I/O — only memory-capable vendors, only ExtendedUnlocked +// Ввод-вывод памяти: только поставщики с памятью, только в состоянии ExtendedUnlocked impl Jtag { fn read_memory(&self, addr: u64, buf: &mut [u8]) { self.vendor.raw_read_memory(addr, buf); @@ -903,81 +862,76 @@ impl Jtag { } ``` -Each `impl` block encodes one cell (or row) of the capability matrix. -The compiler enforces the matrix — no runtime checks anywhere. +Каждый блок `impl` кодирует одну ячейку (или строку) матрицы возможностей. Компилятор обеспечивает соблюдение матрицы: никаких проверок во время выполнения. -### Vendor Implementations +### Реализации поставщиков -Adding a vendor means implementing raw methods on **one struct** — no -per-state struct duplication, no delegation boilerplate: +Добавление поставщика означает реализацию низкоуровневых методов в **одной структуре**: без дублирования структур для каждого состояния и без шаблонного кода делегирования: ```rust,ignore -// Vendor A: basic probe — register access only +// Поставщик A: базовый зонд, только доступ к регистрам struct BasicProbe { port: u16 } impl JtagVendor for BasicProbe { - fn raw_unlock(&mut self) { /* TAP reset sequence */ } + fn raw_unlock(&mut self) { /* последовательность TAP reset */ } fn raw_read_reg(&self, addr: u32) -> u32 { /* DR scan */ 0 } fn raw_write_reg(&mut self, addr: u32, val: u32) { /* DR scan */ } } -// BasicProbe does NOT impl JtagMemoryVendor. -// extended_unlock() will not compile on Jtag. +// BasicProbe НЕ реализует JtagMemoryVendor. +// extended_unlock() не скомпилируется для Jtag. -// Vendor B: full-featured probe — registers + memory +// Поставщик B: полнофункциональный зонд, регистры и память struct DapProbe { serial: String } impl JtagVendor for DapProbe { - fn raw_unlock(&mut self) { /* SWD switch, read DPIDR */ } - fn raw_read_reg(&self, addr: u32) -> u32 { /* AP register read */ 0 } - fn raw_write_reg(&mut self, addr: u32, val: u32) { /* AP register write */ } + fn raw_unlock(&mut self) { /* переключение на SWD, чтение DPIDR */ } + fn raw_read_reg(&self, addr: u32) -> u32 { /* чтение регистра AP */ 0 } + fn raw_write_reg(&mut self, addr: u32, val: u32) { /* запись регистра AP */ } } impl JtagMemoryVendor for DapProbe { - fn raw_extended_unlock(&mut self) { /* select MEM-AP, power up */ } - fn raw_read_memory(&self, addr: u64, buf: &mut [u8]) { /* MEM-AP read */ } - fn raw_write_memory(&mut self, addr: u64, data: &[u8]) { /* MEM-AP write */ } + fn raw_extended_unlock(&mut self) { /* выбор MEM-AP, включение питания */ } + fn raw_read_memory(&self, addr: u64, buf: &mut [u8]) { /* чтение MEM-AP */ } + fn raw_write_memory(&mut self, addr: u64, data: &[u8]) { /* запись MEM-AP */ } } ``` -### What the Compiler Prevents +### Что предотвращает компилятор -| Attempt | Error | Why | -|---------|-------|-----| -| `Jtag<_, Locked>::read_reg()` | no method `read_reg` | `Locked` doesn't impl `HasRegAccess` | -| `Jtag::extended_unlock()` | no method `extended_unlock` | `BasicProbe` doesn't impl `JtagMemoryVendor` | -| `Jtag<_, Unlocked>::read_memory()` | no method `read_memory` | `Unlocked` doesn't impl `HasMemAccess` | -| Calling `unlock()` twice | value used after move | `unlock()` consumes `self` | +| Попытка | Ошибка | Причина | +|---------|--------|---------| +| `Jtag<_, Locked>::read_reg()` | no method `read_reg` | `Locked` не реализует `HasRegAccess` | +| `Jtag::extended_unlock()` | no method `extended_unlock` | `BasicProbe` не реализует `JtagMemoryVendor` | +| `Jtag<_, Unlocked>::read_memory()` | no method `read_memory` | `Unlocked` не реализует `HasMemAccess` | +| Двойной вызов `unlock()` | value used after move | `unlock()` поглощает `self` | -All four errors are caught **at compile time**. No panics, no `Option`, no runtime state enum. +Все четыре ошибки обнаруживаются **на этапе компиляции**. Никаких паник, никакого `Option` и никакого перечисления состояний во время выполнения. -### Writing Generic Functions +### Написание обобщённых функций -Functions bind only the axes they care about: +Функции привязываются только к тем осям, которые им важны: ```rust,ignore -/// Works with ANY vendor, ANY state that grants register access. +/// Работает с ЛЮБЫМ поставщиком и ЛЮБЫМ состоянием, которое даёт доступ к регистрам. fn read_idcode(jtag: &Jtag) -> u32 { jtag.read_reg(0x00) } -/// Only compiles for memory-capable vendors in ExtendedUnlocked state. +/// Компилируется только для поставщиков с памятью в состоянии ExtendedUnlocked. fn dump_firmware(jtag: &Jtag) { let mut buf = [0u8; 256]; jtag.read_memory(0x0800_0000, &mut buf); } ``` -`read_idcode` doesn't care whether you're in `Unlocked` or `ExtendedUnlocked` — -it only requires `HasRegAccess`. This is where marker traits pay off over -hardcoding specific states in signatures. +`read_idcode` не важно, находитесь ли вы в `Unlocked` или `ExtendedUnlocked`: ему нужен только `HasRegAccess`. Здесь маркерные трейты окупаются по сравнению с жёсткой фиксацией конкретных состояний в сигнатурах. -### Same Pattern, Different Domain: Storage Backends +### Тот же паттерн в другой предметной области: бэкенды хранилища -The dual-axis technique isn't hardware-specific. Here's the same structure -for a storage layer where some backends support transactions: +Приём с двумя осями не привязан к аппаратуре. Вот та же структура для слоя хранения, где некоторые бэкенды поддерживают транзакции: ```rust,ignore -// States +// Состояния struct Closed; struct Open; struct InTransaction; @@ -986,7 +940,7 @@ trait HasReadWrite {} impl HasReadWrite for Open {} impl HasReadWrite for InTransaction {} -// Vendor traits +// Трейты поставщиков trait StorageBackend { fn raw_open(&mut self); fn raw_read(&self, key: &[u8]) -> Option>; @@ -999,7 +953,7 @@ trait TransactionalBackend: StorageBackend { fn raw_rollback(&mut self); } -// Wrapper +// Обёртка struct Store { backend: B, _s: PhantomData } impl Store { @@ -1018,49 +972,36 @@ impl Store { } ``` -A flat-file backend implements `StorageBackend` only — `begin()` won't -compile. A database backend adds `TransactionalBackend` — the full -`Open → InTransaction → Open` cycle becomes available. - -### When to Reach for This Pattern - -| Signal | Why dual-axis fits | -|--------|--------------------| -| Two independent axes: "who provides it" and "what state is it in" | The `impl` block matrix directly encodes both | -| Some providers have strictly more capabilities than others | Super-trait (`MemoryVendor: Vendor`) + conditional `impl` | -| Misusing state or capability is a safety/correctness bug | Compile-time prevention > runtime checks | -| You want static dispatch (no vtables) | `PhantomData` + generics = zero-cost | - -| Signal | Consider something simpler | -|--------|---------------------------| -| Only one axis varies (state OR vendor, not both) | Single-axis typestate or plain trait objects | -| Three or more independent axes | Config Trait Pattern (above) bundles axes into associated types | -| Runtime polymorphism is acceptable | `enum` state + `dyn` dispatch is simpler | - -> **When two axes become three or more:** -> If you find yourself writing `Handle` — vendor, state, debug -> level, transport — the generic parameter list is telling you something. -> Consider collapsing the *vendor* axis into an associated-type config trait -> (the [Config Trait Pattern](#config-trait-pattern--taming-generic-parameter-explosion) -> from earlier in this chapter), keeping only the *state* axis as a generic -> parameter: `Handle`. The config trait bundles `type Vendor`, `type Transport`, etc. -> into one parameter, and the state axis retains its compile-time transition guarantees. -> This is a natural evolution, not a rewrite — you lift vendor-related types -> into `Cfg` and leave the typestate machinery untouched. - -> **Key Takeaway:** The dual-axis pattern is the intersection of typestate and -> trait-based abstraction. Each `impl` block maps to one cell of the -> (vendor × state) matrix. The compiler enforces the entire matrix — no -> runtime state checks, no impossible-state panics, no cost. +Бэкенд на плоских файлах реализует только `StorageBackend`, поэтому `begin()` не скомпилируется. Бэкенд базы данных добавляет `TransactionalBackend`, и становится доступен полный цикл `Open → InTransaction → Open`. + +### Когда прибегать к этому паттерну + +| Признак | Почему подходит двухосевой паттерн | +|---------|------------------------------------| +| Две независимые оси: «кто предоставляет» и «в каком состоянии» | Матрица блоков `impl` напрямую кодирует обе оси | +| У одних поставщиков строго больше возможностей, чем у других | Супертрейт (`MemoryVendor: Vendor`) и условный `impl` | +| Неправильное использование состояния или возможности — ошибка безопасности или корректности | Предотвращение на этапе компиляции важнее проверок во время выполнения | +| Нужна статическая диспетчеризация (без vtable) | `PhantomData` + обобщения = без накладных расходов | + +| Признак | Что рассмотреть проще | +|---------|-----------------------| +| Меняется только одна ось (состояние ИЛИ поставщик, но не обе) | Одноосевой typestate или обычные трейт-объекты | +| Три и более независимых осей | Конфигурационный трейт (выше) объединяет оси в ассоциированные типы | +| Допустим полиморфизм во время выполнения | Состояние в виде `enum` и диспетчеризация через `dyn` проще | + +> **Когда осей становится три и больше:** +> Если вы обнаруживаете, что пишете `Handle` (поставщик, состояние, уровень отладки, транспорт), то список обобщённых параметров говорит сам за себя. Подумайте о том, чтобы свернуть ось *поставщика* в конфигурационный трейт с ассоциированными типами (см. [Паттерн конфигурационного трейта](#паттерн-конфигурационного-трейта-укрощаем-разрастание-обобщённых-параметров) выше в этой главе), оставив ось *состояния* обобщённым параметром: `Handle`. Конфигурационный трейт объединяет `type Vendor`, `type Transport` и т. д. в один параметр, а ось состояния сохраняет гарантии переходов на этапе компиляции. Это естественная эволюция, а не переписывание: вы переносите типы, связанные с поставщиком, в `Cfg`, а механизм typestate не трогаете. + +> **Ключевой вывод:** двухосевой паттерн — это пересечение typestate и абстракции на основе трейтов. Каждый блок `impl` соответствует одной ячейке матрицы (поставщик × состояние). Компилятор обеспечивает соблюдение всей матрицы: никаких проверок состояния во время выполнения, никаких паник из-за невозможного состояния и никаких затрат. --- -### Exercise: Type-Safe State Machine ★★ (~30 min) +### Упражнение: типобезопасный автомат состояний ★★ (~30 минут) -Build a traffic light state machine using the type-state pattern. The light must transition `Red → Green → Yellow → Red` and no other order should be possible. +Постройте автомат состояний светофора с помощью паттерна type-state. Свет должен переходить `Red → Green → Yellow → Red`, и никакой другой порядок не должен быть возможен.
-🔑 Solution +🔑 Решение ```rust use std::marker::PhantomData; @@ -1075,44 +1016,43 @@ struct TrafficLight { impl TrafficLight { fn new() -> Self { - println!("🔴 Red — STOP"); + println!("🔴 Красный — СТОП"); TrafficLight { _state: PhantomData } } fn go(self) -> TrafficLight { - println!("🟢 Green — GO"); + println!("🟢 Зелёный — ВПЕРЁД"); TrafficLight { _state: PhantomData } } } impl TrafficLight { fn caution(self) -> TrafficLight { - println!("🟡 Yellow — CAUTION"); + println!("🟡 Жёлтый — ВНИМАНИЕ"); TrafficLight { _state: PhantomData } } } impl TrafficLight { fn stop(self) -> TrafficLight { - println!("🔴 Red — STOP"); + println!("🔴 Красный — СТОП"); TrafficLight { _state: PhantomData } } } fn main() { - let light = TrafficLight::new(); // Red - let light = light.go(); // Green - let light = light.caution(); // Yellow - let _light = light.stop(); // Red + let light = TrafficLight::new(); // Красный + let light = light.go(); // Зелёный + let light = light.caution(); // Жёлтый + let _light = light.stop(); // Красный // light.caution(); // ❌ Compile error: no method `caution` on Red // TrafficLight::new().stop(); // ❌ Compile error: no method `stop` on Red } ``` -**Key takeaway**: Invalid transitions are compile errors, not runtime panics. +**Ключевой вывод**: недопустимые переходы — это ошибки компиляции, а не паники во время выполнения.
*** - diff --git a/rust-patterns-book/src/ch04-phantomdata-types-that-carry-no-data.md b/rust-patterns-book/src/ch04-phantomdata-types-that-carry-no-data.md index fb08840..b058ee4 100644 --- a/rust-patterns-book/src/ch04-phantomdata-types-that-carry-no-data.md +++ b/rust-patterns-book/src/ch04-phantomdata-types-that-carry-no-data.md @@ -1,54 +1,54 @@ -# 4. PhantomData — Types That Carry No Data 🔴 +# 4. PhantomData — типы, которые не несут данных 🔴 -> **What you'll learn:** -> - Why `PhantomData` exists and the three problems it solves -> - Lifetime branding for compile-time scope enforcement -> - The unit-of-measure pattern for dimension-safe arithmetic -> - Variance (covariant, contravariant, invariant) and how PhantomData controls it +> **Что вы узнаете:** +> - Зачем нужен `PhantomData` и три проблемы, которые он решает +> - Маркировка времён жизни (lifetime branding) для проверки области действия на этапе компиляции +> - Паттерн «единицы измерения» для арифметики с проверкой размерностей +> - Вариантность (ковариантность, контравариантность, инвариантность) и то, как ею управляет PhantomData -## What PhantomData Solves +## Что решает PhantomData -`PhantomData` is a zero-sized type that tells the compiler "this struct is logically associated with `T`, even though it doesn't contain a `T`." It affects variance, drop checking, and auto-trait inference — without using any memory. +`PhantomData` — это тип нулевого размера, который сообщает компилятору: «эта структура логически связана с `T`, хотя самого `T` она не содержит». Он влияет на вариантность, проверку drop и автоматический вывод auto-трейтов, не занимая при этом памяти. ```rust use std::marker::PhantomData; -// Without PhantomData: +// Без PhantomData: struct Slice<'a, T> { ptr: *const T, len: usize, - // Problem: compiler doesn't know this struct borrows from 'a - // or that it's associated with T for drop-check purposes + // Проблема: компилятор не знает, что эта структура заимствует данные из 'a, + // и не учитывает связь с T при проверке drop } -// With PhantomData: +// С PhantomData: struct Slice<'a, T> { ptr: *const T, len: usize, _marker: PhantomData<&'a T>, - // Now the compiler knows: - // 1. This struct borrows data with lifetime 'a - // 2. It's covariant over 'a (lifetimes can shrink) - // 3. Drop check considers T + // Теперь компилятор знает: + // 1. Эта структура заимствует данные с временем жизни 'a + // 2. Она ковариантна по 'a (времена жизни могут сокращаться) + // 3. Проверка drop учитывает T } ``` -**The three jobs of PhantomData**: +**Три задачи PhantomData**: -| Job | Example | What It Does | -|-----|---------|-------------| -| **Lifetime binding** | `PhantomData<&'a T>` | Struct is treated as borrowing `'a` | -| **Ownership simulation** | `PhantomData` | Drop check assumes struct owns a `T` | -| **Variance control** | `PhantomData` | Makes struct contravariant over `T` | +| Задача | Пример | Что делает | +|--------|--------|-----------| +| **Привязка времени жизни** | `PhantomData<&'a T>` | Структура считается заимствующей `'a` | +| **Имитация владения** | `PhantomData` | Проверка drop считает, что структура владеет `T` | +| **Управление вариантностью** | `PhantomData` | Делает структуру контравариантной по `T` | -### Lifetime Branding +### Маркировка времени жизни (lifetime branding) -Use `PhantomData` to prevent mixing values from different "sessions" or "contexts": +Используйте `PhantomData`, чтобы не допустить смешивания значений из разных «сессий» или «контекстов»: ```rust use std::marker::PhantomData; -/// A handle that's valid only within a specific arena's lifetime +/// Дескриптор, действительный только в пределах времени жизни конкретной арены struct ArenaHandle<'arena> { index: usize, _brand: PhantomData<&'arena ()>, @@ -63,14 +63,14 @@ impl Arena { Arena { data: Vec::new() } } - /// Allocate a string and return a branded handle + /// Выделяет строку и возвращает дескриптор с маркером арены fn alloc<'a>(&'a mut self, value: String) -> ArenaHandle<'a> { let index = self.data.len(); self.data.push(value); ArenaHandle { index, _brand: PhantomData } } - /// Look up by handle — only accepts handles from THIS arena + /// Поиск по дескриптору: принимает только дескрипторы ЭТОЙ арены fn get<'a>(&'a self, handle: ArenaHandle<'a>) -> &'a str { &self.data[handle.index] } @@ -80,7 +80,7 @@ fn main() { let mut arena1 = Arena::new(); let handle1 = arena1.alloc("hello".to_string()); - // Can't use handle1 with a different arena — lifetimes won't match + // С другой ареной handle1 использовать нельзя: времена жизни не совпадут // let mut arena2 = Arena::new(); // arena2.get(handle1); // ❌ Lifetime mismatch @@ -88,15 +88,15 @@ fn main() { } ``` -### Unit-of-Measure Pattern +### Паттерн «единицы измерения» -Prevent mixing incompatible units at compile time, with zero runtime cost: +Предотвращайте смешивание несовместимых единиц на этапе компиляции без затрат во время выполнения: ```rust use std::marker::PhantomData; use std::ops::{Add, Mul}; -// Unit marker types (zero-sized) +// Маркерные типы единиц (нулевого размера) struct Meters; struct Seconds; struct MetersPerSecond; @@ -113,7 +113,7 @@ impl Quantity { } } -// Can only add same units: +// Складывать можно только одинаковые единицы: impl Add for Quantity { type Output = Quantity; fn add(self, rhs: Self) -> Self::Output { @@ -121,7 +121,7 @@ impl Add for Quantity { } } -// Meters / Seconds = MetersPerSecond (custom trait) +// Метры / секунды = метры в секунду (собственная реализация трейта) impl std::ops::Div> for Quantity { type Output = Quantity; fn div(self, rhs: Quantity) -> Quantity { @@ -133,63 +133,59 @@ fn main() { let dist = Quantity::::new(100.0); let time = Quantity::::new(9.58); let speed = dist / time; // Quantity - println!("Speed: {:.2} m/s", speed.value); // 10.44 m/s + println!("Скорость: {:.2} м/с", speed.value); // 10.44 м/с // let nonsense = dist + time; // ❌ Compile error: can't add Meters + Seconds } ``` -> **This is pure type-system magic** — `PhantomData` is zero-sized, -> so `Quantity` has the same layout as `f64`. No wrapper overhead -> at runtime, but full unit safety at compile time. +> **Это чистая магия системы типов**: `PhantomData` имеет нулевой размер, поэтому `Quantity` имеет ту же раскладку в памяти, что и `f64`. Во время выполнения никаких накладных расходов на обёртку, но полная проверка единиц на этапе компиляции. -### PhantomData and Drop Check +### PhantomData и проверка drop -When the compiler checks whether a struct's destructor might access expired data, it uses `PhantomData` to decide: +Когда компилятор проверяет, не может ли деструктор структуры обратиться к уже недействительным данным, он использует `PhantomData`, чтобы принять решение: ```rust use std::marker::PhantomData; -// PhantomData — compiler assumes we MIGHT drop a T -// This means T must outlive our struct +// PhantomData: компилятор считает, что мы МОЖЕМ удалить T +// Это значит, что T должен жить дольше нашей структуры struct OwningSemantic { ptr: *const T, - _marker: PhantomData, // "I logically own a T" + _marker: PhantomData, // «Логически я владею T» } -// PhantomData<*const T> — compiler assumes we DON'T own T -// More permissive — T doesn't need to outlive us +// PhantomData<*const T>: компилятор считает, что мы НЕ владеем T +// Более либерально: T не обязан жить дольше нас struct NonOwningSemantic { ptr: *const T, - _marker: PhantomData<*const T>, // "I just point to T" + _marker: PhantomData<*const T>, // «Я лишь указываю на T» } ``` -**Practical rule**: When wrapping raw pointers, choose PhantomData carefully: -- Writing a container that owns its data? → `PhantomData` -- Writing a view/reference type? → `PhantomData<&'a T>` or `PhantomData<*const T>` +**Практическое правило**: при обёртывании сырых указателей выбирайте PhantomData внимательно: +- Пишете контейнер, который владеет своими данными? → `PhantomData` +- Пишете тип-представление (view) или ссылку? → `PhantomData<&'a T>` или `PhantomData<*const T>` -### Variance — Why PhantomData's Type Parameter Matters +### Вариантность: почему важен параметр типа в PhantomData -**Variance** determines whether a generic type can be substituted with a sub- or -super-type (in Rust, "subtype" means "has a longer lifetime"). Getting variance -wrong causes either rejected-good-code or unsound-accepted-code. +**Вариантность** определяет, можно ли подставить вместо обобщённого типа его подтип или надтип (в Rust «подтип» означает «время жизни длиннее»). Ошибка в вариантности приводит либо к отклонению корректного кода, либо к принятию небезопасного. ```mermaid graph LR - subgraph Covariant + subgraph Ковариантность direction TB - A1["&'long T"] -->|"can become"| A2["&'short T"] + A1["&'long T"] -->|"может стать"| A2["&'short T"] end - subgraph Contravariant + subgraph Контравариантность direction TB - B1["fn(&'short T)"] -->|"can become"| B2["fn(&'long T)"] + B1["fn(&'short T)"] -->|"может стать"| B2["fn(&'long T)"] end - subgraph Invariant + subgraph Инвариантность direction TB - C1["&'a mut T"] ---|"NO substitution"| C2["&'b mut T"] + C1["&'a mut T"] ---|"подстановка НЕ допускается"| C2["&'b mut T"] end style A1 fill:#d4efdf,stroke:#27ae60,color:#000 @@ -200,15 +196,15 @@ graph LR style C2 fill:#fadbd8,stroke:#e74c3c,color:#000 ``` -#### The Three Variances +#### Три вариантности -| Variance | Meaning | "Can I substitute…" | Rust example | -|----------|---------|---------------------|--------------| -| **Covariant** | Subtype flows through | `'long` where `'short` expected ✅ | `&'a T`, `Vec`, `Box` | -| **Contravariant** | Subtype flows *against* | `'short` where `'long` expected ✅ | `fn(T)` (in parameter position) | -| **Invariant** | No substitution allowed | Neither direction ✅ | `&mut T`, `Cell`, `UnsafeCell` | +| Вариантность | Смысл | «Можно ли подставить…» | Пример в Rust | +|--------------|-------|------------------------|---------------| +| **Ковариантность** | Подтип передаётся по направлению | `'long` там, где ожидается `'short` ✅ | `&'a T`, `Vec`, `Box` | +| **Контравариантность** | Подтип передаётся *против* направления | `'short` там, где ожидается `'long` ✅ | `fn(T)` (в позиции параметра) | +| **Инвариантность** | Подстановка не допускается | Ни в одну сторону ✅ | `&mut T`, `Cell`, `UnsafeCell` | -#### Why `&'a T` is Covariant Over `'a` +#### Почему `&'a T` ковариантна по `'a` ```rust fn print_str(s: &str) { @@ -217,114 +213,111 @@ fn print_str(s: &str) { fn main() { let owned = String::from("hello"); - // owned lives for the entire function ('long) - // print_str expects &'_ str ('short — just for the call) - print_str(&owned); // ✅ Covariance: 'long → 'short is safe - // A longer-lived reference can always be used where a shorter one is needed. + // owned живёт всё время выполнения функции ('long) + // print_str ожидает &'_ str ('short: только на время вызова) + print_str(&owned); // ✅ Ковариантность: 'long → 'short безопасно + // Ссылка с более долгим временем жизни всегда может использоваться там, где нужна более короткая. } ``` -#### Why `&mut T` is Invariant Over `T` +#### Почему `&mut T` инвариантна по `T` ```rust -// If &mut T were covariant over T, this would compile: +// Если бы &mut T была ковариантна по T, этот код скомпилировался бы: fn evil(s: &mut &'static str) { - // We could write a shorter-lived &str into a &'static str slot! + // Мы могли бы записать в слот &'static str ссылку с более коротким временем жизни! let local = String::from("temporary"); - // *s = &local; // ← Would create a dangling &'static str + // *s = &local; // ← Создал бы висячий &'static str } -// Invariance prevents this: &'static str ≠ &'a str when mutating. -// The compiler rejects the substitution entirely. +// Инвариантность запрещает это: &'static str ≠ &'a str при мутации. +// Компилятор полностью отклоняет подстановку. ``` -#### How PhantomData Controls Variance +#### Как PhantomData управляет вариантностью -`PhantomData` gives your struct the **same variance as `X`**: +`PhantomData` придаёт вашей структуре **ту же вариантность, что и у `X`**: ```rust use std::marker::PhantomData; -// Covariant over 'a — a Ref<'long> can be used as Ref<'short> +// Ковариантна по 'a: Ref<'long> можно использовать как Ref<'short> struct Ref<'a, T> { ptr: *const T, - _marker: PhantomData<&'a T>, // Covariant over 'a, covariant over T + _marker: PhantomData<&'a T>, // Ковариантна по 'a, ковариантна по T } -// Invariant over T — prevents unsound lifetime shortening of T +// Инвариантна по T: предотвращает небезопасное сокращение времени жизни T struct MutRef<'a, T> { ptr: *mut T, - _marker: PhantomData<&'a mut T>, // Covariant over 'a, INVARIANT over T + _marker: PhantomData<&'a mut T>, // Ковариантна по 'a, ИНВАРИАНТНА по T } -// Contravariant over T — useful for callback containers +// Контравариантна по T: полезна для контейнеров callback-функций struct CallbackSlot { - _marker: PhantomData, // Contravariant over T + _marker: PhantomData, // Контравариантна по T } ``` -**PhantomData variance cheat sheet**: +**Шпаргалка по вариантности PhantomData**: -| PhantomData type | Variance over `T` | Variance over `'a` | Use when | -|------------------|--------------------|--------------------|-----------| -| `PhantomData` | Covariant | — | You logically own a `T` | -| `PhantomData<&'a T>` | Covariant | Covariant | You borrow a `T` with lifetime `'a` | -| `PhantomData<&'a mut T>` | **Invariant** | Covariant | You mutably borrow `T` | -| `PhantomData<*const T>` | Covariant | — | Non-owning pointer to `T` | -| `PhantomData<*mut T>` | **Invariant** | — | Non-owning mutable pointer | -| `PhantomData` | **Contravariant** | — | `T` appears in argument position | -| `PhantomData T>` | Covariant | — | `T` appears in return position | -| `PhantomData T>` | **Invariant** | — | `T` in both positions cancels out | +| Тип PhantomData | Вариантность по `T` | Вариантность по `'a` | Когда использовать | +|-----------------|---------------------|----------------------|--------------------| +| `PhantomData` | Ковариантна | — | Вы логически владеете `T` | +| `PhantomData<&'a T>` | Ковариантна | Ковариантна | Вы заимствуете `T` со временем жизни `'a` | +| `PhantomData<&'a mut T>` | **Инвариантна** | Ковариантна | Вы заимствуете `T` изменяемо | +| `PhantomData<*const T>` | Ковариантна | — | Невладеющий указатель на `T` | +| `PhantomData<*mut T>` | **Инвариантна** | — | Невладеющий изменяемый указатель | +| `PhantomData` | **Контравариантна** | — | `T` находится в позиции аргумента | +| `PhantomData T>` | Ковариантна | — | `T` находится в позиции возвращаемого значения | +| `PhantomData T>` | **Инвариантна** | — | `T` в обеих позициях: ковариантность и контравариантность уравновешивают друг друга | -#### Worked Example: Why This Matters in Practice +#### Пример: почему это важно на практике ```rust use std::marker::PhantomData; -// A token that brands values with a session lifetime. -// MUST be covariant over 'a — otherwise callers can't shorten -// the lifetime when passing to functions that need a shorter borrow. +// Токен, который помечает значения временем жизни сессии. +// ДОЛЖЕН быть ковариантен по 'a, иначе вызывающий код не сможет сократить +// время жизни при передаче в функции, которым нужно более короткое заимствование. struct SessionToken<'a> { id: u64, - _brand: PhantomData<&'a ()>, // ✅ Covariant — callers can shorten 'a - // _brand: PhantomData, // ❌ Contravariant — breaks ergonomics - // _brand: PhantomData<&'a mut ()>; // ❌ Invariant over () — overly restrictive + _brand: PhantomData<&'a ()>, // ✅ Ковариантна: вызывающий код может сократить 'a + // _brand: PhantomData, // ❌ Контравариантна: ломает эргономику + // _brand: PhantomData<&'a mut ()>; // ❌ Инвариантна по (): слишком ограничительно } fn use_token(token: &SessionToken<'_>) { - println!("Using token {}", token.id); + println!("Используем токен {}", token.id); } fn main() { let token = SessionToken { id: 42, _brand: PhantomData }; - use_token(&token); // ✅ Works because SessionToken is covariant over 'a + use_token(&token); // ✅ Работает, потому что SessionToken ковариантна по 'a } ``` -> **Decision rule**: Start with `PhantomData<&'a T>` (covariant). Switch to -> `PhantomData<&'a mut T>` (invariant) only if your abstraction hands out -> mutable access to `T`. Use `PhantomData` (contravariant) almost -> never — it's only correct for callback-storage scenarios. +> **Правило выбора**: начинайте с `PhantomData<&'a T>` (ковариантна). Переходите к `PhantomData<&'a mut T>` (инвариантна), только если ваша абстракция выдаёт изменяемый доступ к `T`. `PhantomData` (контравариантна) почти никогда не нужна: она корректна только в сценариях хранения callback-функций. -> **Key Takeaways — PhantomData** -> - `PhantomData` carries type/lifetime information without runtime cost -> - Use it for lifetime branding, variance control, and unit-of-measure patterns -> - Drop check: `PhantomData` tells the compiler your type logically owns a `T` +> **Ключевые выводы: PhantomData** +> - `PhantomData` несёт информацию о типе и времени жизни без затрат во время выполнения +> - Используйте его для маркировки времён жизни, управления вариантностью и паттерна единиц измерения +> - Проверка drop: `PhantomData` сообщает компилятору, что ваш тип логически владеет `T` -> **See also:** [Ch 3 — Newtype & Type-State](ch03-the-newtype-and-type-state-patterns.md) for type-state patterns that use PhantomData. [Ch 11 — Unsafe Rust](ch11-unsafe-rust-controlled-danger.md) for how PhantomData interacts with raw pointers. +> **См. также:** [гл. 3 — Newtype и type-state](ch03-the-newtype-and-type-state-patterns.md) — паттерны type-state, которые используют PhantomData. [гл. 12 — Unsafe Rust](ch12-unsafe-rust-controlled-danger.md) — о том, как PhantomData взаимодействует с сырыми указателями. --- -### Exercise: Unit-of-Measure with PhantomData ★★ (~30 min) +### Упражнение: единицы измерения с PhantomData ★★ (~30 минут) -Extend the unit-of-measure pattern to support: +Расширьте паттерн единиц измерения, чтобы он поддерживал: - `Meters`, `Seconds`, `Kilograms` -- Addition of same units -- Multiplication: `Meters * Meters = SquareMeters` -- Division: `Meters / Seconds = MetersPerSecond` +- сложение одинаковых единиц +- умножение: `Meters * Meters = SquareMeters` +- деление: `Meters / Seconds = MetersPerSecond`
-🔑 Solution +🔑 Решение ```rust use std::marker::PhantomData; @@ -374,15 +367,15 @@ fn main() { let width = Qty::::new(5.0); let height = Qty::::new(3.0); let area = width * height; // Qty - println!("Area: {:.1} m²", area.value); + println!("Площадь: {:.1} м²", area.value); let dist = Qty::::new(100.0); let time = Qty::::new(9.58); let speed = dist / time; - println!("Speed: {:.2} m/s", speed.value); + println!("Скорость: {:.2} м/с", speed.value); - let sum = width + height; // Same unit ✅ - println!("Sum: {:.1} m", sum.value); + let sum = width + height; // Одинаковые единицы ✅ + println!("Сумма: {:.1} м", sum.value); // let bad = width + time; // ❌ Compile error: can't add Meters + Seconds } @@ -391,4 +384,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch05-channels-and-message-passing.md b/rust-patterns-book/src/ch05-channels-and-message-passing.md index 8d3c639..a06087f 100644 --- a/rust-patterns-book/src/ch05-channels-and-message-passing.md +++ b/rust-patterns-book/src/ch05-channels-and-message-passing.md @@ -1,14 +1,14 @@ -# 5. Channels and Message Passing 🟢 +# 5. Каналы и передача сообщений 🟢 -> **What you'll learn:** -> - `std::sync::mpsc` basics and when to upgrade to crossbeam-channel -> - Channel selection with `select!` for multi-source message handling -> - Bounded vs unbounded channels and backpressure strategies -> - The actor pattern for encapsulating concurrent state +> **Что вы узнаете:** +> - Основы `std::sync::mpsc` и когда стоит перейти на crossbeam-channel +> - Выбор канала через `select!` для обработки сообщений из нескольких источников +> - Ограниченные и неограниченные каналы и стратегии обратного давления (backpressure) +> - Паттерн актора для инкапсуляции конкурентного состояния -## std::sync::mpsc — The Standard Channel +## std::sync::mpsc: стандартный канал -Rust's standard library provides a multi-producer, single-consumer channel: +Стандартная библиотека Rust предоставляет канал с несколькими отправителями и одним получателем (multi-producer, single-consumer): ```rust use std::sync::mpsc; @@ -16,56 +16,56 @@ use std::thread; use std::time::Duration; fn main() { - // Create a channel: tx (transmitter) and rx (receiver) + // Создаём канал: tx (отправитель) и rx (получатель) let (tx, rx) = mpsc::channel(); - // Spawn a producer thread - let tx1 = tx.clone(); // Clone for multiple producers + // Запускаем поток-производитель + let tx1 = tx.clone(); // Клонируем для нескольких производителей thread::spawn(move || { for i in 0..5 { - tx1.send(format!("producer-1: msg {i}")).unwrap(); + tx1.send(format!("производитель-1: сообщение {i}")).unwrap(); thread::sleep(Duration::from_millis(100)); } }); - // Second producer + // Второй производитель thread::spawn(move || { for i in 0..5 { - tx.send(format!("producer-2: msg {i}")).unwrap(); + tx.send(format!("производитель-2: сообщение {i}")).unwrap(); thread::sleep(Duration::from_millis(150)); } }); - // Consumer: receive all messages + // Потребитель: получает все сообщения for msg in rx { - // rx iterator ends when ALL senders are dropped - println!("Received: {msg}"); + // Итератор rx завершается, когда уничтожены ВСЕ отправители + println!("Получено: {msg}"); } - println!("All producers done."); + println!("Все производители завершили работу."); } ``` -**Key properties**: -- **Unbounded** by default (can fill memory if consumer is slow) -- `mpsc::sync_channel(N)` creates a **bounded** channel with backpressure -- `rx.recv()` blocks the current thread until a message arrives -- `rx.try_recv()` returns immediately with `Err(TryRecvError::Empty)` if nothing is ready -- The channel closes when all `Sender`s are dropped +**Ключевые свойства**: +- **Неограниченный** по умолчанию: может заполнить память, если потребитель медленный +- `mpsc::sync_channel(N)` создаёт **ограниченный** канал с обратным давлением +- `rx.recv()` блокирует текущий поток, пока не придёт сообщение +- `rx.try_recv()` сразу возвращает `Err(TryRecvError::Empty)`, если ничего не готово +- Канал закрывается, когда уничтожены все `Sender` ```rust -// Bounded channel with backpressure: -let (tx, rx) = mpsc::sync_channel(10); // Buffer of 10 messages +// Ограниченный канал с обратным давлением: +let (tx, rx) = mpsc::sync_channel(10); // Буфер на 10 сообщений thread::spawn(move || { for i in 0..1000 { - tx.send(i).unwrap(); // BLOCKS if buffer is full — natural backpressure + tx.send(i).unwrap(); // БЛОКИРУЕТСЯ, если буфер полон: естественное обратное давление } }); ``` -### crossbeam-channel — The Production Workhorse +### crossbeam-channel: рабочая лошадка для продакшена -`crossbeam-channel` is the de facto standard for production channel usage. It's faster than `std::sync::mpsc` and supports multi-consumer (`mpmc`): +`crossbeam-channel` — де-факто стандарт для каналов в продакшене. Он быстрее `std::sync::mpsc` и поддерживает нескольких получателей (`mpmc`): ```rust,ignore // Cargo.toml: @@ -76,30 +76,30 @@ use std::thread; use std::time::Duration; fn main() { - // Bounded MPMC channel + // Ограниченный канал MPMC let (tx, rx) = bounded::(100); - // Multiple producers + // Несколько производителей for id in 0..4 { let tx = tx.clone(); thread::spawn(move || { for i in 0..10 { - tx.send(format!("worker-{id}: item-{i}")).unwrap(); + tx.send(format!("рабочий-{id}: элемент-{i}")).unwrap(); } }); } - drop(tx); // Drop the original sender so the channel can close + drop(tx); // Отбрасываем исходный отправитель, чтобы канал мог закрыться - // Multiple consumers (not possible with std::sync::mpsc!) + // Несколько потребителей (с std::sync::mpsc так нельзя!) let rx2 = rx.clone(); let consumer1 = thread::spawn(move || { while let Ok(msg) = rx.recv() { - println!("[consumer-1] {msg}"); + println!("[потребитель-1] {msg}"); } }); let consumer2 = thread::spawn(move || { while let Ok(msg) = rx2.recv() { - println!("[consumer-2] {msg}"); + println!("[потребитель-2] {msg}"); } }); @@ -108,9 +108,9 @@ fn main() { } ``` -### Channel Selection (select!) +### Выбор канала (select!) -Listen on multiple channels simultaneously — like `select` in Go: +Слушайте несколько каналов одновременно, как `select` в Go: ```rust,ignore use crossbeam_channel::{bounded, tick, after, select}; @@ -118,14 +118,14 @@ use std::time::Duration; fn main() { let (work_tx, work_rx) = bounded::(10); - let ticker = tick(Duration::from_secs(1)); // Periodic tick - let deadline = after(Duration::from_secs(10)); // One-shot timeout + let ticker = tick(Duration::from_secs(1)); // Периодический тик + let deadline = after(Duration::from_secs(10)); // Одноразовый таймаут - // Producer + // Производитель let tx = work_tx.clone(); std::thread::spawn(move || { for i in 0..100 { - tx.send(format!("job-{i}")).unwrap(); + tx.send(format!("задача-{i}")).unwrap(); std::thread::sleep(Duration::from_millis(500)); } }); @@ -135,18 +135,18 @@ fn main() { select! { recv(work_rx) -> msg => { match msg { - Ok(job) => println!("Processing: {job}"), + Ok(job) => println!("Обработка: {job}"), Err(_) => { - println!("Work channel closed"); + println!("Канал задач закрыт"); break; } } }, recv(ticker) -> _ => { - println!("Tick — heartbeat"); + println!("Тик — сигнал жизни"); }, recv(deadline) -> _ => { - println!("Deadline reached — shutting down"); + println!("Достигнут дедлайн — завершение работы"); break; }, } @@ -154,40 +154,38 @@ fn main() { } ``` -> **Go comparison**: This is exactly like Go's `select` statement over channels. -> crossbeam's `select!` macro randomizes order to prevent starvation, just like Go. +> **Сравнение с Go**: это в точности похоже на оператор `select` для каналов в Go. Макрос `select!` из crossbeam, как и в Go, случайным образом выбирает порядок веток, чтобы избежать голодания. -### Bounded vs Unbounded and Backpressure +### Ограниченные и неограниченные каналы и обратное давление -| Type | Behavior When Full | Memory | Use Case | -|------|-------------------|--------|----------| -| **Unbounded** | Never blocks (grows heap) | Unbounded ⚠️ | Rare — only when producer is slower than consumer | -| **Bounded** | `send()` blocks until space | Fixed | Production default — prevents OOM | -| **Rendezvous** (bounded(0)) | `send()` blocks until receiver is ready | None | Synchronization / handoff | +| Тип | Поведение при заполнении | Память | Сценарий использования | +|-----|--------------------------|--------|------------------------| +| **Неограниченный** | Никогда не блокирует (растёт в куче) | Не ограничена ⚠️ | Редко: только когда производитель медленнее потребителя | +| **Ограниченный** | `send()` блокируется, пока не появится место | Фиксирована | Выбор по умолчанию в продакшене: защита от OOM | +| **Рандеву** (bounded(0)) | `send()` блокируется, пока получатель не готов | Нет | Синхронизация и передача управления | ```rust -// Rendezvous channel — zero capacity, direct handoff +// Канал рандеву: нулевая ёмкость, прямая передача let (tx, rx) = crossbeam_channel::bounded(0); -// tx.send(x) blocks until rx.recv() is called, and vice versa. -// This synchronizes the two threads precisely. +// tx.send(x) блокируется, пока не вызван rx.recv(), и наоборот. +// Это точно синхронизирует два потока. ``` -**Rule**: Always use bounded channels in production unless you can prove the -producer will never outpace the consumer. +**Правило**: в продакшене всегда используйте ограниченные каналы, если только вы не можете доказать, что производитель никогда не обгонит потребителя. -### Actor Pattern with Channels +### Паттерн актора на каналах -The actor pattern uses channels to serialize access to mutable state — no mutexes needed: +Паттерн актора использует каналы, чтобы сериализовать доступ к изменяемому состоянию: никакие мьютексы не нужны. ```rust use std::sync::mpsc; use std::thread; -// Messages the actor can receive +// Сообщения, которые может получить актор enum CounterMsg { Increment, Decrement, - Get(mpsc::Sender), // Reply channel + Get(mpsc::Sender), // Канал для ответа } struct CounterActor { @@ -213,7 +211,7 @@ impl CounterActor { } } -// Actor handle — cheap to clone, Send + Sync +// Дескриптор актора: дёшево клонируется, реализует Send + Sync #[derive(Clone)] struct Counter { tx: mpsc::Sender, @@ -239,7 +237,7 @@ impl Counter { fn main() { let counter = Counter::spawn(); - // Multiple threads can safely use the counter — no mutex! + // Несколько потоков могут безопасно использовать счётчик: никакого мьютекса! let handles: Vec<_> = (0..10).map(|_| { let counter = counter.clone(); thread::spawn(move || { @@ -250,32 +248,30 @@ fn main() { }).collect(); for h in handles { h.join().unwrap(); } - println!("Final count: {}", counter.get()); // 10000 + println!("Итоговое значение: {}", counter.get()); // 10000 } ``` -> **When to use actors vs mutexes**: Actors are great when the state has complex -> invariants, operations take a long time, or you want to serialize access -> without thinking about lock ordering. Mutexes are simpler for short critical sections. +> **Когда использовать акторы, а когда мьютексы**: акторы хороши, когда у состояния сложные инварианты, операции занимают много времени или вы хотите сериализовать доступ, не думая о порядке захвата блокировок. Для коротких критических секций мьютексы проще. -> **Key Takeaways — Channels** -> - `crossbeam-channel` is the production workhorse — faster and more feature-rich than `std::sync::mpsc` -> - `select!` replaces complex multi-source polling with declarative channel selection -> - Bounded channels provide natural backpressure; unbounded channels risk OOM +> **Ключевые выводы: каналы** +> - `crossbeam-channel` — рабочая лошадка для продакшена: быстрее и богаче по возможностям, чем `std::sync::mpsc` +> - `select!` заменяет сложный опрос нескольких источников декларативным выбором канала +> - Ограниченные каналы дают естественное обратное давление; неограниченные каналы рискуют привести к OOM -> **See also:** [Ch 6 — Concurrency](ch06-concurrency-vs-parallelism-vs-threads.md) for threads, Mutex, and shared state. [Ch 15 — Async](ch15-asyncawait-essentials.md) for async channels (`tokio::sync::mpsc`). +> **См. также:** [гл. 6 — Конкурентность](ch06-concurrency-vs-parallelism-vs-threads.md) о потоках, Mutex и разделяемом состоянии. [гл. 16 — Async](ch16-asyncawait-essentials.md) о каналах в async (`tokio::sync::mpsc`). --- -### Exercise: Channel-Based Worker Pool ★★★ (~45 min) +### Упражнение: пул воркеров на каналах ★★★ (~45 минут) -Build a worker pool using channels where: -- A dispatcher sends `Job` structs through a channel -- N workers consume jobs and send results back -- Use `std::sync::mpsc` with `Arc>` for work-stealing +Постройте пул воркеров на каналах, где: +- диспетчер отправляет структуры `Job` через канал +- N воркеров забирают задачи и отправляют результаты обратно +- используйте `std::sync::mpsc` с `Arc>` для распределения работы между воркерами (work-stealing)
-🔑 Solution +🔑 Решение ```rust use std::sync::mpsc; @@ -310,7 +306,7 @@ fn worker_pool(jobs: Vec, num_workers: usize) -> Vec { }; match job { Ok(job) => { - let output = format!("processed '{}' by worker {worker_id}", job.data); + let output = format!("обработано '{}' воркером {worker_id}", job.data); result_tx.send(JobResult { job_id: job.id, output, worker_id, }).unwrap(); @@ -337,12 +333,12 @@ fn worker_pool(jobs: Vec, num_workers: usize) -> Vec { fn main() { let jobs: Vec = (0..20).map(|i| Job { - id: i, data: format!("task-{i}"), + id: i, data: format!("задача-{i}"), }).collect(); let results = worker_pool(jobs, 4); for r in &results { - println!("[worker {}] job {}: {}", r.worker_id, r.job_id, r.output); + println!("[воркер {}] задача {}: {}", r.worker_id, r.job_id, r.output); } } ``` @@ -350,4 +346,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch06-concurrency-vs-parallelism-vs-threads.md b/rust-patterns-book/src/ch06-concurrency-vs-parallelism-vs-threads.md index 9c51208..7273380 100644 --- a/rust-patterns-book/src/ch06-concurrency-vs-parallelism-vs-threads.md +++ b/rust-patterns-book/src/ch06-concurrency-vs-parallelism-vs-threads.md @@ -1,82 +1,82 @@ -# 6. Concurrency vs Parallelism vs Threads 🟡 +# 6. Конкурентность, параллелизм и потоки 🟡 -> **What you'll learn:** -> - The precise distinction between concurrency and parallelism -> - OS threads, scoped threads, and rayon for data parallelism -> - Shared state primitives: Arc, Mutex, RwLock, Atomics, Condvar -> - Lazy initialization with OnceLock/LazyLock and lock-free patterns +> **Что вы узнаете:** +> - Точное различие между конкурентностью и параллелизмом +> - Потоки ОС, scoped-потоки и rayon для параллелизма по данным +> - Примитивы разделяемого состояния: Arc, Mutex, RwLock, атомарные типы, Condvar +> - Ленивая инициализация через OnceLock/LazyLock и паттерны без блокировок -## Terminology: Concurrency ≠ Parallelism +## Терминология: конкурентность ≠ параллелизм -These terms are often confused. Here is the precise distinction: +Эти термины часто путают. Вот точное различие: -| | Concurrency | Parallelism | +| | Конкурентность | Параллелизм | |---|---|---| -| **Definition** | Managing multiple tasks that can make progress | Executing multiple tasks simultaneously | -| **Hardware requirement** | One core is enough | Requires multiple cores | -| **Analogy** | One cook, multiple dishes (switching between them) | Multiple cooks, each working on a dish | -| **Rust tools** | `async/await`, channels, `select!` | `rayon`, `thread::spawn`, `par_iter()` | +| **Определение** | Управление несколькими задачами, которые могут продвигаться вперёд | Одновременное выполнение нескольких задач | +| **Требование к железу** | Достаточно одного ядра | Нужно несколько ядер | +| **Аналогия** | Один повар, много блюд (переключается между ними) | Несколько поваров, каждый готовит своё блюдо | +| **Инструменты Rust** | `async/await`, каналы, `select!` | `rayon`, `thread::spawn`, `par_iter()` | ```text -Concurrency (single core): Parallelism (multi-core): - -Task A: ██░░██░░██ Task A: ██████████ -Task B: ░░██░░██░░ Task B: ██████████ -─────────────────→ time ─────────────────→ time -(interleaved on one core) (simultaneous on two cores) +Конкурентность (одно ядро): Параллелизм (несколько ядер): + +Задача A: ██░░██░░██ Задача A: ██████████ +Задача B: ░░██░░██░░ Задача B: ██████████ +─────────────────→ время ─────────────────→ время +(чередуются на одном ядре) (выполняются одновременно на двух ядрах) ``` -### std::thread — OS Threads +### std::thread: потоки ОС -Rust threads map 1:1 to OS threads. Each gets its own stack (typically 2-8 MB): +Потоки Rust соответствуют потокам ОС один к одному. У каждого из них свой стек (обычно 2–8 МБ): ```rust use std::thread; use std::time::Duration; fn main() { - // Spawn a thread — takes a closure + // Запускаем поток: принимает замыкание let handle = thread::spawn(|| { for i in 0..5 { - println!("spawned thread: {i}"); + println!("порождённый поток: {i}"); thread::sleep(Duration::from_millis(100)); } - 42 // Return value + 42 // Возвращаемое значение }); - // Do work on the main thread simultaneously + // Одновременно работаем в главном потоке for i in 0..3 { - println!("main thread: {i}"); + println!("главный поток: {i}"); thread::sleep(Duration::from_millis(150)); } - // Wait for the thread to finish and get its return value - let result = handle.join().unwrap(); // unwrap panics if thread panicked - println!("Thread returned: {result}"); + // Ждём завершения потока и получаем его возвращаемое значение + let result = handle.join().unwrap(); // unwrap запаникует, если поток упал + println!("Поток вернул: {result}"); } ``` -**Thread::spawn type requirements**: +**Требования к типам в `Thread::spawn`**: ```rust -// The closure must be: -// 1. Send — can be transferred to another thread -// 2. 'static — can't borrow from the calling scope -// 3. FnOnce — takes ownership of captured variables +// Замыкание должно быть: +// 1. Send: может быть передано в другой поток +// 2. 'static: не может заимствовать данные из вызывающей области видимости +// 3. FnOnce: забирает владение захваченными переменными let data = vec![1, 2, 3]; -// ❌ Borrows data — not 'static +// ❌ Заимствует data — не 'static // thread::spawn(|| println!("{data:?}")); -// ✅ Move ownership into the thread +// ✅ Передаём владение в поток thread::spawn(move || println!("{data:?}")); -// data is no longer accessible here +// data здесь больше недоступна ``` -### Scoped Threads (std::thread::scope) +### Scoped-потоки (std::thread::scope) -Since Rust 1.63, scoped threads solve the `'static` requirement — threads can borrow from the parent scope: +Начиная с Rust 1.63, scoped-потоки снимают требование `'static`: потоки могут заимствовать данные из родительской области видимости: ```rust use std::thread; @@ -85,36 +85,34 @@ fn main() { let mut data = vec![1, 2, 3, 4, 5]; thread::scope(|s| { - // Thread 1: borrow shared reference + // Поток 1: заимствуем разделяемую ссылку s.spawn(|| { let sum: i32 = data.iter().sum(); - println!("Sum: {sum}"); + println!("Сумма: {sum}"); }); - // Thread 2: also borrow shared reference (multiple readers OK) + // Поток 2: тоже заимствует разделяемую ссылку (несколько читателей — это нормально) s.spawn(|| { let max = data.iter().max().unwrap(); - println!("Max: {max}"); + println!("Максимум: {max}"); }); - // ❌ Can't mutably borrow while shared borrows exist: + // ❌ Нельзя взять изменяемую ссылку, пока существуют разделяемые заимствования: // s.spawn(|| data.push(6)); }); - // ALL scoped threads joined here — guaranteed before scope returns + // ВСЕ scoped-потоки завершаются здесь: гарантированно до возврата из scope - // Now safe to mutate — all threads have finished + // Теперь безопасно менять данные: все потоки завершены data.push(6); - println!("Updated: {data:?}"); + println!("Обновлено: {data:?}"); } ``` -> **This is huge**: Before scoped threads, you had to `Arc::clone()` everything -> to share with threads. Now you can borrow directly, and the compiler proves -> all threads finish before the data goes out of scope. +> **Это большое улучшение**: до scoped-потоков всё, что нужно было разделить с потоками, приходилось клонировать через `Arc::clone()`. Теперь можно заимствовать данные напрямую, а компилятор доказывает, что все потоки завершатся до того, как данные выйдут из области видимости. -### rayon — Data Parallelism +### rayon: параллелизм по данным -`rayon` provides parallel iterators that distribute work across a thread pool automatically: +`rayon` предоставляет параллельные итераторы, которые автоматически распределяют работу по пулу потоков: ```rust,ignore // Cargo.toml: rayon = "1" @@ -123,19 +121,19 @@ use rayon::prelude::*; fn main() { let data: Vec = (0..1_000_000).collect(); - // Sequential: + // Последовательно: let sum_seq: u64 = data.iter().map(|x| x * x).sum(); - // Parallel — just change .iter() to .par_iter(): + // Параллельно: достаточно заменить .iter() на .par_iter() let sum_par: u64 = data.par_iter().map(|x| x * x).sum(); assert_eq!(sum_seq, sum_par); - // Parallel sort: + // Параллельная сортировка: let mut numbers = vec![5, 2, 8, 1, 9, 3]; numbers.par_sort(); - // Parallel processing with map/filter/collect: + // Параллельная обработка через map/filter/collect: let results: Vec<_> = data .par_iter() .filter(|&&x| x % 2 == 0) @@ -144,30 +142,30 @@ fn main() { } fn expensive_computation(x: u64) -> u64 { - // Simulate CPU-heavy work + // Имитация вычислений, требующих много ресурсов CPU (0..1000).fold(x, |acc, _| acc.wrapping_mul(7).wrapping_add(13)) } ``` -**When to use rayon vs threads**: +**Когда использовать rayon, а когда потоки**: -| Use | When | -|-----|------| -| `rayon::par_iter()` | Processing collections in parallel (map, filter, reduce) | -| `thread::spawn` | Long-running background tasks, I/O workers | -| `thread::scope` | Short-lived parallel tasks that borrow local data | -| `async` + `tokio` | I/O-bound concurrency (networking, file I/O) | +| Инструмент | Когда | +|------------|-------| +| `rayon::par_iter()` | Параллельная обработка коллекций (map, filter, reduce) | +| `thread::spawn` | Долгоживущие фоновые задачи, рабочие потоки для I/O | +| `thread::scope` | Короткоживущие параллельные задачи, которые заимствуют локальные данные | +| `async` + `tokio` | Конкурентность, ограниченная I/O (сеть, файловый ввод-вывод) | -### Shared State: Arc, Mutex, RwLock, Atomics +### Разделяемое состояние: Arc, Mutex, RwLock, атомарные типы -When threads need shared mutable state, Rust provides safe abstractions: +Когда потокам нужно разделяемое изменяемое состояние, Rust предоставляет безопасные абстракции: ```rust use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicU64, Ordering}; use std::thread; -// --- Arc>: Shared + Exclusive access --- +// --- Arc>: разделяемый и эксклюзивный доступ --- fn mutex_example() { let counter = Arc::new(Mutex::new(0u64)); let mut handles = vec![]; @@ -178,28 +176,28 @@ fn mutex_example() { for _ in 0..1000 { let mut guard = counter.lock().unwrap(); *guard += 1; - } // Guard dropped → lock released + } // Защитник уничтожен → блокировка снята })); } for h in handles { h.join().unwrap(); } - println!("Counter: {}", counter.lock().unwrap()); // 10000 + println!("Счётчик: {}", counter.lock().unwrap()); // 10000 } -// --- Arc>: Multiple readers OR one writer --- +// --- Arc>: несколько читателей ИЛИ один писатель --- fn rwlock_example() { let config = Arc::new(RwLock::new(String::from("initial"))); - // Many readers — don't block each other + // Много читателей: не блокируют друг друга let readers: Vec<_> = (0..5).map(|id| { let config = Arc::clone(&config); thread::spawn(move || { let guard = config.read().unwrap(); - println!("Reader {id}: {guard}"); + println!("Читатель {id}: {guard}"); }) }).collect(); - // Writer — blocks and waits for all readers to finish + // Писатель: ждёт, пока завершатся все читатели { let mut guard = config.write().unwrap(); *guard = "updated".to_string(); @@ -208,7 +206,7 @@ fn rwlock_example() { for r in readers { r.join().unwrap(); } } -// --- Atomics: Lock-free for simple values --- +// --- Атомарные типы: без блокировок для простых значений --- fn atomic_example() { let counter = Arc::new(AtomicU64::new(0)); let mut handles = vec![]; @@ -218,29 +216,28 @@ fn atomic_example() { handles.push(thread::spawn(move || { for _ in 0..1000 { counter.fetch_add(1, Ordering::Relaxed); - // No lock, no mutex — hardware atomic instruction + // Без блокировки и мьютекса: аппаратная атомарная инструкция } })); } for h in handles { h.join().unwrap(); } - println!("Atomic counter: {}", counter.load(Ordering::Relaxed)); // 10000 + println!("Атомарный счётчик: {}", counter.load(Ordering::Relaxed)); // 10000 } ``` -### Quick Comparison +### Краткое сравнение -| Primitive | Use Case | Cost | Contention | -|-----------|----------|------|------------| -| `Mutex` | Short critical sections | Lock + unlock | Threads wait in line | -| `RwLock` | Read-heavy, rare writes | Reader-writer lock | Readers concurrent, writer exclusive | -| `AtomicU64` etc. | Counters, flags | Hardware CAS | Lock-free — no waiting | -| Channels | Message passing | Queue ops | Producer/consumer decouple | +| Примитив | Сценарий | Стоимость | Конкуренция | +|----------|----------|-----------|-------------| +| `Mutex` | Короткие критические секции | Захват и освобождение | Потоки стоят в очереди | +| `RwLock` | Много чтений, редкая запись | Блокировка «читатель — писатель» | Читатели работают одновременно, писатель эксклюзивен | +| `AtomicU64` и др. | Счётчики, флаги | Аппаратный CAS | Без блокировок, без ожидания | +| Каналы | Передача сообщений | Операции с очередью | Производитель и потребитель разделены | -### Condition Variables (`Condvar`) +### Условные переменные (`Condvar`) -A `Condvar` lets a thread **wait** until another thread signals that a condition is -true, without busy-looping. It is always paired with a `Mutex`: +`Condvar` позволяет потоку **ждать**, пока другой поток не сообщит, что условие выполнено, без активного опроса (busy-loop). Он всегда используется вместе с `Mutex`: ```rust use std::sync::{Arc, Mutex, Condvar}; @@ -249,89 +246,83 @@ use std::thread; let pair = Arc::new((Mutex::new(false), Condvar::new())); let pair2 = Arc::clone(&pair); -// Spawned thread: wait until ready == true +// Порождённый поток: ждём, пока ready == true let handle = thread::spawn(move || { let (lock, cvar) = &*pair2; let mut ready = lock.lock().unwrap(); while !*ready { - ready = cvar.wait(ready).unwrap(); // atomically unlocks + sleeps + ready = cvar.wait(ready).unwrap(); // атомарно снимает блокировку и засыпает } - println!("Worker: condition met, proceeding"); + println!("Рабочий: условие выполнено, продолжаем"); }); -// Main thread: set ready = true, then signal +// Главный поток: выставляем ready = true и сигнализируем { let (lock, cvar) = &*pair; let mut ready = lock.lock().unwrap(); *ready = true; - cvar.notify_one(); // wake one waiting thread (use notify_all for many) + cvar.notify_one(); // будим один ждущий поток (для многих используйте notify_all) } handle.join().unwrap(); ``` -> **Pattern**: Always re-check the condition in a `while` loop after `wait()` returns -> — spurious wakeups are allowed by the OS. +> **Шаблон**: после возврата из `wait()` всегда перепроверяйте условие в цикле `while`, потому что ОС допускает ложные пробуждения (spurious wakeups). -### Lazy Initialization: OnceLock and LazyLock +### Ленивая инициализация: OnceLock и LazyLock -Before Rust 1.80, initializing a global static that requires runtime computation -(e.g., parsing a config, compiling a regex) needed the `lazy_static!` macro or the -`once_cell` crate. The standard library now provides two types that cover these -use cases natively: +До Rust 1.80 для инициализации глобальной `static`-переменной, которой нужны вычисления во время выполнения (например, разбор конфигурации или компиляция регулярного выражения), требовался макрос `lazy_static!` или крейт `once_cell`. Теперь стандартная библиотека предоставляет два типа, которые покрывают эти сценарии без внешних зависимостей: ```rust use std::sync::{OnceLock, LazyLock}; use std::collections::HashMap; -// OnceLock — initialize on first use via `get_or_init`. -// Useful when the init value depends on runtime arguments. +// OnceLock: инициализация при первом использовании через `get_or_init`. +// Полезно, когда начальное значение зависит от аргументов времени выполнения. static CONFIG: OnceLock> = OnceLock::new(); fn get_config() -> &'static HashMap { CONFIG.get_or_init(|| { - // Expensive: read & parse config file — happens exactly once. + // Дорогая операция: читаем и разбираем файл конфигурации. Выполняется ровно один раз. let mut m = HashMap::new(); m.insert("log_level".into(), "info".into()); m }) } -// LazyLock — initialize on first access, closure provided at definition site. -// Equivalent to lazy_static! but without a macro. +// LazyLock: инициализация при первом обращении, замыкание задаётся в месте определения. +// Эквивалент lazy_static!, но без макроса. static REGEX: LazyLock = LazyLock::new(|| { regex::Regex::new(r"^[a-zA-Z0-9_]+$").unwrap() }); fn is_valid_identifier(s: &str) -> bool { - REGEX.is_match(s) // First call compiles the regex; subsequent calls reuse it. + REGEX.is_match(s) // Первый вызов компилирует регулярное выражение; последующие переиспользуют его. } ``` -| Type | Stabilized | Init Timing | Use When | -|------|-----------|-------------|----------| -| `OnceLock` | Rust 1.70 | Call-site (`get_or_init`) | Init depends on runtime args | -| `LazyLock` | Rust 1.80 | Definition-site (closure) | Init is self-contained | -| `lazy_static!` | — | Definition-site (macro) | Pre-1.80 codebases (migrate away) | -| `const fn` + `static` | Always | Compile-time | Value is computable at compile time | +| Тип | Стабилизирован | Момент инициализации | Когда использовать | +|-----|----------------|----------------------|--------------------| +| `OnceLock` | Rust 1.70 | В месте вызова (`get_or_init`) | Инициализация зависит от аргументов времени выполнения | +| `LazyLock` | Rust 1.80 | В месте определения (замыкание) | Инициализация самодостаточна | +| `lazy_static!` | — | В месте определения (макрос) | Кодовые базы до 1.80 (лучше мигрировать) | +| `const fn` + `static` | Всегда | На этапе компиляции | Значение вычисляется на этапе компиляции | -> **Migration tip**: Replace `lazy_static! { static ref X: T = expr; }` with -> `static X: LazyLock = LazyLock::new(|| expr);` — same semantics, no macro, -> no external dependency. +> **Совет по миграции**: замените `lazy_static! { static ref X: T = expr; }` на `static X: LazyLock = LazyLock::new(|| expr);`. Семантика та же, но без макроса и без внешней зависимости. -### Lock-Free Patterns +### Паттерны без блокировок -For high-performance code, avoid locks entirely: +Для высокопроизводительного кода полностью избегайте блокировок: ```rust use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; -// Pattern 1: Spin lock (educational — prefer std::sync::Mutex) -// ⚠️ WARNING: This is a teaching example only. Real spinlocks need: -// - A RAII guard (so a panic while holding doesn't deadlock forever) -// - Fairness guarantees (this starves under contention) -// - Backoff strategies (exponential backoff, yield to OS) -// Use std::sync::Mutex or parking_lot::Mutex in production. +// Паттерн 1: спин-лок (учебный пример, в продакшене используйте std::sync::Mutex) +// ⚠️ ВНИМАНИЕ: это только учебный пример. Настоящим спин-локам нужны: +// - RAII-защитник (чтобы паника во время удержания не привела к вечной блокировке) +// - Гарантии справедливости (при конкуренции этот вариант приводит к голоданию) +// - Стратегии отката (экспоненциальная задержка, передача управления ОС) +// В продакшене используйте std::sync::Mutex или parking_lot::Mutex. struct SpinLock { locked: AtomicBool, } @@ -344,7 +335,7 @@ impl SpinLock { .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed) .is_err() { - std::hint::spin_loop(); // CPU hint: we're spinning + std::hint::spin_loop(); // Подсказка процессору: мы крутимся в цикле } } @@ -353,12 +344,13 @@ impl SpinLock { } } -// Pattern 2: Lock-free SPSC (single producer, single consumer) -// Use crossbeam::queue::ArrayQueue or similar in production -// roll-your-own only for learning. +// Паттерн 2: lock-free SPSC (один производитель, один потребитель) +// В продакшене используйте crossbeam::queue::ArrayQueue или аналог; +// писать свою реализацию стоит только для обучения. -// Pattern 3: Sequence counter for wait-free reads -// ⚠️ Best for single-machine-word types (u64, f64); wider T may tear on read. +// Паттерн 3: счётчик последовательности для чтений без ожидания (wait-free) +// ⚠️ Лучше всего для типов, помещающихся в одно машинное слово (u64, f64). +// Более широкие T могут быть прочитаны «порванными» (tearing). struct SeqLock { seq: AtomicUsize, data: std::cell::UnsafeCell, @@ -377,80 +369,68 @@ impl SeqLock { fn read(&self) -> T { loop { let s1 = self.seq.load(Ordering::Acquire); - if s1 & 1 != 0 { continue; } // Writer in progress, retry + if s1 & 1 != 0 { continue; } // Записывающий поток в процессе, повторяем - // SAFETY: We use ptr::read_volatile to prevent the compiler from - // reordering or caching the read. The SeqLock protocol (checking - // s1 == s2 after reading) ensures we retry if a writer was active. - // This mirrors the C SeqLock pattern where the data read must use - // volatile/relaxed semantics to avoid tearing under concurrency. + // SAFETY: используем ptr::read_volatile, чтобы компилятор не переупорядочивал + // и не кэшировал чтение. Протокол SeqLock (проверка s1 == s2 после чтения) + // гарантирует повтор, если писатель был активен. + // Это повторяет паттерн C SeqLock, где чтение данных должно использовать + // семантику volatile/relaxed, чтобы избежать «разрыва» значения при конкуренции. let value = unsafe { core::ptr::read_volatile(self.data.get() as *const T) }; - // Acquire fence: ensures the data read above is ordered before - // we re-check the sequence counter. + // Acquire-барьер: гарантирует, что чтение данных выше упорядочено раньше + // повторной проверки счётчика последовательности. std::sync::atomic::fence(Ordering::Acquire); let s2 = self.seq.load(Ordering::Relaxed); - if s1 == s2 { return value; } // No writer intervened - // else retry + if s1 == s2 { return value; } // Писатель не вмешался + // иначе повторяем } } - /// # Safety contract - /// Only ONE thread may call `write()` at a time. If multiple writers - /// are needed, wrap the `write()` call in an external `Mutex`. + /// # Контракт безопасности + /// Только ОДИН поток может вызывать `write()` одновременно. Если нужны несколько писателей, + /// оберните вызов `write()` во внешний `Mutex`. fn write(&self, val: T) { - // Increment to odd (signals write in progress). - // AcqRel: the Acquire side prevents the subsequent data write - // from being reordered before this increment (readers must see - // odd before they could observe a partial write). The Release - // side is technically unnecessary for a single writer but - // harmless and consistent. + // Увеличиваем до нечётного значения (сигнал: запись в процессе). + // AcqRel: сторона Acquire не даёт последующей записи данных + // переупорядочиться раньше этого инкремента (читатели должны увидеть + // нечётное значение до того, как смогут наблюдать частичную запись). Сторона + // Release формально не нужна для единственного писателя, но безвредна и согласована. self.seq.fetch_add(1, Ordering::AcqRel); unsafe { *self.data.get() = val; } - // Increment to even (signals write complete). - // Release: ensure the data write is visible before readers see the even seq. + // Увеличиваем до чётного значения (сигнал: запись завершена). + // Release: гарантирует, что запись данных видна раньше, чем читатели увидят чётный номер. self.seq.fetch_add(1, Ordering::Release); } } ``` -> **⚠️ Rust memory model caveat**: The non-atomic write through `UnsafeCell` in -> `write()` concurrent with the non-atomic `ptr::read_volatile` in `read()` is -> technically a data race under the Rust abstract machine — even though the -> SeqLock protocol ensures readers always retry on stale data. This mirrors the -> C kernel SeqLock pattern and is sound in practice on all modern hardware for -> types `T` that fit in a single machine word (e.g., `u64`). For wider types, -> consider using `AtomicU64` for the data field or wrapping access in a `Mutex`. -> See [the Rust unsafe code guidelines](https://rust-lang.github.io/unsafe-code-guidelines/) -> for the evolving story on `UnsafeCell` concurrency. - -> **Practical advice**: Lock-free code is hard to get right. Use `Mutex` or -> `RwLock` unless profiling shows lock contention is your bottleneck. When you -> do need lock-free, reach for proven crates (`crossbeam`, `arc-swap`, `dashmap`) -> rather than rolling your own. - -> **Key Takeaways — Concurrency** -> - Scoped threads (`thread::scope`) let you borrow stack data without `Arc` -> - `rayon::par_iter()` parallelizes iterators with one method call -> - Use `OnceLock`/`LazyLock` instead of `lazy_static!`; use `Mutex` before reaching for atomics -> - Lock-free code is hard — prefer proven crates over hand-rolled implementations - -> **See also:** [Ch 5 — Channels](ch05-channels-and-message-passing.md) for message-passing concurrency. [Ch 8 — Smart Pointers](ch08-smart-pointers-and-interior-mutability.md) for Arc/Rc details. +> **⚠️ Оговорка о модели памяти Rust**: неатомарная запись через `UnsafeCell` в `write()`, выполняемая одновременно с неатомарным `ptr::read_volatile` в `read()`, формально является гонкой данных (data race) в абстрактной машине Rust, даже несмотря на то, что протокол SeqLock гарантирует повтор читателями при устаревших данных. Это повторяет паттерн SeqLock из ядра C и на всех современных процессорах корректно на практике для типов `T`, помещающихся в одно машинное слово (например, `u64`). Для более широких типов рассмотрите `AtomicU64` для поля данных или оберните доступ в `Mutex`. Актуальные обсуждения по `UnsafeCell` в конкурентности см. в [рекомендациях Rust по небезопасному коду](https://rust-lang.github.io/unsafe-code-guidelines/). + +> **Практический совет**: код без блокировок трудно написать правильно. Используйте `Mutex` или `RwLock`, если профилирование не показывает, что узкое место именно в конкуренции за блокировки. Если блокировки без них действительно не обойтись, берите проверенные крейты (`crossbeam`, `arc-swap`, `dashmap`), а не пишите собственные реализации. + +> **Ключевые выводы: конкурентность** +> - Scoped-потоки (`thread::scope`) позволяют заимствовать данные со стека без `Arc` +> - `rayon::par_iter()` распараллеливает итераторы одним вызовом метода +> - Используйте `OnceLock`/`LazyLock` вместо `lazy_static!`; берите `Mutex` раньше, чем атомарные типы +> - Код без блокировок сложен: предпочитайте проверенные крейты собственным реализациям + +> **См. также:** [гл. 5 — Каналы](ch05-channels-and-message-passing.md) о конкурентности через передачу сообщений. [гл. 9 — Умные указатели](ch09-smart-pointers-and-interior-mutability.md) о подробностях Arc и Rc. ```mermaid flowchart TD - A["Need shared
mutable state?"] -->|Yes| B{"How much
contention?"} - A -->|No| C["Use channels
(Ch 5)"] + A["Нужно разделяемое
изменяемое состояние?"] -->|Да| B{"Насколько велика
конкуренция?"} + A -->|Нет| C["Используйте каналы
(гл. 5)"] - B -->|"Read-heavy"| D["RwLock"] - B -->|"Short critical
section"| E["Mutex"] - B -->|"Simple counter
or flag"| F["Atomics"] - B -->|"Complex state"| G["Actor + channels"] + B -->|"Много чтений"| D["RwLock"] + B -->|"Короткая критическая
секция"| E["Mutex"] + B -->|"Простой счётчик
или флаг"| F["Атомарные типы"] + B -->|"Сложное состояние"| G["Актор + каналы"] - H["Need parallelism?"] -->|"Collection
processing"| I["rayon::par_iter"] - H -->|"Background task"| J["thread::spawn"] - H -->|"Borrow local data"| K["thread::scope"] + H["Нужен параллелизм?"] -->|"Обработка
коллекций"| I["rayon::par_iter"] + H -->|"Фоновая задача"| J["thread::spawn"] + H -->|"Заимствовать
локальные данные"| K["thread::scope"] style A fill:#e8f4f8,stroke:#2980b9,color:#000 style B fill:#fef9e7,stroke:#f1c40f,color:#000 @@ -467,12 +447,12 @@ flowchart TD --- -### Exercise: Parallel Map with Scoped Threads ★★ (~25 min) +### Упражнение: параллельный map на scoped-потоках ★★ (~25 минут) -Write a function `parallel_map(data: &[T], f: fn(&T) -> R, num_threads: usize) -> Vec` that splits `data` into `num_threads` chunks and processes each in a scoped thread. Do not use `rayon` — use `std::thread::scope`. +Напишите функцию `parallel_map(data: &[T], f: fn(&T) -> R, num_threads: usize) -> Vec`, которая делит `data` на `num_threads` частей и обрабатывает каждую часть в scoped-потоке. Не используйте `rayon`, используйте `std::thread::scope`.
-🔑 Solution +🔑 Решение ```rust fn parallel_map(data: &[T], f: fn(&T) -> R, num_threads: usize) -> Vec { @@ -498,11 +478,10 @@ fn main() { let data: Vec = (1..=20).collect(); let squares = parallel_map(&data, |x| x * x, 4); assert_eq!(squares, (1..=20).map(|x: u64| x * x).collect::>()); - println!("Parallel squares: {squares:?}"); + println!("Параллельные квадраты: {squares:?}"); } ```
*** - diff --git a/rust-patterns-book/src/ch07-closures-and-higher-order-functions.md b/rust-patterns-book/src/ch07-closures-and-higher-order-functions.md index 9ef8d44..8be6189 100644 --- a/rust-patterns-book/src/ch07-closures-and-higher-order-functions.md +++ b/rust-patterns-book/src/ch07-closures-and-higher-order-functions.md @@ -1,82 +1,82 @@ -# 7. Closures and Higher-Order Functions 🟢 +# 7. Замыкания и функции высшего порядка 🟢 -> **What you'll learn:** -> - The three closure traits (`Fn`, `FnMut`, `FnOnce`) and how capture works -> - Passing closures as parameters and returning them from functions -> - Combinator chains and iterator adapters for functional-style programming -> - Designing your own higher-order APIs with the right trait bounds +> **Что вы узнаете:** +> - Три трейта замыканий (`Fn`, `FnMut`, `FnOnce`) и то, как устроен захват переменных +> - Передачу замыканий в качестве параметров и возврат их из функций +> - Цепочки комбинаторов и адаптеры итераторов для функционального стиля +> - Проектирование собственных API высшего порядка с правильными границами трейтов -## Fn, FnMut, FnOnce — The Closure Traits +## Fn, FnMut, FnOnce: трейты замыканий -Every closure in Rust implements one or more of three traits, based on how it captures variables: +Каждое замыкание в Rust реализует один или несколько из трёх трейтов в зависимости от того, как оно захватывает переменные: ```rust -// FnOnce — consumes captured values (can only be called once) +// FnOnce: потребляет захваченные значения (вызвать можно только один раз) let name = String::from("Alice"); let greet = move || { - println!("Hello, {name}!"); // Takes ownership of `name` - drop(name); // name is consumed + println!("Hello, {name}!"); // Забирает владение `name` + drop(name); // name потреблено }; -greet(); // ✅ First call -// greet(); // ❌ Can't call again — `name` was consumed +greet(); // ✅ Первый вызов +// greet(); // ❌ Нельзя вызвать снова: `name` уже потреблено -// FnMut — mutably borrows captured values (can be called many times) +// FnMut: изменяемо заимствует захваченные значения (можно вызывать многократно) let mut count = 0; let mut increment = || { - count += 1; // Mutably borrows `count` + count += 1; // Изменяемо заимствует `count` }; increment(); // count == 1 increment(); // count == 2 -// Fn — immutably borrows captured values (can be called many times, concurrently) +// Fn: неизменяемо заимствует захваченные значения (можно вызывать многократно и одновременно) let prefix = "Result"; let display = |x: i32| { - println!("{prefix}: {x}"); // Immutably borrows `prefix` + println!("{prefix}: {x}"); // Неизменяемо заимствует `prefix` }; display(1); display(2); ``` -**The hierarchy**: `Fn` : `FnMut` : `FnOnce` — each is a subtrait of the next: +**Иерархия**: `Fn` : `FnMut` : `FnOnce`, каждый из них является подтрейтом следующего: ```text -FnOnce ← everything can be called at least once +FnOnce ← любое замыкание можно вызвать хотя бы один раз ↑ -FnMut ← can be called repeatedly (may mutate state) +FnMut ← можно вызывать многократно (может изменять состояние) ↑ -Fn ← can be called repeatedly and concurrently (no mutation) +Fn ← можно вызывать многократно и одновременно (без изменения состояния) ``` -If a closure implements `Fn`, it also implements `FnMut` and `FnOnce`. +Если замыкание реализует `Fn`, то оно реализует также `FnMut` и `FnOnce`. -### Closures as Parameters and Return Values +### Замыкания как параметры и возвращаемые значения ```rust -// --- Parameters --- +// --- Параметры --- -// Static dispatch (monomorphized — fastest) +// Статическая диспетчеризация (мономорфизация, самая быстрая) fn apply_twice i32>(f: F, x: i32) -> i32 { f(f(x)) } -// Also written with impl Trait: +// То же, записанное через impl Trait: fn apply_twice_v2(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(f(x)) } -// Dynamic dispatch (trait object — flexible, slight overhead) +// Динамическая диспетчеризация (трейт-объект, гибко, с небольшими накладными расходами) fn apply_dyn(f: &dyn Fn(i32) -> i32, x: i32) -> i32 { f(x) } -// --- Return Values --- +// --- Возвращаемые значения --- -// Can't return closures by value without boxing (they have anonymous types): +// Нельзя вернуть замыкание по значению без упаковки в Box (у него анонимный тип): fn make_adder(n: i32) -> Box i32> { Box::new(move |x| x + n) } -// With impl Trait (simpler, monomorphized, but can't be dynamic): +// С impl Trait (проще, мономорфизируется, но динамически использовать нельзя): fn make_adder_v2(n: i32) -> impl Fn(i32) -> i32 { move |x| x + n } @@ -90,12 +90,12 @@ fn main() { } ``` -### Combinator Chains and Iterator Adapters +### Цепочки комбинаторов и адаптеры итераторов -Higher-order functions shine with iterators — this is idiomatic Rust: +Функции высшего порядка особенно хороши с итераторами: это идиоматичный Rust: ```rust -// C-style loop (imperative): +// Императивный цикл в стиле C: let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let mut result = Vec::new(); for x in &data { @@ -104,39 +104,39 @@ for x in &data { } } -// Idiomatic Rust (functional combinator chain): +// Идиоматичный Rust (функциональная цепочка комбинаторов): let result: Vec = data.iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * x) .collect(); -// Same performance — iterators are lazy and optimized by LLVM +// Производительность та же: итераторы ленивые и оптимизируются LLVM assert_eq!(result, vec![4, 16, 36, 64, 100]); ``` -**Common combinators cheat sheet**: +**Шпаргалка по распространённым комбинаторам**: -| Combinator | What It Does | Example | -|-----------|-------------|---------| -| `.map(f)` | Transform each element | `.map(|x| x * 2)` | -| `.filter(p)` | Keep elements where predicate is true | `.filter(|x| x > &5)` | -| `.filter_map(f)` | Map + filter in one step (returns `Option`) | `.filter_map(|x| x.parse().ok())` | -| `.flat_map(f)` | Map then flatten nested iterators | `.flat_map(|s| s.chars())` | -| `.fold(init, f)` | Reduce to single value (like `Aggregate` in C#) | `.fold(0, |acc, x| acc + x)` | -| `.any(p)` / `.all(p)` | Short-circuit boolean check | `.any(|x| x > 100)` | -| `.enumerate()` | Add index | `.enumerate().map(|(i, x)| ...)` | -| `.zip(other)` | Pair with another iterator | `.zip(labels.iter())` | -| `.take(n)` / `.skip(n)` | First/skip N elements | `.take(10)` | -| `.chain(other)` | Concatenate two iterators | `.chain(extra.iter())` | -| `.peekable()` | Look ahead without consuming | `.peek()` | -| `.collect()` | Gather into a collection | `.collect::>()` | +| Комбинатор | Что делает | Пример | +|------------|-----------|--------| +| `.map(f)` | Преобразует каждый элемент | `.map(|x| x * 2)` | +| `.filter(p)` | Оставляет элементы, для которых предикат истинен | `.filter(|x| x > &5)` | +| `.filter_map(f)` | Отображение и фильтрация за один шаг (возвращает `Option`) | `.filter_map(|x| x.parse().ok())` | +| `.flat_map(f)` | Отображение с последующим разворачиванием вложенных итераторов | `.flat_map(|s| s.chars())` | +| `.fold(init, f)` | Свёртка в одно значение (аналог `Aggregate` в C#) | `.fold(0, |acc, x| acc + x)` | +| `.any(p)` / `.all(p)` | Логическая проверка с ранним выходом | `.any(|x| x > 100)` | +| `.enumerate()` | Добавляет индекс | `.enumerate().map(|(i, x)| ...)` | +| `.zip(other)` | Объединяет в пары с другим итератором | `.zip(labels.iter())` | +| `.take(n)` / `.skip(n)` | Первые N элементов / пропуск N элементов | `.take(10)` | +| `.chain(other)` | Конкатенация двух итераторов | `.chain(extra.iter())` | +| `.peekable()` | Заглядывание вперёд без потребления | `.peek()` | +| `.collect()` | Собирает результат в коллекцию | `.collect::>()` | -### Implementing Your Own Higher-Order APIs +### Собственные API высшего порядка -Design APIs that accept closures for customization: +Проектируйте API, которые принимают замыкания для настройки: ```rust -/// Retry an operation with a configurable strategy +/// Повторяет операцию с настраиваемой стратегией fn retry( mut operation: F, mut should_retry: S, @@ -144,7 +144,7 @@ fn retry( ) -> Result where F: FnMut() -> Result, - S: FnMut(&E, usize) -> bool, // (error, attempt) → try again? + S: FnMut(&E, usize) -> bool, // (ошибка, попытка) → повторить ли? { for attempt in 1..=max_attempts { match operation() { @@ -158,7 +158,7 @@ where unreachable!() } -// Usage — caller controls retry logic: +// Использование: вызывающая сторона управляет логикой повторов: ``` ```rust @@ -170,48 +170,41 @@ where let result = retry( || connect_to_database(), |err, attempt| { - eprintln!("Attempt {attempt} failed: {err}"); - true // Always retry + eprintln!("Попытка {attempt} не удалась: {err}"); + true // Всегда повторять }, 3, ); -// Usage — retry only specific errors: +// Использование: повторять только определённые ошибки let result = retry( || http_get(url), - |err, _| err.is_transient(), // Only retry transient errors + |err, _| err.is_transient(), // Повторять только временные ошибки 5, ); ``` -### The `with` Pattern — Bracketed Resource Access +### Паттерн `with`: доступ к ресурсу в скобках -Sometimes you need to guarantee that a resource is in a specific state for the -duration of an operation, and restored afterward — regardless of how the caller's -code exits (early return, `?`, panic). Instead of exposing the resource directly -and hoping callers remember to set up and tear down, **lend it through a closure**: +Иногда нужно гарантировать, что ресурс находится в определённом состоянии на время операции и восстанавливается после неё, независимо от того, как завершился код вызывающей стороны (досрочный выход, `?`, паника). Вместо того чтобы открывать ресурс напрямую и надеяться, что вызывающие не забудут его настроить и откатить, **передайте ресурс через замыкание**: ```text -set up → call closure with resource → tear down +настройка → вызов замыкания с ресурсом → откат ``` -The caller never touches setup or teardown. They can't forget, can't get it wrong, -and can't hold the resource beyond the closure's scope. +Вызывающий код никогда не трогает настройку и откат. Он не может ничего забыть или сделать неправильно, и не может удерживать ресурс за пределами области действия замыкания. -#### Example: GPIO Pin Direction +#### Пример: направление GPIO-пина -A GPIO controller manages pins that support bidirectional I/O. Some callers need -the pin configured as input, others as output. Rather than exposing raw pin access -and trusting callers to set direction correctly, the controller provides -`with_pin_input` and `with_pin_output`: +Контроллер GPIO управляет пинами, которые поддерживают двунаправленный ввод-вывод. Одним вызывающим нужно, чтобы пин был настроен на ввод, другим на вывод. Вместо того чтобы открывать прямой доступ к пину и надеяться, что вызывающие правильно выставят направление, контроллер предоставляет `with_pin_input` и `with_pin_output`: ```rust -/// GPIO pin direction — not public, callers never set this directly. +/// Направление GPIO-пина: не публичное, вызывающие никогда не выставляют его напрямую. #[derive(Debug, Clone, Copy, PartialEq)] enum Direction { In, Out } -/// A GPIO pin handle lent to the closure. Cannot be stored or cloned — -/// it exists only for the duration of the callback. +/// Дескриптор GPIO-пина, переданный в замыкание. Его нельзя сохранить или клонировать: +/// он существует только на время вызова callback-функции. pub struct GpioPin<'a> { pin_number: u8, _controller: &'a GpioController, @@ -219,14 +212,14 @@ pub struct GpioPin<'a> { impl GpioPin<'_> { pub fn read(&self) -> bool { - // Read pin level from hardware register - println!(" reading pin {}", self.pin_number); - true // stub + // Считываем уровень пина из аппаратного регистра + println!(" чтение пина {}", self.pin_number); + true // заглушка } pub fn write(&self, high: bool) { - // Drive pin level via hardware register - println!(" writing pin {} = {high}", self.pin_number); + // Устанавливаем уровень пина через аппаратный регистр + println!(" запись в пин {} = {high}", self.pin_number); } } @@ -241,8 +234,8 @@ impl GpioController { } } - /// Configure pin as input, run the closure, restore state. - /// The caller receives a `GpioPin` that lives only for the callback. + /// Настраиваем пин на ввод, выполняем замыкание, восстанавливаем состояние. + /// Вызывающая сторона получает `GpioPin`, который живёт только на время callback-функции. pub fn with_pin_input( &self, pin: u8, @@ -252,14 +245,14 @@ impl GpioController { self.set_direction(pin, Direction::In); let handle = GpioPin { pin_number: pin, _controller: self }; let result = f(&handle); - // Restore previous direction (or leave as-is — policy choice) + // Восстанавливаем предыдущее направление (или оставляем как есть: вопрос политики) if let Some(dir) = prev { self.set_direction(pin, dir); } result } - /// Configure pin as output, run the closure, restore state. + /// Настраиваем пин на вывод, выполняем замыкание, восстанавливаем состояние. pub fn with_pin_output( &self, pin: u8, @@ -276,7 +269,7 @@ impl GpioController { } fn set_direction(&self, pin: u8, dir: Direction) { - println!(" [hw] pin {pin} → {dir:?}"); + println!(" [hw] пин {pin} → {dir:?}"); self.current_direction.set(Some(dir)); } } @@ -284,95 +277,84 @@ impl GpioController { fn main() { let gpio = GpioController::new(); - // Caller 1: needs input — doesn't know or care how direction is managed + // Вызывающая сторона 1: нужен ввод. Ей не важно и не нужно знать, как управляется направление let level = gpio.with_pin_input(4, |pin| { pin.read() }); - println!("Pin 4 level: {level}"); + println!("Уровень пина 4: {level}"); - // Caller 2: needs output — same API shape, different guarantee + // Вызывающая сторона 2: нужен вывод. Форма API та же, гарантия другая gpio.with_pin_output(4, |pin| { pin.write(true); - // do more work... + // делаем другую работу... pin.write(false); }); - // Can't use the pin handle outside the closure: + // Нельзя использовать дескриптор пина вне замыкания: // let escaped_pin = gpio.with_pin_input(4, |pin| pin); // ❌ ERROR: borrowed value does not live long enough } ``` -**What the `with` pattern guarantees:** -- Direction is **always set before** the caller's code runs -- Direction is **always restored after**, even if the closure returns early -- The `GpioPin` handle **cannot escape** the closure — the borrow checker enforces - this via the lifetime tied to the controller reference -- Callers never import `Direction`, never call `set_direction` — the API is - impossible to misuse - -#### Where This Pattern Appears - -The `with` pattern shows up throughout Rust's standard library and ecosystem: - -| API | Setup | Callback | Teardown | -|-----|-------|----------|----------| -| `std::thread::scope` | Create scope | `\|s\| { s.spawn(...) }` | Join all threads | -| `Mutex::lock` | Acquire lock | Use `MutexGuard` (RAII, not closure, but same idea) | Release on drop | -| `tempfile::tempdir` | Create temp directory | Use path | Delete on drop | -| `std::io::BufWriter::new` | Buffer writes | Write operations | Flush on drop | -| GPIO `with_pin_*` (above) | Set direction | Use pin handle | Restore direction | - -The closure-based variant is strongest when: -- **Setup and teardown are paired** and forgetting either is a bug -- **The resource shouldn't outlive the operation** — the borrow checker enforces - this naturally -- **Multiple configurations exist** (`with_pin_input` vs `with_pin_output`) — each - `with_*` method encapsulates a different setup without exposing the configuration - to the caller - -> **`with` vs RAII (Drop):** Both guarantee cleanup. Use RAII / `Drop` when the -> caller needs to hold the resource across multiple statements and function calls. -> Use `with` when the operation is **bracketed** — one setup, one block of work, -> one teardown — and you don't want the caller to be able to break the bracket. - -> **FnMut vs Fn in API design**: Use `FnMut` as the default bound — it's -> the most flexible (callers can pass `Fn` or `FnMut` closures). Only -> require `Fn` if you need to call the closure concurrently (e.g., from -> multiple threads). Only require `FnOnce` if you call it exactly once. - -> **Key Takeaways — Closures** -> - `Fn` borrows, `FnMut` borrows mutably, `FnOnce` consumes — accept the weakest bound your API needs -> - `impl Fn` in parameters, `Box` for storage, `impl Fn` in return (or `Box` if dynamic) -> - Combinator chains (`map`, `filter`, `and_then`) compose cleanly and inline to tight loops -> - The `with` pattern (bracketed access via closure) guarantees setup/teardown and prevents resource escape — use it when the caller shouldn't manage configuration lifecycle - -> **See also:** [Ch 2 — Traits In Depth](ch02-traits-in-depth.md) for how `Fn`/`FnMut`/`FnOnce` relate to trait objects. [Ch 8 — Functional vs. Imperative](ch08-functional-vs-imperative-when-elegance-wins.md) for when to choose combinators over loops. [Ch 15 — API Design](ch15-crate-architecture-and-api-design.md) for ergonomic parameter patterns. +**Что гарантирует паттерн `with`:** +- Направление **всегда выставляется до** выполнения кода вызывающей стороны +- Направление **всегда восстанавливается после**, даже если замыкание вернуло управление досрочно +- Дескриптор `GpioPin` **не может выйти** за пределы замыкания: заимствующая проверка обеспечивает это через время жизни, привязанное к ссылке на контроллер +- Вызывающим не нужно импортировать `Direction` или вызывать `set_direction`: API невозможно использовать неправильно + +#### Где встречается этот паттерн + +Паттерн `with` встречается по всей стандартной библиотеке Rust и экосистеме: + +| API | Настройка | Замыкание (callback) | Откат | +|-----|-----------|----------------------|-------| +| `std::thread::scope` | Создание области | `\|s\| { s.spawn(...) }` | Ожидание всех потоков | +| `Mutex::lock` | Захват блокировки | Использование `MutexGuard` (RAII, не замыкание, но та же идея) | Освобождение при уничтожении | +| `tempfile::tempdir` | Создание временного каталога | Использование пути | Удаление при уничтожении | +| `std::io::BufWriter::new` | Буферизация записи | Операции записи | Сброс буфера при уничтожении | +| GPIO `with_pin_*` (выше) | Выставление направления | Использование дескриптора пина | Восстановление направления | + +Вариант на основе замыканий наиболее эффективен, когда: +- **настройка и откат идут парами**, и пропуск любого из них является ошибкой +- **ресурс не должен жить дольше операции**: заимствующая проверка обеспечивает это естественным образом +- **существует несколько конфигураций** (`with_pin_input` и `with_pin_output`): каждый метод `with_*` инкапсулирует свою настройку и не раскрывает конфигурацию вызывающей стороне + +> **`with` и RAII (Drop):** оба варианта гарантируют очистку. Используйте RAII / `Drop`, когда вызывающему нужно удерживать ресурс через несколько инструкций и вызовов функций. Используйте `with`, когда операция **заключена в скобки**: одна настройка, один блок работы, один откат, и вы не хотите, чтобы вызывающий мог нарушить эти скобки. + +> **FnMut или Fn в дизайне API**: используйте `FnMut` как границу по умолчанию. Это самый гибкий вариант: вызывающие могут передавать и `Fn`, и `FnMut`. Требуйте `Fn`, только если нужно вызывать замыкание одновременно (например, из нескольких потоков). Требуйте `FnOnce`, только если вызываете его ровно один раз. + +> **Ключевые выводы: замыкания** +> - `Fn` заимствует, `FnMut` заимствует изменяемо, `FnOnce` потребляет: принимайте самую слабую границу, которая нужна вашему API +> - `impl Fn` в параметрах, `Box` для хранения, `impl Fn` в возвращаемом значении (или `Box`, если нужна динамика) +> - Цепочки комбинаторов (`map`, `filter`, `and_then`) чисто компонуются и инлайнятся в плотные циклы +> - Паттерн `with` (доступ в скобках через замыкание) гарантирует настройку и откат и не даёт ресурсу «утечь»: используйте его, когда вызывающий не должен управлять жизненным циклом конфигурации + +> **См. также:** [гл. 2 — Трейты в деталях](ch02-traits-in-depth.md) о том, как `Fn`/`FnMut`/`FnOnce` связаны с трейт-объектами. [гл. 8 — Функциональный и императивный стили](ch08-functional-vs-imperative-when-elegance-wins.md) о том, когда выбирать комбинаторы вместо циклов. [гл. 15 — Дизайн API](ch15-crate-architecture-and-api-design.md) о паттернах эргономичных параметров. ```mermaid graph TD - FnOnce["FnOnce
(can call once)"] - FnMut["FnMut
(can call many times,
may mutate captures)"] - Fn["Fn
(can call many times,
immutable captures)"] + FnOnce["FnOnce
(можно вызвать один раз)"] + FnMut["FnMut
(можно вызывать многократно,
может изменять захваченное)"] + Fn["Fn
(можно вызывать многократно,
захваченное неизменяемо)"] - Fn -->|"implements"| FnMut - FnMut -->|"implements"| FnOnce + Fn -->|"реализует"| FnMut + FnMut -->|"реализует"| FnOnce style Fn fill:#d4efdf,stroke:#27ae60,color:#000 style FnMut fill:#fef9e7,stroke:#f1c40f,color:#000 style FnOnce fill:#fadbd8,stroke:#e74c3c,color:#000 ``` -> Every `Fn` is also `FnMut`, and every `FnMut` is also `FnOnce`. Accept `FnMut` by default — it’s the most flexible bound for callers. +> Каждый `Fn` также является `FnMut`, а каждый `FnMut` также является `FnOnce`. По умолчанию принимайте `FnMut`: это самая гибкая граница для вызывающих. --- -### Exercise: Higher-Order Combinator Pipeline ★★ (~25 min) +### Упражнение: конвейер комбинаторов высшего порядка ★★ (~25 минут) -Create a `Pipeline` struct that chains transformations. It should support `.pipe(f)` to add a transformation and `.execute(input)` to run the full chain. +Создайте структуру `Pipeline`, которая связывает преобразования в цепочку. Она должна поддерживать `.pipe(f)` для добавления преобразования и `.execute(input)` для выполнения всей цепочки.
-🔑 Solution +🔑 Решение ```rust struct Pipeline { @@ -416,4 +398,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch08-functional-vs-imperative-when-elegance-wins.md b/rust-patterns-book/src/ch08-functional-vs-imperative-when-elegance-wins.md index a52839f..01a00e4 100644 --- a/rust-patterns-book/src/ch08-functional-vs-imperative-when-elegance-wins.md +++ b/rust-patterns-book/src/ch08-functional-vs-imperative-when-elegance-wins.md @@ -1,16 +1,16 @@ -# Chapter 8 — Functional vs. Imperative: When Elegance Wins (and When It Doesn't) +# Глава 8 — Функциональный и императивный стили: когда выигрывает изящество (и когда нет) -> **Difficulty:** 🟡 Intermediate | **Time:** 2–3 hours | **Prerequisites:** [Ch 7 — Closures](ch07-closures-and-higher-order-functions.md) +> **Сложность:** 🟡 Средний | **Время:** 2–3 часа | **Предварительные знания:** [гл. 7 — Замыкания](ch07-closures-and-higher-order-functions.md) -Rust gives you genuine parity between functional and imperative styles. Unlike Haskell (functional by fiat) or C (imperative by default), Rust lets you choose — and the right choice depends on what you're expressing. This chapter builds the judgment to pick well. +Rust предоставляет настоящее равноправие функционального и императивного стилей. В отличие от Haskell (функциональный по определению) или C (императивный по умолчанию), Rust позволяет выбирать, и правильный выбор зависит от того, что вы выражаете. Эта глава помогает выработать навык правильного выбора. -**The core principle:** Functional style shines when you're *transforming data through a pipeline*. Imperative style shines when you're *managing state transitions with side effects*. Most real code has both, and the skill is knowing where the boundary falls. +**Основной принцип:** функциональный стиль хорош, когда вы *преобразуете данные через конвейер*. Императивный стиль хорош, когда вы *управляете переходами состояний с побочными эффектами*. В большинстве реального кода есть и то и другое, и мастерство состоит в том, чтобы видеть, где проходит граница. --- -## 8.1 The Combinator You Didn't Know You Wanted +## 8.1 Комбинатор, о котором вы не знали, что он вам нужен -Many Rust developers write this: +Многие разработчики на Rust пишут так: ```rust let value = if let Some(x) = maybe_config() { @@ -21,13 +21,13 @@ let value = if let Some(x) = maybe_config() { process(value); ``` -When they could write this: +Хотя могли бы написать так: ```rust process(maybe_config().unwrap_or_else(default_config)); ``` -Or this common pattern: +Или вот такой распространённый шаблон: ```rust let display_name = if let Some(name) = user.nickname() { @@ -37,7 +37,7 @@ let display_name = if let Some(name) = user.nickname() { }; ``` -Which is: +Который эквивалентен: ```rust let display_name = user.nickname() @@ -45,54 +45,54 @@ let display_name = user.nickname() .unwrap_or_else(|| "ANONYMOUS".to_string()); ``` -The functional version isn't just shorter — it tells you *what* is happening (transform, then default) without making you trace control flow. The `if let` version makes you read the branches to figure out that both paths end up in the same place. +Функциональная версия не просто короче: она сразу говорит, *что* происходит (преобразовать, затем подставить значение по умолчанию), и не заставляет прослеживать поток управления. Версия с `if let` заставляет читать каждую ветку, чтобы понять, что оба пути приводят в одно и то же место. -### The Option combinator family +### Семейство комбинаторов Option -Here's the mental model: `Option` is a one-element-or-empty collection. Every combinator on `Option` has an analogy to a collection operation. +Модель в голове такая: `Option` — это коллекция из одного элемента или пустая коллекция. У каждого комбинатора `Option` есть аналогия с операцией над коллекцией. -| You write... | Instead of... | What it communicates | +| Вы пишете... | Вместо... | Что это сообщает | |---|---|---| -| `opt.unwrap_or(default)` | `if let Some(x) = opt { x } else { default }` | "Use this value or fall back" | -| `opt.unwrap_or_else(\|\| expensive())` | `if let Some(x) = opt { x } else { expensive() }` | Same, but default is lazy | -| `opt.map(f)` | `match opt { Some(x) => Some(f(x)), None => None }` | "Transform the inside, propagate absence" | -| `opt.and_then(f)` | `match opt { Some(x) => f(x), None => None }` | "Chain fallible operations" (flatmap) | -| `opt.filter(\|x\| pred(x))` | `match opt { Some(x) if pred(&x) => Some(x), _ => None }` | "Keep only if it passes" | -| `opt.zip(other)` | `if let (Some(a), Some(b)) = (opt, other) { Some((a,b)) } else { None }` | "Both or neither" | -| `opt.or(fallback)` | `if opt.is_some() { opt } else { fallback }` | "First available" | -| `opt.or_else(\|\| try_another())` | `if opt.is_some() { opt } else { try_another() }` | "Try alternatives in order" | -| `opt.map_or(default, f)` | `if let Some(x) = opt { f(x) } else { default }` | "Transform or default" — one-liner | -| `opt.map_or_else(default_fn, f)` | `if let Some(x) = opt { f(x) } else { default_fn() }` | Same, both sides are closures | -| `opt?` | `match opt { Some(x) => x, None => return None }` | "Propagate absence upward" | - -### The Result combinator family - -The same pattern applies to `Result`: - -| You write... | Instead of... | What it communicates | +| `opt.unwrap_or(default)` | `if let Some(x) = opt { x } else { default }` | «Используй это значение или запасной вариант» | +| `opt.unwrap_or_else(\|\| expensive())` | `if let Some(x) = opt { x } else { expensive() }` | То же, но значение по умолчанию вычисляется лениво | +| `opt.map(f)` | `match opt { Some(x) => Some(f(x)), None => None }` | «Преобразовать содержимое, сохранить отсутствие» | +| `opt.and_then(f)` | `match opt { Some(x) => f(x), None => None }` | «Связать операции, которые могут не удаться» (flatmap) | +| `opt.filter(\|x\| pred(x))` | `match opt { Some(x) if pred(&x) => Some(x), _ => None }` | «Оставить, только если проходит проверку» | +| `opt.zip(other)` | `if let (Some(a), Some(b)) = (opt, other) { Some((a,b)) } else { None }` | «Оба или ни одного» | +| `opt.or(fallback)` | `if opt.is_some() { opt } else { fallback }` | «Первое доступное» | +| `opt.or_else(\|\| try_another())` | `if opt.is_some() { opt } else { try_another() }` | «Пробовать альтернативы по порядку» | +| `opt.map_or(default, f)` | `if let Some(x) = opt { f(x) } else { default }` | «Преобразовать или подставить значение по умолчанию»: одна строка | +| `opt.map_or_else(default_fn, f)` | `if let Some(x) = opt { f(x) } else { default_fn() }` | То же, но обе стороны — замыкания | +| `opt?` | `match opt { Some(x) => x, None => return None }` | «Передать отсутствие вверх по стеку» | + +### Семейство комбинаторов Result + +Тот же паттерн применим к `Result`: + +| Вы пишете... | Вместо... | Что это сообщает | |---|---|---| -| `res.map(f)` | `match res { Ok(x) => Ok(f(x)), Err(e) => Err(e) }` | Transform the success path | -| `res.map_err(f)` | `match res { Ok(x) => Ok(x), Err(e) => Err(f(e)) }` | Transform the error | -| `res.and_then(f)` | `match res { Ok(x) => f(x), Err(e) => Err(e) }` | Chain fallible operations | -| `res.unwrap_or_else(\|e\| default(e))` | `match res { Ok(x) => x, Err(e) => default(e) }` | Recover from error | -| `res.ok()` | `match res { Ok(x) => Some(x), Err(_) => None }` | "I don't care about the error" | -| `res?` | `match res { Ok(x) => x, Err(e) => return Err(e.into()) }` | Propagate errors upward | +| `res.map(f)` | `match res { Ok(x) => Ok(f(x)), Err(e) => Err(e) }` | Преобразовать путь успеха | +| `res.map_err(f)` | `match res { Ok(x) => Ok(x), Err(e) => Err(f(e)) }` | Преобразовать ошибку | +| `res.and_then(f)` | `match res { Ok(x) => f(x), Err(e) => Err(e) }` | Связать операции, которые могут не удаться | +| `res.unwrap_or_else(\|e\| default(e))` | `match res { Ok(x) => x, Err(e) => default(e) }` | Восстановиться после ошибки | +| `res.ok()` | `match res { Ok(x) => Some(x), Err(_) => None }` | «Ошибка мне не важна» | +| `res?` | `match res { Ok(x) => x, Err(e) => return Err(e.into()) }` | Передать ошибку вверх по стеку | -### When `if let` IS better +### Когда `if let` лучше -The combinators lose when: +Комбинаторы проигрывают, когда: -- **You need multiple statements in the `Some` branch.** A map closure with 5 lines is worse than an `if let` with 5 lines. -- **The control flow is the point.** `if let Some(connection) = pool.try_get() { /* use it */ } else { /* log, retry, alert */ }` — the two branches are genuinely different code paths, not a transform-or-default. -- **Side effects dominate.** If both branches do I/O with different error handling, the combinator version obscures the important differences. +- **В ветке `Some` нужно несколько инструкций.** Замыкание map на 5 строк хуже, чем `if let` на 5 строк. +- **Сам поток управления и есть суть.** `if let Some(connection) = pool.try_get() { /* использовать */ } else { /* записать в журнал, повторить, оповестить */ }`: обе ветки — действительно разные пути выполнения, а не «преобразовать или подставить значение по умолчанию». +- **Доминируют побочные эффекты.** Если обе ветки выполняют ввод-вывод с разной обработкой ошибок, версия с комбинаторами скрывает важные различия. -**Rule of thumb:** If the `else` branch produces the *same type* as the `Some` branch and the bodies are short expressions, use a combinator. If the branches do fundamentally different things, use `if let` or `match`. +**Практическое правило:** если ветка `else` даёт *тот же тип*, что и ветка `Some`, а тела короткие, используйте комбинатор. Если ветки делают принципиально разные вещи, используйте `if let` или `match`. --- -## 8.2 Bool Combinators: `.then()` and `.then_some()` +## 8.2 Булевы комбинаторы: `.then()` и `.then_some()` -Another pattern that's more common than it should be: +Ещё один шаблон, который встречается чаще, чем следовало бы: ```rust let label = if is_admin { @@ -102,28 +102,28 @@ let label = if is_admin { }; ``` -Rust 1.62+ gives you: +Начиная с Rust 1.62, можно написать так: ```rust let label = is_admin.then_some("ADMIN"); ``` -Or with a computed value: +Или с вычисляемым значением: ```rust let permissions = is_admin.then(|| compute_admin_permissions()); ``` -This is especially powerful in chains: +Это особенно полезно в цепочках: ```rust -// Imperative +// Императивно let mut tags = Vec::new(); if user.is_admin { tags.push("admin"); } if user.is_verified { tags.push("verified"); } if user.score > 100 { tags.push("power-user"); } -// Functional +// Функционально let tags: Vec<&str> = [ user.is_admin.then_some("admin"), user.is_verified.then_some("verified"), @@ -134,20 +134,20 @@ let tags: Vec<&str> = [ .collect(); ``` -The functional version makes the pattern explicit: "build a list from conditional elements." The imperative version makes you read each `if` to confirm they all do the same thing (push a tag). +Функциональная версия явно выражает паттерн: «собрать список из условных элементов». Императивная версия заставляет читать каждый `if`, чтобы убедиться, что все они делают одно и то же: добавляют тег. --- -## 8.3 Iterator Chains vs. Loops: The Decision Framework +## 8.3 Цепочки итераторов и циклы: система принятия решений -Ch 7 showed the mechanics. This section builds the judgment. +В главе 7 показана механика. Этот раздел помогает выработать суждение. -### When iterators win +### Когда выигрывают итераторы -**Data pipelines** — transforming a collection through a series of steps: +**Конвейеры данных**: преобразование коллекции через последовательность шагов: ```rust -// Imperative: 8 lines, 2 mutable variables +// Императивно: 8 строк, 2 изменяемые переменные let mut results = Vec::new(); for item in inventory { if item.category == Category::Server { @@ -159,7 +159,7 @@ for item in inventory { } } -// Functional: 6 lines, 0 mutable variables, one pipeline +// Функционально: 6 строк, 0 изменяемых переменных, один конвейер let results: Vec<_> = inventory.iter() .filter(|item| item.category == Category::Server) .filter_map(|item| item.last_temperature().map(|t| (item.id, t))) @@ -167,16 +167,16 @@ let results: Vec<_> = inventory.iter() .collect(); ``` -The functional version wins because: -- Each filter is independently readable -- No `mut` — the data flows in one direction -- You can add/remove/reorder pipeline stages without restructuring -- LLVM inlines iterator adapters to the same machine code as the loop +Функциональная версия выигрывает, потому что: +- Каждый фильтр читается независимо +- Нет `mut`: данные текут в одном направлении +- Этапы конвейера можно добавлять, удалять и переставлять без перестройки кода +- LLVM инлайнит адаптеры итераторов в тот же машинный код, что и цикл -**Aggregation** — computing a single value from a collection: +**Агрегация**: вычисление одного значения из коллекции: ```rust -// Imperative +// Императивно let mut total_power = 0.0; let mut count = 0; for server in fleet { @@ -185,48 +185,48 @@ for server in fleet { } let avg = total_power / count as f64; -// Functional +// Функционально let (total_power, count) = fleet.iter() .map(|s| s.power_draw()) .fold((0.0, 0usize), |(sum, n), p| (sum + p, n + 1)); let avg = total_power / count as f64; ``` -Or even simpler if you just need the sum: +Или даже проще, если нужна только сумма: ```rust let total: f64 = fleet.iter().map(|s| s.power_draw()).sum(); ``` -### When loops win +### Когда выигрывают циклы -**Early exit with complex state:** +**Ранний выход со сложным состоянием:** ```rust -// This is clear and direct +// Это понятно и прямолинейно let mut best_candidate = None; for server in fleet { let score = evaluate(server); if score > threshold { if server.is_available() { best_candidate = Some(server); - break; // Found one — stop immediately + break; // Нашли один: останавливаемся сразу } } } -// The functional version is strained +// Функциональная версия выглядит натянуто let best_candidate = fleet.iter() .filter(|s| evaluate(s) > threshold) .find(|s| s.is_available()); ``` -Wait — that functional version is actually pretty clean. Let's try a case where it genuinely loses: +Постойте, эта функциональная версия на самом деле довольно чистая. Попробуем случай, где она действительно проигрывает: -**Building multiple outputs simultaneously:** +**Одновременное формирование нескольких результатов:** ```rust -// Imperative: clear, each branch does something different +// Императивно: понятно, каждая ветка делает своё let mut warnings = Vec::new(); let mut errors = Vec::new(); let mut stats = Stats::default(); @@ -248,7 +248,7 @@ for event in log_stream { } } -// Functional version: forced, awkward, nobody wants to read this +// Функциональная версия: вынужденная, неуклюжая, её никто не захочет читать let (warnings, errors, stats) = log_stream.iter().fold( (Vec::new(), Vec::new(), Stats::default()), |(mut w, mut e, mut s), event| { @@ -265,15 +265,15 @@ let (warnings, errors, stats) = log_stream.iter().fold( ); ``` -The fold version is *longer*, *harder to read*, and has mutation anyway (the `mut` deconstructed accumulators). The loop wins because: -- Multiple outputs being built in parallel -- Side effects (alerting) mixed into the logic -- Branch bodies are statements, not expressions +Версия с fold *длиннее*, *труднее читается* и всё равно содержит мутацию (деструктурированные изменяемые аккумуляторы). Цикл выигрывает, потому что: +- Несколько результатов строятся параллельно +- Побочные эффекты (оповещение) смешаны с логикой +- Тела ветвей это инструкции, а не выражения -**State machines with I/O:** +**Конечные автоматы с вводом-выводом:** ```rust -// A parser that reads tokens — the loop IS the algorithm +// Парсер, который читает токены: цикл и есть алгоритм let mut state = ParseState::Start; loop { let token = lexer.next_token()?; @@ -287,29 +287,29 @@ loop { Token::Ident(name) => ParseState::GotName(k, name), _ => return Err(ParseError::ExpectedIdentifier), }, - // ...more states + // ...остальные состояния }; } ``` -No functional equivalent is cleaner. The loop with `match state` is the natural expression of a state machine. +Чище функциональной альтернативы не получится. Цикл с `match state` это естественная запись конечного автомата. -### The decision flowchart +### Блок-схема выбора ```mermaid flowchart TB - START{What are you doing?} + START{Что вы делаете?} - START -->|"Transforming a collection\ninto another collection"| PIPE[Use iterator chain] - START -->|"Computing a single value\nfrom a collection"| AGG{How complex?} - START -->|"Multiple outputs from\none pass"| LOOP[Use a for loop] - START -->|"State machine with\nI/O or side effects"| LOOP - START -->|"One Option/Result\ntransform + default"| COMB[Use combinators] + START -->|"Преобразование коллекции\nв другую коллекцию"| PIPE[Цепочка итераторов] + START -->|"Вычисление одного значения\nиз коллекции"| AGG{Насколько сложно?} + START -->|"Несколько результатов\nза один проход"| LOOP[Цикл for] + START -->|"Конечный автомат с\nI/O или побочными эффектами"| LOOP + START -->|"Один Option/Result:\nпреобразование + значение по умолчанию"| COMB[Комбинаторы] - AGG -->|"Sum, count, min, max"| BUILTIN["Use .sum(), .count(),\n.min(), .max()"] - AGG -->|"Custom accumulation"| FOLD{Accumulator has mutation\nor side effects?} - FOLD -->|"No"| FOLDF["Use .fold()"] - FOLD -->|"Yes"| LOOP + AGG -->|"Сумма, количество, мин, макс"| BUILTIN["Используйте .sum(), .count(),\n.min(), .max()"] + AGG -->|"Произвольная аккумуляция"| FOLD{В аккумуляторе есть мутация\nили побочные эффекты?} + FOLD -->|"Нет"| FOLDF["Используйте .fold()"] + FOLD -->|"Да"| LOOP style PIPE fill:#d4efdf,stroke:#27ae60,color:#000 style COMB fill:#d4efdf,stroke:#27ae60,color:#000 @@ -318,10 +318,9 @@ flowchart TB style LOOP fill:#fef9e7,stroke:#f1c40f,color:#000 ``` -### Sidebar: Scoped mutability — imperative inside, functional outside +### Врезка: ограниченная изменяемость, императивно внутри и функционально снаружи -Rust blocks are expressions. This lets you confine mutation to a construction phase and -bind the result immutably: +Блоки Rust являются выражениями. Это позволяет ограничить мутацию фазой построения и связать результат неизменяемо: ```rust use rand::random; @@ -331,17 +330,16 @@ let samples = { while buf.len() < 10 { let reading: f64 = random(); buf.push(reading); - if random::() % 3 == 0 { break; } // randomly stop early + if random::() % 3 == 0 { break; } // случайная досрочная остановка } buf }; -// samples is immutable — contains between 1 and 10 elements +// samples неизменяем: содержит от 1 до 10 элементов ``` -The inner `buf` is mutable only inside the block. Once the block yields, the outer binding -`samples` is immutable and the compiler will reject any later `samples.push(...)`. +Внутренний `buf` изменяем только внутри блока. Когда блок возвращает значение, внешняя привязка `samples` становится неизменяемой, и компилятор отклонит любой последующий вызов `samples.push(...)`. -**Why not an iterator chain?** You might try: +**Почему не цепочка итераторов?** Можно попробовать так: ```rust let samples: Vec = std::iter::from_fn(|| Some(random())) @@ -350,33 +348,26 @@ let samples: Vec = std::iter::from_fn(|| Some(random())) .collect(); ``` -But `take_while` *excludes* the element that fails the predicate, producing anywhere from -zero to nine elements instead of the guaranteed-at-least-one the imperative version provides. You can work around it with `scan` or `chain`, but the imperative version -is clearer. +Но `take_while` *исключает* элемент, который не прошёл предикат, поэтому получается от нуля до девяти элементов, а не гарантированно хотя бы один, как в императивной версии. Это можно обойти через `scan` или `chain`, но императивная версия понятнее. -**When scoped mutability genuinely wins:** +**Когда ограниченная изменяемость действительно выигрывает:** -| Scenario | Why iterators struggle | +| Сценарий | Почему итераторам трудно | |---|---| -| **Sort-then-freeze** (`sort_unstable()` + `dedup()`) | Both return `()` — no chainable output (itertools offers `.sorted().dedup()` if available) | -| **Stateful termination** (stop on a condition unrelated to the data) | `take_while` drops the boundary element | -| **Multi-step struct population** (field-by-field from different sources) | No natural single pipeline | +| **Сортировка, затем фиксация** (`sort_unstable()` + `dedup()`) | Оба возвращают `()`: нет цепочечного результата (itertools предлагает `.sorted().dedup()`, если доступен) | +| **Остановка по состоянию** (остановка по условию, не связанному с данными) | `take_while` отбрасывает граничный элемент | +| **Пошаговое заполнение структуры** (поле за полем из разных источников) | Нет естественного единого конвейера | -**Honest calibration:** For most collection-building tasks, iterator chains or -[itertools](https://docs.rs/itertools) are preferred. Reach for scoped mutability when the -construction logic has branching, early exit, or in-place mutation that doesn't map to a -single pipeline. The pattern's real value is teaching that *mutation scope can be smaller -than variable lifetime* — a Rust fundamental that surprises developers coming from -C++, C#, and Python. +**Честная оценка:** для большинства задач построения коллекций предпочтительны цепочки итераторов или [itertools](https://docs.rs/itertools). Прибегайте к ограниченной изменяемости, когда логика построения содержит ветвления, ранний выход или мутацию на месте, которая не укладывается в один конвейер. Настоящая ценность этого паттерна в том, что *область видимости мутации может быть меньше времени жизни переменной*. Это основополагающая особенность Rust, которая удивляет разработчиков, пришедших из C++, C# и Python. --- -## 8.4 The `?` Operator: Where Functional Meets Imperative +## 8.4 Оператор `?`: там, где функциональное встречается с императивным -The `?` operator is Rust's most elegant synthesis of both styles. It's essentially `.and_then()` combined with early return: +Оператор `?` это самый элегантный синтез обоих стилей в Rust. По сути это `.and_then()` в сочетании с ранним возвратом: ```rust -// This chain of and_then... +// Эта цепочка and_then... fn load_config() -> Result { read_file("config.toml") .and_then(|contents| parse_toml(&contents)) @@ -384,7 +375,7 @@ fn load_config() -> Result { .and_then(|valid| Config::from_validated(valid)) } -// ...is exactly equivalent to this +// ...в точности эквивалентна этой fn load_config() -> Result { let contents = read_file("config.toml")?; let table = parse_toml(&contents)?; @@ -393,90 +384,90 @@ fn load_config() -> Result { } ``` -Both are functional in spirit (they propagate errors automatically) but the `?` version gives you named intermediate variables, which matter when: +Обе версии по духу функциональны (они автоматически передают ошибки), но версия с `?` даёт именованные промежуточные переменные. Это важно, когда: -- You need to use `contents` again later -- You want to add `.context("while parsing config")?` per step -- You're debugging and want to inspect intermediate values +- Вам снова нужна `contents` позже +- Вы хотите добавить `.context("при разборе конфигурации")?` на каждом шаге +- Вы отлаживаете код и хотите посмотреть промежуточные значения -**The anti-pattern:** long `.and_then()` chains when `?` is available. If every closure in the chain is `|x| next_step(x)`, you've reinvented `?` without the readability. +**Антипаттерн:** длинные цепочки `.and_then()`, когда доступен `?`. Если каждое замыкание в цепочке имеет вид `|x| next_step(x)`, вы заново изобрели `?` без читаемости. -**When `.and_then()` IS better than `?`:** +**Когда `.and_then()` лучше, чем `?`:** ```rust -// Transforming inside an Option, without early return +// Преобразование внутри Option без раннего возврата let port: Option = config.get("port") .and_then(|v| v.parse::().ok()) .filter(|&p| p > 0 && p < 65535); ``` -You can't use `?` here because there's no enclosing function to return from — you're building an `Option`, not propagating it. +Здесь нельзя использовать `?`, потому что нет охватывающей функции, из которой можно вернуться: вы строите `Option`, а не передаёте его дальше. --- -## 8.5 Collection Building: `collect()` vs. Push Loops +## 8.5 Построение коллекций: `collect()` и циклы с push -`collect()` is more powerful than most developers realize: +`collect()` мощнее, чем обычно думают разработчики: -### Collecting into a Result +### Сбор в Result ```rust -// Imperative: parse a list, fail on first error +// Императивно: разбираем список, падаем на первой ошибке let mut numbers = Vec::new(); for s in input_strings { let n: i64 = s.parse().map_err(|_| Error::BadInput(s.clone()))?; numbers.push(n); } -// Functional: collect into Result, _> +// Функционально: собираем в Result, _> let numbers: Vec = input_strings.iter() .map(|s| s.parse::().map_err(|_| Error::BadInput(s.clone()))) .collect::>()?; ``` -The `collect::, _>>()` trick works because `Result` implements `FromIterator`. It short-circuits on the first `Err`, just like the loop with `?`. +Приём `collect::, _>>()` работает, потому что `Result` реализует `FromIterator`. Он прекращает работу на первом `Err`, точно так же, как цикл с `?`. -### Collecting into a HashMap +### Сбор в HashMap ```rust -// Imperative +// Императивно let mut index = HashMap::new(); for server in fleet { index.insert(server.id.clone(), server); } -// Functional +// Функционально let index: HashMap<_, _> = fleet.into_iter() .map(|s| (s.id.clone(), s)) .collect(); ``` -### Collecting into a String +### Сбор в String ```rust -// Imperative +// Императивно let mut csv = String::new(); for (i, field) in fields.iter().enumerate() { if i > 0 { csv.push(','); } csv.push_str(field); } -// Functional +// Функционально let csv = fields.join(","); -// Or for more complex formatting: +// Или для более сложного форматирования: let csv: String = fields.iter() .map(|f| format!("\"{f}\"")) .collect::>() .join(","); ``` -### When the loop version wins +### Когда выигрывает версия с циклом -`collect()` allocates a new collection. If you're *modifying in place*, the loop is both clearer and more efficient: +`collect()` выделяет новую коллекцию. Если вы *меняете на месте*, цикл одновременно понятнее и эффективнее: ```rust -// In-place update — no functional equivalent that's better +// Обновление на месте: функционального аналога, который был бы лучше, нет for server in &mut fleet { if server.needs_refresh() { server.refresh_telemetry()?; @@ -484,18 +475,18 @@ for server in &mut fleet { } ``` -The functional version would require `.iter_mut().for_each(|s| { ... })`, which is just a loop with extra syntax. +Функциональная версия потребовала бы `.iter_mut().for_each(|s| { ... })`, а это просто цикл с лишним синтаксисом. --- -## 8.6 Pattern Matching as Function Dispatch +## 8.6 Сопоставление с образцом как диспетчеризация функций -Rust's `match` is a functional construct that most developers use imperatively. Here's the functional lens: +`match` в Rust это функциональная конструкция, которую большинство разработчиков использует императивно. Вот функциональный взгляд: -### Match as a lookup table +### match как таблица поиска ```rust -// Imperative thinking: "check each case" +// Императивное мышление: «проверяем каждый случай» fn status_message(code: StatusCode) -> &'static str { if code == StatusCode::OK { "Success" } else if code == StatusCode::NOT_FOUND { "Not found" } @@ -503,7 +494,7 @@ fn status_message(code: StatusCode) -> &'static str { else { "Unknown" } } -// Functional thinking: "map from domain to range" +// Функциональное мышление: «отображение из области определения в область значений» fn status_message(code: StatusCode) -> &'static str { match code { StatusCode::OK => "Success", @@ -514,12 +505,12 @@ fn status_message(code: StatusCode) -> &'static str { } ``` -The `match` version isn't just style — the compiler verifies exhaustiveness. Add a new variant, and every `match` that doesn't handle it becomes a compile error. The `if/else` chain silently falls through to the default. +Версия с `match` это не только вопрос стиля: компилятор проверяет исчерпывающность. Добавьте новый вариант, и каждый `match`, который его не обрабатывает, станет ошибкой компиляции. Цепочка `if/else` молча провалится в ветку по умолчанию. -### Match + destructuring as a pipeline +### Сопоставление с образцом и деструктуризация как конвейер ```rust -// Parsing a command — each arm extracts and transforms +// Разбор команды: каждая ветка извлекает данные и преобразует их fn execute(cmd: Command) -> Result { match cmd { Command::Get { key } => db.get(&key).map(Response::Value), @@ -533,16 +524,16 @@ fn execute(cmd: Command) -> Result { } ``` -Each arm is an expression that returns the same type. This is pattern matching as function dispatch — the `match` arms are essentially a function table indexed by the enum variant. +Каждая ветка является выражением, возвращающим один и тот же тип. Это сопоставление с образцом как диспетчеризация функций: ветки `match` по сути образуют таблицу функций, индексированную вариантом перечисления. --- -## 8.7 Chaining Methods on Custom Types +## 8.7 Цепочки методов на собственных типах -The functional style extends beyond standard library types. Builder patterns and fluent APIs are functional programming in disguise: +Функциональный стиль выходит за пределы типов стандартной библиотеки. Паттерны builder и fluent API это функциональное программирование в маскировке: ```rust -// This is a combinator chain over your own type +// Это цепочка комбинаторов над вашим собственным типом let query = QueryBuilder::new("servers") .filter("status", Eq, "active") .filter("rack", In, &["A1", "A2", "B1"]) @@ -551,47 +542,47 @@ let query = QueryBuilder::new("servers") .build(); ``` -**The key insight:** if your type has methods that take `self` and return `Self` (or a transformed type), you've built a combinator. The same functional/imperative judgment applies: +**Ключевая мысль**: если у вашего типа есть методы, которые принимают `self` и возвращают `Self` (или преобразованный тип), вы построили комбинатор. К нему применимо то же суждение о функциональном и императивном стиле: ```rust -// Good: chainable because each step is a simple transform +// Хорошо: цепочка работает, потому что каждый шаг это простое преобразование let config = Config::default() .with_timeout(Duration::from_secs(30)) .with_retries(3) .with_tls(true); -// Bad: chainable but the chain is doing too many unrelated things +// Плохо: цепочка работает, но делает слишком много не связанных между собой вещей let result = processor .load_data(path)? // I/O - .validate() // Pure - .transform(rule_set) // Pure + .validate() // Чисто + .transform(rule_set) // Чисто .save_to_disk(output)? // I/O - .notify_downstream()?; // Side effect + .notify_downstream()?; // Побочный эффект -// Better: separate the pure pipeline from the I/O bookends +// Лучше: отделить чистый конвейер от I/O на концах let data = load_data(path)?; let processed = data.validate().transform(rule_set); save_to_disk(output, &processed)?; notify_downstream()?; ``` -The chain fails when it mixes pure transforms with I/O. The reader can't tell which calls might fail, which have side effects, and where the actual data transformations happen. +Цепочка ломается, когда смешивает чистые преобразования с I/O. Читатель не может понять, какие вызовы могут завершиться ошибкой, какие имеют побочные эффекты и где на самом деле происходят преобразования данных. --- -## 8.8 Performance: They're the Same +## 8.8 Производительность: они одинаковы -A common misconception: "functional style is slower because of all the closures and allocations." +Распространённое заблуждение: «функциональный стиль медленнее из-за всех этих замыканий и выделений памяти». -In Rust, **iterator chains compile to the same machine code as hand-written loops.** LLVM inlines the closure calls, eliminates the iterator adapter structs, and often produces identical assembly. This is called *zero-cost abstraction* and it's not aspirational — it's measured. +В Rust **цепочки итераторов компилируются в тот же машинный код, что и написанные вручную циклы.** LLVM инлайнит вызовы замыканий, устраняет структуры адаптеров итераторов и часто выдаёт идентичный ассемблер. Это называется *абстракцией с нулевой стоимостью*, и это не пожелание, а измеренный факт. ```rust -// These produce identical assembly on release builds: +// На release-сборках эти варианты дают идентичный ассемблер: -// Functional +// Функционально let sum: i64 = (0..1000).filter(|n| n % 2 == 0).map(|n| n * n).sum(); -// Imperative +// Императивно let mut sum: i64 = 0; for n in 0..1000 { if n % 2 == 0 { @@ -600,35 +591,35 @@ for n in 0..1000 { } ``` -**The one exception:** `.collect()` allocates. If you're chaining `.map().collect().iter().map().collect()` with intermediate collections, you're paying for allocations the loop version avoids. The fix: eliminate intermediate collects by chaining adapters directly, or use a loop if you need the intermediate collections for other reasons. +**Единственное исключение**: `.collect()` выделяет память. Если вы строите цепочку `.map().collect().iter().map().collect()` с промежуточными коллекциями, вы платите за выделения, которых избежала бы версия с циклом. Решение: убрать промежуточные `collect` и соединить адаптеры напрямую или использовать цикл, если промежуточные коллекции нужны по другим причинам. --- -## 8.9 The Taste Test: A Catalog of Transformations +## 8.9 Проверка вкуса: каталог преобразований -Here's a reference table for the most common "I wrote 6 lines but there's a one-liner" patterns: +Справочная таблица для самых частых шаблонов «я написал 6 строк, а есть однострочник»: -| Imperative pattern | Functional equivalent | When to prefer functional | +| Императивный шаблон | Функциональный аналог | Когда предпочесть функциональный | |---|---|---| -| `if let Some(x) = opt { f(x) } else { default }` | `opt.map_or(default, f)` | Short expressions on both sides | -| `if let Some(x) = opt { Some(g(x)) } else { None }` | `opt.map(g)` | Always — this is what `map` is for | -| `if condition { Some(x) } else { None }` | `condition.then_some(x)` | Always | -| `if condition { Some(compute()) } else { None }` | `condition.then(compute)` | Always | -| `match opt { Some(x) if pred(x) => Some(x), _ => None }` | `opt.filter(pred)` | Always | -| `for x in iter { if pred(x) { result.push(f(x)); } }` | `iter.filter(pred).map(f).collect()` | When the pipeline is readable in one screen | -| `if a.is_some() && b.is_some() { Some((a?, b?)) }` | `a.zip(b)` | Always — `.zip()` is exactly this | -| `match (a, b) { (Some(x), Some(y)) => x + y, _ => 0 }` | `a.zip(b).map(\|(x,y)\| x + y).unwrap_or(0)` | Judgment call — depends on complexity | -| `iter.map(f).collect::>()[0]` | `iter.map(f).next().unwrap()` | Don't allocate a Vec for one element | -| `let mut v = vec; v.sort(); v` | `{ let mut v = vec; v.sort(); v }` | Rust doesn't have a `.sorted()` in std (use itertools) | +| `if let Some(x) = opt { f(x) } else { default }` | `opt.map_or(default, f)` | Короткие выражения с обеих сторон | +| `if let Some(x) = opt { Some(g(x)) } else { None }` | `opt.map(g)` | Всегда: для этого и существует `map` | +| `if condition { Some(x) } else { None }` | `condition.then_some(x)` | Всегда | +| `if condition { Some(compute()) } else { None }` | `condition.then(compute)` | Всегда | +| `match opt { Some(x) if pred(x) => Some(x), _ => None }` | `opt.filter(pred)` | Всегда | +| `for x in iter { if pred(x) { result.push(f(x)); } }` | `iter.filter(pred).map(f).collect()` | Когда конвейер читается с одного экрана | +| `if a.is_some() && b.is_some() { Some((a?, b?)) }` | `a.zip(b)` | Всегда: `.zip()` делает ровно это | +| `match (a, b) { (Some(x), Some(y)) => x + y, _ => 0 }` | `a.zip(b).map(\|(x,y)\| x + y).unwrap_or(0)` | Вопрос суждения, зависит от сложности | +| `iter.map(f).collect::>()[0]` | `iter.map(f).next().unwrap()` | Не выделяйте Vec под один элемент | +| `let mut v = vec; v.sort(); v` | `{ let mut v = vec; v.sort(); v }` | В стандартной библиотеке нет `.sorted()` (используйте itertools) | --- -## 8.10 The Anti-Patterns +## 8.10 Антипаттерны -### Over-functionalizing: the 5-deep chain nobody can read +### Чрезмерная функциональность: цепочка из пяти уровней, которую никто не может прочитать ```rust -// This is not elegant. This is a puzzle. +// Это не элегантно. Это головоломка. let result = data.iter() .filter_map(|x| x.metadata.as_ref()) .flat_map(|m| m.tags.iter()) @@ -642,7 +633,7 @@ let result = data.iter() .collect::>(); ``` -When a chain exceeds ~4 adapters, break it up with named intermediate variables or extract a helper: +Когда цепочка превышает примерно 4 адаптера, разбейте её на именованные промежуточные переменные или вынесите часть в отдельную функцию: ```rust let env_tags = data.iter() @@ -657,10 +648,10 @@ let allowed: Vec<_> = env_tags .collect(); ``` -### Under-functionalizing: the C-style loop that Rust has a word for +### Недостаточная функциональность: цикл в стиле C, для которого у Rust есть слово ```rust -// This is just .any() +// Это просто .any() let mut found = false; for item in &list { if item.is_expired() { @@ -669,12 +660,12 @@ for item in &list { } } -// Write this instead +// Напишите вместо этого let found = list.iter().any(|item| item.is_expired()); ``` ```rust -// This is just .find() +// Это просто .find() let mut target = None; for server in &fleet { if server.id == target_id { @@ -683,12 +674,12 @@ for server in &fleet { } } -// Write this instead +// Напишите вместо этого let target = fleet.iter().find(|s| s.id == target_id); ``` ```rust -// This is just .all() +// Это просто .all() let mut all_healthy = true; for server in &fleet { if !server.is_healthy() { @@ -697,31 +688,31 @@ for server in &fleet { } } -// Write this instead +// Напишите вместо этого let all_healthy = fleet.iter().all(|s| s.is_healthy()); ``` -The standard library has these for a reason. Learn the vocabulary and the patterns become obvious. +В стандартной библиотеке эти методы есть не случайно. Выучите словарь, и паттерны станут очевидны. --- -## Key Takeaways +## Ключевые выводы -> - **Option and Result are one-element collections.** Their combinators (`.map()`, `.and_then()`, `.unwrap_or_else()`, `.filter()`, `.zip()`) replace most `if let` / `match` boilerplate. -> - **Use `bool::then_some()`** — it replaces `if cond { Some(x) } else { None }` in every case. -> - **Iterator chains win for data pipelines** — filter/map/collect with zero mutable state. They compile to the same machine code as loops. -> - **Loops win for multi-output state machines** — when you're building multiple collections, doing I/O in branches, or managing a state transition. -> - **The `?` operator is the best of both worlds** — functional error propagation with imperative readability. -> - **Break chains at ~4 adapters** — use named intermediates for readability. Over-functionalizing is as bad as under-functionalizing. -> - **Learn the standard-library vocabulary** — `.any()`, `.all()`, `.find()`, `.position()`, `.sum()`, `.min_by_key()` — each one replaces a multi-line loop with a single intent-revealing call. +> - **Option и Result это коллекции из одного элемента.** Их комбинаторы (`.map()`, `.and_then()`, `.unwrap_or_else()`, `.filter()`, `.zip()`) заменяют большую часть шаблонного кода с `if let` и `match`. +> - **Используйте `bool::then_some()`**: он заменяет `if cond { Some(x) } else { None }` в любом случае. +> - **Цепочки итераторов выигрывают для конвейеров данных**: filter, map и collect без изменяемого состояния. Они компилируются в тот же машинный код, что и циклы. +> - **Циклы выигрывают для автоматов с несколькими результатами**: когда вы строите несколько коллекций, выполняете I/O в ветках или управляете переходом состояния. +> - **Оператор `?` это лучшее из двух миров**: функциональная передача ошибок с императивной читаемостью. +> - **Разбивайте цепочки примерно на 4 адаптерах**: используйте именованные промежуточные переменные для читаемости. Чрезмерная функциональность так же вредна, как недостаточная. +> - **Выучите словарь стандартной библиотеки**: `.any()`, `.all()`, `.find()`, `.position()`, `.sum()`, `.min_by_key()`. Каждый из них заменяет многострочный цикл одним вызовом, который выражает намерение. -> **See also:** [Ch 7](ch07-closures-and-higher-order-functions.md) for closure mechanics and the `Fn` trait hierarchy. [Ch 10](ch10-error-handling-patterns.md) for error combinator patterns. [Ch 15](ch15-crate-architecture-and-api-design.md) for fluent API design. +> **См. также:** [гл. 7](ch07-closures-and-higher-order-functions.md) о механике замыканий и иерархии трейта `Fn`. [гл. 10](ch10-error-handling-patterns.md) о комбинаторах для обработки ошибок. [гл. 15](ch15-crate-architecture-and-api-design.md) о проектировании fluent API. --- -### Exercise: Refactoring Imperative to Functional ★★ (~30 min) +## Упражнение: рефакторинг императивного кода в функциональный ★★ (~30 мин) -Refactor the following function from imperative to functional style. Then identify one place where the functional version is *worse* and explain why. +Перепишите следующую функцию из императивного стиля в функциональный. Затем найдите одно место, где функциональная версия *хуже*, и объясните почему. ```rust fn summarize_fleet(fleet: &[Server]) -> FleetSummary { @@ -754,9 +745,9 @@ fn summarize_fleet(fleet: &[Server]) -> FleetSummary { ```
-🔑 Solution +🔑 Решение -The `total_power` and `max_temp` are clean functional rewrites: +`total_power` и `max_temp` — чистые функциональные переписывания: ```rust fn summarize_fleet(fleet: &[Server]) -> FleetSummary { @@ -767,9 +758,9 @@ fn summarize_fleet(fleet: &[Server]) -> FleetSummary { .map(|s| s.max_temperature()) .fold(f64::NEG_INFINITY, f64::max); - // But the three-way partition is BETTER as a loop. - // Functional version would require three separate passes - // or an awkward fold with three mutable accumulators. + // Но разбиение на три группы ЛУЧШЕ сделать циклом. + // Функциональная версия потребовала бы трёх отдельных проходов + // или неуклюжего fold с тремя изменяемыми аккумуляторами. let mut healthy = Vec::new(); let mut degraded = Vec::new(); let mut failed = Vec::new(); @@ -786,7 +777,7 @@ fn summarize_fleet(fleet: &[Server]) -> FleetSummary { } ``` -**Why the loop is better for the three-way partition:** A functional version would either require three `.filter().collect()` passes (3x iteration), or a `.fold()` with three `mut Vec` accumulators inside a tuple — which is just the loop rewritten with worse syntax. The imperative single-pass loop is clearer, more efficient, and easier to extend. +**Почему цикл лучше для разбиения на три группы:** функциональная версия либо потребовала бы три прохода `.filter().collect()` (трёхкратный обход), либо `.fold()` с тремя `mut Vec`-аккумуляторами внутри кортежа, то есть тот же цикл, переписанный с худшим синтаксисом. Императивный цикл за один проход понятнее, эффективнее и проще расширять.
diff --git a/rust-patterns-book/src/ch09-smart-pointers-and-interior-mutability.md b/rust-patterns-book/src/ch09-smart-pointers-and-interior-mutability.md index 5190d3d..aa0a814 100644 --- a/rust-patterns-book/src/ch09-smart-pointers-and-interior-mutability.md +++ b/rust-patterns-book/src/ch09-smart-pointers-and-interior-mutability.md @@ -1,45 +1,45 @@ -# 8. Smart Pointers and Interior Mutability 🟡 +# 9. Умные указатели и внутренняя изменяемость 🟡 -> **What you'll learn:** -> - Box, Rc, Arc for heap allocation and shared ownership -> - Weak references for breaking Rc/Arc reference cycles -> - Cell, RefCell, and Cow for interior mutability patterns -> - Pin for self-referential types and ManuallyDrop for lifecycle control +> **Что вы узнаете:** +> - Box, Rc и Arc для размещения в куче и совместного владения +> - Слабые ссылки (Weak) для разрыва циклов ссылок в Rc/Arc +> - Cell, RefCell и Cow для паттернов внутренней изменяемости +> - Pin для самоссылающихся типов и ManuallyDrop для управления жизненным циклом -## Box, Rc, Arc — Heap Allocation and Sharing +## Box, Rc, Arc: размещение в куче и совместное использование ```rust -// --- Box: Single owner, heap allocation --- -// Use when: recursive types, large values, trait objects +// --- Box: единственный владелец, размещение в куче --- +// Использовать, когда нужны рекурсивные типы, большие значения или трейт-объекты let boxed: Box = Box::new(42); -println!("{}", *boxed); // Deref to i32 +println!("{}", *boxed); // Deref к i32 -// Recursive type requires Box (otherwise infinite size): +// Рекурсивный тип требует Box (иначе размер бесконечен): enum List { Cons(T, Box>), Nil, } -// Trait object (dynamic dispatch): +// Трейт-объект (динамическая диспетчеризация): let writer: Box = Box::new(std::io::stdout()); -// --- Rc: Multiple owners, single-threaded --- -// Use when: shared ownership within one thread (no Send/Sync) +// --- Rc: несколько владельцев, один поток --- +// Использовать, когда нужно совместное владение внутри одного потока (без Send/Sync) use std::rc::Rc; let a = Rc::new(vec![1, 2, 3]); -let b = Rc::clone(&a); // Increments reference count (NOT deep clone) +let b = Rc::clone(&a); // Увеличивает счётчик ссылок (НЕ глубокое копирование) let c = Rc::clone(&a); -println!("Ref count: {}", Rc::strong_count(&a)); // 3 +println!("Счётчик ссылок: {}", Rc::strong_count(&a)); // 3 -// All three point to the same Vec. When the last Rc is dropped, -// the Vec is deallocated. +// Все три указывают на один и тот же Vec. Когда уничтожается последний Rc, +// Vec освобождается. -// --- Arc: Multiple owners, thread-safe --- -// Use when: shared ownership across threads +// --- Arc: несколько владельцев, потокобезопасно --- +// Использовать, когда нужно совместное владение между потоками use std::sync::Arc; -let shared = Arc::new(String::from("shared data")); +let shared = Arc::new(String::from("общие данные")); let handles: Vec<_> = (0..5).map(|_| { let shared = Arc::clone(&shared); std::thread::spawn(move || println!("{shared}")) @@ -47,10 +47,9 @@ let handles: Vec<_> = (0..5).map(|_| { for h in handles { h.join().unwrap(); } ``` -### Weak References — Breaking Reference Cycles +### Weak-ссылки: разрыв циклов ссылок -`Rc` and `Arc` use reference counting, which cannot free cycles (A → B → A). -`Weak` is a non-owning handle that does **not** increment the strong count: +`Rc` и `Arc` используют подсчёт ссылок, который не может освободить циклы (A → B → A). `Weak` это невладеющий дескриптор, который **не** увеличивает сильный счётчик: ```rust use std::rc::{Rc, Weak}; @@ -58,7 +57,7 @@ use std::cell::RefCell; struct Node { value: i32, - parent: RefCell>, // does NOT keep parent alive + parent: RefCell>, // НЕ удерживает родителя живым children: RefCell>>, } @@ -70,26 +69,25 @@ let child = Rc::new(Node { }); parent.children.borrow_mut().push(Rc::clone(&child)); -// Access parent from child — returns Option>: +// Доступ к родителю из потомка: возвращает Option>: if let Some(p) = child.parent.borrow().upgrade() { - println!("Child's parent value: {}", p.value); // 0 + println!("Значение родителя потомка: {}", p.value); // 0 } -// When `parent` is dropped, strong_count → 0, memory is freed. -// `child.parent.upgrade()` would then return `None`. +// Когда `parent` уничтожается, strong_count → 0, память освобождается. +// Тогда `child.parent.upgrade()` вернёт `None`. ``` -**Rule of thumb**: Use `Rc`/`Arc` for ownership edges, `Weak` for back-references -and caches. For thread-safe code, use `Arc` with `sync::Weak`. +**Практическое правило**: используйте `Rc`/`Arc` для рёбер владения, а `Weak` для обратных ссылок и кэшей. Для потокобезопасного кода используйте `Arc` вместе с `sync::Weak`. -### Cell and RefCell — Interior Mutability +### Cell и RefCell: внутренняя изменяемость -Sometimes you need to mutate data behind a shared (`&`) reference. Rust provides *interior mutability* with runtime borrow checking: +Иногда нужно изменять данные за разделяемой ссылкой (`&`). Rust предоставляет *внутреннюю изменяемость* с проверкой заимствований во время выполнения: ```rust use std::cell::{Cell, RefCell}; -// --- Cell: Copy-based interior mutability --- -// Only for Copy types (or types you swap in/out) +// --- Cell: внутренняя изменяемость через копирование --- +// Только для типов Copy (или для типов, которые можно заменять через swap/replace) struct Counter { count: Cell, } @@ -97,15 +95,15 @@ struct Counter { impl Counter { fn new() -> Self { Counter { count: Cell::new(0) } } - fn increment(&self) { // &self, not &mut self! + fn increment(&self) { // &self, а не &mut self! self.count.set(self.count.get() + 1); } fn value(&self) -> u32 { self.count.get() } } -// --- RefCell: Runtime borrow checking --- -// Panics if you violate borrow rules at runtime +// --- RefCell: проверка заимствований во время выполнения --- +// Паникует, если нарушить правила заимствования во время выполнения struct Cache { data: RefCell>, } @@ -113,41 +111,38 @@ struct Cache { impl Cache { fn new() -> Self { Cache { data: RefCell::new(Vec::new()) } } - fn add(&self, item: String) { // &self — looks immutable from outside - self.data.borrow_mut().push(item); // Runtime-checked &mut + fn add(&self, item: String) { // &self: снаружи выглядит неизменяемым + self.data.borrow_mut().push(item); // &mut, проверяемый во время выполнения } fn get_all(&self) -> Vec { - self.data.borrow().clone() // Runtime-checked & + self.data.borrow().clone() // & , проверяемый во время выполнения } fn bad_example(&self) { let _guard1 = self.data.borrow(); // let _guard2 = self.data.borrow_mut(); - // ❌ PANICS at runtime — can't have &mut while & exists + // ❌ ПАНИКА во время выполнения: нельзя иметь &mut, пока существует & } } ``` -> **Cell vs RefCell**: `Cell` never panics (it copies/swaps values) but only -> works with `Copy` types or via `swap()`/`replace()`. `RefCell` works with any -> type but panics on double-mutable-borrow. Neither is `Sync` — for multithreaded -> use, see `Mutex`/`RwLock`. +> **Cell или RefCell**: `Cell` никогда не паникует (он копирует или меняет значения местами), но работает только с типами `Copy` или через `swap()`/`replace()`. `RefCell` работает с любым типом, но паникует при двойном изменяемом заимствовании. Ни один из них не является `Sync`: для многопоточного использования смотрите `Mutex`/`RwLock`. -### Cow — Clone on Write +### Cow: клонирование при записи -`Cow` (Clone on Write) holds either a borrowed or owned value. It clones *only* when mutation is needed: +`Cow` (Clone on Write) хранит либо заимствованное, либо владеющее значение. Он клонирует данные *только* тогда, когда нужна мутация: ```rust use std::borrow::Cow; -// Avoids allocating when no modification is needed: +// Не выделяет память, если изменение не нужно: fn normalize(input: &str) -> Cow<'_, str> { if input.contains('\t') { - // Only allocate if tabs need replacing + // Выделяем память, только если табуляции нужно заменить Cow::Owned(input.replace('\t', " ")) } else { - // No allocation — just return a reference + // Без выделения памяти: просто возвращаем ссылку Cow::Borrowed(input) } } @@ -156,76 +151,71 @@ fn main() { let clean = "no tabs here"; let dirty = "tabs\there"; - let r1 = normalize(clean); // Cow::Borrowed — zero allocation - let r2 = normalize(dirty); // Cow::Owned — allocated new String + let r1 = normalize(clean); // Cow::Borrowed: без выделения памяти + let r2 = normalize(dirty); // Cow::Owned: выделена новая String println!("{r1}"); println!("{r2}"); } -// Also useful for function parameters that MIGHT need ownership: +// Также полезно для параметров функции, которым МОЖЕТ понадобиться владение: fn process(data: Cow<'_, [u8]>) { - // Can read data without copying - println!("Length: {}", data.len()); - // If we need to mutate, Cow auto-clones: - let mut owned = data.into_owned(); // Clone only if Borrowed + // Можно читать данные без копирования + println!("Длина: {}", data.len()); + // Если нужна мутация, Cow сам клонирует: + let mut owned = data.into_owned(); // Клонируем, только если Borrowed owned.push(0xFF); } ``` -#### `Cow<'_, [u8]>` for Binary Data +#### `Cow<'_, [u8]>` для бинарных данных -`Cow` is especially useful for byte-oriented APIs where data may or may not -need transformation (checksum insertion, padding, escaping). This avoids -allocating a `Vec` on the common fast path: +`Cow` особенно полезен в API для работы с байтами, где данные могут требовать преобразования (вставка контрольной суммы, дополнение, экранирование), а могут и не требовать. Это позволяет не выделять `Vec` на частом быстром пути: ```rust use std::borrow::Cow; -/// Pads a frame to a minimum length, borrowing when no padding is needed. +/// Дополняет кадр до минимальной длины, заимствуя данные, если дополнение не нужно. fn pad_frame(frame: &[u8], min_len: usize) -> Cow<'_, [u8]> { if frame.len() >= min_len { - Cow::Borrowed(frame) // Already long enough — zero allocation + Cow::Borrowed(frame) // Уже достаточно длинный: без выделения памяти } else { let mut padded = frame.to_vec(); padded.resize(min_len, 0x00); - Cow::Owned(padded) // Allocate only when padding is required + Cow::Owned(padded) // Выделяем память, только когда нужно дополнение } } -let short = pad_frame(&[0xDE, 0xAD], 8); // Owned — padded to 8 bytes -let long = pad_frame(&[0; 64], 8); // Borrowed — already ≥ 8 +let short = pad_frame(&[0xDE, 0xAD], 8); // Owned: дополнен до 8 байт +let long = pad_frame(&[0; 64], 8); // Borrowed: уже ≥ 8 ``` -> **Tip**: Combine `Cow<[u8]>` with `bytes::Bytes` (Ch10) when you need -> reference-counted sharing of potentially-transformed buffers. +> **Совет**: сочетайте `Cow<[u8]>` с `bytes::Bytes` (гл. 11), когда нужно совместное использование с подсчётом ссылок для потенциально преобразованных буферов. -### When to Use Which Pointer +### Какой указатель выбрать -| Pointer | Owner Count | Thread-Safe | Mutability | Use When | -|---------|:-----------:|:-----------:|:----------:|----------| -| `Box` | 1 | ✅ (if T: Send) | Via `&mut` | Heap allocation, trait objects, recursive types | -| `Rc` | N | ❌ | None (wrap in Cell/RefCell) | Shared ownership, single thread, graphs/trees | -| `Arc` | N | ✅ | None (wrap in Mutex/RwLock) | Shared ownership across threads | -| `Cell` | — | ❌ | `.get()` / `.set()` | Interior mutability for Copy types | -| `RefCell` | — | ❌ | `.borrow()` / `.borrow_mut()` | Interior mutability for any type, single thread | -| `Cow<'_, T>` | 0 or 1 | ✅ (if T: Send) | Clone on write | Avoid allocation when data is often unchanged | +| Указатель | Число владельцев | Потокобезопасный | Изменяемость | Когда использовать | +|-----------|:----------------:|:----------------:|:------------:|--------------------| +| `Box` | 1 | ✅ (если T: Send) | Через `&mut` | Размещение в куче, трейт-объекты, рекурсивные типы | +| `Rc` | N | ❌ | Нет (оберните в Cell/RefCell) | Совместное владение в одном потоке, графы и деревья | +| `Arc` | N | ✅ | Нет (оберните в Mutex/RwLock) | Совместное владение между потоками | +| `Cell` | — | ❌ | `.get()` / `.set()` | Внутренняя изменяемость для типов Copy | +| `RefCell` | — | ❌ | `.borrow()` / `.borrow_mut()` | Внутренняя изменяемость для любого типа в одном потоке | +| `Cow<'_, T>` | 0 или 1 | ✅ (если T: Send) | Клонирование при записи | Избегать выделения памяти, когда данные часто не меняются | -### Pin and Self-Referential Types +### Pin и самоссылающиеся типы -`Pin

` prevents a value from being moved in memory. This is essential for -**self-referential types** — structs that contain a pointer to their own data — -and for `Future`s, which may hold references across `.await` points. +`Pin

` не даёт значению переместиться в памяти. Это необходимо для **самоссылающихся типов** (структур, которые содержат указатель на собственные данные) и для `Future`, которые могут удерживать ссылки через точки `.await`. ```rust use std::pin::Pin; use std::marker::PhantomPinned; -// A self-referential struct (simplified): +// Самоссылающаяся структура (упрощённо): struct SelfRef { data: String, - ptr: *const String, // Points to `data` above - _pin: PhantomPinned, // Opts out of Unpin — can't be moved + ptr: *const String, // Указывает на `data` выше + _pin: PhantomPinned, // Отказывается от Unpin: её нельзя переместить } impl SelfRef { @@ -237,7 +227,7 @@ impl SelfRef { }; let mut boxed = Box::pin(val); - // SAFETY: we don't move the data after setting the pointer + // SAFETY: не перемещаем данные после установки указателя let self_ptr: *const String = &boxed.data; unsafe { let mut_ref = Pin::as_mut(&mut boxed); @@ -251,109 +241,98 @@ impl SelfRef { } fn ptr_data(&self) -> &str { - // SAFETY: ptr was set to point to self.data while pinned + // SAFETY: ptr был установлен на указатель на self.data, пока структура закреплена unsafe { &*self.ptr } } } fn main() { let pinned = SelfRef::new("hello"); - assert_eq!(pinned.data(), pinned.ptr_data()); // Both "hello" - // std::mem::swap would invalidate ptr — but Pin prevents it + assert_eq!(pinned.data(), pinned.ptr_data()); // Оба "hello" + // std::mem::swap сделал бы ptr недействительным, но Pin этого не допускает } ``` -**Key concepts**: +**Ключевые понятия**: -| Concept | Meaning | -|---------|--------| -| `Unpin` (auto-trait) | "Moving this type is safe." Most types are `Unpin` by default. | -| `!Unpin` / `PhantomPinned` | "I have internal pointers — don't move me." | -| `Pin<&mut T>` | A mutable reference that guarantees `T` won't move | -| `Pin>` | An owned, heap-pinned value | +| Понятие | Значение | +|---------|----------| +| `Unpin` (авто-трейт) | «Перемещать этот тип безопасно». Большинство типов по умолчанию `Unpin`. | +| `!Unpin` / `PhantomPinned` | «У меня есть внутренние указатели: не перемещайте меня». | +| `Pin<&mut T>` | Изменяемая ссылка, которая гарантирует, что `T` не переместится | +| `Pin>` | Владеющее значение, закреплённое в куче | -**Why this matters for async**: Every `async fn` desugars to a `Future` that may -hold references across `.await` points — making it self-referential. The async -runtime uses `Pin<&mut Future>` to guarantee the future isn't moved once polled. +**Почему это важно для async**: каждая `async fn` превращается в `Future`, который может удерживать ссылки через точки `.await`, то есть становится самоссылающимся. Асинхронный рантайм использует `Pin<&mut Future>`, чтобы гарантировать, что future не переместят после опроса (poll). ```rust -// When you write: +// Когда вы пишете: async fn fetch(url: &str) -> String { - let response = http_get(url).await; // reference held across await + let response = http_get(url).await; // ссылка удерживается через await response.text().await } -// The compiler generates a state machine struct that is !Unpin, -// and the runtime pins it before calling Future::poll(). +// Компилятор генерирует структуру-автомат, которая является !Unpin, +// а рантайм закрепляет её перед вызовом Future::poll(). ``` -> **When to care about Pin**: (1) Implementing `Future` manually, (2) writing -> async runtimes or combinators, (3) any struct with self-referential pointers. -> For normal application code, `async/await` handles pinning transparently. -> See the companion *Async Rust Training* for deeper coverage. +> **Когда задумываться о Pin**: (1) при ручной реализации `Future`, (2) при написании асинхронных рантаймов или комбинаторов, (3) для любой структуры с самоссылающимися указателями. В обычном прикладном коде `async/await` прозрачно занимается закреплением. Подробнее см. в сопутствующем курсе *Async Rust Training*. > -> **Crate alternatives**: For self-referential structs without manual `Pin`, -> consider [`ouroboros`](https://crates.io/crates/ouroboros) or -> [`self_cell`](https://crates.io/crates/self_cell) — they generate safe -> wrappers with correct pinning and drop semantics. +> **Альтернативы в виде крейтов**: для самоссылающихся структур без ручного `Pin` рассмотрите [`ouroboros`](https://crates.io/crates/ouroboros) или [`self_cell`](https://crates.io/crates/self_cell). Они генерируют безопасные обёртки с правильным закреплением и семантикой drop. -### Pin Projections — Structural Pinning +### Проекции pin: структурное закрепление -When you have a `Pin<&mut MyStruct>`, you often need to access individual fields. -**Pin projection** is the pattern for safely going from `Pin<&mut Struct>` to -`Pin<&mut Field>` (for pinned fields) or `&mut Field` (for unpinned fields). +Когда у вас есть `Pin<&mut MyStruct>`, часто нужно обращаться к отдельным полям. **Проекция pin** это паттерн безопасного перехода от `Pin<&mut Struct>` к `Pin<&mut Field>` (для закреплённых полей) или к `&mut Field` (для незакреплённых полей). -#### The Problem: Field Access on Pinned Types +#### Проблема: доступ к полям закреплённых типов ```rust use std::pin::Pin; use std::marker::PhantomPinned; struct MyFuture { - data: String, // Regular field — safe to move - state: InternalState, // Self-referential — must stay pinned + data: String, // Обычное поле: безопасно перемещать + state: InternalState, // Самоссылающееся: должно оставаться закреплённым _pin: PhantomPinned, } enum InternalState { - Waiting { ptr: *const String }, // Points to `data` — self-referential + Waiting { ptr: *const String }, // Указывает на `data`: самоссылка Done, } -// Given `Pin<&mut MyFuture>`, how do you access `data` and `state`? -// You CAN'T just do `pinned.data` — the compiler won't let you -// get a &mut to a field of a pinned value without unsafe. +// Дано `Pin<&mut MyFuture>`, как получить доступ к `data` и `state`? +// НЕЛЬЗЯ просто написать `pinned.data`: компилятор не даст +// получить &mut к полю закреплённого значения без unsafe. ``` -#### Manual Pin Projection (unsafe) +#### Ручная проекция pin (unsafe) ```rust impl MyFuture { - // Project to `data` — this field is structurally unpinned (safe to move) + // Проекция на `data`: это поле структурно не закреплено (безопасно перемещать) fn data(self: Pin<&mut Self>) -> &mut String { - // SAFETY: `data` is not structurally pinned. Moving `data` alone - // doesn't move the whole struct, so Pin's guarantee is preserved. + // SAFETY: `data` структурно не закреплено. Перемещение только `data` + // не перемещает всю структуру, поэтому гарантия Pin сохраняется. unsafe { &mut self.get_unchecked_mut().data } } - // Project to `state` — this field IS structurally pinned + // Проекция на `state`: это поле структурно закреплено fn state(self: Pin<&mut Self>) -> Pin<&mut InternalState> { - // SAFETY: `state` is structurally pinned — we maintain the - // pin invariant by returning Pin<&mut InternalState>. + // SAFETY: `state` структурно закреплено: мы поддерживаем инвариант + // закрепления, возвращая Pin<&mut InternalState>. unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().state) } } } ``` -**Structural pinning rules** — a field is "structurally pinned" if: -1. Moving/swapping that field alone could invalidate a self-reference -2. The struct's `Drop` impl must not move the field -3. The struct must be `!Unpin` (enforced by `PhantomPinned` or a `!Unpin` field) +**Правила структурного закрепления**: поле «структурно закреплено», если: +1. Перемещение или обмен только этого поля может нарушить самоссылку +2. Реализация `Drop` для структуры не должна перемещать это поле +3. Структура должна быть `!Unpin` (обеспечивается `PhantomPinned` или полем `!Unpin`) -#### `pin-project` — Safe Pin Projections (Zero Unsafe) +#### `pin-project`: безопасные проекции pin (без unsafe) -The `pin-project` crate generates provably correct projections at compile time, -eliminating the need for manual `unsafe`: +Крейт `pin-project` генерирует корректные проекции на этапе компиляции, избавляя от необходимости ручного `unsafe`: ```rust use pin_project::pin_project; @@ -361,20 +340,20 @@ use std::pin::Pin; use std::future::Future; use std::task::{Context, Poll}; -#[pin_project] // <-- Generates projection methods +#[pin_project] // <-- Генерирует методы проекции struct TimedFuture { - #[pin] // <-- Structurally pinned (it's a Future) + #[pin] // <-- Структурно закреплено (это Future) inner: F, - started_at: std::time::Instant, // NOT pinned — plain data + started_at: std::time::Instant, // НЕ закреплено: обычные данные } impl Future for TimedFuture { type Output = (F::Output, std::time::Duration); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); // Safe! Generated by pin_project - // this.inner : Pin<&mut F> — pinned field - // this.started_at : &mut std::time::Instant — unpinned field + let this = self.project(); // Безопасно! Генерируется pin_project + // this.inner : Pin<&mut F> — закреплённое поле + // this.started_at : &mut std::time::Instant — незакреплённое поле match this.inner.poll(cx) { Poll::Ready(output) => { @@ -387,20 +366,19 @@ impl Future for TimedFuture { } ``` -#### `pin-project` vs Manual Projection +#### `pin-project` и ручная проекция -| Aspect | Manual (`unsafe`) | `pin-project` | -|--------|-------------------|---------------| -| Safety | You prove invariants | Compiler-verified | -| Boilerplate | Low (but error-prone) | Zero — derive macro | -| `Drop` interaction | Must not move pinned fields | Enforced: `#[pinned_drop]` | -| Compile-time cost | None | Proc-macro expansion | -| Use case | Primitives, `no_std` | Application / library code | +| Аспект | Вручную (`unsafe`) | `pin-project` | +|--------|--------------------|---------------| +| Безопасность | Вы доказываете инварианты | Проверяется компилятором | +| Шаблонный код | Мало (но легко ошибиться) | Нет: derive-макрос | +| Взаимодействие с `Drop` | Нельзя перемещать закреплённые поля | Обеспечивается через `#[pinned_drop]` | +| Стоимость на этапе компиляции | Нет | Раскрытие процедурного макроса | +| Применение | Примитивы, `no_std` | Прикладной код и библиотеки | -#### `#[pinned_drop]` — Drop for Pinned Types +#### `#[pinned_drop]`: Drop для закреплённых типов -When a type has `#[pin]` fields, `pin-project` requires `#[pinned_drop]` -instead of a regular `Drop` impl to prevent accidentally moving pinned fields: +Если у типа есть поля с `#[pin]`, `pin-project` требует `#[pinned_drop]` вместо обычной реализации `Drop`, чтобы случайно не переместить закреплённые поля: ```rust use pin_project::{pin_project, pinned_drop}; @@ -410,105 +388,95 @@ use std::pin::Pin; struct Connection { #[pin] future: F, - buffer: Vec, // Not pinned — can be moved in drop + buffer: Vec, // Не закреплено: можно переместить в drop } #[pinned_drop] impl PinnedDrop for Connection { fn drop(self: Pin<&mut Self>) { let this = self.project(); - // `this.future` is Pin<&mut F> — can't be moved, only dropped in place - // `this.buffer` is &mut Vec — can be drained, cleared, etc. + // `this.future` имеет тип Pin<&mut F>: его нельзя переместить, только уничтожить на месте + // `this.buffer` имеет тип &mut Vec: его можно опустошить, очистить и т. д. this.buffer.clear(); - println!("Connection dropped, buffer cleared"); + println!("Соединение уничтожено, буфер очищен"); } } ``` -#### When Pin Projections Matter in Practice +#### Когда проекции pin важны на практике -> **Note**: The diagram below uses Mermaid syntax. It renders on GitHub and in -> tools that support Mermaid (mdBook with `mermaid` plugin, VS Code with -> Mermaid extension). In plain Markdown viewers, you'll see the raw source. +> **Примечание**: диаграмма ниже использует синтаксис Mermaid. Она отображается на GitHub и в инструментах с поддержкой Mermaid (mdBook с плагином `mermaid`, VS Code с расширением Mermaid). В простых просмотрщиках Markdown вы увидите исходный код. ```mermaid graph TD - A["Do you implement Future manually?"] -->|Yes| B["Does the future hold references
across .await points?"] - A -->|No| C["async/await handles Pin for you
✅ No projections needed"] - B -->|Yes| D["Use #[pin_project] on your
future struct"] - B -->|No| E["Your future is Unpin
✅ No projections needed"] - D --> F["Mark futures/streams as #[pin]
Leave data fields unpinned"] - + A["Вы реализуете Future вручную?"] -->|Да| B["Удерживает ли future ссылки
через точки .await?"] + A -->|Нет| C["async/await сам занимается Pin
✅ Проекции не нужны"] + B -->|Да| D["Используйте #[pin_project] на
вашей структуре future"] + B -->|Нет| E["Ваш future является Unpin
✅ Проекции не нужны"] + D --> F["Помечайте futures/streams как #[pin]
Оставляйте поля данных незакреплёнными"] + style C fill:#91e5a3,color:#000 style E fill:#91e5a3,color:#000 style D fill:#ffa07a,color:#000 style F fill:#ffa07a,color:#000 ``` -> **Rule of thumb**: If you're wrapping another `Future` or `Stream`, use -> `pin-project`. If you're writing application code with `async/await`, you'll -> never need pin projections directly. See the companion -> *Async Rust Training* for async combinator patterns that use pin projections. +> **Практическое правило**: если вы оборачиваете другой `Future` или `Stream`, используйте `pin-project`. Если вы пишете прикладной код с `async/await`, проекции pin вам напрямую никогда не понадобятся. Подробнее о комбинаторах async с проекциями pin см. в сопутствующем курсе *Async Rust Training*. -### Drop Ordering and ManuallyDrop +### Порядок drop и ManuallyDrop -Rust's drop order is deterministic but has rules worth knowing: +Порядок drop в Rust детерминирован, но у него есть правила, которые стоит знать: -#### Drop Order Rules +#### Правила порядка drop ```rust struct Label(&'static str); impl Drop for Label { - fn drop(&mut self) { println!("Dropping {}", self.0); } + fn drop(&mut self) { println!("Уничтожается {}", self.0); } } fn main() { - let a = Label("first"); // Declared first - let b = Label("second"); // Declared second - let c = Label("third"); // Declared third -} -// Output: -// Dropping third ← locals drop in REVERSE declaration order -// Dropping second -// Dropping first + let a = Label("first"); // Объявлена первой + let b = Label("second"); // Объявлена второй + let c = Label("third"); // Объявлена третьей +} +// Вывод: +// Уничтожается third ← локальные переменные уничтожаются в ОБРАТНОМ порядке объявления +// Уничтожается second +// Уничтожается first ``` -**The three rules**: +**Три правила**: -| What | Drop Order | Rationale | -|------|-----------|----------| -| **Local variables** | Reverse declaration order | Later variables might reference earlier ones | -| **Struct fields** | Declaration order (top to bottom) | Matches construction order (stable since Rust 1.0, guaranteed by [RFC 1857](https://rust-lang.github.io/rfcs/1857-stabilize-drop-order.html)) | -| **Tuple elements** | Declaration order (left to right) | `(a, b, c)` → drop `a`, then `b`, then `c` | +| Что | Порядок drop | Обоснование | +|-----|--------------|-------------| +| **Локальные переменные** | В обратном порядке объявления | Более поздние переменные могут ссылаться на более ранние | +| **Поля структуры** | В порядке объявления (сверху вниз) | Совпадает с порядком создания (стабильно с Rust 1.0, гарантируется [RFC 1857](https://rust-lang.github.io/rfcs/1857-stabilize-drop-order.html)) | +| **Элементы кортежа** | В порядке объявления (слева направо) | `(a, b, c)`: сначала `a`, затем `b`, затем `c` | ```rust struct Server { - listener: Label, // Dropped 1st - handler: Label, // Dropped 2nd - logger: Label, // Dropped 3rd + listener: Label, // Уничтожается 1-м + handler: Label, // Уничтожается 2-м + logger: Label, // Уничтожается 3-м } -// Fields drop top-to-bottom (declaration order). -// This matters when fields reference each other or hold resources. +// Поля уничтожаются сверху вниз (в порядке объявления). +// Это важно, когда поля ссылаются друг на друга или держат ресурсы. ``` -> **Practical impact**: If your struct has a `JoinHandle` and a `Sender`, -> field order determines which drops first. If the thread reads from the -> channel, drop the `Sender` first (close the channel) so the thread exits, -> then join the handle. Put `Sender` above `JoinHandle` in the struct. +> **Практическое влияние**: если в вашей структуре есть `JoinHandle` и `Sender`, порядок полей определяет, что уничтожается первым. Если поток читает из канала, сначала уничтожьте `Sender` (закройте канал), чтобы поток завершился, а потом дождитесь `JoinHandle`. Поставьте `Sender` выше `JoinHandle` в структуре. -#### `ManuallyDrop` — Suppressing Automatic Drop +#### `ManuallyDrop`: подавление автоматического drop -`ManuallyDrop` wraps a value and prevents its destructor from running -automatically. You take responsibility for dropping it (or intentionally -leaking it): +`ManuallyDrop` оборачивает значение и не даёт его деструктору выполниться автоматически. Ответственность за уничтожение (или намеренную утечку) лежит на вас: ```rust use std::mem::ManuallyDrop; -// Use case 1: Prevent double-free in unsafe code +// Случай 1: предотвращаем двойное освобождение в unsafe-коде struct TwoPhaseBuffer { - // We need to drop the Vec ourselves to control timing + // Vec нужно уничтожить самим, чтобы контролировать момент data: ManuallyDrop>, committed: bool, } @@ -527,93 +495,91 @@ impl TwoPhaseBuffer { fn commit(&mut self) { self.committed = true; - println!("Committed {} bytes", self.data.len()); + println!("Зафиксировано байт: {}", self.data.len()); } } impl Drop for TwoPhaseBuffer { fn drop(&mut self) { if !self.committed { - println!("Rolling back — dropping uncommitted data"); + println!("Откат: уничтожаем незафиксированные данные"); } - // SAFETY: data is always valid here; we only drop it once. + // SAFETY: data здесь всегда корректен; уничтожаем его только один раз. unsafe { ManuallyDrop::drop(&mut self.data); } } } ``` ```rust -// Use case 2: Intentional leak (e.g., global singletons) +// Случай 2: намеренная утечка (например, глобальные синглтоны) fn leaked_string() -> &'static str { - // Box::leak() is the idiomatic way to create a &'static reference: - let s = String::from("lives forever"); + // Box::leak() — идиоматичный способ создать ссылку &'static: + let s = String::from("живёт вечно"); Box::leak(s.into_boxed_str()) - // ⚠️ This is a controlled memory leak. The String's heap allocation - // is never freed. Only use for long-lived singletons. + // ⚠️ Это контролируемая утечка памяти. Выделение String в куче + // никогда не освобождается. Используйте только для долгоживущих синглтонов. } -// ManuallyDrop alternative (requires unsafe): -// ⚠️ Prefer Box::leak() above — this is shown only to illustrate -// ManuallyDrop semantics (suppressing Drop while the heap data survives). +// Альтернатива через ManuallyDrop (требует unsafe): +// ⚠️ Предпочитайте Box::leak() выше: это показано только для иллюстрации +// семантики ManuallyDrop (подавление Drop при сохранении данных в куче). fn leaked_string_manual() -> &'static str { use std::mem::ManuallyDrop; - let md = ManuallyDrop::new(String::from("lives forever")); - // SAFETY: ManuallyDrop prevents deallocation; the heap data lives - // forever, so a 'static reference is valid. + let md = ManuallyDrop::new(String::from("живёт вечно")); + // SAFETY: ManuallyDrop предотвращает освобождение; данные в куче живут + // вечно, поэтому ссылка 'static корректна. unsafe { &*(md.as_str() as *const str) } } ``` ```rust -// Use case 3: Union fields (only one variant is valid at a time) +// Случай 3: поля union (в каждый момент действителен только один вариант) use std::mem::ManuallyDrop; union IntOrString { i: u64, s: ManuallyDrop, - // String has a Drop impl, so it MUST be wrapped in ManuallyDrop - // inside a union — the compiler can't know which field is active. + // String имеет реализацию Drop, поэтому он ДОЛЖЕН быть обёрнут в ManuallyDrop + // внутри union: компилятор не может знать, какое поле активно. } -// No automatic Drop — the code that constructs IntOrString must also -// handle cleanup. If the String variant is active, call: +// Автоматического Drop нет: код, который создаёт IntOrString, тоже должен +// заниматься очисткой. Если активен вариант String, вызовите: // unsafe { ManuallyDrop::drop(&mut value.s); } -// without a Drop impl, the union is simply leaked (no UB, just a leak). +// Без реализации Drop union просто утекает (без UB, только утечка). ``` -**ManuallyDrop vs `mem::forget`**: +**ManuallyDrop против `mem::forget`**: | | `ManuallyDrop` | `mem::forget(value)` | |---|---|---| -| When | Wrap at construction | Consume later | -| Access inner | `&*md` / `&mut *md` | Value is gone | -| Drop later | `ManuallyDrop::drop(&mut md)` | Not possible | -| Use case | Fine-grained lifecycle control | Fire-and-forget leak | +| Когда | Оборачивание при создании | Потребление позже | +| Доступ к внутреннему | `&*md` / `&mut *md` | Значение уже недоступно | +| Уничтожить позже | `ManuallyDrop::drop(&mut md)` | Невозможно | +| Сценарий | Тонкое управление жизненным циклом | «Выстрелил и забыл»: утечка | -> **Rule**: Use `ManuallyDrop` in unsafe abstractions where you need to control -> *exactly* when a destructor runs. In safe application code, you almost never -> need it — Rust's automatic drop ordering handles things correctly. +> **Правило**: используйте `ManuallyDrop` в небезопасных абстракциях, где нужно контролировать, *когда именно* выполняется деструктор. В безопасном прикладном коде это почти никогда не нужно: автоматический порядок drop в Rust справляется сам. -> **Key Takeaways — Smart Pointers** -> - `Box` for single ownership on heap; `Rc`/`Arc` for shared ownership (single-/multi-threaded) -> - `Cell`/`RefCell` provide interior mutability; `RefCell` panics on violations at runtime -> - `Cow` avoids allocation on the common path; `Pin` prevents moves for self-referential types -> - Drop order: fields drop in declaration order (RFC 1857); locals drop in reverse declaration order +> **Ключевые выводы: умные указатели** +> - `Box` для единоличного владения в куче; `Rc`/`Arc` для совместного владения (в одном потоке / в нескольких) +> - `Cell`/`RefCell` дают внутреннюю изменяемость; `RefCell` паникует при нарушении правил во время выполнения +> - `Cow` избегает выделения памяти на частом пути; `Pin` запрещает перемещение самоссылающихся типов +> - Порядок drop: поля уничтожаются в порядке объявления (RFC 1857); локальные переменные в обратном порядке объявления -> **See also:** [Ch 6 — Concurrency](ch06-concurrency-vs-parallelism-vs-threads.md) for Arc + Mutex patterns. [Ch 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) for PhantomData used with smart pointers. +> **См. также:** [гл. 6 — Конкурентность](ch06-concurrency-vs-parallelism-vs-threads.md) о паттернах Arc + Mutex. [гл. 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) о PhantomData с умными указателями. ```mermaid graph TD - Box["Box<T>
Single owner, heap"] --> Heap["Heap allocation"] - Rc["Rc<T>
Shared, single-thread"] --> Heap - Arc["Arc<T>
Shared, multi-thread"] --> Heap + Box["Box<T>
Единственный владелец, куча"] --> Heap["Размещение в куче"] + Rc["Rc<T>
Общий, один поток"] --> Heap + Arc["Arc<T>
Общий, несколько потоков"] --> Heap - Rc --> Weak1["Weak<T>
Non-owning"] - Arc --> Weak2["Weak<T>
Non-owning"] + Rc --> Weak1["Weak<T>
Невладеющий"] + Arc --> Weak2["Weak<T>
Невладеющий"] - Cell["Cell<T>
Copy interior mut"] --> Stack["Stack / interior"] - RefCell["RefCell<T>
Runtime borrow check"] --> Stack - Cow["Cow<T>
Clone on write"] --> Stack + Cell["Cell<T>
Внутр. изменяемость для Copy"] --> Stack["Стек / внутри значения"] + RefCell["RefCell<T>
Проверка заимствований во время выполнения"] --> Stack + Cow["Cow<T>
Клонирование при записи"] --> Stack style Box fill:#d4efdf,stroke:#27ae60,color:#000 style Rc fill:#e8f4f8,stroke:#2980b9,color:#000 @@ -629,12 +595,12 @@ graph TD --- -### Exercise: Reference-Counted Graph ★★ (~30 min) +### Упражнение: граф с подсчётом ссылок ★★ (~30 минут) -Build a directed graph using `Rc>` where each node has a name and a list of children. Create a cycle (A → B → C → A) using `Weak` to break the back-edge. Verify no memory leak with `Rc::strong_count`. +Постройте направленный граф с помощью `Rc>`, где каждый узел имеет имя и список потомков. Создайте цикл (A → B → C → A), используя `Weak` для обратного ребра. Убедитесь, что утечки памяти нет, с помощью `Rc::strong_count`.

-🔑 Solution +🔑 Решение ```rust use std::cell::RefCell; @@ -658,7 +624,7 @@ impl Node { impl Drop for Node { fn drop(&mut self) { - println!("Dropping {}", self.name); + println!("Уничтожается {}", self.name); } } @@ -667,27 +633,26 @@ fn main() { let b = Node::new("B"); let c = Node::new("C"); - // A → B → C, with C back-referencing A via Weak + // A → B → C, при этом C ссылается назад на A через Weak a.borrow_mut().children.push(Rc::clone(&b)); b.borrow_mut().children.push(Rc::clone(&c)); - c.borrow_mut().back_ref = Some(Rc::downgrade(&a)); // Weak ref! + c.borrow_mut().back_ref = Some(Rc::downgrade(&a)); // Weak-ссылка! - println!("A strong count: {}", Rc::strong_count(&a)); // 1 (only `a` binding) - println!("B strong count: {}", Rc::strong_count(&b)); // 2 (b + A's child) - println!("C strong count: {}", Rc::strong_count(&c)); // 2 (c + B's child) + println!("Сильных ссылок у A: {}", Rc::strong_count(&a)); // 1 (только привязка `a`) + println!("Сильных ссылок у B: {}", Rc::strong_count(&b)); // 2 (b + потомок A) + println!("Сильных ссылок у C: {}", Rc::strong_count(&c)); // 2 (c + потомок B) - // Upgrade the weak ref to prove it works: + // Поднимаем weak-ссылку, чтобы проверить, что она работает: let c_ref = c.borrow(); if let Some(back) = &c_ref.back_ref { if let Some(a_ref) = back.upgrade() { - println!("C points back to: {}", a_ref.borrow().name); + println!("C ссылается назад на: {}", a_ref.borrow().name); } } - // When a, b, c go out of scope, all Nodes drop (no cycle leak!) + // Когда a, b, c выходят из области видимости, все узлы уничтожаются (утечки цикла нет!) } ```
*** - diff --git a/rust-patterns-book/src/ch10-error-handling-patterns.md b/rust-patterns-book/src/ch10-error-handling-patterns.md index 18ac215..737a4cb 100644 --- a/rust-patterns-book/src/ch10-error-handling-patterns.md +++ b/rust-patterns-book/src/ch10-error-handling-patterns.md @@ -1,50 +1,50 @@ -# 9. Error Handling Patterns 🟢 +# 10. Паттерны обработки ошибок 🟢 -> **What you'll learn:** -> - When to use `thiserror` (libraries) vs `anyhow` (applications) -> - Error conversion chains with `#[from]` and `.context()` wrappers -> - How the `?` operator desugars and works in `main()` -> - When to panic vs return errors, and `catch_unwind` for FFI boundaries +> **Что вы узнаете:** +> - Когда использовать `thiserror` (библиотеки), а когда `anyhow` (приложения) +> - Цепочки преобразования ошибок через `#[from]` и обёртки `.context()` +> - Как оператор `?` раскрывается и как работает в `main()` +> - Когда паниковать, а когда возвращать ошибки, и `catch_unwind` для границ FFI -## thiserror vs anyhow — Library vs Application +## thiserror и anyhow: библиотеки и приложения -Rust error handling centers on the `Result` type. Two crates dominate: +Обработка ошибок в Rust строится вокруг типа `Result`. Доминируют два крейта: ```rust,ignore -// --- thiserror: For LIBRARIES --- -// Generates Display, Error, and From impls via derive macros +// --- thiserror: ДЛЯ БИБЛИОТЕК --- +// Генерирует реализации Display, Error и From через derive-макросы use thiserror::Error; #[derive(Error, Debug)] pub enum DatabaseError { - #[error("connection failed: {0}")] + #[error("ошибка подключения: {0}")] ConnectionFailed(String), - #[error("query error: {source}")] + #[error("ошибка запроса: {source}")] QueryError { #[source] source: sqlx::Error, }, - #[error("record not found: table={table} id={id}")] + #[error("запись не найдена: таблица={table}, id={id}")] NotFound { table: String, id: u64 }, - #[error(transparent)] // Delegate Display to the inner error - Io(#[from] std::io::Error), // Auto-generates From + #[error(transparent)] // Делегировать Display внутренней ошибке + Io(#[from] std::io::Error), // Автоматически генерирует From } -// --- anyhow: For APPLICATIONS --- -// Dynamic error type — great for top-level code where you just want errors to propagate +// --- anyhow: ДЛЯ ПРИЛОЖЕНИЙ --- +// Динамический тип ошибки: удобен в верхнеуровневом коде, где нужно просто пробросить ошибку дальше use anyhow::{Context, Result, bail, ensure}; fn read_config(path: &str) -> Result { let content = std::fs::read_to_string(path) - .with_context(|| format!("failed to read config from {path}"))?; + .with_context(|| format!("не удалось прочитать конфигурацию из {path}"))?; let config: Config = serde_json::from_str(&content) - .context("failed to parse config JSON")?; + .context("не удалось разобрать JSON конфигурации")?; - ensure!(config.port > 0, "port must be positive, got {}", config.port); + ensure!(config.port > 0, "порт должен быть положительным, получено {}", config.port); Ok(config) } @@ -53,40 +53,40 @@ fn main() -> Result<()> { let config = read_config("server.toml")?; if config.name.is_empty() { - bail!("server name cannot be empty"); // Return Err immediately + bail!("имя сервера не может быть пустым"); // Немедленно возвращаем Err } Ok(()) } ``` -**When to use which**: +**Когда что использовать**: | | `thiserror` | `anyhow` | |---|---|---| -| **Use in** | Libraries, shared crates | Applications, binaries | -| **Error types** | Concrete enums — callers can match | `anyhow::Error` — opaque | -| **Effort** | Define your error enum | Just use `Result` | -| **Downcasting** | Not needed — pattern match | `error.downcast_ref::()` | +| **Где использовать** | Библиотеки, общие крейты | Приложения, исполняемые файлы | +| **Типы ошибок** | Конкретные перечисления: вызывающий код может сопоставлять их с образцом | `anyhow::Error`: непрозрачный тип | +| **Усилия** | Определить собственное перечисление ошибок | Просто использовать `Result` | +| **Приведение типа (downcasting)** | Не нужно: сопоставление с образцом | `error.downcast_ref::()` | -### Error Conversion Chains (#[from]) +### Цепочки преобразования ошибок (#[from]) ```rust,ignore use thiserror::Error; #[derive(Error, Debug)] enum AppError { - #[error("I/O error: {0}")] + #[error("ошибка ввода-вывода: {0}")] Io(#[from] std::io::Error), - #[error("JSON error: {0}")] + #[error("ошибка JSON: {0}")] Json(#[from] serde_json::Error), - #[error("HTTP error: {0}")] + #[error("ошибка HTTP: {0}")] Http(#[from] reqwest::Error), } -// Now ? automatically converts: +// Теперь ? автоматически выполняет преобразование: fn fetch_and_parse(url: &str) -> Result { let body = reqwest::blocking::get(url)?.text()?; // reqwest::Error → AppError::Http let config: Config = serde_json::from_str(&body)?; // serde_json::Error → AppError::Json @@ -94,109 +94,106 @@ fn fetch_and_parse(url: &str) -> Result { } ``` -### Context and Error Wrapping +### Контекст и оборачивание ошибок -Add human-readable context to errors without losing the original: +Добавляйте понятный контекст к ошибкам, не теряя исходную: ```rust,ignore use anyhow::{Context, Result}; fn process_file(path: &str) -> Result { let content = std::fs::read_to_string(path) - .with_context(|| format!("failed to read {path}"))?; + .with_context(|| format!("не удалось прочитать {path}"))?; let data = parse_content(&content) - .with_context(|| format!("failed to parse {path}"))?; + .with_context(|| format!("не удалось разобрать {path}"))?; validate(&data) - .context("validation failed")?; + .context("ошибка валидации")?; Ok(data) } -// Error output: -// Error: validation failed +// Вывод ошибки: +// Error: ошибка валидации // // Caused by: -// 0: failed to parse config.json +// 0: не удалось разобрать config.json // 1: expected ',' at line 5 column 12 ``` -### The ? Operator in Depth +### Оператор ? подробнее -`?` is syntactic sugar for a `match` + `From` conversion + early return: +`?` это синтаксический сахар для `match`, преобразования `From` и раннего возврата: ```rust -// This: +// Это: let value = operation()?; -// Desugars to: +// Раскрывается в: let value = match operation() { Ok(v) => v, Err(e) => return Err(From::from(e)), // ^^^^^^^^^^^^^^ - // Automatic conversion via From trait + // Автоматическое преобразование через трейт From }; ``` -**`?` also works with `Option`** (in functions returning `Option`): +**`?` также работает с `Option`** (в функциях, которые возвращают `Option`): ```rust fn find_user_email(users: &[User], name: &str) -> Option { - let user = users.iter().find(|u| u.name == name)?; // Returns None if not found - let email = user.email.as_ref()?; // Returns None if email is None + let user = users.iter().find(|u| u.name == name)?; // Возвращает None, если не найден + let email = user.email.as_ref()?; // Возвращает None, если email равен None Some(email.to_uppercase()) } ``` -### Panics, catch_unwind, and When to Abort +### Паника, catch_unwind и когда прерывать работу ```rust -// Panics: for BUGS, not expected errors +// Паника: для ОШИБОК В КОДЕ, а не для ожидаемых ошибок fn get_element(data: &[i32], index: usize) -> &i32 { - // If this panics, it's a programming error (bug). - // Don't "handle" it — fix the caller. + // Если здесь случится паника, это ошибка программиста (баг). + // Не «обрабатывайте» её, а исправьте вызывающий код. &data[index] } -// catch_unwind: for boundaries (FFI, thread pools) +// catch_unwind: для границ (FFI, пулы потоков) use std::panic; let result = panic::catch_unwind(|| { - // Run potentially panicking code safely + // Безопасно запускаем код, который может паниковать risky_operation() }); match result { - Ok(value) => println!("Success: {value:?}"), - Err(_) => eprintln!("Operation panicked — continuing safely"), + Ok(value) => println!("Успех: {value:?}"), + Err(_) => eprintln!("Операция завершилась паникой, продолжаем работу"), } -// When to use which: -// - Result → expected failures (file not found, network timeout) -// - panic!() → programming bugs (index out of bounds, invariant violated) -// - process::abort() → unrecoverable state (security violation, corrupt data) +// Когда что использовать: +// - Result → ожидаемые сбои (файл не найден, таймаут сети) +// - panic!() → ошибки в программе (выход за границы индекса, нарушен инвариант) +// - process::abort() → невосстановимое состояние (нарушение безопасности, повреждённые данные) ``` -> **C++ comparison**: `Result` replaces exceptions for expected errors. -> `panic!()` is like `assert()` or `std::terminate()` — it's for bugs, not -> control flow. Rust's `?` operator makes error propagation as ergonomic as -> exceptions without the unpredictable control flow. +> **Сравнение с C++**: `Result` заменяет исключения для ожидаемых ошибок. `panic!()` похож на `assert()` или `std::terminate()`: он предназначен для багов, а не для управления ходом программы. Оператор `?` делает распространение ошибок таким же удобным, как исключения, но без непредсказуемого потока управления. -> **Key Takeaways — Error Handling** -> - Libraries: `thiserror` for structured error enums; applications: `anyhow` for ergonomic propagation -> - `#[from]` auto-generates `From` impls; `.context()` adds human-readable wrappers -> - `?` desugars to `From::from()` + early return; works in `main()` returning `Result` +> **Ключевые выводы: обработка ошибок** +> - Библиотеки: `thiserror` для структурированных перечислений ошибок; приложения: `anyhow` для удобного распространения ошибок +> - `#[from]` автоматически генерирует реализации `From`; `.context()` добавляет понятные обёртки +> - `?` раскрывается в `From::from()` и ранний возврат; работает в `main()`, возвращающей `Result` -> **See also:** [Ch 14 — API Design](ch14-crate-architecture-and-api-design.md) for "parse, don't validate" patterns. [Ch 10 — Serialization](ch10-serialization-zero-copy-and-binary-data.md) for serde error handling. +> **См. также:** [гл. 15 — Дизайн API](ch15-crate-architecture-and-api-design.md) о паттернах «parse, don't validate». [гл. 11 — Сериализация](ch11-serialization-zero-copy-and-binary-data.md) об обработке ошибок в serde. ```mermaid flowchart LR A["std::io::Error"] -->|"#[from]"| B["AppError::Io"] C["serde_json::Error"] -->|"#[from]"| D["AppError::Json"] - E["Custom validation"] -->|"manual"| F["AppError::Validation"] + E["Пользовательская проверка"] -->|"вручную"| F["AppError::Validation"] - B --> G["? operator"] + B --> G["Оператор ?"] D --> G F --> G G --> H["Result<T, AppError>"] @@ -213,33 +210,33 @@ flowchart LR --- -### Exercise: Error Hierarchy with thiserror ★★ (~30 min) +### Упражнение: иерархия ошибок на thiserror ★★ (~30 минут) -Design an error type hierarchy for a file-processing application that can fail during I/O, parsing (JSON and CSV), and validation. Use `thiserror` and demonstrate `?` propagation. +Спроектируйте иерархию типов ошибок для приложения обработки файлов, которое может завершиться ошибкой при вводе-выводе, разборе (JSON и CSV) или валидации. Используйте `thiserror` и продемонстрируйте распространение ошибок через `?`.
-🔑 Solution +🔑 Решение ```rust,ignore use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { - #[error("I/O error: {0}")] + #[error("ошибка ввода-вывода: {0}")] Io(#[from] std::io::Error), - #[error("JSON parse error: {0}")] + #[error("ошибка разбора JSON: {0}")] Json(#[from] serde_json::Error), - #[error("CSV error at line {line}: {message}")] + #[error("ошибка CSV в строке {line}: {message}")] Csv { line: usize, message: String }, - #[error("validation error: {field} — {reason}")] + #[error("ошибка валидации: {field} — {reason}")] Validation { field: String, reason: String }, } fn read_file(path: &str) -> Result { - Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io via #[from] + Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io через #[from] } fn parse_json(content: &str) -> Result { @@ -251,13 +248,13 @@ fn validate_name(value: &serde_json::Value) -> Result { .and_then(|v| v.as_str()) .ok_or_else(|| AppError::Validation { field: "name".into(), - reason: "must be a non-null string".into(), + reason: "должно быть строкой, а не null".into(), })?; if name.is_empty() { return Err(AppError::Validation { field: "name".into(), - reason: "must not be empty".into(), + reason: "не должно быть пустым".into(), }); } @@ -273,8 +270,8 @@ fn process_file(path: &str) -> Result { fn main() { match process_file("config.json") { - Ok(name) => println!("Name: {name}"), - Err(e) => eprintln!("Error: {e}"), + Ok(name) => println!("Имя: {name}"), + Err(e) => eprintln!("Ошибка: {e}"), } } ``` @@ -282,4 +279,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch11-serialization-zero-copy-and-binary-data.md b/rust-patterns-book/src/ch11-serialization-zero-copy-and-binary-data.md index d9bb786..225de62 100644 --- a/rust-patterns-book/src/ch11-serialization-zero-copy-and-binary-data.md +++ b/rust-patterns-book/src/ch11-serialization-zero-copy-and-binary-data.md @@ -1,15 +1,14 @@ -# 10. Serialization, Zero-Copy, and Binary Data 🟡 +# 11. Сериализация, zero-copy и бинарные данные 🟡 -> **What you'll learn:** -> - serde fundamentals: derive macros, attributes, and enum representations -> - Zero-copy deserialization for high-performance read-heavy workloads -> - The serde format ecosystem (JSON, TOML, bincode, MessagePack) -> - Binary data handling with `repr(C)`, zerocopy, and `bytes::Bytes` +> **Что вы узнаете:** +> - Основы serde: derive-макросы, атрибуты и представления перечислений +> - Десериализацию без копирования (zero-copy) для высоконагруженных сценариев чтения +> - Экосистему форматов serde (JSON, TOML, bincode, MessagePack) +> - Работу с бинарными данными через `repr(C)`, zerocopy и `bytes::Bytes` -## serde Fundamentals +## Основы serde -`serde` (SERialize/DEserialize) is the universal serialization framework for Rust. -It separates **data model** (your structs) from **format** (JSON, TOML, binary): +`serde` (SERialize/DEserialize) это универсальная система сериализации для Rust. Она отделяет **модель данных** (ваши структуры) от **формата** (JSON, TOML, бинарный): ```rust,ignore use serde::{Serialize, Deserialize}; @@ -18,14 +17,14 @@ use serde::{Serialize, Deserialize}; struct ServerConfig { name: String, port: u16, - #[serde(default)] // Use Default::default() if missing + #[serde(default)] // Использовать Default::default(), если поле отсутствует max_connections: usize, #[serde(skip_serializing_if = "Option::is_none")] tls_cert_path: Option, } fn main() -> Result<(), Box> { - // Deserialize from JSON: + // Десериализация из JSON: let json_input = r#"{ "name": "hw-diag", "port": 8080 @@ -34,11 +33,11 @@ fn main() -> Result<(), Box> { println!("{config:?}"); // ServerConfig { name: "hw-diag", port: 8080, max_connections: 0, tls_cert_path: None } - // Serialize to JSON: + // Сериализация в JSON: let output = serde_json::to_string_pretty(&config)?; println!("{output}"); - // Same struct, different format — no code changes: + // Та же структура, другой формат, без изменений в коде: let toml_input = r#" name = "hw-diag" port = 8080 @@ -50,49 +49,47 @@ fn main() -> Result<(), Box> { } ``` -> **Key insight**: Your struct derives `Serialize` and `Deserialize` once. -> Then it works with *every* serde-compatible format — JSON, TOML, YAML, -> bincode, MessagePack, CBOR, postcard, and dozens more. +> **Ключевая мысль**: ваша структура один раз получает derive `Serialize` и `Deserialize`. После этого она работает с *любым* совместимым с serde форматом: JSON, TOML, YAML, bincode, MessagePack, CBOR, postcard и десятками других. -### Common serde Attributes +### Распространённые атрибуты serde -serde provides fine-grained control over serialization through field and container attributes: +serde даёт тонкое управление сериализацией через атрибуты полей и контейнеров: ```rust,ignore use serde::{Serialize, Deserialize}; -// --- Container attributes (on the struct/enum) --- +// --- Атрибуты контейнера (на структуре или перечислении) --- #[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] // JSON convention: field_name → fieldName -#[serde(deny_unknown_fields)] // Reject extra keys — strict parsing +#[serde(rename_all = "camelCase")] // Соглашение JSON: field_name → fieldName +#[serde(deny_unknown_fields)] // Отвергать лишние ключи: строгий разбор struct DiagResult { - test_name: String, // Serialized as "testName" - pass_count: u32, // Serialized as "passCount" - fail_count: u32, // Serialized as "failCount" + test_name: String, // Сериализуется как "testName" + pass_count: u32, // Сериализуется как "passCount" + fail_count: u32, // Сериализуется как "failCount" } -// --- Field attributes --- +// --- Атрибуты полей --- #[derive(Serialize, Deserialize)] struct Sensor { - #[serde(rename = "sensor_id")] // Override field name for serialization + #[serde(rename = "sensor_id")] // Переопределить имя поля при сериализации id: u64, - #[serde(default)] // Use Default if missing from input + #[serde(default)] // Использовать Default, если поля нет во входных данных enabled: bool, #[serde(default = "default_threshold")] threshold: f64, - #[serde(skip)] // Never serialize or deserialize + #[serde(skip)] // Никогда не сериализовать и не десериализовать cached_value: Option, #[serde(skip_serializing_if = "Vec::is_empty")] tags: Vec, - #[serde(flatten)] // Inline nested struct fields + #[serde(flatten)] // Встроить поля вложенной структуры metadata: Metadata, - #[serde(with = "hex_bytes")] // Custom ser/de module + #[serde(with = "hex_bytes")] // Пользовательский модуль сериализации/десериализации raw_data: Vec, } @@ -103,35 +100,35 @@ struct Metadata { vendor: String, model: String, } -// With #[serde(flatten)], the JSON looks like: +// С #[serde(flatten)] JSON выглядит так: // { "sensor_id": 1, "vendor": "Intel", "model": "X200", ... } -// NOT: { "sensor_id": 1, "metadata": { "vendor": "Intel", ... } } +// А НЕ так: { "sensor_id": 1, "metadata": { "vendor": "Intel", ... } } ``` -**Most-used attributes cheat sheet**: +**Шпаргалка по самым используемым атрибутам**: -| Attribute | Level | Effect | -|-----------|-------|--------| -| `rename_all = "camelCase"` | Container | Rename all fields to camelCase/snake_case/SCREAMING_SNAKE_CASE | -| `deny_unknown_fields` | Container | Error on unexpected keys (strict mode) | -| `default` | Field | Use `Default::default()` when field missing | -| `rename = "..."` | Field | Custom serialized name | -| `skip` | Field | Exclude from ser/de entirely | -| `skip_serializing_if = "fn"` | Field | Conditionally exclude (e.g., `Option::is_none`) | -| `flatten` | Field | Inline a nested struct's fields | -| `with = "module"` | Field | Use custom serialize/deserialize functions | -| `alias = "..."` | Field | Accept alternative names during deserialization | -| `deserialize_with = "fn"` | Field | Custom deserialize function only | -| `untagged` | Enum | Try each variant in order (no discriminant in output) | +| Атрибут | Уровень | Эффект | +|---------|---------|--------| +| `rename_all = "camelCase"` | Контейнер | Переименовать все поля в camelCase, snake_case или SCREAMING_SNAKE_CASE | +| `deny_unknown_fields` | Контейнер | Ошибка при неожиданных ключах (строгий режим) | +| `default` | Поле | Использовать `Default::default()`, если поле отсутствует | +| `rename = "..."` | Поле | Пользовательское имя при сериализации | +| `skip` | Поле | Полностью исключить из сериализации и десериализации | +| `skip_serializing_if = "fn"` | Поле | Условно исключить (например, `Option::is_none`) | +| `flatten` | Поле | Встроить поля вложенной структуры | +| `with = "module"` | Поле | Использовать пользовательские функции сериализации и десериализации | +| `alias = "..."` | Поле | Принимать альтернативные имена при десериализации | +| `deserialize_with = "fn"` | Поле | Только пользовательская функция десериализации | +| `untagged` | Перечисление | Пробовать каждый вариант по порядку (в выводе нет дискриминанта) | -### Enum Representations +### Представления перечислений -serde provides four representations for enums in formats like JSON: +serde предоставляет четыре представления перечислений для форматов вроде JSON: ```rust,ignore use serde::{Serialize, Deserialize}; -// 1. Externally tagged (DEFAULT): +// 1. Внешне помеченное (по умолчанию): #[derive(Serialize, Deserialize)] enum Command { Reboot, @@ -141,7 +138,7 @@ enum Command { // "Reboot" → Command::Reboot // {"RunDiag": {"test_name": "gpu", "timeout_secs": 60}} → Command::RunDiag { ... } -// 2. Internally tagged — #[serde(tag = "type")]: +// 2. Внутренне помеченное: #[serde(tag = "type")]: #[derive(Serialize, Deserialize)] #[serde(tag = "type")] enum Event { @@ -152,7 +149,7 @@ enum Event { // {"type": "Start", "timestamp": 1706000000} // {"type": "Error", "code": 42, "message": "timeout"} -// 3. Adjacently tagged — #[serde(tag = "t", content = "c")]: +// 3. Смежно помеченное: #[serde(tag = "t", content = "c")]: #[derive(Serialize, Deserialize)] #[serde(tag = "t", content = "c")] enum Payload { @@ -162,7 +159,7 @@ enum Payload { // {"t": "Text", "c": "hello"} // {"t": "Binary", "c": [0, 1, 2]} -// 4. Untagged — #[serde(untagged)]: +// 4. Без метки: #[serde(untagged)]: #[derive(Serialize, Deserialize)] #[serde(untagged)] enum StringOrNumber { @@ -171,104 +168,98 @@ enum StringOrNumber { } // "hello" → StringOrNumber::Str("hello") // 42.0 → StringOrNumber::Num(42.0) -// ⚠️ Tried IN ORDER — first matching variant wins +// ⚠️ Проверяются ПО ПОРЯДКУ: побеждает первый подходящий вариант ``` -> **Which representation to choose**: Use internally tagged (`tag = "type"`) -> for most JSON APIs — it's the most readable and matches conventions in -> Go, Python, and TypeScript. Use untagged only for "union" types where the -> shape alone disambiguates. +> **Какое представление выбрать**: для большинства JSON API используйте внутренне помеченное (`tag = "type"`): оно самое читаемое и соответствует соглашениям в Go, Python и TypeScript. Без метки используйте только для «объединённых» типов, где одна лишь форма однозначно определяет вариант. -### Zero-Copy Deserialization +### Десериализация без копирования -serde can deserialize without allocating new strings — borrowing directly from -the input buffer. This is the key to high-performance parsing: +serde умеет десериализовать без выделения памяти под новые строки: заимствовать данные прямо из входного буфера. Это ключ к высокопроизводительному разбору: ```rust,ignore use serde::Deserialize; -// --- Owned (allocating) --- -// Each String field copies bytes from the input into new heap allocations. +// --- Владеющая (с выделением памяти) --- +// Каждое строковое поле копирует байты из входных данных в новую выделенную память в куче. #[derive(Deserialize)] struct OwnedRecord { - name: String, // Allocates a new String - value: String, // Allocates another String + name: String, // Выделяет новую String + value: String, // Выделяет ещё одну String } -// --- Zero-copy (borrowing) --- -// &'de str fields borrow directly from the input — ZERO allocation. +// --- Без копирования (заимствование) --- +// Поля &'de str заимствуют данные напрямую из входа: НИКАКИХ выделений памяти. #[derive(Deserialize)] struct BorrowedRecord<'a> { - name: &'a str, // Points into the input buffer - value: &'a str, // Points into the input buffer + name: &'a str, // Указывает во входной буфер + value: &'a str, // Указывает во входной буфер } fn main() { let input = r#"{"name": "cpu_temp", "value": "72.5"}"#; - // Owned: allocates two String objects + // Владеющая: выделяет два объекта String let owned: OwnedRecord = serde_json::from_str(input).unwrap(); - // Zero-copy: `name` and `value` point into `input` — no allocation + // Без копирования: `name` и `value` указывают в `input`, без выделения памяти let borrowed: BorrowedRecord = serde_json::from_str(input).unwrap(); - // The output is lifetime-bound: borrowed can't outlive input + // Результат привязан ко времени жизни: borrowed не может пережить input println!("{}: {}", borrowed.name, borrowed.value); } ``` -**Understanding the lifetime**: +**Понимание времени жизни**: ```rust,ignore -// Deserialize<'de> — the struct can borrow from data with lifetime 'de: +// Deserialize<'de>: структура может заимствовать данные с временем жизни 'de: // struct BorrowedRecord<'a> where 'a == 'de -// Only works when the input buffer lives long enough +// Работает, только если входной буфер живёт достаточно долго -// DeserializeOwned — the struct owns all its data, no borrowing: +// DeserializeOwned: структура владеет всеми своими данными, без заимствования: // trait DeserializeOwned: for<'de> Deserialize<'de> {} -// Works with any input lifetime (the struct is independent) +// Работает с любым временем жизни входа (структура независима от него) use serde::de::DeserializeOwned; -// This function requires owned types — input can be temporary +// Эта функция требует владеющих типов: вход может быть временным fn parse_owned(input: &str) -> T { serde_json::from_str(input).unwrap() } -// This function allows borrowing — more efficient but restricts lifetimes +// Эта функция допускает заимствование: эффективнее, но ограничивает времена жизни fn parse_borrowed<'a, T: Deserialize<'a>>(input: &'a str) -> T { serde_json::from_str(input).unwrap() } ``` -**When to use zero-copy**: -- Parsing large files where you only need a few fields -- High-throughput pipelines (network packets, log lines) -- When the input buffer already lives long enough (e.g., memory-mapped file) +**Когда использовать zero-copy**: +- Разбор больших файлов, из которых нужны лишь несколько полей +- Конвейеры с высокой пропускной способностью (сетевые пакеты, строки журнала) +- Когда входной буфер уже живёт достаточно долго (например, memory-mapped файл) -**When NOT to use zero-copy**: -- Input is ephemeral (network read buffer that's reused) -- You need to store the result beyond the input's lifetime -- Fields need transformation (escapes, normalization) +**Когда НЕ использовать zero-copy**: +- Вход эфемерен (буфер сетевого чтения, который переиспользуется) +- Результат нужно хранить дольше, чем живёт вход +- Полям нужны преобразования (экранирование, нормализация) -> **Practical tip**: `Cow<'a, str>` gives you the best of both — borrow when -> possible, allocate when necessary (e.g., when JSON escape sequences need -> unescaping). serde supports Cow natively. +> **Практический совет**: `Cow<'a, str>` даёт лучшее из обоих миров: заимствует, когда возможно, и выделяет память, когда необходимо (например, когда нужно раскрыть экранированные последовательности JSON). serde поддерживает Cow напрямую. -### The Format Ecosystem +### Экосистема форматов -| Format | Crate | Human-Readable | Size | Speed | Use Case | -|--------|-------|:--------------:|:----:|:-----:|----------| -| JSON | `serde_json` | ✅ | Large | Good | Config files, REST APIs, logging | -| TOML | `toml` | ✅ | Medium | Good | Config files (Cargo.toml style) | -| YAML | `serde_yaml` | ✅ | Medium | Good | Config files (complex nesting) | -| bincode | `bincode` | ❌ | Small | Fast | IPC, caches, Rust-to-Rust | -| postcard | `postcard` | ❌ | Tiny | Very fast | Embedded systems, `no_std` | -| MessagePack | `rmp-serde` | ❌ | Small | Fast | Cross-language binary protocol | -| CBOR | `ciborium` | ❌ | Small | Fast | IoT, constrained environments | +| Формат | Крейт | Читается человеком | Размер | Скорость | Сценарий использования | +|--------|-------|:------------------:|:------:|:--------:|------------------------| +| JSON | `serde_json` | ✅ | Большой | Хорошая | Конфигурационные файлы, REST API, журналирование | +| TOML | `toml` | ✅ | Средний | Хорошая | Конфигурационные файлы (в стиле Cargo.toml) | +| YAML | `serde_yaml` | ✅ | Средний | Хорошая | Конфигурационные файлы (сложная вложенность) | +| bincode | `bincode` | ❌ | Маленький | Быстрая | IPC, кэши, обмен между программами на Rust | +| postcard | `postcard` | ❌ | Крошечный | Очень быстрая | Встраиваемые системы, `no_std` | +| MessagePack | `rmp-serde` | ❌ | Маленький | Быстрая | Межъязыковой бинарный протокол | +| CBOR | `ciborium` | ❌ | Маленький | Быстрая | IoT, ограниченные окружения | ```rust -// Same struct, many formats — serde's power: +// Одна структура, множество форматов: сила serde #[derive(serde::Serialize, serde::Deserialize, Debug)] struct DiagConfig { @@ -284,30 +275,29 @@ let config = DiagConfig { }; // JSON: {"name":"accel_diag","tests":["memory","compute"],"timeout_secs":300} -let json = serde_json::to_string(&config).unwrap(); // 67 bytes +let json = serde_json::to_string(&config).unwrap(); // 67 байт -// bincode: compact binary — ~40 bytes, no field names -let bin = bincode::serialize(&config).unwrap(); // Much smaller +// bincode: компактный бинарный формат, ~40 байт, без имён полей +let bin = bincode::serialize(&config).unwrap(); // Заметно меньше -// postcard: even smaller, varint encoding — great for embedded +// postcard: ещё меньше, кодирование varint, отлично для встраиваемых систем // let post = postcard::to_allocvec(&config).unwrap(); ``` -> **Choose your format**: -> - Config files humans edit → TOML or JSON -> - Rust-to-Rust IPC/caching → bincode (fast, compact, not cross-language) -> - Cross-language binary → MessagePack or CBOR -> - Embedded / `no_std` → postcard +> **Выбирайте формат**: +> - Конфигурационные файлы, которые редактируют люди: TOML или JSON +> - IPC и кэширование между программами на Rust: bincode (быстро, компактно, но только для Rust) +> - Межъязыковой бинарный формат: MessagePack или CBOR +> - Встраиваемые системы и `no_std`: postcard -### Binary Data and repr(C) +### Бинарные данные и repr(C) -For hardware diagnostics, parsing binary protocol data is common. Rust provides -tools for safe, zero-copy binary data handling: +В аппаратной диагностике часто приходится разбирать данные бинарных протоколов. Rust предоставляет инструменты для безопасной работы с бинарными данными без копирования: ```rust -// --- #[repr(C)]: Predictable memory layout --- -// Ensures fields are laid out in declaration order with C padding rules. -// Essential for matching hardware register layouts and protocol headers. +// --- #[repr(C)]: предсказуемая раскладка в памяти --- +// Гарантирует, что поля размещаются в порядке объявления по правилам выравнивания C. +// Необходимо для соответствия раскладке регистров оборудования и заголовков протоколов. #[repr(C)] #[derive(Debug, Clone, Copy)] @@ -320,7 +310,7 @@ struct IpmiHeader { cmd: u8, } -// --- Safe binary parsing with manual deserialization --- +// --- Безопасный разбор бинарных данных вручную --- impl IpmiHeader { fn from_bytes(data: &[u8]) -> Option { if data.len() < std::mem::size_of::() { @@ -340,7 +330,7 @@ impl IpmiHeader { fn lun(&self) -> u8 { self.net_fn_lun & 0x03 } } -// --- Endianness-aware parsing --- +// --- Разбор с учётом порядка байтов (endianness) --- fn read_u16_le(data: &[u8], offset: usize) -> u16 { u16::from_le_bytes([data[offset], data[offset + 1]]) } @@ -352,25 +342,25 @@ fn read_u32_be(data: &[u8], offset: usize) -> u32 { ]) } -// --- #[repr(C, packed)]: Remove padding (alignment = 1) --- +// --- #[repr(C, packed)]: убираем выравнивание (alignment = 1) --- #[repr(C, packed)] #[derive(Debug, Clone, Copy)] struct PcieCapabilityHeader { - cap_id: u8, // Capability ID - next_cap: u8, // Pointer to next capability - cap_reg: u16, // Capability-specific register + cap_id: u8, // Идентификатор capability + next_cap: u8, // Указатель на следующую capability + cap_reg: u16, // Регистр, специфичный для capability } -// ⚠️ Packed structs: taking &field creates an unaligned reference — UB. -// Always copy fields out: let id = header.cap_id; // OK (Copy) -// Never do: let r = &header.cap_reg; // UB if unaligned +// ⚠️ Упакованные структуры: взятие &field создаёт невыровненную ссылку — UB. +// Всегда копируйте поля: let id = header.cap_id; // OK (Copy) +// Никогда не делайте так: let r = &header.cap_reg; // UB, если невыровнено ``` -### zerocopy and bytemuck — Safe Transmutation +### zerocopy и bytemuck: безопасная трансмутация -Instead of `unsafe` transmute, use crates that verify layout safety at compile time: +Вместо `unsafe` transmute используйте крейты, которые проверяют безопасность раскладки: ```rust -// --- zerocopy: Compile-time checked zero-copy conversions --- +// --- zerocopy: преобразования без копирования с проверкой раскладки на этапе компиляции --- // Cargo.toml: zerocopy = { version = "0.8", features = ["derive"] } use zerocopy::{FromBytes, IntoBytes, KnownLayout, Immutable}; @@ -381,16 +371,18 @@ struct SensorReading { sensor_id: u16, flags: u8, _reserved: u8, - value: u32, // Fixed-point: actual = value / 1000.0 + value: u32, // Фиксированная точка: реальное значение = value / 1000.0 } fn parse_sensor(raw: &[u8]) -> Option<&SensorReading> { - // Safe zero-copy: verifies alignment and size AT COMPILE TIME + // Безопасное zero-copy: раскладка типа проверяется на этапе компиляции (через derive), + // а размер и выравнивание конкретного среза проверяются при вызове. + // При несоответствии возвращается None. SensorReading::ref_from_bytes(raw).ok() - // Returns &SensorReading pointing INTO raw — no copy, no allocation + // Возвращает &SensorReading, указывающий ВНУТРЬ raw: без копирования и выделения памяти } -// --- bytemuck: Simple, battle-tested --- +// --- bytemuck: простой, проверенный временем --- // Cargo.toml: bytemuck = { version = "1", features = ["derive"] } use bytemuck::{Pod, Zeroable}; @@ -403,95 +395,92 @@ struct GpuRegister { } fn cast_registers(data: &[u8]) -> &[GpuRegister] { - // Safe cast: Pod guarantees all bit patterns are valid + // Безопасное приведение: Pod гарантирует, что все битовые комбинации допустимы bytemuck::cast_slice(data) } ``` -**When to use which**: +**Когда что использовать**: -| Approach | Safety | Overhead | Use When | -|----------|:------:|:--------:|----------| -| Manual field-by-field parsing | ✅ Safe | Copy fields | Small structs, complex layouts | -| `zerocopy` | ✅ Safe | Zero-copy | Large buffers, many reads, compile-time checks | -| `bytemuck` | ✅ Safe | Zero-copy | Simple `Pod` types, casting slices | -| `unsafe { transmute() }` | ❌ Unsafe | Zero-copy | Last resort — avoid in application code | +| Подход | Безопасность | Накладные расходы | Когда использовать | +|--------|:------------:|-------------------|--------------------| +| Ручной разбор поле за полем | ✅ Безопасно | Копирование полей | Небольшие структуры, сложные раскладки | +| `zerocopy` | ✅ Безопасно | Без копирования | Большие буферы, много чтений, проверки на этапе компиляции | +| `bytemuck` | ✅ Безопасно | Без копирования | Простые типы `Pod`, приведение срезов | +| `unsafe { transmute() }` | ❌ Небезопасно | Без копирования | Крайняя мера: избегайте в прикладном коде | -### bytes::Bytes — Reference-Counted Buffers +### bytes::Bytes: буферы с подсчётом ссылок -The `bytes` crate (used by tokio, hyper, tonic) provides zero-copy byte buffers -with reference counting — `Bytes` is to `Vec` what `Arc<[u8]>` is to owned slices: +Крейт `bytes` (его используют tokio, hyper и tonic) предоставляет байтовые буферы без копирования с подсчётом ссылок. `Bytes` относится к `Vec` так же, как `Arc<[u8]>` относится к владеющим срезам: ```rust use bytes::{Bytes, BytesMut, Buf, BufMut}; fn main() { - // --- BytesMut: mutable buffer for building data --- + // --- BytesMut: изменяемый буфер для построения данных --- let mut buf = BytesMut::with_capacity(1024); - buf.put_u8(0x01); // Write a byte - buf.put_u16(0x1234); // Write u16 (big-endian) - buf.put_slice(b"hello"); // Write raw bytes - buf.put(&b"world"[..]); // Write from slice + buf.put_u8(0x01); // Записать байт + buf.put_u16(0x1234); // Записать u16 (big-endian) + buf.put_slice(b"hello"); // Записать сырые байты + buf.put(&b"world"[..]); // Записать из среза - // Freeze into immutable Bytes (zero cost): + // Заморозить в неизменяемые Bytes (бесплатно): let data: Bytes = buf.freeze(); - // --- Bytes: immutable, reference-counted, cloneable --- - let data2 = data.clone(); // Cheap: increments refcount, NOT deep copy - let slice = data.slice(3..8); // Zero-copy sub-slice (shares buffer) + // --- Bytes: неизменяемый, с подсчётом ссылок, клонируемый --- + let data2 = data.clone(); // Дёшево: увеличивает счётчик ссылок, НЕ глубокое копирование + let slice = data.slice(3..8); // Подсрез без копирования (разделяет буфер) - // Read from Bytes using the Buf trait: + // Чтение из Bytes через трейт Buf: let mut reader = &data[..]; let byte = reader.get_u8(); // 0x01 let short = reader.get_u16(); // 0x1234 - // Split without copying: + // Разделение без копирования: let mut original = Bytes::from_static(b"HEADER\x00PAYLOAD"); let header = original.split_to(6); // header = "HEADER", original = "\x00PAYLOAD" - println!("header: {:?}", &header[..]); - println!("payload: {:?}", &original[1..]); + println!("заголовок: {:?}", &header[..]); + println!("полезная нагрузка: {:?}", &original[1..]); } ``` -**`bytes` vs `Vec`**: +**`bytes` против `Vec`**: -| Feature | `Vec` | `Bytes` | -|---------|-----------|---------| -| Clone cost | O(n) deep copy | O(1) refcount increment | -| Sub-slicing | Borrows with lifetime | Owned, refcount-tracked | -| Thread safety | Not `Sync` (needs `Arc`) | `Send + Sync` built in | -| Mutability | Direct `&mut` | Split into `BytesMut` first | -| Ecosystem | Standard library | tokio, hyper, tonic, axum | +| Свойство | `Vec` | `Bytes` | +|----------|-----------|---------| +| Стоимость клонирования | O(n): глубокое копирование | O(1): увеличение счётчика ссылок | +| Подсрезы | Заимствуются с временем жизни | Владеющие, с отслеживанием счётчика ссылок | +| Потокобезопасность | Не `Sync` (нужен `Arc`) | `Send + Sync` встроены | +| Изменяемость | Прямой `&mut` | Сначала разделить на `BytesMut` | +| Экосистема | Стандартная библиотека | tokio, hyper, tonic, axum | -> **When to use bytes**: Network protocols, packet parsing, any scenario where -> you receive a buffer and need to split it into parts that are processed by -> different components or threads. The zero-copy splitting is the killer feature. +> **Когда использовать bytes**: сетевые протоколы, разбор пакетов и любой сценарий, в котором вы получаете буфер и должны разрезать его на части, которые обрабатываются разными компонентами или потоками. Разделение без копирования главное преимущество. -> **Key Takeaways — Serialization & Binary Data** -> - serde's derive macros handle 90% of cases; use attributes (`rename`, `skip`, `default`) for the rest -> - Zero-copy deserialization (`&'a str` in structs) avoids allocation for read-heavy workloads -> - `repr(C)` + `zerocopy`/`bytemuck` for hardware register layouts; `bytes::Bytes` for reference-counted buffers +> **Ключевые выводы: сериализация и бинарные данные** +> - Derive-макросы serde покрывают 90% случаев; для остальных используйте атрибуты (`rename`, `skip`, `default`) +> - Десериализация без копирования (`&'a str` в структурах) исключает выделения памяти для сценариев с интенсивным чтением +> - `repr(C)` с `zerocopy` или `bytemuck` для раскладок регистров оборудования; `bytes::Bytes` для буферов с подсчётом ссылок -> **See also:** [Ch 9 — Error Handling](ch09-error-handling-patterns.md) for combining serde errors with `thiserror`. [Ch 11 — Unsafe](ch11-unsafe-rust-controlled-danger.md) for `repr(C)` and FFI data layouts. +> **См. также:** [гл. 10 — Обработка ошибок](ch10-error-handling-patterns.md) о совмещении ошибок serde с `thiserror`. [гл. 12 — Unsafe](ch12-unsafe-rust-controlled-danger.md) о `repr(C)` и раскладках данных для FFI. ```mermaid flowchart LR - subgraph Input + subgraph Input["Вход"] JSON["JSON"] TOML["TOML"] Bin["bincode"] MsgP["MessagePack"] end - subgraph serde["serde data model"] + subgraph serde["Модель данных serde"] Ser["Serialize"] De["Deserialize"] end - subgraph Output - Struct["Rust struct"] - Enum["Rust enum"] + subgraph Output["Выход"] + Struct["Структура Rust"] + Enum["Перечисление Rust"] end JSON --> De @@ -517,12 +506,12 @@ flowchart LR --- -### Exercise: Custom serde Deserialization ★★★ (~45 min) +### Упражнение: собственная десериализация serde ★★★ (~45 минут) -Design a `HumanDuration` wrapper that deserializes from human-readable strings like `"30s"`, `"5m"`, `"2h"` using a custom serde deserializer. It should also serialize back to the same format. +Спроектируйте обёртку `HumanDuration`, которая десериализуется из читаемых человеком строк вроде `"30s"`, `"5m"`, `"2h"` с помощью собственного десериализатора serde. Она также должна сериализоваться обратно в тот же формат.
-🔑 Solution +🔑 Решение ```rust,ignore use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -534,20 +523,20 @@ struct HumanDuration(std::time::Duration); impl HumanDuration { fn from_str(s: &str) -> Result { let s = s.trim(); - if s.is_empty() { return Err("empty duration string".into()); } + if s.is_empty() { return Err("пустая строка длительности".into()); } let (num_str, suffix) = s.split_at( s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()) ); let value: u64 = num_str.parse() - .map_err(|_| format!("invalid number: {num_str}"))?; + .map_err(|_| format!("некорректное число: {num_str}"))?; let duration = match suffix { "s" | "sec" => std::time::Duration::from_secs(value), "m" | "min" => std::time::Duration::from_secs(value * 60), "h" | "hr" => std::time::Duration::from_secs(value * 3600), "ms" => std::time::Duration::from_millis(value), - other => return Err(format!("unknown suffix: {other}")), + other => return Err(format!("неизвестный суффикс: {other}")), }; Ok(HumanDuration(duration)) } @@ -596,11 +585,10 @@ fn main() { let serialized = serde_json::to_string(&config).unwrap(); assert!(serialized.contains("30s")); - println!("Config: {serialized}"); + println!("Конфигурация: {serialized}"); } ```
*** - diff --git a/rust-patterns-book/src/ch12-unsafe-rust-controlled-danger.md b/rust-patterns-book/src/ch12-unsafe-rust-controlled-danger.md index c5d686e..07f9af8 100644 --- a/rust-patterns-book/src/ch12-unsafe-rust-controlled-danger.md +++ b/rust-patterns-book/src/ch12-unsafe-rust-controlled-danger.md @@ -1,49 +1,48 @@ -# 11. Unsafe Rust — Controlled Danger 🔴 +# 12. Unsafe Rust: контролируемая опасность 🔴 -> **What you'll learn:** -> - The five unsafe superpowers and when each is needed -> - Writing sound abstractions: safe API, unsafe internals -> - FFI patterns for calling C from Rust (and back) -> - Common UB pitfalls and arena/slab allocator patterns +> **Что вы узнаете:** +> - Пять сверхспособностей unsafe и когда нужна каждая из них +> - Как писать корректные абстракции: безопасный API поверх небезопасного кода +> - Паттерны FFI для вызова C из Rust (и наоборот) +> - Распространённые ловушки UB и паттерны арена- и slab-аллокаторов -## The Five Unsafe Superpowers +## Пять сверхспособностей unsafe -`unsafe` unlocks five operations that the compiler can't verify: +`unsafe` открывает пять операций, которые компилятор не может проверить: ```rust unsafe { - // 1. Dereference a raw pointer + // 1. Разыменование сырого указателя let ptr: *const i32 = &42; - let value = *ptr; // Could be a dangling/null pointer + let value = *ptr; // Может быть висячим или нулевым указателем - // 2. Call an unsafe function + // 2. Вызов небезопасной функции let layout = std::alloc::Layout::new::(); let mem = std::alloc::alloc(layout); - // 3. Access a mutable static variable + // 3. Доступ к изменяемой статической переменной static mut COUNTER: u32 = 0; - COUNTER += 1; // Data race if multiple threads access + COUNTER += 1; // Гонка данных, если к ней обращаются несколько потоков - // 4. Implement an unsafe trait + // 4. Реализация небезопасного трейта // unsafe impl Send for MyType {} - // 5. Access fields of a union + // 5. Доступ к полям union // union IntOrFloat { i: i32, f: f32 } // let u = IntOrFloat { i: 42 }; - // let f = u.f; // Reinterpret bits — could be garbage + // let f = u.f; // Переинтерпретация битов: результат может быть мусором } ``` -> **Key principle**: `unsafe` doesn't turn off the borrow checker or type system. -> It only unlocks these five specific capabilities. All other Rust rules still apply. +> **Ключевой принцип**: `unsafe` не отключает проверку заимствований и систему типов. Он открывает только эти пять конкретных возможностей. Все остальные правила Rust по-прежнему действуют. -### Writing Sound Abstractions +### Написание корректных абстракций -The purpose of `unsafe` is to build **safe abstractions** around unsafe operations: +Назначение `unsafe` в том, чтобы построить **безопасные абстракции** вокруг небезопасных операций: ```rust -/// A fixed-capacity stack-allocated buffer. -/// All public methods are safe — the unsafe is encapsulated. +/// Буфер фиксированной ёмкости, размещённый на стеке. +/// Все публичные методы безопасны: unsafe инкапсулирован. pub struct StackBuf { data: [std::mem::MaybeUninit; N], len: usize, @@ -52,9 +51,9 @@ pub struct StackBuf { impl StackBuf { pub fn new() -> Self { StackBuf { - // Each element is individually MaybeUninit — no unsafe needed. - // `const { ... }` blocks (Rust 1.79+) let us repeat a non-Copy - // const expression N times. + // Каждый элемент — отдельный MaybeUninit: unsafe не нужен. + // Блоки `const { ... }` (Rust 1.79+) позволяют повторить + // константное выражение, не реализующее Copy, N раз. data: [const { std::mem::MaybeUninit::uninit() }; N], len: 0, } @@ -62,10 +61,10 @@ impl StackBuf { pub fn push(&mut self, value: T) -> Result<(), T> { if self.len >= N { - return Err(value); // Buffer full — return value to caller + return Err(value); // Буфер заполнен: возвращаем значение вызывающей стороне } - // SAFETY: len < N, so data[len] is within bounds. - // We write a valid T into the MaybeUninit slot. + // SAFETY: len < N, поэтому data[len] находится в пределах. + // Записываем корректный T в ячейку MaybeUninit. self.data[self.len] = std::mem::MaybeUninit::new(value); self.len += 1; Ok(()) @@ -73,7 +72,7 @@ impl StackBuf { pub fn get(&self, index: usize) -> Option<&T> { if index < self.len { - // SAFETY: index < len, and data[0..len] are all initialized. + // SAFETY: index < len, и data[0..len] все инициализированы. Some(unsafe { self.data[index].assume_init_ref() }) } else { None @@ -83,7 +82,7 @@ impl StackBuf { impl Drop for StackBuf { fn drop(&mut self) { - // SAFETY: data[0..len] are initialized — drop them properly. + // SAFETY: data[0..len] инициализированы: уничтожаем их правильно. for i in 0..self.len { unsafe { self.data[i].assume_init_drop(); } } @@ -91,84 +90,78 @@ impl Drop for StackBuf { } ``` -**The three rules of sound unsafe code**: -1. **Document invariants** — every `// SAFETY:` comment explains why the operation is valid -2. **Encapsulate** — the unsafe is inside a safe API; users can't trigger UB -3. **Minimize** — only the smallest possible block is `unsafe` +**Три правила корректного unsafe-кода**: +1. **Документируйте инварианты**: каждый комментарий `// SAFETY:` объясняет, почему операция допустима +2. **Инкапсулируйте**: unsafe находится внутри безопасного API, и пользователи не могут вызвать UB +3. **Минимизируйте**: unsafe-блок должен быть как можно меньше -### FFI Patterns: Calling C from Rust +### Паттерны FFI: вызов C из Rust ```rust -// Declare the C function signature: +// Объявляем сигнатуру функции C: extern "C" { fn strlen(s: *const std::ffi::c_char) -> usize; fn printf(format: *const std::ffi::c_char, ...) -> std::ffi::c_int; } -// Safe wrapper: +// Безопасная обёртка: fn safe_strlen(s: &str) -> usize { - let c_string = std::ffi::CString::new(s).expect("string contains null byte"); - // SAFETY: c_string is a valid null-terminated string, alive for the call. + let c_string = std::ffi::CString::new(s).expect("строка содержит нулевой байт"); + // SAFETY: c_string — корректная строка с нулевым завершителем, живёт на время вызова. unsafe { strlen(c_string.as_ptr()) } } -// Calling Rust from C (export a function): +// Вызов Rust из C (экспорт функции): #[no_mangle] pub extern "C" fn rust_add(a: i32, b: i32) -> i32 { a + b } ``` -**Common FFI types**: +**Распространённые типы FFI**: -| Rust | C | Notes | -|------|---|-------| -| `i32` / `u32` | `int32_t` / `uint32_t` | Fixed-width, safe | -| `*const T` / `*mut T` | `const T*` / `T*` | Raw pointers | -| `std::ffi::CStr` | `const char*` (borrowed) | Null-terminated, borrowed | -| `std::ffi::CString` | `char*` (owned) | Null-terminated, owned | -| `std::ffi::c_void` | `void` | Opaque pointer target | -| `Option` | Nullable function pointer | `None` = NULL | +| Rust | C | Примечания | +|------|---|------------| +| `i32` / `u32` | `int32_t` / `uint32_t` | Фиксированная ширина, безопасно | +| `*const T` / `*mut T` | `const T*` / `T*` | Сырые указатели | +| `std::ffi::CStr` | `const char*` (заимствованная) | С нулевым завершителем, заимствованная | +| `std::ffi::CString` | `char*` (владеющая) | С нулевым завершителем, владеющая | +| `std::ffi::c_void` | `void` | Непрозрачная цель указателя | +| `Option` | Указатель на функцию, который может быть NULL | `None` = NULL | -### Common UB Pitfalls +### Распространённые ловушки UB -| Pitfall | Example | Why It's UB | -|---------|---------|------------| -| Null dereference | `*std::ptr::null::()` | Dereferencing null is always UB | -| Dangling pointer | Dereference after `drop()` | Memory may be reused | -| Data race | Two threads write to `static mut` | Unsynchronized concurrent writes | -| Wrong `assume_init` | `MaybeUninit::::uninit().assume_init()` | Reading uninitialized memory. **Note**: `[const { MaybeUninit::uninit() }; N]` (Rust 1.79+) is the safe way to create an array of `MaybeUninit` — no `unsafe` or `assume_init` needed (see `StackBuf::new()` above). | -| Aliasing violation | Creating two `&mut` to same data | Violates Rust's aliasing model | -| Invalid enum value | `std::mem::transmute::(2)` | `bool` can only be 0 or 1 | +| Ловушка | Пример | Почему это UB | +|---------|--------|---------------| +| Разыменование null | `*std::ptr::null::()` | Разыменование null всегда UB | +| Висячий указатель | Разыменование после `drop()` | Память может быть переиспользована | +| Гонка данных | Два потока пишут в `static mut` | Несинхронизированная параллельная запись | +| Неверный `assume_init` | `MaybeUninit::::uninit().assume_init()` | Чтение неинициализированной памяти. **Примечание**: `[const { MaybeUninit::uninit() }; N]` (Rust 1.79+) это безопасный способ создать массив `MaybeUninit`: не нужны ни `unsafe`, ни `assume_init` (см. `StackBuf::new()` выше). | +| Нарушение псевдонимов | Создание двух `&mut` на одни и те же данные | Нарушает модель псевдонимов Rust | +| Недопустимое значение перечисления | `std::mem::transmute::(2)` | `bool` может быть только 0 или 1 | -> **When to use `unsafe` in production**: -> - FFI boundaries (calling C/C++ code) -> - Performance-critical inner loops (avoid bounds checks) -> - Building primitives (`Vec`, `HashMap` — these use unsafe internally) -> - Never in application logic if you can avoid it +> **Когда использовать `unsafe` в продакшене**: +> - Границы FFI (вызов кода на C/C++) +> - Критичные к производительности внутренние циклы (избегаем проверок границ) +> - Создание примитивов (`Vec`, `HashMap`: они используют unsafe внутри) +> - Никогда не используйте в прикладной логике, если можно обойтись -### Custom Allocators — Arena and Slab Patterns +### Пользовательские аллокаторы: паттерны арен и slab -In C, you'd write custom `malloc()` replacements for specific allocation patterns — -arena allocators that free everything at once, slab allocators for fixed-size objects, -or pool allocators for high-throughput systems. Rust provides the same power through -the `GlobalAlloc` trait and allocator crates, with the added benefit of lifetime-scoped -arenas that **prevent use-after-free at compile time**. +В C для особых паттернов выделения памяти пишут собственные замены `malloc()`: арена-аллокаторы, которые освобождают всё разом, slab-аллокаторы для объектов фиксированного размера или пул-аллокаторы для систем с высокой пропускной способностью. Rust даёт ту же мощь через трейт `GlobalAlloc` и крейты-аллокаторы, плюс дополнительное преимущество: арены с ограничением по времени жизни **предотвращают use-after-free на этапе компиляции**. -#### Arena Allocators — Bulk Allocation, Bulk Free +#### Арена-аллокаторы: массовое выделение, массовое освобождение -An arena allocates by bumping a pointer forward. Individual items can't be freed — -the entire arena is freed at once. This is perfect for request-scoped or -frame-scoped allocations: +Арена выделяет память, продвигая указатель вперёд. Отдельные элементы освободить нельзя: вся арена освобождается разом. Это идеально подходит для выделений, привязанных к запросу или кадру: ```rust use bumpalo::Bump; fn process_sensor_frame(raw_data: &[u8]) { - // Create an arena for this frame's allocations + // Создаём арену для выделений этого кадра let arena = Bump::new(); - // Allocate objects in the arena — ~2ns each (just a pointer bump) + // Выделяем объекты в арене: ~2 нс каждое (просто сдвиг указателя) let header = arena.alloc(parse_header(raw_data)); let readings: &mut [f32] = arena.alloc_slice_fill_default(header.sensor_count); @@ -178,31 +171,30 @@ fn process_sensor_frame(raw_data: &[u8]) { } } - // Use readings... + // Используем readings... let avg = readings.iter().sum::() / readings.len() as f32; - println!("Frame avg: {avg:.2}"); + println!("Средняя по кадру: {avg:.2}"); - // `arena` drops here — ALL allocations freed at once in O(1) - // No per-object destructor overhead, no fragmentation + // Арена уничтожается здесь: ВСЕ выделения освобождаются разом за O(1) + // Никаких деструкторов для каждого объекта, никакой фрагментации } # fn parse_header(_: &[u8]) -> Header { Header { sensor_count: 4, payload_offset: 8 } } # struct Header { sensor_count: usize, payload_offset: usize } ``` -**Arena vs standard allocator**: +**Арена против стандартного аллокатора**: -| Aspect | `Vec::new()` / `Box::new()` | `Bump` arena | -|--------|---------------------------|--------------| -| Alloc speed | ~25ns (malloc) | ~2ns (pointer bump) | -| Free speed | Per-object destructor | O(1) bulk free | -| Fragmentation | Yes (long-lived processes) | None within arena | -| Lifetime safety | Heap — freed on `Drop` | Arena reference — compile-time scoped | -| Use case | General purpose | Request/frame/batch processing | +| Аспект | `Vec::new()` / `Box::new()` | Арена `Bump` | +|--------|-----------------------------|--------------| +| Скорость выделения | ~25 нс (malloc) | ~2 нс (сдвиг указателя) | +| Скорость освобождения | Деструктор каждого объекта | Массовое освобождение за O(1) | +| Фрагментация | Есть (долгоживущие процессы) | Нет внутри арены | +| Безопасность времён жизни | Куча: освобождается при `Drop` | Ссылка на арену: проверяется на этапе компиляции | +| Сценарий | Универсальное назначение | Обработка запросов, кадров, пакетов | -#### `typed-arena` — Type-Safe Arena +#### `typed-arena`: арена с контролем типов -When all arena objects are the same type, `typed-arena` provides a simpler API -that returns references with the arena's lifetime: +Когда все объекты арены одного типа, `typed-arena` предоставляет более простой API, который возвращает ссылки с временем жизни арены: ```rust use typed_arena::Arena; @@ -215,25 +207,23 @@ struct AstNode<'a> { fn build_tree() { let arena: Arena> = Arena::new(); - // Allocate nodes — returns &AstNode tied to arena's lifetime + // Выделяем узлы: возвращается &AstNode, привязанный ко времени жизни арены let root = arena.alloc(AstNode { value: 1, children: vec![] }); let left = arena.alloc(AstNode { value: 2, children: vec![] }); let right = arena.alloc(AstNode { value: 3, children: vec![] }); - // Build the tree — all references valid as long as `arena` lives - // (Mutable access requires interior mutability for truly mutable trees) + // Строим дерево: все ссылки действительны, пока жива `arena` + // (для по-настоящему изменяемых деревьев нужна внутренняя изменяемость) - println!("Root: {}, Left: {}, Right: {}", root.value, left.value, right.value); + println!("Корень: {}, левый: {}, правый: {}", root.value, left.value, right.value); - // `arena` drops here — all nodes freed at once + // `arena` уничтожается здесь: все узлы освобождаются разом } ``` -#### Slab Allocators — Fixed-Size Object Pools +#### Slab-аллокаторы: пулы объектов фиксированного размера -A slab allocator pre-allocates a pool of fixed-size slots. Objects are allocated -and returned individually, but all slots are the same size — eliminating -fragmentation and enabling O(1) alloc/free: +Slab-аллокатор заранее выделяет пул слотов фиксированного размера. Объекты выделяются и возвращаются по отдельности, но все слоты одного размера. Это устраняет фрагментацию и обеспечивает выделение и освобождение за O(1): ```rust use slab::Slab; @@ -245,10 +235,10 @@ struct Connection { } fn connection_pool_example() { - // Pre-allocate a slab for connections + // Заранее выделяем slab для соединений let mut connections: Slab = Slab::with_capacity(256); - // Insert returns a key (usize index) — O(1) + // insert возвращает ключ (индекс usize): O(1) let key1 = connections.insert(Connection { id: 1001, buffer: [0; 1024], @@ -261,29 +251,28 @@ fn connection_pool_example() { active: true, }); - // Access by key — O(1) + // Доступ по ключу: O(1) if let Some(conn) = connections.get_mut(key1) { conn.buffer[0..5].copy_from_slice(b"hello"); } - // Remove returns the value — O(1), slot is reused for next insert + // remove возвращает значение: O(1), слот переиспользуется при следующей вставке let removed = connections.remove(key2); assert_eq!(removed.id, 1002); - // Next insert reuses the freed slot — no fragmentation + // Следующая вставка переиспользует освобождённый слот: без фрагментации let key3 = connections.insert(Connection { id: 1003, buffer: [0; 1024], active: true, }); - assert_eq!(key3, key2); // Same slot reused! + assert_eq!(key3, key2); // Тот же слот переиспользован! } ``` -#### Implementing a Minimal Arena (for `no_std`) +#### Минимальная арена (для `no_std`) -For bare-metal environments where you can't pull in `bumpalo`, here's a -minimal arena built on `unsafe`: +Для bare-metal окружений, где нельзя подключить `bumpalo`, вот минимальная арена на `unsafe`: ```rust #![cfg_attr(not(test), no_std)] @@ -291,19 +280,20 @@ minimal arena built on `unsafe`: use core::alloc::Layout; use core::cell::{Cell, UnsafeCell}; -/// A simple bump allocator backed by a fixed-size byte array. -/// Not thread-safe — use per-core or with a lock for multi-threaded contexts. +/// Простой bump-аллокатор на основе массива байтов фиксированного размера. +/// Не потокобезопасен: используйте отдельную арену на ядро или защищайте её блокировкой +/// для многопоточного кода. /// -/// **Important**: Like `bumpalo`, this arena does NOT call destructors on -/// allocated items when the arena is dropped. Types with `Drop` impls will -/// leak their resources (file handles, sockets, etc.). Only allocate types -/// without meaningful `Drop` impls, or manually drop them before the arena. +/// **Важно**: как и `bumpalo`, эта арена НЕ вызывает деструкторы для выделенных +/// объектов при уничтожении арены. Типы с реализацией `Drop` будут утекать +/// свои ресурсы (дескрипторы файлов, сокеты и т. п.). Выделяйте только типы +/// без существенной реализации `Drop` или уничтожайте их вручную до арены. pub struct FixedArena { - // UnsafeCell is REQUIRED here: we mutate `buf` through `&self`. - // Without UnsafeCell, casting &self.buf to *mut u8 would be UB - // (violates Rust's aliasing model — shared ref implies immutable). + // UnsafeCell обязателен здесь: мы изменяем `buf` через `&self`. + // Без UnsafeCell приведение &self.buf к *mut u8 было бы UB + // (нарушает модель псевдонимов Rust: разделяемая ссылка подразумевает неизменяемость). buf: UnsafeCell<[u8; N]>, - offset: Cell, // Interior mutability for &self allocation + offset: Cell, // Внутренняя изменяемость для выделения через &self } impl FixedArena { @@ -314,27 +304,27 @@ impl FixedArena { } } - /// Allocate a `T` in the arena. Returns `None` if out of space. + /// Выделяет `T` в арене. Возвращает `None`, если места нет. pub fn alloc(&self, value: T) -> Option<&mut T> { let layout = Layout::new::(); let current = self.offset.get(); - // Align up + // Выравнивание вверх let aligned = (current + layout.align() - 1) & !(layout.align() - 1); let new_offset = aligned + layout.size(); if new_offset > N { - return None; // Arena full + return None; // Арена заполнена } self.offset.set(new_offset); // SAFETY: - // - `aligned` is within `buf` bounds (checked above) - // - Alignment is correct (aligned to T's requirement) - // - No aliasing: each alloc returns a unique, non-overlapping region - // - UnsafeCell grants permission to mutate through &self - // - The arena outlives the returned reference (caller must ensure) + // - `aligned` находится в пределах `buf` (проверено выше) + // - Выравнивание корректно (по требованию T) + // - Без псевдонимов: каждое выделение возвращает уникальную непересекающуюся область + // - UnsafeCell даёт право изменять через &self + // - Арена переживёт возвращённую ссылку (это должен обеспечить вызывающий код) let ptr = unsafe { let base = (self.buf.get() as *mut u8).add(aligned); let typed = base as *mut T; @@ -345,8 +335,8 @@ impl FixedArena { Some(ptr) } - /// Reset the arena — invalidates all previous allocations. - /// Caller must ensure no references to arena-allocated data exist. + /// Сбрасывает арену: делает недействительными все предыдущие выделения. + /// Вызывающий код должен гарантировать, что ссылок на данные арены не осталось. pub unsafe fn reset(&self) { self.offset.set(0); } @@ -361,26 +351,24 @@ impl FixedArena { } ``` -#### Choosing an Allocator Strategy +#### Выбор стратегии аллокатора -> **Note**: The diagram below uses Mermaid syntax. It renders on GitHub and in -> tools that support Mermaid (mdBook with `mermaid` plugin, VS Code with -> Mermaid extension). In plain Markdown viewers, you'll see the raw source. +> **Примечание**: диаграмма ниже использует синтаксис Mermaid. Она отображается на GitHub и в инструментах с поддержкой Mermaid (mdBook с плагином `mermaid`, VS Code с расширением Mermaid). В простых просмотрщиках Markdown вы увидите исходный код. ```mermaid graph TD - A["What's your allocation pattern?"] --> B{All same type?} - A --> I{"Environment?"} - B -->|Yes| C{Need individual free?} - B -->|No| D{Need individual free?} - C -->|Yes| E["Slab
slab crate
O(1) alloc + free
Index-based access"] - C -->|No| F["typed-arena
Bulk alloc, bulk free
Lifetime-scoped refs"] - D -->|Yes| G["Standard allocator
Box, Vec, etc.
General-purpose malloc"] - D -->|No| H["Bump arena
bumpalo crate
~2ns alloc, O(1) bulk free"] - - I -->|no_std| J["FixedArena (custom)
or embedded-alloc"] + A["Какой у вас паттерн выделения памяти?"] --> B{Все одного типа?} + A --> I{"Окружение?"} + B -->|Да| C{Нужно освобождать по отдельности?} + B -->|Нет| D{Нужно освобождать по отдельности?} + C -->|Да| E["Slab
крейт slab
Выделение и освобождение за O(1)
Доступ по индексу"] + C -->|Нет| F["typed-arena
Массовое выделение и освобождение
Ссылки с ограниченным временем жизни"] + D -->|Да| G["Стандартный аллокатор
Box, Vec и др.
Универсальный malloc"] + D -->|Нет| H["Bump-арена
крейт bumpalo
~2 нс на выделение, массовое освобождение за O(1)"] + + I -->|no_std| J["FixedArena (своя)
или embedded-alloc"] I -->|std| K["bumpalo / typed-arena / slab"] - + style E fill:#91e5a3,color:#000 style F fill:#91e5a3,color:#000 style G fill:#89CFF0,color:#000 @@ -389,40 +377,37 @@ graph TD style K fill:#91e5a3,color:#000 ``` -| C Pattern | Rust Equivalent | Key Advantage | -|-----------|----------------|---------------| -| Custom `malloc()` pool | `#[global_allocator]` impl | Type-safe, debuggable | -| `obstack` (GNU) | `bumpalo::Bump` | Lifetime-scoped, no use-after-free | -| Kernel slab (`kmem_cache`) | `slab::Slab` | Type-safe, index-based | -| Stack-allocated temp buffer | `FixedArena` (above) | No heap, `const` constructible | -| `alloca()` | `[T; N]` or `SmallVec` | Compile-time sized, no UB | +| Паттерн C | Аналог в Rust | Ключевое преимущество | +|-----------|---------------|------------------------| +| Пул поверх `malloc()` | Реализация `#[global_allocator]` | Типобезопасно, удобно для отладки | +| `obstack` (GNU) | `bumpalo::Bump` | Ограничено временем жизни, без use-after-free | +| Slab ядра (`kmem_cache`) | `slab::Slab` | Типобезопасно, доступ по индексу | +| Временный буфер на стеке | `FixedArena` (выше) | Без кучи, создаётся в `const`-контексте | +| `alloca()` | `[T; N]` или `SmallVec` | Размер известен на этапе компиляции, без UB | -> **Cross-reference**: For bare-metal allocator setup (`#[global_allocator]` with -> `embedded-alloc`), see the *Rust Training for C Programmers*, Chapter 15.1 -> "Global Allocator Setup" which covers the embedded-specific bootstrapping. +> **Перекрёстная ссылка**: настройка аллокатора для bare-metal (`#[global_allocator]` с `embedded-alloc`) описана в книге *Rust Training for C Programmers*, глава 15.1 «Global Allocator Setup», где рассмотрена начальная загрузка, специфичная для встраиваемых систем. -> **Key Takeaways — Unsafe Rust** -> - Document invariants (`SAFETY:` comments), encapsulate behind safe APIs, minimize unsafe scope -> - `[const { MaybeUninit::uninit() }; N]` (Rust 1.79+) replaces the old `assume_init` anti-pattern -> - FFI requires `extern "C"`, `#[repr(C)]`, and careful null/lifetime handling -> - Arena and slab allocators trade general-purpose flexibility for allocation speed +> **Ключевые выводы: unsafe Rust** +> - Документируйте инварианты (комментарии `SAFETY:`), инкапсулируйте их за безопасными API и минимизируйте область unsafe +> - `[const { MaybeUninit::uninit() }; N]` (Rust 1.79+) заменяет устаревший антипаттерн с `assume_init` +> - FFI требует `extern "C"`, `#[repr(C)]` и аккуратной обработки null и времён жизни +> - Арена- и slab-аллокаторы обменивают универсальность на скорость выделения -> **See also:** [Ch 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) for variance and drop-check interactions with unsafe code. [Ch 8 — Smart Pointers](ch08-smart-pointers-and-interior-mutability.md) for Pin and self-referential types. +> **См. также:** [гл. 4 — PhantomData](ch04-phantomdata-types-that-carry-no-data.md) о взаимодействии вариантности и проверки drop с unsafe-кодом. [гл. 9 — Умные указатели](ch09-smart-pointers-and-interior-mutability.md) о Pin и самоссылающихся типах. --- -### Exercise: Safe Wrapper around Unsafe ★★★ (~45 min) +### Упражнение: безопасная обёртка над unsafe ★★★ (~45 минут) -Write a `FixedVec` — a fixed-capacity, stack-allocated vector. -Requirements: -- `push(&mut self, value: T) -> Result<(), T>` returns `Err(value)` when full -- `pop(&mut self) -> Option` returns and removes the last element -- `as_slice(&self) -> &[T]` borrows initialized elements -- All public methods must be safe; all unsafe must be encapsulated with `SAFETY:` comments -- `Drop` must clean up initialized elements +Напишите `FixedVec`: вектор фиксированной ёмкости, размещённый на стеке. Требования: +- `push(&mut self, value: T) -> Result<(), T>` возвращает `Err(value)`, когда вектор заполнен +- `pop(&mut self) -> Option` возвращает и удаляет последний элемент +- `as_slice(&self) -> &[T]` заимствует инициализированные элементы +- Все публичные методы должны быть безопасными; весь unsafe инкапсулирован с комментариями `SAFETY:` +- `Drop` должен очищать инициализированные элементы
-🔑 Solution +🔑 Решение ```rust use std::mem::MaybeUninit; @@ -442,7 +427,7 @@ impl FixedVec { pub fn push(&mut self, value: T) -> Result<(), T> { if self.len >= N { return Err(value); } - // SAFETY: len < N, so data[len] is within bounds. + // SAFETY: len < N, поэтому data[len] находится в пределах. self.data[self.len] = MaybeUninit::new(value); self.len += 1; Ok(()) @@ -451,13 +436,13 @@ impl FixedVec { pub fn pop(&mut self) -> Option { if self.len == 0 { return None; } self.len -= 1; - // SAFETY: data[len] was initialized (len was > 0 before decrement). + // SAFETY: data[len] был инициализирован (len был > 0 до уменьшения). Some(unsafe { self.data[self.len].assume_init_read() }) } pub fn as_slice(&self) -> &[T] { - // SAFETY: data[0..len] are all initialized, and MaybeUninit - // has the same layout as T. + // SAFETY: data[0..len] инициализированы, и MaybeUninit + // имеет ту же раскладку, что и T. unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const T, self.len) } } @@ -467,7 +452,7 @@ impl FixedVec { impl Drop for FixedVec { fn drop(&mut self) { - // SAFETY: data[0..len] are initialized — drop each one. + // SAFETY: data[0..len] инициализированы: уничтожаем каждый. for i in 0..self.len { unsafe { self.data[i].assume_init_drop(); } } @@ -487,4 +472,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch13-macros-code-that-writes-code.md b/rust-patterns-book/src/ch13-macros-code-that-writes-code.md index ac69b63..8dc2ea5 100644 --- a/rust-patterns-book/src/ch13-macros-code-that-writes-code.md +++ b/rust-patterns-book/src/ch13-macros-code-that-writes-code.md @@ -1,19 +1,19 @@ -# 12. Macros — Code That Writes Code 🟡 +# 13. Макросы: код, который пишет код 🟡 -> **What you'll learn:** -> - Declarative macros (`macro_rules!`) with pattern matching and repetition -> - When macros are the right tool vs generics/traits -> - Procedural macros: derive, attribute, and function-like -> - Writing a custom derive macro with `syn` and `quote` +> **Что вы узнаете:** +> - Декларативные макросы (`macro_rules!`) с сопоставлением с образцом и повторениями +> - Когда макросы подходят лучше обобщений и трейтов +> - Процедурные макросы: derive-, атрибутные и функциональные +> - Написание собственного derive-макроса с `syn` и `quote` -## Declarative Macros (macro_rules!) +## Декларативные макросы (macro_rules!) -Macros match patterns on syntax and expand to code at compile time: +Макросы сопоставляют образцы с синтаксисом и раскрываются в код на этапе компиляции: ```rust -// A simple macro that creates a HashMap +// Простой макрос, который создаёт HashMap macro_rules! hashmap { - // Match: key => value pairs separated by commas + // Сопоставление: пары «ключ => значение», разделённые запятыми ( $( $key:expr => $value:expr ),* $(,)? ) => { { let mut map = std::collections::HashMap::new(); @@ -28,7 +28,7 @@ let scores = hashmap! { "Bob" => 87, "Carol" => 92, }; -// Expands to: +// Раскрывается в: // let mut map = HashMap::new(); // map.insert("Alice", 95); // map.insert("Bob", 87); @@ -36,22 +36,22 @@ let scores = hashmap! { // map ``` -**Macro fragment types**: +**Типы фрагментов в макросах**: -| Fragment | Matches | Example | -|----------|---------|---------| -| `$x:expr` | Any expression | `42`, `a + b`, `foo()` | -| `$x:ty` | A type | `i32`, `Vec` | -| `$x:ident` | An identifier | `my_var`, `Config` | -| `$x:pat` | A pattern | `Some(x)`, `_` | -| `$x:stmt` | A statement | `let x = 5;` | -| `$x:tt` | A single token tree | Anything (most flexible) | -| `$x:literal` | A literal value | `42`, `"hello"`, `true` | +| Фрагмент | Что сопоставляет | Пример | +|----------|------------------|--------| +| `$x:expr` | Любое выражение | `42`, `a + b`, `foo()` | +| `$x:ty` | Тип | `i32`, `Vec` | +| `$x:ident` | Идентификатор | `my_var`, `Config` | +| `$x:pat` | Образец | `Some(x)`, `_` | +| `$x:stmt` | Инструкцию | `let x = 5;` | +| `$x:tt` | Одно дерево токенов | Что угодно (самый гибкий) | +| `$x:literal` | Литерал | `42`, `"hello"`, `true` | -**Repetition**: `$( ... ),*` means "zero or more, comma-separated" +**Повторения**: `$( ... ),*` означает «ноль или больше, разделённых запятыми» ```rust -// Generate test functions automatically +// Автоматически генерируем тестовые функции macro_rules! test_cases { ( $( $name:ident: $input:expr => $expected:expr ),* $(,)? ) => { $( @@ -68,74 +68,74 @@ test_cases! { test_hello: "hello" => "HELLO", test_trim: " spaces " => "SPACES", } -// Generates three separate #[test] functions +// Генерирует три отдельные функции #[test] ``` -### When (Not) to Use Macros +### Когда (не) стоит использовать макросы -**Use macros when**: -- Reducing boilerplate that traits/generics can't handle (variadic arguments, DRY test generation) -- Creating DSLs (`html!`, `sql!`, `vec!`) -- Conditional code generation (`cfg!`, `compile_error!`) +**Используйте макросы, когда**: +- Нужно убрать шаблонный код, с которым не справляются трейты и обобщения (вариативные аргументы, генерация тестов без повторов, DRY) +- Создаёте предметно-ориентированный язык (DSL), например `html!`, `sql!`, `vec!` +- Нужна условная генерация кода (`cfg!`, `compile_error!`) -**Don't use macros when**: -- A function or generic would work (macros are harder to debug, autocomplete doesn't help) -- You need type checking inside the macro (macros operate on tokens, not types) -- The pattern is used once or twice (not worth the abstraction cost) +**Не используйте макросы, когда**: +- Подойдёт функция или обобщение (макросы труднее отлаживать, и автодополнение в них не помогает) +- Внутри макроса нужна проверка типов (макросы работают с токенами, а не с типами) +- Шаблон используется один-два раза (абстракция не стоит того) ```rust -// ❌ Unnecessary macro — a function works fine: +// ❌ Лишний макрос: функция справляется macro_rules! double { ($x:expr) => { $x * 2 }; } -// ✅ Just use a function: +// ✅ Просто используйте функцию: fn double(x: i32) -> i32 { x * 2 } -// ✅ Good macro use — variadic, can't be a function: +// ✅ Хорошее применение макроса: вариативные аргументы, не может быть функцией: macro_rules! println { - ($($arg:tt)*) => { /* format string + args */ }; + ($($arg:tt)*) => { /* строка формата + аргументы */ }; } ``` -### Procedural Macros Overview +### Обзор процедурных макросов -Procedural macros are Rust functions that transform token streams. They require a separate crate with `proc-macro = true`: +Процедурные макросы это функции на Rust, которые преобразуют потоки токенов. Они требуют отдельного крейта с `proc-macro = true`: ```rust -// Three types of proc macros: +// Три вида процедурных макросов: -// 1. Derive macros — #[derive(MyTrait)] -// Generate trait implementations from struct definitions +// 1. Derive-макросы — #[derive(MyTrait)] +// Генерируют реализации трейтов по определениям структур #[derive(Debug, Clone, Serialize, Deserialize)] struct Config { name: String, port: u16, } -// 2. Attribute macros — #[my_attribute] -// Transform the annotated item +// 2. Атрибутные макросы — #[my_attribute] +// Преобразуют помеченный элемент #[route(GET, "/api/users")] async fn list_users() -> Json> { /* ... */ } -// 3. Function-like macros — my_macro!(...) -// Custom syntax +// 3. Функциональные макросы — my_macro!(...) +// Пользовательский синтаксис let query = sql!(SELECT * FROM users WHERE id = ?); ``` -### Derive Macros in Practice +### Derive-макросы на практике -The most common proc macro type. Here's how `#[derive(Debug)]` works conceptually: +Это самый распространённый вид процедурных макросов. Вот как концептуально работает `#[derive(Debug)]`: ```rust -// Input (your struct): +// Вход (ваша структура): #[derive(Debug)] struct Point { x: f64, y: f64, } -// The derive macro generates: +// Derive-макрос генерирует: impl std::fmt::Debug for Point { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Point") @@ -146,47 +146,43 @@ impl std::fmt::Debug for Point { } ``` -**Commonly used derive macros**: +**Часто используемые derive-макросы**: -| Derive | Crate | What It Generates | -|--------|-------|-------------------| -| `Debug` | std | `fmt::Debug` impl (debug printing) | -| `Clone`, `Copy` | std | Value duplication | -| `PartialEq`, `Eq` | std | Equality comparison | -| `Hash` | std | Hashing for HashMap keys | -| `Serialize`, `Deserialize` | serde | JSON/YAML/etc. encoding | -| `Error` | thiserror | `std::error::Error` + `Display` | -| `Parser` | `clap` | CLI argument parsing | -| `Builder` | derive_builder | Builder pattern | +| Derive | Крейт | Что генерирует | +|--------|-------|----------------| +| `Debug` | std | Реализацию `fmt::Debug` (отладочный вывод) | +| `Clone`, `Copy` | std | Дублирование значений | +| `PartialEq`, `Eq` | std | Сравнение на равенство | +| `Hash` | std | Хеширование для ключей HashMap | +| `Serialize`, `Deserialize` | serde | Кодирование в JSON, YAML и др. | +| `Error` | thiserror | `std::error::Error` и `Display` | +| `Parser` | `clap` | Разбор аргументов командной строки | +| `Builder` | derive_builder | Паттерн builder | -> **Practical advice**: Use derive macros liberally — they eliminate error-prone -> boilerplate. Writing your own proc macros is an advanced topic; use existing -> ones (`serde`, `thiserror`, `clap`) before building custom ones. +> **Практический совет**: используйте derive-макросы без колебаний: они убирают подверженный ошибкам шаблонный код. Написание собственных процедурных макросов относится к продвинутым темам, поэтому сначала используйте готовые (`serde`, `thiserror`, `clap`). -### Macro Hygiene and `$crate` +### Гигиена макросов и `$crate` -**Hygiene** means that identifiers created inside a macro don't collide with -identifiers in the caller's scope. Rust's `macro_rules!` is *partially* hygienic: +**Гигиена** означает, что идентификаторы, созданные внутри макроса, не конфликтуют с идентификаторами в области видимости вызывающего кода. `macro_rules!` в Rust *частично* гигиеничен: ```rust macro_rules! make_var { () => { - let x = 42; // This 'x' is in the MACRO's scope + let x = 42; // Этот 'x' находится в области видимости МАКРОСА }; } fn main() { let x = 10; - make_var!(); // Creates a different 'x' (hygienic) - println!("{x}"); // Prints 10, not 42 — macro's x doesn't leak + make_var!(); // Создаёт другой 'x' (гигиенично) + println!("{x}"); // Выведет 10, а не 42: x из макроса не просачивается наружу } ``` -**`$crate`**: When writing macros in a library, use `$crate` to refer to -your own crate — it resolves correctly regardless of how users import your crate: +**`$crate`**: при написании макросов в библиотеке используйте `$crate`, чтобы ссылаться на собственный крейт. Он корректно разрешается независимо от того, как пользователи импортируют ваш крейт: ```rust -// In my_diagnostics crate: +// В крейте my_diagnostics: pub fn log_result(msg: &str) { println!("[diag] {msg}"); @@ -195,32 +191,30 @@ pub fn log_result(msg: &str) { #[macro_export] macro_rules! diag_log { ($($arg:tt)*) => { - // ✅ $crate always resolves to my_diagnostics, even if the user - // renamed the crate in their Cargo.toml + // ✅ $crate всегда ссылается на my_diagnostics, даже если пользователь + // переименовал крейт в своём Cargo.toml $crate::log_result(&format!($($arg)*)) }; } -// ❌ Without $crate: -// my_diagnostics::log_result(...) ← breaks if user writes: +// ❌ Без $crate: +// my_diagnostics::log_result(...) ← сломается, если пользователь напишет: // [dependencies] // diag = { package = "my_diagnostics", version = "1" } ``` -> **Rule**: Always use `$crate::` in `#[macro_export]` macros. Never use -> your crate's name directly. +> **Правило**: всегда используйте `$crate::` в макросах с `#[macro_export]`. Никогда не используйте имя своего крейта напрямую. -### Recursive Macros and `tt` Munching +### Рекурсивные макросы и `tt` munching -Recursive macros process input one token at a time — a technique called -**`tt` munching** (token-tree munching): +Рекурсивные макросы обрабатывают вход по одному токену за раз. Этот приём называется **`tt` munching** (token-tree munching, «поедание» деревьев токенов): ```rust -// Count the number of expressions passed to the macro +// Считаем, сколько выражений передано в макрос macro_rules! count { - // Base case: no tokens left + // Базовый случай: токенов не осталось () => { 0usize }; - // Recursive case: consume one expression, count the rest + // Рекурсивный случай: забираем одно выражение и считаем остальные ($head:expr $(, $tail:expr)* $(,)?) => { 1usize + count!($($tail),*) }; @@ -230,49 +224,44 @@ fn main() { let n = count!("a", "b", "c", "d"); assert_eq!(n, 4); - // Works at compile time too: + // Работает и на этапе компиляции: const N: usize = count!(1, 2, 3); assert_eq!(N, 3); } ``` ```rust -// Build a heterogeneous tuple from a list of expressions: +// Строим гетерогенный кортеж из списка выражений: macro_rules! tuple_from { - // Base: single element + // Базовый случай: один элемент ($single:expr $(,)?) => { ($single,) }; - // Recursive: first element + rest + // Рекурсивный случай: первый элемент и остальные ($head:expr, $($tail:expr),+ $(,)?) => { ($head, tuple_from!($($tail),+)) }; } let t = tuple_from!(1, "hello", 3.14, true); -// Expands to: (1, ("hello", (3.14, (true,)))) +// Раскрывается в: (1, ("hello", (3.14, (true,)))) ``` -**Fragment specifier subtleties**: +**Тонкости фрагментов**: -| Fragment | Gotcha | +| Фрагмент | Подвох | |----------|--------| -| `$x:expr` | Greedily parses — `1 + 2` is ONE expression, not three tokens | -| `$x:ty` | Greedily parses — `Vec` is one type; can't be followed by `+` or `<` | -| `$x:tt` | Matches exactly ONE token tree — most flexible, least checked | -| `$x:ident` | Only plain identifiers — not paths like `std::io` | -| `$x:pat` | In Rust 2021, matches `A \| B` patterns; use `$x:pat_param` for single patterns | +| `$x:expr` | Жадный разбор: `1 + 2` это ОДНО выражение, а не три токена | +| `$x:ty` | Жадный разбор: `Vec` это один тип; за ним нельзя ставить `+` или `<` | +| `$x:tt` | Сопоставляет ровно ОДНО дерево токенов: самый гибкий, но меньше всего проверяется | +| `$x:ident` | Только простые идентификаторы, а не пути вроде `std::io` | +| `$x:pat` | В Rust 2021 сопоставляет шаблоны `A \| B`; для одиночных образцов используйте `$x:pat_param` | -> **When to use `tt`**: When you need to forward tokens to another macro without -> the parser constraining them. `$($args:tt)*` is the "accept everything" pattern -> (used by `println!`, `format!`, `vec!`). +> **Когда использовать `tt`**: когда нужно передать токены другому макросу, не ограничивая их парсером. Шаблон `$($args:tt)*` означает «принять всё» (его используют `println!`, `format!`, `vec!`). -### Writing a Derive Macro with `syn` and `quote` +### Пишем derive-макрос с `syn` и `quote` -Derive macros live in a separate crate (`proc-macro = true`) and transform -a token stream using `syn` (parse Rust) and `quote` (generate Rust): +Derive-макросы живут в отдельном крейте (`proc-macro = true`) и преобразуют поток токенов с помощью `syn` (разбор Rust) и `quote` (генерация Rust): ```toml -``` - # my_derive/Cargo.toml [lib] proc-macro = true @@ -281,6 +270,7 @@ proc-macro = true syn = { version = "2", features = ["full"] } quote = "1" proc-macro2 = "1" +``` ```rust // my_derive/src/lib.rs @@ -288,15 +278,15 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; -/// Derive macro that generates a `describe()` method -/// returning the struct name and field names. +/// Derive-макрос, который генерирует метод `describe()`, +/// возвращающий имя структуры и имена её полей. #[proc_macro_derive(Describe)] pub fn derive_describe(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let name_str = name.to_string(); - // Extract field names (only for structs with named fields) + // Извлекаем имена полей (только для структур с именованными полями) let fields = match &input.data { syn::Data::Struct(data) => { data.fields.iter() @@ -322,7 +312,7 @@ pub fn derive_describe(input: TokenStream) -> TokenStream { ``` ```rust -// In the application crate: +// В приложении: use my_derive::Describe; #[derive(Describe)] @@ -338,40 +328,36 @@ fn main() { } ``` -**The workflow**: `TokenStream` (raw tokens) → `syn::parse` (AST) → -inspect/transform → `quote!` (generate tokens) → `TokenStream` (back to compiler). +**Рабочий процесс**: `TokenStream` (сырые токены) → `syn::parse` (AST) → анализ и преобразование → `quote!` (генерация токенов) → `TokenStream` (обратно компилятору). -| Crate | Role | Key types | -|-------|------|-----------| -| `proc-macro` | Compiler interface | `TokenStream` | -| `syn` | Parse Rust source into AST | `DeriveInput`, `ItemFn`, `Type` | -| `quote` | Generate Rust tokens from templates | `quote!{}`, `#variable` interpolation | -| `proc-macro2` | Bridge between syn/quote and proc-macro | `TokenStream`, `Span` | +| Крейт | Роль | Ключевые типы | +|-------|------|---------------| +| `proc-macro` | Интерфейс с компилятором | `TokenStream` | +| `syn` | Разбор исходного кода Rust в AST | `DeriveInput`, `ItemFn`, `Type` | +| `quote` | Генерация токенов Rust из шаблонов | `quote!{}`, интерполяция `#variable` | +| `proc-macro2` | Мост между syn/quote и proc-macro | `TokenStream`, `Span` | -> **Practical tip**: Start by studying the source of a simple derive macro -> like `thiserror` or `derive_more` before writing your own. The -> `cargo expand` command (via `cargo-expand`) shows what any macro expands -> to — invaluable for debugging. +> **Практический совет**: прежде чем писать собственный макрос, изучите исходники простого derive-макроса, например `thiserror` или `derive_more`. Команда `cargo expand` (через `cargo-expand`) показывает, во что раскрывается любой макрос: это бесценно для отладки. -> **Key Takeaways — Macros** -> - `macro_rules!` for simple code generation; proc macros (`syn` + `quote`) for complex derives -> - Prefer generics/traits over macros when possible — macros are harder to debug and maintain -> - `$crate` ensures hygiene; `tt` munching enables recursive pattern matching +> **Ключевые выводы: макросы** +> - `macro_rules!` для простой генерации кода; процедурные макросы (`syn` и `quote`) для сложных derive-макросов +> - По возможности предпочитайте обобщения и трейты макросам: макросы труднее отлаживать и поддерживать +> - `$crate` обеспечивает гигиену; `tt` munching даёт рекурсивное сопоставление с образцом -> **See also:** [Ch 2 — Traits](ch02-traits-in-depth.md) for when traits/generics beat macros. [Ch 13 — Testing](ch13-testing-and-benchmarking-patterns.md) for testing macro-generated code. +> **См. также:** [гл. 2 — Трейты](ch02-traits-in-depth.md) о том, когда трейты и обобщения лучше макросов. [гл. 14 — Тестирование](ch14-testing-and-benchmarking-patterns.md) о тестировании кода, сгенерированного макросами. ```mermaid flowchart LR - A["Source code"] --> B["macro_rules!
pattern matching"] - A --> C["#[derive(MyMacro)]
proc macro"] + A["Исходный код"] --> B["macro_rules!
сопоставление с образцом"] + A --> C["#[derive(MyMacro)]
процедурный макрос"] - B --> D["Token expansion"] - C --> E["syn: parse AST"] - E --> F["Transform"] - F --> G["quote!: generate tokens"] + B --> D["Раскрытие токенов"] + C --> E["syn: разбор AST"] + E --> F["Преобразование"] + F --> G["quote!: генерация токенов"] G --> D - D --> H["Compiled code"] + D --> H["Скомпилированный код"] style A fill:#e8f4f8,stroke:#2980b9,color:#000 style B fill:#d4efdf,stroke:#27ae60,color:#000 @@ -385,9 +371,9 @@ flowchart LR --- -### Exercise: Declarative Macro — `map!` ★ (~15 min) +### Упражнение: декларативный макрос `map!` ★ (~15 минут) -Write a `map!` macro that creates a `HashMap` from key-value pairs: +Напишите макрос `map!`, который создаёт `HashMap` из пар ключ-значение: ```rust,ignore let m = map! { @@ -397,10 +383,10 @@ let m = map! { assert_eq!(m.get("host"), Some(&"localhost")); ``` -Requirements: support trailing comma and empty invocation `map!{}`. +Требования: поддержите завершающую запятую и пустой вызов `map!{}`.
-🔑 Solution +🔑 Решение ```rust macro_rules! map { @@ -432,4 +418,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch14-testing-and-benchmarking-patterns.md b/rust-patterns-book/src/ch14-testing-and-benchmarking-patterns.md index be51ab5..742b72b 100644 --- a/rust-patterns-book/src/ch14-testing-and-benchmarking-patterns.md +++ b/rust-patterns-book/src/ch14-testing-and-benchmarking-patterns.md @@ -1,17 +1,17 @@ -# 13. Testing and Benchmarking Patterns 🟢 +# 14. Паттерны тестирования и бенчмаркинга 🟢 -> **What you'll learn:** -> - Rust's three test tiers: unit, integration, and doc tests -> - Property-based testing with proptest for discovering edge cases -> - Benchmarking with criterion for reliable performance measurement -> - Mocking strategies without heavyweight frameworks +> **Что вы узнаете:** +> - Три уровня тестов в Rust: модульные, интеграционные и doc-тесты +> - Тестирование свойств (property-based testing) с proptest для поиска граничных случаев +> - Бенчмаркинг с criterion для надёжного измерения производительности +> - Стратегии моков без громоздких фреймворков -## Unit Tests, Integration Tests, Doc Tests +## Модульные тесты, интеграционные тесты, doc-тесты -Rust has three testing tiers built into the language: +В Rust есть три уровня тестирования, встроенные в язык: ```rust -// --- Unit tests: in the same file as the code --- +// --- Модульные тесты: в том же файле, что и код --- pub fn factorial(n: u64) -> u64 { (1..=n).product() } @@ -22,7 +22,7 @@ mod tests { #[test] fn test_factorial_zero() { - // (1..=0).product() returns 1 — the multiplication identity for empty ranges + // (1..=0).product() возвращает 1: единица умножения для пустого диапазона assert_eq!(factorial(0), 1); } @@ -32,19 +32,19 @@ mod tests { } #[test] - #[cfg(debug_assertions)] // overflow checks are only enabled in debug mode + #[cfg(debug_assertions)] // Проверки переполнения включены только в отладочном режиме #[should_panic(expected = "overflow")] fn test_factorial_overflow() { - // ⚠️ This test only passes in debug mode (overflow checks enabled). - // In release mode (`cargo test --release`), u64 arithmetic wraps - // silently and no panic occurs. Use `checked_mul` or the - // `overflow-checks = true` profile setting for release-mode safety. - factorial(100); // Should panic on overflow + // ⚠️ Этот тест проходит только в отладочном режиме (проверки переполнения включены). + // В релизном режиме (`cargo test --release`) арифметика u64 молча переполняется, + // и паники не происходит. Для безопасности в релизе используйте `checked_mul` + // или настройку профиля `overflow-checks = true`. + factorial(100); // Должна паниковать при переполнении } #[test] fn test_with_result() -> Result<(), Box> { - // Tests can return Result — ? works inside! + // Тесты могут возвращать Result: внутри работает `?`! let value: u64 = "42".parse()?; assert_eq!(value, 42); Ok(()) @@ -53,9 +53,9 @@ mod tests { ``` ```rust -// --- Integration tests: in tests/ directory --- +// --- Интеграционные тесты: в директории tests/ --- // tests/integration_test.rs -// These test your crate's PUBLIC API only +// Эти тесты проверяют ТОЛЬКО публичный API вашего крейта use my_crate::factorial; @@ -66,19 +66,19 @@ fn test_factorial_from_outside() { ``` ```rust -// --- Doc tests: in documentation comments --- -/// Computes the factorial of `n`. +// --- Doc-тесты: в комментариях к документации --- +/// Вычисляет факториал `n`. /// -/// # Examples +/// # Примеры /// /// ``` /// use my_crate::factorial; /// assert_eq!(factorial(5), 120); /// ``` /// -/// # Panics +/// # Паника /// -/// Panics if the result overflows `u64`. +/// Паникует, если результат переполняет `u64`. /// /// ```should_panic /// my_crate::factorial(100); @@ -86,17 +86,17 @@ fn test_factorial_from_outside() { pub fn factorial(n: u64) -> u64 { (1..=n).product() } -// Doc tests are compiled and run by `cargo test` — they keep examples honest. +// Doc-тесты компилируются и запускаются через `cargo test`: они не дают примерам устареть. ``` -### Test Fixtures and Setup +### Фикстуры и подготовка тестов ```rust #[cfg(test)] mod tests { use super::*; - // Shared setup — create a helper function + // Общая подготовка: вынесем в вспомогательную функцию fn setup_database() -> TestDb { let db = TestDb::new_in_memory(); db.run_migrations(); @@ -119,7 +119,7 @@ mod tests { assert!(db.get_user("Bob").is_none()); } - // Cleanup with Drop (RAII): + // Очистка через Drop (RAII): struct TempDir { path: std::path::PathBuf, } @@ -141,16 +141,16 @@ mod tests { #[test] fn test_file_operations() { - let dir = TempDir::new(); // Created + let dir = TempDir::new(); // Создан std::fs::write(dir.path.join("test.txt"), "hello").unwrap(); assert!(dir.path.join("test.txt").exists()); - } // dir dropped here → temp directory cleaned up + } // dir уничтожается здесь → временный каталог удаляется } ``` -### Property-Based Testing (proptest) +### Тестирование свойств (proptest) -Instead of testing specific values, test *properties* that should always hold: +Вместо проверки конкретных значений тестируйте *свойства*, которые должны выполняться всегда: ```rust // Cargo.toml: proptest = "1" @@ -163,12 +163,13 @@ fn reverse(v: &[i32]) -> Vec { proptest! { #[test] fn test_reverse_twice_is_identity(v in prop::collection::vec(any::(), 0..100)) { - // Property: reversing twice gives back the original + // Свойство: двойной разворот возвращает исходный вектор assert_eq!(reverse(&reverse(&v)), v); } #[test] fn test_reverse_preserves_length(v in prop::collection::vec(any::(), 0..100)) { + // Свойство: разворот сохраняет длину assert_eq!(reverse(&v).len(), v.len()); } @@ -177,12 +178,12 @@ proptest! { v.sort(); let sorted_once = v.clone(); v.sort(); - assert_eq!(v, sorted_once); // Sorting twice = sorting once + assert_eq!(v, sorted_once); // Двойная сортировка = однократная сортировка } #[test] fn test_parse_roundtrip(x in any::().prop_filter("finite", |x| x.is_finite())) { - // Property: formatting then parsing gives back the same value + // Свойство: форматирование, затем разбор возвращают то же значение let s = format!("{x}"); let parsed: f64 = s.parse().unwrap(); prop_assert!((x - parsed).abs() < f64::EPSILON); @@ -190,12 +191,9 @@ proptest! { } ``` -> **When to use proptest**: When you're testing a function with a large input -> space and want confidence it works for edge cases you didn't think of. -> proptest generates hundreds of random inputs and shrinks failures to the -> minimal reproducing case. +> **Когда использовать proptest**: когда вы тестируете функцию с большим пространством входных данных и хотите быть уверены, что она работает на граничных случаях, о которых вы не подумали. proptest генерирует сотни случайных входов и минимизирует найденные ошибки до минимального воспроизводящего примера. -### Benchmarking with criterion +### Бенчмаркинг с criterion ```rust // Cargo.toml: @@ -221,7 +219,7 @@ fn bench_fibonacci(c: &mut Criterion) { b.iter(|| fibonacci(black_box(20))) }); - // Compare different implementations: + // Сравниваем разные входные данные: let mut group = c.benchmark_group("fibonacci_compare"); for size in [10, 15, 20, 25] { group.bench_with_input( @@ -236,16 +234,16 @@ fn bench_fibonacci(c: &mut Criterion) { criterion_group!(benches, bench_fibonacci); criterion_main!(benches); -// Run: cargo bench -// Produces HTML reports in target/criterion/ +// Запуск: cargo bench +// Создаёт HTML-отчёты в target/criterion/ ``` -### Mocking Strategies without Frameworks +### Стратегии моков без фреймворков -Rust's trait system provides natural dependency injection — no mocking framework required: +Система трейтов Rust даёт естественное внедрение зависимостей, и никакой фреймворк для моков не нужен: ```rust -// Define behavior as a trait +// Описываем поведение через трейт trait Clock { fn now(&self) -> std::time::Instant; } @@ -254,13 +252,13 @@ trait HttpClient { fn get(&self, url: &str) -> Result; } -// Production implementations +// Реализации для продакшена struct RealClock; impl Clock for RealClock { fn now(&self) -> std::time::Instant { std::time::Instant::now() } } -// Service depends on abstractions +// Сервис зависит от абстракций struct CacheService { clock: C, client: H, @@ -269,12 +267,12 @@ struct CacheService { impl CacheService { fn fetch(&self, url: &str) -> Result { - // Uses self.clock and self.client — injectable + // Использует self.clock и self.client: их можно подменить self.client.get(url) } } -// Test with mock implementations — no framework needed! +// Тест с моками-реализациями: никакой фреймворк не нужен! #[cfg(test)] mod tests { use super::*; @@ -308,29 +306,27 @@ mod tests { } ``` -> **Test philosophy**: Prefer real dependencies in integration tests, trait-based -> mocks in unit tests. Avoid mocking frameworks unless your dependency graph is -> complex — Rust's trait generics handle most cases naturally. +> **Философия тестирования**: в интеграционных тестах предпочитайте реальные зависимости, а в модульных тестах моки на основе трейтов. Не используйте фреймворки для моков, если граф зависимостей не сложный: обобщения трейтов справляются с большинством случаев естественным образом. -> **Key Takeaways — Testing** -> - Doc tests (`///`) double as documentation and regression tests — they're compiled and run -> - `proptest` generates random inputs to find edge cases you'd never write manually -> - `criterion` provides statistically rigorous benchmarks with HTML reports -> - Mock via trait generics + test doubles, not mock frameworks +> **Ключевые выводы: тестирование** +> - Doc-тесты (`///`) одновременно служат документацией и регрессионными тестами: их компилируют и запускают +> - `proptest` генерирует случайные входные данные, чтобы находить граничные случаи, которые вы никогда не написали бы вручную +> - `criterion` даёт статистически обоснованные бенчмарки с HTML-отчётами +> - Мокайте через обобщения трейтов и тестовые двойники, а не через фреймворки для моков -> **See also:** [Ch 12 — Macros](ch12-macros-code-that-writes-code.md) for testing macro-generated code. [Ch 14 — API Design](ch14-crate-architecture-and-api-design.md) for how module layout affects test organization. +> **См. также:** [гл. 13 — Макросы](ch13-macros-code-that-writes-code.md) о тестировании кода, сгенерированного макросами. [гл. 15 — Дизайн API](ch15-crate-architecture-and-api-design.md) о том, как структура модулей влияет на организацию тестов. --- -### Exercise: Property-Based Testing with proptest ★★ (~25 min) +### Упражнение: тестирование свойств с proptest ★★ (~25 минут) -Write a `SortedVec` wrapper that maintains a sorted invariant. Use `proptest` to verify that: -1. After any sequence of insertions, the internal vec is always sorted -2. `contains()` agrees with the stdlib `Vec::contains()` -3. The length equals the number of insertions +Напишите обёртку `SortedVec`, которая поддерживает инвариант сортировки. Используйте `proptest`, чтобы проверить, что: +1. После любой последовательности вставок внутренний вектор всегда отсортирован +2. `contains()` даёт тот же результат, что и `Vec::contains()` из стандартной библиотеки +3. Длина равна количеству вставок
-🔑 Solution +🔑 Решение ```rust,ignore #[derive(Debug)] @@ -390,4 +386,3 @@ mod tests {
*** - diff --git a/rust-patterns-book/src/ch15-crate-architecture-and-api-design.md b/rust-patterns-book/src/ch15-crate-architecture-and-api-design.md index 177810d..4a700a4 100644 --- a/rust-patterns-book/src/ch15-crate-architecture-and-api-design.md +++ b/rust-patterns-book/src/ch15-crate-architecture-and-api-design.md @@ -1,73 +1,73 @@ -# 14. Crate Architecture and API Design 🟡 +# 15. Архитектура крейтов и дизайн API 🟡 -> **What you'll learn:** -> - Module layout conventions and re-export strategies -> - The public API design checklist for polished crates -> - Ergonomic parameter patterns: `impl Into`, `AsRef`, `Cow` -> - "Parse, don't validate" with `TryFrom` and validated types -> - Feature flags, conditional compilation, and workspace organization +> **Что вы узнаете:** +> - Соглашения о структуре модулей и стратегии реэкспорта +> - Чек-лист проектирования публичного API для аккуратных крейтов +> - Эргономичные паттерны параметров: `impl Into`, `AsRef`, `Cow` +> - «Parse, don't validate» с `TryFrom` и проверенными типами +> - Флаги функций, условную компиляцию и организацию воркспейсов -## Module Layout Conventions +## Соглашения о структуре модулей ```text my_crate/ ├── Cargo.toml ├── src/ -│ ├── lib.rs # Crate root — re-exports and public API -│ ├── config.rs # Feature module -│ ├── parser/ # Complex module with sub-modules -│ │ ├── mod.rs # or parser.rs at parent level (Rust 2018+) +│ ├── lib.rs # Корень крейта: реэкспорты и публичный API +│ ├── config.rs # Модуль функциональности +│ ├── parser/ # Сложный модуль с подмодулями +│ │ ├── mod.rs # или parser.rs на уровне родителя (Rust 2018+) │ │ ├── lexer.rs │ │ └── ast.rs -│ ├── error.rs # Error types -│ └── utils.rs # Internal helpers (pub(crate)) +│ ├── error.rs # Типы ошибок +│ └── utils.rs # Внутренние утилиты (pub(crate)) ├── tests/ -│ └── integration.rs # Integration tests +│ └── integration.rs # Интеграционные тесты ├── benches/ -│ └── perf.rs # Benchmarks +│ └── perf.rs # Бенчмарки └── examples/ └── basic.rs # cargo run --example basic ``` ```rust -// lib.rs — curate your public API with re-exports: +// lib.rs: курируйте публичный API через реэкспорты: mod config; mod error; mod parser; mod utils; -// Re-export what users need: +// Реэкспортируем то, что нужно пользователям: pub use config::Config; pub use error::Error; pub use parser::Parser; -// Public types are at the crate root — users write: +// Публичные типы находятся в корне крейта, пользователи пишут: // use my_crate::Config; -// NOT: use my_crate::config::Config; +// НЕ так: use my_crate::config::Config; ``` -**Visibility modifiers**: - -| Modifier | Visible To | -|----------|-----------| -| `pub` | Everyone | -| `pub(crate)` | This crate only | -| `pub(super)` | Parent module | -| `pub(in path)` | Specific ancestor module | -| (none) | Current module and its children | - -### Public API Design Checklist - -1. **Accept references, return owned** — `fn process(input: &str) -> String` -2. **Use `impl Trait` for parameters** — `fn read(r: impl Read)` instead of `fn read(r: R)` for cleaner signatures -3. **Return `Result`, not `panic!`** — let callers decide how to handle errors -4. **Implement standard traits** — `Debug`, `Display`, `Clone`, `Default`, `From`/`Into` -5. **Make invalid states unrepresentable** — use type states and newtypes -6. **Follow the builder pattern for complex configuration** — with type-state if fields are required -7. **Seal traits you don't want users to implement** — `pub trait Sealed: private::Sealed {}` -8. **Mark types and functions `#[must_use]`** — prevents silent discard of important `Result`s, guards, or values. Apply to any type where ignoring the return value is almost certainly a bug: +**Модификаторы видимости**: + +| Модификатор | Видим для | +|-------------|-----------| +| `pub` | Всех | +| `pub(crate)` | Только этого крейта | +| `pub(super)` | Родительского модуля | +| `pub(in path)` | Конкретного модуля-предка | +| (нет) | Текущего модуля и его потомков | + +### Чек-лист проектирования публичного API + +1. **Принимайте ссылки, возвращайте владеющие значения**: `fn process(input: &str) -> String` +2. **Используйте `impl Trait` для параметров**: `fn read(r: impl Read)` вместо `fn read(r: R)` для более чистых сигнатур +3. **Возвращайте `Result`, а не `panic!`**: пусть вызывающий код решает, как обрабатывать ошибки +4. **Реализуйте стандартные трейты**: `Debug`, `Display`, `Clone`, `Default`, `From`/`Into` +5. **Делайте недопустимые состояния невыразимыми**: используйте type-state и newtype +6. **Используйте паттерн builder для сложной конфигурации**: с type-state, если поля обязательны +7. **Запечатывайте трейты, которые не должны реализовывать пользователи**: `pub trait Sealed: private::Sealed {}` +8. **Помечайте типы и функции `#[must_use]`**: это предотвращает молчаливое игнорирование важных `Result`, защитников или значений. Применяйте к любому типу, где игнорирование возвращаемого значения почти наверняка является ошибкой: ```rust - #[must_use = "dropping the guard immediately releases the lock"] + #[must_use = "немедленное уничтожение защитника сразу освобождает блокировку"] pub struct LockGuard<'a, T> { /* ... */ } #[must_use] @@ -75,7 +75,7 @@ pub use parser::Parser; ``` ```rust -// Sealed trait pattern — users can use but not implement: +// Паттерн sealed-трейта: пользователи могут использовать, но не реализовать: mod private { pub trait Sealed {} } @@ -84,7 +84,7 @@ pub trait DatabaseDriver: private::Sealed { fn connect(&self, url: &str) -> Connection; } -// Only types in THIS crate can implement Sealed → only we can implement DatabaseDriver +// Только типы ЭТОГО крейта могут реализовать Sealed, значит, только мы можем реализовать DatabaseDriver pub struct PostgresDriver; impl private::Sealed for PostgresDriver {} impl DatabaseDriver for PostgresDriver { @@ -92,64 +92,56 @@ impl DatabaseDriver for PostgresDriver { } ``` -> **`#[non_exhaustive]`** — mark public enums and structs so that adding variants -> or fields is not a breaking change. Downstream crates must use a wildcard arm -> (`_ =>`) in match statements, and cannot construct the type with struct literal -> syntax: +> **`#[non_exhaustive]`**: помечайте публичные перечисления и структуры так, чтобы добавление вариантов или полей не было несовместимым изменением. Внешние крейты должны использовать шаблонную ветку (`_ =>`) в `match` и не могут создавать этот тип с помощью синтаксиса литерала структуры: > ```rust > #[non_exhaustive] > pub enum DiagError { > Timeout, > HardwareFault, -> // Adding a new variant in a future release is NOT a semver break. +> // Добавление нового варианта в будущем выпуске НЕ является несовместимым изменением semver. > } > ``` -### Ergonomic Parameter Patterns — `impl Into`, `AsRef`, `Cow` +### Эргономичные паттерны параметров: `impl Into`, `AsRef`, `Cow` -One of Rust's most impactful API patterns is accepting the **most general type** in -function parameters, so callers don't need repetitive `.to_string()`, `&*s`, or `.as_ref()` -at every call site. This is the Rust-specific version of "be liberal in what you accept." +Один из самых влиятельных паттернов API в Rust: принимать в параметрах функций **наиболее общий тип**, чтобы вызывающему коду не приходилось на каждом шагу писать повторяющиеся `.to_string()`, `&*s` или `.as_ref()`. Это версия принципа «будь либерален в том, что принимаешь», специфичная для Rust. -#### `impl Into` — Accept Anything Convertible +#### `impl Into`: принимаем всё, что конвертируется ```rust -// ❌ Friction: callers must convert manually +// ❌ Неудобно: вызывающие должны конвертировать вручную fn connect(host: String, port: u16) -> Connection { // ... } -connect("localhost".to_string(), 5432); // Annoying .to_string() -connect(hostname.clone(), 5432); // Unnecessary clone if we already have String +connect("localhost".to_string(), 5432); // Раздражающий .to_string() +connect(hostname.clone(), 5432); // Лишнее клонирование, если уже есть String -// ✅ Ergonomic: accept anything that converts to String +// ✅ Удобно: принимаем всё, что конвертируется в String fn connect(host: impl Into, port: u16) -> Connection { - let host = host.into(); // Convert once, inside the function + let host = host.into(); // Конвертируем один раз, внутри функции // ... } -connect("localhost", 5432); // &str — zero friction -connect(hostname, 5432); // String — moved, no clone -connect(arc_str, 5432); // Arc if From is implemented +connect("localhost", 5432); // &str: без лишних усилий +connect(hostname, 5432); // String: перемещаем, без клонирования +connect(arc_str, 5432); // Arc, если реализован From ``` -This works because Rust's `From`/`Into` trait pair provides blanket conversions. -When you accept `impl Into`, you're saying: "give me anything that knows how to -become a `T`." +Это работает, потому что пара трейтов `From`/`Into` обеспечивает общие преобразования. Принимая `impl Into`, вы говорите: «дайте мне всё, что умеет стать `T`». -#### `AsRef` — Borrow as a Reference +#### `AsRef`: заимствование как ссылка -`AsRef` is the borrowing counterpart to `Into`. Use it when you only need -to *read* the data, not take ownership: +`AsRef` это заимствующий аналог `Into`. Используйте его, когда нужно только *читать* данные, а не забирать владение: ```rust use std::path::Path; -// ❌ Forces callers to convert to &Path +// ❌ Вынуждает вызывающих конвертировать в &Path fn file_exists(path: &Path) -> bool { path.exists() } -file_exists(Path::new("/tmp/test.txt")); // Awkward +file_exists(Path::new("/tmp/test.txt")); // Неудобно -// ✅ Accept anything that can behave as a &Path +// ✅ Принимаем всё, что может вести себя как &Path fn file_exists(path: impl AsRef) -> bool { path.as_ref().exists() } @@ -158,7 +150,7 @@ file_exists(String::from("/tmp/test.txt")); // String ✅ file_exists(Path::new("/tmp/test.txt")); // &Path ✅ file_exists(PathBuf::from("/tmp/test.txt")); // PathBuf ✅ -// Same pattern for string-like parameters: +// Тот же паттерн для строковых параметров: fn log_message(msg: impl AsRef) { println!("[LOG] {}", msg.as_ref()); } @@ -166,73 +158,68 @@ log_message("hello"); // &str ✅ log_message(String::from("hello")); // String ✅ ``` -#### `Cow` — Clone on Write +#### `Cow`: клонирование при записи -`Cow<'a, T>` (Clone on Write) delays allocation until mutation is needed. -It holds either a borrowed `&T` or an owned `T::Owned`. This is perfect when -most calls don't need to modify the data: +`Cow<'a, T>` (Clone on Write) откладывает выделение памяти до момента, когда понадобится изменение. Он хранит либо заимствованный `&T`, либо владеющий `T::Owned`. Это идеально, когда большинство вызовов не требует изменения данных: ```rust use std::borrow::Cow; -/// Normalizes a diagnostic message — only allocates if changes are needed. +/// Нормализует диагностическое сообщение: выделяет память, только если нужны изменения. fn normalize_message(msg: &str) -> Cow<'_, str> { if msg.contains('\t') || msg.contains('\r') { - // Must allocate — we need to modify the content + // Нужно выделить память: требуется изменить содержимое Cow::Owned(msg.replace('\t', " ").replace('\r', "")) } else { - // No allocation — just borrow the original + // Без выделения памяти: просто заимствуем исходное Cow::Borrowed(msg) } } -// Most messages pass through without allocation: -let clean = normalize_message("All tests passed"); // Borrowed — free -let fixed = normalize_message("Error:\tfailed\r\n"); // Owned — allocated +// Большинство сообщений проходят без выделения памяти: +let clean = normalize_message("Все тесты пройдены"); // Borrowed: бесплатно +let fixed = normalize_message("Ошибка:\tсбой\r\n"); // Owned: выделено -// Cow implements Deref, so it works like &str: +// Cow реализует Deref, поэтому работает как &str: println!("{}", clean); println!("{}", fixed.to_uppercase()); ``` -#### Quick Reference: Which to Use +#### Краткая справка: что выбрать ```text -Do you need ownership of the data inside the function? -├── YES → impl Into -│ "Give me anything that can become a T" -└── NO → Do you only need to read it? - ├── YES → impl AsRef or &T - │ "Give me anything I can borrow as a &T" - └── MAYBE (might need to modify sometimes?) +Нужно ли функции владеть данными внутри? +├── ДА → impl Into +│ «Дайте мне всё, что может стать T» +└── НЕТ → Нужно ли только читать? + ├── ДА → impl AsRef или &T + │ «Дайте мне всё, что можно заимствовать как &T» + └── ВОЗМОЖНО (иногда нужно изменять?) └── Cow<'_, T> - "Borrow if possible, clone only when you must" + «Заимствуем, если возможно, клонируем, только когда это необходимо» ``` -| Pattern | Ownership | Allocation | When to use | -|---------|-----------|------------|-------------| -| `&str` | Borrowed | Never | Simple string params | -| `impl AsRef` | Borrowed | Never | Accept String, &str, etc. — read only | -| `impl Into` | Owned | On conversion | Accept &str, String — will store/own | -| `Cow<'_, str>` | Either | Only if modified | Processing that usually doesn't modify | -| `&[u8]` / `impl AsRef<[u8]>` | Borrowed | Never | Byte-oriented APIs | +| Паттерн | Владение | Выделение памяти | Когда использовать | +|---------|----------|------------------|--------------------| +| `&str` | Заимствование | Никогда | Простые строковые параметры | +| `impl AsRef` | Заимствование | Никогда | Принимает String, &str и т. д.: только чтение | +| `impl Into` | Владение | При конвертации | Принимает &str и String: будет хранить или владеть | +| `Cow<'_, str>` | Любое | Только при изменении | Обработка, которая обычно не меняет данные | +| `&[u8]` / `impl AsRef<[u8]>` | Заимствование | Никогда | API для работы с байтами | -> **`Borrow` vs `AsRef`**: Both provide `&T`, but `Borrow` additionally -> guarantees that `Eq`, `Ord`, and `Hash` are **consistent** between the original -> and borrowed form. This is why `HashMap::get()` accepts `&Q where String: Borrow` — not `AsRef`. Use `Borrow` when the borrowed form is used -> as a lookup key; use `AsRef` for general "give me a reference" parameters. +> **`Borrow` против `AsRef`**: оба дают `&T`, но `Borrow` дополнительно гарантирует, что `Eq`, `Ord` и `Hash` **согласованы** между исходной и заимствованной формой. Поэтому `HashMap::get()` принимает `&Q` при условии `String: Borrow`, а не `AsRef`. Используйте `Borrow`, когда заимствованная форма применяется как ключ поиска, а `AsRef` для общих параметров «дайте мне ссылку». -#### Composing Conversions in APIs +#### Композиция преобразований в API ```rust -/// A well-designed diagnostic API using ergonomic parameters: +/// Хорошо спроектированный API для диагностики с эргономичными параметрами: pub struct DiagRunner { name: String, config_path: PathBuf, } impl DiagRunner { - /// Accept any string-like type for name, any path-like type for config. + /// Принимаем любой строковый тип для name и любой путь для config. pub fn new( name: impl Into, config_path: impl Into, @@ -243,13 +230,13 @@ impl DiagRunner { } } - /// Accept any AsRef for read-only lookup. + /// Принимаем любой AsRef для поиска только для чтения. pub fn get_result(&self, test_name: impl AsRef) -> Option<&TestResult> { self.results.get(test_name.as_ref()) } } -// All of these work with zero caller friction: +// Все эти вызовы работают без лишних усилий со стороны вызывающего кода: let runner = DiagRunner::new("GPU Diag", "/etc/diag_tool/config.json"); let runner = DiagRunner::new(format!("Diag-{}", node_id), config_path); let runner = DiagRunner::new(name_string, path_buf); @@ -257,103 +244,99 @@ let runner = DiagRunner::new(name_string, path_buf); *** -## Case Study: Designing a Public Crate API — Before & After +## Пример из практики: проектирование публичного API крейта, до и после -A real-world example of evolving a stringly-typed internal API into an ergonomic, type-safe public API. Consider a configuration parser crate: +Реальный пример перехода от внутреннего API, построенного на строках («строковый» API, stringly-typed), к эргономичному типобезопасному публичному API. Рассмотрим крейт-парсер конфигурации: -**Before** (stringly-typed, easy to misuse): +**До** (строки повсюду, легко ошибиться): ```rust -// ❌ All parameters are strings — no compile-time validation +// ❌ Все параметры — строки: нет проверки на этапе компиляции pub fn parse_config(path: &str, format: &str, strict: bool) -> Result { - // What formats are valid? "json"? "JSON"? "Json"? - // Is path a file path or URL? - // What does "strict" even mean? + // Какие форматы допустимы? "json"? "JSON"? "Json"? + // path — это путь к файлу или URL? + // Что вообще значит "strict"? todo!() } ``` -**After** (type-safe, self-documenting): +**После** (типобезопасно, самодокументируется): ```rust use std::path::Path; -/// Supported configuration formats. +/// Поддерживаемые форматы конфигурации. #[derive(Debug, Clone, Copy)] -#[non_exhaustive] // Adding formats won't break downstream +#[non_exhaustive] // Добавление форматов не сломает внешний код pub enum Format { Json, Toml, Yaml, } -/// Controls parsing strictness. +/// Управляет строгостью разбора. #[derive(Debug, Clone, Copy, Default)] pub enum Strictness { - /// Reject unknown fields (default for libraries) + /// Отвергать неизвестные поля (по умолчанию для библиотек) #[default] Strict, - /// Ignore unknown fields (useful for forward-compatible configs) + /// Игнорировать неизвестные поля (полезно для конфигов с прямой совместимостью) Lenient, } pub fn parse_config( - path: &Path, // Type-enforced: must be a filesystem path - format: Format, // Enum: impossible to pass invalid format - strictness: Strictness, // Named alternatives, not a bare bool + path: &Path, // Проверяется типом: должен быть путём файловой системы + format: Format, // Перечисление: невозможно передать неверный формат + strictness: Strictness, // Именованные варианты, а не голый bool ) -> Result { todo!() } ``` -**What improved**: +**Что улучшилось**: -| Aspect | Before | After | -|--------|--------|-------| -| Format validation | Runtime string comparison | Compile-time enum | -| Path type | Raw `&str` (could be anything) | `&Path` (filesystem-specific) | -| Strictness | Mystery `bool` | Self-documenting enum | -| Error type | `String` (opaque) | `ConfigError` (structured) | -| Extensibility | Breaking changes | `#[non_exhaustive]` | +| Аспект | До | После | +|--------|----|-------| +| Проверка формата | Сравнение строк во время выполнения | Перечисление на этапе компиляции | +| Тип пути | Сырой `&str` (что угодно) | `&Path` (специфичный для файловой системы) | +| Строгость | Загадочный `bool` | Самодокументируемое перечисление | +| Тип ошибки | `String` (непрозрачный) | `ConfigError` (структурированный) | +| Расширяемость | Несовместимые изменения | `#[non_exhaustive]` | -> **Rule of thumb**: If you find yourself writing a `match` on string values, -> consider replacing the parameter with an enum. If a parameter is a boolean -> that isn't obvious from context, use a two-variant enum instead. +> **Практическое правило**: если вы пишете `match` по строковым значениям, подумайте о замене параметра перечислением. Если параметр это булев флаг, смысл которого неочевиден из контекста, используйте перечисление с двумя вариантами. *** -### Parse Don't Validate — `TryFrom` and Validated Types +### Parse Don't Validate: `TryFrom` и проверенные типы -"Parse, don't validate" is a principle that says: **don't check data and then pass -around the raw unchecked form — instead, parse it into a type that can only exist -if the data is valid.** Rust's `TryFrom` trait is the standard tool for this. +«Parse, don't validate» (разбирай, а не проверяй) это принцип, который говорит: **не проверяйте данные и затем не передавайте дальше непроверенную сырую форму. Вместо этого разберите их в тип, который может существовать только тогда, когда данные корректны.** Стандартный инструмент Rust для этого трейт `TryFrom`. -#### The Problem: Validation Without Enforcement +#### Проблема: проверка без принуждения ```rust -// ❌ Validate-then-use: nothing prevents using an invalid value after the check +// ❌ Проверяем, а потом используем: ничто не мешает использовать некорректное значение после проверки fn process_port(port: u16) { if port == 0 || port > 65535 { - panic!("Invalid port"); // We checked, but... + panic!("Некорректный порт"); // Мы проверили, но... } - start_server(port); // What if someone calls start_server(0) directly? + start_server(port); // Что если кто-то вызовет start_server(0) напрямую? } -// ❌ Stringly-typed: an email is just a String — any garbage gets through +// ❌ Строка на все случаи: email это просто String, любой мусор проходит fn send_email(to: String, body: String) { - // Is `to` actually a valid email? We don't know. - // Someone could pass "not-an-email" and we only find out at the SMTP server. + // Действительно ли `to` корректный email? Мы не знаем. + // Кто-то может передать "not-an-email", и мы узнаем об этом только на SMTP-сервере. } ``` -#### The Solution: Parse Into Validated Newtypes with `TryFrom` +#### Решение: разбор в проверенные newtype через `TryFrom` ```rust use std::convert::TryFrom; use std::fmt; -/// A validated TCP port number (1–65535). -/// If you have a `Port`, it is guaranteed valid. +/// Проверенный номер TCP-порта (1–65535). +/// Если у вас есть `Port`, он гарантированно корректен. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Port(u16); @@ -382,35 +365,35 @@ pub enum PortError { impl fmt::Display for PortError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PortError::Zero => write!(f, "port must be non-zero"), - PortError::InvalidFormat => write!(f, "invalid port format"), + PortError::Zero => write!(f, "порт должен быть ненулевым"), + PortError::InvalidFormat => write!(f, "некорректный формат порта"), } } } impl std::error::Error for PortError {} -// Now the type system enforces validity: +// Теперь система типов обеспечивает корректность: fn start_server(port: Port) { - // No validation needed — Port can only be constructed via TryFrom, - // which already verified it's valid. - println!("Listening on port {}", port.get()); + // Проверка не нужна: Port можно создать только через TryFrom, + // который уже проверил его корректность. + println!("Слушаем порт {}", port.get()); } -// Usage: +// Использование: fn main() -> Result<(), Box> { - let port = Port::try_from(8080)?; // ✅ Validated once at the boundary - start_server(port); // No re-validation anywhere downstream + let port = Port::try_from(8080)?; // ✅ Проверяем один раз на границе + start_server(port); // Никакой повторной проверки ниже по цепочке let bad = Port::try_from(0); // ❌ Err(PortError::Zero) Ok(()) } ``` -#### Real-World Example: Validated IPMI Address +#### Пример из практики: проверенный адрес IPMI ```rust -/// A validated IPMI slave address (0x20–0xFE, even only). +/// Проверенный адрес ведомого устройства IPMI (0x20–0xFE, только чётные). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IpmiAddr(u8); @@ -423,9 +406,9 @@ pub enum IpmiAddrError { impl fmt::Display for IpmiAddrError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IpmiAddrError::Odd(v) => write!(f, "IPMI address 0x{v:02X} must be even"), + IpmiAddrError::Odd(v) => write!(f, "адрес IPMI 0x{v:02X} должен быть чётным"), IpmiAddrError::OutOfRange(v) => { - write!(f, "IPMI address 0x{v:02X} out of range (0x20..=0xFE)") + write!(f, "адрес IPMI 0x{v:02X} вне диапазона (0x20..=0xFE)") } } } @@ -449,16 +432,16 @@ impl IpmiAddr { pub fn get(&self) -> u8 { self.0 } } -// Downstream code never needs to re-check: +// Код ниже по цепочке никогда не нужно проверять заново: fn send_ipmi_command(addr: IpmiAddr, cmd: u8, data: &[u8]) -> Result, IpmiError> { - // addr.get() is guaranteed to be a valid, even IPMI address + // addr.get() гарантированно является корректным чётным адресом IPMI raw_ipmi_send(addr.get(), cmd, data) } ``` -#### Parsing Strings with `FromStr` +#### Разбор строк через `FromStr` -For types that are commonly parsed from text (CLI args, config files), implement `FromStr`: +Для типов, которые часто разбираются из текста (аргументы CLI, файлы конфигурации), реализуйте `FromStr`: ```rust use std::str::FromStr; @@ -472,22 +455,22 @@ impl FromStr for Port { } } -// Now works with .parse(): -let port: Port = "8080".parse()?; // Validates in one step +// Теперь работает с .parse(): +let port: Port = "8080".parse()?; // Проверяется за один шаг -// And with clap CLI parsing: +// И с разбором аргументов clap: // #[derive(Parser)] // struct Args { // #[arg(short, long)] -// port: Port, // clap calls FromStr automatically +// port: Port, // clap вызывает FromStr автоматически // } ``` -#### `TryFrom` Chain for Complex Validation +#### Цепочка `TryFrom` для сложной проверки ```rust -// Stub types for this example — in production these would be in -// separate modules with their own TryFrom implementations. +// Заглушки типов для этого примера. В продакшене они были бы в +// отдельных модулях со своими реализациями TryFrom. ``` ```rust @@ -500,7 +483,7 @@ let port: Port = "8080".parse()?; // Validates in one step # impl TryFrom for Timeout { # type Error = String; # fn try_from(ms: u64) -> Result { -# if ms == 0 { Err("timeout must be > 0".into()) } else { Ok(Timeout(ms)) } +# if ms == 0 { Err("таймаут должен быть > 0".into()) } else { Ok(Timeout(ms)) } # } # } # struct RawConfig { host: String, port: u16, timeout_ms: u64 } @@ -516,7 +499,7 @@ let port: Port = "8080".parse()?; // Validates in one step # impl From for ConfigError { # fn from(e: serde_json::Error) -> Self { ConfigError::InvalidHost(e.to_string()) } # } -/// A validated configuration that can only exist if all fields are valid. +/// Проверенная конфигурация, которая может существовать, только если все поля корректны. pub struct ValidConfig { pub host: Hostname, pub port: Port, @@ -538,44 +521,40 @@ impl TryFrom for ValidConfig { } } -// Parse once at the boundary, use the validated type everywhere: +// Разбираем один раз на границе, используем проверенный тип везде: fn load_config(path: &str) -> Result { let raw: RawConfig = serde_json::from_str(&std::fs::read_to_string(path)?)?; - ValidConfig::try_from(raw) // All validation happens here + ValidConfig::try_from(raw) // Вся проверка происходит здесь } ``` -#### Summary: Validate vs Parse +#### Итоги: проверка против разбора -| Approach | Data checked? | Compiler enforces validity? | Re-validation needed? | -|----------|:---:|:---:|:---:| -| Runtime checks (if/assert) | ✅ | ❌ | Every function boundary | -| Validated newtype + `TryFrom` | ✅ | ✅ | Never — type is proof | +| Подход | Данные проверены? | Компилятор следит за корректностью? | Нужна повторная проверка? | +|--------|:-----------------:|:----------------------------------:|:-------------------------:| +| Проверки во время выполнения (if/assert) | ✅ | ❌ | На каждой границе функции | +| Проверенный newtype + `TryFrom` | ✅ | ✅ | Никогда: тип является доказательством | -The rule: **parse at the boundary, use validated types everywhere inside.** -Raw strings, integers, and byte slices enter your system, get parsed into -validated types via `TryFrom`/`FromStr`, and from that point forward the type -system guarantees they're valid. +Правило: **разбирайте на границе, а внутри используйте проверенные типы.** Сырые строки, целые числа и срезы байтов попадают в вашу систему, разбираются в проверенные типы через `TryFrom`/`FromStr`, и с этого момента система типов гарантирует их корректность. -### Feature Flags and Conditional Compilation +### Флаги функций и условная компиляция ```toml -``` - # Cargo.toml [features] -default = ["json"] # Enabled by default -json = ["dep:serde_json"] # Enables JSON support -xml = ["dep:quick-xml"] # Enables XML support -full = ["json", "xml"] # Meta-feature: enables all +default = ["json"] # Включена по умолчанию +json = ["dep:serde_json"] # Включает поддержку JSON +xml = ["dep:quick-xml"] # Включает поддержку XML +full = ["json", "xml"] # Мета-фича: включает всё [dependencies] serde = "1" serde_json = { version = "1", optional = true } quick-xml = { version = "0.31", optional = true } +``` ```rust -// Conditional compilation based on features: +// Условная компиляция в зависимости от фич: #[cfg(feature = "json")] pub fn to_json(value: &T) -> String { serde_json::to_string(value).unwrap() @@ -586,138 +565,127 @@ pub fn to_xml(value: &T) -> String { quick_xml::se::to_string(value).unwrap() } -// Compile error if a required feature isn't enabled: +// Ошибка компиляции, если нужная фича не включена: #[cfg(not(any(feature = "json", feature = "xml")))] -compile_error!("At least one format feature (json, xml) must be enabled"); +compile_error!("Должна быть включена хотя бы одна фича формата (json, xml)"); ``` -**Best practices**: -- Keep `default` features minimal — users can opt in -- Use `dep:` syntax (Rust 1.60+) for optional dependencies to avoid creating implicit features -- Document features in your README and crate-level docs +**Лучшие практики**: +- Держите фичи в `default` минимальными: пользователи сами включат то, что им нужно +- Используйте синтаксис `dep:` (Rust 1.60+) для необязательных зависимостей, чтобы не создавать неявные фичи +- Документируйте фичи в README и в документации крейта -### Workspace Organization +### Организация воркспейса -For large projects, use a Cargo workspace to share dependencies and build artifacts: +Для больших проектов используйте воркспейс Cargo, чтобы разделять зависимости и артефакты сборки: ```toml -``` - -# Root Cargo.toml +# Корневой Cargo.toml [workspace] members = [ - "core", # Shared types and traits - "parser", # Parsing library - "server", # Binary — the main application - "client", # Client library - "cli", # CLI binary + "core", # Общие типы и трейты + "parser", # Библиотека разбора + "server", # Бинарный файл: основное приложение + "client", # Клиентская библиотека + "cli", # Бинарный файл CLI ] -# Shared dependency versions: +# Общие версии зависимостей: [workspace.dependencies] serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } tracing = "0.1" -# In each member's Cargo.toml: +# В Cargo.toml каждого участника: # [dependencies] # serde = { workspace = true } - -```rust - -**Benefits**: ``` -- Single `Cargo.lock` — all crates use the same dependency versions -- `cargo test --workspace` runs all tests -- Shared build cache — compiling one crate benefits all -- Clean dependency boundaries between components +**Преимущества**: +- Один `Cargo.lock`: все крейты используют одни и те же версии зависимостей +- `cargo test --workspace` запускает все тесты +- Общий кэш сборки: компиляция одного крейта ускоряет остальные +- Чистые границы зависимостей между компонентами -### `.cargo/config.toml`: Project-Level Configuration +### `.cargo/config.toml`: конфигурация на уровне проекта -The `.cargo/config.toml` file (at the workspace root or in `$HOME/.cargo/`) -customizes Cargo behavior without modifying `Cargo.toml`: +Файл `.cargo/config.toml` (в корне воркспейса или в `$HOME/.cargo/`) настраивает поведение Cargo без изменения `Cargo.toml`: ```toml -``` - # .cargo/config.toml -# Default target for this workspace +# Цель сборки по умолчанию для этого воркспейса [build] target = "x86_64-unknown-linux-gnu" -# Custom runner — e.g., run via QEMU for cross-compiled binaries +# Пользовательский runner: например, запуск через QEMU для кросс-компилированных бинарников [target.aarch64-unknown-linux-gnu] runner = "qemu-aarch64-static" linker = "aarch64-linux-gnu-gcc" -# Cargo aliases — custom shortcut commands +# Псевдонимы Cargo: пользовательские короткие команды [alias] -xt = "test --workspace --release" # cargo xt = run all tests in release -ci = "clippy --workspace -- -D warnings" # cargo ci = lint with errors on warnings -cov = "llvm-cov --workspace" # cargo cov = coverage (requires cargo-llvm-cov) +xt = "test --workspace --release" # cargo xt = запустить все тесты в release +ci = "clippy --workspace -- -D warnings" # cargo ci = линтинг с ошибками на предупреждениях +cov = "llvm-cov --workspace" # cargo cov = покрытие (требует cargo-llvm-cov) -# Environment variables for build scripts +# Переменные окружения для build-скриптов [env] IPMI_LIB_PATH = "/usr/lib/bmc" -# Use a custom registry (for internal packages) +# Использовать пользовательский реестр (для внутренних пакетов) # [registries.internal] # index = "https://gitlab.internal/crates/index" - -```rust - -Common configuration patterns: - ``` -| Setting | Purpose | Example | -|---------|---------|---------| -| `[build] target` | Default compilation target | `x86_64-unknown-linux-musl` for static builds | -| `[target.X] runner` | How to run the binary | `"qemu-aarch64-static"` for cross-compiled | -| `[target.X] linker` | Which linker to use | `"aarch64-linux-gnu-gcc"` | -| `[alias]` | Custom `cargo` subcommands | `xt = "test --workspace"` | -| `[env]` | Build-time environment variables | Library paths, feature toggles | -| `[net] offline` | Prevent network access | `true` for air-gapped builds | +Типичные настройки: + +| Настройка | Назначение | Пример | +|-----------|------------|--------| +| `[build] target` | Цель компиляции по умолчанию | `x86_64-unknown-linux-musl` для статических сборок | +| `[target.X] runner` | Как запускать бинарный файл | `"qemu-aarch64-static"` для кросс-компилированных | +| `[target.X] linker` | Какой линкер использовать | `"aarch64-linux-gnu-gcc"` | +| `[alias]` | Пользовательские подкоманды `cargo` | `xt = "test --workspace"` | +| `[env]` | Переменные окружения на этапе сборки | Пути к библиотекам, переключатели фич | +| `[net] offline` | Запрет доступа к сети | `true` для сборок в изолированной среде | -### Compile-Time Environment Variables: `env!()` and `option_env!()` +### Переменные окружения на этапе компиляции: `env!()` и `option_env!()` -Rust can embed environment variables into the binary at compile time — useful for -version strings, build metadata, and configuration: +Rust может встраивать переменные окружения в бинарный файл на этапе компиляции. Это полезно для строк версий, метаданных сборки и конфигурации: ```rust -// env!() — panics at compile time if the variable is missing -const VERSION: &str = env!("CARGO_PKG_VERSION"); // "0.1.0" from Cargo.toml -const PKG_NAME: &str = env!("CARGO_PKG_NAME"); // Crate name from Cargo.toml +// env!() паникует на этапе компиляции, если переменной нет +const VERSION: &str = env!("CARGO_PKG_VERSION"); // "0.1.0" из Cargo.toml +const PKG_NAME: &str = env!("CARGO_PKG_NAME"); // Имя крейта из Cargo.toml -// option_env!() — returns Option<&str>, doesn't panic if missing +// option_env!() возвращает Option<&str> и не паникует, если переменной нет const BUILD_SHA: Option<&str> = option_env!("GIT_SHA"); const BUILD_TIME: Option<&str> = option_env!("BUILD_TIMESTAMP"); fn print_version() { println!("{PKG_NAME} v{VERSION}"); if let Some(sha) = BUILD_SHA { - println!(" commit: {sha}"); + println!(" коммит: {sha}"); } if let Some(time) = BUILD_TIME { - println!(" built: {time}"); + println!(" собран: {time}"); } } ``` -Cargo automatically sets many useful environment variables: +Cargo автоматически задаёт множество полезных переменных окружения: -| Variable | Value | Use case | -|----------|-------|----------| -| `CARGO_PKG_VERSION` | `"1.2.3"` | Version reporting | -| `CARGO_PKG_NAME` | `"diag_tool"` | Binary identification | -| `CARGO_PKG_AUTHORS` | From `Cargo.toml` | About/help text | -| `CARGO_MANIFEST_DIR` | Absolute path to `Cargo.toml` | Locating test data files | -| `OUT_DIR` | Build output directory | `build.rs` code generation target | -| `TARGET` | Target triple | Platform-specific logic in `build.rs` | +| Переменная | Значение | Сценарий использования | +|------------|----------|------------------------| +| `CARGO_PKG_VERSION` | `"1.2.3"` | Вывод версии | +| `CARGO_PKG_NAME` | `"diag_tool"` | Идентификация бинарного файла | +| `CARGO_PKG_AUTHORS` | Из `Cargo.toml` | Текст «о программе» и справки | +| `CARGO_MANIFEST_DIR` | Абсолютный путь к `Cargo.toml` | Поиск файлов тестовых данных | +| `OUT_DIR` | Каталог выходных данных сборки | Цель для генерации кода в `build.rs` | +| `TARGET` | Target triple | Логика, специфичная для платформы, в `build.rs` | + +Свои переменные окружения можно задать из `build.rs`: -You can set custom env vars from `build.rs`: ```rust // build.rs fn main() { @@ -726,13 +694,12 @@ fn main() { } ``` -### `cfg_attr`: Conditional Attributes +### `cfg_attr`: условные атрибуты -`cfg_attr` applies an attribute **only when** a condition is true. This is more -targeted than `#[cfg()]`, which includes/excludes entire items: +`cfg_attr` применяет атрибут **только тогда**, когда условие истинно. Это точечнее, чем `#[cfg()]`, который включает или исключает целые элементы: ```rust -// Derive Serialize only when the "serde" feature is enabled: +// Derive Serialize только когда включена фича "serde": #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone)] pub struct DiagResult { @@ -740,86 +707,77 @@ pub struct DiagResult { pub passed: bool, pub message: String, } -// Without "serde" feature: no serde dependency needed at all -// With "serde" feature: DiagResult is serializable +// Без фичи "serde": зависимость от serde вообще не нужна +// С фичей "serde": DiagResult можно сериализовать -// Conditional attribute for testing: -#[cfg_attr(test, derive(PartialEq))] // Only derive PartialEq in test builds +// Условный атрибут для тестов: +#[cfg_attr(test, derive(PartialEq))] // Derive PartialEq только в тестовых сборках pub struct LargeStruct { /* ... */ } -// Platform-specific function attributes: +// Атрибуты функций, зависящие от платформы: #[cfg_attr(target_os = "linux", link_name = "ioctl")] #[cfg_attr(target_os = "freebsd", link_name = "__ioctl")] extern "C" fn platform_ioctl(fd: i32, request: u64) -> i32; ``` -| Pattern | What it does | -|---------|-------------| -| `#[cfg(feature = "x")]` | Include/exclude the entire item | -| `#[cfg_attr(feature = "x", derive(Foo))]` | Add `derive(Foo)` only when feature "x" is on | -| `#[cfg_attr(test, allow(unused))]` | Suppress warnings only in test builds | -| `#[cfg_attr(doc, doc = "...")]` | Documentation visible only in `cargo doc` | +| Паттерн | Что делает | +|---------|------------| +| `#[cfg(feature = "x")]` | Включает или исключает весь элемент | +| `#[cfg_attr(feature = "x", derive(Foo))]` | Добавляет `derive(Foo)` только при включённой фиче «x» | +| `#[cfg_attr(test, allow(unused))]` | Подавляет предупреждения только в тестовых сборках | +| `#[cfg_attr(doc, doc = "...")]` | Документация видна только в `cargo doc` | -### `cargo deny` and `cargo audit`: Supply-Chain Security +### `cargo deny` и `cargo audit`: безопасность цепочки поставок ```bash -``` - -# Install security audit tools +# Установка инструментов безопасности cargo install cargo-deny cargo install cargo-audit -# Check for known vulnerabilities in dependencies +# Проверка известных уязвимостей в зависимостях cargo audit -# Comprehensive checks: licenses, bans, advisories, sources +# Комплексные проверки: лицензии, запреты, уязвимости, источники cargo deny check - -```rust - -Configure `cargo deny` with a `deny.toml` at the workspace root: - ``` -```toml -``` +Настройте `cargo deny` с помощью `deny.toml` в корне воркспейса: +```toml # deny.toml [advisories] -vulnerability = "deny" # Fail on known vulnerabilities -unmaintained = "warn" # Warn on unmaintained crates +vulnerability = "deny" # Завершать с ошибкой при известных уязвимостях +unmaintained = "warn" # Предупреждать о неподдерживаемых крейтах [licenses] allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause"] -deny = ["GPL-3.0"] # Reject copyleft licenses +deny = ["GPL-3.0"] # Отвергать копилефт-лицензии [bans] -multiple-versions = "warn" # Warn if multiple versions of same crate +multiple-versions = "warn" # Предупреждать, если есть несколько версий одного крейта deny = [ - -```rust - { name = "openssl" }, # Force use of rustls instead + { name = "openssl" }, # Принудительно использовать rustls вместо неё ] [sources] -allow-git = [] # No git dependencies in production +allow-git = [] # Никаких git-зависимостей в продакшене ``` -| Tool | Purpose | When to run | -|------|---------|-------------| -| `cargo audit` | Check for known CVEs in dependencies | CI pipeline, pre-release | -| `cargo deny check` | Licenses, bans, advisories, sources | CI pipeline | -| `cargo deny check licenses` | License compliance only | Before open-sourcing | -| `cargo deny check bans` | Prevent specific crates | Enforce architecture decisions | +| Инструмент | Назначение | Когда запускать | +|------------|------------|-----------------| +| `cargo audit` | Проверка известных CVE в зависимостях | CI-конвейер, перед выпуском | +| `cargo deny check` | Лицензии, запреты, уязвимости, источники | CI-конвейер | +| `cargo deny check licenses` | Только соответствие лицензиям | Перед открытием исходного кода | +| `cargo deny check bans` | Запрет конкретных крейтов | Соблюдение архитектурных решений | -### Doc Tests: Tests Inside Documentation +### Doc-тесты: тесты внутри документации -Rust doc comments (`///`) can contain code blocks that are **compiled and run as tests**: +Комментарии документации Rust (`///`) могут содержать блоки кода, которые **компилируются и запускаются как тесты**: ```rust -/// Parses a diagnostic fault code from a string. +/// Разбирает код неисправности диагностики из строки. /// -/// # Examples +/// # Примеры /// /// ``` /// use my_crate::parse_fc; @@ -828,7 +786,7 @@ Rust doc comments (`///`) can contain code blocks that are **compiled and run as /// assert_eq!(fc, 12345); /// ``` /// -/// Invalid input returns an error: +/// Некорректный ввод возвращает ошибку: /// /// ``` /// use my_crate::parse_fc; @@ -844,20 +802,20 @@ pub fn parse_fc(input: &str) -> Result { ``` ```bash -cargo test --doc # Run only doc tests -cargo test # Runs unit + integration + doc tests +cargo test --doc # Запустить только doc-тесты +cargo test # Запускает модульные, интеграционные и doc-тесты ``` -**Module-level documentation** uses `//!` at the top of a file: +**Документация уровня модуля** пишется с помощью `//!` в начале файла: ```rust -//! # Diagnostic Framework +//! # Фреймворк диагностики //! -//! This crate provides the core diagnostic execution engine. -//! It supports running diagnostic tests, collecting results, -//! and reporting to the BMC via IPMI. +//! Этот крейт предоставляет ядро движка выполнения диагностики. +//! Он запускает диагностические тесты, собирает результаты +//! и передаёт их в BMC через IPMI. //! -//! ## Quick Start +//! ## Быстрый старт //! //! ```no_run //! use diag_framework::Framework; @@ -867,46 +825,41 @@ cargo test # Runs unit + integration + doc tests //! ``` ``` -### Benchmarking with Criterion +### Бенчмаркинг с criterion -> **Full coverage**: See the [Benchmarking with criterion](ch13-testing-and-benchmarking-patterns.md#benchmarking-with-criterion) -> section in Chapter 13 (Testing and Benchmarking Patterns) for complete -> `criterion` setup, API examples, and a comparison table vs `cargo bench`. -> Below is a quick-reference for architecture-specific usage. +> **Полное описание**: см. раздел [Бенчмаркинг с criterion](ch14-testing-and-benchmarking-patterns.md#бенчмаркинг-с-criterion) в главе 14 (Паттерны тестирования и бенчмаркинга). Там полная настройка `criterion`, примеры API и сравнительная таблица с `cargo bench`. Ниже приведена краткая справка для использования, специфичного для архитектуры. -When benchmarking your crate's public API, place benchmarks in `benches/` and -keep them focused on the hot path — typically parsers, serializers, or -validation boundaries: +При бенчмаркинге публичного API крейта помещайте бенчмарки в `benches/` и держите их сфокусированными на горячем пути: обычно это парсеры, сериализаторы или границы валидации: ```bash -cargo bench # Run all benchmarks -cargo bench -- parse_config # Run specific benchmark -# Results in target/criterion/ with HTML reports +cargo bench # Запустить все бенчмарки +cargo bench -- parse_config # Запустить конкретный бенчмарк +# Результаты в target/criterion/ с HTML-отчётами ``` -> **Key Takeaways — Architecture & API Design** -> - Accept the most general type (`impl Into`, `impl AsRef`, `Cow`); return the most specific -> - Parse Don't Validate: use `TryFrom` to create types that are valid by construction -> - `#[non_exhaustive]` on public enums prevents breaking changes when adding variants -> - `#[must_use]` catches silent discards of important values +> **Ключевые выводы: архитектура и дизайн API** +> - Принимайте самый общий тип (`impl Into`, `impl AsRef`, `Cow`), а возвращайте самый конкретный +> - Parse, don't validate: используйте `TryFrom`, чтобы создавать типы, корректные по построению +> - `#[non_exhaustive]` на публичных перечислениях предотвращает несовместимые изменения при добавлении вариантов +> - `#[must_use]` ловит молчаливое игнорирование важных значений -> **See also:** [Ch 9 — Error Handling](ch09-error-handling-patterns.md) for error type design in public APIs. [Ch 13 — Testing](ch13-testing-and-benchmarking-patterns.md) for testing your crate's public API. +> **См. также:** [гл. 10 — Обработка ошибок](ch10-error-handling-patterns.md) о проектировании типов ошибок в публичных API. [гл. 14 — Тестирование](ch14-testing-and-benchmarking-patterns.md) о тестировании публичного API крейта. --- -### Exercise: Crate API Refactoring ★★ (~30 min) +### Упражнение: рефакторинг API крейта ★★ (~30 минут) -Refactor the following "stringly-typed" API into one that uses `TryFrom`, newtypes, and builder pattern: +Перепишите следующий «строковый» API в вариант, который использует `TryFrom`, newtype и паттерн builder: ```rust,ignore -// BEFORE: Easy to misuse +// ДО: легко ошибиться fn create_server(host: &str, port: &str, max_conn: &str) -> Server { ... } ``` -Design a `ServerConfig` with validated types `Host`, `Port` (1–65535), and `MaxConnections` (1–10000) that reject invalid values at parse time. +Спроектируйте `ServerConfig` с проверенными типами `Host`, `Port` (1–65535) и `MaxConnections` (1–10000), которые отвергают некорректные значения на этапе разбора.
-🔑 Solution +🔑 Решение ```rust #[derive(Debug, Clone)] @@ -915,8 +868,8 @@ struct Host(String); impl TryFrom<&str> for Host { type Error = String; fn try_from(s: &str) -> Result { - if s.is_empty() { return Err("host cannot be empty".into()); } - if s.contains(' ') { return Err("host cannot contain spaces".into()); } + if s.is_empty() { return Err("имя хоста не может быть пустым".into()); } + if s.contains(' ') { return Err("имя хоста не может содержать пробелы".into()); } Ok(Host(s.to_string())) } } @@ -927,7 +880,7 @@ struct Port(u16); impl TryFrom for Port { type Error = String; fn try_from(p: u16) -> Result { - if p == 0 { return Err("port must be >= 1".into()); } + if p == 0 { return Err("порт должен быть >= 1".into()); } Ok(Port(p)) } } @@ -939,7 +892,7 @@ impl TryFrom for MaxConnections { type Error = String; fn try_from(n: u32) -> Result { if n == 0 || n > 10_000 { - return Err(format!("max_connections must be 1–10000, got {n}")); + return Err(format!("max_connections должен быть в диапазоне 1–10000, получено {n}")); } Ok(MaxConnections(n)) } @@ -966,7 +919,7 @@ fn main() { ); println!("{config:?}"); - // Invalid values caught at parse time: + // Некорректные значения отлавливаются на этапе разбора: assert!(Host::try_from("").is_err()); assert!(Port::try_from(0).is_err()); assert!(MaxConnections::try_from(99999).is_err()); @@ -976,4 +929,3 @@ fn main() {
*** - diff --git a/rust-patterns-book/src/ch16-asyncawait-essentials.md b/rust-patterns-book/src/ch16-asyncawait-essentials.md index 4667f8a..50d8c2b 100644 --- a/rust-patterns-book/src/ch16-asyncawait-essentials.md +++ b/rust-patterns-book/src/ch16-asyncawait-essentials.md @@ -1,47 +1,42 @@ -# 15. Async/Await Essentials 🔴 +# 16. Основы async/await 🔴 -> **What you'll learn:** -> - How Rust's `Future` trait differs from Go's goroutines and Python's asyncio -> - Tokio quick-start: spawning tasks, `join!`, and runtime configuration -> - Common async pitfalls and how to fix them -> - When to offload blocking work with `spawn_blocking` +> **Что вы узнаете:** +> - Чем трейт `Future` в Rust отличается от горутин Go и asyncio в Python +> - Быстрый старт с Tokio: запуск задач, `join!` и настройка рантайма +> - Распространённые ошибки в async-коде и способы их исправления +> - Когда выносить блокирующую работу в `spawn_blocking` -## Futures, Runtimes, and `async fn` +## Future, рантаймы и `async fn` -Rust's async model is *fundamentally different* from Go's goroutines or Python's `asyncio`. -Understanding three concepts is enough to get started: +Модель async в Rust *принципиально отличается* от горутин Go или `asyncio` в Python. Для начала достаточно понимать три концепции: -1. **A `Future` is a lazy state machine** — calling `async fn` doesn't execute anything; - it returns a `Future` that must be polled. -2. **You need a runtime** to poll futures — `tokio`, `async-std`, or `smol`. - The standard library defines `Future` but provides no runtime. -3. **`async fn` is sugar** — the compiler transforms it into a state machine that - implements `Future`. +1. **`Future` это ленивый конечный автомат**: вызов `async fn` ничего не выполняет, он возвращает `Future`, который нужно опросить (poll). +2. **Для опроса `Future` нужен рантайм**: `tokio`, `async-std` или `smol`. Стандартная библиотека определяет `Future`, но рантайма не предоставляет. +3. **`async fn` это синтаксический сахар**: компилятор преобразует её в конечный автомат, который реализует `Future`. ```rust -// A Future is just a trait: +// Future — это просто трейт: pub trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; } -// async fn desugars to: +// async fn раскрывается в: // fn fetch_data(url: &str) -> impl Future, Error>> async fn fetch_data(url: &str) -> Result, reqwest::Error> { - let response = reqwest::get(url).await?; // .await yields until ready + let response = reqwest::get(url).await?; // .await отдаёт управление, пока результат не готов let bytes = response.bytes().await?; Ok(bytes.to_vec()) } ``` -### Tokio Quick Start +### Быстрый старт с Tokio ```toml -``` - # Cargo.toml [dependencies] tokio = { version = "1", features = ["full"] } +``` ```rust,ignore use tokio::time::{sleep, Duration}; @@ -49,56 +44,52 @@ use tokio::task; #[tokio::main] async fn main() { - // Spawn concurrent tasks (like lightweight threads): + // Запускаем конкурентные задачи (как лёгкие потоки): let handle_a = task::spawn(async { sleep(Duration::from_millis(100)).await; - "task A done" + "задача A выполнена" }); let handle_b = task::spawn(async { sleep(Duration::from_millis(50)).await; - "task B done" + "задача B выполнена" }); - // .await both — they run concurrently, not sequentially: + // .await обе: они выполняются конкурентно, а не последовательно: let (a, b) = tokio::join!(handle_a, handle_b); println!("{}, {}", a.unwrap(), b.unwrap()); } ``` -### Async Common Pitfalls +### Распространённые ошибки в async -| Pitfall | Why It Happens | Fix | -|---------|---------------|-----| -| Blocking in async | `std::thread::sleep` or CPU work blocks the executor | Use `tokio::task::spawn_blocking` or `rayon` | -| `Send` bound errors | Future held across `.await` contains `!Send` type (e.g., `Rc`, `MutexGuard`) | Restructure to drop non-Send values before `.await` | -| Future not polled | Calling `async fn` without `.await` or spawning — nothing happens | Always `.await` or `tokio::spawn` the returned future | -| Holding `MutexGuard` across `.await` | `std::sync::MutexGuard` is `!Send`; async tasks may resume on different thread | Use `tokio::sync::Mutex` or drop the guard before `.await` | -| Accidental sequential execution | `let a = foo().await; let b = bar().await;` runs sequentially | Use `tokio::join!` or `tokio::spawn` for concurrency | +| Ошибка | Почему возникает | Решение | +|--------|------------------|---------| +| Блокировка в async | `std::thread::sleep` или вычисления блокируют исполнитель | Используйте `tokio::task::spawn_blocking` или `rayon` | +| Ошибки границы `Send` | Future удерживает через `.await` значение `!Send` (например, `Rc`, `MutexGuard`) | Перестройте код так, чтобы не-Send значения уничтожались до `.await` | +| Future не опрошен | Вызов `async fn` без `.await` или запуска: ничего не происходит | Всегда делайте `.await` или `tokio::spawn` для возвращённого future | +| Удержание `MutexGuard` через `.await` | `std::sync::MutexGuard` это `!Send`; async-задача может продолжиться в другом потоке | Используйте `tokio::sync::Mutex` или уничтожьте защитника до `.await` | +| Непреднамеренно последовательное выполнение | `let a = foo().await; let b = bar().await;` выполняется последовательно | Используйте `tokio::join!` или `tokio::spawn` для конкурентности | ```rust -// ❌ Blocking the async executor: +// ❌ Блокировка async-исполнителя: async fn bad() { - std::thread::sleep(std::time::Duration::from_secs(5)); // Blocks entire thread! + std::thread::sleep(std::time::Duration::from_secs(5)); // Блокирует весь поток! } -// ✅ Offload blocking work: +// ✅ Выносим блокирующую работу: async fn good() { tokio::task::spawn_blocking(|| { - std::thread::sleep(std::time::Duration::from_secs(5)); // Runs on blocking pool + std::thread::sleep(std::time::Duration::from_secs(5)); // Выполняется в пуле для блокирующих задач }).await.unwrap(); } ``` -> **Comprehensive async coverage**: For `Stream`, `select!`, cancellation safety, -> structured concurrency, and `tower` middleware, see our dedicated -> **Async Rust Training** guide. This section covers just enough to read and -> write basic async code. +> **Полное описание async**: о `Stream`, `select!`, безопасности отмены, структурированной конкурентности и middleware `tower` см. в нашем отдельном руководстве **Async Rust Training**. Этот раздел охватывает лишь то, что нужно для чтения и написания базового async-кода. -### Spawning and Structured Concurrency +### Порождение задач и структурированная конкурентность -Tokio's `spawn` creates a new asynchronous task — similar to `thread::spawn` but -much lighter: +`spawn` в Tokio создаёт новую асинхронную задачу: это похоже на `thread::spawn`, но намного легче: ```rust,ignore use tokio::task; @@ -106,23 +97,23 @@ use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { - // Spawn three concurrent tasks + // Запускаем три конкурентные задачи let h1 = task::spawn(async { sleep(Duration::from_millis(200)).await; - "fetched user profile" + "профиль пользователя получен" }); let h2 = task::spawn(async { sleep(Duration::from_millis(100)).await; - "fetched order history" + "история заказов получена" }); let h3 = task::spawn(async { sleep(Duration::from_millis(150)).await; - "fetched recommendations" + "рекомендации получены" }); - // Wait for all three concurrently (not sequentially!) + // Ждём все три конкурентно (не последовательно!) let (r1, r2, r3) = tokio::join!(h1, h2, h3); println!("{}", r1.unwrap()); println!("{}", r2.unwrap()); @@ -130,85 +121,77 @@ async fn main() { } ``` -**`join!` vs `try_join!` vs `select!`**: +**`join!` против `try_join!` против `select!`**: -| Macro | Behavior | Use when | -|-------|----------|----------| -| `join!` | Waits for ALL futures | All tasks must complete | -| `try_join!` | Waits for all, short-circuits on first `Err` | Tasks return `Result` | -| `select!` | Returns when FIRST future completes | Timeouts, cancellation | +| Макрос | Поведение | Когда использовать | +|--------|-----------|--------------------| +| `join!` | Ждёт ВСЕ future | Все задачи должны завершиться | +| `try_join!` | Ждёт все, прерывается на первом `Err` | Задачи возвращают `Result` | +| `select!` | Возвращает управление, когда ПЕРВЫЙ future завершился | Таймауты, отмена | ```rust,ignore use tokio::time::{timeout, Duration}; async fn fetch_with_timeout() -> Result> { let result = timeout(Duration::from_secs(5), async { - // Simulate slow network call + // Имитация медленного сетевого вызова tokio::time::sleep(Duration::from_millis(100)).await; Ok::<_, Box>("data".to_string()) - }).await??; // First ? unwraps Elapsed, second ? unwraps inner Result + }).await??; // Первый ? разворачивает Elapsed, второй ? разворачивает внутренний Result Ok(result) } ``` -### `Send` Bounds and Why Futures Must Be `Send` +### `Send` и почему future должны быть `Send` -When you `tokio::spawn` a future, it may resume on a different OS thread. -This means the future must be `Send`. Common pitfalls: +Когда вы делаете `tokio::spawn` для future, он может продолжиться в другом потоке ОС. Поэтому future должен быть `Send`. Распространённые ошибки: ```rust,ignore use std::rc::Rc; async fn not_send() { - let rc = Rc::new(42); // Rc is !Send + let rc = Rc::new(42); // Rc является !Send tokio::time::sleep(std::time::Duration::from_millis(10)).await; - println!("{}", rc); // rc is held across .await — future is !Send + println!("{}", rc); // rc удерживается через .await: future является !Send } -// Fix 1: Drop before .await +// Исправление 1: уничтожить до .await async fn fixed_drop() { let data = { let rc = Rc::new(42); - *rc // Copy the value out - }; // rc dropped here + *rc // Копируем значение наружу + }; // rc уничтожается здесь tokio::time::sleep(std::time::Duration::from_millis(10)).await; - println!("{}", data); // Just an i32, which is Send + println!("{}", data); // Просто i32, который является Send } -// Fix 2: Use Arc instead of Rc +// Исправление 2: использовать Arc вместо Rc async fn fixed_arc() { - let arc = std::sync::Arc::new(42); // Arc is Send + let arc = std::sync::Arc::new(42); // Arc является Send tokio::time::sleep(std::time::Duration::from_millis(10)).await; - println!("{}", arc); // ✅ Future is Send + println!("{}", arc); // ✅ Future является Send } ``` -> **Comprehensive async coverage**: For `Stream`, `select!`, cancellation safety, -> structured concurrency, and `tower` middleware, see our dedicated -> **Async Rust Training** guide. This section covers just enough to read and -> write basic async code. +> **Полное описание async**: о `Stream`, `select!`, безопасности отмены, структурированной конкурентности и middleware `tower` см. в нашем отдельном руководстве **Async Rust Training**. Этот раздел охватывает лишь то, что нужно для чтения и написания базового async-кода. -> **See also:** [Ch 5 — Channels](ch05-channels-and-message-passing.md) for synchronous channels. [Ch 6 — Concurrency](ch06-concurrency-vs-parallelism-vs-threads.md) for OS threads vs async tasks. +> **См. также:** [гл. 5 — Каналы](ch05-channels-and-message-passing.md) о синхронных каналах. [гл. 6 — Конкурентность](ch06-concurrency-vs-parallelism-vs-threads.md) о потоках ОС и async-задачах. -> **Key Takeaways — Async** -> - `async fn` returns a lazy `Future` — nothing runs until you `.await` or spawn it -> - Use `tokio::task::spawn_blocking` for CPU-heavy or blocking work inside async contexts -> - Don't hold `std::sync::MutexGuard` across `.await` — use `tokio::sync::Mutex` instead -> - Futures must be `Send` when spawned — drop `!Send` types before `.await` points +> **Ключевые выводы: async** +> - `async fn` возвращает ленивый `Future`: ничего не выполняется, пока вы не сделаете `.await` или не запустите его +> - Используйте `tokio::task::spawn_blocking` для тяжёлых вычислений или блокирующей работы внутри async-контекста +> - Не держите `std::sync::MutexGuard` через `.await`: используйте `tokio::sync::Mutex` +> - Future должны быть `Send` при запуске: уничтожайте `!Send`-значения до точек `.await` --- -### Exercise: Concurrent Fetcher with Timeout ★★ (~25 min) +### Упражнение: конкурентный загрузчик с таймаутом ★★ (~25 минут) -Write an async function `fetch_all` that spawns three `tokio::spawn` tasks, each -simulating a network call with `tokio::time::sleep`. Join all three with -`tokio::try_join!` wrapped in `tokio::time::timeout(Duration::from_secs(5), ...)`. -Return `Result, ...>` or an error if any task fails or the deadline -expires. +Напишите асинхронную функцию `fetch_all`, которая запускает три задачи `tokio::spawn`, каждая из которых имитирует сетевой вызов через `tokio::time::sleep`. Объедините все три через `tokio::try_join!`, обёрнутый в `tokio::time::timeout(Duration::from_secs(5), ...)`. Верните `Result, ...>` или ошибку, если какая-либо задача завершилась неудачно или истёк срок.
-🔑 Solution +🔑 Решение ```rust,ignore use tokio::time::{sleep, timeout, Duration}; @@ -244,4 +227,3 @@ async fn main() {
*** - diff --git a/rust-patterns-book/src/ch17-exercises.md b/rust-patterns-book/src/ch17-exercises.md index b53f7bc..000fa8a 100644 --- a/rust-patterns-book/src/ch17-exercises.md +++ b/rust-patterns-book/src/ch17-exercises.md @@ -1,11 +1,11 @@ -## Exercises +## Упражнения -### Exercise 1: Type-Safe State Machine ★★ (~30 min) +### Упражнение 1: типобезопасный автомат состояний ★★ (~30 минут) -Build a traffic light state machine using the type-state pattern. The light must transition `Red → Green → Yellow → Red` and no other order should be possible. +Постройте автомат состояний светофора с помощью паттерна type-state. Свет должен переходить `Red → Green → Yellow → Red`, и никакой другой порядок не должен быть возможен.
-🔑 Solution +🔑 Решение ```rust use std::marker::PhantomData; @@ -20,57 +20,57 @@ struct TrafficLight { impl TrafficLight { fn new() -> Self { - println!("🔴 Red — STOP"); + println!("🔴 Красный — СТОП"); TrafficLight { _state: PhantomData } } fn go(self) -> TrafficLight { - println!("🟢 Green — GO"); + println!("🟢 Зелёный — ВПЕРЁД"); TrafficLight { _state: PhantomData } } } impl TrafficLight { fn caution(self) -> TrafficLight { - println!("🟡 Yellow — CAUTION"); + println!("🟡 Жёлтый — ВНИМАНИЕ"); TrafficLight { _state: PhantomData } } } impl TrafficLight { fn stop(self) -> TrafficLight { - println!("🔴 Red — STOP"); + println!("🔴 Красный — СТОП"); TrafficLight { _state: PhantomData } } } fn main() { - let light = TrafficLight::new(); // Red - let light = light.go(); // Green - let light = light.caution(); // Yellow - let light = light.stop(); // Red + let light = TrafficLight::new(); // Красный + let light = light.go(); // Зелёный + let light = light.caution(); // Жёлтый + let light = light.stop(); // Красный - // light.caution(); // ❌ Compile error: no method `caution` on Red - // TrafficLight::new().stop(); // ❌ Compile error: no method `stop` on Red + // light.caution(); // ❌ Ошибка компиляции: нет метода `caution` для Red + // TrafficLight::new().stop(); // ❌ Ошибка компиляции: нет метода `stop` для Red } ``` -**Key takeaway**: Invalid transitions are compile errors, not runtime panics. +**Ключевой вывод**: недопустимые переходы это ошибки компиляции, а не паники во время выполнения.
--- -### Exercise 2: Unit-of-Measure with PhantomData ★★ (~30 min) +### Упражнение 2: единицы измерения с PhantomData ★★ (~30 минут) -Extend the unit-of-measure pattern from Ch4 to support: +Расширьте паттерн единиц измерения из гл. 4 так, чтобы он поддерживал: - `Meters`, `Seconds`, `Kilograms` -- Addition of same units -- Multiplication: `Meters * Meters = SquareMeters` -- Division: `Meters / Seconds = MetersPerSecond` +- сложение одинаковых единиц +- умножение: `Meters * Meters = SquareMeters` +- деление: `Meters / Seconds = MetersPerSecond`
-🔑 Solution +🔑 Решение ```rust use std::marker::PhantomData; @@ -120,17 +120,17 @@ fn main() { let width = Qty::::new(5.0); let height = Qty::::new(3.0); let area = width * height; // Qty - println!("Area: {:.1} m²", area.value); + println!("Площадь: {:.1} м²", area.value); let dist = Qty::::new(100.0); let time = Qty::::new(9.58); let speed = dist / time; - println!("Speed: {:.2} m/s", speed.value); + println!("Скорость: {:.2} м/с", speed.value); - let sum = width + height; // Same unit ✅ - println!("Sum: {:.1} m", sum.value); + let sum = width + height; // Одинаковые единицы ✅ + println!("Сумма: {:.1} м", sum.value); - // let bad = width + time; // ❌ Compile error: can't add Meters + Seconds + // let bad = width + time; // ❌ Ошибка компиляции: нельзя сложить Meters и Seconds } ``` @@ -138,15 +138,15 @@ fn main() { --- -### Exercise 3: Channel-Based Worker Pool ★★★ (~45 min) +### Упражнение 3: пул воркеров на каналах ★★★ (~45 минут) -Build a worker pool using channels where: -- A dispatcher sends `Job` structs through a channel -- N workers consume jobs and send results back -- Use `crossbeam-channel` (or `std::sync::mpsc` if crossbeam is unavailable) +Постройте пул воркеров на каналах, где: +- диспетчер отправляет структуры `Job` через канал +- N воркеров забирают задачи и отправляют результаты обратно +- используйте `crossbeam-channel` (или `std::sync::mpsc`, если crossbeam недоступен)
-🔑 Solution +🔑 Решение ```rust use std::sync::mpsc; @@ -167,45 +167,45 @@ fn worker_pool(jobs: Vec, num_workers: usize) -> Vec { let (job_tx, job_rx) = mpsc::channel::(); let (result_tx, result_rx) = mpsc::channel::(); - // Wrap receiver in Arc for sharing among workers + // Оборачиваем получатель в Arc для совместного использования воркерами let job_rx = std::sync::Arc::new(std::sync::Mutex::new(job_rx)); - // Spawn workers + // Запускаем воркеров let mut handles = Vec::new(); for worker_id in 0..num_workers { let job_rx = job_rx.clone(); let result_tx = result_tx.clone(); handles.push(thread::spawn(move || { loop { - // Lock, receive, unlock — short critical section + // Блокировка, получение, разблокировка: короткая критическая секция let job = { let rx = job_rx.lock().unwrap(); - rx.recv() // Blocks until a job or channel closes + rx.recv() // Блокирует, пока не придёт задача или канал не закроется }; match job { Ok(job) => { - let output = format!("processed '{}' by worker {worker_id}", job.data); + let output = format!("обработано '{}' воркером {worker_id}", job.data); result_tx.send(JobResult { job_id: job.id, output, worker_id, }).unwrap(); } - Err(_) => break, // Channel closed — exit + Err(_) => break, // Канал закрыт: выходим } } })); } - drop(result_tx); // Drop our copy so result channel closes when workers finish + drop(result_tx); // Отбрасываем нашу копию, чтобы канал результатов закрылся, когда воркеры завершат работу - // Dispatch jobs + // Раздаём задачи let num_jobs = jobs.len(); for job in jobs { job_tx.send(job).unwrap(); } - drop(job_tx); // Close the job channel — workers will exit after draining + drop(job_tx); // Закрываем канал задач: воркеры завершатся после обработки оставшихся - // Collect results + // Собираем результаты let mut results = Vec::new(); for result in result_rx { results.push(result); @@ -219,12 +219,12 @@ fn worker_pool(jobs: Vec, num_workers: usize) -> Vec { fn main() { let jobs: Vec = (0..20).map(|i| Job { id: i, - data: format!("task-{i}"), + data: format!("задача-{i}"), }).collect(); let results = worker_pool(jobs, 4); for r in &results { - println!("[worker {}] job {}: {}", r.worker_id, r.job_id, r.output); + println!("[воркер {}] задача {}: {}", r.worker_id, r.job_id, r.output); } } ``` @@ -233,12 +233,12 @@ fn main() { --- -### Exercise 4: Higher-Order Combinator Pipeline ★★ (~25 min) +### Упражнение 4: конвейер комбинаторов высшего порядка ★★ (~25 минут) -Create a `Pipeline` struct that chains transformations. It should support `.pipe(f)` to add a transformation and `.execute(input)` to run the full chain. +Создайте структуру `Pipeline`, которая связывает преобразования в цепочку. Она должна поддерживать `.pipe(f)` для добавления преобразования и `.execute(input)` для выполнения всей цепочки.
-🔑 Solution +🔑 Решение ```rust struct Pipeline { @@ -269,7 +269,7 @@ fn main() { println!("{result}"); // >>> HELLO WORLD <<< - // Numeric pipeline: + // Числовой конвейер: let result = Pipeline::new() .pipe(|x: i32| x * 2) .pipe(|x| x + 10) @@ -280,39 +280,39 @@ fn main() { } ``` -**Bonus**: Generic pipeline that changes type between stages would use a different design — each `.pipe()` returns a `Pipeline` with a different output type (this requires more advanced generic plumbing). +**Бонус**: обобщённый конвейер, который меняет тип между этапами, потребовал бы другой конструкции: каждый `.pipe()` возвращал бы `Pipeline` с другим выходным типом (это требует более продвинутой обобщённой «обвязки»).
--- -### Exercise 5: Error Hierarchy with thiserror ★★ (~30 min) +### Упражнение 5: иерархия ошибок на thiserror ★★ (~30 минут) -Design an error type hierarchy for a file-processing application that can fail during I/O, parsing (JSON and CSV), and validation. Use `thiserror` and demonstrate `?` propagation. +Спроектируйте иерархию типов ошибок для приложения обработки файлов, которое может завершиться ошибкой при вводе-выводе, разборе (JSON и CSV) или валидации. Используйте `thiserror` и продемонстрируйте распространение ошибок через `?`.
-🔑 Solution +🔑 Решение ```rust,ignore use thiserror::Error; #[derive(Error, Debug)] pub enum AppError { - #[error("I/O error: {0}")] + #[error("ошибка ввода-вывода: {0}")] Io(#[from] std::io::Error), - #[error("JSON parse error: {0}")] + #[error("ошибка разбора JSON: {0}")] Json(#[from] serde_json::Error), - #[error("CSV error at line {line}: {message}")] + #[error("ошибка CSV в строке {line}: {message}")] Csv { line: usize, message: String }, - #[error("validation error: {field} — {reason}")] + #[error("ошибка валидации: {field} — {reason}")] Validation { field: String, reason: String }, } fn read_file(path: &str) -> Result { - Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io via #[from] + Ok(std::fs::read_to_string(path)?) // io::Error → AppError::Io через #[from] } fn parse_json(content: &str) -> Result { @@ -324,13 +324,13 @@ fn validate_name(value: &serde_json::Value) -> Result { .and_then(|v| v.as_str()) .ok_or_else(|| AppError::Validation { field: "name".into(), - reason: "must be a non-null string".into(), + reason: "должно быть строкой, а не null".into(), })?; if name.is_empty() { return Err(AppError::Validation { field: "name".into(), - reason: "must not be empty".into(), + reason: "не должно быть пустым".into(), }); } @@ -346,8 +346,8 @@ fn process_file(path: &str) -> Result { fn main() { match process_file("config.json") { - Ok(name) => println!("Name: {name}"), - Err(e) => eprintln!("Error: {e}"), + Ok(name) => println!("Имя: {name}"), + Err(e) => eprintln!("Ошибка: {e}"), } } ``` @@ -356,12 +356,12 @@ fn main() { --- -### Exercise 6: Generic Trait with Associated Types ★★★ (~40 min) +### Упражнение 6: обобщённый трейт с ассоциированными типами ★★★ (~40 минут) -Design a `Repository` trait with associated `Error` and `Id` types. Implement it for an in-memory store and demonstrate compile-time type safety. +Спроектируйте трейт `Repository` с ассоциированными типами `Error` и `Id`. Реализуйте его для хранилища в памяти и продемонстрируйте безопасность типов на этапе компиляции.
-🔑 Solution +🔑 Решение ```rust use std::collections::HashMap; @@ -393,7 +393,7 @@ impl InMemoryUserRepo { } } -// Error type is Infallible — in-memory ops never fail +// Тип ошибки Infallible: операции в памяти никогда не падают impl Repository for InMemoryUserRepo { type Item = User; type Id = u64; @@ -415,16 +415,16 @@ impl Repository for InMemoryUserRepo { } } -// Generic function works with ANY repository: +// Обобщённая функция работает с ЛЮБЫМ репозиторием: fn create_and_fetch(repo: &mut R, item: R::Item) -> Result<(), R::Error> where R::Item: std::fmt::Debug, R::Id: std::fmt::Debug, { let id = repo.insert(item)?; - println!("Inserted with id: {id:?}"); + println!("Вставлено с id: {id:?}"); let retrieved = repo.get(&id)?; - println!("Retrieved: {retrieved:?}"); + println!("Получено: {retrieved:?}"); Ok(()) } @@ -441,20 +441,21 @@ fn main() { --- -### Exercise 7: Safe Wrapper around Unsafe (Ch11) ★★★ (~45 min) +### Упражнение 7: безопасная обёртка над unsafe (гл. 12) ★★★ (~45 минут) -Write a `FixedVec` — a fixed-capacity, stack-allocated vector. -Requirements: -- `push(&mut self, value: T) -> Result<(), T>` returns `Err(value)` when full -- `pop(&mut self) -> Option` returns and removes the last element -- `as_slice(&self) -> &[T]` borrows initialized elements -- All public methods must be safe; all unsafe must be encapsulated with `SAFETY:` comments -- `Drop` must clean up initialized elements +Напишите `FixedVec`: вектор фиксированной ёмкости, размещённый на стеке. -**Hint**: Use `MaybeUninit` and `[const { MaybeUninit::uninit() }; N]`. +Требования: +- `push(&mut self, value: T) -> Result<(), T>` возвращает `Err(value)`, когда вектор заполнен +- `pop(&mut self) -> Option` возвращает и удаляет последний элемент +- `as_slice(&self) -> &[T]` заимствует инициализированные элементы +- Все публичные методы должны быть безопасными; весь unsafe инкапсулирован с комментариями `SAFETY:` +- `Drop` должен очищать инициализированные элементы + +**Подсказка**: используйте `MaybeUninit` и `[const { MaybeUninit::uninit() }; N]`.
-🔑 Solution +🔑 Решение ```rust use std::mem::MaybeUninit; @@ -474,7 +475,7 @@ impl FixedVec { pub fn push(&mut self, value: T) -> Result<(), T> { if self.len >= N { return Err(value); } - // SAFETY: len < N, so data[len] is within bounds. + // SAFETY: len < N, поэтому data[len] находится в пределах. self.data[self.len] = MaybeUninit::new(value); self.len += 1; Ok(()) @@ -483,13 +484,13 @@ impl FixedVec { pub fn pop(&mut self) -> Option { if self.len == 0 { return None; } self.len -= 1; - // SAFETY: data[len] was initialized (len was > 0 before decrement). + // SAFETY: data[len] был инициализирован (len был > 0 до уменьшения). Some(unsafe { self.data[self.len].assume_init_read() }) } pub fn as_slice(&self) -> &[T] { - // SAFETY: data[0..len] are all initialized, and MaybeUninit - // has the same layout as T. + // SAFETY: data[0..len] инициализированы, и MaybeUninit + // имеет ту же раскладку, что и T. unsafe { std::slice::from_raw_parts(self.data.as_ptr() as *const T, self.len) } } @@ -499,7 +500,7 @@ impl FixedVec { impl Drop for FixedVec { fn drop(&mut self) { - // SAFETY: data[0..len] are initialized — drop each one. + // SAFETY: data[0..len] инициализированы: уничтожаем каждый. for i in 0..self.len { unsafe { self.data[i].assume_init_drop(); } } @@ -513,7 +514,7 @@ fn main() { assert_eq!(v.as_slice(), &["hello", "world"]); assert_eq!(v.pop(), Some("world".into())); assert_eq!(v.len(), 1); - // Drop cleans up remaining "hello" + // Drop очищает оставшийся элемент "hello" } ``` @@ -521,9 +522,9 @@ fn main() { --- -### Exercise 8: Declarative Macro — `map!` (Ch12) ★ (~15 min) +### Упражнение 8: декларативный макрос `map!` (гл. 13) ★ (~15 минут) -Write a `map!` macro that creates a `HashMap` from key-value pairs, similar to `vec![]`: +Напишите макрос `map!`, который создаёт `HashMap` из пар ключ-значение, аналогично `vec![]`: ```rust let m = map! { @@ -534,21 +535,21 @@ assert_eq!(m.get("host"), Some(&"localhost")); assert_eq!(m.len(), 2); ``` -Requirements: -- Support trailing comma -- Support empty invocation `map!{}` -- Work with any types that implement `Into` and `Into` for maximum flexibility +Требования: +- Поддержка завершающей запятой +- Поддержка пустого вызова `map!{}` +- Работа с любыми типами, которые реализуют `Into` и `Into`, для максимальной гибкости
-🔑 Solution +🔑 Решение ```rust macro_rules! map { - // Empty case + // Пустой случай () => { std::collections::HashMap::new() }; - // One or more key => value pairs (trailing comma optional) + // Одна или более пар ключ => значение (завершающая запятая необязательна) ( $( $key:expr => $val:expr ),+ $(,)? ) => {{ let mut m = std::collections::HashMap::new(); $( m.insert($key, $val); )+ @@ -557,7 +558,7 @@ macro_rules! map { } fn main() { - // Basic usage: + // Базовое использование: let config = map! { "host" => "localhost", "port" => "8080", @@ -566,11 +567,11 @@ fn main() { assert_eq!(config.len(), 3); assert_eq!(config["host"], "localhost"); - // Empty map: + // Пустая карта: let empty: std::collections::HashMap = map!(); assert!(empty.is_empty()); - // Different types: + // Разные типы: let scores = map! { 1 => 100, 2 => 200, @@ -583,12 +584,12 @@ fn main() { --- -### Exercise 9: Custom serde Deserialization (Ch10) ★★★ (~45 min) +### Упражнение 9: собственная десериализация serde (гл. 11) ★★★ (~45 минут) -Design a `Duration` wrapper that deserializes from human-readable strings like `"30s"`, `"5m"`, `"2h"` using a custom serde deserializer. The struct should also serialize back to the same format. +Спроектируйте обёртку `Duration`, которая десериализуется из читаемых человеком строк вроде `"30s"`, `"5m"`, `"2h"` с помощью собственного десериализатора serde. Структура также должна сериализоваться обратно в тот же формат.
-🔑 Solution +🔑 Решение ```rust,ignore use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -600,20 +601,20 @@ struct HumanDuration(std::time::Duration); impl HumanDuration { fn from_str(s: &str) -> Result { let s = s.trim(); - if s.is_empty() { return Err("empty duration string".into()); } + if s.is_empty() { return Err("пустая строка длительности".into()); } let (num_str, suffix) = s.split_at( s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len()) ); let value: u64 = num_str.parse() - .map_err(|_| format!("invalid number: {num_str}"))?; + .map_err(|_| format!("некорректное число: {num_str}"))?; let duration = match suffix { "s" | "sec" => std::time::Duration::from_secs(value), "m" | "min" => std::time::Duration::from_secs(value * 60), "h" | "hr" => std::time::Duration::from_secs(value * 3600), "ms" => std::time::Duration::from_millis(value), - other => return Err(format!("unknown suffix: {other}")), + other => return Err(format!("неизвестный суффикс: {other}")), }; Ok(HumanDuration(duration)) } @@ -660,38 +661,31 @@ fn main() { assert_eq!(config.timeout.0, std::time::Duration::from_secs(30)); assert_eq!(config.retry_interval.0, std::time::Duration::from_secs(300)); - // Round-trips correctly: + // Корректно выполняется туда и обратно: let serialized = serde_json::to_string(&config).unwrap(); assert!(serialized.contains("30s")); assert!(serialized.contains("5m")); - println!("Config: {serialized}"); + println!("Конфигурация: {serialized}"); } ```
-### Exercise 10 — Concurrent Fetcher with Timeout ★★ (~25 min) +### Упражнение 10: конкурентный загрузчик с таймаутом ★★ (~25 минут) -Write an async function `fetch_all` that spawns three `tokio::spawn` tasks, each -simulating a network call with `tokio::time::sleep`. Join all three with -`tokio::try_join!` wrapped in `tokio::time::timeout(Duration::from_secs(5), ...)`. -Return `Result, ...>` or an error if any task fails or the deadline -expires. +Напишите асинхронную функцию `fetch_all`, которая запускает три задачи `tokio::spawn`, каждая из которых имитирует сетевой вызов через `tokio::time::sleep`. Объедините все три через `tokio::try_join!`, обёрнутый в `tokio::time::timeout(Duration::from_secs(5), ...)`. Верните `Result, ...>` или ошибку, если какая-либо задача завершилась неудачно или истёк срок. -**Learning goals**: `tokio::spawn`, `try_join!`, `timeout`, error propagation -across task boundaries. +**Цели обучения**: `tokio::spawn`, `try_join!`, `timeout`, распространение ошибок через границы задач.
-Hint +Подсказка -Each spawned task returns `Result`. `try_join!` unwraps all three. -Wrap the whole `try_join!` in `timeout()` — the `Elapsed` error means you hit the -deadline. +Каждая запущенная задача возвращает `Result`. `try_join!` разворачивает все три. Оберните весь `try_join!` в `timeout()`: ошибка `Elapsed` означает, что вы достигли дедлайна.
-Solution +Решение ```rust,ignore use tokio::time::{sleep, timeout, Duration}; @@ -710,9 +704,9 @@ async fn fetch_all() -> Result, Box> { let h3 = tokio::spawn(fake_fetch("svc-c", 150)); tokio::try_join!(h1, h2, h3) }) - .await??; // first ? = timeout, second ? = join + .await??; // первый ? = таймаут, второй ? = join - Ok(vec![a?, b?, c?]) // unwrap inner Results + Ok(vec![a?, b?, c?]) // разворачиваем внутренние Result } #[tokio::main] @@ -726,48 +720,46 @@ async fn main() {
-### Exercise 11 — Async Channel Pipeline ★★★ (~40 min) +### Упражнение 11: конвейер async-каналов ★★★ (~40 минут) -Build a producer → transformer → consumer pipeline using `tokio::sync::mpsc`: +Постройте конвейер «производитель → преобразователь → потребитель» с помощью `tokio::sync::mpsc`: -1. **Producer**: sends integers 1..=20 into channel A (capacity 4). -2. **Transformer**: reads from channel A, squares each value, sends into channel B. -3. **Consumer**: reads from channel B, collects into a `Vec`, returns it. +1. **Производитель**: отправляет целые числа 1..=20 в канал A (ёмкость 4). +2. **Преобразователь**: читает из канала A, возводит каждое значение в квадрат и отправляет в канал B. +3. **Потребитель**: читает из канала B, собирает значения в `Vec` и возвращает его. -All three stages run as concurrent `tokio::spawn` tasks. Use bounded channels to -demonstrate back-pressure. Assert the final vec equals `[1, 4, 9, ..., 400]`. +Все три этапа работают как конкурентные задачи `tokio::spawn`. Используйте ограниченные каналы, чтобы продемонстрировать обратное давление. Проверьте, что итоговый вектор равен `[1, 4, 9, ..., 400]`. -**Learning goals**: `mpsc::channel`, bounded back-pressure, `tokio::spawn` with -move closures, graceful shutdown via channel close. +**Цели обучения**: `mpsc::channel`, ограниченное обратное давление, `tokio::spawn` с замыканиями `move`, корректное завершение через закрытие канала.
-Solution +Решение ```rust,ignore use tokio::sync::mpsc; #[tokio::main] async fn main() { - let (tx_a, mut rx_a) = mpsc::channel::(4); // bounded — back-pressure + let (tx_a, mut rx_a) = mpsc::channel::(4); // ограниченный: обратное давление let (tx_b, mut rx_b) = mpsc::channel::(4); - // Producer + // Производитель let producer = tokio::spawn(async move { for i in 1..=20u64 { tx_a.send(i).await.unwrap(); } - // tx_a dropped here → channel A closes + // tx_a уничтожается здесь → канал A закрывается }); - // Transformer + // Преобразователь let transformer = tokio::spawn(async move { while let Some(val) = rx_a.recv().await { tx_b.send(val * val).await.unwrap(); } - // tx_b dropped here → channel B closes + // tx_b уничтожается здесь → канал B закрывается }); - // Consumer + // Потребитель let consumer = tokio::spawn(async move { let mut results = Vec::new(); while let Some(val) = rx_b.recv().await { @@ -782,11 +774,10 @@ async fn main() { let expected: Vec = (1..=20).map(|x: u64| x * x).collect(); assert_eq!(results, expected); - println!("Pipeline complete: {results:?}"); + println!("Конвейер завершён: {results:?}"); } ```
*** - diff --git a/rust-patterns-book/src/ch18-summary-and-reference-card.md b/rust-patterns-book/src/ch18-summary-and-reference-card.md index 0e2723f..1efb1f2 100644 --- a/rust-patterns-book/src/ch18-summary-and-reference-card.md +++ b/rust-patterns-book/src/ch18-summary-and-reference-card.md @@ -1,130 +1,129 @@ -## Quick Reference Card +## Справочная карточка -### Pattern Decision Guide +### Руководство по выбору паттерна ```text -Need type safety for primitives? -└── Newtype pattern (Ch3) +Нужна безопасность типов для примитивов? +└── Паттерн newtype (гл. 3) -Need compile-time state enforcement? -└── Type-state pattern (Ch3) +Нужно обеспечить состояния на этапе компиляции? +└── Паттерн type-state (гл. 3) -Need a "tag" with no runtime data? -└── PhantomData (Ch4) +Нужен «тег» без данных во время выполнения? +└── PhantomData (гл. 4) -Need to break Rc/Arc reference cycles? -└── Weak / sync::Weak (Ch8) +Нужно разорвать циклы ссылок Rc/Arc? +└── Weak / sync::Weak (гл. 9) -Need to wait for a condition without busy-looping? -└── Condvar + Mutex (Ch6) +Нужно дождаться условия без активного опроса? +└── Condvar + Mutex (гл. 6) -Need to handle "one of N types"? -├── Known closed set → Enum -├── Open set, hot path → Generics -├── Open set, cold path → dyn Trait -└── Completely unknown types → Any + TypeId (Ch2) +Нужно обрабатывать «один из N типов»? +├── Известное закрытое множество → перечисление (enum) +├── Открытое множество, горячий путь → обобщения +├── Открытое множество, холодный путь → dyn Trait +└── Полностью неизвестные типы → Any + TypeId (гл. 2) -Need shared state across threads? -├── Simple counter/flag → Atomics -├── Short critical section → Mutex -├── Read-heavy → RwLock -├── Lazy one-time init → OnceLock / LazyLock (Ch6) -└── Complex state → Actor + Channels +Нужно разделяемое состояние между потоками? +├── Простой счётчик или флаг → атомарные типы +├── Короткая критическая секция → Mutex +├── Много чтений → RwLock +├── Ленивая однократная инициализация → OnceLock / LazyLock (гл. 6) +└── Сложное состояние → актор + каналы -Need to parallelize computation? -├── Collection processing → rayon::par_iter -├── Background task → thread::spawn -└── Borrow local data → thread::scope +Нужно распараллелить вычисления? +├── Обработка коллекций → rayon::par_iter +├── Фоновая задача → thread::spawn +└── Заимствование локальных данных → thread::scope -Need async I/O or concurrent networking? -├── Basic → tokio + async/await (Ch15) -└── Advanced (streams, middleware) → see Async Rust Training +Нужен асинхронный ввод-вывод или конкурентная работа с сетью? +├── Базовый уровень → tokio + async/await (гл. 16) +└── Продвинутый (потоки, middleware) → см. Async Rust Training -Need error handling? -├── Library → thiserror (#[derive(Error)]) -└── Application → anyhow (Result) +Нужна обработка ошибок? +├── Библиотека → thiserror (#[derive(Error)]) +└── Приложение → anyhow (Result) -Need to prevent a value from being moved? -└── Pin (Ch8) — required for Futures, self-referential types +Нужно запретить перемещение значения? +└── Pin (гл. 9): обязателен для Future и самоссылающихся типов ``` -### Trait Bounds Cheat Sheet - -| Bound | Meaning | -|-------|---------| -| `T: Clone` | Can be duplicated | -| `T: Send` | Can be moved to another thread | -| `T: Sync` | `&T` can be shared between threads | -| `T: 'static` | Contains no non-static references | -| `T: Sized` | Size known at compile time (default) | -| `T: ?Sized` | Size may not be known (`[T]`, `dyn Trait`) | -| `T: Unpin` | Safe to move after pinning | -| `T: Default` | Has a default value | -| `T: Into` | Can be converted to `U` | -| `T: AsRef` | Can be borrowed as `&U` | -| `T: Deref` | Auto-derefs to `&U` | -| `F: Fn(A) -> B` | Callable, borrows state immutably | -| `F: FnMut(A) -> B` | Callable, may mutate state | -| `F: FnOnce(A) -> B` | Callable exactly once, may consume state | - -### Lifetime Elision Rules - -The compiler inserts lifetimes automatically in three cases (so you don't have to): +### Шпаргалка по границам трейтов + +| Ограничение | Значение | +|-------------|----------| +| `T: Clone` | Можно дублировать | +| `T: Send` | Можно передать в другой поток | +| `T: Sync` | `&T` можно разделять между потоками | +| `T: 'static` | Не содержит заимствованных ссылок с ограниченным временем жизни | +| `T: Sized` | Размер известен на этапе компиляции (по умолчанию) | +| `T: ?Sized` | Размер может быть неизвестен (`[T]`, `dyn Trait`) | +| `T: Unpin` | Безопасно перемещать после закрепления (pin) | +| `T: Default` | Есть значение по умолчанию | +| `T: Into` | Можно преобразовать в `U` | +| `T: AsRef` | Можно заимствовать как `&U` | +| `T: Deref` | Автоматически разыменовывается в `&U` | +| `F: Fn(A) -> B` | Вызываемый, заимствует состояние неизменяемо | +| `F: FnMut(A) -> B` | Вызываемый, может изменять состояние | +| `F: FnOnce(A) -> B` | Вызываемый ровно один раз, может потребить состояние | + +### Правила elision времён жизни + +Компилятор автоматически вставляет времена жизни в трёх случаях, чтобы вам не приходилось делать это вручную: ```rust -// Rule 1: Each reference parameter gets its own lifetime +// Правило 1: каждый параметр-ссылка получает собственное время жизни // fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str) -// Rule 2: If there's exactly ONE input lifetime, it's used for all outputs +// Правило 2: если есть ровно ОДНО входное время жизни, оно используется для всех выходных // fn foo(x: &str) -> &str → fn foo<'a>(x: &'a str) -> &'a str -// Rule 3: If one parameter is &self or &mut self, its lifetime is used +// Правило 3: если один из параметров это &self или &mut self, используется его время жизни // fn foo(&self, x: &str) -> &str → fn foo<'a>(&'a self, x: &str) -> &'a str ``` -**When you MUST write explicit lifetimes**: -- Multiple input references and a reference output (compiler can't guess which input) -- Struct fields that hold references: `struct Ref<'a> { data: &'a str }` -- `'static` bounds when you need data without borrowed references +**Когда время жизни НУЖНО писать явно**: +- Несколько входных ссылок и ссылка на выходе (компилятор не может угадать, какой вход имеется в виду) +- Поля структур, которые хранят ссылки: `struct Ref<'a> { data: &'a str }` +- Границы `'static`, когда нужны данные без заимствованных ссылок -### Common Derive Traits +### Распространённые производные трейты ```rust #[derive( - Debug, // {:?} formatting + Debug, // Форматирование {:?} Clone, // .clone() - Copy, // Implicit copy (only for simple types) - PartialEq, Eq, // == comparison - PartialOrd, Ord, // < > comparison + sorting - Hash, // HashMap/HashSet key + Copy, // Неявное копирование (только для простых типов) + PartialEq, Eq, // Сравнение == + PartialOrd, Ord, // Сравнение < > и сортировка + Hash, // Ключ HashMap/HashSet Default, // Type::default() )] struct MyType { /* ... */ } ``` -### Module Visibility Quick Reference +### Краткая справка по видимости модулей ```text -pub → visible everywhere -pub(crate) → visible within the crate -pub(super) → visible to parent module -pub(in path) → visible within a specific path -(nothing) → private to current module + children +pub → видим везде +pub(crate) → видим внутри крейта +pub(super) → видим родительскому модулю +pub(in path) → видим внутри конкретного пути +(ничего) → приватен для текущего модуля и его потомков ``` -### Further Reading +### Дополнительная литература -| Resource | Why | -|----------|-----| -| [Rust Design Patterns](https://rust-unofficial.github.io/patterns/) | Catalog of idiomatic patterns and anti-patterns | -| [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) | Official checklist for polished public APIs | -| [Rust Atomics and Locks](https://marabos.nl/atomics/) | Mara Bos's deep dive into concurrency primitives | -| [The Rustonomicon](https://doc.rust-lang.org/nomicon/) | Official guide to unsafe Rust and dark corners | -| [Error Handling in Rust](https://blog.burntsushi.net/rust-error-handling/) | Andrew Gallant's comprehensive guide | -| [Jon Gjengset — Crust of Rust series](https://www.youtube.com/playlist?list=PLqbS7AVVErFiWDOAVrPt7aYmnuuOLYvOa) | Deep dives into iterators, lifetimes, channels, etc. | -| [Effective Rust](https://www.lurklurk.org/effective-rust/) | 35 specific ways to improve your Rust code | +| Ресурс | Почему стоит прочитать | +|--------|------------------------| +| [Rust Design Patterns](https://rust-unofficial.github.io/patterns/) | Каталог идиоматичных паттернов и антипаттернов | +| [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) | Официальный чек-лист для аккуратных публичных API | +| [Rust Atomics and Locks](https://marabos.nl/atomics/) | Глубокий разбор примитивов конкурентности от Mara Bos | +| [The Rustonomicon](https://doc.rust-lang.org/nomicon/) | Официальное руководство по unsafe Rust и тёмным углам языка | +| [Error Handling in Rust](https://blog.burntsushi.net/rust-error-handling/) | Подробное руководство Andrew Gallant | +| [Jon Gjengset — Crust of Rust series](https://www.youtube.com/playlist?list=PLqbS7AVVErFiWDOAVrPt7aYmnuuOLYvOa) | Глубокие разборы итераторов, времён жизни, каналов и др. | +| [Effective Rust](https://www.lurklurk.org/effective-rust/) | 35 конкретных способов улучшить ваш код на Rust | *** -*End of Rust Patterns & Engineering How-Tos* - +*Конец книги «Паттерны Rust и практические руководства по инженерии»* diff --git a/rust-patterns-book/src/ch19-capstone-project.md b/rust-patterns-book/src/ch19-capstone-project.md index a24840c..baeb918 100644 --- a/rust-patterns-book/src/ch19-capstone-project.md +++ b/rust-patterns-book/src/ch19-capstone-project.md @@ -1,59 +1,59 @@ -# Capstone Project: Type-Safe Task Scheduler +# Итоговый проект: планировщик задач с безопасностью типов -This project integrates patterns from across the book into a single, production-style system. You'll build a **type-safe, concurrent task scheduler** that uses generics, traits, typestate, channels, error handling, and testing. +Этот проект объединяет паттерны из всей книги в одну систему промышленного уровня. Вы построите **конкурентный планировщик задач с безопасностью типов**, который использует обобщённые типы, трейты, typestate, каналы, обработку ошибок и тестирование. -**Estimated time**: 4–6 hours | **Difficulty**: ★★★ +**Примерное время**: 4–6 часов | **Сложность**: ★★★ -> **What you'll practice:** -> - Generics and trait bounds (Ch 1–2) -> - Typestate pattern for task lifecycle (Ch 3) -> - PhantomData for zero-cost state markers (Ch 4) -> - Channels for worker communication (Ch 5) -> - Concurrency with scoped threads (Ch 6) -> - Error handling with `thiserror` (Ch 9) -> - Testing with property-based tests (Ch 13) -> - API design with `TryFrom` and validated types (Ch 14) +> **Что вы будете практиковать:** +> - Обобщённые типы и границы трейтов (гл. 1–2) +> - Паттерн typestate для жизненного цикла задачи (гл. 3) +> - PhantomData для маркеров состояний без затрат во время выполнения (гл. 4) +> - Каналы для взаимодействия с воркерами (гл. 5) +> - Конкурентность со scoped-потоками (гл. 6) +> - Обработка ошибок с `thiserror` (гл. 10) +> - Тестирование свойств (гл. 14) +> - Дизайн API с `TryFrom` и проверенными типами (гл. 15) -## The Problem +## Постановка задачи -Build a task scheduler where: +Постройте планировщик задач, в котором: -1. **Tasks** have a typed lifecycle: `Pending → Running → Completed` (or `Failed`) -2. **Workers** pull tasks from a channel, execute them, and report results -3. The **scheduler** manages task submission, worker coordination, and result collection -4. Invalid state transitions are **compile-time errors** +1. **Задачи** имеют типизированный жизненный цикл: `Pending → Running → Completed` (или `Failed`) +2. **Воркеры** берут задачи из канала, выполняют их и сообщают результаты +3. **Планировщик** управляет отправкой задач, координацией воркеров и сбором результатов +4. Недопустимые переходы состояний являются **ошибками компиляции** ```mermaid stateDiagram-v2 [*] --> Pending: scheduler.submit(task) - Pending --> Running: worker picks up task - Running --> Completed: task succeeds - Running --> Failed: task returns Err + Pending --> Running: воркер берёт задачу + Running --> Completed: задача успешна + Running --> Failed: задача вернула Err Completed --> [*]: scheduler.results() Failed --> [*]: scheduler.results() - Pending --> Pending: ❌ can't execute directly - Completed --> Running: ❌ can't re-run + Pending --> Pending: ❌ нельзя выполнить напрямую + Completed --> Running: ❌ нельзя запустить повторно ``` -## Step 1: Define the Task Types +## Шаг 1: определите типы задач -Start with the typestate markers and a generic `Task`: +Начните с маркеров typestate и обобщённого `Task`: ```rust use std::marker::PhantomData; -// --- State markers (zero-sized) --- +// --- Маркеры состояний (нулевого размера) --- struct Pending; struct Running; struct Completed; struct Failed; -// --- Task ID (newtype for type safety) --- +// --- Идентификатор задачи (newtype для безопасности типов) --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct TaskId(u64); -// --- The Task struct, parameterized by lifecycle state --- +// --- Структура Task, параметризованная состоянием жизненного цикла --- struct Task { id: TaskId, name: String, @@ -62,15 +62,15 @@ struct Task { } ``` -**Your job**: Implement state transitions so that: -- `Task` can transition to `Task` (via `start()`) -- `Task` can transition to `Task` or `Task` -- No other transitions compile +**Ваша задача**: реализуйте переходы состояний так, чтобы: +- `Task` переходит в `Task` (через `start()`) +- `Task` переходит в `Task` или `Task` +- Никакие другие переходы не должны компилироваться
-💡 Hint +💡 Подсказка -Each transition method should consume `self` and return the new state: +Каждый метод перехода должен поглощать `self` и возвращать новое состояние: ```rust impl Task { @@ -87,9 +87,9 @@ impl Task {
-## Step 2: Define the Work Function +## Шаг 2: определите рабочую функцию -Tasks need a function to execute. Use a boxed closure: +Задачам нужна функция для выполнения. Используйте замыкание в `Box`: ```rust struct WorkItem { @@ -99,35 +99,34 @@ struct WorkItem { } ``` -**Your job**: Implement `WorkItem::new()` that accepts a task name and closure. -Add a `TaskId` generator (simple atomic counter or mutex-protected counter). +**Ваша задача**: реализуйте `WorkItem::new()`, который принимает имя задачи и замыкание. Добавьте генератор `TaskId` (простой атомарный счётчик или счётчик под мьютексом). -## Step 3: Error Handling +## Шаг 3: обработка ошибок -Define the scheduler's error types using `thiserror`: +Определите типы ошибок планировщика с помощью `thiserror`: ```rust,ignore use thiserror::Error; #[derive(Error, Debug)] pub enum SchedulerError { - #[error("scheduler is shut down")] + #[error("планировщик остановлен")] ShutDown, - #[error("task {0:?} failed: {1}")] + #[error("задача {0:?} завершилась с ошибкой: {1}")] TaskFailed(TaskId, String), - #[error("channel send error")] + #[error("ошибка отправки в канал")] ChannelError(#[from] std::sync::mpsc::SendError<()>), - #[error("worker panicked")] + #[error("воркер завершился паникой")] WorkerPanic, } ``` -## Step 4: The Scheduler +## Шаг 4: планировщик -Build the scheduler using channels (Ch 5) and scoped threads (Ch 6): +Постройте планировщик с помощью каналов (гл. 5) и scoped-потоков (гл. 6): ```rust use std::sync::mpsc; @@ -145,13 +144,13 @@ struct TaskResult { } ``` -**Your job**: Implement: -- `Scheduler::new(num_workers: usize) -> Self` — creates channels and spawns workers +**Ваша задача**: реализуйте: +- `Scheduler::new(num_workers: usize) -> Self`: создаёт каналы и запускает воркеров - `Scheduler::submit(&self, item: WorkItem) -> Result` -- `Scheduler::shutdown(self) -> Vec>` — drops the sender, joins workers, collects results +- `Scheduler::shutdown(self) -> Vec>`: отбрасывает отправителя, дожидается воркеров и собирает результаты
-💡 Hint — Worker loop +💡 Подсказка: цикл воркера ```rust fn worker_loop( @@ -173,7 +172,7 @@ fn worker_loop( outcome, }); } - Err(_) => break, // Channel closed + Err(_) => break, // Канал закрыт } } } @@ -181,14 +180,14 @@ fn worker_loop(
-## Step 5: Integration Test +## Шаг 5: интеграционный тест -Write tests that verify: +Напишите тесты, которые проверяют: -1. **Happy path**: Submit 10 tasks, shut down, verify all 10 results are `Ok` -2. **Error handling**: Submit tasks that fail, verify `TaskResult.outcome` is `Err` -3. **Empty scheduler**: Create and immediately shut down — no panics -4. **Property test** (bonus): Use `proptest` to verify that for any N tasks (1..100), the scheduler always returns exactly N results +1. **Успешный сценарий**: отправьте 10 задач, остановите планировщик и убедитесь, что все 10 результатов имеют значение `Ok` +2. **Обработка ошибок**: отправьте задачи, которые завершаются ошибкой, и убедитесь, что `TaskResult.outcome` равен `Err` +3. **Пустой планировщик**: создайте и сразу остановите его, паник быть не должно +4. **Тест свойств** (бонус): используйте `proptest`, чтобы проверить, что для любого N задач (1..100) планировщик всегда возвращает ровно N результатов ```rust #[cfg(test)] @@ -201,8 +200,8 @@ mod tests { for i in 0..10 { let item = WorkItem::new( - format!("task-{i}"), - move || Ok(format!("result-{i}")), + format!("задача-{i}"), + move || Ok(format!("результат-{i}")), ); scheduler.submit(item).unwrap(); } @@ -218,8 +217,8 @@ mod tests { fn handles_failures() { let scheduler = Scheduler::::new(2); - scheduler.submit(WorkItem::new("good", || Ok("ok".into()))).unwrap(); - scheduler.submit(WorkItem::new("bad", || Err("boom".into()))).unwrap(); + scheduler.submit(WorkItem::new("удачная", || Ok("ок".into()))).unwrap(); + scheduler.submit(WorkItem::new("неудачная", || Err("сбой".into()))).unwrap(); let results = scheduler.shutdown(); assert_eq!(results.len(), 2); @@ -232,68 +231,68 @@ mod tests { } ``` -## Step 6: Put It All Together +## Шаг 6: соберём всё вместе -Here's the `main()` that demonstrates the full system: +Вот `main()`, который демонстрирует работу всей системы: ```rust,ignore fn main() { let scheduler = Scheduler::::new(4); - // Submit tasks with varying workloads + // Отправляем задачи с разной нагрузкой for i in 0..20 { let item = WorkItem::new( - format!("compute-{i}"), + format!("вычисление-{i}"), move || { - // Simulate work + // Имитация работы std::thread::sleep(std::time::Duration::from_millis(10)); if i % 7 == 0 { - Err(format!("task {i} hit a simulated error")) + Err(format!("задача {i}: смоделированная ошибка")) } else { - Ok(format!("task {i} completed with value {}", i * i)) + Ok(format!("задача {i} завершена, значение {}", i * i)) } }, ); scheduler.submit(item).unwrap(); } - println!("All tasks submitted. Shutting down..."); + println!("Все задачи отправлены. Останавливаем планировщик..."); let results = scheduler.shutdown(); let (ok, err): (Vec<_>, Vec<_>) = results.iter() .partition(|r| r.outcome.is_ok()); - println!("\n✅ Succeeded: {}", ok.len()); + println!("\n✅ Успешно: {}", ok.len()); for r in &ok { println!(" {} → {}", r.name, r.outcome.as_ref().unwrap()); } - println!("\n❌ Failed: {}", err.len()); + println!("\n❌ С ошибкой: {}", err.len()); for r in &err { println!(" {} → {}", r.name, r.outcome.as_ref().unwrap_err()); } } ``` -## Evaluation Criteria +## Критерии оценки -| Criterion | Target | -|-----------|--------| -| Type safety | Invalid state transitions don't compile | -| Concurrency | Workers run in parallel, no data races | -| Error handling | All failures captured in `TaskResult`, no panics | -| Testing | At least 3 tests; bonus for proptest | -| Code organization | Clean module structure, public API uses validated types | -| Documentation | Key types have doc comments explaining invariants | +| Критерий | Требование | +|----------|------------| +| Безопасность типов | Недопустимые переходы состояний не компилируются | +| Конкурентность | Воркеры работают параллельно, гонок данных нет | +| Обработка ошибок | Все сбои сохраняются в `TaskResult`, паник нет | +| Тестирование | Не менее 3 тестов; бонус за proptest | +| Организация кода | Понятная структура модулей, публичный API использует проверенные типы | +| Документация | У ключевых типов есть doc-комментарии с описанием инвариантов | -## Extension Ideas +## Идеи для развития -Once the basic scheduler works, try these enhancements: +Когда базовый планировщик заработает, попробуйте эти улучшения: -1. **Priority queue**: Add a `Priority` newtype (1–10) and process higher-priority tasks first -2. **Retry policy**: Failed tasks retry up to N times before being marked permanently failed -3. **Cancellation**: Add a `cancel(TaskId)` method that removes pending tasks -4. **Async version**: Port to `tokio::spawn` with `tokio::sync::mpsc` channels (Ch 15) -5. **Metrics**: Track per-worker task counts, average execution time, and failure rates +1. **Очередь с приоритетами**: добавьте newtype `Priority` (1–10) и сначала обрабатывайте задачи с более высоким приоритетом +2. **Политика повторов**: задача, завершившаяся ошибкой, повторяется до N раз, прежде чем получит статус окончательно проваленной +3. **Отмена**: добавьте метод `cancel(TaskId)`, который удаляет ожидающие задачи +4. **Асинхронная версия**: перенесите на `tokio::spawn` с каналами `tokio::sync::mpsc` (гл. 16) +5. **Метрики**: отслеживайте количество задач по каждому воркеру, среднее время выполнения и долю ошибок *** From 6a3f8cc754f370391fdb39e5e30f7c702215c519 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:00:42 +0400 Subject: [PATCH 03/18] Translate README content to Russian --- README.md | 106 +++++++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 95ee81b..d28de94 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,105 @@
-**License** This project is dual-licensed under the [MIT License](LICENSE) and [Creative Commons Attribution 4.0 International (CC-BY-4.0)](LICENSE-DOCS). +**Лицензия** Проект распространяется по двойной лицензии: [MIT License](LICENSE) и [Creative Commons Attribution 4.0 International (CC-BY-4.0)](LICENSE-DOCS).
-**Trademarks** This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. +**Товарные знаки** Проект может содержать товарные знаки или логотипы проектов, продуктов и сервисов. Использование товарных знаков и логотипов Microsoft допускается только в соответствии с [правилами использования товарных знаков и фирменного стиля Microsoft](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Использование товарных знаков или логотипов Microsoft в изменённых версиях проекта не должно вводить в заблуждение и создавать впечатление поддержки со стороны Microsoft. Использование сторонних товарных знаков и логотипов регулируется политиками их правообладателей.
-# Rust Training Books +> *Это перевод README на русский язык. Для юридических формулировок (лицензии и товарные знаки) приоритет имеет английская версия.* -Seven training courses covering Rust from different programming backgrounds, plus deep-dives on async, advanced patterns, and engineering practices. +# Учебные книги по Rust -This material combines original content with ideas and examples inspired by some of the best resources in the Rust ecosystem. The goal is to present an in-depth, technically accurate curriculum that weaves together knowledge scattered across books, blogs, conference talks, and video series into a cohesive, pedagogically structured experience. +Семь учебных курсов по Rust для разработчиков с разным опытом, а также углублённые материалы по async, продвинутым паттернам и инженерным практикам. -> **Disclaimer:** These books are training material, not an authoritative reference. While we strive for accuracy, always verify critical details against the [official Rust documentation](https://doc.rust-lang.org/) and the [Rust Reference](https://doc.rust-lang.org/reference/). +Материалы сочетают оригинальное содержание с идеями и примерами, вдохновлёнными лучшими ресурсами экосистемы Rust. Цель — подробная и технически точная программа, которая собирает знания, разбросанные по книгам, блогам, докладам и видеосериям, в цельный и педагогически выстроенный курс. -### Inspirations & Acknowledgments +> **Важно:** Эти книги — учебные материалы, а не авторитетный справочник. Мы стремимся к точности, но критически важные детали всегда сверяйте с [официальной документацией Rust](https://doc.rust-lang.org/) и [Rust Reference](https://doc.rust-lang.org/reference/). -- [**The Rust Programming Language**](https://doc.rust-lang.org/book/) — the foundation everything builds on -- [**Jon Gjengset**](https://www.youtube.com/c/JonGjengset) — deep-dive streams on advanced Rust internals, `Crust of Rust` series -- [**withoutboats**](https://without.boats/blog/) — async design, `Pin`, and the futures model -- [**fasterthanlime (Amos)**](https://fasterthanli.me/) — systems programming from first principles, engaging long-form explorations -- [**Mara Bos**](https://marabos.nl/) — *Rust Atomics and Locks*, concurrency primitives -- [**Aleksey Kladov (matklad)**](https://matklad.github.io/) — Rust analyzer insights, API design, error handling patterns -- [**Niko Matsakis**](https://smallcultfollowing.com/babysteps/) — language design, borrow checker internals, Polonius -- [**Rust by Example**](https://doc.rust-lang.org/rust-by-example/) and [**Rustonomicon**](https://doc.rust-lang.org/nomicon/) — practical patterns and unsafe deep-dives -- [**This Week in Rust**](https://this-week-in-rust.org/) — community discoveries that shaped many examples -- …and many others in the **Rust community at large** whose blog posts, conference talks, RFCs, and forum discussions have informed this material — too numerous to list individually, but deeply appreciated +### Источники вдохновения и благодарности -## 📖 Start Reading +- [**The Rust Programming Language**](https://doc.rust-lang.org/book/) — основа, на которой строится всё остальное +- [**Jon Gjengset**](https://www.youtube.com/c/JonGjengset) — глубокие стримы о внутреннем устройстве Rust, серия `Crust of Rust` +- [**withoutboats**](https://without.boats/blog/) — дизайн async, `Pin` и модель futures +- [**fasterthanlime (Amos)**](https://fasterthanli.me/) — системное программирование с нуля и подробные развёрнутые разборы +- [**Mara Bos**](https://marabos.nl/) — *Rust Atomics and Locks*, примитивы конкурентности +- [**Aleksey Kladov (matklad)**](https://matklad.github.io/) — идеи о rust-analyzer, дизайне API и обработке ошибок +- [**Niko Matsakis**](https://smallcultfollowing.com/babysteps/) — дизайн языка, внутреннее устройство borrow checker, Polonius +- [**Rust by Example**](https://doc.rust-lang.org/rust-by-example/) и [**Rustonomicon**](https://doc.rust-lang.org/nomicon/) — практические паттерны и углублённое изучение unsafe +- [**This Week in Rust**](https://this-week-in-rust.org/) — находки сообщества, которые повлияли на многие примеры +- …и многие другие участники **сообщества Rust**, чьи статьи, доклады, RFC и обсуждения на форумах легли в основу этих материалов. Их слишком много, чтобы перечислить поимённо, но мы искренне благодарны каждому -Pick the book that matches your background. Books are grouped by complexity so you can chart a learning path: +## 📖 С чего начать -| Level | Description | -|-------|-------------| -| 🟢 **Bridge** | Learn Rust coming from another language — start here | -| 🔵 **Deep Dive** | Focused exploration of a major Rust subsystem | -| 🟡 **Advanced** | Patterns and techniques for experienced Rustaceans | -| 🟣 **Expert** | Cutting-edge type-level and correctness techniques | -| 🟤 **Practices** | Engineering, tooling, and production readiness | +Выберите книгу, которая соответствует вашему опыту. Книги сгруппированы по сложности, чтобы вы могли построить путь обучения: -| Book | Level | Who it's for | -|------|-------|-------------| -| [**Rust for C/C++ Programmers**](c-cpp-book/src/SUMMARY.md) | 🟢 Bridge | Move semantics, RAII, FFI, embedded, no_std | -| [**Rust for C# Programmers**](csharp-book/src/SUMMARY.md) | 🟢 Bridge | Swift / C# / Java → ownership & type system | -| [**Rust for Python Programmers**](python-book/src/SUMMARY.md) | 🟢 Bridge | Dynamic → static typing, GIL-free concurrency | -| [**Async Rust**](async-book/src/SUMMARY.md) | 🔵 Deep Dive | Tokio, streams, cancellation safety | -| [**Rust Patterns**](rust-patterns-book/src/SUMMARY.md) | 🟡 Advanced | Pin, allocators, lock-free structures, unsafe | -| [**Type-Driven Correctness**](type-driven-correctness-book/src/SUMMARY.md) | 🟣 Expert | Type-state, phantom types, capability tokens | -| [**Rust Engineering Practices**](engineering-book/src/SUMMARY.md) | 🟤 Practices | Build scripts, cross-compilation, CI/CD, Miri | +| Уровень | Описание | +|---------|----------| +| 🟢 **Мост** | Изучение Rust для тех, кто пришёл из другого языка — начните отсюда | +| 🔵 **Погружение** | Углублённое изучение крупной подсистемы Rust | +| 🟡 **Продвинутый** | Паттерны и приёмы для опытных Rust-разработчиков | +| 🟣 **Эксперт** | Передовые техники на уровне типов и корректности | +| 🟤 **Практики** | Инженерия, инструменты и готовность к продакшну | -Each book has 15–16 chapters with Mermaid diagrams, editable Rust playgrounds, exercises, and full-text search. +| Книга | Уровень | Для кого | +|-------|---------|----------| +| [**Rust для программистов C/C++**](c-cpp-book/src/SUMMARY.md) | 🟢 Мост | Семантика перемещения, RAII, FFI, embedded, no_std | +| [**Rust для программистов C#**](csharp-book/src/SUMMARY.md) | 🟢 Мост | Swift / C# / Java → владение и система типов | +| [**Rust для программистов Python**](python-book/src/SUMMARY.md) | 🟢 Мост | От динамической типизации к статической, конкурентность без GIL | +| [**Async Rust**](async-book/src/SUMMARY.md) | 🔵 Погружение | Tokio, потоки (streams), отмена и безопасность при отмене | +| [**Паттерны Rust**](rust-patterns-book/src/SUMMARY.md) | 🟡 Продвинутый | Pin, аллокаторы, lock-free-структуры, unsafe | +| [**Корректность на уровне типов**](type-driven-correctness-book/src/SUMMARY.md) | 🟣 Эксперт | Typestate, phantom-типы, capability-токены | +| [**Инженерные практики Rust**](engineering-book/src/SUMMARY.md) | 🟤 Практики | Build scripts, кросс-компиляция, CI/CD, Miri | -> **Tip:** You can read the markdown source directly on GitHub, or browse the rendered site with sidebar navigation and search at the GitHub Pages site (link in the repo's About section). +В каждой книге 15–16 глав с диаграммами Mermaid, редактируемыми песочницами Rust, упражнениями и полнотекстовым поиском. + +> **Совет:** Исходный markdown можно читать прямо на GitHub или открыть сайт с навигацией по главам и поиском на GitHub Pages (ссылка указана в разделе About репозитория). > -> **Local serving:** For the best reading experience (keyboard navigation between chapters, instant search, offline access), clone the repo and run: +> **Локальный просмотр:** Для лучшего опыта чтения (навигация по главам с клавиатуры, мгновенный поиск, работа без интернета) склонируйте репозиторий и выполните: > ``` -> # Install Rust via rustup if you don't have it yet: +> # Если Rust ещё не установлен, поставьте его через rustup: > # https://rustup.rs/ > > cargo install mdbook mdbook-mermaid -> cargo xtask serve # builds all books and opens a local server +> cargo xtask serve # собирает все книги и открывает локальный сервер > ``` --- -## 🔧 For Maintainers +## 🔧 Для мейнтейнеров
-Building, serving, and editing the books locally +Сборка, просмотр и редактирование книг локально -### Prerequisites +### Требования -Install [Rust via **rustup**](https://rustup.rs/) if you haven't already, then: +Установите [Rust через **rustup**](https://rustup.rs/), если у вас его ещё нет, затем выполните: ```bash cargo install mdbook mdbook-mermaid ``` -### Build & serve +### Сборка и запуск ```bash -cargo xtask build # Build all books into site/ (local preview) -cargo xtask serve # Build and serve at http://localhost:3000 -cargo xtask deploy # Build all books into docs/ (for GitHub Pages) -cargo xtask clean # Remove site/ and docs/ +cargo xtask build # Собрать все книги в site/ (для локального просмотра) +cargo xtask serve # Собрать и запустить на http://localhost:3000 +cargo xtask deploy # Собрать все книги в docs/ (для GitHub Pages) +cargo xtask clean # Удалить site/ и docs/ ``` -To build or serve a single book: +Чтобы собрать или запустить одну книгу: ```bash cd c-cpp-book && mdbook serve --open # http://localhost:3000 ``` -### Deployment +### Публикация -The site auto-deploys to GitHub Pages on push to `master` via `.github/workflows/pages.yml`. No manual steps needed. +Сайт автоматически публикуется на GitHub Pages при push в `master` через `.github/workflows/pages.yml`. Никаких ручных действий не требуется.
From c78f94fafa81b3fe508eb52300e8ba35c8a5213e Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:26:29 +0400 Subject: [PATCH 04/18] Translate Engineering book intro, TOC and chapter 1 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings in engineering-book/src for SUMMARY.md, ch00 and ch01. Code identifiers, crate names, file names and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- engineering-book/src/SUMMARY.md | 38 +- engineering-book/src/ch00-introduction.md | 145 +++--- .../ch01-build-scripts-buildrs-in-depth.md | 433 +++++++++--------- 3 files changed, 311 insertions(+), 305 deletions(-) diff --git a/engineering-book/src/SUMMARY.md b/engineering-book/src/SUMMARY.md index 7918865..b4acabd 100644 --- a/engineering-book/src/SUMMARY.md +++ b/engineering-book/src/SUMMARY.md @@ -1,36 +1,36 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I — Build & Ship +# Часть I — Сборка и поставка -- [1. Build Scripts — build.rs in Depth](ch01-build-scripts-buildrs-in-depth.md) -- [2. Cross-Compilation — One Source, Many Targets](ch02-cross-compilation-one-source-many-target.md) +- [1. Build-скрипты — build.rs в деталях](ch01-build-scripts-buildrs-in-depth.md) +- [2. Кросс-компиляция — один исходник, много целей](ch02-cross-compilation-one-source-many-target.md) --- -# Part II — Measure & Verify +# Часть II — Измерение и верификация -- [3. Benchmarking — Measuring What Matters](ch03-benchmarking-measuring-what-matters.md) -- [4. Code Coverage — Seeing What Tests Miss](ch04-code-coverage-seeing-what-tests-miss.md) -- [5. Miri, Valgrind, and Sanitizers — Verifying Unsafe Code](ch05-miri-valgrind-and-sanitizers-verifying-u.md) +- [3. Бенчмаркинг — измеряем то, что важно](ch03-benchmarking-measuring-what-matters.md) +- [4. Покрытие кода — видим то, что пропускают тесты](ch04-code-coverage-seeing-what-tests-miss.md) +- [5. Miri, Valgrind и санитайзеры — проверка unsafe-кода](ch05-miri-valgrind-and-sanitizers-verifying-u.md) --- -# Part III — Harden & Optimize +# Часть III — Укрепление и оптимизация -- [6. Dependency Management and Supply Chain Security](ch06-dependency-management-and-supply-chain-s.md) -- [7. Release Profiles and Binary Size](ch07-release-profiles-and-binary-size.md) -- [8. Compile-Time and Developer Tools](ch08-compile-time-and-developer-tools.md) -- [9. `no_std` and Feature Verification](ch09-no-std-and-feature-verification.md) -- [10. Windows and Conditional Compilation](ch10-windows-and-conditional-compilation.md) +- [6. Управление зависимостями и безопасность цепочки поставок](ch06-dependency-management-and-supply-chain-s.md) +- [7. Профили релиза и размер бинарного файла](ch07-release-profiles-and-binary-size.md) +- [8. Инструменты времени компиляции и разработки](ch08-compile-time-and-developer-tools.md) +- [9. `no_std` и проверка фич](ch09-no-std-and-feature-verification.md) +- [10. Windows и условная компиляция](ch10-windows-and-conditional-compilation.md) --- -# Part IV — Integrate +# Часть IV — Интеграция -- [11. Putting It All Together — A Production CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) -- [12. Tricks from the Trenches](ch12-tricks-from-the-trenches.md) -- [13. Quick Reference Card](ch13-quick-reference-card.md) +- [11. Собираем всё вместе — продакшн-конвейер CI/CD](ch11-putting-it-all-together-a-production-cic.md) +- [12. Проверенные на практике приёмы](ch12-tricks-from-the-trenches.md) +- [13. Краткая справочная карточка](ch13-quick-reference-card.md) diff --git a/engineering-book/src/ch00-introduction.md b/engineering-book/src/ch00-introduction.md index 8fc63ec..ff4c1a1 100644 --- a/engineering-book/src/ch00-introduction.md +++ b/engineering-book/src/ch00-introduction.md @@ -1,126 +1,121 @@ -# Rust Engineering Practices — Beyond `cargo build` +# Инженерные практики Rust — за пределами `cargo build` -## Speaker Intro +## Об авторе -- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team -- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems -- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since +- Ведущий архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft +- Опытный инженер с экспертизой в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре CPU и платформ, а также системах на C++ +- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык --- -> A practical guide to the Rust toolchain features that most teams discover too late: -> build scripts, cross-compilation, benchmarking, code coverage, and safety verification -> with Miri and Valgrind. Each chapter uses concrete examples drawn from -> a real hardware-diagnostics codebase — -> a large multi-crate workspace — so every technique maps directly to production code. +> Практическое руководство по возможностям инструментария Rust, которые большинство команд открывают слишком поздно: build-скрипты, кросс-компиляция, бенчмаркинг, покрытие кода и проверка безопасности с помощью Miri и Valgrind. Каждая глава опирается на конкретные примеры из реального кода для диагностики оборудования — большого многокрейтового воркспейса, — поэтому каждый приём напрямую переносится в продакшн-код. -## How to Use This Book +## Как пользоваться этой книгой -This book is designed for **self-paced study or team workshops**. Each chapter is largely independent — read them in order or jump to the topic you need. +Эта книга рассчитана на **самостоятельное изучение или командные воркшопы**. Каждая глава в значительной степени независима: читайте их по порядку или сразу переходите к нужной теме. -### Difficulty Legend +### Обозначения сложности -| Symbol | Level | Meaning | -|:------:|-------|---------| -| 🟢 | Starter | Straightforward tools with clear patterns — useful on day one | -| 🟡 | Intermediate | Requires understanding of toolchain internals or platform concepts | -| 🔴 | Advanced | Deep toolchain knowledge, nightly features, or multi-tool orchestration | +| Символ | Уровень | Значение | +|:------:|---------|----------| +| 🟢 | Начальный | Простые инструменты с понятными шаблонами — полезны с первого дня | +| 🟡 | Средний | Требуют понимания внутреннего устройства инструментов или платформенных концепций | +| 🔴 | Продвинутый | Глубокое знание инструментов, nightly-функции или оркестрация нескольких инструментов | -### Pacing Guide +### План изучения -| Part | Chapters | Est. Time | Key Outcome | -|------|----------|:---------:|-------------| -| **I — Build & Ship** | ch01–02 | 3–4 h | Build metadata, cross-compilation, static binaries | -| **II — Measure & Verify** | ch03–05 | 4–5 h | Statistical benchmarking, coverage gates, Miri/sanitizers | -| **III — Harden & Optimize** | ch06–10 | 6–8 h | Supply chain security, release profiles, compile-time tools, `no_std`, Windows | -| **IV — Integrate** | ch11–13 | 3–4 h | Production CI/CD pipeline, tricks, capstone exercise | -| | | **16–21 h** | **Full production engineering pipeline** | +| Часть | Главы | Примерное время | Ключевой результат | +|-------|-------|:---------------:|--------------------| +| **I — Сборка и поставка** | ch01–02 | 3–4 ч | Метаданные сборки, кросс-компиляция, статические бинарные файлы | +| **II — Измерение и верификация** | ch03–05 | 4–5 ч | Статистический бенчмаркинг, пороги покрытия, Miri и санитайзеры | +| **III — Укрепление и оптимизация** | ch06–10 | 6–8 ч | Безопасность цепочки поставок, профили релиза, инструменты времени компиляции, `no_std`, Windows | +| **IV — Интеграция** | ch11–13 | 3–4 ч | Продакшн-конвейер CI/CD, приёмы, итоговое упражнение | +| | | **16–21 ч** | **Полный инженерный конвейер для продакшна** | -### Working Through Exercises +### Работа с упражнениями -Each chapter contains **🏋️ exercises** with difficulty indicators. Solutions are provided in expandable `
` blocks — try the exercise first, then check your work. +Каждая глава содержит **🏋️ упражнения** с указанием сложности. Решения приведены в раскрывающихся блоках `
` — сначала попробуйте решить упражнение самостоятельно, а затем сверьтесь с решением. -- 🟢 exercises can often be done in 10–15 minutes -- 🟡 exercises require 20–40 minutes and may involve running tools locally -- 🔴 exercises require significant setup and experimentation (1+ hour) +- Упражнения 🟢 обычно занимают 10–15 минут +- Упражнения 🟡 требуют 20–40 минут и могут включать запуск инструментов локально +- Упражнения 🔴 требуют серьёзной настройки и экспериментов (от одного часа) -## Prerequisites +## Предварительные требования -| Concept | Where to learn it | -|---------|-------------------| -| Cargo workspace layout | [Rust Book ch14.3](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) | -| Feature flags | [Cargo Reference — Features](https://doc.rust-lang.org/cargo/reference/features.html) | -| `#[cfg(test)]` and basic testing | Rust Patterns ch12 | -| `unsafe` blocks and FFI basics | Rust Patterns ch10 | +| Концепция | Где изучить | +|-----------|-------------| +| Структура воркспейса Cargo | [Книга по Rust, гл. 14.3](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) | +| Флаги функций (feature flags) | [Справочник Cargo — Features](https://doc.rust-lang.org/cargo/reference/features.html) | +| `#[cfg(test)]` и базовое тестирование | Паттерны Rust, гл. 12 | +| Блоки `unsafe` и основы FFI | Паттерны Rust, гл. 10 | -## Chapter Dependency Map +## Карта зависимостей между главами ```text ┌──────────┐ │ ch00 │ - │ Intro │ + │ Введение │ └────┬─────┘ ┌─────┬───┬──┴──┬──────┬──────┐ ▼ ▼ ▼ ▼ ▼ ▼ ch01 ch03 ch04 ch05 ch06 ch09 - Build Bench Cov Miri Deps no_std + Сборка Бенч Покр Miri Зав. no_std │ │ │ │ │ │ │ └────┴────┘ │ ▼ │ │ │ ch10 ▼ ▼ ▼ Windows ch02 ch07 ch07 │ - Cross RelProf RelProf │ + Кросс Профиль Профиль │ │ │ │ │ │ ▼ │ │ │ ch08 │ │ - │ CompTime │ │ + │ Компил │ │ └──────────┴───────────┴─────┘ │ ▼ ch11 - CI/CD Pipeline + CI/CD-конвейер │ ▼ ch12 ─── ch13 - Tricks Quick Ref + Приёмы Справка ``` -**Read in any order**: ch01, ch03, ch04, ch05, ch06, ch09 are independent. -**Read after prerequisites**: ch02 (needs ch01), ch07–ch08 (benefit from ch03–ch06), ch10 (benefits from ch09). -**Read last**: ch11 (ties everything together), ch12 (tricks), ch13 (reference). +**Читать в любом порядке**: ch01, ch03, ch04, ch05, ch06 и ch09 независимы друг от друга. +**Читать после необходимых предварительных глав**: ch02 (нужна ch01), ch07–ch08 (выигрывают от ch03–ch06), ch10 (выигрывает от ch09). +**Читать в последнюю очередь**: ch11 (связывает всё воедино), ch12 (приёмы), ch13 (справочник). -## Annotated Table of Contents +## Аннотированное оглавление -### Part I — Build & Ship +### Часть I — Сборка и поставка -| # | Chapter | Difficulty | Description | -|---|---------|:----------:|-------------| -| 1 | [Build Scripts — `build.rs` in Depth](ch01-build-scripts-buildrs-in-depth.md) | 🟢 | Compile-time constants, compiling C code, protobuf generation, system library linking, anti-patterns | -| 2 | [Cross-Compilation — One Source, Many Targets](ch02-cross-compilation-one-source-many-target.md) | 🟡 | Target triples, musl static binaries, ARM cross-compile, `cross` tool, `cargo-zigbuild`, GitHub Actions | +| № | Глава | Сложность | Описание | +|---|-------|:---------:|----------| +| 1 | [Build-скрипты — `build.rs` в деталях](ch01-build-scripts-buildrs-in-depth.md) | 🟢 | Константы времени компиляции, компиляция C-кода, генерация protobuf, линковка системных библиотек, антипаттерны | +| 2 | [Кросс-компиляция — один исходник, много целей](ch02-cross-compilation-one-source-many-target.md) | 🟡 | Target triple, статические бинарные файлы на musl, кросс-компиляция для ARM, инструмент `cross`, `cargo-zigbuild`, GitHub Actions | -### Part II — Measure & Verify +### Часть II — Измерение и верификация -| # | Chapter | Difficulty | Description | -|---|---------|:----------:|-------------| -| 3 | [Benchmarking — Measuring What Matters](ch03-benchmarking-measuring-what-matters.md) | 🟡 | Criterion.rs, Divan, `perf` flamegraphs, PGO, continuous benchmarking in CI | -| 4 | [Code Coverage — Seeing What Tests Miss](ch04-code-coverage-seeing-what-tests-miss.md) | 🟢 | `cargo-llvm-cov`, `cargo-tarpaulin`, `grcov`, Codecov/Coveralls CI integration | -| 5 | [Miri, Valgrind, and Sanitizers](ch05-miri-valgrind-and-sanitizers-verifying-u.md) | 🔴 | MIR interpreter, Valgrind memcheck/Helgrind, ASan/MSan/TSan, cargo-fuzz, loom | +| № | Глава | Сложность | Описание | +|---|-------|:---------:|----------| +| 3 | [Бенчмаркинг — измеряем то, что важно](ch03-benchmarking-measuring-what-matters.md) | 🟡 | Criterion.rs, Divan, флеймграфы `perf`, PGO, непрерывный бенчмаркинг в CI | +| 4 | [Покрытие кода — видим то, что пропускают тесты](ch04-code-coverage-seeing-what-tests-miss.md) | 🟢 | `cargo-llvm-cov`, `cargo-tarpaulin`, `grcov`, интеграция с Codecov/Coveralls в CI | +| 5 | [Miri, Valgrind и санитайзеры — проверка unsafe-кода](ch05-miri-valgrind-and-sanitizers-verifying-u.md) | 🔴 | Интерпретатор MIR, Valgrind memcheck/Helgrind, ASan/MSan/TSan, cargo-fuzz, loom | -### Part III — Harden & Optimize +### Часть III — Укрепление и оптимизация -| # | Chapter | Difficulty | Description | -|---|---------|:----------:|-------------| -| 6 | [Dependency Management and Supply Chain Security](ch06-dependency-management-and-supply-chain-s.md) | 🟢 | `cargo-audit`, `cargo-deny`, `cargo-vet`, `cargo-outdated`, `cargo-semver-checks` | -| 7 | [Release Profiles and Binary Size](ch07-release-profiles-and-binary-size.md) | 🟡 | Release profile anatomy, LTO trade-offs, `cargo-bloat`, `cargo-udeps` | -| 8 | [Compile-Time and Developer Tools](ch08-compile-time-and-developer-tools.md) | 🟡 | `sccache`, `mold`, `cargo-nextest`, `cargo-expand`, `cargo-geiger`, workspace lints, MSRV | -| 9 | [`no_std` and Feature Verification](ch09-no-std-and-feature-verification.md) | 🔴 | `cargo-hack`, `core`/`alloc`/`std` layers, custom panic handlers, testing `no_std` code | -| 10 | [Windows and Conditional Compilation](ch10-windows-and-conditional-compilation.md) | 🟡 | `#[cfg]` patterns, `windows-sys`/`windows` crates, `cargo-xwin`, platform abstraction | +| № | Глава | Сложность | Описание | +|---|-------|:---------:|----------| +| 6 | [Управление зависимостями и безопасность цепочки поставок](ch06-dependency-management-and-supply-chain-s.md) | 🟢 | `cargo-audit`, `cargo-deny`, `cargo-vet`, `cargo-outdated`, `cargo-semver-checks` | +| 7 | [Профили релиза и размер бинарного файла](ch07-release-profiles-and-binary-size.md) | 🟡 | Устройство профиля релиза, компромиссы LTO, `cargo-bloat`, `cargo-udeps` | +| 8 | [Инструменты времени компиляции и разработки](ch08-compile-time-and-developer-tools.md) | 🟡 | `sccache`, `mold`, `cargo-nextest`, `cargo-expand`, `cargo-geiger`, линты воркспейса, MSRV | +| 9 | [`no_std` и проверка фич](ch09-no-std-and-feature-verification.md) | 🔴 | `cargo-hack`, слои `core`/`alloc`/`std`, пользовательские обработчики паники, тестирование кода `no_std` | +| 10 | [Windows и условная компиляция](ch10-windows-and-conditional-compilation.md) | 🟡 | Паттерны `#[cfg]`, крейты `windows-sys`/`windows`, `cargo-xwin`, абстракция платформы | -### Part IV — Integrate - -| # | Chapter | Difficulty | Description | -|---|---------|:----------:|-------------| -| 11 | [Putting It All Together — A Production CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) | 🟡 | GitHub Actions workflow, `cargo-make`, pre-commit hooks, `cargo-dist`, capstone | -| 12 | [Tricks from the Trenches](ch12-tricks-from-the-trenches.md) | 🟡 | 10 battle-tested patterns: `deny(warnings)` trap, cache tuning, dep dedup, RUSTFLAGS, more | -| 13 | [Quick Reference Card](ch13-quick-reference-card.md) | — | Commands at a glance, 60+ decision table entries, further reading links | +### Часть IV — Интеграция +| № | Глава | Сложность | Описание | +|---|-------|:---------:|----------| +| 11 | [Собираем всё вместе — продакшн-конвейер CI/CD](ch11-putting-it-all-together-a-production-cic.md) | 🟡 | Workflow GitHub Actions, `cargo-make`, pre-commit-хуки, `cargo-dist`, итоговый пример | +| 12 | [Проверенные на практике приёмы](ch12-tricks-from-the-trenches.md) | 🟡 | 10 проверенных паттернов: ловушка `deny(warnings)`, настройка кэша, дедупликация зависимостей, RUSTFLAGS и другие | +| 13 | [Краткая справочная карточка](ch13-quick-reference-card.md) | — | Команды под рукой, более 60 записей таблицы решений, ссылки на дополнительную литературу | diff --git a/engineering-book/src/ch01-build-scripts-buildrs-in-depth.md b/engineering-book/src/ch01-build-scripts-buildrs-in-depth.md index 3dd1dbb..715e0a8 100644 --- a/engineering-book/src/ch01-build-scripts-buildrs-in-depth.md +++ b/engineering-book/src/ch01-build-scripts-buildrs-in-depth.md @@ -1,90 +1,91 @@ -# Build Scripts — `build.rs` in Depth 🟢 +# Build-скрипты — `build.rs` в деталях 🟢 -> **What you'll learn:** -> - How `build.rs` fits into the Cargo build pipeline and when it runs -> - Five production patterns: compile-time constants, C/C++ compilation, protobuf codegen, `pkg-config` linking, and feature detection -> - Anti-patterns that slow builds or break cross-compilation -> - How to balance traceability with reproducible builds +> **Чему вы научитесь:** +> - Как `build.rs` встраивается в конвейер сборки Cargo и когда он запускается +> - Пять продакшн-паттернов: константы времени компиляции, компиляция C/C++, кодогенерация protobuf, линковка через `pkg-config` и определение фич +> - Антипаттерны, которые замедляют сборку или ломают кросс-компиляцию +> - Как совмещать прослеживаемость с воспроизводимыми сборками > -> **Cross-references:** [Cross-Compilation](ch02-cross-compilation-one-source-many-target.md) uses build scripts for target-aware builds · [`no_std` & Features](ch09-no-std-and-feature-verification.md) extends `cfg` flags set here · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) orchestrates build scripts in automation +> **Перекрёстные ссылки:** [Кросс-компиляция](ch02-cross-compilation-one-source-many-target.md) использует build-скрипты для сборок с учётом целевой платформы · [`no_std` и фичи](ch09-no-std-and-feature-verification.md) расширяет `cfg`-флаги, заданные здесь · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) оркестрирует build-скрипты в автоматизации -Every Cargo package can include a file named `build.rs` at the crate root. -Cargo compiles and executes this file *before* compiling your crate. The build -script communicates back to Cargo through `println!` instructions on stdout. +Каждый пакет Cargo может содержать файл с именем `build.rs` в корне крейта. +Cargo компилирует и выполняет этот файл *до* компиляции вашего крейта. Build-скрипт +сообщает результаты Cargo через инструкции `println!`, которые выводятся в stdout. -### What build.rs Is and When It Runs +### Что такое build.rs и когда он запускается ```text ┌─────────────────────────────────────────────────────────┐ -│ Cargo Build Pipeline │ +│ Конвейер сборки Cargo │ │ │ -│ 1. Resolve dependencies │ -│ 2. Download crates │ -│ 3. Compile build.rs ← ordinary Rust, runs on HOST │ -│ 4. Execute build.rs ← stdout → Cargo instructions │ -│ 5. Compile the crate (using instructions from step 4) │ -│ 6. Link │ +│ 1. Разрешение зависимостей │ +│ 2. Загрузка крейтов │ +│ 3. Компиляция build.rs ← обычный Rust, на HOST │ +│ 4. Запуск build.rs ← stdout → инструкции для Cargo │ +│ 5. Компиляция крейта (по инструкциям из шага 4) │ +│ 6. Линковка │ └─────────────────────────────────────────────────────────┘ ``` -Key facts: -- `build.rs` runs on the **host** machine, not the target. During cross-compilation, - the build script runs on your development machine even when the final binary targets - a different architecture. -- The build script's scope is limited to its own package. It cannot affect how - other crates compile — unless the package declares a `links` key in `Cargo.toml`, - which enables passing metadata to dependent crates via - `cargo::metadata=KEY=VALUE`. -- It runs **every time** Cargo detects a change — unless you emit `cargo::rerun-if-changed` - instructions to limit re-runs. - -> **Note (Rust 1.71+)**: Since Rust 1.71, Cargo fingerprints the compiled -> `build.rs` binary — if the binary is identical, it won't re-run even if -> source timestamps changed. However, `cargo::rerun-if-changed=build.rs` is -> still valuable: without *any* `rerun-if-changed` instruction, Cargo re-runs -> `build.rs` whenever **any file in the package** changes (not just `build.rs`). -> Emitting `cargo::rerun-if-changed=build.rs` limits re-runs to only when -> `build.rs` itself changes — a significant compile-time saving in large crates. -- It can emit *cfg flags*, *environment variables*, *linker arguments*, and - *file paths* that the main crate consumes. - -The minimal `Cargo.toml` entry: +Ключевые факты: +- `build.rs` выполняется на **хост-машине**, а не на целевой. При кросс-компиляции + build-скрипт запускается на вашей машине разработки, даже если итоговый бинарник + предназначен для другой архитектуры. +- Область действия build-скрипта ограничена его собственным пакетом. Он не может + повлиять на то, как компилируются другие крейты, — если только пакет не объявляет + ключ `links` в `Cargo.toml`, который позволяет передавать метаданные зависимым крейтам + через `cargo::metadata=KEY=VALUE`. +- Он запускается **каждый раз**, когда Cargo обнаруживает изменения, — если только вы + не выводите инструкции `cargo::rerun-if-changed`, чтобы ограничить повторные запуски. + +> **Примечание (Rust 1.71+)**: начиная с Rust 1.71 Cargo вычисляет «отпечаток» +> (fingerprint) скомпилированного бинарника `build.rs` — если бинарник не изменился, +> он не будет перезапущен, даже если временные метки исходников изменились. Однако +> `cargo::rerun-if-changed=build.rs` всё равно полезна: без *какой-либо* инструкции +> `rerun-if-changed` Cargo перезапускает `build.rs` при изменении **любого файла в пакете** +> (а не только самого `build.rs`). Если вывести `cargo::rerun-if-changed=build.rs`, +> повторные запуски будут происходить только при изменении самого `build.rs` — заметная +> экономия времени компиляции в крупных крейтах. +- Он может выводить *cfg-флаги*, *переменные окружения*, *аргументы линкера* и + *пути к файлам*, которые использует основной крейт. + +Минимальная запись в `Cargo.toml`: ```toml [package] name = "my-crate" version = "0.1.0" edition = "2021" -build = "build.rs" # default — Cargo looks for build.rs automatically -# build = "src/build.rs" # or put it elsewhere +build = "build.rs" # значение по умолчанию — Cargo ищет build.rs автоматически +# build = "src/build.rs" # или разместите его в другом месте ``` -### The Cargo Instruction Protocol - -Your build script communicates with Cargo by printing instructions to stdout. -Since Rust 1.77, the preferred prefix is `cargo::` (replacing the older -`cargo:` single-colon form). - -| Instruction | Purpose | -|-------------|---------| -| `cargo::rerun-if-changed=PATH` | Only re-run build.rs when PATH changes | -| `cargo::rerun-if-env-changed=VAR` | Only re-run when environment variable VAR changes | -| `cargo::rustc-link-lib=NAME` | Link against native library NAME | -| `cargo::rustc-link-search=PATH` | Add PATH to the library search path | -| `cargo::rustc-cfg=KEY` | Set a `#[cfg(KEY)]` flag for conditional compilation | -| `cargo::rustc-cfg=KEY="VALUE"` | Set a `#[cfg(KEY = "VALUE")]` flag | -| `cargo::rustc-env=KEY=VALUE` | Set an environment variable accessible via `env!()` | -| `cargo::rustc-cdylib-link-arg=FLAG` | Pass FLAG to the linker for cdylib targets | -| `cargo::warning=MESSAGE` | Display a warning during compilation | -| `cargo::metadata=KEY=VALUE` | Store metadata readable by dependent crates | +### Протокол инструкций Cargo + +Build-скрипт взаимодействует с Cargo, выводя инструкции в stdout. +Начиная с Rust 1.77 предпочтительным префиксом является `cargo::` (он заменил +старую форму с одним двоеточием `cargo:`). + +| Инструкция | Назначение | +|------------|------------| +| `cargo::rerun-if-changed=PATH` | Перезапускать build.rs только при изменении PATH | +| `cargo::rerun-if-env-changed=VAR` | Перезапускать только при изменении переменной окружения VAR | +| `cargo::rustc-link-lib=NAME` | Линковаться с нативной библиотекой NAME | +| `cargo::rustc-link-search=PATH` | Добавить PATH в путь поиска библиотек | +| `cargo::rustc-cfg=KEY` | Задать флаг `#[cfg(KEY)]` для условной компиляции | +| `cargo::rustc-cfg=KEY="VALUE"` | Задать флаг `#[cfg(KEY = "VALUE")]` | +| `cargo::rustc-env=KEY=VALUE` | Задать переменную окружения, доступную через `env!()` | +| `cargo::rustc-cdylib-link-arg=FLAG` | Передать FLAG линкеру для целей cdylib | +| `cargo::warning=MESSAGE` | Вывести предупреждение во время компиляции | +| `cargo::metadata=KEY=VALUE` | Сохранить метаданные, доступные зависимым крейтам | ```rust -// build.rs — minimal example +// build.rs — минимальный пример fn main() { - // Only re-run if build.rs itself changes + // Перезапускать, только если сам build.rs изменился println!("cargo::rerun-if-changed=build.rs"); - // Set a compile-time environment variable + // Задать переменную окружения времени компиляции let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs().to_string()) @@ -93,10 +94,10 @@ fn main() { } ``` -### Pattern 1: Compile-Time Constants +### Паттерн 1: константы времени компиляции -The most common use case: baking build metadata into the binary so you can -report it at runtime (git hash, build date, CI job ID). +Самый распространённый случай: встроить метаданные сборки в бинарник, чтобы выводить +их во время выполнения (хеш git, дата сборки, ID задачи CI). ```rust // build.rs @@ -106,15 +107,15 @@ fn main() { println!("cargo::rerun-if-changed=.git/HEAD"); println!("cargo::rerun-if-changed=.git/refs"); - // Git commit hash + // Хеш коммита git let output = Command::new("git") .args(["rev-parse", "--short", "HEAD"]) .output() - .expect("git not found"); + .expect("git не найден"); let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); println!("cargo::rustc-env=GIT_HASH={git_hash}"); - // Build profile (debug or release) + // Профиль сборки (debug или release) let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into()); println!("cargo::rustc-env=BUILD_PROFILE={profile}"); @@ -125,7 +126,7 @@ fn main() { ``` ```rust -// src/main.rs — consuming the build-time values +// src/main.rs — использование значений, известных на этапе сборки fn print_version() { println!( "{} {} (git:{} target:{} profile:{})", @@ -138,16 +139,16 @@ fn print_version() { } ``` -> **Built-in Cargo environment variables** you get for free, no build.rs needed: +> **Встроенные переменные окружения Cargo**, которые доступны без build.rs: > `CARGO_PKG_NAME`, `CARGO_PKG_VERSION`, `CARGO_PKG_AUTHORS`, > `CARGO_PKG_DESCRIPTION`, `CARGO_MANIFEST_DIR`. -> See the [full list](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates). +> См. [полный список](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates). -### Pattern 2: Compiling C/C++ Code with the `cc` Crate +### Паттерн 2: компиляция C/C++ кода с помощью крейта `cc` -When your Rust crate wraps a C library or needs a small C helper (common in -hardware interfaces), the [`cc`](https://docs.rs/cc) crate simplifies -compilation inside build.rs. +Когда ваш крейт на Rust оборачивает C-библиотеку или нуждается в небольшом C-помощнике +(типично для аппаратных интерфейсов), крейт [`cc`](https://docs.rs/cc) упрощает +компиляцию внутри build.rs. ```toml # Cargo.toml @@ -168,13 +169,13 @@ fn main() { .flag("-Wextra") .opt_level(2) .compile("diag_helpers"); - // This produces libdiag_helpers.a and emits the right - // cargo::rustc-link-lib and cargo::rustc-link-search instructions. + // Это создаёт libdiag_helpers.a и выводит правильные инструкции + // cargo::rustc-link-lib и cargo::rustc-link-search. } ``` ```rust -// src/lib.rs — FFI bindings to the compiled C code +// src/lib.rs — FFI-привязки к скомпилированному C-коду extern "C" { fn ipmi_raw_command( netfn: u8, @@ -186,13 +187,13 @@ extern "C" { ) -> i32; } -/// Safe wrapper around the raw IPMI command interface. -/// Assumes: enum IpmiError { CommandFailed(i32), ... } +/// Безопасная обёртка над сырым интерфейсом команд IPMI. +/// Предполагается: enum IpmiError { CommandFailed(i32), ... } pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result, IpmiError> { let mut response = vec![0u8; 256]; let mut response_len: usize = response.len(); - // SAFETY: response buffer is large enough and response_len is correctly initialized. + // SAFETY: буфер ответа достаточно велик, а response_len корректно инициализирован. let rc = unsafe { ipmi_raw_command( netfn, @@ -212,10 +213,10 @@ pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result, Ipm } ``` -For C++ code, use `.cpp(true)` and `.flag("-std=c++17")`: +Для C++-кода используйте `.cpp(true)` и `.flag("-std=c++17")`: ```rust -// build.rs — C++ variant +// build.rs — вариант для C++ fn main() { println!("cargo::rerun-if-changed=cppsrc/"); @@ -223,16 +224,16 @@ fn main() { .cpp(true) .file("cppsrc/vendor_parser.cpp") .flag("-std=c++17") - .flag("-fno-exceptions") // match Rust's no-exception model + .flag("-fno-exceptions") // соответствует модели Rust без исключений .compile("vendor_helpers"); } ``` -### Pattern 3: Protocol Buffers and Code Generation +### Паттерн 3: Protocol Buffers и кодогенерация -Build scripts excel at code generation — turning `.proto`, `.fbs`, or `.json` -schema files into Rust source at compile time. Here's the protobuf pattern -using [`prost-build`](https://docs.rs/prost-build): +Build-скрипты отлично подходят для кодогенерации — превращения файлов схем `.proto`, +`.fbs` или `.json` в исходный код Rust во время компиляции. Вот паттерн для protobuf +с использованием [`prost-build`](https://docs.rs/prost-build): ```toml # Cargo.toml @@ -249,12 +250,12 @@ fn main() { &["proto/diagnostics.proto", "proto/telemetry.proto"], &["proto/"], ) - .expect("Failed to compile protobuf definitions"); + .expect("Не удалось скомпилировать определения protobuf"); } ``` ```rust -// src/lib.rs — include the generated code +// src/lib.rs — подключаем сгенерированный код pub mod diagnostics { include!(concat!(env!("OUT_DIR"), "/diagnostics.rs")); } @@ -264,14 +265,15 @@ pub mod telemetry { } ``` -> **`OUT_DIR`** is a Cargo-provided directory where build scripts should place -> generated files. Each crate gets its own `OUT_DIR` under `target/`. +> **`OUT_DIR`** — это директория, которую предоставляет Cargo и в которую build-скрипты +> должны помещать сгенерированные файлы. Каждый крейт получает собственный `OUT_DIR` +> внутри `target/`. -### Pattern 4: Linking System Libraries with `pkg-config` +### Паттерн 4: линковка системных библиотек через `pkg-config` -For system libraries that provide `.pc` files (systemd, OpenSSL, libpci), -the [`pkg-config`](https://docs.rs/pkg-config) crate probes the system and -emits the right link instructions: +Для системных библиотек, которые предоставляют файлы `.pc` (systemd, OpenSSL, libpci), +крейт [`pkg-config`](https://docs.rs/pkg-config) проверяет систему и выводит +нужные инструкции для линковки: ```toml # Cargo.toml @@ -282,13 +284,13 @@ pkg-config = "0.3" ```rust // build.rs fn main() { - // Probe for libpci (used for PCIe device enumeration) + // Проверяем наличие libpci (используется для перечисления устройств PCIe) pkg_config::Config::new() .atleast_version("3.6.0") .probe("libpci") - .expect("libpci >= 3.6.0 not found — install pciutils-dev"); + .expect("libpci >= 3.6.0 не найдена — установите pciutils-dev"); - // Probe for libsystemd (optional — for sd_notify integration) + // Проверяем наличие libsystemd (необязательно — для интеграции с sd_notify) if pkg_config::probe_library("libsystemd").is_ok() { println!("cargo::rustc-cfg=has_systemd"); } @@ -296,7 +298,7 @@ fn main() { ``` ```rust -// src/lib.rs — conditional compilation based on pkg-config probing +// src/lib.rs — условная компиляция на основе проверки через pkg-config #[cfg(has_systemd)] mod systemd_notify { extern "C" { @@ -305,7 +307,7 @@ mod systemd_notify { pub fn notify_ready() { let state = std::ffi::CString::new("READY=1").unwrap(); - // SAFETY: state is a valid null-terminated C string. + // SAFETY: state — корректная C-строка с нулевым завершителем. unsafe { sd_notify(0, state.as_ptr()) }; } } @@ -313,52 +315,52 @@ mod systemd_notify { #[cfg(not(has_systemd))] mod systemd_notify { pub fn notify_ready() { - // no-op on systems without systemd + // ничего не делаем на системах без systemd } } ``` -### Pattern 5: Feature Detection and Conditional Compilation +### Паттерн 5: определение фич и условная компиляция -Build scripts can probe the compilation environment and set cfg flags that -the main crate uses for conditional code paths. +Build-скрипты могут проверять окружение компиляции и задавать cfg-флаги, которые основной +крейт использует для условных путей выполнения. -**CPU architecture and OS detection** (safe — these are compile-time constants): +**Определение архитектуры процессора и ОС** (безопасно — это константы времени компиляции): ```rust -// build.rs — detect CPU features and OS capabilities +// build.rs — определяем возможности CPU и ОС fn main() { println!("cargo::rerun-if-changed=build.rs"); let target = std::env::var("TARGET").unwrap(); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - // Enable AVX2-optimized paths on x86_64 + // Включаем оптимизированные пути AVX2 на x86_64 if target.starts_with("x86_64") { println!("cargo::rustc-cfg=has_x86_64"); } - // Enable ARM NEON paths on aarch64 + // Включаем пути ARM NEON на aarch64 if target.starts_with("aarch64") { println!("cargo::rustc-cfg=has_aarch64"); } - // Detect if /dev/ipmi0 is available (build-time check) + // Проверяем, доступно ли /dev/ipmi0 (проверка во время сборки) if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() { println!("cargo::rustc-cfg=has_ipmi_device"); } } ``` -> ⚠️ **Anti-pattern demonstration** — The code below shows a tempting but -> problematic approach. **Do not use this in production.** +> ⚠️ **Демонстрация антипаттерна** — код ниже показывает заманчивый, но проблемный подход. +> **Не используйте его в продакшне.** ```rust -// build.rs — BAD: runtime hardware detection at build time +// build.rs — ПЛОХО: определение аппаратуры во время выполнения, но на этапе сборки fn main() { - // ANTI-PATTERN: Binary is baked to the BUILD machine's hardware. - // If you build on a machine with a GPU and deploy to one without, - // the binary silently assumes a GPU is present. + // АНТИПАТТЕРН: бинарник «привязывается» к железу машины сборки. + // Если собрать его на машине с GPU, а развернуть на машине без GPU, + // бинарник молча будет считать, что GPU есть. if std::process::Command::new("accel-query") .arg("--query-gpu=name") .arg("--format=csv,noheader") @@ -371,7 +373,7 @@ fn main() { ``` ```rust -// src/gpu.rs — code that adapts based on build-time detection +// src/gpu.rs — код, который адаптируется на основе определения на этапе сборки pub fn query_gpu_info() -> GpuResult { #[cfg(has_accel_device)] { @@ -380,42 +382,43 @@ pub fn query_gpu_info() -> GpuResult { #[cfg(not(has_accel_device))] { - GpuResult::NotAvailable("accel-query not found at build time".into()) + GpuResult::NotAvailable("accel-query не найден на этапе сборки".into()) } } ``` -> ⚠️ **Why this is wrong**: Runtime device detection is almost always better than -> build-time detection for optional hardware. The binary produced above is -> *tied to the build machine's hardware configuration* — it will behave differently -> on the deployment target. Use build-time detection only for capabilities that are -> truly fixed at compile time (architecture, OS, library availability). -> For hardware like GPUs, detect at runtime with `which accel-query` or `accel-mgmt` probing. +> ⚠️ **Почему это неправильно**: определение устройств во время выполнения почти всегда +> лучше, чем определение во время сборки, для необязательного оборудования. Бинарник, +> созданный выше, *привязан к конфигурации оборудования машины сборки* — на целевой +> машине он будет вести себя иначе. Определение на этапе сборки уместно только для +> возможностей, которые действительно фиксированы на этапе компиляции (архитектура, ОС, +> наличие библиотек). Для оборудования вроде GPU определяйте его во время выполнения, +> например проверкой `which accel-query` или через `accel-mgmt`. -### Anti-Patterns and Pitfalls +### Антипаттерны и ловушки -| Anti-Pattern | Why It's Bad | Fix | -|-------------|-------------|-----| -| No `rerun-if-changed` | build.rs runs on *every* build, slowing iteration | Always emit at least `cargo::rerun-if-changed=build.rs` | -| Network calls in build.rs | Builds fail offline, non-reproducible | Vendor files or use a separate fetch step | -| Writing to `src/` | Cargo doesn't expect source to change during build | Write to `OUT_DIR` and use `include!()` | -| Heavy computation | Slows every `cargo build` | Cache results in `OUT_DIR`, gate with `rerun-if-changed` | -| Ignoring cross-compilation | Using `Command::new("gcc")` without respecting `$CC` | Use the `cc` crate which handles cross-compilation toolchains | -| Panicking without context | `unwrap()` gives opaque "build script failed" error | Use `.expect("descriptive message")` or print `cargo::warning=` | +| Антипаттерн | Почему это плохо | Решение | +|-------------|------------------|---------| +| Нет `rerun-if-changed` | build.rs запускается при *каждой* сборке, замедляя итерации | Всегда выводите как минимум `cargo::rerun-if-changed=build.rs` | +| Сетевые вызовы в build.rs | Сборки падают без сети, результат невоспроизводим | Вендорьте файлы или используйте отдельный шаг загрузки | +| Запись в `src/` | Cargo не ожидает, что исходники меняются во время сборки | Пишите в `OUT_DIR` и используйте `include!()` | +| Тяжёлые вычисления | Замедляют каждый `cargo build` | Кешируйте результаты в `OUT_DIR` и ограничивайте через `rerun-if-changed` | +| Игнорирование кросс-компиляции | `Command::new("gcc")` без учёта `$CC` | Используйте крейт `cc`, который работает с кросс-компиляционными тулчейнами | +| Паника без контекста | `unwrap()` выдаёт непрозрачную ошибку «build script failed» | Используйте `.expect("понятное сообщение")` или выводите `cargo::warning=` | -### Application: Embedding Build Metadata +### Применение: встраивание метаданных сборки -The project currently uses `env!("CARGO_PKG_VERSION")` for version -reporting. A build script would extend this with richer metadata: +Сейчас проект использует `env!("CARGO_PKG_VERSION")` для вывода версии. Build-скрипт +мог бы расширить это более богатыми метаданными: ```rust -// build.rs — proposed addition +// build.rs — предлагаемое дополнение fn main() { println!("cargo::rerun-if-changed=.git/HEAD"); println!("cargo::rerun-if-changed=.git/refs"); println!("cargo::rerun-if-changed=build.rs"); - // Embed git hash for traceability in diagnostic reports + // Встраиваем хеш git для прослеживаемости в диагностических отчётах if let Ok(output) = std::process::Command::new("git") .args(["rev-parse", "--short=10", "HEAD"]) .output() @@ -426,21 +429,21 @@ fn main() { println!("cargo::rustc-env=APP_GIT_HASH=unknown"); } - // Embed build timestamp for report correlation + // Встраиваем временную метку сборки для связи с отчётами let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs().to_string()) .unwrap_or_else(|_| "0".into()); println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}"); - // Emit target triple — useful in multi-arch deployment + // Выводим target triple — полезно при развёртывании на нескольких архитектурах let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into()); println!("cargo::rustc-env=APP_TARGET={target}"); } ``` ```rust -// src/version.rs — consuming the metadata +// src/version.rs — использование метаданных pub struct BuildInfo { pub version: &'static str, pub git_hash: &'static str, @@ -456,8 +459,8 @@ pub const BUILD_INFO: BuildInfo = BuildInfo { }; impl BuildInfo { - /// Parse the epoch at runtime when needed (const &str → u64 is not - /// possible on stable Rust — there is no const fn for str-to-int). + /// Разбираем эпоху во время выполнения, когда это нужно (преобразовать + /// const &str в u64 на stable Rust нельзя — нет const fn для разбора строки в число). pub fn build_epoch_secs(&self) -> u64 { self.build_epoch.parse().unwrap_or(0) } @@ -474,55 +477,54 @@ impl std::fmt::Display for BuildInfo { } ``` -> **Key insight from the project**: The codebase has zero `build.rs` files -> across all many crates because it's pure Rust with no C dependencies, no codegen, -> and no system library linking. When you need these, `build.rs` is the tool — but -> don't add it "just because." The absence of build scripts in a large codebase -> is a feature, not a gap. See [Dependency Management](ch06-dependency-management-and-supply-chain-s.md) -> for how the project manages its supply chain without custom build logic. -> is a *positive* signal of a clean architecture. +> **Ключевая мысль из проекта**: в кодовой базе нет ни одного файла `build.rs` ни в одном +> из крейтов, потому что это чистый Rust без зависимостей от C, без кодогенерации и без +> линковки системных библиотек. Когда это понадобится, `build.rs` — правильный инструмент, +> но не добавляйте его «просто так». Отсутствие build-скриптов в большой кодовой базе — +> признак чистой архитектуры, а не пробел. См. [Управление зависимостями](ch06-dependency-management-and-supply-chain-s.md), +> чтобы узнать, как проект управляет цепочкой поставок без собственной логики сборки. -### Try It Yourself +### Попробуйте сами -1. **Embed git metadata**: Create a `build.rs` that emits `APP_GIT_HASH` and - `APP_BUILD_EPOCH` as environment variables. Consume them with `env!()` in - `main.rs` and print the build info. Verify the hash changes after a commit. +1. **Встройте метаданные git**: создайте `build.rs`, который выводит `APP_GIT_HASH` и + `APP_BUILD_EPOCH` как переменные окружения. Прочитайте их через `env!()` в `main.rs` + и выведите информацию о сборке. Убедитесь, что хеш меняется после коммита. -2. **Probe a system library**: Write a `build.rs` that uses `pkg-config` to probe - for `libz` (zlib). Emit `cargo::rustc-cfg=has_zlib` if found. In `main.rs`, - conditionally print "zlib available" or "zlib not found" based on the cfg flag. +2. **Проверьте системную библиотеку**: напишите `build.rs`, который использует `pkg-config`, + чтобы найти `libz` (zlib). Если библиотека найдена, выведите `cargo::rustc-cfg=has_zlib`. + В `main.rs` выведите «zlib доступен» или «zlib не найден» в зависимости от флага cfg. -3. **Trigger a build failure intentionally**: Remove the `rerun-if-changed` line - from your `build.rs` and observe how many times it reruns during `cargo build` - and `cargo test`. Then add it back and compare. +3. **Понаблюдайте за лишними перезапусками**: удалите строку `rerun-if-changed` из + `build.rs` и посмотрите, сколько раз он перезапускается во время `cargo build` и + `cargo test`. Затем верните строку и сравните результаты. -### Reproducible Builds +### Воспроизводимые сборки -Chapter 1 teaches embedding timestamps and git hashes into binaries. This is -useful for traceability, but it **conflicts with reproducible builds** — the -property that building the same source always produces the same binary. +Глава 1 учит встраивать временные метки и хеши git в бинарники. Это полезно для +прослеживаемости, но **противоречит воспроизводимым сборкам** — свойству, при котором +сборка одного и того же исходного кода всегда даёт один и тот же бинарник. -**The tension:** +**Противоречие:** -| Goal | Achievement | Cost | -|------|-------------|------| -| Traceability | `APP_BUILD_EPOCH` in binary | Every build is unique — can't verify integrity | -| Reproducibility | `cargo build --locked` always produces same output | No build-time metadata | +| Цель | Достигается через | Цена | +|------|-------------------|------| +| Прослеживаемость | `APP_BUILD_EPOCH` в бинарнике | Каждая сборка уникальна — нельзя проверить целостность | +| Воспроизводимость | `cargo build --locked` всегда даёт одинаковый результат | Нет метаданных времени сборки | -**Practical resolution:** +**Практическое решение:** ```bash -# 1. Always use --locked in CI (ensures Cargo.lock is respected) +# 1. Всегда используйте --locked в CI (гарантирует, что Cargo.lock учитывается) cargo build --release --locked -# Fails if Cargo.lock is missing or outdated — catches "works on my machine" +# Завершится с ошибкой, если Cargo.lock отсутствует или устарел — ловит «у меня работает» -# 2. For reproducibility-critical builds, set SOURCE_DATE_EPOCH +# 2. Для сборок, критичных к воспроизводимости, задайте SOURCE_DATE_EPOCH SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked -# Uses the last commit timestamp instead of "now" — same commit = same binary +# Использует временную метку последнего коммита вместо «сейчас» — один коммит = один бинарник ``` ```rust -// In build.rs: respect SOURCE_DATE_EPOCH for reproducibility +// В build.rs: учитываем SOURCE_DATE_EPOCH для воспроизводимости let timestamp = std::env::var("SOURCE_DATE_EPOCH") .unwrap_or_else(|_| { std::time::SystemTime::now() @@ -533,29 +535,30 @@ let timestamp = std::env::var("SOURCE_DATE_EPOCH") println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}"); ``` -> **Best practice**: Use `SOURCE_DATE_EPOCH` in build scripts so release builds -> are reproducible (`git-hash + locked deps + deterministic timestamp = same binary`), -> while dev builds still get live timestamps for convenience. +> **Лучшая практика**: используйте `SOURCE_DATE_EPOCH` в build-скриптах, чтобы релизные +> сборки были воспроизводимыми (`хеш git + зафиксированные зависимости + детерминированная +> временная метка = один и тот же бинарник`), а dev-сборки по-прежнему получали актуальные +> временные метки для удобства. -### Build Pipeline Decision Diagram +### Диаграмма выбора в конвейере сборки ```mermaid flowchart TD - START["Need compile-time work?"] -->|No| SKIP["No build.rs needed"] - START -->|Yes| WHAT{"What kind?"} - - WHAT -->|"Embed metadata"| P1["Pattern 1
Compile-Time Constants"] - WHAT -->|"Compile C/C++"| P2["Pattern 2
cc crate"] - WHAT -->|"Code generation"| P3["Pattern 3
prost-build / tonic-build"] - WHAT -->|"Link system lib"| P4["Pattern 4
pkg-config"] - WHAT -->|"Detect features"| P5["Pattern 5
cfg flags"] - - P1 --> RERUN["Always emit
cargo::rerun-if-changed"] + START["Нужна работа на этапе компиляции?"] -->|Нет| SKIP["build.rs не нужен"] + START -->|Да| WHAT{"Какого типа?"} + + WHAT -->|"Встроить метаданные"| P1["Паттерн 1
Константы времени компиляции"] + WHAT -->|"Компилировать C/C++"| P2["Паттерн 2
крейт cc"] + WHAT -->|"Кодогенерация"| P3["Паттерн 3
prost-build / tonic-build"] + WHAT -->|"Линковать системную библиотеку"| P4["Паттерн 4
pkg-config"] + WHAT -->|"Определить фичи"| P5["Паттерн 5
cfg-флаги"] + + P1 --> RERUN["Всегда выводите
cargo::rerun-if-changed"] P2 --> RERUN P3 --> RERUN P4 --> RERUN P5 --> RERUN - + style SKIP fill:#91e5a3,color:#000 style RERUN fill:#ffd43b,color:#000 style P1 fill:#e3f2fd,color:#000 @@ -565,14 +568,16 @@ flowchart TD style P5 fill:#e3f2fd,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Version Stamp +#### 🟢 Упражнение 1: штамп версии -Create a minimal crate with a `build.rs` that embeds the current git hash and build profile into environment variables. Print them from `main()`. Verify the output changes between debug and release builds. +Создайте минимальный крейт с `build.rs`, который встраивает текущий хеш git и профиль +сборки в переменные окружения. Выведите их из `main()`. Убедитесь, что вывод меняется +между debug- и release-сборками.
-Solution +Решение ```rust // build.rs @@ -603,17 +608,19 @@ fn main() { ``` ```bash -cargo run # shows profile:debug -cargo run --release # shows profile:release +cargo run # покажет profile:debug +cargo run --release # покажет profile:release ```
-#### 🟡 Exercise 2: Conditional System Library +#### 🟡 Упражнение 2: условная системная библиотека -Write a `build.rs` that probes for both `libz` and `libpci` using `pkg-config`. Emit a `cfg` flag for each one found. In `main.rs`, print which libraries were detected at build time. +Напишите `build.rs`, который с помощью `pkg-config` проверяет наличие и `libz`, и `libpci`. +Выведите cfg-флаг для каждой найденной библиотеки. В `main.rs` выведите, какие библиотеки +были обнаружены во время сборки.
-Solution +Решение ```toml # Cargo.toml @@ -638,25 +645,29 @@ fn main() { // src/main.rs fn main() { #[cfg(has_zlib)] - println!("✅ zlib detected"); + println!("✅ zlib обнаружен"); #[cfg(not(has_zlib))] - println!("❌ zlib not found"); + println!("❌ zlib не найден"); #[cfg(has_libpci)] - println!("✅ libpci detected"); + println!("✅ libpci обнаружен"); #[cfg(not(has_libpci))] - println!("❌ libpci not found"); + println!("❌ libpci не найден"); } ```
-### Key Takeaways +### Ключевые выводы -- `build.rs` runs on the **host** at compile time — always emit `cargo::rerun-if-changed` to avoid unnecessary rebuilds -- Use the `cc` crate (not raw `gcc` commands) for C/C++ compilation — it handles cross-compilation toolchains correctly -- Write generated files to `OUT_DIR`, never to `src/` — Cargo doesn't expect source to change during builds -- Prefer runtime detection over build-time detection for optional hardware -- Use `SOURCE_DATE_EPOCH` to make builds reproducible when embedding timestamps +- `build.rs` выполняется на **хосте** во время компиляции — всегда выводите + `cargo::rerun-if-changed`, чтобы избежать лишних пересборок +- Используйте крейт `cc` (а не прямые вызовы `gcc`) для компиляции C/C++ — он корректно + работает с кросс-компиляционными тулчейнами +- Записывайте сгенерированные файлы в `OUT_DIR`, но никогда в `src/` — Cargo не ожидает, + что исходники будут меняться во время сборки +- Предпочитайте определение во время выполнения определению во время сборки для + необязательного оборудования +- Используйте `SOURCE_DATE_EPOCH`, чтобы сборки были воспроизводимыми при встраивании + временных меток --- - From f29a1c2ded6268e0a81b2d5699243fdad9647cbd Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:30:49 +0400 Subject: [PATCH 05/18] Translate Engineering book chapters 2-5 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for cross-compilation, benchmarking, code coverage and Miri/sanitizers chapters. Code identifiers, crate names, file names, tool output and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- ...ross-compilation-one-source-many-target.md | 408 ++++++------- ...h03-benchmarking-measuring-what-matters.md | 386 ++++++------ ...04-code-coverage-seeing-what-tests-miss.md | 372 ++++++------ ...iri-valgrind-and-sanitizers-verifying-u.md | 557 +++++++++--------- 4 files changed, 879 insertions(+), 844 deletions(-) diff --git a/engineering-book/src/ch02-cross-compilation-one-source-many-target.md b/engineering-book/src/ch02-cross-compilation-one-source-many-target.md index 2511701..45bafa6 100644 --- a/engineering-book/src/ch02-cross-compilation-one-source-many-target.md +++ b/engineering-book/src/ch02-cross-compilation-one-source-many-target.md @@ -1,127 +1,127 @@ -# Cross-Compilation — One Source, Many Targets 🟡 +# Кросс-компиляция — один исходник, много целей 🟡 -> **What you'll learn:** -> - How Rust target triples work and how to add them with `rustup` -> - Building static musl binaries for container/cloud deployment -> - Cross-compiling to ARM (aarch64) with native toolchains, `cross`, and `cargo-zigbuild` -> - Setting up GitHub Actions matrix builds for multi-architecture CI +> **Чему вы научитесь:** +> - Как устроены target triple в Rust и как добавлять их через `rustup` +> - Сборка статических бинарников musl для развёртывания в контейнерах и облаке +> - Кросс-компиляция под ARM (aarch64) с нативными тулчейнами, `cross` и `cargo-zigbuild` +> - Настройка матричных сборок GitHub Actions для мультиархитектурного CI > -> **Cross-references:** [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) — build.rs runs on HOST during cross-compilation · [Release Profiles](ch07-release-profiles-and-binary-size.md) — LTO and strip settings for cross-compiled release binaries · [Windows](ch10-windows-and-conditional-compilation.md) — Windows cross-compilation and `no_std` targets +> **Перекрёстные ссылки:** [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) — build.rs выполняется на HOST при кросс-компиляции · [Профили релиза](ch07-release-profiles-and-binary-size.md) — настройки LTO и strip для релизных кросс-скомпилированных бинарников · [Windows](ch10-windows-and-conditional-compilation.md) — кросс-компиляция под Windows и цели `no_std` -Cross-compilation means building an executable on one machine (the **host**) that -runs on a different machine (the **target**). The host might be your x86_64 laptop; -the target might be an ARM server, a musl-based container, or even a Windows machine. -Rust makes this remarkably feasible because `rustc` is already a cross-compiler — -it just needs the right target libraries and a compatible linker. +Кросс-компиляция — это сборка исполняемого файла на одной машине (**хост**), который +будет работать на другой машине (**целевая**). Хостом может быть ваш ноутбук x86_64, +а целью — ARM-сервер, контейнер на базе musl или даже машина с Windows. +Rust делает это на удивление просто, потому что `rustc` уже является кросс-компилятором — +ему нужны только подходящие библиотеки для целевой платформы и совместимый линкер. -### The Target Triple Anatomy +### Устройство target triple -Every Rust compilation target is identified by a **target triple** (which often has -four parts despite the name): +Каждая цель компиляции в Rust задаётся **target triple** (несмотря на название, у неё +часто четыре части): ```text --- -Examples: - x86_64 - unknown - linux - gnu ← standard Linux (glibc) - x86_64 - unknown - linux - musl ← static Linux (musl libc) - aarch64 - unknown - linux - gnu ← ARM 64-bit Linux - x86_64 - pc - windows- msvc ← Windows with MSVC - aarch64 - apple - darwin ← macOS on Apple Silicon - x86_64 - unknown - none ← bare metal (no OS) +Примеры: + x86_64 - unknown - linux - gnu ← стандартный Linux (glibc) + x86_64 - unknown - linux - musl ← статический Linux (musl libc) + aarch64 - unknown - linux - gnu ← 64-бит ARM Linux + x86_64 - pc - windows- msvc ← Windows с MSVC + aarch64 - apple - darwin ← macOS на Apple Silicon + x86_64 - unknown - none ← голое железо (без ОС) ``` -List all available targets: +Список всех доступных целей: ```bash -# Show all targets rustc can compile to (~250 targets) +# Все цели, в которые может компилировать rustc (~250 целей) rustc --print target-list | wc -l -# Show installed targets on your system +# Установленные на вашей системе цели rustup target list --installed -# Show current default target +# Текущая цель по умолчанию rustc -vV | grep host ``` -### Installing Toolchains with rustup +### Установка тулчейнов через rustup ```bash -# Add target libraries (Rust std for that target) +# Добавляем библиотеки для целевых платформ (std для нужной цели) rustup target add x86_64-unknown-linux-musl rustup target add aarch64-unknown-linux-gnu -# Now you can cross-compile: +# Теперь можно кросс-компилировать: cargo build --target x86_64-unknown-linux-musl -cargo build --target aarch64-unknown-linux-gnu # needs a linker — see below +cargo build --target aarch64-unknown-linux-gnu # нужен линкер — см. ниже ``` -**What `rustup target add` gives you**: the pre-compiled `std`, `core`, and `alloc` -libraries for that target. It does *not* give you a C linker or C library. For targets -that need a C toolchain (most `gnu` targets), you need to install one separately. +**Что даёт `rustup target add`**: предварительно скомпилированные библиотеки `std`, `core` +и `alloc` для этой цели. Он *не* предоставляет C-линкер или C-библиотеку. Для целей, +которым нужен C-тулчейн (большинство целей `gnu`), его нужно установить отдельно. ```bash -# Ubuntu/Debian — install the cross-linker for aarch64 +# Ubuntu/Debian — устанавливаем кросс-линкер для aarch64 sudo apt install gcc-aarch64-linux-gnu -# Ubuntu/Debian — install musl toolchain for static builds +# Ubuntu/Debian — устанавливаем тулчейн musl для статических сборок sudo apt install musl-tools # Fedora sudo dnf install gcc-aarch64-linux-gnu ``` -### `.cargo/config.toml` — Per-Target Configuration +### `.cargo/config.toml` — настройка по целям -Instead of passing `--target` on every command, configure defaults in -`.cargo/config.toml` at your project root or home directory: +Вместо того чтобы передавать `--target` в каждой команде, задайте значения по умолчанию в +`.cargo/config.toml` в корне проекта или в домашней директории: ```toml # .cargo/config.toml -# Default target for this project (optional — omit to keep native default) +# Цель по умолчанию для этого проекта (необязательно — уберите, чтобы оставить нативную) # [build] # target = "x86_64-unknown-linux-musl" -# Linker for aarch64 cross-compilation +# Линкер для кросс-компиляции под aarch64 [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" rustflags = ["-C", "target-feature=+crc"] -# Linker for musl static builds (usually just the system gcc works) +# Линкер для статических сборок musl (обычно подходит и системный gcc) [target.x86_64-unknown-linux-musl] linker = "musl-gcc" rustflags = ["-C", "target-feature=+crc,+aes"] -# ARM 32-bit (Raspberry Pi, embedded) +# ARM 32-бит (Raspberry Pi, встраиваемые системы) [target.armv7-unknown-linux-gnueabihf] linker = "arm-linux-gnueabihf-gcc" -# Environment variables for all targets +# Переменные окружения для всех целей [env] -# Example: set a custom sysroot +# Пример: задаём собственный sysroot # SYSROOT = "/opt/cross/sysroot" ``` -**Config file search order** (first match wins): +**Порядок поиска файла конфигурации** (побеждает первый найденный): 1. `/.cargo/config.toml` -2. `/../.cargo/config.toml` (parent directories, walking up) -3. `$CARGO_HOME/config.toml` (usually `~/.cargo/config.toml`) +2. `/../.cargo/config.toml` (родительские директории, подъём вверх по дереву) +3. `$CARGO_HOME/config.toml` (обычно `~/.cargo/config.toml`) -### Static Binaries with musl +### Статические бинарники с musl -For deploying to minimal containers (Alpine, scratch Docker images) or systems -where you can't control the glibc version, build with musl: +Для развёртывания в минимальных контейнерах (Alpine, образы Docker `scratch`) или на +системах, где нельзя контролировать версию glibc, собирайте проект с musl: ```bash -# Install musl target +# Устанавливаем musl-цель rustup target add x86_64-unknown-linux-musl -sudo apt install musl-tools # provides musl-gcc +sudo apt install musl-tools # предоставляет musl-gcc -# Build a fully static binary +# Собираем полностью статический бинарник cargo build --release --target x86_64-unknown-linux-musl -# Verify it's static +# Проверяем, что он статический file target/x86_64-unknown-linux-musl/release/diag_tool # → ELF 64-bit LSB executable, x86-64, statically linked @@ -129,55 +129,55 @@ ldd target/x86_64-unknown-linux-musl/release/diag_tool # → not a dynamic executable ``` -**Static vs dynamic trade-offs:** +**Компромиссы статической и динамической линковки:** -| Aspect | glibc (dynamic) | musl (static) | -|--------|-----------------|---------------| -| Binary size | Smaller (shared libs) | Larger (~5-15 MB increase) | -| Portability | Needs matching glibc version | Runs anywhere on Linux | -| DNS resolution | Full `nsswitch` support | Basic resolver (no mDNS) | -| Deployment | Needs sysroot or container | Single binary, no deps | -| Performance | Slightly faster malloc | Slightly slower malloc | -| `dlopen()` support | Yes | No | +| Аспект | glibc (динамическая) | musl (статическая) | +|--------|----------------------|--------------------| +| Размер бинарника | Меньше (общие библиотеки) | Больше (примерно на 5–15 МБ) | +| Переносимость | Нужна совместимая версия glibc | Работает на любом Linux | +| Разрешение DNS | Полная поддержка `nsswitch` | Базовый резолвер (без mDNS) | +| Развёртывание | Нужны sysroot или контейнер | Один бинарник, без зависимостей | +| Производительность | Немного быстрее malloc | Немного медленнее malloc | +| Поддержка `dlopen()` | Да | Нет | -> **For the project**: A static musl build is ideal for deployment to diverse -> server hardware where you can't guarantee the host OS version. The single-binary -> deployment model eliminates "works on my machine" issues. +> **Для проекта**: статическая сборка musl идеально подходит для развёртывания на +> разнородном серверном железе, где нельзя гарантировать версию ОС на хосте. Модель +> развёртывания одним бинарником устраняет проблемы «у меня работает». -### Cross-Compiling to ARM (aarch64) +### Кросс-компиляция под ARM (aarch64) -ARM servers (AWS Graviton, Ampere Altra, Grace) are increasingly common -in data centers. Cross-compiling for aarch64 from an x86_64 host: +ARM-серверы (AWS Graviton, Ampere Altra, Grace) всё чаще встречаются в дата-центрах. +Кросс-компиляция под aarch64 с хоста x86_64: ```bash -# Step 1: Install target + cross-linker +# Шаг 1: установите цель и кросс-линкер rustup target add aarch64-unknown-linux-gnu sudo apt install gcc-aarch64-linux-gnu -# Step 2: Configure linker in .cargo/config.toml (see above) +# Шаг 2: настройте линкер в .cargo/config.toml (см. выше) -# Step 3: Build +# Шаг 3: сборка cargo build --release --target aarch64-unknown-linux-gnu -# Step 4: Verify the binary +# Шаг 4: проверяем бинарник file target/aarch64-unknown-linux-gnu/release/diag_tool # → ELF 64-bit LSB executable, ARM aarch64 ``` -**Running tests for the target architecture** requires either: -- An actual ARM machine -- QEMU user-mode emulation +**Для запуска тестов под целевую архитектуру** нужно одно из двух: +- Реальная ARM-машина +- Эмуляция QEMU в пользовательском режиме ```bash -# Install QEMU user-mode (runs ARM binaries on x86_64) +# Устанавливаем QEMU в пользовательском режиме (запускает ARM-бинарники на x86_64) sudo apt install qemu-user qemu-user-static binfmt-support -# Now cargo test can run cross-compiled tests through QEMU +# Теперь cargo test может запускать кросс-скомпилированные тесты через QEMU cargo test --target aarch64-unknown-linux-gnu -# (Slow — each test binary is emulated. Use for CI validation, not daily dev.) +# (Медленно — каждый тестовый бинарник эмулируется. Используйте для проверки в CI, а не в ежедневной разработке.) ``` -Configure QEMU as the test runner in `.cargo/config.toml`: +Настройте QEMU как тестовый раннер в `.cargo/config.toml`: ```toml [target.aarch64-unknown-linux-gnu] @@ -185,108 +185,107 @@ linker = "aarch64-linux-gnu-gcc" runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu" ``` -### The `cross` Tool — Docker-Based Cross-Compilation +### Инструмент `cross` — кросс-компиляция на основе Docker -The [`cross`](https://github.com/cross-rs/cross) tool provides a zero-setup -cross-compilation experience using pre-configured Docker images: +Инструмент [`cross`](https://github.com/cross-rs/cross) даёт кросс-компиляцию без +настройки, используя готовые Docker-образы с предварительно настроенными тулчейнами: ```bash -# Install cross (from crates.io — stable releases) +# Установка cross (с crates.io — стабильные релизы) cargo install cross -# Or from git for latest features (less stable): +# Или из git для новейших возможностей (менее стабильно): # cargo install cross --git https://github.com/cross-rs/cross -# Cross-compile — no toolchain setup needed! +# Кросс-компиляция — настраивать тулчейн не нужно! cross build --release --target aarch64-unknown-linux-gnu cross build --release --target x86_64-unknown-linux-musl cross build --release --target armv7-unknown-linux-gnueabihf -# Cross-test — QEMU included in the Docker image +# Кросс-тестирование — QEMU входит в состав Docker-образа cross test --target aarch64-unknown-linux-gnu ``` -**How it works**: `cross` replaces `cargo` and runs the build inside a Docker -container that has the correct cross-compilation toolchain pre-installed. Your -source is mounted into the container, and the output goes to your normal `target/` -directory. +**Как это работает**: `cross` заменяет `cargo` и запускает сборку внутри Docker-контейнера, +в котором уже установлен нужный кросс-тулчейн. Ваш исходный код монтируется в контейнер, +а результат попадает в обычную директорию `target/`. -**Customizing the Docker image** with `Cross.toml`: +**Настройка Docker-образа** через `Cross.toml`: ```toml # Cross.toml [target.aarch64-unknown-linux-gnu] -# Use a custom Docker image with extra system libraries +# Используем собственный Docker-образ с дополнительными системными библиотеками image = "my-registry/cross-aarch64:latest" -# Pre-install system packages +# Предустановка системных пакетов pre-build = [ "dpkg --add-architecture arm64", "apt-get update && apt-get install -y libpci-dev:arm64" ] [target.aarch64-unknown-linux-gnu.env] -# Pass environment variables into the container +# Передаём переменные окружения в контейнер passthrough = ["CI", "GITHUB_TOKEN"] ``` -`cross` requires Docker (or Podman) but eliminates the need to manually install -cross-compilers, sysroots, and QEMU. It's the recommended approach for CI. +Для `cross` нужен Docker (или Podman), но он избавляет от ручной установки +кросс-компиляторов, sysroot и QEMU. Это рекомендуемый подход для CI. -### Using Zig as a Cross-Compilation Linker +### Zig как кросс-линкер -[Zig](https://ziglang.org/) bundles a C compiler and cross-compilation sysroot -for ~40 targets in a single ~40 MB download. This makes it a remarkably convenient -cross-linker for Rust: +[Zig](https://ziglang.org/) включает в один архив размером около 40 МБ C-компилятор и +sysroot для кросс-компиляции примерно под 40 целей. Это делает его удобным +кросс-линкером для Rust: ```bash -# Install Zig (single binary, no package manager needed) -# Download from https://ziglang.org/download/ -# Or via package manager: +# Установка Zig (один бинарник, менеджер пакетов не нужен) +# Скачайте с https://ziglang.org/download/ +# Или через менеджер пакетов: sudo snap install zig --classic --beta # Ubuntu brew install zig # macOS -# Install cargo-zigbuild +# Установка cargo-zigbuild cargo install cargo-zigbuild ``` -**Why Zig?** The key advantage is **glibc version targeting**. Zig lets you specify -the exact glibc version to link against, ensuring your binary runs on older Linux -distributions: +**Зачем Zig?** Главное преимущество — **выбор версии glibc**. Zig позволяет указать +точную версию glibc для линковки, гарантируя, что бинарник запустится на старых +дистрибутивах Linux: ```bash -# Build for glibc 2.17 (CentOS 7 / RHEL 7 compatibility) +# Сборка под glibc 2.17 (совместимость с CentOS 7 / RHEL 7) cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17 -# Build for aarch64 with glibc 2.28 (Ubuntu 18.04+) +# Сборка под aarch64 с glibc 2.28 (Ubuntu 18.04+) cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28 -# Build for musl (fully static) +# Сборка под musl (полностью статическая) cargo zigbuild --release --target x86_64-unknown-linux-musl ``` -The `.2.17` suffix is a Zig extension — it tells Zig's linker to use glibc 2.17 -symbol versions, so the resulting binary runs on CentOS 7 and later. No Docker, -no sysroot management, no cross-compiler installation. +Суффикс `.2.17` — расширение Zig: он указывает линкеру использовать версии символов +glibc 2.17, благодаря чему полученный бинарник работает на CentOS 7 и более поздних +системах. Без Docker, без управления sysroot и без установки кросс-компиляторов. -**Comparison: cross vs cargo-zigbuild vs manual:** +**Сравнение: cross, cargo-zigbuild и ручной способ:** -| Feature | Manual | cross | cargo-zigbuild | -|---------|--------|-------|----------------| -| Setup effort | High (install toolchain per target) | Low (needs Docker) | Low (single binary) | -| Docker required | No | Yes | No | -| glibc version targeting | No (uses host glibc) | No (uses container glibc) | Yes (exact version) | -| Test execution | Needs QEMU | Included | Needs QEMU | -| macOS → Linux | Difficult | Easy | Easy | -| Linux → macOS | Very difficult | Not supported | Limited | -| Binary size overhead | None | None | None | +| Возможность | Вручную | cross | cargo-zigbuild | +|-------------|---------|-------|----------------| +| Усилия на настройку | Высокие (тулчейн на каждую цель) | Низкие (нужен Docker) | Низкие (один бинарник) | +| Нужен Docker | Нет | Да | Нет | +| Выбор версии glibc | Нет (используется glibc хоста) | Нет (используется glibc контейнера) | Да (точная версия) | +| Запуск тестов | Нужен QEMU | Входит в состав | Нужен QEMU | +| macOS → Linux | Сложно | Легко | Легко | +| Linux → macOS | Очень сложно | Не поддерживается | Ограниченно | +| Накладные расходы на размер бинарника | Нет | Нет | Нет | -### CI Pipeline: GitHub Actions Matrix +### CI-конвейер: матрица GitHub Actions -A production-grade CI workflow that builds for multiple targets: +Продакшн-уровень CI-workflow, который собирает проект под несколько целей: ```yaml # .github/workflows/cross-build.yml -name: Cross-Platform Build +name: Кросс-платформенная сборка on: [push, pull_request] @@ -313,7 +312,7 @@ jobs: name: windows-x86_64 runs-on: ${{ matrix.os }} - name: Build (${{ matrix.name }}) + name: Сборка (${{ matrix.name }}) steps: - uses: actions/checkout@v4 @@ -322,113 +321,113 @@ jobs: with: targets: ${{ matrix.target }} - - name: Install musl tools + - name: Установка инструментов musl if: matrix.target == 'x86_64-unknown-linux-musl' run: sudo apt-get install -y musl-tools - - name: Install cross + - name: Установка cross if: matrix.use_cross run: cargo install cross - - name: Build (native) + - name: Сборка (нативная) if: "!matrix.use_cross" run: cargo build --release --target ${{ matrix.target }} - - name: Build (cross) + - name: Сборка (cross) if: matrix.use_cross run: cross build --release --target ${{ matrix.target }} - - name: Run tests + - name: Запуск тестов if: "!matrix.use_cross" run: cargo test --target ${{ matrix.target }} - - name: Upload artifact + - name: Загрузка артефакта uses: actions/upload-artifact@v4 with: name: diag_tool-${{ matrix.name }} path: target/${{ matrix.target }}/release/diag_tool* ``` -### Application: Multi-Architecture Server Builds +### Применение: сборки для серверов с разной архитектурой -The binary currently has no cross-compilation setup. For a hardware -diagnostics tool deployed across diverse server fleets, the recommended addition: +Сейчас у бинарника нет настройки кросс-компиляции. Для инструмента диагностики +оборудования, который развёртывается на разнородном парке серверов, рекомендуется добавить: ```text my_workspace/ ├── .cargo/ -│ └── config.toml ← linker configs per target -├── Cross.toml ← cross tool configuration +│ └── config.toml ← конфигурация линкеров для каждой цели +├── Cross.toml ← конфигурация инструмента cross └── .github/workflows/ - └── cross-build.yml ← CI matrix for 3 targets + └── cross-build.yml ← матрица CI для 3 целей ``` -**Recommended `.cargo/config.toml`:** +**Рекомендуемый `.cargo/config.toml`:** ```toml -# .cargo/config.toml for the project +# .cargo/config.toml для проекта -# Release profile optimizations (already in Cargo.toml, shown for reference) +# Оптимизации профиля релиза (уже есть в Cargo.toml, показано для справки) # [profile.release] # lto = true # codegen-units = 1 # panic = "abort" # strip = true -# aarch64 for ARM servers (Graviton, Ampere, Grace) +# aarch64 для ARM-серверов (Graviton, Ampere, Grace) [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" -# musl for portable static binaries +# musl для переносимых статических бинарников [target.x86_64-unknown-linux-musl] linker = "musl-gcc" ``` -**Recommended build targets:** +**Рекомендуемые цели сборки:** -| Target | Use Case | Deploy To | -|--------|----------|-----------| -| `x86_64-unknown-linux-gnu` | Default native build | Standard x86 servers | -| `x86_64-unknown-linux-musl` | Static binary, any distro | Containers, minimal hosts | -| `aarch64-unknown-linux-gnu` | ARM servers | Graviton, Ampere, Grace | +| Цель | Сценарий | Куда развёртывать | +|------|----------|-------------------| +| `x86_64-unknown-linux-gnu` | Стандартная нативная сборка | Обычные x86-серверы | +| `x86_64-unknown-linux-musl` | Статический бинарник, любой дистрибутив | Контейнеры, минимальные хосты | +| `aarch64-unknown-linux-gnu` | ARM-серверы | Graviton, Ampere, Grace | -> **Key insight**: The `[profile.release]` in the workspace's root `Cargo.toml` -> already has `lto = true`, `codegen-units = 1`, `panic = "abort"`, and -> `strip = true` — an ideal release profile for cross-compiled deployment binaries -> (see [Release Profiles](ch07-release-profiles-and-binary-size.md) for the full impact table). -> Combined with musl, this produces a single ~10 MB static binary with no runtime -> dependencies. +> **Ключевая мысль**: секция `[profile.release]` в корневом `Cargo.toml` воркспейса уже +> содержит `lto = true`, `codegen-units = 1`, `panic = "abort"` и `strip = true` — это +> идеальный профиль релиза для кросс-скомпилированных бинарников, которые развёртываются +> (полную таблицу влияния см. в разделе [Профили релиза](ch07-release-profiles-and-binary-size.md)). +> В сочетании с musl это даёт один статический бинарник размером около 10 МБ без +> зависимостей во время выполнения. -### Troubleshooting Cross-Compilation +### Устранение неполадок при кросс-компиляции -| Symptom | Cause | Fix | -|---------|-------|-----| -| `linker 'aarch64-linux-gnu-gcc' not found` | Missing cross-linker toolchain | `sudo apt install gcc-aarch64-linux-gnu` | -| `cannot find -lssl` (musl target) | System OpenSSL is glibc-linked | Use `vendored` feature: `openssl = { version = "0.10", features = ["vendored"] }` | -| `build.rs` runs wrong binary | build.rs runs on HOST, not target | Check `CARGO_CFG_TARGET_OS` in build.rs, not `cfg!(target_os)` | -| Tests pass locally, fail in `cross` | Docker image missing test fixtures | Mount test data via `Cross.toml`: `[build.env] volumes = ["./TestArea:/TestArea"]` | -| `undefined reference to __cxa_thread_atexit_impl` | Old glibc on target | Use `cargo-zigbuild` with explicit glibc version: `--target x86_64-unknown-linux-gnu.2.17` | -| Binary segfaults on ARM | Compiled for wrong ARM variant | Verify target triple matches hardware: `aarch64-unknown-linux-gnu` for 64-bit ARM | -| `GLIBC_2.XX not found` at runtime | Build machine has newer glibc | Use musl for static builds, or `cargo-zigbuild` for glibc version pinning | +| Симптом | Причина | Решение | +|---------|---------|---------| +| `linker 'aarch64-linux-gnu-gcc' not found` | Не установлен тулчейн кросс-линкера | `sudo apt install gcc-aarch64-linux-gnu` | +| `cannot find -lssl` (цель musl) | Системный OpenSSL слинкован с glibc | Используйте фичу `vendored`: `openssl = { version = "0.10", features = ["vendored"] }` | +| `build.rs` запускает не тот бинарник | build.rs выполняется на HOST, а не на целевой платформе | Проверяйте `CARGO_CFG_TARGET_OS` в build.rs, а не `cfg!(target_os)` | +| Тесты проходят локально, но падают в `cross` | В Docker-образе нет тестовых данных | Подключите данные через `Cross.toml`: `[build.env] volumes = ["./TestArea:/TestArea"]` | +| `undefined reference to __cxa_thread_atexit_impl` | На целевой машине старая glibc | Используйте `cargo-zigbuild` с явной версией glibc: `--target x86_64-unknown-linux-gnu.2.17` | +| Бинарник падает с segfault на ARM | Собран под другой вариант ARM | Убедитесь, что target triple соответствует железу: `aarch64-unknown-linux-gnu` для 64-битного ARM | +| `GLIBC_2.XX not found` во время выполнения | На машине сборки более новая glibc | Используйте musl для статических сборок или `cargo-zigbuild` для фиксации версии glibc | -### Cross-Compilation Decision Tree +### Дерево решений для кросс-компиляции ```mermaid flowchart TD - START["Need to cross-compile?"] --> STATIC{"Static binary?"} - - STATIC -->|Yes| MUSL["musl target
--target x86_64-unknown-linux-musl"] - STATIC -->|No| GLIBC{"Need old glibc?"} - - GLIBC -->|Yes| ZIG["cargo-zigbuild
--target x86_64-unknown-linux-gnu.2.17"] - GLIBC -->|No| ARCH{"Target arch?"} - - ARCH -->|"Same arch"| NATIVE["Native toolchain
rustup target add + linker"] - ARCH -->|"ARM/other"| DOCKER{"Docker available?"} - - DOCKER -->|Yes| CROSS["cross build
Docker-based, zero setup"] - DOCKER -->|No| MANUAL["Manual sysroot
apt install gcc-aarch64-linux-gnu"] - + START["Нужна кросс-компиляция?"] --> STATIC{"Статический бинарник?"} + + STATIC -->|Да| MUSL["Цель musl
--target x86_64-unknown-linux-musl"] + STATIC -->|Нет| GLIBC{"Нужна старая glibc?"} + + GLIBC -->|Да| ZIG["cargo-zigbuild
--target x86_64-unknown-linux-gnu.2.17"] + GLIBC -->|Нет| ARCH{"Архитектура цели?"} + + ARCH -->|"Та же архитектура"| NATIVE["Нативный тулчейн
rustup target add + линкер"] + ARCH -->|"ARM/другая"| DOCKER{"Доступен Docker?"} + + DOCKER -->|Да| CROSS["cross build
на базе Docker, без настройки"] + DOCKER -->|Нет| MANUAL["Ручной sysroot
apt install gcc-aarch64-linux-gnu"] + style MUSL fill:#91e5a3,color:#000 style ZIG fill:#91e5a3,color:#000 style CROSS fill:#91e5a3,color:#000 @@ -436,38 +435,41 @@ flowchart TD style MANUAL fill:#ffd43b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Static musl Binary +#### 🟢 Упражнение 1: статический бинарник musl -Build any Rust binary for `x86_64-unknown-linux-musl`. Verify it's statically linked using `file` and `ldd`. +Соберите любой бинарник Rust для `x86_64-unknown-linux-musl`. Проверьте, что он +статически слинкован, с помощью `file` и `ldd`.
-Solution +Решение ```bash rustup target add x86_64-unknown-linux-musl cargo new hello-static && cd hello-static cargo build --release --target x86_64-unknown-linux-musl -# Verify +# Проверка file target/x86_64-unknown-linux-musl/release/hello-static -# Output: ... statically linked ... +# Вывод: ... statically linked ... ldd target/x86_64-unknown-linux-musl/release/hello-static -# Output: not a dynamic executable +# Вывод: not a dynamic executable ```
-#### 🟡 Exercise 2: GitHub Actions Cross-Build Matrix +#### 🟡 Упражнение 2: матрица GitHub Actions для кросс-сборки -Write a GitHub Actions workflow that builds a Rust project for three targets: `x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, and `aarch64-unknown-linux-gnu`. Use a matrix strategy. +Напишите workflow GitHub Actions, который собирает проект на Rust для трёх целей: +`x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl` и `aarch64-unknown-linux-gnu`. +Используйте стратегию matrix.
-Solution +Решение ```yaml -name: Cross-build +name: Кросс-сборка on: [push] jobs: build: @@ -483,9 +485,9 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} - - name: Install cross + - name: Установка cross run: cargo install cross --locked - - name: Build + - name: Сборка run: cross build --release --target ${{ matrix.target }} - uses: actions/upload-artifact@v4 with: @@ -494,13 +496,15 @@ jobs: ```
-### Key Takeaways +### Ключевые выводы -- Rust's `rustc` is already a cross-compiler — you just need the right target and linker -- **musl** produces fully static binaries with zero runtime dependencies — ideal for containers -- **`cargo-zigbuild`** solves the "which glibc version" problem for enterprise Linux targets -- **`cross`** is the easiest path for ARM and other exotic targets — Docker handles the sysroot -- Always test with `file` and `ldd` to verify the binary matches your deployment target +- `rustc` уже является кросс-компилятором — нужны только правильная цель и линкер +- **musl** создаёт полностью статические бинарники без зависимостей во время выполнения — + идеально для контейнеров +- **`cargo-zigbuild`** решает проблему «какую версию glibc выбрать» для корпоративных + дистрибутивов Linux +- **`cross`** — самый простой путь для ARM и других экзотических целей: Docker берёт на себя sysroot +- Всегда проверяйте бинарник через `file` и `ldd`, чтобы убедиться, что он соответствует + целевой платформе развёртывания --- - diff --git a/engineering-book/src/ch03-benchmarking-measuring-what-matters.md b/engineering-book/src/ch03-benchmarking-measuring-what-matters.md index f6b72fa..209fe3d 100644 --- a/engineering-book/src/ch03-benchmarking-measuring-what-matters.md +++ b/engineering-book/src/ch03-benchmarking-measuring-what-matters.md @@ -1,58 +1,58 @@ -# Benchmarking — Measuring What Matters 🟡 +# Бенчмаркинг — измеряем то, что важно 🟡 -> **What you'll learn:** -> - Why naive timing with `Instant::now()` produces unreliable results -> - Statistical benchmarking with Criterion.rs and the lighter Divan alternative -> - Profiling hot spots with `perf`, flamegraphs, and PGO -> - Setting up continuous benchmarking in CI to catch regressions automatically +> **Чему вы научитесь:** +> - Почему наивный замер через `Instant::now()` даёт ненадёжные результаты +> - Статистический бенчмаркинг с Criterion.rs и более лёгкой альтернативой — Divan +> - Профилирование горячих точек с помощью `perf`, флеймграфов и PGO +> - Настройка непрерывного бенчмаркинга в CI для автоматического обнаружения регрессий > -> **Cross-references:** [Release Profiles](ch07-release-profiles-and-binary-size.md) — once you find the hot spot, optimize the binary · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) — benchmark job in the pipeline · [Code Coverage](ch04-code-coverage-seeing-what-tests-miss.md) — coverage tells you what's tested, benchmarks tell you what's fast +> **Перекрёстные ссылки:** [Профили релиза](ch07-release-profiles-and-binary-size.md) — когда горячая точка найдена, оптимизируйте бинарник · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) — джоба с бенчмарками в конвейере · [Покрытие кода](ch04-code-coverage-seeing-what-tests-miss.md) — покрытие показывает, что протестировано, а бенчмарки — что работает быстро -"We should forget about small efficiencies, say about 97% of the time: premature -optimization is the root of all evil. Yet we should not pass up our opportunities -in that critical 3%." — Donald Knuth +«Мы должны забыть о мелкой эффективности примерно в 97% случаев: преждевременная +оптимизация — корень всех зол. Однако не стоит упускать возможности в тех критических +3%.» — Дональд Кнут -The hard part isn't *writing* benchmarks — it's writing benchmarks that produce -**meaningful, reproducible, actionable** numbers. This chapter covers the tools -and techniques that get you from "it seems fast" to "we have statistical evidence -that PR #347 regressed parsing throughput by 4.2%." +Самое трудное — не *написать* бенчмарки, а написать такие, которые дают **осмысленные, +воспроизводимые и пригодные для действий** числа. Эта глава описывает инструменты и приёмы, +которые переводят вас от фразы «кажется, быстро» к «у нас есть статистическое подтверждение, +что PR #347 снизил пропускную способность парсинга на 4,2%». -### Why Not `std::time::Instant`? +### Почему не `std::time::Instant`? -The temptation: +Соблазн: ```rust -// ❌ Naive benchmarking — unreliable results +// ❌ Наивный бенчмарк — ненадёжные результаты use std::time::Instant; fn main() { let start = Instant::now(); let result = parse_device_query_output(&sample_data); let elapsed = start.elapsed(); - println!("Parsing took {:?}", elapsed); - // Problem 1: Compiler may optimize away `result` (dead code elimination) - // Problem 2: Single sample — no statistical significance - // Problem 3: CPU frequency scaling, thermal throttling, other processes - // Problem 4: Cold cache vs warm cache not controlled + println!("Разбор занял {:?}", elapsed); + // Проблема 1: компилятор может оптимизировать `result` (исключение мёртвого кода) + // Проблема 2: одно измерение — нет статистической значимости + // Проблема 3: масштабирование частоты CPU, тепловое ограничение, другие процессы + // Проблема 4: холодный и горячий кеш не контролируются } ``` -Problems with manual timing: -1. **Dead code elimination** — the compiler may skip the computation entirely if - the result isn't used. -2. **No warm-up** — the first run includes cache misses, JIT effects (irrelevant - in Rust, but OS page faults apply), and lazy initialization. -3. **No statistical analysis** — a single measurement tells you nothing about - variance, outliers, or confidence intervals. -4. **No regression detection** — you can't compare against previous runs. +Проблемы ручного замера: +1. **Исключение мёртвого кода** — компилятор может вообще пропустить вычисление, если + результат не используется. +2. **Нет прогрева** — первый запуск включает промахи кеша, ошибки страниц ОС и ленивую + инициализацию. (JIT-эффекты к Rust неприменимы, а вот ошибки страниц ОС — применимы.) +3. **Нет статистического анализа** — одно измерение ничего не говорит о разбросе, + выбросах или доверительных интервалах. +4. **Нет обнаружения регрессий** — нельзя сравнить с предыдущими запусками. -### Criterion.rs — Statistical Benchmarking +### Criterion.rs — статистический бенчмаркинг -[Criterion.rs](https://bheisler.github.io/criterion.rs/book/) is the de facto -standard for Rust micro-benchmarks. It uses statistical methods to produce -reliable measurements and detects performance regressions automatically. +[Criterion.rs](https://bheisler.github.io/criterion.rs/book/) — де-факто стандарт +для микробенчмарков в Rust. Он использует статистические методы, чтобы получать надёжные +измерения, и автоматически обнаруживает регрессии производительности. -**Setup:** +**Настройка:** ```toml # Cargo.toml @@ -61,16 +61,16 @@ criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support" [[bench]] name = "parsing_bench" -harness = false # Use Criterion's harness, not the built-in test harness +harness = false # Используем harness Criterion, а не встроенный тестовый harness ``` -**A complete benchmark:** +**Полный бенчмарк:** ```rust // benches/parsing_bench.rs use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId}; -/// Data type for parsed GPU information +/// Тип данных для разобранной информации о GPU #[derive(Debug, Clone)] struct GpuInfo { index: u32, @@ -79,7 +79,7 @@ struct GpuInfo { power_w: f64, } -/// The function under test — simulate parsing device-query CSV output +/// Тестируемая функция — имитирует разбор CSV-вывода device-query fn parse_gpu_csv(input: &str) -> Vec { input .lines() @@ -101,7 +101,7 @@ fn parse_gpu_csv(input: &str) -> Vec { } fn bench_parse_gpu_csv(c: &mut Criterion) { - // Representative test data + // Репрезентативные тестовые данные let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\ 1, Acme Accel-V1-80GB, 34, 67.2\n"; @@ -122,33 +122,33 @@ criterion_group!(benches, bench_parse_gpu_csv); criterion_main!(benches); ``` -**Running and reading results:** +**Запуск и чтение результатов:** ```bash -# Run all benchmarks +# Запустить все бенчмарки cargo bench -# Run a specific benchmark by name +# Запустить конкретный бенчмарк по имени cargo bench -- parse_64 -# Output: +# Вывод: # parse_2_gpus time: [1.2345 µs 1.2456 µs 1.2578 µs] # ▲ ▲ ▲ -# │ confidence interval -# lower 95% median upper 95% +# │ доверительный интервал +# нижняя 95% медиана верхняя 95% # # parse_64_gpus time: [38.123 µs 38.456 µs 38.812 µs] # change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05) # No change in performance detected. ``` -**What `black_box()` does**: It's a compiler hint that prevents dead-code -elimination and over-aggressive constant folding. The compiler cannot see -through `black_box`, so it must actually compute the result. +**Что делает `black_box()`**: это подсказка компилятору, которая не даёт исключить мёртвый +код и слишком агрессивно сворачивать константы. Компилятор не может «заглянуть» сквозь +`black_box`, поэтому ему приходится действительно вычислить результат. -### Parameterized Benchmarks and Benchmark Groups +### Параметризованные бенчмарки и группы бенчмарков -Compare multiple implementations or input sizes: +Сравнение нескольких реализаций или размеров входных данных: ```rust // benches/comparison_bench.rs @@ -157,11 +157,11 @@ use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Through fn bench_parsing_strategies(c: &mut Criterion) { let mut group = c.benchmark_group("csv_parsing"); - // Test across different input sizes + // Тестируем на разных размерах входных данных for num_gpus in [1, 8, 32, 64, 128] { let input = generate_gpu_csv(num_gpus); - // Set throughput for bytes-per-second reporting + // Задаём пропускную способность для отчёта в байтах в секунду group.throughput(Throughput::Bytes(input.len() as u64)); group.bench_with_input( @@ -189,13 +189,13 @@ criterion_group!(benches, bench_parsing_strategies); criterion_main!(benches); ``` -**Output**: Criterion generates an HTML report at `target/criterion/report/index.html` -with violin plots, comparison charts, and regression analysis — open in a browser. +**Вывод**: Criterion создаёт HTML-отчёт `target/criterion/report/index.html` с violin-графиками, +сравнительными диаграммами и анализом регрессий — откройте его в браузере. -### Divan — A Lighter Alternative +### Divan — более лёгкая альтернатива -[Divan](https://github.com/nvzqz/divan) is a newer benchmarking framework that -uses attribute macros instead of Criterion's macro DSL: +[Divan](https://github.com/nvzqz/divan) — более новый фреймворк для бенчмаркинга, который +использует атрибутные макросы вместо DSL-макросов Criterion: ```toml # Cargo.toml @@ -235,140 +235,152 @@ fn parse_n_gpus(n: usize) -> Vec { parse_gpu_csv(black_box(&input)) } -// Divan output is a clean table: +// Вывод Divan — аккуратная таблица: // ╰─ parse_2_gpus fastest │ slowest │ median │ mean │ samples │ iters // 1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100 │ 1600 ``` -**When to choose Divan over Criterion:** -- Simpler API (attribute macros, less boilerplate) -- Faster compilation (fewer dependencies) -- Good for quick perf checks during development +**Когда выбирать Divan вместо Criterion:** +- Проще API (атрибутные макросы, меньше шаблонного кода) +- Быстрее компиляция (меньше зависимостей) +- Хорошо подходит для быстрых проверок производительности во время разработки -**When to choose Criterion:** -- Statistical regression detection across runs -- HTML reports with charts -- Established ecosystem, more CI integrations +**Когда выбирать Criterion:** +- Статистическое обнаружение регрессий между запусками +- HTML-отчёты с графиками +- Устоявшаяся экосистема, больше интеграций с CI -### Profiling with `perf` and Flamegraphs +### Профилирование с `perf` и флеймграфами -Benchmarks tell you *how fast* — profiling tells you *where the time goes*. +Бенчмарки показывают *насколько быстро*, а профилирование — *куда уходит время*. ```bash -# Step 1: Build with debug info (release speed, debug symbols) +# Шаг 1: сборка с отладочной информацией (скорость release, отладочные символы) cargo build --release -# Ensure debug info is available: +# Убедитесь, что отладочная информация доступна: # [profile.release] -# debug = true # Add this temporarily for profiling +# debug = true # Добавьте временно для профилирования -# Step 2: Record with perf +# Шаг 2: запись с помощью perf perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics -# Step 3: Generate a flamegraph -# Install: cargo install flamegraph -# Install: cargo install addr2line --features=bin (optional, speedup cargo-flamegraph) +# Шаг 3: построение флеймграфа +# Установка: cargo install flamegraph +# Установка: cargo install addr2line --features=bin (необязательно, ускоряет cargo-flamegraph) cargo flamegraph --root -- --run-diagnostics -# Opens an interactive SVG flamegraph +# Откроется интерактивный SVG-флеймграф -# Alternative: use perf + inferno +# Альтернатива: perf + inferno perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg ``` -**Reading a flamegraph:** -- **Width** = time spent in that function (wider = slower) -- **Height** = call stack depth (taller ≠ slower, just deeper) -- **Bottom** = entry point, **Top** = leaf functions doing actual work -- Look for wide plateaus at the top — those are your hot spots +**Как читать флеймграф:** +- **Ширина** = время, проведённое в функции (шире = медленнее) +- **Высота** = глубина стека вызовов (выше ≠ медленнее, просто глубже) +- **Низ** = точка входа, **верх** = листовые функции, которые выполняют реальную работу +- Ищите широкие «плато» наверху — это и есть ваши горячие точки -### Profile-Guided Optimization (PGO) +### Оптимизация по профилю (PGO) -Profile-Guided Optimization (PGO) is a compiler optimization technique for improving performance of CPU-intensive applications. The basic concept of PGO is to collect data about the typical execution of a program (e.g. which branches it is likely to take) and then use this data to inform optimizations such as inlining, machine-code layout, register allocation, etc. +Profile-Guided Optimization (PGO) — техника оптимизации компилятора, которая повышает +производительность приложений, интенсивно использующих процессор. Основная идея PGO — собрать +данные о типичном выполнении программы (например, какие ветвления она выбирает чаще всего), +а затем использовать эти данные для таких оптимизаций, как инлайнинг, размещение машинного +кода, распределение регистров и т. д. -There are different ways of collecting data about a program’s execution. One is to run the program inside a profiler (such as `perf`) and another is to create an instrumented binary, that is, a binary that has data collection built into it, and run that. The latter usually provides more accurate data and it is also what is supported by Rustc. +Данные о выполнении программы можно собирать двумя способами. Первый — запустить программу +под профилировщиком (например, `perf`). Второй — собрать инструментированный бинарник, то есть +бинарник со встроенным сбором данных, и запустить его. Второй способ обычно даёт более точные +данные, и именно он поддерживается rustc. -Below there is an example of instrumentation-based PGO: +Ниже приведён пример PGO на основе инструментирования: ```bash -# Step 1: Build with instrumentation +# Шаг 1: сборка с инструментированием RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release -# Step 2: Run representative workloads -./target/release/diag_tool --run-full # generates profiling data +# Шаг 2: запуск репрезентативных рабочих нагрузок +./target/release/diag_tool --run-full # генерирует данные профилирования -# Step 3: Merge profiling data -# Use the llvm-profdata that matches rustc's LLVM version: +# Шаг 3: объединение данных профилирования +# Используйте llvm-profdata, соответствующий версии LLVM в rustc: # $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata -# Or if llvm-tools is installed: rustup component add llvm-tools +# Или если установлен llvm-tools: rustup component add llvm-tools llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/ -# Step 4: Rebuild with profiling feedback +# Шаг 4: пересборка с обратной связью от профилирования RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release -# Typical improvement: 5-20% for compute-bound code (parsing, crypto, codegen). -# I/O-bound or syscall-heavy code (like a large project) will see much less benefit -# because the CPU is mostly waiting, not executing hot loops. +# Типичный прирост: 5–20% для вычислительно нагруженного кода (парсинг, криптография, кодогенерация). +# Код, ограниченный I/O или системными вызовами (как большой проект), получит гораздо +# меньший выигрыш, потому что процессор в основном ждёт, а не выполняет горячие циклы. ``` -As an alternative to directly using the compiler for PGO, you may choose to go with [cargo-pgo](https://github.com/kobzol/cargo-pgo), which has an intuitive command-line API and saves you the trouble of doing all the manual work. +Вместо прямого использования компилятора для PGO можно воспользоваться +[cargo-pgo](https://github.com/kobzol/cargo-pgo) — у него интуитивный интерфейс командной +строки, и он избавляет от всей ручной работы. -With `cargo-pgo`, the optimization workflow from above can look like that: +С `cargo-pgo` процесс оптимизации из примера выше выглядит так: ```bash -# Step 1: Build with instrumentation +# Шаг 1: сборка с инструментированием cargo pgo build -# Step 2: Run representative workloads +# Шаг 2: запуск репрезентативных рабочих нагрузок cargo pgo run -- --run-full -# Step 3: Rebuild with profiling feedback +# Шаг 3: пересборка с обратной связью от профилирования cargo pgo optimize ``` -Sampling PGO or SPGO is a more complicated way to perform PGO in a price of reduced runtime overhead compared to instrumentation-based PGO. For now, the best place to read about it is the Clang PGO [manual](https://clang.llvm.org/docs/UsersManual.html#using-sampling-profilers). +Sampling PGO (SPGO) — более сложный способ выполнить PGO, зато с меньшими накладными +расходами во время выполнения по сравнению с PGO на основе инструментирования. Пока лучшее +место, чтобы почитать о нём, — [руководство по PGO для Clang](https://clang.llvm.org/docs/UsersManual.html#using-sampling-profilers). -> **Tip**: Before spending time on PGO, ensure your [release profile](ch07-release-profiles-and-binary-size.md) -> already has LTO enabled — it typically delivers a bigger win for less effort. +> **Совет**: прежде чем тратить время на PGO, убедитесь, что в вашем +> [профиле релиза](ch07-release-profiles-and-binary-size.md) уже включён LTO — обычно это +> даёт больший эффект при меньших усилиях. -Further reading: +Дополнительная литература: -* Official Rustc [guide](https://doc.rust-lang.org/rustc/profile-guided-optimization.html) about PGO. -* [Awesome PGO](https://github.com/zamazan4ik/awesome-pgo) - a collection of PGO benchmarks for real applications, including PGO guides for different compilers (including Sampling PGO) -* [LLVM BOLT](https://github.com/llvm/llvm-project/blob/main/bolt/README.md) - Post-Link Optimization (PLO) optimization technique. PLO can be used for performing additional optimizations even after applying PGO for getting better performance. `cargo-pgo` supports `llvm-bolt` too. +* Официальное [руководство](https://doc.rust-lang.org/rustc/profile-guided-optimization.html) rustc по PGO. +* [Awesome PGO](https://github.com/zamazan4ik/awesome-pgo) — коллекция бенчмарков PGO для реальных приложений, включая руководства по PGO для разных компиляторов (в том числе Sampling PGO) +* [LLVM BOLT](https://github.com/llvm/llvm-project/blob/main/bolt/README.md) — оптимизация после линковки (Post-Link Optimization, PLO). PLO можно применять для дополнительных оптимизаций даже после PGO, чтобы получить ещё лучшую производительность. `cargo-pgo` также поддерживает `llvm-bolt`. -### `hyperfine` — Quick End-to-End Timing +### `hyperfine` — быстрый сквозной замер -[`hyperfine`](https://github.com/sharkdp/hyperfine) benchmarks entire commands, -not individual functions. It's perfect for measuring overall binary performance: +[`hyperfine`](https://github.com/sharkdp/hyperfine) замеряет целые команды, а не отдельные +функции. Он идеально подходит для измерения общей производительности бинарника: ```bash -# Install +# Установка cargo install hyperfine -# Or: sudo apt install hyperfine (Ubuntu 23.04+) +# Или: sudo apt install hyperfine (Ubuntu 23.04+) -# Basic benchmark +# Базовый бенчмарк hyperfine './target/release/diag_tool --run-diagnostics' -# Compare two implementations +# Сравнение двух реализаций hyperfine './target/release/diag_tool_v1 --run-diagnostics' \ './target/release/diag_tool_v2 --run-diagnostics' -# Warm-up runs + minimum iterations +# Прогревочные запуски + минимальное количество итераций hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all' -# Export results as JSON for CI comparison +# Экспорт результатов в JSON для сравнения в CI hyperfine --export-json bench.json './target/release/diag_tool --run-all' ``` -**When to use `hyperfine` vs Criterion:** -- `hyperfine`: whole-binary timing, comparing before/after a refactor, I/O-bound workloads -- Criterion: micro-benchmarks of individual functions, statistical regression detection +**Когда использовать `hyperfine`, а когда Criterion:** +- `hyperfine`: замер всего бинарника, сравнение до и после рефакторинга, нагрузки, ограниченные I/O +- Criterion: микробенчмарки отдельных функций, статистическое обнаружение регрессий -### Continuous Benchmarking in CI +### Непрерывный бенчмаркинг в CI -Detect performance regressions before they ship: +Обнаруживайте регрессии производительности до того, как они попадут в релиз: ```yaml # .github/workflows/bench.yml -name: Benchmarks +name: Бенчмарки on: pull_request: @@ -382,46 +394,46 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - name: Run benchmarks - # Requires criterion = { features = ["cargo_bench_support"] } for --output-format + - name: Запуск бенчмарков + # Требуется criterion = { features = ["cargo_bench_support"] } для --output-format run: cargo bench -- --output-format bencher | tee bench_output.txt - - name: Store benchmark result + - name: Сохранение результата бенчмарка uses: benchmark-action/github-action-benchmark@v1 with: tool: 'cargo' output-file-path: bench_output.txt github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true - alert-threshold: '120%' # Alert if 20% slower + alert-threshold: '120%' # Оповещать, если на 20% медленнее comment-on-alert: true - fail-on-alert: true # Block PR if regression detected + fail-on-alert: true # Блокировать PR при обнаружении регрессии ``` -**Key CI considerations:** -- Use **dedicated benchmark runners** (not shared CI) for consistent results -- Pin the runner to a specific machine type if using cloud CI -- Store historical data to detect gradual regressions -- Set thresholds based on your workload's tolerance (5% for hot paths, 20% for cold) +**Ключевые моменты для CI:** +- Используйте **выделенные раннеры для бенчмарков** (а не общий CI), чтобы результаты были стабильными +- Закрепите раннер за конкретным типом машины, если используете облачный CI +- Храните исторические данные, чтобы обнаруживать постепенные регрессии +- Задавайте пороги исходя из допустимых отклонений вашей нагрузки (5% для горячих путей, 20% для холодных) -### Application: Parsing Performance +### Применение: производительность парсинга -The project has several performance-sensitive parsing paths that -would benefit from benchmarks: +В проекте есть несколько чувствительных к производительности путей парсинга, которым +пошли бы на пользу бенчмарки: -| Parsing Hot Spot | Crate | Why It Matters | -|------------------|-------|----------------| -| accelerator-query CSV/XML output | `device_diag` | Called per-GPU, up to 8× per run | -| Sensor event parsing | `event_log` | Thousands of records on busy servers | -| PCIe topology JSON | `topology_lib` | Complex nested structures, golden-file validated | -| Report JSON serialization | `diag_framework` | Final report output, size-sensitive | -| Config JSON loading | `config_loader` | Startup latency | +| Горячая точка парсинга | Крейт | Почему это важно | +|------------------------|-------|------------------| +| Вывод CSV/XML от accelerator-query | `device_diag` | Вызывается для каждого GPU, до 8× за запуск | +| Разбор событий сенсоров | `event_log` | Тысячи записей на загруженных серверах | +| JSON топологии PCIe | `topology_lib` | Сложные вложенные структуры, проверяются golden-файлами | +| Сериализация JSON-отчёта | `diag_framework` | Итоговый вывод отчёта, чувствителен к размеру | +| Загрузка JSON-конфигурации | `config_loader` | Задержка при запуске | -**Recommended first benchmark** — the topology parser, which already has golden-file -test data: +**Рекомендуемый первый бенчмарк** — парсер топологии, у которого уже есть тестовые +данные golden-файлов: ```rust -// topology_lib/benches/parse_bench.rs (proposed) +// topology_lib/benches/parse_bench.rs (предложено) use criterion::{criterion_group, criterion_main, Criterion, Throughput}; use std::fs; @@ -430,7 +442,7 @@ fn bench_topology_parse(c: &mut Criterion) { for golden_file in ["S2001", "S1015", "S1035", "S1080"] { let path = format!("tests/test_data/{golden_file}.json"); - let data = fs::read_to_string(&path).expect("golden file not found"); + let data = fs::read_to_string(&path).expect("golden-файл не найден"); group.throughput(Throughput::Bytes(data.len() as u64)); group.bench_function(golden_file, |b| { @@ -448,34 +460,34 @@ criterion_group!(benches, bench_topology_parse); criterion_main!(benches); ``` -### Try It Yourself +### Попробуйте сами -1. **Write a Criterion benchmark**: Pick any parsing function in your codebase. - Create a `benches/` directory, set up a Criterion benchmark that measures - throughput in bytes/second. Run `cargo bench` and examine the HTML report. +1. **Напишите бенчмарк Criterion**: выберите любую функцию парсинга в своей кодовой базе. + Создайте директорию `benches/`, настройте бенчмарк Criterion, который измеряет + пропускную способность в байтах в секунду. Запустите `cargo bench` и изучите HTML-отчёт. -2. **Generate a flamegraph**: Build your project with `debug = true` in - `[profile.release]`, then run `cargo flamegraph -- `. Identify - the three widest stacks at the top of the flamegraph — those are your hot spots. +2. **Постройте флеймграф**: соберите проект с `debug = true` в `[profile.release]`, затем + запустите `cargo flamegraph -- <ваши аргументы>`. Определите три самых широких стека + наверху флеймграфа — это и есть ваши горячие точки. -3. **Compare with `hyperfine`**: Install `hyperfine` and benchmark the overall - execution time of your binary with different flags. Compare it to the - per-function times from Criterion. Where does the time go that Criterion - doesn't see? (Answer: I/O, syscalls, process startup.) +3. **Сравните с `hyperfine`**: установите `hyperfine` и замерьте общее время выполнения + бинарника с разными флагами. Сравните его с временами отдельных функций из Criterion. + Какое время уходит там, что Criterion не видит? (Ответ: I/O, системные вызовы, + запуск процесса.) -### Benchmark Tool Selection +### Выбор инструмента для бенчмарков ```mermaid flowchart TD - START["Want to measure performance?"] --> WHAT{"What level?"} + START["Хотите измерить производительность?"] --> WHAT{"Какой уровень?"} - WHAT -->|"Single function"| CRITERION["Criterion.rs
Statistical, regression detection"] - WHAT -->|"Quick function check"| DIVAN["Divan
Lighter, attribute macros"] - WHAT -->|"Whole binary"| HYPERFINE["hyperfine
End-to-end, wall-clock"] - WHAT -->|"Find hot spots"| PERF["perf + flamegraph
CPU sampling profiler"] + WHAT -->|"Отдельная функция"| CRITERION["Criterion.rs
Статистика, обнаружение регрессий"] + WHAT -->|"Быстрая проверка функции"| DIVAN["Divan
Легче, атрибутные макросы"] + WHAT -->|"Весь бинарник"| HYPERFINE["hyperfine
Сквозной, реальное время"] + WHAT -->|"Поиск горячих точек"| PERF["perf + флеймграф
Профилировщик с выборкой CPU"] - CRITERION --> CI_BENCH["Continuous benchmarking
in GitHub Actions"] - PERF --> OPTIMIZE["Profile-Guided
Optimization (PGO)"] + CRITERION --> CI_BENCH["Непрерывный бенчмаркинг
в GitHub Actions"] + PERF --> OPTIMIZE["Оптимизация по профилю
(PGO)"] style CRITERION fill:#91e5a3,color:#000 style DIVAN fill:#91e5a3,color:#000 @@ -485,14 +497,16 @@ flowchart TD style OPTIMIZE fill:#ffd43b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: First Criterion Benchmark +#### 🟢 Упражнение 1: первый бенчмарк Criterion -Create a crate with a function that sorts a `Vec` of 10,000 random elements. Write a Criterion benchmark for it, then switch to `.sort_unstable()` and observe the performance difference in the HTML report. +Создайте крейт с функцией, которая сортирует `Vec` из 10 000 случайных элементов. +Напишите для неё бенчмарк Criterion, затем переключитесь на `.sort_unstable()` и посмотрите +разницу в производительности в HTML-отчёте.
-Solution +Решение ```toml # Cargo.toml @@ -547,33 +561,37 @@ open target/criterion/sort-10k/report/index.html ```
-#### 🟡 Exercise 2: Flamegraph Hot Spot +#### 🟡 Упражнение 2: горячая точка на флеймграфе -Build a project with `debug = true` in `[profile.release]`, then generate a flamegraph. Identify the top 3 widest stacks. +Соберите проект с `debug = true` в `[profile.release]`, затем постройте флеймграф. +Определите три самых широких стека.
-Solution +Решение ```toml # Cargo.toml [profile.release] -debug = true # Keep symbols for flamegraph +debug = true # Сохраняем символы для флеймграфа ``` ```bash cargo install flamegraph -cargo flamegraph --release -- -# Opens flamegraph.svg in browser -# The widest stacks at the top are your hot spots +cargo flamegraph --release -- <ваши аргументы> +# Откроет flamegraph.svg в браузере +# Самые широкие стеки наверху — это и есть ваши горячие точки ```
-### Key Takeaways +### Ключевые выводы -- Never benchmark with `Instant::now()` — use Criterion.rs for statistical rigor and regression detection -- `black_box()` prevents the compiler from optimizing away your benchmark target -- `hyperfine` measures wall-clock time for the whole binary; Criterion measures individual functions — use both -- Flamegraphs show *where* time is spent; benchmarks show *how much* time is spent -- Continuous benchmarking in CI catches performance regressions before they ship +- Никогда не замеряйте бенчмарки через `Instant::now()` — используйте Criterion.rs для + статистической строгости и обнаружения регрессий +- `black_box()` не даёт компилятору оптимизировать цель бенчмарка «в ничто» +- `hyperfine` измеряет реальное время всего бинарника, а Criterion — отдельных функций: + используйте оба инструмента +- Флеймграфы показывают, *где* тратится время; бенчмарки показывают, *сколько* времени + тратится +- Непрерывный бенчмаркинг в CI ловит регрессии производительности до выпуска --- diff --git a/engineering-book/src/ch04-code-coverage-seeing-what-tests-miss.md b/engineering-book/src/ch04-code-coverage-seeing-what-tests-miss.md index 5fd99e8..cd5ac44 100644 --- a/engineering-book/src/ch04-code-coverage-seeing-what-tests-miss.md +++ b/engineering-book/src/ch04-code-coverage-seeing-what-tests-miss.md @@ -1,95 +1,95 @@ -# Code Coverage — Seeing What Tests Miss 🟢 +# Покрытие кода — видим то, что пропускают тесты 🟢 -> **What you'll learn:** -> - Source-based coverage with `cargo-llvm-cov` (the most accurate Rust coverage tool) -> - Quick coverage checks with `cargo-tarpaulin` and Mozilla's `grcov` -> - Setting up coverage gates in CI with Codecov and Coveralls -> - A coverage-guided testing strategy that prioritizes high-risk blind spots +> **Чему вы научитесь:** +> - Покрытие на основе исходников с `cargo-llvm-cov` (самый точный инструмент покрытия для Rust) +> - Быстрые проверки покрытия с `cargo-tarpaulin` и `grcov` от Mozilla +> - Настройка порогов покрытия в CI с помощью Codecov и Coveralls +> - Стратегия тестирования, управляемая покрытием, которая в первую очередь закрывает опасные слепые зоны > -> **Cross-references:** [Miri and Sanitizers](ch05-miri-valgrind-and-sanitizers-verifying-u.md) — coverage finds untested code, Miri finds UB in tested code · [Benchmarking](ch03-benchmarking-measuring-what-matters.md) — coverage shows *what's tested*, benchmarks show *what's fast* · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) — coverage gate in the pipeline +> **Перекрёстные ссылки:** [Miri и санитайзеры](ch05-miri-valgrind-and-sanitizers-verifying-u.md) — покрытие находит непротестированный код, Miri находит UB в протестированном · [Бенчмаркинг](ch03-benchmarking-measuring-what-matters.md) — покрытие показывает *что протестировано*, бенчмарки — *что быстро* · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) — порог покрытия в конвейере -Code coverage measures which lines, branches, or functions your tests actually -execute. It doesn't prove correctness (a covered line can still have bugs), but -it reliably reveals **blind spots** — code paths that no test exercises at all. +Покрытие кода показывает, какие строки, ветви или функции ваши тесты на самом деле +выполняют. Оно не доказывает корректность (покрытая строка всё равно может содержать +ошибку), но надёжно выявляет **слепые зоны** — пути в коде, которые не проверяет ни один тест. -With 1,006 tests across many crates, the project has substantial test investment. -Coverage analysis answers: "Is that investment reaching the code that matters?" +В проекте 1 006 тестов в нескольких крейтах, то есть вложения в тестирование серьёзные. +Анализ покрытия отвечает на вопрос: «Доходят ли эти вложения до кода, который важен?» -### Source-Based Coverage with `llvm-cov` +### Покрытие на основе исходного кода с `llvm-cov` -Rust uses LLVM, which provides source-based coverage instrumentation — the most -accurate coverage method available. The recommended tool is +Rust использует LLVM, который предоставляет инструментирование покрытия на основе +исходного кода — самый точный из доступных методов. Рекомендуемый инструмент — [`cargo-llvm-cov`](https://github.com/taiki-e/cargo-llvm-cov): ```bash -# Install +# Установка cargo install cargo-llvm-cov -# Or via rustup component (for the raw llvm tools) +# Или через компонент rustup (для «сырых» инструментов LLVM) rustup component add llvm-tools-preview ``` -**Basic usage:** +**Базовое использование:** ```bash -# Run tests and show per-file coverage summary +# Запустить тесты и показать сводку покрытия по файлам cargo llvm-cov -# Generate HTML report (browsable, line-by-line highlighting) +# Сгенерировать HTML-отчёт (просмотр с построчной подсветкой) cargo llvm-cov --html -# Output: target/llvm-cov/html/index.html +# Результат: target/llvm-cov/html/index.html -# Generate LCOV format (for CI integrations) +# Сгенерировать отчёт в формате LCOV (для интеграций с CI) cargo llvm-cov --lcov --output-path lcov.info -# Workspace-wide coverage (all crates) +# Покрытие всего воркспейса (все крейты) cargo llvm-cov --workspace -# Include only specific packages +# Включить только определённые пакеты cargo llvm-cov --package accel_diag --package topology_lib -# Coverage including doc tests +# Покрытие с учётом doc-тестов cargo llvm-cov --doctests ``` -**Reading the HTML report:** +**Чтение HTML-отчёта:** ```text target/llvm-cov/html/index.html -├── Filename │ Function │ Line │ Branch │ Region +├── Файл │ Функции │ Строки │ Ветви │ Регионы ├─ accel_diag/src/lib.rs │ 78.5% │ 82.3% │ 61.2% │ 74.1% ├─ sel_mgr/src/parse.rs│ 95.2% │ 96.8% │ 88.0% │ 93.5% ├─ topology_lib/src/.. │ 91.0% │ 93.4% │ 79.5% │ 89.2% └─ ... -Green = covered Red = not covered Yellow = partially covered (branch) +Зелёный = покрыто Красный = не покрыто Жёлтый = покрыто частично (ветвление) ``` -**Coverage types explained:** +**Типы покрытия:** -| Type | What It Measures | Significance | -|------|------------------|-------------| -| **Line coverage** | Which source lines were executed | Basic "was this code reached?" | -| **Branch coverage** | Which `if`/`match` arms were taken | Catches untested conditions | -| **Function coverage** | Which functions were called | Finds dead code | -| **Region coverage** | Which code regions (sub-expressions) were hit | Most granular | +| Тип | Что измеряет | Значение | +|-----|--------------|----------| +| **Покрытие строк** | Какие строки исходника выполнялись | Базовый вопрос: «до этого кода вообще дошло?» | +| **Покрытие ветвлений** | Какие ветки `if`/`match` выполнялись | Выявляет непроверенные условия | +| **Покрытие функций** | Какие функции вызывались | Находит мёртвый код | +| **Покрытие регионов** | Какие регионы кода (подвыражения) выполнялись | Самый детальный уровень | -### cargo-tarpaulin — The Quick Path +### cargo-tarpaulin — быстрый путь -[`cargo-tarpaulin`](https://github.com/xd009642/tarpaulin) is a Linux-specific -coverage tool that's simpler to set up (no LLVM components needed): +[`cargo-tarpaulin`](https://github.com/xd009642/tarpaulin) — инструмент покрытия, +работающий только в Linux, который проще настроить (не нужны компоненты LLVM): ```bash -# Install +# Установка cargo install cargo-tarpaulin -# Basic coverage report +# Базовый отчёт о покрытии cargo tarpaulin -# HTML output +# HTML-вывод cargo tarpaulin --out Html -# With specific options +# С дополнительными опциями cargo tarpaulin \ --workspace \ --timeout 120 \ @@ -98,43 +98,43 @@ cargo tarpaulin \ --exclude-files "*/tests/*" "*/benches/*" \ --ignore-panics -# Skip certain crates -cargo tarpaulin --workspace --exclude diag_tool # exclude the binary crate +# Пропустить некоторые крейты +cargo tarpaulin --workspace --exclude diag_tool # исключить крейт-бинарник ``` -**tarpaulin vs llvm-cov comparison:** +**Сравнение tarpaulin и llvm-cov:** -| Feature | cargo-llvm-cov | cargo-tarpaulin | -|---------|----------------|-----------------| -| Accuracy | Source-based (most accurate) | Ptrace-based (occasional overcounting) | -| Platform | Any (llvm-based) | Linux only | -| Branch coverage | Yes | Limited | -| Doc tests | Yes | No | -| Setup | Needs `llvm-tools-preview` | Self-contained | -| Speed | Faster (compile-time instrumentation) | Slower (ptrace overhead) | -| Stability | Very stable | Occasional false positives | +| Возможность | cargo-llvm-cov | cargo-tarpaulin | +|-------------|----------------|-----------------| +| Точность | На основе исходников (самый точный) | На основе ptrace (иногда завышает покрытие) | +| Платформа | Любая (на основе LLVM) | Только Linux | +| Покрытие ветвлений | Да | Ограниченно | +| Doc-тесты | Да | Нет | +| Настройка | Нужен `llvm-tools-preview` | Самодостаточный | +| Скорость | Быстрее (инструментирование на этапе компиляции) | Медленнее (накладные расходы ptrace) | +| Стабильность | Очень стабильный | Иногда ложные срабатывания | -**Recommendation**: Use `cargo-llvm-cov` for accuracy. Use `cargo-tarpaulin` when -you need a quick check without installing LLVM tools. +**Рекомендация**: используйте `cargo-llvm-cov` ради точности. `cargo-tarpaulin` — когда +нужна быстрая проверка без установки инструментов LLVM. -### grcov — Mozilla's Coverage Tool +### grcov — инструмент покрытия от Mozilla -[`grcov`](https://github.com/mozilla/grcov) is Mozilla's coverage aggregator. -It consumes raw LLVM profiling data and produces reports in multiple formats: +[`grcov`](https://github.com/mozilla/grcov) — агрегатор покрытия от Mozilla. +Он обрабатывает сырые данные профилирования LLVM и формирует отчёты в нескольких форматах: ```bash -# Install +# Установка cargo install grcov -# Step 1: Build with coverage instrumentation +# Шаг 1: сборка с инструментированием покрытия export RUSTFLAGS="-Cinstrument-coverage" export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw" cargo build --tests -# Step 2: Run tests (generates .profraw files) +# Шаг 2: запуск тестов (генерирует файлы .profraw) cargo test -# Step 3: Aggregate with grcov +# Шаг 3: агрегация с помощью grcov grcov target/coverage/ \ --binary-path target/debug/ \ --source-dir . \ @@ -145,21 +145,22 @@ grcov target/coverage/ \ --ignore "*/tests/*" \ --ignore "*/.cargo/*" -# Step 4: View report +# Шаг 4: просмотр отчёта open target/coverage/report/html/index.html ``` -**When to use grcov**: It's most useful when you need to **merge coverage from -multiple test runs** (e.g., unit tests + integration tests + fuzz tests) into a -single report. +**Когда использовать grcov**: он полезнее всего, когда нужно **объединить покрытие из +нескольких запусков тестов** (например, модульных, интеграционных и фаззинг-тестов) +в один отчёт. -### Coverage in CI: Codecov and Coveralls +### Покрытие в CI: Codecov и Coveralls -Upload coverage data to a tracking service for historical trends and PR annotations: +Загружайте данные о покрытии в сервис мониторинга, чтобы видеть исторические тренды +и аннотации в PR: ```yaml # .github/workflows/coverage.yml -name: Code Coverage +name: Покрытие кода on: [push, pull_request] @@ -172,96 +173,97 @@ jobs: with: components: llvm-tools-preview - - name: Install cargo-llvm-cov + - name: Установка cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - name: Generate coverage + - name: Генерация покрытия run: cargo llvm-cov --workspace --lcov --output-path lcov.info - - name: Upload to Codecov + - name: Загрузка в Codecov uses: codecov/codecov-action@v4 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true - # Optional: enforce minimum coverage - - name: Check coverage threshold + # Опционально: задать минимальный порог покрытия + - name: Проверка порога покрытия run: | cargo llvm-cov --workspace --fail-under-lines 80 - # Fails the build if line coverage drops below 80% + # Сборка падает, если покрытие строк опускается ниже 80% ``` -**Coverage gates** — enforce minimums per crate by reading the JSON output: +**Пороги покрытия** — задавайте минимумы для каждого крейта, читая JSON-вывод: ```bash -# Get per-crate coverage as JSON +# Получить покрытие по крейтам в формате JSON cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent' -# Fail if below threshold +# Упасть, если покрытие ниже порога cargo llvm-cov --workspace --fail-under-lines 80 cargo llvm-cov --workspace --fail-under-functions 70 cargo llvm-cov --workspace --fail-under-regions 60 ``` -### Coverage-Guided Testing Strategy +### Стратегия тестирования, управляемая покрытием -Coverage numbers alone are meaningless without a strategy. Here's how to use -coverage data effectively: +Сами по себе цифры покрытия бессмысленны без стратегии. Вот как эффективно использовать +данные о покрытии: -**Step 1: Triage by risk** +**Шаг 1: сортировка по риску** ```text -High coverage, high risk → ✅ Good — maintain it -High coverage, low risk → 🔄 Possibly over-tested — skip if slow -Low coverage, high risk → 🔴 Write tests NOW — this is where bugs hide -Low coverage, low risk → 🟡 Track but don't panic +Высокое покрытие, высокий риск → ✅ Хорошо — поддерживайте +Высокое покрытие, низкий риск → 🔄 Возможно, избыточно протестировано — пропускайте, если медленно +Низкое покрытие, высокий риск → 🔴 Пишите тесты СЕЙЧАС — здесь прячутся баги +Низкое покрытие, низкий риск → 🟡 Отслеживайте, но не паникуйте ``` -**Step 2: Focus on branch coverage, not line coverage** +**Шаг 2: ориентируйтесь на покрытие ветвлений, а не строк** ```rust -// 100% line coverage, 50% branch coverage — still risky! +// 100% покрытие строк, 50% покрытие ветвлений — всё ещё рискованно! pub fn classify_temperature(temp_c: i32) -> ThermalState { - if temp_c > 105 { // ← tested with temp=110 → Critical + if temp_c > 105 { // ← проверено при temp=110 → Critical ThermalState::Critical - } else if temp_c > 85 { // ← tested with temp=90 → Warning + } else if temp_c > 85 { // ← проверено при temp=90 → Warning ThermalState::Warning - } else if temp_c < -10 { // ← NEVER TESTED → sensor error case missed + } else if temp_c < -10 { // ← НИКОГДА НЕ ПРОВЕРЯЛОСЬ → случай ошибки датчика пропущен ThermalState::SensorError } else { - ThermalState::Normal // ← tested with temp=25 → Normal + ThermalState::Normal // ← проверено при temp=25 → Normal } } ``` -**Step 3: Exclude noise** +**Шаг 3: отсекайте шум** ```bash -# Exclude test code from coverage (it's always "covered") +# Исключаем тестовый код из покрытия (он всегда «покрыт») cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/' -# Exclude generated code +# Исключаем сгенерированный код cargo llvm-cov --workspace --ignore-filename-regex 'target/' ``` -In code, mark untestable sections: +В коде отмечайте участки, которые нельзя протестировать: ```rust -// Coverage tools recognize this pattern +// Инструменты покрытия распознают этот паттерн #[cfg(not(tarpaulin_include))] // tarpaulin fn unreachable_hardware_path() { - // This path requires actual GPU hardware to trigger + // Этот путь требует реального GPU, чтобы сработать } -// For llvm-cov, use a more targeted approach: -// Simply accept that some paths need integration/hardware tests, -// not unit tests. Track them in a coverage exceptions list. +// Для llvm-cov используйте более точный подход: +// просто примите, что некоторые пути нуждаются в интеграционных и аппаратных тестах, +// а не в модульных. Отслеживайте их в списке исключений покрытия. ``` -### Complementary Testing Tools +### Дополнительные инструменты тестирования -**`proptest` — Property-Based Testing** finds edge cases that hand-written tests miss: +**`proptest` — property-based тестирование** находит крайние случаи, которые пропускают +написанные вручную тесты: ```toml [dev-dependencies] @@ -274,9 +276,9 @@ use proptest::prelude::*; proptest! { #[test] fn parse_never_panics(input in "\\PC*") { - // proptest generates thousands of random strings - // If parse_gpu_csv panics on any input, the test fails - // and proptest minimizes the failing case for you. + // proptest генерирует тысячи случайных строк + // Если parse_gpu_csv паникует на каком-либо входе, тест падает, + // и proptest минимизирует упавший пример за вас. let _ = parse_gpu_csv(&input); } @@ -284,13 +286,13 @@ proptest! { fn temperature_roundtrip(raw in 0u16..4096) { let temp = Temperature::from_raw(raw); let md = temp.millidegrees_c(); - // Property: millidegrees should always be derivable from raw + // Свойство: значение в тысячных долях градуса всегда должно выводиться из raw assert_eq!(md, (raw as i32) * 625 / 10); } } ``` -**`insta` — Snapshot Testing** for large structured outputs (JSON, text reports): +**`insta` — snapshot-тестирование** для больших структурированных выводов (JSON, текстовые отчёты): ```toml [dev-dependencies] @@ -301,130 +303,132 @@ insta = { version = "1", features = ["json"] } #[test] fn test_der_report_format() { let report = generate_der_report(&test_results); - // First run: creates a snapshot file. Subsequent runs: compares against it. - // Run `cargo insta review` to accept changes interactively. + // При первом запуске создаётся файл снимка. При последующих — сравнение с ним. + // Запустите `cargo insta review`, чтобы принять изменения в интерактивном режиме. insta::assert_json_snapshot!(report); } ``` -> **When to add proptest/insta**: If your unit tests are all "happy path" examples, -> proptest will find the edge cases you missed. If you're testing large output -> formats (JSON reports, DER records), insta snapshots are faster to write and -> maintain than hand-written assertions. +> **Когда добавлять proptest и insta**: если ваши модульные тесты — это только примеры +> «счастливого пути», proptest найдёт крайние случаи, которые вы упустили. Если вы тестируете +> большие форматы вывода (JSON-отчёты, записи DER), снимки insta писать и поддерживать +> быстрее, чем ручные assert-ы. -### Application: 1,000+ Tests Coverage Map +### Применение: карта покрытия для более чем 1000 тестов -The project has 1,000+ tests but no coverage tracking. Adding it -reveals the testing investment distribution. Uncovered paths are prime candidates -for [Miri and sanitizer](ch05-miri-valgrind-and-sanitizers-verifying-u.md) verification: +В проекте больше 1000 тестов, но отслеживания покрытия нет. Его добавление покажет, +как распределены тестовые усилия. Непокрытые пути — первые кандидаты на проверку с помощью +[Miri и санитайзеров](ch05-miri-valgrind-and-sanitizers-verifying-u.md): -**Recommended coverage configuration:** +**Рекомендуемая конфигурация покрытия:** ```bash -# Quick workspace coverage (proposed CI command) +# Быстрое покрытие всего воркспейса (предлагаемая команда для CI) cargo llvm-cov --workspace \ --ignore-filename-regex 'tests?\.rs$' \ --fail-under-lines 75 \ --html -# Per-crate coverage for targeted improvement +# Покрытие по крейтам для целенаправленного улучшения for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do echo "=== $crate ===" cargo llvm-cov --package "$crate" --json 2>/dev/null | \ - jq -r '.data[0].totals | "Lines: \(.lines.percent | round)% Branches: \(.branches.percent | round)%"' + jq -r '.data[0].totals | "Строки: \(.lines.percent | round)% Ветви: \(.branches.percent | round)%"' done ``` -**Expected high-coverage crates** (based on test density): -- `topology_lib` — 922-line golden-file test suite -- `event_log` — registry with `create_test_record()` helpers -- `cable_diag` — `make_test_event()` / `make_test_context()` patterns +**Крейты с ожидаемо высоким покрытием** (исходя из плотности тестов): +- `topology_lib` — набор тестов golden-файлов на 922 строки +- `event_log` — реестр с хелперами `create_test_record()` +- `cable_diag` — паттерны `make_test_event()` / `make_test_context()` -**Expected coverage gaps** (based on code inspection): -- Error handling arms in IPMI communication paths -- GPU hardware-specific branches (require actual GPU) -- `dmesg` parsing edge cases (platform-dependent output) +**Ожидаемые пробелы в покрытии** (по результатам просмотра кода): +- Ветки обработки ошибок в путях IPMI-коммуникации +- Ветки, специфичные для железа GPU (требуют реального GPU) +- Крайние случаи парсинга `dmesg` (вывод зависит от платформы) -> **The 80/20 rule of coverage**: Getting from 0% to 80% coverage is straightforward. -> Getting from 80% to 95% requires increasingly contrived test scenarios. Getting -> from 95% to 100% requires `#[cfg(not(...))]` exclusions and is rarely worth the -> effort. Target **80% line coverage and 70% branch coverage** as a practical floor. +> **Правило 80/20 для покрытия**: от 0% до 80% добраться несложно. От 80% до 95% требует +> всё более искусственных тестовых сценариев. От 95% до 100% требует исключений +> `#[cfg(not(...))]` и редко стоит усилий. Ориентируйтесь на **80% покрытия строк и 70% +> покрытия ветвлений** как на разумный практический минимум. -### Troubleshooting Coverage +### Устранение неполадок с покрытием -| Symptom | Cause | Fix | -|---------|-------|-----| -| `llvm-cov` shows 0% for all files | Instrumentation not applied | Ensure you run `cargo llvm-cov`, not `cargo test` + `llvm-cov` separately | -| Coverage counts `unreachable!()` as uncovered | Those branches exist in compiled code | Use `#[cfg(not(tarpaulin_include))]` or add to exclusion regex | -| Test binary crashes under coverage | Instrumentation + sanitizer conflict | Don't combine `cargo llvm-cov` with `-Zsanitizer=address`; run them separately | -| Coverage differs between `llvm-cov` and `tarpaulin` | Different instrumentation techniques | Use `llvm-cov` as source of truth (compiler-native); file issues for large discrepancies | -| `error: profraw file is malformed` | Test binary crashed mid-execution | Fix the test failure first; profraw files are corrupt when the process exits abnormally | -| Branch coverage seems impossibly low | Optimizer creates branches for match arms, unwrap, etc. | Focus on *line* coverage for practical thresholds; branch coverage is inherently lower | +| Симптом | Причина | Решение | +|---------|---------|---------| +| `llvm-cov` показывает 0% для всех файлов | Инструментирование не применено | Запускайте `cargo llvm-cov`, а не `cargo test` и `llvm-cov` по отдельности | +| Покрытие считает `unreachable!()` непокрытым | Эти ветки есть в скомпилированном коде | Используйте `#[cfg(not(tarpaulin_include))]` или добавьте в регулярное выражение исключений | +| Тестовый бинарник падает под покрытием | Конфликт инструментирования и санитайзера | Не комбинируйте `cargo llvm-cov` с `-Zsanitizer=address`; запускайте их отдельно | +| Покрытие различается между `llvm-cov` и `tarpaulin` | Разные методы инструментирования | Считайте источником истины `llvm-cov` (встроен в компилятор); заводите задачи при крупных расхождениях | +| `error: profraw file is malformed` | Тестовый бинарник упал во время выполнения | Сначала исправьте падающий тест; profraw-файлы повреждаются, если процесс завершился аварийно | +| Покрытие ветвлений кажется невероятно низким | Оптимизатор создаёт ветки для ветвей `match`, `unwrap` и т. д. | Для практических порогов ориентируйтесь на *покрытие строк*; покрытие ветвлений по природе ниже | -### Try It Yourself +### Попробуйте сами -1. **Measure coverage on your project**: Run `cargo llvm-cov --workspace --html` - and open the report. Find the three files with the lowest coverage. Are they - untested, or inherently hard to test (hardware-dependent code)? +1. **Измерьте покрытие своего проекта**: запустите `cargo llvm-cov --workspace --html` + и откройте отчёт. Найдите три файла с самым низким покрытием. Они просто не тестируются + или им по природе трудно быть протестированными (код, зависящий от железа)? -2. **Set a coverage gate**: Add `cargo llvm-cov --workspace --fail-under-lines 60` - to your CI. Intentionally comment out a test and verify CI fails. Then raise - the threshold to your project's actual coverage level minus 2%. +2. **Задайте порог покрытия**: добавьте `cargo llvm-cov --workspace --fail-under-lines 60` + в CI. Намеренно закомментируйте тест и убедитесь, что CI падает. Затем поднимите порог + до фактического покрытия проекта минус 2%. -3. **Branch vs. line coverage**: Write a function with a 3-arm `match` and - test only 2 arms. Compare line coverage (may show 66%) vs. branch coverage - (may show 50%). Which metric is more useful for your project? +3. **Покрытие ветвлений и строк**: напишите функцию с `match` из трёх веток и протестируйте + только две из них. Сравните покрытие строк (может показать 66%) и покрытие ветвлений + (может показать 50%). Какая метрика полезнее для вашего проекта? -### Coverage Tool Selection +### Выбор инструмента покрытия ```mermaid flowchart TD - START["Need code coverage?"] --> ACCURACY{"Priority?"} - - ACCURACY -->|"Most accurate"| LLVM["cargo-llvm-cov
Source-based, compiler-native"] - ACCURACY -->|"Quick check"| TARP["cargo-tarpaulin
Linux only, fast"] - ACCURACY -->|"Multi-run aggregate"| GRCOV["grcov
Mozilla, combines profiles"] - - LLVM --> CI_GATE["CI coverage gate
--fail-under-lines 80"] + START["Нужно покрытие кода?"] --> ACCURACY{"Приоритет?"} + + ACCURACY -->|"Максимальная точность"| LLVM["cargo-llvm-cov
На основе исходников, нативно для компилятора"] + ACCURACY -->|"Быстрая проверка"| TARP["cargo-tarpaulin
Только Linux, быстрый"] + ACCURACY -->|"Агрегация нескольких запусков"| GRCOV["grcov
Mozilla, объединяет профили"] + + LLVM --> CI_GATE["Порог покрытия в CI
--fail-under-lines 80"] TARP --> CI_GATE - - CI_GATE --> UPLOAD{"Upload to?"} + + CI_GATE --> UPLOAD{"Куда загружать?"} UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"] UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"] - + style LLVM fill:#91e5a3,color:#000 style TARP fill:#e3f2fd,color:#000 style GRCOV fill:#e3f2fd,color:#000 style CI_GATE fill:#ffd43b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: First Coverage Report +#### 🟢 Упражнение 1: первый отчёт о покрытии -Install `cargo-llvm-cov`, run it on any Rust project, and open the HTML report. Find the three files with the lowest line coverage. +Установите `cargo-llvm-cov`, запустите его на любом проекте на Rust и откройте HTML-отчёт. +Найдите три файла с самым низким покрытием строк.
-Solution +Решение ```bash cargo install cargo-llvm-cov cargo llvm-cov --workspace --html --open -# The report sorts files by coverage — lowest at the bottom -# Look for files under 50% — those are your blind spots +# Отчёт сортирует файлы по покрытию — самые низкие внизу +# Ищите файлы ниже 50% — это ваши слепые зоны ```
-#### 🟡 Exercise 2: CI Coverage Gate +#### 🟡 Упражнение 2: порог покрытия в CI -Add a coverage gate to a GitHub Actions workflow that fails if line coverage drops below 60%. Verify it works by commenting out a test. +Добавьте порог покрытия в workflow GitHub Actions, который падает, если покрытие строк +опускается ниже 60%. Проверьте работу, закомментировав один тест.
-Solution +Решение ```yaml # .github/workflows/coverage.yml -name: Coverage +name: Покрытие on: [push, pull_request] jobs: coverage: @@ -438,16 +442,18 @@ jobs: - run: cargo llvm-cov --workspace --fail-under-lines 60 ``` -Comment out a test, push, and watch the workflow fail. +Закомментируйте тест, отправьте изменения и посмотрите, как workflow падает.
-### Key Takeaways +### Ключевые выводы -- `cargo-llvm-cov` is the most accurate coverage tool for Rust — it uses the compiler's own instrumentation -- Coverage doesn't prove correctness, but **zero coverage proves zero testing** — use it to find blind spots -- Set a coverage gate in CI (e.g., `--fail-under-lines 80`) to prevent regressions -- Don't chase 100% coverage — focus on high-risk code paths (error handling, unsafe, parsing) -- Never combine coverage instrumentation with sanitizers in the same run +- `cargo-llvm-cov` — самый точный инструмент покрытия для Rust: он использует собственное + инструментирование компилятора +- Покрытие не доказывает корректность, но **нулевое покрытие доказывает отсутствие тестов** — + используйте его, чтобы находить слепые зоны +- Задайте порог покрытия в CI (например, `--fail-under-lines 80`), чтобы предотвращать регрессии +- Не гонитесь за 100% покрытием — сосредоточьтесь на путях с высоким риском (обработка ошибок, + unsafe, парсинг) +- Никогда не комбинируйте инструментирование покрытия с санитайзерами в одном запуске --- - diff --git a/engineering-book/src/ch05-miri-valgrind-and-sanitizers-verifying-u.md b/engineering-book/src/ch05-miri-valgrind-and-sanitizers-verifying-u.md index b76f959..277232c 100644 --- a/engineering-book/src/ch05-miri-valgrind-and-sanitizers-verifying-u.md +++ b/engineering-book/src/ch05-miri-valgrind-and-sanitizers-verifying-u.md @@ -1,102 +1,102 @@ -# Miri, Valgrind, and Sanitizers — Verifying Unsafe Code 🔴 - -> **What you'll learn:** -> - Miri as a MIR interpreter — what it catches (aliasing, UB, leaks) and what it can't (FFI, syscalls) -> - Valgrind memcheck, Helgrind (data races), Callgrind (profiling), and Massif (heap) -> - LLVM sanitizers: ASan, MSan, TSan, LSan with nightly `-Zbuild-std` -> - `cargo-fuzz` for crash discovery and `loom` for concurrency model checking -> - A decision tree for choosing the right verification tool +# Miri, Valgrind и санитайзеры — проверка unsafe-кода 🔴 + +> **Чему вы научитесь:** +> - Miri как интерпретатор MIR — что он ловит (алиасинг, UB, утечки) и что не может (FFI, системные вызовы) +> - Valgrind memcheck, Helgrind (гонки данных), Callgrind (профилирование) и Massif (куча) +> - Санитайзеры LLVM: ASan, MSan, TSan, LSan с nightly-сборкой `-Zbuild-std` +> - `cargo-fuzz` для поиска падений и `loom` для проверки моделей конкурентности +> - Дерево решений для выбора подходящего инструмента проверки > -> **Cross-references:** [Code Coverage](ch04-code-coverage-seeing-what-tests-miss.md) — coverage finds untested paths, Miri verifies the tested ones · [`no_std` & Features](ch09-no-std-and-feature-verification.md) — `no_std` code often requires `unsafe` that Miri can verify · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) — Miri job in the pipeline +> **Перекрёстные ссылки:** [Покрытие кода](ch04-code-coverage-seeing-what-tests-miss.md) — покрытие находит непротестированные пути, Miri проверяет протестированные · [`no_std` и фичи](ch09-no-std-and-feature-verification.md) — код `no_std` часто требует `unsafe`, который может проверить Miri · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) — джоба Miri в конвейере -Safe Rust guarantees memory safety and data-race freedom at compile time. But the -moment you write `unsafe` — for FFI, hand-rolled data structures, or performance -tricks — those guarantees become *your* responsibility. This chapter covers the -tools that verify your `unsafe` code actually upholds the safety contracts it claims. +Безопасный Rust гарантирует безопасность памяти и отсутствие гонок данных на этапе компиляции. +Но как только вы пишете `unsafe` — для FFI, собственных структур данных или ради +производительности, — эти гарантии становятся *вашей* ответственностью. Эта глава описывает +инструменты, которые проверяют, действительно ли ваш `unsafe`-код выполняет заявленные +контракты безопасности. -### Miri — An Interpreter for Unsafe Rust +### Miri — интерпретатор для unsafe Rust -[Miri](https://github.com/rust-lang/miri) is an **interpreter** for Rust's -Mid-level Intermediate Representation (MIR). Instead of compiling to machine code, -Miri *executes* your program step-by-step with exhaustive checks for undefined -behavior at every operation. +[Miri](https://github.com/rust-lang/miri) — это **интерпретатор** промежуточного +представления Rust (Mid-level Intermediate Representation, MIR). Вместо компиляции в машинный +код Miri *выполняет* программу шаг за шагом, проверяя на каждой операции отсутствие +неопределённого поведения (UB). ```bash -# Install Miri (nightly-only component) +# Установка Miri (компонент только для nightly) rustup +nightly component add miri -# Run your test suite under Miri +# Запуск тестового набора под Miri cargo +nightly miri test -# Run a specific binary under Miri +# Запуск конкретного бинарника под Miri cargo +nightly miri run -# Run a specific test +# Запуск конкретного теста cargo +nightly miri test -- test_name ``` -**How Miri works:** +**Как работает Miri:** ```text -Source → rustc → MIR → Miri interprets MIR +Исходник → rustc → MIR → Miri интерпретирует MIR │ - ├─ Tracks every pointer's provenance - ├─ Validates every memory access - ├─ Checks alignment at every deref - ├─ Detects use-after-free - ├─ Detects data races (with threads) - └─ Enforces Stacked Borrows / Tree Borrows rules + ├─ Отслеживает происхождение (provenance) каждого указателя + ├─ Проверяет каждое обращение к памяти + ├─ Проверяет выравнивание при каждом разыменовании + ├─ Обнаруживает use-after-free + ├─ Обнаруживает гонки данных (в многопоточном коде) + └─ Применяет правила Stacked Borrows / Tree Borrows ``` -### What Miri Catches (and What It Cannot) +### Что ловит Miri (а что не может) -**Miri detects:** +**Miri обнаруживает:** -| Category | Example | Would Crash at Runtime? | -|----------|---------|------------------------| -| Out-of-bounds access | `ptr.add(100).read()` past allocation | Sometimes (depends on page layout) | -| Use after free | Reading a dropped `Box` through raw pointer | Sometimes (depends on allocator) | -| Double free | Calling `drop_in_place` twice | Usually | -| Unaligned access | `(ptr as *const u32).read()` on odd address | On some architectures | -| Invalid values | `transmute::(2)` | Silently wrong | -| Dangling references | `&*ptr` where ptr is freed | No (silent corruption) | -| Data races | Two threads, one writing, no synchronization | Intermittent, hard to reproduce | -| Stacked Borrows violation | Aliasing `&mut` references | No (silent corruption) | +| Категория | Пример | Упадёт ли во время выполнения? | +|-----------|--------|--------------------------------| +| Выход за границы | `ptr.add(100).read()` за пределами выделения | Иногда (зависит от раскладки страниц) | +| Use-after-free | Чтение `Box` после освобождения через сырой указатель | Иногда (зависит от аллокатора) | +| Double free | Двойной вызов `drop_in_place` | Обычно | +| Невыровненный доступ | `(ptr as *const u32).read()` по нечётному адресу | На некоторых архитектурах | +| Некорректные значения | `transmute::(2)` | Молча даёт неверный результат | +| Висящие ссылки | `&*ptr`, где ptr уже освобождён | Нет (молчаливое повреждение) | +| Гонки данных | Два потока, один пишет, без синхронизации | Периодически, трудно воспроизвести | +| Нарушение Stacked Borrows | Алиасинг `&mut`-ссылок | Нет (молчаливое повреждение) | -**Miri does NOT detect:** +**Miri НЕ обнаруживает:** -| Limitation | Why | -|-----------|-----| -| Logic bugs | Miri checks memory safety, not correctness | -| Concurrency deadlocks | Miri checks data races, not livelocks | -| Performance issues | Interpretation is 10-100× slower than native | -| OS/hardware interaction | Miri can't emulate syscalls, device I/O | -| All FFI calls | Can't interpret C code (only Rust MIR) | -| Exhaustive path coverage | Only tests the paths your test suite reaches | +| Ограничение | Почему | +|-------------|--------| +| Логические ошибки | Miri проверяет безопасность памяти, а не корректность | +| Взаимоблокировки | Miri проверяет гонки данных, а не livelock | +| Проблемы производительности | Интерпретация в 10–100× медленнее нативного выполнения | +| Взаимодействие с ОС и железом | Miri не может эмулировать системные вызовы и ввод-вывод устройств | +| Все вызовы FFI | Не может интерпретировать C-код (только Rust MIR) | +| Полное покрытие путей | Проверяет только те пути, до которых доходит ваш тестовый набор | -**A concrete example — catching unsound code that "works" in practice:** +**Конкретный пример — поиск некорректного кода, который «работает» на практике:** ```rust #[cfg(test)] mod tests { #[test] fn test_miri_catches_ub() { - // This "works" in release builds but is undefined behavior + // Это «работает» в release-сборках, но является неопределённым поведением let mut v = vec![1, 2, 3]; let ptr = v.as_ptr(); - // Push may reallocate, invalidating ptr + // Push может перевыделить буфер, и тогда ptr станет недействительным v.push(4); - // ❌ UB: ptr may be dangling after reallocation - // Miri will catch this even if the allocator happens to - // not move the buffer. + // ❌ UB: ptr может висеть после перевыделения + // Miri поймает это, даже если аллокатор случайно не переместит буфер. // let _val = unsafe { *ptr }; - // Error: Miri would report: + // Ошибка: Miri сообщит: // "pointer to alloc1234 was dereferenced after this // allocation got freed" - - // ✅ Correct: get a fresh pointer after mutation + + // ✅ Правильно: получаем новый указатель после изменения let ptr = v.as_ptr(); let val = unsafe { *ptr }; assert_eq!(val, 1); @@ -104,50 +104,50 @@ mod tests { } ``` -### Running Miri on a Real Crate +### Запуск Miri на реальном крейте -**Practical Miri workflow for a crate with `unsafe`:** +**Практический workflow Miri для крейта с `unsafe`:** ```bash -# Step 1: Run all tests under Miri +# Шаг 1: запустите все тесты под Miri cargo +nightly miri test 2>&1 | tee miri_output.txt -# Step 2: If Miri reports errors, isolate them +# Шаг 2: если Miri сообщил об ошибках, изолируйте их cargo +nightly miri test -- failing_test_name -# Step 3: Use Miri's backtrace for diagnosis +# Шаг 3: используйте бэктрейс Miri для диагностики MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test -# Step 4: Choose a borrow model -# Stacked Borrows (default, stricter): +# Шаг 4: выберите модель заимствований +# Stacked Borrows (по умолчанию, строже): cargo +nightly miri test -# Tree Borrows (experimental, more permissive): +# Tree Borrows (экспериментальная, менее строгая): MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test ``` -**Miri flags for common scenarios:** +**Флаги Miri для типичных сценариев:** ```bash -# Disable isolation (allow file system access, env vars) +# Отключить изоляцию (разрешить доступ к файловой системе и переменным окружения) MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test -# Memory leak detection is ON by default in Miri. -# To suppress leak errors (e.g., for intentional leaks): +# Проверка утечек памяти включена в Miri по умолчанию. +# Чтобы подавить ошибки утечек (например, для намеренных утечек): # MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test -# Seed the RNG for reproducible results with randomized tests +# Задать seed для ГСЧ — воспроизводимые результаты в тестах с рандомизацией MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test -# Enable strict provenance checking +# Включить строгую проверку provenance MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test -# Multiple flags +# Несколько флагов сразу MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \ cargo +nightly miri test ``` -**Miri in CI:** +**Miri в CI:** ```yaml # .github/workflows/miri.yml @@ -163,138 +163,139 @@ jobs: with: components: miri - - name: Run Miri + - name: Запуск Miri run: cargo miri test --workspace env: MIRIFLAGS: "-Zmiri-backtrace=full" - # Leak checking is on by default. - # Skip tests that use system calls Miri can't handle - # (file I/O, networking, etc.) + # Проверка утечек включена по умолчанию. + # Пропускайте тесты, которые используют системные вызовы, с которыми Miri не справляется + # (файловый ввод-вывод, сеть и т. п.) ``` -> **Performance note**: Miri is 10-100× slower than native execution. A test suite -> that runs in 5 seconds natively may take 5 minutes under Miri. In CI, run Miri -> on a focused subset: crates with `unsafe` code only. +> **Замечание о производительности**: Miri в 10–100× медленнее нативного выполнения. Тестовый +> набор, который нативно выполняется за 5 секунд, под Miri может идти 5 минут. В CI запускайте +> Miri на выбранном подмножестве: только крейты с `unsafe`-кодом. -### Valgrind and Its Rust Integration +### Valgrind и его интеграция с Rust -[Valgrind](https://valgrind.org/) is the classic C/C++ memory checker. It works -on compiled Rust binaries too, checking for memory errors at the machine-code level. +[Valgrind](https://valgrind.org/) — классический инструмент проверки памяти для C/C++. +Он работает и со скомпилированными бинарниками Rust, проверяя ошибки памяти на уровне +машинного кода. ```bash -# Install Valgrind +# Установка Valgrind sudo apt install valgrind # Debian/Ubuntu sudo dnf install valgrind # Fedora -# Build with debug info (Valgrind needs symbols) +# Сборка с отладочной информацией (Valgrind нужны символы) cargo build --tests -# or for release with debug info: +# или для release с отладочной информацией: # cargo build --release # [profile.release] # debug = true -# Run a specific test binary under Valgrind +# Запуск конкретного тестового бинарника под Valgrind valgrind --tool=memcheck \ --leak-check=full \ --show-leak-kinds=all \ --track-origins=yes \ ./target/debug/deps/my_crate-abc123 --test-threads=1 -# Run the main binary +# Запуск основного бинарника valgrind --tool=memcheck \ --leak-check=full \ --error-exitcode=1 \ ./target/debug/diag_tool --run-diagnostics ``` -**Valgrind tools beyond memcheck:** +**Инструменты Valgrind помимо memcheck:** -| Tool | Command | What It Detects | -|------|---------|----------------| -| **Memcheck** | `--tool=memcheck` | Memory leaks, use-after-free, buffer overflows | -| **Helgrind** | `--tool=helgrind` | Data races and lock-order violations | -| **DRD** | `--tool=drd` | Data races (different detection algorithm) | -| **Callgrind** | `--tool=callgrind` | CPU instruction profiling (path-level) | -| **Massif** | `--tool=massif` | Heap memory profiling over time | -| **Cachegrind** | `--tool=cachegrind` | Cache miss analysis | +| Инструмент | Команда | Что обнаруживает | +|------------|---------|------------------| +| **Memcheck** | `--tool=memcheck` | Утечки памяти, use-after-free, переполнение буфера | +| **Helgrind** | `--tool=helgrind` | Гонки данных и нарушения порядка захвата блокировок | +| **DRD** | `--tool=drd` | Гонки данных (другой алгоритм обнаружения) | +| **Callgrind** | `--tool=callgrind` | Профилирование количества инструкций CPU (на уровне путей) | +| **Massif** | `--tool=massif` | Профилирование кучи во времени | +| **Cachegrind** | `--tool=cachegrind` | Анализ промахов кеша | -**Using Callgrind for instruction-level profiling:** +**Callgrind для профилирования на уровне инструкций:** ```bash -# Record instruction counts (more stable than wall-clock time) +# Подсчёт инструкций (стабильнее, чем измерение реального времени) valgrind --tool=callgrind \ --callgrind-out-file=callgrind.out \ ./target/release/diag_tool --run-diagnostics -# Visualize with KCachegrind +# Визуализация в KCachegrind kcachegrind callgrind.out -# or the text-based alternative: +# или текстовый вариант: callgrind_annotate callgrind.out | head -100 ``` -**Miri vs Valgrind — when to use which:** +**Miri или Valgrind — когда что использовать:** -| Aspect | Miri | Valgrind | +| Аспект | Miri | Valgrind | |--------|------|----------| -| Checks Rust-specific UB | ✅ Stacked/Tree Borrows | ❌ Not aware of Rust rules | -| Checks C FFI code | ❌ Can't interpret C | ✅ Checks all machine code | -| Needs nightly | ✅ Yes | ❌ No | -| Speed | 10-100× slower | 10-50× slower | -| Platform | Any (interprets MIR) | Linux, macOS (runs native code) | -| Data race detection | ✅ Yes | ✅ Yes (Helgrind/DRD) | -| Leak detection | ✅ Yes | ✅ Yes (more thorough) | -| False positives | Very rare | Occasional (especially with allocators) | - -**Use both**: -- **Miri** for pure-Rust `unsafe` code (Stacked Borrows, provenance) -- **Valgrind** for FFI-heavy code and whole-program leak analysis +| Проверяет UB, специфичный для Rust | ✅ Stacked/Tree Borrows | ❌ Не знает правил Rust | +| Проверяет C FFI-код | ❌ Не может интерпретировать C | ✅ Проверяет весь машинный код | +| Нужен nightly | ✅ Да | ❌ Нет | +| Скорость | В 10–100× медленнее | В 10–50× медленнее | +| Платформа | Любая (интерпретирует MIR) | Linux, macOS (выполняет нативный код) | +| Обнаружение гонок данных | ✅ Да | ✅ Да (Helgrind/DRD) | +| Обнаружение утечек | ✅ Да | ✅ Да (более тщательно) | +| Ложные срабатывания | Очень редко | Иногда (особенно с аллокаторами) | + +**Используйте оба:** +- **Miri** — для чистого Rust `unsafe` (Stacked Borrows, provenance) +- **Valgrind** — для кода с большим количеством FFI и для анализа утечек всей программы ### AddressSanitizer, MemorySanitizer, ThreadSanitizer -LLVM sanitizers are compile-time instrumentation passes that insert runtime checks. -They're faster than Valgrind (2-5× overhead vs 10-50×) and catch different classes -of bugs. +Санитайзеры LLVM — это проходы инструментирования на этапе компиляции, которые добавляют +проверки времени выполнения. Они быстрее Valgrind (накладные расходы 2–5× против 10–50×) +и ловят другие классы ошибок. ```bash -# Required: install Rust source for rebuilding std with sanitizer instrumentation +# Нужно: установите исходники Rust для пересборки std с инструментированием санитайзера rustup component add rust-src --toolchain nightly -# AddressSanitizer (ASan) — buffer overflows, use-after-free, stack overflows +# AddressSanitizer (ASan) — переполнение буфера, use-after-free, переполнение стека RUSTFLAGS="-Zsanitizer=address" \ cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu -# MemorySanitizer (MSan) — uninitialized memory reads +# MemorySanitizer (MSan) — чтение неинициализированной памяти RUSTFLAGS="-Zsanitizer=memory" \ cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu -# ThreadSanitizer (TSan) — data races +# ThreadSanitizer (TSan) — гонки данных RUSTFLAGS="-Zsanitizer=thread" \ cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu -# LeakSanitizer (LSan) — memory leaks (included in ASan by default) +# LeakSanitizer (LSan) — утечки памяти (по умолчанию входит в ASan) RUSTFLAGS="-Zsanitizer=leak" \ cargo +nightly test --target x86_64-unknown-linux-gnu ``` -> **Note**: ASan, MSan, and TSan require `-Zbuild-std` to rebuild the standard -> library with sanitizer instrumentation. LSan does not. +> **Примечание**: ASan, MSan и TSan требуют `-Zbuild-std`, чтобы пересобрать стандартную +> библиотеку с инструментированием санитайзера. LSan — не требует. -**Sanitizer comparison:** +**Сравнение санитайзеров:** -| Sanitizer | Overhead | Catches | Nightly? | `-Zbuild-std`? | -|-----------|----------|---------|----------|----------------| -| **ASan** | 2× memory, 2× CPU | Buffer overflow, use-after-free, stack overflow | Yes | Yes | -| **MSan** | 3× memory, 3× CPU | Uninitialized reads | Yes | Yes | -| **TSan** | 5-10× memory, 5× CPU | Data races | Yes | Yes | -| **LSan** | Minimal | Memory leaks | Yes | No | +| Санитайзер | Накладные расходы | Что ловит | Nightly? | `-Zbuild-std`? | +|------------|-------------------|-----------|----------|----------------| +| **ASan** | 2× память, 2× CPU | Переполнение буфера, use-after-free, переполнение стека | Да | Да | +| **MSan** | 3× память, 3× CPU | Чтение неинициализированных данных | Да | Да | +| **TSan** | 5–10× память, 5× CPU | Гонки данных | Да | Да | +| **LSan** | Минимальные | Утечки памяти | Да | Нет | -**Practical example — catching a data race with TSan:** +**Практический пример — ловим гонку данных с помощью TSan:** ```rust use std::sync::Arc; use std::thread; fn racy_counter() -> u64 { - // ❌ UB: unsynchronized shared mutable state + // ❌ UB: неслинхронизированное разделяемое изменяемое состояние let data = Arc::new(std::cell::UnsafeCell::new(0u64)); let mut handles = vec![]; @@ -302,7 +303,7 @@ fn racy_counter() -> u64 { let data = Arc::clone(&data); handles.push(thread::spawn(move || { for _ in 0..1000 { - // SAFETY: UNSOUND — data race! + // SAFETY: НЕКОРРЕКТНО — гонка данных! unsafe { *data.get() += 1; } @@ -314,26 +315,26 @@ fn racy_counter() -> u64 { h.join().unwrap(); } - // Value should be 4000 but may be anything due to race + // Значение должно быть 4000, но из-за гонки может оказаться любым unsafe { *data.get() } } -// Both Miri and TSan catch this: +// И Miri, и TSan находят это: // Miri: "Data race detected between (1) write and (2) write" // TSan: "WARNING: ThreadSanitizer: data race" // -// Fix: use AtomicU64 or Mutex +// Исправление: используйте AtomicU64 или Mutex ``` -### Related Tools: Fuzzing and Concurrency Verification +### Смежные инструменты: фаззинг и проверка конкурентности -**`cargo-fuzz` — Coverage-Guided Fuzzing** (finds crashes in parsers and decoders): +**`cargo-fuzz` — фаззинг с обратной связью по покрытию** (находит падения в парсерах и декодерах): ```bash -# Install +# Установка cargo install cargo-fuzz -# Initialize a fuzz target +# Инициализация цели фаззинга cargo fuzz init cargo fuzz add parse_gpu_csv ``` @@ -345,25 +346,25 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { if let Ok(s) = std::str::from_utf8(data) { - // The fuzzer generates millions of inputs looking for panics/crashes. + // Фаззер генерирует миллионы входных данных в поисках паник и падений. let _ = diag_tool::parse_gpu_csv(s); } }); ``` ```bash -# Run the fuzzer (runs until interrupted or crash found) -cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300 # 5 minutes +# Запуск фаззера (работает до прерывания или до обнаружения падения) +cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300 # 5 минут -# Minimize a crash +# Минимизация падения cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-... ``` -> **When to fuzz**: Any function that parses untrusted/semi-trusted input (sensor output, -> config files, network data, JSON/CSV). Fuzzing found real bugs in every major -> Rust parser crate (serde, regex, image). +> **Когда фаззить**: любая функция, которая разбирает недоверенные или полудоверенные входные +> данные (вывод датчиков, конфигурационные файлы, сетевые данные, JSON/CSV). Фаззинг находил +> реальные ошибки во всех крупных крейтах-парсерах Rust (serde, regex, image). -**`loom` — Concurrency Model Checker** (exhaustively tests atomic orderings): +**`loom` — проверка моделей конкурентности** (исчерпывающе перебирает порядки атомарных операций): ```toml [dev-dependencies] @@ -389,38 +390,38 @@ mod tests { t1.join().unwrap(); t2.join().unwrap(); - // loom explores ALL possible thread interleavings + // loom перебирает ВСЕ возможные чередования потоков assert_eq!(counter.load(Ordering::SeqCst), 2); }); } } ``` -> **When to use `loom`**: When you have lock-free data structures or custom -> synchronization primitives. Loom exhaustively explores thread interleavings — -> it's a model checker, not a stress test. Not needed for `Mutex`/`RwLock`-based code. +> **Когда использовать `loom`**: когда у вас есть структуры данных без блокировок или +> собственные примитивы синхронизации. Loom исчерпывающе перебирает чередования потоков — +> это проверка модели, а не стресс-тест. Для кода на основе `Mutex`/`RwLock` он не нужен. -### When to Use Which Tool +### Когда какой инструмент использовать ```text -Decision tree for unsafe verification: - -Is the code pure Rust (no FFI)? -├─ Yes → Use Miri (catches Rust-specific UB, Stacked Borrows) -│ Also run ASan in CI for defense-in-depth -└─ No (calls C/C++ code via FFI) - ├─ Memory safety concerns? - │ └─ Yes → Use Valgrind memcheck AND ASan - ├─ Concurrency concerns? - │ └─ Yes → Use TSan (faster) or Helgrind (more thorough) - └─ Memory leak concerns? - └─ Yes → Use Valgrind --leak-check=full +Дерево решений для проверки unsafe-кода: + +Код написан на чистом Rust (без FFI)? +├─ Да → Используйте Miri (ловит UB, специфичный для Rust, Stacked Borrows) +│ В CI также запускайте ASan для защиты в глубину +└─ Нет (вызывает C/C++ код через FFI) + ├─ Есть опасения по безопасности памяти? + │ └─ Да → Используйте Valgrind memcheck И ASan + ├─ Есть опасения по конкурентности? + │ └─ Да → Используйте TSan (быстрее) или Helgrind (тщательнее) + └─ Есть опасения по утечкам памяти? + └─ Да → Используйте Valgrind --leak-check=full ``` -**Recommended CI matrix:** +**Рекомендуемая матрица CI:** ```yaml -# Run all tools in parallel for fast feedback +# Запускаем все инструменты параллельно для быстрой обратной связи jobs: miri: runs-on: ubuntu-latest @@ -449,145 +450,145 @@ jobs: done ``` -### Application: Zero Unsafe — and When You'll Need It +### Применение: ноль unsafe — и когда он понадобится -The project contains **zero `unsafe` blocks** across 90K+ lines of -Rust. This is a remarkable achievement for a systems-level diagnostics tool and -demonstrates that safe Rust is sufficient for: -- IPMI communication (via `std::process::Command` to `ipmitool`) -- GPU queries (via `std::process::Command` to `accel-query`) -- PCIe topology parsing (pure JSON/text parsing) -- SEL record management (pure data structures) -- DER report generation (JSON serialization) +В проекте **ни одного блока `unsafe`** на более чем 90 тысячах строк Rust. Для инструмента +диагностики системного уровня это замечательное достижение, которое показывает, что безопасного +Rust достаточно для: +- Связи с IPMI (через `std::process::Command` к `ipmitool`) +- Запросов к GPU (через `std::process::Command` к `accel-query`) +- Разбора топологии PCIe (чистый разбор JSON и текста) +- Управления записями SEL (чистые структуры данных) +- Генерации DER-отчётов (сериализация JSON) -**When will the project need `unsafe`?** +**Когда проекту понадобится `unsafe`?** -The likely triggers for introducing `unsafe`: +Вероятные причины появления `unsafe`: -| Scenario | Why `unsafe` | Recommended Verification | -|----------|-------------|-------------------------| -| Direct ioctl-based IPMI | `libc::ioctl()` bypasses `ipmitool` subprocess | Miri + Valgrind | -| Direct GPU driver queries | accel-mgmt FFI instead of `accel-query` parsing | Valgrind (C library) | -| Memory-mapped PCIe config | `mmap` for direct config-space reads | ASan + Valgrind | -| Lock-free SEL buffer | `AtomicPtr` for concurrent event collection | Miri + TSan | -| Embedded/no_std variant | Raw pointer manipulation for bare-metal | Miri | +| Сценарий | Почему нужен `unsafe` | Рекомендуемая проверка | +|----------|----------------------|------------------------| +| Прямой IPMI через ioctl | `libc::ioctl()` обходит подпроцесс `ipmitool` | Miri + Valgrind | +| Прямые запросы к драйверу GPU | FFI к accel-mgmt вместо разбора вывода `accel-query` | Valgrind (C-библиотека) | +| Memory-mapped конфигурация PCIe | `mmap` для прямого чтения конфигурационного пространства | ASan + Valgrind | +| Буфер SEL без блокировок | `AtomicPtr` для параллельного сбора событий | Miri + TSan | +| Вариант для встраиваемых систем / `no_std` | Манипуляции с сырыми указателями для bare-metal | Miri | -**Preparation**: Before introducing `unsafe`, add the verification tools to CI: +**Подготовка**: прежде чем вводить `unsafe`, добавьте инструменты проверки в CI: ```toml -# Cargo.toml — add a feature flag for unsafe optimizations +# Cargo.toml — добавляем флаги фич для unsafe-оптимизаций [features] default = [] -direct-ipmi = [] # Enable direct ioctl IPMI instead of ipmitool subprocess -direct-accel-api = [] # Enable accel-mgmt FFI instead of accel-query parsing +direct-ipmi = [] # Включить прямой ioctl-доступ к IPMI вместо подпроцесса ipmitool +direct-accel-api = [] # Включить FFI accel-mgmt вместо разбора вывода accel-query ``` ```rust -// src/ipmi.rs — gated behind a feature flag +// src/ipmi.rs — за флагом фичи #[cfg(feature = "direct-ipmi")] mod direct { - //! Direct IPMI device access via /dev/ipmi0 ioctl. + //! Прямой доступ к IPMI-устройству через ioctl на /dev/ipmi0. //! //! # Safety - //! This module uses `unsafe` for ioctl system calls. - //! Verified with: Miri (where possible), Valgrind memcheck, ASan. + //! Этот модуль использует `unsafe` для системных вызовов ioctl. + //! Проверено: Miri (где возможно), Valgrind memcheck, ASan. use std::os::unix::io::RawFd; - // ... unsafe ioctl implementation ... + // ... реализация unsafe ioctl ... } #[cfg(not(feature = "direct-ipmi"))] mod subprocess { - //! IPMI via ipmitool subprocess (default, fully safe). - // ... current implementation ... + //! IPMI через подпроцесс ipmitool (по умолчанию, полностью безопасно). + // ... текущая реализация ... } ``` -> **Key insight**: Keep `unsafe` behind [feature flags](ch09-no-std-and-feature-verification.md) -> so it can be verified independently. Run `cargo +nightly miri test --features direct-ipmi` -> in [CI](ch11-putting-it-all-together-a-production-cic.md) to continuously verify the unsafe -> paths without affecting the safe default build. +> **Ключевая мысль**: держите `unsafe` за [флагами фич](ch09-no-std-and-feature-verification.md), +> чтобы его можно было проверять независимо. Запускайте `cargo +nightly miri test --features direct-ipmi` +> в [CI](ch11-putting-it-all-together-a-production-cic.md), чтобы непрерывно проверять unsafe-пути, +> не затрагивая безопасную сборку по умолчанию. -### `cargo-careful` — Extra UB Checks on Stable +### `cargo-careful` — дополнительные проверки UB без Miri -[`cargo-careful`](https://github.com/RalfJung/cargo-careful) runs your code -with extra standard library checks enabled — catching some undefined behavior -that normal builds ignore, without requiring nightly or Miri's 10-100× slowdown: +[`cargo-careful`](https://github.com/RalfJung/cargo-careful) запускает ваш код с включёнными +дополнительными проверками стандартной библиотеки. Он ловит часть неопределённого поведения, +которое обычные сборки игнорируют, не требуя nightly или замедления Miri в 10–100×: ```bash -# Install (requires nightly, but runs your code at near-native speed) +# Установка (требуется nightly, но код выполняется почти с нативной скоростью) cargo install cargo-careful -# Run tests with extra UB checks (catches uninitialized memory, invalid values) +# Запуск тестов с дополнительными проверками UB (ловит неинициализированную память и некорректные значения) cargo +nightly careful test -# Run a binary with extra checks +# Запуск бинарника с дополнительными проверками cargo +nightly careful run -- --run-diagnostics ``` -**What `cargo-careful` catches that normal builds don't:** -- Reads of uninitialized memory in `MaybeUninit` and `zeroed()` -- Creating invalid `bool`, `char`, or enum values via transmute -- Unaligned pointer reads/writes -- `copy_nonoverlapping` with overlapping ranges +**Что `cargo-careful` ловит, чего не ловят обычные сборки:** +- Чтение неинициализированной памяти в `MaybeUninit` и `zeroed()` +- Создание некорректных значений `bool`, `char` или перечислений через transmute +- Невыровненное чтение и запись через указатели +- `copy_nonoverlapping` с перекрывающимися диапазонами -**Where it fits in the verification ladder:** +**Место в лестнице проверок:** ```text -Least overhead Most thorough +Наименьшие накладные расходы Наибольшая тщательность ├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤ -│ (0× overhead) (~1.5× overhead) (10-100×) (2×) (10-50×) │ -│ Safe Rust only Catches some UB Pure-Rust FFI+Rust FFI+Rust │ +│ (0× накладные) (~1.5× накладные) (10-100×) (2×) (10-50×) │ +│ Только safe Rust Ловит часть UB Чистый Rust FFI+Rust FFI+Rust │ ``` -> **Recommendation**: Add `cargo +nightly careful test` to CI as a fast safety -> check. It runs at near-native speed (unlike Miri) and catches real bugs that -> safe Rust abstractions mask. +> **Рекомендация**: добавьте `cargo +nightly careful test` в CI как быструю проверку безопасности. +> Он работает почти с нативной скоростью (в отличие от Miri) и ловит реальные ошибки, которые +> маскируют безопасные абстракции Rust. -### Troubleshooting Miri and Sanitizers +### Устранение неполадок с Miri и санитайзерами -| Symptom | Cause | Fix | -|---------|-------|-----| -| `Miri does not support FFI` | Miri is a Rust interpreter; it can't execute C code | Use Valgrind or ASan for FFI code instead | -| `error: unsupported operation: can't call foreign function` | Miri hit an `extern "C"` call | Mock the FFI boundary or gate behind `#[cfg(miri)]` | -| `Stacked Borrows violation` | Aliasing rule violation — even if code "works" | Miri is correct; refactor to avoid aliasing `&mut` with `&` | -| Sanitizer says `DEADLYSIGNAL` | ASan detected buffer overflow | Check array indexing, slice operations, and pointer arithmetic | -| `LeakSanitizer: detected memory leaks` | `Box::leak()`, `forget()`, or missing `drop()` | Intentional: suppress with `__lsan_disable()`; unintentional: fix the leak | -| Miri is extremely slow | Miri interprets, doesn't compile — 10-100× slower | Run only on `--lib` tests or tag specific tests with `#[cfg_attr(miri, ignore)]` for slow ones | -| `TSan: false positive` with atomics | TSan doesn't understand Rust's atomic ordering model perfectly | Add `TSAN_OPTIONS=suppressions=tsan.supp` with specific suppressions | +| Симптом | Причина | Решение | +|---------|---------|---------| +| `Miri does not support FFI` | Miri — интерпретатор Rust, он не может выполнять C-код | Для FFI-кода используйте Valgrind или ASan | +| `error: unsupported operation: can't call foreign function` | Miri встретил вызов `extern "C"` | Замокайте границу FFI или ограничьте код через `#[cfg(miri)]` | +| `Stacked Borrows violation` | Нарушение правила алиасинга — даже если код «работает» | Miri прав; перепишите код, чтобы не алиасить `&mut` с `&` | +| Санитайзер сообщает `DEADLYSIGNAL` | ASan обнаружил переполнение буфера | Проверьте индексацию массивов, операции со срезами и арифметику указателей | +| `LeakSanitizer: detected memory leaks` | `Box::leak()`, `forget()` или пропущенный `drop()` | Намеренно: подавите через `__lsan_disable()`; непреднамеренно: исправьте утечку | +| Miri невероятно медленный | Miri интерпретирует, а не компилирует — в 10–100× медленнее | Запускайте только тесты `--lib` или помечайте медленные тесты через `#[cfg_attr(miri, ignore)]` | +| `TSan: false positive` с атомарными операциями | TSan не до конца понимает модель порядка атомарных операций Rust | Добавьте `TSAN_OPTIONS=suppressions=tsan.supp` с конкретными подавлениями | -### Try It Yourself +### Попробуйте сами -1. **Trigger a Miri UB detection**: Write an `unsafe` function that creates two - `&mut` references to the same `i32` (aliasing violation). Run `cargo +nightly miri test` - and observe the "Stacked Borrows" error. Fix it with `UnsafeCell` or separate allocations. +1. **Вызовите обнаружение UB в Miri**: напишите `unsafe`-функцию, которая создаёт две ссылки + `&mut` на один и тот же `i32` (нарушение алиасинга). Запустите `cargo +nightly miri test` + и посмотрите на ошибку «Stacked Borrows». Исправьте её через `UnsafeCell` или отдельные выделения. -2. **Run ASan on a deliberate bug**: Create a test that does `unsafe` out-of-bounds - array access. Build with `RUSTFLAGS="-Zsanitizer=address"` and observe ASan's - report. Note how it pinpoints the exact line. +2. **Запустите ASan на намеренной ошибке**: создайте тест с `unsafe`-доступом к массиву за + пределами границ. Соберите с `RUSTFLAGS="-Zsanitizer=address"` и посмотрите отчёт ASan. + Обратите внимание, как он указывает точную строку. -3. **Benchmark Miri overhead**: Time `cargo test --lib` vs `cargo +nightly miri test --lib` - on the same test suite. Calculate the slowdown factor. Based on this, decide - which tests to run under Miri in CI and which to skip with `#[cfg_attr(miri, ignore)]`. +3. **Измерьте накладные расходы Miri**: замерьте `cargo test --lib` и `cargo +nightly miri test --lib` + на одном и том же наборе тестов. Рассчитайте коэффициент замедления. По результатам решите, + какие тесты запускать под Miri в CI, а какие пропускать через `#[cfg_attr(miri, ignore)]`. -### Safety Verification Decision Tree +### Дерево решений для проверки безопасности ```mermaid flowchart TD - START["Have unsafe code?"] -->|No| SAFE["Safe Rust — no
verification needed"] - START -->|Yes| KIND{"What kind?"} - - KIND -->|"Pure Rust unsafe"| MIRI["Miri
MIR interpreter
catches aliasing, UB, leaks"] - KIND -->|"FFI / C interop"| VALGRIND["Valgrind memcheck
or ASan"] - KIND -->|"Concurrent unsafe"| CONC{"Lock-free?"} - - CONC -->|"Atomics/lock-free"| LOOM["loom
Model checker for atomics"] - CONC -->|"Mutex/shared state"| TSAN["TSan or
Miri -Zmiri-check-number-validity"] - + START["Есть unsafe-код?"] -->|Нет| SAFE["Безопасный Rust — проверка
не нужна"] + START -->|Да| KIND{"Какой именно?"} + + KIND -->|"Чистый Rust unsafe"| MIRI["Miri
интерпретатор MIR
ловит алиасинг, UB, утечки"] + KIND -->|"FFI / взаимодействие с C"| VALGRIND["Valgrind memcheck
или ASan"] + KIND -->|"Конкурентный unsafe"| CONC{"Без блокировок?"} + + CONC -->|"Атомарные операции / без блокировок"| LOOM["loom
Проверка моделей для атомарных операций"] + CONC -->|"Mutex / разделяемое состояние"| TSAN["TSan или
Miri -Zmiri-check-number-validity"] + MIRI --> CI_MIRI["CI: cargo +nightly miri test"] VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"] - + style SAFE fill:#91e5a3,color:#000 style MIRI fill:#e3f2fd,color:#000 style VALGRIND fill:#ffd43b,color:#000 @@ -595,14 +596,16 @@ flowchart TD style TSAN fill:#ffd43b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟡 Exercise 1: Trigger a Miri UB Detection +#### 🟡 Упражнение 1: вызовите обнаружение UB в Miri -Write an `unsafe` function that creates two `&mut` references to the same `i32` (aliasing violation). Run `cargo +nightly miri test` and observe the Stacked Borrows error. Fix it. +Напишите `unsafe`-функцию, которая создаёт две ссылки `&mut` на один и тот же `i32` +(нарушение алиасинга). Запустите `cargo +nightly miri test`, найдите ошибку Stacked Borrows +и исправьте её.
-Solution +Решение ```rust #[cfg(test)] @@ -612,15 +615,15 @@ mod tests { let mut x: i32 = 42; let ptr = &mut x as *mut i32; unsafe { - // BUG: Two &mut references to the same location + // ОШИБКА: две ссылки &mut на одно и то же место let _a = &mut *ptr; - let _b = &mut *ptr; // Miri: Stacked Borrows violation! + let _b = &mut *ptr; // Miri: нарушение Stacked Borrows! } } } ``` -Fix: use separate allocations or `UnsafeCell`: +Исправление: используйте отдельные выделения или `UnsafeCell`: ```rust use std::cell::UnsafeCell; @@ -636,12 +639,13 @@ fn no_aliasing_ub() { ```
-#### 🔴 Exercise 2: ASan Out-of-Bounds Detection +#### 🔴 Упражнение 2: обнаружение выхода за границы с помощью ASan -Create a test with `unsafe` out-of-bounds array access. Build with `RUSTFLAGS="-Zsanitizer=address"` on nightly and observe ASan's report. +Создайте тест с `unsafe`-доступом к массиву за пределами границ. Соберите на nightly с +`RUSTFLAGS="-Zsanitizer=address"` и посмотрите отчёт ASan.
-Solution +Решение ```rust #[test] @@ -649,7 +653,7 @@ fn oob_access() { let arr = [1u8, 2, 3, 4, 5]; let ptr = arr.as_ptr(); unsafe { - let _val = *ptr.add(10); // Out of bounds! + let _val = *ptr.add(10); // Выход за границы! } } ``` @@ -657,17 +661,20 @@ fn oob_access() { ```bash RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \ --target x86_64-unknown-linux-gnu -- oob_access -# ASan report: stack-buffer-overflow at +# Отчёт ASan: stack-buffer-overflow по адресу <точный адрес> ```
-### Key Takeaways +### Ключевые выводы -- **Miri** is the tool for pure-Rust `unsafe` — it catches aliasing violations, use-after-free, and leaks that compile and pass tests -- **Valgrind** is the tool for FFI/C interop — it works on the final binary without recompilation -- **Sanitizers** (ASan, TSan, MSan) require nightly but run at near-native speed — ideal for large test suites -- **`loom`** is purpose-built for verifying lock-free concurrent data structures -- Run Miri in CI on every push; run sanitizers on a nightly schedule to avoid slowing the main pipeline +- **Miri** — инструмент для чистого Rust `unsafe`: он ловит нарушения алиасинга, use-after-free + и утечки, которые компилируются и проходят тесты +- **Valgrind** — инструмент для FFI и взаимодействия с C: он работает с готовым бинарником + без перекомпиляции +- **Санитайзеры** (ASan, TSan, MSan) требуют nightly, но работают почти с нативной скоростью — + идеально для больших тестовых наборов +- **`loom`** создан специально для проверки структур данных без блокировок +- Запускайте Miri в CI на каждый push, а санитайзеры — по ночному расписанию, чтобы не замедлять + основной конвейер --- - From 5e229384b27baf6f105374dd40cf10a2bbf3fbf2 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:36:11 +0400 Subject: [PATCH 06/18] Translate Engineering book chapters 6-13 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for dependency management, release profiles, compile-time tools, no_std, Windows, CI/CD pipeline, tricks and the quick reference card. Code identifiers, crate names, file names, tool output and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- ...ependency-management-and-supply-chain-s.md | 293 +++++++------ .../ch07-release-profiles-and-binary-size.md | 314 +++++++------- .../ch08-compile-time-and-developer-tools.md | 407 +++++++++--------- .../ch09-no-std-and-feature-verification.md | 343 +++++++-------- ...h10-windows-and-conditional-compilation.md | 315 +++++++------- ...utting-it-all-together-a-production-cic.md | 344 ++++++++------- .../src/ch12-tricks-from-the-trenches.md | 236 +++++----- .../src/ch13-quick-reference-card.md | 293 +++++++------ 8 files changed, 1270 insertions(+), 1275 deletions(-) diff --git a/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md b/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md index 75c37cb..22b755c 100644 --- a/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md +++ b/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md @@ -1,33 +1,33 @@ -# Dependency Management and Supply Chain Security 🟢 - -> **What you'll learn:** -> - Scanning for known vulnerabilities with `cargo-audit` -> - Enforcing license, advisory, and source policies with `cargo-deny` -> - Supply chain trust verification with Mozilla's `cargo-vet` -> - Tracking outdated dependencies and detecting breaking API changes -> - Visualizing and deduplicating your dependency tree +# Управление зависимостями и безопасность цепочки поставок 🟢 + +> **Чему вы научитесь:** +> - Поиск известных уязвимостей с помощью `cargo-audit` +> - Соблюдение политик по лицензиям, рекомендациям и источникам с помощью `cargo-deny` +> - Проверка доверия к цепочке поставок с помощью `cargo-vet` от Mozilla +> - Отслеживание устаревших зависимостей и обнаружение несовместимых изменений API +> - Визуализация и дедупликация дерева зависимостей > -> **Cross-references:** [Release Profiles](ch07-release-profiles-and-binary-size.md) — `cargo-udeps` trims unused dependencies found here · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) — audit and deny jobs in the pipeline · [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) — `build-dependencies` are part of your supply chain too +> **Перекрёстные ссылки:** [Профили релиза](ch07-release-profiles-and-binary-size.md) — `cargo-udeps` убирает неиспользуемые зависимости, найденные здесь · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) — джобы audit и deny в конвейере · [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) — `build-dependencies` тоже входят в вашу цепочку поставок -A Rust binary doesn't just contain your code — it contains every transitive -dependency in your `Cargo.lock`. A vulnerability, license violation, or -malicious crate anywhere in that tree becomes *your* problem. This chapter -covers the tools that make dependency management auditable and automated. +Бинарник на Rust содержит не только ваш код — он содержит все транзитивные зависимости +из `Cargo.lock`. Уязвимость, нарушение лицензии или вредоносный крейт в любой точке этого +дерева становятся *вашей* проблемой. Эта глава описывает инструменты, которые делают +управление зависимостями поддающимся аудиту и автоматизированным. -### cargo-audit — Known Vulnerability Scanning +### cargo-audit — поиск известных уязвимостей -[`cargo-audit`](https://github.com/rustsec/rustsec/tree/main/cargo-audit) -checks your `Cargo.lock` against the [RustSec Advisory Database](https://rustsec.org/), -which tracks known vulnerabilities in published crates. +[`cargo-audit`](https://github.com/rustsec/rustsec/tree/main/cargo-audit) сверяет ваш +`Cargo.lock` с [базой рекомендаций RustSec](https://rustsec.org/), в которой отслеживаются +известные уязвимости опубликованных крейтов. ```bash -# Install +# Установка cargo install cargo-audit -# Scan for known vulnerabilities +# Поиск известных уязвимостей cargo audit -# Output: +# Вывод: # Crate: chrono # Version: 0.4.19 # Title: Potential segfault in localtime_r invocations @@ -36,24 +36,24 @@ cargo audit # URL: https://rustsec.org/advisories/RUSTSEC-2020-0159 # Solution: Upgrade to >= 0.4.20 -# Check and fail CI if vulnerabilities exist +# Проверка с падением CI, если есть уязвимости cargo audit --deny warnings -# Generate JSON output for automated processing +# Генерация JSON-вывода для автоматической обработки cargo audit --json -# Fix vulnerabilities by updating Cargo.lock +# Исправление уязвимостей обновлением Cargo.lock cargo audit fix ``` -**CI integration:** +**Интеграция с CI:** ```yaml # .github/workflows/audit.yml -name: Security Audit +name: Проверка безопасности on: schedule: - - cron: '0 0 * * *' # Daily check — advisories appear continuously + - cron: '0 0 * * *' # Ежедневная проверка — рекомендации появляются постоянно push: paths: ['Cargo.lock'] @@ -67,47 +67,47 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ``` -### cargo-deny — Comprehensive Policy Enforcement +### cargo-deny — комплексное применение политик -[`cargo-deny`](https://github.com/EmbarkStudios/cargo-deny) goes far beyond -vulnerability scanning. It enforces policies across four dimensions: +[`cargo-deny`](https://github.com/EmbarkStudios/cargo-deny) выходит далеко за рамки поиска +уязвимостей. Он применяет политики по четырём направлениям: -1. **Advisories** — known vulnerabilities (like cargo-audit) -2. **Licenses** — allowed/denied license list -3. **Bans** — forbidden crates or duplicate versions -4. **Sources** — allowed registries and git sources +1. **Рекомендации (advisories)** — известные уязвимости (как и в cargo-audit) +2. **Лицензии** — список разрешённых и запрещённых лицензий +3. **Ограничения (bans)** — запрещённые крейты или дублирующиеся версии +4. **Источники (sources)** — разрешённые реестры и git-источники ```bash -# Install +# Установка cargo install cargo-deny -# Initialize configuration +# Инициализация конфигурации cargo deny init -# Creates deny.toml with documented defaults +# Создаёт deny.toml с документированными значениями по умолчанию -# Run all checks +# Запуск всех проверок cargo deny check -# Run specific checks +# Запуск отдельных проверок cargo deny check advisories cargo deny check licenses cargo deny check bans cargo deny check sources ``` -**Example `deny.toml`:** +**Пример `deny.toml`:** ```toml # deny.toml [advisories] -vulnerability = "deny" # Fail on known vulnerabilities -unmaintained = "warn" # Warn on unmaintained crates -yanked = "deny" # Fail on yanked crates -notice = "warn" # Warn on informational advisories +vulnerability = "deny" # Падать при известных уязвимостях +unmaintained = "warn" # Предупреждать о неподдерживаемых крейтах +yanked = "deny" # Падать на отозванных (yanked) крейтах +notice = "warn" # Предупреждать об информационных рекомендациях [licenses] -unlicensed = "deny" # All crates must have a license +unlicensed = "deny" # У всех крейтов должна быть лицензия allow = [ "MIT", "Apache-2.0", @@ -116,109 +116,107 @@ allow = [ "ISC", "Unicode-DFS-2016", ] -copyleft = "deny" # No GPL/LGPL/AGPL in this project -default = "deny" # Deny anything not explicitly allowed +copyleft = "deny" # Никаких GPL/LGPL/AGPL в этом проекте +default = "deny" # Запрещать всё, что не разрешено явно [bans] -multiple-versions = "warn" # Warn if same crate appears at 2 versions -wildcards = "deny" # No path = "*" in dependencies -highlight = "all" # Show all duplicates, not just first +multiple-versions = "warn" # Предупреждать, если один крейт присутствует в 2 версиях +wildcards = "deny" # Никаких path = "*" в зависимостях +highlight = "all" # Показывать все дубликаты, а не только первый -# Ban specific problematic crates +# Запрещаем отдельные проблемные крейты deny = [ - # openssl-sys pulls in C OpenSSL — prefer rustls + # openssl-sys подтягивает C-библиотеку OpenSSL — предпочитайте rustls { name = "openssl-sys", wrappers = ["native-tls"] }, ] -# Allow specific duplicate versions (when unavoidable) +# Разрешаем отдельные дублирующиеся версии (когда их не избежать) [[bans.skip]] name = "syn" -version = "1.0" # syn 1.x and 2.x often coexist +version = "1.0" # syn 1.x и 2.x часто сосуществуют [sources] -unknown-registry = "deny" # Only allow crates.io -unknown-git = "deny" # No random git dependencies +unknown-registry = "deny" # Разрешён только crates.io +unknown-git = "deny" # Никаких случайных git-зависимостей allow-registry = ["https://github.com/rust-lang/crates.io-index"] ``` -**License enforcement** is particularly valuable for commercial projects: +**Контроль лицензий** особенно ценен для коммерческих проектов: ```bash -# Check which licenses are in your dependency tree +# Проверить, какие лицензии есть в дереве зависимостей cargo deny list -# Output: +# Вывод: # MIT — 127 crates # Apache-2.0 — 89 crates # BSD-3-Clause — 12 crates -# MPL-2.0 — 3 crates ← might need legal review +# MPL-2.0 — 3 crates ← может потребоваться юридическая проверка # Unicode-DFS — 1 crate ``` -### cargo-vet — Supply Chain Trust Verification +### cargo-vet — проверка доверия к цепочке поставок -[`cargo-vet`](https://github.com/mozilla/cargo-vet) (from Mozilla) addresses a -different question: not "does this crate have known bugs?" but "has a trusted -human actually reviewed this code?" +[`cargo-vet`](https://github.com/mozilla/cargo-vet) (от Mozilla) отвечает на другой вопрос: +не «есть ли у этого крейта известные баги?», а «проверил ли этот код доверенный человек?» ```bash -# Install +# Установка cargo install cargo-vet -# Initialize (creates supply-chain/ directory) +# Инициализация (создаёт директорию supply-chain/) cargo vet init -# Check which crates need review +# Проверить, какие крейты нужно проверить cargo vet -# After reviewing a crate, certify it: +# После проверки крейта подтвердите это: cargo vet certify serde 1.0.203 -# Records that you've audited serde 1.0.203 for your criteria +# Фиксирует, что вы проверили serde 1.0.203 по своим критериям -# Import audits from trusted organizations +# Импорт аудитов от доверенных организаций cargo vet import mozilla cargo vet import google cargo vet import bytecode-alliance ``` -**How it works:** +**Как это работает:** ```text supply-chain/ -├── audits.toml ← Your team's audit certifications -├── config.toml ← Trust configuration and criteria -└── imports.lock ← Pinned imports from other organizations +├── audits.toml ← Сертификаты аудита вашей команды +├── config.toml ← Конфигурация доверия и критерии +└── imports.lock ← Зафиксированные импорты от других организаций ``` -`cargo-vet` is most valuable for organizations with strict supply-chain -requirements (government, finance, infrastructure). For most teams, -`cargo-deny` provides sufficient protection. +`cargo-vet` наиболее полезен организациям со строгими требованиями к цепочке поставок +(госсектор, финансы, инфраструктура). Для большинства команд `cargo-deny` даёт достаточную защиту. -### cargo-outdated and cargo-semver-checks +### cargo-outdated и cargo-semver-checks -**`cargo-outdated`** — find dependencies that have newer versions: +**`cargo-outdated`** — поиск зависимостей, для которых есть более новые версии: ```bash cargo install cargo-outdated cargo outdated --workspace -# Output: +# Вывод: # Name Project Compat Latest Kind # serde 1.0.193 1.0.203 1.0.203 Normal # regex 1.9.6 1.10.4 1.10.4 Normal -# thiserror 1.0.50 1.0.61 2.0.3 Normal ← major version available +# thiserror 1.0.50 1.0.61 2.0.3 Normal ← доступна мажорная версия ``` -**`cargo-semver-checks`** — detect breaking API changes before publishing. -Essential for library crates: +**`cargo-semver-checks`** — обнаружение несовместимых изменений API до публикации. +Незаменим для библиотечных крейтов: ```bash cargo install cargo-semver-checks -# Check if your changes are semver-compatible +# Проверить, совместимы ли ваши изменения с semver cargo semver-checks -# Output: +# Вывод: # ✗ Function `parse_gpu_csv` is now private (was public) # → This is a BREAKING change. Bump MAJOR version. # @@ -228,22 +226,22 @@ cargo semver-checks # ✓ Function `parse_gpu_csv_v2` was added (non-breaking) ``` -### cargo-tree — Dependency Visualization and Deduplication +### cargo-tree — визуализация и дедупликация зависимостей -`cargo tree` is built into Cargo (no installation needed) and is invaluable -for understanding your dependency graph: +`cargo tree` встроен в Cargo (ничего устанавливать не нужно) и незаменим для понимания +графа зависимостей: ```bash -# Full dependency tree +# Полное дерево зависимостей cargo tree -# Find why a specific crate is included +# Понять, почему включён конкретный крейт cargo tree --invert --package openssl-sys -# Shows all paths from your crate to openssl-sys +# Показывает все пути от вашего крейта к openssl-sys -# Find duplicate versions +# Найти дублирующиеся версии cargo tree --duplicates -# Output: +# Вывод: # syn v1.0.109 # └── serde_derive v1.0.193 # @@ -251,29 +249,28 @@ cargo tree --duplicates # ├── thiserror-impl v1.0.56 # └── tokio-macros v2.2.0 -# Show only direct dependencies +# Показать только прямые зависимости cargo tree --depth 1 -# Show dependency features +# Показать фичи зависимостей cargo tree --format "{p} {f}" -# Count total dependencies +# Подсчитать общее количество зависимостей cargo tree | wc -l ``` -**Deduplication strategy**: When `cargo tree --duplicates` shows the same crate -at two major versions, check if you can update the dependency chain to unify them. -Each duplicate adds compile time and binary size. +**Стратегия дедупликации**: если `cargo tree --duplicates` показывает один и тот же крейт +в двух мажорных версиях, проверьте, можно ли обновить цепочку зависимостей, чтобы объединить +их. Каждый дубликат увеличивает время компиляции и размер бинарника. -### Application: Multi-Crate Dependency Hygiene +### Применение: гигиена зависимостей в мультикрейтовом проекте -The workspace uses `[workspace.dependencies]` for centralized -version management — an excellent practice. Combined with -[`cargo tree --duplicates`](ch07-release-profiles-and-binary-size.md) for size -analysis, this prevents version drift and reduces binary bloat: +Воркспейс использует `[workspace.dependencies]` для централизованного управления версиями — +отличная практика. В сочетании с [`cargo tree --duplicates`](ch07-release-profiles-and-binary-size.md) +для анализа размера это предотвращает расхождение версий и уменьшает раздувание бинарника: ```toml -# Root Cargo.toml — all versions pinned in one place +# Корневой Cargo.toml — все версии зафиксированы в одном месте [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } @@ -283,17 +280,17 @@ anyhow = "1.0" rayon = "1.8" ``` -**Recommended additions for the project:** +**Рекомендуемые дополнения для проекта:** ```bash -# Add to CI pipeline: -cargo deny init # One-time setup -cargo deny check # Every PR — licenses, advisories, bans -cargo audit --deny warnings # Every push — vulnerability scanning -cargo outdated --workspace # Weekly — track available updates +# Добавить в CI-конвейер: +cargo deny init # Однократная настройка +cargo deny check # Каждый PR — лицензии, рекомендации, ограничения +cargo audit --deny warnings # Каждый push — поиск уязвимостей +cargo outdated --workspace # Раз в неделю — отслеживание доступных обновлений ``` -**Recommended `deny.toml` for the project:** +**Рекомендуемый `deny.toml` для проекта:** ```toml [advisories] @@ -302,10 +299,10 @@ yanked = "deny" [licenses] allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"] -copyleft = "deny" # Hardware diagnostics tool — no copyleft +copyleft = "deny" # Инструмент диагностики железа — никакого copyleft [bans] -multiple-versions = "warn" # Track duplicates, don't block yet +multiple-versions = "warn" # Отслеживаем дубликаты, пока не блокируем wildcards = "deny" [sources] @@ -313,79 +310,81 @@ unknown-registry = "deny" unknown-git = "deny" ``` -### Supply Chain Audit Pipeline +### Конвейер аудита цепочки поставок ```mermaid flowchart LR - PR["Pull Request"] --> AUDIT["cargo audit
Known CVEs"] - AUDIT --> DENY["cargo deny check
Licenses + Bans + Sources"] - DENY --> OUTDATED["cargo outdated
Weekly schedule"] - OUTDATED --> SEMVER["cargo semver-checks
Library crates only"] - - AUDIT -->|"Fail"| BLOCK["❌ Block merge"] - DENY -->|"Fail"| BLOCK - SEMVER -->|"Breaking change"| BUMP["Bump major version"] - + PR["Pull Request"] --> AUDIT["cargo audit
Известные CVE"] + AUDIT --> DENY["cargo deny check
Лицензии + Ограничения + Источники"] + DENY --> OUTDATED["cargo outdated
По расписанию, раз в неделю"] + OUTDATED --> SEMVER["cargo semver-checks
Только библиотечные крейты"] + + AUDIT -->|"Ошибка"| BLOCK["❌ Блокировать слияние"] + DENY -->|"Ошибка"| BLOCK + SEMVER -->|"Несовместимое изменение"| BUMP["Поднять мажорную версию"] + style BLOCK fill:#ff6b6b,color:#000 style BUMP fill:#ffd43b,color:#000 style PR fill:#e3f2fd,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Audit Your Dependencies +#### 🟢 Упражнение 1: проверьте свои зависимости -Run `cargo audit` and `cargo deny init && cargo deny check` on any Rust project. How many advisories are found? How many license categories are in your tree? +Запустите `cargo audit` и `cargo deny init && cargo deny check` на любом проекте на Rust. +Сколько рекомендаций найдено? Сколько категорий лицензий в вашем дереве?
-Solution +Решение ```bash cargo audit -# Note any advisories — often chrono, time, or older crates +# Обратите внимание на рекомендации — чаще всего это chrono, time или старые крейты cargo deny init cargo deny list -# Shows license breakdown: MIT (N), Apache-2.0 (N), etc. +# Показывает разбивку по лицензиям: MIT (N), Apache-2.0 (N) и т. д. cargo deny check -# Shows full audit across all four dimensions +# Показывает полную проверку по всем четырём направлениям ```
-#### 🟡 Exercise 2: Find and Eliminate Duplicate Dependencies +#### 🟡 Упражнение 2: найдите и устраните дублирующиеся зависимости -Run `cargo tree --duplicates` on a workspace. Find a crate that appears at two versions. Can you update `Cargo.toml` to unify them? Measure the compile-time and binary-size impact. +Запустите `cargo tree --duplicates` на воркспейсе. Найдите крейт, который присутствует в двух +версиях. Можно ли обновить `Cargo.toml`, чтобы их объединить? Измерьте влияние на время +компиляции и размер бинарника.
-Solution +Решение ```bash cargo tree --duplicates -# Typical: syn 1.x and syn 2.x +# Типично: syn 1.x и syn 2.x -# Find who pulls in the old version: +# Выясняем, кто подтягивает старую версию: cargo tree --invert --package syn@1.0.109 -# Output: serde_derive 1.0.xxx -> syn 1.0.109 +# Вывод: serde_derive 1.0.xxx -> syn 1.0.109 -# Check if a newer serde_derive uses syn 2.x: +# Проверяем, использует ли новый serde_derive syn 2.x: cargo update -p serde_derive cargo tree --duplicates -# If syn 1.x is gone, you've eliminated a duplicate +# Если syn 1.x исчез, вы устранили дубликат -# Measure impact: -time cargo build --release # Before and after +# Измеряем влияние: +time cargo build --release # До и после cargo bloat --release --crates | head -20 ```
-### Key Takeaways +### Ключевые выводы -- `cargo audit` catches known CVEs — run it on every push and on a daily schedule -- `cargo deny` enforces four policy dimensions: advisories, licenses, bans, and sources -- Use `[workspace.dependencies]` to centralize version management across a multi-crate workspace -- `cargo tree --duplicates` reveals bloat; each duplicate adds compile time and binary size -- `cargo-vet` is for high-security environments; `cargo-deny` is sufficient for most teams +- `cargo audit` ловит известные CVE — запускайте его на каждый push и по ежедневному расписанию +- `cargo deny` применяет политики по четырём направлениям: рекомендации, лицензии, ограничения и источники +- Используйте `[workspace.dependencies]`, чтобы централизовать управление версиями в мультикрейтовом воркспейсе +- `cargo tree --duplicates` выявляет раздувание: каждый дубликат увеличивает время компиляции и размер бинарника +- `cargo-vet` предназначен для сред с высокими требованиями к безопасности; для большинства команд достаточно `cargo-deny` --- - diff --git a/engineering-book/src/ch07-release-profiles-and-binary-size.md b/engineering-book/src/ch07-release-profiles-and-binary-size.md index 1709352..4ffedc7 100644 --- a/engineering-book/src/ch07-release-profiles-and-binary-size.md +++ b/engineering-book/src/ch07-release-profiles-and-binary-size.md @@ -1,144 +1,144 @@ -# Release Profiles and Binary Size 🟡 +# Профили релиза и размер бинарного файла 🟡 -> **What you'll learn:** -> - Release profile anatomy: LTO, codegen-units, panic strategy, strip, opt-level -> - Thin vs Fat vs Cross-Language LTO trade-offs -> - Binary size analysis with `cargo-bloat` -> - Dependency trimming with `cargo-udeps`, `cargo-machete` and `cargo-shear` +> **Чему вы научитесь:** +> - Устройство профиля релиза: LTO, codegen-units, стратегия паники, strip, opt-level +> - Компромиссы thin LTO, fat LTO и кросс-языкового LTO +> - Анализ размера бинарника с помощью `cargo-bloat` +> - Сокращение зависимостей с помощью `cargo-udeps`, `cargo-machete` и `cargo-shear` > -> **Cross-references:** [Compile-Time Tools](ch08-compile-time-and-developer-tools.md) — the other half of optimization · [Benchmarking](ch03-benchmarking-measuring-what-matters.md) — measure runtime before you optimize · [Dependencies](ch06-dependency-management-and-supply-chain-s.md) — trimming deps reduces both size and compile time +> **Перекрёстные ссылки:** [Инструменты времени компиляции](ch08-compile-time-and-developer-tools.md) — вторая половина оптимизации · [Бенчмаркинг](ch03-benchmarking-measuring-what-matters.md) — измеряйте время выполнения до оптимизации · [Зависимости](ch06-dependency-management-and-supply-chain-s.md) — сокращение зависимостей уменьшает и размер, и время компиляции -The default `cargo build --release` is already good. But for production -deployment — especially single-binary tools deployed to thousands of servers — -there's a significant gap between "good" and "optimized." This chapter covers -the profile knobs and the tools to measure binary size. +Стандартный `cargo build --release` уже хорош. Но для продакшн-развёртывания — особенно +инструментов из одного бинарника, которые ставятся на тысячи серверов, — между «хорошо» +и «оптимизировано» есть существенная разница. Эта глава описывает ручки настройки профиля +и инструменты для измерения размера бинарника. -### Release Profile Anatomy +### Устройство профиля релиза -Cargo profiles control how `rustc` compiles your code. The defaults are -conservative — designed for broad compatibility, not maximum performance: +Профили Cargo определяют, как `rustc` компилирует ваш код. Значения по умолчанию +консервативны — они рассчитаны на широкую совместимость, а не на максимальную производительность: ```toml -# Cargo.toml — Cargo's built-in defaults (what you get if you specify nothing) +# Cargo.toml — встроенные значения Cargo по умолчанию (то, что получается, если ничего не указывать) [profile.release] -opt-level = 3 # Optimization level (0=none, 1=basic, 2=good, 3=aggressive) -lto = false # Link-time optimization OFF -codegen-units = 16 # Parallel compilation units (faster compile, less optimization) -panic = "unwind" # Stack unwinding on panic (larger binary, catch_unwind works) -strip = "none" # Keep all symbols and debug info -overflow-checks = false # No integer overflow checks in release -debug = false # No debug info in release +opt-level = 3 # Уровень оптимизации (0=нет, 1=базовый, 2=хороший, 3=агрессивный) +lto = false # Оптимизация на этапе линковки ВЫКЛЮЧЕНА +codegen-units = 16 # Параллельные единицы генерации кода (быстрее компиляция, меньше оптимизации) +panic = "unwind" # Раскрутка стека при панике (бинарник больше, catch_unwind работает) +strip = "none" # Сохранять все символы и отладочную информацию +overflow-checks = false # Проверки переполнения целых чисел в release отключены +debug = false # Отладочной информации в release нет ``` -**Production-optimized profile** (what the project already uses): +**Оптимизированный профиль для продакшна** (то, что уже использует проект): ```toml [profile.release] -lto = true # Full cross-crate optimization -codegen-units = 1 # Single codegen unit — maximum optimization opportunity -panic = "abort" # No unwinding overhead — smaller, faster -strip = true # Remove all symbols — smaller binary +lto = true # Полная оптимизация между крейтами +codegen-units = 1 # Одна единица генерации кода — максимум возможностей для оптимизации +panic = "abort" # Без накладных расходов на раскрутку — меньше и быстрее +strip = true # Удалить все символы — меньше бинарник ``` -**The impact of each setting:** +**Влияние каждой настройки:** -| Setting | Default → Optimized | Binary Size | Runtime Speed | Compile Time | -|---------|---------------------|-------------|---------------|--------------| -| `lto = false → true` | — | -10 to -20% | +5 to +20% | 2-5× slower | -| `codegen-units = 16 → 1` | — | -5 to -10% | +5 to +10% | 1.5-2× slower | -| `panic = "unwind" → "abort"` | — | -5 to -10% | Negligible | Negligible | -| `strip = "none" → true` | — | -50 to -70% | None | None | -| `opt-level = 3 → "s"` | — | -10 to -30% | -5 to -10% | Similar | -| `opt-level = 3 → "z"` | — | -15 to -40% | -10 to -20% | Similar | +| Настройка | По умолчанию → оптимизировано | Размер бинарника | Скорость выполнения | Время компиляции | +|-----------|-------------------------------|------------------|---------------------|------------------| +| `lto = false → true` | — | -10 до -20% | +5 до +20% | В 2–5× медленнее | +| `codegen-units = 16 → 1` | — | -5 до -10% | +5 до +10% | В 1,5–2× медленнее | +| `panic = "unwind" → "abort"` | — | -5 до -10% | Незначительно | Незначительно | +| `strip = "none" → true` | — | -50 до -70% | Нет | Нет | +| `opt-level = 3 → "s"` | — | -10 до -30% | -5 до -10% | Примерно так же | +| `opt-level = 3 → "z"` | — | -15 до -40% | -10 до -20% | Примерно так же | -**Additional profile tweaks:** +**Дополнительные тонкие настройки профиля:** ```toml [profile.release] -# All of the above, plus: -overflow-checks = true # Keep overflow checks even in release (safety > speed) -debug = "line-tables-only" # Minimal debug info for backtraces without full DWARF -rpath = false # Don't embed runtime library paths -incremental = false # Disable incremental compilation (cleaner builds) - -# For size-optimized builds (embedded, WASM): -# opt-level = "z" # Optimize for size aggressively -# strip = "symbols" # Strip symbols but keep debug sections +# Всё перечисленное выше, плюс: +overflow-checks = true # Оставить проверки переполнения даже в release (безопасность важнее скорости) +debug = "line-tables-only" # Минимальная отладочная информация для бэктрейсов без полного DWARF +rpath = false # Не встраивать пути к runtime-библиотекам +incremental = false # Отключить инкрементальную компиляцию (чище сборки) + +# Для сборок, оптимизированных по размеру (встраиваемые системы, WASM): +# opt-level = "z" # Агрессивная оптимизация по размеру +# strip = "symbols" # Удалить символы, но оставить секции отладки ``` -**Per-crate profile overrides** — optimize hot crates, leave others alone: +**Переопределения профиля для отдельных крейтов** — оптимизируйте горячие крейты, остальные не трогайте: ```toml -# Dev builds: optimize dependencies but not your code (fast recompile) +# Dev-сборки: оптимизируем зависимости, но не свой код (быстрая пересборка) [profile.dev.package."*"] -opt-level = 2 # Optimize all dependencies in dev mode +opt-level = 2 # Оптимизировать все зависимости в dev-режиме -# Release builds: override specific crate optimization +# Release-сборки: переопределяем оптимизацию для конкретного крейта [profile.release.package.serde_json] -opt-level = 3 # Maximum optimization for JSON parsing +opt-level = 3 # Максимальная оптимизация для разбора JSON codegen-units = 1 -# Test profile: match release behavior for accurate integration tests +# Профиль тестов: совпадает с release для точных интеграционных тестов [profile.test] -opt-level = 1 # Some optimization to avoid timeout in slow tests +opt-level = 1 # Небольшая оптимизация, чтобы медленные тесты не упирались в таймаут ``` -### LTO in Depth — Thin vs Fat vs Cross-Language +### LTO подробно — thin, fat и кросс-языковой -Link-Time Optimization lets LLVM optimize across crate boundaries — inlining -functions from `serde_json` into your parsing code, removing dead code from -`regex`, etc. Without LTO, each crate is a separate optimization island. +Link-Time Optimization позволяет LLVM оптимизировать код через границы крейтов — встраивать +функции из `serde_json` в ваш код разбора, удалять мёртвый код из `regex` и т. д. Без LTO +каждый крейт — это отдельный «островок» оптимизации. ```toml [profile.release] -# Option 1: Fat LTO (default when lto = true) +# Вариант 1: Fat LTO (по умолчанию при lto = true) lto = true -# All code merged into one LLVM module → maximum optimization -# Slowest compile, smallest/fastest binary +# Весь код объединяется в один модуль LLVM → максимальная оптимизация +# Самая долгая компиляция, самый маленький и быстрый бинарник -# Option 2: Thin LTO +# Вариант 2: Thin LTO lto = "thin" -# Each crate stays separate but LLVM does cross-module optimization -# Faster compile than fat LTO, nearly as good optimization -# Best trade-off for most projects +# Каждый крейт остаётся отдельным, но LLVM делает межмодульную оптимизацию +# Компиляция быстрее, чем у fat LTO, оптимизация почти такая же +# Лучший компромисс для большинства проектов -# Option 3: No LTO +# Вариант 3: без LTO lto = false -# Only intra-crate optimization -# Fastest compile, larger binary +# Только оптимизация внутри крейта +# Самая быстрая компиляция, бинарник больше -# Option 4: Off (explicit) +# Вариант 4: выключено (явно) lto = "off" -# Same as false +# То же, что false ``` -**Fat LTO vs Thin LTO:** +**Fat LTO и Thin LTO:** -| Aspect | Fat LTO (`true`) | Thin LTO (`"thin"`) | -|--------|-------------------|----------------------| -| Optimization quality | Best | ~95% of fat | -| Compile time | Slow (all code in one module) | Moderate (parallel modules) | -| Memory usage | High (all LLVM IR in memory) | Lower (streaming) | -| Parallelism | None (single module) | Good (per-module) | -| Recommended for | Final release builds | CI builds, development | +| Аспект | Fat LTO (`true`) | Thin LTO (`"thin"`) | +|--------|------------------|---------------------| +| Качество оптимизации | Лучшее | ~95% от fat | +| Время компиляции | Долго (весь код в одном модуле) | Умеренно (параллельные модули) | +| Потребление памяти | Высокое (весь LLVM IR в памяти) | Ниже (потоковая обработка) | +| Параллелизм | Нет (один модуль) | Хороший (по модулям) | +| Рекомендуется для | Финальных релизных сборок | Сборок CI и разработки | -**Cross-language LTO** — optimize across Rust and C boundaries: +**Кросс-языковой LTO** — оптимизация через границы Rust и C: ```toml [profile.release] lto = true -# Cargo.toml — for crates using the cc crate +# Cargo.toml — для крейтов, использующих крейт cc [build-dependencies] cc = "1.0" ``` ```rust -// build.rs — enable cross-language (linker-plugin) LTO +// build.rs — включаем кросс-языковой LTO (через линкер-плагин) fn main() { - // The cc crate respects CFLAGS from the environment. - // For cross-language LTO, compile C code with: + // Крейт cc учитывает CFLAGS из окружения. + // Для кросс-языкового LTO компилируйте C-код с флагами: // -flto=thin -O2 cc::Build::new() .file("csrc/fast_parser.c") @@ -149,36 +149,36 @@ fn main() { ``` ```bash -# Enable linker-plugin LTO (requires compatible LLD or gold linker) +# Включаем LTO через линкер-плагин (требуется совместимый линкер LLD или gold) RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \ cargo build --release ``` -Cross-language LTO allows LLVM to inline C functions into Rust callers -and vice versa. This is most impactful for FFI-heavy code where small C -functions are called frequently (e.g., IPMI ioctl wrappers). +Кросс-языковой LTO позволяет LLVM встраивать C-функции в вызывающий их Rust-код и наоборот. +Наибольший эффект — в коде с большим количеством FFI, где часто вызываются маленькие +C-функции (например, обёртки над ioctl для IPMI). -### Binary Size Analysis with cargo-bloat +### Анализ размера бинарника с cargo-bloat -[`cargo-bloat`](https://github.com/RazrFalcon/cargo-bloat) answers: -"What functions and crates are taking up the most space in my binary?" +[`cargo-bloat`](https://github.com/RazrFalcon/cargo-bloat) отвечает на вопрос: +«Какие функции и крейты занимают больше всего места в моём бинарнике?» ```bash -# Install +# Установка cargo install cargo-bloat -# Show largest functions +# Показать самые большие функции cargo bloat --release -n 20 -# Output: +# Вывод: # File .text Size Crate Name # 2.8% 5.1% 78.5KiB serde_json serde_json::de::Deserializer::parse_... # 2.1% 3.8% 58.2KiB regex_syntax regex_syntax::ast::parse::ParserI::p... # 1.5% 2.7% 42.1KiB accel_diag accel_diag::vendor::parse_smi_output # ... -# Show by crate (which dependencies are biggest) +# Показать по крейтам (какие зависимости самые большие) cargo bloat --release --crates -# Output: +# Вывод: # File .text Size Crate # 12.3% 22.1% 340KiB serde_json # 8.7% 15.6% 240KiB regex @@ -186,81 +186,81 @@ cargo bloat --release --crates # 5.1% 9.2% 141KiB accel_diag # ... -# Compare two builds (before/after optimization) +# Сравнить две сборки (до и после оптимизации) cargo bloat --release --crates > before.txt -# ... make changes ... +# ... вносим изменения ... cargo bloat --release --crates > after.txt diff before.txt after.txt ``` -**Common bloat sources and fixes:** +**Типичные источники раздувания и способы борьбы с ними:** -| Bloat Source | Typical Size | Fix | -|-------------|-------------|-----| -| `regex` (full engine) | 200-400 KB | Use `regex-lite` if you don't need Unicode | -| `serde_json` (full) | 200-350 KB | Consider `simd-json` or `sonic-rs` if perf matters | -| Generics monomorphization | Varies | Use `dyn Trait` at API boundaries | -| Formatting machinery (`Display`, `Debug`) | 50-150 KB | `#[derive(Debug)]` on large enums adds up | -| Panic message strings | 20-80 KB | `panic = "abort"` removes unwinding, `strip` removes strings | -| Unused features | Varies | Disable default features: `serde = { version = "1", default-features = false }` | +| Источник раздувания | Типичный размер | Решение | +|---------------------|-----------------|---------| +| `regex` (полный движок) | 200–400 КБ | Используйте `regex-lite`, если Unicode не нужен | +| `serde_json` (полный) | 200–350 КБ | Рассмотрите `simd-json` или `sonic-rs`, если важна производительность | +| Мономорфизация обобщений | Зависит от случая | Используйте `dyn Trait` на границах API | +| Механизмы форматирования (`Display`, `Debug`) | 50–150 КБ | `#[derive(Debug)]` на больших перечислениях накапливается | +| Строки сообщений о панике | 20–80 КБ | `panic = "abort"` убирает раскрутку, `strip` убирает строки | +| Неиспользуемые фичи | Зависит от случая | Отключите фичи по умолчанию: `serde = { version = "1", default-features = false }` | -### Trimming Dependencies with cargo-udeps +### Сокращение зависимостей с помощью cargo-udeps -[`cargo-udeps`](https://github.com/est31/cargo-udeps) finds dependencies -declared in `Cargo.toml` that your code doesn't actually use: +[`cargo-udeps`](https://github.com/est31/cargo-udeps) находит зависимости, объявленные в +`Cargo.toml`, которые ваш код на самом деле не использует: ```bash -# Install (requires nightly) +# Установка (требуется nightly) cargo install cargo-udeps -# Find unused dependencies +# Поиск неиспользуемых зависимостей cargo +nightly udeps --workspace -# Output: +# Вывод: # unused dependencies: # `diag_tool v0.1.0` # └── "tempfile" (dev-dependency) # # `accel_diag v0.1.0` -# └── "once_cell" ← was needed before LazyLock, now dead +# └── "once_cell" ← нужна была до LazyLock, теперь мёртвая ``` -Every unused dependency: -- Increases compile time -- Increases binary size -- Adds supply chain risk -- Adds potential license complications +Каждая неиспользуемая зависимость: +- Увеличивает время компиляции +- Увеличивает размер бинарника +- Добавляет риски цепочки поставок +- Добавляет возможные проблемы с лицензиями -**Alternative: `cargo-machete`** — faster, heuristic-based approach: +**Альтернатива: `cargo-machete`** — быстрый эвристический подход: ```bash cargo install cargo-machete cargo machete -# Faster but may have false positives (heuristic, not compilation-based) +# Быстрее, но возможны ложные срабатывания (эвристика, а не анализ на основе компиляции) ``` -**Alternative: `cargo-shear`** — sweet spot between `cargo-udeps` and `cargo-machete`: +**Альтернатива: `cargo-shear`** — золотая середина между `cargo-udeps` и `cargo-machete`: ```bash cargo install cargo-shear cargo shear --fix -# Slower than cargo-machete but much faster than cargo-udeps -# Much less false positives than cargo-machete +# Медленнее, чем cargo-machete, но намного быстрее cargo-udeps +# Значительно меньше ложных срабатываний, чем у cargo-machete ``` -### Size Optimization Decision Tree +### Дерево решений по оптимизации размера ```mermaid flowchart TD - START["Binary too large?"] --> STRIP{"strip = true?"} - STRIP -->|"No"| DO_STRIP["Add strip = true
-50 to -70% size"] - STRIP -->|"Yes"| LTO{"LTO enabled?"} - LTO -->|"No"| DO_LTO["Add lto = true
codegen-units = 1"] - LTO -->|"Yes"| BLOAT["Run cargo-bloat
--crates"] - BLOAT --> BIG_DEP{"Large dependency?"} - BIG_DEP -->|"Yes"| REPLACE["Replace with lighter
alternative or disable
default features"] - BIG_DEP -->|"No"| UDEPS["cargo-udeps
Remove unused deps"] - UDEPS --> OPT_LEVEL{"Need smaller?"} - OPT_LEVEL -->|"Yes"| SIZE_OPT["opt-level = 's' or 'z'"] + START["Бинарник слишком большой?"] --> STRIP{"strip = true?"} + STRIP -->|"Нет"| DO_STRIP["Добавьте strip = true
-50…-70% размера"] + STRIP -->|"Да"| LTO{"LTO включён?"} + LTO -->|"Нет"| DO_LTO["Добавьте lto = true
codegen-units = 1"] + LTO -->|"Да"| BLOAT["Запустите cargo-bloat
--crates"] + BLOAT --> BIG_DEP{"Крупная зависимость?"} + BIG_DEP -->|"Да"| REPLACE["Замените на более лёгкую
альтернативу или отключите
фичи по умолчанию"] + BIG_DEP -->|"Нет"| UDEPS["cargo-udeps
Удалите неиспользуемые зависимости"] + UDEPS --> OPT_LEVEL{"Нужно меньше?"} + OPT_LEVEL -->|"Да"| SIZE_OPT["opt-level = 's' или 'z'"] style DO_STRIP fill:#91e5a3,color:#000 style DO_LTO fill:#e3f2fd,color:#000 @@ -268,22 +268,23 @@ flowchart TD style SIZE_OPT fill:#ff6b6b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Measure LTO Impact +#### 🟢 Упражнение 1: измерьте эффект LTO -Build a project with default release settings, then with `lto = true` + `codegen-units = 1` + `strip = true`. Compare binary size and compile time. +Соберите проект с настройками release по умолчанию, затем с `lto = true` + +`codegen-units = 1` + `strip = true`. Сравните размер бинарника и время компиляции.
-Solution +Решение ```bash -# Default release +# Release по умолчанию cargo build --release ls -lh target/release/my-binary -time cargo build --release # Note time +time cargo build --release # Запомните время -# Optimized release — add to Cargo.toml: +# Оптимизированный release — добавьте в Cargo.toml: # [profile.release] # lto = true # codegen-units = 1 @@ -292,42 +293,43 @@ time cargo build --release # Note time cargo clean cargo build --release -ls -lh target/release/my-binary # Typically 30-50% smaller -time cargo build --release # Typically 2-3× slower to compile +ls -lh target/release/my-binary # Обычно на 30–50% меньше +time cargo build --release # Обычно компиляция в 2–3× медленнее ```
-#### 🟡 Exercise 2: Find Your Biggest Crate +#### 🟡 Упражнение 2: найдите самый большой крейт -Run `cargo bloat --release --crates` on a project. Identify the largest dependency. Can you reduce it by disabling default features or switching to a lighter alternative? +Запустите `cargo bloat --release --crates` на проекте. Определите самую большую зависимость. +Можно ли её уменьшить, отключив фичи по умолчанию или заменив на более лёгкую альтернативу?
-Solution +Решение ```bash cargo install cargo-bloat cargo bloat --release --crates -# Output: +# Вывод: # File .text Size Crate # 12.3% 22.1% 340KiB serde_json # 8.7% 15.6% 240KiB regex -# For regex — try regex-lite if you don't need Unicode: -# regex-lite = "0.1" # ~10× smaller than full regex +# Для regex — попробуйте regex-lite, если Unicode не нужен: +# regex-lite = "0.1" # ~10× меньше полного regex -# For serde — disable default features if you don't need std: +# Для serde — отключите фичи по умолчанию, если std не нужен: # serde = { version = "1", default-features = false, features = ["derive"] } -cargo bloat --release --crates # Compare after changes +cargo bloat --release --crates # Сравните результаты после изменений ```
-### Key Takeaways +### Ключевые выводы -- `lto = true` + `codegen-units = 1` + `strip = true` + `panic = "abort"` is the production release profile -- Thin LTO (`lto = "thin"`) gives 80% of Fat LTO's benefit at a fraction of the compile cost -- `cargo-bloat --crates` tells you exactly which dependencies are eating binary space -- `cargo-udeps`, `cargo-machete` and `cargo-shear` find dead dependencies that waste compile time and binary size -- Per-crate profile overrides let you optimize hot crates without slowing the whole build +- `lto = true` + `codegen-units = 1` + `strip = true` + `panic = "abort"` — это профиль релиза для продакшна +- Thin LTO (`lto = "thin"`) даёт около 80% выигрыша fat LTO при малой части затрат на компиляцию +- `cargo-bloat --crates` точно показывает, какие зависимости занимают место в бинарнике +- `cargo-udeps`, `cargo-machete` и `cargo-shear` находят мёртвые зависимости, которые тратят время компиляции и размер бинарника +- Переопределения профиля для отдельных крейтов позволяют оптимизировать горячие крейты, не замедляя всю сборку --- diff --git a/engineering-book/src/ch08-compile-time-and-developer-tools.md b/engineering-book/src/ch08-compile-time-and-developer-tools.md index 9455926..a4d790c 100644 --- a/engineering-book/src/ch08-compile-time-and-developer-tools.md +++ b/engineering-book/src/ch08-compile-time-and-developer-tools.md @@ -1,97 +1,96 @@ -# Compile-Time and Developer Tools 🟡 - -> **What you'll learn:** -> - Compilation caching with `sccache` for local and CI builds -> - Faster linking with `mold` (3-10× faster than the default linker) -> - `cargo-nextest`: a faster, more informative test runner -> - Developer visibility tools: `cargo-expand`, `cargo-geiger`, `cargo-watch` -> - Workspace lints, MSRV policy, and documentation-as-CI +# Инструменты времени компиляции и разработки 🟡 + +> **Чему вы научитесь:** +> - Кеширование компиляции с помощью `sccache` для локальных сборок и CI +> - Более быстрая линковка с `mold` (в 3–10× быстрее линкера по умолчанию) +> - `cargo-nextest`: более быстрый и информативный запускатель тестов +> - Инструменты наблюдения за разработкой: `cargo-expand`, `cargo-geiger`, `cargo-watch` +> - Линты воркспейса, политика MSRV и документация как часть CI > -> **Cross-references:** [Release Profiles](ch07-release-profiles-and-binary-size.md) — LTO and binary size optimization · [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) — these tools integrate into your pipeline · [Dependencies](ch06-dependency-management-and-supply-chain-s.md) — fewer deps = faster compiles +> **Перекрёстные ссылки:** [Профили релиза](ch07-release-profiles-and-binary-size.md) — LTO и оптимизация размера бинарника · [CI/CD-конвейер](ch11-putting-it-all-together-a-production-cic.md) — эти инструменты встраиваются в ваш конвейер · [Зависимости](ch06-dependency-management-and-supply-chain-s.md) — меньше зависимостей = быстрее компиляция -### Compile-Time Optimization: sccache, mold, cargo-nextest +### Оптимизация времени компиляции: sccache, mold, cargo-nextest -Long compile times are the #1 developer pain point in Rust. These tools -collectively can cut iteration time by 50-80%: +Долгая компиляция — главная боль разработчиков на Rust. Эти инструменты вместе могут +сократить время итерации на 50–80%: -**`sccache` — Shared compilation cache:** +**`sccache` — общий кеш компиляции:** ```bash -# Install +# Установка cargo install sccache -# Configure as the Rust wrapper +# Настраиваем как обёртку rustc export RUSTC_WRAPPER=sccache -# Or set permanently in .cargo/config.toml: +# Или задаём постоянно в .cargo/config.toml: # [build] # rustc-wrapper = "sccache" -# First build: normal speed (populates cache) -cargo build --release # 3 minutes +# Первая сборка: обычная скорость (заполняет кеш) +cargo build --release # 3 минуты -# Clean + rebuild: cache hits for unchanged crates -cargo clean && cargo build --release # 45 seconds +# Очистка и пересборка: попадания в кеш для неизменившихся крейтов +cargo clean && cargo build --release # 45 секунд -# Check cache statistics +# Проверка статистики кеша sccache --show-stats # Compile requests 1,234 # Cache hits 987 (80%) # Cache misses 247 ``` -`sccache` supports shared caches (S3, GCS, Azure Blob) for team-wide and CI -cache sharing. +`sccache` поддерживает общие кеши (S3, GCS, Azure Blob) для обмена кешем в команде и в CI. -**`mold` — A faster linker:** +**`mold` — более быстрый линкер:** -Linking is often the slowest phase. `mold` is 3-5× faster than `lld` and -10-20× faster than the default GNU `ld`: +Линковка часто оказывается самой медленной фазой. `mold` в 3–5× быстрее `lld` и в 10–20× +быстрее стандартного GNU `ld`: ```bash -# Install +# Установка sudo apt install mold # Ubuntu 22.04+ -# Note: mold is for ELF targets (Linux). macOS uses Mach-O, not ELF. -# The macOS linker (ld64) is already quite fast; if you need faster: -# brew install sold # sold = mold for Mach-O (experimental, less mature) -# In practice, macOS link times are rarely a bottleneck. +# Примечание: mold предназначен для целей ELF (Linux). macOS использует Mach-O, а не ELF. +# Линкер macOS (ld64) и так довольно быстрый; если нужен более быстрый: +# brew install sold # sold = mold для Mach-O (экспериментально, менее зрелый) +# На практике время линковки на macOS редко становится узким местом. ``` ```toml -# Use mold for linking +# Используем mold для линковки # .cargo/config.toml [target.x86_64-unknown-linux-gnu] rustflags = ["-C", "link-arg=-fuse-ld=mold"] ``` ```bash -# See https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables +# См. https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables export MOLD_JOBS=1 -# Verify mold is being used +# Проверяем, что используется mold cargo build -v 2>&1 | grep mold ``` -**`cargo-nextest` — A faster test runner:** +**`cargo-nextest` — более быстрый запускатель тестов:** ```bash -# Install +# Установка cargo install cargo-nextest -# Run tests (parallel by default, per-test timeout, retry) +# Запуск тестов (по умолчанию параллельно, с таймаутом на тест и повторами) cargo nextest run -# Key advantages over cargo test: -# - Each test runs in its own process → better isolation -# - Parallel execution with smart scheduling -# - Per-test timeouts (no more hanging CI) -# - JUnit XML output for CI -# - Retry failed tests +# Ключевые преимущества перед cargo test: +# - Каждый тест выполняется в своём процессе → лучше изоляция +# - Параллельное выполнение с умным планированием +# - Таймауты на тест (больше никаких зависших CI) +# - Вывод в JUnit XML для CI +# - Повтор упавших тестов -# Configuration +# Конфигурация cargo nextest run --retries 2 --fail-fast -# Archive test binaries (useful for CI: build once, test on multiple machines) +# Архивируем тестовые бинарники (полезно для CI: собрать один раз, тестировать на нескольких машинах) cargo nextest archive --archive-file tests.tar.zst cargo nextest run --archive-file tests.tar.zst ``` @@ -109,48 +108,48 @@ fail-fast = false junit = { path = "test-results.xml" } ``` -**Combined dev configuration:** +**Общая конфигурация для разработки:** ```toml -# .cargo/config.toml — optimize the development inner loop +# .cargo/config.toml — оптимизируем внутренний цикл разработки [build] -rustc-wrapper = "sccache" # Cache compilation artifacts +rustc-wrapper = "sccache" # Кешировать артефакты компиляции [target.x86_64-unknown-linux-gnu] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] # Faster linking +rustflags = ["-C", "link-arg=-fuse-ld=mold"] # Более быстрая линковка -# Dev profile: optimize deps but not your code -# (put in Cargo.toml) +# Dev-профиль: оптимизируем зависимости, но не свой код +# (поместить в Cargo.toml) # [profile.dev.package."*"] # opt-level = 2 ``` -### cargo-expand and cargo-geiger — Visibility Tools +### cargo-expand и cargo-geiger — инструменты наблюдения -**`cargo-expand`** — see what macros generate: +**`cargo-expand`** — посмотреть, что генерируют макросы: ```bash cargo install cargo-expand -# Expand all macros in a specific module +# Раскрыть все макросы в конкретном модуле cargo expand --lib accel_diag::vendor -# Expand a specific derive -# Given: #[derive(Debug, Serialize, Deserialize)] -# cargo expand shows the generated impl blocks +# Раскрыть конкретный derive +# Дано: #[derive(Debug, Serialize, Deserialize)] +# cargo expand покажет сгенерированные блоки impl cargo expand --lib --tests ``` -Invaluable for debugging `#[derive]` macro output, `macro_rules!` expansions, -and understanding what `serde` generates for your types. +Незаменим для отладки вывода макросов `#[derive]`, раскрытий `macro_rules!` и для понимания +того, что генерирует `serde` для ваших типов. -In addition to `cargo-expand`, you can also use rust-analyzer to expand macros: +Помимо `cargo-expand`, раскрыть макросы можно и с помощью rust-analyzer: -1. Move cursor to the macro you want to check. -2. Open command palette (e.g. `F1` on VSCode). -3. Search for `rust-analyzer: Expand macro recursively at caret`. +1. Установите курсор на макрос, который хотите проверить. +2. Откройте палитру команд (например, `F1` в VSCode). +3. Найдите `rust-analyzer: Expand macro recursively at caret`. -**`cargo-geiger`** — count `unsafe` usage across your dependency tree: +**`cargo-geiger`** — подсчёт использования `unsafe` во всём дереве зависимостей: ```bash cargo install cargo-geiger @@ -167,265 +166,263 @@ cargo geiger # 3/3 14/14 0/0 0/0 2/2 ❗ libc # 15/15 142/142 4/4 0/0 12/12 ☢️ ring -# The symbols: -# ✅ = no unsafe used -# ❗ = some unsafe used -# ☢️ = heavily unsafe +# Обозначения: +# ✅ = unsafe не используется +# ❗ = используется некоторое количество unsafe +# ☢️ = сильно использует unsafe ``` -For the project's zero-unsafe policy, `cargo geiger` verifies that no -dependency introduces unsafe code into the call graph that your code actually -exercises. +Для политики проекта «без unsafe» `cargo geiger` проверяет, что ни одна зависимость не +вносит в граф вызовов, который реально выполняет ваш код, unsafe-код. -### Workspace Lints — `[workspace.lints]` +### Линты воркспейса — `[workspace.lints]` -Since Rust 1.74, you can configure Clippy and compiler lints centrally in -`Cargo.toml` — no more `#![deny(...)]` at the top of every crate: +Начиная с Rust 1.74, Clippy и линты компилятора можно настраивать централизованно в +`Cargo.toml` — больше не нужны `#![deny(...)]` в начале каждого крейта: ```toml -# Root Cargo.toml — lint configuration for all crates +# Корневой Cargo.toml — конфигурация линтов для всех крейтов [workspace.lints.clippy] -unwrap_used = "warn" # Prefer ? or expect("reason") -dbg_macro = "deny" # No dbg!() in committed code -todo = "warn" # Track incomplete implementations -large_enum_variant = "warn" # Catch accidental size bloat +unwrap_used = "warn" # Предпочитайте ? или expect("причина") +dbg_macro = "deny" # Не допускаем dbg!() в закоммиченном коде +todo = "warn" # Отслеживаем незавершённые реализации +large_enum_variant = "warn" # Ловим случайное раздувание размера [workspace.lints.rust] -unsafe_code = "deny" # Enforce zero-unsafe policy -missing_docs = "warn" # Encourage documentation +unsafe_code = "deny" # Обеспечиваем политику «без unsafe» +missing_docs = "warn" # Поощряем документирование ``` ```toml -# Each crate's Cargo.toml — opt into workspace lints +# Cargo.toml каждого крейта — подключаем линты воркспейса [lints] workspace = true ``` -This replaces scattered `#![deny(clippy::unwrap_used)]` attributes and ensures -consistent policy across the entire workspace. +Это заменяет разрозненные атрибуты `#![deny(clippy::unwrap_used)]` и обеспечивает единую +политику во всём воркспейсе. -**Auto-fixing Clippy warnings:** +**Автоисправление предупреждений Clippy:** ```bash -# Let Clippy automatically fix machine-applicable suggestions +# Пусть Clippy автоматически применит предложения, которые можно применить механически cargo clippy --fix --workspace --all-targets --allow-dirty -# Fix and also apply suggestions that may change behavior (review carefully!) +# Исправляем и применяем предложения, которые могут изменить поведение (проверяйте внимательно!) cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic ``` -> **Tip**: Run `cargo clippy --fix` before committing. It handles trivial -> issues (unused imports, redundant clones, type simplifications) that are -> tedious to fix by hand. +> **Совет**: запускайте `cargo clippy --fix` перед коммитом. Он обрабатывает тривиальные +> проблемы (неиспользуемые импорты, избыточные клоны, упрощения типов), которые вручную +> исправлять скучно. -### MSRV Policy and rust-version +### Политика MSRV и rust-version -Minimum Supported Rust Version (MSRV) ensures your crate compiles on older -toolchains. This matters when deploying to systems with frozen Rust versions. +Minimum Supported Rust Version (MSRV) гарантирует, что ваш крейт компилируется на старых +тулчейнах. Это важно при развёртывании на системах с замороженной версией Rust. ```toml # Cargo.toml [package] name = "diag_tool" version = "0.1.0" -rust-version = "1.75" # Minimum Rust version required +rust-version = "1.75" # Минимальная требуемая версия Rust ``` ```bash -# Verify MSRV compliance +# Проверка соответствия MSRV cargo +1.75.0 check --workspace -# Automated MSRV discovery +# Автоматический поиск MSRV cargo install cargo-msrv cargo msrv find # Output: Minimum Supported Rust Version is 1.75.0 -# Verify in CI +# Проверка в CI cargo msrv verify ``` -**MSRV in CI:** +**MSRV в CI:** ```yaml jobs: msrv: - name: Check MSRV + name: Проверка MSRV runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: "1.75.0" # Match rust-version in Cargo.toml + toolchain: "1.75.0" # Совпадает с rust-version в Cargo.toml - run: cargo check --workspace ``` -**MSRV strategy:** -- **Binary applications** (like a large project): Use latest stable. No MSRV needed. -- **Library crates** (published to crates.io): Set MSRV to oldest Rust version - that supports all features you use. Commonly `N-2` (two versions behind current). -- **Enterprise deployments**: Set MSRV to match the oldest Rust version installed - on your fleet. +**Стратегия MSRV:** +- **Бинарные приложения** (как большой проект): используйте последний stable. MSRV не нужен. +- **Библиотечные крейты** (публикуемые на crates.io): задайте MSRV как самую старую версию + Rust, которая поддерживает все используемые фичи. Обычно это `N-2` (на две версии позади текущей). +- **Корпоративные развёртывания**: задайте MSRV по самой старой версии Rust, установленной на вашем парке. -### Application: Production Binary Profile +### Применение: профиль продакшн-бинарника -The project already has an excellent [release profile](ch07-release-profiles-and-binary-size.md): +В проекте уже есть отличный [профиль релиза](ch07-release-profiles-and-binary-size.md): ```toml -# Current workspace Cargo.toml +# Текущий Cargo.toml воркспейса [profile.release] -lto = true # ✅ Full cross-crate optimization -codegen-units = 1 # ✅ Maximum optimization -panic = "abort" # ✅ No unwinding overhead -strip = true # ✅ Remove symbols for deployment +lto = true # ✅ Полная оптимизация между крейтами +codegen-units = 1 # ✅ Максимальная оптимизация +panic = "abort" # ✅ Нет накладных расходов на раскрутку +strip = true # ✅ Удаляем символы для развёртывания [profile.dev] -opt-level = 0 # ✅ Fast compilation -debug = true # ✅ Full debug info +opt-level = 0 # ✅ Быстрая компиляция +debug = true # ✅ Полная отладочная информация ``` -**Recommended additions:** +**Рекомендуемые дополнения:** ```toml -# Optimize dependencies in dev mode (faster test execution) +# Оптимизируем зависимости в dev-режиме (быстрее выполнение тестов) [profile.dev.package."*"] opt-level = 2 -# Test profile: some optimization to prevent timeout in slow tests +# Профиль тестов: немного оптимизации, чтобы медленные тесты не упирались в таймаут [profile.test] opt-level = 1 -# Keep overflow checks in release (safety) +# Оставляем проверки переполнения в release (безопасность) [profile.release] lto = true codegen-units = 1 panic = "abort" strip = true -overflow-checks = true # ← add this: catch integer overflows -debug = "line-tables-only" # ← add this: backtraces without full DWARF +overflow-checks = true # ← добавьте: ловим переполнения целых чисел +debug = "line-tables-only" # ← добавьте: бэктрейсы без полного DWARF ``` -**Recommended developer tooling:** +**Рекомендуемые инструменты для разработчика:** ```toml -# .cargo/config.toml (proposed) +# .cargo/config.toml (предлагаемый) [build] -rustc-wrapper = "sccache" # 80%+ cache hit after first build +rustc-wrapper = "sccache" # 80%+ попаданий в кеш после первой сборки [target.x86_64-unknown-linux-gnu] -rustflags = ["-C", "link-arg=-fuse-ld=mold"] # 3-5× faster linking +rustflags = ["-C", "link-arg=-fuse-ld=mold"] # В 3–5× быстрее линковка ``` -**Expected impact on the project:** +**Ожидаемый эффект на проект:** -| Metric | Current | With Additions | -|--------|---------|----------------| -| Release binary | ~10 MB (stripped, LTO) | Same | -| Dev build time | ~45s | ~25s (sccache + mold) | -| Rebuild (1 file change) | ~15s | ~5s (sccache + mold) | -| Test execution | `cargo test` | `cargo nextest` — 2× faster | -| Dep vulnerability scanning | None | `cargo audit` in CI | -| License compliance | Manual | `cargo deny` automated | -| Unused dependency detection | Manual | `cargo udeps` in CI | +| Метрика | Сейчас | С дополнениями | +|---------|--------|----------------| +| Release-бинарник | ~10 МБ (stripped, LTO) | То же | +| Время dev-сборки | ~45 с | ~25 с (sccache + mold) | +| Пересборка (изменение одного файла) | ~15 с | ~5 с (sccache + mold) | +| Запуск тестов | `cargo test` | `cargo nextest` — в 2× быстрее | +| Сканирование уязвимостей зависимостей | Нет | `cargo audit` в CI | +| Соответствие лицензиям | Вручную | `cargo deny` автоматически | +| Поиск неиспользуемых зависимостей | Вручную | `cargo udeps` в CI | -### `cargo-watch` — Auto-Rebuild on File Changes +### `cargo-watch` — автоматическая пересборка при изменении файлов -[`cargo-watch`](https://github.com/watchexec/cargo-watch) re-runs a command -every time a source file changes — essential for tight feedback loops: +[`cargo-watch`](https://github.com/watchexec/cargo-watch) перезапускает команду при каждом +изменении исходного файла — незаменим для быстрого цикла обратной связи: ```bash -# Install +# Установка cargo install cargo-watch -# Re-check on every save (instant feedback) +# Повторная проверка при каждом сохранении (мгновенная обратная связь) cargo watch -x check -# Run clippy + tests on change +# Запуск clippy и тестов при изменении cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib' -# Watch only specific crates (faster for large workspaces) +# Следим только за конкретными крейтами (быстрее для больших воркспейсов) cargo watch -w accel_diag/src -x 'test -p accel_diag' -# Clear screen between runs +# Очищать экран между запусками cargo watch -c -x check ``` -> **Tip**: Combine with `mold` + `sccache` from above for sub-second -> re-check times on incremental changes. +> **Совет**: сочетайте с `mold` и `sccache` из примеров выше, чтобы инкрементальная проверка +> занимала меньше секунды. -### `cargo doc` and Workspace Documentation +### `cargo doc` и документация воркспейса -For a large workspace, generated documentation is essential for -discoverability. `cargo doc` uses rustdoc to produce HTML docs from -doc-comments and type signatures: +Для большого воркспейса сгенерированная документация необходима, чтобы API было легко +найти. `cargo doc` использует rustdoc, чтобы создать HTML-документацию из doc-комментариев +и сигнатур типов: ```bash -# Generate docs for all workspace crates (opens in browser) +# Генерация документации для всех крейтов воркспейса (откроется в браузере) cargo doc --workspace --no-deps --open -# Include private items (useful during development) +# Включить приватные элементы (полезно при разработке) cargo doc --workspace --no-deps --document-private-items -# Check doc-links without generating HTML (fast CI check) +# Проверить ссылки в документации без генерации HTML (быстрая проверка в CI) cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error' ``` -**Intra-doc links** — link between types across crates without URLs: +**Внутридокументные ссылки** — ссылки между типами разных крейтов без URL: ```rust -/// Runs GPU diagnostics using [`GpuConfig`] settings. +/// Запускает диагностику GPU с настройками из [`GpuConfig`]. /// -/// See [`crate::accel_diag::run_diagnostics`] for the implementation. -/// Returns [`DiagResult`] which can be serialized to the -/// [`DerReport`](crate::core_lib::DerReport) format. +/// Реализацию см. в [`crate::accel_diag::run_diagnostics`]. +/// Возвращает [`DiagResult`], который можно сериализовать +/// в формат [`DerReport`](crate::core_lib::DerReport). pub fn run_accel_diag(config: &GpuConfig) -> DiagResult { // ... } ``` -**Show platform-specific APIs in docs:** +**Показываем платформенно-специфичные API в документации:** ```rust // Cargo.toml: [package.metadata.docs.rs] // all-features = true // rustdoc-args = ["--cfg", "docsrs"] -/// Windows-only: read battery status via Win32 API. +/// Только для Windows: читает состояние батареи через Win32 API. /// -/// Only available on `cfg(windows)` builds. +/// Доступно только в сборках `cfg(windows)`. #[cfg(windows)] -#[doc(cfg(windows))] // Shows "Available on Windows only" badge in docs +#[doc(cfg(windows))] // Показывает бейдж «Доступно только в Windows» в документации pub fn get_battery_status() -> Option { // ... } ``` -**CI documentation check:** +**Проверка документации в CI:** ```yaml -# Add to CI workflow -- name: Check documentation +# Добавьте в workflow CI +- name: Проверка документации run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps - # Treats broken intra-doc links as errors + # Считает битые внутридокументные ссылки ошибками ``` -> **For the project**: With many crates, `cargo doc --workspace` is the best -> way for new team members to discover the API surface. Add -> `RUSTDOCFLAGS="-D warnings"` to CI to catch broken doc-links before merge. +> **Для проекта**: при большом количестве крейтов `cargo doc --workspace` — лучший способ +> для новых участников команды увидеть поверхность API. Добавьте `RUSTDOCFLAGS="-D warnings"` +> в CI, чтобы ловить битые ссылки до слияния. -### Compile-Time Decision Tree +### Дерево решений по времени компиляции ```mermaid flowchart TD - START["Compile too slow?"] --> WHERE{"Where's the time?"} + START["Компиляция слишком медленная?"] --> WHERE{"Где тратится время?"} - WHERE -->|"Recompiling
unchanged crates"| SCCACHE["sccache
Shared compilation cache"] - WHERE -->|"Linking phase"| MOLD["mold linker
3-10× faster linking"] - WHERE -->|"Running tests"| NEXTEST["cargo-nextest
Parallel test runner"] - WHERE -->|"Everything"| COMBO["All of the above +
cargo-udeps to trim deps"] + WHERE -->|"Перекомпиляция
неизменённых крейтов"| SCCACHE["sccache
Общий кеш компиляции"] + WHERE -->|"Фаза линковки"| MOLD["Линкер mold
линковка в 3–10× быстрее"] + WHERE -->|"Запуск тестов"| NEXTEST["cargo-nextest
Параллельный запускатель тестов"] + WHERE -->|"Всё сразу"| COMBO["Всё перечисленное +
cargo-udeps для сокращения зависимостей"] - SCCACHE --> CI_CACHE{"CI or local?"} - CI_CACHE -->|"CI"| S3["S3/GCS shared cache"] - CI_CACHE -->|"Local"| LOCAL["Local disk cache
auto-configured"] + SCCACHE --> CI_CACHE{"CI или локально?"} + CI_CACHE -->|"CI"| S3["Общий кеш S3/GCS"] + CI_CACHE -->|"Локально"| LOCAL["Локальный дисковый кеш
настраивается автоматически"] style SCCACHE fill:#91e5a3,color:#000 style MOLD fill:#e3f2fd,color:#000 @@ -433,21 +430,22 @@ flowchart TD style COMBO fill:#b39ddb,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Set Up sccache + mold +#### 🟢 Упражнение 1: настройте sccache и mold -Install `sccache` and `mold`, configure them in `.cargo/config.toml`, then measure the compile time improvement on a clean rebuild. +Установите `sccache` и `mold`, настройте их в `.cargo/config.toml`, затем измерьте выигрыш +по времени компиляции при чистой пересборке.
-Solution +Решение ```bash -# Install +# Установка cargo install sccache sudo apt install mold # Ubuntu 22.04+ -# Configure .cargo/config.toml: +# Настраиваем .cargo/config.toml: cat > .cargo/config.toml << 'EOF' [build] rustc-wrapper = "sccache" @@ -457,49 +455,50 @@ linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] EOF -# First build (populates cache) -time cargo build --release # e.g., 180s +# Первая сборка (заполняет кеш) +time cargo build --release # например, 180 с -# Clean + rebuild (cache hits) +# Очистка и пересборка (попадания в кеш) cargo clean -time cargo build --release # e.g., 45s +time cargo build --release # например, 45 с sccache --show-stats -# Cache hits should be 60-80%+ +# Попаданий в кеш должно быть 60–80%+ ```
-#### 🟡 Exercise 2: Switch to cargo-nextest +#### 🟡 Упражнение 2: переходим на cargo-nextest -Install `cargo-nextest` and run your test suite. Compare wall-clock time with `cargo test`. What's the speedup? +Установите `cargo-nextest` и запустите набор тестов. Сравните реальное время с `cargo test`. +Каков прирост скорости?
-Solution +Решение ```bash cargo install cargo-nextest -# Standard test runner +# Стандартный запускатель тестов time cargo test --workspace 2>&1 | tail -5 -# nextest (parallel per-test-binary execution) +# nextest (параллельное выполнение по тестовым бинарникам) time cargo nextest run --workspace 2>&1 | tail -5 -# Typical speedup: 2-5× for large workspaces -# nextest also provides: -# - Per-test timing -# - Retries for flaky tests -# - JUnit XML output for CI +# Типичный прирост: в 2–5× для больших воркспейсов +# nextest также предоставляет: +# - время выполнения каждого теста +# - повторы для нестабильных тестов +# - вывод JUnit XML для CI cargo nextest run --workspace --retries 2 ```
-### Key Takeaways +### Ключевые выводы -- `sccache` with S3/GCS backend shares compilation cache across team and CI -- `mold` is the fastest ELF linker — link times drop from seconds to milliseconds -- `cargo-nextest` runs tests in parallel per-binary with better output and retry support -- `cargo-geiger` counts `unsafe` usage — run it before accepting new dependencies -- `[workspace.lints]` centralizes Clippy and rustc lint configuration across a multi-crate workspace +- `sccache` с бэкендом S3/GCS разделяет кеш компиляции между командой и CI +- `mold` — самый быстрый линкер для ELF: время линковки падает с секунд до миллисекунд +- `cargo-nextest` запускает тесты параллельно по бинарникам, с лучшим выводом и поддержкой повторов +- `cargo-geiger` считает использование `unsafe` — запускайте его перед тем, как принимать новые зависимости +- `[workspace.lints]` централизует настройку линтов Clippy и rustc во всём многокрейтовом воркспейсе --- diff --git a/engineering-book/src/ch09-no-std-and-feature-verification.md b/engineering-book/src/ch09-no-std-and-feature-verification.md index a8648bd..ff1d621 100644 --- a/engineering-book/src/ch09-no-std-and-feature-verification.md +++ b/engineering-book/src/ch09-no-std-and-feature-verification.md @@ -1,151 +1,150 @@ -# `no_std` and Feature Verification 🔴 +# `no_std` и проверка фич 🔴 -> **What you'll learn:** -> - Verifying feature combinations systematically with `cargo-hack` -> - The three layers of Rust: `core` vs `alloc` vs `std` and when to use each -> - Building `no_std` crates with custom panic handlers and allocators -> - Testing `no_std` code on host and with QEMU +> **Чему вы научитесь:** +> - Систематическая проверка комбинаций фич с помощью `cargo-hack` +> - Три уровня Rust: `core`, `alloc` и `std` — когда что использовать +> - Сборка крейтов `no_std` с собственными обработчиками паники и аллокаторами +> - Тестирование кода `no_std` на хосте и с помощью QEMU > -> **Cross-references:** [Windows & Conditional Compilation](ch10-windows-and-conditional-compilation.md) — the platform half of this topic · [Cross-Compilation](ch02-cross-compilation-one-source-many-target.md) — cross-compiling to ARM and embedded targets · [Miri and Sanitizers](ch05-miri-valgrind-and-sanitizers-verifying-u.md) — verifying `unsafe` code in `no_std` environments · [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) — `cfg` flags emitted by `build.rs` +> **Перекрёстные ссылки:** [Windows и условная компиляция](ch10-windows-and-conditional-compilation.md) — платформенная половина этой темы · [Кросс-компиляция](ch02-cross-compilation-one-source-many-target.md) — кросс-компиляция под ARM и встраиваемые цели · [Miri и санитайзеры](ch05-miri-valgrind-and-sanitizers-verifying-u.md) — проверка `unsafe`-кода в окружениях `no_std` · [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) — `cfg`-флаги, которые выводит `build.rs` -Rust runs everywhere from 8-bit microcontrollers to cloud servers. This chapter -covers the foundation: stripping the standard library with `#![no_std]` and -verifying that your feature combinations actually compile. +Rust работает везде — от 8-битных микроконтроллеров до облачных серверов. Эта глава описывает +фундамент: как отказаться от стандартной библиотеки через `#![no_std]` и проверить, что +ваши комбинации фич действительно компилируются. -### Verifying Feature Combinations with `cargo-hack` +### Проверка комбинаций фич с помощью `cargo-hack` -[`cargo-hack`](https://github.com/taiki-e/cargo-hack) tests all feature -combinations systematically — essential for crates with `#[cfg(...)]` code: +[`cargo-hack`](https://github.com/taiki-e/cargo-hack) систематически проверяет все комбинации +фич — это необходимо для крейтов с кодом под `#[cfg(...)]`: ```bash -# Install +# Установка cargo install cargo-hack -# Check that every feature compiles individually +# Проверяем, что каждая фича компилируется по отдельности cargo hack check --each-feature --workspace -# The nuclear option: test ALL feature combinations (exponential!) -# Only practical for crates with <8 features. +# Ядерный вариант: проверяем ВСЕ комбинации фич (экспоненциально!) +# Годится только для крейтов с менее чем 8 фичами. cargo hack check --feature-powerset --workspace -# Practical compromise: test each feature alone + all features + no features +# Практичный компромисс: каждая фича отдельно + все фичи + ни одной фичи cargo hack check --each-feature --workspace --no-dev-deps cargo check --workspace --all-features cargo check --workspace --no-default-features ``` -**Why this matters for the project:** +**Почему это важно для проекта:** -If you add platform features (`linux`, `windows`, `direct-ipmi`, `direct-accel-api`), -`cargo-hack` catches combinations that break: +Если вы добавляете платформенные фичи (`linux`, `windows`, `direct-ipmi`, `direct-accel-api`), +`cargo-hack` поймает комбинации, которые ломаются: ```toml -# Example: features that gate platform code +# Пример: фичи, которые закрывают платформенный код [features] default = ["linux"] -linux = [] # Linux-specific hardware access -windows = ["dep:windows-sys"] # Windows-specific APIs -direct-ipmi = [] # unsafe IPMI ioctl (ch05) -direct-accel-api = [] # unsafe accel-mgmt FFI (ch05) +linux = [] # Доступ к железу, специфичный для Linux +windows = ["dep:windows-sys"] # API, специфичные для Windows +direct-ipmi = [] # небезопасный ioctl IPMI (гл. 5) +direct-accel-api = [] # небезопасный FFI accel-mgmt (гл. 5) ``` ```bash -# Verify all features compile in isolation AND together +# Проверяем, что все фичи компилируются по отдельности И вместе cargo hack check --each-feature -p diag_tool -# Catches: "feature 'windows' doesn't compile without 'direct-ipmi'" -# Catches: "#[cfg(feature = \"linux\")] has a typo — it's 'lnux'" +# Ловит: «feature 'windows' не компилируется без 'direct-ipmi'» +# Ловит: опечатку в #[cfg(feature = \"linux\")] — там написано 'lnux' ``` -**CI integration:** +**Интеграция с CI:** ```yaml -# Add to CI pipeline (fast — just compilation checks) -- name: Feature matrix check +# Добавьте в конвейер CI (быстро — только проверки компиляции) +- name: Проверка матрицы фич run: cargo hack check --each-feature --workspace --no-dev-deps ``` -> **Rule of thumb**: Run `cargo hack check --each-feature` in CI for any crate -> with 2+ features. Run `--feature-powerset` only for core library crates with -> <8 features — it's exponential ($2^n$ combinations). +> **Эмпирическое правило**: запускайте `cargo hack check --each-feature` в CI для любого +> крейта с двумя и более фичами. `--feature-powerset` — только для базовых библиотечных крейтов +> с менее чем 8 фичами: это экспоненциально ($2^n$ комбинаций). -### `no_std` — When and Why +### `no_std` — когда и зачем -`#![no_std]` tells the compiler: "don't link the standard library." Your -crate can only use `core` (and optionally `alloc`). Why would you want this? +`#![no_std]` говорит компилятору: «не линкуй стандартную библиотеку». Крейт может использовать +только `core` (и, при необходимости, `alloc`). Зачем это нужно? -| Scenario | Why `no_std` | -|----------|-------------| -| Embedded firmware (ARM Cortex-M, RISC-V) | No OS, no heap, no file system | -| UEFI diagnostics tool | Pre-boot environment, no OS APIs | -| Kernel modules | Kernel space can't use userspace `std` | -| WebAssembly (WASM) | Minimize binary size, no OS dependencies | -| Bootloaders | Run before any OS exists | -| Shared library with C interface | Avoid Rust runtime in callers | +| Сценарий | Зачем `no_std` | +|----------|----------------| +| Встраиваемая прошивка (ARM Cortex-M, RISC-V) | Нет ОС, нет кучи, нет файловой системы | +| UEFI-диагностический инструмент | Среда до загрузки ОС, нет API ОС | +| Модули ядра | Пространство ядра не может использовать пользовательский `std` | +| WebAssembly (WASM) | Минимизация размера бинарника, нет зависимостей от ОС | +| Загрузчики | Работают до появления какой-либо ОС | +| Разделяемая библиотека с C-интерфейсом | Не тянуть рантайм Rust в вызывающий код | -**For hardware diagnostics**, `no_std` becomes relevant when building: -- UEFI-based pre-boot diagnostic tools (before the OS loads) -- BMC firmware diagnostics (resource-constrained ARM SoCs) -- Kernel-level PCIe diagnostics (kernel module or eBPF probe) +**Для диагностики железа** `no_std` становится актуален при создании: +- UEFI-диагностических инструментов, которые работают до загрузки ОС +- Диагностики прошивки BMC (ограниченные по ресурсам ARM SoC) +- Диагностики PCIe на уровне ядра (модуль ядра или eBPF-зонд) -### `core` vs `alloc` vs `std` — The Three Layers +### `core`, `alloc` и `std` — три уровня ```text ┌─────────────────────────────────────────────────────────────┐ │ std │ -│ Everything in core + alloc, PLUS: │ -│ • File I/O (std::fs, std::io) │ -│ • Networking (std::net) │ -│ • Threads (std::thread) │ -│ • Time (std::time) │ -│ • Environment (std::env) │ -│ • Process (std::process) │ -│ • OS-specific (std::os::unix, std::os::windows) │ +│ Всё из core и alloc, ПЛЮС: │ +│ • Файловый ввод-вывод (std::fs, std::io) │ +│ • Сеть (std::net) │ +│ • Потоки (std::thread) │ +│ • Время (std::time) │ +│ • Окружение (std::env) │ +│ • Процессы (std::process) │ +│ • Специфичное для ОС (std::os::unix, std::os::windows) │ ├─────────────────────────────────────────────────────────────┤ -│ alloc (available with #![no_std] + extern crate │ -│ alloc, if you have a global allocator) │ +│ alloc (доступен с #![no_std] + extern crate │ +│ alloc, если есть глобальный аллокатор) │ │ • String, Vec, Box, Rc, Arc │ │ • BTreeMap, BTreeSet │ -│ • format!() macro │ -│ • Collections and smart pointers that need heap │ +│ • Макрос format!() │ +│ • Коллекции и умные указатели, которым нужна куча │ ├─────────────────────────────────────────────────────────────┤ -│ core (always available, even in #![no_std]) │ -│ • Primitive types (u8, bool, char, etc.) │ +│ core (всегда доступен, даже в #![no_std]) │ +│ • Примитивные типы (u8, bool, char и др.) │ │ • Option, Result │ -│ • Iterator, slice, array, str (slices, not String) │ -│ • Traits: Clone, Copy, Debug, Display, From, Into │ -│ • Atomics (core::sync::atomic) │ -│ • Cell, RefCell (core::cell) — Pin (core::pin) │ -│ • core::fmt (formatting without allocation) │ -│ • core::mem, core::ptr (low-level memory operations) │ -│ • Math: core::num, basic arithmetic │ +│ • Iterator, slice, array, str (срезы, а не String) │ +│ • Трейты: Clone, Copy, Debug, Display, From, Into │ +│ • Атомарные типы (core::sync::atomic) │ +│ • Cell, RefCell (core::cell) — Pin (core::pin) │ +│ • core::fmt (форматирование без выделения памяти) │ +│ • core::mem, core::ptr (низкоуровневые операции с памятью) │ +│ • Математика: core::num, базовая арифметика │ └─────────────────────────────────────────────────────────────┘ ``` -**What you lose without `std`:** -- No `HashMap` (requires a hasher — use `BTreeMap` from `alloc`, or `hashbrown`) -- No `println!()` (requires stdout — use `core::fmt::Write` to a buffer) -- No `std::error::Error` (stabilized in `core` since Rust 1.81, but many - ecosystems haven't migrated) -- No file I/O, no networking, no threads (unless provided by a platform HAL) -- No `Mutex` (use `spin::Mutex` or platform-specific locks) +**Что вы теряете без `std`:** +- Нет `HashMap` (нужен хешер — используйте `BTreeMap` из `alloc` или `hashbrown`) +- Нет `println!()` (нужен stdout — используйте `core::fmt::Write` в буфер) +- Нет `std::error::Error` (стабилизирован в `core` начиная с Rust 1.81, но многие экосистемы ещё не перешли) +- Нет файлового ввода-вывода, сети и потоков (если их не предоставляет платформенный HAL) +- Нет `Mutex` (используйте `spin::Mutex` или платформенные блокировки) -### Building a `no_std` Crate +### Сборка крейта `no_std` ```rust -// src/lib.rs — a no_std library crate +// src/lib.rs — библиотечный крейт no_std #![no_std] -// Optionally use heap allocation +// При желании используем выделение памяти в куче extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use core::fmt; -/// Temperature reading from a thermal sensor. -/// This struct works in any environment — bare metal to Linux. +/// Показание температуры с термодатчика. +/// Работает в любой среде — от голого железа до Linux. #[derive(Clone, Copy, Debug)] pub struct Temperature { - /// Raw sensor value (0.0625°C per LSB for typical I2C sensors) + /// Сырое значение датчика (0.0625°C на LSB для типичных I2C-датчиков) raw: u16, } @@ -154,9 +153,9 @@ impl Temperature { Self { raw } } - /// Convert to degrees Celsius (fixed-point, no FPU required) + /// Преобразование в градусы Цельсия (fixed-point, FPU не нужен) pub const fn millidegrees_c(&self) -> i32 { - (self.raw as i32) * 625 / 10 // 0.0625°C resolution + (self.raw as i32) * 625 / 10 // разрешение 0.0625°C } pub fn degrees_c(&self) -> f32 { @@ -167,8 +166,8 @@ impl Temperature { impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let md = self.millidegrees_c(); - // Handle sign correctly for values between -0.999°C and -0.001°C - // where md / 1000 == 0 but the value is negative. + // Обрабатываем знак корректно для значений между -0.999°C и -0.001°C, + // где md / 1000 == 0, но значение отрицательное. if md < 0 && md > -1000 { write!(f, "-0.{:03}°C", (-md) % 1000) } else { @@ -177,8 +176,8 @@ impl fmt::Display for Temperature { } } -/// Parse space-separated temperature values. -/// Uses alloc — requires a global allocator. +/// Разбор значений температуры, разделённых пробелами. +/// Использует alloc — требуется глобальный аллокатор. pub fn parse_temperatures(input: &str) -> Vec { input .split_whitespace() @@ -187,8 +186,8 @@ pub fn parse_temperatures(input: &str) -> Vec { .collect() } -/// Format without allocation — writes directly to a buffer. -/// Works in `core`-only environments (no alloc, no heap). +/// Форматирование без выделения памяти — пишем прямо в буфер. +/// Работает в окружениях только с `core` (без alloc и кучи). pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize { use core::fmt::Write; struct SliceWriter<'a> { @@ -200,8 +199,8 @@ pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize { let bytes = s.as_bytes(); let remaining = self.buf.len() - self.pos; if bytes.len() > remaining { - // Buffer full — signal the error instead of silently truncating. - // Callers can check the returned pos for partial writes. + // Буфер полон — сообщаем об ошибке вместо тихого обрезания. + // Вызывающий код может проверить возвращённый pos на частичную запись. return Err(fmt::Error); } self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes); @@ -216,7 +215,7 @@ pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize { ``` ```toml -# Cargo.toml for a no_std crate +# Cargo.toml для крейта no_std [package] name = "thermal-sensor" version = "0.1.0" @@ -224,28 +223,28 @@ edition = "2021" [features] default = ["alloc"] -alloc = [] # Enable Vec, String, etc. -std = [] # Enable full std (implies alloc) +alloc = [] # Включает Vec, String и т. д. +std = [] # Включает полный std (подразумевает alloc) [dependencies] -# Use no_std-compatible crates +# Используем совместимые с no_std крейты serde = { version = "1.0", default-features = false, features = ["derive"] } -# ↑ default-features = false drops std dependency! +# ↑ default-features = false убирает зависимость от std! ``` -> **Key crate pattern**: Many popular crates (serde, log, rand, embedded-hal) -> support `no_std` via `default-features = false`. Always check whether a -> dependency requires `std` before using it in a `no_std` context. Note that -> some crates (e.g., `regex`) require at least `alloc` and don't work in -> `core`-only environments. +> **Ключевой паттерн крейтов**: многие популярные крейты (serde, log, rand, embedded-hal) +> поддерживают `no_std` через `default-features = false`. Всегда проверяйте, требует ли +> зависимость `std`, прежде чем использовать её в контексте `no_std`. Учтите, что некоторым +> крейтам (например, `regex`) нужен как минимум `alloc`, и в окружениях только с `core` +> они не работают. -### Custom Panic Handlers and Allocators +### Собственные обработчики паники и аллокаторы -In `#![no_std]` binaries (not libraries), you must provide a panic handler -and optionally a global allocator: +В бинарниках `#![no_std]` (а не в библиотеках) нужно предоставить обработчик паники и, +при необходимости, глобальный аллокатор: ```rust -// src/main.rs — a no_std binary (e.g., UEFI diagnostic) +// src/main.rs — бинарник no_std (например, UEFI-диагностика) #![no_std] #![no_main] @@ -253,67 +252,67 @@ extern crate alloc; use core::panic::PanicInfo; -// Required: what to do on panic (no stack unwinding available) +// Обязательно: что делать при панике (раскрутки стека нет) #[panic_handler] fn panic(info: &PanicInfo) -> ! { - // In embedded: blink an LED, write to UART, hang - // In UEFI: write to console, halt - // Minimal: just loop forever + // В embedded: мигнуть светодиодом, писать в UART, зависнуть + // В UEFI: вывести сообщение в консоль, остановиться + // Минимально: просто бесконечный цикл loop { core::hint::spin_loop(); } } -// Required if using alloc: provide a global allocator +// Обязательно при использовании alloc: предоставить глобальный аллокатор use alloc::alloc::{GlobalAlloc, Layout}; struct BumpAllocator { - // Simple bump allocator for embedded/UEFI - // In practice, use a crate like `linked_list_allocator` or `embedded-alloc` + // Простой bump-аллокатор для embedded/UEFI + // В реальности используйте крейт вроде `linked_list_allocator` или `embedded-alloc` } -// WARNING: This is a non-functional placeholder! Calling alloc() will return -// null, causing immediate UB (the global allocator contract requires non-null -// returns for non-zero-sized allocations). In real code, use an established -// allocator crate: -// - embedded-alloc (embedded targets) -// - linked_list_allocator (UEFI / OS kernels) -// - talc (general-purpose no_std) +// ПРЕДУПРЕЖДЕНИЕ: это нерабочая заглушка! Вызов alloc() вернёт null, +// что сразу приведёт к UB (контракт глобального аллокатора требует +// ненулевых результатов для выделений ненулевого размера). В реальном коде +// используйте проверенный крейт-аллокатор: +// - embedded-alloc (embedded-цели) +// - linked_list_allocator (UEFI / ядра ОС) +// - talc (универсальный no_std) unsafe impl GlobalAlloc for BumpAllocator { /// # Safety - /// Layout must have non-zero size. Returns null (placeholder — will crash). + /// Layout должен иметь ненулевой размер. Возвращает null (заглушка — упадёт). unsafe fn alloc(&self, _layout: Layout) -> *mut u8 { - // PLACEHOLDER — will crash! Replace with real allocation logic. + // ЗАГЛУШКА — упадёт! Замените на реальную логику выделения. core::ptr::null_mut() } /// # Safety - /// `_ptr` must have been returned by `alloc` with a compatible layout. + /// `_ptr` должен быть возвращён `alloc` с совместимым layout. unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { - // No-op for bump allocator + // Ничего не делаем для bump-аллокатора } } #[global_allocator] static ALLOCATOR: BumpAllocator = BumpAllocator {}; -// Entry point (platform-specific, not fn main) -// For UEFI: #[entry] or efi_main -// For embedded: #[cortex_m_rt::entry] +// Точка входа (зависит от платформы, а не fn main) +// Для UEFI: #[entry] или efi_main +// Для embedded: #[cortex_m_rt::entry] ``` -### Testing `no_std` Code +### Тестирование кода `no_std` -Tests run on the host machine, which has `std`. The trick: your library is -`no_std`, but your test harness uses `std`: +Тесты выполняются на хост-машине, где есть `std`. Хитрость: ваша библиотека — `no_std`, +но тестовый харнес использует `std`: ```rust -// Your crate: #![no_std] in src/lib.rs -// But tests run under std automatically: +// Ваш крейт: #![no_std] в src/lib.rs +// Но тесты автоматически работают под std: #[cfg(test)] mod tests { use super::*; - // std is available here — println!, assert!, Vec all work + // std здесь доступен — println!, assert!, Vec работают #[test] fn test_temperature_conversion() { @@ -333,75 +332,77 @@ mod tests { } ``` -**Testing on the actual target** (when `std` isn't available at all): +**Тестирование на реальной цели** (когда `std` вообще недоступен): ```bash -# Use defmt-test for on-device testing (embedded ARM) -# Use uefi-test-runner for UEFI targets -# Use QEMU for cross-architecture tests without hardware +# Для тестов на устройстве (встраиваемый ARM) используйте defmt-test +# Для целей UEFI используйте uefi-test-runner +# Для кросс-архитектурных тестов без железа используйте QEMU -# Run no_std library tests on host (always works): +# Запуск тестов библиотеки no_std на хосте (всегда работает): cargo test --lib -# Verify no_std compilation against a no_std target: +# Проверка компиляции no_std под целевую платформу no_std: cargo check --target thumbv7em-none-eabihf # ARM Cortex-M cargo check --target riscv32imac-unknown-none-elf # RISC-V ``` -### `no_std` Decision Tree +### Дерево решений по `no_std` ```mermaid flowchart TD - START["Does your code need
the standard library?"] --> NEED_FS{"File system,
network, threads?"} - NEED_FS -->|"Yes"| USE_STD["Use std
Normal application"] - NEED_FS -->|"No"| NEED_HEAP{"Need heap allocation?
Vec, String, Box"} - NEED_HEAP -->|"Yes"| USE_ALLOC["#![no_std]
extern crate alloc"] - NEED_HEAP -->|"No"| USE_CORE["#![no_std]
core only"] - + START["Нужна ли вашему коду
стандартная библиотека?"] --> NEED_FS{"Файловая система,
сеть, потоки?"} + NEED_FS -->|"Да"| USE_STD["Используем std
Обычное приложение"] + NEED_FS -->|"Нет"| NEED_HEAP{"Нужно выделение в куче?
Vec, String, Box"} + NEED_HEAP -->|"Да"| USE_ALLOC["#![no_std]
extern crate alloc"] + NEED_HEAP -->|"Нет"| USE_CORE["#![no_std]
только core"] + USE_ALLOC --> VERIFY["cargo-hack
--each-feature"] USE_CORE --> VERIFY USE_STD --> VERIFY - VERIFY --> TARGET{"Target has OS?"} - TARGET -->|"Yes"| HOST_TEST["cargo test --lib
Standard testing"] - TARGET -->|"No"| CROSS_TEST["QEMU / defmt-test
On-device testing"] - + VERIFY --> TARGET{"На цели есть ОС?"} + TARGET -->|"Да"| HOST_TEST["cargo test --lib
Обычное тестирование"] + TARGET -->|"Нет"| CROSS_TEST["QEMU / defmt-test
Тестирование на устройстве"] + style USE_STD fill:#91e5a3,color:#000 style USE_ALLOC fill:#ffd43b,color:#000 style USE_CORE fill:#ff6b6b,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟡 Exercise 1: Feature Combination Verification +#### 🟡 Упражнение 1: проверка комбинаций фич -Install `cargo-hack` and run `cargo hack check --each-feature --workspace` on a project with multiple features. Does it find any broken combinations? +Установите `cargo-hack` и запустите `cargo hack check --each-feature --workspace` на проекте +с несколькими фичами. Находит ли он нерабочие комбинации?
-Solution +Решение ```bash cargo install cargo-hack -# Check each feature individually +# Проверяем каждую фичу по отдельности cargo hack check --each-feature --workspace --no-dev-deps -# If a feature combination fails: +# Если какая-то комбинация фич не компилируется: # error[E0433]: failed to resolve: use of undeclared crate or module `std` -# → This means a feature gate is missing a #[cfg] guard +# → Значит, в фича-гейте не хватает защиты #[cfg] -# Check all features + no features + each individually: +# Проверяем все фичи, отсутствие фич и каждую по отдельности: cargo hack check --each-feature --workspace cargo check --workspace --all-features cargo check --workspace --no-default-features ```
-#### 🔴 Exercise 2: Build a `no_std` Library +#### 🔴 Упражнение 2: сборка библиотеки `no_std` -Create a library crate that compiles with `#![no_std]`. Implement a simple stack-allocated ring buffer. Verify it compiles for `thumbv7em-none-eabihf` (ARM Cortex-M). +Создайте библиотечный крейт, который компилируется с `#![no_std]`. Реализуйте простой кольцевой +буфер на стеке. Проверьте компиляцию под `thumbv7em-none-eabihf` (ARM Cortex-M).
-Solution +Решение ```rust // lib.rs @@ -454,16 +455,16 @@ mod tests { ```bash rustup target add thumbv7em-none-eabihf cargo check --target thumbv7em-none-eabihf -# ✅ Compiles for bare-metal ARM +# ✅ Компилируется для bare-metal ARM ```
-### Key Takeaways +### Ключевые выводы -- `cargo-hack --each-feature` is essential for any crate with conditional compilation — run it in CI -- `core` → `alloc` → `std` are layered: each adds capabilities but requires more runtime support -- Custom panic handlers and allocators are required for bare-metal `no_std` binaries -- Test `no_std` libraries on the host with `cargo test --lib` — no hardware needed -- Run `--feature-powerset` only for core libraries with <8 features — it's $2^n$ combinations +- `cargo-hack --each-feature` обязателен для любого крейта с условной компиляцией — запускайте его в CI +- `core` → `alloc` → `std` — это слои: каждый добавляет возможности, но требует больше поддержки рантайма +- Собственные обработчики паники и аллокаторы обязательны для бинарников `no_std` на голом железе +- Тестируйте библиотеки `no_std` на хосте через `cargo test --lib` — железо не нужно +- Запускайте `--feature-powerset` только для базовых библиотек с менее чем 8 фичами — это $2^n$ комбинаций --- diff --git a/engineering-book/src/ch10-windows-and-conditional-compilation.md b/engineering-book/src/ch10-windows-and-conditional-compilation.md index 835e71e..009c30f 100644 --- a/engineering-book/src/ch10-windows-and-conditional-compilation.md +++ b/engineering-book/src/ch10-windows-and-conditional-compilation.md @@ -1,21 +1,20 @@ -# Windows and Conditional Compilation 🟡 +# Windows и условная компиляция 🟡 -> **What you'll learn:** -> - Windows support patterns: `windows-sys`/`windows` crates, `cargo-xwin` -> - Conditional compilation with `#[cfg]` — checked by the compiler, not the preprocessor -> - Platform abstraction architecture: when `#[cfg]` blocks suffice vs when to use traits -> - Cross-compiling for Windows from Linux +> **Чему вы научитесь:** +> - Паттерны поддержки Windows: крейты `windows-sys`/`windows`, `cargo-xwin` +> - Условная компиляция через `#[cfg]` — её проверяет компилятор, а не препроцессор +> - Архитектура платформенных абстракций: когда хватает блоков `#[cfg]`, а когда нужны трейты +> - Кросс-компиляция под Windows из Linux > -> **Cross-references:** [`no_std` & Features](ch09-no-std-and-feature-verification.md) — `cargo-hack` and feature verification · [Cross-Compilation](ch02-cross-compilation-one-source-many-target.md) — general cross-build setup · [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) — `cfg` flags emitted by `build.rs` +> **Перекрёстные ссылки:** [`no_std` и фичи](ch09-no-std-and-feature-verification.md) — `cargo-hack` и проверка фич · [Кросс-компиляция](ch02-cross-compilation-one-source-many-target.md) — общая настройка кросс-сборки · [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) — `cfg`-флаги, которые выводит `build.rs` -### Windows Support — Platform Abstractions +### Поддержка Windows — платформенные абстракции -Rust's `#[cfg()]` attributes and Cargo features allow a single codebase to -target both Linux and Windows cleanly. The project already -demonstrates this pattern in `platform::run_command`: +Атрибуты `#[cfg()]` в Rust и фичи Cargo позволяют одной кодовой базе аккуратно работать +и на Linux, и на Windows. В проекте этот паттерн уже есть в `platform::run_command`: ```rust -// Real pattern from the project — platform-specific shell invocation +// Реальный паттерн из проекта — запуск оболочки в зависимости от платформы pub fn exec_cmd(cmd: &str, timeout_secs: Option) -> Result { #[cfg(windows)] let mut child = Command::new("cmd") @@ -31,50 +30,50 @@ pub fn exec_cmd(cmd: &str, timeout_secs: Option) -> Result Option { let mut status = SYSTEM_POWER_STATUS::default(); - // SAFETY: GetSystemPowerStatus writes to the provided buffer. - // The buffer is correctly sized and aligned. + // SAFETY: GetSystemPowerStatus записывает в переданный буфер. + // Буфер корректного размера и выравнивания. let ok = unsafe { GetSystemPowerStatus(&mut status) }; if ok != 0 { Some(status.BatteryLifePercent) @@ -113,53 +111,53 @@ mod win { } ``` -**`windows-sys` vs `windows` crate:** +**`windows-sys` и крейт `windows`:** -| Aspect | `windows-sys` | `windows` | -|--------|---------------|----------| -| API style | Raw FFI (`unsafe` calls) | Safe Rust wrappers | -| Binary size | Minimal (just extern declarations) | Larger (wrapper code) | -| Compile time | Fast | Slower | -| Ergonomics | C-style, manual safety | Rust-idiomatic | -| Error handling | Raw `BOOL` / `HRESULT` | `Result` | -| Use when | Performance-critical, thin wrapper | Application code, ease of use | +| Аспект | `windows-sys` | `windows` | +|--------|---------------|-----------| +| Стиль API | Сырой FFI (вызовы через `unsafe`) | Безопасные обёртки на Rust | +| Размер бинарника | Минимальный (только объявления extern) | Больше (код обёрток) | +| Время компиляции | Быстрое | Медленнее | +| Эргономика | В стиле C, безопасность вручную | Идиоматично для Rust | +| Обработка ошибок | Сырые `BOOL` / `HRESULT` | `Result` | +| Когда использовать | Критичная к производительности тонкая обёртка | Прикладной код, удобство | -### Cross-Compiling for Windows from Linux +### Кросс-компиляция под Windows из Linux ```bash -# Option 1: MinGW (GNU ABI) +# Вариант 1: MinGW (GNU ABI) rustup target add x86_64-pc-windows-gnu sudo apt install gcc-mingw-w64-x86-64 cargo build --target x86_64-pc-windows-gnu -# Produces a .exe — runs on Windows, links against msvcrt +# Получаем .exe — запускается на Windows, линкуется с msvcrt -# Option 2: MSVC ABI via xwin (for full MSVC compatibility) +# Вариант 2: ABI MSVC через xwin (для полной совместимости с MSVC) cargo install cargo-xwin cargo xwin build --target x86_64-pc-windows-msvc -# Uses Microsoft's CRT and SDK headers downloaded automatically +# Использует CRT и заголовки SDK от Microsoft, которые скачиваются автоматически -# Option 3: Zig-based cross-compilation +# Вариант 3: кросс-компиляция на основе Zig cargo zigbuild --target x86_64-pc-windows-gnu ``` -**GNU vs MSVC ABI on Windows:** +**GNU и MSVC ABI в Windows:** -| Aspect | `x86_64-pc-windows-gnu` | `x86_64-pc-windows-msvc` | -|--------|-------------------------|---------------------------| -| Linker | MinGW `ld` | MSVC `link.exe` or `lld-link` | -| C runtime | `msvcrt.dll` (universal) | `ucrtbase.dll` (modern) | -| C++ interop | GCC ABI | MSVC ABI | -| Cross-compile from Linux | Easy (MinGW) | Possible (`cargo-xwin`) | -| Windows API support | Full | Full | -| Debug info format | DWARF | PDB | -| Recommended for | Simple tools, CI builds | Full Windows integration | +| Аспект | `x86_64-pc-windows-gnu` | `x86_64-pc-windows-msvc` | +|--------|-------------------------|--------------------------| +| Линкер | MinGW `ld` | MSVC `link.exe` или `lld-link` | +| C-рантайм | `msvcrt.dll` (универсальный) | `ucrtbase.dll` (современный) | +| Совместимость с C++ | ABI GCC | ABI MSVC | +| Кросс-компиляция из Linux | Просто (MinGW) | Возможна (`cargo-xwin`) | +| Поддержка Windows API | Полная | Полная | +| Формат отладочной информации | DWARF | PDB | +| Рекомендуется для | Простых инструментов, сборок CI | Полной интеграции с Windows | -### Conditional Compilation Patterns +### Паттерны условной компиляции -**Pattern 1: Platform module selection** +**Паттерн 1: выбор платформенного модуля** ```rust -// src/platform/mod.rs — compile different modules per OS +// src/platform/mod.rs — компилируем разные модули для разных ОС #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] @@ -170,34 +168,34 @@ mod windows; #[cfg(target_os = "windows")] pub use windows::*; -// Both modules implement the same public API: +// Оба модуля реализуют один и тот же публичный API: // pub fn get_cpu_temperature() -> Result // pub fn list_pci_devices() -> Result, PlatformError> ``` -**Pattern 2: Feature-gated platform support** +**Паттерн 2: платформенная поддержка за фичами** ```toml # Cargo.toml [features] default = ["linux"] -linux = [] # Linux-specific hardware access -windows = ["dep:windows-sys"] # Windows-specific APIs +linux = [] # Доступ к железу, специфичный для Linux +windows = ["dep:windows-sys"] # API, специфичные для Windows [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.59", features = [...], optional = true } ``` ```rust -// Compile error if someone tries to build for Windows without the feature: +// Ошибка компиляции, если кто-то пытается собрать под Windows без фичи: #[cfg(all(target_os = "windows", not(feature = "windows")))] -compile_error!("Enable the 'windows' feature to build for Windows"); +compile_error!("Включите фичу 'windows', чтобы собрать проект для Windows"); ``` -**Pattern 3: Trait-based platform abstraction** +**Паттерн 3: платформенная абстракция на трейтах** ```rust -/// Platform-independent interface for hardware access. +/// Не зависящий от платформы интерфейс доступа к железу. pub trait HardwareAccess { type Error: std::error::Error; @@ -215,7 +213,7 @@ impl HardwareAccess for LinuxHardware { type Error = LinuxHwError; fn read_cpu_temperature(&self) -> Result { - // Read from /sys/class/thermal/thermal_zone0/temp + // Читаем из /sys/class/thermal/thermal_zone0/temp let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?; Ok(raw.trim().parse::()? / 1000.0) } @@ -230,13 +228,13 @@ impl HardwareAccess for WindowsHardware { type Error = WindowsHwError; fn read_cpu_temperature(&self) -> Result { - // Read via WMI (Win32_TemperatureProbe) or Open Hardware Monitor - todo!("WMI temperature query") + // Читаем через WMI (Win32_TemperatureProbe) или Open Hardware Monitor + todo!("запрос температуры через WMI") } // ... } -/// Create the platform-appropriate implementation +/// Создаём реализацию, подходящую для текущей платформы pub fn create_hardware() -> impl HardwareAccess { #[cfg(target_os = "linux")] { LinuxHardware } @@ -245,24 +243,25 @@ pub fn create_hardware() -> impl HardwareAccess { } ``` -### Platform Abstraction Architecture +### Архитектура платформенной абстракции -For a project that targets multiple platforms, organize code into three layers: +Для проекта, который работает на нескольких платформах, код стоит разложить по трём слоям: ```text ┌──────────────────────────────────────────────────┐ -│ Application Logic (platform-independent) │ -│ diag_tool, accel_diag, network_diag, event_log, etc. │ -│ Uses only the platform abstraction trait │ +│ Прикладная логика (не зависит от платформы) │ +│ diag_tool, accel_diag, network_diag, event_log │ +│ Использует только трейт абстракции платформы │ ├──────────────────────────────────────────────────┤ -│ Platform Abstraction Layer (trait definitions) │ -│ trait HardwareAccess { ... } │ -│ trait CommandRunner { ... } │ -│ trait FileSystem { ... } │ +│ Слой абстракции платформы (определения трейтов) │ +│ trait HardwareAccess { ... } │ +│ trait CommandRunner { ... } │ +│ trait FileSystem { ... } │ ├──────────────────────────────────────────────────┤ -│ Platform Implementations (cfg-gated) │ +│ Платформенные реализации (через cfg) │ │ ┌──────────────┐ ┌──────────────┐ │ -│ │ Linux impl │ │ Windows impl │ │ +│ │ Реализация │ │ Реализация │ │ +│ │ для Linux │ │ для Windows │ │ │ │ /sys, /proc │ │ WMI, Registry│ │ │ │ ipmitool │ │ ipmiutil │ │ │ │ lspci │ │ devcon │ │ @@ -270,7 +269,7 @@ For a project that targets multiple platforms, organize code into three layers: └──────────────────────────────────────────────────┘ ``` -**Testing the abstraction**: Mock the platform trait for unit tests: +**Тестирование абстракции**: мокайте платформенный трейт в модульных тестах: ```rust #[cfg(test)] @@ -294,12 +293,12 @@ mod tests { .copied() .ok_or_else(|| std::io::Error::new( std::io::ErrorKind::NotFound, - format!("GPU {index} not found") + format!("GPU {index} не найден") )) } fn list_pci_devices(&self) -> Result, Self::Error> { - Ok(vec![]) // Mock returns empty + Ok(vec![]) // Мок возвращает пустой результат } fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result { @@ -319,87 +318,87 @@ mod tests { } ``` -### Application: Linux-First, Windows-Ready +### Приложение: сначала Linux, готовность к Windows -The project is already partially Windows-ready. Use -[`cargo-hack`](ch09-no-std-and-feature-verification.md) to verify all feature -combinations, and [cross-compile](ch02-cross-compilation-one-source-many-target.md) -to test on Windows from Linux: +Проект уже частично готов к Windows. Используйте +[`cargo-hack`](ch09-no-std-and-feature-verification.md), чтобы проверить все комбинации фич, +и [кросс-компиляцию](ch02-cross-compilation-one-source-many-target.md), чтобы тестировать +под Windows из Linux: -**Already done:** -- `platform::run_command` uses `#[cfg(windows)]` for shell selection -- Tests use `#[cfg(windows)]` / `#[cfg(not(windows))]` for platform-appropriate - test commands +**Уже сделано:** +- `platform::run_command` использует `#[cfg(windows)]` для выбора оболочки +- Тесты используют `#[cfg(windows)]` / `#[cfg(not(windows))]`, чтобы подбирать команды, + подходящие для платформы -**Recommended evolution path for Windows support:** +**Рекомендуемый путь развития поддержки Windows:** ```text -Phase 1: Extract platform abstraction trait (current → 2 weeks) - ├─ Define HardwareAccess trait in core_lib - ├─ Wrap current Linux code behind LinuxHardware impl - └─ All diagnostic modules depend on trait, not Linux specifics - -Phase 2: Add Windows stubs (2 weeks) - ├─ Implement WindowsHardware with TODO stubs - ├─ CI builds for x86_64-pc-windows-msvc (compile check only) - └─ Tests pass with MockHardware on all platforms - -Phase 3: Windows implementation (ongoing) - ├─ IPMI via ipmiutil.exe or OpenIPMI Windows driver - ├─ GPU via accel-mgmt (accel-api.dll) — same API as Linux - ├─ PCIe via Windows Setup API (SetupDiEnumDeviceInfo) - └─ NIC via WMI (Win32_NetworkAdapter) +Фаза 1: выделяем трейт платформенной абстракции (текущий этап → 2 недели) + ├─ определяем трейт HardwareAccess в core_lib + ├─ оборачиваем текущий код для Linux в реализацию LinuxHardware + └─ все диагностические модули зависят от трейта, а не от особенностей Linux + +Фаза 2: добавляем заглушки для Windows (2 недели) + ├─ реализуем WindowsHardware с заглушками TODO + ├─ CI собирает проект для x86_64-pc-windows-msvc (только проверка компиляции) + └─ тесты проходят с MockHardware на всех платформах + +Фаза 3: реализация для Windows (постоянная работа) + ├─ IPMI через ipmiutil.exe или драйвер OpenIPMI для Windows + ├─ GPU через accel-mgmt (accel-api.dll) — тот же API, что и в Linux + ├─ PCIe через Windows Setup API (SetupDiEnumDeviceInfo) + └─ NIC через WMI (Win32_NetworkAdapter) ``` -**Cross-platform CI addition:** +**Дополнение к кросс-платформенному CI:** ```yaml -# Add to CI matrix +# Добавьте в матрицу CI - target: x86_64-pc-windows-msvc os: windows-latest name: windows-x86_64 ``` -This ensures the codebase compiles on Windows even before full Windows -implementation is complete — catching `cfg` mistakes early. +Это гарантирует, что кодовая база компилируется под Windows ещё до завершения полной +реализации, а ошибки в `cfg` выявляются рано. -> **Key insight**: The abstraction doesn't need to be perfect on day one. -> Start with `#[cfg]` blocks in leaf functions (like `exec_cmd` already does), -> then refactor to traits when you have two or more platform implementations. -> Premature abstraction is worse than `#[cfg]` blocks. +> **Ключевая мысль**: абстракция не обязана быть идеальной с первого дня. Начинайте с блоков +> `#[cfg]` в листовых функциях (как уже делает `exec_cmd`), а переходите к трейтам, когда +> появятся две и более платформенные реализации. Преждевременная абстракция хуже, чем блоки `#[cfg]`. -### Conditional Compilation Decision Tree +### Дерево решений по условной компиляции ```mermaid flowchart TD - START["Platform-specific code?"] --> HOW_MANY{"How many platforms?"} - - HOW_MANY -->|"2 (Linux + Windows)"| CFG_BLOCKS["#[cfg] blocks
in leaf functions"] - HOW_MANY -->|"3+"| TRAIT_APPROACH["Platform trait
+ per-platform impl"] - - CFG_BLOCKS --> WINAPI{"Need Windows APIs?"} - WINAPI -->|"Minimal"| WIN_SYS["windows-sys
Raw FFI bindings"] - WINAPI -->|"Rich (COM, etc)"| WIN_RS["windows crate
Safe idiomatic wrappers"] - WINAPI -->|"None
(just #[cfg])"| NATIVE["cfg(windows)
cfg(unix)"] - + START["Платформенно-специфичный код?"] --> HOW_MANY{"Сколько платформ?"} + + HOW_MANY -->|"2 (Linux + Windows)"| CFG_BLOCKS["Блоки #[cfg]
в листовых функциях"] + HOW_MANY -->|"3+"| TRAIT_APPROACH["Трейт платформы
+ реализация для каждой"] + + CFG_BLOCKS --> WINAPI{"Нужны API Windows?"} + WINAPI -->|"Минимально"| WIN_SYS["windows-sys
Привязки сырого FFI"] + WINAPI -->|"Развёрнуто (COM и др.)"| WIN_RS["Крейт windows
Безопасные идиоматичные обёртки"] + WINAPI -->|"Не нужны
(только #[cfg])"| NATIVE["cfg(windows)
cfg(unix)"] + TRAIT_APPROACH --> CI_CHECK["cargo-hack
--each-feature"] CFG_BLOCKS --> CI_CHECK - CI_CHECK --> XCOMPILE["Cross-compile in CI
cargo-xwin or
native runners"] - + CI_CHECK --> XCOMPILE["Кросс-компиляция в CI
cargo-xwin или
нативные раннеры"] + style CFG_BLOCKS fill:#91e5a3,color:#000 style TRAIT_APPROACH fill:#ffd43b,color:#000 style WIN_SYS fill:#e3f2fd,color:#000 style WIN_RS fill:#e3f2fd,color:#000 ``` -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Platform-Conditional Module +#### 🟢 Упражнение 1: платформенно-условный модуль -Create a module with `#[cfg(unix)]` and `#[cfg(windows)]` implementations of a `get_hostname()` function. Verify both compile with `cargo check` and `cargo check --target x86_64-pc-windows-msvc`. +Создайте модуль с реализациями `get_hostname()` под `#[cfg(unix)]` и `#[cfg(windows)]`. +Проверьте, что обе компилируются через `cargo check` и `cargo check --target x86_64-pc-windows-msvc`.
-Solution +Решение ```rust // src/hostname.rs @@ -431,44 +430,44 @@ mod tests { ``` ```bash -# Verify Linux compilation +# Проверка компиляции под Linux cargo check -# Verify Windows compilation (cross-check) +# Проверка компиляции под Windows (кросс-проверка) rustup target add x86_64-pc-windows-msvc cargo check --target x86_64-pc-windows-msvc ```
-#### 🟡 Exercise 2: Cross-Compile for Windows with cargo-xwin +#### 🟡 Упражнение 2: кросс-компиляция под Windows с cargo-xwin -Install `cargo-xwin` and build a simple binary for `x86_64-pc-windows-msvc` from Linux. Verify the output is a `.exe`. +Установите `cargo-xwin` и соберите простой бинарник для `x86_64-pc-windows-msvc` из Linux. +Убедитесь, что результат — `.exe`.
-Solution +Решение ```bash cargo install cargo-xwin rustup target add x86_64-pc-windows-msvc cargo xwin build --release --target x86_64-pc-windows-msvc -# Downloads Windows SDK headers/libs automatically +# Заголовки и библиотеки Windows SDK скачиваются автоматически file target/x86_64-pc-windows-msvc/release/my-binary.exe -# Output: PE32+ executable (console) x86-64, for MS Windows +# Вывод: PE32+ executable (console) x86-64, for MS Windows -# You can also test with Wine: +# Можно также проверить через Wine: wine target/x86_64-pc-windows-msvc/release/my-binary.exe ```
-### Key Takeaways +### Ключевые выводы -- Start with `#[cfg]` blocks in leaf functions; refactor to traits only when three or more platforms diverge -- `windows-sys` is for raw FFI; the `windows` crate provides safe, idiomatic wrappers -- `cargo-xwin` cross-compiles to Windows MSVC ABI from Linux — no Windows machine needed -- Always check `--target x86_64-pc-windows-msvc` in CI even if you only ship on Linux -- Combine `#[cfg]` with Cargo features for optional platform support (e.g., `feature = "windows"`) +- Начинайте с блоков `#[cfg]` в листовых функциях; переходите к трейтам, когда три и более платформ начинают расходиться +- `windows-sys` — для сырого FFI; крейт `windows` предоставляет безопасные идиоматичные обёртки +- `cargo-xwin` кросс-компилирует под ABI MSVC Windows из Linux — машина с Windows не нужна +- Всегда проверяйте `--target x86_64-pc-windows-msvc` в CI, даже если выпускаете только под Linux +- Комбинируйте `#[cfg]` с фичами Cargo для необязательной поддержки платформ (например, `feature = "windows"`) --- - diff --git a/engineering-book/src/ch11-putting-it-all-together-a-production-cic.md b/engineering-book/src/ch11-putting-it-all-together-a-production-cic.md index 8a08900..7c29965 100644 --- a/engineering-book/src/ch11-putting-it-all-together-a-production-cic.md +++ b/engineering-book/src/ch11-putting-it-all-together-a-production-cic.md @@ -1,21 +1,20 @@ -# Putting It All Together — A Production CI/CD Pipeline 🟡 - -> **What you'll learn:** -> - Structuring a multi-stage GitHub Actions CI workflow (check → test → coverage → security → cross → release) -> - Caching strategies with `rust-cache` and `save-if` tuning -> - Running Miri and sanitizers on a nightly schedule -> - Task automation with `Makefile.toml` and pre-commit hooks -> - Automated releases with `cargo-dist` +# Собираем всё вместе — продакшн-конвейер CI/CD 🟡 + +> **Чему вы научитесь:** +> - Структура многоэтапного workflow CI в GitHub Actions (check → test → coverage → security → cross → release) +> - Стратегии кеширования с `rust-cache` и тонкая настройка `save-if` +> - Запуск Miri и санитайзеров по ночному расписанию +> - Автоматизация задач с помощью `Makefile.toml` и pre-commit-хуков +> - Автоматические релизы с `cargo-dist` > -> **Cross-references:** [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) · [Cross-Compilation](ch02-cross-compilation-one-source-many-target.md) · [Benchmarking](ch03-benchmarking-measuring-what-matters.md) · [Coverage](ch04-code-coverage-seeing-what-tests-miss.md) · [Miri/Sanitizers](ch05-miri-valgrind-and-sanitizers-verifying-u.md) · [Dependencies](ch06-dependency-management-and-supply-chain-s.md) · [Release Profiles](ch07-release-profiles-and-binary-size.md) · [Compile-Time Tools](ch08-compile-time-and-developer-tools.md) · [`no_std`](ch09-no-std-and-feature-verification.md) · [Windows](ch10-windows-and-conditional-compilation.md) +> **Перекрёстные ссылки:** [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) · [Кросс-компиляция](ch02-cross-compilation-one-source-many-target.md) · [Бенчмаркинг](ch03-benchmarking-measuring-what-matters.md) · [Покрытие](ch04-code-coverage-seeing-what-tests-miss.md) · [Miri и санитайзеры](ch05-miri-valgrind-and-sanitizers-verifying-u.md) · [Зависимости](ch06-dependency-management-and-supply-chain-s.md) · [Профили релиза](ch07-release-profiles-and-binary-size.md) · [Инструменты времени компиляции](ch08-compile-time-and-developer-tools.md) · [`no_std`](ch09-no-std-and-feature-verification.md) · [Windows](ch10-windows-and-conditional-compilation.md) -Individual tools are useful. A pipeline that orchestrates them automatically on -every push is transformative. This chapter assembles the tools from chapters 1–10 -into a cohesive CI/CD workflow. +Отдельные инструменты полезны. Конвейер, который автоматически запускает их на каждый push, +меняет всё. Эта глава собирает инструменты из глав 1–10 в единый CI/CD-workflow. -### The Complete GitHub Actions Workflow +### Полный workflow GitHub Actions -A single workflow file that runs all verification stages in parallel: +Один файл workflow, который запускает все этапы проверки параллельно: ```yaml # .github/workflows/ci.yml @@ -29,15 +28,15 @@ on: env: CARGO_TERM_COLOR: always - CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" # Treat warnings as errors (top-level crate only) - # NOTE: Unlike RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS does not affect build scripts - # or proc-macros, which avoids false failures from third-party warnings. - # Use RUSTFLAGS="-Dwarnings" instead if you want to enforce on build scripts too. + CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" # Считать предупреждения ошибками (только для верхнеуровневого крейта) + # ПРИМЕЧАНИЕ: в отличие от RUSTFLAGS, CARGO_ENCODED_RUSTFLAGS не влияет на build-скрипты + # и процедурные макросы, что избавляет от ложных падений из-за предупреждений сторонних крейтов. + # Если хотите применять правило и к build-скриптам, используйте RUSTFLAGS="-Dwarnings". jobs: - # ─── Stage 1: Fast feedback (< 2 min) ─── + # ─── Этап 1: быстрая обратная связь (< 2 мин) ─── check: - name: Check + Clippy + Format + name: Проверка + Clippy + форматирование runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -45,26 +44,26 @@ jobs: with: components: clippy, rustfmt - - uses: Swatinem/rust-cache@v2 # Cache dependencies + - uses: Swatinem/rust-cache@v2 # Кешируем зависимости - - name: Check Cargo.lock + - name: Проверка Cargo.lock run: cargo fetch --locked - - name: Check doc + - name: Проверка документации run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps - - name: Check compilation + - name: Проверка компиляции run: cargo check --workspace --all-targets --all-features - - name: Clippy lints + - name: Линты Clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings - - name: Formatting + - name: Форматирование run: cargo fmt --all -- --check - # ─── Stage 2: Tests (< 5 min) ─── + # ─── Этап 2: тесты (< 5 мин) ─── test: - name: Test (${{ matrix.os }}) + name: Тесты (${{ matrix.os }}) needs: check strategy: matrix: @@ -75,15 +74,15 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - name: Run tests + - name: Запуск тестов run: cargo test --workspace - - name: Run doc tests + - name: Запуск doc-тестов run: cargo test --workspace --doc - # ─── Stage 3: Cross-compilation (< 10 min) ─── + # ─── Этап 3: кросс-компиляция (< 10 мин) ─── cross: - name: Cross (${{ matrix.target }}) + name: Кросс-сборка (${{ matrix.target }}) needs: check strategy: matrix: @@ -100,31 +99,31 @@ jobs: with: targets: ${{ matrix.target }} - - name: Install musl-tools + - name: Установка musl-tools if: contains(matrix.target, 'musl') run: sudo apt-get install -y musl-tools - - name: Install cross + - name: Установка cross if: matrix.use_cross uses: taiki-e/install-action@cross - - name: Build (native) + - name: Сборка (нативная) if: "!matrix.use_cross" run: cargo build --release --target ${{ matrix.target }} - - name: Build (cross) + - name: Сборка (cross) if: matrix.use_cross run: cross build --release --target ${{ matrix.target }} - - name: Upload artifact + - name: Загрузка артефакта uses: actions/upload-artifact@v4 with: name: binary-${{ matrix.target }} path: target/${{ matrix.target }}/release/diag_tool - # ─── Stage 4: Coverage (< 10 min) ─── + # ─── Этап 4: покрытие (< 10 мин) ─── coverage: - name: Code Coverage + name: Покрытие кода needs: check runs-on: ubuntu-latest steps: @@ -134,19 +133,19 @@ jobs: components: llvm-tools-preview - uses: taiki-e/install-action@cargo-llvm-cov - - name: Generate coverage + - name: Генерация покрытия run: cargo llvm-cov --workspace --lcov --output-path lcov.info - - name: Enforce minimum coverage + - name: Проверка минимального покрытия run: cargo llvm-cov --workspace --fail-under-lines 75 - - name: Upload to Codecov + - name: Загрузка в Codecov uses: codecov/codecov-action@v4 with: files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} - # ─── Stage 5: Safety verification (< 15 min) ─── + # ─── Этап 5: проверка безопасности (< 15 мин) ─── miri: name: Miri needs: check @@ -157,14 +156,14 @@ jobs: with: components: miri - - name: Run Miri + - name: Запуск Miri run: cargo miri test --workspace env: MIRIFLAGS: "-Zmiri-backtrace=full" - # ─── Stage 6: Benchmarks (PR only, < 10 min) ─── + # ─── Этап 6: бенчмарки (только для PR, < 10 мин) ─── bench: - name: Benchmarks + name: Бенчмарки if: github.event_name == 'pull_request' needs: check runs-on: ubuntu-latest @@ -172,10 +171,10 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - name: Run benchmarks + - name: Запуск бенчмарков run: cargo bench -- --output-format bencher | tee bench.txt - - name: Compare with baseline + - name: Сравнение с базовой линией uses: benchmark-action/github-action-benchmark@v1 with: tool: 'cargo' @@ -185,11 +184,11 @@ jobs: comment-on-alert: true ``` -**Pipeline execution flow:** +**Схема выполнения конвейера:** ```text ┌─────────┐ - │ check │ ← clippy + fmt + cargo check (2 min) + │ check │ ← clippy + fmt + cargo check (2 мин) └────┬────┘ ┌─────────┬──┴──┬──────────┬──────────┐ ▼ ▼ ▼ ▼ ▼ @@ -197,73 +196,73 @@ jobs: │ test │ │cross │ │coverage│ │ miri │ │bench │ │ (2×) │ │ (2×) │ │ │ │ │ │(PR) │ └──────┘ └──────┘ └────────┘ └──────┘ └──────┘ - 3 min 8 min 8 min 12 min 5 min + 3 мин 8 мин 8 мин 12 мин 5 мин -Total wall-clock: ~14 min (parallel after check gate) +Общее время: ~14 мин (параллельно после шлюза check) ``` -### CI Caching Strategies +### Стратегии кеширования в CI -[`Swatinem/rust-cache@v2`](https://github.com/Swatinem/rust-cache) is the -standard Rust CI cache action. It caches `~/.cargo` and `target/` between -runs, but large workspaces need tuning: +[`Swatinem/rust-cache@v2`](https://github.com/Swatinem/rust-cache) — стандартное для Rust +действие кеширования в CI. Оно кеширует `~/.cargo` и `target/` между запусками, но большим +воркспейсам нужна тонкая настройка: ```yaml -# Basic (what we use above) +# Базовый вариант (то, что используем выше) - uses: Swatinem/rust-cache@v2 -# Tuned for a large workspace: +# Настроенный для большого воркспейса: - uses: Swatinem/rust-cache@v2 with: - # Separate caches per job — prevents test artifacts bloating build cache + # Отдельные кеши для каждой джобы — не даём артефактам тестов раздувать кеш сборки prefix-key: "v1-rust" key: ${{ matrix.os }}-${{ matrix.target || 'default' }} - # Only save cache on main branch (PRs read but don't write) + # Сохраняем кеш только в ветке main (PR читают, но не записывают) save-if: ${{ github.ref == 'refs/heads/main' }} - # Cache Cargo registry + git checkouts + target dir + # Кешируем реестр Cargo, git-чекауты и директорию target cache-targets: true cache-all-crates: true ``` -**Cache invalidation gotchas:** +**Подводные камни инвалидации кеша:** -| Problem | Fix | -|---------|-----| -| Cache grows unbounded (>5 GB) | Set `prefix-key: "v2-rust"` to force fresh cache | -| Different features pollute cache | Use `key: ${{ hashFiles('**/Cargo.lock') }}` | -| PR cache overwrites main | Set `save-if: ${{ github.ref == 'refs/heads/main' }}` | -| Cross-compilation targets bloat | Use separate `key` per target triple | +| Проблема | Решение | +|----------|---------| +| Кеш растёт без границ (>5 ГБ) | Задайте `prefix-key: "v2-rust"`, чтобы принудительно создать новый кеш | +| Разные фичи засоряют кеш | Используйте `key: ${{ hashFiles('**/Cargo.lock') }}` | +| Кеш из PR перезаписывает main | Задайте `save-if: ${{ github.ref == 'refs/heads/main' }}` | +| Цели кросс-компиляции раздувают кеш | Используйте отдельный `key` для каждой тройки целей (target triple) | -**Sharing cache between jobs:** +**Совместное использование кеша между джобами:** -The `check` job saves the cache; downstream jobs (`test`, `cross`, `coverage`) -read it. With `save-if` on `main` only, PR runs get the benefit of cached -dependencies without writing stale caches. +Джоба `check` сохраняет кеш, а последующие джобы (`test`, `cross`, `coverage`) его читают. +Благодаря `save-if` только для `main` запуски PR получают выгоду от кешированных зависимостей +и не записывают устаревшие кеши. -> **Measured impact on large-scale workspace**: Cold build ~4 min → -> cached build ~45 sec. The cache action alone saves ~25 min of CI time per -> pipeline run (across all parallel jobs). +> **Измеренный эффект на крупном воркспейсе**: холодная сборка ~4 мин → сборка из кеша ~45 с. +> Одно только действие кеширования экономит около 25 минут CI-времени за запуск конвейера +> (по всем параллельным джобам). -### Makefile.toml with cargo-make +### Makefile.toml с cargo-make -[`cargo-make`](https://sagiegurari.github.io/cargo-make/) provides a portable -task runner that works across platforms (unlike `make`/`Makefile`): +[`cargo-make`](https://sagiegurari.github.io/cargo-make/) — переносимый исполнитель задач, +который работает на разных платформах (в отличие от `make`/`Makefile`): ```bash -# Install +# Установка cargo install cargo-make ``` ```toml -# Makefile.toml — at workspace root +# Makefile.toml — в корне воркспейса [config] default_to_workspace = false -# ─── Developer workflows ─── +# ─── Рабочие сценарии разработчика ─── [tasks.dev] -description = "Full local verification (same checks as CI)" +description = "Полная локальная проверка (те же проверки, что и в CI)" dependencies = ["check", "test", "clippy", "fmt-check"] [tasks.check] @@ -286,89 +285,89 @@ args = ["fmt", "--all"] command = "cargo" args = ["fmt", "--all", "--", "--check"] -# ─── Coverage ─── +# ─── Покрытие ─── [tasks.coverage] -description = "Generate HTML coverage report" +description = "Генерация HTML-отчёта о покрытии" install_crate = "cargo-llvm-cov" command = "cargo" args = ["llvm-cov", "--workspace", "--html", "--open"] [tasks.coverage-ci] -description = "Generate LCOV for CI upload" +description = "Генерация LCOV для загрузки в CI" install_crate = "cargo-llvm-cov" command = "cargo" args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"] -# ─── Benchmarks ─── +# ─── Бенчмарки ─── [tasks.bench] -description = "Run all benchmarks" +description = "Запуск всех бенчмарков" command = "cargo" args = ["bench"] -# ─── Cross-compilation ─── +# ─── Кросс-компиляция ─── [tasks.build-musl] -description = "Build static binary (musl)" +description = "Сборка статического бинарника (musl)" command = "cargo" args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"] [tasks.build-arm] -description = "Build for aarch64 (requires cross)" +description = "Сборка для aarch64 (требует cross)" command = "cross" args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"] [tasks.build-all] -description = "Build for all deployment targets" +description = "Сборка для всех целей развёртывания" dependencies = ["build-musl", "build-arm"] -# ─── Safety verification ─── +# ─── Проверка безопасности ─── [tasks.miri] -description = "Run Miri on all tests" +description = "Запуск Miri на всех тестах" toolchain = "nightly" command = "cargo" args = ["miri", "test", "--workspace"] [tasks.audit] -description = "Check for known vulnerabilities" +description = "Проверка на известные уязвимости" install_crate = "cargo-audit" command = "cargo" args = ["audit"] -# ─── Release ─── +# ─── Релиз ─── [tasks.release-dry] -description = "Preview what cargo-release would do" +description = "Показать, что сделал бы cargo-release (без выполнения)" install_crate = "cargo-release" command = "cargo" args = ["release", "--workspace", "--dry-run"] ``` -**Usage:** +**Использование:** ```bash -# Equivalent of CI pipeline, locally +# Аналог конвейера CI, но локально cargo make dev -# Generate and view coverage +# Генерация и просмотр покрытия cargo make coverage -# Build for all targets +# Сборка под все цели cargo make build-all -# Run safety checks +# Проверки безопасности cargo make miri -# Check for vulnerabilities +# Проверка на уязвимости cargo make audit ``` -### Pre-Commit Hooks: Custom Scripts and `cargo-husky` +### Pre-commit-хуки: собственные скрипты и `cargo-husky` -Catch issues *before* they reach CI. The recommended approach is a custom -git hook — it's simple, transparent, and has no external dependencies: +Ловите проблемы *до* того, как они попадут в CI. Рекомендуемый подход — собственный git-хук: +он простой, прозрачный и не имеет внешних зависимостей: ```bash #!/bin/sh @@ -376,9 +375,9 @@ git hook — it's simple, transparent, and has no external dependencies: set -e -echo "=== Pre-commit checks ===" +echo "=== Проверки перед коммитом ===" -# Fast checks first +# Сначала быстрые проверки echo "→ cargo fmt --check" cargo fmt --all -- --check @@ -388,30 +387,29 @@ cargo check --workspace --all-targets echo "→ cargo clippy" cargo clippy --workspace --all-targets -- -D warnings -echo "→ cargo test (lib only, fast)" +echo "→ cargo test (только lib, быстро)" cargo test --workspace --lib -echo "=== All checks passed ===" +echo "=== Все проверки пройдены ===" ``` ```bash -# Install the hook +# Установка хука git config core.hooksPath .githooks chmod +x .githooks/pre-commit ``` -**Alternative: `cargo-husky`** (auto-installs hooks via build script): +**Альтернатива: `cargo-husky`** (автоматически устанавливает хуки через build-скрипт): -> ⚠️ **Note**: `cargo-husky` has not been updated since 2022. It still works -> but is effectively unmaintained. Consider the custom hook approach above -> for new projects. +> ⚠️ **Примечание**: `cargo-husky` не обновлялся с 2022 года. Он всё ещё работает, но +> фактически не поддерживается. Для новых проектов рассмотрите описанный выше собственный хук. ```bash cargo install cargo-husky ``` ```toml -# Cargo.toml — add to dev-dependencies of root crate +# Cargo.toml — добавьте в dev-dependencies корневого крейта [dev-dependencies] cargo-husky = { version = "1", default-features = false, features = [ "precommit-hook", @@ -422,24 +420,24 @@ cargo-husky = { version = "1", default-features = false, features = [ ] } ``` -### Release Workflow: `cargo-release` and `cargo-dist` +### Workflow релиза: `cargo-release` и `cargo-dist` -**`cargo-release`** — automates version bumping, tagging, and publishing: +**`cargo-release`** — автоматизирует повышение версии, создание тегов и публикацию: ```bash -# Install +# Установка cargo install cargo-release ``` ```toml -# release.toml — at workspace root +# release.toml — в корне воркспейса [workspace] consolidate-commits = true pre-release-commit-message = "chore: release {{version}}" tag-message = "v{{version}}" tag-name = "v{{version}}" -# Don't publish internal crates +# Не публикуем внутренние крейты [[package]] name = "core_lib" release = false @@ -448,17 +446,17 @@ release = false name = "diag_framework" release = false -# Only publish the main binary +# Публикуем только основной бинарник [[package]] name = "diag_tool" release = true ``` ```bash -# Preview release +# Предварительный просмотр релиза cargo release patch --dry-run -# Execute release (bumps version, commits, tags, optionally publishes) +# Выполнение релиза (повышает версию, коммитит, ставит тег, при необходимости публикует) cargo release patch --execute # 0.1.0 → 0.1.1 @@ -466,24 +464,24 @@ cargo release minor --execute # 0.1.1 → 0.2.0 ``` -**`cargo-dist`** — generates downloadable release binaries for GitHub Releases: +**`cargo-dist`** — генерирует готовые к скачиванию бинарники для GitHub Releases: ```bash -# Install +# Установка cargo install cargo-dist -# Initialize (creates CI workflow + metadata) +# Инициализация (создаёт CI-workflow и метаданные) cargo dist init -# Preview what would be built +# Предварительный просмотр того, что будет собрано cargo dist plan -# Generate the release (usually done by CI on tag push) +# Генерация релиза (обычно выполняется CI при пуше тега) cargo dist build ``` ```toml -# Cargo.toml additions from `cargo dist init` +# Дополнения в Cargo.toml, которые добавляет `cargo dist init` [workspace.metadata.dist] cargo-dist-version = "0.28.0" ci = "github" @@ -496,75 +494,73 @@ targets = [ install-path = "CARGO_HOME" ``` -This generates a GitHub Actions workflow that, on tag push: -1. Builds the binary for all target platforms -2. Creates a GitHub Release with downloadable `.tar.gz` / `.zip` archives -3. Generates shell/PowerShell installer scripts -4. Publishes to crates.io (if configured) +Это генерирует workflow GitHub Actions, который при пуше тега: +1. Собирает бинарник для всех целевых платформ +2. Создаёт GitHub Release с архивами `.tar.gz` / `.zip` для скачивания +3. Генерирует установочные скрипты для shell и PowerShell +4. Публикует на crates.io (если это настроено) -### Try It Yourself — Capstone Exercise +### Try It Yourself — итоговое упражнение -This exercise ties together every chapter. You will build a complete -engineering pipeline for a fresh Rust workspace: +Это упражнение связывает все главы. Вы создадите полный инженерный конвейер для нового +воркспейса Rust: -1. **Create a new workspace** with two crates: a library (`core_lib`) and a - binary (`cli`). Add a `build.rs` that embeds the git hash and build - timestamp using `SOURCE_DATE_EPOCH` (ch01). +1. **Создайте новый воркспейс** из двух крейтов: библиотеки (`core_lib`) и бинарника (`cli`). + Добавьте `build.rs`, который встраивает хеш git и временную метку сборки, используя + `SOURCE_DATE_EPOCH` (гл. 1). -2. **Set up cross-compilation** for `x86_64-unknown-linux-musl` and - `aarch64-unknown-linux-gnu`. Verify both targets build with - `cargo zigbuild` or `cross` (ch02). +2. **Настройте кросс-компиляцию** для `x86_64-unknown-linux-musl` и `aarch64-unknown-linux-gnu`. + Убедитесь, что обе цели собираются через `cargo zigbuild` или `cross` (гл. 2). -3. **Add a benchmark** using Criterion or Divan for a function in `core_lib`. - Run it locally and record a baseline (ch03). +3. **Добавьте бенчмарк** с помощью Criterion или Divan для одной функции в `core_lib`. + Запустите его локально и зафиксируйте базовую линию (гл. 3). -4. **Measure code coverage** with `cargo llvm-cov`. Set a minimum threshold - of 80% and verify it passes (ch04). +4. **Измерьте покрытие кода** с помощью `cargo llvm-cov`. Задайте минимальный порог 80% + и убедитесь, что он проходит (гл. 4). -5. **Run `cargo +nightly careful test`** and `cargo miri test`. Add a test - that exercises `unsafe` code if you have any (ch05). +5. **Запустите `cargo +nightly careful test`** и `cargo miri test`. Добавьте тест, который + проверяет `unsafe`-код, если он у вас есть (гл. 5). -6. **Configure `cargo-deny`** with a `deny.toml` that bans `openssl` and - enforces MIT/Apache-2.0 licensing (ch06). +6. **Настройте `cargo-deny`** с `deny.toml`, который запрещает `openssl` и требует лицензии + MIT/Apache-2.0 (гл. 6). -7. **Optimize the release profile** with `lto = "thin"`, `strip = true`, and - `codegen-units = 1`. Measure binary size before/after with `cargo bloat` - (ch07). +7. **Оптимизируйте профиль релиза** с `lto = "thin"`, `strip = true` и `codegen-units = 1`. + Замерьте размер бинарника до и после с помощью `cargo bloat` (гл. 7). -8. **Add `cargo hack --each-feature`** verification. Create a feature flag - for an optional dependency and ensure it compiles alone (ch09). +8. **Добавьте проверку `cargo hack --each-feature`**. Создайте флаг фичи для необязательной + зависимости и убедитесь, что он компилируется отдельно (гл. 9). -9. **Write the GitHub Actions workflow** (this chapter) with all 6 stages. - Add `Swatinem/rust-cache@v2` with `save-if` tuning. +9. **Напишите workflow GitHub Actions** (эта глава) со всеми 6 этапами. Добавьте + `Swatinem/rust-cache@v2` с настройкой `save-if`. -**Success criteria**: Push to GitHub → all CI stages green → `cargo dist plan` -shows your release targets. You now have a production-grade Rust pipeline. +**Критерий успеха**: отправьте код на GitHub → все этапы CI зелёные → `cargo dist plan` +показывает ваши целевые платформы релиза. Теперь у вас есть продакшн-уровня конвейер на Rust. -### CI Pipeline Architecture +### Архитектура CI-конвейера ```mermaid flowchart LR - subgraph "Stage 1 — Fast Feedback < 2 min" + subgraph "Этап 1 — быстрая обратная связь < 2 мин" CHECK["cargo check
cargo clippy
cargo fmt"] end - subgraph "Stage 2 — Tests < 5 min" + subgraph "Этап 2 — тесты < 5 мин" TEST["cargo nextest
cargo test --doc"] end - subgraph "Stage 3 — Coverage" - COV["cargo llvm-cov
fail-under 80%"] + subgraph "Этап 3 — покрытие" + COV["cargo llvm-cov
порог 80%"] end - subgraph "Stage 4 — Security" + subgraph "Этап 4 — безопасность" SEC["cargo audit
cargo deny check"] end - subgraph "Stage 5 — Cross-Build" + subgraph "Этап 5 — кросс-сборка" CROSS["musl static
aarch64 + x86_64"] end - subgraph "Stage 6 — Release (tag only)" + subgraph "Этап 6 — релиз (только теги)" REL["cargo dist
GitHub Release"] end @@ -578,12 +574,12 @@ flowchart LR style REL fill:#b39ddb,color:#000 ``` -### Key Takeaways +### Ключевые выводы -- Structure CI as parallel stages: fast checks first, expensive jobs behind gates -- `Swatinem/rust-cache@v2` with `save-if: ${{ github.ref == 'refs/heads/main' }}` prevents PR cache thrashing -- Run Miri and heavier sanitizers on a nightly `schedule:` trigger, not on every push -- `Makefile.toml` (`cargo make`) bundles multi-tool workflows into a single command for local dev -- `cargo-dist` automates cross-platform release builds — stop writing platform matrix YAML by hand +- Стройте CI как параллельные этапы: быстрые проверки — первыми, дорогие джобы — за шлюзами +- `Swatinem/rust-cache@v2` с `save-if: ${{ github.ref == 'refs/heads/main' }}` предотвращает перезапись кеша из PR +- Запускайте Miri и более тяжёлые санитайзеры по расписанию `schedule:` в nightly, а не на каждый push +- `Makefile.toml` (`cargo make`) объединяет многоинструментальные сценарии в одну команду для локальной разработки +- `cargo-dist` автоматизирует сборку релизов для разных платформ — больше не нужно писать матрицу платформ в YAML вручную --- diff --git a/engineering-book/src/ch12-tricks-from-the-trenches.md b/engineering-book/src/ch12-tricks-from-the-trenches.md index 60fa45b..cd9cb86 100644 --- a/engineering-book/src/ch12-tricks-from-the-trenches.md +++ b/engineering-book/src/ch12-tricks-from-the-trenches.md @@ -1,32 +1,31 @@ -# Tricks from the Trenches 🟡 +# Проверенные на практике приёмы 🟡 -> **What you'll learn:** -> - Battle-tested patterns that don't fit neatly into one chapter -> - Common pitfalls and their fixes — from CI flake to binary bloat -> - Quick-win techniques you can apply to any Rust project today +> **Чему вы научитесь:** +> - Проверенные в бою паттерны, которые не умещаются в одну главу +> - Типичные ловушки и способы их обойти — от нестабильного CI до раздувания бинарника +> - Быстрые приёмы, которые можно применить к любому проекту на Rust уже сегодня > -> **Cross-references:** Every chapter in this book — these tricks cut across all topics +> **Перекрёстные ссылки:** каждая глава этой книги — приёмы затрагивают все темы -This chapter collects engineering patterns that come up repeatedly in -production Rust codebases. Each trick is self-contained — read them in -any order. +Эта глава собирает инженерные паттерны, которые регулярно встречаются в продакшн-кодовых базах +на Rust. Каждый приём самостоятельный — читайте их в любом порядке. --- -### 1. The `deny(warnings)` Trap +### 1. Ловушка `deny(warnings)` -**Problem**: `#![deny(warnings)]` in source code breaks builds when Clippy -adds new lints — your code that compiled yesterday fails today. +**Проблема**: `#![deny(warnings)]` в исходном коде ломает сборку, когда Clippy добавляет новые +линты: код, который компилировался вчера, сегодня не собирается. -**Fix**: Use `CARGO_ENCODED_RUSTFLAGS` in CI instead of a source-level attribute: +**Решение**: используйте `CARGO_ENCODED_RUSTFLAGS` в CI вместо атрибута на уровне исходного кода: ```yaml -# CI: treat warnings as errors without touching source +# CI: считаем предупреждения ошибками, не трогая исходный код env: CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" ``` -Or use `[workspace.lints]` for finer control: +Или используйте `[workspace.lints]` для более точной настройки: ```toml # Cargo.toml @@ -38,93 +37,93 @@ all = { level = "deny", priority = -1 } pedantic = { level = "warn", priority = -1 } ``` -> See [Compile-Time Tools, Workspace Lints](ch08-compile-time-and-developer-tools.md) for the full pattern. +> См. [Инструменты времени компиляции, линты воркспейса](ch08-compile-time-and-developer-tools.md) для полного паттерна. --- -### 2. Compile Once, Test Everywhere +### 2. Компилируем один раз, тестируем везде -**Problem**: `cargo test` recompiles when switching between `--lib`, `--doc`, -and `--test` because they use different profiles. +**Проблема**: `cargo test` пересобирает код при переключении между `--lib`, `--doc` и `--test`, +потому что они используют разные профили. -**Fix**: Use `cargo nextest` for unit/integration tests and run doc-tests -separately: +**Решение**: используйте `cargo nextest` для модульных и интеграционных тестов, а doc-тесты +запускайте отдельно: ```bash -cargo nextest run --workspace # Fast: parallel, cached -cargo test --workspace --doc # Doc-tests (nextest can't run these) +cargo nextest run --workspace # Быстро: параллельно, с кешем +cargo test --workspace --doc # Doc-тесты (nextest их запустить не может) ``` -> See [Compile-Time Tools](ch08-compile-time-and-developer-tools.md) for `cargo-nextest` setup. +> См. [Инструменты времени компиляции](ch08-compile-time-and-developer-tools.md) для настройки `cargo-nextest`. --- -### 3. Feature Flag Hygiene +### 3. Гигиена фич -**Problem**: A library crate has `default = ["std"]` but nobody tests -`--no-default-features`. One day an embedded user reports it doesn't compile. +**Проблема**: у библиотечного крейта `default = ["std"]`, но никто не тестирует +`--no-default-features`. Однажды пользователь для встраиваемых систем сообщает, что крейт +не компилируется. -**Fix**: Add `cargo-hack` to CI: +**Решение**: добавьте `cargo-hack` в CI: ```yaml -- name: Feature matrix +- name: Матрица фич run: | cargo hack check --each-feature --no-dev-deps cargo check --no-default-features cargo check --all-features ``` -> See [`no_std` and Feature Verification](ch09-no-std-and-feature-verification.md) for the full pattern. +> См. [`no_std` и проверка фич](ch09-no-std-and-feature-verification.md) для полного паттерна. --- -### 4. The Lock File Debate — Commit or Ignore? +### 4. Дебаты о lock-файле — коммитить или игнорировать? -**Rule of thumb:** +**Простое правило:** -| Crate Type | Commit `Cargo.lock`? | Why | -|------------|---------------------|-----| -| Binary / application | **Yes** | Reproducible builds | -| Library | **No** (`.gitignore`) | Let downstream choose versions | -| Workspace with both | **Yes** | Binary wins | +| Тип крейта | Коммитить `Cargo.lock`? | Почему | +|------------|-------------------------|--------| +| Бинарник / приложение | **Да** | Воспроизводимые сборки | +| Библиотека | **Нет** (`.gitignore`) | Пусть потребители выбирают версии | +| Воркспейс, содержащий оба типа | **Да** | Побеждает бинарник | -Add a CI check to ensure the lock file stays up-to-date: +Добавьте проверку в CI, чтобы lock-файл оставался актуальным: ```yaml -- name: Check lock file - run: cargo update --locked # Fails if Cargo.lock is stale +- name: Проверка lock-файла + run: cargo update --locked # Падает, если Cargo.lock устарел ``` --- -### 5. Debug Builds with Optimized Dependencies +### 5. Отладочные сборки с оптимизированными зависимостями -**Problem**: Debug builds are painfully slow because dependencies (especially -`serde`, `regex`) aren't optimized. +**Проблема**: отладочные сборки мучительно медленные, потому что зависимости (особенно `serde`, +`regex`) не оптимизированы. -**Fix**: Optimize deps in dev profile while keeping your code unoptimized -for fast recompilation: +**Решение**: оптимизируйте зависимости в dev-профиле, оставив свой код неоптимизированным для +быстрой пересборки: ```toml # Cargo.toml [profile.dev.package."*"] -opt-level = 2 # Optimize all dependencies in dev mode +opt-level = 2 # Оптимизировать все зависимости в dev-режиме ``` -This slows the first build slightly but makes runtime dramatically faster -during development. Particularly impactful for database-backed services and -parsers. +Это немного замедляет первую сборку, зато заметно ускоряет выполнение во время разработки. +Особенно полезно для сервисов с базами данных и парсеров. -> See [Release Profiles](ch07-release-profiles-and-binary-size.md) for per-crate profile overrides. +> См. [Профили релиза](ch07-release-profiles-and-binary-size.md) для переопределений профиля по отдельным крейтам. --- -### 6. CI Cache Thrashing +### 6. Постоянная перезапись кеша CI -**Problem**: `Swatinem/rust-cache@v2` saves a new cache on every PR, bloating -storage and slowing restore times. +**Проблема**: `Swatinem/rust-cache@v2` сохраняет новый кеш при каждом PR, раздувая хранилище +и замедляя восстановление. -**Fix**: Only save cache from `main`, restore from anywhere: +**Решение**: сохраняйте кеш только из `main`, а восстанавливать его можно откуда угодно: ```yaml - uses: Swatinem/rust-cache@v2 @@ -132,7 +131,7 @@ storage and slowing restore times. save-if: ${{ github.ref == 'refs/heads/main' }} ``` -For workspaces with multiple binaries, add a `shared-key`: +Для воркспейсов с несколькими бинарниками добавьте `shared-key`: ```yaml - uses: Swatinem/rust-cache@v2 @@ -141,39 +140,37 @@ For workspaces with multiple binaries, add a `shared-key`: save-if: ${{ github.ref == 'refs/heads/main' }} ``` -> See [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) for the full workflow. +> См. [Собираем всё вместе](ch11-putting-it-all-together-a-production-cic.md) для полного workflow. --- -### 7. `RUSTFLAGS` vs `CARGO_ENCODED_RUSTFLAGS` +### 7. `RUSTFLAGS` против `CARGO_ENCODED_RUSTFLAGS` -**Problem**: `RUSTFLAGS="-Dwarnings"` applies to *everything* — including -build scripts and proc-macros. A warning in `serde_derive`'s build.rs -fails your CI. +**Проблема**: `RUSTFLAGS="-Dwarnings"` применяется ко *всему* — включая build-скрипты и +процедурные макросы. Предупреждение в build.rs крейта `serde_derive` роняет ваш CI. -**Fix**: Use `CARGO_ENCODED_RUSTFLAGS` which only applies to the top-level -crate: +**Решение**: используйте `CARGO_ENCODED_RUSTFLAGS`, который действует только на верхнеуровневый крейт: ```bash -# BAD — breaks on third-party build script warnings +# ПЛОХО — ломается из-за предупреждений build-скриптов сторонних крейтов RUSTFLAGS="-Dwarnings" cargo build -# GOOD — only affects your crate +# ХОРОШО — затрагивает только ваш крейт CARGO_ENCODED_RUSTFLAGS="-Dwarnings" cargo build -# ALSO GOOD — workspace lints (Cargo.toml) +# ТОЖЕ ХОРОШО — линты воркспейса (Cargo.toml) [workspace.lints.rust] warnings = "deny" ``` --- -### 8. Reproducible Builds with `SOURCE_DATE_EPOCH` +### 8. Воспроизводимые сборки с `SOURCE_DATE_EPOCH` -**Problem**: Embedding `chrono::Utc::now()` in `build.rs` makes builds -non-reproducible — every build produces a different binary hash. +**Проблема**: встраивание `chrono::Utc::now()` в `build.rs` делает сборки невоспроизводимыми: +каждая сборка даёт другой хеш бинарника. -**Fix**: Honor `SOURCE_DATE_EPOCH`: +**Решение**: учитывайте `SOURCE_DATE_EPOCH`: ```rust // build.rs @@ -184,49 +181,50 @@ let timestamp = std::env::var("SOURCE_DATE_EPOCH") println!("cargo:rustc-env=BUILD_TIMESTAMP={timestamp}"); ``` -> See [Build Scripts](ch01-build-scripts-buildrs-in-depth.md) for the full build.rs patterns. +> См. [Build-скрипты](ch01-build-scripts-buildrs-in-depth.md) для полных паттернов build.rs. --- -### 9. The `cargo tree` Deduplication Workflow +### 9. Workflow дедупликации `cargo tree` -**Problem**: `cargo tree --duplicates` shows 5 versions of `syn` and 3 of -`tokio-util`. Compile time is painful. +**Проблема**: `cargo tree --duplicates` показывает 5 версий `syn` и 3 версии `tokio-util`. +Время компиляции мучительно большое. -**Fix**: Systematic deduplication: +**Решение**: систематическая дедупликация: ```bash -# Step 1: Find duplicates +# Шаг 1: находим дубликаты cargo tree --duplicates -# Step 2: Find who pulls the old version +# Шаг 2: находим, кто тянет старую версию cargo tree --invert --package syn@1.0.109 -# Step 3: Update the culprit -cargo update -p serde_derive # Might pull in syn 2.x +# Шаг 3: обновляем виновника +cargo update -p serde_derive # Может подтянуть syn 2.x -# Step 4: If no update available, pin in [patch] +# Шаг 4: если обновления нет, фиксируем через [patch] # [patch.crates-io] # old-crate = { git = "...", branch = "syn2-migration" } -# Step 5: Verify -cargo tree --duplicates # Should be shorter +# Шаг 5: проверяем +cargo tree --duplicates # Должно стать короче ``` -> See [Dependency Management](ch06-dependency-management-and-supply-chain-s.md) for `cargo-deny` and supply chain security. +> См. [Управление зависимостями](ch06-dependency-management-and-supply-chain-s.md) для `cargo-deny` +> и безопасности цепочки поставок. --- -### 10. Pre-Push Smoke Test +### 10. Дымовой тест перед push -**Problem**: You push, CI takes 10 minutes, fails on a formatting issue. +**Проблема**: вы делаете push, CI работает 10 минут и падает из-за проблемы с форматированием. -**Fix**: Run the fast checks locally before push: +**Решение**: запускайте быстрые проверки локально перед push: ```toml # Makefile.toml (cargo-make) [tasks.pre-push] -description = "Local smoke test before pushing" +description = "Локальный дымовой тест перед push" script = ''' cargo fmt --all -- --check cargo clippy --workspace --all-targets -- -D warnings @@ -235,11 +233,11 @@ cargo test --workspace --lib ``` ```bash -cargo make pre-push # < 30 seconds +cargo make pre-push # < 30 секунд git push ``` -Or use a git pre-push hook: +Или используйте git-хук pre-push: ```bash #!/bin/sh @@ -247,73 +245,77 @@ Or use a git pre-push hook: cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings ``` -> See [CI/CD Pipeline](ch11-putting-it-all-together-a-production-cic.md) for `Makefile.toml` patterns. +> См. [Собираем всё вместе](ch11-putting-it-all-together-a-production-cic.md) для паттернов `Makefile.toml`. --- -### 🏋️ Exercises +### 🏋️ Упражнения -#### 🟢 Exercise 1: Apply Three Tricks +#### 🟢 Упражнение 1: примените три приёма -Pick three tricks from this chapter and apply them to an existing Rust project. Which had the biggest impact? +Выберите три приёма из этой главы и примените их к существующему проекту на Rust. Какой +оказал наибольший эффект?
-Solution +Решение -Typical high-impact combination: +Типичная комбинация с наибольшим эффектом: -1. **`[profile.dev.package."*"] opt-level = 2`** — Immediate improvement in dev-mode runtime (2-10× faster for parsing-heavy code) +1. **`[profile.dev.package."*"] opt-level = 2`** — немедленное улучшение скорости выполнения + в dev-режиме (в 2–10× быстрее для кода, который много парсит) -2. **`CARGO_ENCODED_RUSTFLAGS`** — Eliminates false CI failures from third-party warnings +2. **`CARGO_ENCODED_RUSTFLAGS`** — устраняет ложные падения CI из-за предупреждений сторонних крейтов -3. **`cargo-hack --each-feature`** — Usually finds at least one broken feature combination in any project with 3+ features +3. **`cargo-hack --each-feature`** — обычно находит хотя бы одну нерабочую комбинацию фич + в любом проекте с 3+ фичами ```bash -# Apply trick 5: +# Применяем приём 5: echo '[profile.dev.package."*"]' >> Cargo.toml echo 'opt-level = 2' >> Cargo.toml -# Apply trick 7 in CI: -# Replace RUSTFLAGS with CARGO_ENCODED_RUSTFLAGS +# Применяем приём 7 в CI: +# Заменяем RUSTFLAGS на CARGO_ENCODED_RUSTFLAGS -# Apply trick 3: +# Применяем приём 3: cargo install cargo-hack cargo hack check --each-feature --no-dev-deps ```
-#### 🟡 Exercise 2: Deduplicate Your Dependency Tree +#### 🟡 Упражнение 2: устраните дубликаты в дереве зависимостей -Run `cargo tree --duplicates` on a real project. Eliminate at least one duplicate. Measure compile-time before and after. +Запустите `cargo tree --duplicates` на реальном проекте. Устраните хотя бы один дубликат. +Замерьте время компиляции до и после.
-Solution +Решение ```bash -# Before +# До time cargo build --release 2>&1 | tail -1 -cargo tree --duplicates | wc -l # Count duplicate lines +cargo tree --duplicates | wc -l # Считаем строки с дубликатами -# Find and fix one duplicate +# Находим и исправляем один дубликат cargo tree --duplicates cargo tree --invert --package @ cargo update -p -# After +# После time cargo build --release 2>&1 | tail -1 -cargo tree --duplicates | wc -l # Should be fewer +cargo tree --duplicates | wc -l # Должно стать меньше -# Typical result: 5-15% compile time reduction per eliminated -# duplicate (especially for heavy crates like syn, tokio) +# Типичный результат: снижение времени компиляции на 5–15% за каждый устранённый +# дубликат (особенно для тяжёлых крейтов вроде syn, tokio) ```
-### Key Takeaways +### Ключевые выводы -- Use `CARGO_ENCODED_RUSTFLAGS` instead of `RUSTFLAGS` to avoid breaking third-party build scripts -- `[profile.dev.package."*"] opt-level = 2` is the single highest-impact dev experience trick -- Cache tuning (`save-if` on main only) prevents CI cache bloat on active repositories -- `cargo tree --duplicates` + `cargo update` is a free compile-time win — do it monthly -- Run fast checks locally with `cargo make pre-push` to avoid CI round-trip waste +- Используйте `CARGO_ENCODED_RUSTFLAGS` вместо `RUSTFLAGS`, чтобы не ломать build-скрипты сторонних крейтов +- `[profile.dev.package."*"] opt-level = 2` — самый эффективный приём для улучшения опыта разработки +- Настройка кеша (`save-if` только для main) предотвращает раздувание кеша CI в активных репозиториях +- `cargo tree --duplicates` + `cargo update` — бесплатный выигрыш во времени компиляции: делайте это раз в месяц +- Запускайте быстрые проверки локально через `cargo make pre-push`, чтобы не тратить время на круги через CI --- diff --git a/engineering-book/src/ch13-quick-reference-card.md b/engineering-book/src/ch13-quick-reference-card.md index ec698bb..4d1f20c 100644 --- a/engineering-book/src/ch13-quick-reference-card.md +++ b/engineering-book/src/ch13-quick-reference-card.md @@ -1,185 +1,182 @@ -# Quick Reference Card +# Краткая справочная карточка -### Cheat Sheet: Commands at a Glance +### Шпаргалка: команды под рукой ```bash -# ─── Build Scripts ─── -cargo build # Compiles build.rs first, then crate -cargo build -vv # Verbose — shows build.rs output +# ─── Build-скрипты ─── +cargo build # Сначала компилирует build.rs, затем крейт +cargo build -vv # Подробный режим — показывает вывод build.rs -# ─── Cross-Compilation ─── +# ─── Кросс-компиляция ─── rustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17 cross build --release --target aarch64-unknown-linux-gnu -# ─── Benchmarking ─── -cargo bench # Run all benchmarks -cargo bench -- parse # Run benchmarks matching "parse" -cargo flamegraph -- --args # Generate flamegraph from binary -perf record -g ./target/release/bin # Record perf data -perf report # View perf data interactively +# ─── Бенчмаркинг ─── +cargo bench # Запустить все бенчмарки +cargo bench -- parse # Запустить бенчмарки, в названии которых есть «parse» +cargo flamegraph -- --args # Построить флеймграф по бинарнику +perf record -g ./target/release/bin # Записать данные perf +perf report # Просмотреть данные perf в интерактивном режиме -# ─── Coverage ─── -cargo llvm-cov --html # HTML report +# ─── Покрытие ─── +cargo llvm-cov --html # HTML-отчёт cargo llvm-cov --lcov --output-path lcov.info cargo llvm-cov --workspace --fail-under-lines 80 -cargo tarpaulin --out Html # Alternative tool +cargo tarpaulin --out Html # Альтернативный инструмент -# ─── Safety Verification ─── -cargo +nightly miri test # Run tests under Miri +# ─── Проверка безопасности ─── +cargo +nightly miri test # Запустить тесты под Miri MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test valgrind --leak-check=full ./target/debug/binary RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu -# ─── Audit & Supply Chain ─── -cargo audit # Known vulnerability scan -cargo audit --deny warnings # Fail CI on any advisory -cargo deny check # License + advisory + ban + source checks -cargo deny list # List all licenses in dep tree -cargo vet # Supply chain trust verification -cargo outdated --workspace # Find outdated dependencies -cargo semver-checks # Detect breaking API changes -cargo geiger # Count unsafe in dependency tree - -# ─── Binary Optimization ─── -cargo bloat --release --crates # Size contribution per crate -cargo bloat --release -n 20 # 20 largest functions -cargo +nightly udeps --workspace # Find unused dependencies -cargo machete # Fast unused dep detection -cargo expand --lib module::name # See macro expansions -cargo msrv find # Discover minimum Rust version -cargo clippy --fix --workspace --allow-dirty # Auto-fix lint warnings - -# ─── Compile-Time Optimization ─── -export RUSTC_WRAPPER=sccache # Shared compilation cache -sccache --show-stats # Cache hit statistics -cargo nextest run # Faster test runner -cargo nextest run --retries 2 # Retry flaky tests - -# ─── Platform Engineering ─── -cargo check --target thumbv7em-none-eabihf # Verify no_std builds -cargo build --target x86_64-pc-windows-gnu # Cross-compile to Windows -cargo xwin build --target x86_64-pc-windows-msvc # MSVC ABI cross-compile -cfg!(target_os = "linux") # Compile-time cfg (evaluates to bool) - -# ─── Release ─── -cargo release patch --dry-run # Preview release -cargo release patch --execute # Bump, commit, tag, publish -cargo dist plan # Preview distribution artifacts +# ─── Аудит и цепочка поставок ─── +cargo audit # Поиск известных уязвимостей +cargo audit --deny warnings # Падать в CI при любой рекомендации +cargo deny check # Проверки лицензий, рекомендаций, запретов и источников +cargo deny list # Список всех лицензий в дереве зависимостей +cargo vet # Проверка доверия к цепочке поставок +cargo outdated --workspace # Поиск устаревших зависимостей +cargo semver-checks # Обнаружение несовместимых изменений API +cargo geiger # Подсчёт unsafe в дереве зависимостей + +# ─── Оптимизация бинарника ─── +cargo bloat --release --crates # Вклад каждого крейта в размер +cargo bloat --release -n 20 # 20 самых больших функций +cargo +nightly udeps --workspace # Поиск неиспользуемых зависимостей +cargo machete # Быстрый поиск неиспользуемых зависимостей +cargo expand --lib module::name # Посмотреть раскрытия макросов +cargo msrv find # Определить минимальную версию Rust +cargo clippy --fix --workspace --allow-dirty # Автоисправление предупреждений линтеров + +# ─── Оптимизация времени компиляции ─── +export RUSTC_WRAPPER=sccache # Общий кеш компиляции +sccache --show-stats # Статистика попаданий в кеш +cargo nextest run # Более быстрый запускатель тестов +cargo nextest run --retries 2 # Повторять нестабильные тесты + +# ─── Платформенная инженерия ─── +cargo check --target thumbv7em-none-eabihf # Проверить сборку no_std +cargo build --target x86_64-pc-windows-gnu # Кросс-компиляция под Windows +cargo xwin build --target x86_64-pc-windows-msvc # Кросс-компиляция с ABI MSVC +cfg!(target_os = "linux") # cfg времени компиляции (даёт bool) + +# ─── Релиз ─── +cargo release patch --dry-run # Предварительный просмотр релиза +cargo release patch --execute # Повысить версию, закоммитить, поставить тег, опубликовать +cargo dist plan # Предварительный просмотр артефактов дистрибуции ``` -### Decision Table: Which Tool When - -| Goal | Tool | When to Use | -|------|------|-------------| -| Embed git hash / build info | `build.rs` | Binary needs traceability | -| Compile C code with Rust | `cc` crate in `build.rs` | FFI to small C libraries | -| Generate code from schemas | `prost-build` / `tonic-build` | Protobuf, gRPC, FlatBuffers | -| Link system library | `pkg-config` in `build.rs` | OpenSSL, libpci, systemd | -| Static Linux binary | `--target x86_64-unknown-linux-musl` | Container/cloud deployment | -| Target old glibc | `cargo-zigbuild` | RHEL 7, CentOS 7 compatibility | -| ARM server binary | `cross` or `cargo-zigbuild` | Graviton/Ampere deployment | -| Statistical benchmarks | Criterion.rs | Performance regression detection | -| Quick perf check | Divan | Development-time profiling | -| Find hot spots | `cargo flamegraph` / `perf` | After benchmark identifies slow code | -| Line/branch coverage | `cargo-llvm-cov` | CI coverage gates, gap analysis | -| Quick coverage check | `cargo-tarpaulin` | Local development | -| Rust UB detection | Miri | Pure-Rust `unsafe` code | -| C FFI memory safety | Valgrind memcheck | Mixed Rust/C codebases | -| Data race detection | TSan or Miri | Concurrent `unsafe` code | -| Buffer overflow detection | ASan | `unsafe` pointer arithmetic | -| Leak detection | Valgrind or LSan | Long-running services | -| Local CI equivalent | `cargo-make` | Developer workflow automation | -| Pre-commit checks | `cargo-husky` or git hooks | Catch issues before push | -| Automated releases | `cargo-release` + `cargo-dist` | Version management + distribution | -| Dependency auditing | `cargo-audit` / `cargo-deny` | Supply chain security | -| License compliance | `cargo-deny` (licenses) | Commercial / enterprise projects | -| Supply chain trust | `cargo-vet` | High-security environments | -| Find outdated deps | `cargo-outdated` | Scheduled maintenance | -| Detect breaking changes | `cargo-semver-checks` | Library crate publishing | -| Dependency tree analysis | `cargo tree --duplicates` | Dedup and trim dep graph | -| Binary size analysis | `cargo-bloat` | Size-constrained deployments | -| Find unused deps | `cargo-udeps` / `cargo-machete` | Trim compile time and size | -| LTO tuning | `lto = true` or `"thin"` | Release binary optimization | -| Size-optimized binary | `opt-level = "z"` + `strip = true` | Embedded / WASM / containers | -| Unsafe usage audit | `cargo-geiger` | Security policy enforcement | -| Macro debugging | `cargo-expand` | Derive / macro_rules debugging | -| Faster linking | `mold` linker | Developer inner loop | -| Compilation cache | `sccache` | CI and local build speed | -| Faster tests | `cargo-nextest` | CI and local test speed | -| MSRV compliance | `cargo-msrv` | Library publishing | -| `no_std` library | `#![no_std]` + `default-features = false` | Embedded, UEFI, WASM | -| Windows cross-compile | `cargo-xwin` / MinGW | Linux → Windows builds | -| Platform abstraction | `#[cfg]` + trait pattern | Multi-OS codebases | -| Windows API calls | `windows-sys` / `windows` crate | Native Windows functionality | -| End-to-end timing | `hyperfine` | Whole-binary benchmarks, before/after comparison | -| Property-based testing | `proptest` | Edge case discovery, parser robustness | -| Snapshot testing | `insta` | Large structured output verification | -| Coverage-guided fuzzing | `cargo-fuzz` | Crash discovery in parsers | -| Concurrency model checking | `loom` | Lock-free data structures, atomic ordering | -| Feature combination testing | `cargo-hack` | Crates with multiple `#[cfg]` features | -| Fast UB checks (near-native) | `cargo-careful` | CI safety gate, lighter than Miri | -| Auto-rebuild on save | `cargo-watch` | Developer inner loop, tight feedback | -| Workspace documentation | `cargo doc` + rustdoc | API discovery, onboarding, doc-link CI | -| Reproducible builds | `--locked` + `SOURCE_DATE_EPOCH` | Release integrity verification | -| CI cache tuning | `Swatinem/rust-cache@v2` | Build time reduction (cold → cached) | -| Workspace lint policy | `[workspace.lints]` in Cargo.toml | Consistent Clippy/compiler lints across all crates | -| Auto-fix lint warnings | `cargo clippy --fix` | Automated cleanup of trivial issues | - -### Further Reading - -| Topic | Resource | -|-------|----------| -| Cargo build scripts | [Cargo Book — Build Scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) | -| Cross-compilation | [Rust Cross-Compilation](https://rust-lang.github.io/rustup/cross-compilation.html) | -| `cross` tool | [cross-rs/cross](https://github.com/cross-rs/cross) | -| `cargo-zigbuild` | [cargo-zigbuild docs](https://github.com/rust-cross/cargo-zigbuild) | -| Criterion.rs | [Criterion User Guide](https://bheisler.github.io/criterion.rs/book/) | -| Divan | [Divan docs](https://github.com/nvzqz/divan) | +### Таблица решений: какой инструмент когда + +| Задача | Инструмент | Когда использовать | +|--------|------------|--------------------| +| Встроить хеш git / информацию о сборке | `build.rs` | Бинарнику нужна прослеживаемость | +| Скомпилировать C-код из Rust | крейт `cc` в `build.rs` | FFI к небольшим C-библиотекам | +| Сгенерировать код из схем | `prost-build` / `tonic-build` | Protobuf, gRPC, FlatBuffers | +| Слинковать системную библиотеку | `pkg-config` в `build.rs` | OpenSSL, libpci, systemd | +| Статический бинарник для Linux | `--target x86_64-unknown-linux-musl` | Развёртывание в контейнерах и облаке | +| Собрать под старую glibc | `cargo-zigbuild` | Совместимость с RHEL 7, CentOS 7 | +| Бинарник для ARM-сервера | `cross` или `cargo-zigbuild` | Развёртывание на Graviton/Ampere | +| Статистические бенчмарки | Criterion.rs | Обнаружение регрессий производительности | +| Быстрая проверка производительности | Divan | Профилирование во время разработки | +| Поиск горячих точек | `cargo flamegraph` / `perf` | Когда бенчмарк уже показал медленный код | +| Покрытие строк и ветвлений | `cargo-llvm-cov` | Пороги покрытия в CI, анализ пробелов | +| Быстрая проверка покрытия | `cargo-tarpaulin` | Локальная разработка | +| Обнаружение UB в Rust | Miri | Чистый Rust `unsafe`-код | +| Безопасность памяти при FFI с C | Valgrind memcheck | Смешанные кодовые базы Rust/C | +| Обнаружение гонок данных | TSan или Miri | Конкурентный `unsafe`-код | +| Обнаружение переполнения буфера | ASan | `unsafe` арифметика указателей | +| Обнаружение утечек | Valgrind или LSan | Долго работающие сервисы | +| Локальный аналог CI | `cargo-make` | Автоматизация рабочих сценариев разработчика | +| Проверки перед коммитом | `cargo-husky` или git-хуки | Ловить проблемы до push | +| Автоматические релизы | `cargo-release` + `cargo-dist` | Управление версиями и дистрибуция | +| Аудит зависимостей | `cargo-audit` / `cargo-deny` | Безопасность цепочки поставок | +| Соответствие лицензиям | `cargo-deny` (лицензии) | Коммерческие и корпоративные проекты | +| Доверие к цепочке поставок | `cargo-vet` | Среды с высокими требованиями безопасности | +| Поиск устаревших зависимостей | `cargo-outdated` | Плановое обслуживание | +| Обнаружение несовместимых изменений | `cargo-semver-checks` | Публикация библиотечных крейтов | +| Анализ дерева зависимостей | `cargo tree --duplicates` | Дедупликация и сокращение графа зависимостей | +| Анализ размера бинарника | `cargo-bloat` | Развёртывания с ограничением по размеру | +| Поиск неиспользуемых зависимостей | `cargo-udeps` / `cargo-machete` | Сокращение времени компиляции и размера | +| Настройка LTO | `lto = true` или `"thin"` | Оптимизация релизного бинарника | +| Бинарник, оптимизированный по размеру | `opt-level = "z"` + `strip = true` | Встраиваемые системы / WASM / контейнеры | +| Аудит использования unsafe | `cargo-geiger` | Соблюдение политики безопасности | +| Отладка макросов | `cargo-expand` | Отладка derive и `macro_rules` | +| Более быстрая линковка | линкер `mold` | Внутренний цикл разработчика | +| Кеш компиляции | `sccache` | Скорость CI и локальных сборок | +| Более быстрые тесты | `cargo-nextest` | Скорость тестов в CI и локально | +| Соответствие MSRV | `cargo-msrv` | Публикация библиотек | +| Библиотека `no_std` | `#![no_std]` + `default-features = false` | Встраиваемые системы, UEFI, WASM | +| Кросс-компиляция под Windows | `cargo-xwin` / MinGW | Сборки Linux → Windows | +| Платформенная абстракция | `#[cfg]` + паттерн с трейтами | Кодовые базы для нескольких ОС | +| Вызовы API Windows | `windows-sys` / крейт `windows` | Нативная функциональность Windows | +| Сквозной замер времени | `hyperfine` | Бенчмарки всего бинарника, сравнение до и после | +| Property-based тестирование | `proptest` | Поиск крайних случаев, устойчивость парсеров | +| Snapshot-тестирование | `insta` | Проверка больших структурированных выводов | +| Фаззинг с обратной связью по покрытию | `cargo-fuzz` | Поиск падений в парсерах | +| Проверка моделей конкурентности | `loom` | Структуры данных без блокировок, порядок атомарных операций | +| Тестирование комбинаций фич | `cargo-hack` | Крейты с несколькими фичами `#[cfg]` | +| Быстрые проверки UB (почти нативная скорость) | `cargo-careful` | Ворота безопасности в CI, легче Miri | +| Автопересборка при сохранении | `cargo-watch` | Внутренний цикл разработчика, быстрая обратная связь | +| Документация воркспейса | `cargo doc` + rustdoc | Поиск API, онбординг, проверка ссылок в CI | +| Воспроизводимые сборки | `--locked` + `SOURCE_DATE_EPOCH` | Проверка целостности релиза | +| Настройка кеша CI | `Swatinem/rust-cache@v2` | Сокращение времени сборки (с холодного старта → из кеша) | +| Политика линтов воркспейса | `[workspace.lints]` в Cargo.toml | Единые линты Clippy и компилятора во всех крейтах | +| Автоисправление предупреждений линтеров | `cargo clippy --fix` | Автоматическая очистка тривиальных проблем | + +### Дополнительная литература + +| Тема | Ресурс | +|------|--------| +| Build-скрипты Cargo | [Книга Cargo — Build Scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html) | +| Кросс-компиляция | [Кросс-компиляция в Rust](https://rust-lang.github.io/rustup/cross-compilation.html) | +| Инструмент `cross` | [cross-rs/cross](https://github.com/cross-rs/cross) | +| `cargo-zigbuild` | [документация cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild) | +| Criterion.rs | [Руководство по Criterion](https://bheisler.github.io/criterion.rs/book/) | +| Divan | [документация Divan](https://github.com/nvzqz/divan) | | `cargo-llvm-cov` | [cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) | -| `cargo-tarpaulin` | [tarpaulin docs](https://github.com/xd009642/tarpaulin) | -| Miri | [Miri GitHub](https://github.com/rust-lang/miri) | -| Sanitizers in Rust | [rustc Sanitizer docs](https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/sanitizer.html) | -| `cargo-make` | [cargo-make book](https://sagiegurari.github.io/cargo-make/) | -| `cargo-release` | [cargo-release docs](https://github.com/crate-ci/cargo-release) | -| `cargo-dist` | [cargo-dist docs](https://axodotdev.github.io/cargo-dist/book/) | -| Profile-guided optimization | [Rust PGO guide](https://doc.rust-lang.org/rustc/profile-guided-optimization.html) | -| Flamegraphs | [cargo-flamegraph](https://github.com/flamegraph-rs/flamegraph) | -| `cargo-deny` | [cargo-deny docs](https://embarkstudios.github.io/cargo-deny/) | -| `cargo-vet` | [cargo-vet docs](https://mozilla.github.io/cargo-vet/) | +| `cargo-tarpaulin` | [документация tarpaulin](https://github.com/xd009642/tarpaulin) | +| Miri | [Miri на GitHub](https://github.com/rust-lang/miri) | +| Санитайзеры в Rust | [документация rustc по санитайзерам](https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/sanitizer.html) | +| `cargo-make` | [книга cargo-make](https://sagiegurari.github.io/cargo-make/) | +| `cargo-release` | [документация cargo-release](https://github.com/crate-ci/cargo-release) | +| `cargo-dist` | [книга cargo-dist](https://axodotdev.github.io/cargo-dist/book/) | +| Оптимизация по профилю (PGO) | [Руководство Rust по PGO](https://doc.rust-lang.org/rustc/profile-guided-optimization.html) | +| Флеймграфы | [cargo-flamegraph](https://github.com/flamegraph-rs/flamegraph) | +| `cargo-deny` | [документация cargo-deny](https://embarkstudios.github.io/cargo-deny/) | +| `cargo-vet` | [документация cargo-vet](https://mozilla.github.io/cargo-vet/) | | `cargo-audit` | [cargo-audit](https://github.com/rustsec/rustsec/tree/main/cargo-audit) | | `cargo-bloat` | [cargo-bloat](https://github.com/RazrFalcon/cargo-bloat) | | `cargo-udeps` | [cargo-udeps](https://github.com/est31/cargo-udeps) | | `cargo-geiger` | [cargo-geiger](https://github.com/geiger-rs/cargo-geiger) | | `cargo-semver-checks` | [cargo-semver-checks](https://github.com/obi1kenobi/cargo-semver-checks) | -| `cargo-nextest` | [nextest docs](https://nexte.st/) | +| `cargo-nextest` | [документация nextest](https://nexte.st/) | | `sccache` | [sccache](https://github.com/mozilla/sccache) | -| `mold` linker | [mold](https://github.com/rui314/mold) | +| Линкер `mold` | [mold](https://github.com/rui314/mold) | | `cargo-msrv` | [cargo-msrv](https://github.com/foresterre/cargo-msrv) | -| LTO | [rustc Codegen Options](https://doc.rust-lang.org/rustc/codegen-options/index.html) | -| Cargo Profiles | [Cargo Book — Profiles](https://doc.rust-lang.org/cargo/reference/profiles.html) | -| `no_std` | [Rust Embedded Book](https://docs.rust-embedded.org/book/) | -| `windows-sys` crate | [windows-rs](https://github.com/microsoft/windows-rs) | -| `cargo-xwin` | [cargo-xwin docs](https://github.com/rust-cross/cargo-xwin) | +| LTO | [Опции генерации кода rustc](https://doc.rust-lang.org/rustc/codegen-options/index.html) | +| Профили Cargo | [Книга Cargo — Profiles](https://doc.rust-lang.org/cargo/reference/profiles.html) | +| `no_std` | [Embedded Book для Rust](https://docs.rust-embedded.org/book/) | +| Крейт `windows-sys` | [windows-rs](https://github.com/microsoft/windows-rs) | +| `cargo-xwin` | [документация cargo-xwin](https://github.com/rust-cross/cargo-xwin) | | `cargo-hack` | [cargo-hack](https://github.com/taiki-e/cargo-hack) | | `cargo-careful` | [cargo-careful](https://github.com/RalfJung/cargo-careful) | | `cargo-watch` | [cargo-watch](https://github.com/watchexec/cargo-watch) | -| Rust CI cache | [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache) | -| Rustdoc book | [Rustdoc Book](https://doc.rust-lang.org/rustdoc/) | -| Conditional compilation | [Rust Reference — cfg](https://doc.rust-lang.org/reference/conditional-compilation.html) | -| Embedded Rust | [Awesome Embedded Rust](https://github.com/rust-embedded/awesome-embedded-rust) | +| Кеш Rust в CI | [Swatinem/rust-cache](https://github.com/Swatinem/rust-cache) | +| Книга по rustdoc | [Rustdoc Book](https://doc.rust-lang.org/rustdoc/) | +| Условная компиляция | [Справочник Rust — cfg](https://doc.rust-lang.org/reference/conditional-compilation.html) | +| Встраиваемый Rust | [Awesome Embedded Rust](https://github.com/rust-embedded/awesome-embedded-rust) | | `hyperfine` | [hyperfine](https://github.com/sharkdp/hyperfine) | | `proptest` | [proptest](https://github.com/proptest-rs/proptest) | -| `insta` | [insta snapshot testing](https://insta.rs/) | +| `insta` | [insta — snapshot-тестирование](https://insta.rs/) | | `cargo-fuzz` | [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) | -| `loom` | [loom concurrency testing](https://github.com/tokio-rs/loom) | +| `loom` | [loom — тестирование конкурентности](https://github.com/tokio-rs/loom) | --- -*Generated as a companion reference — a companion to Rust Patterns and -Type-Driven Correctness.* +*Создано как сопроводительный справочник к книгам «Паттерны Rust» и «Корректность на уровне типов».* -*Version 1.3 — Added cargo-hack, cargo-careful, cargo-watch, cargo doc, -reproducible builds, CI caching strategies, capstone exercise, and chapter -dependency diagram for completeness.* +*Версия 1.3 — добавлены cargo-hack, cargo-careful, cargo-watch, cargo doc, воспроизводимые сборки, стратегии кеширования CI, итоговое упражнение и диаграмма зависимостей между главами для полноты.* From 448306d4b5c3340cd4c6fcf12e43eafdb7f3ce66 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:52:48 +0400 Subject: [PATCH 07/18] Translate Async Rust book intro, TOC and chapters 1-9 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings in async-book/src for SUMMARY.md, ch00 through ch09. Code identifiers, crate names, file names and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- async-book/src/SUMMARY.md | 46 +-- async-book/src/ch00-introduction.md | 131 +++++---- .../ch01-why-async-is-different-in-rust.md | 180 ++++++------ async-book/src/ch02-the-future-trait.md | 144 +++++---- async-book/src/ch03-how-poll-works.md | 158 +++++----- async-book/src/ch04-pin-and-unpin.md | 170 ++++++----- .../src/ch05-the-state-machine-reveal.md | 111 ++++--- .../src/ch06-building-futures-by-hand.md | 138 +++++---- async-book/src/ch07-executors-and-runtimes.md | 274 +++++++++--------- async-book/src/ch08-tokio-deep-dive.md | 220 +++++++------- .../src/ch09-when-tokio-isnt-the-right-fit.md | 142 +++++---- 11 files changed, 848 insertions(+), 866 deletions(-) diff --git a/async-book/src/SUMMARY.md b/async-book/src/SUMMARY.md index ef7cf4f..6427873 100644 --- a/async-book/src/SUMMARY.md +++ b/async-book/src/SUMMARY.md @@ -1,40 +1,40 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I: How Async Works +# Часть I: Как работает async -- [1. Why Async is Different in Rust](ch01-why-async-is-different-in-rust.md) -- [2. The Future Trait](ch02-the-future-trait.md) -- [3. How Poll Works](ch03-how-poll-works.md) -- [4. Pin and Unpin](ch04-pin-and-unpin.md) -- [5. The State Machine Reveal](ch05-the-state-machine-reveal.md) +- [1. Почему async в Rust устроен иначе](ch01-why-async-is-different-in-rust.md) +- [2. Трейт Future](ch02-the-future-trait.md) +- [3. Как работает poll](ch03-how-poll-works.md) +- [4. Pin и Unpin](ch04-pin-and-unpin.md) +- [5. Раскрываем конечный автомат](ch05-the-state-machine-reveal.md) --- -# Part II: The Ecosystem +# Часть II: Экосистема -- [6. Building Futures by Hand](ch06-building-futures-by-hand.md) -- [7. Executors and Runtimes](ch07-executors-and-runtimes.md) -- [8. Tokio Deep Dive](ch08-tokio-deep-dive.md) -- [9. When Tokio Isn't the Right Fit](ch09-when-tokio-isnt-the-right-fit.md) -- [10. Async Traits](ch10-async-traits.md) +- [6. Создаём фьючи вручную](ch06-building-futures-by-hand.md) +- [7. Исполнители и рантаймы](ch07-executors-and-runtimes.md) +- [8. Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) +- [9. Когда Tokio не подходит](ch09-when-tokio-isnt-the-right-fit.md) +- [10. Async-трейты](ch10-async-traits.md) --- -# Part III: Production Async +# Часть III: Async в продакшене -- [11. Streams and AsyncIterator](ch11-streams-and-asynciterator.md) -- [12. Common Pitfalls](ch12-common-pitfalls.md) -- [13. Production Patterns](ch13-production-patterns.md) -- [14. Async Is an Optimization, Not an Architecture](ch14-async-is-an-optimization-not-an-architecture.md) -- [15. Exercises](ch15-exercises.md) +- [11. Потоки и AsyncIterator](ch11-streams-and-asynciterator.md) +- [12. Типичные ловушки](ch12-common-pitfalls.md) +- [13. Продакшен-паттерны](ch13-production-patterns.md) +- [14. Async — это оптимизация, а не архитектура](ch14-async-is-an-optimization-not-an-architecture.md) +- [15. Упражнения](ch15-exercises.md) --- -# Appendices +# Приложения -- [Summary and Reference Card](ch16-summary-and-reference-card.md) -- [Capstone Project: Async Chat Server](ch17-capstone-project.md) +- [Итоги и справочная карточка](ch16-summary-and-reference-card.md) +- [Итоговый проект: асинхронный чат-сервер](ch17-capstone-project.md) diff --git a/async-book/src/ch00-introduction.md b/async-book/src/ch00-introduction.md index 17a3337..10098a2 100644 --- a/async-book/src/ch00-introduction.md +++ b/async-book/src/ch00-introduction.md @@ -1,96 +1,95 @@ -# Async Rust: From Futures to Production +# Async Rust: от Future до продакшена -## Speaker Intro +## Об авторе -- Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team -- Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems -- Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since +- Главный архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft +- Опытный специалист в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также систем на C++ +- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык --- -A deep-dive guide to asynchronous programming in Rust. Unlike most async tutorials that start with `tokio::main` and hand-wave the internals, this guide builds understanding from first principles — the `Future` trait, polling, state machines — then progresses to real-world patterns, runtime selection, and production pitfalls. +Подробное руководство по асинхронному программированию в Rust. В отличие от большинства туториалов по async, которые начинаются с `tokio::main` и обходят внутреннее устройство стороной, эта книга строит понимание с первых принципов: трейт `Future`, опрос (polling), конечные автоматы — и только потом переходит к практическим паттернам, выбору рантайма и продакшен-ловушкам. -## Who This Is For -- Rust developers who can write synchronous Rust but find async confusing -- Developers from C#, Go, Python, or JavaScript who know `async/await` but not Rust's model -- Anyone who's been bitten by `Future is not Send`, `Pin>`, or "why does my program hang?" +## Для кого эта книга -## Prerequisites +- Разработчики на Rust, которые уверенно пишут синхронный код, но путаются в async +- Разработчики из C#, Go, Python или JavaScript, которые знают `async/await`, но не знают модели Rust +- Все, кто натыкался на `Future is not Send`, `Pin>` или вопрос «почему программа зависла?» -You should be comfortable with: -- Ownership, borrowing, and lifetimes -- Traits and generics (including `impl Trait`) -- Using `Result` and the `?` operator -- Basic multi-threading (`std::thread::spawn`, `Arc`, `Mutex`) +## Предварительные требования -No prior async Rust experience is needed. +Вы должны уверенно владеть: +- Владением, заимствованием и временами жизни (lifetimes) +- Трейтами и обобщёнными типами (включая `impl Trait`) +- Типом `Result` и оператором `?` +- Базовой многопоточностью (`std::thread::spawn`, `Arc`, `Mutex`) -## How to Use This Book +Предыдущий опыт асинхронного Rust не нужен. -**Read linearly the first time.** Parts I–III build on each other. Each chapter has: +## Как пользоваться этой книгой -| Symbol | Meaning | -|--------|---------| -| 🟢 | Beginner — foundational concept | -| 🟡 | Intermediate — requires earlier chapters | -| 🔴 | Advanced — deep internals or production patterns | +**При первом чтении идите по порядку.** Части I–III опираются друг на друга. Каждая глава содержит: -Each chapter includes: -- A **"What you'll learn"** block at the top -- **Mermaid diagrams** for visual learners -- An **inline exercise** with a hidden solution -- **Key Takeaways** summarizing the core ideas -- **Cross-references** to related chapters +| Символ | Значение | +|--------|----------| +| 🟢 | Начальный уровень — базовое понятие | +| 🟡 | Средний уровень — требует знания предыдущих глав | +| 🔴 | Продвинутый уровень — глубокие внутренние механизмы или продакшен-паттерны | -## Pacing Guide +В каждой главе есть: +- Блок **«Что вы узнаете»** в начале +- **Диаграммы Mermaid** для тех, кто лучше воспринимает визуально +- **Встроенное упражнение** со скрытым решением +- **Ключевые выводы**, которые суммируют основные идеи +- **Перекрёстные ссылки** на связанные главы -| Chapters | Topic | Suggested Time | Checkpoint | -|----------|-------|----------------|------------| -| 1–5 | How Async Works | 6–8 hours | You can explain `Future`, `Poll`, `Pin`, and why Rust has no built-in runtime | -| 6–10 | The Ecosystem | 6–8 hours | You can build futures by hand, choose a runtime, and use tokio's API | -| 11–13 | Production Async | 6–8 hours | You can write production-grade async code with streams, proper error handling, and graceful shutdown | -| Capstone | Chat Server | 4–6 hours | You've built a real async application integrating all concepts | +## План изучения -**Total estimated time: 22–30 hours** +| Главы | Тема | Примерное время | Контрольная точка | +|-------|------|-----------------|-------------------| +| 1–5 | Как работает async | 6–8 ч | Вы можете объяснить `Future`, `Poll`, `Pin` и то, почему в Rust нет встроенного рантайма | +| 6–10 | Экосистема | 6–8 ч | Вы можете написать фьючи вручную, выбрать рантайм и использовать API tokio | +| 11–13 | Async в продакшене | 6–8 ч | Вы можете писать продакшен-код с потоками, корректной обработкой ошибок и graceful shutdown | +| Итог | Чат-сервер | 4–6 ч | Вы создали реальное асинхронное приложение, объединяющее все концепции | -## Working Through Exercises +**Общее расчётное время: 22–30 часов** -Every content chapter has an inline exercise. The capstone (Ch 16) integrates everything into a single project. For maximum learning: +## Работа с упражнениями -1. **Try the exercise before expanding the solution** — struggling is where learning happens -2. **Type the code, don't copy-paste** — muscle memory matters for Rust's syntax -3. **Run every example** — `cargo new async-exercises` and test as you go +Каждая содержательная глава содержит встроенное упражнение. Итоговый проект (гл. 16) объединяет всё в одном приложении. Для максимальной пользы: -## Table of Contents +1. **Сначала решите упражнение, и только потом раскрывайте решение** — именно в процессе борьбы с задачей и происходит обучение +2. **Набирайте код, а не копируйте его** — для синтаксиса Rust важна мышечная память +3. **Запускайте каждый пример** — `cargo new async-exercises` и проверяйте по ходу чтения -### Part I: How Async Works +## Содержание -- [1. Why Async is Different in Rust](ch01-why-async-is-different-in-rust.md) 🟢 — The fundamental difference: Rust has no built-in runtime -- [2. The Future Trait](ch02-the-future-trait.md) 🟡 — `poll()`, `Waker`, and the contract that makes it all work -- [3. How Poll Works](ch03-how-poll-works.md) 🟡 — The polling state machine and a minimal executor -- [4. Pin and Unpin](ch04-pin-and-unpin.md) 🔴 — Why self-referential structs need pinning -- [5. The State Machine Reveal](ch05-the-state-machine-reveal.md) 🟢 — What the compiler actually generates from `async fn` +### Часть I: Как работает async -### Part II: The Ecosystem +- [1. Почему async в Rust устроен иначе](ch01-why-async-is-different-in-rust.md) 🟢 — Фундаментальное отличие: в Rust нет встроенного рантайма +- [2. Трейт Future](ch02-the-future-trait.md) 🟡 — `poll()`, `Waker` и контракт, на котором всё держится +- [3. Как работает poll](ch03-how-poll-works.md) 🟡 — Конечный автомат опроса и минимальный исполнитель +- [4. Pin и Unpin](ch04-pin-and-unpin.md) 🔴 — Зачем самоссылающимся структурам нужен pinning +- [5. Раскрываем конечный автомат](ch05-the-state-machine-reveal.md) 🟢 — Что компилятор на самом деле генерирует из `async fn` -- [6. Building Futures by Hand](ch06-building-futures-by-hand.md) 🟡 — TimerFuture, Join, Select from scratch -- [7. Executors and Runtimes](ch07-executors-and-runtimes.md) 🟡 — tokio, smol, async-std, embassy — how to choose -- [8. Tokio Deep Dive](ch08-tokio-deep-dive.md) 🟡 — Runtime flavors, spawn, channels, sync primitives -- [9. When Tokio Isn't the Right Fit](ch09-when-tokio-isnt-the-right-fit.md) 🟡 — LocalSet, FuturesUnordered, runtime-agnostic design -- [10. Async Traits](ch10-async-traits.md) 🟡 — RPITIT, dyn dispatch, trait_variant, async closures +### Часть II: Экосистема -### Part III: Production Async +- [6. Создаём фьючи вручную](ch06-building-futures-by-hand.md) 🟡 — TimerFuture, Join, Select с нуля +- [7. Исполнители и рантаймы](ch07-executors-and-runtimes.md) 🟡 — tokio, smol, async-std, embassy — как выбрать +- [8. Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) 🟡 — Варианты рантайма, spawn, каналы, примитивы синхронизации +- [9. Когда Tokio не подходит](ch09-when-tokio-isnt-the-right-fit.md) 🟡 — LocalSet, FuturesUnordered, дизайн, не привязанный к рантайму +- [10. Async-трейты](ch10-async-traits.md) 🟡 — RPITIT, динамическая диспетчеризация, trait_variant, async-замыкания -- [11. Streams and AsyncIterator](ch11-streams-and-asynciterator.md) 🟡 — Async iteration, AsyncRead/Write, stream combinators -- [12. Common Pitfalls](ch12-common-pitfalls.md) 🔴 — 9 production bugs and how to avoid them -- [13. Production Patterns](ch13-production-patterns.md) 🔴 — Graceful shutdown, backpressure, Tower middleware -- [14. Async Is an Optimization, Not an Architecture](ch14-async-is-an-optimization-not-an-architecture.md) 🔴 — Sync core / async shell, the function coloring tax +### Часть III: Async в продакшене -### Appendices +- [11. Потоки и AsyncIterator](ch11-streams-and-asynciterator.md) 🟡 — Асинхронная итерация, AsyncRead/Write, комбинаторы потоков +- [12. Типичные ловушки](ch12-common-pitfalls.md) 🔴 — 9 продакшен-багов и как их избежать +- [13. Продакшен-паттерны](ch13-production-patterns.md) 🔴 — Graceful shutdown, обратное давление, middleware Tower +- [14. Async — это оптимизация, а не архитектура](ch14-async-is-an-optimization-not-an-architecture.md) 🔴 — Синхронное ядро / асинхронная оболочка, налог на раскраску функций -- [Summary and Reference Card](ch16-summary-and-reference-card.md) — Quick-lookup tables and decision trees -- [Capstone Project: Async Chat Server](ch17-capstone-project.md) — Build a complete async application - -*** +### Приложения +- [Итоги и справочная карточка](ch16-summary-and-reference-card.md) — Таблицы для быстрого поиска и деревья решений +- [Итоговый проект: асинхронный чат-сервер](ch17-capstone-project.md) — Создайте полноценное асинхронное приложение +*** diff --git a/async-book/src/ch01-why-async-is-different-in-rust.md b/async-book/src/ch01-why-async-is-different-in-rust.md index 1ced1dd..d9d38a5 100644 --- a/async-book/src/ch01-why-async-is-different-in-rust.md +++ b/async-book/src/ch01-why-async-is-different-in-rust.md @@ -1,38 +1,38 @@ -# 1. Why Async is Different in Rust 🟢 +# 1. Почему async в Rust устроен иначе 🟢 -> **What you'll learn:** -> - Why Rust has no built-in async runtime (and what that means for you) -> - The three key properties: lazy execution, no runtime, zero-cost abstraction -> - When async is the right tool (and when it's slower) -> - How Rust's model compares to C#, Go, Python, and JavaScript +> **Что вы узнаете:** +> - Почему в Rust нет встроенного асинхронного рантайма (и что это значит для вас) +> - Три ключевых свойства: ленивое выполнение, отсутствие рантайма, абстракции с нулевой стоимостью +> - Когда async — правильный инструмент (а когда он медленнее) +> - Как модель Rust соотносится с C#, Go, Python и JavaScript -## The Fundamental Difference +## Фундаментальное отличие -Most languages with `async/await` hide the machinery. C# has the CLR thread pool. JavaScript has the event loop. Go has goroutines and a scheduler built into the runtime. Python has `asyncio`. +Большинство языков с `async/await` скрывают внутренний механизм. В C# есть пул потоков CLR. В JavaScript есть цикл событий (event loop). В Go есть горутины и планировщик, встроенный в рантайм. В Python есть `asyncio`. -**Rust has nothing.** +**В Rust ничего этого нет.** -There is no built-in runtime, no thread pool, no event loop. The `async` keyword is a zero-cost compilation strategy — it transforms your function into a state machine that implements the `Future` trait. Someone else (an *executor*) must drive that state machine forward. +Нет встроенного рантайма, нет пула потоков, нет цикла событий. Ключевое слово `async` — это стратегия компиляции с нулевой стоимостью: она превращает вашу функцию в конечный автомат, реализующий трейт `Future`. Кто-то другой (*исполнитель*, executor) должен продвигать этот автомат вперёд. -### Three Key Properties of Rust Async +### Три ключевых свойства async в Rust ```mermaid graph LR subgraph "C# / JS / Go" - EAGER["Eager Execution
Task starts immediately"] - BUILTIN["Built-in Runtime
Thread pool included"] - GC["GC-Managed
No lifetime concerns"] + EAGER["Жадное выполнение
Задача стартует сразу"] + BUILTIN["Встроенный рантайм
Пул потоков в комплекте"] + GC["Управляется GC
Никаких забот о временах жизни"] end - subgraph "Rust (and Python*)" - LAZY["Lazy Execution
Nothing happens until polled/awaited"] - BYOB["Bring Your Own Runtime
You choose the executor"] - OWNED["Ownership Applies
Lifetimes, Send, Sync matter"] + subgraph "Rust (и Python*)" + LAZY["Ленивое выполнение
Ничего не происходит, пока не вызван poll/await"] + BYOB["Свой рантайм
Исполнитель выбираете вы"] + OWNED["Действует владение
Важны времена жизни, Send, Sync"] end - EAGER -. "opposite" .-> LAZY - BUILTIN -. "opposite" .-> BYOB - GC -. "opposite" .-> OWNED + EAGER -. "противоположность" .-> LAZY + BUILTIN -. "противоположность" .-> BYOB + GC -. "противоположность" .-> OWNED style LAZY fill:#e8f5e8,color:#000 style BYOB fill:#e8f5e8,color:#000 @@ -42,78 +42,78 @@ graph LR style GC fill:#e3f2fd,color:#000 ``` -> \* Python coroutines are lazy like Rust futures — they don't execute until awaited or scheduled. However, Python still uses GC and has no ownership/lifetime concerns. +> \* Корутины Python, как и фьючи Rust, ленивые — они не выполняются, пока их не `await`-ят или не запланируют. Однако Python по-прежнему использует GC и не знает о владении и временах жизни. -### No Built-In Runtime +### Нет встроенного рантайма ```rust -// This compiles but does NOTHING: +// Это компилируется, но НИЧЕГО не делает: async fn fetch_data() -> String { "hello".to_string() } fn main() { - let future = fetch_data(); // Creates the Future, but doesn't execute it - // future is just a struct sitting on the stack - // No output, no side effects, nothing happens - drop(future); // Silently dropped — work was never started + let future = fetch_data(); // Создаёт Future, но не выполняет его + // future — это просто структура на стеке + // Никакого вывода, никаких побочных эффектов, ничего не происходит + drop(future); // Молча уничтожена — работа так и не начиналась } ``` -Compare with C# where `Task` starts eagerly: +Сравните с C#, где `Task` стартует сразу: ```csharp -// C# — this immediately starts executing: +// C# — это немедленно начинает выполняться: async Task FetchData() => "hello"; -var task = FetchData(); // Already running! -var result = await task; // Just waits for completion +var task = FetchData(); // Уже выполняется! +var result = await task; // Только ждём завершения ``` -### Lazy Futures vs Eager Tasks +### Ленивые фьючи против жадных задач -This is the single most important mental shift: +Это самая важная смена мышления: | | C# / JavaScript | Python | Go | Rust | |---|---|---|---|---| -| **Creation** | `Task` starts executing immediately | Coroutine is **lazy** — returns an object, doesn't run until awaited or scheduled | Goroutine starts immediately | `Future` does nothing until polled | -| **Dropping** | Detached task continues running | Unawaited coroutine is garbage-collected (with a warning) | Goroutine runs until return | Dropping a Future cancels it | -| **Runtime** | Built into the language/VM | `asyncio` event loop (must be explicitly started) | Built into the binary (M:N scheduler) | You choose (tokio, smol, etc.) | -| **Scheduling** | Automatic (thread pool) | Event loop + `await` or `create_task()` | Automatic (GMP scheduler) | Explicit (`spawn`, `block_on`) | -| **Cancellation** | `CancellationToken` (cooperative) | `Task.cancel()` (cooperative, raises `CancelledError`) | `context.Context` (cooperative) | Drop the future (immediate) | +| **Создание** | `Task` начинает выполняться сразу | Корутина **ленивая** — возвращает объект, не выполняется, пока её не `await`-ят или не запланируют | Горутина стартует сразу | `Future` ничего не делает, пока его не опросили (polled) | +| **Уничтожение** | Отсоединённая задача продолжает работать | Невызванная корутина собирается GC (с предупреждением) | Горутина работает до возврата | Уничтожение Future отменяет его | +| **Рантайм** | Встроен в язык / VM | Цикл событий `asyncio` (нужно запускать явно) | Встроен в бинарник (планировщик M:N) | Выбираете вы (tokio, smol и др.) | +| **Планирование** | Автоматическое (пул потоков) | Цикл событий + `await` или `create_task()` | Автоматическое (планировщик GMP) | Явное (`spawn`, `block_on`) | +| **Отмена** | `CancellationToken` (кооперативная) | `Task.cancel()` (кооперативная, выбрасывает `CancelledError`) | `context.Context` (кооперативная) | Уничтожить фьючу (мгновенно) | ```rust -// To actually RUN a future, you need an executor: +// Чтобы РЕАЛЬНО выполнить фьючу, нужен исполнитель: #[tokio::main] async fn main() { - let result = fetch_data().await; // NOW it executes + let result = fetch_data().await; // ТЕПЕРЬ оно выполняется println!("{result}"); } ``` -### When to Use Async (and When Not To) +### Когда использовать async (и когда не стоит) ```mermaid graph TD - START["What kind of work?"] - - IO["I/O-bound?
(network, files, DB)"] - CPU["CPU-bound?
(computation, parsing)"] - MANY["Many concurrent connections?
(100+)"] - FEW["Few concurrent tasks?
(<10)"] - - USE_ASYNC["✅ Use async/await"] - USE_THREADS["✅ Use std::thread or rayon"] - USE_SPAWN_BLOCKING["✅ Use spawn_blocking()"] - MAYBE_SYNC["Consider synchronous code
(simpler, less overhead)"] - - START -->|Network, files, DB| IO - START -->|Computation| CPU - IO -->|Yes, many| MANY - IO -->|Just a few| FEW + START["Какой характер работы?"] + + IO["Ограничено вводом-выводом?
(сеть, файлы, БД)"] + CPU["Ограничено процессором?
(вычисления, парсинг)"] + MANY["Много одновременных соединений?
(100+)"] + FEW["Мало одновременных задач?
(<10)"] + + USE_ASYNC["✅ Используйте async/await"] + USE_THREADS["✅ Используйте std::thread или rayon"] + USE_SPAWN_BLOCKING["✅ Используйте spawn_blocking()"] + MAYBE_SYNC["Рассмотрите синхронный код
(проще, меньше накладных расходов)"] + + START -->|Сеть, файлы, БД| IO + START -->|Вычисления| CPU + IO -->|Да, много| MANY + IO -->|Всего несколько| FEW MANY --> USE_ASYNC FEW --> MAYBE_SYNC - CPU -->|Parallelize| USE_THREADS - CPU -->|Inside async context| USE_SPAWN_BLOCKING + CPU -->|Распараллелить| USE_THREADS + CPU -->|Внутри async-контекста| USE_SPAWN_BLOCKING style USE_ASYNC fill:#c8e6c9,color:#000 style USE_THREADS fill:#c8e6c9,color:#000 @@ -121,53 +121,51 @@ graph TD style MAYBE_SYNC fill:#fff3e0,color:#000 ``` -**Rule of thumb**: Async is for I/O concurrency (doing many things at once while waiting), not CPU parallelism (making one thing faster). If you have 10,000 network connections, async shines. If you're crunching numbers, use `rayon` or OS threads. +**Практическое правило**: async нужен для конкурентности ввода-вывода (делать много дел одновременно, пока ждём), а не для параллелизма вычислений (ускорение одной операции). Если у вас 10 000 сетевых соединений, async проявляет себя во всей красе. Если вы считаете числа, используйте `rayon` или потоки ОС. -### When Async Can Be *Slower* +### Когда async может быть *медленнее* -Async isn't free. For low-concurrency workloads, synchronous code can outperform async: +Async не бесплатен. При низкой конкурентности синхронный код может работать быстрее async: -| Cost | Why | -|------|-----| -| **State machine overhead** | Each `.await` adds an enum variant; deeply nested futures produce large, complex state machines | -| **Dynamic dispatch** | `Box` adds indirection and kills inlining | -| **Context switching** | Cooperative scheduling still has cost — the executor must manage a task queue, wakers, and I/O registrations | -| **Compile time** | Async code generates more complex types, slowing down compilation | -| **Debuggability** | Stack traces through state machines are harder to read (see Ch. 12) | +| Издержка | Почему | +|----------|--------| +| **Накладные расходы конечного автомата** | Каждый `.await` добавляет вариант enum; глубоко вложенные фьючи порождают большие и сложные автоматы | +| **Динамическая диспетчеризация** | `Box` добавляет косвенность и мешает инлайнингу | +| **Переключение контекста** | Кооперативное планирование тоже стоит ресурсов — исполнителю нужно управлять очередью задач, wakers и регистрациями I/O | +| **Время компиляции** | Async-код порождает более сложные типы и замедляет компиляцию | +| **Отладка** | Трассировки стека через конечные автоматы труднее читать (см. гл. 12) | -**Benchmarking guidance**: If fewer than ~10 concurrent I/O operations, profile before committing to async. A simple `std::thread::spawn` per connection scales fine to hundreds of threads on modern Linux. +**Рекомендация по бенчмаркам**: если одновременных операций ввода-вывода меньше ~10, сначала профилируйте, и только потом переходите на async. Простой `std::thread::spawn` на соединение хорошо масштабируется до сотен потоков на современном Linux. -### Exercise: When Would You Use Async? +### Упражнение: когда использовать async?
-🏋️ Exercise (click to expand) +🏋️ Упражнение (нажмите, чтобы раскрыть) -For each scenario, decide whether async is appropriate and explain why: +Для каждого сценария решите, уместен ли async, и объясните почему: -1. A web server handling 10,000 concurrent WebSocket connections -2. A CLI tool that compresses a single large file -3. A service that queries 5 different databases and merges results -4. A game engine running a physics simulation at 60 FPS +1. Веб-сервер, обрабатывающий 10 000 одновременных WebSocket-соединений +2. CLI-утилита, которая сжимает один большой файл +3. Сервис, который опрашивает 5 разных баз данных и объединяет результаты +4. Игровой движок, который выполняет физическую симуляцию с 60 FPS
-🔑 Solution +🔑 Решение -1. **Async** — I/O-bound with massive concurrency. Each connection spends most time waiting for data. Threads would require 10K stacks. -2. **Sync/threads** — CPU-bound, single task. Async adds overhead with no benefit. Use `rayon` for parallel compression. -3. **Async** — Five concurrent I/O waits. `tokio::join!` runs all five queries simultaneously. -4. **Sync/threads** — CPU-bound, latency-sensitive. Async's cooperative scheduling could introduce frame jitter. +1. **Async** — ограничено вводом-выводом, огромная конкурентность. Каждое соединение большую часть времени ждёт данных. Для потоков понадобилось бы 10 000 стеков. +2. **Синхронно / потоки** — ограничено процессором, одна задача. Async добавляет накладные расходы без пользы. Для параллельного сжатия используйте `rayon`. +3. **Async** — пять одновременных ожиданий I/O. `tokio::join!` выполняет все пять запросов одновременно. +4. **Синхронно / потоки** — ограничено процессором, чувствительно к задержкам. Кооперативное планирование async может вызвать джиттер кадров.
-> **Key Takeaways — Why Async is Different** -> - Rust futures are **lazy** — they do nothing until polled by an executor -> - There is **no built-in runtime** — you choose (or build) your own -> - Async is a **zero-cost compilation strategy** that produces state machines -> - Async shines for **I/O-bound concurrency**; for CPU-bound work, use threads or rayon +> **Ключевые выводы — почему async устроен иначе** +> - Фьючи в Rust **ленивые** — они ничего не делают, пока исполнитель их не опросит +> - Встроенного рантайма **нет** — вы выбираете (или создаёте) свой +> - Async — это **стратегия компиляции с нулевой стоимостью**, которая порождает конечные автоматы +> - Async проявляет себя в **конкурентности ввода-вывода**; для вычислений используйте потоки или rayon -> **See also:** [Ch 2 — The Future Trait](ch02-the-future-trait.md) for the trait that makes this all work, [Ch 7 — Executors and Runtimes](ch07-executors-and-runtimes.md) for choosing your runtime +> **См. также:** [Гл. 2 — Трейт Future](ch02-the-future-trait.md) — трейт, на котором всё это держится, [Гл. 7 — Исполнители и рантаймы](ch07-executors-and-runtimes.md) — как выбрать рантайм *** - - diff --git a/async-book/src/ch02-the-future-trait.md b/async-book/src/ch02-the-future-trait.md index 62858ed..bad14d2 100644 --- a/async-book/src/ch02-the-future-trait.md +++ b/async-book/src/ch02-the-future-trait.md @@ -1,14 +1,14 @@ -# 2. The Future Trait 🟡 +# 2. Трейт Future 🟡 -> **What you'll learn:** -> - The `Future` trait: `Output`, `poll()`, `Context`, `Waker` -> - How a waker tells the executor "poll me again" -> - The contract: never call `wake()` = your program silently hangs -> - Implementing a real future by hand (`Delay`) +> **Что вы узнаете:** +> - Трейт `Future`: `Output`, `poll()`, `Context`, `Waker` +> - Как waker сообщает исполнителю «опроси меня снова» +> - Контракт: если никогда не вызвать `wake()`, программа молча зависнет +> - Реализация настоящего фьючи вручную (`Delay`) -## Anatomy of a Future +## Анатомия Future -Everything in async Rust ultimately implements this trait: +Всё в асинхронном Rust в конечном счёте реализует этот трейт: ```rust pub trait Future { @@ -18,76 +18,76 @@ pub trait Future { } pub enum Poll { - Ready(T), // The future has completed with value T - Pending, // The future is not ready yet — call me back later + Ready(T), // Future завершился со значением T + Pending, // Future ещё не готов — вызови меня позже } ``` -That's it. A `Future` is anything that can be *polled* — asked "are you done yet?" — and responds with either "yes, here's the result" or "not yet, I'll wake you up when I'm ready." +Вот и всё. `Future` — это всё, что можно *опросить* (polled): у него спрашивают «ты уже готов?», и он отвечает либо «да, вот результат», либо «пока нет, я разбужу тебя, когда буду готов». ### Output, poll(), Context, Waker ```mermaid sequenceDiagram - participant E as Executor - participant F as Future (Task) - participant OS as Operating System
(e.g., epoll/kqueue) - participant R as Reactor (Runtime) - - E->>F: Calls poll(cx) - Note right of F: Future attempts operation - F->>OS: Syscall (e.g., read TCP socket) - OS-->>F: Returns Error: Not Ready + participant E as Исполнитель (Executor) + participant F as Future (Задача) + participant OS as Операционная система
(например, epoll/kqueue) + participant R as Реактор (Runtime) + + E->>F: Вызывает poll(cx) + Note right of F: Future пытается выполнить операцию + F->>OS: Системный вызов (например, read из TCP-сокета) + OS-->>F: Возвращает ошибку: не готово - F->>R: Registers: (Waker) - F-->>E: Returns Poll::Pending - Note left of E: Task is moved out
of run queue + F->>R: Регистрирует: (Waker) + F-->>E: Возвращает Poll::Pending + Note left of E: Задача убирается
из очереди на выполнение - E->>E: (Executor runs other tasks OR sleeps) - R->>OS: epoll_wait() / Polls OS for events + E->>E: (Исполнитель выполняет другие задачи ИЛИ засыпает) + R->>OS: epoll_wait() / опрашивает ОС на события - Note right of OS: (Sometime Later) New data arrives - OS-->>R: Wakes Reactor: data is NOW READY + Note right of OS: (Спустя некоторое время) приходят новые данные + OS-->>R: Будит реактор: данные УЖЕ ГОТОВЫ - R->>R: Reactor finds Waker - R->>E: Calls Waker::wake() - Note right of E: Task is pushed back
to Executor's run queue - - E->>F: Calls poll(cx) again - Note right of F: Future attempts operation again - F->>OS: Syscall (e.g., read TCP socket) - OS-->>F: Success: Returns Data Buffer - F-->>E: Returns Poll::Ready(Data) + R->>R: Реактор находит Waker + R->>E: Вызывает Waker::wake() + Note right of E: Задача возвращается
в очередь исполнителя + + E->>F: Снова вызывает poll(cx) + Note right of F: Future снова пытается выполнить операцию + F->>OS: Системный вызов (например, read из TCP-сокета) + OS-->>F: Успех: возвращает буфер с данными + F-->>E: Возвращает Poll::Ready(Data) ``` -Let's break down each piece: +Разберём каждую часть: ```rust use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -// A future that returns 42 immediately +// Future, который сразу возвращает 42 struct Ready42; impl Future for Ready42 { - type Output = i32; // What the future eventually produces + type Output = i32; // Что в итоге вернёт future fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { - Poll::Ready(42) // Always ready — no waiting + Poll::Ready(42) // Всегда готов — ожидание не нужно } } ``` -**The components**: -- **`Output`** — the type of value produced when the future completes -- **`poll()`** — called by the executor to check progress; returns `Ready(value)` or `Pending` -- **`Pin<&mut Self>`** — ensures the future won't be moved in memory (we'll cover why in Ch. 4) -- **`Context`** — carries the `Waker` so the future can signal the executor when it's ready to make progress +**Компоненты**: +- **`Output`** — тип значения, которое получится по завершении future +- **`poll()`** — вызывается исполнителем, чтобы проверить прогресс; возвращает `Ready(value)` или `Pending` +- **`Pin<&mut Self>`** — гарантирует, что future не переместят в памяти (почему это важно, разберём в гл. 4) +- **`Context`** — содержит `Waker`, чтобы future мог сообщить исполнителю, что готов двигаться дальше -### The Waker Contract +### Контракт Waker -The `Waker` is the callback mechanism. When a future returns `Pending`, it *must* arrange for `waker.wake()` to be called later — otherwise the executor will never poll it again and the program hangs. +`Waker` — это механизм обратного вызова. Когда future возвращает `Pending`, он *обязан* обеспечить последующий вызов `waker.wake()` — иначе исполнитель никогда не опросит его снова, и программа зависнет. ```rust use std::task::{Context, Poll, Waker}; @@ -97,7 +97,7 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; -/// A future that completes after a delay (toy implementation) +/// Future, который завершается через заданное время (упрощённая реализация) struct Delay { completed: Arc>, waker_stored: Arc>>, @@ -120,15 +120,15 @@ impl Future for Delay { type Output = (); fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { - // Check if already completed before storing waker + // Проверяем, не завершился ли уже, до того как сохранить waker if *self.completed.lock().unwrap() { return Poll::Ready(()); } - // Store the waker - executor may pass a new one on each poll + // Сохраняем waker — исполнитель может передавать новый при каждом poll *self.waker_stored.lock().unwrap() = Some(cx.waker().clone()); - // Start the background timer on first poll + // Запускаем фоновый таймер при первом poll if !self.started { self.started = true; let completed = Arc::clone(&self.completed); @@ -139,38 +139,38 @@ impl Future for Delay { thread::sleep(duration); *completed.lock().unwrap() = true; - // CRITICAL: wake the executor so it polls us again + // ВАЖНО: будим исполнитель, чтобы он снова опросил нас if let Some(w) = waker.lock().unwrap().take() { - w.wake(); // "Hey executor, I'm ready — poll me again!" + w.wake(); // «Эй, исполнитель, я готов — опроси меня снова!» } }); } - // Double-check completion after storing waker (handles race condition) + // Перепроверяем завершение после сохранения waker (защита от гонки) if *self.completed.lock().unwrap() { return Poll::Ready(()); } - Poll::Pending // Not done yet + Poll::Pending // Ещё не готово } } ``` -> **Key insight**: In C#, the TaskScheduler handles waking automatically. -> In Rust, **you** (or the I/O library you use) are responsible for calling -> `waker.wake()`. Forget it, and your program silently hangs. +> **Ключевая мысль**: в C# за пробуждение отвечает TaskScheduler автоматически. +> В Rust **вы** (или библиотека ввода-вывода, которую вы используете) отвечаете за вызов +> `waker.wake()`. Забудете — и программа молча зависнет. -### Exercise: Implement a CountdownFuture +### Упражнение: реализуйте CountdownFuture
-🏋️ Exercise (click to expand) +🏋️ Упражнение (нажмите, чтобы раскрыть) -**Challenge**: Implement a `CountdownFuture` that counts down from N to 0, printing the current count each time it's polled. When it reaches 0, it completes with `Ready("Liftoff!")`. +**Задача**: реализуйте `CountdownFuture`, который отсчитывает от N до 0 и выводит текущее значение при каждом опросе. Когда дойдёт до 0, он завершается с `Ready("Liftoff!")`. -*Hint*: The future needs to store the current count and decrement it on each poll. Remember to always re-register the waker! +*Подсказка*: future должен хранить текущее значение счётчика и уменьшать его при каждом poll. Не забывайте каждый раз заново регистрировать waker!
-🔑 Solution +🔑 Решение ```rust use std::future::Future; @@ -197,26 +197,24 @@ impl Future for CountdownFuture { } else { println!("{}...", self.count); self.count -= 1; - cx.waker().wake_by_ref(); // Schedule re-poll immediately + cx.waker().wake_by_ref(); // Немедленно планируем повторный poll Poll::Pending } } } ``` -**Key takeaway**: This future is polled once per count. Each time it returns `Pending`, it immediately wakes itself to be polled again. In production, you'd use a timer instead of busy-polling. +**Ключевой вывод**: этот future опрашивается по одному разу на каждое значение счётчика. Каждый раз, возвращая `Pending`, он сразу будит себя для повторного опроса. В продакшене вместо активного опроса (busy-polling) следует использовать таймер.
-> **Key Takeaways — The Future Trait** -> - `Future::poll()` returns `Poll::Ready(value)` or `Poll::Pending` -> - A future must register a `Waker` before returning `Pending` — the executor uses it to know when to re-poll -> - `Pin<&mut Self>` guarantees the future won't be moved in memory (needed for self-referential state machines — see Ch 4) -> - Everything in async Rust — `async fn`, `.await`, combinators — is built on this one trait +> **Ключевые выводы — трейт Future** +> - `Future::poll()` возвращает `Poll::Ready(value)` или `Poll::Pending` +> - Перед возвратом `Pending` future обязан зарегистрировать `Waker` — исполнитель использует его, чтобы понять, когда опрашивать снова +> - `Pin<&mut Self>` гарантирует, что future не переместят в памяти (нужно для самоссылающихся конечных автоматов — см. гл. 4) +> - Всё в асинхронном Rust — `async fn`, `.await`, комбинаторы — построено на этом одном трейте -> **See also:** [Ch 3 — How Poll Works](ch03-how-poll-works.md) for the executor loop, [Ch 6 — Building Futures by Hand](ch06-building-futures-by-hand.md) for more complex implementations +> **См. также:** [Гл. 3 — Как работает poll](ch03-how-poll-works.md) — цикл исполнителя, [Гл. 6 — Создаём фьючи вручную](ch06-building-futures-by-hand.md) — более сложные реализации *** - - diff --git a/async-book/src/ch03-how-poll-works.md b/async-book/src/ch03-how-poll-works.md index 8260929..6bc1680 100644 --- a/async-book/src/ch03-how-poll-works.md +++ b/async-book/src/ch03-how-poll-works.md @@ -1,45 +1,45 @@ -# 3. How Poll Works 🟡 +# 3. Как работает poll 🟡 -> **What you'll learn:** -> - The executor's poll loop: poll → pending → wake → poll again -> - How to build a minimal executor from scratch -> - Spurious wake rules and why they matter -> - Utility functions: `poll_fn()` and `yield_now()` +> **Что вы узнаете:** +> - Цикл опроса исполнителя: poll → Pending → wake → снова poll +> - Как собрать минимальный исполнитель с нуля +> - Правила «ложных» пробуждений и почему они важны +> - Вспомогательные функции: `poll_fn()` и `yield_now()` -## The Polling State Machine +## Конечный автомат опроса -The executor runs a loop: poll a future, if it's `Pending`, park it until its waker fires, then poll again. This is fundamentally different from OS threads where the kernel handles scheduling. +Исполнитель работает в цикле: опрашивает future, и если он возвращает `Pending`, откладывает его до срабатывания waker, а затем опрашивает снова. Это принципиально отличается от потоков ОС, где планированием занимается ядро. ```mermaid stateDiagram-v2 - [*] --> Idle : Future created - Idle --> Polling : executor calls poll() + [*] --> Idle : Future создан + Idle --> Polling : исполнитель вызывает poll() Polling --> Complete : Ready(value) Polling --> Waiting : Pending - Waiting --> Polling : waker.wake() called - Complete --> [*] : Value returned + Waiting --> Polling : вызван waker.wake() + Complete --> [*] : Значение возвращено ``` -> **Important:** While in the *Waiting* state the future **must** have registered -> the waker with an I/O source. No registration = hang forever. +> **Важно:** находясь в состоянии *Waiting*, future **обязан** был зарегистрировать +> waker у источника ввода-вывода. Без регистрации — зависание навсегда. -### A Minimal Executor +### Минимальный исполнитель -To demystify executors, let's build the simplest possible one: +Чтобы развеять магию исполнителей, построим самый простой из возможных: ```rust use std::future::Future; use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; use std::pin::Pin; -/// The simplest possible executor: busy-loop poll until Ready +/// Простейший исполнитель: крутим poll в цикле, пока не получим Ready fn block_on(mut future: F) -> F::Output { - // Pin the future on the stack - // SAFETY: `future` is never moved after this point — we only - // access it through the pinned reference until it completes. + // Закрепляем future на стеке + // SAFETY: `future` не перемещается после этого момента — мы обращаемся + // к нему только через закреплённую ссылку до самого завершения. let mut future = unsafe { Pin::new_unchecked(&mut future) }; - // Create a no-op waker (just keeps polling — inefficient but simple) + // Создаём пустой waker (он просто продолжает опрос — неэффективно, зато просто) fn noop_raw_waker() -> RawWaker { fn no_op(_: *const ()) {} fn clone(_: *const ()) -> RawWaker { noop_raw_waker() } @@ -47,24 +47,24 @@ fn block_on(mut future: F) -> F::Output { RawWaker::new(std::ptr::null(), vtable) } - // SAFETY: noop_raw_waker() returns a valid RawWaker with a correct vtable. + // SAFETY: noop_raw_waker() возвращает корректный RawWaker с правильной таблицей vtable. let waker = unsafe { Waker::from_raw(noop_raw_waker()) }; let mut cx = Context::from_waker(&waker); - // Busy-loop until the future completes + // Крутим цикл, пока future не завершится loop { match future.as_mut().poll(&mut cx) { Poll::Ready(value) => return value, Poll::Pending => { - // A real executor would park the thread here - // and wait for waker.wake() — we just spin + // Настоящий исполнитель усыпил бы поток здесь + // и ждал бы waker.wake() — а мы просто крутимся std::thread::yield_now(); } } } } -// Usage: +// Использование: fn main() { let result = block_on(async { println!("Hello from our mini executor!"); @@ -74,73 +74,73 @@ fn main() { } ``` -> **Don't use this in production!** It busy-loops, wasting CPU. Real executors -> (tokio, smol) use `epoll`/`kqueue`/`io_uring` to sleep until I/O is ready. -> But this shows the core idea: an executor is just a loop that calls `poll()`. +> **Не используйте это в продакшене!** Здесь активный цикл, он тратит CPU впустую. Настоящие исполнители +> (tokio, smol) используют `epoll`/`kqueue`/`io_uring`, чтобы спать до готовности ввода-вывода. +> Но этот пример показывает главную идею: исполнитель — это просто цикл, который вызывает `poll()`. -### Wake-Up Notifications +### Уведомления о пробуждении -A real executor is event-driven. When all futures are `Pending`, the executor sleeps. The waker is an interrupt mechanism: +Настоящий исполнитель управляется событиями. Когда все future находятся в `Pending`, исполнитель засыпает. Waker работает как механизм прерывания: ```rust -// Conceptual model of a real executor's main loop: +// Концептуальная модель главного цикла настоящего исполнителя: fn executor_loop(tasks: &mut TaskQueue) { loop { - // 1. Poll all tasks that have been woken + // 1. Опрашиваем все задачи, которые были разбужены while let Some(task) = tasks.get_woken_task() { match task.poll() { Poll::Ready(result) => task.complete(result), - Poll::Pending => { /* task stays in queue, waiting for wake */ } + Poll::Pending => { /* задача остаётся в очереди, ждём пробуждения */ } } } - // 2. Sleep until something wakes us up (epoll_wait, kevent, etc.) - // This is where mio/polling does the heavy lifting - tasks.wait_for_events(); // blocks until an I/O event or waker fires + // 2. Спим, пока что-нибудь нас не разбудит (epoll_wait, kevent и т. п.) + // Вот где основную работу делает mio/polling + tasks.wait_for_events(); // блокируется до события ввода-вывода или срабатывания waker } } ``` -### Spurious Wakes +### Ложные пробуждения -A future may be polled even when its I/O isn't ready. This is called a *spurious wake*. Futures must handle this correctly: +Future может быть опрошен, даже когда его ввод-вывод ещё не готов. Это называется *ложным пробуждением* (spurious wake). Future должен корректно с этим справляться: ```rust impl Future for MyFuture { type Output = Data; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // ✅ CORRECT: Always re-check the actual condition + // ✅ ПРАВИЛЬНО: всегда перепроверяем реальное условие if let Some(data) = self.try_read_data() { Poll::Ready(data) } else { - // Re-register the waker (it might have changed!) + // Регистрируем waker заново (он мог измениться!) self.register_waker(cx.waker()); Poll::Pending } - // ❌ WRONG: Assuming poll means data is ready - // let data = self.read_data(); // might block or panic + // ❌ НЕПРАВИЛЬНО: считать, что poll означает готовность данных + // let data = self.read_data(); // может заблокироваться или упасть с паникой // Poll::Ready(data) } } ``` -**Rules for implementing `poll()`**: -1. **Never block** — return `Pending` immediately if not ready -2. **Always re-register the waker** — it may have changed between polls -3. **Handle spurious wakes** — check the actual condition, don't assume readiness -4. **Don't poll after `Ready`** — behavior is **unspecified** (may panic, return `Pending`, or repeat `Ready`). Only `FusedFuture` guarantees safe post-completion polling +**Правила реализации `poll()`**: +1. **Никогда не блокируйтесь** — если данных нет, сразу возвращайте `Pending` +2. **Всегда регистрируйте waker заново** — он мог измениться между опросами +3. **Обрабатывайте ложные пробуждения** — проверяйте реальное условие, а не полагайтесь на готовность +4. **Не опрашивайте после `Ready`** — поведение **не определено** (может вызвать панику, вернуть `Pending` или снова `Ready`). Гарантированно безопасный опрос после завершения даёт только `FusedFuture`
-🏋️ Exercise: Spurious-Wake-Safe Flag Future (click to expand) +🏋️ Упражнение: FlagFuture, устойчивый к ложным пробуждениям (нажмите, чтобы раскрыть) -**Challenge**: Implement a `FlagFuture` that wraps a shared `Arc` flag. When polled, it checks whether the flag is `true`. If so, it completes with `Ready(())`. If not, it stores the waker and returns `Pending`. The twist: the future must handle **spurious wakes** correctly — it must re-check the flag on every poll, never assuming the flag is set just because it was woken. +**Задача**: реализуйте `FlagFuture`, который оборачивает общий флаг `Arc`. При опросе он проверяет, равен ли флаг `true`. Если да — завершается с `Ready(())`. Если нет — сохраняет waker и возвращает `Pending`. Тонкость: future должен корректно обрабатывать **ложные пробуждения** — он должен перепроверять флаг при каждом опросе и никогда не считать, что флаг установлен, только потому что его разбудили. -*Hint*: You'll need an `Arc>>` (or similar) so an external thread can set the flag and wake the future. Use `poll_fn` for a concise alternative solution. +*Подсказка*: понадобится `Arc>>` (или что-то похожее), чтобы внешний поток мог установить флаг и разбудить future. Для краткого альтернативного решения используйте `poll_fn`.
-🔑 Solution +🔑 Решение ```rust use std::future::Future; @@ -164,18 +164,18 @@ impl Future for FlagFuture { type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // Always re-check the actual condition — never trust the wake alone + // Всегда перепроверяем реальное условие — никогда не доверяем одному только пробуждению if self.flag.load(Ordering::Acquire) { return Poll::Ready(()); } - // Store/update the waker so we get notified + // Сохраняем/обновляем waker, чтобы нас уведомили let mut slot = self.waker_slot.lock().unwrap(); *slot = Some(cx.waker().clone()); - // Re-check after storing the waker to avoid a race: - // the flag could have been set between our first check - // and storing the waker + // Перепроверяем после сохранения waker, чтобы избежать гонки: + // флаг мог быть установлен между нашей первой проверкой + // и сохранением waker if self.flag.load(Ordering::Acquire) { Poll::Ready(()) } else { @@ -184,7 +184,7 @@ impl Future for FlagFuture { } } -// The setter side (e.g., another thread or task): +// Сторона, которая устанавливает флаг (например, другой поток или задача): fn set_flag(flag: &AtomicBool, waker_slot: &Mutex>) { flag.store(true, Ordering::Release); if let Some(waker) = waker_slot.lock().unwrap().take() { @@ -192,7 +192,7 @@ fn set_flag(flag: &AtomicBool, waker_slot: &Mutex>) { } } -// Equivalent using poll_fn: +// Эквивалент через poll_fn: // async fn wait_for_flag(flag: Arc, waker_slot: Arc>>) { // std::future::poll_fn(|cx| { // if flag.load(Ordering::Acquire) { @@ -204,39 +204,39 @@ fn set_flag(flag: &AtomicBool, waker_slot: &Mutex>) { // } ``` -**Key takeaway**: The double-check pattern (check → store waker → check again) is essential to avoid a race between the condition changing and the waker being registered. This is the real-world pattern that all I/O futures use internally, and it demonstrates why handling spurious wakes matters. +**Ключевой вывод**: шаблон «двойной проверки» (проверить → сохранить waker → проверить снова) необходим, чтобы избежать гонки между изменением условия и регистрацией waker. Именно так внутри работают все фьючи ввода-вывода, и это показывает, почему обработка ложных пробуждений так важна.
-### Handy Utilities: `poll_fn` and `yield_now` +### Полезные утилиты: `poll_fn` и `yield_now` -Two utilities from the standard library and tokio that avoid writing full `Future` impls: +Две утилиты из стандартной библиотеки и tokio, которые избавляют от написания полных реализаций `Future`: ```rust use std::future::poll_fn; use std::task::Poll; -// poll_fn: create a one-off future from a closure +// poll_fn: создаём разовый future из замыкания let value = poll_fn(|cx| { - // Do something with cx.waker(), return Ready or Pending + // Делаем что-нибудь с cx.waker(), возвращаем Ready или Pending Poll::Ready(42) }).await; -// Real-world use: bridge a callback-based API into async +// Пример из практики: адаптируем API на основе колбэков к async async fn read_when_ready(source: &MySource) -> Data { poll_fn(|cx| source.poll_read(cx)).await } ``` ```rust -// yield_now: voluntarily yield control to the executor -// Useful in CPU-heavy async loops to avoid starving other tasks +// yield_now: добровольно отдаём управление исполнителю +// Полезно в тяжёлых по CPU асинхронных циклах, чтобы не морить голодом другие задачи async fn cpu_heavy_work(items: &[Item]) { for (i, item) in items.iter().enumerate() { - process(item); // CPU work + process(item); // работа с CPU - // Every 100 items, yield to let other tasks run + // Каждые 100 элементов уступаем управление, чтобы могли выполниться другие задачи if i % 100 == 0 { tokio::task::yield_now().await; } @@ -244,18 +244,16 @@ async fn cpu_heavy_work(items: &[Item]) { } ``` -> **When to use `yield_now()`**: If your async function does CPU work in a loop -> without any `.await` points, it monopolizes the executor thread. Insert -> `yield_now().await` periodically to enable cooperative multitasking. +> **Когда использовать `yield_now()`**: если асинхронная функция выполняет работу с CPU в цикле +> без единой точки `.await`, она монополизирует поток исполнителя. Периодически вставляйте +> `yield_now().await`, чтобы включить кооперативную многозадачность. -> **Key Takeaways — How Poll Works** -> - An executor repeatedly calls `poll()` on futures that have been woken -> - Futures must handle **spurious wakes** — always re-check the actual condition -> - `poll_fn()` lets you create ad-hoc futures from closures -> - `yield_now()` is a cooperative scheduling escape hatch for CPU-heavy async code +> **Ключевые выводы — как работает poll** +> - Исполнитель многократно вызывает `poll()` для future, которые были разбужены +> - Future должны обрабатывать **ложные пробуждения** — всегда перепроверяйте реальное условие +> - `poll_fn()` позволяет создавать разовые future из замыканий +> - `yield_now()` — это лаз для кооперативного планирования в async-коде с тяжёлыми вычислениями -> **See also:** [Ch 2 — The Future Trait](ch02-the-future-trait.md) for the trait definition, [Ch 5 — The State Machine Reveal](ch05-the-state-machine-reveal.md) for what the compiler generates +> **См. также:** [Гл. 2 — Трейт Future](ch02-the-future-trait.md) — определение трейта, [Гл. 5 — Раскрываем конечный автомат](ch05-the-state-machine-reveal.md) — что генерирует компилятор *** - - diff --git a/async-book/src/ch04-pin-and-unpin.md b/async-book/src/ch04-pin-and-unpin.md index af25757..426cd6e 100644 --- a/async-book/src/ch04-pin-and-unpin.md +++ b/async-book/src/ch04-pin-and-unpin.md @@ -1,37 +1,37 @@ -# 4. Pin and Unpin 🔴 +# 4. Pin и Unpin 🔴 -> **What you'll learn:** -> - Why self-referential structs break when moved in memory -> - What `Pin

` guarantees and how it prevents moves -> - The three practical pinning patterns: `Box::pin()`, `tokio::pin!()`, `Pin::new()` -> - When `Unpin` gives you an escape hatch +> **Что вы узнаете:** +> - Почему самоссылающиеся структуры ломаются при перемещении в памяти +> - Что гарантирует `Pin

` и как он предотвращает перемещение +> - Три практических паттерна закрепления: `Box::pin()`, `tokio::pin!()`, `Pin::new()` +> - Когда `Unpin` даёт вам лазейку -## Why Pin Exists +## Зачем нужен Pin -This is the most confusing concept in async Rust. Let's build the intuition step by step. +Это самое запутанное понятие в асинхронном Rust. Построим интуицию шаг за шагом. -### The Problem: Self-Referential Structs +### Проблема: самоссылающиеся структуры -When the compiler transforms an `async fn` into a state machine, that state machine may contain references to its own fields. This creates a *self-referential struct* — and moving it in memory would invalidate those internal references. +Когда компилятор превращает `async fn` в конечный автомат, этот автомат может содержать ссылки на собственные поля. Так возникает *самоссылающаяся структура* — и её перемещение в памяти сделает внутренние ссылки недействительными. ```rust -// What the compiler generates (simplified) for: +// Что компилятор генерирует (упрощённо) для: // async fn example() { // let data = vec![1, 2, 3]; -// let reference = &data; // Points to data above +// let reference = &data; // Указывает на data выше // use_ref(reference).await; // } -// Becomes something like: +// Превращается во что-то вроде: enum ExampleStateMachine { State0 { data: Vec, - // reference: &Vec, // PROBLEM: points to `data` above - // // If this struct moves, the pointer is dangling! + // reference: &Vec, // ПРОБЛЕМА: указывает на `data` выше + // // Если структура переместится, указатель повиснет! }, State1 { data: Vec, - reference: *const Vec, // Internal pointer to data field + reference: *const Vec, // Внутренний указатель на поле data }, Complete, } @@ -39,16 +39,16 @@ enum ExampleStateMachine { ```mermaid graph LR - subgraph "Before Move (Valid)" - A["data: [1,2,3]
at addr 0x1000"] - B["reference: 0x1000
(points to data)"] - B -->|"valid"| A + subgraph "До перемещения (корректно)" + A["data: [1,2,3]
по адресу 0x1000"] + B["reference: 0x1000
(указывает на data)"] + B -->|"корректно"| A end - subgraph "After Move (INVALID)" - C["data: [1,2,3]
at addr 0x2000"] - D["reference: 0x1000
(still points to OLD location!)"] - D -->|"dangling!"| E["💥 0x1000
(freed/garbage)"] + subgraph "После перемещения (НЕКОРРЕКТНО)" + C["data: [1,2,3]
по адресу 0x2000"] + D["reference: 0x1000
(всё ещё указывает на СТАРОЕ место!)"] + D -->|"висячий указатель!"| E["💥 0x1000
(освобождено / мусор)"] end style E fill:#ffcdd2,color:#000 @@ -56,141 +56,139 @@ graph LR style B fill:#c8e6c9,color:#000 ``` -### Self-Referential Structs +### Самоссылающиеся структуры -This isn't an academic concern. Every `async fn` that holds a reference across an `.await` point creates a self-referential state machine: +Это не академическая проблема. Любая `async fn`, которая удерживает ссылку через точку `.await`, создаёт самоссылающийся конечный автомат: ```rust async fn problematic() { let data = String::from("hello"); - let slice = &data[..]; // slice borrows data - - some_io().await; // <-- .await point: state machine stores both data AND slice - - println!("{slice}"); // uses the reference after await + let slice = &data[..]; // slice заимствует data + + some_io().await; // <-- точка .await: автомат хранит и data, и slice + + println!("{slice}"); // использует ссылку после await } -// The generated state machine has `data: String` and `slice: &str` -// where slice points INTO data. Moving the state machine = dangling pointer. +// Сгенерированный автомат содержит `data: String` и `slice: &str`, +// где slice указывает ВНУТРЬ data. Перемещение автомата = висячий указатель. ``` -### Pin in Practice +### Pin на практике -`Pin

` is a wrapper that prevents moving the value behind the pointer: +`Pin

` — это обёртка, которая запрещает перемещать значение, на которое указывает указатель: ```rust use std::pin::Pin; let mut data = String::from("hello"); -// Pin it — now it can't be moved +// Закрепляем — теперь его нельзя переместить let pinned: Pin<&mut String> = Pin::new(&mut data); -// Can still use it: +// Всё ещё можно использовать: println!("{}", pinned.as_ref().get_ref()); // "hello" -// But we can't get &mut String back (which would allow mem::swap): -// let mutable: &mut String = Pin::into_inner(pinned); // Only if String: Unpin -// String IS Unpin, so this actually works for String. -// But for self-referential state machines (which are !Unpin), it's blocked. +// Но получить обратно &mut String нельзя (это позволило бы mem::swap): +// let mutable: &mut String = Pin::into_inner(pinned); // Только если String: Unpin +// String — это Unpin, поэтому здесь это на самом деле работает. +// Но для самоссылающихся конечных автоматов (они !Unpin) это заблокировано. ``` -In real code, you mostly encounter Pin in three places: +В реальном коде Pin встречается в основном в трёх местах: ```rust -// 1. poll() signature — all futures are polled through Pin +// 1. Сигнатура poll() — все future опрашиваются через Pin fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; -// 2. Box::pin() — heap-allocate and pin a future +// 2. Box::pin() — разместить future в куче и закрепить его let future: Pin>> = Box::pin(async { 42 }); -// 3. tokio::pin!() — pin a future on the stack +// 3. tokio::pin!() — закрепить future на стеке tokio::pin!(my_future); -// Now my_future: Pin<&mut impl Future> +// Теперь my_future: Pin<&mut impl Future> ``` -### The Unpin Escape Hatch +### Лазейка Unpin -Most types in Rust are `Unpin` — they don't contain self-references, so pinning is a no-op. Only compiler-generated state machines (from `async fn`) are `!Unpin`. +Большинство типов в Rust — `Unpin`: они не содержат самоссылок, поэтому закрепление для них ничего не меняет. `!Unpin` — только автоматы, сгенерированные компилятором из `async fn`. ```rust -// These are all Unpin — pinning them does nothing special: +// Все эти типы — Unpin, закрепление для них ничего особого не делает: // i32, String, Vec, HashMap, Box, &T, &mut T -// These are !Unpin — they MUST be pinned before polling: -// The state machines generated by `async fn` and `async {}` +// Эти типы — !Unpin, их ОБЯЗАТЕЛЬНО нужно закрепить перед опросом: +// Конечные автоматы, сгенерированные из `async fn` и `async {}` -// Practical implication: -// If you write a Future by hand and it has NO self-references, -// implement Unpin to make it easier to work with: -impl Unpin for MySimpleFuture {} // "I'm safe to move, trust me" +// Практический вывод: +// Если вы пишете Future вручную и в нём НЕТ самоссылок, +// реализуйте Unpin, чтобы с ним было проще работать: +impl Unpin for MySimpleFuture {} // «Его безопасно перемещать, поверьте мне» ``` -### Quick Reference +### Краткая справка -| What | When | How | -|------|------|-----| -| Pin a future on the heap | Storing in a collection, returning from function | `Box::pin(future)` | -| Pin a future on the stack | Local use in `select!` or manual polling | `std::pin::pin!(future)` or `tokio::pin!(future)` | -| Pin in function signature | Accepting pinned futures | `future: Pin<&mut F>` | -| Require Unpin | When you need to move a future after creation | `F: Future + Unpin` | +| Что | Когда | Как | +|-----|-------|-----| +| Закрепить future в куче | Хранение в коллекции, возврат из функции | `Box::pin(future)` | +| Закрепить future на стеке | Локальное использование в `select!` или при ручном опросе | `std::pin::pin!(future)` или `tokio::pin!(future)` | +| Pin в сигнатуре функции | Приём уже закреплённых future | `future: Pin<&mut F>` | +| Требовать Unpin | Когда нужно переместить future после создания | `F: Future + Unpin` |

-🏋️ Exercise: Pin and Move (click to expand) +🏋️ Упражнение: Pin и перемещение (нажмите, чтобы раскрыть) -**Challenge**: Which of these code snippets compile? For each one that doesn't, explain why and fix it. +**Задача**: какие из этих фрагментов кода компилируются? Для каждого, который не компилируется, объясните почему и исправьте. ```rust -// Snippet A +// Фрагмент A let fut = async { 42 }; let pinned = Box::pin(fut); -let moved = pinned; // Move the Box +let moved = pinned; // Перемещаем Box let result = moved.await; -// Snippet B +// Фрагмент B let fut = async { 42 }; tokio::pin!(fut); -let moved = fut; // Move the pinned future +let moved = fut; // Перемещаем закреплённый future let result = moved.await; -// Snippet C +// Фрагмент C use std::pin::Pin; let mut fut = async { 42 }; let pinned = Pin::new(&mut fut); ```
-🔑 Solution +🔑 Решение -**Snippet A**: ✅ **Compiles.** `Box::pin()` puts the future on the heap. Moving the `Box` moves the *pointer*, not the future itself. The future stays pinned in its heap location. +**Фрагмент A**: ✅ **Компилируется.** `Box::pin()` размещает future в куче. Перемещение `Box` перемещает *указатель*, а не сам future. Future остаётся закреплённым в своём месте в куче. -**Snippet B**: ✅ **Compiles.** `tokio::pin!` pins the future to the stack and rebinds `fut` as `Pin<&mut ...>`. `let moved = fut` moves the **`Pin` wrapper** (a pointer), not the underlying future — the future stays pinned on the stack. This is just like `Box::pin`: moving the `Box` doesn't move the heap allocation. However, `fut` is consumed by the move, so you can't use `fut` afterwards — only `moved`: +**Фрагмент B**: ✅ **Компилируется.** `tokio::pin!` закрепляет future на стеке и переобъявляет `fut` как `Pin<&mut ...>`. `let moved = fut` перемещает **обёртку `Pin`** (по сути, указатель), а не лежащий в ней future — future остаётся закреплённым на стеке. Это похоже на `Box::pin`: перемещение `Box` не перемещает выделение в куче. Однако `fut` потребляется перемещением, поэтому после него нельзя использовать `fut` — только `moved`: ```rust let fut = async { 42 }; tokio::pin!(fut); -let moved = fut; // Moves the Pin<&mut> wrapper — OK -// fut.await; // ❌ Error: fut was moved -let result = moved.await; // ✅ Use moved instead +let moved = fut; // Перемещаем обёртку Pin<&mut> — OK +// fut.await; // ❌ Ошибка: fut уже перемещён +let result = moved.await; // ✅ Используем moved ``` -**Snippet C**: ❌ **Does not compile.** `Pin::new()` requires `T: Unpin`. Async blocks generate `!Unpin` types. **Fix**: Use `Box::pin()` or `unsafe Pin::new_unchecked()`: +**Фрагмент C**: ❌ **Не компилируется.** `Pin::new()` требует `T: Unpin`. Async-блоки порождают типы `!Unpin`. **Исправление**: используйте `Box::pin()` или `unsafe Pin::new_unchecked()`: ```rust let fut = async { 42 }; -let pinned = Box::pin(fut); // Heap-pin — works with !Unpin +let pinned = Box::pin(fut); // Закрепление в куче — работает с !Unpin ``` -**Key takeaway**: `Box::pin()` is the safe, easy way to pin `!Unpin` futures. `tokio::pin!()` pins on the stack — you can move the `Pin<&mut>` wrapper (it's just a pointer), but the underlying future stays put. `Pin::new()` only works with `Unpin` types. +**Ключевой вывод**: `Box::pin()` — безопасный и простой способ закрепить `!Unpin` future. `tokio::pin!()` закрепляет на стеке — можно перемещать обёртку `Pin<&mut>` (это всего лишь указатель), но сам future остаётся на месте. `Pin::new()` работает только с типами `Unpin`.
-> **Key Takeaways — Pin and Unpin** -> - `Pin

` is a wrapper that **prevents the pointee from being moved** — essential for self-referential state machines -> - `Box::pin()` is the safe, easy default for pinning futures on the heap -> - `tokio::pin!()` pins on the stack — you can move the `Pin<&mut>` wrapper, but the underlying future stays put -> - `Unpin` is an auto-trait opt-out: types that implement `Unpin` can be moved even when pinned (most types are `Unpin`; async blocks are not) +> **Ключевые выводы — Pin и Unpin** +> - `Pin

` — это обёртка, которая **запрещает перемещать то, на что указывает**; без неё невозможны самоссылающиеся конечные автоматы +> - `Box::pin()` — безопасный и простой вариант по умолчанию для закрепления future в куче +> - `tokio::pin!()` закрепляет на стеке — обёртку `Pin<&mut>` можно перемещать, но сам future остаётся на месте +> - `Unpin` — это автотрейт-исключение: типы, реализующие `Unpin`, можно перемещать даже будучи закреплёнными (большинство типов — `Unpin`; async-блоки — нет) -> **See also:** [Ch 2 — The Future Trait](ch02-the-future-trait.md) for `Pin<&mut Self>` in poll, [Ch 5 — The State Machine Reveal](ch05-the-state-machine-reveal.md) for why async state machines are self-referential +> **См. также:** [Гл. 2 — Трейт Future](ch02-the-future-trait.md) — `Pin<&mut Self>` в poll, [Гл. 5 — Раскрываем конечный автомат](ch05-the-state-machine-reveal.md) — почему async-автоматы самоссылающиеся *** - - diff --git a/async-book/src/ch05-the-state-machine-reveal.md b/async-book/src/ch05-the-state-machine-reveal.md index 9e68269..3761a90 100644 --- a/async-book/src/ch05-the-state-machine-reveal.md +++ b/async-book/src/ch05-the-state-machine-reveal.md @@ -1,19 +1,19 @@ -# 5. The State Machine Reveal 🟢 +# 5. Раскрываем конечный автомат 🟢 -> **What you'll learn:** -> - How the compiler transforms `async fn` into an enum state machine -> - Side-by-side comparison: source code vs generated states -> - Why large stack allocations in `async fn` blow up future sizes -> - The drop optimization: values drop as soon as they're no longer needed +> **Что вы узнаете:** +> - Как компилятор превращает `async fn` в перечисление (enum) — конечный автомат +> - Сравнение бок о бок: исходный код и сгенерированные состояния +> - Почему большие стековые выделения в `async fn` раздувают размер future +> - Оптимизация уничтожения: значения освобождаются, как только больше не нужны -## What the Compiler Actually Generates +## Что компилятор генерирует на самом деле -When you write `async fn`, the compiler transforms your sequential-looking code into an enum-based state machine. Understanding this transformation is the key to understanding async Rust's performance characteristics and many of its quirks. +Когда вы пишете `async fn`, компилятор превращает ваш последовательно выглядящий код в конечный автомат на основе enum. Понимание этого преобразования — ключ к пониманию производительности асинхронного Rust и многих его особенностей. -### Side-by-Side: async fn vs State Machine +### Бок о бок: async fn и конечный автомат ```rust -// What you write: +// Что вы пишете: async fn fetch_two_pages() -> String { let page1 = http_get("https://example.com/a").await; let page2 = http_get("https://example.com/b").await; @@ -21,25 +21,25 @@ async fn fetch_two_pages() -> String { } ``` -The compiler generates something conceptually like this: +Компилятор генерирует примерно такой код: ```rust enum FetchTwoPagesStateMachine { - // State 0: About to call http_get for page1 + // Состояние 0: собираемся вызвать http_get для page1 Start, - // State 1: Waiting for page1, holding the future + // Состояние 1: ждём page1, удерживая future WaitingPage1 { fut1: HttpGetFuture, }, - // State 2: Got page1, waiting for page2 + // Состояние 2: получили page1, ждём page2 WaitingPage2 { page1: String, fut2: HttpGetFuture, }, - // Terminal state + // Конечное состояние Complete, } @@ -77,31 +77,31 @@ impl Future for FetchTwoPagesStateMachine { } ``` -> **Note**: This desugaring is *conceptual*. The real compiler output uses -> `unsafe` pin projections — the `get_mut()` calls shown here require -> `Unpin`, but async state machines are `!Unpin`. The goal is to illustrate -> state transitions, not produce compilable code. +> **Примечание**: это преобразование *концептуальное*. Реальный вывод компилятора использует +> `unsafe`-проекции Pin — вызовы `get_mut()`, показанные здесь, требуют +> `Unpin`, а async-автоматы являются `!Unpin`. Цель — проиллюстрировать +> переходы между состояниями, а не получить код, который компилируется. ```mermaid stateDiagram-v2 [*] --> Start - Start --> WaitingPage1: Create http_get future #1 + Start --> WaitingPage1: Создаём future http_get #1 WaitingPage1 --> WaitingPage1: poll() → Pending WaitingPage1 --> WaitingPage2: poll() → Ready(page1) WaitingPage2 --> WaitingPage2: poll() → Pending WaitingPage2 --> Complete: poll() → Ready(page2) - Complete --> [*]: Return format!("{page1}\\n{page2}") + Complete --> [*]: Возвращаем format!("{page1}\\n{page2}") ``` -> **State contents:** -> - **WaitingPage1** — stores `fut1: HttpGetFuture` (page2 not yet allocated) -> - **WaitingPage2** — stores `page1: String`, `fut2: HttpGetFuture` (fut1 has been dropped) +> **Содержимое состояний:** +> - **WaitingPage1** — хранит `fut1: HttpGetFuture` (page2 ещё не выделен) +> - **WaitingPage2** — хранит `page1: String`, `fut2: HttpGetFuture` (fut1 уже уничтожен) -### Why This Matters for Performance +### Почему это важно для производительности -**Zero-cost**: The state machine is a stack-allocated enum. No heap allocation per future, no garbage collector, no boxing — unless you explicitly use `Box::pin()`. +**Нулевая стоимость**: конечный автомат — это enum на стеке. Никаких выделений в куче на каждый future, никакого сборщика мусора, никакого боксинга — если вы явно не используете `Box::pin()`. -**Size**: The enum's size is the maximum of all its variants. Each `.await` point creates a new variant. This means: +**Размер**: размер enum равен максимуму размеров всех его вариантов. Каждая точка `.await` создаёт новый вариант. Это означает: ```rust async fn small() { @@ -110,31 +110,32 @@ async fn small() { let b: u8 = 0; yield_now().await; } -// Size ≈ max(size_of(u8), size_of(u8)) + discriminant + future sizes -// ≈ small! +// Размер ≈ max(size_of(u8), size_of(u8)) + дискриминант + размеры future +// ≈ маленький! async fn big() { - let buf: [u8; 1_000_000] = [0; 1_000_000]; // 1MB on the stack! + let buf: [u8; 1_000_000] = [0; 1_000_000]; // 1 МБ на стеке! some_io().await; process(&buf); } -// Size ≈ 1MB + inner future sizes -// ⚠️ Don't stack-allocate huge buffers in async functions! -// Use Vec or Box<[u8]> instead. +// Размер ≈ 1 МБ + размеры вложенных future +// ⚠️ Не размещайте огромные буферы на стеке в async-функциях! +// Используйте Vec или Box<[u8]>. ``` -**Drop optimization**: When a state machine transitions, it drops values no longer needed. In the example above, `fut1` is dropped when we transition from `WaitingPage1` to `WaitingPage2` — the compiler inserts the drop automatically. +**Оптимизация уничтожения**: при переходе между состояниями автомат уничтожает значения, которые больше не нужны. В примере выше `fut1` уничтожается при переходе из `WaitingPage1` в `WaitingPage2` — компилятор вставляет уничтожение автоматически. -> **Practical rule**: Large stack allocations in `async fn` blow up the future's -> size. If you see stack overflows in async code, check for large arrays or -> deeply nested futures. Use `Box::pin()` to heap-allocate sub-futures if needed. +> **Практическое правило**: большие стековые выделения в `async fn` раздувают размер future. +> Если вы видите переполнение стека в async-коде, проверьте большие массивы или +> глубоко вложенные future. При необходимости используйте `Box::pin()`, чтобы +> разместить вложенные future в куче. -### Exercise: Predict the State Machine +### Упражнение: предскажите конечный автомат

-🏋️ Exercise (click to expand) +🏋️ Упражнение (нажмите, чтобы раскрыть) -**Challenge**: Given this async function, sketch the state machine the compiler generates. How many states (enum variants) does it have? What values are stored in each? +**Задача**: для этой async-функции набросайте конечный автомат, который генерирует компилятор. Сколько у него состояний (вариантов enum)? Какие значения хранятся в каждом? ```rust async fn pipeline(url: &str) -> Result { @@ -146,29 +147,27 @@ async fn pipeline(url: &str) -> Result { ```
-🔑 Solution +🔑 Решение -Five states: +Пять состояний: -1. **Start** — stores `url` -2. **WaitingFetch** — stores `url`, `fetch` future -3. **WaitingText** — stores `response`, `text()` future -4. **WaitingParse** — stores `body`, `parse` future -5. **Done** — returned `Ok(parsed.len())` +1. **Start** — хранит `url` +2. **WaitingFetch** — хранит `url`, future `fetch` +3. **WaitingText** — хранит `response`, future `text()` +4. **WaitingParse** — хранит `body`, future `parse` +5. **Done** — вернул `Ok(parsed.len())` -Each `.await` creates a yield point = a new enum variant. The `?` adds early-exit paths but doesn't add extra states — it's just a `match` on the `Poll::Ready` value. +Каждый `.await` создаёт точку передачи управления = новый вариант enum. Оператор `?` добавляет пути раннего выхода, но не добавляет лишних состояний — это просто `match` по значению `Poll::Ready`.
-> **Key Takeaways — The State Machine Reveal** -> - `async fn` compiles to an enum with one variant per `.await` point -> - The future's **size** = max of all variant sizes — large stack values blow it up -> - The compiler inserts **drops** at state transitions automatically -> - Use `Box::pin()` or heap allocation when future size becomes a problem +> **Ключевые выводы — раскрываем конечный автомат** +> - `async fn` компилируется в enum с одним вариантом на каждую точку `.await` +> - **Размер** future = максимум размеров всех вариантов — большие значения на стеке его раздувают +> - Компилятор автоматически вставляет **уничтожение** значений при переходах между состояниями +> - Используйте `Box::pin()` или выделение в куче, когда размер future становится проблемой -> **See also:** [Ch 4 — Pin and Unpin](ch04-pin-and-unpin.md) for why the generated enum needs pinning, [Ch 6 — Building Futures by Hand](ch06-building-futures-by-hand.md) to build these state machines yourself +> **См. также:** [Гл. 4 — Pin и Unpin](ch04-pin-and-unpin.md) — почему сгенерированный enum нужно закреплять, [Гл. 6 — Создаём фьючи вручную](ch06-building-futures-by-hand.md) — построить такие автоматы самостоятельно *** - - diff --git a/async-book/src/ch06-building-futures-by-hand.md b/async-book/src/ch06-building-futures-by-hand.md index 3c9eee4..2e97524 100644 --- a/async-book/src/ch06-building-futures-by-hand.md +++ b/async-book/src/ch06-building-futures-by-hand.md @@ -1,16 +1,16 @@ -# 6. Building Futures by Hand 🟡 +# 6. Создаём фьючи вручную 🟡 -> **What you'll learn:** -> - Implementing a `TimerFuture` with thread-based waking -> - Building a `Join` combinator: run two futures concurrently -> - Building a `Select` combinator: race two futures -> - How combinators compose — futures all the way down +> **Что вы узнаете:** +> - Реализация `TimerFuture` с пробуждением через поток +> - Построение комбинатора `Join`: одновременный запуск двух фьючей +> - Построение комбинатора `Select`: гонка двух фьючей +> - Как комбинаторы компонуются — фьючи внутри фьючей до самого низа -## A Simple Timer Future +## Простой таймер-фьюча -Now let's build real, useful futures from scratch. This cements the theory from chapters 2-5. +Теперь построим настоящие, полезные фьючи с нуля. Это закрепит теорию из глав 2–5. -### TimerFuture: A Complete Example +### TimerFuture: полный пример ```rust use std::future::Future; @@ -36,14 +36,14 @@ impl TimerFuture { waker: None, })); - // Spawn a thread that sets completed=true after the duration + // Запускаем поток, который через duration выставит completed=true let thread_shared_state = Arc::clone(&shared_state); thread::spawn(move || { thread::sleep(duration); let mut state = thread_shared_state.lock().unwrap(); state.completed = true; if let Some(waker) = state.waker.take() { - waker.wake(); // Notify the executor + waker.wake(); // Уведомляем исполнитель } }); @@ -59,37 +59,37 @@ impl Future for TimerFuture { if state.completed { Poll::Ready(()) } else { - // Store the waker so the timer thread can wake us - // IMPORTANT: Always update the waker — the executor may - // have changed it between polls + // Сохраняем waker, чтобы поток таймера смог нас разбудить + // ВАЖНО: всегда обновляйте waker — исполнитель мог заменить его + // между опросами state.waker = Some(cx.waker().clone()); Poll::Pending } } } -// Usage: +// Использование: // async fn example() { -// println!("Starting timer..."); +// println!("Запускаем таймер..."); // TimerFuture::new(Duration::from_secs(2)).await; -// println!("Timer done!"); +// println!("Таймер сработал!"); // } // -// ⚠️ This spawns an OS thread per timer — fine for learning, but in -// production use `tokio::time::sleep` which is backed by a shared -// timer wheel and requires zero extra threads. +// ⚠️ Здесь на каждый таймер создаётся системный поток — для обучения это нормально, +// но в продакшене используйте `tokio::time::sleep`: он работает на общем +// колесе таймеров и не требует дополнительных потоков. ``` -### Join: Running Two Futures Concurrently +### Join: одновременный запуск двух фьючей -`Join` polls two futures and completes when *both* finish. This is how `tokio::join!` works internally: +`Join` опрашивает два future и завершается, когда *оба* закончились. Так `tokio::join!` работает внутри: ```rust use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -/// Polls two futures concurrently, returns both results as a tuple +/// Опрашивает два future одновременно и возвращает оба результата в кортеже pub struct Join where A: Future, @@ -102,13 +102,13 @@ where enum MaybeDone { Pending(F), Done(F::Output), - Taken, // Output has been taken + Taken, // Результат уже забран } -// MaybeDone stores F::Output, which the compiler can't prove -// is Unpin even when F: Unpin. Since we only use Join with Unpin -// futures and never pin-project into fields, implementing Unpin -// by hand is safe and lets us call self.get_mut() in poll(). +// MaybeDone хранит F::Output, и компилятор не может доказать, +// что он Unpin, даже когда F: Unpin. Поскольку мы используем Join только с Unpin- +// future и никогда не проецируем закрепление на поля, ручная реализация +// Unpin безопасна и позволяет вызывать self.get_mut() в poll(). impl Unpin for Join {} impl Join @@ -134,24 +134,24 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - // Poll A if not done + // Опрашиваем A, если он ещё не завершён if let MaybeDone::Pending(ref mut fut) = this.a { if let Poll::Ready(val) = Pin::new(fut).poll(cx) { this.a = MaybeDone::Done(val); } } - // Poll B if not done + // Опрашиваем B, если он ещё не завершён if let MaybeDone::Pending(ref mut fut) = this.b { if let Poll::Ready(val) = Pin::new(fut).poll(cx) { this.b = MaybeDone::Done(val); } } - // Both done? + // Оба готовы? match (&this.a, &this.b) { (MaybeDone::Done(_), MaybeDone::Done(_)) => { - // Take both outputs + // Забираем оба результата let a_val = match std::mem::replace(&mut this.a, MaybeDone::Taken) { MaybeDone::Done(v) => v, _ => unreachable!(), @@ -162,31 +162,31 @@ where }; Poll::Ready((a_val, b_val)) } - _ => Poll::Pending, // At least one is still pending + _ => Poll::Pending, // Хотя бы один ещё ожидает } } } -// Usage (async blocks are !Unpin, so wrap them with Box::pin): +// Использование (async-блоки являются !Unpin, поэтому оборачиваем их в Box::pin): // let (page1, page2) = Join::new( // Box::pin(http_get("https://example.com/a")), // Box::pin(http_get("https://example.com/b")), // ).await; -// Both requests run concurrently! +// Оба запроса выполняются одновременно! ``` -> **Key insight**: "Concurrent" here means *interleaved on the same thread*. -> Join doesn't spawn threads — it polls both futures in the same `poll()` call. -> This is cooperative concurrency, not parallelism. +> **Ключевая мысль**: «одновременно» здесь означает *чередование на одном потоке*. +> Join не создаёт потоков — он опрашивает оба future в одном и том же вызове `poll()`. +> Это кооперативная конкурентность, а не параллелизм. ```mermaid graph LR - subgraph "Future Combinators" + subgraph "Комбинаторы фьючей" direction TB - TIMER["TimerFuture
Single future, wake after delay"] - JOIN["Join<A, B>
Wait for BOTH"] - SELECT["Select<A, B>
Wait for FIRST"] - RETRY["RetryFuture
Re-create on failure"] + TIMER["TimerFuture
Один future, пробуждение через задержку"] + JOIN["Join<A, B>
Ждём ОБА"] + SELECT["Select<A, B>
Ждём ПЕРВЫЙ"] + RETRY["RetryFuture
Пересоздаём при ошибке"] end TIMER --> JOIN @@ -199,9 +199,9 @@ graph LR style RETRY fill:#fadbd8,stroke:#e74c3c,color:#000 ``` -### Select: Racing Two Futures +### Select: гонка двух фьючей -`Select` completes when *either* future finishes first (the other is dropped): +`Select` завершается, когда *любой* из future закончился первым (второй уничтожается): ```rust use std::future::Future; @@ -213,7 +213,7 @@ pub enum Either { Right(B), } -/// Returns whichever future completes first; drops the other +/// Возвращает тот future, который завершился первым; второй уничтожает pub struct Select { a: A, b: B, @@ -237,12 +237,12 @@ where type Output = Either; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // Poll A first + // Сначала опрашиваем A if let Poll::Ready(val) = Pin::new(&mut self.a).poll(cx) { return Poll::Ready(Either::Left(val)); } - // Then poll B + // Затем опрашиваем B if let Poll::Ready(val) = Pin::new(&mut self.b).poll(cx) { return Poll::Ready(Either::Right(val)); } @@ -251,25 +251,25 @@ where } } -// Usage with timeout: +// Использование с таймаутом: // match Select::new(http_get(url), TimerFuture::new(timeout)).await { -// Either::Left(response) => println!("Got response: {}", response), -// Either::Right(()) => println!("Request timed out!"), +// Either::Left(response) => println!("Получен ответ: {}", response), +// Either::Right(()) => println!("Запрос просрочен по таймауту!"), // } ``` -> **Fairness note**: Our `Select` always polls A first — if both are ready, A -> always wins. Tokio's `select!` macro randomizes the poll order for fairness. +> **Замечание о справедливости**: наш `Select` всегда опрашивает A первым — если готовы оба, всегда побеждает A. +> Макрос `select!` в tokio рандомизирует порядок опроса, чтобы обеспечить справедливость.
-🏋️ Exercise: Build a RetryFuture (click to expand) +🏋️ Упражнение: реализуйте RetryFuture (нажмите, чтобы раскрыть) -**Challenge**: Build a `RetryFuture` that takes a closure `F: Fn() -> Fut` and retries up to N times if the inner future returns `Err`. It should return the first `Ok` result or the last `Err`. +**Задача**: реализуйте `RetryFuture`, который принимает замыкание `F: Fn() -> Fut` и повторяет попытку до N раз, если внутренний future возвращает `Err`. Он должен вернуть первый `Ok` или последний `Err`. -*Hint*: You'll need states for "running attempt" and "all attempts exhausted." +*Подсказка*: понадобятся состояния «выполняется попытка» и «все попытки исчерпаны».
-🔑 Solution +🔑 Решение ```rust use std::future::Future; @@ -312,8 +312,8 @@ where type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // Pin> is always Unpin, so the struct is Unpin when F and E are. - // This lets us safely use get_mut() without any unsafe code. + // Pin> всегда Unpin, поэтому структура Unpin, когда Unpin F и E. + // Это позволяет безопасно использовать get_mut() без unsafe-кода. loop { if let Some(ref mut fut) = self.current { match fut.as_mut().poll(cx) { @@ -323,7 +323,7 @@ where if self.remaining > 0 { self.remaining -= 1; self.current = Some(Box::pin((self.factory)())); - // Loop to poll the new future immediately + // Возвращаемся в цикл, чтобы сразу опросить новый future } else { return Poll::Ready(Err(self.last_error.take().unwrap())); } @@ -337,25 +337,23 @@ where } } -// Usage: +// Использование: // let result = RetryFuture::new(3, || async { // http_get("https://flaky-server.com/api").await // }).await; ``` -**Key takeaway**: The retry future is itself a state machine: it holds the current attempt and creates new inner futures on failure. Wrapping the inner future in `Pin>` removes the `Fut: Unpin` bound — since `Pin>` is always `Unpin`, the struct remains easy to work with while supporting any future type. This is how combinators compose — futures all the way down. +**Ключевой вывод**: сам future для повторов — это конечный автомат: он хранит текущую попытку и создаёт новые внутренние future при ошибке. Оборачивание внутреннего future в `Pin>` снимает ограничение `Fut: Unpin` — поскольку `Pin>` всегда `Unpin`, со структурой легко работать, и при этом она поддерживает любой тип future. Так компонуются комбинаторы — фьючи внутри фьючей до самого низа.
-> **Key Takeaways — Building Futures by Hand** -> - A future needs three things: state, a `poll()` implementation, and a waker registration -> - `Join` polls both sub-futures; `Select` returns whichever finishes first -> - Combinators are themselves futures wrapping other futures — it's turtles all the way down -> - Building futures by hand gives deep insight, but in production use `tokio::join!`/`select!` +> **Ключевые выводы — создаём фьючи вручную** +> - Future нужны три вещи: состояние, реализация `poll()` и регистрация waker +> - `Join` опрашивает оба вложенных future; `Select` возвращает тот, что завершится первым +> - Комбинаторы — это сами фьючи, оборачивающие другие фьючи: и так до самого низа +> - Построение фьючей вручную даёт глубокое понимание, но в продакшене используйте `tokio::join!`/`select!` -> **See also:** [Ch 2 — The Future Trait](ch02-the-future-trait.md) for the trait definition, [Ch 8 — Tokio Deep Dive](ch08-tokio-deep-dive.md) for production-grade equivalents +> **См. также:** [Гл. 2 — Трейт Future](ch02-the-future-trait.md) — определение трейта, [Гл. 8 — Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) — продакшен-аналоги *** - - diff --git a/async-book/src/ch07-executors-and-runtimes.md b/async-book/src/ch07-executors-and-runtimes.md index 83d48bd..366bcd3 100644 --- a/async-book/src/ch07-executors-and-runtimes.md +++ b/async-book/src/ch07-executors-and-runtimes.md @@ -1,34 +1,34 @@ -# 7. Executors and Runtimes 🟡 +# 7. Исполнители и рантаймы 🟡 -> **What you'll learn:** -> - What an executor does: poll + sleep efficiently -> - The six major runtimes: mio, io_uring, tokio, async-std, smol, embassy -> - A decision tree for choosing the right runtime -> - Why runtime-agnostic library design matters +> **Что вы узнаете:** +> - Что делает исполнитель: опрашивает future и эффективно засыпает +> - Шесть основных рантаймов: mio, io_uring, tokio, async-std, smol, embassy +> - Дерево решений для выбора подходящего рантайма +> - Почему важен дизайн библиотек, не привязанных к рантайму -## What an Executor Does +## Что делает исполнитель -An executor has two jobs: -1. **Poll futures** when they're ready to make progress -2. **Sleep efficiently** when no futures are ready (using OS I/O notification APIs) +У исполнителя две задачи: +1. **Опрашивать future**, когда они готовы продвинуться дальше +2. **Эффективно засыпать**, когда ни один future не готов (используя API уведомлений ввода-вывода ОС) ```mermaid graph TB - subgraph Executor["Executor (e.g., tokio)"] - QUEUE["Task Queue"] - POLLER["I/O Poller
(epoll/kqueue/io_uring)"] - THREADS["Worker Thread Pool"] + subgraph Executor["Исполнитель (например, tokio)"] + QUEUE["Очередь задач"] + POLLER["Опрос I/O
(epoll/kqueue/io_uring)"] + THREADS["Пул рабочих потоков"] end - subgraph Tasks - T1["Task 1
(HTTP request)"] - T2["Task 2
(DB query)"] - T3["Task 3
(File read)"] + subgraph Tasks["Задачи"] + T1["Задача 1
(HTTP-запрос)"] + T2["Задача 2
(запрос к БД)"] + T3["Задача 3
(чтение файла)"] end - subgraph OS["Operating System"] - NET["Network Stack"] - DISK["Disk I/O"] + subgraph OS["Операционная система"] + NET["Сетевой стек"] + DISK["Дисковый ввод-вывод"] end T1 --> QUEUE @@ -38,20 +38,20 @@ graph TB THREADS -->|"poll()"| T1 THREADS -->|"poll()"| T2 THREADS -->|"poll()"| T3 - POLLER <-->|"register/notify"| NET - POLLER <-->|"register/notify"| DISK - POLLER -->|"wake tasks"| QUEUE + POLLER <-->|"регистрация/уведомление"| NET + POLLER <-->|"регистрация/уведомление"| DISK + POLLER -->|"пробуждение задач"| QUEUE style Executor fill:#e3f2fd,color:#000 style OS fill:#f3e5f5,color:#000 ``` -### mio: The Foundation Layer +### mio: базовый слой -[mio](https://github.com/tokio-rs/mio) (Metal I/O) is not an executor — it's the lowest-level cross-platform I/O notification library. It wraps `epoll` (Linux), `kqueue` (macOS/BSD), and IOCP (Windows). +[mio](https://github.com/tokio-rs/mio) (Metal I/O) — это не исполнитель, а самая низкоуровневая кроссплатформенная библиотека уведомлений о вводе-выводе. Она оборачивает `epoll` (Linux), `kqueue` (macOS/BSD) и IOCP (Windows). ```rust -// Conceptual mio usage (simplified): +// Условное использование mio (упрощённо): use mio::{Events, Interest, Poll, Token}; use mio::net::TcpListener; @@ -61,67 +61,67 @@ let mut events = Events::with_capacity(128); let mut server = TcpListener::bind("0.0.0.0:8080")?; poll.registry().register(&mut server, Token(0), Interest::READABLE)?; -// Event loop — blocks until something happens +// Цикл событий — блокируется, пока что-нибудь не произойдёт loop { - poll.poll(&mut events, None)?; // Sleeps until I/O event + poll.poll(&mut events, None)?; // Спим до события ввода-вывода for event in events.iter() { match event.token() { - Token(0) => { /* server has a new connection */ } - _ => { /* other I/O ready */ } + Token(0) => { /* у сервера новое соединение */ } + _ => { /* другой ввод-вывод готов */ } } } } ``` -Most developers never touch mio directly — tokio and smol build on top of it. +Большинство разработчиков никогда не работают с mio напрямую — tokio и smol строятся поверх него. -### io_uring: The Completion-Based Future +### io_uring: завершение как основа -Linux's `io_uring` (kernel 5.1+) represents a fundamental shift from the readiness-based I/O model that mio/epoll use: +`io_uring` в Linux (ядро 5.1+) — это принципиальный сдвиг по сравнению с моделью ввода-вывода на основе готовности, которую используют mio/epoll: ```text -Readiness-based (epoll / mio / tokio): - 1. Ask: "Is this socket readable?" → epoll_wait() - 2. Kernel: "Yes, it's ready" → EPOLLIN event - 3. App: read(fd, buf) → might still block briefly! - -Completion-based (io_uring): - 1. Submit: "Read from this socket into this buffer" → SQE - 2. Kernel: does the read asynchronously - 3. App: gets completed result with data → CQE +На основе готовности (epoll / mio / tokio): + 1. Спрашиваем: «Можно ли читать из этого сокета?» → epoll_wait() + 2. Ядро: «Да, готов» → событие EPOLLIN + 3. Приложение: read(fd, buf) → может всё ещё ненадолго заблокироваться! + +На основе завершения (io_uring): + 1. Отправляем: «Прочитай из этого сокета в этот буфер» → SQE + 2. Ядро: выполняет чтение асинхронно + 3. Приложение: получает готовый результат с данными → CQE ``` ```mermaid graph LR - subgraph "Readiness Model (epoll)" - A1["App: is it ready?"] --> K1["Kernel: yes"] - K1 --> A2["App: now read()"] - A2 --> K2["Kernel: here's data"] + subgraph "Модель готовности (epoll)" + A1["Приложение: готово?"] --> K1["Ядро: да"] + K1 --> A2["Приложение: теперь read()"] + A2 --> K2["Ядро: вот данные"] end - subgraph "Completion Model (io_uring)" - B1["App: read this for me"] --> K3["Kernel: working..."] - K3 --> B2["App: got result + data"] + subgraph "Модель завершения (io_uring)" + B1["Приложение: прочитай это за меня"] --> K3["Ядро: работает..."] + K3 --> B2["Приложение: получил результат + данные"] end style B1 fill:#c8e6c9,color:#000 style B2 fill:#c8e6c9,color:#000 ``` -**The ownership challenge**: io_uring requires the kernel to own the buffer until the operation completes. This conflicts with Rust's standard `AsyncRead` trait which borrows the buffer. That's why `tokio-uring` has different I/O traits: +**Проблема владения**: io_uring требует, чтобы ядро владело буфером до завершения операции. Это конфликтует со стандартным трейтом `AsyncRead` в Rust, который заимствует буфер. Поэтому в `tokio-uring` другие трейты ввода-вывода: ```rust -// Standard tokio (readiness-based) — borrows the buffer: -let n = stream.read(&mut buf).await?; // buf is borrowed +// Стандартный tokio (на основе готовности) — заимствует буфер: +let n = stream.read(&mut buf).await?; // buf заимствован -// tokio-uring (completion-based) — takes ownership of the buffer: -let (result, buf) = stream.read(buf).await; // buf is moved in, returned back +// tokio-uring (на основе завершения) — забирает владение буфером: +let (result, buf) = stream.read(buf).await; // buf передан внутрь и возвращён обратно let n = result?; ``` ```rust // Cargo.toml: tokio-uring = "0.5" -// NOTE: Linux-only, requires kernel 5.1+ +// ЗАМЕТЬТЕ: только Linux, требуется ядро 5.1+ fn main() { tokio_uring::start(async { @@ -129,25 +129,25 @@ fn main() { let buf = vec![0u8; 4096]; let (result, buf) = file.read_at(buf, 0).await; let bytes_read = result.unwrap(); - println!("Read {} bytes: {:?}", bytes_read, &buf[..bytes_read]); + println!("Прочитано байт: {}: {:?}", bytes_read, &buf[..bytes_read]); }); } ``` -| Aspect | epoll (tokio) | io_uring (tokio-uring) | +| Аспект | epoll (tokio) | io_uring (tokio-uring) | |--------|--------------|----------------------| -| **Model** | Readiness notification | Completion notification | -| **Syscalls** | epoll_wait + read/write | Batched SQE/CQE ring | -| **Buffer ownership** | App retains (&mut buf) | Ownership transfer (move buf) | -| **Platform** | Linux, macOS (kqueue), Windows (IOCP) | Linux 5.1+ only | -| **Zero-copy** | No (userspace copy) | Yes (registered buffers) | -| **Maturity** | Production-ready | Experimental | +| **Модель** | Уведомление о готовности | Уведомление о завершении | +| **Системные вызовы** | epoll_wait + read/write | Пакетное кольцо SQE/CQE | +| **Владение буфером** | Приложение сохраняет (&mut buf) | Передача владения (move buf) | +| **Платформа** | Linux, macOS (kqueue), Windows (IOCP) | Только Linux 5.1+ | +| **Zero-copy** | Нет (копирование в пользовательском пространстве) | Да (зарегистрированные буферы) | +| **Зрелость** | Готов к продакшену | Экспериментальный | -> **When to use io_uring**: High-throughput file I/O or networking where syscall overhead is the bottleneck (databases, storage engines, proxies serving 100k+ connections). For most applications, standard tokio with epoll is the right choice. +> **Когда использовать io_uring**: высокопроизводительный файловый ввод-вывод или сеть, где узким местом становятся накладные расходы системных вызовов (базы данных, движки хранения, прокси на 100k+ соединений). Для большинства приложений правильный выбор — стандартный tokio с epoll. -### tokio: The Batteries-Included Runtime +### tokio: рантайм «всё в комплекте» -The dominant async runtime in the Rust ecosystem. Used by Axum, Hyper, Tonic, and most production Rust servers. +Доминирующий асинхронный рантайм в экосистеме Rust. Используется в Axum, Hyper, Tonic и большинстве продакшен-серверов на Rust. ```rust // Cargo.toml: @@ -156,7 +156,7 @@ The dominant async runtime in the Rust ecosystem. Used by Axum, Hyper, Tonic, an #[tokio::main] async fn main() { - // Spawns a multi-threaded runtime with work-stealing scheduler + // Запускает многопоточный рантайм с планировщиком work-stealing let handle = tokio::spawn(async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; "done" @@ -167,11 +167,11 @@ async fn main() { } ``` -**tokio features**: Timer, I/O, TCP/UDP, Unix sockets, signal handling, sync primitives (Mutex, RwLock, Semaphore, channels), fs, process, tracing integration. +**Возможности tokio**: таймеры, ввод-вывод, TCP/UDP, Unix-сокеты, обработка сигналов, примитивы синхронизации (Mutex, RwLock, Semaphore, каналы), fs, process, интеграция с tracing. -### async-std: The Standard Library Mirror +### async-std: зеркало стандартной библиотеки -Mirrors the `std` API with async versions. Less popular than tokio but simpler for beginners. +Повторяет API `std` в асинхронных версиях. Менее популярен, чем tokio, но проще для начинающих. ```rust // Cargo.toml: @@ -186,9 +186,9 @@ async fn main() { } ``` -### smol: The Minimalist Runtime +### smol: минималистичный рантайм -Small, zero-dependency async runtime. Great for libraries that want async without pulling in tokio. +Небольшой асинхронный рантайм без зависимостей. Отлично подходит для библиотек, которые хотят async, но не хотят тянуть tokio. ```rust // Cargo.toml: @@ -198,7 +198,7 @@ Small, zero-dependency async runtime. Great for libraries that want async withou fn main() { smol::block_on(async { let result = smol::unblock(|| { - // Runs blocking code on a thread pool + // Выполняет блокирующий код в пуле потоков std::fs::read_to_string("hello.txt") }).await.unwrap(); println!("{result}"); @@ -206,15 +206,15 @@ fn main() { } ``` -### embassy: Async for Embedded (no_std) +### embassy: async для встраиваемых систем (no_std) -Async runtime for embedded systems. No heap allocation, no `std` required. +Асинхронный рантайм для встраиваемых систем. Без выделения памяти в куче, `std` не нужен. ```rust -// Runs on microcontrollers (e.g., STM32, nRF52, RP2040) +// Работает на микроконтроллерах (например, STM32, nRF52, RP2040) #[embassy_executor::main] async fn main(spawner: embassy_executor::Spawner) { - // Blink an LED with async/await — no RTOS needed! + // Мигаем светодиодом через async/await — RTOS не нужна! let mut led = Output::new(p.PA5, Level::Low, Speed::Low); loop { led.set_high(); @@ -225,35 +225,35 @@ async fn main(spawner: embassy_executor::Spawner) { } ``` -### Runtime Decision Tree +### Дерево решений по выбору рантайма ```mermaid graph TD - START["Choosing a Runtime"] + START["Выбор рантайма"] - Q1{"Building a
network server?"} - Q2{"Need tokio ecosystem
(Axum, Tonic, Hyper)?"} - Q3{"Building a library?"} - Q4{"Embedded /
no_std?"} - Q5{"Want minimal
dependencies?"} + Q1{"Пишете
сетевой сервер?"} + Q2{"Нужна экосистема tokio
(Axum, Tonic, Hyper)?"} + Q3{"Пишете
библиотеку?"} + Q4{"Встраиваемая система /
no_std?"} + Q5{"Нужны минимальные
зависимости?"} - TOKIO["🟢 tokio
Best ecosystem, most popular"] - SMOL["🔵 smol
Minimal, no ecosystem lock-in"] - EMBASSY["🟠 embassy
Embedded-first, no alloc"] - ASYNC_STD["🟣 async-std
std-like API, good for learning"] - AGNOSTIC["🔵 runtime-agnostic
Use futures crate only"] + TOKIO["🟢 tokio
Лучшая экосистема, самый популярный"] + SMOL["🔵 smol
Минимальный, без привязки к экосистеме"] + EMBASSY["🟠 embassy
Ориентирован на встраиваемые, без выделений"] + ASYNC_STD["🟣 async-std
API как у std, хорош для обучения"] + AGNOSTIC["🔵 независимый от рантайма
Используйте только крейт futures"] START --> Q1 - Q1 -->|Yes| Q2 - Q1 -->|No| Q3 - Q2 -->|Yes| TOKIO - Q2 -->|No| Q5 - Q3 -->|Yes| AGNOSTIC - Q3 -->|No| Q4 - Q4 -->|Yes| EMBASSY - Q4 -->|No| Q5 - Q5 -->|Yes| SMOL - Q5 -->|No| ASYNC_STD + Q1 -->|Да| Q2 + Q1 -->|Нет| Q3 + Q2 -->|Да| TOKIO + Q2 -->|Нет| Q5 + Q3 -->|Да| AGNOSTIC + Q3 -->|Нет| Q4 + Q4 -->|Да| EMBASSY + Q4 -->|Нет| Q5 + Q5 -->|Да| SMOL + Q5 -->|Нет| ASYNC_STD style TOKIO fill:#c8e6c9,color:#000 style SMOL fill:#bbdefb,color:#000 @@ -262,99 +262,97 @@ graph TD style AGNOSTIC fill:#bbdefb,color:#000 ``` -### Runtime Comparison Table +### Сравнительная таблица рантаймов -| Feature | tokio | async-std | smol | embassy | -|---------|-------|-----------|------|---------| -| **Ecosystem** | Dominant | Small | Minimal | Embedded | -| **Multi-threaded** | ✅ Work-stealing | ✅ | ✅ | ❌ (single-core) | +| Характеристика | tokio | async-std | smol | embassy | +|----------------|-------|-----------|------|---------| +| **Экосистема** | Доминирующая | Небольшая | Минимальная | Встраиваемая | +| **Многопоточность** | ✅ Work-stealing | ✅ | ✅ | ❌ (одно ядро) | | **no_std** | ❌ | ❌ | ❌ | ✅ | -| **Timer** | ✅ Built-in | ✅ Built-in | Via `async-io` | ✅ HAL-based | -| **I/O** | ✅ Own abstractions | ✅ std mirror | ✅ Via `async-io` | ✅ HAL drivers | -| **Channels** | ✅ Rich set | ✅ | Via `async-channel` | ✅ | -| **Learning curve** | Medium | Low | Low | High (HW) | -| **Binary size** | Large | Medium | Small | Tiny | +| **Таймер** | ✅ Встроенный | ✅ Встроенный | Через `async-io` | ✅ На основе HAL | +| **Ввод-вывод** | ✅ Собственные абстракции | ✅ Зеркало std | ✅ Через `async-io` | ✅ Драйверы HAL | +| **Каналы** | ✅ Богатый набор | ✅ | Через `async-channel` | ✅ | +| **Порог входа** | Средний | Низкий | Низкий | Высокий (железо) | +| **Размер бинарника** | Большой | Средний | Маленький | Крошечный |
-🏋️ Exercise: Runtime Comparison (click to expand) +🏋️ Упражнение: сравнение рантаймов (нажмите, чтобы раскрыть) -**Challenge**: Write the same program using three different runtimes (tokio, smol, and async-std). The program should: -1. Fetch a URL (simulate with a sleep) -2. Read a file (simulate with a sleep) -3. Print both results +**Задача**: напишите одну и ту же программу на трёх разных рантаймах (tokio, smol и async-std). Программа должна: +1. Получить URL (имитируется через sleep) +2. Прочитать файл (имитируется через sleep) +3. Вывести оба результата -This exercise demonstrates that the async/await code is the same — only the runtime setup differs. +Это упражнение показывает, что код async/await одинаков — меняется только настройка рантайма.
-🔑 Solution +🔑 Решение ```rust -// ----- tokio version ----- +// ----- версия для tokio ----- // Cargo.toml: tokio = { version = "1", features = ["full"] } #[tokio::main] async fn main() { let (url_result, file_result) = tokio::join!( async { tokio::time::sleep(std::time::Duration::from_millis(100)).await; - "Response from URL" + "Ответ с URL" }, async { tokio::time::sleep(std::time::Duration::from_millis(50)).await; - "Contents of file" + "Содержимое файла" }, ); - println!("URL: {url_result}, File: {file_result}"); + println!("URL: {url_result}, Файл: {file_result}"); } -// ----- smol version ----- +// ----- версия для smol ----- // Cargo.toml: smol = "2", futures-lite = "2" fn main() { smol::block_on(async { let (url_result, file_result) = futures_lite::future::zip( async { smol::Timer::after(std::time::Duration::from_millis(100)).await; - "Response from URL" + "Ответ с URL" }, async { smol::Timer::after(std::time::Duration::from_millis(50)).await; - "Contents of file" + "Содержимое файла" }, ).await; - println!("URL: {url_result}, File: {file_result}"); + println!("URL: {url_result}, Файл: {file_result}"); }); } -// ----- async-std version ----- +// ----- версия для async-std ----- // Cargo.toml: async-std = { version = "1", features = ["attributes"] } #[async_std::main] async fn main() { let (url_result, file_result) = futures::future::join( async { async_std::task::sleep(std::time::Duration::from_millis(100)).await; - "Response from URL" + "Ответ с URL" }, async { async_std::task::sleep(std::time::Duration::from_millis(50)).await; - "Contents of file" + "Содержимое файла" }, ).await; - println!("URL: {url_result}, File: {file_result}"); + println!("URL: {url_result}, Файл: {file_result}"); } ``` -**Key takeaway**: The async business logic is identical across runtimes. Only the entry point and timer/IO APIs differ. This is why writing runtime-agnostic libraries (using only `std::future::Future`) is valuable. +**Ключевой вывод**: асинхронная бизнес-логика одинакова во всех рантаймах. Различаются только точка входа и API таймеров/ввода-вывода. Поэтому написание библиотек, не привязанных к рантайму (использующих только `std::future::Future`), так ценно.
-> **Key Takeaways — Executors and Runtimes** -> - An executor's job: poll futures when woken, sleep efficiently using OS I/O APIs -> - **tokio** is the default for servers; **smol** for minimal footprint; **embassy** for embedded -> - Your business logic should depend on `std::future::Future`, not a specific runtime -> - io_uring (Linux 5.1+) is the future of high-perf I/O but the ecosystem is still maturing +> **Ключевые выводы — исполнители и рантаймы** +> - Работа исполнителя: опрашивать future при пробуждении и эффективно засыпать, используя API ввода-вывода ОС +> - **tokio** — выбор по умолчанию для серверов; **smol** — для минимального отпечатка; **embassy** — для встраиваемых систем +> - Ваша бизнес-логика должна зависеть от `std::future::Future`, а не от конкретного рантайма +> - io_uring (Linux 5.1+) — будущее высокопроизводительного ввода-вывода, но экосистема ещё созревает -> **See also:** [Ch 8 — Tokio Deep Dive](ch08-tokio-deep-dive.md) for tokio specifics, [Ch 9 — When Tokio Isn't the Right Fit](ch09-when-tokio-isnt-the-right-fit.md) for alternatives +> **См. также:** [Гл. 8 — Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) — специфика tokio, [Гл. 9 — Когда Tokio не подходит](ch09-when-tokio-isnt-the-right-fit.md) — альтернативы *** - - diff --git a/async-book/src/ch08-tokio-deep-dive.md b/async-book/src/ch08-tokio-deep-dive.md index ebe5c66..ac8315e 100644 --- a/async-book/src/ch08-tokio-deep-dive.md +++ b/async-book/src/ch08-tokio-deep-dive.md @@ -1,32 +1,32 @@ -# 8. Tokio Deep Dive 🟡 +# 8. Глубокое погружение в Tokio 🟡 -> **What you'll learn:** -> - Runtime flavors: multi-thread vs current-thread and when to use each -> - `tokio::spawn`, the `'static` requirement, and `JoinHandle` -> - Task cancellation semantics (cancel-on-drop) -> - Sync primitives: Mutex, RwLock, Semaphore, and all four channel types +> **Что вы узнаете:** +> - Варианты рантайма: multi-thread и current-thread и когда какой использовать +> - `tokio::spawn`, требование `'static` и `JoinHandle` +> - Семантика отмены задач (отмена при уничтожении) +> - Примитивы синхронизации: Mutex, RwLock, Semaphore и все четыре типа каналов -## Runtime Flavors: Multi-Thread vs Current-Thread +## Варианты рантайма: multi-thread и current-thread -Tokio offers two runtime configurations: +Tokio предлагает две конфигурации рантайма: ```rust -// Multi-threaded (default with #[tokio::main]) -// Uses a work-stealing thread pool — tasks can move between threads +// Многопоточный (по умолчанию с #[tokio::main]) +// Использует пул потоков с work-stealing — задачи могут переходить между потоками #[tokio::main] async fn main() { - // N worker threads (default = number of CPU cores) - // Tasks are Send + 'static + // N рабочих потоков (по умолчанию — количество ядер CPU) + // Задачи должны быть Send + 'static } -// Current-thread — everything runs on one thread +// Current-thread — всё выполняется в одном потоке #[tokio::main(flavor = "current_thread")] async fn main() { - // Single-threaded — tasks don't need to be Send - // Lighter weight, good for simple tools or WASM + // Однопоточный — задачам не нужен Send + // Легче по весу, подходит для простых утилит или WASM } -// Manual runtime construction: +// Ручное создание рантайма: let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(4) .enable_all() @@ -34,24 +34,24 @@ let rt = tokio::runtime::Builder::new_multi_thread() .unwrap(); rt.block_on(async { - println!("Running on custom runtime"); + println!("Работаем на своём рантайме"); }); ``` ```mermaid graph TB - subgraph "Multi-Thread (default)" - MT_Q1["Thread 1
Task A, Task D"] - MT_Q2["Thread 2
Task B"] - MT_Q3["Thread 3
Task C, Task E"] - STEAL["Work Stealing:
idle threads steal from busy ones"] + subgraph "Multi-Thread (по умолчанию)" + MT_Q1["Поток 1
Задача A, Задача D"] + MT_Q2["Поток 2
Задача B"] + MT_Q3["Поток 3
Задача C, Задача E"] + STEAL["Кража работы:
простаивающие потоки забирают задачи у занятых"] MT_Q1 <--> STEAL MT_Q2 <--> STEAL MT_Q3 <--> STEAL end subgraph "Current-Thread" - ST_Q["Single Thread
Task A → Task B → Task C → Task D"] + ST_Q["Один поток
Задача A → Задача B → Задача C → Задача D"] end style MT_Q1 fill:#c8e6c9,color:#000 @@ -60,9 +60,9 @@ graph TB style ST_Q fill:#bbdefb,color:#000 ``` -### tokio::spawn and the 'static Requirement +### tokio::spawn и требование 'static -`tokio::spawn` puts a future onto the runtime's task queue. Because it might run on *any* worker thread at *any* time, the future must be `Send + 'static`: +`tokio::spawn` помещает future в очередь задач рантайма. Поскольку он может выполняться на *любом* рабочем потоке в *любой* момент, future должен быть `Send + 'static`: ```rust use tokio::task; @@ -70,49 +70,49 @@ use tokio::task; async fn example() { let data = String::from("hello"); - // ✅ Works: move ownership into the task + // ✅ Работает: передаём владение в задачу let handle = task::spawn(async move { println!("{data}"); data.len() }); let len = handle.await.unwrap(); - println!("Length: {len}"); + println!("Длина: {len}"); } async fn problem() { let data = String::from("hello"); - // ❌ FAILS: data is borrowed, not 'static + // ❌ НЕ РАБОТАЕТ: data заимствован, а не 'static // task::spawn(async { - // println!("{data}"); // borrows `data` — not 'static + // println!("{data}"); // заимствует `data` — не 'static // }); - // ❌ FAILS: Rc is not Send + // ❌ НЕ РАБОТАЕТ: Rc не является Send // let rc = std::rc::Rc::new(42); // task::spawn(async move { - // println!("{rc}"); // Rc is !Send — can't cross thread boundary + // println!("{rc}"); // Rc — !Send, нельзя пересечь границу потока // }); } ``` -**Why `'static`?** The spawned task runs independently — it might outlive the scope that created it. The compiler can't prove the references will remain valid, so it requires owned data. +**Почему `'static`?** Порождённая задача выполняется независимо — она может пережить область видимости, в которой была создана. Компилятор не может доказать, что ссылки останутся действительными, поэтому требуются данные, которыми задача владеет. -**Why `Send`?** The task might be resumed on a different thread than where it was suspended. All data held across `.await` points must be safe to send between threads. +**Почему `Send`?** Задачу могут возобновить на другом потоке, а не на том, где её приостановили. Все данные, которые хранятся через точки `.await`, должны безопасно передаваться между потоками. ```rust -// Common pattern: clone shared data into the task +// Типичный паттерн: клонируем общие данные в задачу let shared = Arc::new(config); for i in 0..10 { - let shared = Arc::clone(&shared); // Clone the Arc, not the data + let shared = Arc::clone(&shared); // Клонируем Arc, а не данные tokio::spawn(async move { process_item(i, &shared).await; }); } ``` -### JoinHandle and Task Cancellation +### JoinHandle и отмена задач ```rust use tokio::task::JoinHandle; @@ -124,44 +124,44 @@ async fn cancellation_example() { "completed".to_string() }); - // Cancel the task by dropping the handle? NO — task keeps running! - // drop(handle); // Task continues in the background + // Отменить задачу, уничтожив handle? НЕТ — задача продолжит работать! + // drop(handle); // Задача продолжает выполняться в фоне - // To actually cancel, call abort(): + // Чтобы действительно отменить, вызовите abort(): handle.abort(); - // Awaiting an aborted task returns JoinError + // Ожидание отменённой задачи возвращает JoinError match handle.await { - Ok(val) => println!("Got: {val}"), - Err(e) if e.is_cancelled() => println!("Task was cancelled"), - Err(e) => println!("Task panicked: {e}"), + Ok(val) => println!("Получено: {val}"), + Err(e) if e.is_cancelled() => println!("Задача отменена"), + Err(e) => println!("Задача завершилась паникой: {e}"), } } ``` -> **Important**: Dropping a `JoinHandle` does NOT cancel the task in tokio. -> The task becomes *detached* and keeps running. You must explicitly call -> `.abort()` to cancel it. This is different from dropping a `Future` directly, -> which does cancel/drop the underlying computation. +> **Важно**: уничтожение `JoinHandle` НЕ отменяет задачу в tokio. +> Задача становится *отсоединённой* и продолжает работать. Чтобы её отменить, нужно явно вызвать +> `.abort()`. Это отличается от уничтожения `Future` напрямую, +> которое действительно отменяет (уничтожает) базовое вычисление. -### Tokio Sync Primitives +### Примитивы синхронизации Tokio -Tokio provides async-aware synchronization primitives. The key principle: **don't use `std::sync::Mutex` across `.await` points**. +Tokio предоставляет примитивы синхронизации, которые понимают async. Ключевой принцип: **не используйте `std::sync::Mutex` через точки `.await`**. ```rust use tokio::sync::{Mutex, RwLock, Semaphore, mpsc, oneshot, broadcast, watch}; // --- Mutex --- -// Async mutex: the lock() method is async and won't block the thread +// Асинхронный мьютекс: метод lock() асинхронный и не блокирует поток let data = Arc::new(Mutex::new(vec![1, 2, 3])); { - let mut guard = data.lock().await; // Non-blocking lock + let mut guard = data.lock().await; // Неблокирующая блокировка guard.push(4); -} // Guard dropped here — lock released +} // Guard уничтожен здесь — блокировка снята -// --- Channels --- -// mpsc: Multiple producer, single consumer -let (tx, mut rx) = mpsc::channel::(100); // Bounded buffer +// --- Каналы --- +// mpsc: несколько отправителей, один получатель +let (tx, mut rx) = mpsc::channel::(100); // Буфер ограниченного размера tokio::spawn(async move { tx.send("hello".into()).await.unwrap(); @@ -169,76 +169,76 @@ tokio::spawn(async move { let msg = rx.recv().await.unwrap(); -// oneshot: Single value, single consumer +// oneshot: одно значение, один получатель let (tx, rx) = oneshot::channel::(); -tx.send(42).unwrap(); // No await needed — either sends or fails +tx.send(42).unwrap(); // await не нужен — либо отправляет, либо ошибка let val = rx.await.unwrap(); -// broadcast: Multiple producers, multiple consumers (all get every message) +// broadcast: несколько отправителей, несколько получателей (каждый получает все сообщения) let (tx, _) = broadcast::channel::(100); let mut rx1 = tx.subscribe(); let mut rx2 = tx.subscribe(); -// watch: Single value, multiple consumers (only latest value) +// watch: одно значение, несколько получателей (только последнее значение) let (tx, rx) = watch::channel(0u64); tx.send(42).unwrap(); -println!("Latest: {}", *rx.borrow()); +println!("Последнее значение: {}", *rx.borrow()); ``` -> **Note:** `.unwrap()` is used for brevity throughout these channel examples. -> In production, handle send/receive errors gracefully — a failed `.send()` means -> the receiver was dropped, and a failed `.recv()` means the channel is closed. +> **Примечание:** `.unwrap()` в примерах с каналами используется для краткости. +> В продакшене обрабатывайте ошибки отправки и получения аккуратно: неудачный `.send()` означает, +> что получатель уже уничтожен, а неудачный `.recv()` — что канал закрыт. ```mermaid graph LR - subgraph "Channel Types" + subgraph "Типы каналов" direction TB - MPSC["mpsc
N→1
Buffered queue"] - ONESHOT["oneshot
1→1
Single value"] - BROADCAST["broadcast
N→N
All receivers get all"] - WATCH["watch
1→N
Latest value only"] + MPSC["mpsc
N→1
Буферизованная очередь"] + ONESHOT["oneshot
1→1
Одно значение"] + BROADCAST["broadcast
N→N
Все получатели получают всё"] + WATCH["watch
1→N
Только последнее значение"] end - P1["Producer 1"] --> MPSC - P2["Producer 2"] --> MPSC - MPSC --> C1["Consumer"] + P1["Отправитель 1"] --> MPSC + P2["Отправитель 2"] --> MPSC + MPSC --> C1["Получатель"] - P3["Producer"] --> ONESHOT - ONESHOT --> C2["Consumer"] + P3["Отправитель"] --> ONESHOT + ONESHOT --> C2["Получатель"] - P4["Producer"] --> BROADCAST - BROADCAST --> C3["Consumer 1"] - BROADCAST --> C4["Consumer 2"] + P4["Отправитель"] --> BROADCAST + BROADCAST --> C3["Получатель 1"] + BROADCAST --> C4["Получатель 2"] - P5["Producer"] --> WATCH - WATCH --> C5["Consumer 1"] - WATCH --> C6["Consumer 2"] + P5["Отправитель"] --> WATCH + WATCH --> C5["Получатель 1"] + WATCH --> C6["Получатель 2"] ``` -## Case Study: Choosing the Right Channel for a Notification Service +## Разбор кейса: выбор канала для сервиса уведомлений -You're building a notification service where: -- Multiple API handlers produce events -- A single background task batches and sends them -- A config watcher updates rate limits at runtime -- A shutdown signal must reach all components +Вы создаёте сервис уведомлений, где: +- Несколько обработчиков API порождают события +- Одна фоновая задача группирует их в пакеты и отправляет +- Наблюдатель за конфигурацией обновляет лимиты скорости во время работы +- Сигнал остановки должен дойти до всех компонентов -**Which channels for each?** +**Какой канал для каждого случая?** -| Requirement | Channel | Why | -|-------------|---------|-----| -| API handlers → Batcher | `mpsc` (bounded) | N producers, 1 consumer. Bounded for backpressure — if the batcher falls behind, API handlers slow down instead of OOM | -| Config watcher → Rate limiter | `watch` | Only the latest config matters. Multiple readers (each worker) see the current value | -| Shutdown signal → All components | `broadcast` | Every component must receive the shutdown notification independently | -| Single health-check response | `oneshot` | Request/response pattern — one value, then done | +| Требование | Канал | Почему | +|------------|-------|--------| +| Обработчики API → Batcher | `mpsc` (ограниченный) | N отправителей, 1 получатель. Ограничение нужно для backpressure — если batcher отстаёт, обработчики API замедляются вместо переполнения памяти (OOM) | +| Наблюдатель конфигурации → Ограничитель скорости | `watch` | Важна только последняя конфигурация. Несколько читателей (каждый воркер) видят текущее значение | +| Сигнал остановки → Все компоненты | `broadcast` | Каждый компонент должен независимо получить уведомление об остановке | +| Ответ на единичную проверку здоровья | `oneshot` | Шаблон запрос/ответ — одно значение, и всё | ```mermaid graph LR - subgraph "Notification Service" + subgraph "Сервис уведомлений" direction TB - API1["API Handler 1"] -->|mpsc| BATCH["Batcher"] - API2["API Handler 2"] -->|mpsc| BATCH - CONFIG["Config Watcher"] -->|watch| RATE["Rate Limiter"] + API1["Обработчик API 1"] -->|mpsc| BATCH["Batcher"] + API2["Обработчик API 2"] -->|mpsc| BATCH + CONFIG["Наблюдатель конфигурации"] -->|watch| RATE["Ограничитель скорости"] CTRL["Ctrl+C"] -->|broadcast| API1 CTRL -->|broadcast| BATCH CTRL -->|broadcast| RATE @@ -253,12 +253,12 @@ graph LR ```
-🏋️ Exercise: Build a Task Pool (click to expand) +🏋️ Упражнение: пул задач (нажмите, чтобы раскрыть) -**Challenge**: Build a function `run_with_limit` that accepts a list of async closures and a concurrency limit, executing at most N tasks simultaneously. Use `tokio::sync::Semaphore`. +**Задача**: реализуйте функцию `run_with_limit`, которая принимает список асинхронных замыканий и лимит конкурентности и выполняет не более N задач одновременно. Используйте `tokio::sync::Semaphore`.
-🔑 Solution +🔑 Решение ```rust use std::future::Future; @@ -278,7 +278,7 @@ where let permit = Arc::clone(&semaphore); let handle = tokio::spawn(async move { let _permit = permit.acquire().await.unwrap(); - // Permit is held while task runs, then dropped + // Разрешение удерживается, пока выполняется задача, затем освобождается task().await }); handles.push(handle); @@ -291,26 +291,24 @@ where results } -// Usage: +// Использование: // let tasks: Vec<_> = urls.into_iter().map(|url| { // move || async move { fetch(url).await } // }).collect(); -// let results = run_with_limit(tasks, 10).await; // Max 10 concurrent +// let results = run_with_limit(tasks, 10).await; // Не более 10 одновременно ``` -**Key takeaway**: `Semaphore` is the standard way to limit concurrency in tokio. Each task acquires a permit before starting work. When the semaphore is full, new tasks wait asynchronously (non-blocking) until a slot opens. +**Ключевой вывод**: `Semaphore` — стандартный способ ограничить конкурентность в tokio. Каждая задача получает разрешение перед началом работы. Когда семафор заполнен, новые задачи асинхронно (без блокировки потока) ждут, пока не освободится слот.
-> **Key Takeaways — Tokio Deep Dive** -> - Use `multi_thread` for servers (default); `current_thread` for CLI tools, tests, or `!Send` types -> - `tokio::spawn` requires `'static` futures — use `Arc` or channels to share data -> - Dropping a `JoinHandle` does **not** cancel the task — call `.abort()` explicitly -> - Choose sync primitives by need: `Mutex` for shared state, `Semaphore` for concurrency limits, `mpsc`/`oneshot`/`broadcast`/`watch` for communication +> **Ключевые выводы — глубокое погружение в Tokio** +> - Используйте `multi_thread` для серверов (по умолчанию); `current_thread` — для CLI-утилит, тестов или типов `!Send` +> - `tokio::spawn` требует future типа `'static` — для совместного использования данных применяйте `Arc` или каналы +> - Уничтожение `JoinHandle` **не** отменяет задачу — вызывайте `.abort()` явно +> - Выбирайте примитивы синхронизации по потребности: `Mutex` для общего состояния, `Semaphore` для лимитов конкурентности, `mpsc`/`oneshot`/`broadcast`/`watch` для обмена сообщениями -> **See also:** [Ch 9 — When Tokio Isn't the Right Fit](ch09-when-tokio-isnt-the-right-fit.md) for alternatives to spawn, [Ch 12 — Common Pitfalls](ch12-common-pitfalls.md) for MutexGuard-across-await bugs +> **См. также:** [Гл. 9 — Когда Tokio не подходит](ch09-when-tokio-isnt-the-right-fit.md) — альтернативы spawn, [Гл. 12 — Типичные ловушки](ch12-common-pitfalls.md) — ошибки с MutexGuard через await *** - - diff --git a/async-book/src/ch09-when-tokio-isnt-the-right-fit.md b/async-book/src/ch09-when-tokio-isnt-the-right-fit.md index 9fa69ee..aef4574 100644 --- a/async-book/src/ch09-when-tokio-isnt-the-right-fit.md +++ b/async-book/src/ch09-when-tokio-isnt-the-right-fit.md @@ -1,22 +1,22 @@ -# 9. When Tokio Isn't the Right Fit 🟡 +# 9. Когда Tokio не подходит 🟡 -> **What you'll learn:** -> - The `'static` problem: when `tokio::spawn` forces you into `Arc` everywhere -> - `LocalSet` for `!Send` futures -> - `FuturesUnordered` for borrow-friendly concurrency (no spawn needed) -> - `JoinSet` for managed task groups -> - Writing runtime-agnostic libraries +> **Что вы узнаете:** +> - Проблему `'static`: когда `tokio::spawn` заставляет использовать `Arc` повсюду +> - `LocalSet` для фьючей типа `!Send` +> - `FuturesUnordered` для конкурентности, дружественной к заимствованиям (без spawn) +> - `JoinSet` для управляемых групп задач +> - Написание библиотек, не привязанных к рантайму ```mermaid graph TD - START["Need concurrent futures?"] --> STATIC{"Can futures be 'static?"} - STATIC -->|Yes| SEND{"Are futures Send?"} - STATIC -->|No| FU["FuturesUnordered
Runs on current task"] - SEND -->|Yes| SPAWN["tokio::spawn
Multi-threaded"] - SEND -->|No| LOCAL["LocalSet
Single-threaded"] - SPAWN --> MANAGE{"Need to track/abort tasks?"} - MANAGE -->|Yes| JOINSET["JoinSet / TaskTracker"] - MANAGE -->|No| HANDLE["JoinHandle"] + START["Нужны конкурентные фьючи?"] --> STATIC{"Фьючи могут быть 'static?"} + STATIC -->|Да| SEND{"Фьючи являются Send?"} + STATIC -->|Нет| FU["FuturesUnordered
Выполняется в текущей задаче"] + SEND -->|Да| SPAWN["tokio::spawn
Многопоточный"] + SEND -->|Нет| LOCAL["LocalSet
Однопоточный"] + SPAWN --> MANAGE{"Нужно отслеживать/прерывать задачи?"} + MANAGE -->|Да| JOINSET["JoinSet / TaskTracker"] + MANAGE -->|Нет| HANDLE["JoinHandle"] style START fill:#f5f5f5,stroke:#333,color:#000 style FU fill:#d4efdf,stroke:#27ae60,color:#000 @@ -26,20 +26,20 @@ graph TD style HANDLE fill:#e8f4f8,stroke:#2980b9,color:#000 ``` -## The 'static Future Problem +## Проблема 'static у фьючей -Tokio's `spawn` requires `'static` futures. This means you can't borrow local data in spawned tasks: +`spawn` в Tokio требует future типа `'static`. Это значит, что в порождённых задачах нельзя заимствовать локальные данные: ```rust async fn process_items(items: &[String]) { - // ❌ Can't do this — items is borrowed, not 'static + // ❌ Так нельзя — items заимствован, а не 'static // for item in items { // tokio::spawn(async { // process(item).await; // }); // } - // 😐 Workaround 1: Clone everything + // 😐 Обходной путь 1: клонировать всё for item in items { let item = item.clone(); tokio::spawn(async move { @@ -47,7 +47,7 @@ async fn process_items(items: &[String]) { }); } - // 😐 Workaround 2: Use Arc + // 😐 Обходной путь 2: использовать Arc let items = Arc::new(items.to_vec()); for i in 0..items.len() { let items = Arc::clone(&items); @@ -58,57 +58,57 @@ async fn process_items(items: &[String]) { } ``` -This is annoying! In Go, you can just `go func() { use(item) }` with a closure. In Rust, the ownership system forces you to think about who owns what and how long it lives. +Это раздражает! В Go можно просто написать `go func() { use(item) }` с замыканием. В Rust система владения заставляет думать о том, кому что принадлежит и как долго это живёт. -### Alternatives to `tokio::spawn` +### Альтернативы `tokio::spawn` -Not every problem requires `spawn`. Here are three tools that each solve a -*different* constraint: +Не каждая задача требует `spawn`. Вот три инструмента, каждый из которых решает +*другое* ограничение: ```rust -// 1. FuturesUnordered — avoids 'static entirely (no spawn!) +// 1. FuturesUnordered — полностью обходит 'static (без spawn!) use futures::stream::{FuturesUnordered, StreamExt}; async fn process_items(items: &[String]) { let futures: FuturesUnordered<_> = items .iter() .map(|item| async move { - // ✅ Can borrow item — no spawn, no 'static needed! + // ✅ Может заимствовать item — ни spawn, ни 'static не нужны! process(item).await }) .collect(); - // Drive all futures to completion + // Доводим все фьючи до завершения futures.for_each(|result| async move { - println!("Result: {result:?}"); + println!("Результат: {result:?}"); }).await; } -// 2. tokio::task::LocalSet — run !Send futures on current thread -// ⚠️ Still requires 'static — solves Send, not 'static +// 2. tokio::task::LocalSet — запускает фьючи !Send в текущем потоке +// ⚠️ Всё ещё требует 'static — решает Send, а не 'static use tokio::task::LocalSet; let local_set = LocalSet::new(); local_set.run_until(async { tokio::task::spawn_local(async { - // Can use Rc, Cell, and other !Send types here + // Здесь можно использовать Rc, Cell и другие типы !Send let rc = std::rc::Rc::new(42); println!("{rc}"); }).await.unwrap(); }).await; -// 3. tokio JoinSet (tokio 1.21+) — managed set of spawned tasks -// ⚠️ Still requires 'static + Send — solves task *management*, -// not the 'static problem. Useful for tracking, aborting, and -// joining a dynamic group of tasks. +// 3. JoinSet в tokio (tokio 1.21+) — управляемый набор порождённых задач +// ⚠️ Всё ещё требует 'static + Send — решает управление задачами, +// а не проблему 'static. Полезен для отслеживания, прерывания и +// ожидания динамической группы задач. use tokio::task::JoinSet; async fn with_joinset() { let mut set = JoinSet::new(); for i in 0..10 { - // i is Copy and moved into the closure — already 'static. - // You'd still need Arc or clone for borrowed data. + // i — Copy и перемещается в замыкание, поэтому он уже 'static. + // Для заимствованных данных всё равно понадобятся Arc или clone. set.spawn(async move { tokio::time::sleep(Duration::from_millis(100)).await; i * 2 @@ -116,37 +116,37 @@ async fn with_joinset() { } while let Some(result) = set.join_next().await { - println!("Task completed: {:?}", result.unwrap()); + println!("Задача завершена: {:?}", result.unwrap()); } } ``` -> **Which tool solves which problem?** +> **Какой инструмент решает какую проблему?** > -> | Constraint you hit | Tool | Avoids `'static`? | Avoids `Send`? | +> | Ограничение, с которым вы столкнулись | Инструмент | Обходит `'static`? | Обходит `Send`? | > |---|---|---|---| -> | Can't make futures `'static` | `FuturesUnordered` | ✅ Yes | ✅ Yes | -> | Futures are `'static` but `!Send` | `LocalSet` | ❌ No | ✅ Yes | -> | Need to track / abort spawned tasks | `JoinSet` | ❌ No | ❌ No | +> | Нельзя сделать фьючи `'static` | `FuturesUnordered` | ✅ Да | ✅ Да | +> | Фьючи `'static`, но `!Send` | `LocalSet` | ❌ Нет | ✅ Да | +> | Нужно отслеживать / прерывать порождённые задачи | `JoinSet` | ❌ Нет | ❌ Нет | -### Lightweight Runtimes for Libraries +### Лёгкие рантаймы для библиотек -If you're writing a library — don't force users into tokio: +Если вы пишете библиотеку — не заставляйте пользователей использовать tokio: ```rust -// ❌ BAD: Library forces tokio on users +// ❌ ПЛОХО: библиотека навязывает tokio пользователям pub async fn my_lib_function() { tokio::time::sleep(Duration::from_secs(1)).await; - // Now your users MUST use tokio + // Теперь твои пользователи ОБЯЗАНЫ использовать tokio } -// ✅ GOOD: Library is runtime-agnostic +// ✅ ХОРОШО: библиотека не привязана к рантайму pub async fn my_lib_function() { - // Use only types from std::future and futures crate + // Используем только типы из std::future и крейта futures do_computation().await; } -// ✅ GOOD: Accept a generic future for I/O operations +// ✅ ХОРОШО: принимаем обобщённую фьючу для операций ввода-вывода pub async fn fetch_with_retry( operation: F, max_retries: usize, @@ -166,29 +166,29 @@ where } ``` -> **Rule of thumb**: Libraries should depend on `futures` crate, not `tokio`. -> Applications should depend on `tokio` (or their chosen runtime). -> This keeps the ecosystem composable. +> **Эмпирическое правило**: библиотеки должны зависеть от крейта `futures`, а не от `tokio`. +> Приложения должны зависеть от `tokio` (или от выбранного рантайма). +> Так экосистема остаётся компонуемой.
-🏋️ Exercise: FuturesUnordered vs Spawn (click to expand) +🏋️ Упражнение: FuturesUnordered против spawn (нажмите, чтобы раскрыть) -**Challenge**: Write the same function two ways — once using `tokio::spawn` (requires `'static`) and once using `FuturesUnordered` (borrows data). The function receives `&[String]` and returns the length of each string after a simulated async lookup. +**Задача**: напишите одну и ту же функцию двумя способами — один раз с `tokio::spawn` (требует `'static`) и один раз с `FuturesUnordered` (заимствует данные). Функция получает `&[String]` и возвращает длину каждой строки после имитации асинхронного поиска. -Compare: Which approach requires `.clone()`? Which can borrow the input slice? +Сравните: какой подход требует `.clone()`? Какой может заимствовать входной срез?
-🔑 Solution +🔑 Решение ```rust use futures::stream::{FuturesUnordered, StreamExt}; use tokio::time::{sleep, Duration}; -// Version 1: tokio::spawn — requires 'static, must clone +// Версия 1: tokio::spawn — требует 'static, нужно клонирование async fn lengths_with_spawn(items: &[String]) -> Vec { let mut handles = Vec::new(); for item in items { - let owned = item.clone(); // Must clone — spawn requires 'static + let owned = item.clone(); // Обязательно клонируем — spawn требует 'static handles.push(tokio::spawn(async move { sleep(Duration::from_millis(10)).await; owned.len() @@ -202,13 +202,13 @@ async fn lengths_with_spawn(items: &[String]) -> Vec { results } -// Version 2: FuturesUnordered — borrows data, no clone needed +// Версия 2: FuturesUnordered — заимствует данные, клонирование не нужно async fn lengths_without_spawn(items: &[String]) -> Vec { let futures: FuturesUnordered<_> = items .iter() .map(|item| async move { sleep(Duration::from_millis(10)).await; - item.len() // ✅ Borrows item — no clone! + item.len() // ✅ Заимствует item — без клонирования! }) .collect(); @@ -220,29 +220,27 @@ async fn test_both_versions() { let items = vec!["hello".into(), "world".into(), "rust".into()]; let v1 = lengths_with_spawn(&items).await; - // Note: v1 preserves insertion order (sequential join) + // Примечание: v1 сохраняет порядок вставки (последовательное ожидание) let mut v2 = lengths_without_spawn(&items).await; - v2.sort(); // FuturesUnordered returns in completion order + v2.sort(); // FuturesUnordered возвращает результаты в порядке завершения assert_eq!(v1, vec![5, 5, 4]); assert_eq!(v2, vec![4, 5, 5]); } ``` -**Key takeaway**: `FuturesUnordered` avoids the `'static` requirement by running all futures on the current task (no thread migration). The trade-off: all futures share one task — if one blocks, the others stall. Use `spawn` for CPU-heavy work that should run on separate threads. +**Ключевой вывод**: `FuturesUnordered` обходит требование `'static`, выполняя все фьючи в текущей задаче (без миграции между потоками). Цена: все фьючи делят одну задачу — если один блокируется, остальные стоят. Для тяжёлых по CPU вычислений, которые должны идти в отдельных потоках, используйте `spawn`.
-> **Key Takeaways — When Tokio Isn't the Right Fit** -> - `FuturesUnordered` runs futures concurrently on the current task — no `'static` requirement -> - `LocalSet` enables `!Send` futures on a single-threaded executor -> - `JoinSet` (tokio 1.21+) provides managed task groups with automatic cleanup -> - For libraries: depend only on `std::future::Future` + `futures` crate, not tokio directly +> **Ключевые выводы — когда Tokio не подходит** +> - `FuturesUnordered` выполняет фьючи конкурентно в текущей задаче — требования `'static` нет +> - `LocalSet` позволяет запускать фьючи `!Send` в однопоточном исполнителе +> - `JoinSet` (tokio 1.21+) предоставляет управляемые группы задач с автоматической очисткой +> - Для библиотек: зависьте только от `std::future::Future` и крейта `futures`, а не напрямую от tokio -> **See also:** [Ch 8 — Tokio Deep Dive](ch08-tokio-deep-dive.md) for when spawn is the right tool, [Ch 11 — Streams](ch11-streams-and-asynciterator.md) for `buffer_unordered()` as another concurrency limiter +> **См. также:** [Гл. 8 — Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) — когда spawn — правильный инструмент, [Гл. 11 — Потоки](ch11-streams-and-asynciterator.md) — `buffer_unordered()` как ещё один ограничитель конкурентности *** - - From e044e4a9987d1be98651548bce116a9e81c57fb4 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:54:28 +0400 Subject: [PATCH 08/18] Translate Async Rust book chapters 10-13 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for async traits, streams, common pitfalls and production patterns. Code identifiers, crate names, file names and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- async-book/src/ch10-async-traits.md | 170 ++++---- .../src/ch11-streams-and-asynciterator.md | 174 ++++---- async-book/src/ch12-common-pitfalls.md | 401 +++++++++--------- async-book/src/ch13-production-patterns.md | 264 ++++++------ 4 files changed, 497 insertions(+), 512 deletions(-) diff --git a/async-book/src/ch10-async-traits.md b/async-book/src/ch10-async-traits.md index e2a6988..5a8d498 100644 --- a/async-book/src/ch10-async-traits.md +++ b/async-book/src/ch10-async-traits.md @@ -1,24 +1,24 @@ -# 10. Async Traits 🟡 +# 10. Async-трейты 🟡 -> **What you'll learn:** -> - Why async methods in traits took years to stabilize -> - RPITIT: native async trait methods (Rust 1.75+) -> - The dyn dispatch challenge and `Send` bounds via `trait_variant` -> - Async closures (Rust 1.85+): `async Fn()` and `async FnOnce()` +> **Что вы узнаете:** +> - Почему асинхронные методы в трейтах стабилизировались так долго +> - RPITIT: нативные асинхронные методы трейтов (Rust 1.75+) +> - Проблему динамической диспетчеризации и ограничения `Send` через `trait_variant` +> - Асинхронные замыкания (Rust 1.85+): `async Fn()` и `async FnOnce()` ```mermaid graph TD - subgraph "Async Trait Approaches" + subgraph "Подходы к async-трейтам" direction TB - RPITIT["RPITIT (Rust 1.75+)
async fn in trait
Static dispatch only"] - VARIANT["trait_variant
Auto-generates Send variant
Static dispatch only"] - BOXED["Box<dyn Future>
Manual boxing
Works everywhere"] - CLOSURE["Async Closures (1.85+)
async Fn() / async FnOnce()
Callbacks & middleware"] + RPITIT["RPITIT (Rust 1.75+)
async fn в трейте
Только статическая диспетчеризация"] + VARIANT["trait_variant
Автоматически генерирует вариант с Send
Только статическая диспетчеризация"] + BOXED["Box<dyn Future>
Ручное боксирование
Работает везде"] + CLOSURE["Асинхронные замыкания (1.85+)
async Fn() / async FnOnce()
Колбэки и middleware"] end - RPITIT -->|"Need Send?"| VARIANT - RPITIT -->|"Need dyn?"| BOXED - CLOSURE -->|"Replaces"| BOXED + RPITIT -->|"Нужен Send?"| VARIANT + RPITIT -->|"Нужен dyn?"| BOXED + CLOSURE -->|"Заменяет"| BOXED style RPITIT fill:#d4efdf,stroke:#27ae60,color:#000 style VARIANT fill:#e8f4f8,stroke:#2980b9,color:#000 @@ -26,29 +26,29 @@ graph TD style CLOSURE fill:#e8daef,stroke:#8e44ad,color:#000 ``` -## The History: Why It Took So Long +## История: почему это заняло так много времени -Async methods in traits were Rust's most requested feature for years. The problem: +Асинхронные методы в трейтах многие годы были самой востребованной функцией Rust. Проблема: ```rust -// This didn't compile until Rust 1.75 (Dec 2023): +// Это не компилировалось до Rust 1.75 (декабрь 2023): trait DataStore { async fn get(&self, key: &str) -> Option; } -// Why? Because async fn returns `impl Future`, -// and `impl Trait` in trait return position wasn't supported. +// Почему? Потому что async fn возвращает `impl Future`, +// а `impl Trait` в позиции возвращаемого значения трейта не поддерживался. ``` -The fundamental challenge: when a trait method returns `impl Future`, each implementor returns a *different concrete type*. The compiler needs to know the size of the return type, but trait methods are dynamically dispatched. +Основная сложность: когда метод трейта возвращает `impl Future`, каждый реализующий тип возвращает *свой конкретный тип*. Компилятору нужно знать размер возвращаемого типа, но методы трейтов диспетчеризуются динамически. ### RPITIT: Return Position Impl Trait in Trait -Since Rust 1.75, this just works for static dispatch: +С Rust 1.75 это просто работает для статической диспетчеризации: ```rust trait DataStore { async fn get(&self, key: &str) -> Option; - // Desugars to: + // Эквивалентно: // fn get(&self, key: &str) -> impl Future>; } @@ -62,7 +62,7 @@ impl DataStore for InMemoryStore { } } -// ✅ Works with generics (static dispatch): +// ✅ Работает с обобщёнными типами (статическая диспетчеризация): async fn lookup(store: &S, key: &str) { if let Some(val) = store.get(key).await { println!("{key} = {val}"); @@ -70,51 +70,51 @@ async fn lookup(store: &S, key: &str) { } ``` -### dyn Dispatch and Send Bounds +### Динамическая диспетчеризация и ограничения Send -The limitation: you can't use `dyn DataStore` directly because the compiler doesn't know the size of the returned future: +Ограничение: нельзя напрямую использовать `dyn DataStore`, потому что компилятор не знает размер возвращаемого future: ```rust -// ❌ Doesn't work: +// ❌ Не работает: // async fn lookup_dyn(store: &dyn DataStore, key: &str) { ... } -// Error: the trait `DataStore` is not dyn-compatible because method `get` -// is `async` +// Ошибка: трейт `DataStore` не является dyn-совместимым, потому что метод `get` +// является `async` -// ✅ Workaround: Return a boxed future +// ✅ Обходной путь: возвращаем упакованный (boxed) future trait DynDataStore { fn get(&self, key: &str) -> Pin> + Send + '_>>; } ``` -**The Send problem**: In multi-threaded runtimes, spawned tasks must be `Send`. But async trait methods don't automatically add `Send` bounds: +**Проблема Send**: в многопоточных рантаймах порождённые задачи должны быть `Send`. Но асинхронные методы трейтов не добавляют ограничение `Send` автоматически: ```rust trait Worker { - async fn run(self); // Future might or might not be Send + async fn run(self); // Future может быть Send, а может и не быть } struct MyWorker; impl Worker for MyWorker { async fn run(self) { - // If this uses !Send types, the future is !Send + // Если здесь используются типы !Send, future тоже !Send let rc = std::rc::Rc::new(42); some_work().await; println!("{rc}"); } } -// ❌ This fails because the future is !Send (Rc is !Send): -// tokio::spawn(worker.run()); // Requires Send + 'static +// ❌ Это не сработает, потому что future — !Send (Rc — !Send): +// tokio::spawn(worker.run()); // Требует Send + 'static // -// Note: We use `self` (owned) here because tokio::spawn also -// requires 'static — a future borrowing &self can't be 'static. -// Even without Rc, `async fn run(&self)` wouldn't be spawnable. +// Примечание: здесь мы используем `self` (владение), потому что tokio::spawn +// также требует 'static — future, заимствующий &self, не может быть 'static. +// Даже без Rc `async fn run(&self)` нельзя было бы порождать. ``` -### The trait_variant Crate +### Крейт trait_variant -The `trait_variant` crate (from the Rust async working group) generates a `Send` variant automatically: +Крейт `trait_variant` (из рабочей группы по async в Rust) автоматически генерирует вариант с `Send`: ```rust // Cargo.toml: trait-variant = "0.1" @@ -125,63 +125,63 @@ trait DataStore { async fn set(&self, key: &str, value: String); } -// Now you have two traits: -// - DataStore: no Send bound on the futures -// - SendDataStore: all futures are Send -// Both have the same methods, implementors implement DataStore -// and get SendDataStore for free if their futures are Send. +// Теперь у вас два трейта: +// - DataStore: без ограничения Send для future +// - SendDataStore: все future являются Send +// У обоих одинаковые методы; реализующие типы реализуют DataStore +// и получают SendDataStore бесплатно, если их future являются Send. -// Use SendDataStore when you need to spawn tasks: +// Используйте SendDataStore, когда нужно порождать задачи: async fn spawn_lookup(store: Arc) { tokio::spawn(async move { store.get("key").await; }); } -// ⚠️ Note: trait_variant does NOT enable dyn dispatch. -// The generated trait still uses `impl Future`, so `dyn SendDataStore` -// is not dyn compatible. For dyn dispatch, you still need manual boxing -// (see the Box::pin approach above) or the `async-trait` crate. +// ⚠️ Важно: trait_variant НЕ включает динамическую диспетчеризацию. +// Сгенерированный трейт по-прежнему использует `impl Future`, поэтому `dyn SendDataStore` +// не является dyn-совместимым. Для dyn-диспетчеризации всё ещё нужно ручное боксирование +// (см. подход с Box::pin выше) или крейт `async-trait`. ``` -### Quick Reference: Async Traits +### Краткая справка: async-трейты -| Approach | Static Dispatch | Dynamic Dispatch | Send | Syntax Overhead | -|----------|:---:|:---:|:---:|---| -| Native `async fn` in trait | ✅ | ❌ | Implicit | None | -| `trait_variant` | ✅ | ❌ | Explicit | `#[trait_variant::make]` | -| Manual `Box::pin` | ✅ | ✅ | Explicit | High | -| `async-trait` crate | ✅ | ✅ | `#[async_trait]` | Medium (proc macro) | +| Подход | Статическая диспетчеризация | Динамическая диспетчеризация | Send | Синтаксические издержки | +|--------|:---:|:---:|:---:|---| +| Нативный `async fn` в трейте | ✅ | ❌ | Неявно | Нет | +| `trait_variant` | ✅ | ❌ | Явно | `#[trait_variant::make]` | +| Ручной `Box::pin` | ✅ | ✅ | Явно | Высокие | +| Крейт `async-trait` | ✅ | ✅ | `#[async_trait]` | Средние (макрос-процедура) | -> **Recommendation**: For new code (Rust 1.75+), use native async traits. Add -> `trait_variant` when you need `Send` bounds for spawning tasks. For `dyn` -> dispatch, use manual `Box::pin` or the `async-trait` crate. The native -> approach is zero-cost for static dispatch. +> **Рекомендация**: для нового кода (Rust 1.75+) используйте нативные async-трейты. Добавляйте +> `trait_variant`, когда нужны ограничения `Send` для порождения задач. Для +> динамической диспетчеризации используйте ручной `Box::pin` или крейт `async-trait`. Нативный +> подход не имеет накладных расходов для статической диспетчеризации. -### Async Closures (Rust 1.85+) +### Асинхронные замыкания (Rust 1.85+) -Since Rust 1.85, `async closures` are stable — closures that capture their environment and return a future: +С Rust 1.85 стабилизированы `async closures` — замыкания, которые захватывают окружение и возвращают future: ```rust -// Before 1.85: awkward workaround +// До 1.85: неудобный обходной путь let urls = vec!["https://a.com", "https://b.com"]; let fetchers: Vec<_> = urls.iter().map(|url| { let url = url.to_string(); - // Returns a non-async closure that returns an async block + // Возвращаем обычное замыкание, которое возвращает async-блок move || async move { reqwest::get(&url).await } }).collect(); -// After 1.85: async closures just work +// После 1.85: асинхронные замыкания работают напрямую let fetchers: Vec<_> = urls.iter().map(|url| { async move || { reqwest::get(url).await } - // ↑ This is an async closure — captures url, returns a Future + // ↑ Это асинхронное замыкание — захватывает url и возвращает Future }).collect(); ``` -Async closures implement the new `AsyncFn`, `AsyncFnMut`, and `AsyncFnOnce` traits, which mirror `Fn`, `FnMut`, `FnOnce`: +Асинхронные замыкания реализуют новые трейты `AsyncFn`, `AsyncFnMut` и `AsyncFnOnce`, которые повторяют `Fn`, `FnMut`, `FnOnce`: ```rust -// Generic function accepting an async closure +// Обобщённая функция, принимающая асинхронное замыкание async fn retry(max: usize, f: F) -> Result where F: AsyncFn() -> Result, @@ -195,16 +195,16 @@ where } ``` -> **Migration tip**: If you have code using `Fn() -> impl Future`, -> consider switching to `AsyncFn() -> T` for cleaner signatures. +> **Совет по миграции**: если у вас код с `Fn() -> impl Future`, +> рассмотрите переход на `AsyncFn() -> T` — сигнатуры станут чище.
-🏋️ Exercise: Design an Async Service Trait (click to expand) +🏋️ Упражнение: спроектируйте асинхронный трейт сервиса (нажмите, чтобы раскрыть) -**Challenge**: Design a `Cache` trait with async `get` and `set` methods. Implement it twice: once with a `HashMap` (in-memory) and once with a simulated Redis backend (use `tokio::time::sleep` to simulate network latency). Write a generic function that works with both. +**Задача**: спроектируйте трейт `Cache` с асинхронными методами `get` и `set`. Реализуйте его дважды: один раз на `HashMap` (в памяти) и один раз на имитации бэкенда Redis (используйте `tokio::time::sleep`, чтобы имитировать задержку сети). Напишите обобщённую функцию, которая работает с обеими реализациями.
-🔑 Solution +🔑 Решение ```rust use std::collections::HashMap; @@ -217,7 +217,7 @@ trait Cache { async fn set(&self, key: &str, value: String); } -// --- In-memory implementation --- +// --- Реализация в памяти --- struct MemoryCache { store: Mutex>, } @@ -240,7 +240,7 @@ impl Cache for MemoryCache { } } -// --- Simulated Redis implementation --- +// --- Имитация реализации Redis --- struct RedisCache { store: Mutex>, latency: Duration, @@ -257,7 +257,7 @@ impl RedisCache { impl Cache for RedisCache { async fn get(&self, key: &str) -> Option { - sleep(self.latency).await; // Simulate network round-trip + sleep(self.latency).await; // Имитируем сетевой round-trip self.store.lock().await.get(key).cloned() } @@ -267,7 +267,7 @@ impl Cache for RedisCache { } } -// --- Generic function working with any Cache --- +// --- Обобщённая функция, работающая с любым Cache --- async fn cache_demo(cache: &C, label: &str) { cache.set("greeting", "Hello, async!".into()).await; let val = cache.get("greeting").await; @@ -284,19 +284,17 @@ async fn main() { } ``` -**Key takeaway**: The same generic function works with both implementations through static dispatch. No boxing, no allocation overhead. If you need to spawn these futures on a multi-threaded runtime, add `trait_variant::make(SendCache: Send)` to get `Send` bounds. For dynamic dispatch, use manual `Box::pin` or the `async-trait` crate. +**Ключевой вывод**: одна и та же обобщённая функция работает с обеими реализациями через статическую диспетчеризацию. Без боксирования и без накладных расходов на выделение памяти. Если нужно порождать эти future в многопоточном рантайме, добавьте `trait_variant::make(SendCache: Send)`, чтобы получить ограничения `Send`. Для динамической диспетчеризации используйте ручной `Box::pin` или крейт `async-trait`.
-> **Key Takeaways — Async Traits** -> - Since Rust 1.75, you can write `async fn` directly in traits (no `#[async_trait]` crate needed) -> - `trait_variant::make` auto-generates a `Send` variant for spawning tasks (static dispatch only) -> - Async closures (`async Fn()`) stabilized in 1.85 — use for callbacks and middleware -> - Prefer static dispatch (``) over `dyn` for performance-critical code +> **Ключевые выводы — async-трейты** +> - С Rust 1.75 можно писать `async fn` прямо в трейтах (крейт `#[async_trait]` не нужен) +> - `trait_variant::make` автоматически генерирует вариант с `Send` для порождения задач (только статическая диспетчеризация) +> - Асинхронные замыкания (`async Fn()`) стабилизированы в 1.85 — используйте их для колбэков и middleware +> - Для кода, критичного к производительности, предпочитайте статическую диспетчеризацию (``) вместо `dyn` -> **See also:** [Ch 13 — Production Patterns](ch13-production-patterns.md) for Tower's `Service` trait, [Ch 6 — Building Futures by Hand](ch06-building-futures-by-hand.md) for manual trait implementations +> **См. также:** [Гл. 13 — Продакшен-паттерны](ch13-production-patterns.md) — трейт `Service` из Tower, [Гл. 6 — Создаём фьючи вручную](ch06-building-futures-by-hand.md) — ручные реализации трейтов *** - - diff --git a/async-book/src/ch11-streams-and-asynciterator.md b/async-book/src/ch11-streams-and-asynciterator.md index aeb4364..8a29358 100644 --- a/async-book/src/ch11-streams-and-asynciterator.md +++ b/async-book/src/ch11-streams-and-asynciterator.md @@ -1,23 +1,23 @@ -# 11. Streams and AsyncIterator 🟡 +# 11. Потоки и AsyncIterator 🟡 -> **What you'll learn:** -> - The `Stream` trait: async iteration over multiple values -> - Creating streams: `stream::iter`, `async_stream`, `unfold` -> - Stream combinators: `map`, `filter`, `buffer_unordered`, `fold` -> - Async I/O traits: `AsyncRead`, `AsyncWrite`, `AsyncBufRead` +> **Что вы узнаете:** +> - Трейт `Stream`: асинхронная итерация по нескольким значениям +> - Создание потоков: `stream::iter`, `async_stream`, `unfold` +> - Комбинаторы потоков: `map`, `filter`, `buffer_unordered`, `fold` +> - Асинхронные трейты ввода-вывода: `AsyncRead`, `AsyncWrite`, `AsyncBufRead` -## Stream Trait Overview +## Обзор трейта Stream -A `Stream` is to `Iterator` what `Future` is to a single value — it yields multiple values asynchronously: +`Stream` относится к `Iterator` так же, как `Future` относится к одиночному значению, — он асинхронно выдаёт несколько значений: ```rust -// std::iter::Iterator (synchronous, multiple values) +// std::iter::Iterator (синхронный, несколько значений) trait Iterator { type Item; fn next(&mut self) -> Option; } -// futures::Stream (async, multiple values) +// futures::Stream (асинхронный, несколько значений) trait Stream { type Item; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; @@ -26,20 +26,20 @@ trait Stream { ```mermaid graph LR - subgraph "Sync" - VAL["Value
(T)"] - ITER["Iterator
(multiple T)"] + subgraph "Синхронно" + VAL["Значение
(T)"] + ITER["Итератор
(несколько T)"] end - subgraph "Async" + subgraph "Асинхронно" FUT["Future
(async T)"] - STREAM["Stream
(async multiple T)"] + STREAM["Stream
(async несколько T)"] end - VAL -->|"make async"| FUT - ITER -->|"make async"| STREAM - VAL -->|"make multiple"| ITER - FUT -->|"make multiple"| STREAM + VAL -->|"сделать асинхронным"| FUT + ITER -->|"сделать асинхронным"| STREAM + VAL -->|"сделать множественным"| ITER + FUT -->|"сделать множественным"| STREAM style VAL fill:#e3f2fd,color:#000 style ITER fill:#e3f2fd,color:#000 @@ -47,17 +47,17 @@ graph LR style STREAM fill:#c8e6c9,color:#000 ``` -### Creating Streams +### Создание потоков ```rust use futures::stream::{self, StreamExt}; use tokio::time::{interval, Duration}; use tokio_stream::wrappers::IntervalStream; -// 1. From an iterator +// 1. Из итератора let s = stream::iter(vec![1, 2, 3]); -// 2. From an async generator (using async_stream crate) +// 2. Из асинхронного генератора (с помощью крейта async_stream) // Cargo.toml: async-stream = "0.3" use async_stream::stream; @@ -70,25 +70,25 @@ fn countdown(from: u32) -> impl futures::Stream { } } -// 3. From a tokio interval +// 3. Из интервала tokio let tick_stream = IntervalStream::new(interval(Duration::from_secs(1))); -// 4. From a channel receiver (tokio_stream::wrappers) +// 4. Из приёмника канала (tokio_stream::wrappers) let (tx, rx) = tokio::sync::mpsc::channel::(100); let rx_stream = tokio_stream::wrappers::ReceiverStream::new(rx); -// 5. From unfold (generate from async state) +// 5. Из unfold (генерация из асинхронного состояния) let s = stream::unfold(0u32, |state| async move { if state >= 5 { - None // Stream ends + None // Поток завершается } else { let next = state + 1; - Some((state, next)) // yield `state`, new state is `next` + Some((state, next)) // выдаём `state`, новое состояние — `next` } }); ``` -### Consuming Streams +### Потребление потоков ```rust use futures::stream::{self, StreamExt}; @@ -96,7 +96,7 @@ use futures::stream::{self, StreamExt}; async fn stream_examples() { let s = stream::iter(vec![1, 2, 3, 4, 5]); - // for_each — process each item + // for_each — обрабатываем каждый элемент s.for_each(|x| async move { println!("{x}"); }).await; @@ -113,18 +113,18 @@ async fn stream_examples() { .collect() .await; - // buffer_unordered — process N items concurrently + // buffer_unordered — обрабатываем N элементов одновременно let results: Vec<_> = stream::iter(vec!["url1", "url2", "url3"]) .map(|url| async move { - // Simulate HTTP fetch + // Имитация HTTP-запроса tokio::time::sleep(Duration::from_millis(100)).await; - format!("response from {url}") + format!("ответ от {url}") }) - .buffer_unordered(10) // Up to 10 concurrent fetches + .buffer_unordered(10) // До 10 одновременных запросов .collect() .await; - // take, skip, zip, chain — just like Iterator + // take, skip, zip, chain — точно как у Iterator let first_three: Vec = stream::iter(1..=100) .take(3) .collect() @@ -132,21 +132,21 @@ async fn stream_examples() { } ``` -### Comparison with C# IAsyncEnumerable +### Сравнение с C# IAsyncEnumerable -| Feature | Rust `Stream` | C# `IAsyncEnumerable` | -|---------|--------------|--------------------------| -| **Syntax** | `stream! { yield x; }` | `await foreach` / `yield return` | -| **Cancellation** | Drop the stream | `CancellationToken` | -| **Backpressure** | Consumer controls poll rate | Consumer controls `MoveNextAsync` | -| **Built-in** | No (needs `futures` crate) | Yes (since C# 8.0) | -| **Combinators** | `.map()`, `.filter()`, `.buffer_unordered()` | LINQ + `System.Linq.Async` | -| **Error handling** | `Stream>` | Throw in async iterator | +| Характеристика | Rust `Stream` | C# `IAsyncEnumerable` | +|----------------|---------------|--------------------------| +| **Синтаксис** | `stream! { yield x; }` | `await foreach` / `yield return` | +| **Отмена** | Уничтожить поток | `CancellationToken` | +| **Обратное давление** | Потребитель управляет частотой poll | Потребитель управляет `MoveNextAsync` | +| **Встроено** | Нет (нужен крейт `futures`) | Да (с C# 8.0) | +| **Комбинаторы** | `.map()`, `.filter()`, `.buffer_unordered()` | LINQ + `System.Linq.Async` | +| **Обработка ошибок** | `Stream>` | Выброс исключения в асинхронном итераторе | ```rust -// Rust: Stream of database rows -// NOTE: try_stream! (not stream!) is required when using ? inside the body. -// stream! doesn't propagate errors — try_stream! yields Err(e) and ends. +// Rust: поток строк из базы данных +// ПРИМЕЧАНИЕ: при использовании ? внутри тела нужен try_stream! (а не stream!). +// stream! не пробрасывает ошибки — try_stream! выдаёт Err(e) и завершается. fn get_users(db: &Database) -> impl Stream> + '_ { try_stream! { let mut cursor = db.query("SELECT * FROM users").await?; @@ -156,18 +156,18 @@ fn get_users(db: &Database) -> impl Stream> + '_ { } } -// Consume: +// Потребление: let mut users = pin!(get_users(&db)); while let Some(result) = users.next().await { match result { Ok(user) => println!("{}", user.name), - Err(e) => eprintln!("Error: {e}"), + Err(e) => eprintln!("Ошибка: {e}"), } } ``` ```csharp -// C# equivalent: +// Эквивалент на C#: async IAsyncEnumerable GetUsers() { await using var reader = await db.QueryAsync("SELECT * FROM users"); while (await reader.ReadAsync()) { @@ -175,21 +175,21 @@ async IAsyncEnumerable GetUsers() { } } -// Consume: +// Потребление: await foreach (var user in GetUsers()) { Console.WriteLine(user.Name); } ```
-🏋️ Exercise: Build an Async Stats Aggregator (click to expand) +🏋️ Упражнение: напишите асинхронный агрегатор статистики (нажмите, чтобы раскрыть) -**Challenge**: Given a stream of sensor readings `Stream`, write an async function that consumes the stream and returns `(count, min, max, average)`. Use `StreamExt` combinators — don't just collect into a Vec. +**Задача**: дан поток показаний датчиков `Stream`. Напишите асинхронную функцию, которая потребляет поток и возвращает `(count, min, max, average)`. Используйте комбинаторы `StreamExt` — не собирайте всё в Vec. -*Hint*: Use `.fold()` to accumulate state across the stream. +*Подсказка*: используйте `.fold()`, чтобы накапливать состояние по мере прохода по потоку.
-🔑 Solution +🔑 Решение ```rust use futures::stream::{self, StreamExt}; @@ -235,28 +235,28 @@ async fn test_stats() { } ``` -**Key takeaway**: Stream combinators like `.fold()` process items one-at-a-time without collecting into memory — essential for processing large or unbounded data streams. +**Ключевой вывод**: комбинаторы потоков вроде `.fold()` обрабатывают элементы по одному, не собирая их в память — это необходимо для обработки больших или бесконечных потоков данных.
-### Async I/O Traits: AsyncRead, AsyncWrite, AsyncBufRead +### Асинхронные трейты ввода-вывода: AsyncRead, AsyncWrite, AsyncBufRead -Just as `std::io::Read`/`Write` are the foundation of synchronous I/O, their async counterparts are the foundation of async I/O. These traits are provided by `tokio::io` (or `futures::io` for runtime-agnostic code): +Так же как `std::io::Read`/`Write` являются основой синхронного ввода-вывода, их асинхронные аналоги являются основой асинхронного ввода-вывода. Эти трейты предоставляются `tokio::io` (или `futures::io` для кода, не привязанного к рантайму): ```rust -// tokio::io — the async versions of std::io traits +// tokio::io — асинхронные версии трейтов std::io -/// Read bytes from a source asynchronously +/// Асинхронное чтение байтов из источника pub trait AsyncRead { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, // Tokio's safe wrapper around uninitialized memory + buf: &mut ReadBuf<'_>, // Безопасная обёртка tokio над неинициализированной памятью ) -> Poll>; } -/// Write bytes to a sink asynchronously +/// Асинхронная запись байтов в приёмник pub trait AsyncWrite { fn poll_write( self: Pin<&mut Self>, @@ -268,14 +268,14 @@ pub trait AsyncWrite { fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; } -/// Buffered reading with line support +/// Буферизованное чтение с поддержкой строк pub trait AsyncBufRead: AsyncRead { fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>; fn consume(self: Pin<&mut Self>, amt: usize); } ``` -**In practice**, you rarely call these `poll_*` methods directly. Instead, use the extension traits `AsyncReadExt` and `AsyncWriteExt` which provide `.await`-friendly helper methods: +**На практике** вы редко вызываете эти методы `poll_*` напрямую. Вместо этого используйте трейты-расширения `AsyncReadExt` и `AsyncWriteExt`, которые предоставляют удобные вспомогательные методы для `.await`: ```rust use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt}; @@ -285,7 +285,7 @@ use tokio::io::BufReader; async fn io_examples() -> tokio::io::Result<()> { let mut stream = TcpStream::connect("127.0.0.1:8080").await?; - // AsyncWriteExt: write_all, write_u32, write_buf, etc. + // AsyncWriteExt: write_all, write_u32, write_buf и др. stream.write_all(b"GET / HTTP/1.0\r\n\r\n").await?; // AsyncReadExt: read, read_exact, read_to_end, read_to_string @@ -304,26 +304,26 @@ async fn io_examples() -> tokio::io::Result<()> { } ``` -**Implementing custom async I/O** — wrap a protocol over raw TCP: +**Реализация собственного асинхронного ввода-вывода** — оборачиваем протокол поверх сырого TCP: ```rust use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use std::pin::Pin; use std::task::{Context, Poll}; -/// A length-prefixed protocol: [u32 length][payload bytes] +/// Протокол с префиксом длины: [u32 длина][байты полезной нагрузки] struct FramedStream { inner: T, } impl FramedStream { - /// Read one complete frame + /// Прочитать один полный кадр async fn read_frame(&mut self) -> tokio::io::Result> { - // Read the 4-byte length prefix + // Читаем 4-байтовый префикс длины let len = self.inner.read_u32().await? as usize; - // Read exactly that many bytes + // Читаем ровно столько байт let mut payload = vec![0u8; len]; self.inner.read_exact(&mut payload).await?; Ok(payload) @@ -331,7 +331,7 @@ impl FramedStream { } impl FramedStream { - /// Write one complete frame + /// Записать один полный кадр async fn write_frame(&mut self, data: &[u8]) -> tokio::io::Result<()> { self.inner.write_u32(data.len() as u32).await?; @@ -342,26 +342,26 @@ impl FramedStream { } ``` -| Sync Trait | Async Trait (tokio) | Async Trait (futures) | Extension Trait | -|-----------|--------------------|-----------------------|----------------| +| Синхронный трейт | Асинхронный трейт (tokio) | Асинхронный трейт (futures) | Трейт-расширение | +|-----------------|--------------------------|-----------------------------|-----------------| | `std::io::Read` | `tokio::io::AsyncRead` | `futures::io::AsyncRead` | `AsyncReadExt` | | `std::io::Write` | `tokio::io::AsyncWrite` | `futures::io::AsyncWrite` | `AsyncWriteExt` | | `std::io::BufRead` | `tokio::io::AsyncBufRead` | `futures::io::AsyncBufRead` | `AsyncBufReadExt` | | `std::io::Seek` | `tokio::io::AsyncSeek` | `futures::io::AsyncSeek` | `AsyncSeekExt` | -> **tokio vs futures I/O traits**: They're similar but not identical — tokio's `AsyncRead` uses `ReadBuf` (handles uninitialized memory safely), while `futures::AsyncRead` uses `&mut [u8]`. Use `tokio_util::compat` to convert between them. +> **Трейты ввода-вывода tokio и futures**: они похожи, но не идентичны — `AsyncRead` из tokio использует `ReadBuf` (безопасно работает с неинициализированной памятью), а `futures::AsyncRead` использует `&mut [u8]`. Для конвертации между ними используйте `tokio_util::compat`. -> **Copy utilities**: `tokio::io::copy(&mut reader, &mut writer)` is the async equivalent of `std::io::copy` — useful for proxy servers or file transfers. `tokio::io::copy_bidirectional` copies both directions concurrently. +> **Утилиты копирования**: `tokio::io::copy(&mut reader, &mut writer)` — асинхронный аналог `std::io::copy`, полезен для прокси-серверов или передачи файлов. `tokio::io::copy_bidirectional` копирует данные в обоих направлениях одновременно.
-🏋️ Exercise: Build an Async Line Counter (click to expand) +🏋️ Упражнение: напишите асинхронный счётчик строк (нажмите, чтобы раскрыть) -**Challenge**: Write an async function that takes any `AsyncBufRead` source and returns the number of non-empty lines. It should work with files, TCP streams, or any buffered reader. +**Задача**: напишите асинхронную функцию, которая принимает любой источник `AsyncBufRead` и возвращает количество непустых строк. Она должна работать с файлами, TCP-потоками или любым буферизованным читателем. -*Hint*: Use `AsyncBufReadExt::lines()` and count lines where `!line.is_empty()`. +*Подсказка*: используйте `AsyncBufReadExt::lines()` и считайте строки, для которых `!line.is_empty()`.
-🔑 Solution +🔑 Решение ```rust use tokio::io::AsyncBufReadExt; @@ -379,7 +379,7 @@ async fn count_non_empty_lines( Ok(count) } -// Works with any AsyncBufRead: +// Работает с любым AsyncBufRead: // let file = tokio::io::BufReader::new(tokio::fs::File::open("data.txt").await?); // let count = count_non_empty_lines(file).await?; // @@ -387,19 +387,17 @@ async fn count_non_empty_lines( // let count = count_non_empty_lines(tcp).await?; ``` -**Key takeaway**: By programming against `AsyncBufRead` instead of a concrete type, your I/O code is reusable across files, sockets, pipes, and even in-memory buffers (`tokio::io::BufReader::new(std::io::Cursor::new(data))`). +**Ключевой вывод**: программируя против `AsyncBufRead`, а не против конкретного типа, вы делаете код ввода-вывода пригодным для файлов, сокетов, каналов и даже буферов в памяти (`tokio::io::BufReader::new(std::io::Cursor::new(data))`).
-> **Key Takeaways — Streams and AsyncIterator** -> - `Stream` is the async equivalent of `Iterator` — yields `Poll::Ready(Some(item))` or `Poll::Ready(None)` -> - `.buffer_unordered(N)` processes N stream items concurrently — the key concurrency tool for streams -> - `async_stream::stream!` is the easiest way to create custom streams (uses `yield`) -> - `AsyncRead`/`AsyncBufRead` enable generic, reusable I/O code across files, sockets, and pipes +> **Ключевые выводы — потоки и AsyncIterator** +> - `Stream` — асинхронный аналог `Iterator`: выдаёт `Poll::Ready(Some(item))` или `Poll::Ready(None)` +> - `.buffer_unordered(N)` обрабатывает N элементов потока одновременно — главный инструмент конкурентности для потоков +> - `async_stream::stream!` — самый простой способ создать собственный поток (использует `yield`) +> - `AsyncRead`/`AsyncBufRead` позволяют писать обобщённый, переиспользуемый код ввода-вывода для файлов, сокетов и каналов -> **See also:** [Ch 9 — When Tokio Isn't the Right Fit](ch09-when-tokio-isnt-the-right-fit.md) for `FuturesUnordered` (related pattern), [Ch 13 — Production Patterns](ch13-production-patterns.md) for backpressure with bounded channels +> **См. также:** [Гл. 9 — Когда Tokio не подходит](ch09-when-tokio-isnt-the-right-fit.md) — `FuturesUnordered` (родственный паттерн), [Гл. 13 — Продакшен-паттерны](ch13-production-patterns.md) — обратное давление с ограниченными каналами *** - - diff --git a/async-book/src/ch12-common-pitfalls.md b/async-book/src/ch12-common-pitfalls.md index 1e1be7b..889bfcf 100644 --- a/async-book/src/ch12-common-pitfalls.md +++ b/async-book/src/ch12-common-pitfalls.md @@ -1,24 +1,24 @@ -# 12. Common Pitfalls 🔴 +# 12. Типичные ловушки 🔴 -> **What you'll learn:** -> - 9 common async Rust bugs and how to fix each one -> - Why blocking the executor is the #1 mistake (and how `spawn_blocking` fixes it) -> - Cancellation hazards: what happens when a future is dropped mid-await -> - Debugging: `tokio-console`, `tracing`, `#[instrument]` -> - Testing: `#[tokio::test]`, `time::pause()`, trait-based mocking +> **Что вы узнаете:** +> - 9 типичных ошибок асинхронного Rust и как исправить каждую +> - Почему блокировка исполнителя — ошибка номер один (и как её исправляет `spawn_blocking`) +> - Опасности отмены: что происходит, когда future уничтожается посреди `await` +> - Отладка: `tokio-console`, `tracing`, `#[instrument]` +> - Тестирование: `#[tokio::test]`, `time::pause()`, моки на основе трейтов -## Blocking the Executor +## Блокировка исполнителя -The #1 mistake in async Rust: running blocking code on the async executor thread. This starves other tasks. +Ошибка номер один в асинхронном Rust: выполнение блокирующего кода в потоке исполнителя. Это морит голодом другие задачи. ```rust -// ❌ WRONG: Blocks the entire executor thread +// ❌ НЕПРАВИЛЬНО: блокирует весь поток исполнителя async fn bad_handler() -> String { - let data = std::fs::read_to_string("big_file.txt").unwrap(); // BLOCKS! + let data = std::fs::read_to_string("big_file.txt").unwrap(); // БЛОКИРУЕТ! process(&data) } -// ✅ CORRECT: Offload blocking work to a dedicated thread pool +// ✅ ПРАВИЛЬНО: переносим блокирующую работу в отдельный пул потоков async fn good_handler() -> String { let data = tokio::task::spawn_blocking(|| { std::fs::read_to_string("big_file.txt").unwrap() @@ -26,7 +26,7 @@ async fn good_handler() -> String { process(&data) } -// ✅ ALSO CORRECT: Use tokio's async fs +// ✅ ТОЖЕ ПРАВИЛЬНО: используем асинхронную fs из tokio async fn also_good_handler() -> String { let data = tokio::fs::read_to_string("big_file.txt").await.unwrap(); process(&data) @@ -35,257 +35,250 @@ async fn also_good_handler() -> String { ```mermaid graph TB - subgraph "❌ Blocking Call on Executor" - T1_BAD["Thread 1: std::fs::read()
🔴 BLOCKED for 500ms"] - T2_BAD["Thread 2: handling requests
🟢 Working alone"] - TASKS_BAD["100 pending tasks
⏳ Starved"] - T1_BAD -->|"can't poll"| TASKS_BAD + subgraph "❌ Блокирующий вызов в исполнителе" + T1_BAD["Поток 1: std::fs::read()
🔴 ЗАБЛОКИРОВАН на 500 мс"] + T2_BAD["Поток 2: обрабатывает запросы
🟢 Работает в одиночку"] + TASKS_BAD["100 ожидающих задач
⏳ Голодают"] + T1_BAD -->|"не может опрашивать"| TASKS_BAD end subgraph "✅ spawn_blocking" - T1_GOOD["Thread 1: polling futures
🟢 Available"] - T2_GOOD["Thread 2: polling futures
🟢 Available"] - BT["Blocking pool thread:
std::fs::read()
🔵 Separate pool"] - TASKS_GOOD["100 tasks
✅ All making progress"] - T1_GOOD -->|"polls"| TASKS_GOOD - T2_GOOD -->|"polls"| TASKS_GOOD + T1_GOOD["Поток 1: опрашивает future
🟢 Свободен"] + T2_GOOD["Поток 2: опрашивает future
🟢 Свободен"] + BT["Поток пула для блокирующих задач:
std::fs::read()
🔵 Отдельный пул"] + TASKS_GOOD["100 задач
✅ Все продвигаются"] + T1_GOOD -->|"опрашивает"| TASKS_GOOD + T2_GOOD -->|"опрашивает"| TASKS_GOOD end ``` -### std::thread::sleep vs tokio::time::sleep +### std::thread::sleep и tokio::time::sleep ```rust -// ❌ WRONG: Blocks the executor thread for 5 seconds +// ❌ НЕПРАВИЛЬНО: блокирует поток исполнителя на 5 секунд async fn bad_delay() { - std::thread::sleep(Duration::from_secs(5)); // Thread can't poll anything else! + std::thread::sleep(Duration::from_secs(5)); // Поток не может опрашивать ничего другого! } -// ✅ CORRECT: Yields to the executor, other tasks can run +// ✅ ПРАВИЛЬНО: уступаем управление исполнителю, другие задачи могут работать async fn good_delay() { - tokio::time::sleep(Duration::from_secs(5)).await; // Non-blocking! + tokio::time::sleep(Duration::from_secs(5)).await; // Не блокирует! } ``` -### Holding MutexGuard Across .await +### Удержание MutexGuard через .await ```rust -use std::sync::Mutex; // std Mutex — NOT async-aware +use std::sync::Mutex; // std Mutex — НЕ учитывает async -// ⚠️ RISKY: MutexGuard held across .await +// ⚠️ РИСКОВАННО: MutexGuard удерживается через .await async fn bad_mutex(data: &Mutex>) { let mut guard = data.lock().unwrap(); guard.push("item".into()); - some_io().await; // Guard is held here — blocks other threads from locking! + some_io().await; // Guard удерживается здесь — другие потоки не могут взять блокировку! guard.push("another".into()); } -// NOTE: This compiles! std::sync::MutexGuard is !Send, but the compiler only -// enforces Send on the Future when you pass it to something that requires it -// (e.g., tokio::spawn). Calling bad_mutex(...).await directly compiles fine. -// However, tokio::spawn(bad_mutex(data)) will fail with a Send bound error. +// ПРИМЕЧАНИЕ: это компилируется! std::sync::MutexGuard — !Send, но компилятор требует Send +// для Future только тогда, когда вы передаёте его туда, где это требуется +// (например, в tokio::spawn). Прямой вызов bad_mutex(...).await компилируется без проблем. +// Однако tokio::spawn(bad_mutex(data)) упадёт с ошибкой ограничения Send. ``` -**Why this is usually a problem** — but not always: +**Почему это обычно проблема** — но не всегда: -Holding a `std::sync::Mutex` across `.await` blocks the **OS thread** for the -duration of the I/O, preventing the executor from polling other tasks on that -thread. For short critical sections this is wasteful; for long I/O it's a -performance trap. +Удержание `std::sync::Mutex` через `.await` блокирует **поток ОС** на всё время I/O, не давая исполнителю опрашивать другие задачи в этом потоке. Для коротких критических секций это расточительно; для длительного I/O — это ловушка производительности. -**However**, there are legitimate cases where you *must* hold a lock across an -`.await` — the same way a database transaction holds a lock between read and -commit. Dropping and re-acquiring the lock introduces a **TOCTOU (time-of-check -to time-of-use) race**: another task can modify the data between your two -critical sections. The right fix depends on the use case: +**Однако** бывают законные случаи, когда блокировку *необходимо* удерживать через `.await` — так же как транзакция базы данных удерживает блокировку между чтением и фиксацией. Если отпустить и заново захватить блокировку, возникает **гонка TOCTOU (time-of-check to time-of-use)**: другая задача может изменить данные между вашими двумя критическими секциями. Правильное решение зависит от сценария: ```rust -// OPTION 1: Scope the guard — works when operations are independent +// ВАРИАНТ 1: ограничиваем область видимости guard — подходит, когда операции независимы async fn scoped_mutex(data: &Mutex>) { { let mut guard = data.lock().unwrap(); guard.push("item".into()); - } // Guard dropped here - some_io().await; // Lock is released — other tasks can proceed + } // Guard уничтожается здесь + some_io().await; // Блокировка снята — другие задачи могут продолжать { let mut guard = data.lock().unwrap(); guard.push("another".into()); } } -// ⚠️ Careful: another task can lock + modify the Vec between the two sections. -// This is fine if the two pushes are independent, but wrong if "another" -// depends on state set by "item". +// ⚠️ Осторожно: другая задача может захватить блокировку и изменить Vec между двумя секциями. +// Это нормально, если два push независимы, но неверно, если "another" +// зависит от состояния, установленного "item". -// OPTION 2: Use tokio::sync::Mutex — holds lock across .await without -// blocking the OS thread. Best when you need transactional -// read-modify-write across an await point. +// ВАРИАНТ 2: используем tokio::sync::Mutex — удерживает блокировку через .await, +// не блокируя поток ОС. Лучше всего, когда нужна транзакционная +// операция «прочитать-изменить-записать» через точку await. use tokio::sync::Mutex as AsyncMutex; async fn async_mutex(data: &AsyncMutex>) { - let mut guard = data.lock().await; // Async lock — doesn't block the thread + let mut guard = data.lock().await; // Асинхронная блокировка — не блокирует поток guard.push("item".into()); - some_io().await; // OK — tokio Mutex guard is Send + some_io().await; // OK — guard tokio Mutex является Send guard.push("another".into()); - // Guard held the whole time — no TOCTOU race, no thread blocked. + // Guard удерживался всё время — ни гонки TOCTOU, ни заблокированного потока. } ``` -> **When to use which Mutex**: -> - `std::sync::Mutex`: Short critical sections with no `.await` inside -> - `tokio::sync::Mutex`: When you need to hold the lock across `.await` points -> (transactional semantics, TOCTOU avoidance) -> - `parking_lot::Mutex`: Drop-in `std` replacement, faster, smaller, still no `.await` +> **Когда какой Mutex использовать**: +> - `std::sync::Mutex`: короткие критические секции без `.await` внутри +> - `tokio::sync::Mutex`: когда нужно удерживать блокировку через точки `.await` +> (транзакционная семантика, предотвращение TOCTOU) +> - `parking_lot::Mutex`: прямая замена `std`, быстрее и компактнее, но тоже без `.await` > -> **Rule of thumb**: Don't blindly split a critical section around an `.await`. -> Ask whether the two halves are truly independent. If they aren't — if the -> second half depends on state from the first — use `tokio::sync::Mutex` or -> redesign the data flow. +> **Эмпирическое правило**: не режьте критическую секцию вокруг `.await` не задумываясь. +> Спросите себя, действительно ли обе половины независимы. Если нет — если вторая половина +> зависит от состояния из первой, — используйте `tokio::sync::Mutex` или перепроектируйте +> поток данных. -### Cancellation Hazards +### Опасности отмены -Dropping a future cancels it — but this can leave things in an inconsistent state: +Уничтожение future отменяет его — но это может оставить систему в несогласованном состоянии: ```rust -// ❌ DANGEROUS: Resource leak on cancellation +// ❌ ОПАСНО: утечка ресурсов при отмене async fn transfer(from: &Account, to: &Account, amount: u64) { - from.debit(amount).await; // If cancelled HERE... - to.credit(amount).await; // ...money vanishes! + from.debit(amount).await; // Если отменить ЗДЕСЬ... + to.credit(amount).await; // ...деньги пропадут! } -// ✅ SAFE: Make operations atomic or use compensation +// ✅ БЕЗОПАСНО: делаем операции атомарными или используем компенсацию async fn safe_transfer(from: &Account, to: &Account, amount: u64) -> Result<(), Error> { - // Use a database transaction (all-or-nothing) + // Используем транзакцию базы данных (всё или ничего) let tx = db.begin_transaction().await?; tx.debit(from, amount).await?; tx.credit(to, amount).await?; - tx.commit().await?; // Only commits if everything succeeded + tx.commit().await?; // Фиксируется, только если всё прошло успешно Ok(()) } -// ✅ ALSO SAFE: Use tokio::select! with cancellation awareness +// ✅ ТОЖЕ БЕЗОПАСНО: tokio::select! с учётом отмены tokio::select! { result = transfer(from, to, amount) => { - // Transfer completed + // Перевод завершён } _ = shutdown_signal() => { - // Don't cancel mid-transfer — let it finish - // Or: roll back explicitly + // Не прерываем перевод на полпути — даём ему завершиться + // Или: явно откатываем } } ``` -### No Async Drop +### Нет асинхронного Drop -Rust's `Drop` trait is synchronous — you **cannot** `.await` inside `drop()`. This is a frequent source of confusion: +Трейт `Drop` в Rust синхронный — вы **не можете** вызвать `.await` внутри `drop()`. Это частый источник путаницы: ```rust struct DbConnection { /* ... */ } impl Drop for DbConnection { fn drop(&mut self) { - // ❌ Can't do this — drop() is sync! + // ❌ Так нельзя — drop() синхронный! // self.connection.shutdown().await; - // ✅ Workaround 1: Spawn a cleanup task (fire-and-forget) + // ✅ Обходной путь 1: порождаем задачу очистки (fire-and-forget) let conn = self.connection.take(); tokio::spawn(async move { let _ = conn.shutdown().await; }); - // ✅ Workaround 2: Use a synchronous close + // ✅ Обходной путь 2: синхронное закрытие // self.connection.blocking_close(); } } ``` -**Best practice**: Provide an explicit `async fn close(self)` method and document that callers should use it. Rely on `Drop` only as a safety net, not the primary cleanup path. +**Лучшая практика**: предоставьте явный метод `async fn close(self)` и задокументируйте, что вызывающий код должен использовать его. Полагайтесь на `Drop` только как на страховочную сетку, а не как на основной путь очистки. -### select! Fairness and Starvation +### Справедливость и голодание в select! ```rust use tokio::sync::mpsc; -// ❌ UNFAIR: busy_stream always wins, slow_stream starves +// ❌ НЕСПРАВЕДЛИВО: busy_stream всегда побеждает, slow_stream голодает async fn unfair(mut fast: mpsc::Receiver, mut slow: mpsc::Receiver) { loop { tokio::select! { Some(v) = fast.recv() => println!("fast: {v}"), Some(v) = slow.recv() => println!("slow: {v}"), - // If both are ready, tokio randomly picks one. - // But if `fast` is ALWAYS ready, `slow` rarely gets polled. + // Если готовы оба, tokio выбирает случайно. + // Но если `fast` ВСЕГДА готов, `slow` почти никогда не опрашивается. } } } -// ✅ FAIR: Use biased select or drain in batches +// ✅ СПРАВЕДЛИВО: используем biased select или вычитываем пачками async fn fair(mut fast: mpsc::Receiver, mut slow: mpsc::Receiver) { loop { tokio::select! { - biased; // Always check in order — explicit priority + biased; // Всегда проверяем по порядку — явный приоритет - Some(v) = slow.recv() => println!("slow: {v}"), // Priority! + Some(v) = slow.recv() => println!("slow: {v}"), // Приоритет! Some(v) = fast.recv() => println!("fast: {v}"), } } } ``` -### Accidental Sequential Execution +### Случайное последовательное выполнение ```rust -// ❌ SEQUENTIAL: Takes 2 seconds total +// ❌ ПОСЛЕДОВАТЕЛЬНО: занимает в сумме 2 секунды async fn slow() { - let a = fetch("url_a").await; // 1 second - let b = fetch("url_b").await; // 1 second (waits for a to finish first!) + let a = fetch("url_a").await; // 1 секунда + let b = fetch("url_b").await; // 1 секунда (ждёт, пока завершится a!) } -// ✅ CONCURRENT: Takes 1 second total +// ✅ КОНКУРЕНТНО: занимает в сумме 1 секунду async fn fast() { let (a, b) = tokio::join!( - fetch("url_a"), // Both start immediately + fetch("url_a"), // Оба стартуют сразу fetch("url_b"), ); } -// ✅ ALSO CONCURRENT: Using let + join +// ✅ ТОЖЕ КОНКУРЕНТНО: через let + join async fn also_fast() { - let fut_a = fetch("url_a"); // Create future (lazy — not started yet) - let fut_b = fetch("url_b"); // Create future - let (a, b) = tokio::join!(fut_a, fut_b); // NOW both run concurrently + let fut_a = fetch("url_a"); // Создаём future (ленивый — ещё не запущен) + let fut_b = fetch("url_b"); // Создаём future + let (a, b) = tokio::join!(fut_a, fut_b); // ТЕПЕРЬ оба выполняются конкурентно } ``` -> **Trap**: `let a = fetch(url).await; let b = fetch(url).await;` is sequential! -> The second `.await` doesn't start until the first finishes. Use `join!` or -> `spawn` for concurrency. +> **Ловушка**: `let a = fetch(url).await; let b = fetch(url).await;` выполняется последовательно! +> Второй `.await` не начнётся, пока не завершится первый. Для конкурентности используйте `join!` или +> `spawn`. -## Case Study: Debugging a Hung Production Service +## Разбор кейса: отладка зависшего продакшен-сервиса -A real-world scenario: a service handles requests fine for 10 minutes, then stops responding. No errors in logs. CPU at 0%. +Реальный сценарий: сервис 10 минут обрабатывает запросы нормально, а потом перестаёт отвечать. В логах нет ошибок. Загрузка CPU — 0%. -**Diagnosis steps:** +**Шаги диагностики:** -1. **Attach `tokio-console`** — reveals 200+ tasks stuck in `Pending` state -2. **Check task details** — all waiting on the same `Mutex::lock().await` -3. **Root cause** — one task held a `std::sync::MutexGuard` across an `.await` and panicked, poisoning the mutex. All other tasks now fail on `lock().unwrap()` +1. **Подключаем `tokio-console`** — показывает 200+ задач, застрявших в состоянии `Pending` +2. **Смотрим детали задач** — все ждут одного и того же `Mutex::lock().await` +3. **Первопричина** — одна задача удерживала `std::sync::MutexGuard` через `.await` и упала с паникой, «отравив» мьютекс. Теперь все остальные задачи падают на `lock().unwrap()` -**The fix:** +**Исправление:** -| Before (broken) | After (fixed) | -|-----------------|---------------| +| Было (сломано) | Стало (исправлено) | +|----------------|--------------------| | `std::sync::Mutex` | `tokio::sync::Mutex` | -| `.lock().unwrap()` across `.await` | Scope lock before `.await` | -| No timeout on lock acquisition | `tokio::time::timeout(dur, mutex.lock())` | -| No recovery on poisoned mutex | `tokio::sync::Mutex` doesn't poison | +| `.lock().unwrap()` через `.await` | Ограничиваем блокировку до `.await` | +| Нет таймаута на получение блокировки | `tokio::time::timeout(dur, mutex.lock())` | +| Нет восстановления после отравленного мьютекса | `tokio::sync::Mutex` не отравляется | -**Prevention checklist:** -- [ ] Use `tokio::sync::Mutex` if the guard crosses any `.await` -- [ ] Add `#[tracing::instrument]` to async functions for span tracking -- [ ] Run `tokio-console` in staging to catch hung tasks early -- [ ] Add health check endpoints that verify task responsiveness +**Чек-лист профилактики:** +- [ ] Используйте `tokio::sync::Mutex`, если guard пересекает любой `.await` +- [ ] Добавляйте `#[tracing::instrument]` к асинхронным функциям для отслеживания span +- [ ] Запускайте `tokio-console` в staging, чтобы рано замечать зависшие задачи +- [ ] Добавляйте эндпоинты проверки работоспособности, которые проверяют отзывчивость задач
-🏋️ Exercise: Spot the Bugs (click to expand) +🏋️ Упражнение: найдите ошибки (нажмите, чтобы раскрыть) -**Challenge**: Find all the async pitfalls in this code and fix them. +**Задача**: найдите все асинхронные ловушки в этом коде и исправьте их. ```rust use std::sync::Mutex; @@ -295,10 +288,10 @@ async fn process_requests(urls: Vec) -> Vec { for url in &urls { let response = reqwest::get(url).await.unwrap().text().await.unwrap(); - std::thread::sleep(std::time::Duration::from_millis(100)); // Rate limit + std::thread::sleep(std::time::Duration::from_millis(100)); // Ограничение частоты let mut guard = results.lock().unwrap(); guard.push(response); - expensive_parse(&guard).await; // Parse all results so far + expensive_parse(&guard).await; // Разбираем все результаты на данный момент } results.into_inner().unwrap() @@ -306,14 +299,14 @@ async fn process_requests(urls: Vec) -> Vec { ```
-🔑 Solution +🔑 Решение -**Bugs found:** +**Найденные ошибки:** -1. **Sequential fetches** — URLs are fetched one at a time instead of concurrently -2. **`std::thread::sleep`** — Blocks the executor thread -3. **MutexGuard held across `.await`** — `guard` is alive when `expensive_parse` is awaited -4. **No concurrency** — Should use `join!` or `FuturesUnordered` +1. **Последовательные запросы** — URL запрашиваются по одному, а не конкурентно +2. **`std::thread::sleep`** — блокирует поток исполнителя +3. **MutexGuard удерживается через `.await`** — `guard` жив, когда ожидается `expensive_parse` +4. **Нет конкурентности** — следовало использовать `join!` или `FuturesUnordered` ```rust use tokio::sync::Mutex; @@ -321,19 +314,19 @@ use std::sync::Arc; use futures::stream::{self, StreamExt}; async fn process_requests(urls: Vec) -> Vec { - // Fix 4: Process URLs concurrently with buffer_unordered + // Исправление 4: обрабатываем URL конкурентно с помощью buffer_unordered let results: Vec = stream::iter(urls) .map(|url| async move { let response = reqwest::get(&url).await.unwrap().text().await.unwrap(); - // Fix 2: Use tokio::time::sleep instead of std::thread::sleep + // Исправление 2: используем tokio::time::sleep вместо std::thread::sleep tokio::time::sleep(std::time::Duration::from_millis(100)).await; response }) - .buffer_unordered(10) // Up to 10 concurrent requests + .buffer_unordered(10) // До 10 одновременных запросов .collect() .await; - // Fix 3: Parse after collecting — no mutex needed at all! + // Исправление 3: разбираем после сбора — мьютекс вообще не нужен! for result in &results { expensive_parse(result).await; } @@ -342,20 +335,20 @@ async fn process_requests(urls: Vec) -> Vec { } ``` -**Key takeaway**: Often you can restructure async code to eliminate mutexes entirely. Collect results with streams/join, then process. Simpler, faster, no deadlock risk. +**Ключевой вывод**: часто асинхронный код можно перестроить так, чтобы мьютексы не понадобились вовсе. Собирайте результаты через потоки или join, а затем обрабатывайте. Это проще, быстрее и без риска взаимной блокировки (deadlock).
--- -### Debugging Async Code +### Отладка асинхронного кода -Async stack traces are notoriously cryptic — they show the executor's poll loop rather than your logical call chain. Here are the essential debugging tools. +Асинхронные трассировки стека известны своей невнятностью — они показывают цикл опроса исполнителя, а не вашу логическую цепочку вызовов. Ниже перечислены основные инструменты отладки. -#### tokio-console: Real-Time Task Inspector +#### tokio-console: инспектор задач в реальном времени -[tokio-console](https://github.com/tokio-rs/console) gives you an `htop`-like view of every spawned task: its state, poll duration, waker activity, and resource usage. +[tokio-console](https://github.com/tokio-rs/console) даёт вид, похожий на `htop`, для каждой порождённой задачи: её состояние, длительность опроса, активность waker и использование ресурсов. ```toml # Cargo.toml @@ -367,59 +360,59 @@ tokio = { version = "1", features = ["full", "tracing"] } ```rust #[tokio::main] async fn main() { - console_subscriber::init(); // Replaces the default tracing subscriber - // ... rest of your application + console_subscriber::init(); // Заменяет стандартный подписчик tracing + // ... остальная часть вашего приложения } ``` -Then in another terminal: +Затем в другом терминале: ```bash -$ RUSTFLAGS="--cfg tokio_unstable" cargo run # Required compile-time flag -$ tokio-console # Connects to 127.0.0.1:6669 +$ RUSTFLAGS="--cfg tokio_unstable" cargo run # Обязательный флаг на этапе компиляции +$ tokio-console # Подключается к 127.0.0.1:6669 ``` -#### tracing + #[instrument]: Structured Logging for Async +#### tracing + #[instrument]: структурированное логирование для async -The [`tracing`](https://docs.rs/tracing) crate understands `Future` lifetimes. Spans stay open across `.await` points, giving you a logical call stack even when the OS thread has moved on: +Крейт [`tracing`](https://docs.rs/tracing) понимает времена жизни `Future`. Span остаются открытыми через точки `.await`, давая логический стек вызовов, даже когда поток ОС уже переключился на другую работу: ```rust use tracing::{info, instrument}; #[instrument(skip(db_pool), fields(user_id = %user_id))] async fn handle_request(user_id: u64, db_pool: &Pool) -> Result { - info!("looking up user"); - let user = db_pool.get_user(user_id).await?; // span stays open across .await - info!(email = %user.email, "found user"); - let orders = fetch_orders(user_id).await?; // still the same span + info!("ищем пользователя"); + let user = db_pool.get_user(user_id).await?; // span остаётся открытым через .await + info!(email = %user.email, "пользователь найден"); + let orders = fetch_orders(user_id).await?; // всё тот же span Ok(build_response(user, orders)) } ``` -Output (with `tracing_subscriber::fmt::json()`): +Вывод (с `tracing_subscriber::fmt::json()`): ```json -{"timestamp":"...","level":"INFO","span":{"name":"handle_request","user_id":"42"},"message":"looking up user"} -{"timestamp":"...","level":"INFO","span":{"name":"handle_request","user_id":"42"},"fields":{"email":"a@b.com"},"message":"found user"} +{"timestamp":"...","level":"INFO","span":{"name":"handle_request","user_id":"42"},"message":"ищем пользователя"} +{"timestamp":"...","level":"INFO","span":{"name":"handle_request","user_id":"42"},"fields":{"email":"a@b.com"},"message":"пользователь найден"} ``` -#### Debugging Checklist +#### Чек-лист отладки -| Symptom | Likely Cause | Tool | -|---------|-------------|------| -| Task hangs forever | Missing `.await` or deadlocked `Mutex` | `tokio-console` task view | -| Low throughput | Blocking call on async thread | `tokio-console` poll-time histogram | -| `Future is not Send` | Non-Send type held across `.await` | Compiler error + `#[instrument]` to locate | -| Mysterious cancellation | Parent `select!` dropped a branch | `tracing` span lifecycle events | +| Симптом | Вероятная причина | Инструмент | +|---------|-------------------|------------| +| Задача зависает навсегда | Пропущен `.await` или взаимная блокировка `Mutex` | Представление задач в `tokio-console` | +| Низкая пропускная способность | Блокирующий вызов в асинхронном потоке | Гистограмма времени опроса в `tokio-console` | +| `Future is not Send` | Тип !Send удерживается через `.await` | Ошибка компилятора + `#[instrument]` для поиска места | +| Загадочная отмена | Родительский `select!` отбросил ветку | События жизненного цикла span в `tracing` | -> **Tip**: Enable `RUSTFLAGS="--cfg tokio_unstable"` to get task-level metrics -> in tokio-console. This is a compile-time flag, not a runtime one. +> **Совет**: включите `RUSTFLAGS="--cfg tokio_unstable"`, чтобы получить метрики на уровне задач +> в tokio-console. Это флаг времени компиляции, а не времени выполнения. -### Testing Async Code +### Тестирование асинхронного кода -Async code introduces unique testing challenges — you need a runtime, time control, and strategies for testing concurrent behavior. +Асинхронный код создаёт особые трудности для тестирования: нужны рантайм, управление временем и стратегии проверки конкурентного поведения. -**Basic async tests** with `#[tokio::test]`: +**Базовые асинхронные тесты** с `#[tokio::test]`: ```rust // Cargo.toml @@ -432,7 +425,7 @@ async fn test_basic_async() { assert_eq!(result, "expected"); } -// Single-threaded test (useful for !Send types): +// Однопоточный тест (полезно для типов !Send): #[tokio::test(flavor = "current_thread")] async fn test_single_threaded() { let rc = std::rc::Rc::new(42); @@ -440,10 +433,10 @@ async fn test_single_threaded() { assert_eq!(val, 42); } -// Multi-threaded with explicit worker count: +// Многопоточный тест с явным количеством воркеров: #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_concurrent_behavior() { - // Tests race conditions with real concurrency + // Проверяет гонки при реальной конкурентности let counter = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); let c1 = counter.clone(); let c2 = counter.clone(); @@ -457,27 +450,27 @@ async fn test_concurrent_behavior() { } ``` -**Time manipulation** — test timeouts without actually waiting: +**Управление временем** — тестируем таймауты, не дожидаясь их: ```rust use tokio::time::{self, Duration, Instant}; #[tokio::test] async fn test_timeout_behavior() { - // Pause time — sleep() advances instantly, no real wall-clock delay + // Останавливаем время — sleep() переходит вперёд мгновенно, без реальной задержки time::pause(); let start = Instant::now(); - time::sleep(Duration::from_secs(3600)).await; // "waits" 1 hour — takes 0ms + time::sleep(Duration::from_secs(3600)).await; // «ждём» 1 час — занимает 0 мс assert!(start.elapsed() >= Duration::from_secs(3600)); - // Test ran in milliseconds, not an hour! + // Тест выполнился за миллисекунды, а не за час! } #[tokio::test] async fn test_retry_timing() { time::pause(); - // Test that our retry logic waits the expected durations + // Проверяем, что логика повторов ждёт ожидаемое время let start = Instant::now(); let result = retry_with_backoff(|| async { Err::<(), _>("simulated failure") @@ -485,7 +478,7 @@ async fn test_retry_timing() { .await; assert!(result.is_err()); - // 1s + 2s + 4s = 7s of backoff (exponential) + // 1с + 2с + 4с = 7с ожидания (экспоненциально) assert!(start.elapsed() >= Duration::from_secs(7)); } @@ -496,30 +489,30 @@ async fn test_deadline_exceeded() { let result = tokio::time::timeout( Duration::from_secs(5), async { - // Simulate slow operation + // Имитируем медленную операцию time::sleep(Duration::from_secs(10)).await; "done" } ).await; - assert!(result.is_err()); // Timed out + assert!(result.is_err()); // Таймаут } ``` -**Mocking async dependencies** — use trait objects or generics: +**Мокирование асинхронных зависимостей** — используйте трейт-объекты или дженерики: ```rust -// Define a trait for the dependency: +// Определяем трейт для зависимости: trait Storage { async fn get(&self, key: &str) -> Option; async fn set(&self, key: &str, value: String); } -// Production implementation: +// Продакшен-реализация: struct RedisStorage { /* ... */ } impl Storage for RedisStorage { async fn get(&self, key: &str) -> Option { - // Real Redis call + // Реальный вызов Redis todo!() } async fn set(&self, key: &str, value: String) { @@ -527,7 +520,7 @@ impl Storage for RedisStorage { } } -// Test mock: +// Тестовый мок: struct MockStorage { data: std::sync::Mutex>, } @@ -547,7 +540,7 @@ impl Storage for MockStorage { } } -// Tested function is generic over Storage: +// Тестируемая функция обобщена по Storage: async fn cache_lookup(store: &S, key: &str) -> String { match store.get(key).await { Some(val) => val, @@ -563,18 +556,18 @@ async fn cache_lookup(store: &S, key: &str) -> String { async fn test_cache_miss_then_hit() { let mock = MockStorage::new(); - // First call: miss → computes and stores + // Первый вызов: промах → вычисляем и сохраняем let val = cache_lookup(&mock, "key1").await; assert_eq!(val, "computed"); - // Second call: hit → returns stored value + // Второй вызов: попадание → возвращаем сохранённое значение let val = cache_lookup(&mock, "key1").await; assert_eq!(val, "computed"); assert!(mock.data.lock().unwrap().contains_key("key1")); } ``` -**Testing channels and task communication**: +**Тестирование каналов и обмена между задачами**: ```rust #[tokio::test] @@ -585,7 +578,7 @@ async fn test_producer_consumer() { for i in 0..5 { tx.send(i).await.unwrap(); } - // tx dropped here — channel closes + // tx уничтожается здесь — канал закрывается }); let mut received = Vec::new(); @@ -597,23 +590,21 @@ async fn test_producer_consumer() { } ``` -| Test Pattern | When to Use | Key Tool | -|-------------|-------------|----------| -| `#[tokio::test]` | All async tests | `tokio = { features = ["macros", "rt"] }` | -| `time::pause()` | Testing timeouts, retries, periodic tasks | `tokio::time::pause()` | -| Trait mocking | Testing business logic without I/O | Generic `` | -| `current_thread` flavor | Testing `!Send` types or deterministic scheduling | `#[tokio::test(flavor = "current_thread")]` | -| `multi_thread` flavor | Testing race conditions | `#[tokio::test(flavor = "multi_thread")]` | +| Паттерн тестирования | Когда использовать | Ключевой инструмент | +|---------------------|--------------------|---------------------| +| `#[tokio::test]` | Все асинхронные тесты | `tokio = { features = ["macros", "rt"] }` | +| `time::pause()` | Тестирование таймаутов, повторов, периодических задач | `tokio::time::pause()` | +| Мокирование через трейты | Тестирование бизнес-логики без I/O | Обобщение `` | +| Вариант `current_thread` | Тестирование типов `!Send` или детерминированного планирования | `#[tokio::test(flavor = "current_thread")]` | +| Вариант `multi_thread` | Тестирование гонок | `#[tokio::test(flavor = "multi_thread")]` | -> **Key Takeaways — Common Pitfalls** -> - Never block the executor — use `spawn_blocking` for CPU/sync work -> - Never hold a `MutexGuard` across `.await` — scope locks tightly or use `tokio::sync::Mutex` -> - Cancellation drops the future instantly — use "cancel-safe" patterns for partial operations -> - Use `tokio-console` and `#[tracing::instrument]` for debugging async code -> - Test async code with `#[tokio::test]` and `time::pause()` for deterministic timing +> **Ключевые выводы — типичные ловушки** +> - Никогда не блокируйте исполнитель — используйте `spawn_blocking` для работы с CPU или синхронного кода +> - Никогда не удерживайте `MutexGuard` через `.await` — ограничивайте блокировки или используйте `tokio::sync::Mutex` +> - Отмена мгновенно уничтожает future — для частично выполненных операций используйте паттерны, устойчивые к отмене (cancel-safe) +> - Используйте `tokio-console` и `#[tracing::instrument]` для отладки асинхронного кода +> - Тестируйте асинхронный код через `#[tokio::test]` и `time::pause()` для детерминированного тайминга -> **See also:** [Ch 8 — Tokio Deep Dive](ch08-tokio-deep-dive.md) for sync primitives, [Ch 13 — Production Patterns](ch13-production-patterns.md) for graceful shutdown and structured concurrency +> **См. также:** [Гл. 8 — Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) — примитивы синхронизации, [Гл. 13 — Продакшен-паттерны](ch13-production-patterns.md) — graceful shutdown и структурированную конкурентность *** - - diff --git a/async-book/src/ch13-production-patterns.md b/async-book/src/ch13-production-patterns.md index 6ae6e41..4ad8c51 100644 --- a/async-book/src/ch13-production-patterns.md +++ b/async-book/src/ch13-production-patterns.md @@ -1,79 +1,79 @@ -# 13. Production Patterns 🔴 +# 13. Продакшен-паттерны 🔴 -> **What you'll learn:** -> - Graceful shutdown with `watch` channels and `select!` -> - Backpressure: bounded channels prevent OOM -> - Structured concurrency: `JoinSet` and `TaskTracker` -> - Timeouts, retries, and exponential backoff -> - Error handling: `thiserror` vs `anyhow`, the double-`?` pattern -> - Tower: the middleware pattern used by axum, tonic, and hyper +> **Что вы узнаете:** +> - Graceful shutdown с помощью каналов `watch` и `select!` +> - Обратное давление: ограниченные каналы защищают от OOM +> - Структурированная конкурентность: `JoinSet` и `TaskTracker` +> - Таймауты, повторы и экспоненциальная задержка (backoff) +> - Обработка ошибок: `thiserror` против `anyhow`, паттерн двойного `?` +> - Tower: паттерн middleware, который используют axum, tonic и hyper ## Graceful Shutdown -Production servers must shut down cleanly — finish in-flight requests, flush buffers, close connections: +Продакшен-серверы должны корректно завершаться: доводить до конца текущие запросы, сбрасывать буферы, закрывать соединения: ```rust use tokio::signal; use tokio::sync::watch; async fn main_server() { - // Create a shutdown signal channel + // Создаём канал для сигнала остановки let (shutdown_tx, shutdown_rx) = watch::channel(false); - // Spawn the server + // Запускаем сервер let server_handle = tokio::spawn(run_server(shutdown_rx.clone())); - // Wait for Ctrl+C - signal::ctrl_c().await.expect("Failed to listen for Ctrl+C"); - println!("Shutdown signal received, finishing in-flight requests..."); + // Ждём Ctrl+C + signal::ctrl_c().await.expect("Не удалось подписаться на Ctrl+C"); + println!("Получен сигнал остановки, доводим текущие запросы..."); - // Notify all tasks to shut down - // NOTE: .unwrap() is used for brevity. Production code should handle - // the case where all receivers have been dropped. + // Уведомляем все задачи о завершении + // ПРИМЕЧАНИЕ: .unwrap() используется для краткости. В продакшен-коде нужно обрабатывать + // случай, когда все получатели уже уничтожены. shutdown_tx.send(true).unwrap(); - // Wait for server to finish (with timeout) + // Ждём завершения сервера (с таймаутом) match tokio::time::timeout( std::time::Duration::from_secs(30), server_handle, ).await { - Ok(Ok(())) => println!("Server shut down gracefully"), - Ok(Err(e)) => eprintln!("Server error: {e}"), - Err(_) => eprintln!("Server shutdown timed out — forcing exit"), + Ok(Ok(())) => println!("Сервер корректно остановлен"), + Ok(Err(e)) => eprintln!("Ошибка сервера: {e}"), + Err(_) => eprintln!("Таймаут остановки сервера — принудительный выход"), } } async fn run_server(mut shutdown: watch::Receiver) { loop { tokio::select! { - // Accept new connections + // Принимаем новые соединения conn = accept_connection() => { let shutdown = shutdown.clone(); tokio::spawn(handle_connection(conn, shutdown)); } - // Shutdown signal + // Сигнал остановки _ = shutdown.changed() => { if *shutdown.borrow() { - println!("Stopping accepting new connections"); + println!("Прекращаем приём новых соединений"); break; } } } } - // In-flight connections will finish on their own - // because they have their own shutdown_rx clone + // Активные соединения завершатся сами, + // потому что у них есть собственный клон shutdown_rx } async fn handle_connection(conn: Connection, mut shutdown: watch::Receiver) { loop { tokio::select! { request = conn.next_request() => { - // Process the request fully — don't abandon mid-request + // Обрабатываем запрос полностью — не бросаем его на полпути process_request(request).await; } _ = shutdown.changed() => { if *shutdown.borrow() { - // Finish current request, then exit + // Завершаем текущий запрос и выходим break; } } @@ -84,63 +84,63 @@ async fn handle_connection(conn: Connection, mut shutdown: watch::Receiver ```mermaid sequenceDiagram - participant OS as OS Signal - participant Main as Main Task - participant WCH as watch Channel - participant W1 as Worker 1 - participant W2 as Worker 2 + participant OS as Сигнал ОС + participant Main as Главная задача + participant WCH as Канал watch + participant W1 as Воркер 1 + participant W2 as Воркер 2 OS->>Main: SIGINT (Ctrl+C) Main->>WCH: send(true) WCH-->>W1: changed() WCH-->>W2: changed() - Note over W1: Finish current request - Note over W2: Finish current request + Note over W1: Доводим текущий запрос + Note over W2: Доводим текущий запрос - W1-->>Main: Task complete - W2-->>Main: Task complete - Main->>Main: All workers done → exit + W1-->>Main: Задача завершена + W2-->>Main: Задача завершена + Main->>Main: Все воркеры закончили → выход ``` -### Backpressure with Bounded Channels +### Обратное давление через ограниченные каналы -Unbounded channels can lead to OOM if the producer is faster than the consumer. Always use bounded channels in production: +Неограниченные каналы могут привести к OOM, если производитель быстрее потребителя. В продакшене всегда используйте ограниченные каналы: ```rust use tokio::sync::mpsc; async fn backpressure_example() { - // Bounded channel: max 100 items buffered + // Ограниченный канал: не более 100 элементов в буфере let (tx, mut rx) = mpsc::channel::(100); - // Producer: slows down naturally when buffer is full + // Производитель: естественно замедляется, когда буфер заполнен let producer = tokio::spawn(async move { for i in 0..1_000_000 { - // send() is async — waits if buffer is full - // This creates natural backpressure! + // send() асинхронный — ждёт, если буфер полон + // Это создаёт естественное обратное давление! tx.send(WorkItem { id: i }).await.unwrap(); } }); - // Consumer: processes items at its own pace + // Потребитель: обрабатывает элементы в своём темпе let consumer = tokio::spawn(async move { while let Some(item) = rx.recv().await { - process(item).await; // Slow processing is OK — producer waits + process(item).await; // Медленная обработка — нормально, производитель будет ждать } }); let _ = tokio::join!(producer, consumer); } -// Compare with unbounded — DANGEROUS: -// let (tx, rx) = mpsc::unbounded_channel(); // No backpressure! -// Producer can fill memory indefinitely +// Сравните с неограниченным — ОПАСНО: +// let (tx, rx) = mpsc::unbounded_channel(); // Никакого обратного давления! +// Производитель может бесконечно заполнять память ``` -### Structured Concurrency: JoinSet and TaskTracker +### Структурированная конкурентность: JoinSet и TaskTracker -`JoinSet` groups related tasks and ensures they all complete: +`JoinSet` объединяет связанные задачи и гарантирует, что все они завершатся: ```rust use tokio::task::JoinSet; @@ -149,28 +149,28 @@ use tokio::time::{sleep, Duration}; async fn structured_concurrency() { let mut set = JoinSet::new(); - // Spawn a batch of tasks + // Порождаем пачку задач for url in get_urls() { set.spawn(async move { fetch_and_process(url).await }); } - // Collect all results (order not guaranteed) + // Собираем все результаты (порядок не гарантирован) let mut results = Vec::new(); while let Some(result) = set.join_next().await { match result { Ok(Ok(data)) => results.push(data), - Ok(Err(e)) => eprintln!("Task error: {e}"), - Err(e) => eprintln!("Task panicked: {e}"), + Ok(Err(e)) => eprintln!("Ошибка задачи: {e}"), + Err(e) => eprintln!("Задача завершилась паникой: {e}"), } } - // ALL tasks are done here — no dangling background work - println!("Processed {} items", results.len()); + // ВСЕ задачи завершены — фоновой работы не осталось + println!("Обработано элементов: {}", results.len()); } -// TaskTracker (tokio-util 0.7.9+) — wait for all spawned tasks +// TaskTracker (tokio-util 0.7.9+) — ждём все порождённые задачи use tokio_util::task::TaskTracker; async fn with_tracker() { @@ -179,22 +179,22 @@ async fn with_tracker() { for i in 0..10 { tracker.spawn(async move { sleep(Duration::from_millis(100 * i)).await; - println!("Task {i} done"); + println!("Задача {i} завершена"); }); } - tracker.close(); // No more tasks will be added - tracker.wait().await; // Wait for ALL tracked tasks - println!("All tasks finished"); + tracker.close(); // Больше задач добавлять не будут + tracker.wait().await; // Ждём ВСЕ отслеживаемые задачи + println!("Все задачи завершены"); } ``` -### Timeouts and Retries +### Таймауты и повторы ```rust use tokio::time::{timeout, sleep, Duration}; -// Simple timeout +// Простой таймаут async fn with_timeout() -> Result { match timeout(Duration::from_secs(5), fetch_data()).await { Ok(Ok(response)) => Ok(response), @@ -203,7 +203,7 @@ async fn with_timeout() -> Result { } } -// Exponential backoff retry +// Повтор с экспоненциальной задержкой async fn retry_with_backoff( max_attempts: u32, base_delay_ms: u64, @@ -221,80 +221,80 @@ where Ok(result) => return Ok(result), Err(e) => { if attempt == max_attempts { - eprintln!("Final attempt {attempt} failed: {e}"); + eprintln!("Последняя попытка {attempt} не удалась: {e}"); return Err(e); } - eprintln!("Attempt {attempt} failed: {e}, retrying in {delay:?}"); + eprintln!("Попытка {attempt} не удалась: {e}, повтор через {delay:?}"); sleep(delay).await; - delay *= 2; // Exponential backoff + delay *= 2; // Экспоненциальная задержка } } } unreachable!() } -// Usage: +// Использование: // let result = retry_with_backoff(3, 100, || async { // reqwest::get("https://api.example.com/data").await // }).await?; ``` -> **Production tip — add jitter**: The function above uses pure exponential backoff, but in -> production many clients failing simultaneously will all retry at the same intervals (thundering -> herd). Add random *jitter* — e.g., `sleep(delay + rand_jitter)` where `rand_jitter` is -> `0..delay/4` — so retries spread out over time. +> **Совет для продакшена — добавляйте jitter**: функция выше использует чистую экспоненциальную задержку, но в продакшене +> много клиентов, падающих одновременно, будут повторять запросы в одни и те же моменты (эффект «стада», +> thundering herd). Добавьте случайный *jitter* — например, `sleep(delay + rand_jitter)`, где `rand_jitter` — +> это значение из `0..delay/4`, — чтобы повторы распределялись во времени. -### Error Handling in Async Code +### Обработка ошибок в асинхронном коде -Async introduces unique error propagation challenges — spawned tasks create error boundaries, timeout errors wrap inner errors, and `?` interacts differently when futures cross task boundaries. +Асинхронность создаёт особые сложности при распространении ошибок: порождённые задачи образуют границы ошибок, ошибки таймаута оборачивают внутренние ошибки, и `?` ведёт себя иначе, когда futures пересекают границы задач. -**`thiserror` vs `anyhow`** — choosing the right tool: +**`thiserror` против `anyhow`** — выбираем подходящий инструмент: ```rust -// thiserror: Define typed errors for libraries and public APIs -// Every variant is explicit — callers can match on specific errors +// thiserror: определяем типизированные ошибки для библиотек и публичных API +// Каждый вариант явный — вызывающий код может сопоставлять конкретные ошибки use thiserror::Error; #[derive(Error, Debug)] enum DiagError { - #[error("IPMI command failed: {0}")] + #[error("Команда IPMI не выполнена: {0}")] Ipmi(#[from] IpmiError), - #[error("Sensor {sensor} out of range: {value}°C (max {max}°C)")] + #[error("Датчик {sensor} вне диапазона: {value}°C (максимум {max}°C)")] OverTemp { sensor: String, value: f64, max: f64 }, - #[error("Operation timed out after {0:?}")] + #[error("Операция завершилась по таймауту через {0:?}")] Timeout(std::time::Duration), - #[error("Task panicked: {0}")] + #[error("Задача завершилась паникой: {0}")] TaskPanic(#[from] tokio::task::JoinError), } -// anyhow: Quick error handling for applications and prototypes -// Wraps any error — no need to define types for every case +// anyhow: быстрая обработка ошибок для приложений и прототипов +// Оборачивает любую ошибку — не нужно определять типы для каждого случая use anyhow::{Context, Result}; async fn run_diagnostics() -> Result<()> { let config = load_config() .await - .context("Failed to load diagnostic config")?; // Adds context + .context("Не удалось загрузить конфигурацию диагностики")?; // Добавляет контекст let result = run_gpu_test(&config) .await - .context("GPU diagnostic failed")?; // Chains context + .context("Диагностика GPU не выполнена")?; // Строит цепочку контекста Ok(()) } -// anyhow prints: "GPU diagnostic failed: IPMI command failed: timeout" +// anyhow выведет: "Диагностика GPU не выполнена: Команда IPMI не выполнена: timeout" ``` -| Crate | Use When | Error Type | Matching | -|-------|----------|-----------|----------| -| `thiserror` | Library code, public APIs | `enum MyError { ... }` | `match err { MyError::Timeout => ... }` | -| `anyhow` | Applications, CLI tools, scripts | `anyhow::Error` (type-erased) | `err.downcast_ref::()` | -| Both together | Library exposes `thiserror`, app wraps with `anyhow` | Best of both | Library errors are typed, app doesn't care | +| Крейт | Когда использовать | Тип ошибки | Сопоставление | +|-------|--------------------|------------|---------------| +| `thiserror` | Код библиотек, публичные API | `enum MyError { ... }` | `match err { MyError::Timeout => ... }` | +| `anyhow` | Приложения, CLI-утилиты, скрипты | `anyhow::Error` (стёртый тип) | `err.downcast_ref::()` | +| Оба вместе | Библиотека отдаёт `thiserror`, приложение оборачивает `anyhow` | Лучшее из двух | Ошибки библиотеки типизированы, приложению это безразлично | -**The double-`?` pattern** with `tokio::spawn`: +**Паттерн двойного `?`** с `tokio::spawn`: ```rust use thiserror::Error; @@ -302,10 +302,10 @@ use tokio::task::JoinError; #[derive(Error, Debug)] enum AppError { - #[error("HTTP error: {0}")] + #[error("Ошибка HTTP: {0}")] Http(#[from] reqwest::Error), - #[error("Task panicked: {0}")] + #[error("Задача завершилась паникой: {0}")] TaskPanic(#[from] JoinError), } @@ -315,42 +315,42 @@ async fn spawn_with_errors() -> Result { Ok::<_, reqwest::Error>(resp.text().await?) }); - // Double ?: First ? unwraps JoinError (task panic), second ? unwraps inner Result + // Двойной ?: первый ? разворачивает JoinError (паника задачи), второй ? — внутренний Result let result = handle.await??; Ok(result) } ``` -**The error boundary problem** — `tokio::spawn` erases context: +**Проблема границы ошибок** — `tokio::spawn` стирает контекст: ```rust -// ❌ Error context is lost across spawn boundaries: +// ❌ Контекст ошибки теряется на границе spawn: async fn bad_error_handling() -> Result<()> { let handle = tokio::spawn(async { - some_fallible_work().await // Returns Result + some_fallible_work().await // Возвращает Result }); - // handle.await returns Result, JoinError> - // The inner error has no context about what task failed + // handle.await возвращает Result, JoinError> + // У внутренней ошибки нет контекста о том, какая задача упала let result = handle.await??; Ok(()) } -// ✅ Add context at the spawn boundary: +// ✅ Добавляем контекст на границе spawn: async fn good_error_handling() -> Result<()> { let handle = tokio::spawn(async { some_fallible_work() .await - .context("worker task failed") // Context before crossing boundary + .context("ошибка рабочей задачи") // Контекст до пересечения границы }); let result = handle.await - .context("worker task panicked")??; // Context for JoinError too + .context("рабочая задача завершилась паникой")??; // Контекст и для JoinError Ok(()) } ``` -**Timeout errors** — wrapping vs replacing: +**Ошибки таймаута** — оборачивание или замена: ```rust use tokio::time::{timeout, Duration}; @@ -359,18 +359,18 @@ async fn with_timeout_context() -> Result { let dur = Duration::from_secs(30); match timeout(dur, fetch_sensor_data()).await { Ok(Ok(data)) => Ok(data), - Ok(Err(e)) => Err(e), // Inner error preserved - Err(_) => Err(DiagError::Timeout(dur)), // Timeout → typed error + Ok(Err(e)) => Err(e), // Внутренняя ошибка сохраняется + Err(_) => Err(DiagError::Timeout(dur)), // Таймаут → типизированная ошибка } } ``` -### Tower: The Middleware Pattern +### Tower: паттерн middleware -The [Tower](https://docs.rs/tower) crate defines a composable `Service` trait — the backbone of async middleware in Rust (used by `axum`, `tonic`, `hyper`): +Крейт [Tower](https://docs.rs/tower) определяет компонуемый трейт `Service` — основу асинхронных middleware в Rust (его используют `axum`, `tonic`, `hyper`): ```rust -// Tower's core trait (simplified): +// Основной трейт Tower (упрощённо): pub trait Service { type Response; type Error; @@ -381,29 +381,29 @@ pub trait Service { } ``` -Middleware wraps a `Service` to add cross-cutting behavior — logging, timeouts, rate-limiting — without modifying inner logic: +Middleware оборачивает `Service`, добавляя сквозное поведение — логирование, таймауты, ограничение частоты — без изменения внутренней логики: ```rust use tower::{ServiceBuilder, timeout::TimeoutLayer, limit::RateLimitLayer}; use std::time::Duration; let service = ServiceBuilder::new() - .layer(TimeoutLayer::new(Duration::from_secs(10))) // Outermost: timeout - .layer(RateLimitLayer::new(100, Duration::from_secs(1))) // Then: rate limit - .service(my_handler); // Innermost: your code + .layer(TimeoutLayer::new(Duration::from_secs(10))) // Самый внешний: таймаут + .layer(RateLimitLayer::new(100, Duration::from_secs(1))) // Затем: ограничение частоты + .service(my_handler); // Самый внутренний: ваш код ``` -**Why this matters**: If you've used ASP.NET middleware or Express.js middleware, Tower is the Rust equivalent. It's how production Rust services add cross-cutting concerns without code duplication. +**Почему это важно**: если вы использовали middleware в ASP.NET или Express.js, Tower — это его аналог в Rust. Так продакшен-сервисы на Rust добавляют сквозные задачи без дублирования кода. -### Exercise: Graceful Shutdown with Worker Pool +### Упражнение: graceful shutdown с пулом воркеров
-🏋️ Exercise (click to expand) +🏋️ Упражнение (нажмите, чтобы раскрыть) -**Challenge**: Build a task processor with a channel-based work queue, N worker tasks, and graceful shutdown on Ctrl+C. Workers should finish in-flight work before exiting. +**Задача**: постройте обработчик задач с очередью работы на каналах, N задачами-воркерами и graceful shutdown по Ctrl+C. Воркеры должны доделать текущую работу, прежде чем завершиться.
-🔑 Solution +🔑 Решение ```rust use tokio::sync::{mpsc, watch}; @@ -434,7 +434,7 @@ async fn main() { }; match item { Some(work) => { - println!("Worker {id}: processing {}", work.id); + println!("Воркер {id}: обрабатываю {}", work.id); sleep(Duration::from_millis(200)).await; } None => break, @@ -443,33 +443,31 @@ async fn main() { })); } - // Submit work + // Отправляем работу for i in 0..20 { let _ = work_tx.send(WorkItem { id: i, payload: format!("task-{i}") }).await; sleep(Duration::from_millis(50)).await; } - // On Ctrl+C: signal shutdown, wait for workers - // NOTE: .unwrap() is used for brevity — handle errors in production. + // По Ctrl+C: сигнализируем об остановке и ждём воркеров + // ПРИМЕЧАНИЕ: .unwrap() используется для краткости — в продакшене обрабатывайте ошибки. tokio::signal::ctrl_c().await.unwrap(); shutdown_tx.send(true).unwrap(); for h in handles { let _ = h.await; } - println!("Shut down cleanly."); + println!("Корректно завершено."); } ```
-> **Key Takeaways — Production Patterns** -> - Use a `watch` channel + `select!` for coordinated graceful shutdown -> - Bounded channels (`mpsc::channel(N)`) provide **backpressure** — senders block when the buffer is full -> - `JoinSet` and `TaskTracker` provide **structured concurrency**: track, abort, and await task groups -> - Always add timeouts to network operations — `tokio::time::timeout(dur, fut)` -> - Tower's `Service` trait is the standard middleware pattern for production Rust services +> **Ключевые выводы — продакшен-паттерны** +> - Используйте канал `watch` + `select!` для согласованного graceful shutdown +> - Ограниченные каналы (`mpsc::channel(N)`) обеспечивают **обратное давление** — отправители блокируются, когда буфер полон +> - `JoinSet` и `TaskTracker` обеспечивают **структурированную конкурентность**: отслеживание, прерывание и ожидание групп задач +> - Для сетевых операций всегда задавайте таймауты — `tokio::time::timeout(dur, fut)` +> - Трейт `Service` из Tower — стандартный паттерн middleware для продакшен-сервисов на Rust -> **See also:** [Ch 8 — Tokio Deep Dive](ch08-tokio-deep-dive.md) for channels and sync primitives, [Ch 12 — Common Pitfalls](ch12-common-pitfalls.md) for cancellation hazards during shutdown +> **См. также:** [Гл. 8 — Глубокое погружение в Tokio](ch08-tokio-deep-dive.md) — каналы и примитивы синхронизации, [Гл. 12 — Типичные ловушки](ch12-common-pitfalls.md) — опасности отмены при остановке *** - - From 056be9db61d9c3e38505c28cf99330c2a2dc2a24 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 08:55:57 +0400 Subject: [PATCH 09/18] Translate Async Rust book chapters 14-17 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for the async-is-an-optimization chapter, exercises, summary and reference card, and the capstone chat server project. Code identifiers, crate names, file names and links are unchanged. Co-Authored-By: Claude Haiku 5.5 --- ...-is-an-optimization-not-an-architecture.md | 266 +++++++++--------- async-book/src/ch15-exercises.md | 177 ++++++------ .../src/ch16-summary-and-reference-card.md | 159 ++++++----- async-book/src/ch17-capstone-project.md | 194 ++++++------- 4 files changed, 397 insertions(+), 399 deletions(-) diff --git a/async-book/src/ch14-async-is-an-optimization-not-an-architecture.md b/async-book/src/ch14-async-is-an-optimization-not-an-architecture.md index 6a9335b..1ff97da 100644 --- a/async-book/src/ch14-async-is-an-optimization-not-an-architecture.md +++ b/async-book/src/ch14-async-is-an-optimization-not-an-architecture.md @@ -1,84 +1,84 @@ -# 14. Async Is an Optimization, Not an Architecture 🔴 +# 14. Async — это оптимизация, а не архитектура 🔴 -> **What you'll learn:** -> - Why async tends to contaminate entire codebases — and why that's a design flaw, not a feature -> - The "sync core, async shell" pattern for keeping most code testable and debuggable -> - How to handle the hard case: logic that *also* needs I/O -> - When `spawn_blocking` is a fix vs. a symptom -> - When async genuinely belongs in your core logic -> - Why sync-first libraries are more composable than async-first ones +> **Что вы узнаете:** +> - Почему async обычно «заражает» всю кодовую базу — и почему это недостаток дизайна, а не возможность +> - Паттерн «синхронное ядро, асинхронная оболочка», который делает большую часть кода тестируемой и понятной +> - Как справляться со сложным случаем: логикой, которой *тоже* нужен ввод-вывод +> - Когда `spawn_blocking` — это решение, а когда — симптом +> - Когда async действительно уместен в основной логике +> - Почему библиотеки, построенные сначала на синхронном коде, компонуемее библиотек, построенных сначала на async -You've now spent 13 chapters learning async Rust. Here's the most important thing the book hasn't told you: **most of your code shouldn't be async.** +Вы уже прошли 13 глав, изучая асинхронный Rust. Вот самое важное, о чём эта книга до сих пор не говорила: **большая часть вашего кода не должна быть асинхронной.** -## The Function Coloring Problem +## Проблема раскраски функций -Bob Nystrom's ["What Color is Your Function?"](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) identifies the core issue: async functions can call sync functions, but sync functions cannot call async functions. Once one function goes async, everything above it in the call chain must follow. +В статье Боба Ныстрома [«What Color is Your Function?»](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/) описана основная проблема: асинхронные функции могут вызывать синхронные, а синхронные функции не могут вызывать асинхронные. Как только одна функция становится асинхронной, всё, что выше по цепочке вызовов, тоже должно стать асинхронным. -In Rust this is **worse** than in C# or JavaScript, because async doesn't just infect function signatures — it infects types: +В Rust это **хуже**, чем в C# или JavaScript, потому что async заражает не только сигнатуры функций, но и типы: -| Sync code | Async equivalent | Why it's different | +| Синхронный код | Асинхронный аналог | Чем отличается | |---|---|---| -| `fn process(&self)` | `async fn process(&self)` | Callers must be async too | -| `&mut T` | `Arc>` | Spawned tasks need `'static + Send` | -| `std::sync::Mutex` | `tokio::sync::Mutex` | Different type if held across `.await` | -| `impl Trait` return | `impl Future + Send` | Simpler since RPITIT (Rust 1.75, ch10), but still colored | -| `#[test]` | `#[tokio::test]` | Tests need a runtime | -| Stack trace: 5 frames | Stack trace: 25 frames | Half are runtime internals | +| `fn process(&self)` | `async fn process(&self)` | Вызывающие тоже должны быть асинхронными | +| `&mut T` | `Arc>` | Порождаемым задачам нужны `'static + Send` | +| `std::sync::Mutex` | `tokio::sync::Mutex` | Другой тип, если блокировка удерживается через `.await` | +| Возврат `impl Trait` | `impl Future + Send` | Проще с RPITIT (Rust 1.75, гл. 10), но всё равно «окрашено» | +| `#[test]` | `#[tokio::test]` | Тестам нужен рантайм | +| Трассировка стека: 5 кадров | Трассировка стека: 25 кадров | Половина — внутренности рантайма | -Every row is a decision someone must make, get right, and maintain — and none of it is about business logic. The industry is moving *away* from this: Java's Project Loom (virtual threads) and Go's goroutines both let you write synchronous-looking code that the runtime multiplexes cheaply. Rust chose explicit async for zero-cost control, but that control has a complexity cost that should be paid consciously, not by default. +Каждая строка — это решение, которое кто-то должен принять, сделать правильно и поддерживать, и ни одно из них не относится к бизнес-логике. Индустрия движется *от* этого: виртуальные потоки Project Loom в Java и горутины в Go позволяют писать код, который выглядит синхронным, а рантайм дёшево мультиплексирует его. Rust выбрал явный async ради контроля с нулевой стоимостью, но у этого контроля есть цена в сложности, которую стоит платить осознанно, а не по умолчанию. -## "But Threads Are Expensive" +## «Но потоки дорогие» -The reflexive counter: "we need async because threads are expensive." Mostly wrong at the scale where most teams operate. +Рефлекторный аргумент: «нам нужен async, потому что потоки дорогие». В масштабах, в которых работает большинство команд, это в основном неверно. -- **Stack memory:** Each OS thread reserves 8MB of virtual address space (Linux default), but the OS only commits pages as touched — a mostly-idle thread uses 20-80KB of physical memory. -- **Context switches:** ~1-5µs on modern hardware. At 50 concurrent requests, this is noise. At 100K switches/second, it's measurable. -- **Creation cost:** ~10-30µs per thread on Linux. A thread pool (rayon, `std::thread::scope`) amortizes this to zero. +- **Память стека:** каждый поток ОС резервирует 8 МБ виртуального адресного пространства (по умолчанию в Linux), но ОС выделяет физическую память только под реально затронутые страницы — поток, который в основном простаивает, занимает 20–80 КБ физической памяти. +- **Переключение контекста:** ~1–5 мкс на современном железе. При 50 одновременных запросах это шум. При 100 тысячах переключений в секунду это уже заметно. +- **Стоимость создания:** ~10–30 мкс на поток в Linux. Пул потоков (rayon, `std::thread::scope`) амортизирует это до нуля. -The honest threshold where async earns its complexity is roughly **1K-10K concurrent mostly-idle connections** — the epoll/io_uring sweet spot where per-connection stacks become a real cost. Below that, a thread pool is simpler, faster to debug, and fast enough. Above that, async wins. Most services are below that. +Честный порог, при котором async окупает свою сложность, — примерно **1–10 тысяч одновременных, в основном простаивающих соединений**: зона epoll/io_uring, где стеки на соединение становятся реальной стоимостью. Ниже этого порога пул потоков проще, легче отлаживается и достаточно быстр. Выше — побеждает async. Большинство сервисов находятся ниже этого порога. -## The Hard Example: Logic That Also Needs I/O +## Трудный пример: логика, которой тоже нужен ввод-вывод -A trivial pure function — `fn add(a: i32, b: i32) -> i32` — obviously doesn't need async. That's not an interesting lesson. The interesting case is when business logic *seems* to need I/O in the middle: validation that checks inventory, pricing that queries an exchange rate, an order pipeline that looks up a customer. +Тривиальная чистая функция — `fn add(a: i32, b: i32) -> i32` — очевидно не нуждается в async. Это неинтересный урок. Интересный случай — когда бизнес-логике *кажется*, что ей посреди работы нужен ввод-вывод: валидация, которая проверяет остатки на складе, ценообразование, которое запрашивает курс валют, конвейер заказов, который ищет клиента. -Consider an order processing service. The async-everywhere version looks natural: +Рассмотрим сервис обработки заказов. Вариант «async везде» выглядит естественно: -### Version A: Async Through the Core +### Вариант A: async через всё ядро ```rust -// orders.rs — async all the way down +// orders.rs — async до самого низа pub async fn process_order(order: Order) -> Result { - // Step 1: Validate — pure business rules, no I/O + // Шаг 1: валидация — чистые бизнес-правила, без ввода-вывода validate_items(&order)?; validate_quantities(&order)?; - // Step 2: Check inventory — needs a database call + // Шаг 2: проверка остатков — нужен вызов к базе данных let stock = inventory_client.check(&order.items).await?; if !stock.all_available() { return Err(OrderError::OutOfStock(stock.missing())); } - // Step 3: Calculate pricing — pure math, but async because we're already here + // Шаг 3: расчёт цены — чистая арифметика, но async, потому что мы уже здесь let pricing = calculate_pricing(&order, &stock); - // Step 4: Apply discount — needs an external service call + // Шаг 4: применение скидки — нужен вызов внешнего сервиса let discount = discount_service.lookup(order.customer_id).await?; let final_price = pricing.apply_discount(discount); - // Step 5: Format receipt — pure + // Шаг 5: формирование чека — чисто Ok(Receipt::new(order, final_price)) } ``` -This is *reasonable* async code. No `Arc` abuse — just sequential awaits. Most developers would write it this way and move on. But look at what happened: `validate_items`, `validate_quantities`, `calculate_pricing`, and `Receipt::new` are all pure functions that got dragged into an async context because steps 2 and 4 need I/O. The entire function must be async, its tests need a runtime, and every caller up the chain is now colored. +Это *разумный* асинхронный код. Никакого злоупотребления `Arc` — просто последовательные `await`. Большинство разработчиков написали бы именно так и пошли дальше. Но посмотрите, что произошло: `validate_items`, `validate_quantities`, `calculate_pricing` и `Receipt::new` — это чистые функции, которые затянуло в асинхронный контекст, потому что шагам 2 и 4 нужен ввод-вывод. Вся функция должна стать асинхронной, её тестам нужен рантайм, и каждый вызывающий выше по цепочке тоже оказывается «окрашенным». -### Version B: Sync Core, Async Shell +### Вариант B: синхронное ядро, асинхронная оболочка -The alternative: separate *what to decide* from *how to fetch*: +Альтернатива: отделить *что решить* от *как получить данные*: ```rust -// core.rs — pure business logic, zero async, zero tokio dependency +// core.rs — чистая бизнес-логика, ноль async, ноль зависимостей от tokio pub fn validate_order(order: &Order) -> Result { validate_items(order)?; @@ -107,42 +107,42 @@ pub fn finalize( ``` ```rust -// shell.rs — thin async orchestrator +// shell.rs — тонкий асинхронный оркестратор // -// Note: the `?` on network calls requires `impl From for OrderError` -// (or a unified error enum). See ch12 for async error handling patterns. +// Примечание: `?` на сетевых вызовах требует `impl From for OrderError` +// (или единого перечисления ошибок). См. гл. 12 — паттерны обработки ошибок в async. use crate::core; pub async fn process_order(order: Order) -> Result { - // Sync: validate + // Синхронно: валидация let validated = core::validate_order(&order)?; - // Async: fetch inventory (this is the shell's job) + // Асинхронно: получаем остатки (это задача оболочки) let stock = inventory_client.check(&validated.items).await?; - // Sync: apply business rule to fetched data + // Синхронно: применяем бизнес-правило к полученным данным let stocked = core::check_stock(&validated, &stock)?; - // Async: fetch discount + // Асинхронно: получаем скидку let discount = discount_service.lookup(order.customer_id).await?; - // Sync: finalize + // Синхронно: финализируем Ok(core::finalize(&stocked, discount)) } ``` -The async shell is a **pipeline of fetch → decide → fetch → decide**. Each "decide" step is a sync function that takes the I/O result as input instead of reaching out for it. +Асинхронная оболочка — это **конвейер «получить → решить → получить → решить»**. Каждый шаг «решить» — синхронная функция, которая получает результат ввода-вывода как входные данные, а не ходит за ним сама. -### Testing the Difference +### Тестирование разницы -The sync core tests every business rule without a runtime or mocks: +Синхронное ядро проверяет каждое бизнес-правило без рантайма и моков: ```rust #[test] fn out_of_stock_rejects_order() { let order = validated_order(vec![item("widget", 10)]); - let stock = stock_result(vec![("widget", 3)]); // only 3 available + let stock = stock_result(vec![("widget", 3)]); // доступно только 3 let result = core::check_stock(&order, &stock); assert_eq!(result.unwrap_err(), OrderError::OutOfStock(vec!["widget"])); @@ -150,137 +150,137 @@ fn out_of_stock_rejects_order() { #[test] fn discount_applied_correctly() { - let order = stocked_order(100_00); // price in cents + let order = stocked_order(100_00); // цена в копейках let receipt = core::finalize(&order, Discount::Percent(15)); assert_eq!(receipt.final_price, 85_00); } ``` -The async shell gets a thinner *integration* test that verifies the wiring, not the logic: +Асинхронная оболочка получает более тонкий *интеграционный* тест, который проверяет связку компонентов, а не логику: ```rust #[tokio::test] async fn process_order_integration() { - let mock_inventory = mock_service(/* returns stock */); - let mock_discounts = mock_service(/* returns 10% */); + let mock_inventory = mock_service(/* возвращает остатки */); + let mock_discounts = mock_service(/* возвращает 10% */); let receipt = process_order(sample_order()).await.unwrap(); assert!(receipt.final_price > 0); - // Logic correctness is already proven by core tests above + // Корректность логики уже доказана тестами ядра выше } ``` -### Why This Matters +### Почему это важно -| Concern | Async through the core | Sync core + async shell | +| Аспект | Async через всё ядро | Синхронное ядро + асинхронная оболочка | |---|---|---| -| Business rules testable without runtime | No | **Yes** | -| Number of unit tests needing `#[tokio::test]` | All of them | **Only integration tests** | -| I/O failures entangled with logic errors | Yes — one `Result` type for both | **No** — sync returns logic errors, shell handles I/O errors | -| `validate_order` reusable in CLI / WASM / batch | No — pulls in tokio transitively | **Yes** — pure `fn` | -| Stack traces through business logic | Interleaved with runtime frames | **Clean** | -| Can swap HTTP client for gRPC later | Requires changing core functions | **Shell change only** | +| Бизнес-правила тестируются без рантайма | Нет | **Да** | +| Число модульных тестов, которым нужен `#[tokio::test]` | Все | **Только интеграционные** | +| Ошибки ввода-вывода перемешаны с логическими ошибками | Да — один тип `Result` для обоих | **Нет** — синхронный код возвращает логические ошибки, оболочка обрабатывает ошибки I/O | +| `validate_order` переиспользуем в CLI / WASM / пакетной обработке | Нет — тянет tokio транзитивно | **Да** — чистая `fn` | +| Трассировки стека через бизнес-логику | Перемешаны с кадрами рантайма | **Чистые** | +| HTTP-клиент можно заменить на gRPC позже | Нужно менять функции ядра | **Меняется только оболочка** | -The key insight: **the I/O calls in steps 2 and 4 don't *need* to be inside the business logic. They're inputs to it.** The sync core takes `StockResult` and `Discount` as arguments. Where those values came from — HTTP, gRPC, a test fixture, a cache — is the shell's concern. +Ключевая мысль: **вызовы I/O на шагах 2 и 4 не обязаны находиться внутри бизнес-логики. Они — её входные данные.** Синхронное ядро принимает `StockResult` и `Discount` как аргументы. Откуда пришли эти значения — из HTTP, gRPC, тестовой фикстуры или кэша — это забота оболочки. -## The `spawn_blocking` Smell +## Запах `spawn_blocking` -Chapter 12 introduced `spawn_blocking` as a fix for accidentally blocking the executor. That's the right fix when you have a one-off blocking call — `std::fs::read`, a compression library, a legacy FFI function. +В главе 12 `spawn_blocking` был представлен как средство от случайной блокировки исполнителя. Это правильное решение для разовых блокирующих вызовов — `std::fs::read`, библиотеки сжатия, устаревшей FFI-функции. -But if you find yourself wrapping large sections of code in `spawn_blocking`: +Но если вы обнаруживаете, что оборачиваете в `spawn_blocking` большие участки кода: ```rust async fn handler(req: Request) -> Response { - // If this is your codebase, the boundary is in the wrong place + // Если это ваша кодовая база, граница проведена не там tokio::task::spawn_blocking(move || { - let validated = validate(&req); // sync - let enriched = enrich(validated); // sync - let result = process(enriched); // sync - let output = format_response(result); // sync + let validated = validate(&req); // синхронно + let enriched = enrich(validated); // синхронно + let result = process(enriched); // синхронно + let output = format_response(result); // синхронно output }).await.unwrap() } ``` -...that's the codebase telling you: **this logic was never async to begin with.** You don't need `spawn_blocking` — you need a sync module that the async handler calls directly: +...то это кодовая база сообщает вам: **эта логика никогда не была асинхронной.** `spawn_blocking` здесь не нужен — нужен синхронный модуль, который асинхронный обработчик вызывает напрямую: ```rust async fn handler(req: Request) -> Response { - // validate → enrich → process → format are all sync. - // No spawn_blocking needed — they're fast and CPU-light. + // validate → enrich → process → format — всё синхронное. + // spawn_blocking не нужен: это быстро и почти не нагружает CPU. let response = my_core::handle(req); response } ``` -Reserve `spawn_blocking` for genuinely heavy CPU work (parsing large payloads, image processing, compression) where the time cost would actually starve the executor. For ordinary business logic that runs in microseconds, a direct sync call is simpler and correct. +Оставляйте `spawn_blocking` для действительно тяжёлой работы с CPU (разбор больших полезных нагрузок, обработка изображений, сжатие), где затраты времени реально могут заморить исполнитель. Для обычной бизнес-логики, которая выполняется за микросекунды, прямой синхронный вызов проще и корректен. -## Libraries: Sync First, Async Wrapper Optional +## Библиотеки: сначала синхронные, асинхронная обёртка — по желанию -The boundary question is even more consequential for library authors. A sync library can be used from both sync and async callers: +Вопрос границы ещё важнее для авторов библиотек. Синхронная библиотека может использоваться как из синхронного, так и из асинхронного кода: ```rust -// A sync library — usable everywhere +// Синхронная библиотека — пригодна везде let report = my_lib::analyze(&data); -// Caller A: sync CLI +// Вызывающий A: синхронная CLI-утилита fn main() { let report = my_lib::analyze(&data); println!("{report}"); } -// Caller B: async handler, works fine +// Вызывающий B: асинхронный обработчик, работает нормально async fn handler() -> Json { - let report = my_lib::analyze(&data); // sync call in async context — fine + let report = my_lib::analyze(&data); // синхронный вызов в async-контексте — нормально Json(report) } -// Caller C: heavy analysis — caller decides to offload +// Вызывающий C: тяжёлый анализ — вызывающий решает разгрузить его async fn handler_heavy() -> Json { let data = data.clone(); let report = tokio::task::spawn_blocking(move || { - my_lib::analyze(&data) // caller controls the async boundary + my_lib::analyze(&data) // граница async контролирует вызывающий }).await.unwrap(); Json(report) } ``` -An async library forces *all* callers into a runtime: +Асинхронная библиотека загоняет *всех* вызывающих в рантайм: ```rust -// An async library — only usable from async contexts -let report = my_lib::analyze(&data).await; // caller MUST be async +// Асинхронная библиотека — пригодна только из асинхронного контекста +let report = my_lib::analyze(&data).await; // вызывающий ОБЯЗАН быть асинхронным -// Sync caller? Now you need block_on — and hope there's no nested runtime +// Синхронный вызывающий? Теперь нужен block_on — и надежда, что нет вложенного рантайма let report = tokio::runtime::Runtime::new().unwrap().block_on( my_lib::analyze(&data) -); // fragile, panic-prone if already inside a runtime +); // хрупко, может паниковать, если мы уже внутри рантайма ``` -**Default to sync APIs.** If your library does pure computation, data transformation, or parsing, there is no reason for it to be async. If it does I/O, consider offering a sync core with an optional async convenience layer behind a feature flag — let the caller own the boundary decision. +**По умолчанию проектируйте синхронные API.** Если ваша библиотека выполняет чистые вычисления, преобразование данных или разбор, нет причин делать её асинхронной. Если она выполняет ввод-вывод, рассмотрите синхронное ядро с необязательным асинхронным удобным слоем за feature-флагом — пусть вызывающий сам решает, где граница. -## When Async Belongs in the Core +## Когда async принадлежит ядру -Not everything can be cleanly separated. Async belongs in your core logic when: +Не всё можно аккуратно разделить. Async уместен в основной логике, когда: -- **Fan-out/fan-in is the logic.** If your business rule is "query 5 pricing services concurrently and return the cheapest," the concurrency *is* the logic, not plumbing. Forcing this through sync + threads is reinventing a worse async. +- **Fan-out/fan-in и есть логика.** Если бизнес-правило звучит как «одновременно запросить 5 сервисов ценообразования и вернуть самый дешёвый», конкурентность — это *и есть* логика, а не сантехника. Пытаться протащить это через синхронный код и потоки — значит заново изобретать худший async. -- **Streaming is the logic.** Processing a continuous event stream with backpressure — the stream management is non-trivial business logic, not just an I/O wrapper. +- **Потоковая обработка и есть логика.** Обработка непрерывного потока событий с обратным давлением — управление потоком нетривиальная бизнес-логика, а не просто обёртка над I/O. -- **Long-lived stateful connections.** WebSocket handlers, gRPC bidirectional streams, and protocol state machines have state transitions inherently tied to I/O events. The capstone project in [ch17](ch17-capstone-project.md) — an async chat server — is exactly this case: concurrent connections, room-based fan-out, and graceful shutdown are fundamentally async work. +- **Долгоживущие состоятельные соединения.** Обработчики WebSocket, двунаправленные потоки gRPC и конечные автоматы протоколов имеют переходы состояний, неразрывно связанные с событиями ввода-вывода. Итоговый проект в [гл. 17](ch17-capstone-project.md) — асинхронный чат-сервер — как раз такой случай: одновременные соединения, рассылка по комнатам и корректное завершение по своей природе являются асинхронной работой. -**The test:** if removing `async` from a function would require replacing it with threads, channels, or manual polling, then async is pulling its weight. If removing `async` would just mean deleting the keyword with no other changes, it never needed to be async. +**Проверка:** если удаление `async` из функции потребовало бы заменить её потоками, каналами или ручным опросом, значит async оправдывает себя. Если же удаление `async` сводится к тому, чтобы просто убрать ключевое слово без других изменений, в асинхронности не было необходимости. -## Decision Rule +## Правило принятия решения ```mermaid graph TD - START["Should this function be async?"] --> IO{"Does it do I/O?"} - IO -->|No| SYNC["sync fn — always"] - IO -->|Yes| BOUNDARY{"Is it at the boundary?
handler, main loop, accept()"} - BOUNDARY -->|Yes| ASYNC_SHELL["async fn — this is the shell"] - BOUNDARY -->|No| CORE_IO{"Is the I/O the core logic?
fan-out, streaming, stateful conn"} - CORE_IO -->|Yes| ASYNC_CORE["async fn — justified"] - CORE_IO -->|No| EXTRACT["Extract logic into sync fn.
Pass I/O results in as arguments."] + START["Должна ли эта функция быть async?"] --> IO{"Она выполняет I/O?"} + IO -->|Нет| SYNC["sync fn — всегда"] + IO -->|Да| BOUNDARY{"Она на границе?
обработчик, главный цикл, accept()"} + BOUNDARY -->|Да| ASYNC_SHELL["async fn — это оболочка"] + BOUNDARY -->|Нет| CORE_IO{"Является ли I/O основной логикой?
fan-out, потоковая обработка, состоятельное соединение"} + CORE_IO -->|Да| ASYNC_CORE["async fn — оправдано"] + CORE_IO -->|Нет| EXTRACT["Вынесите логику в sync fn.
Передавайте результаты I/O как аргументы."] style SYNC fill:#d4efdf,stroke:#27ae60,color:#000 style ASYNC_SHELL fill:#e8f4f8,stroke:#2980b9,color:#000 @@ -288,26 +288,26 @@ graph TD style EXTRACT fill:#d4efdf,stroke:#27ae60,color:#000 ``` -> **Rule of thumb:** Start sync. Add async only at the outermost I/O boundary. Pull it inward only when you can articulate *which concurrent I/O operations* justify the complexity tax. +> **Эмпирическое правило:** начинайте с синхронного кода. Добавляйте async только на самой внешней границе ввода-вывода. Перемещайте его внутрь, только когда можете назвать, *какие конкурентные операции ввода-вывода* оправдывают налог на сложность. ---
-🏋️ Exercise: Extract the Sync Core (click to expand) +🏋️ Упражнение: выделите синхронное ядро (нажмите, чтобы раскрыть) -The following axum handler has async contamination — business logic mixed with I/O. Refactor it into a sync core module and a thin async shell. +Следующий обработчик axum «заражён» async: бизнес-логика смешана с вводом-выводом. Отрефакторите его в модуль синхронного ядра и тонкую асинхронную оболочку. ```rust use axum::{Json, extract::Path}; async fn get_device_report(Path(device_id): Path) -> Result, AppError> { - // Fetch raw telemetry from the device over HTTP + // Получаем сырую телеметрию с устройства по HTTP let raw = reqwest::get(format!("http://bmc-{device_id}/telemetry")) .await? .json::() .await?; - // Business logic: convert raw sensor readings to calibrated values + // Бизнес-логика: преобразуем сырые показания датчиков в калиброванные значения let mut readings = Vec::new(); for sensor in &raw.sensors { let calibrated = (sensor.raw_value as f64) * sensor.scale + sensor.offset; @@ -324,7 +324,7 @@ async fn get_device_report(Path(device_id): Path) -> Result }); } - // Business logic: classify device health + // Бизнес-логика: классифицируем состояние устройства let critical_count = readings.iter() .filter(|r| r.value > 90.0) .count(); @@ -332,7 +332,7 @@ async fn get_device_report(Path(device_id): Path) -> Result else if critical_count > 0 { Health::Warning } else { Health::Ok }; - // Fetch device metadata from inventory service + // Получаем метаданные устройства из сервиса инвентаризации let meta = reqwest::get(format!("http://inventory/devices/{device_id}")) .await? .json::() @@ -348,21 +348,21 @@ async fn get_device_report(Path(device_id): Path) -> Result } ``` -**Your goals:** +**Ваши задачи:** -1. Create `core.rs` with sync functions: `calibrate_sensors`, `classify_health`, and `build_report` -2. Create `shell.rs` with a thin async handler that fetches, then calls the sync core -3. Write `#[test]` (not `#[tokio::test]`) for: a sensor out of range, health classification thresholds, and a normal report +1. Создайте `core.rs` с синхронными функциями: `calibrate_sensors`, `classify_health` и `build_report` +2. Создайте `shell.rs` с тонким асинхронным обработчиком, который сначала получает данные, а затем вызывает синхронное ядро +3. Напишите `#[test]` (а не `#[tokio::test]`) для: датчика вне диапазона, порогов классификации состояния и обычного отчёта -**Hints:** -- The sync core should take `RawTelemetry` and `DeviceMetadata` as inputs — it should never know those came from HTTP. -- You'll need to define small test helper functions (e.g., `raw_telemetry()`, `sensor()`, `reading()`, `device_meta()`) that construct test fixtures. Their signatures should be obvious from usage. +**Подсказки:** +- Синхронное ядро должно принимать `RawTelemetry` и `DeviceMetadata` как входные данные — оно никогда не должно знать, что они пришли по HTTP. +- Понадобятся небольшие вспомогательные функции для тестов (например, `raw_telemetry()`, `sensor()`, `reading()`, `device_meta()`), которые конструируют тестовые фикстуры. Их сигнатуры должны быть очевидны из использования.
-🔑 Solution +🔑 Решение ```rust -// core.rs — zero async dependency +// core.rs — ноль зависимостей от async pub fn calibrate_sensors(raw: &RawTelemetry) -> Result, AppError> { raw.sensors.iter().map(|sensor| { @@ -406,7 +406,7 @@ pub fn build_report( ``` ```rust -// shell.rs — async boundary only +// shell.rs — только асинхронная граница pub async fn get_device_report( Path(device_id): Path, @@ -428,9 +428,9 @@ pub async fn get_device_report( ``` ```rust -// core_tests.rs — no runtime needed +// core_tests.rs — рантайм не нужен -// Test fixture helpers — construct data without any I/O +// Вспомогательные функции для фикстур — создают данные без какого-либо ввода-вывода fn sensor(name: &str, raw_value: f64, valid_range: std::ops::Range) -> RawSensor { RawSensor { name: name.into(), @@ -484,19 +484,19 @@ fn normal_report() { } ``` -**What changed:** The async handler went from 30 lines of mixed logic and I/O to 8 lines of pure orchestration. The business rules (calibration math, range validation, health thresholds) are now tested with `#[test]`, run in milliseconds, and have zero dependency on tokio, reqwest, or any HTTP mock server. +**Что изменилось:** асинхронный обработчик сократился с 30 строк смешанной логики и ввода-вывода до 8 строк чистой оркестрации. Бизнес-правила (математика калибровки, проверка диапазонов, пороги состояния) теперь проверяются через `#[test]`, выполняются за миллисекунды и не зависят ни от tokio, ни от reqwest, ни от какого-либо мок-сервера HTTP.
--- -> **Key Takeaways:** +> **Ключевые выводы:** > -> 1. Async is an **I/O multiplexing optimization**, not an application architecture. Most business logic is sync. -> 2. **Sync core, async shell:** keep business rules in pure sync functions that take I/O results as arguments. The async shell orchestrates fetches and calls the core. -> 3. If you're wrapping large blocks in `spawn_blocking`, **the boundary is in the wrong place** — refactor the logic into a sync module instead. -> 4. **Libraries should default to sync APIs.** An async library forces all callers into a runtime; a sync library lets the caller own the async boundary. -> 5. Async earns its keep for **fan-out/fan-in, streaming, and stateful connections** — the cases where the concurrency *is* the business logic. +> 1. Async — это **оптимизация мультиплексирования I/O**, а не архитектура приложения. Большая часть бизнес-логики синхронна. +> 2. **Синхронное ядро, асинхронная оболочка:** держите бизнес-правила в чистых синхронных функциях, которые принимают результаты I/O как аргументы. Асинхронная оболочка организует запросы и вызывает ядро. +> 3. Если вы оборачиваете большие блоки в `spawn_blocking`, **граница проведена не там** — вместо этого вынесите логику в синхронный модуль. +> 4. **Библиотеки должны по умолчанию предлагать синхронные API.** Асинхронная библиотека заставляет всех вызывающих работать с рантаймом; синхронная позволяет вызывающему самому определять асинхронную границу. +> 5. Async окупает себя в **fan-out/fan-in, потоковой обработке и состоятельных соединениях** — там, где конкурентность *и есть* бизнес-логика. > -> **See also:** [Ch12 — Common Pitfalls](ch12-common-pitfalls.md) (spawn_blocking as a tactical fix) · [Ch13 — Production Patterns](ch13-production-patterns.md) (backpressure, structured concurrency) · [Ch17 — Capstone: Async Chat Server](ch17-capstone-project.md) (a case where async is the right architecture) +> **См. также:** [Гл. 12 — Типичные ловушки](ch12-common-pitfalls.md) (spawn_blocking как тактическое решение) · [Гл. 13 — Продакшен-паттерны](ch13-production-patterns.md) (обратное давление, структурированная конкурентность) · [Гл. 17 — Итоговый проект: асинхронный чат-сервер](ch17-capstone-project.md) (случай, когда async — правильная архитектура) diff --git a/async-book/src/ch15-exercises.md b/async-book/src/ch15-exercises.md index 5b991e7..3cdce76 100644 --- a/async-book/src/ch15-exercises.md +++ b/async-book/src/ch15-exercises.md @@ -1,17 +1,17 @@ -## Exercises +## Упражнения -### Exercise 1: Async Echo Server +### Упражнение 1: асинхронный эхо-сервер -Build a TCP echo server that handles multiple clients concurrently. +Напишите TCP-эхо-сервер, который обрабатывает нескольких клиентов одновременно. -**Requirements**: -- Listen on `127.0.0.1:8080` -- Accept connections and echo back each line -- Handle client disconnections gracefully -- Print a log when clients connect/disconnect +**Требования**: +- Слушать `127.0.0.1:8080` +- Принимать соединения и возвращать обратно каждую строку +- Корректно обрабатывать отключения клиентов +- Выводить лог при подключении и отключении клиентов
-🔑 Solution +🔑 Решение ```rust use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -20,11 +20,11 @@ use tokio::net::TcpListener; #[tokio::main] async fn main() -> Result<(), Box> { let listener = TcpListener::bind("127.0.0.1:8080").await?; - println!("Echo server listening on :8080"); + println!("Эхо-сервер слушает :8080"); loop { let (socket, addr) = listener.accept().await?; - println!("[{addr}] Connected"); + println!("[{addr}] Подключён"); tokio::spawn(async move { let (reader, mut writer) = socket.into_split(); @@ -35,18 +35,18 @@ async fn main() -> Result<(), Box> { line.clear(); match reader.read_line(&mut line).await { Ok(0) => { - println!("[{addr}] Disconnected"); + println!("[{addr}] Отключён"); break; } Ok(_) => { - print!("[{addr}] Echo: {line}"); + print!("[{addr}] Эхо: {line}"); if writer.write_all(line.as_bytes()).await.is_err() { - println!("[{addr}] Write error, disconnecting"); + println!("[{addr}] Ошибка записи, отключаем"); break; } } Err(e) => { - eprintln!("[{addr}] Read error: {e}"); + eprintln!("[{addr}] Ошибка чтения: {e}"); break; } } @@ -60,24 +60,24 @@ async fn main() -> Result<(), Box> { --- -### Exercise 2: Concurrent URL Fetcher with Rate Limiting +### Упражнение 2: конкурентный загрузчик URL с ограничением частоты -Fetch a list of URLs concurrently, with at most 5 concurrent requests. +Загрузите список URL одновременно, но не более 5 запросов одновременно.
-🔑 Solution +🔑 Решение ```rust use futures::stream::{self, StreamExt}; use tokio::time::{sleep, Duration}; async fn fetch_urls(urls: Vec) -> Vec> { - // buffer_unordered(5) ensures at most 5 futures are polled - // concurrently — no separate Semaphore needed here. + // buffer_unordered(5) гарантирует, что опрашивается не более 5 future + // одновременно — отдельный Semaphore здесь не нужен. let results: Vec<_> = stream::iter(urls) .map(|url| { async move { - println!("Fetching: {url}"); + println!("Загружаем: {url}"); match reqwest::get(&url).await { Ok(resp) => match resp.text().await { @@ -88,31 +88,31 @@ async fn fetch_urls(urls: Vec) -> Vec> { } } }) - .buffer_unordered(5) // ← This alone limits concurrency to 5 + .buffer_unordered(5) // ← Этого достаточно, чтобы ограничить конкурентность 5 .collect() .await; results } -// NOTE: Use Semaphore when you need to limit concurrency across -// independently spawned tasks (tokio::spawn). Use buffer_unordered -// when processing a stream. Don't combine both for the same limit. +// ПРИМЕЧАНИЕ: используйте Semaphore, когда нужно ограничить конкурентность +// для независимо порождённых задач (tokio::spawn). Используйте buffer_unordered, +// когда обрабатываете поток. Не комбинируйте оба механизма для одного и того же лимита. ```
--- -### Exercise 3: Graceful Shutdown with Worker Pool +### Упражнение 3: graceful shutdown с пулом воркеров -Build a task processor with: -- A channel-based work queue -- N worker tasks consuming from the queue -- Graceful shutdown on Ctrl+C: stop accepting, finish in-flight work +Постройте обработчик задач с: +- очередью работы на каналах +- N задачами-воркерами, которые читают из очереди +- graceful shutdown по Ctrl+C: прекратить приём, доделать текущую работу
-🔑 Solution +🔑 Решение ```rust use tokio::sync::{mpsc, watch}; @@ -128,7 +128,7 @@ async fn main() { let (work_tx, work_rx) = mpsc::channel::(100); let (shutdown_tx, shutdown_rx) = watch::channel(false); - // Spawn 4 workers + // Запускаем 4 воркера let mut worker_handles = Vec::new(); let work_rx = std::sync::Arc::new(tokio::sync::Mutex::new(work_rx)); @@ -149,12 +149,12 @@ async fn main() { match item { Some(work) => { - println!("Worker {id}: processing item {}", work.id); - sleep(Duration::from_millis(200)).await; // Simulate work - println!("Worker {id}: done with item {}", work.id); + println!("Воркер {id}: обрабатываю элемент {}", work.id); + sleep(Duration::from_millis(200)).await; // Имитация работы + println!("Воркер {id}: закончил элемент {}", work.id); } None => { - println!("Worker {id}: channel closed, exiting"); + println!("Воркер {id}: канал закрыт, выходим"); break; } } @@ -163,7 +163,7 @@ async fn main() { worker_handles.push(handle); } - // Producer: submit some work + // Производитель: отправляет работу let producer = tokio::spawn(async move { for i in 0..20 { let _ = work_tx.send(WorkItem { @@ -174,17 +174,17 @@ async fn main() { } }); - // Wait for Ctrl+C + // Ждём Ctrl+C tokio::signal::ctrl_c().await.unwrap(); - println!("\nShutdown signal received!"); + println!("\nПолучен сигнал остановки!"); shutdown_tx.send(true).unwrap(); - producer.abort(); // Cancel the producer task + producer.abort(); // Отменяем производителя - // Wait for workers to finish + // Ждём, пока воркеры завершатся for handle in worker_handles { let _ = handle.await; } - println!("All workers shut down. Goodbye!"); + println!("Все воркеры остановлены. До свидания!"); } ``` @@ -192,14 +192,14 @@ async fn main() { --- -### Exercise 4: Build a Simple Async Mutex from Scratch +### Упражнение 4: простой асинхронный мьютекс с нуля -Implement an async-aware mutex using channels (without using `tokio::sync::Mutex`). +Реализуйте асинхронный мьютекс на основе каналов (без `tokio::sync::Mutex`). -*Hint*: Use a `tokio::sync::Semaphore` with 1 permit to serialize access. +*Подсказка*: используйте `tokio::sync::Semaphore` с одним разрешением, чтобы сериализовать доступ.
-🔑 Solution +🔑 Решение ```rust use std::cell::UnsafeCell; @@ -211,13 +211,13 @@ pub struct SimpleAsyncMutex { semaphore: Arc, } -// SAFETY: Access to T is serialized by the semaphore (max 1 permit). +// SAFETY: доступ к T сериализуется семафором (не более одного разрешения). unsafe impl Send for SimpleAsyncMutex {} unsafe impl Sync for SimpleAsyncMutex {} pub struct SimpleGuard { data: Arc>, - _permit: OwnedSemaphorePermit, // Dropped on guard drop → releases lock + _permit: OwnedSemaphorePermit, // Уничтожается вместе с guard → освобождает блокировку } impl SimpleAsyncMutex { @@ -240,53 +240,53 @@ impl SimpleAsyncMutex { impl std::ops::Deref for SimpleGuard { type Target = T; fn deref(&self) -> &T { - // SAFETY: We hold the only semaphore permit, so no other - // SimpleGuard exists → exclusive access is guaranteed. + // SAFETY: у нас единственное разрешение семафора, поэтому других + // SimpleGuard не существует → эксклюзивный доступ гарантирован. unsafe { &*self.data.get() } } } impl std::ops::DerefMut for SimpleGuard { fn deref_mut(&mut self) -> &mut T { - // SAFETY: Same reasoning — single permit guarantees exclusivity. + // SAFETY: то же рассуждение — единственное разрешение гарантирует эксклюзивность. unsafe { &mut *self.data.get() } } } -// When SimpleGuard is dropped, _permit is dropped, -// which releases the semaphore permit — another lock() can proceed. +// Когда SimpleGuard уничтожается, уничтожается и _permit, +// что освобождает разрешение семафора — следующий lock() может продолжить. -// Usage: +// Использование: // let mutex = SimpleAsyncMutex::new(vec![1, 2, 3]); // { // let mut guard = mutex.lock().await; // guard.push(4); -// } // permit released here +// } // разрешение освобождено здесь ``` -**Key takeaway**: Async mutexes are typically built on top of semaphores. The semaphore provides the async wait mechanism — when locked, `acquire()` suspends the task until the permit is released. This is exactly how `tokio::sync::Mutex` works internally. +**Ключевой вывод**: асинхронные мьютексы обычно строятся поверх семафоров. Семафор обеспечивает асинхронное ожидание: когда блокировка занята, `acquire()` приостанавливает задачу до освобождения разрешения. Именно так `tokio::sync::Mutex` работает внутри. -> **Why `UnsafeCell` and not `std::sync::Mutex`?** A previous version of this -> exercise used `Arc>` with `Deref`/`DerefMut` calling `.lock().unwrap()`. -> That doesn't compile — the returned `&T` borrows from a temporary `MutexGuard` -> that's dropped immediately. `UnsafeCell` avoids the intermediate guard, and the -> semaphore-based serialization makes the `unsafe` sound. +> **Почему `UnsafeCell`, а не `std::sync::Mutex`?** В предыдущей версии этого +> упражнения использовался `Arc>` с `Deref`/`DerefMut`, которые вызывали `.lock().unwrap()`. +> Это не компилируется — возвращаемая `&T` заимствует данные у временного `MutexGuard`, +> который уничтожается сразу. `UnsafeCell` обходит промежуточный guard, а сериализация +> через семафор делает `unsafe` корректным.
--- -### Exercise 5: Stream Pipeline +### Упражнение 5: конвейер потоков -Build a data processing pipeline using streams: -1. Generate numbers 1..=100 -2. Filter to even numbers -3. Map each to its square -4. Process 10 at a time concurrently (simulate with sleep) -5. Collect results +Постройте конвейер обработки данных на потоках: +1. Сгенерируйте числа 1..=100 +2. Отфильтруйте чётные +3. Возведите каждое в квадрат +4. Обрабатывайте по 10 штук одновременно (имитация через sleep) +5. Соберите результаты
-🔑 Solution +🔑 Решение ```rust use futures::stream::{self, StreamExt}; @@ -295,23 +295,23 @@ use tokio::time::{sleep, Duration}; #[tokio::main] async fn main() { let results: Vec = stream::iter(1u64..=100) - // Step 2: Filter evens + // Шаг 2: отфильтровать чётные .filter(|x| futures::future::ready(x % 2 == 0)) - // Step 3: Square each + // Шаг 3: возвести каждое в квадрат .map(|x| x * x) - // Step 4: Process concurrently (simulate async work) + // Шаг 4: обрабатывать одновременно (имитация асинхронной работы) .map(|x| async move { sleep(Duration::from_millis(50)).await; - println!("Processed: {x}"); + println!("Обработано: {x}"); x }) - .buffer_unordered(10) // 10 concurrent - // Step 5: Collect + .buffer_unordered(10) // 10 одновременно + // Шаг 5: собрать .collect() .await; - println!("Got {} results", results.len()); - println!("Sum: {}", results.iter().sum::()); + println!("Получено результатов: {}", results.len()); + println!("Сумма: {}", results.iter().sum::()); } ``` @@ -319,14 +319,14 @@ async fn main() { --- -### Exercise 6: Implement Select with Timeout +### Упражнение 6: реализуйте select с таймаутом -Without using `tokio::select!` or `tokio::time::timeout`, implement a function that races a future against a deadline and returns `Either::Left(result)` or `Either::Right(())` on timeout. +Не используя `tokio::select!` и `tokio::time::timeout`, реализуйте функцию, которая состязает future с дедлайном и возвращает `Either::Left(result)` или `Either::Right(())` при таймауте. -*Hint*: Build on the `Select` combinator from Chapter 6 and the `TimerFuture` from the same chapter. +*Подсказка*: опирайтесь на комбинатор `Select` из главы 6 и `TimerFuture` из той же главы.
-🔑 Solution +🔑 Решение ```rust,ignore use std::future::Future; @@ -341,7 +341,7 @@ pub enum Either { pub struct Timeout { future: F, - timer: TimerFuture, // From Chapter 6 + timer: TimerFuture, // Из главы 6 } impl Timeout { @@ -357,12 +357,12 @@ impl Future for Timeout { type Output = Either; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - // Check if the main future is done + // Проверяем, завершился ли основной future if let Poll::Ready(val) = Pin::new(&mut self.future).poll(cx) { return Poll::Ready(Either::Left(val)); } - // Check if the timer expired + // Проверяем, не истёк ли таймер if let Poll::Ready(()) = Pin::new(&mut self.timer).poll(cx) { return Poll::Ready(Either::Right(())); } @@ -371,16 +371,15 @@ impl Future for Timeout { } } -// Usage: +// Использование: // match Timeout::new(fetch_data(), Duration::from_secs(5)).await { -// Either::Left(data) => println!("Got data: {data}"), -// Either::Right(()) => println!("Timed out!"), +// Either::Left(data) => println!("Получены данные: {data}"), +// Either::Right(()) => println!("Время вышло!"), // } ``` -**Key takeaway**: `select`/`timeout` is just polling two futures and seeing which completes first. The entire async ecosystem is built from this simple primitive: poll, Pending/Ready, Waker. +**Ключевой вывод**: `select`/`timeout` — это просто опрос двух future и выяснение, какой завершится первым. Вся асинхронная экосистема построена на этом простом примитиве: poll, Pending/Ready, Waker.
*** - diff --git a/async-book/src/ch16-summary-and-reference-card.md b/async-book/src/ch16-summary-and-reference-card.md index 3b3026d..c6b80e7 100644 --- a/async-book/src/ch16-summary-and-reference-card.md +++ b/async-book/src/ch16-summary-and-reference-card.md @@ -1,106 +1,105 @@ -# Summary and Reference Card +# Итоги и справочная карточка -## Quick Reference Card +## Краткая справочная карточка -### Async Mental Model +### Ментальная модель async ```text ┌─────────────────────────────────────────────────────┐ -│ async fn → State Machine (enum) → impl Future │ -│ .await → poll() the inner future │ -│ executor → loop { poll(); sleep_until_woken(); } │ -│ waker → "hey executor, poll me again" │ -│ Pin → "promise I won't move in memory" │ +│ async fn → конечный автомат (enum) → impl Future │ +│ .await → poll() вложенного future │ +│ исполнитель → loop { poll(); спать_до_пробуждения; }│ +│ waker → «эй, исполнитель, опроси меня снова» │ +│ Pin → «обещаю, что не переместят в памяти» │ └─────────────────────────────────────────────────────┘ ``` -### Common Patterns Cheat Sheet - -| Goal | Use | -|------|-----| -| Run two futures concurrently | `tokio::join!(a, b)` | -| Race two futures | `tokio::select! { ... }` | -| Spawn a background task | `tokio::spawn(async { ... })` | -| Run blocking code in async | `tokio::task::spawn_blocking(\|\| { ... })` | -| Limit concurrency | `Semaphore::new(N)` | -| Collect many task results | `JoinSet` | -| Share state across tasks | `Arc>` or channels | +### Шпаргалка по частым паттернам + +| Задача | Решение | +|--------|---------| +| Запустить два future одновременно | `tokio::join!(a, b)` | +| Состязать два future | `tokio::select! { ... }` | +| Запустить фоновую задачу | `tokio::spawn(async { ... })` | +| Выполнить блокирующий код в async | `tokio::task::spawn_blocking(\|\| { ... })` | +| Ограничить конкурентность | `Semaphore::new(N)` | +| Собрать результаты многих задач | `JoinSet` | +| Делить состояние между задачами | `Arc>` или каналы | | Graceful shutdown | `watch::channel` + `select!` | -| Process a stream N-at-a-time | `.buffer_unordered(N)` | -| Timeout a future | `tokio::time::timeout(dur, fut)` | -| Retry with backoff | Custom combinator (see Ch. 13) | +| Обрабатывать поток по N элементов | `.buffer_unordered(N)` | +| Установить таймаут на future | `tokio::time::timeout(dur, fut)` | +| Повтор с задержкой | Собственный комбинатор (см. гл. 13) | -### Pinning Quick Reference +### Краткая справка по закреплению (pinning) -| Situation | Use | -|-----------|-----| -| Pin a future on the heap | `Box::pin(fut)` | -| Pin a future on the stack | `tokio::pin!(fut)` | -| Pin an `Unpin` type | `Pin::new(&mut val)` — safe, free | -| Return a pinned trait object | `-> Pin + Send>>` | +| Ситуация | Что использовать | +|----------|------------------| +| Закрепить future в куче | `Box::pin(fut)` | +| Закрепить future на стеке | `tokio::pin!(fut)` | +| Закрепить тип `Unpin` | `Pin::new(&mut val)` — безопасно и бесплатно | +| Вернуть закреплённый трейт-объект | `-> Pin + Send>>` | -### Channel Selection Guide +### Выбор канала -| Channel | Producers | Consumers | Values | Use When | -|---------|-----------|-----------|--------|----------| -| `mpsc` | N | 1 | Stream | Work queues, event buses | -| `oneshot` | 1 | 1 | Single | Request/response, completion notification | -| `broadcast` | N | N | All recv all | Fan-out notifications, shutdown signals | -| `watch` | 1 | N | Latest only | Config updates, health status | +| Канал | Отправители | Получатели | Значения | Когда использовать | +|-------|-------------|------------|----------|--------------------| +| `mpsc` | N | 1 | Поток | Очереди задач, шины событий | +| `oneshot` | 1 | 1 | Одно | Запрос/ответ, уведомление о завершении | +| `broadcast` | N | N | Все получают всё | Рассылка уведомлений, сигналы остановки | +| `watch` | 1 | N | Только последнее | Обновления конфигурации, статус здоровья | -### Mutex Selection Guide +### Выбор мьютекса -| Mutex | Use When | -|-------|----------| -| `std::sync::Mutex` | Lock is held briefly, never across `.await` | -| `tokio::sync::Mutex` | Lock must be held across `.await` | -| `parking_lot::Mutex` | High contention, no `.await`, need performance | -| `tokio::sync::RwLock` | Many readers, few writers, locks cross `.await` | +| Мьютекс | Когда использовать | +|---------|--------------------| +| `std::sync::Mutex` | Блокировка удерживается недолго, никогда через `.await` | +| `tokio::sync::Mutex` | Блокировку нужно удерживать через `.await` | +| `parking_lot::Mutex` | Высокая конкуренция, без `.await`, нужна производительность | +| `tokio::sync::RwLock` | Много читателей, мало писателей, блокировки пересекают `.await` | -### Decision Quick Reference +### Краткая справка по решениям ```text -Need concurrency? -├── I/O-bound → async/await -├── CPU-bound → rayon / std::thread -└── Mixed → spawn_blocking for CPU parts - -Choosing runtime? -├── Server app → tokio -├── Library → runtime-agnostic (futures crate) -├── Embedded → embassy -└── Minimal → smol - -Need concurrent futures? -├── Can be 'static + Send → tokio::spawn -├── Can be 'static + !Send → LocalSet -├── Can't be 'static → FuturesUnordered -└── Need to track/abort → JoinSet +Нужна конкурентность? +├── Ограничена I/O → async/await +├── Ограничена CPU → rayon / std::thread +└── Смешанная → spawn_blocking для CPU-части + +Выбор рантайма? +├── Серверное приложение → tokio +├── Библиотека → не привязана к рантайму (крейт futures) +├── Встраиваемая система → embassy +└── Минимальный → smol + +Нужны конкурентные future? +├── Могут быть 'static + Send → tokio::spawn +├── Могут быть 'static + !Send → LocalSet +├── Не могут быть 'static → FuturesUnordered +└── Нужно отслеживать/прерывать → JoinSet ``` -### Common Error Messages and Fixes +### Частые сообщения об ошибках и их исправление -| Error | Cause | Fix | -|-------|-------|-----| -| `future is not Send` | Holding `!Send` type across `.await` | Scope the value so it's dropped before `.await`, or use `current_thread` runtime | -| `borrowed value does not live long enough` in spawn | `tokio::spawn` requires `'static` | Use `Arc`, `clone()`, or `FuturesUnordered` | -| `the trait Future is not implemented for ()` | Missing `.await` | Add `.await` to the async call | -| `cannot borrow as mutable` in poll | Self-referential borrow | Use `Pin<&mut Self>` correctly (see Ch. 4) | -| Program hangs silently | Forgot to call `waker.wake()` | Ensure every `Pending` path registers and triggers the waker | +| Ошибка | Причина | Исправление | +|--------|---------|-------------| +| `future is not Send` | Тип `!Send` удерживается через `.await` | Ограничьте область видимости значения, чтобы оно уничтожалось до `.await`, или используйте рантайм `current_thread` | +| `borrowed value does not live long enough` в spawn | `tokio::spawn` требует `'static` | Используйте `Arc`, `clone()` или `FuturesUnordered` | +| `the trait Future is not implemented for ()` | Пропущен `.await` | Добавьте `.await` к асинхронному вызову | +| `cannot borrow as mutable` в poll | Самоссылающееся заимствование | Правильно используйте `Pin<&mut Self>` (см. гл. 4) | +| Программа молча зависает | Забыли вызвать `waker.wake()` | Убедитесь, что каждый путь с `Pending` регистрирует waker и вызывает его | -### Further Reading +### Дополнительная литература -| Resource | Why | -|----------|-----| -| [Tokio Tutorial](https://tokio.rs/tokio/tutorial) | Official hands-on guide — excellent for first projects | -| [Async Book (official)](https://rust-lang.github.io/async-book/) | Covers `Future`, `Pin`, `Stream` at the language level | -| [Jon Gjengset — Crust of Rust: async/await](https://www.youtube.com/watch?v=ThjvMReOXYM) | 2-hour deep dive into internals with live coding | -| [Alice Ryhl — Actors with Tokio](https://ryhl.io/blog/actors-with-tokio/) | Production architecture pattern for stateful services | -| [Without Boats — Pin, Unpin, and why Rust needs them](https://without.boats/blog/pin/) | The original motivation from the language designer | -| [Tokio mini-Redis](https://github.com/tokio-rs/mini-redis) | Complete async Rust project — study-quality production code | -| [Tower documentation](https://docs.rs/tower) | Middleware/service architecture used by axum, tonic, hyper | +| Ресурс | Зачем | +|--------|-------| +| [Tokio Tutorial](https://tokio.rs/tokio/tutorial) | Официальное практическое руководство — отлично для первых проектов | +| [Async Book (официальная)](https://rust-lang.github.io/async-book/) | Рассказывает о `Future`, `Pin`, `Stream` на уровне языка | +| [Jon Gjengset — Crust of Rust: async/await](https://www.youtube.com/watch?v=ThjvMReOXYM) | Двухчасовой глубокий разбор внутреннего устройства с живым кодом | +| [Alice Ryhl — Actors with Tokio](https://ryhl.io/blog/actors-with-tokio/) | Архитектурный паттерн для продакшен-сервисов с состоянием | +| [Without Boats — Pin, Unpin, and why Rust needs them](https://without.boats/blog/pin/) | Исходная мотивация от автора языка | +| [Tokio mini-Redis](https://github.com/tokio-rs/mini-redis) | Полный асинхронный проект на Rust — учебный код продакшен-качества | +| [Документация Tower](https://docs.rs/tower) | Архитектура middleware/сервисов, которую используют axum, tonic, hyper | *** -*End of Async Rust Training Guide* - +*Конец руководства по асинхронному Rust* diff --git a/async-book/src/ch17-capstone-project.md b/async-book/src/ch17-capstone-project.md index 0f7e6c5..f31d6b4 100644 --- a/async-book/src/ch17-capstone-project.md +++ b/async-book/src/ch17-capstone-project.md @@ -1,38 +1,38 @@ -# Capstone Project: Async Chat Server +# Итоговый проект: асинхронный чат-сервер -This project integrates patterns from across the book into a single, production-style application. You'll build a **multi-room async chat server** using tokio, channels, streams, graceful shutdown, and proper error handling. +Этот проект объединяет паттерны из всей книги в одно приложение продакшен-уровня. Вы построите **многокомнатный асинхронный чат-сервер** с использованием tokio, каналов, потоков, graceful shutdown и корректной обработки ошибок. -**Estimated time**: 4–6 hours | **Difficulty**: ★★★ +**Расчётное время**: 4–6 часов | **Сложность**: ★★★ -> **What you'll practice:** -> - `tokio::spawn` and the `'static` requirement (Ch 8) -> - Channels: `mpsc` for messages, `broadcast` for rooms, `watch` for shutdown (Ch 8) -> - Streams: reading lines from TCP connections (Ch 11) -> - Common pitfalls: cancellation safety, MutexGuard across `.await` (Ch 12) -> - Production patterns: graceful shutdown, backpressure (Ch 13) -> - Async traits for pluggable backends (Ch 10) +> **Что вы потренируете:** +> - `tokio::spawn` и требование `'static` (гл. 8) +> - Каналы: `mpsc` для сообщений, `broadcast` для комнат, `watch` для остановки (гл. 8) +> - Потоки: чтение строк из TCP-соединений (гл. 11) +> - Типичные ловушки: безопасность при отмене, MutexGuard через `.await` (гл. 12) +> - Продакшен-паттерны: graceful shutdown, обратное давление (гл. 13) +> - Async-трейты для подключаемых бэкендов (гл. 10) -## The Problem +## Постановка задачи -Build a TCP chat server where: +Постройте TCP-чат-сервер, где: -1. **Clients** connect via TCP and join named rooms -2. **Messages** are broadcast to all clients in the same room -3. **Commands**: `/join `, `/nick `, `/rooms`, `/quit` -4. The server shuts down gracefully on Ctrl+C — finishing in-flight messages +1. **Клиенты** подключаются по TCP и входят в именованные комнаты +2. **Сообщения** рассылаются всем клиентам в той же комнате +3. **Команды**: `/join <комната>`, `/nick <имя>`, `/rooms`, `/quit` +4. Сервер корректно завершается по Ctrl+C — доставляя сообщения, которые уже в пути ```mermaid graph LR - C1["Client 1
(Alice)"] -->|TCP| SERVER["Chat Server"] - C2["Client 2
(Bob)"] -->|TCP| SERVER - C3["Client 3
(Carol)"] -->|TCP| SERVER + C1["Клиент 1
(Alice)"] -->|TCP| SERVER["Чат-сервер"] + C2["Клиент 2
(Bob)"] -->|TCP| SERVER + C3["Клиент 3
(Carol)"] -->|TCP| SERVER - SERVER --> R1["#general
broadcast channel"] - SERVER --> R2["#rust
broadcast channel"] + SERVER --> R1["#general
канал broadcast"] + SERVER --> R2["#rust
канал broadcast"] - R1 -->|msg| C1 - R1 -->|msg| C2 - R2 -->|msg| C3 + R1 -->|сообщение| C1 + R1 -->|сообщение| C2 + R2 -->|сообщение| C3 CTRL["Ctrl+C"] -->|watch| SERVER @@ -42,9 +42,9 @@ graph LR style CTRL fill:#fadbd8,stroke:#e74c3c,color:#000 ``` -## Step 1: Basic TCP Accept Loop +## Шаг 1: базовый цикл принятия TCP-соединений -Start with a server that accepts connections and echoes lines back: +Начните с сервера, который принимает соединения и возвращает строки обратно: ```rust use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -53,11 +53,11 @@ use tokio::net::TcpListener; #[tokio::main] async fn main() -> anyhow::Result<()> { let listener = TcpListener::bind("127.0.0.1:8080").await?; - println!("Chat server listening on :8080"); + println!("Чат-сервер слушает :8080"); loop { let (socket, addr) = listener.accept().await?; - println!("[{addr}] Connected"); + println!("[{addr}] Подключён"); tokio::spawn(async move { let (reader, mut writer) = socket.into_split(); @@ -73,17 +73,17 @@ async fn main() -> anyhow::Result<()> { } } } - println!("[{addr}] Disconnected"); + println!("[{addr}] Отключён"); }); } } ``` -**Your job**: Verify this compiles and works with `telnet localhost 8080`. +**Ваша задача**: убедитесь, что это компилируется и работает через `telnet localhost 8080`. -## Step 2: Room State with Broadcast Channels +## Шаг 2: состояние комнат с broadcast-каналами -Each room is a `broadcast::Sender`. All clients in a room subscribe to receive messages. +Каждая комната — это `broadcast::Sender`. Все клиенты в комнате подписываются, чтобы получать сообщения. ```rust use std::collections::HashMap; @@ -95,40 +95,40 @@ type RoomMap = Arc>>>; fn get_or_create_room(rooms: &mut HashMap>, name: &str) -> broadcast::Sender { rooms.entry(name.to_string()) .or_insert_with(|| { - let (tx, _) = broadcast::channel(100); // 100-message buffer + let (tx, _) = broadcast::channel(100); // Буфер на 100 сообщений tx }) .clone() } ``` -**Your job**: Implement room state so that: -- Clients start in `#general` -- `/join ` switches rooms (unsubscribe from old, subscribe to new) -- Messages are broadcast to all clients in the sender's current room +**Ваша задача**: реализуйте состояние комнат так, чтобы: +- Клиенты начинают в `#general` +- `/join <комната>` переключает комнату (отписка от старой, подписка на новую) +- Сообщения рассылаются всем клиентам в текущей комнате отправителя
-💡 Hint — Client task structure +💡 Подсказка — структура клиентской задачи -Each client task needs two concurrent loops: -1. **Read from TCP** → parse commands or broadcast to room -2. **Read from broadcast receiver** → write to TCP +Каждой клиентской задаче нужны два конкурентных цикла: +1. **Чтение из TCP** → разбор команд или рассылка в комнату +2. **Чтение из broadcast-приёмника** → запись в TCP -Use `tokio::select!` to run both: +Используйте `tokio::select!`, чтобы запускать оба: ```rust loop { tokio::select! { - // Client sent us a line + // Клиент прислал строку result = reader.read_line(&mut line) => { match result { Ok(0) | Err(_) => break, Ok(_) => { - // Parse command or broadcast message + // Разобрать команду или разослать сообщение } } } - // Room broadcast received + // Получено сообщение комнаты result = room_rx.recv() => { match result { Ok(msg) => { @@ -143,42 +143,42 @@ loop {
-## Step 3: Commands +## Шаг 3: команды -Implement the command protocol: +Реализуйте протокол команд: -| Command | Action | -|---------|--------| -| `/join ` | Leave current room, join new room, announce in both | -| `/nick ` | Change display name | -| `/rooms` | List all active rooms and member counts | -| `/quit` | Disconnect gracefully | -| Anything else | Broadcast as a chat message | +| Команда | Действие | +|---------|----------| +| `/join <комната>` | Покинуть текущую комнату, войти в новую, объявить об этом в обеих | +| `/nick <имя>` | Сменить отображаемое имя | +| `/rooms` | Показать все активные комнаты и число участников | +| `/quit` | Корректно отключиться | +| Всё остальное | Разослать как обычное сообщение чата | -**Your job**: Parse commands from the input line. For `/rooms`, you'll need to read from the `RoomMap` — use `RwLock::read()` to avoid blocking other clients. +**Ваша задача**: разберите команды из входной строки. Для `/rooms` нужно читать из `RoomMap` — используйте `RwLock::read()`, чтобы не блокировать других клиентов. -## Step 4: Graceful Shutdown +## Шаг 4: graceful shutdown -Add Ctrl+C handling so the server: -1. Stops accepting new connections -2. Sends "Server shutting down..." to all rooms -3. Waits for in-flight messages to drain -4. Exits cleanly +Добавьте обработку Ctrl+C, чтобы сервер: +1. Прекратил приём новых соединений +2. Отправил «Сервер останавливается...» во все комнаты +3. Дождался, пока сообщения в пути будут доставлены +4. Корректно завершился ```rust use tokio::sync::watch; let (shutdown_tx, shutdown_rx) = watch::channel(false); -// In the accept loop: +// В цикле принятия соединений: loop { tokio::select! { result = listener.accept() => { let (socket, addr) = result?; - // spawn client task with shutdown_rx.clone() + // запускаем клиентскую задачу с shutdown_rx.clone() } _ = tokio::signal::ctrl_c() => { - println!("Shutdown signal received"); + println!("Получен сигнал остановки"); shutdown_tx.send(true)?; break; } @@ -186,45 +186,45 @@ loop { } ``` -**Your job**: Add `shutdown_rx.changed()` to each client's `select!` loop so clients exit when shutdown is signaled. +**Ваша задача**: добавьте `shutdown_rx.changed()` в `select!`-цикл каждого клиента, чтобы клиенты завершались при сигнале остановки. -## Step 5: Error Handling and Edge Cases +## Шаг 5: обработка ошибок и граничных случаев -Production-harden the server: +Усильте сервер для продакшена: -1. **Lagging receivers**: `broadcast::recv()` returns `RecvError::Lagged(n)` if a slow client misses messages. Handle it gracefully (log + continue, don't crash). -2. **Nickname validation**: Reject empty or too-long nicknames. -3. **Backpressure**: The broadcast channel buffer is bounded (100). If a client can't keep up, they get the `Lagged` error. -4. **Timeout**: Disconnect clients that are idle for >5 minutes. +1. **Отстающие получатели**: `broadcast::recv()` возвращает `RecvError::Lagged(n)`, если медленный клиент пропустил сообщения. Обработайте это аккуратно (запишите в лог и продолжайте, не падайте). +2. **Валидация имени**: отклоняйте пустые и слишком длинные никнеймы. +3. **Обратное давление**: буфер broadcast-канала ограничен (100). Если клиент не успевает, он получает ошибку `Lagged`. +4. **Таймаут**: отключайте клиентов, которые простаивают больше 5 минут. ```rust use tokio::time::{timeout, Duration}; -// Wrap the read in a timeout: +// Оборачиваем чтение в таймаут: match timeout(Duration::from_secs(300), reader.read_line(&mut line)).await { - Ok(Ok(0)) | Ok(Err(_)) | Err(_) => break, // EOF, error, or timeout - Ok(Ok(_)) => { /* process line */ } + Ok(Ok(0)) | Ok(Err(_)) | Err(_) => break, // конец потока, ошибка или таймаут + Ok(Ok(_)) => { /* обрабатываем строку */ } } ``` -## Step 6: Integration Test +## Шаг 6: интеграционный тест -Write a test that starts the server, connects two clients, and verifies message delivery: +Напишите тест, который запускает сервер, подключает двух клиентов и проверяет доставку сообщений: ```rust #[tokio::test] async fn two_clients_can_chat() { - // Start server in background - let server = tokio::spawn(run_server("127.0.0.1:0")); // Port 0 = OS picks + // Запускаем сервер в фоне + let server = tokio::spawn(run_server("127.0.0.1:0")); // Порт 0 = ОС выберет сама - // Connect two clients + // Подключаем двух клиентов let mut client1 = TcpStream::connect(addr).await.unwrap(); let mut client2 = TcpStream::connect(addr).await.unwrap(); - // Client 1 sends a message + // Клиент 1 отправляет сообщение client1.write_all(b"Hello from client 1\n").await.unwrap(); - // Client 2 should receive it + // Клиент 2 должен его получить let mut buf = vec![0u8; 1024]; let n = client2.read(&mut buf).await.unwrap(); let msg = String::from_utf8_lossy(&buf[..n]); @@ -232,25 +232,25 @@ async fn two_clients_can_chat() { } ``` -## Evaluation Criteria +## Критерии оценки -| Criterion | Target | -|-----------|--------| -| Concurrency | Multiple clients in multiple rooms, no blocking | -| Correctness | Messages only go to clients in the same room | -| Graceful shutdown | Ctrl+C drains messages and exits cleanly | -| Error handling | Lagged receivers, disconnections, timeouts handled | -| Code organization | Clean separation: accept loop, client task, room state | -| Testing | At least 2 integration tests | +| Критерий | Цель | +|----------|------| +| Конкурентность | Много клиентов в нескольких комнатах, без блокировок | +| Корректность | Сообщения попадают только клиентам той же комнаты | +| Graceful shutdown | Ctrl+C доставляет сообщения и корректно завершает работу | +| Обработка ошибок | Отстающие получатели, отключения и таймауты обработаны | +| Организация кода | Чёткое разделение: цикл принятия, клиентская задача, состояние комнат | +| Тестирование | Минимум 2 интеграционных теста | -## Extension Ideas +## Идеи для расширения -Once the basic chat server works, try these enhancements: +Когда базовый чат-сервер заработает, попробуйте такие улучшения: -1. **Persistent history**: Store last N messages per room; replay to new joiners -2. **WebSocket support**: Accept both TCP and WebSocket clients using `tokio-tungstenite` -3. **Rate limiting**: Use `tokio::time::Interval` to limit messages per client per second -4. **Metrics**: Track connected clients, messages/sec, room count via `prometheus` crate -5. **TLS**: Add `tokio-rustls` for encrypted connections +1. **Постоянная история**: храните последние N сообщений каждой комнаты и проигрывайте их новым участникам +2. **Поддержка WebSocket**: принимайте и TCP-, и WebSocket-клиентов с помощью `tokio-tungstenite` +3. **Ограничение частоты**: используйте `tokio::time::Interval`, чтобы ограничить число сообщений от клиента в секунду +4. **Метрики**: отслеживайте число подключённых клиентов, сообщений в секунду и число комнат через крейт `prometheus` +5. **TLS**: добавьте `tokio-rustls` для зашифрованных соединений *** From 6d49b0e5ae31e80371ae7e48aada39a7c77e3b15 Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 09:13:40 +0400 Subject: [PATCH 10/18] Translate Python book intro, TOC and chapters 1-9 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings in python-book/src for SUMMARY.md, ch00 through ch09. Code identifiers, crate names, file names and links are unchanged. TOC anchors in ch00 are updated to the Russian heading slugs. Co-Authored-By: Claude Haiku 5.5 --- python-book/src/SUMMARY.md | 46 +-- python-book/src/ch00-introduction.md | 268 ++++++------ .../src/ch01-introduction-and-motivation.md | 380 +++++++++--------- python-book/src/ch02-getting-started.md | 292 +++++++------- .../src/ch03-built-in-types-and-variables.md | 377 +++++++++-------- python-book/src/ch04-control-flow.md | 200 +++++---- .../ch05-data-structures-and-collections.md | 307 +++++++------- .../src/ch06-enums-and-pattern-matching.md | 194 +++++---- .../src/ch07-ownership-and-borrowing.md | 284 +++++++------ python-book/src/ch08-crates-and-modules.md | 232 ++++++----- python-book/src/ch09-error-handling.md | 245 ++++++----- 11 files changed, 1382 insertions(+), 1443 deletions(-) diff --git a/python-book/src/SUMMARY.md b/python-book/src/SUMMARY.md index b0a3a0f..e2e7003 100644 --- a/python-book/src/SUMMARY.md +++ b/python-book/src/SUMMARY.md @@ -1,40 +1,40 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I — Foundations +# Часть I — Основы -- [1. Introduction and Motivation](ch01-introduction-and-motivation.md) -- [2. Getting Started](ch02-getting-started.md) -- [3. Built-in Types and Variables](ch03-built-in-types-and-variables.md) -- [4. Control Flow](ch04-control-flow.md) -- [5. Data Structures and Collections](ch05-data-structures-and-collections.md) -- [6. Enums and Pattern Matching](ch06-enums-and-pattern-matching.md) +- [1. Введение и мотивация](ch01-introduction-and-motivation.md) +- [2. Начало работы](ch02-getting-started.md) +- [3. Встроенные типы и переменные](ch03-built-in-types-and-variables.md) +- [4. Управление потоком выполнения](ch04-control-flow.md) +- [5. Структуры данных и коллекции](ch05-data-structures-and-collections.md) +- [6. Перечисления и сопоставление с образцом](ch06-enums-and-pattern-matching.md) --- -# Part II — Core Concepts +# Часть II — Ключевые концепции -- [7. Ownership and Borrowing](ch07-ownership-and-borrowing.md) -- [8. Crates and Modules](ch08-crates-and-modules.md) -- [9. Error Handling](ch09-error-handling.md) -- [10. Traits and Generics](ch10-traits-and-generics.md) -- [11. From and Into Traits](ch11-from-and-into-traits.md) -- [12. Closures and Iterators](ch12-closures-and-iterators.md) +- [7. Владение и заимствование](ch07-ownership-and-borrowing.md) +- [8. Крейты и модули](ch08-crates-and-modules.md) +- [9. Обработка ошибок](ch09-error-handling.md) +- [10. Трейты и обобщения](ch10-traits-and-generics.md) +- [11. Трейты From и Into](ch11-from-and-into-traits.md) +- [12. Замыкания и итераторы](ch12-closures-and-iterators.md) --- -# Part III — Advanced Topics & Migration +# Часть III — Продвинутые темы и миграция -- [13. Concurrency](ch13-concurrency.md) -- [14. Unsafe Rust and FFI](ch14-unsafe-rust-and-ffi.md) -- [15. Migration Patterns](ch15-migration-patterns.md) -- [16. Best Practices](ch16-best-practices.md) +- [13. Конкурентность](ch13-concurrency.md) +- [14. Unsafe Rust и FFI](ch14-unsafe-rust-and-ffi.md) +- [15. Паттерны миграции](ch15-migration-patterns.md) +- [16. Лучшие практики](ch16-best-practices.md) --- -# Part IV — Capstone +# Часть IV — Итоговый проект -- [17. Capstone Project: CLI Task Manager](ch17-capstone-project.md) +- [17. Итоговый проект: CLI-менеджер задач](ch17-capstone-project.md) diff --git a/python-book/src/ch00-introduction.md b/python-book/src/ch00-introduction.md index 20275ee..496987b 100644 --- a/python-book/src/ch00-introduction.md +++ b/python-book/src/ch00-introduction.md @@ -1,146 +1,142 @@ -# Rust for Python Programmers: Complete Training Guide - -A comprehensive guide to learning Rust for developers with Python experience. This guide -covers everything from basic syntax to advanced patterns, focusing on the conceptual shifts -required when moving from a dynamically-typed, garbage-collected language to a statically-typed -systems language with compile-time memory safety. - -## How to Use This Book - -**Self-study format**: Work through Part I (ch 1–6) first — these map closely to Python concepts you already know. Part II (ch 7–12) introduces Rust-specific ideas like ownership and traits. Part III (ch 13–16) covers advanced topics and migration. - -**Pacing recommendations:** - -| Chapters | Topic | Suggested Time | Checkpoint | -|----------|-------|---------------|------------| -| 1–4 | Setup, types, control flow | 1 day | You can write a CLI temperature converter in Rust | -| 5–6 | Data structures, enums, pattern matching | 1–2 days | You can define an enum with data and `match` exhaustively on it | -| 7 | Ownership and borrowing | 1–2 days | You can explain *why* `let s2 = s1` invalidates `s1` | -| 8–9 | Modules, error handling | 1 day | You can create a multi-file project that propagates errors with `?` | -| 10–12 | Traits, generics, closures, iterators | 1–2 days | You can translate a list comprehension to an iterator chain | -| 13 | Concurrency | 1 day | You can write a thread-safe counter with `Arc>` | -| 14 | Unsafe, PyO3, testing | 1 day | You can call a Rust function from Python via PyO3 | -| 15–16 | Migration, best practices | At your own pace | Reference material — consult as you write real code | -| 17 | Capstone project | 2–3 days | Build a complete CLI app tying everything together | - -**How to use the exercises:** -- Chapters include hands-on exercises in collapsible `
` blocks with solutions -- **Always try the exercise before expanding the solution.** Struggling with the borrow checker is part of learning — the compiler's error messages are your teacher -- If you're stuck for more than 15 minutes, expand the solution, study it, then close it and try again from scratch -- The [Rust Playground](https://play.rust-lang.org/) lets you run code without a local install - -**Difficulty indicators:** -- 🟢 **Beginner** — Direct translation from Python concepts -- 🟡 **Intermediate** — Requires understanding ownership or traits -- 🔴 **Advanced** — Lifetimes, async internals, or unsafe code - -**When you hit a wall:** -- Read the compiler error message carefully — Rust's errors are exceptionally helpful -- Re-read the relevant section; concepts like ownership (ch7) often click on the second pass -- The [Rust standard library docs](https://doc.rust-lang.org/std/) are excellent — search for any type or method -- For deeper async patterns, see the companion [Async Rust Training](../async-book/) +# Rust для программистов на Python: полное учебное руководство + +Подробное руководство по изучению Rust для разработчиков, которые уже знакомы с Python. Книга охватывает всё: от базового синтаксиса до продвинутых паттернов. Основной акцент сделан на концептуальных сдвигах, которые происходят при переходе от языка с динамической типизацией и сборщиком мусора к системному языку со статической типизацией и проверкой безопасности памяти на этапе компиляции. + +## Как пользоваться этой книгой + +**Формат самостоятельного изучения**: сначала пройдите часть I (гл. 1–6), она тесно связана с понятиями Python, которые вы уже знаете. Часть II (гл. 7–12) знакомит с идеями, специфичными для Rust, такими как владение и трейты. Часть III (гл. 13–16) посвящена продвинутым темам и миграции. + +**Рекомендации по темпу:** + +| Главы | Тема | Примерное время | Контрольная точка | +|-------|------|-----------------|-------------------| +| 1–4 | Настройка, типы, управление потоком выполнения | 1 день | Вы можете написать CLI-конвертер температур на Rust | +| 5–6 | Структуры данных, перечисления, сопоставление с образцом | 1–2 дня | Вы можете определить перечисление с данными и исчерпывающе разобрать его через `match` | +| 7 | Владение и заимствование | 1–2 дня | Вы можете объяснить, *почему* `let s2 = s1` делает `s1` недействительным | +| 8–9 | Модули, обработка ошибок | 1 день | Вы можете создать многофайловый проект, который передаёт ошибки дальше с помощью `?` | +| 10–12 | Трейты, обобщения, замыкания, итераторы | 1–2 дня | Вы можете перевести list comprehension в цепочку итераторов | +| 13 | Конкурентность | 1 день | Вы можете написать потокобезопасный счётчик с `Arc>` | +| 14 | Unsafe, PyO3, тестирование | 1 день | Вы можете вызвать функцию Rust из Python через PyO3 | +| 15–16 | Миграция, лучшие практики | В своём темпе | Справочный материал: обращайтесь к нему при написании реального кода | +| 17 | Итоговый проект | 2–3 дня | Создайте полноценное CLI-приложение, которое объединяет всё изученное | + +**Как работать с упражнениями:** +- В главах есть практические упражнения в сворачиваемых блоках `
` с решениями +- **Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение.** Борьба с borrow checker — часть обучения, а сообщения компилятора — ваш учитель +- Если вы застряли больше чем на 15 минут, раскройте решение, изучите его, затем закройте и попробуйте снова с нуля +- [Rust Playground](https://play.rust-lang.org/) позволяет запускать код без локальной установки + +**Индикаторы сложности:** +- 🟢 **Начальный**: прямой перевод понятий из Python +- 🟡 **Средний**: требует понимания владения или трейтов +- 🔴 **Продвинутый**: времена жизни, внутреннее устройство async или unsafe-код + +**Если вы упёрлись в стену:** +- Внимательно читайте сообщение компилятора: ошибки Rust исключительно полезны +- Перечитайте нужный раздел. Концепции вроде владения (гл. 7) часто становятся понятны при втором прочтении +- [Документация стандартной библиотеки Rust](https://doc.rust-lang.org/std/) отличная: ищите любой тип или метод +- Более глубокие async-паттерны описаны в сопутствующем курсе [Async Rust](../async-book/) --- -## Table of Contents - -### Part I — Foundations - -#### 1. Introduction and Motivation 🟢 -- [The Case for Rust for Python Developers](ch01-introduction-and-motivation.md#the-case-for-rust-for-python-developers) -- [Common Python Pain Points That Rust Addresses](ch01-introduction-and-motivation.md#common-python-pain-points-that-rust-addresses) -- [When to Choose Rust Over Python](ch01-introduction-and-motivation.md#when-to-choose-rust-over-python) - -#### 2. Getting Started 🟢 -- [Installation and Setup](ch02-getting-started.md#installation-and-setup) -- [Your First Rust Program](ch02-getting-started.md#your-first-rust-program) -- [Cargo vs pip/Poetry](ch02-getting-started.md#cargo-vs-pippoetry) - -#### 3. Built-in Types and Variables 🟢 -- [Variables and Mutability](ch03-built-in-types-and-variables.md#variables-and-mutability) -- [Primitive Types Comparison](ch03-built-in-types-and-variables.md#primitive-types-comparison) -- [String Types: String vs &str](ch03-built-in-types-and-variables.md#string-types-string-vs-str) - -#### 4. Control Flow 🟢 -- [Conditional Statements](ch04-control-flow.md#conditional-statements) -- [Loops and Iteration](ch04-control-flow.md#loops-and-iteration) -- [Expression Blocks](ch04-control-flow.md#expression-blocks) -- [Functions and Type Signatures](ch04-control-flow.md#functions-and-type-signatures) - -#### 5. Data Structures and Collections 🟢 -- [Tuples, Arrays, Slices](ch05-data-structures-and-collections.md#tuples-and-destructuring) -- [Structs vs Classes](ch05-data-structures-and-collections.md#structs-vs-classes) -- [Vec vs list, HashMap vs dict](ch05-data-structures-and-collections.md#vec-vs-list) - -#### 6. Enums and Pattern Matching 🟡 -- [Algebraic Data Types vs Union Types](ch06-enums-and-pattern-matching.md#algebraic-data-types-vs-union-types) -- [Exhaustive Pattern Matching](ch06-enums-and-pattern-matching.md#exhaustive-pattern-matching) -- [Option for None Safety](ch06-enums-and-pattern-matching.md#option-for-none-safety) - -### Part II — Core Concepts - -#### 7. Ownership and Borrowing 🟡 -- [Understanding Ownership](ch07-ownership-and-borrowing.md#understanding-ownership) -- [Move Semantics vs Reference Counting](ch07-ownership-and-borrowing.md#move-semantics-vs-reference-counting) -- [Borrowing and Lifetimes](ch07-ownership-and-borrowing.md#borrowing-and-lifetimes) -- [Smart Pointers](ch07-ownership-and-borrowing.md#smart-pointers) - -#### 8. Crates and Modules 🟢 -- [Rust Modules vs Python Packages](ch08-crates-and-modules.md#rust-modules-vs-python-packages) -- [Crates vs PyPI Packages](ch08-crates-and-modules.md#crates-vs-pypi-packages) - -#### 9. Error Handling 🟡 -- [Exceptions vs Result](ch09-error-handling.md#exceptions-vs-result) -- [The ? Operator](ch09-error-handling.md#the--operator) -- [Custom Error Types with thiserror](ch09-error-handling.md#custom-error-types-with-thiserror) - -#### 10. Traits and Generics 🟡 -- [Traits vs Duck Typing](ch10-traits-and-generics.md#traits-vs-duck-typing) -- [Protocols (PEP 544) vs Traits](ch10-traits-and-generics.md#protocols-pep-544-vs-traits) -- [Generic Constraints](ch10-traits-and-generics.md#generic-constraints) - -#### 11. From and Into Traits 🟡 -- [Type Conversions in Rust](ch11-from-and-into-traits.md#type-conversions-in-rust) -- [From, Into, TryFrom](ch11-from-and-into-traits.md#rust-frominto) -- [String Conversion Patterns](ch11-from-and-into-traits.md#string-conversions) - -#### 12. Closures and Iterators 🟡 -- [Closures vs Lambdas](ch12-closures-and-iterators.md#rust-closures-vs-python-lambdas) -- [Iterators vs Generators](ch12-closures-and-iterators.md#iterators-vs-generators) -- [Macros: Code That Writes Code](ch12-closures-and-iterators.md#why-macros-exist-in-rust) - -### Part III — Advanced Topics & Migration - -#### 13. Concurrency 🔴 -- [No GIL: True Parallelism](ch13-concurrency.md#no-gil-true-parallelism) -- [Thread Safety: Type System Guarantees](ch13-concurrency.md#thread-safety-type-system-guarantees) -- [async/await Comparison](ch13-concurrency.md#asyncawait-comparison) - -#### 14. Unsafe Rust, FFI, and Testing 🔴 -- [When and Why to Use Unsafe](ch14-unsafe-rust-and-ffi.md#when-and-why-to-use-unsafe) -- [PyO3: Rust Extensions for Python](ch14-unsafe-rust-and-ffi.md#pyo3-rust-extensions-for-python) -- [Unit Tests vs pytest](ch14-unsafe-rust-and-ffi.md#unit-tests-vs-pytest) - -#### 15. Migration Patterns 🟡 -- [Common Python Patterns in Rust](ch15-migration-patterns.md#common-python-patterns-in-rust) -- [Essential Crates for Python Developers](ch08-crates-and-modules.md#essential-crates-for-python-developers) -- [Incremental Adoption Strategy](ch15-migration-patterns.md#incremental-adoption-strategy) - -#### 16. Best Practices 🟡 -- [Idiomatic Rust for Python Developers](ch16-best-practices.md#idiomatic-rust-for-python-developers) -- [Common Pitfalls and Solutions](ch16-best-practices.md#common-pitfalls-and-solutions) -- [Python→Rust Rosetta Stone](ch16-best-practices.md#rosetta-stone-python-to-rust) -- [Learning Path and Resources](ch16-best-practices.md#learning-path-and-resources) +## Содержание + +### Часть I — Основы + +#### 1. Введение и мотивация 🟢 +- [Аргументы в пользу Rust для разработчиков на Python](ch01-introduction-and-motivation.md#аргументы-в-пользу-rust-для-разработчиков-на-python) +- [Частые проблемы Python, которые решает Rust](ch01-introduction-and-motivation.md#частые-проблемы-python-которые-решает-rust) +- [Когда выбирать Rust вместо Python](ch01-introduction-and-motivation.md#когда-выбирать-rust-вместо-python) + +#### 2. Начало работы 🟢 +- [Установка и настройка](ch02-getting-started.md#установка-и-настройка) +- [Ваша первая программа на Rust](ch02-getting-started.md#ваша-первая-программа-на-rust) +- [Cargo и pip/Poetry](ch02-getting-started.md#cargo-и-pippoetry) + +#### 3. Встроенные типы и переменные 🟢 +- [Переменные и изменяемость](ch03-built-in-types-and-variables.md#переменные-и-изменяемость) +- [Сравнение примитивных типов](ch03-built-in-types-and-variables.md#сравнение-примитивных-типов) +- [Строковые типы: String и &str](ch03-built-in-types-and-variables.md#строковые-типы-string-и-str) + +#### 4. Управление потоком выполнения 🟢 +- [Условные конструкции](ch04-control-flow.md#условные-конструкции) +- [Циклы и итерация](ch04-control-flow.md#циклы-и-итерация) +- [Блоки-выражения](ch04-control-flow.md#блоки-выражения) +- [Функции и сигнатуры типов](ch04-control-flow.md#функции-и-сигнатуры-типов) + +#### 5. Структуры данных и коллекции 🟢 +- [Кортежи, массивы, срезы](ch05-data-structures-and-collections.md#кортежи-и-деструктуризация) +- [Структуры и классы](ch05-data-structures-and-collections.md#структуры-и-классы) +- [Vec и list, HashMap и dict](ch05-data-structures-and-collections.md#vec-и-list) + +#### 6. Перечисления и сопоставление с образцом 🟡 +- [Алгебраические типы данных и объединения типов](ch06-enums-and-pattern-matching.md#алгебраические-типы-данных-и-объединения-типов) +- [Исчерпывающее сопоставление с образцом](ch06-enums-and-pattern-matching.md#исчерпывающее-сопоставление-с-образцом) +- [Option для безопасной работы с None](ch06-enums-and-pattern-matching.md#option-для-безопасной-работы-с-none) + +### Часть II — Ключевые концепции + +#### 7. Владение и заимствование 🟡 +- [Понимание владения](ch07-ownership-and-borrowing.md#понимание-владения) +- [Семантика перемещения и подсчёт ссылок](ch07-ownership-and-borrowing.md#семантика-перемещения-и-подсчёт-ссылок) +- [Заимствование и времена жизни](ch07-ownership-and-borrowing.md#заимствование-и-времена-жизни) +- [Умные указатели](ch07-ownership-and-borrowing.md#умные-указатели) + +#### 8. Крейты и модули 🟢 +- [Модули Rust и пакеты Python](ch08-crates-and-modules.md#модули-rust-и-пакеты-python) +- [Крейты и пакеты PyPI](ch08-crates-and-modules.md#крейты-и-пакеты-pypi) + +#### 9. Обработка ошибок 🟡 +- [Исключения и Result](ch09-error-handling.md#исключения-и-result) +- [Оператор ?](ch09-error-handling.md#оператор-) +- [Собственные типы ошибок с thiserror](ch09-error-handling.md#собственные-типы-ошибок-с-thiserror) + +#### 10. Трейты и обобщения 🟡 +- [Трейты и утиная типизация](ch10-traits-and-generics.md#трейты-и-утиная-типизация) +- [Протоколы (PEP 544) и трейты](ch10-traits-and-generics.md#протоколы-pep-544-и-трейты) +- [Ограничения обобщений](ch10-traits-and-generics.md#ограничения-обобщений) + +#### 11. Трейты From и Into 🟡 +- [Преобразование типов в Rust](ch11-from-and-into-traits.md#преобразование-типов-в-rust) +- [From, Into, TryFrom](ch11-from-and-into-traits.md#трейты-from-into) +- [Паттерны преобразования строк](ch11-from-and-into-traits.md#преобразования-строк) + +#### 12. Замыкания и итераторы 🟡 +- [Замыкания и лямбды](ch12-closures-and-iterators.md#замыкания-rust-и-лямбды-python) +- [Итераторы и генераторы](ch12-closures-and-iterators.md#итераторы-и-генераторы) +- [Макросы: код, который пишет код](ch12-closures-and-iterators.md#зачем-нужны-макросы-в-rust) + +### Часть III — Продвинутые темы и миграция + +#### 13. Конкурентность 🔴 +- [Нет GIL: настоящий параллелизм](ch13-concurrency.md#нет-gil-настоящий-параллелизм) +- [Безопасность потоков: гарантии системы типов](ch13-concurrency.md#безопасность-потоков-гарантии-системы-типов) +- [Сравнение async/await](ch13-concurrency.md#сравнение-asyncawait) + +#### 14. Unsafe Rust, FFI и тестирование 🔴 +- [Когда и зачем использовать unsafe](ch14-unsafe-rust-and-ffi.md#когда-и-зачем-использовать-unsafe) +- [PyO3: расширения Rust для Python](ch14-unsafe-rust-and-ffi.md#pyo3-расширения-rust-для-python) +- [Модульные тесты и pytest](ch14-unsafe-rust-and-ffi.md#модульные-тесты-и-pytest) + +#### 15. Паттерны миграции 🟡 +- [Частые паттерны Python в Rust](ch15-migration-patterns.md#частые-паттерны-python-в-rust) +- [Основные крейты для разработчиков на Python](ch08-crates-and-modules.md#основные-крейты-для-разработчиков-на-python) +- [Стратегия постепенного внедрения](ch15-migration-patterns.md#стратегия-постепенного-внедрения) + +#### 16. Лучшие практики 🟡 +- [Идиоматичный Rust для разработчиков на Python](ch16-best-practices.md#идиоматичный-rust-для-разработчиков-на-python) +- [Частые ошибки и их решения](ch16-best-practices.md#частые-ошибки-и-их-решения) +- [Шпаргалка «Python → Rust»](ch16-best-practices.md#шпаргалка-python-rust) +- [Путь обучения и ресурсы](ch16-best-practices.md#путь-обучения-и-ресурсы) --- -### Part IV — Capstone +### Часть IV — Итоговый проект -#### 17. Capstone Project: CLI Task Manager 🔴 -- [The Project: `rustdo`](ch17-capstone-project.md#the-project-rustdo) -- [Data Model, Storage, Commands, Business Logic](ch17-capstone-project.md#step-1-define-the-data-model-ch-3-6-10-11) -- [Tests and Stretch Goals](ch17-capstone-project.md#step-7-tests-ch-14) +#### 17. Итоговый проект: CLI-менеджер задач 🔴 +- [Проект: `rustdo`](ch17-capstone-project.md#проект-rustdo) +- [Модель данных, хранение, команды, бизнес-логика](ch17-capstone-project.md#шаг-1-определите-модель-данных-гл-3-6-10-11) +- [Тесты и дополнительные задания](ch17-capstone-project.md#шаг-7-тесты-гл-14) *** - diff --git a/python-book/src/ch01-introduction-and-motivation.md b/python-book/src/ch01-introduction-and-motivation.md index b9ab24c..53f1b1f 100644 --- a/python-book/src/ch01-introduction-and-motivation.md +++ b/python-book/src/ch01-introduction-and-motivation.md @@ -1,30 +1,28 @@ -## Speaker Intro and General Approach +## Об авторе и общий подход -- Speaker intro - - Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team - - Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems - - Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since -- This course is intended to be as interactive as possible - - Assumption: You know Python and its ecosystem - - Examples deliberately map Python concepts to Rust equivalents - - **Please feel free to ask clarifying questions at any point of time** +- Об авторе + - Главный архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft + - Опытный специалист в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также систем на C++ + - Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- Курс задуман максимально интерактивным + - Предполагается, что вы знаете Python и его экосистему + - Примеры намеренно сопоставляют понятия Python с их аналогами в Rust + - **Не стесняйтесь задавать уточняющие вопросы в любой момент** --- -## The Case for Rust for Python Developers +## Аргументы в пользу Rust для разработчиков на Python -> **What you'll learn:** Why Python developers are adopting Rust, real-world performance wins (Dropbox, Discord, Pydantic), -> when Rust is the right choice vs staying with Python, and the core philosophical differences between the two languages. +> **Что вы узнаете:** почему разработчики на Python переходят на Rust, какие реальные выигрыши в производительности получили компании (Dropbox, Discord, Pydantic), когда Rust — правильный выбор по сравнению с Python и в чём принципиальные философские различия между двумя языками. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Performance: From Minutes to Milliseconds +### Производительность: от минут до миллисекунд -Python is famously slow for CPU-bound work. Rust provides C-level performance -with a high-level feel. +Python известен медленной работой с задачами, ограниченными процессором. Rust даёт производительность уровня C при высокоуровневом стиле написания кода. ```python -# Python — ~2 seconds for 10 million calls +# Python — ~2 секунды на 10 миллионов вызовов import time def fibonacci(n: int) -> int: @@ -38,11 +36,11 @@ def fibonacci(n: int) -> int: start = time.perf_counter() results = [fibonacci(n % 30) for n in range(10_000_000)] elapsed = time.perf_counter() - start -print(f"Elapsed: {elapsed:.2f}s") # ~2s on typical hardware +print(f"Прошло: {elapsed:.2f}s") # ~2 с на типичном железе ``` ```rust -// Rust — ~0.07 seconds for the same 10 million calls +// Rust — ~0.07 секунды на те же 10 миллионов вызовов use std::time::Instant; fn fibonacci(n: u64) -> u64 { @@ -61,22 +59,18 @@ fn fibonacci(n: u64) -> u64 { fn main() { let start = Instant::now(); let results: Vec = (0..10_000_000).map(|n| fibonacci(n % 30)).collect(); - println!("Elapsed: {:.2?}", start.elapsed()); // ~0.07s + println!("Прошло: {:.2?}", start.elapsed()); // ~0.07 с } ``` -> Note: Rust should be run in release mode (`cargo run --release`) for a fair performance comparison. -> **Why the difference?** Python dispatches every `+` through a dictionary lookup, -> unboxes integers from heap objects, and checks types at every operation. Rust compiles -> `fibonacci` directly to a handful of x86 `add`/`mov` instructions — the same code a -> C compiler would produce. +> Примечание: для честного сравнения производительности Rust нужно запускать в release-режиме (`cargo run --release`). +> **В чём разница?** Python при каждом `+` обращается к поиску в словаре, извлекает целые числа из объектов в куче и проверяет типы на каждой операции. Rust компилирует `fibonacci` напрямую в несколько инструкций x86 `add`/`mov` — ровно тот код, который выдал бы компилятор C. -### Memory Safety Without a Garbage Collector +### Безопасность памяти без сборщика мусора -Python's reference-counting GC has known issues: circular references, unpredictable -`__del__` timing, and memory fragmentation. Rust eliminates these at compile time. +У сборщика мусора Python на основе подсчёта ссылок есть известные проблемы: циклические ссылки, непредсказуемое время вызова `__del__` и фрагментация памяти. Rust устраняет их на этапе компиляции. ```python -# Python — circular reference that CPython's ref counter can't free +# Python — циклическая ссылка, которую счётчик ссылок CPython не может освободить class Node: def __init__(self, value): self.value = value @@ -85,21 +79,21 @@ class Node: def add_child(self, child): self.children.append(child) - child.parent = self # Circular reference! + child.parent = self # Циклическая ссылка! -# These two nodes reference each other — ref count never reaches 0. -# CPython's cycle detector will *eventually* clean them up, -# but you can't control when, and it adds GC pause overhead. +# Эти два узла ссылаются друг на друга — счётчик ссылок никогда не станет 0. +# Детектор циклов CPython *в конце концов* их очистит, +# но вы не контролируете, когда это произойдёт, и это добавляет накладные расходы на паузы GC. root = Node("root") child = Node("child") root.add_child(child) ``` ```rust -// Rust — ownership prevents circular references by design +// Rust — владение по замыслу исключает циклические ссылки struct Node { value: String, - children: Vec, // Children are OWNED — no cycles possible + children: Vec, // Дети ПРИНАДЛЕЖАТ родителю — циклы невозможны } impl Node { @@ -111,7 +105,7 @@ impl Node { } fn add_child(&mut self, child: Node) { - self.children.push(child); // Ownership transfers here + self.children.push(child); // Владение переходит здесь } } @@ -119,40 +113,37 @@ fn main() { let mut root = Node::new("root"); let child = Node::new("child"); root.add_child(child); - // When root is dropped, all children are dropped too. - // Deterministic, zero overhead, no GC. + // Когда root уничтожается, уничтожаются и все дети. + // Детерминированно, без накладных расходов, без GC. } ``` -> **Key insight**: In Rust, the child doesn't hold a reference back to the parent. -> If you truly need cross-references (like a graph), you use explicit mechanisms -> like `Rc>` or indices — making the complexity visible and intentional. +> **Ключевая мысль**: в Rust потомок не хранит ссылку обратно на родителя. Если действительно нужны перекрёстные ссылки (например, для графа), используются явные механизмы вроде `Rc>` или индексов, что делает сложность видимой и осознанной. *** -## Common Python Pain Points That Rust Addresses +## Частые проблемы Python, которые решает Rust -### 1. Runtime Type Errors +### 1. Ошибки типов во время выполнения -The most common Python production bug: passing the wrong type to a function. -Type hints help, but they aren't enforced. +Самая распространённая продакшен-ошибка в Python: передача функции значения неправильного типа. Подсказки типов помогают, но они не проверяются. ```python -# Python — type hints are suggestions, not rules +# Python — подсказки типов являются рекомендациями, а не правилами def process_user(user_id: int, name: str) -> dict: return {"id": user_id, "name": name.upper()} -# These all "work" at the call site — fail at runtime +# Все эти вызовы «проходят» на месте вызова и падают во время выполнения process_user("not-a-number", 42) # TypeError: int has no .upper() -process_user(None, "Alice") # Silently stores None as id — bug hides until downstream code expects int +process_user(None, "Alice") # Молча сохраняет None как id — ошибка спрячется, пока последующий код не ожидает int -# Even with mypy, you can still bypass types: -data = json.loads('{"id": "oops"}') # Always returns Any -process_user(data["id"], data["name"]) # mypy can't catch this +# Даже с mypy типы можно обойти: +data = json.loads('{"id": "oops"}') # Всегда возвращает Any +process_user(data["id"], data["name"]) # mypy это не поймает ``` ```rust -// Rust — the compiler catches all of these before the program runs +// Rust — компилятор ловит всё это до запуска программы fn process_user(user_id: i64, name: &str) -> User { User { id: user_id, @@ -160,45 +151,44 @@ fn process_user(user_id: i64, name: &str) -> User { } } -// process_user("not-a-number", 42); // ❌ Compile error: expected i64, found &str -// process_user(None, "Alice"); // ❌ Compile error: expected i64, found Option -// Extra arguments are always a compile error. +// process_user("not-a-number", 42); // ❌ Ошибка компиляции: ожидался i64, найден &str +// process_user(None, "Alice"); // ❌ Ошибка компиляции: ожидался i64, найден Option +// Лишние аргументы — всегда ошибка компиляции. -// Deserializing JSON is type-safe too: +// Десериализация JSON тоже типобезопасна: #[derive(Deserialize)] struct UserInput { - id: i64, // Must be a number in the JSON - name: String, // Must be a string in the JSON + id: i64, // Должно быть числом в JSON + name: String, // Должно быть строкой в JSON } -let input: UserInput = serde_json::from_str(json_str)?; // Returns Err if types mismatch -process_user(input.id, &input.name); // ✅ Guaranteed correct types +let input: UserInput = serde_json::from_str(json_str)?; // Возвращает Err, если типы не совпадают +process_user(input.id, &input.name); // ✅ Типы гарантированно корректны ``` -### 2. None: The Billion Dollar Mistake (Python Edition) +### 2. None: миллиардная ошибка (издание для Python) -`None` can appear anywhere a value is expected. Python has no compile-time way -to prevent `AttributeError: 'NoneType' object has no attribute ...`. +`None` может появиться там, где ожидается значение. В Python нет способа на этапе компиляции предотвратить `AttributeError: 'NoneType' object has no attribute ...`. ```python -# Python — None sneaks in everywhere +# Python — None проникает повсюду def find_user(user_id: int) -> dict | None: users = {1: {"name": "Alice"}, 2: {"name": "Bob"}} return users.get(user_id) -user = find_user(999) # Returns None +user = find_user(999) # Возвращает None print(user["name"]) # 💥 TypeError: 'NoneType' object is not subscriptable -# Even with Optional type hint, nothing enforces the check: +# Даже с подсказкой типа Optional ничто не требует проверки: from typing import Optional def get_name(user_id: int) -> Optional[str]: return None name: Optional[str] = get_name(1) -print(name.upper()) # 💥 AttributeError — mypy warns, runtime doesn't care +print(name.upper()) # 💥 AttributeError — mypy предупреждает, а во время выполнения это никого не волнует ``` ```rust -// Rust — None is impossible unless explicitly handled +// Rust — None невозможен, пока вы его явно не обработаете fn find_user(user_id: i64) -> Option { let users = HashMap::from([ (1, User { name: "Alice".into() }), @@ -207,29 +197,27 @@ fn find_user(user_id: i64) -> Option { users.get(&user_id).cloned() } -let user = find_user(999); // Returns None variant of Option -// println!("{}", user.name); // ❌ Compile error: Option has no field `name` +let user = find_user(999); // Возвращает вариант None типа Option +// println!("{}", user.name); // ❌ Ошибка компиляции: у Option нет поля `name` -// You MUST handle the None case: +// Вариант None ОБЯЗАТЕЛЬНО нужно обработать: match find_user(999) { Some(user) => println!("{}", user.name), - None => println!("User not found"), + None => println!("Пользователь не найден"), } -// Or use combinators: +// Или используйте комбинаторы: let name = find_user(999) .map(|u| u.name) .unwrap_or_else(|| "Unknown".to_string()); ``` -### 3. The GIL: Python's Concurrency Ceiling +### 3. GIL: потолок конкурентности Python -Python's Global Interpreter Lock means threads don't run Python code in parallel. -`threading` is only useful for I/O-bound work; CPU-bound work requires `multiprocessing` -(with its serialization overhead) or C extensions. +Глобальная блокировка интерпретатора (GIL) в Python означает, что потоки не выполняют код Python параллельно. `threading` полезен только для задач, ограниченных вводом-выводом; задачи, ограниченные процессором, требуют `multiprocessing` (с накладными расходами на сериализацию) или C-расширений. ```python -# Python — threads DON'T speed up CPU work because of the GIL +# Python — потоки НЕ ускоряют работу с процессором из-за GIL import threading import time @@ -246,16 +234,16 @@ for t in threads: for t in threads: t.join() elapsed = time.perf_counter() - start -print(f"4 threads: {elapsed:.2f}s") # About the SAME as 1 thread! GIL prevents parallelism. +print(f"4 потока: {elapsed:.2f}s") # Примерно ТАКОЕ ЖЕ время, как у 1 потока! GIL не даёт параллелизму работать. -# multiprocessing "works" but serializes data between processes: +# multiprocessing «работает», но сериализует данные между процессами: from multiprocessing import Pool with Pool(4) as p: - results = p.map(cpu_work, [10_000_000] * 4) # ~4x faster, but pickle overhead + results = p.map(cpu_work, [10_000_000] * 4) # ~в 4 раза быстрее, но с накладными расходами на pickle ``` ```rust -// Rust — true parallelism, no GIL, no serialization overhead +// Rust — настоящий параллелизм, без GIL и без накладных расходов на сериализацию use std::thread; fn cpu_work(n: u64) -> u64 { @@ -272,31 +260,30 @@ fn main() { .map(|h| h.join().unwrap()) .collect(); - println!("4 threads: {:.2?}", start.elapsed()); // ~4x faster than single thread + println!("4 потока: {:.2?}", start.elapsed()); // ~в 4 раза быстрее одного потока } ``` -> **With Rayon** (Rust's parallel iterator library), parallelism is even simpler: +> **С Rayon** (библиотекой параллельных итераторов для Rust) параллелизм становится ещё проще: > ```rust > use rayon::prelude::*; > let results: Vec = inputs.par_iter().map(|&n| cpu_work(n)).collect(); > ``` -### 4. Deployment and Distribution Pain +### 4. Боль развёртывания и распространения -Python deployment is notoriously difficult: venvs, system Python conflicts, -`pip install` failures, C extension wheels, Docker images with full Python runtime. +Развёртывание Python известно своей сложностью: venv, конфликты с системным Python, сбои `pip install`, колёса (wheels) C-расширений, Docker-образы с полным рантаймом Python. ```python -# Python deployment checklist: -# 1. Which Python version? 3.9? 3.10? 3.11? 3.12? -# 2. Virtual environment: venv, conda, poetry, pipenv? -# 3. C extensions: need compiler? manylinux wheels? -# 4. System dependencies: libssl, libffi, etc.? -# 5. Docker: full python:3.12 image is 1.0 GB -# 6. Startup time: 200-500ms for import-heavy apps - -# Docker image: ~1 GB +# Чек-лист развёртывания Python: +# 1. Какая версия Python? 3.9? 3.10? 3.11? 3.12? +# 2. Виртуальное окружение: venv, conda, poetry, pipenv? +# 3. C-расширения: нужен компилятор? manylinux-колёса? +# 4. Системные зависимости: libssl, libffi и т. д.? +# 5. Docker: полный образ python:3.12 весит 1,0 ГБ +# 6. Время запуска: 200–500 мс для приложений с большим количеством импортов + +# Docker-образ: ~1 ГБ # FROM python:3.12-slim # COPY requirements.txt . # RUN pip install -r requirements.txt @@ -305,112 +292,112 @@ Python deployment is notoriously difficult: venvs, system Python conflicts, ``` ```rust -// Rust deployment: single static binary, no runtime needed -// cargo build --release → one binary, ~5-20 MB -// Copy it anywhere — no Python, no venv, no dependencies +// Развёртывание Rust: один статический бинарник, рантайм не нужен +// cargo build --release → один бинарник, ~5–20 МБ +// Копируйте куда угодно — ни Python, ни venv, ни зависимостей -// Docker image: ~5 MB (from scratch or distroless) +// Docker-образ: ~5 МБ (from scratch или distroless) // FROM scratch // COPY target/release/my_app /my_app // CMD ["/my_app"] -// Startup time: <1ms -// Cross-compile: cargo build --target x86_64-unknown-linux-musl +// Время запуска: <1 мс +// Кросс-компиляция: cargo build --target x86_64-unknown-linux-musl ``` *** -## When to Choose Rust Over Python - -### Choose Rust When: -- **Performance is critical**: Data pipelines, real-time processing, compute-heavy services -- **Correctness matters**: Financial systems, safety-critical code, protocol implementations -- **Deployment simplicity**: Single binary, no runtime dependencies -- **Low-level control**: Hardware interaction, OS integration, embedded systems -- **True concurrency**: CPU-bound parallelism without GIL workarounds -- **Memory efficiency**: Reduce cloud costs for memory-intensive services -- **Long-running services**: Where predictable latency matters (no GC pauses) - -### Stay with Python When: -- **Rapid prototyping**: Exploratory data analysis, scripts, one-off tools -- **ML/AI workflows**: PyTorch, TensorFlow, scikit-learn ecosystem -- **Glue code**: Connecting APIs, data transformation scripts -- **Team expertise**: When Rust learning curve doesn't justify benefits -- **Time to market**: When development speed trumps execution speed -- **Interactive work**: Jupyter notebooks, REPL-driven development -- **Scripting**: Automation, sys-admin tasks, quick utilities - -### Consider Both (Hybrid Approach with PyO3): -- **Compute-heavy code in Rust**: Called from Python via PyO3/maturin -- **Business logic and orchestration in Python**: Familiar, productive -- **Gradual migration**: Identify hotspots, replace with Rust extensions -- **Best of both**: Python's ecosystem + Rust's performance +## Когда выбирать Rust вместо Python + +### Выбирайте Rust, когда: +- **Производительность критична**: конвейеры данных, обработка в реальном времени, вычислительно тяжёлые сервисы +- **Корректность важна**: финансовые системы, код, критичный к безопасности, реализации протоколов +- **Простота развёртывания**: один бинарник, без зависимостей рантайма +- **Низкоуровневый контроль**: взаимодействие с железом, интеграция с ОС, встраиваемые системы +- **Настоящая конкурентность**: параллелизм для задач, ограниченных процессором, без обходных путей вокруг GIL +- **Эффективность памяти**: снижение облачных расходов для сервисов, которые много используют память +- **Долгоработающие сервисы**: где важна предсказуемая задержка (без пауз GC) + +### Оставайтесь на Python, когда: +- **Быстрое прототипирование**: исследовательский анализ данных, скрипты, разовые инструменты +- **Рабочие процессы ML/AI**: экосистема PyTorch, TensorFlow, scikit-learn +- **Клеящий код**: подключение API, скрипты преобразования данных +- **Экспертиза команды**: когда кривая обучения Rust не окупается выгодами +- **Время выхода на рынок**: когда скорость разработки важнее скорости выполнения +- **Интерактивная работа**: блокноты Jupyter, разработка через REPL +- **Скрипты**: автоматизация, задачи системного администрирования, быстрые утилиты + +### Рассмотрите оба варианта (гибридный подход с PyO3): +- **Вычислительно тяжёлый код на Rust**: вызывается из Python через PyO3/maturin +- **Бизнес-логика и оркестрация на Python**: привычно и продуктивно +- **Постепенная миграция**: найти узкие места и заменить их расширениями на Rust +- **Лучшее из двух миров**: экосистема Python + производительность Rust *** -## Real-World Impact: Why Companies Choose Rust +## Практический эффект: почему компании выбирают Rust -### Dropbox: Storage Infrastructure -- **Before (Python)**: High CPU usage, memory overhead in sync engine -- **After (Rust)**: 10x performance improvement, 50% memory reduction -- **Result**: Millions saved in infrastructure costs +### Dropbox: инфраструктура хранения +- **До (Python)**: высокая загрузка CPU, избыточное потребление памяти в движке синхронизации +- **После (Rust)**: прирост производительности в 10 раз, снижение потребления памяти на 50% +- **Результат**: миллионы долларов экономии на инфраструктуре -### Discord: Voice/Video Backend -- **Before (Python → Go)**: GC pauses causing audio drops -- **After (Rust)**: Consistent low-latency performance -- **Result**: Better user experience, reduced server costs +### Discord: бэкенд голоса и видео +- **До (Python → Go)**: паузы GC приводили к обрывам звука +- **После (Rust)**: стабильно низкая задержка +- **Результат**: лучше пользовательский опыт и ниже расходы на серверы -### Cloudflare: Edge Workers -- **Why Rust**: WebAssembly compilation, predictable performance at edge -- **Result**: Workers run with microsecond cold starts +### Cloudflare: edge-воркеры +- **Почему Rust**: компиляция в WebAssembly, предсказуемая производительность на периферии сети +- **Результат**: воркеры запускаются с холодным стартом в микросекундах ### Pydantic V2 -- **Before**: Pure Python validation — slow for large payloads -- **After**: Rust core (via PyO3) — **5–50x faster** validation -- **Result**: Same Python API, dramatically faster execution +- **До**: валидация на чистом Python — медленно для больших объёмов данных +- **После**: ядро на Rust (через PyO3) — валидация **в 5–50 раз быстрее** +- **Результат**: тот же API на Python, значительно более быстрое выполнение -### Why This Matters for Python Developers: -1. **Complementary skills**: Rust and Python solve different problems -2. **PyO3 bridge**: Write Rust extensions callable from Python -3. **Performance understanding**: Learn why Python is slow and how to fix hotspots -4. **Career growth**: Systems programming expertise increasingly valuable -5. **Cloud costs**: 10x faster code = significantly lower infrastructure spend +### Почему это важно для разработчиков на Python: +1. **Взаимодополняющие навыки**: Rust и Python решают разные задачи +2. **Мост PyO3**: пишите расширения на Rust, которые можно вызывать из Python +3. **Понимание производительности**: вы узнаете, почему Python медленный и как исправлять узкие места +4. **Карьерный рост**: экспертиза в системном программировании становится всё ценнее +5. **Облачные расходы**: код в 10 раз быстрее = значительно меньше расходов на инфраструктуру *** -## Language Philosophy Comparison +## Сравнение языковой философии -### Python Philosophy -- **Readability counts**: Clean syntax, "one obvious way to do it" -- **Batteries included**: Extensive standard library, rapid prototyping -- **Duck typing**: "If it walks like a duck and quacks like a duck..." -- **Developer velocity**: Optimize for writing speed, not execution speed -- **Dynamic everything**: Modify classes at runtime, monkey-patching, metaclasses +### Философия Python +- **Читаемость важна**: чистый синтаксис, «один очевидный способ сделать это» +- **Всё в комплекте**: обширная стандартная библиотека, быстрое прототипирование +- **Утиная типизация**: «Если это ходит как утка и крякает как утка...» +- **Скорость разработки**: оптимизация под скорость написания кода, а не скорость выполнения +- **Всё динамически**: изменение классов во время выполнения, monkey-patching, метаклассы -### Rust Philosophy -- **Performance without sacrifice**: Zero-cost abstractions, no runtime overhead -- **Correctness first**: If it compiles, entire categories of bugs are impossible -- **Explicit over implicit**: No hidden behavior, no implicit conversions -- **Ownership**: Resources have exactly one owner — memory, files, sockets -- **Fearless concurrency**: The type system prevents data races at compile time +### Философия Rust +- **Производительность без компромиссов**: абстракции с нулевой стоимостью, без накладных расходов рантайма +- **Корректность прежде всего**: если код компилируется, целые классы ошибок становятся невозможными +- **Явное лучше неявного**: никакого скрытого поведения и неявных преобразований +- **Владение**: у ресурсов ровно один владелец: память, файлы, сокеты +- **Конкурентность без страха**: система типов предотвращает гонки данных на этапе компиляции ```mermaid graph LR subgraph PY["🐍 Python"] direction TB - PY_CODE["Your Code"] --> PY_INTERP["Interpreter — CPython VM"] - PY_INTERP --> PY_GC["Garbage Collector — ref count + GC"] - PY_GC --> PY_GIL["GIL — no true parallelism"] - PY_GIL --> PY_OS["OS / Hardware"] + PY_CODE["Ваш код"] --> PY_INTERP["Интерпретатор — виртуальная машина CPython"] + PY_INTERP --> PY_GC["Сборщик мусора — подсчёт ссылок + GC"] + PY_GC --> PY_GIL["GIL — нет настоящего параллелизма"] + PY_GIL --> PY_OS["ОС / Железо"] end PY ~~~ RS subgraph RS["🦀 Rust"] direction TB - RS_CODE["Your Code"] --> RS_NONE["No runtime overhead"] - RS_NONE --> RS_OWN["Ownership — compile-time, zero-cost"] - RS_OWN --> RS_THR["Native threads — true parallelism"] - RS_THR --> RS_OS["OS / Hardware"] + RS_CODE["Ваш код"] --> RS_NONE["Нет накладных расходов рантайма"] + RS_NONE --> RS_OWN["Владение — на этапе компиляции, без затрат"] + RS_OWN --> RS_THR["Нативные потоки — настоящий параллелизм"] + RS_THR --> RS_OS["ОС / Железо"] end style PY_INTERP fill:#fff3e0,color:#000,stroke:#e65100 @@ -423,48 +410,47 @@ graph LR *** -## Quick Reference: Rust vs Python - -| **Concept** | **Python** | **Rust** | **Key Difference** | -|-------------|-----------|----------|-------------------| -| Typing | Dynamic (`duck typing`) | Static (compile-time) | Errors caught before runtime | -| Memory | Garbage collected (ref counting + cycle GC) | Ownership system | Zero-cost, deterministic cleanup | -| None/null | `None` anywhere | `Option` | Compile-time None safety | -| Error handling | `raise`/`try`/`except` | `Result` | Explicit, no hidden control flow | -| Mutability | Everything mutable | Immutable by default | Opt-in to mutation | -| Speed | Interpreted (~10–100x slower) | Compiled (C/C++ speed) | Orders of magnitude faster | -| Concurrency | GIL limits threads | No GIL, `Send`/`Sync` traits | True parallelism by default | -| Dependencies | `pip install` / `poetry add` | `cargo add` | Built-in dependency management | -| Build system | setuptools/poetry/hatch | Cargo | Single unified tool | -| Packaging | `pyproject.toml` | `Cargo.toml` | Similar declarative config | -| REPL | `python` interactive | No REPL (use tests/`cargo run`) | Compile-first workflow | -| Type hints | Optional, not enforced | Required, compiler-enforced | Types are not decorative | +## Краткая справка: Rust против Python + +| **Концепция** | **Python** | **Rust** | **Ключевое отличие** | +|---------------|-----------|----------|---------------------| +| Типизация | Динамическая (`duck typing`) | Статическая (на этапе компиляции) | Ошибки обнаруживаются до выполнения | +| Память | Сборка мусора (подсчёт ссылок + GC для циклов) | Система владения | Без накладных расходов, детерминированная очистка | +| None/null | `None` где угодно | `Option` | Безопасность None на этапе компиляции | +| Обработка ошибок | `raise`/`try`/`except` | `Result` | Явно, без скрытого потока управления | +| Изменяемость | Всё изменяемо | Неизменяемость по умолчанию | Изменение нужно включать явно | +| Скорость | Интерпретируемый (~в 10–100 раз медленнее) | Компилируемый (скорость C/C++) | На порядки быстрее | +| Конкурентность | GIL ограничивает потоки | Нет GIL, трейты `Send`/`Sync` | Настоящий параллелизм по умолчанию | +| Зависимости | `pip install` / `poetry add` | `cargo add` | Управление зависимостями встроено | +| Система сборки | setuptools/poetry/hatch | Cargo | Один единый инструмент | +| Упаковка | `pyproject.toml` | `Cargo.toml` | Похожая декларативная конфигурация | +| REPL | Интерактивный `python` | Нет REPL (используйте тесты или `cargo run`) | Процесс, ориентированный на компиляцию | +| Подсказки типов | Необязательные, не проверяются | Обязательные, проверяются компилятором | Типы — не украшение | --- -## Exercises +## Упражнения
-🏋️ Exercise: Mental Model Check (click to expand) +🏋️ Упражнение: проверка ментальной модели (нажмите, чтобы раскрыть) -**Challenge**: For each Python snippet, predict what Rust would require differently. Don't write code — just describe the constraint. +**Задание**: для каждого фрагмента на Python предскажите, что в Rust потребуется иначе. Код писать не нужно, просто опишите ограничение. -1. `x = [1, 2, 3]; y = x; x.append(4)` — What happens in Rust? -2. `data = None; print(data.upper())` — How does Rust prevent this? -3. `import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start()` — What does Rust demand? +1. `x = [1, 2, 3]; y = x; x.append(4)` — что произойдёт в Rust? +2. `data = None; print(data.upper())` — как Rust этому помешает? +3. `import threading; shared = []; threading.Thread(target=shared.append, args=(1,)).start()` — что требует Rust?
-🔑 Solution +🔑 Решение -1. **Ownership move**: `let y = x;` moves `x` — `x.push(4)` is a compile error. You'd need `let y = x.clone();` or borrow with `let y = &x;`. -2. **No null**: `data` can't be `None` unless it's `Option`. You must `match` or use `.unwrap()` / `if let` — no surprise `NoneType` errors. -3. **Send + Sync**: The compiler requires `shared` to be wrapped in `Arc>>`. Forgetting the lock = compile error, not a race condition. +1. **Перемещение владения**: `let y = x;` перемещает `x`, поэтому `x.push(4)` — ошибка компиляции. Понадобилось бы `let y = x.clone();` или заимствование через `let y = &x;`. +2. **Нет null**: `data` не может быть `None`, если только это не `Option`. Нужно использовать `match`, `.unwrap()` или `if let`, и неожиданных ошибок `NoneType` не будет. +3. **Send + Sync**: компилятор требует обернуть `shared` в `Arc>>`. Забытая блокировка — это ошибка компиляции, а не гонка данных. -**Key takeaway**: Rust shifts runtime failures to compile-time errors. The "friction" you feel is the compiler catching real bugs. +**Главный вывод**: Rust переносит ошибки времени выполнения в ошибки времени компиляции. «Трение», которое вы чувствуете, — это компилятор, который ловит настоящие баги.
*** - diff --git a/python-book/src/ch02-getting-started.md b/python-book/src/ch02-getting-started.md index 822597d..61d8bd1 100644 --- a/python-book/src/ch02-getting-started.md +++ b/python-book/src/ch02-getting-started.md @@ -1,106 +1,105 @@ -## Installation and Setup +## Установка и настройка -> **What you'll learn:** How to install Rust and its toolchain, the Cargo build system vs pip/Poetry, -> IDE setup, your first `Hello, world!` program, and essential Rust keywords mapped to Python equivalents. +> **Что вы узнаете:** как установить Rust и его тулчейн, чем система сборки Cargo отличается от pip/Poetry, как настроить IDE, как написать первую программу `Hello, world!` и какие ключевые слова Rust соответствуют конструкциям Python. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Installing Rust +### Установка Rust ```bash -# Install Rust via rustup (Linux/macOS/WSL) +# Установка Rust через rustup (Linux/macOS/WSL) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -# Verify installation -rustc --version # Rust compiler -cargo --version # Build tool + package manager (like pip + setuptools combined) +# Проверка установки +rustc --version # Компилятор Rust +cargo --version # Инструмент сборки + менеджер пакетов (как pip и setuptools вместе) -# Update Rust +# Обновление Rust rustup update ``` -### Rust Tools vs Python Tools - -| Purpose | Python | Rust | -|---------|--------|------| -| Language runtime | `python` (interpreter) | `rustc` (compiler, rarely called directly) | -| Package manager | `pip` / `poetry` / `uv` | `cargo` (built-in) | -| Project config | `pyproject.toml` | `Cargo.toml` | -| Lock file | `poetry.lock` / `requirements.txt` | `Cargo.lock` | -| Virtual env | `venv` / `conda` | Not needed (deps are per-project) | -| Formatter | `black` / `ruff format` | `rustfmt` (built-in: `cargo fmt`) | -| Linter | `ruff` / `flake8` / `pylint` | `clippy` (built-in: `cargo clippy`) | -| Type checker | `mypy` / `pyright` | Built into compiler (always on) | -| Test runner | `pytest` | `cargo test` (built-in) | -| Docs | `sphinx` / `mkdocs` | `cargo doc` (built-in) | -| REPL | `python` / `ipython` | None (use `cargo test` or Rust Playground) | - -### IDE Setup - -**VS Code** (recommended): +### Инструменты Rust и инструменты Python + +| Назначение | Python | Rust | +|------------|--------|------| +| Среда выполнения языка | `python` (интерпретатор) | `rustc` (компилятор, напрямую вызывается редко) | +| Менеджер пакетов | `pip` / `poetry` / `uv` | `cargo` (встроен) | +| Конфигурация проекта | `pyproject.toml` | `Cargo.toml` | +| Lock-файл | `poetry.lock` / `requirements.txt` | `Cargo.lock` | +| Виртуальное окружение | `venv` / `conda` | Не нужно (зависимости хранятся для каждого проекта) | +| Форматтер | `black` / `ruff format` | `rustfmt` (встроен: `cargo fmt`) | +| Линтер | `ruff` / `flake8` / `pylint` | `clippy` (встроен: `cargo clippy`) | +| Проверка типов | `mypy` / `pyright` | Встроена в компилятор (всегда включена) | +| Запуск тестов | `pytest` | `cargo test` (встроен) | +| Документация | `sphinx` / `mkdocs` | `cargo doc` (встроен) | +| REPL | `python` / `ipython` | Нет (используйте `cargo test` или Rust Playground) | + +### Настройка IDE + +**VS Code** (рекомендуется): ```text -Extensions to install: -- rust-analyzer ← Essential: IDE features, type hints, completions -- Even Better TOML ← Syntax highlighting for Cargo.toml -- CodeLLDB ← Debugger support - -# Python equivalent mapping: -# rust-analyzer ≈ Pylance (but with 100% type coverage, always) -# cargo clippy ≈ ruff (but checks correctness, not just style) +Расширения для установки: +- rust-analyzer ← Обязательно: возможности IDE, подсказки типов, автодополнение +- Even Better TOML ← Подсветка синтаксиса для Cargo.toml +- CodeLLDB ← Поддержка отладчика + +# Соответствие с Python: +# rust-analyzer ≈ Pylance (но с полным покрытием типами всегда) +# cargo clippy ≈ ruff (но проверяет корректность, а не только стиль) ``` *** -## Your First Rust Program +## Ваша первая программа на Rust -### Python Hello World +### Hello World на Python ```python -# hello.py — just run it +# hello.py — просто запустите print("Hello, World!") -# Run: +# Запуск: # python hello.py ``` -### Rust Hello World +### Hello World на Rust ```rust -// src/main.rs — must be compiled first +// src/main.rs — сначала нужно скомпилировать fn main() { - println!("Hello, World!"); // println! is a macro (note the !) + println!("Hello, World!"); // println! — это макрос (обратите внимание на !) } -// Build and run: +// Сборка и запуск: // cargo run ``` -### Key Differences for Python Developers +### Ключевые различия для разработчиков на Python ```text Python: Rust: ───────── ───── -- No main() needed - fn main() is the entry point -- Indentation = blocks - Curly braces {} = blocks -- print() is a function - println!() is a macro (the ! matters) -- No semicolons - Semicolons end statements -- No type declarations - Types inferred but always known -- Interpreted (run directly) - Compiled (cargo build, then run) -- Errors at runtime - Most errors at compile time +- main() не нужен - fn main() — точка входа +- Отступы = блоки - Фигурные скобки {} = блоки +- print() — это функция - println!() — это макрос (важен восклицательный знак) +- Без точек с запятой - Точка с запятой завершает инструкции +- Без объявлений типов - Типы выводятся, но всегда известны +- Интерпретируется (запуск напрямую) - Компилируется (cargo build, затем запуск) +- Ошибки во время выполнения - Большинство ошибок на этапе компиляции ``` -### Creating Your First Project +### Создание первого проекта ```bash # Python # Rust mkdir myproject cargo new myproject cd myproject cd myproject -python -m venv .venv # No virtual env needed -source .venv/bin/activate # No activation needed -# Create files manually # src/main.rs already created +python -m venv .venv # Виртуальное окружение не нужно +source .venv/bin/activate # Активация не нужна +# Создаём файлы вручную # src/main.rs уже создан -# Python project structure: Rust project structure: +# Структура проекта Python: Структура проекта Rust: # myproject/ myproject/ -# ├── pyproject.toml ├── Cargo.toml (like pyproject.toml) +# ├── pyproject.toml ├── Cargo.toml (аналог pyproject.toml) # ├── src/ ├── src/ -# │ └── myproject/ │ └── main.rs (entry point) -# │ ├── __init__.py └── (no __init__.py needed) +# │ └── myproject/ │ └── main.rs (точка входа) +# │ ├── __init__.py └── (__init__.py не нужен) # │ └── main.py # └── tests/ # └── test_main.py @@ -108,30 +107,30 @@ source .venv/bin/activate # No activation needed ```mermaid graph LR - subgraph Python ["Python Project"] + subgraph Python ["Проект Python"] PP["pyproject.toml"] --- PS["src/"] PS --- PM["myproject/"] PM --- PI["__init__.py"] PM --- PMN["main.py"] PP --- PT["tests/"] end - subgraph Rust ["Rust Project"] + subgraph Rust ["Проект Rust"] RC["Cargo.toml"] --- RS["src/"] RS --- RM["main.rs"] - RC --- RTG["target/ (auto-generated)"] + RC --- RTG["target/ (генерируется автоматически)"] end Python ~~~ Rust style Python fill:#ffeeba style Rust fill:#d4edda ``` -> **Key difference**: Rust projects are simpler — no `__init__.py`, no virtual environments, no `setup.py` vs `setup.cfg` vs `pyproject.toml` confusion. Just `Cargo.toml` + `src/`. +> **Ключевое отличие**: проекты на Rust проще. Не нужны `__init__.py`, виртуальные окружения и путаница между `setup.py`, `setup.cfg` и `pyproject.toml`. Достаточно `Cargo.toml` и `src/`. *** -## Cargo vs pip/Poetry +## Cargo и pip/Poetry -### Project Configuration +### Конфигурация проекта ```toml # Python — pyproject.toml @@ -153,170 +152,170 @@ dev = ["pytest", "ruff", "mypy"] [package] name = "myproject" version = "0.1.0" -edition = "2021" # Rust edition (like Python version) +edition = "2021" # Редакция Rust (аналог версии Python) [dependencies] -reqwest = "0.12" # HTTP client (like requests) -serde = { version = "1.0", features = ["derive"] } # Serialization (like pydantic) +reqwest = "0.12" # HTTP-клиент (аналог requests) +serde = { version = "1.0", features = ["derive"] } # Сериализация (аналог pydantic) [dev-dependencies] -# Test dependencies — only compiled for `cargo test` -# (No separate test config needed — `cargo test` is built in) +# Тестовые зависимости — собираются только для `cargo test` +# (Отдельная конфигурация для тестов не нужна — `cargo test` встроен) ``` -### Common Cargo Commands +### Частые команды Cargo ```bash -# Python equivalent # Rust +# Аналог в Python # Rust pip install requests cargo add reqwest -pip install -r requirements.txt cargo build # auto-installs deps -pip install -e . cargo build # always "editable" +pip install -r requirements.txt cargo build # автоматически устанавливает зависимости +pip install -e . cargo build # всегда «редактируемая» сборка python -m pytest cargo test -python -m mypy . # Built into compiler — always runs +python -m mypy . # Встроено в компилятор — работает всегда ruff check . cargo clippy ruff format . cargo fmt python main.py cargo run -python -c "..." # No equivalent — use cargo run or tests +python -c "..." # Аналога нет — используйте cargo run или тесты -# Rust-specific: -cargo new myproject # Create new project -cargo build --release # Optimized build (10-100x faster than debug) -cargo doc --open # Generate and browse API docs -cargo update # Update deps (like pip install --upgrade) +# Специфичное для Rust: +cargo new myproject # Создать новый проект +cargo build --release # Оптимизированная сборка (в 10–100 раз быстрее отладочной) +cargo doc --open # Сгенерировать и открыть документацию API +cargo update # Обновить зависимости (аналог pip install --upgrade) ``` *** -## Essential Rust Keywords for Python Developers +## Ключевые слова Rust для разработчиков на Python -### Variable and Mutability Keywords +### Ключевые слова для переменных и изменяемости ```rust -// let — declare a variable (like Python assignment, but immutable by default) -let name = "Alice"; // Python: name = "Alice" (but mutable) -// name = "Bob"; // ❌ Compile error! Immutable by default +// let — объявление переменной (как присваивание в Python, но неизменяемой по умолчанию) +let name = "Alice"; // Python: name = "Alice" (но изменяемая) +// name = "Bob"; // ❌ Ошибка компиляции! По умолчанию неизменяемая -// mut — opt into mutability -let mut count = 0; // Python: count = 0 (always mutable in Python) -count += 1; // ✅ Allowed because of `mut` +// mut — явное включение изменяемости +let mut count = 0; // Python: count = 0 (в Python всегда изменяемая) +count += 1; // ✅ Разрешено благодаря `mut` -// const — compile-time constant (like Python's convention of UPPER_CASE, but enforced) -const MAX_SIZE: usize = 1024; // Python: MAX_SIZE = 1024 (convention only) +// const — константа времени компиляции (как соглашение Python об UPPER_CASE, но с принудительной проверкой) +const MAX_SIZE: usize = 1024; // Python: MAX_SIZE = 1024 (только соглашение) -// static — global variable (use sparingly; Python has module-level globals) +// static — глобальная переменная (используйте осторожно; в Python есть глобальные переменные уровня модуля) static VERSION: &str = "1.0"; ``` -### Ownership and Borrowing Keywords +### Ключевые слова владения и заимствования ```rust -// These have NO Python equivalents — they're Rust-specific concepts +// У этих ключевых слов НЕТ аналогов в Python — это специфичные для Rust концепции -// & — borrow (read-only reference) -fn print_name(name: &str) { } // Python: def print_name(name: str) — but Python passes ref always +// & — заимствование (ссылка только для чтения) +fn print_name(name: &str) { } // Python: def print_name(name: str) — но Python всегда передаёт по ссылке -// &mut — mutable borrow -fn append(list: &mut Vec) { } // Python: def append(lst: list) — always mutable in Python +// &mut — изменяемое заимствование +fn append(list: &mut Vec) { } // Python: def append(lst: list) — в Python всегда изменяемый -// move — transfer ownership (happens implicitly in Rust, never in Python) +// move — передача владения (в Rust происходит неявно, в Python — никогда) let s1 = String::from("hello"); -let s2 = s1; // s1 is MOVED to s2 — s1 is no longer valid -// println!("{}", s1); // ❌ Compile error: value moved +let s2 = s1; // s1 ПЕРЕМЕЩЁН в s2 — s1 больше не действителен +// println!("{}", s1); // ❌ Ошибка компиляции: значение перемещено ``` -### Type Definition Keywords +### Ключевые слова определения типов ```rust -// struct — like a Python dataclass or NamedTuple +// struct — как dataclass или NamedTuple в Python struct Point { // @dataclass x: f64, // class Point: y: f64, // x: float } // y: float -// enum — like Python's enum but MUCH more powerful (carries data) -enum Shape { // No direct Python equivalent - Circle(f64), // Each variant can hold different data +// enum — как enum в Python, но гораздо мощнее (хранит данные) +enum Shape { // Прямого аналога в Python нет + Circle(f64), // Каждый вариант может хранить свои данные Rectangle(f64, f64), } -// impl — attach methods to a type (like defining methods in a class) +// impl — присоединяет методы к типу (как определение методов в классе) impl Point { // class Point: fn distance(&self) -> f64 { // def distance(self) -> float: (self.x.powi(2) + self.y.powi(2)).sqrt() } } -// trait — like Python's ABC or Protocol (PEP 544) +// trait — как ABC или Protocol (PEP 544) в Python trait Drawable { // class Drawable(Protocol): fn draw(&self); // def draw(self) -> None: ... } -// type — type alias (like Python's TypeAlias) -type UserId = i64; // UserId = int (or TypeAlias) +// type — псевдоним типа (как TypeAlias в Python) +type UserId = i64; // UserId = int (или TypeAlias) ``` -### Control Flow Keywords +### Ключевые слова управления потоком ```rust -// match — exhaustive pattern matching (like Python 3.10+ match, but enforced) +// match — исчерпывающее сопоставление с образцом (как match в Python 3.10+, но с обязательной проверкой) match value { 1 => println!("one"), 2 | 3 => println!("two or three"), - _ => println!("other"), // _ = wildcard (like Python's case _:) + _ => println!("other"), // _ = подстановка (как case _: в Python) } -// if let — destructure + conditional (Pythonic: if (m := regex.match(s)):) +// if let — деструктуризация с условием (по-питоновски: if (m := regex.match(s)):) if let Some(x) = optional_value { println!("{}", x); } -// loop — infinite loop (like while True:) +// loop — бесконечный цикл (как while True:) loop { - break; // Must break to exit + break; // Для выхода нужен break } -// for — iteration (like Python's for, but needs .iter() more often) +// for — итерация (как for в Python, но .iter() нужен чаще) for item in collection.iter() { // for item in collection: println!("{}", item); } -// while let — loop with destructuring +// while let — цикл с деструктуризацией while let Some(item) = stack.pop() { process(item); } ``` -### Visibility Keywords +### Ключевые слова видимости ```rust -// pub — public (Python has no real private; uses _ convention) -pub fn greet() { } // def greet(): — everything is "public" in Python +// pub — публичный (в Python нет настоящего private; используется соглашение с _) +pub fn greet() { } // def greet(): — в Python всё «публично» -// pub(crate) — visible within the crate only -pub(crate) fn internal() { } // def _internal(): — single underscore convention +// pub(crate) — видим только внутри крейта +pub(crate) fn internal() { } // def _internal(): — соглашение с одним подчёркиванием -// (no keyword) — private to the module -fn private_helper() { } // def __private(): — double underscore name mangling +// (без ключевого слова) — приватный для модуля +fn private_helper() { } // def __private(): — искажение имён с двумя подчёркиваниями -// In Python, "private" is a gentleman's agreement. -// In Rust, private is enforced by the compiler. +// В Python «приватность» — это джентльменское соглашение. +// В Rust приватность гарантирует компилятор. ``` --- -## Exercises +## Упражнения
-🏋️ Exercise: First Rust Program (click to expand) +🏋️ Упражнение: первая программа на Rust (нажмите, чтобы раскрыть) -**Challenge**: Create a new Rust project and write a program that: -1. Declares a variable `name` with your name (type `&str`) -2. Declares a mutable variable `count` starting at 0 -3. Uses a `for` loop from 1..=5 to increment `count` and print `"Hello, {name}! (count: {count})"` -4. After the loop, print whether count is even or odd using a `match` expression +**Задание**: создайте новый проект на Rust и напишите программу, которая: +1. Объявляет переменную `name` с вашим именем (тип `&str`) +2. Объявляет изменяемую переменную `count`, начальное значение 0 +3. Использует цикл `for` от 1..=5, чтобы увеличивать `count` и выводить `"Привет, {name}! (счётчик: {count})"` +4. После цикла выводит, чётное или нечётное значение `count`, с помощью выражения `match`
-🔑 Solution +🔑 Решение ```bash cargo new hello_rust && cd hello_rust @@ -330,26 +329,25 @@ fn main() { for _ in 1..=5 { count += 1; - println!("Hello, {name}! (count: {count})"); + println!("Привет, {name}! (счётчик: {count})"); } let parity = match count % 2 { - 0 => "even", - _ => "odd", + 0 => "чётное", + _ => "нечётное", }; - println!("Final count {count} is {parity}"); + println!("Итоговый счётчик {count} — {parity}"); } ``` -**Key takeaways**: -- `let` is immutable by default (you need `mut` to change `count`) -- `1..=5` is inclusive range (Python's `range(1, 6)`) -- `match` is an expression that returns a value -- No `self`, no `if __name__ == "__main__"` — just `fn main()` +**Главные выводы**: +- `let` по умолчанию неизменяем (чтобы менять `count`, нужен `mut`) +- `1..=5` — диапазон с включённой правой границей (аналог `range(1, 6)` в Python) +- `match` — это выражение, которое возвращает значение +- Никакого `self` и никакого `if __name__ == "__main__"`, только `fn main()`
*** - diff --git a/python-book/src/ch03-built-in-types-and-variables.md b/python-book/src/ch03-built-in-types-and-variables.md index d082d4e..628a55c 100644 --- a/python-book/src/ch03-built-in-types-and-variables.md +++ b/python-book/src/ch03-built-in-types-and-variables.md @@ -1,54 +1,53 @@ -## Variables and Mutability +## Переменные и изменяемость -> **What you'll learn:** Immutable-by-default variables, explicit `mut`, primitive numeric types vs Python's arbitrary-precision `int`, -> `String` vs `&str` (the hardest early concept), string formatting, and Rust's required type annotations. +> **Что вы узнаете:** переменные, неизменяемые по умолчанию, явный `mut`, примитивные числовые типы в сравнении с `int` произвольной точности в Python, `String` и `&str` (самая трудная концепция на раннем этапе), форматирование строк и обязательные аннотации типов в Rust. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Python Variable Declaration +### Объявление переменных в Python ```python -# Python — everything is mutable, dynamically typed -count = 0 # Mutable, type inferred as int -count = 5 # ✅ Works -count = "hello" # ✅ Works — type can change! (dynamic typing) +# Python — всё изменяемо, типизация динамическая +count = 0 # Изменяемая, тип выводится как int +count = 5 # ✅ Работает +count = "hello" # ✅ Работает — тип может меняться! (динамическая типизация) -# "Constants" are just convention: -MAX_SIZE = 1024 # Nothing prevents MAX_SIZE = 999 later +# «Константы» — это только соглашение: +MAX_SIZE = 1024 # Ничто не мешает потом написать MAX_SIZE = 999 ``` -### Rust Variable Declaration +### Объявление переменных в Rust ```rust -// Rust — immutable by default, statically typed -let count = 0; // Immutable, type inferred as i32 -// count = 5; // ❌ Compile error: cannot assign twice to immutable variable -// count = "hello"; // ❌ Compile error: expected integer, found &str +// Rust — по умолчанию неизменяемые, статически типизированные +let count = 0; // Неизменяемая, тип выводится как i32 +// count = 5; // ❌ Ошибка компиляции: нельзя дважды присвоить значение неизменяемой переменной +// count = "hello"; // ❌ Ошибка компиляции: ожидалось целое число, найден &str -let mut count = 0; // Explicitly mutable -count = 5; // ✅ Works -// count = "hello"; // ❌ Still can't change type +let mut count = 0; // Явно изменяемая +count = 5; // ✅ Работает +// count = "hello"; // ❌ Тип по-прежнему нельзя менять -const MAX_SIZE: usize = 1024; // True constant — enforced by compiler +const MAX_SIZE: usize = 1024; // Настоящая константа — гарантируется компилятором ``` -### Key Mental Shift for Python Developers +### Ключевой сдвиг мышления для разработчиков на Python ```rust -// Python: variables are labels that point to objects -// Rust: variables are named storage locations that OWN their values +// Python: переменные — это метки, которые указывают на объекты +// Rust: переменные — это именованные места хранения, которые ВЛАДЕЮТ своими значениями -// Variable shadowing — unique to Rust, very useful +// Затенение (shadowing) — характерно для Rust, очень полезно let input = "42"; // &str -let input = input.parse::().unwrap(); // Now it's i32 — new variable, same name -let input = input * 2; // Now it's 84 — another new variable +let input = input.parse::().unwrap(); // Теперь i32 — новая переменная с тем же именем +let input = input * 2; // Теперь 84 — ещё одна новая переменная -// In Python, you'd just reassign and lose the old type: +// В Python вы бы просто переприсвоили значение и потеряли старый тип: # input = "42" -# input = int(input) # Same name, different type — Python allows this too -# But in Rust, each `let` creates a genuinely new binding. The old one is gone. +# input = int(input) # То же имя, другой тип — Python тоже это позволяет +# Но в Rust каждый `let` создаёт действительно новую привязку. Старая исчезает. ``` -### Practical Example: Counter +### Практический пример: счётчик ```python -# Python version +# Версия на Python class Counter: def __init__(self): self.value = 0 @@ -65,7 +64,7 @@ print(c.get_value()) # 1 ``` ```rust -// Rust version +// Версия на Rust struct Counter { value: i64, } @@ -75,170 +74,167 @@ impl Counter { Counter { value: 0 } } - fn increment(&mut self) { // &mut self = I will modify this + fn increment(&mut self) { // &mut self — этот метод будет изменять объект self.value += 1; } - fn get_value(&self) -> i64 { // &self = I only read this + fn get_value(&self) -> i64 { // &self — этот метод только читает self.value } } fn main() { - let mut c = Counter::new(); // Must be `mut` to call increment() + let mut c = Counter::new(); // Нужен `mut`, чтобы вызвать increment() c.increment(); println!("{}", c.get_value()); // 1 } ``` -> **Key difference**: In Rust, `&mut self` in the method signature tells you (and the -> compiler) that `increment` modifies the counter. In Python, any method can mutate -> anything — you have to read the code to know. +> **Ключевое отличие**: в Rust `&mut self` в сигнатуре метода сообщает и вам, и компилятору, что `increment` изменяет счётчик. В Python любой метод может изменить что угодно, и чтобы это понять, нужно читать код. *** -## Primitive Types Comparison +## Сравнение примитивных типов ```mermaid flowchart LR - subgraph Python ["Python Types"] - PI["int
(arbitrary precision)"] - PF["float
(64-bit only)"] + subgraph Python ["Типы Python"] + PI["int
(произвольная точность)"] + PF["float
(только 64 бита)"] PB["bool"] PS["str
(Unicode)"] end - subgraph Rust ["Rust Types"] + subgraph Rust ["Типы Rust"] RI["i8 / i16 / i32 / i64 / i128
u8 / u16 / u32 / u64 / u128"] RF["f32 / f64"] RB["bool"] RS["String / &str"] end - PI -->|"fixed-size"| RI - PF -->|"choose precision"| RF - PB -->|"same"| RB - PS -->|"owned vs borrowed"| RS + PI -->|"фиксированный размер"| RI + PF -->|"выбор точности"| RF + PB -->|"то же"| RB + PS -->|"владение или заимствование"| RS style Python fill:#ffeeba style Rust fill:#d4edda ``` -### Numeric Types +### Числовые типы -| Python | Rust | Notes | -|--------|------|-------| -| `int` (arbitrary precision) | `i8`, `i16`, `i32`, `i64`, `i128`, `isize` | Rust integers have fixed size | -| `int` (unsigned: no separate type) | `u8`, `u16`, `u32`, `u64`, `u128`, `usize` | Explicit unsigned types | -| `float` (64-bit IEEE 754) | `f32`, `f64` | Python only has 64-bit float | -| `bool` | `bool` | Same concept | -| `complex` | No built-in (use `num` crate) | Rare in systems code | +| Python | Rust | Примечания | +|--------|------|------------| +| `int` (произвольная точность) | `i8`, `i16`, `i32`, `i64`, `i128`, `isize` | Целые числа в Rust имеют фиксированный размер | +| `int` (беззнаковые: отдельного типа нет) | `u8`, `u16`, `u32`, `u64`, `u128`, `usize` | Беззнаковые типы указываются явно | +| `float` (64-бит, IEEE 754) | `f32`, `f64` | В Python есть только 64-битный float | +| `bool` | `bool` | Та же концепция | +| `complex` | Нет встроенного (используйте крейт `num`) | Редко встречается в системном коде | ```python -# Python — one integer type, arbitrary precision -x = 42 # int — can grow to any size -big = 2 ** 1000 # Still works — thousands of digits -y = 3.14 # float — always 64-bit +# Python — один целочисленный тип, произвольная точность +x = 42 # int — может расти до любого размера +big = 2 ** 1000 # По-прежнему работает — тысячи цифр +y = 3.14 # float — всегда 64-битный ``` ```rust -// Rust — explicit sizes, overflow is a compile/runtime error -let x: i32 = 42; // 32-bit signed integer -let y: f64 = 3.14; // 64-bit float (Python's float equivalent) -let big: i128 = 2_i128.pow(100); // 128-bit max — no arbitrary precision -// For arbitrary precision: use the `num-bigint` crate +// Rust — явные размеры, переполнение — ошибка компиляции или выполнения +let x: i32 = 42; // 32-битное знаковое целое +let y: f64 = 3.14; // 64-битное число с плавающей точкой (аналог float в Python) +let big: i128 = 2_i128.pow(100); // Максимум 128 бит — произвольной точности нет +// Для произвольной точности используйте крейт `num-bigint` -// Underscores for readability (like Python's 1_000_000): -let million = 1_000_000; // Same syntax as Python! +// Подчёркивания для читаемости (как 1_000_000 в Python): +let million = 1_000_000; // Тот же синтаксис, что и в Python! -// Type suffix syntax: +// Синтаксис суффиксов типа: let a = 42u8; // u8 let b = 3.14f32; // f32 ``` -### Size Types (Important!) +### Типы размера (важно!) ```rust -// usize and isize — pointer-sized integers, used for indexing -let length: usize = vec![1, 2, 3].len(); // .len() returns usize -let index: usize = 0; // Array indices are always usize +// usize и isize — целые размером с указатель, используются для индексации +let length: usize = vec![1, 2, 3].len(); // .len() возвращает usize +let index: usize = 0; // Индексы массивов всегда usize -// In Python, len() returns int and indices are int — no distinction. -// In Rust, mixing i32 and usize requires explicit conversion: +// В Python len() возвращает int, и индексы тоже int — различий нет. +// В Rust смешивание i32 и usize требует явного преобразования: let i: i32 = 5; -// let item = vec[i]; // ❌ Error: expected usize, found i32 -let item = vec[i as usize]; // ✅ Explicit conversion +// let item = vec[i]; // ❌ Ошибка: ожидался usize, найден i32 +let item = vec[i as usize]; // ✅ Явное преобразование ``` -### Type Inference +### Вывод типов ```rust -// Rust infers types but they're FIXED — not dynamic -let x = 42; // Compiler infers i32 (default integer type) -let y = 3.14; // Compiler infers f64 (default float type) -let s = "hello"; // Compiler infers &str (string slice) -let v = vec![1, 2]; // Compiler infers Vec +// Rust выводит типы, но они ФИКСИРОВАННЫЕ — не динамические +let x = 42; // Компилятор выводит i32 (целый тип по умолчанию) +let y = 3.14; // Компилятор выводит f64 (вещественный тип по умолчанию) +let s = "hello"; // Компилятор выводит &str (срез строки) +let v = vec![1, 2]; // Компилятор выводит Vec -// You can always be explicit: +// Можно всегда указать тип явно: let x: i64 = 42; let y: f32 = 3.14; -// Unlike Python, the type can NEVER change after inference: +// В отличие от Python, тип НИКОГДА не меняется после вывода: let x = 42; -// x = "hello"; // ❌ Error: expected integer, found &str +// x = "hello"; // ❌ Ошибка: ожидалось целое число, найден &str ``` *** -## String Types: String vs &str +## Строковые типы: String и &str -This is one of the biggest surprises for Python developers. Rust has **two** main -string types where Python has one. +Это один из самых больших сюрпризов для разработчиков на Python. В Rust есть **два** основных строковых типа там, где в Python один. -### Python String Handling +### Работа со строками в Python ```python -# Python — one string type, immutable, reference counted -name = "Alice" # str — immutable, heap allocated -greeting = f"Hello, {name}!" # f-string formatting -chars = list(name) # Convert to list of characters -upper = name.upper() # Returns new string (immutable) +# Python — один строковый тип, неизменяемый, с подсчётом ссылок +name = "Alice" # str — неизменяемый, размещается в куче +greeting = f"Hello, {name}!" # Форматирование через f-строку +chars = list(name) # Преобразование в список символов +upper = name.upper() # Возвращает новую строку (строки неизменяемы) ``` -### Rust String Types +### Строковые типы Rust ```rust -// Rust has TWO string types: +// В Rust ДВА строковых типа: -// 1. &str (string slice) — borrowed, immutable, like a "view" into string data -let name: &str = "Alice"; // Points to string data in the binary - // Closest to Python's str, but it's a REFERENCE +// 1. &str (срез строки) — заимствованный, неизменяемый, «вид» на данные строки +let name: &str = "Alice"; // Указывает на данные строки в бинарнике + // Ближе всего к str в Python, но это ССЫЛКА -// 2. String (owned string) — heap-allocated, growable, owned -let mut greeting = String::from("Hello, "); // Owned, can be modified +// 2. String (строка-владелец) — в куче, растёт, принадлежит владельцу +let mut greeting = String::from("Hello, "); // Владеющая строка, её можно изменять greeting.push_str(name); greeting.push('!'); -// greeting is now "Hello, Alice!" +// greeting теперь "Hello, Alice!" ``` -### When to Use Which? +### Когда что использовать? ```rust -// Think of it like this: -// &str = "I'm looking at a string someone else owns" (read-only view) -// String = "I own this string and can modify it" (owned data) +// Думайте так: +// &str = «Я смотрю на строку, которой владеет кто-то другой» (вид только для чтения) +// String = «Я владею этой строкой и могу её менять» (владеющие данные) -// Function parameters: prefer &str (accepts both types) -fn greet(name: &str) -> String { // accepts &str AND &String - format!("Hello, {}!", name) // format! creates a new String +// Параметры функций: лучше &str (принимает оба типа) +fn greet(name: &str) -> String { // принимает и &str, и &String + format!("Hello, {}!", name) // format! создаёт новый String } -let s1 = "world"; // &str literal +let s1 = "world"; // литерал &str let s2 = String::from("Rust"); // String -greet(s1); // ✅ &str works directly -greet(&s2); // ✅ &String auto-converts to &str (Deref coercion) +greet(s1); // ✅ &str работает напрямую +greet(&s2); // ✅ &String автоматически преобразуется в &str (Deref coercion) ``` -### Practical Examples +### Практические примеры ```python -# Python string operations +# Строковые операции в Python name = "alice" upper = name.upper() # "ALICE" contains = "lic" in name # True @@ -249,174 +245,172 @@ replaced = name.replace("a", "A") # "Alice" ``` ```rust -// Rust equivalents +// Аналоги в Rust let name = "alice"; -let upper = name.to_uppercase(); // String — new allocation +let upper = name.to_uppercase(); // String — новое выделение памяти let contains = name.contains("lic"); // bool let parts: Vec<&str> = "a,b,c".split(',').collect(); // Vec<&str> let joined = ["a", "b", "c"].join("-"); // String -let stripped = " hello ".trim(); // &str — no allocation! +let stripped = " hello ".trim(); // &str — без выделения памяти! let replaced = name.replace("a", "A"); // String -// Key insight: some operations return &str (no allocation), others return String. -// .trim() returns a slice of the original — efficient! -// .to_uppercase() must create a new String — allocation required. +// Ключевая мысль: одни операции возвращают &str (без выделения памяти), другие — String. +// .trim() возвращает срез исходной строки — это эффективно! +// .to_uppercase() должен создать новый String — нужно выделение памяти. ``` -### Python Developers: Think of it This Way +### Разработчикам на Python: думайте так ```text -Python str ≈ Rust &str (you usually read strings) -Python str ≈ Rust String (when you need to own/modify) - -Rule of thumb: -- Function parameters → use &str (most flexible) -- Struct fields → use String (struct owns its data) -- Return values → use String (caller needs to own it) -- String literals → automatically &str +Python str ≈ Rust &str (вы в основном читаете строки) +Python str ≈ Rust String (когда нужно владеть или изменять) + +Правило большого пальца: +- Параметры функций → используйте &str (самый гибкий вариант) +- Поля структур → используйте String (структура владеет своими данными) +- Возвращаемые значения → используйте String (вызывающий код должен владеть ими) +- Строковые литералы → автоматически &str ``` *** -## Printing and String Formatting +## Вывод и форматирование строк -### Basic Output +### Базовый вывод ```python # Python print("Hello, World!") -print("Name:", name, "Age:", age) # Space-separated -print(f"Name: {name}, Age: {age}") # f-string +print("Имя:", name, "Возраст:", age) # Через пробел +print(f"Имя: {name}, Возраст: {age}") # f-строка ``` ```rust // Rust println!("Hello, World!"); -println!("Name: {} Age: {}", name, age); // Positional {} -println!("Name: {name}, Age: {age}"); // Inline variables (Rust 1.58+, like f-strings!) +println!("Имя: {} Возраст: {}", name, age); // Позиционные {} +println!("Имя: {name}, Возраст: {age}"); // Встроенные переменные (Rust 1.58+, как f-строки!) ``` -### Format Specifiers +### Спецификаторы формата ```python -# Python formatting -print(f"{3.14159:.2f}") # "3.14" — 2 decimal places -print(f"{42:05d}") # "00042" — zero-padded -print(f"{255:#x}") # "0xff" — hex -print(f"{42:>10}") # " 42" — right-aligned -print(f"{'left':<10}|") # "left |" — left-aligned +# Форматирование в Python +print(f"{3.14159:.2f}") # "3.14" — 2 знака после запятой +print(f"{42:05d}") # "00042" — дополнение нулями +print(f"{255:#x}") # "0xff" — шестнадцатеричный вид +print(f"{42:>10}") # " 42" — выравнивание по правому краю +print(f"{'left':<10}|") # "left |" — выравнивание по левому краю ``` ```rust -// Rust formatting (very similar to Python!) -println!("{:.2}", 3.14159); // "3.14" — 2 decimal places -println!("{:05}", 42); // "00042" — zero-padded -println!("{:#x}", 255); // "0xff" — hex -println!("{:>10}", 42); // " 42" — right-aligned -println!("{:<10}|", "left"); // "left |" — left-aligned +// Форматирование в Rust (очень похоже на Python!) +println!("{:.2}", 3.14159); // "3.14" — 2 знака после запятой +println!("{:05}", 42); // "00042" — дополнение нулями +println!("{:#x}", 255); // "0xff" — шестнадцатеричный вид +println!("{:>10}", 42); // " 42" — выравнивание по правому краю +println!("{:<10}|", "left"); // "left |" — выравнивание по левому краю ``` -### Debug Printing +### Отладочный вывод ```python -# Python — repr() and pprint +# Python — repr() и pprint print(repr([1, 2, 3])) # "[1, 2, 3]" from pprint import pprint -pprint({"key": [1, 2, 3]}) # Pretty-printed +pprint({"key": [1, 2, 3]}) # Красивый вывод ``` ```rust -// Rust — {:?} and {:#?} -println!("{:?}", vec![1, 2, 3]); // "[1, 2, 3]" — Debug format -println!("{:#?}", vec![1, 2, 3]); // Pretty-printed Debug format +// Rust — {:?} и {:#?} +println!("{:?}", vec![1, 2, 3]); // "[1, 2, 3]" — формат Debug +println!("{:#?}", vec![1, 2, 3]); // Красивый формат Debug -// To make your types printable, derive Debug: +// Чтобы типы можно было вывести, добавьте derive Debug: #[derive(Debug)] struct Point { x: f64, y: f64 } let p = Point { x: 1.0, y: 2.0 }; println!("{:?}", p); // "Point { x: 1.0, y: 2.0 }" -println!("{p:?}"); // Same, with inline syntax +println!("{p:?}"); // То же самое, но со встроенным синтаксисом ``` -### Quick Reference +### Краткая справка -| Python | Rust | Notes | -|--------|------|-------| -| `print(x)` | `println!("{}", x)` or `println!("{x}")` | Display format | -| `print(repr(x))` | `println!("{:?}", x)` | Debug format | -| `f"Hello {name}"` | `format!("Hello {name}")` | Returns String | -| `print(x, end="")` | `print!("{x}")` | No newline (`print!` vs `println!`) | -| `print(x, file=sys.stderr)` | `eprintln!("{x}")` | Print to stderr | -| `sys.stdout.write(s)` | `print!("{s}")` | No newline | +| Python | Rust | Примечания | +|--------|------|------------| +| `print(x)` | `println!("{}", x)` или `println!("{x}")` | Формат Display | +| `print(repr(x))` | `println!("{:?}", x)` | Формат Debug | +| `f"Hello {name}"` | `format!("Hello {name}")` | Возвращает String | +| `print(x, end="")` | `print!("{x}")` | Без перевода строки (`print!` и `println!`) | +| `print(x, file=sys.stderr)` | `eprintln!("{x}")` | Вывод в stderr | +| `sys.stdout.write(s)` | `print!("{s}")` | Без перевода строки | *** -## Type Annotations: Optional vs Required +## Аннотации типов: необязательные и обязательные -### Python Type Hints (Optional, Not Enforced) +### Подсказки типов в Python (необязательные, не проверяются) ```python -# Python — type hints are documentation, not enforcement +# Python — подсказки типов служат документацией, а не принудительной проверкой def add(a: int, b: int) -> int: return a + b add(1, 2) # ✅ -add("a", "b") # ✅ Python doesn't care — returns "ab" -add(1, "2") # ✅ Until it crashes at runtime: TypeError +add("a", "b") # ✅ Python не возражает — возвращает "ab" +add(1, "2") # ✅ Пока не упадёт во время выполнения: TypeError -# Union types, Optional +# Union-типы, Optional def find(key: str) -> int | None: ... -# Generic types +# Обобщённые типы def first(items: list[int]) -> int | None: return items[0] if items else None -# Type aliases +# Псевдонимы типов UserId = int Mapping = dict[str, list[int]] ``` -### Rust Type Declarations (Required, Compiler-Enforced) +### Объявления типов в Rust (обязательные, проверяются компилятором) ```rust -// Rust — types are enforced. Always. No exceptions. +// Rust — типы проверяются всегда, без исключений. fn add(a: i32, b: i32) -> i32 { a + b } add(1, 2); // ✅ -// add("a", "b"); // ❌ Compile error: expected i32, found &str +// add("a", "b"); // ❌ Ошибка компиляции: ожидался i32, найден &str -// Optional values use Option +// Необязательные значения используют Option fn find(key: &str) -> Option { - // Returns Some(value) or None + // Возвращает Some(value) или None Some(42) } -// Generic types +// Обобщённые типы fn first(items: &[i32]) -> Option { items.first().copied() } -// Type aliases +// Псевдонимы типов type UserId = i64; type Mapping = HashMap>; ``` -> **Key insight**: In Python, type hints help your IDE and mypy but don't affect runtime. -> In Rust, types ARE the program — the compiler uses them to guarantee memory safety, -> prevent data races, and eliminate null pointer errors. +> **Ключевая мысль**: в Python подсказки типов помогают вашей IDE и mypy, но на выполнение программы не влияют. В Rust типы — это и есть программа: компилятор использует их, чтобы гарантировать безопасность памяти, предотвратить гонки данных и исключить ошибки нулевых указателей. > -> 📌 **See also**: [Ch. 6 — Enums and Pattern Matching](ch06-enums-and-pattern-matching.md) shows how Rust's type system replaces Python's `Union` types and `isinstance()` checks. +> 📌 **См. также**: [Гл. 6 — Перечисления и сопоставление с образцом](ch06-enums-and-pattern-matching.md) показывает, как система типов Rust заменяет `Union`-типы Python и проверки `isinstance()`. --- -## Exercises +## Упражнения
-🏋️ Exercise: Temperature Converter (click to expand) +🏋️ Упражнение: конвертер температуры (нажмите, чтобы раскрыть) -**Challenge**: Write a function `celsius_to_fahrenheit(c: f64) -> f64` and a function `classify(temp_f: f64) -> &'static str` that returns "cold", "mild", or "hot" based on thresholds. Print the result for 0, 20, and 35 degrees Celsius. Use string formatting. +**Задание**: напишите функцию `celsius_to_fahrenheit(c: f64) -> f64` и функцию `classify(temp_f: f64) -> &'static str`, которая по пороговым значениям возвращает `"холодно"`, `"умеренно"` или `"жарко"`. Выведите результат для 0, 20 и 35 градусов Цельсия. Используйте форматирование строк.
-🔑 Solution +🔑 Решение ```rust fn celsius_to_fahrenheit(c: f64) -> f64 { @@ -424,9 +418,9 @@ fn celsius_to_fahrenheit(c: f64) -> f64 { } fn classify(temp_f: f64) -> &'static str { - if temp_f < 50.0 { "cold" } - else if temp_f < 77.0 { "mild" } - else { "hot" } + if temp_f < 50.0 { "холодно" } + else if temp_f < 77.0 { "умеренно" } + else { "жарко" } } fn main() { @@ -437,11 +431,10 @@ fn main() { } ``` -**Key takeaway**: Rust requires explicit `f64` (no implicit int→float), `for` iterates over arrays directly (no `range()`), and `if/else` blocks are expressions. +**Ключевой вывод**: Rust требует явного `f64` (нет неявного преобразования int → float), `for` перебирает массивы напрямую (без `range()`), а блоки `if/else` — это выражения.
*** - diff --git a/python-book/src/ch04-control-flow.md b/python-book/src/ch04-control-flow.md index 0c88084..141a395 100644 --- a/python-book/src/ch04-control-flow.md +++ b/python-book/src/ch04-control-flow.md @@ -1,67 +1,66 @@ -## Conditional Statements +## Условные конструкции -> **What you'll learn:** `if`/`else` without parentheses (but with braces), `loop`/`while`/`for` vs Python's iteration model, -> expression blocks (everything returns a value), and function signatures with mandatory return types. +> **Что вы узнаете:** `if`/`else` без скобок (но с фигурными скобками), `loop`/`while`/`for` в сравнении с моделью итерации Python, блоки-выражения (всё возвращает значение) и сигнатуры функций с обязательными типами возвращаемых значений. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный ### if/else ```python # Python if temperature > 100: - print("Too hot!") + print("Слишком жарко!") elif temperature < 0: - print("Too cold!") + print("Слишком холодно!") else: - print("Just right") + print("В самый раз") -# Ternary +# Тернарный оператор status = "hot" if temperature > 100 else "ok" ``` ```rust -// Rust — braces required, no colons, `else if` not `elif` +// Rust — фигурные скобки обязательны, двоеточий нет, пишется `else if`, а не `elif` if temperature > 100 { - println!("Too hot!"); + println!("Слишком жарко!"); } else if temperature < 0 { - println!("Too cold!"); + println!("Слишком холодно!"); } else { - println!("Just right"); + println!("В самый раз"); } -// if is an EXPRESSION — returns a value (like Python ternary, but more powerful) +// if — это ВЫРАЖЕНИЕ, оно возвращает значение (как тернарный оператор Python, но мощнее) let status = if temperature > 100 { "hot" } else { "ok" }; ``` -### Important Differences +### Важные различия ```rust -// 1. Condition must be a bool — no truthy/falsy +// 1. Условие должно быть bool — никакой «истинности» и «ложности» let x = 42; -// if x { } // ❌ Error: expected bool, found integer -if x != 0 { } // ✅ Explicit comparison required +// if x { } // ❌ Ошибка: ожидался bool, найден целый тип +if x != 0 { } // ✅ Нужно явное сравнение -// In Python, these are all truthy/falsy: -// if []: → False (empty list) -// if "": → False (empty string) -// if 0: → False (zero) +// В Python всё это приводится к True/False: +// if []: → False (пустой список) +// if "": → False (пустая строка) +// if 0: → False (ноль) // if None: → False -// In Rust, ONLY bool works in conditions: +// В Rust в условиях работает ТОЛЬКО bool: let items: Vec = vec![]; -// if items { } // ❌ Error -if !items.is_empty() { } // ✅ Explicit check +// if items { } // ❌ Ошибка +if !items.is_empty() { } // ✅ Явная проверка let name = ""; -// if name { } // ❌ Error -if !name.is_empty() { } // ✅ Explicit check +// if name { } // ❌ Ошибка +if !name.is_empty() { } // ✅ Явная проверка ``` *** -## Loops and Iteration +## Циклы и итерация -### for Loops +### Циклы for ```python # Python for i in range(5): @@ -83,7 +82,7 @@ for i in 0..5 { // range(5) → 0..5 println!("{}", i); } -for item in ["a", "b", "c"] { // Direct iteration +for item in ["a", "b", "c"] { // Прямая итерация println!("{}", item); } @@ -91,24 +90,24 @@ for (i, item) in ["a", "b", "c"].iter().enumerate() { // enumerate() println!("{}: {}", i, item); } -// HashMap iteration +// Итерация по HashMap use std::collections::HashMap; let map = HashMap::from([("x", 1), ("y", 2)]); -for (key, value) in &map { // & borrows the map +for (key, value) in &map { // & — заимствование словаря println!("{} = {}", key, value); } ``` -### Range Syntax +### Синтаксис диапазонов ```rust -Python: Rust: Notes: -range(5) 0..5 Half-open (excludes end) -range(1, 10) 1..10 Half-open -range(1, 11) 1..=10 Inclusive (includes end) -range(0, 10, 2) (0..10).step_by(2) Step (method, not syntax) +Python: Rust: Примечания: +range(5) 0..5 Полуоткрытый (правая граница не включается) +range(1, 10) 1..10 Полуоткрытый +range(1, 11) 1..=10 Закрытый (правая граница включается) +range(0, 10, 2) (0..10).step_by(2) Шаг (метод, а не синтаксис) ``` -### while Loops +### Циклы while ```python # Python count = 0 @@ -116,7 +115,7 @@ while count < 5: print(count) count += 1 -# Infinite loop +# Бесконечный цикл while True: data = get_input() if data == "quit": @@ -131,7 +130,7 @@ while count < 5 { count += 1; } -// Infinite loop — use `loop`, not `while true` +// Бесконечный цикл — используйте `loop`, а не `while true` loop { let data = get_input(); if data == "quit" { @@ -139,67 +138,66 @@ loop { } } -// loop can return a value! (unique to Rust) +// loop может вернуть значение! (уникально для Rust) let result = loop { let input = get_input(); if let Ok(num) = input.parse::() { - break num; // `break` with a value — like return for loops + break num; // `break` со значением — как return для циклов } - println!("Not a number, try again"); + println!("Не число, попробуйте ещё раз"); }; ``` -### List Comprehensions vs Iterator Chains +### Списочные включения и цепочки итераторов ```python -# Python — list comprehensions +# Python — списочные включения (list comprehensions) squares = [x ** 2 for x in range(10)] evens = [x for x in range(20) if x % 2 == 0] pairs = [(x, y) for x in range(3) for y in range(3)] ``` ```rust -// Rust — iterator chains (.map, .filter, .collect) +// Rust — цепочки итераторов (.map, .filter, .collect) let squares: Vec = (0..10).map(|x| x * x).collect(); let evens: Vec = (0..20).filter(|x| x % 2 == 0).collect(); let pairs: Vec<(i32, i32)> = (0..3) .flat_map(|x| (0..3).map(move |y| (x, y))) .collect(); -// These are LAZY — nothing runs until .collect() -// Python comprehensions are eager (run immediately) -// Rust iterators can be more efficient for large datasets +// Они ЛЕНИВЫЕ — ничего не выполняется до .collect() +// Списочные включения в Python энергичные (выполняются сразу) +// Итераторы Rust могут быть эффективнее на больших наборах данных ``` *** -## Expression Blocks +## Блоки-выражения -Everything in Rust is an expression (or can be). This is a big shift from Python, -where `if`/`for` are statements. +Всё в Rust является выражением (или может им быть). Это большой сдвиг по сравнению с Python, где `if` и `for` — это инструкции. ```python -# Python — if is a statement (except ternary) +# Python — if — это инструкция (кроме тернарного оператора) if condition: result = "yes" else: result = "no" -# Or ternary (limited to one expression): +# Или тернарный оператор (только одно выражение): result = "yes" if condition else "no" ``` ```rust -// Rust — if is an expression (returns a value) +// Rust — if — это выражение (возвращает значение) let result = if condition { "yes" } else { "no" }; -// Blocks are expressions — the last line (without semicolon) is the return value +// Блоки — это выражения: последняя строка (без точки с запятой) и есть возвращаемое значение let value = { let x = 5; let y = 10; - x + y // No semicolon → this is the value of the block (15) + x + y // Без точки с запятой → это значение блока (15) }; -// match is an expression too +// match — тоже выражение let description = match temperature { t if t > 100 => "boiling", t if t > 50 => "hot", @@ -208,72 +206,70 @@ let description = match temperature { }; ``` -The following diagram illustrates the core difference between Python's statement-based and Rust's expression-based control flow: +Диаграмма ниже иллюстрирует ключевое различие между управлением потоком в Python на основе инструкций и в Rust на основе выражений: ```mermaid flowchart LR - subgraph Python ["Python — Statements"] + subgraph Python ["Python — инструкции"] P1["if condition:"] --> P2["result = 'yes'"] P1 --> P3["result = 'no'"] - P2 --> P4["result used later"] + P2 --> P4["результат используется позже"] P3 --> P4 end Python ~~~ Rust - subgraph Rust ["Rust — Expressions"] + subgraph Rust ["Rust — выражения"] R1["let result = if cond"] --> R2["{ 'yes' }"] R1 --> R3["{ 'no' }"] - R2 --> R4["value returned directly"] + R2 --> R4["значение возвращается напрямую"] R3 --> R4 end style Python fill:#ffeeba style Rust fill:#d4edda ``` -> **The semicolon rule**: In Rust, the last expression in a block **without a semicolon** -> is the block's return value. Adding a semicolon makes it a statement (returns `()`). -> This trips up Python developers initially — it's like an implicit `return`. +> **Правило точки с запятой**: в Rust последнее выражение блока **без точки с запятой** — это возвращаемое значение блока. Добавленная точка с запятой превращает его в инструкцию (возвращается `()`). Поначалу это сбивает с толку разработчиков на Python: это похоже на неявный `return`. *** -## Functions and Type Signatures +## Функции и сигнатуры типов -### Python Functions +### Функции в Python ```python -# Python — types optional, dynamic dispatch +# Python — типы необязательны, диспетчеризация динамическая def greet(name: str, greeting: str = "Hello") -> str: return f"{greeting}, {name}!" -# Default args, *args, **kwargs +# Аргументы по умолчанию, *args, **kwargs def flexible(*args, **kwargs): pass -# First-class functions +# Функции первого класса def apply(f, x): return f(x) result = apply(lambda x: x * 2, 5) # 10 ``` -### Rust Functions +### Функции в Rust ```rust -// Rust — types REQUIRED on function signatures, no defaults +// Rust — типы в сигнатурах функций ОБЯЗАТЕЛЬНЫ, значений по умолчанию нет fn greet(name: &str, greeting: &str) -> String { format!("{}, {}!", greeting, name) } -// No default arguments — use builder pattern or Option +// Аргументов по умолчанию нет — используйте паттерн «строитель» или Option fn greet_with_default(name: &str, greeting: Option<&str>) -> String { let greeting = greeting.unwrap_or("Hello"); format!("{}, {}!", greeting, name) } -// No *args/**kwargs — use slices or structs +// Нет *args/**kwargs — используйте срезы или структуры fn sum_all(numbers: &[i32]) -> i32 { numbers.iter().sum() } -// First-class functions and closures +// Функции первого класса и замыкания fn apply(f: fn(i32) -> i32, x: i32) -> i32 { f(x) } @@ -281,29 +277,29 @@ fn apply(f: fn(i32) -> i32, x: i32) -> i32 { let result = apply(|x| x * 2, 5); // 10 ``` -### Return Values +### Возвращаемые значения ```python -# Python — return is explicit, None is implicit +# Python — return пишется явно, None подразумевается def divide(a, b): if b == 0: - return None # Or raise an exception + return None # Или выбросить исключение return a / b ``` ```rust -// Rust — last expression is the return value (no semicolon) +// Rust — последнее выражение и есть возвращаемое значение (без точки с запятой) fn divide(a: f64, b: f64) -> Option { if b == 0.0 { - None // Early return (could also write `return None;`) + None // Ранний возврат (можно также написать `return None;`) } else { - Some(a / b) // Last expression — implicit return + Some(a / b) // Последнее выражение — неявный возврат } } ``` -### Multiple Return Values +### Несколько возвращаемых значений ```python -# Python — return a tuple +# Python — возвращаем кортеж def min_max(numbers): return min(numbers), max(numbers) @@ -311,7 +307,7 @@ lo, hi = min_max([3, 1, 4, 1, 5]) ``` ```rust -// Rust — return a tuple (same concept!) +// Rust — возвращаем кортеж (та же концепция!) fn min_max(numbers: &[i32]) -> (i32, i32) { let min = *numbers.iter().min().unwrap(); let max = *numbers.iter().max().unwrap(); @@ -321,38 +317,38 @@ fn min_max(numbers: &[i32]) -> (i32, i32) { let (lo, hi) = min_max(&[3, 1, 4, 1, 5]); ``` -### Methods: self vs &self vs &mut self +### Методы: self, &self и &mut self ```rust -// In Python, `self` is always a mutable reference to the object. -// In Rust, you choose: +// В Python `self` — это всегда изменяемая ссылка на объект. +// В Rust выбираете вы: impl MyStruct { - fn new() -> Self { ... } // No self — "static method" / "classmethod" - fn read_only(&self) { ... } // &self — borrows immutably (can't modify) - fn modify(&mut self) { ... } // &mut self — borrows mutably (can modify) - fn consume(self) { ... } // self — takes ownership (object is moved) + fn new() -> Self { ... } // Без self — «статический метод» / «classmethod» + fn read_only(&self) { ... } // &self — заимствует неизменяемо (нельзя менять) + fn modify(&mut self) { ... } // &mut self — заимствует изменяемо (можно менять) + fn consume(self) { ... } // self — забирает владение (объект перемещается) } -// Python equivalent: +// Аналог в Python: // class MyStruct: // @classmethod -// def new(cls): ... # No instance needed -// def read_only(self): ... # All three are the same in Python: -// def modify(self): ... # Python self is always mutable -// def consume(self): ... # Python never "consumes" self +// def new(cls): ... # Экземпляр не нужен +// def read_only(self): ... # В Python все три варианта одинаковы: +// def modify(self): ... # Python self всегда изменяем +// def consume(self): ... # Python никогда не «потребляет» self ``` --- -## Exercises +## Упражнения
-🏋️ Exercise: FizzBuzz with Expressions (click to expand) +🏋️ Упражнение: FizzBuzz на выражениях (нажмите, чтобы раскрыть) -**Challenge**: Write FizzBuzz for 1..=30 using Rust's expression-based `match`. Each number should print "Fizz", "Buzz", "FizzBuzz", or the number. Use `match (n % 3, n % 5)` as the expression. +**Задание**: напишите FizzBuzz для 1..=30 с использованием `match` как выражения Rust. Каждое число должно выводить `"Fizz"`, `"Buzz"`, `"FizzBuzz"` или само число. Используйте `match (n % 3, n % 5)` в качестве выражения.
-🔑 Solution +🔑 Решение ```rust fn main() { @@ -368,7 +364,7 @@ fn main() { } ``` -**Key takeaway**: `match` is an expression that returns a value — no need for `if/elif/else` chains. The `_` wildcard replaces Python's `case _:` default. +**Ключевой вывод**: `match` — это выражение, которое возвращает значение, поэтому цепочки `if/elif/else` не нужны. Подстановка `_` заменяет `case _:` из Python в роли значения по умолчанию.
diff --git a/python-book/src/ch05-data-structures-and-collections.md b/python-book/src/ch05-data-structures-and-collections.md index 4efc124..7975bc1 100644 --- a/python-book/src/ch05-data-structures-and-collections.md +++ b/python-book/src/ch05-data-structures-and-collections.md @@ -1,22 +1,21 @@ -## Tuples and Destructuring +## Кортежи и деструктуризация -> **What you'll learn:** Rust tuples vs Python tuples, arrays and slices, structs (Rust's replacement for classes), -> `Vec` vs `list`, `HashMap` vs `dict`, and the newtype pattern for domain modeling. +> **Что вы узнаете:** кортежи Rust и Python, массивы и срезы, структуры (замена классам в Rust), `Vec` и `list`, `HashMap` и `dict`, а также паттерн newtype для моделирования предметной области. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Python Tuples +### Кортежи в Python ```python -# Python — tuples are immutable sequences +# Python — кортежи это неизменяемые последовательности point = (3.0, 4.0) -x, y = point # Unpacking +x, y = point # Распаковка print(f"x={x}, y={y}") -# Tuples can hold mixed types +# Кортежи могут содержать значения разных типов record = ("Alice", 30, True) name, age, active = record -# Named tuples for clarity +# Именованные кортежи для ясности from typing import NamedTuple class Point(NamedTuple): @@ -24,104 +23,104 @@ class Point(NamedTuple): y: float p = Point(3.0, 4.0) -print(p.x) # Named access +print(p.x) # Доступ по имени ``` -### Rust Tuples +### Кортежи в Rust ```rust -// Rust — tuples are fixed-size, typed, can hold mixed types +// Rust — кортежи фиксированного размера, с типами, могут содержать значения разных типов let point: (f64, f64) = (3.0, 4.0); -let (x, y) = point; // Destructuring (same as Python unpacking) +let (x, y) = point; // Деструктуризация (то же, что распаковка в Python) println!("x={x}, y={y}"); -// Mixed types +// Значения разных типов let record: (&str, i32, bool) = ("Alice", 30, true); let (name, age, active) = record; -// Access by index (unlike Python, uses .0 .1 .2 syntax) +// Доступ по индексу (в отличие от Python, используется синтаксис .0 .1 .2) let first = record.0; // "Alice" let second = record.1; // 30 // Python: record[0] -// Rust: record.0 ← dot-index, not bracket-index +// Rust: record.0 ← индекс через точку, а не квадратные скобки ``` -### When to Use Tuples vs Structs +### Когда использовать кортежи, а когда структуры ```rust -// Tuples: quick grouping, function returns, temporary values +// Кортежи: быстрая группировка, возврат из функций, временные значения fn min_max(data: &[i32]) -> (i32, i32) { (*data.iter().min().unwrap(), *data.iter().max().unwrap()) } let (lo, hi) = min_max(&[3, 1, 4, 1, 5]); -// Structs: named fields, clear intent, methods +// Структуры: именованные поля, понятный смысл, методы struct Point { x: f64, y: f64 } -// Rule of thumb: -// - 2-3 same-type fields → tuple is fine -// - Named fields needed → use struct -// - Methods needed → use struct -// (Same guidance as Python: tuple vs namedtuple vs dataclass) +// Эмпирическое правило: +// - 2–3 поля одного типа → кортеж подойдёт +// - Нужны именованные поля → используйте структуру +// - Нужны методы → используйте структуру +// (Те же рекомендации, что и в Python: tuple vs namedtuple vs dataclass) ``` *** -## Arrays and Slices +## Массивы и срезы -### Python Lists vs Rust Arrays +### Списки Python и массивы Rust ```python -# Python — lists are dynamic, heterogeneous -numbers = [1, 2, 3, 4, 5] # Can grow, shrink, hold mixed types +# Python — списки динамические, гетерогенные +numbers = [1, 2, 3, 4, 5] # Могут расти, уменьшаться, содержать значения разных типов numbers.append(6) -mixed = [1, "two", 3.0] # Mixed types allowed +mixed = [1, "two", 3.0] # Смешанные типы допустимы ``` ```rust -// Rust has TWO fixed-size vs dynamic concepts: +// В Rust есть ДВА понятия: фиксированный размер и динамический размер -// 1. Array — fixed size, stack-allocated (no Python equivalent) -let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Size is part of the type! -// numbers.push(6); // ❌ Arrays can't grow +// 1. Массив — фиксированный размер, размещается на стеке (аналога в Python нет) +let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Размер входит в тип! +// numbers.push(6); // ❌ Массивы не могут расти -// Initialize all elements to same value: +// Инициализация всех элементов одним значением: let zeros = [0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -// 2. Slice — a view into an array or Vec (like Python slicing, but borrowed) -let slice: &[i32] = &numbers[1..4]; // [2, 3, 4] — a reference, not a copy! +// 2. Срез — представление участка массива или Vec (как срезы в Python, но заимствованный) +let slice: &[i32] = &numbers[1..4]; // [2, 3, 4] — ссылка, а не копия! -// Python: numbers[1:4] creates a NEW list (copy) -// Rust: &numbers[1..4] creates a VIEW (no copy, no allocation) +// Python: numbers[1:4] создаёт НОВЫЙ список (копию) +// Rust: &numbers[1..4] создаёт ПРЕДСТАВЛЕНИЕ (без копирования и выделения памяти) ``` -### Practical Comparison +### Практическое сравнение ```python -# Python slicing — creates copies +# Срезы в Python создают копии data = [10, 20, 30, 40, 50] -first_three = data[:3] # New list: [10, 20, 30] -last_two = data[-2:] # New list: [40, 50] -reversed_data = data[::-1] # New list: [50, 40, 30, 20, 10] +first_three = data[:3] # Новый список: [10, 20, 30] +last_two = data[-2:] # Новый список: [40, 50] +reversed_data = data[::-1] # Новый список: [50, 40, 30, 20, 10] ``` ```rust -// Rust slicing — creates views (references) +// Срезы в Rust создают представления (ссылки) let data = [10, 20, 30, 40, 50]; -let first_three = &data[..3]; // &[i32], view: [10, 20, 30] -let last_two = &data[3..]; // &[i32], view: [40, 50] +let first_three = &data[..3]; // &[i32], представление: [10, 20, 30] +let last_two = &data[3..]; // &[i32], представление: [40, 50] -// No negative indexing — use .len() -let last_two = &data[data.len()-2..]; // &[i32], view: [40, 50] +// Отрицательных индексов нет — используйте .len() +let last_two = &data[data.len()-2..]; // &[i32], представление: [40, 50] -// Reverse: use an iterator +// Разворот: используйте итератор let reversed: Vec = data.iter().rev().copied().collect(); ``` *** -## Structs vs Classes +## Структуры и классы -### Python Classes +### Классы в Python ```python -# Python — class with __init__, methods, properties +# Python — класс с __init__, методами и свойствами from dataclasses import dataclass @dataclass @@ -146,9 +145,9 @@ print(r.area()) # 50.0 print(r) # Rectangle(10.0 x 5.0) ``` -### Rust Structs +### Структуры в Rust ```rust -// Rust — struct + impl blocks (no inheritance!) +// Rust — struct + блоки impl (без наследования!) #[derive(Debug, Clone)] struct Rectangle { width: f64, @@ -156,9 +155,9 @@ struct Rectangle { } impl Rectangle { - // "Constructor" — associated function (no self) + // «Конструктор» — ассоциированная функция (без self) fn new(width: f64, height: f64) -> Self { - Rectangle { width, height } // Field shorthand when names match + Rectangle { width, height } // Сокращённая запись полей, когда имена совпадают } fn area(&self) -> f64 { @@ -174,7 +173,7 @@ impl Rectangle { } } -// Display trait = Python's __str__ +// Трейт Display — аналог __str__ в Python impl std::fmt::Display for Rectangle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Rectangle({} x {})", self.width, self.height) @@ -190,43 +189,43 @@ fn main() { ```mermaid flowchart LR - subgraph Python ["Python Object (Heap)"] - PH["PyObject Header
(refcount + type ptr)"] --> PW["width: float obj"] - PH --> PHT["height: float obj"] + subgraph Python ["Объект Python (куча)"] + PH["Заголовок PyObject
(refcount + указатель на тип)"] --> PW["width: объект float"] + PH --> PHT["height: объект float"] PH --> PD["__dict__"] end Python ~~~ Rust - subgraph Rust ["Rust Struct (Stack)"] - RW["width: f64
(8 bytes)"] --- RH["height: f64
(8 bytes)"] + subgraph Rust ["Структура Rust (стек)"] + RW["width: f64
(8 байт)"] --- RH["height: f64
(8 байт)"] end style Python fill:#ffeeba style Rust fill:#d4edda ``` -> **Memory insight**: A Python `Rectangle` object has a 56-byte header + separate heap-allocated float objects. A Rust `Rectangle` is exactly 16 bytes on the stack — no indirection, no GC pressure. +> **Наблюдение о памяти**: объект Python `Rectangle` имеет 56-байтовый заголовок плюс отдельные объекты float в куче. Структура Rust `Rectangle` занимает ровно 16 байт на стеке: без косвенных обращений и без нагрузки на GC. > -> 📌 **See also**: [Ch. 10 — Traits and Generics](ch10-traits-and-generics.md) covers implementing traits like `Display`, `Debug`, and operator overloading for your structs. - -### Key Mapping: Python Dunder Methods → Rust Traits - -| Python | Rust | Purpose | -|--------|------|---------| -| `__str__` | `impl Display` | Human-readable string | -| `__repr__` | `#[derive(Debug)]` | Debug representation | -| `__eq__` | `#[derive(PartialEq)]` | Equality comparison | -| `__hash__` | `#[derive(Hash)]` | Hashable (for dict keys / HashSet) | -| `__lt__`, `__le__`, etc. | `#[derive(PartialOrd, Ord)]` | Ordering | -| `__add__` | `impl Add` | `+` operator | -| `__iter__` | `impl Iterator` | Iteration | -| `__len__` | `.len()` method | Length | -| `__enter__`/`__exit__` | RAII + `impl Drop` | Automatic cleanup; no direct equivalent of context manager's two-phase protocol | -| `__init__` | `fn new()` (convention) | Constructor | -| `__getitem__` | `impl Index` | Indexing with `[]` | -| `__contains__` | `.contains()` method | `in` operator | - -### No Inheritance — Composition Instead +> 📌 **См. также**: [Гл. 10 — Трейты и обобщения](ch10-traits-and-generics.md) рассказывает о реализации трейтов вроде `Display`, `Debug` и перегрузки операторов для ваших структур. + +### Соответствие: магические методы Python → трейты Rust + +| Python | Rust | Назначение | +|--------|------|------------| +| `__str__` | `impl Display` | Человекочитаемая строка | +| `__repr__` | `#[derive(Debug)]` | Отладочное представление | +| `__eq__` | `#[derive(PartialEq)]` | Сравнение на равенство | +| `__hash__` | `#[derive(Hash)]` | Хешируемость (для ключей dict и HashSet) | +| `__lt__`, `__le__` и т. д. | `#[derive(PartialOrd, Ord)]` | Упорядочивание | +| `__add__` | `impl Add` | Оператор `+` | +| `__iter__` | `impl Iterator` | Итерация | +| `__len__` | метод `.len()` | Длина | +| `__enter__`/`__exit__` | RAII + `impl Drop` | Автоматическая очистка; прямого аналога двухфазного протокола менеджера контекста нет | +| `__init__` | `fn new()` (соглашение) | Конструктор | +| `__getitem__` | `impl Index` | Индексация через `[]` | +| `__contains__` | метод `.contains()` | Оператор `in` | + +### Нет наследования: вместо него композиция ```python -# Python — inheritance +# Python — наследование class Animal: def __init__(self, name: str): self.name = name @@ -235,15 +234,15 @@ class Animal: class Dog(Animal): def speak(self) -> str: - return f"{self.name} says Woof!" + return f"{self.name} говорит: Гав!" class Cat(Animal): def speak(self) -> str: - return f"{self.name} says Meow!" + return f"{self.name} говорит: Мяу!" ``` ```rust -// Rust — traits + composition (no inheritance) +// Rust — трейты и композиция (без наследования) trait Animal { fn name(&self) -> &str; fn speak(&self) -> String; @@ -255,18 +254,18 @@ struct Cat { name: String } impl Animal for Dog { fn name(&self) -> &str { &self.name } fn speak(&self) -> String { - format!("{} says Woof!", self.name) + format!("{} говорит: Гав!", self.name) } } impl Animal for Cat { fn name(&self) -> &str { &self.name } fn speak(&self) -> String { - format!("{} says Meow!", self.name) + format!("{} говорит: Мяу!", self.name) } } -// Use trait objects for polymorphism (like Python's duck typing): +// Для полиморфизма используйте трейт-объекты (как утиную типизацию в Python): fn animal_roll_call(animals: &[&dyn Animal]) { for a in animals { println!("{}", a.speak()); @@ -274,16 +273,15 @@ fn animal_roll_call(animals: &[&dyn Animal]) { } ``` -> **Mental model**: Python says "inherit behavior". Rust says "implement contracts". -> The result is similar, but Rust avoids the diamond problem and fragile base class issues. +> **Ментальная модель**: Python говорит «унаследуй поведение». Rust говорит «реализуй контракт». Результат похож, но Rust избегает проблемы ромбовидного наследования и хрупкости базовых классов. *** -## Vec vs list +## Vec и list -`Vec` is Rust's growable, heap-allocated array — the closest equivalent to Python's `list`. +`Vec` — это растущий массив Rust в куче, ближайший аналог `list` в Python. -### Creating Vectors +### Создание векторов ```python # Python numbers = [1, 2, 3] @@ -294,69 +292,69 @@ from_range = list(range(1, 6)) ```rust // Rust -let numbers = vec![1, 2, 3]; // vec! macro (like a list literal) -let empty: Vec = Vec::new(); // Empty vec (type annotation needed) +let numbers = vec![1, 2, 3]; // макрос vec! (аналог литерала списка) +let empty: Vec = Vec::new(); // Пустой vec (нужна аннотация типа) let repeated = vec![0; 10]; // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] let from_range: Vec = (1..6).collect(); // [1, 2, 3, 4, 5] ``` -### Common Operations +### Частые операции ```python -# Python list operations +# Операции со списками Python nums = [1, 2, 3] nums.append(4) # [1, 2, 3, 4] nums.extend([5, 6]) # [1, 2, 3, 4, 5, 6] nums.insert(0, 0) # [0, 1, 2, 3, 4, 5, 6] last = nums.pop() # 6, nums = [0, 1, 2, 3, 4, 5] length = len(nums) # 6 -nums.sort() # In-place sort -sorted_copy = sorted(nums) # New sorted list -nums.reverse() # In-place reverse +nums.sort() # Сортировка на месте +sorted_copy = sorted(nums) # Новый отсортированный список +nums.reverse() # Разворот на месте contains = 3 in nums # True -index = nums.index(3) # Index of first 3 +index = nums.index(3) # Индекс первой тройки ``` ```rust -// Rust Vec operations +// Операции с Vec в Rust let mut nums = vec![1, 2, 3]; nums.push(4); // [1, 2, 3, 4] nums.extend([5, 6]); // [1, 2, 3, 4, 5, 6] nums.insert(0, 0); // [0, 1, 2, 3, 4, 5, 6] let last = nums.pop(); // Some(6), nums = [0, 1, 2, 3, 4, 5] let length = nums.len(); // 6 -nums.sort(); // In-place sort +nums.sort(); // Сортировка на месте let mut sorted_copy = nums.clone(); -sorted_copy.sort(); // Sort a clone -nums.reverse(); // In-place reverse +sorted_copy.sort(); // Сортируем копию +nums.reverse(); // Разворот на месте let contains = nums.contains(&3); // true -let index = nums.iter().position(|&x| x == 3); // Some(index) or None +let index = nums.iter().position(|&x| x == 3); // Some(index) или None ``` -### Quick Reference +### Краткая справка -| Python | Rust | Notes | -|--------|------|-------| +| Python | Rust | Примечания | +|--------|------|------------| | `lst.append(x)` | `vec.push(x)` | | | `lst.extend(other)` | `vec.extend(other)` | | -| `lst.pop()` | `vec.pop()` | Returns `Option` | +| `lst.pop()` | `vec.pop()` | Возвращает `Option` | | `lst.insert(i, x)` | `vec.insert(i, x)` | | -| `lst.remove(x)` | `vec.iter().position(\|v\| v == &x).map(\|i\| vec.remove(i))` | Removes first match only (use `retain` to remove all) | -| `del lst[i]` | `vec.remove(i)` | Returns the removed element | +| `lst.remove(x)` | `vec.iter().position(\|v\| v == &x).map(\|i\| vec.remove(i))` | Удаляет только первое совпадение (чтобы удалить все, используйте `retain`) | +| `del lst[i]` | `vec.remove(i)` | Возвращает удалённый элемент | | `len(lst)` | `vec.len()` | | | `x in lst` | `vec.contains(&x)` | | | `lst.sort()` | `vec.sort()` | | -| `sorted(lst)` | Clone + sort, or iterator | | -| `lst[i]` | `vec[i]` | Panics if out of bounds | -| `lst.get(i, default)` | `vec.get(i)` | Returns `Option<&T>` | -| `lst[1:3]` | `&vec[1..3]` | Returns a slice (no copy) | +| `sorted(lst)` | Клонирование + sort или итератор | | +| `lst[i]` | `vec[i]` | Паника, если индекс вне границ | +| `lst.get(i, default)` | `vec.get(i)` | Возвращает `Option<&T>` | +| `lst[1:3]` | `&vec[1..3]` | Возвращает срез (без копирования) | *** -## HashMap vs dict +## HashMap и dict -`HashMap` is Rust's hash map — equivalent to Python's `dict`. +`HashMap` — хеш-таблица Rust, аналог `dict` в Python. -### Creating HashMaps +### Создание HashMap ```python # Python scores = {"Alice": 100, "Bob": 85} @@ -375,14 +373,14 @@ let from_pairs: HashMap<&str, i32> = [("x", 1), ("y", 2)].into_iter().collect(); let comprehension: HashMap<_, _> = keys.iter().zip(values.iter()).collect(); ``` -### Common Operations +### Частые операции ```python -# Python dict operations +# Операции со словарём Python d = {"a": 1, "b": 2} -d["c"] = 3 # Insert -val = d["a"] # 1 (KeyError if missing) -val = d.get("z", 0) # 0 (default if missing) -del d["b"] # Remove +d["c"] = 3 # Вставка +val = d["a"] # 1 (KeyError, если ключа нет) +val = d.get("z", 0) # 0 (значение по умолчанию, если ключа нет) +del d["b"] # Удаление exists = "a" in d # True keys = list(d.keys()) # ["a", "c"] values = list(d.values()) # [1, 3] @@ -397,42 +395,42 @@ for word in words: ``` ```rust -// Rust HashMap operations +// Операции с HashMap в Rust use std::collections::HashMap; let mut d = HashMap::new(); d.insert("a", 1); d.insert("b", 2); -d.insert("c", 3); // Insert or overwrite +d.insert("c", 3); // Вставка или перезапись -let val = d["a"]; // 1 (panics if missing) -let val = d.get("z").copied().unwrap_or(0); // 0 (safe access) -d.remove("b"); // Remove +let val = d["a"]; // 1 (паника, если ключа нет) +let val = d.get("z").copied().unwrap_or(0); // 0 (безопасный доступ) +d.remove("b"); // Удаление let exists = d.contains_key("a"); // true let keys: Vec<_> = d.keys().collect(); let values: Vec<_> = d.values().collect(); let length = d.len(); -// entry API = Python's setdefault / defaultdict pattern +// Entry API — аналог паттерна setdefault / defaultdict в Python let mut word_count: HashMap<&str, i32> = HashMap::new(); for word in words { *word_count.entry(word).or_insert(0) += 1; } ``` -### Quick Reference +### Краткая справка -| Python | Rust | Notes | -|--------|------|-------| -| `d[key] = val` | `d.insert(key, val)` | Returns `Option` (old value) | -| `d[key]` | `d[&key]` | Panics if missing | -| `d.get(key)` | `d.get(&key)` | Returns `Option<&V>` | +| Python | Rust | Примечания | +|--------|------|------------| +| `d[key] = val` | `d.insert(key, val)` | Возвращает `Option` (старое значение) | +| `d[key]` | `d[&key]` | Паника, если ключа нет | +| `d.get(key)` | `d.get(&key)` | Возвращает `Option<&V>` | | `d.get(key, default)` | `d.get(&key).unwrap_or(&default)` | | | `key in d` | `d.contains_key(&key)` | | -| `del d[key]` | `d.remove(&key)` | Returns `Option` | -| `d.keys()` | `d.keys()` | Iterator | -| `d.values()` | `d.values()` | Iterator | -| `d.items()` | `d.iter()` | Iterator of `(&K, &V)` | +| `del d[key]` | `d.remove(&key)` | Возвращает `Option` | +| `d.keys()` | `d.keys()` | Итератор | +| `d.values()` | `d.values()` | Итератор | +| `d.items()` | `d.iter()` | Итератор по `(&K, &V)` | | `len(d)` | `d.len()` | | | `d.update(other)` | `d.extend(other)` | | | `defaultdict(int)` | `.entry().or_insert(0)` | Entry API | @@ -440,27 +438,27 @@ for word in words { *** -### Other Collections +### Другие коллекции -| Python | Rust | Notes | -|--------|------|-------| +| Python | Rust | Примечания | +|--------|------|------------| | `set()` | `HashSet` | `use std::collections::HashSet;` | | `collections.deque` | `VecDeque` | `use std::collections::VecDeque;` | -| `heapq` | `BinaryHeap` | Max-heap by default | -| `collections.OrderedDict` | `IndexMap` (crate) | HashMap doesn't preserve order | -| `sortedcontainers.SortedList` | `BTreeSet` / `BTreeMap` | Tree-based, sorted | +| `heapq` | `BinaryHeap` | Max-куча по умолчанию | +| `collections.OrderedDict` | `IndexMap` (крейт) | HashMap не сохраняет порядок | +| `sortedcontainers.SortedList` | `BTreeSet` / `BTreeMap` | На основе дерева, отсортировано | --- -## Exercises +## Упражнения
-🏋️ Exercise: Word Frequency Counter (click to expand) +🏋️ Упражнение: счётчик частоты слов (нажмите, чтобы раскрыть) -**Challenge**: Write a function that takes a `&str` sentence and returns a `HashMap` of word frequencies (case-insensitive). In Python this is `Counter(s.lower().split())`. Translate it to Rust. +**Задание**: напишите функцию, которая принимает предложение `&str` и возвращает `HashMap` с частотами слов (без учёта регистра). В Python это `Counter(s.lower().split())`. Перепишите её на Rust.
-🔑 Solution +🔑 Решение ```rust use std::collections::HashMap; @@ -483,11 +481,10 @@ fn main() { } ``` -**Key takeaway**: `HashMap::entry().or_insert()` is Rust's equivalent of Python's `defaultdict` or `Counter`. The `*` dereference is needed because `or_insert` returns `&mut usize`. +**Ключевой вывод**: `HashMap::entry().or_insert()` — аналог `defaultdict` или `Counter` в Python. Разыменование `*` нужно, потому что `or_insert` возвращает `&mut usize`.
*** - diff --git a/python-book/src/ch06-enums-and-pattern-matching.md b/python-book/src/ch06-enums-and-pattern-matching.md index dda246a..354e50f 100644 --- a/python-book/src/ch06-enums-and-pattern-matching.md +++ b/python-book/src/ch06-enums-and-pattern-matching.md @@ -1,16 +1,14 @@ -## Algebraic Data Types vs Union Types +## Алгебраические типы данных и объединения типов -> **What you'll learn:** Rust enums with data vs Python `Union` types, exhaustive `match` vs `match/case`, -> `Option` as a compile-time replacement for `None`, and guard patterns. +> **Что вы узнаете:** перечисления Rust с данными в сравнении с типами `Union` в Python, исчерпывающий `match` в сравнении с `match/case`, `Option` как замену `None` на этапе компиляции и защитные условия (guards). > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -Python 3.10 introduced `match` statements and type unions. Rust's enums go further — -each variant can carry different data, and the compiler ensures you handle every case. +Python 3.10 ввёл оператор `match` и объединения типов. Перечисления Rust идут дальше: каждый вариант может хранить собственные данные, а компилятор следит, чтобы обрабатывался каждый случай. -### Python Union Types and Match +### Объединения типов и match в Python ```python -# Python 3.10+ — structural pattern matching +# Python 3.10+ — структурное сопоставление с образцом from typing import Union from dataclasses import dataclass @@ -28,7 +26,7 @@ class Triangle: base: float height: float -Shape = Union[Circle, Rectangle, Triangle] # Type alias +Shape = Union[Circle, Rectangle, Triangle] # Псевдоним типа def area(shape: Shape) -> float: match shape: @@ -38,17 +36,17 @@ def area(shape: Shape) -> float: return w * h case Triangle(base=b, height=h): return 0.5 * b * h - # No compiler warning if you miss a case! - # Adding a new shape? grep the codebase and hope you find all match blocks. + # Компилятор не предупредит, если вы что-то пропустите! + # Добавляете новую фигуру? Остаётся надеяться, что вы нашли все блоки match через grep. ``` -### Rust Enums — Data-Carrying Variants +### Перечисления Rust с вариантами, которые хранят данные ```rust -// Rust — enum variants carry data, compiler enforces exhaustive matching +// Rust — варианты перечисления хранят данные, компилятор требует исчерпывающего сопоставления enum Shape { - Circle(f64), // Circle carries radius - Rectangle(f64, f64), // Rectangle carries width, height - Triangle { base: f64, height: f64 }, // Named fields also work + Circle(f64), // Круг хранит радиус + Rectangle(f64, f64), // Прямоугольник хранит ширину и высоту + Triangle { base: f64, height: f64 }, // Можно использовать и именованные поля } fn area(shape: &Shape) -> f64 { @@ -56,34 +54,32 @@ fn area(shape: &Shape) -> f64 { Shape::Circle(r) => std::f64::consts::PI * r * r, Shape::Rectangle(w, h) => w * h, Shape::Triangle { base, height } => 0.5 * base * height, - // ❌ If you add Shape::Pentagon and forget to handle it here, - // the compiler refuses to build. No grep needed. + // ❌ Если вы добавите Shape::Pentagon и забудете обработать его здесь, + // компилятор откажется собирать программу. grep не нужен. } } ``` -> **Key insight**: Rust's `match` is **exhaustive** — the compiler verifies you handle -> every variant. Add a new variant to an enum and the compiler tells you exactly which -> `match` blocks need updating. Python's `match` has no such guarantee. +> **Ключевая мысль**: `match` в Rust — **исчерпывающий**: компилятор проверяет, что вы обработали каждый вариант. Добавьте новый вариант в перечисление, и компилятор точно укажет, в каких блоках `match` его нужно учесть. У `match` в Python такой гарантии нет. -### Enums Replace Multiple Python Patterns +### Перечисления заменяют несколько паттернов Python ```python -# Python — several patterns that Rust enums replace: +# Python — несколько паттернов, которые заменяют перечисления Rust: -# 1. String constants +# 1. Строковые константы STATUS_PENDING = "pending" STATUS_ACTIVE = "active" STATUS_CLOSED = "closed" -# 2. Python Enum (no data) +# 2. Перечисление Python (без данных) from enum import Enum class Status(Enum): PENDING = "pending" ACTIVE = "active" CLOSED = "closed" -# 3. Tagged unions (class + type field) +# 3. Теговые объединения (класс + поле типа) class Message: def __init__(self, kind, **data): self.kind = kind @@ -93,30 +89,30 @@ class Message: ``` ```rust -// Rust — one enum does all three and more +// Rust — одно перечисление заменяет все три и даже больше -// 1. Simple enum (like Python's Enum) +// 1. Простое перечисление (как Enum в Python) enum Status { Pending, Active, Closed, } -// 2. Data-carrying enum (tagged union — type-safe!) +// 2. Перечисление с данными (теговое объединение, с проверкой типов!) enum Message { Text(String), Image { url: String, width: u32, height: u32 }, - Quit, // No data + Quit, // Без данных Move { x: i32, y: i32 }, } ``` ```mermaid flowchart TD - E["enum Message"] --> T["Text(String)
🏷️ tag=0 + String data"] - E --> I["Image { url, width, height }
🏷️ tag=1 + 3 fields"] - E --> Q["Quit
🏷️ tag=2 + no data"] - E --> M["Move { x, y }
🏷️ tag=3 + 2 fields"] + E["enum Message"] --> T["Text(String)
🏷️ tag=0 + данные String"] + E --> I["Image { url, width, height }
🏷️ tag=1 + 3 поля"] + E --> Q["Quit
🏷️ tag=2 + без данных"] + E --> M["Move { x, y }
🏷️ tag=3 + 2 поля"] style E fill:#d4edda,stroke:#28a745 style T fill:#fff3cd style I fill:#fff3cd @@ -124,56 +120,56 @@ flowchart TD style M fill:#fff3cd ``` -> **Memory insight**: Rust enums are "tagged unions" — the compiler stores a discriminant tag + enough space for the largest variant. Python's equivalent (`Union[str, dict, None]`) has no compact representation. +> **Наблюдение о памяти**: перечисления Rust — это «помеченные объединения» (tagged unions): компилятор хранит дискриминант-тег и место под самый большой вариант. У Python-аналога (`Union[str, dict, None]`) компактного представления нет. > -> 📌 **See also**: [Ch. 9 — Error Handling](ch09-error-handling.md) uses enums extensively — `Result` and `Option` are just enums with `match`. +> 📌 **См. также**: [Гл. 9 — Обработка ошибок](ch09-error-handling.md) широко использует перечисления: `Result` и `Option` — это просто перечисления и `match`. ```rust fn process(msg: &Message) { match msg { - Message::Text(content) => println!("Text: {content}"), + Message::Text(content) => println!("Текст: {content}"), Message::Image { url, width, height } => { - println!("Image: {url} ({width}x{height})") + println!("Изображение: {url} ({width}x{height})") } - Message::Quit => println!("Quitting"), - Message::Move { x, y } => println!("Moving to ({x}, {y})"), + Message::Quit => println!("Выход"), + Message::Move { x, y } => println!("Перемещение в ({x}, {y})"), } } ``` *** -## Exhaustive Pattern Matching +## Исчерпывающее сопоставление с образцом -### Python's match — Not Exhaustive +### match в Python — не исчерпывающий ```python -# Python — the wildcard case is optional, no compiler help +# Python — случай-«заглушка» необязателен, помощи компилятора нет def describe(value): match value: case 0: return "zero" case 1: return "one" - # If you forget the default, Python returns None silently. - # No warning, no error. + # Если вы забудете default, Python молча вернёт None. + # Ни предупреждения, ни ошибки. -describe(42) # Returns None — a silent bug +describe(42) # Возвращает None — молчаливая ошибка ``` -### Rust's match — Compiler-Enforced +### match в Rust — проверяется компилятором ```rust -// Rust — MUST handle every possible case +// Rust — ОБЯЗАТЕЛЬНО обработать каждый возможный случай fn describe(value: i32) -> &'static str { match value { 0 => "zero", 1 => "one", - // ❌ Compile error: non-exhaustive patterns: `i32::MIN..=-1_i32` - // and `2_i32..=i32::MAX` not covered - _ => "other", // _ = catch-all (required for open-ended types) + // ❌ Ошибка компиляции: паттерны не исчерпывающие: `i32::MIN..=-1_i32` + // и `2_i32..=i32::MAX` не покрыты + _ => "other", // _ = «ловушка» для всех остальных значений (обязательна для типов с открытым множеством значений) } } -// For enums, NO catch-all needed — compiler knows all variants: +// Для перечислений «ловушка» НЕ нужна — компилятор знает все варианты: enum Color { Red, Green, Blue } fn color_hex(c: Color) -> &'static str { @@ -181,32 +177,32 @@ fn color_hex(c: Color) -> &'static str { Color::Red => "#ff0000", Color::Green => "#00ff00", Color::Blue => "#0000ff", - // No _ needed — all variants covered - // Add Color::Yellow later → compiler error HERE + // _ не нужен — все варианты покрыты + // Добавите Color::Yellow позже → ошибка компиляции ЗДЕСЬ } } ``` -### Pattern Matching Features +### Возможности сопоставления с образцом ```rust -// Multiple values (like Python's case 1 | 2 | 3:) +// Несколько значений (как case 1 | 2 | 3: в Python) match value { - 1 | 2 | 3 => println!("small"), - 4..=9 => println!("medium"), // Range patterns - _ => println!("large"), + 1 | 2 | 3 => println!("маленькое"), + 4..=9 => println!("среднее"), // Диапазоны + _ => println!("большое"), } -// Guards (like Python's case x if x > 0:) +// Защитные условия (guards, как case x if x > 0: в Python) match temperature { - t if t > 100 => println!("boiling"), - t if t < 0 => println!("freezing"), - t => println!("normal: {t}°"), + t if t > 100 => println!("кипение"), + t if t < 0 => println!("ниже нуля"), + t => println!("норма: {t}°"), } -// Nested destructuring +// Вложенная деструктуризация let point = (3, (4, 5)); match point { - (0, _) => println!("on y-axis"), + (0, _) => println!("на оси Y"), (_, (0, _)) => println!("y=0"), (x, (y, z)) => println!("x={x}, y={y}, z={z}"), } @@ -214,82 +210,81 @@ match point { *** -## Option for None Safety +## Option для безопасной работы с None -`Option` is the most important Rust enum for Python developers. It replaces -`None` with a type-safe alternative. +`Option` — самое важное перечисление Rust для разработчиков на Python. Оно заменяет `None` безопасной по типам альтернативой. -### Python None +### None в Python ```python -# Python — None is a value that can appear anywhere +# Python — None — это значение, которое может появиться где угодно def find_user(user_id: int) -> dict | None: users = {1: {"name": "Alice"}} return users.get(user_id) user = find_user(999) -# user is None — but nothing forces you to check! -print(user["name"]) # 💥 TypeError at runtime +# user — None, но ничто не заставляет вас проверить это! +print(user["name"]) # 💥 TypeError во время выполнения ``` -### Rust Option +### Option в Rust ```rust -// Rust — Option forces you to handle the None case +// Rust — Option заставляет обработать случай None fn find_user(user_id: i64) -> Option { let users = HashMap::from([(1, User { name: "Alice".into() })]); users.get(&user_id).cloned() } let user = find_user(999); -// user is Option — you CANNOT use it without handling None +// user имеет тип Option — НЕЛЬЗЯ использовать его, не обработав None -// Method 1: match +// Способ 1: match match find_user(999) { - Some(user) => println!("Found: {}", user.name), - None => println!("Not found"), + Some(user) => println!("Найден: {}", user.name), + None => println!("Не найден"), } -// Method 2: if let (like Python's if (x := expr) is not None) +// Способ 2: if let (как if (x := expr) is not None в Python) if let Some(user) = find_user(1) { - println!("Found: {}", user.name); + println!("Найден: {}", user.name); } -// Method 3: unwrap_or +// Способ 3: unwrap_or let name = find_user(999) .map(|u| u.name) .unwrap_or_else(|| "Unknown".to_string()); -// Method 4: ? operator (in functions that return Option) +// Способ 4: оператор ? (в функциях, которые возвращают Option) fn get_user_name(id: i64) -> Option { - let user = find_user(id)?; // Returns None early if not found + let user = find_user(id)?; // Досрочно возвращает None, если пользователь не найден Some(user.name) } ``` -### Option Methods — Python Equivalents +### Методы Option: аналоги в Python -| Pattern | Python | Rust | -|---------|--------|------| -| Check if exists | `if x is not None:` | `if let Some(x) = opt {` | -| Default value | `x or default` | `opt.unwrap_or(default)` | -| Default factory | `x or compute()` | `opt.unwrap_or_else(\|\| compute())` | -| Transform if exists | `f(x) if x else None` | `opt.map(f)` | -| Chain lookups | `x and x.attr and x.attr.method()` | `opt.and_then(\|x\| x.method())` | -| Crash if None | Not possible to prevent | `opt.unwrap()` (panic) or `opt.expect("msg")` | -| Get or raise | `x if x else raise` | `opt.ok_or(Error)?` | +| Задача | Python | Rust | +|--------|--------|------| +| Проверка наличия | `if x is not None:` | `if let Some(x) = opt {` | +| Значение по умолчанию | `x or default` | `opt.unwrap_or(default)` | +| Фабрика значения по умолчанию | `x or compute()` | `opt.unwrap_or_else(\|\| compute())` | +| Преобразование, если значение есть | `f(x) if x else None` | `opt.map(f)` | +| Цепочка обращений | `x and x.attr and x.attr.method()` | `opt.and_then(\|x\| x.method())` | +| Падение при None | Нельзя предотвратить | `opt.unwrap()` (паника) или `opt.expect("msg")` | +| Получить значение или выбросить ошибку | `x if x else raise` | `opt.ok_or(Error)?` | --- -## Exercises +## Упражнения
-🏋️ Exercise: Shape Area Calculator (click to expand) +🏋️ Упражнение: калькулятор площади фигур (нажмите, чтобы раскрыть) -**Challenge**: Define an enum `Shape` with variants `Circle(f64)` (radius), `Rectangle(f64, f64)` (width, height), and `Triangle(f64, f64)` (base, height). Implement a method `fn area(&self) -> f64` using `match`. Create one of each and print the area. +**Задание**: определите перечисление `Shape` с вариантами `Circle(f64)` (радиус), `Rectangle(f64, f64)` (ширина, высота) и `Triangle(f64, f64)` (основание, высота). Реализуйте метод `fn area(&self) -> f64` с использованием `match`. Создайте по одной фигуре каждого вида и выведите их площади.
-🔑 Solution +🔑 Решение ```rust use std::f64::consts::PI; @@ -317,16 +312,15 @@ fn main() { Shape::Triangle(3.0, 8.0), ]; for shape in &shapes { - println!("Area: {:.2}", shape.area()); + println!("Площадь: {:.2}", shape.area()); } } ``` -**Key takeaway**: Rust enums replace Python's `Union[Circle, Rectangle, Triangle]` + `isinstance()` checks. The compiler ensures you handle every variant — adding a new shape without updating `area()` is a compile error. +**Ключевой вывод**: перечисления Rust заменяют `Union[Circle, Rectangle, Triangle]` и проверки `isinstance()` в Python. Компилятор следит, чтобы обрабатывался каждый вариант: добавление новой фигуры без обновления `area()` — ошибка компиляции.
*** - diff --git a/python-book/src/ch07-ownership-and-borrowing.md b/python-book/src/ch07-ownership-and-borrowing.md index 92bca68..802ee44 100644 --- a/python-book/src/ch07-ownership-and-borrowing.md +++ b/python-book/src/ch07-ownership-and-borrowing.md @@ -1,156 +1,152 @@ -## Understanding Ownership +## Понимание владения -> **What you'll learn:** Why Rust has ownership (no GC!), move semantics vs Python's reference counting, -> borrowing (`&` and `&mut`), lifetime basics, and smart pointers (`Box`, `Rc`, `Arc`). +> **Что вы узнаете:** зачем в Rust нужно владение (без GC!), семантику перемещения в сравнении с подсчётом ссылок в Python, заимствование (`&` и `&mut`), основы времён жизни и умные указатели (`Box`, `Rc`, `Arc`). > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -This is the hardest concept for Python developers. In Python, you never think about -who "owns" data — the garbage collector handles it. In Rust, every value has exactly -one owner, and the compiler tracks this at compile time. +Это самая трудная концепция для разработчиков на Python. В Python вы никогда не задумываетесь о том, кто «владеет» данными: этим занимается сборщик мусора. В Rust у каждого значения ровно один владелец, и компилятор отслеживает это на этапе компиляции. -### Python: Shared References Everywhere +### Python: повсюду разделяемые ссылки ```python -# Python — everything is a reference, gc cleans up +# Python — всё является ссылкой, сборщик мусора всё очищает a = [1, 2, 3] -b = a # b and a point to the SAME list +b = a # b и a указывают на ОДИН И ТОТ ЖЕ список b.append(4) -print(a) # [1, 2, 3, 4] — surprise! a changed too +print(a) # [1, 2, 3, 4] — сюрприз! a тоже изменился -# Who owns the list? Both a and b reference it. -# The garbage collector frees it when no references remain. -# You never think about this. +# Кто владеет списком? Обе переменные ссылаются на него. +# Сборщик мусора освободит его, когда не останется ссылок. +# Об этом вы никогда не задумываетесь. ``` -### Rust: Single Ownership +### Rust: единоличное владение ```rust -// Rust — every value has exactly ONE owner +// Rust — у каждого значения ровно ОДИН владелец let a = vec![1, 2, 3]; -let b = a; // Ownership MOVES from a to b -// println!("{:?}", a); // ❌ Compile error: value used after move +let b = a; // Владение ПЕРЕМЕЩАЕТСЯ из a в b +// println!("{:?}", a); // ❌ Ошибка компиляции: значение использовано после перемещения -// a no longer exists. b is the sole owner. +// a больше не существует. b — единственный владелец. println!("{:?}", b); // ✅ [1, 2, 3] -// When b goes out of scope, the Vec is freed. Deterministic. No GC. +// Когда b выходит из области видимости, Vec освобождается. Детерминированно. Без GC. ``` -### The Three Ownership Rules +### Три правила владения ```rust -1. Each value has exactly ONE owner variable. -2. When the owner goes out of scope, the value is dropped (freed). -3. Ownership can be transferred (moved) but not duplicated (unless Clone). +1. У каждого значения ровно ОДНА переменная-владелец. +2. Когда владелец выходит из области видимости, значение уничтожается (освобождается). +3. Владение можно передать (переместить), но нельзя продублировать (если тип не реализует Clone). ``` -### Move Semantics — The Biggest Python Shock +### Семантика перемещения: самый большой шок для Python ```python -# Python — assignment copies the reference, not the data +# Python — присваивание копирует ссылку, а не данные def process(data): data.append(42) - # Original list is modified! + # Исходный список изменён! my_list = [1, 2, 3] process(my_list) -print(my_list) # [1, 2, 3, 42] — modified by process! +print(my_list) # [1, 2, 3, 42] — изменён функцией process! ``` ```rust -// Rust — passing to a function MOVES ownership (for non-Copy types) +// Rust — передача в функцию ПЕРЕМЕЩАЕТ владение (для типов, не реализующих Copy) fn process(mut data: Vec) -> Vec { data.push(42); - data // Must return it to give ownership back! + data // Нужно вернуть его, чтобы вернуть владение! } let my_vec = vec![1, 2, 3]; -let my_vec = process(my_vec); // Ownership moves in and back out +let my_vec = process(my_vec); // Владение уходит внутрь и возвращается обратно println!("{:?}", my_vec); // [1, 2, 3, 42] -// Or better — borrow instead of moving: +// Или лучше — заимствовать вместо перемещения: fn process_borrowed(data: &mut Vec) { data.push(42); } let mut my_vec = vec![1, 2, 3]; -process_borrowed(&mut my_vec); // Lend it temporarily -println!("{:?}", my_vec); // [1, 2, 3, 42] — still ours +process_borrowed(&mut my_vec); // Временно одалживаем +println!("{:?}", my_vec); // [1, 2, 3, 42] — по-прежнему наш ``` -### Ownership Visualized - +### Владение в наглядном виде ```text Python: Rust: a ──────┐ a ──→ [1, 2, 3] ├──→ [1, 2, 3] - b ──────┘ After: let b = a; + b ──────┘ После: let b = a; - (a and b share one object) a (invalid, moved) + (a и b делят один объект) a (недействительна, перемещена) (refcount = 2) b ──→ [1, 2, 3] - (only b owns the data) + (владеет данными только b) - del a → refcount = 1 drop(b) → data freed - del b → refcount = 0 → freed (deterministic, no GC) + del a → refcount = 1 drop(b) → данные освобождены + del b → refcount = 0 → освобождено (детерминированно, без GC) ``` ```mermaid stateDiagram-v2 - state "Python (Reference Counting)" as PY { + state "Python (подсчёт ссылок)" as PY { [*] --> a_owns: a = [1,2,3] a_owns --> shared: b = a shared --> b_only: del a (refcount 2→1) b_only --> freed: del b (refcount 1→0) - note right of shared: Both a and b point
to the SAME object + note right of shared: и a, и b указывают
на ОДИН И ТОТ ЖЕ объект } - state "Rust (Ownership Move)" as RS { + state "Rust (перемещение владения)" as RS { [*] --> a_owns2: let a = vec![1,2,3] a_owns2 --> b_owns: let b = a (MOVE) - b_owns --> freed2: b goes out of scope - note right of b_owns: a is INVALID after move
Compile error if used + b_owns --> freed2: b выходит из области видимости + note right of b_owns: a НЕДЕЙСТВИТЕЛЬНА после перемещения
При использовании — ошибка компиляции } ``` *** -## Move Semantics vs Reference Counting +## Семантика перемещения и подсчёт ссылок -### Copy vs Move +### Копирование и перемещение ```rust -// Simple types (integers, floats, bools, chars) are COPIED, not moved +// Простые типы (целые, числа с плавающей точкой, bool, char) КОПИРУЮТСЯ, а не перемещаются let x = 42; -let y = x; // x is COPIED to y (both valid) +let y = x; // x КОПИРУЕТСЯ в y (оба остаются действительными) println!("{x} {y}"); // ✅ 42 42 -// Heap-allocated types (String, Vec, HashMap) are MOVED +// Типы, размещённые в куче (String, Vec, HashMap), ПЕРЕМЕЩАЮТСЯ let s1 = String::from("hello"); -let s2 = s1; // s1 is MOVED to s2 -// println!("{s1}"); // ❌ Error: value used after move +let s2 = s1; // s1 ПЕРЕМЕЩАЕТСЯ в s2 +// println!("{s1}"); // ❌ Ошибка: значение использовано после перемещения -// To explicitly copy heap data, use .clone() +// Чтобы явно скопировать данные из кучи, используйте .clone() let s1 = String::from("hello"); -let s2 = s1.clone(); // Deep copy -println!("{s1} {s2}"); // ✅ hello hello (both valid) +let s2 = s1.clone(); // Глубокая копия +println!("{s1} {s2}"); // ✅ hello hello (оба действительны) ``` -### Python Developer's Mental Model +### Ментальная модель разработчика на Python ```text Python: Rust: ───────── ───── -int, float, bool Copy types (i32, f64, bool, char) -→ shared refs to immutable → bitwise copied on assignment - objects (no real copy) (always independent values) - (Note: Python caches small ints; Rust copies are always predictable) - -list, dict, str Move types (Vec, HashMap, String) -→ shared reference → ownership transfer (different behavior!) -→ gc cleans up → owner drops data -→ clone with list(x) → clone with x.clone() - or copy.deepcopy(x) +int, float, bool Копируемые типы (i32, f64, bool, char) +→ общие ссылки на → побитовое копирование при присваивании + неизменяемые объекты (всегда независимые значения) + (настоящей копии нет) (Примечание: Python кэширует малые int; копии в Rust всегда предсказуемы) + +list, dict, str Типы с перемещением (Vec, HashMap, String) +→ общая ссылка → передача владения (поведение другое!) +→ сборщик мусора очищает → владелец уничтожает данные +→ копия через list(x) → копия через x.clone() + или copy.deepcopy(x) ``` -### When Python's Sharing Model Causes Bugs +### Когда модель совместного использования в Python приводит к ошибкам ```python -# Python — accidental aliasing +# Python — случайное создание псевдонимов def remove_duplicates(items): seen = set() result = [] @@ -161,16 +157,16 @@ def remove_duplicates(items): return result original = [1, 2, 2, 3, 3, 3] -alias = original # Alias, NOT a copy +alias = original # Псевдоним, НЕ копия unique = remove_duplicates(alias) -# original is still [1, 2, 2, 3, 3, 3] — but only because we didn't mutate -# If remove_duplicates modified the input, original would be affected too +# original по-прежнему [1, 2, 2, 3, 3, 3] — но только потому, что мы его не меняли +# Если бы remove_duplicates изменял входные данные, original тоже пострадал бы ``` ```rust use std::collections::HashSet; -// Rust — ownership prevents accidental aliasing +// Rust — владение предотвращает случайное создание псевдонимов fn remove_duplicates(items: &[i32]) -> Vec { let mut seen = HashSet::new(); items.iter() @@ -180,35 +176,35 @@ fn remove_duplicates(items: &[i32]) -> Vec { } let original = vec![1, 2, 2, 3, 3, 3]; -let unique = remove_duplicates(&original); // Borrows — can't modify -// original is guaranteed unchanged — compiler prevented mutation via & +let unique = remove_duplicates(&original); // Заимствование — изменить нельзя +// original гарантированно не изменится — компилятор запретил мутацию через & ``` *** -## Borrowing and Lifetimes +## Заимствование и времена жизни -### Borrowing = Lending a Book +### Заимствование = одолжить книгу ```rust -Think of ownership like a physical book: +Представьте владение как физическую книгу: -Python: Everyone has a photocopy (shared references + GC) -Rust: One person owns the book. Others can: - - &book = look at it (immutable borrow, many allowed) - - &mut book = write in it (mutable borrow, exclusive) - - book = give it away (move) +Python: У каждого есть фотокопия (общие ссылки + GC) +Rust: Книга принадлежит одному человеку. Остальные могут: + - &book = посмотреть (неизменяемое заимствование, таких может быть много) + - &mut book = писать в ней (изменяемое заимствование, эксклюзивное) + - book = отдать её (перемещение) ``` -### Borrowing Rules +### Правила заимствования ```mermaid flowchart TD - R["Borrowing Rules"] --> IMM["✅ Many &T
(shared/immutable)"] - R --> MUT["✅ One &mut T
(exclusive/mutable)"] - R --> CONFLICT["❌ &T + &mut T
(NEVER at same time)"] - IMM --> SAFE["Multiple readers, safe"] - MUT --> SAFE2["Single writer, safe"] - CONFLICT --> ERR["Compile error!"] + R["Правила заимствования"] --> IMM["✅ Много &T
(общие/неизменяемые)"] + R --> MUT["✅ Один &mut T
(эксклюзивный/изменяемый)"] + R --> CONFLICT["❌ &T + &mut T
(НИКОГДА одновременно)"] + IMM --> SAFE["Много читателей, безопасно"] + MUT --> SAFE2["Один писатель, безопасно"] + CONFLICT --> ERR["Ошибка компиляции!"] style IMM fill:#d4edda style MUT fill:#d4edda style CONFLICT fill:#f8d7da @@ -216,103 +212,96 @@ flowchart TD ``` ```rust -// Rule 1: You can have MANY immutable borrows OR ONE mutable borrow (not both) +// Правило 1: можно иметь МНОГО неизменяемых заимствований ИЛИ ОДНО изменяемое (но не оба сразу) let mut data = vec![1, 2, 3]; -// Multiple immutable borrows — fine +// Несколько неизменяемых заимствований — допустимо let a = &data; let b = &data; println!("{:?} {:?}", a, b); // ✅ -// Mutable borrow — must be exclusive +// Изменяемое заимствование — должно быть эксклюзивным let c = &mut data; c.push(4); -// println!("{:?}", a); // ❌ Error: can't use immutable borrow while mutable exists +// println!("{:?}", a); // ❌ Ошибка: нельзя использовать неизменяемое заимствование, пока существует изменяемое -// This prevents data races at compile time! -// Python has no equivalent — it's why Python dict modified-during-iteration crashes at runtime. +// Так гонки данных предотвращаются на этапе компиляции! +// В Python аналога нет: именно поэтому изменение словаря во время итерации приводит к падению во время выполнения. ``` -### Lifetimes — A Brief Introduction +### Времена жизни: краткое введение ```rust -// Lifetimes answer: "How long does this reference live?" -// Usually the compiler infers them. You rarely write them explicitly. +// Времена жизни отвечают на вопрос: «Как долго живёт эта ссылка?» +// Обычно компилятор выводит их сам. Вы редко пишете их явно. -// Simple case — compiler handles it: +// Простой случай — компилятор справляется сам: fn first_word(s: &str) -> &str { s.split_whitespace().next().unwrap_or("") } -// The compiler knows: the returned &str lives as long as the input &str +// Компилятор знает: возвращаемый &str живёт столько же, сколько входной &str -// When you need explicit lifetimes (rare): +// Когда нужны явные времена жизни (редко): fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } } -// 'a says: "the return value lives as long as both inputs" +// 'a означает: «возвращаемое значение живёт, пока живут оба входных параметра» ``` -> **For Python developers**: Don't worry about lifetimes initially. The compiler will -> tell you when you need them, and 95% of the time it infers them automatically. -> Think of lifetime annotations as hints you give the compiler when it can't figure -> out the relationships on its own. +> **Для разработчиков на Python**: поначалу не беспокойтесь о временах жизни. Компилятор сам скажет, когда они понадобятся, а в 95% случаев он выводит их автоматически. Думайте об аннотациях времён жизни как о подсказках, которые вы даёте компилятору, когда он не может сам разобраться в связях. *** -## Smart Pointers +## Умные указатели -For cases where single ownership is too restrictive, Rust provides smart pointers. -These are closer to Python's reference model — but explicit and opt-in. +Там, где единоличного владения недостаточно, Rust предоставляет умные указатели. Они ближе к модели ссылок Python, но явные и включаются осознанно. ```rust -// Box — heap allocation with single owner (like Python's normal allocation) -let boxed = Box::new(42); // Heap-allocated i32 +// Box — выделение в куче с одним владельцем (как обычное выделение памяти в Python) +let boxed = Box::new(42); // i32 в куче -// Rc — reference counted (like Python's refcount!) +// Rc — подсчёт ссылок (как refcount в Python!) use std::rc::Rc; let shared = Rc::new(vec![1, 2, 3]); -let clone1 = Rc::clone(&shared); // Increment refcount -let clone2 = Rc::clone(&shared); // Increment refcount -// All three point to the same Vec. When all are dropped, Vec is freed. -// Similar to Python's reference counting, but Rc does NOT handle cycles — -// use Weak to break cycles (Python's GC handles cycles automatically) +let clone1 = Rc::clone(&shared); // Увеличиваем счётчик ссылок +let clone2 = Rc::clone(&shared); // Увеличиваем счётчик ссылок +// Все три указывают на один Vec. Когда все они уничтожены, Vec освобождается. +// Похоже на подсчёт ссылок в Python, но Rc НЕ обрабатывает циклы — +// используйте Weak, чтобы разрывать циклы (сборщик мусора Python обрабатывает циклы автоматически) -// Arc — atomic reference counting (Rc for multi-threaded code) +// Arc — атомарный подсчёт ссылок (Rc для многопоточного кода) use std::sync::Arc; let thread_safe = Arc::new(vec![1, 2, 3]); -// Use Arc when sharing across threads (Rc is single-threaded) +// Используйте Arc, когда данные разделяются между потоками (Rc — только для одного потока) -// RefCell — runtime borrow checking (like Python's "anything goes" model) +// RefCell — проверка заимствований во время выполнения (как модель «всё можно» в Python) use std::cell::RefCell; let cell = RefCell::new(42); -*cell.borrow_mut() = 99; // Mutable borrow at runtime (panics if double-borrowed) +*cell.borrow_mut() = 99; // Изменяемое заимствование во время выполнения (паника при двойном заимствовании) ``` -### When to Use Each +### Когда что использовать -| Smart Pointer | Python Analogy | Use Case | -|---------------|----------------|----------| -| `Box` | Normal allocation | Large data, recursive types, trait objects | -| `Rc` | Python's default refcount | Shared ownership, single-threaded | -| `Arc` | Thread-safe refcount | Shared ownership, multi-threaded | -| `RefCell` | Python's "just mutate it" | Interior mutability (escape hatch) | -| `Rc>` | Python's normal object model | Shared + mutable (graph structures) | +| Умный указатель | Аналог в Python | Сценарий использования | +|-----------------|-----------------|------------------------| +| `Box` | Обычное выделение памяти | Большие данные, рекурсивные типы, трейт-объекты | +| `Rc` | Стандартный подсчёт ссылок Python | Общее владение, один поток | +| `Arc` | Потокобезопасный подсчёт ссылок | Общее владение, несколько потоков | +| `RefCell` | Модель «просто меняй» в Python | Внутренняя изменяемость (запасной люк) | +| `Rc>` | Обычная объектная модель Python | Общие и изменяемые данные (графовые структуры) | -> **Key insight**: `Rc>` gives you Python-like semantics (shared, mutable data) -> but you have to opt in explicitly. Rust's default (owned, moved) is faster and avoids -> the overhead of reference counting. For graph-like structures with cycles, use `Weak` -> to break reference loops — unlike Python, Rust's `Rc` has no cycle collector. +> **Ключевая мысль**: `Rc>` даёт семантику, похожую на Python (общие, изменяемые данные), но включать её нужно явно. Принятое в Rust по умолчанию владение с перемещением быстрее и не несёт накладных расходов на подсчёт ссылок. Для графоподобных структур с циклами используйте `Weak`, чтобы разрывать циклы ссылок: в отличие от Python, у `Rc` в Rust нет сборщика циклов. -> 📌 **See also**: [Ch. 13 — Concurrency](ch13-concurrency.md) covers `Arc>` for multi-threaded shared state. +> 📌 **См. также**: [Гл. 13 — Конкурентность](ch13-concurrency.md) описывает `Arc>` для общего состояния в многопоточном коде. --- -## Exercises +## Упражнения
-🏋️ Exercise: Spot the Borrow Checker Error (click to expand) +🏋️ Упражнение: найдите ошибку заимствования (нажмите, чтобы раскрыть) -**Challenge**: The following code has 3 borrow checker errors. Identify each one and fix them without using `.clone()`: +**Задание**: в следующем коде есть 3 ошибки borrow checker. Найдите каждую и исправьте их, не используя `.clone()`: ```rust fn main() { @@ -331,32 +320,31 @@ fn make_greeting(name: String) -> String { ```
-🔑 Solution +🔑 Решение ```rust fn main() { let mut names = vec!["Alice".to_string(), "Bob".to_string()]; let first = &names[0]; - println!("First: {first}"); // Use borrow BEFORE mutating - names.push("Charlie".to_string()); // Now safe — no live immutable borrow + println!("First: {first}"); // Используем заимствование ДО изменения + names.push("Charlie".to_string()); // Теперь безопасно — живого неизменяемого заимствования нет - let greeting = make_greeting(&names[0]); // Pass reference, not owned + let greeting = make_greeting(&names[0]); // Передаём ссылку, а не владение println!("{greeting}"); } -fn make_greeting(name: &str) -> String { // Accept &str, not String +fn make_greeting(name: &str) -> String { // Принимаем &str, а не String format!("Hello, {name}!") } ``` -**Errors fixed**: -1. **Immutable borrow + mutation**: `first` borrows `names`, then `push` mutates it. Fix: use `first` before pushing. -2. **Move out of Vec**: `names[0]` tries to move a String out of Vec (not allowed). Fix: borrow with `&names[0]`. -3. **Function takes ownership**: `make_greeting(String)` consumes the value. Fix: take `&str` instead. +**Исправленные ошибки**: +1. **Неизменяемое заимствование и изменение**: `first` заимствует `names`, а затем `push` его изменяет. Исправление: использовать `first` до `push`. +2. **Перемещение из Vec**: `names[0]` пытается переместить String из Vec, а это запрещено. Исправление: заимствовать через `&names[0]`. +3. **Функция забирает владение**: `make_greeting(String)` потребляет значение. Исправление: принимать `&str`.
*** - diff --git a/python-book/src/ch08-crates-and-modules.md b/python-book/src/ch08-crates-and-modules.md index 96c0a29..1d1d9d7 100644 --- a/python-book/src/ch08-crates-and-modules.md +++ b/python-book/src/ch08-crates-and-modules.md @@ -1,19 +1,18 @@ -## Rust Modules vs Python Packages +## Модули Rust и пакеты Python -> **What you'll learn:** `mod` and `use` vs `import`, visibility (`pub`) vs Python's convention-based privacy, -> Cargo.toml vs pyproject.toml, crates.io vs PyPI, and workspaces vs monorepos. +> **Что вы узнаете:** `mod` и `use` в сравнении с `import`, видимость (`pub`) в сравнении с приватностью Python, основанной на соглашениях, Cargo.toml в сравнении с pyproject.toml, crates.io в сравнении с PyPI, а также рабочие пространства в сравнении с монорепозиториями. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Python Module System +### Система модулей Python ```python -# Python — files are modules, directories with __init__.py are packages +# Python — файлы являются модулями, каталоги с __init__.py — пакетами # myproject/ -# ├── __init__.py # Makes it a package +# ├── __init__.py # Делает каталог пакетом # ├── main.py # ├── utils/ -# │ ├── __init__.py # Makes utils a sub-package +# │ ├── __init__.py # Делает utils подпакетом # │ ├── helpers.py # │ └── validators.py # └── models/ @@ -21,20 +20,20 @@ # ├── user.py # └── product.py -# Importing: +# Импорт: from myproject.utils.helpers import format_name from myproject.models.user import User import myproject.utils.validators as validators ``` -### Rust Module System +### Система модулей Rust ```rust -// Rust — mod declarations create the module tree, files provide content +// Rust — объявления mod создают дерево модулей, файлы содержат код // src/ -// ├── main.rs # Crate root — declares modules +// ├── main.rs # Корень крейта — объявляет модули // ├── utils/ -// │ ├── mod.rs # Module declaration (like __init__.py) +// │ ├── mod.rs # Объявление модуля (как __init__.py) // │ ├── helpers.rs // │ └── validators.rs // └── models/ @@ -42,21 +41,21 @@ import myproject.utils.validators as validators // ├── user.rs // └── product.rs -// In src/main.rs: -mod utils; // Tells Rust to look for src/utils/mod.rs -mod models; // Tells Rust to look for src/models/mod.rs +// В src/main.rs: +mod utils; // Говорит Rust искать src/utils/mod.rs +mod models; // Говорит Rust искать src/models/mod.rs use utils::helpers::format_name; use models::user::User; -// In src/utils/mod.rs: -pub mod helpers; // Declares and re-exports helpers.rs -pub mod validators; // Declares and re-exports validators.rs +// В src/utils/mod.rs: +pub mod helpers; // Объявляет модуль helpers из helpers.rs и делает его публичным +pub mod validators; // Объявляет модуль validators из validators.rs и делает его публичным ``` ```mermaid graph TD - A["main.rs
(crate root)"] --> B["mod utils"] + A["main.rs
(корень крейта)"] --> B["mod utils"] A --> C["mod models"] B --> D["utils/mod.rs"] D --> E["helpers.rs"] @@ -69,40 +68,40 @@ graph TD style G fill:#fff3cd,stroke:#ffc107 ``` -> **Python equivalent**: Think of `mod.rs` as `__init__.py` — it declares what the module exports. The crate root (`main.rs` / `lib.rs`) is like your top-level package `__init__.py`. +> **Аналог в Python**: думайте о `mod.rs` как о `__init__.py`: он объявляет, что экспортирует модуль. Корень крейта (`main.rs` / `lib.rs`) похож на `__init__.py` вашего верхнеуровневого пакета. -### Key Differences +### Ключевые различия -| Concept | Python | Rust | +| Понятие | Python | Rust | |---------|--------|------| -| Module = file | ✅ Automatic | Must declare with `mod` | -| Package = directory | `__init__.py` | `mod.rs` | -| Public by default | ✅ Everything | ❌ Private by default | -| Make public | `_prefix` convention | `pub` keyword | -| Import syntax | `from x import y` | `use x::y;` | -| Wildcard import | `from x import *` | `use x::*;` (discouraged) | -| Relative imports | `from . import sibling` | `use super::sibling;` | -| Re-export | `__all__` or explicit | `pub use inner::Thing;` | - -### Visibility — Private by Default +| Модуль = файл | ✅ Автоматически | Нужно объявить через `mod` | +| Пакет = каталог | `__init__.py` | `mod.rs` | +| Публично по умолчанию | ✅ Всё | ❌ По умолчанию приватно | +| Сделать публичным | Соглашение с префиксом `_` | Ключевое слово `pub` | +| Синтаксис импорта | `from x import y` | `use x::y;` | +| Импорт всего | `from x import *` | `use x::*;` (не рекомендуется) | +| Относительный импорт | `from . import sibling` | `use super::sibling;` | +| Реэкспорт | `__all__` или явно | `pub use inner::Thing;` | + +### Видимость: приватно по умолчанию ```python -# Python — "we're all adults here" +# Python — «здесь все взрослые» class User: def __init__(self): - self.name = "Alice" # Public (by convention) - self._age = 30 # "Private" (convention: single underscore) - self.__secret = "shhh" # Name-mangled (not truly private) + self.name = "Alice" # Публичное (по соглашению) + self._age = 30 # «Приватное» (соглашение: одно подчёркивание) + self.__secret = "тсс" # Искажённое имя (не по-настоящему приватное) -# Nothing stops you from accessing _age or even __secret -print(user._age) # Works fine -print(user._User__secret) # Works too (name mangling) +# Ничто не мешает обратиться к _age или даже к __secret +print(user._age) # Работает +print(user._User__secret) # Тоже работает (искажение имён) ``` ```rust -// Rust — private is enforced by the compiler +// Rust — приватность гарантируется компилятором pub struct User { - pub name: String, // Public — anyone can access - age: i32, // Private — only this module can access + pub name: String, // Публичное — доступно всем + age: i32, // Приватное — доступно только этому модулю } impl User { @@ -110,45 +109,45 @@ impl User { User { name: name.to_string(), age } } - pub fn age(&self) -> i32 { // Public getter + pub fn age(&self) -> i32 { // Публичный геттер self.age } - fn validate(&self) -> bool { // Private method + fn validate(&self) -> bool { // Приватный метод self.age > 0 } } -// Outside the module: +// Снаружи модуля: let user = User::new("Alice", 30); -println!("{}", user.name); // ✅ Public -// println!("{}", user.age); // ❌ Compile error: field is private -println!("{}", user.age()); // ✅ Public method (getter) +println!("{}", user.name); // ✅ Публичное +// println!("{}", user.age); // ❌ Ошибка компиляции: поле приватное +println!("{}", user.age()); // ✅ Публичный метод (геттер) ``` *** -## Crates vs PyPI Packages +## Крейты и пакеты PyPI -### Python Packages (PyPI) +### Пакеты Python (PyPI) ```bash # Python -pip install requests # Install from PyPI -pip install "requests>=2.28" # Version constraint -pip freeze > requirements.txt # Lock versions -pip install -r requirements.txt # Reproduce environment +pip install requests # Установка из PyPI +pip install "requests>=2.28" # Ограничение версии +pip freeze > requirements.txt # Зафиксировать версии +pip install -r requirements.txt # Воссоздать окружение ``` -### Rust Crates (crates.io) +### Крейты Rust (crates.io) ```bash # Rust -cargo add reqwest # Install from crates.io (adds to Cargo.toml) -cargo add reqwest@0.12 # Version constraint -# Cargo.lock is auto-generated — no manual step -cargo build # Downloads and compiles dependencies +cargo add reqwest # Установка из crates.io (добавляет в Cargo.toml) +cargo add reqwest@0.12 # Ограничение версии +# Cargo.lock создаётся автоматически, никаких ручных действий +cargo build # Загружает и компилирует зависимости ``` -### Cargo.toml vs pyproject.toml +### Cargo.toml и pyproject.toml ```toml # Rust — Cargo.toml [package] @@ -157,7 +156,7 @@ version = "0.1.0" edition = "2021" [dependencies] -serde = { version = "1.0", features = ["derive"] } # With feature flags +serde = { version = "1.0", features = ["derive"] } # С флагами функций reqwest = { version = "0.12", features = ["json"] } tokio = { version = "1", features = ["full"] } log = "0.4" @@ -166,40 +165,40 @@ log = "0.4" mockall = "0.13" ``` -### Essential Crates for Python Developers - -| Python Library | Rust Crate | Purpose | -|---------------|------------|---------| -| `requests` | `reqwest` | HTTP client | -| `json` (stdlib) | `serde_json` | JSON parsing | -| `pydantic` | `serde` | Serialization/validation | -| `pathlib` | `std::path` (stdlib) | Path handling | -| `os` / `shutil` | `std::fs` (stdlib) | File operations | -| `re` | `regex` | Regular expressions | -| `logging` | `tracing` / `log` | Logging | -| `click` / `argparse` | `clap` | CLI argument parsing | -| `asyncio` | `tokio` | Async runtime | -| `datetime` | `chrono` | Date and time | -| `pytest` | Built-in + `rstest` | Testing | -| `dataclasses` | `#[derive(...)]` | Data structures | -| `typing.Protocol` | Traits | Structural typing | -| `subprocess` | `std::process` (stdlib) | Run external commands | +### Основные крейты для разработчиков на Python + +| Библиотека Python | Крейт Rust | Назначение | +|-------------------|------------|------------| +| `requests` | `reqwest` | HTTP-клиент | +| `json` (stdlib) | `serde_json` | Разбор JSON | +| `pydantic` | `serde` | Сериализация и валидация | +| `pathlib` | `std::path` (stdlib) | Работа с путями | +| `os` / `shutil` | `std::fs` (stdlib) | Операции с файлами | +| `re` | `regex` | Регулярные выражения | +| `logging` | `tracing` / `log` | Логирование | +| `click` / `argparse` | `clap` | Разбор аргументов CLI | +| `asyncio` | `tokio` | Асинхронный рантайм | +| `datetime` | `chrono` | Дата и время | +| `pytest` | Встроенное + `rstest` | Тестирование | +| `dataclasses` | `#[derive(...)]` | Структуры данных | +| `typing.Protocol` | Трейты | Структурная типизация | +| `subprocess` | `std::process` (stdlib) | Запуск внешних команд | | `sqlite3` | `rusqlite` | SQLite | -| `sqlalchemy` | `diesel` / `sqlx` | ORM / SQL toolkit | -| `fastapi` | `axum` / `actix-web` | Web framework | +| `sqlalchemy` | `diesel` / `sqlx` | ORM / инструментарий SQL | +| `fastapi` | `axum` / `actix-web` | Веб-фреймворк | *** -## Workspaces vs Monorepos +## Рабочие пространства и монорепозитории -### Python Monorepo (typical) +### Монорепозиторий Python (типичный) ```text -# Python monorepo (various approaches, no standard) +# Монорепозиторий Python (разные подходы, стандарта нет) myproject/ -├── pyproject.toml # Root project +├── pyproject.toml # Корневой проект ├── packages/ │ ├── core/ -│ │ ├── pyproject.toml # Each package has its own config +│ │ ├── pyproject.toml # У каждого пакета своя конфигурация │ │ └── src/core/... │ ├── api/ │ │ ├── pyproject.toml @@ -207,12 +206,12 @@ myproject/ │ └── cli/ │ ├── pyproject.toml │ └── src/cli/... -# Tools: poetry workspaces, pip -e ., uv workspaces — no standard +# Инструменты: poetry workspaces, pip -e ., uv workspaces — стандарта нет ``` -### Rust Workspace +### Рабочее пространство Rust ```toml -# Rust — Cargo.toml at root +# Rust — Cargo.toml в корне [workspace] members = [ "core", @@ -220,17 +219,17 @@ members = [ "cli", ] -# Shared dependencies across workspace +# Общие зависимости для всего рабочего пространства [workspace.dependencies] serde = { version = "1.0", features = ["derive"] } tokio = { version = "1", features = ["full"] } ``` ```text -# Rust workspace structure — standardized, built into Cargo +# Структура рабочего пространства Rust — стандартизирована, встроена в Cargo myproject/ -├── Cargo.toml # Workspace root -├── Cargo.lock # Single lock file for all crates +├── Cargo.toml # Корень рабочего пространства +├── Cargo.lock # Единый lock-файл для всех крейтов ├── core/ │ ├── Cargo.toml # [dependencies] serde.workspace = true │ └── src/lib.rs @@ -243,55 +242,52 @@ myproject/ ``` ```bash -# Workspace commands -cargo build # Build everything -cargo test # Test everything -cargo build -p core # Build just the core crate -cargo test -p api # Test just the api crate -cargo clippy --all # Lint everything +# Команды рабочего пространства +cargo build # Собрать всё +cargo test # Протестировать всё +cargo build -p core # Собрать только крейт core +cargo test -p api # Протестировать только крейт api +cargo clippy --all # Проверить линтером всё ``` -> **Key insight**: Rust workspaces are first-class, built into Cargo. Python monorepos -> require third-party tools (poetry, uv, pants) with varying levels of support. -> In a Rust workspace, all crates share a single `Cargo.lock`, ensuring consistent -> dependency versions across the project. +> **Ключевая мысль**: рабочие пространства в Rust — полноценная возможность, встроенная в Cargo. Монорепозиториям Python нужны сторонние инструменты (poetry, uv, pants) с разным уровнем поддержки. В рабочем пространстве Rust все крейты используют один `Cargo.lock`, что обеспечивает согласованные версии зависимостей во всём проекте. --- -## Exercises +## Упражнения
-🏋️ Exercise: Module Visibility (click to expand) +🏋️ Упражнение: видимость модулей (нажмите, чтобы раскрыть) -**Challenge**: Given this module structure, predict which lines compile and which don't: +**Задание**: для этой структуры модулей предскажите, какие строки компилируются, а какие нет: ```rust mod kitchen { - fn secret_recipe() -> &'static str { "42 spices" } - pub fn menu() -> &'static str { "Today's special" } + fn secret_recipe() -> &'static str { "42 специи" } + pub fn menu() -> &'static str { "Фирменное блюдо дня" } pub mod staff { pub fn cook() -> String { - format!("Cooking with {}", super::secret_recipe()) + format!("Готовим с помощью {}", super::secret_recipe()) } } } fn main() { - println!("{}", kitchen::menu()); // Line A - println!("{}", kitchen::secret_recipe()); // Line B - println!("{}", kitchen::staff::cook()); // Line C + println!("{}", kitchen::menu()); // Строка A + println!("{}", kitchen::secret_recipe()); // Строка B + println!("{}", kitchen::staff::cook()); // Строка C } ```
-🔑 Solution +🔑 Решение -- **Line A**: ✅ Compiles — `menu()` is `pub` -- **Line B**: ❌ Compile error — `secret_recipe()` is private to `kitchen` -- **Line C**: ✅ Compiles — `staff::cook()` is `pub`, and `cook()` can access `secret_recipe()` via `super::` (child modules can access parent's private items) +- **Строка A**: ✅ Компилируется: `menu()` объявлена как `pub` +- **Строка B**: ❌ Ошибка компиляции: `secret_recipe()` приватна для `kitchen` +- **Строка C**: ✅ Компилируется: `staff::cook()` объявлена как `pub`, а `cook()` может обращаться к `secret_recipe()` через `super::` (дочерние модули видят приватные элементы родителя) -**Key takeaway**: In Rust, child modules can see parent's privates (like Python's `_private` convention, but enforced). Outsiders cannot. This is the opposite of Python where `_private` is just a hint. +**Ключевой вывод**: в Rust дочерние модули видят приватные элементы родителя (как соглашение `_private` в Python, но с гарантией компилятора). Посторонние — нет. Это противоположность Python, где `_private` — лишь подсказка.
diff --git a/python-book/src/ch09-error-handling.md b/python-book/src/ch09-error-handling.md index e87a34b..cd35246 100644 --- a/python-book/src/ch09-error-handling.md +++ b/python-book/src/ch09-error-handling.md @@ -1,48 +1,45 @@ -## Exceptions vs Result +## Исключения и Result -> **What you'll learn:** `Result` vs `try`/`except`, the `?` operator for concise error propagation, -> custom error types with `thiserror`, `anyhow` for applications, and why explicit errors prevent hidden bugs. +> **Что вы узнаете:** `Result` в сравнении с `try`/`except`, оператор `?` для краткой передачи ошибок, собственные типы ошибок с `thiserror`, `anyhow` для приложений и то, почему явные ошибки предотвращают скрытые баги. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -This is one of the biggest mindset changes for Python developers. Python uses exceptions -for error handling — errors can be thrown from anywhere and caught anywhere (or not at all). -Rust uses `Result` — errors are values that must be explicitly handled. +Для разработчиков на Python это одна из самых больших смен мышления. Python использует исключения: ошибку можно выбросить откуда угодно и перехватить где угодно (или не перехватывать вовсе). Rust использует `Result`: ошибки — это значения, которые нужно явно обработать. -### Python Exception Handling +### Обработка исключений в Python ```python -# Python — exceptions can be thrown from anywhere +# Python — исключения могут быть выброшены откуда угодно import json def load_config(path: str) -> dict: try: with open(path) as f: - data = json.load(f) # Can raise JSONDecodeError + data = json.load(f) # Может выбросить JSONDecodeError if "version" not in data: - raise ValueError("Missing version field") + raise ValueError("Отсутствует поле version") return data except FileNotFoundError: - print(f"Config file not found: {path}") + print(f"Файл конфигурации не найден: {path}") return {} except json.JSONDecodeError as e: - print(f"Invalid JSON: {e}") + print(f"Некорректный JSON: {e}") return {} - # What other exceptions can this throw? + # Какие ещё исключения может выбросить эта функция? # IOError? PermissionError? UnicodeDecodeError? - # You can't tell from the function signature! + # По сигнатуре функции этого не понять! ``` -### Rust Result-Based Error Handling +### Обработка ошибок через Result в Rust ```rust -// Rust — errors are return values, visible in the function signature +// Rust — ошибки являются возвращаемыми значениями, видимыми в сигнатуре функции use std::fs; use serde_json::Value; fn load_config(path: &str) -> Result { - let contents = fs::read_to_string(path) // Returns Result + let contents = fs::read_to_string(path) // Возвращает Result .map_err(|e| ConfigError::FileError(e.to_string()))?; - let data: Value = serde_json::from_str(&contents) // Returns Result + let data: Value = serde_json::from_str(&contents) // Возвращает Result .map_err(|e| ConfigError::ParseError(e.to_string()))?; if data.get("version").is_none() { @@ -60,123 +57,122 @@ enum ConfigError { } ``` -### Key Differences +### Ключевые различия ```text Python: Rust: ───────── ───── -- Errors are exceptions (thrown) - Errors are values (returned) -- Hidden control flow (stack unwinding) - Explicit control flow (? operator) -- Can't tell what errors from signature- MUST see errors in return type -- Uncaught exceptions crash at runtime - Unhandled Results produce compile warnings (always handle them) -- try/except is optional - Handling Result is required -- Broad except catches everything - match arms are exhaustive +- Ошибки — это исключения (выбрасываются) - Ошибки — это значения (возвращаются) +- Скрытый поток управления (раскрутка стека) - Явный поток управления (оператор ?) +- По сигнатуре не понять, какие ошибки - Ошибки ОБЯЗАНЫ быть видны в типе возврата +- Необработанные исключения роняют программу в рантайме - Необработанные Result дают предупреждения компилятора (их всегда нужно обрабатывать) +- try/except необязательны - Обработка Result обязательна +- Широкий except ловит всё - Ветки match исчерпывающие ``` -### The Two Result Variants +### Два варианта Result ```rust -// Result has exactly two variants: +// У Result ровно два варианта: enum Result { - Ok(T), // Success — contains the value (like Python's return value) - Err(E), // Failure — contains the error (like Python's raised exception) + Ok(T), // Успех — содержит значение (как return в Python) + Err(E), // Неудача — содержит ошибку (как выброшенное исключение в Python) } -// Using Result: +// Использование Result: fn divide(a: f64, b: f64) -> Result { if b == 0.0 { - Err("Division by zero".to_string()) // Like: raise ValueError("...") + Err("Деление на ноль".to_string()) // Как: raise ValueError("...") } else { - Ok(a / b) // Like: return a / b + Ok(a / b) // Как: return a / b } } -// Handling Result — like try/except but explicit +// Обработка Result — как try/except, только явная match divide(10.0, 0.0) { - Ok(result) => println!("Result: {result}"), - Err(msg) => println!("Error: {msg}"), + Ok(result) => println!("Результат: {result}"), + Err(msg) => println!("Ошибка: {msg}"), } ``` *** -## The ? Operator +## Оператор ? -The `?` operator is Rust's equivalent of letting exceptions propagate up the call stack, -but it's visible and explicit. +Оператор `?` — это аналог в Rust того, что исключения всплывают вверх по стеку вызовов, только он виден и явен. -### Python — Implicit Propagation +### Python: неявное распространение ```python -# Python — exceptions propagate silently up the call stack +# Python — исключения тихо всплывают вверх по стеку вызовов def read_username() -> str: - with open("config.txt") as f: # FileNotFoundError propagates - return f.readline().strip() # IOError propagates + with open("config.txt") as f: # FileNotFoundError всплывает + return f.readline().strip() # IOError всплывает def greet(): - name = read_username() # If this throws, greet() also throws - print(f"Hello, {name}!") # This is skipped on error + name = read_username() # Если здесь выброшено исключение, greet() тоже его выбросит + print(f"Привет, {name}!") # При ошибке эта строка пропускается -# The error propagation is INVISIBLE — you have to read the implementation -# to know what exceptions might escape. +# Распространение ошибок НЕВИДИМО — чтобы узнать, какие исключения могут выйти наружу, +# нужно читать реализацию. ``` -### Rust — Explicit Propagation with ? +### Rust: явное распространение с помощью ? ```rust -// Rust — ? propagates errors, but it's visible in the code AND the signature +// Rust — ? передаёт ошибки дальше, и это видно и в коде, и в сигнатуре use std::fs; use std::io; fn read_username() -> Result { - let contents = fs::read_to_string("config.txt")?; // ? = propagate on Err + let contents = fs::read_to_string("config.txt")?; // ? = передать дальше при Err Ok(contents.lines().next().unwrap_or("").to_string()) } fn greet() -> Result<(), io::Error> { - let name = read_username()?; // ? = if Err, return Err immediately - println!("Hello, {name}!"); // Only reached on Ok + let name = read_username()?; // ? = если Err, сразу вернуть Err + println!("Привет, {name}!"); // Достигается только при Ok Ok(()) } -// The ? says: "if this is Err, return it from THIS function immediately." -// It's like Python's exception propagation, but: -// 1. It's visible (you see the ?) -// 2. It's in the return type (Result<..., io::Error>) -// 3. The compiler ensures you handle it somewhere +// ? означает: «если здесь Err, немедленно вернуть его ИЗ ЭТОЙ функции». +// Это похоже на распространение исключений в Python, но: +// 1. Это видно (вы видите ?) +// 2. Это есть в типе возврата (Result<..., io::Error>) +// 3. Компилятор следит, чтобы ошибка была обработана где-то ``` -### Chaining with ? +### Цепочки с ? ```python -# Python — multiple operations that might fail +# Python — несколько операций, которые могут завершиться ошибкой def process_file(path: str) -> dict: - with open(path) as f: # Might fail - text = f.read() # Might fail - data = json.loads(text) # Might fail - validate(data) # Might fail - return transform(data) # Might fail - # Any of these can throw — and the exception type varies! + with open(path) as f: # Может завершиться ошибкой + text = f.read() # Может завершиться ошибкой + data = json.loads(text) # Может завершиться ошибкой + validate(data) # Может завершиться ошибкой + return transform(data) # Может завершиться ошибкой + # Любая из них может выбросить исключение, и тип исключения у каждой свой! ``` ```rust -// Rust — same chain, but explicit +// Rust — та же цепочка, но явная fn process_file(path: &str) -> Result { - let text = fs::read_to_string(path)?; // ? propagates io::Error - let data: Value = serde_json::from_str(&text)?; // ? propagates serde error - let validated = validate(&data)?; // ? propagates validation error - let result = transform(&validated)?; // ? propagates transform error + let text = fs::read_to_string(path)?; // ? передаёт io::Error дальше + let data: Value = serde_json::from_str(&text)?; // ? передаёт ошибку serde дальше + let validated = validate(&data)?; // ? передаёт ошибку валидации дальше + let result = transform(&validated)?; // ? передаёт ошибку преобразования дальше Ok(result) } -// Every ? is a potential early return — and they're all visible! +// Каждый ? — это потенциальный досрочный возврат, и все они видны! ``` ```mermaid flowchart TD - A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"] - A -->|Err| X["Return Err(io::Error)"] + A["read_to_string(path)?"] -->|Ok| B["serde_json::from_str?"] + A -->|Err| X["Возврат Err(io::Error)"] B -->|Ok| C["validate(&data)?"] - B -->|Err| Y["Return Err(serde::Error)"] + B -->|Err| Y["Возврат Err(serde::Error)"] C -->|Ok| D["transform(&validated)?"] - C -->|Err| Z["Return Err(ValidationError)"] + C -->|Err| Z["Возврат Err(ValidationError)"] D -->|Ok| E["Ok(result) ✅"] - D -->|Err| W["Return Err(TransformError)"] + D -->|Err| W["Возврат Err(TransformError)"] style E fill:#d4edda,stroke:#28a745 style X fill:#f8d7da,stroke:#dc3545 style Y fill:#f8d7da,stroke:#dc3545 @@ -184,22 +180,22 @@ flowchart TD style W fill:#f8d7da,stroke:#dc3545 ``` -> Each `?` is an exit point — unlike Python's try/except where you can't see which line might throw without reading the docs. +> Каждый `?` — это точка выхода. В отличие от try/except в Python, где без чтения документации не видно, какая строка может выбросить исключение. > -> 📌 **See also**: [Ch. 15 — Migration Patterns](ch15-migration-patterns.md) covers translating Python try/except patterns to Rust in real codebases. +> 📌 **См. также**: [Гл. 15 — Паттерны миграции](ch15-migration-patterns.md) рассказывает о переводе паттернов try/except из Python на Rust в реальных проектах. *** -## Custom Error Types with thiserror +## Собственные типы ошибок с thiserror ```mermaid graph TD - AE["AppError (enum)"] --> NF["NotFound
{ entity, id }"] + AE["AppError (перечисление)"] --> NF["NotFound
{ entity, id }"] AE --> VE["Validation
{ field, message }"] AE --> IO["Io(std::io::Error)
#[from]"] AE --> JSON["Json(serde_json::Error)
#[from]"] - IO2["std::io::Error"] -->|"auto-convert via From"| IO - JSON2["serde_json::Error"] -->|"auto-convert via From"| JSON + IO2["std::io::Error"] -->|"автопреобразование через From"| IO + JSON2["serde_json::Error"] -->|"автопреобразование через From"| JSON style AE fill:#d4edda,stroke:#28a745 style NF fill:#fff3cd style VE fill:#fff3cd @@ -209,11 +205,11 @@ graph TD style JSON2 fill:#f8d7da ``` -> The `#[from]` attribute auto-generates `impl From for AppError`, so `?` converts library errors into your app errors automatically. +> Атрибут `#[from]` автоматически генерирует `impl From for AppError`, поэтому `?` сам превращает ошибки библиотек в ошибки вашего приложения. -### Python Custom Exceptions +### Собственные исключения в Python ```python -# Python — custom exception classes +# Python — собственные классы исключений class AppError(Exception): pass @@ -221,98 +217,98 @@ class NotFoundError(AppError): def __init__(self, entity: str, id: int): self.entity = entity self.id = id - super().__init__(f"{entity} with id {id} not found") + super().__init__(f"{entity} с id {id} не найден") class ValidationError(AppError): def __init__(self, field: str, message: str): self.field = field - super().__init__(f"Validation error on {field}: {message}") + super().__init__(f"Ошибка валидации поля {field}: {message}") -# Usage: +# Использование: def find_user(user_id: int) -> dict: if user_id not in users: - raise NotFoundError("User", user_id) + raise NotFoundError("Пользователь", user_id) return users[user_id] ``` -### Rust Custom Errors with thiserror +### Собственные ошибки в Rust с thiserror ```rust -// Rust — error enums with thiserror (most popular approach) +// Rust — перечисления ошибок с thiserror (самый популярный подход) // Cargo.toml: thiserror = "2" use thiserror::Error; #[derive(Debug, Error)] enum AppError { - #[error("{entity} with id {id} not found")] + #[error("{entity} с id {id} не найден")] NotFound { entity: String, id: i64 }, - #[error("Validation error on {field}: {message}")] + #[error("Ошибка валидации поля {field}: {message}")] Validation { field: String, message: String }, - #[error("IO error: {0}")] - Io(#[from] std::io::Error), // Auto-convert from io::Error + #[error("Ошибка ввода-вывода: {0}")] + Io(#[from] std::io::Error), // Автопреобразование из io::Error - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), // Auto-convert from serde error + #[error("Ошибка JSON: {0}")] + Json(#[from] serde_json::Error), // Автопреобразование из ошибки serde } -// Usage: +// Использование: fn find_user(user_id: i64) -> Result { users.get(&user_id) .cloned() .ok_or(AppError::NotFound { - entity: "User".to_string(), + entity: "Пользователь".to_string(), id: user_id, }) } -// The #[from] attribute means ? auto-converts io::Error → AppError::Io +// Атрибут #[from] означает, что ? автоматически преобразует io::Error в AppError::Io fn load_users(path: &str) -> Result, AppError> { - let data = fs::read_to_string(path)?; // io::Error → AppError::Io automatically + let data = fs::read_to_string(path)?; // io::Error → AppError::Io автоматически let users: Vec = serde_json::from_str(&data)?; // → AppError::Json Ok(users) } ``` -### Error Handling Quick Reference +### Краткая справка по обработке ошибок -| Python | Rust | Notes | -|--------|------|-------| -| `raise ValueError("msg")` | `return Err(AppError::Validation {...})` | Explicit return | -| `try: ... except:` | `match result { Ok(v) => ..., Err(e) => ... }` | Exhaustive | -| `except ValueError as e:` | `Err(AppError::Validation { .. }) =>` | Pattern match | -| `raise ... from e` | `#[from]` attribute or `.map_err()` | Error chaining | -| `finally:` | `Drop` trait (automatic) | Deterministic cleanup | -| `with open(...):` | Scope-based drop (automatic) | RAII pattern | -| Exception propagates silently | `?` propagates visibly | Always in return type | -| `isinstance(e, ValueError)` | `matches!(e, AppError::Validation {..})` | Type checking | +| Python | Rust | Примечания | +|--------|------|------------| +| `raise ValueError("msg")` | `return Err(AppError::Validation {...})` | Явный возврат | +| `try: ... except:` | `match result { Ok(v) => ..., Err(e) => ... }` | Исчерпывающе | +| `except ValueError as e:` | `Err(AppError::Validation { .. }) =>` | Сопоставление с образцом | +| `raise ... from e` | Атрибут `#[from]` или `.map_err()` | Цепочка ошибок | +| `finally:` | Трейт `Drop` (автоматически) | Детерминированная очистка | +| `with open(...):` | Освобождение по области видимости (автоматически) | Паттерн RAII | +| Исключение распространяется молча | `?` распространяет явно | Всегда в типе возврата | +| `isinstance(e, ValueError)` | `matches!(e, AppError::Validation {..})` | Проверка типа | --- -## Exercises +## Упражнения
-🏋️ Exercise: Parse Config Value (click to expand) +🏋️ Упражнение: разбор значения конфигурации (нажмите, чтобы раскрыть) -**Challenge**: Write a function `parse_port(s: &str) -> Result` that: -1. Rejects empty strings with error `"empty input"` -2. Parses the string to `u16`, mapping the parse error to `"invalid number: {original_error}"` -3. Rejects ports below 1024 with `"port {n} is privileged"` +**Задание**: напишите функцию `parse_port(s: &str) -> Result`, которая: +1. Отклоняет пустые строки с ошибкой `"пустой ввод"` +2. Разбирает строку в `u16`, преобразуя ошибку разбора в `"некорректное число: {исходная_ошибка}"` +3. Отклоняет порты ниже 1024 с ошибкой `"порт {n} привилегированный"` -Call it with `""`, `"hello"`, `"80"`, and `"8080"` and print the results. +Вызовите её с `""`, `"hello"`, `"80"` и `"8080"` и выведите результаты.
-🔑 Solution +🔑 Решение ```rust fn parse_port(s: &str) -> Result { if s.is_empty() { - return Err("empty input".to_string()); + return Err("пустой ввод".to_string()); } - let port: u16 = s.parse().map_err(|e| format!("invalid number: {e}"))?; + let port: u16 = s.parse().map_err(|e| format!("некорректное число: {e}"))?; if port < 1024 { - return Err(format!("port {port} is privileged")); + return Err(format!("порт {port} привилегированный")); } Ok(port) } @@ -327,11 +323,10 @@ fn main() { } ``` -**Key takeaway**: `?` with `.map_err()` is Rust's replacement for `try/except ValueError as e: raise ConfigError(...) from e`. Every error path is visible in the return type. +**Ключевой вывод**: `?` вместе с `.map_err()` — это аналог в Rust конструкции `try/except ValueError as e: raise ConfigError(...) from e`. Каждый путь ошибки виден в типе возврата.
*** - From 2a73e17f3f80fb3fa1e2ab73017dec440f45b0fb Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 09:18:07 +0400 Subject: [PATCH 11/18] Translate Python book chapters 10-13 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings in python-book/src for ch10 through ch13. Code identifiers, crate names, file names and links are unchanged. TOC anchors in ch00 for chapter 11 are updated to match the Russian headings. Co-Authored-By: Claude Haiku 5.5 --- python-book/src/ch00-introduction.md | 4 +- python-book/src/ch10-traits-and-generics.md | 247 +++++++------- python-book/src/ch11-from-and-into-traits.md | 195 ++++++----- .../src/ch12-closures-and-iterators.md | 275 ++++++++-------- python-book/src/ch13-concurrency.md | 308 +++++++++--------- 5 files changed, 506 insertions(+), 523 deletions(-) diff --git a/python-book/src/ch00-introduction.md b/python-book/src/ch00-introduction.md index 496987b..4fc1d8a 100644 --- a/python-book/src/ch00-introduction.md +++ b/python-book/src/ch00-introduction.md @@ -98,8 +98,8 @@ #### 11. Трейты From и Into 🟡 - [Преобразование типов в Rust](ch11-from-and-into-traits.md#преобразование-типов-в-rust) -- [From, Into, TryFrom](ch11-from-and-into-traits.md#трейты-from-into) -- [Паттерны преобразования строк](ch11-from-and-into-traits.md#преобразования-строк) +- [From, Into, TryFrom](ch11-from-and-into-traits.md#rust-frominto) +- [Паттерны преобразования строк](ch11-from-and-into-traits.md#паттерны-преобразования-строк) #### 12. Замыкания и итераторы 🟡 - [Замыкания и лямбды](ch12-closures-and-iterators.md#замыкания-rust-и-лямбды-python) diff --git a/python-book/src/ch10-traits-and-generics.md b/python-book/src/ch10-traits-and-generics.md index 56a7dfe..6dbc075 100644 --- a/python-book/src/ch10-traits-and-generics.md +++ b/python-book/src/ch10-traits-and-generics.md @@ -1,19 +1,16 @@ -## Traits vs Duck Typing +## Трейты и утиная типизация -> **What you'll learn:** Traits as explicit contracts (vs Python duck typing), `Protocol` (PEP 544) ≈ Trait, -> generic type bounds with `where` clauses, trait objects (`dyn Trait`) vs static dispatch, and common std traits. +> **Что вы узнаете:** трейты как явные контракты (в отличие от утиной типизации Python), `Protocol` (PEP 544) ≈ трейт, ограничения обобщений через предложения `where`, трейт-объекты (`dyn Trait`) в сравнении со статической диспетчеризацией и распространённые трейты стандартной библиотеки. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -This is where Rust's type system really shines for Python developers. Python's -"duck typing" says: "if it walks like a duck and quacks like a duck, it's a duck." -Rust's traits say: "I'll tell you exactly which duck behaviors I need, at compile time." +Именно здесь система типов Rust по-настоящему раскрывается для разработчиков на Python. Утиная типизация Python говорит: «если это ходит как утка и крякает как утка, значит, это утка». Трейты Rust говорят: «я точно скажу, какое поведение утки мне нужно, и это будет проверено на этапе компиляции». -### Python Duck Typing +### Утиная типизация в Python ```python -# Python — duck typing: anything with the right methods works +# Python — утиная типизация: подходит всё, у чего есть нужные методы def total_area(shapes): - """Works with anything that has an .area() method.""" + """Работает с любым объектом, у которого есть метод .area().""" return sum(shape.area() for shape in shapes) class Circle: @@ -24,23 +21,23 @@ class Rectangle: def __init__(self, w, h): self.w, self.h = w, h def area(self): return self.w * self.h -# Works at runtime — no inheritance needed! +# Работает во время выполнения, наследование не нужно! shapes = [Circle(5), Rectangle(3, 4)] print(total_area(shapes)) # 90.54 -# But what if something doesn't have .area()? +# А что, если у объекта нет .area()? class Dog: - def bark(self): return "Woof!" + def bark(self): return "Гав!" total_area([Dog()]) # 💥 AttributeError: 'Dog' has no attribute 'area' -# Error happens at RUNTIME, not at definition time +# Ошибка возникает во ВРЕМЯ ВЫПОЛНЕНИЯ, а не при определении ``` -### Rust Traits — Explicit Duck Typing +### Трейты Rust: явная утиная типизация ```rust -// Rust — traits make the "duck" contract explicit +// Rust — трейты делают контракт «утки» явным trait HasArea { - fn area(&self) -> f64; // Any type that implements this trait has .area() + fn area(&self) -> f64; // Любой тип, реализующий этот трейт, имеет .area() } struct Circle { radius: f64 } @@ -58,32 +55,30 @@ impl HasArea for Rectangle { } } -// The trait constraint is explicit — compiler checks at compile time +// Ограничение трейтом явное, компилятор проверяет его на этапе компиляции fn total_area(shapes: &[&dyn HasArea]) -> f64 { shapes.iter().map(|s| s.area()).sum() } -// Using it: +// Использование: let shapes: Vec<&dyn HasArea> = vec![&Circle { radius: 5.0 }, &Rectangle { width: 3.0, height: 4.0 }]; println!("{}", total_area(&shapes)); // 90.54 // struct Dog; -// total_area(&[&Dog {}]); // ❌ Compile error: Dog doesn't implement HasArea +// total_area(&[&Dog {}]); // ❌ Ошибка компиляции: Dog не реализует HasArea ``` -> **Key insight**: Python's duck typing defers errors to runtime. Rust's traits catch -> them at compile time. Same flexibility, earlier error detection. +> **Ключевая мысль**: утиная типизация Python откладывает ошибки до времени выполнения. Трейты Rust ловят их на этапе компиляции. Гибкость та же, а ошибки обнаруживаются раньше. *** -## Protocols (PEP 544) vs Traits +## Протоколы (PEP 544) и трейты -Python 3.8 introduced `Protocol` (PEP 544) for structural subtyping — it's the -closest Python concept to Rust traits. +Python 3.8 ввёл `Protocol` (PEP 544) для структурной типизации: это ближайшее к трейтам Rust понятие в Python. -### Python Protocol +### Protocol в Python ```python -# Python — Protocol (structural typing, like Rust traits) +# Python — Protocol (структурная типизация, как трейты Rust) from typing import Protocol, runtime_checkable @runtime_checkable @@ -107,16 +102,16 @@ def print_all(items: list[Printable]) -> None: for item in items: print(item.to_string()) -# Works because User and Product both have to_string() +# Работает, потому что у User и Product есть to_string() print_all([User("Alice"), Product("Widget", 9.99)]) -# BUT: mypy checks this, Python runtime does NOT enforce it -# print_all([42]) # mypy warns, but Python runs it and crashes +# НО: mypy это проверяет, а среда выполнения Python — НЕТ +# print_all([42]) # mypy предупредит, но Python запустит это и упадёт ``` -### Rust Trait (Equivalent, but enforced!) +### Трейт Rust (аналог, но с принуждением!) ```rust -// Rust — traits are enforced at compile time +// Rust — трейты проверяются на этапе компиляции trait Printable { fn to_string(&self) -> String; } @@ -142,28 +137,28 @@ fn print_all(items: &[&dyn Printable]) { } } -// print_all(&[&42i32]); // ❌ Compile error: i32 doesn't implement Printable +// print_all(&[&42i32]); // ❌ Ошибка компиляции: i32 не реализует Printable ``` -### Comparison Table +### Сравнительная таблица -| Feature | Python Protocol | Rust Trait | -|---------|-----------------|------------| -| Structural typing | ✅ (implicit) | ❌ (explicit `impl`) | -| Checked at | Runtime (or mypy) | Compile time (always) | -| Default implementations | ❌ | ✅ | -| Can add to foreign types | ❌ | ✅ (within limits) | -| Multiple protocols | ✅ | ✅ (multiple traits) | -| Associated types | ❌ | ✅ | -| Generic constraints | ✅ (with `TypeVar`) | ✅ (trait bounds) | +| Свойство | Protocol в Python | Трейт Rust | +|----------|-------------------|------------| +| Структурная типизация | ✅ (неявно) | ❌ (нужен явный `impl`) | +| Когда проверяется | Во время выполнения (или mypy) | На этапе компиляции (всегда) | +| Реализации по умолчанию | ❌ | ✅ | +| Можно добавить к чужим типам | ❌ | ✅ (в определённых пределах) | +| Несколько протоколов | ✅ | ✅ (несколько трейтов) | +| Ассоциированные типы | ❌ | ✅ | +| Ограничения обобщений | ✅ (с `TypeVar`) | ✅ (ограничения трейтов) | *** -## Generic Constraints +## Ограничения обобщений -### Python Generics +### Обобщения в Python ```python -# Python — TypeVar for generic functions +# Python — TypeVar для обобщённых функций from typing import TypeVar, Sequence T = TypeVar('T') @@ -171,7 +166,7 @@ T = TypeVar('T') def first(items: Sequence[T]) -> T | None: return items[0] if items else None -# Bounded TypeVar +# Ограниченный TypeVar from typing import SupportsFloat T = TypeVar('T', bound=SupportsFloat) @@ -179,77 +174,76 @@ def average(items: Sequence[T]) -> float: return sum(float(x) for x in items) / len(items) ``` -### Rust Generics with Trait Bounds +### Обобщения Rust с ограничениями трейтов ```rust -// Rust — generics with trait bounds +// Rust — обобщения с ограничениями трейтов fn first(items: &[T]) -> Option<&T> { items.first() } -// With trait bounds — "T must implement these traits" +// С ограничениями трейтов — «T должен реализовывать эти трейты» fn average(items: &[T]) -> f64 where - T: Into + Copy, // T must convert to f64 and be copyable + T: Into + Copy, // T должен преобразовываться в f64 и быть копируемым { let sum: f64 = items.iter().map(|&x| x.into()).sum(); sum / items.len() as f64 } -// Multiple bounds — "T must implement Display AND Debug AND Clone" +// Несколько ограничений — «T должен реализовывать Display, Debug и Clone» fn log_and_clone(item: &T) -> T { println!("Display: {}", item); println!("Debug: {:?}", item); item.clone() } -// Shorthand with impl Trait (for simple cases) +// Краткая запись с impl Trait (для простых случаев) fn print_it(item: &impl std::fmt::Display) { println!("{}", item); } ``` -### Generics Quick Reference +### Краткая справка по обобщениям -| Python | Rust | Notes | -|--------|------|-------| -| `TypeVar('T')` | `` | Unbounded generic | -| `TypeVar('T', bound=X)` | `` | Bounded generic | -| `Union[int, str]` | `enum` or trait object | Rust has no union types | -| `Sequence[T]` | `&[T]` (slice) | Borrowed sequence | -| `Callable[[A], R]` | `Fn(A) -> R` | Function trait | -| `Optional[T]` | `Option` | Built into the language | +| Python | Rust | Примечания | +|--------|------|------------| +| `TypeVar('T')` | `` | Обобщённый тип без ограничений | +| `TypeVar('T', bound=X)` | `` | Обобщённый тип с ограничением | +| `Union[int, str]` | `enum` или трейт-объект | В Rust нет объединений типов | +| `Sequence[T]` | `&[T]` (срез) | Заимствованная последовательность | +| `Callable[[A], R]` | `Fn(A) -> R` | Трейт функции | +| `Optional[T]` | `Option` | Встроено в язык | *** -## Common Standard Library Traits +## Распространённые трейты стандартной библиотеки -These are Rust's version of Python's "dunder methods" — they define how types -behave in common situations. +Это аналоги «магических методов» Python в Rust: они определяют, как типы ведут себя в типичных ситуациях. -### Display and Debug (Printing) +### Display и Debug (вывод) ```rust use std::fmt; -// Debug — like __repr__ (auto-derivable) +// Debug — как __repr__ (можно получить через derive) #[derive(Debug)] struct Point { x: f64, y: f64 } -// Now you can: println!("{:?}", point); +// Теперь можно: println!("{:?}", point); -// Display — like __str__ (must implement manually) +// Display — как __str__ (реализуется вручную) impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } -// Now you can: println!("{}", point); +// Теперь можно: println!("{}", point); ``` -### Comparison Traits +### Трейты сравнения ```rust -// PartialEq — like __eq__ -// Eq — total equality (f64 is PartialEq but not Eq because NaN != NaN) -// PartialOrd — like __lt__, __le__, etc. -// Ord — total ordering +// PartialEq — как __eq__ +// Eq — полное равенство (f64 реализует PartialEq, но не Eq, потому что NaN != NaN) +// PartialOrd — как __lt__, __le__ и т. д. +// Ord — полный порядок #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] struct Student { @@ -257,68 +251,68 @@ struct Student { grade: i32, } -// Now students can be: compared, sorted, used as HashMap keys, cloned +// Теперь студентов можно сравнивать, сортировать, использовать как ключи HashMap и клонировать let mut students = vec![ Student { name: "Charlie".into(), grade: 85 }, Student { name: "Alice".into(), grade: 92 }, ]; -students.sort(); // Uses Ord — sorts by name then grade (struct field order) +students.sort(); // Использует Ord: сортирует по имени, затем по оценке (порядок полей структуры) ``` -### Iterator Trait +### Трейт Iterator ```rust -// Implementing Iterator — like Python's __iter__/__next__ +// Реализация Iterator — как __iter__/__next__ в Python struct Countdown { value: i32 } impl Iterator for Countdown { - type Item = i32; // What the iterator yields + type Item = i32; // Что выдаёт итератор fn next(&mut self) -> Option { if self.value > 0 { self.value -= 1; Some(self.value + 1) } else { - None // Iteration complete + None // Итерация завершена } } } -// Usage: +// Использование: for n in (Countdown { value: 5 }) { println!("{n}"); // 5, 4, 3, 2, 1 } ``` -### Common Traits at a Glance - -| Rust Trait | Python Equivalent | Purpose | -|-----------|-------------------|---------| -| `Display` | `__str__` | Human-readable string | -| `Debug` | `__repr__` | Debug string (derivable) | -| `Clone` | `copy.deepcopy` | Deep copy | -| `Copy` | (int/float auto-copy) | Implicit copy for simple types | -| `PartialEq` / `Eq` | `__eq__` | Equality comparison | -| `PartialOrd` / `Ord` | `__lt__` etc. | Ordering | -| `Hash` | `__hash__` | Hashable (for dict keys) | -| `Default` | Default `__init__` | Default values | -| `From` / `Into` | `__init__` overloads | Type conversions | -| `Iterator` | `__iter__` / `__next__` | Iteration | -| `Drop` | `__del__` / `__exit__` | Cleanup | -| `Add`, `Sub`, `Mul` | `__add__`, `__sub__`, `__mul__` | Operator overloading | -| `Index` | `__getitem__` | Indexing with `[]` | -| `Deref` | (no equivalent) | Smart pointer dereferencing | -| `Send` / `Sync` | (no equivalent) | Thread safety markers | +### Общие трейты в одном взгляде + +| Трейт Rust | Аналог в Python | Назначение | +|------------|-----------------|------------| +| `Display` | `__str__` | Человекочитаемая строка | +| `Debug` | `__repr__` | Отладочная строка (можно получить через derive) | +| `Clone` | `copy.deepcopy` | Глубокая копия | +| `Copy` | (автокопирование int/float) | Неявное копирование простых типов | +| `PartialEq` / `Eq` | `__eq__` | Сравнение на равенство | +| `PartialOrd` / `Ord` | `__lt__` и др. | Упорядочивание | +| `Hash` | `__hash__` | Хешируемость (для ключей dict) | +| `Default` | Значения по умолчанию в `__init__` | Значения по умолчанию | +| `From` / `Into` | Перегрузки `__init__` | Преобразования типов | +| `Iterator` | `__iter__` / `__next__` | Итерация | +| `Drop` | `__del__` / `__exit__` | Очистка | +| `Add`, `Sub`, `Mul` | `__add__`, `__sub__`, `__mul__` | Перегрузка операторов | +| `Index` | `__getitem__` | Индексация через `[]` | +| `Deref` | (аналога нет) | Разыменование умных указателей | +| `Send` / `Sync` | (аналога нет) | Маркеры потокобезопасности | ```mermaid flowchart TB - subgraph Static ["Static Dispatch (impl Trait)"] - G["fn notify(item: &impl Summary)"] --> M1["Compiled: notify_Article()"] - G --> M2["Compiled: notify_Tweet()"] - M1 --> O1["Inlined, zero-cost"] - M2 --> O2["Inlined, zero-cost"] + subgraph Static ["Статическая диспетчеризация (impl Trait)"] + G["fn notify(item: &impl Summary)"] --> M1["Скомпилировано: notify_Article()"] + G --> M2["Скомпилировано: notify_Tweet()"] + M1 --> O1["Встроено, без затрат"] + M2 --> O2["Встроено, без затрат"] end - subgraph Dynamic ["Dynamic Dispatch (dyn Trait)"] - D["fn notify(item: &dyn Summary)"] --> VT["vtable lookup"] + subgraph Dynamic ["Динамическая диспетчеризация (dyn Trait)"] + D["fn notify(item: &dyn Summary)"] --> VT["Поиск в таблице виртуальных методов (vtable)"] VT --> I1["Article::summarize()"] VT --> I2["Tweet::summarize()"] end @@ -327,16 +321,16 @@ flowchart TB style Dynamic fill:#fff3cd ``` -> **Python equivalent**: Python *always* uses dynamic dispatch (`getattr` at runtime). Rust defaults to static dispatch (monomorphization — the compiler generates specialized code for each concrete type). Use `dyn Trait` only when you need runtime polymorphism. +> **Аналог в Python**: Python *всегда* использует динамическую диспетчеризацию (`getattr` во время выполнения). Rust по умолчанию использует статическую диспетчеризацию (мономорфизацию: компилятор генерирует специализированный код для каждого конкретного типа). `dyn Trait` используйте только тогда, когда нужен полиморфизм во время выполнения. > -> 📌 **See also**: [Ch. 11 — From/Into Traits](ch11-from-and-into-traits.md) covers the conversion traits (`From`, `Into`, `TryFrom`) in depth. +> 📌 **См. также**: [Гл. 11 — Трейты From и Into](ch11-from-and-into-traits.md) подробно описывает трейты преобразования (`From`, `Into`, `TryFrom`). -### Associated Types +### Ассоциированные типы -Rust traits can define *associated types* — type placeholders that each implementor fills in. Python has no equivalent: +Трейты Rust могут определять *ассоциированные типы*: заполнители типов, которые заполняет каждая реализация. У Python нет аналога: ```rust -// Iterator defines an associated type 'Item' +// Iterator определяет ассоциированный тип 'Item' trait Iterator { type Item; fn next(&mut self) -> Option; @@ -345,7 +339,7 @@ trait Iterator { struct Countdown { remaining: u32 } impl Iterator for Countdown { - type Item = u32; // This iterator yields u32 values + type Item = u32; // Этот итератор выдаёт значения u32 fn next(&mut self) -> Option { if self.remaining > 0 { self.remaining -= 1; @@ -357,11 +351,11 @@ impl Iterator for Countdown { } ``` -In Python, `__iter__` / `__next__` return `Any` — there's no way to declare "this iterator yields `int`" and have it enforced (type hints with `Iterator[int]` are advisory only). +В Python `__iter__` / `__next__` возвращают `Any`: нельзя объявить «этот итератор выдаёт `int`» и получить принудительную проверку (подсказки типов `Iterator[int]` носят лишь рекомендательный характер). -### Operator Overloading: `__add__` → `impl Add` +### Перегрузка операторов: `__add__` → `impl Add` -Python uses magic methods (`__add__`, `__mul__`). Rust uses trait implementations — same idea, but type-checked at compile time: +Python использует магические методы (`__add__`, `__mul__`). Rust использует реализации трейтов: та же идея, но с проверкой типов на этапе компиляции: ```python # Python @@ -369,7 +363,7 @@ class Vec2: def __init__(self, x, y): self.x, self.y = x, y def __add__(self, other): - return Vec2(self.x + other.x, self.y + other.y) # No type checking on 'other' + return Vec2(self.x + other.x, self.y + other.y) # Нет проверки типа 'other' ``` ```rust @@ -379,7 +373,7 @@ use std::ops::Add; struct Vec2 { x: f64, y: f64 } impl Add for Vec2 { - type Output = Vec2; // Associated type: what does + return? + type Output = Vec2; // Ассоциированный тип: что возвращает +? fn add(self, rhs: Vec2) -> Vec2 { Vec2 { x: self.x + rhs.x, y: self.y + rhs.y } } @@ -387,22 +381,22 @@ impl Add for Vec2 { let a = Vec2 { x: 1.0, y: 2.0 }; let b = Vec2 { x: 3.0, y: 4.0 }; -let c = a + b; // Type-safe: only Vec2 + Vec2 is allowed +let c = a + b; // Безопасно по типам: допускается только Vec2 + Vec2 ``` -Key difference: Python's `__add__` accepts *any* `other` at runtime (you check types manually or get a `TypeError`). Rust's `Add` trait enforces the operand types at compile time — `Vec2 + i32` is a compile error unless you explicitly `impl Add for Vec2`. +Ключевое отличие: `__add__` в Python принимает *любой* `other` во время выполнения (типы приходится проверять вручную или получить `TypeError`). Трейт `Add` в Rust проверяет типы операндов на этапе компиляции: `Vec2 + i32` — ошибка компиляции, если вы явно не напишете `impl Add for Vec2`. --- -## Exercises +## Упражнения
-🏋️ Exercise: Generic Summary Trait (click to expand) +🏋️ Упражнение: обобщённый трейт Summary (нажмите, чтобы раскрыть) -**Challenge**: Define a trait `Summary` with a method `fn summarize(&self) -> String`. Implement it for two structs: `Article { title: String, body: String }` and `Tweet { username: String, content: String }`. Then write a function `fn notify(item: &impl Summary)` that prints the summary. +**Задание**: определите трейт `Summary` с методом `fn summarize(&self) -> String`. Реализуйте его для двух структур: `Article { title: String, body: String }` и `Tweet { username: String, content: String }`. Затем напишите функцию `fn notify(item: &impl Summary)`, которая выводит сводку.
-🔑 Solution +🔑 Решение ```rust trait Summary { @@ -442,11 +436,10 @@ fn main() { } ``` -**Key takeaway**: `&impl Summary` is the Rust equivalent of Python's `Protocol` with a `summarize` method. But Rust checks it at compile time — passing a type that doesn't implement `Summary` is a compile error, not a runtime `AttributeError`. +**Ключевой вывод**: `&impl Summary` — это аналог `Protocol` из Python с методом `summarize`. Но Rust проверяет это на этапе компиляции: передача типа, который не реализует `Summary`, — ошибка компиляции, а не `AttributeError` во время выполнения.
*** - diff --git a/python-book/src/ch11-from-and-into-traits.md b/python-book/src/ch11-from-and-into-traits.md index 09f77a5..8b4909b 100644 --- a/python-book/src/ch11-from-and-into-traits.md +++ b/python-book/src/ch11-from-and-into-traits.md @@ -1,22 +1,20 @@ -## Type Conversions in Rust +## Преобразование типов в Rust -> **What you'll learn:** `From` and `Into` traits for zero-cost type conversions, `TryFrom` for fallible conversions, -> how `impl From for B` auto-generates `Into`, and string conversion patterns. +> **Что вы узнаете:** трейты `From` и `Into` для преобразований без накладных расходов, `TryFrom` для преобразований, которые могут завершиться ошибкой, как `impl From for B` автоматически создаёт `Into`, и паттерны преобразования строк. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -Python handles type conversions with constructor calls (`int("42")`, `str(42)`, -`float("3.14")`). Rust uses the `From` and `Into` traits for type-safe conversions. +Python выполняет преобразования типов через вызовы конструкторов (`int("42")`, `str(42)`, `float("3.14")`). Rust использует трейты `From` и `Into` для типобезопасных преобразований. -### Python Type Conversion +### Преобразование типов в Python ```python -# Python — explicit constructors for conversion -x = int("42") # str → int (can raise ValueError) +# Python — явные конструкторы для преобразований +x = int("42") # str → int (может выбросить ValueError) s = str(42) # int → str f = float("3.14") # str → float lst = list((1, 2, 3)) # tuple → list -# Custom conversion via __init__ or class methods +# Собственное преобразование через __init__ или методы класса class Celsius: def __init__(self, temp: float): self.temp = temp @@ -30,8 +28,8 @@ c = Celsius.from_fahrenheit(212.0) # 100.0°C ### Rust From/Into ```rust -// Rust — From trait defines conversions -// Implementing From gives you Into automatically! +// Rust — трейт From задаёт преобразования +// Реализация From даёт вам Into автоматически! struct Celsius(f64); struct Fahrenheit(f64); @@ -42,42 +40,42 @@ impl From for Celsius { } } -// Now both work: -let c1 = Celsius::from(Fahrenheit(212.0)); // Explicit From -let c2: Celsius = Fahrenheit(212.0).into(); // Into (automatically derived) +// Теперь работают оба варианта: +let c1 = Celsius::from(Fahrenheit(212.0)); // Явный From +let c2: Celsius = Fahrenheit(212.0).into(); // Into (выводится автоматически) -// String conversions: +// Преобразования строк: let s: String = String::from("hello"); // &str → String -let s: String = "hello".to_string(); // Same thing -let s: String = "hello".into(); // Also works (From is implemented) +let s: String = "hello".to_string(); // То же самое +let s: String = "hello".into(); // Тоже работает (From реализован) -let num: i64 = 42i32.into(); // i32 → i64 (lossless, so From exists) -// let small: i32 = 42i64.into(); // ❌ i64 → i32 might lose data — no From +let num: i64 = 42i32.into(); // i32 → i64 (без потерь, поэтому From есть) +// let small: i32 = 42i64.into(); // ❌ i64 → i32 может потерять данные, From нет -// For fallible conversions, use TryFrom: -let n: Result = "42".parse(); // str → i32 (might fail) -let n: i32 = "42".parse().unwrap(); // Panic if not a number -let n: i32 = "42".parse()?; // Propagate error with ? +// Для преобразований, которые могут завершиться ошибкой, используйте TryFrom: +let n: Result = "42".parse(); // str → i32 (может завершиться ошибкой) +let n: i32 = "42".parse().unwrap(); // Паника, если это не число +let n: i32 = "42".parse()?; // Передать ошибку дальше с помощью ? ``` -### The From/Into Relationship +### Связь From и Into ```mermaid flowchart TB - A["impl From<A> for B"] -->|"auto-generates"| B["impl Into<B> for A"] - C["Celsius::from(Fahrenheit(212.0))"] ---|"same as"| D["Fahrenheit(212.0).into()"] + A["impl From<A> for B"] -->|"генерирует автоматически"| B["impl Into<B> for A"] + C["Celsius::from(Fahrenheit(212.0))"] ---|"то же, что"| D["Fahrenheit(212.0).into()"] style A fill:#d4edda style B fill:#d4edda ``` -> **Rule of thumb**: Always implement `From`, never implement `Into` directly. Implementing `From for B` gives you `Into for A` for free. +> **Правило большого пальца**: всегда реализуйте `From`, а не `Into` напрямую. Реализация `From for B` даёт вам `Into for A` бесплатно. *** -### When to Use From/Into +### Когда использовать From/Into ```rust -// Implement From for your types to enable ergonomic API design: +// Реализуйте From для своих типов, чтобы API было удобнее: #[derive(Debug)] struct UserId(i64); @@ -88,39 +86,39 @@ impl From for UserId { } } -// Now functions can accept anything convertible to UserId: +// Теперь функции могут принимать всё, что можно преобразовать в UserId: fn find_user(id: impl Into) -> Option { let user_id = id.into(); - // ... lookup logic + // ... логика поиска Some(format!("User #{:?}", user_id)) } -find_user(42i64); // ✅ i64 auto-converts to UserId -find_user(UserId(42)); // ✅ UserId stays as-is +find_user(42i64); // ✅ i64 автоматически преобразуется в UserId +find_user(UserId(42)); // ✅ UserId остаётся как есть ``` *** -## TryFrom — Fallible Conversions +## TryFrom: преобразования, которые могут завершиться ошибкой -Not all conversions can succeed. Python raises exceptions; Rust uses `TryFrom` which returns a `Result`: +Не все преобразования могут быть успешными. Python выбрасывает исключения; Rust использует `TryFrom`, который возвращает `Result`: ```python -# Python — fallible conversions raise exceptions +# Python — преобразования, которые могут завершиться ошибкой, выбрасывают исключения try: port = int("not_a_number") # ValueError except ValueError as e: - print(f"Invalid: {e}") + print(f"Некорректно: {e}") -# Custom validation in __init__ +# Собственная валидация в __init__ class Port: def __init__(self, value: int): if not (1 <= value <= 65535): - raise ValueError(f"Invalid port: {value}") + raise ValueError(f"Некорректный порт: {value}") self.value = value try: - p = Port(99999) # ValueError at runtime + p = Port(99999) # ValueError во время выполнения except ValueError: pass ``` @@ -128,11 +126,11 @@ except ValueError: ```rust use std::num::ParseIntError; -// TryFrom for built-in types +// TryFrom для встроенных типов let n: Result = "42".try_into(); // Ok(42) let n: Result = "bad".try_into(); // Err(...) -// Custom TryFrom for validation +// Собственный TryFrom для валидации #[derive(Debug)] struct Port(u16); @@ -155,45 +153,45 @@ impl TryFrom for Port { impl std::fmt::Display for PortError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PortError::Zero => write!(f, "port cannot be zero"), + PortError::Zero => write!(f, "порт не может быть нулём"), } } } -// Usage: +// Использование: let p: Result = 8080u16.try_into(); // Ok(Port(8080)) let p: Result = 0u16.try_into(); // Err(PortError::Zero) ``` -> **Python → Rust mental model**: `TryFrom` = `__init__` that validates and can fail. But instead of raising an exception, it returns `Result` — so callers **must** handle the error case. +> **Ментальная модель Python → Rust**: `TryFrom` — это `__init__`, который проверяет данные и может завершиться ошибкой. Но вместо выбрасывания исключения он возвращает `Result`, поэтому вызывающий код **обязан** обработать случай ошибки. *** -## String Conversion Patterns +## Паттерны преобразования строк -Strings are the most common source of conversion confusion for Python developers: +Строки — самый частый источник путаницы при преобразованиях для разработчиков на Python: ```rust -// String → &str (borrowing, free) +// String → &str (заимствование, бесплатно) let s = String::from("hello"); -let r: &str = &s; // Automatic Deref coercion -let r: &str = s.as_str(); // Explicit +let r: &str = &s; // Автоматическое приведение через Deref +let r: &str = s.as_str(); // Явно -// &str → String (allocating, costs memory) +// &str → String (выделение памяти, стоит ресурсов) let r: &str = "hello"; -let s1 = String::from(r); // From trait -let s2 = r.to_string(); // ToString trait (via Display) -let s3: String = r.into(); // Into trait +let s1 = String::from(r); // Трейт From +let s2 = r.to_string(); // Трейт ToString (через Display) +let s3: String = r.into(); // Трейт Into -// Number → String -let s = 42.to_string(); // "42" — like Python's str(42) -let s = format!("{:.2}", 3.14); // "3.14" — like Python's f"{3.14:.2f}" +// Число → String +let s = 42.to_string(); // "42" — как str(42) в Python +let s = format!("{:.2}", 3.14); // "3.14" — как f"{3.14:.2f}" в Python -// String → Number -let n: i32 = "42".parse().unwrap(); // like Python's int("42") -let f: f64 = "3.14".parse().unwrap(); // like Python's float("3.14") +// Строка → число +let n: i32 = "42".parse().unwrap(); // как int("42") в Python +let f: f64 = "3.14".parse().unwrap(); // как float("3.14") в Python -// Custom types → String (implement Display) +// Собственные типы → String (реализуйте Display) use std::fmt; struct Point { x: f64, y: f64 } @@ -205,54 +203,54 @@ impl fmt::Display for Point { } let p = Point { x: 1.0, y: 2.0 }; -println!("{p}"); // (1, 2) — like Python's __str__ -let s = p.to_string(); // Also works! Display gives you ToString for free. +println!("{p}"); // (1, 2) — как __str__ в Python +let s = p.to_string(); // Тоже работает! Display даёт ToString бесплатно. ``` -### Conversion Quick Reference +### Краткая справка по преобразованиям -| Python | Rust | Notes | -|--------|------|-------| -| `str(x)` | `x.to_string()` | Requires `Display` impl | -| `int("42")` | `"42".parse::()` | Returns `Result` | -| `float("3.14")` | `"3.14".parse::()` | Returns `Result` | -| `list(iter)` | `iter.collect::>()` | Type annotation needed | -| `dict(pairs)` | `pairs.collect::>()` | Type annotation needed | -| `bool(x)` | No direct equivalent | Use explicit checks | -| `MyClass(x)` | `MyClass::from(x)` | Implement `From` | -| `MyClass(x)` (validates) | `MyClass::try_from(x)?` | Implement `TryFrom` | +| Python | Rust | Примечания | +|--------|------|------------| +| `str(x)` | `x.to_string()` | Требует реализации `Display` | +| `int("42")` | `"42".parse::()` | Возвращает `Result` | +| `float("3.14")` | `"3.14".parse::()` | Возвращает `Result` | +| `list(iter)` | `iter.collect::>()` | Нужна аннотация типа | +| `dict(pairs)` | `pairs.collect::>()` | Нужна аннотация типа | +| `bool(x)` | Прямого аналога нет | Используйте явные проверки | +| `MyClass(x)` | `MyClass::from(x)` | Реализуйте `From` | +| `MyClass(x)` (с валидацией) | `MyClass::try_from(x)?` | Реализуйте `TryFrom` | *** -## Conversion Chains and Error Handling +## Цепочки преобразований и обработка ошибок -Real-world code often chains multiple conversions. Compare the approaches: +В реальном коде часто встречаются цепочки из нескольких преобразований. Сравните подходы: ```python -# Python — chain of conversions with try/except +# Python — цепочка преобразований с try/except def parse_config(raw: str) -> tuple[str, int]: try: host, port_str = raw.split(":") port = int(port_str) if not (1 <= port <= 65535): - raise ValueError(f"Bad port: {port}") + raise ValueError(f"Некорректный порт: {port}") return (host, port) except (ValueError, AttributeError) as e: - raise ConfigError(f"Invalid config: {e}") from e + raise ConfigError(f"Некорректная конфигурация: {e}") from e ``` ```rust fn parse_config(raw: &str) -> Result<(String, u16), String> { let (host, port_str) = raw .split_once(':') - .ok_or_else(|| "missing ':' separator".to_string())?; + .ok_or_else(|| "отсутствует разделитель ':'".to_string())?; let port: u16 = port_str .parse() - .map_err(|e| format!("invalid port: {e}"))?; + .map_err(|e| format!("некорректный порт: {e}"))?; if port == 0 { - return Err("port cannot be zero".to_string()); + return Err("порт не может быть нулём".to_string()); } Ok((host.to_string(), port)) @@ -260,31 +258,31 @@ fn parse_config(raw: &str) -> Result<(String, u16), String> { fn main() { match parse_config("localhost:8080") { - Ok((host, port)) => println!("Connecting to {host}:{port}"), - Err(e) => eprintln!("Config error: {e}"), + Ok((host, port)) => println!("Подключение к {host}:{port}"), + Err(e) => eprintln!("Ошибка конфигурации: {e}"), } } ``` -> **Key insight**: Each `?` is a visible exit point. In Python, any line inside `try` could be the one that throws — in Rust, only lines ending with `?` can fail. +> **Ключевая мысль**: каждый `?` — это видимая точка выхода. В Python любая строка внутри `try` может оказаться той, что выбросит исключение, а в Rust ошибиться могут только строки, оканчивающиеся на `?`. > -> 📌 **See also**: [Ch. 9 — Error Handling](ch09-error-handling.md) covers `Result`, `?`, and custom error types with `thiserror` in depth. +> 📌 **См. также**: [Гл. 9 — Обработка ошибок](ch09-error-handling.md) подробно описывает `Result`, `?` и собственные типы ошибок с `thiserror`. --- -## Exercises +## Упражнения
-🏋️ Exercise: Temperature Conversion Library (click to expand) +🏋️ Упражнение: библиотека преобразования температур (нажмите, чтобы раскрыть) -**Challenge**: Build a mini temperature conversion library: -1. Define `Celsius(f64)`, `Fahrenheit(f64)`, and `Kelvin(f64)` structs -2. Implement `From for Fahrenheit` and `From for Kelvin` -3. Implement `TryFrom for Kelvin` that rejects values below absolute zero (-273.15°C = 0K) -4. Implement `Display` for all three types (e.g., `"100.00°C"`) +**Задание**: создайте мини-библиотеку преобразования температур: +1. Определите структуры `Celsius(f64)`, `Fahrenheit(f64)` и `Kelvin(f64)` +2. Реализуйте `From for Fahrenheit` и `From for Kelvin` +3. Реализуйте `TryFrom for Kelvin`, который отклоняет значения ниже абсолютного нуля (-273.15°C = 0K) +4. Реализуйте `Display` для всех трёх типов (например, `"100.00°C"`)
-🔑 Solution +🔑 Решение ```rust use std::fmt; @@ -310,7 +308,7 @@ struct BelowAbsoluteZero; impl fmt::Display for BelowAbsoluteZero { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "temperature below absolute zero") + write!(f, "температура ниже абсолютного нуля") } } @@ -338,16 +336,15 @@ fn main() { match Kelvin::try_from(-10.0) { Ok(k) => println!("{k}"), - Err(e) => println!("Error: {e}"), + Err(e) => println!("Ошибка: {e}"), } } ``` -**Key takeaway**: `From` handles infallible conversions (Celsius→Fahrenheit always works). `TryFrom` handles fallible ones (negative Kelvin is impossible). Python conflates both in `__init__` — Rust makes the distinction explicit in the type system. +**Ключевой вывод**: `From` отвечает за преобразования, которые всегда успешны (Celsius → Fahrenheit работает всегда). `TryFrom` отвечает за те, которые могут не удаться (отрицательный Kelvin невозможен). Python смешивает оба варианта в `__init__`, а Rust делает различие явным в системе типов.
*** - diff --git a/python-book/src/ch12-closures-and-iterators.md b/python-book/src/ch12-closures-and-iterators.md index d32650d..3e8ae63 100644 --- a/python-book/src/ch12-closures-and-iterators.md +++ b/python-book/src/ch12-closures-and-iterators.md @@ -1,52 +1,51 @@ -## Rust Closures vs Python Lambdas +## Замыкания Rust и лямбды Python -> **What you'll learn:** Multi-line closures (not just one-expression lambdas), `Fn`/`FnMut`/`FnOnce` capture semantics, -> iterator chains vs list comprehensions, `map`/`filter`/`fold`, and `macro_rules!` basics. +> **Что вы узнаете:** многострочные замыкания (а не только однострочные лямбды), семантику захвата `Fn`/`FnMut`/`FnOnce`, цепочки итераторов в сравнении со списочными включениями, `map`/`filter`/`fold` и основы `macro_rules!`. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Python Closures and Lambdas +### Замыкания и лямбды в Python ```python -# Python — lambdas are one-expression anonymous functions +# Python — лямбды это анонимные функции из одного выражения double = lambda x: x * 2 result = double(5) # 10 -# Full closures capture variables from enclosing scope: +# Полноценные замыкания захватывают переменные из окружающей области видимости: def make_adder(n): def adder(x): - return x + n # Captures `n` from outer scope + return x + n # Захватывает `n` из внешней области видимости return adder add_5 = make_adder(5) print(add_5(10)) # 15 -# Higher-order functions: +# Функции высшего порядка: numbers = [1, 2, 3, 4, 5] doubled = list(map(lambda x: x * 2, numbers)) evens = list(filter(lambda x: x % 2 == 0, numbers)) ``` -### Rust Closures +### Замыкания в Rust ```rust -// Rust — closures use |args| body syntax +// Rust — замыкания используют синтаксис |аргументы| тело let double = |x: i32| x * 2; let result = double(5); // 10 -// Closures capture variables from enclosing scope: +// Замыкания захватывают переменные из окружающей области видимости: fn make_adder(n: i32) -> impl Fn(i32) -> i32 { - move |x| x + n // `move` transfers ownership of `n` into the closure + move |x| x + n // `move` передаёт владение `n` внутрь замыкания } let add_5 = make_adder(5); println!("{}", add_5(10)); // 15 -// Higher-order functions with iterators: +// Функции высшего порядка с итераторами: let numbers = vec![1, 2, 3, 4, 5]; let doubled: Vec = numbers.iter().map(|x| x * 2).collect(); let evens: Vec = numbers.iter().filter(|&&x| x % 2 == 0).copied().collect(); ``` -### Closure Syntax Comparison +### Сравнение синтаксиса замыканий ```text Python: Rust: ───────── ───── @@ -54,78 +53,78 @@ lambda x: x * 2 |x| x * 2 lambda x, y: x + y |x, y| x + y lambda: 42 || 42 -# Multi-line +# Многострочное def f(x): |x| { y = x * 2 let y = x * 2; return y + 1 y + 1 } ``` -### Closure Capture — How Rust Differs +### Захват в замыканиях: чем отличается Rust ```python -# Python — closures capture by reference (late binding!) +# Python — замыкания захватывают по ссылке (позднее связывание!) funcs = [lambda: i for i in range(3)] -print([f() for f in funcs]) # [2, 2, 2] — surprise! All captured the same `i` +print([f() for f in funcs]) # [2, 2, 2] — сюрприз! Все захватили один и тот же `i` -# Fix with default arg trick: +# Исправление через трюк со значением по умолчанию: funcs = [lambda i=i: i for i in range(3)] print([f() for f in funcs]) # [0, 1, 2] ``` ```rust -// Rust — closures capture correctly (no late-binding gotcha) +// Rust — замыкания захватывают корректно (ловушки позднего связывания нет) let funcs: Vec i32>> = (0..3) .map(|i| Box::new(move || i) as Box i32>) .collect(); let results: Vec = funcs.iter().map(|f| f()).collect(); -println!("{:?}", results); // [0, 1, 2] — correct! +println!("{:?}", results); // [0, 1, 2] — правильно! -// `move` captures a COPY of `i` for each closure — no late-binding surprise. +// `move` захватывает КОПИЮ `i` для каждого замыкания, никаких сюрпризов позднего связывания. ``` -### Three Closure Traits +### Три трейта замыканий ```rust -// Rust closures implement one or more of these traits: +// Замыкания Rust реализуют один или несколько из этих трейтов: -// Fn — can be called multiple times, doesn't mutate captures (most common) +// Fn — можно вызывать много раз, не изменяет захваченное (самый распространённый) fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) } -// FnMut — can be called multiple times, MAY mutate captures +// FnMut — можно вызывать много раз, МОЖЕТ изменять захваченное fn apply_mut(mut f: impl FnMut(i32) -> i32, x: i32) -> i32 { f(x) } -// FnOnce — can only be called ONCE (consumes captures) +// FnOnce — можно вызвать ТОЛЬКО ОДИН раз (потребляет захваченное) fn apply_once(f: impl FnOnce() -> String) -> String { f() } -// Python has no equivalent — closures are always Fn-like. -// In Rust, the compiler automatically determines which trait to use. +// В Python аналога нет: замыкания всегда ведут себя как Fn. +// В Rust компилятор сам определяет, какой трейт использовать. ``` *** -## Iterators vs Generators +## Итераторы и генераторы -### Python Generators +### Генераторы в Python ```python -# Python — generators with yield +# Python — генераторы с yield def fibonacci(): a, b = 0, 1 while True: yield a a, b = b, a + b -# Lazy — values computed on demand +# Ленивые: значения вычисляются по требованию fib = fibonacci() first_10 = [next(fib) for _ in range(10)] -# Generator expressions — like lazy list comprehensions -squares = (x ** 2 for x in range(1000000)) # No memory allocation +# Генераторные выражения — как ленивые списочные включения +squares = (x ** 2 for x in range(1000000)) # Без выделения памяти first_5 = [next(squares) for _ in range(5)] ``` -### Rust Iterators +### Итераторы Rust ```rust -// Rust — Iterator trait (similar concept, different syntax) +// Rust — трейт Iterator (похожая концепция, другой синтаксис) struct Fibonacci { a: u64, b: u64, @@ -148,22 +147,22 @@ impl Iterator for Fibonacci { } } -// Lazy — values computed on demand (just like Python generators) +// Ленивые: значения вычисляются по требованию (как генераторы Python) let first_10: Vec = Fibonacci::new().take(10).collect(); -// Iterator chains — like generator expressions +// Цепочки итераторов — как генераторные выражения let squares: Vec = (0..1_000_000u64).map(|x| x * x).take(5).collect(); ``` *** -## Comprehensions vs Iterator Chains +## Списочные включения и цепочки итераторов -This section maps Python's comprehension syntax to Rust's iterator chains. +Этот раздел сопоставляет синтаксис списочных включений Python с цепочками итераторов Rust. -### List Comprehension → map/filter/collect +### Списочные включения → map/filter/collect ```python -# Python comprehensions: +# Списочные включения в Python: squares = [x ** 2 for x in range(10)] evens = [x for x in range(20) if x % 2 == 0] names = [user.name for user in users if user.active] @@ -173,7 +172,7 @@ flat = [item for sublist in nested for item in sublist] ```mermaid flowchart LR - A["Source
[1,2,3,4,5]"] -->|".iter()"| B["Iterator"] + A["Источник
[1,2,3,4,5]"] -->|".iter()"| B["Итератор"] B -->|".filter(x: x%2==0)"| C["[2, 4]"] C -->|".map(x: x*x)"| D["[4, 16]"] D -->|".collect()"| E["Vec<i32>
[4, 16]"] @@ -181,10 +180,10 @@ flowchart LR style E fill:#d4edda ``` -> **Key insight**: Rust iterators are lazy — nothing happens until `.collect()`. Python's generators work similarly, but list comprehensions evaluate eagerly. +> **Ключевая мысль**: итераторы Rust ленивые: ничего не происходит до `.collect()`. Генераторы Python работают похоже, но списочные включения вычисляются сразу (энергично). ```rust -// Rust iterator chains: +// Цепочки итераторов Rust: let squares: Vec = (0..10).map(|x| x * x).collect(); let evens: Vec = (0..20).filter(|x| x % 2 == 0).collect(); let names: Vec<&str> = users.iter() @@ -199,7 +198,7 @@ let flat: Vec = nested.iter() .collect(); ``` -### Dict Comprehension → collect into HashMap +### Словарное включение → collect в HashMap ```python # Python word_lengths = {word: len(word) for word in words} @@ -216,7 +215,7 @@ let inverted: HashMap<&V, &K> = mapping.iter() .collect(); ``` -### Set Comprehension → collect into HashSet +### Включение множества → collect в HashSet ```python # Python unique_lengths = {len(word) for word in words} @@ -229,64 +228,62 @@ let unique_lengths: HashSet = words.iter() .collect(); ``` -### Common Iterator Methods - -| Python | Rust | Notes | -|--------|------|-------| -| `map(f, iter)` | `.map(f)` | Transform each element | -| `filter(f, iter)` | `.filter(f)` | Keep matching elements | -| `sum(iter)` | `.sum()` | Sum all elements | -| `min(iter)` / `max(iter)` | `.min()` / `.max()` | Returns `Option` | -| `any(f(x) for x in iter)` | `.any(f)` | True if any match | -| `all(f(x) for x in iter)` | `.all(f)` | True if all match | -| `enumerate(iter)` | `.enumerate()` | Index + value | -| `zip(a, b)` | `a.zip(b)` | Pair elements | -| `len(list)` | `.count()` (consumes!) or `.len()` | Count elements | -| `list(reversed(x))` | `.rev()` | Reverse iteration | -| `itertools.chain(a, b)` | `a.chain(b)` | Concatenate iterators | -| `next(iter)` | `.next()` | Get next element | -| `next(iter, default)` | `.next().unwrap_or(default)` | With default | -| `list(iter)` | `.collect::>()` | Materialize into collection | -| `sorted(iter)` | Collect, then `.sort()` | No lazy sorted iterator | -| `functools.reduce(f, iter)` | `.fold(init, f)` or `.reduce(f)` | Accumulate | - -### Key Differences +### Частые методы итераторов + +| Python | Rust | Примечания | +|--------|------|------------| +| `map(f, iter)` | `.map(f)` | Преобразует каждый элемент | +| `filter(f, iter)` | `.filter(f)` | Оставляет подходящие элементы | +| `sum(iter)` | `.sum()` | Суммирует все элементы | +| `min(iter)` / `max(iter)` | `.min()` / `.max()` | Возвращает `Option` | +| `any(f(x) for x in iter)` | `.any(f)` | True, если хотя бы один подходит | +| `all(f(x) for x in iter)` | `.all(f)` | True, если подходят все | +| `enumerate(iter)` | `.enumerate()` | Индекс и значение | +| `zip(a, b)` | `a.zip(b)` | Попарное объединение элементов | +| `len(list)` | `.count()` (потребляет итератор!) или `.len()` | Подсчёт элементов | +| `list(reversed(x))` | `.rev()` | Обратный обход | +| `itertools.chain(a, b)` | `a.chain(b)` | Соединение итераторов | +| `next(iter)` | `.next()` | Получить следующий элемент | +| `next(iter, default)` | `.next().unwrap_or(default)` | С значением по умолчанию | +| `list(iter)` | `.collect::>()` | Материализация в коллекцию | +| `sorted(iter)` | Собрать, затем `.sort()` | Ленивого итератора сортировки нет | +| `functools.reduce(f, iter)` | `.fold(init, f)` или `.reduce(f)` | Накопление | + +### Ключевые различия ```text -Python iterators: Rust iterators: +Итераторы Python: Итераторы Rust: ───────────────── ────────────── -- Lazy by default (generators) - Lazy by default (all iterator chains) -- yield creates generators - impl Iterator { fn next() } -- StopIteration to end - None to end -- Can be consumed once - Can be consumed once -- No type safety - Fully type-safe -- Slightly slower (interpreter) - Zero-cost (compiled away) +- Ленивые по умолчанию (генераторы) - Ленивые по умолчанию (все цепочки итераторов) +- yield создаёт генераторы - impl Iterator { fn next() } +- StopIteration для завершения - None для завершения +- Потребляются один раз - Потребляются один раз +- Нет типовой безопасности - Полностью типобезопасны +- Немного медленнее (интерпретатор) - Без накладных расходов (компилируется) ``` *** - -## Why Macros Exist in Rust +## Зачем нужны макросы в Rust -Python has no macro system — it uses decorators, metaclasses, and runtime -introspection for metaprogramming. Rust uses macros for compile-time code generation. +В Python нет системы макросов: для метапрограммирования используются декораторы, метаклассы и интроспекция во время выполнения. Rust использует макросы для генерации кода на этапе компиляции. -### Python Metaprogramming vs Rust Macros +### Метапрограммирование в Python и макросы Rust ```python -# Python — decorators and metaclasses for metaprogramming +# Python — декораторы и метаклассы для метапрограммирования from dataclasses import dataclass from functools import wraps -@dataclass # Generates __init__, __repr__, __eq__ at import time +@dataclass # Генерирует __init__, __repr__, __eq__ во время импорта class Point: x: float y: float -# Custom decorator +# Собственный декоратор def log_calls(func): @wraps(func) def wrapper(*args, **kwargs): - print(f"Calling {func.__name__}") + print(f"Вызов {func.__name__}") return func(*args, **kwargs) return wrapper @@ -296,18 +293,18 @@ def process(data): ``` ```rust -// Rust — derive macros and declarative macros for code generation -#[derive(Debug, Clone, PartialEq)] // Generates Debug, Clone, PartialEq impls at COMPILE time +// Rust — производные макросы и декларативные макросы для генерации кода +#[derive(Debug, Clone, PartialEq)] // Генерирует реализации Debug, Clone, PartialEq на этапе КОМПИЛЯЦИИ struct Point { x: f64, y: f64, } -// Declarative macro (like a template) +// Декларативный макрос (как шаблон) macro_rules! log_call { ($func_name:expr, $body:expr) => { { - println!("Calling {}", $func_name); + println!("Вызов {}", $func_name); $body } }; @@ -318,30 +315,30 @@ fn process(data: &str) -> String { } ``` -### Common Built-in Macros +### Частые встроенные макросы ```rust -// These macros are used everywhere in Rust: - -println!("Hello, {}!", name); // Print with formatting -format!("Value: {}", x); // Create formatted String -vec![1, 2, 3]; // Create a Vec -assert_eq!(2 + 2, 4); // Test assertion -assert!(value > 0, "must be positive"); // Boolean assertion -dbg!(expression); // Debug print: prints expression AND value -todo!(); // Placeholder — compiles but panics if reached -unimplemented!(); // Mark code as unimplemented -panic!("something went wrong"); // Crash with message (like raise RuntimeError) - -// Why are these macros instead of functions? -// - println! accepts variable arguments (Rust functions can't) -// - vec! generates code for any type and size -// - assert_eq! knows the SOURCE CODE of what you compared -// - dbg! knows the FILE NAME and LINE NUMBER +// Эти макросы используются повсюду в Rust: + +println!("Hello, {}!", name); // Вывод с форматированием +format!("Value: {}", x); // Создание форматированной String +vec![1, 2, 3]; // Создание Vec +assert_eq!(2 + 2, 4); // Проверка равенства в тесте +assert!(value > 0, "должно быть положительным"); // Проверка логического условия +dbg!(expression); // Отладочный вывод: печатает выражение И его значение +todo!(); // Заглушка: компилируется, но паникует при вызове +unimplemented!(); // Отметить код как нереализованный +panic!("something went wrong"); // Аварийное завершение с сообщением (как raise RuntimeError) + +// Почему это макросы, а не функции? +// - println! принимает переменное число аргументов (функции Rust не могут) +// - vec! генерирует код для любого типа и размера +// - assert_eq! знает ИСХОДНЫЙ КОД того, что сравнивалось +// - dbg! знает ИМЯ ФАЙЛА и НОМЕР СТРОКИ ``` -## Writing a Simple Macro with macro_rules! +## Пишем простой макрос с помощью macro_rules! ```rust -// Python dict() equivalent +// Аналог dict() в Python // Python: d = dict(a=1, b=2) // Rust: let d = hashmap!{ "a" => 1, "b" => 2 }; @@ -362,9 +359,9 @@ let scores = hashmap! { }; ``` -## Derive Macros — Auto-Implementing Traits +## Производные макросы: автоматическая реализация трейтов ```rust -// #[derive(...)] is the Rust equivalent of Python's @dataclass decorator +// #[derive(...)] — это аналог декоратора @dataclass в Python // Python: // @dataclass(frozen=True, order=True) @@ -379,42 +376,42 @@ struct Student { grade: i32, } -// Common derive macros: -// Debug → {:?} formatting (like __repr__) -// Clone → .clone() deep copy -// Copy → implicit copy (only for simple types) -// PartialEq, Eq → == comparison (like __eq__) -// PartialOrd, Ord → <, >, sorting (like __lt__ etc.) -// Hash → usable as HashMap key (like __hash__) -// Default → MyType::default() (like __init__ with no args) - -// Crate-provided derive macros: -// Serialize, Deserialize (serde) → JSON/YAML/TOML serialization -// (like Python's json.dumps/loads but type-safe) +// Частые производные макросы: +// Debug → форматирование {:?} (как __repr__) +// Clone → .clone(), глубокая копия +// Copy → неявное копирование (только для простых типов) +// PartialEq, Eq → сравнение == (как __eq__) +// PartialOrd, Ord → <, >, сортировка (как __lt__ и др.) +// Hash → можно использовать как ключ HashMap (как __hash__) +// Default → MyType::default() (как __init__ без аргументов) + +// Производные макросы из крейтов: +// Serialize, Deserialize (serde) → сериализация в JSON/YAML/TOML +// (как json.dumps/loads в Python, но с проверкой типов) ``` -### Python Decorator vs Rust Derive +### Декоратор Python и производный макрос Rust -| Python Decorator | Rust Derive | Purpose | -|-----------------|-------------|---------| -| `@dataclass` | `#[derive(Debug, Clone, PartialEq)]` | Data class | -| `@dataclass(frozen=True)` | Immutable by default | Immutability | -| `@dataclass(order=True)` | `#[derive(Ord, PartialOrd)]` | Comparison/sorting | -| `@total_ordering` | `#[derive(PartialOrd, Ord)]` | Full ordering | -| JSON `json.dumps(obj.__dict__)` | `#[derive(Serialize)]` | Serialization | -| JSON `MyClass(**json.loads(s))` | `#[derive(Deserialize)]` | Deserialization | +| Декоратор Python | Производный макрос Rust | Назначение | +|------------------|-------------------------|------------| +| `@dataclass` | `#[derive(Debug, Clone, PartialEq)]` | Класс данных | +| `@dataclass(frozen=True)` | Неизменяемость по умолчанию | Неизменяемость | +| `@dataclass(order=True)` | `#[derive(Ord, PartialOrd)]` | Сравнение и сортировка | +| `@total_ordering` | `#[derive(PartialOrd, Ord)]` | Полный порядок | +| JSON `json.dumps(obj.__dict__)` | `#[derive(Serialize)]` | Сериализация | +| JSON `MyClass(**json.loads(s))` | `#[derive(Deserialize)]` | Десериализация | --- -## Exercises +## Упражнения
-🏋️ Exercise: Derive and Custom Debug (click to expand) +🏋️ Упражнение: derive и собственный Debug (нажмите, чтобы раскрыть) -**Challenge**: Create a `User` struct with fields `name: String`, `email: String`, and `password_hash: String`. Derive `Clone` and `PartialEq`, but implement `Debug` manually so it prints the name and email but redacts the password (shows `"***"` instead). +**Задание**: создайте структуру `User` с полями `name: String`, `email: String` и `password_hash: String`. Реализуйте `Clone` и `PartialEq` через derive, а `Debug` напишите вручную так, чтобы он выводил имя и email, а пароль скрывал (вместо него показывал `"***"`).
-🔑 Solution +🔑 Решение ```rust use std::fmt; @@ -447,7 +444,7 @@ fn main() { } ``` -**Key takeaway**: Unlike Python's `__repr__`, Rust lets you derive `Debug` for free — but you can override it for sensitive fields. This is safer than Python where `print(user)` might accidentally leak secrets. +**Ключевой вывод**: в отличие от `__repr__` в Python, Rust позволяет получить `Debug` через derive бесплатно, но его можно переопределить для чувствительных полей. Это безопаснее, чем в Python, где `print(user)` может случайно раскрыть секреты.
diff --git a/python-book/src/ch13-concurrency.md b/python-book/src/ch13-concurrency.md index a180d16..d1ef37c 100644 --- a/python-book/src/ch13-concurrency.md +++ b/python-book/src/ch13-concurrency.md @@ -1,38 +1,35 @@ -## No GIL: True Parallelism +## Нет GIL: настоящий параллелизм -> **What you'll learn:** Why the GIL limits Python concurrency, Rust's `Send`/`Sync` traits for compile-time thread safety, -> `Arc>` vs Python `threading.Lock`, channels vs `queue.Queue`, and async/await differences. +> **Что вы узнаете:** почему GIL ограничивает конкурентность в Python, трейты `Send`/`Sync` Rust для проверки потокобезопасности на этапе компиляции, `Arc>` в сравнении с `threading.Lock` в Python, каналы в сравнении с `queue.Queue` и различия async/await. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -The GIL (Global Interpreter Lock) is Python's biggest limitation for CPU-bound work. -Rust has no GIL — threads run truly in parallel, and the type system prevents data races -at compile time. +GIL (Global Interpreter Lock, глобальная блокировка интерпретатора) — главное ограничение Python для задач, ограниченных CPU. В Rust GIL нет: потоки действительно работают параллельно, а система типов предотвращает гонки данных на этапе компиляции. ```mermaid gantt - title CPU-bound Work: Python GIL vs Rust Threads + title Работа, ограниченная CPU: GIL в Python против потоков Rust dateFormat X axisFormat %s section Python (GIL) - Thread 1 :a1, 0, 4 - Thread 2 :a2, 4, 8 - Thread 3 :a3, 8, 12 - Thread 4 :a4, 12, 16 - section Rust (no GIL) - Thread 1 :b1, 0, 4 - Thread 2 :b2, 0, 4 - Thread 3 :b3, 0, 4 - Thread 4 :b4, 0, 4 + Поток 1 :a1, 0, 4 + Поток 2 :a2, 4, 8 + Поток 3 :a3, 8, 12 + Поток 4 :a4, 12, 16 + section Rust (без GIL) + Поток 1 :b1, 0, 4 + Поток 2 :b2, 0, 4 + Поток 3 :b3, 0, 4 + Поток 4 :b4, 0, 4 ``` -> **Key insight**: Python threads run sequentially for CPU work (GIL serializes them). Rust threads run truly in parallel — 4 threads = ~4x speedup. +> **Ключевая мысль**: потоки Python при работе с CPU выполняются последовательно (GIL их сериализует). Потоки Rust действительно работают параллельно: 4 потока дают ускорение примерно в 4 раза. > -> 📌 **Prerequisite**: Make sure you're comfortable with [Ch. 7 — Ownership and Borrowing](ch07-ownership-and-borrowing.md) before tackling this chapter. `Arc`, `Mutex`, and move closures all build on ownership concepts. +> 📌 **Предварительное требование**: убедитесь, что вы освоили [Гл. 7 — Владение и заимствование](ch07-ownership-and-borrowing.md), прежде чем переходить к этой главе. `Arc`, `Mutex` и замыкания `move` опираются на понятия владения. -### Python's GIL Problem +### Проблема GIL в Python ```python -# Python — threads don't help for CPU-bound work +# Python — потоки не помогают при работе, ограниченной CPU import threading import time @@ -41,7 +38,7 @@ counter = 0 def increment(n): global counter for _ in range(n): - counter += 1 # NOT thread-safe! But GIL "protects" simple operations + counter += 1 # НЕ потокобезопасно! Но GIL как бы «защищает» простые операции threads = [threading.Thread(target=increment, args=(1_000_000,)) for _ in range(4)] start = time.perf_counter() @@ -51,16 +48,16 @@ for t in threads: t.join() elapsed = time.perf_counter() - start -print(f"Counter: {counter}") # Might not be 4,000,000! -print(f"Time: {elapsed:.2f}s") # About the SAME as single-threaded (GIL) +print(f"Счётчик: {counter}") # Может быть не 4 000 000! +print(f"Время: {elapsed:.2f}s") # Примерно ТАКОЕ ЖЕ время, как у одного потока (GIL) -# For true parallelism, Python requires multiprocessing: +# Для настоящего параллелизма в Python нужен multiprocessing: from multiprocessing import Pool with Pool(4) as pool: - results = pool.map(cpu_work, data) # Separate processes, pickle overhead + results = pool.map(cpu_work, data) # Отдельные процессы, накладные расходы на pickle ``` -### Rust — True Parallelism, Compile-Time Safety +### Rust: настоящий параллелизм и проверка на этапе компиляции ```rust use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::Arc; @@ -82,60 +79,60 @@ fn main() { h.join().unwrap(); } - println!("Counter: {}", counter.load(Ordering::Relaxed)); // Always 4,000,000 - // Runs on ALL cores — true parallelism, no GIL + println!("Счётчик: {}", counter.load(Ordering::Relaxed)); // Всегда 4 000 000 + // Работает на ВСЕХ ядрах: настоящий параллелизм, без GIL } ``` *** -## Thread Safety: Type System Guarantees +## Безопасность потоков: гарантии системы типов -### Python — Runtime Errors +### Python: ошибки во время выполнения ```python -# Python — data races caught at runtime (or not at all) +# Python — гонки данных обнаруживаются во время выполнения (или не обнаруживаются вовсе) import threading shared_list = [] def append_items(items): for item in items: - shared_list.append(item) # "Thread-safe" due to GIL for append - # But complex operations are NOT safe: + shared_list.append(item) # «Потокобезопасно» благодаря GIL для append + # Но сложные операции НЕ безопасны: # if item not in shared_list: - # shared_list.append(item) # RACE CONDITION! + # shared_list.append(item) # ГОНКА ДАННЫХ! -# Using Lock for safety: +# Использование Lock для безопасности: lock = threading.Lock() def safe_append(items): for item in items: with lock: if item not in shared_list: shared_list.append(item) -# Forgetting the lock? No compiler warning. Bug discovered in production. +# Забыли блокировку? Предупреждения компилятора не будет. Ошибка обнаружится в продакшене. ``` -### Rust — Compile-Time Errors +### Rust: ошибки на этапе компиляции ```rust use std::sync::{Arc, Mutex}; use std::thread; fn main() { - // Trying to share a Vec across threads without protection: + // Попытка разделить Vec между потоками без защиты: // let shared = vec![]; // thread::spawn(move || shared.push(1)); - // ❌ Compile error: Vec is not Send/Sync without protection + // ❌ Ошибка компиляции: Vec не реализует Send/Sync без защиты - // With Mutex (Rust's equivalent of threading.Lock): + // С Mutex (аналог threading.Lock в Rust): let shared = Arc::new(Mutex::new(Vec::new())); let handles: Vec<_> = (0..4).map(|i| { let shared = Arc::clone(&shared); thread::spawn(move || { - let mut data = shared.lock().unwrap(); // Lock is REQUIRED to access + let mut data = shared.lock().unwrap(); // Блокировка ОБЯЗАТЕЛЬНА для доступа data.push(i); - // Lock is automatically released when `data` goes out of scope - // No "forgetting to unlock" — RAII guarantees it + // Блокировка автоматически освобождается, когда `data` выходит из области видимости + // Нельзя «забыть разблокировать»: RAII гарантирует это }) }).collect(); @@ -143,47 +140,47 @@ fn main() { h.join().unwrap(); } - println!("{:?}", shared.lock().unwrap()); // [0, 1, 2, 3] (order may vary) + println!("{:?}", shared.lock().unwrap()); // [0, 1, 2, 3] (порядок может отличаться) } ``` -### Send and Sync Traits +### Трейты Send и Sync ```rust -// Rust uses two marker traits to enforce thread safety: +// Rust использует два маркерных трейта для обеспечения потокобезопасности: -// Send — "this type can be transferred to another thread" -// Most types are Send. Rc is NOT (use Arc for threads). +// Send — «этот тип можно передать в другой поток» +// Большинство типов — Send. Rc — НЕТ (для потоков используйте Arc). -// Sync — "this type can be referenced from multiple threads" -// Most types are Sync. Cell/RefCell are NOT (use Mutex). +// Sync — «на этот тип можно ссылаться из нескольких потоков» +// Большинство типов — Sync. Cell/RefCell — НЕТ (используйте Mutex). -// The compiler checks these automatically: +// Компилятор проверяет это автоматически: // thread::spawn(move || { ... }) -// ↑ The closure's captures must be Send -// ↑ Shared references must be Sync -// ↑ If they're not → compile error +// ↑ Захваченные замыканием значения должны быть Send +// ↑ Общие ссылки должны быть Sync +// ↑ Если нет, то ошибка компиляции -// Python has no equivalent. Thread safety bugs are discovered at runtime. -// Rust catches them at compile time. This is "fearless concurrency." +// У Python аналога нет. Ошибки потокобезопасности обнаруживаются во время выполнения. +// Rust ловит их на этапе компиляции. Это и есть «конкурентность без страха». ``` -### Concurrency Primitives Comparison +### Сравнение примитивов конкурентности -| Python | Rust | Purpose | -|--------|------|---------| -| `threading.Lock()` | `Mutex` | Mutual exclusion | -| `threading.RLock()` | `Mutex` (no reentrant) | Reentrant lock (use differently) | -| `threading.RWLock` (N/A) | `RwLock` | Multiple readers OR one writer | -| `threading.Event()` | `Condvar` | Condition variable | -| `queue.Queue()` | `mpsc::channel()` | Thread-safe channel | -| `multiprocessing.Pool` | `rayon::ThreadPool` | Thread pool | -| `concurrent.futures` | `rayon` / `tokio::spawn` | Task-based parallelism | -| `threading.local()` | `thread_local!` | Thread-local storage | -| N/A | `Atomic*` types | Lock-free counters and flags | +| Python | Rust | Назначение | +|--------|------|------------| +| `threading.Lock()` | `Mutex` | Взаимное исключение | +| `threading.RLock()` | `Mutex` (не реентерабельный) | Реентерабельная блокировка (используется иначе) | +| `threading.RWLock` (нет) | `RwLock` | Много читателей ИЛИ один писатель | +| `threading.Event()` | `Condvar` | Условная переменная | +| `queue.Queue()` | `mpsc::channel()` | Потокобезопасный канал | +| `multiprocessing.Pool` | `rayon::ThreadPool` | Пул потоков | +| `concurrent.futures` | `rayon` / `tokio::spawn` | Параллелизм на основе задач | +| `threading.local()` | `thread_local!` | Локальное для потока хранилище | +| Нет | Типы `Atomic*` | Безблокировочные счётчики и флаги | -### Mutex Poisoning +### Отравление мьютекса -If a thread **panics** while holding a `Mutex`, the lock becomes *poisoned*. Python has no equivalent — if a thread crashes holding a `threading.Lock()`, the lock stays stuck. +Если поток **паникует**, удерживая `Mutex`, блокировка становится *отравленной* (poisoned). В Python аналога нет: если поток падает, удерживая `threading.Lock()`, блокировка остаётся захваченной навсегда. ```rust use std::sync::{Arc, Mutex}; @@ -195,42 +192,41 @@ let data2 = Arc::clone(&data); let _ = thread::spawn(move || { let mut guard = data2.lock().unwrap(); guard.push(4); - panic!("oops!"); // Lock is now poisoned + panic!("упс!"); // Блокировка теперь отравлена }).join(); -// Subsequent lock attempts return Err(PoisonError) +// Последующие попытки блокировки возвращают Err(PoisonError) match data.lock() { - Ok(guard) => println!("Data: {guard:?}"), + Ok(guard) => println!("Данные: {guard:?}"), Err(poisoned) => { - println!("Lock was poisoned! Recovering..."); + println!("Блокировка отравлена! Восстанавливаем..."); let guard = poisoned.into_inner(); - println!("Recovered: {guard:?}"); // [1, 2, 3, 4] + println!("Восстановлено: {guard:?}"); // [1, 2, 3, 4] } } ``` -### Atomic Ordering (brief note) +### Порядок атомарных операций (краткая заметка) -The `Ordering` parameter on atomic operations controls memory visibility guarantees: +Параметр `Ordering` в атомарных операциях управляет гарантиями видимости памяти: -| Ordering | When to use | -|----------|-------------| -| `Relaxed` | Simple counters where ordering doesn't matter | -| `Acquire`/`Release` | Producer-consumer: writer uses `Release`, reader uses `Acquire` | -| `SeqCst` | When in doubt — strictest ordering, most intuitive | +| Ordering | Когда использовать | +|----------|--------------------| +| `Relaxed` | Простые счётчики, где порядок не важен | +| `Acquire`/`Release` | Производитель-потребитель: писатель использует `Release`, читатель `Acquire` | +| `SeqCst` | Если сомневаетесь: самый строгий порядок, самый понятный | -Python's `threading` module hides these details behind the GIL. In Rust, you choose explicitly — use `SeqCst` until profiling shows you need something weaker. +Модуль `threading` в Python скрывает эти детали за GIL. В Rust вы выбираете явно: используйте `SeqCst`, пока профилирование не покажет, что нужно что-то послабее. *** -## async/await Comparison +## Сравнение async/await -Python and Rust both have `async`/`await` syntax, but they work very differently -under the hood. +Python и Rust имеют синтаксис `async`/`await`, но внутри работают очень по-разному. -### Python async/await +### async/await в Python ```python -# Python — asyncio for concurrent I/O +# Python — asyncio для параллельного ввода-вывода import asyncio import aiohttp @@ -246,21 +242,21 @@ async def main(): results = await asyncio.gather(*tasks) for url, result in zip(urls, results): - print(f"{url}: {len(result)} bytes") + print(f"{url}: {len(result)} байт") asyncio.run(main()) -# Python async is single-threaded (still GIL)! -# It only helps with I/O-bound work (waiting for network/disk). -# CPU-bound work in async still blocks the event loop. +# Async в Python работает в одном потоке (всё ещё под GIL)! +# Помогает только при задачах, ограниченных вводом-выводом (ожидание сети или диска). +# Задачи, ограниченные CPU, внутри async всё равно блокируют цикл событий. ``` -### Rust async/await +### async/await в Rust ```rust -// Rust — tokio for concurrent I/O (and CPU parallelism!) +// Rust — tokio для параллельного ввода-вывода (и параллелизма CPU!) use reqwest; use tokio; -use futures::future::join_all; // add `futures` to Cargo.toml +use futures::future::join_all; // добавьте `futures` в Cargo.toml async fn fetch_url(url: &str) -> Result { reqwest::get(url).await?.text().await @@ -271,16 +267,16 @@ async fn main() -> Result<(), Box> { let urls = vec!["https://example.com", "https://httpbin.org/get"]; let tasks: Vec<_> = urls.iter() - .map(|url| tokio::spawn(fetch_url(url))) // No GIL limitation - .collect(); // Can use all CPU cores + .map(|url| tokio::spawn(fetch_url(url))) // Без ограничений GIL + .collect(); // Можно использовать все ядра CPU let results = futures::future::join_all(tasks).await; for (url, result) in urls.iter().zip(results) { match result { - Ok(Ok(body)) => println!("{url}: {} bytes", body.len()), - Ok(Err(e)) => println!("{url}: error {e}"), - Err(e) => println!("{url}: task failed {e}"), + Ok(Ok(body)) => println!("{url}: {} байт", body.len()), + Ok(Err(e)) => println!("{url}: ошибка {e}"), + Err(e) => println!("{url}: задача завершилась с ошибкой {e}"), } } @@ -288,22 +284,22 @@ async fn main() -> Result<(), Box> { } ``` -### Key Differences +### Ключевые различия -| Aspect | Python asyncio | Rust tokio | +| Аспект | Python asyncio | Rust tokio | |--------|---------------|------------| -| GIL | Still applies | No GIL | -| CPU parallelism | ❌ Single-threaded | ✅ Multi-threaded | -| Runtime | Built-in (asyncio) | External crate (tokio) | -| Ecosystem | aiohttp, asyncpg, etc. | reqwest, sqlx, etc. | -| Performance | Good for I/O | Excellent for I/O AND CPU | -| Error handling | Exceptions | `Result` | -| Cancellation | `task.cancel()` | Drop the future | -| Color problem | Sync ↔ async boundary | Same issue exists | - -### Simple Parallelism with Rayon +| GIL | Действует | Нет GIL | +| Параллелизм CPU | ❌ Один поток | ✅ Многопоточность | +| Рантайм | Встроен (asyncio) | Внешний крейт (tokio) | +| Экосистема | aiohttp, asyncpg и др. | reqwest, sqlx и др. | +| Производительность | Хорошо для I/O | Отлично и для I/O, и для CPU | +| Обработка ошибок | Исключения | `Result` | +| Отмена | `task.cancel()` | Удалить future (drop) | +| «Проблема цвета» функций | Граница sync ↔ async | Та же проблема существует | + +### Простой параллелизм с Rayon ```python -# Python — multiprocessing for CPU parallelism +# Python — multiprocessing для параллелизма CPU from multiprocessing import Pool def process_item(item): @@ -314,47 +310,47 @@ with Pool(8) as pool: ``` ```rust -// Rust — rayon for effortless CPU parallelism (one line change!) +// Rust — rayon для лёгкого параллелизма CPU (изменение одной строки!) use rayon::prelude::*; -// Sequential: +// Последовательно: let results: Vec<_> = items.iter().map(|item| heavy_computation(item)).collect(); -// Parallel (change .iter() to .par_iter() — that's it!): +// Параллельно (замените .iter() на .par_iter(), и всё!): let results: Vec<_> = items.par_iter().map(|item| heavy_computation(item)).collect(); -// No pickle, no process overhead, no serialization. -// Rayon automatically distributes work across cores. +// Без pickle, без накладных расходов на процессы, без сериализации. +// Rayon автоматически распределяет работу по ядрам. ``` --- -## 💼 Case Study: Parallel Image Processing Pipeline +## 💼 Кейс: параллельный конвейер обработки изображений -A data science team processes 50,000 satellite images nightly. Their Python pipeline uses `multiprocessing.Pool`: +Команда специалистов по данным ежедневно обрабатывает 50 000 спутниковых снимков. Их конвейер на Python использует `multiprocessing.Pool`: ```python -# Python — multiprocessing for CPU-bound image work +# Python — multiprocessing для обработки изображений, ограниченной CPU import multiprocessing from PIL import Image import numpy as np def process_image(path: str) -> dict: img = np.array(Image.open(path)) - # CPU-intensive: histogram equalization, edge detection, classification + # Ресурсоёмкие операции: выравнивание гистограммы, выделение границ, классификация histogram = np.histogram(img, bins=256)[0] - edges = detect_edges(img) # ~200ms per image - label = classify(edges) # ~100ms per image + edges = detect_edges(img) # ~200 мс на изображение + label = classify(edges) # ~100 мс на изображение return {"path": path, "label": label, "edge_count": len(edges)} -# Problem: each subprocess copies the full Python interpreter -# Memory: 50MB per worker × 16 workers = 800MB overhead -# Startup: 2-3 seconds to fork and pickle arguments +# Проблема: каждый подпроцесс копирует весь интерпретатор Python +# Память: 50 МБ на воркер × 16 воркеров = 800 МБ накладных расходов +# Запуск: 2–3 секунды на fork и pickle аргументов with multiprocessing.Pool(16) as pool: - results = pool.map(process_image, image_paths) # ~4.5 hours for 50k images + results = pool.map(process_image, image_paths) # ~4,5 часа на 50 тыс. изображений ``` -**Pain points**: 800MB memory overhead from forking, pickle serialization of arguments/results, GIL prevents using threads, error handling is opaque (exceptions in workers are hard to debug). +**Болевые точки**: 800 МБ памяти на форк, сериализация pickle аргументов и результатов, GIL мешает использовать потоки, обработка ошибок непрозрачна (исключения в воркерах трудно отлаживать). ```rust use rayon::prelude::*; @@ -368,10 +364,10 @@ struct ImageResult { fn process_image(path: &str) -> Result { let img = image::open(path)?; - // Application-specific functions (implement for your use case) - let histogram = compute_histogram(&img); // ~50ms (no numpy overhead) - let edges = detect_edges(&img); // ~40ms (SIMD-optimized) - let label = classify(&edges); // ~20ms + // Специфичные для приложения функции (реализуйте под свою задачу) + let histogram = compute_histogram(&img); // ~50 мс (без накладных расходов numpy) + let edges = detect_edges(&img); // ~40 мс (оптимизировано через SIMD) + let label = classify(&edges); // ~20 мс Ok(ImageResult { path: path.to_string(), label, @@ -382,40 +378,41 @@ fn process_image(path: &str) -> Result { fn main() -> Result<(), Box> { let paths: Vec = load_image_paths()?; - // Rayon automatically uses all CPU cores — no forking, no pickle, no GIL + // Rayon автоматически использует все ядра CPU: без форка, без pickle, без GIL let results: Vec = paths - .par_iter() // Parallel iterator - .filter_map(|p| process_image(p).ok()) // Skip errors gracefully - .collect(); // Collect in parallel + .par_iter() // Параллельный итератор + .filter_map(|p| process_image(p).ok()) // Аккуратно пропускаем ошибки + .collect(); // Собираем параллельно - println!("Processed {} images", results.len()); + println!("Обработано изображений: {}", results.len()); Ok(()) } -// 50k images in ~35 minutes (vs 4.5 hours in Python) -// Memory: ~50MB total (shared threads, no forking) +// 50 тыс. изображений за ~35 минут (против 4,5 часа в Python) +// Память: ~50 МБ всего (общие потоки, без форка) ``` -**Results**: -| Metric | Python (multiprocessing) | Rust (rayon) | -|--------|------------------------|--------------| -| Time (50k images) | ~4.5 hours | ~35 minutes | -| Memory overhead | 800MB (16 workers) | ~50MB (shared) | -| Error handling | Opaque pickle errors | `Result` at every step | -| Startup cost | 2–3s (fork + pickle) | None (threads) | +**Результаты**: -> **Key lesson**: For CPU-bound parallel work, Rust's threads + rayon replace Python's `multiprocessing` with zero serialization overhead, shared memory, and compile-time safety. +| Метрика | Python (multiprocessing) | Rust (rayon) | +|---------|--------------------------|--------------| +| Время (50 тыс. изображений) | ~4,5 часа | ~35 минут | +| Накладные расходы памяти | 800 МБ (16 воркеров) | ~50 МБ (общая) | +| Обработка ошибок | Непрозрачные ошибки pickle | `Result` на каждом шаге | +| Стоимость запуска | 2–3 с (fork + pickle) | Нет (потоки) | + +> **Ключевой урок**: для параллельной работы, ограниченной CPU, потоки Rust и rayon заменяют `multiprocessing` из Python без накладных расходов на сериализацию, с общей памятью и проверкой безопасности на этапе компиляции. --- -## Exercises +## Упражнения
-🏋️ Exercise: Thread-Safe Counter (click to expand) +🏋️ Упражнение: потокобезопасный счётчик (нажмите, чтобы раскрыть) -**Challenge**: In Python, you might use `threading.Lock` to protect a shared counter. Translate this to Rust: spawn 10 threads, each incrementing a shared counter 1000 times. Print the final value (should be 10000). Use `Arc>`. +**Задание**: в Python для защиты общего счётчика вы могли бы использовать `threading.Lock`. Перепишите это на Rust: запустите 10 потоков, каждый из которых 1000 раз увеличивает общий счётчик. Выведите итоговое значение (должно быть 10000). Используйте `Arc>`.
-🔑 Solution +🔑 Решение ```rust use std::sync::{Arc, Mutex}; @@ -439,15 +436,14 @@ fn main() { handle.join().unwrap(); } - println!("Final count: {}", *counter.lock().unwrap()); + println!("Итоговый счёт: {}", *counter.lock().unwrap()); } ``` -**Key takeaway**: `Arc>` is Rust's equivalent of Python's `lock = threading.Lock()` + shared variable — but Rust *won't compile* if you forget the `Arc` or `Mutex`. Python happily runs a racy program and gives you wrong answers silently. +**Ключевой вывод**: `Arc>` — это аналог в Rust для `lock = threading.Lock()` плюс общая переменная, но Rust *не скомпилируется*, если вы забудете `Arc` или `Mutex`. Python спокойно запустит программу с гонкой и молча выдаст неверный ответ.
*** - From 19d5f178b0771b3f38187d7d94220efd2e535e4b Mon Sep 17 00:00:00 2001 From: r007 Date: Fri, 9 Oct 2026 09:20:16 +0400 Subject: [PATCH 12/18] Translate Python book chapters 14-17 to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for ch14 (unsafe and FFI, testing), ch15 (migration patterns), ch16 (best practices, Rosetta stone) and ch17 (capstone CLI task manager). Code identifiers, crate names, file names and links are unchanged. TOC anchors in ch00 for chapters 14-17 are updated to the Russian heading slugs; all 53 cross-chapter anchors resolve in the built book. Co-Authored-By: Claude Haiku 5.5 --- python-book/src/ch00-introduction.md | 2 +- python-book/src/ch14-unsafe-rust-and-ffi.md | 281 ++++++++--------- python-book/src/ch15-migration-patterns.md | 305 +++++++++--------- python-book/src/ch16-best-practices.md | 329 ++++++++++---------- python-book/src/ch17-capstone-project.md | 220 ++++++------- 5 files changed, 560 insertions(+), 577 deletions(-) diff --git a/python-book/src/ch00-introduction.md b/python-book/src/ch00-introduction.md index 4fc1d8a..9ad57e0 100644 --- a/python-book/src/ch00-introduction.md +++ b/python-book/src/ch00-introduction.md @@ -126,7 +126,7 @@ #### 16. Лучшие практики 🟡 - [Идиоматичный Rust для разработчиков на Python](ch16-best-practices.md#идиоматичный-rust-для-разработчиков-на-python) - [Частые ошибки и их решения](ch16-best-practices.md#частые-ошибки-и-их-решения) -- [Шпаргалка «Python → Rust»](ch16-best-practices.md#шпаргалка-python-rust) +- [Шпаргалка «Python → Rust»](ch16-best-practices.md#шпаргалка-python--rust) - [Путь обучения и ресурсы](ch16-best-practices.md#путь-обучения-и-ресурсы) --- diff --git a/python-book/src/ch14-unsafe-rust-and-ffi.md b/python-book/src/ch14-unsafe-rust-and-ffi.md index 688ef6c..8b02de0 100644 --- a/python-book/src/ch14-unsafe-rust-and-ffi.md +++ b/python-book/src/ch14-unsafe-rust-and-ffi.md @@ -1,26 +1,23 @@ -## When and Why to Use Unsafe +## Когда и зачем использовать unsafe -> **What you'll learn:** What `unsafe` permits and why it exists, writing Python extensions with PyO3 (the killer feature for Python devs), -> Rust's testing framework vs pytest, mocking with mockall, and benchmarking. +> **Что вы узнаете:** что разрешает `unsafe` и почему он существует, написание расширений Python на PyO3 (главная возможность для разработчиков на Python), систему тестирования Rust в сравнении с pytest, моки с помощью mockall и бенчмаркинг. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -`unsafe` in Rust is an escape hatch — it tells the compiler "I'm doing something -you can't verify, but I promise it's correct." Python has no equivalent because -Python never gives you direct memory access. +`unsafe` в Rust — это запасной люк: он сообщает компилятору «я делаю то, что ты не можешь проверить, но обещаю, что это корректно». У Python аналога нет, потому что Python никогда не даёт прямого доступа к памяти. ```mermaid flowchart TB - subgraph Safe ["Safe Rust (99% of code)"] - S1["Your application logic"] + subgraph Safe ["Безопасный Rust (99% кода)"] + S1["Логика вашего приложения"] S2["pub fn safe_api(&self) -> Result"] end - subgraph Unsafe ["unsafe block (minimal, audited)"] - U1["Raw pointer dereference"] - U2["FFI call to C/Python"] + subgraph Unsafe ["Блок unsafe (минимальный, проверенный)"] + U1["Разыменование сырого указателя"] + U2["Вызов FFI к C/Python"] end - subgraph External ["External (C / Python / OS)"] - E1["libc / PyO3 / system calls"] + subgraph External ["Внешние (C / Python / ОС)"] + E1["libc / PyO3 / системные вызовы"] end S1 --> S2 S2 --> U1 @@ -32,61 +29,60 @@ flowchart TB style External fill:#f8d7da,stroke:#dc3545 ``` -> **The pattern**: Safe API wraps a small `unsafe` block. Callers never see `unsafe`. Python's `ctypes` has no such boundary — every FFI call is implicitly unsafe. +> **Схема**: безопасный API оборачивает небольшой блок `unsafe`. Вызывающий код никогда не видит `unsafe`. В `ctypes` Python такой границы нет: каждый вызов FFI неявно небезопасен. > -> 📌 **See also**: [Ch. 13 — Concurrency](ch13-concurrency.md) covers `Send`/`Sync` traits which are `unsafe` auto-traits that the compiler checks for thread safety. +> 📌 **См. также**: [Гл. 13 — Конкурентность](ch13-concurrency.md) описывает трейты `Send`/`Sync`, которые являются небезопасными автотрейтами, и компилятор проверяет их для потокобезопасности. -### What unsafe Allows +### Что разрешает unsafe ```rust -// unsafe lets you do FIVE things that safe Rust forbids: -// 1. Dereference raw pointers -// 2. Call unsafe functions/methods -// 3. Access mutable static variables -// 4. Implement unsafe traits -// 5. Access union fields - -// Example: calling a C function +// unsafe позволяет делать ПЯТЬ вещей, которые запрещены в безопасном Rust: +// 1. Разыменовывать сырые указатели +// 2. Вызывать небезопасные функции и методы +// 3. Обращаться к изменяемым статическим переменным +// 4. Реализовывать небезопасные трейты +// 5. Обращаться к полям union + +// Пример: вызов функции из C extern "C" { fn abs(input: i32) -> i32; } fn main() { - // SAFETY: abs() is a well-defined C standard library function. - let result = unsafe { abs(-42) }; // Safe Rust can't verify C code + // SAFETY: abs() — корректно определённая функция стандартной библиотеки C. + let result = unsafe { abs(-42) }; // Безопасный Rust не может проверить код на C println!("{result}"); // 42 } ``` -### When to Use unsafe +### Когда использовать unsafe ```rust -// 1. FFI — calling C libraries (most common reason) -// 2. Performance-critical inner loops (rare) -// 3. Data structures the borrow checker can't express (rare) - -// As a Python developer, you'll mostly encounter unsafe in: -// - PyO3 internals (Python ↔ Rust bridge) -// - C library bindings -// - Low-level system calls - -// Rule of thumb: if you're writing application code (not library code), -// you should almost never need unsafe. If you think you do, ask in the -// Rust community first — there's usually a safe alternative. +// 1. FFI — вызов библиотек на C (самая частая причина) +// 2. Критичные к производительности внутренние циклы (редко) +// 3. Структуры данных, которые заимствующий checker не может выразить (редко) + +// Как разработчик на Python, вы в основном встретите unsafe в: +// - внутренностях PyO3 (мост Python ↔ Rust) +// - привязках к библиотекам на C +// - низкоуровневых системных вызовах + +// Практическое правило: если вы пишете прикладной код (а не библиотечный), +// unsafe почти никогда не нужен. Если кажется, что нужен, сначала спросите +// сообщество Rust: обычно есть безопасная альтернатива. ``` *** -## PyO3: Rust Extensions for Python +## PyO3: расширения Rust для Python -PyO3 is the bridge between Python and Rust. It lets you write Rust functions and -classes that are callable from Python — perfect for replacing slow Python hotspots. +PyO3 — мост между Python и Rust. Он позволяет писать функции и классы на Rust, которые вызываются из Python, и это идеально для замены медленных участков кода на Python. -### Creating a Python Extension in Rust +### Создание расширения Python на Rust ```bash -# Setup -pip install maturin # Build tool for Rust Python extensions -maturin init # Creates project structure +# Установка +pip install maturin # Инструмент сборки расширений Rust для Python +maturin init # Создаёт структуру проекта -# Project structure: +# Структура проекта: # my_extension/ # ├── Cargo.toml # ├── pyproject.toml @@ -102,17 +98,17 @@ version = "0.1.0" edition = "2021" [lib] -crate-type = ["cdylib"] # Shared library for Python +crate-type = ["cdylib"] # Разделяемая библиотека для Python [dependencies] pyo3 = { version = "0.22", features = ["extension-module"] } ``` ```rust -// src/lib.rs — Rust functions callable from Python +// src/lib.rs — функции Rust, которые можно вызывать из Python use pyo3::prelude::*; -/// A fast Fibonacci function written in Rust. +/// Быстрая функция Фибоначчи, написанная на Rust. #[pyfunction] fn fibonacci(n: u64) -> u64 { let (mut a, mut b) = (0u64, 1u64); @@ -124,7 +120,7 @@ fn fibonacci(n: u64) -> u64 { a } -/// Find all prime numbers up to n (Sieve of Eratosthenes). +/// Найти все простые числа до n (решето Эратосфена). #[pyfunction] fn primes_up_to(n: usize) -> Vec { let mut is_prime = vec![true; n + 1]; @@ -140,7 +136,7 @@ fn primes_up_to(n: usize) -> Vec { (2..=n).filter(|&i| is_prime[i]).collect() } -/// A Rust class usable from Python. +/// Класс Rust, который можно использовать из Python. #[pyclass] struct Counter { value: i64, @@ -166,7 +162,7 @@ impl Counter { } } -/// The Python module definition. +/// Определение модуля Python. #[pymodule] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(fibonacci, m)?)?; @@ -176,31 +172,31 @@ fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { } ``` -### Using from Python +### Использование из Python ```bash -# Build and install: -maturin develop --release # Builds and installs into current venv +# Сборка и установка: +maturin develop --release # Собирает и устанавливает в текущее venv ``` ```python -# Python — use the Rust extension like any Python module +# Python — используйте расширение Rust как любой модуль Python import my_extension -# Call Rust function +# Вызов функции на Rust result = my_extension.fibonacci(50) -print(result) # 12586269025 — computed in microseconds +print(result) # 12586269025 — вычислено за микросекунды -# Use Rust class +# Использование класса Rust counter = my_extension.Counter(0) counter.increment() counter.increment() print(counter.get_value()) # 2 print(counter) # Counter(value=2) -# Performance comparison: +# Сравнение производительности: import time -# Python version +# Версия на Python def py_primes(n): sieve = [True] * (n + 1) for i in range(2, int(n**0.5) + 1): @@ -217,58 +213,58 @@ start = time.perf_counter() rs_result = my_extension.primes_up_to(10_000_000) rs_time = time.perf_counter() - start -print(f"Python: {py_time:.3f}s") # ~3.5s -print(f"Rust: {rs_time:.3f}s") # ~0.05s — 70x faster! -print(f"Same results: {py_result == rs_result}") # True +print(f"Python: {py_time:.3f}s") # ~3,5 с +print(f"Rust: {rs_time:.3f}s") # ~0,05 с — в 70 раз быстрее! +print(f"Результаты совпадают: {py_result == rs_result}") # True ``` -### PyO3 Quick Reference +### Краткая справка по PyO3 -| Python Concept | PyO3 Attribute | Notes | -|---------------|----------------|-------| -| Function | `#[pyfunction]` | Exposed to Python | -| Class | `#[pyclass]` | Python-visible class | -| Method | `#[pymethods]` | Methods on a pyclass | -| `__init__` | `#[new]` | Constructor | -| `__repr__` | `fn __repr__()` | String representation | -| `__str__` | `fn __str__()` | Display string | -| `__len__` | `fn __len__()` | Length | -| `__getitem__` | `fn __getitem__()` | Indexing | -| Property | `#[getter]` / `#[setter]` | Attribute access | -| Static method | `#[staticmethod]` | No self | -| Class method | `#[classmethod]` | Takes cls | +| Концепция Python | Атрибут PyO3 | Примечания | +|------------------|--------------|------------| +| Функция | `#[pyfunction]` | Доступна в Python | +| Класс | `#[pyclass]` | Класс, видимый в Python | +| Метод | `#[pymethods]` | Методы pyclass | +| `__init__` | `#[new]` | Конструктор | +| `__repr__` | `fn __repr__()` | Строковое представление | +| `__str__` | `fn __str__()` | Строка для вывода | +| `__len__` | `fn __len__()` | Длина | +| `__getitem__` | `fn __getitem__()` | Индексация | +| Property | `#[getter]` / `#[setter]` | Доступ к атрибутам | +| Static method | `#[staticmethod]` | Без self | +| Class method | `#[classmethod]` | Принимает cls | -### FFI Safety Patterns +### Шаблоны безопасности FFI -When exposing Rust to Python (via PyO3 or raw C FFI), these rules prevent the most common bugs: +Когда Rust экспортируется в Python (через PyO3 или сырой C FFI), эти правила предотвращают самые частые ошибки: -1. **Never let a panic cross the FFI boundary** — a Rust panic unwinding into Python (or C) is **undefined behavior**. PyO3 handles this automatically for `#[pyfunction]`, but raw `extern "C"` functions need explicit protection: +1. **Никогда не пропускайте панику через границу FFI.** Паника Rust, раскручивающаяся в Python (или C), — это **неопределённое поведение**. PyO3 делает это автоматически для `#[pyfunction]`, но сырым функциям `extern "C"` нужна явная защита: ```rust #[no_mangle] pub extern "C" fn raw_ffi_function() -> i32 { match std::panic::catch_unwind(|| { - // actual logic + // собственно логика 42 }) { Ok(result) => result, - Err(_) => -1, // Return error code instead of panicking into C/Python + Err(_) => -1, // Возвращаем код ошибки вместо паники в C/Python } } ``` -2. **`#[repr(C)]` for shared structs** — if Python/C reads struct fields directly, you **must** use `#[repr(C)]` to guarantee C-compatible layout. If you're passing opaque pointers (which PyO3 does for `#[pyclass]`), it's not needed. +2. **`#[repr(C)]` для общих структур**: если Python/C читает поля структуры напрямую, **обязательно** используйте `#[repr(C)]`, чтобы гарантировать совместимую с C раскладку. Если передаются непрозрачные указатели (как делает PyO3 для `#[pyclass]`), это не нужно. -3. **`extern "C"`** — required for raw FFI functions so the calling convention matches what C/Python expects. PyO3's `#[pyfunction]` handles this for you. +3. **`extern "C"`**: обязательно для сырых FFI-функций, чтобы соглашение о вызовах совпадало с тем, что ожидают C/Python. `#[pyfunction]` в PyO3 делает это за вас. -> **PyO3 advantage**: PyO3 wraps most of these safety concerns for you — panic catching, type conversion, GIL management. Prefer PyO3 over raw FFI unless you have a specific reason not to. +> **Преимущество PyO3**: PyO3 берёт на себя большинство этих проблем безопасности: перехват паник, преобразование типов, управление GIL. Предпочитайте PyO3 сырому FFI, если нет особой причины не делать этого. *** -## Unit Tests vs pytest +## Модульные тесты и pytest -### Python Testing with pytest +### Тестирование на Python с pytest ```python # test_calculator.py import pytest @@ -287,7 +283,7 @@ def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(1, 0) -# Parameterized tests +# Параметризованные тесты @pytest.mark.parametrize("a,b,expected", [ (1, 2, 3), (0, 0, 0), @@ -297,7 +293,7 @@ def test_divide_by_zero(): def test_add_parametrized(a, b, expected): assert add(a, b) == expected -# Fixtures +# Фикстуры @pytest.fixture def sample_data(): return [1, 2, 3, 4, 5] @@ -307,33 +303,33 @@ def test_sum(sample_data): ``` ```bash -# Running tests -pytest # Run all tests -pytest test_calculator.py # Run one file -pytest -k "test_add" # Run matching tests -pytest -v # Verbose output -pytest --tb=short # Short tracebacks +# Запуск тестов +pytest # Запустить все тесты +pytest test_calculator.py # Запустить один файл +pytest -k "test_add" # Запустить подходящие тесты +pytest -v # Подробный вывод +pytest --tb=short # Короткие трассировки ``` -### Rust Built-in Testing +### Встроенное тестирование в Rust ```rust -// src/calculator.rs — tests live in the SAME file! +// src/calculator.rs — тесты находятся в ТОМ ЖЕ файле! fn add(a: i32, b: i32) -> i32 { a + b } fn divide(a: f64, b: f64) -> Result { if b == 0.0 { - Err("Division by zero".to_string()) + Err("Деление на ноль".to_string()) } else { Ok(a / b) } } -// Tests go in a #[cfg(test)] module — only compiled during `cargo test` +// Тесты размещаются в модуле #[cfg(test)]: он компилируется только при `cargo test` #[cfg(test)] mod tests { - use super::*; // Import everything from parent module + use super::*; // Импортировать всё из родительского модуля #[test] fn test_add() { @@ -355,49 +351,49 @@ mod tests { assert!(divide(1.0, 0.0).is_err()); } - // Test that something panics (like pytest.raises) + // Проверка, что что-то паникует (как pytest.raises) #[test] #[should_panic(expected = "out of bounds")] fn test_out_of_bounds() { let v = vec![1, 2, 3]; - let _ = v[99]; // Panics + let _ = v[99]; // Паника } } ``` ```bash -# Running tests -cargo test # Run all tests -cargo test test_add # Run matching tests -cargo test -- --nocapture # Show println! output -cargo test -p my_crate # Test one crate in workspace -cargo test -- --test-threads=1 # Sequential (for tests with side effects) +# Запуск тестов +cargo test # Запустить все тесты +cargo test test_add # Запустить подходящие тесты +cargo test -- --nocapture # Показать вывод println! +cargo test -p my_crate # Тестировать один крейт в рабочем пространстве +cargo test -- --test-threads=1 # Последовательно (для тестов с побочными эффектами) ``` -### Testing Quick Reference - -| pytest | Rust | Notes | -|--------|------|-------| -| `assert x == y` | `assert_eq!(x, y)` | Equality | -| `assert x != y` | `assert_ne!(x, y)` | Inequality | -| `assert condition` | `assert!(condition)` | Boolean | -| `assert condition, "msg"` | `assert!(condition, "msg")` | With message | -| `pytest.raises(E)` | `#[should_panic]` | Expect panic | -| `@pytest.fixture` | Setup in test or helper fn | No built-in fixtures | -| `@pytest.mark.parametrize` | `rstest` crate | Parameterized tests | -| `conftest.py` | `tests/common/mod.rs` | Shared test helpers | -| `pytest.skip()` | `#[ignore]` | Skip a test | -| `tmp_path` fixture | `tempfile` crate | Temporary directories | +### Краткая справка по тестированию + +| pytest | Rust | Примечания | +|--------|------|------------| +| `assert x == y` | `assert_eq!(x, y)` | Равенство | +| `assert x != y` | `assert_ne!(x, y)` | Неравенство | +| `assert condition` | `assert!(condition)` | Логическое условие | +| `assert condition, "msg"` | `assert!(condition, "msg")` | С сообщением | +| `pytest.raises(E)` | `#[should_panic]` | Ожидается паника | +| `@pytest.fixture` | Настройка в тесте или вспомогательной функции | Встроенных фикстур нет | +| `@pytest.mark.parametrize` | Крейт `rstest` | Параметризованные тесты | +| `conftest.py` | `tests/common/mod.rs` | Общие вспомогательные функции для тестов | +| `pytest.skip()` | `#[ignore]` | Пропустить тест | +| Фикстура `tmp_path` | Крейт `tempfile` | Временные каталоги | *** -## Parameterized Tests with rstest +## Параметризованные тесты с rstest ```rust // Cargo.toml: rstest = "0.23" use rstest::rstest; -// Like @pytest.mark.parametrize +// Как @pytest.mark.parametrize #[rstest] #[case(1, 2, 3)] #[case(0, 0, 0)] @@ -407,7 +403,7 @@ fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) { assert_eq!(add(a, b), expected); } -// Like @pytest.fixture +// Как @pytest.fixture use rstest::fixture; #[fixture] @@ -423,9 +419,9 @@ fn test_sum(sample_data: Vec) { *** -## Mocking with mockall +## Моки с mockall ```python -# Python — mocking with unittest.mock +# Python — моки с помощью unittest.mock from unittest.mock import Mock, patch def test_fetch_user(): @@ -438,12 +434,12 @@ def test_fetch_user(): ``` ```rust -// Rust — mocking with mockall crate +// Rust — моки с помощью крейта mockall // Cargo.toml: mockall = "0.13" use mockall::{automock, predicate::*}; -#[automock] // Generates MockDatabase automatically +#[automock] // Автоматически генерирует MockDatabase trait Database { fn get_user(&self, id: i64) -> Option; } @@ -467,15 +463,15 @@ fn test_fetch_user() { --- -## Exercises +## Упражнения
-🏋️ Exercise: Safe Wrapper Around Unsafe (click to expand) +🏋️ Упражнение: безопасная обёртка вокруг unsafe (нажмите, чтобы раскрыть) -**Challenge**: Write a safe function `split_at_mid` that takes a `&mut [i32]` and returns two mutable slices `(&mut [i32], &mut [i32])` split at the midpoint. Internally, use `unsafe` with raw pointers (simulating what `split_at_mut` does). Then wrap it in a safe API. +**Задание**: напишите безопасную функцию `split_at_mid`, которая принимает `&mut [i32]` и возвращает два изменяемых среза `(&mut [i32], &mut [i32])`, разделённых посередине. Внутри используйте `unsafe` с сырыми указателями (имитируя то, что делает `split_at_mut`). Затем оберните это в безопасный API.
-🔑 Solution +🔑 Решение ```rust fn split_at_mid(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) { @@ -483,10 +479,10 @@ fn split_at_mid(slice: &mut [i32]) -> (&mut [i32], &mut [i32]) { let ptr = slice.as_mut_ptr(); let len = slice.len(); - assert!(mid <= len); // Safety check before unsafe + assert!(mid <= len); // Проверка безопасности до unsafe - // SAFETY: mid <= len (asserted above), and ptr comes from a valid &mut slice, - // so both sub-slices are within bounds and non-overlapping. + // SAFETY: mid <= len (проверено выше), а ptr получен из корректного &mut среза, + // поэтому оба подсреза находятся в границах и не пересекаются. unsafe { ( std::slice::from_raw_parts_mut(ptr, mid), @@ -500,16 +496,15 @@ fn main() { let (left, right) = split_at_mid(&mut data); left[0] = 99; right[0] = 88; - println!("left: {left:?}, right: {right:?}"); - // left: [99, 2, 3], right: [88, 5, 6] + println!("слева: {left:?}, справа: {right:?}"); + // слева: [99, 2, 3], справа: [88, 5, 6] } ``` -**Key takeaway**: The `unsafe` block is small and guarded by the `assert!`. The public API is fully safe — callers never see `unsafe`. This is the Rust pattern: unsafe internals, safe interfaces. Python's `ctypes` gives you no such guarantees. +**Ключевой вывод**: блок `unsafe` небольшой и защищён `assert!`. Публичный API полностью безопасен: вызывающий код никогда не видит `unsafe`. Это паттерн Rust: небезопасные внутренности, безопасные интерфейсы. `ctypes` в Python не даёт таких гарантий.
*** - diff --git a/python-book/src/ch15-migration-patterns.md b/python-book/src/ch15-migration-patterns.md index 492e839..c1fb466 100644 --- a/python-book/src/ch15-migration-patterns.md +++ b/python-book/src/ch15-migration-patterns.md @@ -1,13 +1,12 @@ -## Common Python Patterns in Rust +## Частые паттерны Python в Rust -> **What you'll learn:** How to translate dict→struct, class→struct+impl, list comprehension→iterator chain, -> decorator→trait, and context manager→Drop/RAII. Plus essential crates and an incremental adoption strategy. +> **Что вы узнаете:** как переводить dict → struct, class → struct+impl, списочные включения → цепочки итераторов, декоратор → функции высшего порядка и контекстный менеджер → Drop/RAII. Плюс основные крейты и стратегия постепенного внедрения. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Dictionary → Struct +### Словарь → структура ```python -# Python — dict as data container (very common) +# Python — dict как контейнер данных (очень распространённо) user = { "name": "Alice", "age": 30, @@ -18,7 +17,7 @@ print(user["name"]) ``` ```rust -// Rust — struct with named fields +// Rust — структура с именованными полями #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct User { name: String, @@ -36,9 +35,9 @@ let user = User { println!("{}", user.name); ``` -### Context Manager → RAII (Drop) +### Контекстный менеджер → RAII (Drop) ```python -# Python — context manager for resource cleanup +# Python — контекстный менеджер для освобождения ресурсов class FileManager: def __init__(self, path): self.file = open(path, 'w') @@ -51,11 +50,11 @@ class FileManager: with FileManager("output.txt") as f: f.write("hello") -# File automatically closed when exiting `with` +# Файл автоматически закрывается при выходе из блока `with` ``` ```rust -// Rust — RAII: Drop trait runs when value goes out of scope +// Rust — RAII: трейт Drop выполняется, когда значение выходит из области видимости use std::fs::File; use std::io::Write; @@ -63,14 +62,14 @@ fn write_file() -> std::io::Result<()> { let mut file = File::create("output.txt")?; file.write_all(b"hello")?; Ok(()) - // File automatically closed when `file` goes out of scope - // No `with` needed — RAII handles it! + // Файл автоматически закрывается, когда `file` выходит из области видимости + // `with` не нужен: RAII делает всё сам! } ``` -### Decorator → Higher-Order Function or Macro +### Декоратор → функция высшего порядка или макрос ```python -# Python — decorator for timing +# Python — декоратор для замера времени import functools, time def timed(func): @@ -79,7 +78,7 @@ def timed(func): start = time.perf_counter() result = func(*args, **kwargs) elapsed = time.perf_counter() - start - print(f"{func.__name__} took {elapsed:.4f}s") + print(f"{func.__name__} выполнилась за {elapsed:.4f}s") return result return wrapper @@ -89,7 +88,7 @@ def slow_function(): ``` ```rust -// Rust — no decorators, use wrapper functions or macros +// Rust — никаких декораторов: используйте функции-обёртки или макросы use std::time::Instant; fn timed(name: &str, f: F) -> R @@ -98,20 +97,20 @@ where { let start = Instant::now(); let result = f(); - println!("{} took {:.4?}", name, start.elapsed()); + println!("{} выполнилась за {:.4?}", name, start.elapsed()); result } -// Usage: +// Использование: let result = timed("slow_function", || { std::thread::sleep(std::time::Duration::from_secs(1)); 42 }); ``` -### Iterator Pipeline (Data Processing) +### Конвейер итераторов (обработка данных) ```python -# Python — chain of transformations +# Python — цепочка преобразований import csv from collections import Counter @@ -128,7 +127,7 @@ def analyze_sales(filename): ``` ```rust -// Rust — iterator chains with strong types +// Rust — цепочки итераторов с сильной типизацией use std::collections::HashMap; #[derive(Debug, serde::Deserialize)] @@ -155,9 +154,9 @@ fn analyze_sales(filename: &str) -> Vec<(String, usize)> { } ``` -### Global Config / Singleton +### Глобальная конфигурация / синглтон ```python -# Python — module-level singleton (common pattern) +# Python — синглтон на уровне модуля (распространённый паттерн) # config.py import json @@ -171,11 +170,11 @@ class Config: cls._instance.data = json.load(f) return cls._instance -config = Config() # Module-level singleton +config = Config() # Синглтон на уровне модуля ``` ```rust -// Rust — OnceLock for lazy static initialization (Rust 1.70+) +// Rust — OnceLock для ленивой статической инициализации (Rust 1.70+) use std::sync::OnceLock; use serde_json::Value; @@ -184,80 +183,80 @@ static CONFIG: OnceLock = OnceLock::new(); fn get_config() -> &'static Value { CONFIG.get_or_init(|| { let data = std::fs::read_to_string("config.json") - .expect("Failed to read config"); + .expect("Не удалось прочитать конфигурацию"); serde_json::from_str(&data) - .expect("Failed to parse config") + .expect("Не удалось разобрать конфигурацию") }) } -// Usage anywhere: +// Использование в любом месте: let db_host = get_config()["database"]["host"].as_str().unwrap(); ``` *** -## Essential Crates for Python Developers - -### Data Processing & Serialization - -| Task | Python | Rust Crate | Notes | -|------|--------|-----------|-------| -| JSON | `json` | `serde_json` | Type-safe serialization | -| CSV | `csv`, `pandas` | `csv` | Streaming, low memory | -| YAML | `pyyaml` | `serde_yaml` | Config files | -| TOML | `tomllib` | `toml` | Config files | -| Data validation | `pydantic` | `serde` + custom | Compile-time validation | -| Date/time | `datetime` | `chrono` | Full timezone support | -| Regex | `re` | `regex` | Very fast | -| UUID | `uuid` | `uuid` | Same concept | - -### Web & Network - -| Task | Python | Rust Crate | Notes | -|------|--------|-----------|-------| -| HTTP client | `requests` | `reqwest` | Async-first | -| Web framework | `FastAPI`/`Flask` | `axum` / `actix-web` | Very fast | -| WebSocket | `websockets` | `tokio-tungstenite` | Async | -| gRPC | `grpcio` | `tonic` | Full support | -| Database (SQL) | `sqlalchemy` | `sqlx` / `diesel` | Compile-time checked SQL | -| Redis | `redis-py` | `redis` | Async support | - -### CLI & System - -| Task | Python | Rust Crate | Notes | -|------|--------|-----------|-------| -| CLI args | `argparse`/`click` | `clap` | Derive macros | -| Colored output | `colorama` | `colored` | Terminal colors | -| Progress bar | `tqdm` | `indicatif` | Same UX | -| File watching | `watchdog` | `notify` | Cross-platform | -| Logging | `logging` | `tracing` | Structured, async-ready | -| Env vars | `os.environ` | `std::env` + `dotenvy` | .env support | -| Subprocess | `subprocess` | `std::process::Command` | Built-in | -| Temp files | `tempfile` | `tempfile` | Same name! | - -### Testing - -| Task | Python | Rust Crate | Notes | -|------|--------|-----------|-------| -| Test framework | `pytest` | Built-in + `rstest` | `cargo test` | -| Mocking | `unittest.mock` | `mockall` | Trait-based | -| Property testing | `hypothesis` | `proptest` | Similar API | -| Snapshot testing | `syrupy` | `insta` | Snapshot approval | -| Benchmarking | `pytest-benchmark` | `criterion` | Statistical | -| Code coverage | `coverage.py` | `cargo-tarpaulin` | LLVM-based | +## Основные крейты для разработчиков на Python + +### Обработка данных и сериализация + +| Задача | Python | Крейт Rust | Примечания | +|--------|--------|------------|------------| +| JSON | `json` | `serde_json` | Типобезопасная сериализация | +| CSV | `csv`, `pandas` | `csv` | Потоковая обработка, мало памяти | +| YAML | `pyyaml` | `serde_yaml` | Файлы конфигурации | +| TOML | `tomllib` | `toml` | Файлы конфигурации | +| Валидация данных | `pydantic` | `serde` + собственная логика | Проверка на этапе компиляции | +| Дата и время | `datetime` | `chrono` | Полная поддержка часовых поясов | +| Регулярные выражения | `re` | `regex` | Очень быстро | +| UUID | `uuid` | `uuid` | Та же концепция | + +### Веб и сеть + +| Задача | Python | Крейт Rust | Примечания | +|--------|--------|------------|------------| +| HTTP-клиент | `requests` | `reqwest` | Асинхронный в первую очередь | +| Веб-фреймворк | `FastAPI`/`Flask` | `axum` / `actix-web` | Очень быстро | +| WebSocket | `websockets` | `tokio-tungstenite` | Асинхронный | +| gRPC | `grpcio` | `tonic` | Полная поддержка | +| База данных (SQL) | `sqlalchemy` | `sqlx` / `diesel` | SQL с проверкой на этапе компиляции | +| Redis | `redis-py` | `redis` | Поддержка async | + +### CLI и система + +| Задача | Python | Крейт Rust | Примечания | +|--------|--------|------------|------------| +| Аргументы CLI | `argparse`/`click` | `clap` | Макросы derive | +| Цветной вывод | `colorama` | `colored` | Цвета в терминале | +| Индикатор прогресса | `tqdm` | `indicatif` | Похожий UX | +| Отслеживание файлов | `watchdog` | `notify` | Кроссплатформенный | +| Логирование | `logging` | `tracing` | Структурированный, готов к async | +| Переменные окружения | `os.environ` | `std::env` + `dotenvy` | Поддержка .env | +| Подпроцессы | `subprocess` | `std::process::Command` | Встроен | +| Временные файлы | `tempfile` | `tempfile` | Одинаковое название! | + +### Тестирование + +| Задача | Python | Крейт Rust | Примечания | +|--------|--------|------------|------------| +| Фреймворк тестов | `pytest` | Встроенный + `rstest` | `cargo test` | +| Моки | `unittest.mock` | `mockall` | На основе трейтов | +| Property-тестирование | `hypothesis` | `proptest` | Похожий API | +| Снапшот-тестирование | `syrupy` | `insta` | Подтверждение снимков | +| Бенчмарки | `pytest-benchmark` | `criterion` | Статистические | +| Покрытие кода | `coverage.py` | `cargo-tarpaulin` | На основе LLVM | *** -## Incremental Adoption Strategy +## Стратегия постепенного внедрения ```mermaid flowchart TB - A["1️⃣ Profile Python
(find hotspots)"] --> B["2️⃣ Write Rust Extension
(PyO3 + maturin)"] - B --> C["3️⃣ Replace Python Call
(same API)"] - C --> D["4️⃣ Expand Gradually
(more functions)"] - D --> E{"Full rewrite
worth it?"} - E -->|Yes| F["Pure Rust🦀"] - E -->|No| G["Hybrid🐍+🦀"] + A["1️⃣ Профилируйте Python
(найдите узкие места)"] --> B["2️⃣ Напишите расширение на Rust
(PyO3 + maturin)"] + B --> C["3️⃣ Замените вызов Python
(тот же API)"] + C --> D["4️⃣ Расширяйте постепенно
(больше функций)"] + D --> E{"Полная переписка
оправдана?"} + E -->|Да| F["Чистый Rust🦀"] + E -->|Нет| G["Гибрид🐍+🦀"] style A fill:#ffeeba style B fill:#fff3cd style C fill:#d4edda @@ -266,64 +265,64 @@ flowchart TB style G fill:#c3e6cb ``` -> 📌 **See also**: [Ch. 14 — Unsafe Rust and FFI](ch14-unsafe-rust-and-ffi.md) covers the low-level FFI details needed for PyO3 bindings. +> 📌 **См. также**: [Гл. 14 — Unsafe Rust и FFI](ch14-unsafe-rust-and-ffi.md) описывает низкоуровневые детали FFI, необходимые для привязок PyO3. -### Step 1: Identify Hotspots +### Шаг 1: найдите узкие места ```python -# Profile your Python code first +# Сначала профилируйте код на Python import cProfile -cProfile.run('main()') # Find the CPU-intensive functions +cProfile.run('main()') # Находим функции, которые сильно грузят CPU -# Or use py-spy for sampling profiler: +# Или используйте py-spy как профилировщик сэмплирования: # py-spy top --pid # py-spy record -o profile.svg -- python main.py ``` -### Step 2: Write Rust Extension for Hotspot +### Шаг 2: напишите расширение на Rust для узкого места ```bash -# Create a Rust extension with maturin +# Создаём расширение на Rust с помощью maturin cd my_python_project maturin init --bindings pyo3 -# Write the hot function in Rust (see PyO3 section above) -# Build and install: +# Пишем горячую функцию на Rust (см. раздел про PyO3 выше) +# Собираем и устанавливаем: maturin develop --release ``` -### Step 3: Replace Python Call with Rust Call +### Шаг 3: замените вызов Python вызовом Rust ```python -# Before: -result = python_hot_function(data) # Slow +# До: +result = python_hot_function(data) # Медленно -# After: +# После: import my_rust_extension -result = my_rust_extension.hot_function(data) # Fast! +result = my_rust_extension.hot_function(data) # Быстро! -# Same API, same tests, 10-100x faster +# Тот же API, те же тесты, в 10–100 раз быстрее ``` -### Step 4: Expand Gradually +### Шаг 4: расширяйте постепенно ```rust -Week 1-2: Replace one CPU-bound function with Rust -Week 3-4: Replace data parsing/validation layer -Month 2: Replace core data pipeline -Month 3+: Consider full Rust rewrite if benefits justify it +Неделя 1–2: заменить одну функцию, ограниченную CPU, на Rust +Неделя 3–4: заменить слой разбора и валидации данных +Месяц 2: заменить основной конвейер данных +Месяц 3+: рассмотреть полную переписку на Rust, если выгода оправдывает затраты -Key principle: keep Python for orchestration, use Rust for computation. +Ключевой принцип: оставьте Python для оркестрации, а Rust для вычислений. ``` --- -## 💼 Case Study: Accelerating a Data Pipeline with PyO3 +## 💼 Кейс: ускорение конвейера данных с PyO3 -A fintech startup has a Python data pipeline that processes 2GB of daily transaction CSV files. The critical bottleneck is a validation + transformation step: +Финтех-стартап обрабатывает ежедневно файлы CSV с транзакциями общим объёмом 2 ГБ на Python. Критическое узкое место — шаг валидации и преобразования: ```python -# Python — the slow part (~12 minutes for 2GB) +# Python — медленная часть (~12 минут на 2 ГБ) import csv from decimal import Decimal from datetime import datetime @@ -333,12 +332,12 @@ def validate_and_transform(filepath: str) -> list[dict]: with open(filepath) as f: reader = csv.DictReader(f) for row in reader: - # Parse and validate each field + # Разбираем и проверяем каждое поле amount = Decimal(row["amount"]) if amount < 0: - raise ValueError(f"Negative amount: {amount}") + raise ValueError(f"Отрицательная сумма: {amount}") date = datetime.strptime(row["date"], "%Y-%m-%d") - category = categorize(row["merchant"]) # String matching, ~50 rules + category = categorize(row["merchant"]) # Поиск по строкам, ~50 правил results.append({ "amount_cents": int(amount * 100), @@ -347,15 +346,15 @@ def validate_and_transform(filepath: str) -> list[dict]: "merchant": row["merchant"].strip().lower(), }) return results -# ~12 minutes for 15M rows. Tried pandas — got to ~8 minutes but 6GB RAM. +# ~12 минут на 15 млн строк. Пробовали pandas: вышло ~8 минут, но 6 ГБ ОЗУ. ``` -**Step 1**: Profile and identify the hotspot (CSV parsing + Decimal conversion + string matching = 95% of time). +**Шаг 1**: профилируем и находим узкое место (разбор CSV + преобразование Decimal + поиск по строкам = 95% времени). -**Step 2**: Write the Rust extension: +**Шаг 2**: пишем расширение на Rust: ```rust -// src/lib.rs — PyO3 extension +// src/lib.rs — расширение PyO3 use pyo3::prelude::*; use pyo3::types::PyList; use std::fs::File; @@ -370,7 +369,7 @@ struct Transaction { } fn categorize(merchant: &str) -> &'static str { - // Aho-Corasick or simple rules — compiled once, blazing fast + // Aho-Corasick или простые правила: компилируются один раз и работают очень быстро if merchant.contains("amazon") { "shopping" } else if merchant.contains("uber") || merchant.contains("lyft") { "transport" } else if merchant.contains("starbucks") { "food" } @@ -382,13 +381,13 @@ fn process_transactions(path: &str) -> PyResult) -> PyResult<()> { } ``` -**Step 3**: Replace one line in Python: +**Шаг 3**: заменяем одну строку в Python: ```python -# Before: -results = validate_and_transform("transactions.csv") # 12 minutes +# До: +results = validate_and_transform("transactions.csv") # 12 минут -# After: +# После: import fast_pipeline -results = fast_pipeline.process_transactions("transactions.csv") # 45 seconds +results = fast_pipeline.process_transactions("transactions.csv") # 45 секунд -# Same Python orchestration, same tests, same deployment -# Just one function replaced +# Та же оркестрация на Python, те же тесты, то же развёртывание +# Заменена только одна функция ``` -**Results**: -| Metric | Python (csv + Decimal) | Rust (PyO3 + csv crate) | -|--------|----------------------|------------------------| -| Time (2GB / 15M rows) | 12 minutes | 45 seconds | -| Peak memory | 6GB (pandas) / 2GB (csv) | 200MB | -| Lines changed in Python | — | 1 (import + call) | -| Rust code written | — | ~60 lines | -| Tests passing | 47/47 | 47/47 (unchanged) | +**Результаты**: -> **Key lesson**: You don't need to rewrite your whole application. Find the 5% of code that takes 95% of the time, rewrite that in Rust with PyO3, and keep everything else in Python. The team went from "we need to add more servers" to "one server is enough." +| Метрика | Python (csv + Decimal) | Rust (PyO3 + крейт csv) | +|---------|------------------------|-------------------------| +| Время (2 ГБ / 15 млн строк) | 12 минут | 45 секунд | +| Пиковая память | 6 ГБ (pandas) / 2 ГБ (csv) | 200 МБ | +| Строк изменено в Python | — | 1 (import + вызов) | +| Написано кода на Rust | — | ~60 строк | +| Пройденные тесты | 47/47 | 47/47 (без изменений) | + +> **Ключевой урок**: переписывать всё приложение не нужно. Найдите 5% кода, на который уходит 95% времени, перепишите его на Rust с PyO3, а всё остальное оставьте на Python. Команда перешла от «нам нужно добавить серверов» к «одного сервера достаточно». --- -## Exercises +## Упражнения
-🏋️ Exercise: Migration Decision Matrix (click to expand) +🏋️ Упражнение: матрица решений по миграции (нажмите, чтобы раскрыть) -**Challenge**: You have a Python web application with these components. For each one, decide: **Keep in Python**, **Rewrite in Rust**, or **PyO3 bridge**. Justify each choice. +**Задание**: у вас есть веб-приложение на Python с такими компонентами. Для каждого решите: **оставить на Python**, **переписать на Rust** или **мост PyO3**. Обоснуйте каждый выбор. -1. Flask route handlers (request parsing, JSON responses) -2. Image thumbnail generation (CPU-bound, processes 10k images/day) -3. Database ORM queries (SQLAlchemy) -4. CSV parser for 2GB financial files (runs nightly) -5. Admin dashboard (Jinja2 templates) +1. Обработчики маршрутов Flask (разбор запросов, ответы JSON) +2. Генерация миниатюр изображений (ограничено CPU, 10 тыс. изображений в день) +3. Запросы ORM к базе данных (SQLAlchemy) +4. Разбор CSV для финансовых файлов по 2 ГБ (запускается ночью) +5. Панель администратора (шаблоны Jinja2)
-🔑 Solution +🔑 Решение -| Component | Decision | Rationale | +| Компонент | Решение | Обоснование | |---|---|---| -| Flask route handlers | 🐍 Keep Python | I/O-bound, framework-heavy, low benefit from Rust | -| Image thumbnail generation | 🦀 PyO3 bridge | CPU-bound hot path, keep Python API, Rust internals | -| Database ORM queries | 🐍 Keep Python | SQLAlchemy is mature, queries are I/O-bound | -| CSV parser (2GB) | 🦀 PyO3 bridge or full Rust | CPU + memory bound, Rust's zero-copy parsing shines | -| Admin dashboard | 🐍 Keep Python | UI/template code, no performance concern | +| Обработчики маршрутов Flask | 🐍 Оставить на Python | Ограничено вводом-выводом, завязано на фреймворк, выигрыш от Rust мал | +| Генерация миниатюр изображений | 🦀 Мост PyO3 | Горячий путь, ограниченный CPU: API остаётся на Python, внутренности на Rust | +| Запросы ORM к базе данных | 🐍 Оставить на Python | SQLAlchemy зрелый, запросы ограничены вводом-выводом | +| Разбор CSV (2 ГБ) | 🦀 Мост PyO3 или полный Rust | Ограничено CPU и памятью, здесь хорошо работает разбор без копирования | +| Панель администратора | 🐍 Оставить на Python | Код UI и шаблонов, производительность не проблема | -**Key takeaway**: The migration sweet spot is CPU-bound, performance-critical code that has a clean boundary. Don't rewrite glue code or I/O-bound handlers — the gains don't justify the cost. +**Ключевой вывод**: золотая середина миграции — это код, ограниченный CPU и критичный к производительности, с чистой границей. Не переписывайте клеящий код и обработчики, ограниченные вводом-выводом: выигрыш не окупит затрат.
*** - diff --git a/python-book/src/ch16-best-practices.md b/python-book/src/ch16-best-practices.md index 88cd584..2ee71e1 100644 --- a/python-book/src/ch16-best-practices.md +++ b/python-book/src/ch16-best-practices.md @@ -1,16 +1,15 @@ -## Idiomatic Rust for Python Developers +## Идиоматичный Rust для разработчиков на Python -> **What you'll learn:** Top 10 habits to build, common pitfalls with fixes, a structured 3-month learning path, -> the complete Python→Rust "Rosetta Stone" reference table, and recommended learning resources. +> **Что вы узнаете:** десять привычек, которые стоит выработать, частые ошибки и их исправления, структурированный трёхмесячный план обучения, полную справочную таблицу «Розеттский камень» Python → Rust и рекомендуемые ресурсы для изучения. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний ```mermaid flowchart LR - A["🟢 Week 1-2
Foundations
'Why won't this compile?'"] --> B["🟡 Week 3-4
Core Concepts
'Oh, it's protecting me'"] - B --> C["🟡 Month 2
Intermediate
'I see why this matters'"] - C --> D["🔴 Month 3+
Advanced
'Caught a bug at compile time!'"] - D --> E["🏆 Month 6
Fluent
'Better programmer everywhere'"] + A["🟢 Недели 1–2
Основы
«Почему это не компилируется?»"] --> B["🟡 Недели 3–4
Ключевые концепции
«А, оно меня защищает»"] + B --> C["🟡 Месяц 2
Средний уровень
«Теперь вижу, зачем это нужно»"] + C --> D["🔴 Месяц 3+
Продвинутый уровень
«Поймал баг на этапе компиляции!»"] + D --> E["🏆 Месяц 6
Свободное владение
«Стал лучшим программистом везде»"] style A fill:#d4edda style B fill:#fff3cd style C fill:#fff3cd @@ -18,203 +17,193 @@ flowchart LR style E fill:#c3e6cb,stroke:#28a745 ``` -### Top 10 Habits to Build +### Десять привычек, которые стоит выработать -1. **Use `match` on enums instead of `if isinstance()`** +1. **Используйте `match` для перечислений вместо `if isinstance()`** ```python # Python # Rust if isinstance(shape, Circle): ... match shape { Shape::Circle(r) => ... } ``` -2. **Let the compiler guide you** — Read error messages carefully. Rust's - compiler is the best in any language. It tells you what's wrong AND how to fix it. +2. **Доверяйте компилятору.** Внимательно читайте сообщения об ошибках. Компилятор Rust — лучший среди всех языков. Он говорит, что не так, И как это исправить. -3. **Prefer `&str` over `String` in function parameters** — Accept the most - general type. `&str` works with both `String` and string literals. +3. **Предпочитайте `&str` вместо `String` в параметрах функций.** Принимайте наиболее общий тип. `&str` работает и с `String`, и с строковыми литералами. -4. **Use iterators instead of index loops** — Iterator chains are more idiomatic - and often faster than `for i in 0..vec.len()`. +4. **Используйте итераторы вместо циклов по индексу.** Цепочки итераторов идиоматичнее и часто быстрее, чем `for i in 0..vec.len()`. -5. **Embrace `Option` and `Result`** — Don't `.unwrap()` everything. Use `?`, - `map`, `and_then`, `unwrap_or_else`. +5. **Используйте `Option` и `Result` в полной мере.** Не вызывайте `.unwrap()` везде. Используйте `?`, `map`, `and_then`, `unwrap_or_else`. -6. **Derive traits liberally** — `#[derive(Debug, Clone, PartialEq)]` should be - on most structs. It's free and makes testing easier. +6. **Щедро используйте derive для трейтов.** `#[derive(Debug, Clone, PartialEq)]` должен стоять на большинстве структур. Это бесплатно и упрощает тестирование. -7. **Use `cargo clippy` religiously** — It catches hundreds of style and correctness - issues. Treat it like `ruff` for Rust. +7. **Регулярно запускайте `cargo clippy`.** Он находит сотни проблем со стилем и корректностью. Относитесь к нему как к `ruff` для Rust. -8. **Don't fight the borrow checker** — If you're fighting it, you're probably - structuring data wrong. Refactor to make ownership clear. +8. **Не боритесь с borrow checker.** Если вы с ним воюете, скорее всего, данные структурированы неправильно. Переделайте структуру, чтобы владение стало понятным. -9. **Use enums for state machines** — Instead of string flags or booleans, use - enums. The compiler ensures you handle every state. +9. **Используйте перечисления для конечных автоматов.** Вместо строковых флагов или булевых значений используйте перечисления. Компилятор следит, чтобы обрабатывалось каждое состояние. -10. **Clone first, optimize later** — When learning, use `.clone()` freely to - avoid ownership complexity. Optimize only when profiling shows a need. +10. **Сначала клонируйте, потом оптимизируйте.** На этапе изучения свободно используйте `.clone()`, чтобы не разбираться со сложностью владения. Оптимизируйте, только когда профилирование покажет необходимость. -### Common Mistakes from Python Developers +### Частые ошибки разработчиков на Python -| Mistake | Why | Fix | -|---------|-----|-----| -| `.unwrap()` everywhere | Panics at runtime | Use `?` or `match` | -| String instead of &str | Unnecessary allocation | Use `&str` for params | -| `for i in 0..vec.len()` | Not idiomatic | `for item in &vec` | -| Ignoring clippy warnings | Miss easy improvements | `cargo clippy` | -| Too many `.clone()` calls | Performance overhead | Refactor ownership | -| Giant main() function | Hard to test | Extract into lib.rs | -| Not using `#[derive()]` | Re-inventing the wheel | Derive common traits | -| Panicking on errors | Not recoverable | Return `Result` | +| Ошибка | Почему плохо | Исправление | +|--------|--------------|-------------| +| `.unwrap()` везде | Паника во время выполнения | Используйте `?` или `match` | +| `String` вместо `&str` | Лишнее выделение памяти | Используйте `&str` в параметрах | +| `for i in 0..vec.len()` | Не идиоматично | `for item in &vec` | +| Игнорирование предупреждений clippy | Упускаете простые улучшения | `cargo clippy` | +| Слишком много вызовов `.clone()` | Накладные расходы | Пересмотрите владение | +| Огромная функция main() | Трудно тестировать | Вынесите код в lib.rs | +| Не используете `#[derive()]` | Изобретаете велосипед | Derive для общих трейтов | +| Паника при ошибках | Не восстанавливается | Возвращайте `Result` | *** -## Performance Comparison +## Сравнение производительности -### Benchmark: Common Operations +### Бенчмарк: частые операции ```text -Operation Python 3.12 Rust (release) Speedup +Операция Python 3.12 Rust (release) Ускорение ───────────────────── ──────────── ────────────── ───────── -Fibonacci(40) ~25s ~0.3s ~80x -Sort 10M integers ~5.2s ~0.6s ~9x -JSON parse 100MB ~8.5s ~0.4s ~21x -Regex 1M matches ~3.1s ~0.3s ~10x -HTTP server (req/s) ~5,000 ~150,000 ~30x -SHA-256 1GB file ~12s ~1.2s ~10x -CSV parse 1M rows ~4.5s ~0.2s ~22x -String concatenation ~2.1s ~0.05s ~42x +Фибоначчи(40) ~25s ~0.3s ~80x +Сортировка 10 млн целых ~5.2s ~0.6s ~9x +Разбор JSON 100 МБ ~8.5s ~0.4s ~21x +Регулярные выражения: 1 млн совпадений ~3.1s ~0.3s ~10x +HTTP-сервер (запросов/с) ~5,000 ~150,000 ~30x +SHA-256 для файла 1 ГБ ~12s ~1.2s ~10x +Разбор CSV: 1 млн строк ~4.5s ~0.2s ~22x +Конкатенация строк ~2.1s ~0.05s ~42x ``` -> **Note**: Python with C extensions (NumPy, etc.) dramatically narrows the gap -> for numerical work. These benchmarks compare pure Python vs pure Rust. +> **Примечание**: Python с C-расширениями (NumPy и др.) заметно сокращает разрыв в численных расчётах. Эти бенчмарки сравнивают чистый Python с чистым Rust. -### Memory Usage +### Использование памяти ```text Python: Rust: ───────── ───── -- Object header: 28 bytes/object - No object header -- int: 28 bytes (even for 0) - i32: 4 bytes, i64: 8 bytes -- str "hello": 54 bytes - &str "hello": 16 bytes (ptr + len) -- list of 1000 ints: ~36 KB - Vec: ~4 KB - (8 KB pointers + 28 KB int objects) -- dict of 100 items: ~5.5 KB - HashMap of 100: ~2.4 KB - -Total for typical application: -- Python: 50-200 MB baseline - Rust: 1-5 MB baseline +- Заголовок объекта: 28 байт/объект - Заголовка объекта нет +- int: 28 байт (даже для 0) - i32: 4 байта, i64: 8 байт +- str "hello": 54 байта - &str "hello": 16 байт (указатель + длина) +- список из 1000 int: ~36 КБ - Vec: ~4 КБ + (8 КБ указатели + 28 КБ объекты int) +- dict из 100 элементов: ~5,5 КБ - HashMap из 100 элементов: ~2,4 КБ + +Итого для типичного приложения: +- Python: базово 50–200 МБ - Rust: базово 1–5 МБ ``` *** -## Common Pitfalls and Solutions +## Частые ошибки и их решения -### Pitfall 1: "The Borrow Checker Won't Let Me" +### Ловушка 1: «Borrow checker не даёт» ```rust -// Problem: trying to iterate and modify +// Проблема: попытка одновременно итерировать и менять коллекцию let mut items = vec![1, 2, 3, 4, 5]; // for item in &items { -// if *item > 3 { items.push(*item * 2); } // ❌ Can't borrow mut while borrowed +// if *item > 3 { items.push(*item * 2); } // ❌ Нельзя взять изменяемую ссылку, пока есть заимствование // } -// Solution 1: collect changes, apply after +// Решение 1: собрать изменения и применить их после let additions: Vec = items.iter() .filter(|&&x| x > 3) .map(|&x| x * 2) .collect(); items.extend(additions); -// Solution 2: use retain/extend +// Решение 2: используйте retain/extend items.retain(|&x| x <= 3); ``` -### Pitfall 2: "Too Many String Types" +### Ловушка 2: «Слишком много строковых типов» ```rust -// When in doubt: -// - &str for function parameters -// - String for struct fields and return values -// - &str literals ("hello") work everywhere &str is expected +// Если сомневаетесь: +// - &str для параметров функций +// - String для полей структур и возвращаемых значений +// - литералы &str ("hello") работают везде, где ожидается &str -fn process(input: &str) -> String { // Accept &str, return String +fn process(input: &str) -> String { // Принимаем &str, возвращаем String format!("Processed: {}", input) } ``` -### Pitfall 3: "I Miss Python's Simplicity" +### Ловушка 3: «Мне не хватает простоты Python» ```rust -// Python one-liner: +// Однострочник на Python: // result = [x**2 for x in data if x > 0] -// Rust equivalent: +// Эквивалент на Rust: let result: Vec = data.iter() .filter(|&&x| x > 0) .map(|&x| x * x) .collect(); -// It's more verbose, but: -// - Type-safe at compile time -// - 10-100x faster -// - No runtime type errors possible -// - Explicit about memory allocation (.collect()) +// Он многословнее, зато: +// - типобезопасен на этапе компиляции +// - в 10–100 раз быстрее +// - ошибки типов во время выполнения невозможны +// - явно видно выделение памяти (.collect()) ``` -### Pitfall 4: "Where's My REPL?" +### Ловушка 4: «А где мой REPL?» ```rust -// Rust has no REPL. Instead: -// 1. Use `cargo test` as your REPL — write small tests to try things -// 2. Use Rust Playground (play.rust-lang.org) for quick experiments -// 3. Use `dbg!()` macro for quick debug output -// 4. Use `cargo watch -x test` for auto-running tests on save +// В Rust нет REPL. Вместо этого: +// 1. Используйте `cargo test` как REPL: пишите небольшие тесты, чтобы что-то проверить +// 2. Используйте Rust Playground (play.rust-lang.org) для быстрых экспериментов +// 3. Используйте макрос `dbg!()` для быстрого отладочного вывода +// 4. Используйте `cargo watch -x test`, чтобы тесты запускались при сохранении #[test] fn playground() { - // Use this as your "REPL" — run with `cargo test playground` + // Используйте это как свой «REPL»: запускайте через `cargo test playground` let result = "hello world" .split_whitespace() .map(|w| w.to_uppercase()) .collect::>(); - dbg!(&result); // Prints: [src/main.rs:5] &result = ["HELLO", "WORLD"] + dbg!(&result); // Выводит: [src/main.rs:5] &result = ["HELLO", "WORLD"] } ``` *** -## Learning Path and Resources - -### Week 1-2: Foundations -- [ ] Install Rust, set up VS Code with rust-analyzer -- [ ] Complete chapters 1-4 of this guide (types, control flow) -- [ ] Write 5 small programs converting Python scripts to Rust -- [ ] Get comfortable with `cargo build`, `cargo test`, `cargo clippy` - -### Week 3-4: Core Concepts -- [ ] Complete chapters 5-8 (structs, enums, ownership, modules) -- [ ] Rewrite a Python data processing script in Rust -- [ ] Practice with `Option` and `Result` until natural -- [ ] Read compiler error messages carefully — they're teaching you - -### Month 2: Intermediate -- [ ] Complete chapters 9-12 (error handling, traits, iterators) -- [ ] Build a CLI tool with `clap` and `serde` -- [ ] Write a PyO3 extension for a Python project hotspot -- [ ] Practice iterator chains until they feel like comprehensions - -### Month 3: Advanced -- [ ] Complete chapters 13-16 (concurrency, unsafe, testing) -- [ ] Build a web service with `axum` and `tokio` -- [ ] Contribute to an open-source Rust project -- [ ] Read "Programming Rust" (O'Reilly) for deeper understanding - -### Recommended Resources -- **The Rust Book**: https://doc.rust-lang.org/book/ (official, excellent) -- **Rust by Example**: https://doc.rust-lang.org/rust-by-example/ (learn by doing) -- **Rustlings**: https://github.com/rust-lang/rustlings (exercises) -- **Rust Playground**: https://play.rust-lang.org/ (online compiler) -- **This Week in Rust**: https://this-week-in-rust.org/ (newsletter) -- **PyO3 Guide**: https://pyo3.rs/ (Python ↔ Rust bridge) +## Путь обучения и ресурсы + +### Недели 1–2: основы +- [ ] Установите Rust и настройте VS Code с rust-analyzer +- [ ] Пройдите главы 1–4 этого руководства (типы, управление потоком) +- [ ] Напишите 5 небольших программ, переписав скрипты Python на Rust +- [ ] Освойтесь с `cargo build`, `cargo test`, `cargo clippy` + +### Недели 3–4: ключевые концепции +- [ ] Пройдите главы 5–8 (структуры, перечисления, владение, модули) +- [ ] Перепишите на Rust скрипт обработки данных на Python +- [ ] Практикуйтесь с `Option` и `Result`, пока это не станет естественным +- [ ] Внимательно читайте сообщения компилятора: они вас учат + +### Месяц 2: средний уровень +- [ ] Пройдите главы 9–12 (обработка ошибок, трейты, итераторы) +- [ ] Создайте CLI-утилиту с `clap` и `serde` +- [ ] Напишите расширение PyO3 для узкого места в проекте на Python +- [ ] Практикуйтесь с цепочками итераторов, пока они не станут похожи на списочные включения + +### Месяц 3: продвинутый уровень +- [ ] Пройдите главы 13–16 (конкурентность, unsafe, тестирование) +- [ ] Создайте веб-сервис с `axum` и `tokio` +- [ ] Внесите вклад в проект Rust с открытым исходным кодом +- [ ] Прочитайте «Programming Rust» (O'Reilly), чтобы глубже понять язык + +### Рекомендуемые ресурсы +- **The Rust Book**: https://doc.rust-lang.org/book/ (официальная книга, отличная) +- **Rust by Example**: https://doc.rust-lang.org/rust-by-example/ (учитесь на практике) +- **Rustlings**: https://github.com/rust-lang/rustlings (упражнения) +- **Rust Playground**: https://play.rust-lang.org/ (онлайн-компилятор) +- **This Week in Rust**: https://this-week-in-rust.org/ (рассылка) +- **PyO3 Guide**: https://pyo3.rs/ (мост Python ↔ Rust) - **Comprehensive Rust** (Google): https://google.github.io/comprehensive-rust/ -### Python → Rust Rosetta Stone +### Шпаргалка Python → Rust -| Python | Rust | Chapter | -|--------|------|---------| +| Python | Rust | Глава | +|--------|------|-------| | `list` | `Vec` | 5 | | `dict` | `HashMap` | 5 | | `set` | `HashSet` | 5 | @@ -225,12 +214,12 @@ fn playground() { | `None` | `Option` | 6 | | `raise`/`try`/`except` | `Result` + `?` | 9 | | `Protocol` (PEP 544) | `trait` | 10 | -| `TypeVar` | Generics `` | 10 | -| `__dunder__` methods | Traits (Display, Add, etc.) | 10 | +| `TypeVar` | Обобщения `` | 10 | +| `__dunder__` методы | Трейты (Display, Add и др.) | 10 | | `lambda` | `\|args\| body` | 12 | -| generator `yield` | `impl Iterator` | 12 | -| list comprehension | `.map().filter().collect()` | 12 | -| `@decorator` | Higher-order fn or macro | 12a, 15 | +| генератор `yield` | `impl Iterator` | 12 | +| списочное включение | `.map().filter().collect()` | 12 | +| `@decorator` | Функция высшего порядка или макрос | 12a, 15 | | `asyncio` | `tokio` | 13 | | `threading` | `std::thread` | 13 | | `multiprocessing` | `rayon` | 13 | @@ -239,48 +228,48 @@ fn playground() { | `pip install` | `cargo add` | 8 | | `requirements.txt` | `Cargo.lock` | 8 | | `pyproject.toml` | `Cargo.toml` | 8 | -| `with` (context mgr) | Scope-based `Drop` | 15 | +| `with` (контекстный менеджер) | Освобождение по области видимости (`Drop`) | 15 | | `json.dumps/loads` | `serde_json` | 15 | *** -## Final Thoughts for Python Developers +## Заключительные мысли для разработчиков на Python ```rust -What you'll miss from Python: -- REPL and interactive exploration -- Rapid prototyping speed -- Rich ML/AI ecosystem (PyTorch, etc.) -- "Just works" dynamic typing -- pip install and immediate use - -What you'll gain from Rust: -- "If it compiles, it works" confidence -- 10-100x performance improvement -- No more runtime type errors -- No more None/null crashes -- True parallelism (no GIL!) -- Single binary deployment -- Predictable memory usage -- The best compiler error messages in any language - -The journey: -Week 1: "Why does the compiler hate me?" -Week 2: "Oh, it's actually protecting me from bugs" -Month 1: "I see why this matters" -Month 2: "I caught a bug at compile time that would've been a production incident" -Month 3: "I don't want to go back to untyped code" -Month 6: "Rust has made me a better programmer in every language" +Чего вам будет не хватать из Python: +- REPL и интерактивного исследования +- Скорости быстрого прототипирования +- Богатой экосистемы ML/AI (PyTorch и др.) +- Динамической типизации, которая «просто работает» +- pip install и мгновенного использования + +Что вы получите от Rust: +- Уверенность «если компилируется, значит, работает» +- Прирост производительности в 10–100 раз +- Больше никаких ошибок типов во время выполнения +- Больше никаких падений из-за None/null +- Настоящий параллелизм (без GIL!) +- Развёртывание одним бинарником +- Предсказуемое потребление памяти +- Лучшие сообщения об ошибках компилятора среди всех языков + +Путь: +Неделя 1: «Почему компилятор меня ненавидит?» +Неделя 2: «А, он на самом деле защищает меня от багов» +Месяц 1: «Теперь вижу, зачем это нужно» +Месяц 2: «Поймал баг на этапе компиляции, который стал бы инцидентом в продакшене» +Месяц 3: «Не хочу возвращаться к коду без типов» +Месяц 6: «Rust сделал меня лучшим программистом на любом языке» ``` --- -## Exercises +## Упражнения
-🏋️ Exercise: Code Review Checklist (click to expand) +🏋️ Упражнение: чек-лист код-ревью (нажмите, чтобы раскрыть) -**Challenge**: Review this Rust code (written by a Python developer) and identify 5 idiomatic improvements: +**Задание**: проверьте этот код на Rust (его написал разработчик на Python) и найдите 5 идиоматичных улучшений: ```rust fn get_name(names: Vec, index: i32) -> String { @@ -300,16 +289,16 @@ fn main() { ```
-🔑 Solution +🔑 Решение -Five improvements: +Пять улучшений: ```rust -// 1. Take &[String] not Vec (don't take ownership of the whole vec) -// 2. Use usize for index (not i32 — indices are always non-negative) -// 3. Return Option<&str> instead of empty string (use the type system!) -// 4. Use .get() instead of bounds-checking manually -// 5. Don't clone() in main — pass a reference +// 1. Принимайте &[String], а не Vec (не забирайте владение всем вектором) +// 2. Используйте usize для индекса (не i32: индексы всегда неотрицательны) +// 3. Возвращайте Option<&str> вместо пустой строки (используйте систему типов!) +// 4. Используйте .get() вместо ручной проверки границ +// 5. Не вызывайте clone() в main: передавайте ссылку fn get_name(names: &[String], index: usize) -> Option<&str> { names.get(index).map(|s| s.as_str()) @@ -319,16 +308,16 @@ fn main() { let names = vec!["Alice".to_string(), "Bob".to_string()]; match get_name(&names, 0) { Some(name) => println!("{name}"), - None => println!("Not found"), + None => println!("Не найдено"), } } ``` -**Key takeaway**: Python habits that hurt in Rust: cloning everything (use borrows), using sentinel values like `""` (use `Option`), taking ownership when borrowing suffices, and using signed integers for indices. +**Ключевой вывод**: привычки Python, которые мешают в Rust: клонирование всего (используйте заимствования), значения-заглушки вроде `""` (используйте `Option`), забирание владения там, где достаточно заимствования, и знаковые целые для индексов.
*** -*End of Rust for Python Programmers Training Guide* +*Конец учебного руководства «Rust для программистов на Python»* diff --git a/python-book/src/ch17-capstone-project.md b/python-book/src/ch17-capstone-project.md index 8b2c127..cee66b0 100644 --- a/python-book/src/ch17-capstone-project.md +++ b/python-book/src/ch17-capstone-project.md @@ -1,32 +1,31 @@ -## Capstone Project: Build a CLI Task Manager +## Итоговый проект: CLI-менеджер задач -> **What you'll learn:** Tie together everything from the course by building a complete Rust CLI application -> that a Python developer would typically write with `argparse` + `json` + `pathlib`. +> **Что вы узнаете:** как объединить всё изученное в курсе, написав полноценное CLI-приложение на Rust. Обычно такое приложение пишут на Python с помощью `argparse` + `json` + `pathlib`. > -> **Difficulty:** 🔴 Advanced - -This capstone project exercises concepts from every major chapter: -- **Ch. 3**: Types and variables (structs, enums) -- **Ch. 5**: Collections (`Vec`, `HashMap`) -- **Ch. 6**: Enums and pattern matching (task status, commands) -- **Ch. 7**: Ownership and borrowing (passing references) -- **Ch. 9**: Error handling (`Result`, `?`, custom errors) -- **Ch. 10**: Traits (`Display`, `FromStr`) -- **Ch. 11**: Type conversions (`From`, `TryFrom`) -- **Ch. 12**: Iterators and closures (filtering, mapping) -- **Ch. 8**: Modules (organized project structure) +> **Сложность:** 🔴 Продвинутый + +Этот итоговый проект использует концепции из каждой ключевой главы: +- **Гл. 3**: типы и переменные (структуры, перечисления) +- **Гл. 5**: коллекции (`Vec`, `HashMap`) +- **Гл. 6**: перечисления и сопоставление с образцом (статус задачи, команды) +- **Гл. 7**: владение и заимствование (передача ссылок) +- **Гл. 9**: обработка ошибок (`Result`, `?`, собственные ошибки) +- **Гл. 10**: трейты (`Display`, `FromStr`) +- **Гл. 11**: преобразования типов (`From`, `TryFrom`) +- **Гл. 12**: итераторы и замыкания (фильтрация, преобразование) +- **Гл. 8**: модули (организация структуры проекта) *** -## The Project: `rustdo` +## Проект: `rustdo` -A command-line task manager (like Python's `todo.txt` tools) that stores tasks in a JSON file. +Менеджер задач для командной строки (вроде инструментов `todo.txt` в Python), который хранит задачи в JSON-файле. -### Python Equivalent (what you'd write in Python) +### Аналог на Python (что вы написали бы на Python) ```python #!/usr/bin/env python3 -"""A simple CLI task manager — the Python version.""" +"""Простой CLI-менеджер задач: версия на Python.""" import json import sys from pathlib import Path @@ -57,17 +56,17 @@ def load_tasks() -> list[Task]: def save_tasks(tasks: list[Task]): TASK_FILE.write_text(json.dumps([t.__dict__ for t in tasks], indent=2)) -# Commands: add, list, done, remove, stats -# ... (you know how this goes in Python) +# Команды: add, list, done, remove, stats +# ... (вы и так знаете, как это делается на Python) ``` -### Your Rust Implementation +### Ваша реализация на Rust -Build this step-by-step. Each step maps to concepts from specific chapters. +Собирайте проект по шагам. Каждый шаг соответствует концепциям из определённых глав. *** -## Step 1: Define the Data Model (Ch. 3, 6, 10, 11) +## Шаг 1: определите модель данных (гл. 3, 6, 10, 11) ```rust // src/task.rs @@ -76,7 +75,7 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use chrono::Local; -/// Task priority — maps to Python's Priority(Enum) +/// Приоритет задачи: соответствует Priority(Enum) в Python #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Priority { @@ -85,7 +84,7 @@ pub enum Priority { High, } -// Display trait (Python's __str__) +// Трейт Display (аналог __str__ в Python) impl fmt::Display for Priority { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -96,7 +95,7 @@ impl fmt::Display for Priority { } } -// FromStr trait (parsing "high" → Priority::High) +// Трейт FromStr (разбор "high" → Priority::High) impl FromStr for Priority { type Err = String; @@ -105,12 +104,12 @@ impl FromStr for Priority { "low" | "l" => Ok(Priority::Low), "medium" | "med" | "m" => Ok(Priority::Medium), "high" | "h" => Ok(Priority::High), - other => Err(format!("unknown priority: '{other}' (use low/medium/high)")), + other => Err(format!("неизвестный приоритет: '{other}' (используйте low/medium/high)")), } } } -/// A single task — maps to Python's Task class +/// Одна задача: соответствует классу Task в Python #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Task { pub id: u32, @@ -145,11 +144,11 @@ impl fmt::Display for Task { } ``` -> **Python comparison**: In Python you'd use `@dataclass` + `Enum`. In Rust, `struct` + `enum` + `derive` macros give you serialization, display, and parsing for free. +> **Сравнение с Python**: в Python вы бы использовали `@dataclass` + `Enum`. В Rust `struct` + `enum` + макросы `derive` дают сериализацию, вывод и разбор бесплатно. *** -## Step 2: Storage Layer (Ch. 9, 7) +## Шаг 2: слой хранения (гл. 9, 7) ```rust // src/storage.rs @@ -157,24 +156,24 @@ use std::fs; use std::path::PathBuf; use crate::task::Task; -/// Get the path to the task file (~/.rustdo.json) +/// Путь к файлу задач (~/.rustdo.json) fn task_file_path() -> PathBuf { - let home = dirs::home_dir().expect("Could not determine home directory"); + let home = dirs::home_dir().expect("Не удалось определить домашний каталог"); home.join(".rustdo.json") } -/// Load tasks from disk — returns empty Vec if file doesn't exist +/// Загрузить задачи с диска: возвращает пустой Vec, если файла нет pub fn load_tasks() -> Result, Box> { let path = task_file_path(); if !path.exists() { return Ok(Vec::new()); } - let content = fs::read_to_string(&path)?; // ? propagates io::Error - let tasks: Vec = serde_json::from_str(&content)?; // ? propagates serde error + let content = fs::read_to_string(&path)?; // ? передаёт io::Error дальше + let tasks: Vec = serde_json::from_str(&content)?; // ? передаёт ошибку serde дальше Ok(tasks) } -/// Save tasks to disk +/// Сохранить задачи на диск pub fn save_tasks(tasks: &[Task]) -> Result<(), Box> { let path = task_file_path(); let json = serde_json::to_string_pretty(tasks)?; @@ -183,17 +182,17 @@ pub fn save_tasks(tasks: &[Task]) -> Result<(), Box> { } ``` -> **Python comparison**: Python uses `Path.read_text()` + `json.loads()`. Rust uses `fs::read_to_string()` + `serde_json::from_str()`. Note the `?` — every error is explicit and propagated. +> **Сравнение с Python**: Python использует `Path.read_text()` + `json.loads()`. Rust использует `fs::read_to_string()` + `serde_json::from_str()`. Обратите внимание на `?`: каждая ошибка явная и передаётся дальше. *** -## Step 3: Command Enum (Ch. 6) +## Шаг 3: перечисление команд (гл. 6) ```rust // src/command.rs use crate::task::Priority; -/// All possible commands — one enum variant per action +/// Все возможные команды: один вариант перечисления на каждое действие pub enum Command { Add { title: String, priority: Priority }, List { show_done: bool }, @@ -204,13 +203,13 @@ pub enum Command { } impl Command { - /// Parse command-line arguments into a Command - /// (In production, you'd use `clap` — this is educational) + /// Разбирает аргументы командной строки в Command + /// (в реальном проекте используйте `clap`, а здесь это учебный пример) pub fn parse(args: &[String]) -> Result { match args.first().map(|s| s.as_str()) { Some("add") => { let title = args.get(1) - .ok_or("usage: rustdo add [priority]")? + .ok_or("использование: rustdo add <title> [priority]")? .clone(); let priority = args.get(2) .map(|p| p.parse::<Priority>()) @@ -225,16 +224,16 @@ impl Command { } Some("done") => { let id: u32 = args.get(1) - .ok_or("usage: rustdo done <id>")? + .ok_or("использование: rustdo done <id>")? .parse() - .map_err(|_| "id must be a number")?; + .map_err(|_| "id должен быть числом")?; Ok(Command::Done { id }) } Some("remove") => { let id: u32 = args.get(1) - .ok_or("usage: rustdo remove <id>")? + .ok_or("использование: rustdo remove <id>")? .parse() - .map_err(|_| "id must be a number")?; + .map_err(|_| "id должен быть числом")?; Ok(Command::Remove { id }) } Some("stats") => Ok(Command::Stats), @@ -244,11 +243,11 @@ impl Command { } ``` -> **Python comparison**: Python uses `argparse` or `click`. This hand-rolled parser shows how `match` on enum-like patterns replaces Python's if/elif chains. For real projects, use the `clap` crate. +> **Сравнение с Python**: Python использует `argparse` или `click`. Этот самописный разбор показывает, как `match` по шаблонам, похожим на перечисления, заменяет цепочки if/elif в Python. Для реальных проектов используйте крейт `clap`. *** -## Step 4: Business Logic (Ch. 5, 12, 7) +## Шаг 4: бизнес-логика (гл. 5, 12, 7) ```rust // src/actions.rs @@ -259,7 +258,7 @@ pub fn add_task(title: String, priority: Priority) -> Result<(), Box<dyn std::er let mut tasks = storage::load_tasks()?; let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; let task = Task::new(next_id, title.clone(), priority); - println!("Added: {task}"); + println!("Добавлено: {task}"); tasks.push(task); storage::save_tasks(&tasks)?; Ok(()) @@ -268,28 +267,28 @@ pub fn add_task(title: String, priority: Priority) -> Result<(), Box<dyn std::er pub fn list_tasks(show_done: bool) -> Result<(), Box<dyn std::error::Error>> { let tasks = storage::load_tasks()?; let filtered: Vec<&Task> = tasks.iter() - .filter(|t| show_done || !t.done) // Iterator + closure (Ch. 12) + .filter(|t| show_done || !t.done) // Итератор + замыкание (гл. 12) .collect(); if filtered.is_empty() { - println!("No tasks! 🎉"); + println!("Задач нет! 🎉"); return Ok(()); } for task in &filtered { - println!(" {task}"); // Uses Display trait (Ch. 10) + println!(" {task}"); // Использует трейт Display (гл. 10) } - println!("\n{} task(s) shown", filtered.len()); + println!("\nПоказано задач: {}", filtered.len()); Ok(()) } pub fn complete_task(id: u32) -> Result<(), Box<dyn std::error::Error>> { let mut tasks = storage::load_tasks()?; let task = tasks.iter_mut() - .find(|t| t.id == id) // Iterator::find (Ch. 12) - .ok_or(format!("No task with id {id}"))?; + .find(|t| t.id == id) // Iterator::find (гл. 12) + .ok_or(format!("Задачи с id {id} нет"))?; task.done = true; - println!("Completed: {task}"); + println!("Выполнено: {task}"); storage::save_tasks(&tasks)?; Ok(()) } @@ -297,11 +296,11 @@ pub fn complete_task(id: u32) -> Result<(), Box<dyn std::error::Error>> { pub fn remove_task(id: u32) -> Result<(), Box<dyn std::error::Error>> { let mut tasks = storage::load_tasks()?; let len_before = tasks.len(); - tasks.retain(|t| t.id != id); // Vec::retain (Ch. 5) + tasks.retain(|t| t.id != id); // Vec::retain (гл. 5) if tasks.len() == len_before { - return Err(format!("No task with id {id}").into()); + return Err(format!("Задачи с id {id} нет").into()); } - println!("Removed task {id}"); + println!("Удалена задача {id}"); storage::save_tasks(&tasks)?; Ok(()) } @@ -312,27 +311,27 @@ pub fn show_stats() -> Result<(), Box<dyn std::error::Error>> { let done = tasks.iter().filter(|t| t.done).count(); let pending = total - done; - // Group by priority using iterators (Ch. 12) + // Группировка по приоритету с помощью итераторов (гл. 12) let high = tasks.iter().filter(|t| !t.done && t.priority == Priority::High).count(); let medium = tasks.iter().filter(|t| !t.done && t.priority == Priority::Medium).count(); let low = tasks.iter().filter(|t| !t.done && t.priority == Priority::Low).count(); - println!("📊 Task Statistics"); - println!(" Total: {total}"); - println!(" Done: {done} ✅"); - println!(" Pending: {pending}"); - println!(" 🔴 High: {high}"); - println!(" 🟡 Medium: {medium}"); - println!(" 🟢 Low: {low}"); + println!("📊 Статистика задач"); + println!(" Всего: {total}"); + println!(" Выполнено: {done} ✅"); + println!(" В работе: {pending}"); + println!(" 🔴 Высокий: {high}"); + println!(" 🟡 Средний: {medium}"); + println!(" 🟢 Низкий: {low}"); Ok(()) } ``` -> **Key Rust patterns used**: `iter().map().max()`, `iter().filter().collect()`, `iter_mut().find()`, `retain()`, `iter().filter().count()`. These replace Python's list comprehensions, `next(x for x in ...)`, and `Counter`. +> **Используемые паттерны Rust**: `iter().map().max()`, `iter().filter().collect()`, `iter_mut().find()`, `retain()`, `iter().filter().count()`. Они заменяют списочные включения Python, `next(x for x in ...)` и `Counter`. *** -## Step 5: Wire It Together (Ch. 8) +## Шаг 5: собираем всё вместе (гл. 8) ```rust // src/main.rs @@ -348,7 +347,7 @@ fn main() { let command = match Command::parse(&args) { Ok(cmd) => cmd, Err(e) => { - eprintln!("Error: {e}"); + eprintln!("Ошибка: {e}"); std::process::exit(1); } }; @@ -366,28 +365,28 @@ fn main() { }; if let Err(e) = result { - eprintln!("Error: {e}"); + eprintln!("Ошибка: {e}"); std::process::exit(1); } } fn print_help() { - println!("rustdo — a task manager for Pythonistas learning Rust\n"); - println!("USAGE:"); - println!(" rustdo add <title> [low|medium|high] Add a task"); - println!(" rustdo list [--all] List pending tasks"); - println!(" rustdo done <id> Mark task complete"); - println!(" rustdo remove <id> Remove a task"); - println!(" rustdo stats Show statistics"); + println!("rustdo — менеджер задач для питонистов, которые изучают Rust\n"); + println!("ИСПОЛЬЗОВАНИЕ:"); + println!(" rustdo add <title> [low|medium|high] Добавить задачу"); + println!(" rustdo list [--all] Показать невыполненные задачи"); + println!(" rustdo done <id> Отметить задачу выполненной"); + println!(" rustdo remove <id> Удалить задачу"); + println!(" rustdo stats Показать статистику"); } ``` ```mermaid graph TD - CLI["main.rs<br/>(CLI entry)"] --> CMD["command.rs<br/>(parse args)"] - CMD --> ACT["actions.rs<br/>(business logic)"] - ACT --> STORE["storage.rs<br/>(JSON persistence)"] - ACT --> TASK["task.rs<br/>(data model)"] + CLI["main.rs<br/>(точка входа CLI)"] --> CMD["command.rs<br/>(разбор аргументов)"] + CMD --> ACT["actions.rs<br/>(бизнес-логика)"] + ACT --> STORE["storage.rs<br/>(хранение в JSON)"] + ACT --> TASK["task.rs<br/>(модель данных)"] STORE --> TASK style CLI fill:#d4edda style CMD fill:#fff3cd @@ -398,7 +397,7 @@ graph TD *** -## Step 6: Cargo.toml Dependencies +## Шаг 6: зависимости в Cargo.toml ```toml [package] @@ -413,14 +412,14 @@ chrono = "0.4" dirs = "5" ``` -> **Python equivalent**: This is your `pyproject.toml` `[project.dependencies]`. `cargo add serde serde_json chrono dirs` is like `pip install`. +> **Аналог в Python**: это ваш `[project.dependencies]` из `pyproject.toml`. `cargo add serde serde_json chrono dirs` — аналог `pip install`. *** -## Step 7: Tests (Ch. 14) +## Шаг 7: тесты (гл. 14) ```rust -// src/task.rs — add at the bottom +// src/task.rs — добавьте в конец файла #[cfg(test)] mod tests { use super::*; @@ -439,7 +438,7 @@ mod tests { let display = format!("{task}"); assert!(display.contains("Write Rust")); assert!(display.contains("🔴")); - assert!(display.contains("⬜")); // Not done yet + assert!(display.contains("⬜")); // Ещё не выполнена } #[test] @@ -453,15 +452,15 @@ mod tests { } ``` -> **Python equivalent**: `pytest` tests. Run with `cargo test` instead of `pytest`. No test discovery magic needed — `#[test]` marks test functions explicitly. +> **Аналог в Python**: тесты `pytest`. Запускайте их через `cargo test` вместо `pytest`. Никакой магии поиска тестов не нужно: `#[test]` явно помечает тестовые функции. *** -## Stretch Goals +## Дополнительные задания -Once you have the basic version working, try these enhancements: +Когда базовая версия заработает, попробуйте эти улучшения: -1. **Add `clap` for argument parsing** — Replace the hand-rolled parser with `clap`'s derive macros: +1. **Добавьте `clap` для разбора аргументов**: замените самописный разбор макросами derive из `clap`: ```rust #[derive(Parser)] enum Command { @@ -473,31 +472,32 @@ Once you have the basic version working, try these enhancements: } ``` -2. **Add colored output** — Use the `colored` crate for terminal colors (like Python's `colorama`). +2. **Добавьте цветной вывод**: используйте крейт `colored` для цветов в терминале (аналог `colorama` в Python). -3. **Add due dates** — Add an `Option<NaiveDate>` field and filter overdue tasks. +3. **Добавьте сроки выполнения**: добавьте поле `Option<NaiveDate>` и фильтруйте просроченные задачи. -4. **Add tags/categories** — Use `Vec<String>` for tags and filter with `.iter().any()`. +4. **Добавьте теги и категории**: используйте `Vec<String>` для тегов и фильтруйте через `.iter().any()`. -5. **Make it a library + binary** — Split into `lib.rs` + `main.rs` so the logic is reusable (Ch. 8 module pattern). +5. **Сделайте библиотеку и исполняемый файл**: разделите код на `lib.rs` и `main.rs`, чтобы логику можно было переиспользовать (паттерн модулей, гл. 8). *** -## What You Practiced +## Что вы попрактиковали -| Chapter | Concept | Where It Appeared | -|---------|---------|-------------------| -| Ch. 3 | Types and variables | `Task` struct fields, `u32`, `String`, `bool` | -| Ch. 5 | Collections | `Vec<Task>`, `retain()`, `push()` | -| Ch. 6 | Enums + match | `Priority`, `Command`, exhaustive matching | -| Ch. 7 | Ownership + borrowing | `&[Task]` vs `Vec<Task>`, `&mut` for completion | -| Ch. 8 | Modules | `mod task; mod storage; mod command; mod actions;` | -| Ch. 9 | Error handling | `Result<T, E>`, `?` operator, `.ok_or()` | -| Ch. 10 | Traits | `Display`, `FromStr`, `Serialize`, `Deserialize` | -| Ch. 11 | From/Into | `FromStr` for Priority, `.into()` for error conversion | -| Ch. 12 | Iterators | `filter`, `map`, `find`, `count`, `collect` | -| Ch. 14 | Testing | `#[test]`, `#[cfg(test)]`, assertion macros | +| Глава | Концепция | Где использовалась | +|-------|-----------|--------------------| +| Гл. 3 | Типы и переменные | Поля структуры `Task`, `u32`, `String`, `bool` | +| Гл. 5 | Коллекции | `Vec<Task>`, `retain()`, `push()` | +| Гл. 6 | Перечисления и match | `Priority`, `Command`, исчерпывающее сопоставление | +| Гл. 7 | Владение и заимствование | `&[Task]` и `Vec<Task>`, `&mut` для отметки о выполнении | +| Гл. 8 | Модули | `mod task; mod storage; mod command; mod actions;` | +| Гл. 9 | Обработка ошибок | `Result<T, E>`, оператор `?`, `.ok_or()` | +| Гл. 10 | Трейты | `Display`, `FromStr`, `Serialize`, `Deserialize` | +| Гл. 11 | From/Into | `FromStr` для Priority, `.into()` для преобразования ошибок | +| Гл. 12 | Итераторы | `filter`, `map`, `find`, `count`, `collect` | +| Гл. 14 | Тестирование | `#[test]`, `#[cfg(test)]`, макросы проверок | -> 🎓 **Congratulations!** If you've built this project, you've used every major Rust concept covered in this book. You're no longer a Python developer learning Rust — you're a Rust developer who also knows Python. +> 🎓 **Поздравляем!** Если вы создали этот проект, вы использовали каждую ключевую концепцию Rust, которая разобрана в этой книге. Вы больше не программист на Python, который учит Rust, а разработчик на Rust, который знает Python. *** + From cefea2b1bb1cc2cec394054bcf99f35361dbbab2 Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 09:47:21 +0400 Subject: [PATCH 13/18] Translate C/C++ book TOC and chapters 1-14 to Russian (WIP) Translate prose, headings, tables, diagram labels, code comments and user-facing strings in c-cpp-book/src for SUMMARY.md and ch01 through ch14. Code identifiers, crate names, file names and links are unchanged. Chapter-level TOC anchors in ch00 and cross-chapter anchors are updated in a follow-up commit once the remaining chapters are translated. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- c-cpp-book/src/SUMMARY.md | 76 ++-- .../ch01-1-why-c-cpp-developers-need-rust.md | 358 +++++++-------- .../src/ch01-introduction-and-motivation.md | 220 ++++----- c-cpp-book/src/ch02-getting-started.md | 156 ++++--- c-cpp-book/src/ch03-built-in-types.md | 57 ++- c-cpp-book/src/ch04-control-flow.md | 44 +- c-cpp-book/src/ch05-data-structures.md | 416 +++++++++--------- .../src/ch06-enums-and-pattern-matching.md | 119 +++-- ...h07-1-lifetimes-and-borrowing-deep-dive.md | 198 ++++----- ...-smart-pointers-and-interior-mutability.md | 266 +++++------ .../src/ch07-ownership-and-borrowing.md | 251 ++++++----- c-cpp-book/src/ch08-1-testing-patterns.md | 143 +++--- c-cpp-book/src/ch08-crates-and-modules.md | 369 ++++++++-------- .../ch09-1-error-handling-best-practices.md | 183 ++++---- c-cpp-book/src/ch09-error-handling.md | 267 ++++++----- c-cpp-book/src/ch10-1-generics.md | 111 +++-- c-cpp-book/src/ch10-traits.md | 266 +++++------ c-cpp-book/src/ch11-from-and-into-traits.md | 63 ++- c-cpp-book/src/ch12-1-iterator-power-tools.md | 133 +++--- c-cpp-book/src/ch12-closures.md | 189 ++++---- c-cpp-book/src/ch13-concurrency.md | 185 ++++---- c-cpp-book/src/ch14-unsafe-rust-and-ffi.md | 266 ++++++----- 22 files changed, 2153 insertions(+), 2183 deletions(-) diff --git a/c-cpp-book/src/SUMMARY.md b/c-cpp-book/src/SUMMARY.md index d79ec72..cc9b482 100644 --- a/c-cpp-book/src/SUMMARY.md +++ b/c-cpp-book/src/SUMMARY.md @@ -1,50 +1,50 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I — Foundations - -- [1. Introduction and Motivation](ch01-introduction-and-motivation.md) - - [Why C/C++ Developers Need Rust](ch01-1-why-c-cpp-developers-need-rust.md) -- [2. Getting Started](ch02-getting-started.md) -- [3. Built-in Types](ch03-built-in-types.md) -- [4. Control Flow](ch04-control-flow.md) -- [5. Data Structures](ch05-data-structures.md) -- [6. Enums and Pattern Matching](ch06-enums-and-pattern-matching.md) -- [7. Ownership and Borrowing](ch07-ownership-and-borrowing.md) - - [Lifetimes and Borrowing Deep Dive](ch07-1-lifetimes-and-borrowing-deep-dive.md) - - [Smart Pointers and Interior Mutability](ch07-2-smart-pointers-and-interior-mutability.md) -- [8. Crates and Modules](ch08-crates-and-modules.md) - - [Testing Patterns](ch08-1-testing-patterns.md) -- [9. Error Handling](ch09-error-handling.md) - - [Error Handling Best Practices](ch09-1-error-handling-best-practices.md) -- [10. Traits](ch10-traits.md) - - [Generics](ch10-1-generics.md) -- [11. From and Into Traits](ch11-from-and-into-traits.md) -- [12. Closures](ch12-closures.md) - - [Iterator Power Tools](ch12-1-iterator-power-tools.md) -- [13. Concurrency](ch13-concurrency.md) -- [14. Unsafe Rust and FFI](ch14-unsafe-rust-and-ffi.md) +# Часть I — Основы + +- [1. Введение и мотивация](ch01-introduction-and-motivation.md) + - [Почему разработчикам C/C++ нужен Rust](ch01-1-why-c-cpp-developers-need-rust.md) +- [2. Начало работы](ch02-getting-started.md) +- [3. Встроенные типы](ch03-built-in-types.md) +- [4. Управление потоком выполнения](ch04-control-flow.md) +- [5. Структуры данных](ch05-data-structures.md) +- [6. Перечисления и сопоставление с образцом](ch06-enums-and-pattern-matching.md) +- [7. Владение и заимствование](ch07-ownership-and-borrowing.md) + - [Времена жизни и заимствование: углублённый разбор](ch07-1-lifetimes-and-borrowing-deep-dive.md) + - [Умные указатели и внутренняя изменяемость](ch07-2-smart-pointers-and-interior-mutability.md) +- [8. Крейты и модули](ch08-crates-and-modules.md) + - [Паттерны тестирования](ch08-1-testing-patterns.md) +- [9. Обработка ошибок](ch09-error-handling.md) + - [Лучшие практики обработки ошибок](ch09-1-error-handling-best-practices.md) +- [10. Трейты](ch10-traits.md) + - [Обобщения](ch10-1-generics.md) +- [11. Трейты From и Into](ch11-from-and-into-traits.md) +- [12. Замыкания](ch12-closures.md) + - [Инструменты для работы с итераторами](ch12-1-iterator-power-tools.md) +- [13. Конкурентность](ch13-concurrency.md) +- [14. Unsafe Rust и FFI](ch14-unsafe-rust-and-ffi.md) --- -# Part II — Deep Dives +# Часть II — Углублённые темы -- [15. no_std — Rust Without the Standard Library](ch15-no_std-rust-without-the-standard-library.md) - - [Embedded Deep Dive](ch15-1-embedded-deep-dive.md) -- [16. Case Studies: Real-World C++ to Rust](ch16-case-studies.md) - - [Case Studies 3–5: Lifetimes, Composability, Trait Objects](ch16-cases-3-5-lifetime-borrowing.md) +- [15. no_std — Rust без стандартной библиотеки](ch15-no_std-rust-without-the-standard-library.md) + - [Углублённо о встраиваемых системах](ch15-1-embedded-deep-dive.md) +- [16. Примеры из практики: переход от C++ к Rust](ch16-case-studies.md) + - [Примеры 3–5: времена жизни, компонуемость, трейт-объекты](ch16-cases-3-5-lifetime-borrowing.md) --- -# Part III — Best Practices & Reference +# Часть III — Лучшие практики и справочник -- [17. Best Practices](ch17-best-practices.md) - - [Avoiding Excessive clone()](ch17-1-avoiding-excessive-clone.md) - - [Avoiding Unchecked Indexing](ch17-2-avoiding-unchecked-indexing.md) - - [Collapsing Assignment Pyramids](ch17-3-collapsing-assignment-pyramids.md) - - [Logging and Tracing Ecosystem](ch17-4-logging-and-tracing-ecosystem.md) -- [18. C++ → Rust Semantic Deep Dives](ch18-cpp-rust-semantic-deep-dives.md) -- [19. Rust Macros: From Preprocessor to Metaprogramming](ch19-macros.md) +- [17. Лучшие практики](ch17-best-practices.md) + - [Избегание избыточного clone()](ch17-1-avoiding-excessive-clone.md) + - [Избегание неконтролируемой индексации](ch17-2-avoiding-unchecked-indexing.md) + - [Схлопывание пирамид присваиваний](ch17-3-collapsing-assignment-pyramids.md) + - [Экосистема логирования и трассировки](ch17-4-logging-and-tracing-ecosystem.md) +- [18. Семантические различия C++ и Rust](ch18-cpp-rust-semantic-deep-dives.md) +- [19. Макросы Rust: от препроцессора к метапрограммированию](ch19-macros.md) diff --git a/c-cpp-book/src/ch01-1-why-c-cpp-developers-need-rust.md b/c-cpp-book/src/ch01-1-why-c-cpp-developers-need-rust.md index 6183913..d9c0c35 100644 --- a/c-cpp-book/src/ch01-1-why-c-cpp-developers-need-rust.md +++ b/c-cpp-book/src/ch01-1-why-c-cpp-developers-need-rust.md @@ -1,48 +1,48 @@ -# Why C/C++ Developers Need Rust - -> **What you'll learn:** -> - The full list of problems Rust eliminates — memory safety, undefined behavior, data races, and more -> - Why `shared_ptr`, `unique_ptr`, and other C++ mitigations are bandaids, not solutions -> - Concrete C and C++ vulnerability examples that are structurally impossible in safe Rust - -> **Want to skip straight to code?** Jump to [Show me some code](ch02-getting-started.md#enough-talk-already-show-me-some-code) - -## What Rust Eliminates — The Complete List - -Before diving into examples, here's the executive summary. Safe Rust **structurally prevents** every issue in this list — not through discipline, tooling, or code review, but through the type system and compiler: - -| **Eliminated Issue** | **C** | **C++** | **How Rust Prevents It** | -|----------------------|:-----:|:-------:|--------------------------| -| Buffer overflows / underflows | ✅ | ✅ | All arrays, slices, and strings carry bounds; indexing is checked at runtime | -| Memory leaks (no GC needed) | ✅ | ✅ | `Drop` trait = RAII done right; automatic cleanup, no Rule of Five | -| Dangling pointers | ✅ | ✅ | Lifetime system proves references outlive their referent at compile time | -| Use-after-free | ✅ | ✅ | Ownership system makes this a compile error | -| Use-after-move | — | ✅ | Moves are **destructive** — the original binding ceases to exist | -| Uninitialized variables | ✅ | ✅ | All variables must be initialized before use; compiler enforces it | -| Integer overflow / underflow UB | ✅ | ✅ | Debug builds panic on overflow; release builds wrap (defined behavior either way) | -| NULL pointer dereferences / SEGVs | ✅ | ✅ | No null pointers; `Option<T>` forces explicit handling | -| Data races | ✅ | ✅ | `Send`/`Sync` traits + borrow checker make data races a compile error | -| Uncontrolled side-effects | ✅ | ✅ | Immutability by default; mutation requires explicit `mut` | -| No inheritance (better maintainability) | — | ✅ | Traits + composition replace class hierarchies; promotes reuse without coupling | -| No exceptions; predictable control flow | — | ✅ | Errors are values (`Result<T, E>`); impossible to ignore, no hidden `throw` paths | -| Iterator invalidation | — | ✅ | Borrow checker forbids mutating a collection while iterating | -| Reference cycles / leaked finalizers | — | ✅ | Ownership is tree-shaped; `Rc` cycles are opt-in and catchable with `Weak` | -| No forgotten mutex unlocks | ✅ | ✅ | `Mutex<T>` wraps the data; lock guard is the only way to access it | -| Undefined behavior (general) | ✅ | ✅ | Safe Rust has **zero** undefined behavior; `unsafe` blocks are explicit and auditable | - -> **Bottom line:** These aren't aspirational goals enforced by coding standards. They are **compile-time guarantees**. If your code compiles, these bugs cannot exist. +# Почему разработчикам C/C++ нужен Rust + +> **Что вы узнаете:** +> - Полный список проблем, которые устраняет Rust: безопасность памяти, неопределённое поведение, гонки данных и многое другое +> - Почему `shared_ptr`, `unique_ptr` и другие средства C++ — это пластыри, а не решения +> - Конкретные примеры уязвимостей C и C++, которые в безопасном Rust структурно невозможны + +> **Хотите сразу перейти к коду?** Перейдите к разделу [Покажите мне код](ch02-getting-started.md#хватит-разговоров-покажите-код) + +## Что Rust устраняет — полный список + +Прежде чем переходить к примерам, вот краткое резюме. Безопасный Rust **структурно предотвращает** каждую проблему из этого списка — не дисциплиной, инструментами или код-ревью, а системой типов и компилятором: + +| **Устранённая проблема** | **C** | **C++** | **Как Rust её предотвращает** | +|--------------------------|:-----:|:-------:|-------------------------------| +| Переполнения и недополнения буфера | ✅ | ✅ | У всех массивов, срезов и строк есть границы; индексация проверяется во время выполнения | +| Утечки памяти (без сборщика мусора) | ✅ | ✅ | Трейт `Drop` = RAII, сделанный правильно; автоматическая очистка, правило пяти не нужно | +| Висячие указатели | ✅ | ✅ | Система времён жизни доказывает, что ссылки переживают объект, на который ссылаются, на этапе компиляции | +| Использование после освобождения | ✅ | ✅ | Система владения делает это ошибкой компиляции | +| Использование после перемещения | — | ✅ | Перемещения **разрушающие** — исходная привязка перестаёт существовать | +| Неинициализированные переменные | ✅ | ✅ | Все переменные должны быть инициализированы до использования; это обеспечивает компилятор | +| Неопределённое поведение при переполнении целых | ✅ | ✅ | В отладочных сборках переполнение вызывает panic; в релизных — переполняется (в обоих случаях поведение определено) | +| Разыменование NULL-указателей / SEGV | ✅ | ✅ | Нулевых указателей нет; `Option<T>` вынуждает явно обрабатывать отсутствие значения | +| Гонки данных | ✅ | ✅ | Трейты `Send`/`Sync` и проверка заимствований делают гонки данных ошибкой компиляции | +| Неконтролируемые побочные эффекты | ✅ | ✅ | Неизменяемость по умолчанию; для изменения нужен явный `mut` | +| Отсутствие наследования (лучшая сопровождаемость) | — | ✅ | Трейты и композиция заменяют иерархии классов; способствуют повторному использованию без жёсткой связанности | +| Отсутствие исключений; предсказуемый поток управления | — | ✅ | Ошибки — это значения (`Result<T, E>`); их невозможно проигнорировать, нет скрытых путей `throw` | +| Инвалидация итераторов | — | ✅ | Проверка заимствований запрещает изменять коллекцию во время итерации по ней | +| Циклы ссылок / утечки финализаторов | — | ✅ | Владение имеет древовидную структуру; циклы `Rc` — явный выбор, который можно обнаружить и разорвать с помощью `Weak` | +| Забытые разблокировки мьютексов | ✅ | ✅ | `Mutex<T>` оборачивает данные; защитник блокировки — единственный способ доступа к ним | +| Неопределённое поведение (вообще) | ✅ | ✅ | В безопасном Rust **нет** неопределённого поведения; блоки `unsafe` явные и поддаются аудиту | + +> **Главное:** это не желаемые цели, которые навязываются правилами кодирования. Это **гарантии на этапе компиляции**. Если ваш код компилируется, эти ошибки не могут существовать. --- -## The Problems Shared by C and C++ +## Проблемы, общие для C и C++ -> **Want to skip the examples?** Jump to [How Rust Addresses All of This](#how-rust-addresses-all-of-this) or straight to [Show me some code](ch02-getting-started.md#enough-talk-already-show-me-some-code) +> **Хотите пропустить примеры?** Перейдите к разделу [Как Rust решает всё это](#как-rust-решает-всё-это) или сразу к [Покажите мне код](ch02-getting-started.md#хватит-разговоров-покажите-код) -Both languages share a core set of memory safety problems that are the root cause of over 70% of CVEs (Common Vulnerabilities and Exposures): +Оба языка разделяют базовый набор проблем безопасности памяти, которые являются первопричиной более 70% CVE (Common Vulnerabilities and Exposures — общеизвестных уязвимостей и подверженностей): -### Buffer overflows +### Переполнения буфера -C arrays, pointers, and strings have no intrinsic bounds. It is trivially easy to exceed them: +Массивы, указатели и строки в C не имеют встроенных границ. Выйти за них очень легко: ```c #include <stdlib.h> @@ -50,70 +50,70 @@ C arrays, pointers, and strings have no intrinsic bounds. It is trivially easy t void buffer_dangers() { char buffer[10]; - strcpy(buffer, "This string is way too long!"); // Buffer overflow + strcpy(buffer, "This string is way too long!"); // Переполнение буфера int arr[5] = {1, 2, 3, 4, 5}; - int *ptr = arr; // Loses size information - ptr[10] = 42; // No bounds check — undefined behavior + int *ptr = arr; // Теряется информация о размере + ptr[10] = 42; // Нет проверки границ — неопределённое поведение } ``` -In C++, `std::vector::operator[]` still performs no bounds checking. Only `.at()` does — and who catches the exception? +В C++ `std::vector::operator[]` по-прежнему не проверяет границы. Проверяет только `.at()` — но кто поймает исключение? -### Dangling pointers and use-after-free +### Висячие указатели и использование после освобождения ```c int *bar() { int i = 42; - return &i; // Returns address of stack variable — dangling! + return &i; // Возвращается адрес переменной на стеке — висячий указатель! } void use_after_free() { char *p = (char *)malloc(20); free(p); - *p = '\0'; // Use after free — undefined behavior + *p = '\0'; // Использование после освобождения — неопределённое поведение } ``` -### Uninitialized variables and undefined behavior +### Неинициализированные переменные и неопределённое поведение -C and C++ both allow uninitialized variables. The resulting values are indeterminate, and reading them is undefined behavior: +И C, и C++ допускают неинициализированные переменные. Их значения не определены, и чтение таких значений — неопределённое поведение: ```c -int x; // Uninitialized -if (x > 0) { ... } // UB — x could be anything +int x; // Не инициализирована +if (x > 0) { ... } // Неопределённое поведение — x может быть чем угодно ``` -Integer overflow is **defined** in C for unsigned types but **undefined** for signed types. In C++, signed overflow is also undefined behavior. Both compilers can and do exploit this for "optimizations" that break programs in surprising ways. +Переполнение целых в C **определено** для беззнаковых типов, но **не определено** для знаковых. В C++ переполнение знакового типа тоже является неопределённым поведением. Оба компилятора могут и действительно используют это для «оптимизаций», которые ломают программы самым неожиданным образом. -### NULL pointer dereferences +### Разыменование NULL-указателей ```c int *ptr = NULL; -*ptr = 42; // SEGV — but the compiler won't stop you +*ptr = 42; // SEGV — но компилятор вас не остановит ``` -In C++, `std::optional<T>` helps but is verbose and often bypassed with `.value()` which throws. +В C++ `std::optional<T>` помогает, но он громоздкий, и его часто обходят вызовом `.value()`, который выбрасывает исключение. -### The visualization: shared problems +### Визуализация: общие проблемы ```mermaid graph TD - ROOT["C/C++ Memory Safety Issues"] --> BUF["Buffer Overflows"] - ROOT --> DANGLE["Dangling Pointers"] - ROOT --> UAF["Use-After-Free"] - ROOT --> UNINIT["Uninitialized Variables"] - ROOT --> NULL["NULL Dereferences"] - ROOT --> UB["Undefined Behavior"] - ROOT --> RACE["Data Races"] - - BUF --> BUF1["No bounds on arrays/pointers"] - DANGLE --> DANGLE1["Returning stack addresses"] - UAF --> UAF1["Reusing freed memory"] - UNINIT --> UNINIT1["Indeterminate values"] - NULL --> NULL1["No forced null checks"] - UB --> UB1["Signed overflow, aliasing"] - RACE --> RACE1["No compile-time safety"] + ROOT["Проблемы безопасности памяти в C/C++"] --> BUF["Переполнения буфера"] + ROOT --> DANGLE["Висячие указатели"] + ROOT --> UAF["Использование после освобождения"] + ROOT --> UNINIT["Неинициализированные переменные"] + ROOT --> NULL["Разыменование NULL"] + ROOT --> UB["Неопределённое поведение"] + ROOT --> RACE["Гонки данных"] + + BUF --> BUF1["Нет границ у массивов/указателей"] + DANGLE --> DANGLE1["Возврат адресов со стека"] + UAF --> UAF1["Повторное использование освобождённой памяти"] + UNINIT --> UNINIT1["Неопределённые значения"] + NULL --> NULL1["Нет принудительной проверки на null"] + UB --> UB1["Переполнение знаковых, алиасинг"] + RACE --> RACE1["Нет безопасности на этапе компиляции"] style ROOT fill:#ff6b6b,color:#000 style BUF fill:#ffa07a,color:#000 @@ -127,151 +127,151 @@ graph TD --- -## C++ Adds More Problems on Top +## C++ добавляет новые проблемы -> **C audience**: You can [skip ahead to How Rust Addresses These Issues](#how-rust-addresses-all-of-this) if you don't use C++. +> **Читатели, знакомые только с C**: можете [сразу перейти к разделу Как Rust решает эти проблемы](#как-rust-решает-всё-это), если не используете C++. > -> **Want to skip straight to code?** Jump to [Show me some code](ch02-getting-started.md#enough-talk-already-show-me-some-code) +> **Хотите сразу перейти к коду?** Перейдите к разделу [Покажите мне код](ch02-getting-started.md#хватит-разговоров-покажите-код) -C++ introduced smart pointers, RAII, move semantics, and exceptions to address C's problems. These are **bandaids, not cures** — they shift the failure mode from "crash at runtime" to "subtler bug at runtime": +C++ ввёл умные указатели, RAII, семантику перемещения и исключения, чтобы решить проблемы C. Это **пластыри, а не лекарства** — они переносят сбой из режима «падение во время выполнения» в режим «более тонкая ошибка во время выполнения»: -### `unique_ptr` and `shared_ptr` — bandaids, not solutions +### `unique_ptr` и `shared_ptr` — пластыри, а не решения -C++ smart pointers are a significant improvement over raw `malloc`/`free`, but they don't solve the underlying problems: +Умные указатели C++ — заметное улучшение по сравнению с сырыми `malloc`/`free`, но они не решают основных проблем: -| C++ Mitigation | What It Fixes | What It **Doesn't** Fix | -|----------------|---------------|------------------------| -| `std::unique_ptr` | Prevents leaks via RAII | **Use-after-move** still compiles; leaves a zombie nullptr | -| `std::shared_ptr` | Shared ownership | **Reference cycles** leak silently; `weak_ptr` discipline is manual | -| `std::optional` | Replaces some null use | `.value()` **throws** if empty — hidden control flow | -| `std::string_view` | Avoids copies | **Dangling** if the source string is freed — no lifetime checking | -| Move semantics | Efficient transfers | Moved-from objects are in a **"valid but unspecified state"** — UB waiting to happen | -| RAII | Automatic cleanup | Requires the **Rule of Five** to get right; one mistake breaks everything | +| Средство C++ | Что исправляет | Что **не** исправляет | +|--------------|----------------|------------------------| +| `std::unique_ptr` | Предотвращает утечки через RAII | **Использование после перемещения** по-прежнему компилируется; остаётся «зомби»-nullptr | +| `std::shared_ptr` | Совместное владение | **Циклы ссылок** незаметно приводят к утечкам; дисциплина `weak_ptr` — ручная | +| `std::optional` | Заменяет часть использований null | `.value()` **выбрасывает исключение**, если значения нет — скрытый поток управления | +| `std::string_view` | Избегает копирования | **Висячий**, если исходная строка освобождена — нет проверки времён жизни | +| Семантика перемещения | Эффективная передача | Объекты после перемещения находятся в **«допустимом, но не определённом состоянии»** — неопределённое поведение, которое только ждёт своего часа | +| RAII | Автоматическая очистка | Чтобы всё сделать правильно, нужно **правило пяти**; одна ошибка ломает всё | ```cpp -// unique_ptr: use-after-move compiles cleanly +// unique_ptr: использование после перемещения компилируется без проблем std::unique_ptr<int> ptr = std::make_unique<int>(42); std::unique_ptr<int> ptr2 = std::move(ptr); -std::cout << *ptr; // Compiles! Undefined behavior at runtime. - // In Rust, this is a compile error: "value used after move" +std::cout << *ptr; // Компилируется! Неопределённое поведение во время выполнения. + // В Rust это ошибка компиляции: "value used after move" ``` ```cpp -// shared_ptr: reference cycles leak silently +// shared_ptr: циклы ссылок незаметно приводят к утечкам struct Node { std::shared_ptr<Node> next; - std::shared_ptr<Node> parent; // Cycle! Destructor never called. + std::shared_ptr<Node> parent; // Цикл! Деструктор никогда не вызывается. }; auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; -b->parent = a; // Memory leak — ref count never reaches 0 - // In Rust, Rc<T> + Weak<T> makes cycles explicit and breakable +b->parent = a; // Утечка памяти — счётчик ссылок никогда не станет 0 + // В Rust Rc<T> + Weak<T> делают циклы явными и разрываемыми ``` -### Use-after-move — the silent killer +### Использование после перемещения — тихий убийца -C++ `std::move` is not a move — it's a cast. The original object remains in a "valid but unspecified state". The compiler lets you keep using it: +`std::move` в C++ — это не перемещение, а приведение типа. Исходный объект остаётся в «допустимом, но не определённом состоянии». Компилятор позволяет продолжать его использовать: ```cpp auto vec = std::make_unique<std::vector<int>>({1, 2, 3}); auto vec2 = std::move(vec); -vec->size(); // Compiles! But dereferencing nullptr — crash at runtime +vec->size(); // Компилируется! Но разыменование nullptr — падение во время выполнения ``` -In Rust, moves are **destructive**. The original binding is gone: +В Rust перемещения **разрушающие**. Исходной привязки больше нет: ```rust let vec = vec![1, 2, 3]; -let vec2 = vec; // Move — vec is consumed -// vec.len(); // Compile error: value used after move +let vec2 = vec; // Перемещение — vec поглощён +// vec.len(); // Ошибка компиляции: значение использовано после перемещения ``` -### Iterator invalidation — real bugs from production C++ +### Инвалидация итераторов — настоящие ошибки из боевого C++ -These aren't contrived examples — they represent **real bug patterns** found in large C++ codebases: +Это не надуманные примеры — они представляют собой **реальные шаблоны ошибок**, встречающиеся в больших кодовых базах на C++: ```cpp -// BUG 1: erase without reassigning iterator (undefined behavior) +// ОШИБКА 1: удаление без переприсваивания итератора (неопределённое поведение) while (it != pending_faults.end()) { if (*it != nullptr && (*it)->GetId() == fault->GetId()) { - pending_faults.erase(it); // ← iterator invalidated! - removed_count++; // next loop uses dangling iterator + pending_faults.erase(it); // ← итератор инвалидирован! + removed_count++; // следующая итерация использует висячий итератор } else { ++it; } } -// Fix: it = pending_faults.erase(it); +// Исправление: it = pending_faults.erase(it); ``` ```cpp -// BUG 2: index-based erase skips elements +// ОШИБКА 2: удаление по индексу пропускает элементы for (auto i = 0; i < entries.size(); i++) { if (config_status == ConfigDisable::Status::Disabled) { - entries.erase(entries.begin() + i); // ← shifts elements - } // i++ skips the shifted one + entries.erase(entries.begin() + i); // ← сдвигает элементы + } // i++ пропускает сдвинутый элемент } ``` ```cpp -// BUG 3: one erase path correct, the other isn't +// ОШИБКА 3: один путь удаления правильный, другой — нет while (it != incomplete_ids.end()) { if (current_action == nullptr) { - incomplete_ids.erase(it); // ← BUG: iterator not reassigned + incomplete_ids.erase(it); // ← ОШИБКА: итератор не переприсвоен continue; } - it = incomplete_ids.erase(it); // ← Correct path + it = incomplete_ids.erase(it); // ← Правильный путь } ``` -**These compile without any warning.** In Rust, the borrow checker makes all three a compile error — you cannot mutate a collection while iterating over it, period. +**Все эти примеры компилируются без единого предупреждения.** В Rust проверка заимствований делает все три ошибки ошибками компиляции — вы не можете изменять коллекцию во время итерации по ней, точка. -### Exception safety and the `dynamic_cast`/`new` pattern +### Безопасность исключений и шаблон `dynamic_cast`/`new` -Modern C++ codebases still lean heavily on patterns that have no compile-time safety: +Современные кодовые базы на C++ по-прежнему опираются на шаблоны, у которых нет безопасности на этапе компиляции: ```cpp -// Typical C++ factory pattern — every branch is a potential bug +// Типичный фабричный шаблон C++ — каждая ветка является потенциальной ошибкой DriverBase* driver = nullptr; if (dynamic_cast<ModelA*>(device)) { driver = new DriverForModelA(framework); } else if (dynamic_cast<ModelB*>(device)) { driver = new DriverForModelB(framework); } -// What if driver is still nullptr? What if new throws? Who owns driver? +// Что, если driver всё ещё nullptr? Что, если new выбросит исключение? Кто владеет driver? ``` -In a typical 100K-line C++ codebase you might find hundreds of `dynamic_cast` calls (each a potential runtime failure), hundreds of raw `new` calls (each a potential leak), and hundreds of `virtual`/`override` methods (vtable overhead everywhere). +В типичной кодовой базе на C++ на 100 тысяч строк можно найти сотни вызовов `dynamic_cast` (каждый — потенциальный сбой во время выполнения), сотни сырых вызовов `new` (каждый — потенциальная утечка) и сотни методов `virtual`/`override` (накладные расходы на таблицы виртуальных функций повсюду). -### Dangling references and lambda captures +### Висячие ссылки и захваты в лямбдах ```cpp int& get_reference() { int x = 42; - return x; // Dangling reference — compiles, UB at runtime + return x; // Висячая ссылка — компилируется, неопределённое поведение во время выполнения } auto make_closure() { int local = 42; - return [&local]() { return local; }; // Dangling capture! + return [&local]() { return local; }; // Висячий захват! } ``` -### The visualization: C++ additional problems +### Визуализация: дополнительные проблемы C++ ```mermaid graph TD - ROOT["C++ Additional Problems<br/>(on top of C issues)"] --> UAM["Use-After-Move"] - ROOT --> CYCLE["Reference Cycles"] - ROOT --> ITER["Iterator Invalidation"] - ROOT --> EXC["Exception Safety"] - ROOT --> TMPL["Template Error Messages"] - - UAM --> UAM1["std::move leaves zombie<br/>Compiles without warning"] - CYCLE --> CYCLE1["shared_ptr cycles leak<br/>Destructor never called"] - ITER --> ITER1["erase() invalidates iterators<br/>Real production bugs"] - EXC --> EXC1["Partial construction<br/>new without try/catch"] - TMPL --> TMPL1["30+ lines of nested<br/>template instantiation errors"] + ROOT["Дополнительные проблемы C++<br/>(помимо проблем C)"] --> UAM["Использование после перемещения"] + ROOT --> CYCLE["Циклы ссылок"] + ROOT --> ITER["Инвалидация итераторов"] + ROOT --> EXC["Безопасность исключений"] + ROOT --> TMPL["Сообщения об ошибках шаблонов"] + + UAM --> UAM1["std::move оставляет зомби<br/>Компилируется без предупреждений"] + CYCLE --> CYCLE1["Циклы shared_ptr приводят к утечкам<br/>Деструктор никогда не вызывается"] + ITER --> ITER1["erase() инвалидирует итераторы<br/>Реальные ошибки в продакшене"] + EXC --> EXC1["Частичное конструирование<br/>new без try/catch"] + TMPL --> TMPL1["30+ строк вложенных ошибок<br/>инстанцирования шаблонов"] style ROOT fill:#ff6b6b,color:#000 style UAM fill:#ffa07a,color:#000 @@ -283,65 +283,65 @@ graph TD --- -## How Rust Addresses All of This - -Every problem listed above — from both C and C++ — is prevented by Rust's compile-time guarantees: - -| Problem | Rust's Solution | -|---------|-----------------| -| Buffer overflows | Slices carry length; indexing is bounds-checked | -| Dangling pointers / use-after-free | Lifetime system proves references are valid at compile time | -| Use-after-move | Moves are destructive — compiler refuses to let you touch the original | -| Memory leaks | `Drop` trait = RAII without the Rule of Five; automatic, correct cleanup | -| Reference cycles | Ownership is tree-shaped; `Rc` + `Weak` makes cycles explicit | -| Iterator invalidation | Borrow checker forbids mutating a collection while borrowing it | -| NULL pointers | No null. `Option<T>` forces explicit handling via pattern matching | -| Data races | `Send`/`Sync` traits make data races a compile error | -| Uninitialized variables | All variables must be initialized; compiler enforces it | -| Integer UB | Debug panics on overflow; release wraps (both defined behavior) | -| Exceptions | No exceptions; `Result<T, E>` is visible in type signatures, propagated with `?` | -| Inheritance complexity | Traits + composition; no Diamond Problem, no vtable fragility | -| Forgotten mutex unlocks | `Mutex<T>` wraps the data; lock guard is the only access path | +## Как Rust решает всё это + +Каждая проблема, перечисленная выше — и из C, и из C++, — предотвращается гарантиями Rust на этапе компиляции: + +| Проблема | Решение в Rust | +|----------|----------------| +| Переполнения буфера | Срезы хранят длину; индексация проверяется на границы | +| Висячие указатели / использование после освобождения | Система времён жизни доказывает, что ссылки валидны, на этапе компиляции | +| Использование после перемещения | Перемещения разрушающие — компилятор не позволит трогать исходное значение | +| Утечки памяти | Трейт `Drop` = RAII без правила пяти; автоматическая и корректная очистка | +| Циклы ссылок | Владение имеет древовидную структуру; `Rc` + `Weak` делают циклы явными | +| Инвалидация итераторов | Проверка заимствований запрещает изменять коллекцию, пока она заимствована | +| NULL-указатели | Нет null. `Option<T>` вынуждает явно обрабатывать значение через сопоставление с образцом | +| Гонки данных | Трейты `Send`/`Sync` делают гонки данных ошибкой компиляции | +| Неинициализированные переменные | Все переменные должны быть инициализированы; это проверяет компилятор | +| Неопределённое поведение при переполнении | В отладке panic; в релизе переполнение (оба варианта определены) | +| Исключения | Исключений нет; `Result<T, E>` виден в сигнатурах типов и распространяется с помощью `?` | +| Сложность наследования | Трейты + композиция; нет проблемы ромба, нет хрупкости таблиц виртуальных функций | +| Забытые разблокировки мьютексов | `Mutex<T>` оборачивает данные; защитник блокировки — единственный путь доступа | ```rust fn rust_prevents_everything() { - // ✅ No buffer overflow — bounds checked + // ✅ Нет переполнения буфера — границы проверяются let arr = [1, 2, 3, 4, 5]; - // arr[10]; // panic at runtime, never UB + // arr[10]; // panic во время выполнения, никогда не UB - // ✅ No use-after-move — compile error + // ✅ Нет использования после перемещения — ошибка компиляции let data = vec![1, 2, 3]; let moved = data; // data.len(); // error: value used after move - // ✅ No dangling pointer — lifetime error + // ✅ Нет висячих указателей — ошибка времени жизни // let r; // { let x = 5; r = &x; } // error: x does not live long enough - // ✅ No null — Option forces handling + // ✅ Нет null — Option вынуждает обработку let maybe: Option<i32> = None; - // maybe.unwrap(); // panic, but you'd use match or if let instead + // maybe.unwrap(); // panic, но вместо этого вы бы использовали match или if let - // ✅ No data race — compile error + // ✅ Нет гонки данных — ошибка компиляции // let mut shared = vec![1, 2, 3]; // std::thread::spawn(|| shared.push(4)); // error: closure may outlive // shared.push(5); // borrowed value } ``` -### Rust's safety model — the full picture +### Модель безопасности Rust — полная картина ```mermaid graph TD - RUST["Rust Safety Guarantees"] --> OWN["Ownership System"] - RUST --> BORROW["Borrow Checker"] - RUST --> TYPES["Type System"] - RUST --> TRAITS["Send/Sync Traits"] + RUST["Гарантии безопасности Rust"] --> OWN["Система владения"] + RUST --> BORROW["Проверка заимствований"] + RUST --> TYPES["Система типов"] + RUST --> TRAITS["Трейты Send/Sync"] - OWN --> OWN1["No use-after-free<br/>No use-after-move<br/>No double-free"] - BORROW --> BORROW1["No dangling references<br/>No iterator invalidation<br/>No data races through refs"] - TYPES --> TYPES1["No NULL (Option<T>)<br/>No exceptions (Result<T,E>)<br/>No uninitialized values"] - TRAITS --> TRAITS1["No data races<br/>Send = safe to transfer<br/>Sync = safe to share"] + OWN --> OWN1["Нет использования после освобождения<br/>Нет использования после перемещения<br/>Нет двойного освобождения"] + BORROW --> BORROW1["Нет висячих ссылок<br/>Нет инвалидации итераторов<br/>Нет гонок данных через ссылки"] + TYPES --> TYPES1["Нет NULL (Option<T>)<br/>Нет исключений (Result<T,E>)<br/>Нет неинициализированных значений"] + TRAITS --> TRAITS1["Нет гонок данных<br/>Send = безопасно передавать<br/>Sync = безопасно разделять"] style RUST fill:#51cf66,color:#000 style OWN fill:#91e5a3,color:#000 @@ -350,20 +350,20 @@ graph TD style TRAITS fill:#91e5a3,color:#000 ``` -## Quick Reference: C vs C++ vs Rust - -| **Concept** | **C** | **C++** | **Rust** | **Key Difference** | -|-------------|-------|---------|----------|-------------------| -| Memory management | `malloc()/free()` | `unique_ptr`, `shared_ptr` | `Box<T>`, `Rc<T>`, `Arc<T>` | Automatic, no cycles, no zombies | -| Arrays | `int arr[10]` | `std::vector<T>`, `std::array<T>` | `Vec<T>`, `[T; N]` | Bounds checking by default | -| Strings | `char*` with `\0` | `std::string`, `string_view` | `String`, `&str` | UTF-8 guaranteed, lifetime-checked | -| References | `int*` (raw) | `T&`, `T&&` (move) | `&T`, `&mut T` | Lifetime + borrow checking | -| Polymorphism | Function pointers | Virtual functions, inheritance | Traits, trait objects | Composition over inheritance | -| Generics | Macros / `void*` | Templates | Generics + trait bounds | Clear error messages | -| Error handling | Return codes, `errno` | Exceptions, `std::optional` | `Result<T, E>`, `Option<T>` | No hidden control flow | -| NULL safety | `ptr == NULL` | `nullptr`, `std::optional<T>` | `Option<T>` | Forced null checking | -| Thread safety | Manual (pthreads) | Manual (`std::mutex`, etc.) | Compile-time `Send`/`Sync` | Data races impossible | -| Build system | Make, CMake | CMake, Make, etc. | Cargo | Integrated toolchain | -| Undefined behavior | Rampant | Subtle (signed overflow, aliasing) | Zero in safe code | Safety guaranteed | +## Краткий справочник: C vs C++ vs Rust + +| **Концепция** | **C** | **C++** | **Rust** | **Ключевое отличие** | +|---------------|-------|---------|----------|---------------------| +| Управление памятью | `malloc()/free()` | `unique_ptr`, `shared_ptr` | `Box<T>`, `Rc<T>`, `Arc<T>` | Автоматическое, без циклов и зомби | +| Массивы | `int arr[10]` | `std::vector<T>`, `std::array<T>` | `Vec<T>`, `[T; N]` | Проверка границ по умолчанию | +| Строки | `char*` с `\0` | `std::string`, `string_view` | `String`, `&str` | Гарантированный UTF-8, проверка времён жизни | +| Ссылки | `int*` (сырые) | `T&`, `T&&` (перемещение) | `&T`, `&mut T` | Проверка времён жизни и заимствований | +| Полиморфизм | Указатели на функции | Виртуальные функции, наследование | Трейты, трейт-объекты | Композиция вместо наследования | +| Обобщения | Макросы / `void*` | Шаблоны | Обобщения + ограничения трейтами | Понятные сообщения об ошибках | +| Обработка ошибок | Коды возврата, `errno` | Исключения, `std::optional` | `Result<T, E>`, `Option<T>` | Никакого скрытого потока управления | +| Безопасность при null | `ptr == NULL` | `nullptr`, `std::optional<T>` | `Option<T>` | Принудительная проверка на null | +| Потокобезопасность | Вручную (pthreads) | Вручную (`std::mutex` и др.) | `Send`/`Sync` на этапе компиляции | Гонки данных невозможны | +| Система сборки | Make, CMake | CMake, Make и др. | Cargo | Интегрированный инструментарий | +| Неопределённое поведение | Повсеместно | Тонкое (переполнение знаковых, алиасинг) | Нет в безопасном коде | Безопасность гарантирована | *** diff --git a/c-cpp-book/src/ch01-introduction-and-motivation.md b/c-cpp-book/src/ch01-introduction-and-motivation.md index 615d297..884d0a8 100644 --- a/c-cpp-book/src/ch01-introduction-and-motivation.md +++ b/c-cpp-book/src/ch01-introduction-and-motivation.md @@ -1,87 +1,87 @@ -# Speaker intro and general approach +# Представление преподавателя и общий подход -> **What you'll learn:** Course structure, the interactive format, and how familiar C/C++ concepts map to Rust equivalents. This chapter sets expectations and gives you a roadmap for the rest of the book. +> **Что вы узнаете:** структуру курса, интерактивный формат и то, как знакомые концепции C/C++ соответствуют своим аналогам в Rust. Эта глава задаёт ожидания и даёт дорожную карту для остальной книги. -- Speaker intro - - Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team - - Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems - - Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since -- This course is intended to be as interactive as possible - - Assumption: You know C, C++, or both - - Examples are deliberately designed to map familiar concepts to Rust equivalents - - **Please feel free to ask clarifying questions at any point of time** -- Speaker is looking forward to continued engagement with teams +- О преподавателе + - Principal Firmware Architect (ведущий архитектор прошивок) в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) в Microsoft + - Ветеран отрасли с опытом в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре процессоров и платформ, а также в системах на C++ + - Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- Курс задуман как можно более интерактивный + - Предположение: вы знаете C, C++ или оба языка + - Примеры намеренно построены так, чтобы знакомые концепции сопоставлялись с аналогами в Rust + - **Задавайте уточняющие вопросы в любой момент** +- Преподаватель рассчитывает на продолжение общения с командами -# The case for Rust -> **Want to skip straight to code?** Jump to [Show me some code](ch02-getting-started.md#enough-talk-already-show-me-some-code) +# Аргументы в пользу Rust +> **Хотите сразу перейти к коду?** Перейдите к разделу [Покажите мне код](ch02-getting-started.md#хватит-разговоров-покажите-код) -Whether you're coming from C or C++, the core pain points are the same: memory safety bugs that compile cleanly but crash, corrupt, or leak at runtime. +Независимо от того, приходите ли вы из C или C++, основные болевые точки одни и те же: ошибки безопасности памяти, которые компилируются без проблем, но приводят к падению, порче данных или утечкам во время выполнения. -- Over **70% of CVEs** are caused by memory safety issues — buffer overflows, dangling pointers, use-after-free -- C++ `shared_ptr`, `unique_ptr`, RAII, and move semantics are steps in the right direction, but they are **bandaids, not cures** — they leave use-after-move, reference cycles, iterator invalidation, and exception safety gaps wide open -- Rust provides the performance you rely on from C/C++, but with **compile-time guarantees** for safety +- Более **70% CVE** вызваны проблемами безопасности памяти — переполнениями буфера, висячими указателями, использованием после освобождения +- Умные указатели C++ (`shared_ptr`, `unique_ptr`), RAII и семантика перемещения — шаги в правильном направлении, но это **пластыри, а не лекарство**: они оставляют открытыми использование после перемещения, циклы ссылок, инвалидацию итераторов и пробелы в безопасности исключений +- Rust даёт ту же производительность, на которую вы опираетесь в C/C++, но с **гарантиями безопасности на этапе компиляции** -> **📖 Deep dive:** See [Why C/C++ Developers Need Rust](ch01-1-why-c-cpp-developers-need-rust.md) for concrete vulnerability examples, the complete list of what Rust eliminates, and why C++ smart pointers aren't enough +> **📖 Подробнее:** см. [Почему разработчикам C/C++ нужен Rust](ch01-1-why-c-cpp-developers-need-rust.md) — там конкретные примеры уязвимостей, полный список того, что устраняет Rust, и объяснение, почему умных указателей C++ недостаточно ---- -# How does Rust address these issues? +# Как Rust решает эти проблемы? -## Buffer overflows and bounds violations -- All Rust arrays, slices, and strings have explicit bounds associated with them. The compiler inserts checks to ensure that any bounds violation results in a **runtime crash** (panic in Rust terms) — never undefined behavior +## Переполнения буфера и выход за границы +- У всех массивов, срезов и строк Rust явно заданы границы. Компилятор вставляет проверки, благодаря которым любой выход за границы приводит к **аварийному завершению во время выполнения** (panic в терминологии Rust), а не к неопределённому поведению -## Dangling pointers and references -- Rust introduces lifetimes and borrow checking to eliminate dangling references at **compile time** -- No dangling pointers, no use-after-free — the compiler simply won't let you +## Висячие указатели и ссылки +- Rust вводит времена жизни и проверку заимствований, чтобы устранять висячие ссылки на **этапе компиляции** +- Никаких висячих указателей, никакого использования после освобождения — компилятор просто не позволит это сделать -## Use-after-move -- Rust's ownership system makes moves **destructive** — once you move a value, the compiler **refuses** to let you use the original. No zombie objects, no "valid but unspecified state" +## Использование после перемещения +- Система владения в Rust делает перемещение **разрушающим**: после перемещения значения компилятор **не позволит** использовать исходную переменную. Никаких «зомби-объектов» и состояния «допустимо, но не определено» -## Resource management -- Rust's `Drop` trait is RAII done right — the compiler automatically frees resources when they go out of scope, and **prevents use-after-move** which C++ RAII cannot -- No Rule of Five needed (no copy ctor, move ctor, copy assign, move assign, destructor to define) +## Управление ресурсами +- Трейт `Drop` в Rust — это RAII, реализованный правильно: компилятор автоматически освобождает ресурсы, когда они выходят из области видимости, и **предотвращает использование после перемещения**, чего RAII в C++ не может +- Правило пяти не нужно (не требуется определять конструктор копирования, конструктор перемещения, оператор копирующего присваивания, оператор перемещающего присваивания и деструктор) -## Error handling -- Rust has no exceptions. All errors are values (`Result<T, E>`), making error handling explicit and visible in the type signature +## Обработка ошибок +- В Rust нет исключений. Все ошибки — это значения (`Result<T, E>`), поэтому обработка ошибок явная и видна в сигнатуре типа -## Iterator invalidation -- Rust's borrow checker **forbids modifying a collection while iterating over it**. You simply cannot write the bugs that plague C++ codebases: +## Инвалидация итераторов +- Проверка заимствований в Rust **запрещает изменять коллекцию во время итерации по ней**. Вы просто не можете написать те ошибки, которыми полны кодовые базы на C++: ```rust -// Rust equivalent of erase-during-iteration: retain() +// Аналог удаления во время итерации в Rust: retain() pending_faults.retain(|f| f.id != fault_to_remove.id); -// Or: collect into a new Vec (functional style) +// Или: собрать в новый Vec (функциональный стиль) let remaining: Vec<_> = pending_faults .into_iter() .filter(|f| f.id != fault_to_remove.id) .collect(); ``` -## Data races -- The type system prevents data races at **compile time** through the `Send` and `Sync` traits +## Гонки данных +- Система типов предотвращает гонки данных на **этапе компиляции** благодаря трейтам `Send` и `Sync` -## Memory Safety Visualization +## Визуализация безопасности памяти -### Rust Ownership — Safe by Design +### Владение в Rust — безопасность по замыслу ```rust fn safe_rust_ownership() { - // Move is destructive: original is gone + // Перемещение разрушающее: исходное значение больше недоступно let data = vec![1, 2, 3]; - let data2 = data; // Move happens - // data.len(); // Compile error: value used after move + let data2 = data; // Происходит перемещение + // data.len(); // Ошибка компиляции: значение использовано после перемещения - // Borrowing: safe shared access + // Заимствование: безопасный совместный доступ let owned = String::from("Hello, World!"); - let slice: &str = &owned; // Borrow — no allocation - println!("{}", slice); // Always safe + let slice: &str = &owned; // Заимствование — без выделения памяти + println!("{}", slice); // Всегда безопасно - // No dangling references possible + // Висячие ссылки невозможны /* let dangling_ref; { let temp = String::from("temporary"); - dangling_ref = &temp; // Compile error: temp doesn't live long enough + dangling_ref = &temp; // Ошибка компиляции: temp не живёт достаточно долго } */ } @@ -89,22 +89,22 @@ fn safe_rust_ownership() { ```mermaid graph TD - A[Rust Ownership Safety] --> B[Destructive Moves] - A --> C[Automatic Memory Management] - A --> D[Compile-time Lifetime Checking] - A --> E[No Exceptions — Result Types] + A[Безопасность владения Rust] --> B[Разрушающие перемещения] + A --> C[Автоматическое управление памятью] + A --> D[Проверка времён жизни на этапе компиляции] + A --> E[Никаких исключений — типы Result] - B --> B1["Use-after-move is compile error"] - B --> B2["No zombie objects"] + B --> B1["Использование после перемещения — ошибка компиляции"] + B --> B2["Никаких зомби-объектов"] - C --> C1["Drop trait = RAII done right"] - C --> C2["No Rule of Five needed"] + C --> C1["Трейт Drop = RAII, сделанный правильно"] + C --> C2["Правило пяти не нужно"] - D --> D1["Borrow checker prevents dangling"] - D --> D2["References always valid"] + D --> D1["Проверка заимствований исключает висячие ссылки"] + D --> D2["Ссылки всегда валидны"] - E --> E1["Result<T,E> — errors in types"] - E --> E2["? operator for propagation"] + E --> E1["Result<T,E> — ошибки в типах"] + E --> E2["Оператор ? для распространения"] style A fill:#51cf66,color:#000 style B fill:#91e5a3,color:#000 @@ -113,64 +113,64 @@ graph TD style E fill:#91e5a3,color:#000 ``` -## Memory Layout: Rust References +## Структура памяти: ссылки в Rust ```mermaid graph TD - RM1[Stack] --> RP1["&i32 ref"] - RM2[Stack/Heap] --> RV1["i32 value = 42"] - RP1 -.->|"Safe reference — Lifetime checked"| RV1 - RM3[Borrow Checker] --> RC1["Prevents dangling refs at compile time"] + RM1[Стек] --> RP1["Ссылка &i32"] + RM2[Стек/Куча] --> RV1["Значение i32 = 42"] + RP1 -.->|"Безопасная ссылка — время жизни проверяется"| RV1 + RM3[Проверка заимствований] --> RC1["Исключает висячие ссылки на этапе компиляции"] style RC1 fill:#51cf66,color:#000 style RP1 fill:#91e5a3,color:#000 ``` -### `Box<T>` Heap Allocation Visualization +### Визуализация выделения памяти в `Box<T>` ```rust fn box_allocation_example() { - // Stack allocation + // Выделение на стеке let stack_value = 42; - // Heap allocation with Box + // Выделение в куче через Box let heap_value = Box::new(42); - // Moving ownership + // Передача владения let moved_box = heap_value; - // heap_value is no longer accessible + // heap_value больше недоступен } ``` ```mermaid graph TD - subgraph "Stack Frame" + subgraph "Кадр стека" SV["stack_value: 42"] BP["heap_value: Box<i32>"] BP2["moved_box: Box<i32>"] end - subgraph "Heap" + subgraph "Куча" HV["42"] end - BP -->|"Owns"| HV - BP -.->|"Move ownership"| BP2 - BP2 -->|"Now owns"| HV + BP -->|"Владеет"| HV + BP -.->|"Передача владения"| BP2 + BP2 -->|"Теперь владеет"| HV - subgraph "After Move" - BP_X["heap_value: [WARNING] MOVED"] + subgraph "После перемещения" + BP_X["heap_value: [ВНИМАНИЕ] ПЕРЕМЕЩЕНО"] BP2_A["moved_box: Box<i32>"] end - BP2_A -->|"Owns"| HV + BP2_A -->|"Владеет"| HV style BP_X fill:#ff6b6b,color:#000 style HV fill:#91e5a3,color:#000 style BP2_A fill:#51cf66,color:#000 ``` -## Slice Operations Visualization +## Визуализация операций со срезами ```rust fn slice_operations() { @@ -186,7 +186,7 @@ fn slice_operations() { ```mermaid graph TD V["Vec: [1, 2, 3, 4, 5, 6, 7, 8]"] - V --> FS["&data[..] → all elements"] + V --> FS["&data[..] → все элементы"] V --> PS["&data[2..6] → [3, 4, 5, 6]"] V --> SS["&data[..4] → [1, 2, 3, 4]"] V --> ES["&data[3..] → [4, 5, 6, 7, 8]"] @@ -198,34 +198,34 @@ graph TD style ES fill:#91e5a3,color:#000 ``` -# Other Rust USPs and features -- No data races between threads (compile-time `Send`/`Sync` checking) -- No use-after-move (unlike C++ `std::move` which leaves zombie objects) -- No uninitialized variables - - All variables must be initialized before use -- No trivial memory leaks - - `Drop` trait = RAII done right, no Rule of Five needed - - Compiler automatically releases memory when it goes out of scope -- No forgotten locks on mutexes - - Lock guards are the *only* way to access the data (`Mutex<T>` wraps the data, not the access) -- No exception handling complexity - - Errors are values (`Result<T, E>`), visible in function signatures, propagated with `?` -- Excellent support for type inference, enums, pattern matching, zero cost abstractions -- Built-in support for dependency management, building, testing, formatting, linting - - `cargo` replaces make/CMake + lint + test frameworks - -# Quick Reference: Rust vs C/C++ - -| **Concept** | **C** | **C++** | **Rust** | **Key Difference** | -|-------------|-------|---------|----------|-------------------| -| Memory management | `malloc()/free()` | `unique_ptr`, `shared_ptr` | `Box<T>`, `Rc<T>`, `Arc<T>` | Automatic, no cycles | -| Arrays | `int arr[10]` | `std::vector<T>`, `std::array<T>` | `Vec<T>`, `[T; N]` | Bounds checking by default | -| Strings | `char*` with `\0` | `std::string`, `string_view` | `String`, `&str` | UTF-8 guaranteed, lifetime-checked | -| References | `int* ptr` | `T&`, `T&&` (move) | `&T`, `&mut T` | Borrow checking, lifetimes | -| Polymorphism | Function pointers | Virtual functions, inheritance | Traits, trait objects | Composition over inheritance | -| Generic programming | Macros (`void*`) | Templates | Generics + trait bounds | Better error messages | -| Error handling | Return codes, `errno` | Exceptions, `std::optional` | `Result<T, E>`, `Option<T>` | No hidden control flow | -| NULL/null safety | `ptr == NULL` | `nullptr`, `std::optional<T>` | `Option<T>` | Forced null checking | -| Thread safety | Manual (pthreads) | Manual synchronization | Compile-time guarantees | Data races impossible | -| Build system | Make, CMake | CMake, Make, etc. | Cargo | Integrated toolchain | -| Undefined behavior | Runtime crashes | Subtle UB (signed overflow, aliasing) | Compile-time errors | Safety guaranteed | +# Другие ключевые преимущества и возможности Rust +- Никаких гонок данных между потоками (проверка `Send`/`Sync` на этапе компиляции) +- Никакого использования после перемещения (в отличие от `std::move` в C++, который оставляет зомби-объекты) +- Никаких неинициализированных переменных + - Все переменные должны быть инициализированы до использования +- Никаких тривиальных утечек памяти + - Трейт `Drop` = RAII, сделанный правильно, правило пяти не нужно + - Компилятор автоматически освобождает память, когда она выходит из области видимости +- Никаких забытых блокировок мьютексов + - Защитники блокировок — *единственный* способ доступа к данным (`Mutex<T>` оборачивает данные, а не доступ к ним) +- Никакой сложности обработки исключений + - Ошибки — это значения (`Result<T, E>`), видны в сигнатурах функций и распространяются с помощью `?` +- Отличная поддержка вывода типов, перечислений, сопоставления с образцом и абстракций с нулевой стоимостью +- Встроенная поддержка управления зависимостями, сборки, тестирования, форматирования и линтинга + - `cargo` заменяет make/CMake и фреймворки для линтинга и тестирования + +# Краткий справочник: Rust и C/C++ + +| **Концепция** | **C** | **C++** | **Rust** | **Ключевое отличие** | +|---------------|-------|---------|----------|---------------------| +| Управление памятью | `malloc()/free()` | `unique_ptr`, `shared_ptr` | `Box<T>`, `Rc<T>`, `Arc<T>` | Автоматическое, без циклов | +| Массивы | `int arr[10]` | `std::vector<T>`, `std::array<T>` | `Vec<T>`, `[T; N]` | Проверка границ по умолчанию | +| Строки | `char*` с `\0` | `std::string`, `string_view` | `String`, `&str` | Гарантированный UTF-8, проверка времён жизни | +| Ссылки | `int* ptr` | `T&`, `T&&` (перемещение) | `&T`, `&mut T` | Проверка заимствований, времена жизни | +| Полиморфизм | Указатели на функции | Виртуальные функции, наследование | Трейты, трейт-объекты | Композиция вместо наследования | +| Обобщённое программирование | Макросы (`void*`) | Шаблоны | Обобщения + ограничения трейтами | Понятные сообщения об ошибках | +| Обработка ошибок | Коды возврата, `errno` | Исключения, `std::optional` | `Result<T, E>`, `Option<T>` | Никакого скрытого потока управления | +| Безопасность при null | `ptr == NULL` | `nullptr`, `std::optional<T>` | `Option<T>` | Принудительная проверка на null | +| Потокобезопасность | Вручную (pthreads) | Вручную (`std::mutex` и др.) | Гарантии на этапе компиляции | Гонки данных невозможны | +| Система сборки | Make, CMake | CMake, Make и др. | Cargo | Встроенный инструментарий | +| Неопределённое поведение | Ошибки во время выполнения | Тонкое (переполнение знаковых, алиасинг) | Ошибки на этапе компиляции | Безопасность гарантирована | diff --git a/c-cpp-book/src/ch02-getting-started.md b/c-cpp-book/src/ch02-getting-started.md index bf21b86..9985535 100644 --- a/c-cpp-book/src/ch02-getting-started.md +++ b/c-cpp-book/src/ch02-getting-started.md @@ -1,62 +1,62 @@ -# Enough talk already: Show me some code +# Хватит разговоров: покажите код -> **What you'll learn:** Your first Rust program — `fn main()`, `println!()`, and how Rust macros differ fundamentally from C/C++ preprocessor macros. By the end you'll be able to write, compile, and run simple Rust programs. +> **Что вы узнаете:** вашу первую программу на Rust — `fn main()`, `println!()` и то, чем макросы Rust принципиально отличаются от препроцессорных макросов C/C++. К концу главы вы сможете написать, скомпилировать и запустить простые программы на Rust. ```rust fn main() { println!("Hello world from Rust"); } ``` -- The above syntax should be similar to anyone familiar with C-style languages - - All functions in Rust begin with the ```fn``` keyword - - The default entry point for executables is ```main()``` - - The ```println!``` looks like a function, but is actually a **macro**. Macros in Rust are very different from C/C++ preprocessor macros — they are hygienic, type-safe, and operate on the syntax tree rather than text substitution -- Two great ways to quickly try out Rust snippets: - - **Online**: [Rust Playground](https://play.rust-lang.org/) — paste code, hit Run, share results. No install needed - - **Local REPL**: Install [`evcxr_repl`](https://github.com/evcxr/evcxr) for an interactive Rust REPL (like Python's REPL, but for Rust): +- Этот синтаксис должен быть понятен всем, кто знаком с языками в стиле C + - Все функции в Rust начинаются с ключевого слова ```fn``` + - Точка входа по умолчанию для исполняемых файлов — ```main()``` + - ```println!``` выглядит как функция, но на самом деле это **макрос**. Макросы в Rust сильно отличаются от препроцессорных макросов C/C++: они гигиеничны, типобезопасны и работают с синтаксическим деревом, а не с текстовой подстановкой +- Два удобных способа быстро опробовать фрагменты кода на Rust: + - **Онлайн**: [Rust Playground](https://play.rust-lang.org/) — вставьте код, нажмите Run и поделитесь результатом. Установка не нужна + - **Локальный REPL**: установите [`evcxr_repl`](https://github.com/evcxr/evcxr) для интерактивного REPL Rust (как REPL Python, только для Rust): ```bash cargo install --locked evcxr_repl -evcxr # Start the REPL, type Rust expressions interactively +evcxr # Запустить REPL и вводить выражения Rust интерактивно ``` -### Rust Local installation -- Rust can be locally installed using the following methods +### Локальная установка Rust +- Rust можно установить локально одним из следующих способов - Windows: https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe - Linux / WSL: ```curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh``` -- The Rust ecosystem is composed of the following components - - ```rustc``` is the standalone compiler, but it's seldom used directly - - The preferred tool, ```cargo``` is the Swiss Army knife and is used for dependency management, building, testing, formatting, linting, etc. - - The Rust toolchain comes in the ```stable```, ```beta``` and ```nightly``` (experimental) channels, but we'll stick with ```stable```. Use the ```rustup update``` command to upgrade the ```stable``` installation that's released every six weeks -- We'll also install the ```rust-analyzer``` plug-in for VSCode +- Экосистема Rust состоит из следующих компонентов + - ```rustc``` — самостоятельный компилятор, но им редко пользуются напрямую + - Основной инструмент — ```cargo```, швейцарский армейский нож для управления зависимостями, сборки, тестирования, форматирования, линтинга и т. д. + - Тулчейн Rust бывает в каналах ```stable```, ```beta``` и ```nightly``` (экспериментальный), но мы будем использовать ```stable```. Команда ```rustup update``` обновляет установку ```stable```, которая выходит каждые шесть недель +- Мы также установим плагин ```rust-analyzer``` для VSCode -# Rust packages (crates) -- Rust binaries are created using packages (hereby called crates) - - A crate may either be standalone, or may have dependency on other crates. The crates for the dependencies can be local or remote. Third-party crates are typically downloaded from a centralized repository called ```crates.io```. - - The ```cargo``` tool automatically handles the downloading of crates and their dependencies. This is conceptually equivalent to linking to C-libraries - - Crate dependencies are expressed in a file called ```Cargo.toml```. It also defines the target type for the crate: standalone executable, static library, dynamic library (uncommon) - - Reference: https://doc.rust-lang.org/cargo/reference/cargo-targets.html +# Пакеты Rust (крейты) +- Бинарные файлы Rust создаются из пакетов (далее — крейты) + - Крейт может быть самостоятельным или зависеть от других крейтов. Крейты-зависимости могут быть локальными или удалёнными. Сторонние крейты обычно скачиваются из централизованного репозитория под названием ```crates.io```. + - Инструмент ```cargo``` автоматически загружает крейты и их зависимости. Концептуально это эквивалентно подключению C-библиотек + - Зависимости крейта описываются в файле ```Cargo.toml```. Там же задаётся тип цели для крейта: самостоятельный исполняемый файл, статическая библиотека, динамическая библиотека (встречается редко) + - Справка: https://doc.rust-lang.org/cargo/reference/cargo-targets.html -## Cargo vs Traditional C Build Systems +## Cargo и традиционные системы сборки C -### Dependency Management Comparison +### Сравнение управления зависимостями ```mermaid graph TD - subgraph "Traditional C Build Process" - CC["C Source Files<br/>(.c, .h)"] - CM["Manual Makefile<br/>or CMake"] - CL["Linker"] - CB["Final Binary"] + subgraph "Традиционный процесс сборки на C" + CC["Исходные файлы C<br/>(.c, .h)"] + CM["Ручной Makefile<br/>или CMake"] + CL["Компоновщик"] + CB["Итоговый бинарный файл"] CC --> CM CM --> CL CL --> CB - CDep["Manual dependency<br/>management"] + CDep["Ручное управление<br/>зависимостями"] CLib1["libcurl-dev<br/>(apt install)"] CLib2["libjson-dev<br/>(apt install)"] - CInc["Manual include paths<br/>-I/usr/include/curl"] - CLink["Manual linking<br/>-lcurl -ljson"] + CInc["Ручные пути включения<br/>-I/usr/include/curl"] + CLink["Ручная компоновка<br/>-lcurl -ljson"] CDep --> CLib1 CDep --> CLib2 @@ -65,28 +65,28 @@ graph TD CInc --> CM CLink --> CL - C_ISSUES["[ERROR] Version conflicts<br/>[ERROR] Platform differences<br/>[ERROR] Missing dependencies<br/>[ERROR] Linking order matters<br/>[ERROR] No automated updates"] + C_ISSUES["[ОШИБКА] Конфликты версий<br/>[ОШИБКА] Различия платформ<br/>[ОШИБКА] Отсутствующие зависимости<br/>[ОШИБКА] Важен порядок компоновки<br/>[ОШИБКА] Нет автоматических обновлений"] end - subgraph "Rust Cargo Build Process" - RS["Rust Source Files<br/>(.rs)"] + subgraph "Процесс сборки Rust с Cargo" + RS["Исходные файлы Rust<br/>(.rs)"] CT["Cargo.toml<br/>[dependencies]<br/>reqwest = '0.11'<br/>serde_json = '1.0'"] - CRG["Cargo Build System"] - RB["Final Binary"] + CRG["Система сборки Cargo"] + RB["Итоговый бинарный файл"] RS --> CRG CT --> CRG CRG --> RB - CRATES["crates.io<br/>(Package registry)"] - DEPS["Automatic dependency<br/>resolution"] - LOCK["Cargo.lock<br/>(Version pinning)"] + CRATES["crates.io<br/>(реестр пакетов)"] + DEPS["Автоматическое разрешение<br/>зависимостей"] + LOCK["Cargo.lock<br/>(фиксация версий)"] CRATES --> DEPS DEPS --> CRG CRG --> LOCK - R_BENEFITS["[OK] Semantic versioning<br/>[OK] Automatic downloads<br/>[OK] Cross-platform<br/>[OK] Transitive dependencies<br/>[OK] Reproducible builds"] + R_BENEFITS["[OK] Семантическое версионирование<br/>[OK] Автоматическая загрузка<br/>[OK] Кроссплатформенность<br/>[OK] Транзитивные зависимости<br/>[OK] Воспроизводимые сборки"] end style C_ISSUES fill:#ff6b6b,color:#000 @@ -99,34 +99,34 @@ graph TD style CRATES fill:#91e5a3,color:#000 ``` -### Cargo Project Structure +### Структура проекта Cargo ```text my_project/ -|-- Cargo.toml # Project configuration (like package.json) -|-- Cargo.lock # Exact dependency versions (auto-generated) +|-- Cargo.toml # Конфигурация проекта (аналог package.json) +|-- Cargo.lock # Точные версии зависимостей (генерируется автоматически) |-- src/ -| |-- main.rs # Main entry point for binary -| |-- lib.rs # Library root (if creating a library) -| `-- bin/ # Additional binary targets -|-- tests/ # Integration tests -|-- examples/ # Example code -|-- benches/ # Benchmarks -`-- target/ # Build artifacts (like C's build/ or obj/) - |-- debug/ # Debug builds (fast compile, slow runtime) - `-- release/ # Release builds (slow compile, fast runtime) +| |-- main.rs # Главная точка входа для бинарного файла +| |-- lib.rs # Корень библиотеки (если создаёте библиотеку) +| `-- bin/ # Дополнительные бинарные цели +|-- tests/ # Интеграционные тесты +|-- examples/ # Примеры кода +|-- benches/ # Бенчмарки +`-- target/ # Артефакты сборки (аналог build/ или obj/ в C) + |-- debug/ # Отладочные сборки (быстрая компиляция, медленное выполнение) + `-- release/ # Релизные сборки (медленная компиляция, быстрое выполнение) ``` -### Common Cargo Commands +### Основные команды Cargo ```mermaid graph LR - subgraph "Project Lifecycle" - NEW["cargo new my_project<br/>[FOLDER] Create new project"] - CHECK["cargo check<br/>[SEARCH] Fast syntax check"] - BUILD["cargo build<br/>[BUILD] Compile project"] - RUN["cargo run<br/>[PLAY] Build and execute"] - TEST["cargo test<br/>[TEST] Run all tests"] + subgraph "Жизненный цикл проекта" + NEW["cargo new my_project<br/>[FOLDER] Создать проект"] + CHECK["cargo check<br/>[SEARCH] Быстрая проверка синтаксиса"] + BUILD["cargo build<br/>[BUILD] Скомпилировать проект"] + RUN["cargo run<br/>[PLAY] Собрать и выполнить"] + TEST["cargo test<br/>[TEST] Запустить все тесты"] NEW --> CHECK CHECK --> BUILD @@ -134,17 +134,17 @@ graph LR BUILD --> TEST end - subgraph "Advanced Commands" - UPDATE["cargo update<br/>[CHART] Update dependencies"] - FORMAT["cargo fmt<br/>[SPARKLES] Format code"] - LINT["cargo clippy<br/>[WRENCH] Lint and suggestions"] - DOC["cargo doc<br/>[BOOKS] Generate documentation"] - PUBLISH["cargo publish<br/>[PACKAGE] Publish to crates.io"] + subgraph "Дополнительные команды" + UPDATE["cargo update<br/>[CHART] Обновить зависимости"] + FORMAT["cargo fmt<br/>[SPARKLES] Форматировать код"] + LINT["cargo clippy<br/>[WRENCH] Линтинг и подсказки"] + DOC["cargo doc<br/>[BOOKS] Сгенерировать документацию"] + PUBLISH["cargo publish<br/>[PACKAGE] Опубликовать на crates.io"] end - subgraph "Build Profiles" - DEBUG["cargo build<br/>(debug profile)<br/>Fast compile<br/>Slow runtime<br/>Debug symbols"] - RELEASE["cargo build --release<br/>(release profile)<br/>Slow compile<br/>Fast runtime<br/>Optimized"] + subgraph "Профили сборки" + DEBUG["cargo build<br/>(отладочный профиль)<br/>Быстрая компиляция<br/>Медленное выполнение<br/>Отладочные символы"] + RELEASE["cargo build --release<br/>(релизный профиль)<br/>Медленная компиляция<br/>Быстрое выполнение<br/>Оптимизирован"] end style NEW fill:#a3d5ff,color:#000 @@ -156,17 +156,15 @@ graph LR style RELEASE fill:#ef4444,color:#000 ``` -# Example: cargo and crates -- In this example, we have a standalone executable crate with no other dependencies -- Use the following commands to create a new crate called ```helloworld``` +# Пример: cargo и крейты +- В этом примере у нас есть самостоятельный исполняемый крейт без других зависимостей +- Используйте следующие команды, чтобы создать новый крейт под названием ```helloworld``` ```bash cargo new helloworld cd helloworld cat Cargo.toml ``` -- By default, ```cargo run``` will compile and run the ```debug``` (unoptimized) version of the crate. To execute the ```release``` version, use ```cargo run --release``` -- Note that actual binary file resides under the ```target``` folder under the ```debug``` or ```release``` folder -- We might have also noticed a file called ```Cargo.lock``` in the same folder as the source. It is automatically generated and should not be modified by hand - - We will revisit the specific purpose of ```Cargo.lock``` later - - +- По умолчанию ```cargo run``` скомпилирует и запустит ```debug```-версию крейта (без оптимизаций). Чтобы запустить ```release```-версию, используйте ```cargo run --release``` +- Обратите внимание: сам бинарный файл находится в папке ```target```, внутри папки ```debug``` или ```release``` +- Возможно, вы также заметили файл ```Cargo.lock``` рядом с исходниками. Он генерируется автоматически и не должен редактироваться вручную + - Конкретное назначение ```Cargo.lock``` мы рассмотрим позже diff --git a/c-cpp-book/src/ch03-built-in-types.md b/c-cpp-book/src/ch03-built-in-types.md index baead76..0b98926 100644 --- a/c-cpp-book/src/ch03-built-in-types.md +++ b/c-cpp-book/src/ch03-built-in-types.md @@ -1,41 +1,41 @@ -# Built-in Rust types +# Встроенные типы Rust -> **What you'll learn:** Rust's fundamental types (`i32`, `u64`, `f64`, `bool`, `char`), type inference, explicit type annotations, and how they compare to C/C++ primitive types. No implicit conversions — Rust requires explicit casts. +> **Что вы узнаете:** фундаментальные типы Rust (`i32`, `u64`, `f64`, `bool`, `char`), вывод типов, явные аннотации типов и то, как они соотносятся с примитивными типами C/C++. Неявных преобразований нет — Rust требует явных приведений. -- Rust has type inference, but also allows explicit specification of the type +- В Rust есть вывод типов, но допускается и явное указание типа -| **Description** | **Type** | **Example** | +| **Описание** | **Тип** | **Пример** | |:-----------------:|:------------------------------:|:-----------------------------:| -| Signed integers | i8, i16, i32, i64, i128, isize | -1, 42, 1_00_000, 1_00_000i64 | -| Unsigned integers | u8, u16, u32, u64, u128, usize | 0, 42, 42u32, 42u64 | -| Floating point | f32, f64 | 0.0, 0.42 | +| Знаковые целые | i8, i16, i32, i64, i128, isize | -1, 42, 1_00_000, 1_00_000i64 | +| Беззнаковые целые | u8, u16, u32, u64, u128, usize | 0, 42, 42u32, 42u64 | +| Числа с плавающей точкой | f32, f64 | 0.0, 0.42 | | Unicode | char | 'a', '$' | -| Boolean | bool | true, false | +| Логический | bool | true, false | -- Rust permits arbitrary use of ```_``` between numbers for ease of reading +- Rust допускает произвольное использование ```_``` между цифрами для удобства чтения ---- -### Rust type specification and assignment -- Rust uses the ```let``` keyword to assign values to variables. The type of the variable can be optionally specified after a ```:``` +### Указание типа и присваивание в Rust +- Для присваивания значений переменным в Rust используется ключевое слово ```let```. Тип переменной можно необязательно указать после ```:``` ```rust fn main() { let x : i32 = 42; - // These two assignments are logically equivalent + // Эти два присваивания логически эквивалентны let y : u32 = 42; let z = 42u32; } ``` -- Function parameters and return values (if any) require an explicit type. The following takes a u8 parameter and returns u32 +- Параметры функций и возвращаемые значения (если есть) требуют явного указания типа. Следующая функция принимает параметр u8 и возвращает u32 ```rust fn foo(x : u8) -> u32 { return x as u32 * x as u32; } ``` -- Unused variables are prefixed with ```_``` to avoid compiler warnings +- Неиспользуемые переменные помечаются префиксом ```_```, чтобы не получать предупреждения компилятора ---- -# Rust type specification and inference -- Rust can automatically infer the type of the variable based on the context. -- [▶ Try it in the Rust Playground](https://play.rust-lang.org/) +# Указание типа и вывод типа в Rust +- Rust может автоматически вывести тип переменной из контекста. +- [▶ Попробуйте в Rust Playground](https://play.rust-lang.org/) ```rust fn secret_of_life_u32(x : u32) { println!("The u32 secret_of_life is {}", x); @@ -46,32 +46,29 @@ fn secret_of_life_u8(x : u8) { } fn main() { - let a = 42; // The let keyword assigns a value; type of a is u32 - let b = 42; // The let keyword assigns a value; inferred type of b is u8 + let a = 42; // Ключевое слово let присваивает значение; тип a — u32 + let b = 42; // Ключевое слово let присваивает значение; выведенный тип b — u8 secret_of_life_u32(a); secret_of_life_u8(b); } ``` -# Rust variables and mutability -- Rust variables are **immutable** by default unless the ```mut``` keyword is used to denote that a variable is mutable. For example, the following code will not compile unless the ```let a = 42``` is changed to ```let mut a = 42``` +# Переменные и изменяемость в Rust +- Переменные в Rust по умолчанию **неизменяемые**, если не использовано ключевое слово ```mut```, которое обозначает изменяемость переменной. Например, следующий код не скомпилируется, пока ```let a = 42``` не будет заменено на ```let mut a = 42``` ```rust fn main() { - let a = 42; // Must be changed to let mut a = 42 to permit the assignment below - a = 43; // Will not compile unless the above is changed + let a = 42; // Нужно заменить на let mut a = 42, чтобы разрешить присваивание ниже + a = 43; // Не скомпилируется, пока выше не внесено изменение } ``` -- Rust permits the reuse of the variable names (shadowing) +- Rust допускает повторное использование имён переменных (затенение, shadowing) ```rust fn main() { let a = 42; { - let a = 43; //OK: Different variable with the same name + let a = 43; //OK: Другая переменная с тем же именем } - // a = 43; // Not permitted - let a = 43; // Ok: New variable and assignment + // a = 43; // Не допускается + let a = 43; // Ок: новая переменная и присваивание } ``` - - - diff --git a/c-cpp-book/src/ch04-control-flow.md b/c-cpp-book/src/ch04-control-flow.md index a4bd850..97aedd4 100644 --- a/c-cpp-book/src/ch04-control-flow.md +++ b/c-cpp-book/src/ch04-control-flow.md @@ -1,8 +1,8 @@ -# Rust if keyword +# Ключевое слово if в Rust -> **What you'll learn:** Rust's control flow constructs — `if`/`else` as expressions, `loop`/`while`/`for`, `match`, and how they differ from C/C++ counterparts. The key insight: most Rust control flow returns values. +> **Что вы узнаете:** конструкции управления потоком выполнения в Rust — `if`/`else` как выражения, `loop`/`while`/`for`, `match` и то, чем они отличаются от аналогов в C/C++. Ключевая мысль: большинство конструкций управления потоком в Rust возвращают значения. -- In Rust, ```if``` is actually an expression, i.e., it can be used to assign values, but it also behaves like a statement. [▶ Try it](https://play.rust-lang.org/) +- В Rust ```if``` — это выражение, то есть его можно использовать для присваивания значений, но он также ведёт себя как оператор. [▶ Попробовать](https://play.rust-lang.org/) ```rust fn main() { @@ -19,8 +19,8 @@ fn main() { } ``` -# Rust loops using while and for -- The ```while``` keyword can be used to loop while an expression is true +# Циклы while и for в Rust +- Ключевое слово ```while``` можно использовать для цикла, пока выражение истинно ```rust fn main() { let mut x = 40; @@ -29,55 +29,53 @@ fn main() { } } ``` -- The ```for``` keyword can be used to iterate over ranges +- Ключевое слово ```for``` можно использовать для итерации по диапазонам ```rust fn main() { - // Will not print 43; use 40..=43 to include last element + // Не выведет 43; используйте 40..=43, чтобы включить последний элемент for x in 40..43 { println!("{}", x); } } ``` -# Rust loops using loop -- The ```loop``` keyword creates an infinite loop until a ```break``` is encountered +# Циклы loop в Rust +- Ключевое слово ```loop``` создаёт бесконечный цикл, пока не встретится ```break``` ```rust fn main() { let mut x = 40; - // Change the below to 'here: loop to specify optional label for the loop + // Замените на 'here: loop, чтобы указать необязательную метку цикла loop { if x == 42 { - break; // Use break x; to return the value of x + break; // Используйте break x; чтобы вернуть значение x } x += 1; } } ``` -- The ```break``` statement can include an optional expression that can be used to assign the value of a ```loop``` expression -- The ```continue``` keyword can be used to return to the top of the ```loop``` -- Loop labels can be used with ```break``` or ```continue``` and are useful when dealing with nested loops +- Оператор ```break``` может включать необязательное выражение, которое используется для присваивания значения выражению ```loop``` +- Ключевое слово ```continue``` возвращает выполнение к началу ```loop``` +- Метки циклов можно использовать с ```break``` или ```continue```; они полезны при работе с вложенными циклами -# Rust expression blocks -- Rust expression blocks are simply a sequence of expressions enclosed in ```{}```. The evaluated value is simply the last expression in the block +# Блоки-выражения в Rust +- Блок-выражение в Rust — это просто последовательность выражений в ```{}```. Значением блока становится последнее выражение в нём ```rust fn main() { let x = { let y = 40; - y + 2 // Note: ; must be omitted + y + 2 // Примечание: ; здесь не ставится }; - // Notice the Python style printing + // Обратите внимание на вывод в стиле Python println!("{x}"); } ``` -- Rust style is to use this to omit the ```return``` keyword in functions +- В стиле Rust принято опускать ключевое слово ```return``` в функциях, используя это правило ```rust fn is_secret_of_life(x: u32) -> bool { - // Same as if x == 42 {true} else {false} - x == 42 // Note: ; must be omitted + // То же, что if x == 42 {true} else {false} + x == 42 // Примечание: ; здесь не ставится } fn main() { println!("{}", is_secret_of_life(42)); } ``` - - diff --git a/c-cpp-book/src/ch05-data-structures.md b/c-cpp-book/src/ch05-data-structures.md index 4017076..3d92d4d 100644 --- a/c-cpp-book/src/ch05-data-structures.md +++ b/c-cpp-book/src/ch05-data-structures.md @@ -1,37 +1,37 @@ -### Rust array type +### Массивы в Rust -> **What you'll learn:** Rust's core data structures — arrays, tuples, slices, strings, structs, `Vec`, and `HashMap`. This is a dense chapter; focus on understanding `String` vs `&str` and how structs work. You'll revisit references and borrowing in depth in chapter 7. +> **Что вы узнаете:** основные структуры данных Rust — массивы, кортежи, срезы, строки, структуры, `Vec` и `HashMap`. Глава плотная; сосредоточьтесь на понимании `String` и `&str` и на том, как работают структуры. Ссылки и заимствования вы разберёте подробно в главе 7. -- Arrays contain a fixed number of elements of the same type - - Like all other Rust types, arrays are immutable by default (unless mut is used) - - Arrays are indexed using [] and are bounds checked. The len() method can be used to obtain the length of the array +- Массивы содержат фиксированное количество элементов одного типа + - Как и все остальные типы Rust, массивы по умолчанию неизменяемые (если не используется mut) + - Доступ к элементам массива осуществляется через [] и проверяется на выход за границы. Длину массива можно получить методом len() ```rust fn get_index(y : usize) -> usize { y+1 } fn main() { - // Initializes an array of 3 elements and sets all to 42 + // Инициализирует массив из 3 элементов и задаёт всем значение 42 let a : [u8; 3] = [42; 3]; - // Alternative syntax + // Альтернативный синтаксис // let a = [42u8, 42u8, 42u8]; for x in a { println!("{x}"); } let y = get_index(a.len()); - // Commenting out the below will cause a panic + // Если раскомментировать строку ниже, произойдёт panic //println!("{}", a[y]); } ``` ---- -### Rust array type continued -- Arrays can be nested - - Rust has several built-in formatters for printing. In the below, the ```:?``` is the ```debug``` print formatter. The ```:#?``` formatter can be used for ```pretty print```. These formatters can be customized per type (more on this later) +### Массивы в Rust (продолжение) +- Массивы можно вкладывать друг в друга + - В Rust есть несколько встроенных форматировщиков для вывода. В примере ниже ```:?``` — это форматировщик ```debug```. Форматировщик ```:#?``` используется для ```pretty print``` (красивого вывода). Эти форматировщики можно настроить для каждого типа (подробнее об этом позже) ```rust fn main() { let a = [ - [40, 0], // Define a nested array + [40, 0], // Определяем вложенный массив [41, 0], [42, 1], ]; @@ -41,10 +41,10 @@ } ``` ---- -### Rust tuples -- Tuples have a fixed size and can group arbitrary types into a single compound type - - The constituent types can be indexed by their relative location (.0, .1, .2, ...). An empty tuple, i.e., () is called the unit value and is the equivalent of a void return value - - Rust supports tuple destructuring to make it easy to bind variables to individual elements +### Кортежи в Rust +- Кортежи имеют фиксированный размер и могут объединять значения произвольных типов в один составной тип + - Элементы кортежа индексируются по их позиции (.0, .1, .2, ...). Пустой кортеж, то есть (), называется значением unit и соответствует возврату void + - Rust поддерживает деструктуризацию кортежей, что упрощает привязку переменных к отдельным элементам ```rust fn get_tuple() -> (u32, bool) { (42, true) @@ -55,57 +55,57 @@ fn main() { let u : (u32, bool) = (43, false); println!("{}, {}", t.0, t.1); println!("{}, {}", u.0, u.1); - let (num, flag) = get_tuple(); // Tuple destructuring + let (num, flag) = get_tuple(); // Деструктуризация кортежа println!("{num}, {flag}"); } ``` -### Rust references -- References in Rust are roughly equivalent to pointers in C with some key differences - - It is legal to have any number of read-only (immutable) references to a variable at any point of time. A reference cannot outlive the variable scope (this is a key concept called **lifetime**; discussed in detail later) - - Only a single writable (mutable) reference to a mutable variable is permitted and it must not overlap with any other reference. +### Ссылки в Rust +- Ссылки в Rust примерно соответствуют указателям в C, но с некоторыми ключевыми отличиями + - В любой момент времени допустимо иметь любое количество ссылок только для чтения (неизменяемых) на переменную. Ссылка не может пережить область видимости переменной (это ключевое понятие под названием **время жизни**, которое подробно рассматривается дальше) + - Для изменяемой переменной допускается только одна изменяемая ссылка для записи, и она не должна пересекаться ни с какими другими ссылками. ```rust fn main() { let mut a = 42; { let b = &a; let c = b; - println!("{} {}", *b, *c); // The compiler automatically dereferences *c + println!("{} {}", *b, *c); // Компилятор автоматически разыменовывает *c let d = &mut a; /* - * Uncommenting the line below would cause the - * program to not compile, because `b` is used - * while the mutable reference `d` is live in the current scope + * Если раскомментировать строку ниже, программа + * не скомпилируется, потому что `b` используется, + * пока изменяемая ссылка `d` активна в текущей области * - * You cannot have a mutable and immutable reference in use in the same scope - * at the same time! + * Нельзя одновременно использовать изменяемую и неизменяемую ссылку + * в одной области видимости! */ // println!("{}", *b); } - let d = &mut a; // Ok: b and c are not in scope + let d = &mut a; // Ок: b и c уже вне области видимости *d = 43; } ``` ---- -# Rust slices -- Rust references can be used to create subsets of arrays - - Unlike arrays, which have a static fixed length determined at compile time, slices can be of arbitrary size. Internally, slices are implemented as a "fat-pointer" that contains the length of the slice and a pointer to the starting element in the original array +# Срезы в Rust +- Ссылки в Rust можно использовать для создания подмножеств массивов + - В отличие от массивов, которые имеют статическую фиксированную длину, известную на этапе компиляции, срезы могут иметь произвольный размер. Внутри срез реализован как «толстый указатель» (fat pointer), который хранит длину среза и указатель на начальный элемент исходного массива ```rust fn main() { let a = [40, 41, 42, 43]; - let b = &a[1..a.len()]; // A slice starting with the second element in the original - let c = &a[1..]; // Same as the above - let d = &a[..]; // Same as &a[0..] or &a[0..a.len()] + let b = &a[1..a.len()]; // Срез, начинающийся со второго элемента исходного массива + let c = &a[1..]; // То же, что и выше + let d = &a[..]; // То же, что &a[0..] или &a[0..a.len()] println!("{b:?} {c:?} {d:?}"); } ``` ---- -# Rust constants and statics -- The ```const``` keyword can be used to define a constant value. Constant values are evaluated at **compile time** and are inlined into the program -- The ```static``` keyword is used to define the equivalent of global variables in languages like C/C++ Static variables have an addressable memory location and are created once and last the entire lifetime of the program +# Константы и статики в Rust +- Ключевое слово ```const``` используется для определения константного значения. Константы вычисляются на **этапе компиляции** и встраиваются в программу +- Ключевое слово ```static``` используется для определения аналога глобальных переменных из языков вроде C/C++. Статические переменные имеют адрес в памяти, создаются один раз и существуют на протяжении всего времени работы программы ```rust const SECRET_OF_LIFE: u32 = 42; static GLOBAL_VARIABLE : u32 = 2; @@ -116,80 +116,80 @@ fn main() { ``` ---- -# Rust strings: String vs &str +# Строки в Rust: String и &str -- Rust has **two** string types that serve different purposes - - `String` — owned, heap-allocated, growable (like C's `malloc`'d buffer, or C++'s `std::string`) - - `&str` — borrowed, lightweight reference (like C's `const char*` with length, or C++'s `std::string_view` — but `&str` is **lifetime-checked** so it can never dangle) - - Unlike C's null-terminated strings, Rust strings track their length and are guaranteed valid UTF-8 +- В Rust **два** строковых типа, которые служат разным целям + - `String` — владеющий, размещённый в куче, растущий (как буфер, выделенный через `malloc` в C, или `std::string` в C++) + - `&str` — заимствованная лёгкая ссылка (как `const char*` с длиной в C или `std::string_view` в C++; но `&str` **проверяется по времени жизни**, поэтому никогда не может стать висячим) + - В отличие от строк с нулевым символом в конце, как в C, строки Rust хранят свою длину и гарантированно являются корректным UTF-8 -> **For C++ developers:** `String` ≈ `std::string`, `&str` ≈ `std::string_view`. Unlike `std::string_view`, a `&str` is guaranteed valid for its entire lifetime by the borrow checker. +> **Для программистов C++:** `String` ≈ `std::string`, `&str` ≈ `std::string_view`. В отличие от `std::string_view`, проверка заимствований гарантирует, что `&str` действителен на протяжении всего своего времени жизни. -## String vs &str: Owned vs Borrowed +## String и &str: владение и заимствование -> **Production patterns**: See [JSON handling: nlohmann::json → serde](ch17-2-avoiding-unchecked-indexing.md#json-handling-nlohmannjson--serde) for how string handling works with serde in production code. +> **Промышленные шаблоны**: о том, как работа со строками выглядит вместе с serde в продакшн-коде, см. [Обработка JSON: nlohmann::json → serde](ch17-2-avoiding-unchecked-indexing.md#обработка-json-nlohmannjson--serde). -| **Aspect** | **C `char*`** | **C++ `std::string`** | **Rust `String`** | **Rust `&str`** | +| **Аспект** | **C `char*`** | **C++ `std::string`** | **Rust `String`** | **Rust `&str`** | |------------|--------------|----------------------|-------------------|----------------| -| **Memory** | Manual (`malloc`/`free`) | Heap-allocated, owns buffer | Heap-allocated, auto-freed | Borrowed reference (lifetime-checked) | -| **Mutability** | Always mutable via pointer | Mutable | Mutable with `mut` | Always immutable | -| **Size info** | None (relies on `'\0'`) | Tracks length and capacity | Tracks length and capacity | Tracks length (fat pointer) | -| **Encoding** | Unspecified (usually ASCII) | Unspecified (usually ASCII) | Guaranteed valid UTF-8 | Guaranteed valid UTF-8 | -| **Null terminator** | Required | Required (`c_str()`) | Not used | Not used | +| **Память** | Вручную (`malloc`/`free`) | В куче, владеет буфером | В куче, освобождается автоматически | Заимствованная ссылка (проверяется по времени жизни) | +| **Изменяемость** | Всегда изменяема через указатель | Изменяема | Изменяема при наличии `mut` | Всегда неизменяема | +| **Информация о размере** | Отсутствует (полагается на `'\0'`) | Хранит длину и ёмкость | Хранит длину и ёмкость | Хранит длину (толстый указатель) | +| **Кодировка** | Не определена (обычно ASCII) | Не определена (обычно ASCII) | Гарантированно корректный UTF-8 | Гарантированно корректный UTF-8 | +| **Завершающий нуль** | Обязателен | Обязателен (`c_str()`) | Не используется | Не используется | ```rust fn main() { - // &str - string slice (borrowed, immutable, usually a string literal) - let greeting: &str = "Hello"; // Points to read-only memory + // &str — строковый срез (заимствованный, неизменяемый, обычно строковый литерал) + let greeting: &str = "Hello"; // Указывает на память только для чтения - // String - owned, heap-allocated, growable - let mut owned = String::from(greeting); // Copies data to heap - owned.push_str(", World!"); // Grow the string - owned.push('!'); // Append a single character + // String — владеющая, размещённая в куче, растущая строка + let mut owned = String::from(greeting); // Копирует данные в кучу + owned.push_str(", World!"); // Увеличиваем строку + owned.push('!'); // Добавляем один символ - // Converting between String and &str - let slice: &str = &owned; // String -> &str (free, just a borrow) - let owned2: String = slice.to_string(); // &str -> String (allocates) - let owned3: String = String::from(slice); // Same as above + // Преобразования между String и &str + let slice: &str = &owned; // String -> &str (бесплатно, это просто заимствование) + let owned2: String = slice.to_string(); // &str -> String (выделяет память) + let owned3: String = String::from(slice); // То же, что и выше - // String concatenation (note: + consumes the left operand) + // Конкатенация строк (обратите внимание: + поглощает левый операнд) let hello = String::from("Hello"); let world = String::from(", World!"); - let combined = hello + &world; // hello is moved (consumed), world is borrowed - // println!("{hello}"); // Won't compile: hello was moved + let combined = hello + &world; // hello перемещён (поглощён), world заимствован + // println!("{hello}"); // Не скомпилируется: hello был перемещён - // Use format! to avoid move issues + // Используйте format!, чтобы избежать проблем с перемещением let a = String::from("Hello"); let b = String::from("World"); - let combined = format!("{a}, {b}!"); // Neither a nor b is consumed + let combined = format!("{a}, {b}!"); // Ни a, ни b не поглощаются println!("{combined}"); } ``` -## Why You Cannot Index Strings with `[]` +## Почему нельзя индексировать строки через `[]` ```rust fn main() { let s = String::from("hello"); - // let c = s[0]; // Won't compile! Rust strings are UTF-8, not byte arrays + // let c = s[0]; // Не скомпилируется! Строки Rust — это UTF-8, а не массивы байтов - // Safe alternatives: + // Безопасные альтернативы: let first_char = s.chars().next(); // Option<char>: Some('h') - let as_bytes = s.as_bytes(); // &[u8]: raw UTF-8 bytes - let substring = &s[0..1]; // &str: "h" (byte range, must be valid UTF-8 boundary) + let as_bytes = s.as_bytes(); // &[u8]: сырые байты UTF-8 + let substring = &s[0..1]; // &str: "h" (диапазон байтов, должен попадать на границу UTF-8) println!("First char: {:?}", first_char); println!("Bytes: {:?}", &as_bytes[..5]); } ``` -## Exercise: String manipulation +## Упражнение: работа со строками -🟢 **Starter** -- Write a function `fn count_words(text: &str) -> usize` that counts the number of whitespace-separated words in a string -- Write a function `fn longest_word(text: &str) -> &str` that returns the longest word (hint: you'll need to think about lifetimes -- why does the return type need to be `&str` and not `String`?) +🟢 **Начальный уровень** +- Напишите функцию `fn count_words(text: &str) -> usize`, которая считает количество слов, разделённых пробельными символами, в строке +- Напишите функцию `fn longest_word(text: &str) -> &str`, которая возвращает самое длинное слово (подсказка: придётся подумать о временах жизни — почему возвращаемый тип должен быть `&str`, а не `String`?) -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn count_words(text: &str) -> usize { @@ -211,10 +211,10 @@ fn main() { </details> -# Rust structs -- The ```struct``` keyword declares a user-defined struct type - - ```struct``` members can either be named, or anonymous (tuple structs) -- Unlike languages like C++, there's no notion of "data inheritance" in Rust +# Структуры в Rust +- Ключевое слово ```struct``` объявляет пользовательский тип структуры + - Члены ```struct``` могут быть именованными или безымянными (кортежные структуры) +- В отличие от языков вроде C++, в Rust нет понятия «наследования данных» ```rust fn main() { struct MyStruct { @@ -229,14 +229,14 @@ fn main() { num: x.num, is_secret_of_life: x.is_secret_of_life, }; - let z = MyStruct { num: x.num, ..x }; // The .. means copy remaining + let z = MyStruct { num: x.num, ..x }; // Оператор .. означает: скопировать остальные поля println!("{} {} {}", x.num, y.is_secret_of_life, z.num); } ``` -# Rust tuple structs -- Rust tuple structs are similar to tuples and individual fields don't have names - - Like tuples, individual elements are accessed using .0, .1, .2, .... A common use case for tuple structs is to wrap primitive types to create custom types. **This can be useful to avoid mixing differing values of the same type** +# Кортежные структуры в Rust +- Кортежные структуры в Rust похожи на кортежи, а их поля не имеют имён + - Как и в кортежах, к отдельным элементам обращаются через .0, .1, .2, .... Типичный случай использования кортежных структур — оборачивание примитивных типов для создания собственных типов. **Это может быть полезно, чтобы не перепутать значения одного и того же типа** ```rust struct WeightInGrams(u32); struct WeightInMilligrams(u32); @@ -251,71 +251,71 @@ fn to_weight_in_milligrams(w : WeightInGrams) -> WeightInMilligrams { fn main() { let x = to_weight_in_grams(42); let y = to_weight_in_milligrams(x); - // let z : WeightInGrams = x; // Won't compile: x was moved into to_weight_in_milligrams() - // let a : WeightInGrams = y; // Won't compile: type mismatch (WeightInMilligrams vs WeightInGrams) + // let z : WeightInGrams = x; // Не скомпилируется: x был перемещён в to_weight_in_milligrams() + // let a : WeightInGrams = y; // Не скомпилируется: несоответствие типов (WeightInMilligrams и WeightInGrams) } ``` -**Note**: The `#[derive(...)]` attribute automatically generates common trait implementations for structs and enums. You'll see this used throughout the course: +**Примечание**: атрибут `#[derive(...)]` автоматически генерирует реализации распространённых трейтов для структур и перечислений. Вы будете встречать его на протяжении всего курса: ```rust #[derive(Debug, Clone, PartialEq)] struct Point { x: i32, y: i32 } fn main() { let p = Point { x: 1, y: 2 }; - println!("{:?}", p); // Debug: works because of #[derive(Debug)] - let p2 = p.clone(); // Clone: works because of #[derive(Clone)] - assert_eq!(p, p2); // PartialEq: works because of #[derive(PartialEq)] + println!("{:?}", p); // Debug: работает благодаря #[derive(Debug)] + let p2 = p.clone(); // Clone: работает благодаря #[derive(Clone)] + assert_eq!(p, p2); // PartialEq: работает благодаря #[derive(PartialEq)] } ``` -We'll cover the trait system in depth later, but `#[derive(Debug)]` is so useful that you should add it to nearly every `struct` and `enum` you create. +Трейтовую систему мы разберём подробно позже, но `#[derive(Debug)]` настолько полезен, что его стоит добавлять почти к каждой создаваемой вами `struct` и `enum`. -# Rust Vec type -- The ```Vec<T>``` type implements a dynamic heap allocated buffer (similar to manually managed `malloc`/`realloc` arrays in C, or C++'s `std::vector`) - - Unlike arrays with fixed size, `Vec` can grow and shrink at runtime - - `Vec` owns its data and automatically manages memory allocation/deallocation -- Common operations: `push()`, `pop()`, `insert()`, `remove()`, `len()`, `capacity()` +# Тип Vec в Rust +- Тип ```Vec<T>``` реализует динамический буфер, размещённый в куче (похоже на вручную управляемые массивы через `malloc`/`realloc` в C или на `std::vector` в C++) + - В отличие от массивов фиксированного размера, `Vec` может расти и уменьшаться во время выполнения + - `Vec` владеет своими данными и автоматически управляет выделением и освобождением памяти +- Распространённые операции: `push()`, `pop()`, `insert()`, `remove()`, `len()`, `capacity()` ```rust fn main() { - let mut v = Vec::new(); // Empty vector, type inferred from usage - v.push(42); // Add element to end - Vec<i32> + let mut v = Vec::new(); // Пустой вектор, тип выводится из использования + v.push(42); // Добавляем элемент в конец — Vec<i32> v.push(43); - // Safe iteration (preferred) - for x in &v { // Borrow elements, don't consume vector + // Безопасная итерация (предпочтительно) + for x in &v { // Заимствуем элементы, не поглощая вектор println!("{x}"); } - // Initialization shortcuts - let mut v2 = vec![1, 2, 3, 4, 5]; // Macro for initialization - let v3 = vec![0; 10]; // 10 zeros + // Сокращённые способы инициализации + let mut v2 = vec![1, 2, 3, 4, 5]; // Макрос для инициализации + let v3 = vec![0; 10]; // 10 нулей - // Safe access methods (preferred over indexing) + // Безопасные методы доступа (предпочтительнее индексации) match v2.get(0) { Some(first) => println!("First: {first}"), None => println!("Empty vector"), } - // Useful methods + // Полезные методы println!("Length: {}, Capacity: {}", v2.len(), v2.capacity()); - if let Some(last) = v2.pop() { // Remove and return last element + if let Some(last) = v2.pop() { // Удаляем и возвращаем последний элемент println!("Popped: {last}"); } - // Dangerous: direct indexing (can panic!) - // println!("{}", v2[100]); // Would panic at runtime + // Опасно: прямая индексация (может вызвать panic!) + // println!("{}", v2[100]); // Во время выполнения произойдёт panic } ``` -> **Production patterns**: See [Avoiding unchecked indexing](ch17-2-avoiding-unchecked-indexing.md#avoiding-unchecked-indexing) for safe `.get()` patterns from production Rust code. +> **Промышленные шаблоны**: о безопасных шаблонах `.get()` из продакшн-кода на Rust см. [Избегание неконтролируемой индексации](ch17-2-avoiding-unchecked-indexing.md#избегание-неконтролируемой-индексации). -# Rust HashMap type -- ```HashMap``` implements generic ```key``` -> ```value``` lookups (a.k.a. ```dictionary``` or ```map```) +# Тип HashMap в Rust +- ```HashMap``` реализует поиск по обобщённому ```ключу``` -> ```значению``` (также известен как ```dictionary``` или ```map```) ```rust fn main() { - use std::collections::HashMap; // Need explicit import, unlike Vec - let mut map = HashMap::new(); // Allocate an empty HashMap - map.insert(40, false); // Type is inferred as int -> bool + use std::collections::HashMap; // Нужен явный импорт, в отличие от Vec + let mut map = HashMap::new(); // Выделяем пустой HashMap + map.insert(40, false); // Тип выводится как int -> bool map.insert(41, false); map.insert(42, true); for (key, value) in map { @@ -327,17 +327,17 @@ fn main() { } else { println!("No mapping was found for 43"); } - let x = map.get(&43).or(Some(&false)); // Default value if key isn't found + let x = map.get(&43).or(Some(&false)); // Значение по умолчанию, если ключ не найден println!("{x:?}"); } ``` -# Exercise: Vec and HashMap +# Упражнение: Vec и HashMap -🟢 **Starter** -- Create a ```HashMap<u32, bool>``` with a few entries (make sure that some values are ```true``` and others are ```false```). Loop over all elements in the hashmap and put the keys into one ```Vec``` and the values into another +🟢 **Начальный уровень** +- Создайте `HashMap<u32, bool>` с несколькими записями (убедитесь, что одни значения равны `true`, а другие — `false`). Переберите все элементы хеш-таблицы, поместив ключи в один `Vec`, а значения — в другой -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::collections::HashMap; @@ -353,7 +353,7 @@ fn main() { println!("Keys: {keys:?}"); println!("Values: {values:?}"); - // Alternative: use iterators with unzip() + // Альтернатива: использовать итераторы с unzip() let (keys2, values2): (Vec<u32>, Vec<bool>) = map.into_iter().unzip(); println!("Keys (unzip): {keys2:?}"); println!("Values (unzip): {values2:?}"); @@ -364,100 +364,100 @@ fn main() { --- -## Deep Dive: C++ References vs Rust References +## Углублённый разбор: ссылки C++ и ссылки Rust -> **For C++ developers:** C++ programmers often assume Rust `&T` works like C++ `T&`. While superficially similar, there are fundamental differences that cause confusion. C developers can skip this section — Rust references are covered in [Ownership and Borrowing](ch07-ownership-and-borrowing.md). +> **Для программистов C++:** программисты C++ часто предполагают, что `&T` в Rust работает так же, как `T&` в C++. Хотя внешне они похожи, есть принципиальные различия, которые порождают путаницу. Разработчики на C могут пропустить этот раздел — ссылки в Rust рассматриваются в главе [Владение и заимствование](ch07-ownership-and-borrowing.md). -#### 1. No Rvalue References or Universal References +#### 1. Нет rvalue-ссылок и универсальных ссылок -In C++, `&&` has two meanings depending on context: +В C++ `&&` имеет два значения в зависимости от контекста: ```cpp -// C++: && means different things: -int&& rref = 42; // Rvalue reference — binds to temporaries -void process(Widget&& w); // Rvalue reference — caller must std::move +// C++: && означает разные вещи: +int&& rref = 42; // Rvalue-ссылка — связывается с временными объектами +void process(Widget&& w); // Rvalue-ссылка — вызывающий код должен использовать std::move -// Universal (forwarding) reference — deduced template context: +// Универсальная (forwarding) ссылка — выводится в шаблонном контексте: template<typename T> -void forward(T&& arg) { // NOT an rvalue ref! Deduced as T& or T&& - inner(std::forward<T>(arg)); // Perfect forwarding +void forward(T&& arg) { // НЕ rvalue-ссылка! Выводится как T& или T&& + inner(std::forward<T>(arg)); // Идеальная передача } ``` -**In Rust: none of this exists.** `&&` is simply the logical AND operator. +**В Rust ничего этого нет.** `&&` — это просто оператор логического И. ```rust -// Rust: && is just boolean AND +// Rust: && — это просто булево И let a = true && false; // false -// Rust has NO rvalue references, no universal references, no perfect forwarding. -// Instead: -// - Move is the default for non-Copy types (no std::move needed) -// - Generics + trait bounds replace universal references -// - No temporary-binding distinction — values are values +// В Rust НЕТ rvalue-ссылок, универсальных ссылок и идеальной передачи. +// Вместо этого: +// - Перемещение — поведение по умолчанию для типов, не реализующих Copy (std::move не нужен) +// - Обобщения + ограничения трейтами заменяют универсальные ссылки +// - Нет различия между временными и постоянными привязками — значения есть значения -fn process(w: Widget) { } // Takes ownership (like C++ value param + implicit move) -fn process_ref(w: &Widget) { } // Borrows immutably (like C++ const T&) -fn process_mut(w: &mut Widget) { } // Borrows mutably (like C++ T&, but exclusive) +fn process(w: Widget) { } // Принимает владение (как параметр-значение в C++ + неявное перемещение) +fn process_ref(w: &Widget) { } // Заимствует неизменяемо (как const T& в C++) +fn process_mut(w: &mut Widget) { } // Заимствует изменяемо (как T& в C++, но эксклюзивно) ``` -| C++ Concept | Rust Equivalent | Notes | -|-------------|-----------------|-------| -| `T&` (lvalue ref) | `&T` or `&mut T` | Rust splits into shared vs exclusive | -| `T&&` (rvalue ref) | Just `T` | Take by value = take ownership | -| `T&&` in template (universal ref) | `impl Trait` or `<T: Trait>` | Generics replace forwarding | -| `std::move(x)` | `x` (just use it) | Move is the default | -| `std::forward<T>(x)` | No equivalent needed | No universal references to forward | +| Концепция C++ | Эквивалент в Rust | Примечания | +|---------------|-------------------|------------| +| `T&` (ссылка на lvalue) | `&T` или `&mut T` | Rust разделяет на общие и эксклюзивные | +| `T&&` (rvalue-ссылка) | Просто `T` | Передача по значению = передача владения | +| `T&&` в шаблоне (универсальная ссылка) | `impl Trait` или `<T: Trait>` | Обобщения заменяют передачу | +| `std::move(x)` | `x` (просто используйте) | Перемещение — поведение по умолчанию | +| `std::forward<T>(x)` | Эквивалент не нужен | Нет универсальных ссылок, которые нужно передавать дальше | -#### 2. Moves Are Bitwise — No Move Constructors +#### 2. Перемещения — побитовые, конструкторов перемещения нет -In C++, moving is a *user-defined operation* (move constructor / move assignment). In Rust, moving is always a **bitwise memcpy** of the value, and the source is invalidated: +В C++ перемещение — это *пользовательская операция* (конструктор перемещения / оператор присваивания перемещением). В Rust перемещение всегда является **побитовым memcpy** значения, а исходный объект становится недействительным: ```rust -// Rust move = memcpy the bytes, mark source as invalid +// Перемещение в Rust = memcpy байтов, исходное значение помечается как недействительное let s1 = String::from("hello"); -let s2 = s1; // Bytes of s1 are copied to s2's stack slot - // s1 is now invalid — compiler enforces this -// println!("{s1}"); // ❌ Compile error: value used after move +let s2 = s1; // Байты s1 копируются в слот стека s2 + // s1 теперь недействителен — компилятор это обеспечивает +// println!("{s1}"); // ❌ Ошибка компиляции: значение использовано после перемещения ``` ```cpp -// C++ move = call the move constructor (user-defined!) +// Перемещение в C++ = вызов конструктора перемещения (пользовательского!) std::string s1 = "hello"; -std::string s2 = std::move(s1); // Calls string's move ctor -// s1 is now a "valid but unspecified state" zombie -std::cout << s1; // Compiles! Prints... something (empty string, usually) +std::string s2 = std::move(s1); // Вызывается конструктор перемещения string +// s1 теперь «зомби» в состоянии «допустимо, но не определено» +std::cout << s1; // Компилируется! Выводит... что-то (обычно пустую строку) ``` -**Consequences**: -- Rust has no Rule of Five (no copy ctor, move ctor, copy=, move=, destructor to define) -- No moved-from "zombie" state — the compiler simply prevents access -- No `noexcept` considerations for moves — bitwise copy can't throw +**Следствия**: +- В Rust нет правила пяти (не нужно определять конструктор копирования, конструктор перемещения, оператор копирующего присваивания, оператор перемещающего присваивания и деструктор) +- Нет состояния «зомби» после перемещения — компилятор просто запрещает доступ +- Нет вопросов `noexcept` для перемещений — побитовое копирование не может выбросить исключение -#### 3. Auto-Deref: The Compiler Sees Through Indirection +#### 3. Автоматическое разыменование: компилятор видит сквозь косвенность -Rust automatically dereferences through multiple layers of pointers/wrappers via the `Deref` trait. This has no C++ equivalent: +Rust автоматически разыменовывает через несколько слоёв указателей и оберток с помощью трейта `Deref`. В C++ аналога нет: ```rust use std::sync::{Arc, Mutex}; -// Nested wrapping: Arc<Mutex<Vec<String>>> +// Вложенные обёртки: Arc<Mutex<Vec<String>>> let data = Arc::new(Mutex::new(vec!["hello".to_string()])); -// In C++, you'd need explicit unlocking and manual dereferencing at each layer. -// In Rust, the compiler auto-derefs through Arc → Mutex → MutexGuard → Vec: -let guard = data.lock().unwrap(); // Arc auto-derefs to Mutex +// В C++ пришлось бы явно разблокировать и вручную разыменовывать на каждом уровне. +// В Rust компилятор автоматически разыменовывает через Arc → Mutex → MutexGuard → Vec: +let guard = data.lock().unwrap(); // Arc автоматически разыменовывается в Mutex let first: &str = &guard[0]; // MutexGuard→Vec (Deref), Vec[0] (Index), - // &String→&str (Deref coercion) + // &String→&str (приведение через Deref) println!("First: {first}"); -// Method calls also auto-deref: +// Вызовы методов тоже автоматически разыменовываются: let boxed_string = Box::new(String::from("hello")); -println!("Length: {}", boxed_string.len()); // Box→String, then String::len() -// No need for (*boxed_string).len() or boxed_string->len() +println!("Length: {}", boxed_string.len()); // Box→String, затем String::len() +// Не нужно (*boxed_string).len() или boxed_string->len() ``` -**Deref coercion** also applies to function arguments — the compiler inserts dereferences to make types match: +**Приведение через Deref** работает и для аргументов функций — компилятор вставляет разыменования, чтобы типы совпали: ```rust fn greet(name: &str) { @@ -469,67 +469,67 @@ fn main() { let boxed = Box::new(String::from("Bob")); let arced = std::sync::Arc::new(String::from("Carol")); - greet(&owned); // &String → &str (1 deref coercion) - greet(&boxed); // &Box<String> → &String → &str (2 deref coercions) - greet(&arced); // &Arc<String> → &String → &str (2 deref coercions) - greet("Dave"); // &str already — no coercion needed + greet(&owned); // &String → &str (1 приведение через Deref) + greet(&boxed); // &Box<String> → &String → &str (2 приведения через Deref) + greet(&arced); // &Arc<String> → &String → &str (2 приведения через Deref) + greet("Dave"); // Уже &str — приведение не нужно } -// In C++ you'd need .c_str() or explicit conversions for each case. +// В C++ для каждого случая потребовались бы .c_str() или явные преобразования. ``` -**The Deref chain**: When you call `x.method()`, Rust's method resolution -tries the receiver type `T`, then `&T`, then `&mut T`. If no match, it -dereferences via the `Deref` trait and repeats with the target type. -This continues through multiple layers — which is why `Box<Vec<T>>` -"just works" like a `Vec<T>`. Deref *coercion* (for function arguments) -is a separate but related mechanism that automatically converts `&Box<String>` -to `&str` by chaining `Deref` impls. +**Цепочка Deref**: когда вы вызываете `x.method()`, разрешение метода в Rust +сначала пробует тип получателя `T`, затем `&T`, затем `&mut T`. Если совпадения +нет, выполняется разыменование через трейт `Deref`, и процесс повторяется для целевого типа. +Это продолжается через несколько слоёв — поэтому `Box<Vec<T>>` +«просто работает» как `Vec<T>`. Приведение через Deref (для аргументов функций) +— отдельный, но связанный механизм, который автоматически превращает `&Box<String>` +в `&str`, цепочкой реализаций `Deref`. -#### 4. No Null References, No Optional References +#### 4. Нет нулевых ссылок и нет «опциональных» ссылок ```cpp -// C++: references can't be null, but pointers can, and the distinction is blurry -Widget& ref = *ptr; // If ptr is null → UB -Widget* opt = nullptr; // "optional" reference via pointer +// C++: ссылки не могут быть нулевыми, но указатели могут, и грань между ними размыта +Widget& ref = *ptr; // Если ptr нулевой → UB +Widget* opt = nullptr; // «Опциональная» ссылка через указатель ``` ```rust -// Rust: references are ALWAYS valid — guaranteed by the borrow checker -// No way to create a null or dangling reference in safe code -let r: &i32 = &42; // Always valid +// Rust: ссылки ВСЕГДА валидны — гарантируется проверкой заимствований +// В безопасном коде нельзя создать нулевую или висячую ссылку +let r: &i32 = &42; // Всегда валидна -// "Optional reference" is explicit: -let opt: Option<&Widget> = None; // Clear intent, no null pointer +// «Опциональная ссылка» задаётся явно: +let opt: Option<&Widget> = None; // Намерение очевидно, нулевого указателя нет if let Some(w) = opt { - w.do_something(); // Only reachable when present + w.do_something(); // Достижимо только при наличии значения } ``` -#### 5. References Cannot Be Reseated +#### 5. Ссылки нельзя переназначить ```cpp -// C++: a reference is an alias — it can't be rebound +// C++: ссылка — это псевдоним, её нельзя перепривязать int a = 1, b = 2; int& r = a; -r = b; // This ASSIGNS b's value to a — it does NOT rebind r! -// a is now 2, r still refers to a +r = b; // Это ПРИСВАИВАЕТ значение b переменной a — НЕ перепривязывает r! +// Теперь a равно 2, r по-прежнему ссылается на a ``` ```rust -// Rust: let bindings can shadow, but references follow different rules +// Rust: привязки let могут затенять друг друга, но у ссылок свои правила let a = 1; let b = 2; let r = &a; -// r = &b; // ❌ Cannot assign to immutable variable -let r = &b; // ✅ But you can SHADOW r with a new binding - // The old binding is gone, not reseated +// r = &b; // ❌ Нельзя присвоить неизменяемой переменной +let r = &b; // ✅ Но можно ЗАТЕНИТЬ r новой привязкой + // Старая привязка исчезает, а не перепривязывается -// With mut: +// С mut: let mut r = &a; -r = &b; // ✅ r now points to b — this IS rebinding (not assignment through) +r = &b; // ✅ Теперь r указывает на b — это перепривязка (а не присваивание через ссылку) ``` -> **Mental model**: In C++, a reference is a permanent alias for one object. -> In Rust, a reference is a value (a pointer with lifetime guarantees) that -> follows normal variable binding rules — immutable by default, rebindable -> only if declared `mut`. +> **Ментальная модель**: в C++ ссылка — это постоянный псевдоним для одного объекта. +> В Rust ссылка — это значение (указатель с гарантиями времени жизни), которое +> подчиняется обычным правилам привязки переменных: неизменяемая по умолчанию, +> перепривязываемая только если объявлена с `mut`. diff --git a/c-cpp-book/src/ch06-enums-and-pattern-matching.md b/c-cpp-book/src/ch06-enums-and-pattern-matching.md index a5732c3..f05454f 100644 --- a/c-cpp-book/src/ch06-enums-and-pattern-matching.md +++ b/c-cpp-book/src/ch06-enums-and-pattern-matching.md @@ -1,12 +1,12 @@ -# Rust enum types +# Перечисления в Rust -> **What you'll learn:** Rust enums as discriminated unions (tagged unions done right), `match` for exhaustive pattern matching, and how enums replace C++ class hierarchies and C tagged unions with compiler-enforced safety. +> **Что вы узнаете:** перечисления Rust как размеченные объединения (tagged unions, сделанные правильно), `match` для исчерпывающего сопоставления с образцом и то, как перечисления заменяют иерархии классов C++ и размеченные объединения C с безопасностью, которую обеспечивает компилятор. -- Enum types are discriminated unions, i.e., they are a sum type of several possible different types with a tag that identifies the specific variant - - For C developers: enums in Rust can carry data (tagged unions done right — the compiler tracks which variant is active) - - For C++ developers: Rust enums are like `std::variant` but with exhaustive pattern matching, no `std::get` exceptions, and no `std::visit` boilerplate - - The size of the `enum` is that of the largest possible type. The individual variants are not related to one another and can have completely different types - - `enum` types are one of the most powerful features of the language — they replace entire class hierarchies in C++ (more on this in the Case Studies) +- Перечисления в Rust — это размеченные объединения, то есть суммовые типы из нескольких возможных различных типов с меткой, которая определяет конкретный вариант + - Для разработчиков на C: перечисления в Rust могут содержать данные (размеченные объединения, сделанные правильно — компилятор отслеживает, какой вариант активен) + - Для разработчиков на C++: перечисления Rust похожи на `std::variant`, но с исчерпывающим сопоставлением с образцом, без исключений `std::get` и без шаблонного кода `std::visit` + - Размер `enum` равен размеру самого большого возможного варианта. Отдельные варианты не связаны друг с другом и могут иметь совершенно разные типы + - Типы `enum` — одна из самых мощных возможностей языка: они заменяют целые иерархии классов в C++ (подробнее об этом в разборах кейсов) ```rust fn main() { enum Numbers { @@ -17,38 +17,38 @@ fn main() { } let a = Numbers::Zero; let b = Numbers::SmallNumber(42); - let c : Numbers = a; // Ok -- the type of a is Numbers - let d : Numbers = b; // Ok -- the type of b is Numbers + let c : Numbers = a; // Ок — тип a это Numbers + let d : Numbers = b; // Ок — тип b это Numbers } ``` ---- -# Rust match statement -- The Rust ```match``` is the equivalent of the C "switch" on steroids - - ```match``` can be used for pattern matching on simple data types, ```struct```, ```enum``` - - The ```match``` statement must be exhaustive, i.e., they must cover all possible cases for a given ```type```. The ```_``` can be used a wildcard for the "all else" case - - ```match``` can yield a value, but all arms (```=>```) must return a value of the same type +# Оператор match в Rust +- ```match``` в Rust — это аналог C-шного "switch", но на стероидах + - ```match``` можно использовать для сопоставления с образцом простых типов данных, ```struct```, ```enum``` + - Оператор ```match``` должен быть исчерпывающим, то есть покрывать все возможные случаи для данного ```типа```. Символ ```_``` используется как подстановочный шаблон для случая «все остальные» + - ```match``` может возвращать значение, но все ветки (```=>```) должны возвращать значение одного и того же типа ```rust fn main() { let x = 42; - // In this case, the _ covers all numbers except the ones explicitly listed + // В этом случае _ покрывает все числа, кроме явно перечисленных let is_secret_of_life = match x { - 42 => true, // return type is boolean value - _ => false, // return type boolean value - // This won't compile because return type isn't boolean + 42 => true, // Возвращаемый тип — булево значение + _ => false, // Возвращаемый тип — булево значение + // Это не скомпилируется, потому что возвращаемый тип не булев // _ => 0 }; println!("{is_secret_of_life}"); } ``` -# Rust match statement -- ```match``` supports ranges, boolean filters, and ```if``` guard statements +# Оператор match в Rust +- ```match``` поддерживает диапазоны, булевы фильтры и охранные условия ```if``` ```rust fn main() { let x = 42; match x { - // Note that the =41 ensures the inclusive range + // Обратите внимание: =41 обеспечивает включение верхней границы диапазона 0..=41 => println!("Less than the secret of life"), 42 => println!("Secret of life"), _ => println!("More than the secret of life"), @@ -57,15 +57,15 @@ fn main() { match y { 100 if x == 43 => println!("y is 100% not secret of life"), 100 if x == 42 => println!("y is 100% secret of life"), - _ => (), // Do nothing + _ => (), // Ничего не делать } } ``` -# Rust match statement -- ```match``` and ```enums``` are often combined together - - The match statement can "bind" the contained value to a variable. Use ```_``` if the value is a don't care - - The ```matches!``` macro can be used to match to specific variant +# Оператор match в Rust +- ```match``` и ```enum``` часто используются вместе + - Оператор match может «связать» содержащееся значение с переменной. Используйте ```_```, если значение не важно + - Макрос ```matches!``` можно использовать для проверки на конкретный вариант ```rust fn main() { enum Numbers { @@ -81,15 +81,15 @@ fn main() { Numbers::BiggerNumber(_) | Numbers::EvenBiggerNumber(_) => println!("Some BiggerNumber or EvenBiggerNumber"), } - // Boolean test for specific variants + // Булева проверка на конкретные варианты if matches!(b, Numbers::Zero | Numbers::SmallNumber(_)) { println!("Matched Zero or small number"); } } ``` -# Rust match statement -- ```match``` can also perform matches using destructuring and slices +# Оператор match в Rust +- ```match``` также может выполнять сопоставление с помощью деструктуризации и срезов ```rust fn main() { struct Foo { @@ -98,52 +98,52 @@ fn main() { } let f = Foo {x: (42, true), y: 100}; match f { - // Capture the value of x into a variable called tuple + // Захватываем значение x в переменную tuple Foo{y: 100, x : tuple} => println!("Matched x: {tuple:?}"), _ => () } let a = [40, 41, 42]; match a { - // Last element of slice must be 42. @ is used to bind the match + // Последний элемент среза должен быть 42. @ используется для связывания с образцом [rest @ .., 42] => println!("{rest:?}"), - // First element of the slice must be 42. @ is used to bind the match + // Первый элемент среза должен быть 42. @ используется для связывания с образцом [42, rest @ ..] => println!("{rest:?}"), _ => (), } } ``` -# Exercise: Implement add and subtract using match and enum +# Упражнение: сложение и вычитание с помощью match и enum -🟢 **Starter** +🟢 **Начальный уровень** -- Write a function that implements arithmetic operations on unsigned 64-bit numbers -- **Step 1**: Define an enum for operations: +- Напишите функцию, которая реализует арифметические операции над беззнаковыми 64-битными числами +- **Шаг 1**: определите перечисление для операций: ```rust enum Operation { Add(u64, u64), Subtract(u64, u64), } ``` -- **Step 2**: Define a result enum: +- **Шаг 2**: определите перечисление результата: ```rust enum CalcResult { - Ok(u64), // Successful result - Invalid(String), // Error message for invalid operations + Ok(u64), // Успешный результат + Invalid(String), // Сообщение об ошибке для недопустимых операций } ``` -- **Step 3**: Implement `calculate(op: Operation) -> CalcResult` - - For Add: return Ok(sum) - - For Subtract: return Ok(difference) if first >= second, otherwise Invalid("Underflow") -- **Hint**: Use pattern matching in your function: +- **Шаг 3**: реализуйте `calculate(op: Operation) -> CalcResult` + - Для Add: вернуть Ok(sum) + - Для Subtract: вернуть Ok(difference), если первое число >= второго, иначе Invalid("Underflow") +- **Подсказка**: используйте сопоставление с образцом в функции: ```rust match op { - Operation::Add(a, b) => { /* your code */ }, - Operation::Subtract(a, b) => { /* your code */ }, + Operation::Add(a, b) => { /* ваш код */ }, + Operation::Subtract(a, b) => { /* ваш код */ }, } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust enum Operation { @@ -179,18 +179,18 @@ fn main() { CalcResult::Invalid(msg) => println!("Error: {msg}"), } } -// Output: +// Вывод: // 10 + 20 = 30 // Error: Underflow ``` </details> -# Rust associated methods -- ```impl``` can define methods associated for types like ```struct```, ```enum```, etc - - The methods may optionally take ```self``` as a parameter. ```self``` is conceptually similar to passing a pointer to the struct as the first parameter in C, or ```this``` in C++ - - The reference to ```self``` can be immutable (default: ```&self```), mutable (```&mut self```), or ```self``` (transferring ownership) - - The ```Self``` keyword can be used a shortcut to imply the type +# Ассоциированные методы в Rust +- ```impl``` может определять методы, ассоциированные с типами вроде ```struct```, ```enum``` и т. д. + - Методы могут необязательно принимать ```self``` как параметр. ```self``` концептуально похож на передачу указателя на структуру первым параметром в C или на ```this``` в C++ + - Ссылка на ```self``` может быть неизменяемой (по умолчанию: ```&self```), изменяемой (```&mut self```) или ```self``` (передача владения) + - Ключевое слово ```Self``` можно использовать как сокращение, подразумевающее тип ```rust struct Point {x: u32, y: u32} impl Point { @@ -207,14 +207,14 @@ fn main() { } ``` -# Exercise: Point add and transform +# Упражнение: сложение и преобразование точки -🟡 **Intermediate** — requires understanding move vs borrow from method signatures -- Implement the following associated methods for ```Point``` - - ```add()``` will take another ```Point``` and will increment the x and y values in place (hint: use ```&mut self```) - - ```transform()``` will consume an existing ```Point``` (hint: use ```self```) and return a new ```Point``` by squaring the x and y +🟡 **Средний уровень** — требует понимания разницы между перемещением и заимствованием по сигнатурам методов +- Реализуйте следующие ассоциированные методы для ```Point``` + - ```add()``` принимает другую ```Point``` и увеличивает значения x и y на месте (подсказка: используйте ```&mut self```) + - ```transform()``` поглощает существующую ```Point``` (подсказка: используйте ```self```) и возвращает новую ```Point```, возводя x и y в квадрат -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust struct Point { x: u32, y: u32 } @@ -239,11 +239,10 @@ fn main() { println!("After add: x={}, y={}", p1.x, p1.y); // x=12, y=23 let p3 = p1.transform(); println!("After transform: x={}, y={}", p3.x, p3.y); // x=144, y=529 - // p1 is no longer accessible — transform() consumed it + // p1 больше недоступен — transform() его поглотил } ``` </details> ---- - diff --git a/c-cpp-book/src/ch07-1-lifetimes-and-borrowing-deep-dive.md b/c-cpp-book/src/ch07-1-lifetimes-and-borrowing-deep-dive.md index a8f44e8..a85cd3e 100644 --- a/c-cpp-book/src/ch07-1-lifetimes-and-borrowing-deep-dive.md +++ b/c-cpp-book/src/ch07-1-lifetimes-and-borrowing-deep-dive.md @@ -1,9 +1,9 @@ -# Rust lifetime and borrowing +# Времена жизни и заимствование в Rust -> **What you'll learn:** How Rust's lifetime system ensures references never dangle — from implicit lifetimes through explicit annotations to the three elision rules that make most code annotation-free. Understanding lifetimes here is essential before moving on to smart pointers in the next section. +> **Что вы узнаете:** как система времён жизни Rust гарантирует, что ссылки никогда не станут висячими — от неявных времён жизни через явные аннотации до трёх правил элизии, благодаря которым большинству кода аннотации не нужны. Понимание времён жизни здесь необходимо, прежде чем переходить к умным указателям в следующем разделе. -- Rust enforces a single mutable reference and any number of immutable references - - The lifetime of any reference must be at least as long as the original owning lifetime. These are implicit lifetimes and are inferred by the compiler (see https://doc.rust-lang.org/nomicon/lifetime-elision.html) +- Rust требует единственной изменяемой ссылки и любого количества неизменяемых ссылок + - Время жизни любой ссылки должно быть не меньше, чем время жизни исходного владельца. Это неявные времена жизни, которые выводит компилятор (см. https://doc.rust-lang.org/nomicon/lifetime-elision.html) ```rust fn borrow_mut(x: &mut u32) { *x = 43; @@ -12,34 +12,34 @@ fn main() { let mut x = 42; let y = &mut x; borrow_mut(y); - let _z = &x; // Permitted because the compiler knows y isn't subsequently used - //println!("{y}"); // Will not compile if this is uncommented - borrow_mut(&mut x); // Permitted because _z isn't used - let z = &x; // Ok -- mutable borrow of x ended after borrow_mut() returned + let _z = &x; // Допустимо, потому что компилятор знает, что y далее не используется + //println!("{y}"); // Не скомпилируется, если раскомментировать + borrow_mut(&mut x); // Допустимо, потому что _z не используется + let z = &x; // Ок — изменяемое заимствование x закончилось после возврата из borrow_mut() println!("{z}"); } ``` -# Rust lifetime annotations -- Explicit lifetime annotations are needed when dealing with multiple lifetimes - - Lifetimes are denoted with `'` and can be any identifier (`'a`, `'b`, `'static`, etc.) - - The compiler needs help when it can't figure out how long references should live -- **Common scenario**: Function returns a reference, but which input does it come from? +# Аннотации времён жизни в Rust +- Явные аннотации времён жизни нужны, когда имеют дело с несколькими временами жизни + - Времена жизни обозначаются символом `'`, и могут быть любым идентификатором (`'a`, `'b`, `'static` и т. д.) + - Компилятору нужна подсказка, когда он не может понять, сколько должны жить ссылки +- **Типичный сценарий**: функция возвращает ссылку, но из какого входного параметра она взята? ```rust #[derive(Debug)] struct Point {x: u32, y: u32} -// Without lifetime annotation, this won't compile: +// Без аннотации времени жизни это не скомпилируется: // fn left_or_right(pick_left: bool, left: &Point, right: &Point) -> &Point -// With lifetime annotation - all references share the same lifetime 'a +// С аннотацией времени жизни — все ссылки разделяют одно время жизни 'a fn left_or_right<'a>(pick_left: bool, left: &'a Point, right: &'a Point) -> &'a Point { if pick_left { left } else { right } } -// More complex: different lifetimes for inputs +// Сложнее: разные времена жизни для входных параметров fn get_x_coordinate<'a, 'b>(p1: &'a Point, _p2: &'b Point) -> &'a u32 { - &p1.x // Return value lifetime tied to p1, not p2 + &p1.x // Время жизни возвращаемого значения привязано к p1, а не к p2 } fn main() { @@ -48,16 +48,16 @@ fn main() { { let p2 = Point {x: 42, y: 50}; result = left_or_right(true, &p1, &p2); - // This works because we use result before p2 goes out of scope + // Это работает, потому что мы используем result до выхода p2 из области видимости println!("Selected: {result:?}"); } - // This would NOT work - result references p2 which is now gone: + // Это НЕ сработает — result ссылается на p2, которого уже нет: // println!("After scope: {result:?}"); } ``` -# Rust lifetime annotations -- Lifetime annotations are also needed for references in data structures +# Аннотации времён жизни в Rust +- Аннотации времён жизни также нужны для ссылок внутри структур данных ```rust use std::collections::HashMap; #[derive(Debug)] @@ -73,30 +73,30 @@ fn main() { m.map.insert(1, &p1); { let p3 = Point{x: 60, y:70}; - //m.map.insert(3, &p3); // Will not compile - // p3 is dropped here, but m will outlive + //m.map.insert(3, &p3); // Не скомпилируется + // p3 уничтожается здесь, но m переживёт её } for (k, v) in m.map { println!("{v:?}"); } - // m is dropped here - // p1 and p are dropped here in that order + // m уничтожается здесь + // p1 и p уничтожаются здесь, в этом порядке } ``` -# Exercise: First word with lifetimes +# Упражнение: первое слово с временами жизни -🟢 **Starter** — practice lifetime elision in action +🟢 **Начальный уровень** — практика элизии времён жизни в действии -Write a function `fn first_word(s: &str) -> &str` that returns the first whitespace-delimited word from a string. Think about why this compiles without explicit lifetime annotations (hint: elision rule #1 and #2). +Напишите функцию `fn first_word(s: &str) -> &str`, которая возвращает первое слово строки, разделённое пробелами. Подумайте, почему это компилируется без явных аннотаций времён жизни (подсказка: правила элизии №1 и №2). -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn first_word(s: &str) -> &str { - // The compiler applies elision rules: - // Rule 1: input &str gets lifetime 'a → fn first_word(s: &'a str) -> &str - // Rule 2: single input lifetime → output gets same → fn first_word(s: &'a str) -> &'a str + // Компилятор применяет правила элизии: + // Правило 1: входной &str получает время жизни 'a → fn first_word(s: &'a str) -> &str + // Правило 2: одно входное время жизни → выход получает то же → fn first_word(s: &'a str) -> &'a str match s.find(' ') { Some(pos) => &s[..pos], None => s, @@ -115,14 +115,14 @@ fn main() { </details> -# Exercise: Slice storage with lifetimes +# Упражнение: хранение срезов с временами жизни -🟡 **Intermediate** — your first encounter with lifetime annotations -- Create a structure that stores references to the slice of a ```&str``` - - Create a long ```&str``` and store references slices from it inside the structure - - Write a function that accepts the structure and returns the contained slice +🟡 **Средний уровень** — ваше первое знакомство с аннотациями времён жизни +- Создайте структуру, которая хранит ссылки на срез `&str` + - Создайте длинный `&str` и сохраните в структуре ссылки на его срезы + - Напишите функцию, которая принимает структуру и возвращает содержащийся в ней срез ```rust -// TODO: Create a structure to store a reference to a slice +// TODO: Создайте структуру для хранения ссылки на срез struct SliceStore { } @@ -135,7 +135,7 @@ fn main() { } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust struct SliceStore<'a> { @@ -159,7 +159,7 @@ fn main() { println!("store1: {}", store1.get_slice()); println!("store2: {}", store2.get_slice()); } -// Output: +// Вывод: // store1: This // store2: is ``` @@ -168,154 +168,148 @@ fn main() { --- -## Lifetime Elision Rules Deep Dive +## Углублённый разбор правил элизии времён жизни -C programmers often ask: "If lifetimes are so important, why don't most Rust functions -have `'a` annotations?" The answer is **lifetime elision** — the compiler applies three -deterministic rules to infer lifetimes automatically. +Разработчики на C часто спрашивают: «Если времена жизни так важны, почему у большинства функций в Rust нет аннотаций `'a`?» Ответ — **элизия времён жизни**: компилятор применяет три детерминированных правила, чтобы выводить времена жизни автоматически. -### The Three Elision Rules +### Три правила элизии -The Rust compiler applies these rules **in order** to function signatures. If all output -lifetimes are determined after applying the rules, no annotations are needed. +Компилятор Rust применяет эти правила **по порядку** к сигнатурам функций. Если после применения правил определены все времена жизни выходных значений, аннотации не нужны. ```mermaid flowchart TD - A["Function signature<br/>with references"] --> R1 - R1["Rule 1: Each input<br/>reference gets its own<br/>lifetime<br/><br/>fn f(&str, &str)<br/>→ fn f<'a,'b>(&'a str,<br/>&'b str)"] + A["Сигнатура функции<br/>со ссылками"] --> R1 + R1["Правило 1: каждая входная<br/>ссылка получает собственное<br/>время жизни<br/><br/>fn f(&str, &str)<br/>→ fn f<'a,'b>(&'a str,<br/>&'b str)"] R1 --> R2 - R2["Rule 2: If exactly ONE<br/>input lifetime, assign it<br/>to ALL outputs<br/><br/>fn f(&str) → &str<br/>→ fn f<'a>(&'a str)<br/>→ &'a str"] + R2["Правило 2: если есть ровно ОДНО<br/>входное время жизни, назначаем его<br/>ВСЕМ выходным<br/><br/>fn f(&str) → &str<br/>→ fn f<'a>(&'a str)<br/>→ &'a str"] R2 --> R3 - R3["Rule 3: If one input is<br/>&self or &mut self,<br/>assign its lifetime to<br/>ALL outputs<br/><br/>fn f(&self, &str) → &str<br/>→ fn f<'a>(&'a self, &str)<br/>→ &'a str"] - R3 --> CHECK{{"All output<br/>lifetimes<br/>determined?"}} - CHECK -->|Yes| OK["✅ No annotations<br/>needed"] - CHECK -->|No| ERR["❌ Compile error:<br/>must annotate<br/>manually"] + R3["Правило 3: если один из входов —<br/>&self или &mut self, назначаем его<br/>время жизни ВСЕМ выходным<br/><br/>fn f(&self, &str) → &str<br/>→ fn f<'a>(&'a self, &str)<br/>→ &'a str"] + R3 --> CHECK{{"Определены ли<br/>времена жизни всех<br/>выходных значений?"}} + CHECK -->|Да| OK["✅ Аннотации<br/>не нужны"] + CHECK -->|Нет| ERR["❌ Ошибка компиляции:<br/>нужно аннотировать<br/>вручную"] style OK fill:#91e5a3,color:#000 style ERR fill:#ff6b6b,color:#000 ``` -### Rule-by-Rule Examples +### Разбор правил на примерах -**Rule 1** — each input reference gets its own lifetime parameter: +**Правило 1** — каждая входная ссылка получает собственный параметр времени жизни: ```rust -// What you write: +// Что вы пишете: fn first_word(s: &str) -> &str { ... } -// What the compiler sees after Rule 1: +// Что видит компилятор после правила 1: fn first_word<'a>(s: &'a str) -> &str { ... } -// Only one input lifetime → Rule 2 applies +// Есть только одно входное время жизни → применяется правило 2 ``` -**Rule 2** — single input lifetime propagates to all outputs: +**Правило 2** — единственное входное время жизни распространяется на все выходные: ```rust -// After Rule 2: +// После правила 2: fn first_word<'a>(s: &'a str) -> &'a str { ... } -// ✅ All output lifetimes determined — no annotation needed! +// ✅ Все выходные времена жизни определены — аннотация не нужна! ``` -**Rule 3** — `&self` lifetime propagates to outputs: +**Правило 3** — время жизни `&self` распространяется на выходные значения: ```rust -// What you write: +// Что вы пишете: impl SliceStore<'_> { fn get_slice(&self) -> &str { self.slice } } -// What the compiler sees after Rules 1 + 3: +// Что видит компилятор после правил 1 и 3: impl SliceStore<'_> { fn get_slice<'a>(&'a self) -> &'a str { self.slice } } -// ✅ No annotation needed — &self lifetime used for output +// ✅ Аннотация не нужна — время жизни &self используется для результата ``` -**When elision fails** — you must annotate: +**Когда элизия не срабатывает** — нужно аннотировать: ```rust -// Two input references, no &self → Rules 2 and 3 don't apply -// fn longest(a: &str, b: &str) -> &str ← WON'T COMPILE +// Два входных параметра-ссылки, нет &self → правила 2 и 3 не применяются +// fn longest(a: &str, b: &str) -> &str ← НЕ СКОМПИЛИРУЕТСЯ -// Fix: tell the compiler which input the output borrows from +// Исправление: сообщаем компилятору, из какого входа заимствует результат fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b } } ``` -### C Programmer Mental Model +### Модель мышления программиста на C -In C, every pointer is independent — the programmer mentally tracks which allocation -each pointer refers to, and the compiler trusts you completely. In Rust, lifetimes make -this tracking **explicit and compiler-verified**: +В C каждый указатель независим — программист мысленно отслеживает, на какое выделение памяти ссылается каждый указатель, а компилятор полностью доверяет вам. В Rust времена жизни делают это отслеживание **явным и проверяемым компилятором**: -| C | Rust | What happens | +| C | Rust | Что происходит | |---|------|-------------| -| `char* get_name(struct User* u)` | `fn get_name(&self) -> &str` | Rule 3 elides: output borrows from `self` | -| `char* concat(char* a, char* b)` | `fn concat<'a>(a: &'a str, b: &'a str) -> &'a str` | Must annotate — two inputs | -| `void process(char* in, char* out)` | `fn process(input: &str, output: &mut String)` | No output reference — no lifetime needed | -| `char* buf; /* who owns this? */` | Compile error if lifetime is wrong | Compiler catches dangling pointers | +| `char* get_name(struct User* u)` | `fn get_name(&self) -> &str` | Правило 3 выводит: результат заимствован у `self` | +| `char* concat(char* a, char* b)` | `fn concat<'a>(a: &'a str, b: &'a str) -> &'a str` | Нужна аннотация — два входа | +| `void process(char* in, char* out)` | `fn process(input: &str, output: &mut String)` | Выходной ссылки нет — время жизни не нужно | +| `char* buf; /* кто владеет этим? */` | Ошибка компиляции, если время жизни неверно | Компилятор ловит висячие указатели | -### The `'static` Lifetime +### Время жизни `'static` -`'static` means the reference is valid for the **entire program duration**. It's the -Rust equivalent of a C global or string literal: +`'static` означает, что ссылка валидна на **протяжении всего времени работы программы**. Это аналог глобальной переменной или строкового литерала в C: ```rust -// String literals are always 'static — they live in the binary's read-only section -let s: &'static str = "hello"; // Same as: static const char* s = "hello"; in C +// Строковые литералы всегда 'static — они находятся в read-only секции бинарника +let s: &'static str = "hello"; // То же, что static const char* s = "hello"; в C -// Constants are also 'static +// Константы тоже 'static static GREETING: &str = "hello"; -// Common in trait bounds for thread spawning: +// Часто встречается в ограничениях трейтов при запуске потоков: fn spawn<F: FnOnce() + Send + 'static>(f: F) { /* ... */ } -// 'static here means: "the closure must not borrow any local variables" -// (either move them in, or use only 'static data) +// 'static здесь означает: «замыкание не должно заимствовать никакие локальные переменные» +// (либо перемещать их внутрь, либо использовать только данные 'static) ``` -### Exercise: Predict the Elision +### Упражнение: предскажите элизию -🟡 **Intermediate** +🟡 **Средний уровень** -For each function signature below, predict whether the compiler can elide lifetimes. -If not, add the necessary annotations: +Для каждой сигнатуры функции ниже предскажите, может ли компилятор вывести времена жизни. +Если нет, добавьте необходимые аннотации: ```rust -// 1. Can the compiler elide? +// 1. Может ли компилятор вывести? fn trim_prefix(s: &str) -> &str { &s[1..] } -// 2. Can the compiler elide? +// 2. Может ли компилятор вывести? fn pick(flag: bool, a: &str, b: &str) -> &str { if flag { a } else { b } } -// 3. Can the compiler elide? +// 3. Может ли компилятор вывести? struct Parser { data: String } impl Parser { fn next_token(&self) -> &str { &self.data[..5] } } -// 4. Can the compiler elide? +// 4. Может ли компилятор вывести? fn split_at(s: &str, pos: usize) -> (&str, &str) { (&s[..pos], &s[pos..]) } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust,ignore -// 1. YES — Rule 1 gives 'a to s, Rule 2 propagates to output +// 1. ДА — правило 1 даёт 'a для s, правило 2 распространяет его на результат fn trim_prefix(s: &str) -> &str { &s[1..] } -// 2. NO — Two input references, no &self. Must annotate: +// 2. НЕТ — два входных параметра-ссылки, нет &self. Нужна аннотация: fn pick<'a>(flag: bool, a: &'a str, b: &'a str) -> &'a str { if flag { a } else { b } } -// 3. YES — Rule 1 gives 'a to &self, Rule 3 propagates to output +// 3. ДА — правило 1 даёт 'a для &self, правило 3 распространяет его на результат impl Parser { fn next_token(&self) -> &str { &self.data[..5] } } -// 4. YES — Rule 1 gives 'a to s (only one input reference), -// Rule 2 propagates to BOTH outputs. Both slices borrow from s. +// 4. ДА — правило 1 даёт 'a для s (единственная входная ссылка), +// правило 2 распространяет его на ОБА выхода. Оба среза заимствованы у s. fn split_at(s: &str, pos: usize) -> (&str, &str) { (&s[..pos], &s[pos..]) } diff --git a/c-cpp-book/src/ch07-2-smart-pointers-and-interior-mutability.md b/c-cpp-book/src/ch07-2-smart-pointers-and-interior-mutability.md index 2f5ec94..69266c5 100644 --- a/c-cpp-book/src/ch07-2-smart-pointers-and-interior-mutability.md +++ b/c-cpp-book/src/ch07-2-smart-pointers-and-interior-mutability.md @@ -1,41 +1,41 @@ -# Rust `Box<T>` +# `Box<T>` в Rust -> **What you'll learn:** Rust's smart pointer types — `Box<T>` for heap allocation, `Rc<T>` for shared ownership, and `Cell<T>`/`RefCell<T>` for interior mutability. These build on the ownership and lifetime concepts from the previous sections. You'll also see a brief introduction to `Weak<T>` for breaking reference cycles. +> **Что вы узнаете:** умные указатели Rust — `Box<T>` для выделения памяти в куче, `Rc<T>` для совместного владения и `Cell<T>`/`RefCell<T>` для внутренней изменяемости. Они опираются на понятия владения и времён жизни из предыдущих разделов. Вы также кратко познакомитесь с `Weak<T>` для разрыва циклов ссылок. -**Why `Box<T>`?** In C, you use `malloc`/`free` for heap allocation. In C++, `std::unique_ptr<T>` wraps `new`/`delete`. Rust's `Box<T>` is the equivalent — a heap-allocated, single-owner pointer that is automatically freed when it goes out of scope. Unlike `malloc`, there's no matching `free` to forget. Unlike `unique_ptr`, there's no use-after-move — the compiler prevents it entirely. +**Зачем `Box<T>`?** В C для выделения памяти в куче используют `malloc`/`free`. В C++ `std::unique_ptr<T>` оборачивает `new`/`delete`. `Box<T>` в Rust — это аналог: указатель в куче с единственным владельцем, который автоматически освобождается при выходе из области видимости. В отличие от `malloc`, нет парного `free`, который можно забыть. В отличие от `unique_ptr`, нет использования после перемещения — компилятор полностью это предотвращает. -**When to use `Box` vs stack allocation:** -- The contained type is large and you don't want to copy it on the stack -- You need a recursive type (e.g., a linked list node that contains itself) -- You need trait objects (`Box<dyn Trait>`) +**Когда использовать `Box`, а когда выделение на стеке:** +- Содержащийся тип большой, и вы не хотите копировать его на стеке +- Нужен рекурсивный тип (например, узел связного списка, который содержит сам себя) +- Нужны трейт-объекты (`Box<dyn Trait>`) -- ```Box<T>``` can be use to create a pointer to a heap allocated type. The pointer is always a fixed size regardless of the type of ```<T>``` +- ```Box<T>``` можно использовать, чтобы создать указатель на тип, размещённый в куче. Размер указателя всегда фиксирован, независимо от типа ```<T>``` ```rust fn main() { - // Creates a pointer to an integer (with value 42) created on the heap + // Создаём указатель на целое число (со значением 42), размещённое в куче let f = Box::new(42); println!("{} {}", *f, f); - // Cloning a box creates a new heap allocation + // Клонирование box создаёт новое выделение в куче let mut g = f.clone(); *g = 43; println!("{f} {g}"); - // g and f go out of scope here and are automatically deallocated + // g и f выходят из области видимости здесь и автоматически освобождаются } ``` ```mermaid graph LR - subgraph "Stack" + subgraph "Стек" F["f: Box<i32>"] G["g: Box<i32>"] end - subgraph "Heap" + subgraph "Куча" HF["42"] HG["43"] end - F -->|"owns"| HF - G -->|"owns (cloned)"| HG + F -->|"владеет"| HF + G -->|"владеет (клон)"| HG style F fill:#51cf66,color:#000,stroke:#333 style G fill:#51cf66,color:#000,stroke:#333 @@ -43,79 +43,79 @@ graph LR style HG fill:#91e5a3,color:#000,stroke:#333 ``` -## Ownership and Borrowing Visualization +## Визуализация владения и заимствования -### C/C++ vs Rust: Pointer and Ownership Management +### C/C++ и Rust: управление указателями и владением ```c -// C - Manual memory management, potential issues +// C — ручное управление памятью, потенциальные проблемы void c_pointer_problems() { int* ptr1 = malloc(sizeof(int)); *ptr1 = 42; - int* ptr2 = ptr1; // Both point to same memory - int* ptr3 = ptr1; // Three pointers to same memory + int* ptr2 = ptr1; // Оба указывают на одну и ту же память + int* ptr3 = ptr1; // Три указателя на одну и ту же память - free(ptr1); // Frees the memory + free(ptr1); // Освобождает память - *ptr2 = 43; // Use after free - undefined behavior! - *ptr3 = 44; // Use after free - undefined behavior! + *ptr2 = 43; // Использование после освобождения — неопределённое поведение! + *ptr3 = 44; // Использование после освобождения — неопределённое поведение! } ``` -> **For C++ developers:** Smart pointers help, but don't prevent all issues: +> **Для программистов C++:** умные указатели помогают, но не предотвращают все проблемы: > > ```cpp -> // C++ - Smart pointers help, but don't prevent all issues +> // C++ — умные указатели помогают, но не предотвращают все проблемы > void cpp_pointer_issues() { > auto ptr1 = std::make_unique<int>(42); > -> // auto ptr2 = ptr1; // Compile error: unique_ptr not copyable -> auto ptr2 = std::move(ptr1); // OK: ownership transferred +> // auto ptr2 = ptr1; // Ошибка компиляции: unique_ptr нельзя копировать +> auto ptr2 = std::move(ptr1); // Ок: владение передано > -> // But C++ still allows use-after-move: -> // std::cout << *ptr1; // Compiles! But undefined behavior! +> // Но C++ всё равно допускает использование после перемещения: +> // std::cout << *ptr1; // Компилируется! Но это неопределённое поведение! > -> // shared_ptr aliasing: +> // Псевдонимы shared_ptr: > auto shared1 = std::make_shared<int>(42); -> auto shared2 = shared1; // Both own the data -> // Who "really" owns it? Neither. Ref count overhead everywhere. +> auto shared2 = shared1; // Оба владеют данными +> // Кто «на самом деле» владеет? Никто. Накладные расходы на счётчик ссылок везде. > } > ``` ```rust -// Rust - Ownership system prevents these issues +// Rust — система владения предотвращает эти проблемы fn rust_ownership_safety() { - let data = Box::new(42); // data owns the heap allocation + let data = Box::new(42); // data владеет выделением в куче - let moved_data = data; // Ownership transferred to moved_data - // data is no longer accessible - compile error if used + let moved_data = data; // Владение передано moved_data + // data больше недоступна — ошибка компиляции при использовании - let borrowed = &moved_data; // Immutable borrow - println!("{}", borrowed); // Safe to use + let borrowed = &moved_data; // Неизменяемое заимствование + println!("{}", borrowed); // Безопасно использовать - // moved_data automatically freed when it goes out of scope + // moved_data автоматически освобождается при выходе из области видимости } ``` ```mermaid graph TD - subgraph "C/C++ Memory Management Issues" - CP1["int* ptr1"] --> CM["Heap Memory<br/>value: 42"] + subgraph "Проблемы управления памятью в C/C++" + CP1["int* ptr1"] --> CM["Память в куче<br/>значение: 42"] CP2["int* ptr2"] --> CM CP3["int* ptr3"] --> CM - CF["free(ptr1)"] --> CM_F["[ERROR] Freed Memory"] - CP2 -.->|"Use after free<br/>Undefined Behavior"| CM_F - CP3 -.->|"Use after free<br/>Undefined Behavior"| CM_F + CF["free(ptr1)"] --> CM_F["[ОШИБКА] Освобождённая память"] + CP2 -.->|"Использование после освобождения<br/>Неопределённое поведение"| CM_F + CP3 -.->|"Использование после освобождения<br/>Неопределённое поведение"| CM_F end - subgraph "Rust Ownership System" - RO1["data: Box<i32>"] --> RM["Heap Memory<br/>value: 42"] - RO1 -.->|"Move ownership"| RO2["moved_data: Box<i32>"] + subgraph "Система владения Rust" + RO1["data: Box<i32>"] --> RM["Память в куче<br/>значение: 42"] + RO1 -.->|"Передача владения"| RO2["moved_data: Box<i32>"] RO2 --> RM - RO1_X["data: [WARNING] MOVED<br/>Cannot access"] - RB["&moved_data<br/>Immutable borrow"] -.->|"Safe reference"| RM - RD["Drop automatically<br/>when out of scope"] --> RM + RO1_X["data: [ВНИМАНИЕ] ПЕРЕМЕЩЕНО<br/>Доступа нет"] + RB["&moved_data<br/>Неизменяемое заимствование"] -.->|"Безопасная ссылка"| RM + RD["Drop автоматически<br/>при выходе из области видимости"] --> RM end style CM_F fill:#ff6b6b,color:#000 @@ -127,23 +127,23 @@ graph TD style RD fill:#91e5a3,color:#000 ``` -### Borrowing Rules Visualization +### Визуализация правил заимствования ```rust fn borrowing_rules_example() { let mut data = vec![1, 2, 3, 4, 5]; - // Multiple immutable borrows - OK + // Несколько неизменяемых заимствований — Ок let ref1 = &data; let ref2 = &data; - println!("{:?} {:?}", ref1, ref2); // Both can be used + println!("{:?} {:?}", ref1, ref2); // Оба можно использовать - // Mutable borrow - exclusive access + // Изменяемое заимствование — эксклюзивный доступ let ref_mut = &mut data; ref_mut.push(6); - // ref1 and ref2 can't be used while ref_mut is active + // ref1 и ref2 нельзя использовать, пока активна ref_mut - // After ref_mut is done, immutable borrows work again + // Когда ref_mut закончена, неизменяемые заимствования снова работают let ref3 = &data; println!("{:?}", ref3); } @@ -151,40 +151,40 @@ fn borrowing_rules_example() { ```mermaid graph TD - subgraph "Rust Borrowing Rules" + subgraph "Правила заимствования в Rust" D["mut data: Vec<i32>"] - subgraph "Phase 1: Multiple Immutable Borrows [OK]" + subgraph "Фаза 1: несколько неизменяемых заимствований [OK]" IR1["&data (ref1)"] IR2["&data (ref2)"] D --> IR1 D --> IR2 - IR1 -.->|"Read-only access"| MEM1["Memory: [1,2,3,4,5]"] - IR2 -.->|"Read-only access"| MEM1 + IR1 -.->|"Доступ только для чтения"| MEM1["Память: [1,2,3,4,5]"] + IR2 -.->|"Доступ только для чтения"| MEM1 end - subgraph "Phase 2: Exclusive Mutable Borrow [OK]" + subgraph "Фаза 2: эксклюзивное изменяемое заимствование [OK]" MR["&mut data (ref_mut)"] D --> MR - MR -.->|"Exclusive read/write"| MEM2["Memory: [1,2,3,4,5,6]"] - BLOCK["[ERROR] Other borrows blocked"] + MR -.->|"Эксклюзивное чтение/запись"| MEM2["Память: [1,2,3,4,5,6]"] + BLOCK["[ОШИБКА] Остальные заимствования заблокированы"] end - subgraph "Phase 3: Immutable Borrows Again [OK]" + subgraph "Фаза 3: снова неизменяемые заимствования [OK]" IR3["&data (ref3)"] D --> IR3 - IR3 -.->|"Read-only access"| MEM3["Memory: [1,2,3,4,5,6]"] + IR3 -.->|"Доступ только для чтения"| MEM3["Память: [1,2,3,4,5,6]"] end end - subgraph "What C/C++ Allows (Dangerous)" + subgraph "Что разрешают C/C++ (опасно)" CP["int* ptr"] CP2["int* ptr2"] CP3["int* ptr3"] - CP --> CMEM["Same Memory"] + CP --> CMEM["Та же память"] CP2 --> CMEM CP3 --> CMEM - RACE["[ERROR] Data races possible<br/>[ERROR] Use after free possible"] + RACE["[ОШИБКА] Возможны гонки данных<br/>[ОШИБКА] Возможно использование после освобождения"] end style MEM1 fill:#91e5a3,color:#000 @@ -197,47 +197,47 @@ graph TD --- -## Interior Mutability: `Cell<T>` and `RefCell<T>` +## Внутренняя изменяемость: `Cell<T>` и `RefCell<T>` -Recall that by default variables are immutable in Rust. Sometimes it's desirable to have most of a type read-only while permitting write access to a single field. +Напомним, что по умолчанию переменные в Rust неизменяемые. Иногда хочется, чтобы большая часть типа была доступна только для чтения, но при этом разрешалась запись в одно поле. ```rust struct Employee { - employee_id : u64, // This must be immutable - on_vacation: bool, // What if we wanted to permit write-access to this field, but make employee_id immutable? + employee_id : u64, // Это поле должно быть неизменяемым + on_vacation: bool, // А что, если мы хотим разрешить запись в это поле, но сделать employee_id неизменяемым? } ``` -- Recall that Rust permits a *single mutable* reference to a variable and any number of *immutable* references — enforced at *compile-time* -- What if we wanted to pass an *immutable* vector of employees, *but* allow the `on_vacation` field to be updated, while ensuring `employee_id` cannot be mutated? +- Напомним, что Rust допускает *одну изменяемую* ссылку на переменную и любое количество *неизменяемых* ссылок — это проверяется на *этапе компиляции* +- Что, если мы хотим передать *неизменяемый* вектор сотрудников, но при этом разрешить обновлять поле `on_vacation`, гарантируя, что `employee_id` нельзя будет изменить? -### `Cell<T>` — interior mutability for Copy types +### `Cell<T>` — внутренняя изменяемость для типов Copy -- `Cell<T>` provides **interior mutability**, i.e., write access to specific elements of references that are otherwise read-only -- Works by copying values in and out (requires `T: Copy` for `.get()`) +- `Cell<T>` обеспечивает **внутреннюю изменяемость**, то есть доступ на запись к отдельным элементам через ссылки, которые в остальном доступны только для чтения +- Работает копированием значений внутрь и наружу (для `.get()` требуется `T: Copy`) -### `RefCell<T>` — interior mutability with runtime borrow checking +### `RefCell<T>` — внутренняя изменяемость с проверкой заимствований во время выполнения -- `RefCell<T>` provides a variation that works with references - - Enforces Rust borrow-checks at **runtime** instead of compile-time - - Allows a single *mutable* borrow, but **panics** if there are any other references outstanding - - Use `.borrow()` for immutable access and `.borrow_mut()` for mutable access +- `RefCell<T>` — вариант, который работает со ссылками + - Применяет проверку заимствований Rust **во время выполнения**, а не на этапе компиляции + - Допускает одно *изменяемое* заимствование, но **вызывает panic**, если есть любые другие активные ссылки + - Используйте `.borrow()` для неизменяемого доступа и `.borrow_mut()` для изменяемого -### When to Choose `Cell` vs `RefCell` +### Когда выбрать `Cell`, а когда `RefCell` -| Criterion | `Cell<T>` | `RefCell<T>` | -|-----------|-----------|-------------| -| Works with | `Copy` types (integers, bools, floats) | Any type (`String`, `Vec`, structs) | -| Access pattern | Copies values in/out (`.get()`, `.set()`) | Borrows in place (`.borrow()`, `.borrow_mut()`) | -| Failure mode | Cannot fail — no runtime checks | **Panics** if you borrow mutably while another borrow is active | -| Overhead | Zero — just copies bytes | Small — tracks borrow state at runtime | -| Use when | You need a mutable flag, counter, or small value inside an immutable struct | You need to mutate a `String`, `Vec`, or complex type inside an immutable struct | +| Критерий | `Cell<T>` | `RefCell<T>` | +|----------|-----------|-------------| +| Работает с | Типами `Copy` (целые, bool, числа с плавающей точкой) | Любыми типами (`String`, `Vec`, структуры) | +| Схема доступа | Копирует значения внутрь/наружу (`.get()`, `.set()`) | Заимствует на месте (`.borrow()`, `.borrow_mut()`) | +| Режим сбоя | Не может упасть — нет проверок во время выполнения | **Panic**, если берёте изменяемое заимствование, пока активно другое | +| Накладные расходы | Нулевые — просто копирование байтов | Небольшие — отслеживание состояния заимствования во время выполнения | +| Использовать, когда | Нужен изменяемый флаг, счётчик или небольшое значение внутри неизменяемой структуры | Нужно изменить `String`, `Vec` или сложный тип внутри неизменяемой структуры | --- -## Shared Ownership: `Rc<T>` +## Совместное владение: `Rc<T>` -`Rc<T>` allows reference-counted shared ownership of *immutable* data. What if we wanted to store the same `Employee` in multiple places without copying? +`Rc<T>` позволяет совместное владение *неизменяемыми* данными с подсчётом ссылок. Что, если мы хотим хранить одного и того же `Employee` в нескольких местах без копирования? ```rust #[derive(Debug)] @@ -249,14 +249,14 @@ fn main() { let mut all_global_employees = Vec::<Employee>::new(); let employee = Employee { employee_id: 42 }; us_employees.push(employee); - // Won't compile — employee was already moved + // Не скомпилируется — employee уже перемещён //all_global_employees.push(employee); } ``` -`Rc<T>` solves the problem by allowing shared *immutable* access: -- The contained type is automatically dereferenced -- The type is dropped when the reference count goes to 0 +`Rc<T>` решает эту проблему, разрешая совместный *неизменяемый* доступ: +- Содержащийся тип автоматически разыменовывается +- Тип уничтожается, когда счётчик ссылок становится 0 ```rust use std::rc::Rc; @@ -269,76 +269,76 @@ fn main() { let employee_rc = Rc::new(employee); us_employees.push(employee_rc.clone()); all_global_employees.push(employee_rc.clone()); - let employee_one = all_global_employees.get(0); // Shared immutable reference + let employee_one = all_global_employees.get(0); // Совместная неизменяемая ссылка for e in us_employees { - println!("{}", e.employee_id); // Shared immutable reference + println!("{}", e.employee_id); // Совместная неизменяемая ссылка } println!("{employee_one:?}"); } ``` -> **For C++ developers: Smart Pointer Mapping** +> **Для программистов C++: соответствие умных указателей** > -> | C++ Smart Pointer | Rust Equivalent | Key Difference | +> | Умный указатель C++ | Эквивалент в Rust | Ключевое отличие | > |---|---|---| -> | `std::unique_ptr<T>` | `Box<T>` | Rust's version is the default — move is language-level, not opt-in | -> | `std::shared_ptr<T>` | `Rc<T>` (single-thread) / `Arc<T>` (multi-thread) | No atomic overhead for `Rc`; use `Arc` only when sharing across threads | -> | `std::weak_ptr<T>` | `Weak<T>` (from `Rc::downgrade()` or `Arc::downgrade()`) | Same purpose: break reference cycles | +> | `std::unique_ptr<T>` | `Box<T>` | Версия Rust используется по умолчанию — перемещение задаётся на уровне языка, а не включается явно | +> | `std::shared_ptr<T>` | `Rc<T>` (один поток) / `Arc<T>` (несколько потоков) | У `Rc` нет накладных расходов на атомарные операции; `Arc` используйте, только когда делитесь данными между потоками | +> | `std::weak_ptr<T>` | `Weak<T>` (из `Rc::downgrade()` или `Arc::downgrade()`) | Та же цель: разрыв циклов ссылок | > -> **Key distinction**: In C++, you *choose* to use smart pointers. In Rust, owned values (`T`) and borrowing (`&T`) cover most use cases — reach for `Box`/`Rc`/`Arc` only when you need heap allocation or shared ownership. +> **Ключевое различие**: в C++ вы *выбираете* использовать умные указатели. В Rust владеющие значения (`T`) и заимствование (`&T`) покрывают большинство случаев — прибегайте к `Box`/`Rc`/`Arc`, только когда нужно выделение в куче или совместное владение. -### Breaking Reference Cycles with `Weak<T>` +### Разрыв циклов ссылок с помощью `Weak<T>` -`Rc<T>` uses reference counting — if two `Rc` values point to each other, neither will ever be dropped (a cycle). `Weak<T>` solves this: +`Rc<T>` использует подсчёт ссылок — если два значения `Rc` указывают друг на друга, ни одно из них никогда не будет уничтожено (цикл). `Weak<T>` решает эту проблему: ```rust use std::rc::{Rc, Weak}; struct Node { value: i32, - parent: Option<Weak<Node>>, // Weak reference — doesn't prevent drop + parent: Option<Weak<Node>>, // Слабая ссылка — не мешает уничтожению } fn main() { let parent = Rc::new(Node { value: 1, parent: None }); let child = Rc::new(Node { value: 2, - parent: Some(Rc::downgrade(&parent)), // Weak ref to parent + parent: Some(Rc::downgrade(&parent)), // Слабая ссылка на родителя }); - // To use a Weak, try to upgrade it — returns Option<Rc<T>> + // Чтобы использовать Weak, попробуйте повысить его — возвращается Option<Rc<T>> if let Some(parent_rc) = child.parent.as_ref().unwrap().upgrade() { println!("Parent value: {}", parent_rc.value); } - println!("Parent strong count: {}", Rc::strong_count(&parent)); // 1, not 2 + println!("Parent strong count: {}", Rc::strong_count(&parent)); // 1, а не 2 } ``` -> `Weak<T>` is covered in more depth in [Avoiding Excessive clone()](ch17-1-avoiding-excessive-clone.md). For now, the key takeaway: **use `Weak` for "back-references" in tree/graph structures to avoid memory leaks.** +> `Weak<T>` подробнее рассматривается в главе [Избегание избыточного clone()](ch17-1-avoiding-excessive-clone.md). Пока главное: **используйте `Weak` для «обратных ссылок» в древовидных и графовых структурах, чтобы избежать утечек памяти.** --- -## Combining `Rc` with Interior Mutability +## Комбинирование `Rc` с внутренней изменяемостью -The real power emerges when you combine `Rc<T>` (shared ownership) with `Cell<T>` or `RefCell<T>` (interior mutability). This lets multiple owners **read and modify** shared data: +Настоящая мощь проявляется, когда вы комбинируете `Rc<T>` (совместное владение) с `Cell<T>` или `RefCell<T>` (внутренняя изменяемость). Это позволяет нескольким владельцам **читать и изменять** общие данные: -| Pattern | Use case | -|---------|----------| -| `Rc<RefCell<T>>` | Shared, mutable data (single-threaded) | -| `Arc<Mutex<T>>` | Shared, mutable data (multi-threaded — see [ch13](ch13-concurrency.md)) | -| `Rc<Cell<T>>` | Shared, mutable Copy types (simple flags, counters) | +| Шаблон | Сценарий использования | +|--------|----------| +| `Rc<RefCell<T>>` | Общие изменяемые данные (один поток) | +| `Arc<Mutex<T>>` | Общие изменяемые данные (несколько потоков — см. [ch13](ch13-concurrency.md)) | +| `Rc<Cell<T>>` | Общие изменяемые типы Copy (простые флаги, счётчики) | --- -# Exercise: Shared ownership and interior mutability +# Упражнение: совместное владение и внутренняя изменяемость -🟡 **Intermediate** +🟡 **Средний уровень** -- **Part 1 (Rc)**: Create an `Employee` struct with `employee_id: u64` and `name: String`. Place it in an `Rc<Employee>` and clone it into two separate `Vec`s (`us_employees` and `global_employees`). Print from both vectors to show they share the same data. -- **Part 2 (Cell)**: Add an `on_vacation: Cell<bool>` field to `Employee`. Pass an immutable `&Employee` reference to a function and toggle `on_vacation` from inside that function — without making the reference mutable. -- **Part 3 (RefCell)**: Replace `name: String` with `name: RefCell<String>` and write a function that appends a suffix to the employee's name through an `&Employee` (immutable reference). +- **Часть 1 (Rc)**: создайте структуру `Employee` с полями `employee_id: u64` и `name: String`. Поместите её в `Rc<Employee>` и клонируйте в два отдельных `Vec` (`us_employees` и `global_employees`). Выведите данные из обоих векторов, чтобы показать, что они разделяют одни и те же данные. +- **Часть 2 (Cell)**: добавьте поле `on_vacation: Cell<bool>` в `Employee`. Передайте неизменяемую ссылку `&Employee` в функцию и переключите `on_vacation` изнутри этой функции — без того, чтобы делать ссылку изменяемой. +- **Часть 3 (RefCell)**: замените `name: String` на `name: RefCell<String>` и напишите функцию, которая добавляет суффикс к имени сотрудника через `&Employee` (неизменяемую ссылку). -**Starter code:** +**Стартовый код:** ```rust use std::cell::{Cell, RefCell}; use std::rc::Rc; @@ -351,20 +351,20 @@ struct Employee { } fn toggle_vacation(emp: &Employee) { - // TODO: Flip on_vacation using Cell::set() + // TODO: Переключите on_vacation с помощью Cell::set() } fn append_title(emp: &Employee, title: &str) { - // TODO: Borrow name mutably via RefCell and push_str the title + // TODO: Заимствуйте name изменяемо через RefCell и добавьте title с помощью push_str } fn main() { - // TODO: Create an employee, wrap in Rc, clone into two Vecs, - // call toggle_vacation and append_title, print results + // TODO: Создайте сотрудника, оберните в Rc, клонируйте в два Vec, + // вызовите toggle_vacation и append_title, выведите результаты } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::cell::{Cell, RefCell}; @@ -397,20 +397,20 @@ fn main() { us_employees.push(Rc::clone(&emp)); global_employees.push(Rc::clone(&emp)); - // Toggle vacation through an immutable reference + // Переключаем отпуск через неизменяемую ссылку toggle_vacation(&emp); println!("On vacation: {}", emp.on_vacation.get()); // true - // Append title through an immutable reference + // Добавляем звание через неизменяемую ссылку append_title(&emp, ", Sr. Engineer"); println!("Name: {}", emp.name.borrow()); // "Alice, Sr. Engineer" - // Both Vecs see the same data (Rc shares ownership) + // Оба Vec видят одни и те же данные (Rc разделяет владение) println!("US: {:?}", us_employees[0].name.borrow()); println!("Global: {:?}", global_employees[0].name.borrow()); println!("Rc strong count: {}", Rc::strong_count(&emp)); } -// Output: +// Вывод: // On vacation: true // Name: Alice, Sr. Engineer // US: "Alice, Sr. Engineer" diff --git a/c-cpp-book/src/ch07-ownership-and-borrowing.md b/c-cpp-book/src/ch07-ownership-and-borrowing.md index f9dfa60..558450a 100644 --- a/c-cpp-book/src/ch07-ownership-and-borrowing.md +++ b/c-cpp-book/src/ch07-ownership-and-borrowing.md @@ -1,53 +1,53 @@ -# Rust memory management +# Управление памятью в Rust -> **What you'll learn:** Rust's ownership system — the single most important concept in the language. After this chapter you'll understand move semantics, borrowing rules, and the `Drop` trait. If you grasp this chapter, the rest of Rust follows naturally. If you're struggling, re-read it — ownership clicks on the second pass for most C/C++ developers. +> **Что вы узнаете:** систему владения Rust — самую важную концепцию языка. После этой главы вы поймёте семантику перемещения, правила заимствования и трейт `Drop`. Если вы усвоите эту главу, остальной Rust будет даваться естественно. Если трудно — перечитайте её: владение для большинства разработчиков C/C++ становится понятным при втором прочтении. -- Memory management in C/C++ is a source of bugs: - - In C: memory is allocated with `malloc()` and freed with `free()`. No checks against dangling pointers, use-after-free, or double-free - - In C++: RAII (Resource Acquisition Is Initialization) and smart pointers help, but `std::move(ptr)` compiles even after the move — use-after-move is UB -- Rust makes RAII **foolproof**: - - Move is **destructive** — the compiler refuses to let you touch the moved-from variable - - No Rule of Five needed (no copy ctor, move ctor, copy assign, move assign, destructor) - - Rust gives complete control of memory allocation, but enforces safety at **compile time** - - This is done by a combination of mechanisms including ownership, borrowing, mutability and lifetimes - - Rust runtime allocations can happen both on the stack and the heap +- Управление памятью в C/C++ — источник ошибок: + - В C память выделяется через `malloc()` и освобождается через `free()`. Нет проверок на висячие указатели, использование после освобождения или двойное освобождение + - В C++ помогают RAII (Resource Acquisition Is Initialization) и умные указатели, но `std::move(ptr)` компилируется и после перемещения — использование после перемещения является неопределённым поведением +- Rust делает RAII **надёжным даже для невнимательных**: + - Перемещение **разрушающее** — компилятор не позволит трогать переменную, из которой переместили значение + - Правило пяти не нужно (нет конструктора копирования, конструктора перемещения, копирующего присваивания, перемещающего присваивания, деструктора) + - Rust даёт полный контроль над выделением памяти, но обеспечивает безопасность на **этапе компиляции** + - Это достигается комбинацией механизмов: владения, заимствования, изменяемости и времён жизни + - Выделения памяти во время выполнения в Rust могут происходить как на стеке, так и в куче -> **For C++ developers — Smart Pointer Mapping:** +> **Для программистов C++ — соответствие умных указателей:** > -> | **C++** | **Rust** | **Safety Improvement** | +> | **C++** | **Rust** | **Улучшение безопасности** | > |---------|----------|----------------------| -> | `std::unique_ptr<T>` | `Box<T>` | No use-after-move possible | -> | `std::shared_ptr<T>` | `Rc<T>` (single-thread) | No reference cycles by default | -> | `std::shared_ptr<T>` (thread-safe) | `Arc<T>` | Explicit thread-safety | -> | `std::weak_ptr<T>` | `Weak<T>` | Must check validity | -> | Raw pointer | `*const T` / `*mut T` | Only in `unsafe` blocks | +> | `std::unique_ptr<T>` | `Box<T>` | Использование после перемещения невозможно | +> | `std::shared_ptr<T>` | `Rc<T>` (один поток) | Циклов ссылок по умолчанию нет | +> | `std::shared_ptr<T>` (потокобезопасный) | `Arc<T>` | Потокобезопасность задаётся явно | +> | `std::weak_ptr<T>` | `Weak<T>` | Валидность нужно проверять | +> | Сырой указатель | `*const T` / `*mut T` | Только в блоках `unsafe` | > -> For C developers: `Box<T>` replaces `malloc`/`free` pairs. `Rc<T>` replaces manual reference counting. Raw pointers exist but are confined to `unsafe` blocks. +> Для разработчиков на C: `Box<T>` заменяет пары `malloc`/`free`. `Rc<T>` заменяет ручной подсчёт ссылок. Сырые указатели существуют, но ограничены блоками `unsafe`. -# Rust ownership, borrowing and lifetimes -- Recall that Rust only permits a single mutable reference to a variable and multiple read-only references - - The initial declaration of the variable establishes ```ownership``` - - Subsequent references ```borrow``` from the original owner. The rule is that the scope of the borrow can never exceed the owning scope. In other words, the ```lifetime``` of a borrow cannot exceed the owning lifetime +# Владение, заимствование и времена жизни в Rust +- Напомним, что Rust допускает только одну изменяемую ссылку на переменную и несколько ссылок только для чтения + - Первоначальное объявление переменной устанавливает ```владение``` + - Последующие ссылки ```заимствуют``` у первоначального владельца. Правило таково: область действия заимствования никогда не может превышать область владельца. Иначе говоря, ```время жизни``` заимствования не может превышать время жизни владельца ```rust fn main() { - let a = 42; // Owner - let b = &a; // First borrow + let a = 42; // Владелец + let b = &a; // Первое заимствование { let aa = 42; - let c = &a; // Second borrow; a is still in scope - // Ok: c goes out of scope here - // aa goes out of scope here + let c = &a; // Второе заимствование; a всё ещё в области видимости + // Ок: c выходит из области видимости здесь + // aa выходит из области видимости здесь } - // let d = &aa; // Will not compile unless aa is moved to outside scope - // b implicitly goes out of scope before a - // a goes out of scope last + // let d = &aa; // Не скомпилируется, если aa не перемещена за пределы области + // b неявно выходит из области видимости раньше a + // a выходит из области видимости последней } ``` -- Rust can pass parameters to methods using several different mechanisms - - By value (copy): Typically types that can be trivially copied (ex: u8, u32, i8, i32) - - By reference: This is the equivalent of passing a pointer to the actual value. This is also commonly known as borrowing, and the reference can be immutable (```&```), or mutable (```&mut```) - - By moving: This transfers "ownership" of the value to the function. The caller can no longer reference the original value +- Rust может передавать параметры в функции несколькими разными способами + - По значению (копия): обычно для типов, которые можно тривиально скопировать (например, u8, u32, i8, i32) + - По ссылке: это аналог передачи указателя на само значение. Также известно как заимствование; ссылка может быть неизменяемой (```&```) или изменяемой (```&mut```) + - Перемещением: это передаёт «владение» значением функции. Вызывающий код больше не может ссылаться на исходное значение ```rust fn foo(x: &u32) { println!("{x}"); @@ -57,89 +57,89 @@ fn bar(x: u32) { } fn main() { let a = 42; - foo(&a); // By reference - bar(a); // By value (copy) + foo(&a); // По ссылке + bar(a); // По значению (копия) } ``` -- Rust prohibits dangling references from methods - - References returned by methods must still be in scope - - Rust will automatically ```drop``` a reference when it goes out of scope. +- Rust запрещает висячие ссылки из методов + - Ссылки, возвращаемые методами, должны оставаться в области видимости + - Rust автоматически ```уничтожает``` ссылку, когда она выходит из области видимости. ```rust fn no_dangling() -> &u32 { - // lifetime of a begins here + // Время жизни a начинается здесь let a = 42; - // Won't compile. lifetime of a ends here + // Не скомпилируется. Время жизни a заканчивается здесь &a } fn ok_reference(a: &u32) -> &u32 { - // Ok because the lifetime of a always exceeds ok_reference() + // Ок, потому что время жизни a всегда превышает время жизни ok_reference() a } fn main() { - let a = 42; // lifetime of a begins here + let a = 42; // Время жизни a начинается здесь let b = ok_reference(&a); - // lifetime of b ends here - // lifetime of a ends here + // Время жизни b заканчивается здесь + // Время жизни a заканчивается здесь } ``` -# Rust move semantics -- By default, Rust assignment transfers ownership +# Семантика перемещения в Rust +- По умолчанию присваивание в Rust передаёт владение ```rust fn main() { - let s = String::from("Rust"); // Allocate a string from the heap - let s1 = s; // Transfer ownership to s1. s is invalid at this point + let s = String::from("Rust"); // Выделяем строку в куче + let s1 = s; // Передаём владение s1. На этой точке s недействительна println!("{s1}"); - // This will not compile + // Это не скомпилируется //println!("{s}"); - // s1 goes out of scope here and the memory is deallocated - // s goes out of scope here, but nothing happens because it doesn't own anything + // s1 выходит из области видимости здесь, и память освобождается + // s выходит из области видимости здесь, но ничего не происходит, потому что она ничем не владеет } ``` ```mermaid graph LR - subgraph "Before: let s1 = s" - S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"] + subgraph "До: let s1 = s" + S["s (стек)<br/>ptr"] -->|"владеет"| H1["Куча: R u s t"] end - subgraph "After: let s1 = s" - S_MOVED["s (stack)<br/>⚠️ MOVED"] -.->|"invalid"| H2["Heap: R u s t"] - S1["s1 (stack)<br/>ptr"] -->|"now owns"| H2 + subgraph "После: let s1 = s" + S_MOVED["s (стек)<br/>⚠️ ПЕРЕМЕЩЕНО"] -.->|"недействительна"| H2["Куча: R u s t"] + S1["s1 (стек)<br/>ptr"] -->|"теперь владеет"| H2 end style S_MOVED fill:#ff6b6b,color:#000,stroke:#333 style S1 fill:#51cf66,color:#000,stroke:#333 style H2 fill:#91e5a3,color:#000,stroke:#333 ``` -*After `let s1 = s`, ownership transfers to `s1`. The heap data stays put — only the stack pointer moves. `s` is now invalid.* +*После `let s1 = s` владение переходит к `s1`. Данные в куче остаются на месте — перемещается только указатель на стеке. `s` теперь недействительна.* ---- -# Rust move semantics and borrowing +# Семантика перемещения и заимствование в Rust ```rust fn foo(s : String) { println!("{s}"); - // The heap memory pointed to by s will be deallocated here + // Память в куче, на которую указывает s, освобождается здесь } fn bar(s : &String) { println!("{s}"); - // Nothing happens -- s is borrowed + // Ничего не происходит — s заимствована } fn main() { - let s = String::from("Rust string move example"); // Allocate a string from the heap - foo(s); // Transfers ownership; s is invalid now - // println!("{s}"); // will not compile + let s = String::from("Rust string move example"); // Выделяем строку в куче + foo(s); // Передаём владение; s теперь недействительна + // println!("{s}"); // не скомпилируется let t = String::from("Rust string borrow example"); - bar(&t); // t continues to hold ownership + bar(&t); // t продолжает владеть значением println!("{t}"); } ``` -# Rust move semantics and ownership -- It is possible to transfer ownership by moving - - It is illegal to reference outstanding references after the move is completed - - Consider borrowing if a move is not desirable +# Семантика перемещения и владение в Rust +- Владение можно передать перемещением + - Нельзя использовать ссылки, которые остались открытыми после завершения перемещения + - Рассмотрите заимствование, если перемещение нежелательно ```rust struct Point { x: u32, @@ -153,29 +153,29 @@ fn borrow_point(p: &Point) { } fn main() { let p = Point {x: 10, y: 20}; - // Try flipping the two lines + // Попробуйте поменять местами эти две строки borrow_point(&p); consume_point(p); } ``` -# Rust Clone -- The ```clone()``` method can be used to copy the original memory. The original reference continues to be valid (the downside is that we have 2x the allocation) +# Clone в Rust +- Метод ```clone()``` можно использовать для копирования исходной памяти. Исходная ссылка продолжает оставаться валидной (минус в том, что выделяется вдвое больше памяти) ```rust fn main() { - let s = String::from("Rust"); // Allocate a string from the heap - let s1 = s.clone(); // Copy the string; creates a new allocation on the heap + let s = String::from("Rust"); // Выделяем строку в куче + let s1 = s.clone(); // Копируем строку; создаётся новое выделение в куче println!("{s1}"); println!("{s}"); - // s1 goes out of scope here and the memory is deallocated - // s goes out of scope here, and the memory is deallocated + // s1 выходит из области видимости здесь, и память освобождается + // s выходит из области видимости здесь, и память освобождается } ``` ```mermaid graph LR - subgraph "After: let s1 = s.clone()" - S["s (stack)<br/>ptr"] -->|"owns"| H1["Heap: R u s t"] - S1["s1 (stack)<br/>ptr"] -->|"owns (copy)"| H2["Heap: R u s t"] + subgraph "После: let s1 = s.clone()" + S["s (стек)<br/>ptr"] -->|"владеет"| H1["Куча: R u s t"] + S1["s1 (стек)<br/>ptr"] -->|"владеет (копия)"| H2["Куча: R u s t"] end style S fill:#51cf66,color:#000,stroke:#333 @@ -183,49 +183,49 @@ graph LR style H1 fill:#91e5a3,color:#000,stroke:#333 style H2 fill:#91e5a3,color:#000,stroke:#333 ``` -*`clone()` creates a **separate** heap allocation. Both `s` and `s1` are valid — each owns its own copy.* +*`clone()` создаёт **отдельное** выделение в куче. Обе `s` и `s1` валидны — каждая владеет своей копией.* -# Rust Copy trait -- Rust implements copy semantics for built-in types using the ```Copy``` trait - - Examples include u8, u32, i8, i32, etc. Copy semantics use "pass by value" - - User defined data types can optionally opt into ```copy``` semantics using the ```derive``` macro with to automatically implement the ```Copy``` trait - - The compiler will allocate space for the copy following a new assignment +# Трейт Copy в Rust +- Rust реализует семантику копирования для встроенных типов через трейт ```Copy``` + - Примеры: u8, u32, i8, i32 и т. д. Семантика копирования использует «передачу по значению» + - Пользовательские типы данных могут при желании получить семантику ```copy```, использовав макрос ```derive``` для автоматической реализации трейта ```Copy``` + - Компилятор выделит место под копию после нового присваивания ```rust -// Try commenting this out to see the change in let p1 = p; below -#[derive(Copy, Clone, Debug)] // We'll discuss this more later +// Попробуйте закомментировать эту строку, чтобы увидеть изменение в let p1 = p; ниже +#[derive(Copy, Clone, Debug)] // Мы подробнее поговорим об этом позже struct Point{x: u32, y:u32} fn main() { let p = Point {x: 42, y: 40}; - let p1 = p; // This will perform a copy now instead of move + let p1 = p; // Теперь здесь будет копирование вместо перемещения println!("p: {p:?}"); println!("p1: {p:?}"); - let p2 = p1.clone(); // Semantically the same as copy + let p2 = p1.clone(); // Семантически то же, что и копирование } ``` -# Rust Drop trait +# Трейт Drop в Rust -- Rust automatically calls the `drop()` method at the end of scope - - `drop` is part of a generic trait called `Drop`. The compiler provides a blanket NOP implementation for all types, but types can override it. For example, the `String` type overrides it to release heap-allocated memory - - For C developers: this replaces the need for manual `free()` calls — resources are automatically released when they go out of scope (RAII) -- **Key safety:** You cannot call `.drop()` directly (the compiler forbids it). Instead, use `drop(obj)` which moves the value into the function, runs its destructor, and prevents any further use — eliminating double-free bugs +- Rust автоматически вызывает метод `drop()` в конце области видимости + - `drop` — часть обобщённого трейта `Drop`. Компилятор предоставляет пустую (NOP) реализацию для всех типов по умолчанию, но типы могут её переопределить. Например, тип `String` переопределяет её, чтобы освободить память, выделенную в куче + - Для разработчиков на C: это избавляет от необходимости вручную вызывать `free()` — ресурсы автоматически освобождаются при выходе из области видимости (RAII) +- **Ключевое свойство безопасности:** нельзя вызвать `.drop()` напрямую (компилятор это запрещает). Вместо этого используйте `drop(obj)`, которая перемещает значение в функцию, запускает его деструктор и запрещает дальнейшее использование — это исключает ошибки двойного освобождения -> **For C++ developers:** `Drop` maps directly to C++ destructors (`~ClassName()`): +> **Для программистов C++:** `Drop` напрямую соответствует деструкторам C++ (`~ClassName()`): > -> | | **C++ destructor** | **Rust `Drop`** | +> | | **Деструктор C++** | **Rust `Drop`** | > |---|---|---| -> | **Syntax** | `~MyClass() { ... }` | `impl Drop for MyType { fn drop(&mut self) { ... } }` | -> | **When called** | End of scope (RAII) | End of scope (same) | -> | **Called on move** | Source left in "valid but unspecified" state — destructor still runs on the moved-from object | Source is **gone** — no destructor call on moved-from value | -> | **Manual call** | `obj.~MyClass()` (dangerous, rarely used) | `drop(obj)` (safe — takes ownership, calls `drop`, prevents further use) | -> | **Order** | Reverse declaration order | Reverse declaration order (same) | -> | **Rule of Five** | Must manage copy ctor, move ctor, copy assign, move assign, destructor | Only `Drop` — compiler handles move semantics, and `Clone` is opt-in | -> | **Virtual dtor needed?** | Yes, if deleting through base pointer | No — no inheritance, so no slicing problem | +> | **Синтаксис** | `~MyClass() { ... }` | `impl Drop for MyType { fn drop(&mut self) { ... } }` | +> | **Когда вызывается** | В конце области видимости (RAII) | В конце области видимости (так же) | +> | **При перемещении** | Исходный объект остаётся в «допустимом, но не определённом состоянии» — деструктор всё равно вызывается для перемещённого объекта | Исходного значения **больше нет** — деструктор для перемещённого значения не вызывается | +> | **Ручной вызов** | `obj.~MyClass()` (опасно, редко используется) | `drop(obj)` (безопасно — забирает владение, вызывает `drop` и запрещает дальнейшее использование) | +> | **Порядок** | Обратный порядок объявления | Обратный порядок объявления (так же) | +> | **Правило пяти** | Нужно управлять конструктором копирования, конструктором перемещения, копирующим присваиванием, перемещающим присваиванием, деструктором | Только `Drop` — компилятор обрабатывает семантику перемещения, а `Clone` подключается явно | +> | **Нужен виртуальный деструктор?** | Да, если удаляем через базовый указатель | Нет — наследования нет, поэтому проблемы среза (slicing) не возникает | ```rust struct Point {x: u32, y:u32} -// Equivalent to: ~Point() { printf("Goodbye point x:%u, y:%u\n", x, y); } +// Эквивалентно: ~Point() { printf("Goodbye point x:%u, y:%u\n", x, y); } impl Drop for Point { fn drop(&mut self) { println!("Goodbye point x:{}, y:{}", self.x, self.y); @@ -236,27 +236,27 @@ fn main() { { let p1 = Point{x:43, y: 43}; println!("Exiting inner block"); - // p1.drop() called here — like C++ end-of-scope destructor + // p1.drop() вызывается здесь — как деструктор C++ в конце области видимости } println!("Exiting main"); - // p.drop() called here + // p.drop() вызывается здесь } ``` -# Exercise: Move, Copy and Drop +# Упражнение: перемещение, копирование и Drop -🟡 **Intermediate** — experiment freely; the compiler will guide you -- Create your own experiments with ```Point``` with and without ```Copy``` in ```#[derive(Debug)]``` in the below make sure you understand the differences. The idea is to get a solid understanding of how move vs. copy works, so make sure to ask -- Implement a custom ```Drop``` for ```Point``` that sets x and y to 0 in ```drop```. This is a pattern that's useful for releasing locks and other resources for example +🟡 **Средний уровень** — экспериментируйте свободно; компилятор подскажет +- Поэкспериментируйте с ```Point``` с ```Copy``` в ```#[derive(Debug)]``` и без него, как показано ниже, и убедитесь, что понимаете разницу. Идея в том, чтобы твёрдо понять, как работают перемещение и копирование, поэтому обязательно задавайте вопросы +- Реализуйте собственный ```Drop``` для ```Point```, который в ```drop``` обнуляет x и y. Такой шаблон полезен, например, для освобождения блокировок и других ресурсов ```rust struct Point{x: u32, y: u32} fn main() { - // Create Point, assign it to a different variable, create a new scope, - // pass point to a function, etc. + // Создайте Point, присвойте её другой переменной, создайте новую область видимости, + // передайте point в функцию и т. д. } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust #[derive(Debug)] @@ -267,34 +267,34 @@ impl Drop for Point { println!("Dropping Point({}, {})", self.x, self.y); self.x = 0; self.y = 0; - // Note: setting to 0 in drop demonstrates the pattern, - // but you can't observe these values after drop completes + // Примечание: обнуление в drop демонстрирует шаблон, + // но эти значения нельзя наблюдать после завершения drop } } fn consume(p: Point) { println!("Consuming: {:?}", p); - // p is dropped here + // p уничтожается здесь } fn main() { let p1 = Point { x: 10, y: 20 }; - let p2 = p1; // Move — p1 is no longer valid - // println!("{:?}", p1); // Won't compile: p1 was moved + let p2 = p1; // Перемещение — p1 больше не валидна + // println!("{:?}", p1); // Не скомпилируется: p1 была перемещена { let p3 = Point { x: 30, y: 40 }; println!("p3 in inner scope: {:?}", p3); - // p3 is dropped here (end of scope) + // p3 уничтожается здесь (конец области видимости) } - consume(p2); // p2 is moved into consume and dropped there - // println!("{:?}", p2); // Won't compile: p2 was moved + consume(p2); // p2 перемещается в consume и уничтожается там + // println!("{:?}", p2); // Не скомпилируется: p2 была перемещена - // Now try: add #[derive(Copy, Clone)] to Point (and remove the Drop impl) - // and observe how p1 remains valid after let p2 = p1; + // Теперь попробуйте: добавить #[derive(Copy, Clone)] к Point (и удалить реализацию Drop) + // и посмотрите, как p1 остаётся валидной после let p2 = p1; } -// Output: +// Вывод: // p3 in inner scope: Point { x: 30, y: 40 } // Dropping Point(30, 40) // Consuming: Point { x: 10, y: 20 } @@ -303,4 +303,3 @@ fn main() { </details> - diff --git a/c-cpp-book/src/ch08-1-testing-patterns.md b/c-cpp-book/src/ch08-1-testing-patterns.md index 3f67640..03c3030 100644 --- a/c-cpp-book/src/ch08-1-testing-patterns.md +++ b/c-cpp-book/src/ch08-1-testing-patterns.md @@ -1,12 +1,12 @@ -## Testing Patterns for C++ Programmers +## Паттерны тестирования для программистов C++ -> **What you'll learn:** Rust's built-in test framework — `#[test]`, `#[should_panic]`, `Result`-returning tests, builder patterns for test data, trait-based mocking, property testing with `proptest`, snapshot testing with `insta`, and integration test organization. Zero-config testing that replaces Google Test + CMake. +> **Что вы узнаете:** встроенный тестовый фреймворк Rust — `#[test]`, `#[should_panic]`, тесты, возвращающие `Result`, паттерны-билдеры для тестовых данных, мокирование на основе трейтов, property-based тестирование с `proptest`, snapshot-тестирование с `insta` и организацию интеграционных тестов. Тестирование без настройки, которое заменяет Google Test + CMake. -C++ testing typically relies on external frameworks (Google Test, Catch2, Boost.Test) -with complex build integration. Rust's test framework is **built into the language -and toolchain** — no dependencies, no CMake integration, no test runner configuration. +Тестирование в C++ обычно опирается на внешние фреймворки (Google Test, Catch2, Boost.Test) +со сложной интеграцией в сборку. Тестовый фреймворк Rust **встроен в язык +и тулчейн** — никаких зависимостей, никакой интеграции с CMake, никакой настройки тестового раннера. -### Test attributes beyond `#[test]` +### Атрибуты тестов помимо `#[test]` ```rust #[cfg(test)] @@ -18,15 +18,15 @@ mod tests { assert_eq!(2 + 2, 4); } - // Expect a panic — equivalent to GTest's EXPECT_DEATH + // Ожидаем panic — аналог EXPECT_DEATH в GTest #[test] #[should_panic] fn out_of_bounds_panics() { let v = vec![1, 2, 3]; - let _ = v[10]; // Panics — test passes + let _ = v[10]; // Panic — тест проходит } - // Expect a panic with a specific message substring + // Ожидаем panic с определённой подстрокой в сообщении #[test] #[should_panic(expected = "index out of bounds")] fn specific_panic_message() { @@ -34,7 +34,7 @@ mod tests { let _ = v[10]; } - // Tests that return Result<(), E> — use ? instead of unwrap() + // Тесты, возвращающие Result<(), E> — используйте ? вместо unwrap() #[test] fn test_with_result() -> Result<(), String> { let value: u32 = "42".parse().map_err(|e| format!("{e}"))?; @@ -42,7 +42,7 @@ mod tests { Ok(()) } - // Ignore slow tests by default — run with `cargo test -- --ignored` + // По умолчанию пропускаем медленные тесты — запуск через `cargo test -- --ignored` #[test] #[ignore] fn slow_integration_test() { @@ -52,25 +52,25 @@ mod tests { ``` ```bash -cargo test # Run all non-ignored tests -cargo test -- --ignored # Run only ignored tests -cargo test -- --include-ignored # Run ALL tests including ignored -cargo test test_name # Run tests matching a name pattern -cargo test -- --nocapture # Show println! output during tests -cargo test -- --test-threads=1 # Run tests serially (for shared state) +cargo test # Запустить все не игнорируемые тесты +cargo test -- --ignored # Запустить только игнорируемые тесты +cargo test -- --include-ignored # Запустить ВСЕ тесты, включая игнорируемые +cargo test test_name # Запустить тесты, имя которых соответствует шаблону +cargo test -- --nocapture # Показывать вывод println! во время тестов +cargo test -- --test-threads=1 # Запускать тесты последовательно (для общего состояния) ``` -### Test helpers: builder pattern for test data +### Вспомогательные функции для тестов: паттерн «строитель» для тестовых данных -In C++ you'd use Google Test fixtures (`class MyTest : public ::testing::Test`). -In Rust, use builder functions or the `Default` trait: +В C++ вы бы использовали фикстуры Google Test (`class MyTest : public ::testing::Test`). +В Rust используйте функции-билдеры или трейт `Default`: ```rust #[cfg(test)] mod tests { use super::*; - // Builder function — creates test data with sensible defaults + // Функция-билдер — создаёт тестовые данные с разумными значениями по умолчанию fn make_gpu_event(severity: Severity, fault_code: u32) -> DiagEvent { DiagEvent { source: "accel_diag".to_string(), @@ -80,7 +80,7 @@ mod tests { } } - // Reusable test fixture — a set of pre-built events + // Переиспользуемая фикстура — набор заранее созданных событий fn sample_events() -> Vec<DiagEvent> { vec![ make_gpu_event(Severity::Critical, 67956), @@ -101,27 +101,27 @@ mod tests { } ``` -### Mocking with traits +### Мокирование с помощью трейтов -In C++, mocking requires frameworks like Google Mock or manual virtual overrides. -In Rust, define a trait for the dependency and swap implementations in tests: +В C++ для мокирования нужны фреймворки вроде Google Mock или ручные переопределения virtual-методов. +В Rust вы определяете трейт для зависимости и подменяете реализации в тестах: ```rust -// Production trait +// Продакшн-трейт trait SensorReader { fn read_temperature(&self, sensor_id: u32) -> Result<f64, String>; } -// Production implementation +// Продакшн-реализация struct HwSensorReader; impl SensorReader for HwSensorReader { fn read_temperature(&self, sensor_id: u32) -> Result<f64, String> { - // Real hardware call... + // Реальный вызов оборудования... Ok(72.5) } } -// Test mock — returns predictable values +// Тестовый мок — возвращает предсказуемые значения #[cfg(test)] struct MockSensorReader { temperatures: std::collections::HashMap<u32, f64>, @@ -136,7 +136,7 @@ impl SensorReader for MockSensorReader { } } -// Function under test — generic over the reader +// Тестируемая функция — обобщённая по типу reader fn check_overtemp(reader: &impl SensorReader, ids: &[u32], threshold: f64) -> Vec<u32> { ids.iter() .filter(|&&id| reader.read_temperature(id).unwrap_or(0.0) > threshold) @@ -152,7 +152,7 @@ mod tests { fn detect_overtemp_sensors() { let mut mock = MockSensorReader { temperatures: Default::default() }; mock.temperatures.insert(0, 72.5); - mock.temperatures.insert(1, 91.0); // Over threshold + mock.temperatures.insert(1, 91.0); // Выше порога mock.temperatures.insert(2, 65.0); let hot = check_overtemp(&mock, &[0, 1, 2], 80.0); @@ -161,9 +161,9 @@ mod tests { } ``` -### Temporary files and directories in tests +### Временные файлы и каталоги в тестах -C++ tests often use platform-specific temp directories. Rust has `tempfile`: +В C++ тесты часто используют платформо-специфичные временные каталоги. В Rust есть крейт `tempfile`: ```rust // Cargo.toml: [dev-dependencies] @@ -177,22 +177,22 @@ mod tests { #[test] fn parse_config_from_file() -> Result<(), Box<dyn std::error::Error>> { - // Create a temp file that's auto-deleted when dropped + // Создаём временный файл, который удаляется автоматически при уничтожении let mut file = NamedTempFile::new()?; writeln!(file, r#"{{"sku": "ServerNode", "level": "Quick"}}"#)?; let config = load_config(file.path().to_str().unwrap())?; assert_eq!(config.sku, "ServerNode"); Ok(()) - // file is deleted here — no cleanup code needed + // file удаляется здесь — код очистки не нужен } } ``` -### Property-based testing with `proptest` +### Property-based тестирование с `proptest` -Instead of writing specific test cases, describe **properties** that should hold -for all inputs. `proptest` generates random inputs and finds minimal failing cases: +Вместо написания конкретных тестовых случаев опишите **свойства**, которые должны выполняться +для всех входных данных. `proptest` генерирует случайные входные данные и находит минимальные падающие примеры: ```rust // Cargo.toml: [dev-dependencies] @@ -222,10 +222,10 @@ mod tests { } ``` -### Snapshot testing with `insta` +### Snapshot-тестирование с `insta` -For tests that produce complex output (JSON, formatted strings), `insta` auto-generates -and manages reference snapshots: +Для тестов, которые выдают сложный вывод (JSON, отформатированные строки), `insta` автоматически создаёт +и поддерживает эталонные снимки: ```rust // Cargo.toml: [dev-dependencies] @@ -242,55 +242,55 @@ mod tests { component: "GPU".to_string(), message: "ECC error detected".to_string(), }; - // First run: creates a snapshot file in tests/snapshots/ - // Subsequent runs: compares against the saved snapshot + // Первый запуск: создаёт файл снимка в tests/snapshots/ + // Последующие запуски: сравнивают с сохранённым снимком assert_json_snapshot!(entry); } } ``` ```bash -cargo insta test # Run tests and review new/changed snapshots -cargo insta review # Interactive review of snapshot changes +cargo insta test # Запустить тесты и просмотреть новые/изменённые снимки +cargo insta review # Интерактивный просмотр изменений снимков ``` -### C++ vs Rust testing comparison +### Сравнение тестирования C++ и Rust -| **C++ (Google Test)** | **Rust** | **Notes** | +| **C++ (Google Test)** | **Rust** | **Примечания** | |----------------------|---------|----------| -| `TEST(Suite, Name) { }` | `#[test] fn name() { }` | No suite/class hierarchy needed | -| `ASSERT_EQ(a, b)` | `assert_eq!(a, b)` | Built-in macro, no framework needed | -| `ASSERT_NEAR(a, b, eps)` | `assert!((a - b).abs() < eps)` | Or use `approx` crate | -| `EXPECT_THROW(expr, type)` | `#[should_panic(expected = "...")]` | Or `catch_unwind` for fine control | +| `TEST(Suite, Name) { }` | `#[test] fn name() { }` | Иерархия suite/class не нужна | +| `ASSERT_EQ(a, b)` | `assert_eq!(a, b)` | Встроенный макрос, фреймворк не нужен | +| `ASSERT_NEAR(a, b, eps)` | `assert!((a - b).abs() < eps)` | Или используйте крейт `approx` | +| `EXPECT_THROW(expr, type)` | `#[should_panic(expected = "...")]` | Или `catch_unwind` для тонкого контроля | | `EXPECT_DEATH(expr, "msg")` | `#[should_panic(expected = "msg")]` | | -| `class Fixture : public ::testing::Test` | Builder functions + `Default` | No inheritance needed | -| Google Mock `MOCK_METHOD` | Trait + test impl | More explicit, no macro magic | -| `INSTANTIATE_TEST_SUITE_P` (parameterized) | `proptest!` or macro-generated tests | | -| `SetUp()` / `TearDown()` | RAII via `Drop` — cleanup is automatic | Variables dropped at end of test | -| Separate test binary + CMake | `cargo test` — zero config | | +| `class Fixture : public ::testing::Test` | Функции-билдеры + `Default` | Наследование не нужно | +| Google Mock `MOCK_METHOD` | Трейт + тестовая реализация | Более явно, без магии макросов | +| `INSTANTIATE_TEST_SUITE_P` (параметризованные) | `proptest!` или тесты, сгенерированные макросом | | +| `SetUp()` / `TearDown()` | RAII через `Drop` — очистка автоматическая | Переменные уничтожаются в конце теста | +| Отдельный тестовый бинарник + CMake | `cargo test` — без настройки | | | `ctest --output-on-failure` | `cargo test -- --nocapture` | | ---- -### Integration tests: the `tests/` directory +### Интеграционные тесты: каталог `tests/` -Unit tests live inside `#[cfg(test)]` modules alongside your code. **Integration tests** live in a separate `tests/` directory at the crate root and test your library's public API as an external consumer would: +Модульные тесты находятся внутри модулей `#[cfg(test)]` рядом с вашим кодом. **Интеграционные тесты** находятся в отдельном каталоге `tests/` в корне крейта и проверяют публичный API вашей библиотеки так, как это делал бы внешний потребитель: ``` my_crate/ ├── src/ -│ └── lib.rs # Your library code +│ └── lib.rs # Код вашей библиотеки ├── tests/ -│ ├── smoke.rs # Each .rs file is a separate test binary +│ ├── smoke.rs # Каждый .rs-файл — отдельный тестовый бинарник │ ├── regression.rs │ └── common/ -│ └── mod.rs # Shared test helpers (NOT a test itself) +│ └── mod.rs # Общие вспомогательные функции для тестов (НЕ сам тест) └── Cargo.toml ``` ```rust -// tests/smoke.rs — tests your crate as an external user would -use my_crate::DiagEngine; // Only public API is accessible +// tests/smoke.rs — тестирует ваш крейт так, как это делал бы внешний пользователь +use my_crate::DiagEngine; // Доступен только публичный API #[test] fn engine_starts_successfully() { @@ -306,7 +306,7 @@ fn engine_rejects_invalid_config() { ``` ```rust -// tests/common/mod.rs — shared helpers, NOT compiled as a test binary +// tests/common/mod.rs — общие помощники, НЕ компилируются как тестовый бинарник pub fn setup_test_environment() -> tempfile::TempDir { let dir = tempfile::tempdir().unwrap(); std::fs::write(dir.path().join("config.json"), r#"{"log_level": "debug"}"#).unwrap(); @@ -315,7 +315,7 @@ pub fn setup_test_environment() -> tempfile::TempDir { ``` ```rust -// tests/regression.rs — can use shared helpers +// tests/regression.rs — может использовать общие помощники mod common; #[test] @@ -328,16 +328,15 @@ fn regression_issue_42() { } ``` -**Running integration tests:** +**Запуск интеграционных тестов:** ```bash -cargo test # Runs unit AND integration tests -cargo test --test smoke # Run only tests/smoke.rs -cargo test --test regression # Run only tests/regression.rs -cargo test --lib # Run ONLY unit tests (skip integration) +cargo test # Запускает и модульные, и интеграционные тесты +cargo test --test smoke # Запустить только tests/smoke.rs +cargo test --test regression # Запустить только tests/regression.rs +cargo test --lib # Запустить ТОЛЬКО модульные тесты (без интеграционных) ``` -> **Key difference from unit tests**: Integration tests cannot access private functions or `pub(crate)` items. This forces you to verify that your public API is sufficient — a valuable design signal. In C++ terms, it's like testing against only the public header with no `friend` access. +> **Ключевое отличие от модульных тестов**: интеграционные тесты не могут обращаться к приватным функциям или элементам `pub(crate)`. Это заставляет проверять, что публичного API достаточно — полезный сигнал для дизайна. В терминах C++ это похоже на тестирование только по публичному заголовку без доступа `friend`. ---- - diff --git a/c-cpp-book/src/ch08-crates-and-modules.md b/c-cpp-book/src/ch08-crates-and-modules.md index 09c8dcf..77592af 100644 --- a/c-cpp-book/src/ch08-crates-and-modules.md +++ b/c-cpp-book/src/ch08-crates-and-modules.md @@ -1,27 +1,27 @@ -# Rust crates and modules +# Крейты и модули в Rust -> **What you'll learn:** How Rust organizes code into modules and crates — privacy-by-default visibility, `pub` modifiers, workspaces, and the `crates.io` ecosystem. Replaces C/C++ header files, `#include`, and CMake dependency management. +> **Что вы узнаете:** как Rust организует код в модули и крейты — видимость по умолчанию (приватная), модификаторы `pub`, рабочие пространства и экосистему `crates.io`. Заменяет заголовочные файлы C/C++, `#include` и управление зависимостями в CMake. -- Modules are the fundamental organizational unit of code within crates - - Each source file (.rs) is its own module, and can create nested modules using the ```mod``` keyword. - - All types in a (sub-) module are **private** by default, and aren't externally visible within the same crate unless they are explicitly marked as ```pub``` (public). The scope of ```pub``` can be further restricted to ```pub(crate)```, etc - - Even if a type is public, it doesn't automatically become visible within the scope of another module unless it's imported using the ```use``` keyword. Child submodules can reference types in the parent scope using the ```use super::``` - - Source files (.rs) aren't automatically included in the crate **unless** they are explicitly listed in ```main.rs``` (executable) or ```lib.rs``` +- Модули — базовая организационная единица кода внутри крейтов + - Каждый исходный файл (.rs) — это отдельный модуль, и он может создавать вложенные модули с помощью ключевого слова ```mod```. + - Все типы внутри (под)модуля по умолчанию **приватные** и не видны извне в пределах того же крейта, если явно не помечены как ```pub``` (публичные). Область действия ```pub``` можно дополнительно ограничить через ```pub(crate)``` и т. д. + - Даже публичный тип автоматически не становится видимым в другом модуле, если он не импортирован с помощью ключевого слова ```use```. Дочерние подмодули могут обращаться к типам родительской области через ```use super::``` + - Исходные файлы (.rs) **не** включаются в крейт автоматически, если явно не перечислены в ```main.rs``` (исполняемый файл) или ```lib.rs``` -# Exercise: Modules and functions -- We'll take a look at modifying our [hello world](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=522d86dbb8c4af71ff2ec081fb76aee7) to call another function - - As previously mentioned, function are defined with the ```fn``` keyword. The ```->``` keyword declares that the function returns a value (the default is void) with the type ```u32``` (unsigned 32-bit integer) - - Functions are scoped by module, i.e., two functions with exact same name in two modules won't have a name collision - - The module scoping extends to all types (for example, a ```struct foo``` in ```mod a { struct foo; }``` is a distinct type (```a::foo```) from ```mod b { struct foo; }``` (```b::foo```)) +# Упражнение: модули и функции +- Посмотрим, как изменить наш [hello world](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=522d86dbb8c4af71ff2ec081fb76aee7), чтобы он вызывал другую функцию + - Как уже говорилось, функции определяются ключевым словом ```fn```. Ключевой элемент ```->``` объявляет, что функция возвращает значение (по умолчанию — void) типа ```u32``` (беззнаковое 32-битное целое) + - Функции ограничены областью модуля, то есть две функции с одинаковым именем в разных модулях не конфликтуют + - Ограничение модулем распространяется на все типы (например, ```struct foo``` в ```mod a { struct foo; }``` — это отдельный тип (```a::foo```), отличный от ```mod b { struct foo; }``` (```b::foo```)) -**Starter code** — complete the functions: +**Стартовый код** — дополните функции: ```rust mod math { - // TODO: implement pub fn add(a: u32, b: u32) -> u32 + // TODO: реализуйте pub fn add(a: u32, b: u32) -> u32 } fn greet(name: &str) -> String { - // TODO: return "Hello, <name>! The secret number is <math::add(21,21)>" + // TODO: верните "Hello, <name>! The secret number is <math::add(21,21)>" todo!() } @@ -30,7 +30,7 @@ fn main() { } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust mod math { @@ -46,16 +46,16 @@ fn greet(name: &str) -> String { fn main() { println!("{}", greet("Rustacean")); } -// Output: Hello, Rustacean! The secret number is 42 +// Вывод: Hello, Rustacean! The secret number is 42 ``` </details> -## Workspaces and crates (packages) +## Рабочие пространства и крейты (пакеты) -- Any significant Rust project should use workspaces to organize component crates - - A workspace is simply a collection of local crates that will be used to build the target binaries. The `Cargo.toml` at the workspace root should have a pointer to the constituent packages (crates) +- Любой серьёзный проект на Rust должен использовать рабочие пространства (workspaces) для организации составных крейтов + - Рабочее пространство — это просто набор локальных крейтов, которые используются для сборки целевых бинарных файлов. `Cargo.toml` в корне рабочего пространства должен содержать ссылки на составные пакеты (крейты) ```toml [workspace] @@ -65,61 +65,61 @@ members = ["package1", "package2"] ```text workspace_root/ -|-- Cargo.toml # Workspace configuration +|-- Cargo.toml # Конфигурация рабочего пространства |-- package1/ -| |-- Cargo.toml # Package 1 configuration +| |-- Cargo.toml # Конфигурация пакета 1 | `-- src/ -| `-- lib.rs # Package 1 source code +| `-- lib.rs # Исходный код пакета 1 |-- package2/ -| |-- Cargo.toml # Package 2 configuration +| |-- Cargo.toml # Конфигурация пакета 2 | `-- src/ -| `-- main.rs # Package 2 source code +| `-- main.rs # Исходный код пакета 2 ``` --- -## Exercise: Using workspaces and package dependencies -- We'll create a simple package and use it from our ```hello world``` program` -- Create the workspace directory +## Упражнение: использование рабочих пространств и зависимостей пакетов +- Создадим простой пакет и используем его из нашей программы ```hello world``` +- Создайте каталог рабочего пространства ```bash mkdir workspace cd workspace ``` -- Create a file called Cargo.toml and add the following to it. This creates an empty workspace +- Создайте файл Cargo.toml и добавьте в него следующее. Это создаёт пустое рабочее пространство ```toml [workspace] resolver = "2" members = [] ``` -- Add the packages (```cargo new --lib``` specifies a library instead of an executable`) +- Добавьте пакеты (```cargo new --lib``` создаёт библиотеку вместо исполняемого файла) ```bash cargo new hello cargo new --lib hellolib ``` -## Exercise: Using workspaces and package dependencies -- Take a look at the generated Cargo.toml in ```hello``` and ```hellolib```. Notice that both of them have been added to the upper level ```Cargo.toml``` -- The presence of ```lib.rs``` in ```hellolib``` implies a library package (see https://doc.rust-lang.org/cargo/reference/cargo-targets.html for customization options) -- Adding a dependency on ```hellolib``` in ```Cargo.toml``` for ```hello``` +## Упражнение: использование рабочих пространств и зависимостей пакетов +- Посмотрите сгенерированные Cargo.toml в ```hello``` и ```hellolib```. Обратите внимание, что оба добавлены в Cargo.toml верхнего уровня +- Наличие ```lib.rs``` в ```hellolib``` означает, что это библиотечный пакет (см. https://doc.rust-lang.org/cargo/reference/cargo-targets.html, там описаны варианты настройки) +- Добавляем зависимость от ```hellolib``` в Cargo.toml для ```hello``` ```toml [dependencies] hellolib = {path = "../hellolib"} ``` -- Using ```add()``` from ```hellolib``` +- Используем ```add()``` из ```hellolib``` ```rust fn main() { println!("Hello, world! {}", hellolib::add(21, 21)); } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> -The complete workspace setup: +Полная настройка рабочего пространства: ```bash -# Terminal commands +# Команды терминала mkdir workspace && cd workspace -# Create workspace Cargo.toml +# Создаём Cargo.toml рабочего пространства cat > Cargo.toml << 'EOF' [workspace] resolver = "2" @@ -131,13 +131,13 @@ cargo new --lib hellolib ``` ```toml -# hello/Cargo.toml — add dependency +# hello/Cargo.toml — добавляем зависимость [dependencies] hellolib = {path = "../hellolib"} ``` ```rust -// hellolib/src/lib.rs — already has add() from cargo new --lib +// hellolib/src/lib.rs — add() уже есть после cargo new --lib pub fn add(left: u64, right: u64) -> u64 { left + right } @@ -148,56 +148,56 @@ pub fn add(left: u64, right: u64) -> u64 { fn main() { println!("Hello, world! {}", hellolib::add(21, 21)); } -// Output: Hello, world! 42 +// Вывод: Hello, world! 42 ``` </details> -# Using community crates from crates.io -- Rust has a vibrant ecosystem of community crates (see https://crates.io/) - - The Rust philosophy is to keep the standard library compact and outsource functionality to community crates - - There is no hard and fast rule about using community crates, but the rule of thumb should be to ensure that the crate has a decent maturity level (indicated by the version number), and that it's being actively maintained. Reach out to internal sources if in doubt about a crate -- Every crate published on ```crates.io``` has a major and minor version - - Crates are expected to observe the major and minor ```SemVer``` guidelines defined here: https://doc.rust-lang.org/cargo/reference/semver.html - - The TL;DR version is that there should be no breaking changes for the same minor version. For example, v0.11 must be compatible with v0.15 (but v0.20 may have breaking changes) - -# Crates dependencies and SemVer -- Crates can define dependencies on a specific versions of a crate, specific minor or major version, or don't care. The following examples show the ```Cargo.toml``` entries for declaring a dependency on the ```rand``` crate -- At least ```0.10.0```, but anything ```< 0.11.0``` is fine +# Использование крейтов сообщества из crates.io +- В Rust богатая экосистема крейтов сообщества (см. https://crates.io/) + - Философия Rust — держать стандартную библиотеку компактной и выносить функциональность в крейты сообщества + - Жёсткого правила насчёт использования крейтов сообщества нет, но ориентир такой: крейт должен быть достаточно зрелым (это видно по номеру версии) и активно поддерживаться. Если сомневаетесь в крейте, уточните во внутренних источниках +- Каждый крейт, опубликованный на ```crates.io```, имеет мажорную и минорную версии + - Ожидается, что крейты соблюдают рекомендации ```SemVer`` по мажорной и минорной версиям: https://doc.rust-lang.org/cargo/reference/semver.html + - Вкратце: внутри одной минорной версии не должно быть несовместимых изменений. Например, v0.11 должна быть совместима с v0.15 (а вот v0.20 может содержать несовместимые изменения) + +# Зависимости крейтов и SemVer +- Крейты могут объявлять зависимости от конкретной версии крейта, конкретной минорной или мажорной версии либо быть безразличными к версии. Следующие примеры показывают записи в ```Cargo.toml``` для объявления зависимости от крейта ```rand``` +- Не ниже ```0.10.0```, но подходит всё, что ```< 0.11.0``` ```toml [dependencies] rand = { version = "0.10.0"} ``` -- Only ```0.10.0```, and nothing else +- Только ```0.10.0```, и ничего другого ```toml [dependencies] rand = { version = "=0.10.0"} ``` -- Don't care; ```cargo``` will select the latest version +- Версия не важна; ```cargo``` выберет последнюю ```toml [dependencies] rand = { version = "*"} ``` -- Reference: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html +- Справка: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html ---- -# Exercise: Using the rand crate -- Modify the ```helloworld``` example to print a random number -- Use ```cargo add rand``` to add a dependency -- Use ```https://docs.rs/rand/latest/rand/``` as a reference for the API +# Упражнение: использование крейта rand +- Измените пример ```helloworld```, чтобы он выводил случайное число +- Используйте ```cargo add rand``` для добавления зависимости +- Используйте ```https://docs.rs/rand/latest/rand/``` как справочник по API -**Starter code** — add this to `main.rs` after running `cargo add rand`: +**Стартовый код** — добавьте это в `main.rs` после выполнения `cargo add rand`: ```rust,ignore use rand::RngExt; fn main() { let mut rng = rand::rng(); - // TODO: Generate and print a random u32 in 1..=100 - // TODO: Generate and print a random bool - // TODO: Generate and print a random f64 + // TODO: Сгенерируйте и выведите случайное u32 в диапазоне 1..=100 + // TODO: Сгенерируйте и выведите случайный bool + // TODO: Сгенерируйте и выведите случайный f64 } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use rand::RngExt; @@ -207,11 +207,11 @@ fn main() { let n: u32 = rng.random_range(1..=100); println!("Random number (1-100): {n}"); - // Generate a random boolean + // Генерируем случайный булев let b: bool = rng.random(); println!("Random bool: {b}"); - // Generate a random float between 0.0 and 1.0 + // Генерируем случайное число с плавающей точкой от 0.0 до 1.0 let f: f64 = rng.random(); println!("Random float: {f:.4}"); } @@ -219,98 +219,97 @@ fn main() { </details> -# Cargo.toml and Cargo.lock -- As mentioned previously, Cargo.lock is automatically generated from Cargo.toml - - The main idea behind Cargo.lock is to ensure reproducible builds. For example, if ```Cargo.toml``` had specified a version of ```0.10.0```, cargo is free to choose any version that is ```< 0.11.0``` - - Cargo.lock contains the *specific* version of the rand crate that was used during the build. - - The recommendation is to include ```Cargo.lock``` in the git repo to ensure reproducible builds +# Cargo.toml и Cargo.lock +- Как уже говорилось, Cargo.lock автоматически генерируется из Cargo.toml + - Основная идея Cargo.lock — обеспечить воспроизводимые сборки. Например, если в ```Cargo.toml``` указана версия ```0.10.0```, cargo вправе выбрать любую версию ```< 0.11.0``` + - Cargo.lock содержит *конкретную* версию крейта rand, которая использовалась при сборке. + - Рекомендуется включать ```Cargo.lock``` в git-репозиторий, чтобы сборки были воспроизводимыми -## Cargo test feature -- Rust unit tests reside in the same source file (by convention), and are usually grouped into separate module - - The test code is never included in the actual binary. This is made possible by the ```cfg``` (configuration) feature. Configurations are useful for creating platform specific code (```Linux``` vs. ```Windows```) for example - - Tests can be executed with ```cargo test```. Reference: https://doc.rust-lang.org/reference/conditional-compilation.html +## Функция cargo test +- Модульные тесты Rust по соглашению находятся в том же исходном файле и обычно группируются в отдельный модуль + - Тестовый код никогда не попадает в итоговый бинарный файл. Это возможно благодаря функции ```cfg``` (конфигурация). Конфигурации полезны, например, для платформо-специфичного кода (```Linux``` и ```Windows```) + - Тесты запускаются командой ```cargo test```. Справка: https://doc.rust-lang.org/reference/conditional-compilation.html ```rust pub fn add(left: u64, right: u64) -> u64 { left + right } -// Will be included only during testing +// Будет включён только во время тестирования #[cfg(test)] mod tests { - use super::*; // This makes all types in the parent scope visible + use super::*; // Делает видимыми все типы родительской области #[test] fn it_works() { - let result = add(2, 2); // Alternatively, super::add(2, 2); + let result = add(2, 2); // Или то же самое: super::add(2, 2); assert_eq!(result, 4); } } ``` -# Other Cargo features -- ```cargo``` has several other useful features including: - - ```cargo clippy``` is a great way of linting Rust code. In general, warnings should be fixed (or rarely suppressed if really warranted) - - ```cargo format``` executes the ```rustfmt``` tool to format source code. Using the tool ensures standard formatting of checked-in code and puts an end to debates about style - - ```cargo doc``` can be used to generate documentation from the ```///``` style comments. The documentation for all crates on ```crates.io``` was generated using this method +# Другие возможности Cargo +- У ```cargo``` есть и другие полезные возможности, в том числе: + - ```cargo clippy``` — отличный способ линтинга кода на Rust. Как правило, предупреждения стоит исправлять (подавлять их имеет смысл лишь в действительно обоснованных случаях) + - ```cargo format``` запускает инструмент ```rustfmt``` для форматирования исходного кода. Использование этого инструмента обеспечивает единообразное форматирование кода в репозитории и кладёт конец спорам о стиле + - ```cargo doc``` генерирует документацию из комментариев в стиле ```///```. Документация для всех крейтов на ```crates.io``` создана именно этим способом -### Build Profiles: Controlling Optimization +### Профили сборки: управление оптимизацией -In C, you pass `-O0`, `-O2`, `-Os`, `-flto` to `gcc`/`clang`. In Rust, you configure -build profiles in `Cargo.toml`: +В C вы передаёте `-O0`, `-O2`, `-Os`, `-flto` в `gcc`/`clang`. В Rust профили сборки настраиваются в `Cargo.toml`: ```toml -# Cargo.toml — build profile configuration +# Cargo.toml — настройка профилей сборки [profile.dev] -opt-level = 0 # No optimization (fast compile, like -O0) -debug = true # Full debug symbols (like -g) +opt-level = 0 # Без оптимизаций (быстрая компиляция, как -O0) +debug = true # Полные отладочные символы (как -g) [profile.release] -opt-level = 3 # Maximum optimization (like -O3) -lto = "fat" # Link-Time Optimization (like -flto) -strip = true # Strip symbols (like the strip command) -codegen-units = 1 # Single codegen unit — slower compile, better optimization -panic = "abort" # No unwind tables (smaller binary) +opt-level = 3 # Максимальная оптимизация (как -O3) +lto = "fat" # Оптимизация на этапе компоновки (как -flto) +strip = true # Удалить символы (как команда strip) +codegen-units = 1 # Один блок генерации кода — компиляция медленнее, оптимизация лучше +panic = "abort" # Без таблиц раскрутки стека (меньше бинарный файл) ``` -| C/GCC Flag | Cargo.toml Key | Values | +| Флаг C/GCC | Ключ в Cargo.toml | Значения | |------------|---------------|--------| | `-O0` / `-O2` / `-O3` | `opt-level` | `0`, `1`, `2`, `3`, `"s"`, `"z"` | | `-flto` | `lto` | `false`, `"thin"`, `"fat"` | -| `-g` / no `-g` | `debug` | `true`, `false`, `"line-tables-only"` | -| `strip` command | `strip` | `"none"`, `"debuginfo"`, `"symbols"`, `true`/`false` | -| — | `codegen-units` | `1` = best opt, slowest compile | +| `-g` / без `-g` | `debug` | `true`, `false`, `"line-tables-only"` | +| команда `strip` | `strip` | `"none"`, `"debuginfo"`, `"symbols"`, `true`/`false` | +| — | `codegen-units` | `1` = лучшая оптимизация, самая медленная компиляция | ```bash -cargo build # Uses [profile.dev] -cargo build --release # Uses [profile.release] +cargo build # Использует [profile.dev] +cargo build --release # Использует [profile.release] ``` -### Build Scripts (`build.rs`): Linking C Libraries +### Скрипты сборки (`build.rs`): подключение C-библиотек -In C, you use Makefiles or CMake to link libraries and run code generation. -Rust uses a `build.rs` file at the crate root: +В C для подключения библиотек и запуска генерации кода используют Makefile или CMake. +В Rust для этого в корне крейта есть файл `build.rs`: ```rust -// build.rs — runs before compiling the crate +// build.rs — запускается перед компиляцией крейта fn main() { - // Link a system C library (like -lbmc_ipmi in gcc) + // Подключаем системную C-библиотеку (как -lbmc_ipmi в gcc) println!("cargo::rustc-link-lib=bmc_ipmi"); - // Where to find the library (like -L/usr/lib/bmc) + // Где искать библиотеку (как -L/usr/lib/bmc) println!("cargo::rustc-link-search=/usr/lib/bmc"); - // Re-run if the C header changes + // Пересобрать, если изменится заголовочный файл C println!("cargo::rerun-if-changed=wrapper.h"); } ``` -You can even compile C source files directly from a Rust crate: +Можно даже компилировать исходники на C прямо из крейта Rust: ```toml # Cargo.toml [build-dependencies] -cc = "1" # C compiler integration +cc = "1" # Интеграция с C-компилятором ``` ```rust @@ -319,7 +318,7 @@ fn main() { cc::Build::new() .file("src/c_helpers/ipmi_raw.c") .include("/usr/include/bmc") - .compile("ipmi_raw"); // Produces libipmi_raw.a, linked automatically + .compile("ipmi_raw"); // Создаёт libipmi_raw.a, который подключается автоматически println!("cargo::rerun-if-changed=src/c_helpers/ipmi_raw.c"); } ``` @@ -328,52 +327,52 @@ fn main() { |-----------------|-----------------| | `-lfoo` | `println!("cargo::rustc-link-lib=foo")` | | `-L/path` | `println!("cargo::rustc-link-search=/path")` | -| Compile C source | `cc::Build::new().file("foo.c").compile("foo")` | -| Generate code | Write files to `$OUT_DIR`, then `include!()` | +| Компиляция исходников C | `cc::Build::new().file("foo.c").compile("foo")` | +| Генерация кода | Записать файлы в `$OUT_DIR`, затем `include!()` | -### Cross-Compilation +### Кросс-компиляция -In C, cross-compilation requires installing a separate toolchain (`arm-linux-gnueabihf-gcc`) -and configuring Make/CMake. In Rust: +В C для кросс-компиляции нужно установить отдельный тулчейн (`arm-linux-gnueabihf-gcc`) +и настроить Make/CMake. В Rust: ```bash -# Install a cross-compilation target +# Устанавливаем цель для кросс-компиляции rustup target add aarch64-unknown-linux-gnu -# Cross-compile +# Кросс-компиляция cargo build --target aarch64-unknown-linux-gnu --release ``` -Specify the linker in `.cargo/config.toml`: +Компоновщик указывается в `.cargo/config.toml`: ```toml [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" ``` -| C Cross-Compile | Rust Equivalent | +| Кросс-компиляция в C | Аналог в Rust | |-----------------|-----------------| -| `apt install gcc-aarch64-linux-gnu` | `rustup target add aarch64-unknown-linux-gnu` + install linker | -| `CC=aarch64-linux-gnu-gcc make` | `.cargo/config.toml` `[target.X] linker = "..."` | +| `apt install gcc-aarch64-linux-gnu` | `rustup target add aarch64-unknown-linux-gnu` + установка компоновщика | +| `CC=aarch64-linux-gnu-gcc make` | `.cargo/config.toml`, `[target.X] linker = "..."` | | `#ifdef __aarch64__` | `#[cfg(target_arch = "aarch64")]` | -| Separate Makefile targets | `cargo build --target ...` | +| Отдельные цели Makefile | `cargo build --target ...` | -### Feature Flags: Conditional Compilation +### Флаги функций: условная компиляция -C uses `#ifdef` and `-DFOO` for conditional compilation. Rust uses feature flags -defined in `Cargo.toml`: +В C для условной компиляции используют `#ifdef` и `-DFOO`. В Rust — флаги функций, +объявленные в `Cargo.toml`: ```toml # Cargo.toml [features] -default = ["json"] # Enabled by default -json = ["dep:serde_json"] # Optional dependency -verbose = [] # Flag with no dependency -gpu = ["dep:cuda-sys"] # Optional GPU support +default = ["json"] # Включено по умолчанию +json = ["dep:serde_json"] # Необязательная зависимость +verbose = [] # Флаг без зависимости +gpu = ["dep:cuda-sys"] # Необязательная поддержка GPU ``` ```rust -// Code gated on features: +// Код, зависящий от флагов: #[cfg(feature = "json")] pub fn parse_config(data: &str) -> Result<Config, Error> { serde_json::from_str(data).map_err(Error::from) @@ -385,24 +384,24 @@ macro_rules! verbose { } #[cfg(not(feature = "verbose"))] macro_rules! verbose { - ($($arg:tt)*) => {}; // Compiles to nothing + ($($arg:tt)*) => {}; // Компилируется в ничего } ``` -| C Preprocessor | Rust Feature Flags | +| Препроцессор C | Флаги функций Rust | |---------------|-------------------| | `gcc -DDEBUG` | `cargo build --features verbose` | | `#ifdef DEBUG` | `#[cfg(feature = "verbose")]` | | `#define MAX 100` | `const MAX: u32 = 100;` | | `#ifdef __linux__` | `#[cfg(target_os = "linux")]` | -### Integration Tests vs Unit Tests +### Интеграционные тесты и модульные тесты -Unit tests live next to the code with `#[cfg(test)]`. **Integration tests** live in -`tests/` and test your crate's **public API only**: +Модульные тесты располагаются рядом с кодом и помечаются `#[cfg(test)]`. **Интеграционные тесты** находятся в каталоге +`tests/` и проверяют только **публичный API** вашего крейта: ```rust -// tests/smoke_test.rs — no #[cfg(test)] needed +// tests/smoke_test.rs — без #[cfg(test)] use my_crate::parse_config; #[test] @@ -412,28 +411,26 @@ fn parse_valid_config() { } ``` -| Aspect | Unit Tests (`#[cfg(test)]`) | Integration Tests (`tests/`) | +| Аспект | Модульные тесты (`#[cfg(test)]`) | Интеграционные тесты (`tests/`) | |--------|----------------------------|------------------------------| -| Location | Same file as code | Separate `tests/` directory | -| Access | Private + public items | **Public API only** | -| Run command | `cargo test` | `cargo test --test smoke_test` | +| Расположение | Тот же файл, что и код | Отдельный каталог `tests/` | +| Доступ | Приватные и публичные элементы | **Только публичный API** | +| Команда запуска | `cargo test` | `cargo test --test smoke_test` | -### Testing Patterns and Strategies +### Паттерны и стратегии тестирования -C firmware teams typically write tests in CUnit, CMocka, or custom frameworks with a -lot of boilerplate. Rust's built-in test harness is far more capable. This section -covers patterns you'll need for production code. +Команды C-прошивок обычно пишут тесты на CUnit, CMocka или собственных фреймворках, с большим количеством шаблонного кода. Встроенный тестовый харнесс Rust гораздо мощнее. В этом разделе рассматриваются паттерны, которые понадобятся в продакшн-коде. -#### `#[should_panic]` — Testing Expected Failures +#### `#[should_panic]` — тестирование ожидаемых сбоев ```rust -// Test that certain conditions cause panics (like C's assert failures) +// Проверяем, что определённые условия приводят к panic (как срабатывание assert в C) #[test] #[should_panic(expected = "index out of bounds")] fn test_bounds_check() { let v = vec![1, 2, 3]; - let _ = v[10]; // Should panic + let _ = v[10]; // Должно вызвать panic } #[test] @@ -448,36 +445,36 @@ fn test_thermal_shutdown() { } ``` -#### `#[ignore]` — Slow or Hardware-Dependent Tests +#### `#[ignore]` — медленные тесты и тесты, зависящие от оборудования ```rust -// Mark tests that require special conditions (like C's #ifdef HARDWARE_TEST) +// Помечаем тесты, которым нужны особые условия (как #ifdef HARDWARE_TEST в C) #[test] #[ignore = "requires GPU hardware"] fn test_gpu_ecc_scrub() { - // This test only runs on machines with GPUs - // Run with: cargo test -- --ignored - // Run with: cargo test -- --include-ignored (runs ALL tests) + // Этот тест запускается только на машинах с GPU + // Запуск: cargo test -- --ignored + // Запуск: cargo test -- --include-ignored (запускает ВСЕ тесты) } ``` -#### Result-Returning Tests (replacing `unwrap` chains) +#### Тесты, возвращающие Result (вместо цепочек unwrap) ```rust -// Instead of many unwrap() calls that hide the actual failure: +// Вместо множества вызовов unwrap(), которые скрывают реальный сбой: #[test] fn test_config_parsing() -> Result<(), Box<dyn std::error::Error>> { let json = r#"{"hostname": "node-01", "port": 8080}"#; - let config: ServerConfig = serde_json::from_str(json)?; // ? instead of unwrap() + let config: ServerConfig = serde_json::from_str(json)?; // ? вместо unwrap() assert_eq!(config.hostname, "node-01"); assert_eq!(config.port, 8080); - Ok(()) // Test passes if we reach here without error + Ok(()) // Тест проходит, если мы дошли сюда без ошибок } ``` -#### Test Fixtures with Builder Functions +#### Фикстуры тестов с функциями-билдерами -C uses `setUp()`/`tearDown()` functions. Rust uses helper functions and `Drop`: +В C используют функции `setUp()`/`tearDown()`. В Rust — вспомогательные функции и `Drop`: ```rust struct TestFixture { @@ -500,7 +497,7 @@ impl TestFixture { impl Drop for TestFixture { fn drop(&mut self) { - // Automatic cleanup — like C's tearDown() but can't be forgotten + // Автоматическая очистка — как tearDown() в C, но её нельзя забыть вызвать let _ = std::fs::remove_dir_all(&self.temp_dir); } } @@ -508,33 +505,33 @@ impl Drop for TestFixture { #[test] fn test_with_fixture() { let fixture = TestFixture::new(); - // Use fixture.config, fixture.temp_dir... + // Используем fixture.config, fixture.temp_dir... assert!(fixture.temp_dir.exists()); - // fixture is automatically dropped here → cleanup runs + // fixture автоматически уничтожается здесь → выполняется очистка } ``` -#### Mocking Traits for Hardware Interfaces +#### Мокирование трейтов для аппаратных интерфейсов -In C, mocking hardware requires preprocessor tricks or function pointer swapping. -In Rust, traits make this natural: +В C мокирование оборудования требует хитростей препроцессора или подмены указателей на функции. +В Rust трейты делают это естественным: ```rust -// Production trait for IPMI communication +// Продакшн-трейт для связи по IPMI trait IpmiTransport { fn send_command(&self, cmd: u8, data: &[u8]) -> Result<Vec<u8>, String>; } -// Real implementation (used in production) -struct RealIpmi { /* BMC connection details */ } +// Реальная реализация (используется в продакшне) +struct RealIpmi { /* Параметры подключения к BMC */ } impl IpmiTransport for RealIpmi { fn send_command(&self, cmd: u8, data: &[u8]) -> Result<Vec<u8>, String> { - // Actually talks to BMC hardware + // Реально обращается к аппаратуре BMC todo!("Real IPMI call") } } -// Mock implementation (used in tests) +// Мок-реализация (используется в тестах) struct MockIpmi { responses: std::collections::HashMap<u8, Vec<u8>>, } @@ -546,7 +543,7 @@ impl IpmiTransport for MockIpmi { } } -// Generic function that works with both real and mock +// Обобщённая функция, которая работает и с реальной, и с мок-реализацией fn read_sensor_temperature(transport: &dyn IpmiTransport) -> Result<f64, String> { let response = transport.send_command(0x2D, &[])?; if response.len() < 2 { @@ -571,15 +568,15 @@ mod tests { #[test] fn test_short_response() { let mock = MockIpmi { responses: std::collections::HashMap::new() }; - // No response configured → error + // Ответ не настроен → ошибка assert!(read_sensor_temperature(&mock).is_err()); } } ``` -#### Property-Based Testing with `proptest` +#### Property-based тестирование с `proptest` -Instead of testing specific values, test **properties** that must always hold: +Вместо проверки конкретных значений тестируйте **свойства**, которые должны выполняться всегда: ```rust // Cargo.toml: [dev-dependencies] proptest = "1" @@ -596,7 +593,7 @@ fn format_sensor_id(id: u32) -> String { proptest! { #[test] fn roundtrip_sensor_id(id in 0u32..10000) { - // Property: format then parse should give back the original + // Свойство: форматирование, а затем разбор должны вернуть исходное значение let formatted = format_sensor_id(id); let parsed = parse_sensor_id(&formatted); prop_assert_eq!(parsed, Some(id)); @@ -604,25 +601,25 @@ proptest! { #[test] fn parse_rejects_garbage(s in "[^s].*") { - // Property: strings not starting with 's' should never parse + // Свойство: строки, не начинающиеся с 's', никогда не должны разбираться let result = parse_sensor_id(&s); prop_assert!(result.is_none()); } } ``` -#### C vs Rust Testing Comparison +#### Сравнение тестирования в C и Rust -| C Testing | Rust Equivalent | +| Тестирование в C | Аналог в Rust | |-----------|----------------| -| `CUnit`, `CMocka`, custom framework | Built-in `#[test]` + `cargo test` | -| `setUp()` / `tearDown()` | Builder function + `Drop` trait | -| `#ifdef TEST` mock functions | Trait-based dependency injection | -| `assert(x == y)` | `assert_eq!(x, y)` with auto diff output | -| Separate test executable | Same binary, conditional compilation with `#[cfg(test)]` | -| `valgrind --leak-check=full ./test` | `cargo test` (memory safe by default) + `cargo miri test` | -| Code coverage: `gcov` / `lcov` | `cargo tarpaulin` or `cargo llvm-cov` | -| Test discovery: manual registration | Automatic — any `#[test]` fn is discovered | +| `CUnit`, `CMocka`, собственный фреймворк | Встроенный `#[test]` + `cargo test` | +| `setUp()` / `tearDown()` | Функция-билдер + трейт `Drop` | +| Mock-функции через `#ifdef TEST` | Внедрение зависимостей через трейты | +| `assert(x == y)` | `assert_eq!(x, y)` с автоматическим выводом различий | +| Отдельный исполняемый файл для тестов | Тот же бинарный файл, условная компиляция через `#[cfg(test)]` | +| `valgrind --leak-check=full ./test` | `cargo test` (безопасно по памяти по умолчанию) + `cargo miri test` | +| Покрытие кода: `gcov` / `lcov` | `cargo tarpaulin` или `cargo llvm-cov` | +| Обнаружение тестов: ручная регистрация | Автоматическое — любая функция `#[test]` обнаруживается | diff --git a/c-cpp-book/src/ch09-1-error-handling-best-practices.md b/c-cpp-book/src/ch09-1-error-handling-best-practices.md index 11d0de4..c08d2a0 100644 --- a/c-cpp-book/src/ch09-1-error-handling-best-practices.md +++ b/c-cpp-book/src/ch09-1-error-handling-best-practices.md @@ -1,22 +1,22 @@ -# Rust Option and Result key takeaways +# Ключевые выводы по Option и Result в Rust -> **What you'll learn:** Idiomatic error handling patterns — safe alternatives to `unwrap()`, the `?` operator for propagation, custom error types, and when to use `anyhow` vs `thiserror` in production code. +> **Что вы узнаете:** идиоматические паттерны обработки ошибок — безопасные альтернативы `unwrap()`, оператор `?` для распространения ошибок, пользовательские типы ошибок и когда использовать `anyhow`, а когда `thiserror` в продакшн-коде. -- ```Option``` and ```Result``` are an integral part of idiomatic Rust -- **Safe alternatives to `unwrap()`**: +- ```Option``` и ```Result``` — неотъемлемая часть идиоматичного Rust +- **Безопасные альтернативы `unwrap()`**: ```rust -// Option<T> safe alternatives -let value = opt.unwrap_or(default); // Provide fallback value -let value = opt.unwrap_or_else(|| compute()); // Lazy computation for fallback -let value = opt.unwrap_or_default(); // Use Default trait implementation -let value = opt.expect("descriptive message"); // Only when panic is acceptable - -// Result<T, E> safe alternatives -let value = result.unwrap_or(fallback); // Ignore error, use fallback -let value = result.unwrap_or_else(|e| handle(e)); // Handle error, return fallback -let value = result.unwrap_or_default(); // Use Default trait +// Безопасные альтернативы для Option<T> +let value = opt.unwrap_or(default); // Запасное значение +let value = opt.unwrap_or_else(|| compute()); // Ленивое вычисление запасного значения +let value = opt.unwrap_or_default(); // Использовать реализацию трейта Default +let value = opt.expect("descriptive message"); // Только когда panic допустим + +// Безопасные альтернативы для Result<T, E> +let value = result.unwrap_or(fallback); // Игнорировать ошибку, использовать запасное значение +let value = result.unwrap_or_else(|e| handle(e)); // Обработать ошибку и вернуть запасное значение +let value = result.unwrap_or_default(); // Использовать трейт Default ``` -- **Pattern matching for explicit control**: +- **Сопоставление с образцом для явного контроля**: ```rust match some_option { Some(value) => println!("Got: {}", value), @@ -28,57 +28,57 @@ match some_result { Err(error) => log_error(error), } ``` -- **Use `?` operator for error propagation**: Short-circuit and bubble up errors +- **Используйте оператор `?` для распространения ошибок**: досрочный выход и передача ошибок наверх ```rust fn process_file(path: &str) -> Result<String, std::io::Error> { - let content = std::fs::read_to_string(path)?; // Automatically returns error + let content = std::fs::read_to_string(path)?; // Автоматически возвращает ошибку Ok(content.to_uppercase()) } ``` -- **Transformation methods**: - - `map()`: Transform the success value `Ok(T)` -> `Ok(U)` or `Some(T)` -> `Some(U)` - - `map_err()`: Transform the error type `Err(E)` -> `Err(F)` - - `and_then()`: Chain operations that can fail -- **Use in your own APIs**: Prefer `Result<T, E>` over exceptions or error codes -- **References**: [Option docs](https://doc.rust-lang.org/std/option/enum.Option.html) | [Result docs](https://doc.rust-lang.org/std/result/enum.Result.html) - -# Rust Common Pitfalls and Debugging Tips -- **Borrowing issues**: Most common beginner mistake - - "cannot borrow as mutable" -> Only one mutable reference allowed at a time - - "borrowed value does not live long enough" -> Reference outlives the data it points to - - **Fix**: Use scopes `{}` to limit reference lifetimes, or clone data when needed -- **Missing trait implementations**: "method not found" errors - - **Fix**: Add `#[derive(Debug, Clone, PartialEq)]` for common traits - - Use `cargo check` to get better error messages than `cargo run` -- **Integer overflow in debug mode**: Rust panics on overflow - - **Fix**: Use `wrapping_add()`, `saturating_add()`, or `checked_add()` for explicit behavior -- **String vs &str confusion**: Different types for different use cases - - Use `&str` for string slices (borrowed), `String` for owned strings - - **Fix**: Use `.to_string()` or `String::from()` to convert `&str` to `String` -- **Fighting the borrow checker**: Don't try to outsmart it - - **Fix**: Restructure code to work with ownership rules rather than against them - - Consider using `Rc<RefCell<T>>` for complex sharing scenarios (sparingly) - -## Error Handling Examples: Good vs Bad +- **Методы преобразования**: + - `map()`: преобразует успешное значение `Ok(T)` -> `Ok(U)` или `Some(T)` -> `Some(U)` + - `map_err()`: преобразует тип ошибки `Err(E)` -> `Err(F)` + - `and_then()`: объединяет цепочкой операции, которые могут завершиться неудачей +- **Используйте в собственных API**: предпочитайте `Result<T, E>` исключениям или кодам ошибок +- **Ссылки**: [документация Option](https://doc.rust-lang.org/std/option/enum.Option.html) | [документация Result](https://doc.rust-lang.org/std/result/enum.Result.html) + +# Частые ошибки Rust и советы по отладке +- **Проблемы с заимствованием**: самая распространённая ошибка новичков + - "cannot borrow as mutable" -> одновременно допускается только одна изменяемая ссылка + - "borrowed value does not live long enough" -> ссылка переживает данные, на которые указывает + - **Решение**: используйте области видимости `{}`, чтобы ограничить время жизни ссылок, или клонируйте данные при необходимости +- **Отсутствующие реализации трейтов**: ошибки "method not found" + - **Решение**: добавьте `#[derive(Debug, Clone, PartialEq)]` для распространённых трейтов + - Используйте `cargo check`, чтобы получать более понятные сообщения об ошибках, чем при `cargo run` +- **Переполнение целых в отладочной сборке**: Rust вызывает panic при переполнении + - **Решение**: используйте `wrapping_add()`, `saturating_add()` или `checked_add()` для явного поведения +- **Путаница между String и &str**: разные типы для разных задач + - Используйте `&str` для срезов строк (заимствованных), `String` — для владеющих строк + - **Решение**: используйте `.to_string()` или `String::from()`, чтобы преобразовать `&str` в `String` +- **Борьба с проверкой заимствований**: не пытайтесь её перехитрить + - **Решение**: перестройте код так, чтобы он работал с правилами владения, а не против них + - Для сложных случаев совместного использования рассмотрите `Rc<RefCell<T>>` (но умеренно) + +## Примеры обработки ошибок: хорошо и плохо ```rust -// [ERROR] BAD: Can panic unexpectedly +// [ОШИБКА] ПЛОХО: может неожиданно вызвать panic fn bad_config_reader() -> String { - let config = std::env::var("CONFIG_FILE").unwrap(); // Panic if not set! - std::fs::read_to_string(config).unwrap() // Panic if file missing! + let config = std::env::var("CONFIG_FILE").unwrap(); // Panic, если не задано! + std::fs::read_to_string(config).unwrap() // Panic, если файла нет! } -// [OK] GOOD: Handles errors gracefully +// [OK] ХОРОШО: обрабатывает ошибки корректно fn good_config_reader() -> Result<String, ConfigError> { let config_path = std::env::var("CONFIG_FILE") - .unwrap_or_else(|_| "default.conf".to_string()); // Fallback to default + .unwrap_or_else(|_| "default.conf".to_string()); // Запасной вариант по умолчанию let content = std::fs::read_to_string(config_path) - .map_err(ConfigError::FileRead)?; // Convert and propagate error + .map_err(ConfigError::FileRead)?; // Преобразовать и передать ошибку дальше Ok(content) } -// [OK] EVEN BETTER: With proper error types +// [OK] ЕЩЁ ЛУЧШЕ: с правильными типами ошибок use thiserror::Error; #[derive(Error, Debug)] @@ -91,14 +91,14 @@ enum ConfigError { } ``` -Let's break down what's happening here. `ConfigError` has just **two variants** — one for I/O errors and one for validation errors. This is the right starting point for most modules: +Разберём, что здесь происходит. У `ConfigError` всего **два варианта** — один для ошибок ввода-вывода и один для ошибок валидации. Это правильная отправная точка для большинства модулей: -| `ConfigError` variant | Holds | Created by | +| Вариант `ConfigError` | Содержит | Создаётся | |----------------------|-------|-----------| -| `FileRead(io::Error)` | The original I/O error | `#[from]` auto-converts via `?` | -| `Invalid { message }` | A human-readable explanation | Your validation code | +| `FileRead(io::Error)` | Исходную ошибку ввода-вывода | `#[from]` автоматически преобразует через `?` | +| `Invalid { message }` | Понятное человеку объяснение | Ваш код валидации | -Now you can write functions that return `Result<T, ConfigError>`: +Теперь можно писать функции, которые возвращают `Result<T, ConfigError>`: ```rust fn read_config(path: &str) -> Result<String, ConfigError> { @@ -112,58 +112,58 @@ fn read_config(path: &str) -> Result<String, ConfigError> { } ``` -> **🟢 Self-study checkpoint:** Before continuing, make sure you can answer: -> 1. Why does `?` on the `read_to_string` call work? (Because `#[from]` generates `impl From<io::Error> for ConfigError`) -> 2. What happens if you add a third variant `MissingKey(String)` — what code changes? (Just add the variant; existing code still compiles) +> **🟢 Контрольная точка для самостоятельного изучения:** прежде чем продолжить, убедитесь, что можете ответить: +> 1. Почему `?` после вызова `read_to_string` работает? (Потому что `#[from]` генерирует `impl From<io::Error> for ConfigError`) +> 2. Что произойдёт, если добавить третий вариант `MissingKey(String)` — какой код придётся менять? (Просто добавить вариант; существующий код по-прежнему компилируется) -## Crate-Level Error Types and Result Aliases +## Типы ошибок на уровне крейта и псевдонимы Result -As your project grows beyond a single file, you'll combine multiple module-level errors into a **crate-level error type**. This is the standard pattern in production Rust. Let's build up from the `ConfigError` above. +По мере роста проекта за пределы одного файла вы будете объединять ошибки отдельных модулей в **тип ошибок на уровне крейта**. Это стандартный паттерн в продакшн-коде на Rust. Построим его на основе `ConfigError` выше. -In real-world Rust projects, every crate (or significant module) defines its own `Error` -enum and a `Result` type alias. This is the idiomatic pattern — analogous to how in C++ -you'd define a per-library exception hierarchy and `using Result = std::expected<T, Error>`. +В реальных проектах на Rust каждый крейт (или значимый модуль) определяет собственное перечисление `Error` +и псевдоним типа `Result`. Это идиоматичный паттерн — аналогичный тому, как в C++ +вы определяли бы иерархию исключений для библиотеки и `using Result = std::expected<T, Error>`. -### The pattern +### Шаблон ```rust -// src/error.rs (or at the top of lib.rs) +// src/error.rs (или в начале lib.rs) use thiserror::Error; -/// Every error this crate can produce. +/// Все ошибки, которые может породить этот крейт. #[derive(Error, Debug)] pub enum Error { #[error("I/O error: {0}")] - Io(#[from] std::io::Error), // auto-converts via From + Io(#[from] std::io::Error), // автоматически преобразуется через From #[error("JSON parse error: {0}")] - Json(#[from] serde_json::Error), // auto-converts via From + Json(#[from] serde_json::Error), // автоматически преобразуется через From #[error("Invalid sensor id: {0}")] - InvalidSensor(u32), // domain-specific variant + InvalidSensor(u32), // специфичный для предметной области вариант #[error("Timeout after {ms} ms")] Timeout { ms: u64 }, } -/// Crate-wide Result alias — saves typing throughout the crate. +/// Псевдоним Result на уровне всего крейта — экономит набор текста во всём крейте. pub type Result<T> = core::result::Result<T, Error>; ``` -### How it simplifies every function +### Как это упрощает каждую функцию -Without the alias you'd write: +Без псевдонима вы бы писали: ```rust -// Verbose — error type repeated everywhere +// Многословно — тип ошибки повторяется везде fn read_sensor(id: u32) -> Result<f64, crate::Error> { ... } fn parse_config(path: &str) -> Result<Config, crate::Error> { ... } ``` -With the alias: +С псевдонимом: ```rust -// Clean — just `Result<T>` +// Чисто — просто `Result<T>` use crate::{Error, Result}; fn read_sensor(id: u32) -> Result<f64> { @@ -177,10 +177,10 @@ fn read_sensor(id: u32) -> Result<f64> { } ``` -The `#[from]` attribute on `Io` generates this `impl` for free: +Атрибут `#[from]` на `Io` генерирует эту реализацию бесплатно: ```rust -// Auto-generated by thiserror's #[from] +// Генерируется автоматически атрибутом #[from] из thiserror impl From<std::io::Error> for Error { fn from(source: std::io::Error) -> Self { Error::Io(source) @@ -188,13 +188,13 @@ impl From<std::io::Error> for Error { } ``` -That's what makes `?` work: when a function returns `std::io::Error` and your function -returns `Result<T>` (your alias), the compiler calls `From::from()` to convert it -automatically. +Именно это делает `?` работающим: когда функция возвращает `std::io::Error`, а ваша функция +возвращает `Result<T>` (ваш псевдоним), компилятор вызывает `From::from()`, чтобы автоматически +преобразовать ошибку. -### Composing module-level errors +### Композиция ошибок отдельных модулей -Larger crates split errors by module, then compose them at the crate root: +В больших крейтах ошибки разделяют по модулям, а затем объединяют в корне крейта: ```rust // src/config/error.rs @@ -206,10 +206,10 @@ pub enum ConfigError { InvalidValue { key: String, reason: String }, } -// src/error.rs (crate-level) +// src/error.rs (уровень крейта) #[derive(thiserror::Error, Debug)] pub enum Error { - #[error(transparent)] // delegates Display to inner error + #[error(transparent)] // делегирует Display внутренней ошибке Config(#[from] crate::config::ConfigError), #[error("I/O error: {0}")] @@ -218,7 +218,7 @@ pub enum Error { pub type Result<T> = core::result::Result<T, Error>; ``` -Callers can still match on specific config errors: +Вызывающий код по-прежнему может сопоставлять конкретные ошибки конфигурации: ```rust match result { @@ -228,14 +228,13 @@ match result { } ``` -### C++ comparison +### Сравнение с C++ -| Concept | C++ | Rust | +| Понятие | C++ | Rust | |---------|-----|------| -| Error hierarchy | `class AppError : public std::runtime_error` | `#[derive(thiserror::Error)] enum Error { ... }` | -| Return error | `std::expected<T, Error>` or `throw` | `fn foo() -> Result<T>` | -| Convert error | Manual `try/catch` + rethrow | `#[from]` + `?` — zero boilerplate | -| Result alias | `template<class T> using Result = std::expected<T, Error>;` | `pub type Result<T> = core::result::Result<T, Error>;` | -| Error message | Override `what()` | `#[error("...")]` — compiled into `Display` impl | - +| Иерархия ошибок | `class AppError : public std::runtime_error` | `#[derive(thiserror::Error)] enum Error { ... }` | +| Возврат ошибки | `std::expected<T, Error>` или `throw` | `fn foo() -> Result<T>` | +| Преобразование ошибки | Ручной `try/catch` + повторный `throw` | `#[from]` + `?` — без шаблонного кода | +| Псевдоним Result | `template<class T> using Result = std::expected<T, Error>;` | `pub type Result<T> = core::result::Result<T, Error>;` | +| Сообщение об ошибке | Переопределить `what()` | `#[error("...")]` — компилируется в реализацию `Display` | diff --git a/c-cpp-book/src/ch09-error-handling.md b/c-cpp-book/src/ch09-error-handling.md index 6e30e01..57f0cc9 100644 --- a/c-cpp-book/src/ch09-error-handling.md +++ b/c-cpp-book/src/ch09-error-handling.md @@ -1,40 +1,40 @@ -## Connecting enums to Option and Result +## Связываем перечисления с Option и Result -> **What you'll learn:** How Rust replaces null pointers with `Option<T>` and exceptions with `Result<T, E>`, and how the `?` operator makes error propagation concise. This is Rust's most distinctive pattern — errors are values, not hidden control flow. +> **Что вы узнаете:** как Rust заменяет нулевые указатели на `Option<T>`, а исключения — на `Result<T, E>`, и как оператор `?` делает распространение ошибок кратким. Это самый характерный паттерн Rust: ошибки — это значения, а не скрытый поток управления. -- Remember the `enum` type we learned earlier? Rust's `Option` and `Result` are simply enums defined in the standard library: +- Помните тип `enum`, который мы изучали раньше? `Option` и `Result` в Rust — это просто перечисления, определённые в стандартной библиотеке: ```rust -// This is literally how Option is defined in std: +// Именно так Option определён в std: enum Option<T> { - Some(T), // Contains a value - None, // No value + Some(T), // Содержит значение + None, // Значения нет } -// And Result: +// А вот Result: enum Result<T, E> { - Ok(T), // Success with value - Err(E), // Error with details + Ok(T), // Успех со значением + Err(E), // Ошибка с подробностями } ``` -- This means everything you learned about pattern matching with `match` works directly with `Option` and `Result` -- There is **no null pointer** in Rust -- `Option<T>` is the replacement, and the compiler forces you to handle the `None` case +- Это значит, что всё, что вы знаете о сопоставлении с образцом через `match`, напрямую работает с `Option` и `Result` +- В Rust **нет нулевых указателей** — их замена это `Option<T>`, и компилятор заставляет вас обработать случай `None` -### C++ Comparison: Exceptions vs Result -| **C++ Pattern** | **Rust Equivalent** | **Advantage** | +### Сравнение с C++: исключения и Result +| **Шаблон C++** | **Аналог в Rust** | **Преимущество** | |----------------|--------------------|--------------| -| `throw std::runtime_error(msg)` | `Err(MyError::Runtime(msg))` | Error in return type — can't forget to handle | -| `try { } catch (...) { }` | `match result { Ok(v) => ..., Err(e) => ... }` | No hidden control flow | -| `std::optional<T>` | `Option<T>` | Exhaustive match required — can't forget None | -| `noexcept` annotation | Default — all Rust functions are "noexcept" | Exceptions don't exist | -| `errno` / return codes | `Result<T, E>` | Type-safe, can't ignore | - -# Rust Option type -- The Rust ```Option``` type is an ```enum``` with only two variants: ```Some<T>``` and ```None``` - - The idea is that this represents a ```nullable``` type, i.e., it either contains a valid value of that type (```Some<T>```), or has no valid value (```None```) - - The ```Option``` type is used in APIs where the result of an operation either succeeds and returns a valid value or it fails (but the specific error is irrelevant). For example, consider parsing a string for an integer value +| `throw std::runtime_error(msg)` | `Err(MyError::Runtime(msg))` | Ошибка в типе возвращаемого значения — её нельзя забыть обработать | +| `try { } catch (...) { }` | `match result { Ok(v) => ..., Err(e) => ... }` | Никакого скрытого потока управления | +| `std::optional<T>` | `Option<T>` | Требуется исчерпывающее сопоставление — нельзя забыть про None | +| Аннотация `noexcept` | По умолчанию — все функции Rust «noexcept» | Исключений не существует | +| `errno` / коды возврата | `Result<T, E>` | Типобезопасно, игнорировать нельзя | + +# Тип Option в Rust +- Тип ```Option``` в Rust — это ```enum``` всего с двумя вариантами: ```Some<T>``` и ```None``` + - Идея в том, что он представляет ```nullable``` тип, то есть либо содержит корректное значение этого типа (```Some<T>```), либо не содержит значения (```None```) + - Тип ```Option``` используется в API там, где результат операции либо успешен и возвращает корректное значение, либо завершается неудачей (причина ошибки при этом неважна). Например, разбор строки в целое число ```rust fn main() { - // Returns Option<usize> + // Возвращает Option<usize> let a = "1234".find("1"); match a { Some(a) => println!("Found 1 at index {a}"), @@ -43,16 +43,16 @@ fn main() { } ``` -# Rust Option type -- Rust ```Option``` can be processed in various ways - - ```unwrap()``` panics if the ```Option<T>``` is ```None``` and returns ```T``` otherwise and it is the least preferred approach - - ```or()``` can be used to return an alternative value - - ```if let``` lets us test for ```Some<T>``` +# Работа с Option в Rust +- ```Option``` в Rust можно обрабатывать разными способами + - ```unwrap()``` вызывает panic, если ```Option<T>``` равен ```None```, и возвращает ```T``` в противном случае; это наименее предпочтительный способ + - ```or()``` позволяет вернуть альтернативное значение + - ```if let``` позволяет проверить наличие ```Some<T>``` -> **Production patterns**: See [Safe value extraction with unwrap_or](ch17-2-avoiding-unchecked-indexing.md#safe-value-extraction-with-unwrap_or) and [Functional transforms: map, map_err, find_map](ch17-2-avoiding-unchecked-indexing.md#functional-transforms-map-map_err-find_map) for real-world examples from production Rust code. +> **Промышленные шаблоны**: реальные примеры из продакшн-кода на Rust — [Безопасное извлечение значения с unwrap_or](ch17-2-avoiding-unchecked-indexing.md#безопасное-извлечение-значения-с-unwrap_or) и [Функциональные преобразования: map, map_err, find_map](ch17-2-avoiding-unchecked-indexing.md#функциональные-преобразования-map-map_err-find_map). ```rust fn main() { - // This return an Option<usize> + // Это возвращает Option<usize> let a = "1234".find("1"); println!("{a:?} {}", a.unwrap()); let a = "1234".find("5").or(Some(42)); @@ -62,14 +62,14 @@ fn main() { } else { println!("Not found in string"); } - // This will panic + // Это вызовет panic // "1234".find("5").unwrap(); } ``` -# Rust Result type -- Result is an ```enum``` type similar to ```Option``` with two variants: ```Ok<T>``` or ```Err<E>``` - - ```Result``` is used extensively in Rust APIs that can fail. The idea is that on success, functions will return a ```Ok<T>```, or they will return a specific error ```Err<T>``` +# Тип Result в Rust +- Result — это тип ```enum``` похожий на ```Option```, с двумя вариантами: ```Ok<T>``` или ```Err<E>``` + - ```Result``` широко используется в API Rust, которые могут завершиться неудачей. Идея в том, что при успехе функции вернут ```Ok<T>```, а при ошибке — конкретную ошибку ```Err<T>``` ```rust use std::num::ParseIntError; fn main() { @@ -83,21 +83,21 @@ fn main() { if let Ok(a) = "1234".parse::<i32>() { println!("Let OK {a}"); } - // This will panic + // Это вызовет panic //"1234z".parse().unwrap(); } ``` -## Option and Result: Two Sides of the Same Coin +## Option и Result: две стороны одной медали -`Option` and `Result` are deeply related — `Option<T>` is essentially `Result<T, ()>` (a result where the error carries no information): +`Option` и `Result` тесно связаны — `Option<T>` по сути является `Result<T, ()>` (результатом, в котором ошибка не несёт никакой информации): -| `Option<T>` | `Result<T, E>` | Meaning | +| `Option<T>` | `Result<T, E>` | Значение | |-------------|---------------|---------| -| `Some(value)` | `Ok(value)` | Success — value is present | -| `None` | `Err(error)` | Failure — no value (Option) or error details (Result) | +| `Some(value)` | `Ok(value)` | Успех — значение есть | +| `None` | `Err(error)` | Неудача — значения нет (Option) или подробности ошибки (Result) | -**Converting between them:** +**Преобразование между ними:** ```rust fn main() { @@ -105,23 +105,23 @@ fn main() { let res: Result<i32, &str> = opt.ok_or("value was None"); // Option → Result let res: Result<i32, &str> = Ok(42); - let opt: Option<i32> = res.ok(); // Result → Option (discards error) + let opt: Option<i32> = res.ok(); // Result → Option (отбрасывает ошибку) - // They share many of the same methods: + // У них много общих методов: // .map(), .and_then(), .unwrap_or(), .unwrap_or_else(), .is_some()/is_ok() } ``` -> **Rule of thumb**: Use `Option` when absence is normal (e.g., looking up a key). Use `Result` when failure needs explanation (e.g., file I/O, parsing). +> **Эмпирическое правило**: используйте `Option`, когда отсутствие значения — это нормально (например, поиск по ключу). Используйте `Result`, когда неудаче нужно объяснение (например, операции ввода-вывода с файлами, разбор). -# Exercise: log() function implementation with Option +# Упражнение: реализация функции log() с Option -🟢 **Starter** +🟢 **Начальный уровень** -- Implement a ```log()``` function that accepts an ```Option<&str>``` parameter. If the parameter is ```None```, it should print a default string -- The function should return a ```Result``` with ```()``` for both success and error (in this case we'll never have an error) +- Реализуйте функцию ```log()```, которая принимает параметр ```Option<&str>```. Если параметр равен ```None```, она должна выводить строку по умолчанию +- Функция должна возвращать ```Result``` с ```()``` и для успеха, и для ошибки (в этом случае ошибка никогда не возникнет) -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn log(message: Option<&str>) -> Result<(), ()> { @@ -136,11 +136,11 @@ fn main() { let _ = log(Some("System initialized")); let _ = log(None); - // Alternative using unwrap_or: + // Альтернатива с unwrap_or: let msg: Option<&str> = None; println!("LOG: {}", msg.unwrap_or("(default message)")); } -// Output: +// Вывод: // LOG: System initialized // LOG: (no message provided) // LOG: (default message) @@ -149,29 +149,29 @@ fn main() { </details> ---- -# Rust error handling - - Rust errors can be irrecoverable (fatal) or recoverable. Fatal errors result in a ``panic``` - - In general, situations that result in ```panics``` should be avoided. ```panics``` are caused by bugs in the program, including exceeding index bounds, calling ```unwrap()``` on an ```Option<None>```, etc. - - It is OK to have explicit ```panics``` for conditions that should be impossible. The ```panic!``` or ```assert!``` macros can be used for sanity checks +# Обработка ошибок в Rust + - Ошибки в Rust бывают неустранимыми (фатальными) и устранимыми. Фатальные ошибки приводят к ```panic``` + - В целом ситуаций, которые приводят к ```panic```, следует избегать. ```panic``` вызывается ошибками в программе, включая выход за границы индекса, вызов ```unwrap()``` для ```Option<None>``` и т. д. + - Явные ```panic``` допустимы для условий, которые по замыслу невозможны. Макросы ```panic!``` или ```assert!``` можно использовать для проверок здравого смысла ```rust fn main() { let x : Option<u32> = None; - // println!("{x}", x.unwrap()); // Will panic - println!("{}", x.unwrap_or(0)); // OK -- prints 0 + // println!("{x}", x.unwrap()); // Вызовет panic + println!("{}", x.unwrap_or(0)); // OK — выводит 0 let x = 41; - //assert!(x == 42); // Will panic - //panic!("Something went wrong"); // Unconditional panic + //assert!(x == 42); // Вызовет panic + //panic!("Something went wrong"); // Безусловный panic let _a = vec![0, 1]; - // println!("{}", a[2]); // Out of bounds panic; use a.get(2) which will return Option<T> + // println!("{}", a[2]); // Panic из-за выхода за границы; используйте a.get(2), который вернёт Option<T> } ``` -## Error Handling: C++ vs Rust +## Обработка ошибок: C++ и Rust -### C++ Exception-Based Error Handling Problems +### Проблемы обработки ошибок на исключениях в C++ ```cpp -// C++ error handling - exceptions create hidden control flow +// Обработка ошибок в C++ — исключения создают скрытый поток управления #include <fstream> #include <stdexcept> @@ -181,30 +181,30 @@ std::string read_config(const std::string& path) { throw std::runtime_error("Cannot open: " + path); } std::string content; - // What if getline throws? Is file properly closed? - // With RAII yes, but what about other resources? + // Что, если getline выбросит исключение? Закроется ли файл правильно? + // С RAII — да, но как насчёт других ресурсов? std::getline(file, content); - return content; // What if caller doesn't try/catch? + return content; // Что, если вызывающий код не использует try/catch? } int main() { - // ERROR: Forgot to wrap in try/catch! + // ОШИБКА: забыли обернуть в try/catch! auto config = read_config("nonexistent.txt"); - // Exception propagates silently, program crashes - // Nothing in the function signature warned us + // Исключение распространяется молча, программа падает + // В сигнатуре функции ничего не предупредило нас return 0; } ``` ```mermaid graph TD - subgraph "C++ Error Handling Issues" - CF["Function Call"] - CR["throw exception<br/>or return code"] - CIGNORE["[ERROR] Exception not caught<br/>or return code ignored"] - CCHECK["try/catch or check"] - CERROR["Hidden control flow<br/>throws not in signature"] - CERRNO["No compile-time<br/>enforcement"] + subgraph "Проблемы обработки ошибок в C++" + CF["Вызов функции"] + CR["throw исключение<br/>или код возврата"] + CIGNORE["[ОШИБКА] Исключение не перехвачено<br/>или код возврата проигнорирован"] + CCHECK["try/catch или проверка"] + CERROR["Скрытый поток управления<br/>throw не отражён в сигнатуре"] + CERRNO["Нет принудительной проверки<br/>на этапе компиляции"] CF --> CR CR --> CIGNORE @@ -212,16 +212,16 @@ graph TD CCHECK --> CERROR CERROR --> CERRNO - CPROBLEMS["[ERROR] Exceptions invisible in types<br/>[ERROR] Hidden control flow<br/>[ERROR] Easy to forget try/catch<br/>[ERROR] Exception safety is hard<br/>[ERROR] noexcept is opt-in"] + CPROBLEMS["[ОШИБКА] Исключения не видны в типах<br/>[ОШИБКА] Скрытый поток управления<br/>[ОШИБКА] Легко забыть try/catch<br/>[ОШИБКА] Безопасность исключений трудна<br/>[ОШИБКА] noexcept — опционален"] end - subgraph "Rust Result<T, E> System" - RF["Function Call"] + subgraph "Система Result<T, E> в Rust" + RF["Вызов функции"] RR["Result<T, E><br/>Ok(value) | Err(error)"] - RMUST["[OK] Must handle<br/>Compile error if ignored"] - RMATCH["Pattern matching<br/>match, if let, ?"] - RDETAIL["Detailed error info<br/>Custom error types"] - RSAFE["Type-safe<br/>No global state"] + RMUST["[OK] Нужно обработать<br/>Ошибка компиляции, если проигнорировано"] + RMATCH["Сопоставление с образцом<br/>match, if let, ?"] + RDETAIL["Подробная информация об ошибке<br/>Пользовательские типы ошибок"] + RSAFE["Типобезопасно<br/>Нет глобального состояния"] RF --> RR RR --> RMUST @@ -229,7 +229,7 @@ graph TD RMATCH --> RDETAIL RDETAIL --> RSAFE - RBENEFITS["[OK] Forced error handling<br/>[OK] Type-safe errors<br/>[OK] Detailed error info<br/>[OK] Composable with ?<br/>[OK] Zero runtime cost"] + RBENEFITS["[OK] Принудительная обработка ошибок<br/>[OK] Типобезопасные ошибки<br/>[OK] Подробная информация об ошибке<br/>[OK] Компонуется с помощью ?<br/>[OK] Нулевая стоимость во время выполнения"] end style CPROBLEMS fill:#ff6b6b,color:#000 @@ -238,37 +238,37 @@ graph TD style RMUST fill:#91e5a3,color:#000 ``` -### `Result<T, E>` Visualization +### Визуализация `Result<T, E>` ```rust -// Rust error handling - comprehensive and forced +// Обработка ошибок в Rust — полная и обязательная use std::fs::File; use std::io::Read; fn read_file_content(filename: &str) -> Result<String, std::io::Error> { - let mut file = File::open(filename)?; // ? automatically propagates errors + let mut file = File::open(filename)?; // ? автоматически распространяет ошибки let mut contents = String::new(); file.read_to_string(&mut contents)?; - Ok(contents) // Success case + Ok(contents) // Случай успеха } fn main() { match read_file_content("example.txt") { Ok(content) => println!("File content: {}", content), Err(error) => println!("Failed to read file: {}", error), - // Compiler forces us to handle both cases! + // Компилятор заставляет обработать оба случая! } } ``` ```mermaid graph TD - subgraph "Result<T, E> Flow" - START["Function starts"] + subgraph "Поток Result<T, E>" + START["Начало функции"] OP1["File::open()"] - CHECK1{{"Result check"}} + CHECK1{{"Проверка Result"}} OP2["file.read_to_string()"] - CHECK2{{"Result check"}} + CHECK2{{"Проверка Result"}} SUCCESS["Ok(contents)"] ERROR1["Err(io::Error)"] ERROR2["Err(io::Error)"] @@ -281,25 +281,25 @@ graph TD CHECK2 -->|"Ok(())"| SUCCESS CHECK2 -->|"Err(e)"| ERROR2 - ERROR1 --> PROPAGATE["? operator<br/>propagates error"] + ERROR1 --> PROPAGATE["Оператор ?<br/>распространяет ошибку"] ERROR2 --> PROPAGATE - PROPAGATE --> CALLER["Caller must<br/>handle error"] + PROPAGATE --> CALLER["Вызывающий код должен<br/>обработать ошибку"] end - subgraph "Pattern Matching Options" + subgraph "Варианты сопоставления с образцом" MATCH["match result"] IFLET["if let Ok(val) = result"] - UNWRAP["result.unwrap()<br/>[WARNING] Panics on error"] - EXPECT["result.expect(msg)<br/>[WARNING] Panics with message"] - UNWRAP_OR["result.unwrap_or(default)<br/>[OK] Safe fallback"] - QUESTION["result?<br/>[OK] Early return"] + UNWRAP["result.unwrap()<br/>[ВНИМАНИЕ] Panic при ошибке"] + EXPECT["result.expect(msg)<br/>[ВНИМАНИЕ] Panic с сообщением"] + UNWRAP_OR["result.unwrap_or(default)<br/>[OK] Безопасный запасной вариант"] + QUESTION["result?<br/>[OK] Ранний возврат"] - MATCH --> SAFE1["[OK] Handles both cases"] - IFLET --> SAFE2["[OK] Handles error case"] - UNWRAP_OR --> SAFE3["[OK] Always returns value"] - QUESTION --> SAFE4["[OK] Propagates to caller"] - UNWRAP --> UNSAFE1["[ERROR] Can panic"] - EXPECT --> UNSAFE2["[ERROR] Can panic"] + MATCH --> SAFE1["[OK] Обрабатывает оба случая"] + IFLET --> SAFE2["[OK] Обрабатывает случай ошибки"] + UNWRAP_OR --> SAFE3["[OK] Всегда возвращает значение"] + QUESTION --> SAFE4["[OK] Передаёт ошибку вызывающему"] + UNWRAP --> UNSAFE1["[ОШИБКА] Может вызвать panic"] + EXPECT --> UNSAFE2["[ОШИБКА] Может вызвать panic"] end style SUCCESS fill:#91e5a3,color:#000 @@ -313,9 +313,9 @@ graph TD style UNSAFE2 fill:#ff6b6b,color:#000 ``` -# Rust error handling -- Rust uses the ```enum Result<T, E>``` enum for recoverable error handling - - The ```Ok<T>``` variant contains the result in case of success and ```Err<E>``` contains the error +# Обработка ошибок в Rust +- Rust использует перечисление ```enum Result<T, E>``` для обработки устранимых ошибок + - Вариант ```Ok<T>``` содержит результат в случае успеха, а ```Err<E>``` содержит ошибку ```rust fn main() { let x = "1234x".parse::<u32>(); @@ -324,7 +324,7 @@ fn main() { Err(e) => println!("Parsing error {e:?}"), } let x = "1234".parse::<u32>(); - // Same as above, but with valid number + // То же, что и выше, но с корректным числом if let Ok(x) = &x { println!("Parsed number {x}") } else if let Err(e) = &x { @@ -333,13 +333,13 @@ fn main() { } ``` -# Rust error handling -- The try-operator ```?``` is a convenient short hand for the ```match``` ```Ok``` / ```Err``` pattern - - Note the method must return ```Result<T, E>``` to enable use of ```?``` - - The type for ```Result<T, E>``` can be changed. In the example below, we return the same error type (```std::num::ParseIntError```) returned by ```str::parse()``` +# Оператор ? в Rust +- Оператор ```?``` — это удобная краткая запись для шаблона ```match``` с ветками ```Ok``` / ```Err``` + - Обратите внимание: метод должен возвращать ```Result<T, E>```, чтобы можно было использовать ```?``` + - Тип ```Result<T, E>``` можно менять. В примере ниже мы возвращаем тот же тип ошибки (```std::num::ParseIntError```), который возвращает ```str::parse()``` ```rust fn double_string_number(s : &str) -> Result<u32, std::num::ParseIntError> { - let x = s.parse::<u32>()?; // Returns immediately in case of an error + let x = s.parse::<u32>()?; // Немедленно возвращает результат в случае ошибки Ok(x*2) } fn main() { @@ -350,33 +350,33 @@ fn main() { } ``` -# Rust error handling -- Errors can be mapped to other types, or to default values (https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_default) +# Преобразование ошибок в Rust +- Ошибки можно преобразовывать в другие типы или в значения по умолчанию (https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or_default) ```rust -// Changes the error type to () in case of error +// Меняет тип ошибки на () в случае ошибки fn double_string_number(s : &str) -> Result<u32, ()> { - let x = s.parse::<u32>().map_err(|_|())?; // Returns immediately in case of an error + let x = s.parse::<u32>().map_err(|_|())?; // Немедленно возвращает результат в случае ошибки Ok(x*2) } ``` ```rust fn double_string_number(s : &str) -> Result<u32, ()> { - let x = s.parse::<u32>().unwrap_or_default(); // Defaults to 0 in case of parse error + let x = s.parse::<u32>().unwrap_or_default(); // По умолчанию 0 в случае ошибки разбора Ok(x*2) } ``` ```rust fn double_optional_number(x : Option<u32>) -> Result<u32, ()> { - // ok_or converts Option<None> to Result<u32, ()> in the below - x.ok_or(()).map(|x|x*2) // .map() is applied only on Ok(u32) + // ok_or преобразует Option<None> в Result<u32, ()> в этом примере + x.ok_or(()).map(|x|x*2) // .map() применяется только к Ok(u32) } ``` -# Exercise: error handling +# Упражнение: обработка ошибок -🟡 **Intermediate** -- Implement a ```log()``` function with a single u32 parameter. If the parameter is not 42, return an error. The ```Result<>``` for success and error type is ```()``` -- Invoke ```log()``` function that exits with the same ```Result<>``` type if ```log()``` return an error. Otherwise print a message saying that log was successfully called +🟡 **Средний уровень** +- Реализуйте функцию ```log()``` с одним параметром u32. Если параметр не равен 42, верните ошибку. Тип ```Result<>``` для успеха и ошибки — ```()``` +- Вызовите функцию ```log()``` так, чтобы она сама завершалась с тем же типом ```Result<>```, если ```log()``` вернула ошибку. В противном случае выведите сообщение, что log был успешно вызван ```rust fn log(x: u32) -> ?? { @@ -384,7 +384,7 @@ fn log(x: u32) -> ?? { } fn call_log(x: u32) -> ?? { - // Call log(x), then exit immediately if it return an error + // Вызовите log(x), затем немедленно выйдите, если она вернула ошибку println!("log was successfully called"); } @@ -394,7 +394,7 @@ fn main() { } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn log(x: u32) -> Result<(), ()> { @@ -406,19 +406,18 @@ fn log(x: u32) -> Result<(), ()> { } fn call_log(x: u32) -> Result<(), ()> { - log(x)?; // Exit immediately if log() returns an error + log(x)?; // Немедленно выходим, если log() вернула ошибку println!("log was successfully called with {x}"); Ok(()) } fn main() { - let _ = call_log(42); // Prints: log was successfully called with 42 - let _ = call_log(43); // Returns Err(()), nothing printed + let _ = call_log(42); // Выводит: log was successfully called with 42 + let _ = call_log(43); // Возвращает Err(()), ничего не выводится } -// Output: +// Вывод: // log was successfully called with 42 ``` </details> - diff --git a/c-cpp-book/src/ch10-1-generics.md b/c-cpp-book/src/ch10-1-generics.md index 0edb392..36aefba 100644 --- a/c-cpp-book/src/ch10-1-generics.md +++ b/c-cpp-book/src/ch10-1-generics.md @@ -1,12 +1,12 @@ -# Rust generics +# Обобщения в Rust -> **What you'll learn:** Generic type parameters, monomorphization (zero-cost generics), trait bounds, and how Rust generics compare to C++ templates — with better error messages and no SFINAE. +> **Что вы узнаете:** обобщённые параметры типов, мономорфизацию (обобщения без накладных расходов), ограничения трейтами и то, как обобщения Rust соотносятся с шаблонами C++ — с более понятными сообщениями об ошибках и без SFINAE. -- Generics allow the same algorithm or data structure to be reused across data types - - The generic parameter appears as an identifier within ```<>```, e.g.: ```<T>```. The parameter can have any legal identifier name, but is typically kept short for brevity - - The compiler performs monomorphization at compile time, i.e., it generates a new type for every variation of ```T``` that is encountered +- Обобщения позволяют повторно использовать один и тот же алгоритм или структуру данных для разных типов данных + - Обобщённый параметр записывается как идентификатор в ```<>```, например: ```<T>```. Параметр может иметь любое допустимое имя, но обычно его делают коротким + - Компилятор выполняет мономорфизацию на этапе компиляции, то есть генерирует новый тип для каждого варианта ```T```, который встречается в коде ```rust -// Returns a tuple of type <T> composed of left and right of type <T> +// Возвращает кортеж типа <T>, составленный из left и right типа <T> fn pick<T>(x: u32, left: T, right: T) -> (T, T) { if x == 42 { (left, right) @@ -21,10 +21,10 @@ fn main() { } ``` -# Rust generics -- Generics can also be applied to data types and associated methods. It is possible to specialize the implementation for a specific ```<T>``` (example: ```f32``` vs. ```u32```) +# Обобщения в Rust: специализация +- Обобщения можно применять и к типам данных, и к ассоциированным методам. Реализацию можно специализировать для конкретного ```<T>``` (например, ```f32``` и ```u32```) ```rust -#[derive(Debug)] // We will discuss this later +#[derive(Debug)] // Мы обсудим это позже struct Point<T> { x : T, y : T, @@ -54,12 +54,12 @@ fn main() { } ``` -# Exercise: Generics +# Упражнение: обобщения -🟢 **Starter** -- Modify the ```Point``` type to use two different types (```T``` and ```U```) for x and y +🟢 **Начальный уровень** +- Измените тип ```Point```, чтобы для x и y использовались два разных типа (```T``` и ```U```) -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust #[derive(Debug)] @@ -82,7 +82,7 @@ fn main() { println!("{p2:?}"); println!("{p3:?}"); } -// Output: +// Вывод: // Point { x: 42, y: 3.14 } // Point { x: "hello", y: true } // Point { x: 1, y: 1000 } @@ -90,9 +90,9 @@ fn main() { </details> -### Combining Rust traits and generics -- Traits can be used to place restrictions on generic types (constraints) -- The constraint can be specified using a ```:``` after the generic type parameter, or using ```where```. The following defines a generic function ```get_area``` that takes any type ```T``` as long as it implements the ```ComputeArea``` ```trait``` +### Комбинирование трейтов и обобщений в Rust +- Трейты можно использовать для наложения ограничений на обобщённые типы (constraints) +- Ограничение задаётся через ```:``` после обобщённого параметра типа или через ```where```. Ниже определена обобщённая функция ```get_area```, которая принимает любой тип ```T```, если он реализует трейт ```ComputeArea``` ```rust trait ComputeArea { fn area(&self) -> u64; @@ -101,10 +101,10 @@ fn main() { t.area() } ``` -- [▶ Try it in the Rust Playground](https://play.rust-lang.org/) +- [▶ Попробуйте в Rust Playground](https://play.rust-lang.org/) -### Combining Rust traits and generics -- It is possible to have multiple trait constraints +### Комбинирование трейтов и обобщений в Rust: несколько ограничений +- Можно задать несколько ограничений трейтами ```rust trait Fish {} trait Mammal {} @@ -118,14 +118,14 @@ fn main() { let w = Whale {}; only_fish_and_mammals(&w); let _s = Shark {}; - // Won't compile + // Не скомпилируется only_fish_and_mammals(&_s); } ``` -### Rust traits constraints in data types -- Trait constraints can be combined with generics in data types -- In the following example, we define the ```PrintDescription``` ```trait``` and a generic ```struct``` ```Shape``` with a member constrained by the trait +### Ограничения трейтов в типах данных +- Ограничения трейтами можно комбинировать с обобщениями в типах данных +- В следующем примере мы определяем трейт ```PrintDescription``` и обобщённую структуру ```Shape``` с полем, ограниченным этим трейтом ```rust trait PrintDescription { fn print_description(&self); @@ -133,19 +133,19 @@ trait PrintDescription { struct Shape<S: PrintDescription> { shape: S, } -// Generic Shape implementation for any type that implements PrintDescription +// Обобщённая реализация Shape для любого типа, который реализует PrintDescription impl<S: PrintDescription> Shape<S> { fn print(&self) { self.shape.print_description(); } } ``` -- [▶ Try it in the Rust Playground](https://play.rust-lang.org/) +- [▶ Попробуйте в Rust Playground](https://play.rust-lang.org/) -# Exercise: Trait constraints and generics +# Упражнение: ограничения трейтов и обобщения -🟡 **Intermediate** -- Implement a ```struct``` with a generic member ```cipher``` that implements ```CipherText``` +🟡 **Средний уровень** +- Реализуйте структуру с обобщённым полем ```cipher```, которое реализует ```CipherText``` ```rust trait CipherText { fn encrypt(&self); @@ -154,14 +154,14 @@ trait CipherText { //struct Cipher<> ``` -- Next, implement a method called ```encrypt``` on the ```struct``` ```impl``` that invokes ```encrypt``` on ```cipher``` +- Затем реализуйте метод ```encrypt``` для структуры ```impl```, который вызывает ```encrypt``` у ```cipher``` ```rust // TO DO impl for Cipher<> {} ``` -- Next, implement ```CipherText``` on two structs called ```CipherOne``` and ```CipherTwo``` (just ```println()``` is fine). Create ```CipherOne``` and ```CipherTwo```, and use ```Cipher``` to invoke them +- Далее реализуйте ```CipherText``` для двух структур, ```CipherOne``` и ```CipherTwo``` (достаточно ```println()```). Создайте ```CipherOne``` и ```CipherTwo``` и используйте ```Cipher``` для их вызова -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust trait CipherText { @@ -199,31 +199,31 @@ fn main() { c1.encrypt(); c2.encrypt(); } -// Output: +// Вывод: // CipherOne encryption applied // CipherTwo encryption applied ``` </details> -### Rust type state pattern and generics -- Rust types can be used to enforce state machine transitions at *compile* time - - Consider a ```Drone``` with say two states: ```Idle``` and ```Flying```. In the ```Idle``` state, the only permitted method is ```takeoff()```. In the ```Flying``` state, we permit ```land()``` +### Паттерн состояний (type state) и обобщения в Rust +- Типы Rust можно использовать, чтобы обеспечить переходы конечного автомата на этапе *компиляции* + - Рассмотрим ```Drone``` с двумя состояниями: ```Idle``` и ```Flying```. В состоянии ```Idle``` единственный допустимый метод — ```takeoff()```. В состоянии ```Flying``` разрешён ```land()``` -- One approach is to model the state machine using something like the following +- Один из подходов — смоделировать конечный автомат примерно так ```rust enum DroneState { Idle, Flying } -struct Drone {x: u64, y: u64, z: u64, state: DroneState} // x, y, z are coordinates +struct Drone {x: u64, y: u64, z: u64, state: DroneState} // x, y, z — координаты ``` -- This requires a lot of runtime checks to enforce the state machine semantics — [▶ try it](https://play.rust-lang.org/) to see why +- Это требует множества проверок во время выполнения, чтобы обеспечить семантику конечного автомата — [▶ попробуйте](https://play.rust-lang.org/), чтобы увидеть почему -### Rust type state pattern generics -- Generics allows us to enforce the state machine at *compile time*. This requires using a special generic called ```PhantomData<T>``` -- The ```PhantomData<T>``` is a ```zero-sized``` marker data type. In this case, we use it to represent the ```Idle``` and ```Flying``` states, but it has ```zero``` runtime size -- Notice that the ```takeoff``` and ```land``` methods take ```self``` as a parameter. This is referred to as ```consuming``` (contrast with ```&self``` which uses borrowing). Basically, once we call the ```takeoff()``` on ```Drone<Idle>```, we can only get back a ```Drone<Flying>``` and viceversa +### Паттерн состояний на обобщениях +- Обобщения позволяют обеспечить конечный автомат на этапе *компиляции*. Для этого нужен специальный обобщённый тип ```PhantomData<T>``` +- ```PhantomData<T>``` — это маркерный тип данных ```нулевого размера```. В данном случае мы используем его для представления состояний ```Idle``` и ```Flying```, но во время выполнения он занимает ```ноль``` байт +- Обратите внимание, что методы ```takeoff``` и ```land``` принимают ```self``` как параметр. Это называется ```поглощением``` (в отличие от ```&self```, которое использует заимствование). По сути, после вызова ```takeoff()``` для ```Drone<Idle>``` мы можем получить только ```Drone<Flying>``` и наоборот ```rust struct Drone<T> {x: u64, y: u64, z: u64, state: PhantomData<T> } impl Drone<Idle> { @@ -233,19 +233,19 @@ impl Drone<Flying> { fn land(self) -> Drone<Idle> { ...} } ``` - - [▶ Try it in the Rust Playground](https://play.rust-lang.org/) + - [▶ Попробуйте в Rust Playground](https://play.rust-lang.org/) -### Rust type state pattern generics -- Key takeaways: - - States can be represented using structs (zero-size) - - We can combine the state ```T``` with ```PhantomData<T>``` (zero-size) - - Implementing the methods for a particular stage of the state machine is now just a matter of ```impl State<T>``` - - Use a method that consumes ```self``` to transition from one state to another - - This gives us ```zero cost``` abstractions. The compiler can enforce the state machine at compile time and it's impossible to call methods unless the state is right +### Паттерн состояний: ключевые выводы +- Основные выводы: + - Состояния можно представить структурами (нулевого размера) + - Состояние ```T``` можно комбинировать с ```PhantomData<T>``` (нулевого размера) + - Реализация методов для конкретного этапа конечного автомата — это просто ```impl State<T>``` + - Для перехода из одного состояния в другое используйте метод, который поглощает ```self``` + - Это даёт абстракции ```без накладных расходов```. Компилятор может обеспечить конечный автомат на этапе компиляции, и невозможно вызвать методы, если состояние неподходящее -### Rust builder pattern -- The consume ```self``` can be useful for builder patterns -- Consider a GPIO configuration with several dozen pins. The pins can be configured to high or low (default is low) +### Паттерн-строитель в Rust +- Поглощение ```self``` может быть полезным для паттерна-строителя +- Рассмотрим конфигурацию GPIO с несколькими десятками выводов. Каждый вывод может быть установлен в высокий или низкий уровень (по умолчанию — низкий) ```rust #[derive(default)] enum PinState { @@ -260,6 +260,5 @@ struct GPIOConfig { ... } ``` -- The builder pattern can be used to construct a GPIO configuration by chaining — [▶ Try it](https://play.rust-lang.org/) - +- Паттерн-строитель можно использовать, чтобы собрать конфигурацию GPIO через цепочку вызовов — [▶ Попробуйте](https://play.rust-lang.org/) diff --git a/c-cpp-book/src/ch10-traits.md b/c-cpp-book/src/ch10-traits.md index a1b6dc8..d970d69 100644 --- a/c-cpp-book/src/ch10-traits.md +++ b/c-cpp-book/src/ch10-traits.md @@ -1,9 +1,9 @@ -# Rust traits +# Трейты в Rust -> **What you'll learn:** Traits — Rust's answer to interfaces, abstract base classes, and operator overloading. You'll learn how to define traits, implement them for your types, and use dynamic dispatch (`dyn Trait`) vs static dispatch (generics). For C++ developers: traits replace virtual functions, CRTP, and concepts. For C developers: traits are the structured way Rust does polymorphism. +> **Что вы узнаете:** трейты — ответ Rust на интерфейсы, абстрактные базовые классы и перегрузку операторов. Вы научитесь определять трейты, реализовывать их для своих типов и использовать динамическую диспетчеризацию (`dyn Trait`) или статическую (обобщения). Для программистов C++: трейты заменяют виртуальные функции, CRTP и концепты. Для программистов на C: трейты — структурированный способ полиморфизма в Rust. -- Rust traits are similar to interfaces in other languages - - Traits define methods that must be defined by types that implement the trait. +- Трейты в Rust похожи на интерфейсы в других языках + - Трейты определяют методы, которые должны быть реализованы типами, реализующими трейт. ```rust fn main() { trait Pet { @@ -23,80 +23,80 @@ fn main() { } let c = Cat{}; let d = Dog{}; - c.speak(); // There is no "is a" relationship between Cat and Dog - d.speak(); // There is no "is a" relationship between Cat and Dog + c.speak(); // Между Cat и Dog нет отношения «является» + d.speak(); // Между Cat и Dog нет отношения «является» } ``` -## Traits vs C++ Concepts and Interfaces +## Трейты в сравнении с концептами и интерфейсами C++ -### Traditional C++ Inheritance vs Rust Traits +### Традиционное наследование в C++ и трейты Rust ```cpp -// C++ - Inheritance-based polymorphism +// C++ — полиморфизм на основе наследования class Animal { public: - virtual void speak() = 0; // Pure virtual function + virtual void speak() = 0; // Чисто виртуальная функция virtual ~Animal() = default; }; -class Cat : public Animal { // "Cat IS-A Animal" +class Cat : public Animal { // «Cat ЯВЛЯЕТСЯ Animal» public: void speak() override { std::cout << "Meow" << std::endl; } }; -void make_sound(Animal* animal) { // Runtime polymorphism - animal->speak(); // Virtual function call +void make_sound(Animal* animal) { // Полиморфизм времени выполнения + animal->speak(); // Вызов виртуальной функции } ``` ```rust -// Rust - Composition over inheritance with traits +// Rust — композиция вместо наследования, через трейты trait Animal { fn speak(&self); } -struct Cat; // Cat is NOT an Animal, but IMPLEMENTS Animal behavior +struct Cat; // Cat НЕ является Animal, но РЕАЛИЗУЕТ поведение Animal -impl Animal for Cat { // "Cat CAN-DO Animal behavior" +impl Animal for Cat { // «Cat УМЕЕТ поведение Animal» fn speak(&self) { println!("Meow"); } } -fn make_sound<T: Animal>(animal: &T) { // Static polymorphism - animal.speak(); // Direct function call (zero cost) +fn make_sound<T: Animal>(animal: &T) { // Статический полиморфизм + animal.speak(); // Прямой вызов функции (без накладных расходов) } ``` ```mermaid graph TD - subgraph "C++ Object-Oriented Hierarchy" - CPP_ANIMAL["Animal<br/>(Abstract base class)"] - CPP_CAT["Cat : public Animal<br/>(IS-A relationship)"] - CPP_DOG["Dog : public Animal<br/>(IS-A relationship)"] + subgraph "Объектно-ориентированная иерархия C++" + CPP_ANIMAL["Animal<br/>(абстрактный базовый класс)"] + CPP_CAT["Cat : public Animal<br/>(отношение IS-A)"] + CPP_DOG["Dog : public Animal<br/>(отношение IS-A)"] CPP_ANIMAL --> CPP_CAT CPP_ANIMAL --> CPP_DOG - CPP_VTABLE["Virtual function table<br/>(Runtime dispatch)"] - CPP_HEAP["Often requires<br/>heap allocation"] - CPP_ISSUES["[ERROR] Deep inheritance trees<br/>[ERROR] Diamond problem<br/>[ERROR] Runtime overhead<br/>[ERROR] Tight coupling"] + CPP_VTABLE["Таблица виртуальных функций<br/>(диспетчеризация во время выполнения)"] + CPP_HEAP["Часто требует<br/>выделения в куче"] + CPP_ISSUES["[ОШИБКА] Глубокие деревья наследования<br/>[ОШИБКА] Проблема ромба<br/>[ОШИБКА] Накладные расходы во время выполнения<br/>[ОШИБКА] Жёсткая связанность"] end - subgraph "Rust Trait-Based Composition" - RUST_TRAIT["trait Animal<br/>(Behavior definition)"] - RUST_CAT["struct Cat<br/>(Data only)"] - RUST_DOG["struct Dog<br/>(Data only)"] + subgraph "Композиция на трейтах в Rust" + RUST_TRAIT["trait Animal<br/>(определение поведения)"] + RUST_CAT["struct Cat<br/>(только данные)"] + RUST_DOG["struct Dog<br/>(только данные)"] - RUST_CAT -.->|"impl Animal for Cat<br/>(CAN-DO behavior)"| RUST_TRAIT - RUST_DOG -.->|"impl Animal for Dog<br/>(CAN-DO behavior)"| RUST_TRAIT + RUST_CAT -.->|"impl Animal for Cat<br/>(УМЕЕТ поведение)"| RUST_TRAIT + RUST_DOG -.->|"impl Animal for Dog<br/>(УМЕЕТ поведение)"| RUST_TRAIT - RUST_STATIC["Static dispatch<br/>(Compile-time)"] - RUST_STACK["Stack allocation<br/>possible"] - RUST_BENEFITS["[OK] No inheritance hierarchy<br/>[OK] Multiple trait impls<br/>[OK] Zero runtime cost<br/>[OK] Loose coupling"] + RUST_STATIC["Статическая диспетчеризация<br/>(на этапе компиляции)"] + RUST_STACK["Возможно размещение<br/>на стеке"] + RUST_BENEFITS["[OK] Нет иерархии наследования<br/>[OK] Несколько реализаций трейтов<br/>[OK] Нет накладных расходов во время выполнения<br/>[OK] Слабая связанность"] end style CPP_ISSUES fill:#ff6b6b,color:#000 @@ -105,59 +105,59 @@ graph TD style RUST_STATIC fill:#91e5a3,color:#000 ``` -### Trait Bounds and Generic Constraints +### Ограничения трейтов и обобщённые ограничения ```rust use std::fmt::Display; use std::ops::Add; -// C++ template equivalent (less constrained) +// Аналог на C++ с шаблонами (меньше ограничений) // template<typename T> // T add_and_print(T a, T b) { -// // No guarantee T supports + or printing -// return a + b; // Might fail at compile time +// // Нет гарантии, что T поддерживает + или вывод +// return a + b; // Может не скомпилироваться // } -// Rust - explicit trait bounds +// Rust — явные ограничения трейтами fn add_and_print<T>(a: T, b: T) -> T where T: Display + Add<Output = T> + Copy, { - println!("Adding {} + {}", a, b); // Display trait - a + b // Add trait + println!("Adding {} + {}", a, b); // Трейт Display + a + b // Трейт Add } ``` ```mermaid graph TD - subgraph "Generic Constraints Evolution" - UNCONSTRAINED["fn process<T>(data: T)<br/>[ERROR] T can be anything"] - SINGLE_BOUND["fn process<T: Display>(data: T)<br/>[OK] T must implement Display"] - MULTI_BOUND["fn process<T>(data: T)<br/>where T: Display + Clone + Debug<br/>[OK] Multiple requirements"] + subgraph "Эволюция обобщённых ограничений" + UNCONSTRAINED["fn process<T>(data: T)<br/>[ОШИБКА] T может быть чем угодно"] + SINGLE_BOUND["fn process<T: Display>(data: T)<br/>[OK] T должен реализовывать Display"] + MULTI_BOUND["fn process<T>(data: T)<br/>where T: Display + Clone + Debug<br/>[OK] Несколько требований"] UNCONSTRAINED --> SINGLE_BOUND SINGLE_BOUND --> MULTI_BOUND end - subgraph "Trait Bound Syntax" + subgraph "Синтаксис ограничений трейтов" INLINE["fn func<T: Trait>(param: T)"] WHERE_CLAUSE["fn func<T>(param: T)<br/>where T: Trait"] IMPL_PARAM["fn func(param: impl Trait)"] - COMPARISON["Inline: Simple cases<br/>Where: Complex bounds<br/>impl: Concise syntax"] + COMPARISON["Inline: простые случаи<br/>Where: сложные ограничения<br/>impl: краткий синтаксис"] end - subgraph "Compile-time Magic" - GENERIC_FUNC["Generic function<br/>with trait bounds"] - TYPE_CHECK["Compiler verifies<br/>trait implementations"] - MONOMORPH["Monomorphization<br/>(Create specialized versions)"] - OPTIMIZED["Fully optimized<br/>machine code"] + subgraph "Магия времени компиляции" + GENERIC_FUNC["Обобщённая функция<br/>с ограничениями трейтов"] + TYPE_CHECK["Компилятор проверяет<br/>реализации трейтов"] + MONOMORPH["Мономорфизация<br/>(создание специализированных версий)"] + OPTIMIZED["Полностью оптимизированный<br/>машинный код"] GENERIC_FUNC --> TYPE_CHECK TYPE_CHECK --> MONOMORPH MONOMORPH --> OPTIMIZED - EXAMPLE["add_and_print::<i32><br/>add_and_print::<f64><br/>(Separate functions generated)"] + EXAMPLE["add_and_print::<i32><br/>add_and_print::<f64><br/>(генерируются отдельные функции)"] MONOMORPH --> EXAMPLE end @@ -167,14 +167,14 @@ graph TD style OPTIMIZED fill:#91e5a3,color:#000 ``` -### C++ Operator Overloading → Rust `std::ops` Traits +### Перегрузка операторов в C++ → трейты `std::ops` в Rust -In C++, you overload operators by writing free functions or member functions with special names (`operator+`, `operator<<`, `operator[]`, etc.). In Rust, every operator maps to a trait in `std::ops` (or `std::fmt` for output). You **implement the trait** instead of writing a magic-named function. +В C++ операторы перегружают, записывая свободные функции или методы с особыми именами (`operator+`, `operator<<`, `operator[]` и т. д.). В Rust каждый оператор соответствует трейту из `std::ops` (или `std::fmt` для вывода). Вы **реализуете трейт** вместо того, чтобы писать функцию с магическим именем. -#### Side-by-side: `+` operator +#### Рядом: оператор `+` ```cpp -// C++: operator overloading as a member or free function +// C++: перегрузка оператора как метода или свободной функции struct Vec2 { double x, y; Vec2 operator+(const Vec2& rhs) const { @@ -183,7 +183,7 @@ struct Vec2 { }; Vec2 a{1.0, 2.0}, b{3.0, 4.0}; -Vec2 c = a + b; // calls a.operator+(b) +Vec2 c = a + b; // вызывает a.operator+(b) ``` ```rust @@ -193,7 +193,7 @@ use std::ops::Add; struct Vec2 { x: f64, y: f64 } impl Add for Vec2 { - type Output = Vec2; // Associated type — the result of + + type Output = Vec2; // Ассоциированный тип — результат + fn add(self, rhs: Vec2) -> Vec2 { Vec2 { x: self.x + rhs.x, y: self.y + rhs.y } } @@ -201,77 +201,77 @@ impl Add for Vec2 { let a = Vec2 { x: 1.0, y: 2.0 }; let b = Vec2 { x: 3.0, y: 4.0 }; -let c = a + b; // calls <Vec2 as Add>::add(a, b) +let c = a + b; // вызывает <Vec2 as Add>::add(a, b) println!("{c:?}"); // Vec2 { x: 4.0, y: 6.0 } ``` -#### Key differences from C++ +#### Ключевые отличия от C++ -| Aspect | C++ | Rust | +| Аспект | C++ | Rust | |--------|-----|------| -| **Mechanism** | Magic function names (`operator+`) | Implement a trait (`impl Add for T`) | -| **Discovery** | Grep for `operator+` or read the header | Look at trait impls — IDE support excellent | -| **Return type** | Free choice | Fixed by the `Output` associated type | -| **Receiver** | Usually takes `const T&` (borrows) | Takes `self` by value (moves!) by default | -| **Symmetry** | Can write `impl operator+(int, Vec2)` | Must add `impl Add<Vec2> for i32` (foreign trait rules apply) | -| **`<<` for printing** | `operator<<(ostream&, T)` — overload for *any* stream | `impl fmt::Display for T` — one canonical `to_string` representation | +| **Механизм** | Магические имена функций (`operator+`) | Реализация трейта (`impl Add for T`) | +| **Поиск** | Grep по `operator+` или чтение заголовка | Смотрите реализации трейтов — отличная поддержка в IDE | +| **Тип результата** | Выбирается свободно | Фиксируется ассоциированным типом `Output` | +| **Получатель** | Обычно принимает `const T&` (заимствование) | По умолчанию принимает `self` по значению (перемещение!) | +| **Симметрия** | Можно написать `impl operator+(int, Vec2)` | Нужно добавить `impl Add<Vec2> for i32` (действуют правила для чужих трейтов) | +| **`<<` для вывода** | `operator<<(ostream&, T)` — перегрузка для *любого* потока | `impl fmt::Display for T` — одно каноническое представление `to_string` | -#### The `self` by value gotcha +#### Ловушка `self` по значению -In Rust, `Add::add(self, rhs)` takes `self` **by value**. For `Copy` types (like `Vec2` above, which derives `Copy`) this is fine — the compiler copies. But for non-`Copy` types, `+` **consumes** the operands: +В Rust `Add::add(self, rhs)` принимает `self` **по значению**. Для типов `Copy` (как `Vec2` выше, у которого есть `derive(Copy)`) это нормально — компилятор копирует. Но для типов, не реализующих `Copy`, `+` **поглощает** операнды: ```rust let s1 = String::from("hello "); let s2 = String::from("world"); -let s3 = s1 + &s2; // s1 is MOVED into s3! -// println!("{s1}"); // ❌ Compile error: value used after move -println!("{s2}"); // ✅ s2 was only borrowed (&s2) +let s3 = s1 + &s2; // s1 ПЕРЕМЕЩЁН в s3! +// println!("{s1}"); // ❌ Ошибка компиляции: значение использовано после перемещения +println!("{s2}"); // ✅ s2 была только заимствована (&s2) ``` -This is why `String + &str` works but `&str + &str` does not — `Add` is only implemented for `String + &str`, consuming the left-hand `String` to reuse its buffer. This has no C++ analogue: `std::string::operator+` always creates a new string. +Именно поэтому `String + &str` работает, а `&str + &str` — нет: `Add` реализован только для `String + &str`, и он поглощает левую `String`, чтобы переиспользовать её буфер. В C++ этому нет аналога: `std::string::operator+` всегда создаёт новую строку. -#### Full mapping: C++ operators → Rust traits +#### Полное соответствие: операторы C++ → трейты Rust -| C++ Operator | Rust Trait | Notes | +| Оператор C++ | Трейт Rust | Примечания | |-------------|-----------|-------| -| `operator+` | `std::ops::Add` | `Output` associated type | +| `operator+` | `std::ops::Add` | Ассоциированный тип `Output` | | `operator-` | `std::ops::Sub` | | -| `operator*` | `std::ops::Mul` | Not pointer deref — that's `Deref` | +| `operator*` | `std::ops::Mul` | Не разыменование указателя — это `Deref` | | `operator/` | `std::ops::Div` | | | `operator%` | `std::ops::Rem` | | -| `operator-` (unary) | `std::ops::Neg` | | -| `operator!` / `operator~` | `std::ops::Not` | Rust uses `!` for both logical and bitwise NOT (no `~` operator) | +| `operator-` (унарный) | `std::ops::Neg` | | +| `operator!` / `operator~` | `std::ops::Not` | Rust использует `!` и для логического, и для побитового НЕ (оператора `~` нет) | | `operator&`, `\|`, `^` | `BitAnd`, `BitOr`, `BitXor` | | -| `operator<<`, `>>` (shift) | `Shl`, `Shr` | NOT stream I/O! | -| `operator+=` | `std::ops::AddAssign` | Takes `&mut self` (not `self`) | -| `operator[]` | `std::ops::Index` / `IndexMut` | Returns `&Output` / `&mut Output` | -| `operator()` | `Fn` / `FnMut` / `FnOnce` | Closures implement these; you cannot `impl Fn` directly | -| `operator==` | `PartialEq` (+ `Eq`) | In `std::cmp`, not `std::ops` | -| `operator<` | `PartialOrd` (+ `Ord`) | In `std::cmp` | -| `operator<<` (stream) | `fmt::Display` | `println!("{}", x)` | -| `operator<<` (debug) | `fmt::Debug` | `println!("{:?}", x)` | -| `operator bool` | No direct equivalent | Use `impl From<T> for bool` or a named method like `.is_empty()` | -| `operator T()` (implicit conversion) | No implicit conversions | Use `From`/`Into` traits (explicit) | - -#### Guardrails: what Rust prevents - -1. **No implicit conversions**: C++ `operator int()` can cause silent, surprising casts. Rust has no implicit conversion operators — use `From`/`Into` and call `.into()` explicitly. -2. **No overloading `&&` / `||`**: C++ allows it (breaking short-circuit semantics!). Rust does not. -3. **No overloading `=`**: Assignment is always a move or copy, never user-defined. Compound assignment (`+=`) IS overloadable via `AddAssign`, etc. -4. **No overloading `,`**: C++ allows `operator,()` — one of the most infamous C++ footguns. Rust does not. -5. **No overloading `&` (address-of)**: Another C++ footgun (`std::addressof` exists to work around it). Rust's `&` always means "borrow." -6. **Coherence rules**: You can only implement `Add<Foreign>` for your own type, or `Add<YourType>` for a foreign type — never `Add<Foreign>` for `Foreign`. This prevents conflicting operator definitions across crates. - -> **Bottom line**: In C++, operator overloading is powerful but largely unregulated — you can overload almost anything, including comma and address-of, and implicit conversions can trigger silently. Rust gives you the same expressiveness for arithmetic and comparison operators via traits, but **blocks the historically dangerous overloads** and forces all conversions to be explicit. +| `operator<<`, `>>` (сдвиг) | `Shl`, `Shr` | НЕ потоковый ввод-вывод! | +| `operator+=` | `std::ops::AddAssign` | Принимает `&mut self` (а не `self`) | +| `operator[]` | `std::ops::Index` / `IndexMut` | Возвращает `&Output` / `&mut Output` | +| `operator()` | `Fn` / `FnMut` / `FnOnce` | Замыкания реализуют их; напрямую `impl Fn` написать нельзя | +| `operator==` | `PartialEq` (+ `Eq`) | В `std::cmp`, а не в `std::ops` | +| `operator<` | `PartialOrd` (+ `Ord`) | В `std::cmp` | +| `operator<<` (поток) | `fmt::Display` | `println!("{}", x)` | +| `operator<<` (отладка) | `fmt::Debug` | `println!("{:?}", x)` | +| `operator bool` | Прямого аналога нет | Используйте `impl From<T> for bool` или именованный метод вроде `.is_empty()` | +| `operator T()` (неявное преобразование) | Неявных преобразований нет | Используйте трейты `From`/`Into` (явные) | + +#### Ограничители: что запрещает Rust + +1. **Нет неявных преобразований**: `operator int()` в C++ может вызывать тихие и неожиданные приведения. В Rust нет операторов неявного преобразования — используйте `From`/`Into` и вызывайте `.into()` явно. +2. **Нельзя перегружать `&&` / `||`**: C++ это позволяет (и ломает семантику короткого замыкания!). Rust — нет. +3. **Нельзя перегружать `=`**: присваивание всегда является перемещением или копированием, никогда не определяется пользователем. Составное присваивание (`+=`) перегружается через `AddAssign` и т. д. +4. **Нельзя перегружать `,`**: C++ позволяет `operator,()` — один из самых печально известных подводных камней C++. Rust — нет. +5. **Нельзя перегружать `&` (взятие адреса)**: ещё один подводный камень C++ (для обхода существует `std::addressof`). Оператор `&` в Rust всегда означает «заимствование». +6. **Правила согласованности (coherence)**: можно реализовать `Add<Foreign>` только для собственного типа или `Add<YourType>` для чужого типа — но никогда `Add<Foreign>` для `Foreign`. Это предотвращает конфликтующие определения операторов между крейтами. + +> **Главное**: в C++ перегрузка операторов мощна, но в основном не регламентирована — можно перегружать почти что угодно, включая запятую и взятие адреса, а неявные преобразования могут срабатывать незаметно. Rust даёт ту же выразительность для арифметических и операторов сравнения через трейты, но **блокирует исторически опасные перегрузки** и требует, чтобы все преобразования были явными. ---- -# Rust traits -- Rust allows implementing a user defined trait on even built-in types like u32 in this example. However, either the trait or the type must belong to the crate +# Трейты в Rust (реализация для встроенных типов) +- Rust позволяет реализовать пользовательский трейт даже для встроенных типов, например u32, как в этом примере. Однако либо трейт, либо тип должен принадлежать крейту ```rust trait IsSecret { fn is_secret(&self); } -// The IsSecret trait belongs to the crate, so we are OK +// Трейт IsSecret принадлежит нашему крейту, поэтому всё в порядке impl IsSecret for u32 { fn is_secret(&self) { if *self == 42 { @@ -287,24 +287,24 @@ fn main() { ``` -# Rust traits -- Traits support interface inheritance and default implementations +# Трейты в Rust (наследование интерфейсов) +- Трейты поддерживают наследование интерфейсов и реализации по умолчанию ```rust trait Animal { - // Default implementation + // Реализация по умолчанию fn is_mammal(&self) -> bool { true } } trait Feline : Animal { - // Default implementation + // Реализация по умолчанию fn is_feline(&self) -> bool { true } } struct Cat; -// Use default implementations. Note that all traits for the supertrait must be individually implemented +// Используем реализации по умолчанию. Обратите внимание: все надтрейты нужно реализовать отдельно impl Feline for Cat {} impl Animal for Cat {} fn main() { @@ -313,14 +313,14 @@ fn main() { } ``` ---- -# Exercise: Logger trait implementation +# Упражнение: реализация трейта Logger -🟡 **Intermediate** +🟡 **Средний уровень** -- Implement a ```Log trait``` with a single method called log() that accepts a u64 - - Implement two different loggers ```SimpleLogger``` and ```ComplexLogger``` that implement the ```Log trait```. One should output "Simple logger" with the ```u64``` and the other should output "Complex logger" with the ```u64``` +- Реализуйте трейт ```Log``` с единственным методом log(), который принимает u64 + - Реализуйте два разных логгера ```SimpleLogger``` и ```ComplexLogger```, которые реализуют трейт ```Log```. Один должен выводить "Simple logger" вместе с ```u64```, а другой — "Complex logger" вместе с ```u64``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust trait Log { @@ -348,7 +348,7 @@ fn main() { simple.log(42); complex.log(42); } -// Output: +// Вывод: // Simple logger: 42 // Complex logger: 42 (hex: 0x2a, binary: 101010) ``` @@ -356,7 +356,7 @@ fn main() { </details> ---- -# Rust trait associated types +# Ассоциированные типы трейтов в Rust ```rust #[derive(Debug)] struct Small(u32); @@ -379,8 +379,8 @@ fn main() { } ``` -# Rust trait impl -- ```impl``` can be used with traits to accept any type that implements a trait +# Реализация трейтов через impl в Rust +- ```impl``` можно использовать вместе с трейтами, чтобы принимать любой тип, который реализует трейт ```rust trait Pet { fn speak(&self); @@ -404,8 +404,8 @@ fn main() { } ``` -# Rust trait impl -- ```impl``` can be also be used be used in a return value +# Реализация трейтов через impl: возвращаемые значения +- ```impl``` также можно использовать в возвращаемом значении ```rust trait Pet {} struct Dog; @@ -426,8 +426,8 @@ fn main() { } ``` ---- -# Rust dynamic traits -- Dynamic traits can be used to invoke the trait functionality without knowing the underlying type. This is known as ```type erasure``` +# Динамические трейты в Rust +- Динамические трейты позволяют вызывать функциональность трейта, не зная конкретного типа. Это называется ```стирание типа``` (type erasure) ```rust trait Pet { fn speak(&self); @@ -452,15 +452,15 @@ fn main() { ``` ---- -## Choosing Between `impl Trait`, `dyn Trait`, and Enums +## Выбор между `impl Trait`, `dyn Trait` и перечислениями -These three approaches all achieve polymorphism but with different trade-offs: +Все три подхода дают полиморфизм, но с разными компромиссами: -| Approach | Dispatch | Performance | Heterogeneous collections? | When to use | +| Подход | Диспетчеризация | Производительность | Разнородные коллекции? | Когда использовать | |----------|----------|-------------|---------------------------|-------------| -| `impl Trait` / generics | Static (monomorphized) | Zero-cost — inlined at compile time | No — each slot has one concrete type | Default choice. Function arguments, return types | -| `dyn Trait` | Dynamic (vtable) | Small overhead per call (~1 pointer indirection) | Yes — `Vec<Box<dyn Trait>>` | When you need mixed types in a collection, or plugin-style extensibility | -| `enum` | Match | Zero-cost — known variants at compile time | Yes — but only known variants | When the set of variants is **closed** and known at compile time | +| `impl Trait` / обобщения | Статическая (мономорфизация) | Без накладных расходов — встраивается на этапе компиляции | Нет — каждый слот имеет один конкретный тип | Выбор по умолчанию. Аргументы функций, возвращаемые типы | +| `dyn Trait` | Динамическая (vtable) | Небольшие накладные расходы на вызов (~одно косвенное обращение через указатель) | Да — `Vec<Box<dyn Trait>>` | Когда нужны смешанные типы в коллекции или расширяемость в стиле плагинов | +| `enum` | Сопоставление с образцом | Без накладных расходов — варианты известны на этапе компиляции | Да — но только известные варианты | Когда набор вариантов **закрыт** и известен на этапе компиляции | ```rust trait Shape { @@ -471,13 +471,13 @@ struct Rect { w: f64, h: f64 } impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Shape for Rect { fn area(&self) -> f64 { self.w * self.h } } -// Static dispatch — compiler generates separate code for each type +// Статическая диспетчеризация — компилятор генерирует отдельный код для каждого типа fn print_area(s: &impl Shape) { println!("{}", s.area()); } -// Dynamic dispatch — one function, works with any Shape behind a pointer +// Динамическая диспетчеризация — одна функция, работает с любым Shape за указателем fn print_area_dyn(s: &dyn Shape) { println!("{}", s.area()); } -// Enum — closed set, no trait needed +// Перечисление — закрытый набор, трейт не нужен enum ShapeEnum { Circle(f64), Rect(f64, f64) } impl ShapeEnum { fn area(&self) -> f64 { @@ -489,7 +489,7 @@ impl ShapeEnum { } ``` -> **For C++ developers:** `impl Trait` is like C++ templates (monomorphized, zero-cost). `dyn Trait` is like C++ virtual functions (vtable dispatch). Rust enums with `match` are like `std::variant` with `std::visit` — but exhaustive matching is enforced by the compiler. +> **Для программистов C++:** `impl Trait` похож на шаблоны C++ (мономорфизация, без накладных расходов). `dyn Trait` похож на виртуальные функции C++ (диспетчеризация через vtable). Перечисления Rust с `match` похожи на `std::variant` с `std::visit` — но исчерпывающее сопоставление обеспечивает компилятор. -> **Rule of thumb**: Start with `impl Trait` (static dispatch). Reach for `dyn Trait` only when you need heterogeneous collections or can't know the concrete type at compile time. Use `enum` when you own all the variants. +> **Эмпирическое правило**: начинайте с `impl Trait` (статическая диспетчеризация). Прибегайте к `dyn Trait`, только когда нужны разнородные коллекции или когда конкретный тип неизвестен на этапе компиляции. Используйте `enum`, когда вы владеете всеми вариантами. diff --git a/c-cpp-book/src/ch11-from-and-into-traits.md b/c-cpp-book/src/ch11-from-and-into-traits.md index 6a4c2c4..6852155 100644 --- a/c-cpp-book/src/ch11-from-and-into-traits.md +++ b/c-cpp-book/src/ch11-from-and-into-traits.md @@ -1,31 +1,31 @@ -# Rust From and Into traits +# Трейты From и Into в Rust -> **What you'll learn:** Rust's type conversion traits — `From<T>` and `Into<T>` for infallible conversions, `TryFrom` and `TryInto` for fallible ones. Implement `From` and get `Into` for free. Replaces C++ conversion operators and constructors. +> **Что вы узнаете:** трейты преобразования типов в Rust — `From<T>` и `Into<T>` для преобразований, которые не могут завершиться ошибкой, и `TryFrom` и `TryInto` для преобразований, которые могут. Реализуйте `From` и получите `Into` бесплатно. Заменяют операторы преобразования и конструкторы C++. -- ```From``` and ```Into``` are complementary traits to facilitate type conversion -- Types normally implement the ```From``` trait.```String::from("Rust")``` converts a ```&str``` into a ```String```. -When ```From<T> for U``` exists, Rust also provides ```Into<U> for T```, so ```let s: String = "Rust".into();``` works too. +- ```From``` и ```Into``` — взаимодополняющие трейты для преобразования типов +- Обычно типы реализуют трейт ```From```. ```String::from("Rust")``` преобразует ```&str``` в ```String```. +Когда существует ```From<T> for U```, Rust также предоставляет ```Into<U> for T```, поэтому ```let s: String = "Rust".into();``` тоже работает. ```rust struct Point {x: u32, y: u32} -// Construct a Point from a tuple +// Создаём Point из кортежа impl From<(u32, u32)> for Point { fn from(xy : (u32, u32)) -> Self { - Point {x : xy.0, y: xy.1} // Construct Point using the tuple elements + Point {x : xy.0, y: xy.1} // Создаём Point из элементов кортежа } } fn main() { let s = String::from("Rust"); let x = u32::from(true); let p = Point::from((40, 42)); - // let p : Point = (40,42).into(); // Alternate form of the above + // let p : Point = (40,42).into(); // Альтернативная запись выражения выше println!("s: {s} x:{x} p.x:{} p.y {}", p.x, p.y); } ``` -# Exercise: From and Into -- Implement a ```From``` trait for ```Point``` to convert into a type called ```TransposePoint```. ```TransposePoint``` swaps the ```x``` and ```y``` elements of ```Point``` +# Упражнение: From и Into +- Реализуйте трейт ```From``` для ```Point``` с преобразованием в тип ```TransposePoint```. ```TransposePoint``` меняет местами элементы ```x``` и ```y``` у ```Point``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust struct Point { x: u32, y: u32 } @@ -42,21 +42,21 @@ fn main() { let tp = TransposePoint::from(p); println!("Transposed: x={}, y={}", tp.x, tp.y); // x=20, y=10 - // Using .into() — works automatically when From is implemented + // Через .into() — работает автоматически, когда реализован From let p2 = Point { x: 3, y: 7 }; let tp2: TransposePoint = p2.into(); println!("Transposed: x={}, y={}", tp2.x, tp2.y); // x=7, y=3 } -// Output: +// Вывод: // Transposed: x=20, y=10 // Transposed: x=7, y=3 ``` </details> -# Rust Default trait -- ```Default``` can be used to implement default values for a type - - Types can use the ```Derive``` macro with ```Default``` or provide a custom implementation +# Трейт Default в Rust +- ```Default``` можно использовать, чтобы задать значения по умолчанию для типа + - Типы могут использовать макрос ```Derive``` с ```Default``` или предоставить собственную реализацию ```rust #[derive(Default, Debug)] struct Point {x: u32, y: u32} @@ -68,17 +68,17 @@ impl Default for CustomPoint { } } fn main() { - let x = Point::default(); // Creates a Point{0, 0} + let x = Point::default(); // Создаёт Point{0, 0} println!("{x:?}"); let y = CustomPoint::default(); println!("{y:?}"); } ``` -### Rust Default trait -- ```Default``` trait has several use cases including - - Performing a partial copy and using default initialization for rest - - Default alternative for ```Option``` types in methods like ```unwrap_or_default()``` +### Применение трейта Default +- Трейт ```Default``` применяется в нескольких случаях, в том числе + - Частичное копирование с инициализацией остальных полей значениями по умолчанию + - Значение по умолчанию для типов ```Option``` в методах вроде ```unwrap_or_default()``` ```rust #[derive(Debug)] struct CustomPoint {x: u32, y: u32} @@ -89,31 +89,30 @@ impl Default for CustomPoint { } fn main() { let x = CustomPoint::default(); - // Override y, but leave rest of elements as the default + // Переопределяем y, а остальные поля оставляем по умолчанию let y = CustomPoint {y: 43, ..CustomPoint::default()}; println!("{x:?} {y:?}"); let z : Option<CustomPoint> = None; - // Try changing the unwrap_or_default() to unwrap() + // Попробуйте заменить unwrap_or_default() на unwrap() println!("{:?}", z.unwrap_or_default()); } ``` -### Other Rust type conversions -- Rust doesn't support implicit type conversions and ```as``` can be used for ```explicit``` conversions -- ```as``` should be sparingly used because it's subject to loss of data by narrowing and so forth. In general, it's preferable to use ```into()``` or ```from()``` where possible +### Другие преобразования типов в Rust +- Rust не поддерживает неявных преобразований типов, а для ```явных``` преобразований используется ```as``` +- ```as``` следует использовать с осторожностью, потому что он может приводить к потере данных при сужении и т. п. В общем случае предпочтительнее использовать ```into()``` или ```from()```, где это возможно ```rust fn main() { let f = 42u8; - // let g : u32 = f; // Will not compile - let g = f as u32; // Ok, but not preferred. Subject to rules around narrowing - let g : u32 = f.into(); // Most preferred form; infallible and checked by the compiler - // let k : u8 = g.into(); // Fails to compile; narrowing can result in loss of data + // let g : u32 = f; // Не скомпилируется + let g = f as u32; // Работает, но не рекомендуется. Подчиняется правилам сужения + let g : u32 = f.into(); // Предпочтительная форма; не может завершиться ошибкой, проверяется компилятором + // let k : u8 = g.into(); // Не скомпилируется; сужение может привести к потере данных - // Attempting a narrowing operation requires use of try_into + // Для операции сужения нужно использовать try_into if let Ok(k) = TryInto::<u8>::try_into(g) { println!("{k}"); } } ``` - diff --git a/c-cpp-book/src/ch12-1-iterator-power-tools.md b/c-cpp-book/src/ch12-1-iterator-power-tools.md index b5d38ad..d05b992 100644 --- a/c-cpp-book/src/ch12-1-iterator-power-tools.md +++ b/c-cpp-book/src/ch12-1-iterator-power-tools.md @@ -1,52 +1,52 @@ -## Iterator Power Tools Reference - -> **What you'll learn:** Advanced iterator combinators beyond `filter`/`map`/`collect` — `enumerate`, `zip`, `chain`, `flat_map`, `scan`, `windows`, and `chunks`. Essential for replacing C-style indexed `for` loops with safe, expressive Rust iterators. - -The basic `filter`/`map`/`collect` chain covers many cases, but Rust's iterator library -is far richer. This section covers the tools you'll reach for daily — especially when -translating C loops that manually track indices, accumulate results, or process -data in fixed-size chunks. - -### Quick Reference Table - -| Method | C Equivalent | What it does | Returns | -|--------|-------------|-------------|---------| -| `enumerate()` | `for (int i=0; ...)` | Pairs each element with its index | `(usize, T)` | -| `zip(other)` | Parallel arrays with same index | Pairs elements from two iterators | `(A, B)` | -| `chain(other)` | Process array1 then array2 | Concatenates two iterators | `T` | -| `flat_map(f)` | Nested loops | Maps then flattens one level | `U` | -| `windows(n)` | `for (int i=0; i<len-n+1; i++) &arr[i..i+n]` | Overlapping slices of size `n` | `&[T]` | -| `chunks(n)` | Process `n` elements at a time | Non-overlapping slices of size `n` | `&[T]` | -| `fold(init, f)` | `int acc = init; for (...) acc = f(acc, x);` | Reduce to single value | `Acc` | -| `scan(init, f)` | Running accumulator with output | Like `fold` but yields intermediate results | `Option<B>` | -| `take(n)` / `skip(n)` | Start loop at offset / limit | First `n` / skip first `n` elements | `T` | -| `take_while(f)` / `skip_while(f)` | `while (pred) {...}` | Take/skip while predicate holds | `T` | -| `peekable()` | Lookahead with `arr[i+1]` | Allows `.peek()` without consuming | `T` | -| `step_by(n)` | `for (i=0; i<len; i+=n)` | Take every nth element | `T` | -| `unzip()` | Split parallel arrays | Collect pairs into two collections | `(A, B)` | -| `sum()` / `product()` | Accumulate sum/product | Reduce with `+` or `*` | `T` | -| `min()` / `max()` | Find extremes | Return `Option<T>` | `Option<T>` | -| `any(f)` / `all(f)` | `bool found = false; for (...) ...` | Short-circuit boolean search | `bool` | -| `position(f)` | `for (i=0; ...) if (pred) return i;` | Index of first match | `Option<usize>` | - -### `enumerate` — Index + Value (replaces C index loops) +## Справочник по инструментам для работы с итераторами + +> **Что вы узнаете:** продвинутые комбинаторы итераторов помимо `filter`/`map`/`collect` — `enumerate`, `zip`, `chain`, `flat_map`, `scan`, `windows` и `chunks`. Они необходимы, чтобы заменить индексные циклы `for` в стиле C безопасными и выразительными итераторами Rust. + +Базовой цепочки `filter`/`map`/`collect` хватает для многих случаев, но библиотека итераторов Rust +гораздо богаче. Этот раздел описывает инструменты, которыми вы будете пользоваться каждый день — особенно при +переводе циклов C, которые вручную отслеживают индексы, накапливают результаты или обрабатывают +данные фиксированными порциями. + +### Краткая таблица + +| Метод | Аналог в C | Что делает | Возвращает | +|-------|-------------|-------------|---------| +| `enumerate()` | `for (int i=0; ...)` | Сопоставляет каждый элемент с его индексом | `(usize, T)` | +| `zip(other)` | Параллельные массивы с одним индексом | Попарно объединяет элементы двух итераторов | `(A, B)` | +| `chain(other)` | Обработать массив1, затем массив2 | Конкатенирует два итератора | `T` | +| `flat_map(f)` | Вложенные циклы | Отображает и выравнивает на один уровень | `U` | +| `windows(n)` | `for (int i=0; i<len-n+1; i++) &arr[i..i+n]` | Перекрывающиеся срезы размера `n` | `&[T]` | +| `chunks(n)` | Обрабатывать по `n` элементов за раз | Непересекающиеся срезы размера `n` | `&[T]` | +| `fold(init, f)` | `int acc = init; for (...) acc = f(acc, x);` | Свести к одному значению | `Acc` | +| `scan(init, f)` | Накапливающий аккумулятор с выводом | Как `fold`, но выдаёт промежуточные результаты | `Option<B>` | +| `take(n)` / `skip(n)` | Начать цикл со смещения / ограничить | Первые `n` / пропустить первые `n` элементов | `T` | +| `take_while(f)` / `skip_while(f)` | `while (pred) {...}` | Брать/пропускать, пока предикат истинен | `T` | +| `peekable()` | Заглядывание вперёд через `arr[i+1]` | Позволяет `.peek()` без потребления элемента | `T` | +| `step_by(n)` | `for (i=0; i<len; i+=n)` | Брать каждый n-й элемент | `T` | +| `unzip()` | Разбиение параллельных массивов | Собрать пары в две коллекции | `(A, B)` | +| `sum()` / `product()` | Накопление суммы/произведения | Свести через `+` или `*` | `T` | +| `min()` / `max()` | Поиск экстремумов | Возвращают `Option<T>` | `Option<T>` | +| `any(f)` / `all(f)` | `bool found = false; for (...) ...` | Булев поиск с коротким замыканием | `bool` | +| `position(f)` | `for (i=0; ...) if (pred) return i;` | Индекс первого совпадения | `Option<usize>` | + +### `enumerate` — индекс и значение (заменяет индексные циклы C) ```rust fn main() { let sensors = ["GPU_TEMP", "CPU_TEMP", "FAN_RPM", "PSU_WATT"]; - // C style: for (int i = 0; i < 4; i++) printf("[%d] %s\n", i, sensors[i]); + // В стиле C: for (int i = 0; i < 4; i++) printf("[%d] %s\n", i, sensors[i]); for (i, name) in sensors.iter().enumerate() { println!("[{i}] {name}"); } - // Find the index of a specific sensor + // Найти индекс конкретного датчика let gpu_idx = sensors.iter().position(|&s| s == "GPU_TEMP"); println!("GPU sensor at index: {gpu_idx:?}"); // Some(0) } ``` -### `zip` — Parallel Iteration (replaces parallel array loops) +### `zip` — параллельный перебор (заменяет циклы по параллельным массивам) ```rust fn main() { @@ -62,27 +62,27 @@ fn main() { } ``` -### `chain` — Concatenate Iterators +### `chain` — конкатенация итераторов ```rust fn main() { let critical = vec!["ECC error", "Thermal shutdown"]; let warnings = vec!["Link degraded", "Fan slow"]; - // Process all events in priority order + // Обрабатываем все события в порядке приоритета let all_events: Vec<_> = critical.iter().chain(warnings.iter()).collect(); println!("{all_events:?}"); // ["ECC error", "Thermal shutdown", "Link degraded", "Fan slow"] } ``` -### `flat_map` — Flatten Nested Results +### `flat_map` — выравнивание вложенных результатов ```rust fn main() { let lines = vec!["gpu:42:ok", "nic:99:fail", "cpu:7:ok"]; - // Extract all numeric values from colon-separated lines + // Извлекаем все числовые значения из строк, разделённых двоеточиями let numbers: Vec<u32> = lines.iter() .flat_map(|line| line.split(':')) .filter_map(|token| token.parse::<u32>().ok()) @@ -91,41 +91,41 @@ fn main() { } ``` -### `windows` and `chunks` — Sliding and Fixed-Size Groups +### `windows` и `chunks` — скользящие и фиксированные группы ```rust fn main() { let temps = [65, 68, 72, 71, 75, 80, 78, 76]; - // windows(3): overlapping groups of 3 (like a sliding average) + // windows(3): перекрывающиеся группы по 3 (как скользящее среднее) // C: for (int i = 0; i <= len-3; i++) avg(arr[i], arr[i+1], arr[i+2]); let moving_avg: Vec<f64> = temps.windows(3) .map(|w| w.iter().sum::<i32>() as f64 / 3.0) .collect(); println!("Moving avg: {moving_avg:.1?}"); - // chunks(2): non-overlapping groups of 2 + // chunks(2): непересекающиеся группы по 2 // C: for (int i = 0; i < len; i += 2) process(arr[i], arr[i+1]); for pair in temps.chunks(2) { println!("Chunk: {pair:?}"); } - // chunks_exact(2): same but panics if remainder exists - // Also: .remainder() gives leftover elements + // chunks_exact(2): то же самое, но возникает panic при наличии остатка + // Также: .remainder() возвращает оставшиеся элементы } ``` -### `fold` and `scan` — Accumulation +### `fold` и `scan` — накопление ```rust fn main() { let values = [10, 20, 30, 40, 50]; - // fold: single final result (like C's accumulator loop) + // fold: единственный итоговый результат (как накопительный цикл в C) let sum = values.iter().fold(0, |acc, &x| acc + x); println!("Sum: {sum}"); // 150 - // Build a string with fold + // Собираем строку через fold let csv = values.iter() .fold(String::new(), |acc, x| { if acc.is_empty() { format!("{x}") } @@ -133,7 +133,7 @@ fn main() { }); println!("CSV: {csv}"); // "10,20,30,40,50" - // scan: like fold but yields intermediate results + // scan: как fold, но выдаёт промежуточные результаты let running_sum: Vec<i32> = values.iter() .scan(0, |state, &x| { *state += x; @@ -144,17 +144,17 @@ fn main() { } ``` -### Exercise: Sensor Data Pipeline +### Упражнение: конвейер данных датчиков -Given raw sensor readings (one per line, format `"sensor_name:value:unit"`), write an -iterator pipeline that: -1. Parses each line into `(name, f64, unit)` -2. Filters out readings below a threshold -3. Groups by sensor name using `fold` into a `HashMap` -4. Prints the average reading per sensor +Даны сырые показания датчиков (по одному на строку, формат `"sensor_name:value:unit"`). Напишите +конвейер итераторов, который: +1. Разбирает каждую строку в `(name, f64, unit)` +2. Отбрасывает показания ниже порога +3. Группирует по имени датчика с помощью `fold` в `HashMap` +4. Выводит среднее показание для каждого датчика ```rust -// Starter code +// Стартовый код fn main() { let raw_data = vec![ "gpu_temp:72.5:C", @@ -166,11 +166,11 @@ fn main() { "fan_rpm:1150.0:RPM", ]; let threshold = 70.0; - // TODO: Parse, filter values >= threshold, group by name, compute averages + // TODO: Разобрать, отфильтровать значения >= threshold, сгруппировать по имени, посчитать средние } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::collections::HashMap; @@ -187,7 +187,7 @@ fn main() { ]; let threshold = 70.0; - // Parse → filter → group → average + // Разбор → фильтрация → группировка → среднее let grouped = raw_data.iter() .filter_map(|line| { let parts: Vec<&str> = line.splitn(3, ':').collect(); @@ -209,7 +209,7 @@ fn main() { println!("{name}: avg={avg:.1} ({} readings)", values.len()); } } -// Output (order may vary): +// Вывод (порядок может отличаться): // gpu_temp: avg=75.6 (3 readings) // fan_rpm: avg=1175.0 (2 readings) ``` @@ -217,12 +217,11 @@ fn main() { </details> -# Rust iterators -- The ```Iterator``` trait is used to implement iteration over user-defined types (https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) - - In the example, we'll implement an iterator for the Fibonacci sequence, which starts with 1, 1, 2, ... and the successor is the sum of the previous two numbers - - The ```associated type``` in the ```Iterator``` (```type Item = u32;```) defines the output type from our iterator (```u32```) - - The ```next()``` method simply contains the logic for implementing our iterator. In this case, all state information is available in the ```Fibonacci``` structure - - We could have implemented another trait called ```IntoIterator``` to implement the ```into_iter()``` method for more specialized iterators +# Итераторы в Rust: собственные типы +- Трейт ```Iterator``` используется для реализации итерации по пользовательским типам (https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) + - В примере мы реализуем итератор для последовательности Фибоначчи, которая начинается с 1, 1, 2, ..., а каждый следующий элемент — это сумма двух предыдущих + - Ассоциированный тип в ```Iterator``` (```type Item = u32;```) задаёт тип значений, которые выдаёт наш итератор (```u32```) + - Метод ```next()``` содержит саму логику итератора. В данном случае всё состояние хранится в структуре ```Fibonacci``` + - Можно было бы реализовать ещё один трейт, ```IntoIterator```, с методом ```into_iter()``` для более специализированных итераторов - https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ab367dc2611e1b5a0bf98f1185b38f3f - diff --git a/c-cpp-book/src/ch12-closures.md b/c-cpp-book/src/ch12-closures.md index fb381b9..f87eaaa 100644 --- a/c-cpp-book/src/ch12-closures.md +++ b/c-cpp-book/src/ch12-closures.md @@ -1,39 +1,39 @@ -## Rust closures +## Замыкания в Rust -> **What you'll learn:** Closures as anonymous functions, the three capture traits (`Fn`, `FnMut`, `FnOnce`), `move` closures, and how Rust closures compare to C++ lambdas — with automatic capture analysis instead of manual `[&]`/`[=]` specifications. +> **Что вы узнаете:** замыкания как анонимные функции, три трейта захвата (`Fn`, `FnMut`, `FnOnce`), замыкания `move` и то, как замыкания Rust соотносятся с лямбдами C++ — с автоматическим анализом захвата вместо ручных спецификаций `[&]`/`[=]`. -- Closures are anonymous functions that can capture their environment - - C++ equivalent: lambdas (`[&](int x) { return x + 1; }`) - - Key difference: Rust closures have **three** capture traits (`Fn`, `FnMut`, `FnOnce`) that the compiler selects automatically - - C++ capture modes (`[=]`, `[&]`, `[this]`) are manual and error-prone (dangling `[&]`!) - - Rust's borrow checker prevents dangling captures at compile time -- Closures can be identified by the `||` symbol. The parameters for the types are enclosed within the `||` and can use type inference -- Closures are frequently used in conjunction with iterators (next topic) +- Замыкания — это анонимные функции, которые могут захватывать своё окружение + - Аналог в C++: лямбды (`[&](int x) { return x + 1; }`) + - Ключевое отличие: у замыканий Rust есть **три** трейта захвата (`Fn`, `FnMut`, `FnOnce`), которые компилятор выбирает автоматически + - Режимы захвата в C++ (`[=]`, `[&]`, `[this]`) задаются вручную и подвержены ошибкам (висячий `[&]`!) + - Проверка заимствований Rust предотвращает висячие захваты на этапе компиляции +- Замыкания обозначаются символом `||`. Параметры размещаются внутри `||` и могут использовать вывод типов +- Замыкания часто используются вместе с итераторами (следующая тема) ```rust fn add_one(x: u32) -> u32 { x + 1 } fn main() { - let add_one_v1 = |x : u32| {x + 1}; // Explicitly specified type - let add_one_v2 = |x| {x + 1}; // Type is inferred from call site - let add_one_v3 = |x| x+1; // Permitted for single line functions + let add_one_v1 = |x : u32| {x + 1}; // Тип указан явно + let add_one_v2 = |x| {x + 1}; // Тип выводится из места вызова + let add_one_v3 = |x| x+1; // Допустимо для однострочных функций println!("{} {} {} {}", add_one(42), add_one_v1(42), add_one_v2(42), add_one_v3(42) ); } ``` -# Exercise: Closures and capturing +# Упражнение: замыкания и захват переменных -🟡 **Intermediate** +🟡 **Средний уровень** -- Create a closure that captures a `String` from the enclosing scope and appends to it (hint: use `move`) -- Create a vector of closures: `Vec<Box<dyn Fn(i32) -> i32>>` containing closures that add 1, multiply by 2, and square the input. Iterate over the vector and apply each closure to the number 5 +- Создайте замыкание, которое захватывает `String` из внешней области видимости и дописывает к ней текст (подсказка: используйте `move`) +- Создайте вектор замыканий `Vec<Box<dyn Fn(i32) -> i32>>`, содержащий замыкания, которые прибавляют 1, умножают на 2 и возводят входное значение в квадрат. Переберите вектор и примените каждое замыкание к числу 5 -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn main() { - // Part 1: Closure that captures and appends to a String + // Часть 1: замыкание, которое захватывает String и дописывает к ней let mut greeting = String::from("Hello"); let mut append = |suffix: &str| { greeting.push_str(suffix); @@ -42,11 +42,11 @@ fn main() { append("!"); println!("{greeting}"); // "Hello, world!" - // Part 2: Vector of closures + // Часть 2: вектор замыканий let operations: Vec<Box<dyn Fn(i32) -> i32>> = vec![ - Box::new(|x| x + 1), // add 1 - Box::new(|x| x * 2), // multiply by 2 - Box::new(|x| x * x), // square + Box::new(|x| x + 1), // прибавить 1 + Box::new(|x| x * 2), // умножить на 2 + Box::new(|x| x * x), // возвести в квадрат ]; let input = 5; @@ -54,7 +54,7 @@ fn main() { println!("Operation {i} on {input}: {}", op(input)); } } -// Output: +// Вывод: // Hello, world! // Operation 0 on 5: 6 // Operation 1 on 5: 10 @@ -63,9 +63,9 @@ fn main() { </details> -# Rust iterators -- Iterators are one of the most powerful features of Rust. They enable very elegant methods for performing operations on collections, including filtering (```filter()```), transformation (```map()```), filter and map (```filter_map()```), searching (```find()```) and much more -- In the example below, the ```|&x| *x >= 42``` is a closure that performs the same comparison. The ```|x| println!("{x}")``` is another closure +# Итераторы в Rust +- Итераторы — одна из самых мощных возможностей Rust. Они позволяют очень изящно выполнять операции над коллекциями, включая фильтрацию (```filter()```), преобразование (```map()```), фильтрацию с преобразованием (```filter_map()```), поиск (```find()```) и многое другое +- В примере ниже ```|&x| *x >= 42``` — замыкание, которое выполняет то же сравнение. ```|x| println!("{x}")``` — ещё одно замыкание ```rust fn main() { let a = [0, 1, 2, 3, 42, 43]; @@ -74,29 +74,29 @@ fn main() { println!("{x}"); } } - // Same as above + // То же, что и выше a.iter().filter(|&x| *x >= 42).for_each(|x| println!("{x}")) } ``` -# Rust iterators -- A key feature of iterators is that most of them are ```lazy```, i.e., they do not do anything until they are evaluated. For example, ```a.iter().filter(|&x| *x >= 42);``` wouldn't have done *anything* without the ```for_each```. The Rust compiler emits an explicit warning when it detects such a situation +# Итераторы в Rust: ленивость +- Ключевая особенность итераторов в том, что большинство из них ```ленивые```, то есть ничего не делают, пока их не вычислят. Например, ```a.iter().filter(|&x| *x >= 42);``` без ```for_each``` не сделало бы *ничего*. Компилятор Rust выдаёт явное предупреждение, когда замечает такую ситуацию ```rust fn main() { let a = [0, 1, 2, 3, 42, 43]; - // Add one to each element and print it + // Прибавляем единицу к каждому элементу и выводим его let _ = a.iter().map(|x|x + 1).for_each(|x|println!("{x}")); let found = a.iter().find(|&x|*x == 42); println!("{found:?}"); - // Count elements + // Считаем элементы let count = a.iter().count(); println!("{count}"); } ``` -# Rust iterators -- The ```collect()``` method can be used to gather the results into a separate collection - - In the below the ```_``` in ```Vec<_>``` is the equivalent of a wildcard character for the type returned by the ```map```. For example, we can even return a ```String``` from ```map``` +# Итераторы в Rust: сбор результатов +- Метод ```collect()``` позволяет собрать результаты в отдельную коллекцию + - В примере ниже ```_``` в ```Vec<_>``` — это подстановочный символ для типа, который возвращает ```map```. Например, из ```map``` можно даже вернуть ```String``` ```rust fn main() { let a = [0, 1, 2, 3, 42, 43]; @@ -105,26 +105,26 @@ fn main() { println!("{x}"); } let squared_a_strings : Vec<_> = a.iter().map(|x|(x*x).to_string()).collect(); - // These are actually string representations + // Это уже строковые представления for x in &squared_a_strings { println!("{x}"); } } ``` -# Exercise: Rust iterators +# Упражнение: итераторы в Rust -🟢 **Starter** -- Create an integer array composed of odd and even elements. Iterate over the array and split it into two different vectors with even and odd elements in each -- Can this be done in a single pass (hint: use ```partition()```)? +🟢 **Начальный уровень** +- Создайте целочисленный массив из нечётных и чётных элементов. Переберите массив и разделите его на два разных вектора: в одном — чётные элементы, в другом — нечётные +- Можно ли сделать это за один проход (подсказка: используйте ```partition()```)? -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn main() { let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - // Approach 1: Manual iteration + // Подход 1: ручная итерация let mut evens = Vec::new(); let mut odds = Vec::new(); for n in numbers { @@ -137,14 +137,14 @@ fn main() { println!("Evens: {evens:?}"); println!("Odds: {odds:?}"); - // Approach 2: Single pass with partition() + // Подход 2: один проход с partition() let (evens, odds): (Vec<i32>, Vec<i32>) = numbers .into_iter() .partition(|n| *n % 2 == 0); println!("Evens (partition): {evens:?}"); println!("Odds (partition): {odds:?}"); } -// Output: +// Вывод: // Evens: [2, 4, 6, 8, 10] // Odds: [1, 3, 5, 7, 9] // Evens (partition): [2, 4, 6, 8, 10] @@ -153,15 +153,13 @@ fn main() { </details> -> **Production patterns**: See [Collapsing assignment pyramids with closures](ch17-3-collapsing-assignment-pyramids.md#collapsing-assignment-pyramids-with-closures) for real iterator chains (`.map().collect()`, `.filter().collect()`, `.find_map()`) from production Rust code. +> **Промышленные шаблоны**: реальные цепочки итераторов (`.map().collect()`, `.filter().collect()`, `.find_map()`) из продакшн-кода на Rust см. в разделе [Схлопывание пирамид присваиваний с помощью замыканий](ch17-3-collapsing-assignment-pyramids.md#схлопывание-пирамид-присваиваний-с-помощью-замыканий). -### Iterator power tools: the methods that replace C++ loops +### Инструменты для работы с итераторами: методы, заменяющие циклы C++ -The following iterator adapters are used *extensively* in production Rust code. C++ has -`<algorithm>` and C++20 ranges, but Rust's iterator chains are more composable -and more commonly used. +Следующие адаптеры итераторов *широко* используются в продакшн-коде на Rust. В C++ есть `<algorithm>` и диапазоны C++20, но цепочки итераторов Rust более компонуемы и используются чаще. -#### `enumerate` — index + value (replaces `for (int i = 0; ...)`) +#### `enumerate` — индекс и значение (заменяет `for (int i = 0; ...)`) ```rust let sensors = vec!["temp0", "temp1", "temp2"]; @@ -173,9 +171,9 @@ for (idx, name) in sensors.iter().enumerate() { // Sensor 2: temp2 ``` -C++ equivalent: `for (size_t i = 0; i < sensors.size(); ++i) { auto& name = sensors[i]; ... }` +Аналог в C++: `for (size_t i = 0; i < sensors.size(); ++i) { auto& name = sensors[i]; ... }` -#### `zip` — pair elements from two iterators (replaces parallel index loops) +#### `zip` — попарное объединение элементов двух итераторов (заменяет параллельные циклы по индексу) ```rust let names = ["gpu0", "gpu1", "gpu2"]; @@ -188,15 +186,15 @@ let report: Vec<String> = names.iter() println!("{report:?}"); // ["gpu0: 72.5°C", "gpu1: 68.0°C", "gpu2: 75.3°C"] -// Stops at the shorter iterator — no out-of-bounds risk +// Останавливается на более коротком итераторе — риска выхода за границы нет ``` -C++ equivalent: `for (size_t i = 0; i < std::min(names.size(), temps.size()); ++i) { ... }` +Аналог в C++: `for (size_t i = 0; i < std::min(names.size(), temps.size()); ++i) { ... }` -#### `flat_map` — map + flatten nested collections +#### `flat_map` — map и flatten для вложенных коллекций ```rust -// Each GPU has multiple PCIe BDFs; collect all BDFs across all GPUs +// У каждого GPU несколько BDF на PCIe; собираем все BDF со всех GPU let gpu_bdfs = vec![ vec!["0000:01:00.0", "0000:02:00.0"], vec!["0000:41:00.0"], @@ -210,43 +208,43 @@ println!("{all_bdfs:?}"); // ["0000:01:00.0", "0000:02:00.0", "0000:41:00.0", "0000:81:00.0", "0000:82:00.0"] ``` -C++ equivalent: nested `for` loop pushing into a single vector. +Аналог в C++: вложенный цикл `for`, который добавляет элементы в один вектор. -#### `chain` — concatenate two iterators +#### `chain` — конкатенация двух итераторов ```rust let critical_gpus = vec!["gpu0", "gpu3"]; let warning_gpus = vec!["gpu1", "gpu5"]; -// Process all flagged GPUs, critical first +// Обрабатываем все отмеченные GPU, сначала критичные for gpu in critical_gpus.iter().chain(warning_gpus.iter()) { println!("Flagged: {gpu}"); } ``` -#### `windows` and `chunks` — sliding/fixed-size views over slices +#### `windows` и `chunks` — скользящие и фиксированные окна над срезами ```rust let temps = [70, 72, 75, 73, 71, 68, 65]; -// windows(3): sliding window of size 3 — detect trends +// windows(3): скользящее окно размера 3 — выявляем тренды let rising = temps.windows(3) .any(|w| w[0] < w[1] && w[1] < w[2]); println!("Rising trend detected: {rising}"); // true (70 < 72 < 75) -// chunks(2): fixed-size groups — process in pairs +// chunks(2): группы фиксированного размера — обрабатываем парами for pair in temps.chunks(2) { println!("Pair: {pair:?}"); } // Pair: [70, 72] // Pair: [75, 73] // Pair: [71, 68] -// Pair: [65] ← last chunk can be smaller +// Pair: [65] ← последний фрагмент может быть меньше ``` -C++ equivalent: manual index arithmetic with `i` and `i+1`/`i+2`. +Аналог в C++: ручная арифметика индексов с `i` и `i+1`/`i+2`. -#### `fold` — accumulate into a single value (replaces `std::accumulate`) +#### `fold` — накопление в одно значение (заменяет `std::accumulate`) ```rust let errors = vec![ @@ -256,7 +254,7 @@ let errors = vec![ ("gpu3", 1), ]; -// Count total errors and build summary in one pass +// Считаем общее число ошибок и формируем сводку за один проход let (total, summary) = errors.iter().fold( (0u32, String::new()), |(count, mut s), (name, errs)| { @@ -270,35 +268,35 @@ println!("Total errors: {total}, details: {summary}"); // Total errors: 11, details: gpu0:3 gpu2:7 gpu3:1 ``` -#### `scan` — stateful transform (running total, delta detection) +#### `scan` — преобразование с состоянием (накопленная сумма, отслеживание разностей) ```rust let readings = [100, 105, 103, 110, 108]; -// Compute deltas between consecutive readings +// Вычисляем разности между соседними показаниями let deltas: Vec<i32> = readings.iter() .scan(None::<i32>, |prev, &val| { let delta = prev.map(|p| val - p); *prev = Some(val); Some(delta) }) - .flatten() // Remove the initial None + .flatten() // Убираем начальный None .collect(); println!("Deltas: {deltas:?}"); // [5, -2, 7, -2] ``` -#### Quick reference: C++ loop → Rust iterator +#### Краткий справочник: цикл C++ → итератор Rust -| **C++ Pattern** | **Rust Iterator** | **Example** | +| **Шаблон C++** | **Итератор Rust** | **Пример** | |----------------|------------------|------------| | `for (int i = 0; i < v.size(); i++)` | `.enumerate()` | `v.iter().enumerate()` | -| Parallel iteration with index | `.zip()` | `a.iter().zip(b.iter())` | -| Nested loop → flat result | `.flat_map()` | `vecs.iter().flat_map(\|v\| v.iter())` | -| Concatenate two containers | `.chain()` | `a.iter().chain(b.iter())` | -| Sliding window `v[i..i+n]` | `.windows(n)` | `v.windows(3)` | -| Process in fixed-size groups | `.chunks(n)` | `v.chunks(4)` | -| `std::accumulate` / manual accumulator | `.fold()` | `.fold(init, \|acc, x\| ...)` | -| Running total / delta tracking | `.scan()` | `.scan(state, \|s, x\| ...)` | +| Параллельный перебор с индексом | `.zip()` | `a.iter().zip(b.iter())` | +| Вложенный цикл → плоский результат | `.flat_map()` | `vecs.iter().flat_map(\|v\| v.iter())` | +| Конкатенация двух контейнеров | `.chain()` | `a.iter().chain(b.iter())` | +| Скользящее окно `v[i..i+n]` | `.windows(n)` | `v.windows(3)` | +| Обработка группами фиксированного размера | `.chunks(n)` | `v.chunks(4)` | +| `std::accumulate` / ручной аккумулятор | `.fold()` | `.fold(init, \|acc, x\| ...)` | +| Накопленная сумма / отслеживание разностей | `.scan()` | `.scan(state, \|s, x\| ...)` | | `while (it != end && count < n) { ++it; ++count; }` | `.take(n)` | `.iter().take(5)` | | `while (it != end && !pred(*it)) { ++it; }` | `.skip_while()` | `.skip_while(\|x\| x < &threshold)` | | `std::any_of` | `.any()` | `.iter().any(\|x\| x > &limit)` | @@ -306,20 +304,20 @@ println!("Deltas: {deltas:?}"); // [5, -2, 7, -2] | `std::none_of` | `!.any()` | `!iter.any(\|x\| x.failed())` | | `std::count_if` | `.filter().count()` | `.filter(\|x\| x > &0).count()` | | `std::min_element` / `std::max_element` | `.min()` / `.max()` | `.iter().max()` → `Option<&T>` | -| `std::unique` | `.dedup()` (on sorted) | `v.dedup()` (in-place on Vec) | +| `std::unique` | `.dedup()` (на отсортированном) | `v.dedup()` (на месте в Vec) | -### Exercise: Iterator chains +### Упражнение: цепочки итераторов -Given sensor data as `Vec<(String, f64)>` (name, temperature), write a **single -iterator chain** that: -1. Filters sensors with temp > 80.0 -2. Sorts them by temperature (descending) -3. Formats each as `"{name}: {temp}°C [ALARM]"` -4. Collects into `Vec<String>` +Дано: данные датчиков в виде `Vec<(String, f64)>` (имя, температура). Напишите **одну +цепочку итераторов**, которая: +1. Отфильтровывает датчики с температурой > 80.0 +2. Сортирует их по температуре (по убыванию) +3. Форматирует каждый как `"{name}: {temp}°C [ALARM]"` +4. Собирает результат в `Vec<String>` -Hint: you'll need `.collect()` before `.sort_by()`, since sorting requires a `Vec`. +Подсказка: нужно вызвать `.collect()` перед `.sort_by()`, поскольку для сортировки нужен `Vec`. -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust fn alarm_report(sensors: &[(String, f64)]) -> Vec<String> { @@ -344,7 +342,7 @@ fn main() { println!("{line}"); } } -// Output: +// Вывод: // gpu2: 91°C [ALARM] // gpu4: 88.7°C [ALARM] // gpu1: 85.3°C [ALARM] @@ -354,12 +352,11 @@ fn main() { ---- -# Rust iterators -- The ```Iterator``` trait is used to implement iteration over user-defined types (https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) - - In the example, we'll implement an iterator for the Fibonacci sequence, which starts with 1, 1, 2, ... and the successor is the sum of the previous two numbers - - The ```associated type``` in the ```Iterator``` (```type Item = u32;```) defines the output type from our iterator (```u32```) - - The ```next()``` method simply contains the logic for implementing our iterator. In this case, all state information is available in the ```Fibonacci``` structure - - We could have implemented another trait called ```IntoIterator``` to implement the ```into_iter()``` method for more specialized iterators - - [▶ Try it in the Rust Playground](https://play.rust-lang.org/) - +# Итераторы в Rust: собственные типы +- Трейт ```Iterator``` используется для реализации итерации по пользовательским типам (https://doc.rust-lang.org/std/iter/trait.IntoIterator.html) + - В примере мы реализуем итератор для последовательности Фибоначчи, которая начинается с 1, 1, 2, ..., а каждый следующий элемент — это сумма двух предыдущих + - Ассоциированный тип в ```Iterator``` (```type Item = u32;```) задаёт тип значений, которые выдаёт наш итератор (```u32```) + - Метод ```next()``` содержит саму логику итератора. В данном случае всё состояние хранится в структуре ```Fibonacci``` + - Можно было бы реализовать ещё один трейт, ```IntoIterator```, с методом ```into_iter()``` для более специализированных итераторов + - [▶ Попробуйте в Rust Playground](https://play.rust-lang.org/) diff --git a/c-cpp-book/src/ch13-concurrency.md b/c-cpp-book/src/ch13-concurrency.md index bc6a6a2..0c8dbe8 100644 --- a/c-cpp-book/src/ch13-concurrency.md +++ b/c-cpp-book/src/ch13-concurrency.md @@ -1,12 +1,12 @@ -# Rust concurrency +# Конкурентность в Rust -> **What you'll learn:** Rust's concurrency model — threads, `Send`/`Sync` marker traits, `Mutex<T>`, `Arc<T>`, channels, and how the compiler prevents data races at compile time. No runtime overhead for thread safety you don't use. +> **Что вы узнаете:** модель конкурентности Rust — потоки, маркерные трейты `Send`/`Sync`, `Mutex<T>`, `Arc<T>`, каналы и то, как компилятор предотвращает гонки данных на этапе компиляции. Никаких накладных расходов на потокобезопасность, которой вы не пользуетесь. -- Rust has built-in support for concurrency, similar to `std::thread` in C++ - - Key difference: Rust **prevents data races at compile time** through `Send` and `Sync` marker traits - - In C++, sharing a `std::vector` across threads without a mutex is UB but compiles fine. In Rust, it won't compile. - - `Mutex<T>` in Rust wraps the **data**, not just the access — you literally cannot read the data without locking -- The `thread::spawn()` can be used to create a separate thread that executes the closure `||` in parallel +- В Rust есть встроенная поддержка конкурентности, похожая на `std::thread` в C++ + - Ключевое отличие: Rust **предотвращает гонки данных на этапе компиляции** через маркерные трейты `Send` и `Sync` + - В C++ передача `std::vector` между потоками без мьютекса — это неопределённое поведение, но код компилируется без проблем. В Rust такой код не скомпилируется. + - `Mutex<T>` в Rust оборачивает **данные**, а не только доступ к ним: прочитать данные, не взяв блокировку, физически невозможно +- `thread::spawn()` создаёт отдельный поток, который параллельно выполняет замыкание `||` ```rust use std::thread; use std::time::Duration; @@ -23,13 +23,13 @@ fn main() { thread::sleep(Duration::from_millis(5)); } - handle.join().unwrap(); // The handle.join() ensures that the spawned thread exits + handle.join().unwrap(); // handle.join() гарантирует, что порождённый поток завершится } ``` -# Rust concurrency -- ```thread::scope()``` can be used in cases where it is necessary to borrow from the environment. This works because ```thread::scope``` waits until the internal thread returns -- Try executing this exercise without ```thread::scope``` to see the issue +# Конкурентность в Rust: области видимости потоков +- ```thread::scope()``` можно использовать, когда нужно заимствовать данные из окружения. Это работает, потому что ```thread::scope``` ждёт завершения внутренних потоков +- Попробуйте выполнить это упражнение без ```thread::scope```, чтобы увидеть проблему ```rust use std::thread; fn main() { @@ -44,8 +44,8 @@ fn main() { } ``` ---- -# Rust concurrency -- We can also use ```move``` to transfer ownership to the thread. For `Copy` types like `[i32; 3]`, the `move` keyword copies the data into the closure, and the original remains usable +# Конкурентность в Rust: перемещение владения в поток +- Можно также использовать ```move```, чтобы передать владение потоку. Для типов `Copy`, таких как `[i32; 3]`, ключевое слово `move` копирует данные в замыкание, и исходная переменная остаётся пригодной для использования ```rust use std::thread; fn main() { @@ -55,15 +55,15 @@ fn main() { println!("{x}"); } }); - a[0] = 42; // Doesn't affect the copy sent to the thread + a[0] = 42; // Не влияет на копию, переданную в поток handle.join().unwrap(); } ``` -# Rust concurrency -- ```Arc<T>``` can be used to share *read-only* references between multiple threads - - ```Arc``` stands for Atomic Reference Counted. The reference isn't released until the reference count reaches 0 - - ```Arc::clone()``` simply increases the reference count without cloning the data +# Конкурентность в Rust: Arc +- ```Arc<T>``` можно использовать, чтобы разделить *доступные только для чтения* ссылки между несколькими потоками + - ```Arc``` расшифровывается как Atomic Reference Counted (атомарный подсчёт ссылок). Ссылка не освобождается, пока счётчик не достигнет 0 + - ```Arc::clone()``` просто увеличивает счётчик ссылок, не копируя данные ```rust use std::sync::Arc; use std::thread; @@ -80,10 +80,10 @@ fn main() { } ``` -# Rust concurrency -- ```Arc<T>``` can be combined with ```Mutex<T>``` to provide mutable references. - - ```Mutex``` guards the protected data and ensures that only the thread holding the lock has access. - - The `MutexGuard` is automatically released when it goes out of scope (RAII). Note: `std::mem::forget` can still leak a guard — so "impossible to forget to unlock" is more accurate than "impossible to leak." +# Конкурентность в Rust: Mutex +- ```Arc<T>``` можно комбинировать с ```Mutex<T>```, чтобы получить изменяемые ссылки + - ```Mutex``` охраняет защищаемые данные и гарантирует, что доступ имеет только поток, который держит блокировку. + - `MutexGuard` автоматически освобождается, когда выходит из области видимости (RAII). Примечание: `std::mem::forget` всё же может «утечь» защитник — поэтому точнее сказать «невозможно забыть разблокировать», а не «невозможно допустить утечку». ```rust use std::sync::{Arc, Mutex}; use std::thread; @@ -97,7 +97,7 @@ fn main() { handles.push(thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; - // MutexGuard dropped here — lock released automatically + // MutexGuard уничтожается здесь — блокировка снимается автоматически })); } @@ -106,14 +106,14 @@ fn main() { } println!("Final count: {}", *counter.lock().unwrap()); - // Output: Final count: 5 + // Вывод: Final count: 5 } ``` -# Rust concurrency: RwLock -- `RwLock<T>` allows **multiple concurrent readers** or **one exclusive writer** — the read/write lock pattern from C++ (`std::shared_mutex`) - - Use `RwLock` when reads far outnumber writes (e.g., configuration, caches) - - Use `Mutex` when read/write frequency is similar or critical sections are short +# Конкурентность в Rust: RwLock +- `RwLock<T>` допускает **несколько одновременных читателей** или **одного эксклюзивного писателя** — шаблон блокировки для чтения/записи из C++ (`std::shared_mutex`) + - Используйте `RwLock`, когда чтений намного больше, чем записей (например, конфигурация, кэши) + - Используйте `Mutex`, когда частота чтения и записи примерно одинакова или критические секции короткие ```rust use std::sync::{Arc, RwLock}; use std::thread; @@ -122,20 +122,20 @@ fn main() { let config = Arc::new(RwLock::new(String::from("v1.0"))); let mut handles = Vec::new(); - // Spawn 5 readers — all can run concurrently + // Запускаем 5 читателей — все могут работать одновременно for i in 0..5 { let config = Arc::clone(&config); handles.push(thread::spawn(move || { - let val = config.read().unwrap(); // Multiple readers OK + let val = config.read().unwrap(); // Несколько читателей — это нормально println!("Reader {i}: {val}"); })); } - // One writer — blocks until all readers finish + // Один писатель — блокируется, пока все читатели не закончат { let config = Arc::clone(&config); handles.push(thread::spawn(move || { - let mut val = config.write().unwrap(); // Exclusive access + let mut val = config.write().unwrap(); // Эксклюзивный доступ *val = String::from("v2.0"); println!("Writer: updated to {val}"); })); @@ -147,11 +147,11 @@ fn main() { } ``` -# Rust concurrency: Mutex poisoning -- If a thread **panics** while holding a `Mutex` or `RwLock`, the lock becomes **poisoned** - - Subsequent calls to `.lock()` return `Err(PoisonError)` — the data may be in an inconsistent state - - You can recover with `.into_inner()` if you're confident the data is still valid - - This has no C++ equivalent — `std::mutex` has no poisoning concept; a panicking thread just leaves the lock held +# Конкурентность в Rust: отравление Mutex +- Если поток **падает** (panic), удерживая `Mutex` или `RwLock`, блокировка становится **отравленной** (poisoned) + - Последующие вызовы `.lock()` возвращают `Err(PoisonError)` — данные могут находиться в несогласованном состоянии + - Можно восстановиться с помощью `.into_inner()`, если вы уверены, что данные по-прежнему корректны + - В C++ аналога нет — `std::mutex` не знает понятия отравления; упавший поток просто оставляет блокировку захваченной ```rust use std::sync::{Arc, Mutex}; use std::thread; @@ -163,27 +163,27 @@ fn main() { let handle = thread::spawn(move || { let mut guard = data2.lock().unwrap(); guard.push(4); - panic!("oops!"); // Lock is now poisoned + panic!("oops!"); // Блокировка теперь отравлена }); - let _ = handle.join(); // Thread panicked + let _ = handle.join(); // Поток упал с panic - // Subsequent lock attempts return Err(PoisonError) + // Последующие попытки захватить блокировку возвращают Err(PoisonError) match data.lock() { Ok(guard) => println!("Data: {guard:?}"), Err(poisoned) => { println!("Lock was poisoned! Recovering..."); - let guard = poisoned.into_inner(); // Access data anyway - println!("Recovered data: {guard:?}"); // [1, 2, 3, 4] — push succeeded before panic + let guard = poisoned.into_inner(); // Всё равно получаем доступ к данным + println!("Recovered data: {guard:?}"); // [1, 2, 3, 4] — push выполнился до panic } } } ``` -# Rust concurrency: Atomics -- For simple counters and flags, `std::sync::atomic` types avoid the overhead of a `Mutex` - - `AtomicBool`, `AtomicI32`, `AtomicU64`, `AtomicUsize`, etc. - - Equivalent to C++ `std::atomic<T>` — same memory ordering model (`Relaxed`, `Acquire`, `Release`, `SeqCst`) +# Конкурентность в Rust: атомарные типы +- Для простых счётчиков и флагов типы `std::sync::atomic` позволяют избежать накладных расходов `Mutex` + - `AtomicBool`, `AtomicI32`, `AtomicU64`, `AtomicUsize` и т. д. + - Аналог `std::atomic<T>` из C++ — та же модель порядка доступа к памяти (`Relaxed`, `Acquire`, `Release`, `SeqCst`) ```rust use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -207,22 +207,22 @@ fn main() { } println!("Counter: {}", counter.load(Ordering::SeqCst)); - // Output: Counter: 10000 + // Вывод: Counter: 10000 } ``` -| Primitive | When to use | C++ equivalent | +| Примитив | Когда использовать | Аналог в C++ | |-----------|-------------|----------------| -| `Mutex<T>` | General mutable shared state | `std::mutex` + manual data association | -| `RwLock<T>` | Read-heavy workloads | `std::shared_mutex` | -| `Atomic*` | Simple counters, flags, lock-free patterns | `std::atomic<T>` | -| `Condvar` | Wait for a condition to become true | `std::condition_variable` | - -# Rust concurrency: Condvar -- `Condvar` (condition variable) lets a thread **sleep until another thread signals** that a condition has changed - - Always paired with a `Mutex` — the pattern is: lock, check condition, wait if not ready, act when ready - - Equivalent to C++ `std::condition_variable` / `std::condition_variable::wait` - - Handles **spurious wakeups** — always re-check the condition in a loop (or use `wait_while`/`wait_until`) +| `Mutex<T>` | Общее изменяемое состояние общего назначения | `std::mutex` + ручная привязка данных | +| `RwLock<T>` | Нагрузка с преобладанием чтения | `std::shared_mutex` | +| `Atomic*` | Простые счётчики, флаги, lock-free-шаблоны | `std::atomic<T>` | +| `Condvar` | Ожидание, пока условие не станет истинным | `std::condition_variable` | + +# Конкурентность в Rust: Condvar +- `Condvar` (условная переменная) позволяет потоку **заснуть до тех пор, пока другой поток не сигнализирует**, что условие изменилось + - Всегда используется вместе с `Mutex` — шаблон такой: захватить блокировку, проверить условие, ждать, если оно не выполнено, действовать, когда оно выполнено + - Эквивалент `std::condition_variable` / `std::condition_variable::wait` в C++ + - Учитывает **ложные пробуждения** (spurious wakeups) — всегда перепроверяйте условие в цикле (или используйте `wait_while`/`wait_until`) ```rust use std::sync::{Arc, Condvar, Mutex}; use std::thread; @@ -230,37 +230,37 @@ use std::thread; fn main() { let pair = Arc::new((Mutex::new(false), Condvar::new())); - // Spawn a worker that waits for a signal + // Порождаем рабочий поток, который ждёт сигнала let pair2 = Arc::clone(&pair); let worker = thread::spawn(move || { let (lock, cvar) = &*pair2; let mut ready = lock.lock().unwrap(); - // wait: sleeps until signaled (always re-check in a loop for spurious wakeups) + // wait: засыпает до сигнала (всегда перепроверяйте в цикле из-за ложных пробуждений) while !*ready { ready = cvar.wait(ready).unwrap(); } println!("Worker: condition met, proceeding!"); }); - // Main thread does some work, then signals the worker + // Главный поток что-то делает, затем сигнализирует рабочему потоку thread::sleep(std::time::Duration::from_millis(100)); { let (lock, cvar) = &*pair; let mut ready = lock.lock().unwrap(); *ready = true; - cvar.notify_one(); // Wake one waiting thread (notify_all() wakes all) + cvar.notify_one(); // Будим один ожидающий поток (notify_all() будит все) } worker.join().unwrap(); } ``` -> **When to use Condvar vs channels:** Use `Condvar` when threads share mutable state and need to wait for a condition on that state (e.g., "buffer not empty"). Use channels (`mpsc`) when threads need to pass *messages*. Channels are generally easier to reason about. +> **Когда использовать Condvar, а когда каналы:** используйте `Condvar`, когда потоки разделяют изменяемое состояние и должны ждать условия на этом состоянии (например, «буфер не пуст»). Используйте каналы (`mpsc`), когда потокам нужно передавать *сообщения*. Каналы обычно проще для рассуждений. -# Rust concurrency -- Rust channels can be used to exchange messages between ```Sender``` and ```Receiver``` - - This uses a paradigm called ```mpsc``` or ```Multi-producer, Single-Consumer``` - - Both ```send()``` and ```recv()``` can block the thread +# Конкурентность в Rust: каналы +- Каналы Rust позволяют обмениваться сообщениями между ```Sender``` и ```Receiver``` + - Используется парадигма под названием ```mpsc``` или ```Multi-producer, Single-Consumer``` (много производителей, один потребитель) + - И ```send()```, и ```recv()``` могут блокировать поток ```rust use std::sync::mpsc; @@ -279,8 +279,8 @@ fn main() { } ``` -# Rust concurrency -- Channels can be combined with threads +# Конкурентность в Rust: каналы и потоки +- Каналы можно комбинировать с потоками ```rust use std::sync::mpsc; use std::thread; @@ -300,7 +300,7 @@ fn main() { }); } - // Drop the original sender so rx.iter() terminates when all cloned senders are dropped + // Удаляем исходного отправителя, чтобы rx.iter() завершился, когда все клонированные отправители будут уничтожены drop(tx); thread::sleep(Duration::from_millis(100)); @@ -313,34 +313,34 @@ fn main() { -## Why Rust prevents data races: Send and Sync +## Почему Rust предотвращает гонки данных: Send и Sync -- Rust uses two marker traits to enforce thread safety at compile time: - - `Send`: A type is `Send` if it can be safely **transferred** to another thread - - `Sync`: A type is `Sync` if it can be safely **shared** (via `&T`) between threads -- Most types are automatically `Send + Sync`. Notable exceptions: - - `Rc<T>` is **neither** Send nor Sync (use `Arc<T>` for threads) - - `Cell<T>` and `RefCell<T>` are **not** Sync (use `Mutex<T>` or `RwLock<T>`) - - Raw pointers (`*const T`, `*mut T`) are **neither** Send nor Sync -- This is why the compiler stops you from using `Rc<T>` across threads -- it literally doesn't implement `Send` -- `Arc<Mutex<T>>` is the thread-safe equivalent of `Rc<RefCell<T>>` +- Rust использует два маркерных трейта, чтобы обеспечивать потокобезопасность на этапе компиляции: + - `Send`: тип является `Send`, если его можно безопасно **передать** в другой поток + - `Sync`: тип является `Sync`, если его можно безопасно **разделять** (через `&T`) между потоками +- Большинство типов автоматически являются `Send + Sync`. Заметные исключения: + - `Rc<T>` **не** является ни Send, ни Sync (для потоков используйте `Arc<T>`) + - `Cell<T>` и `RefCell<T>` **не** являются Sync (используйте `Mutex<T>` или `RwLock<T>`) + - Сырые указатели (`*const T`, `*mut T`) **не** являются ни Send, ни Sync +- Именно поэтому компилятор не даёт использовать `Rc<T>` между потоками — он просто не реализует `Send` +- `Arc<Mutex<T>>` — потокобезопасный аналог `Rc<RefCell<T>>` -> **Intuition** *(Jon Gjengset)*: Think of values as toys. -> **`Send`** = you can **give your toy away** to another child (thread) — transferring ownership is safe. -> **`Sync`** = you can **let others play with your toy at the same time** — sharing a reference is safe. -> An `Rc<T>` has a fragile (non-atomic) reference counter; handing it off or sharing it would corrupt the count, so it is neither `Send` nor `Sync`. +> **Интуиция** *(Jon Gjengset)*: представьте значения как игрушки. +> **`Send`** = вы можете **отдать свою игрушку** другому ребёнку (потоку) — передача владения безопасна. +> **`Sync`** = вы можете **позволить другим играть с вашей игрушкой одновременно** — совместное использование ссылки безопасно. +> У `Rc<T>` хрупкий (неатомарный) счётчик ссылок; передача или совместное использование повредили бы счётчик, поэтому он не является ни `Send`, ни `Sync`. -# Exercise: Multi-threaded word count +# Упражнение: подсчёт слов в многопоточном режиме -🔴 **Challenge** — combines threads, Arc, Mutex, and HashMap +🔴 **Сложный уровень** — объединяет потоки, Arc, Mutex и HashMap -- Given a `Vec<String>` of text lines, spawn one thread per line to count the words in that line -- Use `Arc<Mutex<HashMap<String, usize>>>` to collect results -- Print the total word count across all lines -- **Bonus**: Try implementing this with channels (`mpsc`) instead of shared state +- Дан `Vec<String>` со строками текста. Запустите по одному потоку на каждую строку, чтобы подсчитать слова в ней +- Используйте `Arc<Mutex<HashMap<String, usize>>>`, чтобы собрать результаты +- Выведите общее количество слов по всем строкам +- **Бонус**: попробуйте реализовать то же самое с каналами (`mpsc`) вместо разделяемого состояния -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::collections::HashMap; @@ -378,7 +378,7 @@ fn main() { println!("Word frequencies: {counts:#?}"); println!("Total words: {total}"); } -// Output (order may vary): +// Вывод (порядок может отличаться): // Word frequencies: { // "the": 3, // "quick": 2, @@ -395,4 +395,3 @@ fn main() { </details> - diff --git a/c-cpp-book/src/ch14-unsafe-rust-and-ffi.md b/c-cpp-book/src/ch14-unsafe-rust-and-ffi.md index 6af3147..6e6e793 100644 --- a/c-cpp-book/src/ch14-unsafe-rust-and-ffi.md +++ b/c-cpp-book/src/ch14-unsafe-rust-and-ffi.md @@ -1,81 +1,81 @@ -### Unsafe Rust +### Небезопасный Rust (unsafe) -> **What you'll learn:** When and how to use `unsafe` — raw pointer dereferencing, FFI (Foreign Function Interface) for calling C from Rust and vice versa, `CString`/`CStr` for string interop, and how to write safe wrappers around unsafe code. +> **Что вы узнаете:** когда и как использовать `unsafe` — разыменование сырых указателей, FFI (Foreign Function Interface) для вызова C из Rust и наоборот, `CString`/`CStr` для взаимодействия со строками и как писать безопасные обёртки вокруг небезопасного кода. -- ```unsafe``` unlocks access to features that are normally disallowed by the Rust compiler - - Dereferencing raw pointers - - Accessing *mutable* static variables +- ```unsafe``` открывает доступ к возможностям, которые обычно запрещены компилятором Rust + - Разыменование сырых указателей + - Доступ к *изменяемым* статическим переменным - https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html -- With great power comes great responsibility - - ```unsafe``` tells the compiler "I, the programmer, take responsibility for upholding the invariants that the compiler normally guarantees" - - Must guarantee no aliased mutable and immutable references, no dangling pointers, no invalid references, ... - - The use of ```unsafe``` should be limited to the smallest possible scope - - All code using ```unsafe``` should have a "safety" comment describing the assumptions +- С большой силой приходит большая ответственность + - ```unsafe``` говорит компилятору: «Я, программист, беру на себя ответственность за соблюдение инвариантов, которые компилятор обычно гарантирует» + - Нужно гарантировать отсутствие одновременных изменяемых и неизменяемых псевдонимов ссылок, висячих указателей, некорректных ссылок и т. д. + - Использование ```unsafe``` следует ограничивать как можно меньшей областью + - У всего кода с ```unsafe``` должен быть комментарий «safety», описывающий предположения -### Unsafe Rust examples +### Примеры unsafe в Rust ```rust unsafe fn harmless() {} fn main() { - // Safety: We are calling a harmless unsafe function + // Safety: вызываем безобидную небезопасную функцию unsafe { harmless(); } let a = 42u32; let p = &a as *const u32; - // Safety: p is a valid pointer to a variable that will remain in scope + // Safety: p — корректный указатель на переменную, которая останется в области видимости unsafe { println!("{}", *p); } - // Safety: Not safe; for illustration purposes only + // Safety: небезопасно; только для иллюстрации let dangerous_buffer = 0xb8000 as *mut u32; unsafe { println!("About to go kaboom!!!"); - *dangerous_buffer = 0; // This will SEGV on most modern machines + *dangerous_buffer = 0; // На большинстве современных машин это вызовет SEGV } } ``` -### Simple FFI example (Rust library function consumed by C) +### Простой пример FFI (функция Rust, вызываемая из C) -## FFI Strings: CString and CStr +## Строки FFI: CString и CStr -FFI stands for *Foreign Function Interface* — the mechanism Rust uses to call functions written in other languages (such as C) and vice versa. +FFI расшифровывается как *Foreign Function Interface* (интерфейс внешних функций) — механизм, с помощью которого Rust вызывает функции, написанные на других языках (например, C), и наоборот. -When interfacing with C code, Rust's `String` and `&str` types (which are UTF-8 without null terminators) aren't directly compatible with C strings (which are null-terminated byte arrays). Rust provides `CString` (owned) and `CStr` (borrowed) from `std::ffi` for this purpose: +При взаимодействии с кодом на C типы `String` и `&str` в Rust (которые являются UTF-8 без завершающего нуля) напрямую несовместимы с C-строками (которые представляют собой массивы байтов с завершающим нулём). Для этого в `std::ffi` Rust предоставляет `CString` (владеющий) и `CStr` (заимствованный): -| Type | Analogous to | Use when | +| Тип | Аналог | Когда использовать | |------|-------------|----------| -| `CString` | `String` (owned) | Creating a C string from Rust data | -| `&CStr` | `&str` (borrowed) | Receiving a C string from foreign code | +| `CString` | `String` (владеющий) | Создание C-строки из данных Rust | +| `&CStr` | `&str` (заимствованный) | Получение C-строки из внешнего кода | ```rust use std::ffi::{CString, CStr}; use std::os::raw::c_char; fn demo_ffi_strings() { - // Creating a C-compatible string (adds null terminator) + // Создаём совместимую с C строку (добавляется завершающий нуль) let c_string = CString::new("Hello from Rust").expect("CString::new failed"); let ptr: *const c_char = c_string.as_ptr(); - // Converting a C string back to Rust (unsafe because we trust the pointer) - // Safety: ptr is valid and null-terminated (we just created it above) + // Преобразуем C-строку обратно в Rust (unsafe, потому что мы доверяем указателю) + // Safety: ptr валиден и заканчивается нулём (мы только что его создали) let back_to_rust: &CStr = unsafe { CStr::from_ptr(ptr) }; let rust_str: &str = back_to_rust.to_str().expect("Invalid UTF-8"); println!("{}", rust_str); } ``` -> **Warning**: `CString::new()` will return an error if the input contains interior null bytes (`\0`). Always handle the `Result`. You'll see `CStr` used extensively in the FFI examples below. +> **Предупреждение**: `CString::new()` вернёт ошибку, если входные данные содержат внутренние нулевые байты (`\0`). Всегда обрабатывайте `Result`. `CStr` активно используется в примерах FFI ниже. -- ```FFI``` methods must be marked with ```#[no_mangle]``` to ensure that the compiler doesn't mangle the name -- We'll compile the crate as a static library +- Методы ```FFI``` должны быть помечены ```#[no_mangle]```, чтобы компилятор не искажал их имена +- Скомпилируем крейт как статическую библиотеку ``` #[no_mangle] pub extern "C" fn add(left: u64, right: u64) -> u64 { left + right } ``` -- We'll compile the following C-code and link it against our static library. +- Скомпилируем следующий C-код и скомпонуем его с нашей статической библиотекой ``` #include <stdio.h> #include <stdint.h> @@ -85,14 +85,13 @@ fn demo_ffi_strings() { } ``` -### Complex FFI example -- In the following examples, we'll create a Rust logging interface and expose it to -[PYTHON] and ```C``` - - We'll see how the same interface can be used natively from Rust and C - - We will explore the use of tools like ```cbindgen``` to generate header files for ```C``` - - We will see how ```unsafe``` wrappers can act as a bridge to safe Rust code +### Сложный пример FFI +- В следующих примерах мы создадим интерфейс логирования на Rust и предоставим его для [PYTHON] и ```C``` + - Мы увидим, как один и тот же интерфейс можно использовать нативно из Rust и из C + - Мы рассмотрим инструменты вроде ```cbindgen`` для генерации заголовочных файлов для ```C``` + - Мы увидим, как обёртки ```unsafe``` служат мостом к безопасному коду на Rust -## Logger helper functions +## Вспомогательные функции логгера ```rust fn create_or_open_log_file(log_file: &str, overwrite: bool) -> Result<File, String> { if overwrite { @@ -113,7 +112,7 @@ fn log_to_file(file_handle: &mut File, message: &str) -> Result<(), String> { } ``` -## Logger struct +## Структура логгера ```rust struct SimpleLogger { log_level: LogLevel, @@ -141,39 +140,39 @@ impl SimpleLogger { } ``` -## Testing -- Testing functionality with Rust is trivial - - Test methods are decorated with ```#[test]```, and aren't part of the compiled binary - - It's easy to create mock methods for testing purposes +## Тестирование +- Тестирование в Rust — это просто + - Тестовые функции помечаются ```#[test]``` и не входят в скомпилированный бинарный файл + - Легко создавать мок-методы для тестов ```rust #[test] fn testfunc() -> Result<(), String> { let mut logger = SimpleLogger::new("test.log", false, LogLevel::INFO)?; logger.log_message(LogLevel::TRACELEVEL1, "Hello world")?; logger.log_message(LogLevel::CRITICAL, "Critical message")?; - Ok(()) // The compiler automatically drops logger here + Ok(()) // Компилятор автоматически уничтожает logger здесь } ``` ```bash cargo test ``` -## (C)-Rust FFI -- cbindgen is a great tool for generating header files for exported Rust functions - - Can be installed using cargo +## FFI: вызов из C в Rust +- cbindgen — отличный инструмент для генерации заголовочных файлов для экспортируемых функций Rust + - Устанавливается через cargo ```bash cargo install cbindgen cbindgen ``` -- Function and structures can be exported using ```#[no_mangle]``` and ```#[repr(C)]``` - - We'll assume the common interface pattern passing in a `**` to the actual implementation and returning 0 on success and non-zero on error - - **Opaque vs transparent structs**: Our `SimpleLogger` is passed as an *opaque pointer* (`*mut SimpleLogger`) — the C side never accesses its fields, so `#[repr(C)]` is **not** needed. Use `#[repr(C)]` when C code needs to read/write struct fields directly: +- Функции и структуры экспортируются с помощью ```#[no_mangle]``` и ```#[repr(C)]``` + - Мы будем использовать распространённый шаблон интерфейса: передаём `**` в реализацию и возвращаем 0 при успехе и ненулевое значение при ошибке + - **Непрозрачные и прозрачные структуры**: наш `SimpleLogger` передаётся как *непрозрачный указатель* (`*mut SimpleLogger`) — сторона C никогда не обращается к его полям, поэтому `#[repr(C)]` **не** нужен. Используйте `#[repr(C)]`, когда код на C должен напрямую читать и записывать поля структуры: ```rust -// Opaque — C only holds a pointer, never inspects fields. No #[repr(C)] needed. -struct SimpleLogger { /* Rust-only fields */ } +// Непрозрачная — C хранит только указатель и никогда не проверяет поля. #[repr(C)] не нужен. +struct SimpleLogger { /* Поля только для Rust */ } -// Transparent — C reads/writes fields directly. MUST use #[repr(C)]. +// Прозрачная — C читает и записывает поля напрямую. ОБЯЗАТЕЛЬНО #[repr(C)]. #[repr(C)] pub struct Point { pub x: f64, @@ -187,36 +186,36 @@ uint32_t log_entry(struct SimpleLogger *logger, const char *message); uint32_t drop_logger(struct SimpleLogger *logger); ``` -- Note that we need to a lot of sanity checks -- We have to explicitly leak memory to prevent Rust from automatically deallocating +- Обратите внимание, что нам нужно много проверок на корректность +- Мы должны явно «утечь» память, чтобы Rust не освободил её автоматически ```rust #[no_mangle] pub extern "C" fn create_simple_logger(file_name: *const std::os::raw::c_char, out_logger: *mut *mut SimpleLogger) -> u32 { use std::ffi::CStr; - // Make sure pointer isn't NULL + // Убеждаемся, что указатель не NULL if file_name.is_null() || out_logger.is_null() { return 1; } - // Safety: The passed in pointer is either NULL or 0-terminated by contract + // Safety: переданный указатель либо NULL, либо по контракту заканчивается нулём let file_name = unsafe { CStr::from_ptr(file_name) }; let file_name = file_name.to_str(); - // Make sure that file_name doesn't have garbage characters + // Убеждаемся, что в file_name нет мусорных символов if file_name.is_err() { return 1; } let file_name = file_name.unwrap(); - // Assume some defaults; we'll pass them in real life + // Задаём значения по умолчанию; в реальности их следовало бы передавать let new_logger = SimpleLogger::new(file_name, true, LogLevel::CRITICAL); - // Check that we were able to construct the logger + // Проверяем, что логгер удалось создать if new_logger.is_err() { return 1; } let new_logger = Box::new(new_logger.unwrap()); - // This prevents the Box from being dropped when if goes out of scope + // Это не даёт Box уничтожиться при выходе из области видимости let logger_ptr: *mut SimpleLogger = Box::leak(new_logger); - // Safety: logger is non-null and logger_ptr is valid + // Safety: logger не NULL, а logger_ptr валиден unsafe { *out_logger = logger_ptr; } @@ -224,7 +223,7 @@ pub extern "C" fn create_simple_logger(file_name: *const std::os::raw::c_char, o } ``` -- We have similar error checks in ```log_entry()``` +- Аналогичные проверки ошибок есть и в ```log_entry()``` ```rust #[no_mangle] pub extern "C" fn log_entry(logger: *mut SimpleLogger, message: *const std::os::raw::c_char) -> u32 { @@ -232,16 +231,16 @@ pub extern "C" fn log_entry(logger: *mut SimpleLogger, message: *const std::os:: if message.is_null() || logger.is_null() { return 1; } - // Safety: message is non-null + // Safety: message не NULL let message = unsafe { CStr::from_ptr(message) }; let message = message.to_str(); - // Make sure that file_name doesn't have garbage characters + // Убеждаемся, что в сообщении нет мусорных символов if message.is_err() { return 1; } - // Safety: logger is valid pointer previously constructed by create_simple_logger() + // Safety: logger — валидный указатель, ранее созданный create_simple_logger() unsafe { (*logger).log_message(LogLevel::CRITICAL, message.unwrap()).is_err() as u32 } @@ -252,24 +251,24 @@ pub extern "C" fn drop_logger(logger: *mut SimpleLogger) -> u32 { if logger.is_null() { return 1; } - // Safety: logger is valid pointer previously constructed by create_simple_logger() + // Safety: logger — валидный указатель, ранее созданный create_simple_logger() unsafe { - // This constructs a Box<SimpleLogger>, which is dropped when it goes out of scope + // Это создаёт Box<SimpleLogger>, который уничтожается при выходе из области видимости let _ = Box::from_raw(logger); } 0 } ``` -- We can test our (C)-FFI using Rust, or by writing a (C)-program +- Тестировать наш FFI (C) можно средствами Rust или написав программу на C ```rust #[test] fn test_c_logger() { - // The c".." creates a NULL terminated string + // Конструкция c".." создаёт строку с завершающим нулём let file_name = c"test.log".as_ptr() as *const std::os::raw::c_char; let mut c_logger: *mut SimpleLogger = std::ptr::null_mut(); assert_eq!(create_simple_logger(file_name, &mut c_logger), 0); - // This is the manual way to create c"..." strings + // Ручной способ создать строку c"..." let message = b"message from C\0".as_ptr() as *const std::os::raw::c_char; assert_eq!(log_entry(c_logger, message), 0); drop_logger(c_logger); @@ -282,96 +281,96 @@ int main() { SimpleLogger *logger = NULL; if (create_simple_logger("test.log", &logger) == 0) { log_entry(logger, "Hello from C"); - drop_logger(logger); /*Needed to close handle, etc.*/ + drop_logger(logger); /* Необходимо для закрытия дескриптора и т. д. */ } ... } ``` -## Ensuring correctness of unsafe code -- The TL;DR version is that using ```unsafe``` requires deliberate thought - - Always document the safety assumptions made by the code and review it with experts - - Use tools like cbindgen, Miri, Valgrind that can help verify correctness - - **Never let a panic unwind across an FFI boundary** — this is UB. Use `std::panic::catch_unwind` at FFI entry points, or configure `panic = "abort"` in your profile - - If a struct is shared across FFI, mark it `#[repr(C)]` to guarantee C-compatible memory layout - - Consult https://doc.rust-lang.org/nomicon/intro.html (the "Rustonomicon" — the dark arts of unsafe Rust) - - Seek help of internal experts +## Обеспечение корректности unsafe-кода +- Если коротко: использование ```unsafe``` требует осознанных размышлений + - Всегда документируйте предположения о безопасности, которые делает код, и проверяйте его с экспертами + - Используйте инструменты вроде cbindgen, Miri, Valgrind, которые помогают проверять корректность + - **Никогда не позволяйте panic раскручиваться через границу FFI** — это неопределённое поведение. Используйте `std::panic::catch_unwind` в точках входа FFI или настройте `panic = "abort"` в профиле + - Если структура используется через FFI, пометьте её `#[repr(C)]`, чтобы гарантировать совместимую с C раскладку в памяти + - Обратитесь к https://doc.rust-lang.org/nomicon/intro.html («Rustonomicon» — тёмные искусства unsafe Rust) + - Обращайтесь за помощью к внутренним экспертам -### Verification tools: Miri vs Valgrind +### Инструменты проверки: Miri и Valgrind -C++ developers are familiar with Valgrind and sanitizers. Rust has those **plus** Miri, which is far more precise for Rust-specific UB: +Программисты C++ знакомы с Valgrind и санитайзерами. У Rust есть всё это **плюс** Miri, который гораздо точнее для UB, специфичного для Rust: -| | **Miri** | **Valgrind** | **C++ sanitizers (ASan/MSan/UBSan)** | +| | **Miri** | **Valgrind** | **Санитайзеры C++ (ASan/MSan/UBSan)** | |---|---------|-------------|--------------------------------------| -| **What it catches** | Rust-specific UB: stacked borrows, invalid `enum` discriminants, uninitialized reads, aliasing violations | Memory leaks, use-after-free, invalid reads/writes, uninitialized memory | Buffer overflow, use-after-free, data races, UB | -| **How it works** | Interprets MIR (Rust's mid-level IR) — no native execution | Instruments compiled binary at runtime | Compile-time instrumentation | -| **FFI support** | ❌ Cannot cross FFI boundary (skips C calls) | ✅ Works on any compiled binary, including FFI | ✅ Works if C code also compiled with sanitizers | -| **Speed** | ~100x slower than native | ~10-50x slower | ~2-5x slower | -| **When to use** | Pure Rust `unsafe` code, data structure invariants | FFI code, full binary integration tests | C/C++ side of FFI, performance-sensitive testing | -| **Catches aliasing bugs** | ✅ Stacked Borrows model | ❌ | Partially (TSan for data races) | +| **Что находит** | UB, специфичное для Rust: stacked borrows, некорректные дискриминанты `enum`, чтение неинициализированных данных, нарушения алиасинга | Утечки памяти, использование после освобождения, некорректные чтения/записи, неинициализированная память | Переполнение буфера, использование после освобождения, гонки данных, UB | +| **Как работает** | Интерпретирует MIR (промежуточное представление Rust) — без нативного выполнения | Инструментирует скомпилированный бинарный файл во время выполнения | Инструментирование на этапе компиляции | +| **Поддержка FFI** | ❌ Не может пересекать границу FFI (пропускает вызовы C) | ✅ Работает с любым скомпилированным бинарным файлом, включая FFI | ✅ Работает, если код C тоже скомпилирован с санитайзерами | +| **Скорость** | ~100x медленнее нативного | ~10-50x медленнее | ~2-5x медленнее | +| **Когда использовать** | Чистый Rust `unsafe`-код, инварианты структур данных | Код FFI, полные интеграционные тесты бинарного файла | Сторона C/C++ в FFI, тестирование, чувствительное к производительности | +| **Находит ошибки алиасинга** | ✅ Модель Stacked Borrows | ❌ | Частично (TSan для гонок данных) | -**Recommendation**: Use **both** — Miri for pure Rust unsafe, Valgrind for FFI integration: +**Рекомендация**: используйте **оба** — Miri для чистого unsafe Rust, Valgrind для интеграции FFI: -- **Miri** — catches Rust-specific UB that Valgrind cannot see (aliasing violations, invalid enum values, stacked borrows): +- **Miri** — находит UB, специфичное для Rust, которое Valgrind не видит (нарушения алиасинга, некорректные значения enum, stacked borrows): ``` rustup +nightly component add miri - cargo +nightly miri test # Run all tests under Miri - cargo +nightly miri test -- test_name # Run a specific test + cargo +nightly miri test # Запустить все тесты под Miri + cargo +nightly miri test -- test_name # Запустить конкретный тест ``` - > ⚠️ Miri requires nightly and cannot execute FFI calls. Isolate unsafe Rust logic into testable units. + > ⚠️ Miri требует nightly и не может выполнять вызовы FFI. Выносите логику unsafe-Rust в тестируемые модули. -- **Valgrind** — the tool you already know, works on the compiled binary including FFI: +- **Valgrind** — инструмент, который вы уже знаете; работает со скомпилированным бинарным файлом, включая FFI: ``` sudo apt install valgrind cargo install cargo-valgrind - cargo valgrind test # Run all tests under Valgrind + cargo valgrind test # Запустить все тесты под Valgrind ``` - > Catches leaks in `Box::leak` / `Box::from_raw` patterns common in FFI code. + > Находит утечки в шаблонах `Box::leak` / `Box::from_raw`, которые распространены в коде FFI. -- **cargo-careful** — runs tests with extra runtime checks enabled (between regular tests and Miri): +- **cargo-careful** — запускает тесты с дополнительными проверками времени выполнения (между обычными тестами и Miri): ``` cargo install cargo-careful cargo +nightly careful test ``` -## Unsafe Rust summary -- ```cbindgen``` is a great tool for (C) FFI to Rust - - Use ```bindgen``` for FFI-interfaces in the other direction (consult the extensive documentation) -- **Do not assume that your unsafe code is correct, or that it's fine to use from safe Rust. It's really easy to make mistakes, and even code that seemingly works correctly can be wrong for subtle reasons** - - Use tools to verify correctness - - If still in doubt, reach out for expert advice -- Make sure that your ```unsafe``` code has comments with an explicit documentation about assumptions and why it's correct - - Callers of ```unsafe``` code should have corresponding comments on safety as well, and observe restrictions +## Итоги по unsafe Rust +- ```cbindgen``` — отличный инструмент для FFI (C) в сторону Rust + - Для FFI-интерфейсов в обратную сторону используйте ```bindgen``` (см. обширную документацию) +- **Не предполагайте, что ваш unsafe-код корректен или что его безопасно использовать из безопасного Rust. Ошибиться очень легко, и даже код, который внешне работает правильно, может быть неверным по тонким причинам** + - Используйте инструменты для проверки корректности + - Если сомневаетесь, обратитесь за экспертным советом +- Убедитесь, что ваш код с ```unsafe``` содержит комментарии с явным описанием предположений и причин, по которым он корректен + - У вызывающего кода ```unsafe``` тоже должны быть соответствующие комментарии о безопасности, и он должен соблюдать ограничения -# Exercise: Writing a safe FFI wrapper +# Упражнение: написание безопасной обёртки FFI -🔴 **Challenge** — requires understanding unsafe blocks, raw pointers, and safe API design +🔴 **Сложный уровень** — требует понимания блоков unsafe, сырых указателей и проектирования безопасного API -- Write a safe Rust wrapper around an `unsafe` FFI-style function. The exercise simulates calling a C function that writes a formatted string into a caller-provided buffer. -- **Step 1**: Implement the unsafe function `unsafe_greet` that writes a greeting into a raw `*mut u8` buffer -- **Step 2**: Write a safe wrapper `safe_greet` that allocates a `Vec<u8>`, calls the unsafe function, and returns a `String` -- **Step 3**: Add proper `// Safety:` comments to every unsafe block +- Напишите безопасную обёртку на Rust вокруг функции `unsafe` в стиле FFI. Упражнение имитирует вызов функции на C, которая записывает отформатированную строку в буфер, предоставленный вызывающей стороной +- **Шаг 1**: реализуйте небезопасную функцию `unsafe_greet`, которая записывает приветствие в сырой буфер `*mut u8` +- **Шаг 2**: напишите безопасную обёртку `safe_greet`, которая выделяет `Vec<u8>`, вызывает небезопасную функцию и возвращает `String` +- **Шаг 3**: добавьте корректные комментарии `// Safety:` к каждому блоку unsafe -**Starter code:** +**Стартовый код:** ```rust use std::fmt::Write as _; -/// Simulates a C function: writes "Hello, <name>!" into buffer. -/// Returns the number of bytes written (excluding null terminator). +/// Имитирует функцию на C: записывает "Hello, <name>!" в буфер. +/// Возвращает количество записанных байтов (без нулевого терминатора). /// # Safety -/// - `buf` must point to at least `buf_len` writable bytes -/// - `name` must be a valid pointer to a null-terminated C string +/// - `buf` должен указывать на не менее чем `buf_len` доступных для записи байтов +/// - `name` должен быть валидным указателем на C-строку с завершающим нулём unsafe fn unsafe_greet(buf: *mut u8, buf_len: usize, name: *const u8) -> isize { - // TODO: Build greeting, copy bytes into buf, return length - // Hint: use std::ffi::CStr::from_ptr or iterate bytes manually + // TODO: Сформируйте приветствие, скопируйте байты в buf, верните длину + // Подсказка: используйте std::ffi::CStr::from_ptr или перебирайте байты вручную todo!() } -/// Safe wrapper — no unsafe in the public API +/// Безопасная обёртка — в публичном API нет unsafe fn safe_greet(name: &str) -> Result<String, String> { - // TODO: Allocate a Vec<u8> buffer, create a null-terminated name, - // call unsafe_greet inside an unsafe block with Safety comment, - // convert the result back to a String + // TODO: Выделите буфер Vec<u8>, создайте C-строку с нулём в конце для имени, + // вызовите unsafe_greet внутри блока unsafe с комментарием Safety, + // преобразуйте результат обратно в String todo!() } @@ -380,22 +379,22 @@ fn main() { Ok(msg) => println!("{msg}"), Err(e) => eprintln!("Error: {e}"), } - // Expected output: Hello, Rustacean! + // Ожидаемый вывод: Hello, Rustacean! } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::ffi::CStr; -/// Simulates a C function: writes "Hello, <name>!" into buffer. -/// Returns the number of bytes written, or -1 if buffer too small. +/// Имитирует функцию на C: записывает "Hello, <name>!" в буфер. +/// Возвращает количество записанных байтов или -1, если буфер слишком мал. /// # Safety -/// - `buf` must point to at least `buf_len` writable bytes -/// - `name` must be a valid pointer to a null-terminated C string +/// - `buf` должен указывать на не менее чем `buf_len` доступных для записи байтов +/// - `name` должен быть валидным указателем на C-строку с завершающим нулём unsafe fn unsafe_greet(buf: *mut u8, buf_len: usize, name: *const u8) -> isize { - // Safety: caller guarantees name is a valid null-terminated string + // Safety: вызывающая сторона гарантирует, что name — валидная строка с завершающим нулём let name_cstr = unsafe { CStr::from_ptr(name as *const std::os::raw::c_char) }; let name_str = match name_cstr.to_str() { Ok(s) => s, @@ -405,20 +404,20 @@ unsafe fn unsafe_greet(buf: *mut u8, buf_len: usize, name: *const u8) -> isize { if greeting.len() > buf_len { return -1; } - // Safety: buf points to at least buf_len writable bytes (caller guarantee) + // Safety: buf указывает на не менее чем buf_len доступных для записи байтов (гарантия вызывающей стороны) unsafe { std::ptr::copy_nonoverlapping(greeting.as_ptr(), buf, greeting.len()); } greeting.len() as isize } -/// Safe wrapper — no unsafe in the public API +/// Безопасная обёртка — в публичном API нет unsafe fn safe_greet(name: &str) -> Result<String, String> { let mut buffer = vec![0u8; 256]; - // Create a null-terminated version of name for the C API + // Создаём версию name с завершающим нулём для C API let name_with_null: Vec<u8> = name.bytes().chain(std::iter::once(0)).collect(); - // Safety: buffer has 256 writable bytes, name_with_null is null-terminated + // Safety: в buffer 256 доступных для записи байтов, name_with_null заканчивается нулём let bytes_written = unsafe { unsafe_greet(buffer.as_mut_ptr(), buffer.len(), name_with_null.as_ptr()) }; @@ -437,7 +436,7 @@ fn main() { Err(e) => eprintln!("Error: {e}"), } } -// Output: +// Вывод: // Hello, Rustacean! ``` @@ -445,4 +444,3 @@ fn main() { ---- - From ac769e0734ff3d1c1b18806f17853876855cb040 Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 09:55:51 +0400 Subject: [PATCH 14/18] Translate C/C++ book chapters 15-19 and TOC to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for ch15 through ch19 and ch00 (introduction, self-study guide and table of contents). Code identifiers, crate names, file names and links are unchanged. TOC anchors in ch00 and all cross-chapter anchors are updated to the Russian heading slugs. Two pre-existing broken references are fixed along the way: the ch16 Case Study 3-5 link now points at ch16-cases-3-5-lifetime-borrowing.md, and the "Exercise: Think Before You Translate" entry now points at its actual heading in ch16. All 1356 internal anchors resolve in the built book. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- c-cpp-book/src/ch00-introduction.md | 408 ++++++------- c-cpp-book/src/ch15-1-embedded-deep-dive.md | 435 +++++++------- ...o_std-rust-without-the-standard-library.md | 169 +++--- c-cpp-book/src/ch16-case-studies.md | 133 ++--- .../src/ch16-cases-3-5-lifetime-borrowing.md | 165 +++-- .../src/ch17-1-avoiding-excessive-clone.md | 371 ++++++------ .../src/ch17-2-avoiding-unchecked-indexing.md | 235 ++++---- .../ch17-3-collapsing-assignment-pyramids.md | 86 +-- .../ch17-4-logging-and-tracing-ecosystem.md | 128 ++-- c-cpp-book/src/ch17-best-practices.md | 229 ++++--- .../src/ch18-cpp-rust-semantic-deep-dives.md | 564 +++++++++--------- c-cpp-book/src/ch19-macros.md | 412 ++++++------- 12 files changed, 1644 insertions(+), 1691 deletions(-) diff --git a/c-cpp-book/src/ch00-introduction.md b/c-cpp-book/src/ch00-introduction.md index 6a099c4..fc5a629 100644 --- a/c-cpp-book/src/ch00-introduction.md +++ b/c-cpp-book/src/ch00-introduction.md @@ -1,216 +1,216 @@ -# Rust Bootstrap Course for C/C++ Programmers - -## Course Overview -- Course overview - - The case for Rust (from both C and C++ perspectives) - - Local installation - - Types, functions, control flow, pattern matching - - Modules, cargo - - Traits, generics - - Collections, error handling - - Closures, memory management, lifetimes, smart pointers - - Concurrency - - Unsafe Rust, including Foreign Function Interface (FFI) - - `no_std` and embedded Rust essentials for firmware teams - - Case studies: real-world C++ to Rust translation patterns -- We'll not cover `async` Rust in this course — see the companion [Async Rust Training](../async-book/) for a full treatment of futures, executors, `Pin`, tokio, and production async patterns +# Курс Rust для программистов на C/C++ + +## Обзор курса +- Обзор курса + - Аргументы в пользу Rust (с точки зрения и C, и C++) + - Локальная установка + - Типы, функции, управление потоком выполнения, сопоставление с образцом + - Модули, cargo + - Трейты, обобщения + - Коллекции, обработка ошибок + - Замыкания, управление памятью, времена жизни, умные указатели + - Конкурентность + - Небезопасный Rust, включая Foreign Function Interface (FFI) + - `no_std` и основы встраиваемого Rust для команд, разрабатывающих прошивки + - Примеры из практики: типовые шаблоны перевода реального кода C++ на Rust +- Асинхронный Rust в этом курсе не рассматривается — см. сопутствующий [курс по Async Rust](../async-book/), где подробно разобраны футуры, исполнители, `Pin`, tokio и промышленные асинхронные шаблоны --- -# Self-Study Guide +# Руководство для самостоятельного изучения -This material works both as an instructor-led course and for self-study. If you're working through it on your own, here's how to get the most out of it: +Материал подходит как для курса с преподавателем, так и для самостоятельного изучения. Если вы работаете с ним самостоятельно, вот как получить от него максимум пользы: -**Pacing recommendations:** +**Рекомендации по темпу:** -| Chapters | Topic | Suggested Time | Checkpoint | -|----------|-------|---------------|------------| -| 1–4 | Setup, types, control flow | 1 day | You can write a CLI temperature converter | -| 5–7 | Data structures, ownership | 1–2 days | You can explain *why* `let s2 = s1` invalidates `s1` | -| 8–9 | Modules, error handling | 1 day | You can create a multi-file project that propagates errors with `?` | -| 10–12 | Traits, generics, closures | 1–2 days | You can write a generic function with trait bounds | -| 13–14 | Concurrency, unsafe/FFI | 1 day | You can write a thread-safe counter with `Arc<Mutex<T>>` | -| 15–16 | Deep dives | At your own pace | Reference material — read when relevant | -| 17–19 | Best practices & reference | At your own pace | Consult as you write real code | +| Главы | Тема | Примерное время | Контрольная точка | +|-------|------|-----------------|-------------------| +| 1–4 | Настройка, типы, управление потоком выполнения | 1 день | Вы можете написать CLI-конвертер температур | +| 5–7 | Структуры данных, владение | 1–2 дня | Вы можете объяснить, *почему* `let s2 = s1` делает `s1` недействительной | +| 8–9 | Модули, обработка ошибок | 1 день | Вы можете создать многофайловый проект, который передаёт ошибки дальше с помощью `?` | +| 10–12 | Трейты, обобщения, замыкания | 1–2 дня | Вы можете написать обобщённую функцию с ограничениями трейтами | +| 13–14 | Конкурентность, unsafe/FFI | 1 день | Вы можете написать потокобезопасный счётчик с `Arc<Mutex<T>>` | +| 15–16 | Углублённые темы | В своём темпе | Справочный материал — читайте, когда это актуально | +| 17–19 | Лучшие практики и справочник | В своём темпе | Обращайтесь к нему, когда пишете реальный код | -**How to use the exercises:** -- Every chapter has hands-on exercises marked with difficulty: 🟢 Starter, 🟡 Intermediate, 🔴 Challenge -- **Always try the exercise before expanding the solution.** Struggling with the borrow checker is part of learning — the compiler's error messages are your teacher -- If you're stuck for more than 15 minutes, expand the solution, study it, then close it and try again from scratch -- The [Rust Playground](https://play.rust-lang.org/) lets you run code without a local install +**Как работать с упражнениями:** +- В каждой главе есть практические упражнения с указанием сложности: 🟢 Начальный, 🟡 Средний, 🔴 Сложный +- **Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение.** Борьба с проверкой заимствований — часть обучения, а сообщения компилятора — ваш учитель +- Если вы застряли больше чем на 15 минут, раскройте решение, изучите его, затем закройте и попробуйте снова с нуля +- [Rust Playground](https://play.rust-lang.org/) позволяет запускать код без локальной установки -**When you hit a wall:** -- Read the compiler error message carefully — Rust's errors are exceptionally helpful -- Re-read the relevant section; concepts like ownership (ch7) often click on the second pass -- The [Rust standard library docs](https://doc.rust-lang.org/std/) are excellent — search for any type or method -- For async patterns, see the companion [Async Rust Training](../async-book/) +**Если вы упёрлись в стену:** +- Внимательно читайте сообщение компилятора: ошибки Rust исключительно полезны +- Перечитайте нужный раздел. Концепции вроде владения (гл. 7) часто становятся понятны при втором прочтении +- [Документация стандартной библиотеки Rust](https://doc.rust-lang.org/std/) отличная: ищите любой тип или метод +- Более глубокие асинхронные шаблоны описаны в сопутствующем курсе [Async Rust](../async-book/) --- -# Table of Contents - -## Part I — Foundations - -### 1. Introduction and Motivation -- [Speaker intro and general approach](ch01-introduction-and-motivation.md#speaker-intro-and-general-approach) -- [The case for Rust](ch01-introduction-and-motivation.md#the-case-for-rust) -- [How does Rust address these issues?](ch01-introduction-and-motivation.md#how-does-rust-address-these-issues) -- [Other Rust USPs and features](ch01-introduction-and-motivation.md#other-rust-usps-and-features) -- [Quick Reference: Rust vs C/C++](ch01-introduction-and-motivation.md#quick-reference-rust-vs-cc) -- [Why C/C++ Developers Need Rust](ch01-1-why-c-cpp-developers-need-rust.md) - - [What Rust Eliminates — The Complete List](ch01-1-why-c-cpp-developers-need-rust.md#what-rust-eliminates--the-complete-list) - - [The Problems Shared by C and C++](ch01-1-why-c-cpp-developers-need-rust.md#the-problems-shared-by-c-and-c) - - [C++ Adds More Problems on Top](ch01-1-why-c-cpp-developers-need-rust.md#c-adds-more-problems-on-top) - - [How Rust Addresses All of This](ch01-1-why-c-cpp-developers-need-rust.md#how-rust-addresses-all-of-this) - -### 2. Getting Started -- [Enough talk already: Show me some code](ch02-getting-started.md#enough-talk-already-show-me-some-code) -- [Rust Local installation](ch02-getting-started.md#rust-local-installation) -- [Rust packages (crates)](ch02-getting-started.md#rust-packages-crates) -- [Example: cargo and crates](ch02-getting-started.md#example-cargo-and-crates) - -### 3. Basic Types and Variables -- [Built-in Rust types](ch03-built-in-types.md#built-in-rust-types) -- [Rust type specification and assignment](ch03-built-in-types.md#rust-type-specification-and-assignment) -- [Rust type specification and inference](ch03-built-in-types.md#rust-type-specification-and-inference) -- [Rust variables and mutability](ch03-built-in-types.md#rust-variables-and-mutability) - -### 4. Control Flow -- [Rust if keyword](ch04-control-flow.md#rust-if-keyword) -- [Rust loops using while and for](ch04-control-flow.md#rust-loops-using-while-and-for) -- [Rust loops using loop](ch04-control-flow.md#rust-loops-using-loop) -- [Rust expression blocks](ch04-control-flow.md#rust-expression-blocks) - -### 5. Data Structures and Collections -- [Rust array type](ch05-data-structures.md#rust-array-type) -- [Rust tuples](ch05-data-structures.md#rust-tuples) -- [Rust references](ch05-data-structures.md#rust-references) -- [C++ References vs Rust References — Key Differences](ch05-data-structures.md#c-references-vs-rust-references--key-differences) -- [Rust slices](ch05-data-structures.md#rust-slices) -- [Rust constants and statics](ch05-data-structures.md#rust-constants-and-statics) -- [Rust strings: String vs &str](ch05-data-structures.md#rust-strings-string-vs-str) -- [Rust structs](ch05-data-structures.md#rust-structs) -- [Rust Vec\<T\>](ch05-data-structures.md#rust-vec-type) -- [Rust HashMap](ch05-data-structures.md#rust-hashmap-type) -- [Exercise: Vec and HashMap](ch05-data-structures.md#exercise-vec-and-hashmap) - -### 6. Pattern Matching and Enums -- [Rust enum types](ch06-enums-and-pattern-matching.md#rust-enum-types) -- [Rust match statement](ch06-enums-and-pattern-matching.md#rust-match-statement) -- [Exercise: Implement add and subtract using match and enum](ch06-enums-and-pattern-matching.md#exercise-implement-add-and-subtract-using-match-and-enum) - -### 7. Ownership and Memory Management -- [Rust memory management](ch07-ownership-and-borrowing.md#rust-memory-management) -- [Rust ownership, borrowing and lifetimes](ch07-ownership-and-borrowing.md#rust-ownership-borrowing-and-lifetimes) -- [Rust move semantics](ch07-ownership-and-borrowing.md#rust-move-semantics) -- [Rust Clone](ch07-ownership-and-borrowing.md#rust-clone) -- [Rust Copy trait](ch07-ownership-and-borrowing.md#rust-copy-trait) -- [Rust Drop trait](ch07-ownership-and-borrowing.md#rust-drop-trait) -- [Exercise: Move, Copy and Drop](ch07-ownership-and-borrowing.md#exercise-move-copy-and-drop) -- [Rust lifetime and borrowing](ch07-1-lifetimes-and-borrowing-deep-dive.md#rust-lifetime-and-borrowing) -- [Rust lifetime annotations](ch07-1-lifetimes-and-borrowing-deep-dive.md#rust-lifetime-annotations) -- [Exercise: Slice storage with lifetimes](ch07-1-lifetimes-and-borrowing-deep-dive.md#exercise-slice-storage-with-lifetimes) -- [Lifetime Elision Rules Deep Dive](ch07-1-lifetimes-and-borrowing-deep-dive.md#lifetime-elision-rules-deep-dive) -- [Rust Box\<T\>](ch07-2-smart-pointers-and-interior-mutability.md#rust-boxt) -- [Interior Mutability: Cell\<T\> and RefCell\<T\>](ch07-2-smart-pointers-and-interior-mutability.md#interior-mutability-cellt-and-refcellt) -- [Shared Ownership: Rc\<T\>](ch07-2-smart-pointers-and-interior-mutability.md#shared-ownership-rct) -- [Exercise: Shared ownership and interior mutability](ch07-2-smart-pointers-and-interior-mutability.md#exercise-shared-ownership-and-interior-mutability) - -### 8. Modules and Crates -- [Rust crates and modules](ch08-crates-and-modules.md#rust-crates-and-modules) -- [Exercise: Modules and functions](ch08-crates-and-modules.md#exercise-modules-and-functions) -- [Workspaces and crates (packages)](ch08-crates-and-modules.md#workspaces-and-crates-packages) -- [Exercise: Using workspaces and package dependencies](ch08-crates-and-modules.md#exercise-using-workspaces-and-package-dependencies) -- [Using community crates from crates.io](ch08-crates-and-modules.md#using-community-crates-from-cratesio) -- [Crates dependencies and SemVer](ch08-crates-and-modules.md#crates-dependencies-and-semver) -- [Exercise: Using the rand crate](ch08-crates-and-modules.md#exercise-using-the-rand-crate) -- [Cargo.toml and Cargo.lock](ch08-crates-and-modules.md#cargotoml-and-cargolock) -- [Cargo test feature](ch08-crates-and-modules.md#cargo-test-feature) -- [Other Cargo features](ch08-crates-and-modules.md#other-cargo-features) -- [Testing Patterns](ch08-1-testing-patterns.md) - -### 9. Error Handling -- [Connecting enums to Option and Result](ch09-error-handling.md#connecting-enums-to-option-and-result) -- [Rust Option type](ch09-error-handling.md#rust-option-type) -- [Rust Result type](ch09-error-handling.md#rust-result-type) -- [Exercise: log() function implementation with Option](ch09-error-handling.md#exercise-log-function-implementation-with-option) -- [Rust error handling](ch09-error-handling.md#rust-error-handling) -- [Exercise: error handling](ch09-error-handling.md#exercise-error-handling) -- [Error Handling Best Practices](ch09-1-error-handling-best-practices.md) - -### 10. Traits and Generics -- [Rust traits](ch10-traits.md#rust-traits) -- [C++ Operator Overloading → Rust std::ops Traits](ch10-traits.md#c-operator-overloading--rust-stdops-traits) -- [Exercise: Logger trait implementation](ch10-traits.md#exercise-logger-trait-implementation) -- [When to use enum vs dyn Trait](ch10-traits.md#when-to-use-enum-vs-dyn-trait) -- [Exercise: Think Before You Translate](ch10-traits.md#exercise-think-before-you-translate) -- [Rust generics](ch10-1-generics.md#rust-generics) -- [Exercise: Generics](ch10-1-generics.md#exercise-generics) -- [Combining Rust traits and generics](ch10-1-generics.md#combining-rust-traits-and-generics) -- [Rust traits constraints in data types](ch10-1-generics.md#rust-traits-constraints-in-data-types) -- [Exercise: Trait constraints and generics](ch10-1-generics.md#exercise-traits-constraints-and-generics) -- [Rust type state pattern and generics](ch10-1-generics.md#rust-type-state-pattern-and-generics) -- [Rust builder pattern](ch10-1-generics.md#rust-builder-pattern) - -### 11. Type System Advanced Features -- [Rust From and Into traits](ch11-from-and-into-traits.md#rust-from-and-into-traits) -- [Exercise: From and Into](ch11-from-and-into-traits.md#exercise-from-and-into) -- [Rust Default trait](ch11-from-and-into-traits.md#rust-default-trait) -- [Other Rust type conversions](ch11-from-and-into-traits.md#other-rust-type-conversions) - -### 12. Functional Programming -- [Rust closures](ch12-closures.md#rust-closures) -- [Exercise: Closures and capturing](ch12-closures.md#exercise-closures-and-capturing) -- [Rust iterators](ch12-closures.md#rust-iterators) -- [Exercise: Rust iterators](ch12-closures.md#exercise-rust-iterators) -- [Iterator Power Tools Reference](ch12-1-iterator-power-tools.md#iterator-power-tools-reference) - -### 13. Concurrency -- [Rust concurrency](ch13-concurrency.md#rust-concurrency) -- [Why Rust prevents data races: Send and Sync](ch13-concurrency.md#why-rust-prevents-data-races-send-and-sync) -- [Exercise: Multi-threaded word count](ch13-concurrency.md#exercise-multi-threaded-word-count) - -### 14. Unsafe Rust and FFI -- [Unsafe Rust](ch14-unsafe-rust-and-ffi.md#unsafe-rust) -- [Simple FFI example](ch14-unsafe-rust-and-ffi.md#simple-ffi-example-rust-library-function-consumed-by-c) -- [Complex FFI example](ch14-unsafe-rust-and-ffi.md#complex-ffi-example) -- [Ensuring correctness of unsafe code](ch14-unsafe-rust-and-ffi.md#ensuring-correctness-of-unsafe-code) -- [Exercise: Writing a safe FFI wrapper](ch14-unsafe-rust-and-ffi.md#exercise-writing-a-safe-ffi-wrapper) - -## Part II — Deep Dives - -### 15. no_std — Rust for Bare Metal -- [What is no_std?](ch15-no_std-rust-without-the-standard-library.md#what-is-no_std) -- [When to use no_std vs std](ch15-no_std-rust-without-the-standard-library.md#when-to-use-no_std-vs-std) -- [Exercise: no_std ring buffer](ch15-no_std-rust-without-the-standard-library.md#exercise-no_std-ring-buffer) -- [Embedded Deep Dive](ch15-1-embedded-deep-dive.md) - -### 16. Case Studies: Real-World C++ to Rust Translation -- [Case Study 1: Inheritance hierarchy → Enum dispatch](ch16-case-studies.md#case-study-1-inheritance-hierarchy--enum-dispatch) -- [Case Study 2: shared_ptr tree → Arena/index pattern](ch16-case-studies.md#case-study-2-shared_ptr-tree--arenaindex-pattern) -- [Case Study 3: Framework communication → Lifetime borrowing](ch16-1-case-study-lifetime-borrowing.md#case-study-3-framework-communication--lifetime-borrowing) -- [Case Study 4: God object → Composable state](ch16-1-case-study-lifetime-borrowing.md#case-study-4-god-object--composable-state) -- [Case Study 5: Trait objects — when they ARE right](ch16-1-case-study-lifetime-borrowing.md#case-study-5-trait-objects--when-they-are-right) - -## Part III — Best Practices & Reference - -### 17. Best Practices -- [Rust Best Practices Summary](ch17-best-practices.md#rust-best-practices-summary) -- [Avoiding excessive clone()](ch17-1-avoiding-excessive-clone.md#avoiding-excessive-clone) -- [Avoiding unchecked indexing](ch17-2-avoiding-unchecked-indexing.md#avoiding-unchecked-indexing) -- [Collapsing assignment pyramids](ch17-3-collapsing-assignment-pyramids.md#collapsing-assignment-pyramids) -- [Capstone Exercise: Diagnostic Event Pipeline](ch17-3-collapsing-assignment-pyramids.md#capstone-exercise-diagnostic-event-pipeline) -- [Logging and Tracing Ecosystem](ch17-4-logging-and-tracing-ecosystem.md#logging-and-tracing-ecosystem) - -### 18. C++ → Rust Semantic Deep Dives -- [Casting, Preprocessor, Modules, volatile, static, constexpr, SFINAE, and more](ch18-cpp-rust-semantic-deep-dives.md) - -### 19. Rust Macros -- [Declarative macros (`macro_rules!`)](ch19-macros.md#declarative-macros-with-macro_rules) -- [Common standard library macros](ch19-macros.md#common-standard-library-macros) -- [Derive macros](ch19-macros.md#derive-macros) -- [Attribute macros](ch19-macros.md#attribute-macros) -- [Procedural macros](ch19-macros.md#procedural-macros-conceptual-overview) -- [When to use what: macros vs functions vs generics](ch19-macros.md#when-to-use-what-macros-vs-functions-vs-generics) -- [Exercises](ch19-macros.md#exercises) +# Содержание + +## Часть I — Основы + +### 1. Представление и общий подход +- [Представление преподавателя и общий подход](ch01-introduction-and-motivation.md#представление-преподавателя-и-общий-подход) +- [Аргументы в пользу Rust](ch01-introduction-and-motivation.md#аргументы-в-пользу-rust) +- [Как Rust решает эти проблемы?](ch01-introduction-and-motivation.md#как-rust-решает-эти-проблемы) +- [Другие ключевые преимущества и возможности Rust](ch01-introduction-and-motivation.md#другие-ключевые-преимущества-и-возможности-rust) +- [Краткий справочник: Rust и C/C++](ch01-introduction-and-motivation.md#краткий-справочник-rust-и-cc) +- [Почему разработчикам C/C++ нужен Rust](ch01-1-why-c-cpp-developers-need-rust.md) + - [Что Rust устраняет — полный список](ch01-1-why-c-cpp-developers-need-rust.md#что-rust-устраняет--полный-список) + - [Проблемы, общие для C и C++](ch01-1-why-c-cpp-developers-need-rust.md#проблемы-общие-для-c-и-c) + - [C++ добавляет новые проблемы](ch01-1-why-c-cpp-developers-need-rust.md#c-добавляет-новые-проблемы) + - [Как Rust решает всё это](ch01-1-why-c-cpp-developers-need-rust.md#как-rust-решает-всё-это) + +### 2. Начало работы +- [Хватит разговоров: покажите код](ch02-getting-started.md#хватит-разговоров-покажите-код) +- [Локальная установка Rust](ch02-getting-started.md#локальная-установка-rust) +- [Пакеты Rust (крейты)](ch02-getting-started.md#пакеты-rust-крейты) +- [Пример: cargo и крейты](ch02-getting-started.md#пример-cargo-и-крейты) + +### 3. Встроенные типы и переменные +- [Встроенные типы Rust](ch03-built-in-types.md#встроенные-типы-rust) +- [Указание типа и присваивание в Rust](ch03-built-in-types.md#указание-типа-и-присваивание-в-rust) +- [Указание типа и вывод типа в Rust](ch03-built-in-types.md#указание-типа-и-вывод-типа-в-rust) +- [Переменные и изменяемость в Rust](ch03-built-in-types.md#переменные-и-изменяемость-в-rust) + +### 4. Управление потоком выполнения +- [Ключевое слово if в Rust](ch04-control-flow.md#ключевое-слово-if-в-rust) +- [Циклы while и for в Rust](ch04-control-flow.md#циклы-while-и-for-в-rust) +- [Циклы loop в Rust](ch04-control-flow.md#циклы-loop-в-rust) +- [Блоки-выражения в Rust](ch04-control-flow.md#блоки-выражения-в-rust) + +### 5. Структуры данных и коллекции +- [Массивы в Rust](ch05-data-structures.md#массивы-в-rust) +- [Кортежи в Rust](ch05-data-structures.md#кортежи-в-rust) +- [Ссылки в Rust](ch05-data-structures.md#ссылки-в-rust) +- [Ссылки C++ и ссылки Rust — ключевые различия](ch05-data-structures.md#углублённый-разбор-ссылки-c-и-ссылки-rust) +- [Срезы в Rust](ch05-data-structures.md#срезы-в-rust) +- [Константы и статики в Rust](ch05-data-structures.md#константы-и-статики-в-rust) +- [Строки в Rust: String и &str](ch05-data-structures.md#строки-в-rust-string-и-str) +- [Структуры в Rust](ch05-data-structures.md#структуры-в-rust) +- [Тип Vec\<T\> в Rust](ch05-data-structures.md#тип-vec-в-rust) +- [Тип HashMap в Rust](ch05-data-structures.md#тип-hashmap-в-rust) +- [Упражнение: Vec и HashMap](ch05-data-structures.md#упражнение-vec-и-hashmap) + +### 6. Сопоставление с образцом и перечисления +- [Перечисления в Rust](ch06-enums-and-pattern-matching.md#перечисления-в-rust) +- [Оператор match в Rust](ch06-enums-and-pattern-matching.md#оператор-match-в-rust) +- [Упражнение: сложение и вычитание с помощью match и enum](ch06-enums-and-pattern-matching.md#упражнение-сложение-и-вычитание-с-помощью-match-и-enum) + +### 7. Владение и управление памятью +- [Управление памятью в Rust](ch07-ownership-and-borrowing.md#управление-памятью-в-rust) +- [Владение, заимствование и времена жизни в Rust](ch07-ownership-and-borrowing.md#владение-заимствование-и-времена-жизни-в-rust) +- [Семантика перемещения в Rust](ch07-ownership-and-borrowing.md#семантика-перемещения-в-rust) +- [Clone в Rust](ch07-ownership-and-borrowing.md#clone-в-rust) +- [Трейт Copy в Rust](ch07-ownership-and-borrowing.md#трейт-copy-в-rust) +- [Трейт Drop в Rust](ch07-ownership-and-borrowing.md#трейт-drop-в-rust) +- [Упражнение: перемещение, копирование и Drop](ch07-ownership-and-borrowing.md#упражнение-перемещение-копирование-и-drop) +- [Времена жизни и заимствование в Rust](ch07-1-lifetimes-and-borrowing-deep-dive.md#времена-жизни-и-заимствование-в-rust) +- [Аннотации времён жизни в Rust](ch07-1-lifetimes-and-borrowing-deep-dive.md#аннотации-времён-жизни-в-rust) +- [Упражнение: хранение срезов с временами жизни](ch07-1-lifetimes-and-borrowing-deep-dive.md#упражнение-хранение-срезов-с-временами-жизни) +- [Углублённый разбор правил элизии времён жизни](ch07-1-lifetimes-and-borrowing-deep-dive.md#углублённый-разбор-правил-элизии-времён-жизни) +- [Box\<T\> в Rust](ch07-2-smart-pointers-and-interior-mutability.md#boxt-в-rust) +- [Внутренняя изменяемость: Cell\<T\> и RefCell\<T\>](ch07-2-smart-pointers-and-interior-mutability.md#внутренняя-изменяемость-cellt-и-refcellt) +- [Совместное владение: Rc\<T\>](ch07-2-smart-pointers-and-interior-mutability.md#совместное-владение-rct) +- [Упражнение: совместное владение и внутренняя изменяемость](ch07-2-smart-pointers-and-interior-mutability.md#упражнение-совместное-владение-и-внутренняя-изменяемость) + +### 8. Модули и крейты +- [Крейты и модули в Rust](ch08-crates-and-modules.md#крейты-и-модули-в-rust) +- [Упражнение: модули и функции](ch08-crates-and-modules.md#упражнение-модули-и-функции) +- [Рабочие пространства и крейты (пакеты)](ch08-crates-and-modules.md#рабочие-пространства-и-крейты-пакеты) +- [Упражнение: использование рабочих пространств и зависимостей пакетов](ch08-crates-and-modules.md#упражнение-использование-рабочих-пространств-и-зависимостей-пакетов) +- [Использование крейтов сообщества из crates.io](ch08-crates-and-modules.md#использование-крейтов-сообщества-из-cratesio) +- [Зависимости крейтов и SemVer](ch08-crates-and-modules.md#зависимости-крейтов-и-semver) +- [Упражнение: использование крейта rand](ch08-crates-and-modules.md#упражнение-использование-крейта-rand) +- [Cargo.toml и Cargo.lock](ch08-crates-and-modules.md#cargotoml-и-cargolock) +- [Функция cargo test](ch08-crates-and-modules.md#функция-cargo-test) +- [Другие возможности Cargo](ch08-crates-and-modules.md#другие-возможности-cargo) +- [Паттерны тестирования](ch08-1-testing-patterns.md) + +### 9. Обработка ошибок +- [Связываем перечисления с Option и Result](ch09-error-handling.md#связываем-перечисления-с-option-и-result) +- [Тип Option в Rust](ch09-error-handling.md#тип-option-в-rust) +- [Тип Result в Rust](ch09-error-handling.md#тип-result-в-rust) +- [Упражнение: реализация функции log() с Option](ch09-error-handling.md#упражнение-реализация-функции-log-с-option) +- [Обработка ошибок в Rust](ch09-error-handling.md#обработка-ошибок-в-rust) +- [Упражнение: обработка ошибок](ch09-error-handling.md#упражнение-обработка-ошибок) +- [Лучшие практики обработки ошибок](ch09-1-error-handling-best-practices.md) + +### 10. Трейты и обобщения +- [Трейты в Rust](ch10-traits.md#трейты-в-rust) +- [Перегрузка операторов в C++ → трейты std::ops в Rust](ch10-traits.md#перегрузка-операторов-в-c--трейты-stdops-в-rust) +- [Упражнение: реализация трейта Logger](ch10-traits.md#упражнение-реализация-трейта-logger) +- [Когда использовать enum и dyn Trait](ch10-traits.md#выбор-между-impl-trait-dyn-trait-и-перечислениями) +- [Упражнение: подумайте, прежде чем переводить](ch16-cases-3-5-lifetime-borrowing.md#упражнение-подумайте-прежде-чем-переводить) +- [Обобщения в Rust](ch10-1-generics.md#обобщения-в-rust) +- [Упражнение: обобщения](ch10-1-generics.md#упражнение-обобщения) +- [Комбинирование трейтов и обобщений в Rust](ch10-1-generics.md#комбинирование-трейтов-и-обобщений-в-rust) +- [Ограничения трейтов в типах данных](ch10-1-generics.md#ограничения-трейтов-в-типах-данных) +- [Упражнение: ограничения трейтов и обобщения](ch10-1-generics.md#упражнение-ограничения-трейтов-и-обобщения) +- [Паттерн состояний (type state) и обобщения в Rust](ch10-1-generics.md#паттерн-состояний-type-state-и-обобщения-в-rust) +- [Паттерн-строитель в Rust](ch10-1-generics.md#паттерн-строитель-в-rust) + +### 11. Расширенные возможности системы типов +- [Трейты From и Into в Rust](ch11-from-and-into-traits.md#трейты-from-и-into-в-rust) +- [Упражнение: From и Into](ch11-from-and-into-traits.md#упражнение-from-и-into) +- [Трейт Default в Rust](ch11-from-and-into-traits.md#трейт-default-в-rust) +- [Другие преобразования типов в Rust](ch11-from-and-into-traits.md#другие-преобразования-типов-в-rust) + +### 12. Функциональное программирование +- [Замыкания в Rust](ch12-closures.md#замыкания-в-rust) +- [Упражнение: замыкания и захват переменных](ch12-closures.md#упражнение-замыкания-и-захват-переменных) +- [Итераторы в Rust](ch12-closures.md#итераторы-в-rust) +- [Упражнение: итераторы в Rust](ch12-closures.md#упражнение-итераторы-в-rust) +- [Справочник по инструментам для работы с итераторами](ch12-1-iterator-power-tools.md#справочник-по-инструментам-для-работы-с-итераторами) + +### 13. Конкурентность +- [Конкурентность в Rust](ch13-concurrency.md#конкурентность-в-rust) +- [Почему Rust предотвращает гонки данных: Send и Sync](ch13-concurrency.md#почему-rust-предотвращает-гонки-данных-send-и-sync) +- [Упражнение: подсчёт слов в многопоточном режиме](ch13-concurrency.md#упражнение-подсчёт-слов-в-многопоточном-режиме) + +### 14. Небезопасный Rust и FFI +- [Небезопасный Rust (unsafe)](ch14-unsafe-rust-and-ffi.md#небезопасный-rust-unsafe) +- [Простой пример FFI (функция Rust, вызываемая из C)](ch14-unsafe-rust-and-ffi.md#простой-пример-ffi-функция-rust-вызываемая-из-c) +- [Сложный пример FFI](ch14-unsafe-rust-and-ffi.md#сложный-пример-ffi) +- [Обеспечение корректности unsafe-кода](ch14-unsafe-rust-and-ffi.md#обеспечение-корректности-unsafe-кода) +- [Упражнение: написание безопасной обёртки FFI](ch14-unsafe-rust-and-ffi.md#упражнение-написание-безопасной-обёртки-ffi) + +## Часть II — Углублённые темы + +### 15. no_std — Rust для bare metal +- [Что такое no_std?](ch15-no_std-rust-without-the-standard-library.md#что-такое-no_std) +- [Когда использовать no_std, а когда std](ch15-no_std-rust-without-the-standard-library.md#когда-использовать-no_std-а-когда-std) +- [Упражнение: кольцевой буфер no_std](ch15-no_std-rust-without-the-standard-library.md#упражнение-кольцевой-буфер-no_std) +- [Углублённо о встраиваемых системах](ch15-1-embedded-deep-dive.md) + +### 16. Примеры из практики: перевод реального кода C++ на Rust +- [Кейс 1: иерархия наследования → диспетчеризация через перечисления](ch16-case-studies.md#кейс-1-иерархия-наследования--диспетчеризация-через-перечисления) +- [Кейс 2: дерево на shared_ptr → паттерн арены и индексов](ch16-case-studies.md#кейс-2-дерево-на-shared_ptr--паттерн-арены-и-индексов) +- [Кейс 3: коммуникация с фреймворком → заимствование по времени жизни](ch16-cases-3-5-lifetime-borrowing.md#кейс-3-коммуникация-с-фреймворком--заимствование-по-времени-жизни) +- [Кейс 4: «божественный объект» → компонуемое состояние](ch16-cases-3-5-lifetime-borrowing.md#кейс-4-божественный-объект--компонуемое-состояние) +- [Кейс 5: трейт-объекты — когда они действительно уместны](ch16-cases-3-5-lifetime-borrowing.md#кейс-5-трейт-объекты--когда-они-действительно-уместны) + +## Часть III — Лучшие практики и справочник + +### 17. Лучшие практики +- [Сводка лучших практик Rust](ch17-best-practices.md#сводка-лучших-практик-rust) +- [Избегание избыточного clone()](ch17-1-avoiding-excessive-clone.md#избегание-избыточного-clone) +- [Избегание неконтролируемой индексации](ch17-2-avoiding-unchecked-indexing.md#избегание-неконтролируемой-индексации) +- [Схлопывание пирамид присваиваний](ch17-3-collapsing-assignment-pyramids.md#схлопывание-пирамид-присваиваний-с-помощью-замыканий) +- [Итоговое упражнение: конвейер диагностических событий](ch17-3-collapsing-assignment-pyramids.md#итоговое-упражнение-конвейер-диагностических-событий) +- [Экосистема логирования и трассировки](ch17-4-logging-and-tracing-ecosystem.md#экосистема-логирования-и-трассировки-syslogprintf--log--tracing) + +### 18. Семантические различия C++ → Rust +- [Приведения, препроцессор, модули, volatile, static, constexpr, SFINAE и многое другое](ch18-cpp-rust-semantic-deep-dives.md) + +### 19. Макросы Rust +- [Декларативные макросы (`macro_rules!`)](ch19-macros.md#декларативные-макросы-с-macro_rules) +- [Распространённые макросы стандартной библиотеки](ch19-macros.md#распространённые-макросы-стандартной-библиотеки) +- [Макросы derive](ch19-macros.md#макросы-derive) +- [Макросы-атрибуты](ch19-macros.md#макросы-атрибуты) +- [Процедурные макросы](ch19-macros.md#процедурные-макросы-концептуальный-обзор) +- [Когда что использовать: макросы, функции или обобщения](ch19-macros.md#когда-что-использовать-макросы-функции-или-обобщения) +- [Упражнения](ch19-macros.md#упражнения) diff --git a/c-cpp-book/src/ch15-1-embedded-deep-dive.md b/c-cpp-book/src/ch15-1-embedded-deep-dive.md index 7129839..565e0a6 100644 --- a/c-cpp-book/src/ch15-1-embedded-deep-dive.md +++ b/c-cpp-book/src/ch15-1-embedded-deep-dive.md @@ -1,109 +1,109 @@ -## MMIO and Volatile Register Access +## Доступ к MMIO и volatile-регистрам -> **What you'll learn:** Type-safe hardware register access in embedded Rust — volatile MMIO patterns, register abstraction crates, and how Rust's type system can encode register permissions that C's `volatile` keyword cannot. +> **Что вы узнаете:** типобезопасный доступ к аппаратным регистрам во встраиваемом Rust — шаблоны volatile MMIO, крейты абстракции регистров и то, как система типов Rust может кодировать права доступа к регистрам, чего не умеет ключевое слово `volatile` из C. -In C firmware, you access hardware registers via `volatile` pointers to specific -memory addresses. Rust has equivalent mechanisms — but with type safety. +В прошивке на C вы обращаетесь к аппаратным регистрам через `volatile`-указатели на конкретные +адреса памяти. В Rust есть эквивалентные механизмы — но с типобезопасностью. -### C volatile vs Rust volatile +### volatile в C и volatile в Rust ```c -// C — typical MMIO register access +// C — типичный доступ к регистрам MMIO #define GPIO_BASE 0x40020000 #define GPIO_MODER (*(volatile uint32_t*)(GPIO_BASE + 0x00)) #define GPIO_ODR (*(volatile uint32_t*)(GPIO_BASE + 0x14)) void toggle_led(void) { - GPIO_ODR ^= (1 << 5); // Toggle pin 5 + GPIO_ODR ^= (1 << 5); // Переключаем пин 5 } ``` ```rust -// Rust — raw volatile (low-level, rarely used directly) +// Rust — сырой volatile (низкий уровень, редко используется напрямую) use core::ptr; const GPIO_BASE: usize = 0x4002_0000; const GPIO_ODR: *mut u32 = (GPIO_BASE + 0x14) as *mut u32; /// # Safety -/// Caller must ensure GPIO_BASE is a valid mapped peripheral address. +/// Вызывающая сторона должна гарантировать, что GPIO_BASE — валидный отображённый периферийный адрес. unsafe fn toggle_led() { - // SAFETY: GPIO_ODR is a valid memory-mapped register address. + // SAFETY: GPIO_ODR — валидный адрес регистра, отображённого в память. let current = unsafe { ptr::read_volatile(GPIO_ODR) }; unsafe { ptr::write_volatile(GPIO_ODR, current ^ (1 << 5)) }; } ``` -### svd2rust — Type-Safe Register Access (the Rust way) +### svd2rust — типобезопасный доступ к регистрам (путь Rust) -In practice, you **never** write raw volatile pointers. Instead, `svd2rust` generates -a **Peripheral Access Crate (PAC)** from the chip's SVD file (the same XML file used by -your IDE's debug view): +На практике вы **никогда** не пишете сырые volatile-указатели. Вместо этого `svd2rust` генерирует +**крейт доступа к периферии (PAC, Peripheral Access Crate)** из SVD-файла чипа (того же XML-файла, +который использует отладочное представление вашей IDE): ```rust -// Generated PAC code (you don't write this — svd2rust does) -// The PAC makes invalid register access a compile error +// Сгенерированный код PAC (вы его не пишете — это делает svd2rust) +// PAC превращает некорректный доступ к регистрам в ошибку компиляции -// Usage with PAC: -use stm32f4::stm32f401; // PAC crate for your chip +// Использование PAC: +use stm32f4::stm32f401; // Крейт PAC для вашего чипа fn configure_gpio(dp: stm32f401::Peripherals) { - // Enable GPIOA clock — type-safe, no magic numbers + // Включаем тактирование GPIOA — типобезопасно, без магических чисел dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled()); - // Set pin 5 to output — can't accidentally write to a read-only field + // Настраиваем пин 5 на выход — нельзя случайно записать в поле только для чтения dp.GPIOA.moder.modify(|_, w| w.moder5().output()); - // Toggle pin 5 — type-checked field access + // Переключаем пин 5 — доступ к полю проверяется типами dp.GPIOA.odr.modify(|r, w| { - // SAFETY: toggling a single bit in a valid register field. + // SAFETY: переключаем один бит в корректном поле регистра. unsafe { w.bits(r.bits() ^ (1 << 5)) } }); } ``` -| C register access | Rust PAC equivalent | +| Доступ к регистрам в C | Аналог в Rust PAC | |-------------------|---------------------| -| `#define REG (*(volatile uint32_t*)ADDR)` | PAC crate generated by `svd2rust` | +| `#define REG (*(volatile uint32_t*)ADDR)` | Крейт PAC, сгенерированный `svd2rust` | | `REG |= BITMASK;` | `periph.reg.modify(\|_, w\| w.field().variant())` | | `value = REG;` | `let val = periph.reg.read().field().bits()` | -| Wrong register field → silent UB | Compile error — field doesn't exist | -| Wrong register width → silent UB | Type-checked — u8 vs u16 vs u32 | +| Неверное поле регистра → молчаливое UB | Ошибка компиляции — поля не существует | +| Неверная ширина регистра → молчаливое UB | Проверяется типами — u8, u16 или u32 | -## Interrupt Handling and Critical Sections +## Обработка прерываний и критические секции -C firmware uses `__disable_irq()` / `__enable_irq()` and ISR functions with `void` -signatures. Rust provides type-safe equivalents. +Прошивка на C использует `__disable_irq()` / `__enable_irq()` и функции ISR с сигнатурами `void`. +Rust предоставляет типобезопасные эквиваленты. -### C vs Rust Interrupt Patterns +### Шаблоны прерываний в C и Rust ```c -// C — traditional interrupt handler +// C — традиционный обработчик прерывания volatile uint32_t tick_count = 0; -void SysTick_Handler(void) { // Naming convention is critical — get it wrong → HardFault +void SysTick_Handler(void) { // Имя соглашения критично — ошибка → HardFault tick_count++; } uint32_t get_ticks(void) { __disable_irq(); - uint32_t t = tick_count; // Read inside critical section + uint32_t t = tick_count; // Чтение внутри критической секции __enable_irq(); return t; } ``` ```rust -// Rust — using cortex-m and critical sections +// Rust — с использованием cortex-m и критических секций use core::cell::Cell; use cortex_m::interrupt::{self, Mutex}; -// Shared state protected by a critical-section Mutex +// Разделяемое состояние, защищённое Mutex на основе критической секции static TICK_COUNT: Mutex<Cell<u32>> = Mutex::new(Cell::new(0)); -#[cortex_m_rt::exception] // Attribute ensures correct vector table placement -fn SysTick() { // Compile error if name doesn't match a valid exception - interrupt::free(|cs| { // cs = critical section token (proof IRQs disabled) +#[cortex_m_rt::exception] // Атрибут обеспечивает правильное размещение в таблице векторов +fn SysTick() { // Ошибка компиляции, если имя не совпадает с допустимым исключением + interrupt::free(|cs| { // cs — токен критической секции (доказательство, что IRQ запрещены) let count = TICK_COUNT.borrow(cs).get(); TICK_COUNT.borrow(cs).set(count + 1); }); @@ -114,10 +114,10 @@ fn get_ticks() -> u32 { } ``` -### RTIC — Real-Time Interrupt-driven Concurrency +### RTIC — конкурентность в реальном времени, управляемая прерываниями -For complex firmware with multiple interrupt priorities, RTIC (formerly RTFM) provides -**compile-time task scheduling with zero overhead**: +Для сложной прошивки с несколькими приоритетами прерываний RTIC (ранее RTFM) предоставляет +**планирование задач на этапе компиляции без накладных расходов**: ```rust #[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [USART1])] @@ -126,7 +126,7 @@ mod app { #[shared] struct Shared { - temperature: f32, // Shared between tasks — RTIC manages locking + temperature: f32, // Разделяется между задачами — RTIC управляет блокировками } #[local] @@ -142,116 +142,116 @@ mod app { (Shared { temperature: 25.0 }, Local { led }) } - // Hardware task: runs on SysTick interrupt + // Аппаратная задача: выполняется по прерыванию SysTick #[task(binds = SysTick, shared = [temperature], local = [led])] fn tick(mut cx: tick::Context) { cx.local.led.toggle(); cx.shared.temperature.lock(|temp| { - // RTIC guarantees exclusive access here — no manual locking needed + // RTIC гарантирует эксклюзивный доступ здесь — ручная блокировка не нужна *temp += 0.1; }); } } ``` -**Why RTIC matters for C firmware devs:** -- The `#[shared]` annotation replaces manual mutex management -- Priority-based preemption is configured at compile time — no runtime overhead -- Deadlock-free by construction (the framework proves it at compile time) -- ISR naming errors are compile errors, not runtime HardFaults +**Почему RTIC важен для разработчиков прошивок на C:** +- Аннотация `#[shared]` заменяет ручное управление мьютексами +- Вытесняющее планирование по приоритетам настраивается на этапе компиляции — без накладных расходов во время выполнения +- Взаимная блокировка исключена по построению (фреймворк доказывает это на этапе компиляции) +- Ошибки в именах обработчиков прерываний — это ошибки компиляции, а не сбои HardFault во время выполнения -## Panic Handler Strategies +## Стратегии обработки panic -In C, when something goes wrong in firmware, you typically reset or blink an LED. -Rust's panic handler gives you structured control: +В C, когда в прошивке что-то идёт не так, обычно выполняют сброс или мигают светодиодом. +Обработчик panic в Rust даёт структурированный контроль: ```rust -// Strategy 1: Halt (for debugging — attach debugger, inspect state) -use panic_halt as _; // Infinite loop on panic +// Стратегия 1: остановка (для отладки — подключаем отладчик, смотрим состояние) +use panic_halt as _; // Бесконечный цикл при panic -// Strategy 2: Reset the MCU -use panic_reset as _; // Triggers system reset +// Стратегия 2: сброс MCU +use panic_reset as _; // Вызывает системный сброс -// Strategy 3: Log via probe (development) -use panic_probe as _; // Sends panic info over debug probe (with defmt) +// Стратегия 3: логирование через probe (разработка) +use panic_probe as _; // Отправляет информацию о panic через отладочный зонд (с defmt) -// Strategy 4: Log over defmt then halt -use defmt_panic as _; // Rich panic messages over ITM/RTT +// Стратегия 4: логирование через defmt, затем остановка +use defmt_panic as _; // Подробные сообщения о panic через ITM/RTT -// Strategy 5: Custom handler (production firmware) +// Стратегия 5: собственный обработчик (продакшн-прошивка) use core::panic::PanicInfo; #[panic_handler] fn panic(info: &PanicInfo) -> ! { - // 1. Disable interrupts to prevent further damage + // 1. Запрещаем прерывания, чтобы не нанести дополнительный ущерб cortex_m::interrupt::disable(); - // 2. Write panic info to a reserved RAM region (survives reset) - // SAFETY: PANIC_LOG is a reserved memory region defined in linker script. + // 2. Записываем информацию о panic в зарезервированную область RAM (переживает сброс) + // SAFETY: PANIC_LOG — зарезервированная область памяти, определённая в скрипте компоновщика. unsafe { let log = 0x2000_0000 as *mut [u8; 256]; - // Write truncated panic message + // Записываем обрезанное сообщение о panic use core::fmt::Write; let mut writer = FixedWriter::new(&mut *log); let _ = write!(writer, "{}", info); } - // 3. Trigger watchdog reset (or blink error LED) + // 3. Запускаем сброс по сторожевому таймеру (или мигаем светодиодом ошибки) loop { - cortex_m::asm::wfi(); // Wait for interrupt (low power while halted) + cortex_m::asm::wfi(); // Ожидание прерывания (малое потребление, пока остановлены) } } ``` -## Linker Scripts and Memory Layout +## Скрипты компоновщика и раскладка памяти -C firmware devs write linker scripts to define FLASH/RAM regions. Rust embedded -uses the same concept via `memory.x`: +Разработчики прошивок на C пишут скрипты компоновщика, чтобы описать области FLASH/RAM. Встраиваемый +Rust использует ту же концепцию через `memory.x`: ```ld -/* memory.x — placed at crate root, consumed by cortex-m-rt */ +/* memory.x — размещается в корне крейта, используется cortex-m-rt */ MEMORY { - /* Adjust for your MCU — these are STM32F401 values */ + /* Подстройте под ваш MCU — здесь значения для STM32F401 */ FLASH : ORIGIN = 0x08000000, LENGTH = 512K RAM : ORIGIN = 0x20000000, LENGTH = 96K } -/* Optional: reserve space for panic log (see panic handler above) */ +/* Необязательно: резервируем место для журнала panic (см. обработчик panic выше) */ _panic_log_start = ORIGIN(RAM); _panic_log_size = 256; ``` ```toml -# .cargo/config.toml — set the target and linker flags +# .cargo/config.toml — задаём цель и флаги компоновщика [target.thumbv7em-none-eabihf] -runner = "probe-rs run --chip STM32F401RE" # flash and run via debug probe +runner = "probe-rs run --chip STM32F401RE" # прошивка и запуск через отладочный зонд rustflags = [ - "-C", "link-arg=-Tlink.x", # cortex-m-rt linker script + "-C", "link-arg=-Tlink.x", # скрипт компоновщика cortex-m-rt ] [build] -target = "thumbv7em-none-eabihf" # Cortex-M4F with hardware FPU +target = "thumbv7em-none-eabihf" # Cortex-M4F с аппаратным FPU ``` -| C linker script | Rust equivalent | +| Скрипт компоновщика C | Аналог в Rust | |-----------------|-----------------| -| `MEMORY { FLASH ..., RAM ... }` | `memory.x` at crate root | +| `MEMORY { FLASH ..., RAM ... }` | `memory.x` в корне крейта | | `__attribute__((section(".data")))` | `#[link_section = ".data"]` | -| `-T linker.ld` in Makefile | `-C link-arg=-Tlink.x` in `.cargo/config.toml` | -| `__bss_start__`, `__bss_end__` | Handled by `cortex-m-rt` automatically | -| Startup assembly (`startup.s`) | `cortex-m-rt` `#[entry]` macro | +| `-T linker.ld` в Makefile | `-C link-arg=-Tlink.x` в `.cargo/config.toml` | +| `__bss_start__`, `__bss_end__` | Обрабатывается `cortex-m-rt` автоматически | +| Стартовый ассемблер (`startup.s`) | Макрос `#[entry]` из `cortex-m-rt` | -## Writing `embedded-hal` Drivers +## Написание драйверов на `embedded-hal` -The `embedded-hal` crate defines traits for SPI, I2C, GPIO, UART, etc. Drivers -written against these traits work on **any MCU** — this is Rust's killer feature -for embedded reuse. +Крейт `embedded-hal` определяет трейты для SPI, I2C, GPIO, UART и т. д. Драйверы, +написанные на основе этих трейтов, работают на **любом MCU** — это главная возможность Rust +для повторного использования во встраиваемых системах. -### C vs Rust: A Temperature Sensor Driver +### C и Rust: драйвер датчика температуры ```c -// C — driver tightly coupled to STM32 HAL +// C — драйвер жёстко связан с HAL STM32 #include "stm32f4xx_hal.h" float read_temperature(I2C_HandleTypeDef* hi2c, uint8_t addr) { @@ -261,11 +261,11 @@ float read_temperature(I2C_HandleTypeDef* hi2c, uint8_t addr) { int16_t raw = ((int16_t)buf[0] << 4) | (buf[1] >> 4); return raw * 0.0625; } -// Problem: This driver ONLY works with STM32 HAL. Porting to Nordic = rewrite. +// Проблема: этот драйвер работает ТОЛЬКО с HAL STM32. Перенос на Nordic = переписывание. ``` ```rust -// Rust — driver works on ANY MCU that implements embedded-hal +// Rust — драйвер работает на ЛЮБОМ MCU, реализующем embedded-hal use embedded_hal::i2c::I2c; pub struct Tmp102<I2C> { @@ -286,26 +286,26 @@ impl<I2C: I2c> Tmp102<I2C> { } } -// Works on STM32, Nordic nRF, ESP32, RP2040 — any chip with an embedded-hal I2C impl +// Работает на STM32, Nordic nRF, ESP32, RP2040 — на любом чипе с реализацией I2C из embedded-hal ``` ```mermaid graph TD - subgraph "C Driver Architecture" - CD["Temperature Driver"] - CD --> STM["STM32 HAL"] - CD -.->|"Port = REWRITE"| NRF["Nordic HAL"] - CD -.->|"Port = REWRITE"| ESP["ESP-IDF"] + subgraph "Архитектура драйвера в C" + CD["Драйвер датчика температуры"] + CD --> STM["HAL STM32"] + CD -.->|"Перенос = ПЕРЕПИСАТЬ"| NRF["HAL Nordic"] + CD -.->|"Перенос = ПЕРЕПИСАТЬ"| ESP["ESP-IDF"] end - subgraph "Rust embedded-hal Architecture" - RD["Temperature Driver<br/>impl<I2C: I2c>"] - RD --> EHAL["embedded-hal::I2c trait"] + subgraph "Архитектура embedded-hal в Rust" + RD["Драйвер датчика температуры<br/>impl<I2C: I2c>"] + RD --> EHAL["Трейт embedded-hal::I2c"] EHAL --> STM2["stm32f4xx-hal"] EHAL --> NRF2["nrf52-hal"] EHAL --> ESP2["esp-hal"] EHAL --> RP2["rp2040-hal"] - NOTE["Write driver ONCE,<br/>runs on ALL chips"] + NOTE["Пишем драйвер ОДИН раз,<br/>работает на ВСЕХ чипах"] end style CD fill:#ffa07a,color:#000 @@ -314,11 +314,11 @@ graph TD style NOTE fill:#91e5a3,color:#000 ``` -## Global Allocator Setup +## Настройка глобального аллокатора -The `alloc` crate gives you `Vec`, `String`, `Box` — but you need to tell Rust -where heap memory comes from. This is the equivalent of implementing `malloc()` -for your platform: +Крейт `alloc` даёт вам `Vec`, `String`, `Box`, но нужно указать Rust, +откуда берётся память кучи. Это эквивалент реализации `malloc()` +для вашей платформы: ```rust #![no_std] @@ -333,16 +333,16 @@ static HEAP: Heap = Heap::empty(); #[cortex_m_rt::entry] fn main() -> ! { - // Initialize the allocator with a memory region - // (typically a portion of RAM not used by stack or static data) + // Инициализируем аллокатор на области памяти + // (обычно часть RAM, не используемая стеком или статическими данными) { const HEAP_SIZE: usize = 4096; static mut HEAP_MEM: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; - // SAFETY: HEAP_MEM is only accessed here during init, before any allocation. + // SAFETY: HEAP_MEM здесь используется только при инициализации, до любого выделения памяти. unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } } - // Now you can use heap types! + // Теперь можно использовать типы кучи! let mut log_buffer: Vec<u8> = Vec::with_capacity(256); let name: String = String::from("sensor_01"); // ... @@ -351,38 +351,38 @@ fn main() -> ! { } ``` -| C heap setup | Rust equivalent | +| Настройка кучи в C | Аналог в Rust | |-------------|-----------------| -| `_sbrk()` / custom `malloc()` | `#[global_allocator]` + `Heap::init()` | -| `configTOTAL_HEAP_SIZE` (FreeRTOS) | `HEAP_SIZE` constant | -| `pvPortMalloc()` | `alloc::vec::Vec::new()` — automatic | -| Heap exhaustion → undefined behavior | `alloc_error_handler` → controlled panic | +| `_sbrk()` / собственный `malloc()` | `#[global_allocator]` + `Heap::init()` | +| `configTOTAL_HEAP_SIZE` (FreeRTOS) | Константа `HEAP_SIZE` | +| `pvPortMalloc()` | `alloc::vec::Vec::new()` — автоматически | +| Исчерпание кучи → неопределённое поведение | `alloc_error_handler` → контролируемый panic | -## Mixed `no_std` + `std` Workspaces +## Смешанные рабочие пространства `no_std` + `std` -Real projects (like a large Rust workspace) often have: -- `no_std` library crates for hardware-portable logic -- `std` binary crates for the Linux application layer +В реальных проектах (например, в большом рабочем пространстве Rust) часто встречаются: +- Библиотечные крейты `no_std` для переносимой между платформами логики +- Бинарные крейты `std` для прикладного уровня под Linux ```text workspace_root/ ├── Cargo.toml # [workspace] members = [...] -├── protocol/ # no_std — wire protocol, parsing -│ ├── Cargo.toml # no default-features, no std +├── protocol/ # no_std — протокол обмена, разбор +│ ├── Cargo.toml # без default-features, без std │ └── src/lib.rs # #![no_std] -├── driver/ # no_std — hardware abstraction +├── driver/ # no_std — абстракция оборудования │ ├── Cargo.toml -│ └── src/lib.rs # #![no_std], uses embedded-hal traits -├── firmware/ # no_std — MCU binary -│ ├── Cargo.toml # depends on protocol, driver +│ └── src/lib.rs # #![no_std], использует трейты embedded-hal +├── firmware/ # no_std — бинарный файл MCU +│ ├── Cargo.toml # зависит от protocol, driver │ └── src/main.rs # #![no_std] #![no_main] -└── host_tool/ # std — Linux CLI tool - ├── Cargo.toml # depends on protocol (same crate!) - └── src/main.rs # Uses std::fs, std::net, etc. +└── host_tool/ # std — CLI-инструмент под Linux + ├── Cargo.toml # зависит от protocol (тот же крейт!) + └── src/main.rs # Использует std::fs, std::net и т. д. ``` -The key pattern: the `protocol` crate uses `#![no_std]` so it compiles for **both** -the MCU firmware and the Linux host tool. Shared code, zero duplication. +Ключевой паттерн: крейт `protocol` использует `#![no_std]`, поэтому он компилируется и для +прошивки MCU, и для инструмента на Linux. Общий код, нулевое дублирование. ```toml # protocol/Cargo.toml @@ -391,11 +391,11 @@ name = "protocol" [features] default = [] -std = [] # Optional: enable std-specific features when building for host +std = [] # Необязательно: включает возможности, специфичные для std, при сборке для хоста [dependencies] serde = { version = "1", default-features = false, features = ["derive"] } -# Note: default-features = false drops serde's std dependency +# Примечание: default-features = false убирает зависимость serde от std ``` ```rust @@ -416,7 +416,7 @@ pub struct DiagPacket { pub fault_code: u16, } -// This function works in both no_std and std contexts +// Эта функция работает и в контексте no_std, и в контексте std pub fn parse_packet(data: &[u8]) -> Result<DiagPacket, &'static str> { if data.len() < 8 { return Err("packet too short"); @@ -429,19 +429,19 @@ pub fn parse_packet(data: &[u8]) -> Result<DiagPacket, &'static str> { } ``` -## Exercise: Hardware Abstraction Layer Driver +## Упражнение: драйвер слоя абстракции оборудования -Write a `no_std` driver for a hypothetical LED controller that communicates over SPI. -The driver should be generic over any SPI implementation using `embedded-hal`. +Напишите драйвер `no_std` для гипотетического контроллера светодиодов, который обменивается данными по SPI. +Драйвер должен быть обобщённым по любой реализации SPI на основе `embedded-hal`. -**Requirements:** -1. Define a `LedController<SPI>` struct -2. Implement `new()`, `set_brightness(led: u8, brightness: u8)`, and `all_off()` -3. SPI protocol: send `[led_index, brightness_value]` as 2-byte transaction -4. Write tests using a mock SPI implementation +**Требования:** +1. Определите структуру `LedController<SPI>` +2. Реализуйте `new()`, `set_brightness(led: u8, brightness: u8)` и `all_off()` +3. Протокол SPI: отправлять `[led_index, brightness_value]` как транзакцию из 2 байт +4. Напишите тесты с использованием мок-реализации SPI ```rust -// Starter code +// Стартовый код #![no_std] use embedded_hal::spi::SpiDevice; @@ -450,11 +450,11 @@ pub struct LedController<SPI> { num_leds: u8, } -// TODO: Implement new(), set_brightness(), all_off() -// TODO: Create MockSpi for testing +// TODO: Реализуйте new(), set_brightness(), all_off() +// TODO: Создайте MockSpi для тестирования ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust #![no_std] @@ -472,7 +472,7 @@ impl<SPI: SpiDevice> LedController<SPI> { pub fn set_brightness(&mut self, led: u8, brightness: u8) -> Result<(), SPI::Error> { if led >= self.num_leds { - return Ok(()); // Silently ignore out-of-range LEDs + return Ok(()); // Молча игнорируем светодиоды вне диапазона } self.spi.write(&[led, brightness]) } @@ -489,12 +489,12 @@ impl<SPI: SpiDevice> LedController<SPI> { mod tests { use super::*; - // Mock SPI that records all transactions + // Мок SPI, который записывает все транзакции struct MockSpi { transactions: Vec<Vec<u8>>, } - // Minimal error type for mock + // Минимальный тип ошибки для мока #[derive(Debug)] struct MockError; impl embedded_hal::spi::Error for MockError { @@ -540,7 +540,7 @@ mod tests { fn test_out_of_range_led() { let mock = MockSpi { transactions: vec![] }; let mut ctrl = LedController::new(mock, 2); - ctrl.set_brightness(5, 255).unwrap(); // Out of range — ignored + ctrl.set_brightness(5, 255).unwrap(); // Вне диапазона — игнорируется assert!(ctrl.spi.transactions.is_empty()); } } @@ -548,86 +548,86 @@ mod tests { </details> -## Debugging Embedded Rust — probe-rs, defmt, and VS Code +## Отладка встраиваемого Rust — probe-rs, defmt и VS Code -C firmware developers typically debug with OpenOCD + GDB or vendor-specific IDEs -(Keil, IAR, Segger Ozone). Rust's embedded ecosystem has converged on **probe-rs** -as the unified debug probe interface, replacing the OpenOCD + GDB stack with a -single, Rust-native tool. +Разработчики прошивок на C обычно отлаживают с помощью OpenOCD + GDB или фирменных IDE +(Keil, IAR, Segger Ozone). Экосистема встраиваемого Rust сошлась на **probe-rs** +как едином интерфейсе отладочного зонда, заменив связку OpenOCD + GDB одним +инструментом на Rust. -### probe-rs — The All-in-One Debug Probe Tool +### probe-rs — универсальный инструмент отладочного зонда -`probe-rs` replaces the OpenOCD + GDB combination. It supports CMSIS-DAP, -ST-Link, J-Link, and other debug probes out of the box: +`probe-rs` заменяет связку OpenOCD + GDB. Из коробки поддерживаются CMSIS-DAP, +ST-Link, J-Link и другие отладочные зонды: ```bash -# Install probe-rs (includes cargo-flash and cargo-embed) +# Установка probe-rs (включает cargo-flash и cargo-embed) cargo install probe-rs-tools -# Flash and run your firmware +# Прошивка и запуск вашей прошивки cargo flash --chip STM32F401RE --release -# Flash, run, and open RTT (Real-Time Transfer) console +# Прошивка, запуск и открытие консоли RTT (Real-Time Transfer) cargo embed --chip STM32F401RE ``` -**probe-rs vs OpenOCD + GDB**: +**probe-rs против OpenOCD + GDB**: -| Aspect | OpenOCD + GDB | probe-rs | +| Аспект | OpenOCD + GDB | probe-rs | |--------|--------------|----------| -| Install | 2 separate packages + scripts | `cargo install probe-rs-tools` | -| Config | `.cfg` files per board/probe | `--chip` flag or `Embed.toml` | -| Console output | Semihosting (very slow) | RTT (~10× faster) | -| Log framework | `printf` | `defmt` (structured, zero-cost) | -| Flash algorithm | XML pack files | Built-in for 1000+ chips | -| GDB support | Native | `probe-rs gdb` adapter | +| Установка | 2 отдельных пакета + скрипты | `cargo install probe-rs-tools` | +| Настройка | Файлы `.cfg` для каждой платы/зонда | Флаг `--chip` или `Embed.toml` | +| Вывод в консоль | Semihosting (очень медленно) | RTT (~10× быстрее) | +| Фреймворк логирования | `printf` | `defmt` (структурированный, без накладных расходов) | +| Алгоритм прошивки | XML-пакеты | Встроен для более чем 1000 чипов | +| Поддержка GDB | Нативная | Адаптер `probe-rs gdb` | -### `Embed.toml` — Project Configuration +### `Embed.toml` — конфигурация проекта -Instead of juggling `.cfg` and `.gdbinit` files, probe-rs uses a single config: +Вместо возни с файлами `.cfg` и `.gdbinit` probe-rs использует один конфигурационный файл: ```toml -# Embed.toml — placed in your project root +# Embed.toml — размещается в корне проекта [default.general] chip = "STM32F401RETx" [default.rtt] -enabled = true # Enable Real-Time Transfer console +enabled = true # Включить консоль Real-Time Transfer channels = [ { up = 0, mode = "BlockIfFull", name = "Terminal" }, ] [default.flashing] -enabled = true # Flash before running +enabled = true # Прошивать перед запуском restore_unwritten_bytes = false [default.reset] -halt_afterwards = false # Start running after flash + reset +halt_afterwards = false # Запускать после прошивки и сброса [default.gdb] -enabled = false # Set true to expose GDB server on :1337 +enabled = false # Установите true, чтобы открыть GDB-сервер на :1337 gdb_connection_string = "127.0.0.1:1337" ``` ```bash -# With Embed.toml, just run: -cargo embed # Flash + RTT console — zero flags needed -cargo embed --release # Release build +# С Embed.toml достаточно запустить: +cargo embed # Прошивка + консоль RTT — без флагов +cargo embed --release # Релизная сборка ``` -### defmt — Deferred Formatting for Embedded Logging +### defmt — отложенное форматирование для логирования на встраиваемых системах -`defmt` (deferred formatting) replaces `printf` debugging. Format strings are -stored in the ELF file, not in flash — so log calls on the target send only -an index + argument bytes. This makes logging **10–100× faster** than `printf` -and uses a fraction of the flash space: +`defmt` (deferred formatting, отложенное форматирование) заменяет отладку через `printf`. Строки формата +хранятся в ELF-файле, а не во flash — поэтому вызов логирования на цели отправляет +только индекс и байты аргументов. Благодаря этому логирование работает в **10–100 раз быстрее**, чем `printf`, +и занимает малую долю flash-памяти: ```rust #![no_std] #![no_main] use defmt::{info, warn, error, debug, trace}; -use defmt_rtt as _; // RTT transport — links the defmt output to probe-rs +use defmt_rtt as _; // Транспорт RTT — связывает вывод defmt с probe-rs #[cortex_m_rt::entry] fn main() -> ! { @@ -636,7 +636,7 @@ fn main() -> ! { let sensor_id: u16 = 0x4A; let temperature: f32 = 23.5; - // Format strings stay in ELF, not flash — near-zero overhead + // Строки формата остаются в ELF, а не во flash — практически без накладных расходов debug!("Sensor {:#06X}: {:.1}°C", sensor_id, temperature); if temperature > 80.0 { @@ -644,11 +644,11 @@ fn main() -> ! { } loop { - cortex_m::asm::wfi(); // Wait for interrupt + cortex_m::asm::wfi(); // Ожидание прерывания } } -// Custom types — derive defmt::Format instead of Debug +// Собственные типы — реализуйте defmt::Format вместо Debug #[derive(defmt::Format)] struct SensorReading { id: u16, @@ -663,25 +663,25 @@ enum SensorStatus { Fault(u8), } -// Usage: -// info!("Reading: {:?}", reading); // <-- uses defmt::Format, NOT std Debug +// Использование: +// info!("Reading: {:?}", reading); // <-- использует defmt::Format, а НЕ std Debug ``` -**defmt vs `printf` vs `log`**: +**defmt против `printf` и `log`**: -| Feature | C `printf` (semihosting) | Rust `log` crate | `defmt` | +| Характеристика | C `printf` (semihosting) | Крейт Rust `log` | `defmt` | |---------|-------------------------|-------------------|---------| -| Speed | ~100ms per call | N/A (needs `std`) | ~1μs per call | -| Flash usage | Full format strings | Full format strings | Index only (bytes) | -| Transport | Semihosting (halts CPU) | Serial/UART | RTT (non-blocking) | -| Structured output | No | Text only | Typed, binary-encoded | -| `no_std` | Via semihosting | Facade only (backends need `std`) | ✅ Native | -| Filter levels | Manual `#ifdef` | `RUST_LOG=debug` | `defmt::println` + features | +| Скорость | ~100 мс на вызов | Н/Д (нужен `std`) | ~1 мкс на вызов | +| Использование flash | Полные строки формата | Полные строки формата | Только индекс (байты) | +| Транспорт | Semihosting (останавливает CPU) | Serial/UART | RTT (неблокирующий) | +| Структурированный вывод | Нет | Только текст | Типизированный, бинарно закодированный | +| `no_std` | Через semihosting | Только фасад (бэкенды требуют `std`) | ✅ Нативно | +| Уровни фильтрации | Ручной `#ifdef` | `RUST_LOG=debug` | `defmt::println` + features | -### VS Code Debug Configuration +### Конфигурация отладки в VS Code -With the `probe-rs` VS Code extension, you get full graphical debugging — -breakpoints, variable inspection, call stack, and register view: +С расширением VS Code `probe-rs` вы получаете полноценную графическую отладку — +точки останова, просмотр переменных, стек вызовов и просмотр регистров: ```jsonc // .vscode/launch.json @@ -713,27 +713,27 @@ breakpoints, variable inspection, call stack, and register view: } ``` -Install the extension: +Установите расширение: ```rust ext install probe-rs.probe-rs-debugger ``` -### C Debugger Workflow vs Rust Embedded Debugging +### Рабочий процесс отладки C и отладка встраиваемого Rust ```mermaid graph LR - subgraph "C Workflow (Traditional)" - C1["Write code"] --> C2["make flash"] + subgraph "Рабочий процесс C (традиционный)" + C1["Пишем код"] --> C2["make flash"] C2 --> C3["openocd -f board.cfg"] C3 --> C4["arm-none-eabi-gdb<br/>target remote :3333"] - C4 --> C5["printf via semihosting<br/>(~100ms per call, halts CPU)"] + C4 --> C5["printf через semihosting<br/>(~100 мс на вызов, останавливает CPU)"] end - subgraph "Rust Workflow (probe-rs)" - R1["Write code"] --> R2["cargo embed"] - R2 --> R3["Flash + RTT console<br/>in one command"] - R3 --> R4["defmt logs stream<br/>in real-time (~1μs)"] - R2 -.->|"Or"| R5["VS Code F5<br/>Full GUI debugger"] + subgraph "Рабочий процесс Rust (probe-rs)" + R1["Пишем код"] --> R2["cargo embed"] + R2 --> R3["Прошивка + консоль RTT<br/>одной командой"] + R3 --> R4["Поток логов defmt<br/>в реальном времени (~1 мкс)"] + R2 -.->|"Или"| R5["VS Code F5<br/>Полноценный графический отладчик"] end style C5 fill:#ffa07a,color:#000 @@ -742,22 +742,21 @@ graph LR style R5 fill:#91e5a3,color:#000 ``` -| C Debug Action | Rust Equivalent | +| Действие отладки в C | Аналог в Rust | |---------------|-----------------| -| `openocd -f board/st_nucleo_f4.cfg` | `probe-rs info` (auto-detects probe + chip) | +| `openocd -f board/st_nucleo_f4.cfg` | `probe-rs info` (автоопределение зонда и чипа) | | `arm-none-eabi-gdb -x .gdbinit` | `probe-rs gdb --chip STM32F401RE` | -| `target remote :3333` | GDB connects to `localhost:1337` | +| `target remote :3333` | GDB подключается к `localhost:1337` | | `monitor reset halt` | `probe-rs reset --chip ...` | | `load firmware.elf` | `cargo flash --chip ...` | | `printf("debug: %d\n", val)` (semihosting) | `defmt::info!("debug: {}", val)` (RTT) | -| Keil/IAR GUI debugger | VS Code + `probe-rs-debugger` extension | -| Segger SystemView | `defmt` + `probe-rs` RTT viewer | +| Графический отладчик Keil/IAR | VS Code + расширение `probe-rs-debugger` | +| Segger SystemView | `defmt` + просмотрщик RTT из `probe-rs` | -> **Cross-reference**: For advanced unsafe patterns used in embedded drivers -> (pin projections, custom arena/slab allocators), see the companion -> *Rust Patterns* guide, sections "Pin Projections — Structural Pinning" -> and "Custom Allocators — Arena and Slab Patterns." +> **Перекрёстная ссылка**: о продвинутых паттернах unsafe, используемых в драйверах для встраиваемых систем +> (проекции Pin, собственные аллокаторы арены и slab), см. сопутствующее руководство +> *Паттерны Rust*, разделы «Проекции pin: структурное закрепление» и +> «Пользовательские аллокаторы: паттерны арен и slab». --- - diff --git a/c-cpp-book/src/ch15-no_std-rust-without-the-standard-library.md b/c-cpp-book/src/ch15-no_std-rust-without-the-standard-library.md index de34957..f6824e0 100644 --- a/c-cpp-book/src/ch15-no_std-rust-without-the-standard-library.md +++ b/c-cpp-book/src/ch15-no_std-rust-without-the-standard-library.md @@ -1,43 +1,39 @@ -# `no_std` — Rust Without the Standard Library +# `no_std` — Rust без стандартной библиотеки -> **What you'll learn:** How to write Rust for bare-metal and embedded targets using `#![no_std]` — the `core` and `alloc` crate split, panic handlers, and how this compares to embedded C without `libc`. +> **Что вы узнаете:** как писать на Rust под bare-metal и встраиваемые платформы с помощью `#![no_std]` — разделение крейтов `core` и `alloc`, обработчики panic и то, как это сравнивается со встраиваемым C без `libc`. -If you come from embedded C, you're already used to working without `libc` or with a minimal -runtime. Rust has a first-class equivalent: the **`#![no_std]`** attribute. +Если вы работаете со встраиваемым C, вам уже привычно работать без `libc` или с минимальной средой выполнения. У Rust есть полноценный аналог: атрибут **`#![no_std]`**. -## What is `no_std`? +## Что такое `no_std`? -When you add `#![no_std]` to the crate root, the compiler removes the -implicit `extern crate std;` and links only against **`core`** (and optionally **`alloc`**). +Когда вы добавляете `#![no_std]` в корень крейта, компилятор убирает неявное `extern crate std;` и линкуется только с **`core`** (и, при необходимости, с **`alloc`**). -| Layer | What it provides | Requires OS / heap? | +| Уровень | Что предоставляет | Требует ОС / кучу? | |-------|-----------------|---------------------| -| `core` | Primitive types, `Option`, `Result`, `Iterator`, math, `slice`, `str`, atomics, `fmt` | **No** — runs on bare metal | -| `alloc` | `Vec`, `String`, `Box`, `Rc`, `Arc`, `BTreeMap` | Needs a global allocator, but **no OS** | -| `std` | `HashMap`, `fs`, `net`, `thread`, `io`, `env`, `process` | **Yes** — needs an OS | +| `core` | Примитивные типы, `Option`, `Result`, `Iterator`, математика, `slice`, `str`, атомарные операции, `fmt` | **Нет** — работает на bare metal | +| `alloc` | `Vec`, `String`, `Box`, `Rc`, `Arc`, `BTreeMap` | Нужен глобальный аллокатор, но **без ОС** | +| `std` | `HashMap`, `fs`, `net`, `thread`, `io`, `env`, `process` | **Да** — нужна ОС | -> **Rule of thumb for embedded devs:** if your C project links against `-lc` and -> uses `malloc`, you can probably use `core` + `alloc`. If it runs on bare metal -> without `malloc`, stick with `core` only. +> **Эмпирическое правило для встраиваемых разработчиков:** если ваш проект на C линкуется с `-lc` и использует `malloc`, скорее всего, можно использовать `core` + `alloc`. Если же он работает на bare metal без `malloc`, оставайтесь только на `core`. -## Declaring `no_std` +## Объявление `no_std` ```rust -// src/lib.rs (or src/main.rs for a binary with #![no_main]) +// src/lib.rs (или src/main.rs для бинарника с #![no_main]) #![no_std] -// You still get everything in `core`: +// Всё из `core` вам по-прежнему доступно: use core::fmt; use core::result::Result; use core::option::Option; -// If you have an allocator, opt in to heap types: +// Если есть аллокатор, подключаем типы, работающие с кучей: extern crate alloc; use alloc::vec::Vec; use alloc::string::String; ``` -For a bare-metal binary you also need `#![no_main]` and a panic handler: +Для bare-metal бинарника также нужны `#![no_main]` и обработчик panic: ```rust #![no_std] @@ -47,46 +43,46 @@ use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { - loop {} // hang on panic — replace with your board's reset/LED blink + loop {} // Зависаем при panic — замените на перезагрузку платы или мигание светодиодом } -// Entry point depends on your HAL / linker script +// Точка входа зависит от вашего HAL / скрипта компоновщика ``` -## What you lose (and alternatives) +## Что вы теряете (и альтернативы) -| `std` feature | `no_std` alternative | +| Возможность `std` | Альтернатива в `no_std` | |---------------|---------------------| -| `println!` | `core::write!` to a UART / `defmt` | -| `HashMap` | `heapless::FnvIndexMap` (fixed capacity) or `BTreeMap` (with `alloc`) | -| `Vec` | `heapless::Vec` (stack-allocated, fixed capacity) | -| `String` | `heapless::String` or `&str` | +| `println!` | `core::write!` в UART / `defmt` | +| `HashMap` | `heapless::FnvIndexMap` (фиксированная ёмкость) или `BTreeMap` (с `alloc`) | +| `Vec` | `heapless::Vec` (на стеке, фиксированная ёмкость) | +| `String` | `heapless::String` или `&str` | | `std::io::Read/Write` | `embedded_io::Read/Write` | -| `thread::spawn` | Interrupt handlers, RTIC tasks | -| `std::time` | Hardware timer peripherals | -| `std::fs` | Flash / EEPROM drivers | +| `thread::spawn` | Обработчики прерываний, задачи RTIC | +| `std::time` | Аппаратные таймерные периферийные блоки | +| `std::fs` | Драйверы flash / EEPROM | -## Notable `no_std` crates for embedded +## Заметные крейты `no_std` для встраиваемых систем -| Crate | Purpose | Notes | +| Крейт | Назначение | Примечания | |-------|---------|-------| -| [`heapless`](https://crates.io/crates/heapless) | Fixed-capacity `Vec`, `String`, `Queue`, `Map` | No allocator needed — all on the stack | -| [`defmt`](https://crates.io/crates/defmt) | Efficient logging over probe/ITM | Like `printf` but deferred formatting on the host | -| [`embedded-hal`](https://crates.io/crates/embedded-hal) | Hardware abstraction traits (SPI, I²C, GPIO, UART) | Implement once, run on any MCU | -| [`cortex-m`](https://crates.io/crates/cortex-m) | ARM Cortex-M intrinsics & register access | Low-level, like CMSIS | -| [`cortex-m-rt`](https://crates.io/crates/cortex-m-rt) | Runtime / startup code for Cortex-M | Replaces your `startup.s` | -| [`rtic`](https://crates.io/crates/rtic) | Real-Time Interrupt-driven Concurrency | Compile-time task scheduling, zero overhead | -| [`embassy`](https://crates.io/crates/embassy-executor) | Async executor for embedded | `async/await` on bare metal | -| [`postcard`](https://crates.io/crates/postcard) | `no_std` serde serialization (binary) | Replaces `serde_json` when you can't afford strings | -| [`thiserror`](https://crates.io/crates/thiserror) | Derive macro for `Error` trait | Works in `no_std` since v2; prefer over `anyhow` | -| [`smoltcp`](https://crates.io/crates/smoltcp) | `no_std` TCP/IP stack | When you need networking without an OS | +| [`heapless`](https://crates.io/crates/heapless) | Vec, String, Queue, Map фиксированной ёмкости | Аллокатор не нужен — всё на стеке | +| [`defmt`](https://crates.io/crates/defmt) | Эффективное логирование через probe/ITM | Как `printf`, но форматирование откладывается на хост | +| [`embedded-hal`](https://crates.io/crates/embedded-hal) | Трейты абстракции оборудования (SPI, I²C, GPIO, UART) | Реализуете один раз — работает на любом MCU | +| [`cortex-m`](https://crates.io/crates/cortex-m) | Интринсики и доступ к регистрам ARM Cortex-M | Низкоуровневый, как CMSIS | +| [`cortex-m-rt`](https://crates.io/crates/cortex-m-rt) | Среда выполнения / стартовый код для Cortex-M | Заменяет ваш `startup.s` | +| [`rtic`](https://crates.io/crates/rtic) | Real-Time Interrupt-driven Concurrency | Планирование задач на этапе компиляции, без накладных расходов | +| [`embassy`](https://crates.io/crates/embassy-executor) | Асинхронный исполнитель для встраиваемых систем | `async/await` на bare metal | +| [`postcard`](https://crates.io/crates/postcard) | `no_std` сериализация serde (бинарная) | Заменяет `serde_json`, когда строки не по карману | +| [`thiserror`](https://crates.io/crates/thiserror) | Derive-макрос для трейта `Error` | Работает в `no_std` начиная с v2; предпочтительнее `anyhow` | +| [`smoltcp`](https://crates.io/crates/smoltcp) | Стек TCP/IP на `no_std` | Когда нужна сеть без ОС | -## C vs Rust: bare-metal comparison +## C и Rust: сравнение для bare metal -A typical embedded C blinky: +Типичный встраиваемый «blinky» на C: ```c -// C — bare metal, vendor HAL +// C — bare metal, вендорский HAL #include "stm32f4xx_hal.h" void SysTick_Handler(void) { @@ -103,14 +99,14 @@ int main(void) { } ``` -The Rust equivalent (using `embedded-hal` + a board crate): +Эквивалент на Rust (с использованием `embedded-hal` и крейта платы): ```rust #![no_std] #![no_main] use cortex_m_rt::entry; -use panic_halt as _; // panic handler: infinite loop +use panic_halt as _; // обработчик panic: бесконечный цикл use stm32f4xx_hal::{pac, prelude::*}; #[entry] @@ -130,51 +126,51 @@ fn main() -> ! { } ``` -**Key differences for C devs:** -- `Peripherals::take()` returns `Option` — ensures the singleton pattern at compile time (no double-init bugs) -- `.split()` moves ownership of individual pins — no risk of two modules driving the same pin -- All register access is type-checked — you can't accidentally write to a read-only register -- The borrow checker prevents data races between `main` and interrupt handlers (with RTIC) +**Ключевые отличия для разработчиков на C:** +- `Peripherals::take()` возвращает `Option` — это обеспечивает паттерн синглтона на этапе компиляции (без ошибок двойной инициализации) +- `.split()` передаёт владение отдельными выводами — нет риска, что два модуля будут управлять одним выводом +- Весь доступ к регистрам проверяется типами — вы не сможете случайно записать в регистр только для чтения +- Проверка заимствований предотвращает гонки данных между `main` и обработчиками прерываний (с RTIC) -## When to use `no_std` vs `std` +## Когда использовать `no_std`, а когда `std` ```mermaid flowchart TD - A[Does your target have an OS?] -->|Yes| B[Use std] - A -->|No| C[Do you have a heap allocator?] - C -->|Yes| D["Use #![no_std] + extern crate alloc"] - C -->|No| E["Use #![no_std] with core only"] - B --> F[Full Vec, HashMap, threads, fs, net] - D --> G[Vec, String, Box, BTreeMap — no fs/net/threads] - E --> H[Fixed-size arrays, heapless collections, no allocation] + A[Есть ли у вашей целевой платформы ОС?] -->|Да| B[Используйте std] + A -->|Нет| C[Есть ли у вас аллокатор кучи?] + C -->|Да| D["Используйте #![no_std] + extern crate alloc"] + C -->|Нет| E["Используйте #![no_std] только с core"] + B --> F[Полные Vec, HashMap, потоки, fs, net] + D --> G[Vec, String, Box, BTreeMap — без fs/net/потоков] + E --> H[Массивы фиксированного размера, коллекции heapless, без выделения памяти] ``` -# Exercise: `no_std` ring buffer +# Упражнение: кольцевой буфер `no_std` -🔴 **Challenge** — combines generics, `MaybeUninit`, and `#[cfg(test)]` in a `no_std` context +🔴 **Сложный уровень** — объединяет обобщения, `MaybeUninit` и `#[cfg(test)]` в контексте `no_std` -In embedded systems you often need a fixed-size ring buffer (circular buffer) that -never allocates. Implement one using only `core` (no `alloc`, no `std`). +Во встраиваемых системах часто нужен кольцевой буфер фиксированного размера, который +никогда не выделяет память. Реализуйте его, используя только `core` (без `alloc` и без `std`). -**Requirements:** -- Generic over element type `T: Copy` -- Fixed capacity `N` (const generic) -- `push(&mut self, item: T)` — overwrites oldest element when full -- `pop(&mut self) -> Option<T>` — returns oldest element +**Требования:** +- Обобщённый по типу элемента `T: Copy` +- Фиксированная ёмкость `N` (константный обобщённый параметр) +- `push(&mut self, item: T)` — перезаписывает самый старый элемент, когда буфер заполнен +- `pop(&mut self) -> Option<T>` — возвращает самый старый элемент - `len(&self) -> usize` - `is_empty(&self) -> bool` -- Must compile with `#![no_std]` +- Должен компилироваться с `#![no_std]` ```rust -// Starter code +// Стартовый код #![no_std] use core::mem::MaybeUninit; pub struct RingBuffer<T: Copy, const N: usize> { buf: [MaybeUninit<T>; N], - head: usize, // next write position - tail: usize, // next read position + head: usize, // позиция следующей записи + tail: usize, // позиция следующего чтения count: usize, } @@ -198,7 +194,7 @@ impl<T: Copy, const N: usize> RingBuffer<T, N> { ``` <details> -<summary>Solution</summary> +<summary>Решение</summary> ```rust #![no_std] @@ -215,7 +211,7 @@ pub struct RingBuffer<T: Copy, const N: usize> { impl<T: Copy, const N: usize> RingBuffer<T, N> { pub const fn new() -> Self { Self { - // SAFETY: MaybeUninit does not require initialization + // SAFETY: MaybeUninit не требует инициализации buf: unsafe { MaybeUninit::uninit().assume_init() }, head: 0, tail: 0, @@ -227,7 +223,7 @@ impl<T: Copy, const N: usize> RingBuffer<T, N> { self.buf[self.head] = MaybeUninit::new(item); self.head = (self.head + 1) % N; if self.count == N { - // Buffer is full — overwrite oldest, advance tail + // Буфер заполнен — перезаписываем самый старый элемент, сдвигаем tail self.tail = (self.tail + 1) % N; } else { self.count += 1; @@ -238,7 +234,7 @@ impl<T: Copy, const N: usize> RingBuffer<T, N> { if self.count == 0 { return None; } - // SAFETY: We only read positions that were previously written via push() + // SAFETY: читаем только те позиции, которые ранее были записаны через push() let item = unsafe { self.buf[self.tail].assume_init() }; self.tail = (self.tail + 1) % N; self.count -= 1; @@ -280,11 +276,11 @@ mod tests { rb.push(1); rb.push(2); rb.push(3); - // Buffer full: [1, 2, 3] + // Буфер заполнен: [1, 2, 3] - rb.push(4); // Overwrites 1 → [4, 2, 3], tail advances + rb.push(4); // Перезаписывает 1 → [4, 2, 3], tail сдвигается assert_eq!(rb.len(), 3); - assert_eq!(rb.pop(), Some(2)); // oldest surviving + assert_eq!(rb.pop(), Some(2)); // самый старый из оставшихся assert_eq!(rb.pop(), Some(3)); assert_eq!(rb.pop(), Some(4)); assert_eq!(rb.pop(), None); @@ -292,14 +288,11 @@ mod tests { } ``` -**Why this matters for embedded C devs:** -- `MaybeUninit` is Rust's equivalent of uninitialized memory — the compiler - won't insert zero-fills, just like `char buf[N];` in C -- The `unsafe` blocks are minimal (2 lines) and each has a `// SAFETY:` comment -- The `const fn new()` means you can create ring buffers in `static` variables - without a runtime constructor -- The tests run on your host with `cargo test` even though the code is `no_std` +**Почему это важно для разработчиков встраиваемого C:** +- `MaybeUninit` — это аналог неинициализированной памяти в Rust: компилятор не вставляет заполнение нулями, так же как в `char buf[N];` в C +- Блоки `unsafe` минимальны (по 2 строки), и у каждого есть комментарий `// SAFETY:` +- `const fn new()` означает, что кольцевые буферы можно создавать в переменных `static` без конструктора времени выполнения +- Тесты запускаются на вашей машине командой `cargo test`, хотя код рассчитан на `no_std` </details> - diff --git a/c-cpp-book/src/ch16-case-studies.md b/c-cpp-book/src/ch16-case-studies.md index 6dfc9d8..ef12955 100644 --- a/c-cpp-book/src/ch16-case-studies.md +++ b/c-cpp-book/src/ch16-case-studies.md @@ -1,45 +1,45 @@ -# Case Study Overview: C++ to Rust Translation +# Обзор кейсов: перевод с C++ на Rust -> **What you'll learn:** Lessons from a real-world translation of ~100K lines of C++ to ~90K lines of Rust across ~20 crates. Five key transformation patterns and the architectural decisions behind them. +> **Что вы узнаете:** уроки из реального перевода примерно 100 тысяч строк C++ примерно на 90 тысяч строк Rust в составе около 20 крейтов. Пять ключевых паттернов преобразования и архитектурные решения, стоящие за ними. -- We translated a large C++ diagnostic system (~100K lines of C++) into a Rust implementation (~20 Rust crates, ~90K lines) -- This section shows the **actual patterns** used — not toy examples, but real production code -- The five key transformations: +- Мы перевели большую диагностическую систему на C++ (около 100 тысяч строк C++) на реализацию на Rust (около 20 крейтов Rust, около 90 тысяч строк) +- В этом разделе показаны **реальные паттерны**, которые использовались, — не игрушечные примеры, а настоящий продакшн-код +- Пять ключевых преобразований: -| **#** | **C++ Pattern** | **Rust Pattern** | **Impact** | +| **№** | **Паттерн C++** | **Паттерн Rust** | **Эффект** | |-------|----------------|-----------------|-----------| -| 1 | Class hierarchy + `dynamic_cast` | Enum dispatch + `match` | ~400 → 0 dynamic_casts | -| 2 | `shared_ptr` / `enable_shared_from_this` tree | Arena + index linkage | No reference cycles | -| 3 | `Framework*` raw pointer in every module | `DiagContext<'a>` with lifetime borrowing | Compile-time validity | -| 4 | God object | Composable state structs | Testable, modular | -| 5 | `vector<unique_ptr<Base>>` everywhere | Trait objects **only** where needed (~25 uses) | Static dispatch default | +| 1 | Иерархия классов + `dynamic_cast` | Диспетчеризация через перечисления + `match` | ~400 → 0 вызовов dynamic_cast | +| 2 | Дерево на `shared_ptr` / `enable_shared_from_this` | Арена + связи через индексы | Циклов ссылок нет | +| 3 | Сырой указатель `Framework*` в каждом модуле | `DiagContext<'a>` с заимствованием по времени жизни | Корректность ссылок проверяется на этапе компиляции | +| 4 | «Божественный объект» (god object) | Компонуемые структуры состояния | Тестируемость, модульность | +| 5 | `vector<unique_ptr<Base>>` повсюду | Трейт-объекты **только** там, где нужно (~25 мест) | По умолчанию — статическая диспетчеризация | -### Before and After Metrics +### Метрики до и после -| **Metric** | **C++ (Original)** | **Rust (Rewrite)** | +| **Метрика** | **C++ (исходный)** | **Rust (переписанный)** | |------------|---------------------|------------------------| -| `dynamic_cast` / type downcasts | ~400 | 0 | -| `virtual` / `override` methods | ~900 | ~25 (`Box<dyn Trait>`) | -| Raw `new` allocations | ~200 | 0 (all owned types) | -| `shared_ptr` / reference counting | ~10 (topology lib) | 0 (`Arc` only at FFI boundary) | -| `enum class` definitions | ~60 | ~190 `pub enum` | -| Pattern matching expressions | N/A | ~750 `match` | -| God objects (>5K lines) | 2 | 0 | +| `dynamic_cast` / понижающие приведения типов | ~400 | 0 | +| Методы `virtual` / `override` | ~900 | ~25 (`Box<dyn Trait>`) | +| Сырые выделения `new` | ~200 | 0 (все типы владеющие) | +| `shared_ptr` / подсчёт ссылок | ~10 (библиотека топологии) | 0 (`Arc` только на границе FFI) | +| Определения `enum class` | ~60 | ~190 `pub enum` | +| Выражения сопоставления с образцом | Н/Д | ~750 `match` | +| «Божественные объекты» (>5 тыс. строк) | 2 | 0 | ---- -# Case Study 1: Inheritance hierarchy → Enum dispatch +# Кейс 1: иерархия наследования → диспетчеризация через перечисления -## The C++ Pattern: Event Class Hierarchy +## Паттерн C++: иерархия классов событий ```cpp -// C++ original: Every GPU event type is a class inheriting from GpuEventBase +// Исходный C++: каждый тип события GPU — это класс, наследующий GpuEventBase class GpuEventBase { public: virtual ~GpuEventBase() = default; virtual void Process(DiagFramework* fw) = 0; uint16_t m_recordId; uint8_t m_sensorType; - // ... common fields + // ... общие поля }; class GpuPcieDegradeEvent : public GpuEventBase { @@ -51,25 +51,25 @@ public: class GpuPcieFatalEvent : public GpuEventBase { /* ... */ }; class GpuBootEvent : public GpuEventBase { /* ... */ }; -// ... 10+ event classes inheriting from GpuEventBase +// ... 10+ классов событий, наследующих GpuEventBase -// Processing requires dynamic_cast: +// Обработка требует dynamic_cast: void ProcessEvents(std::vector<std::unique_ptr<GpuEventBase>>& events, DiagFramework* fw) { for (auto& event : events) { if (auto* degrade = dynamic_cast<GpuPcieDegradeEvent*>(event.get())) { - // handle degrade... + // обработка degrade... } else if (auto* fatal = dynamic_cast<GpuPcieFatalEvent*>(event.get())) { - // handle fatal... + // обработка fatal... } - // ... 10 more branches + // ... ещё 10 веток } } ``` -## The Rust Solution: Enum Dispatch +## Решение на Rust: диспетчеризация через перечисления ```rust -// Example: types.rs — No inheritance, no vtable, no dynamic_cast +// Пример: types.rs — без наследования, без vtable, без dynamic_cast #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum GpuEventKind { PcieDegrade, @@ -86,19 +86,19 @@ pub enum GpuEventKind { ``` ```rust -// Example: manager.rs — Separate typed Vecs, no downcasting needed +// Пример: manager.rs — отдельные типизированные Vec, понижающее приведение не нужно pub struct GpuEventManager { sku: SkuVariant, - degrade_events: Vec<GpuPcieDegradeEvent>, // Concrete type, not Box<dyn> + degrade_events: Vec<GpuPcieDegradeEvent>, // Конкретный тип, а не Box<dyn> fatal_events: Vec<GpuPcieFatalEvent>, uncorr_events: Vec<GpuPcieUncorrEvent>, boot_events: Vec<GpuBootEvent>, baseboard_events: Vec<GpuBaseboardEvent>, ecc_events: Vec<GpuEccEvent>, - // ... each event type gets its own Vec + // ... у каждого типа события свой Vec } -// Accessors return typed slices — zero ambiguity +// Методы доступа возвращают типизированные срезы — никакой двусмысленности impl GpuEventManager { pub fn degrade_events(&self) -> &[GpuPcieDegradeEvent] { &self.degrade_events @@ -109,49 +109,49 @@ impl GpuEventManager { } ``` -### Why Not `Vec<Box<dyn GpuEvent>>`? -- **The Wrong Approach** (literal translation): Put all events in one heterogeneous collection, then downcast — this is what C++ does with `vector<unique_ptr<Base>>` -- **The Right Approach**: Separate typed Vecs eliminate *all* downcasting. Each consumer asks for exactly the event type it needs -- **Performance**: Separate Vecs give better cache locality (all degrade events are contiguous in memory) +### Почему не `Vec<Box<dyn GpuEvent>>`? +- **Неверный подход** (буквальный перевод): сложить все события в одну разнородную коллекцию, а затем приводить типы — именно так C++ делает с `vector<unique_ptr<Base>>` +- **Верный подход**: отдельные типизированные Vec устраняют *всё* понижающее приведение типов. Каждый потребитель запрашивает ровно тот тип события, который ему нужен +- **Производительность**: отдельные Vec дают лучшую локальность кэша (все события degrade лежат в памяти непрерывно) ---- -# Case Study 2: shared_ptr tree → Arena/index pattern +# Кейс 2: дерево на shared_ptr → паттерн арены и индексов -## The C++ Pattern: Reference-Counted Tree +## Паттерн C++: дерево с подсчётом ссылок ```cpp -// C++ topology library: PcieDevice uses enable_shared_from_this -// because parent and child nodes both need to reference each other +// Библиотека топологии на C++: PcieDevice использует enable_shared_from_this, +// потому что и родительский, и дочерние узлы должны ссылаться друг на друга class PcieDevice : public std::enable_shared_from_this<PcieDevice> { public: std::shared_ptr<PcieDevice> m_upstream; std::vector<std::shared_ptr<PcieDevice>> m_downstream; - // ... device data + // ... данные устройства void AddChild(std::shared_ptr<PcieDevice> child) { - child->m_upstream = shared_from_this(); // Parent ↔ child cycle! + child->m_upstream = shared_from_this(); // Цикл родитель ↔ потомок! m_downstream.push_back(child); } }; -// Problem: parent→child and child→parent create reference cycles -// Need weak_ptr to break cycles, but easy to forget +// Проблема: связи родитель→потомок и потомок→родитель создают циклы ссылок +// Чтобы разорвать циклы, нужен weak_ptr, но про него легко забыть ``` -## The Rust Solution: Arena with Index Linkage +## Решение на Rust: арена со связями через индексы ```rust -// Example: components.rs — Flat Vec owns all devices +// Пример: components.rs — плоский Vec владеет всеми устройствами pub struct PcieDevice { pub base: PcieDeviceBase, pub kind: PcieDeviceKind, - // Tree linkage via indices — no reference counting, no cycles - pub upstream_idx: Option<usize>, // Index into the arena Vec - pub downstream_idxs: Vec<usize>, // Indices into the arena Vec + // Связи дерева через индексы — без подсчёта ссылок и без циклов + pub upstream_idx: Option<usize>, // Индекс в Vec-арене + pub downstream_idxs: Vec<usize>, // Индексы в Vec-арене } -// The "arena" is simply a Vec<PcieDevice> owned by the tree: +// «Арена» — это просто Vec<PcieDevice>, которым владеет дерево: pub struct DeviceTree { - devices: Vec<PcieDevice>, // Flat ownership — one Vec owns everything + devices: Vec<PcieDevice>, // Плоское владение — один Vec владеет всем } impl DeviceTree { @@ -169,29 +169,29 @@ impl DeviceTree { } ``` -### Key Insight -- **No `shared_ptr`, no `weak_ptr`, no `enable_shared_from_this`** -- **No reference cycles possible** — indices are just `usize` values -- **Better cache performance** — all devices in contiguous memory -- **Simpler reasoning** — one owner (the Vec), many viewers (indices) +### Главная мысль +- **Никаких `shared_ptr`, `weak_ptr` и `enable_shared_from_this`** +- **Циклов ссылок быть не может** — индексы — это просто значения `usize` +- **Лучшая производительность кэша** — все устройства лежат в памяти непрерывно +- **Проще рассуждать** — один владелец (Vec), много «наблюдателей» (индексы) ```mermaid graph LR - subgraph "C++ shared_ptr Tree" + subgraph "Дерево на shared_ptr в C++" A1["shared_ptr<Device>"] -->|"shared_ptr"| B1["shared_ptr<Device>"] - B1 -->|"shared_ptr (parent)"| A1 + B1 -->|"shared_ptr (родитель)"| A1 A1 -->|"shared_ptr"| C1["shared_ptr<Device>"] - C1 -->|"shared_ptr (parent)"| A1 + C1 -->|"shared_ptr (родитель)"| A1 style A1 fill:#ff6b6b,color:#000 style B1 fill:#ffa07a,color:#000 style C1 fill:#ffa07a,color:#000 end - subgraph "Rust Arena + Index" + subgraph "Арена и индексы в Rust" V["Vec<PcieDevice>"] - V --> D0["[0] Root<br/>upstream: None<br/>down: [1,2]"] - V --> D1["[1] Child<br/>upstream: Some(0)<br/>down: []"] - V --> D2["[2] Child<br/>upstream: Some(0)<br/>down: []"] + V --> D0["[0] Корень<br/>upstream: None<br/>down: [1,2]"] + V --> D1["[1] Потомок<br/>upstream: Some(0)<br/>down: []"] + V --> D2["[2] Потомок<br/>upstream: Some(0)<br/>down: []"] style V fill:#51cf66,color:#000 style D0 fill:#91e5a3,color:#000 style D1 fill:#91e5a3,color:#000 @@ -201,4 +201,3 @@ graph LR ---- - diff --git a/c-cpp-book/src/ch16-cases-3-5-lifetime-borrowing.md b/c-cpp-book/src/ch16-cases-3-5-lifetime-borrowing.md index 31771d7..5df618b 100644 --- a/c-cpp-book/src/ch16-cases-3-5-lifetime-borrowing.md +++ b/c-cpp-book/src/ch16-cases-3-5-lifetime-borrowing.md @@ -1,37 +1,37 @@ -# Case Study 3: Framework communication → Lifetime borrowing +# Кейс 3: коммуникация с фреймворком → заимствование по времени жизни -> **What you'll learn:** How to convert C++ raw-pointer framework communication patterns to Rust's lifetime-based borrowing system, eliminating dangling pointer risks while maintaining zero-cost abstractions. +> **Что вы узнаете:** как перевести паттерны взаимодействия с фреймворком через сырые указатели из C++ на систему заимствований Rust, основанную на временах жизни, устраняя риск висячих указателей и сохраняя абстракции без накладных расходов. -## The C++ Pattern: Raw Pointer to Framework +## Паттерн C++: сырой указатель на фреймворк ```cpp -// C++ original: Every diagnostic module stores a raw pointer to the framework +// Исходный C++: каждый диагностический модуль хранит сырой указатель на фреймворк class DiagBase { protected: - DiagFramework* m_pFramework; // Raw pointer — who owns this? + DiagFramework* m_pFramework; // Сырой указатель — кто им владеет? public: DiagBase(DiagFramework* fw) : m_pFramework(fw) {} void LogEvent(uint32_t code, const std::string& msg) { - m_pFramework->GetEventLog()->Record(code, msg); // Hope it's still alive! + m_pFramework->GetEventLog()->Record(code, msg); // Надеемся, что он ещё жив! } }; -// Problem: m_pFramework is a raw pointer with no lifetime guarantee -// If framework is destroyed while modules still reference it → UB +// Проблема: m_pFramework — сырой указатель без гарантии времени жизни +// Если фреймворк уничтожен, пока модули ещё на него ссылаются → неопределённое поведение ``` -## The Rust Solution: DiagContext with Lifetime Borrowing +## Решение на Rust: DiagContext с заимствованием по времени жизни ```rust -// Example: module.rs — Borrow, don't store +// Пример: module.rs — заимствуем, а не храним -/// Context passed to diagnostic modules during execution. -/// The lifetime 'a guarantees the framework outlives the context. +/// Контекст, передаваемый диагностическим модулям во время выполнения. +/// Время жизни 'a гарантирует, что фреймворк переживёт контекст. pub struct DiagContext<'a> { pub der_log: &'a mut EventLogManager, pub config: &'a ModuleConfig, pub framework_opts: &'a HashMap<String, String>, } -/// Modules receive context as a parameter — never store framework pointers +/// Модули получают контекст как параметр — никогда не храним указатели на фреймворк pub trait DiagModule { fn id(&self) -> &str; fn execute(&mut self, ctx: &mut DiagContext) -> DiagResult<()>; @@ -44,48 +44,48 @@ pub trait DiagModule { } ``` -### Key Insight -- C++ modules **store** a pointer to the framework (danger: what if the framework is destroyed first?) -- Rust modules **receive** a context as a function parameter — the borrow checker guarantees the framework is alive during the call -- No raw pointers, no lifetime ambiguity, no "hope it's still alive" +### Главная мысль +- Модули C++ **хранят** указатель на фреймворк (опасность: что если фреймворк уничтожат первым?) +- Модули Rust **получают** контекст как параметр функции — проверка заимствований гарантирует, что фреймворк жив во время вызова +- Никаких сырых указателей, никакой неоднозначности времён жизни, никаких «надеемся, что он ещё жив» ---- -# Case Study 4: God object → Composable state +# Кейс 4: «божественный объект» → компонуемое состояние -## The C++ Pattern: Monolithic Framework Class +## Паттерн C++: монолитный класс фреймворка ```cpp -// C++ original: The framework is god object +// Исходный C++: фреймворк — божественный объект class DiagFramework { - // Health-monitor trap processing + // Обработка ловушек монитора здоровья std::vector<AlertTriggerInfo> m_alertTriggers; std::vector<WarnTriggerInfo> m_warnTriggers; bool m_healthMonHasBootTimeError; uint32_t m_healthMonActionCounter; - // GPU diagnostics + // Диагностика GPU std::map<uint32_t, GpuPcieInfo> m_gpuPcieMap; bool m_isRecoveryContext; bool m_healthcheckDetectedDevices; - // ... 30+ more GPU-related fields + // ... ещё 30+ полей, связанных с GPU - // PCIe tree + // Дерево PCIe std::shared_ptr<CPcieTreeLinux> m_pPcieTree; - // Event logging + // Логирование событий CEventLogMgr* m_pEventLogMgr; - // ... several other methods + // ... несколько других методов void HandleGpuEvents(); void HandleNicEvents(); void RunGpuDiag(); - // Everything depends on everything + // Всё зависит от всего }; ``` -## The Rust Solution: Composable State Structs +## Решение на Rust: компонуемые структуры состояния ```rust -// Example: main.rs — State decomposed into focused structs +// Пример: main.rs — состояние разбито на сфокусированные структуры #[derive(Default)] struct HealthMonitorState { @@ -93,7 +93,7 @@ struct HealthMonitorState { warn_triggers: Vec<WarnTriggerInfo>, health_monitor_action_counter: u32, health_monitor_has_boot_time_error: bool, - // Only health-monitor-related fields + // Только поля, связанные с монитором здоровья } #[derive(Default)] @@ -101,45 +101,45 @@ struct GpuDiagState { gpu_pcie_map: HashMap<u32, GpuPcieInfo>, is_recovery_context: bool, healthcheck_detected_devices: bool, - // Only GPU-related fields + // Только поля, связанные с GPU } -/// The framework composes these states rather than owning everything flat +/// Фреймворк компонует эти состояния, а не владеет всем плоско struct DiagFramework { - ctx: DiagContext, // Execution context - args: Args, // CLI arguments - pcie_tree: Option<DeviceTree>, // No shared_ptr needed - event_log_mgr: EventLogManager, // Owned, not raw pointer - fc_manager: FcManager, // Fault code management - health: HealthMonitorState, // Health-monitor state — its own struct - gpu: GpuDiagState, // GPU state — its own struct + ctx: DiagContext, // Контекст выполнения + args: Args, // Аргументы командной строки + pcie_tree: Option<DeviceTree>, // shared_ptr не нужен + event_log_mgr: EventLogManager, // Владеющий, а не сырой указатель + fc_manager: FcManager, // Управление кодами неисправностей + health: HealthMonitorState, // Состояние монитора здоровья — отдельная структура + gpu: GpuDiagState, // Состояние GPU — отдельная структура } ``` -### Key Insight -- **Testability**: Each state struct can be unit-tested independently -- **Readability**: `self.health.alert_triggers` vs `m_alertTriggers` — clear ownership -- **Fearless refactoring**: Changing `GpuDiagState` can't accidentally affect health-monitor processing -- **No method soup**: Functions that only need health-monitor state take `&mut HealthMonitorState`, not the entire framework +### Главная мысль +- **Тестируемость**: каждую структуру состояния можно тестировать независимо +- **Читаемость**: `self.health.alert_triggers` против `m_alertTriggers` — владение очевидно +- **Рефакторинг без страха**: изменение `GpuDiagState` не может случайно повлиять на обработку монитора здоровья +- **Никакой «каши из методов»**: функции, которым нужно только состояние монитора здоровья, принимают `&mut HealthMonitorState`, а не весь фреймворк ---- -# Case Study 5: Trait objects — when they ARE right +# Кейс 5: трейт-объекты — когда они действительно уместны -- Not everything should be an enum! The **diagnostic module plugin system** is a genuine use case for trait objects -- Why? Because diagnostic modules are **open for extension** — new modules can be added without modifying the framework +- Не всё должно быть перечислением! **Система плагинов диагностических модулей** — подлинный случай для трейт-объектов +- Почему? Потому что диагностические модули **открыты для расширения** — новые модули можно добавлять, не меняя фреймворк ```rust -// Example: framework.rs — Vec<Box<dyn DiagModule>> is correct here +// Пример: framework.rs — Vec<Box<dyn DiagModule>> здесь уместен pub struct DiagFramework { - modules: Vec<Box<dyn DiagModule>>, // Runtime polymorphism + modules: Vec<Box<dyn DiagModule>>, // Полиморфизм времени выполнения pre_diag_modules: Vec<Box<dyn DiagModule>>, event_log_mgr: EventLogManager, // ... } impl DiagFramework { - /// Register a diagnostic module — any type implementing DiagModule + /// Регистрация диагностического модуля — любой тип, реализующий DiagModule pub fn register_module(&mut self, module: Box<dyn DiagModule>) { info!("Registering module: {}", module.id()); self.modules.push(module); @@ -147,32 +147,32 @@ impl DiagFramework { } ``` -### When to Use Each Pattern +### Когда использовать каждый паттерн -| **Use Case** | **Pattern** | **Why** | +| **Сценарий** | **Паттерн** | **Почему** | |-------------|-----------|--------| -| Fixed set of variants known at compile time | `enum` + `match` | Exhaustive checking, no vtable | -| Hardware event types (Degrade, Fatal, Boot, ...) | `enum GpuEventKind` | All variants known, performance matters | -| PCIe device types (GPU, NIC, Switch, ...) | `enum PcieDeviceKind` | Fixed set, each variant has different data | -| Plugin/module system (open for extension) | `Box<dyn Trait>` | New modules added without modifying framework | -| Test mocking | `Box<dyn Trait>` | Inject test doubles | - -### Exercise: Think Before You Translate -Given this C++ code: +| Фиксированный набор вариантов, известный на этапе компиляции | `enum` + `match` | Исчерпывающая проверка, без vtable | +| Типы аппаратных событий (Degrade, Fatal, Boot, ...) | `enum GpuEventKind` | Все варианты известны, важна производительность | +| Типы устройств PCIe (GPU, NIC, Switch, ...) | `enum PcieDeviceKind` | Фиксированный набор, у каждого варианта свои данные | +| Система плагинов/модулей (открыта для расширения) | `Box<dyn Trait>` | Новые модули добавляются без изменения фреймворка | +| Мокирование в тестах | `Box<dyn Trait>` | Внедрение тестовых двойников | + +### Упражнение: подумайте, прежде чем переводить +Дан следующий код на C++: ```cpp class Shape { public: virtual double area() = 0; }; class Circle : public Shape { double r; double area() override { return 3.14*r*r; } }; class Rect : public Shape { double w, h; double area() override { return w*h; } }; std::vector<std::unique_ptr<Shape>> shapes; ``` -**Question**: Should the Rust translation use `enum Shape` or `Vec<Box<dyn Shape>>`? +**Вопрос**: должен ли перевод на Rust использовать `enum Shape` или `Vec<Box<dyn Shape>>`? -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> -**Answer**: `enum Shape` — because the set of shapes is **closed** (known at compile time). You'd only use `Box<dyn Shape>` if users could add new shape types at runtime. +**Ответ**: `enum Shape` — потому что набор фигур **закрыт** (известен на этапе компиляции). `Box<dyn Shape>` понадобился бы, только если бы пользователи могли добавлять новые типы фигур во время выполнения. ```rust -// Correct Rust translation: +// Правильный перевод на Rust: enum Shape { Circle { r: f64 }, Rect { w: f64, h: f64 }, @@ -196,7 +196,7 @@ fn main() { println!("Area: {:.2}", shape.area()); } } -// Output: +// Вывод: // Area: 78.54 // Area: 12.00 ``` @@ -205,27 +205,26 @@ fn main() { ---- -# Translation metrics and lessons learned +# Метрики перевода и извлечённые уроки -## What We Learned -1. **Default to enum dispatch** — In ~100K lines of C++, only ~25 uses of `Box<dyn Trait>` were genuinely needed (plugin systems, test mocks). The other ~900 virtual methods became enums with match -2. **Arena pattern eliminates reference cycles** — `shared_ptr` and `enable_shared_from_this` are symptoms of unclear ownership. Think about who **owns** the data first -3. **Pass context, don't store pointers** — Lifetime-bounded `DiagContext<'a>` is safer and clearer than storing `Framework*` in every module -4. **Decompose god objects** — If a struct has 30+ fields, it's probably 3-4 structs wearing a trenchcoat -5. **The compiler is your pair programmer** — ~400 `dynamic_cast` calls meant ~400 potential runtime failures. Zero `dynamic_cast` equivalents in Rust means zero runtime type errors +## Что мы узнали +1. **По умолчанию используйте диспетчеризацию через перечисления** — в примерно 100 тысячах строк C++ действительно понадобилось лишь около 25 использований `Box<dyn Trait>` (системы плагинов, моки для тестов). Остальные примерно 900 виртуальных методов стали перечислениями с `match` +2. **Паттерн арены устраняет циклы ссылок** — `shared_ptr` и `enable_shared_from_this` — признаки неясного владения. Сначала подумайте, кто **владеет** данными +3. **Передавайте контекст, а не храните указатели** — `DiagContext<'a>` с ограниченным временем жизни безопаснее и понятнее, чем хранение `Framework*` в каждом модуле +4. **Разбивайте божественные объекты** — если в структуре 30+ полей, то, скорее всего, это 3–4 структуры в одном плаще +5. **Компилятор — ваш напарник по парному программированию** — около 400 вызовов `dynamic_cast` означали около 400 потенциальных сбоев во время выполнения. Отсутствие аналогов `dynamic_cast` в Rust означает отсутствие ошибок типов во время выполнения -## The Hardest Parts -- **Lifetime annotations**: Getting borrows right takes time when you're used to raw pointers — but once it compiles, it's correct -- **Fighting the borrow checker**: Wanting `&mut self` in two places at once. Solution: decompose state into separate structs -- **Resisting literal translation**: The temptation to write `Vec<Box<dyn Base>>` everywhere. Ask: "Is this set of variants closed?" → If yes, use enum +## Самые трудные места +- **Аннотации времён жизни**: правильно расставить заимствования непросто, если вы привыкли к сырым указателям, — но когда код компилируется, он корректен +- **Борьба с проверкой заимствований**: желание иметь `&mut self` в двух местах одновременно. Решение: разбить состояние на отдельные структуры +- **Противостояние буквальному переводу**: соблазн писать `Vec<Box<dyn Base>>` повсюду. Спросите себя: «Закрыт ли этот набор вариантов?» → Если да, используйте enum -## Recommendation for C++ Teams -1. Start with a small, self-contained module (not the god object) -2. Translate data structures first, then behavior -3. Let the compiler guide you — its error messages are excellent -4. Reach for `enum` before `dyn Trait` -5. Use the [Rust playground](https://play.rust-lang.org/) to prototype patterns before integrating +## Рекомендации для команд C++ +1. Начните с небольшого, самодостаточного модуля (а не с божественного объекта) +2. Сначала переводите структуры данных, потом поведение +3. Пусть компилятор вас направляет — его сообщения об ошибках отличны +4. Выбирайте `enum` раньше, чем `dyn Trait` +5. Используйте [Rust playground](https://play.rust-lang.org/), чтобы опробовать паттерны перед интеграцией ---- - diff --git a/c-cpp-book/src/ch17-1-avoiding-excessive-clone.md b/c-cpp-book/src/ch17-1-avoiding-excessive-clone.md index 82d7386..6a8efa1 100644 --- a/c-cpp-book/src/ch17-1-avoiding-excessive-clone.md +++ b/c-cpp-book/src/ch17-1-avoiding-excessive-clone.md @@ -1,35 +1,35 @@ -## Avoiding excessive clone() +## Избегание избыточного clone() -> **What you'll learn:** Why `.clone()` is a code smell in Rust, how to restructure ownership to eliminate unnecessary copies, and the specific patterns that signal an ownership design problem. +> **Что вы узнаете:** почему `.clone()` в Rust — это «запах кода», как перестроить владение, чтобы убрать лишние копии, и конкретные паттерны, которые указывают на проблему в дизайне владения. -- Coming from C++, `.clone()` feels like a safe default — "just copy it". But excessive cloning hides ownership problems and hurts performance. -- **Rule of thumb**: If you're cloning to satisfy the borrow checker, you probably need to restructure ownership instead. +- Приходя из C++, `.clone()` кажется безопасным значением по умолчанию — «просто скопируй». Но избыточное клонирование скрывает проблемы владения и вредит производительности. +- **Эмпирическое правило**: если вы клонируете, чтобы угодить проверке заимствований, скорее всего, нужно перестроить владение. -### When clone() is wrong +### Когда clone() ошибочен ```rust -// BAD: Cloning a String just to pass it to a function that only reads it -fn log_message(msg: String) { // Takes ownership unnecessarily +// ПЛОХО: клонируем String только для того, чтобы передать функции, которая лишь читает его +fn log_message(msg: String) { // Без нужды забирает владение println!("[LOG] {}", msg); } let message = String::from("GPU test passed"); -log_message(message.clone()); // Wasteful: allocates a whole new String -log_message(message); // Original consumed — clone was pointless +log_message(message.clone()); // Расточительно: выделяет новую String целиком +log_message(message); // Оригинал поглощён — клонирование было бессмысленным ``` ```rust -// GOOD: Accept a borrow — zero allocation -fn log_message(msg: &str) { // Borrows, doesn't own +// ХОРОШО: принимаем заимствование — ноль выделений +fn log_message(msg: &str) { // Заимствует, не владеет println!("[LOG] {}", msg); } let message = String::from("GPU test passed"); -log_message(&message); // No clone, no allocation -log_message(&message); // Can call again — message not consumed +log_message(&message); // Без клонирования, без выделения памяти +log_message(&message); // Можно вызвать снова — message не поглощена ``` -### Real example: returning `&str` instead of cloning +### Реальный пример: возврат `&str` вместо клонирования ```rust -// Example: healthcheck.rs — returns a borrowed view, zero allocation +// Пример: healthcheck.rs — возвращает заимствованное представление, без выделений pub fn serial_or_unknown(&self) -> &str { self.serial.as_deref().unwrap_or(UNKNOWN_VALUE) } @@ -38,97 +38,96 @@ pub fn model_or_unknown(&self) -> &str { self.model.as_deref().unwrap_or(UNKNOWN_VALUE) } ``` -The C++ equivalent would return `const std::string&` or `std::string_view` — but in C++ neither is lifetime-checked. In Rust, the borrow checker guarantees the returned `&str` can't outlive `self`. +Аналог в C++ вернул бы `const std::string&` или `std::string_view`, но в C++ ни то, ни другое не проверяется по времени жизни. В Rust проверка заимствований гарантирует, что возвращаемый `&str` не переживёт `self`. -### Real example: static string slices — no heap at all +### Реальный пример: статические срезы строк — вообще без кучи ```rust -// Example: healthcheck.rs — compile-time string tables +// Пример: healthcheck.rs — таблицы строк, известные на этапе компиляции const HBM_SCREEN_RECIPES: &[&str] = &[ "hbm_ds_ntd", "hbm_ds_ntd_gfx", "hbm_dt_ntd", "hbm_dt_ntd_gfx", "hbm_burnin_8h", "hbm_burnin_24h", ]; ``` -In C++ this would typically be `std::vector<std::string>` (heap-allocated on first use). Rust's `&'static [&'static str]` lives in read-only memory — zero runtime cost. +В C++ это обычно был бы `std::vector<std::string>` (выделяется в куче при первом использовании). Тип `&'static [&'static str]` в Rust живёт в памяти только для чтения — без накладных расходов во время выполнения. -### When clone() IS appropriate +### Когда clone() действительно уместен -| **Situation** | **Why clone is OK** | **Example** | +| **Ситуация** | **Почему клонирование допустимо** | **Пример** | |--------------|--------------------|-----------| -| `Arc::clone()` for threading | Bumps ref count (~1 ns), doesn't copy data | `let flag = stop_flag.clone();` | -| Moving data into a spawned thread | Thread needs its own copy | `let ctx = ctx.clone(); thread::spawn(move \|\| { ... })` | -| Extracting from `&self` fields | Can't move out of a borrow | `self.name.clone()` when returning owned `String` | -| Small `Copy` types wrapped in `Option` | `.copied()` is clearer than `.clone()` | `opt.get(0).copied()` for `Option<&u32>` → `Option<u32>` | +| `Arc::clone()` для потоков | Увеличивает счётчик ссылок (~1 нс), не копирует данные | `let flag = stop_flag.clone();` | +| Перемещение данных в порождённый поток | Поток нуждается в собственной копии | `let ctx = ctx.clone(); thread::spawn(move \|\| { ... })` | +| Извлечение из полей `&self` | Нельзя переместить значение из заимствования | `self.name.clone()`, когда возвращаем владеющий `String` | +| Небольшие типы `Copy` в `Option` | `.copied()` понятнее, чем `.clone()` | `opt.get(0).copied()` для `Option<&u32>` → `Option<u32>` | -### Real example: Arc::clone for thread sharing +### Реальный пример: Arc::clone для совместного использования потоками ```rust -// Example: workload.rs — Arc::clone is cheap (ref count bump) +// Пример: workload.rs — Arc::clone дёшев (увеличение счётчика ссылок) let stop_flag = Arc::new(AtomicBool::new(false)); -let stop_flag_clone = stop_flag.clone(); // ~1 ns, no data copied -let ctx_clone = ctx.clone(); // Clone context for move into thread +let stop_flag_clone = stop_flag.clone(); // ~1 нс, данные не копируются +let ctx_clone = ctx.clone(); // Клонируем контекст для перемещения в поток let sensor_handle = thread::spawn(move || { - // ...uses stop_flag_clone and ctx_clone + // ...использует stop_flag_clone и ctx_clone }); ``` -### Checklist: Should I clone? -1. **Can I accept `&str` / `&T` instead of `String` / `T`?** → Borrow, don't clone -2. **Can I restructure to avoid needing two owners?** → Pass by reference or use scopes -3. **Is this `Arc::clone()`?** → That's fine, it's O(1) -4. **Am I moving data into a thread/closure?** → Clone is necessary -5. **Am I cloning in a hot loop?** → Profile and consider borrowing or `Cow<T>` +### Чек-лист: нужно ли клонировать? +1. **Могу ли я принять `&str` / `&T` вместо `String` / `T`?** → Заимствуйте, не клонируйте +2. **Могу ли я перестроить код, чтобы не требовалось два владельца?** → Передавайте по ссылке или используйте области видимости +3. **Это `Arc::clone()`?** → Это нормально, это O(1) +4. **Я перемещаю данные в поток или замыкание?** → Клонирование необходимо +5. **Я клонирую в горячем цикле?** → Профилируйте и рассмотрите заимствование или `Cow<T>` ---- -## `Cow<'a, T>`: Clone-on-Write — borrow when you can, clone when you must +## `Cow<'a, T>`: Clone-on-Write — заимствуй, когда можешь, клонируй, когда должен -`Cow` (Clone on Write) is an enum that holds **either** a borrowed reference **or** -an owned value. It's the Rust equivalent of "avoid allocation when possible, but -allocate if you need to modify." C++ has no direct equivalent — the closest is a function -that returns `const std::string&` sometimes and `std::string` other times. +`Cow` (Clone on Write, «клонирование при записи») — это перечисление, которое хранит **либо** заимствованную ссылку, **либо** +владеющее значение. Это аналог в Rust идеи «избегай выделения памяти, когда возможно, но выделяй, если нужно изменить». +В C++ прямого аналога нет — ближайший вариант — функция, которая иногда возвращает `const std::string&`, а иногда `std::string`. -### Why `Cow` exists +### Зачем нужен `Cow` ```rust -// Without Cow — you must choose: always borrow OR always clone -fn normalize(s: &str) -> String { // Always allocates! +// Без Cow — приходится выбирать: всегда заимствовать ИЛИ всегда клонировать +fn normalize(s: &str) -> String { // Всегда выделяет память! if s.contains(' ') { - s.replace(' ', "_") // New String (allocation needed) + s.replace(' ', "_") // Новая String (выделение необходимо) } else { - s.to_string() // Unnecessary allocation! + s.to_string() // Ненужное выделение памяти! } } -// With Cow — borrow when unchanged, allocate only when modified +// С Cow — заимствуем, если строка не изменилась, выделяем только при изменении use std::borrow::Cow; fn normalize(s: &str) -> Cow<'_, str> { if s.contains(' ') { - Cow::Owned(s.replace(' ', "_")) // Allocates (must modify) + Cow::Owned(s.replace(' ', "_")) // Выделяет память (нужно изменить) } else { - Cow::Borrowed(s) // Zero allocation (passthrough) + Cow::Borrowed(s) // Ноль выделений (проход насквозь) } } ``` -### How `Cow` works +### Как работает `Cow` ```rust use std::borrow::Cow; -// Cow<'a, str> is essentially: +// Cow<'a, str> — по сути: // enum Cow<'a, str> { -// Borrowed(&'a str), // Zero-cost reference -// Owned(String), // Heap-allocated owned value +// Borrowed(&'a str), // Ссылка без накладных расходов +// Owned(String), // Владеющее значение в куче // } fn greet(name: &str) -> Cow<'_, str> { if name.is_empty() { - Cow::Borrowed("stranger") // Static string — no allocation + Cow::Borrowed("stranger") // Статическая строка — без выделения } else if name.starts_with(' ') { - Cow::Owned(name.trim().to_string()) // Modified — allocation needed + Cow::Owned(name.trim().to_string()) // Изменено — нужно выделение } else { - Cow::Borrowed(name) // Passthrough — no allocation + Cow::Borrowed(name) // Проход насквозь — без выделения } } @@ -137,62 +136,57 @@ fn main() { let g2 = greet(""); // Cow::Borrowed("stranger") let g3 = greet(" Bob "); // Cow::Owned("Bob") - // Cow<str> implements Deref<Target = str>, so you can use it as &str: - println!("Hello, {g1}!"); // Works — Cow auto-derefs to &str + // Cow<str> реализует Deref<Target = str>, поэтому его можно использовать как &str: + println!("Hello, {g1}!"); // Работает — Cow автоматически разыменовывается в &str println!("Hello, {g2}!"); println!("Hello, {g3}!"); } ``` -### Real-world use case: config value normalization +### Пример из практики: нормализация значений конфигурации ```rust use std::borrow::Cow; -/// Normalize a SKU name: trim whitespace, lowercase. -/// Returns Cow::Borrowed if already normalized (zero allocation). +/// Нормализует имя SKU: убирает пробелы по краям, приводит к нижнему регистру. +/// Возвращает Cow::Borrowed, если строка уже нормализована (ноль выделений). fn normalize_sku(sku: &str) -> Cow<'_, str> { let trimmed = sku.trim(); if trimmed == sku && sku.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) { - Cow::Borrowed(sku) // Already normalized — no allocation + Cow::Borrowed(sku) // Уже нормализована — без выделения } else { - Cow::Owned(trimmed.to_lowercase()) // Needs modification — allocate + Cow::Owned(trimmed.to_lowercase()) // Нужно изменить — выделяем } } fn main() { - let s1 = normalize_sku("server-x1"); // Borrowed — zero alloc - let s2 = normalize_sku(" Server-X1 "); // Owned — must allocate + let s1 = normalize_sku("server-x1"); // Borrowed — ноль выделений + let s2 = normalize_sku(" Server-X1 "); // Owned — приходится выделять println!("{s1}, {s2}"); // "server-x1, server-x1" } ``` -### When to use `Cow` +### Когда использовать `Cow` -| **Situation** | **Use `Cow`?** | +| **Ситуация** | **Использовать `Cow`?** | |--------------|---------------| -| Function returns input unchanged most of the time | ✅ Yes — avoid unnecessary clones | -| Parsing/normalizing strings (trim, lowercase, replace) | ✅ Yes — often input is already valid | -| Always modifying — every code path allocates | ❌ No — just return `String` | -| Simple pass-through (never modifies) | ❌ No — just return `&str` | -| Data stored in a struct long-term | ❌ No — use `String` (owned) | +| Функция чаще всего возвращает вход без изменений | ✅ Да — избегаем ненужных клонирований | +| Разбор/нормализация строк (trim, lowercase, replace) | ✅ Да — часто вход уже корректен | +| Всегда что-то меняем — каждый путь выполнения выделяет память | ❌ Нет — просто возвращаем `String` | +| Простой проход насквозь (никогда не меняем) | ❌ Нет — просто возвращаем `&str` | +| Данные долгосрочно хранятся в структуре | ❌ Нет — используйте `String` (владеющую) | -> **C++ comparison**: `Cow<str>` is like a function that returns `std::variant<std::string_view, std::string>` -> — except with automatic deref and no boilerplate to access the value. +> **Сравнение с C++**: `Cow<str>` похож на функцию, которая возвращает `std::variant<std::string_view, std::string>`, — только с автоматическим разыменованием и без шаблонного кода для доступа к значению. ---- -## `Weak<T>`: Breaking Reference Cycles — Rust's `weak_ptr` +## `Weak<T>`: разрыв циклов ссылок — `weak_ptr` в Rust -`Weak<T>` is the Rust equivalent of C++ `std::weak_ptr<T>`. It holds a non-owning -reference to an `Rc<T>` or `Arc<T>` value. The value can be deallocated while -`Weak` references still exist — calling `upgrade()` returns `None` if the value is gone. +`Weak<T>` — это аналог `std::weak_ptr<T>` из C++. Он хранит невладеющую ссылку на значение `Rc<T>` или `Arc<T>`. Значение может быть освобождено, пока существуют слабые ссылки `Weak` — вызов `upgrade()` вернёт `None`, если значения уже нет. -### Why `Weak` exists +### Зачем нужен `Weak` -`Rc<T>` and `Arc<T>` create reference cycles if two values point to each -other — neither ever reaches refcount 0, so neither is dropped (memory leak). -`Weak` breaks the cycle: +`Rc<T>` и `Arc<T>` образуют циклы ссылок, если два значения указывают друг на друга — ни одно из них никогда не достигнет счётчика 0, поэтому ни одно не будет уничтожено (утечка памяти). `Weak` разрывает цикл: ```rust use std::rc::{Rc, Weak}; @@ -201,8 +195,8 @@ use std::cell::RefCell; #[derive(Debug)] struct Node { value: String, - parent: RefCell<Weak<Node>>, // Weak — doesn't prevent parent from dropping - children: RefCell<Vec<Rc<Node>>>, // Strong — parent owns children + parent: RefCell<Weak<Node>>, // Weak — не мешает родителю уничтожиться + children: RefCell<Vec<Rc<Node>>>, // Сильная — родитель владеет потомками } impl Node { @@ -215,9 +209,9 @@ impl Node { } fn add_child(parent: &Rc<Node>, child: &Rc<Node>) { - // Child gets a weak reference to parent (no cycle) + // У потомка слабая ссылка на родителя (цикла нет) *child.parent.borrow_mut() = Rc::downgrade(parent); - // Parent gets a strong reference to child + // У родителя сильная ссылка на потомка parent.children.borrow_mut().push(Rc::clone(child)); } } @@ -227,7 +221,7 @@ fn main() { let child = Node::new("child"); Node::add_child(&root, &child); - // Access parent from child via upgrade() + // Доступ к родителю из потомка через upgrade() if let Some(parent) = child.parent.borrow().upgrade() { println!("Child's parent: {}", parent.value); // "root" } @@ -237,14 +231,14 @@ fn main() { } ``` -### C++ comparison +### Сравнение с C++ ```cpp -// C++ — weak_ptr to break shared_ptr cycle +// C++ — weak_ptr для разрыва цикла shared_ptr struct Node { std::string value; - std::weak_ptr<Node> parent; // Weak — no ownership - std::vector<std::shared_ptr<Node>> children; // Strong — owns children + std::weak_ptr<Node> parent; // Слабая — без владения + std::vector<std::shared_ptr<Node>> children; // Сильные — владеет потомками static auto create(const std::string& v) { return std::make_shared<Node>(Node{v, {}, {}}); @@ -253,57 +247,55 @@ struct Node { auto root = Node::create("root"); auto child = Node::create("child"); -child->parent = root; // weak_ptr assignment +child->parent = root; // Присваивание weak_ptr root->children.push_back(child); -if (auto p = child->parent.lock()) { // lock() → shared_ptr or null +if (auto p = child->parent.lock()) { // lock() → shared_ptr или null std::cout << "Parent: " << p->value << std::endl; } ``` -| C++ | Rust | Notes | +| C++ | Rust | Примечания | |-----|------|-------| -| `shared_ptr<T>` | `Rc<T>` (single-thread) / `Arc<T>` (multi-thread) | Same semantics | -| `weak_ptr<T>` | `Weak<T>` from `Rc::downgrade()` / `Arc::downgrade()` | Same semantics | -| `weak_ptr::lock()` → `shared_ptr` or null | `Weak::upgrade()` → `Option<Rc<T>>` | `None` if dropped | -| `shared_ptr::use_count()` | `Rc::strong_count()` | Same meaning | +| `shared_ptr<T>` | `Rc<T>` (один поток) / `Arc<T>` (несколько потоков) | Та же семантика | +| `weak_ptr<T>` | `Weak<T>` из `Rc::downgrade()` / `Arc::downgrade()` | Та же семантика | +| `weak_ptr::lock()` → `shared_ptr` или null | `Weak::upgrade()` → `Option<Rc<T>>` | `None`, если значение уничтожено | +| `shared_ptr::use_count()` | `Rc::strong_count()` | Тот же смысл | -### When to use `Weak` +### Когда использовать `Weak` -| **Situation** | **Pattern** | +| **Ситуация** | **Паттерн** | |--------------|-----------| -| Parent ↔ child tree relationships | Parent holds `Rc<Child>`, child holds `Weak<Parent>` | -| Observer pattern / event listeners | Event source holds `Weak<Observer>`, observer holds `Rc<Source>` | -| Cache that doesn't prevent deallocation | `HashMap<Key, Weak<Value>>` — entries go stale naturally | -| Breaking cycles in graph structures | Cross-links use `Weak`, tree edges use `Rc`/`Arc` | +| Связи «родитель ↔ потомок» в дереве | Родитель хранит `Rc<Child>`, потомок хранит `Weak<Parent>` | +| Паттерн «наблюдатель» / слушатели событий | Источник событий хранит `Weak<Observer>`, наблюдатель хранит `Rc<Source>` | +| Кэш, который не мешает освобождению | `HashMap<Key, Weak<Value>>` — записи естественным образом устаревают | +| Разрыв циклов в графовых структурах | Перекрёстные связи используют `Weak`, рёбра дерева — `Rc`/`Arc` | -> **Prefer the arena pattern** (Case Study 2) over `Rc/Weak` for tree structures in -> new code. `Vec<T>` + indices is simpler, faster, and has zero reference-counting -> overhead. Use `Rc/Weak` when you need shared ownership with dynamic lifetimes. +> **В новом коде предпочитайте паттерн арены** (кейс 2) вместо `Rc/Weak` для древовидных структур. `Vec<T>` с индексами проще, быстрее и не имеет накладных расходов на подсчёт ссылок. Используйте `Rc/Weak`, когда нужно совместное владение с динамическими временами жизни. ---- -## Copy vs Clone, PartialEq vs Eq — when to derive what +## Copy и Clone, PartialEq и Eq — что и когда выводить через derive -- **Copy ≈ C++ trivially copyable (no custom copy ctor/dtor).** Types like `int`, `enum`, and simple POD structs — the compiler generates a bitwise `memcpy` automatically. In Rust, `Copy` is the same idea: assignment `let b = a;` does an implicit bitwise copy and both variables remain valid. -- **Clone ≈ C++ copy constructor / `operator=` deep-copy.** When a C++ class has a custom copy constructor (e.g., to deep-copy a `std::vector` member), the equivalent in Rust is implementing `Clone`. You must call `.clone()` explicitly — Rust never hides an expensive copy behind `=`. -- **Key distinction:** In C++, both trivial copies and deep copies happen implicitly via the same `=` syntax. Rust forces you to choose: `Copy` types copy silently (cheap), non-`Copy` types **move** by default, and you must opt in to an expensive duplicate with `.clone()`. -- Similarly, C++ `operator==` doesn't distinguish between types where `a == a` always holds (like integers) and types where it doesn't (like `float` with NaN). Rust encodes this in `PartialEq` vs `Eq`. +- **Copy ≈ тривиально копируемые типы C++ (без пользовательского конструктора копирования и деструктора).** Такие типы, как `int`, `enum` и простые POD-структуры, — компилятор автоматически генерирует побитовый `memcpy`. В Rust `Copy` — та же идея: присваивание `let b = a;` выполняет неявное побитовое копирование, и обе переменные остаются валидными. +- **Clone ≈ глубокое копирование в конструкторе копирования / `operator=` C++.** Когда у класса C++ есть пользовательский конструктор копирования (например, для глубокого копирования члена `std::vector`), в Rust эквивалент — реализация `Clone`. Вызывать `.clone()` нужно явно — Rust никогда не прячет дорогое копирование за `=`. +- **Ключевое различие:** в C++ и тривиальные, и глубокие копии происходят неявно через один и тот же синтаксис `=`. Rust заставляет выбирать: типы `Copy` копируются молча (дёшево), нециклопические типы по умолчанию **перемещаются**, а дорогое дублирование нужно включить явно через `.clone()`. +- Аналогично, C++ `operator==` не различает типы, где `a == a` всегда истинно (как целые), и типы, где это не так (как `float` с NaN). Rust кодирует это в `PartialEq` и `Eq`. -### Copy vs Clone +### Copy и Clone | | **Copy** | **Clone** | |---|---------|----------| -| **How it works** | Bitwise memcpy (implicit) | Custom logic (explicit `.clone()`) | -| **When it happens** | On assignment: `let b = a;` | Only when you call `.clone()` | -| **After copy/clone** | Both `a` and `b` are valid | Both `a` and `b` are valid | -| **Without either** | `let b = a;` **moves** `a` (a is gone) | `let b = a;` **moves** `a` (a is gone) | -| **Allowed for** | Types with no heap data | Any type | -| **C++ analogy** | Trivially copyable / POD types (no custom copy ctor) | Custom copy constructor (deep copy) | - -### Real example: Copy — simple enums +| **Как работает** | Побитовый memcpy (неявно) | Пользовательская логика (явно через `.clone()`) | +| **Когда происходит** | При присваивании: `let b = a;` | Только при вызове `.clone()` | +| **После копирования/клонирования** | И `a`, и `b` валидны | И `a`, и `b` валидны | +| **Без них** | `let b = a;` **перемещает** `a` (a больше нет) | `let b = a;` **перемещает** `a` (a больше нет) | +| **Допустимо для** | Типов без данных в куче | Любых типов | +| **Аналогия в C++** | Тривиально копируемые / POD-типы (без пользовательского конструктора копирования) | Пользовательский конструктор копирования (глубокая копия) | + +### Реальный пример: Copy — простые перечисления ```rust -// From fan_diag/src/sensor.rs — all unit variants, fits in 1 byte +// Из fan_diag/src/sensor.rs — все варианты без данных, помещаются в 1 байт #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum FanStatus { #[default] @@ -316,13 +308,13 @@ pub enum FanStatus { } let status = FanStatus::Normal; -let copy = status; // Implicit copy — status is still valid -println!("{:?} {:?}", status, copy); // Both work +let copy = status; // Неявное копирование — status по-прежнему валиден +println!("{:?} {:?}", status, copy); // Работает оба ``` -### Real example: Copy — enum with integer payloads +### Реальный пример: Copy — перечисление с целочисленными данными ```rust -// Example: healthcheck.rs — u32 payloads are Copy, so the whole enum is too +// Пример: healthcheck.rs — полезная нагрузка u32 является Copy, значит, и всё перечисление тоже #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum HealthcheckStatus { Pass, @@ -334,41 +326,41 @@ pub enum HealthcheckStatus { } ``` -### Real example: Clone only — struct with heap data +### Реальный пример: только Clone — структура с данными в куче ```rust -// Example: components.rs — String prevents Copy +// Пример: components.rs — String мешает Copy #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FruData { pub technology: DeviceTechnology, - pub physical_location: String, // ← String: heap-allocated, can't Copy + pub physical_location: String, // ← String: в куче, не может быть Copy pub expected: bool, pub removable: bool, } -// let a = fru_data; → MOVES (a is gone) -// let a = fru_data.clone(); → CLONES (fru_data still valid, new heap allocation) +// let a = fru_data; → ПЕРЕМЕЩАЕТ (a — это единственный владелец, fru_data больше недоступна) +// let a = fru_data.clone(); → КЛОНИРУЕТ (fru_data по-прежнему валидна, новое выделение в куче) ``` -### The rule: Can it be Copy? +### Правило: может ли тип быть Copy? ```text -Does the type contain String, Vec, Box, HashMap, -Rc, Arc, or any other heap-owning type? - YES → Clone only (cannot be Copy) - NO → You CAN derive Copy (and should, if the type is small) +Содержит ли тип String, Vec, Box, HashMap, +Rc, Arc или любой другой тип, владеющий кучей? + ДА → только Clone (не может быть Copy) + НЕТ → МОЖНО выводить Copy (и стоит, если тип небольшой) ``` -### PartialEq vs Eq +### PartialEq и Eq | | **PartialEq** | **Eq** | |---|--------------|-------| -| **What it gives you** | `==` and `!=` operators | Marker: "equality is reflexive" | -| **Reflexive? (a == a)** | Not guaranteed | **Guaranteed** | -| **Why it matters** | `f32::NAN != f32::NAN` | `HashMap` keys **require** `Eq` | -| **When to derive** | Almost always | When the type has no `f32`/`f64` fields | -| **C++ analogy** | `operator==` | No direct equivalent (C++ doesn't check) | +| **Что даёт** | Операторы `==` и `!=` | Маркер: «равенство рефлексивно» | +| **Рефлексивно? (a == a)** | Не гарантируется | **Гарантируется** | +| **Почему важно** | `f32::NAN != f32::NAN` | Ключи `HashMap` **требуют** `Eq` | +| **Когда выводить** | Почти всегда | Когда в типе нет полей `f32`/`f64` | +| **Аналогия в C++** | `operator==` | Прямого аналога нет (C++ не проверяет) | -### Real example: Eq — used as HashMap key +### Реальный пример: Eq — используется как ключ HashMap ```rust -// From hms_trap/src/cpu_handler.rs — Hash requires Eq +// Из hms_trap/src/cpu_handler.rs — Hash требует Eq #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CpuFaultType { InvalidFaultType, @@ -377,100 +369,99 @@ pub enum CpuFaultType { CpuC2CUceFatalErr, // ... } -// Used as: HashMap<CpuFaultType, FaultHandler> -// HashMap keys must be Eq + Hash — PartialEq alone won't compile +// Используется как: HashMap<CpuFaultType, FaultHandler> +// Ключи HashMap должны быть Eq + Hash — одного PartialEq недостаточно для компиляции ``` -### Real example: No Eq possible — type contains f32 +### Реальный пример: Eq невозможен — тип содержит f32 ```rust -// Example: types.rs — f32 prevents Eq +// Пример: types.rs — f32 не позволяет Eq #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TemperatureSensors { - pub warning_threshold: Option<f32>, // ← f32 has NaN ≠ NaN - pub critical_threshold: Option<f32>, // ← can't derive Eq + pub warning_threshold: Option<f32>, // ← f32: NaN ≠ NaN + pub critical_threshold: Option<f32>, // ← нельзя вывести Eq pub sensor_names: Vec<String>, } -// Cannot be used as HashMap key. Cannot derive Eq. -// Because: f32::NAN == f32::NAN is false, violating reflexivity. +// Нельзя использовать как ключ HashMap. Нельзя вывести Eq. +// Потому что: f32::NAN == f32::NAN ложно, что нарушает рефлексивность. ``` -### PartialOrd vs Ord +### PartialOrd и Ord | | **PartialOrd** | **Ord** | |---|---------------|--------| -| **What it gives you** | `<`, `>`, `<=`, `>=` | `.sort()`, `BTreeMap` keys | -| **Total ordering?** | No (some pairs may be incomparable) | **Yes** (every pair is comparable) | -| **f32/f64?** | PartialOrd only (NaN breaks ordering) | Cannot derive Ord | +| **Что даёт** | `<`, `>`, `<=`, `>=` | `.sort()`, ключи `BTreeMap` | +| **Полный порядок?** | Нет (некоторые пары могут быть несравнимы) | **Да** (любые два значения сравнимы) | +| **f32/f64?** | Только PartialOrd (NaN ломает порядок) | Нельзя вывести Ord | -### Real example: Ord — severity ranking +### Реальный пример: Ord — ранжирование серьёзности ```rust -// From hms_trap/src/fault.rs — variant order defines severity +// Из hms_trap/src/fault.rs — порядок вариантов задаёт серьёзность #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum FaultSeverity { - Info, // lowest (discriminant 0) - Warning, // (discriminant 1) - Error, // (discriminant 2) - Critical, // highest (discriminant 3) + Info, // самый низкий (дискриминант 0) + Warning, // (дискриминант 1) + Error, // (дискриминант 2) + Critical, // самый высокий (дискриминант 3) } // FaultSeverity::Info < FaultSeverity::Critical → true -// Enables: if severity >= FaultSeverity::Error { escalate(); } +// Позволяет: if severity >= FaultSeverity::Error { escalate(); } ``` -### Real example: Ord — diagnostic levels for comparison +### Реальный пример: Ord — уровни диагностики для сравнения ```rust -// Example: orchestration.rs +// Пример: orchestration.rs #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum GpuDiagLevel { #[default] - Quick, // lowest + Quick, // самый низкий Standard, Extended, - Full, // highest + Full, // самый высокий } -// Enables: if requested_level >= GpuDiagLevel::Extended { run_extended_tests(); } +// Позволяет: if requested_level >= GpuDiagLevel::Extended { run_extended_tests(); } ``` -### Derive decision tree +### Дерево решений по derive ```text - Your new type + Ваш новый тип │ - Contains String/Vec/Box? + Содержит String/Vec/Box? / \ - YES NO + ДА НЕТ │ │ - Clone only Clone + Copy + Только Clone Clone + Copy │ │ - Contains f32/f64? Contains f32/f64? + Содержит f32/f64? Содержит f32/f64? / \ / \ - YES NO YES NO + ДА НЕТ ДА НЕТ │ │ │ │ - PartialEq PartialEq PartialEq PartialEq - only + Eq only + Eq + Только PartialEq Только PartialEq + PartialEq + Eq PartialEq + Eq │ │ - Need sorting? Need sorting? + Нужна сортировка? Нужна сортировка? / \ / \ - YES NO YES NO + ДА НЕТ ДА НЕТ │ │ │ │ - PartialOrd Done PartialOrd Done + PartialOrd Готово PartialOrd Готово + Ord + Ord │ │ - Need as Need as - map key? map key? + Нужен как Нужен как + ключ словаря? ключ словаря? │ │ + Hash + Hash ``` -### Quick reference: common derive combos from production Rust code +### Краткий справочник: типичные комбинации derive из продакшн-кода на Rust -| **Type category** | **Typical derive** | **Example** | +| **Категория типа** | **Типичный derive** | **Пример** | |-------------------|--------------------|------------| -| Simple status enum | `Copy, Clone, PartialEq, Eq, Default` | `FanStatus` | -| Enum used as HashMap key | `Copy, Clone, PartialEq, Eq, Hash` | `CpuFaultType`, `SelComponent` | -| Sortable severity enum | `Copy, Clone, PartialEq, Eq, PartialOrd, Ord` | `FaultSeverity`, `GpuDiagLevel` | -| Data struct with Strings | `Clone, Debug, Serialize, Deserialize` | `FruData`, `OverallSummary` | -| Serializable config | `Clone, Debug, Default, Serialize, Deserialize` | `DiagConfig` | +| Простое перечисление статусов | `Copy, Clone, PartialEq, Eq, Default` | `FanStatus` | +| Перечисление как ключ HashMap | `Copy, Clone, PartialEq, Eq, Hash` | `CpuFaultType`, `SelComponent` | +| Сортируемое перечисление серьёзности | `Copy, Clone, PartialEq, Eq, PartialOrd, Ord` | `FaultSeverity`, `GpuDiagLevel` | +| Структура данных со строками | `Clone, Debug, Serialize, Deserialize` | `FruData`, `OverallSummary` | +| Сериализуемая конфигурация | `Clone, Debug, Default, Serialize, Deserialize` | `DiagConfig` | ---- - diff --git a/c-cpp-book/src/ch17-2-avoiding-unchecked-indexing.md b/c-cpp-book/src/ch17-2-avoiding-unchecked-indexing.md index 7f5eea7..0518381 100644 --- a/c-cpp-book/src/ch17-2-avoiding-unchecked-indexing.md +++ b/c-cpp-book/src/ch17-2-avoiding-unchecked-indexing.md @@ -1,100 +1,100 @@ -## Avoiding unchecked indexing +## Избегание неконтролируемой индексации -> **What you'll learn:** Why `vec[i]` is dangerous in Rust (panics on out-of-bounds), and safe alternatives like `.get()`, iterators, and `entry()` API for `HashMap`. Replaces C++'s undefined behavior with explicit handling. +> **Что вы узнаете:** почему `vec[i]` опасен в Rust (вызывает panic при выходе за границы), и безопасные альтернативы — `.get()`, итераторы и API `entry()` для `HashMap`. Они заменяют неопределённое поведение C++ явной обработкой. -- In C++, `vec[i]` and `map[key]` have undefined behavior / auto-insert on missing keys. Rust's `[]` panics on out-of-bounds. -- **Rule**: Use `.get()` instead of `[]` unless you can *prove* the index is valid. +- В C++ `vec[i]` и `map[key]` ведут к неопределённому поведению или вставляют новый ключ, если его не было. `[]` в Rust вызывает panic при выходе за границы. +- **Правило**: используйте `.get()` вместо `[]`, если не можете *доказать*, что индекс корректен. -### C++ → Rust comparison +### Сравнение C++ → Rust ```cpp -// C++ — silent UB or insertion +// C++ — тихое неопределённое поведение или вставка std::vector<int> v = {1, 2, 3}; -int x = v[10]; // UB! No bounds check with operator[] +int x = v[10]; // Неопределённое поведение! operator[] не проверяет границы std::map<std::string, int> m; -int y = m["missing"]; // Silently inserts key with value 0! +int y = m["missing"]; // Тихо вставляет ключ со значением 0! ``` ```rust -// Rust — safe alternatives +// Rust — безопасные альтернативы let v = vec![1, 2, 3]; -// Bad: panics if index out of bounds +// Плохо: вызывает panic, если индекс вне границ // let x = v[10]; -// Good: returns Option<&i32> -let x = v.get(10); // None — no panic -let x = v.get(1).copied().unwrap_or(0); // 2, or 0 if missing +// Хорошо: возвращает Option<&i32> +let x = v.get(10); // None — без panic +let x = v.get(1).copied().unwrap_or(0); // 2, или 0, если элемента нет ``` -### Real example: safe byte parsing from production Rust code +### Реальный пример: безопасный разбор байтов из продакшн-кода на Rust ```rust -// Example: diagnostics.rs -// Parsing a binary SEL record — buffer might be shorter than expected +// Пример: diagnostics.rs +// Разбор бинарной записи SEL — буфер может оказаться короче, чем ожидалось let sensor_num = bytes.get(7).copied().unwrap_or(0); let ppin = cpu_ppin.get(i).map(|s| s.as_str()).unwrap_or(""); ``` -### Real example: chained safe lookups with `.and_then()` +### Реальный пример: цепочка безопасных поисков через `.and_then()` ```rust -// Example: profile.rs — double lookup: HashMap → Vec +// Пример: profile.rs — двойной поиск: HashMap → Vec pub fn get_processor(&self, location: &str) -> Option<&Processor> { self.processor_by_location .get(location) // HashMap → Option<&usize> .and_then(|&idx| self.processors.get(idx)) // Vec → Option<&Processor> } -// Both lookups return Option — no panics, no UB +// Оба поиска возвращают Option — ни panic, ни неопределённого поведения ``` -### Real example: safe JSON navigation +### Реальный пример: безопасная навигация по JSON ```rust -// Example: framework.rs — every JSON key returns Option +// Пример: framework.rs — каждый ключ JSON возвращает Option let manufacturer = product_fru .get("Manufacturer") // Option<&Value> .and_then(|v| v.as_str()) // Option<&str> - .unwrap_or(UNKNOWN_VALUE) // &str (safe fallback) + .unwrap_or(UNKNOWN_VALUE) // &str (безопасное значение по умолчанию) .to_string(); ``` -Compare to the C++ pattern: `json["SystemInfo"]["ProductFru"]["Manufacturer"]` — any missing key throws `nlohmann::json::out_of_range`. +Сравните с паттерном C++: `json["SystemInfo"]["ProductFru"]["Manufacturer"]` — любой отсутствующий ключ выбрасывает `nlohmann::json::out_of_range`. -### When `[]` is acceptable -- **After a bounds check**: `if i < v.len() { v[i] }` -- **In tests**: Where panicking is the desired behavior -- **With constants**: `let first = v[0];` right after `assert!(!v.is_empty());` +### Когда `[]` допустим +- **После проверки границ**: `if i < v.len() { v[i] }` +- **В тестах**: где panic — желаемое поведение +- **С константами**: `let first = v[0];` сразу после `assert!(!v.is_empty());` ---- -## Safe value extraction with unwrap_or +## Безопасное извлечение значения с unwrap_or -- `unwrap()` panics on `None` / `Err`. In production code, prefer the safe alternatives. +- `unwrap()` вызывает panic при `None` / `Err`. В продакшн-коде предпочитайте безопасные альтернативы. -### The unwrap family -| **Method** | **Behavior on None/Err** | **Use When** | +### Семейство unwrap +| **Метод** | **Поведение при None/Err** | **Когда использовать** | |-----------|------------------------|-------------| -| `.unwrap()` | **Panics** | Tests only, or provably infallible | -| `.expect("msg")` | Panics with message | When panic is justified, explain why | -| `.unwrap_or(default)` | Returns `default` | You have a cheap constant fallback | -| `.unwrap_or_else(\|\| expr)` | Calls closure | Fallback is expensive to compute | -| `.unwrap_or_default()` | Returns `Default::default()` | Type implements `Default` | +| `.unwrap()` | **Panic** | Только в тестах или когда ошибка исключена | +| `.expect("msg")` | Panic с сообщением | Когда panic оправдан — объясните почему | +| `.unwrap_or(default)` | Возвращает `default` | Есть дешёвое константное значение по умолчанию | +| `.unwrap_or_else(\|\| expr)` | Вызывает замыкание | Значение по умолчанию дорого вычислять | +| `.unwrap_or_default()` | Возвращает `Default::default()` | Тип реализует `Default` | -### Real example: parsing with safe defaults +### Реальный пример: разбор с безопасными значениями по умолчанию ```rust -// Example: peripherals.rs -// Regex capture groups might not match — provide safe fallbacks +// Пример: peripherals.rs +// Группы захвата регулярного выражения могут не совпасть — предусматриваем безопасные значения let bus_hex = caps.get(1).map(|m| m.as_str()).unwrap_or("00"); let fw_status = caps.get(5).map(|m| m.as_str()).unwrap_or("0x0"); let bus = u8::from_str_radix(bus_hex, 16).unwrap_or(0); ``` -### Real example: `unwrap_or_else` with fallback struct +### Реальный пример: `unwrap_or_else` с запасной структурой ```rust -// Example: framework.rs -// Full function wraps logic in an Option-returning closure; -// if anything fails, return a default struct: +// Пример: framework.rs +// Вся функция оборачивает логику в замыкание, возвращающее Option; +// если что-то не получилось, возвращаем структуру по умолчанию: (|| -> Option<BaseboardFru> { let content = std::fs::read_to_string(path).ok()?; let json: serde_json::Value = serde_json::from_str(&content).ok()?; - // ... extract fields with .get()? chains + // ... извлекаем поля цепочками .get()? Some(baseboard_fru) })() .unwrap_or_else(|| BaseboardFru { @@ -106,58 +106,58 @@ let bus = u8::from_str_radix(bus_hex, 16).unwrap_or(0); }) ``` -### Real example: `unwrap_or_default` on config deserialization +### Реальный пример: `unwrap_or_default` при десериализации конфигурации ```rust -// Example: framework.rs -// If JSON config parsing fails, fall back to Default — no crash +// Пример: framework.rs +// Если разбор JSON-конфигурации не удался, используем Default — без падения Ok(json) => serde_json::from_str(&json).unwrap_or_default(), ``` -The C++ equivalent would be a `try/catch` around `nlohmann::json::parse()` with manual default construction in the catch block. +Аналог в C++ — `try/catch` вокруг `nlohmann::json::parse()` с ручным созданием значения по умолчанию в блоке catch. ---- -## Functional transforms: map, map_err, find_map +## Функциональные преобразования: map, map_err, find_map -- These methods on `Option` and `Result` let you transform the contained value without unwrapping, replacing nested `if/else` with linear chains. +- Эти методы у `Option` и `Result` позволяют преобразовывать содержащееся значение без распаковки, заменяя вложенные `if/else` линейными цепочками. -### Quick reference -| **Method** | **On** | **Does** | **C++ Equivalent** | +### Краткий справочник +| **Метод** | **Применяется к** | **Что делает** | **Аналог в C++** | |-----------|-------|---------|-------------------| -| `.map(\|v\| ...)` | `Option` / `Result` | Transform the `Some`/`Ok` value | `if (opt) { *opt = transform(*opt); }` | -| `.map_err(\|e\| ...)` | `Result` | Transform the `Err` value | Adding context to catch block | -| `.and_then(\|v\| ...)` | `Option` / `Result` | Chain operations that return `Option`/`Result` | Nested if-checks | -| `.find_map(\|v\| ...)` | Iterator | `find` + `map` in one pass | Loop with `if + break` | -| `.filter(\|v\| ...)` | `Option` / Iterator | Keep only values matching predicate | `if (!predicate) return nullopt;` | -| `.ok()?` | `Result` | Convert `Result → Option` and propagate `None` | `if (result.has_error()) return nullopt;` | - -### Real example: `.and_then()` chain for JSON field extraction +| `.map(\|v\| ...)` | `Option` / `Result` | Преобразует значение `Some`/`Ok` | `if (opt) { *opt = transform(*opt); }` | +| `.map_err(\|e\| ...)` | `Result` | Преобразует значение `Err` | Добавление контекста в блок catch | +| `.and_then(\|v\| ...)` | `Option` / `Result` | Объединяет в цепочку операции, возвращающие `Option`/`Result` | Вложенные проверки if | +| `.find_map(\|v\| ...)` | Итератор | `find` + `map` за один проход | Цикл с `if + break` | +| `.filter(\|v\| ...)` | `Option` / Итератор | Оставляет только значения, подходящие под предикат | `if (!predicate) return nullopt;` | +| `.ok()?` | `Result` | Преобразует `Result → Option` и передаёт `None` дальше | `if (result.has_error()) return nullopt;` | + +### Реальный пример: цепочка `.and_then()` для извлечения полей JSON ```rust -// Example: framework.rs — finding serial number with fallbacks +// Пример: framework.rs — поиск серийного номера с запасными вариантами let sys_info = json.get("SystemInfo")?; -// Try BaseboardFru.BoardSerialNumber first +// Сначала пробуем BaseboardFru.BoardSerialNumber if let Some(serial) = sys_info .get("BaseboardFru") .and_then(|b| b.get("BoardSerialNumber")) .and_then(|v| v.as_str()) - .filter(valid_serial) // Only accept non-empty, valid serials + .filter(valid_serial) // Принимаем только непустые корректные серийные номера { return Some(serial.to_string()); } -// Fallback to BoardFru.SerialNumber +// Запасной вариант: BoardFru.SerialNumber sys_info .get("BoardFru") .and_then(|b| b.get("SerialNumber")) .and_then(|v| v.as_str()) .filter(valid_serial) - .map(|s| s.to_string()) // Convert &str → String only if Some + .map(|s| s.to_string()) // Преобразуем &str → String, только если Some ``` -In C++ this would be a pyramid of `if (json.contains("BaseboardFru")) { if (json["BaseboardFru"].contains("BoardSerialNumber")) { ... } }`. +В C++ это была бы пирамида `if (json.contains("BaseboardFru")) { if (json["BaseboardFru"].contains("BoardSerialNumber")) { ... } }`. -### Real example: `find_map` — search + transform in one pass +### Реальный пример: `find_map` — поиск и преобразование за один проход ```rust -// Example: context.rs — find SDR record matching sensor + owner +// Пример: context.rs — находим запись SDR, соответствующую датчику и владельцу pub fn find_for_event(&self, sensor_number: u8, owner_id: u8) -> Option<&SdrRecord> { self.by_sensor.get(&sensor_number).and_then(|indices| { indices.iter().find_map(|&i| { @@ -171,26 +171,26 @@ pub fn find_for_event(&self, sensor_number: u8, owner_id: u8) -> Option<&SdrReco }) } ``` -`find_map` is `find` + `map` fused: it stops at the first match and transforms it. The C++ equivalent is a `for` loop with an `if` + `break`. +`find_map` — это `find` и `map` в одном: останавливается на первом совпадении и сразу преобразует его. Аналог в C++ — цикл `for` с `if` и `break`. -### Real example: `map_err` for error context +### Реальный пример: `map_err` для контекста ошибки ```rust -// Example: main.rs — add context to errors before propagating +// Пример: main.rs — добавляем контекст к ошибкам перед передачей наверх let json_str = serde_json::to_string_pretty(&config) .map_err(|e| format!("Failed to serialize config: {}", e))?; ``` -Transforms a `serde_json::Error` into a descriptive `String` error that includes context about *what* failed. +Преобразует `serde_json::Error` в понятную ошибку `String`, которая содержит контекст о том, *что именно* не удалось. ---- -## JSON handling: nlohmann::json → serde +## Обработка JSON: nlohmann::json → serde -- C++ teams typically use `nlohmann::json` for JSON parsing. Rust uses **serde** + **serde_json** — which is more powerful because the JSON schema is encoded *in the type system*. +- Команды C++ обычно используют `nlohmann::json` для разбора JSON. В Rust используются **serde** и **serde_json** — это мощнее, потому что схема JSON кодируется *в системе типов*. -### C++ (nlohmann) vs Rust (serde) comparison +### Сравнение C++ (nlohmann) и Rust (serde) ```cpp -// C++ with nlohmann::json — runtime field access +// C++ с nlohmann::json — доступ к полям во время выполнения #include <nlohmann/json.hpp> using json = nlohmann::json; @@ -201,8 +201,8 @@ struct Fan { Fan parse_fan(const json& j) { Fan f; - f.logical_id = j.at("LogicalID").get<std::string>(); // throws if missing - if (j.contains("SDRSensorIdHexes")) { // manual default handling + f.logical_id = j.at("LogicalID").get<std::string>(); // бросает исключение, если нет + if (j.contains("SDRSensorIdHexes")) { // ручная обработка значения по умолчанию f.sensor_ids = j["SDRSensorIdHexes"].get<std::vector<std::string>>(); } return f; @@ -210,72 +210,72 @@ Fan parse_fan(const json& j) { ``` ```rust -// Rust with serde — compile-time schema, automatic field mapping +// Rust с serde — схема на этапе компиляции, автоматическое сопоставление полей use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Fan { pub logical_id: String, - #[serde(rename = "SDRSensorIdHexes", default)] // JSON key → Rust field - pub sensor_ids: Vec<String>, // Missing → empty Vec + #[serde(rename = "SDRSensorIdHexes", default)] // Ключ JSON → поле Rust + pub sensor_ids: Vec<String>, // Нет в JSON → пустой Vec #[serde(default)] - pub sensor_names: Vec<String>, // Missing → empty Vec + pub sensor_names: Vec<String>, // Нет в JSON → пустой Vec } -// One line replaces the entire parse function: +// Одна строка заменяет всю функцию разбора: let fan: Fan = serde_json::from_str(json_str)?; ``` -### Key serde attributes (real examples from production Rust code) +### Ключевые атрибуты serde (реальные примеры из продакшн-кода на Rust) -| **Attribute** | **Purpose** | **C++ Equivalent** | +| **Атрибут** | **Назначение** | **Аналог в C++** | |--------------|------------|--------------------| -| `#[serde(default)]` | Use `Default::default()` for missing fields | `if (j.contains(key)) { ... } else { default; }` | -| `#[serde(rename = "Key")]` | Map JSON key name to Rust field name | Manual `j.at("Key")` access | -| `#[serde(flatten)]` | Absorb unknown keys into `HashMap` | `for (auto& [k,v] : j.items()) { ... }` | -| `#[serde(skip)]` | Don't serialize/deserialize this field | Not storing in JSON | -| `#[serde(tag = "type")]` | Internally tagged enum (discriminator field) | `if (j["type"] == "gpu") { ... }` | +| `#[serde(default)]` | Использовать `Default::default()` для отсутствующих полей | `if (j.contains(key)) { ... } else { default; }` | +| `#[serde(rename = "Key")]` | Сопоставить имя ключа JSON с именем поля Rust | Ручной доступ `j.at("Key")` | +| `#[serde(flatten)]` | Поглощать неизвестные ключи в `HashMap` | `for (auto& [k,v] : j.items()) { ... }` | +| `#[serde(skip)]` | Не сериализовать/десериализовать это поле | Не сохранять в JSON | +| `#[serde(tag = "type")]` | Перечисление с внутренним тегом (поле-дискриминатор) | `if (j["type"] == "gpu") { ... }` | -### Real example: full config struct +### Реальный пример: полная структура конфигурации ```rust -// Example: diag.rs +// Пример: diag.rs #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DiagConfig { pub sku: SkuConfig, #[serde(default)] - pub level: DiagLevel, // Missing → DiagLevel::default() + pub level: DiagLevel, // Нет в JSON → DiagLevel::default() #[serde(default)] - pub modules: ModuleConfig, // Missing → ModuleConfig::default() + pub modules: ModuleConfig, // Нет в JSON → ModuleConfig::default() #[serde(default)] - pub output_dir: String, // Missing → "" + pub output_dir: String, // Нет в JSON → "" #[serde(default, flatten)] - pub options: HashMap<String, serde_json::Value>, // Absorbs unknown keys + pub options: HashMap<String, serde_json::Value>, // Поглощает неизвестные ключи } -// Loading is 3 lines (vs ~20+ in C++ with nlohmann): +// Загрузка — 3 строки (против ~20+ в C++ с nlohmann): let content = std::fs::read_to_string(path)?; let config: DiagConfig = serde_json::from_str(&content)?; Ok(config) ``` -### Enum deserialization with `#[serde(tag = "type")]` +### Десериализация перечислений с `#[serde(tag = "type")]` ```rust -// Example: components.rs +// Пример: components.rs #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] // JSON: {"type": "Gpu", "product": ...} pub enum PcieDeviceKind { Gpu { product: GpuProduct, manufacturer: GpuManufacturer }, Nic { product: NicProduct, manufacturer: NicManufacturer }, NvmeDrive { drive_type: StorageDriveType, capacity_gb: u32 }, - // ... 9 more variants + // ... ещё 9 вариантов } -// serde automatically dispatches on the "type" field — no manual if/else chain +// serde автоматически выбирает вариант по полю "type" — без цепочки if/else вручную ``` -The C++ equivalent would be: `if (j["type"] == "Gpu") { parse_gpu(j); } else if (j["type"] == "Nic") { parse_nic(j); } ...` +Аналог в C++: `if (j["type"] == "Gpu") { parse_gpu(j); } else if (j["type"] == "Nic") { parse_nic(j); } ...` -# Exercise: JSON deserialization with serde +# Упражнение: десериализация JSON с serde -- Define a `ServerConfig` struct that can be deserialized from the following JSON: +- Определите структуру `ServerConfig`, которую можно десериализовать из следующего JSON: ```json { "hostname": "diag-node-01", @@ -284,17 +284,17 @@ The C++ equivalent would be: `if (j["type"] == "Gpu") { parse_gpu(j); } else if "modules": ["accel_diag", "nic_diag", "cpu_diag"] } ``` -- Use `#[derive(Deserialize)]` and `serde_json::from_str()` to parse it -- Add `#[serde(default)]` to `debug` so it defaults to `false` if missing -- **Bonus**: Add an `enum DiagLevel { Quick, Full, Extended }` field with `#[serde(default)]` that defaults to `Quick` +- Используйте `#[derive(Deserialize)]` и `serde_json::from_str()` для разбора +- Добавьте `#[serde(default)]` к полю `debug`, чтобы оно по умолчанию было `false`, если отсутствует +- **Бонус**: добавьте поле `enum DiagLevel { Quick, Full, Extended }` с `#[serde(default)]`, которое по умолчанию равно `Quick` -**Starter code** (requires `cargo add serde --features derive` and `cargo add serde_json`): +**Стартовый код** (нужны `cargo add serde --features derive` и `cargo add serde_json`): ```rust use serde::Deserialize; -// TODO: Define DiagLevel enum with Default impl +// TODO: Определите перечисление DiagLevel с реализацией Default -// TODO: Define ServerConfig struct with serde attributes +// TODO: Определите структуру ServerConfig с атрибутами serde fn main() { let json_input = r#"{ @@ -304,12 +304,12 @@ fn main() { "modules": ["accel_diag", "nic_diag", "cpu_diag"] }"#; - // TODO: Deserialize and print the config - // TODO: Try parsing JSON with "debug" field missing — verify it defaults to false + // TODO: Десериализуйте и выведите конфигурацию + // TODO: Попробуйте разобрать JSON без поля "debug" — убедитесь, что оно по умолчанию false } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use serde::Deserialize; @@ -326,10 +326,10 @@ enum DiagLevel { struct ServerConfig { hostname: String, port: u16, - #[serde(default)] // defaults to false if missing + #[serde(default)] // по умолчанию false, если отсутствует debug: bool, modules: Vec<String>, - #[serde(default)] // defaults to DiagLevel::Quick if missing + #[serde(default)] // по умолчанию DiagLevel::Quick, если отсутствует level: DiagLevel, } @@ -345,7 +345,7 @@ fn main() { .expect("Failed to parse JSON"); println!("{config:#?}"); - // Test with missing optional fields + // Проверка с отсутствующими необязательными полями let minimal = r#"{ "hostname": "node-02", "port": 9090, @@ -356,7 +356,7 @@ fn main() { println!("debug (default): {}", config2.debug); // false println!("level (default): {:?}", config2.level); // Quick } -// Output: +// Вывод: // ServerConfig { // hostname: "diag-node-01", // port: 8080, @@ -372,4 +372,3 @@ fn main() { ---- - diff --git a/c-cpp-book/src/ch17-3-collapsing-assignment-pyramids.md b/c-cpp-book/src/ch17-3-collapsing-assignment-pyramids.md index 14b88e5..89e19e6 100644 --- a/c-cpp-book/src/ch17-3-collapsing-assignment-pyramids.md +++ b/c-cpp-book/src/ch17-3-collapsing-assignment-pyramids.md @@ -1,12 +1,12 @@ -## Collapsing assignment pyramids with closures +## Схлопывание пирамид присваиваний с помощью замыканий -> **What you'll learn:** How Rust's expression-based syntax and closures flatten deeply-nested C++ `if/else` validation chains into clean, linear code. +> **Что вы узнаете:** как выразительный синтаксис Rust на основе выражений и замыкания превращают глубоко вложенные цепочки проверок `if/else` из C++ в чистый линейный код. -- C++ often requires multi-block `if/else` chains to assign variables, especially when validation or fallback logic is involved. Rust's expression-based syntax and closures collapse these into flat, linear code. +- В C++ для присваивания переменных часто нужны многоблочные цепочки `if/else`, особенно когда есть проверки или логика запасных вариантов. Синтаксис Rust на основе выражений и замыкания сворачивают их в плоский линейный код. -### Pattern 1: Tuple assignment with `if` expression +### Паттерн 1: присваивание кортежа через выражение `if` ```cpp -// C++ — three variables set across a multi-block if/else chain +// C++ — три переменные задаются в многоблочной цепочке if/else uint32_t fault_code; const char* der_marker; const char* action; @@ -20,8 +20,8 @@ if (is_c44ad) { ``` ```rust -// Rust equivalent:accel_fieldiag.rs -// Single expression assigns all three at once: +// Эквивалент на Rust:accel_fieldiag.rs +// Одно выражение присваивает все три сразу: let (fault_code, der_marker, recommended_action) = if is_c44ad { (32709u32, "CSI_WARN", "No action") } else if error.is_hardware_error() { @@ -31,9 +31,9 @@ let (fault_code, der_marker, recommended_action) = if is_c44ad { }; ``` -### Pattern 2: IIFE (Immediately Invoked Function Expression) for fallible chains +### Паттерн 2: IIFE (немедленно вызываемое функциональное выражение) для цепочек с возможными ошибками ```cpp -// C++ — pyramid of doom for JSON navigation +// C++ — «пирамида гибели» для навигации по JSON std::string get_part_number(const nlohmann::json& root) { if (root.contains("SystemInfo")) { auto& sys = root["SystemInfo"]; @@ -49,8 +49,8 @@ std::string get_part_number(const nlohmann::json& root) { ``` ```rust -// Rust equivalent:framework.rs -// Closure + ? operator collapses the pyramid into linear code: +// Эквивалент на Rust:framework.rs +// Замыкание + оператор ? превращают пирамиду в линейный код: let part_number = (|| -> Option<String> { let path = self.args.sysinfo.as_ref()?; let content = std::fs::read_to_string(path).ok()?; @@ -64,15 +64,15 @@ let part_number = (|| -> Option<String> { })() .unwrap_or_else(|| "UNKNOWN".to_string()); ``` -The closure creates an `Option<String>` scope where `?` bails early at any step. The `.unwrap_or_else()` provides the fallback once, at the end. +Замыкание создаёт область `Option<String>`, в которой `?` досрочно прерывает выполнение на любом шаге. `.unwrap_or_else()` задаёт запасной вариант один раз, в конце. -### Pattern 3: Iterator chain replacing manual loop + push_back +### Паттерн 3: цепочка итераторов вместо ручного цикла + push_back ```cpp -// C++ — manual loop with intermediate variables +// C++ — ручной цикл с промежуточными переменными std::vector<std::tuple<std::vector<std::string>, std::string, std::string>> gpu_info; for (const auto& [key, info] : gpu_pcie_map) { std::vector<std::string> bdfs; - // ... parse bdf_path into bdfs + // ... разбираем bdf_path в bdfs std::string serial = info.serial_number.value_or("UNKNOWN"); std::string model = info.model_number.value_or(model_name); gpu_info.push_back({bdfs, serial, model}); @@ -80,8 +80,8 @@ for (const auto& [key, info] : gpu_pcie_map) { ``` ```rust -// Rust equivalent:peripherals.rs -// Single chain: values() → map → collect +// Эквивалент на Rust:peripherals.rs +// Одна цепочка: values() → map → collect let gpu_info: Vec<(Vec<String>, String, String, String)> = self .gpu_pcie_map .values() @@ -102,7 +102,7 @@ let gpu_info: Vec<(Vec<String>, String, String, String)> = self .collect(); ``` -### Pattern 4: `.filter().collect()` replacing loop + `if (condition) continue` +### Паттерн 4: `.filter().collect()` вместо цикла + `if (condition) continue` ```cpp // C++ std::vector<TestResult*> failures; @@ -114,37 +114,37 @@ for (auto& t : test_results) { ``` ```rust -// Rust — from accel_diag/src/healthcheck.rs +// Rust — из accel_diag/src/healthcheck.rs pub fn failed_tests(&self) -> Vec<&TestResult> { self.test_results.iter().filter(|t| !t.is_pass()).collect() } ``` -### Summary: When to use each pattern -| **C++ Pattern** | **Rust Replacement** | **Key Benefit** | +### Итоги: когда применять каждый паттерн +| **Паттерн C++** | **Замена в Rust** | **Ключевое преимущество** | |----------------|---------------------|-----------------| -| Multi-block variable assignment | `let (a, b) = if ... { } else { };` | All variables bound atomically | -| Nested `if (contains)` pyramid | IIFE closure with `?` operator | Linear, flat, early-exit | -| `for` loop + `push_back` | `.iter().map(\|\|).collect()` | No intermediate mut Vec | -| `for` + `if (cond) continue` | `.iter().filter(\|\|).collect()` | Declarative intent | -| `for` + `if + break` (find first) | `.iter().find_map(\|\|)` | Search + transform in one pass | +| Присваивание переменных в многоблочном `if` | `let (a, b) = if ... { } else { };` | Все переменные связываются атомарно | +| Вложенная пирамида `if (contains)` | IIFE-замыкание с оператором `?` | Линейно, плоско, ранний выход | +| Цикл `for` + `push_back` | `.iter().map(\|\|).collect()` | Не нужен промежуточный изменяемый Vec | +| `for` + `if (cond) continue` | `.iter().filter(\|\|).collect()` | Декларативное намерение | +| `for` + `if + break` (поиск первого) | `.iter().find_map(\|\|)` | Поиск и преобразование за один проход | ---- -# Capstone Exercise: Diagnostic Event Pipeline +# Итоговое упражнение: конвейер диагностических событий -🔴 **Challenge** — integrative exercise combining enums, traits, iterators, error handling, and generics +🔴 **Сложный уровень** — комплексное упражнение, объединяющее перечисления, трейты, итераторы, обработку ошибок и обобщения -This integrative exercise brings together enums, traits, iterators, error handling, and generics. You'll build a simplified diagnostic event processing pipeline similar to patterns used in production Rust code. +Это комплексное упражнение объединяет перечисления, трейты, итераторы, обработку ошибок и обобщения. Вы создадите упрощённый конвейер обработки диагностических событий, похожий на паттерны, используемые в продакшн-коде на Rust. -**Requirements:** -1. Define an `enum Severity { Info, Warning, Critical }` with `Display`, and a `struct DiagEvent` containing `source: String`, `severity: Severity`, `message: String`, and `fault_code: u32` -2. Define a `trait EventFilter` with a method `fn should_include(&self, event: &DiagEvent) -> bool` -3. Implement two filters: `SeverityFilter` (only events >= a given severity) and `SourceFilter` (only events from a specific source string) -4. Write a function `fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String>` that returns formatted report lines for events that pass **all** filters -5. Write a `fn parse_event(line: &str) -> Result<DiagEvent, String>` that parses lines of the form `"source:severity:fault_code:message"` (return `Err` for bad input) +**Требования:** +1. Определите `enum Severity { Info, Warning, Critical }` с реализацией `Display`, а также `struct DiagEvent`, содержащую `source: String`, `severity: Severity`, `message: String` и `fault_code: u32` +2. Определите `trait EventFilter` с методом `fn should_include(&self, event: &DiagEvent) -> bool` +3. Реализуйте два фильтра: `SeverityFilter` (только события с серьёзностью >= заданной) и `SourceFilter` (только события от определённого источника) +4. Напишите функцию `fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String>`, которая возвращает отформатированные строки отчёта для событий, прошедших **все** фильтры +5. Напишите `fn parse_event(line: &str) -> Result<DiagEvent, String>`, которая разбирает строки вида `"source:severity:fault_code:message"` (возвращает `Err` для некорректного ввода) -**Starter code:** +**Стартовый код:** ```rust use std::fmt; @@ -184,14 +184,14 @@ struct SourceFilter { // TODO: impl EventFilter for SourceFilter fn process_events(events: &[DiagEvent], filters: &[&dyn EventFilter]) -> Vec<String> { - // TODO: Filter events that pass ALL filters, format as + // TODO: Отфильтровать события, прошедшие ВСЕ фильтры, и отформатировать как // "[SEVERITY] source (FC:fault_code): message" todo!() } fn parse_event(line: &str) -> Result<DiagEvent, String> { - // Parse "source:severity:fault_code:message" - // Return Err for invalid input + // Разобрать "source:severity:fault_code:message" + // Вернуть Err для некорректного ввода todo!() } @@ -204,7 +204,7 @@ fn main() { "accel_diag:Warning:32710:PCIe link width reduced", ]; - // Parse all lines, collect successes and report errors + // Разобрать все строки, собрать успешные и вывести ошибки разбора let events: Vec<DiagEvent> = raw_lines.iter() .filter_map(|line| match parse_event(line) { Ok(e) => Some(e), @@ -212,7 +212,7 @@ fn main() { }) .collect(); - // Apply filters: only Critical+Warning events from accel_diag + // Применяем фильтры: только Critical и Warning от accel_diag let sev_filter = SeverityFilter { min_severity: Severity::Warning }; let src_filter = SourceFilter { source: "accel_diag".to_string() }; let filters: Vec<&dyn EventFilter> = vec![&sev_filter, &src_filter]; @@ -225,7 +225,7 @@ fn main() { } ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::fmt; @@ -338,7 +338,7 @@ fn main() { } println!("--- {} event(s) matched ---", report.len()); } -// Output: +// Вывод: // [CRITICAL] accel_diag (FC:67956): ECC uncorrectable error detected // [WARNING] accel_diag (FC:32710): PCIe link width reduced // --- 2 event(s) matched --- diff --git a/c-cpp-book/src/ch17-4-logging-and-tracing-ecosystem.md b/c-cpp-book/src/ch17-4-logging-and-tracing-ecosystem.md index c2c3e9c..346b8de 100644 --- a/c-cpp-book/src/ch17-4-logging-and-tracing-ecosystem.md +++ b/c-cpp-book/src/ch17-4-logging-and-tracing-ecosystem.md @@ -1,40 +1,40 @@ -## Logging and Tracing: syslog/printf → `log` + `tracing` +## Экосистема логирования и трассировки: syslog/printf → `log` + `tracing` -> **What you'll learn:** Rust's two-layer logging architecture (facade + backend), the `log` and `tracing` crates, structured logging with spans, and how this replaces `printf`/`syslog` debugging. +> **Что вы узнаете:** двухуровневую архитектуру логирования в Rust (фасад + бэкенд), крейты `log` и `tracing`, структурированное логирование со спанами и то, чем это заменяет отладку через `printf`/`syslog`. -C++ diagnostic code typically uses `printf`, `syslog`, or custom logging frameworks. -Rust has a standardized two-layer logging architecture: a **facade** crate (`log` or -`tracing`) and a **backend** (the actual logger implementation). +Диагностический код на C++ обычно использует `printf`, `syslog` или собственные фреймворки логирования. +В Rust есть стандартизированная двухуровневая архитектура логирования: крейт-**фасад** (`log` или +`tracing`) и **бэкенд** (собственно реализация логгера). -### The `log` facade — Rust's universal logging API +### Фасад `log` — универсальный API логирования в Rust -The `log` crate provides macros that mirror syslog severity levels. Libraries use -`log` macros; binaries choose a backend: +Крейт `log` предоставляет макросы, которые повторяют уровни серьёзности syslog. Библиотеки используют +макросы `log`, а бинарные крейты выбирают бэкенд: ```rust // Cargo.toml // [dependencies] // log = "0.4" -// env_logger = "0.11" # One of many backends +// env_logger = "0.11" # Один из многих бэкендов use log::{info, warn, error, debug, trace}; fn check_sensor(id: u32, temp: f64) { - trace!("Reading sensor {id}"); // Finest granularity - debug!("Sensor {id} raw value: {temp}"); // Development-time detail - + trace!("Reading sensor {id}"); // Самая мелкая детализация + debug!("Sensor {id} raw value: {temp}"); // Подробности для разработки + if temp > 85.0 { warn!("Sensor {id} high temperature: {temp}°C"); } if temp > 95.0 { error!("Sensor {id} CRITICAL: {temp}°C — initiating shutdown"); } - info!("Sensor {id} check complete"); // Normal operation + info!("Sensor {id} check complete"); // Штатная работа } fn main() { - // Initialize the backend — typically done once in main() - env_logger::init(); // Controlled by RUST_LOG env var + // Инициализация бэкенда — обычно делается один раз в main() + env_logger::init(); // Управляется переменной окружения RUST_LOG check_sensor(0, 72.5); check_sensor(1, 91.0); @@ -42,27 +42,27 @@ fn main() { ``` ```bash -# Control log level via environment variable -RUST_LOG=debug cargo run # Show debug and above -RUST_LOG=warn cargo run # Show only warn and error -RUST_LOG=my_crate=trace cargo run # Per-module filtering -RUST_LOG=my_crate::gpu=debug,warn cargo run # Mix levels +# Управление уровнем логирования через переменную окружения +RUST_LOG=debug cargo run # Показать debug и выше +RUST_LOG=warn cargo run # Показать только warn и error +RUST_LOG=my_crate=trace cargo run # Фильтрация по модулям +RUST_LOG=my_crate::gpu=debug,warn cargo run # Разные уровни для разных частей ``` -### C++ comparison +### Сравнение с C++ -| C++ | Rust (`log`) | Notes | +| C++ | Rust (`log`) | Примечания | |-----|-------------|-------| -| `printf("DEBUG: %s\n", msg)` | `debug!("{msg}")` | Format checked at compile time | -| `syslog(LOG_ERR, "...")` | `error!("...")` | Backend decides where output goes | -| `#ifdef DEBUG` around log calls | `trace!` / `debug!` compiled out at max_level | Zero-cost when disabled | -| Custom `Logger::log(level, msg)` | `log::info!("...")` — all crates use same API | Universal facade, swappable backend | -| Per-file log verbosity | `RUST_LOG=crate::module=level` | Environment-based, no recompile | +| `printf("DEBUG: %s\n", msg)` | `debug!("{msg}")` | Формат проверяется на этапе компиляции | +| `syslog(LOG_ERR, "...")` | `error!("...")` | Бэкенд решает, куда попадёт вывод | +| `#ifdef DEBUG` вокруг вызовов логирования | `trace!` / `debug!` удаляются при max_level | Нулевая стоимость, когда отключено | +| Собственный `Logger::log(level, msg)` | `log::info!("...")` — все крейты используют один API | Универсальный фасад, заменяемый бэкенд | +| Детализация логов для каждого файла | `RUST_LOG=crate::module=level` | Настраивается через окружение, без перекомпиляции | -### The `tracing` crate — structured logging with spans +### Крейт `tracing` — структурированное логирование со спанами -`tracing` extends `log` with **structured fields** and **spans** (timed scopes). -This is especially useful for diagnostics code where you want to track context: +`tracing` расширяет `log` **структурированными полями** и **спанами** (ограниченными по времени областями). +Это особенно полезно в диагностическом коде, где нужно отслеживать контекст: ```rust // Cargo.toml @@ -77,14 +77,14 @@ fn run_gpu_test(gpu_id: u32, data: &[u8]) -> Result<(), String> { info!("Starting GPU test"); let span = info_span!("ecc_check", gpu_id); - let _guard = span.enter(); // All logs inside this scope include gpu_id + let _guard = span.enter(); // Все логи внутри этой области включают gpu_id if data.is_empty() { error!(gpu_id, "No test data provided"); return Err("empty data".to_string()); } - // Structured fields — machine-parseable, not just string interpolation + // Структурированные поля — пригодны для машинного разбора, а не просто подстановка строк info!( gpu_id, temp_celsius = 72.5, @@ -96,41 +96,41 @@ fn run_gpu_test(gpu_id: u32, data: &[u8]) -> Result<(), String> { } fn main() { - // Initialize tracing subscriber + // Инициализация подписчика tracing tracing_subscriber::fmt() - .with_env_filter("debug") // Or use RUST_LOG env var - .with_target(true) // Show module path - .with_thread_ids(true) // Show thread IDs + .with_env_filter("debug") // Или используйте переменную окружения RUST_LOG + .with_target(true) // Показывать путь модуля + .with_thread_ids(true) // Показывать ID потоков .init(); let _ = run_gpu_test(0, &[1, 2, 3]); } ``` -Output with `tracing-subscriber`: +Вывод `tracing-subscriber`: ```rust 2026-02-15T10:30:00.123Z DEBUG ThreadId(01) run_gpu_test{gpu_id=0 data_len=3}: my_crate: Starting GPU test 2026-02-15T10:30:00.124Z INFO ThreadId(01) run_gpu_test{gpu_id=0 data_len=3}:ecc_check{gpu_id=0}: my_crate: ECC check passed gpu_id=0 temp_celsius=72.5 ecc_errors=0 ``` -### `#[instrument]` — automatic span creation +### `#[instrument]` — автоматическое создание спанов -The `#[instrument]` attribute automatically creates a span with the function name -and its arguments: +Атрибут `#[instrument]` автоматически создаёт спан с именем функции +и её аргументами: ```rust use tracing::instrument; #[instrument] fn parse_sel_record(record_id: u16, sensor_type: u8, data: &[u8]) -> Result<(), String> { - // Every log inside this function automatically includes: - // record_id, sensor_type, and data (if Debug) + // Каждый лог внутри этой функции автоматически включает: + // record_id, sensor_type и data (если реализован Debug) tracing::debug!("Parsing SEL record"); Ok(()) } -// skip: exclude large/sensitive args from the span -// fields: add computed fields +// skip: исключить из спана большие/чувствительные аргументы +// fields: добавить вычисляемые поля #[instrument(skip(raw_buffer), fields(buf_len = raw_buffer.len()))] fn decode_ipmi_response(raw_buffer: &[u8]) -> Result<Vec<u8>, String> { tracing::trace!("Decoding {} bytes", raw_buffer.len()); @@ -138,32 +138,32 @@ fn decode_ipmi_response(raw_buffer: &[u8]) -> Result<Vec<u8>, String> { } ``` -### `log` vs `tracing` — which to use +### `log` или `tracing` — что выбрать -| Aspect | `log` | `tracing` | +| Аспект | `log` | `tracing` | |--------|-------|-----------| -| **Complexity** | Simple — 5 macros | Richer — spans, fields, instruments | -| **Structured data** | String interpolation only | Key-value fields: `info!(gpu_id = 0, "msg")` | -| **Timing / spans** | No | Yes — `#[instrument]`, `span.enter()` | -| **Async support** | Basic | First-class — spans propagate across `.await` | -| **Compatibility** | Universal facade | Compatible with `log` (has a `log` bridge) | -| **When to use** | Simple applications, libraries | Diagnostic tools, async code, observability | +| **Сложность** | Простой — 5 макросов | Богаче — спаны, поля, instrument | +| **Структурированные данные** | Только подстановка строк | Поля ключ-значение: `info!(gpu_id = 0, "msg")` | +| **Время / спаны** | Нет | Да — `#[instrument]`, `span.enter()` | +| **Поддержка async** | Базовая | Полноценная — спаны передаются через `.await` | +| **Совместимость** | Универсальный фасад | Совместим с `log` (есть мост `log`) | +| **Когда использовать** | Простые приложения, библиотеки | Диагностические инструменты, async-код, наблюдаемость | -> **Recommendation**: Use `tracing` for production diagnostic-style projects (diagnostic tools -> with structured output). Use `log` for simple libraries where you want minimal -> dependencies. `tracing` includes a compatibility layer so libraries using `log` -> macros still work with a `tracing` subscriber. +> **Рекомендация**: используйте `tracing` для продакшн-проектов диагностического характера +> (диагностические инструменты со структурированным выводом). Используйте `log` для простых библиотек, где +> нужно минимум зависимостей. `tracing` включает слой совместимости, так что библиотеки, +> использующие макросы `log`, продолжат работать с подписчиком `tracing`. -### Backend options +### Варианты бэкендов -| Backend Crate | Output | Use Case | +| Крейт бэкенда | Вывод | Сценарий использования | |--------------|--------|----------| -| `env_logger` | stderr, colored | Development, simple CLI tools | -| `tracing-subscriber` | stderr, formatted | Production with `tracing` | -| `syslog` | System syslog | Linux system services | -| `tracing-journald` | systemd journal | systemd-managed services | -| `tracing-appender` | Rotating log files | Long-running daemons | -| `tracing-opentelemetry` | OpenTelemetry collector | Distributed tracing | +| `env_logger` | stderr, цветной | Разработка, простые CLI-инструменты | +| `tracing-subscriber` | stderr, форматированный | Продакшн с `tracing` | +| `syslog` | Системный syslog | Системные службы Linux | +| `tracing-journald` | Журнал systemd | Службы под управлением systemd | +| `tracing-appender` | Ротируемые файлы логов | Долгоживущие демоны | +| `tracing-opentelemetry` | Коллектор OpenTelemetry | Распределённая трассировка | ---- diff --git a/c-cpp-book/src/ch17-best-practices.md b/c-cpp-book/src/ch17-best-practices.md index 80ef57f..cd905c3 100644 --- a/c-cpp-book/src/ch17-best-practices.md +++ b/c-cpp-book/src/ch17-best-practices.md @@ -1,38 +1,38 @@ -# Rust Best Practices Summary +# Сводка лучших практик Rust -> **What you'll learn:** Practical guidelines for writing idiomatic Rust — code organization, naming conventions, error handling patterns, and documentation. A quick-reference chapter you'll return to often. +> **Что вы узнаете:** практические рекомендации по написанию идиоматичного Rust — организация кода, соглашения об именовании, паттерны обработки ошибок и документирование. Краткая справочная глава, к которой вы будете возвращаться часто. -## Code Organization -- **Prefer small functions**: Easy to test and reason about -- **Use descriptive names**: `calculate_total_price()` vs `calc()` -- **Group related functionality**: Use modules and separate files -- **Write documentation**: Use `///` for public APIs +## Организация кода +- **Предпочитайте небольшие функции**: их легко тестировать и понимать +- **Используйте описательные имена**: `calculate_total_price()` вместо `calc()` +- **Группируйте связанную функциональность**: используйте модули и отдельные файлы +- **Пишите документацию**: используйте `///` для публичных API -## Error Handling -- **Avoid `unwrap()` unless infallible**: Only use when you're 100% certain it won't panic +## Обработка ошибок +- **Избегайте `unwrap()`, если операция не может завершиться неудачей**: используйте его, только когда на 100% уверены, что panic не будет ```rust -// Bad: Can panic +// Плохо: может вызвать panic let value = some_option.unwrap(); -// Good: Handle the None case +// Хорошо: обрабатываем случай None let value = some_option.unwrap_or(default_value); let value = some_option.unwrap_or_else(|| expensive_computation()); -let value = some_option.unwrap_or_default(); // Uses Default trait +let value = some_option.unwrap_or_default(); // Использует трейт Default -// For Result<T, E> +// Для Result<T, E> let value = some_result.unwrap_or(fallback_value); let value = some_result.unwrap_or_else(|err| { eprintln!("Error occurred: {err}"); default_value }); ``` -- **Use `expect()` with descriptive messages**: When unwrap is justified, explain why +- **Используйте `expect()` с понятными сообщениями**: если unwrap оправдан, объясните почему ```rust let config = std::env::var("CONFIG_PATH") .expect("CONFIG_PATH environment variable must be set"); ``` -- **Return `Result<T, E>` for fallible operations**: Let callers decide how to handle errors -- **Use `thiserror` for custom error types**: More ergonomic than manual implementations +- **Возвращайте `Result<T, E>` из операций, которые могут завершиться ошибкой**: пусть вызывающий код решает, как обрабатывать ошибки +- **Используйте `thiserror` для собственных типов ошибок**: это удобнее ручной реализации ```rust use thiserror::Error; @@ -48,153 +48,149 @@ pub enum MyError { OutOfRange { value: i32 }, } ``` -- **Chain errors with `?` operator**: Propagate errors up the call stack -- **Prefer `thiserror` over `anyhow`**: Our team convention is to define explicit error - enums with `#[derive(thiserror::Error)]` so callers can match on specific variants. - `anyhow::Error` is convenient for quick prototyping but erases the error type, making - it harder for callers to handle specific failures. Use `thiserror` for library and - production code; reserve `anyhow` for throwaway scripts or top-level binaries where - you only need to print the error. -- **When `unwrap()` is acceptable**: - - **Unit tests**: `assert_eq!(result.unwrap(), expected)` - - **Prototyping**: Quick and dirty code that you'll replace - - **Infallible operations**: When you can prove it won't fail +- **Цепочка ошибок через оператор `?`**: передавайте ошибки вверх по стеку вызовов +- **Предпочитайте `thiserror` перед `anyhow`**: соглашение нашей команды — определять явные перечисления ошибок через `#[derive(thiserror::Error)]`, чтобы вызывающий код мог сопоставлять конкретные варианты. + `anyhow::Error` удобен для быстрых прототипов, но стирает тип ошибки, из-за чего вызывающему коду сложнее обрабатывать конкретные сбои. Используйте `thiserror` для библиотечного и продакшн-кода; `anyhow` оставьте для одноразовых скриптов или бинарников верхнего уровня, где нужно только вывести ошибку. +- **Когда `unwrap()` допустим**: + - **Модульные тесты**: `assert_eq!(result.unwrap(), expected)` + - **Прототипирование**: быстрый и грязный код, который вы потом замените + - **Операции, которые не могут завершиться ошибкой**: когда вы можете доказать, что сбоя не будет ```rust let numbers = vec![1, 2, 3]; -let first = numbers.get(0).unwrap(); // Safe: we just created the vec with elements +let first = numbers.get(0).unwrap(); // Безопасно: мы только что создали вектор с элементами -// Better: Use expect() with explanation +// Лучше: используем expect() с объяснением let first = numbers.get(0).expect("numbers vec is non-empty by construction"); ``` -- **Fail fast**: Check preconditions early and return errors immediately +- **Быстро падайте (fail fast)**: проверяйте предусловия в начале и сразу возвращайте ошибки -## Memory Management -- **Prefer borrowing over cloning**: Use `&T` instead of cloning when possible -- **Use `Rc<T>` sparingly**: Only when you need shared ownership -- **Limit lifetimes**: Use scopes `{}` to control when values are dropped -- **Avoid `RefCell<T>` in public APIs**: Keep interior mutability internal +## Управление памятью +- **Предпочитайте заимствование клонированию**: используйте `&T` вместо клонирования, когда это возможно +- **Используйте `Rc<T>` умеренно**: только когда нужно совместное владение +- **Ограничивайте времена жизни**: используйте области видимости `{}`, чтобы контролировать, когда значения уничтожаются +- **Избегайте `RefCell<T>` в публичных API**: внутреннюю изменяемость держите внутри -## Performance -- **Profile before optimizing**: Use `cargo bench` and profiling tools -- **Prefer iterators over loops**: More readable and often faster -- **Use `&str` over `String`**: When you don't need ownership -- **Consider `Box<T>` for large stack objects**: Move them to heap if needed +## Производительность +- **Профилируйте перед оптимизацией**: используйте `cargo bench` и инструменты профилирования +- **Предпочитайте итераторы циклам**: они читаемее и часто быстрее +- **Используйте `&str` вместо `String`**: когда не нужно владение +- **Рассмотрите `Box<T>` для больших объектов на стеке**: при необходимости переносите их в кучу -## Essential Traits to Implement +## Трейты, которые стоит реализовать -### Core Traits Every Type Should Consider +### Базовые трейты, о которых стоит подумать для каждого типа -When creating custom types, consider implementing these fundamental traits to make your types feel native to Rust: +Создавая собственные типы, рассмотрите реализацию этих базовых трейтов, чтобы ваши типы ощущались в Rust «родными»: -#### **Debug and Display** +#### **Debug и Display** ```rust use std::fmt; -#[derive(Debug)] // Automatic implementation for debugging +#[derive(Debug)] // Автоматическая реализация для отладки struct Person { name: String, age: u32, } -// Manual Display implementation for user-facing output +// Ручная реализация Display для вывода, который видит пользователь impl fmt::Display for Person { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} (age {})", self.name, self.age) } } -// Usage: +// Использование: let person = Person { name: "Alice".to_string(), age: 30 }; println!("{:?}", person); // Debug: Person { name: "Alice", age: 30 } println!("{}", person); // Display: Alice (age 30) ``` -#### **Clone and Copy** +#### **Clone и Copy** ```rust -// Copy: Implicit duplication for small, simple types +// Copy: неявное дублирование для малых простых типов #[derive(Debug, Clone, Copy)] struct Point { x: i32, y: i32, } -// Clone: Explicit duplication for complex types +// Clone: явное дублирование для сложных типов #[derive(Debug, Clone)] struct Person { - name: String, // String doesn't implement Copy + name: String, // String не реализует Copy age: u32, } let p1 = Point { x: 1, y: 2 }; -let p2 = p1; // Copy (implicit) +let p2 = p1; // Copy (неявно) let person1 = Person { name: "Bob".to_string(), age: 25 }; -let person2 = person1.clone(); // Clone (explicit) +let person2 = person1.clone(); // Clone (явно) ``` -#### **PartialEq and Eq** +#### **PartialEq и Eq** ```rust #[derive(Debug, PartialEq, Eq)] struct UserId(u64); #[derive(Debug, PartialEq)] struct Temperature { - celsius: f64, // f64 doesn't implement Eq (due to NaN) + celsius: f64, // f64 не реализует Eq (из-за NaN) } let id1 = UserId(123); let id2 = UserId(123); -assert_eq!(id1, id2); // Works because of PartialEq +assert_eq!(id1, id2); // Работает благодаря PartialEq let temp1 = Temperature { celsius: 20.0 }; let temp2 = Temperature { celsius: 20.0 }; -assert_eq!(temp1, temp2); // Works with PartialEq +assert_eq!(temp1, temp2); // Работает с PartialEq ``` -#### **PartialOrd and Ord** +#### **PartialOrd и Ord** ```rust #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct Priority(u8); let high = Priority(1); let low = Priority(10); -assert!(high < low); // Lower numbers = higher priority +assert!(high < low); // Меньшие числа = выше приоритет -// Use in collections +// Использование в коллекциях let mut priorities = vec![Priority(5), Priority(1), Priority(8)]; -priorities.sort(); // Works because Priority implements Ord +priorities.sort(); // Работает, потому что Priority реализует Ord ``` #### **Default** ```rust #[derive(Debug, Default)] struct Config { - debug: bool, // false (default) - max_connections: u32, // 0 (default) - timeout: Option<u64>, // None (default) + debug: bool, // false (по умолчанию) + max_connections: u32, // 0 (по умолчанию) + timeout: Option<u64>, // None (по умолчанию) } -// Custom Default implementation +// Собственная реализация Default impl Default for Config { fn default() -> Self { Config { debug: false, - max_connections: 100, // Custom default - timeout: Some(30), // Custom default + max_connections: 100, // Собственное значение по умолчанию + timeout: Some(30), // Собственное значение по умолчанию } } } let config = Config::default(); -let config = Config { debug: true, ..Default::default() }; // Partial override +let config = Config { debug: true, ..Default::default() }; // Частичное переопределение ``` -#### **From and Into** +#### **From и Into** ```rust struct UserId(u64); struct UserName(String); -// Implement From, and Into comes for free +// Реализуем From, а Into получаем бесплатно impl From<u64> for UserId { fn from(id: u64) -> Self { UserId(id) @@ -213,14 +209,14 @@ impl From<&str> for UserName { } } -// Usage: -let user_id: UserId = 123u64.into(); // Using Into -let user_id = UserId::from(123u64); // Using From +// Использование: +let user_id: UserId = 123u64.into(); // Через Into +let user_id = UserId::from(123u64); // Через From let username = UserName::from("alice"); // &str -> UserName -let username: UserName = "bob".into(); // Using Into +let username: UserName = "bob".into(); // Через Into ``` -#### **TryFrom and TryInto** +#### **TryFrom и TryInto** ```rust use std::convert::TryFrom; @@ -241,12 +237,12 @@ impl TryFrom<i32> for PositiveNumber { } } -// Usage: +// Использование: let positive = PositiveNumber::try_from(42)?; // Ok(PositiveNumber(42)) let error = PositiveNumber::try_from(-5); // Err(NegativeNumberError) ``` -#### **Serde (for serialization)** +#### **Serde (для сериализации)** ```rust use serde::{Deserialize, Serialize}; @@ -257,7 +253,7 @@ struct User { email: String, } -// Automatic JSON serialization/deserialization +// Автоматическая сериализация и десериализация в JSON let user = User { id: 1, name: "Alice".to_string(), @@ -268,57 +264,56 @@ let json = serde_json::to_string(&user)?; let deserialized: User = serde_json::from_str(&json)?; ``` -### Trait Implementation Checklist +### Чек-лист реализации трейтов -For any new type, consider this checklist: +Для любого нового типа рассмотрите этот чек-лист: ```rust #[derive( - Debug, // [OK] Always implement for debugging - Clone, // [OK] If the type should be duplicatable - PartialEq, // [OK] If the type should be comparable - Eq, // [OK] If comparison is reflexive/transitive - PartialOrd, // [OK] If the type has ordering - Ord, // [OK] If ordering is total - Hash, // [OK] If type will be used as HashMap key - Default, // [OK] If there's a sensible default value + Debug, // [OK] Всегда реализуйте для отладки + Clone, // [OK] Если тип должен можно было дублировать + PartialEq, // [OK] Если тип должен можно было сравнивать + Eq, // [OK] Если сравнение рефлексивно и транзитивно + PartialOrd, // [OK] Если у типа есть порядок + Ord, // [OK] Если порядок полный + Hash, // [OK] Если тип будет ключом HashMap + Default, // [OK] Если есть разумное значение по умолчанию )] struct MyType { - // fields... + // поля... } -// Manual implementations to consider: -impl Display for MyType { /* user-facing representation */ } -impl From<OtherType> for MyType { /* convenient conversion */ } -impl TryFrom<FallibleType> for MyType { /* fallible conversion */ } +// Ручные реализации, которые стоит рассмотреть: +impl Display for MyType { /* представление для пользователя */ } +impl From<OtherType> for MyType { /* удобное преобразование */ } +impl TryFrom<FallibleType> for MyType { /* преобразование, которое может завершиться ошибкой */ } ``` -### When NOT to Implement Traits - -- **Don't implement Copy for types with heap data**: `String`, `Vec`, `HashMap` etc. -- **Don't implement Eq if values can be NaN**: Types containing `f32`/`f64` -- **Don't implement Default if there's no sensible default**: File handles, network connections -- **Don't implement Clone if cloning is expensive**: Large data structures (consider `Rc<T>` instead) - -### Summary: Trait Benefits - -| Trait | Benefit | When to Use | -|-------|---------|-------------| -| `Debug` | `println!("{:?}", value)` | Always (except rare cases) | -| `Display` | `println!("{}", value)` | User-facing types | -| `Clone` | `value.clone()` | When explicit duplication makes sense | -| `Copy` | Implicit duplication | Small, simple types | -| `PartialEq` | `==` and `!=` operators | Most types | -| `Eq` | Reflexive equality | When equality is mathematically sound | -| `PartialOrd` | `<`, `>`, `<=`, `>=` | Types with natural ordering | -| `Ord` | `sort()`, `BinaryHeap` | When ordering is total | -| `Hash` | `HashMap` keys | Types used as map keys | -| `Default` | `Default::default()` | Types with obvious defaults | -| `From/Into` | Convenient conversions | Common type conversions | -| `TryFrom/TryInto` | Fallible conversions | Conversions that can fail | +### Когда НЕ реализовывать трейты + +- **Не реализуйте Copy для типов с данными в куче**: `String`, `Vec`, `HashMap` и т. д. +- **Не реализуйте Eq, если значения могут быть NaN**: типы, содержащие `f32`/`f64` +- **Не реализуйте Default, если нет разумного значения по умолчанию**: файловые дескрипторы, сетевые соединения +- **Не реализуйте Clone, если клонирование дорогое**: большие структуры данных (вместо этого рассмотрите `Rc<T>`) + +### Итоги: преимущества трейтов + +| Трейт | Преимущество | Когда использовать | +|-------|-----------|-------------| +| `Debug` | `println!("{:?}", value)` | Всегда (кроме редких случаев) | +| `Display` | `println!("{}", value)` | Типы для пользователя | +| `Clone` | `value.clone()` | Когда явное дублирование имеет смысл | +| `Copy` | Неявное дублирование | Малые простые типы | +| `PartialEq` | Операторы `==` и `!=` | Большинство типов | +| `Eq` | Рефлексивное равенство | Когда равенство математически корректно | +| `PartialOrd` | `<`, `>`, `<=`, `>=` | Типы с естественным порядком | +| `Ord` | `sort()`, `BinaryHeap` | Когда порядок полный | +| `Hash` | Ключи `HashMap` | Типы, используемые как ключи словаря | +| `Default` | `Default::default()` | Типы с очевидными значениями по умолчанию | +| `From/Into` | Удобные преобразования | Частые преобразования типов | +| `TryFrom/TryInto` | Преобразования с возможной ошибкой | Преобразования, которые могут завершиться неудачей | ---- ---- - diff --git a/c-cpp-book/src/ch18-cpp-rust-semantic-deep-dives.md b/c-cpp-book/src/ch18-cpp-rust-semantic-deep-dives.md index 1d896f1..628cc2d 100644 --- a/c-cpp-book/src/ch18-cpp-rust-semantic-deep-dives.md +++ b/c-cpp-book/src/ch18-cpp-rust-semantic-deep-dives.md @@ -1,41 +1,40 @@ -## C++ → Rust Semantic Deep Dives +## Семантические различия C++ → Rust: углублённый разбор -> **What you'll learn:** Detailed mappings for C++ concepts that don't have obvious Rust equivalents — the four named casts, SFINAE vs trait bounds, CRTP vs associated types, and other common friction points during translation. +> **Что вы узнаете:** подробное соответствие концепций C++, у которых нет очевидного аналога в Rust, — четыре именованных приведения, SFINAE и ограничения трейтами, CRTP и ассоциированные типы и другие частые трудности при переводе. -The sections below map C++ concepts that don't have an obvious 1:1 Rust -equivalent. These differences frequently trip up C++ programmers during -translation work. +Ниже описаны концепции C++, для которых нет очевидного взаимно однозначного +соответствия в Rust. Эти различия часто мешают программистам C++ при переводе кода. -### Casting Hierarchy: Four C++ Casts → Rust Equivalents +### Иерархия приведений: четыре приведения C++ → эквиваленты в Rust -C++ has four named casts. Rust replaces them with different, more explicit mechanisms: +В C++ есть четыре именованных приведения. Rust заменяет их другими, более явными механизмами: ```cpp -// C++ casting hierarchy -int i = static_cast<int>(3.14); // 1. Numeric / up-cast -Derived* d = dynamic_cast<Derived*>(base); // 2. Runtime downcasting -int* p = const_cast<int*>(cp); // 3. Cast away const -auto* raw = reinterpret_cast<char*>(&obj); // 4. Bit-level reinterpretation +// Иерархия приведений C++ +int i = static_cast<int>(3.14); // 1. Числовое / приведение вверх +Derived* d = dynamic_cast<Derived*>(base); // 2. Приведение вниз во время выполнения +int* p = const_cast<int*>(cp); // 3. Снятие const +auto* raw = reinterpret_cast<char*>(&obj); // 4. Переинтерпретация на уровне битов ``` -| C++ Cast | Rust Equivalent | Safety | Notes | +| Приведение C++ | Эквивалент в Rust | Безопасность | Примечания | |----------|----------------|--------|-------| -| `static_cast` (numeric) | `as` keyword | Safe but can truncate/wrap | `let i = 3.14_f64 as i32;` — truncates to 3 | -| `static_cast` (numeric, checked) | `From`/`Into` | Safe, compile-time verified | `let i: i32 = 42_u8.into();` — only widens | -| `static_cast` (numeric, fallible) | `TryFrom`/`TryInto` | Safe, returns `Result` | `let i: u8 = 300_u16.try_into()?;` — returns Err | -| `dynamic_cast` (downcast) | `match` on enum / `Any::downcast_ref` | Safe | Pattern matching for enums; `Any` for trait objects | -| `const_cast` | No equivalent | | Rust has no way to cast away `&` → `&mut` in safe code. Use `Cell`/`RefCell` for interior mutability | -| `reinterpret_cast` | `std::mem::transmute` | **`unsafe`** | Reinterprets bit pattern. Almost always wrong — prefer `from_le_bytes()` etc. | +| `static_cast` (числовое) | Ключевое слово `as` | Безопасно, но может обрезать/переполнить | `let i = 3.14_f64 as i32;` — обрезает до 3 | +| `static_cast` (числовое, проверяемое) | `From`/`Into` | Безопасно, проверяется на этапе компиляции | `let i: i32 = 42_u8.into();` — только расширение | +| `static_cast` (числовое, с возможной ошибкой) | `TryFrom`/`TryInto` | Безопасно, возвращает `Result` | `let i: u8 = 300_u16.try_into()?;` — вернёт Err | +| `dynamic_cast` (приведение вниз) | `match` по перечислению / `Any::downcast_ref` | Безопасно | Сопоставление с образцом для перечислений; `Any` для трейт-объектов | +| `const_cast` | Эквивалента нет | | В безопасном Rust нет способа превратить `&` в `&mut`. Для внутренней изменяемости используйте `Cell`/`RefCell` | +| `reinterpret_cast` | `std::mem::transmute` | **`unsafe`** | Переинтерпретирует битовый шаблон. Почти всегда ошибочно — предпочитайте `from_le_bytes()` и т. п. | ```rust -// Rust equivalents: +// Эквиваленты в Rust: -// 1. Numeric casts — prefer From/Into over `as` -let widened: u32 = 42_u8.into(); // Infallible widening — always prefer -let truncated = 300_u16 as u8; // ⚠ Wraps to 44! Silent data loss -let checked: Result<u8, _> = 300_u16.try_into(); // Err — safe fallible conversion +// 1. Числовые приведения — предпочитайте From/Into вместо `as` +let widened: u32 = 42_u8.into(); // Расширение без ошибок — всегда предпочтительно +let truncated = 300_u16 as u8; // ⚠ Переполняется до 44! Тихая потеря данных +let checked: Result<u8, _> = 300_u16.try_into(); // Err — безопасное преобразование с возможной ошибкой -// 2. Downcast: enum (preferred) or Any (when needed for type erasure) +// 2. Приведение вниз: перечисление (предпочтительно) или Any (когда нужно стирание типа) use std::any::Any; fn handle_any(val: &dyn Any) { @@ -46,58 +45,54 @@ fn handle_any(val: &dyn Any) { } } -// 3. "const_cast" → interior mutability (no unsafe needed) +// 3. «const_cast» → внутренняя изменяемость (unsafe не нужен) use std::cell::Cell; struct Sensor { - read_count: Cell<u32>, // Mutate through &self + read_count: Cell<u32>, // Изменяем через &self } impl Sensor { fn read(&self) -> f64 { - self.read_count.set(self.read_count.get() + 1); // &self, not &mut self + self.read_count.set(self.read_count.get() + 1); // &self, а не &mut self 42.0 } } -// 4. reinterpret_cast → transmute (almost never needed) -// Prefer safe alternatives: -let bytes: [u8; 4] = 0x12345678_u32.to_ne_bytes(); // ✅ Safe -let val = u32::from_ne_bytes(bytes); // ✅ Safe -// unsafe { std::mem::transmute::<u32, [u8; 4]>(val) } // ❌ Avoid +// 4. reinterpret_cast → transmute (почти никогда не нужно) +// Предпочитайте безопасные альтернативы: +let bytes: [u8; 4] = 0x12345678_u32.to_ne_bytes(); // ✅ Безопасно +let val = u32::from_ne_bytes(bytes); // ✅ Безопасно +// unsafe { std::mem::transmute::<u32, [u8; 4]>(val) } // ❌ Избегайте ``` -> **Guideline**: In idiomatic Rust, `as` should be rare (use `From`/`Into` -> for widening, `TryFrom`/`TryInto` for narrowing), `transmute` should be -> exceptional, and `const_cast` has no equivalent because interior mutability -> types make it unnecessary. +> **Правило**: в идиоматичном Rust `as` должен встречаться редко (для расширения используйте `From`/`Into`, для сужения — `TryFrom`/`TryInto`), `transmute` — исключительный случай, а у `const_cast` нет эквивалента, потому что типы с внутренней изменяемостью делают его ненужным. --- -### Preprocessor → `cfg`, Feature Flags, and `macro_rules!` +### Препроцессор → `cfg`, флаги функций и `macro_rules!` -C++ relies heavily on the preprocessor for conditional compilation, constants, and -code generation. Rust replaces all of these with first-class language features. +C++ сильно опирается на препроцессор для условной компиляции, констант и генерации кода. Rust заменяет всё это полноценными возможностями языка. -#### `#define` constants → `const` or `const fn` +#### `#define`-константы → `const` или `const fn` ```cpp // C++ #define MAX_RETRIES 5 #define BUFFER_SIZE (1024 * 64) -#define SQUARE(x) ((x) * (x)) // Macro — textual substitution, no type safety +#define SQUARE(x) ((x) * (x)) // Макрос — текстовая подстановка, без проверки типов ``` ```rust -// Rust — type-safe, scoped, no textual substitution +// Rust — с проверкой типов, с областью видимости, без текстовой подстановки const MAX_RETRIES: u32 = 5; const BUFFER_SIZE: usize = 1024 * 64; -const fn square(x: u32) -> u32 { x * x } // Evaluated at compile time +const fn square(x: u32) -> u32 { x * x } // Вычисляется на этапе компиляции -// Can be used in const contexts: -const AREA: u32 = square(12); // Computed at compile time +// Можно использовать в константных контекстах: +const AREA: u32 = square(12); // Вычисляется на этапе компиляции static BUFFER: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; ``` -#### `#ifdef` / `#if` → `#[cfg()]` and `cfg!()` +#### `#ifdef` / `#if` → `#[cfg()]` и `cfg!()` ```cpp // C++ @@ -113,39 +108,39 @@ static BUFFER: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; ``` ```rust -// Rust — attribute-based conditional compilation +// Rust — условная компиляция на основе атрибутов #[cfg(debug_assertions)] fn log_verbose(msg: &str) { eprintln!("[VERBOSE] {msg}"); } #[cfg(not(debug_assertions))] -fn log_verbose(_msg: &str) { /* compiled away in release */ } +fn log_verbose(_msg: &str) { /* удаляется в релизной сборке */ } -// Combine conditions: +// Комбинирование условий: #[cfg(all(target_os = "linux", target_arch = "x86_64"))] fn use_x86_path() { /* ... */ } #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] fn use_generic_path() { /* ... */ } -// Runtime check (condition is still compile-time, but usable in expressions): +// Проверка во время выполнения (условие всё равно вычисляется на этапе компиляции, но его можно использовать в выражениях): if cfg!(target_os = "windows") { println!("Running on Windows"); } ``` -#### Feature flags in `Cargo.toml` +#### Флаги функций в `Cargo.toml` ```toml -# Cargo.toml — replace #ifdef FEATURE_FOO +# Cargo.toml — замена #ifdef FEATURE_FOO [features] default = ["json"] -json = ["dep:serde_json"] # Optional dependency -verbose-logging = [] # Flag with no extra dependency -gpu-support = ["dep:cuda-sys"] # Optional GPU support +json = ["dep:serde_json"] # Необязательная зависимость +verbose-logging = [] # Флаг без дополнительной зависимости +gpu-support = ["dep:cuda-sys"] # Необязательная поддержка GPU ``` ```rust -// Conditional code based on feature flags: +// Условный код на основе флагов функций: #[cfg(feature = "json")] pub fn parse_config(data: &str) -> Result<Config, Error> { serde_json::from_str(data).map_err(Error::from) @@ -157,20 +152,20 @@ macro_rules! verbose { } #[cfg(not(feature = "verbose-logging"))] macro_rules! verbose { - ($($arg:tt)*) => { }; // Compiles to nothing + ($($arg:tt)*) => { }; // Компилируется в ничего } ``` #### `#define MACRO(x)` → `macro_rules!` ```cpp -// C++ — textual substitution, notoriously error-prone +// C++ — текстовая подстановка, печально известная своей подверженностью ошибкам #define DIAG_CHECK(cond, msg) \ do { if (!(cond)) { log_error(msg); return false; } } while(0) ``` ```rust -// Rust — hygienic, type-checked, operates on syntax tree +// Rust — гигиеничный, с проверкой типов, работает с синтаксическим деревом macro_rules! diag_check { ($cond:expr, $msg:expr) => { if !($cond) { @@ -187,23 +182,23 @@ fn run_test() -> Result<(), DiagError> { } ``` -| C++ Preprocessor | Rust Equivalent | Advantage | +| Препроцессор C++ | Эквивалент в Rust | Преимущество | |-----------------|----------------|-----------| -| `#define PI 3.14` | `const PI: f64 = 3.14;` | Typed, scoped, visible to debugger | -| `#define MAX(a,b) ((a)>(b)?(a):(b))` | `macro_rules!` or generic `fn max<T: Ord>` | No double-evaluation bugs | -| `#ifdef DEBUG` | `#[cfg(debug_assertions)]` | Checked by compiler, no typo risk | -| `#ifdef FEATURE_X` | `#[cfg(feature = "x")]` | Cargo manages features; dependency-aware | -| `#include "header.h"` | `mod module;` + `use module::Item;` | No include guards, no circular includes | -| `#pragma once` | Not needed | Each `.rs` file is a module — included exactly once | +| `#define PI 3.14` | `const PI: f64 = 3.14;` | С типом, с областью видимости, видна отладчику | +| `#define MAX(a,b) ((a)>(b)?(a):(b))` | `macro_rules!` или обобщённая `fn max<T: Ord>` | Нет ошибок двойного вычисления | +| `#ifdef DEBUG` | `#[cfg(debug_assertions)]` | Проверяется компилятором, нет риска опечатки | +| `#ifdef FEATURE_X` | `#[cfg(feature = "x")]` | Cargo управляет флагами; учитывает зависимости | +| `#include "header.h"` | `mod module;` + `use module::Item;` | Нет защиты от повторного включения, нет циклических включений | +| `#pragma once` | Не нужно | Каждый файл `.rs` — модуль, подключается ровно один раз | --- -### Header Files and `#include` → Modules and `use` +### Заголовочные файлы и `#include` → модули и `use` -In C++, the compilation model revolves around textual inclusion: +В C++ модель компиляции основана на текстовом включении: ```cpp -// widget.h — every translation unit that uses Widget includes this +// widget.h — каждая единица трансляции, использующая Widget, включает этот файл #pragma once #include <string> #include <vector> @@ -219,18 +214,18 @@ private: ``` ```cpp -// widget.cpp — separate definition +// widget.cpp — отдельное определение #include "widget.h" Widget::Widget(std::string name) : name_(std::move(name)) {} void Widget::activate() { /* ... */ } ``` -In Rust, there are **no header files, no forward declarations, no include guards**: +В Rust **нет заголовочных файлов, предварительных объявлений и защиты от повторного включения**: ```rust -// src/widget.rs — declaration AND definition in one file +// src/widget.rs — объявление И определение в одном файле pub struct Widget { - name: String, // Private by default + name: String, // По умолчанию приватное data: Vec<i32>, } @@ -243,8 +238,8 @@ impl Widget { ``` ```rust -// src/main.rs — import by module path -mod widget; // Tells compiler to include src/widget.rs +// src/main.rs — импорт по пути модуля +mod widget; // Сообщает компилятору, что нужно включить src/widget.rs use widget::Widget; fn main() { @@ -253,27 +248,27 @@ fn main() { } ``` -| C++ | Rust | Why it's better | +| C++ | Rust | Почему лучше | |-----|------|-----------------| -| `#include "foo.h"` | `mod foo;` in parent + `use foo::Item;` | No textual inclusion, no ODR violations | -| `#pragma once` / include guards | Not needed | Each `.rs` file is a module — compiled once | -| Forward declarations | Not needed | Compiler sees entire crate; order doesn't matter | -| `class Foo;` (incomplete type) | Not needed | No separate declaration/definition split | -| `.h` + `.cpp` for each class | Single `.rs` file | No declaration/definition mismatch bugs | -| `using namespace std;` | `use std::collections::HashMap;` | Always explicit — no global namespace pollution | -| Nested `namespace a::b` | Nested `mod a { mod b { } }` or `a/b.rs` | File system mirrors module tree | +| `#include "foo.h"` | `mod foo;` в родителе + `use foo::Item;` | Нет текстового включения, нет нарушений ODR | +| `#pragma once` / защитные макросы | Не нужно | Каждый файл `.rs` — модуль, компилируется один раз | +| Предварительные объявления | Не нужны | Компилятор видит весь крейт; порядок не важен | +| `class Foo;` (неполный тип) | Не нужно | Нет разделения на объявление и определение | +| `.h` + `.cpp` для каждого класса | Один файл `.rs` | Нет ошибок рассогласования объявления и определения | +| `using namespace std;` | `use std::collections::HashMap;` | Всегда явно — нет загрязнения глобального пространства имён | +| Вложенные `namespace a::b` | Вложенные `mod a { mod b { } }` или `a/b.rs` | Структура файловой системы повторяет дерево модулей | --- -### `friend` and Access Control → Module Visibility +### `friend` и контроль доступа → видимость модулей -C++ uses `friend` to grant specific classes or functions access to private members. -Rust has no `friend` keyword — instead, **privacy is module-scoped**: +В C++ `friend` даёт определённым классам или функциям доступ к приватным членам. +В Rust нет ключевого слова `friend` — вместо этого **приватность ограничена модулем**: ```cpp // C++ class Engine { - friend class Car; // Car can access private members + friend class Car; // Car может обращаться к приватным членам int rpm_; void set_rpm(int r) { rpm_ = r; } public: @@ -282,10 +277,10 @@ public: ``` ```rust -// Rust — items in the same module can access all fields, no `friend` needed +// Rust — элементы одного модуля могут обращаться ко всем полям, `friend` не нужен mod vehicle { pub struct Engine { - rpm: u32, // Private to the module (not to the struct!) + rpm: u32, // Приватно для модуля (а не для структуры!) } impl Engine { @@ -300,10 +295,10 @@ mod vehicle { impl Car { pub fn new() -> Self { Car { engine: Engine::new() } } pub fn accelerate(&mut self) { - self.engine.rpm = 3000; // ✅ Same module — direct field access + self.engine.rpm = 3000; // ✅ Тот же модуль — прямой доступ к полю } pub fn rpm(&self) -> u32 { - self.engine.rpm // ✅ Same module — can read private field + self.engine.rpm // ✅ Тот же модуль — можно читать приватное поле } } } @@ -311,127 +306,122 @@ mod vehicle { fn main() { let mut car = vehicle::Car::new(); car.accelerate(); - // car.engine.rpm = 9000; // ❌ Compile error: `engine` is private - println!("RPM: {}", car.rpm()); // ✅ Public method on Car + // car.engine.rpm = 9000; // ❌ Ошибка компиляции: `engine` приватно + println!("RPM: {}", car.rpm()); // ✅ Публичный метод Car } ``` -| C++ Access | Rust Equivalent | Scope | +| Доступ в C++ | Эквивалент в Rust | Область действия | |-----------|----------------|-------| -| `private` | (default, no keyword) | Accessible within the same module only | -| `protected` | No direct equivalent | Use `pub(super)` for parent module access | -| `public` | `pub` | Accessible everywhere | -| `friend class Foo` | Put `Foo` in the same module | Module-level privacy replaces friend | -| — | `pub(crate)` | Visible within the crate but not to external dependents | -| — | `pub(super)` | Visible to the parent module only | -| — | `pub(in crate::path)` | Visible within a specific module subtree | - -> **Key insight**: C++ privacy is per-class. Rust privacy is per-module. -> This means you control access by choosing which types live in the same module — -> colocated types have full access to each other's private fields. +| `private` | (по умолчанию, без ключевого слова) | Доступно только внутри того же модуля | +| `protected` | Прямого аналога нет | Используйте `pub(super)` для доступа из родительского модуля | +| `public` | `pub` | Доступно везде | +| `friend class Foo` | Поместить `Foo` в тот же модуль | Приватность на уровне модуля заменяет friend | +| — | `pub(crate)` | Видно внутри крейта, но не внешним зависимостям | +| — | `pub(super)` | Видно только родительскому модулю | +| — | `pub(in crate::path)` | Видно внутри конкретного поддерева модулей | + +> **Ключевая мысль**: приватность в C++ задаётся на уровне класса. Приватность в Rust — на уровне модуля. Это значит, что вы управляете доступом, выбирая, какие типы находятся в одном модуле — размещённые вместе типы имеют полный доступ к приватным полям друг друга. --- -### `volatile` → Atomics and `read_volatile`/`write_volatile` +### `volatile` → атомарные операции и `read_volatile`/`write_volatile` -In C++, `volatile` tells the compiler not to optimize away reads/writes — typically -used for memory-mapped hardware registers. **Rust has no `volatile` keyword.** +В C++ `volatile` говорит компилятору не оптимизировать чтения и записи — обычно это используется для регистров аппаратуры, отображённых в память. **В Rust нет ключевого слова `volatile`.** ```cpp -// C++: volatile for hardware registers +// C++: volatile для аппаратных регистров volatile uint32_t* const GPIO_REG = reinterpret_cast<volatile uint32_t*>(0x4002'0000); -*GPIO_REG = 0x01; // Write not optimized away -uint32_t val = *GPIO_REG; // Read not optimized away +*GPIO_REG = 0x01; // Запись не оптимизируется +uint32_t val = *GPIO_REG; // Чтение не оптимизируется ``` ```rust -// Rust: explicit volatile operations — only in unsafe code +// Rust: явные volatile-операции — только в unsafe-коде use std::ptr; const GPIO_REG: *mut u32 = 0x4002_0000 as *mut u32; -// SAFETY: GPIO_REG is a valid memory-mapped I/O address. +// SAFETY: GPIO_REG — валидный адрес ввода-вывода, отображённый в память. unsafe { - ptr::write_volatile(GPIO_REG, 0x01); // Write not optimized away - let val = ptr::read_volatile(GPIO_REG); // Read not optimized away + ptr::write_volatile(GPIO_REG, 0x01); // Запись не оптимизируется + let val = ptr::read_volatile(GPIO_REG); // Чтение не оптимизируется } ``` -For **concurrent shared state** (the other common C++ `volatile` use), Rust uses atomics: +Для **совместно используемого состояния в конкурентном коде** (другое частое применение `volatile` в C++) Rust использует атомарные типы: ```cpp -// C++: volatile is NOT sufficient for thread safety (common mistake!) -volatile bool stop_flag = false; // ❌ Data race — UB in C++11+ +// C++: volatile НЕДОСТАТОЧЕН для потокобезопасности (распространённая ошибка!) +volatile bool stop_flag = false; // ❌ Гонка данных — неопределённое поведение в C++11+ -// Correct C++: +// Правильно в C++: std::atomic<bool> stop_flag{false}; ``` ```rust -// Rust: atomics are the only way to share mutable state across threads +// Rust: атомарные типы — единственный способ разделять изменяемое состояние между потоками use std::sync::atomic::{AtomicBool, Ordering}; static STOP_FLAG: AtomicBool = AtomicBool::new(false); -// From another thread: +// Из другого потока: STOP_FLAG.store(true, Ordering::Release); -// Check: +// Проверка: if STOP_FLAG.load(Ordering::Acquire) { println!("Stopping"); } ``` -| C++ Usage | Rust Equivalent | Notes | +| Применение в C++ | Эквивалент в Rust | Примечания | |-----------|----------------|-------| -| `volatile` for hardware registers | `ptr::read_volatile` / `ptr::write_volatile` | Requires `unsafe` — correct for MMIO | -| `volatile` for thread signaling | `AtomicBool` / `AtomicU32` etc. | C++ `volatile` is wrong for this too! | -| `std::atomic<T>` | `std::sync::atomic::AtomicT` | Same semantics, same orderings | -| `std::atomic<T>::load(memory_order_acquire)` | `AtomicT::load(Ordering::Acquire)` | 1:1 mapping | +| `volatile` для аппаратных регистров | `ptr::read_volatile` / `ptr::write_volatile` | Требует `unsafe` — корректно для MMIO | +| `volatile` для сигнализации между потоками | `AtomicBool` / `AtomicU32` и т. д. | `volatile` в C++ для этого тоже неверен! | +| `std::atomic<T>` | `std::sync::atomic::AtomicT` | Та же семантика, те же порядки доступа | +| `std::atomic<T>::load(memory_order_acquire)` | `AtomicT::load(Ordering::Acquire)` | Соответствие 1:1 | --- -### `static` Variables → `static`, `const`, `LazyLock`, `OnceLock` +### Переменные `static` → `static`, `const`, `LazyLock`, `OnceLock` -#### Basic `static` and `const` +#### Базовые `static` и `const` ```cpp // C++ -const int MAX_RETRIES = 5; // Compile-time constant -static std::string CONFIG_PATH = "/etc/app"; // Static init — order undefined! +const int MAX_RETRIES = 5; // Константа времени компиляции +static std::string CONFIG_PATH = "/etc/app"; // Статическая инициализация — порядок не определён! ``` ```rust // Rust -const MAX_RETRIES: u32 = 5; // Compile-time constant, inlined -static CONFIG_PATH: &str = "/etc/app"; // 'static lifetime, fixed address +const MAX_RETRIES: u32 = 5; // Константа времени компиляции, встраивается +static CONFIG_PATH: &str = "/etc/app"; // Время жизни 'static, фиксированный адрес ``` -#### The static initialization order fiasco +#### Печально известная проблема порядка статической инициализации -C++ has a well-known problem: global constructors in different translation units -execute in **unspecified order**. Rust avoids this entirely — `static` values must -be compile-time constants (no constructors). +В C++ есть хорошо известная проблема: глобальные конструкторы в разных единицах трансляции выполняются в **неопределённом порядке**. Rust полностью обходит её — значения `static` должны быть константами времени компиляции (без конструкторов). -For runtime-initialized globals, use `LazyLock` (Rust 1.80+) or `OnceLock`: +Для глобальных переменных, инициализируемых во время выполнения, используйте `LazyLock` (Rust 1.80+) или `OnceLock`: ```rust use std::sync::LazyLock; -// Equivalent to C++ `static std::regex` — initialized on first access, thread-safe +// Аналог C++ `static std::regex` — инициализируется при первом обращении, потокобезопасно static CONFIG_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| { regex::Regex::new(r"^[a-z]+_diag$").expect("invalid regex") }); fn is_valid_diag(name: &str) -> bool { - CONFIG_REGEX.is_match(name) // First call initializes; subsequent calls are fast + CONFIG_REGEX.is_match(name) // Первый вызов инициализирует; последующие работают быстро } ``` ```rust use std::sync::OnceLock; -// OnceLock: initialized once, can be set from runtime data +// OnceLock: инициализируется один раз, может быть задан из данных времени выполнения static DB_CONN: OnceLock<String> = OnceLock::new(); fn init_db(connection_string: &str) { @@ -444,28 +434,28 @@ fn get_db() -> &'static str { } ``` -| C++ | Rust | Notes | +| C++ | Rust | Примечания | |-----|------|-------| -| `const int X = 5;` | `const X: i32 = 5;` | Both compile-time. Rust requires type annotation | -| `constexpr int X = 5;` | `const X: i32 = 5;` | Rust `const` is always constexpr | -| `static int count = 0;` (file scope) | `static COUNT: AtomicI32 = AtomicI32::new(0);` | Mutable statics require `unsafe` or atomics | -| `static std::string s = "hi";` | `static S: &str = "hi";` or `LazyLock<String>` | No runtime constructor for simple cases | -| `static MyObj obj;` (complex init) | `static OBJ: LazyLock<MyObj> = LazyLock::new(\|\| { ... });` | Thread-safe, lazy, no init order issues | -| `thread_local` | `thread_local! { static X: Cell<u32> = Cell::new(0); }` | Same semantics | +| `const int X = 5;` | `const X: i32 = 5;` | Оба — на этапе компиляции. В Rust нужна аннотация типа | +| `constexpr int X = 5;` | `const X: i32 = 5;` | `const` в Rust всегда constexpr | +| `static int count = 0;` (на уровне файла) | `static COUNT: AtomicI32 = AtomicI32::new(0);` | Изменяемые статики требуют `unsafe` или атомарных типов | +| `static std::string s = "hi";` | `static S: &str = "hi";` или `LazyLock<String>` | Для простых случаев нет конструктора времени выполнения | +| `static MyObj obj;` (сложная инициализация) | `static OBJ: LazyLock<MyObj> = LazyLock::new(\|\| { ... });` | Потокобезопасно, ленивая инициализация, нет проблем с порядком | +| `thread_local` | `thread_local! { static X: Cell<u32> = Cell::new(0); }` | Та же семантика | --- ### `constexpr` → `const fn` -C++ `constexpr` marks functions and variables for compile-time evaluation. Rust -uses `const fn` and `const` for the same purpose: +`constexpr` в C++ помечает функции и переменные для вычисления на этапе компиляции. Rust +использует `const fn` и `const` для той же цели: ```cpp // C++ constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } -constexpr int val = factorial(5); // Computed at compile time → 120 +constexpr int val = factorial(5); // Вычисляется на этапе компиляции → 120 ``` ```rust @@ -473,37 +463,37 @@ constexpr int val = factorial(5); // Computed at compile time → 120 const fn factorial(n: u32) -> u32 { if n <= 1 { 1 } else { n * factorial(n - 1) } } -const VAL: u32 = factorial(5); // Computed at compile time → 120 +const VAL: u32 = factorial(5); // Вычисляется на этапе компиляции → 120 -// Also works in array sizes and match patterns: +// Также работает в размерах массивов и в шаблонах match: const LOOKUP: [u32; 5] = [factorial(1), factorial(2), factorial(3), factorial(4), factorial(5)]; ``` -| C++ | Rust | Notes | +| C++ | Rust | Примечания | |-----|------|-------| -| `constexpr int f()` | `const fn f() -> i32` | Same intent — compile-time evaluable | -| `constexpr` variable | `const` variable | Rust `const` is always compile-time | -| `consteval` (C++20) | No equivalent | `const fn` can also run at runtime | -| `if constexpr` (C++17) | No equivalent (use `cfg!` or generics) | Trait specialization fills some use cases | -| `constinit` (C++20) | `static` with const initializer | Rust `static` must be const-initialized by default | - -> **Current limitations of `const fn`** (stabilized as of Rust 1.82): -> - No trait methods (can't call `.len()` on a `Vec` in const context) -> - No heap allocation (`Box::new`, `Vec::new` not const) -> - ~~No floating-point arithmetic~~ — **stabilized in Rust 1.82** -> - Can't use `for` loops (use recursion or `while` with manual index) +| `constexpr int f()` | `const fn f() -> i32` | Та же цель — вычисляемая на этапе компиляции | +| Переменная `constexpr` | Переменная `const` | `const` в Rust всегда вычисляется на этапе компиляции | +| `consteval` (C++20) | Эквивалента нет | `const fn` может выполняться и во время выполнения | +| `if constexpr` (C++17) | Эквивалента нет (используйте `cfg!` или обобщения) | Специализация трейтов покрывает часть случаев | +| `constinit` (C++20) | `static` с константным инициализатором | `static` в Rust по умолчанию должен инициализироваться константой | + +> **Текущие ограничения `const fn`** (стабилизировано в Rust 1.82): +> - Нельзя вызывать методы трейтов (нельзя вызвать `.len()` у `Vec` в константном контексте) +> - Нет выделения памяти в куче (`Box::new`, `Vec::new` не являются `const`) +> - ~~Нет арифметики с плавающей точкой~~ — **стабилизирована в Rust 1.82** +> - Нельзя использовать циклы `for` (используйте рекурсию или `while` с ручным индексом) --- -### SFINAE and `enable_if` → Trait Bounds and `where` Clauses +### SFINAE и `enable_if` → ограничения трейтами и предложения `where` -In C++, SFINAE (Substitution Failure Is Not An Error) is the mechanism behind -conditional generic programming. It is powerful but notoriously unreadable. Rust -replaces it entirely with **trait bounds**: +В C++ SFINAE (Substitution Failure Is Not An Error, «неудача подстановки — не ошибка») — механизм +условного обобщённого программирования. Он мощен, но печально известен нечитаемостью. Rust +полностью заменяет его **ограничениями трейтами** (trait bounds): ```cpp -// C++: SFINAE-based conditional function (pre-C++20) +// C++: условная функция на основе SFINAE (до C++20) template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0> T double_it(T val) { return val * 2; } @@ -512,20 +502,20 @@ template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0> T double_it(T val) { return val * 2.0; } -// C++20 concepts — cleaner but still verbose: +// Концепты C++20 — чище, но всё ещё многословны: template<std::integral T> T double_it(T val) { return val * 2; } ``` ```rust -// Rust: trait bounds — readable, composable, excellent error messages +// Rust: ограничения трейтами — читаемо, компонуемо, отличные сообщения об ошибках use std::ops::Mul; fn double_it<T: Mul<Output = T> + From<u8>>(val: T) -> T { val * T::from(2) } -// Or with where clause for complex bounds: +// Или с предложением where для сложных ограничений: fn process<T>(val: T) -> String where T: std::fmt::Display + Clone + Send, @@ -533,7 +523,7 @@ where format!("Processing: {}", val) } -// Conditional behavior via separate impls (replaces SFINAE overloads): +// Условное поведение через отдельные реализации (замена перегрузок на основе SFINAE): trait Describable { fn describe(&self) -> String; } @@ -547,30 +537,27 @@ impl Describable for f64 { } ``` -| C++ Template Metaprogramming | Rust Equivalent | Readability | +| Шаблонное метапрограммирование C++ | Эквивалент в Rust | Читаемость | |-----------------------------|----------------|-------------| -| `std::enable_if_t<cond>` | `where T: Trait` | 🟢 Clear English | -| `std::is_integral_v<T>` | Bound on a numeric trait or specific types | 🟢 No `_v` / `_t` suffixes | -| SFINAE overload sets | Separate `impl Trait for ConcreteType` blocks | 🟢 Each impl stands alone | -| `if constexpr (std::is_same_v<T, int>)` | Specialization via trait impls | 🟢 Compile-time dispatched | -| C++20 `concept` | `trait` | 🟢 Nearly identical intent | -| `requires` clause | `where` clause | 🟢 Same position, similar syntax | -| Compilation fails deep inside template | Compilation fails at the call site with trait mismatch | 🟢 No 200-line error cascades | - -> **Key insight**: C++ concepts (C++20) are the closest thing to Rust traits. -> If you're familiar with C++20 concepts, think of Rust traits as concepts -> that have been a first-class language feature since 1.0, with a coherent -> implementation model (trait impls) instead of duck typing. +| `std::enable_if_t<cond>` | `where T: Trait` | 🟢 Понятно, как обычный английский | +| `std::is_integral_v<T>` | Ограничение числовым трейтом или конкретными типами | 🟢 Без суффиксов `_v` / `_t` | +| Наборы перегрузок SFINAE | Отдельные блоки `impl Trait for ConcreteType` | 🟢 Каждая реализация самостоятельна | +| `if constexpr (std::is_same_v<T, int>)` | Специализация через реализации трейтов | 🟢 Диспетчеризация на этапе компиляции | +| Концепт C++20 | `trait` | 🟢 Почти одинаковый смысл | +| Клауза `requires` | Предложение `where` | 🟢 Та же позиция, похожий синтаксис | +| Компиляция падает глубоко внутри шаблона | Компиляция падает в месте вызова с несоответствием трейта | 🟢 Без каскадов ошибок на 200 строк | + +> **Ключевая мысль**: концепты C++20 — самое близкое к трейтам Rust. Если вы знакомы с концептами C++20, думайте о трейтах Rust как о концептах, которые являются полноценной возможностью языка с версии 1.0 и имеют согласованную модель реализации (реализации трейтов), а не утиной типизации. --- -### `std::function` → Function Pointers, `impl Fn`, and `Box<dyn Fn>` +### `std::function` → указатели на функции, `impl Fn` и `Box<dyn Fn>` -C++ `std::function<R(Args...)>` is a type-erased callable. Rust has three options, -each with different trade-offs: +`std::function<R(Args...)>` в C++ — это вызываемый объект со стёртым типом. В Rust есть три варианта +с разными компромиссами: ```cpp -// C++: one-size-fits-all (heap-allocated, type-erased) +// C++: универсальное решение (в куче, со стёртым типом) #include <functional> std::function<int(int)> make_adder(int n) { return [n](int x) { return x + n; }; @@ -578,24 +565,24 @@ std::function<int(int)> make_adder(int n) { ``` ```rust -// Rust Option 1: fn pointer — simple, no captures, no allocation +// Вариант 1 в Rust: указатель на функцию — просто, без захватов, без выделения памяти fn add_one(x: i32) -> i32 { x + 1 } let f: fn(i32) -> i32 = add_one; println!("{}", f(5)); // 6 -// Rust Option 2: impl Fn — monomorphized, zero overhead, can capture +// Вариант 2 в Rust: impl Fn — мономорфизация, без накладных расходов, может захватывать fn apply(val: i32, f: impl Fn(i32) -> i32) -> i32 { f(val) } let n = 10; -let result = apply(5, |x| x + n); // Closure captures `n` +let result = apply(5, |x| x + n); // Замыкание захватывает `n` -// Rust Option 3: Box<dyn Fn> — type-erased, heap-allocated (like std::function) +// Вариант 3 в Rust: Box<dyn Fn> — стирание типа, выделение в куче (как std::function) fn make_adder(n: i32) -> Box<dyn Fn(i32) -> i32> { Box::new(move |x| x + n) } let adder = make_adder(10); println!("{}", adder(5)); // 15 -// Storing heterogeneous callables (like vector<function<int(int)>>): +// Хранение разнородных вызываемых объектов (как vector<function<int(int)>>): let callbacks: Vec<Box<dyn Fn(i32) -> i32>> = vec![ Box::new(|x| x + 1), Box::new(|x| x * 2), @@ -606,86 +593,86 @@ for cb in &callbacks { } ``` -| When to use | C++ Equivalent | Rust Choice | +| Когда использовать | Аналог в C++ | Выбор в Rust | |------------|---------------|-------------| -| Top-level function, no captures | Function pointer | `fn(Args) -> Ret` | -| Generic function accepting callables | Template parameter | `impl Fn(Args) -> Ret` (static dispatch) | -| Trait bound in generics | `template<typename F>` | `F: Fn(Args) -> Ret` | -| Stored callable, type-erased | `std::function<R(Args)>` | `Box<dyn Fn(Args) -> Ret>` | -| Callback that mutates state | `std::function` with mutable lambda | `Box<dyn FnMut(Args) -> Ret>` | -| One-shot callback (consumed) | `std::function` (moved) | `Box<dyn FnOnce(Args) -> Ret>` | +| Функция верхнего уровня без захватов | Указатель на функцию | `fn(Args) -> Ret` | +| Обобщённая функция, принимающая вызываемые объекты | Параметр шаблона | `impl Fn(Args) -> Ret` (статическая диспетчеризация) | +| Ограничение трейтом в обобщениях | `template<typename F>` | `F: Fn(Args) -> Ret` | +| Хранимый вызываемый объект со стёртым типом | `std::function<R(Args)>` | `Box<dyn Fn(Args) -> Ret>` | +| Обратный вызов, изменяющий состояние | `std::function` с изменяемой лямбдой | `Box<dyn FnMut(Args) -> Ret>` | +| Одноразовый обратный вызов (потребляется) | `std::function` (перемещённый) | `Box<dyn FnOnce(Args) -> Ret>` | -> **Performance note**: `impl Fn` has zero overhead (monomorphized, like a C++ template). -> `Box<dyn Fn>` has the same overhead as `std::function` (vtable + heap allocation). -> Prefer `impl Fn` unless you need to store heterogeneous callables. +> **Замечание о производительности**: `impl Fn` не имеет накладных расходов (мономорфизация, как шаблон C++). +> `Box<dyn Fn>` имеет те же накладные расходы, что и `std::function` (vtable и выделение в куче). +> Предпочитайте `impl Fn`, если не нужно хранить разнородные вызываемые объекты. --- -### Container Mapping: C++ STL → Rust `std::collections` +### Соответствие контейнеров: STL C++ → `std::collections` Rust -| C++ STL Container | Rust Equivalent | Notes | +| Контейнер STL C++ | Эквивалент в Rust | Примечания | |------------------|----------------|-------| -| `std::vector<T>` | `Vec<T>` | Nearly identical API. Rust checks bounds by default | -| `std::array<T, N>` | `[T; N]` | Stack-allocated fixed-size array | -| `std::deque<T>` | `std::collections::VecDeque<T>` | Ring buffer. Efficient push/pop at both ends | -| `std::list<T>` | `std::collections::LinkedList<T>` | Rarely used in Rust — `Vec` is almost always faster | -| `std::forward_list<T>` | No equivalent | Use `Vec` or `VecDeque` | -| `std::unordered_map<K, V>` | `std::collections::HashMap<K, V>` | Uses `SipHash` by default (DoS-resistant) | -| `std::map<K, V>` | `std::collections::BTreeMap<K, V>` | B-tree; keys sorted; `K: Ord` required | -| `std::unordered_set<T>` | `std::collections::HashSet<T>` | `T: Hash + Eq` required | -| `std::set<T>` | `std::collections::BTreeSet<T>` | Sorted set; `T: Ord` required | -| `std::priority_queue<T>` | `std::collections::BinaryHeap<T>` | Max-heap by default (same as C++) | -| `std::stack<T>` | `Vec<T>` with `.push()` / `.pop()` | No separate stack type needed | -| `std::queue<T>` | `VecDeque<T>` with `.push_back()` / `.pop_front()` | No separate queue type needed | -| `std::string` | `String` | UTF-8 guaranteed, not null-terminated | -| `std::string_view` | `&str` | Borrowed UTF-8 slice | -| `std::span<T>` (C++20) | `&[T]` / `&mut [T]` | Rust slices have been a first-class type since 1.0 | -| `std::tuple<A, B, C>` | `(A, B, C)` | First-class syntax, destructurable | -| `std::pair<A, B>` | `(A, B)` | Just a 2-element tuple | -| `std::bitset<N>` | No std equivalent | Use the `bitvec` crate or `[u8; N/8]` | - -**Key differences**: -- Rust's `HashMap`/`HashSet` require `K: Hash + Eq` — the compiler enforces this at the type level, unlike C++ where using an unhashable key gives a template error deep in the STL -- `Vec` indexing (`v[i]`) panics on out-of-bounds by default. Use `.get(i)` for `Option<&T>` or iterators to avoid bounds checks entirely -- No `std::multimap` or `std::multiset` — use `HashMap<K, Vec<V>>` or `BTreeMap<K, Vec<V>>` +| `std::vector<T>` | `Vec<T>` | Почти идентичный API. Rust проверяет границы по умолчанию | +| `std::array<T, N>` | `[T; N]` | Массив фиксированного размера на стеке | +| `std::deque<T>` | `std::collections::VecDeque<T>` | Кольцевой буфер. Эффективные push/pop с обоих концов | +| `std::list<T>` | `std::collections::LinkedList<T>` | В Rust используется редко — `Vec` почти всегда быстрее | +| `std::forward_list<T>` | Эквивалента нет | Используйте `Vec` или `VecDeque` | +| `std::unordered_map<K, V>` | `std::collections::HashMap<K, V>` | По умолчанию использует `SipHash` (устойчив к DoS) | +| `std::map<K, V>` | `std::collections::BTreeMap<K, V>` | B-дерево; ключи отсортированы; требуется `K: Ord` | +| `std::unordered_set<T>` | `std::collections::HashSet<T>` | Требуется `T: Hash + Eq` | +| `std::set<T>` | `std::collections::BTreeSet<T>` | Отсортированное множество; требуется `T: Ord` | +| `std::priority_queue<T>` | `std::collections::BinaryHeap<T>` | Max-куча по умолчанию (как в C++) | +| `std::stack<T>` | `Vec<T>` с `.push()` / `.pop()` | Отдельный тип стека не нужен | +| `std::queue<T>` | `VecDeque<T>` с `.push_back()` / `.pop_front()` | Отдельный тип очереди не нужен | +| `std::string` | `String` | Гарантированный UTF-8, без завершающего нуля | +| `std::string_view` | `&str` | Заимствованный срез UTF-8 | +| `std::span<T>` (C++20) | `&[T]` / `&mut [T]` | Срезы Rust — полноценный тип с версии 1.0 | +| `std::tuple<A, B, C>` | `(A, B, C)` | Встроенный синтаксис, поддерживает деструктуризацию | +| `std::pair<A, B>` | `(A, B)` | Просто кортеж из двух элементов | +| `std::bitset<N>` | Эквивалента в std нет | Используйте крейт `bitvec` или `[u8; N/8]` | + +**Ключевые различия**: +- Для `HashMap`/`HashSet` в Rust требуется `K: Hash + Eq` — компилятор проверяет это на уровне типов, в отличие от C++, где использование ключа без хеширования даёт ошибку шаблона глубоко внутри STL +- Индексация `Vec` (`v[i]`) по умолчанию вызывает panic при выходе за границы. Используйте `.get(i)` для `Option<&T>` или итераторы, чтобы полностью избежать проверок границ +- Нет `std::multimap` и `std::multiset` — используйте `HashMap<K, Vec<V>>` или `BTreeMap<K, Vec<V>>` --- -### Exception Safety → Panic Safety +### Безопасность исключений → безопасность при panic -C++ defines three levels of exception safety (Abrahams guarantees): +C++ определяет три уровня безопасности исключений (гарантии Абрахамса): -| C++ Level | Meaning | Rust Equivalent | +| Уровень C++ | Значение | Эквивалент в Rust | |----------|---------|----------------| -| **No-throw** | Function never throws | Function never panics (returns `Result`) | -| **Strong** (commit-or-rollback) | If it throws, state is unchanged | Ownership model makes this natural — if `?` returns early, partially built values are dropped | -| **Basic** | If it throws, invariants are preserved | Rust's default — `Drop` runs, no leaks | +| **No-throw** | Функция никогда не выбрасывает исключение | Функция никогда не вызывает panic (возвращает `Result`) | +| **Strong** (фиксация или откат) | Если выброшено исключение, состояние не меняется | Модель владения делает это естественным — если `?` досрочно возвращает управление, частично построенные значения уничтожаются | +| **Basic** | Если выброшено исключение, инварианты сохраняются | Поведение Rust по умолчанию — `Drop` выполняется, утечек нет | -#### How Rust's ownership model helps +#### Как модель владения помогает ```rust -// Strong guarantee for free — if file.write() fails, config is unchanged +// Сильная гарантия бесплатно — если file.write() завершится ошибкой, config не меняется fn update_config(config: &mut Config, path: &str) -> Result<(), Error> { - let new_data = fetch_from_network()?; // Err → early return, config untouched - let validated = validate(new_data)?; // Err → early return, config untouched - *config = validated; // Only reached on success (commit) + let new_data = fetch_from_network()?; // Err → ранний возврат, config не тронут + let validated = validate(new_data)?; // Err → ранний возврат, config не тронут + *config = validated; // Выполняется только при успехе (фиксация) Ok(()) } ``` -In C++, achieving the strong guarantee requires manual rollback or the copy-and-swap -idiom. In Rust, `?` propagation gives you the strong guarantee by default for most code. +В C++ для сильной гарантии нужен ручной откат или идиома copy-and-swap. В Rust +распространение через `?` даёт сильную гарантию по умолчанию для большинства кода. -#### `catch_unwind` — Rust's equivalent of `catch(...)` +#### `catch_unwind` — аналог `catch(...)` в Rust ```rust use std::panic; -// Catch a panic (like catch(...) in C++) — rarely needed +// Перехват panic (как catch(...) в C++) — нужно редко let result = panic::catch_unwind(|| { - // Code that might panic + // Код, который может вызвать panic let v = vec![1, 2, 3]; - v[10] // Panics! (index out of bounds) + v[10] // Panic! (выход за границы индекса) }); match result { @@ -694,18 +681,18 @@ match result { } ``` -#### `UnwindSafe` — marking types as panic-safe +#### `UnwindSafe` — отметка типов как безопасных при panic ```rust use std::panic::UnwindSafe; -// Types behind &mut are NOT UnwindSafe by default — the panic may have -// left them in a partially-modified state +// Типы за &mut по умолчанию НЕ UnwindSafe — panic мог оставить их +// в частично изменённом состоянии fn safe_execute<F: FnOnce() + UnwindSafe>(f: F) { let _ = std::panic::catch_unwind(f); } -// Use AssertUnwindSafe to override when you've audited the code: +// Используйте AssertUnwindSafe, чтобы переопределить, когда вы проверили код: use std::panic::AssertUnwindSafe; let mut data = vec![1, 2, 3]; let _ = std::panic::catch_unwind(AssertUnwindSafe(|| { @@ -713,53 +700,44 @@ let _ = std::panic::catch_unwind(AssertUnwindSafe(|| { })); ``` -| C++ Exception Pattern | Rust Equivalent | +| Шаблон исключений C++ | Эквивалент в Rust | |-----------------------|-----------------| -| `throw MyException()` | `return Err(MyError::...)` (preferred) or `panic!("...")` | -| `try { } catch (const E& e)` | `match result { Ok(v) => ..., Err(e) => ... }` or `?` | +| `throw MyException()` | `return Err(MyError::...)` (предпочтительно) или `panic!("...")` | +| `try { } catch (const E& e)` | `match result { Ok(v) => ..., Err(e) => ... }` или `?` | | `catch (...)` | `std::panic::catch_unwind(...)` | -| `noexcept` | `-> Result<T, E>` (errors are values, not exceptions) | -| RAII cleanup in stack unwinding | `Drop::drop()` runs during panic unwinding | +| `noexcept` | `-> Result<T, E>` (ошибки — это значения, а не исключения) | +| Очистка RAII при раскрутке стека | `Drop::drop()` выполняется при раскрутке panic | | `std::uncaught_exceptions()` | `std::thread::panicking()` | -| `-fno-exceptions` compile flag | `panic = "abort"` in `Cargo.toml` [profile] | +| Флаг компиляции `-fno-exceptions` | `panic = "abort"` в секции `[profile]` файла `Cargo.toml` | -> **Bottom line**: In Rust, most code uses `Result<T, E>` instead of exceptions, -> making error paths explicit and composable. `panic!` is reserved for bugs -> (like `assert!` failures), not routine errors. This means "exception safety" -> is largely a non-issue — the ownership system handles cleanup automatically. +> **Главное**: в Rust большинство кода использует `Result<T, E>` вместо исключений, что делает пути ошибок явными и компонуемыми. `panic!` зарезервирован для ошибок в программе (например, при сбое `assert!`), а не для рутинных ошибок. Поэтому «безопасность исключений» во многом перестаёт быть проблемой — система владения автоматически занимается очисткой. --- -## C++ to Rust Migration Patterns +## Шаблоны миграции с C++ на Rust -### Quick Reference: C++ → Rust Idiom Map +### Краткий справочник: соответствие идиом C++ → Rust -| **C++ Pattern** | **Rust Idiom** | **Notes** | +| **Шаблон C++** | **Идиома Rust** | **Примечания** | |----------------|---------------|----------| -| `class Derived : public Base` | `enum Variant { A {...}, B {...} }` | Prefer enums for closed sets | -| `virtual void method() = 0` | `trait MyTrait { fn method(&self); }` | Use for open/extensible interfaces | -| `dynamic_cast<Derived*>(ptr)` | `match value { Variant::A(data) => ..., }` | Exhaustive, no runtime failure | -| `vector<unique_ptr<Base>>` | `Vec<Box<dyn Trait>>` | Only when genuinely polymorphic | -| `shared_ptr<T>` | `Rc<T>` or `Arc<T>` | Prefer `Box<T>` or owned values first | -| `enable_shared_from_this<T>` | Arena pattern (`Vec<T>` + indices) | Eliminates reference cycles entirely | -| `Base* m_pFramework` in every class | `fn execute(&mut self, ctx: &mut Context)` | Pass context, don't store pointers | -| `try { } catch (...) { }` | `match result { Ok(v) => ..., Err(e) => ... }` | Or use `?` for propagation | -| `std::optional<T>` | `Option<T>` | `match` required, can't forget None | -| `const std::string&` parameter | `&str` parameter | Accepts both `String` and `&str` | -| `enum class Foo { A, B, C }` | `enum Foo { A, B, C }` | Rust enums can also carry data | -| `auto x = std::move(obj)` | `let x = obj;` | Move is the default, no `std::move` needed | -| CMake + make + lint | `cargo build / test / clippy / fmt` | One tool for everything | - -### Migration Strategy -1. **Start with data types**: Translate structs and enums first — this forces you to think about ownership -2. **Convert factories to enums**: If a factory creates different derived types, it should probably be `enum` + `match` -3. **Convert god objects to composed structs**: Group related fields into focused structs -4. **Replace pointers with borrows**: Convert `Base*` stored pointers to `&'a T` lifetime-bounded borrows -5. **Use `Box<dyn Trait>` sparingly**: Only for plugin systems and test mocking -6. **Let the compiler guide you**: Rust's error messages are excellent — read them carefully - - - - - - +| `class Derived : public Base` | `enum Variant { A {...}, B {...} }` | Для закрытых наборов предпочитайте перечисления | +| `virtual void method() = 0` | `trait MyTrait { fn method(&self); }` | Для открытых/расширяемых интерфейсов | +| `dynamic_cast<Derived*>(ptr)` | `match value { Variant::A(data) => ..., }` | Исчерпывающе, без ошибок во время выполнения | +| `vector<unique_ptr<Base>>` | `Vec<Box<dyn Trait>>` | Только когда действительно нужен полиморфизм | +| `shared_ptr<T>` | `Rc<T>` или `Arc<T>` | Сначала рассмотрите `Box<T>` или владеющие значения | +| `enable_shared_from_this<T>` | Паттерн арены (`Vec<T>` + индексы) | Полностью устраняет циклы ссылок | +| `Base* m_pFramework` в каждом классе | `fn execute(&mut self, ctx: &mut Context)` | Передавайте контекст, не храните указатели | +| `try { } catch (...) { }` | `match result { Ok(v) => ..., Err(e) => ... }` | Или `?` для распространения | +| `std::optional<T>` | `Option<T>` | Требуется `match`, нельзя забыть None | +| Параметр `const std::string&` | Параметр `&str` | Принимает и `String`, и `&str` | +| `enum class Foo { A, B, C }` | `enum Foo { A, B, C }` | Перечисления Rust могут также хранить данные | +| `auto x = std::move(obj)` | `let x = obj;` | Перемещение по умолчанию, `std::move` не нужен | +| CMake + make + линтер | `cargo build / test / clippy / fmt` | Один инструмент для всего | + +### Стратегия миграции +1. **Начните с типов данных**: сначала переводите структуры и перечисления — это заставляет думать о владении +2. **Превращайте фабрики в перечисления**: если фабрика создаёт разные производные типы, скорее всего, это должно быть `enum` + `match` +3. **Превращайте божественные объекты в компонуемые структуры**: группируйте связанные поля в сфокусированные структуры +4. **Заменяйте указатели заимствованиями**: переводите хранимые указатели `Base*` в заимствования с ограниченным временем жизни `&'a T` +5. **Используйте `Box<dyn Trait>` экономно**: только для систем плагинов и моков в тестах +6. **Доверьтесь компилятору**: сообщения об ошибках Rust отличны — читайте их внимательно diff --git a/c-cpp-book/src/ch19-macros.md b/c-cpp-book/src/ch19-macros.md index 80e0c24..5080e51 100644 --- a/c-cpp-book/src/ch19-macros.md +++ b/c-cpp-book/src/ch19-macros.md @@ -1,32 +1,32 @@ -## Rust Macros: From Preprocessor to Metaprogramming +## Макросы Rust: от препроцессора к метапрограммированию -> **What you'll learn:** How Rust macros work, when to use them instead of functions or generics, and how they replace the C/C++ preprocessor. By the end of this chapter you can write your own `macro_rules!` macros and understand what `#[derive(Debug)]` does under the hood. +> **Что вы узнаете:** как работают макросы Rust, когда использовать их вместо функций или обобщений и как они заменяют препроцессор C/C++. К концу главы вы сможете писать собственные макросы `macro_rules!` и понимать, что скрывается за `#[derive(Debug)]`. -Macros are one of the first things you encounter in Rust (`println!("hello")` on line one) but one of the last things most courses explain. This chapter fixes that. +Макросы — одна из первых вещей, с которой вы сталкиваетесь в Rust (`println!("hello")` в первой же строке), но одна из последних, что объясняют большинство курсов. Эта глава восполняет этот пробел. -### Why Macros Exist +### Зачем нужны макросы -Functions and generics handle most code reuse in Rust. Macros fill the gaps where the type system can't reach: +Функции и обобщения покрывают большую часть повторного использования кода в Rust. Макросы закрывают пробелы, до которых не дотягивается система типов: -| Need | Function/Generic? | Macro? | Why | +| Потребность | Функция/обобщение? | Макрос? | Почему | |------|-------------------|--------|-----| -| Compute a value | ✅ `fn max<T: Ord>(a: T, b: T) -> T` | — | Type system handles it | -| Accept variable number of arguments | ❌ Rust has no variadic functions | ✅ `println!("{} {}", a, b)` | Macros accept any number of tokens | -| Generate repetitive `impl` blocks | ❌ No way with generics alone | ✅ `macro_rules!` | Macros generate code at compile time | -| Run code at compile time | ❌ `const fn` is limited | ✅ Procedural macros | Full Rust code runs at compile time | -| Conditionally include code | ❌ | ✅ `#[cfg(...)]` | Attribute macros control compilation | +| Вычислить значение | ✅ `fn max<T: Ord>(a: T, b: T) -> T` | — | Это делает система типов | +| Принимать переменное число аргументов | ❌ В Rust нет вариативных функций | ✅ `println!("{} {}", a, b)` | Макросы принимают любое число токенов | +| Генерировать однотипные блоки `impl` | ❌ Одними обобщениями не получится | ✅ `macro_rules!` | Макросы генерируют код на этапе компиляции | +| Выполнять код на этапе компиляции | ❌ `const fn` ограничен | ✅ Процедурные макросы | Полноценный код Rust выполняется на этапе компиляции | +| Условно включать код | ❌ | ✅ `#[cfg(...)]` | Макросы-атрибуты управляют компиляцией | -If you're coming from C/C++, think of macros as the *only correct replacement for the preprocessor* — except they operate on the syntax tree instead of raw text, so they're hygienic (no accidental name collisions) and type-aware. +Если вы пришли из C/C++, думайте о макросах как о *единственной правильной замене препроцессора* — с той разницей, что они работают с синтаксическим деревом, а не с сырым текстом, поэтому они гигиеничны (нет случайных коллизий имён) и учитывают типы. -> **For C developers:** Rust macros replace `#define` entirely. There is no textual preprocessor. See [ch18](ch18-cpp-rust-semantic-deep-dives.md) for the full preprocessor → Rust mapping. +> **Для разработчиков на C:** макросы Rust полностью заменяют `#define`. Текстового препроцессора нет. Полное соответствие препроцессора и Rust см. в [ch18](ch18-cpp-rust-semantic-deep-dives.md). --- -## Declarative Macros with `macro_rules!` +## Декларативные макросы с `macro_rules!` -Declarative macros (also called "macros by example") are Rust's most common macro form. They use pattern matching on syntax, similar to `match` on values. +Декларативные макросы (также называемые «макросами по примеру») — самая распространённая форма макросов в Rust. Они используют сопоставление с образцом по синтаксису, похожее на `match` по значениям. -### Basic syntax +### Базовый синтаксис ```rust macro_rules! say_hello { @@ -36,23 +36,23 @@ macro_rules! say_hello { } fn main() { - say_hello!(); // Expands to: println!("Hello!"); + say_hello!(); // Раскрывается в: println!("Hello!"); } ``` -The `!` after the name is what tells you (and the compiler) this is a macro invocation. +`!` после имени — это то, что сообщает (и вам, и компилятору), что это вызов макроса. -### Pattern matching with arguments +### Сопоставление с образцом по аргументам -Macros match on *token trees* using fragment specifiers: +Макросы сопоставляют *деревья токенов* с помощью спецификаторов фрагментов: ```rust macro_rules! greet { - // Pattern 1: no arguments + // Образец 1: без аргументов () => { println!("Hello, world!"); }; - // Pattern 2: one expression argument + // Образец 2: один аргумент-выражение ($name:expr) => { println!("Hello, {}!", $name); }; @@ -64,31 +64,31 @@ fn main() { } ``` -#### Fragment specifiers reference +#### Справочник спецификаторов фрагментов -| Specifier | Matches | Example | +| Спецификатор | Соответствует | Пример | |-----------|---------|---------| -| `$x:expr` | Any expression | `42`, `a + b`, `foo()` | -| `$x:ty` | A type | `i32`, `Vec<String>`, `&str` | -| `$x:ident` | An identifier | `foo`, `my_var` | -| `$x:pat` | A pattern | `Some(x)`, `_`, `(a, b)` | -| `$x:stmt` | A statement | `let x = 5;` | -| `$x:block` | A block | `{ println!("hi"); 42 }` | -| `$x:literal` | A literal | `42`, `"hello"`, `true` | -| `$x:tt` | A single token tree | Anything — the wildcard | -| `$x:item` | An item (fn, struct, impl, etc.) | `fn foo() {}` | +| `$x:expr` | Любому выражению | `42`, `a + b`, `foo()` | +| `$x:ty` | Типу | `i32`, `Vec<String>`, `&str` | +| `$x:ident` | Идентификатору | `foo`, `my_var` | +| `$x:pat` | Образцу | `Some(x)`, `_`, `(a, b)` | +| `$x:stmt` | Оператору | `let x = 5;` | +| `$x:block` | Блоку | `{ println!("hi"); 42 }` | +| `$x:literal` | Литералу | `42`, `"hello"`, `true` | +| `$x:tt` | Одному дереву токенов | Что угодно — универсальный шаблон | +| `$x:item` | Элементу (fn, struct, impl и т. д.) | `fn foo() {}` | -### Repetition — the killer feature +### Повторение — ключевая возможность -C/C++ macros can't loop. Rust macros can repeat patterns: +Макросы C/C++ не умеют циклов. Макросы Rust умеют повторять образцы: ```rust macro_rules! make_vec { - // Match zero or more comma-separated expressions + // Сопоставляем ноль или более выражений, разделённых запятыми ( $( $element:expr ),* ) => { { let mut v = Vec::new(); - $( v.push($element); )* // Repeat for each matched element + $( v.push($element); )* // Повторяем для каждого сопоставленного элемента v } }; @@ -100,9 +100,9 @@ fn main() { } ``` -The `$( ... ),*` syntax means "match zero or more of this pattern, separated by commas." The `$( ... )*` in the expansion repeats the body once for each match. +Синтаксис `$( ... ),*` означает «сопоставить ноль или более таких образцов, разделённых запятыми». Конструкция `$( ... )*` в раскрытии повторяет тело один раз для каждого совпадения. -> **This is exactly how `vec![]` is implemented in the standard library.** The actual source is: +> **Именно так реализован `vec![]` в стандартной библиотеке.** Реальный исходный код: > ```rust > macro_rules! vec { > () => { Vec::new() }; @@ -110,19 +110,19 @@ The `$( ... ),*` syntax means "match zero or more of this pattern, separated by > ($($x:expr),+ $(,)?) => { <[_]>::into_vec(Box::new([$($x),+])) }; > } > ``` -> The `$(,)?` at the end allows an optional trailing comma. +> Конструкция `$(,)?` в конце разрешает необязательную завершающую запятую. -#### Repetition operators +#### Операторы повторения -| Operator | Meaning | Example | -|----------|---------|---------| -| `$( ... )*` | Zero or more | `vec![]`, `vec![1]`, `vec![1, 2, 3]` | -| `$( ... )+` | One or more | At least one element required | -| `$( ... )?` | Zero or one | Optional element | +| Оператор | Значение | Пример | +|----------|---------|--------| +| `$( ... )*` | Ноль или более | `vec![]`, `vec![1]`, `vec![1, 2, 3]` | +| `$( ... )+` | Один или более | Требуется хотя бы один элемент | +| `$( ... )?` | Ноль или один | Необязательный элемент | -### Practical example: a `hashmap!` constructor +### Практический пример: конструктор `hashmap!` -The standard library has `vec![]` but no `hashmap!{}`. Let's build one: +В стандартной библиотеке есть `vec![]`, но нет `hashmap!{}`. Давайте создадим его: ```rust macro_rules! hashmap { @@ -139,15 +139,15 @@ fn main() { let scores = hashmap! { "Alice" => 95, "Bob" => 87, - "Carol" => 92, // trailing comma OK thanks to $(,)? + "Carol" => 92, // Завершающая запятая допустима благодаря $(,)? }; println!("{scores:?}"); } ``` -### Practical example: diagnostic check macro +### Практический пример: макрос проверки диагностики -A pattern common in embedded/diagnostic code — check a condition and return an error: +Шаблон, распространённый во встраиваемом и диагностическом коде, — проверить условие и вернуть ошибку: ```rust use thiserror::Error; @@ -175,114 +175,114 @@ fn run_diagnostics(temp: f64, voltage: f64) -> Result<(), DiagError> { } ``` -> **C/C++ comparison:** +> **Сравнение с C/C++:** > ```c -> // C preprocessor — textual substitution, no type safety, no hygiene +> // Препроцессор C — текстовая подстановка, без проверки типов, без гигиены > #define DIAG_CHECK(cond, msg) \ > do { if (!(cond)) { log_error(msg); return -1; } } while(0) > ``` -> The Rust version returns a proper `Result` type, has no double-evaluation risk, and the compiler checks that `$cond` is actually a `bool` expression. +> Версия на Rust возвращает корректный тип `Result`, не рискует двойным вычислением, а компилятор проверяет, что `$cond` действительно является выражением типа `bool`. -### Hygiene: why Rust macros are safe +### Гигиена: почему макросы Rust безопасны -C/C++ macro bugs often come from name collisions: +Ошибки макросов C/C++ часто возникают из-за коллизий имён: ```c -// C: dangerous — `x` could shadow the caller's `x` +// C: опасно — `x` может затенить `x` вызывающего кода #define SQUARE(x) ((x) * (x)) int x = 5; -int result = SQUARE(x++); // UB: x incremented twice! +int result = SQUARE(x++); // Неопределённое поведение: x инкрементируется дважды! ``` -Rust macros are **hygienic** — variables created inside a macro don't leak out: +Макросы Rust **гигиеничны** — переменные, созданные внутри макроса, не утекают наружу: ```rust macro_rules! make_x { () => { - let x = 42; // This `x` is scoped to the macro expansion + let x = 42; // Этот `x` ограничен раскрытием макроса }; } fn main() { let x = 10; make_x!(); - println!("{x}"); // Prints 10, not 42 — hygiene prevents collision + println!("{x}"); // Выводит 10, а не 42 — гигиена предотвращает коллизию } ``` -The macro's `x` and the caller's `x` are treated as different variables by the compiler, even though they have the same name. **This is impossible with the C preprocessor.** +Компилятор считает `x` макроса и `x` вызывающего кода разными переменными, даже несмотря на одинаковые имена. **С препроцессором C это невозможно.** --- -## Common Standard Library Macros +## Распространённые макросы стандартной библиотеки -You've been using these since chapter 1 — here's what they actually do: +Вы используете их с первой главы — вот что они делают на самом деле: -| Macro | What it does | Expands to (simplified) | +| Макрос | Что делает | Раскрывается в (упрощённо) | |-------|-------------|------------------------| -| `println!("{}", x)` | Format and print to stdout + newline | `std::io::_print(format_args!(...))` | -| `eprintln!("{}", x)` | Print to stderr + newline | Same but to stderr | -| `format!("{}", x)` | Format into a `String` | Allocates and returns a `String` | -| `vec![1, 2, 3]` | Create a `Vec` with elements | `Vec::from([1, 2, 3])` (approximately) | -| `todo!()` | Mark unfinished code | `panic!("not yet implemented")` | -| `unimplemented!()` | Mark deliberately unimplemented code | `panic!("not implemented")` | -| `unreachable!()` | Mark code the compiler can't prove unreachable | `panic!("unreachable")` | -| `assert!(cond)` | Panic if condition is false | `if !cond { panic!(...) }` | -| `assert_eq!(a, b)` | Panic if values aren't equal | Shows both values on failure | -| `dbg!(expr)` | Print expression + value to stderr, return value | `eprintln!("[file:line] expr = {:#?}", &expr); expr` | -| `include_str!("file.txt")` | Embed file contents as `&str` at compile time | Reads file during compilation | -| `include_bytes!("data.bin")` | Embed file contents as `&[u8]` at compile time | Reads file during compilation | -| `cfg!(condition)` | Compile-time condition as a `bool` | `true` or `false` based on target | -| `env!("VAR")` | Read environment variable at compile time | Fails compilation if not set | -| `concat!("a", "b")` | Concatenate literals at compile time | `"ab"` | - -### `dbg!` — the debugging macro you'll use daily +| `println!("{}", x)` | Форматирует и выводит в stdout с переводом строки | `std::io::_print(format_args!(...))` | +| `eprintln!("{}", x)` | Выводит в stderr с переводом строки | То же, но в stderr | +| `format!("{}", x)` | Форматирует в `String` | Выделяет память и возвращает `String` | +| `vec![1, 2, 3]` | Создаёт `Vec` с элементами | `Vec::from([1, 2, 3])` (примерно) | +| `todo!()` | Отмечает незавершённый код | `panic!("not yet implemented")` | +| `unimplemented!()` | Отмечает намеренно не реализованный код | `panic!("not implemented")` | +| `unreachable!()` | Отмечает код, недостижимость которого компилятор не может доказать | `panic!("unreachable")` | +| `assert!(cond)` | Вызывает panic, если условие ложно | `if !cond { panic!(...) }` | +| `assert_eq!(a, b)` | Вызывает panic, если значения не равны | При ошибке показывает оба значения | +| `dbg!(expr)` | Выводит выражение и его значение в stderr, возвращает значение | `eprintln!("[file:line] expr = {:#?}", &expr); expr` | +| `include_str!("file.txt")` | Встраивает содержимое файла как `&str` на этапе компиляции | Читает файл во время компиляции | +| `include_bytes!("data.bin")` | Встраивает содержимое файла как `&[u8]` на этапе компиляции | Читает файл во время компиляции | +| `cfg!(condition)` | Условие времени компиляции как `bool` | `true` или `false` в зависимости от цели | +| `env!("VAR")` | Читает переменную окружения на этапе компиляции | Компиляция завершается ошибкой, если её нет | +| `concat!("a", "b")` | Объединяет литералы на этапе компиляции | `"ab"` | + +### `dbg!` — макрос для отладки, которым вы будете пользоваться каждый день ```rust fn factorial(n: u32) -> u32 { - if dbg!(n <= 1) { // Prints: [src/main.rs:2] n <= 1 = false - dbg!(1) // Prints: [src/main.rs:3] 1 = 1 + if dbg!(n <= 1) { // Выводит: [src/main.rs:2] n <= 1 = false + dbg!(1) // Выводит: [src/main.rs:3] 1 = 1 } else { - dbg!(n * factorial(n - 1)) // Prints intermediate values + dbg!(n * factorial(n - 1)) // Выводит промежуточные значения } } fn main() { - dbg!(factorial(4)); // Prints all recursive calls with file:line + dbg!(factorial(4)); // Выводит все рекурсивные вызовы с файлом и строкой } ``` -`dbg!` returns the value it wraps, so you can insert it anywhere without changing program behavior. It prints to stderr (not stdout), so it doesn't interfere with program output. **Remove all `dbg!` calls before committing code.** +`dbg!` возвращает значение, которое оборачивает, поэтому его можно вставить куда угодно, не меняя поведение программы. Он выводит в stderr (а не в stdout), поэтому не мешает выводу программы. **Перед коммитом удаляйте все вызовы `dbg!`.** -### Format string syntax +### Синтаксис строк формата -Since `println!`, `format!`, `eprintln!`, and `write!` all use the same format machinery, here's the quick reference: +Поскольку `println!`, `format!`, `eprintln!` и `write!` используют один и тот же механизм форматирования, вот краткий справочник: ```rust let name = "sensor"; let value = 3.14159; let count = 42; -println!("{name}"); // Variable by name (Rust 1.58+) -println!("{}", name); // Positional -println!("{value:.2}"); // 2 decimal places: "3.14" -println!("{count:>10}"); // Right-aligned, width 10: " 42" -println!("{count:0>10}"); // Zero-padded: "0000000042" -println!("{count:#06x}"); // Hex with prefix: "0x002a" -println!("{count:#010b}"); // Binary with prefix: "0b00101010" -println!("{value:?}"); // Debug format -println!("{value:#?}"); // Pretty-printed Debug format +println!("{name}"); // Переменная по имени (Rust 1.58+) +println!("{}", name); // Позиционно +println!("{value:.2}"); // 2 знака после запятой: "3.14" +println!("{count:>10}"); // Выравнивание по правому краю, ширина 10: " 42" +println!("{count:0>10}"); // Дополнение нулями: "0000000042" +println!("{count:#06x}"); // Шестнадцатеричный с префиксом: "0x002a" +println!("{count:#010b}"); // Двоичный с префиксом: "0b00101010" +println!("{value:?}"); // Формат Debug +println!("{value:#?}"); // Формат Debug с красивым выводом ``` -> **For C developers:** Think of this as a type-safe `printf` — the compiler checks that `{:.2}` is applied to a float, not a string. No `%s`/`%d` format mismatch bugs. +> **Для разработчиков на C:** думайте об этом как о типобезопасном `printf` — компилятор проверяет, что `{:.2}` применяется к числу с плавающей точкой, а не к строке. Ошибок несоответствия `%s`/`%d` не бывает. > -> **For C++ developers:** This replaces `std::cout << std::fixed << std::setprecision(2) << value` with a single readable format string. +> **Для разработчиков на C++:** это заменяет `std::cout << std::fixed << std::setprecision(2) << value` одной читаемой строкой формата. --- -## Derive Macros +## Макросы derive -You've seen `#[derive(...)]` on nearly every struct in this book: +Вы видели `#[derive(...)]` почти у каждой структуры в этой книге: ```rust #[derive(Debug, Clone, PartialEq)] @@ -292,10 +292,10 @@ struct Point { } ``` -`#[derive(Debug)]` is a **derive macro** — a special kind of procedural macro that generates trait implementations automatically. Here's what it produces (simplified): +`#[derive(Debug)]` — это **макрос derive**, особый вид процедурного макроса, который автоматически генерирует реализации трейтов. Вот что он создаёт (упрощённо): ```rust -// What #[derive(Debug)] generates for Point: +// То, что #[derive(Debug)] генерирует для Point: impl std::fmt::Debug for Point { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Point") @@ -306,152 +306,152 @@ impl std::fmt::Debug for Point { } ``` -Without `#[derive(Debug)]`, you'd have to write that `impl` block by hand for every struct. +Без `#[derive(Debug)]` вам пришлось бы писать такой блок `impl` вручную для каждой структуры. -### Commonly derived traits +### Распространённые производные трейты -| Derive | What it generates | When to use | +| Derive | Что генерирует | Когда использовать | |--------|-------------------|-------------| -| `Debug` | `{:?}` formatting | Almost always — enables printing for debugging | -| `Clone` | `.clone()` method | When you need to duplicate values | -| `Copy` | Implicit copy on assignment | Small, stack-only types (integers, `[f64; 3]`) | -| `PartialEq` / `Eq` | `==` and `!=` operators | When you need equality comparison | -| `PartialOrd` / `Ord` | `<`, `>`, `<=`, `>=` operators | When you need ordering | -| `Hash` | Hashing for `HashMap`/`HashSet` keys | Types used as map keys | -| `Default` | `Type::default()` constructor | Types with sensible zero/empty values | -| `serde::Serialize` / `Deserialize` | JSON/TOML/etc. serialization | Data types that cross API boundaries | +| `Debug` | Форматирование `{:?}` | Почти всегда — позволяет выводить для отладки | +| `Clone` | Метод `.clone()` | Когда нужно дублировать значения | +| `Copy` | Неявное копирование при присваивании | Небольшие типы только на стеке (целые, `[f64; 3]`) | +| `PartialEq` / `Eq` | Операторы `==` и `!=` | Когда нужно сравнение на равенство | +| `PartialOrd` / `Ord` | Операторы `<`, `>`, `<=`, `>=` | Когда нужен порядок | +| `Hash` | Хеширование для ключей `HashMap`/`HashSet` | Типы, используемые как ключи словаря | +| `Default` | Конструктор `Type::default()` | Типы с разумными нулевыми/пустыми значениями | +| `serde::Serialize` / `Deserialize` | Сериализация в JSON/TOML и т. д. | Типы данных, пересекающие границы API | -### The derive decision tree +### Дерево решений по derive ```text -Should I derive it? +Стоит ли выводить этот трейт? │ - ├── Does my type contain only types that implement the trait? - │ ├── Yes → #[derive] will work - │ └── No → Write a manual impl (or skip it) + ├── Содержит ли мой тип только типы, которые реализуют трейт? + │ ├── Да → #[derive] сработает + │ └── Нет → Напишите реализацию вручную (или пропустите) │ - └── Will users of my type reasonably expect this behavior? - ├── Yes → Derive it (Debug, Clone, PartialEq are almost always reasonable) - └── No → Don't derive (e.g., don't derive Copy for a type with a file handle) + └── Будут ли пользователи моего типа разумно ожидать такого поведения? + ├── Да → Выводите (Debug, Clone, PartialEq почти всегда разумны) + └── Нет → Не выводите (например, не выводите Copy для типа с файловым дескриптором) ``` -> **C++ comparison:** `#[derive(Clone)]` is like auto-generating a correct copy constructor. `#[derive(PartialEq)]` is like auto-generating `operator==` that compares each field — something C++20's `= default` spaceship operator finally provides. +> **Сравнение с C++:** `#[derive(Clone)]` похож на автоматическую генерацию корректного конструктора копирования. `#[derive(PartialEq)]` похож на автоматически сгенерированный `operator==`, который сравнивает каждое поле — то, что оператор «космического корабля» `= default` из C++20 наконец предоставляет. --- -## Attribute Macros +## Макросы-атрибуты -Attribute macros transform the item they're attached to. You've already used several: +Макросы-атрибуты преобразуют элемент, к которому они прикреплены. Некоторые вы уже использовали: ```rust -#[test] // Marks a function as a test +#[test] // Помечает функцию как тест fn test_addition() { assert_eq!(2 + 2, 4); } -#[cfg(target_os = "linux")] // Conditionally includes this function +#[cfg(target_os = "linux")] // Условно включает эту функцию fn linux_only() { /* ... */ } -#[derive(Debug)] // Generates Debug implementation +#[derive(Debug)] // Генерирует реализацию Debug struct MyType { /* ... */ } -#[allow(dead_code)] // Suppresses a compiler warning +#[allow(dead_code)] // Подавляет предупреждение компилятора fn unused_helper() { /* ... */ } -#[must_use] // Warn if return value is discarded +#[must_use] // Предупреждает, если возвращаемое значение отброшено fn compute_checksum(data: &[u8]) -> u32 { /* ... */ } ``` -Common built-in attributes: +Распространённые встроенные атрибуты: -| Attribute | Purpose | +| Атрибут | Назначение | |-----------|---------| -| `#[test]` | Mark as test function | -| `#[cfg(...)]` | Conditional compilation | -| `#[derive(...)]` | Auto-generate trait impls | -| `#[allow(...)]` / `#[deny(...)]` / `#[warn(...)]` | Control lint levels | -| `#[must_use]` | Warn on unused return values | -| `#[inline]` / `#[inline(always)]` | Hint to inline the function | -| `#[repr(C)]` | Use C-compatible memory layout (for FFI) | -| `#[no_mangle]` | Don't mangle the symbol name (for FFI) | -| `#[deprecated]` | Mark as deprecated with optional message | - -> **For C/C++ developers:** Attributes replace a mix of preprocessor directives (`#pragma`, `__attribute__((...))`), and compiler-specific extensions. They're part of the language grammar, not bolted-on extensions. +| `#[test]` | Пометить как тестовую функцию | +| `#[cfg(...)]` | Условная компиляция | +| `#[derive(...)]` | Автоматическая генерация реализаций трейтов | +| `#[allow(...)]` / `#[deny(...)]` / `#[warn(...)]` | Управление уровнями линтера | +| `#[must_use]` | Предупреждать о неиспользованных возвращаемых значениях | +| `#[inline]` / `#[inline(always)]` | Подсказка компилятору встроить функцию | +| `#[repr(C)]` | Использовать раскладку памяти, совместимую с C (для FFI) | +| `#[no_mangle]` | Не искажать имя символа (для FFI) | +| `#[deprecated]` | Пометить как устаревший с необязательным сообщением | + +> **Для разработчиков на C/C++:** атрибуты заменяют смесь директив препроцессора (`#pragma`, `__attribute__((...))`) и расширений, специфичных для компилятора. Они входят в грамматику языка, а не являются приклеенными расширениями. --- -## Procedural Macros (Conceptual Overview) +## Процедурные макросы (концептуальный обзор) -Procedural macros ("proc macros") are macros written as *separate Rust programs* that run at compile time and generate code. They're more powerful than `macro_rules!` but also more complex. +Процедурные макросы («proc macros») — это макросы, написанные как *отдельные программы на Rust*, которые выполняются на этапе компиляции и генерируют код. Они мощнее, чем `macro_rules!`, но и сложнее. -There are three kinds: +Существует три вида: -| Kind | Syntax | Example | What it does | +| Вид | Синтаксис | Пример | Что делает | |------|--------|---------|-------------| -| **Function-like** | `my_macro!(...)` | `sql!(SELECT * FROM users)` | Parses custom syntax, generates Rust code | -| **Derive** | `#[derive(MyTrait)]` | `#[derive(Serialize)]` | Generates trait impl from struct definition | -| **Attribute** | `#[my_attr]` | `#[tokio::main]`, `#[instrument]` | Transforms the annotated item | +| **Функциональный** | `my_macro!(...)` | `sql!(SELECT * FROM users)` | Разбирает пользовательский синтаксис, генерирует код Rust | +| **Derive** | `#[derive(MyTrait)]` | `#[derive(Serialize)]` | Генерирует реализацию трейта по определению структуры | +| **Атрибут** | `#[my_attr]` | `#[tokio::main]`, `#[instrument]` | Преобразует помеченный элемент | -### You've already used proc macros +### Вы уже используете процедурные макросы -- `#[derive(Error)]` from `thiserror` — generates `Display` and `From` impls for error enums -- `#[derive(Serialize, Deserialize)]` from `serde` — generates serialization code -- `#[tokio::main]` — transforms `async fn main()` into a runtime setup + block_on -- `#[test]` — registered by the test harness (built-in proc macro) +- `#[derive(Error)]` из `thiserror` — генерирует реализации `Display` и `From` для перечислений ошибок +- `#[derive(Serialize, Deserialize)]` из `serde` — генерирует код сериализации +- `#[tokio::main]` — превращает `async fn main()` в настройку среды выполнения + block_on +- `#[test]` — регистрируется тестовым харнессом (встроенный процедурный макрос) -### When to write your own proc macro +### Когда стоит писать собственный процедурный макрос -You likely won't need to write proc macros during this course. They're useful when: -- You need to inspect struct fields/enum variants at compile time (derive macros) -- You're building a domain-specific language (function-like macros) -- You need to transform function signatures (attribute macros) +Скорее всего, в ходе этого курса писать процедурные макросы не понадобится. Они полезны, когда: +- Нужно анализировать поля структур или варианты перечислений на этапе компиляции (макросы derive) +- Вы создаёте предметно-ориентированный язык (функциональные макросы) +- Нужно преобразовывать сигнатуры функций (макросы-атрибуты) -For most code, `macro_rules!` or plain functions are sufficient. +Для большинства кода достаточно `macro_rules!` или обычных функций. -> **C++ comparison:** Procedural macros fill the role that code generators, template metaprogramming, and external tools like `protoc` fill in C++. The difference is that proc macros are part of the cargo build pipeline — no external build steps, no CMake custom commands. +> **Сравнение с C++:** процедурные макросы выполняют ту роль, которую в C++ играют генераторы кода, метапрограммирование на шаблонах и внешние инструменты вроде `protoc`. Разница в том, что процедурные макросы — часть конвейера сборки cargo: никаких внешних шагов сборки, никаких пользовательских команд CMake. --- -## When to Use What: Macros vs Functions vs Generics +## Когда что использовать: макросы, функции или обобщения ```text -Need to generate code? +Нужно генерировать код? │ - ├── No → Use a function or generic function - │ (simpler, better error messages, IDE support) + ├── Нет → Используйте функцию или обобщённую функцию + │ (проще, лучшие сообщения об ошибках, поддержка IDE) │ - └── Yes ─┬── Variable number of arguments? - │ └── Yes → macro_rules! (e.g., println!, vec!) + └── Да ─┬── Переменное число аргументов? + │ └── Да → macro_rules! (например, println!, vec!) │ - ├── Repetitive impl blocks for many types? - │ └── Yes → macro_rules! with repetition + ├── Однотипные блоки impl для многих типов? + │ └── Да → macro_rules! с повторением │ - ├── Need to inspect struct fields? - │ └── Yes → Derive macro (proc macro) + ├── Нужно анализировать поля структуры? + │ └── Да → Макрос derive (процедурный макрос) │ - ├── Need custom syntax (DSL)? - │ └── Yes → Function-like proc macro + ├── Нужен собственный синтаксис (DSL)? + │ └── Да → Функциональный процедурный макрос │ - └── Need to transform a function/struct? - └── Yes → Attribute proc macro + └── Нужно преобразовать функцию/структуру? + └── Да → Процедурный макрос-атрибут ``` -**General guideline:** If a function or generic can do it, don't use a macro. Macros have worse error messages, no IDE auto-complete inside the macro body, and are harder to debug. +**Общее правило:** если можно обойтись функцией или обобщением, не используйте макрос. У макросов хуже сообщения об ошибках, нет автодополнения IDE внутри тела макроса, и их труднее отлаживать. --- -## Exercises +## Упражнения -### 🟢 Exercise 1: `min!` macro +### 🟢 Упражнение 1: макрос `min!` -Write a `min!` macro that: -- `min!(a, b)` returns the smaller of two values -- `min!(a, b, c)` returns the smallest of three values -- Works with any type that implements `PartialOrd` +Напишите макрос `min!`, который: +- `min!(a, b)` возвращает меньшее из двух значений +- `min!(a, b, c)` возвращает наименьшее из трёх значений +- Работает с любым типом, реализующим `PartialOrd` -**Hint:** You'll need two match arms in your `macro_rules!`. +**Подсказка:** в вашем `macro_rules!` понадобятся два варианта ветвления. -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust macro_rules! min { @@ -470,18 +470,18 @@ fn main() { } ``` -**Note:** For production code, prefer `std::cmp::min` or `a.min(b)`. This exercise demonstrates the mechanics of multi-arm macros. +**Примечание:** для продакшн-кода лучше использовать `std::cmp::min` или `a.min(b)`. Это упражнение демонстрирует устройство макросов с несколькими ветвями. </details> -### 🟡 Exercise 2: `hashmap!` from scratch +### 🟡 Упражнение 2: `hashmap!` с нуля -Without looking at the example above, write a `hashmap!` macro that: -- Creates a `HashMap` from `key => value` pairs -- Supports trailing commas -- Works with any hashable key type +Не глядя на пример выше, напишите макрос `hashmap!`, который: +- Создаёт `HashMap` из пар `ключ => значение` +- Поддерживает завершающие запятые +- Работает с любым хешируемым типом ключа -Test with: +Проверьте на: ```rust let m = hashmap! { "name" => "Alice", @@ -491,7 +491,7 @@ assert_eq!(m["name"], "Alice"); assert_eq!(m.len(), 2); ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::collections::HashMap; @@ -517,18 +517,18 @@ fn main() { </details> -### 🟡 Exercise 3: `assert_approx_eq!` for floating-point comparison +### 🟡 Упражнение 3: `assert_approx_eq!` для сравнения чисел с плавающей точкой -Write a macro `assert_approx_eq!(a, b, epsilon)` that panics if `|a - b| > epsilon`. This is useful for testing floating-point calculations where exact equality fails. +Напишите макрос `assert_approx_eq!(a, b, epsilon)`, который вызывает panic, если `|a - b| > epsilon`. Он полезен для тестирования вычислений с плавающей точкой, где точное равенство не выполняется. -Test with: +Проверьте на: ```rust -assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10); // Should pass -assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Should pass -// assert_approx_eq!(1.0, 2.0, 0.5); // Should panic +assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10); // Должно пройти +assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Должно пройти +// assert_approx_eq!(1.0, 2.0, 0.5); // Должно вызвать panic ``` -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust macro_rules! assert_approx_eq { @@ -553,9 +553,9 @@ fn main() { </details> -### 🔴 Exercise 4: `impl_display_for_enum!` +### 🔴 Упражнение 4: `impl_display_for_enum!` -Write a macro that generates a `Display` implementation for simple C-like enums. Given: +Напишите макрос, который генерирует реализацию `Display` для простых перечислений в стиле C. Дано: ```rust impl_display_for_enum! { @@ -567,11 +567,11 @@ impl_display_for_enum! { } ``` -It should generate both the `enum Color { Red, Green, Blue }` definition AND the `impl Display for Color` that maps each variant to its string. +Он должен сгенерировать и определение `enum Color { Red, Green, Blue }`, И `impl Display for Color`, который сопоставляет каждый вариант с его строкой. -**Hint:** You'll need both `$( ... ),*` repetition and multiple fragment specifiers. +**Подсказка:** понадобятся и повторение `$( ... ),*`, и несколько спецификаторов фрагментов. -<details><summary>Solution (click to expand)</summary> +<details><summary>Решение (нажмите, чтобы развернуть)</summary> ```rust use std::fmt; From e2a13a1032be63c07d47bd01d8fac35e3e8b1b70 Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 10:23:58 +0400 Subject: [PATCH 15/18] Translate C# book SUMMARY and chapters 1-6 to Russian (WIP) Translate prose, headings, tables, diagram labels, code comments and user-facing strings in csharp-book/src for SUMMARY.md and ch01 through ch06. Code identifiers, crate names, file names and links are unchanged. Chapter-level TOC anchors in ch00 and cross-chapter anchors are updated in a follow-up commit once the remaining chapters are translated. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- csharp-book/src/SUMMARY.md | 88 ++-- .../src/ch01-introduction-and-motivation.md | 424 ++++++++------- .../ch02-1-essential-keywords-reference.md | 313 ++++++----- csharp-book/src/ch02-getting-started.md | 236 ++++----- ...1-true-immutability-vs-record-illusions.md | 119 +++-- .../src/ch03-built-in-types-and-variables.md | 494 +++++++++--------- csharp-book/src/ch04-control-flow.md | 168 +++--- .../src/ch05-1-constructor-patterns.md | 56 +- ...2-collections-vec-hashmap-and-iterators.md | 278 +++++----- .../ch05-data-structures-and-collections.md | 398 +++++++------- ...6-1-exhaustive-matching-and-null-safety.md | 190 +++---- .../src/ch06-enums-and-pattern-matching.md | 129 ++--- 12 files changed, 1445 insertions(+), 1448 deletions(-) diff --git a/csharp-book/src/SUMMARY.md b/csharp-book/src/SUMMARY.md index e34c547..0547fc6 100644 --- a/csharp-book/src/SUMMARY.md +++ b/csharp-book/src/SUMMARY.md @@ -1,60 +1,60 @@ -# Summary +# Содержание -[Introduction](ch00-introduction.md) +[Введение](ch00-introduction.md) --- -# Part I — Foundations - -- [1. Introduction and Motivation](ch01-introduction-and-motivation.md) -- [2. Getting Started](ch02-getting-started.md) - - [Essential Keywords Reference *(optional)*](ch02-1-essential-keywords-reference.md) -- [3. Built-in Types and Variables](ch03-built-in-types-and-variables.md) - - [True Immutability vs Record Illusions](ch03-1-true-immutability-vs-record-illusions.md) -- [4. Control Flow](ch04-control-flow.md) -- [5. Data Structures and Collections](ch05-data-structures-and-collections.md) - - [Constructor Patterns](ch05-1-constructor-patterns.md) - - [Collections — Vec, HashMap, and Iterators](ch05-2-collections-vec-hashmap-and-iterators.md) -- [6. Enums and Pattern Matching](ch06-enums-and-pattern-matching.md) - - [Exhaustive Matching and Null Safety](ch06-1-exhaustive-matching-and-null-safety.md) -- [7. Ownership and Borrowing](ch07-ownership-and-borrowing.md) - - [Memory Safety Deep Dive](ch07-1-memory-safety-deep-dive.md) - - [Lifetimes Deep Dive](ch07-2-lifetimes-deep-dive.md) - - [Smart Pointers — Beyond Single Ownership](ch07-3-smart-pointers-beyond-single-ownership.md) -- [8. Crates and Modules](ch08-crates-and-modules.md) - - [Package Management — Cargo vs NuGet](ch08-1-package-management-cargo-vs-nuget.md) -- [9. Error Handling](ch09-error-handling.md) - - [Crate-Level Error Types and Result Aliases](ch09-1-crate-level-error-types-and-result-alias.md) -- [10. Traits and Generics](ch10-traits-and-generics.md) - - [Generic Constraints](ch10-1-generic-constraints.md) - - [Inheritance vs Composition](ch10-2-inheritance-vs-composition.md) -- [11. From and Into Traits](ch11-from-and-into-traits.md) -- [12. Closures and Iterators](ch12-closures-and-iterators.md) - - [Macros Primer](ch12-1-macros-primer.md) +# Часть I — Основы + +- [1. Введение и мотивация](ch01-introduction-and-motivation.md) +- [2. Начало работы](ch02-getting-started.md) + - [Справочник по ключевым словам *(необязательно)*](ch02-1-essential-keywords-reference.md) +- [3. Встроенные типы и переменные](ch03-built-in-types-and-variables.md) + - [Истинная неизменяемость и иллюзии record](ch03-1-true-immutability-vs-record-illusions.md) +- [4. Управление потоком выполнения](ch04-control-flow.md) +- [5. Структуры данных и коллекции](ch05-data-structures-and-collections.md) + - [Паттерны конструкторов](ch05-1-constructor-patterns.md) + - [Коллекции — Vec, HashMap и итераторы](ch05-2-collections-vec-hashmap-and-iterators.md) +- [6. Перечисления и сопоставление с образцом](ch06-enums-and-pattern-matching.md) + - [Исчерпывающее сопоставление и null-безопасность](ch06-1-exhaustive-matching-and-null-safety.md) +- [7. Владение и заимствование](ch07-ownership-and-borrowing.md) + - [Глубокое погружение в безопасность памяти](ch07-1-memory-safety-deep-dive.md) + - [Глубокое погружение во времена жизни](ch07-2-lifetimes-deep-dive.md) + - [Умные указатели — не только единоличное владение](ch07-3-smart-pointers-beyond-single-ownership.md) +- [8. Крейты и модули](ch08-crates-and-modules.md) + - [Управление пакетами — Cargo против NuGet](ch08-1-package-management-cargo-vs-nuget.md) +- [9. Обработка ошибок](ch09-error-handling.md) + - [Типы ошибок уровня крейта и псевдонимы Result](ch09-1-crate-level-error-types-and-result-alias.md) +- [10. Трейты и обобщения](ch10-traits-and-generics.md) + - [Ограничения обобщений](ch10-1-generic-constraints.md) + - [Наследование против композиции](ch10-2-inheritance-vs-composition.md) +- [11. Трейты From и Into](ch11-from-and-into-traits.md) +- [12. Замыкания и итераторы](ch12-closures-and-iterators.md) + - [Введение в макросы](ch12-1-macros-primer.md) --- -# Part II — Concurrency & Systems +# Часть II — Конкурентность и системное программирование -- [13. Concurrency](ch13-concurrency.md) - - [Async/Await Deep Dive](ch13-1-asyncawait-deep-dive.md) -- [14. Unsafe Rust and FFI](ch14-unsafe-rust-and-ffi.md) - - [Testing](ch14-1-testing.md) +- [13. Конкурентность](ch13-concurrency.md) + - [Глубокое погружение в async/await](ch13-1-asyncawait-deep-dive.md) +- [14. Unsafe Rust и FFI](ch14-unsafe-rust-and-ffi.md) + - [Тестирование](ch14-1-testing.md) --- -# Part III — Migration & Best Practices +# Часть III — Миграция и лучшие практики -- [15. Migration Patterns and Case Studies](ch15-migration-patterns-and-case-studies.md) - - [Essential Crates for C# Developers](ch15-1-essential-crates-for-c-developers.md) - - [Incremental Adoption Strategy](ch15-2-incremental-adoption-strategy.md) -- [16. Best Practices](ch16-best-practices.md) - - [Performance Comparison and Migration](ch16-1-performance-comparison-and-migration.md) - - [Learning Path and Resources](ch16-2-learning-path-and-resources.md) - - [Rust Tooling Ecosystem](ch16-3-rust-tooling-ecosystem.md) +- [15. Паттерны миграции и разборы случаев](ch15-migration-patterns-and-case-studies.md) + - [Основные крейты для разработчиков C#](ch15-1-essential-crates-for-c-developers.md) + - [Стратегия постепенного внедрения](ch15-2-incremental-adoption-strategy.md) +- [16. Лучшие практики](ch16-best-practices.md) + - [Сравнение производительности и миграция](ch16-1-performance-comparison-and-migration.md) + - [Путь обучения и ресурсы](ch16-2-learning-path-and-resources.md) + - [Экосистема инструментов Rust](ch16-3-rust-tooling-ecosystem.md) --- -# Capstone +# Итоговый проект -- [17. Capstone Project: Build a CLI Weather Tool](ch17-capstone-project.md) +- [17. Итоговый проект: CLI-утилита для погоды на Rust](ch17-capstone-project.md) diff --git a/csharp-book/src/ch01-introduction-and-motivation.md b/csharp-book/src/ch01-introduction-and-motivation.md index 59092d8..b7e0456 100644 --- a/csharp-book/src/ch01-introduction-and-motivation.md +++ b/csharp-book/src/ch01-introduction-and-motivation.md @@ -1,89 +1,89 @@ -## Speaker Intro and General Approach +## Представление автора и общий подход -- Speaker intro - - Principal Firmware Architect in Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) team - - Industry veteran with expertise in security, systems programming (firmware, operating systems, hypervisors), CPU and platform architecture, and C++ systems - - Started programming in Rust in 2017 (@AWS EC2), and have been in love with the language ever since -- This course is intended to be as interactive as possible - - Assumption: You know C# and .NET development - - Examples deliberately map C# concepts to Rust equivalents - - **Please feel free to ask clarifying questions at any point of time** +- Представление автора + - Principal Firmware Architect в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) в Microsoft + - Ветеран отрасли с опытом в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также C++-систем + - Начал программировать на Rust в 2017 году (в AWS EC2) и с тех пор без памяти влюблён в этот язык +- Этот курс задуман как можно более интерактивный + - Предположение: вы знаете C# и разработку на .NET + - Примеры намеренно сопоставляют концепции C# с их аналогами в Rust + - **Задавайте уточняющие вопросы в любой момент** --- -## The Case for Rust for C# Developers +## Аргументы в пользу Rust для разработчиков C# -> **What you'll learn:** Why Rust matters for C# developers — the performance gap between managed and native code, -> how Rust eliminates null-reference exceptions and hidden control flow at compile time, -> and the key scenarios where Rust complements or replaces C#. +> **Что вы узнаете:** почему Rust важен для разработчиков C# — разрыв в производительности между управляемым и нативным кодом, +> как Rust на этапе компиляции устраняет ошибки NullReferenceException и скрытые потоки управления, +> а также ключевые сценарии, в которых Rust дополняет C# или заменяет его. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Performance Without the Runtime Tax +### Производительность без налога на рантайм ```csharp -// C# - Great productivity, runtime overhead +// C# — отличная продуктивность, но накладные расходы рантайма public class DataProcessor { private List<int> data = new List<int>(); public void ProcessLargeDataset() { - // Allocations trigger GC + // Выделения памяти запускают GC for (int i = 0; i < 10_000_000; i++) { - data.Add(i * 2); // GC pressure + data.Add(i * 2); // Нагрузка на GC } - // Unpredictable GC pauses during processing + // Непредсказуемые паузы GC во время обработки } } -// Runtime: Variable (50-200ms due to GC) -// Memory: ~80MB (including GC overhead) -// Predictability: Low (GC pauses) +// Время выполнения: переменное (50–200 мс из-за GC) +// Память: ~80 МБ (включая накладные расходы GC) +// Предсказуемость: низкая (паузы GC) ``` ```rust -// Rust - Same expressiveness, zero runtime overhead +// Rust — та же выразительность, нулевые накладные расходы рантайма struct DataProcessor { data: Vec<i32>, } impl DataProcessor { fn process_large_dataset(&mut self) { - // Zero-cost abstractions + // Абстракции с нулевой стоимостью for i in 0..10_000_000 { - self.data.push(i * 2); // No GC pressure + self.data.push(i * 2); // Нет нагрузки на GC } - // Deterministic performance + // Детерминированная производительность } } -// Runtime: Consistent (~30ms) -// Memory: ~40MB (exact allocation) -// Predictability: High (no GC) +// Время выполнения: стабильное (~30 мс) +// Память: ~40 МБ (точное выделение) +// Предсказуемость: высокая (нет GC) ``` -### Memory Safety Without Runtime Checks +### Безопасность памяти без проверок во время выполнения ```csharp -// C# - Runtime safety with overhead +// C# — безопасность во время выполнения с накладными расходами public class RuntimeCheckedOperations { public string? ProcessArray(int[] array) { - // Runtime bounds checking on every access + // Проверка границ во время выполнения при каждом обращении if (array.Length > 0) { - return array[0].ToString(); // Safe — int is a value type, never null + return array[0].ToString(); // Безопасно — int это значимый тип, он никогда не бывает null } - return null; // Nullable return (string? with C# 8+ nullable reference types) + return null; // Nullable-возвращаемое значение (string? с nullable-ссылочными типами C# 8+) } public void ProcessConcurrently() { var list = new List<int>(); - // Data races possible, requires careful locking + // Гонки данных возможны, нужна аккуратная синхронизация Parallel.For(0, 1000, i => { - lock (list) // Runtime overhead + lock (list) // Накладные расходы рантайма { list.Add(i); } @@ -93,15 +93,15 @@ public class RuntimeCheckedOperations ``` ```rust -// Rust - Compile-time safety with zero runtime cost +// Rust — безопасность на этапе компиляции без затрат во время выполнения struct SafeOperations; impl SafeOperations { - // Compile-time null safety, no runtime checks + // Null-безопасность на этапе компиляции, без проверок во время выполнения fn process_array(array: &[i32]) -> Option<String> { array.first().map(|x| x.to_string()) - // No null references possible - // Bounds checking optimized away when provably safe + // Null-ссылки здесь невозможны + // Проверка границ убирается оптимизатором, когда безопасность доказуема } fn process_concurrently() { @@ -110,7 +110,7 @@ impl SafeOperations { let data = Arc::new(Mutex::new(Vec::new())); - // Data races prevented at compile time + // Гонки данных предотвращаются на этапе компиляции let handles: Vec<_> = (0..1000).map(|i| { let data = Arc::clone(&data); thread::spawn(move || { @@ -127,33 +127,33 @@ impl SafeOperations { *** -## Common C# Pain Points That Rust Addresses +## Типичные болевые точки C#, которые решает Rust -### 1. The Billion Dollar Mistake: Null References +### 1. «Миллиардная ошибка»: null-ссылки ```csharp -// C# - Null reference exceptions are runtime bombs +// C# — исключения NullReferenceException это мины замедленного действия во время выполнения public class UserService { public string GetUserDisplayName(User user) { - // Any of these could throw NullReferenceException + // Любое из этих обращений может бросить NullReferenceException return user.Profile.DisplayName.ToUpper(); // ^^^^^ ^^^^^^^ ^^^^^^^^^^^ ^^^^^^^ - // Could be null at runtime + // Во время выполнения любое из них может оказаться null } - // Nullable reference types (C# 8+) help, but nulls can still slip through + // Nullable-ссылочные типы (C# 8+) помогают, но null всё равно может проскочить public string GetDisplayName(User? user) { return user?.Profile?.DisplayName?.ToUpper() ?? "Unknown"; - // This specific line is null-safe thanks to ?. and ??, - // but NRTs are advisory — the compiler can be overridden with `!` + // Эта конкретная строка безопасна благодаря ?. и ??, + // но NRT носят рекомендательный характер — компилятор можно обойти с помощью `!` } } ``` ```rust -// Rust - Null safety guaranteed at compile time +// Rust — null-безопасность гарантируется на этапе компиляции struct UserService; impl UserService { @@ -161,8 +161,8 @@ impl UserService { user.profile.as_ref()? .display_name.as_ref() .map(|name| name.to_uppercase()) - // Compiler forces you to handle None case - // Impossible to have null pointer exceptions + // Компилятор заставляет обработать случай None + // Null-указатели и их исключения здесь невозможны } fn get_display_name_safe(user: Option<&User>) -> String { @@ -170,28 +170,28 @@ impl UserService { .and_then(|p| p.display_name.as_ref()) .map(|name| name.to_uppercase()) .unwrap_or_else(|| "Unknown".to_string()) - // Explicit handling, no surprises + // Явная обработка, никаких сюрпризов } } ``` -### 2. Hidden Exceptions and Control Flow +### 2. Скрытые исключения и управление потоком ```csharp -// C# - Exceptions can be thrown from anywhere +// C# — исключения могут бросаться откуда угодно public async Task<UserData> GetUserDataAsync(int userId) { - // Each of these might throw different exceptions + // Каждый из этих вызовов может бросить разные исключения var user = await userRepository.GetAsync(userId); // SqlException var permissions = await permissionService.GetAsync(user); // HttpRequestException var preferences = await preferenceService.GetAsync(user); // TimeoutException return new UserData(user, permissions, preferences); - // Caller has no idea what exceptions to expect + // Вызывающий код понятия не имеет, какие исключения ожидать } ``` ```rust -// Rust - All errors explicit in function signatures +// Rust — все ошибки явно указаны в сигнатурах функций #[derive(Debug)] enum UserDataError { DatabaseError(String), @@ -201,7 +201,7 @@ enum UserDataError { } async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> { - // All errors explicit and handled + // Все ошибки явные и обработаны let user = user_repository.get(user_id).await .map_err(UserDataError::DatabaseError)?; @@ -212,19 +212,19 @@ async fn get_user_data(user_id: i32) -> Result<UserData, UserDataError> { .map_err(|_| UserDataError::Timeout)?; Ok(UserData::new(user, permissions, preferences)) - // Caller knows exactly what errors are possible + // Вызывающий код точно знает, какие ошибки возможны } ``` -### 3. Correctness: The Type System as a Proof Engine +### 3. Корректность: система типов как движок доказательств -Rust's type system catches entire categories of logic bugs at compile time that C# can only catch at runtime — or not at all. +Система типов Rust отлавливает целые классы логических ошибок на этапе компиляции — тогда как C# может поймать их только во время выполнения, или не поймать вовсе. -#### ADTs vs Sealed-Class Workarounds +#### ADT против обходных путей через sealed-классы ```csharp -// C# — Discriminated unions require sealed-class boilerplate. -// The compiler warns about missing cases (CS8524) ONLY when there's no _ catch-all. -// In practice, most C# code uses _ as a default, which silences the warning. +// C# — дискриминируемые объединения требуют шаблонного кода с sealed-классами. +// Компилятор предупреждает о пропущенных случаях (CS8524) ТОЛЬКО если нет catch-all `_`. +// На практике большинство C#-кода использует `_` по умолчанию, и это заглушает предупреждение. public abstract record Shape; public sealed record Circle(double Radius) : Shape; public sealed record Rectangle(double W, double H) : Shape; @@ -234,15 +234,15 @@ public static double Area(Shape shape) => shape switch { Circle c => Math.PI * c.Radius * c.Radius, Rectangle r => r.W * r.H, - // Forgot Triangle? The _ catch-all silences any compiler warning. + // Забыли Triangle? Паттерн `_` заглушает любое предупреждение компилятора. _ => throw new ArgumentException("Unknown shape") }; -// Add a new variant six months later — the _ pattern hides the missing case. -// No compiler warning tells you about the 47 switch expressions you need to update. +// Через полгода добавляете новый вариант — паттерн `_` скрывает пропущенный случай. +// Ни одно предупреждение компилятора не скажет о 47 switch-выражениях, которые нужно обновить. ``` ```rust -// Rust — ADTs + exhaustive matching = compile-time proof +// Rust — ADT + исчерпывающее сопоставление = доказательство на этапе компиляции enum Shape { Circle { radius: f64 }, Rectangle { w: f64, h: f64 }, @@ -253,36 +253,36 @@ fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { w, h } => w * h, - // Forget Triangle? ERROR: non-exhaustive pattern + // Забыли Triangle? ОШИБКА: сопоставление не исчерпывающее Shape::Triangle { a, b, c } => { let s = (a + b + c) / 2.0; (s * (s - a) * (s - b) * (s - c)).sqrt() } } } -// Add a new variant → compiler shows you EVERY match that needs updating. +// Добавляете новый вариант → компилятор покажет ВСЕ сопоставления, которые нужно обновить. ``` -#### Immutability by Default vs Opt-In Immutability +#### Неизменяемость по умолчанию против неизменяемости по выбору ```csharp -// C# — Everything is mutable by default +// C# — всё изменяемо по умолчанию public class Config { - public string Host { get; set; } // Mutable by default + public string Host { get; set; } // Изменяемо по умолчанию public int Port { get; set; } } -// "readonly" and "record" help, but don't prevent deep mutation: +// "readonly" и "record" помогают, но не предотвращают глубокую мутацию: public record ServerConfig(string Host, int Port, List<string> AllowedOrigins); var config = new ServerConfig("localhost", 8080, new List<string> { "*.example.com" }); -// Records are "immutable" but reference-type fields are NOT: -config.AllowedOrigins.Add("*.evil.com"); // Compiles and mutates! ← bug -// The compiler gives you no warning. +// Records «неизменяемы», но поля ссылочных типов — НЕТ: +config.AllowedOrigins.Add("*.evil.com"); // Компилируется и мутирует! ← баг +// Компилятор не выдаёт ни одного предупреждения. ``` ```rust -// Rust — Immutable by default, mutation is explicit and visible +// Rust — неизменяемо по умолчанию, мутация явная и видимая struct Config { host: String, port: u16, @@ -295,57 +295,57 @@ let config = Config { allowed_origins: vec!["*.example.com".into()], }; -// config.allowed_origins.push("*.evil.com".into()); // ERROR: cannot borrow as mutable +// config.allowed_origins.push("*.evil.com".into()); // ОШИБКА: нельзя заимствовать как изменяемое -// Mutation requires explicit opt-in: +// Мутация требует явного согласия: let mut config = config; -config.allowed_origins.push("*.safe.com".into()); // OK — visibly mutable +config.allowed_origins.push("*.safe.com".into()); // OK — мутация видна -// "mut" in the signature tells every reader: "this function modifies data" +// «mut» в сигнатуре говорит каждому читателю: «эта функция изменяет данные» fn add_origin(config: &mut Config, origin: String) { config.allowed_origins.push(origin); } ``` -#### Functional Programming: First-Class vs Afterthought +#### Функциональное программирование: полноправная возможность или запоздалая мысль ```csharp -// C# — FP bolted on; LINQ is expressive but the language fights you +// C# — ФП прикручено сбоку; LINQ выразителен, но язык сопротивляется public IEnumerable<Order> GetHighValueOrders(IEnumerable<Order> orders) { return orders - .Where(o => o.Total > 1000) // Func<Order, bool> — heap-allocated delegate - .Select(o => new OrderSummary // Anonymous type or extra class + .Where(o => o.Total > 1000) // Func<Order, bool> — делегат в куче + .Select(o => new OrderSummary // Анонимный тип или дополнительный класс { Id = o.Id, Total = o.Total }) .OrderByDescending(o => o.Total); - // No exhaustive matching on results - // Null can sneak in anywhere in the pipeline - // Can't enforce purity — any lambda might have side effects + // Нет исчерпывающего сопоставления результатов + // Null может проскользнуть куда угодно в конвейере + // Нельзя обеспечить чистоту — любая лямбда может иметь побочные эффекты } ``` ```rust -// Rust — FP is a first-class citizen +// Rust — ФП является полноправной частью языка fn get_high_value_orders(orders: &[Order]) -> Vec<OrderSummary> { orders.iter() - .filter(|o| o.total > 1000) // Zero-cost closure, no heap allocation - .map(|o| OrderSummary { // Type-checked struct + .filter(|o| o.total > 1000) // Замыкание с нулевой стоимостью, без выделения в куче + .map(|o| OrderSummary { // Структура с проверкой типов id: o.id, total: o.total, }) .sorted_by(|a, b| b.total.cmp(&a.total)) // itertools .collect() - // No nulls anywhere in the pipeline - // Closures are monomorphized — zero overhead vs hand-written loops - // Purity enforced: &[Order] means the function CAN'T modify orders + // Нигде в конвейере нет null + // Замыкания мономорфизируются — нулевые накладные расходы по сравнению с написанными вручную циклами + // Чистота обеспечивается: &[Order] означает, что функция НЕ может изменить orders } ``` -#### Inheritance: Elegant in Theory, Fragile in Practice +#### Наследование: элегантно в теории, хрупко на практике ```csharp -// C# — The fragile base class problem +// C# — проблема хрупкого базового класса public class Animal { public virtual string Speak() => "..."; @@ -359,20 +359,20 @@ public class Dog : Animal public class RobotDog : Dog { - // Which Speak() does Greet() call? What if Dog changes? - // Diamond problem with interfaces + default methods - // Tight coupling: changing Animal can break RobotDog silently + // Какой Speak() вызовет Greet()? Что если Dog изменится? + // Ромбовидная проблема с интерфейсами + методами по умолчанию + // Жёсткая связанность: изменение Animal может тихо сломать RobotDog } -// Common C# anti-patterns: -// - God base classes with 20 virtual methods -// - Deep hierarchies (5+ levels) nobody can reason about -// - "protected" fields creating hidden coupling -// - Base class changes silently altering derived behavior +// Распространённые антипаттерны C#: +// - «Божественные» базовые классы с 20 виртуальными методами +// - Глубокие иерархии (5+ уровней), в которых никто не может разобраться +// - Поля "protected", создающие скрытую связанность +// - Изменения базового класса, которые молча меняют поведение производных ``` ```rust -// Rust — Composition over inheritance, enforced by the language +// Rust — композиция вместо наследования, обеспечивается самим языком trait Speaker { fn speak(&self) -> &str; } @@ -387,149 +387,149 @@ struct Dog; impl Speaker for Dog { fn speak(&self) -> &str { "Woof!" } } -impl Greeter for Dog {} // Uses default greet() +impl Greeter for Dog {} // Использует greet() по умолчанию struct RobotDog { - voice: String, // Composition: owns its own data + voice: String, // Композиция: владеет собственными данными } impl Speaker for RobotDog { fn speak(&self) -> &str { &self.voice } } -impl Greeter for RobotDog {} // Clear, explicit behavior +impl Greeter for RobotDog {} // Понятное и явное поведение -// No fragile base class problem — no base classes at all -// No hidden coupling — traits are explicit contracts -// No diamond problem — trait coherence rules prevent ambiguity -// Adding a method to Speaker? Compiler tells you everywhere to implement it. +// Нет проблемы хрупкого базового класса — базовых классов вообще нет +// Нет скрытой связанности — трейты это явные контракты +// Нет ромбовидной проблемы — правила согласованности трейтов исключают неоднозначность +// Добавляете метод в Speaker? Компилятор покажет все места, где его нужно реализовать. ``` -> **Key insight**: In C#, correctness is a discipline — you hope developers -> follow conventions, write tests, and catch edge cases in code review. -> In Rust, correctness is a **property of the type system** — entire -> categories of bugs (null derefs, forgotten variants, accidental mutation, -> data races) are structurally impossible. +> **Ключевая мысль**: в C# корректность — это дисциплина: вы надеетесь, что разработчики +> следуют соглашениям, пишут тесты и ловят пограничные случаи на код-ревью. +> В Rust корректность — **свойство системы типов**: целые классы ошибок +> (разыменование null, забытые варианты, случайная мутация, +> гонки данных) структурно невозможны. *** -### 4. Unpredictable Performance Due to GC +### 4. Непредсказуемая производительность из-за GC ```csharp -// C# - GC can pause at any time +// C# — GC может приостановить работу в любой момент public class HighFrequencyTrader { private List<Trade> trades = new List<Trade>(); public void ProcessMarketData(MarketTick tick) { - // Allocations can trigger GC at worst possible moment + // Выделения памяти могут запустить GC в худший возможный момент var analysis = new MarketAnalysis(tick); trades.Add(new Trade(analysis.Signal, tick.Price)); - // GC might pause here during critical market moment - // Pause duration: 1-100ms depending on heap size + // GC может приостановить работу прямо здесь, в критический момент рынка + // Длительность паузы: 1–100 мс в зависимости от размера кучи } } ``` ```rust -// Rust - Predictable, deterministic performance +// Rust — предсказуемая, детерминированная производительность struct HighFrequencyTrader { trades: Vec<Trade>, } impl HighFrequencyTrader { fn process_market_data(&mut self, tick: MarketTick) { - // Extract Copy field before moving `tick` into analysis + // Извлекаем поле Copy до того, как переместим `tick` в analysis let price = tick.price; - // Zero allocations, predictable performance + // Нулевые выделения, предсказуемая производительность let analysis = MarketAnalysis::from(tick); self.trades.push(Trade::new(analysis.signal(), price)); - // No GC pauses, consistent sub-microsecond latency - // Performance guaranteed by type system + // Никаких пауз GC, стабильная задержка на уровне субмикросекунд + // Производительность гарантируется системой типов } } ``` *** -## When to Choose Rust Over C# - -### ✅ Choose Rust When: -- **Correctness matters**: State machines, protocol implementations, financial logic — where a missed case is a production incident, not a test failure -- **Performance is critical**: Real-time systems, high-frequency trading, game engines -- **Memory usage matters**: Embedded systems, cloud costs, mobile applications -- **Predictability required**: Medical devices, automotive, financial systems -- **Security is paramount**: Cryptography, network security, system-level code -- **Long-running services**: Where GC pauses cause issues -- **Resource-constrained environments**: IoT, edge computing -- **System programming**: CLI tools, databases, web servers, operating systems - -### ✅ Stay with C# When: -- **Rapid application development**: Business applications, CRUD applications -- **Large existing codebase**: When migration cost is prohibitive -- **Team expertise**: When Rust learning curve doesn't justify benefits -- **Enterprise integrations**: Heavy .NET Framework/Windows dependencies -- **GUI applications**: WPF, WinUI, Blazor ecosystems -- **Time to market**: When development speed trumps performance - -### 🔄 Consider Both (Hybrid Approach): -- **Performance-critical components in Rust**: Called from C# via P/Invoke -- **Business logic in C#**: Familiar, productive development -- **Gradual migration**: Start with new services in Rust +## Когда выбирать Rust вместо C# + +### ✅ Выбирайте Rust, когда: +- **Важна корректность**: конечные автоматы, реализации протоколов, финансовая логика — там, где пропущенный случай становится инцидентом в продакшене, а не провалом теста +- **Критична производительность**: системы реального времени, высокочастотная торговля, игровые движки +- **Важно потребление памяти**: встраиваемые системы, облачные затраты, мобильные приложения +- **Требуется предсказуемость**: медицинское оборудование, автомобильная отрасль, финансовые системы +- **Безопасность превыше всего**: криптография, сетевая безопасность, низкоуровневый код +- **Долгоживущие сервисы**: там, где паузы GC создают проблемы +- **Ограниченные ресурсы**: IoT, edge-вычисления +- **Системное программирование**: CLI-утилиты, базы данных, веб-серверы, операционные системы + +### ✅ Оставайтесь на C#, когда: +- **Быстрая разработка приложений**: бизнес-приложения, CRUD-приложения +- **Большая существующая кодовая база**: когда стоимость миграции непомерна +- **Экспертиза команды**: когда кривая обучения Rust не окупается выгодами +- **Корпоративные интеграции**: сильные зависимости от .NET Framework/Windows +- **GUI-приложения**: экосистемы WPF, WinUI, Blazor +- **Скорость выхода на рынок**: когда скорость разработки важнее производительности + +### 🔄 Рассмотрите оба варианта (гибридный подход): +- **Компоненты, критичные к производительности, на Rust**: вызываются из C# через P/Invoke +- **Бизнес-логика на C#**: знакомая и продуктивная разработка +- **Постепенная миграция**: начните с новых сервисов на Rust *** -## Real-World Impact: Why Companies Choose Rust +## Влияние на практике: почему компании выбирают Rust -### Dropbox: Storage Infrastructure -- **Before (Python)**: High CPU usage, memory overhead -- **After (Rust)**: 10x performance improvement, 50% memory reduction -- **Result**: Millions saved in infrastructure costs +### Dropbox: инфраструктура хранения +- **До (Python)**: высокая загрузка CPU, накладные расходы памяти +- **После (Rust)**: прирост производительности в 10 раз, снижение потребления памяти на 50% +- **Результат**: миллионы экономии на инфраструктуре -### Discord: Voice/Video Backend -- **Before (Go)**: GC pauses causing audio drops -- **After (Rust)**: Consistent low-latency performance -- **Result**: Better user experience, reduced server costs +### Discord: бэкенд голоса и видео +- **До (Go)**: паузы GC, приводившие к обрывам аудио +- **После (Rust)**: стабильно низкая задержка +- **Результат**: лучший пользовательский опыт, меньше затрат на серверы -### Microsoft: Windows Components -- **Rust in Windows**: File system, networking stack components -- **Benefit**: Memory safety without performance cost -- **Impact**: Fewer security vulnerabilities, same performance +### Microsoft: компоненты Windows +- **Rust в Windows**: компоненты файловой системы и сетевого стека +- **Преимущество**: безопасность памяти без потери производительности +- **Эффект**: меньше уязвимостей безопасности при той же производительности -### Why This Matters for C# Developers: -1. **Complementary skills**: Rust and C# solve different problems -2. **Career growth**: Systems programming expertise increasingly valuable -3. **Performance understanding**: Learn zero-cost abstractions -4. **Safety mindset**: Apply ownership thinking to any language -5. **Cloud costs**: Performance directly impacts infrastructure spend +### Почему это важно для разработчиков C#: +1. **Взаимодополняющие навыки**: Rust и C# решают разные задачи +2. **Карьерный рост**: экспертиза в системном программировании всё более ценна +3. **Понимание производительности**: изучение абстракций с нулевой стоимостью +4. **Мышление о безопасности**: применяйте мышление о владении в любом языке +5. **Облачные затраты**: производительность напрямую влияет на расходы на инфраструктуру *** -## Language Philosophy Comparison +## Сравнение философий языков -### C# Philosophy -- **Productivity first**: Rich tooling, extensive framework, "pit of success" -- **Managed runtime**: Garbage collection handles memory automatically -- **Enterprise-focused**: Strong typing with reflection, extensive standard library -- **Object-oriented**: Classes, inheritance, interfaces as primary abstractions +### Философия C# +- **Продуктивность прежде всего**: богатые инструменты, обширный фреймворк, «яма успеха» (pit of success) +- **Управляемый рантайм**: сборщик мусора автоматически управляет памятью +- **Ориентация на корпоративный сегмент**: строгая типизация с рефлексией, обширная стандартная библиотека +- **Объектно-ориентированность**: классы, наследование, интерфейсы как основные абстракции -### Rust Philosophy -- **Performance without sacrifice**: Zero-cost abstractions, no runtime overhead -- **Memory safety**: Compile-time guarantees prevent crashes and security vulnerabilities -- **Systems programming**: Direct hardware access with high-level abstractions -- **Functional + systems**: Immutability by default, ownership-based resource management +### Философия Rust +- **Производительность без компромиссов**: абстракции с нулевой стоимостью, без накладных расходов рантайма +- **Безопасность памяти**: гарантии на этапе компиляции предотвращают сбои и уязвимости безопасности +- **Системное программирование**: прямой доступ к оборудованию с высокоуровневыми абстракциями +- **Функциональность + системность**: неизменяемость по умолчанию, управление ресурсами на основе владения ```mermaid graph TD - subgraph "C# Development Model" - CS_CODE["C# Source Code<br/>Classes, Methods, Properties"] - CS_COMPILE["C# Compiler<br/>(csc.exe)"] - CS_IL["Intermediate Language<br/>(IL bytecode)"] + subgraph "Модель разработки C#" + CS_CODE["Исходный код C#<br/>Классы, методы, свойства"] + CS_COMPILE["Компилятор C#<br/>(csc.exe)"] + CS_IL["Промежуточный язык<br/>(байт-код IL)"] CS_RUNTIME[".NET Runtime<br/>(CLR)"] - CS_JIT["Just-In-Time Compiler"] - CS_NATIVE["Native Machine Code"] - CS_GC["Garbage Collector<br/>(Memory management)"] + CS_JIT["Just-In-Time компилятор"] + CS_NATIVE["Машинный код"] + CS_GC["Сборщик мусора<br/>(управление памятью)"] CS_CODE --> CS_COMPILE CS_COMPILE --> CS_IL @@ -538,20 +538,20 @@ graph TD CS_JIT --> CS_NATIVE CS_RUNTIME --> CS_GC - CS_BENEFITS["[OK] Fast development<br/>[OK] Rich ecosystem<br/>[OK] Automatic memory management<br/>[ERROR] Runtime overhead<br/>[ERROR] GC pauses<br/>[ERROR] Platform dependency"] + CS_BENEFITS["[OK] Быстрая разработка<br/>[OK] Богатая экосистема<br/>[OK] Автоматическое управление памятью<br/>[ERROR] Накладные расходы рантайма<br/>[ERROR] Паузы GC<br/>[ERROR] Зависимость от платформы"] end - subgraph "Rust Development Model" - RUST_CODE["Rust Source Code<br/>Structs, Enums, Functions"] - RUST_COMPILE["Rust Compiler<br/>(rustc)"] - RUST_NATIVE["Native Machine Code<br/>(Direct compilation)"] - RUST_ZERO["Zero Runtime<br/>(No VM, No GC)"] + subgraph "Модель разработки Rust" + RUST_CODE["Исходный код Rust<br/>Структуры, перечисления, функции"] + RUST_COMPILE["Компилятор Rust<br/>(rustc)"] + RUST_NATIVE["Машинный код<br/>(прямая компиляция)"] + RUST_ZERO["Нулевой рантайм<br/>(нет VM, нет GC)"] RUST_CODE --> RUST_COMPILE RUST_COMPILE --> RUST_NATIVE RUST_NATIVE --> RUST_ZERO - RUST_BENEFITS["[OK] Maximum performance<br/>[OK] Memory safety<br/>[OK] No runtime dependencies<br/>[ERROR] Steeper learning curve<br/>[ERROR] Longer compile times<br/>[ERROR] More explicit code"] + RUST_BENEFITS["[OK] Максимальная производительность<br/>[OK] Безопасность памяти<br/>[OK] Нет зависимостей рантайма<br/>[ERROR] Более крутая кривая обучения<br/>[ERROR] Более долгая сборка<br/>[ERROR] Более явный код"] end style CS_BENEFITS fill:#e3f2fd,color:#000 @@ -562,22 +562,20 @@ graph TD *** -## Quick Reference: Rust vs C# +## Краткая справка: Rust против C# -| **Concept** | **C#** | **Rust** | **Key Difference** | +| **Концепция** | **C#** | **Rust** | **Ключевое отличие** | |-------------|--------|----------|-------------------| -| Memory management | Garbage collector | Ownership system | Zero-cost, deterministic cleanup | -| Null references | `null` everywhere | `Option<T>` | Compile-time null safety | -| Error handling | Exceptions | `Result<T, E>` | Explicit, no hidden control flow | -| Mutability | Mutable by default | Immutable by default | Opt-in to mutation | -| Type system | Reference/value types | Ownership types | Move semantics, borrowing | -| Assemblies | GAC, app domains (.NET Framework); side-by-side (.NET 5+) | Crates | Static linking, no runtime | -| Namespaces | `using System.IO` | `use std::fs` | Module system | -| Interfaces | `interface IFoo` | `trait Foo` | Default implementations | -| Generics | `List<T>` (optional constraints via `where`) | `Vec<T>` (trait bounds like `T: Clone`) | Zero-cost abstractions | -| Threading | locks, async/await | Ownership + Send/Sync | Data race prevention | -| Performance | JIT compilation | AOT compilation | Predictable, no GC pauses | +| Управление памятью | Сборщик мусора | Система владения | Нулевая стоимость, детерминированная очистка | +| Null-ссылки | `null` повсюду | `Option<T>` | Null-безопасность на этапе компиляции | +| Обработка ошибок | Исключения | `Result<T, E>` | Явно, без скрытого управления потоком | +| Изменяемость | Изменяемо по умолчанию | Неизменяемо по умолчанию | Мутация по явному выбору | +| Система типов | Ссылочные/значимые типы | Типы владения | Семантика перемещения, заимствование | +| Сборки | GAC, домены приложений (.NET Framework); side-by-side (.NET 5+) | Крейты | Статическая линковка, без рантайма | +| Пространства имён | `using System.IO` | `use std::fs` | Система модулей | +| Интерфейсы | `interface IFoo` | `trait Foo` | Реализации по умолчанию | +| Обобщения | `List<T>` (необязательные ограничения через `where`) | `Vec<T>` (ограничения трейтов вида `T: Clone`) | Абстракции с нулевой стоимостью | +| Многопоточность | Блокировки, async/await | Владение + Send/Sync | Предотвращение гонок данных | +| Производительность | JIT-компиляция | AOT-компиляция | Предсказуемо, без пауз GC | *** - - diff --git a/csharp-book/src/ch02-1-essential-keywords-reference.md b/csharp-book/src/ch02-1-essential-keywords-reference.md index 83424bf..92f64dd 100644 --- a/csharp-book/src/ch02-1-essential-keywords-reference.md +++ b/csharp-book/src/ch02-1-essential-keywords-reference.md @@ -1,97 +1,97 @@ -## Essential Rust Keywords for C# Developers +## Ключевые слова Rust для разработчиков C# -> **What you'll learn:** A quick-reference mapping of Rust keywords to their C# equivalents — -> visibility modifiers, ownership keywords, control flow, type definitions, and pattern matching syntax. +> **Что вы узнаете:** краткое соответствие ключевых слов Rust их аналогам в C# — +> модификаторы видимости, ключевые слова владения, управление потоком, определения типов и синтаксис сопоставления с образцом. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -Understanding Rust's keywords and their purposes helps C# developers navigate the language more effectively. +Понимание ключевых слов Rust и их назначения помогает разработчикам C# эффективнее осваивать язык. -### Visibility and Access Control Keywords +### Ключевые слова видимости и контроля доступа -#### C# Access Modifiers +#### Модификаторы доступа C# ```csharp public class Example { - public int PublicField; // Accessible everywhere - private int privateField; // Only within this class - protected int protectedField; // This class and subclasses - internal int internalField; // Within this assembly - protected internal int protectedInternalField; // Combination + public int PublicField; // Доступно везде + private int privateField; // Только внутри этого класса + protected int protectedField; // Этот класс и его наследники + internal int internalField; // Внутри этой сборки + protected internal int protectedInternalField; // Комбинация } ``` -#### Rust Visibility Keywords +#### Ключевые слова видимости Rust ```rust -// pub - Makes items public (like C# public) +// pub - делает элементы публичными (как public в C#) pub struct PublicStruct { - pub public_field: i32, // Public field - private_field: i32, // Private by default (no keyword) + pub public_field: i32, // Публичное поле + private_field: i32, // По умолчанию приватное (без ключевого слова) } pub mod my_module { - pub(crate) fn crate_public() {} // Public within current crate (like internal) - pub(super) fn parent_public() {} // Public to parent module - pub(self) fn self_public() {} // Public within current module (same as private) + pub(crate) fn crate_public() {} // Публично внутри текущего крейта (как internal) + pub(super) fn parent_public() {} // Публично для родительского модуля + pub(self) fn self_public() {} // Публично внутри текущего модуля (то же, что и приватное) - pub use super::PublicStruct; // Re-export (like using alias) + pub use super::PublicStruct; // Реэкспорт (как псевдоним через using) } -// No direct equivalent to C# protected - use composition instead +// Прямого аналога protected в C# нет — используйте композицию ``` -### Memory and Ownership Keywords +### Ключевые слова памяти и владения -#### C# Memory Keywords +#### Ключевые слова памяти C# ```csharp -// ref - Pass by reference +// ref - передача по ссылке public void Method(ref int value) { value = 10; } -// out - Output parameter +// out - выходной параметр public bool TryParse(string input, out int result) { /* */ } -// in - Readonly reference (C# 7.2+) -public void ReadOnly(in LargeStruct data) { /* Cannot modify data */ } +// in - ссылка только для чтения (C# 7.2+) +public void ReadOnly(in LargeStruct data) { /* Нельзя изменить data */ } ``` -#### Rust Ownership Keywords +#### Ключевые слова владения Rust ```rust -// & - Immutable reference (like C# in parameter) +// & - неизменяемая ссылка (как in-параметр в C#) fn read_only(data: &Vec<i32>) { - println!("Length: {}", data.len()); // Can read, cannot modify + println!("Длина: {}", data.len()); // Можно читать, нельзя изменять } -// &mut - Mutable reference (like C# ref parameter) +// &mut - изменяемая ссылка (как ref-параметр в C#) fn modify(data: &mut Vec<i32>) { - data.push(42); // Can modify + data.push(42); // Можно изменять } -// move - Force move capture in closures +// move - принудительный захват перемещением в замыканиях let data = vec![1, 2, 3]; let closure = move || { - println!("{:?}", data); // data is moved into closure + println!("{:?}", data); // data перемещается в замыкание }; -// data is no longer accessible here +// data здесь больше недоступен -// Box - Heap allocation (like C# new for reference types) -let boxed_data = Box::new(42); // Allocate on heap +// Box - выделение в куче (как new для ссылочных типов в C#) +let boxed_data = Box::new(42); // Выделяем в куче ``` -### Control Flow Keywords +### Ключевые слова управления потоком -#### C# Control Flow +#### Управление потоком C# ```csharp -// return - Exit function with value +// return - выход из функции со значением public int GetValue() { return 42; } -// yield return - Iterator pattern +// yield return - паттерн итератора public IEnumerable<int> GetNumbers() { yield return 1; yield return 2; } -// break/continue - Loop control +// break/continue - управление циклом foreach (var item in items) { if (item == null) continue; @@ -99,89 +99,89 @@ foreach (var item in items) } ``` -#### Rust Control Flow Keywords +#### Ключевые слова управления потоком Rust ```rust -// return - Explicit return (usually not needed) +// return - явный возврат (обычно не нужен) fn get_value() -> i32 { - return 42; // Explicit return - // OR just: 42 (implicit return) + return 42; // Явный возврат + // ИЛИ просто: 42 (неявный возврат) } -// break/continue - Loop control with optional values +// break/continue - управление циклом с необязательными значениями fn find_value() -> Option<i32> { loop { let value = get_next(); if value < 0 { continue; } - if value > 100 { break None; } // Break with value - if value == 42 { break Some(value); } // Break with success + if value > 100 { break None; } // Выход с значением + if value == 42 { break Some(value); } // Выход с успешным результатом } } -// loop - Infinite loop (like while(true)) +// loop - бесконечный цикл (как while(true)) loop { if condition { break; } } -// while - Conditional loop +// while - условный цикл while condition { - // code + // код } -// for - Iterator loop +// for - цикл по итератору for item in collection { - // code + // код } ``` -### Type Definition Keywords +### Ключевые слова определения типов -#### C# Type Keywords +#### Ключевые слова типов C# ```csharp -// class - Reference type +// class - ссылочный тип public class MyClass { } -// struct - Value type +// struct - значимый тип public struct MyStruct { } -// interface - Contract definition +// interface - определение контракта public interface IMyInterface { } -// enum - Enumeration +// enum - перечисление public enum MyEnum { Value1, Value2 } -// delegate - Function pointer +// delegate - указатель на функцию public delegate void MyDelegate(int value); ``` -#### Rust Type Keywords +#### Ключевые слова типов Rust ```rust -// struct - Data structure (like C# class/struct combined) +// struct - структура данных (объединяет class и struct из C#) struct MyStruct { field: i32, } -// enum - Algebraic data type (much more powerful than C# enum) +// enum - алгебраический тип данных (гораздо мощнее enum из C#) enum MyEnum { Variant1, - Variant2(i32), // Can hold data - Variant3 { x: i32, y: i32 }, // Struct-like variant + Variant2(i32), // Может хранить данные + Variant3 { x: i32, y: i32 }, // Вариант в виде структуры } -// trait - Interface definition (like C# interface but more powerful) +// trait - определение интерфейса (как interface в C#, но мощнее) trait MyTrait { fn method(&self); - // Default implementation (like C# 8+ default interface methods) + // Реализация по умолчанию (как default-методы интерфейсов в C# 8+) fn default_method(&self) { - println!("Default implementation"); + println!("Реализация по умолчанию"); } } -// type - Type alias (like C# using alias) +// type - псевдоним типа (как псевдоним using в C#) type UserId = u32; type Result<T> = std::result::Result<T, MyError>; -// impl - Implementation block (no C# equivalent - methods defined separately) +// impl - блок реализации (аналога в C# нет — методы определяются отдельно) impl MyStruct { fn new() -> MyStruct { MyStruct { field: 0 } @@ -190,220 +190,219 @@ impl MyStruct { impl MyTrait for MyStruct { fn method(&self) { - println!("Implementation"); + println!("Реализация"); } } ``` -### Function Definition Keywords +### Ключевые слова определения функций -#### C# Function Keywords +#### Ключевые слова функций C# ```csharp -// static - Class method +// static - метод класса public static void StaticMethod() { } -// virtual - Can be overridden +// virtual - может быть переопределён public virtual void VirtualMethod() { } -// override - Override base method +// override - переопределение базового метода public override void VirtualMethod() { } -// abstract - Must be implemented +// abstract - обязан быть реализован public abstract void AbstractMethod(); -// async - Asynchronous method +// async - асинхронный метод public async Task<int> AsyncMethod() { return await SomeTask(); } ``` -#### Rust Function Keywords +#### Ключевые слова функций Rust ```rust -// fn - Function definition (like C# method but standalone) +// fn - определение функции (как метод в C#, но существует самостоятельно) fn regular_function() { - println!("Hello"); + println!("Привет"); } -// const fn - Compile-time function (like C# const but for functions) +// const fn - функция времени компиляции (как const в C#, но для функций) const fn compile_time_function() -> i32 { - 42 // Can be evaluated at compile time + 42 // Может быть вычислена на этапе компиляции } -// async fn - Asynchronous function (like C# async) +// async fn - асинхронная функция (как async в C#) async fn async_function() -> i32 { some_async_operation().await } -// unsafe fn - Function that may violate memory safety +// unsafe fn - функция, которая может нарушить безопасность памяти unsafe fn unsafe_function() { - // Can perform unsafe operations + // Может выполнять небезопасные операции } -// extern fn - Foreign function interface +// extern fn - интерфейс внешних функций (FFI) extern "C" fn c_compatible_function() { - // Can be called from C + // Может вызываться из C } ``` -### Variable Declaration Keywords +### Ключевые слова объявления переменных -#### C# Variable Keywords +#### Ключевые слова переменных C# ```csharp -// var - Type inference -var name = "John"; // Inferred as string +// var - вывод типа +var name = "John"; // Выводится как string -// const - Compile-time constant +// const - константа времени компиляции const int MaxSize = 100; -// readonly - Runtime constant (fields only, not local variables) +// readonly - константа времени выполнения (только для полей, не для локальных переменных) // readonly DateTime createdAt = DateTime.Now; -// static - Class-level variable +// static - переменная уровня класса static int instanceCount = 0; ``` -#### Rust Variable Keywords +#### Ключевые слова переменных Rust ```rust -// let - Variable binding (like C# var) -let name = "John"; // Immutable by default +// let - привязка переменной (как var в C#) +let name = "John"; // По умолчанию неизменяемая -// let mut - Mutable variable binding -let mut count = 0; // Can be changed +// let mut - изменяемая привязка переменной +let mut count = 0; // Может быть изменена count += 1; -// const - Compile-time constant (like C# const) +// const - константа времени компиляции (как const в C#) const MAX_SIZE: usize = 100; -// static - Global variable (like C# static) +// static - глобальная переменная (как static в C#) static INSTANCE_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); ``` -### Pattern Matching Keywords +### Ключевые слова сопоставления с образцом -#### C# Pattern Matching (C# 8+) +#### Сопоставление с образцом C# (C# 8+) ```csharp -// switch expression +// switch-выражение string result = value switch { - 1 => "One", - 2 => "Two", - _ => "Other" + 1 => "Один", + 2 => "Два", + _ => "Другое" }; -// is pattern +// паттерн is if (obj is string str) { Console.WriteLine(str.Length); } ``` -#### Rust Pattern Matching Keywords +#### Ключевые слова сопоставления с образцом Rust ```rust -// match - Pattern matching (like C# switch but much more powerful) +// match - сопоставление с образцом (как switch в C#, но гораздо мощнее) let result = match value { - 1 => "One", - 2 => "Two", - 3..=10 => "Between 3 and 10", // Range patterns - _ => "Other", // Wildcard (like C# _) + 1 => "Один", + 2 => "Два", + 3..=10 => "От 3 до 10", // Диапазонные паттерны + _ => "Другое", // Подстановочный паттерн (как _ в C#) }; -// if let - Conditional pattern matching +// if let - условное сопоставление с образцом if let Some(value) = optional { - println!("Got value: {}", value); + println!("Получено значение: {}", value); } -// while let - Loop with pattern matching +// while let - цикл с сопоставлением с образцом while let Some(item) = iterator.next() { - println!("Item: {}", item); + println!("Элемент: {}", item); } -// let with patterns - Destructuring -let (x, y) = point; // Destructure tuple +// let с паттернами - деструктуризация +let (x, y) = point; // Деструктуризация кортежа let Some(value) = optional else { - return; // Early return if pattern doesn't match + return; // Ранний выход, если паттерн не совпал }; ``` -### Memory Safety Keywords +### Ключевые слова безопасности памяти -#### C# Memory Keywords +#### Ключевые слова памяти C# ```csharp -// unsafe - Disable safety checks +// unsafe - отключение проверок безопасности unsafe { int* ptr = &variable; *ptr = 42; } -// fixed - Pin managed memory +// fixed - закрепление управляемой памяти unsafe { fixed (byte* ptr = array) { - // Use ptr + // Использовать ptr } } ``` -#### Rust Safety Keywords +#### Ключевые слова безопасности Rust ```rust -// unsafe - Disable borrow checker (use sparingly!) +// unsafe - отключение проверок заимствований (используйте осторожно!) unsafe { let ptr = &variable as *const i32; - let value = *ptr; // Dereference raw pointer + let value = *ptr; // Разыменование сырого указателя } -// Raw pointer types (no C# equivalent - usually not needed) -let ptr: *const i32 = &42; // Immutable raw pointer -let ptr: *mut i32 = &mut 42; // Mutable raw pointer +// Типы сырых указателей (аналога в C# нет — обычно не нужны) +let ptr: *const i32 = &42; // Неизменяемый сырой указатель +let ptr: *mut i32 = &mut 42; // Изменяемый сырой указатель ``` -### Common Rust Keywords Not in C# +### Распространённые ключевые слова Rust, которых нет в C# ```rust -// where - Generic constraints (more flexible than C# where) +// where - ограничения обобщений (гибче, чем where в C#) fn generic_function<T>() where T: Clone + Send + Sync, { - // T must implement Clone, Send, and Sync traits + // T должен реализовывать трейты Clone, Send и Sync } -// dyn - Dynamic trait objects (like C# object but type-safe) +// dyn - динамические трейт-объекты (как object в C#, но с проверкой типов) let drawable: Box<dyn Draw> = Box::new(Circle::new()); -// Self - Refer to the implementing type (like C# this but for types) +// Self - ссылка на реализующий тип (как this в C#, но для типов) impl MyStruct { fn new() -> Self { // Self = MyStruct Self { field: 0 } } } -// self - Method receiver +// self - получатель метода impl MyStruct { - fn method(&self) { } // Immutable borrow - fn method_mut(&mut self) { } // Mutable borrow - fn consume(self) { } // Take ownership + fn method(&self) { } // Неизменяемое заимствование + fn method_mut(&mut self) { } // Изменяемое заимствование + fn consume(self) { } // Принять владение } -// crate - Refer to current crate root -use crate::models::User; // Absolute path from crate root +// crate - ссылка на корень текущего крейта +use crate::models::User; // Абсолютный путь от корня крейта -// super - Refer to parent module -use super::utils; // Import from parent module +// super - ссылка на родительский модуль +use super::utils; // Импорт из родительского модуля ``` -### Keywords Summary for C# Developers +### Сводка ключевых слов для разработчиков C# -| Purpose | C# | Rust | Key Difference | +| Назначение | C# | Rust | Ключевое отличие | |---------|----|----|----------------| -| Visibility | `public`, `private`, `internal` | `pub`, default private | More granular with `pub(crate)` | -| Variables | `var`, `readonly`, `const` | `let`, `let mut`, `const` | Immutable by default | -| Functions | `method()` | `fn` | Standalone functions | -| Types | `class`, `struct`, `interface` | `struct`, `enum`, `trait` | Enums are algebraic types | -| Generics | `<T> where T : IFoo` | `<T> where T: Foo` | More flexible constraints | -| References | `ref`, `out`, `in` | `&`, `&mut` | Compile-time borrow checking | -| Patterns | `switch`, `is` | `match`, `if let` | Exhaustive matching required | +| Видимость | `public`, `private`, `internal` | `pub`, по умолчанию приватное | Более тонкая настройка через `pub(crate)` | +| Переменные | `var`, `readonly`, `const` | `let`, `let mut`, `const` | Неизменяемость по умолчанию | +| Функции | `method()` | `fn` | Функции могут существовать самостоятельно | +| Типы | `class`, `struct`, `interface` | `struct`, `enum`, `trait` | Перечисления — алгебраические типы | +| Обобщения | `<T> where T : IFoo` | `<T> where T: Foo` | Более гибкие ограничения | +| Ссылки | `ref`, `out`, `in` | `&`, `&mut` | Проверка заимствований на этапе компиляции | +| Паттерны | `switch`, `is` | `match`, `if let` | Требуется исчерпывающее сопоставление | *** - diff --git a/csharp-book/src/ch02-getting-started.md b/csharp-book/src/ch02-getting-started.md index cad65b0..799943a 100644 --- a/csharp-book/src/ch02-getting-started.md +++ b/csharp-book/src/ch02-getting-started.md @@ -1,45 +1,45 @@ -## Installation and Setup +## Установка и настройка -> **What you'll learn:** How to install Rust and set up your IDE, the Cargo build system vs MSBuild/NuGet, -> your first Rust program compared to C#, and how to read command-line input. +> **Что вы узнаете:** как установить Rust и настроить IDE, чем система сборки Cargo отличается от MSBuild/NuGet, +> как выглядит первая программа на Rust по сравнению с C#, и как читать ввод из командной строки. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Installing Rust +### Установка Rust ```bash -# Install Rust (works on Windows, macOS, Linux) +# Установка Rust (работает на Windows, macOS, Linux) curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -# On Windows, you can also download from: https://rustup.rs/ +# В Windows можно также скачать с: https://rustup.rs/ ``` -### Rust Tools vs C# Tools -| C# Tool | Rust Equivalent | Purpose | +### Инструменты Rust против инструментов C# +| Инструмент C# | Аналог в Rust | Назначение | |---------|----------------|---------| -| `dotnet new` | `cargo new` | Create new project | -| `dotnet build` | `cargo build` | Compile project | -| `dotnet run` | `cargo run` | Run project | -| `dotnet test` | `cargo test` | Run tests | -| NuGet | Crates.io | Package repository | -| MSBuild | Cargo | Build system | +| `dotnet new` | `cargo new` | Создание нового проекта | +| `dotnet build` | `cargo build` | Компиляция проекта | +| `dotnet run` | `cargo run` | Запуск проекта | +| `dotnet test` | `cargo test` | Запуск тестов | +| NuGet | Crates.io | Репозиторий пакетов | +| MSBuild | Cargo | Система сборки | | Visual Studio | VS Code + rust-analyzer | IDE | -### IDE Setup -1. **VS Code** (Recommended for beginners) - - Install "rust-analyzer" extension - - Install "CodeLLDB" for debugging +### Настройка IDE +1. **VS Code** (рекомендуется для начинающих) + - Установите расширение «rust-analyzer» + - Установите «CodeLLDB» для отладки 2. **Visual Studio** (Windows) - - Install Rust support extension + - Установите расширение с поддержкой Rust -3. **JetBrains RustRover** (Full IDE) - - Similar to Rider for C# +3. **JetBrains RustRover** (полноценная IDE) + - Похожа на Rider для C# *** -## Your First Rust Program +## Первая программа на Rust -### C# Hello World +### Hello World на C# ```csharp // Program.cs using System; @@ -56,7 +56,7 @@ namespace HelloWorld } ``` -### Rust Hello World +### Hello World на Rust ```rust // main.rs fn main() { @@ -64,34 +64,34 @@ fn main() { } ``` -### Key Differences for C# Developers -1. **No classes required** - Functions can exist at the top level -2. **No namespaces** - Uses module system instead -3. **`println!` is a macro** - Notice the `!` -4. **Semicolons matter** - Omitting the trailing semicolon turns a statement into a return expression -5. **No explicit return type** - `main` returns `()` (unit type) +### Ключевые отличия для разработчиков C# +1. **Классы не требуются** — функции могут существовать на верхнем уровне +2. **Пространств имён нет** — вместо них используется система модулей +3. **`println!` — это макрос** — обратите внимание на `!` +4. **Точка с запятой важна** — отсутствие завершающей точки с запятой превращает инструкцию в возвращаемое выражение +5. **Явного типа возвращаемого значения нет** — `main` возвращает `()` (тип unit) -### Creating Your First Project +### Создание первого проекта ```bash -# Create new project (like 'dotnet new console') +# Создание нового проекта (аналог 'dotnet new console') cargo new hello_rust cd hello_rust -# Project structure created: +# Созданная структура проекта: # hello_rust/ -# ├── Cargo.toml (like .csproj file) +# ├── Cargo.toml (аналог файла .csproj) # └── src/ -# └── main.rs (like Program.cs) +# └── main.rs (аналог Program.cs) -# Run the project (like 'dotnet run') +# Запуск проекта (аналог 'dotnet run') cargo run ``` *** -## Cargo vs NuGet/MSBuild +## Cargo против NuGet/MSBuild -### Project Configuration +### Конфигурация проекта **C# (.csproj)** ```xml @@ -114,34 +114,34 @@ version = "0.1.0" edition = "2021" [dependencies] -serde_json = "1.0" # Like Newtonsoft.Json -log = "0.4" # Like Serilog +serde_json = "1.0" # Аналог Newtonsoft.Json +log = "0.4" # Аналог Serilog ``` -### Common Cargo Commands +### Основные команды Cargo ```bash -# Create new project +# Создание нового проекта cargo new my_project -cargo new my_project --lib # Create library project +cargo new my_project --lib # Создать библиотечный проект -# Build and run -cargo build # Like 'dotnet build' -cargo run # Like 'dotnet run' -cargo test # Like 'dotnet test' +# Сборка и запуск +cargo build # Аналог 'dotnet build' +cargo run # Аналог 'dotnet run' +cargo test # Аналог 'dotnet test' -# Package management -cargo add serde # Add dependency (like 'dotnet add package') -cargo update # Update dependencies +# Управление пакетами +cargo add serde # Добавить зависимость (аналог 'dotnet add package') +cargo update # Обновить зависимости -# Release build -cargo build --release # Optimized build -cargo run --release # Run optimized version +# Релизная сборка +cargo build --release # Оптимизированная сборка +cargo run --release # Запуск оптимизированной версии -# Documentation -cargo doc --open # Generate and open docs +# Документация +cargo doc --open # Сгенерировать и открыть документацию ``` -### Workspace vs Solution +### Workspace против Solution **C# Solution (.sln)** ```text @@ -167,26 +167,26 @@ members = [ *** -## Reading Input and CLI Arguments +## Чтение ввода и аргументов командной строки -Every C# developer knows `Console.ReadLine()`. Here's how to handle user input, environment variables, and command-line arguments in Rust. +Каждый разработчик C# знает `Console.ReadLine()`. Вот как в Rust обрабатывать пользовательский ввод, переменные окружения и аргументы командной строки. -### Console Input +### Ввод с консоли ```csharp -// C# — reading user input -Console.Write("Enter your name: "); -string? name = Console.ReadLine(); // Returns string? in .NET 6+ -Console.WriteLine($"Hello, {name}!"); +// C# — чтение пользовательского ввода +Console.Write("Введите ваше имя: "); +string? name = Console.ReadLine(); // Возвращает string? начиная с .NET 6+ +Console.WriteLine($"Привет, {name}!"); -// Parsing input -Console.Write("Enter a number: "); +// Разбор ввода +Console.Write("Введите число: "); if (int.TryParse(Console.ReadLine(), out int number)) { - Console.WriteLine($"You entered: {number}"); + Console.WriteLine($"Вы ввели: {number}"); } else { - Console.WriteLine("That's not a valid number."); + Console.WriteLine("Это не число."); } ``` @@ -194,40 +194,40 @@ else use std::io::{self, Write}; fn main() { - // Reading a line of input - print!("Enter your name: "); - io::stdout().flush().unwrap(); // flush because print! doesn't auto-flush + // Чтение строки ввода + print!("Введите ваше имя: "); + io::stdout().flush().unwrap(); // flush нужен, потому что print! не сбрасывает буфер автоматически let mut name = String::new(); - io::stdin().read_line(&mut name).expect("Failed to read line"); - let name = name.trim(); // remove trailing newline - println!("Hello, {name}!"); + io::stdin().read_line(&mut name).expect("Не удалось прочитать строку"); + let name = name.trim(); // убираем завершающий перенос строки + println!("Привет, {name}!"); - // Parsing input - print!("Enter a number: "); + // Разбор ввода + print!("Введите число: "); io::stdout().flush().unwrap(); let mut input = String::new(); - io::stdin().read_line(&mut input).expect("Failed to read"); + io::stdin().read_line(&mut input).expect("Не удалось прочитать"); match input.trim().parse::<i32>() { - Ok(number) => println!("You entered: {number}"), - Err(_) => println!("That's not a valid number."), + Ok(number) => println!("Вы ввели: {number}"), + Err(_) => println!("Это не число."), } } ``` -### Command-Line Arguments +### Аргументы командной строки ```csharp -// C# — reading CLI args +// C# — чтение аргументов CLI static void Main(string[] args) { if (args.Length < 1) { - Console.WriteLine("Usage: program <filename>"); + Console.WriteLine("Использование: program <filename>"); return; } string filename = args[0]; - Console.WriteLine($"Processing {filename}"); + Console.WriteLine($"Обработка {filename}"); } ``` @@ -236,19 +236,19 @@ use std::env; fn main() { let args: Vec<String> = env::args().collect(); - // args[0] = program name (like C#'s Assembly name) - // args[1..] = actual arguments + // args[0] = имя программы (аналог имени сборки в C#) + // args[1..] = собственно аргументы if args.len() < 2 { - eprintln!("Usage: {} <filename>", args[0]); // eprintln! → stderr + eprintln!("Использование: {} <filename>", args[0]); // eprintln! → stderr std::process::exit(1); } let filename = &args[1]; - println!("Processing {filename}"); + println!("Обработка {filename}"); } ``` -### Environment Variables +### Переменные окружения ```csharp // C# string dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "localhost"; @@ -258,12 +258,12 @@ string dbUrl = Environment.GetEnvironmentVariable("DATABASE_URL") ?? "localhost" use std::env; let db_url = env::var("DATABASE_URL").unwrap_or_else(|_| "localhost".to_string()); -// env::var returns Result<String, VarError> — no nulls! +// env::var возвращает Result<String, VarError> — никаких null! ``` -### Production CLI Apps with `clap` +### Промышленные CLI-приложения с `clap` -For anything beyond trivial argument parsing, use the **`clap`** crate — it's the Rust equivalent of `System.CommandLine` or libraries like `CommandLineParser`. +Для всего, что выходит за рамки тривиального разбора аргументов, используйте крейт **`clap`** — это аналог `System.CommandLine` или библиотек вроде `CommandLineParser` в Rust. ```toml # Cargo.toml @@ -274,75 +274,75 @@ clap = { version = "4", features = ["derive"] } ```rust use clap::Parser; -/// A simple file processor — this doc comment becomes the help text +/// Простой обработчик файлов — этот doc-комментарий станет текстом справки #[derive(Parser, Debug)] #[command(name = "processor", version, about)] struct Args { - /// Input file to process + /// Входной файл для обработки #[arg(short, long)] input: String, - /// Output file (defaults to stdout) + /// Выходной файл (по умолчанию — stdout) #[arg(short, long)] output: Option<String>, - /// Enable verbose logging + /// Включить подробное логирование #[arg(short, long, default_value_t = false)] verbose: bool, - /// Number of worker threads + /// Количество рабочих потоков #[arg(short = 'j', long, default_value_t = 4)] threads: usize, } fn main() { - let args = Args::parse(); // auto-parses, validates, generates --help + let args = Args::parse(); // автоматически разбирает, проверяет и генерирует --help if args.verbose { - println!("Input: {}", args.input); - println!("Output: {:?}", args.output); - println!("Threads: {}", args.threads); + println!("Вход: {}", args.input); + println!("Выход: {:?}", args.output); + println!("Потоки: {}", args.threads); } - // Use args.input, args.output, etc. + // Используйте args.input, args.output и т.д. } ``` ```bash -# Auto-generated help: +# Автоматически сгенерированная справка: $ processor --help -A simple file processor +Простой обработчик файлов Usage: processor [OPTIONS] --input <INPUT> Options: - -i, --input <INPUT> Input file to process - -o, --output <OUTPUT> Output file (defaults to stdout) - -v, --verbose Enable verbose logging - -j, --threads <THREADS> Number of worker threads [default: 4] + -i, --input <INPUT> Входной файл для обработки + -o, --output <OUTPUT> Выходной файл (по умолчанию — stdout) + -v, --verbose Включить подробное логирование + -j, --threads <THREADS> Количество рабочих потоков [default: 4] -h, --help Print help -V, --version Print version ``` ```csharp -// C# equivalent with System.CommandLine (more boilerplate): -var inputOption = new Option<string>("--input", "Input file") { IsRequired = true }; -var verboseOption = new Option<bool>("--verbose", "Enable verbose logging"); -var rootCommand = new RootCommand("A simple file processor"); +// Эквивалент на C# с System.CommandLine (больше шаблонного кода): +var inputOption = new Option<string>("--input", "Входной файл") { IsRequired = true }; +var verboseOption = new Option<bool>("--verbose", "Включить подробное логирование"); +var rootCommand = new RootCommand("Простой обработчик файлов"); rootCommand.AddOption(inputOption); rootCommand.AddOption(verboseOption); rootCommand.SetHandler((input, verbose) => { /* ... */ }, inputOption, verboseOption); await rootCommand.InvokeAsync(args); -// clap's derive macro approach is more concise and type-safe +// Подход с derive-макросом в clap короче и безопаснее по типам ``` -| C# | Rust | Notes | +| C# | Rust | Примечания | |----|------|-------| -| `Console.ReadLine()` | `io::stdin().read_line(&mut buf)` | Must provide buffer, returns `Result` | -| `int.TryParse(s, out n)` | `s.parse::<i32>()` | Returns `Result<i32, ParseIntError>` | -| `args[0]` | `env::args().nth(1)` | Rust args[0] = program name | -| `Environment.GetEnvironmentVariable` | `env::var("KEY")` | Returns `Result`, not nullable | -| `System.CommandLine` | `clap` | Derive-based, auto-generates help | +| `Console.ReadLine()` | `io::stdin().read_line(&mut buf)` | Нужно передать буфер, возвращает `Result` | +| `int.TryParse(s, out n)` | `s.parse::<i32>()` | Возвращает `Result<i32, ParseIntError>` | +| `args[0]` | `env::args().nth(1)` | В Rust args[0] — имя программы | +| `Environment.GetEnvironmentVariable` | `env::var("KEY")` | Возвращает `Result`, а не nullable-значение | +| `System.CommandLine` | `clap` | Основан на derive, автоматически генерирует справку | *** diff --git a/csharp-book/src/ch03-1-true-immutability-vs-record-illusions.md b/csharp-book/src/ch03-1-true-immutability-vs-record-illusions.md index 14191f8..8cb1a23 100644 --- a/csharp-book/src/ch03-1-true-immutability-vs-record-illusions.md +++ b/csharp-book/src/ch03-1-true-immutability-vs-record-illusions.md @@ -1,43 +1,43 @@ -## True Immutability vs Record Illusions +## Истинная неизменяемость против иллюзий record -> **What you'll learn:** Why C# `record` types aren't truly immutable (mutable fields, reflection bypass), -> how Rust enforces real immutability at compile time, and when to use interior mutability patterns. +> **Что вы узнаете:** почему типы `record` в C# не являются по-настоящему неизменяемыми (изменяемые поля, обход через рефлексию), +> как Rust обеспечивает настоящую неизменяемость на этапе компиляции и когда использовать паттерны внутренней изменяемости. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### C# Records - Immutability Theater +### Records в C# — театр неизменяемости ```csharp -// C# records look immutable but have escape hatches +// Records в C# выглядят неизменяемыми, но у них есть лазейки public record Person(string Name, int Age, List<string> Hobbies); var person = new Person("John", 30, new List<string> { "reading" }); -// These all "look" like they create new instances: -var older = person with { Age = 31 }; // New record -var renamed = person with { Name = "Jonathan" }; // New record +// Все эти операции «выглядят» как создание новых экземпляров: +var older = person with { Age = 31 }; // Новый record +var renamed = person with { Name = "Jonathan" }; // Новый record -// But the reference types are still mutable! -person.Hobbies.Add("gaming"); // Mutates the original! -Console.WriteLine(older.Hobbies.Count); // 2 - older person affected! -Console.WriteLine(renamed.Hobbies.Count); // 2 - renamed person also affected! +// Но ссылочные типы всё ещё изменяемы! +person.Hobbies.Add("gaming"); // Меняет исходный объект! +Console.WriteLine(older.Hobbies.Count); // 2 — older тоже затронут! +Console.WriteLine(renamed.Hobbies.Count); // 2 — renamed тоже затронут! -// Init-only properties can still be set via reflection +// Свойства с init-доступом всё равно можно установить через рефлексию typeof(Person).GetProperty("Age")?.SetValue(person, 25); -// Collection expressions help but don't solve the fundamental issue +// Коллекционные выражения помогают, но не решают фундаментальную проблему public record BetterPerson(string Name, int Age, IReadOnlyList<string> Hobbies); var betterPerson = new BetterPerson("Jane", 25, new List<string> { "painting" }); -// Still mutable via casting: +// Всё равно изменяемо через приведение типов: ((List<string>)betterPerson.Hobbies).Add("hacking the system"); -// Even "immutable" collections aren't truly immutable +// Даже «неизменяемые» коллекции не являются по-настоящему неизменяемыми using System.Collections.Immutable; public record SafePerson(string Name, int Age, ImmutableList<string> Hobbies); -// This is better, but requires discipline and has performance overhead +// Это лучше, но требует дисциплины и имеет накладные расходы на производительность ``` -### Rust - True Immutability by Default +### Rust — настоящая неизменяемость по умолчанию ```rust #[derive(Debug, Clone)] struct Person { @@ -52,34 +52,34 @@ let person = Person { hobbies: vec!["reading".to_string()], }; -// This simply won't compile: -// person.age = 31; // ERROR: cannot assign to immutable field -// person.hobbies.push("gaming".to_string()); // ERROR: cannot borrow as mutable +// Этот код просто не скомпилируется: +// person.age = 31; // ОШИБКА: нельзя присвоить значение неизменяемому полю +// person.hobbies.push("gaming".to_string()); // ОШИБКА: нельзя заимствовать как изменяемое -// To modify, you must explicitly opt-in with 'mut': +// Для изменения нужно явно включить 'mut': let mut older_person = person.clone(); -older_person.age = 31; // Now it's clear this is mutation +older_person.age = 31; // Теперь ясно, что это мутация -// Or use functional update patterns: +// Или используйте паттерн обновления через структурное выражение: let renamed = Person { name: "Jonathan".to_string(), - ..person // Copies other fields (move semantics apply) + ..person // Копирует остальные поля (применяется семантика перемещения) }; -// The original is guaranteed unchanged (until moved): -println!("{:?}", person.hobbies); // Always ["reading"] - immutable +// Исходный объект гарантированно не меняется (пока не перемещён): +println!("{:?}", person.hobbies); // Всегда ["reading"] — неизменяемо -// Structural sharing with efficient immutable data structures +// Структурное разделение с эффективными неизменяемыми структурами данных use std::rc::Rc; #[derive(Debug, Clone)] struct EfficientPerson { name: String, age: u32, - hobbies: Rc<Vec<String>>, // Shared, immutable reference + hobbies: Rc<Vec<String>>, // Общая, неизменяемая ссылка } -// Creating new versions shares data efficiently +// Создание новых версий эффективно разделяет данные let person1 = EfficientPerson { name: "Alice".to_string(), age: 30, @@ -89,20 +89,20 @@ let person1 = EfficientPerson { let person2 = EfficientPerson { name: "Bob".to_string(), age: 25, - hobbies: Rc::clone(&person1.hobbies), // Shared reference, no deep copy + hobbies: Rc::clone(&person1.hobbies), // Общая ссылка, без глубокого копирования }; ``` ```mermaid graph TD - subgraph "C# Records - Shallow Immutability" + subgraph "Records C# — поверхностная неизменяемость" CS_RECORD["record Person(...)"] - CS_WITH["with expressions"] - CS_SHALLOW["⚠️ Only top-level immutable"] - CS_REF_MUT["❌ Reference types still mutable"] - CS_REFLECTION["❌ Reflection can bypass"] - CS_RUNTIME["❌ Runtime surprises"] - CS_DISCIPLINE["😓 Requires team discipline"] + CS_WITH["выражения with"] + CS_SHALLOW["⚠️ Неизменяем только верхний уровень"] + CS_REF_MUT["❌ Ссылочные типы всё ещё изменяемы"] + CS_REFLECTION["❌ Рефлексия может обойти ограничения"] + CS_RUNTIME["❌ Сюрпризы во время выполнения"] + CS_DISCIPLINE["😓 Требуется дисциплина команды"] CS_RECORD --> CS_WITH CS_WITH --> CS_SHALLOW @@ -112,14 +112,14 @@ graph TD CS_RUNTIME --> CS_DISCIPLINE end - subgraph "Rust - True Immutability" + subgraph "Rust — настоящая неизменяемость" RUST_STRUCT["struct Person { ... }"] - RUST_DEFAULT["✅ Immutable by default"] - RUST_COMPILE["✅ Compile-time enforcement"] - RUST_MUT["🔒 Explicit 'mut' required"] - RUST_MOVE["🔄 Move semantics"] - RUST_ZERO["⚡ Zero runtime overhead"] - RUST_SAFE["🛡️ Memory safe"] + RUST_DEFAULT["✅ Неизменяемо по умолчанию"] + RUST_COMPILE["✅ Принудительная проверка на этапе компиляции"] + RUST_MUT["🔒 Требуется явный 'mut'"] + RUST_MOVE["🔄 Семантика перемещения"] + RUST_ZERO["⚡ Нулевые накладные расходы рантайма"] + RUST_SAFE["🛡️ Безопасность памяти"] RUST_STRUCT --> RUST_DEFAULT RUST_DEFAULT --> RUST_COMPILE @@ -139,27 +139,27 @@ graph TD --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Prove the Immutability</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: докажите неизменяемость</strong> (нажмите, чтобы раскрыть)</summary> -A C# colleague claims their `record` is immutable. Translate this C# code to Rust and explain why Rust's version is truly immutable: +Ваш коллега на C# утверждает, что его `record` неизменяем. Переведите этот код на Rust и объясните, почему версия на Rust действительно неизменяема: ```csharp public record Config(string Host, int Port, List<string> AllowedOrigins); var config = new Config("localhost", 8080, new List<string> { "example.com" }); -// "Immutable" record... but: -config.AllowedOrigins.Add("evil.com"); // Compiles! List is mutable. +// «Неизменяемый» record... но: +config.AllowedOrigins.Add("evil.com"); // Компилируется! List изменяем. ``` -1. Create an equivalent Rust struct that is **truly** immutable -2. Show that attempting to mutate `allowed_origins` is a **compile error** -3. Write a function that creates a modified copy (new host) without mutation +1. Создайте эквивалентную структуру на Rust, которая **действительно** неизменяема +2. Покажите, что попытка изменить `allowed_origins` — это **ошибка компиляции** +3. Напишите функцию, которая создаёт изменённую копию (с новым хостом) без мутации <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug, Clone)] @@ -186,19 +186,18 @@ fn main() { }; // config.allowed_origins.push("evil.com".into()); - // ❌ ERROR: cannot borrow `config.allowed_origins` as mutable + // ❌ ОШИБКА: нельзя заимствовать `config.allowed_origins` как изменяемое let production = config.with_host("prod.example.com"); - println!("Dev: {:?}", config); // original unchanged - println!("Prod: {:?}", production); // new copy with different host + println!("Dev: {:?}", config); // исходный объект не изменился + println!("Prod: {:?}", production); // новая копия с другим хостом } ``` -**Key insight**: In Rust, `let config = ...` (no `mut`) makes the *entire value tree* immutable — including nested `Vec`. C# records only make the *reference* immutable, not the contents. +**Ключевая мысль**: в Rust `let config = ...` (без `mut`) делает *всё дерево значений* неизменяемым — включая вложенный `Vec`. Records в C# делают неизменяемой только *ссылку*, но не содержимое. </details> </details> *** - diff --git a/csharp-book/src/ch03-built-in-types-and-variables.md b/csharp-book/src/ch03-built-in-types-and-variables.md index 3b85362..82a3865 100644 --- a/csharp-book/src/ch03-built-in-types-and-variables.md +++ b/csharp-book/src/ch03-built-in-types-and-variables.md @@ -1,57 +1,57 @@ -## Variables and Mutability +## Переменные и изменяемость -> **What you'll learn:** Rust's variable declaration and mutability model vs C#'s `var`/`const`, -> primitive type mappings, the critical `String` vs `&str` distinction, type inference, -> and how Rust handles casting and conversions differently from C#. +> **Что вы узнаете:** модель объявления переменных и изменяемости в Rust в сравнении с `var`/`const` в C#, +> соответствие примитивных типов, важнейшее различие между `String` и `&str`, вывод типов, +> а также то, как Rust по-другому обрабатывает приведение и преобразования типов. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### C# Variable Declaration +### Объявление переменных в C# ```csharp -// C# - Variables are mutable by default -int count = 0; // Mutable -count = 5; // ✅ Works +// C# — переменные по умолчанию изменяемы +int count = 0; // Изменяемая +count = 5; // ✅ Работает -// readonly fields (class-level only, not for local variables) -// readonly int maxSize = 100; // Immutable after initialization +// readonly-поля (только на уровне класса, не для локальных переменных) +// readonly int maxSize = 100; // Неизменяемо после инициализации -const int BUFFER_SIZE = 1024; // Compile-time constant (works as local or field) +const int BUFFER_SIZE = 1024; // Константа времени компиляции (работает как локальная переменная или поле) ``` -### Rust Variable Declaration +### Объявление переменных в Rust ```rust -// Rust - Variables are immutable by default -let count = 0; // Immutable by default -// count = 5; // ❌ Compile error: cannot assign twice to immutable variable +// Rust — переменные по умолчанию неизменяемы +let count = 0; // Неизменяемая по умолчанию +// count = 5; // ❌ Ошибка компиляции: нельзя дважды присвоить неизменяемой переменной -let mut count = 0; // Explicitly mutable -count = 5; // ✅ Works +let mut count = 0; // Явно изменяемая +count = 5; // ✅ Работает -const BUFFER_SIZE: usize = 1024; // Compile-time constant +const BUFFER_SIZE: usize = 1024; // Константа времени компиляции ``` -### Key Mental Shift for C# Developers +### Ключевой сдвиг мышления для разработчиков C# ```rust -// Think of 'let' as C#'s readonly field semantics applied to all variables -let name = "John"; // Like a readonly field: once set, cannot change -let mut age = 30; // Like: int age = 30; - -// Variable shadowing (unique to Rust) -let spaces = " "; // String -let spaces = spaces.len(); // Now it's a number (usize) -// This is different from mutation - we're creating a new variable +// Думайте о 'let' как о семантике readonly-поля, применённой ко всем переменным +let name = "John"; // Как readonly-поле: после присваивания изменить нельзя +let mut age = 30; // Как: int age = 30; + +// Затенение переменных (особенность Rust) +let spaces = " "; // Строка +let spaces = spaces.len(); // Теперь это число (usize) +// Это не мутация — мы создаём новую переменную ``` -### Practical Example: Counter +### Практический пример: счётчик ```csharp -// C# version +// Версия на C# public class Counter { private int value = 0; public void Increment() { - value++; // Mutation + value++; // Мутация } public int GetValue() => value; @@ -59,9 +59,9 @@ public class Counter ``` ```rust -// Rust version +// Версия на Rust pub struct Counter { - value: i32, // Private by default + value: i32, // По умолчанию приватное } impl Counter { @@ -69,7 +69,7 @@ impl Counter { Counter { value: 0 } } - pub fn increment(&mut self) { // &mut needed for mutation + pub fn increment(&mut self) { // &mut нужен для мутации self.value += 1; } @@ -81,114 +81,114 @@ impl Counter { *** -## Data Types Comparison +## Сравнение типов данных -### Primitive Types +### Примитивные типы -| C# Type | Rust Type | Size | Range | +| Тип C# | Тип Rust | Размер | Диапазон | |---------|-----------|------|-------| -| `byte` | `u8` | 8 bits | 0 to 255 | -| `sbyte` | `i8` | 8 bits | -128 to 127 | -| `short` | `i16` | 16 bits | -32,768 to 32,767 | -| `ushort` | `u16` | 16 bits | 0 to 65,535 | -| `int` | `i32` | 32 bits | -2³¹ to 2³¹-1 | -| `uint` | `u32` | 32 bits | 0 to 2³²-1 | -| `long` | `i64` | 64 bits | -2⁶³ to 2⁶³-1 | -| `ulong` | `u64` | 64 bits | 0 to 2⁶⁴-1 | -| `float` | `f32` | 32 bits | IEEE 754 | -| `double` | `f64` | 64 bits | IEEE 754 | -| `bool` | `bool` | 1 bit | true/false | -| `char` | `char` | 32 bits | Unicode scalar | - -### Size Types (Important!) +| `byte` | `u8` | 8 бит | от 0 до 255 | +| `sbyte` | `i8` | 8 бит | от -128 до 127 | +| `short` | `i16` | 16 бит | от -32 768 до 32 767 | +| `ushort` | `u16` | 16 бит | от 0 до 65 535 | +| `int` | `i32` | 32 бита | от -2³¹ до 2³¹-1 | +| `uint` | `u32` | 32 бита | от 0 до 2³²-1 | +| `long` | `i64` | 64 бита | от -2⁶³ до 2⁶³-1 | +| `ulong` | `u64` | 64 бита | от 0 до 2⁶⁴-1 | +| `float` | `f32` | 32 бита | IEEE 754 | +| `double` | `f64` | 64 бита | IEEE 754 | +| `bool` | `bool` | 1 бит | true/false | +| `char` | `char` | 32 бита | Скалярное значение Unicode | + +### Типы размера (важно!) ```csharp -// C# - int is always 32-bit +// C# — int всегда 32-битный int arrayIndex = 0; long fileSize = file.Length; ``` ```rust -// Rust - size types match pointer size (32-bit or 64-bit) -let array_index: usize = 0; // Like size_t in C -let file_size: u64 = file.len(); // Explicit 64-bit +// Rust — типы размера совпадают с размером указателя (32 или 64 бита) +let array_index: usize = 0; // Как size_t в C +let file_size: u64 = file.len(); // Явно 64-битный ``` -### Type Inference +### Вывод типов ```csharp -// C# - var keyword +// C# — ключевое слово var var name = "John"; // string var count = 42; // int var price = 29.99; // double ``` ```rust -// Rust - automatic type inference -let name = "John"; // &str (string slice) -let count = 42; // i32 (default integer) -let price = 29.99; // f64 (default float) +// Rust — автоматический вывод типов +let name = "John"; // &str (срез строки) +let count = 42; // i32 (целое по умолчанию) +let price = 29.99; // f64 (вещественное по умолчанию) -// Explicit type annotations +// Явные аннотации типов let count: u32 = 42; let price: f32 = 29.99; ``` -### Arrays and Collections Overview +### Обзор массивов и коллекций ```csharp -// C# - reference types, heap allocated -int[] numbers = new int[5]; // Fixed size -List<int> list = new List<int>(); // Dynamic size +// C# — ссылочные типы, размещаются в куче +int[] numbers = new int[5]; // Фиксированный размер +List<int> list = new List<int>(); // Динамический размер ``` ```rust -// Rust - multiple options -let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Stack array, fixed size -let mut list: Vec<i32> = Vec::new(); // Heap vector, dynamic size +// Rust — несколько вариантов +let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Массив на стеке, фиксированный размер +let mut list: Vec<i32> = Vec::new(); // Вектор в куче, динамический размер ``` *** -## String Types: String vs &str +## Типы строк: String и &str -This is one of the most confusing concepts for C# developers, so let's break it down carefully. +Это одна из самых запутанных концепций для разработчиков C#, поэтому разберём её внимательно. -### C# String Handling +### Работа со строками в C# ```csharp -// C# - Simple string model -string name = "John"; // String literal -string greeting = "Hello, " + name; // String concatenation -string upper = name.ToUpper(); // Method call +// C# — простая модель строк +string name = "John"; // Строковый литерал +string greeting = "Hello, " + name; // Конкатенация строк +string upper = name.ToUpper(); // Вызов метода ``` -### Rust String Types +### Строковые типы Rust ```rust -// Rust - Two main string types +// Rust — два основных строковых типа -// 1. &str (string slice) - like ReadOnlySpan<char> in C# -let name: &str = "John"; // String literal (immutable, borrowed) +// 1. &str (срез строки) — похож на ReadOnlySpan<char> в C# +let name: &str = "John"; // Строковый литерал (неизменяемый, заимствованный) -// 2. String - like StringBuilder or mutable string -let mut greeting = String::new(); // Empty string -greeting.push_str("Hello, "); // Append -greeting.push_str(name); // Append +// 2. String — похож на StringBuilder или изменяемую строку +let mut greeting = String::new(); // Пустая строка +greeting.push_str("Hello, "); // Добавление +greeting.push_str(name); // Добавление -// Or create directly +// Или создаём сразу let greeting = String::from("Hello, John"); -let greeting = "Hello, John".to_string(); // Convert &str to String +let greeting = "Hello, John".to_string(); // Преобразование &str в String ``` -### When to Use Which? +### Когда что использовать? -| Scenario | Use | C# Equivalent | +| Сценарий | Использовать | Аналог в C# | |----------|-----|---------------| -| String literals | `&str` | `string` literal | -| Function parameters (read-only) | `&str` | `string` or `ReadOnlySpan<char>` | -| Owned, mutable strings | `String` | `StringBuilder` | -| Return owned strings | `String` | `string` | +| Строковые литералы | `&str` | Строковый литерал `string` | +| Параметры функций (только для чтения) | `&str` | `string` или `ReadOnlySpan<char>` | +| Владеемые, изменяемые строки | `String` | `StringBuilder` | +| Возврат владеемых строк | `String` | `string` | -### Practical Examples +### Практические примеры ```rust -// Function that accepts any string type -fn greet(name: &str) { // Accepts both String and &str +// Функция, принимающая любой строковый тип +fn greet(name: &str) { // Принимает и String, и &str println!("Hello, {}!", name); } @@ -196,109 +196,109 @@ fn main() { let literal = "John"; // &str let owned = String::from("Jane"); // String - greet(literal); // Works - greet(&owned); // Works (borrow String as &str) - greet("Bob"); // Works + greet(literal); // Работает + greet(&owned); // Работает (заимствуем String как &str) + greet("Bob"); // Работает } -// Function that returns owned string +// Функция, возвращающая владеемую строку fn create_greeting(name: &str) -> String { - format!("Hello, {}!", name) // format! macro returns String + format!("Hello, {}!", name) // Макрос format! возвращает String } ``` -### C# Developers: Think of it This Way +### Разработчикам C# — смотрите так ```rust -// &str is like ReadOnlySpan<char> - a view into string data -// String is like a char[] that you own and can modify +// &str — это как ReadOnlySpan<char>: представление данных строки +// String — это как char[], которым вы владеете и которое можете изменять -let borrowed: &str = "I don't own this data"; -let owned: String = String::from("I own this data"); +let borrowed: &str = "Я не владею этими данными"; +let owned: String = String::from("Я владею этими данными"); -// Convert between them -let owned_copy: String = borrowed.to_string(); // Copy to owned -let borrowed_view: &str = &owned; // Borrow from owned +// Преобразования между ними +let owned_copy: String = borrowed.to_string(); // Копия во владение +let borrowed_view: &str = &owned; // Заимствование из owned ``` *** -## Printing and String Formatting +## Вывод и форматирование строк -C# developers rely heavily on `Console.WriteLine` and string interpolation (`$""`). Rust's formatting system is equally powerful but uses macros and format specifiers instead. +Разработчики C# активно используют `Console.WriteLine` и интерполяцию строк (`$""`). Система форматирования Rust не менее мощная, но основана на макросах и спецификаторах формата. -### Basic Output +### Базовый вывод ```csharp -// C# output -Console.Write("no newline"); -Console.WriteLine("with newline"); -Console.Error.WriteLine("to stderr"); +// Вывод в C# +Console.Write("без перевода строки"); +Console.WriteLine("с переводом строки"); +Console.Error.WriteLine("в stderr"); -// String interpolation (C# 6+) +// Интерполяция строк (C# 6+) string name = "Alice"; int age = 30; Console.WriteLine($"{name} is {age} years old"); ``` ```rust -// Rust output — all macros (note the !) -print!("no newline"); // → stdout, no newline -println!("with newline"); // → stdout + newline -eprint!("to stderr"); // → stderr, no newline -eprintln!("to stderr with newline"); // → stderr + newline +// Вывод в Rust — всё через макросы (обратите внимание на !) +print!("без перевода строки"); // → stdout, без перевода строки +println!("с переводом строки"); // → stdout + перевод строки +eprint!("в stderr"); // → stderr, без перевода строки +eprintln!("в stderr с переводом строки"); // → stderr + перевод строки -// String formatting (like $"" interpolation) +// Форматирование строк (аналог интерполяции $"") let name = "Alice"; let age = 30; -println!("{name} is {age} years old"); // Inline variable capture (Rust 1.58+) -println!("{} is {} years old", name, age); // Positional arguments +println!("{name} is {age} years old"); // Захват переменной прямо в строке (Rust 1.58+) +println!("{} is {} years old", name, age); // Позиционные аргументы -// format! returns a String instead of printing +// format! возвращает String вместо вывода let msg = format!("{name} is {age} years old"); ``` -### Format Specifiers +### Спецификаторы формата ```csharp -// C# format specifiers -Console.WriteLine($"{price:F2}"); // Fixed decimal: 29.99 -Console.WriteLine($"{count:D5}"); // Padded integer: 00042 -Console.WriteLine($"{value,10}"); // Right-aligned, width 10 -Console.WriteLine($"{value,-10}"); // Left-aligned, width 10 -Console.WriteLine($"{hex:X}"); // Hexadecimal: FF -Console.WriteLine($"{ratio:P1}"); // Percentage: 85.0% +// Спецификаторы формата в C# +Console.WriteLine($"{price:F2}"); // Фиксированная запись: 29.99 +Console.WriteLine($"{count:D5}"); // Дополнение нулями: 00042 +Console.WriteLine($"{value,10}"); // Выравнивание по правому краю, ширина 10 +Console.WriteLine($"{value,-10}"); // Выравнивание по левому краю, ширина 10 +Console.WriteLine($"{hex:X}"); // Шестнадцатеричный: FF +Console.WriteLine($"{ratio:P1}"); // Проценты: 85.0% ``` ```rust -// Rust format specifiers -println!("{price:.2}"); // 2 decimal places: 29.99 -println!("{count:05}"); // Zero-padded, width 5: 00042 -println!("{value:>10}"); // Right-aligned, width 10 -println!("{value:<10}"); // Left-aligned, width 10 -println!("{value:^10}"); // Center-aligned, width 10 -println!("{hex:#X}"); // Hex with prefix: 0xFF -println!("{hex:08X}"); // Hex zero-padded: 000000FF -println!("{bits:#010b}"); // Binary with prefix: 0b00001010 -println!("{big}", big = 1_000_000); // Named parameter +// Спецификаторы формата в Rust +println!("{price:.2}"); // 2 знака после запятой: 29.99 +println!("{count:05}"); // Дополнение нулями, ширина 5: 00042 +println!("{value:>10}"); // Выравнивание по правому краю, ширина 10 +println!("{value:<10}"); // Выравнивание по левому краю, ширина 10 +println!("{value:^10}"); // Выравнивание по центру, ширина 10 +println!("{hex:#X}"); // Шестнадцатеричный с префиксом: 0xFF +println!("{hex:08X}"); // Шестнадцатеричный с нулями: 000000FF +println!("{bits:#010b}"); // Двоичный с префиксом: 0b00001010 +println!("{big}", big = 1_000_000); // Именованный параметр ``` -### Debug vs Display Printing +### Вывод Debug и Display ```rust -// {:?} — Debug trait (for developers, auto-derived) -// {:#?} — Pretty-printed Debug (indented, multi-line) -// {} — Display trait (for users, must implement manually) +// {:?} — трейт Debug (для разработчиков, реализуется автоматически) +// {:#?} — Debug с красивым форматированием (с отступами, в несколько строк) +// {} — трейт Display (для пользователей, нужно реализовать вручную) -#[derive(Debug)] // Auto-generates Debug output +#[derive(Debug)] // Автоматически генерирует вывод Debug struct Point { x: f64, y: f64 } let p = Point { x: 1.5, y: 2.7 }; -println!("{:?}", p); // Point { x: 1.5, y: 2.7 } — compact debug -println!("{:#?}", p); // Point { — pretty debug +println!("{:?}", p); // Point { x: 1.5, y: 2.7 } — компактный debug +println!("{:#?}", p); // Point { — debug с форматированием // x: 1.5, // y: 2.7, // } -// println!("{}", p); // ❌ ERROR: Point doesn't implement Display +// println!("{}", p); // ❌ ОШИБКА: Point не реализует Display -// Implement Display for user-facing output: +// Реализуем Display для вывода, понятного пользователю: use std::fmt; impl fmt::Display for Point { @@ -306,149 +306,149 @@ impl fmt::Display for Point { write!(f, "({}, {})", self.x, self.y) } } -println!("{}", p); // (1.5, 2.7) — user-friendly +println!("{}", p); // (1.5, 2.7) — удобно для пользователя ``` ```csharp -// C# equivalent: -// {:?} ≈ object.GetType().ToString() or reflection dump +// Эквивалент в C#: +// {:?} ≈ object.GetType().ToString() или дамп через рефлексию // {} ≈ object.ToString() -// In C# you override ToString(); in Rust you implement Display +// В C# вы переопределяете ToString(); в Rust вы реализуете Display ``` -### Quick Reference +### Краткая справка -| C# | Rust | Output | +| C# | Rust | Вывод | |----|------|--------| -| `Console.WriteLine(x)` | `println!("{x}")` | Display formatting | -| `$"{x}"` (interpolation) | `format!("{x}")` | Returns `String` | -| `x.ToString()` | `x.to_string()` | Requires `Display` trait | -| Override `ToString()` | `impl Display` | User-facing output | -| Debugger view | `{:?}` or `dbg!(x)` | Developer output | -| `String.Format("{0:F2}", x)` | `format!("{x:.2}")` | Formatted `String` | -| `Console.Error.WriteLine` | `eprintln!()` | Write to stderr | +| `Console.WriteLine(x)` | `println!("{x}")` | Форматирование Display | +| `$"{x}"` (интерполяция) | `format!("{x}")` | Возвращает `String` | +| `x.ToString()` | `x.to_string()` | Требует трейт `Display` | +| Переопределение `ToString()` | `impl Display` | Вывод для пользователя | +| Просмотр в отладчике | `{:?}` или `dbg!(x)` | Вывод для разработчика | +| `String.Format("{0:F2}", x)` | `format!("{x:.2}")` | Форматированная `String` | +| `Console.Error.WriteLine` | `eprintln!()` | Запись в stderr | *** -## Type Casting and Conversions +## Приведение типов и преобразования -C# has implicit conversions, explicit casts `(int)x`, and `Convert.To*()`. Rust is stricter — there are no implicit numeric conversions. +В C# есть неявные преобразования, явные приведения `(int)x` и `Convert.To*()`. Rust строже — неявных числовых преобразований здесь нет. -### Numeric Conversions +### Числовые преобразования ```csharp -// C# — implicit and explicit conversions +// C# — неявные и явные преобразования int small = 42; -long big = small; // Implicit widening: OK -double d = small; // Implicit widening: OK -int truncated = (int)3.14; // Explicit narrowing: 3 -byte b = (byte)300; // Silent overflow: 44 +long big = small; // Неявное расширение: OK +double d = small; // Неявное расширение: OK +int truncated = (int)3.14; // Явное сужение: 3 +byte b = (byte)300; // Тихое переполнение: 44 -// Safe conversion +// Безопасное преобразование if (int.TryParse("42", out int parsed)) { /* ... */ } ``` ```rust -// Rust — ALL numeric conversions are explicit +// Rust — ВСЕ числовые преобразования явные let small: i32 = 42; -let big: i64 = small as i64; // Widening: explicit with 'as' -let d: f64 = small as f64; // Int to float: explicit -let truncated: i32 = 3.14_f64 as i32; // Narrowing: 3 (truncates) -let b: u8 = 300_u16 as u8; // Overflow: wraps to 44 (like C# unchecked) +let big: i64 = small as i64; // Расширение: явно через 'as' +let d: f64 = small as f64; // Целое в вещественное: явно +let truncated: i32 = 3.14_f64 as i32; // Сужение: 3 (отбрасывается дробная часть) +let b: u8 = 300_u16 as u8; // Переполнение: оборачивается до 44 (как unchecked в C#) -// Safe conversion with TryFrom +// Безопасное преобразование через TryFrom use std::convert::TryFrom; -let safe: Result<u8, _> = u8::try_from(300_u16); // Err — out of range +let safe: Result<u8, _> = u8::try_from(300_u16); // Err — вне диапазона let ok: Result<u8, _> = u8::try_from(42_u16); // Ok(42) -// String parsing — returns Result, not bool + out param +// Разбор строки — возвращает Result, а не bool + out-параметр let parsed: Result<i32, _> = "42".parse::<i32>(); // Ok(42) let bad: Result<i32, _> = "abc".parse::<i32>(); // Err(ParseIntError) -// With turbofish syntax: +// С синтаксисом turbofish: let n = "42".parse::<f64>().unwrap(); // 42.0 ``` -### String Conversions +### Преобразования строк ```csharp // C# int n = 42; string s = n.ToString(); // "42" string formatted = $"{n:X}"; -int back = int.Parse(s); // 42 or throws +int back = int.Parse(s); // 42 или исключение bool ok = int.TryParse(s, out int result); ``` ```rust -// Rust — to_string() via Display, parse() via FromStr +// Rust — to_string() через Display, parse() через FromStr let n: i32 = 42; -let s: String = n.to_string(); // "42" (uses Display trait) +let s: String = n.to_string(); // "42" (использует трейт Display) let formatted = format!("{n:X}"); // "2A" -let back: i32 = s.parse().unwrap(); // 42 or panics -let result: Result<i32, _> = s.parse(); // Ok(42) — safe version +let back: i32 = s.parse().unwrap(); // 42 или panic +let result: Result<i32, _> = s.parse(); // Ok(42) — безопасная версия -// &str ↔ String conversions (most common conversion in Rust) +// Преобразования &str ↔ String (самое частое преобразование в Rust) let owned: String = "hello".to_string(); // &str → String -let owned2: String = String::from("hello"); // &str → String (equivalent) -let borrowed: &str = &owned; // String → &str (free, just a borrow) +let owned2: String = String::from("hello"); // &str → String (то же самое) +let borrowed: &str = &owned; // String → &str (бесплатно, это просто заимствование) ``` -### Reference Conversions (No Inheritance Casting!) +### Преобразования ссылочных типов (никакого приведения по наследованию!) ```csharp -// C# — upcasting and downcasting -Animal a = new Dog(); // Upcast (implicit) -Dog d = (Dog)a; // Downcast (explicit, can throw) -if (a is Dog dog) { /* ... */ } // Safe downcast with pattern match +// C# — приведение вверх и вниз по иерархии +Animal a = new Dog(); // Приведение вверх (неявное) +Dog d = (Dog)a; // Приведение вниз (явное, может бросить исключение) +if (a is Dog dog) { /* ... */ } // Безопасное приведение вниз через сопоставление с образцом ``` ```rust -// Rust — No inheritance, no upcasting/downcasting -// Use trait objects for polymorphism: +// Rust — нет наследования, нет приведения вверх и вниз +// Для полиморфизма используйте трейт-объекты: let animal: Box<dyn Animal> = Box::new(Dog); -// "Downcasting" requires the Any trait (rarely needed): +// «Приведение вниз» требует трейта Any (нужно редко): use std::any::Any; if let Some(dog) = animal_any.downcast_ref::<Dog>() { - // Use dog + // Используем dog } -// In practice, use enums instead of downcasting: +// На практике вместо приведения вниз используйте перечисления: enum Animal { Dog(Dog), Cat(Cat), } match animal { - Animal::Dog(d) => { /* use d */ } - Animal::Cat(c) => { /* use c */ } + Animal::Dog(d) => { /* используем d */ } + Animal::Cat(c) => { /* используем c */ } } ``` -### Quick Reference +### Краткая справка -| C# | Rust | Notes | +| C# | Rust | Примечания | |----|------|-------| -| `(int)x` | `x as i32` | Truncating/wrapping cast | -| Implicit widening | Must use `as` | No implicit numeric conversion | -| `Convert.ToInt32(x)` | `i32::try_from(x)` | Safe, returns `Result` | -| `int.Parse(s)` | `s.parse::<i32>().unwrap()` | Panics on failure | -| `int.TryParse(s, out n)` | `s.parse::<i32>()` | Returns `Result<i32, _>` | -| `(Dog)animal` | Not available | Use enums or `Any` | -| `as Dog` / `is Dog` | `downcast_ref::<Dog>()` | Via `Any` trait; prefer enums | +| `(int)x` | `x as i32` | Усечение/оборачивание при приведении | +| Неявное расширение | Нужно использовать `as` | Неявных числовых преобразований нет | +| `Convert.ToInt32(x)` | `i32::try_from(x)` | Безопасно, возвращает `Result` | +| `int.Parse(s)` | `s.parse::<i32>().unwrap()` | Паника при ошибке | +| `int.TryParse(s, out n)` | `s.parse::<i32>()` | Возвращает `Result<i32, _>` | +| `(Dog)animal` | Недоступно | Используйте перечисления или `Any` | +| `as Dog` / `is Dog` | `downcast_ref::<Dog>()` | Через трейт `Any`; предпочтительны перечисления | *** -## Comments and Documentation +## Комментарии и документация -### Regular Comments +### Обычные комментарии ```csharp -// C# comments -// Single line comment -/* Multi-line - comment */ +// Комментарии в C# +// Однострочный комментарий +/* Многострочный + комментарий */ /// <summary> -/// XML documentation comment +/// XML-комментарий документации /// </summary> -/// <param name="name">The user's name</param> -/// <returns>A greeting string</returns> +/// <param name="name">Имя пользователя</param> +/// <returns>Строка приветствия</returns> public string Greet(string name) { return $"Hello, {name}!"; @@ -456,23 +456,23 @@ public string Greet(string name) ``` ```rust -// Rust comments -// Single line comment -/* Multi-line - comment */ +// Комментарии в Rust +// Однострочный комментарий +/* Многострочный + комментарий */ -/// Documentation comment (like C# ///) -/// This function greets a user by name. +/// Комментарий документации (аналог C# ///) +/// Эта функция приветствует пользователя по имени. /// -/// # Arguments +/// # Аргументы /// -/// * `name` - The user's name as a string slice +/// * `name` - Имя пользователя в виде среза строки /// -/// # Returns +/// # Возвращает /// -/// A `String` containing the greeting +/// `String`, содержащую приветствие /// -/// # Examples +/// # Примеры /// /// ``` /// let greeting = greet("Alice"); @@ -483,30 +483,30 @@ pub fn greet(name: &str) -> String { } ``` -### Documentation Generation +### Генерация документации ```bash -# Generate documentation (like XML docs in C#) +# Генерация документации (аналог XML-документации в C#) cargo doc --open -# Run documentation tests +# Запуск тестов документации cargo test --doc ``` --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Type-Safe Temperature</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: температура с безопасными типами</strong> (нажмите, чтобы раскрыть)</summary> -Create a Rust program that: -1. Declares a `const` for absolute zero in Celsius (`-273.15`) -2. Declares a `static` counter for how many conversions have been performed (use `AtomicU32`) -3. Writes a function `celsius_to_fahrenheit(c: f64) -> f64` that rejects temperatures below absolute zero by returning `f64::NAN` -4. Demonstrates shadowing by parsing a string `"98.6"` into an `f64`, then converting it +Напишите программу на Rust, которая: +1. Объявляет `const` для абсолютного нуля в градусах Цельсия (`-273.15`) +2. Объявляет `static`-счётчик количества выполненных преобразований (используйте `AtomicU32`) +3. Пишет функцию `celsius_to_fahrenheit(c: f64) -> f64`, которая отвергает температуры ниже абсолютного нуля, возвращая `f64::NAN` +4. Демонстрирует затенение: разбирает строку `"98.6"` в `f64`, а затем преобразует её <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::sync::atomic::{AtomicU32, Ordering}; @@ -524,10 +524,10 @@ fn celsius_to_fahrenheit(c: f64) -> f64 { fn main() { let temp = "98.6"; // &str - let temp: f64 = temp.parse().unwrap(); // shadow as f64 - let temp = celsius_to_fahrenheit(temp); // shadow as Fahrenheit + let temp: f64 = temp.parse().unwrap(); // затеняем как f64 + let temp = celsius_to_fahrenheit(temp); // затеняем как Фаренгейт println!("{temp:.1}°F"); - println!("Conversions: {}", CONVERSION_COUNT.load(Ordering::Relaxed)); + println!("Преобразований: {}", CONVERSION_COUNT.load(Ordering::Relaxed)); } ``` diff --git a/csharp-book/src/ch04-control-flow.md b/csharp-book/src/ch04-control-flow.md index bda25c7..ab74ee9 100644 --- a/csharp-book/src/ch04-control-flow.md +++ b/csharp-book/src/ch04-control-flow.md @@ -1,29 +1,29 @@ -## Functions vs Methods +## Функции и методы -> **What you'll learn:** Functions and methods in Rust vs C#, the critical distinction between -> expressions and statements, `if`/`match`/`loop`/`while`/`for` syntax, and how Rust's -> expression-oriented design eliminates the need for ternary operators. +> **Что вы узнаете:** функции и методы в Rust по сравнению с C#, важнейшее различие между +> выражениями и инструкциями, синтаксис `if`/`match`/`loop`/`while`/`for`, а также то, как +> ориентированный на выражения дизайн Rust делает ненужными тернарные операторы. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### C# Function Declaration +### Объявление функций в C# ```csharp -// C# - Methods in classes +// C# — методы внутри классов public class Calculator { - // Instance method + // Метод экземпляра public int Add(int a, int b) { return a + b; } - // Static method + // Статический метод public static int Multiply(int a, int b) { return a * b; } - // Method with ref parameter + // Метод с параметром ref public void Increment(ref int value) { value++; @@ -31,18 +31,18 @@ public class Calculator } ``` -### Rust Function Declaration +### Объявление функций в Rust ```rust -// Rust - Standalone functions +// Rust — самостоятельные функции fn add(a: i32, b: i32) -> i32 { - a + b // No 'return' needed for final expression + a + b // Ключевое слово 'return' не нужно для последнего выражения } fn multiply(a: i32, b: i32) -> i32 { - return a * b; // Explicit return is also fine + return a * b; // Явный return тоже допустим } -// Function with mutable reference +// Функция с изменяемой ссылкой fn increment(value: &mut i32) { *value += 1; } @@ -53,24 +53,24 @@ fn main() { let mut x = 10; increment(&mut x); - println!("After increment: {}", x); + println!("После increment: {}", x); } ``` -### Expression vs Statement (Important!) +### Выражение против инструкции (важно!) ```mermaid graph LR - subgraph "C# — Statements" + subgraph "C# — инструкции" CS1["if (cond)"] --> CS2["return 42;"] CS1 --> CS3["return 0;"] - CS2 --> CS4["Value exits via return"] + CS2 --> CS4["Значение выходит через return"] CS3 --> CS4 end - subgraph "Rust — Expressions" - RS1["if cond"] --> RS2["42 (no semicolon)"] - RS1 --> RS3["0 (no semicolon)"] - RS2 --> RS4["Block IS the value"] + subgraph "Rust — выражения" + RS1["if cond"] --> RS2["42 (без точки с запятой)"] + RS1 --> RS3["0 (без точки с запятой)"] + RS2 --> RS4["Блок И ЕСТЬ значение"] RS3 --> RS4 end @@ -79,123 +79,123 @@ graph LR ``` ```csharp -// C# - Statements vs expressions +// C# — инструкции и выражения public int GetValue() { if (condition) { - return 42; // Statement + return 42; // Инструкция } - return 0; // Statement + return 0; // Инструкция } ``` ```rust -// Rust - Everything can be an expression +// Rust — почти всё может быть выражением fn get_value(condition: bool) -> i32 { if condition { - 42 // Expression (no semicolon) + 42 // Выражение (без точки с запятой) } else { - 0 // Expression (no semicolon) + 0 // Выражение (без точки с запятой) } - // The if-else block itself is an expression that returns a value + // Сам блок if-else — это выражение, которое возвращает значение } -// Or even simpler +// Или ещё проще fn get_value_ternary(condition: bool) -> i32 { if condition { 42 } else { 0 } } ``` -### Function Parameters and Return Types +### Параметры функций и типы возвращаемых значений ```rust -// No parameters, no return value (returns unit type ()) +// Без параметров и без возвращаемого значения (возвращает тип unit ()) fn say_hello() { - println!("Hello!"); + println!("Привет!"); } -// Multiple parameters +// Несколько параметров fn greet(name: &str, age: u32) { - println!("{} is {} years old", name, age); + println!("{} — {} лет", name, age); } -// Multiple return values using tuple +// Несколько возвращаемых значений через кортеж fn divide_and_remainder(dividend: i32, divisor: i32) -> (i32, i32) { (dividend / divisor, dividend % divisor) } fn main() { let (quotient, remainder) = divide_and_remainder(10, 3); - println!("10 ÷ 3 = {} remainder {}", quotient, remainder); + println!("10 ÷ 3 = {} остаток {}", quotient, remainder); } ``` *** -## Control Flow Basics +## Основы управления потоком -### Conditional Statements +### Условные инструкции ```csharp -// C# if statements +// Инструкции if в C# int x = 5; if (x > 10) { - Console.WriteLine("Big number"); + Console.WriteLine("Большое число"); } else if (x > 5) { - Console.WriteLine("Medium number"); + Console.WriteLine("Среднее число"); } else { - Console.WriteLine("Small number"); + Console.WriteLine("Маленькое число"); } -// C# ternary operator -string message = x > 10 ? "Big" : "Small"; +// Тернарный оператор C# +string message = x > 10 ? "Большое" : "Маленькое"; ``` ```rust -// Rust if expressions +// Выражения if в Rust let x = 5; if x > 10 { - println!("Big number"); + println!("Большое число"); } else if x > 5 { - println!("Medium number"); + println!("Среднее число"); } else { - println!("Small number"); + println!("Маленькое число"); } -// Rust if as expression (like ternary) -let message = if x > 10 { "Big" } else { "Small" }; +// if как выражение (аналог тернарного оператора) +let message = if x > 10 { "Большое" } else { "Маленькое" }; -// Multiple conditions +// Несколько условий let message = if x > 10 { - "Big" + "Большое" } else if x > 5 { - "Medium" + "Среднее" } else { - "Small" + "Маленькое" }; ``` -### Loops +### Циклы ```csharp -// C# loops -// For loop +// Циклы в C# +// Цикл for for (int i = 0; i < 5; i++) { Console.WriteLine(i); } -// Foreach loop +// Цикл foreach var numbers = new[] { 1, 2, 3, 4, 5 }; foreach (var num in numbers) { Console.WriteLine(num); } -// While loop +// Цикл while int count = 0; while (count < 3) { @@ -205,32 +205,32 @@ while (count < 3) ``` ```rust -// Rust loops -// Range-based for loop -for i in 0..5 { // 0 to 4 (exclusive end) +// Циклы в Rust +// Цикл по диапазону +for i in 0..5 { // От 0 до 4 (правая граница не включается) println!("{}", i); } -// Iterate over collection +// Перебор коллекции let numbers = vec![1, 2, 3, 4, 5]; -for num in numbers { // Takes ownership +for num in numbers { // Забирает владение println!("{}", num); } -// Iterate over references (more common) +// Перебор ссылок (встречается чаще) let numbers = vec![1, 2, 3, 4, 5]; -for num in &numbers { // Borrows elements +for num in &numbers { // Заимствует элементы println!("{}", num); } -// While loop +// Цикл while let mut count = 0; while count < 3 { println!("{}", count); count += 1; } -// Infinite loop with break +// Бесконечный цикл с break let mut counter = 0; loop { if counter >= 3 { @@ -241,9 +241,9 @@ loop { } ``` -### Loop Control +### Управление циклом ```csharp -// C# loop control +// Управление циклом в C# for (int i = 0; i < 10; i++) { if (i == 3) continue; @@ -253,18 +253,18 @@ for (int i = 0; i < 10; i++) ``` ```rust -// Rust loop control +// Управление циклом в Rust for i in 0..10 { if i == 3 { continue; } if i == 7 { break; } println!("{}", i); } -// Loop labels (for nested loops) +// Метки циклов (для вложенных циклов) 'outer: for i in 0..3 { 'inner: for j in 0..3 { if i == 1 && j == 1 { - break 'outer; // Break out of outer loop + break 'outer; // Выход из внешнего цикла } println!("i: {}, j: {}", i, j); } @@ -275,12 +275,12 @@ for i in 0..10 { <details> -<summary><strong>🏋️ Exercise: Temperature Converter</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: конвертер температур</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Convert this C# program to idiomatic Rust. Use expressions, pattern matching, and proper error handling. +**Задача**: переведите эту программу на C# на идиоматичный Rust. Используйте выражения, сопоставление с образцом и правильную обработку ошибок. ```csharp -// C# — convert this to Rust +// C# — переведите это на Rust public static double Convert(double value, string from, string to) { double celsius = from switch @@ -301,7 +301,7 @@ public static double Convert(double value, string from, string to) ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug, Clone, Copy)] @@ -312,7 +312,7 @@ fn parse_unit(s: &str) -> Result<TempUnit, String> { "C" => Ok(TempUnit::Celsius), "F" => Ok(TempUnit::Fahrenheit), "K" => Ok(TempUnit::Kelvin), - _ => Err(format!("Unknown unit: {s}")), + _ => Err(format!("Неизвестная единица: {s}")), } } @@ -337,10 +337,10 @@ fn main() -> Result<(), String> { } ``` -**Key takeaways**: -- Enums replace magic strings — exhaustive matching catches missing units at compile time -- `Result<T, E>` replaces exceptions — the caller sees possible failures in the signature -- `match` is an expression that returns a value — no `return` statements needed +**Ключевые выводы**: +- Перечисления заменяют «магические» строки — исчерпывающее сопоставление ловит пропущенные единицы на этапе компиляции +- `Result<T, E>` заменяет исключения — вызывающий код видит возможные ошибки в сигнатуре +- `match` — это выражение, которое возвращает значение, — инструкции `return` не нужны </details> </details> diff --git a/csharp-book/src/ch05-1-constructor-patterns.md b/csharp-book/src/ch05-1-constructor-patterns.md index 83c4a20..9b16d6d 100644 --- a/csharp-book/src/ch05-1-constructor-patterns.md +++ b/csharp-book/src/ch05-1-constructor-patterns.md @@ -1,11 +1,11 @@ -## Constructor Patterns +## Паттерны конструкторов -> **What you'll learn:** How to create Rust structs without traditional constructors — `new()` conventions, -> the `Default` trait, factory methods, and the builder pattern for complex initialization. +> **Что вы узнаете:** как создавать структуры Rust без традиционных конструкторов — соглашения `new()`, +> трейт `Default`, фабричные методы и паттерн builder для сложной инициализации. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### C# Constructor Patterns +### Паттерны конструкторов в C# ```csharp public class Configuration { @@ -13,7 +13,7 @@ public class Configuration public int MaxConnections { get; set; } public bool EnableLogging { get; set; } - // Default constructor + // Конструктор по умолчанию public Configuration() { DatabaseUrl = "localhost"; @@ -21,7 +21,7 @@ public class Configuration EnableLogging = false; } - // Parameterized constructor + // Конструктор с параметрами public Configuration(string databaseUrl, int maxConnections) { DatabaseUrl = databaseUrl; @@ -29,7 +29,7 @@ public class Configuration EnableLogging = false; } - // Factory method + // Фабричный метод public static Configuration ForProduction() { return new Configuration("prod.db.server", 100) @@ -40,7 +40,7 @@ public class Configuration } ``` -### Rust Constructor Patterns +### Паттерны конструкторов в Rust ```rust #[derive(Debug)] pub struct Configuration { @@ -50,7 +50,7 @@ pub struct Configuration { } impl Configuration { - // Default constructor + // Конструктор по умолчанию pub fn new() -> Configuration { Configuration { database_url: "localhost".to_string(), @@ -59,7 +59,7 @@ impl Configuration { } } - // Parameterized constructor + // Конструктор с параметрами pub fn with_database(database_url: String, max_connections: u32) -> Configuration { Configuration { database_url, @@ -68,7 +68,7 @@ impl Configuration { } } - // Factory method + // Фабричный метод pub fn for_production() -> Configuration { Configuration { database_url: "prod.db.server".to_string(), @@ -77,10 +77,10 @@ impl Configuration { } } - // Builder pattern method + // Метод в стиле builder pub fn enable_logging(mut self) -> Configuration { self.enable_logging = true; - self // Return self for chaining + self // Возвращаем self для цепочки вызовов } pub fn max_connections(mut self, count: u32) -> Configuration { @@ -89,7 +89,7 @@ impl Configuration { } } -// Default trait implementation +// Реализация трейта Default impl Default for Configuration { fn default() -> Self { Self::new() @@ -97,26 +97,26 @@ impl Default for Configuration { } fn main() { - // Different construction patterns + // Разные способы создания let config1 = Configuration::new(); let config2 = Configuration::with_database("localhost:5432".to_string(), 20); let config3 = Configuration::for_production(); - // Builder pattern + // Паттерн builder let config4 = Configuration::new() .enable_logging() .max_connections(50); - // Using Default trait + // Использование трейта Default let config5 = Configuration::default(); println!("{:?}", config4); } ``` -### Builder Pattern Implementation +### Реализация паттерна Builder ```rust -// More complex builder pattern +// Более сложный паттерн builder #[derive(Debug)] pub struct DatabaseConfig { host: String, @@ -211,19 +211,19 @@ fn main() { --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Builder with Validation</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: builder с валидацией</strong> (нажмите, чтобы раскрыть)</summary> -Create an `EmailBuilder` that: -1. Requires `to` and `subject` (builder won't compile without them — use a typestate or validate in `build()`) -2. Has optional `body` and `cc` (Vec of addresses) -3. `build()` returns `Result<Email, String>` — rejects empty `to` or `subject` -4. Write tests proving invalid inputs are rejected +Создайте `EmailBuilder`, который: +1. Требует `to` и `subject` (builder не должен компилироваться без них — используйте typestate или проверяйте в `build()`) +2. Имеет необязательные `body` и `cc` (Vec адресов) +3. `build()` возвращает `Result<Email, String>` — отвергает пустые `to` или `subject` +4. Напишите тесты, доказывающие, что некорректные входные данные отвергаются <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug)] diff --git a/csharp-book/src/ch05-2-collections-vec-hashmap-and-iterators.md b/csharp-book/src/ch05-2-collections-vec-hashmap-and-iterators.md index fbfc377..d782532 100644 --- a/csharp-book/src/ch05-2-collections-vec-hashmap-and-iterators.md +++ b/csharp-book/src/ch05-2-collections-vec-hashmap-and-iterators.md @@ -1,117 +1,117 @@ -## `Vec<T>` vs `List<T>` +## `Vec<T>` против `List<T>` -> **What you'll learn:** `Vec<T>` vs `List<T>`, `HashMap` vs `Dictionary`, safe access patterns -> (why Rust returns `Option` instead of throwing), and the ownership implications of collections. +> **Что вы узнаете:** `Vec<T>` против `List<T>`, `HashMap` против `Dictionary`, безопасные паттерны доступа +> (почему Rust возвращает `Option` вместо генерации исключения), а также последствия владения для коллекций. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -`Vec<T>` is Rust's equivalent to C#'s `List<T>`, but with ownership semantics. +`Vec<T>` — аналог `List<T>` из C# в Rust, но с семантикой владения. -### C# `List<T>` +### `List<T>` в C# ```csharp -// C# List<T> - Reference type, heap allocated +// List<T> в C# — ссылочный тип, размещается в куче var numbers = new List<int>(); numbers.Add(1); numbers.Add(2); numbers.Add(3); -// Pass to method - reference is copied +// Передача в метод — ссылка копируется ProcessList(numbers); -Console.WriteLine(numbers.Count); // Still accessible +Console.WriteLine(numbers.Count); // По-прежнему доступен void ProcessList(List<int> list) { - list.Add(4); // Modifies original list + list.Add(4); // Изменяет исходный список Console.WriteLine($"Count in method: {list.Count}"); } ``` -### Rust `Vec<T>` +### `Vec<T>` в Rust ```rust -// Rust Vec<T> - Owned type, heap allocated +// Vec<T> в Rust — владеемый тип, размещается в куче let mut numbers = Vec::new(); numbers.push(1); numbers.push(2); numbers.push(3); -// Method that takes ownership +// Метод, который забирает владение process_vec(numbers); -// println!("{:?}", numbers); // ❌ Error: numbers was moved +// println!("{:?}", numbers); // ❌ Ошибка: numbers было перемещено -// Method that borrows -let mut numbers = vec![1, 2, 3]; // vec! macro for convenience +// Метод, который заимствует +let mut numbers = vec![1, 2, 3]; // Макрос vec! для удобства process_vec_borrowed(&mut numbers); -println!("{:?}", numbers); // ✅ Still accessible +println!("{:?}", numbers); // ✅ По-прежнему доступен -fn process_vec(mut vec: Vec<i32>) { // Takes ownership +fn process_vec(mut vec: Vec<i32>) { // Забирает владение vec.push(4); println!("Count in method: {}", vec.len()); - // vec is dropped here + // vec освобождается здесь } -fn process_vec_borrowed(vec: &mut Vec<i32>) { // Borrows mutably +fn process_vec_borrowed(vec: &mut Vec<i32>) { // Заимствует изменяемо vec.push(4); println!("Count in method: {}", vec.len()); } ``` -### Creating and Initializing Vectors +### Создание и инициализация векторов ```csharp -// C# List initialization +// Инициализация List в C# var numbers = new List<int> { 1, 2, 3, 4, 5 }; var empty = new List<int>(); -var sized = new List<int>(10); // Initial capacity +var sized = new List<int>(10); // Начальная ёмкость -// From other collections +// Из других коллекций var fromArray = new List<int>(new[] { 1, 2, 3 }); ``` ```rust -// Rust Vec initialization -let numbers = vec![1, 2, 3, 4, 5]; // vec! macro -let empty: Vec<i32> = Vec::new(); // Type annotation needed for empty -let sized = Vec::with_capacity(10); // Pre-allocate capacity +// Инициализация Vec в Rust +let numbers = vec![1, 2, 3, 4, 5]; // Макрос vec! +let empty: Vec<i32> = Vec::new(); // Для пустого нужна аннотация типа +let sized = Vec::with_capacity(10); // Предварительное выделение ёмкости -// From iterator +// Из итератора let from_range: Vec<i32> = (1..=5).collect(); let from_array = vec![1, 2, 3]; ``` -### Common Operations Comparison +### Сравнение распространённых операций ```csharp -// C# List operations +// Операции с List в C# var list = new List<int> { 1, 2, 3 }; -list.Add(4); // Add element -list.Insert(0, 0); // Insert at index -list.Remove(2); // Remove first occurrence -list.RemoveAt(1); // Remove at index -list.Clear(); // Remove all +list.Add(4); // Добавить элемент +list.Insert(0, 0); // Вставить по индексу +list.Remove(2); // Удалить первое вхождение +list.RemoveAt(1); // Удалить по индексу +list.Clear(); // Удалить все -int first = list[0]; // Index access -int count = list.Count; // Get count -bool contains = list.Contains(3); // Check if contains +int first = list[0]; // Доступ по индексу +int count = list.Count; // Получить количество +bool contains = list.Contains(3); // Проверка наличия ``` ```rust -// Rust Vec operations +// Операции с Vec в Rust let mut vec = vec![1, 2, 3]; -vec.push(4); // Add element -vec.insert(0, 0); // Insert at index -vec.retain(|&x| x != 2); // Remove elements (functional style) -vec.remove(1); // Remove at index -vec.clear(); // Remove all +vec.push(4); // Добавить элемент +vec.insert(0, 0); // Вставить по индексу +vec.retain(|&x| x != 2); // Удалить элементы (в функциональном стиле) +vec.remove(1); // Удалить по индексу +vec.clear(); // Удалить все -let first = vec[0]; // Index access (panics if out of bounds) -let safe_first = vec.get(0); // Safe access, returns Option<&T> -let count = vec.len(); // Get count -let contains = vec.contains(&3); // Check if contains +let first = vec[0]; // Доступ по индексу (паника, если выход за границы) +let safe_first = vec.get(0); // Безопасный доступ, возвращает Option<&T> +let count = vec.len(); // Получить количество +let contains = vec.contains(&3); // Проверка наличия ``` -### Safe Access Patterns +### Паттерны безопасного доступа ```csharp -// C# - Exception-based bounds checking +// C# — проверка границ через исключения public int SafeAccess(List<int> list, int index) { try @@ -120,27 +120,27 @@ public int SafeAccess(List<int> list, int index) } catch (ArgumentOutOfRangeException) { - return -1; // Default value + return -1; // Значение по умолчанию } } ``` ```rust -// Rust - Option-based safe access +// Rust — безопасный доступ через Option fn safe_access(vec: &[i32], index: usize) -> Option<i32> { - vec.get(index).copied() // Returns Option<i32> + vec.get(index).copied() // Возвращает Option<i32> } fn main() { let vec = vec![1, 2, 3]; - // Safe access patterns + // Паттерны безопасного доступа match vec.get(10) { Some(value) => println!("Value: {}", value), - None => println!("Index out of bounds"), + None => println!("Индекс вне границ"), } - // Or with unwrap_or + // Или через unwrap_or let value = vec.get(10).copied().unwrap_or(-1); println!("Value: {}", value); } @@ -148,13 +148,13 @@ fn main() { *** -## HashMap vs Dictionary +## HashMap против Dictionary -HashMap is Rust's equivalent to C#'s `Dictionary<K,V>`. +HashMap — аналог `Dictionary<K,V>` из C# в Rust. -### C# Dictionary +### Dictionary в C# ```csharp -// C# Dictionary<TKey, TValue> +// Dictionary<TKey, TValue> в C# var scores = new Dictionary<string, int> { ["Alice"] = 100, @@ -162,11 +162,11 @@ var scores = new Dictionary<string, int> ["Charlie"] = 92 }; -// Add/Update +// Добавление/обновление scores["Dave"] = 78; -scores["Alice"] = 105; // Update existing +scores["Alice"] = 105; // Обновление существующего -// Safe access +// Безопасный доступ if (scores.TryGetValue("Eve", out int score)) { Console.WriteLine($"Eve's score: {score}"); @@ -176,192 +176,192 @@ else Console.WriteLine("Eve not found"); } -// Iteration +// Итерация foreach (var kvp in scores) { Console.WriteLine($"{kvp.Key}: {kvp.Value}"); } ``` -### Rust HashMap +### HashMap в Rust ```rust use std::collections::HashMap; -// Create and initialize HashMap +// Создание и инициализация HashMap let mut scores = HashMap::new(); scores.insert("Alice".to_string(), 100); scores.insert("Bob".to_string(), 85); scores.insert("Charlie".to_string(), 92); -// Or use from iterator +// Или из итератора let scores: HashMap<String, i32> = [ ("Alice".to_string(), 100), ("Bob".to_string(), 85), ("Charlie".to_string(), 92), ].into_iter().collect(); -// Add/Update -let mut scores = scores; // Make mutable +// Добавление/обновление +let mut scores = scores; // Делаем изменяемой scores.insert("Dave".to_string(), 78); -scores.insert("Alice".to_string(), 105); // Update existing +scores.insert("Alice".to_string(), 105); // Обновление существующего -// Safe access +// Безопасный доступ match scores.get("Eve") { Some(score) => println!("Eve's score: {}", score), None => println!("Eve not found"), } -// Iteration +// Итерация for (name, score) in &scores { println!("{}: {}", name, score); } ``` -### HashMap Operations +### Операции с HashMap ```csharp -// C# Dictionary operations +// Операции с Dictionary в C# var dict = new Dictionary<string, int>(); -dict["key"] = 42; // Insert/update -bool exists = dict.ContainsKey("key"); // Check existence -bool removed = dict.Remove("key"); // Remove -dict.Clear(); // Clear all +dict["key"] = 42; // Вставка/обновление +bool exists = dict.ContainsKey("key"); // Проверка наличия +bool removed = dict.Remove("key"); // Удаление +dict.Clear(); // Очистка -// Get with default +// Получение со значением по умолчанию int value = dict.GetValueOrDefault("missing", 0); ``` ```rust use std::collections::HashMap; -// Rust HashMap operations +// Операции с HashMap в Rust let mut map = HashMap::new(); -map.insert("key".to_string(), 42); // Insert/update -let exists = map.contains_key("key"); // Check existence -let removed = map.remove("key"); // Remove, returns Option<V> -map.clear(); // Clear all +map.insert("key".to_string(), 42); // Вставка/обновление +let exists = map.contains_key("key"); // Проверка наличия +let removed = map.remove("key"); // Удаление, возвращает Option<V> +map.clear(); // Очистка -// Entry API for advanced operations +// Entry API для продвинутых операций let mut map = HashMap::new(); -map.entry("key".to_string()).or_insert(42); // Insert if not exists -map.entry("key".to_string()).and_modify(|v| *v += 1); // Modify if exists +map.entry("key".to_string()).or_insert(42); // Вставить, если не существует +map.entry("key".to_string()).and_modify(|v| *v += 1); // Изменить, если существует -// Get with default +// Получение со значением по умолчанию let value = map.get("missing").copied().unwrap_or(0); ``` -### Ownership with HashMap Keys and Values +### Владение ключами и значениями HashMap ```rust -// Understanding ownership with HashMap +// Понимание владения в HashMap fn ownership_example() { let mut map = HashMap::new(); - // String keys and values are moved into the map + // Строковые ключи и значения перемещаются в map let key = String::from("name"); let value = String::from("Alice"); map.insert(key, value); - // println!("{}", key); // ❌ Error: key was moved - // println!("{}", value); // ❌ Error: value was moved + // println!("{}", key); // ❌ Ошибка: key было перемещено + // println!("{}", value); // ❌ Ошибка: value было перемещено - // Access via references + // Доступ через ссылки if let Some(name) = map.get("name") { - println!("Name: {}", name); // Borrowing the value + println!("Name: {}", name); // Заимствуем значение } } -// Using &str keys (no ownership transfer) +// Использование ключей &str (без передачи владения) fn string_slice_keys() { let mut map = HashMap::new(); - map.insert("name", "Alice"); // &str keys and values + map.insert("name", "Alice"); // Ключи и значения типа &str map.insert("age", "30"); - // No ownership issues with string literals + // Со строковыми литералами проблем с владением нет println!("Name exists: {}", map.contains_key("name")); } ``` *** -## Working with Collections +## Работа с коллекциями -### Iteration Patterns +### Паттерны итерации ```csharp -// C# iteration patterns +// Паттерны итерации в C# var numbers = new List<int> { 1, 2, 3, 4, 5 }; -// For loop with index +// Цикл for с индексом for (int i = 0; i < numbers.Count; i++) { Console.WriteLine($"Index {i}: {numbers[i]}"); } -// Foreach loop +// Цикл foreach foreach (int num in numbers) { Console.WriteLine(num); } -// LINQ methods +// Методы LINQ var doubled = numbers.Select(x => x * 2).ToList(); var evens = numbers.Where(x => x % 2 == 0).ToList(); ``` ```rust -// Rust iteration patterns +// Паттерны итерации в Rust let numbers = vec![1, 2, 3, 4, 5]; -// For loop with index +// Цикл for с индексом for (i, num) in numbers.iter().enumerate() { println!("Index {}: {}", i, num); } -// For loop over values -for num in &numbers { // Borrow each element +// Цикл for по значениям +for num in &numbers { // Заимствуем каждый элемент println!("{}", num); } -// Iterator methods (like LINQ) +// Методы итераторов (как LINQ) let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect(); let evens: Vec<i32> = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect(); -// Or more efficiently, consuming iterator +// Или эффективнее — потребляющий итератор let doubled: Vec<i32> = numbers.into_iter().map(|x| x * 2).collect(); ``` -### Iterator vs IntoIterator vs Iter +### Iterator против IntoIterator против Iter ```rust -// Understanding different iteration methods +// Понимание различных способов итерации fn iteration_methods() { let vec = vec![1, 2, 3, 4, 5]; - // 1. iter() - borrows elements (&T) + // 1. iter() — заимствует элементы (&T) for item in vec.iter() { - println!("{}", item); // item is &i32 + println!("{}", item); // item имеет тип &i32 } - // vec is still usable here + // vec здесь по-прежнему доступен - // 2. into_iter() - takes ownership (T) + // 2. into_iter() — забирает владение (T) for item in vec.into_iter() { - println!("{}", item); // item is i32 + println!("{}", item); // item имеет тип i32 } - // vec is no longer usable here + // vec здесь больше недоступен let mut vec = vec![1, 2, 3, 4, 5]; - // 3. iter_mut() - mutable borrows (&mut T) + // 3. iter_mut() — изменяемые заимствования (&mut T) for item in vec.iter_mut() { - *item *= 2; // item is &mut i32 + *item *= 2; // item имеет тип &mut i32 } println!("{:?}", vec); // [2, 4, 6, 8, 10] } ``` -### Collecting Results +### Сбор результатов ```csharp -// C# - Processing collections with potential errors +// C# — обработка коллекций с возможными ошибками public List<int> ParseNumbers(List<string> inputs) { var results = new List<int>(); @@ -371,37 +371,37 @@ public List<int> ParseNumbers(List<string> inputs) { results.Add(result); } - // Silently skip invalid inputs + // Молча пропускаем некорректные значения } return results; } ``` ```rust -// Rust - Explicit error handling with collect +// Rust — явная обработка ошибок через collect fn parse_numbers(inputs: Vec<String>) -> Result<Vec<i32>, std::num::ParseIntError> { inputs.into_iter() - .map(|s| s.parse::<i32>()) // Returns Result<i32, ParseIntError> - .collect() // Collects into Result<Vec<i32>, ParseIntError> + .map(|s| s.parse::<i32>()) // Возвращает Result<i32, ParseIntError> + .collect() // Собирает в Result<Vec<i32>, ParseIntError> } -// Alternative: Filter out errors +// Альтернатива: отфильтровать ошибки fn parse_numbers_filter(inputs: Vec<String>) -> Vec<i32> { inputs.into_iter() - .filter_map(|s| s.parse::<i32>().ok()) // Keep only Ok values + .filter_map(|s| s.parse::<i32>().ok()) // Оставляем только значения Ok .collect() } fn main() { let inputs = vec!["1".to_string(), "2".to_string(), "invalid".to_string(), "4".to_string()]; - // Version that fails on first error + // Вариант, который завершается при первой ошибке match parse_numbers(inputs.clone()) { Ok(numbers) => println!("All parsed: {:?}", numbers), Err(error) => println!("Parse error: {}", error), } - // Version that skips errors + // Вариант, который пропускает ошибки let numbers = parse_numbers_filter(inputs); println!("Successfully parsed: {:?}", numbers); // [1, 2, 4] } @@ -409,12 +409,12 @@ fn main() { --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: LINQ to Iterators</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: из LINQ в итераторы</strong> (нажмите, чтобы раскрыть)</summary> -Translate this C# LINQ query to idiomatic Rust iterators: +Переведите этот LINQ-запрос на C# в идиоматичные итераторы Rust: ```csharp var result = students @@ -425,22 +425,22 @@ var result = students .ToList(); ``` -Use this struct: +Используйте эту структуру: ```rust struct Student { name: String, grade: u32 } ``` -Return a `Vec<String>` of the top 3 students with grade ≥ 90, formatted as `"Name: Grade"`. +Верните `Vec<String>` из трёх лучших студентов с оценкой ≥ 90, отформатированных как `"Name: Grade"`. <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug)] struct Student { name: String, grade: u32 } fn top_students(students: &mut [Student]) -> Vec<String> { - students.sort_by(|a, b| b.grade.cmp(&a.grade)); // sort descending + students.sort_by(|a, b| b.grade.cmp(&a.grade)); // сортировка по убыванию students.iter() .filter(|s| s.grade >= 90) .take(3) @@ -462,7 +462,7 @@ fn main() { } ``` -**Key difference from C#**: Rust iterators are lazy (like LINQ), but `.sort_by()` is eager and in-place — there's no lazy `OrderBy`. You sort first, then chain lazy operations. +**Ключевое отличие от C#**: итераторы Rust ленивы (как LINQ), но `.sort_by()` — энергичная операция, которая сортирует на месте; ленивого `OrderBy` здесь нет. Сначала сортируете, затем строите цепочку ленивых операций. </details> </details> diff --git a/csharp-book/src/ch05-data-structures-and-collections.md b/csharp-book/src/ch05-data-structures-and-collections.md index b9fcf2c..4e31230 100644 --- a/csharp-book/src/ch05-data-structures-and-collections.md +++ b/csharp-book/src/ch05-data-structures-and-collections.md @@ -1,99 +1,100 @@ -## Tuples and Destructuring +## Кортежи и деструктуризация -> **What you'll learn:** Rust tuples vs C# `ValueTuple`, arrays and slices, structs vs classes, -> the newtype pattern for domain modeling with zero-cost type safety, and destructuring syntax. +> **Что вы узнаете:** кортежи Rust против `ValueTuple` в C#, массивы и срезы, структуры против классов, +> паттерн newtype для моделирования предметной области с безопасностью типов без накладных расходов, +> а также синтаксис деструктуризации. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -C# has `ValueTuple` (since C# 7). Rust tuples are similar but more deeply integrated into the language. +В C# есть `ValueTuple` (начиная с C# 7). Кортежи Rust похожи на них, но более глубоко интегрированы в язык. -### C# Tuples +### Кортежи в C# ```csharp -// C# ValueTuple (C# 7+) +// ValueTuple в C# (C# 7+) var point = (10, 20); // (int, int) -var named = (X: 10, Y: 20); // Named elements +var named = (X: 10, Y: 20); // Именованные элементы Console.WriteLine($"{named.X}, {named.Y}"); -// Tuple as return type +// Кортеж как возвращаемый тип public (int Quotient, int Remainder) Divide(int a, int b) { return (a / b, a % b); } -var (q, r) = Divide(10, 3); // Deconstruction +var (q, r) = Divide(10, 3); // Деконструкция Console.WriteLine($"{q} remainder {r}"); -// Discards -var (_, remainder) = Divide(10, 3); // Ignore quotient +// Пропуски (discards) +var (_, remainder) = Divide(10, 3); // Игнорируем частное ``` -### Rust Tuples +### Кортежи в Rust ```rust -// Rust tuples — immutable by default, no named elements +// Кортежи Rust — неизменяемы по умолчанию, без именованных элементов let point = (10, 20); // (i32, i32) let point3d: (f64, f64, f64) = (1.0, 2.0, 3.0); -// Access by index (0-based) +// Доступ по индексу (с нуля) println!("x={}, y={}", point.0, point.1); -// Tuple as return type +// Кортеж как возвращаемый тип fn divide(a: i32, b: i32) -> (i32, i32) { (a / b, a % b) } -let (q, r) = divide(10, 3); // Destructuring +let (q, r) = divide(10, 3); // Деструктуризация println!("{q} remainder {r}"); -// Discards with _ +// Пропуск через _ let (_, remainder) = divide(10, 3); -// Unit type () — the "empty tuple" (like C# void) -fn greet() { // implicit return type is () +// Тип unit () — «пустой кортеж» (аналог void в C#) +fn greet() { // неявный возвращаемый тип — () println!("hi"); } ``` -### Key Differences +### Ключевые различия -| Feature | C# `ValueTuple` | Rust Tuple | +| Характеристика | `ValueTuple` в C# | Кортеж Rust | |---------|-----------------|------------| -| Named elements | `(int X, int Y)` | Not supported — use structs | -| Max arity | ~8 (nesting for more) | Unlimited (practical limit ~12) | -| Comparisons | Automatic | Automatic for tuples ≤ 12 elements | -| Used as dict key | Yes | Yes (if elements implement `Hash`) | -| Return from functions | Common | Common | -| Mutable elements | Always mutable | Only with `let mut` | - -### Tuple Structs (Newtypes) +| Именованные элементы | `(int X, int Y)` | Не поддерживаются — используйте структуры | +| Максимальная арность | ~8 (для большего — вложенность) | Без ограничений (практически ~12) | +| Сравнение | Автоматически | Автоматически для кортежей до 12 элементов | +| Использование как ключ словаря | Да | Да (если элементы реализуют `Hash`) | +| Возврат из функций | Часто | Часто | +| Изменяемые элементы | Всегда изменяемы | Только при `let mut` | + +### Кортежные структуры (newtype) ```rust -// When a plain tuple isn't descriptive enough, use a tuple struct: -struct Meters(f64); // Single-field "newtype" wrapper +// Когда обычного кортежа недостаточно, используйте кортежную структуру: +struct Meters(f64); // Обёртка «newtype» с одним полем struct Celsius(f64); struct Fahrenheit(f64); -// The compiler treats these as DIFFERENT types: +// Компилятор считает их РАЗНЫМИ типами: let distance = Meters(100.0); let temp = Celsius(36.6); -// distance == temp; // ❌ ERROR: can't compare Meters with Celsius +// distance == temp; // ❌ ОШИБКА: нельзя сравнивать Meters и Celsius -// Newtype pattern prevents unit-confusion bugs at compile time! -// In C# you'd need a full class/struct for the same safety. +// Паттерн newtype предотвращает ошибки перепутанных единиц на этапе компиляции! +// В C# для той же безопасности понадобился бы полноценный класс или структура. ``` ```csharp -// C# equivalent requires more ceremony: +// В C# аналог требует больше церемоний: public readonly record struct Meters(double Value); public readonly record struct Celsius(double Value); -// Not interchangeable, but records add overhead vs Rust's zero-cost newtypes +// Они не взаимозаменяемы, но records добавляют накладные расходы по сравнению с newtype в Rust, которые не несут затрат ``` -### The Newtype Pattern in Depth: Domain Modeling with Zero Cost +### Паттерн newtype в деталях: моделирование предметной области без затрат -Newtypes go far beyond preventing unit confusion. They're Rust's primary tool for **encoding business rules into the type system** — replacing the "guard clause" and "validation class" patterns common in C#. +Newtype'ы гораздо полезнее, чем защита от путаницы единиц измерения. Это основной инструмент Rust для **кодирования бизнес-правил в системе типов** — он заменяет шаблоны «защитного условия» и «класса валидации», распространённые в C#. -#### C# Validation Approach: Runtime Guards +#### Подход C# к валидации: проверки во время выполнения ```csharp -// C# — validation happens at runtime, every time +// C# — валидация происходит во время выполнения, каждый раз public class UserService { public User CreateUser(string email, int age) @@ -108,21 +109,21 @@ public class UserService public void SendEmail(string email) { - // Must re-validate — or trust the caller? + // Нужно перепроверить — или доверять вызывающему коду? if (!email.Contains('@')) throw new ArgumentException("Invalid email"); // ... } } ``` -#### Rust Newtype Approach: Compile-Time Proof +#### Подход Rust с newtype: доказательство на этапе компиляции ```rust -/// A validated email address — the type itself IS the proof of validity. +/// Проверенный адрес электронной почты — сам тип является доказательством корректности. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Email(String); impl Email { - /// The ONLY way to create an Email — validation happens once at construction. + /// ЕДИНСТВЕННЫЙ способ создать Email — валидация выполняется один раз, при создании. pub fn new(raw: &str) -> Result<Self, &'static str> { if raw.contains('@') && raw.len() > 3 { Ok(Email(raw.to_lowercase())) @@ -131,11 +132,11 @@ impl Email { } } - /// Safe access to the inner value + /// Безопасный доступ к внутреннему значению pub fn as_str(&self) -> &str { &self.0 } } -/// A validated age — impossible to create an invalid one. +/// Проверенный возраст — невозможно создать некорректный. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Age(u8); @@ -146,90 +147,90 @@ impl Age { pub fn value(&self) -> u8 { self.0 } } -// Now functions take PROVEN types — no re-validation needed! +// Теперь функции принимают ДОКАЗАННЫЕ типы — повторная валидация не нужна! fn create_user(email: Email, age: Age) -> User { - // email is GUARANTEED valid — it's a type invariant + // email ГАРАНТИРОВАННО корректен — это инвариант типа User { email, age } } fn send_email(to: &Email) { - // No validation needed — Email type proves validity + // Валидация не нужна — тип Email доказывает корректность println!("Sending to: {}", to.as_str()); } ``` -#### Common Newtype Uses for C# Developers +#### Типичные применения newtype для разработчиков C# -| C# Pattern | Rust Newtype | What It Prevents | +| Паттерн в C# | Newtype в Rust | Что предотвращает | |------------|-------------|------------------| -| `string` for UserId, Email, etc. | `struct UserId(Uuid)` | Passing wrong string to wrong parameter | -| `int` for Port, Count, Index | `struct Port(u16)` | Port and Count are not interchangeable | -| Guard clauses everywhere | Constructor validation once | Re-validation, missed validation | -| `decimal` for USD, EUR | `struct Usd(Decimal)` | Adding USD to EUR by accident | -| `TimeSpan` for different semantics | `struct Timeout(Duration)` | Passing connection timeout as request timeout | +| `string` для UserId, Email и т. д. | `struct UserId(Uuid)` | Передачу неверной строки в неверный параметр | +| `int` для Port, Count, Index | `struct Port(u16)` | Port и Count нельзя перепутать | +| Защитные условия повсюду | Однократная валидация в конструкторе | Повторную проверку, пропущенную валидацию | +| `decimal` для USD, EUR | `struct Usd(Decimal)` | Случайное сложение USD с EUR | +| `TimeSpan` с разной семантикой | `struct Timeout(Duration)` | Передачу таймаута соединения как таймаута запроса | ```rust -// Zero-cost: newtypes compile to the same assembly as the inner type. -// This Rust code: +// Без затрат: newtype компилируется в тот же машинный код, что и внутренний тип. +// Этот код на Rust: struct UserId(u64); fn lookup(id: UserId) -> Option<User> { /* ... */ } -// Generates the SAME machine code as: +// Генерирует ТОТ ЖЕ машинный код, что и: fn lookup(id: u64) -> Option<User> { /* ... */ } -// But with full type safety at compile time! +// Но с полной безопасностью типов на этапе компиляции! ``` *** -## Arrays and Slices +## Массивы и срезы -Understanding the difference between arrays, slices, and vectors is crucial. +Понимание различий между массивами, срезами и векторами крайне важно. -### C# Arrays +### Массивы в C# ```csharp -// C# arrays -int[] numbers = new int[5]; // Fixed size, heap allocated -int[] initialized = { 1, 2, 3, 4, 5 }; // Array literal +// Массивы в C# +int[] numbers = new int[5]; // Фиксированный размер, размещается в куче +int[] initialized = { 1, 2, 3, 4, 5 }; // Литерал массива -// Access +// Доступ numbers[0] = 10; int first = numbers[0]; -// Length +// Длина int length = numbers.Length; -// Array as parameter (reference type) +// Массив как параметр (ссылочный тип) void ProcessArray(int[] array) { - array[0] = 99; // Modifies original + array[0] = 99; // Изменяет исходный массив } ``` -### Rust Arrays, Slices, and Vectors +### Массивы, срезы и векторы в Rust ```rust -// 1. Arrays - Fixed size, stack allocated -let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Type: [i32; 5] -let zeros = [0; 10]; // 10 zeros +// 1. Массивы — фиксированный размер, размещаются на стеке +let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Тип: [i32; 5] +let zeros = [0; 10]; // 10 нулей -// Access +// Доступ let first = numbers[0]; -// numbers[0] = 10; // ❌ Error: arrays are immutable by default +// numbers[0] = 10; // ❌ Ошибка: массивы по умолчанию неизменяемы let mut mut_array = [1, 2, 3, 4, 5]; -mut_array[0] = 10; // ✅ Works with mut +mut_array[0] = 10; // ✅ Работает с mut -// 2. Slices - Views into arrays or vectors -let slice: &[i32] = &numbers[1..4]; // Elements 1, 2, 3 -let all_slice: &[i32] = &numbers; // Entire array as slice +// 2. Срезы — представления массивов или векторов +let slice: &[i32] = &numbers[1..4]; // Элементы 1, 2, 3 +let all_slice: &[i32] = &numbers; // Весь массив как срез -// 3. Vectors - Dynamic size, heap allocated (covered earlier) +// 3. Векторы — динамический размер, размещаются в куче (рассмотрены ранее) let mut vec = vec![1, 2, 3, 4, 5]; -vec.push(6); // Can grow +vec.push(6); // Может расти ``` -### Slices as Function Parameters +### Срезы как параметры функций ```csharp -// C# - Method that works with arrays +// C# — метод, работающий с массивами public void ProcessNumbers(int[] numbers) { for (int i = 0; i < numbers.Length; i++) @@ -238,15 +239,15 @@ public void ProcessNumbers(int[] numbers) } } -// Works with arrays only +// Работает только с массивами ProcessNumbers(new int[] { 1, 2, 3 }); ``` ```rust -// Rust - Function that works with any sequence -fn process_numbers(numbers: &[i32]) { // Slice parameter +// Rust — функция, работающая с любой последовательностью +fn process_numbers(numbers: &[i32]) { // Параметр-срез for (i, num) in numbers.iter().enumerate() { - println!("Index {}: {}", i, num); + println!("Индекс {}: {}", i, num); } } @@ -254,16 +255,16 @@ fn main() { let array = [1, 2, 3, 4, 5]; let vec = vec![1, 2, 3, 4, 5]; - // Same function works with both! - process_numbers(&array); // Array as slice - process_numbers(&vec); // Vector as slice - process_numbers(&vec[1..4]); // Partial slice + // Одна и та же функция работает с обоими! + process_numbers(&array); // Массив как срез + process_numbers(&vec); // Вектор как срез + process_numbers(&vec[1..4]); // Частичный срез } ``` -### String Slices (&str) Revisited +### Строковые срезы (&str) — повторно ```rust -// String and &str relationship +// Связь между String и &str fn string_slice_example() { let owned = String::from("Hello, World!"); let slice: &str = &owned[0..5]; // "Hello" @@ -272,10 +273,10 @@ fn string_slice_example() { println!("{}", slice); // "Hello" println!("{}", slice2); // "World!" - // Function that accepts any string type + // Функция, принимающая любой строковый тип print_string("String literal"); // &str - print_string(&owned); // String as &str - print_string(slice); // &str slice + print_string(&owned); // String как &str + print_string(slice); // Срез &str } fn print_string(s: &str) { @@ -283,25 +284,25 @@ fn print_string(s: &str) { } ``` -### Modern C#: Span\<T\> and Inline Arrays +### Современный C#: Span\<T\> и встроенные массивы -C# has evolved beyond traditional arrays. `Span<T>` provides type-safe, contiguous memory views that can live on the stack, while Inline Arrays (C# 12) offer fixed-size stack buffers. +C# ушёл далеко от традиционных массивов. `Span<T>` предоставляет типобезопасные представления непрерывной памяти, которые могут находиться на стеке, а встроенные массивы (C# 12) дают буферы фиксированного размера на стеке. ```csharp -// C# Span<T> - view into contiguous memory +// Span<T> в C# — представление непрерывной памяти Span<int> span = stackalloc int[] { 1, 2, 3, 4, 5 }; span[0] = 10; ReadOnlySpan<char> text = "Hello".AsSpan(); -// Method accepting any contiguous memory view +// Метод, принимающий любое представление непрерывной памяти void ProcessSpan(ReadOnlySpan<int> data) { for (int i = 0; i < data.Length; i++) Console.WriteLine(data[i]); } -// Inline Arrays (C# 12) - fixed-size stack buffer +// Встроенные массивы (C# 12) — буфер фиксированного размера на стеке [InlineArray(5)] struct IntBuffer { @@ -310,7 +311,7 @@ struct IntBuffer ``` ```rust -// Rust &[T] / &mut [T] - borrowed view into contiguous memory +// Rust &[T] / &mut [T] — заимствованное представление непрерывной памяти let mut array = [1, 2, 3, 4, 5]; let slice: &mut [i32] = &mut array; slice[0] = 10; @@ -318,55 +319,55 @@ slice[0] = 10; let slice: &[i32] = &array; let text: &str = "Hello"; -// Function accepting any sequential data +// Функция, принимающая любые последовательные данные fn process_slice(data: &[i32]) { for (i, num) in data.iter().enumerate() { - println!("Index {}: {}", i, num); + println!("Индекс {}: {}", i, num); } } -// Fixed-size arrays (stack allocated) +// Массивы фиксированного размера (размещаются на стеке) let buffer: [i32; 5] = [0; 5]; ``` | C# | Rust | |----|------| -| `Span<T>` (ref struct, stack-only) | `&mut [T]` / `&[T]` (borrowed slice) | -| `ReadOnlySpan<T>` | `&[T]` (immutable slice) | -| `ReadOnlySpan<char>` / `string.AsSpan()` | `&str` (string slice) | -| `[InlineArray(N)]` struct (C# 12) | `[T; N]` (fixed-size array) | -| `stackalloc T[]` with `Span<T>` | `let arr: [T; N] = ...` (local array) | +| `Span<T>` (ref struct, только на стеке) | `&mut [T]` / `&[T]` (заимствованный срез) | +| `ReadOnlySpan<T>` | `&[T]` (неизменяемый срез) | +| `ReadOnlySpan<char>` / `string.AsSpan()` | `&str` (строковый срез) | +| Структура `[InlineArray(N)]` (C# 12) | `[T; N]` (массив фиксированного размера) | +| `stackalloc T[]` с `Span<T>` | `let arr: [T; N] = ...` (локальный массив) | -> **Key insight:** Rust's `&[T]` combines the role of C#'s `ArraySegment<T>`, `Span<T>`, and `ReadOnlySpan<T>` — it's a fat pointer (pointer + length) that works with arrays, vectors, and subslices. C#'s Inline Arrays map naturally to Rust's `[T; N]` arrays, which are also stack-allocated by default. +> **Ключевая мысль:** `&[T]` в Rust совмещает роли `ArraySegment<T>`, `Span<T>` и `ReadOnlySpan<T>` из C# — это «толстый» указатель (указатель + длина), который работает с массивами, векторами и подсрезами. Встроенные массивы C# естественно соответствуют массивам `[T; N]` в Rust, которые тоже по умолчанию размещаются на стеке. *** -## Structs vs Classes +## Структуры против классов -Structs in Rust are similar to classes in C#, but with some key differences around ownership and methods. +Структуры в Rust похожи на классы C#, но имеют несколько ключевых отличий, связанных с владением и методами. ```mermaid graph TD - subgraph "C# Class (Heap)" - CObj["Object Header<br/>+ vtable ptr"] --> CFields["Name: string ref<br/>Age: int<br/>Hobbies: List ref"] - CFields --> CHeap1["#quot;Alice#quot; on heap"] - CFields --> CHeap2["List<string> on heap"] + subgraph "Класс C# (куча)" + CObj["Заголовок объекта<br/>+ указатель на vtable"] --> CFields["Name: ссылка на string<br/>Age: int<br/>Hobbies: ссылка на List"] + CFields --> CHeap1["#quot;Alice#quot; в куче"] + CFields --> CHeap2["List<string> в куче"] end - subgraph "Rust Struct (Stack)" + subgraph "Структура Rust (стек)" RFields["name: String<br/> ptr | len | cap<br/>age: i32<br/>hobbies: Vec<br/> ptr | len | cap"] - RFields --> RHeap1["#quot;Alice#quot; heap buffer"] - RFields --> RHeap2["Vec heap buffer"] + RFields --> RHeap1["#quot;Alice#quot; буфер в куче"] + RFields --> RHeap2["Буфер Vec в куче"] end style CObj fill:#bbdefb,color:#000 style RFields fill:#c8e6c9,color:#000 ``` -> **Key insight**: C# classes always live on the heap behind a reference. Rust structs live on the stack by default — only the dynamically-sized data (like `String` contents) goes to the heap. This eliminates GC overhead for small, frequently-created objects. +> **Ключевая мысль**: классы C# всегда живут в куче за ссылкой. Структуры Rust по умолчанию живут на стеке — в куче находятся только данные динамического размера (например, содержимое `String`). Это устраняет накладные расходы GC для небольших часто создаваемых объектов. -### C# Class Definition +### Определение класса в C# ```csharp -// C# class with properties and methods +// Класс C# со свойствами и методами public class Person { public string Name { get; set; } @@ -392,18 +393,18 @@ public class Person } ``` -### Rust Struct Definition +### Определение структуры в Rust ```rust -// Rust struct with associated functions and methods -#[derive(Debug)] // Automatically implement Debug trait +// Структура Rust с ассоциированными функциями и методами +#[derive(Debug)] // Автоматически реализует трейт Debug pub struct Person { - pub name: String, // Public field - pub age: u32, // Public field - hobbies: Vec<String>, // Private field (no pub) + pub name: String, // Публичное поле + pub age: u32, // Публичное поле + hobbies: Vec<String>, // Приватное поле (без pub) } impl Person { - // Associated function (like static method) + // Ассоциированная функция (аналог статического метода) pub fn new(name: String, age: u32) -> Person { Person { name, @@ -412,26 +413,26 @@ impl Person { } } - // Method (takes &self, &mut self, or self) + // Метод (принимает &self, &mut self или self) pub fn add_hobby(&mut self, hobby: String) { self.hobbies.push(hobby); } - // Method that borrows immutably + // Метод, который заимствует неизменяемо pub fn get_info(&self) -> String { format!("{} is {} years old", self.name, self.age) } - // Getter for private field + // Геттер для приватного поля pub fn hobbies(&self) -> &Vec<String> { &self.hobbies } } ``` -### Creating and Using Instances +### Создание и использование экземпляров ```csharp -// C# object creation and usage +// Создание и использование объекта в C# var person = new Person("Alice", 30); person.AddHobby("Reading"); person.AddHobby("Swimming"); @@ -439,12 +440,12 @@ person.AddHobby("Swimming"); Console.WriteLine(person.GetInfo()); Console.WriteLine($"Hobbies: {string.Join(", ", person.Hobbies)}"); -// Modify properties directly +// Прямое изменение свойств person.Age = 31; ``` ```rust -// Rust struct creation and usage +// Создание и использование структуры в Rust let mut person = Person::new("Alice".to_string(), 30); person.add_hobby("Reading".to_string()); person.add_hobby("Swimming".to_string()); @@ -452,40 +453,40 @@ person.add_hobby("Swimming".to_string()); println!("{}", person.get_info()); println!("Hobbies: {:?}", person.hobbies()); -// Modify public fields directly +// Прямое изменение публичных полей person.age = 31; -// Debug print the entire struct +// Вывод всей структуры в отладочном формате println!("{:?}", person); ``` -### Struct Initialization Patterns +### Паттерны инициализации структур ```csharp -// C# object initialization +// Инициализация объекта в C# var person = new Person("Bob", 25) { Hobbies = new List<string> { "Gaming", "Coding" } }; -// Anonymous types +// Анонимные типы var anonymous = new { Name = "Charlie", Age = 35 }; ``` ```rust -// Rust struct initialization +// Инициализация структуры в Rust let person = Person { name: "Bob".to_string(), age: 25, hobbies: vec!["Gaming".to_string(), "Coding".to_string()], }; -// Struct update syntax (like object spread) +// Синтаксис обновления структуры (аналог spread для объектов) let older_person = Person { age: 26, - ..person // Use remaining fields from person (moves person!) + ..person // Берём остальные поля из person (перемещает person!) }; -// Tuple structs (like anonymous types) +// Кортежные структуры (аналог анонимных типов) #[derive(Debug)] struct Point(i32, i32); @@ -495,35 +496,35 @@ println!("Point: ({}, {})", point.0, point.1); *** -## Methods and Associated Functions +## Методы и ассоциированные функции -Understanding the difference between methods and associated functions is key. +Понимание различий между методами и ассоциированными функциями — ключ к успеху. -### C# Method Types +### Виды методов в C# ```csharp public class Calculator { private int memory = 0; - // Instance method + // Метод экземпляра public int Add(int a, int b) { return a + b; } - // Instance method that uses state + // Метод экземпляра, который использует состояние public void StoreInMemory(int value) { memory = value; } - // Static method + // Статический метод public static int Multiply(int a, int b) { return a * b; } - // Static factory method + // Статический фабричный метод public static Calculator CreateWithMemory(int initialMemory) { var calc = new Calculator(); @@ -533,7 +534,7 @@ public class Calculator } ``` -### Rust Method Types +### Виды методов в Rust ```rust #[derive(Debug)] pub struct Calculator { @@ -541,104 +542,104 @@ pub struct Calculator { } impl Calculator { - // Associated function (like static method) - no self parameter + // Ассоциированная функция (аналог статического метода) — без параметра self pub fn new() -> Calculator { Calculator { memory: 0 } } - // Associated function with parameters + // Ассоциированная функция с параметрами pub fn with_memory(initial_memory: i32) -> Calculator { Calculator { memory: initial_memory } } - // Method that borrows immutably (&self) + // Метод, который заимствует неизменяемо (&self) pub fn add(&self, a: i32, b: i32) -> i32 { a + b } - // Method that borrows mutably (&mut self) + // Метод, который заимствует изменяемо (&mut self) pub fn store_in_memory(&mut self, value: i32) { self.memory = value; } - // Method that takes ownership (self) + // Метод, который забирает владение (self) pub fn into_memory(self) -> i32 { - self.memory // Calculator is consumed + self.memory // Calculator потребляется } - // Getter method + // Метод-геттер pub fn memory(&self) -> i32 { self.memory } } fn main() { - // Associated functions called with :: + // Ассоциированные функции вызываются через :: let mut calc = Calculator::new(); let calc2 = Calculator::with_memory(42); - // Methods called with . + // Методы вызываются через . let result = calc.add(5, 3); calc.store_in_memory(result); println!("Memory: {}", calc.memory()); - // Consuming method - let memory_value = calc.into_memory(); // calc is no longer usable + // Метод, потребляющий значение + let memory_value = calc.into_memory(); // calc больше нельзя использовать println!("Final memory: {}", memory_value); } ``` -### Method Receiver Types Explained +### Виды получателей методов (receiver) ```rust impl Person { - // &self - Immutable borrow (most common) - // Use when you only need to read the data + // &self — неизменяемое заимствование (самый частый вариант) + // Используйте, когда нужно только читать данные pub fn get_name(&self) -> &str { &self.name } - // &mut self - Mutable borrow - // Use when you need to modify the data + // &mut self — изменяемое заимствование + // Используйте, когда нужно изменять данные pub fn set_name(&mut self, name: String) { self.name = name; } - // self - Take ownership (less common) - // Use when you want to consume the struct + // self — забрать владение (встречается реже) + // Используйте, когда структура должна быть потреблена pub fn consume(self) -> String { - self.name // Person is moved, no longer accessible + self.name // Person перемещён, больше недоступен } } fn method_examples() { let mut person = Person::new("Alice".to_string(), 30); - // Immutable borrow - let name = person.get_name(); // person can still be used + // Неизменяемое заимствование + let name = person.get_name(); // person по-прежнему можно использовать println!("Name: {}", name); - // Mutable borrow - person.set_name("Alice Smith".to_string()); // person can still be used + // Изменяемое заимствование + person.set_name("Alice Smith".to_string()); // person по-прежнему можно использовать - // Taking ownership - let final_name = person.consume(); // person is no longer usable + // Забираем владение + let final_name = person.consume(); // person больше нельзя использовать println!("Final name: {}", final_name); } ``` --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Slice Window Average</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: скользящее среднее по срезу</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Write a function that takes a slice of `f64` values and a window size, and returns a `Vec<f64>` of rolling averages. For example, `[1.0, 2.0, 3.0, 4.0, 5.0]` with window 3 → `[2.0, 3.0, 4.0]`. +**Задача**: напишите функцию, которая принимает срез значений `f64` и размер окна, и возвращает `Vec<f64>` со скользящими средними. Например, `[1.0, 2.0, 3.0, 4.0, 5.0]` с окном 3 → `[2.0, 3.0, 4.0]`. ```rust fn rolling_average(data: &[f64], window: usize) -> Vec<f64> { - // Your implementation here + // Ваша реализация здесь todo!() } @@ -650,7 +651,7 @@ fn main() { ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust fn rolling_average(data: &[f64], window: usize) -> Vec<f64> { @@ -667,25 +668,25 @@ fn main() { } ``` -**Key takeaway**: Slices have powerful built-in methods like `.windows()`, `.chunks()`, and `.split()` that replace manual index arithmetic. In C#, you'd use `Enumerable.Range` or LINQ `.Skip().Take()`. +**Ключевая мысль**: у срезов есть мощные встроенные методы вроде `.windows()`, `.chunks()` и `.split()`, которые заменяют ручную арифметику индексов. В C# вы бы использовали `Enumerable.Range` или LINQ `.Skip().Take()`. </details> </details> <details> -<summary><strong>🏋️ Exercise: Mini Address Book</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: мини-адресная книга</strong> (нажмите, чтобы раскрыть)</summary> -Build a small address book using structs, enums, and methods: +Создайте небольшую адресную книгу с использованием структур, перечислений и методов: -1. Define an enum `PhoneType { Mobile, Home, Work }` -2. Define a struct `Contact` with `name: String` and `phones: Vec<(PhoneType, String)>` -3. Implement `Contact::new(name: impl Into<String>) -> Self` -4. Implement `Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)` -5. Implement `Contact::mobile_numbers(&self) -> Vec<&str>` that returns only mobile numbers -6. In `main`, create a contact, add two phones, and print the mobile numbers +1. Определите перечисление `PhoneType { Mobile, Home, Work }` +2. Определите структуру `Contact` с полями `name: String` и `phones: Vec<(PhoneType, String)>` +3. Реализуйте `Contact::new(name: impl Into<String>) -> Self` +4. Реализуйте `Contact::add_phone(&mut self, kind: PhoneType, number: impl Into<String>)` +5. Реализуйте `Contact::mobile_numbers(&self) -> Vec<&str>`, который возвращает только мобильные номера +6. В `main` создайте контакт, добавьте два телефона и выведите мобильные номера <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug, PartialEq)] @@ -721,7 +722,7 @@ fn main() { alice.add_phone(PhoneType::Work, "+1-555-0200"); alice.add_phone(PhoneType::Mobile, "+1-555-0101"); - println!("{}'s mobile numbers: {:?}", alice.name, alice.mobile_numbers()); + println!("Мобильные номера {}: {:?}", alice.name, alice.mobile_numbers()); } ``` @@ -730,4 +731,3 @@ fn main() { *** - diff --git a/csharp-book/src/ch06-1-exhaustive-matching-and-null-safety.md b/csharp-book/src/ch06-1-exhaustive-matching-and-null-safety.md index 50c334f..95f93ba 100644 --- a/csharp-book/src/ch06-1-exhaustive-matching-and-null-safety.md +++ b/csharp-book/src/ch06-1-exhaustive-matching-and-null-safety.md @@ -1,13 +1,13 @@ -## Exhaustive Pattern Matching: Compiler Guarantees vs Runtime Errors +## Исчерпывающее сопоставление с образцом: гарантии компилятора против ошибок времени выполнения -> **What you'll learn:** Why C# `switch` expressions silently miss cases while Rust's `match` catches them at compile time, -> `Option<T>` vs `Nullable<T>` for null safety, and custom error types with `Result<T, E>`. +> **Что вы узнаете:** почему выражения `switch` в C# молча пропускают случаи, тогда как `match` в Rust ловит их на этапе компиляции, +> `Option<T>` против `Nullable<T>` для null-безопасности, а также собственные типы ошибок с `Result<T, E>`. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### C# Switch Expressions - Still Incomplete +### Выражения switch в C# — всё ещё неполные ```csharp -// C# switch expressions look exhaustive but aren't guaranteed +// Выражения switch в C# выглядят исчерпывающими, но это не гарантировано public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized } public string HandleResponse(HttpStatus status) => status switch @@ -15,11 +15,11 @@ public string HandleResponse(HttpStatus status) => status switch HttpStatus.Ok => "Success", HttpStatus.NotFound => "Resource not found", HttpStatus.ServerError => "Internal error", - // Missing Unauthorized case — compiles with warning CS8524, but NOT an error! - // Runtime: SwitchExpressionException if status is Unauthorized + // Нет варианта Unauthorized — компилируется с предупреждением CS8524, но это НЕ ошибка! + // Во время выполнения: SwitchExpressionException, если status равен Unauthorized }; -// Even with nullable warnings, this compiles: +// Даже с предупреждениями о nullable этот код компилируется: public class User { public string Name { get; set; } @@ -30,24 +30,24 @@ public string ProcessUser(User? user) => user switch { { IsActive: true } => $"Active: {user.Name}", { IsActive: false } => $"Inactive: {user.Name}", - // Missing null case — compiler warning CS8655, but NOT an error! - // Runtime: SwitchExpressionException when user is null + // Нет случая для null — предупреждение компилятора CS8655, но НЕ ошибка! + // Во время выполнения: SwitchExpressionException, если user равен null }; ``` ```csharp -// Adding an enum variant later doesn't break compilation of existing switches +// Добавление варианта в перечисление позже не ломает компиляцию существующих switch public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized, - Forbidden // Adding this produces another CS8524 warning but doesn't break compilation! + Forbidden // Добавление этого даёт ещё одно предупреждение CS8524, но не ломает компиляцию! } ``` -### Rust Pattern Matching - True Exhaustiveness +### Сопоставление с образцом в Rust — настоящая исчерпываемость ```rust #[derive(Debug)] enum HttpStatus { @@ -63,41 +63,41 @@ fn handle_response(status: HttpStatus) -> &'static str { HttpStatus::NotFound => "Resource not found", HttpStatus::ServerError => "Internal error", HttpStatus::Unauthorized => "Authentication required", - // Compiler ERROR if any case is missing! - // This literally will not compile + // Ошибка компилятора, если пропущен любой случай! + // Этот код буквально не скомпилируется } } -// Adding a new variant breaks compilation everywhere it's used +// Добавление нового варианта ломает компиляцию везде, где он используется #[derive(Debug)] enum HttpStatus { Ok, NotFound, ServerError, Unauthorized, - Forbidden, // Adding this breaks compilation in handle_response() + Forbidden, // Добавление этого ломает компиляцию в handle_response() } -// The compiler forces you to handle ALL cases +// Компилятор заставляет обработать ВСЕ случаи -// Option<T> pattern matching is also exhaustive +// Сопоставление с образцом для Option<T> тоже исчерпывающее fn process_optional_value(value: Option<i32>) -> String { match value { Some(n) => format!("Got value: {}", n), None => "No value".to_string(), - // Forgetting either case = compilation error + // Пропуск любого из случаев = ошибка компиляции } } ``` ```mermaid graph TD - subgraph "C# Pattern Matching Limitations" - CS_SWITCH["switch expression"] - CS_WARNING["⚠️ Compiler warnings only"] - CS_COMPILE["✅ Compiles successfully"] - CS_RUNTIME["💥 Runtime exceptions"] - CS_DEPLOY["❌ Bugs reach production"] - CS_SILENT["😰 Silent failures on enum changes"] + subgraph "Ограничения сопоставления с образцом в C#" + CS_SWITCH["switch-выражение"] + CS_WARNING["⚠️ Только предупреждения компилятора"] + CS_COMPILE["✅ Компилируется успешно"] + CS_RUNTIME["💥 Исключения во время выполнения"] + CS_DEPLOY["❌ Ошибки попадают в продакшен"] + CS_SILENT["😰 Тихие сбои при изменении перечислений"] CS_SWITCH --> CS_WARNING CS_WARNING --> CS_COMPILE @@ -106,13 +106,13 @@ graph TD CS_SWITCH --> CS_SILENT end - subgraph "Rust Exhaustive Matching" - RUST_MATCH["match expression"] - RUST_ERROR["🛑 Compilation fails"] - RUST_FIX["✅ Must handle all cases"] - RUST_SAFE["✅ Zero runtime surprises"] - RUST_EVOLUTION["🔄 Enum changes break compilation"] - RUST_REFACTOR["🛠️ Forced refactoring"] + subgraph "Исчерпывающее сопоставление в Rust" + RUST_MATCH["match-выражение"] + RUST_ERROR["🛑 Компиляция падает"] + RUST_FIX["✅ Нужно обработать все случаи"] + RUST_SAFE["✅ Никаких сюрпризов во время выполнения"] + RUST_EVOLUTION["🔄 Изменения перечислений ломают компиляцию"] + RUST_REFACTOR["🛠️ Принудительный рефакторинг"] RUST_MATCH --> RUST_ERROR RUST_ERROR --> RUST_FIX @@ -130,20 +130,20 @@ graph TD *** -## Null Safety: `Nullable<T>` vs `Option<T>` +## Null-безопасность: `Nullable<T>` против `Option<T>` -### C# Null Handling Evolution +### Эволюция работы с null в C# ```csharp -// C# - Traditional null handling (error-prone) +// C# — традиционная работа с null (подвержена ошибкам) public class User { - public string Name { get; set; } // Can be null! - public string Email { get; set; } // Can be null! + public string Name { get; set; } // Может быть null! + public string Email { get; set; } // Может быть null! } public string GetUserDisplayName(User user) { - if (user?.Name != null) // Null conditional operator + if (user?.Name != null) // Оператор null-условного доступа { return user.Name; } @@ -152,14 +152,14 @@ public string GetUserDisplayName(User user) ``` ```csharp -// C# 8+ Nullable Reference Types +// Nullable-ссылочные типы C# 8+ public class User { - public string Name { get; set; } // Non-nullable - public string? Email { get; set; } // Explicitly nullable + public string Name { get; set; } // Не допускает null + public string? Email { get; set; } // Явно допускает null } -// C# Nullable<T> for value types +// Nullable<T> в C# для значимых типов int? maybeNumber = GetNumber(); if (maybeNumber.HasValue) { @@ -167,18 +167,18 @@ if (maybeNumber.HasValue) } ``` -### Rust `Option<T>` System +### Система `Option<T>` в Rust ```rust -// Rust - Explicit null handling with Option<T> +// Rust — явная работа с null через Option<T> #[derive(Debug)] pub struct User { - name: String, // Never null - email: Option<String>, // Explicitly optional + name: String, // Никогда не null + email: Option<String>, // Явно необязательное } impl User { pub fn get_display_name(&self) -> &str { - &self.name // No null check needed - guaranteed to exist + &self.name // Проверка на null не нужна — значение гарантированно существует } pub fn get_email_or_default(&self) -> String { @@ -189,41 +189,41 @@ impl User { } } -// Pattern matching forces handling of None case +// Сопоставление с образцом заставляет обработать случай None fn handle_optional_user(user: Option<User>) { match user { Some(u) => println!("User: {}", u.get_display_name()), None => println!("No user found"), - // Compiler error if None case is not handled! + // Ошибка компилятора, если случай None не обработан! } } ``` ```mermaid graph TD - subgraph "C# Null Handling Evolution" - CS_NULL["Traditional: string name<br/>[ERROR] Can be null"] - CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Explicit for value types"] - CS_NRT["Nullable Reference Types<br/>string? name<br/>[WARNING] Compile-time warnings only"] + subgraph "Эволюция работы с null в C#" + CS_NULL["Традиционно: string name<br/>[ERROR] Может быть null"] + CS_NULLABLE["Nullable<T>: int? value<br/>[OK] Явно для значимых типов"] + CS_NRT["Nullable-ссылочные типы<br/>string? name<br/>[WARNING] Только предупреждения компилятора"] - CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Can still crash"] + CS_RUNTIME["Runtime NullReferenceException<br/>[ERROR] Всё ещё может упасть"] CS_NULL --> CS_RUNTIME CS_NRT -.-> CS_RUNTIME - CS_CHECKS["Manual null checks<br/>if (obj?.Property != null)"] + CS_CHECKS["Ручные проверки на null<br/>if (obj?.Property != null)"] end - subgraph "Rust Option<T> System" + subgraph "Система Option<T> в Rust" RUST_OPTION["Option<T><br/>Some(value) | None"] - RUST_FORCE["Compiler forces handling<br/>[OK] Cannot ignore None"] - RUST_MATCH["Pattern matching<br/>match option { ... }"] - RUST_METHODS["Rich API<br/>.map(), .unwrap_or(), .and_then()"] + RUST_FORCE["Компилятор заставляет обработать<br/>[OK] Нельзя проигнорировать None"] + RUST_MATCH["Сопоставление с образцом<br/>match option { ... }"] + RUST_METHODS["Богатый API<br/>.map(), .unwrap_or(), .and_then()"] RUST_OPTION --> RUST_FORCE RUST_FORCE --> RUST_MATCH RUST_FORCE --> RUST_METHODS - RUST_SAFE["Compile-time null safety<br/>[OK] No null pointer exceptions"] + RUST_SAFE["Null-безопасность на этапе компиляции<br/>[OK] Нет исключений null-указателя"] RUST_MATCH --> RUST_SAFE RUST_METHODS --> RUST_SAFE end @@ -245,18 +245,18 @@ struct Point { fn describe_point(point: Point) -> String { match point { - Point { x: 0, y: 0 } => "origin".to_string(), - Point { x: 0, y } => format!("on y-axis at y={}", y), - Point { x, y: 0 } => format!("on x-axis at x={}", x), - Point { x, y } if x == y => format!("on diagonal at ({}, {})", x, y), - Point { x, y } => format!("point at ({}, {})", x, y), + Point { x: 0, y: 0 } => "начало координат".to_string(), + Point { x: 0, y } => format!("на оси Y при y={}", y), + Point { x, y: 0 } => format!("на оси X при x={}", x), + Point { x, y } if x == y => format!("на диагонали в ({}, {})", x, y), + Point { x, y } => format!("точка в ({}, {})", x, y), } } ``` -### Option and Result Types +### Option и Result ```csharp -// C# nullable reference types (C# 8+) +// Nullable-ссылочные типы C# (C# 8+) public class PersonService { private Dictionary<int, string> people = new(); @@ -271,7 +271,7 @@ public class PersonService return FindPerson(id) ?? "Unknown"; } - // Exception-based error handling + // Обработка ошибок через исключения public void SavePerson(int id, string name) { if (string.IsNullOrEmpty(name)) @@ -285,7 +285,7 @@ public class PersonService ```rust use std::collections::HashMap; -// Rust uses Option<T> instead of null +// Rust использует Option<T> вместо null struct PersonService { people: HashMap<i32, String>, } @@ -297,12 +297,12 @@ impl PersonService { } } - // Returns Option<T> - no null! + // Возвращает Option<T> — никакого null! fn find_person(&self, id: i32) -> Option<&String> { self.people.get(&id) } - // Pattern matching on Option + // Сопоставление с образцом по Option fn get_person_or_default(&self, id: i32) -> String { match self.find_person(id) { Some(name) => name.clone(), @@ -310,14 +310,14 @@ impl PersonService { } } - // Using Option methods (more functional style) + // Использование методов Option (более функциональный стиль) fn get_person_or_default_functional(&self, id: i32) -> String { self.find_person(id) .map(|name| name.clone()) .unwrap_or_else(|| "Unknown".to_string()) } - // Result<T, E> for error handling + // Result<T, E> для обработки ошибок fn save_person(&mut self, id: i32, name: String) -> Result<(), String> { if name.is_empty() { return Err("Name cannot be empty".to_string()); @@ -327,7 +327,7 @@ impl PersonService { Ok(()) } - // Chaining operations + // Цепочка операций fn get_person_length(&self, id: i32) -> Option<usize> { self.find_person(id).map(|name| name.len()) } @@ -336,27 +336,27 @@ impl PersonService { fn main() { let mut service = PersonService::new(); - // Handle Result + // Обработка Result match service.save_person(1, "Alice".to_string()) { Ok(()) => println!("Person saved successfully"), Err(error) => println!("Error: {}", error), } - // Handle Option + // Обработка Option match service.find_person(1) { Some(name) => println!("Found: {}", name), None => println!("Person not found"), } - // Functional style with Option + // Функциональный стиль с Option let name_length = service.get_person_length(1) .unwrap_or(0); println!("Name length: {}", name_length); - // Question mark operator for early returns + // Оператор вопросительного знака для раннего возврата fn try_operation(service: &mut PersonService) -> Result<String, String> { - service.save_person(2, "Bob".to_string())?; // Early return if error - let name = service.find_person(2).ok_or("Person not found")?; // Convert Option to Result + service.save_person(2, "Bob".to_string())?; // Ранний возврат при ошибке + let name = service.find_person(2).ok_or("Person not found")?; // Преобразуем Option в Result Ok(format!("Hello, {}", name)) } @@ -367,9 +367,9 @@ fn main() { } ``` -### Custom Error Types +### Собственные типы ошибок ```rust -// Define custom error enum +// Определяем собственное перечисление ошибок #[derive(Debug)] enum PersonError { NotFound(i32), @@ -389,14 +389,14 @@ impl std::fmt::Display for PersonError { impl std::error::Error for PersonError {} -// Enhanced PersonService with custom errors +// Расширенный PersonService с собственными ошибками impl PersonService { fn save_person_enhanced(&mut self, id: i32, name: String) -> Result<(), PersonError> { if name.is_empty() || name.len() > 50 { return Err(PersonError::InvalidName(name)); } - // Simulate database operation that might fail + // Имитация операции с базой данных, которая может завершиться ошибкой if id < 0 { return Err(PersonError::DatabaseError("Negative IDs not allowed".to_string())); } @@ -413,7 +413,7 @@ impl PersonService { fn demo_error_handling() { let mut service = PersonService::new(); - // Handle different error types + // Обработка разных типов ошибок match service.save_person_enhanced(-1, "Invalid".to_string()) { Ok(()) => println!("Success"), Err(PersonError::NotFound(id)) => println!("Not found: {}", id), @@ -425,12 +425,12 @@ fn demo_error_handling() { --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Option Combinators</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: комбинаторы Option</strong> (нажмите, чтобы раскрыть)</summary> -Rewrite this deeply nested C# null-checking code using Rust `Option` combinators (`and_then`, `map`, `unwrap_or`): +Перепишите этот глубоко вложенный C#-код с проверками на null, используя комбинаторы `Option` в Rust (`and_then`, `map`, `unwrap_or`): ```csharp string GetCityName(User? user) @@ -443,16 +443,16 @@ string GetCityName(User? user) } ``` -Use these Rust types: +Используйте такие типы Rust: ```rust struct User { address: Option<Address> } struct Address { city: Option<String> } ``` -Write it as a **single expression** with no `if let` or `match`. +Запишите это как **одно выражение** без `if let` и `match`. <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust struct User { address: Option<Address> } @@ -477,7 +477,7 @@ fn main() { } ``` -**Key insight**: `and_then` is Rust's `?.` operator for `Option`. Each step returns `Option`, and the chain short-circuits on `None` — exactly like C#'s null-conditional operator `?.`, but explicit and type-safe. +**Ключевая мысль**: `and_then` — это аналог оператора `?.` из Rust для `Option`. Каждый шаг возвращает `Option`, а цепочка прерывается на `None` — ровно как оператор null-условного доступа `?.` в C#, но явно и с проверкой типов. </details> </details> diff --git a/csharp-book/src/ch06-enums-and-pattern-matching.md b/csharp-book/src/ch06-enums-and-pattern-matching.md index a71b20f..7d64997 100644 --- a/csharp-book/src/ch06-enums-and-pattern-matching.md +++ b/csharp-book/src/ch06-enums-and-pattern-matching.md @@ -1,13 +1,14 @@ -## Algebraic Data Types vs C# Unions +## Алгебраические типы данных против объединений C# -> **What you'll learn:** Rust's algebraic data types (enums with data) vs C#'s limited discriminated unions, -> `match` expressions with exhaustive checking, guard clauses, and nested pattern destructuring. +> **Что вы узнаете:** алгебраические типы данных Rust (перечисления с данными) в сравнении с ограниченными +> дискриминируемыми объединениями C#, выражения `match` с проверкой исчерпываемости, защитные условия (guards) +> и деструктуризацию вложенных паттернов. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### C# Discriminated Unions (Limited) +### Дискриминируемые объединения в C# (ограниченные) ```csharp -// C# - Limited union support with inheritance +// C# — ограниченная поддержка объединений через наследование public abstract class Result { public abstract T Match<T>(Func<Success, T> onSuccess, Func<Error, T> onError); @@ -31,7 +32,7 @@ public class Error : Result => onError(this); } -// C# 9+ Records with pattern matching (better) +// Records с сопоставлением с образцом в C# 9+ (лучше) public abstract record Shape; public record Circle(double Radius) : Shape; public record Rectangle(double Width, double Height) : Shape; @@ -40,13 +41,13 @@ public static double Area(Shape shape) => shape switch { Circle(var radius) => Math.PI * radius * radius, Rectangle(var width, var height) => width * height, - _ => throw new ArgumentException("Unknown shape") // [ERROR] Runtime error possible + _ => throw new ArgumentException("Unknown shape") // [ERROR] Возможна ошибка времени выполнения }; ``` -### Rust Algebraic Data Types (Enums) +### Алгебраические типы данных Rust (перечисления) ```rust -// Rust - True algebraic data types with exhaustive pattern matching +// Rust — настоящие алгебраические типы данных с исчерпывающим сопоставлением с образцом #[derive(Debug, Clone)] pub enum Result<T, E> { Ok(T), @@ -66,19 +67,19 @@ impl Shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Triangle { base, height } => 0.5 * base * height, - // [OK] Compiler error if any variant is missing! + // [OK] Ошибка компиляции, если какой-либо вариант пропущен! } } } -// Advanced: Enums can hold different types +// Продвинутый пример: перечисления могут хранить значения разных типов #[derive(Debug)] pub enum Value { Integer(i64), Float(f64), Text(String), Boolean(bool), - List(Vec<Value>), // Recursive types! + List(Vec<Value>), // Рекурсивные типы! } impl Value { @@ -96,13 +97,13 @@ impl Value { ```mermaid graph TD - subgraph "C# Discriminated Unions (Workarounds)" + subgraph "Дискриминируемые объединения C# (обходные пути)" CS_ABSTRACT["abstract class Result"] CS_SUCCESS["class Success : Result"] CS_ERROR["class Error : Result"] - CS_MATCH["Manual Match method<br/>or switch expressions"] - CS_RUNTIME["[ERROR] Runtime exceptions<br/>for missing cases"] - CS_HEAP["[ERROR] Heap allocation<br/>for class inheritance"] + CS_MATCH["Ручной метод Match<br/>или switch-выражения"] + CS_RUNTIME["[ERROR] Исключения во время выполнения<br/>для пропущенных случаев"] + CS_HEAP["[ERROR] Выделение в куче<br/>для наследования классов"] CS_ABSTRACT --> CS_SUCCESS CS_ABSTRACT --> CS_ERROR @@ -112,13 +113,13 @@ graph TD CS_ABSTRACT --> CS_HEAP end - subgraph "Rust Algebraic Data Types" + subgraph "Алгебраические типы данных Rust" RUST_ENUM["enum Shape { ... }"] RUST_VARIANTS["Circle { radius }<br/>Rectangle { width, height }<br/>Triangle { base, height }"] RUST_MATCH["match shape { ... }"] - RUST_EXHAUSTIVE["[OK] Exhaustive checking<br/>Compile-time guarantee"] - RUST_STACK["[OK] Stack allocation<br/>Efficient memory use"] - RUST_ZERO["[OK] Zero-cost abstraction"] + RUST_EXHAUSTIVE["[OK] Проверка исчерпываемости<br/>Гарантия на этапе компиляции"] + RUST_STACK["[OK] Размещение на стеке<br/>Эффективное использование памяти"] + RUST_ZERO["[OK] Абстракция с нулевой стоимостью"] RUST_ENUM --> RUST_VARIANTS RUST_VARIANTS --> RUST_MATCH @@ -136,13 +137,13 @@ graph TD *** -## Enums and Pattern Matching +## Перечисления и сопоставление с образцом -Rust enums are much more powerful than C# enums - they can hold data and are the foundation of type-safe programming. +Перечисления Rust намного мощнее перечислений C# — они могут хранить данные и составляют основу типобезопасного программирования. -### C# Enum Limitations +### Ограничения перечислений C# ```csharp -// C# enum - just named constants +// Перечисление C# — всего лишь именованные константы public enum Status { Pending, @@ -150,7 +151,7 @@ public enum Status Rejected } -// C# enum with backing values +// Перечисление C# с базовыми значениями public enum HttpStatusCode { OK = 200, @@ -158,7 +159,7 @@ public enum HttpStatusCode InternalServerError = 500 } -// Need separate classes for complex data +// Для сложных данных нужны отдельные классы public abstract class Result { public abstract bool IsSuccess { get; } @@ -187,9 +188,9 @@ public class Error : Result } ``` -### Rust Enum Power +### Мощь перечислений Rust ```rust -// Simple enum (like C# enum) +// Простое перечисление (как enum в C#) #[derive(Debug, PartialEq)] enum Status { Pending, @@ -197,23 +198,23 @@ enum Status { Rejected, } -// Enum with data (this is where Rust shines!) +// Перечисление с данными (здесь Rust раскрывается!) #[derive(Debug)] enum Result<T, E> { - Ok(T), // Success variant holding value of type T - Err(E), // Error variant holding error of type E + Ok(T), // Вариант успеха, хранящий значение типа T + Err(E), // Вариант ошибки, хранящий ошибку типа E } -// Complex enum with different data types +// Сложное перечисление с данными разных типов #[derive(Debug)] enum Message { - Quit, // No data - Move { x: i32, y: i32 }, // Struct-like variant - Write(String), // Tuple-like variant - ChangeColor(i32, i32, i32), // Multiple values + Quit, // Без данных + Move { x: i32, y: i32 }, // Вариант в виде структуры + Write(String), // Вариант в виде кортежа + ChangeColor(i32, i32, i32), // Несколько значений } -// Real-world example: HTTP Response +// Пример из практики: HTTP-ответ #[derive(Debug)] enum HttpResponse { Ok { body: String, headers: Vec<String> }, @@ -223,9 +224,9 @@ enum HttpResponse { } ``` -### Pattern Matching with Match +### Сопоставление с образцом через match ```csharp -// C# switch statement (limited) +// Оператор switch в C# (ограниченный) public string HandleStatus(Status status) { switch (status) @@ -237,34 +238,34 @@ public string HandleStatus(Status status) case Status.Rejected: return "Request rejected"; default: - return "Unknown status"; // Always need default + return "Unknown status"; // Всегда нужен default } } -// C# pattern matching (C# 8+) +// Сопоставление с образцом в C# (C# 8+) public string HandleResult(Result result) { return result switch { Success success => $"Success: {success.Value}", Error error => $"Error: {error.Message}", - _ => "Unknown result" // Still need catch-all + _ => "Unknown result" // Всё равно нужен перехватывающий случай }; } ``` ```rust -// Rust match - exhaustive and powerful +// match в Rust — исчерпывающий и мощный fn handle_status(status: Status) -> String { match status { Status::Pending => "Waiting for approval".to_string(), Status::Approved => "Request approved".to_string(), Status::Rejected => "Request rejected".to_string(), - // No default needed - compiler ensures exhaustiveness + // Default не нужен — компилятор гарантирует исчерпываемость } } -// Pattern matching with data extraction +// Сопоставление с образцом с извлечением данных fn handle_result<T, E>(result: Result<T, E>) -> String where T: std::fmt::Debug, @@ -273,11 +274,11 @@ where match result { Result::Ok(value) => format!("Success: {:?}", value), Result::Err(error) => format!("Error: {:?}", error), - // Exhaustive - no default needed + // Исчерпывающе — default не нужен } } -// Complex pattern matching +// Сложное сопоставление с образцом fn handle_message(msg: Message) -> String { match msg { Message::Quit => "Goodbye!".to_string(), @@ -287,7 +288,7 @@ fn handle_message(msg: Message) -> String { } } -// HTTP response handling +// Обработка HTTP-ответа fn handle_http_response(response: HttpResponse) -> String { match response { HttpResponse::Ok { body, headers } => { @@ -306,9 +307,9 @@ fn handle_http_response(response: HttpResponse) -> String { } ``` -### Guards and Advanced Patterns +### Защитные условия и продвинутые паттерны ```rust -// Pattern matching with guards +// Сопоставление с образцом с защитными условиями (guards) fn describe_number(x: i32) -> String { match x { n if n < 0 => "negative".to_string(), @@ -319,7 +320,7 @@ fn describe_number(x: i32) -> String { } } -// Matching ranges +// Сопоставление с диапазонами fn describe_age(age: u32) -> String { match age { 0..=12 => "child".to_string(), @@ -329,35 +330,35 @@ fn describe_age(age: u32) -> String { } } -// Destructuring structs and tuples +// Деструктуризация структур и кортежей ``` <details> -<summary><strong>🏋️ Exercise: Command Parser</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: парсер команд</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Model a CLI command system using Rust enums. Parse string input into a `Command` enum and execute each variant. Handle unknown commands with proper error handling. +**Задача**: смоделируйте систему CLI-команд с помощью перечислений Rust. Разберите строку ввода в перечисление `Command` и выполните каждый вариант. Обработайте неизвестные команды с правильной обработкой ошибок. ```rust -// Starter code — fill in the blanks +// Стартовый код — заполните пропуски #[derive(Debug)] enum Command { - // TODO: Add variants for Quit, Echo(String), Move { x: i32, y: i32 }, Count(u32) + // TODO: Добавьте варианты Quit, Echo(String), Move { x: i32, y: i32 }, Count(u32) } fn parse_command(input: &str) -> Result<Command, String> { let parts: Vec<&str> = input.splitn(2, ' ').collect(); - // TODO: match on parts[0] and parse arguments + // TODO: сопоставьте parts[0] и разберите аргументы todo!() } fn execute(cmd: &Command) -> String { - // TODO: match on each variant and return a description + // TODO: сопоставьте каждый вариант и верните описание todo!() } ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[derive(Debug)] @@ -402,10 +403,10 @@ fn execute(cmd: &Command) -> String { } ``` -**Key takeaways**: -- Each enum variant can hold different data — no need for class hierarchies -- `match` forces you to handle every variant, preventing forgotten cases -- `?` operator chains error propagation cleanly — no nested try-catch +**Ключевые выводы**: +- Каждый вариант перечисления может хранить разные данные — иерархии классов не нужны +- `match` заставляет обработать каждый вариант, что предотвращает забытые случаи +- Оператор `?` аккуратно связывает распространение ошибок — никаких вложенных try-catch </details> </details> From 11fa51810e582a403f51913c331c6fbbd4662b9b Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 10:27:50 +0400 Subject: [PATCH 16/18] Translate C# book chapters 7-12 to Russian (WIP) Translate prose, headings, tables, diagram labels, code comments and user-facing strings for ch07 through ch12 in csharp-book/src. Code identifiers, crate names, file names and links are unchanged. Anchors in ch00 and cross-chapter links are updated once the remaining chapters are translated. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- .../src/ch07-1-memory-safety-deep-dive.md | 175 +++++---- csharp-book/src/ch07-2-lifetimes-deep-dive.md | 190 +++++----- ...-smart-pointers-beyond-single-ownership.md | 223 ++++++------ .../src/ch07-ownership-and-borrowing.md | 344 +++++++++--------- ...h08-1-package-management-cargo-vs-nuget.md | 130 +++---- csharp-book/src/ch08-crates-and-modules.md | 186 +++++----- ...rate-level-error-types-and-result-alias.md | 120 +++--- csharp-book/src/ch09-error-handling.md | 110 +++--- csharp-book/src/ch10-1-generic-constraints.md | 104 +++--- .../src/ch10-2-inheritance-vs-composition.md | 58 +-- csharp-book/src/ch10-traits-and-generics.md | 252 ++++++------- csharp-book/src/ch11-from-and-into-traits.md | 68 ++-- csharp-book/src/ch12-1-macros-primer.md | 148 ++++---- .../src/ch12-closures-and-iterators.md | 236 ++++++------ 14 files changed, 1171 insertions(+), 1173 deletions(-) diff --git a/csharp-book/src/ch07-1-memory-safety-deep-dive.md b/csharp-book/src/ch07-1-memory-safety-deep-dive.md index 67c933e..2ef947c 100644 --- a/csharp-book/src/ch07-1-memory-safety-deep-dive.md +++ b/csharp-book/src/ch07-1-memory-safety-deep-dive.md @@ -1,80 +1,81 @@ -## References vs Pointers +## Ссылки против указателей -> **What you'll learn:** Rust references vs C# pointers and unsafe contexts, lifetime basics, -> and why compile-time safety proofs are stronger than C#'s runtime checks (bounds checking, null guards). +> **Что вы узнаете:** ссылки Rust против указателей и небезопасных контекстов C#, основы времён жизни, +> а также почему доказательства безопасности на этапе компиляции надёжнее проверок времени выполнения в C# +> (проверка границ, защита от null). > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### C# Pointers (Unsafe Context) +### Указатели в C# (небезопасный контекст) ```csharp -// C# unsafe pointers (rarely used) +// Небезопасные указатели в C# (используются редко) unsafe void UnsafeExample() { int value = 42; - int* ptr = &value; // Pointer to value - *ptr = 100; // Dereference and modify + int* ptr = &value; // Указатель на value + *ptr = 100; // Разыменование и изменение Console.WriteLine(value); // 100 } ``` -### Rust References (Safe by Default) +### Ссылки в Rust (безопасны по умолчанию) ```rust -// Rust references (always safe) +// Ссылки в Rust (всегда безопасны) fn safe_example() { let mut value = 42; - let ptr = &mut value; // Mutable reference - *ptr = 100; // Dereference and modify + let ptr = &mut value; // Изменяемая ссылка + *ptr = 100; // Разыменование и изменение println!("{}", value); // 100 } -// No "unsafe" keyword needed - borrow checker ensures safety +// Ключевое слово "unsafe" не нужно — проверщик заимствований гарантирует безопасность ``` -### Lifetime Basics for C# Developers +### Основы времён жизни для разработчиков C# ```csharp -// C# - Can return references that might become invalid +// C# — можно вернуть ссылки, которые могут стать недействительными public class LifetimeIssues { public string GetFirstWord(string input) { - return input.Split(' ')[0]; // Returns new string (safe) + return input.Split(' ')[0]; // Возвращает новую строку (безопасно) } public unsafe char* GetFirstChar(string input) { - // This would be dangerous - returning pointer to managed memory + // Это было бы опасно — возврат указателя на управляемую память fixed (char* ptr = input) - return ptr; // ❌ Bad: ptr becomes invalid after method ends + return ptr; // ❌ Плохо: ptr становится недействительным после выхода из метода } } ``` ```rust -// Rust - Lifetime checking prevents dangling references +// Rust — проверка времён жизни предотвращает висячие ссылки fn get_first_word(input: &str) -> &str { input.split_whitespace().next().unwrap_or("") - // ✅ Safe: returned reference has same lifetime as input + // ✅ Безопасно: возвращаемая ссылка имеет то же время жизни, что и input } fn invalid_reference() -> &str { let temp = String::from("hello"); - &temp // ❌ Compile error: temp doesn't live long enough - // temp would be dropped at end of function + &temp // ❌ Ошибка компиляции: temp живёт недостаточно долго + // temp будет уничтожена в конце функции } fn valid_reference() -> String { let temp = String::from("hello"); - temp // ✅ Works: ownership is transferred to caller + temp // ✅ Работает: владение передаётся вызывающему коду } ``` *** -## Memory Safety: Runtime Checks vs Compile-Time Proofs +## Безопасность памяти: проверки во время выполнения против доказательств на этапе компиляции -### C# - Runtime Safety Net +### C# — страховка времени выполнения ```csharp -// C# relies on runtime checks and GC +// C# полагается на проверки во время выполнения и GC public class Buffer { private byte[] data; @@ -86,39 +87,39 @@ public class Buffer public void ProcessData(int index) { - // Runtime bounds checking + // Проверка границ во время выполнения if (index >= data.Length) throw new IndexOutOfRangeException(); - data[index] = 42; // Safe, but checked at runtime + data[index] = 42; // Безопасно, но проверяется во время выполнения } - // Memory leaks still possible with events/static references + // Утечки памяти всё равно возможны через события и статические ссылки public static event Action<string> GlobalEvent; public void Subscribe() { - GlobalEvent += HandleEvent; // Can create memory leaks - // Forgot to unsubscribe - object won't be collected + GlobalEvent += HandleEvent; // Может создать утечку памяти + // Забыли отписаться — объект не будет собран сборщиком мусора } private void HandleEvent(string message) { /* ... */ } - // Null reference exceptions are still possible + // Исключения NullReferenceException по-прежнему возможны public void ProcessUser(User user) { - Console.WriteLine(user.Name.ToUpper()); // NullReferenceException if user.Name is null + Console.WriteLine(user.Name.ToUpper()); // NullReferenceException, если user.Name равен null } - // Array access can fail at runtime + // Доступ к массиву может завершиться ошибкой во время выполнения public int GetValue(int[] array, int index) { - return array[index]; // IndexOutOfRangeException possible + return array[index]; // Возможно IndexOutOfRangeException } } ``` -### Rust - Compile-Time Guarantees +### Rust — гарантии на этапе компиляции ```rust struct Buffer { data: Vec<u8>, @@ -132,72 +133,72 @@ impl Buffer { } fn process_data(&mut self, index: usize) { - // Bounds checking can be optimized away by compiler when proven safe + // Проверку границ компилятор может убрать, если безопасность доказана if let Some(item) = self.data.get_mut(index) { - *item = 42; // Safe access, proven at compile time + *item = 42; // Безопасный доступ, доказанный на этапе компиляции } - // Or use indexing with explicit bounds check: - // self.data[index] = 42; // Panics in debug, but memory-safe + // Или используйте индексирование с явной проверкой границ: + // self.data[index] = 42; // Паника в debug-сборке, но память в безопасности } - // Memory leaks impossible - ownership system prevents them + // Утечки памяти невозможны — система владения их предотвращает fn process_with_closure<F>(&mut self, processor: F) where F: FnOnce(&mut Vec<u8>) { processor(&mut self.data); - // When processor goes out of scope, it's automatically cleaned up - // No way to create dangling references or memory leaks + // Когда processor выходит из области видимости, он автоматически очищается + // Создать висячие ссылки или утечку памяти нельзя } - // Null pointer dereferences impossible - no null pointers! + // Разыменование null-указателей невозможно — null-указателей здесь нет! fn process_user(&self, user: &User) { - println!("{}", user.name.to_uppercase()); // user.name cannot be null + println!("{}", user.name.to_uppercase()); // user.name не может быть null } - // Array access is bounds-checked or explicitly unsafe + // Доступ к массиву либо проверяется на границы, либо явно небезопасен fn get_value(array: &[i32], index: usize) -> Option<i32> { - array.get(index).copied() // Returns None if out of bounds + array.get(index).copied() // Возвращает None, если индекс вне границ } - // Or explicitly unsafe if you know what you're doing: + // Или явно небезопасно, если вы точно знаете, что делаете: /// # Safety - /// `index` must be less than `array.len()`. + /// `index` должен быть меньше `array.len()`. unsafe fn get_value_unchecked(array: &[i32], index: usize) -> i32 { - *array.get_unchecked(index) // Fast but must prove bounds manually + *array.get_unchecked(index) // Быстро, но границы нужно доказать вручную } } struct User { - name: String, // String cannot be null in Rust + name: String, // String в Rust не может быть null } -// Ownership prevents use-after-free +// Владение предотвращает использование после освобождения fn ownership_example() { let data = vec![1, 2, 3, 4, 5]; - let reference = &data[0]; // Borrow data + let reference = &data[0]; // Заимствуем data - // drop(data); // ERROR: cannot drop while borrowed - println!("{}", reference); // This is guaranteed safe + // drop(data); // ОШИБКА: нельзя уничтожить, пока есть заимствование + println!("{}", reference); // Это гарантированно безопасно } -// Borrowing prevents data races +// Заимствование предотвращает гонки данных fn borrowing_example(data: &mut Vec<i32>) { - let first = &data[0]; // Immutable borrow - // data.push(6); // ERROR: cannot mutably borrow while immutably borrowed - println!("{}", first); // Guaranteed no data race + let first = &data[0]; // Неизменяемое заимствование + // data.push(6); // ОШИБКА: нельзя заимствовать изменяемо, пока есть неизменяемое заимствование + println!("{}", first); // Гарантированно нет гонки данных } ``` ```mermaid graph TD - subgraph "C# Runtime Safety" - CS_RUNTIME["Runtime Checks"] - CS_GC["Garbage Collector"] - CS_EXCEPTIONS["Exception Handling"] - CS_BOUNDS["Runtime bounds checking"] - CS_NULL["Null reference exceptions"] - CS_LEAKS["Memory leaks possible"] - CS_OVERHEAD["Performance overhead"] + subgraph "Безопасность во время выполнения в C#" + CS_RUNTIME["Проверки во время выполнения"] + CS_GC["Сборщик мусора"] + CS_EXCEPTIONS["Обработка исключений"] + CS_BOUNDS["Проверка границ во время выполнения"] + CS_NULL["Исключения null-ссылок"] + CS_LEAKS["Возможны утечки памяти"] + CS_OVERHEAD["Накладные расходы производительности"] CS_RUNTIME --> CS_BOUNDS CS_RUNTIME --> CS_NULL @@ -205,14 +206,14 @@ graph TD CS_EXCEPTIONS --> CS_OVERHEAD end - subgraph "Rust Compile-Time Safety" - RUST_OWNERSHIP["Ownership System"] - RUST_BORROWING["Borrow Checker"] - RUST_TYPES["Type System"] - RUST_ZERO_COST["Zero-cost abstractions"] - RUST_NO_NULL["No null pointers"] - RUST_NO_LEAKS["No memory leaks"] - RUST_FAST["Optimal performance"] + subgraph "Безопасность на этапе компиляции в Rust" + RUST_OWNERSHIP["Система владения"] + RUST_BORROWING["Проверщик заимствований"] + RUST_TYPES["Система типов"] + RUST_ZERO_COST["Абстракции с нулевой стоимостью"] + RUST_NO_NULL["Нет null-указателей"] + RUST_NO_LEAKS["Нет утечек памяти"] + RUST_FAST["Оптимальная производительность"] RUST_OWNERSHIP --> RUST_NO_LEAKS RUST_BORROWING --> RUST_NO_NULL @@ -230,12 +231,12 @@ graph TD --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Spot the Safety Bug</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: найдите ошибку безопасности</strong> (нажмите, чтобы раскрыть)</summary> -This C# code has a subtle safety bug. Identify it, then write the Rust equivalent and explain why the Rust version **won't compile**: +В этом C#-коде есть тонкая ошибка безопасности. Найдите её, затем напишите эквивалент на Rust и объясните, почему версия на Rust **не скомпилируется**: ```csharp public List<int> GetEvenNumbers(List<int> numbers) @@ -246,7 +247,7 @@ public List<int> GetEvenNumbers(List<int> numbers) if (n % 2 == 0) { result.Add(n); - numbers.Remove(n); // Bug: modifying collection while iterating + numbers.Remove(n); // Ошибка: изменение коллекции во время перебора } } return result; @@ -254,9 +255,9 @@ public List<int> GetEvenNumbers(List<int> numbers) ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> -**C# bug**: Modifying `numbers` while iterating throws `InvalidOperationException` at *runtime*. Easy to miss in code review. +**Ошибка в C#**: изменение `numbers` во время перебора приводит к `InvalidOperationException` во *время выполнения*. Её легко пропустить на код-ревью. ```rust fn get_even_numbers(numbers: &mut Vec<i32>) -> Vec<i32> { @@ -265,17 +266,17 @@ fn get_even_numbers(numbers: &mut Vec<i32>) -> Vec<i32> { if n % 2 == 0 { result.push(n); // numbers.retain(|&x| x != n); - // ❌ ERROR: cannot borrow `*numbers` as mutable because - // it is also borrowed as immutable (by the iterator) + // ❌ ОШИБКА: нельзя заимствовать `*numbers` как изменяемое, потому что + // оно уже заимствовано как неизменяемое (итератором) } } result } -// Idiomatic Rust: use partition or retain +// Идиоматичный Rust: используйте retain или partition fn get_even_numbers_idiomatic(numbers: &mut Vec<i32>) -> Vec<i32> { let evens: Vec<i32> = numbers.iter().copied().filter(|n| n % 2 == 0).collect(); - numbers.retain(|n| n % 2 != 0); // remove evens after iteration + numbers.retain(|n| n % 2 != 0); // удаляем чётные после перебора evens } @@ -287,12 +288,10 @@ fn main() { } ``` -**Key insight**: Rust's borrow checker prevents the entire *category* of "mutate while iterating" bugs at compile time. C# catches this at runtime; many languages don't catch it at all. +**Ключевая мысль**: проверщик заимствований Rust предотвращает целую *категорию* ошибок «изменение во время перебора» на этапе компиляции. C# ловит это во время выполнения; многие языки не ловят вообще. </details> </details> *** - - diff --git a/csharp-book/src/ch07-2-lifetimes-deep-dive.md b/csharp-book/src/ch07-2-lifetimes-deep-dive.md index d4b52d0..8a81663 100644 --- a/csharp-book/src/ch07-2-lifetimes-deep-dive.md +++ b/csharp-book/src/ch07-2-lifetimes-deep-dive.md @@ -1,25 +1,25 @@ -## Lifetimes: Telling the Compiler How Long References Live +## Времена жизни: как сообщить компилятору, сколько живут ссылки -> **What you'll learn:** Why lifetimes exist (no GC means the compiler needs proof), lifetime annotation syntax, -> elision rules, struct lifetimes, the `'static` lifetime, and common borrow checker errors with fixes. +> **Что вы узнаете:** зачем нужны времена жизни (без GC компилятору нужно доказательство), синтаксис аннотаций времён жизни, +> правила элизии, времена жизни в структурах, время жизни `'static`, а также типичные ошибки проверщика заимствований и способы их исправить. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -C# developers never think about reference lifetimes — the garbage collector handles reachability. In Rust, the compiler needs *proof* that every reference is valid for as long as it's used. Lifetimes are that proof. +Разработчики C# никогда не думают о временах жизни ссылок — достижимостью занимается сборщик мусора. В Rust компилятору нужно *доказательство* того, что каждая ссылка действительна столько, сколько она используется. Времена жизни и есть это доказательство. -### Why Lifetimes Exist +### Зачем нужны времена жизни ```rust -// This won't compile — the compiler can't prove the returned reference is valid +// Этот код не скомпилируется — компилятор не может доказать, что возвращаемая ссылка действительна fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b } } -// ERROR: missing lifetime specifier — the compiler doesn't know -// whether the return value borrows from `a` or `b` +// ОШИБКА: пропущен specifier времени жизни — компилятор не знает, +// заимствует ли возвращаемое значение из `a` или из `b` ``` -### Lifetime Annotations +### Аннотации времён жизни ```rust -// Lifetime 'a says: "the return value lives at least as long as BOTH inputs" +// Время жизни 'a означает: «возвращаемое значение живёт не меньше, чем ОБА входных параметра» fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } } @@ -30,45 +30,45 @@ fn main() { { let string2 = String::from("xyz"); result = longest(&string1, &string2); - println!("Longest: {result}"); // ✅ both references still valid here + println!("Longest: {result}"); // ✅ обе ссылки здесь ещё действительны } - // println!("{result}"); // ❌ ERROR: string2 doesn't live long enough + // println!("{result}"); // ❌ ОШИБКА: string2 живёт недостаточно долго } ``` -### C# Comparison +### Сравнение с C# ```csharp -// C# — the GC keeps objects alive as long as any reference exists +// C# — GC держит объекты живыми, пока существует хотя бы одна ссылка string Longest(string a, string b) => a.Length > b.Length ? a : b; -// No lifetime issues — GC tracks reachability automatically -// But: GC pauses, unpredictable memory usage, no compile-time proof +// Проблем с временами жизни нет — GC сам отслеживает достижимость +// Но: паузы GC, непредсказуемое потребление памяти, нет доказательств на этапе компиляции ``` -### Lifetime Elision Rules +### Правила элизии времён жизни -Most of the time you **don't need to write lifetime annotations**. The compiler applies three rules automatically: +В большинстве случаев аннотации времён жизни **писать не нужно**. Компилятор применяет три правила автоматически: -| Rule | Description | Example | +| Правило | Описание | Пример | |------|-------------|---------| -| **Rule 1** | Each reference parameter gets its own lifetime | `fn foo(x: &str, y: &str)` → `fn foo<'a, 'b>(x: &'a str, y: &'b str)` | -| **Rule 2** | If there's exactly one input lifetime, it's assigned to all output lifetimes | `fn first(s: &str) -> &str` → `fn first<'a>(s: &'a str) -> &'a str` | -| **Rule 3** | If one input is `&self` or `&mut self`, that lifetime is assigned to all outputs | `fn name(&self) -> &str` → works because of &self | +| **Правило 1** | Каждый параметр-ссылка получает собственное время жизни | `fn foo(x: &str, y: &str)` → `fn foo<'a, 'b>(x: &'a str, y: &'b str)` | +| **Правило 2** | Если есть ровно одно входное время жизни, оно присваивается всем выходным | `fn first(s: &str) -> &str` → `fn first<'a>(s: &'a str) -> &'a str` | +| **Правило 3** | Если один из входов — `&self` или `&mut self`, его время жизни присваивается всем выходным | `fn name(&self) -> &str` → работает благодаря &self | ```rust -// These are equivalent — the compiler adds lifetimes automatically: -fn first_word(s: &str) -> &str { /* ... */ } // elided -fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // explicit +// Эти записи эквивалентны — компилятор добавляет времена жизни автоматически: +fn first_word(s: &str) -> &str { /* ... */ } // с элизией +fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ } // явно -// But this REQUIRES explicit annotation — two inputs, which one does output borrow? +// А вот здесь нужна явная аннотация — два входа, из какого из них заимствует выход? fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ } ``` -### Struct Lifetimes +### Времена жизни в структурах ```rust -// A struct that borrows data (instead of owning it) +// Структура, которая заимствует данные (а не владеет ими) struct Excerpt<'a> { - text: &'a str, // borrows from some String that must outlive this struct + text: &'a str, // заимствует из String, которая должна пережить эту структуру } impl<'a> Excerpt<'a> { @@ -83,79 +83,79 @@ impl<'a> Excerpt<'a> { fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); - let excerpt = Excerpt::new(&novel); // excerpt borrows from novel + let excerpt = Excerpt::new(&novel); // excerpt заимствует из novel println!("First sentence: {}", excerpt.first_sentence()); - // novel must stay alive as long as excerpt exists + // novel должен жить, пока существует excerpt } ``` ```csharp -// C# equivalent — no lifetime concerns, but no compile-time guarantee either +// Аналог в C# — проблем с временами жизни нет, но и гарантий на этапе компиляции нет class Excerpt { public string Text { get; } public Excerpt(string text) => Text = text; public string FirstSentence() => Text.Split('.')[0]; } -// What if the string is mutated elsewhere? Runtime surprise. +// Что если строку изменят где-то в другом месте? Сюрприз во время выполнения. ``` -### The `'static` Lifetime +### Время жизни `'static` ```rust -// 'static means "lives for the entire program duration" -let s: &'static str = "I'm a string literal"; // stored in binary, always valid +// 'static означает «живёт всё время работы программы» +let s: &'static str = "I'm a string literal"; // хранится в бинарнике, всегда действителен -// Common places you see 'static: -// 1. String literals -// 2. Global constants -// 3. Thread::spawn requires 'static (thread might outlive the caller) +// Типичные места, где встречается 'static: +// 1. Строковые литералы +// 2. Глобальные константы +// 3. Thread::spawn требует 'static (поток может пережить вызывающий код) std::thread::spawn(move || { - // Closures sent to threads must own their data or use 'static references + // Замыкания, передаваемые в потоки, должны владеть своими данными или использовать ссылки 'static println!("{s}"); // OK: &'static str }); -// 'static does NOT mean "immortal" — it means "CAN live forever if needed" +// 'static НЕ означает «бессмертный» — это значит «МОЖЕТ жить вечно, если потребуется» let owned = String::from("hello"); -// owned is NOT 'static, but it can be moved into a thread (ownership transfer) +// owned не имеет времени жизни 'static, но его можно переместить в поток (передача владения) ``` -### Common Borrow Checker Errors and Fixes +### Типичные ошибки проверщика заимствований и их исправление -| Error | Cause | Fix | -|-------|-------|-----| -| `missing lifetime specifier` | Multiple input references, ambiguous output | Add `<'a>` annotation tying output to correct input | -| `does not live long enough` | Reference outlives the data it points to | Extend the data's scope, or return owned data instead | -| `cannot borrow as mutable` | Immutable borrow still active | Use the immutable reference before mutating, or restructure | -| `cannot move out of borrowed content` | Trying to take ownership of borrowed data | Use `.clone()`, or restructure to avoid the move | -| `lifetime may not live long enough` | Struct borrow outlives source | Ensure the source data's scope encompasses the struct's usage | +| Ошибка | Причина | Исправление | +|-------|-------|-------| +| `missing lifetime specifier` | Несколько входных ссылок, неоднозначный выход | Добавьте `<'a>`, связав выход с нужным входом | +| `does not live long enough` | Ссылка переживает данные, на которые указывает | Расширьте область видимости данных или верните владеемые данные | +| `cannot borrow as mutable` | Неизменяемое заимствование ещё активно | Используйте неизменяемую ссылку до изменения или перестройте код | +| `cannot move out of borrowed content` | Попытка забрать владение заимствованными данными | Используйте `.clone()` или перестройте код, чтобы избежать перемещения | +| `lifetime may not live long enough` | Заимствование структуры переживает источник | Убедитесь, что область видимости исходных данных охватывает использование структуры | -### Visualizing Lifetime Scopes +### Визуализация областей видимости времён жизни ```mermaid graph TD - subgraph "Scope Visualization" + subgraph "Визуализация областей видимости" direction TB A["fn main()"] --> B["let s1 = String::from("hello")"] - B --> C["{ // inner scope"] + B --> C["{ // внутренняя область"] C --> D["let s2 = String::from("world")"] D --> E["let r = longest(&s1, &s2)"] - E --> F["println!("{r}") ✅ both alive"] - F --> G["} // s2 dropped here"] - G --> H["println!("{r}") ❌ s2 gone!"] + E --> F["println!("{r}") ✅ обе живы"] + F --> G["} // s2 уничтожается здесь"] + G --> H["println!("{r}") ❌ s2 уже нет!"] end style F fill:#c8e6c9,color:#000 style H fill:#ffcdd2,color:#000 ``` -### Multiple Lifetime Parameters +### Несколько параметров времени жизни -Sometimes references come from different sources with different lifetimes: +Иногда ссылки приходят из разных источников с разными временами жизни: ```rust -// Two independent lifetimes: the return borrows only from 'a, not 'b +// Два независимых времени жизни: результат заимствует только из 'a, а не из 'b fn first_with_context<'a, 'b>(data: &'a str, _context: &'b str) -> &'a str { - // Return borrows from 'data' only — 'context' can have a shorter lifetime + // Результат заимствуется только из 'data' — у 'context' может быть более короткое время жизни data.split(',').next().unwrap_or(data) } @@ -163,76 +163,76 @@ fn main() { let data = String::from("alice,bob,charlie"); let result; { - let context = String::from("user lookup"); // shorter lifetime + let context = String::from("user lookup"); // более короткое время жизни result = first_with_context(&data, &context); - } // context dropped — but result borrows from data, not context ✅ + } // context уничтожается — но result заимствует из data, а не из context ✅ println!("{result}"); } ``` ```csharp -// C# — no lifetime tracking means you can't express "borrows from A but not B" +// C# — без отслеживания времён жизни нельзя выразить «заимствует из A, но не из B» string FirstWithContext(string data, string context) => data.Split(',')[0]; -// Fine for GC'd languages, but Rust can prove safety without a GC +// Для языков с GC это нормально, но Rust может доказать безопасность без GC ``` -### Real-World Lifetime Patterns +### Типичные паттерны из практики -**Pattern 1: Iterator returning references** +**Паттерн 1: итератор, возвращающий ссылки** ```rust -// A parser that yields borrowed slices from the input +// Парсер, который отдаёт заимствованные срезы из входной строки struct CsvRow<'a> { fields: Vec<&'a str>, } fn parse_csv_line(line: &str) -> CsvRow<'_> { - // '_ tells the compiler "infer the lifetime from the input" + // '_ говорит компилятору «выведи время жизни из входного параметра» CsvRow { fields: line.split(',').collect(), } } ``` -**Pattern 2: "Return owned when in doubt"** +**Паттерн 2: «если сомневаешься — возвращай владеемые данные»** ```rust -// When lifetimes get complex, returning owned data is the pragmatic fix +// Когда времена жизни становятся сложными, практичное решение — вернуть владеемые данные fn format_greeting(first: &str, last: &str) -> String { - // Returns owned String — no lifetime annotation needed + // Возвращаем владеемую String — аннотация времени жизни не нужна format!("Hello, {first} {last}!") } -// Only borrow when: -// 1. Performance matters (avoiding allocation) -// 2. The relationship between input and output lifetime is clear +// Заимствуйте только тогда, когда: +// 1. Важна производительность (избегаем выделения памяти) +// 2. Связь между временем жизни входа и выхода очевидна ``` -**Pattern 3: Lifetime bounds on generics** +**Паттерн 3: ограничения времён жизни для обобщений** ```rust -// "T must live at least as long as 'a" +// «T должен жить не меньше, чем 'a» fn store_reference<'a, T: 'a>(cache: &mut Vec<&'a T>, item: &'a T) { cache.push(item); } -// Common in trait objects: Box<dyn Display + 'a> +// Часто встречается в трейт-объектах: Box<dyn Display + 'a> fn make_printer<'a>(text: &'a str) -> Box<dyn std::fmt::Display + 'a> { Box::new(text) } ``` -### When to Reach for `'static` +### Когда использовать `'static` -| Scenario | Use `'static`? | Alternative | +| Сценарий | Использовать `'static`? | Альтернатива | |----------|:-----------:|-------------| -| String literals | ✅ Yes — they're always `'static` | — | -| `thread::spawn` closure | Often — thread outlives caller | Use `thread::scope` for borrowed data | -| Global config | ✅ `lazy_static!` or `OnceLock` | Pass references through params | -| Trait objects stored long-term | Often — `Box<dyn Trait + 'static>` | Parameterize the container with `'a` | -| Temporary borrowing | ❌ Never — over-constraining | Use the actual lifetime | +| Строковые литералы | ✅ Да — они всегда `'static` | — | +| Замыкание для `thread::spawn` | Часто — поток переживает вызывающий код | Используйте `thread::scope` для заимствованных данных | +| Глобальная конфигурация | ✅ `lazy_static!` или `OnceLock` | Передавайте ссылки через параметры | +| Трейт-объекты, которые хранятся долго | Часто — `Box<dyn Trait + 'static>` | Параметризуйте контейнер через `'a` | +| Временные заимствования | ❌ Никогда — излишнее ограничение | Используйте реальное время жизни | <details> -<summary><strong>🏋️ Exercise: Lifetime Annotations</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: аннотации времён жизни</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Add the correct lifetime annotations to make this compile: +**Задача**: добавьте правильные аннотации времён жизни, чтобы код скомпилировался: ```rust struct Config { @@ -240,12 +240,12 @@ struct Config { api_key: String, } -// TODO: Add lifetime annotations +// TODO: Добавьте аннотации времён жизни fn get_connection_info(config: &Config) -> (&str, &str) { (&config.db_url, &config.api_key) } -// TODO: This struct borrows from Config — add lifetime parameter +// TODO: Эта структура заимствует из Config — добавьте параметр времени жизни struct ConnectionInfo { db_url: &str, api_key: &str, @@ -253,7 +253,7 @@ struct ConnectionInfo { ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust struct Config { @@ -261,13 +261,13 @@ struct Config { api_key: String, } -// Rule 3 doesn't apply (no &self), Rule 2 applies (one input → output) -// So the compiler handles this automatically — no annotation needed! +// Правило 3 не применяется (нет &self), применяется правило 2 (один вход → выход) +// Поэтому компилятор справляется сам — аннотация не нужна! fn get_connection_info(config: &Config) -> (&str, &str) { (&config.db_url, &config.api_key) } -// Struct lifetime annotation needed: +// Аннотация времени жизни для структуры нужна: struct ConnectionInfo<'a> { db_url: &'a str, api_key: &'a str, @@ -281,7 +281,7 @@ fn make_info<'a>(config: &'a Config) -> ConnectionInfo<'a> { } ``` -**Key takeaway**: Lifetime elision often saves you from writing annotations on functions, but structs that borrow data always need explicit `<'a>`. +**Ключевая мысль**: элизия времён жизни часто избавляет от необходимости писать аннотации у функций, но структуры, которые заимствуют данные, всегда требуют явного `<'a>`. </details> </details> diff --git a/csharp-book/src/ch07-3-smart-pointers-beyond-single-ownership.md b/csharp-book/src/ch07-3-smart-pointers-beyond-single-ownership.md index 776498c..6b2bb26 100644 --- a/csharp-book/src/ch07-3-smart-pointers-beyond-single-ownership.md +++ b/csharp-book/src/ch07-3-smart-pointers-beyond-single-ownership.md @@ -1,26 +1,26 @@ -## Smart Pointers: When Single Ownership Isn't Enough +## Умные указатели: когда единоличного владения недостаточно -> **What you'll learn:** `Box<T>`, `Rc<T>`, `Arc<T>`, `Cell<T>`, `RefCell<T>`, and `Cow<'a, T>` — -> when to use each, how they compare to C#'s GC-managed references, `Drop` as Rust's `IDisposable`, -> `Deref` coercion, and a decision tree for choosing the right smart pointer. +> **Что вы узнаете:** `Box<T>`, `Rc<T>`, `Arc<T>`, `Cell<T>`, `RefCell<T>` и `Cow<'a, T>` — +> когда что использовать, как они соотносятся со ссылками, которыми управляет GC в C#, `Drop` как аналог `IDisposable` в Rust, +> приведение через `Deref`, а также дерево решений для выбора подходящего умного указателя. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -In C#, every object is essentially reference-counted by the GC. In Rust, single ownership is the default — but sometimes you need shared ownership, heap allocation, or interior mutability. That's where smart pointers come in. +В C# каждый объект по сути подсчитывается сборщиком мусора. В Rust по умолчанию действует единоличное владение, но иногда нужны разделяемое владение, выделение в куче или внутренняя изменяемость. Для этого и существуют умные указатели. -### Box<T> — Simple Heap Allocation +### Box<T> — простое выделение в куче ```rust -// Stack allocation (default in Rust) -let x = 42; // on the stack +// Выделение на стеке (по умолчанию в Rust) +let x = 42; // на стеке -// Heap allocation with Box -let y = Box::new(42); // on the heap, like C# `new int(42)` (boxed) -println!("{}", y); // auto-derefs: prints 42 +// Выделение в куче через Box +let y = Box::new(42); // в куче, как `new int(42)` в C# (упаковка) +println!("{}", y); // автоматическое разыменование: выводит 42 -// Common use: recursive types (can't know size at compile time) +// Типичное применение: рекурсивные типы (размер неизвестен на этапе компиляции) #[derive(Debug)] enum List { - Cons(i32, Box<List>), // Box gives a known pointer size + Cons(i32, Box<List>), // Box даёт указатель известного размера Nil, } @@ -28,32 +28,32 @@ let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil)))); ``` ```csharp -// C# — everything on the heap already (reference types) -// Box<T> is only needed in Rust because stack is the default -var list = new LinkedListNode<int>(1); // always heap-allocated +// C# — всё уже в куче (ссылочные типы) +// Box<T> нужен в Rust только потому, что стек — это умолчание +var list = new LinkedListNode<int>(1); // всегда выделяется в куче ``` -### Rc<T> — Shared Ownership (Single Thread) +### Rc<T> — разделяемое владение (один поток) ```rust use std::rc::Rc; -// Multiple owners of the same data — like multiple C# references +// Несколько владельцев одних и тех же данных — как несколько ссылок в C# let shared = Rc::new(vec![1, 2, 3]); -let clone1 = Rc::clone(&shared); // reference count: 2 -let clone2 = Rc::clone(&shared); // reference count: 3 +let clone1 = Rc::clone(&shared); // счётчик ссылок: 2 +let clone2 = Rc::clone(&shared); // счётчик ссылок: 3 println!("Count: {}", Rc::strong_count(&shared)); // 3 -// Data is dropped when last Rc goes out of scope +// Данные уничтожаются, когда выходит из области видимости последний Rc -// Common use: shared configuration, graph nodes, tree structures +// Типичное применение: общая конфигурация, узлы графа, древовидные структуры ``` -### Arc<T> — Shared Ownership (Thread-Safe) +### Arc<T> — разделяемое владение (потокобезопасное) ```rust use std::sync::Arc; use std::thread; -// Arc = Atomic Reference Counting — safe to share across threads +// Arc = Atomic Reference Counting — безопасно разделяется между потоками let data = Arc::new(vec![1, 2, 3]); let handles: Vec<_> = (0..3).map(|i| { @@ -67,17 +67,17 @@ for h in handles { h.join().unwrap(); } ``` ```csharp -// C# — all references are thread-safe by default (GC handles it) +// C# — все ссылки потокобезопасны по умолчанию (этим занимается GC) var data = new List<int> { 1, 2, 3 }; -// Can share freely across threads (but mutation is still unsafe!) +// Можно свободно разделять между потоками (но мутация по-прежнему небезопасна!) ``` -### Cell<T> and RefCell<T> — Interior Mutability +### Cell<T> и RefCell<T> — внутренняя изменяемость ```rust use std::cell::RefCell; -// Sometimes you need to mutate data behind a shared reference. -// RefCell moves borrow checking from compile time to runtime. +// Иногда нужно изменять данные за разделяемой ссылкой. +// RefCell переносит проверку заимствований из времени компиляции во время выполнения. struct Logger { entries: RefCell<Vec<String>>, } @@ -87,7 +87,7 @@ impl Logger { Logger { entries: RefCell::new(Vec::new()) } } - fn log(&self, msg: &str) { // &self, not &mut self! + fn log(&self, msg: &str) { // &self, а не &mut self! self.entries.borrow_mut().push(msg.to_string()); } @@ -97,60 +97,60 @@ impl Logger { } } } -// ⚠️ RefCell panics at runtime if borrow rules are violated -// Use sparingly — prefer compile-time checking when possible +// ⚠️ RefCell вызывает панику во время выполнения, если нарушены правила заимствования +// Используйте осторожно — по возможности предпочитайте проверку на этапе компиляции ``` -### Cow<'a, str> — Clone on Write +### Cow<'a, str> — копирование при записи (Clone on Write) ```rust use std::borrow::Cow; -// Sometimes you have a &str that MIGHT need to become a String +// Иногда есть &str, который МОЖЕТ понадобиться превратить в String fn normalize(input: &str) -> Cow<'_, str> { if input.contains('\t') { - // Only allocate when we need to modify + // Выделяем память, только когда нужно изменить данные Cow::Owned(input.replace('\t', " ")) } else { - // Borrow the original — zero allocation + // Заимствуем исходные данные — без выделения памяти Cow::Borrowed(input) } } -let clean = normalize("hello"); // Cow::Borrowed — no allocation -let dirty = normalize("hello\tworld"); // Cow::Owned — allocated -// Both can be used as &str via Deref +let clean = normalize("hello"); // Cow::Borrowed — без выделения памяти +let dirty = normalize("hello\tworld"); // Cow::Owned — выделено +// Оба можно использовать как &str через Deref println!("{clean} / {dirty}"); ``` -### Drop: Rust's `IDisposable` +### Drop: аналог `IDisposable` в Rust -In C#, `IDisposable` + `using` handles resource cleanup. Rust's equivalent is the `Drop` trait — but it's **automatic**, not opt-in: +В C# очисткой ресурсов занимаются `IDisposable` и `using`. Аналог в Rust — трейт `Drop`, но он **автоматический**, а не включаемый по желанию: ```csharp -// C# — must remember to use 'using' or call Dispose() +// C# — нужно не забыть использовать 'using' или вызвать Dispose() using var file = File.OpenRead("data.bin"); -// Dispose() called at end of scope +// Dispose() вызывается в конце области видимости -// Forgetting 'using' is a resource leak! +// Забыть 'using' — значит получить утечку ресурса! var file2 = File.OpenRead("data.bin"); -// GC will *eventually* finalize, but timing is unpredictable +// GC в итоге финализирует объект, но момент непредсказуем ``` ```rust -// Rust — Drop runs automatically when value goes out of scope +// Rust — Drop выполняется автоматически, когда значение выходит из области видимости { let file = File::open("data.bin")?; - // use file... -} // file.drop() called HERE, deterministically — no 'using' needed + // работаем с file... +} // file.drop() вызывается ЗДЕСЬ, детерминированно — 'using' не нужен -// Custom Drop (like implementing IDisposable) +// Собственный Drop (аналог реализации IDisposable) struct TempFile { path: std::path::PathBuf, } impl Drop for TempFile { fn drop(&mut self) { - // Guaranteed to run when TempFile goes out of scope + // Гарантированно выполняется, когда TempFile выходит из области видимости let _ = std::fs::remove_file(&self.path); println!("Cleaned up {:?}", self.path); } @@ -158,81 +158,81 @@ impl Drop for TempFile { fn main() { let tmp = TempFile { path: "scratch.tmp".into() }; - // ... use tmp ... -} // scratch.tmp deleted automatically here + // ... используем tmp ... +} // scratch.tmp удаляется автоматически здесь ``` -**Key difference from C#:** In Rust, *every* type can have deterministic cleanup. You never forget `using` because there's nothing to forget — `Drop` runs when the owner goes out of scope. This pattern is called **RAII** (Resource Acquisition Is Initialization). +**Ключевое отличие от C#:** в Rust *любой* тип может иметь детерминированную очистку. Вы никогда не забудете `using`, потому что забывать нечего — `Drop` выполняется, когда владелец выходит из области видимости. Этот паттерн называется **RAII** (Resource Acquisition Is Initialization — получение ресурса есть инициализация). -> **Rule**: If your type holds a resource (file handle, network connection, lock guard, temp file), implement `Drop`. The ownership system guarantees it runs exactly once. +> **Правило**: если ваш тип хранит ресурс (дескриптор файла, сетевое соединение, охранник блокировки, временный файл), реализуйте `Drop`. Система владения гарантирует, что он выполнится ровно один раз. -### Deref Coercion: Automatic Smart Pointer Unwrapping +### Приведение через Deref: автоматическое разворачивание умных указателей -Rust automatically "unwraps" smart pointers when you call methods or pass them to functions. This is called **Deref coercion**: +Rust автоматически «разворачивает» умные указатели, когда вы вызываете методы или передаёте их в функции. Это называется **приведением через Deref** (Deref coercion): ```rust let boxed: Box<String> = Box::new(String::from("hello")); -// Deref coercion chain: Box<String> → String → str -println!("Length: {}", boxed.len()); // calls str::len() — auto-deref! +// Цепочка приведения через Deref: Box<String> → String → str +println!("Length: {}", boxed.len()); // вызывается str::len() — автоматическое разыменование! fn greet(name: &str) { println!("Hello, {name}"); } let s = String::from("Alice"); -greet(&s); // &String → &str via Deref coercion -greet(&boxed); // &Box<String> → &String → &str — two levels! +greet(&s); // &String → &str через приведение Deref +greet(&boxed); // &Box<String> → &String → &str — два уровня! ``` ```csharp -// C# has no equivalent — you'd need explicit casts or .ToString() -// Closest: implicit conversion operators, but those require explicit definition +// В C# аналога нет — понадобились бы явные приведения или .ToString() +// Ближайшее — неявные операторы преобразования, но их нужно явно определить ``` -**Why this matters:** You can pass `&String` where `&str` is expected, `&Vec<T>` where `&[T]` is expected, and `&Box<T>` where `&T` is expected — all without explicit conversion. This is why Rust APIs typically accept `&str` and `&[T]` rather than `&String` and `&Vec<T>`. +**Почему это важно:** вы можете передать `&String` там, где ожидается `&str`, `&Vec<T>` там, где ожидается `&[T]`, и `&Box<T>` там, где ожидается `&T`, — и всё это без явного преобразования. Поэтому API на Rust обычно принимают `&str` и `&[T]`, а не `&String` и `&Vec<T>`. -### Rc vs Arc: When to Use Which +### Rc против Arc: что выбрать | | `Rc<T>` | `Arc<T>` | |---|---|---| -| **Thread safety** | ❌ Single-thread only | ✅ Thread-safe (atomic ops) | -| **Overhead** | Lower (non-atomic refcount) | Higher (atomic refcount) | -| **Compiler enforced** | Won't compile across `thread::spawn` | Works everywhere | -| **Combine with** | `RefCell<T>` for mutation | `Mutex<T>` or `RwLock<T>` for mutation | +| **Потокобезопасность** | ❌ Только один поток | ✅ Потокобезопасен (атомарные операции) | +| **Накладные расходы** | Ниже (неатомарный счётчик ссылок) | Выше (атомарный счётчик ссылок) | +| **Проверка компилятором** | Не скомпилируется при передаче в `thread::spawn` | Работает везде | +| **Сочетается с** | `RefCell<T>` для изменения | `Mutex<T>` или `RwLock<T>` для изменения | -**Rule of thumb:** Start with `Rc`. The compiler will tell you if you need `Arc`. +**Эмпирическое правило:** начинайте с `Rc`. Компилятор подскажет, если понадобится `Arc`. -### Decision Tree: Which Smart Pointer? +### Дерево решений: какой умный указатель выбрать? ```mermaid graph TD - START["Need shared ownership<br/>or heap allocation?"] - HEAP["Just need heap allocation?"] - SHARED["Shared ownership needed?"] - THREADED["Shared across threads?"] - MUTABLE["Need interior mutability?"] - MAYBE_OWN["Sometimes borrowed,<br/>sometimes owned?"] - - BOX["Use Box<T>"] - RC["Use Rc<T>"] - ARC["Use Arc<T>"] - REFCELL["Use RefCell<T><br/>(or Rc<RefCell<T>>)"] - MUTEX["Use Arc<Mutex<T>>"] - COW["Use Cow<'a, T>"] - OWN["Use owned type<br/>(String, Vec, etc.)"] - - START -->|Yes| HEAP - START -->|No| OWN - HEAP -->|Yes| BOX - HEAP -->|Shared| SHARED - SHARED -->|Single thread| RC - SHARED -->|Multi thread| THREADED - THREADED -->|Read only| ARC - THREADED -->|Read + write| MUTEX - RC -->|Need mutation?| MUTABLE - MUTABLE -->|Yes| REFCELL - MAYBE_OWN -->|Yes| COW + START["Нужно разделяемое владение<br/>или выделение в куче?"] + HEAP["Нужно только выделение в куче?"] + SHARED["Нужно разделяемое владение?"] + THREADED["Разделяется между потоками?"] + MUTABLE["Нужна внутренняя изменяемость?"] + MAYBE_OWN["Иногда заимствуется,<br/>иногда владеется?"] + + BOX["Используйте Box<T>"] + RC["Используйте Rc<T>"] + ARC["Используйте Arc<T>"] + REFCELL["Используйте RefCell<T><br/>(или Rc<RefCell<T>>)"] + MUTEX["Используйте Arc<Mutex<T>>"] + COW["Используйте Cow<'a, T>"] + OWN["Используйте владеемый тип<br/>(String, Vec и т. д.)"] + + START -->|Да| HEAP + START -->|Нет| OWN + HEAP -->|Да| BOX + HEAP -->|Разделяемое| SHARED + SHARED -->|Один поток| RC + SHARED -->|Несколько потоков| THREADED + THREADED -->|Только чтение| ARC + THREADED -->|Чтение и запись| MUTEX + RC -->|Нужна мутация?| MUTABLE + MUTABLE -->|Да| REFCELL + MAYBE_OWN -->|Да| COW style BOX fill:#e3f2fd,color:#000 style RC fill:#e8f5e8,color:#000 @@ -244,30 +244,29 @@ graph TD ``` <details> -<summary><strong>🏋️ Exercise: Choose the Right Smart Pointer</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: выберите подходящий умный указатель</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: For each scenario, choose the correct smart pointer and explain why. +**Задача**: для каждого сценария выберите подходящий умный указатель и объясните почему. -1. A recursive tree data structure -2. A shared configuration object read by multiple components (single thread) -3. A request counter shared across HTTP handler threads -4. A cache that might return borrowed or owned strings -5. A logging buffer that needs mutation through a shared reference +1. Рекурсивная древовидная структура данных +2. Общий объект конфигурации, который читают несколько компонентов (один поток) +3. Счётчик запросов, разделяемый между обработчиками HTTP-запросов в разных потоках +4. Кэш, который может возвращать заимствованные или владеемые строки +5. Буфер логов, который нужно изменять через разделяемую ссылку <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> -1. **`Box<T>`** — recursive types need indirection for known size at compile time -2. **`Rc<T>`** — shared read-only access, single thread, no `Arc` overhead needed -3. **`Arc<Mutex<u64>>`** — shared across threads (`Arc`) with mutation (`Mutex`) -4. **`Cow<'a, str>`** — sometimes returns `&str` (cache hit), sometimes `String` (cache miss) -5. **`RefCell<Vec<String>>`** — interior mutability behind `&self` (single thread) +1. **`Box<T>`** — рекурсивным типам нужна косвенная адресация, чтобы размер был известен на этапе компиляции +2. **`Rc<T>`** — разделяемый доступ только для чтения, один поток, накладные расходы `Arc` не нужны +3. **`Arc<Mutex<u64>>`** — разделяется между потоками (`Arc`) с изменением (`Mutex`) +4. **`Cow<'a, str>`** — иногда возвращает `&str` (попадание в кэш), иногда `String` (промах кэша) +5. **`RefCell<Vec<String>>`** — внутренняя изменяемость за `&self` (один поток) -**Rule of thumb**: Start with owned types. Reach for `Box` when you need indirection, `Rc`/`Arc` when you need sharing, `RefCell`/`Mutex` when you need interior mutability, `Cow` when you want zero-copy for the common case. +**Эмпирическое правило**: начинайте с владеемых типов. Берите `Box`, когда нужна косвенная адресация, `Rc`/`Arc`, когда нужно разделение, `RefCell`/`Mutex`, когда нужна внутренняя изменяемость, и `Cow`, когда хотите работать без копирования в типичном случае. </details> </details> *** - diff --git a/csharp-book/src/ch07-ownership-and-borrowing.md b/csharp-book/src/ch07-ownership-and-borrowing.md index ac71304..16d9f8b 100644 --- a/csharp-book/src/ch07-ownership-and-borrowing.md +++ b/csharp-book/src/ch07-ownership-and-borrowing.md @@ -1,86 +1,86 @@ -## Understanding Ownership +## Понимание владения -> **What you'll learn:** Rust's ownership system — why `let s2 = s1` invalidates `s1` (unlike C# reference copying), -> the three ownership rules, `Copy` vs `Move` types, borrowing with `&` and `&mut`, -> and how the borrow checker replaces garbage collection. +> **Что вы узнаете:** систему владения Rust — почему `let s2 = s1` делает `s1` недействительным (в отличие от копирования ссылок в C#), +> три правила владения, типы `Copy` и `Move`, заимствование через `&` и `&mut`, +> а также то, как проверщик заимствований заменяет сборку мусора. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -Ownership is Rust's most unique feature and the biggest conceptual shift for C# developers. Let's approach it step by step. +Владение — самая уникальная особенность Rust и самый большой концептуальный сдвиг для разработчиков C#. Разберём её шаг за шагом. -### C# Memory Model (Review) +### Модель памяти C# (повторение) ```csharp -// C# - Automatic memory management +// C# — автоматическое управление памятью public void ProcessData() { var data = new List<int> { 1, 2, 3, 4, 5 }; ProcessList(data); - // data is still accessible here - Console.WriteLine(data.Count); // Works fine + // data здесь по-прежнему доступен + Console.WriteLine(data.Count); // Работает нормально - // GC will clean up when no references remain + // GC очистит память, когда не останется ссылок } public void ProcessList(List<int> list) { - list.Add(6); // Modifies the original list + list.Add(6); // Изменяет исходный список } ``` -### Rust Ownership Rules -1. **Each value has exactly one owner** (unless you opt into shared ownership with `Rc<T>`/`Arc<T>` — see [Smart Pointers](ch07-3-smart-pointers-beyond-single-ownership.md)) -2. **When the owner goes out of scope, the value is dropped** (deterministic cleanup — see [Drop](ch07-3-smart-pointers-beyond-single-ownership.md#drop-rusts-idisposable)) -3. **Ownership can be transferred (moved)** +### Правила владения в Rust +1. **У каждого значения ровно один владелец** (если вы не выберете разделяемое владение через `Rc<T>`/`Arc<T>` — см. [Умные указатели](ch07-3-smart-pointers-beyond-single-ownership.md)) +2. **Когда владелец выходит из области видимости, значение уничтожается** (детерминированная очистка — см. [Drop](ch07-3-smart-pointers-beyond-single-ownership.md#drop-rusts-idisposable)) +3. **Владение можно передать (переместить)** ```rust -// Rust - Explicit ownership management +// Rust — явное управление владением fn process_data() { - let data = vec![1, 2, 3, 4, 5]; // data owns the vector - process_list(data); // Ownership moved to function - // println!("{:?}", data); // ❌ Error: data no longer owned here + let data = vec![1, 2, 3, 4, 5]; // data владеет вектором + process_list(data); // Владение передано функции + // println!("{:?}", data); // ❌ Ошибка: data здесь больше не владеет значением } -fn process_list(mut list: Vec<i32>) { // list now owns the vector +fn process_list(mut list: Vec<i32>) { // list теперь владеет вектором list.push(6); - // list is dropped here when function ends + // list уничтожается здесь, при завершении функции } ``` -### Understanding "Move" for C# Developers +### Понятие «перемещения» для разработчиков C# ```csharp -// C# - References are copied, objects stay in place -// (Only reference types — classes — work this way; -// C# value types like struct behave differently) +// C# — ссылки копируются, объекты остаются на месте +// (так работают только ссылочные типы — классы; +// значимые типы C#, такие как struct, ведут себя иначе) var original = new List<int> { 1, 2, 3 }; -var reference = original; // Both variables point to same object +var reference = original; // Обе переменные указывают на один объект original.Add(4); -Console.WriteLine(reference.Count); // 4 - same object +Console.WriteLine(reference.Count); // 4 — тот же объект ``` ```rust -// Rust - Ownership is transferred +// Rust — владение передаётся let original = vec![1, 2, 3]; -let moved = original; // Ownership transferred -// println!("{:?}", original); // ❌ Error: original no longer owns the data -println!("{:?}", moved); // ✅ Works: moved now owns the data +let moved = original; // Владение передано +// println!("{:?}", original); // ❌ Ошибка: original больше не владеет данными +println!("{:?}", moved); // ✅ Работает: moved теперь владеет данными ``` -### Copy Types vs Move Types +### Типы Copy и типы Move ```rust -// Copy types (like C# value types) - copied, not moved -let x = 5; // i32 implements Copy -let y = x; // x is copied to y -println!("{}", x); // ✅ Works: x is still valid - -// Move types (like C# reference types) - moved, not copied -let s1 = String::from("hello"); // String doesn't implement Copy -let s2 = s1; // s1 is moved to s2 -// println!("{}", s1); // ❌ Error: s1 is no longer valid +// Типы Copy (как значимые типы в C#) — копируются, а не перемещаются +let x = 5; // i32 реализует Copy +let y = x; // x копируется в y +println!("{}", x); // ✅ Работает: x по-прежнему действителен + +// Типы Move (как ссылочные типы в C#) — перемещаются, а не копируются +let s1 = String::from("hello"); // String не реализует Copy +let s2 = s1; // s1 перемещается в s2 +// println!("{}", s1); // ❌ Ошибка: s1 больше не действителен ``` -### Practical Example: Swapping Values +### Практический пример: обмен значениями ```csharp -// C# - Simple reference swapping +// C# — простой обмен ссылками public void SwapLists(ref List<int> a, ref List<int> b) { var temp = a; @@ -90,19 +90,19 @@ public void SwapLists(ref List<int> a, ref List<int> b) ``` ```rust -// Rust - Ownership-aware swapping +// Rust — обмен с учётом владения fn swap_vectors(a: &mut Vec<i32>, b: &mut Vec<i32>) { - std::mem::swap(a, b); // Built-in swap function + std::mem::swap(a, b); // Встроенная функция обмена } -// Or manual approach +// Или ручной вариант fn manual_swap() { let mut a = vec![1, 2, 3]; let mut b = vec![4, 5, 6]; - let temp = a; // Move a to temp - a = b; // Move b to a - b = temp; // Move temp to b + let temp = a; // Перемещаем a во временную переменную + a = b; // Перемещаем b в a + b = temp; // Перемещаем temp в b println!("a: {:?}, b: {:?}", a, b); } @@ -110,19 +110,19 @@ fn manual_swap() { *** -## Borrowing Basics +## Основы заимствования -Borrowing is like getting a reference in C#, but with compile-time safety guarantees. +Заимствование похоже на получение ссылки в C#, но с гарантиями безопасности на этапе компиляции. -### C# Reference Parameters +### Ссылочные параметры в C# ```csharp -// C# - ref and out parameters +// C# — параметры ref и out public void ModifyValue(ref int value) { value += 10; } -public void ReadValue(in int value) // readonly reference +public void ReadValue(in int value) // ссылка только для чтения { Console.WriteLine(value); } @@ -133,87 +133,87 @@ public bool TryParse(string input, out int result) } ``` -### Rust Borrowing +### Заимствование в Rust ```rust -// Rust - borrowing with & and &mut -fn modify_value(value: &mut i32) { // Mutable borrow +// Rust — заимствование через & и &mut +fn modify_value(value: &mut i32) { // Изменяемое заимствование *value += 10; } -fn read_value(value: &i32) { // Immutable borrow +fn read_value(value: &i32) { // Неизменяемое заимствование println!("{}", value); } fn main() { let mut x = 5; - read_value(&x); // Borrow immutably - modify_value(&mut x); // Borrow mutably + read_value(&x); // Заимствуем неизменяемо + modify_value(&mut x); // Заимствуем изменяемо - println!("{}", x); // x is still owned here + println!("{}", x); // x по-прежнему принадлежит нам } ``` -### Borrowing Rules (Enforced at Compile Time!) +### Правила заимствования (проверяются на этапе компиляции!) ```rust fn borrowing_rules() { let mut data = vec![1, 2, 3]; - // Rule 1: Multiple immutable borrows are OK + // Правило 1: несколько неизменяемых заимствований допустимы let r1 = &data; let r2 = &data; - println!("{:?} {:?}", r1, r2); // ✅ Works + println!("{:?} {:?}", r1, r2); // ✅ Работает - // Rule 2: Only one mutable borrow at a time + // Правило 2: одновременно — только одно изменяемое заимствование let r3 = &mut data; - // let r4 = &mut data; // ❌ Error: cannot borrow mutably twice - // let r5 = &data; // ❌ Error: cannot borrow immutably while borrowed mutably + // let r4 = &mut data; // ❌ Ошибка: нельзя заимствовать изменяемо дважды + // let r5 = &data; // ❌ Ошибка: нельзя заимствовать неизменяемо, пока есть изменяемое заимствование - r3.push(4); // Use the mutable borrow - // r3 goes out of scope here + r3.push(4); // Используем изменяемое заимствование + // r3 выходит из области видимости здесь - // Rule 3: Can borrow again after previous borrows end - let r6 = &data; // ✅ Works now + // Правило 3: можно заимствовать снова после окончания предыдущих заимствований + let r6 = &data; // ✅ Теперь работает println!("{:?}", r6); } ``` -### C# vs Rust: Reference Safety +### C# против Rust: безопасность ссылок ```csharp -// C# - Potential runtime errors +// C# — возможны ошибки времени выполнения public class ReferenceSafety { private List<int> data = new List<int>(); - public List<int> GetData() => data; // Returns reference to internal data + public List<int> GetData() => data; // Возвращает ссылку на внутреннее состояние public void UnsafeExample() { var reference = GetData(); - // Another thread could modify data here! + // Другой поток может изменить data прямо здесь! Thread.Sleep(1000); - // reference might be invalid or changed - reference.Add(42); // Potential race condition + // reference может быть недействительным или изменённым + reference.Add(42); // Возможное состояние гонки } } ``` ```rust -// Rust - Compile-time safety +// Rust — безопасность на этапе компиляции pub struct SafeContainer { data: Vec<i32>, } impl SafeContainer { - // Return immutable borrow - caller can't modify - // Prefer &[i32] over &Vec<i32> — accept the broadest type + // Возвращаем неизменяемое заимствование — вызывающий код не может изменить данные + // Предпочитайте &[i32] вместо &Vec<i32> — принимайте самый широкий тип pub fn get_data(&self) -> &[i32] { &self.data } - // Return mutable borrow - exclusive access guaranteed + // Возвращаем изменяемое заимствование — эксклюзивный доступ гарантирован pub fn get_data_mut(&mut self) -> &mut Vec<i32> { &mut self.data } @@ -223,23 +223,23 @@ fn safe_example() { let mut container = SafeContainer { data: vec![1, 2, 3] }; let reference = container.get_data(); - // container.get_data_mut(); // ❌ Error: can't borrow mutably while immutably borrowed + // container.get_data_mut(); // ❌ Ошибка: нельзя заимствовать изменяемо, пока есть неизменяемое заимствование - println!("{:?}", reference); // Use immutable reference - // reference goes out of scope here + println!("{:?}", reference); // Используем неизменяемую ссылку + // reference выходит из области видимости здесь - let mut_reference = container.get_data_mut(); // ✅ Now OK + let mut_reference = container.get_data_mut(); // ✅ Теперь можно mut_reference.push(4); } ``` *** -## Move Semantics +## Семантика перемещения -### C# Value Types vs Reference Types +### Значимые и ссылочные типы в C# ```csharp -// C# - Value types are copied +// C# — значимые типы копируются struct Point { public int X { get; set; } @@ -247,20 +247,20 @@ struct Point } var p1 = new Point { X = 1, Y = 2 }; -var p2 = p1; // Copy +var p2 = p1; // Копирование p2.X = 10; -Console.WriteLine(p1.X); // Still 1 +Console.WriteLine(p1.X); // По-прежнему 1 -// C# - Reference types share the object +// C# — ссылочные типы разделяют объект var list1 = new List<int> { 1, 2, 3 }; -var list2 = list1; // Reference copy +var list2 = list1; // Копирование ссылки list2.Add(4); -Console.WriteLine(list1.Count); // 4 - same object +Console.WriteLine(list1.Count); // 4 — тот же объект ``` -### Rust Move Semantics +### Семантика перемещения в Rust ```rust -// Rust - Move by default for non-Copy types +// Rust — по умолчанию перемещение для типов, не реализующих Copy #[derive(Debug)] struct Point { x: i32, @@ -269,12 +269,12 @@ struct Point { fn move_example() { let p1 = Point { x: 1, y: 2 }; - let p2 = p1; // Move (not copy) - // println!("{:?}", p1); // ❌ Error: p1 was moved - println!("{:?}", p2); // ✅ Works + let p2 = p1; // Перемещение (не копирование) + // println!("{:?}", p1); // ❌ Ошибка: p1 перемещён + println!("{:?}", p2); // ✅ Работает } -// To enable copying, implement Copy trait +// Чтобы включить копирование, реализуйте трейт Copy #[derive(Debug, Copy, Clone)] struct CopyablePoint { x: i32, @@ -283,61 +283,61 @@ struct CopyablePoint { fn copy_example() { let p1 = CopyablePoint { x: 1, y: 2 }; - let p2 = p1; // Copy (because it implements Copy) - println!("{:?}", p1); // ✅ Works - println!("{:?}", p2); // ✅ Works + let p2 = p1; // Копирование (потому что реализован Copy) + println!("{:?}", p1); // ✅ Работает + println!("{:?}", p2); // ✅ Работает } ``` -### When Values Are Moved +### Когда значения перемещаются ```rust fn demonstrate_moves() { let s = String::from("hello"); - // 1. Assignment moves - let s2 = s; // s moved to s2 + // 1. Присваивание перемещает + let s2 = s; // s перемещается в s2 - // 2. Function calls move - take_ownership(s2); // s2 moved into function + // 2. Вызовы функций перемещают + take_ownership(s2); // s2 перемещается в функцию - // 3. Returning from functions moves - let s3 = give_ownership(); // Return value moved to s3 + // 3. Возврат из функций перемещает + let s3 = give_ownership(); // Возвращаемое значение перемещается в s3 - println!("{}", s3); // s3 is valid + println!("{}", s3); // s3 действителен } fn take_ownership(s: String) { println!("{}", s); - // s is dropped here + // s уничтожается здесь } fn give_ownership() -> String { - String::from("yours") // Ownership moved to caller + String::from("yours") // Владение перемещается к вызывающему коду } ``` -### Avoiding Moves with Borrowing +### Избегаем перемещений с помощью заимствования ```rust fn demonstrate_borrowing() { let s = String::from("hello"); - // Borrow instead of move - let len = calculate_length(&s); // s is borrowed - println!("'{}' has length {}", s, len); // s is still valid + // Заимствуем вместо перемещения + let len = calculate_length(&s); // s заимствуется + println!("'{}' has length {}", s, len); // s по-прежнему действителен } fn calculate_length(s: &String) -> usize { - s.len() // s is not owned, so it's not dropped + s.len() // s не принадлежит нам, поэтому не уничтожается } ``` *** -## Memory Management: GC vs RAII +## Управление памятью: GC против RAII -### C# Garbage Collection +### Сборка мусора в C# ```csharp -// C# - Automatic memory management +// C# — автоматическое управление памятью public class Person { public string Name { get; set; } @@ -345,20 +345,20 @@ public class Person public void AddHobby(string hobby) { - Hobbies.Add(hobby); // Memory allocated automatically + Hobbies.Add(hobby); // Память выделяется автоматически } - // No explicit cleanup needed - GC handles it - // But IDisposable pattern for resources + // Явная очистка не нужна — GC с ней справляется + // Но для ресурсов существует паттерн IDisposable } using var file = new FileStream("data.txt", FileMode.Open); -// 'using' ensures Dispose() is called +// 'using' гарантирует вызов Dispose() ``` -### Rust Ownership and RAII +### Владение и RAII в Rust ```rust -// Rust - Compile-time memory management +// Rust — управление памятью на этапе компиляции pub struct Person { name: String, hobbies: Vec<String>, @@ -366,32 +366,32 @@ pub struct Person { impl Person { pub fn add_hobby(&mut self, hobby: String) { - self.hobbies.push(hobby); // Memory management tracked at compile time + self.hobbies.push(hobby); // Управление памятью отслеживается на этапе компиляции } - // Drop trait automatically implemented - cleanup is guaranteed - // Compare to C#'s IDisposable: - // C#: using var file = new FileStream(...) // Dispose() called at end of using block - // Rust: let file = File::open(...)? // drop() called at end of scope — no 'using' needed + // Трейт Drop реализуется автоматически — очистка гарантирована + // Сравните с IDisposable в C#: + // C#: using var file = new FileStream(...) // Dispose() вызывается в конце блока using + // Rust: let file = File::open(...)? // drop() вызывается в конце области видимости — 'using' не нужен } -// RAII - Resource Acquisition Is Initialization +// RAII — Resource Acquisition Is Initialization (получение ресурса есть инициализация) { let file = std::fs::File::open("data.txt")?; - // File automatically closed when 'file' goes out of scope - // No 'using' statement needed - handled by type system + // Файл автоматически закрывается, когда 'file' выходит из области видимости + // Оператор using не нужен — это обеспечивает система типов } ``` ```mermaid graph TD - subgraph "C# Memory Management" - CS_ALLOC["Object Allocation<br/>new Person()"] - CS_HEAP["Managed Heap"] - CS_REF["References point to heap"] - CS_GC_CHECK["GC periodically checks<br/>for unreachable objects"] - CS_SWEEP["Mark and sweep<br/>collection"] - CS_PAUSE["[ERROR] GC pause times"] + subgraph "Управление памятью в C#" + CS_ALLOC["Выделение объекта<br/>new Person()"] + CS_HEAP["Управляемая куча"] + CS_REF["Ссылки указывают на кучу"] + CS_GC_CHECK["GC периодически проверяет<br/>недостижимые объекты"] + CS_SWEEP["Сборка по принципу<br/>mark and sweep"] + CS_PAUSE["[ERROR] Паузы GC"] CS_ALLOC --> CS_HEAP CS_HEAP --> CS_REF @@ -399,22 +399,22 @@ graph TD CS_GC_CHECK --> CS_SWEEP CS_SWEEP --> CS_PAUSE - CS_ISSUES["[ERROR] Non-deterministic cleanup<br/>[ERROR] Memory pressure<br/>[ERROR] Finalization complexity<br/>[OK] Easy to use"] + CS_ISSUES["[ERROR] Недетерминированная очистка<br/>[ERROR] Давление на память<br/>[ERROR] Сложность финализации<br/>[OK] Легко использовать"] end - subgraph "Rust Ownership System" - RUST_ALLOC["Value Creation<br/>Person { ... }"] - RUST_OWNER["Single owner<br/>on stack or heap"] - RUST_BORROW["Borrowing system<br/>&T, &mut T"] - RUST_SCOPE["Scope-based cleanup<br/>Drop trait"] - RUST_COMPILE["Compile-time verification"] + subgraph "Система владения Rust" + RUST_ALLOC["Создание значения<br/>Person { ... }"] + RUST_OWNER["Единственный владелец<br/>на стеке или в куче"] + RUST_BORROW["Система заимствований<br/>&T, &mut T"] + RUST_SCOPE["Очистка по области видимости<br/>трейт Drop"] + RUST_COMPILE["Проверка на этапе компиляции"] RUST_ALLOC --> RUST_OWNER RUST_OWNER --> RUST_BORROW RUST_BORROW --> RUST_SCOPE RUST_SCOPE --> RUST_COMPILE - RUST_BENEFITS["[OK] Deterministic cleanup<br/>[OK] Zero runtime cost<br/>[OK] No memory leaks<br/>[ERROR] Learning curve"] + RUST_BENEFITS["[OK] Детерминированная очистка<br/>[OK] Нулевые накладные расходы рантайма<br/>[OK] Никаких утечек памяти<br/>[ERROR] Кривая обучения"] end style CS_ISSUES fill:#ffebee,color:#000 @@ -427,67 +427,67 @@ graph TD <details> -<summary><strong>🏋️ Exercise: Fix the Borrow Checker Errors</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: исправьте ошибки проверщика заимствований</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Each snippet below has a borrow checker error. Fix them without changing the output. +**Задача**: в каждом фрагменте ниже есть ошибка проверщика заимствований. Исправьте их, не меняя вывод. ```rust -// 1. Move after use +// 1. Перемещение после использования fn problem_1() { let name = String::from("Alice"); let greeting = format!("Hello, {name}!"); - let upper = name.to_uppercase(); // hint: borrow instead of move + let upper = name.to_uppercase(); // подсказка: заимствуйте вместо перемещения println!("{greeting} — {upper}"); } -// 2. Mutable + immutable borrow overlap +// 2. Пересечение изменяемого и неизменяемого заимствований fn problem_2() { let mut numbers = vec![1, 2, 3]; let first = &numbers[0]; - numbers.push(4); // hint: reorder operations + numbers.push(4); // подсказка: переупорядочьте операции println!("first = {first}"); } -// 3. Returning a reference to a local +// 3. Возврат ссылки на локальную переменную fn problem_3() -> String { let s = String::from("hello"); - s // hint: return owned value, not &str + s // подсказка: верните владеемое значение, а не &str } ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust -// 1. format! already borrows — the fix is that format! takes a reference. -// The original code actually compiles! But if we had `let greeting = name;` -// then fix by using &name: +// 1. format! уже заимствует — исправление в том, что format! принимает ссылку. +// Исходный код на самом деле компилируется! Но если бы было `let greeting = name;`, +// исправили бы через &name: fn solution_1() { let name = String::from("Alice"); - let greeting = format!("Hello, {}!", &name); // borrow - let upper = name.to_uppercase(); // name still valid + let greeting = format!("Hello, {}!", &name); // заимствование + let upper = name.to_uppercase(); // name по-прежнему действителен println!("{greeting} — {upper}"); } -// 2. Use the immutable borrow before the mutable operation: +// 2. Используем неизменяемое заимствование до изменяющей операции: fn solution_2() { let mut numbers = vec![1, 2, 3]; - let first = numbers[0]; // copy the i32 value (i32 is Copy) + let first = numbers[0]; // копируем значение i32 (i32 реализует Copy) numbers.push(4); println!("first = {first}"); } -// 3. Return the owned String (already correct — common beginner confusion): +// 3. Возвращаем владеемую String (уже правильно — частая путаница у новичков): fn solution_3() -> String { let s = String::from("hello"); - s // ownership transferred to caller — this is the correct pattern + s // владение передаётся вызывающему коду — это правильный паттерн } ``` -**Key takeaways**: -- `format!()` borrows its arguments — it doesn't move them -- Primitive types like `i32` implement `Copy`, so indexing copies the value -- Returning an owned value transfers ownership to the caller — no lifetime issues +**Ключевые выводы**: +- `format!()` заимствует свои аргументы — он их не перемещает +- Примитивные типы вроде `i32` реализуют `Copy`, поэтому индексация копирует значение +- Возврат владеемого значения передаёт владение вызывающему коду — проблем с временами жизни нет </details> </details> diff --git a/csharp-book/src/ch08-1-package-management-cargo-vs-nuget.md b/csharp-book/src/ch08-1-package-management-cargo-vs-nuget.md index 074b8ea..d07ff81 100644 --- a/csharp-book/src/ch08-1-package-management-cargo-vs-nuget.md +++ b/csharp-book/src/ch08-1-package-management-cargo-vs-nuget.md @@ -1,13 +1,13 @@ -## Package Management: Cargo vs NuGet +## Управление пакетами: Cargo против NuGet -> **What you'll learn:** `Cargo.toml` vs `.csproj`, version specifiers, `Cargo.lock`, -> feature flags for conditional compilation, and common Cargo commands mapped to their NuGet/dotnet equivalents. +> **Что вы узнаете:** `Cargo.toml` против `.csproj`, спецификаторы версий, `Cargo.lock`, +> флаги функций для условной компиляции, а также распространённые команды Cargo и их аналоги в NuGet/dotnet. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Dependency Declaration +### Объявление зависимостей -#### C# NuGet Dependencies +#### Зависимости NuGet в C# ```xml <!-- MyApp.csproj --> <Project Sdk="Microsoft.NET.Sdk"> @@ -23,7 +23,7 @@ </Project> ``` -#### Rust Cargo Dependencies +#### Зависимости Cargo в Rust ```toml # Cargo.toml [package] @@ -32,28 +32,28 @@ version = "0.1.0" edition = "2021" [dependencies] -serde_json = "1.0" # From crates.io (like NuGet) -serde = { version = "1.0", features = ["derive"] } # With features +serde_json = "1.0" # Из crates.io (как NuGet) +serde = { version = "1.0", features = ["derive"] } # С функциями log = "0.4" tokio = { version = "1.0", features = ["full"] } -# Local dependencies (like ProjectReference) +# Локальные зависимости (как ProjectReference) my_library = { path = "../my_library" } -# Git dependencies +# Зависимости из git my_git_crate = { git = "https://github.com/user/repo" } -# Development dependencies (like test packages) +# Зависимости для разработки (как тестовые пакеты) [dev-dependencies] -criterion = "0.5" # Benchmarking -proptest = "1.0" # Property testing +criterion = "0.5" # Бенчмарки +proptest = "1.0" # Property-тестирование ``` -### Version Management +### Управление версиями -#### C# Package Versioning +#### Версионирование пакетов в C# ```xml -<!-- Centralized package management (Directory.Packages.props) --> +<!-- Централизованное управление пакетами (Directory.Packages.props) --> <Project> <PropertyGroup> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> @@ -63,29 +63,29 @@ proptest = "1.0" # Property testing <PackageVersion Include="Serilog" Version="3.0.1" /> </Project> -<!-- packages.lock.json for reproducible builds --> +<!-- packages.lock.json для воспроизводимых сборок --> ``` -#### Rust Version Management +#### Управление версиями в Rust ```toml -# Cargo.toml - Semantic versioning +# Cargo.toml — семантическое версионирование [dependencies] -serde = "1.0" # Compatible with 1.x.x (>=1.0.0, <2.0.0) -log = "0.4.17" # Compatible with 0.4.x (>=0.4.17, <0.5.0) -regex = "=1.5.4" # Exact version -chrono = "^0.4" # Caret requirements (default) -uuid = "~1.3.0" # Tilde requirements (>=1.3.0, <1.4.0) +serde = "1.0" # Совместимо с 1.x.x (>=1.0.0, <2.0.0) +log = "0.4.17" # Совместимо с 0.4.x (>=0.4.17, <0.5.0) +regex = "=1.5.4" # Точная версия +chrono = "^0.4" # Требования с кареткой (по умолчанию) +uuid = "~1.3.0" # Требования с тильдой (>=1.3.0, <1.4.0) -# Cargo.lock - Exact versions for reproducible builds (auto-generated) +# Cargo.lock — точные версии для воспроизводимых сборок (генерируется автоматически) [[package]] name = "serde" version = "1.0.163" -# ... exact dependency tree +# ... точное дерево зависимостей ``` -### Package Sources +### Источники пакетов -#### C# Package Sources +#### Источники пакетов в C# ```xml <!-- nuget.config --> <configuration> @@ -96,7 +96,7 @@ version = "1.0.163" </configuration> ``` -#### Rust Package Sources +#### Источники пакетов в Rust ```toml # .cargo/config.toml [source.crates-io] @@ -105,30 +105,30 @@ replace-with = "my-awesome-registry" [source.my-awesome-registry] registry = "https://my-intranet:8080/index" -# Alternative registries +# Альтернативные реестры [registries] my-registry = { index = "https://my-intranet:8080/index" } -# In Cargo.toml +# В Cargo.toml [dependencies] my_crate = { version = "1.0", registry = "my-registry" } ``` -### Common Commands Comparison +### Сравнение распространённых команд -| Task | C# Command | Rust Command | +| Задача | Команда C# | Команда Rust | |------|------------|-------------| -| Restore packages | `dotnet restore` | `cargo fetch` | -| Add package | `dotnet add package Newtonsoft.Json` | `cargo add serde_json` | -| Remove package | `dotnet remove package Newtonsoft.Json` | `cargo remove serde_json` | -| Update packages | `dotnet update` | `cargo update` | -| List packages | `dotnet list package` | `cargo tree` | -| Audit security | `dotnet list package --vulnerable` | `cargo audit` | -| Clean build | `dotnet clean` | `cargo clean` | +| Восстановить пакеты | `dotnet restore` | `cargo fetch` | +| Добавить пакет | `dotnet add package Newtonsoft.Json` | `cargo add serde_json` | +| Удалить пакет | `dotnet remove package Newtonsoft.Json` | `cargo remove serde_json` | +| Обновить пакеты | `dotnet update` | `cargo update` | +| Список пакетов | `dotnet list package` | `cargo tree` | +| Аудит безопасности | `dotnet list package --vulnerable` | `cargo audit` | +| Чистая сборка | `dotnet clean` | `cargo clean` | -### Features: Conditional Compilation +### Features: условная компиляция -#### C# Conditional Compilation +#### Условная компиляция в C# ```csharp #if DEBUG Console.WriteLine("Debug mode"); @@ -136,20 +136,20 @@ my_crate = { version = "1.0", registry = "my-registry" } Console.WriteLine("Release mode"); #endif -// Project file features +// Функции в файле проекта <PropertyGroup Condition="'$(Configuration)'=='Debug'"> <DefineConstants>DEBUG;TRACE</DefineConstants> </PropertyGroup> ``` -#### Rust Feature Gates +#### Feature-флаги в Rust ```toml # Cargo.toml [features] -default = ["json"] # Default features -json = ["serde_json"] # Feature that enables serde_json -xml = ["serde_xml"] # Alternative serialization -advanced = ["json", "xml"] # Composite feature +default = ["json"] # Функции по умолчанию +json = ["serde_json"] # Функция, включающая serde_json +xml = ["serde_xml"] # Альтернативная сериализация +advanced = ["json", "xml"] # Составная функция [dependencies] serde_json = { version = "1.0", optional = true } @@ -157,7 +157,7 @@ serde_xml = { version = "0.4", optional = true } ``` ```rust -// Conditional compilation based on features +// Условная компиляция на основе функций #[cfg(feature = "json")] use serde_json; @@ -176,24 +176,24 @@ pub fn serialize_data(data: &MyStruct) -> String { } ``` -### Using External Crates +### Использование внешних крейтов -#### Popular Crates for C# Developers +#### Популярные крейты для разработчиков C# -| C# Library | Rust Crate | Purpose | +| Библиотека C# | Крейт Rust | Назначение | |------------|------------|---------| -| System.Text.Json / Newtonsoft.Json | `serde_json` | JSON serialization | -| HttpClient | `reqwest` | HTTP client | -| Entity Framework | `diesel` / `sqlx` | ORM / SQL toolkit | -| NLog/Serilog | `log` + `env_logger` | Logging | -| xUnit/NUnit | Built-in `#[test]` | Unit testing | -| Moq | `mockall` | Mocking | -| Flurl | `url` | URL manipulation | -| Polly | `tower` | Resilience patterns | - -#### Example: HTTP Client Migration +| System.Text.Json / Newtonsoft.Json | `serde_json` | Сериализация JSON | +| HttpClient | `reqwest` | HTTP-клиент | +| Entity Framework | `diesel` / `sqlx` | ORM / инструментарий SQL | +| NLog/Serilog | `log` + `env_logger` | Логирование | +| xUnit/NUnit | Встроенный `#[test]` | Модульное тестирование | +| Moq | `mockall` | Моки | +| Flurl | `url` | Работа с URL | +| Polly | `tower` | Паттерны отказоустойчивости | + +#### Пример: миграция HTTP-клиента ```csharp -// C# HttpClient usage +// Использование HttpClient в C# public class ApiClient { private readonly HttpClient _httpClient; @@ -208,7 +208,7 @@ public class ApiClient ``` ```rust -// Rust reqwest usage +// Использование reqwest в Rust use reqwest; use serde::Deserialize; diff --git a/csharp-book/src/ch08-crates-and-modules.md b/csharp-book/src/ch08-crates-and-modules.md index a8f56f7..004598f 100644 --- a/csharp-book/src/ch08-crates-and-modules.md +++ b/csharp-book/src/ch08-crates-and-modules.md @@ -1,17 +1,17 @@ -## Modules and Crates: Code Organization +## Модули и крейты: организация кода -> **What you'll learn:** Rust's module system vs C# namespaces and assemblies, `pub`/`pub(crate)`/`pub(super)` visibility, -> file-based module organization, and how crates map to .NET assemblies. +> **Что вы узнаете:** систему модулей Rust в сравнении с пространствами имён и сборками C#, модификаторы видимости `pub`/`pub(crate)`/`pub(super)`, +> организацию модулей по файлам и то, как крейты соответствуют сборкам .NET. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -Understanding Rust's module system is essential for organizing code and managing dependencies. For C# developers, this is analogous to understanding namespaces, assemblies, and NuGet packages. +Понимание системы модулей Rust необходимо для организации кода и управления зависимостями. Разработчикам C# это аналогично пониманию пространств имён, сборок и пакетов NuGet. -### Rust Modules vs C# Namespaces +### Модули Rust против пространств имён C# -#### C# Namespace Organization +#### Организация пространств имён в C# ```csharp -// File: Models/User.cs +// Файл: Models/User.cs namespace MyApp.Models { public class User @@ -21,7 +21,7 @@ namespace MyApp.Models } } -// File: Services/UserService.cs +// Файл: Services/UserService.cs using MyApp.Models; namespace MyApp.Services @@ -35,7 +35,7 @@ namespace MyApp.Services } } -// File: Program.cs +// Файл: Program.cs using MyApp.Models; using MyApp.Services; @@ -52,9 +52,9 @@ namespace MyApp } ``` -#### Rust Module Organization +#### Организация модулей в Rust ```rust -// File: src/models.rs +// Файл: src/models.rs pub struct User { pub name: String, pub age: u32, @@ -66,7 +66,7 @@ impl User { } } -// File: src/services.rs +// Файл: src/services.rs use crate::models::User; pub struct UserService; @@ -77,7 +77,7 @@ impl UserService { } } -// File: src/lib.rs (or main.rs) +// Файл: src/lib.rs (или main.rs) pub mod models; pub mod services; @@ -90,14 +90,14 @@ fn main() { } ``` -### Module Hierarchy and Visibility +### Иерархия модулей и видимость ```mermaid graph TD - Crate["crate (root)"] --> ModA["mod data"] + Crate["crate (корень)"] --> ModA["mod data"] Crate --> ModB["mod api"] ModA --> SubA1["pub struct Repo"] - ModA --> SubA2["fn helper (private)"] + ModA --> SubA2["fn helper (приватная)"] ModB --> SubB1["pub fn handle()"] ModB --> SubB2["pub(crate) fn internal()"] ModB --> SubB3["pub(super) fn parent_only()"] @@ -109,71 +109,71 @@ graph TD style SubB3 fill:#fff9c4,color:#000 ``` -> 🟢 Green = public everywhere  |  🟡 Yellow = restricted visibility  |  🔴 Red = private +> 🟢 Зелёный = публичный везде  |  🟡 Жёлтый = ограниченная видимость  |  🔴 Красный = приватный -#### C# Visibility Modifiers +#### Модификаторы видимости в C# ```csharp namespace MyApp.Data { - // public - accessible from anywhere + // public — доступен откуда угодно public class Repository { - // private - only within this class + // private — только внутри этого класса private string connectionString; - // internal - within this assembly + // internal — внутри этой сборки internal void Connect() { } - // protected - this class and subclasses + // protected — этот класс и его наследники protected virtual void Initialize() { } - // public - accessible from anywhere + // public — доступен откуда угодно public void Save(object data) { } } } ``` -#### Rust Visibility Rules +#### Правила видимости в Rust ```rust -// Everything is private by default in Rust +// В Rust всё по умолчанию приватно mod data { - struct Repository { // Private struct - connection_string: String, // Private field + struct Repository { // Приватная структура + connection_string: String, // Приватное поле } impl Repository { - fn new() -> Repository { // Private function + fn new() -> Repository { // Приватная функция Repository { connection_string: "localhost".to_string(), } } - pub fn connect(&self) { // Public method - // Only accessible within this module and its children + pub fn connect(&self) { // Публичный метод + // Доступен только внутри этого модуля и его потомков } - pub(crate) fn initialize(&self) { // Crate-level public - // Accessible anywhere in this crate + pub(crate) fn initialize(&self) { // Публичен на уровне крейта + // Доступен из любого места этого крейта } - pub(super) fn internal_method(&self) { // Parent module public - // Accessible in parent module + pub(super) fn internal_method(&self) { // Публичен для родительского модуля + // Доступен в родительском модуле } } - // Public struct - accessible from outside the module + // Публичная структура — доступна извне модуля pub struct PublicRepository { - pub data: String, // Public field - private_data: String, // Private field (no pub) + pub data: String, // Публичное поле + private_data: String, // Приватное поле (без pub) } } -pub use data::PublicRepository; // Re-export for external use +pub use data::PublicRepository; // Реэкспорт для внешнего использования ``` -### Module File Organization +### Организация файлов модулей -#### C# Project Structure +#### Структура проекта C# ```text MyApp/ ├── MyApp.csproj @@ -188,18 +188,18 @@ MyApp/ └── Program.cs ``` -#### Rust Module File Structure +#### Структура файлов модулей в Rust ```text my_app/ ├── Cargo.toml └── src/ - ├── main.rs (or lib.rs) + ├── main.rs (или lib.rs) ├── models/ - │ ├── mod.rs // Module declaration + │ ├── mod.rs // Объявление модуля │ ├── user.rs │ └── product.rs ├── services/ - │ ├── mod.rs // Module declaration + │ ├── mod.rs // Объявление модуля │ ├── user_service.rs │ └── product_service.rs └── controllers/ @@ -207,38 +207,38 @@ my_app/ └── api_controller.rs ``` -#### Module Declaration Patterns +#### Паттерны объявления модулей ```rust // src/models/mod.rs -pub mod user; // Declares user.rs as a submodule -pub mod product; // Declares product.rs as a submodule +pub mod user; // Объявляет user.rs как подмодуль +pub mod product; // Объявляет product.rs как подмодуль -// Re-export commonly used types +// Реэкспорт часто используемых типов pub use user::User; pub use product::Product; // src/main.rs -mod models; // Declares models/ as a module -mod services; // Declares services/ as a module +mod models; // Объявляет models/ как модуль +mod services; // Объявляет services/ как модуль -// Import specific items +// Импорт конкретных элементов use models::{User, Product}; use services::UserService; -// Or import the entire module -use models::user::*; // Import all public items from user module +// Или импорт всего модуля +use models::user::*; // Импортирует все публичные элементы модуля user ``` *** -## Crates vs .NET Assemblies +## Крейты против сборок .NET -### Understanding Crates -In Rust, a **crate** is the fundamental unit of compilation and code distribution, similar to how an **assembly** works in .NET. +### Понятие крейта +В Rust **крейт** — это базовая единица компиляции и распространения кода, похожая на **сборку** (assembly) в .NET. -#### C# Assembly Model +#### Модель сборок C# ```csharp -// MyLibrary.dll - Compiled assembly +// MyLibrary.dll — скомпилированная сборка namespace MyLibrary { public class Calculator @@ -247,7 +247,7 @@ namespace MyLibrary } } -// MyApp.exe - Executable assembly that references MyLibrary.dll +// MyApp.exe — исполняемая сборка, которая ссылается на MyLibrary.dll using MyLibrary; class Program @@ -260,9 +260,9 @@ class Program } ``` -#### Rust Crate Model +#### Модель крейтов Rust ```toml -# Cargo.toml for library crate +# Cargo.toml для библиотечного крейта [package] name = "my_calculator" version = "0.1.0" @@ -273,7 +273,7 @@ name = "my_calculator" ``` ```rust -// src/lib.rs - Library crate +// src/lib.rs — библиотечный крейт pub struct Calculator; impl Calculator { @@ -284,7 +284,7 @@ impl Calculator { ``` ```toml -# Cargo.toml for binary crate that uses the library +# Cargo.toml для бинарного крейта, который использует библиотеку [package] name = "my_app" version = "0.1.0" @@ -295,7 +295,7 @@ my_calculator = { path = "../my_calculator" } ``` ```rust -// src/main.rs - Binary crate +// src/main.rs — бинарный крейт use my_calculator::Calculator; fn main() { @@ -304,21 +304,21 @@ fn main() { } ``` -### Crate Types Comparison +### Сравнение типов крейтов -| C# Concept | Rust Equivalent | Purpose | +| Концепция C# | Аналог в Rust | Назначение | |------------|----------------|---------| -| Class Library (.dll) | Library crate | Reusable code | -| Console App (.exe) | Binary crate | Executable program | -| NuGet Package | Published crate | Distribution unit | -| Assembly (.dll/.exe) | Compiled crate | Compilation unit | -| Solution (.sln) | Workspace | Multi-project organization | +| Библиотека классов (.dll) | Библиотечный крейт | Повторно используемый код | +| Консольное приложение (.exe) | Бинарный крейт | Исполняемая программа | +| Пакет NuGet | Опубликованный крейт | Единица распространения | +| Сборка (.dll/.exe) | Скомпилированный крейт | Единица компиляции | +| Solution (.sln) | Workspace | Организация нескольких проектов | -### Workspace vs Solution +### Workspace против Solution -#### C# Solution Structure +#### Структура Solution в C# ```xml -<!-- MySolution.sln structure --> +<!-- Структура MySolution.sln --> <Solution> <Project Include="WebApi/WebApi.csproj" /> <Project Include="Business/Business.csproj" /> @@ -327,9 +327,9 @@ fn main() { </Solution> ``` -#### Rust Workspace Structure +#### Структура Workspace в Rust ```toml -# Cargo.toml at workspace root +# Cargo.toml в корне workspace [workspace] members = [ "web_api", @@ -339,7 +339,7 @@ members = [ ] [workspace.dependencies] -serde = "1.0" # Shared dependency versions +serde = "1.0" # Общие версии зависимостей tokio = "1.0" ``` @@ -352,18 +352,18 @@ edition = "2021" [dependencies] business = { path = "../business" } -serde = { workspace = true } # Use workspace version +serde = { workspace = true } # Использовать версию из workspace tokio = { workspace = true } ``` --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Design a Module Tree</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: спроектируйте дерево модулей</strong> (нажмите, чтобы раскрыть)</summary> -Given this C# project layout, design the equivalent Rust module tree: +Дана такая структура проекта на C#. Спроектируйте эквивалентное дерево модулей на Rust: ```csharp // C# @@ -373,15 +373,15 @@ namespace MyApp.Models { public class User { } } namespace MyApp.Models { public class Session { } } ``` -Requirements: -1. `AuthService` and both models must be public -2. `TokenStore` must be private to the `services` module -3. Provide the file layout **and** the `mod` / `pub` declarations in `lib.rs` +Требования: +1. `AuthService` и обе модели должны быть публичными +2. `TokenStore` должен быть приватным для модуля `services` +3. Приведите структуру файлов **и** объявления `mod` / `pub` в `lib.rs` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> -File layout: +Структура файлов: ``` src/ ├── lib.rs @@ -401,20 +401,20 @@ pub mod services; pub mod models; // src/services/mod.rs -mod token_store; // private — like C# internal -pub mod auth_service; // public +mod token_store; // приватный — как internal в C# +pub mod auth_service; // публичный // src/services/auth_service.rs -use super::token_store::TokenStore; // visible within the module +use super::token_store::TokenStore; // виден внутри модуля pub struct AuthService; impl AuthService { - pub fn login(&self) { /* uses TokenStore internally */ } + pub fn login(&self) { /* использует TokenStore внутри */ } } // src/services/token_store.rs -pub(super) struct TokenStore; // visible to parent (services) only +pub(super) struct TokenStore; // виден только родительскому модулю (services) // src/models/mod.rs pub mod user; diff --git a/csharp-book/src/ch09-1-crate-level-error-types-and-result-alias.md b/csharp-book/src/ch09-1-crate-level-error-types-and-result-alias.md index 6a3fa25..70cbdbf 100644 --- a/csharp-book/src/ch09-1-crate-level-error-types-and-result-alias.md +++ b/csharp-book/src/ch09-1-crate-level-error-types-and-result-alias.md @@ -1,13 +1,13 @@ -## Crate-Level Error Types and Result Aliases +## Типы ошибок уровня крейта и псевдонимы Result -> **What you'll learn:** The production pattern of defining a per-crate error enum with `thiserror`, -> creating a `Result<T>` type alias, and when to choose `thiserror` (libraries) vs `anyhow` (applications). +> **Что вы узнаете:** производственный паттерн определения перечисления ошибок для каждого крейта с помощью `thiserror`, +> создание псевдонима типа `Result<T>`, а также когда выбирать `thiserror` (библиотеки) или `anyhow` (приложения). > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -A critical pattern for production Rust: define a per-crate error enum and a `Result` type alias to eliminate boilerplate. +Ключевой паттерн для производственного кода на Rust: определить перечисление ошибок для каждого крейта и псевдоним типа `Result`, чтобы убрать шаблонный код. -### The Pattern +### Паттерн ```rust // src/error.rs use thiserror::Error; @@ -30,21 +30,21 @@ pub enum AppError { NotFound { entity: String, id: String }, } -/// Crate-wide Result alias — every function returns this +/// Псевдоним Result для всего крейта — каждая функция возвращает именно его pub type Result<T> = std::result::Result<T, AppError>; ``` -### Usage Throughout Your Crate +### Использование во всём крейте ```rust use crate::error::{AppError, Result}; -// Assumes a database pool is available, e.g.: +// Предполагается, что пул соединений с базой доступен, например: // async fn get_user(pool: &PgPool, id: Uuid) -> Result<User> -// Here we show the pattern with `pool` as shorthand. +// Здесь показан паттерн, где `pool` — сокращение. pub async fn get_user(id: Uuid) -> Result<User> { let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) .fetch_optional(&pool) - .await?; // sqlx::Error → AppError::Database via #[from] + .await?; // sqlx::Error → AppError::Database через #[from] user.ok_or_else(|| AppError::NotFound { entity: "User".into(), @@ -62,9 +62,9 @@ pub async fn create_user(req: CreateUserRequest) -> Result<User> { } ``` -### C# Comparison +### Сравнение с C# ```csharp -// C# equivalent pattern +// Аналогичный паттерн в C# public class AppException : Exception { public string ErrorCode { get; } @@ -74,31 +74,31 @@ public class AppException : Exception } } -// But in C#, callers don't know what exceptions to expect! -// In Rust, the error type is in the function signature. +// Но в C# вызывающий код не знает, какие исключения ожидать! +// В Rust тип ошибки указан в сигнатуре функции. ``` -### Why This Matters -- **`thiserror`** generates `Display` and `Error` impls automatically -- **`#[from]`** enables the `?` operator to convert library errors automatically -- The `Result<T>` alias means every function signature is clean: `fn foo() -> Result<Bar>` -- **Unlike C# exceptions**, callers see all possible error variants in the type +### Почему это важно +- **`thiserror`** автоматически генерирует реализации `Display` и `Error` +- **`#[from]`** позволяет оператору `?` автоматически преобразовывать ошибки библиотек +- Псевдоним `Result<T>` делает каждую сигнатуру функции чистой: `fn foo() -> Result<Bar>` +- **В отличие от исключений C#** вызывающий код видит все возможные варианты ошибок в типе -### thiserror vs anyhow: When to Use Which +### thiserror против anyhow: что выбрать -Two crates dominate Rust error handling. Choosing between them is the first decision you'll make: +Обработку ошибок в Rust определяют два крейта. Выбор между ними — первое решение, которое вам предстоит принять: | | `thiserror` | `anyhow` | |---|---|---| -| **Purpose** | Define structured error types for **libraries** | Quick error handling for **applications** | -| **Output** | Custom enum you control | Opaque `anyhow::Error` wrapper | -| **Caller sees** | All error variants in the type | Just `anyhow::Error` — opaque | -| **Best for** | Library crates, APIs, any code with consumers | Binaries, scripts, prototypes, CLI tools | -| **Downcasting** | `match` on variants directly | `error.downcast_ref::<MyError>()` | +| **Назначение** | Определение структурированных типов ошибок для **библиотек** | Быстрая обработка ошибок для **приложений** | +| **Результат** | Собственное перечисление, которым вы управляете | Непрозрачная обёртка `anyhow::Error` | +| **Что видит вызывающий код** | Все варианты ошибок в типе | Просто `anyhow::Error` — непрозрачный | +| **Лучше всего для** | Библиотечных крейтов, API, любого кода с потребителями | Бинарников, скриптов, прототипов, CLI-утилит | +| **Приведение типов** | `match` по вариантам напрямую | `error.downcast_ref::<MyError>()` | ```rust -// thiserror — for LIBRARIES (callers need to match on error variants) +// thiserror — для БИБЛИОТЕК (вызывающему коду нужно сопоставлять варианты ошибок) use thiserror::Error; #[derive(Error, Debug)] @@ -123,7 +123,7 @@ pub fn read_config(path: &str) -> Result<String, StorageError> { ``` ```rust -// anyhow — for APPLICATIONS (just propagate errors, don't define types) +// anyhow — для ПРИЛОЖЕНИЙ (просто пробрасываем ошибки, не определяя типов) use anyhow::{Context, Result}; fn main() -> Result<()> { @@ -137,30 +137,30 @@ fn main() -> Result<()> { Ok(()) } // anyhow::Result<T> = Result<T, anyhow::Error> -// .context() adds human-readable context to any error +// .context() добавляет понятный для человека контекст к любой ошибке ``` ```csharp -// C# comparison: -// thiserror ≈ defining custom exception classes with specific properties -// anyhow ≈ catching Exception and wrapping with message: +// Сравнение с C#: +// thiserror ≈ определение собственных классов исключений с конкретными свойствами +// anyhow ≈ перехват Exception и оборачивание с сообщением: // throw new InvalidOperationException("Failed to read config", ex); ``` -**Guideline**: If your code is a **library** (other code calls it), use `thiserror`. If your code is an **application** (the final binary), use `anyhow`. Many projects use both — `thiserror` for the library crate's public API, `anyhow` in the `main()` binary. +**Правило**: если ваш код — **библиотека** (его вызывает другой код), используйте `thiserror`. Если ваш код — **приложение** (финальный бинарник), используйте `anyhow`. Многие проекты используют оба: `thiserror` для публичного API библиотечного крейта, `anyhow` в бинарнике в `main()`. -### Error Recovery Patterns +### Паттерны восстановления после ошибок -C# developers are used to `try/catch` blocks that recover from specific exceptions. Rust uses combinators on `Result` for the same purpose: +Разработчики C# привыкли к блокам `try/catch`, которые восстанавливаются после конкретных исключений. В Rust для того же используются комбинаторы над `Result`: ```rust use std::fs; -// Pattern 1: Recover with a fallback value +// Паттерн 1: восстановление через значение по умолчанию let config = fs::read_to_string("config.toml") - .unwrap_or_else(|_| String::from("port = 8080")); // default if missing + .unwrap_or_else(|_| String::from("port = 8080")); // значение по умолчанию, если файла нет -// Pattern 2: Recover from specific errors, propagate others +// Паттерн 2: восстанавливаемся после конкретных ошибок, остальные пробрасываем fn read_or_create(path: &str) -> Result<String, std::io::Error> { match fs::read_to_string(path) { Ok(content) => Ok(content), @@ -169,11 +169,11 @@ fn read_or_create(path: &str) -> Result<String, std::io::Error> { fs::write(path, &default)?; Ok(default) } - Err(e) => Err(e), // propagate permission errors, etc. + Err(e) => Err(e), // пробрасываем ошибки доступа и прочие } } -// Pattern 3: Add context before propagating +// Паттерн 3: добавляем контекст перед распространением use anyhow::Context; fn load_config() -> anyhow::Result<Config> { @@ -184,7 +184,7 @@ fn load_config() -> anyhow::Result<Config> { Ok(config) } -// Pattern 4: Map errors to your domain type +// Паттерн 4: преобразуем ошибки в доменный тип fn parse_port(s: &str) -> Result<u16, AppError> { s.parse::<u16>() .map_err(|_| AppError::Validation { @@ -194,35 +194,35 @@ fn parse_port(s: &str) -> Result<u16, AppError> { ``` ```csharp -// C# equivalents: +// Аналоги в C#: try { config = File.ReadAllText("config.toml"); } -catch (FileNotFoundException) { config = "port = 8080"; } // Pattern 1 +catch (FileNotFoundException) { config = "port = 8080"; } // Паттерн 1 try { /* ... */ } -catch (FileNotFoundException) { /* create file */ } // Pattern 2 -catch { throw; } // re-throw others +catch (FileNotFoundException) { /* создать файл */ } // Паттерн 2 +catch { throw; } // остальные пробрасываем ``` -**When to recover vs propagate:** -- **Recover** when the error has a sensible default or retry strategy -- **Propagate with `?`** when the *caller* should decide what to do -- **Add context** (`.context()`) at module boundaries to build an error trail +**Когда восстанавливаться, а когда пробрасывать:** +- **Восстанавливайтесь**, когда у ошибки есть разумное значение по умолчанию или стратегия повтора +- **Пробрасывайте через `?`**, когда решение должен принимать *вызывающий* код +- **Добавляйте контекст** (`.context()`) на границах модулей, чтобы построить цепочку ошибок --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Design a Crate Error Type</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: спроектируйте тип ошибок крейта</strong> (нажмите, чтобы раскрыть)</summary> -You're building a user registration service. Design the error type using `thiserror`: +Вы создаёте сервис регистрации пользователей. Спроектируйте тип ошибок с помощью `thiserror`: -1. Define `RegistrationError` with variants: `DuplicateEmail(String)`, `WeakPassword(String)`, `DatabaseError(#[from] sqlx::Error)`, `RateLimited { retry_after_secs: u64 }` -2. Create a `type Result<T> = std::result::Result<T, RegistrationError>;` alias -3. Write a `register_user(email: &str, password: &str) -> Result<()>` that demonstrates `?` propagation and explicit error construction +1. Определите `RegistrationError` с вариантами: `DuplicateEmail(String)`, `WeakPassword(String)`, `DatabaseError(#[from] sqlx::Error)`, `RateLimited { retry_after_secs: u64 }` +2. Создайте псевдоним `type Result<T> = std::result::Result<T, RegistrationError>;` +3. Напишите `register_user(email: &str, password: &str) -> Result<()>`, которая демонстрирует распространение через `?` и явное создание ошибок <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use thiserror::Error; @@ -251,10 +251,10 @@ pub fn register_user(email: &str, password: &str) -> Result<()> { )); } - // This ? converts sqlx::Error → RegistrationError::Database automatically + // Этот ? преобразует sqlx::Error → RegistrationError::Database автоматически // db.check_email_unique(email).await?; - // This is explicit construction for domain logic + // Это явное создание ошибки для доменной логики if email.contains("+spam") { return Err(RegistrationError::DuplicateEmail(email.to_string())); } @@ -263,7 +263,7 @@ pub fn register_user(email: &str, password: &str) -> Result<()> { } ``` -**Key pattern**: `#[from]` enables `?` for library errors; explicit `Err(...)` for domain logic. The Result alias keeps every signature clean. +**Ключевой паттерн**: `#[from]` включает `?` для ошибок библиотек; явный `Err(...)` — для доменной логики. Псевдоним Result делает каждую сигнатуру чистой. </details> </details> diff --git a/csharp-book/src/ch09-error-handling.md b/csharp-book/src/ch09-error-handling.md index 69426cd..b3b3e84 100644 --- a/csharp-book/src/ch09-error-handling.md +++ b/csharp-book/src/ch09-error-handling.md @@ -1,16 +1,16 @@ -## Exceptions vs `Result<T, E>` +## Исключения против `Result<T, E>` -> **What you'll learn:** Why Rust replaces exceptions with `Result<T, E>` and `Option<T>`, -> the `?` operator for concise error propagation, and how explicit error handling -> eliminates hidden control flow that plagues C# `try`/`catch` code. +> **Что вы узнаете:** почему Rust заменяет исключения на `Result<T, E>` и `Option<T>`, +> оператор `?` для краткого распространения ошибок, и как явная обработка ошибок +> устраняет скрытый поток управления, который осложняет код с `try`/`catch` в C#. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний > -> **See also**: [Crate-Level Error Types](ch09-1-crate-level-error-types-and-result-alias.md) for production error patterns with `thiserror` and `anyhow`, and [Essential Crates](ch15-1-essential-crates-for-c-developers.md) for the error crate ecosystem. +> **См. также**: [Собственные типы ошибок уровня крейта](ch09-1-crate-level-error-types-and-result-alias.md) — производственные паттерны с `thiserror` и `anyhow`, и [Основные крейты](ch15-1-essential-crates-for-c-developers.md) — экосистема крейтов для ошибок. -### C# Exception-Based Error Handling +### Обработка ошибок через исключения в C# ```csharp -// C# - Exception-based error handling +// C# — обработка ошибок через исключения public class UserService { public User GetUser(int userId) @@ -44,13 +44,13 @@ public class UserService catch (Exception ex) { logger.Error(ex, "Unexpected error getting user email"); - throw; // Re-throw + throw; // Повторно выбрасываем } } } ``` -### Rust Result-Based Error Handling +### Обработка ошибок через Result в Rust ```rust use std::fmt; @@ -82,7 +82,7 @@ pub struct User { } pub struct UserService { - users: Vec<User>, // Simulated database + users: Vec<User>, // Имитация базы данных } impl UserService { @@ -95,13 +95,13 @@ impl UserService { return Err(UserError::InvalidId(user_id)); } - // Simulate database lookup + // Имитация поиска в базе данных self.database_find_user(user_id) .ok_or(UserError::NotFound(user_id)) } pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> { - let user = self.get_user(user_id)?; // ? operator propagates errors + let user = self.get_user(user_id)?; // Оператор ? пробрасывает ошибку user.email .ok_or(UserError::NoEmail) @@ -125,13 +125,13 @@ impl UserService { ```mermaid graph TD - subgraph "C# Exception Model" - CS_CALL["Method Call"] - CS_SUCCESS["Success Path"] + subgraph "Модель исключений C#" + CS_CALL["Вызов метода"] + CS_SUCCESS["Путь успеха"] CS_EXCEPTION["throw Exception"] - CS_STACK["Stack unwinding<br/>(Runtime cost)"] - CS_CATCH["try/catch block"] - CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Performance cost<br/>[ERROR] Easy to ignore"] + CS_STACK["Раскрутка стека<br/>(затраты во время выполнения)"] + CS_CATCH["Блок try/catch"] + CS_HIDDEN["[ERROR] Скрытый поток управления<br/>[ERROR] Затраты на производительность<br/>[ERROR] Легко проигнорировать"] CS_CALL --> CS_SUCCESS CS_CALL --> CS_EXCEPTION @@ -140,13 +140,13 @@ graph TD CS_EXCEPTION --> CS_HIDDEN end - subgraph "Rust Result Model" - RUST_CALL["Function Call"] + subgraph "Модель Result в Rust" + RUST_CALL["Вызов функции"] RUST_OK["Ok(value)"] RUST_ERR["Err(error)"] RUST_MATCH["match result"] - RUST_QUESTION["? operator<br/>(early return)"] - RUST_EXPLICIT["[OK] Explicit error handling<br/>[OK] Zero runtime cost<br/>[OK] Cannot ignore errors"] + RUST_QUESTION["Оператор ?<br/>(ранний возврат)"] + RUST_EXPLICIT["[OK] Явная обработка ошибок<br/>[OK] Нулевые затраты во время выполнения<br/>[OK] Ошибки нельзя проигнорировать"] RUST_CALL --> RUST_OK RUST_CALL --> RUST_ERR @@ -165,23 +165,23 @@ graph TD *** -### The ? Operator: Propagating Errors Concisely +### Оператор ?: краткое распространение ошибок ```csharp -// C# - Exception propagation (implicit) +// C# — распространение исключений (неявное) public async Task<string> ProcessFileAsync(string path) { - var content = await File.ReadAllTextAsync(path); // Throws on error - var processed = ProcessContent(content); // Throws on error + var content = await File.ReadAllTextAsync(path); // Бросает исключение при ошибке + var processed = ProcessContent(content); // Бросает исключение при ошибке return processed; } ``` ```rust -// Rust - Error propagation with ? +// Rust — распространение ошибок через ? fn process_file(path: &str) -> Result<String, ConfigError> { - let content = read_config(path)?; // ? propagates error if Err - let processed = process_content(&content)?; // ? propagates error if Err - Ok(processed) // Wrap success value in Ok + let content = read_config(path)?; // ? пробрасывает ошибку, если это Err + let processed = process_content(&content)?; // ? пробрасывает ошибку, если это Err + Ok(processed) // Оборачиваем успешное значение в Ok } fn process_content(content: &str) -> Result<String, ConfigError> { @@ -193,13 +193,13 @@ fn process_content(content: &str) -> Result<String, ConfigError> { } ``` -### `Option<T>` for Nullable Values +### `Option<T>` для значений, которые могут отсутствовать ```csharp -// C# - Nullable reference types +// C# — ссылочные типы с поддержкой null public string? FindUserName(int userId) { var user = database.FindUser(userId); - return user?.Name; // Returns null if user not found + return user?.Name; // Возвращает null, если пользователь не найден } public void ProcessUser(int userId) @@ -217,9 +217,9 @@ public void ProcessUser(int userId) ``` ```rust -// Rust - Option<T> for optional values +// Rust — Option<T> для необязательных значений fn find_user_name(user_id: u32) -> Option<String> { - // Simulate database lookup + // Имитация поиска в базе данных if user_id == 1 { Some("Alice".to_string()) } else { @@ -233,7 +233,7 @@ fn process_user(user_id: u32) { None => println!("User not found"), } - // Or use if let (pattern matching shorthand) + // Или используйте if let (сокращённая форма сопоставления с образцом) if let Some(name) = find_user_name(user_id) { println!("User: {}", name); } else { @@ -242,7 +242,7 @@ fn process_user(user_id: u32) { } ``` -### Combining Option and Result +### Комбинирование Option и Result ```rust fn safe_divide(a: f64, b: f64) -> Option<f64> { if b != 0.0 { @@ -253,9 +253,9 @@ fn safe_divide(a: f64, b: f64) -> Option<f64> { } fn parse_and_divide(a_str: &str, b_str: &str) -> Result<Option<f64>, ParseFloatError> { - let a: f64 = a_str.parse()?; // Return parse error if invalid - let b: f64 = b_str.parse()?; // Return parse error if invalid - Ok(safe_divide(a, b)) // Return Ok(Some(result)) or Ok(None) + let a: f64 = a_str.parse()?; // Возвращаем ошибку разбора, если значение некорректно + let b: f64 = b_str.parse()?; // Возвращаем ошибку разбора, если значение некорректно + Ok(safe_divide(a, b)) // Возвращаем Ok(Some(result)) или Ok(None) } use std::num::ParseFloatError; @@ -273,23 +273,23 @@ fn main() { <details> -<summary><strong>🏋️ Exercise: Build a Crate-Level Error Type</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: создайте тип ошибок уровня крейта</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Create an `AppError` enum for a file processing application that can fail due to I/O errors, JSON parse errors, and validation errors. Implement `From` conversions for automatic `?` propagation. +**Задача**: создайте перечисление `AppError` для приложения обработки файлов, которое может завершиться ошибкой из-за ошибок ввода-вывода, ошибок разбора JSON и ошибок валидации. Реализуйте преобразования `From` для автоматического распространения через `?`. ```rust -// Starter code +// Стартовый код use std::io; -// TODO: Define AppError with variants: +// TODO: Определите AppError с вариантами: // Io(io::Error), Json(serde_json::Error), Validation(String) -// TODO: Implement Display and Error traits -// TODO: Implement From<io::Error> and From<serde_json::Error> -// TODO: Define type alias: type Result<T> = std::result::Result<T, AppError>; +// TODO: Реализуйте трейты Display и Error +// TODO: Реализуйте From<io::Error> и From<serde_json::Error> +// TODO: Определите псевдоним типа: type Result<T> = std::result::Result<T, AppError>; fn load_config(path: &str) -> Result<Config> { let content = std::fs::read_to_string(path)?; // io::Error → AppError - let config: Config = serde_json::from_str(&content)?; // serde error → AppError + let config: Config = serde_json::from_str(&content)?; // ошибка serde → AppError if config.name.is_empty() { return Err(AppError::Validation("name cannot be empty".into())); } @@ -298,7 +298,7 @@ fn load_config(path: &str) -> Result<Config> { ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::io; @@ -334,11 +334,11 @@ fn load_config(path: &str) -> Result<Config> { } ``` -**Key takeaways**: -- `thiserror` generates `Display` and `Error` impls from attributes -- `#[from]` generates `From<T>` impls, enabling automatic `?` conversion -- The `Result<T>` alias eliminates boilerplate throughout your crate -- Unlike C# exceptions, the error type is visible in every function signature +**Ключевые выводы**: +- `thiserror` генерирует реализации `Display` и `Error` на основе атрибутов +- `#[from]` генерирует реализации `From<T>`, что позволяет автоматически преобразовывать ошибки через `?` +- Псевдоним `Result<T>` убирает шаблонный код во всём крейте +- В отличие от исключений C#, тип ошибки виден в каждой сигнатуре функции </details> </details> diff --git a/csharp-book/src/ch10-1-generic-constraints.md b/csharp-book/src/ch10-1-generic-constraints.md index ab06b4a..d9af827 100644 --- a/csharp-book/src/ch10-1-generic-constraints.md +++ b/csharp-book/src/ch10-1-generic-constraints.md @@ -1,31 +1,31 @@ -## Generic Constraints: where vs trait bounds +## Ограничения обобщений: where и ограничения трейтов -> **What you'll learn:** Rust's trait bounds vs C#'s `where` constraints, the `where` clause syntax, -> conditional trait implementations, associated types, and higher-ranked trait bounds (HRTBs). +> **Что вы узнаете:** ограничения трейтов Rust в сравнении с ограничениями `where` в C#, синтаксис предложения `where`, +> условные реализации трейтов, ассоциированные типы и границы трейтов высокого ранга (HRTB). > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -### C# Generic Constraints +### Ограничения обобщений в C# ```csharp -// C# Generic constraints with where clause +// Ограничения обобщений в C# через предложение where public class Repository<T> where T : class, IEntity, new() { public T Create() { - return new T(); // new() constraint allows parameterless constructor + return new T(); // Ограничение new() позволяет конструктор без параметров } public void Save(T entity) { - if (entity.Id == 0) // IEntity constraint provides Id property + if (entity.Id == 0) // Ограничение IEntity даёт свойство Id { entity.Id = GenerateId(); } - // Save to database + // Сохранение в базу данных } } -// Multiple type parameters with constraints +// Несколько параметров типа с ограничениями public class Converter<TInput, TOutput> where TInput : IConvertible where TOutput : class, new() @@ -33,29 +33,29 @@ public class Converter<TInput, TOutput> public TOutput Convert(TInput input) { var output = new TOutput(); - // Conversion logic using IConvertible + // Логика преобразования через IConvertible return output; } } -// Variance in generics +// Вариантность в обобщениях public interface IRepository<out T> where T : IEntity { - IEnumerable<T> GetAll(); // Covariant - can return more derived types + IEnumerable<T> GetAll(); // Ковариантность — может возвращать более производные типы } public interface IWriter<in T> where T : IEntity { - void Write(T entity); // Contravariant - can accept more base types + void Write(T entity); // Контравариантность — может принимать более базовые типы } ``` -### Rust Generic Constraints with Trait Bounds +### Ограничения обобщений Rust через трейт-границы ```rust use std::fmt::{Debug, Display}; use std::clone::Clone; -// Basic trait bounds +// Базовые трейт-границы pub struct Repository<T> where T: Clone + Debug + Default, @@ -72,35 +72,35 @@ where } pub fn create(&self) -> T { - T::default() // Default trait provides default value + T::default() // Трейт Default предоставляет значение по умолчанию } pub fn add(&mut self, item: T) { - println!("Adding item: {:?}", item); // Debug trait for printing + println!("Adding item: {:?}", item); // Трейт Debug для вывода self.items.push(item); } pub fn get_all(&self) -> Vec<T> { - self.items.clone() // Clone trait for duplication + self.items.clone() // Трейт Clone для копирования } } -// Multiple trait bounds with different syntaxes +// Несколько трейт-границ с разным синтаксисом pub fn process_data<T, U>(input: T) -> U where T: Display + Clone, U: From<T> + Debug, { - println!("Processing: {}", input); // Display trait - let cloned = input.clone(); // Clone trait - let output = U::from(cloned); // From trait for conversion - println!("Result: {:?}", output); // Debug trait + println!("Processing: {}", input); // Трейт Display + let cloned = input.clone(); // Трейт Clone + let output = U::from(cloned); // Трейт From для преобразования + println!("Result: {:?}", output); // Трейт Debug output } -// Associated types (similar to C# generic constraints) +// Ассоциированные типы (аналог ограничений обобщений в C#) pub trait Iterator { - type Item; // Associated type instead of generic parameter + type Item; // Ассоциированный тип вместо параметра обобщения fn next(&mut self) -> Option<Self::Item>; } @@ -109,15 +109,15 @@ pub trait Collect<T> { fn collect<I: Iterator<Item = T>>(iter: I) -> Self; } -// Higher-ranked trait bounds (advanced) +// Границы трейтов высокого ранга (продвинутый уровень) fn apply_to_all<F>(items: &[String], f: F) -> Vec<String> where - F: for<'a> Fn(&'a str) -> String, // Function works with any lifetime + F: for<'a> Fn(&'a str) -> String, // Функция работает с любым временем жизни { items.iter().map(|s| f(s)).collect() } -// Conditional trait implementations +// Условные реализации трейтов impl<T> PartialEq for Repository<T> where T: PartialEq + Clone + Debug + Default, @@ -130,12 +130,12 @@ where ```mermaid graph TD - subgraph "C# Generic Constraints" + subgraph "Ограничения обобщений в C#" CS_WHERE["where T : class, IInterface, new()"] - CS_RUNTIME["[ERROR] Some runtime type checking<br/>Virtual method dispatch"] - CS_VARIANCE["[OK] Covariance/Contravariance<br/>in/out keywords"] - CS_REFLECTION["[ERROR] Runtime reflection possible<br/>typeof(T), is, as operators"] - CS_BOXING["[ERROR] Value type boxing<br/>for interface constraints"] + CS_RUNTIME["[ERROR] Проверка типов частично во время выполнения<br/>Диспетчеризация виртуальных методов"] + CS_VARIANCE["[OK] Ковариантность/контравариантность<br/>ключевые слова in/out"] + CS_REFLECTION["[ERROR] Возможна рефлексия во время выполнения<br/>операторы typeof(T), is, as"] + CS_BOXING["[ERROR] Упаковка значимых типов<br/>для ограничений интерфейсов"] CS_WHERE --> CS_RUNTIME CS_WHERE --> CS_VARIANCE @@ -143,12 +143,12 @@ graph TD CS_WHERE --> CS_BOXING end - subgraph "Rust Trait Bounds" + subgraph "Трейт-границы Rust" RUST_WHERE["where T: Trait + Clone + Debug"] - RUST_COMPILE["[OK] Compile-time resolution<br/>Monomorphization"] - RUST_ZERO["[OK] Zero-cost abstractions<br/>No runtime overhead"] - RUST_ASSOCIATED["[OK] Associated types<br/>More flexible than generics"] - RUST_HKT["[OK] Higher-ranked trait bounds<br/>Advanced type relationships"] + RUST_COMPILE["[OK] Разрешение на этапе компиляции<br/>Мономорфизация"] + RUST_ZERO["[OK] Абстракции с нулевой стоимостью<br/>Без накладных расходов рантайма"] + RUST_ASSOCIATED["[OK] Ассоциированные типы<br/>Гибче, чем обобщения"] + RUST_HKT["[OK] Границы трейтов высокого ранга<br/>Продвинутые связи между типами"] RUST_WHERE --> RUST_COMPILE RUST_WHERE --> RUST_ZERO @@ -156,9 +156,9 @@ graph TD RUST_WHERE --> RUST_HKT end - subgraph "Flexibility Comparison" - CS_FLEX["C# Flexibility<br/>[OK] Variance<br/>[OK] Runtime type info<br/>[ERROR] Performance cost"] - RUST_FLEX["Rust Flexibility<br/>[OK] Zero cost<br/>[OK] Compile-time safety<br/>[ERROR] No variance (yet)"] + subgraph "Сравнение гибкости" + CS_FLEX["Гибкость C#<br/>[OK] Вариантность<br/>[OK] Информация о типах во время выполнения<br/>[ERROR] Затраты на производительность"] + RUST_FLEX["Гибкость Rust<br/>[OK] Нулевая стоимость<br/>[OK] Безопасность на этапе компиляции<br/>[ERROR] Вариантности пока нет"] end style CS_RUNTIME fill:#fff3e0,color:#000 @@ -171,12 +171,12 @@ graph TD --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Generic Repository</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: обобщённый репозиторий</strong> (нажмите, чтобы раскрыть)</summary> -Translate this C# generic repository interface to Rust traits: +Переведите этот обобщённый интерфейс репозитория C# на трейты Rust: ```csharp public interface IRepository<T> where T : IEntity, new() @@ -187,14 +187,14 @@ public interface IRepository<T> where T : IEntity, new() } ``` -Requirements: -1. Define an `Entity` trait with `fn id(&self) -> u64` -2. Define a `Repository<T>` trait where `T: Entity + Clone` -3. Implement a `InMemoryRepository<T>` that stores items in a `Vec<T>` -4. The `find` method should accept `impl Fn(&T) -> bool` +Требования: +1. Определите трейт `Entity` с методом `fn id(&self) -> u64` +2. Определите трейт `Repository<T>`, где `T: Entity + Clone` +3. Реализуйте `InMemoryRepository<T>`, который хранит элементы в `Vec<T>` +4. Метод `find` должен принимать `impl Fn(&T) -> bool` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust trait Entity: Clone { @@ -248,7 +248,7 @@ fn main() { } ``` -**Key differences from C#**: No `new()` constraint (use `Default` trait instead). `Fn(&T) -> bool` replaces `Func<T, bool>`. Return `Option` instead of throwing. +**Ключевые отличия от C#**: ограничения `new()` нет (вместо него используется трейт `Default`). `Fn(&T) -> bool` заменяет `Func<T, bool>`. Вместо генерации исключения возвращается `Option`. </details> </details> diff --git a/csharp-book/src/ch10-2-inheritance-vs-composition.md b/csharp-book/src/ch10-2-inheritance-vs-composition.md index 3ab2dbe..b606714 100644 --- a/csharp-book/src/ch10-2-inheritance-vs-composition.md +++ b/csharp-book/src/ch10-2-inheritance-vs-composition.md @@ -1,12 +1,12 @@ -## Inheritance vs Composition +## Наследование против композиции -> **What you'll learn:** Why Rust has no class inheritance, how traits + structs replace deep -> class hierarchies, and practical patterns for achieving polymorphism through composition. +> **Что вы узнаете:** почему в Rust нет наследования классов, как трейты и структуры заменяют глубокие иерархии классов, +> и практические паттерны для достижения полиморфизма через композицию. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний ```csharp -// C# - Class-based inheritance +// C# — наследование на основе классов public abstract class Animal { public string Name { get; protected set; } @@ -33,7 +33,7 @@ public class Dog : Animal } } -// Interface-based contracts +// Контракты на основе интерфейсов public interface IFlyable { void Fly(); @@ -55,14 +55,14 @@ public class Bird : Animal, IFlyable } ``` -### Rust Composition Model +### Модель композиции в Rust ```rust -// Rust - Composition over inheritance with traits +// Rust — композиция вместо наследования, через трейты pub trait Animal { fn name(&self) -> &str; fn make_sound(&self); - // Default implementation (like C# virtual methods) + // Реализация по умолчанию (как виртуальные методы в C#) fn sleep(&self) { println!("{} is sleeping", self.name()); } @@ -72,7 +72,7 @@ pub trait Flyable { fn fly(&self); } -// Separate data from behavior +// Отделяем данные от поведения #[derive(Debug)] pub struct Dog { name: String, @@ -84,7 +84,7 @@ pub struct Bird { wingspan: f64, } -// Implement behaviors for types +// Реализуем поведение для типов impl Animal for Dog { fn name(&self) -> &str { &self.name @@ -121,7 +121,7 @@ impl Flyable for Bird { } } -// Multiple trait bounds (like multiple interfaces) +// Несколько трейт-границ (как несколько интерфейсов) fn make_flying_animal_sound<T>(animal: &T) where T: Animal + Flyable, @@ -133,12 +133,12 @@ where ```mermaid graph TD - subgraph "C# Inheritance Hierarchy" - CS_ANIMAL["Animal (abstract class)"] + subgraph "Иерархия наследования C#" + CS_ANIMAL["Animal (абстрактный класс)"] CS_DOG["Dog : Animal"] CS_BIRD["Bird : Animal, IFlyable"] - CS_VTABLE["Virtual method dispatch<br/>Runtime cost"] - CS_COUPLING["[ERROR] Tight coupling<br/>[ERROR] Diamond problem<br/>[ERROR] Deep hierarchies"] + CS_VTABLE["Диспетчеризация виртуальных методов<br/>Затраты во время выполнения"] + CS_COUPLING["[ERROR] Жёсткая связанность<br/>[ERROR] Ромбовидная проблема<br/>[ERROR] Глубокие иерархии"] CS_ANIMAL --> CS_DOG CS_ANIMAL --> CS_BIRD @@ -147,7 +147,7 @@ graph TD CS_ANIMAL --> CS_COUPLING end - subgraph "Rust Composition Model" + subgraph "Модель композиции Rust" RUST_ANIMAL["trait Animal"] RUST_FLYABLE["trait Flyable"] RUST_DOG["struct Dog"] @@ -155,8 +155,8 @@ graph TD RUST_IMPL1["impl Animal for Dog"] RUST_IMPL2["impl Animal for Bird"] RUST_IMPL3["impl Flyable for Bird"] - RUST_STATIC["Static dispatch<br/>Zero cost"] - RUST_FLEXIBLE["[OK] Flexible composition<br/>[OK] No hierarchy limits<br/>[OK] Mix and match traits"] + RUST_STATIC["Статическая диспетчеризация<br/>Нулевая стоимость"] + RUST_FLEXIBLE["[OK] Гибкая композиция<br/>[OK] Нет ограничений иерархии<br/>[OK] Свободное комбинирование трейтов"] RUST_DOG --> RUST_IMPL1 RUST_BIRD --> RUST_IMPL2 @@ -179,12 +179,12 @@ graph TD --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Replace Inheritance with Traits</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: замените наследование трейтами</strong> (нажмите, чтобы раскрыть)</summary> -This C# code uses inheritance. Rewrite it in Rust using trait composition: +Этот C#-код использует наследование. Перепишите его на Rust, используя композицию трейтов: ```csharp public abstract class Shape { public abstract double Area(); } @@ -199,14 +199,14 @@ public class Cylinder : Shape3D } ``` -Requirements: -1. `HasArea` trait with `fn area(&self) -> f64` -2. `HasVolume` trait with `fn volume(&self) -> f64` -3. `Cylinder` struct implementing both -4. A function `fn print_shape_info(shape: &(impl HasArea + HasVolume))` — note the trait bound composition (no inheritance needed) +Требования: +1. Трейт `HasArea` с методом `fn area(&self) -> f64` +2. Трейт `HasVolume` с методом `fn volume(&self) -> f64` +3. Структура `Cylinder`, реализующая оба трейта +4. Функция `fn print_shape_info(shape: &(impl HasArea + HasVolume))` — обратите внимание на композицию трейт-границ (наследование не нужно) <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::f64::consts::PI; @@ -247,7 +247,7 @@ fn main() { } ``` -**Key insight**: C# needs a 3-level hierarchy (Shape → Shape3D → Cylinder). Rust uses flat trait composition — `impl HasArea + HasVolume` combines capabilities without inheritance depth. +**Ключевая мысль**: C# требует трёхуровневой иерархии (Shape → Shape3D → Cylinder). Rust использует плоскую композицию трейтов — `impl HasArea + HasVolume` объединяет возможности без глубины наследования. </details> </details> diff --git a/csharp-book/src/ch10-traits-and-generics.md b/csharp-book/src/ch10-traits-and-generics.md index 15b0251..882c65d 100644 --- a/csharp-book/src/ch10-traits-and-generics.md +++ b/csharp-book/src/ch10-traits-and-generics.md @@ -1,29 +1,29 @@ -## Traits - Rust's Interfaces +## Трейты — интерфейсы Rust -> **What you'll learn:** Traits vs C# interfaces, default method implementations, trait objects (`dyn Trait`) -> vs generic bounds (`impl Trait`), derived traits, common standard library traits, associated types, -> and operator overloading via traits. +> **Что вы узнаете:** трейты против интерфейсов C#, реализации методов по умолчанию, трейт-объекты (`dyn Trait`) +> против ограничений обобщений (`impl Trait`), производные трейты, распространённые трейты стандартной библиотеки, +> ассоциированные типы и перегрузку операторов через трейты. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -Traits are Rust's way of defining shared behavior, similar to interfaces in C# but more powerful. +Трейты — способ Rust описывать общее поведение. Они похожи на интерфейсы C#, но мощнее. -### C# Interface Comparison +### Сравнение с интерфейсами C# ```csharp -// C# interface definition +// Определение интерфейса в C# public interface IAnimal { string Name { get; } void MakeSound(); - // Default implementation (C# 8+) + // Реализация по умолчанию (C# 8+) string Describe() { return $"{Name} makes a sound"; } } -// C# interface implementation +// Реализация интерфейса в C# public class Dog : IAnimal { public string Name { get; } @@ -38,14 +38,14 @@ public class Dog : IAnimal Console.WriteLine("Woof!"); } - // Can override default implementation + // Можно переопределить реализацию по умолчанию public string Describe() { return $"{Name} is a loyal dog"; } } -// Generic constraints +// Ограничения обобщений public void ProcessAnimal<T>(T animal) where T : IAnimal { animal.MakeSound(); @@ -53,26 +53,26 @@ public void ProcessAnimal<T>(T animal) where T : IAnimal } ``` -### Rust Trait Definition and Implementation +### Определение и реализация трейтов в Rust ```rust -// Trait definition +// Определение трейта trait Animal { fn name(&self) -> &str; fn make_sound(&self); - // Default implementation + // Реализация по умолчанию fn describe(&self) -> String { format!("{} makes a sound", self.name()) } - // Default implementation using other trait methods + // Реализация по умолчанию, использующая другие методы трейта fn introduce(&self) { println!("Hi, I'm {}", self.name()); self.make_sound(); } } -// Struct definition +// Определение структуры #[derive(Debug)] struct Dog { name: String, @@ -85,7 +85,7 @@ impl Dog { } } -// Trait implementation +// Реализация трейта impl Animal for Dog { fn name(&self) -> &str { &self.name @@ -95,13 +95,13 @@ impl Animal for Dog { println!("Woof!"); } - // Override default implementation + // Переопределяем реализацию по умолчанию fn describe(&self) -> String { format!("{} is a loyal {} dog", self.name, self.breed) } } -// Another implementation +// Ещё одна реализация #[derive(Debug)] struct Cat { name: String, @@ -117,17 +117,17 @@ impl Animal for Cat { println!("Meow!"); } - // Use default describe() implementation + // Используем describe() по умолчанию } -// Generic function with trait bounds +// Обобщённая функция с ограничениями трейтов fn process_animal<T: Animal>(animal: &T) { animal.make_sound(); println!("{}", animal.describe()); animal.introduce(); } -// Multiple trait bounds +// Несколько ограничений трейтов fn process_animal_debug<T: Animal + std::fmt::Debug>(animal: &T) { println!("Debug: {:?}", animal); process_animal(animal); @@ -144,19 +144,19 @@ fn main() { } ``` -### Trait Objects and Dynamic Dispatch +### Трейт-объекты и динамическая диспетчеризация ```csharp -// C# dynamic polymorphism +// Динамический полиморфизм в C# public void ProcessAnimals(List<IAnimal> animals) { foreach (var animal in animals) { - animal.MakeSound(); // Dynamic dispatch + animal.MakeSound(); // Динамическая диспетчеризация Console.WriteLine(animal.Describe()); } } -// Usage +// Использование var animals = new List<IAnimal> { new Dog("Buddy"), @@ -168,15 +168,15 @@ ProcessAnimals(animals); ``` ```rust -// Rust trait objects for dynamic dispatch +// Трейт-объекты Rust для динамической диспетчеризации fn process_animals(animals: &[Box<dyn Animal>]) { for animal in animals { - animal.make_sound(); // Dynamic dispatch + animal.make_sound(); // Динамическая диспетчеризация println!("{}", animal.describe()); } } -// Alternative: using references +// Альтернатива: использование ссылок fn process_animal_refs(animals: &[&dyn Animal]) { for animal in animals { animal.make_sound(); @@ -185,7 +185,7 @@ fn process_animal_refs(animals: &[&dyn Animal]) { } fn main() { - // Using Box<dyn Trait> + // Использование Box<dyn Trait> let animals: Vec<Box<dyn Animal>> = vec![ Box::new(Dog::new("Buddy".to_string(), "Golden Retriever".to_string())), Box::new(Cat { name: "Whiskers".to_string(), indoor: true }), @@ -194,7 +194,7 @@ fn main() { process_animals(&animals); - // Using references + // Использование ссылок let dog = Dog::new("Buddy".to_string(), "Golden Retriever".to_string()); let cat = Cat { name: "Whiskers".to_string(), indoor: true }; @@ -203,16 +203,16 @@ fn main() { } ``` -### Derived Traits +### Производные трейты ```rust -// Automatically derive common traits +// Автоматически выводим распространённые трейты #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct Person { name: String, age: u32, } -// What this generates (simplified): +// Что генерируется (упрощённо): impl std::fmt::Debug for Person { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Person") @@ -237,45 +237,45 @@ impl PartialEq for Person { } } -// Usage +// Использование fn main() { let person1 = Person { name: "Alice".to_string(), age: 30, }; - let person2 = person1.clone(); // Clone trait + let person2 = person1.clone(); // Трейт Clone - println!("{:?}", person1); // Debug trait - println!("Equal: {}", person1 == person2); // PartialEq trait + println!("{:?}", person1); // Трейт Debug + println!("Equal: {}", person1 == person2); // Трейт PartialEq } ``` -### Common Standard Library Traits +### Распространённые трейты стандартной библиотеки ```rust use std::collections::HashMap; -// Display trait for user-friendly output +// Трейт Display для удобного для пользователя вывода impl std::fmt::Display for Person { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} (age {})", self.name, self.age) } } -// From trait for conversions +// Трейт From для преобразований impl From<(String, u32)> for Person { fn from((name, age): (String, u32)) -> Self { Person { name, age } } } -// Into trait is automatically implemented when From is implemented +// Трейт Into реализуется автоматически, когда реализован From fn create_person() { let person: Person = ("Alice".to_string(), 30).into(); println!("{}", person); } -// Iterator trait implementation +// Реализация трейта Iterator struct PersonIterator { people: Vec<Person>, index: usize, @@ -308,9 +308,9 @@ fn main() { Person::from(("Charlie".to_string(), 35)), ]; - // Use our custom iterator + // Используем наш собственный итератор for person in Person::iterator(people.clone()) { - println!("{}", person); // Uses Display trait + println!("{}", person); // Использует трейт Display } } ``` @@ -319,12 +319,12 @@ fn main() { <details> -<summary><strong>🏋️ Exercise: Trait-Based Drawing System</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: система рисования на основе трейтов</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Implement a `Drawable` trait with an `area()` method and a `draw()` default method. Create `Circle` and `Rect` structs. Write a function that accepts `&[Box<dyn Drawable>]` and prints total area. +**Задача**: реализуйте трейт `Drawable` с методом `area()` и методом `draw()` по умолчанию. Создайте структуры `Circle` и `Rect`. Напишите функцию, которая принимает `&[Box<dyn Drawable>]` и выводит суммарную площадь. <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::f64::consts::PI; @@ -363,29 +363,29 @@ fn main() { } ``` -**Key takeaways**: -- `dyn Trait` gives runtime polymorphism (like C# `IDrawable`) -- `Box<dyn Trait>` is heap-allocated, needed for heterogeneous collections -- Default methods work exactly like C# 8+ default interface methods +**Ключевые выводы**: +- `dyn Trait` даёт полиморфизм времени выполнения (как `IDrawable` в C#) +- `Box<dyn Trait>` размещается в куче и нужен для коллекций разнородных типов +- Методы по умолчанию работают точно так же, как методы интерфейсов по умолчанию в C# 8+ </details> </details> -### Associated Types: Traits With Type Members +### Ассоциированные типы: трейты с типовыми членами -C# interfaces don't have associated types — Rust traits do. This is how `Iterator` works: +Интерфейсы C# не имеют ассоциированных типов — трейты Rust имеют. Так устроен `Iterator`: ```rust -// The Iterator trait has an associated type 'Item' +// Трейт Iterator имеет ассоциированный тип 'Item' trait Iterator { - type Item; // Each implementor defines what Item is + type Item; // Каждая реализация определяет, что такое Item fn next(&mut self) -> Option<Self::Item>; } struct Counter { max: u32, current: u32 } impl Iterator for Counter { - type Item = u32; // This Counter yields u32 values + type Item = u32; // Этот Counter выдаёт значения u32 fn next(&mut self) -> Option<u32> { if self.current < self.max { self.current += 1; @@ -397,11 +397,11 @@ impl Iterator for Counter { } ``` -In C#, `IEnumerator<T>` uses a generic parameter (`T`) for this purpose. Rust's associated types are different: `Iterator` has *one* `Item` type per implementation, not a generic parameter at the trait level. This makes trait bounds simpler: `impl Iterator<Item = u32>` vs C#'s `IEnumerable<int>`. +В C# для этой цели `IEnumerator<T>` использует параметр обобщения (`T`). Ассоциированные типы Rust устроены иначе: у `Iterator` *один* тип `Item` на каждую реализацию, а не параметр обобщения на уровне трейта. Это упрощает ограничения трейтов: `impl Iterator<Item = u32>` против `IEnumerable<int>` в C#. -### Operator Overloading via Traits +### Перегрузка операторов через трейты -In C#, you define `public static MyType operator+(MyType a, MyType b)`. In Rust, every operator maps to a trait in `std::ops`: +В C# вы определяете `public static MyType operator+(MyType a, MyType b)`. В Rust каждый оператор соответствует трейту из `std::ops`: ```rust use std::ops::Add; @@ -418,81 +418,81 @@ impl Add for Vec2 { let a = Vec2 { x: 1.0, y: 2.0 }; let b = Vec2 { x: 3.0, y: 4.0 }; -let c = a + b; // calls <Vec2 as Add>::add(a, b) +let c = a + b; // вызывает <Vec2 as Add>::add(a, b) ``` -| C# | Rust | Notes | +| C# | Rust | Примечания | |----|------|-------| -| `operator+` | `impl Add` | `self` by value — consumes for non-`Copy` types | -| `operator==` | `impl PartialEq` | Usually `#[derive(PartialEq)]` | -| `operator<` | `impl PartialOrd` | Usually `#[derive(PartialOrd)]` | -| `ToString()` | `impl fmt::Display` | Used by `println!("{}", x)` | -| Implicit conversion | No equivalent | Rust has no implicit conversions — use `From`/`Into` | +| `operator+` | `impl Add` | `self` передаётся по значению — для типов, не реализующих `Copy`, это потребление | +| `operator==` | `impl PartialEq` | Обычно `#[derive(PartialEq)]` | +| `operator<` | `impl PartialOrd` | Обычно `#[derive(PartialOrd)]` | +| `ToString()` | `impl fmt::Display` | Используется в `println!("{}", x)` | +| Неявное преобразование | Аналога нет | В Rust нет неявных преобразований — используйте `From`/`Into` | -### Coherence: The Orphan Rule +### Согласованность: правило сироты -You can only implement a trait if you own either the trait or the type. This prevents conflicting implementations across crates: +Трейт можно реализовать, только если вы владеете либо трейтом, либо типом. Это предотвращает конфликтующие реализации между крейтами: ```rust -// ✅ OK — you own MyType +// ✅ OK — вы владеете MyType impl Display for MyType { ... } -// ✅ OK — you own MyTrait +// ✅ OK — вы владеете MyTrait impl MyTrait for String { ... } -// ❌ ERROR — you own neither Display nor String +// ❌ ОШИБКА — вы не владеете ни Display, ни String impl Display for String { ... } ``` -C# has no equivalent restriction — any code can add extension methods to any type, which can lead to ambiguity. +В C# такого ограничения нет — любой код может добавить методы расширения к любому типу, что может привести к неоднозначности. <!-- ch10.0a: impl Trait and Dispatch Strategies --> -## `impl Trait`: Returning Traits Without Boxing +## `impl Trait`: возврат трейтов без упаковки в Box -C# interfaces can always be used as return types. In Rust, returning a trait requires a decision: static dispatch (`impl Trait`) or dynamic dispatch (`dyn Trait`). +Интерфейсы C# всегда можно использовать как возвращаемые типы. В Rust возврат трейта требует выбора: статическая диспетчеризация (`impl Trait`) или динамическая (`dyn Trait`). -### `impl Trait` in Argument Position (Shorthand for Generics) +### `impl Trait` в позиции аргумента (сокращённая запись обобщения) ```rust -// These two are equivalent: +// Эти две записи эквивалентны: fn print_animal(animal: &impl Animal) { animal.make_sound(); } fn print_animal<T: Animal>(animal: &T) { animal.make_sound(); } -// impl Trait is just syntactic sugar for a generic parameter -// The compiler generates a specialized copy for each concrete type (monomorphization) +// impl Trait — это просто синтаксический сахар для параметра обобщения +// Компилятор генерирует специализированную копию для каждого конкретного типа (мономорфизация) ``` -### `impl Trait` in Return Position (The Key Difference) +### `impl Trait` в позиции возвращаемого значения (ключевое отличие) ```rust -// Return an iterator without exposing the concrete type +// Возвращаем итератор, не раскрывая конкретный тип fn even_squares(limit: u32) -> impl Iterator<Item = u32> { (0..limit) .filter(|n| n % 2 == 0) .map(|n| n * n) } -// The caller sees "some type that implements Iterator<Item = u32>" -// The actual type (Filter<Map<Range<u32>, ...>>) is unnameable — impl Trait solves this. +// Вызывающий код видит «какой-то тип, реализующий Iterator<Item = u32>» +// Фактический тип (Filter<Map<Range<u32>, ...>>) не имеет имени — impl Trait решает эту проблему fn main() { for n in even_squares(20) { print!("{n} "); } - // Output: 0 4 16 36 64 100 144 196 256 324 + // Вывод: 0 4 16 36 64 100 144 196 256 324 } ``` ```csharp -// C# — returning an interface (always dynamic dispatch, heap-allocated iterator object) +// C# — возврат интерфейса (всегда динамическая диспетчеризация, объект итератора в куче) public IEnumerable<int> EvenSquares(int limit) => Enumerable.Range(0, limit) .Where(n => n % 2 == 0) .Select(n => n * n); -// The return type hides the concrete iterator behind the IEnumerable interface -// Unlike Rust's Box<dyn Trait>, C# doesn't explicitly box — the runtime handles allocation +// Тип возвращаемого значения скрывает конкретный итератор за интерфейсом IEnumerable +// В отличие от Box<dyn Trait> в Rust, C# не упаковывает явно — выделением памяти занимается рантайм ``` -### Returning Closures: `impl Fn` vs `Box<dyn Fn>` +### Возврат замыканий: `impl Fn` против `Box<dyn Fn>` ```rust -// Return a closure — you CANNOT name the closure type, so impl Fn is essential +// Возвращаем замыкание — тип замыкания назвать нельзя, поэтому impl Fn необходим fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y } @@ -500,7 +500,7 @@ fn make_adder(x: i32) -> impl Fn(i32) -> i32 { let add5 = make_adder(5); println!("{}", add5(3)); // 8 -// If you need to return DIFFERENT closures conditionally, you need Box: +// Если нужно условно возвращать РАЗНЫЕ замыкания, понадобится Box: fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> { if add { Box::new(|a, b| a + b) @@ -508,43 +508,43 @@ fn choose_op(add: bool) -> Box<dyn Fn(i32, i32) -> i32> { Box::new(|a, b| a * b) } } -// impl Trait requires a SINGLE concrete type; different closures are different types +// impl Trait требует ОДНОГО конкретного типа; разные замыкания — это разные типы ``` ```csharp -// C# — delegates handle this naturally (always heap-allocated) +// C# — делегаты решают это естественно (всегда выделяются в куче) Func<int, int> MakeAdder(int x) => y => x + y; Func<int, int, int> ChooseOp(bool add) => add ? (a, b) => a + b : (a, b) => a * b; ``` -### The Dispatch Decision: `impl Trait` vs `dyn Trait` vs Generics +### Решение о диспетчеризации: `impl Trait` против `dyn Trait` против обобщений -This is an architectural decision C# developers face immediately in Rust. Here's the complete guide: +С этим архитектурным решением разработчики C# сталкиваются в Rust сразу. Вот полное руководство: ```mermaid graph TD - START["Function accepts or returns<br/>a trait-based type?"] - POSITION["Argument or return position?"] - ARG_SAME["All callers pass<br/>the same type?"] - RET_SINGLE["Always returns the<br/>same concrete type?"] - COLLECTION["Storing in a collection<br/>or as struct field?"] - - GENERIC["Use generics<br/><code>fn foo<T: Trait>(x: T)</code>"] - IMPL_ARG["Use impl Trait<br/><code>fn foo(x: impl Trait)</code>"] - IMPL_RET["Use impl Trait<br/><code>fn foo() -> impl Trait</code>"] - DYN_BOX["Use Box<dyn Trait><br/>Dynamic dispatch"] - DYN_REF["Use &dyn Trait<br/>Borrowed dynamic dispatch"] + START["Функция принимает или возвращает<br/>тип на основе трейта?"] + POSITION["Позиция аргумента или возврата?"] + ARG_SAME["Все вызывающие передают<br/>один и тот же тип?"] + RET_SINGLE["Всегда возвращается один и тот же<br/>конкретный тип?"] + COLLECTION["Хранится в коллекции<br/>или как поле структуры?"] + + GENERIC["Используйте обобщения<br/><code>fn foo<T: Trait>(x: T)</code>"] + IMPL_ARG["Используйте impl Trait<br/><code>fn foo(x: impl Trait)</code>"] + IMPL_RET["Используйте impl Trait<br/><code>fn foo() -> impl Trait</code>"] + DYN_BOX["Используйте Box<dyn Trait><br/>Динамическая диспетчеризация"] + DYN_REF["Используйте &dyn Trait<br/>Динамическая диспетчеризация по ссылке"] START --> POSITION - POSITION -->|Argument| ARG_SAME - POSITION -->|Return| RET_SINGLE - ARG_SAME -->|"Yes (syntactic sugar)"| IMPL_ARG - ARG_SAME -->|"Complex bounds/multiple uses"| GENERIC - RET_SINGLE -->|Yes| IMPL_RET - RET_SINGLE -->|"No (conditional types)"| DYN_BOX - RET_SINGLE -->|"Heterogeneous collection"| COLLECTION - COLLECTION -->|Owned| DYN_BOX - COLLECTION -->|Borrowed| DYN_REF + POSITION -->|Аргумент| ARG_SAME + POSITION -->|Возврат| RET_SINGLE + ARG_SAME -->|"Да (синтаксический сахар)"| IMPL_ARG + ARG_SAME -->|"Сложные ограничения/несколько использований"| GENERIC + RET_SINGLE -->|Да| IMPL_RET + RET_SINGLE -->|"Нет (условные типы)"| DYN_BOX + RET_SINGLE -->|"Коллекция разнородных типов"| COLLECTION + COLLECTION -->|Владеемая| DYN_BOX + COLLECTION -->|Заимствованная| DYN_REF style GENERIC fill:#c8e6c9,color:#000 style IMPL_ARG fill:#c8e6c9,color:#000 @@ -553,20 +553,20 @@ graph TD style DYN_REF fill:#fff3e0,color:#000 ``` -| Approach | Dispatch | Allocation | When to Use | +| Подход | Диспетчеризация | Выделение памяти | Когда использовать | |----------|----------|------------|-------------| -| `fn foo<T: Trait>(x: T)` | Static (monomorphized) | Stack | Multiple trait bounds, turbofish needed, same type reused | -| `fn foo(x: impl Trait)` | Static (monomorphized) | Stack | Simple bounds, cleaner syntax, one-off parameters | -| `fn foo() -> impl Trait` | Static | Stack | Single concrete return type, iterators, closures | -| `fn foo() -> Box<dyn Trait>` | Dynamic (vtable) | **Heap** | Different return types, trait objects in collections | -| `&dyn Trait` / `&mut dyn Trait` | Dynamic (vtable) | No alloc | Borrowed heterogeneous references, function parameters | +| `fn foo<T: Trait>(x: T)` | Статическая (мономорфизация) | Стек | Несколько ограничений трейтов, нужен turbofish, один и тот же тип используется повторно | +| `fn foo(x: impl Trait)` | Статическая (мономорфизация) | Стек | Простые ограничения, более чистый синтаксис, разовые параметры | +| `fn foo() -> impl Trait` | Статическая | Стек | Один конкретный тип возвращаемого значения, итераторы, замыкания | +| `fn foo() -> Box<dyn Trait>` | Динамическая (таблица виртуальных методов) | **Куча** | Разные типы возвращаемых значений, трейт-объекты в коллекциях | +| `&dyn Trait` / `&mut dyn Trait` | Динамическая (таблица виртуальных методов) | Без выделения | Заимствованные ссылки на разнородные типы, параметры функций | ```rust -// Summary: from fastest to most flexible -fn static_dispatch(x: impl Display) { /* fastest, no alloc */ } -fn generic_dispatch<T: Display + Clone>(x: T) { /* fastest, multiple bounds */ } -fn dynamic_dispatch(x: &dyn Display) { /* vtable lookup, no alloc */ } -fn boxed_dispatch(x: Box<dyn Display>) { /* vtable lookup + heap alloc */ } +// Сводка: от самого быстрого к самому гибкому +fn static_dispatch(x: impl Display) { /* быстрее всего, без выделения */ } +fn generic_dispatch<T: Display + Clone>(x: T) { /* быстрее всего, несколько ограничений */ } +fn dynamic_dispatch(x: &dyn Display) { /* поиск в таблице виртуальных методов, без выделения */ } +fn boxed_dispatch(x: Box<dyn Display>) { /* поиск в таблице виртуальных методов + выделение в куче */ } ``` *** diff --git a/csharp-book/src/ch11-from-and-into-traits.md b/csharp-book/src/ch11-from-and-into-traits.md index 13ac5b6..faf1c6a 100644 --- a/csharp-book/src/ch11-from-and-into-traits.md +++ b/csharp-book/src/ch11-from-and-into-traits.md @@ -1,33 +1,33 @@ -## Type Conversions in Rust +## Преобразования типов в Rust -> **What you'll learn:** `From`/`Into` traits vs C#'s implicit/explicit operators, `TryFrom`/`TryInto` -> for fallible conversions, `FromStr` for parsing, and idiomatic string conversion patterns. +> **Что вы узнаете:** трейты `From`/`Into` против неявных и явных операторов C#, `TryFrom`/`TryInto` +> для преобразований, которые могут завершиться ошибкой, `FromStr` для разбора строк, а также идиоматичные паттерны преобразования строк. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -C# uses implicit/explicit conversions and casting operators. Rust uses the `From` and `Into` traits for safe, explicit conversions. +В C# используются неявные и явные преобразования и операторы приведения. В Rust для безопасных и явных преобразований применяются трейты `From` и `Into`. -### C# Conversion Patterns +### Паттерны преобразований в C# ```csharp -// C# implicit/explicit conversions +// Неявные и явные преобразования в C# public class Temperature { public double Celsius { get; } public Temperature(double celsius) { Celsius = celsius; } - // Implicit conversion + // Неявное преобразование public static implicit operator double(Temperature t) => t.Celsius; - // Explicit conversion + // Явное преобразование public static explicit operator Temperature(double d) => new Temperature(d); } -double temp = new Temperature(100.0); // implicit -Temperature t = (Temperature)37.5; // explicit +double temp = new Temperature(100.0); // неявно +Temperature t = (Temperature)37.5; // явно ``` -### Rust From and Into +### From и Into в Rust ```rust #[derive(Debug)] struct Temperature { @@ -50,10 +50,10 @@ fn main() { // From let temp = Temperature::from(100.0); - // Into (automatically available when From is implemented) + // Into (доступен автоматически, когда реализован From) let temp2: Temperature = 37.5.into(); - // Works in function arguments too + // Работает и в аргументах функций fn process_temp(temp: impl Into<Temperature>) { let t: Temperature = temp.into(); println!("Temperature: {:.1}°C", t.celsius); @@ -66,18 +66,18 @@ fn main() { ```mermaid graph LR - A["impl From<f64> for Temperature"] -->|"auto-generates"| B["impl Into<Temperature> for f64"] - C["Temperature::from(37.5)"] -->|"explicit"| D["Temperature"] - E["37.5.into()"] -->|"implicit via Into"| D - F["fn process(t: impl Into<Temperature>)"] -->|"accepts both"| D + A["impl From<f64> for Temperature"] -->|"генерирует автоматически"| B["impl Into<Temperature> for f64"] + C["Temperature::from(37.5)"] -->|"явно"| D["Temperature"] + E["37.5.into()"] -->|"неявно через Into"| D + F["fn process(t: impl Into<Temperature>)"] -->|"принимает оба варианта"| D style A fill:#c8e6c9,color:#000 style B fill:#bbdefb,color:#000 ``` -> **Rule of thumb**: Implement `From`, and you get `Into` for free. Callers can use whichever reads better. +> **Эмпирическое правило**: реализуйте `From` — и `Into` получите бесплатно. Вызывающий код может использовать тот вариант, который читается лучше. -### TryFrom for Fallible Conversions +### TryFrom для преобразований, которые могут завершиться ошибкой ```rust use std::convert::TryFrom; @@ -101,19 +101,19 @@ fn main() { } ``` -### String Conversions +### Преобразования строк ```rust -// ToString via Display trait +// ToString через трейт Display impl std::fmt::Display for Temperature { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:.1}°C", self.celsius) } } -// Now .to_string() works automatically +// Теперь .to_string() работает автоматически let s = Temperature::from(100.0).to_string(); // "100.0°C" -// FromStr for parsing +// FromStr для разбора use std::str::FromStr; impl FromStr for Temperature { @@ -131,22 +131,22 @@ let t: Temperature = "100.0°C".parse().unwrap(); --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Currency Converter</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: конвертер валют</strong> (нажмите, чтобы раскрыть)</summary> -Create a `Money` struct that demonstrates the full conversion ecosystem: +Создайте структуру `Money`, которая демонстрирует всю экосистему преобразований: -1. `Money { cents: i64 }` (stores value in cents to avoid floating-point issues) -2. Implement `From<i64>` (treats input as whole dollars → `cents = dollars * 100`) -3. Implement `TryFrom<f64>` — reject negative amounts, round to nearest cent -4. Implement `Display` to show `"$1.50"` format -5. Implement `FromStr` to parse `"$1.50"` or `"1.50"` back into `Money` -6. Write a function `fn total(items: &[impl Into<Money> + Copy]) -> Money` that sums values +1. `Money { cents: i64 }` (хранит значение в центах, чтобы избежать проблем с плавающей точкой) +2. Реализуйте `From<i64>` (входное значение — целые доллары → `cents = dollars * 100`) +3. Реализуйте `TryFrom<f64>` — отвергайте отрицательные суммы, округляйте до ближайшего цента +4. Реализуйте `Display`, чтобы выводить формат `"$1.50"` +5. Реализуйте `FromStr`, чтобы разбирать `"$1.50"` или `"1.50"` обратно в `Money` +6. Напишите функцию `fn total(items: &[impl Into<Money> + Copy]) -> Money`, которая суммирует значения <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::fmt; diff --git a/csharp-book/src/ch12-1-macros-primer.md b/csharp-book/src/ch12-1-macros-primer.md index e25218e..50ef8f6 100644 --- a/csharp-book/src/ch12-1-macros-primer.md +++ b/csharp-book/src/ch12-1-macros-primer.md @@ -1,63 +1,63 @@ -## Macros: Code That Writes Code +## Макросы: код, который пишет код -> **What you'll learn:** Why Rust needs macros (no overloading, no variadic args), `macro_rules!` basics, -> the `!` suffix convention, common derive macros, and `dbg!()` for quick debugging. +> **Что вы узнаете:** зачем Rust нужны макросы (нет перегрузки и вариативных аргументов), основы `macro_rules!`, +> соглашение о суффиксе `!`, распространённые производные макросы и `dbg!()` для быстрой отладки. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -C# has no direct equivalent to Rust macros. Understanding why they exist and how they work removes a major source of confusion for C# developers. +В C# нет прямого аналога макросам Rust. Понимание того, зачем они нужны и как работают, устраняет крупный источник путаницы для разработчиков C#. -### Why Macros Exist in Rust +### Зачем в Rust нужны макросы ```mermaid graph LR - SRC["vec![1, 2, 3]"] -->|"compile time"| EXP["{ + SRC["vec![1, 2, 3]"] -->|"на этапе компиляции"| EXP["{ let mut v = Vec::new(); v.push(1); v.push(2); v.push(3); v }"] - EXP -->|"compiles to"| BIN["machine code"] + EXP -->|"компилируется в"| BIN["машинный код"] style SRC fill:#fff9c4,color:#000 style EXP fill:#c8e6c9,color:#000 ``` ```csharp -// C# has features that make macros unnecessary: -Console.WriteLine("Hello"); // Method overloading (1-16 params) -Console.WriteLine("{0}, {1}", a, b); // Variadic via params array -var list = new List<int> { 1, 2, 3 }; // Collection initializer syntax +// В C# есть возможности, которые делают макросы ненужными: +Console.WriteLine("Hello"); // Перегрузка методов (1–16 параметров) +Console.WriteLine("{0}, {1}", a, b); // Вариативность через массив params +var list = new List<int> { 1, 2, 3 }; // Синтаксис инициализатора коллекции ``` ```rust -// Rust has NO function overloading, NO variadic arguments, NO special syntax. -// Macros fill these gaps: -println!("Hello"); // Macro — handles 0+ args at compile time -println!("{}, {}", a, b); // Macro — type-checked at compile time -let list = vec![1, 2, 3]; // Macro — expands to Vec::new() + push() +// В Rust НЕТ перегрузки функций, НЕТ вариативных аргументов, НЕТ особого синтаксиса. +// Макросы закрывают эти пробелы: +println!("Hello"); // Макрос — принимает 0 и более аргументов на этапе компиляции +println!("{}, {}", a, b); // Макрос — типы проверяются на этапе компиляции +let list = vec![1, 2, 3]; // Макрос — раскрывается в Vec::new() + push() ``` -### Recognizing Macros: The `!` Suffix +### Распознавание макросов: суффикс `!` -Every macro invocation ends with `!`. If you see `!`, it's a macro, not a function: +Каждый вызов макроса заканчивается на `!`. Если вы видите `!`, это макрос, а не функция: ```rust -println!("hello"); // macro — generates format string code at compile time -format!("{x}"); // macro — returns String, compile-time format checking -vec![1, 2, 3]; // macro — creates and populates a Vec -todo!(); // macro — panics with "not yet implemented" -dbg!(expression); // macro — prints file:line + expression + value, returns value -assert_eq!(a, b); // macro — panics with diff if a ≠ b -cfg!(target_os = "linux"); // macro — compile-time platform detection +println!("hello"); // макрос — генерирует код форматирования на этапе компиляции +format!("{x}"); // макрос — возвращает String, проверка формата на этапе компиляции +vec![1, 2, 3]; // макрос — создаёт и заполняет Vec +todo!(); // макрос — паникует с сообщением "not yet implemented" +dbg!(expression); // макрос — выводит файл:строку + выражение + значение, возвращает значение +assert_eq!(a, b); // макрос — паникует с дифом, если a ≠ b +cfg!(target_os = "linux"); // макрос — определение платформы на этапе компиляции ``` -### Writing a Simple Macro with `macro_rules!` +### Простой макрос на `macro_rules!` ```rust -// Define a macro that creates a HashMap from key-value pairs +// Определяем макрос, который создаёт HashMap из пар ключ-значение macro_rules! hashmap { - // Pattern: key => value pairs separated by commas + // Шаблон: пары ключ => значение, разделённые запятыми ( $( $key:expr => $value:expr ),* $(,)? ) => {{ let mut map = std::collections::HashMap::new(); $( map.insert($key, $value); )* @@ -75,83 +75,83 @@ fn main() { } ``` -### Derive Macros: Auto-Implementing Traits +### Производные макросы: автоматическая реализация трейтов ```rust -// #[derive] is a procedural macro that generates trait implementations +// #[derive] — это процедурный макрос, который генерирует реализации трейтов #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct User { name: String, age: u32, } -// The compiler generates Debug::fmt, Clone::clone, PartialEq::eq, etc. -// automatically by examining the struct fields. +// Компилятор автоматически генерирует Debug::fmt, Clone::clone, PartialEq::eq и т. д., +// анализируя поля структуры. ``` ```csharp -// C# equivalent: none — you'd manually implement IEquatable, ICloneable, etc. -// Or use records: public record User(string Name, int Age); -// Records auto-generate Equals, GetHashCode, ToString — similar idea! +// Аналога в C# нет — пришлось бы вручную реализовывать IEquatable, ICloneable и т. д. +// Или использовать records: public record User(string Name, int Age); +// Records автоматически генерируют Equals, GetHashCode, ToString — похожая идея! ``` -### Common Derive Macros +### Распространённые производные макросы -| Derive | Purpose | C# Equivalent | +| Derive | Назначение | Аналог в C# | |--------|---------|---------------| -| `Debug` | `{:?}` format string output | `ToString()` override | -| `Clone` | Deep copy via `.clone()` | `ICloneable` | -| `Copy` | Implicit bitwise copy (no `.clone()` needed) | Value type (`struct`) semantics | -| `PartialEq`, `Eq` | `==` comparison | `IEquatable<T>` | -| `PartialOrd`, `Ord` | `<`, `>` comparison + sorting | `IComparable<T>` | -| `Hash` | Hashing for `HashMap` keys | `GetHashCode()` | -| `Default` | Default values via `Default::default()` | Parameterless constructor | -| `Serialize`, `Deserialize` | JSON/TOML/etc. (serde) | `[JsonProperty]` attributes | +| `Debug` | Вывод в формате `{:?}` | Переопределение `ToString()` | +| `Clone` | Глубокое копирование через `.clone()` | `ICloneable` | +| `Copy` | Неявное побитовое копирование (`.clone()` не нужен) | Семантика значимого типа (`struct`) | +| `PartialEq`, `Eq` | Сравнение `==` | `IEquatable<T>` | +| `PartialOrd`, `Ord` | Сравнение `<`, `>` и сортировка | `IComparable<T>` | +| `Hash` | Хеширование для ключей `HashMap` | `GetHashCode()` | +| `Default` | Значения по умолчанию через `Default::default()` | Конструктор без параметров | +| `Serialize`, `Deserialize` | JSON/TOML и т. д. (serde) | Атрибуты `[JsonProperty]` | -> **Rule of thumb:** Start with `#[derive(Debug)]` on every type. Add `Clone`, `PartialEq` when needed. Add `Serialize, Deserialize` for any type that crosses a boundary (API, file, database). +> **Эмпирическое правило:** начинайте с `#[derive(Debug)]` на каждом типе. Добавляйте `Clone`, `PartialEq`, когда это нужно. Добавляйте `Serialize, Deserialize` для любого типа, который пересекает границу (API, файл, база данных). -### Procedural & Attribute Macros (Awareness Level) +### Процедурные и атрибутные макросы (на уровне понимания) -Derive macros are one kind of **procedural macro** — code that runs at compile time to generate code. You'll encounter two other forms: +Производные макросы — это один из видов **процедурных макросов**: кода, который выполняется на этапе компиляции и генерирует код. Вы встретите ещё две формы: -**Attribute macros** — attached to items with `#[...]`: +**Атрибутные макросы** — присоединяются к элементам через `#[...]`: ```rust -#[tokio::main] // turns main() into an async runtime entry point +#[tokio::main] // превращает main() в точку входа асинхронного рантайма async fn main() { } -#[test] // marks a function as a unit test +#[test] // помечает функцию как модульный тест fn it_works() { assert_eq!(2 + 2, 4); } -#[cfg(test)] // conditionally compile this module only during testing +#[cfg(test)] // условно компилирует этот модуль только во время тестирования mod tests { /* ... */ } ``` -**Function-like macros** — look like function calls: +**Функциональные макросы** — выглядят как вызовы функций: ```rust -// sqlx::query! verifies your SQL against the database at compile time +// sqlx::query! проверяет ваш SQL по базе данных на этапе компиляции let users = sqlx::query!("SELECT id, name FROM users WHERE active = $1", true) .fetch_all(&pool) .await?; ``` -> **Key insight for C# developers:** You rarely *write* procedural macros — they're an advanced library-author tool. But you *use* them constantly (`#[derive(...)]`, `#[tokio::main]`, `#[test]`). Think of them like C# source generators: you benefit from them without implementing them. +> **Ключевая мысль для разработчиков C#:** процедурные макросы вы редко *пишете* — это инструмент для авторов библиотек продвинутого уровня. Но вы *используете* их постоянно (`#[derive(...)]`, `#[tokio::main]`, `#[test]`). Думайте о них как об источниках генерации кода (source generators) в C#: вы получаете выгоду, не реализуя их сами. -### Conditional Compilation with `#[cfg]` +### Условная компиляция через `#[cfg]` -Rust's `#[cfg]` attributes are like C#'s `#if DEBUG` preprocessor directives, but type-checked: +Атрибуты `#[cfg]` в Rust похожи на директивы препроцессора `#if DEBUG` в C#, но с проверкой типов: ```rust -// Compile this function only on Linux +// Компилируем эту функцию только для Linux #[cfg(target_os = "linux")] fn platform_specific() { println!("Running on Linux"); } -// Debug-only assertions (like C# Debug.Assert) +// Проверки только для отладки (как Debug.Assert в C#) #[cfg(debug_assertions)] fn expensive_check(data: &[u8]) { assert!(data.len() < 1_000_000, "data unexpectedly large"); } -// Feature flags (like C# #if FEATURE_X, but declared in Cargo.toml) +// Feature-флаги (как #if FEATURE_X в C#, но объявляются в Cargo.toml) #[cfg(feature = "json")] pub fn to_json<T: Serialize>(val: &T) -> String { serde_json::to_string(val).unwrap() @@ -159,42 +159,42 @@ pub fn to_json<T: Serialize>(val: &T) -> String { ``` ```csharp -// C# equivalent +// Аналог в C# #if DEBUG Debug.Assert(data.Length < 1_000_000); #endif ``` -### `dbg!()` — Your Best Friend for Debugging +### `dbg!()` — ваш лучший друг при отладке ```rust fn calculate(x: i32) -> i32 { - let intermediate = dbg!(x * 2); // prints: [src/main.rs:3] x * 2 = 10 - let result = dbg!(intermediate + 1); // prints: [src/main.rs:4] intermediate + 1 = 11 + let intermediate = dbg!(x * 2); // выводит: [src/main.rs:3] x * 2 = 10 + let result = dbg!(intermediate + 1); // выводит: [src/main.rs:4] intermediate + 1 = 11 result } -// dbg! prints to stderr, includes file:line, and returns the value -// Far more useful than Console.WriteLine for debugging! +// dbg! пишет в stderr, включает файл:строку и возвращает значение +// Гораздо полезнее, чем Console.WriteLine для отладки! ``` <details> -<summary><strong>🏋️ Exercise: Write a min! Macro</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: напишите макрос min!</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Write a `min!` macro that accepts 2 or more arguments and returns the smallest. +**Задача**: напишите макрос `min!`, который принимает два или более аргументов и возвращает наименьший. ```rust -// Should work like: +// Должно работать так: let smallest = min!(5, 3, 8, 1, 4); // → 1 let pair = min!(10, 20); // → 10 ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust macro_rules! min { - // Base case: single value + // Базовый случай: одно значение ($x:expr) => ($x); - // Recursive: compare first with min of rest + // Рекурсивно: сравниваем первое со минимумом остальных ($x:expr, $($rest:expr),+) => {{ let first = $x; let rest = min!($($rest),+); @@ -210,7 +210,7 @@ fn main() { } ``` -**Key takeaway**: `macro_rules!` uses pattern matching on token trees — it's like `match` but for code structure instead of values. +**Ключевая мысль**: `macro_rules!` выполняет сопоставление с образцом по деревьям токенов — это как `match`, только для структуры кода, а не значений. </details> </details> diff --git a/csharp-book/src/ch12-closures-and-iterators.md b/csharp-book/src/ch12-closures-and-iterators.md index 768867f..4ce5de5 100644 --- a/csharp-book/src/ch12-closures-and-iterators.md +++ b/csharp-book/src/ch12-closures-and-iterators.md @@ -1,106 +1,106 @@ -## Rust Closures +## Замыкания в Rust -> **What you'll learn:** Closures with ownership-aware captures (`Fn`/`FnMut`/`FnOnce`) vs C# lambdas, -> Rust iterators as a zero-cost replacement for LINQ, lazy vs eager evaluation, -> and parallel iteration with `rayon`. +> **Что вы узнаете:** замыкания с захватом с учётом владения (`Fn`/`FnMut`/`FnOnce`) в сравнении с лямбдами C#, +> итераторы Rust как замену LINQ с нулевой стоимостью, ленивые и энергичные вычисления, +> а также параллельную итерацию с `rayon`. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -Closures in Rust are similar to C# lambdas and delegates, but with ownership-aware captures. +Замыкания в Rust похожи на лямбды и делегаты C#, но захватывают переменные с учётом владения. -### C# Lambdas and Delegates +### Лямбды и делегаты C# ```csharp -// C# - Lambdas capture by reference +// C# — лямбды захватывают по ссылке Func<int, int> doubler = x => x * 2; Action<string> printer = msg => Console.WriteLine(msg); -// Closure capturing outer variables +// Замыкание, захватывающее внешние переменные int multiplier = 3; Func<int, int> multiply = x => x * multiplier; Console.WriteLine(multiply(5)); // 15 -// LINQ uses lambdas extensively +// LINQ активно использует лямбды var evens = numbers.Where(n => n % 2 == 0).ToList(); ``` -### Rust Closures +### Замыкания в Rust ```rust -// Rust closures - ownership-aware +// Замыкания Rust — с учётом владения let doubler = |x: i32| x * 2; let printer = |msg: &str| println!("{}", msg); -// Closure capturing by reference (default for immutable) +// Замыкание, захватывающее по ссылке (по умолчанию для неизменяемых) let multiplier = 3; -let multiply = |x: i32| x * multiplier; // borrows multiplier +let multiply = |x: i32| x * multiplier; // заимствует multiplier println!("{}", multiply(5)); // 15 -println!("{}", multiplier); // still accessible +println!("{}", multiplier); // по-прежнему доступен -// Closure capturing by move +// Замыкание, захватывающее перемещением let data = vec![1, 2, 3]; let owns_data = move || { - println!("{:?}", data); // data moved into closure + println!("{:?}", data); // data перемещается в замыкание }; owns_data(); -// println!("{:?}", data); // ERROR: data was moved +// println!("{:?}", data); // ОШИБКА: data было перемещено -// Using closures with iterators +// Использование замыканий с итераторами let numbers = vec![1, 2, 3, 4, 5]; let evens: Vec<&i32> = numbers.iter().filter(|&&n| n % 2 == 0).collect(); ``` -### Closure Types +### Виды замыканий ```rust -// Fn - borrows captured values immutably +// Fn — заимствует захваченные значения неизменяемо fn apply_fn(f: impl Fn(i32) -> i32, x: i32) -> i32 { f(x) } -// FnMut - borrows captured values mutably +// FnMut — заимствует захваченные значения изменяемо fn apply_fn_mut(mut f: impl FnMut(i32), values: &[i32]) { for &v in values { f(v); } } -// FnOnce - takes ownership of captured values +// FnOnce — забирает владение захваченными значениями fn apply_fn_once(f: impl FnOnce() -> Vec<i32>) -> Vec<i32> { - f() // can only call once + f() // можно вызвать только один раз } fn main() { - // Fn example + // Пример Fn let multiplier = 3; let result = apply_fn(|x| x * multiplier, 5); - // FnMut example + // Пример FnMut let mut sum = 0; apply_fn_mut(|x| sum += x, &[1, 2, 3, 4, 5]); println!("Sum: {}", sum); // 15 - // FnOnce example + // Пример FnOnce let data = vec![1, 2, 3]; - let result = apply_fn_once(move || data); // moves data + let result = apply_fn_once(move || data); // перемещает data } ``` *** -## LINQ vs Rust Iterators +## LINQ против итераторов Rust -### C# LINQ (Language Integrated Query) +### LINQ в C# (Language Integrated Query) ```csharp -// C# LINQ - Declarative data processing +// LINQ в C# — декларативная обработка данных var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var result = numbers - .Where(n => n % 2 == 0) // Filter even numbers - .Select(n => n * n) // Square them - .Where(n => n > 10) // Filter > 10 - .OrderByDescending(n => n) // Sort descending - .Take(3) // Take first 3 - .ToList(); // Materialize - -// LINQ with complex objects + .Where(n => n % 2 == 0) // Отбираем чётные числа + .Select(n => n * n) // Возводим в квадрат + .Where(n => n > 10) // Отбираем > 10 + .OrderByDescending(n => n) // Сортируем по убыванию + .Take(3) // Берём первые 3 + .ToList(); // Материализуем + +// LINQ со сложными объектами var users = GetUsers(); var activeAdults = users .Where(u => u.IsActive && u.Age >= 18) @@ -113,7 +113,7 @@ var activeAdults = users .OrderBy(x => x.Department) .ToList(); -// Async LINQ (with additional libraries) +// Асинхронный LINQ (с дополнительными библиотеками) var results = await users .ToAsyncEnumerable() .WhereAwait(async u => await IsActiveAsync(u.Id)) @@ -121,23 +121,23 @@ var results = await users .ToListAsync(); ``` -### Rust Iterators +### Итераторы Rust ```rust -// Rust iterators - Lazy, zero-cost abstractions +// Итераторы Rust — ленивые абстракции с нулевой стоимостью let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let result: Vec<i32> = numbers .iter() - .filter(|&&n| n % 2 == 0) // Filter even numbers - .map(|&n| n * n) // Square them - .filter(|&n| n > 10) // Filter > 10 - .collect::<Vec<_>>() // Collect to Vec + .filter(|&&n| n % 2 == 0) // Отбираем чётные числа + .map(|&n| n * n) // Возводим в квадрат + .filter(|&n| n > 10) // Отбираем > 10 + .collect::<Vec<_>>() // Собираем в Vec .into_iter() - .rev() // Reverse iteration order - .take(3) // Take first 3 - .collect(); // Materialize + .rev() // Меняем порядок обхода + .take(3) // Берём первые 3 + .collect(); // Материализуем -// Complex iterator chains +// Сложные цепочки итераторов use std::collections::HashMap; #[derive(Debug, Clone)] @@ -154,41 +154,41 @@ fn process_users(users: Vec<User>) -> HashMap<String, (usize, f64)> { .filter(|u| u.is_active && u.age >= 18) .fold(HashMap::new(), |mut acc, user| { let entry = acc.entry(user.department.clone()).or_insert((0, 0.0)); - entry.0 += 1; // count - entry.1 += user.age as f64; // sum of ages + entry.0 += 1; // количество + entry.1 += user.age as f64; // сумма возрастов acc }) .into_iter() - .map(|(dept, (count, sum))| (dept, (count, sum / count as f64))) // average + .map(|(dept, (count, sum))| (dept, (count, sum / count as f64))) // среднее .collect() } -// Parallel processing with rayon +// Параллельная обработка с rayon use rayon::prelude::*; fn parallel_processing(numbers: Vec<i32>) -> Vec<i32> { numbers - .par_iter() // Parallel iterator + .par_iter() // Параллельный итератор .filter(|&&n| n % 2 == 0) .map(|&n| expensive_computation(n)) .collect() } fn expensive_computation(n: i32) -> i32 { - // Simulate heavy computation + // Имитация тяжёлых вычислений (0..1000).fold(n, |acc, _| acc + 1) } ``` ```mermaid graph TD - subgraph "C# LINQ Characteristics" - CS_LINQ["LINQ Expression"] - CS_EAGER["Often eager evaluation<br/>(ToList(), ToArray())"] - CS_REFLECTION["[ERROR] Some runtime reflection<br/>Expression trees"] - CS_ALLOCATIONS["[ERROR] Intermediate collections<br/>Garbage collection pressure"] - CS_ASYNC["[OK] Async support<br/>(with additional libraries)"] - CS_SQL["[OK] LINQ to SQL/EF integration"] + subgraph "Особенности LINQ в C#" + CS_LINQ["Выражение LINQ"] + CS_EAGER["Часто энергичные вычисления<br/>(ToList(), ToArray())"] + CS_REFLECTION["[ERROR] Часть рефлексии во время выполнения<br/>Деревья выражений"] + CS_ALLOCATIONS["[ERROR] Промежуточные коллекции<br/>Нагрузка на сборщик мусора"] + CS_ASYNC["[OK] Поддержка async<br/>(с дополнительными библиотеками)"] + CS_SQL["[OK] Интеграция LINQ to SQL/EF"] CS_LINQ --> CS_EAGER CS_LINQ --> CS_REFLECTION @@ -197,13 +197,13 @@ graph TD CS_LINQ --> CS_SQL end - subgraph "Rust Iterator Characteristics" - RUST_ITER["Iterator Chain"] - RUST_LAZY["[OK] Lazy evaluation<br/>No work until .collect()"] - RUST_ZERO["[OK] Zero-cost abstractions<br/>Compiles to optimal loops"] - RUST_NO_ALLOC["[OK] No intermediate allocations<br/>Stack-based processing"] - RUST_PARALLEL["[OK] Easy parallelization<br/>(rayon crate)"] - RUST_FUNCTIONAL["[OK] Functional programming<br/>Immutable by default"] + subgraph "Особенности итераторов Rust" + RUST_ITER["Цепочка итераторов"] + RUST_LAZY["[OK] Ленивые вычисления<br/>Никакой работы до .collect()"] + RUST_ZERO["[OK] Абстракции с нулевой стоимостью<br/>Компилируются в оптимальные циклы"] + RUST_NO_ALLOC["[OK] Без промежуточных выделений<br/>Обработка на стеке"] + RUST_PARALLEL["[OK] Простая параллелизация<br/>(крейт rayon)"] + RUST_FUNCTIONAL["[OK] Функциональное программирование<br/>Неизменяемость по умолчанию"] RUST_ITER --> RUST_LAZY RUST_ITER --> RUST_ZERO @@ -212,9 +212,9 @@ graph TD RUST_ITER --> RUST_FUNCTIONAL end - subgraph "Performance Comparison" - CS_PERF["C# LINQ Performance<br/>[ERROR] Allocation overhead<br/>[ERROR] Virtual dispatch<br/>[OK] Good enough for most cases"] - RUST_PERF["Rust Iterator Performance<br/>[OK] Hand-optimized speed<br/>[OK] No allocations<br/>[OK] Compile-time optimization"] + subgraph "Сравнение производительности" + CS_PERF["Производительность LINQ в C#<br/>[ERROR] Накладные расходы на выделение памяти<br/>[ERROR] Виртуальная диспетчеризация<br/>[OK] Достаточно для большинства случаев"] + RUST_PERF["Производительность итераторов Rust<br/>[OK] Скорость уровня ручной оптимизации<br/>[OK] Без выделений памяти<br/>[OK] Оптимизация на этапе компиляции"] end style CS_REFLECTION fill:#ffcdd2,color:#000 @@ -230,12 +230,12 @@ graph TD <details> -<summary><strong>🏋️ Exercise: LINQ to Iterators Translation</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: перевод из LINQ в итераторы</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Translate this C# LINQ pipeline to idiomatic Rust iterators. +**Задача**: переведите этот конвейер LINQ на C# в идиоматичные итераторы Rust. ```csharp -// C# — translate to Rust +// C# — переведите на Rust record Employee(string Name, string Dept, int Salary); var result = employees @@ -251,7 +251,7 @@ var result = employees ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::collections::HashMap; @@ -281,19 +281,19 @@ fn department_stats(employees: &[Employee]) -> Vec<DeptStats> { } ``` -**Key takeaways**: -- Rust has no built-in `group_by` on iterators — `HashMap` + `fold`/`for` is the idiomatic pattern -- `itertools` crate adds `.group_by()` for more LINQ-like syntax -- Iterator chains are zero-cost — the compiler optimizes them to simple loops +**Ключевые выводы**: +- В Rust нет встроенного `group_by` для итераторов — идиоматичный паттерн это `HashMap` + `fold`/`for` +- Крейт `itertools` добавляет `.group_by()` для синтаксиса, более похожего на LINQ +- Цепочки итераторов не имеют накладных расходов — компилятор превращает их в простые циклы </details> </details> <!-- ch12.0a: itertools — LINQ Power Tools --> -## itertools: The Missing LINQ Operations +## itertools: недостающие операции LINQ -Standard Rust iterators cover `map`, `filter`, `fold`, `take`, and `collect`. But C# developers using `GroupBy`, `Zip`, `Chunk`, `SelectMany`, and `Distinct` will immediately notice gaps. The **`itertools`** crate fills them. +Стандартные итераторы Rust покрывают `map`, `filter`, `fold`, `take` и `collect`. Но разработчики C#, привыкшие к `GroupBy`, `Zip`, `Chunk`, `SelectMany` и `Distinct`, сразу заметят пробелы. Крейт **`itertools`** их восполняет. ```toml # Cargo.toml @@ -301,26 +301,26 @@ Standard Rust iterators cover `map`, `filter`, `fold`, `take`, and `collect`. Bu itertools = "0.12" ``` -### Side-by-Side: LINQ vs itertools +### Бок о бок: LINQ и itertools ```csharp // C# — GroupBy var byDept = employees.GroupBy(e => e.Department) .Select(g => new { Dept = g.Key, Count = g.Count() }); -// C# — Chunk (batching) +// C# — Chunk (пакетирование) var batches = items.Chunk(100); // IEnumerable<T[]> // C# — Distinct / DistinctBy var unique = users.DistinctBy(u => u.Email); -// C# — SelectMany (flatten) +// C# — SelectMany (уплощение) var allTags = posts.SelectMany(p => p.Tags); // C# — Zip var pairs = names.Zip(scores, (n, s) => new { Name = n, Score = s }); -// C# — Sliding window +// C# — скользящее окно var windows = data.Zip(data.Skip(1), data.Skip(2)) .Select(triple => (triple.First + triple.Second + triple.Third) / 3.0); ``` @@ -328,7 +328,7 @@ var windows = data.Zip(data.Skip(1), data.Skip(2)) ```rust use itertools::Itertools; -// Rust — group_by (requires sorted input) +// Rust — group_by (требует отсортированный ввод) let by_dept = employees.iter() .sorted_by_key(|e| &e.department) .group_by(|e| &e.department); @@ -336,7 +336,7 @@ for (dept, group) in &by_dept { println!("{}: {} employees", dept, group.count()); } -// Rust — chunks (batching) +// Rust — chunks (пакетирование) let batches = items.iter().chunks(100); for batch in &batches { process_batch(batch.collect::<Vec<_>>()); @@ -345,43 +345,43 @@ for batch in &batches { // Rust — unique / unique_by let unique: Vec<_> = users.iter().unique_by(|u| &u.email).collect(); -// Rust — flat_map (SelectMany equivalent — built-in!) +// Rust — flat_map (аналог SelectMany — встроен в std!) let all_tags: Vec<&str> = posts.iter().flat_map(|p| &p.tags).collect(); -// Rust — zip (built-in!) +// Rust — zip (встроен в std!) let pairs: Vec<_> = names.iter().zip(scores.iter()).collect(); -// Rust — tuple_windows (sliding window) +// Rust — tuple_windows (скользящее окно) let moving_avg: Vec<f64> = data.iter() .tuple_windows::<(_, _, _)>() .map(|(a, b, c)| (*a + *b + *c) as f64 / 3.0) .collect(); ``` -### itertools Quick Reference +### Краткая справка по itertools -| LINQ Method | itertools Equivalent | Notes | +| Метод LINQ | Аналог в itertools | Примечания | |------------|---------------------|-------| -| `GroupBy(key)` | `.sorted_by_key().group_by()` | Requires sorted input (unlike LINQ) | -| `Chunk(n)` | `.chunks(n)` | Returns iterator of iterators | -| `Distinct()` | `.unique()` | Requires `Eq + Hash` | +| `GroupBy(key)` | `.sorted_by_key().group_by()` | Требует отсортированный ввод (в отличие от LINQ) | +| `Chunk(n)` | `.chunks(n)` | Возвращает итератор итераторов | +| `Distinct()` | `.unique()` | Требует `Eq + Hash` | | `DistinctBy(key)` | `.unique_by(key)` | | -| `SelectMany()` | `.flat_map()` | Built into std — no crate needed | -| `Zip()` | `.zip()` | Built into std | -| `Aggregate()` | `.fold()` | Built into std | -| `Any()` / `All()` | `.any()` / `.all()` | Built into std | -| `First()` / `Last()` | `.next()` / `.last()` | Built into std | -| `Skip(n)` / `Take(n)` | `.skip(n)` / `.take(n)` | Built into std | -| `OrderBy()` | `.sorted()` / `.sorted_by()` | `itertools` (std has none) | -| `ThenBy()` | `.sorted_by(\|a,b\| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))` | Chained `Ordering::then` | -| `Intersect()` | `HashSet` intersection | No direct iterator method | -| `Concat()` | `.chain()` | Built into std | -| Sliding window | `.tuple_windows()` | Fixed-size tuples | -| Cartesian product | `.cartesian_product()` | `itertools` | -| Interleave | `.interleave()` | `itertools` | -| Permutations | `.permutations(k)` | `itertools` | - -### Real-World Example: Log Analysis Pipeline +| `SelectMany()` | `.flat_map()` | Встроен в std — крейт не нужен | +| `Zip()` | `.zip()` | Встроен в std | +| `Aggregate()` | `.fold()` | Встроен в std | +| `Any()` / `All()` | `.any()` / `.all()` | Встроены в std | +| `First()` / `Last()` | `.next()` / `.last()` | Встроены в std | +| `Skip(n)` / `Take(n)` | `.skip(n)` / `.take(n)` | Встроены в std | +| `OrderBy()` | `.sorted()` / `.sorted_by()` | `itertools` (в std сортировки нет) | +| `ThenBy()` | `.sorted_by(\|a,b\| a.x.cmp(&b.x).then(a.y.cmp(&b.y)))` | Цепочка через `Ordering::then` | +| `Intersect()` | Пересечение `HashSet` | Прямого метода итератора нет | +| `Concat()` | `.chain()` | Встроен в std | +| Скользящее окно | `.tuple_windows()` | Кортежи фиксированного размера | +| Декартово произведение | `.cartesian_product()` | `itertools` | +| Чередование | `.interleave()` | `itertools` | +| Перестановки | `.permutations(k)` | `itertools` | + +### Пример из практики: конвейер анализа логов ```rust use itertools::Itertools; @@ -391,9 +391,9 @@ use std::collections::HashMap; struct LogEntry { level: String, module: String, message: String } fn analyze_logs(entries: &[LogEntry]) { - // Top 5 noisiest modules (like LINQ GroupBy + OrderByDescending + Take) + // Топ-5 самых «шумных» модулей (как LINQ GroupBy + OrderByDescending + Take) let noisy: Vec<_> = entries.iter() - .into_group_map_by(|e| &e.module) // itertools: direct group into HashMap + .into_group_map_by(|e| &e.module) // itertools: сразу группируем в HashMap .into_iter() .sorted_by(|a, b| b.1.len().cmp(&a.1.len())) .take(5) @@ -403,15 +403,15 @@ fn analyze_logs(entries: &[LogEntry]) { println!("{}: {} entries", module, entries.len()); } - // Error rate per 100-entry window (sliding window) + // Доля ошибок в окне из 100 записей (скользящее окно) let error_rates: Vec<f64> = entries.iter() .map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 }) .collect::<Vec<_>>() - .windows(100) // std slice method + .windows(100) // метод среза из std .map(|w| w.iter().sum::<f64>() / 100.0) .collect(); - // Deduplicate consecutive identical messages + // Удаляем подряд идущие одинаковые сообщения let deduped: Vec<_> = entries.iter().dedup_by(|a, b| a.message == b.message).collect(); println!("Deduped {} → {} entries", entries.len(), deduped.len()); } From 9536909407a3fe017888a32d39d1d7c5f889da30 Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 10:33:54 +0400 Subject: [PATCH 17/18] Translate C# book chapters 13-17 and TOC to Russian Translate prose, headings, tables, diagram labels, code comments and user-facing strings for ch13 through ch17 and ch00 (introduction, self-study guide and table of contents) in csharp-book/src. Code identifiers, crate names, file names and links are unchanged; the capstone CLI's user-facing messages are translated along with their tests. TOC anchors in ch00 and cross-chapter anchors are updated to the Russian heading slugs; the single ch07 link to the Drop section is updated to match. All internal anchors resolve in the built book. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- csharp-book/src/ch00-introduction.md | 310 +++++++++-------- .../src/ch07-ownership-and-borrowing.md | 2 +- .../src/ch13-1-asyncawait-deep-dive.md | 124 +++---- csharp-book/src/ch13-concurrency.md | 180 +++++----- csharp-book/src/ch14-1-testing.md | 172 +++++----- csharp-book/src/ch14-unsafe-rust-and-ffi.md | 163 ++++----- ...h15-1-essential-crates-for-c-developers.md | 169 +++++----- .../ch15-2-incremental-adoption-strategy.md | 106 +++--- ...h15-migration-patterns-and-case-studies.md | 182 +++++----- ...-1-performance-comparison-and-migration.md | 128 +++---- .../src/ch16-2-learning-path-and-resources.md | 314 +++++++++--------- .../src/ch16-3-rust-tooling-ecosystem.md | 122 +++---- csharp-book/src/ch16-best-practices.md | 208 ++++++------ csharp-book/src/ch17-capstone-project.md | 168 +++++----- 14 files changed, 1171 insertions(+), 1177 deletions(-) diff --git a/csharp-book/src/ch00-introduction.md b/csharp-book/src/ch00-introduction.md index 02ef678..42449bc 100644 --- a/csharp-book/src/ch00-introduction.md +++ b/csharp-book/src/ch00-introduction.md @@ -1,180 +1,178 @@ -# Rust for C# Programmers: Complete Training Guide - -A comprehensive guide to learning Rust for developers with C# experience. This guide covers everything from basic syntax to advanced patterns, focusing on the conceptual shifts and practical differences between the two languages. - -## Course Overview -- **The case for Rust** — Why Rust matters for C# developers: performance, safety, and correctness -- **Getting started** — Installation, tooling, and your first program -- **Basic building blocks** — Types, variables, control flow -- **Data structures** — Arrays, tuples, structs, collections -- **Pattern matching and enums** — Algebraic data types and exhaustive matching -- **Ownership and borrowing** — Rust's memory management model -- **Modules and crates** — Code organization and dependencies -- **Error handling** — Result-based error propagation -- **Traits and generics** — Rust's type system -- **Closures and iterators** — Functional programming patterns -- **Concurrency** — Fearless concurrency with type-system guarantees, async/await deep dive -- **Unsafe Rust and FFI** — When and how to go beyond safe Rust -- **Migration patterns** — Real-world C# to Rust patterns and incremental adoption -- **Best practices** — Idiomatic Rust for C# developers +# Rust для программистов C#: полное учебное руководство + +Подробное руководство по изучению Rust для разработчиков, которые уже знакомы с C#. Книга охватывает всё: от базового синтаксиса до продвинутых паттернов. Основной акцент сделан на концептуальных сдвигах и практических различиях между двумя языками. + +## Обзор курса +- **Аргументы в пользу Rust** — зачем Rust важен для разработчиков C#: производительность, безопасность и корректность +- **Начало работы** — установка, инструменты и первая программа +- **Базовые строительные блоки** — типы, переменные, управление потоком выполнения +- **Структуры данных** — массивы, кортежи, структуры, коллекции +- **Сопоставление с образцом и перечисления** — алгебраические типы данных и исчерпывающее сопоставление +- **Владение и заимствование** — модель управления памятью Rust +- **Модули и крейты** — организация кода и зависимости +- **Обработка ошибок** — распространение ошибок на основе Result +- **Трейты и обобщения** — система типов Rust +- **Замыкания и итераторы** — паттерны функционального программирования +- **Конкурентность** — конкурентность без страха благодаря гарантиям системы типов, глубокое погружение в async/await +- **Unsafe Rust и FFI** — когда и как выходить за рамки безопасного Rust +- **Паттерны миграции** — реальные паттерны перехода с C# на Rust и постепенное внедрение +- **Лучшие практики** — идиоматичный Rust для разработчиков C# --- -# Self-Study Guide +# Руководство для самостоятельного изучения -This material works both as an instructor-led course and for self-study. If you're working through it on your own, here's how to get the most out of it. +Материал подходит и для занятий с преподавателем, и для самостоятельного изучения. Если вы работаете над ним самостоятельно, вот как извлечь из него максимум пользы. -**Pacing recommendations:** +**Рекомендации по темпу:** -| Chapters | Topic | Suggested Time | Checkpoint | +| Главы | Тема | Примерное время | Контрольная точка | |----------|-------|---------------|------------| -| 1–4 | Setup, types, control flow | 1 day | You can write a CLI temperature converter in Rust | -| 5–6 | Data structures, enums, pattern matching | 1–2 days | You can define an enum with data and `match` exhaustively on it | -| 7 | Ownership and borrowing | 1–2 days | You can explain *why* `let s2 = s1` invalidates `s1` | -| 8–9 | Modules, error handling | 1 day | You can create a multi-file project that propagates errors with `?` | -| 10–12 | Traits, generics, closures, iterators | 1–2 days | You can translate a LINQ chain to Rust iterators | -| 13 | Concurrency and async | 1 day | You can write a thread-safe counter with `Arc<Mutex<T>>` | -| 14 | Unsafe Rust, FFI, testing | 1 day | You can call a Rust function from C# via P/Invoke | -| 15–16 | Migration, best practices, tooling | At your own pace | Reference material — consult as you write real code | -| 17 | Capstone project | 1–2 days | You have a working CLI tool that fetches weather data | - -**How to use the exercises:** -- Chapters include hands-on exercises in collapsible `<details>` blocks with solutions -- **Always try the exercise before expanding the solution.** Struggling with the borrow checker is part of learning — the compiler's error messages are your teacher -- If you're stuck for more than 15 minutes, expand the solution, study it, then close it and try again from scratch -- The [Rust Playground](https://play.rust-lang.org/) lets you run code without a local install - -**Difficulty indicators:** -- 🟢 **Beginner** — Direct translation from C# concepts -- 🟡 **Intermediate** — Requires understanding ownership or traits -- 🔴 **Advanced** — Lifetimes, async internals, or unsafe code - -**When you hit a wall:** -- Read the compiler error message carefully — Rust's errors are exceptionally helpful -- Re-read the relevant section; concepts like ownership (ch7) often click on the second pass -- The [Rust standard library docs](https://doc.rust-lang.org/std/) are excellent — search for any type or method -- For deeper async patterns, see the companion [Async Rust Training](../async-book/) +| 1–4 | Настройка, типы, управление потоком выполнения | 1 день | Вы можете написать CLI-конвертер температур на Rust | +| 5–6 | Структуры данных, перечисления, сопоставление с образцом | 1–2 дня | Вы можете определить перечисление с данными и исчерпывающе разобрать его через `match` | +| 7 | Владение и заимствование | 1–2 дня | Вы можете объяснить, *почему* `let s2 = s1` делает `s1` недействительным | +| 8–9 | Модули, обработка ошибок | 1 день | Вы можете создать многофайловый проект, который передаёт ошибки дальше с помощью `?` | +| 10–12 | Трейты, обобщения, замыкания, итераторы | 1–2 дня | Вы можете перевести цепочку LINQ в итераторы Rust | +| 13 | Конкурентность и async | 1 день | Вы можете написать потокобезопасный счётчик с `Arc<Mutex<T>>` | +| 14 | Unsafe Rust, FFI, тестирование | 1 день | Вы можете вызвать функцию Rust из C# через P/Invoke | +| 15–16 | Миграция, лучшие практики, инструменты | В своём темпе | Справочный материал — обращайтесь к нему при написании реального кода | +| 17 | Итоговый проект | 1–2 дня | У вас есть работающая CLI-утилита, которая получает данные о погоде | + +**Как работать с упражнениями:** +- В главах есть практические упражнения в сворачиваемых блоках `<details>` с решениями +- **Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение.** Борьба с проверщиком заимствований — часть обучения, а сообщения компилятора — ваш учитель +- Если вы застряли больше чем на 15 минут, раскройте решение, изучите его, затем закройте и попробуйте снова с нуля +- [Rust Playground](https://play.rust-lang.org/) позволяет запускать код без локальной установки + +**Индикаторы сложности:** +- 🟢 **Начальный** — прямой перевод концепций из C# +- 🟡 **Средний** — требует понимания владения или трейтов +- 🔴 **Продвинутый** — времена жизни, внутреннее устройство async или unsafe-код + +**Если вы упёрлись в стену:** +- Внимательно читайте сообщение компилятора: ошибки Rust исключительно полезны +- Перечитайте нужный раздел. Концепции вроде владения (гл. 7) часто становятся понятны при втором прочтении +- [Документация стандартной библиотеки Rust](https://doc.rust-lang.org/std/) отличная: ищите любой тип или метод +- Более глубокие асинхронные паттерны описаны в сопутствующем курсе [Async Rust](../async-book/) --- -# Table of Contents - -## Part I — Foundations - -### 1. Introduction and Motivation 🟢 -- [The Case for Rust for C# Developers](ch01-introduction-and-motivation.md#the-case-for-rust-for-c-developers) -- [Common C# Pain Points That Rust Addresses](ch01-introduction-and-motivation.md#common-c-pain-points-that-rust-addresses) -- [When to Choose Rust Over C#](ch01-introduction-and-motivation.md#when-to-choose-rust-over-c) -- [Language Philosophy Comparison](ch01-introduction-and-motivation.md#language-philosophy-comparison) -- [Quick Reference: Rust vs C#](ch01-introduction-and-motivation.md#quick-reference-rust-vs-c) - -### 2. Getting Started 🟢 -- [Installation and Setup](ch02-getting-started.md#installation-and-setup) -- [Your First Rust Program](ch02-getting-started.md#your-first-rust-program) -- [Cargo vs NuGet/MSBuild](ch02-getting-started.md#cargo-vs-nugetmsbuild) -- [Reading Input and CLI Arguments](ch02-getting-started.md#reading-input-and-cli-arguments) -- [Essential Rust Keywords *(optional reference — consult as needed)*](ch02-1-essential-keywords-reference.md#essential-rust-keywords-for-c-developers) - -### 3. Built-in Types and Variables 🟢 -- [Variables and Mutability](ch03-built-in-types-and-variables.md#variables-and-mutability) -- [Primitive Types Comparison](ch03-built-in-types-and-variables.md#primitive-types) -- [String Types: String vs &str](ch03-built-in-types-and-variables.md#string-types-string-vs-str) -- [Printing and String Formatting](ch03-built-in-types-and-variables.md#printing-and-string-formatting) -- [Type Casting and Conversions](ch03-built-in-types-and-variables.md#type-casting-and-conversions) -- [True Immutability vs Record Illusions](ch03-1-true-immutability-vs-record-illusions.md#true-immutability-vs-record-illusions) - -### 4. Control Flow 🟢 -- [Functions vs Methods](ch04-control-flow.md#functions-vs-methods) -- [Expression vs Statement (Important!)](ch04-control-flow.md#expression-vs-statement-important) -- [Conditional Statements](ch04-control-flow.md#conditional-statements) -- [Loops and Iteration](ch04-control-flow.md#loops) - -### 5. Data Structures and Collections 🟢 -- [Tuples and Destructuring](ch05-data-structures-and-collections.md#tuples-and-destructuring) -- [Arrays and Slices](ch05-data-structures-and-collections.md#arrays-and-slices) -- [Structs vs Classes](ch05-data-structures-and-collections.md#structs-vs-classes) -- [Constructor Patterns](ch05-1-constructor-patterns.md#constructor-patterns) -- [`Vec<T>` vs `List<T>`](ch05-2-collections-vec-hashmap-and-iterators.md#vect-vs-listt) -- [HashMap vs Dictionary](ch05-2-collections-vec-hashmap-and-iterators.md#hashmap-vs-dictionary) - -### 6. Enums and Pattern Matching 🟡 -- [Algebraic Data Types vs C# Unions](ch06-enums-and-pattern-matching.md#algebraic-data-types-vs-c-unions) -- [Exhaustive Pattern Matching](ch06-1-exhaustive-matching-and-null-safety.md#exhaustive-pattern-matching-compiler-guarantees-vs-runtime-errors) -- [`Option<T>` for Null Safety](ch06-1-exhaustive-matching-and-null-safety.md#null-safety-nullablet-vs-optiont) -- [Guards and Advanced Patterns](ch06-enums-and-pattern-matching.md#guards-and-advanced-patterns) - -### 7. Ownership and Borrowing 🟡 -- [Understanding Ownership](ch07-ownership-and-borrowing.md#understanding-ownership) -- [Move Semantics vs Reference Semantics](ch07-ownership-and-borrowing.md#move-semantics) -- [Borrowing and References](ch07-ownership-and-borrowing.md#borrowing-basics) -- [Memory Safety Deep Dive](ch07-1-memory-safety-deep-dive.md#references-vs-pointers) -- [Lifetimes Deep Dive](ch07-2-lifetimes-deep-dive.md#lifetimes-telling-the-compiler-how-long-references-live) 🔴 -- [Smart Pointers, Drop, and Deref](ch07-3-smart-pointers-beyond-single-ownership.md#smart-pointers-when-single-ownership-isnt-enough) 🔴 - -### 8. Crates and Modules 🟢 -- [Rust Modules vs C# Namespaces](ch08-crates-and-modules.md#rust-modules-vs-c-namespaces) -- [Crates vs .NET Assemblies](ch08-crates-and-modules.md#crates-vs-net-assemblies) -- [Package Management: Cargo vs NuGet](ch08-1-package-management-cargo-vs-nuget.md#package-management-cargo-vs-nuget) - -### 9. Error Handling 🟡 -- [Exceptions vs `Result<T, E>`](ch09-error-handling.md#exceptions-vs-resultt-e) -- [The ? Operator](ch09-error-handling.md#the--operator-propagating-errors-concisely) -- [Custom Error Types](ch06-1-exhaustive-matching-and-null-safety.md#custom-error-types) -- [Crate-Level Error Types and Result Aliases](ch09-1-crate-level-error-types-and-result-alias.md#crate-level-error-types-and-result-aliases) -- [Error Recovery Patterns](ch09-1-crate-level-error-types-and-result-alias.md#error-recovery-patterns) - -### 10. Traits and Generics 🟡 -- [Traits vs Interfaces](ch10-traits-and-generics.md#traits---rusts-interfaces) -- [Inheritance vs Composition](ch10-2-inheritance-vs-composition.md#inheritance-vs-composition) -- [Generic Constraints: where vs trait bounds](ch10-1-generic-constraints.md#generic-constraints-where-vs-trait-bounds) -- [Common Standard Library Traits](ch10-traits-and-generics.md#common-standard-library-traits) - -### 11. From and Into Traits 🟡 -- [Type Conversions in Rust](ch11-from-and-into-traits.md#type-conversions-in-rust) -- [Implementing From for Custom Types](ch11-from-and-into-traits.md#rust-from-and-into) - -### 12. Closures and Iterators 🟡 -- [Rust Closures](ch12-closures-and-iterators.md#rust-closures) -- [LINQ vs Rust Iterators](ch12-closures-and-iterators.md#linq-vs-rust-iterators) -- [Macros Primer](ch12-1-macros-primer.md#macros-code-that-writes-code) +# Оглавление + +## Часть I — Основы + +### 1. Введение и мотивация 🟢 +- [Аргументы в пользу Rust для разработчиков C#](ch01-introduction-and-motivation.md#аргументы-в-пользу-rust-для-разработчиков-c) +- [Типичные болевые точки C#, которые решает Rust](ch01-introduction-and-motivation.md#типичные-болевые-точки-c-которые-решает-rust) +- [Когда выбирать Rust вместо C#](ch01-introduction-and-motivation.md#когда-выбирать-rust-вместо-c) +- [Сравнение философий языков](ch01-introduction-and-motivation.md#сравнение-философий-языков) +- [Краткая справка: Rust против C#](ch01-introduction-and-motivation.md#краткая-справка-rust-против-c) + +### 2. Начало работы 🟢 +- [Установка и настройка](ch02-getting-started.md#установка-и-настройка) +- [Первая программа на Rust](ch02-getting-started.md#первая-программа-на-rust) +- [Cargo против NuGet/MSBuild](ch02-getting-started.md#cargo-против-nugetmsbuild) +- [Чтение ввода и аргументов командной строки](ch02-getting-started.md#чтение-ввода-и-аргументов-командной-строки) +- [Ключевые слова Rust *(необязательный справочник — обращайтесь по необходимости)*](ch02-1-essential-keywords-reference.md#ключевые-слова-rust-для-разработчиков-c) + +### 3. Встроенные типы и переменные 🟢 +- [Переменные и изменяемость](ch03-built-in-types-and-variables.md#переменные-и-изменяемость) +- [Сравнение примитивных типов](ch03-built-in-types-and-variables.md#примитивные-типы) +- [Типы строк: String и &str](ch03-built-in-types-and-variables.md#типы-строк-string-и-str) +- [Вывод и форматирование строк](ch03-built-in-types-and-variables.md#вывод-и-форматирование-строк) +- [Приведение типов и преобразования](ch03-built-in-types-and-variables.md#приведение-типов-и-преобразования) +- [Истинная неизменяемость против иллюзий record](ch03-1-true-immutability-vs-record-illusions.md#истинная-неизменяемость-против-иллюзий-record) + +### 4. Управление потоком выполнения 🟢 +- [Функции и методы](ch04-control-flow.md#функции-и-методы) +- [Выражение против инструкции (важно!)](ch04-control-flow.md#выражение-против-инструкции-важно) +- [Условные инструкции](ch04-control-flow.md#условные-инструкции) +- [Циклы](ch04-control-flow.md#циклы) + +### 5. Структуры данных и коллекции 🟢 +- [Кортежи и деструктуризация](ch05-data-structures-and-collections.md#кортежи-и-деструктуризация) +- [Массивы и срезы](ch05-data-structures-and-collections.md#массивы-и-срезы) +- [Структуры против классов](ch05-data-structures-and-collections.md#структуры-против-классов) +- [Паттерны конструкторов](ch05-1-constructor-patterns.md#паттерны-конструкторов) +- [`Vec<T>` против `List<T>`](ch05-2-collections-vec-hashmap-and-iterators.md#vect-против-listt) +- [HashMap против Dictionary](ch05-2-collections-vec-hashmap-and-iterators.md#hashmap-против-dictionary) + +### 6. Перечисления и сопоставление с образцом 🟡 +- [Алгебраические типы данных против объединений C#](ch06-enums-and-pattern-matching.md#алгебраические-типы-данных-против-объединений-c) +- [Исчерпывающее сопоставление с образцом](ch06-1-exhaustive-matching-and-null-safety.md#исчерпывающее-сопоставление-с-образцом-гарантии-компилятора-против-ошибок-времени-выполнения) +- [`Option<T>` для null-безопасности](ch06-1-exhaustive-matching-and-null-safety.md#null-безопасность-nullablet-против-optiont) +- [Защитные условия и продвинутые паттерны](ch06-enums-and-pattern-matching.md#защитные-условия-и-продвинутые-паттерны) + +### 7. Владение и заимствование 🟡 +- [Понимание владения](ch07-ownership-and-borrowing.md#понимание-владения) +- [Семантика перемещения против семантики ссылок](ch07-ownership-and-borrowing.md#семантика-перемещения) +- [Основы заимствования](ch07-ownership-and-borrowing.md#основы-заимствования) +- [Глубокое погружение в безопасность памяти](ch07-1-memory-safety-deep-dive.md#ссылки-против-указателей) +- [Глубокое погружение во времена жизни](ch07-2-lifetimes-deep-dive.md#времена-жизни-как-сообщить-компилятору-сколько-живут-ссылки) 🔴 +- [Умные указатели, Drop и Deref](ch07-3-smart-pointers-beyond-single-ownership.md#умные-указатели-когда-единоличного-владения-недостаточно) 🔴 + +### 8. Крейты и модули 🟢 +- [Модули Rust против пространств имён C#](ch08-crates-and-modules.md#модули-rust-против-пространств-имён-c) +- [Крейты против сборок .NET](ch08-crates-and-modules.md#крейты-против-сборок-net) +- [Управление пакетами: Cargo против NuGet](ch08-1-package-management-cargo-vs-nuget.md#управление-пакетами-cargo-против-nuget) + +### 9. Обработка ошибок 🟡 +- [Исключения против `Result<T, E>`](ch09-error-handling.md#исключения-против-resultt-e) +- [Оператор `?`](ch09-error-handling.md#оператор--краткое-распространение-ошибок) +- [Пользовательские типы ошибок](ch06-1-exhaustive-matching-and-null-safety.md#собственные-типы-ошибок) +- [Типы ошибок уровня крейта и псевдонимы Result](ch09-1-crate-level-error-types-and-result-alias.md#типы-ошибок-уровня-крейта-и-псевдонимы-result) +- [Паттерны восстановления после ошибок](ch09-1-crate-level-error-types-and-result-alias.md#паттерны-восстановления-после-ошибок) + +### 10. Трейты и обобщения 🟡 +- [Трейты против интерфейсов](ch10-traits-and-generics.md#трейты--интерфейсы-rust) +- [Наследование против композиции](ch10-2-inheritance-vs-composition.md#наследование-против-композиции) +- [Ограничения обобщений: where и ограничения трейтов](ch10-1-generic-constraints.md#ограничения-обобщений-where-и-ограничения-трейтов) +- [Распространённые трейты стандартной библиотеки](ch10-traits-and-generics.md#распространённые-трейты-стандартной-библиотеки) + +### 11. Трейты From и Into 🟡 +- [Преобразования типов в Rust](ch11-from-and-into-traits.md#преобразования-типов-в-rust) +- [Реализация From для собственных типов](ch11-from-and-into-traits.md#from-и-into-в-rust) + +### 12. Замыкания и итераторы 🟡 +- [Замыкания в Rust](ch12-closures-and-iterators.md#замыкания-в-rust) +- [LINQ против итераторов Rust](ch12-closures-and-iterators.md#linq-против-итераторов-rust) +- [Введение в макросы](ch12-1-macros-primer.md#макросы-код-который-пишет-код) --- -## Part II — Concurrency & Systems +## Часть II — Конкурентность и системное программирование -### 13. Concurrency 🔴 -- [Thread Safety: Convention vs Type System Guarantees](ch13-concurrency.md#thread-safety-convention-vs-type-system-guarantees) -- [async/await: C# Task vs Rust Future](ch13-1-asyncawait-deep-dive.md#async-programming-c-task-vs-rust-future) -- [Cancellation Patterns](ch13-1-asyncawait-deep-dive.md#cancellation-cancellationtoken-vs-drop--select) -- [Pin and tokio::spawn](ch13-1-asyncawait-deep-dive.md#pin-why-rust-async-has-a-concept-c-doesnt) +### 13. Конкурентность 🔴 +- [Потокобезопасность: соглашение против гарантий системы типов](ch13-concurrency.md#потокобезопасность-соглашение-против-гарантий-системы-типов) +- [async/await: Task в C# против Future в Rust](ch13-1-asyncawait-deep-dive.md#асинхронное-программирование-task-в-c-против-future-в-rust) +- [Отмена операций: CancellationToken против Drop и select!](ch13-1-asyncawait-deep-dive.md#отмена-cancellationtoken-против-drop--select) +- [Pin и tokio::spawn](ch13-1-asyncawait-deep-dive.md#pin-понятие-которого-в-c-нет) -### 14. Unsafe Rust, FFI, and Testing 🟡 -- [When and Why to Use Unsafe](ch14-unsafe-rust-and-ffi.md#when-you-need-unsafe) -- [Interop with C# via FFI](ch14-unsafe-rust-and-ffi.md#interop-with-c-via-ffi) -- [Testing in Rust vs C#](ch14-1-testing.md#testing-in-rust-vs-c) -- [Property Testing and Mocking](ch14-1-testing.md#property-testing-proving-correctness-at-scale) +### 14. Unsafe Rust, FFI и тестирование 🟡 +- [Когда нужен unsafe](ch14-unsafe-rust-and-ffi.md#когда-нужен-unsafe) +- [Взаимодействие с C# через FFI](ch14-unsafe-rust-and-ffi.md#взаимодействие-с-c-через-ffi) +- [Тестирование в Rust и C#](ch14-1-testing.md#тестирование-в-rust-и-c) +- [Property-тестирование и моки](ch14-1-testing.md#property-тестирование-доказательство-корректности-в-масштабе) --- -## Part III — Migration & Best Practices +## Часть III — Миграция и лучшие практики -### 15. Migration Patterns and Case Studies 🟡 -- [Common C# Patterns in Rust](ch15-migration-patterns-and-case-studies.md#common-c-patterns-in-rust) -- [Essential Crates for C# Developers](ch15-1-essential-crates-for-c-developers.md#essential-crates-for-c-developers) -- [Incremental Adoption Strategy](ch15-2-incremental-adoption-strategy.md#incremental-adoption-strategy) +### 15. Паттерны миграции и разборы случаев 🟡 +- [Распространённые паттерны C# в Rust](ch15-migration-patterns-and-case-studies.md#распространённые-паттерны-c-в-rust) +- [Основные крейты для разработчиков C#](ch15-1-essential-crates-for-c-developers.md#основные-крейты-для-разработчиков-c) +- [Стратегия постепенного внедрения](ch15-2-incremental-adoption-strategy.md#стратегия-постепенного-внедрения) -### 16. Best Practices and Reference 🟡 -- [Idiomatic Rust for C# Developers](ch16-best-practices.md#best-practices-for-c-developers) -- [Performance Comparison: Managed vs Native](ch16-1-performance-comparison-and-migration.md#performance-comparison-managed-vs-native) -- [Common Pitfalls and Solutions](ch16-2-learning-path-and-resources.md#common-pitfalls-for-c-developers) -- [Learning Path and Resources](ch16-2-learning-path-and-resources.md#learning-path-and-next-steps) -- [Rust Tooling Ecosystem](ch16-3-rust-tooling-ecosystem.md#essential-rust-tooling-for-c-developers) +### 16. Лучшие практики и справочник 🟡 +- [Идиоматичный Rust для разработчиков C#](ch16-best-practices.md#лучшие-практики-для-разработчиков-c) +- [Сравнение производительности: управляемый и нативный код](ch16-1-performance-comparison-and-migration.md#сравнение-производительности-управляемый-и-нативный-код) +- [Частые ошибки и решения](ch16-2-learning-path-and-resources.md#типичные-ловушки-для-разработчиков-c) +- [Путь обучения и следующие шаги](ch16-2-learning-path-and-resources.md#путь-обучения-и-следующие-шаги) +- [Экосистема инструментов Rust](ch16-3-rust-tooling-ecosystem.md#основные-инструменты-rust-для-разработчиков-c) --- -## Capstone - -### 17. Capstone Project 🟡 -- [Build a CLI Weather Tool](ch17-capstone-project.md#capstone-project-build-a-cli-weather-tool) — combines structs, traits, error handling, async, modules, serde, and testing into a working application - +## Итоговый проект +### 17. Итоговый проект 🟡 +- [Создание CLI-утилиты для погоды](ch17-capstone-project.md#итоговый-проект-cli-утилита-для-погоды-на-rust) — объединяет структуры, трейты, обработку ошибок, async, модули, serde и тестирование в одном работающем приложении diff --git a/csharp-book/src/ch07-ownership-and-borrowing.md b/csharp-book/src/ch07-ownership-and-borrowing.md index 16d9f8b..93ee23a 100644 --- a/csharp-book/src/ch07-ownership-and-borrowing.md +++ b/csharp-book/src/ch07-ownership-and-borrowing.md @@ -29,7 +29,7 @@ public void ProcessList(List<int> list) ### Правила владения в Rust 1. **У каждого значения ровно один владелец** (если вы не выберете разделяемое владение через `Rc<T>`/`Arc<T>` — см. [Умные указатели](ch07-3-smart-pointers-beyond-single-ownership.md)) -2. **Когда владелец выходит из области видимости, значение уничтожается** (детерминированная очистка — см. [Drop](ch07-3-smart-pointers-beyond-single-ownership.md#drop-rusts-idisposable)) +2. **Когда владелец выходит из области видимости, значение уничтожается** (детерминированная очистка — см. [Drop](ch07-3-smart-pointers-beyond-single-ownership.md#drop-аналог-idisposable-в-rust)) 3. **Владение можно передать (переместить)** ```rust diff --git a/csharp-book/src/ch13-1-asyncawait-deep-dive.md b/csharp-book/src/ch13-1-asyncawait-deep-dive.md index 000c009..ebb678b 100644 --- a/csharp-book/src/ch13-1-asyncawait-deep-dive.md +++ b/csharp-book/src/ch13-1-asyncawait-deep-dive.md @@ -1,74 +1,74 @@ -## Async Programming: C# Task vs Rust Future +## Асинхронное программирование: Task в C# против Future в Rust -> **What you'll learn:** Rust's lazy `Future` vs C#'s eager `Task`, the executor model (tokio), -> cancellation via `Drop` + `select!` vs `CancellationToken`, and real-world patterns for concurrent requests. +> **Что вы узнаете:** ленивый `Future` Rust против энергичного `Task` в C#, модель исполнителя (tokio), +> отмену через `Drop` + `select!` против `CancellationToken`, а также практические паттерны для конкурентных запросов. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -C# developers are deeply familiar with `async`/`await`. Rust uses the same keywords but with a fundamentally different execution model. +Разработчики C# хорошо знакомы с `async`/`await`. Rust использует те же ключевые слова, но модель выполнения принципиально иная. -### The Executor Model +### Модель исполнителя ```csharp -// C# — The runtime provides a built-in thread pool and task scheduler -// async/await "just works" out of the box +// C# — рантайм предоставляет встроенный пул потоков и планировщик задач +// async/await «просто работает» из коробки public async Task<string> FetchDataAsync(string url) { using var client = new HttpClient(); - return await client.GetStringAsync(url); // Scheduled by .NET thread pool + return await client.GetStringAsync(url); // Планируется пулом потоков .NET } -// .NET manages the thread pool, task scheduling, and synchronization context +// .NET управляет пулом потоков, планированием задач и контекстом синхронизации ``` ```rust -// Rust — No built-in async runtime. You choose an executor. -// The most popular is tokio. +// Rust — встроенного асинхронного рантайма нет. Исполнитель выбираете вы. +// Самый популярный — tokio. async fn fetch_data(url: &str) -> Result<String, reqwest::Error> { let body = reqwest::get(url).await?.text().await?; Ok(body) } -// You MUST have a runtime to execute async code: -#[tokio::main] // This macro sets up the tokio runtime +// Чтобы выполнить асинхронный код, нужен рантайм: +#[tokio::main] // Этот макрос настраивает рантайм tokio async fn main() { let data = fetch_data("https://example.com").await.unwrap(); println!("{}", &data[..100]); } ``` -### Future vs Task +### Future против Task | | C# `Task<T>` | Rust `Future<Output = T>` | |---|---|---| -| **Execution** | Starts immediately when created | **Lazy** — does nothing until `.await`ed | -| **Runtime** | Built-in (CLR thread pool) | External (tokio, async-std, etc.) | -| **Cancellation** | `CancellationToken` | Drop the `Future` (or `tokio::select!`) | -| **State machine** | Compiler-generated | Compiler-generated | -| **Size** | Heap-allocated | Stack-allocated until boxed | +| **Выполнение** | Запускается сразу при создании | **Ленивый** — ничего не делает, пока его не `await`-ят | +| **Рантайм** | Встроенный (пул потоков CLR) | Внешний (tokio, async-std и т. д.) | +| **Отмена** | `CancellationToken` | Уничтожить `Future` (или `tokio::select!`) | +| **Конечный автомат** | Генерируется компилятором | Генерируется компилятором | +| **Размещение** | В куче | На стеке, пока не упаковано в `Box` | ```rust -// IMPORTANT: Futures are lazy in Rust! +// ВАЖНО: Future в Rust ленивые! async fn compute() -> i32 { println!("Computing!"); 42 } -let future = compute(); // Nothing printed! Future not polled yet. -let result = future.await; // NOW "Computing!" is printed +let future = compute(); // Ничего не выведено! Future ещё не опрошен. +let result = future.await; // ТЕПЕРЬ выводится "Computing!" ``` ```csharp -// C# Tasks start immediately! -var task = ComputeAsync(); // "Computing!" printed immediately -var result = await task; // Just waits for completion +// Task в C# запускаются сразу! +var task = ComputeAsync(); // "Computing!" выводится немедленно +var result = await task; // Просто ждёт завершения ``` -### Cancellation: CancellationToken vs Drop / select! +### Отмена: CancellationToken против Drop / select! ```csharp -// C# — Cooperative cancellation with CancellationToken +// C# — кооперативная отмена через CancellationToken public async Task ProcessAsync(CancellationToken ct) { while (!ct.IsCancellationRequested) { - await Task.Delay(1000, ct); // Throws if cancelled + await Task.Delay(1000, ct); // Бросает исключение, если отменено DoWork(); } } @@ -78,7 +78,7 @@ await ProcessAsync(cts.Token); ``` ```rust -// Rust — Cancellation by dropping the future, or with tokio::select! +// Rust — отмена через уничтожение future или через tokio::select! use tokio::time::{sleep, Duration}; async fn process() { @@ -88,21 +88,21 @@ async fn process() { } } -// Timeout pattern with select! +// Паттерн таймаута с select! async fn run_with_timeout() { tokio::select! { _ = process() => { println!("Completed"); } _ = sleep(Duration::from_secs(5)) => { println!("Timed out!"); } } - // When select! picks the timeout branch, the process() future is DROPPED - // — automatic cleanup, no CancellationToken needed + // Когда select! выбирает ветку таймаута, future process() УНИЧТОЖАЕТСЯ + // — автоматическая очистка, CancellationToken не нужен } ``` -### Real-World Pattern: Concurrent Requests with Timeout +### Практический паттерн: конкурентные запросы с таймаутом ```csharp -// C# — Concurrent HTTP requests with timeout +// C# — конкурентные HTTP-запросы с таймаутом public async Task<string[]> FetchAllAsync(string[] urls, CancellationToken ct) { var tasks = urls.Select(url => httpClient.GetStringAsync(url, ct)); @@ -111,7 +111,7 @@ public async Task<string[]> FetchAllAsync(string[] urls, CancellationToken ct) ``` ```rust -// Rust — Concurrent requests with tokio::join! or futures::join_all +// Rust — конкурентные запросы через tokio::join! или futures::join_all use futures::future::join_all; async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> { @@ -125,7 +125,7 @@ async fn fetch_all(urls: &[&str]) -> Vec<Result<String, reqwest::Error>> { results } -// With timeout: +// С таймаутом: async fn fetch_all_with_timeout(urls: &[&str]) -> Result<Vec<String>, &'static str> { tokio::time::timeout( Duration::from_secs(10), @@ -144,17 +144,17 @@ async fn fetch_all_with_timeout(urls: &[&str]) -> Result<Vec<String>, &'static s ``` <details> -<summary><strong>🏋️ Exercise: Async Timeout Pattern</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: паттерн асинхронного таймаута</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Write an async function that fetches from two URLs concurrently, returns whichever responds first, and cancels the other. (This is `Task.WhenAny` in C#.) +**Задача**: напишите асинхронную функцию, которая запрашивает два URL конкурентно, возвращает тот результат, который пришёл первым, а второй запрос отменяет. (В C# это `Task.WhenAny`.) <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use tokio::time::{sleep, Duration}; -// Simulated async fetch +// Имитация асинхронного запроса async fn fetch(url: &str, delay_ms: u64) -> String { sleep(Duration::from_millis(delay_ms)).await; format!("Response from {url}") @@ -171,7 +171,7 @@ async fn fetch_first(url1: &str, url2: &str) -> String { result } } - // The losing branch's future is automatically dropped (cancelled) + // Future проигравшей ветки автоматически уничтожается (отменяется) } #[tokio::main] @@ -181,71 +181,71 @@ async fn main() { } ``` -**Key takeaway**: `tokio::select!` is Rust's equivalent of `Task.WhenAny` — it races multiple futures, completes when the first one finishes, and drops (cancels) the rest. +**Ключевая мысль**: `tokio::select!` — это аналог `Task.WhenAny` в Rust. Он гонит несколько future, завершается, когда первое из них готово, и уничтожает (отменяет) остальные. </details> </details> -### Spawning Independent Tasks with `tokio::spawn` +### Запуск независимых задач через `tokio::spawn` -In C#, `Task.Run` launches work that runs independently of the caller. Rust's equivalent is `tokio::spawn`: +В C# `Task.Run` запускает работу, которая выполняется независимо от вызывающего кода. Аналог в Rust — `tokio::spawn`: ```rust use tokio::task; async fn background_work() { - // Runs independently — even if the caller's future is dropped + // Выполняется независимо — даже если future вызывающего кода уничтожен let handle = task::spawn(async { tokio::time::sleep(Duration::from_secs(2)).await; 42 }); - // Do other work while the spawned task runs... + // Делаем другую работу, пока порождённая задача выполняется... println!("Doing other work"); - // Await the result when you need it + // Ждём результат, когда он понадобится let result = handle.await.unwrap(); // 42 } ``` ```csharp -// C# equivalent +// Аналог в C# var task = Task.Run(async () => { await Task.Delay(2000); return 42; }); -// Do other work... +// Делаем другую работу... var result = await task; ``` -**Key difference**: A regular `async {}` block is lazy — it does nothing until awaited. `tokio::spawn` launches it on the runtime immediately, like C#'s `Task.Run`. +**Ключевое отличие**: обычный блок `async {}` ленивый — он ничего не делает, пока его не `await`-ят. `tokio::spawn` сразу запускает его в рантайме, как `Task.Run` в C#. -### Pin: Why Rust Async Has a Concept C# Doesn't +### Pin: понятие, которого в C# нет -C# developers never encounter `Pin` — the CLR's garbage collector moves objects freely and updates all references automatically. Rust has no GC. When the compiler transforms an `async fn` into a state machine, that struct may contain internal pointers to its own fields. Moving the struct would invalidate those pointers. +Разработчики C# никогда не сталкиваются с `Pin` — сборщик мусора CLR свободно перемещает объекты и автоматически обновляет все ссылки. В Rust сборщика мусора нет. Когда компилятор превращает `async fn` в конечный автомат, эта структура может содержать внутренние указатели на собственные поля. Перемещение структуры сделало бы эти указатели недействительными. -`Pin<T>` is a wrapper that says: **"this value will not be moved in memory."** +`Pin<T>` — это обёртка, которая говорит: **«это значение не будет перемещено в памяти».** ```rust -// You'll see Pin in these contexts: +// Pin встречается в таких контекстах: trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>; - // ^^^^^^^^^^^^^^ pinned — internal references stay valid + // ^^^^^^^^^^^^^^ закреплено — внутренние ссылки остаются действительными } -// Returning a boxed future from a trait: +// Возврат future в упаковке Box из трейта: fn make_future() -> Pin<Box<dyn Future<Output = i32> + Send>> { Box::pin(async { 42 }) } ``` -**In practice, you almost never write `Pin` yourself.** The `async fn` and `.await` syntax handles it. You'll encounter it only in: -- Compiler error messages (follow the suggestion) -- `tokio::select!` (use the `pin!()` macro) -- Trait methods returning `dyn Future` (use `Box::pin(async { ... })`) +**На практике вы почти никогда не пишете `Pin` сами.** Синтаксис `async fn` и `.await` справляется с этим. С `Pin` вы столкнётесь только в таких случаях: +- Сообщения об ошибках компилятора (следуйте подсказке) +- `tokio::select!` (используйте макрос `pin!()`) +- Методы трейтов, возвращающие `dyn Future` (используйте `Box::pin(async { ... })`) -> **Want the deep dive?** The companion [Async Rust Training](../../async-book/src/ch04-pin-and-unpin.md) covers Pin, Unpin, self-referential structs, and structural pinning in full detail. +> **Хотите глубже?** Сопутствующее руководство [Async Rust Training](../../async-book/src/ch04-pin-and-unpin.md) подробно рассматривает Pin, Unpin, самоссылающиеся структуры и структурное закрепление. *** diff --git a/csharp-book/src/ch13-concurrency.md b/csharp-book/src/ch13-concurrency.md index 1ef85ae..44a825d 100644 --- a/csharp-book/src/ch13-concurrency.md +++ b/csharp-book/src/ch13-concurrency.md @@ -1,72 +1,72 @@ -## Thread Safety: Convention vs Type System Guarantees +## Потокобезопасность: соглашение против гарантий системы типов -> **What you'll learn:** How Rust enforces thread safety at compile time vs C#'s convention-based approach, -> `Arc<Mutex<T>>` vs `lock`, channels vs `ConcurrentQueue`, `Send`/`Sync` traits, -> scoped threads, and the bridge to async/await. +> **Что вы узнаете:** как Rust обеспечивает потокобезопасность на этапе компиляции в отличие от подхода C# на основе соглашений, +> `Arc<Mutex<T>>` против `lock`, каналы против `ConcurrentQueue`, трейты `Send`/`Sync`, +> ограниченные по области потоки и переход к async/await. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -> **Deep dive**: For production async patterns (stream processing, graceful shutdown, connection pooling, cancellation safety), see the companion [Async Rust Training](../../async-book/src/summary.md) guide. +> **Глубокое погружение**: о производственных асинхронных паттернах (обработка потоков, корректное завершение, пулы соединений, безопасность отмены) см. сопутствующее руководство [Async Rust Training](../../async-book/src/summary.md). > -> **Prerequisites**: [Ownership & Borrowing](ch07-ownership-and-borrowing.md) and [Smart Pointers](ch07-3-smart-pointers-beyond-single-ownership.md) (Rc vs Arc decision tree). +> **Предварительные требования**: [Владение и заимствование](ch07-ownership-and-borrowing.md) и [Умные указатели](ch07-3-smart-pointers-beyond-single-ownership.md) (дерево решений Rc против Arc). -### C# - Thread Safety by Convention +### C# — потокобезопасность по соглашению ```csharp -// C# collections aren't thread-safe by default +// Коллекции в C# по умолчанию не потокобезопасны public class UserService { private readonly List<string> items = new(); private readonly Dictionary<int, User> cache = new(); - // This can cause data races: + // Это может привести к гонкам данных: public void AddItem(string item) { - items.Add(item); // Not thread-safe! + items.Add(item); // Не потокобезопасно! } - // Must use locks manually: + // Приходится вручную использовать блокировки: private readonly object lockObject = new(); public void SafeAddItem(string item) { lock (lockObject) { - items.Add(item); // Safe, but runtime overhead + items.Add(item); // Безопасно, но есть накладные расходы рантайма } - // Easy to forget the lock elsewhere + // Легко забыть о блокировке в другом месте } - // ConcurrentCollection helps but limited: + // ConcurrentCollection помогает, но возможности ограничены private readonly ConcurrentBag<string> safeItems = new(); public void ConcurrentAdd(string item) { - safeItems.Add(item); // Thread-safe but limited operations + safeItems.Add(item); // Потокобезопасно, но операции ограничены } - // Complex shared state management + // Сложное управление разделяемым состоянием private readonly ConcurrentDictionary<int, User> threadSafeCache = new(); private volatile bool isShutdown = false; public async Task ProcessUser(int userId) { - if (isShutdown) return; // Race condition possible! + if (isShutdown) return; // Возможна гонка! var user = await GetUser(userId); - threadSafeCache.TryAdd(userId, user); // Must remember which collections are safe + threadSafeCache.TryAdd(userId, user); // Нужно помнить, какие коллекции безопасны } - // Thread-local storage requires careful management + // Локальное для потока хранилище требует аккуратного управления private static readonly ThreadLocal<Random> threadLocalRandom = new ThreadLocal<Random>(() => new Random()); public int GetRandomNumber() { - return threadLocalRandom.Value.Next(); // Safe but manual management + return threadLocalRandom.Value.Next(); // Безопасно, но управление вручную } } -// Event handling with potential race conditions +// Обработка событий с возможными гонками public class EventProcessor { public event Action<string> DataReceived; @@ -74,28 +74,28 @@ public class EventProcessor public void OnDataReceived(string data) { - // Race condition - event might be null between check and invocation + // Гонка: обработчик может стать null между проверкой и вызовом if (DataReceived != null) { DataReceived(data); } - // Modern C# (6+) mitigates the null race with: DataReceived?.Invoke(data); - // but the underlying event-delegate model still allows races on the list below + // Современный C# (6+) смягчает гонку на null через: DataReceived?.Invoke(data); + // но модель событий-делегатов по-прежнему допускает гонки на списке ниже - // Another race condition - list not thread-safe + // Ещё одна гонка — список не потокобезопасен eventLog.Add($"Processed: {data}"); } } ``` -### Rust - Thread Safety Guaranteed by Type System +### Rust — потокобезопасность гарантируется системой типов ```rust use std::sync::{Arc, Mutex, RwLock}; use std::thread; use std::collections::HashMap; use tokio::sync::{mpsc, broadcast}; -// Rust prevents data races at compile time +// Rust предотвращает гонки данных на этапе компиляции pub struct UserService { items: Arc<Mutex<Vec<String>>>, cache: Arc<RwLock<HashMap<i32, User>>>, @@ -112,10 +112,10 @@ impl UserService { pub fn add_item(&self, item: String) { let mut items = self.items.lock().unwrap(); items.push(item); - // Lock automatically released when `items` goes out of scope + // Блокировка автоматически освобождается, когда `items` выходит из области видимости } - // Multiple readers, single writer - automatically enforced + // Несколько читателей, один писатель — обеспечивается автоматически pub async fn get_user(&self, user_id: i32) -> Option<User> { let cache = self.cache.read().unwrap(); cache.get(&user_id).cloned() @@ -126,7 +126,7 @@ impl UserService { cache.insert(user_id, user); } - // Clone the Arc for thread sharing + // Клонируем Arc для совместного использования между потоками pub fn process_in_background(&self) { let items = Arc::clone(&self.items); @@ -139,7 +139,7 @@ impl UserService { } } -// Channel-based communication - no shared state needed +// Взаимодействие через каналы — разделяемое состояние не нужно pub struct MessageProcessor { sender: mpsc::UnboundedSender<String>, } @@ -155,29 +155,29 @@ impl MessageProcessor { } } -// This won't compile - Rust prevents sharing mutable data unsafely: +// Этот код не скомпилируется — Rust не даёт небезопасно разделять изменяемые данные: fn impossible_data_race() { let mut items = vec![1, 2, 3]; - // This won't compile - cannot move `items` into multiple closures + // Не скомпилируется — нельзя переместить `items` в несколько замыканий /* thread::spawn(move || { - items.push(4); // ERROR: use of moved value + items.push(4); // ОШИБКА: использование перемещённого значения }); thread::spawn(move || { - items.push(5); // ERROR: use of moved value + items.push(5); // ОШИБКА: использование перемещённого значения }); */ } -// Safe concurrent data processing +// Безопасная параллельная обработка данных use rayon::prelude::*; fn parallel_processing() { let data = vec![1, 2, 3, 4, 5]; - // Parallel iteration - guaranteed thread-safe + // Параллельная итерация — потокобезопасность гарантирована let results: Vec<i32> = data .par_iter() .map(|&x| x * x) @@ -186,11 +186,11 @@ fn parallel_processing() { println!("{:?}", results); } -// Async concurrency with message passing +// Асинхронная конкурентность с передачей сообщений async fn async_message_passing() { let (tx, mut rx) = mpsc::channel(100); - // Producer task + // Задача-производитель let producer = tokio::spawn(async move { for i in 0..10 { if tx.send(i).await.is_err() { @@ -199,14 +199,14 @@ async fn async_message_passing() { } }); - // Consumer task + // Задача-потребитель let consumer = tokio::spawn(async move { while let Some(value) = rx.recv().await { println!("Received: {}", value); } }); - // Wait for both tasks + // Ждём обе задачи let (producer_result, consumer_result) = tokio::join!(producer, consumer); producer_result.unwrap(); consumer_result.unwrap(); @@ -221,15 +221,15 @@ struct User { ```mermaid graph TD - subgraph "C# Thread Safety Challenges" - CS_MANUAL["Manual synchronization"] - CS_LOCKS["lock statements"] - CS_CONCURRENT["ConcurrentCollections"] - CS_VOLATILE["volatile fields"] - CS_FORGET["😰 Easy to forget locks"] - CS_DEADLOCK["💀 Deadlock possible"] - CS_RACE["🏃 Race conditions"] - CS_OVERHEAD["⚡ Runtime overhead"] + subgraph "Проблемы потокобезопасности в C#" + CS_MANUAL["Ручная синхронизация"] + CS_LOCKS["Инструкции lock"] + CS_CONCURRENT["Потокобезопасные коллекции"] + CS_VOLATILE["Поля volatile"] + CS_FORGET["😰 Легко забыть блокировки"] + CS_DEADLOCK["💀 Возможны взаимные блокировки"] + CS_RACE["🏃 Гонки данных"] + CS_OVERHEAD["⚡ Накладные расходы рантайма"] CS_MANUAL --> CS_LOCKS CS_MANUAL --> CS_CONCURRENT @@ -240,15 +240,15 @@ graph TD CS_LOCKS --> CS_OVERHEAD end - subgraph "Rust Type System Guarantees" - RUST_OWNERSHIP["Ownership system"] - RUST_BORROWING["Borrow checker"] - RUST_SEND["Send trait"] - RUST_SYNC["Sync trait"] + subgraph "Гарантии системы типов Rust" + RUST_OWNERSHIP["Система владения"] + RUST_BORROWING["Проверщик заимствований"] + RUST_SEND["Трейт Send"] + RUST_SYNC["Трейт Sync"] RUST_ARC["Arc<Mutex<T>>"] - RUST_CHANNELS["Message passing"] - RUST_SAFE["✅ Data races impossible"] - RUST_FAST["⚡ Zero-cost abstractions"] + RUST_CHANNELS["Передача сообщений"] + RUST_SAFE["✅ Гонки данных невозможны"] + RUST_FAST["⚡ Абстракции с нулевой стоимостью"] RUST_OWNERSHIP --> RUST_BORROWING RUST_BORROWING --> RUST_SEND @@ -270,12 +270,12 @@ graph TD <details> -<summary><strong>🏋️ Exercise: Thread-Safe Counter</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: потокобезопасный счётчик</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Implement a thread-safe counter that can be incremented from 10 threads simultaneously. Each thread increments 1000 times. The final count should be exactly 10,000. +**Задача**: реализуйте потокобезопасный счётчик, который могут одновременно инкрементировать 10 потоков. Каждый поток увеличивает его 1000 раз. Итоговое значение должно быть ровно 10 000. <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use std::sync::{Arc, Mutex}; @@ -301,7 +301,7 @@ fn main() { } ``` -**Or with atomics (faster, no locking):** +**Или с атомарными типами (быстрее, без блокировок):** ```rust use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; @@ -323,28 +323,28 @@ fn main() { } ``` -**Key takeaway**: `Arc<Mutex<T>>` is the general pattern. For simple counters, `AtomicU64` avoids lock overhead entirely. +**Ключевая мысль**: `Arc<Mutex<T>>` — общий паттерн. Для простых счётчиков `AtomicU64` полностью избавляет от накладных расходов на блокировки. </details> </details> -### Why Rust prevents data races: Send and Sync +### Почему Rust предотвращает гонки данных: Send и Sync -Rust uses two marker traits to enforce thread safety **at compile time** — there is no C# equivalent: +Rust использует два маркерных трейта, чтобы обеспечивать потокобезопасность **на этапе компиляции** — аналога в C# нет: -- `Send`: A type can be safely **transferred** to another thread (e.g., moved into a closure passed to `thread::spawn`) -- `Sync`: A type can be safely **shared** (via `&T`) between threads +- `Send`: тип можно безопасно **передать** в другой поток (например, переместить в замыкание, переданное в `thread::spawn`) +- `Sync`: тип можно безопасно **разделять** (через `&T`) между потоками -Most types are automatically `Send + Sync`. Notable exceptions: -- `Rc<T>` is **neither** Send nor Sync — the compiler will refuse to let you pass it to `thread::spawn` (use `Arc<T>` instead) -- `Cell<T>` and `RefCell<T>` are **not** Sync — use `Mutex<T>` or `RwLock<T>` for thread-safe interior mutability -- Raw pointers (`*const T`, `*mut T`) are **neither** Send nor Sync +Большинство типов автоматически реализуют `Send + Sync`. Заметные исключения: +- `Rc<T>` **не** реализует ни Send, ни Sync — компилятор не даст передать его в `thread::spawn` (используйте `Arc<T>`) +- `Cell<T>` и `RefCell<T>` **не** реализуют Sync — для потокобезопасной внутренней изменяемости используйте `Mutex<T>` или `RwLock<T>` +- Сырые указатели (`*const T`, `*mut T`) **не** реализуют ни Send, ни Sync -In C#, `List<T>` is not thread-safe but the compiler won't stop you from sharing it across threads. In Rust, the equivalent mistake is a **compile error**, not a runtime race condition. +В C# `List<T>` не потокобезопасен, но компилятор не помешает разделить его между потоками. В Rust такая же ошибка — это **ошибка компиляции**, а не гонка во время выполнения. -### Scoped threads: borrowing from the stack +### Ограниченные по области потоки: заимствование из стека -`thread::scope()` lets spawned threads borrow local variables — no `Arc` needed: +`thread::scope()` позволяет порождённым потокам заимствовать локальные переменные — `Arc` не нужен: ```rust use std::thread; @@ -352,30 +352,30 @@ use std::thread; fn main() { let data = vec![1, 2, 3, 4, 5]; - // Scoped threads can borrow 'data' — scope waits for all threads to finish + // Потоки в области видимости могут заимствовать 'data' — scope ждёт завершения всех потоков thread::scope(|s| { s.spawn(|| println!("Thread 1: {data:?}")); s.spawn(|| println!("Thread 2: sum = {}", data.iter().sum::<i32>())); }); - // 'data' is still valid here — threads are guaranteed to have finished + // 'data' здесь по-прежнему действителен — потоки гарантированно завершились } ``` -This is similar to C#'s `Parallel.ForEach` in that the calling code waits for completion, but Rust's borrow checker **proves** there are no data races at compile time. +Это похоже на `Parallel.ForEach` в C# тем, что вызывающий код ждёт завершения, но проверщик заимствований Rust **доказывает** отсутствие гонок данных на этапе компиляции. -### Bridging to async/await +### Переход к async/await -C# developers typically reach for `Task` and `async/await` rather than raw threads. Rust has both paradigms: +Разработчики C# обычно используют `Task` и `async/await`, а не потоки напрямую. В Rust есть обе парадигмы: -| C# | Rust | When to use | +| C# | Rust | Когда использовать | |----|------|-------------| -| `Thread` | `std::thread::spawn` | CPU-bound work, OS thread per task | -| `Task.Run` | `tokio::spawn` | Async task on a runtime | -| `async/await` | `async/await` | I/O-bound concurrency | -| `lock` | `Mutex<T>` | Sync mutual exclusion | -| `SemaphoreSlim` | `tokio::sync::Semaphore` | Async concurrency limiting | -| `Interlocked` | `std::sync::atomic` | Lock-free atomic operations | -| `CancellationToken` | `tokio_util::sync::CancellationToken` | Cooperative cancellation | - -> The next chapter ([Async/Await Deep Dive](ch13-1-asyncawait-deep-dive.md)) covers Rust's async model in detail — including how it differs from C#'s `Task`-based model. +| `Thread` | `std::thread::spawn` | Задачи, ограниченные CPU; один поток ОС на задачу | +| `Task.Run` | `tokio::spawn` | Асинхронная задача в рантайме | +| `async/await` | `async/await` | Конкурентность, ограниченная вводом-выводом | +| `lock` | `Mutex<T>` | Синхронная взаимная блокировка | +| `SemaphoreSlim` | `tokio::sync::Semaphore` | Ограничение асинхронной конкурентности | +| `Interlocked` | `std::sync::atomic` | Атомарные операции без блокировок | +| `CancellationToken` | `tokio_util::sync::CancellationToken` | Кооперативная отмена | + +> Следующая глава ([Глубокое погружение в async/await](ch13-1-asyncawait-deep-dive.md)) подробно описывает асинхронную модель Rust — в том числе то, чем она отличается от модели C# на основе `Task`. diff --git a/csharp-book/src/ch14-1-testing.md b/csharp-book/src/ch14-1-testing.md index 38e87ef..ca0eaaa 100644 --- a/csharp-book/src/ch14-1-testing.md +++ b/csharp-book/src/ch14-1-testing.md @@ -1,11 +1,11 @@ -## Testing in Rust vs C# +## Тестирование в Rust и C# -> **What you'll learn:** Built-in `#[test]` vs xUnit, parameterized tests with `rstest` (like `[Theory]`), -> property testing with `proptest`, mocking with `mockall`, and async test patterns. +> **Что вы узнаете:** встроенный `#[test]` против xUnit, параметризованные тесты с `rstest` (аналог `[Theory]`), +> property-тестирование с `proptest`, моки с `mockall` и паттерны асинхронных тестов. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Unit Tests +### Модульные тесты ```csharp // C# — xUnit using Xunit; @@ -31,12 +31,12 @@ public class CalculatorTests ``` ```rust -// Rust — built-in testing, no external framework needed +// Rust — встроенное тестирование, внешний фреймворк не нужен pub fn add(a: i32, b: i32) -> i32 { a + b } -#[cfg(test)] // Only compiled during `cargo test` +#[cfg(test)] // Компилируется только при `cargo test` mod tests { - use super::*; // Import from parent module + use super::*; // Импорт из родительского модуля #[test] fn add_returns_sum() { @@ -51,14 +51,14 @@ mod tests { #[test] #[should_panic(expected = "overflow")] fn add_overflow_panics() { - let _ = add(i32::MAX, 1); // panics in debug mode + let _ = add(i32::MAX, 1); // паникует в debug-режиме } } ``` -### Parameterized Tests (like `[Theory]`) +### Параметризованные тесты (аналог `[Theory]`) ```rust -// Use the `rstest` crate for parameterized tests +// Для параметризованных тестов используйте крейт `rstest` use rstest::rstest; #[rstest] @@ -69,41 +69,41 @@ fn test_add(#[case] a: i32, #[case] b: i32, #[case] expected: i32) { assert_eq!(add(a, b), expected); } -// Fixtures — like test setup methods +// Фикстуры — аналог методов настройки тестов #[rstest] fn test_with_fixture(#[values(1, 2, 3)] x: i32) { assert!(x > 0); } ``` -### Assertions Comparison +### Сравнение утверждений -| C# (xUnit) | Rust | Notes | +| C# (xUnit) | Rust | Примечания | |-------------|------|-------| -| `Assert.Equal(expected, actual)` | `assert_eq!(expected, actual)` | Prints diff on failure | +| `Assert.Equal(expected, actual)` | `assert_eq!(expected, actual)` | При провале выводит diff | | `Assert.NotEqual(a, b)` | `assert_ne!(a, b)` | | | `Assert.True(condition)` | `assert!(condition)` | | | `Assert.Contains("sub", str)` | `assert!(str.contains("sub"))` | | -| `Assert.Throws<T>(() => ...)` | `#[should_panic]` | Or use `std::panic::catch_unwind` | -| `Assert.Null(obj)` | `assert!(option.is_none())` | No nulls — use `Option` | +| `Assert.Throws<T>(() => ...)` | `#[should_panic]` | Или используйте `std::panic::catch_unwind` | +| `Assert.Null(obj)` | `assert!(option.is_none())` | Нет null — используйте `Option` | -### Test Organization +### Организация тестов ```text my_crate/ ├── src/ -│ ├── lib.rs # Unit tests in #[cfg(test)] mod tests { } -│ └── parser.rs # Each module can have its own test module -├── tests/ # Integration tests (each file is a separate crate) -│ ├── parser_test.rs # Tests the public API as an external consumer +│ ├── lib.rs # Модульные тесты в #[cfg(test)] mod tests { } +│ └── parser.rs # У каждого модуля может быть свой тестовый модуль +├── tests/ # Интеграционные тесты (каждый файл — отдельный крейт) +│ ├── parser_test.rs # Тестирует публичный API как внешний потребитель │ └── api_test.rs -└── benches/ # Benchmarks (with criterion crate) +└── benches/ # Бенчмарки (с крейтом criterion) └── my_benchmark.rs ``` ```rust -// tests/parser_test.rs — integration test -// Can only access PUBLIC API (like testing from outside the assembly) +// tests/parser_test.rs — интеграционный тест +// Имеет доступ только к ПУБЛИЧНОМУ API (как тестирование извне сборки) use my_crate::parser; #[test] @@ -113,9 +113,9 @@ fn test_parse_valid_input() { } ``` -### Async Tests +### Асинхронные тесты ```csharp -// C# — async test with xUnit +// C# — асинхронный тест в xUnit [Fact] public async Task GetUser_ReturnsUser() { @@ -126,7 +126,7 @@ public async Task GetUser_ReturnsUser() ``` ```rust -// Rust — async test with tokio +// Rust — асинхронный тест с tokio #[tokio::test] async fn get_user_returns_user() { let service = UserService::new(); @@ -135,11 +135,11 @@ async fn get_user_returns_user() { } ``` -### Mocking with mockall +### Моки с mockall ```rust use mockall::automock; -#[automock] // Generates MockUserRepo struct +#[automock] // Генерирует структуру MockUserRepo trait UserRepo { fn find_by_id(&self, id: u32) -> Option<User>; } @@ -163,7 +163,7 @@ mod tests { ``` ```csharp -// C# — Moq equivalent +// C# — эквивалент с Moq var mock = new Mock<IUserRepo>(); mock.Setup(r => r.FindById(1)).Returns(new User { Name = "Alice" }); var service = new UserService(mock.Object); @@ -171,9 +171,9 @@ Assert.Equal("Alice", service.GetUser(1).Name); ``` <details> -<summary><strong>🏋️ Exercise: Write Comprehensive Tests</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: напишите полноценные тесты</strong> (нажмите, чтобы раскрыть)</summary> -**Challenge**: Given this function, write tests covering: happy path, empty input, numeric strings, and Unicode. +**Задача**: для этой функции напишите тесты, которые покрывают: основной сценарий, пустой ввод, числовые строки и Unicode. ```rust pub fn title_case(input: &str) -> String { @@ -191,7 +191,7 @@ pub fn title_case(input: &str) -> String { ``` <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust #[cfg(test)] @@ -225,7 +225,7 @@ mod tests { #[test] fn extra_whitespace() { - // split_whitespace handles multiple spaces + // split_whitespace обрабатывает несколько пробелов assert_eq!(title_case(" hello world "), "Hello World"); } @@ -241,20 +241,20 @@ mod tests { } ``` -**Key takeaway**: Rust's built-in test framework handles most unit testing needs. Use `rstest` for parameterized tests and `mockall` for mocking — no need for a large test framework like xUnit. +**Ключевая мысль**: встроенный тестовый фреймворк Rust покрывает большинство потребностей в модульном тестировании. Используйте `rstest` для параметризованных тестов и `mockall` для моков — большой тестовый фреймворк вроде xUnit не нужен. </details> </details> <!-- ch14a.1: Property Testing with proptest --> -## Property Testing: Proving Correctness at Scale +## Property-тестирование: доказательство корректности в масштабе -C# developers familiar with **FsCheck** will recognize property-based testing: instead of writing individual test cases, you describe *properties* that must hold for **all possible inputs**, and the framework generates thousands of random inputs to try to break them. +Разработчикам C#, знакомым с **FsCheck**, property-тестирование будет понятно сразу: вместо написания отдельных тест-кейсов вы описываете *свойства*, которые должны выполняться для **всех возможных входных данных**, а фреймворк генерирует тысячи случайных входов, чтобы их опровергнуть. -### Why Property Testing Matters +### Зачем нужно property-тестирование ```csharp -// C# — Hand-written unit tests check specific cases +// C# — написанные вручную модульные тесты проверяют конкретные случаи [Fact] public void Reverse_Twice_Returns_Original() { @@ -263,12 +263,12 @@ public void Reverse_Twice_Returns_Original() list.Reverse(); Assert.Equal(new[] { 1, 2, 3 }, list); } -// But what about empty lists? Single elements? 10,000 elements? Negative numbers? -// You'd need dozens of hand-written cases. +// Но что насчёт пустых списков? Одиночных элементов? 10 000 элементов? Отрицательных чисел? +// Понадобились бы десятки тестов, написанных вручную. ``` ```rust -// Rust — proptest generates thousands of inputs automatically +// Rust — proptest автоматически генерирует тысячи входов use proptest::prelude::*; fn reverse<T: Clone>(v: &[T]) -> Vec<T> { @@ -281,26 +281,26 @@ proptest! { let reversed_twice = reverse(&reverse(v)); prop_assert_eq!(v, &reversed_twice); } - // proptest runs this with hundreds of random Vec<i32> values: - // [], [0], [i32::MIN, i32::MAX], [42; 999], random sequences... - // If it fails, it SHRINKS to the smallest failing input! + // proptest запускает это на сотнях случайных значений Vec<i32>: + // [], [0], [i32::MIN, i32::MAX], [42; 999], случайные последовательности... + // Если тест падает, он СЖИМАЕТСЯ до наименьшего падающего входа! } ``` -### Getting Started with proptest +### Начало работы с proptest ```toml # Cargo.toml [dev-dependencies] proptest = "1.4" ``` -### Common Patterns for C# Developers +### Распространённые паттерны для разработчиков C# ```rust use proptest::prelude::*; -// 1. Roundtrip property: serialize → deserialize = identity -// (Like testing JsonSerializer.Serialize → Deserialize) +// 1. Свойство «туда и обратно»: сериализация → десериализация = тождество +// (Как тестирование JsonSerializer.Serialize → Deserialize) proptest! { #[test] fn json_roundtrip(name in "[a-zA-Z]{1,50}", age in 0u32..150) { @@ -311,20 +311,20 @@ proptest! { } } -// 2. Invariant property: output always satisfies a condition +// 2. Свойство-инвариант: результат всегда удовлетворяет условию proptest! { #[test] fn sort_output_is_sorted(ref v in prop::collection::vec(any::<i32>(), 0..500)) { let mut sorted = v.clone(); sorted.sort(); - // Every adjacent pair must be in order + // Каждая соседняя пара должна быть в порядке for window in sorted.windows(2) { prop_assert!(window[0] <= window[1]); } } } -// 3. Oracle property: compare two implementations +// 3. Свойство-оракул: сравниваем две реализации proptest! { #[test] fn fast_path_matches_slow_path(input in "[0-9a-f]{1,100}") { @@ -334,7 +334,7 @@ proptest! { } } -// 4. Custom strategies: generate domain-specific test data +// 4. Пользовательские стратегии: генерируем данные, специфичные для предметной области fn valid_email() -> impl Strategy<Value = String> { ("[a-z]{1,20}", "[a-z]{1,10}", prop::sample::select(vec!["com", "org", "io"])) .prop_map(|(user, domain, tld)| format!("{}@{}.{}", user, domain, tld)) @@ -349,52 +349,52 @@ proptest! { } ``` -### proptest vs FsCheck Comparison +### Сравнение proptest и FsCheck -| Feature | C# FsCheck | Rust proptest | +| Возможность | C# FsCheck | Rust proptest | |---------|-----------|---------------| -| Random input generation | `Arb.Generate<T>()` | `any::<T>()` | -| Custom generators | `Arb.Register<T>()` | `impl Strategy<Value = T>` | -| Shrinking on failure | Automatic | Automatic | -| String patterns | Manual | `"[regex]"` strategy | -| Collection generation | `Gen.ListOf` | `prop::collection::vec(strategy, range)` | -| Composing generators | `Gen.Select` | `.prop_map()`, `.prop_flat_map()` | -| Config (# of cases) | `Config.MaxTest` | `#![proptest_config(ProptestConfig::with_cases(10000))]` inside `proptest!` block | +| Генерация случайных входов | `Arb.Generate<T>()` | `any::<T>()` | +| Пользовательские генераторы | `Arb.Register<T>()` | `impl Strategy<Value = T>` | +| Сжатие при падении (shrinking) | Автоматическое | Автоматическое | +| Строковые шаблоны | Вручную | Стратегия `"[regex]"` | +| Генерация коллекций | `Gen.ListOf` | `prop::collection::vec(strategy, range)` | +| Композиция генераторов | `Gen.Select` | `.prop_map()`, `.prop_flat_map()` | +| Конфигурация (число случаев) | `Config.MaxTest` | `#![proptest_config(ProptestConfig::with_cases(10000))]` внутри блока `proptest!` | -### When to Use Property Testing vs Unit Testing +### Когда использовать property-тестирование, а когда модульное -| Use **unit tests** when | Use **proptest** when | +| Используйте **модульные тесты**, когда | Используйте **proptest**, когда | |------------------------|----------------------| -| Testing specific edge cases | Verifying invariants across all inputs | -| Testing error messages/codes | Roundtrip properties (parse ↔ format) | -| Integration/mock tests | Comparing two implementations | -| Behavior depends on exact values | "For all X, property P holds" | +| Проверяются конкретные пограничные случаи | Проверяются инварианты на всех входах | +| Проверяются сообщения об ошибках/коды | Свойства «туда и обратно» (разбор ↔ форматирование) | +| Интеграционные и тесты с моками | Сравнение двух реализаций | +| Поведение зависит от точных значений | «Для всех X выполняется свойство P» | --- -## Integration Tests: the `tests/` Directory +## Интеграционные тесты: каталог `tests/` -Unit tests live inside `src/` with `#[cfg(test)]`. Integration tests live in a separate `tests/` directory and test your crate's **public API** — just like how C# integration tests reference the project as an external assembly. +Модульные тесты живут внутри `src/` с `#[cfg(test)]`. Интеграционные тесты находятся в отдельном каталоге `tests/` и проверяют **публичный API** крейта — так же, как интеграционные тесты в C# ссылаются на проект как на внешнюю сборку. ``` my_crate/ ├── src/ -│ ├── lib.rs // public API -│ └── internal.rs // private implementation +│ ├── lib.rs // публичный API +│ └── internal.rs // приватная реализация ├── tests/ -│ ├── smoke.rs // each file is a separate test binary +│ ├── smoke.rs // каждый файл — отдельный тестовый бинарник │ ├── api_tests.rs │ └── common/ -│ └── mod.rs // shared test helpers +│ └── mod.rs // общие вспомогательные функции для тестов └── Cargo.toml ``` -### Writing Integration Tests +### Написание интеграционных тестов -Each file in `tests/` is compiled as a separate crate that depends on your library: +Каждый файл в `tests/` компилируется как отдельный крейт, который зависит от вашей библиотеки: ```rust -// tests/smoke.rs — can only access pub items from my_crate +// tests/smoke.rs — имеет доступ только к pub-элементам my_crate use my_crate::{process_order, Order, OrderResult}; #[test] @@ -405,9 +405,9 @@ fn process_valid_order_returns_confirmation() { } ``` -### Shared Test Helpers +### Общие вспомогательные функции для тестов -Put shared setup code in `tests/common/mod.rs` (not `tests/common.rs`, which would be treated as its own test file): +Общий код настройки кладите в `tests/common/mod.rs` (а не `tests/common.rs`, который будет считаться отдельным тестовым файлом): ```rust // tests/common/mod.rs @@ -435,16 +435,16 @@ fn app_starts_with_test_config() { } ``` -### Running Specific Test Types +### Запуск отдельных типов тестов ```bash -cargo test # run all tests (unit + integration) -cargo test --lib # unit tests only (like dotnet test --filter Category=Unit) -cargo test --test smoke # run only tests/smoke.rs -cargo test --test api_tests # run only tests/api_tests.rs +cargo test # запустить все тесты (модульные + интеграционные) +cargo test --lib # только модульные тесты (как dotnet test --filter Category=Unit) +cargo test --test smoke # запустить только tests/smoke.rs +cargo test --test api_tests # запустить только tests/api_tests.rs ``` -**Key difference from C#:** Integration test files can only access your crate's `pub` API. Private functions are invisible — this forces you to test through the public interface, which is generally better test design. +**Ключевое отличие от C#:** файлы интеграционных тестов видят только `pub`-API вашего крейта. Приватные функции невидимы — это заставляет тестировать через публичный интерфейс, что обычно даёт лучший дизайн тестов. *** diff --git a/csharp-book/src/ch14-unsafe-rust-and-ffi.md b/csharp-book/src/ch14-unsafe-rust-and-ffi.md index 3456cfc..c715de0 100644 --- a/csharp-book/src/ch14-unsafe-rust-and-ffi.md +++ b/csharp-book/src/ch14-unsafe-rust-and-ffi.md @@ -1,77 +1,77 @@ ## Unsafe Rust -> **What you'll learn:** What `unsafe` permits (raw pointers, FFI, unchecked casts), safe wrapper patterns, -> C# P/Invoke vs Rust FFI for calling native code, and the safety checklist for `unsafe` blocks. +> **Что вы узнаете:** что разрешает `unsafe` (сырые указатели, FFI, непроверяемые приведения), паттерны безопасных обёрток, +> P/Invoke в C# против FFI в Rust для вызова нативного кода, а также чек-лист безопасности для блоков `unsafe`. > -> **Difficulty:** 🔴 Advanced +> **Сложность:** 🔴 Продвинутый -Unsafe Rust allows you to perform operations that the borrow checker cannot verify. Use it sparingly and with clear documentation. +Unsafe Rust позволяет выполнять операции, которые проверщик заимствований не может верифицировать. Используйте его осторожно и с понятной документацией. -> **Advanced coverage**: For safe abstraction patterns over unsafe code (arena allocators, lock-free structures, custom vtables), see [Rust Patterns](../../rust-patterns-book/src/summary.md). +> **Расширенное описание**: о безопасных абстракциях поверх unsafe-кода (арена-аллокаторы, структуры без блокировок, пользовательские таблицы виртуальных методов) см. [Паттерны Rust](../../rust-patterns-book/src/summary.md). -### When You Need Unsafe +### Когда нужен unsafe ```rust -// 1. Dereferencing raw pointers +// 1. Разыменование сырых указателей let mut value = 42; let ptr = &mut value as *mut i32; -// SAFETY: ptr points to a valid, live local variable. +// SAFETY: ptr указывает на действительную, живую локальную переменную. unsafe { - *ptr = 100; // Must be in unsafe block + *ptr = 100; // Должно находиться в блоке unsafe } -// 2. Calling unsafe functions +// 2. Вызов небезопасных функций unsafe fn dangerous() { - // Internal implementation that requires caller to maintain invariants + // Внутренняя реализация, которая требует от вызывающего кода соблюдения инвариантов } -// SAFETY: no invariants to uphold for this example function. +// SAFETY: для этой примерной функции инвариантов нет. unsafe { - dangerous(); // Caller takes responsibility + dangerous(); // Ответственность берёт на себя вызывающий код } -// 3. Accessing mutable static variables +// 3. Доступ к изменяемым статическим переменным static mut COUNTER: u32 = 0; -// SAFETY: single-threaded context; no concurrent access to COUNTER. +// SAFETY: однопоточный контекст; параллельного доступа к COUNTER нет. unsafe { - COUNTER += 1; // Not thread-safe — caller must ensure synchronization + COUNTER += 1; // Не потокобезопасно — вызывающий код должен обеспечить синхронизацию } -// 4. Implementing unsafe traits +// 4. Реализация небезопасных трейтов unsafe trait UnsafeTrait { fn do_something(&self); } ``` -### C# Comparison: unsafe Keyword +### Сравнение с C#: ключевое слово unsafe ```csharp -// C# unsafe - similar concept, different scope +// unsafe в C# — похожая идея, но другой масштаб unsafe void UnsafeExample() { int value = 42; int* ptr = &value; *ptr = 100; - // C# unsafe is about pointer arithmetic - // Rust unsafe is about ownership/borrow rule relaxation + // unsafe в C# — это про арифметику указателей + // unsafe в Rust — это про ослабление правил владения и заимствования } -// C# fixed - pinning managed objects +// fixed в C# — закрепление управляемых объектов unsafe void PinnedExample() { byte[] buffer = new byte[100]; fixed (byte* ptr = buffer) { - // ptr is valid only within this block + // ptr действителен только внутри этого блока } } ``` -### Safe Wrappers +### Безопасные обёртки ```rust -/// The key pattern: wrap unsafe code in a safe API +/// Ключевой паттерн: оборачиваем небезопасный код в безопасный API pub struct SafeBuffer { data: Vec<u8>, } @@ -81,15 +81,15 @@ impl SafeBuffer { SafeBuffer { data: vec![0; size] } } - /// Safe API — bounds-checked access + /// Безопасный API — доступ с проверкой границ pub fn get(&self, index: usize) -> Option<u8> { self.data.get(index).copied() } - /// Fast unchecked access — unsafe but wrapped safely with bounds check + /// Быстрый доступ без проверки — небезопасный, но безопасно обёрнутый проверкой границ pub fn get_unchecked_safe(&self, index: usize) -> Option<u8> { if index < self.data.len() { - // SAFETY: we just checked that index is in bounds + // SAFETY: мы только что проверили, что index находится в границах Some(unsafe { *self.data.get_unchecked(index) }) } else { None @@ -100,19 +100,19 @@ impl SafeBuffer { *** -## Interop with C# via FFI +## Взаимодействие с C# через FFI -Rust can expose C-compatible functions that C# can call via P/Invoke. +Rust может экспортировать функции с C-совместимым интерфейсом, которые C# вызывает через P/Invoke. ```mermaid graph LR - subgraph "C# Process" - CS["C# Code"] -->|"P/Invoke"| MI["Marshal Layer<br/>UTF-16 → UTF-8<br/>struct layout"] + subgraph "Процесс C#" + CS["Код C#"] -->|"P/Invoke"| MI["Слой маршалинга<br/>UTF-16 → UTF-8<br/>раскладка структур"] end - MI -->|"C ABI call"| FFI["FFI Boundary"] + MI -->|"Вызов по C ABI"| FFI["Граница FFI"] subgraph "Rust cdylib (.so / .dll)" FFI --> RF["extern #quot;C#quot; fn<br/>#[no_mangle]"] - RF --> Safe["Safe Rust<br/>internals"] + RF --> Safe["Безопасные внутренности<br/>на Rust"] end style FFI fill:#fff9c4,color:#000 @@ -120,7 +120,7 @@ graph LR style Safe fill:#c8e6c9,color:#000 ``` -### Rust Library (compiled as cdylib) +### Библиотека Rust (собирается как cdylib) ```rust // src/lib.rs @@ -131,7 +131,7 @@ pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { #[no_mangle] pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 { - // SAFETY: input is non-null (checked inside) and assumed null-terminated by caller. + // SAFETY: input проверяется на null внутри, а вызывающий код гарантирует null-терминированность. let c_str = unsafe { if input.is_null() { return -1; @@ -152,7 +152,7 @@ pub extern "C" fn process_string(input: *const std::os::raw::c_char) -> i32 { crate-type = ["cdylib"] ``` -### C# Consumer (P/Invoke) +### Потребитель на C# (P/Invoke) ```csharp using System.Runtime.InteropServices; @@ -167,54 +167,54 @@ public static class RustInterop [MarshalAs(UnmanagedType.LPUTF8Str)] string input); } -// Usage +// Использование int sum = RustInterop.add_numbers(5, 3); // 8 int len = RustInterop.process_string("Hello from C#!"); // 15 ``` -### FFI Safety Checklist +### Чек-лист безопасности FFI -When exposing Rust functions to C#, these rules prevent the most common bugs: +При экспорте функций Rust в C# эти правила предотвращают большинство частых ошибок: -1. **Always use `extern "C"`** — without it, Rust uses its own (unstable) calling convention. C# P/Invoke expects the C ABI. +1. **Всегда используйте `extern "C"`** — без него Rust применяет собственное (нестабильное) соглашение о вызовах. P/Invoke в C# ожидает C ABI. -2. **`#[no_mangle]`** — prevents the Rust compiler from mangling the function name. Without it, C# can't find the symbol. +2. **`#[no_mangle]`** — не даёт компилятору Rust менять имя функции. Без него C# не сможет найти символ. -3. **Never let a panic cross the FFI boundary** — a Rust panic unwinding into C# is **undefined behavior**. Catch panics at FFI entry points: +3. **Никогда не пускайте панику через границу FFI** — раскрутка паники Rust внутрь C# является **неопределённым поведением**. Перехватывайте паники на входах в FFI: ```rust #[no_mangle] pub extern "C" fn safe_ffi_function() -> i32 { match std::panic::catch_unwind(|| { - // actual logic here + // реальная логика здесь 42 }) { Ok(result) => result, - Err(_) => -1, // Return error code instead of panicking into C# + Err(_) => -1, // Возвращаем код ошибки вместо паники внутрь C# } } ``` -4. **Opaque vs transparent structs** — if C# only holds a pointer (opaque handle), `#[repr(C)]` is not needed. If C# reads struct fields via `StructLayout`, you **must** use `#[repr(C)]`: +4. **Непрозрачные и прозрачные структуры** — если C# хранит только указатель (непрозрачный дескриптор), `#[repr(C)]` не нужен. Если C# читает поля структуры через `StructLayout`, `#[repr(C)]` **обязателен**: ```rust - // Opaque — C# only holds IntPtr. No #[repr(C)] needed. - pub struct Connection { /* Rust-only fields */ } + // Непрозрачная — C# хранит только IntPtr. #[repr(C)] не нужен. + pub struct Connection { /* поля только для Rust */ } - // Transparent — C# marshals fields directly. MUST use #[repr(C)]. + // Прозрачная — C# маршалирует поля напрямую. ОБЯЗАТЕЛЬНО #[repr(C)]. #[repr(C)] pub struct Point { pub x: f64, pub y: f64 } ``` -5. **Null pointer checks** — always validate pointers before dereferencing. C# can pass `IntPtr.Zero`. +5. **Проверки на null-указатели** — всегда проверяйте указатели перед разыменованием. C# может передать `IntPtr.Zero`. -6. **String encoding** — C# uses UTF-16 internally. `MarshalAs(UnmanagedType.LPUTF8Str)` converts to UTF-8 for Rust's `CStr`. Document this contract explicitly. +6. **Кодировка строк** — C# использует UTF-16 внутренне. `MarshalAs(UnmanagedType.LPUTF8Str)` конвертирует строку в UTF-8 для `CStr` в Rust. Этот контракт нужно явно документировать. -### End-to-End Example: Opaque Handle with Lifecycle Management +### Сквозной пример: непрозрачный дескриптор с управлением жизненным циклом -This pattern is common in production: Rust owns an object, C# holds an opaque handle, and explicit create/destroy functions manage the lifecycle. +Этот паттерн распространён в продакшене: Rust владеет объектом, C# хранит непрозрачный дескриптор, а явные функции создания и уничтожения управляют жизненным циклом. -**Rust side** (`src/lib.rs`): +**Сторона Rust** (`src/lib.rs`): ```rust use std::ffi::{c_char, CStr}; @@ -225,7 +225,7 @@ pub struct ImageProcessor { pixels: Vec<u8>, } -/// Create a new processor. Returns null on invalid dimensions. +/// Создаёт новый обработчик. Возвращает null при некорректных размерах. #[no_mangle] pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor { if width == 0 || height == 0 { @@ -236,13 +236,13 @@ pub extern "C" fn processor_new(width: u32, height: u32) -> *mut ImageProcessor height, pixels: vec![0u8; (width * height * 4) as usize], }; - Box::into_raw(Box::new(proc)) // Allocate on heap, return raw pointer + Box::into_raw(Box::new(proc)) // Выделяем в куче, возвращаем сырой указатель } -/// Apply a grayscale filter. Returns 0 on success, -1 on null pointer. +/// Применяет фильтр оттенков серого. Возвращает 0 при успехе, -1 при null-указателе. #[no_mangle] pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 { - // SAFETY: ptr was created by Box::into_raw (non-null), still valid. + // SAFETY: ptr был создан через Box::into_raw (не null), всё ещё действителен. let proc = match unsafe { ptr.as_mut() } { Some(p) => p, None => return -1, @@ -258,17 +258,17 @@ pub extern "C" fn processor_grayscale(ptr: *mut ImageProcessor) -> i32 { 0 } -/// Destroy the processor. Safe to call with null. +/// Уничтожает обработчик. Безопасно вызывать с null. #[no_mangle] pub extern "C" fn processor_free(ptr: *mut ImageProcessor) { if !ptr.is_null() { - // SAFETY: ptr was created by processor_new via Box::into_raw + // SAFETY: ptr был создан через processor_new с помощью Box::into_raw unsafe { drop(Box::from_raw(ptr)); } } } ``` -**C# side**: +**Сторона C#**: ```csharp using System.Runtime.InteropServices; @@ -309,40 +309,40 @@ public sealed class ImageProcessor : IDisposable } } -// Usage — IDisposable ensures Rust memory is freed +// Использование — IDisposable гарантирует освобождение памяти Rust using var proc = new ImageProcessor(1920, 1080); proc.Grayscale(); -// proc.Dispose() called automatically → processor_free() → Rust drops the Vec +// proc.Dispose() вызывается автоматически → processor_free() → Rust освобождает Vec ``` -> **Key insight**: This is the Rust equivalent of C#'s `SafeHandle` pattern. Rust's `Box::into_raw` / `Box::from_raw` transfers ownership across the FFI boundary, and the C# `IDisposable` wrapper ensures cleanup. +> **Ключевая мысль**: это аналог паттерна `SafeHandle` в C#. `Box::into_raw` / `Box::from_raw` передают владение через границу FFI, а обёртка `IDisposable` в C# обеспечивает очистку. --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Safe Wrapper for Raw Pointer</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: безопасная обёртка над сырым указателем</strong> (нажмите, чтобы раскрыть)</summary> -You receive a raw pointer from a C library. Write a safe Rust wrapper: +Вы получаете сырой указатель из C-библиотеки. Напишите безопасную обёртку на Rust: ```rust -// Simulated C API +// Имитация C API extern "C" { fn lib_create_buffer(size: usize) -> *mut u8; fn lib_free_buffer(ptr: *mut u8); } ``` -Requirements: +Требования: -1. Create a `SafeBuffer` struct that wraps the raw pointer -2. Implement `Drop` to call `lib_free_buffer` -3. Provide a safe `&[u8]` view via `as_slice()` -4. Ensure `SafeBuffer::new()` returns `None` if the pointer is null +1. Создайте структуру `SafeBuffer`, которая оборачивает сырой указатель +2. Реализуйте `Drop`, который вызывает `lib_free_buffer` +3. Предоставьте безопасное представление `&[u8]` через `as_slice()` +4. Убедитесь, что `SafeBuffer::new()` возвращает `None`, если указатель null <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust,ignore struct SafeBuffer { @@ -352,7 +352,7 @@ struct SafeBuffer { impl SafeBuffer { fn new(size: usize) -> Option<Self> { - // SAFETY: lib_create_buffer returns a valid pointer or null (checked below). + // SAFETY: lib_create_buffer возвращает действительный указатель или null (проверяется ниже). let ptr = unsafe { lib_create_buffer(size) }; if ptr.is_null() { None @@ -362,29 +362,30 @@ impl SafeBuffer { } fn as_slice(&self) -> &[u8] { - // SAFETY: ptr is non-null (checked in new()), len is the - // allocated size, and we hold exclusive ownership. + // SAFETY: ptr не null (проверено в new()), len — это выделенный размер, + // и мы владеем буфером монопольно. unsafe { std::slice::from_raw_parts(self.ptr, self.len) } } } impl Drop for SafeBuffer { fn drop(&mut self) { - // SAFETY: ptr was allocated by lib_create_buffer + // SAFETY: ptr был выделен через lib_create_buffer unsafe { lib_free_buffer(self.ptr); } } } -// Usage: all unsafe is contained in SafeBuffer +// Использование: весь unsafe сосредоточен внутри SafeBuffer fn process(buf: &SafeBuffer) { - let data = buf.as_slice(); // completely safe API + let data = buf.as_slice(); // полностью безопасный API println!("First byte: {}", data[0]); } ``` -**Key pattern**: Encapsulate `unsafe` in a small module with `// SAFETY:` comments. Expose a 100% safe public API. This is how Rust's standard library works — `Vec`, `String`, `HashMap` all contain unsafe internally but present safe interfaces. +**Ключевой паттерн**: инкапсулируйте `unsafe` в небольшом модуле с комментариями `// SAFETY:`. Открывайте наружу полностью безопасный публичный API. Так устроена стандартная библиотека Rust — `Vec`, `String`, `HashMap` внутри содержат unsafe-код, но предоставляют безопасные интерфейсы. </details> </details> *** + diff --git a/csharp-book/src/ch15-1-essential-crates-for-c-developers.md b/csharp-book/src/ch15-1-essential-crates-for-c-developers.md index ba386d9..6072a73 100644 --- a/csharp-book/src/ch15-1-essential-crates-for-c-developers.md +++ b/csharp-book/src/ch15-1-essential-crates-for-c-developers.md @@ -1,62 +1,62 @@ -## Essential Crates for C# Developers +## Основные крейты для разработчиков C# -> **What you'll learn:** The Rust crate equivalents for common .NET libraries — serde (JSON.NET), -> reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework), and a deep dive on serde's -> attribute system compared to `System.Text.Json`. +> **Что вы узнаете:** аналоги крейтов Rust для распространённых библиотек .NET — serde (JSON.NET), +> reqwest (HttpClient), tokio (Task/async), sqlx (Entity Framework), а также подробный разбор системы атрибутов serde +> в сравнении с `System.Text.Json`. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Core Functionality Equivalents +### Аналоги основной функциональности ```rust -// Cargo.toml dependencies for C# developers +// Зависимости Cargo для разработчиков C# [dependencies] -# Serialization (like Newtonsoft.Json or System.Text.Json) +# Сериализация (как Newtonsoft.Json или System.Text.Json) serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -# HTTP client (like HttpClient) +# HTTP-клиент (как HttpClient) reqwest = { version = "0.11", features = ["json"] } -# Async runtime (like Task.Run, async/await) +# Асинхронный рантайм (как Task.Run, async/await) tokio = { version = "1.0", features = ["full"] } -# Error handling (like custom exceptions) +# Обработка ошибок (как собственные исключения) thiserror = "1.0" anyhow = "1.0" -# Logging (like ILogger, Serilog) +# Логирование (как ILogger, Serilog) log = "0.4" env_logger = "0.10" -# Date/time (like DateTime) +# Дата и время (как DateTime) chrono = { version = "0.4", features = ["serde"] } -# UUID (like System.Guid) +# UUID (как System.Guid) uuid = { version = "1.0", features = ["v4", "serde"] } -# Collections (like List<T>, Dictionary<K,V>) -# Built into std, but for advanced collections: -indexmap = "2.0" # Ordered HashMap +# Коллекции (как List<T>, Dictionary<K,V>) +# Встроены в std, но для продвинутых коллекций: +indexmap = "2.0" # HashMap с сохранением порядка -# Configuration (like IConfiguration) +# Конфигурация (как IConfiguration) config = "0.13" -# Database (like Entity Framework) +# База данных (как Entity Framework) sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] } -# Testing (like xUnit, NUnit) -# Built into std, but for more features: -rstest = "0.18" # Parameterized tests +# Тестирование (как xUnit, NUnit) +# Встроено в std, но для расширенных возможностей: +rstest = "0.18" # Параметризованные тесты -# Mocking (like Moq) +# Моки (как Moq) mockall = "0.11" -# Parallel processing (like Parallel.ForEach) +# Параллельная обработка (как Parallel.ForEach) rayon = "1.7" ``` -### Example Usage Patterns +### Примеры использования ```rust use serde::{Deserialize, Serialize}; @@ -66,7 +66,7 @@ use thiserror::Error; use chrono::{DateTime, Utc}; use uuid::Uuid; -// Data models (like C# POCOs with attributes) +// Модели данных (как POCO с атрибутами в C#) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: Uuid, @@ -76,7 +76,7 @@ pub struct User { pub created_at: DateTime<Utc>, } -// Custom error types (like custom exceptions) +// Собственные типы ошибок (как собственные исключения) #[derive(Error, Debug)] pub enum ApiError { #[error("HTTP request failed: {0}")] @@ -92,7 +92,7 @@ pub enum ApiError { Validation { message: String }, } -// Service class equivalent +// Аналог класса сервиса pub struct UserService { client: reqwest::Client, base_url: String, @@ -108,7 +108,7 @@ impl UserService { UserService { client, base_url } } - // Async method (like C# async Task<User>) + // Асинхронный метод (как async Task<User> в C#) pub async fn get_user(&self, id: Uuid) -> Result<User, ApiError> { let url = format!("{}/users/{}", self.base_url, id); @@ -125,7 +125,7 @@ impl UserService { Ok(user) } - // Create user (like C# async Task<User>) + // Создание пользователя (как async Task<User> в C#) pub async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> { if name.trim().is_empty() { return Err(ApiError::Validation { @@ -151,15 +151,15 @@ impl UserService { } } -// Usage example (like C# Main method) +// Пример использования (как метод Main в C#) #[tokio::main] async fn main() -> Result<(), ApiError> { - // Initialize logging (like configuring ILogger) + // Инициализация логирования (как настройка ILogger) env_logger::init(); let service = UserService::new("https://api.example.com".to_string()); - // Create user + // Создание пользователя let user = service.create_user( "John Doe".to_string(), "john@example.com".to_string(), @@ -167,7 +167,7 @@ async fn main() -> Result<(), ApiError> { println!("Created user: {:?}", user); - // Get user + // Получение пользователя let retrieved_user = service.get_user(user.id).await?; println!("Retrieved user: {:?}", retrieved_user); @@ -178,7 +178,7 @@ async fn main() -> Result<(), ApiError> { mod tests { use super::*; - #[tokio::test] // Like C# [Test] or [Fact] + #[tokio::test] // Как [Test] или [Fact] в C# async fn test_user_creation() { let service = UserService::new("http://localhost:8080".to_string()); @@ -195,7 +195,7 @@ mod tests { #[test] fn test_validation() { - // Synchronous test + // Синхронный тест let error = ApiError::Validation { message: "Invalid input".to_string(), }; @@ -207,13 +207,12 @@ mod tests { *** - <!-- ch15.1a: Serde Deep Dive for C# Developers --> -## Serde Deep Dive: JSON Serialization for C# Developers +## Глубокое погружение в serde: сериализация JSON для разработчиков C# -C# developers rely heavily on `System.Text.Json` or `Newtonsoft.Json`. In Rust, **serde** (serialize/deserialize) is the universal framework — understanding its attribute system unlocks most data-handling scenarios. +Разработчики C# активно используют `System.Text.Json` или `Newtonsoft.Json`. В Rust **serde** (serialize/deserialize) — универсальный фреймворк для этого. Понимание его системы атрибутов открывает большинство сценариев работы с данными. -### Basic Derive: The Starting Point +### Базовый derive: отправная точка ```rust use serde::{Deserialize, Serialize}; @@ -230,7 +229,7 @@ let parsed: User = serde_json::from_str(&json)?; ``` ```csharp -// C# equivalent +// Аналог в C# public class User { public string Name { get; set; } @@ -241,42 +240,42 @@ var json = JsonSerializer.Serialize(user, new JsonSerializerOptions { WriteInden var parsed = JsonSerializer.Deserialize<User>(json); ``` -### Field-Level Attributes (Like `[JsonProperty]`) +### Атрибуты уровня полей (аналог `[JsonProperty]`) ```rust use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] struct ApiResponse { - // Rename field in JSON output (like [JsonPropertyName("user_id")]) + // Переименование поля в JSON (как [JsonPropertyName("user_id")]) #[serde(rename = "user_id")] id: u64, - // Use different names for serialize vs deserialize + // Разные имена при сериализации и десериализации #[serde(rename(serialize = "userName", deserialize = "user_name"))] name: String, - // Skip this field entirely (like [JsonIgnore]) + // Полностью пропустить поле (как [JsonIgnore]) #[serde(skip)] internal_cache: Option<String>, - // Skip during serialization only + // Пропустить только при сериализации #[serde(skip_serializing)] password_hash: String, - // Default value if missing from JSON (like default constructor values) + // Значение по умолчанию, если поля нет в JSON (как значения в конструкторе по умолчанию) #[serde(default)] is_active: bool, - // Custom default + // Пользовательское значение по умолчанию #[serde(default = "default_role")] role: String, - // Flatten a nested struct into the parent (like [JsonExtensionData]) + // Развернуть вложенную структуру в родительскую (как [JsonExtensionData]) #[serde(flatten)] metadata: Metadata, - // Skip if the value is None (omit null fields) + // Пропустить, если значение None (не выводить null-поля) #[serde(skip_serializing_if = "Option::is_none")] nickname: Option<String>, } @@ -291,7 +290,7 @@ struct Metadata { ``` ```csharp -// C# equivalent attributes +// Эквивалентные атрибуты в C# public class ApiResponse { [JsonPropertyName("user_id")] @@ -305,25 +304,25 @@ public class ApiResponse } ``` -### Enum Representations (Critical Difference from C#) +### Представления перечислений (ключевое отличие от C#) -Rust serde supports **four different JSON representations** for enums — a concept that has no direct C# equivalent because C# enums are always integers or strings. +В serde для перечислений поддерживаются **четыре разных представления в JSON** — понятие, которому в C# нет прямого аналога, поскольку перечисления C# всегда целочисленные или строковые. ```rust use serde::{Deserialize, Serialize}; -// 1. Externally tagged (DEFAULT) — most common +// 1. Внешне тегированное (ПО УМОЛЧАНИЮ) — самое распространённое #[derive(Serialize, Deserialize)] enum Message { Text(String), Image { url: String, width: u32 }, Ping, } -// Text variant: {"Text": "hello"} -// Image variant: {"Image": {"url": "...", "width": 100}} -// Ping variant: "Ping" +// Вариант Text: {"Text": "hello"} +// Вариант Image: {"Image": {"url": "...", "width": 100}} +// Вариант Ping: "Ping" -// 2. Internally tagged — like discriminated unions in other languages +// 2. Внутренне тегированное — похоже на дискриминируемые объединения в других языках #[derive(Serialize, Deserialize)] #[serde(tag = "type")] enum Event { @@ -334,7 +333,7 @@ enum Event { // {"type": "Created", "id": 1, "name": "Alice"} // {"type": "Deleted", "id": 1} -// 3. Adjacently tagged — tag and content in separate fields +// 3. Смежно тегированное — тег и содержимое в отдельных полях #[derive(Serialize, Deserialize)] #[serde(tag = "t", content = "c")] enum ApiResult { @@ -344,7 +343,7 @@ enum ApiResult { // {"t": "Success", "c": {"name": "Alice"}} // {"t": "Error", "c": "not found"} -// 4. Untagged — serde tries each variant in order +// 4. Без тега — serde пробует варианты по порядку #[derive(Serialize, Deserialize)] #[serde(untagged)] enum FlexibleValue { @@ -353,14 +352,14 @@ enum FlexibleValue { Text(String), Bool(bool), } -// 42, 3.14, "hello", true — serde auto-detects the variant +// 42, 3.14, "hello", true — serde сам определяет вариант ``` -### Custom Serialization (Like `JsonConverter`) +### Пользовательская сериализация (аналог `JsonConverter`) ```rust use serde::{Deserialize, Deserializer, Serialize, Serializer}; -// Custom serialization for a specific field +// Пользовательская сериализация для конкретного поля #[derive(Serialize, Deserialize)] struct Config { #[serde(serialize_with = "serialize_duration", deserialize_with = "deserialize_duration")] @@ -378,11 +377,11 @@ fn deserialize_duration<'de, D: Deserializer<'de>>(d: D) -> Result<std::time::Du // JSON: {"timeout": 5000} ↔ Config { timeout: Duration::from_millis(5000) } ``` -### Container-Level Attributes +### Атрибуты уровня контейнера ```rust #[derive(Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] // All fields become camelCase in JSON +#[serde(rename_all = "camelCase")] // Все поля в JSON становятся camelCase struct UserProfile { first_name: String, // → "firstName" last_name: String, // → "lastName" @@ -390,7 +389,7 @@ struct UserProfile { } #[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] // Reject JSON with extra fields (strict parsing) +#[serde(deny_unknown_fields)] // Отвергать JSON с лишними полями (строгий разбор) struct StrictConfig { port: u16, host: String, @@ -399,33 +398,33 @@ struct StrictConfig { // → Error: unknown field `extra` ``` -### Quick Reference: Serde Attributes +### Краткая справка по атрибутам serde -| Attribute | Level | C# Equivalent | Purpose | +| Атрибут | Уровень | Аналог в C# | Назначение | |-----------|-------|---------------|---------| -| `#[serde(rename = "...")]` | Field | `[JsonPropertyName]` | Rename in JSON | -| `#[serde(skip)]` | Field | `[JsonIgnore]` | Omit entirely | -| `#[serde(default)]` | Field | Default value | Use `Default::default()` if missing | -| `#[serde(flatten)]` | Field | `[JsonExtensionData]` | Merge nested struct into parent | -| `#[serde(skip_serializing_if = "...")]` | Field | `JsonIgnoreCondition` | Conditional skip | -| `#[serde(rename_all = "camelCase")]` | Container | `JsonSerializerOptions.PropertyNamingPolicy` | Naming convention | -| `#[serde(deny_unknown_fields)]` | Container | — | Strict deserialization | -| `#[serde(tag = "type")]` | Enum | Discriminator pattern | Internal tagging | -| `#[serde(untagged)]` | Enum | — | Try variants in order | -| `#[serde(with = "...")]` | Field | `[JsonConverter]` | Custom ser/de | - -### Beyond JSON: serde Works Everywhere +| `#[serde(rename = "...")]` | Поле | `[JsonPropertyName]` | Переименование в JSON | +| `#[serde(skip)]` | Поле | `[JsonIgnore]` | Полностью пропустить | +| `#[serde(default)]` | Поле | Значение по умолчанию | Использовать `Default::default()`, если поля нет | +| `#[serde(flatten)]` | Поле | `[JsonExtensionData]` | Слить вложенную структуру в родительскую | +| `#[serde(skip_serializing_if = "...")]` | Поле | `JsonIgnoreCondition` | Условный пропуск | +| `#[serde(rename_all = "camelCase")]` | Контейнер | `JsonSerializerOptions.PropertyNamingPolicy` | Соглашение об именовании | +| `#[serde(deny_unknown_fields)]` | Контейнер | — | Строгая десериализация | +| `#[serde(tag = "type")]` | Перечисление | Паттерн дискриминатора | Внутреннее тегирование | +| `#[serde(untagged)]` | Перечисление | — | Пробовать варианты по порядку | +| `#[serde(with = "...")]` | Поле | `[JsonConverter]` | Пользовательская сериализация и десериализация | + +### За пределами JSON: serde работает везде ```rust -// The SAME derive works for ALL formats — just change the crate +// ТОТ ЖЕ derive работает для ВСЕХ форматов — нужно лишь сменить крейт let user = User { name: "Alice".into(), age: 30, email: "a@b.com".into() }; let json = serde_json::to_string(&user)?; // JSON -let toml = toml::to_string(&user)?; // TOML (config files) +let toml = toml::to_string(&user)?; // TOML (конфигурационные файлы) let yaml = serde_yaml::to_string(&user)?; // YAML -let cbor = serde_cbor::to_vec(&user)?; // CBOR (binary, compact) -let msgpk = rmp_serde::to_vec(&user)?; // MessagePack (binary) +let cbor = serde_cbor::to_vec(&user)?; // CBOR (бинарный, компактный) +let msgpk = rmp_serde::to_vec(&user)?; // MessagePack (бинарный) -// One #[derive(Serialize, Deserialize)] — every format for free +// Один #[derive(Serialize, Deserialize)] — каждый формат бесплатно ``` *** diff --git a/csharp-book/src/ch15-2-incremental-adoption-strategy.md b/csharp-book/src/ch15-2-incremental-adoption-strategy.md index 6a7306f..6942e4a 100644 --- a/csharp-book/src/ch15-2-incremental-adoption-strategy.md +++ b/csharp-book/src/ch15-2-incremental-adoption-strategy.md @@ -1,15 +1,15 @@ -## Incremental Adoption Strategy +## Стратегия постепенного внедрения -> **What you'll learn:** A phased approach to introducing Rust in a C#/.NET organization — -> from learning exercises (weeks 1–4) to performance-critical replacements (weeks 5–8) -> to new microservices (weeks 9–12), with concrete team adoption timelines. +> **Что вы узнаете:** поэтапный подход к внедрению Rust в организации, работающие на C#/.NET — +> от учебных упражнений (недели 1–4) через замену компонентов, критичных к производительности (недели 5–8), +> до новых микросервисов (недели 9–12), с конкретными сроками освоения для команды. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Phase 1: Learning and Experimentation (Weeks 1-4) +### Этап 1: обучение и эксперименты (недели 1–4) ```rust -// Start with command-line tools and utilities -// Example: Log file analyzer +// Начинаем с инструментов командной строки и утилит +// Пример: анализатор файлов логов use std::fs; use std::collections::HashMap; use clap::Parser; @@ -48,10 +48,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> { } ``` -### Phase 2: Replace Performance-Critical Components (Weeks 5-8) +### Этап 2: замена компонентов, критичных к производительности (недели 5–8) ```rust -// Replace CPU-intensive data processing -// Example: Image processing microservice +// Замена ресурсоёмкой обработки данных +// Пример: микросервис обработки изображений use image::{DynamicImage, ImageBuffer, Rgb}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; @@ -120,10 +120,10 @@ struct ProcessingError(String); impl warp::reject::Reject for ProcessingError {} ``` -### Phase 3: New Microservices (Weeks 9-12) +### Этап 3: новые микросервисы (недели 9–12) ```rust -// Build new services from scratch in Rust -// Example: Authentication service +// Строим новые сервисы с нуля на Rust +// Пример: сервис аутентификации use axum::{ extract::{Query, State}, http::StatusCode, @@ -165,9 +165,9 @@ async fn login( State(state): State<AppState>, Json(request): Json<LoginRequest>, ) -> Result<Json<LoginResponse>, StatusCode> { - // Note: sqlx::query!() is compile-time checked and requires DATABASE_URL - // pointing to a live database during build. For runtime-checked queries, - // use sqlx::query() or sqlx::query_as() instead. + // Примечание: sqlx::query!() проверяется на этапе компиляции и требует, чтобы DATABASE_URL + // указывал на работающую базу данных во время сборки. Для запросов, проверяемых во время выполнения, + // используйте sqlx::query() или sqlx::query_as(). let user = sqlx::query!( "SELECT id, password_hash FROM users WHERE email = $1", request.email @@ -230,22 +230,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { *** -## Team Adoption Timeline +## Календарь освоения для команды -### Month 1: Foundation -**Week 1-2: Syntax and Ownership** -- Basic syntax differences from C# -- Understanding ownership, borrowing, and lifetimes -- Small exercises: CLI tools, file processing +### Месяц 1: основы +**Недели 1–2: синтаксис и владение** +- Базовые синтаксические отличия от C# +- Понимание владения, заимствования и времён жизни +- Небольшие упражнения: CLI-утилиты, обработка файлов -**Week 3-4: Error Handling and Types** -- `Result<T, E>` vs exceptions -- `Option<T>` vs nullable types -- Pattern matching and exhaustive checking +**Недели 3–4: обработка ошибок и типы** +- `Result<T, E>` против исключений +- `Option<T>` против nullable-типов +- Сопоставление с образцом и проверка исчерпываемости -**Recommended exercises:** +**Рекомендуемые упражнения:** ```rust -// Week 1-2: File processor +// Недели 1–2: обработчик файлов fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> { let content = std::fs::read_to_string(path)?; let errors: Vec<String> = content @@ -256,7 +256,7 @@ fn process_log_file(path: &str) -> Result<Vec<String>, std::io::Error> { Ok(errors) } -// Week 3-4: JSON processor with error handling +// Недели 3–4: обработчик JSON с обработкой ошибок use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Debug)] @@ -272,20 +272,20 @@ fn parse_log_entries(json_str: &str) -> Result<Vec<LogEntry>, Box<dyn std::error } ``` -### Month 2: Practical Applications -**Week 5-6: Traits and Generics** -- Trait system vs interfaces -- Generic constraints and bounds -- Common patterns and idioms +### Месяц 2: практическое применение +**Недели 5–6: трейты и обобщения** +- Система трейтов против интерфейсов +- Ограничения обобщений и границы +- Распространённые паттерны и идиомы -**Week 7-8: Async Programming and Concurrency** -- `async`/`await` similarities and differences -- Channels for communication -- Thread safety guarantees +**Недели 7–8: асинхронное программирование и конкурентность** +- Сходства и различия `async`/`await` +- Каналы для коммуникации +- Гарантии потокобезопасности -**Recommended projects:** +**Рекомендуемые проекты:** ```rust -// Week 5-6: Generic data processor +// Недели 5–6: обобщённый обработчик данных trait DataProcessor<T> { type Output; type Error; @@ -304,7 +304,7 @@ impl DataProcessor<&str> for JsonProcessor { } } -// Week 7-8: Async web client +// Недели 7–8: асинхронный веб-клиент async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::error::Error>> { let client = reqwest::Client::new(); @@ -329,17 +329,17 @@ async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box<dyn std::erro } ``` -### Month 3+: Production Integration -**Week 9-12: Real Project Work** -- Choose a non-critical component to rewrite -- Implement comprehensive error handling -- Add logging, metrics, and testing -- Performance profiling and optimization - -**Ongoing: Team Review and Mentoring** -- Code reviews focusing on Rust idioms -- Pair programming sessions -- Knowledge sharing sessions +### Месяц 3 и далее: интеграция в продакшен +**Недели 9–12: работа над реальным проектом** +- Выберите некритичный компонент для переписывания +- Реализуйте полноценную обработку ошибок +- Добавьте логирование, метрики и тестирование +- Профилирование производительности и оптимизация + +**Постоянно: командный ревью и менторство** +- Код-ревью с фокусом на идиомах Rust +- Парное программирование +- Сессии обмена знаниями *** diff --git a/csharp-book/src/ch15-migration-patterns-and-case-studies.md b/csharp-book/src/ch15-migration-patterns-and-case-studies.md index dbe2545..29483e0 100644 --- a/csharp-book/src/ch15-migration-patterns-and-case-studies.md +++ b/csharp-book/src/ch15-migration-patterns-and-case-studies.md @@ -1,34 +1,34 @@ -## Common C# Patterns in Rust +## Распространённые паттерны C# в Rust -> **What you'll learn:** How to translate the Repository pattern, Builder pattern, dependency injection, -> LINQ chains, Entity Framework queries, and configuration patterns from C# to idiomatic Rust. +> **Что вы узнаете:** как переводить с C# на идиоматичный Rust паттерн Repository, паттерн Builder, внедрение зависимостей, +> цепочки LINQ, запросы Entity Framework и паттерны конфигурации. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний ```mermaid graph LR - subgraph "C# Pattern" - I["interface IRepo<T>"] --> DI["DI Container"] + subgraph "Паттерн C#" + I["interface IRepo<T>"] --> DI["Контейнер DI"] EX["try / catch"] --> LOG["ILogger"] LINQ["LINQ .Where().Select()"] --> LIST["List<T>"] end - subgraph "Rust Equivalent" + subgraph "Аналог в Rust" TR["trait Repo<T>"] --> GEN["Generic<R: Repo>"] RES["Result<T, E> + ?"] --> THISERR["thiserror / anyhow"] ITER[".iter().filter().map()"] --> VEC["Vec<T>"] end - I -->|"becomes"| TR - EX -->|"becomes"| RES - LINQ -->|"becomes"| ITER + I -->|"становится"| TR + EX -->|"становится"| RES + LINQ -->|"становится"| ITER style TR fill:#c8e6c9,color:#000 style RES fill:#c8e6c9,color:#000 style ITER fill:#c8e6c9,color:#000 ``` -### Repository Pattern +### Паттерн Repository ```csharp -// C# Repository Pattern +// Паттерн Repository в C# public interface IRepository<T> where T : IEntity { Task<T> GetByIdAsync(int id); @@ -52,12 +52,12 @@ public class UserRepository : IRepository<User> return await _context.Users.FindAsync(id); } - // ... other implementations + // ... остальные реализации } ``` ```rust -// Rust Repository Pattern with traits and generics +// Паттерн Repository в Rust с трейтами и обобщениями use async_trait::async_trait; use std::fmt::Debug; @@ -101,13 +101,13 @@ impl std::fmt::Display for RepositoryError { impl std::error::Error for RepositoryError {} pub struct UserRepository { - // database connection pool, etc. + // пул соединений с базой данных и т. д. } #[async_trait] impl Repository<User, RepositoryError> for UserRepository { async fn get_by_id(&self, id: u64) -> Result<Option<User>, RepositoryError> { - // Simulate database lookup + // Имитация поиска в базе данных if id == 0 { return Ok(None); } @@ -120,12 +120,12 @@ impl Repository<User, RepositoryError> for UserRepository { } async fn get_all(&self) -> Result<Vec<User>, RepositoryError> { - // Implementation here + // Реализация здесь Ok(vec![]) } async fn add(&self, entity: User) -> Result<User, RepositoryError> { - // Validation and database insertion + // Валидация и вставка в базу данных if entity.name.is_empty() { return Err(RepositoryError::ValidationError("Name cannot be empty".to_string())); } @@ -133,20 +133,20 @@ impl Repository<User, RepositoryError> for UserRepository { } async fn update(&self, entity: User) -> Result<User, RepositoryError> { - // Implementation here + // Реализация здесь Ok(entity) } async fn delete(&self, id: u64) -> Result<(), RepositoryError> { - // Implementation here + // Реализация здесь Ok(()) } } ``` -### Builder Pattern +### Паттерн Builder ```csharp -// C# Builder Pattern (fluent interface) +// Паттерн Builder в C# (текучий интерфейс) public class HttpClientBuilder { private TimeSpan? _timeout; @@ -184,7 +184,7 @@ public class HttpClientBuilder } } -// Usage +// Использование var client = new HttpClientBuilder() .WithTimeout(TimeSpan.FromSeconds(30)) .WithBaseAddress("https://api.example.com") @@ -193,7 +193,7 @@ var client = new HttpClientBuilder() ``` ```rust -// Rust Builder Pattern (consuming builder) +// Паттерн Builder в Rust (потребляющий builder) use std::collections::HashMap; use std::time::Duration; @@ -245,14 +245,14 @@ impl HttpClientBuilder { } } -// Usage +// Использование let client = HttpClientBuilder::new() .with_timeout(Duration::from_secs(30)) .with_base_address("https://api.example.com") .with_header("Accept", "application/json") .build()?; -// Alternative: Using Default trait for common cases +// Альтернатива: трейт Default для типичных случаев impl Default for HttpClientBuilder { fn default() -> Self { Self::new() @@ -262,11 +262,11 @@ impl Default for HttpClientBuilder { *** -## C# to Rust Concept Mapping +## Сопоставление концепций C# и Rust -### Dependency Injection → Constructor Injection + Traits +### Внедрение зависимостей → внедрение через конструктор и трейты ```csharp -// C# with DI container +// C# с контейнером DI services.AddScoped<IUserRepository, UserRepository>(); services.AddScoped<IUserService, UserService>(); @@ -282,7 +282,7 @@ public class UserService ``` ```rust -// Rust: Constructor injection with traits +// Rust: внедрение через конструктор с трейтами pub trait UserRepository { async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, Error>; async fn save(&self, user: &User) -> Result<(), Error>; @@ -308,14 +308,14 @@ where } } -// Usage +// Использование let repository = PostgresUserRepository::new(pool); let service = UserService::new(repository); ``` -### LINQ → Iterator Chains +### LINQ → цепочки итераторов ```csharp -// C# LINQ +// LINQ в C# var result = users .Where(u => u.Age > 18) .Select(u => u.Name.ToUpper()) @@ -325,7 +325,7 @@ var result = users ``` ```rust -// Rust: Iterator chains (zero-cost!) +// Rust: цепочки итераторов (с нулевой стоимостью!) let mut result: Vec<String> = users .iter() .filter(|u| u.age > 18) @@ -334,7 +334,7 @@ let mut result: Vec<String> = users result.sort(); result.truncate(10); -// Or with itertools crate for more LINQ-like chaining +// Или с крейтом itertools для более LINQ-подобной цепочки use itertools::Itertools; let result: Vec<String> = users @@ -346,9 +346,9 @@ let result: Vec<String> = users .collect(); ``` -### Entity Framework → SQLx + Migrations +### Entity Framework → SQLx и миграции ```csharp -// C# Entity Framework +// Entity Framework в C# public class ApplicationDbContext : DbContext { public DbSet<User> Users { get; set; } @@ -360,7 +360,7 @@ var user = await context.Users ``` ```rust -// Rust: SQLx with compile-time checked queries +// Rust: SQLx с запросами, проверяемыми на этапе компиляции use sqlx::{PgPool, FromRow}; #[derive(FromRow)] @@ -370,7 +370,7 @@ struct User { name: String, } -// Compile-time checked query +// Запрос, проверяемый на этапе компиляции let user = sqlx::query_as!( User, "SELECT id, email, name FROM users WHERE email = $1", @@ -379,7 +379,7 @@ let user = sqlx::query_as!( .fetch_optional(&pool) .await?; -// Or with dynamic queries +// Или динамический запрос let user = sqlx::query_as::<_, User>( "SELECT id, email, name FROM users WHERE email = $1" ) @@ -388,9 +388,9 @@ let user = sqlx::query_as::<_, User>( .await?; ``` -### Configuration → Config Crates +### Конфигурация → крейты конфигурации ```csharp -// C# Configuration +// Конфигурация в C# public class AppSettings { public string DatabaseUrl { get; set; } @@ -401,7 +401,7 @@ var config = builder.Configuration.Get<AppSettings>(); ``` ```rust -// Rust: Config with serde +// Rust: конфигурация через serde use config::{Config, ConfigError, Environment, File}; use serde::Deserialize; @@ -422,45 +422,45 @@ impl AppSettings { } } -// Usage +// Использование let settings = AppSettings::new()?; ``` --- -## Case Studies +## Разборы случаев -### Case Study 1: CLI Tool Migration (csvtool) +### Случай 1: миграция CLI-утилиты (csvtool) -**Background**: A team maintained a C# console app (`CsvProcessor`) that read large CSV files, applied transformations, and wrote output. At 500 MB files, memory usage spiked to 4 GB and GC pauses caused 30-second stalls. +**Контекст**: команда поддерживала консольное приложение на C# (`CsvProcessor`), которое читало большие CSV-файлы, применяло преобразования и записывало результат. При файлах в 500 МБ потребление памяти подскакивало до 4 ГБ, а паузы GC вызывали зависания на 30 секунд. -**Migration approach**: Rewrote in Rust over 2 weeks, one module at a time. +**Подход к миграции**: переписали на Rust за 2 недели, по одному модулю за раз. -| Step | What Changed | C# → Rust | +| Шаг | Что изменилось | C# → Rust | |------|-------------|-----------| -| 1 | CSV parsing | `CsvHelper` → `csv` crate (streaming `Reader`) | -| 2 | Data model | `class Record` → `struct Record` (stack-allocated, `#[derive(Deserialize)]`) | -| 3 | Transformations | LINQ `.Select().Where()` → `.iter().map().filter()` | -| 4 | File I/O | `StreamReader` → `BufReader<File>` with `?` error propagation | -| 5 | CLI args | `System.CommandLine` → `clap` with derive macros | -| 6 | Parallel processing | `Parallel.ForEach` → `rayon`'s `.par_iter()` | +| 1 | Разбор CSV | `CsvHelper` → крейт `csv` (потоковый `Reader`) | +| 2 | Модель данных | `class Record` → `struct Record` (размещается на стеке, `#[derive(Deserialize)]`) | +| 3 | Преобразования | LINQ `.Select().Where()` → `.iter().map().filter()` | +| 4 | Файловый ввод-вывод | `StreamReader` → `BufReader<File>` с распространением ошибок через `?` | +| 5 | Аргументы CLI | `System.CommandLine` → `clap` с derive-макросами | +| 6 | Параллельная обработка | `Parallel.ForEach` → `.par_iter()` из `rayon` | -**Results**: -- Memory: 4 GB → 12 MB (streaming instead of loading entire file) -- Speed: 45s → 3s for 500 MB file -- Binary size: single 2 MB executable, no runtime dependency +**Результаты**: +- Память: 4 ГБ → 12 МБ (потоковая обработка вместо загрузки всего файла) +- Скорость: 45 с → 3 с для файла в 500 МБ +- Размер бинарника: один исполняемый файл 2 МБ, без зависимости от рантайма -**Key lesson**: The biggest win wasn't Rust itself — it was that Rust's ownership model *forced* a streaming design. In C#, it was easy to `.ToList()` everything into memory. In Rust, the borrow checker naturally steered toward `Iterator`-based processing. +**Главный урок**: основной выигрыш был не в самом Rust — а в том, что модель владения Rust *вынуждала* к потоковому дизайну. В C# было легко сделать `.ToList()` и загрузить всё в память. В Rust проверщик заимствований естественно подталкивал к обработке на основе `Iterator`. -### Case Study 2: Microservice Replacement (auth-gateway) +### Случай 2: замена микросервиса (auth-gateway) -**Background**: A C# ASP.NET Core authentication gateway handled JWT validation and rate limiting for 50+ backend services. At 10K req/s, p99 latency hit 200ms with GC spikes. +**Контекст**: шлюз аутентификации на C# ASP.NET Core отвечал за валидацию JWT и ограничение частоты запросов для более чем 50 бэкенд-сервисов. При 10 000 запросов в секунду p99-задержка достигала 200 мс из-за всплесков GC. -**Migration approach**: Replaced with a Rust service using `axum` + `tower`, keeping the API contract identical. +**Подход к миграции**: заменили на сервис на Rust с использованием `axum` + `tower`, сохранив тот же контракт API. ```rust -// Before (C#): services.AddAuthentication().AddJwtBearer(...) -// After (Rust): tower middleware layer +// Было (C#): services.AddAuthentication().AddJwtBearer(...) +// Стало (Rust): слой middleware из tower use axum::{Router, middleware}; use tower::ServiceBuilder; @@ -474,28 +474,28 @@ let app = Router::new() ); ``` -| Metric | C# (ASP.NET Core) | Rust (axum) | +| Метрика | C# (ASP.NET Core) | Rust (axum) | |--------|-------------------|-------------| -| p50 latency | 5ms | 0.8ms | -| p99 latency | 200ms (GC spikes) | 4ms | -| Memory | 300 MB | 8 MB | -| Docker image | 210 MB (.NET runtime) | 12 MB (static binary) | -| Cold start | 2.1s | 0.05s | - -**Key lessons**: -1. **Keep the same API contract** — no client changes needed. Rust service was a drop-in replacement. -2. **Start with the hot path** — JWT validation was the bottleneck. Migrating just that one middleware would have captured 80% of the win. -3. **Use `tower` middleware** — it mirrors ASP.NET Core's middleware pipeline pattern, so C# developers found the Rust architecture familiar. -4. **p99 latency improvement** came from eliminating GC pauses, not from faster code — Rust's steady-state throughput was only 2x faster, but the absence of GC made the tail latency predictable. +| p50-задержка | 5 мс | 0,8 мс | +| p99-задержка | 200 мс (всплески GC) | 4 мс | +| Память | 300 МБ | 8 МБ | +| Docker-образ | 210 МБ (рантайм .NET) | 12 МБ (статический бинарник) | +| Холодный старт | 2,1 с | 0,05 с | + +**Ключевые уроки**: +1. **Сохраните тот же контракт API** — изменения на стороне клиентов не нужны. Сервис на Rust стал прямой заменой. +2. **Начните с горячего пути** — узким местом была валидация JWT. Миграция только этого middleware дала бы 80% выигрыша. +3. **Используйте middleware `tower`** — он повторяет конвейер middleware ASP.NET Core, поэтому разработчикам C# архитектура на Rust показалась знакомой. +4. **Улучшение p99-задержки** пришло от устранения пауз GC, а не от более быстрого кода — установившаяся пропускная способность Rust была лишь примерно в 2 раза выше, но отсутствие GC сделало «хвост» задержек предсказуемым. --- -## Exercises +## Упражнения <details> -<summary><strong>🏋️ Exercise: Migrate a C# Service</strong> (click to expand)</summary> +<summary><strong>🏋️ Упражнение: мигрируйте сервис на C#</strong> (нажмите, чтобы раскрыть)</summary> -Translate this C# service to idiomatic Rust: +Переведите этот сервис на C# на идиоматичный Rust: ```csharp public interface IUserService @@ -522,10 +522,10 @@ public class UserService : IUserService } ``` -**Hints**: Use a trait, `Option<User>` instead of null, `Result` instead of try/catch, and fix the SQL injection vulnerability. +**Подсказки**: используйте трейт, `Option<User>` вместо null, `Result` вместо try/catch, и устраните уязвимость SQL-инъекции. <details> -<summary>🔑 Solution</summary> +<summary>🔑 Решение</summary> ```rust use async_trait::async_trait; @@ -546,29 +546,29 @@ trait UserService: Send + Sync { } struct UserServiceImpl<D: Database> { - db: D, // No Arc needed — Rust's ownership handles it + db: D, // Arc не нужен — владение Rust справляется с этим само } #[async_trait] impl<D: Database> UserService for UserServiceImpl<D> { async fn get_by_id(&self, id: i64) -> Result<Option<User>, AppError> { - // Option instead of null; Result instead of try/catch + // Option вместо null; Result вместо try/catch Ok(self.db.get_user(id).await?) } async fn search(&self, query: &str) -> Result<Vec<User>, AppError> { - // Parameterized query — NO SQL injection! - // (sqlx uses $1 placeholders, not string interpolation) + // Параметризованный запрос — НИКАКИХ SQL-инъекций! + // (sqlx использует плейсхолдеры $1, а не интерполяцию строк) self.db.search_users(query).await.map_err(Into::into) } } ``` -**Key changes from C#**: -- `null` → `Option<User>` (compile-time null safety) -- `try/catch` → `Result` + `?` (explicit error propagation) -- SQL injection fixed: parameterized queries, not string interpolation -- `IDatabase _db` → generic `D: Database` (static dispatch, no boxing) +**Ключевые изменения по сравнению с C#**: +- `null` → `Option<User>` (null-безопасность на этапе компиляции) +- `try/catch` → `Result` + `?` (явное распространение ошибок) +- SQL-инъекция устранена: параметризованные запросы вместо интерполяции строк +- `IDatabase _db` → обобщение `D: Database` (статическая диспетчеризация, без упаковки) </details> </details> diff --git a/csharp-book/src/ch16-1-performance-comparison-and-migration.md b/csharp-book/src/ch16-1-performance-comparison-and-migration.md index 40200be..265dc70 100644 --- a/csharp-book/src/ch16-1-performance-comparison-and-migration.md +++ b/csharp-book/src/ch16-1-performance-comparison-and-migration.md @@ -1,29 +1,29 @@ -## Performance Comparison: Managed vs Native +## Сравнение производительности: управляемый и нативный код -> **What you'll learn:** Real-world performance differences between C# and Rust — startup time, -> memory usage, throughput benchmarks, CPU-intensive workloads, and a decision tree -> for when to migrate vs when to stay in C#. +> **Что вы узнаете:** реальные различия в производительности между C# и Rust — время запуска, потребление памяти, +> бенчмарки пропускной способности, задачи, интенсивные по CPU, а также дерево решений о том, +> когда стоит мигрировать, а когда остаться на C#. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### Real-World Performance Characteristics +### Реальные характеристики производительности -| **Aspect** | **C# (.NET)** | **Rust** | **Performance Impact** | +| **Аспект** | **C# (.NET)** | **Rust** | **Влияние на производительность** | |------------|---------------|----------|------------------------| -| **Startup Time** | 100-500ms (JIT); 5-30ms (.NET 8 AOT) | 1-10ms (native binary) | 🚀 **10-50x faster** (vs JIT) | -| **Memory Usage** | +30-100% (GC overhead + metadata) | Baseline (minimal runtime) | 💾 **30-50% less RAM** | -| **GC Pauses** | 1-100ms periodic pauses | Never (no GC) | ⚡ **Consistent latency** | -| **CPU Usage** | +10-20% (GC + JIT overhead) | Baseline (direct execution) | 🔋 **10-20% better efficiency** | -| **Binary Size** | 30-200MB (with runtime); 10-30MB (AOT trimmed) | 1-20MB (static binary) | 📦 **Smaller deployments** | -| **Memory Safety** | Runtime checks | Compile-time proofs | 🛡️ **Zero overhead safety** | -| **Concurrent Performance** | Good (with careful synchronization) | Excellent (fearless concurrency) | 🏃 **Superior scalability** | +| **Время запуска** | 100–500 мс (JIT); 5–30 мс (.NET 8 AOT) | 1–10 мс (нативный бинарник) | 🚀 **В 10–50 раз быстрее** (относительно JIT) | +| **Потребление памяти** | +30–100% (накладные расходы GC и метаданных) | Базовый уровень (минимальный рантайм) | 💾 **На 30–50% меньше RAM** | +| **Паузы GC** | Периодические паузы 1–100 мс | Никогда (нет GC) | ⚡ **Стабильная задержка** | +| **Загрузка CPU** | +10–20% (накладные расходы GC и JIT) | Базовый уровень (прямое исполнение) | 🔋 **На 10–20% эффективнее** | +| **Размер бинарника** | 30–200 МБ (с рантаймом); 10–30 МБ (AOT с обрезкой) | 1–20 МБ (статический бинарник) | 📦 **Компактнее развёртывание** | +| **Безопасность памяти** | Проверки во время выполнения | Доказательства на этапе компиляции | 🛡️ **Безопасность без накладных расходов** | +| **Производительность при конкурентности** | Хорошая (при аккуратной синхронизации) | Отличная (конкурентность без страха) | 🏃 **Превосходная масштабируемость** | -> **Note on .NET 8+ AOT**: Native AOT compilation closes the startup gap significantly (5-30ms). For throughput and memory, GC overhead and pauses remain. When evaluating a migration, benchmark your *specific workload* — headline numbers can be misleading. +> **Примечание о .NET 8+ AOT**: нативная AOT-компиляция существенно сокращает разрыв во времени запуска (5–30 мс). По пропускной способности и памяти накладные расходы GC и паузы сохраняются. Оценивая миграцию, измеряйте производительность на *вашей конкретной нагрузке* — цифры из заголовков могут вводить в заблуждение. -### Benchmark Examples +### Примеры бенчмарков ```csharp -// C# - JSON processing benchmark +// C# — бенчмарк обработки JSON public class JsonProcessor { public async Task<List<User>> ProcessJsonFile(string path) @@ -38,13 +38,13 @@ public class JsonProcessor } } -// Typical performance: ~200ms for 100MB file -// Memory usage: ~500MB peak (GC overhead) -// Binary size: ~80MB (self-contained) +// Типичная производительность: ~200 мс для файла 100 МБ +// Пиковое потребление памяти: ~500 МБ (накладные расходы GC) +// Размер бинарника: ~80 МБ (самодостаточный) ``` ```rust -// Rust - Equivalent JSON processing +// Rust — эквивалентная обработка JSON use serde::{Deserialize, Serialize}; use tokio::fs; @@ -65,15 +65,15 @@ pub async fn process_json_file(path: &str) -> Result<Vec<User>, Box<dyn std::err Ok(users) } -// Typical performance: ~120ms for same 100MB file -// Memory usage: ~200MB peak (no GC overhead) -// Binary size: ~8MB (static binary) +// Типичная производительность: ~120 мс для того же файла 100 МБ +// Пиковое потребление памяти: ~200 МБ (без накладных расходов GC) +// Размер бинарника: ~8 МБ (статический бинарник) ``` -### CPU-Intensive Workloads +### Задачи, интенсивные по CPU ```csharp -// C# - Mathematical computation +// C# — математические вычисления public class Mandelbrot { public static int[,] Generate(int width, int height, int maxIterations) @@ -96,12 +96,12 @@ public class Mandelbrot } } -// Performance: ~2.3 seconds (8-core machine) -// Memory: ~500MB +// Производительность: ~2,3 секунды (машина с 8 ядрами) +// Память: ~500 МБ ``` ```rust -// Rust - Same computation with Rayon +// Rust — то же вычисление с Rayon use rayon::prelude::*; use num_complex::Complex; @@ -122,57 +122,57 @@ pub fn generate_mandelbrot(width: usize, height: usize, max_iterations: u32) -> .collect() } -// Performance: ~1.1 seconds (same 8-core machine) -// Memory: ~200MB -// 2x faster with 60% less memory usage +// Производительность: ~1,1 секунды (та же машина с 8 ядрами) +// Память: ~200 МБ +// В 2 раза быстрее при на 60% меньшем потреблении памяти ``` -### When to Choose Each Language +### Когда выбирать каждый язык -**Choose C# when:** -- **Rapid development is crucial** - Rich tooling ecosystem -- **Team expertise in .NET** - Existing knowledge and skills -- **Enterprise integration** - Heavy use of Microsoft ecosystem -- **Moderate performance requirements** - Performance is adequate -- **Rich UI applications** - WPF, WinUI, Blazor applications -- **Prototyping and MVPs** - Fast time to market +**Выбирайте C#, когда:** +- **Быстрая разработка критична** — богатая экосистема инструментов +- **Экспертиза команды в .NET** — имеющиеся знания и навыки +- **Интеграция с корпоративными системами** — активное использование экосистемы Microsoft +- **Умеренные требования к производительности** — производительности достаточно +- **Приложения с богатым UI** — WPF, WinUI, Blazor +- **Прототипы и MVP** — быстрый выход на рынок -**Choose Rust when:** -- **Performance is critical** - CPU/memory-intensive applications -- **Resource constraints matter** - Embedded, edge computing, serverless -- **Long-running services** - Web servers, databases, system services -- **System-level programming** - OS components, drivers, network tools -- **High reliability requirements** - Financial systems, safety-critical applications -- **Concurrent/parallel workloads** - High-throughput data processing +**Выбирайте Rust, когда:** +- **Производительность критична** — приложения, интенсивные по CPU и памяти +- **Важны ограничения ресурсов** — встраиваемые системы, edge-вычисления, serverless +- **Долгоживущие сервисы** — веб-серверы, базы данных, системные сервисы +- **Системное программирование** — компоненты ОС, драйверы, сетевые утилиты +- **Высокие требования к надёжности** — финансовые системы, системы критической безопасности +- **Конкурентные и параллельные нагрузки** — обработка данных с высокой пропускной способностью -### Migration Strategy Decision Tree +### Дерево решений о стратегии миграции ```mermaid graph TD - START["Considering Rust?"] - PERFORMANCE["Is performance critical?"] - TEAM["Team has time to learn?"] - EXISTING["Large existing C# codebase?"] - NEW_PROJECT["New project or component?"] + START["Рассматриваете Rust?"] + PERFORMANCE["Производительность критична?"] + TEAM["У команды есть время на обучение?"] + EXISTING["Большая существующая кодовая база на C#?"] + NEW_PROJECT["Новый проект или компонент?"] - INCREMENTAL["Incremental adoption:<br/>• CLI tools first<br/>• Performance-critical components<br/>• New microservices"] + INCREMENTAL["Постепенное внедрение:<br/>• Сначала CLI-инструменты<br/>• Компоненты, критичные к производительности<br/>• Новые микросервисы"] - FULL_RUST["Full Rust adoption:<br/>• Greenfield projects<br/>• System-level services<br/>• High-performance APIs"] + FULL_RUST["Полный переход на Rust:<br/>• Новые проекты с нуля<br/>• Системные сервисы<br/>• Высокопроизводительные API"] - STAY_CSHARP["Stay with C#:<br/>• Optimize existing code<br/>• Use .NET AOT / performance features<br/>• Consider .NET Native"] + STAY_CSHARP["Остаться на C#:<br/>• Оптимизировать существующий код<br/>• Использовать возможности .NET AOT / производительности<br/>• Рассмотреть .NET Native"] START --> PERFORMANCE - PERFORMANCE -->|Yes| TEAM - PERFORMANCE -->|No| STAY_CSHARP + PERFORMANCE -->|Да| TEAM + PERFORMANCE -->|Нет| STAY_CSHARP - TEAM -->|Yes| EXISTING - TEAM -->|No| STAY_CSHARP + TEAM -->|Да| EXISTING + TEAM -->|Нет| STAY_CSHARP - EXISTING -->|Yes| NEW_PROJECT - EXISTING -->|No| FULL_RUST + EXISTING -->|Да| NEW_PROJECT + EXISTING -->|Нет| FULL_RUST - NEW_PROJECT -->|New| FULL_RUST - NEW_PROJECT -->|Existing| INCREMENTAL + NEW_PROJECT -->|Новый| FULL_RUST + NEW_PROJECT -->|Существующий| INCREMENTAL style FULL_RUST fill:#c8e6c9,color:#000 style INCREMENTAL fill:#fff3e0,color:#000 diff --git a/csharp-book/src/ch16-2-learning-path-and-resources.md b/csharp-book/src/ch16-2-learning-path-and-resources.md index 20658fc..8d107af 100644 --- a/csharp-book/src/ch16-2-learning-path-and-resources.md +++ b/csharp-book/src/ch16-2-learning-path-and-resources.md @@ -1,130 +1,130 @@ -## Learning Path and Next Steps +## Путь обучения и следующие шаги -> **What you'll learn:** A structured learning roadmap (weeks 1–2, months 1–3+), recommended books and resources, -> common pitfalls for C# developers (ownership confusion, fighting the borrow checker), -> and structured observability with `tracing` vs `ILogger`. +> **Что вы узнаете:** структурированную дорожную карту обучения (недели 1–2, месяцы 1–3+), рекомендуемые книги и ресурсы, +> типичные ловушки для разработчиков C# (путаница с владением, борьба с проверщиком заимствований), +> а также структурированную наблюдаемость с `tracing` против `ILogger`. > -> **Difficulty:** 🟢 Beginner - -### Immediate Next Steps (Week 1-2) -1. **Set up your environment** - - Install Rust via [rustup.rs](https://rustup.rs/) - - Configure VS Code with rust-analyzer extension - - Create your first `cargo new hello_world` project - -2. **Master the basics** - - Practice ownership with simple exercises - - Write functions with different parameter types (`&str`, `String`, `&mut`) - - Implement basic structs and methods - -3. **Error handling practice** - - Convert C# try-catch code to Result-based patterns - - Practice with `?` operator and `match` statements - - Implement custom error types - -### Intermediate Goals (Month 1-2) -1. **Collections and iterators** - - Master `Vec<T>`, `HashMap<K,V>`, and `HashSet<T>` - - Learn iterator methods: `map`, `filter`, `collect`, `fold` - - Practice with `for` loops vs iterator chains - -2. **Traits and generics** - - Implement common traits: `Debug`, `Clone`, `PartialEq` - - Write generic functions and structs - - Understand trait bounds and where clauses - -3. **Project structure** - - Organize code into modules - - Understand `pub` visibility - - Work with external crates from crates.io - -### Advanced Topics (Month 3+) -1. **Concurrency** - - Learn about `Send` and `Sync` traits - - Use `std::thread` for basic parallelism - - Explore `tokio` for async programming - -2. **Memory management** - - Understand `Rc<T>` and `Arc<T>` for shared ownership - - Learn when to use `Box<T>` for heap allocation - - Master lifetimes for complex scenarios - -3. **Real-world projects** - - Build a CLI tool with `clap` - - Create a web API with `axum` or `warp` - - Write a library and publish to crates.io - -### Recommended Learning Resources - -#### Books -- **"The Rust Programming Language"** (free online) - The official book -- **"Rust by Example"** (free online) - Hands-on examples -- **"Programming Rust"** by Jim Blandy - Deep technical coverage - -#### Online Resources -- [Rust Playground](https://play.rust-lang.org/) - Try code in browser -- [Rustlings](https://github.com/rust-lang/rustlings) - Interactive exercises -- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) - Practical examples - -#### Practice Projects -1. **Command-line calculator** - Practice with enums and pattern matching -2. **File organizer** - Work with filesystem and error handling -3. **JSON processor** - Learn serde and data transformation -4. **HTTP server** - Understand async programming and networking -5. **Database library** - Master traits, generics, and error handling - -### Common Pitfalls for C# Developers - -#### Ownership Confusion +> **Сложность:** 🟢 Начальный + +### Ближайшие шаги (недели 1–2) +1. **Настройте окружение** + - Установите Rust через [rustup.rs](https://rustup.rs/) + - Настройте VS Code с расширением rust-analyzer + - Создайте первый проект `cargo new hello_world` + +2. **Освойте основы** + - Практикуйте владение на простых упражнениях + - Пишите функции с разными типами параметров (`&str`, `String`, `&mut`) + - Реализуйте простые структуры и методы + +3. **Практика обработки ошибок** + - Переведите код C# с try-catch на паттерны на основе Result + - Практикуйтесь с оператором `?` и выражениями `match` + - Реализуйте собственные типы ошибок + +### Промежуточные цели (месяцы 1–2) +1. **Коллекции и итераторы** + - Освойте `Vec<T>`, `HashMap<K,V>` и `HashSet<T>` + - Изучите методы итераторов: `map`, `filter`, `collect`, `fold` + - Практикуйтесь: циклы `for` против цепочек итераторов + +2. **Трейты и обобщения** + - Реализуйте распространённые трейты: `Debug`, `Clone`, `PartialEq` + - Пишите обобщённые функции и структуры + - Разберитесь с границами трейтов и предложениями where + +3. **Структура проекта** + - Организуйте код в модули + - Разберитесь с видимостью `pub` + - Работайте с внешними крейтами с crates.io + +### Продвинутые темы (месяцы 3 и далее) +1. **Конкурентность** + - Изучите трейты `Send` и `Sync` + - Используйте `std::thread` для базового параллелизма + - Изучите `tokio` для асинхронного программирования + +2. **Управление памятью** + - Разберитесь с `Rc<T>` и `Arc<T>` для разделяемого владения + - Изучите, когда использовать `Box<T>` для выделения в куче + - Освойте времена жизни для сложных сценариев + +3. **Реальные проекты** + - Напишите CLI-утилиту с `clap` + - Создайте веб-API с `axum` или `warp` + - Напишите библиотеку и опубликуйте её на crates.io + +### Рекомендуемые ресурсы для обучения + +#### Книги +- **«The Rust Programming Language»** (бесплатно онлайн) — официальная книга +- **«Rust by Example»** (бесплатно онлайн) — примеры для практики +- **«Programming Rust»** Джима Блэнди — глубокое техническое изложение + +#### Онлайн-ресурсы +- [Rust Playground](https://play.rust-lang.org/) — попробовать код в браузере +- [Rustlings](https://github.com/rust-lang/rustlings) — интерактивные упражнения +- [Rust by Example](https://doc.rust-lang.org/rust-by-example/) — практические примеры + +#### Практические проекты +1. **Калькулятор командной строки** — практика с перечислениями и сопоставлением с образцом +2. **Организатор файлов** — работа с файловой системой и обработкой ошибок +3. **Обработчик JSON** — изучение serde и преобразования данных +4. **HTTP-сервер** — понимание асинхронного программирования и сетей +5. **Библиотека для работы с базой данных** — освоение трейтов, обобщений и обработки ошибок + +### Типичные ловушки для разработчиков C# + +#### Путаница с владением ```rust -// DON'T: Trying to use moved values +// НЕ НАДО: пытаться использовать перемещённые значения fn wrong_way() { let s = String::from("hello"); takes_ownership(s); - // println!("{}", s); // ERROR: s was moved + // println!("{}", s); // ОШИБКА: s было перемещено } -// DO: Use references or clone when needed +// НАДО: используйте ссылки или клонируйте при необходимости fn right_way() { let s = String::from("hello"); borrows_string(&s); - println!("{}", s); // OK: s is still owned here + println!("{}", s); // OK: s здесь по-прежнему принадлежит нам } -fn takes_ownership(s: String) { /* s is moved here */ } -fn borrows_string(s: &str) { /* s is borrowed here */ } +fn takes_ownership(s: String) { /* s перемещается сюда */ } +fn borrows_string(s: &str) { /* s заимствуется сюда */ } ``` -#### Fighting the Borrow Checker +#### Борьба с проверщиком заимствований ```rust -// DON'T: Multiple mutable references +// НЕ НАДО: несколько изменяемых ссылок одновременно fn wrong_borrowing() { let mut v = vec![1, 2, 3]; let r1 = &mut v; - // let r2 = &mut v; // ERROR: cannot borrow as mutable more than once + // let r2 = &mut v; // ОШИБКА: нельзя заимствовать как изменяемое больше одного раза } -// DO: Limit scope of mutable borrows +// НАДО: ограничивайте область видимости изменяемых заимствований fn right_borrowing() { let mut v = vec![1, 2, 3]; { let r1 = &mut v; r1.push(4); - } // r1 goes out of scope here + } // r1 выходит из области видимости здесь - let r2 = &mut v; // OK: no other mutable borrows exist + let r2 = &mut v; // OK: других изменяемых заимствований нет r2.push(5); } ``` -#### Expecting Null Values +#### Ожидание значений null ```rust -// DON'T: Expecting null-like behavior +// НЕ НАДО: ожидать поведения, похожего на null fn no_null_in_rust() { - // let s: String = null; // NO null in Rust! + // let s: String = null; // В Rust НЕТ null! } -// DO: Use Option<T> explicitly +// НАДО: явно используйте Option<T> fn use_option_instead() { let maybe_string: Option<String> = None; @@ -135,38 +135,38 @@ fn use_option_instead() { } ``` -### Final Tips +### Финальные советы -1. **Embrace the compiler** - Rust's compiler errors are helpful, not hostile -2. **Start small** - Begin with simple programs and gradually add complexity -3. **Read other people's code** - Study popular crates on GitHub -4. **Ask for help** - The Rust community is welcoming and helpful -5. **Practice regularly** - Rust's concepts become natural with practice +1. **Прислушивайтесь к компилятору** — ошибки компилятора Rust полезны, а не враждебны +2. **Начинайте с малого** — начните с простых программ и постепенно усложняйте +3. **Читайте чужой код** — изучайте популярные крейты на GitHub +4. **Просите помощи** — сообщество Rust дружелюбно и отзывчиво +5. **Практикуйтесь регулярно** — концепции Rust становятся естественными с практикой -Remember: Rust has a learning curve, but it pays off with memory safety, performance, and fearless concurrency. The ownership system that seems restrictive at first becomes a powerful tool for writing correct, efficient programs. +Помните: у Rust крутая кривая обучения, но она окупается безопасностью памяти, производительностью и конкурентностью без страха. Система владения, которая сначала кажется ограничивающей, становится мощным инструментом для написания корректных и эффективных программ. --- -**Congratulations!** You now have a solid foundation for transitioning from C# to Rust. Start with simple projects, be patient with the learning process, and gradually work your way up to more complex applications. The safety and performance benefits of Rust make the initial learning investment worthwhile. +**Поздравляем!** Теперь у вас есть прочная основа для перехода с C# на Rust. Начинайте с простых проектов, будьте терпеливы в процессе обучения и постепенно переходите к более сложным приложениям. Преимущества Rust в безопасности и производительности делают первоначальные инвестиции в обучение оправданными. <!-- ch16.2a: Structured Observability with tracing --> -## Structured Observability: `tracing` vs ILogger and Serilog +## Структурированная наблюдаемость: `tracing` против ILogger и Serilog -C# developers are accustomed to **structured logging** via `ILogger`, **Serilog**, or **NLog** — where log messages carry typed key-value properties. Rust's `log` crate provides basic leveled logging, but **`tracing`** is the production standard for structured observability with spans, async awareness, and distributed tracing support. +Разработчики C# привыкли к **структурированному логированию** через `ILogger`, **Serilog** или **NLog** — где сообщения журнала несут типизированные пары ключ-значение. Крейт `log` в Rust даёт базовое логирование с уровнями, но **`tracing`** — производственный стандарт структурированной наблюдаемости со спанами, поддержкой асинхронности и распределённой трассировкой. -### Why `tracing` Over `log` +### Почему `tracing`, а не `log` -| Feature | `log` crate | `tracing` crate | C# Equivalent | +| Возможность | Крейт `log` | Крейт `tracing` | Аналог в C# | |---------|------------|-----------------|----------------| -| Leveled messages | ✅ `info!()`, `error!()` | ✅ `info!()`, `error!()` | `ILogger.LogInformation()` | -| Structured fields | ❌ String interpolation only | ✅ Typed key-value fields | Serilog `Log.Information("{User}", user)` | -| Spans (scoped context) | ❌ | ✅ `#[instrument]`, `span!()` | `ILogger.BeginScope()` | -| Async-aware | ❌ Loses context across `.await` | ✅ Spans follow across `.await` | `Activity` / `DiagnosticSource` | -| Distributed tracing | ❌ | ✅ OpenTelemetry integration | `System.Diagnostics.Activity` | -| Multiple output formats | Basic | JSON, pretty, compact, OTLP | Serilog sinks | - -### Getting Started +| Сообщения с уровнями | ✅ `info!()`, `error!()` | ✅ `info!()`, `error!()` | `ILogger.LogInformation()` | +| Структурированные поля | ❌ Только интерполяция строк | ✅ Типизированные пары ключ-значение | Serilog `Log.Information("{User}", user)` | +| Спаны (ограниченный контекст) | ❌ | ✅ `#[instrument]`, `span!()` | `ILogger.BeginScope()` | +| Поддержка асинхронности | ❌ Контекст теряется на `.await` | ✅ Спаны сохраняются через `.await` | `Activity` / `DiagnosticSource` | +| Распределённая трассировка | ❌ | ✅ Интеграция с OpenTelemetry | `System.Diagnostics.Activity` | +| Несколько форматов вывода | Базовый | JSON, pretty, compact, OTLP | Приёмники (sinks) Serilog | + +### Начало работы ```toml # Cargo.toml [dependencies] @@ -174,39 +174,39 @@ tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } ``` -### Basic Usage: Structured Logging +### Базовое использование: структурированное логирование ```csharp -// C# Serilog +// Serilog в C# Log.Information("Processing order {OrderId} for {Customer}, total {Total:C}", orderId, customer.Name, order.Total); -// Output: Processing order 12345 for Alice, total $99.95 +// Вывод: Processing order 12345 for Alice, total $99.95 // JSON: {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...} ``` ```rust use tracing::{info, warn, error, debug, instrument}; -// Structured fields — typed, not string-interpolated +// Структурированные поля — типизированные, а не интерполированные в строку info!(order_id = 12345, customer = "Alice", total = 99.95, "Processing order"); -// Output: INFO Processing order order_id=12345 customer="Alice" total=99.95 +// Вывод: INFO Processing order order_id=12345 customer="Alice" total=99.95 // JSON: {"order_id": 12345, "customer": "Alice", "total": 99.95, ...} -// Dynamic values +// Динамические значения let order_id = 12345; -info!(order_id, "Order received"); // field name = variable name shorthand +info!(order_id, "Order received"); // сокращение: имя поля = имя переменной -// Conditional fields +// Условные поля if let Some(promo) = promo_code { info!(order_id, promo_code = %promo, "Promo applied"); - // ^ % means use Display formatting - // ? would use Debug formatting + // ^ % означает использовать форматирование Display + // ? использовал бы форматирование Debug } ``` -### Spans: The Killer Feature for Async Code +### Спаны: ключевая возможность для асинхронного кода -Spans are scoped contexts that carry fields across function calls and `.await` points — like `ILogger.BeginScope()` but async-safe. +Спаны — это ограниченные контексты, которые переносят поля через вызовы функций и точки `.await`. Это как `ILogger.BeginScope()`, но безопасно для асинхронного кода. ```csharp // C# — Activity / BeginScope @@ -217,36 +217,36 @@ using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId { _logger.LogInformation("Starting processing"); await ProcessPaymentAsync(); - _logger.LogInformation("Payment complete"); // OrderId still in scope + _logger.LogInformation("Payment complete"); // OrderId всё ещё в области видимости } ``` ```rust use tracing::{info, instrument, Instrument}; -// #[instrument] automatically creates a span with function args as fields +// #[instrument] автоматически создаёт спан с аргументами функции в качестве полей #[instrument(skip(db), fields(customer_name))] async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> { let order = db.get_order(order_id).await?; - // Add a field to the current span dynamically + // Динамически добавляем поле в текущий спан tracing::Span::current().record("customer_name", &order.customer_name.as_str()); info!("Starting processing"); - process_payment(&order).await?; // span context preserved across .await! + process_payment(&order).await?; // контекст спана сохраняется через .await! info!(items = order.items.len(), "Payment complete"); Ok(()) } -// Every log message inside this function automatically includes: +// Каждое сообщение журнала внутри этой функции автоматически содержит: // order_id=12345 customer_name="Alice" -// Even in nested async calls! +// Даже во вложенных асинхронных вызовах! -// Manual span creation (like BeginScope) +// Ручное создание спана (как BeginScope) async fn batch_process(orders: Vec<u64>, db: &Database) { for order_id in orders { let span = tracing::info_span!("process_order", order_id); - // .instrument(span) attaches the span to the future + // .instrument(span) прикрепляет спан к future process_order(order_id, db) .instrument(span) .await @@ -255,52 +255,52 @@ async fn batch_process(orders: Vec<u64>, db: &Database) { } ``` -### Subscriber Configuration (Like Serilog Sinks) +### Настройка подписчика (аналог приёмников Serilog) ```rust use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; fn init_tracing() { - // Development: human-readable, colored output + // Разработка: читаемый человеком цветной вывод tracing_subscriber::registry() .with(EnvFilter::try_from_default_env() .unwrap_or_else(|_| "my_app=debug,tower_http=info".into())) - .with(fmt::layer().pretty()) // Colored, indented spans + .with(fmt::layer().pretty()) // Цветные, с отступами спаны .init(); } fn init_tracing_production() { - // Production: JSON output for log aggregation (like Serilog JSON sink) + // Продакшен: вывод в JSON для агрегации логов (как JSON-приёмник Serilog) tracing_subscriber::registry() .with(EnvFilter::new("my_app=info")) - .with(fmt::layer().json()) // Structured JSON + .with(fmt::layer().json()) // Структурированный JSON .init(); - // Output: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...} + // Вывод: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...} } ``` ```bash -# Control log levels via environment variable (like Serilog MinimumLevel) +# Управление уровнями логирования через переменную окружения (как MinimumLevel в Serilog) RUST_LOG=my_app=debug,hyper=warn cargo run -RUST_LOG=trace cargo run # everything +RUST_LOG=trace cargo run # всё подряд ``` -### Serilog → tracing Migration Cheat Sheet +### Шпаргалка по переходу с Serilog на tracing -| Serilog / ILogger | tracing | Notes | +| Serilog / ILogger | tracing | Примечания | |-------------------|---------|-------| -| `Log.Information("{Key}", val)` | `info!(key = val, "message")` | Fields are typed, not interpolated | -| `Log.ForContext("Key", val)` | `span.record("key", val)` | Add fields to current span | -| `using BeginScope(...)` | `#[instrument]` or `info_span!()` | Automatic with `#[instrument]` | -| `.WriteTo.Console()` | `fmt::layer()` | Human-readable | -| `.WriteTo.Seq()` / `.File()` | `fmt::layer().json()` + file redirect | Or use `tracing-appender` | -| `.Enrich.WithProperty()` | `span!(Level::INFO, "name", key = val)` | Span fields | -| `LogEventLevel.Debug` | `tracing::Level::DEBUG` | Same concept | -| `{@Object}` destructuring | `field = ?value` (Debug) or `%value` (Display) | `?` = Debug, `%` = Display | - -### OpenTelemetry Integration +| `Log.Information("{Key}", val)` | `info!(key = val, "message")` | Поля типизированы, не интерполируются | +| `Log.ForContext("Key", val)` | `span.record("key", val)` | Добавляет поля в текущий спан | +| `using BeginScope(...)` | `#[instrument]` или `info_span!()` | Автоматически с `#[instrument]` | +| `.WriteTo.Console()` | `fmt::layer()` | Читаемый человеком вывод | +| `.WriteTo.Seq()` / `.File()` | `fmt::layer().json()` + перенаправление в файл | Или используйте `tracing-appender` | +| `.Enrich.WithProperty()` | `span!(Level::INFO, "name", key = val)` | Поля спана | +| `LogEventLevel.Debug` | `tracing::Level::DEBUG` | Та же концепция | +| Деструктуризация `{@Object}` | `field = ?value` (Debug) или `%value` (Display) | `?` = Debug, `%` = Display | + +### Интеграция с OpenTelemetry ```toml -# For distributed tracing (like System.Diagnostics + OTLP exporter) +# Для распределённой трассировки (как System.Diagnostics + экспортёр OTLP) [dependencies] tracing-opentelemetry = "0.22" opentelemetry = "0.21" @@ -308,7 +308,7 @@ opentelemetry-otlp = "0.14" ``` ```rust -// Add OpenTelemetry layer alongside console output +// Добавляем слой OpenTelemetry вместе с выводом в консоль use tracing_opentelemetry::OpenTelemetryLayer; fn init_otel() { @@ -319,11 +319,11 @@ fn init_otel() { .expect("Failed to create OTLP tracer"); tracing_subscriber::registry() - .with(OpenTelemetryLayer::new(tracer)) // Send spans to Jaeger/Tempo - .with(fmt::layer()) // Also print to console + .with(OpenTelemetryLayer::new(tracer)) // Отправляем спаны в Jaeger/Tempo + .with(fmt::layer()) // Также выводим в консоль .init(); } -// Now #[instrument] spans automatically become distributed traces! +// Теперь спаны от #[instrument] автоматически становятся распределёнными трассами! ``` *** diff --git a/csharp-book/src/ch16-3-rust-tooling-ecosystem.md b/csharp-book/src/ch16-3-rust-tooling-ecosystem.md index 9b7bccb..edb9709 100644 --- a/csharp-book/src/ch16-3-rust-tooling-ecosystem.md +++ b/csharp-book/src/ch16-3-rust-tooling-ecosystem.md @@ -1,125 +1,125 @@ -## Essential Rust Tooling for C# Developers +## Основные инструменты Rust для разработчиков C# -> **What you'll learn:** Rust's development tools mapped to their C# equivalents — Clippy (Roslyn analyzers), -> rustfmt (dotnet format), cargo doc (XML docs), cargo watch (dotnet watch), and VS Code extensions. +> **Что вы узнаете:** инструменты разработки Rust и их аналоги в C# — Clippy (анализаторы Roslyn), +> rustfmt (dotnet format), cargo doc (XML-документация), cargo watch (dotnet watch) и расширения VS Code. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -### Tool Comparison +### Сравнение инструментов -| C# Tool | Rust Equivalent | Install | Purpose | +| Инструмент C# | Аналог в Rust | Установка | Назначение | |---------|----------------|---------|---------| -| Roslyn analyzers | **Clippy** | `rustup component add clippy` | Lint + style suggestions | -| `dotnet format` | **rustfmt** | `rustup component add rustfmt` | Auto-formatting | -| XML doc comments | **`cargo doc`** | Built-in | Generate HTML docs | -| OmniSharp / Roslyn | **rust-analyzer** | VS Code extension | IDE support | -| `dotnet watch` | **cargo-watch** | `cargo install cargo-watch` | Auto-rebuild on save | -| — | **cargo-expand** | `cargo install cargo-expand` | See macro expansion | -| `dotnet audit` | **cargo-audit** | `cargo install cargo-audit` | Security vulnerability scan | - -### Clippy: Your Automated Code Reviewer +| Анализаторы Roslyn | **Clippy** | `rustup component add clippy` | Линтинг и подсказки по стилю | +| `dotnet format` | **rustfmt** | `rustup component add rustfmt` | Автоматическое форматирование | +| XML-комментарии к документации | **`cargo doc`** | Встроен | Генерация HTML-документации | +| OmniSharp / Roslyn | **rust-analyzer** | Расширение VS Code | Поддержка в IDE | +| `dotnet watch` | **cargo-watch** | `cargo install cargo-watch` | Автоматическая пересборка при сохранении | +| — | **cargo-expand** | `cargo install cargo-expand` | Просмотр раскрытия макросов | +| `dotnet audit` | **cargo-audit** | `cargo install cargo-audit` | Проверка уязвимостей безопасности | + +### Clippy: ваш автоматический код-ревьюер ```bash -# Run Clippy on your project +# Запуск Clippy для проекта cargo clippy -# Treat warnings as errors (CI/CD) +# Считать предупреждения ошибками (CI/CD) cargo clippy -- -D warnings -# Auto-fix suggestions +# Автоматическое применение подсказок cargo clippy --fix ``` ```rust -// Clippy catches hundreds of anti-patterns: +// Clippy находит сотни антипаттернов: -// Before Clippy: -if x == true { } // warning: equality check with bool -let _ = vec.len() == 0; // warning: use .is_empty() instead -for i in 0..vec.len() { } // warning: use .iter().enumerate() +// До Clippy: +if x == true { } // предупреждение: проверка равенства с bool +let _ = vec.len() == 0; // предупреждение: используйте .is_empty() +for i in 0..vec.len() { } // предупреждение: используйте .iter().enumerate() -// After Clippy suggestions: +// После применения подсказок Clippy: if x { } let _ = vec.is_empty(); for (i, item) in vec.iter().enumerate() { } ``` -### rustfmt: Consistent Formatting +### rustfmt: единообразное форматирование ```bash -# Format all files +# Форматирование всех файлов cargo fmt -# Check formatting without changing (CI/CD) +# Проверка форматирования без изменений (CI/CD) cargo fmt -- --check ``` ```toml -# rustfmt.toml — customize formatting (like .editorconfig) +# rustfmt.toml — настройка форматирования (как .editorconfig) max_width = 100 tab_spaces = 4 use_field_init_shorthand = true ``` -### cargo doc: Documentation Generation +### cargo doc: генерация документации ```bash -# Generate and open docs (including dependencies) +# Генерация и открытие документации (включая зависимости) cargo doc --open -# Run documentation tests +# Запуск тестов документации cargo test --doc ``` ```rust -/// Calculate the area of a circle. +/// Вычисляет площадь круга. /// -/// # Arguments -/// * `radius` - The radius of the circle (must be non-negative) +/// # Аргументы +/// * `radius` — радиус круга (должен быть неотрицательным) /// -/// # Examples +/// # Примеры /// ``` /// let area = my_crate::circle_area(5.0); /// assert!((area - 78.54).abs() < 0.01); /// ``` /// -/// # Panics -/// Panics if `radius` is negative. +/// # Паника +/// Паникует, если `radius` отрицательный. pub fn circle_area(radius: f64) -> f64 { assert!(radius >= 0.0, "radius must be non-negative"); std::f64::consts::PI * radius * radius } -// The code in /// ``` blocks is compiled and run during `cargo test`! +// Код внутри блоков /// ``` компилируется и запускается во время `cargo test`! ``` -### cargo watch: Auto-Rebuild +### cargo watch: автоматическая пересборка ```bash -# Rebuild on file changes (like dotnet watch) -cargo watch -x check # Type-check only (fastest) -cargo watch -x test # Run tests on save -cargo watch -x 'run -- args' # Run program on save -cargo watch -x clippy # Lint on save +# Пересборка при изменении файлов (как dotnet watch) +cargo watch -x check # Только проверка типов (самый быстрый) +cargo watch -x test # Запуск тестов при сохранении +cargo watch -x 'run -- args' # Запуск программы при сохранении +cargo watch -x clippy # Линтинг при сохранении ``` -### cargo expand: See What Macros Generate +### cargo expand: просмотр того, что генерируют макросы ```bash -# See the expanded output of derive macros -cargo expand --lib # Expand lib.rs -cargo expand module_name # Expand specific module +# Просмотр раскрытого вывода производных макросов +cargo expand --lib # Раскрыть lib.rs +cargo expand module_name # Раскрыть конкретный модуль ``` -### Recommended VS Code Extensions +### Рекомендуемые расширения VS Code -| Extension | Purpose | -|-----------|---------| -| **rust-analyzer** | Code completion, inline errors, refactoring | -| **CodeLLDB** | Debugger (like Visual Studio debugger) | -| **Even Better TOML** | Cargo.toml syntax highlighting | -| **crates** | Show latest crate versions in Cargo.toml | -| **Error Lens** | Inline error/warning display | +| Расширение | Назначение | +|-----------|-----------| +| **rust-analyzer** | Автодополнение кода, встроенные ошибки, рефакторинг | +| **CodeLLDB** | Отладчик (как отладчик Visual Studio) | +| **Even Better TOML** | Подсветка синтаксиса Cargo.toml | +| **crates** | Показ последних версий крейтов в Cargo.toml | +| **Error Lens** | Вывод ошибок и предупреждений прямо в строке | *** -For deeper exploration of advanced topics mentioned in this guide, see the companion training documents: +Для более глубокого изучения продвинутых тем, упомянутых в этом руководстве, см. сопутствующие учебные материалы: -- **[Rust Patterns](../../rust-patterns-book/src/SUMMARY.md)** — Pin projections, custom allocators, arena patterns, lock-free data structures, and advanced unsafe patterns -- **[Async Rust Training](../../async-book/src/SUMMARY.md)** — Deep dive into tokio, async cancellation safety, stream processing, and production async architectures -- **[Rust Training for C++ Developers](../../c-cpp-book/src/SUMMARY.md)** — Useful if your team also has C++ experience; covers move semantics mapping, RAII differences, and template vs generics -- **[Rust Training for C Developers](../../c-cpp-book/src/SUMMARY.md)** — Relevant for interop scenarios; covers FFI patterns, embedded Rust debugging, and `no_std` programming \ No newline at end of file +- **[Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md)** — проекции Pin, пользовательские аллокаторы, арена-паттерны, структуры данных без блокировок и продвинутые паттерны unsafe +- **[Async Rust Training](../../async-book/src/SUMMARY.md)** — подробно о tokio, безопасности отмены в асинхронном коде, обработке потоков и производственной архитектуре async +- **[Rust для программистов C/C++](../../c-cpp-book/src/SUMMARY.md)** — полезно, если в команде есть опыт работы с C++; рассматривает соответствие семантики перемещения, различия RAII и шаблоны против обобщений +- **[Rust для программистов C](../../c-cpp-book/src/SUMMARY.md)** — актуально для сценариев взаимодействия; рассматривает паттерны FFI, отладку встраиваемого Rust и программирование с `no_std` diff --git a/csharp-book/src/ch16-best-practices.md b/csharp-book/src/ch16-best-practices.md index 5988230..368bd35 100644 --- a/csharp-book/src/ch16-best-practices.md +++ b/csharp-book/src/ch16-best-practices.md @@ -1,39 +1,38 @@ -## Best Practices for C# Developers +## Лучшие практики для разработчиков C# -> **What you'll learn:** Five critical mindset shifts (GC→ownership, exceptions→Results, inheritance→composition), -> idiomatic project organization, error handling strategy, testing patterns, and the most common -> mistakes C# developers make in Rust. +> **Что вы узнаете:** пять ключевых сдвигов мышления (GC→владение, исключения→Result, наследование→композиция), +> идиоматичную организацию проекта, стратегию обработки ошибок, паттерны тестирования и самые частые ошибки разработчиков C# в Rust. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -### 1. **Mindset Shifts** -- **From GC to Ownership**: Think about who owns data and when it's freed -- **From Exceptions to Results**: Make error handling explicit and visible -- **From Inheritance to Composition**: Use traits to compose behavior -- **From Null to Option**: Make absence of values explicit in the type system +### 1. **Сдвиги мышления** +- **От GC к владению**: думайте о том, кому принадлежат данные и когда их освобождают +- **От исключений к Result**: делайте обработку ошибок явной и видимой +- **От наследования к композиции**: используйте трейты для композиции поведения +- **От null к Option**: делайте отсутствие значения явным в системе типов -### 2. **Code Organization** +### 2. **Организация кода** ```rust -// Structure projects like C# solutions +// Структурируем проекты, как решения (solutions) C# src/ -├── main.rs // Program.cs equivalent -├── lib.rs // Library entry point -├── models/ // Like Models/ folder in C# +├── main.rs // Аналог Program.cs +├── lib.rs // Точка входа библиотеки +├── models/ // Аналог папки Models/ в C# │ ├── mod.rs │ ├── user.rs │ └── product.rs -├── services/ // Like Services/ folder +├── services/ // Аналог папки Services/ │ ├── mod.rs │ ├── user_service.rs │ └── product_service.rs -├── controllers/ // Like Controllers/ (for web apps) -├── repositories/ // Like Repositories/ -└── utils/ // Like Utilities/ +├── controllers/ // Аналог Controllers/ (для веб-приложений) +├── repositories/ // Аналог Repositories/ +└── utils/ // Аналог Utilities/ ``` -### 3. **Error Handling Strategy** +### 3. **Стратегия обработки ошибок** ```rust -// Create a common Result type for your application +// Создаём общий тип Result для приложения pub type AppResult<T> = Result<T, AppError>; #[derive(Error, Debug)] @@ -51,21 +50,21 @@ pub enum AppError { Business { message: String }, } -// Use throughout your application +// Используем во всём приложении pub async fn create_user(data: CreateUserRequest) -> AppResult<User> { - validate_user_data(&data)?; // Returns AppError::Validation - let user = repository.create_user(data).await?; // Returns AppError::Database + validate_user_data(&data)?; // Возвращает AppError::Validation + let user = repository.create_user(data).await?; // Возвращает AppError::Database Ok(user) } ``` -### 4. **Testing Patterns** +### 4. **Паттерны тестирования** ```rust -// Structure tests like C# unit tests +// Структурируем тесты, как модульные тесты C# #[cfg(test)] mod tests { use super::*; - use rstest::*; // For parameterized tests like C# [Theory] + use rstest::*; // Для параметризованных тестов, как [Theory] в C# #[test] fn test_basic_functionality() { @@ -87,7 +86,7 @@ mod tests { assert_eq!(add(a, b), expected); } - #[tokio::test] // For async tests + #[tokio::test] // Для асинхронных тестов async fn test_async_functionality() { let result = async_function().await; assert!(result.is_ok()); @@ -95,13 +94,13 @@ mod tests { } ``` -### 5. **Common Mistakes to Avoid** +### 5. **Частые ошибки, которых стоит избегать** ```rust -// [ERROR] Don't try to implement inheritance -// Instead of: -// struct Manager : Employee // This doesn't exist in Rust +// [ERROR] Не пытайтесь реализовать наследование +// Вместо: +// struct Manager : Employee // В Rust такого нет -// [OK] Use composition with traits +// [OK] Используйте композицию через трейты trait Employee { fn get_salary(&self) -> u32; } @@ -110,10 +109,10 @@ trait Manager: Employee { fn get_team_size(&self) -> usize; } -// [ERROR] Don't use unwrap() everywhere (like ignoring exceptions) -let value = might_fail().unwrap(); // Can panic! +// [ERROR] Не используйте unwrap() везде (это как игнорирование исключений) +let value = might_fail().unwrap(); // Может вызвать панику! -// [OK] Handle errors properly +// [OK] Обрабатывайте ошибки правильно let value = match might_fail() { Ok(v) => v, Err(e) => { @@ -122,109 +121,109 @@ let value = match might_fail() { } }; -// [ERROR] Don't clone everything (like copying objects unnecessarily) -let data = expensive_data.clone(); // Expensive! +// [ERROR] Не клонируйте всё подряд (как лишнее копирование объектов) +let data = expensive_data.clone(); // Дорого! -// [OK] Use borrowing when possible -let data = &expensive_data; // Just a reference +// [OK] Используйте заимствование, когда возможно +let data = &expensive_data; // Всего лишь ссылка -// [ERROR] Don't use RefCell everywhere (like making everything mutable) +// [ERROR] Не используйте RefCell повсюду (как превращение всего в изменяемое) struct Data { - value: RefCell<i32>, // Interior mutability - use sparingly + value: RefCell<i32>, // Внутренняя изменяемость — используйте осторожно } -// [OK] Prefer owned or borrowed data +// [OK] Предпочитайте владеемые или заимствованные данные struct Data { - value: i32, // Simple and clear + value: i32, // Просто и понятно } ``` -This guide provides C# developers with a comprehensive understanding of how their existing knowledge translates to Rust, highlighting both the similarities and the fundamental differences in approach. The key is understanding that Rust's constraints (like ownership) are designed to prevent entire classes of bugs that are possible in C#, at the cost of some initial complexity. +Это руководство даёт разработчикам C# цельное представление о том, как их существующие знания переносятся на Rust, подчёркивая как сходства, так и принципиальные различия в подходе. Ключевое — понимать, что ограничения Rust (например, владение) призваны предотвратить целые классы ошибок, возможных в C#, ценой некоторой первоначальной сложности. --- -### 6. **Avoiding Excessive `clone()`** 🟡 +### 6. **Избегаем избыточного `clone()`** 🟡 -C# developers instinctively clone data because the GC handles the cost. In Rust, every `.clone()` is an explicit allocation. Most can be eliminated with borrowing. +Разработчики C# инстинктивно клонируют данные, потому что стоимость берёт на себя GC. В Rust каждый `.clone()` — это явное выделение памяти. Большинство из них можно убрать с помощью заимствования. ```rust -// [ERROR] C# habit: cloning strings to pass around +// [ERROR] Привычка из C#: клонировать строки, чтобы передавать их дальше fn greet(name: String) { println!("Hello, {name}"); } let user_name = String::from("Alice"); -greet(user_name.clone()); // unnecessary allocation -greet(user_name.clone()); // and again +greet(user_name.clone()); // ненужное выделение памяти +greet(user_name.clone()); // и снова -// [OK] Borrow instead — zero allocation +// [OK] Заимствуйте вместо этого — без выделений памяти fn greet(name: &str) { println!("Hello, {name}"); } let user_name = String::from("Alice"); -greet(&user_name); // borrows -greet(&user_name); // borrows again — no cost +greet(&user_name); // заимствуем +greet(&user_name); // заимствуем снова — без затрат ``` -**When clone is appropriate:** -- Moving data into a thread or `'static` closure (`Arc::clone` is cheap — it bumps a counter) -- Caching: you genuinely need an independent copy -- Prototyping: get it working, then remove clones later +**Когда clone уместен:** +- Перемещение данных в поток или в замыкание `'static` (`Arc::clone` дёшев — он лишь увеличивает счётчик) +- Кэширование: вам действительно нужна независимая копия +- Прототипирование: сначала заставьте работать, потом уберите лишние clone -**Decision checklist:** -1. Can you pass `&T` or `&str` instead? → Do that -2. Does the callee need ownership? → Pass by move, not clone -3. Is it shared across threads? → Use `Arc<T>` (clone is just a reference count bump) -4. None of the above? → `clone()` is justified +**Чек-лист решения:** +1. Можно передать `&T` или `&str`? → Так и сделайте +2. Вызываемой стороне нужно владение? → Передавайте перемещением, а не клоном +3. Данные разделяются между потоками? → Используйте `Arc<T>` (clone — это лишь увеличение счётчика ссылок) +4. Ничего из перечисленного? → `clone()` оправдан --- -### 7. **Avoiding `unwrap()` in Production Code** 🟡 +### 7. **Избегаем `unwrap()` в продакшен-коде** 🟡 -C# developers who ignore exceptions write `.unwrap()` everywhere in Rust. Both are equally dangerous. +Разработчики C#, которые игнорируют исключения, пишут `.unwrap()` везде в Rust. И то и другое одинаково опасно. ```rust -// [ERROR] The "I'll fix this later" trap +// [ERROR] Ловушка «потом поправлю» let config = std::fs::read_to_string("config.toml").unwrap(); let port: u16 = config_value.parse().unwrap(); let conn = db_pool.get().await.unwrap(); -// [OK] Propagate with ? in application code +// [OK] Пробрасываем через ? в прикладном коде let config = std::fs::read_to_string("config.toml")?; let port: u16 = config_value.parse()?; let conn = db_pool.get().await?; -// [OK] Use expect() only when failure is truly a bug +// [OK] Используйте expect() только тогда, когда сбой — действительно ошибка в программе let home = std::env::var("HOME") - .expect("HOME environment variable must be set"); // documents the invariant + .expect("HOME environment variable must be set"); // фиксирует инвариант ``` -**Rule of thumb:** -| Method | When to use | +**Эмпирическое правило:** +| Метод | Когда использовать | |--------|------------| -| `?` | Application/library code — propagate to caller | -| `expect("reason")` | Startup assertions, invariants that *must* hold | -| `unwrap()` | Tests only, or after an `is_some()`/`is_ok()` check | -| `unwrap_or(default)` | When you have a sensible fallback | -| `unwrap_or_else(|| ...)` | When the fallback is expensive to compute | +| `?` | Код приложения и библиотек — передать вызывающему коду | +| `expect("reason")` | Проверки при запуске, инварианты, которые *обязаны* выполняться | +| `unwrap()` | Только в тестах или после проверки `is_some()`/`is_ok()` | +| `unwrap_or(default)` | Когда есть разумное значение по умолчанию | +| `unwrap_or_else(|| ...)` | Когда значение по умолчанию дорого вычислять | --- -### 8. **Fighting the Borrow Checker (and How to Stop)** 🟡 +### 8. **Борьба с проверщиком заимствований (и как перестать бороться)** 🟡 -Every C# developer hits a phase where the borrow checker rejects valid-seeming code. The fix is usually a structural change, not a workaround. +Каждый разработчик C# проходит этап, когда проверщик заимствований отвергает код, который кажется правильным. Исправление обычно требует изменения структуры, а не обходного манёвра. ```rust -// [ERROR] Trying to mutate while iterating (C# foreach + modify pattern) +// [ERROR] Попытка изменять коллекцию во время перебора (паттерн foreach + изменение из C#) let mut items = vec![1, 2, 3, 4, 5]; for item in &items { if *item > 3 { - items.push(*item * 2); // ERROR: can't borrow items as mutable + items.push(*item * 2); // ОШИБКА: нельзя заимствовать items как изменяемое } } -// [OK] Collect first, then mutate +// [OK] Сначала собираем, потом изменяем let extras: Vec<i32> = items.iter() .filter(|&&x| x > 3) .map(|&x| x * 2) @@ -233,36 +232,36 @@ items.extend(extras); ``` ```rust -// [ERROR] Returning a reference to a local (C# returns references freely via GC) +// [ERROR] Возврат ссылки на локальную переменную (в C# ссылки свободно возвращаются благодаря GC) fn get_greeting() -> &str { let s = String::from("hello"); - &s // ERROR: s is dropped at end of function + &s // ОШИБКА: s уничтожается в конце функции } -// [OK] Return owned data +// [OK] Возвращаем владеемые данные fn get_greeting() -> String { - String::from("hello") // caller owns it + String::from("hello") // вызывающий код владеет ими } ``` -**Common patterns that resolve borrow checker conflicts:** +**Распространённые паттерны, которые снимают конфликты с проверщиком заимствований:** -| C# habit | Rust solution | +| Привычка из C# | Решение в Rust | |----------|--------------| -| Store references in structs | Use owned data, or add lifetime parameters | -| Mutate shared state freely | Use `Arc<Mutex<T>>` or restructure to avoid sharing | -| Return references to locals | Return owned values | -| Modify collection while iterating | Collect changes, then apply | -| Multiple mutable references | Split struct into independent parts | +| Хранить ссылки в структурах | Использовать владеемые данные или добавить параметры времён жизни | +| Свободно изменять разделяемое состояние | Использовать `Arc<Mutex<T>>` или перестроить код, чтобы не разделять данные | +| Возвращать ссылки на локальные переменные | Возвращать владеемые значения | +| Изменять коллекцию во время перебора | Сначала собрать изменения, потом применить | +| Несколько изменяемых ссылок | Разбить структуру на независимые части | --- -### 9. **Collapsing Assignment Pyramids** 🟢 +### 9. **Устраняем «пирамиды» присваиваний** 🟢 -C# developers write chains of `if (x != null) { if (x.Value > 0) { ... } }`. Rust's `match`, `if let`, and `?` flatten these. +Разработчики C# пишут цепочки вида `if (x != null) { if (x.Value > 0) { ... } }`. Конструкции `match`, `if let` и `?` в Rust позволяют выровнять такой код. ```rust -// [ERROR] Nested null-checking style from C# +// [ERROR] Вложенный стиль проверок на null из C# fn process(input: Option<String>) -> Option<usize> { match input { Some(s) => { @@ -285,7 +284,7 @@ fn process(input: Option<String>) -> Option<usize> { } } -// [OK] Flatten with combinators +// [OK] Выравниваем с помощью комбинаторов fn process(input: Option<String>) -> Option<usize> { input .filter(|s| !s.is_empty()) @@ -295,16 +294,13 @@ fn process(input: Option<String>) -> Option<usize> { } ``` -**Key combinators every C# developer should know:** +**Ключевые комбинаторы, которые должен знать каждый разработчик C#:** -| Combinator | What it does | C# equivalent | +| Комбинатор | Что делает | Аналог в C# | |-----------|-------------|---------------| -| `map` | Transform the inner value | `Select` / null-conditional `?.` | -| `and_then` | Chain operations that return Option/Result | `SelectMany` / `?.Method()` | -| `filter` | Keep value only if predicate passes | `Where` | -| `unwrap_or` | Provide default | `?? defaultValue` | -| `ok()` | Convert `Result` to `Option` (discard error) | — | -| `transpose` | Flip `Option<Result>` to `Result<Option>` | — | - - - +| `map` | Преобразует внутреннее значение | `Select` / null-условный `?.` | +| `and_then` | Связывает операции, возвращающие Option/Result | `SelectMany` / `?.Method()` | +| `filter` | Оставляет значение, только если предикат истинен | `Where` | +| `unwrap_or` | Задаёт значение по умолчанию | `?? defaultValue` | +| `ok()` | Превращает `Result` в `Option` (отбрасывая ошибку) | — | +| `transpose` | Меняет местами `Option<Result>` и `Result<Option>` | — | diff --git a/csharp-book/src/ch17-capstone-project.md b/csharp-book/src/ch17-capstone-project.md index abc6bb6..ddf03a3 100644 --- a/csharp-book/src/ch17-capstone-project.md +++ b/csharp-book/src/ch17-capstone-project.md @@ -1,20 +1,20 @@ -## Capstone Project: Build a CLI Weather Tool +## Итоговый проект: CLI-утилита для погоды на Rust -> **What you'll learn:** How to combine everything — structs, traits, error handling, async, modules, -> serde, and CLI argument parsing — into a working Rust application. This mirrors the kind of tool -> a C# developer would build with `HttpClient`, `System.Text.Json`, and `System.CommandLine`. +> **Что вы узнаете:** как объединить всё изученное — структуры, трейты, обработку ошибок, асинхронность, модули, +> serde и разбор аргументов командной строки — в работающее приложение на Rust. Это повторяет тот тип утилиты, который +> разработчик C# написал бы с `HttpClient`, `System.Text.Json` и `System.CommandLine`. > -> **Difficulty:** 🟡 Intermediate +> **Сложность:** 🟡 Средний -This capstone pulls together concepts from every part of the book. You'll build `weather-cli`, a command-line tool that fetches weather data from an API and displays it. The project is structured as a mini-crate with proper module layout, error types, and tests. +Этот итоговый проект собирает концепции из всех частей книги. Вы создадите `weather-cli` — утилиту командной строки, которая получает данные о погоде из API и выводит их. Проект организован как мини-крейт с правильной структурой модулей, типами ошибок и тестами. -### Project Overview +### Обзор проекта ```mermaid graph TD - CLI["main.rs<br/>clap CLI parser"] --> Client["client.rs<br/>reqwest + tokio"] - Client -->|"HTTP GET"| API["Weather API"] - Client -->|"JSON → struct"| Model["weather.rs<br/>serde Deserialize"] + CLI["main.rs<br/>разбор CLI через clap"] --> Client["client.rs<br/>reqwest + tokio"] + Client -->|"HTTP GET"| API["Погодный API"] + Client -->|"JSON → структура"| Model["weather.rs<br/>serde Deserialize"] Model --> Display["display.rs<br/>fmt::Display"] CLI --> Err["error.rs<br/>thiserror"] Client --> Err @@ -24,35 +24,35 @@ graph TD style Model fill:#c8e6c9,color:#000 ``` -**What you'll build:** +**Что вы создадите:** ``` $ weather-cli --city "Seattle" 🌧 Seattle: 12°C, Overcast clouds - Humidity: 82% Wind: 5.4 m/s + Влажность: 82% Ветер: 5.4 м/с ``` -**Concepts exercised:** -| Book Chapter | Concept Used Here | +**Используемые концепции:** +| Глава книги | Концепция, используемая здесь | |---|---| -| Ch05 (Structs) | `WeatherReport`, `Config` data types | -| Ch08 (Modules) | `src/lib.rs`, `src/client.rs`, `src/display.rs` | -| Ch09 (Errors) | Custom `WeatherError` with `thiserror` | -| Ch10 (Traits) | `Display` impl for formatted output | -| Ch11 (From/Into) | JSON deserialization via `serde` | -| Ch12 (Iterators) | Processing API response arrays | -| Ch13 (Async) | `reqwest` + `tokio` for HTTP calls | -| Ch14-1 (Testing) | Unit tests + integration test | +| Гл. 05 (Структуры) | Типы данных `WeatherReport`, `Config` | +| Гл. 08 (Модули) | `src/lib.rs`, `src/client.rs`, `src/display.rs` | +| Гл. 09 (Ошибки) | Собственный `WeatherError` с `thiserror` | +| Гл. 10 (Трейты) | Реализация `Display` для форматированного вывода | +| Гл. 11 (From/Into) | Десериализация JSON через `serde` | +| Гл. 12 (Итераторы) | Обработка массивов из ответа API | +| Гл. 13 (Async) | `reqwest` + `tokio` для HTTP-запросов | +| Гл. 14-1 (Тестирование) | Модульные тесты и интеграционный тест | --- -### Step 1: Project Setup +### Шаг 1: настройка проекта ```bash cargo new weather-cli cd weather-cli ``` -Add dependencies to `Cargo.toml`: +Добавьте зависимости в `Cargo.toml`: ```toml [package] name = "weather-cli" @@ -60,28 +60,28 @@ version = "0.1.0" edition = "2021" [dependencies] -clap = { version = "4", features = ["derive"] } # CLI args (like System.CommandLine) -reqwest = { version = "0.12", features = ["json"] } # HTTP client (like HttpClient) -serde = { version = "1", features = ["derive"] } # Serialization (like System.Text.Json) +clap = { version = "4", features = ["derive"] } # Аргументы CLI (как System.CommandLine) +reqwest = { version = "0.12", features = ["json"] } # HTTP-клиент (как HttpClient) +serde = { version = "1", features = ["derive"] } # Сериализация (как System.Text.Json) serde_json = "1" -thiserror = "2" # Error types -tokio = { version = "1", features = ["full"] } # Async runtime +thiserror = "2" # Типы ошибок +tokio = { version = "1", features = ["full"] } # Асинхронный рантайм ``` ```csharp -// C# equivalent dependencies: +// Эквивалентные зависимости в C#: // dotnet add package System.CommandLine // dotnet add package System.Net.Http.Json -// (System.Text.Json and HttpClient are built-in) +// (System.Text.Json и HttpClient входят в состав платформы) ``` -### Step 2: Define Your Data Types +### Шаг 2: определите типы данных -Create `src/weather.rs`: +Создайте `src/weather.rs`: ```rust use serde::Deserialize; -/// Raw API response (matches JSON shape) +/// Сырой ответ API (повторяет структуру JSON) #[derive(Deserialize, Debug)] pub struct ApiResponse { pub main: MainData, @@ -107,7 +107,7 @@ pub struct WindData { pub speed: f64, } -/// Our domain type (clean, decoupled from API) +/// Наш доменный тип (чистый, отделённый от API) #[derive(Debug, Clone)] pub struct WeatherReport { pub city: String, @@ -136,38 +136,38 @@ impl From<ApiResponse> for WeatherReport { ``` ```csharp -// C# equivalent: +// Эквивалент в C#: // public record ApiResponse(MainData Main, List<WeatherCondition> Weather, ...); // public record WeatherReport(string City, double TempCelsius, ...); -// Manual mapping or AutoMapper +// Ручное маппирование или AutoMapper ``` -**Key difference:** `#[derive(Deserialize)]` + `From` impl replaces C#'s `JsonSerializer.Deserialize<T>()` + AutoMapper. Both happen at compile time in Rust — no reflection. +**Ключевое отличие:** `#[derive(Deserialize)]` + реализация `From` заменяют `JsonSerializer.Deserialize<T>()` + AutoMapper. В Rust оба этих шага выполняются на этапе компиляции — без рефлексии. -### Step 3: Error Type +### Шаг 3: тип ошибок -Create `src/error.rs`: +Создайте `src/error.rs`: ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum WeatherError { - #[error("HTTP request failed: {0}")] + #[error("Ошибка HTTP-запроса: {0}")] Http(#[from] reqwest::Error), - #[error("City not found: {0}")] + #[error("Город не найден: {0}")] CityNotFound(String), - #[error("API key not set — export WEATHER_API_KEY")] + #[error("Ключ API не задан — выполните export WEATHER_API_KEY")] MissingApiKey, } pub type Result<T> = std::result::Result<T, WeatherError>; ``` -### Step 4: HTTP Client +### Шаг 4: HTTP-клиент -Create `src/client.rs`: +Создайте `src/client.rs`: ```rust use crate::error::{WeatherError, Result}; use crate::weather::{ApiResponse, WeatherReport}; @@ -204,21 +204,21 @@ impl WeatherClient { ``` ```csharp -// C# equivalent: +// Эквивалент в C#: // var response = await _httpClient.GetAsync(url); // if (response.StatusCode == HttpStatusCode.NotFound) // throw new CityNotFoundException(city); // var data = await response.Content.ReadFromJsonAsync<ApiResponse>(); ``` -**Key differences:** -- `?` operator replaces `try/catch` — errors propagate automatically via `Result` -- `WeatherReport::from(api_data)` uses the `From` trait instead of AutoMapper -- No `IHttpClientFactory` — `reqwest::Client` handles connection pooling internally +**Ключевые отличия:** +- Оператор `?` заменяет `try/catch` — ошибки распространяются автоматически через `Result` +- `WeatherReport::from(api_data)` использует трейт `From` вместо AutoMapper +- `IHttpClientFactory` не нужен — `reqwest::Client` сам управляет пулом соединений -### Step 5: Display Formatting +### Шаг 5: форматирование вывода -Create `src/display.rs`: +Создайте `src/display.rs`: ```rust use std::fmt; use crate::weather::WeatherReport; @@ -228,7 +228,7 @@ impl fmt::Display for WeatherReport { let icon = weather_icon(&self.description); writeln!(f, "{} {}: {:.0}°C, {}", icon, self.city, self.temp_celsius, self.description)?; - write!(f, " Humidity: {}% Wind: {:.1} m/s", + write!(f, " Влажность: {}% Ветер: {:.1} м/с", self.humidity, self.wind_speed) } } @@ -244,7 +244,7 @@ fn weather_icon(description: &str) -> &str { } ``` -### Step 6: Wire It All Together +### Шаг 6: собираем всё вместе `src/lib.rs`: ```rust @@ -260,9 +260,9 @@ use clap::Parser; use weather_cli::{client::WeatherClient, error::WeatherError}; #[derive(Parser)] -#[command(name = "weather-cli", about = "Fetch weather from the command line")] +#[command(name = "weather-cli", about = "Получение погоды из командной строки")] struct Cli { - /// City name to look up + /// Название города для поиска #[arg(short, long)] city: String, } @@ -274,7 +274,7 @@ async fn main() { let api_key = match std::env::var("WEATHER_API_KEY") { Ok(key) => key, Err(_) => { - eprintln!("Error: {}", WeatherError::MissingApiKey); + eprintln!("Ошибка: {}", WeatherError::MissingApiKey); std::process::exit(1); } }; @@ -284,21 +284,21 @@ async fn main() { match client.get_weather(&cli.city).await { Ok(report) => println!("{report}"), Err(WeatherError::CityNotFound(city)) => { - eprintln!("City not found: {city}"); + eprintln!("Город не найден: {city}"); std::process::exit(1); } Err(e) => { - eprintln!("Error: {e}"); + eprintln!("Ошибка: {e}"); std::process::exit(1); } } } ``` -### Step 7: Tests +### Шаг 7: тесты ```rust -// In src/weather.rs or tests/weather_test.rs +// В src/weather.rs или tests/weather_test.rs #[cfg(test)] mod tests { use super::*; @@ -351,23 +351,23 @@ mod tests { --- -### Final File Layout +### Итоговая структура файлов ``` weather-cli/ ├── Cargo.toml ├── src/ -│ ├── main.rs # CLI entry point (clap) -│ ├── lib.rs # Module declarations -│ ├── client.rs # HTTP client (reqwest + tokio) -│ ├── weather.rs # Data types + From impl + tests -│ ├── display.rs # Display formatting -│ └── error.rs # WeatherError + Result alias +│ ├── main.rs # Точка входа CLI (clap) +│ ├── lib.rs # Объявления модулей +│ ├── client.rs # HTTP-клиент (reqwest + tokio) +│ ├── weather.rs # Типы данных + реализация From + тесты +│ ├── display.rs # Форматирование вывода +│ └── error.rs # WeatherError + псевдоним Result └── tests/ - └── integration.rs # Integration tests + └── integration.rs # Интеграционные тесты ``` -Compare to the C# equivalent: +Сравните с эквивалентом на C#: ``` WeatherCli/ ├── WeatherCli.csproj @@ -381,15 +381,15 @@ WeatherCli/ └── WeatherTests.cs ``` -**The Rust version is remarkably similar in structure.** The main differences are: -- `mod` declarations instead of namespaces -- `Result<T, E>` instead of exceptions -- `From` trait instead of AutoMapper -- Explicit `#[tokio::main]` instead of built-in async runtime +**Версия на Rust удивительно похожа по структуре.** Основные различия: +- Объявления `mod` вместо пространств имён +- `Result<T, E>` вместо исключений +- Трейт `From` вместо AutoMapper +- Явный `#[tokio::main]` вместо встроенного асинхронного рантайма -### Bonus: Integration Test Stub +### Бонус: заготовка интеграционного теста -Create `tests/integration.rs` to test the public API without hitting a real server: +Создайте `tests/integration.rs`, чтобы проверить публичный API без обращения к настоящему серверу: ```rust // tests/integration.rs @@ -412,20 +412,20 @@ fn weather_report_display_roundtrip() { } ``` -Run with `cargo test` — Rust discovers tests in both `src/` (`#[cfg(test)]` modules) and `tests/` (integration tests) automatically. No test framework configuration needed — compare that to setting up xUnit/NUnit in C#. +Запустите `cargo test` — Rust автоматически находит тесты и в `src/` (модули `#[cfg(test)]`), и в `tests/` (интеграционные тесты). Никакой настройки тестового фреймворка не требуется — сравните с настройкой xUnit/NUnit в C#. --- -### Extension Challenges +### Задания для продолжения -Once it works, try these to deepen your skills: +Когда всё заработает, попробуйте эти задания, чтобы углубить навыки: -1. **Add caching** — Store the last API response in a file. On startup, check if it's less than 10 minutes old and skip the HTTP call. This exercises `std::fs`, `serde_json::to_writer`, and `SystemTime`. +1. **Добавьте кэширование** — сохраняйте последний ответ API в файл. При запуске проверяйте, не старше ли он 10 минут, и если нет — пропускайте HTTP-запрос. Это упражнение на `std::fs`, `serde_json::to_writer` и `SystemTime`. -2. **Add multiple cities** — Accept `--city "Seattle,Portland,Vancouver"` and fetch all concurrently with `tokio::join!`. This exercises concurrent async. +2. **Добавьте поддержку нескольких городов** — принимайте `--city "Seattle,Portland,Vancouver"` и загружайте все города конкурентно через `tokio::join!`. Это упражнение на конкурентное асинхронное программирование. -3. **Add a `--format json` flag** — Output the report as JSON instead of human-readable text using `serde_json::to_string_pretty`. This exercises conditional formatting and `Serialize`. +3. **Добавьте флаг `--format json`** — выводите отчёт в формате JSON вместо читаемого текста с помощью `serde_json::to_string_pretty`. Это упражнение на условное форматирование и `Serialize`. -4. **Write an integration test** — Create `tests/integration.rs` that tests the full flow with a mock HTTP server using `wiremock`. This exercises the `tests/` directory pattern from ch14-1. +4. **Напишите интеграционный тест** — создайте `tests/integration.rs`, который проверяет полный сценарий с имитацией HTTP-сервера через `wiremock`. Это упражнение на шаблон каталога `tests/` из главы 14-1. *** From f860909f1eda62fbe3ceb7b0fc726510943545f6 Mon Sep 17 00:00:00 2001 From: r007 <moninsergei@gmail.com> Date: Fri, 9 Oct 2026 11:01:30 +0400 Subject: [PATCH 18/18] Finish Russian translation: metadata, README, xtask, docker, own project docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - book.toml: Russian titles, language = "ru", repo URL and edit links point to r007/RustTraining-Rus - Replace the Microsoft author bios with neutral attribution to the original material - Rewrite README, CONTRIBUTING, CODE_OF_CONDUCT and SECURITY for this repo - Translate xtask (CLI text, landing page, comments); update the CI smoke test to the new page title - Translate docker/ and CI workflow comments and step names - Fix async ch00: capstone is ch17, not гл. 16 - Point cross-book links at the published Pages URLs (32 broken links fixed) - type-driven ch07: exhaustive match in should_alert, so a new sensor event variant is a compile error as the text claims - Translate remaining headings, table labels and code comments Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- .dockerignore | 8 +- .github/workflows/docker.yml | 21 +-- .github/workflows/pages.yml | 18 +- .gitignore | 6 +- CODE_OF_CONDUCT.md | 28 ++- CONTRIBUTING.md | 55 ++++-- Cargo.lock | 101 +++++++++++ README.md | 116 +++++++------ SECURITY.md | 23 ++- async-book/book.toml | 9 +- async-book/src/ch00-introduction.md | 10 +- async-book/src/ch10-async-traits.md | 2 +- async-book/src/ch13-production-patterns.md | 2 +- c-cpp-book/book.toml | 9 +- c-cpp-book/src/ch00-introduction.md | 2 +- .../src/ch01-introduction-and-motivation.md | 9 +- c-cpp-book/src/ch15-1-embedded-deep-dive.md | 2 +- csharp-book/book.toml | 9 +- .../src/ch01-introduction-and-motivation.md | 9 +- csharp-book/src/ch02-getting-started.md | 4 +- .../src/ch13-1-asyncawait-deep-dive.md | 2 +- csharp-book/src/ch13-concurrency.md | 2 +- csharp-book/src/ch14-unsafe-rust-and-ffi.md | 4 +- .../src/ch16-3-rust-tooling-ecosystem.md | 8 +- docker/Dockerfile | 53 +++--- docker/README.md | 58 +++---- docker/compose.yaml | 8 +- docker/nginx.conf | 20 +-- engineering-book/book.toml | 9 +- engineering-book/src/ch00-introduction.md | 8 +- ...ependency-management-and-supply-chain-s.md | 4 +- .../ch08-compile-time-and-developer-tools.md | 4 +- python-book/book.toml | 9 +- python-book/src/ch00-introduction.md | 2 +- .../src/ch01-introduction-and-motivation.md | 9 +- python-book/src/ch11-from-and-into-traits.md | 2 +- rust-patterns-book/book.toml | 5 +- rust-patterns-book/src/ch00-introduction.md | 8 +- .../src/ch02-traits-in-depth.md | 2 +- type-driven-correctness-book/book.toml | 5 +- .../src/ch00-introduction.md | 20 +-- ...alidated-boundaries-parse-dont-validate.md | 10 +- .../ch11-fourteen-tricks-from-the-trenches.md | 4 +- ...onst-fn-compile-time-correctness-proofs.md | 4 +- xtask/src/main.rs | 160 +++++++++--------- 45 files changed, 511 insertions(+), 352 deletions(-) diff --git a/.dockerignore b/.dockerignore index 652c688..5563600 100755 --- a/.dockerignore +++ b/.dockerignore @@ -1,17 +1,17 @@ -# Keep the build context small and deterministic. -# NOTE: do NOT exclude *.md — the book sources are markdown. +# Держим контекст сборки небольшим и детерминированным. +# ПРИМЕЧАНИЕ: НЕ исключайте *.md — исходники книг написаны в Markdown. .git .github .gitignore -# Build output (mirrors .gitignore) +# Результаты сборки (повторяют .gitignore) target/ site/ docs/ **/book/ -# Editor / OS noise +# Файлы редакторов и ОС .vscode/ .idea/ **/*:Zone.Identifier diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2de737c..de3e829 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,7 +1,8 @@ -name: Docker image +name: Образ Docker -# Build-only. Nothing is published, so this adds no release surface or -# registry credentials — it exists so the Dockerfile cannot silently rot. +# Только сборка. Ничего не публикуется, поэтому нет дополнительной поверхности +# релизов и учётных данных реестра. Workflow нужен, чтобы Dockerfile не +# сломался незаметно. on: push: branches: [main] @@ -31,7 +32,7 @@ jobs: - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - - name: Build image + - name: Сборка образа uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . @@ -42,7 +43,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Smoke test + - name: Дымовой тест run: | docker run -d --name books -p 3000:8080 rust-training:ci for i in $(seq 1 30); do @@ -50,12 +51,12 @@ jobs: sleep 2 done if [ "${ok:-0}" != "1" ]; then - echo "::error::server did not come up"; docker logs books; exit 1 + echo "::error::сервер не поднялся"; docker logs books; exit 1 fi - # Landing page and at least one book must resolve. - # NB: match the <title>; the <h1> is split by a <span> tag. - curl -fsS http://localhost:3000/ | grep -q "<title>Rust Training Books" + # Обложка и хотя бы одна книга должны открываться. + # ПРИМЕЧАНИЕ: сравниваем по , потому что <h1> разбит тегом <span>. + curl -fsS http://localhost:3000/ | grep -q "<title>Учебные книги по Rust" curl -fsS -o /dev/null -w '%{http_code}\n' http://localhost:3000/async-book/ | grep -q 200 - # Extensionless links must resolve via try_files. + # Ссылки без расширения должны открываться через try_files. curl -fsS -o /dev/null -w '%{http_code}\n' http://localhost:3000/async-book/ch00-introduction | grep -q 200 docker rm -f books diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6d729ce..27abbdd 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,4 +1,4 @@ -name: Deploy to GitHub Pages +name: Публикация на GitHub Pages on: push: @@ -18,16 +18,16 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Получение кода uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Install Rust + - name: Установка Rust uses: dtolnay/rust-toolchain@stable - - name: Configure GitHub Pages + - name: Настройка GitHub Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - - name: Cache cargo dependencies + - name: Кэш зависимостей cargo uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | @@ -40,15 +40,15 @@ jobs: restore-keys: | ${{ runner.os }}-cargo- - - name: Install mdbook and dependencies + - name: Установка mdbook и зависимостей run: | which mdbook || cargo install mdbook which mdbook-mermaid || cargo install mdbook-mermaid - - name: Build documentation + - name: Сборка документации run: cargo xtask deploy - - name: Upload artifact + - name: Загрузка артефакта uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ./docs @@ -60,6 +60,6 @@ jobs: runs-on: ubuntu-latest needs: build steps: - - name: Deploy to GitHub Pages + - name: Публикация на GitHub Pages id: deployment uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.gitignore b/.gitignore index 98a04b9..254382b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ -# mdbook build output +# Результат сборки mdbook **/book/ -# xtask output +# Результат xtask site/ docs/ target/ -# Zone.Identifier files (Windows/WSL artifact) +# Файлы Zone.Identifier (артефакт Windows/WSL) *:Zone.Identifier diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 397fed5..68c7af0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,11 +1,25 @@ -# Microsoft Open Source Code of Conduct +# Кодекс поведения участников -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +Мы хотим, чтобы участие в проекте было открытым и безопасным для всех, независимо от опыта, возраста, пола, национальности, религии, инвалидности и других личных признаков. -Resources: +## Чего мы ждём -- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) -- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns -- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support) +- Уважительного общения и готовности выслушать других. +- Конструктивной критики, которая направлена на текст и код, а не на человека. +- Внимания к технической сути: проверяйте факты и ссылки на документацию. +- Признания чужого вклада, в том числе в переводе и в ревью. +## Что недопустимо + +- Оскорбления, унижения, преследование и травля. +- Публикация чужих личных данных без согласия. +- Сознательное введение в заблуждение, спам и реклама, не связанные с проектом. +- Любое поведение, которое создаёт враждебную обстановку для участников. + +## Ответственность + +Мейнтейнеры проекта могут удалить комментарии, коммиты и Pull Request'ы, которые нарушают этот кодекс, и ограничить доступ к обсуждениям участникам, которые систематически его нарушают. + +## Как сообщить о проблеме + +Вопросы о кодексе задавайте через Issues репозитория. Если обращение конфиденциально, напишите мейнтейнеру напрямую через его профиль на GitHub: [@r007](https://github.com/r007). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ebf23ac..b9716e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,14 +1,47 @@ -# Contributing +# Как внести вклад -This project welcomes contributions and suggestions. Most contributions require you to -agree to a Contributor License Agreement (CLA) declaring that you have the right to, -and actually do, grant us the rights to use your contribution. For details, visit -https://cla.microsoft.com. +Спасибо, что хотите помочь проекту! Здесь принимаются исправления текста и перевода, правки примеров кода, улучшения сборки и предложения новых тем. -When you submit a pull request, a CLA-bot will automatically determine whether you need -to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the -instructions provided by the bot. You will only need to do this once across all repositories using our CLA. +Подписывать соглашение о вкладе (CLA) не нужно. Отправляя изменения, вы соглашаетесь, что они распространяются по лицензиям проекта: MIT для кода и CC BY 4.0 для текста. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) -or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +## Что можно предложить + +- Исправления опечаток, неточностей и ошибок в переводе. +- Улучшения примеров кода и упражнений. Код должен компилироваться. +- Исправления ссылок, якорей и сборки. +- Новые главы или книги. Сначала откройте Issue, чтобы обсудить объём и структуру. + +## Как работать + +1. Сделайте fork репозитория и создайте ветку от `main`. +2. Внесите изменения и проверьте сборку: `cargo xtask build` или `cd <книга> && mdbook build`. +3. Откройте Pull Request с кратким описанием того, что изменено и почему. + +## Стиль перевода + +- Обращение к читателю на «вы». +- Термины используем единообразно (см. таблицу ниже). +- Код, идентификаторы, имена файлов, команды и сообщения компилятора остаются на английском. +- Комментарии в примерах кода пишем по-русски. +- Имена файлов и якорные ссылки без необходимости не меняем: от них зависят ссылки внутри книг и связь с оригиналом. + +## Глоссарий + +| English | Русский | Примечание | +|---------|---------|------------| +| trait | трейт | | +| crate | крейт | | +| ownership | владение | | +| borrowing | заимствование | | +| lifetime | время жизни | | +| closure | замыкание | | +| iterator | итератор | | +| panic | паника | | +| thread | поток | | +| channel | канал | | +| runtime | рантайм | | +| Future, Pin, Mutex, Send, Sync, PhantomData, newtype, typestate, UB | — | оставляем английскими | + +## Кодекс поведения + +Участие в проекте регулируется [кодексом поведения](CODE_OF_CONDUCT.md). diff --git a/Cargo.lock b/Cargo.lock index 6c15da8..de68588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,107 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "xtask" version = "0.1.0" +dependencies = [ + "ctrlc", +] diff --git a/README.md b/README.md index f90561f..5d9d6b8 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,72 @@ -
- -**Лицензия** Проект распространяется по двойной лицензии: [MIT License](LICENSE) и [Creative Commons Attribution 4.0 International (CC-BY-4.0)](LICENSE-DOCS). - -
- -
- -**Товарные знаки** Проект может содержать товарные знаки или логотипы проектов, продуктов и сервисов. Использование товарных знаков и логотипов Microsoft допускается только в соответствии с [правилами использования товарных знаков и фирменного стиля Microsoft](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Использование товарных знаков или логотипов Microsoft в изменённых версиях проекта не должно вводить в заблуждение и создавать впечатление поддержки со стороны Microsoft. Использование сторонних товарных знаков и логотипов регулируется политиками их правообладателей. - -
- -> *Это перевод README на русский язык. Для юридических формулировок (лицензии и товарные знаки) приоритет имеет английская версия.* - # Учебные книги по Rust -Семь учебных курсов по Rust для разработчиков с разным опытом, а также углублённые материалы по async, продвинутым паттернам и инженерным практикам. +Семь учебных курсов по Rust на русском языке: от перехода с C/C++, C#, Python до асинхронного программирования, продвинутых паттернов, корректности на уровне типов и инженерной практики в продакшене. -Материалы сочетают оригинальное содержание с идеями и примерами, вдохновлёнными лучшими ресурсами экосистемы Rust. Цель — подробная и технически точная программа, которая собирает знания, разбросанные по книгам, блогам, докладам и видеосериям, в цельный и педагогически выстроенный курс. +Каждую книгу можно читать отдельно, но порядок в таблице ниже соответствует естественному пути обучения. Текст — на русском. Код, имена крейтов, команды и сообщения компилятора остаются на английском, как принято в экосистеме Rust. -> **Важно:** Эти книги — учебные материалы, а не авторитетный справочник. Мы стремимся к точности, но критически важные детали всегда сверяйте с [официальной документацией Rust](https://doc.rust-lang.org/) и [Rust Reference](https://doc.rust-lang.org/reference/). +> **Важно:** это учебные материалы, а не авторитетный справочник. Мы стремимся к точности, но ключевые детали сверяйте с [официальной документацией Rust](https://doc.rust-lang.org/) и [Rust Reference](https://doc.rust-lang.org/reference/). -### Источники вдохновения и благодарности - -- [**The Rust Programming Language**](https://doc.rust-lang.org/book/) — основа, на которой строится всё остальное -- [**Jon Gjengset**](https://www.youtube.com/c/JonGjengset) — глубокие стримы о внутреннем устройстве Rust, серия `Crust of Rust` -- [**withoutboats**](https://without.boats/blog/) — дизайн async, `Pin` и модель futures -- [**fasterthanlime (Amos)**](https://fasterthanli.me/) — системное программирование с нуля и подробные развёрнутые разборы -- [**Mara Bos**](https://marabos.nl/) — *Rust Atomics and Locks*, примитивы конкурентности -- [**Aleksey Kladov (matklad)**](https://matklad.github.io/) — идеи о rust-analyzer, дизайне API и обработке ошибок -- [**Niko Matsakis**](https://smallcultfollowing.com/babysteps/) — дизайн языка, внутреннее устройство borrow checker, Polonius -- [**Rust by Example**](https://doc.rust-lang.org/rust-by-example/) и [**Rustonomicon**](https://doc.rust-lang.org/nomicon/) — практические паттерны и углублённое изучение unsafe -- [**This Week in Rust**](https://this-week-in-rust.org/) — находки сообщества, которые повлияли на многие примеры -- [**Binary Musings - Tag(Rust)**](https://binarymusings.org/posts/category/rust/) — глубокое погружение во внутреннее устройство Rust -- …и многие другие участники **сообщества Rust**, чьи статьи, доклады, RFC и обсуждения на форумах легли в основу этих материалов. Их слишком много, чтобы перечислить поимённо, но мы искренне благодарны каждому +**Читать онлайн:** [r007.github.io/RustTraining-Rus](https://r007.github.io/RustTraining-Rus/) +**Исходный код:** [github.com/r007/RustTraining-Rus](https://github.com/r007/RustTraining-Rus) ## 📖 С чего начать -Выберите книгу, которая соответствует вашему опыту. Книги сгруппированы по сложности, чтобы вы могли построить путь обучения: +Выберите книгу, которая соответствует вашему опыту. Книги сгруппированы по сложности: | Уровень | Описание | |---------|----------| -| 🟢 **Мост** | Изучение Rust для тех, кто пришёл из другого языка — начните отсюда | +| 🟢 **Мост** | Изучение Rust для тех, кто пришёл из другого языка. Начните отсюда | | 🔵 **Погружение** | Углублённое изучение крупной подсистемы Rust | | 🟡 **Продвинутый** | Паттерны и приёмы для опытных Rust-разработчиков | | 🟣 **Эксперт** | Передовые техники на уровне типов и корректности | -| 🟤 **Практики** | Инженерия, инструменты и готовность к продакшну | +| 🟤 **Практики** | Инженерия, инструменты и готовность к продакшену | | Книга | Уровень | Для кого | |-------|---------|----------| -| [**Rust для программистов C/C++**](https://microsoft.github.io/RustTraining/c-cpp-book/) | 🟢 Мост | Семантика перемещения, RAII, FFI, embedded, no_std | -| [**Rust для программистов C#**](https://microsoft.github.io/RustTraining/csharp-book/) | 🟢 Мост | Swift / C# / Java → владение и система типов | -| [**Rust для программистов Python**](https://microsoft.github.io/RustTraining/python-book/) | 🟢 Мост | От динамической типизации к статической, конкурентность без GIL | -| [**Async Rust**](https://microsoft.github.io/RustTraining/async-book/) | 🔵 Погружение | Tokio, потоки (streams), отмена и безопасность при отмене | -| [**Паттерны Rust**](https://microsoft.github.io/RustTraining/rust-patterns-book/) | 🟡 Продвинутый | Pin, аллокаторы, lock-free-структуры, unsafe | -| [**Корректность на уровне типов**](https://microsoft.github.io/RustTraining/type-driven-correctness-book/) | 🟣 Эксперт | Typestate, phantom-типы, capability-токены | -| [**Инженерные практики Rust**](https://microsoft.github.io/RustTraining/engineering-book/) | 🟤 Практики | Build scripts, кросс-компиляция, CI/CD, Miri | +| [**Rust для программистов на C/C++**](https://r007.github.io/RustTraining-Rus/c-cpp-book/) | 🟢 Мост | Семантика перемещения, RAII, FFI, embedded, no_std | +| [**Rust для программистов C#**](https://r007.github.io/RustTraining-Rus/csharp-book/) | 🟢 Мост | Swift / C# / Java → владение и система типов | +| [**Rust для программистов на Python**](https://r007.github.io/RustTraining-Rus/python-book/) | 🟢 Мост | От динамической типизации к статической, конкурентность без GIL | +| [**Async Rust**](https://r007.github.io/RustTraining-Rus/async-book/) | 🔵 Погружение | Tokio, потоки (streams), отмена и безопасность при отмене | +| [**Паттерны Rust**](https://r007.github.io/RustTraining-Rus/rust-patterns-book/) | 🟡 Продвинутый | Pin, аллокаторы, lock-free-структуры, unsafe | +| [**Корректность на уровне типов**](https://r007.github.io/RustTraining-Rus/type-driven-correctness-book/) | 🟣 Эксперт | Typestate, phantom-типы, capability-токены | +| [**Инженерные практики Rust**](https://r007.github.io/RustTraining-Rus/engineering-book/) | 🟤 Практики | Build-скрипты, кросс-компиляция, CI/CD, Miri | -В каждой книге 15–16 глав с диаграммами Mermaid, редактируемыми песочницами Rust, упражнениями и полнотекстовым поиском. +Книги содержат диаграммы Mermaid, редактируемые песочницы Rust, упражнения и полнотекстовый поиск. -> **Совет:** Просматривайте готовые книги с навигацией по боковой панели и поиском на [сайте GitHub Pages](https://microsoft.github.io/RustTraining/). +> **Совет:** читайте книги на [сайте](https://r007.github.io/RustTraining-Rus/): там есть навигация по боковой панели и поиск. > -> **Локальный просмотр:** Для работы без интернета или при участии в разработке (сначала [установите Rust](https://rustup.rs/)): +> **Локальный просмотр:** для работы без интернета или при участии в разработке (сначала [установите Rust](https://rustup.rs/)): > ``` -> git clone https://github.com/microsoft/RustTraining.git -> cd RustTraining +> git clone https://github.com/r007/RustTraining-Rus.git +> cd RustTraining-Rus > cargo install mdbook mdbook-mermaid > cargo xtask serve # собирает все книги и запускает сервер: http://localhost:3000 > ``` --- +## 💡 Источники вдохновения и благодарности + +- [**The Rust Programming Language**](https://doc.rust-lang.org/book/): основа, на которой строится всё остальное +- [**Jon Gjengset**](https://www.youtube.com/c/JonGjengset): глубокие стримы о внутреннем устройстве Rust, серия `Crust of Rust` +- [**withoutboats**](https://without.boats/blog/): дизайн async, `Pin` и модель futures +- [**fasterthanlime (Amos)**](https://fasterthanli.me/): системное программирование с нуля и подробные разборы +- [**Mara Bos**](https://marabos.nl/): *Rust Atomics and Locks*, примитивы конкурентности +- [**Aleksey Kladov (matklad)**](https://matklad.github.io/): идеи о rust-analyzer, дизайне API и обработке ошибок +- [**Niko Matsakis**](https://smallcultfollowing.com/babysteps/): дизайн языка, внутреннее устройство borrow checker, Polonius +- [**Rust by Example**](https://doc.rust-lang.org/rust-by-example/) и [**Rustonomicon**](https://doc.rust-lang.org/nomicon/): практические паттерны и углублённое изучение unsafe +- [**This Week in Rust**](https://this-week-in-rust.org/): находки сообщества, которые повлияли на многие примеры +- [**Binary Musings — Tag(Rust)**](https://binarymusings.org/posts/category/rust/): глубокое погружение во внутреннее устройство Rust +- …и многие другие участники **сообщества Rust**, чьи статьи, доклады, RFC и обсуждения на форумах легли в основу этих материалов. Их слишком много, чтобы перечислить поимённо, но мы искренне благодарны каждому + +--- + +## 🤝 Участие в проекте + +Исправления опечаток, неточностей, перевода и примеров приветствуются. Порядок работы и стиль перевода описаны в [CONTRIBUTING.md](CONTRIBUTING.md). Сообщить об уязвимости можно по правилам из [SECURITY.md](SECURITY.md). + +--- + ## 🔧 Для мейнтейнеров
@@ -86,17 +83,17 @@ cargo install mdbook@0.4.52 mdbook-mermaid@0.14.0 ### Клонирование репозитория ```bash -git clone https://github.com/microsoft/RustTraining.git -cd RustTraining +git clone https://github.com/r007/RustTraining-Rus.git +cd RustTraining-Rus ``` ### Сборка и запуск ```bash -cargo xtask build # Собрать все книги в site/ (для локального просмотра) -cargo xtask serve # Собрать и запустить на http://localhost:3000 -cargo xtask deploy # Собрать все книги в docs/ (для GitHub Pages) -cargo xtask clean # Удалить site/ и docs/ +cargo xtask build # собрать все книги в site/ (для локального просмотра) +cargo xtask serve # собрать и запустить на http://localhost:3000 +cargo xtask deploy # собрать все книги в docs/ (для GitHub Pages) +cargo xtask clean # удалить site/ и docs/ ``` Чтобы собрать или запустить одну книгу: @@ -107,6 +104,21 @@ cd c-cpp-book && mdbook serve --open # http://localhost:3000 ### Публикация -Сайт автоматически публикуется на GitHub Pages при push в `main` через `.github/workflows/pages.yml`. Никаких ручных действий не требуется. +Сайт публикуется на GitHub Pages при push в `main` через `.github/workflows/pages.yml`. Вручную ничего делать не нужно, но в настройках репозитория должен быть выбран источник **GitHub Actions** (Settings → Pages → Build and deployment). + +### Docker + +Контейнер для самостоятельного хостинга описан в [docker/README.md](docker/README.md).
+ +--- + +## 📜 Лицензия + +- Код (включая `xtask`, примеры и конфигурацию сборки) распространяется по лицензии [MIT](LICENSE). +- Текст книг и документация распространяются по лицензии [Creative Commons Attribution 4.0 International (CC BY 4.0)](LICENSE-DOCS). + +Материалы основаны на оригинальном курсе Rust Training, распространяемом по тем же лицензиям. Авторские уведомления оригинала сохранены в файлах [LICENSE](LICENSE) и [LICENSE-DOCS](LICENSE-DOCS). Русский перевод и адаптацию выполнили участники проекта RustTraining-Rus. + +Названия языков, инструментов и библиотек (Rust, Cargo, Tokio, .NET, mdBook и другие) принадлежат их правообладателям. Для юридических формулировок приоритет имеет английский текст файлов лицензий. diff --git a/SECURITY.md b/SECURITY.md index 656f791..09e5fd2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,14 +1,19 @@ - +# Политика безопасности -## Security +Проект содержит учебные материалы и примеры кода, а не производственную библиотеку. Тем не менее, если вы нашли проблему безопасности в скриптах сборки, `xtask`, Docker-конфигурации или в примерах, которые могут навредить читателю, сообщите о ней. -Microsoft takes the security of our software products and services seriously, which -includes all source code repositories in our GitHub organizations. +## Как сообщить об уязвимости -**Please do not report security vulnerabilities through public GitHub issues.** +**Не создавайте публичный Issue**, если речь идёт о проблеме безопасности. -For security reporting information, locations, contact information, and policies, -please review the latest guidance for Microsoft repositories at -[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md). +Используйте приватное уведомление: вкладка **Security** репозитория → **Report a vulnerability**. Это работает через GitHub Private Vulnerability Reporting. - +Если эта функция недоступна, напишите мейнтейнеру через профиль GitHub: [@r007](https://github.com/r007). + +## Что важно указать + +- Какой файл или компонент затронут: книга, `xtask`, Dockerfile, nginx-конфигурация или CI. +- Шаги для воспроизведения и ожидаемое поведение. +- Версии инструментов, если они важны. + +Ошибки в тексте, неточности и опечатки не являются уязвимостями. Их лучше сообщать обычным Issue. diff --git a/async-book/book.toml b/async-book/book.toml index 60c288a..0fa5dee 100644 --- a/async-book/book.toml +++ b/async-book/book.toml @@ -1,14 +1,15 @@ [book] -title = "Async Rust: From Futures to Production" -authors = ["Rust Training Team"] -language = "en" +title = "Async Rust: от Future до продакшена" +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] +language = "ru" src = "src" [build] build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/async-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/async-book/src/ch00-introduction.md b/async-book/src/ch00-introduction.md index 10098a2..c94fd96 100644 --- a/async-book/src/ch00-introduction.md +++ b/async-book/src/ch00-introduction.md @@ -1,10 +1,10 @@ # Async Rust: от Future до продакшена -## Об авторе +## О проекте -- Главный архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft -- Опытный специалист в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также систем на C++ -- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст). +- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus. +- Замечания и исправления присылайте через Issues и Pull Requests репозитория. --- @@ -56,7 +56,7 @@ ## Работа с упражнениями -Каждая содержательная глава содержит встроенное упражнение. Итоговый проект (гл. 16) объединяет всё в одном приложении. Для максимальной пользы: +Каждая содержательная глава содержит встроенное упражнение. Итоговый проект (гл. 17) объединяет всё в одном приложении. Для максимальной пользы: 1. **Сначала решите упражнение, и только потом раскрывайте решение** — именно в процессе борьбы с задачей и происходит обучение 2. **Набирайте код, а не копируйте его** — для синтаксиса Rust важна мышечная память diff --git a/async-book/src/ch10-async-traits.md b/async-book/src/ch10-async-traits.md index 5a8d498..149a977 100644 --- a/async-book/src/ch10-async-traits.md +++ b/async-book/src/ch10-async-traits.md @@ -41,7 +41,7 @@ trait DataStore { Основная сложность: когда метод трейта возвращает `impl Future`, каждый реализующий тип возвращает *свой конкретный тип*. Компилятору нужно знать размер возвращаемого типа, но методы трейтов диспетчеризуются динамически. -### RPITIT: Return Position Impl Trait in Trait +### RPITIT: возвращаемый impl Trait в трейте С Rust 1.75 это просто работает для статической диспетчеризации: diff --git a/async-book/src/ch13-production-patterns.md b/async-book/src/ch13-production-patterns.md index 4ad8c51..598aa5a 100644 --- a/async-book/src/ch13-production-patterns.md +++ b/async-book/src/ch13-production-patterns.md @@ -8,7 +8,7 @@ > - Обработка ошибок: `thiserror` против `anyhow`, паттерн двойного `?` > - Tower: паттерн middleware, который используют axum, tonic и hyper -## Graceful Shutdown +## Корректное завершение работы (Graceful Shutdown) Продакшен-серверы должны корректно завершаться: доводить до конца текущие запросы, сбрасывать буферы, закрывать соединения: diff --git a/c-cpp-book/book.toml b/c-cpp-book/book.toml index 956ec57..b3d2a19 100644 --- a/c-cpp-book/book.toml +++ b/c-cpp-book/book.toml @@ -1,14 +1,15 @@ [book] -title = "Rust for C/C++ Programmers" -authors = ["Rust Training Team"] -language = "en" +title = "Курс Rust для программистов на C/C++" +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] +language = "ru" src = "src" [build] build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/c-cpp-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/c-cpp-book/src/ch00-introduction.md b/c-cpp-book/src/ch00-introduction.md index fc5a629..159af1a 100644 --- a/c-cpp-book/src/ch00-introduction.md +++ b/c-cpp-book/src/ch00-introduction.md @@ -53,7 +53,7 @@ ## Часть I — Основы ### 1. Представление и общий подход -- [Представление преподавателя и общий подход](ch01-introduction-and-motivation.md#представление-преподавателя-и-общий-подход) +- [Знакомство с курсом и общий подход](ch01-introduction-and-motivation.md#знакомство-с-курсом-и-общий-подход) - [Аргументы в пользу Rust](ch01-introduction-and-motivation.md#аргументы-в-пользу-rust) - [Как Rust решает эти проблемы?](ch01-introduction-and-motivation.md#как-rust-решает-эти-проблемы) - [Другие ключевые преимущества и возможности Rust](ch01-introduction-and-motivation.md#другие-ключевые-преимущества-и-возможности-rust) diff --git a/c-cpp-book/src/ch01-introduction-and-motivation.md b/c-cpp-book/src/ch01-introduction-and-motivation.md index 884d0a8..411fdc7 100644 --- a/c-cpp-book/src/ch01-introduction-and-motivation.md +++ b/c-cpp-book/src/ch01-introduction-and-motivation.md @@ -1,11 +1,10 @@ -# Представление преподавателя и общий подход +# Знакомство с курсом и общий подход > **Что вы узнаете:** структуру курса, интерактивный формат и то, как знакомые концепции C/C++ соответствуют своим аналогам в Rust. Эта глава задаёт ожидания и даёт дорожную карту для остальной книги. -- О преподавателе - - Principal Firmware Architect (ведущий архитектор прошивок) в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) в Microsoft - - Ветеран отрасли с опытом в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре процессоров и платформ, а также в системах на C++ - - Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- О курсе + - Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0) + - Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus - Курс задуман как можно более интерактивный - Предположение: вы знаете C, C++ или оба языка - Примеры намеренно построены так, чтобы знакомые концепции сопоставлялись с аналогами в Rust diff --git a/c-cpp-book/src/ch15-1-embedded-deep-dive.md b/c-cpp-book/src/ch15-1-embedded-deep-dive.md index 565e0a6..032d0c6 100644 --- a/c-cpp-book/src/ch15-1-embedded-deep-dive.md +++ b/c-cpp-book/src/ch15-1-embedded-deep-dive.md @@ -749,7 +749,7 @@ graph LR | `target remote :3333` | GDB подключается к `localhost:1337` | | `monitor reset halt` | `probe-rs reset --chip ...` | | `load firmware.elf` | `cargo flash --chip ...` | -| `printf("debug: %d\n", val)` (semihosting) | `defmt::info!("debug: {}", val)` (RTT) | +| `printf("debug: %d\n", val)` (семихостинг) | `defmt::info!("debug: {}", val)` (RTT) | | Графический отладчик Keil/IAR | VS Code + расширение `probe-rs-debugger` | | Segger SystemView | `defmt` + просмотрщик RTT из `probe-rs` | diff --git a/csharp-book/book.toml b/csharp-book/book.toml index 2c581a4..1657844 100644 --- a/csharp-book/book.toml +++ b/csharp-book/book.toml @@ -1,14 +1,15 @@ [book] -title = "Rust for C# Programmers" -authors = ["Rust Training Team"] -language = "en" +title = "Rust для программистов C#: полное учебное руководство" +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] +language = "ru" src = "src" [build] build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/csharp-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/csharp-book/src/ch01-introduction-and-motivation.md b/csharp-book/src/ch01-introduction-and-motivation.md index b7e0456..774072c 100644 --- a/csharp-book/src/ch01-introduction-and-motivation.md +++ b/csharp-book/src/ch01-introduction-and-motivation.md @@ -1,9 +1,8 @@ -## Представление автора и общий подход +## О курсе и общий подход -- Представление автора - - Principal Firmware Architect в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) в Microsoft - - Ветеран отрасли с опытом в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также C++-систем - - Начал программировать на Rust в 2017 году (в AWS EC2) и с тех пор без памяти влюблён в этот язык +- О курсе + - Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0) + - Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus - Этот курс задуман как можно более интерактивный - Предположение: вы знаете C# и разработку на .NET - Примеры намеренно сопоставляют концепции C# с их аналогами в Rust diff --git a/csharp-book/src/ch02-getting-started.md b/csharp-book/src/ch02-getting-started.md index 799943a..b42c140 100644 --- a/csharp-book/src/ch02-getting-started.md +++ b/csharp-book/src/ch02-getting-started.md @@ -143,7 +143,7 @@ cargo doc --open # Сгенерировать и открыть докуме ### Workspace против Solution -**C# Solution (.sln)** +**Решение C# (.sln)** ```text MySolution/ ├── MySolution.sln @@ -155,7 +155,7 @@ MySolution/ └── Tests.csproj ``` -**Rust Workspace (Cargo.toml)** +**Рабочее пространство Rust (Cargo.toml)** ```toml [workspace] members = [ diff --git a/csharp-book/src/ch13-1-asyncawait-deep-dive.md b/csharp-book/src/ch13-1-asyncawait-deep-dive.md index ebb678b..7de71bf 100644 --- a/csharp-book/src/ch13-1-asyncawait-deep-dive.md +++ b/csharp-book/src/ch13-1-asyncawait-deep-dive.md @@ -245,7 +245,7 @@ fn make_future() -> Pin + Send>> { - `tokio::select!` (используйте макрос `pin!()`) - Методы трейтов, возвращающие `dyn Future` (используйте `Box::pin(async { ... })`) -> **Хотите глубже?** Сопутствующее руководство [Async Rust Training](../../async-book/src/ch04-pin-and-unpin.md) подробно рассматривает Pin, Unpin, самоссылающиеся структуры и структурное закрепление. +> **Хотите глубже?** Сопутствующее руководство [Async Rust Training](https://r007.github.io/RustTraining-Rus/async-book/ch04-pin-and-unpin.html) подробно рассматривает Pin, Unpin, самоссылающиеся структуры и структурное закрепление. *** diff --git a/csharp-book/src/ch13-concurrency.md b/csharp-book/src/ch13-concurrency.md index 44a825d..2e7c2d7 100644 --- a/csharp-book/src/ch13-concurrency.md +++ b/csharp-book/src/ch13-concurrency.md @@ -6,7 +6,7 @@ > > **Сложность:** 🔴 Продвинутый -> **Глубокое погружение**: о производственных асинхронных паттернах (обработка потоков, корректное завершение, пулы соединений, безопасность отмены) см. сопутствующее руководство [Async Rust Training](../../async-book/src/summary.md). +> **Глубокое погружение**: о производственных асинхронных паттернах (обработка потоков, корректное завершение, пулы соединений, безопасность отмены) см. сопутствующее руководство [Async Rust Training](https://r007.github.io/RustTraining-Rus/async-book/). > > **Предварительные требования**: [Владение и заимствование](ch07-ownership-and-borrowing.md) и [Умные указатели](ch07-3-smart-pointers-beyond-single-ownership.md) (дерево решений Rc против Arc). diff --git a/csharp-book/src/ch14-unsafe-rust-and-ffi.md b/csharp-book/src/ch14-unsafe-rust-and-ffi.md index c715de0..a4d74f1 100644 --- a/csharp-book/src/ch14-unsafe-rust-and-ffi.md +++ b/csharp-book/src/ch14-unsafe-rust-and-ffi.md @@ -1,4 +1,4 @@ -## Unsafe Rust +## Небезопасный Rust (unsafe Rust) > **Что вы узнаете:** что разрешает `unsafe` (сырые указатели, FFI, непроверяемые приведения), паттерны безопасных обёрток, > P/Invoke в C# против FFI в Rust для вызова нативного кода, а также чек-лист безопасности для блоков `unsafe`. @@ -7,7 +7,7 @@ Unsafe Rust позволяет выполнять операции, которые проверщик заимствований не может верифицировать. Используйте его осторожно и с понятной документацией. -> **Расширенное описание**: о безопасных абстракциях поверх unsafe-кода (арена-аллокаторы, структуры без блокировок, пользовательские таблицы виртуальных методов) см. [Паттерны Rust](../../rust-patterns-book/src/summary.md). +> **Расширенное описание**: о безопасных абстракциях поверх unsafe-кода (арена-аллокаторы, структуры без блокировок, пользовательские таблицы виртуальных методов) см. [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/). ### Когда нужен unsafe diff --git a/csharp-book/src/ch16-3-rust-tooling-ecosystem.md b/csharp-book/src/ch16-3-rust-tooling-ecosystem.md index edb9709..cd1b723 100644 --- a/csharp-book/src/ch16-3-rust-tooling-ecosystem.md +++ b/csharp-book/src/ch16-3-rust-tooling-ecosystem.md @@ -119,7 +119,7 @@ cargo expand module_name # Раскрыть конкретный модул Для более глубокого изучения продвинутых тем, упомянутых в этом руководстве, см. сопутствующие учебные материалы: -- **[Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md)** — проекции Pin, пользовательские аллокаторы, арена-паттерны, структуры данных без блокировок и продвинутые паттерны unsafe -- **[Async Rust Training](../../async-book/src/SUMMARY.md)** — подробно о tokio, безопасности отмены в асинхронном коде, обработке потоков и производственной архитектуре async -- **[Rust для программистов C/C++](../../c-cpp-book/src/SUMMARY.md)** — полезно, если в команде есть опыт работы с C++; рассматривает соответствие семантики перемещения, различия RAII и шаблоны против обобщений -- **[Rust для программистов C](../../c-cpp-book/src/SUMMARY.md)** — актуально для сценариев взаимодействия; рассматривает паттерны FFI, отладку встраиваемого Rust и программирование с `no_std` +- **[Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/)** — проекции Pin, пользовательские аллокаторы, арена-паттерны, структуры данных без блокировок и продвинутые паттерны unsafe +- **[Async Rust Training](https://r007.github.io/RustTraining-Rus/async-book/)** — подробно о tokio, безопасности отмены в асинхронном коде, обработке потоков и производственной архитектуре async +- **[Rust для программистов C/C++](https://r007.github.io/RustTraining-Rus/c-cpp-book/)** — полезно, если в команде есть опыт работы с C++; рассматривает соответствие семантики перемещения, различия RAII и шаблоны против обобщений +- **[Rust для программистов C](https://r007.github.io/RustTraining-Rus/c-cpp-book/)** — актуально для сценариев взаимодействия; рассматривает паттерны FFI, отладку встраиваемого Rust и программирование с `no_std` diff --git a/docker/Dockerfile b/docker/Dockerfile index 9e5f9f1..1e333d2 100755 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,13 +1,13 @@ # syntax=docker/dockerfile:1 # -# Multi-stage build for the RustTraining book collection. +# Многоэтапная сборка для сборника книг RustTraining. # -# Stage 1 (builder) — Rust toolchain + mdbook + mdbook-mermaid, runs -# `cargo xtask build` to produce site/. -# Stage 2 (runtime) — unprivileged nginx serving the static output. -# No Rust, no mdbook, no source in the final image. +# Этап 1 (builder) — тулчейн Rust, mdbook и mdbook-mermaid; запускает +# `cargo xtask build`, который создаёт site/. +# Этап 2 (runtime) — непривилегированный nginx, раздающий готовые файлы. +# В итоговом образе нет Rust, mdbook и исходников. # -# Build from the REPOSITORY ROOT, not from docker/: +# Собирайте из КОРНЯ РЕПОЗИТОРИЯ, а не из docker/: # docker build -f docker/Dockerfile -t rust-training . ARG RUST_VERSION=1 @@ -16,7 +16,7 @@ ARG MDBOOK_MERMAID_VERSION=0.14.0 ARG NGINX_VERSION=1.27 # ────────────────────────────────────────────────────────────────────── -# Stage 1: build the books +# Этап 1: сборка книг # ────────────────────────────────────────────────────────────────────── FROM rust:${RUST_VERSION}-slim-bookworm AS builder @@ -30,27 +30,28 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl \ && rm -rf /var/lib/apt/lists/* -# Prefer prebuilt release binaries — `cargo install mdbook mdbook-mermaid` -# compiles both from source and adds several minutes to every cold build. +# По возможности берём готовые бинарники: `cargo install mdbook mdbook-mermaid` +# компилирует оба инструмента из исходников и добавляет несколько минут +# к каждой холодной сборке. # -# Upstream does not ship a complete set of prebuilt targets, so we fall back -# to compiling when an asset is missing: -# - mdbook has linux-gnu on amd64, linux-musl on arm64 -# - mdbook-mermaid has NO published arm64 Linux binary at all -# musl builds are statically linked and run fine on this glibc base image. +# Upstream публикует не все архитектуры, поэтому при отсутствии бинарника +# выполняется компиляция: +# - mdbook есть для linux-gnu на amd64 и linux-musl на arm64 +# - mdbook-mermaid НЕ публикует бинарников для arm64 Linux вообще +# Статические сборки musl работают и на glibc-образе. RUN set -eux; \ case "${TARGETARCH:-amd64}" in \ amd64) mdbook_triple=x86_64-unknown-linux-gnu; mermaid_triple=x86_64-unknown-linux-gnu ;; \ arm64) mdbook_triple=aarch64-unknown-linux-musl; mermaid_triple=aarch64-unknown-linux-musl ;; \ - *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + *) echo "неподдерживаемый TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ esac; \ \ install_tool() { \ bin="$1"; url="$2"; crate="$3"; ver="$4"; \ if curl -fsSL "$url" 2>/dev/null | tar -xz -C /usr/local/bin "$bin" 2>/dev/null; then \ - echo "==> installed $bin from prebuilt binary"; \ + echo "==> $bin установлен из готового бинарника"; \ else \ - echo "==> no prebuilt $bin for ${TARGETARCH}, compiling from source"; \ + echo "==> готового $bin для ${TARGETARCH} нет, компилируем из исходников"; \ cargo install "$crate" --version "$ver" --locked --root /usr/local; \ fi; \ }; \ @@ -65,23 +66,25 @@ RUN set -eux; \ mdbook --version; \ mdbook-mermaid --version -# Copy the whole workspace. xtask resolves the project root from -# CARGO_MANIFEST_DIR at compile time, so it must be built and run in place. +# Копируем весь воркспейс. xtask определяет корень проекта через +# CARGO_MANIFEST_DIR во время компиляции, поэтому собирать и запускать его +# нужно на месте. COPY . . -# `cargo xtask build` writes to site/ (the deploy target, docs/, is for -# GitHub Pages and additionally prints commit instructions we don't want here). +# `cargo xtask build` пишет в site/. Цель deploy (docs/) предназначена для +# GitHub Pages и дополнительно печатает инструкции по коммиту, здесь они не нужны. RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=/build/target \ cargo run --release --package xtask -- build \ && test -f site/index.html \ - && echo "==> built $(find site -mindepth 1 -maxdepth 1 -type d | wc -l) books" + && echo "==> собрано книг: $(find site -mindepth 1 -maxdepth 1 -type d | wc -l)" # ────────────────────────────────────────────────────────────────────── -# Stage 2: serve +# Этап 2: раздача # ────────────────────────────────────────────────────────────────────── -# nginx-unprivileged is the stock nginx image reconfigured to run as uid 101 -# and listen on 8080, so the container needs no root and no NET_BIND_SERVICE. +# nginx-unprivileged — стандартный образ nginx, переконфигурированный на +# работу от uid 101 и порт 8080. Поэтому контейнеру не нужны root-права +# и NET_BIND_SERVICE. FROM nginxinc/nginx-unprivileged:${NGINX_VERSION}-alpine AS runtime COPY docker/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/docker/README.md b/docker/README.md index f1f4c52..da96747 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,67 +1,49 @@ -# Containerized deployment +# Контейнерное развёртывание -Optional, opt-in way to self-host the book collection without GitHub Pages — -useful behind a firewall or on an internal network. +Необязательный способ развернуть сборник книг самостоятельно, без GitHub Pages. Подходит, например, для сети за файрволом или внутренней сети. -**This is not the local development path.** For writing and previewing, use -`cargo xtask serve`, which rebuilds and serves at with -no container involved. +**Это не путь для локальной разработки.** Для написания и просмотра книг используйте `cargo xtask serve`: он пересобирает книги и запускает сервер на без контейнера. -## Usage +## Использование -From the repository root: +Из корня репозитория: ```bash docker compose -f docker/compose.yaml up --build ``` -Then open . Override the host port with `PORT`: +Затем откройте . Порт хоста можно переопределить через `PORT`: ```bash PORT=8080 docker compose -f docker/compose.yaml up --build ``` -Without Compose: +Без Compose: ```bash docker build -f docker/Dockerfile -t rust-training . docker run --rm -p 3000:8080 rust-training ``` -Note the build context is the repository root in both cases — the build needs -the book sources and the `xtask` crate. +В обоих случаях контекст сборки — корень репозитория: сборке нужны исходники книг и крейт `xtask`. -## How it works +## Как это работает -Two stages: +Сборка идёт в две стадии: -1. **builder** (`rust:1-slim-bookworm`) installs `mdbook` and `mdbook-mermaid`, - then runs `cargo xtask build`, which builds all seven books into `site/` - along with the generated landing page. -2. **runtime** (`nginxinc/nginx-unprivileged:alpine`) serves `site/` on port - 8080. No Rust toolchain, no mdbook, no book sources in the final image. +1. **builder** (`rust:1-slim-bookworm`) устанавливает `mdbook` и `mdbook-mermaid`, затем запускает `cargo xtask build`. Эта команда собирает все семь книг в `site/` вместе со страницей-обложкой. +2. **runtime** (`nginxinc/nginx-unprivileged:alpine`) раздаёт `site/` на порту 8080. В итоговом образе нет тулчейна Rust, mdbook и исходников книг. -`xtask build` is used rather than `xtask deploy` because the two produce -identical content — `deploy` only differs in writing to `docs/` and printing -GitHub Pages instructions, which are irrelevant in a container. +Используется `xtask build`, а не `xtask deploy`, потому что результат одинаковый. `deploy` отличается только записью в `docs/` и выводом инструкций для GitHub Pages, которые в контейнере не нужны. -## Pinned versions +## Фиксированные версии -`MDBOOK_VERSION` and `MDBOOK_MERMAID_VERSION` are build args in the Dockerfile. -CI (`pages.yml`) currently installs both unpinned via `cargo install`, so the -container may lag or lead the published site after an upstream mdbook release. -Bump the args when that matters. +`MDBOOK_VERSION` и `MDBOOK_MERMAID_VERSION` задаются аргументами сборки в Dockerfile. CI (`pages.yml`) пока устанавливает оба инструмента через `cargo install` без фиксации версий. Поэтому контейнер может отставать от опубликованного сайта или опережать его после выхода новой версии mdbook. Меняйте аргументы, когда это важно. -Prebuilt release binaries are used where upstream publishes them, falling back -to `cargo install` otherwise. As of the pinned versions, `mdbook-mermaid` has no -published arm64 Linux binary, so arm64 builds compile it from source and take -noticeably longer. +Там, где upstream публикует готовые бинарники, они и используются. Иначе выполняется `cargo install`. На момент фиксации версий `mdbook-mermaid` не публикует готовый бинарник для arm64 Linux, поэтому сборка под arm64 компилирует его из исходников и занимает заметно больше времени. -## Notes +## Примечания -- The container runs as uid 101 and binds an unprivileged port, so it needs no - root and no added capabilities. -- Adding `read_only: true` to the service is possible but requires tmpfs mounts - for nginx's cache and pid paths; it is left off by default rather than shipped - untested. -- Content is baked in at build time. Rebuild the image to pick up book changes. +- Контейнер работает от пользователя с uid 101 и слушает непривилегированный порт. Поэтому ему не нужны root-права и дополнительные capabilities. +- Можно добавить `read_only: true` для сервиса, но тогда понадобятся tmpfs-монтирования для кэша и pid-файлов nginx. По умолчанию этого нет: вариант не проверялся. +- Содержимое встраивается при сборке образа. Чтобы подхватить изменения в книгах, пересоберите образ. diff --git a/docker/compose.yaml b/docker/compose.yaml index dc5c804..e9932c7 100755 --- a/docker/compose.yaml +++ b/docker/compose.yaml @@ -3,15 +3,15 @@ name: rust-training services: books: build: - # Context is the repo root: the build needs the book sources and the - # xtask crate, which live above this file. + # Контекст — корень репозитория: сборке нужны исходники книг и крейт + # xtask, которые лежат выше этого файла. context: .. dockerfile: docker/Dockerfile image: rust-training:local container_name: rust-training-books - # Host port is configurable; the container always listens on 8080 because - # nginx-unprivileged runs as a non-root user. + # Порт хоста настраивается; контейнер всегда слушает 8080, потому что + # nginx-unprivileged работает от непривилегированного пользователя. ports: - "${PORT:-3000}:8080" diff --git a/docker/nginx.conf b/docker/nginx.conf index 7a8b9d7..2969ce3 100755 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -5,13 +5,13 @@ server { root /usr/share/nginx/html; index index.html; - # mdbook emits real .html files, but internal and hand-written links - # sometimes omit the extension. Resolving both keeps parity with the - # GitHub Pages behaviour. + # mdbook выдаёт настоящие .html-файлы, но внутренние и написанные вручную + # ссылки иногда не содержат расширения. Поддержка обоих вариантов сохраняет + # поведение GitHub Pages. # - # NOTE: add_header is NOT inherited into a block that declares its own - # add_header, so every location repeats the headers it needs rather than - # relying on a server-level declaration. + # ПРИМЕЧАНИЕ: add_header НЕ наследуется блоком, в котором объявлен свой + # add_header, поэтому каждый location повторяет нужные заголовки, а не + # полагается на объявление на уровне server. location / { try_files $uri $uri/ $uri.html =404; @@ -21,10 +21,10 @@ server { add_header Referrer-Policy "no-referrer" always; } - # mdbook's assets (book.js, ace.js, css) are NOT content-hashed — the same - # URL serves new bytes after a rebuild — so they must not be marked - # immutable. A short expiry keeps them cheap without pinning stale JS in - # browsers that never revalidate. + # Ресурсы mdbook (book.js, ace.js, css) не имеют хэшей в именах: один и тот + # же URL отдаёт новые байты после пересборки, поэтому их нельзя помечать + # как immutable. Короткий срок жизни кэша дёшев и не закрепляет устаревший + # JS в браузерах, которые не проверяют его повторно. location ~* \.(css|js|woff2?|ttf|svg|png|jpe?g|gif|ico)$ { expires 1d; diff --git a/engineering-book/book.toml b/engineering-book/book.toml index 695e098..36201e9 100644 --- a/engineering-book/book.toml +++ b/engineering-book/book.toml @@ -1,14 +1,15 @@ [book] -title = "Rust Engineering Practices — Beyond cargo build" -authors = ["Rust Training Team"] -language = "en" +title = "Инженерные практики Rust — за пределами cargo build" +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] +language = "ru" src = "src" [build] build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/engineering-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/engineering-book/src/ch00-introduction.md b/engineering-book/src/ch00-introduction.md index ff4c1a1..8a8fa17 100644 --- a/engineering-book/src/ch00-introduction.md +++ b/engineering-book/src/ch00-introduction.md @@ -1,10 +1,10 @@ # Инженерные практики Rust — за пределами `cargo build` -## Об авторе +## О проекте -- Ведущий архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft -- Опытный инженер с экспертизой в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре CPU и платформ, а также системах на C++ -- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст). +- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus. +- Замечания и исправления присылайте через Issues и Pull Requests репозитория. --- diff --git a/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md b/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md index 22b755c..75529b4 100644 --- a/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md +++ b/engineering-book/src/ch06-dependency-management-and-supply-chain-s.md @@ -218,10 +218,10 @@ cargo semver-checks # Вывод: # ✗ Function `parse_gpu_csv` is now private (was public) -# → This is a BREAKING change. Bump MAJOR version. +# → Несовместимое изменение (BREAKING). Повысьте MAJOR-версию. # # ✗ Struct `GpuInfo` has a new required field `power_limit_w` -# → This is a BREAKING change. Bump MAJOR version. +# → Несовместимое изменение (BREAKING). Повысьте MAJOR-версию. # # ✓ Function `parse_gpu_csv_v2` was added (non-breaking) ``` diff --git a/engineering-book/src/ch08-compile-time-and-developer-tools.md b/engineering-book/src/ch08-compile-time-and-developer-tools.md index a4d790c..4f3b9d5 100644 --- a/engineering-book/src/ch08-compile-time-and-developer-tools.md +++ b/engineering-book/src/ch08-compile-time-and-developer-tools.md @@ -157,8 +157,8 @@ cargo install cargo-geiger cargo geiger # Output: # Metric output format: x/y -# x = unsafe code used by the build -# y = total unsafe code found in the crate +# x = небезопасный код, используемый при сборке +# y = весь небезопасный код, найденный в крейте # # Functions Expressions Impls Traits Methods # 0/0 0/0 0/0 0/0 0/0 ✅ my_crate diff --git a/python-book/book.toml b/python-book/book.toml index 4607833..9bc1d4e 100644 --- a/python-book/book.toml +++ b/python-book/book.toml @@ -1,14 +1,15 @@ [book] -title = "Rust for Python Programmers" -authors = ["Rust Training Team"] -language = "en" +title = "Rust для программистов на Python: полное учебное руководство" +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] +language = "ru" src = "src" [build] build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/python-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/python-book/src/ch00-introduction.md b/python-book/src/ch00-introduction.md index 9ad57e0..acdef79 100644 --- a/python-book/src/ch00-introduction.md +++ b/python-book/src/ch00-introduction.md @@ -98,7 +98,7 @@ #### 11. Трейты From и Into 🟡 - [Преобразование типов в Rust](ch11-from-and-into-traits.md#преобразование-типов-в-rust) -- [From, Into, TryFrom](ch11-from-and-into-traits.md#rust-frominto) +- [From, Into, TryFrom](ch11-from-and-into-traits.md#frominto-в-rust) - [Паттерны преобразования строк](ch11-from-and-into-traits.md#паттерны-преобразования-строк) #### 12. Замыкания и итераторы 🟡 diff --git a/python-book/src/ch01-introduction-and-motivation.md b/python-book/src/ch01-introduction-and-motivation.md index 53f1b1f..1c54816 100644 --- a/python-book/src/ch01-introduction-and-motivation.md +++ b/python-book/src/ch01-introduction-and-motivation.md @@ -1,9 +1,8 @@ -## Об авторе и общий подход +## О курсе и общий подход -- Об авторе - - Главный архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft - - Опытный специалист в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры CPU и платформ, а также систем на C++ - - Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- О курсе + - Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0) + - Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus - Курс задуман максимально интерактивным - Предполагается, что вы знаете Python и его экосистему - Примеры намеренно сопоставляют понятия Python с их аналогами в Rust diff --git a/python-book/src/ch11-from-and-into-traits.md b/python-book/src/ch11-from-and-into-traits.md index 8b4909b..5e808e1 100644 --- a/python-book/src/ch11-from-and-into-traits.md +++ b/python-book/src/ch11-from-and-into-traits.md @@ -26,7 +26,7 @@ class Celsius: c = Celsius.from_fahrenheit(212.0) # 100.0°C ``` -### Rust From/Into +### From/Into в Rust ```rust // Rust — трейт From задаёт преобразования // Реализация From даёт вам Into автоматически! diff --git a/rust-patterns-book/book.toml b/rust-patterns-book/book.toml index c115147..c5371be 100644 --- a/rust-patterns-book/book.toml +++ b/rust-patterns-book/book.toml @@ -1,6 +1,6 @@ [book] title = "Паттерны Rust и практические руководства по инженерии" -authors = ["Rust Training Team"] +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] language = "ru" src = "src" @@ -8,7 +8,8 @@ src = "src" build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/rust-patterns-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/rust-patterns-book/src/ch00-introduction.md b/rust-patterns-book/src/ch00-introduction.md index 6bce4c6..d3f3861 100644 --- a/rust-patterns-book/src/ch00-introduction.md +++ b/rust-patterns-book/src/ch00-introduction.md @@ -1,10 +1,10 @@ # Паттерны Rust и практические руководства по инженерии -## Об авторе +## О проекте -- Ведущий архитектор прошивок в команде SCHIE (Silicon and Cloud Hardware Infrastructure Engineering) компании Microsoft -- Опытный инженер с экспертизой в безопасности, системном программировании (прошивки, операционные системы, гипервизоры), архитектуре CPU и платформ, а также системах на C++ -- Начал программировать на Rust в 2017 году (@AWS EC2) и с тех пор влюблён в этот язык +- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст). +- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus. +- Замечания и исправления присылайте через Issues и Pull Requests репозитория. --- diff --git a/rust-patterns-book/src/ch02-traits-in-depth.md b/rust-patterns-book/src/ch02-traits-in-depth.md index 101f26e..fabb783 100644 --- a/rust-patterns-book/src/ch02-traits-in-depth.md +++ b/rust-patterns-book/src/ch02-traits-in-depth.md @@ -494,7 +494,7 @@ fn main() { > **Когда стоимость vtable имеет значение**: в узких циклах, которые миллионы раз вызывают метод трейта, косвенность и невозможность инлайнинга могут заметно замедлить код (в 2–10 раз). Для холодных путей, конфигурации и архитектур с плагинами гибкость `dyn Trait` стоит небольшой цены. -### Higher-Ranked Trait Bounds (HRTB) +### Трейт-границы высшего ранга (HRTB) Иногда нужна функция, которая работает со ссылками с *любым* временем жизни, а не с каким-то конкретным. Здесь и появляется синтаксис `for<'a>`: diff --git a/type-driven-correctness-book/book.toml b/type-driven-correctness-book/book.toml index f7cdc18..cc43ac7 100644 --- a/type-driven-correctness-book/book.toml +++ b/type-driven-correctness-book/book.toml @@ -1,6 +1,6 @@ [book] title = "Корректность на уровне типов в Rust" -authors = ["Rust Training Team"] +authors = ["Rust Training Team (оригинал)", "Участники проекта RustTraining-Rus (перевод)"] language = "ru" src = "src" @@ -8,7 +8,8 @@ src = "src" build-dir = "book" [output.html] -git-repository-url = "https://github.com/microsoft/RustTraining" +git-repository-url = "https://github.com/r007/RustTraining-Rus" +edit-url-template = "https://github.com/r007/RustTraining-Rus/edit/main/type-driven-correctness-book/src/{path}" default-theme = "light" preferred-dark-theme = "ayu" additional-js = ["mermaid.min.js", "mermaid-init.js"] diff --git a/type-driven-correctness-book/src/ch00-introduction.md b/type-driven-correctness-book/src/ch00-introduction.md index 44d6c44..d00e527 100644 --- a/type-driven-correctness-book/src/ch00-introduction.md +++ b/type-driven-correctness-book/src/ch00-introduction.md @@ -1,14 +1,14 @@ # Корректность на уровне типов в Rust -## Об авторе +## О проекте -- Главный архитектор по прошивкам в команде Microsoft SCHIE (Silicon and Cloud Hardware Infrastructure Engineering). -- Инженер с многолетним опытом в области безопасности, системного программирования (прошивки, операционные системы, гипервизоры), архитектуры процессоров и платформ, а также больших систем на C++. -- Пишет на Rust с 2017 года, со времен AWS EC2, и с тех пор остается убежденным сторонником этого языка. +- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст). +- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus. +- Замечания и исправления присылайте через Issues и Pull Requests репозитория. --- -Это практическое руководство по использованию системы типов Rust, чтобы целые классы ошибок становились **невозможными для компиляции**. Сопутствующая книга [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md) объясняет механику (трейты, ассоциированные типы, typestate), а это руководство показывает, как **применять** эту механику к реальным задачам: диагностике аппаратного обеспечения, криптографии, валидации протоколов и встраиваемым системам. +Это практическое руководство по использованию системы типов Rust, чтобы целые классы ошибок становились **невозможными для компиляции**. Сопутствующая книга [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/) объясняет механику (трейты, ассоциированные типы, typestate), а это руководство показывает, как **применять** эту механику к реальным задачам: диагностике аппаратного обеспечения, криптографии, валидации протоколов и встраиваемым системам. Все паттерны здесь следуют одному принципу: **переносите инварианты из проверок во время выполнения в систему типов, чтобы компилятор обеспечивал их соблюдение.** @@ -61,11 +61,11 @@ | Концепция | Где изучить | |-----------|-------------| -| Владение и заимствование | [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md), гл. 01 | -| Трейты и ассоциированные типы | [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md), гл. 02 | -| Newtype и typestate | [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md), гл. 03 | -| PhantomData | [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md), гл. 04 | -| Обобщения и ограничения трейтов | [Паттерны Rust](../../rust-patterns-book/src/SUMMARY.md), гл. 01 | +| Владение и заимствование | [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/), гл. 01 | +| Трейты и ассоциированные типы | [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/), гл. 02 | +| Newtype и typestate | [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/), гл. 03 | +| PhantomData | [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/), гл. 04 | +| Обобщения и ограничения трейтов | [Паттерны Rust](https://r007.github.io/RustTraining-Rus/rust-patterns-book/), гл. 01 | ## Спектр корректности по построению diff --git a/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md b/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md index 56076c6..e811361 100644 --- a/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md +++ b/type-driven-correctness-book/src/ch07-validated-boundaries-parse-dont-validate.md @@ -774,9 +774,13 @@ fn should_alert(record: &ValidSelRecord) -> bool { | ProcessorEvent::ThermalTrip | ProcessorEvent::UncorrectableMachineCheck ), - // Новый вариант типа датчика, добавленный в будущей версии? - // ❌ Ошибка компиляции: non-exhaustive patterns - _ => false, + // Без запасной ветки `_`: новый вариант датчика заставит явно решить, + // вызывает ли он оповещение. Иначе компилятор выдаст ошибку non-exhaustive patterns + SensorSpecificEvent::Temperature(_) + | SensorSpecificEvent::Voltage(_) + | SensorSpecificEvent::Fan(_) + | SensorSpecificEvent::PhysicalSecurity(_) + | SensorSpecificEvent::Watchdog(_) => false, }, TypedEvent::Discrete { .. } => false, }, diff --git a/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md b/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md index 85fdf07..d6e0b1e 100644 --- a/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md +++ b/type-driven-correctness-book/src/ch11-fourteen-tricks-from-the-trenches.md @@ -423,8 +423,8 @@ let level: DiagLevel = "extendedd".parse()?; | Тип | Модуль | Заметные синонимы | |-----|--------|-------------------| -| `DiagLevel` | `config_loader` | `"1"` = Quick, `"4"` = Stress | -| `Component` | `event_handler` | `"MEM"` / `"DIMM"` = Memory, `"SSD"` / `"NVME"` = Disk | +| `DiagLevel` | `config_loader` | `"1"` = быстрый, `"4"` = стресс | +| `Component` | `event_handler` | `"MEM"` / `"DIMM"` = память, `"SSD"` / `"NVME"` = диск | | `SkuVariant` | `net_inventory` | `"Accel-X1"` = S2001, `"Accel-M1"` = S2002, `"Accel-Z1"` = S3001 | | `SkuVariant` | `inventory` | Те же синонимы (отдельный модуль, тот же паттерн) | | `FaultStatus` | `config_loader` | Состояния жизненного цикла неисправности | diff --git a/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md b/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md index 5209297..7d1402d 100644 --- a/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md +++ b/type-driven-correctness-book/src/ch15-const-fn-compile-time-correctness-proofs.md @@ -115,8 +115,8 @@ fn main() { # pub struct Region { pub base: u32, pub size: u32 } # impl Region { # pub const fn new(base: u32, size: u32) -> Self { -# assert!(size > 0, "region size must be non-zero"); -# assert!(base as u64 + size as u64 <= u32::MAX as u64, "overflow"); +# assert!(size > 0, "размер региона должен быть ненулевым"); +# assert!(base as u64 + size as u64 <= u32::MAX as u64, "переполнение"); # Self { base, size } # } # pub const fn end(&self) -> u32 { self.base + self.size } diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 902114c..6b340e7 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -5,48 +5,48 @@ use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::process::Command; -/// (slug, title, description, category) +/// Книги сборника: (каталог, название, описание, категория). const BOOKS: &[(&str, &str, &str, &str)] = &[ ( "c-cpp-book", - "Rust for C/C++ Programmers", - "Move semantics, RAII, FFI, embedded, no_std", + "Курс Rust для программистов на C/C++", + "Семантика перемещения, RAII, FFI, embedded, no_std", "bridge", ), ( "csharp-book", - "Rust for C# Programmers", - "Best for Swift / C# / Java developers", + "Rust для программистов C#", + "Для разработчиков Swift / C# / Java", "bridge", ), ( "python-book", - "Rust for Python Programmers", - "Dynamic → static typing, GIL-free concurrency", + "Rust для программистов на Python", + "От динамической к статической типизации, конкурентность без GIL", "bridge", ), ( "async-book", - "Async Rust: From Futures to Production", - "Tokio, streams, cancellation safety", + "Async Rust: от Future до продакшена", + "Tokio, потоки (streams), отмена и безопасность при отмене", "deep-dive", ), ( "rust-patterns-book", - "Rust Patterns", - "Pin, allocators, lock-free structures, unsafe", + "Паттерны Rust", + "Pin, аллокаторы, lock-free-структуры, unsafe", "advanced", ), ( "type-driven-correctness-book", - "Type-Driven Correctness", - "Type-state, phantom types, capability tokens", + "Корректность на уровне типов", + "Typestate, phantom-типы, capability-токены", "expert", ), ( "engineering-book", - "Rust Engineering Practices", - "Build scripts, cross-compilation, coverage, CI/CD", + "Инженерные практики Rust", + "Build-скрипты, кросс-компиляция, покрытие кода, CI/CD", "practices", ), ]; @@ -54,7 +54,7 @@ const BOOKS: &[(&str, &str, &str, &str)] = &[ fn project_root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() - .expect("xtask must live in a workspace subdirectory") + .expect("xtask должен находиться во вложенном каталоге воркспейса") .to_path_buf() } @@ -70,7 +70,7 @@ fn main() { Some("clean") => cmd_clean(), Some("--help" | "-h" | "help") | None => print_usage(0), Some(other) => { - eprintln!("Unknown command: {other}\n"); + eprintln!("Неизвестная команда: {other}\n"); print_usage(1); } } @@ -85,22 +85,22 @@ fn print_usage(code: i32) { let _ = writeln!( stream, "\ -Usage: cargo xtask +Использование: cargo xtask <КОМАНДА> -Commands: - build Build all books into site/ (for local preview) - serve Build and serve at http://localhost:3000 - deploy Build all books into docs/ (for GitHub Pages) - clean Remove site/ and docs/ directories" +Команды: + build Собрать все книги в site/ (для локального просмотра) + serve Собрать и запустить сервер на http://localhost:3000 + deploy Собрать все книги в docs/ (для GitHub Pages) + clean Удалить каталоги site/ и docs/" ); std::process::exit(code); } -// ── build ──────────────────────────────────────────────────────────── +// ── сборка ─────────────────────────────────────────────────────────── fn cmd_build() { if !check_mdbook() { - eprintln!("Error: 'mdbook' not found in PATH. Please install it: https://rust-lang.github.io/mdbook/guide/installation.html"); + eprintln!("Ошибка: 'mdbook' не найден в PATH. Установите его: https://rust-lang.github.io/mdBook/guide/installation.html"); std::process::exit(1); } build_to("site"); @@ -108,11 +108,11 @@ fn cmd_build() { fn cmd_deploy() { if !check_mdbook() { - eprintln!("Error: 'mdbook' not found in PATH."); + eprintln!("Ошибка: 'mdbook' не найден в PATH."); std::process::exit(1); } build_to("docs"); - println!("\nTo publish, commit docs/ and enable GitHub Pages → \"Deploy from a branch\" → /docs."); + println!("\nЧтобы опубликовать результат, закоммитьте docs/ и включите GitHub Pages: Settings → Pages → Deploy from a branch → /docs."); } fn check_mdbook() -> bool { @@ -130,17 +130,17 @@ fn build_to(dir_name: &str) { let out = root.join(dir_name); if out.exists() { - fs::remove_dir_all(&out).expect("failed to clean output dir"); + fs::remove_dir_all(&out).expect("не удалось очистить каталог вывода"); } - fs::create_dir_all(&out).expect("failed to create output dir"); + fs::create_dir_all(&out).expect("не удалось создать каталог вывода"); - println!("Building unified site into {dir_name}/\n"); + println!("Сборка общего сайта в {dir_name}/\n"); let mut ok = 0u32; for &(slug, _, _, _) in BOOKS { let book_dir = root.join(slug); if !book_dir.is_dir() { - eprintln!(" ✗ {slug}/ not found, skipping"); + eprintln!(" ✗ {slug}/ не найден, пропускаю"); continue; } let dest = out.join(slug); @@ -149,31 +149,31 @@ fn build_to(dir_name: &str) { .arg(&dest) .current_dir(&book_dir) .status() - .expect("failed to run mdbook — is it installed?"); + .expect("не удалось запустить mdbook: установлен ли он?"); if status.success() { println!(" ✓ {slug}"); ok += 1; } else { - eprintln!(" ✗ {slug} FAILED"); + eprintln!(" ✗ {slug}: ОШИБКА СБОРКИ"); } } - println!("\n {ok}/{} books built", BOOKS.len()); + println!("\n собрано книг: {ok}/{}", BOOKS.len()); write_landing_page(&out); - // Prevent GitHub Pages from processing the output with Jekyll - fs::write(out.join(".nojekyll"), "").expect("failed to create .nojekyll"); - println!("\nDone! Output in {dir_name}/"); + // Отключаем обработку Jekyll на GitHub Pages + fs::write(out.join(".nojekyll"), "").expect("не удалось создать .nojekyll"); + println!("\nГотово! Результат в {dir_name}/"); } fn category_label(cat: &str) -> &str { match cat { - "bridge" => "Bridge", - "deep-dive" => "Deep Dive", - "advanced" => "Advanced", - "expert" => "Expert", - "practices" => "Practices", + "bridge" => "Мост", + "deep-dive" => "Погружение", + "advanced" => "Продвинутый", + "expert" => "Эксперт", + "practices" => "Практики", _ => cat, } } @@ -195,11 +195,11 @@ fn write_landing_page(site: &Path) { let html = format!( r##" - + - Rust Training Books + Учебные книги по Rust -

🦀 Rust Training Books

-

Pick the guide that matches your background

+

🦀 Rust: учебные книги

+

Выберите курс, который соответствует вашему опыту

- Bridge — learn Rust from another language - Deep Dive - Advanced - Expert - Practices + Мост: изучение Rust из другого языка + Погружение + Продвинутый + Эксперт + Практики
{cards}
-
+
"## ); let path = site.join("index.html"); - fs::write(&path, html).expect("failed to write index.html"); + fs::write(&path, html).expect("не удалось записать index.html"); println!(" ✓ index.html"); } @@ -316,15 +316,15 @@ enum ResolveResult { NotFound, } -/// Resolve `request_target` (HTTP request path, e.g. `/foo/bar?x=1`) to a file under `site_canon`. -/// Returns `ResolveResult::File` for success, `Redirect` if a trailing slash is needed for a directory, -/// or `NotFound` for traversal attempts or missing files. +/// Преобразует `request_target` (путь HTTP-запроса, например `/foo/bar?x=1`) в файл внутри `site_canon`. +/// Возвращает `ResolveResult::File`, если файл найден, `Redirect`, если для каталога нужен завершающий слеш, +/// и `NotFound` для попыток обхода каталогов и отсутствующих файлов. /// -/// NOTE: This function preserves and hardens the multi-layer security from PR#18: -/// 1. Percent-decoding via `percent_decode_path`. -/// 2. Null byte rejection. -/// 3. Traversal blocking (`..`). -/// 4. Symlink escape prevention via canonicalization and prefix checking. +/// ПРИМЕЧАНИЕ: функция сохраняет и усиливает многоуровневую защиту из PR#18: +/// 1. Декодирование percent-encoding через `percent_decode_path`. +/// 2. Отказ от нулевых байтов. +/// 3. Блокировка обхода каталогов (`..`). +/// 4. Защита от выхода за пределы каталога через символические ссылки: канонизация и проверка префикса. fn resolve_site_file(site_canon: &Path, request_target: &str) -> ResolveResult { let path_only = match request_target .split('?') @@ -335,7 +335,7 @@ fn resolve_site_file(site_canon: &Path, request_target: &str) -> ResolveResult { None => return ResolveResult::NotFound, }; - // [Security] Handle percent-encoding and reject null bytes (from PR#18) + // [Безопасность] Декодируем percent-encoding и отклоняем нулевые байты (из PR#18) let decoded = percent_decode_path(path_only); if decoded.as_bytes().contains(&0) { return ResolveResult::NotFound; @@ -345,7 +345,7 @@ fn resolve_site_file(site_canon: &Path, request_target: &str) -> ResolveResult { let mut file_path = site_canon.to_path_buf(); if !rel.is_empty() { for seg in rel.split('/').filter(|s| !s.is_empty()) { - // [Security] Block directory traversal (from PR#18) + // [Безопасность] Блокируем обход каталогов (из PR#18) if seg == ".." { return ResolveResult::NotFound; } @@ -354,14 +354,14 @@ fn resolve_site_file(site_canon: &Path, request_target: &str) -> ResolveResult { } if file_path.is_dir() { - // If it refers to a directory but lacks a trailing slash, redirect so relative links work. + // Если путь указывает на каталог без завершающего слеша, перенаправляем, чтобы относительные ссылки работали. if !request_target.ends_with('/') && !request_target.is_empty() { return ResolveResult::Redirect(format!("{path_only}/")); } file_path.push("index.html"); } - // [Security] Canonicalize and verify we're still within site_canon (from PR#18) + // [Безопасность] Канонизируем путь и проверяем, что он остаётся внутри site_canon (из PR#18) let real = match fs::canonicalize(&file_path) { Ok(r) => r, Err(_) => return ResolveResult::NotFound, @@ -401,20 +401,20 @@ fn percent_decode_path(input: &str) -> String { String::from_utf8_lossy(&decoded).into_owned() } -// ── serve ──────────────────────────────────────────────────────────── +// ── сервер ─────────────────────────────────────────────────────────── fn cmd_serve() { let site = project_root().join("site"); let site_canon = fs::canonicalize(&site).expect( - "site/ not found — run `cargo xtask build` first (e.g. `cargo xtask serve` runs build automatically)", + "каталог site/ не найден: сначала выполните `cargo xtask build` (команда `cargo xtask serve` запускает сборку автоматически)", ); let addr = "127.0.0.1:3000"; - let listener = TcpListener::bind(addr).expect("failed to bind port 3000"); + let listener = TcpListener::bind(addr).expect("не удалось занять порт 3000"); - // Handle Ctrl+C gracefully so cargo doesn't report an error + // Обрабатываем Ctrl+C, чтобы cargo не сообщал об ошибке ctrlc_exit(); - println!("\nServing at http://localhost:3000 (Ctrl+C to stop)"); + println!("\nСервер запущен: http://localhost:3000 (Ctrl+C — остановить)"); for stream in listener.incoming() { let Ok(mut stream) = stream else { continue }; @@ -458,13 +458,13 @@ fn cmd_serve() { } } -/// Install a Ctrl+C handler that exits cleanly (code 0) instead of -/// letting the OS terminate with STATUS_CONTROL_C_EXIT. +/// Устанавливает обработчик Ctrl+C, который завершает программу с кодом 0. +/// Без него ОС завершает процесс с STATUS_CONTROL_C_EXIT. fn ctrlc_exit() { ctrlc::set_handler(move || { std::process::exit(0); }) - .expect("Error setting Ctrl-C handler"); + .expect("не удалось установить обработчик Ctrl-C"); } fn guess_mime(path: &Path) -> &'static str { @@ -482,15 +482,15 @@ fn guess_mime(path: &Path) -> &'static str { } } -// ── clean ──────────────────────────────────────────────────────────── +// ── очистка ────────────────────────────────────────────────────────── fn cmd_clean() { let root = project_root(); for dir_name in ["site", "docs"] { let dir = root.join(dir_name); if dir.exists() { - fs::remove_dir_all(&dir).expect("failed to remove dir"); - println!("Removed {dir_name}/"); + fs::remove_dir_all(&dir).expect("не удалось удалить каталог"); + println!("Удалено: {dir_name}/"); } } }