Перейти к основному содержимому

200 вопросов по Rust

200 вопросов по Rust

Основы языка Rust

Вопрос

Что такое Rust?

Ответ

Rust — это системный язык программирования с фокусом на безопасность памяти, конкурентность и производительность без использования сборщика мусора.


Вопрос

Какие ключевые особенности отличают Rust от других системных языков?

Ответ

Rust обеспечивает безопасность памяти во время компиляции за счёт системы владения (ownership), заимствования (borrowing) и проверки времени жизни (lifetimes). Он не использует сборщик мусора и предоставляет нулевую стоимость абстракций.


Вопрос

Что такое Cargo?

Ответ

Cargo — это официальный менеджер пакетов и система сборки для Rust. Он управляет зависимостями, компиляцией, тестированием и документацией проекта.


Вопрос

Как создать новый проект с помощью Cargo?

Ответ

Команда cargo new имя_проекта создаёт новый бинарный проект. Для библиотеки используется флаг --lib: cargo new --lib имя_библиотеки.


Вопрос

Что делает команда cargo run?

Ответ

Команда cargo run компилирует проект и запускает исполняемый файл в одном шаге.


Вопрос

Какие два основных режима сборки поддерживает Cargo?

Ответ

Cargo поддерживает режим разработки (dev) и режим релиза (release). Режим релиза включает оптимизации и отключает отладочные проверки.


Вопрос

Что такое crate в Rust?

Ответ

Crate — это минимальная единица компиляции в Rust. Это либо бинарный файл (binary crate), либо библиотека (library crate).


Вопрос

В чём разница между binary crate и library crate?

Ответ

Binary crate содержит функцию main и компилируется в исполняемый файл. Library crate не имеет точки входа и предназначен для повторного использования в других проектах.


Вопрос

Что такое модуль (module) в Rust?

Ответ

Модуль — это механизм организации кода внутри crate. Он определяет область видимости и контролирует приватность элементов с помощью ключевых слов pub и mod.


Вопрос

Как импортировать элемент из другого модуля?

Ответ

Используется ключевое слово use. Например: use std::collections::HashMap;.


Типы данных и переменные

Вопрос

Как объявить переменную в Rust?

Ответ

Переменная объявляется с помощью ключевого слова let. По умолчанию она неизменяема: let x = 5;.


Вопрос

Как сделать переменную изменяемой?

Ответ

Нужно добавить ключевое слово mut: let mut x = 5;.


Вопрос

Какие скалярные типы данных есть в Rust?

Ответ

Скалярные типы включают целые числа (i32, u64 и др.), числа с плавающей точкой (f32, f64), логический тип (bool) и символьный тип (char).


Вопрос

Чем отличаются i32 и u32?

Ответ

i32 — это 32-битное знаковое целое число, диапазон от -2³¹ до 2³¹−1. u32 — 32-битное беззнаковое целое число, диапазон от 0 до 2³²−1.


Вопрос

Какой тип по умолчанию используется для целых чисел в Rust?

Ответ

Если тип не указан явно, компилятор выбирает i32 как наиболее сбалансированный по скорости и размеру.


Вопрос

Какие составные типы данных есть в Rust?

Ответ

Составные типы включают кортежи (tuple) и массивы (array).


Вопрос

Как объявить кортеж?

Ответ

Кортеж объявляется в круглых скобках: let tup = (500, 6.4, "hello");.


Вопрос

Как получить доступ к элементам кортежа?

Ответ

Можно использовать деструктуризацию: let (x, y, z) = tup; или точечную нотацию: tup.0.


Вопрос

Чем отличается массив от вектора?

Ответ

Массив имеет фиксированный размер, известный на этапе компиляции, и хранится в стеке. Вектор (Vec<T>) имеет динамический размер и хранится в куче.


Вопрос

Как объявить массив из пяти нулей типа i32?

Ответ

let arr = [0; 5];.


Владение (Ownership)

Вопрос

Что такое владение в Rust?

Ответ

Владение — это система управления памятью в Rust, основанная на наборе правил, которые компилятор проверяет во время компиляции. Каждое значение имеет одного владельца, и при выходе владельца из области видимости значение автоматически освобождается.


Вопрос

Какие три правила владения в Rust?

Ответ

  1. Каждое значение имеет ровно одного владельца.
  2. В любой момент времени может существовать только один владелец.
  3. Когда владелец выходит из области видимости, значение уничтожается.

Вопрос

Что происходит при присваивании значения одной переменной другой?

Ответ

Значение перемещается (move): первая переменная больше не действительна, а вторая становится владельцем.

let s1 = String::from("hello");
let s2 = s1; // s1 больше не доступна

Вопрос

Почему после перемещения первая переменная становится недействительной?

Ответ

Это предотвращает двойное освобождение памяти (double free), так как обе переменные указывали бы на одну и ту же область памяти.


Вопрос

Какие типы данных копируются при присваивании, а не перемещаются?

Ответ

Типы, реализующие трейт Copy, такие как все целочисленные типы, bool, char, кортежи из Copy-типов. Они копируются побитово и остаются действительными после присваивания.


Вопрос

Можно ли реализовать Copy для типа, содержащего String?

Ответ

Нет. Типы, имеющие данные в куче (например, String, Vec<T>), не могут реализовывать Copy, потому что их глубокое копирование требует выделения памяти, что противоречит семантике Copy.


Вопрос

Что такое клонирование в Rust?

Ответ

Клонирование — это явное создание глубокой копии значения с помощью метода .clone(). Оно работает для типов, реализующих трейт Clone.


Вопрос

В чём разница между clone() и copy()?

Ответ

copy() выполняется автоматически и быстро (побитово), применим только к тривиальным типам. clone() требует вызова вручную и может быть дорогим, так как создаёт полную копию данных.


Вопрос

Что происходит с владельцем при передаче значения в функцию?

Ответ

Владение передаётся функции. После вызова исходная переменная становится недействительной.

fn takes_ownership(s: String) { /* ... */ }
let s = String::from("hello");
takes_ownership(s); // s больше не действительна

Вопрос

Как вернуть владение из функции?

Ответ

Функция может вернуть значение через return, тогда вызывающий код снова становится владельцем.

fn give_back(s: String) -> String {
s
}

Заимствование и ссылки

Вопрос

Что такое заимствование (borrowing) в Rust?

Ответ

Заимствование — это механизм, позволяющий временно использовать значение без передачи владения. Это достигается с помощью ссылок (&T для неизменяемых и &mut T для изменяемых).


Вопрос

Какие два типа ссылок существуют в Rust?

Ответ

Существуют неизменяемые ссылки (&T) и изменяемые ссылки (&mut T). Неизменяемые ссылки позволяют читать данные, изменяемые — читать и изменять.


Вопрос

Можно ли иметь несколько неизменяемых ссылок на одно и то же значение одновременно?

Ответ

Да, можно создавать сколько угодно неизменяемых ссылок на одно значение в одной области видимости.


Вопрос

Можно ли иметь несколько изменяемых ссылок на одно и то же значение одновременно?

Ответ

Нет. В любой момент времени может существовать только одна изменяемая ссылка на значение. Это правило предотвращает гонки данных (data races).


Вопрос

Можно ли одновременно иметь изменяемую и неизменяемую ссылку на одно значение?

Ответ

Нет. Наличие изменяемой ссылки исключает любые другие ссылки (изменяемые или неизменяемые) на то же значение в той же области видимости.


Вопрос

Что происходит при попытке нарушить правила заимствования?

Ответ

Компилятор Rust выдаст ошибку на этапе компиляции. Это гарантирует безопасность памяти без использования сборщика мусора.


Вопрос

Чем отличается &String от &str?

Ответ

&String — это ссылка на владеющий строковый тип String, хранящийся в куче. &str — это строковый срез (string slice), не владеющий данными, обычно указывающий на часть строки или строковый литерал.


Вопрос

Почему предпочтительно использовать &str вместо &String в параметрах функций?

Ответ

&str является более общим типом: он принимает как строковые литералы ("hello"), так и ссылки на String. Это делает функцию более гибкой и эффективной.

fn greet(name: &str) {
println!("Hello, {}", name);
}

Вопрос

Что такое разыменование ссылки?

Ответ

Разыменование — это получение значения, на которое указывает ссылка, с помощью оператора *. Например: let x = 5; let r = &x; let y = *r; // y == 5.


Вопрос

Можно ли изменять значение через неизменяемую ссылку?

Ответ

Нет. Через неизменяемую ссылку (&T) можно только читать значение. Попытка изменения вызовет ошибку компиляции.


Время жизни (Lifetimes)

Вопрос

Что такое время жизни (lifetime) в Rust?

Ответ

Время жизни — это механизм, который гарантирует, что все ссылки в программе действительны на протяжении всего времени их использования. Он предотвращает использование "висячих" ссылок.


Вопрос

Как обозначаются времена жизни в Rust?

Ответ

Времена жизни обозначаются апострофом и идентификатором, например 'a, 'b. Они используются в сигнатурах функций и структур.


Вопрос

Зачем нужны аннотации времени жизни?

Ответ

Аннотации времени жизни сообщают компилятору, как долго должны жить ссылки относительно друг друга. Это необходимо, когда компилятор не может вывести время жизни автоматически.


Вопрос

Что означает 'static?

Ответ

'static — это специальное время жизни, означающее, что ссылка живёт на протяжении всего времени выполнения программы. Строковые литералы имеют время жизни 'static.


Вопрос

Можно ли вернуть ссылку на локальную переменную из функции?

Ответ

Нет. Локальная переменная уничтожается при выходе из функции, поэтому ссылка на неё станет недействительной. Компилятор запретит такой код.

// Ошибка компиляции:
fn bad() -> &str {
let s = String::from("hello");
&s // нельзя вернуть ссылку на локальную переменную
}

Вопрос

Как правильно вернуть строку из функции?

Ответ

Следует вернуть владеющий тип, например String, а не ссылку:

fn good() -> String {
String::from("hello")
}

Вопрос

Что такое элиминирование времени жизни (lifetime elision)?

Ответ

Это набор правил, по которым компилятор автоматически выводит времена жизни в простых случаях, чтобы не требовать явных аннотаций от программиста.


Вопрос

Какие три основных правила элиминирования времён жизни?

Ответ

  1. Каждый параметр-ссылка получает собственное время жизни.
  2. Если есть ровно один входной параметр-ссылка, его время жизни присваивается всем выходным ссылкам.
  3. Если метод имеет &self или &mut self, время жизни self присваивается всем выходным ссылкам.

Вопрос

Нужно ли указывать времена жизни в структурах, содержащих ссылки?

Ответ

Да. Все ссылки в структуре должны иметь явно указанное время жизни, чтобы компилятор знал, как долго структура может существовать.

struct ImportantExcerpt<'a> {
part: &'a str,
}

Вопрос

Может ли время жизни быть короче, чем время жизни владельца данных?

Ответ

Да. Ссылка может быть ограничена меньшей областью видимости, чем само значение. Но она не может превышать время жизни данных, на которые ссылается.


Строки и работа с текстом

Вопрос

Какие основные строковые типы есть в Rust?

Ответ

Основные строковые типы: String (владельческая строка в куче) и &str (строковый срез, не владеющий данными).


Вопрос

Как создать новую пустую строку?

Ответ

С помощью String::new():

let s = String::new();

Вопрос

Как создать строку из строкового литерала?

Ответ

С помощью String::from():

let s = String::from("hello");

Вопрос

Можно ли изменять строку типа String?

Ответ

Да, если она объявлена как изменяемая (mut):

let mut s = String::from("hello");
s.push_str(", world!");

Вопрос

Что делает метод push_str?

Ответ

Метод push_str добавляет строковый срез в конец существующей строки String.


Вопрос

Что такое UTF-8 и как Rust с ним работает?

Ответ

UTF-8 — это кодировка символов, используемая Rust по умолчанию. Все строки в Rust являются валидными UTF-8 последовательностями. Это гарантирует корректную работу с международными символами.


Вопрос

Можно ли получить длину строки в символах?

Ответ

Метод .len() возвращает длину в байтах, а не в символах (Unicode scalar values). Для получения количества символов нужно использовать .chars().count():

let s = "Здравствуйте";
println!("{}", s.chars().count()); // 12 символов

Вопрос

Почему индексация строки по байтовому смещению может быть опасной?

Ответ

Потому что символы UTF-8 могут занимать от 1 до 4 байт. Доступ по произвольному байтовому индексу может попасть внутрь многобайтового символа, что приведёт к панике. Rust не позволяет прямую индексацию строк.


Вопрос

Как перебрать символы строки?

Ответ

С помощью метода .chars():

for c in "hello".chars() {
println!("{}", c);
}

Вопрос

Как конкатенировать две строки?

Ответ

Можно использовать оператор + или метод format!:

let s1 = String::from("Hello");
let s2 = String::from(" world!");
let s3 = s1 + &s2; // s1 перемещается, s2 заимствуется

// Или:
let s = format!("{} {}", s1, s2);

Структуры и перечисления

Вопрос

Что такое структура (struct) в Rust?

Ответ

Структура — это пользовательский составной тип данных, объединяющий несколько значений разных типов под одним именем. Она может содержать поля с именами (обычная структура), быть кортежной (tuple struct) или быть единичной (unit-like struct).


Вопрос

Как объявить структуру с именованными полями?

Ответ

struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

Вопрос

Как создать экземпляр структуры?

Ответ

Указываются значения для всех полей:

let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

Вопрос

Можно ли изменять поля структуры после создания?

Ответ

Да, если экземпляр объявлен как изменяемый (mut):

let mut user1 = User { /* ... */ };
user1.email = String::from("new@email.com");

Вопрос

Что такое кортежная структура (tuple struct)?

Ответ

Кортежная структура — это структура без именованных полей, но с определённым типом. Она полезна для создания новых типов на основе кортежей:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

Вопрос

Что такое метод в контексте структуры?

Ответ

Метод — это функция, определённая внутри блока impl, которая принимает self (или &self, &mut self) в качестве первого параметра и работает с экземпляром структуры.


Вопрос

Как определить метод для структуры?

Ответ

impl User {
fn new(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

fn greet(&self) {
println!("Hello, {}!", self.username);
}
}

Вопрос

Что такое перечисление (enum) в Rust?

Ответ

Перечисление — это тип, который может быть одним из нескольких вариантов (вариантов-значений). Каждый вариант может содержать данные.


Вопрос

Как объявить перечисление?

Ответ

enum IpAddrKind {
V4,
V6,
}

Или с данными:

enum IpAddr {
V4(String),
V6(String),
}

Вопрос

Как использовать перечисление?

Ответ

Создаётся значение одного из вариантов:

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

Вопрос

Что такое Option?

Ответ

Option — это стандартное перечисление, представляющее наличие или отсутствие значения:

enum Option<T> {
Some(T),
None,
}

Вопрос

Почему Option предпочтительнее нулевых указателей?

Ответ

Option заставляет явно обрабатывать случай отсутствия значения. Компилятор не позволяет использовать Option<T> как T без проверки, что исключает ошибки разыменования нулевых указателей.


Вопрос

Что такое сопоставление с образцом (match)?

Ответ

match — это управляющая конструкция, которая сравнивает значение с набором шаблонов и выполняет код, соответствующий совпадающему шаблону. Все возможные варианты должны быть обработаны.


Вопрос

Приведите пример использования match с Option.

Ответ

fn print_number(num: Option<i32>) {
match num {
Some(x) => println!("The number is {}", x),
None => println!("No number provided"),
}
}

Вопрос

Можно ли использовать if let вместо match?

Ответ

Да. if let — это сокращение для обработки одного варианта перечисления без необходимости перечислять все остальные:

if let Some(x) = num {
println!("The number is {}", x);
}

Трейты (Traits)

Вопрос

Что такое трейт (trait) в Rust?

Ответ

Трейт определяет поведение, которое может реализовать тип. Он похож на интерфейс в других языках и используется для абстракции и полиморфизма.


Вопрос

Как объявить трейт?

Ответ

trait Summary {
fn summarize(&self) -> String;
}

Вопрос

Как реализовать трейт для типа?

Ответ

С помощью блока impl:

impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {}", self.headline, self.author)
}
}

Вопрос

Можно ли реализовать трейт для внешнего типа?

Ответ

Только если либо тип, либо трейт определены в текущем crate. Это правило называется «правилом орфана» (orphan rule) и предотвращает конфликты реализаций.


Вопрос

Что такое трейт-объект (trait object)?

Ответ

Трейт-объект — это механизм динамического диспетчеризации, позволяющий хранить значения разных типов, реализующих один трейт, в одном месте. Обозначается как Box<dyn Trait> или &dyn Trait.


Вопрос

В чём разница между статическим и динамическим диспетчеризацией?

Ответ

Статическая диспетчеризация происходит через мономорфизацию (например, при использовании обобщений) и разрешается на этапе компиляции. Динамическая диспетчеризация использует трейт-объекты и разрешается во время выполнения.


Вопрос

Какие трейты есть в стандартной библиотеке?

Ответ

Распространённые трейты: Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Drop, Send, Sync.


Вопрос

Что делает трейт Debug?

Ответ

Трейт Debug позволяет выводить структуру в человекочитаемом формате для отладки с помощью макроса println!("{:?}", value).


Вопрос

Как автоматически реализовать трейт для структуры?

Ответ

С помощью атрибута #[derive]:

#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}

Вопрос

Можно ли реализовать несколько трейтов для одного типа?

Ответ

Да. Тип может реализовывать любое количество трейтов, каждый через отдельный блок impl.


Обработка ошибок

Вопрос

Какие два основных типа используются для обработки ошибок в Rust?

Ответ

Result<T, E> и Option<T>. Result используется, когда нужно передать информацию об ошибке, Option — когда достаточно знать, есть значение или нет.


Вопрос

Что представляет собой Result<T, E>?

Ответ

Result — это перечисление с двумя вариантами: Ok(T) при успехе и Err(E) при ошибке.


Вопрос

Как обработать Result?

Ответ

С помощью match, if let, или методов вроде .unwrap(), .expect(), .unwrap_or(), а также оператора ?.


Вопрос

Что делает оператор ??

Ответ

Оператор ? распаковывает значение из Ok, а при Err немедленно возвращает ошибку из текущей функции. Может использоваться только в функциях, возвращающих Result или Option.


Вопрос

Почему не рекомендуется часто использовать .unwrap()?

Ответ

.unwrap() вызывает панику при получении Err, что приводит к аварийному завершению программы. Это нарушает надёжность и должно использоваться только в прототипах или когда ошибка невозможна.


Вопрос

Как создать собственный тип ошибки?

Ответ

Можно определить перечисление или структуру и реализовать для неё трейт std::error::Error, а также Display и Debug.


Вопрос

Что такое восстановимые и невосстановимые ошибки?

Ответ

Восстановимые ошибки обрабатываются через Result и позволяют программе продолжить работу. Невосстановимые ошибки вызывают панику и завершают поток.


Вопрос

Как вызвать панику вручную?

Ответ

С помощью макроса panic!:

panic!("Something went wrong!");

Вопрос

Что происходит при панике?

Ответ

По умолчанию Rust начинает «раскрутку стека» (unwinding): очищает стек, вызывая деструкторы, и завершает поток. Можно настроить поведение на «прерывание» (abort) через настройки компиляции.


Вопрос

Можно ли перехватить панику?

Ответ

Да, с помощью std::panic::catch_unwind, но это не рекомендуется для обычной обработки ошибок. Паника предназначена для фатальных ситуаций.


Коллекции

Вопрос

Какие основные коллекции предоставляет стандартная библиотека Rust?

Ответ

Основные коллекции: Vec<T> (вектор), String (текстовая строка), HashMap<K, V> (хеш-таблица). Все они хранят данные в куче и могут расти динамически.


Вопрос

Что такое Vec<T>?

Ответ

Vec<T> — это динамический массив, который может изменять свой размер во время выполнения. Он хранит элементы одного типа в непрерывном блоке памяти.


Вопрос

Как создать новый вектор?

Ответ

Можно использовать макрос vec! или функцию Vec::new():

let v1 = vec![1, 2, 3];
let mut v2: Vec<i32> = Vec::new();
v2.push(5);

Вопрос

Как получить доступ к элементу вектора?

Ответ

С помощью индексации (v[0]) или метода .get():

let v = vec![1, 2, 3];
let third = v[2]; // паника при выходе за границы
let third = v.get(2); // возвращает Option<&T>

Вопрос

Что делает метод .get()?

Ответ

Метод .get() возвращает Option<&T>, что позволяет безопасно обрабатывать попытки доступа к несуществующему индексу без паники.


Вопрос

Что такое HashMap<K, V>?

Ответ

HashMap<K, V> — это коллекция, хранящая пары «ключ-значение». Ключи должны реализовывать трейты Eq и Hash.


Вопрос

Как создать HashMap?

Ответ

Через HashMap::new() или с помощью итератора и метода collect:

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

// Или:
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

Вопрос

Как получить значение из HashMap по ключу?

Ответ

С помощью метода .get():

let team_name = String::from("Blue");
let score = scores.get(&team_name); // возвращает Option<&V>

Вопрос

Что происходит при вставке значения с уже существующим ключом?

Ответ

Старое значение заменяется новым, а старое возвращается как Some(value) методом .insert().


Вопрос

Как обновить значение в HashMap, если оно уже существует?

Ответ

Можно использовать метод .entry():

use std::collections::hash_map::Entry;

scores.entry(String::from("Blue")).or_insert(0);
// или
scores.entry(String::from("Blue")).and_modify(|e| *e += 1).or_insert(1);

Итераторы

Вопрос

Что такое итератор в Rust?

Ответ

Итератор — это объект, который последовательно выдаёт элементы из коллекции или диапазона. Он реализует трейт Iterator.


Вопрос

Как получить итератор из вектора?

Ответ

С помощью методов .iter(), .iter_mut() или .into_iter():

  • .iter() — выдаёт &T (заимствование)
  • .iter_mut() — выдаёт &mut T (изменяемое заимствование)
  • .into_iter() — перемещает владение и выдаёт T

Вопрос

Что делает метод .next() у итератора?

Ответ

Метод .next() возвращает Option<T>, где Some(value) — следующий элемент, а None — признак окончания итерации.


Вопрос

Являются ли итераторы ленивыми?

Ответ

Да. Итераторы в Rust ленивые: они не выполняют никакой работы, пока не вызван метод, потребляющий их (например, .collect() или цикл for).


Вопрос

Как преобразовать итератор в коллекцию?

Ответ

С помощью метода .collect():

let v: Vec<i32> = (1..4).collect(); // [1, 2, 3]

Вопрос

Какие полезные адаптеры итераторов есть в стандартной библиотеке?

Ответ

Популярные адаптеры: .map(), .filter(), .take(), .skip(), .enumerate(), .zip(), .fold(), .any(), .all().


Вопрос

Что делает .map()?

Ответ

.map() применяет замыкание к каждому элементу итератора и возвращает новый итератор с преобразованными значениями:

let doubled: Vec<i32> = vec![1, 2, 3].iter().map(|x| x * 2).collect();

Вопрос

Что делает .filter()?

Ответ

.filter() оставляет только те элементы, для которых замыкание возвращает true:

let evens: Vec<i32> = (1..=10).filter(|x| x % 2 == 0).collect();

Вопрос

Что такое цепочка итераторов?

Ответ

Цепочка итераторов — это последовательное применение нескольких адаптеров без промежуточных выделений памяти. Это эффективно благодаря нулевой стоимости абстракций.


Вопрос

Можно ли создать собственный итератор?

Ответ

Да. Нужно реализовать трейт Iterator и определить ассоциированный тип Item и метод .next().


Умные указатели

Вопрос

Что такое умный указатель в Rust?

Ответ

Умный указатель — это структура данных, которая ведёт себя как указатель, но имеет дополнительные метаданные и поведение, например управление владением или подсчёт ссылок.


Вопрос

Какие умные указатели есть в стандартной библиотеке?

Ответ

Основные: Box<T>, Rc<T>, Arc<T>, RefCell<T>, Mutex<T>, Cell<T>.


Вопрос

Что делает Box<T>?

Ответ

Box<T> выделяет значение в куче и обеспечивает единоличное владение. Он используется для рекурсивных типов и перемещения больших данных в кучу.


Вопрос

Зачем нужен Box для рекурсивных структур?

Ответ

Потому что размер рекурсивной структуры неизвестен на этапе компиляции. Box имеет фиксированный размер и разрывает бесконечную рекурсию в определении типа.

enum List {
Cons(i32, Box<List>),
Nil,
}

Вопрос

Что такое Rc<T>?

Ответ

Rc<T> (Reference Counted) — это умный указатель с подсчётом ссылок, позволяющий множественное неизменяемое владение одним значением в одном потоке.


Вопрос

Можно ли изменять значение через Rc<T>?

Ответ

Нет, напрямую нельзя. Но можно комбинировать Rc<T> с RefCell<T> для внутренней изменяемости.


Вопрос

Что такое RefCell<T>?

Ответ

RefCell<T> обеспечивает внутреннюю изменяемость: проверка правил заимствования переносится на этап выполнения вместо компиляции.


Вопрос

В чём разница между Box<T> и Rc<T>?

Ответ

Box<T> даёт единоличное владение, Rc<T> — разделяемое неизменяемое владение. Rc<T> использует подсчёт ссылок для управления временем жизни.


Вопрос

Что такое Arc<T>?

Ответ

Arc<T> (Atomically Reference Counted) — это потокобезопасная версия Rc<T>, используемая для разделяемого владения между потоками.


Вопрос

Когда использовать Arc<Mutex<T>>?

Ответ

Когда нужно разделяемое и изменяемое владение данными между несколькими потоками. Arc обеспечивает совместное владение, Mutex — безопасный доступ.


Многопоточность

Вопрос

Как создать новый поток в Rust?

Ответ

С помощью функции std::thread::spawn:

use std::thread;

let handle = thread::spawn(|| {
println!("Hello from a thread!");
});

handle.join().unwrap();

Вопрос

Что делает метод .join()?

Ответ

.join() блокирует текущий поток до завершения целевого потока и возвращает результат его выполнения.


Вопрос

Как передать данные в поток?

Ответ

Замыкание, передаваемое в spawn, должно захватывать переменные. По умолчанию захват происходит по перемещению (move):

let s = String::from("hello");
let handle = thread::spawn(move || {
println!("{}", s);
});

Вопрос

Почему нужно использовать move в замыкании потока?

Ответ

Потому что поток может жить дольше, чем область видимости родительской функции. move гарантирует, что данные будут перемещены в поток, а не заимствованы.


Вопрос

Какие два основных способа передачи сообщений между потоками?

Ответ

  1. Через общее состояние с синхронизацией (Arc<Mutex<T>>).
  2. Через каналы (std::sync::mpsc — multi-producer, single-consumer).

Вопрос

Что такое канал (channel) в Rust?

Ответ

Канал — это механизм передачи данных между потоками. Он состоит из отправителя (Sender) и получателя (Receiver).


Вопрос

Как создать канал?

Ответ

use std::sync::mpsc;

let (tx, rx) = mpsc::channel();

Вопрос

Как отправить значение через канал?

Ответ

tx.send(value).unwrap();

Вопрос

Как получить значение из канала?

Ответ

let received = rx.recv().unwrap(); // блокирующий вызов
// или
let received = rx.try_recv().unwrap(); // неблокирующий

Вопрос

Что такое Send и Sync?

Ответ

Send — маркерный трейт, означающий, что тип можно безопасно передавать между потоками.
Sync — маркерный трейт, означающий, что к ссылке на тип можно безопасно обращаться из нескольких потоков одновременно.


Асинхронность (async/await)

Вопрос

Что такое асинхронное программирование в Rust?

Ответ

Асинхронное программирование в Rust позволяет выполнять операции ввода-вывода и другие длительные задачи без блокировки потока, используя ключевые слова async и await.


Вопрос

Что делает ключевое слово async?

Ответ

Ключевое слово async превращает функцию или блок в асинхронный, возвращающий тип impl Future. Такой код не выполняется немедленно, а возвращает значение, представляющее отложенную вычислительную задачу.


Вопрос

Что делает await?

Ответ

Оператор .await приостанавливает выполнение текущей асинхронной функции до тех пор, пока завершится ожидаемое Future, после чего возвращает его результат.


Вопрос

Можно ли использовать await вне async-функции?

Ответ

Нет. Оператор .await может использоваться только внутри тела функции или блока, помеченного как async.


Вопрос

Что такое Future в Rust?

Ответ

Future — это трейт, представляющий асинхронную операцию, которая может завершиться значением в будущем. Он определяет метод poll, который вызывается исполнителем (executor) для продвижения выполнения.


Вопрос

Нужен ли внешний runtime для запуска асинхронного кода?

Ответ

Да. Стандартная библиотека не предоставляет исполнитель (executor). Для запуска async-кода требуется внешний крейт, например tokio или async-std.


Вопрос

Как запустить асинхронную функцию с помощью Tokio?

Ответ

С помощью макроса #[tokio::main]:

#[tokio::main]
async fn main() {
let result = fetch_data().await;
println!("{}", result);
}

Вопрос

Что такое async-блок?

Ответ

Async-блок — это анонимный асинхронный блок кода, который возвращает Future:

let future = async {
do_something().await;
};

Вопрос

Можно ли возвращать Future из обычной функции?

Ответ

Да, если указать возвращаемый тип как impl Future<Output = T>, но такая функция сама не будет async:

use std::future::Future;

fn delayed_greeting() -> impl Future<Output = String> {
async { "Hello!".to_string() }
}

Вопрос

В чём разница между многопоточностью и асинхронностью?

Ответ

Многопоточность использует несколько потоков ОС для параллельного выполнения. Асинхронность использует один поток с кооперативной многозадачностью: задачи добровольно уступают управление при ожидании, что повышает эффективность при большом числе I/O-операций.


Макросы

Вопрос

Что такое макрос в Rust?

Ответ

Макрос — это средство метапрограммирования, которое генерирует код на этапе компиляции. Он позволяет писать повторяющиеся шаблоны более кратко и безопасно.


Вопрос

Какие два типа макросов есть в Rust?

Ответ

  1. Декларативные макросы (macro_rules!)
  2. Процедурные макросы (атрибутные, производные и функциональные)

Вопрос

Как объявить декларативный макрос?

Ответ

С помощью macro_rules!:

macro_rules! say_hello {
() => {
println!("Hello!");
};
}

Вопрос

Чем отличаются декларативные макросы от функций?

Ответ

Макросы работают на уровне синтаксического дерева и могут принимать переменное число аргументов, генерировать разный код в зависимости от входных токенов и создавать новые элементы программы (например, функции или структуры).


Вопрос

Что такое процедурный макрос?

Ответ

Процедурный макрос — это функция на Rust, которая принимает фрагмент кода в виде токенов и возвращает изменённый или новый фрагмент кода. Он компилируется как отдельный крейт.


Вопрос

Какие виды процедурных макросов существуют?

Ответ

  • Производные (#[derive(...)]) — генерируют реализации трейтов
  • Атрибутные (#[my_macro]) — применяются к элементам как атрибуты
  • Функциональные — вызываются как функции, но на этапе компиляции

Вопрос

Приведите пример использования #[derive(Debug)].

Ответ

#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}

let p = Point { x: 1, y: 2 };
println!("{:?}", p); // вывод: Point { x: 1, y: 2 }

Вопрос

Можно ли создать собственный #[derive(...)] макрос?

Ответ

Да, с помощью процедурного макроса типа proc_macro_derive.


Вопрос

Что делает макрос vec!?

Ответ

Макрос vec! создаёт вектор из списка значений:

let v = vec![1, 2, 3]; // эквивалентно Vec::from([1, 2, 3])

Вопрос

Почему макросы заканчиваются восклицательным знаком?

Ответ

Восклицательный знак (!) отличает макросы от обычных функций и подчёркивает, что они раскрываются на этапе компиляции, а не вызываются во время выполнения.


Безопасность и unsafe-код

Вопрос

Что такое безопасный код в Rust?

Ответ

Безопасный код — это код, который подчиняется всем правилам системы владения, заимствования и проверок времени жизни, гарантирующим отсутствие ошибок памяти.


Вопрос

Что такое unsafe-блок?

Ответ

unsafe-блок — это участок кода, в котором разрешены операции, которые компилятор не может проверить на безопасность, но программист гарантирует их корректность.


Вопрос

Какие пять операций разрешены только внутри unsafe?

Ответ

  1. Разыменование сырого указателя (*const T, *mut T)
  2. Вызов unsafe-функции или метода
  3. Доступ или изменение статической переменной static mut
  4. Реализация unsafe-трейта
  5. Чтение или запись в поля Union

Вопрос

Можно ли вызывать безопасные функции внутри unsafe-блока?

Ответ

Да. unsafe-блок не делает весь код небезопасным — он лишь разрешает выполнение конкретных небезопасных операций. Остальной код остаётся под контролем компилятора.


Вопрос

Что такое сырой указатель?

Ответ

Сырой указатель — это низкоуровневый указатель (*const T или *mut T), который не подчиняется правилам владения и может быть недействительным, дублированным или не выровненным.


Вопрос

Когда используется unsafe в стандартной библиотеке?

Ответ

Внутренние реализации коллекций, взаимодействие с системными API, работа с FFI (foreign function interface), низкоуровневые оптимизации.


Вопрос

Является ли unsafe признаком плохого кода?

Ответ

Нет. unsafe необходим для реализации низкоуровневых абстракций. Главное — инкапсулировать unsafe внутри безопасного API, чтобы пользователи не подвергались риску.


Вопрос

Что такое FFI?

Ответ

FFI (Foreign Function Interface) — это механизм вызова функций из других языков (например, C) и предоставления Rust-функций для вызова извне.


Вопрос

Как объявить внешнюю функцию на C?

Ответ

extern "C" {
fn abs(input: i32) -> i32;
}

Используется внутри unsafe-блока при вызове.


Вопрос

Почему FFI требует unsafe?

Ответ

Потому что Rust не может проверить корректность сигнатуры, безопасность передаваемых данных или поведение внешней функции. Ответственность лежит на программисте.


Тестирование и документация

Вопрос

Как писать unit-тесты в Rust?

Ответ

С помощью модуля #[cfg(test)] и функций с аннотацией #[test]:

#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}

Вопрос

Как запустить тесты?

Ответ

Командой cargo test.


Вопрос

Что делает макрос assert_eq!?

Ответ

Сравнивает два значения на равенство. При несовпадении вызывает панику с информативным сообщением.


Вопрос

Как написать интеграционный тест?

Ответ

Интеграционные тесты размещаются в директории tests/ проекта. Каждый файл в этой папке — отдельный крейт, который может использовать библиотеку как внешний зависимый.


Вопрос

Что такое doctest?

Ответ

Doctest — это примеры кода в документационных комментариях, которые автоматически тестируются как часть тестового набора.


Вопрос

Как написать документационный комментарий?

Ответ

С помощью трёх слэшей:

/// Adds two numbers.
///
/// # Examples
///
/// ```
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

Вопрос

Как сгенерировать документацию?

Ответ

Командой cargo doc. Документация открывается через cargo doc --open.


Вопрос

Можно ли исключить функцию из документации?

Ответ

Да, с помощью атрибута #[doc(hidden)].


Вопрос

Что делает атрибут #[should_panic]?

Ответ

Указывает, что тест должен завершиться паникой. Тест считается успешным, если паника произошла:

#[test]
#[should_panic]
fn test_panic() {
panic!("Oops!");
}

Вопрос

Как протестировать ошибки (Result)?

Ответ

Проверяя, что результат — Err или Ok, с помощью match, assert!(result.is_err()) или извлечения значения:

let result = some_function_that_might_fail();
assert!(result.is_err());