Как пройти собеседование по Rust: топ вопросов продвинутого уровня
Rust — это язык системного программирования, который набирает популярность благодаря своей безопасности, производительности и отсутствию сборщика мусора. Если вы уже освоили основы и готовитесь к собеседованию на позицию Senior или Middle Rust-разработчика, вам предстоит столкнуться с вопросами, выходящими за рамки простого синтаксиса. В этой статье мы разберём ключевые продвинутые темы, которые часто встречаются на технических интервью.
Мы рассмотрим четыре блока: владение и заимствование (ownership & borrowing), времена жизни (lifetimes), небезопасный код (unsafe) и макросы, а также продвинутую многопоточность. Каждый раздел содержит реальные вопросы и примеры кода.
1. Владение, заимствование и правила мутабельности
Вопрос: Объясните, как работает механизм владения в Rust. В чём разница между &T, &mut T и Box<T>?
Ответ: Владение — это основа безопасности памяти в Rust. Каждое значение имеет ровно одного владельца. Когда владелец выходит из области видимости, значение уничтожается. Заимствование (&T) позволяет временно «одолжить» ссылку на данные без передачи владения. Мутабельное заимствование (&mut T) даёт право изменить данные, но только при отсутствии других ссылок.
Box<T> — это умный указатель, который выделяет память в куче. Он передаёт владение, что отличает его от простых ссылок.
fn main() {
let mut s = String::from("Привет");
let r1 = &s; // иммутабельная ссылка
let r2 = &mut s; // Ошибка! нельзя иметь &mut и & одновременно
// println!("{}", r1);
}
Вопрос: Что такое «правило одного мутабельного заимствования»?
Ответ: В любой момент времени у значения может быть либо одна мутабельная ссылка, либо любое количество иммутабельных ссылок, но не то и другое одновременно. Это предотвращает гонки данных на этапе компиляции.
2. Времена жизни (Lifetimes) — как компилятор понимает, что ссылка жива
Вопрос: Зачем нужны аннотации lifetimes? Приведите пример, когда они обязательны.
Ответ: Аннотации времени жизни ('a, 'b и т.д.) помогают компилятору проверить, что все ссылки действительны. Они обязательны, когда функция возвращает ссылку, связанную с одним из входных параметров, и компилятор не может вывести время жизни автоматически.
// Функция возвращает самую длинную строку
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("длинная");
let result;
{
let s2 = String::from("короткая");
result = longest(&s1, &s2); // ok
}
// println!("{}", result); // Ошибка: s2 умерла
}
Вопрос: Что такое «эллизия времён жизни» (lifetime elision)?
Ответ: Компилятор автоматически добавляет аннотации времени жизни в простых случаях. Например, для метода fn first_word(s: &str) -> &str компилятор сам понимает, что выходное время жизни равно входному. Эллизия работает по трём правилам, описанным в книге Rust.
3. Unsafe Rust и макросы
Вопрос: Когда оправдано использование unsafe? Какие операции доступны только внутри unsafe-блоков?
Ответ: unsafe используется для операций, которые компилятор не может проверить: разыменование сырых указателей, вызов внешних функций (FFI), доступ к изменяемым статическим переменным, реализация небезопасных трейтов. Однако unsafe не означает, что код сломан — вы берёте ответственность за корректность на себя.
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 = {}", *r1);
*r2 = 10;
println!("r2 = {}", *r2);
}
}
Вопрос: Как работают макросы в Rust? Чем отличаются декларативные макросы (macro_rules!) от процедурных?
Ответ: Декларативные макросы (macro_rules!) работают по принципу сопоставления с образцом — они генерируют код на основе переданных токенов. Процедурные макросы (атрибутные, derive, функциональные) — это функции, которые принимают поток токенов и возвращают новый поток. Они сложнее, но мощнее: например, #[derive(Serialize)] из serde — это процедурный макрос.
macro_rules! say_hello {
() => {
println!("Привет!");
};
($name:expr) => {
println!("Привет, {}!", $name);
};
}
fn main() {
say_hello!();
say_hello!("Алексей");
}
4. Продвинутая многопоточность: Send, Sync и Arc
Вопрос: Что такое трейты Send и Sync? В чём разница?
Ответ: Send — тип, владение которым можно безопасно передавать между потоками. Sync — тип, на который можно безопасно ссылаться из нескольких потоков (т.е. &T является Send). Почти все примитивные типы реализуют оба трейта, но Rc<T> — нет (только Arc<T>).
Вопрос: Как правильно использовать Arc и Mutex для разделения состояния между потоками?
use std::sync::{Arc, Mutex};
use std::thread;