Введение: Почему Rust — это вызов для интервьюера
Rust — язык, который требует от разработчика глубокого понимания управления памятью, системы типов и модели параллелизма. Собеседование по Rust — это не проверка синтаксиса, а тест на понимание философии языка. В этой статье мы разберем ключевые вопросы, которые вам могут задать на интервью — от основ владения до продвинутых тем вроде асинхронности. Вы узнаете, какие ответы ожидает интервьюер и как избежать типичных ошибок.
1. Владение, заимствование и время жизни
1.1. Что такое владение (ownership) и как оно работает?
Это фундамент Rust. Каждое значение имеет ровно одного владельца. Когда владелец выходит из области видимости, значение удаляется. Это позволяет избежать сборки мусора и гарантирует безопасность памяти на этапе компиляции.
fn main() {
let s1 = String::from("привет");
let s2 = s1; // s1 перемещается в s2
// println!("{}", s1); // Ошибка! s1 больше недействителен
println!("{}", s2); // Работает
}
Совет: Объясните разницу между перемещением (move) и копированием (copy). Для типов, реализующих трейт Copy (например, целые числа), перемещение не происходит — данные копируются.
1.2. Правила заимствования (borrowing)
Заимствование позволяет использовать ссылку на данные без передачи права собственности. Есть два типа ссылок:
- Неизменяемые (&T): может быть сколько угодно, данные нельзя менять.
- Изменяемые (&mut T): только одна ссылка на данные в текущей области видимости.
fn main() {
let mut s = String::from("hello");
let r1 = &s; // неизменяемая ссылка
let r2 = &s; // ещё одна неизменяемая — ок
// let r3 = &mut s; // Ошибка! Нельзя одновременно иметь неизменяемые и изменяемые ссылки
println!("{} {}", r1, r2);
}
Вопрос с подвохом: "Почему Rust запрещает одновременно изменяемые и неизменяемые ссылки?" Ответ: чтобы предотвратить гонки данных (data races) на этапе компиляции.
1.3. Время жизни (lifetimes) — как объяснить простыми словами
Время жизни — это аннотации, которые сообщают компилятору, как долго ссылки будут действительны. Чаще всего компилятор выводит их сам, но в сложных функциях приходится указывать явно.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Ключевая идея: 'a означает, что возвращаемая ссылка будет жить не дольше, чем обе входные ссылки. Это гарантирует, что ссылка не станет висячей (dangling).
2. Перечисления, сопоставление с образцом и трейты
2.1. Enum в Rust — почему это мощнее, чем в других языках
В Rust перечисления могут хранить данные любого типа — строки, числа, структуры и даже другие перечисления. Это позволяет создавать алгебраические типы данных (ADT).
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Типичный вопрос: "Как работает Option<T> и почему в Rust нет null?" Ответ: Option — это enum, который может быть Some(T) или None. Компилятор заставляет обрабатывать оба варианта, что исключает ошибки с нулевыми указателями.
2.2. Сопоставление с образцом (pattern matching)
Конструкция match — сердце Rust. Она должна быть исчерпывающей (exhaustive).
fn describe_point(point: (i32, i32)) -> &'static str {
match point {
(0, 0) => "начало координат",
(0, _) => "на оси X",
(_, 0) => "на оси Y",
_ => "где-то в плоскости",
}
}
Лайфхак для собеседования: Используйте if let для простых проверок — это делает код чище: if let Some(value) = optional { ... }.
2.3. Трейты (traits) — что это и как использовать
Трейты — это интерфейсы, которые определяют общее поведение для разных типов. Они похожи на интерфейсы в Java или протоколы в Swift.
trait Summarizable {
fn summary(&self) -> String;
}
struct Article {
title: String,
content: String,
}
impl Summarizable for Article {
fn summary(&self) -> String {
format!("{}: {}", self.title, self.content[..30].to_string())
}
}
Важный вопрос: "Чем отличаются трейты от абстрактных классов?" Ответ: трейты не могут хранить поля, только методы. Они поддерживают множественную реализацию для одного типа.
3. Управление памятью и умные указатели
3.1. Box, Rc и Arc — когда что использовать
- Box<T>: для хранения данных в куче, когда размер известен во время компиляции. Используется для рекурсивных типов.
- Rc<T>: подсчёт ссылок для одного потока (не потокобезопасен).
- Arc<T>: атомарный подсчёт ссылок для многопоточности.
use std::rc::Rc;
let a = Rc::new(5);
let b = Rc::clone(&a); // увеличивает счётчик ссылок
println!("{}", Rc::strong_count(&a)); // 2
Типичная ошибка: Использовать Rc без RefCell для изменяемых данных. RefCell<T> обеспечивает внутреннюю изменяемость (interior mutability) с проверкой правил заимствования во время выполнения.
3.2. Как Rust гарантирует безопасность памяти без сборщика мусора
Систем