19 апреля 2025
Rust неизменно привлекает интерес разработчиков, удерживая звание «самого любимого» языка программирования в опросах Stack Overflow несколько лет подряд. Это не просто хайп; Rust предлагает убедительное сочетание производительности, безопасности и современных возможностей языка, которые решают распространенные проблемы, встречающиеся в других языках системного программирования. Если вам интересно, что делает Rust особенным, и вы хотите начать свой путь, это руководство для начинающих предоставит базовые знания для старта. Мы изучим основной синтаксис, уникальные концепции, такие как владение, и основные инструменты, составляющие экосистему Rust.
Rust позиционирует себя как язык для создания надежного и эффективного программного обеспечения. Его основные преимущества заключаются в безопасности памяти без использования сборщика мусора и обеспечении бесстрашной конкурентности. Вот почему вам стоит рассмотреть изучение Rust:
Прежде чем написать свою первую строку кода на Rust, вам необходимо настроить инструментарий Rust. Стандартный и рекомендуемый способ установки Rust — использование rustup
, установщика инструментария Rust.
rustup
: Этот инструмент командной строки управляет вашими установками Rust. Он позволяет устанавливать, обновлять и легко переключаться между различными версиями Rust (например, стабильной, бета-версией или ночными сборками). Посетите официальный сайт Rust (https://www.rust-lang.org/tools/install) для получения инструкций по установке, специфичных для вашей операционной системы.rustc
: Это компилятор Rust. После того как вы напишете свой исходный код Rust в файлах .rs
, rustc
компилирует их в исполняемые бинарные файлы или библиотеки, которые ваш компьютер может понять. Хотя rustc
и важен, вы обычно не будете вызывать его напрямую очень часто в своем повседневном рабочем процессе.cargo
: Это система сборки и менеджер пакетов Rust, и это инструмент, с которым вы будете взаимодействовать чаще всего. Cargo организует множество общих задач разработки:
cargo new
).cargo build
).cargo run
).cargo test
).Cargo.toml
).Для быстрых экспериментов без необходимости локальной установки отличными вариантами являются онлайн-платформы, такие как официальная песочница Rust или интегрированные среды разработки, такие как Replit.
Давайте начнем с традиционной программы "Hello, world!", обряда посвящения при изучении любого нового языка. Создайте файл с именем main.rs
и добавьте следующий код:
fn main() {
// Эта строка выводит текст в консоль
println!("Привет, Rust!");
}
Чтобы скомпилировать и запустить эту простую программу с использованием базовых инструментов:
main.rs
, и выполните:
rustc main.rs
Эта команда вызывает компилятор Rust (rustc
), который создает исполняемый файл (например, main
в Linux/macOS, main.exe
в Windows)../main
# В Windows используйте: .\main.exe
Однако для всего, что выходит за рамки одного файла, использование Cargo является стандартным и гораздо более удобным подходом:
cargo new hello_rust
cd hello_rust
Cargo создает новый каталог с именем hello_rust
, содержащий подкаталог src
с файлом main.rs
(уже заполненным кодом "Привет, Rust!") и конфигурационный файл с именем Cargo.toml
.cargo run
Cargo обработает этап компиляции, а затем выполнит результирующую программу, отобразив “Привет, Rust!” в вашей консоли.Давайте разберем фрагмент кода:
fn main()
: Определяет главную функцию. Ключевое слово fn
обозначает объявление функции. main
— это специальное имя функции; это точка входа, с которой начинается выполнение любой исполняемой программы Rust. Скобки ()
указывают, что эта функция не принимает входных параметров.{}
: Фигурные скобки определяют блок кода или область видимости. Весь код, принадлежащий функции, находится внутри этих скобок.println!("Привет, Rust!");
: Эта строка выполняет действие по выводу текста в консоль.
println!
— это макрос Rust. Макросы похожи на функции, но имеют ключевое отличие: они заканчиваются восклицательным знаком !
. Макросы выполняют генерацию кода во время компиляции, предлагая больше мощности и гибкости, чем обычные функции (например, обработку переменного числа аргументов, что делает println!
). Макрос println!
выводит предоставленный текст в консоль и автоматически добавляет символ новой строки в конце."Привет, Rust!"
— это строковый литерал – фиксированная последовательность символов, представляющая текст, заключенная в двойные кавычки.;
: Точка с запятой отмечает конец инструкции. Большинство строк исполняемого кода Rust (инструкций) заканчиваются точкой с запятой.Теперь давайте погрузимся в фундаментальные строительные блоки языка программирования Rust.
Переменные используются для хранения значений данных. В Rust вы объявляете переменные с помощью ключевого слова let
.
let apples = 5;
let message = "Возьми пять";
Основная концепция в Rust заключается в том, что переменные неизменяемы по умолчанию. Это означает, что как только значение привязано к имени переменной, вы не можете изменить это значение позже.
let x = 10;
// x = 15; // Эта строка вызовет ошибку времени компиляции! Нельзя присвоить значение дважды неизменяемой переменной `x`.
println!("Значение x равно: {}", x); // {} — это заполнитель для значения x
Эта неизменяемость по умолчанию является осознанным проектным решением, которое помогает писать более безопасный и предсказуемый код, предотвращая случайное изменение данных, что может быть частым источником ошибок. Если вам действительно нужна переменная, значение которой может изменяться, вы должны явно пометить ее как изменяемую с помощью ключевого слова mut
при объявлении.
let mut count = 0; // Объявляем 'count' как изменяемую
println!("Начальное значение счетчика: {}", count);
count = 1; // Это разрешено, потому что 'count' была объявлена с 'mut'
println!("Новое значение счетчика: {}", count);
Rust также допускает затенение. Вы можете объявить новую переменную с тем же именем, что и предыдущая переменная в той же области видимости. Новая переменная “затеняет” старую, что означает, что последующее использование имени относится к новой переменной. Это отличается от изменения, потому что мы создаем совершенно новую переменную, которая может даже иметь другой тип.
let spaces = " "; // 'spaces' изначально является строковым срезом (&str)
let spaces = spaces.len(); // 'spaces' теперь затенена новой переменной, содержащей длину (целое число, usize)
println!("Количество пробелов: {}", spaces); // Выводит целочисленное значение
Rust — это статически типизированный язык. Это означает, что тип каждой переменной должен быть известен компилятору во время компиляции. Однако Rust обладает превосходным выводом типов. Во многих ситуациях вам не нужно явно указывать тип; компилятор часто может определить его на основе значения и того, как вы его используете.
let quantity = 10; // Компилятор выводит i32 (знаковый целочисленный тип по умолчанию)
let price = 9.99; // Компилятор выводит f64 (тип с плавающей запятой по умолчанию)
let active = true; // Компилятор выводит bool (логический тип)
let initial = 'R'; // Компилятор выводит char (символьный тип)
Если вы хотите или должны быть явными (например, для ясности или когда компилятору нужна помощь), вы можете предоставить аннотации типов, используя двоеточие :
с последующим именем типа.
let score: i32 = 100; // Явно знаковое 32-битное целое число
let ratio: f32 = 0.5; // Явно число с плавающей запятой одинарной точности
let is_complete: bool = false; // Явно логический тип
let grade: char = 'A'; // Явно символ (скалярное значение Unicode)
Rust имеет несколько встроенных скалярных типов (представляющих одиночные значения):
i8
, i16
, i32
, i64
, i128
, isize
) хранят как положительные, так и отрицательные целые числа. Беззнаковые целые числа (u8
, u16
, u32
, u64
, u128
, usize
) хранят только неотрицательные целые числа. Типы isize
и usize
зависят от архитектуры компьютера (32-битной или 64-битной) и в основном используются для индексации коллекций.f32
(одинарная точность) и f64
(двойная точность). По умолчанию используется f64
.bool
имеет два возможных значения: true
или false
.char
представляет одно скалярное значение Unicode (более всеобъемлющее, чем просто символы ASCII), заключенное в одинарные кавычки (например, 'z'
, 'π'
, '🚀'
).String
против &str
Обработка текста в Rust часто включает два основных типа, что может сбивать с толку новичков:
&str
(произносится “строковый срез”): Это неизменяемая ссылка на последовательность байтов в кодировке UTF-8, хранящуюся где-то в памяти. Строковые литералы (например, "Привет"
) имеют тип &'static str
, что означает, что они хранятся непосредственно в бинарном файле программы и существуют в течение всего времени выполнения программы. Срезы предоставляют представление строковых данных, не владея ими. Они имеют фиксированный размер.String
: Это владеющий, расширяемый, изменяемый строковый тип в кодировке UTF-8. Данные String
хранятся в куче, что позволяет изменять их размер. Обычно вы используете String
, когда вам нужно изменять строковые данные, или когда строка должна владеть своими данными и управлять своим временем жизни (часто при возвращении строк из функций или хранении их в структурах).// Строковый литерал (хранится в бинарном файле программы, неизменяемый)
let static_slice: &'static str = "Я неизменяем";
// Создаем владеющую, размещенную в куче строку String из литерала
let mut dynamic_string: String = String::from("Начало");
// Изменяем String (возможно, потому что она изменяема и владеет своими данными)
dynamic_string.push_str(" и продолжение");
println!("{}", dynamic_string); // Вывод: Начало и продолжение
// Создаем строковый срез, который ссылается на часть String
// Этот срез заимствует данные у dynamic_string
let slice_from_string: &str = &dynamic_string[0..6]; // Ссылается на "Начало" (индексы байт, осторожно с не-ASCII)
println!("Срез: {}", slice_from_string);
Функции являются основой для организации кода в именованные, повторно используемые блоки. Мы уже сталкивались со специальной функцией main
.
// Определение функции
fn greet(name: &str) { // Принимает один параметр: 'name', который является строковым срезом (&str)
println!("Привет, {}!", name);
}
// Функция, которая принимает два параметра i32 и возвращает i32
fn add(a: i32, b: i32) -> i32 {
// В Rust последнее выражение в теле функции автоматически возвращается,
// если оно не заканчивается точкой с запятой.
a + b
// Это эквивалентно написанию: return a + b;
}
fn main() {
greet("Алиса"); // Вызываем функцию 'greet'
let sum = add(5, 3); // Вызываем 'add', привязываем возвращенное значение к 'sum'
println!("5 + 3 = {}", sum); // Вывод: 5 + 3 = 8
}
Ключевые моменты о функциях:
fn
для объявления функций.->
. Если функция не возвращает значение, ее тип возвращаемого значения неявно ()
(пустой кортеж, часто называемый “unit type”).;
) и опционально заканчиваются выражением (что-то, что вычисляется в значение).return
можно использовать для явного, досрочного возврата из любого места внутри функции.Rust предоставляет стандартные структуры управления потоком выполнения для определения порядка выполнения кода:
if
/else
/else if
: Используется для условного выполнения.let number = 6;
if number % 4 == 0 {
println!("число делится на 4");
} else if number % 3 == 0 {
println!("число делится на 3"); // Эта ветка выполняется
} else {
println!("число не делится на 4 или 3");
}
// Важно, что 'if' является выражением в Rust, то есть оно вычисляется в значение.
// Это позволяет использовать его непосредственно в инструкциях 'let'.
let condition = true;
let value = if condition { 5 } else { 6 }; // value будет равно 5
println!("Значение равно: {}", value);
// Примечание: Обе ветви выражения 'if' должны вычисляться в один и тот же тип.
loop
: Создает бесконечный цикл. Обычно вы используете break
для выхода из цикла, опционально возвращая значение.while
: Цикл выполняется, пока указанное условие остается истинным.for
: Итерирует по элементам коллекции или диапазона. Это наиболее часто используемый и часто самый безопасный тип цикла в Rust.// Пример loop
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Выход из цикла и возврат 'counter * 2'
}
};
println!("Результат цикла: {}", result); // Вывод: Результат цикла: 20
// Пример while
let mut num = 3;
while num != 0 {
println!("{}!", num);
num -= 1;
}
println!("ПОЕХАЛИ!!!");
// Пример for (итерация по массиву)
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // .iter() создает итератор по элементам массива
println!("значение: {}", element);
}
// Пример for (итерация по диапазону)
// (1..4) создает диапазон, включающий 1, 2, 3 (не включая 4)
// .rev() разворачивает итератор
for number in (1..4).rev() {
println!("{}!", number); // Выводит 3!, 2!, 1!
}
println!("СНОВА ПОЕХАЛИ!!!");
Используйте комментарии для добавления пояснений и заметок к вашему коду, которые компилятор будет игнорировать.
// Это однострочный комментарий. Он продолжается до конца строки.
/*
* Это многострочный, блочный комментарий.
* Он может занимать несколько строк и полезен
* для более длинных объяснений.
*/
let lucky_number = 7; // Вы также можете размещать комментарии в конце строки.
Владение — самая отличительная и центральная особенность Rust. Это механизм, который позволяет Rust гарантировать безопасность памяти во время компиляции без необходимости в сборщике мусора. Понимание владения является ключом к пониманию Rust. Оно следует трем основным правилам:
{ // s здесь не действительна, она еще не объявлена
let s = String::from("привет"); // s действительна с этого момента;
// s 'владеет' данными String, размещенными в куче.
// Вы можете использовать s здесь
println!("{}", s);
} // Область видимости здесь заканчивается. 's' больше не действительна.
// Rust автоматически вызывает специальную функцию 'drop' для String, которой 'владеет' s,
// освобождая ее память в куче.
Когда вы присваиваете владеемое значение (например, String
, Vec
или структуру, содержащую владеемые типы) другой переменной или передаете его в функцию по значению, владение перемещается. Исходная переменная становится недействительной.
let s1 = String::from("оригинал");
let s2 = s1; // Владение данными String ПЕРЕМЕЩЕНО от s1 к s2.
// s1 больше не считается действительной после этого момента.
// println!("s1 равно: {}", s1); // Ошибка времени компиляции! Значение заимствовано здесь после перемещения.
// s1 больше не владеет данными.
println!("s2 равно: {}", s2); // s2 теперь является владельцем и действительна.
Такое поведение перемещения предотвращает ошибки “двойного освобождения”, когда две переменные могут случайно попытаться освободить одно и то же место в памяти, когда они выходят из области видимости. Примитивные типы, такие как целые числа, числа с плавающей запятой, логические значения и символы, реализуют трейт Copy
, что означает, что они просто копируются вместо перемещения при присваивании или передаче в функции.
Что делать, если вы хотите позволить функции использовать значение, не передавая владение? Вы можете создать ссылки. Создание ссылки называется заимствованием. Ссылка позволяет вам получить доступ к данным, принадлежащим другой переменной, не принимая их во владение.
// Эта функция принимает ссылку (&) на String.
// Она заимствует String, но не берет владение.
fn calculate_length(s: &String) -> usize {
s.len()
} // Здесь s (ссылка) выходит из области видимости. Но поскольку она не владеет
// данными String, данные НЕ удаляются, когда ссылка выходит из области видимости.
fn main() {
let s1 = String::from("привет");
// Мы передаем ссылку на s1, используя символ '&'.
// s1 по-прежнему владеет данными String.
let len = calculate_length(&s1);
// s1 все еще действительна здесь, потому что владение никогда не перемещалось.
println!("Длина '{}' равна {}.", s1, len);
}
Ссылки по умолчанию неизменяемы, как и переменные. Если вы хотите изменить заимствованные данные, вам нужна изменяемая ссылка, обозначаемая &mut
. Однако Rust применяет строгие правила в отношении изменяемых ссылок для предотвращения гонок данных:
Правила заимствования:
&mut T
).&T
).Эти правила обеспечиваются компилятором.
// Эта функция принимает изменяемую ссылку на String
fn change(some_string: &mut String) {
some_string.push_str(", мир");
}
fn main() {
// 's' должна быть объявлена изменяемой, чтобы разрешить изменяемое заимствование
let mut s = String::from("привет");
// Пример принудительного применения правил заимствования (раскомментируйте строки, чтобы увидеть ошибки):
// let r1 = &s; // неизменяемое заимствование - OK
// let r2 = &s; // еще одно неизменяемое заимствование - OK (разрешено несколько неизменяемых заимствований)
// let r3 = &mut s; // ОШИБКА! Нельзя заимствовать `s` как изменяемое, пока активны неизменяемые заимствования
// // Rust обеспечивает: либо несколько читателей (&T), ЛИБО один писатель (&mut T), никогда оба сразу
// println!("{}, {}", r1, r2); // Использование r1/r2 сохраняет их активными, вызывая ошибку
// // Без этого println, NLL (Non-Lexical Lifetimes) Rust освободил бы r1/r2 раньше,
// // делая &mut s действительным здесь
// Изменяемое заимствование разрешено здесь, потому что нет других активных заимствований
change(&mut s);
println!("{}", s); // Вывод: привет, мир
}
Rust предоставляет способы группировки нескольких значений в более сложные типы.
struct
)Структуры (сокращение от structures) позволяют определять пользовательские типы данных путем группировки связанных полей данных под одним именем.
// Определение структуры с именем User
struct User {
active: bool,
username: String, // Использует владеющий тип String
email: String,
sign_in_count: u64,
}
fn main() {
// Создание экземпляра структуры User
// Экземпляры должны предоставлять значения для всех полей
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// Доступ к полям структуры с использованием точечной нотации
// Экземпляр должен быть изменяемым для изменения значений полей
user1.email = String::from("anotheremail@example.com");
println!("Email пользователя: {}", user1.email);
// Использование вспомогательной функции для создания экземпляра User
let user2 = build_user(String::from("user2@test.com"), String::from("user2"));
println!("Статус активности User 2: {}", user2.active);
// Синтаксис обновления структуры: Создает новый экземпляр, используя некоторые поля
// из существующего экземпляра для оставшихся полей.
let user3 = User {
email: String::from("user3@domain.com"),
username: String::from("user3"),
..user2 // берет значения 'active' и 'sign_in_count' из user2
};
println!("Количество входов User 3: {}", user3.sign_in_count);
}
// Функция, которая возвращает экземпляр User
fn build_user(email: String, username: String) -> User {
User {
email, // Сокращенная инициализация полей: если имя параметра совпадает с именем поля
username,
active: true,
sign_in_count: 1,
}
}
Rust также поддерживает кортежные структуры, которые являются именованными кортежами (например, struct Color(i32, i32, i32);
), и юнит-подобные структуры (или структуры-маркеры), которые не имеют полей и полезны, когда вам нужно реализовать трейт для типа, но не нужно хранить какие-либо данные (например, struct AlwaysEqual;
).
enum
)Перечисления (enumerations) позволяют определить тип путем перечисления его возможных вариантов. Значение перечисления может быть только одним из его возможных вариантов.
// Простое перечисление, определяющее типы IP-адресов
enum IpAddrKind {
V4, // Вариант 1
V6, // Вариант 2
}
// Варианты перечисления также могут содержать связанные данные
enum IpAddr {
V4(u8, u8, u8, u8), // Вариант V4 содержит четыре значения u8
V6(String), // Вариант V6 содержит String
}
// Очень распространенное и важное перечисление в стандартной библиотеке Rust: Option<T>
// Оно кодирует концепцию значения, которое может присутствовать или отсутствовать.
// enum Option<T> {
// Some(T), // Представляет наличие значения типа T
// None, // Представляет отсутствие значения
// }
// Еще одно ключевое перечисление стандартной библиотеки: Result<T, E>
// Используется для операций, которые могут завершиться успешно (Ok) или неудачно (Err).
// enum Result<T, E> {
// Ok(T), // Представляет успех, содержит значение типа T
// Err(E), // Представляет неудачу, содержит значение ошибки типа E
// }
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
// Создание экземпляров перечисления IpAddr со связанными данными
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
// Пример с Option<T>
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
// Оператор управления потоком 'match' идеально подходит для работы с перечислениями.
// Он позволяет выполнять различный код в зависимости от варианта перечисления.
match some_number {
Some(i) => println!("Получено число: {}", i), // Если это Some, привязать внутреннее значение к i
None => println!("Ничего не получено."), // Если это None
}
// 'match' должен быть исчерпывающим: вы должны обработать все возможные варианты.
// Подчеркивание '_' можно использовать как шаблон-джокер для перехвата любых вариантов,
// не перечисленных явно.
}
Option<T>
и Result<T, E>
занимают центральное место в подходе Rust к надежной обработке потенциально отсутствующих значений и ошибок.
Cargo является незаменимой частью экосистемы Rust, упрощая процесс сборки, тестирования и управления проектами Rust. Это одна из функций, которую часто хвалят разработчики.
Cargo.toml
: Это файл манифеста для вашего проекта Rust. Он написан в формате TOML (Tom's Obvious, Minimal Language). Он содержит важные метаданные о вашем проекте (например, его имя, версию и авторов) и, что особенно важно, перечисляет его зависимости (другие внешние крейты, на которые полагается ваш проект).
[package]
name = "my_project"
version = "0.1.0"
edition = "2021" # Указывает редакцию Rust для использования (влияет на возможности языка)
# Зависимости перечислены ниже
[dependencies]
# Пример: Добавление крейта 'rand' для генерации случайных чисел
# rand = "0.8.5"
# При сборке Cargo загрузит и скомпилирует 'rand' и его зависимости.
cargo new <project_name>
: Создает структуру нового проекта бинарного (исполняемого) приложения.cargo new --lib <library_name>
: Создает структуру нового проекта библиотеки (крейт, предназначенный для использования другими программами).cargo build
: Компилирует ваш проект и его зависимости. По умолчанию создается неоптимизированная отладочная сборка. Вывод помещается в каталог target/debug/
.cargo build --release
: Компилирует ваш проект с включенными оптимизациями, подходит для распространения или тестирования производительности. Вывод помещается в каталог target/release/
.cargo run
: Компилирует (при необходимости) и запускает ваш бинарный проект.cargo check
: Быстро проверяет ваш код на наличие ошибок компиляции без фактического создания конечного исполняемого файла. Обычно это намного быстрее, чем cargo build
, и полезно во время разработки для быстрой обратной связи.cargo test
: Запускает любые тесты, определенные в вашем проекте (обычно расположенные в каталоге src
или в отдельном каталоге tests
).Когда вы добавляете зависимость в свой файл Cargo.toml
, а затем выполняете команду, такую как cargo build
или cargo run
, Cargo автоматически обрабатывает загрузку необходимого крейта (и его зависимостей) из центрального репозитория crates.io и компилирует все вместе.
Это стартовое руководство охватило самые основы, чтобы помочь вам начать. Язык Rust предлагает множество более мощных функций для изучения по мере вашего продвижения:
Result
, распространение ошибок с помощью оператора ?
и определение пользовательских типов ошибок.Vec<T>
(динамические массивы/векторы), HashMap<K, V>
(хеш-таблицы), HashSet<T>
и т. д.<T>
).'a
).Mutex
и Arc
, а также мощного синтаксиса async
/await
Rust для асинхронного программирования.Rust представляет собой уникальное и убедительное предложение: чистая производительность, ожидаемая от низкоуровневых языков, таких как C++, в сочетании с сильными гарантиями безопасности во время компиляции, которые устраняют целые классы распространенных ошибок, особенно связанных с управлением памятью и конкурентностью. Хотя кривая обучения включает усвоение новых концепций, таких как владение и заимствование (и прислушивание к иногда строгому компилятору), результатом является программное обеспечение, которое является высоконадежным, эффективным и часто более простым в обслуживании в долгосрочной перспективе. Благодаря отличным инструментам через Cargo и поддерживающему сообществу, Rust — это язык, изучение которого приносит удовлетворение.
Начинайте с малого, экспериментируйте с помощью Cargo, принимайте полезные (хотя иногда и многословные) сообщения об ошибках компилятора как руководство, и вы будете на верном пути к освоению этого мощного и все более популярного языка. Удачного программирования на Rust!