[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80034":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":14,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":15,"stars30d":16,"stars90d":14,"forks30d":14,"starsTrendScore":17,"compositeScore":18,"rankGlobal":9,"rankLanguage":9,"license":9,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":9,"pushedAt":9,"updatedAt":23,"readmeContent":24,"aiSummary":25,"trendingCount":14,"starSnapshotCount":14,"syncStatus":13,"lastSyncTime":26,"discoverSource":27},80034,"rustinterviewquiestions","Develp10\u002Frustinterviewquiestions","Develp10","Rust вопорсы с собеседований ",null,"Rust",78,8,2,0,11,12,3,46.56,false,"main",true,[],"2026-06-12 04:01:26","# Rust Interview Deep Dive\n\nСистемная подготовка к собеседованиям по Rust на позиции middle, senior и staff. Сто реальных вопросов с интервью в продуктовых и инфраструктурных компаниях, подробные разборы с кодом и задачи, которые встречаются в продакшене. Фокус на механике языка, на которой держатся настоящие сервисы, а не на угадывании вывода программы.\n\nВ разборах есть lock-free структуры, self-referential типы в async, FFI с тензорными библиотеками, корректный Send на гардах через await, memory ordering под loom, soundness кастомных коллекций. База тоже на месте: владение, заимствование, лайфтаймы, базовые трейты. Заходить можно и с нуля, и с уровня staff.\n\nЖивой контент на русском с уроками и разборами кода - канал [t.me\u002Frust_code](https:\u002F\u002Ft.me\u002Frust_code).\n\n## Содержание\n\n1. [Как работать с репозиторием](#как-работать-с-репозиторием)\n2. [Структура репозитория](#структура-репозитория)\n3. [Уровни сложности](#уровни-сложности)\n4. [Связанные файлы](#связанные-файлы)\n5. [Ответы и разборы 100 вопросов](#ответы-и-разборы-100-вопросов)\n - [Владение, заимствование, лайфтаймы](#владение-заимствование-лайфтаймы)\n - [Типы, трейты, обобщения](#типы-трейты-обобщения)\n - [Конкурентность и параллелизм](#конкурентность-и-параллелизм)\n - [Async и runtime](#async-и-runtime)\n - [Unsafe, FFI, низкий уровень](#unsafe-ffi-низкий-уровень)\n - [Производительность](#производительность)\n - [Макросы и метапрограммирование](#макросы-и-метапрограммирование)\n - [Архитектура и дизайн API](#архитектура-и-дизайн-api)\n6. [Продвинутые вопросы уровня staff и expert](#продвинутые-вопросы-уровня-staff-и-expert)\n7. [Сколько времени занимает подготовка](#сколько-времени-занимает-подготовка)\n8. [Полезные ресурсы](#-полезные-ресурсы)\n9. [Полезные ссылки](#полезные-ссылки)\n10. [Что почитать и попробовать руками](#что-почитать-и-попробовать-руками)\n11. [Контрибьютинг](#контрибьютинг)\n12. [Лицензия](#лицензия)\n\n## Как работать с репозиторием\n\nСначала читайте вопрос или условие задачи и пробуйте ответить или решить самостоятельно. Не подсматривайте в `src\u002Flib.rs` и в раздел с ответами, пока не получите рабочую версию хотя бы на бумаге. Только потом сравнивайте свой подход с разбором. Часто наивное решение даже компилируется, но содержит UB или проседает под нагрузкой, и весь смысл в том, чтобы увидеть это самому до того, как прочесть ответ.\n\nТесты запускайте через `cargo test -p \u003Ctask-name>`. Где есть бенчмарки, прогоняйте `cargo bench`. Для задач с unsafe обязательно `cargo +nightly miri test`. Для проверки на гонки на сложных async путях полезен `loom`. Clippy и rustfmt держите включёнными в редакторе и в CI.\n\nЕсли готовитесь к конкретному собеседованию, идите не подряд, а по темам, которые наиболее вероятны на этой позиции. Содержание сверху построено так, чтобы было удобно прыгнуть в нужный раздел.\n\n## Структура репозитория\n\n```\nrust-interview-deepdive\u002F\n├── README.md # этот файл с вопросами и ответами\n├── QA.md # тот же материал отдельным файлом\n├── ANSWERS.md # тематические разборы с практическим уклоном\n├── POST.md # черновики и заметки\n├── CONTRIBUTING.md\n├── LICENSE\n├── Cargo.toml # workspace\n├── .github\u002Fworkflows\u002Fci.yml # cargo test + clippy + fmt + miri\n├── tasks\u002F\n│ ├── 01-ownership-move\u002F\n│ │ ├── README.md # условие, разбор, подводные камни\n│ │ ├── src\u002Flib.rs # реализация\n│ │ ├── tests\u002Fit.rs # интеграционные тесты\n│ │ └── benches\u002Fbench.rs # где уместно\n│ └── ... до 40\u002F\n└── theory\u002F\n ├── memory-model.md\n ├── send-sync.md\n ├── pin-unpin.md\n ├── async-runtime.md\n └── unsafe-invariants.md\n```\n\nКаталог `tasks\u002F` это практические задачи с тестами и иногда бенчмарками. Каталог `theory\u002F` это длинные тексты по фундаментальным темам, на которые ссылаются разборы задач.\n\n## Уровни сложности\n\nMiddle. Задачи 1 10. Типовые вопросы про владение, заимствование, базовые трейты, базовый async, простое использование Mutex и каналов. Если эти задачи решаются без подглядывания, можно идти дальше.\n\nSenior. Задачи 11 30. Конкурентность, async на чуть более глубоком уровне, unsafe в прикладных дозах, производительность, тонкости трейтов и дженериков. Здесь начинается то, что отделяет уверенного middle от senior.\n\nStaff. Задачи 31 40. Дизайн API, тёмные углы компилятора, макросы и метапрограммирование, FFI, soundness кастомных абстракций, работа с памятью на уровне модели. Эти задачи редко решаются с первого раза, и это нормально.\n\n## Связанные файлы\n\n[`QA.md`](.\u002FQA.md) дублирует раздел с ответами и разборами ниже отдельным файлом. Удобно открывать прямо с GitHub без необходимости листать README.\n\n[`ANSWERS.md`](.\u002FANSWERS.md) содержит тематические разборы по восьми разделам. Здесь меньше формата вопрос ответ и больше связного текста с упором на практику. Подходит для того, чтобы освежить тему целиком.\n\n[`POST.md`](.\u002FPOST.md) черновики и заметки, которые ещё не превратились в полноценные разборы.\n\nКаталог [`tasks\u002F`](.\u002Ftasks) содержит практические задачи с реализациями и тестами. Идти в задачи имеет смысл после того, как теоретические разделы перестали вызывать вопросы.\n\n## Ответы и разборы 100 вопросов\n\nНиже идут все сто вопросов с собеседований по Rust, на каждый дан развёрнутый ответ с пояснением механики, рабочий пример кода и короткий комментарий, на что в этом примере стоит смотреть. Документ рассчитан на тех, кто уже пишет на Rust и хочет проверить понимание перед собеседованием или просто разложить материал по полкам. Без воды и пересказа учебника, с упором на то, что реально спрашивают и что реально встречается в коде. Восемь разделов идут в логичном порядке от базы к деталям.\n\n### Владение, заимствование, лайфтаймы\n\n#### 1. Что такое владение в Rust и какие у него три правила\n\nВладение - статическая модель управления ресурсами. Компилятор отслеживает, какой биндинг отвечает за освобождение значения, и в точке выхода владельца из области видимости вставляет вызов деструктора. Никакого рантайма, никакого подсчёта ссылок, никакого GC.\n\nТри правила. У каждого значения есть ровно один владелец. В один момент времени владелец только один, передача владения называется move. Когда владелец выходит из скобок, значение дропается.\n\nMove в Rust - побайтовое копирование стекового представления плюс инвалидация исходного биндинга на этапе компиляции. Для `String` это копирование трёх машинных слов (указатель, длина, ёмкость), а не аллокация. Поэтому move дешёвый и не вызывает Clone.\n\n```rust\nfn take(s: String) { println!(\"{s}\"); }\n\nfn main() {\n let a = String::from(\"hi\");\n take(a);\n \u002F\u002F println!(\"{a}\"); \u002F\u002F E0382: borrow of moved value\n}\n```\n\nЧасто путают: move и глубокое копирование. Move не копирует кучу. Глубокое копирование делает `Clone`. Для `Copy`-типов (примитивы, `&T`, кортежи из Copy) семантика побитовая и исходный биндинг остаётся валидным, потому что `Copy` и `Drop` взаимоисключающие.\n\nЧек для ревью: если функция принимает `String` по значению, она забирает владение. Если потом нужно вернуть строку вызывающему, либо возвращайте её из функции, либо принимайте `&str` или `&mut String`.\n\n#### 2. Чем move отличается от copy и какие типы реализуют Copy\n\nMove и Copy на уровне ассемблера делают одно: `memcpy` стекового представления. Разница в том, что компилятор делает с исходным биндингом. После move исходник инвалидирован и обращение даёт E0382. После copy исходник остаётся валидным.\n\nCopy реализуют типы, у которых семантически нет смысла различать оригинал и копию: целочисленные и плавающие, `bool`, `char`, разделяемые ссылки `&T`, неизменяемые сырые указатели, кортежи и массивы из Copy-полей, `Option\u003CT>` и `Result\u003CT, E>` если `T` и `E` тоже Copy.\n\nCopy несовместим с Drop. Если у типа есть деструктор, компилятор не даст реализовать Copy, иначе один и тот же ресурс закрылся бы дважды. Поэтому `String`, `Vec`, `Box`, `File` всегда move-only.\n\n```rust\n#[derive(Copy, Clone)]\nstruct Point { x: f32, y: f32 }\n\nfn main() {\n let p = Point { x: 1.0, y: 2.0 };\n let q = p; \u002F\u002F copy\n let r = p; \u002F\u002F тоже copy, p всё ещё валиден\n println!(\"{} {} {}\", p.x, q.x, r.x);\n}\n```\n\nКогда добавлять Copy. Только для маленьких POD-типов, где побайтовое копирование действительно дешёвое и семантически безопасное. Для `[u8; 4096]` Copy технически возможен, но любое присваивание скопирует 4 КБ - почти всегда это случайный пессимизм.\n\n#### 3. Разница между &T и &mut T, какие правила заимствования действуют одновременно\n\n`&T` - shared reference, `&mut T` - exclusive reference. На уровне типа это разные категории, а не модификатор изменяемости. `&mut` гарантирует эксклюзивный доступ: пока живёт `&mut T`, других ссылок на это значение не существует.\n\nПравило XOR. В каждой точке программы либо одна `&mut T`, либо сколько угодно `&T`. Это инвариант, на котором держится noalias-оптимизация и отсутствие data race в безопасном коде.\n\n```rust\nfn main() {\n let mut v = vec![1, 2, 3];\n let r1 = &v;\n let r2 = &v; \u002F\u002F несколько shared ок\n println!(\"{r1:?} {r2:?}\");\n let m = &mut v; \u002F\u002F r1 и r2 уже не используются благодаря NLL\n m.push(4);\n}\n```\n\nЧастая ловушка: итерация и мутация одновременно.\n\n```rust\nlet mut v = vec![1, 2, 3];\nfor x in &v { \u002F\u002F & v держит shared\n if *x == 2 { v.push(99); } \u002F\u002F E0502\n}\n```\n\nЧинится сбором изменений и применением после цикла, `v.retain`, или `Vec::drain_filter` в зависимости от задачи.\n\nНа уровне LLVM `&mut T` транслируется в указатель с атрибутом `noalias`, что даёт компилятору право переупорядочивать чтения и записи. Нарушение этого инварианта через unsafe приводит к UB, даже если код «выглядит правильно».\n\n#### 4. Что такое лайфтайм и зачем нужны явные аннотации\n\nЛайфтайм - область видимости, в течение которой ссылка гарантированно валидна. Каждая ссылка имеет лайфтайм, выведенный компилятором или указанный явно через `'a`. Лайфтаймы существуют только на этапе проверки типов, в рантайме их нет.\n\nЯвные аннотации нужны там, где компилятор не может однозначно связать лайфтаймы входов и выходов. Самый частый случай: функция возвращает ссылку, и есть несколько кандидатов на входе.\n\n```rust\nfn longest\u003C'a>(a: &'a str, b: &'a str) -> &'a str {\n if a.len() >= b.len() { a } else { b }\n}\n```\n\nБез `'a` компилятор не знает, привязать ли результат к `a`, к `b`, или к их пересечению. Аннотация говорит: возвращаемая ссылка живёт не дольше пересечения лайфтаймов входов.\n\nВажно: аннотация не продлевает жизнь данных, она описывает уже существующие отношения. Если написать `'static` там, где значение живёт меньше, компилятор это поймает.\n\n```rust\nstruct Parser\u003C'src> { input: &'src str, pos: usize }\n```\n\nТут `'src` нужен потому что структура держит ссылку, и компилятор обязан знать, что `Parser` не переживёт `input`.\n\nПравило большого пальца: если у функции или структуры одна входная ссылка и одна выходная, аннотации не нужны (правило elision разруливает само). Если несколько входных ссылок и возвращается ссылка, аннотация почти всегда обязательна.\n\n#### 5. Правила elision лайфтаймов\n\nElision - набор детерминированных правил, по которым компилятор вставляет лайфтаймы за вас. Применяется только в сигнатурах `fn` и `impl`-методах. К телам функций и к struct-определениям не применяется.\n\nПравил три, применяются по порядку.\n\nПервое: каждой входной ссылке без аннотации присваивается свой лайфтайм.\n\nВторое: если ровно одна входная ссылка, её лайфтайм присваивается всем выходным.\n\nТретье: если есть `&self` или `&mut self`, лайфтайм `self` присваивается всем выходным.\n\n```rust\nfn first(s: &str) -> &str { &s[..1] } \u002F\u002F (1) и (2)\nimpl Cache { fn get(&self, key: &str) -> &str { \u002F* *\u002F } } \u002F\u002F (1) и (3)\n```\n\nКогда elision не сработает. Несколько входных ссылок без `&self` - второе правило не применяется. Возвращается ссылка, не связанная ни с одним входом - тоже нужна аннотация (часто это `'static` для строковых литералов).\n\n#### 6. Что такое 'static и почему это два разных смысла\n\n`'static` встречается в двух разных контекстах, и их регулярно путают.\n\nПервый: лайфтайм `'static` для ссылок. `&'static T` указывает на данные, живущие всё время работы программы. Строковые литералы имеют тип `&'static str` потому что лежат в `.rodata`. Также `'static` получают глобальные `static`-переменные и значения, помещённые в `Box::leak`.\n\n```rust\nlet s: &'static str = \"hello\";\nlet leaked: &'static mut String = Box::leak(Box::new(String::from(\"dyn\")));\n```\n\nВторой: bound `T: 'static`. Это значит «тип `T` не содержит нестатических ссылок». Сам объект может быть дропнут хоть сразу, но если внутри есть ссылка, она должна быть `'static`.\n\n```rust\nfn spawn\u003CF: FnOnce() + Send + 'static>(f: F) { \u002F* tokio::spawn *\u002F }\n```\n\nТут `'static` не значит «замыкание живёт вечно». Значит «замыкание не захватывает короткоживущих ссылок». `String` подходит под `T: 'static` потому что владеет своими данными.\n\nЧастая ошибка: объявляют `fn make() -> &'static str` и возвращают строку, собранную в рантайме. Решается возвратом `String`, или `Box::leak` с пониманием, что память не освободится.\n\n#### 7. Что такое NLL и как работает borrow checker сейчас\n\nNLL (Non-Lexical Lifetimes) появились в 2018 edition и фактически переписали borrow checker. До NLL лайфтайм заимствования совпадал с лексической областью видимости: ссылка считалась живой до закрывающей скобки, даже если фактически больше не использовалась. Это приводило к ложным отказам.\n\n```rust\nlet mut v = vec![1, 2, 3];\nlet r = &v[0];\nprintln!(\"{r}\");\nv.push(4); \u002F\u002F до NLL ошибка, после NLL ок: r больше не используется\n```\n\nСейчас borrow checker работает на MIR и считает лайфтайм заимствования по последней точке использования. Технически - анализ потока управления на CFG, где компилятор строит регионы и проверяет, не пересекаются ли несовместимые заимствования.\n\nСледующий шаг - Polonius, реализация borrow checker на datalog. Он принимает несколько паттернов, которые NLL ещё отвергает, в первую очередь возврат ссылок через условные ветви.\n\nесли код выглядит корректным, но NLL не пропускает, помогает либо явное скоупирование в `{ ... }`, либо вынесение части кода в отдельную функцию, чтобы лайфтайм закрылся раньше.\n\n#### 8. Что делает Box и когда он нужен\n\n`Box\u003CT>` - владеющий указатель в кучу. Внутри одно машинное слово - указатель на аллокацию из глобального аллокатора. При дропе `Box` вызывает деструктор `T` и возвращает память аллокатору.\n\nКогда нужен.\n\nРекурсивные типы. `enum List { Cons(i32, Box\u003CList>), Nil }`. Без `Box` размер был бы бесконечен, компилятор не сможет вычислить layout.\n\nБольшие значения. Перемещение `Box\u003C[u8; 1_000_000]>` копирует одно слово, а не мегабайт.\n\nType erasure через trait object. `Box\u003Cdyn Trait>` хранит толстый указатель (data + vtable) и позволяет складывать в одну коллекцию разнородные реализации.\n\nВозврат типа неизвестного размера. `fn make() -> Box\u003Cdyn Future\u003COutput = u32>>` - компилятор не знает размер конкретного future, `Box` решает.\n\n```rust\ntrait Shape { fn area(&self) -> f64; }\nstruct Circle(f64);\nimpl Shape for Circle {\n fn area(&self) -> f64 { std::f64::consts::PI * self.0 * self.0 }\n}\n\nlet shapes: Vec\u003CBox\u003Cdyn Shape>> = vec![Box::new(Circle(1.0))];\n```\n\nЧего `Box` не делает: не даёт shared ownership (для этого `Rc`\u002F`Arc`), не даёт мутацию через shared (для этого `Cell`\u002F`RefCell`), не делает thread-safe (это `Arc` и `Mutex`).\n\n#### 9. Rc и Arc, в чем разница и когда что выбирать\n\n`Rc\u003CT>` и `Arc\u003CT>` - shared-ownership через подсчёт ссылок. Разница в том, как обновляется счётчик.\n\n`Rc` использует обычные неатомарные `usize`-операции. Дешевле на инкременте и декременте, но небезопасен для передачи между потоками. `Rc\u003CT>` не реализует `Send` и не реализует `Sync`, попытка отправить его в `thread::spawn` даст ошибку компиляции.\n\n`Arc` использует атомарные операции (`AtomicUsize` с `Relaxed` на инкременте и `Release`\u002F`Acquire` на декременте). Реализует `Send` и `Sync` при `T: Send + Sync`. Атомарный декремент стоит дороже обычного, особенно при contention из многих потоков.\n\n```rust\nuse std::sync::Arc;\nlet data = Arc::new(vec![1, 2, 3]);\nlet d2 = Arc::clone(&data);\nstd::thread::spawn(move || println!(\"{d2:?}\"));\n```\n\nУ каждого внутри два счётчика, strong и weak. Weak-ссылки нужны для разрыва циклов. `Rc::new_cyclic` и `Arc::new_cyclic` создают значение, которое может ссылаться на самого себя через `Weak`.\n\nКогда что выбирать. По умолчанию однопоточно - `Rc`. Данные пересекают границу потоков - `Arc`. Внутри `tokio::spawn` замыкание должно быть `Send + 'static`, поэтому `Arc`. Иммутабельные read-heavy данные - `Arc` отлично работает. Нужна мутация - `Arc\u003CMutex\u003CT>>` или `Arc\u003CRwLock\u003CT>>`.\n\nПодводный камень: цикл из `Rc`\u002F`Arc` - утечка. Если `A` владеет `Rc\u003CB>`, а `B` владеет `Rc\u003CA>`, счётчики никогда не дойдут до нуля. Дерево с обратными ссылками родителей - классический случай, лечится `Weak` для родительских ссылок.\n\n#### 10. Что такое interior mutability и какие типы её реализуют\n\nInterior mutability - шаблон, при котором значение мутируется через shared-ссылку `&T`. Это нарушает обычное правило XOR на уровне API, но безопасность обеспечивается дополнительной проверкой: в рантайме или через системные примитивы.\n\n`Cell\u003CT>` для Copy-типов. Внутри `UnsafeCell`, API через `get`\u002F`set`\u002F`replace`, без выдачи ссылок наружу. Никакой проверки в рантайме, потому что без ссылок нет и aliasing-проблем.\n\n`RefCell\u003CT>` для произвольных типов. Раздаёт `Ref\u003CT>` и `RefMut\u003CT>` через `borrow` и `borrow_mut`, считает заимствования в рантайме. Нарушение XOR (например, два `borrow_mut` подряд) даёт панику. Не Sync, только для одного потока.\n\n`Mutex\u003CT>` и `RwLock\u003CT>` - многопоточные аналоги. Блокируют поток, реализуют Sync.\n\n`OnceCell\u003CT>` и `LazyLock\u003CT>` для однократной инициализации.\n\n`atomic::Atomic*` для lock-free мутации Copy-типов.\n\n```rust\nuse std::cell::RefCell;\nuse std::collections::HashMap;\n\nstruct Cache { map: RefCell\u003CHashMap\u003CString, String>> }\nimpl Cache {\n fn get_or_insert(&self, k: &str, v: String) -> String {\n let mut m = self.map.borrow_mut();\n m.entry(k.into()).or_insert(v).clone()\n }\n}\n```\n\nГде нужно. Структуры, семантически иммутабельные снаружи, но кэширующие что-то внутри (memoization, ленивая инициализация). Графы и деревья с локальными обновлениями.\n\nВсе эти типы построены на `UnsafeCell` - единственном примитиве языка, через который компилятор разрешает получить `&mut T` из `&UnsafeCell\u003CT>`. Прямое использование `UnsafeCell` требует unsafe и ручного соблюдения правил алиасинга.\n\n#### 11. Что такое Cow и где он реально полезен\n\n`Cow\u003C'a, B>` (Clone-on-Write) - enum `Borrowed(&'a B) | Owned(B::Owned)`. Идея: пока изменения не нужны, держим shared-ссылку; как только нужно мутировать, делаем `to_mut`, который при необходимости клонирует данные в Owned-вариант.\n\nГде применяется на практике.\n\nПарсинг и нормализация. Если вход уже валидный, возвращаем `Cow::Borrowed(input)` без аллокации. Если нужно подправить, переходим в Owned.\n\n```rust\nuse std::borrow::Cow;\n\nfn normalize(s: &str) -> Cow\u003C'_, str> {\n if s.contains('\\r') { Cow::Owned(s.replace('\\r', \"\")) }\n else { Cow::Borrowed(s) }\n}\n```\n\nAPI, который может вернуть либо позаимствованную, либо собственную строку. `Path::to_string_lossy` возвращает `Cow\u003Cstr>`: если путь валидный UTF-8 - `Borrowed`, иначе аллоцирует с заменой невалидных байтов.\n\nDeserialization с zero-copy. `serde` поддерживает `Cow\u003C'a, str>`: если входной буфер требует обработки эскейпов - Owned, если нет - Borrowed на исходный буфер.\n\nГде не помогает. Если на горячем пути почти всегда нужна мутация, `Cow` только добавляет ветвление. Когда заведомо нужен `String`, его и берите.\n\n#### 12. Что такое Drop и можно ли вызвать его руками\n\n`Drop` - трейт с одним методом `fn drop(&mut self)`. Компилятор вызывает его в точке выхода значения из области владения. Деструктор гарантированно вызывается при нормальном завершении и при unwinding-панике (если panic=unwind), но не вызывается при `mem::forget` и при abort процесса.\n\nРуками `drop()` метод трейта вызвать нельзя - это даст E0040. Если бы было можно, после ручного вызова компилятор всё равно вставил бы свой, и деструктор отработал бы дважды. Для досрочного освобождения есть свободная функция `std::mem::drop(x)`, которая просто принимает значение по move и роняет его на своём же закрытии скобок.\n\n```rust\nstruct Guard;\nimpl Drop for Guard { fn drop(&mut self) { println!(\"bye\"); } }\n\nfn main() {\n let g = Guard;\n drop(g); \u002F\u002F вызовет деструктор сейчас\n println!(\"after\");\n}\n```\n\nДетали. Порядок дропа полей структуры - в порядке объявления. Порядок дропа локальных переменных - в обратном порядке создания. Это критично для RAII-гардов: лок должен дропаться позже данных, которые он защищает.\n\nВ `Drop::drop` нельзя паниковать, если уже идёт паника: double panic ведёт к abort. Поэтому в деструкторах не вызывают `.unwrap` и не делают потенциально падающих операций.\n\nКонфликт с move. После `drop(x)` биндинг инвалидирован, к нему уже не обратиться. И тип с `Drop` не может быть `Copy`.\n\n#### 13. Move в замыканиях и Fn, FnMut, FnOnce\n\nЗамыкание - анонимный тип с реализацией одного из трёх трейтов: `Fn`, `FnMut` или `FnOnce`. Компилятор автоматически выбирает наиболее «лёгкий» из доступных, исходя из того, как замыкание использует захваченные переменные.\n\n`FnOnce` вызывается ровно один раз, потому что забирает захваченные значения по move внутрь себя или внутрь вызова. `FnMut` вызывается многократно и может мутировать захваченные переменные, требует `&mut` к самому замыканию. `Fn` вызывается многократно и не мутирует, достаточно `&`.\n\nИерархия: `Fn: FnMut: FnOnce`. Если замыкание реализует `Fn`, оно автоматически и `FnMut`, и `FnOnce`.\n\n```rust\nlet s = String::from(\"hi\");\nlet f1 = || println!(\"{s}\"); \u002F\u002F Fn: захват по &\nlet mut v = vec![1];\nlet mut f2 = || v.push(2); \u002F\u002F FnMut: захват по &mut\nlet f3 = move || drop(s); \u002F\u002F FnOnce: потребляет s\n```\n\nКлючевое слово `move` форсирует захват по значению независимо от того, как используются переменные. Это нужно для `thread::spawn` и `tokio::spawn`, потому что замыкание должно быть `'static` и не может держать ссылок на стек породившего потока.\n\nПодводный камень: `move` не делает замыкание `FnOnce` автоматически. Если внутри `move`-замыкания только читаются `Copy`-значения, оно останется `Fn`. Тип трейта определяется тем, что замыкание делает с захваченным, а `move` только меняет способ захвата.\n\n#### 14. Что такое PhantomData и где его применять\n\n`PhantomData\u003CT>` - маркер нулевого размера, который говорит компилятору: «считай, что эта структура владеет или использует `T`, хотя физически его не хранит». Без хранения данных, но с эффектом на dropck, variance и auto traits.\n\nГде это пригождается.\n\nПривязка лайфтайма к структуре, которая держит сырой указатель. Без `PhantomData\u003C&'a T>` компилятор не свяжет `'a` со структурой, и можно получить висящий указатель без жалоб от borrow checker.\n\n```rust\nstruct Slice\u003C'a, T> {\n ptr: *const T,\n len: usize,\n _marker: std::marker::PhantomData\u003C&'a T>,\n}\n```\n\nType-state и типизированные ID. Разделить `Id\u003CUser>` и `Id\u003COrder>`, хотя внутри обе `u64`. PhantomData делает их разными типами без накладных расходов.\n\n```rust\nuse std::marker::PhantomData;\nstruct Id\u003CT> { value: u64, _t: PhantomData\u003CT> }\nstruct User; struct Order;\n```\n\nКонтроль variance и Send\u002FSync. `PhantomData\u003C*const T>` снимает `Send`\u002F`Sync`, `PhantomData\u003Cfn() -> T>` делает тип ковариантным по `T`. Это инструменты для авторов unsafe-кода, которые знают, какую дисперсию они хотят.\n\nЧто эта штука не делает. `PhantomData\u003CT>` не вызывает деструктор `T`, потому что значение не хранится. Если ваш тип логически владеет `T` и должен его дропать, нужен `PhantomData\u003CT>` плюс корректная работа с dropck, либо хранение реального `T`.\n\n#### 15. Borrow, AsRef, Deref, чем они отличаются\n\nВсе три позволяют получить ссылку, но решают разные задачи и имеют разные контракты.\n\n`Deref` - «прозрачное разыменование». Реализуя `Deref\u003CTarget = T>`, вы заявляете, что ваш тип семантически является `T`. Компилятор применяет deref coercion: `&Box\u003CT>` неявно становится `&T`, `&String` становится `&str`. Метод-резолюция тоже идёт по deref-цепочке. Реализуется только для smart pointer-подобных типов; реализация для произвольных контейнеров считается анти-паттерном.\n\n```rust\nlet s = String::from(\"hi\");\nlet r: &str = &s; \u002F\u002F deref coercion\n```\n\n`AsRef\u003CT>` - «дешёвое преобразование ссылки в ссылку». Не требует identity: `AsRef\u003Cstr>` есть у `String`, у `&str`, у `PathBuf`. Используется в обобщённых API: `fn read\u003CP: AsRef\u003CPath>>(p: P)` принимает что угодно, что превращается в `&Path`.\n\n`Borrow\u003CT>` похож на `AsRef`, но с дополнительным контрактом: `Hash`, `Eq`, `Ord` у `Self` и у `Borrow::Target` должны давать одинаковые результаты. Это нужно для `HashMap::get`: ключ типа `String` ищется по `&str`, и хэш строки должен совпадать с хэшем её `&str`-представления.\n\n```rust\nuse std::collections::HashMap;\nlet mut m: HashMap\u003CString, i32> = HashMap::new();\nm.insert(\"a\".into(), 1);\nm.get(\"a\"); \u002F\u002F &str: работает через Borrow\u003Cstr> для String\n```\n\nКогда что выбирать. Smart pointer - `Deref`. Принять любое строкоподобное в API - `AsRef\u003Cstr>` или `AsRef\u003CPath>`. Ключи коллекций с альтернативным lookup - `Borrow`.\n\n### Типы, трейты, обобщения\n\n#### 16. Чем структура отличается от enum\n\nСтруктура это произведение типов. Все поля присутствуют одновременно. Enum это сумма типов. В любой момент активен ровно один вариант. Эту разницу важно понимать, потому что в Rust enum полноценный алгебраический тип, не как в C. Каждый вариант может нести произвольные данные. Память enum занимает по размеру самого большого варианта плюс дискриминант. Компилятор умеет ужимать представление, если есть niche, например None в Option\u003C&T> кодируется нулевым указателем.\n\n```rust\nenum Shape {\n Circle(f64),\n Rect { w: f64, h: f64 },\n}\n\nfn area(s: &Shape) -> f64 {\n match s {\n Shape::Circle(r) => std::f64::consts::PI * r * r,\n Shape::Rect { w, h } => w * h,\n }\n}\n\nfn main() { println!(\"{}\", area(&Shape::Rect { w: 2.0, h: 3.0 })); }\n```\n\nmatch заставляет покрыть все варианты, и забыть про новый случай при расширении enum компилятор не даст.\n\n#### 17. Что такое трейт и чем он отличается от интерфейса в Java\n\nТрейт это набор методов и ассоциированных элементов, который тип может реализовать. От интерфейсов в Java отличается тем, что реализацию можно писать в другом модуле, чем сам тип, при условии соблюдения orphan rule. Трейты поддерживают дефолтные методы, ассоциированные типы и константы, могут быть обобщенными. Диспетчеризация по умолчанию статическая, через мономорфизацию, но можно сделать динамическую через dyn Trait.\n\n```rust\ntrait Greet {\n fn name(&self) -> &str;\n fn hello(&self) { println!(\"hi {}\", self.name()); }\n}\n\nstruct Cat;\nimpl Greet for Cat { fn name(&self) -> &str { \"cat\" } }\n\nfn main() { Cat.hello(); }\n```\n\nДефолтный hello переиспользуется любым типом, который дал свою name.\n\n#### 18. Статическая и динамическая диспетчеризация, плюсы и минусы\n\nСтатическая диспетчеризация это generics с мономорфизацией. Компилятор генерирует свою копию функции под каждый конкретный тип. Быстрее в рантайме, заинлайнено, но раздувает бинарник и увеличивает время компиляции. Динамическая диспетчеризация это dyn Trait. Используется vtable, вызов через указатель. Бинарник меньше, типы можно смешивать в одном Vec\u003CBox\u003Cdyn Trait>>, но есть стоимость косвенного вызова и невозможность инлайна.\n\n```rust\ntrait Op { fn run(&self) -> i32; }\nstruct A; impl Op for A { fn run(&self) -> i32 { 1 } }\nstruct B; impl Op for B { fn run(&self) -> i32 { 2 } }\n\nfn sum_static\u003CT: Op>(x: &T, y: &T) -> i32 { x.run() + y.run() }\nfn sum_dyn(ops: &[Box\u003Cdyn Op>]) -> i32 { ops.iter().map(|o| o.run()).sum() }\n```\n\nВыбор в проде такой. Если типы известны и набор маленький, generics. Если плагины, гетерогенные коллекции, конфигурация в рантайме, dyn.\n\n#### 19. Что такое object safety и почему не любой трейт может быть dyn\n\nЧтобы трейт можно было использовать как dyn Trait, он должен быть object safe. Главные ограничения такие. Методы не принимают и не возвращают Self по значению, не используют дженерики, не имеют where Self: Sized без отдельного маркера. Self не появляется в ассоциированных константах. Причина в том, что для vtable нужен фиксированный набор слотов с известными сигнатурами. Если бы метод принимал Self, размер аргумента зависел бы от конкретного типа, и универсальный vtable стал бы невозможен.\n\n```rust\ntrait Bad { fn make() -> Self; } \u002F\u002F не object safe\ntrait Good { fn name(&self) -> &str; } \u002F\u002F object safe\n\nfn use_good(x: &dyn Good) { println!(\"{}\", x.name()); }\n```\n\nЕсли очень хочется иметь и dyn, и generic метод, обычно разделяют трейт на два или используют where Self: Sized для отдельных методов.\n\n#### 20. Ассоциированные типы и дженерик параметры трейта, в чем разница\n\nГенерик параметр трейта позволяет реализовать трейт несколько раз для одного типа с разными параметрами. Ассоциированный тип фиксируется в реализации, один тип это одна Item. Iterator имеет ассоциированный Item, потому что для одного типа естественно одно Item. From наоборот имеет generic параметр, потому что один тип может конвертироваться из многих других.\n\n```rust\ntrait Counter { type Item; fn next(&mut self) -> Option\u003CSelf::Item>; }\n\nstruct UpTo { i: u32, end: u32 }\nimpl Counter for UpTo {\n type Item = u32;\n fn next(&mut self) -> Option\u003Cu32> {\n if self.i \u003C self.end { self.i += 1; Some(self.i - 1) } else { None }\n }\n}\n```\n\nЕсли бы Item был обычным параметром, в коде пришлось бы постоянно его уточнять, и эргономика провисла.\n\n#### 21. Что такое orphan rule и зачем он нужен\n\nПравило сирот гласит, что реализовать трейт для типа можно только если либо трейт, либо тип определены в том же крейте. Это нужно, чтобы две сторонние библиотеки не сделали независимо несовместимые реализации одного и того же трейта для одного и того же типа. Когерентность системы трейтов держится именно на этом правиле. Обход обычно делают через newtype.\n\n```rust\nuse std::fmt::Display;\n\nstruct Wrap(Vec\u003Ci32>);\n\nimpl Display for Wrap {\n fn fmt(&self, f: &mut std::fmt::Formatter\u003C'_>) -> std::fmt::Result {\n write!(f, \"{:?}\", self.0)\n }\n}\n```\n\nWrap локальный, поэтому реализация Display для него разрешена.\n\n#### 22. Что такое newtype и зачем он применяется\n\nNewtype это структура с одним полем, оборачивающая другой тип. Применений много. Соблюдение orphan rule, типобезопасность доменных значений, скрытие представления, разные реализации трейтов для одного базового типа. В рантайме newtype бесплатен, потому что layout совпадает с внутренним типом, и часто компилятор инлайнит обертку полностью.\n\n```rust\nstruct Meters(f64);\nstruct Seconds(f64);\n\nfn speed(d: Meters, t: Seconds) -> f64 { d.0 \u002F t.0 }\n\nfn main() { println!(\"{}\", speed(Meters(100.0), Seconds(9.58))); }\n```\n\nПерепутать метры и секунды компилятор не даст. Это дешевая статическая защита от глупых багов.\n\n#### 23. Что такое derive и какие трейты обычно автоматически выводятся\n\n#[derive(...)] это атрибут, который запускает соответствующий процедурный макрос и генерирует реализацию трейта. Стандартные derivable трейты это Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord. Они работают, если у всех полей структуры или вариантов enum уже есть нужная реализация. Помимо стандартных, derive часто используют для Serialize, Deserialize, thiserror::Error, strum.\n\n```rust\n#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]\nstruct Point { x: i32, y: i32 }\n\nfn main() {\n let p = Point::default();\n println!(\"{:?}\", p.clone());\n}\n```\n\nDerive это про эргономику и про защиту от ошибок при рефакторинге. Добавили поле, и все реализации обновились автоматически.\n\n#### 24. Что такое impl Trait в позиции возврата и в позиции аргумента\n\nВ позиции возврата impl Trait означает один конкретный, но непоименованный тип, реализующий трейт. Используется чаще всего для возврата итераторов и замыканий, чтобы не писать длинные типы. Тип фиксируется внутри функции и одинаков для всех вызовов. В позиции аргумента impl Trait это просто короткая запись для дженерика, эквивалентно T: Trait.\n\n```rust\nfn iter_evens(v: &[i32]) -> impl Iterator\u003CItem = &i32> {\n v.iter().filter(|x| *x % 2 == 0)\n}\n\nfn log(item: impl std::fmt::Display) { println!(\"{}\", item); }\n```\n\nС возвращаемым impl Trait нужно помнить, что лайфтаймы захваченных аргументов попадают в скрытый тип. С 2024 edition правила захвата стали более явными.\n\n#### 25. dyn Trait в полях структуры, что нужно знать\n\ndyn Trait это unsized тип. Положить его в поле структуры напрямую нельзя, нужен указатель. Чаще всего это Box\u003Cdyn Trait>, реже &dyn Trait, Rc или Arc. При выборе важно решить, кто владеет объектом. Если структура живет долго и должна владеть, обычно Box или Arc. Если структура только использует чужой объект, &dyn с лайфтаймом.\n\n```rust\ntrait Logger { fn log(&self, msg: &str); }\n\nstruct App { logger: Box\u003Cdyn Logger + Send + Sync> }\n\nstruct Console;\nimpl Logger for Console { fn log(&self, m: &str) { println!(\"{}\", m); } }\n\nfn main() {\n let app = App { logger: Box::new(Console) };\n app.logger.log(\"ok\");\n}\n```\n\nТипичный паттерн внедрения зависимостей. Send и Sync добавлены, чтобы App можно было передавать между потоками.\n\n#### 26. Что такое Sized, ?Sized и зачем это\n\nSized это трейт, который реализуют все типы с известным на этапе компиляции размером. Это i32, String, &T, Box\u003CT>. Не Sized это str, [T], dyn Trait. С такими типами нельзя работать по значению, только через указатель. По умолчанию у любого дженерика стоит неявное T: Sized. Если нужно принимать DST, явно пишут T: ?Sized.\n\n```rust\nfn print_ref\u003CT: ?Sized + std::fmt::Debug>(x: &T) {\n println!(\"{:?}\", x);\n}\n\nfn main() {\n print_ref::\u003Cstr>(\"hello\");\n print_ref::\u003C[i32]>(&[1, 2, 3]);\n}\n```\n\nБез ?Sized такой код не собрался бы для str и среза.\n\n#### 27. Что такое where clause и когда она нужнее, чем inline ограничения\n\nWhere позволяет описывать ограничения отдельно от списка параметров. Это удобно, когда ограничений много или они сложные. Например, ограничение на ассоциированный тип. T: Iterator, T::Item: Display. Записать это в угловых скобках громоздко. where в большом коде еще полезен тем, что выравнивает ограничения вертикально и упрощает диффы.\n\n```rust\nuse std::fmt::Display;\n\nfn print_all\u003CI>(iter: I) where I: IntoIterator, I::Item: Display {\n for x in iter { println!(\"{}\", x); }\n}\n\nfn main() { print_all(vec![\"a\", \"b\"]); }\n```\n\nПри усложнении сигнатур where почти всегда читается лучше, чем длинная строка в угловых скобках.\n\n#### 28. Что такое coherence и блансировка реализаций\n\nКогерентность это свойство системы трейтов, при котором для каждой пары тип трейт существует не более одной реализации, видимой во всей программе. Без когерентности один и тот же вызов в разных местах мог бы попадать в разные реализации, и поведение программы стало бы непредсказуемым. На уровне правил это поддерживается orphan rule и запретом перекрывающихся реализаций. Specialization, который частично позволяет иметь более специфичные реализации, до сих пор в стабильном виде не доехал именно по этой причине.\n\nКомпилятор отказывается принимать любую пару реализаций, которые потенциально могут пересекаться.\n\n```rust\n\u002F\u002F Не соберется, если раскомментировать обе реализации.\n\u002F\u002F trait Foo {}\n\u002F\u002F impl\u003CT> Foo for T {}\n\u002F\u002F impl Foo for i32 {}\n```\n\nКомментарий простой. Если хочется специализировать, сейчас приходится обходить это вручную через mini specialization паттерны.\n\n#### 29. Что такое supertrait и зачем он бывает нужен\n\nSupertrait это требование, что любой тип, реализующий данный трейт, должен также реализовывать другой. Записывается как trait Child: Parent. Это позволяет вызывать методы родителя на &dyn Child и сужает множество реализаций. Полезно, когда родительский трейт дает базовые методы, а дочерний расширяет логику.\n\n```rust\nuse std::fmt::Display;\n\ntrait Pretty: Display {\n fn pretty(&self) -> String { format!(\"=> {}\", self) }\n}\n\nimpl Pretty for i32 {}\n\nfn main() { println!(\"{}\", 7i32.pretty()); }\n```\n\nВ методе pretty можно использовать любой метод Display прямо через self.\n\n#### 30. Что такое blanket implementation и где она применяется в стандартной библиотеке\n\nBlanket impl это реализация трейта для всех типов, удовлетворяющих какому то ограничению. Самые известные примеры из стандартной библиотеки это impl\u003CT: Display> ToString for T и impl\u003CT, U> Into\u003CU> for T where U: From\u003CT>. Первый автоматически дает метод to_string для всего, что умеет печататься. Второй автоматически дает Into, как только написана From. Это очень мощный механизм, но он же конфликтует с orphan rule, потому что после blanket добавить ручную реализацию для пересекающегося типа уже нельзя.\n\n```rust\nstruct Celsius(f64);\nstruct Fahrenheit(f64);\n\nimpl From\u003CCelsius> for Fahrenheit {\n fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 1.8 + 32.0) }\n}\n\nfn main() {\n let f: Fahrenheit = Celsius(100.0).into(); \u002F\u002F into получено через blanket\n println!(\"{}\", f.0);\n}\n```\n\nЗдесь руками написана только From, а Into подъехала бесплатно через стандартный blanket.\n\n### Конкурентность и параллелизм\n\n#### 31. Send и Sync, в чем разница и как они выводятся\n\nSend означает, что значение типа можно безопасно передать в другой поток. Sync означает, что &T можно безопасно делить между потоками, то есть T реализует Sync, если &T реализует Send. Оба трейта auto traits, компилятор сам выводит реализацию по полям. Не Send Rc, потому что счетчик не атомарный. Не Send и не Sync сырые указатели в обертках вроде Cell, потому что мутируют через shared ссылку без синхронизации.\n\n```rust\nuse std::sync::Arc;\nuse std::thread;\n\nfn main() {\n let v = Arc::new(vec![1, 2, 3]);\n let v2 = v.clone();\n thread::spawn(move || println!(\"{:?}\", v2)).join().unwrap();\n}\n```\n\nЕсли заменить Arc на Rc, компилятор откажется собирать, потому что Rc не Send.\n\n#### 32. Чем Mutex отличается от RwLock и когда какой выбирать\n\nMutex дает эксклюзивный доступ. В любой момент только один поток внутри секции. RwLock разрешает либо одного писателя, либо несколько читателей одновременно. RwLock дороже на вход и выход, и при коротких критических секциях обычно проигрывает Mutex даже на читающей нагрузке. Имеет смысл, когда читатели держат лок долго, а писатели редки.\n\n```rust\nuse std::sync::RwLock;\n\nfn main() {\n let lock = RwLock::new(0);\n {\n let r1 = lock.read().unwrap();\n let r2 = lock.read().unwrap();\n println!(\"{} {}\", *r1, *r2);\n }\n *lock.write().unwrap() = 5;\n}\n```\n\nИз практики. Если не уверены, начинать с Mutex. Менять на RwLock только после измерения, и не в любом стиле, а с учетом приоритета писателей, иначе можно получить starvation.\n\n#### 33. Что такое poisoning у Mutex и как с ним жить\n\nЕсли поток упал с паникой, удерживая Mutex, лок становится отравленным. Все последующие попытки lock возвращают Err. Это сделано, чтобы случайно не работать с потенциально невалидным состоянием. Часто разумно прочитать данные через into_inner или PoisonError::into_inner и продолжить, если инвариант не нарушен. В новых проектах parking_lot::Mutex обычно предпочитают, у него нет poisoning и он быстрее.\n\n```rust\nuse std::sync::Mutex;\n\nfn main() {\n let m = Mutex::new(0);\n let _ = std::panic::catch_unwind(|| {\n let _g = m.lock().unwrap();\n panic!(\"boom\");\n });\n match m.lock() {\n Ok(g) => println!(\"{}\", *g),\n Err(p) => println!(\"poisoned, value = {}\", *p.into_inner()),\n }\n}\n```\n\nКлюч в том, что Rust не дает молча проигнорировать факт паники под локом.\n\n#### 34. Channel в std::sync::mpsc, его особенности\n\nКанал mpsc это многопроизводитель один потребитель. Передача по значению, отправитель Send, приемник Send но не Sync. Каналы строятся вокруг неограниченной очереди. Это удобно, но опасно. Если получатель медленнее отправителей, память растет. На практике часто используют crossbeam-channel и tokio mpsc, у них есть bounded варианты, select, поддержка отмены.\n\n```rust\nuse std::sync::mpsc;\nuse std::thread;\n\nfn main() {\n let (tx, rx) = mpsc::channel();\n for i in 0..3 {\n let tx = tx.clone();\n thread::spawn(move || tx.send(i).unwrap());\n }\n drop(tx);\n for v in rx { println!(\"{}\", v); }\n}\n```\n\nЦикл for v in rx завершается, когда все клоны tx уничтожены. Поэтому важно сбросить исходный tx, иначе будет дедлок.\n\n#### 35. Что такое scoped threads и зачем они появились в стандартной библиотеке\n\nДо стабилизации std::thread::scope для передачи ссылок в поток приходилось либо использовать Arc, либо crate crossbeam. Scoped threads гарантируют, что все потоки внутри scope завершатся до выхода из него, поэтому компилятор разрешает заимствовать стек вызывающего без 'static. Это убрало целый класс лишних аллокаций и упростило код.\n\n```rust\nuse std::thread;\n\nfn main() {\n let data = vec![1, 2, 3, 4];\n thread::scope(|s| {\n s.spawn(|| println!(\"{:?}\", &data[..2]));\n s.spawn(|| println!(\"{:?}\", &data[2..]));\n });\n}\n```\n\nПосле scope data все еще доступна. Никаких клонов и Arc.\n\n#### 36. Atomic типы, что такое memory ordering и какие порядки бывают\n\nAtomic типы дают неблокирующие операции с явным memory ordering. Доступные порядки это Relaxed, Acquire, Release, AcqRel, SeqCst. Relaxed гарантирует только атомарность самой операции. Acquire запрещает реордеринг последующих чтений выше себя. Release запрещает реордеринг предыдущих записей ниже себя. SeqCst дает глобальный порядок. На практике большинство кода обходится парой Acquire и Release для flag паттернов, а SeqCst используется, когда лень думать, либо когда действительно нужен тотальный порядок.\n\n```rust\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse std::thread;\n\nstatic CNT: AtomicUsize = AtomicUsize::new(0);\n\nfn main() {\n let h: Vec\u003C_> = (0..4).map(|_| thread::spawn(|| {\n for _ in 0..1000 { CNT.fetch_add(1, Ordering::Relaxed); }\n })).collect();\n for t in h { t.join().unwrap(); }\n println!(\"{}\", CNT.load(Ordering::Relaxed));\n}\n```\n\nДля простого счетчика Relaxed достаточно, потому что важна только финальная сумма, а не порядок инкрементов.\n\n#### 37. Что такое гонка данных и чем она отличается от race condition\n\nГонка данных это конкретная ситуация, когда два потока обращаются к одной памяти без синхронизации, и хотя бы один пишет. В безопасном Rust таких гонок нет благодаря системе типов и трейтам Send Sync. Race condition это более широкое понятие, логическая гонка по времени. Например, проверили условие, потом что то сделали, а между этими шагами другой поток уже изменил состояние. Гонок данных Rust не допустит, race condition в логике вполне может быть.\n\n```rust\nuse std::sync::{Arc, Mutex};\nuse std::thread;\n\nfn main() {\n let m = Arc::new(Mutex::new(false));\n let m2 = m.clone();\n let h = thread::spawn(move || {\n let mut g = m2.lock().unwrap();\n if !*g { *g = true; }\n });\n h.join().unwrap();\n println!(\"{}\", *m.lock().unwrap());\n}\n```\n\nЛок здесь решает не гонку данных, а именно атомарность пары чтение запись.\n\n#### 38. Что такое deadlock и как его избегать в Rust\n\nДедлок это ситуация, когда несколько потоков ждут друг друга по кругу и ни один не может продвинуться. Классически возникает при захвате нескольких локов в разном порядке. Базовый рецепт. Всегда брать локи в одном фиксированном порядке. Минимизировать критические секции. По возможности использовать try_lock и таймауты. В асинхронном коде дедлок легко получить, удерживая лок через await. Лучше явно отпустить лок перед await.\n\n```rust\nuse std::sync::Mutex;\n\nfn safe(m: &Mutex\u003Ci32>) -> i32 {\n let v = { let g = m.lock().unwrap(); *g };\n v + 1\n}\n\nfn main() {\n let m = Mutex::new(10);\n println!(\"{}\", safe(&m));\n}\n```\n\nЛок берется в блоке и сразу отпускается. Дальнейшая логика выполняется без удерживания.\n\n#### 39. Rayon, как он работает и где его уместно применять\n\nRayon это библиотека параллельной обработки данных с work stealing. Главная сильная сторона это адаптер par_iter, который превращает обычный итератор в параллельный. Под капотом он рекурсивно делит работу на пары и распределяет задачи между потоками пула. Хорошо подходит для CPU bound задач над коллекциями. Плохо подходит для IO, для смешанных рабочих нагрузок с блокировками и для задач с сильной зависимостью между шагами.\n\n```rust\nuse rayon::prelude::*;\n\nfn main() {\n let sum: u64 = (1u64..=1_000_000).into_par_iter().map(|x| x * x).sum();\n println!(\"{}\", sum);\n}\n```\n\nЕсли в map встретится блокирующий вызов, поток пула будет занят, и parallelism деградирует. Это типичная ловушка при смешении rayon с синхронным IO.\n\n#### 40. Что такое work stealing и почему он эффективен\n\nWork stealing это стратегия планирования, при которой каждый поток имеет собственную очередь задач и берет работу с ее головы. Когда у потока закончились свои задачи, он крадет задачу с хвоста очереди другого потока. Это снижает конкуренцию за общую очередь, дает хорошую локальность, потому что свежие задачи берутся свои, и при этом распределяет нагрузку. На этом принципе работают rayon, tokio multi thread, async-std.\n\nПрактический вывод. Если задачи короткие и однородные, work stealing справляется почти идеально. Если задачи разной длины, важно избегать слишком длинных, иначе они тормозят балансировку. В tokio то же самое означает не блокировать поток.\n\n#### 41. Что такое thread pool и почему ручное spawn не всегда хорошая идея\n\nСоздание потока операции не бесплатно. Это сискол, стек, регистрация в ядре. Если задачи короткие, тратить отдельный поток на каждую расточительно. Thread pool создает потоки заранее и переиспользует их. В Rust типовые решения это rayon::ThreadPool, tokio::runtime, threadpool. Спавн потока руками оправдан для долгоживущих сущностей, например для фонового логгера или для отдельного цикла обработки IO.\n\n```rust\nuse rayon::ThreadPoolBuilder;\n\nfn main() {\n let pool = ThreadPoolBuilder::new().num_threads(4).build().unwrap();\n let r = pool.install(|| (0..100).into_iter().sum::\u003Ci32>());\n println!(\"{}\", r);\n}\n```\n\nЯвный пул удобен, когда нужно отделить горячие задачи от фоновых.\n\n#### 42. Что такое barrier и когда он нужен\n\nБарьер синхронизирует группу потоков. Все потоки доходят до барьера и ждут, пока не подойдут все остальные, после чего проходят дальше одновременно. Используется в симуляциях по шагам, в параллельных алгоритмах, где фаза вычисления должна полностью закончиться до начала следующей. В Rust есть std::sync::Barrier.\n\n```rust\nuse std::sync::{Arc, Barrier};\nuse std::thread;\n\nfn main() {\n let b = Arc::new(Barrier::new(3));\n let mut h = vec![];\n for i in 0..3 {\n let b = b.clone();\n h.push(thread::spawn(move || {\n println!(\"{} before\", i);\n b.wait();\n println!(\"{} after\", i);\n }));\n }\n for t in h { t.join().unwrap(); }\n}\n```\n\nВывод гарантирует, что все before напечатаются до любого after.\n\n#### 43. Что такое condvar и когда он лучше, чем busy wait\n\nCondvar это условная переменная. Поток, удерживая лок, ждет условие через wait. На время wait лок отпускается. Другой поток меняет состояние и вызывает notify_one или notify_all. После пробуждения wait автоматически снова берет лок. Это правильный способ ждать события, без сжигания процессора в цикле проверки.\n\n```rust\nuse std::sync::{Arc, Condvar, Mutex};\nuse std::thread;\n\nfn main() {\n let pair = Arc::new((Mutex::new(false), Condvar::new()));\n let pair2 = pair.clone();\n thread::spawn(move || {\n let (m, cv) = &*pair2;\n let mut g = m.lock().unwrap();\n *g = true;\n cv.notify_one();\n });\n let (m, cv) = &*pair;\n let mut g = m.lock().unwrap();\n while !*g { g = cv.wait(g).unwrap(); }\n println!(\"started\");\n}\n```\n\nПроверка в while нужна потому, что бывают spurious wakeups, и одного wait недостаточно.\n\n#### 44. Что такое spinlock и когда он оправдан\n\nSpinlock это лок, который вместо блокировки потока крутится в цикле проверки. Имеет смысл только тогда, когда критическая секция очень короткая и переключение контекста было бы дороже самого ожидания. В прикладном коде это редкость. В драйверах и низкоуровневых либах встречается. В Rust стандартного spinlock нет, есть параметризованные реализации в crate spin. Но в большинстве случаев правильный ответ это parking_lot::Mutex, который сам решает, спинить или парковать.\n\n#### 45. Что такое thread local и где он реально нужен\n\nThread local это значение, у которого свой экземпляр в каждом потоке. В Rust это макрос thread_local! и тип LocalKey. Полезен для аллокаторов, для пер потокового кэша, для трассировки, для случайных генераторов. Важно понимать, что данные thread local не Send. Если попытаться унести из них что то в другой поток, надо клонировать.\n\n```rust\nuse std::cell::RefCell;\n\nthread_local! {\n static COUNTER: RefCell\u003Cu64> = RefCell::new(0);\n}\n\nfn bump() { COUNTER.with(|c| *c.borrow_mut() += 1); }\n\nfn main() {\n bump(); bump();\n COUNTER.with(|c| println!(\"{}\", c.borrow()));\n}\n```\n\nЭто частый паттерн для статистики и для тестового хука вместо глобального состояния.\n\n### Async и runtime\n\n#### 46. Что такое async fn и во что он превращается компилятором\n\nasync fn это синтаксический сахар над функцией, возвращающей impl Future. Тело функции трансформируется в стейт машину. Каждое .await это точка приостановки. Локальные переменные, живущие через .await, превращаются в поля стейт машины. Сама функция при вызове не делает работу, а возвращает фьючу. Работа начинается только когда фьючу опрашивает рантайм.\n\n```rust\nasync fn add(a: u32, b: u32) -> u32 { a + b }\n\n#[tokio::main]\nasync fn main() {\n let f = add(1, 2); \u002F\u002F ничего еще не вычислено\n let r = f.await; \u002F\u002F только сейчас полл и результат\n println!(\"{}\", r);\n}\n```\n\nЭта ленивость отличает Rust от языков, где async eager.\n\n#### 47. Что такое Future и как работает poll\n\nFuture это трейт с одним методом poll. Поллер передает Context с Waker. Реализация делает шаг вперед и возвращает Poll::Ready, если работа закончена, или Poll::Pending, если нужно подождать. Если вернули Pending, реализация обязана где то сохранить waker и вызвать его, когда сможет продвинуться. Иначе фьюча зависнет. Рантайм не опрашивает фьючи в цикле, он реагирует на пробуждения.\n\n```rust\nuse std::future::Future;\nuse std::pin::Pin;\nuse std::task::{Context, Poll};\n\nstruct Yield(bool);\nimpl Future for Yield {\n type Output = ();\n fn poll(mut self: Pin\u003C&mut Self>, cx: &mut Context\u003C'_>) -> Poll\u003C()> {\n if self.0 { Poll::Ready(()) } else {\n self.0 = true;\n cx.waker().wake_by_ref();\n Poll::Pending\n }\n }\n}\n```\n\nYield один раз отдает управление и сразу будит сам себя. Простой способ дать рантайму прокрутить очередь.\n\n#### 48. Tokio и async-std, в чем разница и что выбирать\n\nTokio это де факто стандарт. Большой набор примитивов, мощный планировщик, экосистема. async-std ставил целью повторить API стандартной библиотеки, но в практике почти весь крупный код в индустрии написан под tokio. Сейчас async-std в значительной мере legacy. Выбор простой. Если проект не имеет жестких требований, берется tokio. Если хочется минимального рантайма для embedded, есть smol, embassy.\n\n#### 49. Что такое executor и reactor в async\n\nExecutor отвечает за то, чтобы поллить фьючи, когда их нужно поллить. Reactor отвечает за регистрацию событий ввода вывода в ядре и за пробуждение wakers, когда событие пришло. В tokio это реализовано через mio. Когда сокет становится готов, mio будит соответствующий waker, executor добавляет таску в очередь, и фьюча получает следующий polл.\n\nПонимать эту пару важно, потому что многие странности async объясняются разделением ответственности. Если фьюча не двигается, либо ее не разбудили, либо executor не дошел до очереди.\n\n#### 50. Кооперативная многозадачность и почему нельзя блокировать поток в async\n\nВ async коде один поток выполняет много задач. Пока задача не отдала управление через await, никакая другая не выполняется на этом потоке. Если внутри async fn вызвать std::thread::sleep или сделать тяжелый sync IO, остальные задачи на этом же рабочем потоке встанут. В tokio есть spawn_blocking, который отправляет работу на отдельный пул блокирующих потоков. Это правильный путь для CPU bound или для синхронных API.\n\n```rust\n#[tokio::main]\nasync fn main() {\n let r = tokio::task::spawn_blocking(|| heavy()).await.unwrap();\n println!(\"{}\", r);\n}\n\nfn heavy() -> u64 { (0..1_000_000u64).sum() }\n```\n\nЛогика простая. Все, что может занять больше микросекунды без await, отправляется в spawn_blocking.\n\n#### 51. Что такое tokio::select и какие у него подводные камни\n\nselect позволяет ждать одновременно несколько фьюч и продолжить, как только одна из них готова. Остальные фьючи дропаются. Это и есть основная ловушка. Если внутри ветки уже была выполнена часть работы с побочным эффектом, дропнутая фьюча не доведет работу до конца. Поэтому при использовании select важно либо использовать cancel safe примитивы, либо явно сохранять состояние через future::pin и biased.\n\n```rust\nuse tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n tokio::select! {\n _ = sleep(Duration::from_millis(50)) => println!(\"timeout\"),\n _ = async { sleep(Duration::from_millis(100)).await; } => println!(\"inner\"),\n }\n}\n```\n\nЗдесь сработает таймаут, а вторая ветка будет отменена.\n\n#### 52. Cancel safety, что это и почему важно\n\nCancel safety означает, что фьюча может быть дропнута на любом await без нарушения инвариантов. Многие операции tokio cancel safe, например чтение из канала. Многие не cancel safe, например AsyncReadExt::read_exact, который при отмене может оставить буфер частично прочитанным. В коде, который использует select, надо знать про cancel safety используемых API. В документации tokio это явно отмечено.\n\n#### 53. Что такое spawn и join в tokio\n\ntokio::spawn отправляет фьючу на исполнение в рантайм и возвращает JoinHandle. Сама задача начинает работать немедленно. JoinHandle это фьюча с результатом. Через await на хендле можно дождаться завершения и забрать значение. Если хендл уронить, задача продолжит работать в фоне. Чтобы прервать, можно вызвать abort.\n\n```rust\n#[tokio::main]\nasync fn main() {\n let h = tokio::spawn(async { 42 });\n println!(\"{}\", h.await.unwrap());\n}\n```\n\nЗадача в spawn должна быть 'static и Send для multi thread рантайма. Для single thread достаточно 'static.\n\n#### 54. Что такое JoinSet и зачем он нужен\n\nJoinSet это структура для управления группой задач. Позволяет ждать завершения любого из них через join_next, отменять все через shutdown, ограничивать число параллельно работающих. По сравнению с FuturesUnordered удобнее именно для tokio задач, потому что хранит JoinHandle и корректно обрабатывает abort.\n\n```rust\nuse tokio::task::JoinSet;\n\n#[tokio::main]\nasync fn main() {\n let mut set = JoinSet::new();\n for i in 0..5 { set.spawn(async move { i * i }); }\n while let Some(r) = set.join_next().await {\n println!(\"{}\", r.unwrap());\n }\n}\n```\n\nЭто типовой паттерн fan out fan in.\n\n#### 55. Что такое async trait и почему он нетривиален\n\nasync fn в трейтах долгое время не было, потому что Future это разный тип у каждой реализации. Сейчас в стабильном Rust async fn в трейтах работает напрямую, но с ограничениями для dyn. Для dyn trait сейчас используют либо макрос async_trait, либо вручную возвращают Box\u003Cdyn Future>. async_trait делает примерно это под капотом, добавляя аллокацию на каждый вызов. Для горячих путей это критично.\n\n```rust\nuse async_trait::async_trait;\n\n#[async_trait]\ntrait Store {\n async fn get(&self, key: &str) -> Option\u003CString>;\n}\n\nstruct Mem;\n#[async_trait]\nimpl Store for Mem {\n async fn get(&self, _k: &str) -> Option\u003CString> { Some(\"v\".into()) }\n}\n```\n\nЕсли динамическая диспетчеризация не нужна, лучше использовать обычный async fn в трейте и не платить за box.\n\n#### 56. Backpressure и как его делать в async\n\nBackpressure это давление потребителя на производителя. Без него быстрый продюсер забивает память. В асинхронных конвейерах backpressure реализуется через bounded каналы и через ограничение параллелизма. В tokio есть mpsc::channel с capacity, у него send становится фьючей, которая ждет, пока в очереди освободится место. Это автоматически тормозит продюсера.\n\n```rust\nuse tokio::sync::mpsc;\n\n#[tokio::main]\nasync fn main() {\n let (tx, mut rx) = mpsc::channel::\u003Ci32>(8);\n tokio::spawn(async move {\n for i in 0..100 { tx.send(i).await.unwrap(); }\n });\n while let Some(v) = rx.recv().await { println!(\"{}\", v); }\n}\n```\n\nЕсли получатель медленный, send в задаче будет ждать. Память не растет бесконтрольно.\n\n#### 57. Streams в async, что это и где применяются\n\nStream это асинхронный аналог Iterator. Метод poll_next возвращает Poll\u003COption\u003CItem>>. Стримы используются для обработки потоков сообщений, событий, чанков из сети, строк из файла. В крейтах futures и tokio_stream есть набор адаптеров map filter try_next chunks throttle.\n\n```rust\nuse tokio_stream::{self as stream, StreamExt};\n\n#[tokio::main]\nasync fn main() {\n let mut s = stream::iter(vec![1, 2, 3, 4, 5]).filter(|x| x % 2 == 0);\n while let Some(v) = s.next().await { println!(\"{}\", v); }\n}\n```\n\nСтримы хорошо ложатся на любую event driven архитектуру. Каждый шаг ленивый, кроме того, что нужно для прокачки следующего элемента.\n\n#### 58. Что такое pin_project и зачем он нужен\n\nКогда пишешь свою фьючу или стрим со сложной структурой, поля надо проектировать с учетом Pin. Какие то поля должны оставаться запиненными, какие то нет. pin_project это макрос, который генерирует безопасный API доступа к полям без unsafe. Без него пришлось бы писать вручную через Pin::map_unchecked_mut, а это легко превращается в UB.\n\n```rust\nuse pin_project::pin_project;\nuse std::pin::Pin;\nuse std::future::Future;\nuse std::task::{Context, Poll};\n\n#[pin_project]\nstruct Then\u003CF> { #[pin] fut: F, n: u32 }\n\nimpl\u003CF: Future\u003COutput = u32>> Future for Then\u003CF> {\n type Output = u32;\n fn poll(self: Pin\u003C&mut Self>, cx: &mut Context\u003C'_>) -> Poll\u003Cu32> {\n let this = self.project();\n match this.fut.poll(cx) {\n Poll::Ready(v) => Poll::Ready(v + *this.n),\n Poll::Pending => Poll::Pending,\n }\n }\n}\n```\n\nproject дает Pin\u003C&mut F> для запиненного поля и &mut u32 для обычного. Никакого unsafe в пользовательском коде.\n\n#### 59. Что такое executor budget в tokio и как его исчерпание влияет на код\n\nTokio начисляет каждой задаче бюджет в poll операциях. Когда задача исчерпала бюджет, кооперативный yield заставляет ее отдать управление, даже если она готова продолжить. Это защищает от ситуации, когда одна тяжелая задача занимает рабочий поток. Если задача делает много синхронной работы между awaits, бюджет тратится медленно, и можно случайно стать тем самым горячим воркером. Решение либо явные tokio::task::yield_now, либо вынесение в spawn_blocking.\n\n#### 60. Что такое LocalSet и когда он нужен\n\nLocalSet позволяет запускать !Send фьючи на одном потоке. Используется для задач, чьи внутренности нельзя двигать между потоками, типичный пример это интеграция с библиотеками, которые держат не Send состояние. В multi thread рантайме обычные spawn требуют Send, а LocalSet снимает это требование, привязывая задачи к текущему потоку.\n\n```rust\nuse std::rc::Rc;\nuse tokio::task;\n\n#[tokio::main(flavor = \"current_thread\")]\nasync fn main() {\n let local = task::LocalSet::new();\n local.run_until(async {\n let r = Rc::new(1);\n task::spawn_local(async move { println!(\"{}\", r); }).await.unwrap();\n }).await;\n}\n```\n\nЗдесь Rc, который не Send, спокойно живет в spawn_local.\n\n#### 61. Что такое timeout и как его делать в tokio\n\nДля ограничения времени операции используется tokio::time::timeout. Он оборачивает фьючу и возвращает Result, где Err это Elapsed. Если внутри был критичный ресурс, важно, что фьюча будет дропнута. См. cancel safety. Иначе можно оставить полузаписанный буфер или незакрытое соединение.\n\n```rust\nuse tokio::time::{timeout, sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n let r = timeout(Duration::from_millis(50), sleep(Duration::from_millis(100))).await;\n println!(\"{:?}\", r);\n}\n```\n\nВывод Err(Elapsed). Удобно для защиты от подвисших внешних вызовов.\n\n#### 62. Структурированная конкурентность, как ее делать в Rust\n\nИдея структурированной конкурентности в том, что все дочерние задачи завершаются до того, как функция, которая их породила, вернет управление. Это упрощает рассуждение об ошибках и о времени жизни данных. В Rust на стандартной библиотеке такой паттерн строится через std::thread::scope. В async аналогов в std пока нет, но JoinSet, tokio_scoped, async-scoped или ручной select с graceful shutdown играют ту же роль.\n\n```rust\nuse tokio::task::JoinSet;\n\nasync fn run() {\n let mut set = JoinSet::new();\n for i in 0..4 { set.spawn(async move { println!(\"{}\", i); }); }\n while set.join_next().await.is_some() {}\n}\n\n#[tokio::main]\nasync fn main() { run().await; }\n```\n\nКогда run завершается, все задачи гарантированно отработали. Это и есть структурированный подход.\n\n### Unsafe, FFI, низкий уровень\n\n#### 63. Что разрешает unsafe и что он не отключает\n\nUnsafe разрешает пять вещей. Разыменование сырого указателя, вызов unsafe функции или метода, доступ или изменение mutable static, реализацию unsafe трейта, обращение к union. Все остальные проверки компилятора остаются. Заимствование, типобезопасность, синтаксис, всё это работает как обычно. Unsafe не делает код быстрее само по себе и не отключает borrow checker.\n\n```rust\nfn main() {\n let mut x = 10;\n let p = &mut x as *mut i32;\n unsafe { *p += 1; }\n println!(\"{}\", x);\n}\n```\n\nКлюч в том, что ответственность за инварианты в этом блоке программист берет на себя.\n\n#### 64. Что такое undefined behavior в Rust и какие самые частые причины UB\n\nUB это поведение, для которого язык не дает никаких гарантий. Компилятор имеет право предполагать, что UB не возникает, и оптимизировать с учетом этого. В Rust типичные источники UB. Гонка данных через unsafe. Висячий указатель. Алиасинг ссылок несовместимым образом, например &mut и & одновременно через сырой указатель. Чтение неинициализированной памяти. Нарушение invariant Strict Aliasing для типизированных указателей. Передача невалидных значений примитивов, например bool со значением 2.\n\nПравильная модель здесь такая. Любой unsafe должен иметь рядом комментарий с обоснованием, почему инварианты соблюдены.\n\n#### 65. raw pointers vs references, что и когда использовать\n\nСсылки это безопасный примитив с гарантиями валидности и алиасинга. Сырые указатели *const T и *mut T этих гарантий не дают, не имеют лайфтайма и могут быть null. В прикладном коде сырые указатели почти не нужны. Появляются в FFI, в реализациях коллекций, в работе с памятью на низком уровне.\n\n```rust\nfn main() {\n let mut v = vec![1, 2, 3];\n let p: *mut i32 = v.as_mut_ptr();\n unsafe { *p.add(1) = 20; }\n println!(\"{:?}\", v);\n}\n```\n\nИндекс через сырой указатель не проверяется на выход за границы. Программист отвечает за это сам.\n\n#### 66. MaybeUninit, зачем он нужен\n\nMaybeUninit\u003CT> это обертка, которая позволяет легально работать с неинициализированной памятью. Раньше для тех же целей использовали mem::uninitialized, но это давало UB для типов с непустыми инвариантами, например для bool или NonNull. MaybeUninit обходит это, потому что компилятор знает, что внутри может быть мусор, и не делает предположений.\n\n```rust\nuse std::mem::MaybeUninit;\n\nfn make_array() -> [u32; 4] {\n let mut arr: [MaybeUninit\u003Cu32>; 4] = unsafe { MaybeUninit::uninit().assume_init() };\n for (i, slot) in arr.iter_mut().enumerate() {\n slot.write(i as u32);\n }\n unsafe { std::mem::transmute(arr) }\n}\n\nfn main() { println!(\"{:?}\", make_array()); }\n```\n\nКлассический паттерн для построения массивов без лишних инициализаций.\n\n#### 67. Что такое UnsafeCell и почему без него не сделать interior mutability\n\nUnsafeCell это единственный легальный способ получить &mut T через &T. Все типы интерьорной мутабельности построены на нем. Сам по себе UnsafeCell не делает синхронизацию и не проверяет правила. Он только разрешает то, что иначе компилятор счел бы UB. Сверху уже строятся Cell, RefCell, Mutex, atomics. Поэтому если человек делает свой контейнер с интерьорной мутабельностью, начинать он будет с UnsafeCell.\n\n#### 68. FFI с C, что нужно знать про репрезентацию типов\n\nПо умолчанию layout структуры в Rust не определен. Для совместимости с C ставят #[repr(C)]. Тогда поля идут в порядке объявления с padding по правилам C ABI. Аналогично, для enum, экспортируемых в C, используют #[repr(C)] или явный примитив #[repr(u32)]. Все типы, передаваемые через FFI, должны быть FFI safe. Например, String и Vec нельзя передавать напрямую, надо использовать сырой указатель и длину.\n\n```rust\n#[repr(C)]\nstruct Point { x: f64, y: f64 }\n\nextern \"C\" {\n fn distance(a: Point, b: Point) -> f64;\n}\n```\n\n#[repr(C)] обязательна, иначе layout может не совпасть.\n\n#### 69. extern fn, calling conventions, panic через границу FFI\n\nfn extern \"C\" задает соглашение вызова C. Без него используется внутренний Rust ABI, и интероп невозможен. Паника, вылетающая через границу FFI, это UB. Поэтому код, который вызывают из C, оборачивают в std::panic::catch_unwind. В Rust 2021 и новее unwinding через границу extern \"C\" по умолчанию приводит к abort, что само по себе уже спасает от UB, но обычно все равно явно ловят панику и возвращают код ошибки.\n\n```rust\n#[no_mangle]\npub extern \"C\" fn add(a: i32, b: i32) -> i32 {\n std::panic::catch_unwind(|| a + b).unwrap_or(-1)\n}\n```\n\nЭто базовый паттерн для библиотек, которые линкуются из C или Python.\n\n#### 70. bindgen, cbindgen, cxx, чем они отличаются\n\nBindgen генерирует Rust обвязки из C заголовков. Cbindgen наоборот, по Rust коду делает C заголовки. Cxx это безопасный интероп с C++, с проверкой совместимости типов на этапе сборки. Если задача переписать часть C либы на Rust, обычно используют bindgen и cbindgen. Если интегрироваться с C++, cxx, потому что unsafe extern \"C++\" с виртуальными методами руками поддерживать невозможно.\n\n#### 71. Что такое repr(transparent) и где он применяется\n\n#[repr(transparent)] говорит, что структура с одним непрозрачным полем имеет такое же ABI и layout, как это поле. Используется в newtype паттернах, когда нужно передавать обертку через FFI или применять методы низкого уровня. Также обычно ставится на типы, реализующие интерьорную мутабельность.\n\n```rust\n#[repr(transparent)]\nstruct Wrapped(u32);\n\nextern \"C\" { fn takes_u32(x: u32); }\n\nfn main() {\n let w = Wrapped(7);\n unsafe { takes_u32(w.0); }\n}\n```\n\nWrapped по ABI неотличим от u32 и может безопасно использоваться в сигнатурах FFI.\n\n#### 72. Что такое no_std и где он применяется\n\nno_std это режим без стандартно","该项目是一个面向中高级Rust开发者的面试准备资源库，提供了100个真实面试题及其详细解析。核心功能包括深入探讨语言机制如所有权、借用、生命周期等基础概念，以及并发、异步编程、unsafe代码和FFI等进阶主题，并附带实际代码示例。特别适合于希望系统性提升Rust技能并为技术岗位面试做准备的开发者使用。通过从零开始到专家级别的内容覆盖，无论是初学者还是有经验的工程师都能找到合适的学习材料。","2026-06-11 03:59:00","CREATED_QUERY"]