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