Как пройти собеседование по Rust: топ вопросов
Rust — один из самых востребованных языков программирования в 2025 году. Он сочетает производительность C/C++ с безопасностью памяти, что делает его идеальным выбором для системного программирования, веб-сборки и блокчейн-проектов. Однако собеседование по Rust часто пугает кандидатов из-за строгой системы владения, заимствования и времени жизни. В этой статье мы разберём топ-10 вопросов, которые вам обязательно зададут, и покажем правильные ответы с примерами кода.
1. Что такое владение (ownership) и как оно работает?
Владение — это основа Rust. Каждое значение в Rust имеет ровно одного владельца. Когда владелец выходит из области видимости, значение автоматически удаляется.
fn main() {
let s = String::from("hello"); // s — владелец строки
let t = s; // владение перемещается в t
// println!("{}", s); // ОШИБКА: s больше не владеет значением
println!("{}", t); // OK
}
Что спрашивают на собеседовании: объясните разницу между перемещением (move) и копированием (copy). Типы, реализующие трейт Copy (например, i32, bool), копируются, а не перемещаются.
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);
let r3 = &mut s; // OK: после того, как r1 и r2 вышли из области видимости
r3.push_str(" world");
println!("{}", r3);
}
Ключевое правило: в любой момент времени может быть либо одно изменяемое заимствование, либо любое количество неизменяемых.
3. Что такое время жизни (lifetimes)?
Время жизни — это аннотации, которые компилятору Rust нужны для проверки, что ссылки не становятся висячими. Чаще всего они используются в функциях, возвращающих ссылки.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("длинная");
let s2 = "короткая";
let result = longest(&s1, s2);
println!("Самая длинная строка: {}", result);
}
Лайфхак: в большинстве случаев компилятор сам выводит время жизни (элизия). Аннотации нужны только для сложных случаев.
4. Что такое трейты (traits) и как их использовать?
Трейты — это аналог интерфейсов в других языках. Они определяют общее поведение для разных типов.
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}...", self.title, &self.content[..20])
}
}
fn main() {
let article = Article {
title: String::from("Rust"),
content: String::from("Rust — безопасный и быстрый язык"),
};
println!("{}", article.summarize());
}
Важно: трейты могут иметь реализации по умолчанию и могут быть параметризованы (обобщённые трейты).
5. В чём разница между String и &str?
Это один из самых частых вопросов. String — это изменяемая, владеемая строка, хранящаяся в куче. &str — это неизменяемое заимствование строкового среза, которое может ссылаться на String, строковый литерал или часть строки.
fn main() {
let s1 = String::from("hello"); // String
let s2: &str = &s1[..2]; // &str — срез "he"
let s3: &str = "мир"; // строковый литерал — это &str
println!("{} {}", s2, s3);
}
Совет: всегда используйте &str для параметров функций, если вам не нужно менять строку.
6. Что такое умные указатели (smart pointers)?
Умные указатели — это структуры, которые ведут себя как указатели, но имеют дополнительную логику. Основные из них: Box<T>, Rc<T>, Arc<T>, RefCell<T>.
use std::rc::Rc;
fn main() {
let a = Rc::new(5);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("Счётчик ссылок: {}", Rc::strong_count(&a)); // 3
}
Ключевая идея: Box для одиночного владения в куче, Rc для множественного владения (только однопоточный), Arc для многопоточного, RefCell для внутренней изменяемости.
7. Как обрабатывать ошибки в Rust?
Rust использует два основных подхода: Result<T, E> для восстанавливаемых ошибок и panic! для невосстанавливаемых.
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, 0.0) {
Ok(result) => println!("Результат: {}", result),
Err(e) => println!("Ошибка: {}", e),
}
// Альтернатива с ?
let result = divide(10.0, 2.0).unwrap_or(0.0);
println!("{}", result);
}
Совет: используйте оператор ? для проброса ошибок вверх по стеку.