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 ; 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 fb1bb72..5d9d6b8 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,124 @@ -
+# Учебные книги по Rust -**License** This project is dual-licensed under the [MIT License](LICENSE) and [Creative Commons Attribution 4.0 International (CC-BY-4.0)](LICENSE-DOCS). +Семь учебных курсов по Rust на русском языке: от перехода с C/C++, C#, Python до асинхронного программирования, продвинутых паттернов, корректности на уровне типов и инженерной практики в продакшене. -
+Каждую книгу можно читать отдельно, но порядок в таблице ниже соответствует естественному пути обучения. Текст — на русском. Код, имена крейтов, команды и сообщения компилятора остаются на английском, как принято в экосистеме Rust. -
+> **Важно:** это учебные материалы, а не авторитетный справочник. Мы стремимся к точности, но ключевые детали сверяйте с [официальной документацией Rust](https://doc.rust-lang.org/) и [Rust Reference](https://doc.rust-lang.org/reference/). -**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. +**Читать онлайн:** [r007.github.io/RustTraining-Rus](https://r007.github.io/RustTraining-Rus/) +**Исходный код:** [github.com/r007/RustTraining-Rus](https://github.com/r007/RustTraining-Rus) -
+## 📖 С чего начать -# Rust Training Books +Выберите книгу, которая соответствует вашему опыту. Книги сгруппированы по сложности: -Seven training courses covering Rust from different programming backgrounds, plus deep-dives on async, advanced patterns, and engineering practices. +| Уровень | Описание | +|---------|----------| +| 🟢 **Мост** | Изучение Rust для тех, кто пришёл из другого языка. Начните отсюда | +| 🔵 **Погружение** | Углублённое изучение крупной подсистемы Rust | +| 🟡 **Продвинутый** | Паттерны и приёмы для опытных 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 для программистов на 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 | -> **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/). +Книги содержат диаграммы Mermaid, редактируемые песочницы Rust, упражнения и полнотекстовый поиск. -### Inspirations & Acknowledgments - -- [**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 -- [**Binary Musings - Tag(Rust)**](https://binarymusings.org/posts/category/rust/) — Deep dive into Rust internals -- …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 +> **Совет:** читайте книги на [сайте](https://r007.github.io/RustTraining-Rus/): там есть навигация по боковой панели и поиск. +> +> **Локальный просмотр:** для работы без интернета или при участии в разработке (сначала [установите Rust](https://rustup.rs/)): +> ``` +> git clone https://github.com/r007/RustTraining-Rus.git +> cd RustTraining-Rus +> cargo install mdbook mdbook-mermaid +> cargo xtask serve # собирает все книги и запускает сервер: http://localhost:3000 +> ``` -## 📖 Start Reading +--- -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 | +- [**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 и обсуждения на форумах легли в основу этих материалов. Их слишком много, чтобы перечислить поимённо, но мы искренне благодарны каждому -| Book | Level | Who it's for | -|------|-------|-------------| -| [**Rust for C/C++ Programmers**](https://microsoft.github.io/RustTraining/c-cpp-book/) | 🟢 Bridge | Move semantics, RAII, FFI, embedded, no_std | -| [**Rust for C# Programmers**](https://microsoft.github.io/RustTraining/csharp-book/) | 🟢 Bridge | Swift / C# / Java → ownership & type system | -| [**Rust for Python Programmers**](https://microsoft.github.io/RustTraining/python-book/) | 🟢 Bridge | Dynamic → static typing, GIL-free concurrency | -| [**Async Rust**](https://microsoft.github.io/RustTraining/async-book/) | 🔵 Deep Dive | Tokio, streams, cancellation safety | -| [**Rust Patterns**](https://microsoft.github.io/RustTraining/rust-patterns-book/) | 🟡 Advanced | Pin, allocators, lock-free structures, unsafe | -| [**Type-Driven Correctness**](https://microsoft.github.io/RustTraining/type-driven-correctness-book/) | 🟣 Expert | Type-state, phantom types, capability tokens | -| [**Rust Engineering Practices**](https://microsoft.github.io/RustTraining/engineering-book/) | 🟤 Practices | Build scripts, cross-compilation, CI/CD, Miri | +--- -Each book has 15–16 chapters with Mermaid diagrams, editable Rust playgrounds, exercises, and full-text search. +## 🤝 Участие в проекте -> **Tip:** Browse the rendered books with sidebar navigation and search at the [GitHub Pages site](https://microsoft.github.io/RustTraining/). -> -> **Local preview:** For offline reading or while contributing ([install Rust](https://rustup.rs/) first): -> ``` -> git clone https://github.com/microsoft/RustTraining.git -> cd RustTraining -> cargo install mdbook mdbook-mermaid -> cargo xtask serve # http://localhost:3000 -> ``` +Исправления опечаток, неточностей, перевода и примеров приветствуются. Порядок работы и стиль перевода описаны в [CONTRIBUTING.md](CONTRIBUTING.md). Сообщить об уязвимости можно по правилам из [SECURITY.md](SECURITY.md). --- -## 🔧 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@0.4.52 mdbook-mermaid@0.14.0 ``` -### Clone the repo +### Клонирование репозитория ```bash -git clone https://github.com/microsoft/RustTraining.git -cd RustTraining +git clone https://github.com/r007/RustTraining-Rus.git +cd RustTraining-Rus ``` -### 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 +### Публикация + +Сайт публикуется на GitHub Pages при push в `main` через `.github/workflows/pages.yml`. Вручную ничего делать не нужно, но в настройках репозитория должен быть выбран источник **GitHub Actions** (Settings → Pages → Build and deployment). -The site auto-deploys to GitHub Pages on push to `main` via `.github/workflows/pages.yml`. No manual steps needed. +### 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/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..c94fd96 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 +- Курс основан на оригинальных материалах Rust Training. Лицензии: MIT (код) и CC-BY-4.0 (текст). +- Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus. +- Замечания и исправления присылайте через Issues и Pull Requests репозитория. --- -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 +Каждая содержательная глава содержит встроенное упражнение. Итоговый проект (гл. 17) объединяет всё в одном приложении. Для максимальной пользы: -## 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()` как ещё один ограничитель конкурентности *** - - diff --git a/async-book/src/ch10-async-traits.md b/async-book/src/ch10-async-traits.md index e2a6988..149a977 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 +### RPITIT: возвращаемый impl 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..598aa5a 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 +## Корректное завершение работы (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) — опасности отмены при остановке *** - - 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` для зашифрованных соединений *** 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/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/ch00-introduction.md b/c-cpp-book/src/ch00-introduction.md index 6a099c4..159af1a 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>` | -| 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>` | +| 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\](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\](ch07-2-smart-pointers-and-interior-mutability.md#rust-boxt) -- [Interior Mutability: Cell\ and RefCell\](ch07-2-smart-pointers-and-interior-mutability.md#interior-mutability-cellt-and-refcellt) -- [Shared Ownership: Rc\](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\ в 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\ в Rust](ch07-2-smart-pointers-and-interior-mutability.md#boxt-в-rust) +- [Внутренняя изменяемость: Cell\ и RefCell\](ch07-2-smart-pointers-and-interior-mutability.md#внутренняя-изменяемость-cellt-и-refcellt) +- [Совместное владение: Rc\](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/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` 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`); 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` 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` вынуждает явно обрабатывать отсутствие значения | +| Гонки данных | ✅ | ✅ | Трейты `Send`/`Sync` и проверка заимствований делают гонки данных ошибкой компиляции | +| Неконтролируемые побочные эффекты | ✅ | ✅ | Неизменяемость по умолчанию; для изменения нужен явный `mut` | +| Отсутствие наследования (лучшая сопровождаемость) | — | ✅ | Трейты и композиция заменяют иерархии классов; способствуют повторному использованию без жёсткой связанности | +| Отсутствие исключений; предсказуемый поток управления | — | ✅ | Ошибки — это значения (`Result`); их невозможно проигнорировать, нет скрытых путей `throw` | +| Инвалидация итераторов | — | ✅ | Проверка заимствований запрещает изменять коллекцию во время итерации по ней | +| Циклы ссылок / утечки финализаторов | — | ✅ | Владение имеет древовидную структуру; циклы `Rc` — явный выбор, который можно обнаружить и разорвать с помощью `Weak` | +| Забытые разблокировки мьютексов | ✅ | ✅ | `Mutex` оборачивает данные; защитник блокировки — единственный способ доступа к ним | +| Неопределённое поведение (вообще) | ✅ | ✅ | В безопасном 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 @@ -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` helps but is verbose and often bypassed with `.value()` which throws. +В C++ `std::optional` помогает, но он громоздкий, и его часто обходят вызовом `.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 ptr = std::make_unique(42); std::unique_ptr 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 next; - std::shared_ptr parent; // Cycle! Destructor never called. + std::shared_ptr parent; // Цикл! Деструктор никогда не вызывается. }; auto a = std::make_shared(); auto b = std::make_shared(); a->next = b; -b->parent = a; // Memory leak — ref count never reaches 0 - // In Rust, Rc + Weak makes cycles explicit and breakable +b->parent = a; // Утечка памяти — счётчик ссылок никогда не станет 0 + // В Rust Rc + Weak делают циклы явными и разрываемыми ``` -### 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>({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(device)) { driver = new DriverForModelA(framework); } else if (dynamic_cast(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
(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
Compiles without warning"] - CYCLE --> CYCLE1["shared_ptr cycles leak
Destructor never called"] - ITER --> ITER1["erase() invalidates iterators
Real production bugs"] - EXC --> EXC1["Partial construction
new without try/catch"] - TMPL --> TMPL1["30+ lines of nested
template instantiation errors"] + ROOT["Дополнительные проблемы C++
(помимо проблем C)"] --> UAM["Использование после перемещения"] + ROOT --> CYCLE["Циклы ссылок"] + ROOT --> ITER["Инвалидация итераторов"] + ROOT --> EXC["Безопасность исключений"] + ROOT --> TMPL["Сообщения об ошибках шаблонов"] + + UAM --> UAM1["std::move оставляет зомби
Компилируется без предупреждений"] + CYCLE --> CYCLE1["Циклы shared_ptr приводят к утечкам
Деструктор никогда не вызывается"] + ITER --> ITER1["erase() инвалидирует итераторы
Реальные ошибки в продакшене"] + EXC --> EXC1["Частичное конструирование
new без try/catch"] + TMPL --> TMPL1["30+ строк вложенных ошибок
инстанцирования шаблонов"] 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` 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` is visible in type signatures, propagated with `?` | -| Inheritance complexity | Traits + composition; no Diamond Problem, no vtable fragility | -| Forgotten mutex unlocks | `Mutex` wraps the data; lock guard is the only access path | +## Как Rust решает всё это + +Каждая проблема, перечисленная выше — и из C, и из C++, — предотвращается гарантиями Rust на этапе компиляции: + +| Проблема | Решение в Rust | +|----------|----------------| +| Переполнения буфера | Срезы хранят длину; индексация проверяется на границы | +| Висячие указатели / использование после освобождения | Система времён жизни доказывает, что ссылки валидны, на этапе компиляции | +| Использование после перемещения | Перемещения разрушающие — компилятор не позволит трогать исходное значение | +| Утечки памяти | Трейт `Drop` = RAII без правила пяти; автоматическая и корректная очистка | +| Циклы ссылок | Владение имеет древовидную структуру; `Rc` + `Weak` делают циклы явными | +| Инвалидация итераторов | Проверка заимствований запрещает изменять коллекцию, пока она заимствована | +| NULL-указатели | Нет null. `Option` вынуждает явно обрабатывать значение через сопоставление с образцом | +| Гонки данных | Трейты `Send`/`Sync` делают гонки данных ошибкой компиляции | +| Неинициализированные переменные | Все переменные должны быть инициализированы; это проверяет компилятор | +| Неопределённое поведение при переполнении | В отладке panic; в релизе переполнение (оба варианта определены) | +| Исключения | Исключений нет; `Result` виден в сигнатурах типов и распространяется с помощью `?` | +| Сложность наследования | Трейты + композиция; нет проблемы ромба, нет хрупкости таблиц виртуальных функций | +| Забытые разблокировки мьютексов | `Mutex` оборачивает данные; защитник блокировки — единственный путь доступа | ```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 = 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
No use-after-move
No double-free"] - BORROW --> BORROW1["No dangling references
No iterator invalidation
No data races through refs"] - TYPES --> TYPES1["No NULL (Option<T>)
No exceptions (Result<T,E>)
No uninitialized values"] - TRAITS --> TRAITS1["No data races
Send = safe to transfer
Sync = safe to share"] + OWN --> OWN1["Нет использования после освобождения
Нет использования после перемещения
Нет двойного освобождения"] + BORROW --> BORROW1["Нет висячих ссылок
Нет инвалидации итераторов
Нет гонок данных через ссылки"] + TYPES --> TYPES1["Нет NULL (Option<T>)
Нет исключений (Result<T,E>)
Нет неинициализированных значений"] + TRAITS --> TRAITS1["Нет гонок данных
Send = безопасно передавать
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`, `Rc`, `Arc` | Automatic, no cycles, no zombies | -| Arrays | `int arr[10]` | `std::vector`, `std::array` | `Vec`, `[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`, `Option` | No hidden control flow | -| NULL safety | `ptr == NULL` | `nullptr`, `std::optional` | `Option` | 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`, `Rc`, `Arc` | Автоматическое, без циклов и зомби | +| Массивы | `int arr[10]` | `std::vector`, `std::array` | `Vec`, `[T; N]` | Проверка границ по умолчанию | +| Строки | `char*` с `\0` | `std::string`, `string_view` | `String`, `&str` | Гарантированный UTF-8, проверка времён жизни | +| Ссылки | `int*` (сырые) | `T&`, `T&&` (перемещение) | `&T`, `&mut T` | Проверка времён жизни и заимствований | +| Полиморфизм | Указатели на функции | Виртуальные функции, наследование | Трейты, трейт-объекты | Композиция вместо наследования | +| Обобщения | Макросы / `void*` | Шаблоны | Обобщения + ограничения трейтами | Понятные сообщения об ошибках | +| Обработка ошибок | Коды возврата, `errno` | Исключения, `std::optional` | `Result`, `Option` | Никакого скрытого потока управления | +| Безопасность при null | `ptr == NULL` | `nullptr`, `std::optional` | `Option` | Принудительная проверка на 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..411fdc7 100644 --- a/c-cpp-book/src/ch01-introduction-and-motivation.md +++ b/c-cpp-book/src/ch01-introduction-and-motivation.md @@ -1,87 +1,86 @@ -# 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 +- О курсе + - Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0) + - Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus +- Курс задуман как можно более интерактивный + - Предположение: вы знаете 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`), making error handling explicit and visible in the type signature +## Обработка ошибок +- В Rust нет исключений. Все ошибки — это значения (`Result`), поэтому обработка ошибок явная и видна в сигнатуре типа -## 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 +88,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 — errors in types"] - E --> E2["? operator for propagation"] + E --> E1["Result — ошибки в типах"] + E --> E2["Оператор ? для распространения"] style A fill:#51cf66,color:#000 style B fill:#91e5a3,color:#000 @@ -113,64 +112,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` Heap Allocation Visualization +### Визуализация выделения памяти в `Box` ```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"] BP2["moved_box: Box"] 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"] 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 +185,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 +197,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` wraps the data, not the access) -- No exception handling complexity - - Errors are values (`Result`), 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`, `Rc`, `Arc` | Automatic, no cycles | -| Arrays | `int arr[10]` | `std::vector`, `std::array` | `Vec`, `[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`, `Option` | No hidden control flow | -| NULL/null safety | `ptr == NULL` | `nullptr`, `std::optional` | `Option` | 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` оборачивает данные, а не доступ к ним) +- Никакой сложности обработки исключений + - Ошибки — это значения (`Result`), видны в сигнатурах функций и распространяются с помощью `?` +- Отличная поддержка вывода типов, перечислений, сопоставления с образцом и абстракций с нулевой стоимостью +- Встроенная поддержка управления зависимостями, сборки, тестирования, форматирования и линтинга + - `cargo` заменяет make/CMake и фреймворки для линтинга и тестирования + +# Краткий справочник: Rust и C/C++ + +| **Концепция** | **C** | **C++** | **Rust** | **Ключевое отличие** | +|---------------|-------|---------|----------|---------------------| +| Управление памятью | `malloc()/free()` | `unique_ptr`, `shared_ptr` | `Box`, `Rc`, `Arc` | Автоматическое, без циклов | +| Массивы | `int arr[10]` | `std::vector`, `std::array` | `Vec`, `[T; N]` | Проверка границ по умолчанию | +| Строки | `char*` с `\0` | `std::string`, `string_view` | `String`, `&str` | Гарантированный UTF-8, проверка времён жизни | +| Ссылки | `int* ptr` | `T&`, `T&&` (перемещение) | `&T`, `&mut T` | Проверка заимствований, времена жизни | +| Полиморфизм | Указатели на функции | Виртуальные функции, наследование | Трейты, трейт-объекты | Композиция вместо наследования | +| Обобщённое программирование | Макросы (`void*`) | Шаблоны | Обобщения + ограничения трейтами | Понятные сообщения об ошибках | +| Обработка ошибок | Коды возврата, `errno` | Исключения, `std::optional` | `Result`, `Option` | Никакого скрытого потока управления | +| Безопасность при null | `ptr == NULL` | `nullptr`, `std::optional` | `Option` | Принудительная проверка на 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
(.c, .h)"] - CM["Manual Makefile
or CMake"] - CL["Linker"] - CB["Final Binary"] + subgraph "Традиционный процесс сборки на C" + CC["Исходные файлы C
(.c, .h)"] + CM["Ручной Makefile
или CMake"] + CL["Компоновщик"] + CB["Итоговый бинарный файл"] CC --> CM CM --> CL CL --> CB - CDep["Manual dependency
management"] + CDep["Ручное управление
зависимостями"] CLib1["libcurl-dev
(apt install)"] CLib2["libjson-dev
(apt install)"] - CInc["Manual include paths
-I/usr/include/curl"] - CLink["Manual linking
-lcurl -ljson"] + CInc["Ручные пути включения
-I/usr/include/curl"] + CLink["Ручная компоновка
-lcurl -ljson"] CDep --> CLib1 CDep --> CLib2 @@ -65,28 +65,28 @@ graph TD CInc --> CM CLink --> CL - C_ISSUES["[ERROR] Version conflicts
[ERROR] Platform differences
[ERROR] Missing dependencies
[ERROR] Linking order matters
[ERROR] No automated updates"] + C_ISSUES["[ОШИБКА] Конфликты версий
[ОШИБКА] Различия платформ
[ОШИБКА] Отсутствующие зависимости
[ОШИБКА] Важен порядок компоновки
[ОШИБКА] Нет автоматических обновлений"] end - subgraph "Rust Cargo Build Process" - RS["Rust Source Files
(.rs)"] + subgraph "Процесс сборки Rust с Cargo" + RS["Исходные файлы Rust
(.rs)"] CT["Cargo.toml
[dependencies]
reqwest = '0.11'
serde_json = '1.0'"] - CRG["Cargo Build System"] - RB["Final Binary"] + CRG["Система сборки Cargo"] + RB["Итоговый бинарный файл"] RS --> CRG CT --> CRG CRG --> RB - CRATES["crates.io
(Package registry)"] - DEPS["Automatic dependency
resolution"] - LOCK["Cargo.lock
(Version pinning)"] + CRATES["crates.io
(реестр пакетов)"] + DEPS["Автоматическое разрешение
зависимостей"] + LOCK["Cargo.lock
(фиксация версий)"] CRATES --> DEPS DEPS --> CRG CRG --> LOCK - R_BENEFITS["[OK] Semantic versioning
[OK] Automatic downloads
[OK] Cross-platform
[OK] Transitive dependencies
[OK] Reproducible builds"] + R_BENEFITS["[OK] Семантическое версионирование
[OK] Автоматическая загрузка
[OK] Кроссплатформенность
[OK] Транзитивные зависимости
[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
[FOLDER] Create new project"] - CHECK["cargo check
[SEARCH] Fast syntax check"] - BUILD["cargo build
[BUILD] Compile project"] - RUN["cargo run
[PLAY] Build and execute"] - TEST["cargo test
[TEST] Run all tests"] + subgraph "Жизненный цикл проекта" + NEW["cargo new my_project
[FOLDER] Создать проект"] + CHECK["cargo check
[SEARCH] Быстрая проверка синтаксиса"] + BUILD["cargo build
[BUILD] Скомпилировать проект"] + RUN["cargo run
[PLAY] Собрать и выполнить"] + TEST["cargo test
[TEST] Запустить все тесты"] NEW --> CHECK CHECK --> BUILD @@ -134,17 +134,17 @@ graph LR BUILD --> TEST end - subgraph "Advanced Commands" - UPDATE["cargo update
[CHART] Update dependencies"] - FORMAT["cargo fmt
[SPARKLES] Format code"] - LINT["cargo clippy
[WRENCH] Lint and suggestions"] - DOC["cargo doc
[BOOKS] Generate documentation"] - PUBLISH["cargo publish
[PACKAGE] Publish to crates.io"] + subgraph "Дополнительные команды" + UPDATE["cargo update
[CHART] Обновить зависимости"] + FORMAT["cargo fmt
[SPARKLES] Форматировать код"] + LINT["cargo clippy
[WRENCH] Линтинг и подсказки"] + DOC["cargo doc
[BOOKS] Сгенерировать документацию"] + PUBLISH["cargo publish
[PACKAGE] Опубликовать на crates.io"] end - subgraph "Build Profiles" - DEBUG["cargo build
(debug profile)
Fast compile
Slow runtime
Debug symbols"] - RELEASE["cargo build --release
(release profile)
Slow compile
Fast runtime
Optimized"] + subgraph "Профили сборки" + DEBUG["cargo build
(отладочный профиль)
Быстрая компиляция
Медленное выполнение
Отладочные символы"] + RELEASE["cargo build --release
(релизный профиль)
Медленная компиляция
Быстрое выполнение
Оптимизирован"] 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: 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`?) -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust fn count_words(text: &str) -> usize { @@ -211,10 +211,10 @@ fn main() {
-# 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``` 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``` реализует динамический буфер, размещённый в куче (похоже на вручную управляемые массивы через `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 + let mut v = Vec::new(); // Пустой вектор, тип выводится из использования + v.push(42); // Добавляем элемент в конец — Vec 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``` 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` с несколькими записями (убедитесь, что одни значения равны `true`, а другие — `false`). Переберите все элементы хеш-таблицы, поместив ключи в один `Vec`, а значения — в другой -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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, Vec) = 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 -void forward(T&& arg) { // NOT an rvalue ref! Deduced as T& or T&& - inner(std::forward(arg)); // Perfect forwarding +void forward(T&& arg) { // НЕ rvalue-ссылка! Выводится как T& или T&& + inner(std::forward(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 `` | Generics replace forwarding | -| `std::move(x)` | `x` (just use it) | Move is the default | -| `std::forward(x)` | No equivalent needed | No universal references to forward | +| Концепция C++ | Эквивалент в Rust | Примечания | +|---------------|-------------------|------------| +| `T&` (ссылка на lvalue) | `&T` или `&mut T` | Rust разделяет на общие и эксклюзивные | +| `T&&` (rvalue-ссылка) | Просто `T` | Передача по значению = передача владения | +| `T&&` в шаблоне (универсальная ссылка) | `impl Trait` или `` | Обобщения заменяют передачу | +| `std::move(x)` | `x` (просто используйте) | Перемещение — поведение по умолчанию | +| `std::forward(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>> +// Вложенные обёртки: Arc>> 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 → &str (2 deref coercions) - greet(&arced); // &Arc → &String → &str (2 deref coercions) - greet("Dave"); // &str already — no coercion needed + greet(&owned); // &String → &str (1 приведение через Deref) + greet(&boxed); // &Box → &String → &str (2 приведения через Deref) + greet(&arced); // &Arc → &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>` -"just works" like a `Vec`. Deref *coercion* (for function arguments) -is a separate but related mechanism that automatically converts `&Box` -to `&str` by chaining `Deref` impls. +**Цепочка Deref**: когда вы вызываете `x.method()`, разрешение метода в Rust +сначала пробует тип получателя `T`, затем `&T`, затем `&mut T`. Если совпадения +нет, выполняется разыменование через трейт `Deref`, и процесс повторяется для целевого типа. +Это продолжается через несколько слоёв — поэтому `Box>` +«просто работает» как `Vec`. Приведение через Deref (для аргументов функций) +— отдельный, но связанный механизм, который автоматически превращает `&Box` +в `&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) => { /* ваш код */ }, } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust enum Operation { @@ -179,18 +179,18 @@ fn main() { CalcResult::Invalid(msg) => println!("Error: {msg}"), } } -// Output: +// Вывод: // 10 + 20 = 30 // Error: Underflow ```
-# 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 в квадрат -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() его поглотил } ```
---- - 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). -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
-# 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() { } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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
with references"] --> R1 - R1["Rule 1: Each input
reference gets its own
lifetime

fn f(&str, &str)
→ fn f<'a,'b>(&'a str,
&'b str)"] + A["Сигнатура функции
со ссылками"] --> R1 + R1["Правило 1: каждая входная
ссылка получает собственное
время жизни

fn f(&str, &str)
→ fn f<'a,'b>(&'a str,
&'b str)"] R1 --> R2 - R2["Rule 2: If exactly ONE
input lifetime, assign it
to ALL outputs

fn f(&str) → &str
→ fn f<'a>(&'a str)
→ &'a str"] + R2["Правило 2: если есть ровно ОДНО
входное время жизни, назначаем его
ВСЕМ выходным

fn f(&str) → &str
→ fn f<'a>(&'a str)
→ &'a str"] R2 --> R3 - R3["Rule 3: If one input is
&self or &mut self,
assign its lifetime to
ALL outputs

fn f(&self, &str) → &str
→ fn f<'a>(&'a self, &str)
→ &'a str"] - R3 --> CHECK{{"All output
lifetimes
determined?"}} - CHECK -->|Yes| OK["✅ No annotations
needed"] - CHECK -->|No| ERR["❌ Compile error:
must annotate
manually"] + R3["Правило 3: если один из входов —
&self или &mut self, назначаем его
время жизни ВСЕМ выходным

fn f(&self, &str) → &str
→ fn f<'a>(&'a self, &str)
→ &'a str"] + R3 --> CHECK{{"Определены ли
времена жизни всех
выходных значений?"}} + CHECK -->|Да| OK["✅ Аннотации
не нужны"] + CHECK -->|Нет| ERR["❌ Ошибка компиляции:
нужно аннотировать
вручную"] 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: 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..]) } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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` +# `Box` в Rust -> **What you'll learn:** Rust's smart pointer types — `Box` for heap allocation, `Rc` for shared ownership, and `Cell`/`RefCell` for interior mutability. These build on the ownership and lifetime concepts from the previous sections. You'll also see a brief introduction to `Weak` for breaking reference cycles. +> **Что вы узнаете:** умные указатели Rust — `Box` для выделения памяти в куче, `Rc` для совместного владения и `Cell`/`RefCell` для внутренней изменяемости. Они опираются на понятия владения и времён жизни из предыдущих разделов. Вы также кратко познакомитесь с `Weak` для разрыва циклов ссылок. -**Why `Box`?** In C, you use `malloc`/`free` for heap allocation. In C++, `std::unique_ptr` wraps `new`/`delete`. Rust's `Box` 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`?** В C для выделения памяти в куче используют `malloc`/`free`. В C++ `std::unique_ptr` оборачивает `new`/`delete`. `Box` в 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`) +**Когда использовать `Box`, а когда выделение на стеке:** +- Содержащийся тип большой, и вы не хотите копировать его на стеке +- Нужен рекурсивный тип (например, узел связного списка, который содержит сам себя) +- Нужны трейт-объекты (`Box`) -- ```Box``` can be use to create a pointer to a heap allocated type. The pointer is always a fixed size regardless of the type of `````` +- ```Box``` можно использовать, чтобы создать указатель на тип, размещённый в куче. Размер указателя всегда фиксирован, независимо от типа `````` ```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(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(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
value: 42"] + subgraph "Проблемы управления памятью в C/C++" + CP1["int* ptr1"] --> CM["Память в куче
значение: 42"] CP2["int* ptr2"] --> CM CP3["int* ptr3"] --> CM - CF["free(ptr1)"] --> CM_F["[ERROR] Freed Memory"] - CP2 -.->|"Use after free
Undefined Behavior"| CM_F - CP3 -.->|"Use after free
Undefined Behavior"| CM_F + CF["free(ptr1)"] --> CM_F["[ОШИБКА] Освобождённая память"] + CP2 -.->|"Использование после освобождения
Неопределённое поведение"| CM_F + CP3 -.->|"Использование после освобождения
Неопределённое поведение"| CM_F end - subgraph "Rust Ownership System" - RO1["data: Box"] --> RM["Heap Memory
value: 42"] - RO1 -.->|"Move ownership"| RO2["moved_data: Box"] + subgraph "Система владения Rust" + RO1["data: Box"] --> RM["Память в куче
значение: 42"] + RO1 -.->|"Передача владения"| RO2["moved_data: Box"] RO2 --> RM - RO1_X["data: [WARNING] MOVED
Cannot access"] - RB["&moved_data
Immutable borrow"] -.->|"Safe reference"| RM - RD["Drop automatically
when out of scope"] --> RM + RO1_X["data: [ВНИМАНИЕ] ПЕРЕМЕЩЕНО
Доступа нет"] + RB["&moved_data
Неизменяемое заимствование"] -.->|"Безопасная ссылка"| RM + RD["Drop автоматически
при выходе из области видимости"] --> 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"] - 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
[ERROR] Use after free possible"] + RACE["[ОШИБКА] Возможны гонки данных
[ОШИБКА] Возможно использование после освобождения"] end style MEM1 fill:#91e5a3,color:#000 @@ -197,47 +197,47 @@ graph TD --- -## Interior Mutability: `Cell` and `RefCell` +## Внутренняя изменяемость: `Cell` и `RefCell` -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` — interior mutability for Copy types +### `Cell` — внутренняя изменяемость для типов Copy -- `Cell` 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` обеспечивает **внутреннюю изменяемость**, то есть доступ на запись к отдельным элементам через ссылки, которые в остальном доступны только для чтения +- Работает копированием значений внутрь и наружу (для `.get()` требуется `T: Copy`) -### `RefCell` — interior mutability with runtime borrow checking +### `RefCell` — внутренняя изменяемость с проверкой заимствований во время выполнения -- `RefCell` 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` — вариант, который работает со ссылками + - Применяет проверку заимствований Rust **во время выполнения**, а не на этапе компиляции + - Допускает одно *изменяемое* заимствование, но **вызывает panic**, если есть любые другие активные ссылки + - Используйте `.borrow()` для неизменяемого доступа и `.borrow_mut()` для изменяемого -### When to Choose `Cell` vs `RefCell` +### Когда выбрать `Cell`, а когда `RefCell` -| Criterion | `Cell` | `RefCell` | -|-----------|-----------|-------------| -| 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` | `RefCell` | +|----------|-----------|-------------| +| Работает с | Типами `Copy` (целые, bool, числа с плавающей точкой) | Любыми типами (`String`, `Vec`, структуры) | +| Схема доступа | Копирует значения внутрь/наружу (`.get()`, `.set()`) | Заимствует на месте (`.borrow()`, `.borrow_mut()`) | +| Режим сбоя | Не может упасть — нет проверок во время выполнения | **Panic**, если берёте изменяемое заимствование, пока активно другое | +| Накладные расходы | Нулевые — просто копирование байтов | Небольшие — отслеживание состояния заимствования во время выполнения | +| Использовать, когда | Нужен изменяемый флаг, счётчик или небольшое значение внутри неизменяемой структуры | Нужно изменить `String`, `Vec` или сложный тип внутри неизменяемой структуры | --- -## Shared Ownership: `Rc` +## Совместное владение: `Rc` -`Rc` allows reference-counted shared ownership of *immutable* data. What if we wanted to store the same `Employee` in multiple places without copying? +`Rc` позволяет совместное владение *неизменяемыми* данными с подсчётом ссылок. Что, если мы хотим хранить одного и того же `Employee` в нескольких местах без копирования? ```rust #[derive(Debug)] @@ -249,14 +249,14 @@ fn main() { let mut all_global_employees = Vec::::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` 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` решает эту проблему, разрешая совместный *неизменяемый* доступ: +- Содержащийся тип автоматически разыменовывается +- Тип уничтожается, когда счётчик ссылок становится 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` | `Box` | Rust's version is the default — move is language-level, not opt-in | -> | `std::shared_ptr` | `Rc` (single-thread) / `Arc` (multi-thread) | No atomic overhead for `Rc`; use `Arc` only when sharing across threads | -> | `std::weak_ptr` | `Weak` (from `Rc::downgrade()` or `Arc::downgrade()`) | Same purpose: break reference cycles | +> | `std::unique_ptr` | `Box` | Версия Rust используется по умолчанию — перемещение задаётся на уровне языка, а не включается явно | +> | `std::shared_ptr` | `Rc` (один поток) / `Arc` (несколько потоков) | У `Rc` нет накладных расходов на атомарные операции; `Arc` используйте, только когда делитесь данными между потоками | +> | `std::weak_ptr` | `Weak` (из `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` +### Разрыв циклов ссылок с помощью `Weak` -`Rc` uses reference counting — if two `Rc` values point to each other, neither will ever be dropped (a cycle). `Weak` solves this: +`Rc` использует подсчёт ссылок — если два значения `Rc` указывают друг на друга, ни одно из них никогда не будет уничтожено (цикл). `Weak` решает эту проблему: ```rust use std::rc::{Rc, Weak}; struct Node { value: i32, - parent: Option>, // Weak reference — doesn't prevent drop + parent: Option>, // Слабая ссылка — не мешает уничтожению } 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> + // Чтобы использовать Weak, попробуйте повысить его — возвращается Option> 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` 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` подробнее рассматривается в главе [Избегание избыточного clone()](ch17-1-avoiding-excessive-clone.md). Пока главное: **используйте `Weak` для «обратных ссылок» в древовидных и графовых структурах, чтобы избежать утечек памяти.** --- -## Combining `Rc` with Interior Mutability +## Комбинирование `Rc` с внутренней изменяемостью -The real power emerges when you combine `Rc` (shared ownership) with `Cell` or `RefCell` (interior mutability). This lets multiple owners **read and modify** shared data: +Настоящая мощь проявляется, когда вы комбинируете `Rc` (совместное владение) с `Cell` или `RefCell` (внутренняя изменяемость). Это позволяет нескольким владельцам **читать и изменять** общие данные: -| Pattern | Use case | -|---------|----------| -| `Rc>` | Shared, mutable data (single-threaded) | -| `Arc>` | Shared, mutable data (multi-threaded — see [ch13](ch13-concurrency.md)) | -| `Rc>` | Shared, mutable Copy types (simple flags, counters) | +| Шаблон | Сценарий использования | +|--------|----------| +| `Rc>` | Общие изменяемые данные (один поток) | +| `Arc>` | Общие изменяемые данные (несколько потоков — см. [ch13](ch13-concurrency.md)) | +| `Rc>` | Общие изменяемые типы 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` 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` 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` 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` и клонируйте в два отдельных `Vec` (`us_employees` и `global_employees`). Выведите данные из обоих векторов, чтобы показать, что они разделяют одни и те же данные. +- **Часть 2 (Cell)**: добавьте поле `on_vacation: Cell` в `Employee`. Передайте неизменяемую ссылку `&Employee` в функцию и переключите `on_vacation` изнутри этой функции — без того, чтобы делать ссылку изменяемой. +- **Часть 3 (RefCell)**: замените `name: String` на `name: RefCell` и напишите функцию, которая добавляет суффикс к имени сотрудника через `&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, выведите результаты } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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` | `Box` | No use-after-move possible | -> | `std::shared_ptr` | `Rc` (single-thread) | No reference cycles by default | -> | `std::shared_ptr` (thread-safe) | `Arc` | Explicit thread-safety | -> | `std::weak_ptr` | `Weak` | Must check validity | -> | Raw pointer | `*const T` / `*mut T` | Only in `unsafe` blocks | +> | `std::unique_ptr` | `Box` | Использование после перемещения невозможно | +> | `std::shared_ptr` | `Rc` (один поток) | Циклов ссылок по умолчанию нет | +> | `std::shared_ptr` (потокобезопасный) | `Arc` | Потокобезопасность задаётся явно | +> | `std::weak_ptr` | `Weak` | Валидность нужно проверять | +> | Сырой указатель | `*const T` / `*mut T` | Только в блоках `unsafe` | > -> For C developers: `Box` replaces `malloc`/`free` pairs. `Rc` replaces manual reference counting. Raw pointers exist but are confined to `unsafe` blocks. +> Для разработчиков на C: `Box` заменяет пары `malloc`/`free`. `Rc` заменяет ручной подсчёт ссылок. Сырые указатели существуют, но ограничены блоками `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)
ptr"] -->|"owns"| H1["Heap: R u s t"] + subgraph "До: let s1 = s" + S["s (стек)
ptr"] -->|"владеет"| H1["Куча: R u s t"] end - subgraph "After: let s1 = s" - S_MOVED["s (stack)
⚠️ MOVED"] -.->|"invalid"| H2["Heap: R u s t"] - S1["s1 (stack)
ptr"] -->|"now owns"| H2 + subgraph "После: let s1 = s" + S_MOVED["s (стек)
⚠️ ПЕРЕМЕЩЕНО"] -.->|"недействительна"| H2["Куча: R u s t"] + S1["s1 (стек)
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)
ptr"] -->|"owns"| H1["Heap: R u s t"] - S1["s1 (stack)
ptr"] -->|"owns (copy)"| H2["Heap: R u s t"] + subgraph "После: let s1 = s.clone()" + S["s (стек)
ptr"] -->|"владеет"| H1["Куча: R u s t"] + S1["s1 (стек)
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 в функцию и т. д. } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
- 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 { 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; } -// Production implementation +// Продакшн-реализация struct HwSensorReader; impl SensorReader for HwSensorReader { fn read_temperature(&self, sensor_id: u32) -> Result { - // Real hardware call... + // Реальный вызов оборудования... Ok(72.5) } } -// Test mock — returns predictable values +// Тестовый мок — возвращает предсказуемые значения #[cfg(test)] struct MockSensorReader { temperatures: std::collections::HashMap, @@ -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 { 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> { - // 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, ! The secret number is " + // TODO: верните "Hello, ! The secret number is " todo!() } @@ -30,7 +30,7 @@ fn main() { } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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 ```
-## 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)); } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) -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 ```
-# 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 } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
-# 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 { 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> { 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, 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, String> { - // Actually talks to BMC hardware + // Реально обращается к аппаратуре BMC todo!("Real IPMI call") } } -// Mock implementation (used in tests) +// Мок-реализация (используется в тестах) struct MockIpmi { responses: std::collections::HashMap>, } @@ -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 { 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 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 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 +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 +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 { - 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` 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>` 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` исключениям или кодам ошибок +- **Ссылки**: [документация 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>` (но умеренно) + +## Примеры обработки ошибок: хорошо и плохо ```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 { 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`: +Теперь можно писать функции, которые возвращают `Result`: ```rust fn read_config(path: &str) -> Result { @@ -112,58 +112,58 @@ fn read_config(path: &str) -> Result { } ``` -> **🟢 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 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 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`. +В реальных проектах на Rust каждый крейт (или значимый модуль) определяет собственное перечисление `Error` +и псевдоним типа `Result`. Это идиоматичный паттерн — аналогичный тому, как в C++ +вы определяли бы иерархию исключений для библиотеки и `using Result = std::expected`. -### 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 = core::result::Result; ``` -### How it simplifies every function +### Как это упрощает каждую функцию -Without the alias you'd write: +Без псевдонима вы бы писали: ```rust -// Verbose — error type repeated everywhere +// Многословно — тип ошибки повторяется везде fn read_sensor(id: u32) -> Result { ... } fn parse_config(path: &str) -> Result { ... } ``` -With the alias: +С псевдонимом: ```rust -// Clean — just `Result` +// Чисто — просто `Result` use crate::{Error, Result}; fn read_sensor(id: u32) -> Result { @@ -177,10 +177,10 @@ fn read_sensor(id: u32) -> Result { } ``` -The `#[from]` attribute on `Io` generates this `impl` for free: +Атрибут `#[from]` на `Io` генерирует эту реализацию бесплатно: ```rust -// Auto-generated by thiserror's #[from] +// Генерируется автоматически атрибутом #[from] из thiserror impl From for Error { fn from(source: std::io::Error) -> Self { Error::Io(source) @@ -188,13 +188,13 @@ impl From for Error { } ``` -That's what makes `?` work: when a function returns `std::io::Error` and your function -returns `Result` (your alias), the compiler calls `From::from()` to convert it -automatically. +Именно это делает `?` работающим: когда функция возвращает `std::io::Error`, а ваша функция +возвращает `Result` (ваш псевдоним), компилятор вызывает `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 = core::result::Result; ``` -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` or `throw` | `fn foo() -> Result` | -| Convert error | Manual `try/catch` + rethrow | `#[from]` + `?` — zero boilerplate | -| Result alias | `template using Result = std::expected;` | `pub type Result = core::result::Result;` | -| Error message | Override `what()` | `#[error("...")]` — compiled into `Display` impl | - +| Иерархия ошибок | `class AppError : public std::runtime_error` | `#[derive(thiserror::Error)] enum Error { ... }` | +| Возврат ошибки | `std::expected` или `throw` | `fn foo() -> Result` | +| Преобразование ошибки | Ручной `try/catch` + повторный `throw` | `#[from]` + `?` — без шаблонного кода | +| Псевдоним Result | `template using Result = std::expected;` | `pub type Result = core::result::Result;` | +| Сообщение об ошибке | Переопределить `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` and exceptions with `Result`, and how the `?` operator makes error propagation concise. This is Rust's most distinctive pattern — errors are values, not hidden control flow. +> **Что вы узнаете:** как Rust заменяет нулевые указатели на `Option`, а исключения — на `Result`, и как оператор `?` делает распространение ошибок кратким. Это самый характерный паттерн 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 { - Some(T), // Contains a value - None, // No value + Some(T), // Содержит значение + None, // Значения нет } -// And Result: +// А вот Result: enum Result { - 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` is the replacement, and the compiler forces you to handle the `None` case +- Это значит, что всё, что вы знаете о сопоставлении с образцом через `match`, напрямую работает с `Option` и `Result` +- В Rust **нет нулевых указателей** — их замена это `Option`, и компилятор заставляет вас обработать случай `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` | `Option` | Exhaustive match required — can't forget None | -| `noexcept` annotation | Default — all Rust functions are "noexcept" | Exceptions don't exist | -| `errno` / return codes | `Result` | Type-safe, can't ignore | - -# Rust Option type -- The Rust ```Option``` type is an ```enum``` with only two variants: ```Some``` and ```None``` - - The idea is that this represents a ```nullable``` type, i.e., it either contains a valid value of that type (```Some```), 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` | `Option` | Требуется исчерпывающее сопоставление — нельзя забыть про None | +| Аннотация `noexcept` | По умолчанию — все функции Rust «noexcept» | Исключений не существует | +| `errno` / коды возврата | `Result` | Типобезопасно, игнорировать нельзя | + +# Тип Option в Rust +- Тип ```Option``` в Rust — это ```enum``` всего с двумя вариантами: ```Some``` и ```None``` + - Идея в том, что он представляет ```nullable``` тип, то есть либо содержит корректное значение этого типа (```Some```), либо не содержит значения (```None```) + - Тип ```Option``` используется в API там, где результат операции либо успешен и возвращает корректное значение, либо завершается неудачей (причина ошибки при этом неважна). Например, разбор строки в целое число ```rust fn main() { - // Returns Option + // Возвращает Option 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``` 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``` +# Работа с Option в Rust +- ```Option``` в Rust можно обрабатывать разными способами + - ```unwrap()``` вызывает panic, если ```Option``` равен ```None```, и возвращает ```T``` в противном случае; это наименее предпочтительный способ + - ```or()``` позволяет вернуть альтернативное значение + - ```if let``` позволяет проверить наличие ```Some``` -> **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 + // Это возвращает Option 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``` or ```Err``` - - ```Result``` is used extensively in Rust APIs that can fail. The idea is that on success, functions will return a ```Ok```, or they will return a specific error ```Err``` +# Тип Result в Rust +- Result — это тип ```enum``` похожий на ```Option```, с двумя вариантами: ```Ok``` или ```Err``` + - ```Result``` широко используется в API Rust, которые могут завершиться неудачей. Идея в том, что при успехе функции вернут ```Ok```, а при ошибке — конкретную ошибку ```Err``` ```rust use std::num::ParseIntError; fn main() { @@ -83,21 +83,21 @@ fn main() { if let Ok(a) = "1234".parse::() { 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` is essentially `Result` (a result where the error carries no information): +`Option` и `Result` тесно связаны — `Option` по сути является `Result` (результатом, в котором ошибка не несёт никакой информации): -| `Option` | `Result` | Meaning | +| `Option` | `Result` | Значение | |-------------|---------------|---------| -| `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 = opt.ok_or("value was None"); // Option → Result let res: Result = Ok(42); - let opt: Option = res.ok(); // Result → Option (discards error) + let opt: Option = 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``` с ```()``` и для успеха, и для ошибки (в этом случае ошибка никогда не возникнет) -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
---- -# 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```, 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``` и т. д. + - Явные ```panic``` допустимы для условий, которые по замыслу невозможны. Макросы ```panic!``` или ```assert!``` можно использовать для проверок здравого смысла ```rust fn main() { let x : Option = 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 + // println!("{}", a[2]); // Panic из-за выхода за границы; используйте a.get(2), который вернёт Option } ``` -## 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 #include @@ -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
or return code"] - CIGNORE["[ERROR] Exception not caught
or return code ignored"] - CCHECK["try/catch or check"] - CERROR["Hidden control flow
throws not in signature"] - CERRNO["No compile-time
enforcement"] + subgraph "Проблемы обработки ошибок в C++" + CF["Вызов функции"] + CR["throw исключение
или код возврата"] + CIGNORE["[ОШИБКА] Исключение не перехвачено
или код возврата проигнорирован"] + CCHECK["try/catch или проверка"] + CERROR["Скрытый поток управления
throw не отражён в сигнатуре"] + CERRNO["Нет принудительной проверки
на этапе компиляции"] CF --> CR CR --> CIGNORE @@ -212,16 +212,16 @@ graph TD CCHECK --> CERROR CERROR --> CERRNO - CPROBLEMS["[ERROR] Exceptions invisible in types
[ERROR] Hidden control flow
[ERROR] Easy to forget try/catch
[ERROR] Exception safety is hard
[ERROR] noexcept is opt-in"] + CPROBLEMS["[ОШИБКА] Исключения не видны в типах
[ОШИБКА] Скрытый поток управления
[ОШИБКА] Легко забыть try/catch
[ОШИБКА] Безопасность исключений трудна
[ОШИБКА] noexcept — опционален"] end - subgraph "Rust Result System" - RF["Function Call"] + subgraph "Система Result в Rust" + RF["Вызов функции"] RR["Result
Ok(value) | Err(error)"] - RMUST["[OK] Must handle
Compile error if ignored"] - RMATCH["Pattern matching
match, if let, ?"] - RDETAIL["Detailed error info
Custom error types"] - RSAFE["Type-safe
No global state"] + RMUST["[OK] Нужно обработать
Ошибка компиляции, если проигнорировано"] + RMATCH["Сопоставление с образцом
match, if let, ?"] + RDETAIL["Подробная информация об ошибке
Пользовательские типы ошибок"] + RSAFE["Типобезопасно
Нет глобального состояния"] RF --> RR RR --> RMUST @@ -229,7 +229,7 @@ graph TD RMATCH --> RDETAIL RDETAIL --> RSAFE - RBENEFITS["[OK] Forced error handling
[OK] Type-safe errors
[OK] Detailed error info
[OK] Composable with ?
[OK] Zero runtime cost"] + RBENEFITS["[OK] Принудительная обработка ошибок
[OK] Типобезопасные ошибки
[OK] Подробная информация об ошибке
[OK] Компонуется с помощью ?
[OK] Нулевая стоимость во время выполнения"] end style CPROBLEMS fill:#ff6b6b,color:#000 @@ -238,37 +238,37 @@ graph TD style RMUST fill:#91e5a3,color:#000 ``` -### `Result` Visualization +### Визуализация `Result` ```rust -// Rust error handling - comprehensive and forced +// Обработка ошибок в Rust — полная и обязательная use std::fs::File; use std::io::Read; fn read_file_content(filename: &str) -> Result { - 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 Flow" - START["Function starts"] + subgraph "Поток Result" + 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
propagates error"] + ERROR1 --> PROPAGATE["Оператор ?
распространяет ошибку"] ERROR2 --> PROPAGATE - PROPAGATE --> CALLER["Caller must
handle error"] + PROPAGATE --> CALLER["Вызывающий код должен
обработать ошибку"] end - subgraph "Pattern Matching Options" + subgraph "Варианты сопоставления с образцом" MATCH["match result"] IFLET["if let Ok(val) = result"] - UNWRAP["result.unwrap()
[WARNING] Panics on error"] - EXPECT["result.expect(msg)
[WARNING] Panics with message"] - UNWRAP_OR["result.unwrap_or(default)
[OK] Safe fallback"] - QUESTION["result?
[OK] Early return"] + UNWRAP["result.unwrap()
[ВНИМАНИЕ] Panic при ошибке"] + EXPECT["result.expect(msg)
[ВНИМАНИЕ] Panic с сообщением"] + UNWRAP_OR["result.unwrap_or(default)
[OK] Безопасный запасной вариант"] + QUESTION["result?
[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``` enum for recoverable error handling - - The ```Ok``` variant contains the result in case of success and ```Err``` contains the error +# Обработка ошибок в Rust +- Rust использует перечисление ```enum Result``` для обработки устранимых ошибок + - Вариант ```Ok``` содержит результат в случае успеха, а ```Err``` содержит ошибку ```rust fn main() { let x = "1234x".parse::(); @@ -324,7 +324,7 @@ fn main() { Err(e) => println!("Parsing error {e:?}"), } let x = "1234".parse::(); - // 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``` to enable use of ```?``` - - The type for ```Result``` 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```, чтобы можно было использовать ```?``` + - Тип ```Result``` можно менять. В примере ниже мы возвращаем тот же тип ошибки (```std::num::ParseIntError```), который возвращает ```str::parse()``` ```rust fn double_string_number(s : &str) -> Result { - let x = s.parse::()?; // Returns immediately in case of an error + let x = s.parse::()?; // Немедленно возвращает результат в случае ошибки 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 { - let x = s.parse::().map_err(|_|())?; // Returns immediately in case of an error + let x = s.parse::().map_err(|_|())?; // Немедленно возвращает результат в случае ошибки Ok(x*2) } ``` ```rust fn double_string_number(s : &str) -> Result { - let x = s.parse::().unwrap_or_default(); // Defaults to 0 in case of parse error + let x = s.parse::().unwrap_or_default(); // По умолчанию 0 в случае ошибки разбора Ok(x*2) } ``` ```rust fn double_optional_number(x : Option) -> Result { - // ok_or converts Option to Result in the below - x.ok_or(()).map(|x|x*2) // .map() is applied only on Ok(u32) + // ok_or преобразует Option в Result в этом примере + 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() { } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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 ```
- 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.: ``````. 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```, который встречается в коде ```rust -// Returns a tuple of type composed of left and right of type +// Возвращает кортеж типа , составленный из left и right типа fn pick(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 `````` (example: ```f32``` vs. ```u32```) +# Обобщения в Rust: специализация +- Обобщения можно применять и к типам данных, и к ассоциированным методам. Реализацию можно специализировать для конкретного `````` (например, ```f32``` и ```u32```) ```rust -#[derive(Debug)] // We will discuss this later +#[derive(Debug)] // Мы обсудим это позже struct Point { 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```) -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
-### 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 { shape: S, } -// Generic Shape implementation for any type that implements PrintDescription +// Обобщённая реализация Shape для любого типа, который реализует PrintDescription impl Shape { 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``` для их вызова -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust trait CipherText { @@ -199,31 +199,31 @@ fn main() { c1.encrypt(); c2.encrypt(); } -// Output: +// Вывод: // CipherOne encryption applied // CipherTwo encryption applied ```
-### 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``` -- The ```PhantomData``` 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```, we can only get back a ```Drone``` and viceversa +### Паттерн состояний на обобщениях +- Обобщения позволяют обеспечить конечный автомат на этапе *компиляции*. Для этого нужен специальный обобщённый тип ```PhantomData``` +- ```PhantomData``` — это маркерный тип данных ```нулевого размера```. В данном случае мы используем его для представления состояний ```Idle``` и ```Flying```, но во время выполнения он занимает ```ноль``` байт +- Обратите внимание, что методы ```takeoff``` и ```land``` принимают ```self``` как параметр. Это называется ```поглощением``` (в отличие от ```&self```, которое использует заимствование). По сути, после вызова ```takeoff()``` для ```Drone``` мы можем получить только ```Drone``` и наоборот ```rust struct Drone {x: u64, y: u64, z: u64, state: PhantomData } impl Drone { @@ -233,19 +233,19 @@ impl Drone { fn land(self) -> Drone { ...} } ``` - - [▶ 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``` (zero-size) - - Implementing the methods for a particular stage of the state machine is now just a matter of ```impl State``` - - 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``` (нулевого размера) + - Реализация методов для конкретного этапа конечного автомата — это просто ```impl State``` + - Для перехода из одного состояния в другое используйте метод, который поглощает ```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(animal: &T) { // Static polymorphism - animal.speak(); // Direct function call (zero cost) +fn make_sound(animal: &T) { // Статический полиморфизм + animal.speak(); // Прямой вызов функции (без накладных расходов) } ``` ```mermaid graph TD - subgraph "C++ Object-Oriented Hierarchy" - CPP_ANIMAL["Animal
(Abstract base class)"] - CPP_CAT["Cat : public Animal
(IS-A relationship)"] - CPP_DOG["Dog : public Animal
(IS-A relationship)"] + subgraph "Объектно-ориентированная иерархия C++" + CPP_ANIMAL["Animal
(абстрактный базовый класс)"] + CPP_CAT["Cat : public Animal
(отношение IS-A)"] + CPP_DOG["Dog : public Animal
(отношение IS-A)"] CPP_ANIMAL --> CPP_CAT CPP_ANIMAL --> CPP_DOG - CPP_VTABLE["Virtual function table
(Runtime dispatch)"] - CPP_HEAP["Often requires
heap allocation"] - CPP_ISSUES["[ERROR] Deep inheritance trees
[ERROR] Diamond problem
[ERROR] Runtime overhead
[ERROR] Tight coupling"] + CPP_VTABLE["Таблица виртуальных функций
(диспетчеризация во время выполнения)"] + CPP_HEAP["Часто требует
выделения в куче"] + CPP_ISSUES["[ОШИБКА] Глубокие деревья наследования
[ОШИБКА] Проблема ромба
[ОШИБКА] Накладные расходы во время выполнения
[ОШИБКА] Жёсткая связанность"] end - subgraph "Rust Trait-Based Composition" - RUST_TRAIT["trait Animal
(Behavior definition)"] - RUST_CAT["struct Cat
(Data only)"] - RUST_DOG["struct Dog
(Data only)"] + subgraph "Композиция на трейтах в Rust" + RUST_TRAIT["trait Animal
(определение поведения)"] + RUST_CAT["struct Cat
(только данные)"] + RUST_DOG["struct Dog
(только данные)"] - RUST_CAT -.->|"impl Animal for Cat
(CAN-DO behavior)"| RUST_TRAIT - RUST_DOG -.->|"impl Animal for Dog
(CAN-DO behavior)"| RUST_TRAIT + RUST_CAT -.->|"impl Animal for Cat
(УМЕЕТ поведение)"| RUST_TRAIT + RUST_DOG -.->|"impl Animal for Dog
(УМЕЕТ поведение)"| RUST_TRAIT - RUST_STATIC["Static dispatch
(Compile-time)"] - RUST_STACK["Stack allocation
possible"] - RUST_BENEFITS["[OK] No inheritance hierarchy
[OK] Multiple trait impls
[OK] Zero runtime cost
[OK] Loose coupling"] + RUST_STATIC["Статическая диспетчеризация
(на этапе компиляции)"] + RUST_STACK["Возможно размещение
на стеке"] + RUST_BENEFITS["[OK] Нет иерархии наследования
[OK] Несколько реализаций трейтов
[OK] Нет накладных расходов во время выполнения
[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 // 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(a: T, b: T) -> T where T: Display + Add + 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(data: T)
[ERROR] T can be anything"] - SINGLE_BOUND["fn process(data: T)
[OK] T must implement Display"] - MULTI_BOUND["fn process(data: T)
where T: Display + Clone + Debug
[OK] Multiple requirements"] + subgraph "Эволюция обобщённых ограничений" + UNCONSTRAINED["fn process(data: T)
[ОШИБКА] T может быть чем угодно"] + SINGLE_BOUND["fn process(data: T)
[OK] T должен реализовывать Display"] + MULTI_BOUND["fn process(data: T)
where T: Display + Clone + Debug
[OK] Несколько требований"] UNCONSTRAINED --> SINGLE_BOUND SINGLE_BOUND --> MULTI_BOUND end - subgraph "Trait Bound Syntax" + subgraph "Синтаксис ограничений трейтов" INLINE["fn func(param: T)"] WHERE_CLAUSE["fn func(param: T)
where T: Trait"] IMPL_PARAM["fn func(param: impl Trait)"] - COMPARISON["Inline: Simple cases
Where: Complex bounds
impl: Concise syntax"] + COMPARISON["Inline: простые случаи
Where: сложные ограничения
impl: краткий синтаксис"] end - subgraph "Compile-time Magic" - GENERIC_FUNC["Generic function
with trait bounds"] - TYPE_CHECK["Compiler verifies
trait implementations"] - MONOMORPH["Monomorphization
(Create specialized versions)"] - OPTIMIZED["Fully optimized
machine code"] + subgraph "Магия времени компиляции" + GENERIC_FUNC["Обобщённая функция
с ограничениями трейтов"] + TYPE_CHECK["Компилятор проверяет
реализации трейтов"] + MONOMORPH["Мономорфизация
(создание специализированных версий)"] + OPTIMIZED["Полностью оптимизированный
машинный код"] GENERIC_FUNC --> TYPE_CHECK TYPE_CHECK --> MONOMORPH MONOMORPH --> OPTIMIZED - EXAMPLE["add_and_print::
add_and_print::
(Separate functions generated)"] + EXAMPLE["add_and_print::
add_and_print::
(генерируются отдельные функции)"] 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 ::add(a, b) +let c = a + b; // вызывает ::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 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 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 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` for your own type, or `Add` for a foreign type — never `Add` 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 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` только для собственного типа или `Add` для чужого типа — но никогда `Add` для `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``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
---- -# 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>` | 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>` | Когда нужны смешанные типы в коллекции или расширяемость в стиле плагинов | +| `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` and `Into` for infallible conversions, `TryFrom` and `TryInto` for fallible ones. Implement `From` and get `Into` for free. Replaces C++ conversion operators and constructors. +> **Что вы узнаете:** трейты преобразования типов в Rust — `From` и `Into` для преобразований, которые не могут завершиться ошибкой, и `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 for U``` exists, Rust also provides ```Into for T```, so ```let s: String = "Rust".into();``` works too. +- ```From``` и ```Into``` — взаимодополняющие трейты для преобразования типов +- Обычно типы реализуют трейт ```From```. ```String::from("Rust")``` преобразует ```&str``` в ```String```. +Когда существует ```From for U```, Rust также предоставляет ```Into 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``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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 ```
-# 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 = 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::::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` | -| `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` | `Option` | -| `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` | - -### `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` | +| `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` | `Option` | +| `any(f)` / `all(f)` | `bool found = false; for (...) ...` | Булев поиск с коротким замыканием | `bool` | +| `position(f)` | `for (i=0; ...) if (pred) return i;` | Индекс первого совпадения | `Option` | + +### `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 = lines.iter() .flat_map(|line| line.split(':')) .filter_map(|token| token.parse::().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 = temps.windows(3) .map(|w| w.iter().sum::() 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 = 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, сгруппировать по имени, посчитать средние } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
-# 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 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 i32>>`, содержащий замыкания, которые прибавляют 1, умножают на 2 и возводят входное значение в квадрат. Переберите вектор и примените каждое замыкание к числу 5 -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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 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() {
-# 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()```)? -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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, Vec) = 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() {
-> **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 -`` and C++20 ranges, but Rust's iterator chains are more composable -and more commonly used. +Следующие адаптеры итераторов *широко* используются в продакшн-коде на Rust. В C++ есть `` и диапазоны 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 = 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 = readings.iter() .scan(None::, |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` +Дано: данные датчиков в виде `Vec<(String, f64)>` (имя, температура). Напишите **одну +цепочку итераторов**, которая: +1. Отфильтровывает датчики с температурой > 80.0 +2. Сортирует их по температуре (по убыванию) +3. Форматирует каждый как `"{name}: {temp}°C [ALARM]"` +4. Собирает результат в `Vec` -Hint: you'll need `.collect()` before `.sort_by()`, since sorting requires a `Vec`. +Подсказка: нужно вызвать `.collect()` перед `.sort_by()`, поскольку для сортировки нужен `Vec`. -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust fn alarm_report(sensors: &[(String, f64)]) -> Vec { @@ -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`, `Arc`, 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`, `Arc`, каналы и то, как компилятор предотвращает гонки данных на этапе компиляции. Никаких накладных расходов на потокобезопасность, которой вы не пользуетесь. -- 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` 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` в 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``` 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``` можно использовать, чтобы разделить *доступные только для чтения* ссылки между несколькими потоками + - ```Arc``` расшифровывается как Atomic Reference Counted (атомарный подсчёт ссылок). Ссылка не освобождается, пока счётчик не достигнет 0 + - ```Arc::clone()``` просто увеличивает счётчик ссылок, не копируя данные ```rust use std::sync::Arc; use std::thread; @@ -80,10 +80,10 @@ fn main() { } ``` -# Rust concurrency -- ```Arc``` can be combined with ```Mutex``` 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``` можно комбинировать с ```Mutex```, чтобы получить изменяемые ссылки + - ```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` 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` допускает **несколько одновременных читателей** или **одного эксклюзивного писателя** — шаблон блокировки для чтения/записи из 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` — same memory ordering model (`Relaxed`, `Acquire`, `Release`, `SeqCst`) +# Конкурентность в Rust: атомарные типы +- Для простых счётчиков и флагов типы `std::sync::atomic` позволяют избежать накладных расходов `Mutex` + - `AtomicBool`, `AtomicI32`, `AtomicU64`, `AtomicUsize` и т. д. + - Аналог `std::atomic` из 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` | General mutable shared state | `std::mutex` + manual data association | -| `RwLock` | Read-heavy workloads | `std::shared_mutex` | -| `Atomic*` | Simple counters, flags, lock-free patterns | `std::atomic` | -| `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` | Общее изменяемое состояние общего назначения | `std::mutex` + ручная привязка данных | +| `RwLock` | Нагрузка с преобладанием чтения | `std::shared_mutex` | +| `Atomic*` | Простые счётчики, флаги, lock-free-шаблоны | `std::atomic` | +| `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` is **neither** Send nor Sync (use `Arc` for threads) - - `Cell` and `RefCell` are **not** Sync (use `Mutex` or `RwLock`) - - Raw pointers (`*const T`, `*mut T`) are **neither** Send nor Sync -- This is why the compiler stops you from using `Rc` across threads -- it literally doesn't implement `Send` -- `Arc>` is the thread-safe equivalent of `Rc>` +- Rust использует два маркерных трейта, чтобы обеспечивать потокобезопасность на этапе компиляции: + - `Send`: тип является `Send`, если его можно безопасно **передать** в другой поток + - `Sync`: тип является `Sync`, если его можно безопасно **разделять** (через `&T`) между потоками +- Большинство типов автоматически являются `Send + Sync`. Заметные исключения: + - `Rc` **не** является ни Send, ни Sync (для потоков используйте `Arc`) + - `Cell` и `RefCell` **не** являются Sync (используйте `Mutex` или `RwLock`) + - Сырые указатели (`*const T`, `*mut T`) **не** являются ни Send, ни Sync +- Именно поэтому компилятор не даёт использовать `Rc` между потоками — он просто не реализует `Send` +- `Arc>` — потокобезопасный аналог `Rc>` -> **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` 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` хрупкий (неатомарный) счётчик ссылок; передача или совместное использование повредили бы счётчик, поэтому он не является ни `Send`, ни `Sync`. -# Exercise: Multi-threaded word count +# Упражнение: подсчёт слов в многопоточном режиме -🔴 **Challenge** — combines threads, Arc, Mutex, and HashMap +🔴 **Сложный уровень** — объединяет потоки, Arc, Mutex и HashMap -- Given a `Vec` of text lines, spawn one thread per line to count the words in that line -- Use `Arc>>` to collect results -- Print the total word count across all lines -- **Bonus**: Try implementing this with channels (`mpsc`) instead of shared state +- Дан `Vec` со строками текста. Запустите по одному потоку на каждую строку, чтобы подсчитать слова в ней +- Используйте `Arc>>`, чтобы собрать результаты +- Выведите общее количество слов по всем строкам +- **Бонус**: попробуйте реализовать то же самое с каналами (`mpsc`) вместо разделяемого состояния -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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() {
- 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 #include @@ -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 { 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, which is dropped when it goes out of scope + // Это создаёт Box, который уничтожается при выходе из области видимости 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`, 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`, вызывает небезопасную функцию и возвращает `String` +- **Шаг 3**: добавьте корректные комментарии `// Safety:` к каждому блоку unsafe -**Starter code:** +**Стартовый код:** ```rust use std::fmt::Write as _; -/// Simulates a C function: writes "Hello, !" into buffer. -/// Returns the number of bytes written (excluding null terminator). +/// Имитирует функцию на C: записывает "Hello, !" в буфер. +/// Возвращает количество записанных байтов (без нулевого терминатора). /// # 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 { - // TODO: Allocate a Vec 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, создайте 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! } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust use std::ffi::CStr; -/// Simulates a C function: writes "Hello, !" into buffer. -/// Returns the number of bytes written, or -1 if buffer too small. +/// Имитирует функцию на C: записывает "Hello, !" в буфер. +/// Возвращает количество записанных байтов или -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 { 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 = 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() { ---- - 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..032d0c6 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> = 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 { @@ -286,26 +286,26 @@ impl Tmp102 { } } -// 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
impl<I2C: I2c>"] - RD --> EHAL["embedded-hal::I2c trait"] + subgraph "Архитектура embedded-hal в Rust" + RD["Драйвер датчика температуры
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,
runs on ALL chips"] + NOTE["Пишем драйвер ОДИН раз,
работает на ВСЕХ чипах"] 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 = 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 { if data.len() < 8 { return Err("packet too short"); @@ -429,19 +429,19 @@ pub fn parse_packet(data: &[u8]) -> Result { } ``` -## 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` 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` +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 { num_leds: u8, } -// TODO: Implement new(), set_brightness(), all_off() -// TODO: Create MockSpi for testing +// TODO: Реализуйте new(), set_brightness(), all_off() +// TODO: Создайте MockSpi для тестирования ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust #![no_std] @@ -472,7 +472,7 @@ impl LedController { 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 LedController { mod tests { use super::*; - // Mock SPI that records all transactions + // Мок SPI, который записывает все транзакции struct MockSpi { transactions: Vec>, } - // 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 {
-## 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
target remote :3333"] - C4 --> C5["printf via semihosting
(~100ms per call, halts CPU)"] + C4 --> C5["printf через semihosting
(~100 мс на вызов, останавливает CPU)"] end - subgraph "Rust Workflow (probe-rs)" - R1["Write code"] --> R2["cargo embed"] - R2 --> R3["Flash + RTT console
in one command"] - R3 --> R4["defmt logs stream
in real-time (~1μs)"] - R2 -.->|"Or"| R5["VS Code F5
Full GUI debugger"] + subgraph "Рабочий процесс Rust (probe-rs)" + R1["Пишем код"] --> R2["cargo embed"] + R2 --> R3["Прошивка + консоль RTT
одной командой"] + R3 --> R4["Поток логов defmt
в реальном времени (~1 мкс)"] + R2 -.->|"Или"| R5["VS Code F5
Полноценный графический отладчик"] 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 | +| `printf("debug: %d\n", val)` (семихостинг) | `defmt::info!("debug: {}", val)` (RTT) | +| Графический отладчик 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` — returns oldest element +**Требования:** +- Обобщённый по типу элемента `T: Copy` +- Фиксированная ёмкость `N` (константный обобщённый параметр) +- `push(&mut self, item: T)` — перезаписывает самый старый элемент, когда буфер заполнен +- `pop(&mut self) -> Option` — возвращает самый старый элемент - `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 { buf: [MaybeUninit; N], - head: usize, // next write position - tail: usize, // next read position + head: usize, // позиция следующей записи + tail: usize, // позиция следующего чтения count: usize, } @@ -198,7 +194,7 @@ impl RingBuffer { ```
-Solution +Решение ```rust #![no_std] @@ -215,7 +211,7 @@ pub struct RingBuffer { impl RingBuffer { 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 RingBuffer { 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 RingBuffer { 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`
- 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>` 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>` повсюду | Трейт-объекты **только** там, где нужно (~25 мест) | По умолчанию — статическая диспетчеризация | -### Before and After Metrics +### Метрики до и после -| **Metric** | **C++ (Original)** | **Rust (Rewrite)** | +| **Метрика** | **C++ (исходный)** | **Rust (переписанный)** | |------------|---------------------|------------------------| -| `dynamic_cast` / type downcasts | ~400 | 0 | -| `virtual` / `override` methods | ~900 | ~25 (`Box`) | -| 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`) | +| Сырые выделения `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>& events, DiagFramework* fw) { for (auto& event : events) { if (auto* degrade = dynamic_cast(event.get())) { - // handle degrade... + // обработка degrade... } else if (auto* fatal = dynamic_cast(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, // Concrete type, not Box + degrade_events: Vec, // Конкретный тип, а не Box fatal_events: Vec, uncorr_events: Vec, boot_events: Vec, baseboard_events: Vec, ecc_events: Vec, - // ... 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>`? -- **The Wrong Approach** (literal translation): Put all events in one heterogeneous collection, then downcast — this is what C++ does with `vector>` -- **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>`? +- **Неверный подход** (буквальный перевод): сложить все события в одну разнородную коллекцию, а затем приводить типы — именно так C++ делает с `vector>` +- **Верный подход**: отдельные типизированные 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 { public: std::shared_ptr m_upstream; std::vector> m_downstream; - // ... device data + // ... данные устройства void AddChild(std::shared_ptr 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, // Index into the arena Vec - pub downstream_idxs: Vec, // Indices into the arena Vec + // Связи дерева через индексы — без подсчёта ссылок и без циклов + pub upstream_idx: Option, // Индекс в Vec-арене + pub downstream_idxs: Vec, // Индексы в Vec-арене } -// The "arena" is simply a Vec owned by the tree: +// «Арена» — это просто Vec, которым владеет дерево: pub struct DeviceTree { - devices: Vec, // Flat ownership — one Vec owns everything + devices: Vec, // Плоское владение — один 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"] -->|"shared_ptr"| B1["shared_ptr"] - B1 -->|"shared_ptr (parent)"| A1 + B1 -->|"shared_ptr (родитель)"| A1 A1 -->|"shared_ptr"| C1["shared_ptr"] - 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"] - V --> D0["[0] Root
upstream: None
down: [1,2]"] - V --> D1["[1] Child
upstream: Some(0)
down: []"] - V --> D2["[2] Child
upstream: Some(0)
down: []"] + V --> D0["[0] Корень
upstream: None
down: [1,2]"] + V --> D1["[1] Потомок
upstream: Some(0)
down: []"] + V --> D2["[2] Потомок
upstream: Some(0)
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, } -/// 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 m_alertTriggers; std::vector m_warnTriggers; bool m_healthMonHasBootTimeError; uint32_t m_healthMonActionCounter; - // GPU diagnostics + // Диагностика GPU std::map m_gpuPcieMap; bool m_isRecoveryContext; bool m_healthcheckDetectedDevices; - // ... 30+ more GPU-related fields + // ... ещё 30+ полей, связанных с GPU - // PCIe tree + // Дерево PCIe std::shared_ptr 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, 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, 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, // 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, // 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> is correct here +// Пример: framework.rs — Vec> здесь уместен pub struct DiagFramework { - modules: Vec>, // Runtime polymorphism + modules: Vec>, // Полиморфизм времени выполнения pre_diag_modules: Vec>, event_log_mgr: EventLogManager, // ... } impl DiagFramework { - /// Register a diagnostic module — any type implementing DiagModule + /// Регистрация диагностического модуля — любой тип, реализующий DiagModule pub fn register_module(&mut self, module: Box) { 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` | New modules added without modifying framework | -| Test mocking | `Box` | 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` | Новые модули добавляются без изменения фреймворка | +| Мокирование в тестах | `Box` | Внедрение тестовых двойников | + +### Упражнение: подумайте, прежде чем переводить +Дан следующий код на 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> shapes; ``` -**Question**: Should the Rust translation use `enum Shape` or `Vec>`? +**Вопрос**: должен ли перевод на Rust использовать `enum Shape` или `Vec>`? -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) -**Answer**: `enum Shape` — because the set of shapes is **closed** (known at compile time). You'd only use `Box` if users could add new shape types at runtime. +**Ответ**: `enum Shape` — потому что набор фигур **закрыт** (известен на этапе компиляции). `Box` понадобился бы, только если бы пользователи могли добавлять новые типы фигур во время выполнения. ```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` 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` (системы плагинов, моки для тестов). Остальные примерно 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>` everywhere. Ask: "Is this set of variants closed?" → If yes, use enum +## Самые трудные места +- **Аннотации времён жизни**: правильно расставить заимствования непросто, если вы привыкли к сырым указателям, — но когда код компилируется, он корректен +- **Борьба с проверкой заимствований**: желание иметь `&mut self` в двух местах одновременно. Решение: разбить состояние на отдельные структуры +- **Противостояние буквальному переводу**: соблазн писать `Vec>` повсюду. Спросите себя: «Закрыт ли этот набор вариантов?» → Если да, используйте 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` (heap-allocated on first use). Rust's `&'static [&'static str]` lives in read-only memory — zero runtime cost. +В C++ это обычно был бы `std::vector` (выделяется в куче при первом использовании). Тип `&'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` | +| `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` | -### 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` +### Чек-лист: нужно ли клонировать? +1. **Могу ли я принять `&str` / `&T` вместо `String` / `T`?** → Заимствуйте, не клонируйте +2. **Могу ли я перестроить код, чтобы не требовалось два владельца?** → Передавайте по ссылке или используйте области видимости +3. **Это `Arc::clone()`?** → Это нормально, это O(1) +4. **Я перемещаю данные в поток или замыкание?** → Клонирование необходимо +5. **Я клонирую в горячем цикле?** → Профилируйте и рассмотрите заимствование или `Cow` ---- -## `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 implements Deref, so you can use it as &str: - println!("Hello, {g1}!"); // Works — Cow auto-derefs to &str + // Cow реализует Deref, поэтому его можно использовать как &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` is like a function that returns `std::variant` -> — except with automatic deref and no boilerplate to access the value. +> **Сравнение с C++**: `Cow` похож на функцию, которая возвращает `std::variant`, — только с автоматическим разыменованием и без шаблонного кода для доступа к значению. ---- -## `Weak`: Breaking Reference Cycles — Rust's `weak_ptr` +## `Weak`: разрыв циклов ссылок — `weak_ptr` в Rust -`Weak` is the Rust equivalent of C++ `std::weak_ptr`. It holds a non-owning -reference to an `Rc` or `Arc` value. The value can be deallocated while -`Weak` references still exist — calling `upgrade()` returns `None` if the value is gone. +`Weak` — это аналог `std::weak_ptr` из C++. Он хранит невладеющую ссылку на значение `Rc` или `Arc`. Значение может быть освобождено, пока существуют слабые ссылки `Weak` — вызов `upgrade()` вернёт `None`, если значения уже нет. -### Why `Weak` exists +### Зачем нужен `Weak` -`Rc` and `Arc` 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` и `Arc` образуют циклы ссылок, если два значения указывают друг на друга — ни одно из них никогда не достигнет счётчика 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 — doesn't prevent parent from dropping - children: RefCell>>, // Strong — parent owns children + parent: RefCell>, // Weak — не мешает родителю уничтожиться + children: RefCell>>, // Сильная — родитель владеет потомками } impl Node { @@ -215,9 +209,9 @@ impl Node { } fn add_child(parent: &Rc, child: &Rc) { - // 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 parent; // Weak — no ownership - std::vector> children; // Strong — owns children + std::weak_ptr parent; // Слабая — без владения + std::vector> children; // Сильные — владеет потомками static auto create(const std::string& v) { return std::make_shared(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` | `Rc` (single-thread) / `Arc` (multi-thread) | Same semantics | -| `weak_ptr` | `Weak` from `Rc::downgrade()` / `Arc::downgrade()` | Same semantics | -| `weak_ptr::lock()` → `shared_ptr` or null | `Weak::upgrade()` → `Option>` | `None` if dropped | -| `shared_ptr::use_count()` | `Rc::strong_count()` | Same meaning | +| `shared_ptr` | `Rc` (один поток) / `Arc` (несколько потоков) | Та же семантика | +| `weak_ptr` | `Weak` из `Rc::downgrade()` / `Arc::downgrade()` | Та же семантика | +| `weak_ptr::lock()` → `shared_ptr` или null | `Weak::upgrade()` → `Option>` | `None`, если значение уничтожено | +| `shared_ptr::use_count()` | `Rc::strong_count()` | Тот же смысл | -### When to use `Weak` +### Когда использовать `Weak` -| **Situation** | **Pattern** | +| **Ситуация** | **Паттерн** | |--------------|-----------| -| Parent ↔ child tree relationships | Parent holds `Rc`, child holds `Weak` | -| Observer pattern / event listeners | Event source holds `Weak`, observer holds `Rc` | -| Cache that doesn't prevent deallocation | `HashMap>` — entries go stale naturally | -| Breaking cycles in graph structures | Cross-links use `Weak`, tree edges use `Rc`/`Arc` | +| Связи «родитель ↔ потомок» в дереве | Родитель хранит `Rc`, потомок хранит `Weak` | +| Паттерн «наблюдатель» / слушатели событий | Источник событий хранит `Weak`, наблюдатель хранит `Rc` | +| Кэш, который не мешает освобождению | `HashMap>` — записи естественным образом устаревают | +| Разрыв циклов в графовых структурах | Перекрёстные связи используют `Weak`, рёбра дерева — `Rc`/`Arc` | -> **Prefer the arena pattern** (Case Study 2) over `Rc/Weak` for tree structures in -> new code. `Vec` + 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` с индексами проще, быстрее и не имеет накладных расходов на подсчёт ссылок. Используйте `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 -// HashMap keys must be Eq + Hash — PartialEq alone won't compile +// Используется как: HashMap +// Ключи 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 has NaN ≠ NaN - pub critical_threshold: Option, // ← can't derive Eq + pub warning_threshold: Option, // ← f32: NaN ≠ NaN + pub critical_threshold: Option, // ← нельзя вывести Eq pub sensor_names: Vec, } -// 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 v = {1, 2, 3}; -int x = v[10]; // UB! No bounds check with operator[] +int x = v[10]; // Неопределённое поведение! operator[] не проверяет границы std::map 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 { 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 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(); // throws if missing - if (j.contains("SDRSensorIdHexes")) { // manual default handling + f.logical_id = j.at("LogicalID").get(); // бросает исключение, если нет + if (j.contains("SDRSensorIdHexes")) { // ручная обработка значения по умолчанию f.sensor_ids = j["SDRSensorIdHexes"].get>(); } 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, // Missing → empty Vec + #[serde(rename = "SDRSensorIdHexes", default)] // Ключ JSON → поле Rust + pub sensor_ids: Vec, // Нет в JSON → пустой Vec #[serde(default)] - pub sensor_names: Vec, // Missing → empty Vec + pub sensor_names: Vec, // Нет в 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, // Absorbs unknown keys + pub options: HashMap, // Поглощает неизвестные ключи } -// 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 } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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, - #[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 { let path = self.args.sysinfo.as_ref()?; let content = std::fs::read_to_string(path).ok()?; @@ -64,15 +64,15 @@ let part_number = (|| -> Option { })() .unwrap_or_else(|| "UNKNOWN".to_string()); ``` -The closure creates an `Option` scope where `?` bails early at any step. The `.unwrap_or_else()` provides the fallback once, at the end. +Замыкание создаёт область `Option`, в которой `?` досрочно прерывает выполнение на любом шаге. `.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::string, std::string>> gpu_info; for (const auto& [key, info] : gpu_pcie_map) { std::vector 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)> = self .gpu_pcie_map .values() @@ -102,7 +102,7 @@ let gpu_info: Vec<(Vec, String, String, String)> = self .collect(); ``` -### Pattern 4: `.filter().collect()` replacing loop + `if (condition) continue` +### Паттерн 4: `.filter().collect()` вместо цикла + `if (condition) continue` ```cpp // C++ std::vector 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` that returns formatted report lines for events that pass **all** filters -5. Write a `fn parse_event(line: &str) -> Result` 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`, которая возвращает отформатированные строки отчёта для событий, прошедших **все** фильтры +5. Напишите `fn parse_event(line: &str) -> Result`, которая разбирает строки вида `"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 { - // TODO: Filter events that pass ALL filters, format as + // TODO: Отфильтровать события, прошедшие ВСЕ фильтры, и отформатировать как // "[SEVERITY] source (FC:fault_code): message" todo!() } fn parse_event(line: &str) -> Result { - // 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 = 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() { } ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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, String> { tracing::trace!("Decoding {} bytes", raw_buffer.len()); @@ -138,32 +138,32 @@ fn decode_ipmi_response(raw_buffer: &[u8]) -> Result, 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 +// Для Result 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` for fallible operations**: Let callers decide how to handle errors -- **Use `thiserror` for custom error types**: More ergonomic than manual implementations +- **Возвращайте `Result` из операций, которые могут завершиться ошибкой**: пусть вызывающий код решает, как обрабатывать ошибки +- **Используйте `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` sparingly**: Only when you need shared ownership -- **Limit lifetimes**: Use scopes `{}` to control when values are dropped -- **Avoid `RefCell` in public APIs**: Keep interior mutability internal +## Управление памятью +- **Предпочитайте заимствование клонированию**: используйте `&T` вместо клонирования, когда это возможно +- **Используйте `Rc` умеренно**: только когда нужно совместное владение +- **Ограничивайте времена жизни**: используйте области видимости `{}`, чтобы контролировать, когда значения уничтожаются +- **Избегайте `RefCell` в публичных 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` for large stack objects**: Move them to heap if needed +## Производительность +- **Профилируйте перед оптимизацией**: используйте `cargo bench` и инструменты профилирования +- **Предпочитайте итераторы циклам**: они читаемее и часто быстрее +- **Используйте `&str` вместо `String`**: когда не нужно владение +- **Рассмотрите `Box` для больших объектов на стеке**: при необходимости переносите их в кучу -## 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, // None (default) + debug: bool, // false (по умолчанию) + max_connections: u32, // 0 (по умолчанию) + timeout: Option, // 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 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 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 for MyType { /* convenient conversion */ } -impl TryFrom for MyType { /* fallible conversion */ } +// Ручные реализации, которые стоит рассмотреть: +impl Display for MyType { /* представление для пользователя */ } +impl From for MyType { /* удобное преобразование */ } +impl TryFrom 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` 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`) + +### Итоги: преимущества трейтов + +| Трейт | Преимущество | Когда использовать | +|-------|-----------|-------------| +| `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(3.14); // 1. Numeric / up-cast -Derived* d = dynamic_cast(base); // 2. Runtime downcasting -int* p = const_cast(cp); // 3. Cast away const -auto* raw = reinterpret_cast(&obj); // 4. Bit-level reinterpretation +// Иерархия приведений C++ +int i = static_cast(3.14); // 1. Числовое / приведение вверх +Derived* d = dynamic_cast(base); // 2. Приведение вниз во время выполнения +int* p = const_cast(cp); // 3. Снятие const +auto* raw = reinterpret_cast(&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 = 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 = 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, // Mutate through &self + read_count: Cell, // Изменяем через &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::(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::(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 { 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` | 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` | Нет ошибок двойного вычисления | +| `#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 #include @@ -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, } @@ -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(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 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` | `std::sync::atomic::AtomicT` | Same semantics, same orderings | -| `std::atomic::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` | `std::sync::atomic::AtomicT` | Та же семантика, те же порядки доступа | +| `std::atomic::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 = 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 = 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` | No runtime constructor for simple cases | -| `static MyObj obj;` (complex init) | `static OBJ: LazyLock = LazyLock::new(\|\| { ... });` | Thread-safe, lazy, no init order issues | -| `thread_local` | `thread_local! { static X: Cell = 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` | Для простых случаев нет конструктора времени выполнения | +| `static MyObj obj;` (сложная инициализация) | `static OBJ: LazyLock = LazyLock::new(\|\| { ... });` | Потокобезопасно, ленивая инициализация, нет проблем с порядком | +| `thread_local` | `thread_local! { static X: Cell = 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, int> = 0> T double_it(T val) { return val * 2; } @@ -512,20 +502,20 @@ template, int> = 0> T double_it(T val) { return val * 2.0; } -// C++20 concepts — cleaner but still verbose: +// Концепты C++20 — чище, но всё ещё многословны: template 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 + From>(val: T) -> T { val * T::from(2) } -// Or with where clause for complex bounds: +// Или с предложением where для сложных ограничений: fn process(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` | `where T: Trait` | 🟢 Clear English | -| `std::is_integral_v` | 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)` | 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` | `where T: Trait` | 🟢 Понятно, как обычный английский | +| `std::is_integral_v` | Ограничение числовым трейтом или конкретными типами | 🟢 Без суффиксов `_v` / `_t` | +| Наборы перегрузок SFINAE | Отдельные блоки `impl Trait for ConcreteType` | 🟢 Каждая реализация самостоятельна | +| `if constexpr (std::is_same_v)` | Специализация через реализации трейтов | 🟢 Диспетчеризация на этапе компиляции | +| Концепт C++20 | `trait` | 🟢 Почти одинаковый смысл | +| Клауза `requires` | Предложение `where` | 🟢 Та же позиция, похожий синтаксис | +| Компиляция падает глубоко внутри шаблона | Компиляция падает в месте вызова с несоответствием трейта | 🟢 Без каскадов ошибок на 200 строк | + +> **Ключевая мысль**: концепты C++20 — самое близкое к трейтам Rust. Если вы знакомы с концептами C++20, думайте о трейтах Rust как о концептах, которые являются полноценной возможностью языка с версии 1.0 и имеют согласованную модель реализации (реализации трейтов), а не утиной типизации. --- -### `std::function` → Function Pointers, `impl Fn`, and `Box` +### `std::function` → указатели на функции, `impl Fn` и `Box` -C++ `std::function` is a type-erased callable. Rust has three options, -each with different trade-offs: +`std::function` в C++ — это вызываемый объект со стёртым типом. В Rust есть три варианта +с разными компромиссами: ```cpp -// C++: one-size-fits-all (heap-allocated, type-erased) +// C++: универсальное решение (в куче, со стёртым типом) #include std::function make_adder(int n) { return [n](int x) { return x + n; }; @@ -578,24 +565,24 @@ std::function 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 — type-erased, heap-allocated (like std::function) +// Вариант 3 в Rust: Box — стирание типа, выделение в куче (как std::function) fn make_adder(n: i32) -> Box i32> { Box::new(move |x| x + n) } let adder = make_adder(10); println!("{}", adder(5)); // 15 -// Storing heterogeneous callables (like vector>): +// Хранение разнородных вызываемых объектов (как vector>): let callbacks: Vec 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` | `F: Fn(Args) -> Ret` | -| Stored callable, type-erased | `std::function` | `Box Ret>` | -| Callback that mutates state | `std::function` with mutable lambda | `Box Ret>` | -| One-shot callback (consumed) | `std::function` (moved) | `Box Ret>` | +| Функция верхнего уровня без захватов | Указатель на функцию | `fn(Args) -> Ret` | +| Обобщённая функция, принимающая вызываемые объекты | Параметр шаблона | `impl Fn(Args) -> Ret` (статическая диспетчеризация) | +| Ограничение трейтом в обобщениях | `template` | `F: Fn(Args) -> Ret` | +| Хранимый вызываемый объект со стёртым типом | `std::function` | `Box Ret>` | +| Обратный вызов, изменяющий состояние | `std::function` с изменяемой лямбдой | `Box Ret>` | +| Одноразовый обратный вызов (потребляется) | `std::function` (перемещённый) | `Box Ret>` | -> **Performance note**: `impl Fn` has zero overhead (monomorphized, like a C++ template). -> `Box` has the same overhead as `std::function` (vtable + heap allocation). -> Prefer `impl Fn` unless you need to store heterogeneous callables. +> **Замечание о производительности**: `impl Fn` не имеет накладных расходов (мономорфизация, как шаблон C++). +> `Box` имеет те же накладные расходы, что и `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` | `Vec` | Nearly identical API. Rust checks bounds by default | -| `std::array` | `[T; N]` | Stack-allocated fixed-size array | -| `std::deque` | `std::collections::VecDeque` | Ring buffer. Efficient push/pop at both ends | -| `std::list` | `std::collections::LinkedList` | Rarely used in Rust — `Vec` is almost always faster | -| `std::forward_list` | No equivalent | Use `Vec` or `VecDeque` | -| `std::unordered_map` | `std::collections::HashMap` | Uses `SipHash` by default (DoS-resistant) | -| `std::map` | `std::collections::BTreeMap` | B-tree; keys sorted; `K: Ord` required | -| `std::unordered_set` | `std::collections::HashSet` | `T: Hash + Eq` required | -| `std::set` | `std::collections::BTreeSet` | Sorted set; `T: Ord` required | -| `std::priority_queue` | `std::collections::BinaryHeap` | Max-heap by default (same as C++) | -| `std::stack` | `Vec` with `.push()` / `.pop()` | No separate stack type needed | -| `std::queue` | `VecDeque` 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` (C++20) | `&[T]` / `&mut [T]` | Rust slices have been a first-class type since 1.0 | -| `std::tuple` | `(A, B, C)` | First-class syntax, destructurable | -| `std::pair` | `(A, B)` | Just a 2-element tuple | -| `std::bitset` | 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>` or `BTreeMap>` +| `std::vector` | `Vec` | Почти идентичный API. Rust проверяет границы по умолчанию | +| `std::array` | `[T; N]` | Массив фиксированного размера на стеке | +| `std::deque` | `std::collections::VecDeque` | Кольцевой буфер. Эффективные push/pop с обоих концов | +| `std::list` | `std::collections::LinkedList` | В Rust используется редко — `Vec` почти всегда быстрее | +| `std::forward_list` | Эквивалента нет | Используйте `Vec` или `VecDeque` | +| `std::unordered_map` | `std::collections::HashMap` | По умолчанию использует `SipHash` (устойчив к DoS) | +| `std::map` | `std::collections::BTreeMap` | B-дерево; ключи отсортированы; требуется `K: Ord` | +| `std::unordered_set` | `std::collections::HashSet` | Требуется `T: Hash + Eq` | +| `std::set` | `std::collections::BTreeSet` | Отсортированное множество; требуется `T: Ord` | +| `std::priority_queue` | `std::collections::BinaryHeap` | Max-куча по умолчанию (как в C++) | +| `std::stack` | `Vec` с `.push()` / `.pop()` | Отдельный тип стека не нужен | +| `std::queue` | `VecDeque` с `.push_back()` / `.pop_front()` | Отдельный тип очереди не нужен | +| `std::string` | `String` | Гарантированный UTF-8, без завершающего нуля | +| `std::string_view` | `&str` | Заимствованный срез UTF-8 | +| `std::span` (C++20) | `&[T]` / `&mut [T]` | Срезы Rust — полноценный тип с версии 1.0 | +| `std::tuple` | `(A, B, C)` | Встроенный синтаксис, поддерживает деструктуризацию | +| `std::pair` | `(A, B)` | Просто кортеж из двух элементов | +| `std::bitset` | Эквивалента в 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>` или `BTreeMap>` --- -### 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: 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` (errors are values, not exceptions) | -| RAII cleanup in stack unwinding | `Drop::drop()` runs during panic unwinding | +| `noexcept` | `-> Result` (ошибки — это значения, а не исключения) | +| Очистка 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` 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` вместо исключений, что делает пути ошибок явными и компонуемыми. `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(ptr)` | `match value { Variant::A(data) => ..., }` | Exhaustive, no runtime failure | -| `vector>` | `Vec>` | Only when genuinely polymorphic | -| `shared_ptr` | `Rc` or `Arc` | Prefer `Box` or owned values first | -| `enable_shared_from_this` | Arena pattern (`Vec` + 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` | `Option` | `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` 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(ptr)` | `match value { Variant::A(data) => ..., }` | Исчерпывающе, без ошибок во время выполнения | +| `vector>` | `Vec>` | Только когда действительно нужен полиморфизм | +| `shared_ptr` | `Rc` или `Arc` | Сначала рассмотрите `Box` или владеющие значения | +| `enable_shared_from_this` | Паттерн арены (`Vec` + индексы) | Полностью устраняет циклы ссылок | +| `Base* m_pFramework` в каждом классе | `fn execute(&mut self, ctx: &mut Context)` | Передавайте контекст, не храните указатели | +| `try { } catch (...) { }` | `match result { Ok(v) => ..., Err(e) => ... }` | Или `?` для распространения | +| `std::optional` | `Option` | Требуется `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` экономно**: только для систем плагинов и моков в тестах +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(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(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`, `&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`, `&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!` понадобятся два варианта ветвления. -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```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)`. Это упражнение демонстрирует устройство макросов с несколькими ветвями.
-### 🟡 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); ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust use std::collections::HashMap; @@ -517,18 +517,18 @@ fn main() {
-### 🟡 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 ``` -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust macro_rules! assert_approx_eq { @@ -553,9 +553,9 @@ fn main() {
-### 🔴 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. +**Подсказка:** понадобятся и повторение `$( ... ),*`, и несколько спецификаторов фрагментов. -
Solution (click to expand) +
Решение (нажмите, чтобы развернуть) ```rust use std::fmt; 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/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/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>` | -| 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 `
` 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>` | +| 14 | Unsafe Rust, FFI, тестирование | 1 день | Вы можете вызвать функцию Rust из C# через P/Invoke | +| 15–16 | Миграция, лучшие практики, инструменты | В своём темпе | Справочный материал — обращайтесь к нему при написании реального кода | +| 17 | Итоговый проект | 1–2 дня | У вас есть работающая CLI-утилита, которая получает данные о погоде | + +**Как работать с упражнениями:** +- В главах есть практические упражнения в сворачиваемых блоках `
` с решениями +- **Всегда сначала попробуйте решить упражнение и только потом раскрывайте решение.** Борьба с проверщиком заимствований — часть обучения, а сообщения компилятора — ваш учитель +- Если вы застряли больше чем на 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` vs `List`](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` 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`](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` против `List`](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` для 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`](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/ch01-introduction-and-motivation.md b/csharp-book/src/ch01-introduction-and-motivation.md index 59092d8..774072c 100644 --- a/csharp-book/src/ch01-introduction-and-motivation.md +++ b/csharp-book/src/ch01-introduction-and-motivation.md @@ -1,89 +1,88 @@ -## 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** +- О курсе + - Курс основан на оригинальных материалах Rust Training (лицензии MIT и CC-BY-4.0) + - Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus +- Этот курс задуман как можно более интерактивный + - Предположение: вы знаете 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 data = new List(); 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, } 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(); - // Data races possible, requires careful locking + // Гонки данных возможны, нужна аккуратная синхронизация Parallel.For(0, 1000, i => { - lock (list) // Runtime overhead + lock (list) // Накладные расходы рантайма { list.Add(i); } @@ -93,15 +92,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 { array.first().map(|x| x.to_string()) - // No null references possible - // Bounds checking optimized away when provably safe + // Null-ссылки здесь невозможны + // Проверка границ убирается оптимизатором, когда безопасность доказуема } fn process_concurrently() { @@ -110,7 +109,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 +126,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 +160,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 +169,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 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 +200,7 @@ enum UserDataError { } async fn get_user_data(user_id: i32) -> Result { - // All errors explicit and handled + // Все ошибки явные и обработаны let user = user_repository.get(user_id).await .map_err(UserDataError::DatabaseError)?; @@ -212,19 +211,19 @@ async fn get_user_data(user_id: i32) -> Result { .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 +233,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 +252,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 AllowedOrigins); var config = new ServerConfig("localhost", 8080, new List { "*.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 +294,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 GetHighValueOrders(IEnumerable orders) { return orders - .Where(o => o.Total > 1000) // Func — heap-allocated delegate - .Select(o => new OrderSummary // Anonymous type or extra class + .Where(o => o.Total > 1000) // Func — делегат в куче + .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 { 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 +358,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 +386,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 trades = new List(); 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, } 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
Classes, Methods, Properties"] - CS_COMPILE["C# Compiler
(csc.exe)"] - CS_IL["Intermediate Language
(IL bytecode)"] + subgraph "Модель разработки C#" + CS_CODE["Исходный код C#
Классы, методы, свойства"] + CS_COMPILE["Компилятор C#
(csc.exe)"] + CS_IL["Промежуточный язык
(байт-код IL)"] CS_RUNTIME[".NET Runtime
(CLR)"] - CS_JIT["Just-In-Time Compiler"] - CS_NATIVE["Native Machine Code"] - CS_GC["Garbage Collector
(Memory management)"] + CS_JIT["Just-In-Time компилятор"] + CS_NATIVE["Машинный код"] + CS_GC["Сборщик мусора
(управление памятью)"] CS_CODE --> CS_COMPILE CS_COMPILE --> CS_IL @@ -538,20 +537,20 @@ graph TD CS_JIT --> CS_NATIVE CS_RUNTIME --> CS_GC - CS_BENEFITS["[OK] Fast development
[OK] Rich ecosystem
[OK] Automatic memory management
[ERROR] Runtime overhead
[ERROR] GC pauses
[ERROR] Platform dependency"] + CS_BENEFITS["[OK] Быстрая разработка
[OK] Богатая экосистема
[OK] Автоматическое управление памятью
[ERROR] Накладные расходы рантайма
[ERROR] Паузы GC
[ERROR] Зависимость от платформы"] end - subgraph "Rust Development Model" - RUST_CODE["Rust Source Code
Structs, Enums, Functions"] - RUST_COMPILE["Rust Compiler
(rustc)"] - RUST_NATIVE["Native Machine Code
(Direct compilation)"] - RUST_ZERO["Zero Runtime
(No VM, No GC)"] + subgraph "Модель разработки Rust" + RUST_CODE["Исходный код Rust
Структуры, перечисления, функции"] + RUST_COMPILE["Компилятор Rust
(rustc)"] + RUST_NATIVE["Машинный код
(прямая компиляция)"] + RUST_ZERO["Нулевой рантайм
(нет VM, нет GC)"] RUST_CODE --> RUST_COMPILE RUST_COMPILE --> RUST_NATIVE RUST_NATIVE --> RUST_ZERO - RUST_BENEFITS["[OK] Maximum performance
[OK] Memory safety
[OK] No runtime dependencies
[ERROR] Steeper learning curve
[ERROR] Longer compile times
[ERROR] More explicit code"] + RUST_BENEFITS["[OK] Максимальная производительность
[OK] Безопасность памяти
[OK] Нет зависимостей рантайма
[ERROR] Более крутая кривая обучения
[ERROR] Более долгая сборка
[ERROR] Более явный код"] end style CS_BENEFITS fill:#e3f2fd,color:#000 @@ -562,22 +561,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` | Compile-time null safety | -| Error handling | Exceptions | `Result` | 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` (optional constraints via `where`) | `Vec` (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` | Null-безопасность на этапе компиляции | +| Обработка ошибок | Исключения | `Result` | Явно, без скрытого управления потоком | +| Изменяемость | Изменяемо по умолчанию | Неизменяемо по умолчанию | Мутация по явному выбору | +| Система типов | Ссылочные/значимые типы | Типы владения | Семантика перемещения, заимствование | +| Сборки | GAC, домены приложений (.NET Framework); side-by-side (.NET 5+) | Крейты | Статическая линковка, без рантайма | +| Пространства имён | `using System.IO` | `use std::fs` | Система модулей | +| Интерфейсы | `interface IFoo` | `trait Foo` | Реализации по умолчанию | +| Обобщения | `List` (необязательные ограничения через `where`) | `Vec` (ограничения трейтов вида `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) { - 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) { - 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 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 { 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 = std::result::Result; -// 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 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() 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 = 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 | ` where T : IFoo` | ` 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` | Перечисления — алгебраические типы | +| Обобщения | ` where T : IFoo` | ` 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..b42c140 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,36 +114,36 @@ 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)** +**Решение C# (.sln)** ```text MySolution/ ├── MySolution.sln @@ -155,7 +155,7 @@ MySolution/ └── Tests.csproj ``` -**Rust Workspace (Cargo.toml)** +**Рабочее пространство Rust (Cargo.toml)** ```toml [workspace] members = [ @@ -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::() { - 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 "); + Console.WriteLine("Использование: program "); return; } string filename = args[0]; - Console.WriteLine($"Processing {filename}"); + Console.WriteLine($"Обработка {filename}"); } ``` @@ -236,19 +236,19 @@ use std::env; fn main() { let args: Vec = 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: {} ", args[0]); // eprintln! → stderr + eprintln!("Использование: {} ", 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 — no nulls! +// env::var возвращает Result — никаких 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, - /// 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 Options: - -i, --input Input file to process - -o, --output Output file (defaults to stdout) - -v, --verbose Enable verbose logging - -j, --threads Number of worker threads [default: 4] + -i, --input Входной файл для обработки + -o, --output Выходной файл (по умолчанию — stdout) + -v, --verbose Включить подробное логирование + -j, --threads Количество рабочих потоков [default: 4] -h, --help Print help -V, --version Print version ``` ```csharp -// C# equivalent with System.CommandLine (more boilerplate): -var inputOption = new Option("--input", "Input file") { IsRequired = true }; -var verboseOption = new Option("--verbose", "Enable verbose logging"); -var rootCommand = new RootCommand("A simple file processor"); +// Эквивалент на C# с System.CommandLine (больше шаблонного кода): +var inputOption = new Option("--input", "Входной файл") { IsRequired = true }; +var verboseOption = new Option("--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::()` | Returns `Result` | -| `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::()` | Возвращает `Result` | +| `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 Hobbies); var person = new Person("John", 30, new List { "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 Hobbies); var betterPerson = new BetterPerson("Jane", 25, new List { "painting" }); -// Still mutable via casting: +// Всё равно изменяемо через приведение типов: ((List)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 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>, // Shared, immutable reference + hobbies: Rc>, // Общая, неизменяемая ссылка } -// 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 +## Упражнения
-🏋️ Exercise: Prove the Immutability (click to expand) +🏋️ Упражнение: докажите неизменяемость (нажмите, чтобы раскрыть) -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 AllowedOrigins); var config = new Config("localhost", 8080, new List { "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. Напишите функцию, которая создаёт изменённую копию (с новым хостом) без мутации
-🔑 Solution +🔑 Решение ```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# делают неизменяемой только *ссылку*, но не содержимое.
*** - 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 list = new List(); // Dynamic size +// C# — ссылочные типы, размещаются в куче +int[] numbers = new int[5]; // Фиксированный размер +List list = new List(); // Динамический размер ``` ```rust -// Rust - multiple options -let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Stack array, fixed size -let mut list: Vec = Vec::new(); // Heap vector, dynamic size +// Rust — несколько вариантов +let numbers: [i32; 5] = [1, 2, 3, 4, 5]; // Массив на стеке, фиксированный размер +let mut list: Vec = 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 in C# -let name: &str = "John"; // String literal (immutable, borrowed) +// 1. &str (срез строки) — похож на ReadOnlySpan в 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` | -| Owned, mutable strings | `String` | `StringBuilder` | -| Return owned strings | `String` | `string` | +| Строковые литералы | `&str` | Строковый литерал `string` | +| Параметры функций (только для чтения) | `&str` | `string` или `ReadOnlySpan` | +| Владеемые, изменяемые строки | `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 - a view into string data -// String is like a char[] that you own and can modify +// &str — это как ReadOnlySpan: представление данных строки +// 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::try_from(300_u16); // Err — out of range +let safe: Result = u8::try_from(300_u16); // Err — вне диапазона let ok: Result = u8::try_from(42_u16); // Ok(42) -// String parsing — returns Result, not bool + out param +// Разбор строки — возвращает Result, а не bool + out-параметр let parsed: Result = "42".parse::(); // Ok(42) let bad: Result = "abc".parse::(); // Err(ParseIntError) -// With turbofish syntax: +// С синтаксисом turbofish: let n = "42".parse::().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 = s.parse(); // Ok(42) — safe version +let back: i32 = s.parse().unwrap(); // 42 или panic +let result: Result = 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 = Box::new(Dog); -// "Downcasting" requires the Any trait (rarely needed): +// «Приведение вниз» требует трейта Any (нужно редко): use std::any::Any; if let Some(dog) = animal_any.downcast_ref::() { - // 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::().unwrap()` | Panics on failure | -| `int.TryParse(s, out n)` | `s.parse::()` | Returns `Result` | -| `(Dog)animal` | Not available | Use enums or `Any` | -| `as Dog` / `is Dog` | `downcast_ref::()` | Via `Any` trait; prefer enums | +| `(int)x` | `x as i32` | Усечение/оборачивание при приведении | +| Неявное расширение | Нужно использовать `as` | Неявных числовых преобразований нет | +| `Convert.ToInt32(x)` | `i32::try_from(x)` | Безопасно, возвращает `Result` | +| `int.Parse(s)` | `s.parse::().unwrap()` | Паника при ошибке | +| `int.TryParse(s, out n)` | `s.parse::()` | Возвращает `Result` | +| `(Dog)animal` | Недоступно | Используйте перечисления или `Any` | +| `as Dog` / `is Dog` | `downcast_ref::()` | Через трейт `Any`; предпочтительны перечисления | *** -## Comments and Documentation +## Комментарии и документация -### Regular Comments +### Обычные комментарии ```csharp -// C# comments -// Single line comment -/* Multi-line - comment */ +// Комментарии в C# +// Однострочный комментарий +/* Многострочный + комментарий */ /// -/// XML documentation comment +/// XML-комментарий документации /// -/// The user's name -/// A greeting string +/// Имя пользователя +/// Строка приветствия 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 +## Упражнения
-🏋️ Exercise: Type-Safe Temperature (click to expand) +🏋️ Упражнение: температура с безопасными типами (нажмите, чтобы раскрыть) -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`, а затем преобразует её
-🔑 Solution +🔑 Решение ```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 {
-🏋️ Exercise: Temperature Converter (click to expand) +🏋️ Упражнение: конвертер температур (нажмите, чтобы раскрыть) -**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) ```
-🔑 Solution +🔑 Решение ```rust #[derive(Debug, Clone, Copy)] @@ -312,7 +312,7 @@ fn parse_unit(s: &str) -> Result { "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` replaces exceptions — the caller sees possible failures in the signature -- `match` is an expression that returns a value — no `return` statements needed +**Ключевые выводы**: +- Перечисления заменяют «магические» строки — исчерпывающее сопоставление ловит пропущенные единицы на этапе компиляции +- `Result` заменяет исключения — вызывающий код видит возможные ошибки в сигнатуре +- `match` — это выражение, которое возвращает значение, — инструкции `return` не нужны
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 +## Упражнения
-🏋️ Exercise: Builder with Validation (click to expand) +🏋️ Упражнение: builder с валидацией (нажмите, чтобы раскрыть) -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` — 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` — отвергает пустые `to` или `subject` +4. Напишите тесты, доказывающие, что некорректные входные данные отвергаются
-🔑 Solution +🔑 Решение ```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` vs `List` +## `Vec` против `List` -> **What you'll learn:** `Vec` vs `List`, `HashMap` vs `Dictionary`, safe access patterns -> (why Rust returns `Option` instead of throwing), and the ownership implications of collections. +> **Что вы узнаете:** `Vec` против `List`, `HashMap` против `Dictionary`, безопасные паттерны доступа +> (почему Rust возвращает `Option` вместо генерации исключения), а также последствия владения для коллекций. > -> **Difficulty:** 🟢 Beginner +> **Сложность:** 🟢 Начальный -`Vec` is Rust's equivalent to C#'s `List`, but with ownership semantics. +`Vec` — аналог `List` из C# в Rust, но с семантикой владения. -### C# `List` +### `List` в C# ```csharp -// C# List - Reference type, heap allocated +// List в C# — ссылочный тип, размещается в куче var numbers = new List(); 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 list) { - list.Add(4); // Modifies original list + list.Add(4); // Изменяет исходный список Console.WriteLine($"Count in method: {list.Count}"); } ``` -### Rust `Vec` +### `Vec` в Rust ```rust -// Rust Vec - Owned type, heap allocated +// Vec в 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) { // Takes ownership +fn process_vec(mut vec: Vec) { // Забирает владение vec.push(4); println!("Count in method: {}", vec.len()); - // vec is dropped here + // vec освобождается здесь } -fn process_vec_borrowed(vec: &mut Vec) { // Borrows mutably +fn process_vec_borrowed(vec: &mut Vec) { // Заимствует изменяемо vec.push(4); println!("Count in method: {}", vec.len()); } ``` -### Creating and Initializing Vectors +### Создание и инициализация векторов ```csharp -// C# List initialization +// Инициализация List в C# var numbers = new List { 1, 2, 3, 4, 5 }; var empty = new List(); -var sized = new List(10); // Initial capacity +var sized = new List(10); // Начальная ёмкость -// From other collections +// Из других коллекций var fromArray = new List(new[] { 1, 2, 3 }); ``` ```rust -// Rust Vec initialization -let numbers = vec![1, 2, 3, 4, 5]; // vec! macro -let empty: Vec = 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 = Vec::new(); // Для пустого нужна аннотация типа +let sized = Vec::with_capacity(10); // Предварительное выделение ёмкости -// From iterator +// Из итератора let from_range: Vec = (1..=5).collect(); let from_array = vec![1, 2, 3]; ``` -### Common Operations Comparison +### Сравнение распространённых операций ```csharp -// C# List operations +// Операции с List в C# var list = new List { 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 list, int index) { try @@ -120,27 +120,27 @@ public int SafeAccess(List 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 { - vec.get(index).copied() // Returns Option + vec.get(index).copied() // Возвращает Option } 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`. +HashMap — аналог `Dictionary` из C# в Rust. -### C# Dictionary +### Dictionary в C# ```csharp -// C# Dictionary +// Dictionary в C# var scores = new Dictionary { ["Alice"] = 100, @@ -162,11 +162,11 @@ var scores = new Dictionary ["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 = [ ("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(); -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 -map.clear(); // Clear all +map.insert("key".to_string(), 42); // Вставка/обновление +let exists = map.contains_key("key"); // Проверка наличия +let removed = map.remove("key"); // Удаление, возвращает Option +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 { 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 = numbers.iter().map(|x| x * 2).collect(); let evens: Vec = numbers.iter().filter(|&x| x % 2 == 0).cloned().collect(); -// Or more efficiently, consuming iterator +// Или эффективнее — потребляющий итератор let doubled: Vec = 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 ParseNumbers(List inputs) { var results = new List(); @@ -371,37 +371,37 @@ public List ParseNumbers(List inputs) { results.Add(result); } - // Silently skip invalid inputs + // Молча пропускаем некорректные значения } return results; } ``` ```rust -// Rust - Explicit error handling with collect +// Rust — явная обработка ошибок через collect fn parse_numbers(inputs: Vec) -> Result, std::num::ParseIntError> { inputs.into_iter() - .map(|s| s.parse::()) // Returns Result - .collect() // Collects into Result, ParseIntError> + .map(|s| s.parse::()) // Возвращает Result + .collect() // Собирает в Result, ParseIntError> } -// Alternative: Filter out errors +// Альтернатива: отфильтровать ошибки fn parse_numbers_filter(inputs: Vec) -> Vec { inputs.into_iter() - .filter_map(|s| s.parse::().ok()) // Keep only Ok values + .filter_map(|s| s.parse::().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 +## Упражнения
-🏋️ Exercise: LINQ to Iterators (click to expand) +🏋️ Упражнение: из LINQ в итераторы (нажмите, чтобы раскрыть) -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` of the top 3 students with grade ≥ 90, formatted as `"Name: Grade"`. +Верните `Vec` из трёх лучших студентов с оценкой ≥ 90, отформатированных как `"Name: Grade"`.
-🔑 Solution +🔑 Решение ```rust #[derive(Debug)] struct Student { name: String, grade: u32 } fn top_students(students: &mut [Student]) -> Vec { - 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` здесь нет. Сначала сортируете, затем строите цепочку ленивых операций.
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 { 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 { /* ... */ } -// Generates the SAME machine code as: +// Генерирует ТОТ ЖЕ машинный код, что и: fn lookup(id: u64) -> Option { /* ... */ } -// 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\ and Inline Arrays +### Современный C#: Span\ и встроенные массивы -C# has evolved beyond traditional arrays. `Span` provides type-safe, contiguous memory views that can live on the stack, while Inline Arrays (C# 12) offer fixed-size stack buffers. +C# ушёл далеко от традиционных массивов. `Span` предоставляет типобезопасные представления непрерывной памяти, которые могут находиться на стеке, а встроенные массивы (C# 12) дают буферы фиксированного размера на стеке. ```csharp -// C# Span - view into contiguous memory +// Span в C# — представление непрерывной памяти Span span = stackalloc int[] { 1, 2, 3, 4, 5 }; span[0] = 10; ReadOnlySpan text = "Hello".AsSpan(); -// Method accepting any contiguous memory view +// Метод, принимающий любое представление непрерывной памяти void ProcessSpan(ReadOnlySpan 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` (ref struct, stack-only) | `&mut [T]` / `&[T]` (borrowed slice) | -| `ReadOnlySpan` | `&[T]` (immutable slice) | -| `ReadOnlySpan` / `string.AsSpan()` | `&str` (string slice) | -| `[InlineArray(N)]` struct (C# 12) | `[T; N]` (fixed-size array) | -| `stackalloc T[]` with `Span` | `let arr: [T; N] = ...` (local array) | +| `Span` (ref struct, только на стеке) | `&mut [T]` / `&[T]` (заимствованный срез) | +| `ReadOnlySpan` | `&[T]` (неизменяемый срез) | +| `ReadOnlySpan` / `string.AsSpan()` | `&str` (строковый срез) | +| Структура `[InlineArray(N)]` (C# 12) | `[T; N]` (массив фиксированного размера) | +| `stackalloc T[]` с `Span` | `let arr: [T; N] = ...` (локальный массив) | -> **Key insight:** Rust's `&[T]` combines the role of C#'s `ArraySegment`, `Span`, and `ReadOnlySpan` — 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`, `Span` и `ReadOnlySpan` из 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
+ vtable ptr"] --> CFields["Name: string ref
Age: int
Hobbies: List ref"] - CFields --> CHeap1["#quot;Alice#quot; on heap"] - CFields --> CHeap2["List<string> on heap"] + subgraph "Класс C# (куча)" + CObj["Заголовок объекта
+ указатель на vtable"] --> CFields["Name: ссылка на string
Age: int
Hobbies: ссылка на List"] + CFields --> CHeap1["#quot;Alice#quot; в куче"] + CFields --> CHeap2["List<string> в куче"] end - subgraph "Rust Struct (Stack)" + subgraph "Структура Rust (стек)" RFields["name: String
ptr | len | cap
age: i32
hobbies: Vec
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, // Private field (no pub) + pub name: String, // Публичное поле + pub age: u32, // Публичное поле + hobbies: Vec, // Приватное поле (без 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 { &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 { "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 +## Упражнения
-🏋️ Exercise: Slice Window Average (click to expand) +🏋️ Упражнение: скользящее среднее по срезу (нажмите, чтобы раскрыть) -**Challenge**: Write a function that takes a slice of `f64` values and a window size, and returns a `Vec` 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` со скользящими средними. Например, `[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 { - // Your implementation here + // Ваша реализация здесь todo!() } @@ -650,7 +651,7 @@ fn main() { ```
-🔑 Solution +🔑 Решение ```rust fn rolling_average(data: &[f64], window: usize) -> Vec { @@ -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()`.
-🏋️ Exercise: Mini Address Book (click to expand) +🏋️ Упражнение: мини-адресная книга (нажмите, чтобы раскрыть) -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) -> Self` -4. Implement `Contact::add_phone(&mut self, kind: PhoneType, number: impl Into)` -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) -> Self` +4. Реализуйте `Contact::add_phone(&mut self, kind: PhoneType, number: impl Into)` +5. Реализуйте `Contact::mobile_numbers(&self) -> Vec<&str>`, который возвращает только мобильные номера +6. В `main` создайте контакт, добавьте два телефона и выведите мобильные номера
-🔑 Solution +🔑 Решение ```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` vs `Nullable` for null safety, and custom error types with `Result`. +> **Что вы узнаете:** почему выражения `switch` в C# молча пропускают случаи, тогда как `match` в Rust ловит их на этапе компиляции, +> `Option` против `Nullable` для null-безопасности, а также собственные типы ошибок с `Result`. > -> **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 pattern matching is also exhaustive +// Сопоставление с образцом для Option тоже исчерпывающее fn process_optional_value(value: Option) -> 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` vs `Option` +## Null-безопасность: `Nullable` против `Option` -### 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 for value types +// Nullable в C# для значимых типов int? maybeNumber = GetNumber(); if (maybeNumber.HasValue) { @@ -167,18 +167,18 @@ if (maybeNumber.HasValue) } ``` -### Rust `Option` System +### Система `Option` в Rust ```rust -// Rust - Explicit null handling with Option +// Rust — явная работа с null через Option #[derive(Debug)] pub struct User { - name: String, // Never null - email: Option, // Explicitly optional + name: String, // Никогда не null + email: Option, // Явно необязательное } 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) { 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
[ERROR] Can be null"] - CS_NULLABLE["Nullable: int? value
[OK] Explicit for value types"] - CS_NRT["Nullable Reference Types
string? name
[WARNING] Compile-time warnings only"] + subgraph "Эволюция работы с null в C#" + CS_NULL["Традиционно: string name
[ERROR] Может быть null"] + CS_NULLABLE["Nullable: int? value
[OK] Явно для значимых типов"] + CS_NRT["Nullable-ссылочные типы
string? name
[WARNING] Только предупреждения компилятора"] - CS_RUNTIME["Runtime NullReferenceException
[ERROR] Can still crash"] + CS_RUNTIME["Runtime NullReferenceException
[ERROR] Всё ещё может упасть"] CS_NULL --> CS_RUNTIME CS_NRT -.-> CS_RUNTIME - CS_CHECKS["Manual null checks
if (obj?.Property != null)"] + CS_CHECKS["Ручные проверки на null
if (obj?.Property != null)"] end - subgraph "Rust Option System" + subgraph "Система Option в Rust" RUST_OPTION["Option
Some(value) | None"] - RUST_FORCE["Compiler forces handling
[OK] Cannot ignore None"] - RUST_MATCH["Pattern matching
match option { ... }"] - RUST_METHODS["Rich API
.map(), .unwrap_or(), .and_then()"] + RUST_FORCE["Компилятор заставляет обработать
[OK] Нельзя проигнорировать None"] + RUST_MATCH["Сопоставление с образцом
match option { ... }"] + RUST_METHODS["Богатый API
.map(), .unwrap_or(), .and_then()"] RUST_OPTION --> RUST_FORCE RUST_FORCE --> RUST_MATCH RUST_FORCE --> RUST_METHODS - RUST_SAFE["Compile-time null safety
[OK] No null pointer exceptions"] + RUST_SAFE["Null-безопасность на этапе компиляции
[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 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 instead of null +// Rust использует Option вместо null struct PersonService { people: HashMap, } @@ -297,12 +297,12 @@ impl PersonService { } } - // Returns Option - no null! + // Возвращает Option — никакого 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 for error handling + // Result для обработки ошибок 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 { 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 { - 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 +## Упражнения
-🏋️ Exercise: Option Combinators (click to expand) +🏋️ Упражнение: комбинаторы Option (нажмите, чтобы раскрыть) -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
} struct Address { city: Option } ``` -Write it as a **single expression** with no `if let` or `match`. +Запишите это как **одно выражение** без `if let` и `match`.
-🔑 Solution +🔑 Решение ```rust struct User { address: Option
} @@ -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#, но явно и с проверкой типов.
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(Func onSuccess, Func 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 { 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), // Recursive types! + List(Vec), // Рекурсивные типы! } 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
or switch expressions"] - CS_RUNTIME["[ERROR] Runtime exceptions
for missing cases"] - CS_HEAP["[ERROR] Heap allocation
for class inheritance"] + CS_MATCH["Ручной метод Match
или switch-выражения"] + CS_RUNTIME["[ERROR] Исключения во время выполнения
для пропущенных случаев"] + CS_HEAP["[ERROR] Выделение в куче
для наследования классов"] 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 }
Rectangle { width, height }
Triangle { base, height }"] RUST_MATCH["match shape { ... }"] - RUST_EXHAUSTIVE["[OK] Exhaustive checking
Compile-time guarantee"] - RUST_STACK["[OK] Stack allocation
Efficient memory use"] - RUST_ZERO["[OK] Zero-cost abstraction"] + RUST_EXHAUSTIVE["[OK] Проверка исчерпываемости
Гарантия на этапе компиляции"] + RUST_STACK["[OK] Размещение на стеке
Эффективное использование памяти"] + 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 { - 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 }, @@ -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(result: Result) -> 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 +// Деструктуризация структур и кортежей ```
-🏋️ Exercise: Command Parser (click to expand) +🏋️ Упражнение: парсер команд (нажмите, чтобы раскрыть) -**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 { 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!() } ```
-🔑 Solution +🔑 Решение ```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
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 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, @@ -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(&mut self, processor: F) where F: FnOnce(&mut Vec) { 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 { - 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) { - 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 +## Упражнения
-🏋️ Exercise: Spot the Safety Bug (click to expand) +🏋️ Упражнение: найдите ошибку безопасности (нажмите, чтобы раскрыть) -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 GetEvenNumbers(List numbers) @@ -246,7 +247,7 @@ public List GetEvenNumbers(List 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 GetEvenNumbers(List numbers) ```
-🔑 Solution +🔑 Решение -**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) -> Vec { @@ -265,17 +266,17 @@ fn get_even_numbers(numbers: &mut Vec) -> Vec { 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) -> Vec { let evens: Vec = 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# ловит это во время выполнения; многие языки не ловят вообще.
*** - - 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 +// Часто встречается в трейт-объектах: Box fn make_printer<'a>(text: &'a str) -> Box { 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` | Parameterize the container with `'a` | -| Temporary borrowing | ❌ Never — over-constraining | Use the actual lifetime | +| Строковые литералы | ✅ Да — они всегда `'static` | — | +| Замыкание для `thread::spawn` | Часто — поток переживает вызывающий код | Используйте `thread::scope` для заимствованных данных | +| Глобальная конфигурация | ✅ `lazy_static!` или `OnceLock` | Передавайте ссылки через параметры | +| Трейт-объекты, которые хранятся долго | Часто — `Box` | Параметризуйте контейнер через `'a` | +| Временные заимствования | ❌ Никогда — излишнее ограничение | Используйте реальное время жизни |
-🏋️ Exercise: Lifetime Annotations (click to expand) +🏋️ Упражнение: аннотации времён жизни (нажмите, чтобы раскрыть) -**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 { ```
-🔑 Solution +🔑 Решение ```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>`.
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`, `Rc`, `Arc`, `Cell`, `RefCell`, 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`, `Rc`, `Arc`, `Cell`, `RefCell` и `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), // Box gives a known pointer size + Cons(i32, Box), // 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 is only needed in Rust because stack is the default -var list = new LinkedListNode(1); // always heap-allocated +// C# — всё уже в куче (ссылочные типы) +// Box нужен в Rust только потому, что стек — это умолчание +var list = new LinkedListNode(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 { 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>, } @@ -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 = Box::new(String::from("hello")); -// Deref coercion chain: Box → String → str -println!("Length: {}", boxed.len()); // calls str::len() — auto-deref! +// Цепочка приведения через Deref: Box → 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 → &str — two levels! +greet(&s); // &String → &str через приведение Deref +greet(&boxed); // &Box → &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` where `&[T]` is expected, and `&Box` where `&T` is expected — all without explicit conversion. This is why Rust APIs typically accept `&str` and `&[T]` rather than `&String` and `&Vec`. +**Почему это важно:** вы можете передать `&String` там, где ожидается `&str`, `&Vec` там, где ожидается `&[T]`, и `&Box` там, где ожидается `&T`, — и всё это без явного преобразования. Поэтому API на Rust обычно принимают `&str` и `&[T]`, а не `&String` и `&Vec`. -### Rc vs Arc: When to Use Which +### Rc против Arc: что выбрать | | `Rc` | `Arc` | |---|---|---| -| **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` for mutation | `Mutex` or `RwLock` for mutation | +| **Потокобезопасность** | ❌ Только один поток | ✅ Потокобезопасен (атомарные операции) | +| **Накладные расходы** | Ниже (неатомарный счётчик ссылок) | Выше (атомарный счётчик ссылок) | +| **Проверка компилятором** | Не скомпилируется при передаче в `thread::spawn` | Работает везде | +| **Сочетается с** | `RefCell` для изменения | `Mutex` или `RwLock` для изменения | -**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
or heap allocation?"] - HEAP["Just need heap allocation?"] - SHARED["Shared ownership needed?"] - THREADED["Shared across threads?"] - MUTABLE["Need interior mutability?"] - MAYBE_OWN["Sometimes borrowed,
sometimes owned?"] - - BOX["Use Box<T>"] - RC["Use Rc<T>"] - ARC["Use Arc<T>"] - REFCELL["Use RefCell<T>
(or Rc<RefCell<T>>)"] - MUTEX["Use Arc<Mutex<T>>"] - COW["Use Cow<'a, T>"] - OWN["Use owned type
(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["Нужно разделяемое владение
или выделение в куче?"] + HEAP["Нужно только выделение в куче?"] + SHARED["Нужно разделяемое владение?"] + THREADED["Разделяется между потоками?"] + MUTABLE["Нужна внутренняя изменяемость?"] + MAYBE_OWN["Иногда заимствуется,
иногда владеется?"] + + BOX["Используйте Box<T>"] + RC["Используйте Rc<T>"] + ARC["Используйте Arc<T>"] + REFCELL["Используйте RefCell<T>
(или Rc<RefCell<T>>)"] + MUTEX["Используйте Arc<Mutex<T>>"] + COW["Используйте Cow<'a, T>"] + OWN["Используйте владеемый тип
(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 ```
-🏋️ Exercise: Choose the Right Smart Pointer (click to expand) +🏋️ Упражнение: выберите подходящий умный указатель (нажмите, чтобы раскрыть) -**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. Буфер логов, который нужно изменять через разделяемую ссылку
-🔑 Solution +🔑 Решение -1. **`Box`** — recursive types need indirection for known size at compile time -2. **`Rc`** — shared read-only access, single thread, no `Arc` overhead needed -3. **`Arc>`** — shared across threads (`Arc`) with mutation (`Mutex`) -4. **`Cow<'a, str>`** — sometimes returns `&str` (cache hit), sometimes `String` (cache miss) -5. **`RefCell>`** — interior mutability behind `&self` (single thread) +1. **`Box`** — рекурсивным типам нужна косвенная адресация, чтобы размер был известен на этапе компиляции +2. **`Rc`** — разделяемый доступ только для чтения, один поток, накладные расходы `Arc` не нужны +3. **`Arc>`** — разделяется между потоками (`Arc`) с изменением (`Mutex`) +4. **`Cow<'a, str>`** — иногда возвращает `&str` (попадание в кэш), иногда `String` (промах кэша) +5. **`RefCell>`** — внутренняя изменяемость за `&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`, когда хотите работать без копирования в типичном случае.
*** - diff --git a/csharp-book/src/ch07-ownership-and-borrowing.md b/csharp-book/src/ch07-ownership-and-borrowing.md index ac71304..93ee23a 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 { 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 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`/`Arc` — 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`/`Arc` — см. [Умные указатели](ch07-3-smart-pointers-beyond-single-ownership.md)) +2. **Когда владелец выходит из области видимости, значение уничтожается** (детерминированная очистка — см. [Drop](ch07-3-smart-pointers-beyond-single-ownership.md#drop-аналог-idisposable-в-rust)) +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) { // list now owns the vector +fn process_list(mut list: Vec) { // 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 { 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 a, ref List b) { var temp = a; @@ -90,19 +90,19 @@ public void SwapLists(ref List a, ref List b) ``` ```rust -// Rust - Ownership-aware swapping +// Rust — обмен с учётом владения fn swap_vectors(a: &mut Vec, b: &mut Vec) { - 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 data = new List(); - public List GetData() => data; // Returns reference to internal data + public List 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, } impl SafeContainer { - // Return immutable borrow - caller can't modify - // Prefer &[i32] over &Vec — accept the broadest type + // Возвращаем неизменяемое заимствование — вызывающий код не может изменить данные + // Предпочитайте &[i32] вместо &Vec — принимайте самый широкий тип pub fn get_data(&self) -> &[i32] { &self.data } - // Return mutable borrow - exclusive access guaranteed + // Возвращаем изменяемое заимствование — эксклюзивный доступ гарантирован pub fn get_data_mut(&mut self) -> &mut Vec { &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 { 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, @@ -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
new Person()"] - CS_HEAP["Managed Heap"] - CS_REF["References point to heap"] - CS_GC_CHECK["GC periodically checks
for unreachable objects"] - CS_SWEEP["Mark and sweep
collection"] - CS_PAUSE["[ERROR] GC pause times"] + subgraph "Управление памятью в C#" + CS_ALLOC["Выделение объекта
new Person()"] + CS_HEAP["Управляемая куча"] + CS_REF["Ссылки указывают на кучу"] + CS_GC_CHECK["GC периодически проверяет
недостижимые объекты"] + CS_SWEEP["Сборка по принципу
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
[ERROR] Memory pressure
[ERROR] Finalization complexity
[OK] Easy to use"] + CS_ISSUES["[ERROR] Недетерминированная очистка
[ERROR] Давление на память
[ERROR] Сложность финализации
[OK] Легко использовать"] end - subgraph "Rust Ownership System" - RUST_ALLOC["Value Creation
Person { ... }"] - RUST_OWNER["Single owner
on stack or heap"] - RUST_BORROW["Borrowing system
&T, &mut T"] - RUST_SCOPE["Scope-based cleanup
Drop trait"] - RUST_COMPILE["Compile-time verification"] + subgraph "Система владения Rust" + RUST_ALLOC["Создание значения
Person { ... }"] + RUST_OWNER["Единственный владелец
на стеке или в куче"] + RUST_BORROW["Система заимствований
&T, &mut T"] + RUST_SCOPE["Очистка по области видимости
трейт Drop"] + RUST_COMPILE["Проверка на этапе компиляции"] RUST_ALLOC --> RUST_OWNER RUST_OWNER --> RUST_BORROW RUST_BORROW --> RUST_SCOPE RUST_SCOPE --> RUST_COMPILE - RUST_BENEFITS["[OK] Deterministic cleanup
[OK] Zero runtime cost
[OK] No memory leaks
[ERROR] Learning curve"] + RUST_BENEFITS["[OK] Детерминированная очистка
[OK] Нулевые накладные расходы рантайма
[OK] Никаких утечек памяти
[ERROR] Кривая обучения"] end style CS_ISSUES fill:#ffebee,color:#000 @@ -427,67 +427,67 @@ graph TD
-🏋️ Exercise: Fix the Borrow Checker Errors (click to expand) +🏋️ Упражнение: исправьте ошибки проверщика заимствований (нажмите, чтобы раскрыть) -**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 } ```
-🔑 Solution +🔑 Решение ```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`, поэтому индексация копирует значение +- Возврат владеемого значения передаёт владение вызывающему коду — проблем с временами жизни нет
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 @@ -23,7 +23,7 @@ ``` -#### 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 - + true @@ -63,29 +63,29 @@ proptest = "1.0" # Property testing - + ``` -#### 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 @@ -96,7 +96,7 @@ version = "1.0.163" ``` -#### 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 +// Функции в файле проекта DEBUG;TRACE ``` -#### 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 - + @@ -327,9 +327,9 @@ fn main() { ``` -#### 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 +## Упражнения
-🏋️ Exercise: Design a Module Tree (click to expand) +🏋️ Упражнение: спроектируйте дерево модулей (нажмите, чтобы раскрыть) -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`
-🔑 Solution +🔑 Решение -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` type alias, and when to choose `thiserror` (libraries) vs `anyhow` (applications). +> **Что вы узнаете:** производственный паттерн определения перечисления ошибок для каждого крейта с помощью `thiserror`, +> создание псевдонима типа `Result`, а также когда выбирать `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 = std::result::Result; ``` -### 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 -// Here we show the pattern with `pool` as shorthand. +// Здесь показан паттерн, где `pool` — сокращение. pub async fn get_user(id: Uuid) -> Result { 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 { } ``` -### 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` alias means every function signature is clean: `fn foo() -> Result` -- **Unlike C# exceptions**, callers see all possible error variants in the type +### Почему это важно +- **`thiserror`** автоматически генерирует реализации `Display` и `Error` +- **`#[from]`** позволяет оператору `?` автоматически преобразовывать ошибки библиотек +- Псевдоним `Result` делает каждую сигнатуру функции чистой: `fn foo() -> Result` +- **В отличие от исключений 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::()` | +| **Назначение** | Определение структурированных типов ошибок для **библиотек** | Быстрая обработка ошибок для **приложений** | +| **Результат** | Собственное перечисление, которым вы управляете | Непрозрачная обёртка `anyhow::Error` | +| **Что видит вызывающий код** | Все варианты ошибок в типе | Просто `anyhow::Error` — непрозрачный | +| **Лучше всего для** | Библиотечных крейтов, API, любого кода с потребителями | Бинарников, скриптов, прототипов, CLI-утилит | +| **Приведение типов** | `match` по вариантам напрямую | `error.downcast_ref::()` | ```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 { ``` ```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 = Result -// .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 { match fs::read_to_string(path) { Ok(content) => Ok(content), @@ -169,11 +169,11 @@ fn read_or_create(path: &str) -> Result { 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 { @@ -184,7 +184,7 @@ fn load_config() -> anyhow::Result { Ok(config) } -// Pattern 4: Map errors to your domain type +// Паттерн 4: преобразуем ошибки в доменный тип fn parse_port(s: &str) -> Result { s.parse::() .map_err(|_| AppError::Validation { @@ -194,35 +194,35 @@ fn parse_port(s: &str) -> Result { ``` ```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 +## Упражнения
-🏋️ Exercise: Design a Crate Error Type (click to expand) +🏋️ Упражнение: спроектируйте тип ошибок крейта (нажмите, чтобы раскрыть) -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 = std::result::Result;` 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 = std::result::Result;` +3. Напишите `register_user(email: &str, password: &str) -> Result<()>`, которая демонстрирует распространение через `?` и явное создание ошибок
-🔑 Solution +🔑 Решение ```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 делает каждую сигнатуру чистой.
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` +## Исключения против `Result` -> **What you'll learn:** Why Rust replaces exceptions with `Result` and `Option`, -> the `?` operator for concise error propagation, and how explicit error handling -> eliminates hidden control flow that plagues C# `try`/`catch` code. +> **Что вы узнаете:** почему Rust заменяет исключения на `Result` и `Option`, +> оператор `?` для краткого распространения ошибок, и как явная обработка ошибок +> устраняет скрытый поток управления, который осложняет код с `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, // Simulated database + users: Vec, // Имитация базы данных } 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 { - 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
(Runtime cost)"] - CS_CATCH["try/catch block"] - CS_HIDDEN["[ERROR] Hidden control flow
[ERROR] Performance cost
[ERROR] Easy to ignore"] + CS_STACK["Раскрутка стека
(затраты во время выполнения)"] + CS_CATCH["Блок try/catch"] + CS_HIDDEN["[ERROR] Скрытый поток управления
[ERROR] Затраты на производительность
[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
(early return)"] - RUST_EXPLICIT["[OK] Explicit error handling
[OK] Zero runtime cost
[OK] Cannot ignore errors"] + RUST_QUESTION["Оператор ?
(ранний возврат)"] + RUST_EXPLICIT["[OK] Явная обработка ошибок
[OK] Нулевые затраты во время выполнения
[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 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 { - 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 { @@ -193,13 +193,13 @@ fn process_content(content: &str) -> Result { } ``` -### `Option` for Nullable Values +### `Option` для значений, которые могут отсутствовать ```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 for optional values +// Rust — Option для необязательных значений fn find_user_name(user_id: u32) -> Option { - // 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 { if b != 0.0 { @@ -253,9 +253,9 @@ fn safe_divide(a: f64, b: f64) -> Option { } fn parse_and_divide(a_str: &str, b_str: &str) -> Result, 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() {
-🏋️ Exercise: Build a Crate-Level Error Type (click to expand) +🏋️ Упражнение: создайте тип ошибок уровня крейта (нажмите, чтобы раскрыть) -**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 and From -// TODO: Define type alias: type Result = std::result::Result; +// TODO: Реализуйте трейты Display и Error +// TODO: Реализуйте From и From +// TODO: Определите псевдоним типа: type Result = std::result::Result; fn load_config(path: &str) -> Result { 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 { ```
-🔑 Solution +🔑 Решение ```rust use std::io; @@ -334,11 +334,11 @@ fn load_config(path: &str) -> Result { } ``` -**Key takeaways**: -- `thiserror` generates `Display` and `Error` impls from attributes -- `#[from]` generates `From` impls, enabling automatic `?` conversion -- The `Result` alias eliminates boilerplate throughout your crate -- Unlike C# exceptions, the error type is visible in every function signature +**Ключевые выводы**: +- `thiserror` генерирует реализации `Display` и `Error` на основе атрибутов +- `#[from]` генерирует реализации `From`, что позволяет автоматически преобразовывать ошибки через `?` +- Псевдоним `Result` убирает шаблонный код во всём крейте +- В отличие от исключений C#, тип ошибки виден в каждой сигнатуре функции
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 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 where TInput : IConvertible where TOutput : class, new() @@ -33,29 +33,29 @@ public class Converter public TOutput Convert(TInput input) { var output = new TOutput(); - // Conversion logic using IConvertible + // Логика преобразования через IConvertible return output; } } -// Variance in generics +// Вариантность в обобщениях public interface IRepository where T : IEntity { - IEnumerable GetAll(); // Covariant - can return more derived types + IEnumerable GetAll(); // Ковариантность — может возвращать более производные типы } public interface IWriter 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 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 { - self.items.clone() // Clone trait for duplication + self.items.clone() // Трейт Clone для копирования } } -// Multiple trait bounds with different syntaxes +// Несколько трейт-границ с разным синтаксисом pub fn process_data(input: T) -> U where T: Display + Clone, U: From + 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; } @@ -109,15 +109,15 @@ pub trait Collect { fn collect>(iter: I) -> Self; } -// Higher-ranked trait bounds (advanced) +// Границы трейтов высокого ранга (продвинутый уровень) fn apply_to_all(items: &[String], f: F) -> Vec 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 PartialEq for Repository 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
Virtual method dispatch"] - CS_VARIANCE["[OK] Covariance/Contravariance
in/out keywords"] - CS_REFLECTION["[ERROR] Runtime reflection possible
typeof(T), is, as operators"] - CS_BOXING["[ERROR] Value type boxing
for interface constraints"] + CS_RUNTIME["[ERROR] Проверка типов частично во время выполнения
Диспетчеризация виртуальных методов"] + CS_VARIANCE["[OK] Ковариантность/контравариантность
ключевые слова in/out"] + CS_REFLECTION["[ERROR] Возможна рефлексия во время выполнения
операторы typeof(T), is, as"] + CS_BOXING["[ERROR] Упаковка значимых типов
для ограничений интерфейсов"] 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
Monomorphization"] - RUST_ZERO["[OK] Zero-cost abstractions
No runtime overhead"] - RUST_ASSOCIATED["[OK] Associated types
More flexible than generics"] - RUST_HKT["[OK] Higher-ranked trait bounds
Advanced type relationships"] + RUST_COMPILE["[OK] Разрешение на этапе компиляции
Мономорфизация"] + RUST_ZERO["[OK] Абстракции с нулевой стоимостью
Без накладных расходов рантайма"] + RUST_ASSOCIATED["[OK] Ассоциированные типы
Гибче, чем обобщения"] + RUST_HKT["[OK] Границы трейтов высокого ранга
Продвинутые связи между типами"] 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
[OK] Variance
[OK] Runtime type info
[ERROR] Performance cost"] - RUST_FLEX["Rust Flexibility
[OK] Zero cost
[OK] Compile-time safety
[ERROR] No variance (yet)"] + subgraph "Сравнение гибкости" + CS_FLEX["Гибкость C#
[OK] Вариантность
[OK] Информация о типах во время выполнения
[ERROR] Затраты на производительность"] + RUST_FLEX["Гибкость Rust
[OK] Нулевая стоимость
[OK] Безопасность на этапе компиляции
[ERROR] Вариантности пока нет"] end style CS_RUNTIME fill:#fff3e0,color:#000 @@ -171,12 +171,12 @@ graph TD --- -## Exercises +## Упражнения
-🏋️ Exercise: Generic Repository (click to expand) +🏋️ Упражнение: обобщённый репозиторий (нажмите, чтобы раскрыть) -Translate this C# generic repository interface to Rust traits: +Переведите этот обобщённый интерфейс репозитория C# на трейты Rust: ```csharp public interface IRepository where T : IEntity, new() @@ -187,14 +187,14 @@ public interface IRepository where T : IEntity, new() } ``` -Requirements: -1. Define an `Entity` trait with `fn id(&self) -> u64` -2. Define a `Repository` trait where `T: Entity + Clone` -3. Implement a `InMemoryRepository` that stores items in a `Vec` -4. The `find` method should accept `impl Fn(&T) -> bool` +Требования: +1. Определите трейт `Entity` с методом `fn id(&self) -> u64` +2. Определите трейт `Repository`, где `T: Entity + Clone` +3. Реализуйте `InMemoryRepository`, который хранит элементы в `Vec` +4. Метод `find` должен принимать `impl Fn(&T) -> bool`
-🔑 Solution +🔑 Решение ```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`. Return `Option` instead of throwing. +**Ключевые отличия от C#**: ограничения `new()` нет (вместо него используется трейт `Default`). `Fn(&T) -> bool` заменяет `Func`. Вместо генерации исключения возвращается `Option`.
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(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
Runtime cost"] - CS_COUPLING["[ERROR] Tight coupling
[ERROR] Diamond problem
[ERROR] Deep hierarchies"] + CS_VTABLE["Диспетчеризация виртуальных методов
Затраты во время выполнения"] + CS_COUPLING["[ERROR] Жёсткая связанность
[ERROR] Ромбовидная проблема
[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
Zero cost"] - RUST_FLEXIBLE["[OK] Flexible composition
[OK] No hierarchy limits
[OK] Mix and match traits"] + RUST_STATIC["Статическая диспетчеризация
Нулевая стоимость"] + RUST_FLEXIBLE["[OK] Гибкая композиция
[OK] Нет ограничений иерархии
[OK] Свободное комбинирование трейтов"] RUST_DOG --> RUST_IMPL1 RUST_BIRD --> RUST_IMPL2 @@ -179,12 +179,12 @@ graph TD --- -## Exercises +## Упражнения
-🏋️ Exercise: Replace Inheritance with Traits (click to expand) +🏋️ Упражнение: замените наследование трейтами (нажмите, чтобы раскрыть) -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))` — обратите внимание на композицию трейт-границ (наследование не нужно)
-🔑 Solution +🔑 Решение ```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` объединяет возможности без глубины наследования.
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 animal) where T : IAnimal { animal.MakeSound(); @@ -53,26 +53,26 @@ public void ProcessAnimal(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(animal: &T) { animal.make_sound(); println!("{}", animal.describe()); animal.introduce(); } -// Multiple trait bounds +// Несколько ограничений трейтов fn process_animal_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 animals) { foreach (var animal in animals) { - animal.MakeSound(); // Dynamic dispatch + animal.MakeSound(); // Динамическая диспетчеризация Console.WriteLine(animal.Describe()); } } -// Usage +// Использование var animals = new List { new Dog("Buddy"), @@ -168,15 +168,15 @@ ProcessAnimals(animals); ``` ```rust -// Rust trait objects for dynamic dispatch +// Трейт-объекты Rust для динамической диспетчеризации fn process_animals(animals: &[Box]) { 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 + // Использование Box let animals: Vec> = 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, 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() {
-🏋️ Exercise: Trait-Based Drawing System (click to expand) +🏋️ Упражнение: система рисования на основе трейтов (нажмите, чтобы раскрыть) -**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]` and prints total area. +**Задача**: реализуйте трейт `Drawable` с методом `area()` и методом `draw()` по умолчанию. Создайте структуры `Circle` и `Rect`. Напишите функцию, которая принимает `&[Box]` и выводит суммарную площадь.
-🔑 Solution +🔑 Решение ```rust use std::f64::consts::PI; @@ -363,29 +363,29 @@ fn main() { } ``` -**Key takeaways**: -- `dyn Trait` gives runtime polymorphism (like C# `IDrawable`) -- `Box` is heap-allocated, needed for heterogeneous collections -- Default methods work exactly like C# 8+ default interface methods +**Ключевые выводы**: +- `dyn Trait` даёт полиморфизм времени выполнения (как `IDrawable` в C#) +- `Box` размещается в куче и нужен для коллекций разнородных типов +- Методы по умолчанию работают точно так же, как методы интерфейсов по умолчанию в C# 8+
-### 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; } 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 { if self.current < self.max { self.current += 1; @@ -397,11 +397,11 @@ impl Iterator for Counter { } ``` -In C#, `IEnumerator` 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` vs C#'s `IEnumerable`. +В C# для этой цели `IEnumerator` использует параметр обобщения (`T`). Ассоциированные типы Rust устроены иначе: у `Iterator` *один* тип `Item` на каждую реализацию, а не параметр обобщения на уровне трейта. Это упрощает ограничения трейтов: `impl Iterator` против `IEnumerable` в 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 ::add(a, b) +let c = a + b; // вызывает ::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# такого ограничения нет — любой код может добавить методы расширения к любому типу, что может привести к неоднозначности. -## `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(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 { (0..limit) .filter(|n| n % 2 == 0) .map(|n| n * n) } -// The caller sees "some type that implements Iterator" -// The actual type (Filter, ...>>) is unnameable — impl Trait solves this. +// Вызывающий код видит «какой-то тип, реализующий Iterator» +// Фактический тип (Filter, ...>>) не имеет имени — 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 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, C# doesn't explicitly box — the runtime handles allocation +// Тип возвращаемого значения скрывает конкретный итератор за интерфейсом IEnumerable +// В отличие от Box в Rust, C# не упаковывает явно — выделением памяти занимается рантайм ``` -### Returning Closures: `impl Fn` vs `Box` +### Возврат замыканий: `impl Fn` против `Box` ```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 i32> { if add { Box::new(|a, b| a + b) @@ -508,43 +508,43 @@ fn choose_op(add: bool) -> Box 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 MakeAdder(int x) => y => x + y; Func 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
a trait-based type?"] - POSITION["Argument or return position?"] - ARG_SAME["All callers pass
the same type?"] - RET_SINGLE["Always returns the
same concrete type?"] - COLLECTION["Storing in a collection
or as struct field?"] - - GENERIC["Use generics
fn foo<T: Trait>(x: T)"] - IMPL_ARG["Use impl Trait
fn foo(x: impl Trait)"] - IMPL_RET["Use impl Trait
fn foo() -> impl Trait"] - DYN_BOX["Use Box<dyn Trait>
Dynamic dispatch"] - DYN_REF["Use &dyn Trait
Borrowed dynamic dispatch"] + START["Функция принимает или возвращает
тип на основе трейта?"] + POSITION["Позиция аргумента или возврата?"] + ARG_SAME["Все вызывающие передают
один и тот же тип?"] + RET_SINGLE["Всегда возвращается один и тот же
конкретный тип?"] + COLLECTION["Хранится в коллекции
или как поле структуры?"] + + GENERIC["Используйте обобщения
fn foo<T: Trait>(x: T)"] + IMPL_ARG["Используйте impl Trait
fn foo(x: impl Trait)"] + IMPL_RET["Используйте impl Trait
fn foo() -> impl Trait"] + DYN_BOX["Используйте Box<dyn Trait>
Динамическая диспетчеризация"] + DYN_REF["Используйте &dyn Trait
Динамическая диспетчеризация по ссылке"] 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(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` | 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(x: T)` | Статическая (мономорфизация) | Стек | Несколько ограничений трейтов, нужен turbofish, один и тот же тип используется повторно | +| `fn foo(x: impl Trait)` | Статическая (мономорфизация) | Стек | Простые ограничения, более чистый синтаксис, разовые параметры | +| `fn foo() -> impl Trait` | Статическая | Стек | Один конкретный тип возвращаемого значения, итераторы, замыкания | +| `fn foo() -> Box` | Динамическая (таблица виртуальных методов) | **Куча** | Разные типы возвращаемых значений, трейт-объекты в коллекциях | +| `&dyn Trait` / `&mut dyn Trait` | Динамическая (таблица виртуальных методов) | Без выделения | Заимствованные ссылки на разнородные типы, параметры функций | ```rust -// Summary: from fastest to most flexible -fn static_dispatch(x: impl Display) { /* fastest, no alloc */ } -fn generic_dispatch(x: T) { /* fastest, multiple bounds */ } -fn dynamic_dispatch(x: &dyn Display) { /* vtable lookup, no alloc */ } -fn boxed_dispatch(x: Box) { /* vtable lookup + heap alloc */ } +// Сводка: от самого быстрого к самому гибкому +fn static_dispatch(x: impl Display) { /* быстрее всего, без выделения */ } +fn generic_dispatch(x: T) { /* быстрее всего, несколько ограничений */ } +fn dynamic_dispatch(x: &dyn Display) { /* поиск в таблице виртуальных методов, без выделения */ } +fn boxed_dispatch(x: Box) { /* поиск в таблице виртуальных методов + выделение в куче */ } ``` *** 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) { 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 +## Упражнения
-🏋️ Exercise: Currency Converter (click to expand) +🏋️ Упражнение: конвертер валют (нажмите, чтобы раскрыть) -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` (treats input as whole dollars → `cents = dollars * 100`) -3. Implement `TryFrom` — 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 + Copy]) -> Money` that sums values +1. `Money { cents: i64 }` (хранит значение в центах, чтобы избежать проблем с плавающей точкой) +2. Реализуйте `From` (входное значение — целые доллары → `cents = dollars * 100`) +3. Реализуйте `TryFrom` — отвергайте отрицательные суммы, округляйте до ближайшего цента +4. Реализуйте `Display`, чтобы выводить формат `"$1.50"` +5. Реализуйте `FromStr`, чтобы разбирать `"$1.50"` или `"1.50"` обратно в `Money` +6. Напишите функцию `fn total(items: &[impl Into + Copy]) -> Money`, которая суммирует значения
-🔑 Solution +🔑 Решение ```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 { 1, 2, 3 }; // Collection initializer syntax +// В C# есть возможности, которые делают макросы ненужными: +Console.WriteLine("Hello"); // Перегрузка методов (1–16 параметров) +Console.WriteLine("{0}, {1}", a, b); // Вариативность через массив params +var list = new List { 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` | -| `PartialOrd`, `Ord` | `<`, `>` comparison + sorting | `IComparable` | -| `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` | +| `PartialOrd`, `Ord` | Сравнение `<`, `>` и сортировка | `IComparable` | +| `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(val: &T) -> String { serde_json::to_string(val).unwrap() @@ -159,42 +159,42 @@ pub fn to_json(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 для отладки! ```
-🏋️ Exercise: Write a min! Macro (click to expand) +🏋️ Упражнение: напишите макрос min! (нажмите, чтобы раскрыть) -**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 ```
-🔑 Solution +🔑 Решение ```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`, только для структуры кода, а не значений.
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 doubler = x => x * 2; Action printer = msg => Console.WriteLine(msg); -// Closure capturing outer variables +// Замыкание, захватывающее внешние переменные int multiplier = 3; Func 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) -> Vec { - 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 = numbers .iter() - .filter(|&&n| n % 2 == 0) // Filter even numbers - .map(|&n| n * n) // Square them - .filter(|&n| n > 10) // Filter > 10 - .collect::>() // Collect to Vec + .filter(|&&n| n % 2 == 0) // Отбираем чётные числа + .map(|&n| n * n) // Возводим в квадрат + .filter(|&n| n > 10) // Отбираем > 10 + .collect::>() // Собираем в 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) -> HashMap { .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) -> Vec { 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
(ToList(), ToArray())"] - CS_REFLECTION["[ERROR] Some runtime reflection
Expression trees"] - CS_ALLOCATIONS["[ERROR] Intermediate collections
Garbage collection pressure"] - CS_ASYNC["[OK] Async support
(with additional libraries)"] - CS_SQL["[OK] LINQ to SQL/EF integration"] + subgraph "Особенности LINQ в C#" + CS_LINQ["Выражение LINQ"] + CS_EAGER["Часто энергичные вычисления
(ToList(), ToArray())"] + CS_REFLECTION["[ERROR] Часть рефлексии во время выполнения
Деревья выражений"] + CS_ALLOCATIONS["[ERROR] Промежуточные коллекции
Нагрузка на сборщик мусора"] + CS_ASYNC["[OK] Поддержка async
(с дополнительными библиотеками)"] + 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
No work until .collect()"] - RUST_ZERO["[OK] Zero-cost abstractions
Compiles to optimal loops"] - RUST_NO_ALLOC["[OK] No intermediate allocations
Stack-based processing"] - RUST_PARALLEL["[OK] Easy parallelization
(rayon crate)"] - RUST_FUNCTIONAL["[OK] Functional programming
Immutable by default"] + subgraph "Особенности итераторов Rust" + RUST_ITER["Цепочка итераторов"] + RUST_LAZY["[OK] Ленивые вычисления
Никакой работы до .collect()"] + RUST_ZERO["[OK] Абстракции с нулевой стоимостью
Компилируются в оптимальные циклы"] + RUST_NO_ALLOC["[OK] Без промежуточных выделений
Обработка на стеке"] + RUST_PARALLEL["[OK] Простая параллелизация
(крейт rayon)"] + RUST_FUNCTIONAL["[OK] Функциональное программирование
Неизменяемость по умолчанию"] 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
[ERROR] Allocation overhead
[ERROR] Virtual dispatch
[OK] Good enough for most cases"] - RUST_PERF["Rust Iterator Performance
[OK] Hand-optimized speed
[OK] No allocations
[OK] Compile-time optimization"] + subgraph "Сравнение производительности" + CS_PERF["Производительность LINQ в C#
[ERROR] Накладные расходы на выделение памяти
[ERROR] Виртуальная диспетчеризация
[OK] Достаточно для большинства случаев"] + RUST_PERF["Производительность итераторов Rust
[OK] Скорость уровня ручной оптимизации
[OK] Без выделений памяти
[OK] Оптимизация на этапе компиляции"] end style CS_REFLECTION fill:#ffcdd2,color:#000 @@ -230,12 +230,12 @@ graph TD
-🏋️ Exercise: LINQ to Iterators Translation (click to expand) +🏋️ Упражнение: перевод из LINQ в итераторы (нажмите, чтобы раскрыть) -**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 ```
-🔑 Solution +🔑 Решение ```rust use std::collections::HashMap; @@ -281,19 +281,19 @@ fn department_stats(employees: &[Employee]) -> Vec { } ``` -**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 +- Цепочки итераторов не имеют накладных расходов — компилятор превращает их в простые циклы
-## 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 // 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::>()); @@ -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 = 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 = entries.iter() .map(|e| if e.level == "ERROR" { 1.0 } else { 0.0 }) .collect::>() - .windows(100) // std slice method + .windows(100) // метод среза из std .map(|w| w.iter().sum::() / 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()); } diff --git a/csharp-book/src/ch13-1-asyncawait-deep-dive.md b/csharp-book/src/ch13-1-asyncawait-deep-dive.md index 000c009..7de71bf 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 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 { 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` | Rust `Future` | |---|---|---| -| **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 FetchAllAsync(string[] urls, CancellationToken ct) { var tasks = urls.Select(url => httpClient.GetStringAsync(url, ct)); @@ -111,7 +111,7 @@ public async Task 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> { @@ -125,7 +125,7 @@ async fn fetch_all(urls: &[&str]) -> Vec> { results } -// With timeout: +// С таймаутом: async fn fetch_all_with_timeout(urls: &[&str]) -> Result, &'static str> { tokio::time::timeout( Duration::from_secs(10), @@ -144,17 +144,17 @@ async fn fetch_all_with_timeout(urls: &[&str]) -> Result, &'static s ```
-🏋️ Exercise: Async Timeout Pattern (click to expand) +🏋️ Упражнение: паттерн асинхронного таймаута (нажмите, чтобы раскрыть) -**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`.)
-🔑 Solution +🔑 Решение ```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, завершается, когда первое из них готово, и уничтожает (отменяет) остальные.
-### 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` is a wrapper that says: **"this value will not be moved in memory."** +`Pin` — это обёртка, которая говорит: **«это значение не будет перемещено в памяти».** ```rust -// You'll see Pin in these contexts: +// Pin встречается в таких контекстах: trait Future { type Output; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; - // ^^^^^^^^^^^^^^ pinned — internal references stay valid + // ^^^^^^^^^^^^^^ закреплено — внутренние ссылки остаются действительными } -// Returning a boxed future from a trait: +// Возврат future в упаковке Box из трейта: fn make_future() -> Pin + 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](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 1ef85ae..2e7c2d7 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>` vs `lock`, channels vs `ConcurrentQueue`, `Send`/`Sync` traits, -> scoped threads, and the bridge to async/await. +> **Что вы узнаете:** как Rust обеспечивает потокобезопасность на этапе компиляции в отличие от подхода C# на основе соглашений, +> `Arc>` против `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](https://r007.github.io/RustTraining-Rus/async-book/). > -> **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 items = new(); private readonly Dictionary 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 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 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 threadLocalRandom = new ThreadLocal(() => 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 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>>, cache: Arc>>, @@ -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 { 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, } @@ -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 = 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>"] - 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
-🏋️ Exercise: Thread-Safe Counter (click to expand) +🏋️ Упражнение: потокобезопасный счётчик (нажмите, чтобы раскрыть) -**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.
-🔑 Solution +🔑 Решение ```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>` is the general pattern. For simple counters, `AtomicU64` avoids lock overhead entirely. +**Ключевая мысль**: `Arc>` — общий паттерн. Для простых счётчиков `AtomicU64` полностью избавляет от накладных расходов на блокировки.
-### 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` is **neither** Send nor Sync — the compiler will refuse to let you pass it to `thread::spawn` (use `Arc` instead) -- `Cell` and `RefCell` are **not** Sync — use `Mutex` or `RwLock` for thread-safe interior mutability -- Raw pointers (`*const T`, `*mut T`) are **neither** Send nor Sync +Большинство типов автоматически реализуют `Send + Sync`. Заметные исключения: +- `Rc` **не** реализует ни Send, ни Sync — компилятор не даст передать его в `thread::spawn` (используйте `Arc`) +- `Cell` и `RefCell` **не** реализуют Sync — для потокобезопасной внутренней изменяемости используйте `Mutex` или `RwLock` +- Сырые указатели (`*const T`, `*mut T`) **не** реализуют ни Send, ни Sync -In C#, `List` 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` не потокобезопасен, но компилятор не помешает разделить его между потоками. В 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::())); }); - // '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` | 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` | Синхронная взаимная блокировка | +| `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(() => ...)` | `#[should_panic]` | Or use `std::panic::catch_unwind` | -| `Assert.Null(obj)` | `assert!(option.is_none())` | No nulls — use `Option` | +| `Assert.Throws(() => ...)` | `#[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; } @@ -163,7 +163,7 @@ mod tests { ``` ```csharp -// C# — Moq equivalent +// C# — эквивалент с Moq var mock = new Mock(); 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); ```
-🏋️ Exercise: Write Comprehensive Tests (click to expand) +🏋️ Упражнение: напишите полноценные тесты (нажмите, чтобы раскрыть) -**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 { ```
-🔑 Solution +🔑 Решение ```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 не нужен.
-## 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(v: &[T]) -> Vec { @@ -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 values: - // [], [0], [i32::MIN, i32::MAX], [42; 999], random sequences... - // If it fails, it SHRINKS to the smallest failing input! + // proptest запускает это на сотнях случайных значений Vec: + // [], [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::(), 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 { ("[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()` | `any::()` | -| Custom generators | `Arb.Register()` | `impl Strategy` | -| 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()` | `any::()` | +| Пользовательские генераторы | `Arb.Register()` | `impl Strategy` | +| Сжатие при падении (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..a4d74f1 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 +## Небезопасный Rust (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](https://r007.github.io/RustTraining-Rus/rust-patterns-book/). -### 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, } @@ -81,15 +81,15 @@ impl SafeBuffer { SafeBuffer { data: vec![0; size] } } - /// Safe API — bounds-checked access + /// Безопасный API — доступ с проверкой границ pub fn get(&self, index: usize) -> Option { self.data.get(index).copied() } - /// Fast unchecked access — unsafe but wrapped safely with bounds check + /// Быстрый доступ без проверки — небезопасный, но безопасно обёрнутый проверкой границ pub fn get_unchecked_safe(&self, index: usize) -> Option { 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
UTF-16 → UTF-8
struct layout"] + subgraph "Процесс C#" + CS["Код C#"] -->|"P/Invoke"| MI["Слой маршалинга
UTF-16 → UTF-8
раскладка структур"] 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
#[no_mangle]"] - RF --> Safe["Safe Rust
internals"] + RF --> Safe["Безопасные внутренности
на 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, } -/// 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 +## Упражнения
-🏋️ Exercise: Safe Wrapper for Raw Pointer (click to expand) +🏋️ Упражнение: безопасная обёртка над сырым указателем (нажмите, чтобы раскрыть) -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
-🔑 Solution +🔑 Решение ```rust,ignore struct SafeBuffer { @@ -352,7 +352,7 @@ struct SafeBuffer { impl SafeBuffer { fn new(size: usize) -> Option { - // 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-код, но предоставляют безопасные интерфейсы.
*** + 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, Dictionary) -# Built into std, but for advanced collections: -indexmap = "2.0" # Ordered HashMap +# Коллекции (как List, Dictionary) +# Встроены в 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, } -// 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) + // Асинхронный метод (как async Task в C#) pub async fn get_user(&self, id: Uuid) -> Result { let url = format!("{}/users/{}", self.base_url, id); @@ -125,7 +125,7 @@ impl UserService { Ok(user) } - // Create user (like C# async Task) + // Создание пользователя (как async Task в C#) pub async fn create_user(&self, name: String, email: String) -> Result { 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 { *** - -## 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(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, - // 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, } @@ -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 **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> { } ``` -### 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, Json(request): Json, ) -> Result, 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> { *** -## 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` vs exceptions -- `Option` vs nullable types -- Pattern matching and exhaustive checking +**Недели 3–4: обработка ошибок и типы** +- `Result` против исключений +- `Option` против nullable-типов +- Сопоставление с образцом и проверка исчерпываемости -**Recommended exercises:** +**Рекомендуемые упражнения:** ```rust -// Week 1-2: File processor +// Недели 1–2: обработчик файлов fn process_log_file(path: &str) -> Result, std::io::Error> { let content = std::fs::read_to_string(path)?; let errors: Vec = content @@ -256,7 +256,7 @@ fn process_log_file(path: &str) -> Result, 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, Box { 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> { let client = reqwest::Client::new(); @@ -329,17 +329,17 @@ async fn fetch_and_process_data(urls: Vec<&str>) -> Result<(), Box **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 where T : IEntity { Task GetByIdAsync(int id); @@ -52,12 +52,12 @@ public class UserRepository : IRepository 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 for UserRepository { async fn get_by_id(&self, id: u64) -> Result, RepositoryError> { - // Simulate database lookup + // Имитация поиска в базе данных if id == 0 { return Ok(None); } @@ -120,12 +120,12 @@ impl Repository for UserRepository { } async fn get_all(&self) -> Result, RepositoryError> { - // Implementation here + // Реализация здесь Ok(vec![]) } async fn add(&self, entity: User) -> Result { - // 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 for UserRepository { } async fn update(&self, entity: User) -> Result { - // 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(); services.AddScoped(); @@ -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, 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 = users .iter() .filter(|u| u.age > 18) @@ -334,7 +334,7 @@ let mut result: Vec = users result.sort(); result.truncate(10); -// Or with itertools crate for more LINQ-like chaining +// Или с крейтом itertools для более LINQ-подобной цепочки use itertools::Itertools; let result: Vec = users @@ -346,9 +346,9 @@ let result: Vec = users .collect(); ``` -### Entity Framework → SQLx + Migrations +### Entity Framework → SQLx и миграции ```csharp -// C# Entity Framework +// Entity Framework в C# public class ApplicationDbContext : DbContext { public DbSet 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(); ``` ```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` 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` с распространением ошибок через `?` | +| 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 +## Упражнения
-🏋️ Exercise: Migrate a C# Service (click to expand) +🏋️ Упражнение: мигрируйте сервис на C# (нажмите, чтобы раскрыть) -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` instead of null, `Result` instead of try/catch, and fix the SQL injection vulnerability. +**Подсказки**: используйте трейт, `Option` вместо null, `Result` вместо try/catch, и устраните уязвимость SQL-инъекции.
-🔑 Solution +🔑 Решение ```rust use async_trait::async_trait; @@ -546,29 +546,29 @@ trait UserService: Send + Sync { } struct UserServiceImpl { - db: D, // No Arc needed — Rust's ownership handles it + db: D, // Arc не нужен — владение Rust справляется с этим само } #[async_trait] impl UserService for UserServiceImpl { async fn get_by_id(&self, id: i64) -> Result, 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, 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` (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` (null-безопасность на этапе компиляции) +- `try/catch` → `Result` + `?` (явное распространение ошибок) +- SQL-инъекция устранена: параметризованные запросы вместо интерполяции строк +- `IDatabase _db` → обобщение `D: Database` (статическая диспетчеризация, без упаковки)
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> 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, Box .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:
• CLI tools first
• Performance-critical components
• New microservices"] + INCREMENTAL["Постепенное внедрение:
• Сначала CLI-инструменты
• Компоненты, критичные к производительности
• Новые микросервисы"] - FULL_RUST["Full Rust adoption:
• Greenfield projects
• System-level services
• High-performance APIs"] + FULL_RUST["Полный переход на Rust:
• Новые проекты с нуля
• Системные сервисы
• Высокопроизводительные API"] - STAY_CSHARP["Stay with C#:
• Optimize existing code
• Use .NET AOT / performance features
• Consider .NET Native"] + STAY_CSHARP["Остаться на C#:
• Оптимизировать существующий код
• Использовать возможности .NET AOT / производительности
• Рассмотреть .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`, `HashMap`, and `HashSet` - - 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` and `Arc` for shared ownership - - Learn when to use `Box` 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`, `HashMap` и `HashSet` + - Изучите методы итераторов: `map`, `filter`, `collect`, `fold` + - Практикуйтесь: циклы `for` против цепочек итераторов + +2. **Трейты и обобщения** + - Реализуйте распространённые трейты: `Debug`, `Clone`, `PartialEq` + - Пишите обобщённые функции и структуры + - Разберитесь с границами трейтов и предложениями where + +3. **Структура проекта** + - Организуйте код в модули + - Разберитесь с видимостью `pub` + - Работайте с внешними крейтами с crates.io + +### Продвинутые темы (месяцы 3 и далее) +1. **Конкурентность** + - Изучите трейты `Send` и `Sync` + - Используйте `std::thread` для базового параллелизма + - Изучите `tokio` для асинхронного программирования + +2. **Управление памятью** + - Разберитесь с `Rc` и `Arc` для разделяемого владения + - Изучите, когда использовать `Box` для выделения в куче + - Освойте времена жизни для сложных сценариев + +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 explicitly +// НАДО: явно используйте Option fn use_option_instead() { let maybe_string: Option = 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 в безопасности и производительности делают первоначальные инвестиции в обучение оправданными. -## 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 { ["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, 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, 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..cd1b723 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](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/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 = Result; #[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 { - 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, // Interior mutability - use sparingly + value: RefCell, // Внутренняя изменяемость — используйте осторожно } -// [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` (clone is just a reference count bump) -4. None of the above? → `clone()` is justified +**Чек-лист решения:** +1. Можно передать `&T` или `&str`? → Так и сделайте +2. Вызываемой стороне нужно владение? → Передавайте перемещением, а не клоном +3. Данные разделяются между потоками? → Используйте `Arc` (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 = 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>` 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>` или перестроить код, чтобы не разделять данные | +| Возвращать ссылки на локальные переменные | Возвращать владеемые значения | +| Изменять коллекцию во время перебора | Сначала собрать изменения, потом применить | +| Несколько изменяемых ссылок | Разбить структуру на независимые части | --- -### 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) -> Option { match input { Some(s) => { @@ -285,7 +284,7 @@ fn process(input: Option) -> Option { } } -// [OK] Flatten with combinators +// [OK] Выравниваем с помощью комбинаторов fn process(input: Option) -> Option { input .filter(|s| !s.is_empty()) @@ -295,16 +294,13 @@ fn process(input: Option) -> Option { } ``` -**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` to `Result