Островок Капча
Островок Капча
Островок Капча

Если бы я хотел стать разработчиком на Rust в 2025, с чего бы я начал?

Аватарка пользователя Анна Ельцова
для
Логотип компании Tproger
Tproger
Отредактировано

Вместе с экспертами Solvery разбираемся, что нужно учить, чтобы стать прогером на Rust сейчас.

915 открытий5К показов
Если бы я хотел стать разработчиком на Rust в 2025, с чего бы я начал?

Rust — язык общего назначения, который ориентирован на высокую производительность и безопасное управление памятью. На нем можно писать софт практически для любого направления: CLI-утилиты, высоконагруженные сервера, десктопные приложения, мобильные приложения (с некоторыми оговорками), игры и игровые движки, прошивки для микроконтроллеров, операционные системы, драйвера и даже браузерные приложения (через компиляцию в WebAssembly).

В рейтинге языков программирования TIOBE Rust занимает 14 место. Для сравнения — в прошлом марте он был на 17 позиции. Вместе с экспертами Solvery Василием Кузенковым, full-stack разработчиком в Web3 стартапе, и Дмитрием Беляевым, Rust developer в Wildberries, разбираемся, как стать разрабом на Расте в 2025 году.

Немного об особенностях

Если вы до этого программировали на ООП языках, то вам, возможно, бросалось в глаза отсутствие привычных классов. Вместо них здесь алгебраические типы данных и трейты для решения expression problem. Их механизм куда больше похож на typeclasses из Haskell, что также будет для вас новой концепцией при построении крупных приложений, и нужно будет перестраивать мышление.

Также когда стартуешь, немного непривычно работать с move-семантикой, RAII, borrow checker’ом и лайф-таймами. Но компилятор сыпет довольно подробными ошибками, которые можно легко поправить, если разобраться.
Василий Кузенковfull-stack разработчик в Web3 стартапе, ментор Solvery

А еще у Rust очень строгая типизация и очень мощная система типов, а сами типы построены так, чтобы предоставлять некоторые гарантии программисту. Например, ссылки в Rust всегда ссылаются на объект, гарантировано существующий в памяти, а стандартные строки содержат только валидный UTF-8. При этом в подавляющем большинстве случаев тип необязательно указывать явно, компилятор способен выводить типы, анализируя контекст функции целиком. Кроме того, Rust следует идеологии «компилируется, значит, работает». От логических ошибок, конечно, Раст не спасёт, но тем не менее очень большой пласт багов можно отловить на этапе кодинга. Да, это сложно, но лучше помучиться при разработке, чем потом разбираться почему упал прод.

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

Хороший пример здесь — итерация по различным коллекциям. Многие языки позволяют использовать итераторы с их абстракциями вроде map или filter. Только такой код, как правило, будет в несколько раз медленнее, чем если то же самое переписать на циклы. Компилятор Rust способен развернуть такой итератор в обычные циклы сам, и производительность будет сравнима, а порой даже лучше, так как программисты часто при написании низкоуровневого кода заставляют процессор делать много лишних вычислений.
Дмитрий БеляевRust developer в Wildberries, ментор Solvery

Сложно ли переходить на Rust

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

На рынке фактически все вакансии требуют уже какого-то опыта в разработке. И сложность перехода сильно разнится от вашего текущего стека. Для Go-программиста переход будет средне-сложным, а язык, возможно, покажется перегруженным. Для С++ — менее сложным, но язык покажется местами ограничивающим. Я переходил на него с JS/TS’а и столкнулся со множеством низкоуровневых концепций, о которых раньше мог не задумываться. Но если у вас есть опыт в системном программировании — переходить будет в разы проще.
Василий Кузенковfull-stack разработчик в Web3 стартапе, ментор Solvery

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

В моей практике менторства много успешных кейсов перехода на Rust с самых разных языков, но проще всего он даётся тем, кто раньше писал на современном C++ и уже понимает такие концепции, как move-семантика и RAII. Много привычного здесь обнаружат и те, кто писал на функциональных языках (Haskell или OCaml). Но в целом, для остальных тоже нет никаких проблем, даже если вы совсем новичок.
Дмитрий БеляевRust developer в Wildberries, ментор Solvery

С чего начать изучать Rust

Вместе со стартом в изучении языка с The Rust Book — бесплатной официальной книгой по Rust’у — стоит углубить свои знания в более низкоуровневых вещах: в чем разница стека и кучи, что такое разметка памяти и адресация в памяти, как работает процессор, подходы и проблемы многопоточного программирования, плюс почитать про операционные системы и сети. Если вы решили осознанно применять Rust, оно вам пригодится.

Начать можно даже имея только самую базу в программировании: переменные, ветвления, циклы, функции. Крайне желательно разобраться в устройстве памяти, что такое стек и куча, а так же какие области памяти бывают помимо них.
Дмитрий БеляевRust developer в Wildberries, ментор Solvery

Вот примерный список того, что нужно учить на старте:

  • Переменные. Они по умолчанию неизменяемые (let x). Чтобы x стал изменяемым, нужно указать это явно через let mut. Константы (const) и статические переменные (static) вам будут нужны редко, они имеют свои особенности.
  • Типы. Стоит разобраться с составными типами, такими как массивы и кортежи, а также с пользовательскими объявляемыми конструкциями struct (тип-произведение) и enum (тип-сумма). Также типы данных в Расте есть стандартные: bool, i32, u64 и прочие числовые, но строки могут удивить, так как их видов сильно больше.
  • Match. Нужно понять такую вещь, как pattern-matching, познакомиться с оператором match, а также осознать, что pattern-matching применяется не только в нём, а везде, где возможно объявление переменных.

Таким образом, вы можете сразу проверить что-то и присвоить результат в переменную:

			let total_count = if count < 0 { 0 } else { count }

		

Здесь мы сразу проверили и присвоили:

			let totalCount = count;
if (count < 0) {
  totalCount = 0
		

Тут нам потребовалась дополнительная изменяемая переменная.

Что такое Cargo

Cargo — это консольная утилита, которая устанавливается вместе с компилятором языка. Она служит одновременно для управления зависимостями, сборки проекта и запуска тестов. Плюс для Cargo есть расширения, например, в поставке по умолчанию уже есть форматтер и линтер clippy.

Cargo рассчитан на то, что вы будете запускать его через терминал, самые полезные команды это:

  • cargo new — создаёт новый шаблонный проект в указанной папке;
  • cargo build — собирает проект;
  • cargo run — собирает проект и запускает получившийся исполняемый файл;
  • cargo check — dry-run сборки, делает все проверки компилятора, но ничего не собирает, что заметно быстрее полноценной сборки;
  • cargo test — собирает проект со всеми тестами и запускает их;
  • cargo fmt — форматирует проект в общепринятый стиль кода;
  • cargo clippy — запускает линтер, очень полезно, можно подсказать более оптимальные варианты кода, найти некоторые потенциальные логические ошибки;
  • cargo install — устанавливает пакет, содержащий исполняемые файлы;
  • cargo clean — очищает все артефакты сборки.

Cargo позволяет описать структуру, настройки (профили сборки, описание крейта) и зависимости вашего крейта или даже монорепозитория (с помощью workspace). Кроме непосредственного запуска через терминал, многие вещи могут запускаться через средства интеграции в IDE, такие как rust-analyzer для VSCode.

Как работать с ownership, borrowing и lifetimes?

Для многих новичков системы владения (ownership), заимствования (borrowing) и времен жизни (lifetimes) выливаются в борьбу с компилятором раста. Эти механизмы нужны в первую очередь для безопасности памяти без сборщика мусора. Общее правило владения такое:

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

			fn main() {
    // s становится владельцем строки
    let s = String::from("hello");
    
    // Владение передается функции takes_ownership
    takes_ownership(s);
    
    // s больше не действительна здесь, попытка использовать s вызовет ошибку компиляции
    // println!("{}", s); // Ошибка!
}
fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // some_string выходит из области видимости и drop вызывается, память освобождается

		

Когда значение перемещается (передается другой переменной или функции), владение переходит, и исходная переменная становится недействительной.

Для типов, реализующих трейт Copy (например, целые числа, булевы значения), значения копируются автоматически:

			let x = 5;
let y = x; // x копируется в y, обе переменные действительны
let s1 = String::from("hello");
let s2 = s1.clone(); // Явное клонирование для типов без трейта Copy

		

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

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

Заимствование позволяет использовать значение без получения владения через ссылки (& и &mut). Для мутабельных ссылок `&mut` есть дополнительные ограничения: в каждый момент времени может существовать только одна изменяемая ссылка на значение.

Нельзя иметь изменяемую ссылку, если уже есть неизменяемая ссылка на то же значение.

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

			// 'a — параметр времени жизни
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

		

Аннотация 'a указывает, что возвращаемая ссылка будет жить как минимум столько же, сколько кратчайшая из входных ссылок.

Онлайн-курс «JAVA-разработчик» от EdMe.pro
  • постоянный доступ
  • бесплатно
  • онлайн
tproger.ru

Общие советы по заимствованию здесь такие:

  • Используйте ссылки, когда не нужно владение.
  • Возвращайте значения из функций для передачи владения обратно.
  • Используйте клонирование для создания новых экземпляров (с пониманием стоимости).
  • Используйте типы с трейтом Copy, когда это возможно.
  • Еще полезно не забывать о контейнерах. Rc<T> позволяет иметь несколько владельцев одного значения через счетчик ссылок, а RefCell<T> обеспечивает проверку правил заимствования в рантайме.

О структурах, перечислениях, модулях и функциях, замыканиях, итераторах

Структуры (structs) в Rust позволяют создавать пользовательские типы данных, объединяющие связанные значения и выступающие типом произведения.

			// Тип User содержит все возможные комбинации username, email, active
struct User {
    username: String,
    email: String,
    active: bool,
}

// Создание экземпляра
let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
};

// Изменяемый экземпляр
let mut user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: true,
};
user2.email = String::from("newemail@example.com");

fn build_user(email: String, username: String) -> User {
    User {
        email,      // Сокращение для email: email
        username,   // Сокращение для username: username
        active: true,
    }
}

let user2 = User {
    email: String::from("another@example.com"),
    ..user1  // Остальные поля берутся из user1
};

		

Перечисления (enum) позволяют определить тип, перечисляя все возможные варианты значений.

			enum IpAddrKind {
    V4,
    V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

		

Перечисления в Rust являются типами суммы, поскольку значение может быть одним из вариантов:

			enum Shape {
    Circle(f64),              // радиус
    Rectangle(f64, f64),      // ширина и высота
    Triangle(f64, f64, f64),  // три стороны
}

		

Тип Shape представляет объединение (сумму) всех возможных вариантов. И к этому есть мощное сопоставление с образцом для работы с ADT:

			fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(radius) => std::f64::consts::PI * radius * radius,
        Shape::Rectangle(width, height) => width * height,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}

		

Модули позволяют организовать код и контролировать видимость элементов. Определяются они через синтаксис mod <name> {} и могут быть вложенны друг в друга:

			// В файле src/lib.rs или src/main.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
        fn seat_at_table() {}
    }
    
    mod serving {
        fn take_order() {}
        fn serve_order() {}
        fn take_payment() {}
    }
}
// Использование модуля
pub fn eat_at_restaurant() {
    // Абсолютный путь
    crate::front_of_house::hosting::add_to_waitlist();
    
    // Относительный путь
    front_of_house::hosting::add_to_waitlist();
}

		

Также модули могут быть организованы в различных файлах (имя файла в таком случае будет именем модуля):

			// src/front_of_house.rs
pub mod hosting {
    pub fn add_to_waitlist() {}
}
// src/lib.rs
mod front_of_house; // Загружает содержимое из src/front_of_house.rs
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

		

Импорт модуля осуществляется через ключевое слово use:

			use std::collections::HashMap;
use std::io::{self, Write};
use std::fmt::Result;
use std::io::Result as IoResult;
// Реэкспорт
pub use crate::front_of_house::hosting;

		

Функции в Rust определяются через ключевое слово fn:

			fn add(x: i32, y: i32) -> i32 {
    x + y  // Неявное возвращение (без точки с запятой)
}

fn print_value(x: i32) {
    println!("Value: {}", x);
}

		

Можно делать функции высшего порядка и передавать в них, как обычные, так и анонимные функции:

			fn apply_twice<F>(f: F, x: i32) -> i32
where
    F: Fn(i32) -> i32,
{
    f(f(x))
}

let result = apply_twice(|x| x + 1, 5);  // 7

// Частичное применение функций через замыкания
let add = |x, y| x + y;
let add_five = |y| add(5, y);

// Композиция функций
let compose = |f, g, x| f(g(x));
let add_one = |x| x + 1;
let multiply_by_two = |x| x * 2;
let add_one_then_multiply = |x| compose(multiply_by_two, add_one, x);

		

Еще одним элементом функционального программирования в Rust выступает итератор:

			struct Counter {
    count: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Counter {
        Counter { count: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;
    
    fn next(&mut self) -> Option<Self::Item> {
        if self.count < self.max {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

// Использование
let sum: u32 = Counter::new(5).sum();  // 1 + 2 + 3 + 4 + 5 = 15

		

Здесь мы создаем собственный итератор Counter, реализуя стандартный трейт Iterator. Он используется для многих встроенных коллекций, таких как Vec или HashMap. Этот трейт особенно удобен из-за различных функциональных комбинаторов из стандартной библиотеки:

			let v = vec![1, 2, 3, 4, 5];

// zip объединяет два итератора
let pairs: Vec<_> = v.iter().zip(v.iter().skip(1)).collect();
// [(1, 2), (2, 3), (3, 4), (4, 5)]

// chain соединяет итераторы последовательно
let combined: Vec<_> = v.iter().chain(v.iter()).collect();
// [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

// cycle создает бесконечный итератор, повторяющий элементы
let first_ten: Vec<_> = v.iter().cycle().take(10).collect();
// [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

		

Трейт Iterator предоставляет множество методов адаптеров, таких как enumerate, filter или map, которые возвращают новый итератор. Методы адаптеров ленивые, они не запускают итерацию. Также есть методы исполнители, которые итерируют пока не закончатся значения, например, collect, fold или count.

Большинство коллекций (и ссылки на них) реализуют трейт IntoIterator (способность кастоваться в Iterator). Также IntoIterator автоматически реализуется для любого Iterator (ничего не стоящий каст сам в себя). Цикл for в Rust работает только с объектами, реализующими IntoIterator.

Как обрабатывать ошибки в Rust

В Rust принято разделять ошибки на 2 вида: паники и результаты операций:

  • Паники используются для непредвиденных ситуаций и ошибок программиста, например, деление целочисленного типа на 0 или выход за границу массива. И хотя паники можно отловить, стандартное и рекомендуемое поведение при них — программа упадёт, будет напечатан стектрейс.
  • Результаты операций выражаются типом Result<T, E>, который является перечислением из двух вариантов — Ok(T) и Err(E). Такой подход гарантирует, что все ошибки строго типизированы, а без обработки ошибки невозможно извлечь результат операции.

Option<T> используется для представления значения, которое может отсутствовать:

			fn find_user(id: u32) -> Option<User> {
    if id == 0 {
        None
    } else {
        Some(User { id, name: String::from("Alice") })
    }
}
// Обработка Option
match find_user(1) {
    Some(user) => println!("Найден пользователь: {}", user.name),
    None => println!("Пользователь не найден"),
}

		

Result<T, E> используется для операций, которые могут завершиться ошибкой:

			use std::fs::File;
use std::io::Error;
fn open_file(path: &str) -> Result<File, Error> {
    File::open(path)
}
// Обработка Result
match open_file("config.txt") {
    Ok(file) => println!("Файл успешно открыт"),
    Err(error) => println!("Ошибка открытия файла: {}", error),
}

		

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

			use std::fs::File;
use std::io::{self, Read};

fn read_file_verbose(path: &str) -> Result<String, io::Error> {
    let file_result = File::open(path);
    let mut file = match file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut content = String::new();
    match file.read_to_string(&mut content) {
        Ok(_) => Ok(content),
        Err(e) => Err(e),
    }
}

fn read_file_shortest(path: &str) -> Result<String, io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

		

Обычно все ошибки описываются в перечислениях, а для уменьшения шаблонного кода используются крейты вроде thiserror.

Про асинхронное программирование

Асинхронное программирование в Rust строится вокруг трейта Future— его реализуют для типов, представляющих значение, которое будет доступно в будущем.

Также в Rust есть синтаксис async/await. Ключевым словом async могут быть отмечены функции и блоки кода — они будут возвращать анонимный тип, реализующий Future. Async-блоки также могут захватывать окружение подобно замыканиям. Внутри async-блоков и функций возможно использовать ключевое слово await на любом выражении, возвращающем Future или IntoFuture (способность кастоваться к Future). В отличие от других языков с подобным синтаксисом, await записывается через точку после выражения, что очень удобно для построения цепочек вычислений.

Для исполнения асинхронного кода необходим рантайм, но стандартная библиотека такого рантайма не предоставляет, поэтому приходится использовать сторонние библиотеки. Самым популярным рантаймом является библиотека tokio.

В асинхронное программирование на Rust я рекомендую приходить уже после углубленного изучения языка, первых пет-проектов и небольшой работы с многопоточным кодом. Хотя синтаксис и общие правила работы с асинхронным кодом покажутся знакомыми тем, кто знает JS или C#, из-за более низкоуровневой природы языка работать с ним немного сложнее.
Василий Кузенковfull-stack разработчик в Web3 стартапе, ментор Solvery&nbsp;

Что еще должен знать новичок в Rust

Вот примерный список:

  • Очень желательно погрузиться в устройство памяти процесса, узнать, что помимо стека и кучи существуют и другие области (например, исполняемый машинный код так же отражён на память, а static-переменные хранятся не в стеке и не в куче, а в своей собственной области). Неплохо было бы и разобраться с тем, что у типов помимо размера есть выравнивание. Что в Rust бывают ZST (zero size type) — типы, размер которых честный 0, и DST (dynamic size type) — типы, размер которых неизвестен во время компиляции.
  • Обязательно разобраться, как Rust освобождает память, не используя сборщик мусора. Почитать, что такое RAII. Понять, как работает трейт Drop. 
  • Избавится от стереотипов о Rust. Rust — не самый сложный язык, как только вы поймёте, как он работает. Плюс платят за Rust, как правило, больше, чем на аналогичных позициях на других языках.
  • Оставить свои привычки из других языков (за исключением разве что Haskell/OCaml). Здесь не получится писать, как на C++/Java/Go и т.д. Привыкайте к хорошему и станете лучше, чем были до освоения Rust.

Что изучать, если есть вся база: чек-лист

  • Макросы и метапрограммирование
  • Unsafe Rust для низкоуровневого контроля
  • Интеграция с C/C++ через FFI
  • Разработка встраиваемых систем
  • WebAssembly 
  • rustnomicon
  • Undefined Behavior
  • unsafe-код

Тренды на 2025 год

На Rust’е пишут все. Более полный список можно посмотреть тут: https://github.com/rust-unofficial/awesome-rust

Вот несколько топовых фреймворков и библиотек:

  • serde — фреймворк для сериализации/десериализации
  • tokio, futures — для асинхронного программирования
  • clap — парсер аргументов командной строки
  • anyhow, thiserror — удобная работа с ошибками
  • chrono — работа с датой и временем
  • dashmap — многопоточная hashmap
  • bytes — эффективная работа с сырыми байтами
  • log, tracing — для логирования
  • reqwest — для http запросов
  • axum — для http сервера и REST api
  • mockall, test-case — упростит написание тестов
  • bevy — игры
  • clippy — линтер
А вы пишете на Rust?
Да, уже хорошо знаю базу
Разрабатываю на Rust уже больше года
Хочу начать!
Следите за новыми постами
Следите за новыми постами по любимым темам
915 открытий5К показов