Rust: язык, который заставляет думать иначе
Вы решили покорить Rust. Отлично. Это не просто очередной язык — это вызов. Rust ломает шаблоны, которые мы привыкли видеть в Python, JavaScript или даже C++. Он заставляет вас думать о памяти, безопасности и конкурентности на новом уровне.
Собеседование по Rust — это не экзамен по синтаксису. Это проверка того, как вы мыслите. Интервьюеры хотят увидеть, что вы понимаете философию языка: владение, заимствование, времена жизни. В этой статье мы разберем реальные темы и вопросы, которые вам зададут, и покажем примеры кода, которые помогут пройти собеседование.
Готовы? Поехали.
1. Владение (Ownership) — фундамент, без которого никуда
Первый и самый важный блок. Если вы не понимаете владение — вы не понимаете Rust. На собеседовании вас обязательно попросят объяснить, как работает передача владения, и написать код, который это демонстрирует.
Ключевые правила владения:
- У каждого значения в Rust есть ровно один владелец.
- Когда владелец выходит из области видимости, значение удаляется.
- Владение может быть передано (move) или временно одолжено (borrow).
Пример: что выведет этот код?
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // Ошибка!
}
Правильный ответ: ошибка компиляции. s1 перемещено в s2, и s1 больше недействительно. Интервьюер хочет услышать, что вы понимаете move semantics и почему Rust так строг к этому.
2. Заимствование и ссылки (Borrowing & References)
После владения — заимствование. Как взять ссылку, не забирая владение? И главное — как не нарушить правила компилятора.
Правила заимствования:
- Может быть сколько угодно неизменяемых ссылок (&T).
- Может быть только одна изменяемая ссылка (&mut T).
- Нельзя одновременно иметь &T и &mut T на одно значение.
Пример: найти ошибку
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{} {}", r1, r2);
}
Ошибка: мы создали неизменяемую ссылку r1, а затем изменяемую r2. Компилятор не позволит. Объяснение: это защита от гонок данных (data races) на этапе компиляции.
3. Времена жизни (Lifetimes) — тема, которая пугает всех
Lifetimes — это не магия. Это аннотации, которые помогают компилятору понять, как долго ссылки остаются валидными. На собеседовании вас попросят объяснить, зачем они нужны, и написать функцию с явными lifetime-параметрами.
Типичный вопрос:
Напишите функцию, которая возвращает самую длинную строку из двух. Как вы укажете lifetimes?
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Здесь 'a — это общее время жизни, которое гарантирует, что возвращаемая ссылка будет валидна до тех пор, пока валидны обе входные ссылки. Интервьюер оценит, если вы упомянете элизию времени жизни (lifetime elision) — когда Rust сам выводит lifetimes для простых случаев.
4. Enums и Pattern Matching — сила Rust
Rust не имеет null. Вместо этого — Option<T> и Result<T, E>. На собеседовании вас попросят написать обработку ошибок с использованием match.
Пример: безопасное деление
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
fn main() {
match safe_divide(10.0, 0.0) {
Some(result) => println!("Result: {}", result),
None => println!("Cannot divide by zero"),
}
}
Покажите, что вы умеете использовать unwrap(), expect(), ? оператор. Но обязательно объясните, когда их применять, а когда нет — это показывает зрелость разработчика.
5. Коллекции: Vec, HashMap, итераторы
Работа с коллекциями — база. Но на собеседовании по Rust вас спросят не просто "как добавить элемент", а "как эффективно пройтись по коллекции и изменить её".
Вопрос: как отсортировать вектор строк по длине?
fn main() {
let mut words = vec!["rust", "python", "c++", "javascript"];
words.sort_by_key(|w| w.len());
println!("{:?}", words); // ["c++", "rust", "python", "javascript"]
}
Дополнительный плюс: расскажите про ленивые итераторы и цепочки методов (map, filter, collect). Это показывает, что вы пишете идиоматичный Rust.
6. Обработка ошибок: Result vs panic
Rust различает два типа ошибок: восстанавливаемые (Result) и невосстанавливаемые (panic!). На собеседовании спросят: "Когда вы используете panic!, а когда Result?"
Правильный ответ: Result — для ожидаемых ошибок (файл не найден, сеть недоступна). panic! — для багов и инвариантов, которые никогда не должны нарушаться (например, индекс вне границ массива).
Пример: функция, которая возвращает Result
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)
}
Обратите внимание на оператор ? — он сокращает boilerplate и автоматически пробрасывает ошибку наверх. Это must-know для любого Rust-разработчика.
7. Async/Await и Tokio — современный Rust
Если вы идете на позицию backend-разработчика, вас обязательно спросят про асинхронность. Rust использует cooperative multitasking с помощью async/.await. Базовый вопрос: "Что такое Future и как работает executor?"
Простой пример с Tokio:
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let task1 = async {
sleep(Duration::from_secs(1)).await;
println!("Task 1 done");
};
let task2 = async {
sleep(D