Введение: Почему Rust так требователен на собеседованиях?
Rust — это язык системного программирования, который славится своей безопасностью памяти и высокой производительностью. Компилятор Rust строг, но справедлив: он не пропустит ошибку владения (ownership) или висячую ссылку (dangling reference). Именно поэтому на собеседованиях по Rust редко спрашивают синтаксис "по бумажке". Вместо этого интервьюеры проверяют глубокое понимание модели памяти, управления ресурсами и идиоматичного кода.
В этой статье мы разберем топ-10 вопросов, которые чаще всего задают на собеседованиях для Junior и Middle Rust-разработчиков. Каждый вопрос сопровождается пояснением и примером кода. Если вы готовитесь к интервью, эта статья станет вашим чек-листом.
1. Владение (Ownership) и заимствование (Borrowing)
Это фундамент Rust. Без понимания владения вы не пройдете собеседование. Интервьюер может попросить объяснить разницу между String и &str или спросить, почему код не компилируется.
Вопрос: Объясните, что такое владение и как работает передача данных в функции.
Ответ: В Rust у каждого значения есть ровно один владелец. Когда переменная выходит из области видимости, значение удаляется (drop). Передача в функцию означает перемещение (move) владения, если тип не реализует трейт Copy.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 перемещено в s2
// println!("{}", s1); // Ошибка! s1 больше не владеет строкой
println!("{}", s2);
}
Если вы хотите передать данные без потери владения, используйте ссылки (&):
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // заимствование
println!("Длина '{}' равна {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
Вопрос: Что такое мутабельное заимствование и почему может быть только одно?
Ответ: Мутабельное заимствование (&mut) позволяет изменить значение через ссылку. Компилятор гарантирует, что в области видимости существует только одна мутабельная ссылка, чтобы предотвратить гонки данных (data races).
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // Ошибка! Нельзя две мутабельные ссылки
r1.push_str(", world");
println!("{}", r1);
}
2. Времена жизни (Lifetimes)
Времена жизни — это то, что пугает новичков. На собеседовании вас попросят объяснить, зачем нужны аннотации 'a и как компилятор выводит lifetimes.
Вопрос: Когда нужно явно указывать времена жизни?
Ответ: Когда функция возвращает ссылку, и компилятор не может самостоятельно определить, как долго она будет жить. Классический пример — функция, возвращающая самую длинную строку из двух:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("длинная строка");
let result;
{
let string2 = String::from("короткая");
result = longest(&string1, &string2);
}
// println!("{}", result); // Ошибка! string2 удалена, ссылка висячая
}
Аннотация 'a говорит: "время жизни возвращаемой ссылки равно меньшему из времен жизни входных ссылок".
Вопрос: Что такое elision (правила пропуска lifetimes)?
Ответ: Компилятор автоматически вставляет lifetimes в простых случаях. Если функция принимает одну ссылку и возвращает ссылку — lifetime выводится. Если несколько входных ссылок — нужно указывать явно.
3. Enum, Pattern Matching и Option/Result
Rust активно использует алгебраические типы данных. Интервьюер проверит, как вы работаете с Option и Result, и понимаете ли мощь match.
Вопрос: В чем разница между Option и Result? Приведите примеры использования.
Ответ: Option<T> означает, что значение может быть (Some(T)) или отсутствовать (None). Result<T, E> означает, что операция может завершиться успешно (Ok(T)) или с ошибкой (Err(E)).
// Option: поиск элемента в векторе
fn find_item(items: &[i32], target: i32) -> Option<usize> {
items.iter().position(|&x| x == target)
}
// Result: деление с проверкой на ноль
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Деление на ноль"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("Результат: {}", result),
Err(e) => println!("Ошибка: {}", e),
}
}
Вопрос: Как работает оператор ? (вопросительный знак)?
Ответ: Оператор ? — это синтаксический сахар для раннего возврата ошибки. Если значение Err, функция немедленно возвращает его. Если Ok — извлекает значение.
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?; // если ошибка — выход
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
4. Управление памятью: Stack, Heap и Box
Интервьюеры любят спрашивать, где что хранится и как Rust управляет памятью без сборщика мусора.