Как пройти собеседование по Rust: продвинутые вопросы и ответы

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как пройти собеседование по 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;                

Блоги

Рекомендаций по книгам