Продвинутые вопросы на собеседовании по Rust: разбор и примеры

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

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

Начать курс

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

Rust — один из самых востребованных языков системного программирования, и спрос на senior-разработчиков продолжает расти. Если вы уже освоили базовый синтаксис и систему владения, на собеседовании вас ждут вопросы, проверяющие глубокое понимание unsafe-кода, асинхронности, сложных времён жизни и метапрограммирования. В этой статье мы разберём ключевые темы и приведём примеры кода, которые помогут вам уверенно пройти интервью.

1. Unsafe Rust: когда и зачем?

Один из самых частых вопросов на продвинутом собеседовании: «Объясните, когда вы бы использовали unsafe и какие гарантии при этом нарушаются».

Ответ должен включать четыре основные возможности, которые даёт unsafe:

  • Разыменование сырых указателей (*const T и *mut T)
  • Вызов unsafe-функций (например, FFI)
  • Доступ к изменяемым статическим переменным
  • Реализация unsafe-трейтов

Пример: реализация простого динамического массива с ручным управлением памятью.

use std::alloc::{alloc, dealloc, Layout};
use std::ptr;

struct MyVec {
    ptr: *mut i32,
    len: usize,
    cap: usize,
}

impl MyVec {
    fn new() -> Self {
        MyVec {
            ptr: ptr::null_mut(),
            len: 0,
            cap: 0,
        }
    }

    fn push(&mut self, val: i32) {
        if self.len == self.cap {
            let new_cap = if self.cap == 0 { 1 } else { self.cap * 2 };
            let new_layout = Layout::array::<i32>(new_cap).unwrap();
            let new_ptr = unsafe { alloc(new_layout) as *mut i32 };
            // копируем старые данные
            unsafe {
                ptr::copy_nonoverlapping(self.ptr, new_ptr, self.len);
                if !self.ptr.is_null() {
                    let old_layout = Layout::array::<i32>(self.cap).unwrap();
                    dealloc(self.ptr as *mut u8, old_layout);
                }
            }
            self.ptr = new_ptr;
            self.cap = new_cap;
        }
        unsafe {
            ptr::write(self.ptr.add(self.len), val);
        }
        self.len += 1;
    }
}

impl Drop for MyVec {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe {
                let layout = Layout::array::<i32>(self.cap).unwrap();
                dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}

Совет: всегда подчёркивайте, что unsafe — это не отключение проверок, а взятие на себя ответственности за инварианты.

2. Lifetimes и variance: как компилятор проверяет ссылки

На продвинутом уровне вас попросят объяснить, что такое variance (ковариантность, контравариантность, инвариантность) в контексте времён жизни. Это ключ к пониманию, почему некоторые типы не компилируются.

  • Ковариантность: &'a T — если 'a больше 'b, то &'a T можно использовать как &'b T.
  • Контравариантность: fn(T) — если 'a больше 'b, то fn(&'b T) можно использовать как fn(&'a T).
  • Инвариантность: &mut T — время жизни должно совпадать в точности.

Пример: почему следующий код не скомпилируется?

fn foo<'a>(x: &'a mut &'a str, y: &'a str) {
    *x = y;
}

fn main() {
    let mut s = "hello";
    let r = &s;
    foo(&mut s, r); // ошибка!
}

Ответ: &mut T инвариантен по T, поэтому время жизни внутренней ссылки должно совпадать с временем жизни внешней. Компилятор не может «сжать» время жизни r.

3. Async Rust: Pin, Unpin и Future

Вопросы по асинхронности — обязательная часть собеседования. Интервьюер может спросить: «Что такое Pin и зачем он нужен?»

Ключевой момент: Pin гарантирует, что данные не будут перемещены в памяти после того, как на них создана ссылка. Это критично для Future, которые могут содержать самореферентные структуры (например, при использовании select! или join!).

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct SelfReferential {
    data: String,
    ptr: *const String,
    _pin: PhantomPinned,
}

impl SelfReferential {
    fn new(data: String) -> Self {
        SelfReferential {
            ptr: std::ptr::null(),
            data,
            _pin: PhantomPinned,
        }
    }

    fn init(self: Pin<&mut Self>) {
        let this = unsafe { self.get_unchecked_mut() };
        this.ptr = &this.data;
    }
}

fn main() {
    let mut val = SelfReferential::new("hello".to_string());
    let pinned = unsafe { Pin::new_unchecked(&mut val) };
    pinned.init();
    // после init val больше нельзя перемещать!
    println!("{:?}", pinned);
}

Важно: объясните, что Unpin — это маркерный трейт, который говорит, что тип можно безопасно перемещать. Большинство типов реализуют Unpin, но Future с самореферентными данными — нет.

4. Макросы: декларативные vs процедурные

Продвинутый разработчик должен уметь не только использовать макросы, но и создавать свои. Вопрос: «В чём разница между macro_rules! и процедурными макросами? Когда что применять?»

  • Декларативные макросы (macro_rules!) — работают на уровне синтаксических шаблонов. Подходят для простого повторения кода.
  • Процедурные макросы — работают на уровне AST. Позволяют генерировать код на основе произвольной логики (например, #[derive(Serialize)]).

Блоги

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