Skip to content

Instantly share code, notes, and snippets.

@dmitry-osin
Created May 3, 2025 02:34
Show Gist options
  • Save dmitry-osin/d7711a982272b4eb281bf2444c7217f2 to your computer and use it in GitHub Desktop.
Save dmitry-osin/d7711a982272b4eb281bf2444c7217f2 to your computer and use it in GitHub Desktop.
Rust для Java разработчиков

Шпаргалка по Rust для Java-разработчика

Содержание

  1. Основы языка
  2. Переменные и типы данных
  3. Управление потоком выполнения
  4. Функции и замыкания
  5. Владение памятью: move, borrow, lifetime
  6. Структуры данных
  7. Перечисления и паттерн-матчинг
  8. Обработка ошибок: Option, Result, panic
  9. Трейты
  10. Generics
  11. Коллекции и итераторы
  12. Умные указатели: Box, Rc, RefCell
  13. Многопоточность
  14. Асинхронное программирование
  15. Модули и организация кода
  16. Макросы
  17. Тестирование

Основы языка

Установка и инструменты

Rust использует менеджер пакетов и инструментов cargo, аналогичный Maven или Gradle в Java.

# Установка Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Создание нового проекта
cargo new my_project

# Сборка проекта
cargo build

# Запуск проекта
cargo run

# Тестирование
cargo test

Структура проекта

my_project/
├── Cargo.toml         # Аналог pom.xml или build.gradle
├── Cargo.lock         # Фиксирует версии зависимостей
└── src/
    └── main.rs        # Точка входа в программу

Комментарии в Rust

// Однострочный комментарий

/*
   Многострочный комментарий
*/

/// Документационный комментарий для функций, структур и т.д.
/// Поддерживает Markdown
/// 
/// # Примеры
/// ```
/// let x = add(1, 2);
/// assert_eq!(x, 3);
/// ```

//! Документационный комментарий для модуля

Точка входа программы

fn main() {
    println!("Hello, world!");
}

Переменные и типы данных

Объявление переменных

// Неизменяемая переменная (по умолчанию)
let x = 5;

// Изменяемая переменная
let mut y = 5;
y = 6; // OK

// Константа (должна иметь явный тип и значение, известное на этапе компиляции)
const MAX_POINTS: u32 = 100_000;

// Затенение переменных (shadowing) - создание новой переменной с тем же именем
let z = 5;
let z = z + 1; // Создаёт новую переменную z
let z = z * 2; // Создаёт новую переменную z

В отличие от Java, где переменные по умолчанию изменяемые, в Rust переменные по умолчанию неизменяемые.

Основные типы данных

// Целые числа
let a: i8 = -128;            // 8-битное со знаком
let b: u8 = 255;             // 8-битное без знака
let c: i16 = -32768;         // 16-битное со знаком
let d: u16 = 65535;          // 16-битное без знака
let e: i32 = -2147483648;    // 32-битное со знаком (по умолчанию)
let f: u32 = 4294967295;     // 32-битное без знака
let g: i64 = -9223372036854775808; // 64-битное со знаком
let h: u64 = 18446744073709551615; // 64-битное без знака
let i: i128 = -170141183460469231731687303715884105728; // 128-битное со знаком
let j: u128 = 340282366920938463463374607431768211455;  // 128-битное без знака
let k: isize = -1;           // Зависит от архитектуры (32 или 64 бита)
let l: usize = 1;            // Зависит от архитектуры (32 или 64 бита)

// Числа с плавающей точкой
let m: f32 = 3.14;           // 32-битное с плавающей точкой
let n: f64 = 3.14159265359;  // 64-битное с плавающей точкой (по умолчанию)

// Логический тип
let o: bool = true;

// Символы (Unicode)
let p: char = 'a';           // 4 байта (не как в Java, где char - 2 байта)

// Кортежи (Tuples)
let q: (i32, f64, char) = (42, 3.14, 'a');
let (r, s, t) = q;           // Деструктуризация
let u = q.0;                 // Доступ по индексу

// Массивы (фиксированной длины)
let v: [i32; 5] = [1, 2, 3, 4, 5];
let w = v[0];                // Доступ по индексу

// Строки
let x: &str = "Hello";       // Строковый срез (string slice)
let y: String = String::from("Hello"); // Владеющая строка (owned string)
let z = x.to_string();       // Конвертация &str в String

Преобразование типов

// Явное преобразование с использованием as
let a = 5;
let b = a as f64;

// С использованием методов
let c = "42".parse::<i32>().unwrap(); // Из строки в число
let d = 42.to_string();              // Из числа в строку

Управление потоком выполнения

Условные выражения

// if-else (как выражение)
let number = 6;
if number % 4 == 0 {
    println!("Число делится на 4");
} else if number % 3 == 0 {
    println!("Число делится на 3");
} else {
    println!("Число не делится ни на 4, ни на 3");
}

// if как выражение (аналог тернарного оператора в Java)
let condition = true;
let number = if condition { 5 } else { 6 };

Циклы

// loop - бесконечный цикл с возможностью возврата значения
let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // Возвращает значение
    }
};

// while
let mut number = 3;
while number != 0 {
    println!("{}!", number);
    number -= 1;
}

// for с диапазоном
for i in 1..=5 {  // Включая 5
    println!("{}!", i);
}

// for с итератором
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
    println!("значение: {}", element);
}

Паттерн-матчинг (match)

let number = 13;
match number {
    // Сопоставление с одним значением
    1 => println!("Один!"),
    
    // Сопоставление с несколькими значениями
    2 | 3 | 5 | 7 | 11 | 13 => println!("Простое число!"),
    
    // Сопоставление с диапазоном
    15..=19 => println!("От 15 до 19"),
    
    // Захват значения
    n @ 20..=30 => println!("От 20 до 30: {}", n),
    
    // Значение по умолчанию
    _ => println!("Другое число"),
}

if let и while let

// if let - упрощённый match для одного шаблона
let some_value = Some(3);
if let Some(3) = some_value {
    println!("Три!");
}

// while let - цикл, который продолжается, пока шаблон соответствует
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("Верхний элемент: {}", top);
}

Функции и замыкания

Определение функций

// Простая функция
fn say_hello() {
    println!("Привет!");
}

// Функция с параметрами
fn add(a: i32, b: i32) -> i32 {
    a + b  // Отсутствие точки с запятой означает возврат значения
}

// Функция с несколькими возвращаемыми значениями через кортеж
fn calculate_stats(values: &[i32]) -> (i32, i32, i32) {
    let sum = values.iter().sum();
    let min = *values.iter().min().unwrap_or(&0);
    let max = *values.iter().max().unwrap_or(&0);
    (sum, min, max)
}

Замыкания (closures)

// Простое замыкание
let add_one = |x| x + 1;
let five = add_one(4);

// Замыкание с явными типами
let add_typed = |x: i32, y: i32| -> i32 { x + y };

// Замыкание, захватывающее переменные из окружения
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y));

// Замыкание как параметр функции
fn apply<F>(f: F, x: i32) -> i32 
where F: Fn(i32) -> i32 
{
    f(x)
}

let double = |x| x * 2;
let result = apply(double, 5); // 10

Типы замыканий

В Rust есть три трейта для замыканий:

  1. Fn - замыкание захватывает по ссылке (&T)
  2. FnMut - замыкание захватывает по изменяемой ссылке (&mut T)
  3. FnOnce - замыкание захватывает по значению (T)
// FnOnce - выполняется только один раз, потому что потребляет захваченное значение
fn consume<F>(func: F)
where F: FnOnce() -> String
{
    println!("{}", func());
}

let s = String::from("hello");
let consume_and_return = move || s;
consume(consume_and_return);
// consume(consume_and_return); // Ошибка! s уже перемещена

// FnMut - может изменять захваченные значения
fn do_twice<F>(mut func: F)
where F: FnMut()
{
    func();
    func();
}

let mut counter = 0;
let mut increment = || {
    counter += 1;
    println!("Counter: {}", counter);
};
do_twice(increment);

// Fn - только читает захваченные значения
fn apply_to_3<F>(f: F) -> i32
where F: Fn(i32) -> i32
{
    f(3)
}

let double = |x| x * 2;
println!("3 * 2 = {}", apply_to_3(double));

Владение памятью: move, borrow, lifetime

Владение (Ownership)

Основные правила владения:

  1. Каждое значение в Rust имеет переменную, которая называется его владельцем.
  2. В каждый момент времени может быть только один владелец.
  3. Когда владелец выходит из области видимости, значение удаляется.
// Пример перемещения владения (move)
let s1 = String::from("hello");
let s2 = s1; // s1 перемещается в s2
// println!("{}", s1); // Ошибка! s1 больше не действительна

// Клонирование (создание глубокой копии)
let s1 = String::from("hello");
let s2 = s1.clone(); // Создаётся новая копия данных
println!("s1 = {}, s2 = {}", s1, s2); // OK

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

Заимствование (Borrowing)

// Заимствование по ссылке (неизменяемое)
fn calculate_length(s: &String) -> usize {
    s.len()
}

let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("Длина '{}' равна {}.", s1, len); // s1 всё ещё действительна

// Заимствование по изменяемой ссылке
fn change(s: &mut String) {
    s.push_str(", world");
}

let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // "hello, world"

// Правила заимствования:
// 1. В любой момент времени может быть ЛИБО одна изменяемая ссылка,
//    ЛИБО любое количество неизменяемых ссылок.
// 2. Ссылки всегда должны быть действительными.

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

let r1 = &s; // OK
let r2 = &s; // OK
// let r3 = &mut s; // Ошибка! Нельзя иметь изменяемую ссылку при наличии неизменяемых

println!("{} and {}", r1, r2);
// r1 и r2 больше не используются после этой точки

let r3 = &mut s; // OK, т.к. r1 и r2 больше не используются
println!("{}", r3);

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

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

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

let string1 = String::from("долгая строка");
let result;
{
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
    println!("Наибольшая строка: {}", result); // OK
}
// println!("Наибольшая строка: {}", result); // Ошибка! string2 уже не существует

// Времена жизни в структурах
struct ImportantExcerpt<'a> {
    part: &'a str,
}

let novel = String::from("Жили-были. Они жили счастливо.");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
    part: first_sentence,
};

Правила вывода времён жизни:

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

Структуры данных

Определение структур

// Обычная структура
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

// Создание экземпляра
let user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

// Изменение полей (структура должна быть изменяемой)
let mut user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    active: true,
    sign_in_count: 1,
};
user2.email = String::from("[email protected]");

// Краткий синтаксис создания
fn build_user(email: String, username: String) -> User {
    User {
        email,      // Вместо email: email
        username,   // Вместо username: username
        active: true,
        sign_in_count: 1,
    }
}

// Создание из другого экземпляра с обновлением
let user3 = User {
    email: String::from("[email protected]"),
    ..user1  // Остальные поля берутся из user1
};

// Кортежные структуры (без именованных полей)
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

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

// Unit-подобные структуры (без полей)
struct AlwaysEqual;
let subject = AlwaysEqual;

Методы структур

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Метод (принимает self)
    fn area(&self) -> u32 {
        self.width * self.height
    }
    
    // Метод с изменяемым self
    fn resize(&mut self, width: u32, height: u32) {
        self.width = width;
        self.height = height;
    }
    
    // Ассоциированная функция (не принимает self)
    // Аналог статического метода в Java
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

let mut rect = Rectangle {
    width: 30,
    height: 50,
};

println!("Площадь: {}", rect.area());
rect.resize(60, 70);
let square = Rectangle::square(20);

Перечисления и паттерн-матчинг

Определение перечислений

// Простое перечисление
enum Direction {
    North,
    South,
    East,
    West,
}

// Перечисление с данными
enum Message {
    Quit,                       // Без данных
    Move { x: i32, y: i32 },    // Именованные поля как в структуре
    Write(String),              // Строка
    ChangeColor(i32, i32, i32), // Кортеж из трёх i32
}

// Использование
let msg = Message::Write(String::from("hello"));

// Перечисление с методами
impl Message {
    fn call(&self) {
        // Метод для обработки сообщения
        match self {
            Message::Quit => println!("Выход"),
            Message::Move { x, y } => println!("Перемещение на x: {}, y: {}", x, y),
            Message::Write(text) => println!("Текстовое сообщение: {}", text),
            Message::ChangeColor(r, g, b) => println!("Изменение цвета на {}, {}, {}", r, g, b),
        }
    }
}

let m = Message::Write(String::from("hello"));
m.call();

Паттерн-матчинг с перечислениями

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // ...и т.д.
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

Обработка ошибок: Option, Result, panic

Option

Option<T> - перечисление для представления возможного отсутствия значения (аналог Optional<T> в Java).

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

// Использование Option
let some_number = Some(5);
let some_string = Some("строка");
let absent_number: Option<i32> = None;

// Извлечение значения с unwrap (вызывает панику при None)
let x = some_number.unwrap();

// Безопасное извлечение с проверкой
match some_number {
    Some(i) => println!("Значение: {}", i),
    None => println!("Значение отсутствует"),
}

// Использование if let
if let Some(value) = some_string {
    println!("Строка: {}", value);
}

// Методы Option
let x = Some(5);
let y = x.map(|n| n * 2);        // Some(10)
let z = None.map(|n: i32| n * 2); // None

let a = Some(5).filter(|n| n > &3);  // Some(5)
let b = Some(2).filter(|n| n > &3);  // None

let c = Some("hello").unwrap_or("world"); // "hello"
let d = None.unwrap_or("world");         // "world"

let e = Some(5).and_then(|n| Some(n * 2)); // Some(10)
let f = None.and_then(|n: i32| Some(n * 2)); // None

Result

Result<T, E> - перечисление для представления результата, который может быть ошибкой.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

// Использование Result
use std::fs::File;
use std::io::{self, Read};

fn read_file() -> Result<String, io::Error> {
    let mut file = match File::open("hello.txt") {
        Ok(file) => file,
        Err(error) => return Err(error),
    };

    let mut contents = String::new();
    match file.read_to_string(&mut contents) {
        Ok(_) => Ok(contents),
        Err(error) => Err(error),
    }
}

// Сокращённая запись с оператором ?
fn read_file_short() -> Result<String, io::Error> {
    let mut file = File::open("hello.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// Ещё короче
fn read_file_shortest() -> Result<String, io::Error> {
    let mut contents = String::new();
    File::open("hello.txt")?.read_to_string(&mut contents)?;
    Ok(contents)
}

// Методы Result
let x: Result<i32, &str> = Ok(5);
let y: Result<i32, &str> = Err("Ошибка");

println!("{}", x.unwrap());        // 5
// println!("{}", y.unwrap());     // Паника с сообщением "Ошибка"

println!("{}", x.unwrap_or(0));    // 5
println!("{}", y.unwrap_or(0));    // 0

let z = x.map(|n| n * 2);          // Ok(10)
let w = y.map(|n| n * 2);          // Err("Ошибка")

let a = x.map_err(|e| format!("Произошла ошибка: {}", e)); // Ok(5)
let b = y.map_err(|e| format!("Произошла ошибка: {}", e)); // Err("Произошла ошибка: Ошибка")

let c = x.and_then(|n| Ok(n * 2)); // Ok(10)
let d = y.and_then(|n| Ok(n * 2)); // Err("Ошибка")

Panic

panic! - макрос для аварийного завершения программы при неисправимых ошибках.

// Вызов паники
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Деление на ноль!");
    }
    a / b
}

// Настройка поведения паники в Cargo.toml
// [profile.release]
// panic = 'abort'  // Немедленное завершение без раскрутки стека

Трейты

Трейты в Rust похожи на интерфейсы в Java, но с некоторыми дополнительными возможностями.

Определение и реализация трейтов

// Определение трейта
trait Summary {
    // Метод без реализации
    fn summarize(&self) -> String;
    
    // Метод с реализацией по умолчанию
    fn default_summary(&self) -> String {
        String::from("(Читать дальше...)")
    }
}

// Структуры
struct NewsArticle {
    headline: String,
    location: String,
    author: String,
    content: String,
}

struct Tweet {
    username: String,
    content: String,
    reply: bool,
    retweet: bool,
}

// Реализация трейта для структуры
impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, автор {} ({})", self.headline, self.author, self.location)
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
    
    // Переопределение метода с реализацией по умолчанию
    fn default_summary(&self) -> String {
        format!("Твит от {}", self.username)
    }
}

// Использование
let article = NewsArticle {
    headline: String::from("Заголовок!"),
    location: String::from("Москва"),
    author: String::from("Иван Иванов"),
    content: String::from("Содержание статьи..."),
};

let tweet = Tweet {
    username: String::from("user123"),
    content: String::from("Привет, мир!"),
    reply: false,
    retweet: false,
};

println!("Статья: {}", article.summarize());
println!("Статья по умолчанию: {}", article.default_summary());
println!("Твит: {}", tweet.summarize());
println!("Твит по умолчанию: {}", tweet.default_summary());

Трейты как параметры

// Функция, принимающая любой тип, реализующий трейт Summary
fn notify(item: &impl Summary) {
    println!("Срочные новости! {}", item.summarize());
}

// Эквивалентная запись с использованием ограничений типов
fn notify_generic<T: Summary>(item: &T) {
    println!("Срочные новости! {}", item.summarize());
}

// Несколько ограничений типов
fn notify_multiple<T: Summary + Display>(item: &T) {
    println!("Срочные новости! {}", item.summarize());
    println!("Отображение: {}", item);
}

// Синтаксис where для сложных ограничений
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    // реализация
}

Возврат типов, реализующих трейт

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("конечно, как вы, наверное, знаете, люди"),
        reply: false,
        retweet: false,
    }
}

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

// Ошибка! Возвращаемый тип должен быть одним конкретным типом
fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from("Заголовок"),
            location: String::from("Москва"),
            author: String::from("Автор"),
            content: String::from("Содержание"),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("контент"),
            reply: false,
            retweet: false,
        }
    }
}

Для таких случаев можно использовать паттерн "динамическая диспетчеризация" с трейт-объектами:

fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
    if switch {
        Box::new(NewsArticle {
            headline: String::from("Заголовок"),
            location: String::from("Москва"),
            author: String::from("Автор"),
            content: String::from("Содержание"),
        })
    } else {
        Box::new(Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("контент"),
            reply: false,
            retweet: false,
        })
    }
}

Трейт-объекты и динамическая диспетчеризация

// Вектор трейт-объектов (аналог полиморфизма в Java)
let articles: Vec<Box<dyn Summary>> = vec![
    Box::new(NewsArticle {
        headline: String::from("Заголовок"),
        location: String::from("Москва"),
        author: String::from("Автор"),
        content: String::from("Содержание"),
    }),
    Box::new(Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("контент"),
        reply: false,
        retweet: false,
    }),
];

// Вызов методов для каждого элемента
for article in &articles {
    println!("{}", article.summarize());
}

Маркерные трейты

Маркерные трейты не имеют методов и используются для добавления свойств типам:

// Встроенные маркерные трейты
// Send - тип может безопасно передаваться между потоками
// Sync - тип может безопасно использоваться из нескольких потоков
// Copy - тип может быть скопирован с помощью memcpy
// Sized - тип имеет известный размер во время компиляции

// Пользовательский маркерный трейт
trait Printable {}

impl Printable for i32 {}
impl Printable for String {}

fn print_item<T: Printable + std::fmt::Debug>(item: T) {
    println!("{:?}", item);
}

Generics

Дженерики в Rust похожи на дженерики в Java, но с более мощными ограничениями типов.

Дженерик-функции

// Функция с одним дженерик-параметром
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];
    
    for item in list {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

let numbers = vec![34, 50, 25, 100, 65];
let result = largest(&numbers);
println!("Наибольшее число: {}", result);

let chars = vec!['y', 'm', 'a', 'q'];
let result = largest(&chars);
println!("Наибольший символ: {}", result);

Дженерик-структуры

// Структура с одним дженерик-параметром
struct Point<T> {
    x: T,
    y: T,
}

// Структура с разными дженерик-параметрами
struct MixedPoint<T, U> {
    x: T,
    y: U,
}

// Использование
let integer_point = Point { x: 5, y: 10 };
let float_point = Point { x: 1.0, y: 4.0 };
let mixed_point = MixedPoint { x: 5, y: 4.0 };

Методы с дженериками

// Методы для дженерик-структуры
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// Методы только для конкретного типа
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

// Методы с дополнительными дженерик-параметрами
impl<T, U> MixedPoint<T, U> {
    fn mixup<V, W>(self, other: MixedPoint<V, W>) -> MixedPoint<T, W> {
        MixedPoint {
            x: self.x,
            y: other.y,
        }
    }
}

Ограничения типов для дженериков

// Несколько ограничений
fn print_and_compare<T: std::fmt::Display + std::cmp::PartialOrd>(a: T, b: T) {
    println!("a = {}, b = {}", a, b);
    if a > b {
        println!("a больше b");
    } else {
        println!("a не больше b");
    }
}

// Синтаксис where для сложных ограничений
fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: std::fmt::Display + Clone,
    U: Clone + std::fmt::Debug,
{
    println!("t = {}", t);
    println!("u = {:?}", u);
    42
}

Монотипизация

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

Коллекции и итераторы

Основные коллекции

Vec - динамический массив

// Создание пустого вектора
let mut v: Vec<i32> = Vec::new();

// Создание с начальными значениями
let v = vec![1, 2, 3, 4, 5];

// Добавление элементов
v.push(6);
v.push(7);

// Чтение элементов
let third: &i32 = &v[2]; // Может вызвать панику при выходе за границы
let third: Option<&i32> = v.get(2); // Безопасный доступ

// Перебор элементов
for i in &v {
    println!("{}", i);
}

// Перебор с изменением
for i in &mut v {
    *i += 50;
}

// Использование enum для хранения разных типов
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("синий")),
    SpreadsheetCell::Float(10.12),
];

String - строка

// Создание новой строки
let mut s = String::new();

// Создание из строкового литерала
let s = String::from("начальное содержимое");
let s = "начальное содержимое".to_string();

// Добавление к строке
let mut s = String::from("foo");
s.push_str("bar"); // foobar
s.push('!'); // foobar!

// Конкатенация
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1 перемещена и больше не действительна

// Форматирование
let s = format!("{}-{}-{}", "tic", "tac", "toe"); // tic-tac-toe

// Индексирование (не работает напрямую из-за UTF-8)
// let c = s[0]; // Ошибка!

// Срезы строк
let hello = "Здравствуйте";
let s = &hello[0..4]; // Первые 4 байта (не символа!)

// Итерация по символам
for c in "Зд".chars() {
    println!("{}", c);
}

// Итерация по байтам
for b in "Зд".bytes() {
    println!("{}", b);
}

HashMap<K, V> - хеш-таблица

use std::collections::HashMap;

// Создание пустой хеш-таблицы
let mut scores = HashMap::new();

// Вставка значений
scores.insert(String::from("Синяя"), 10);
scores.insert(String::from("Жёлтая"), 50);

// Создание из векторов
let teams = vec![String::from("Синяя"), String::from("Жёлтая")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.into_iter().zip(initial_scores.into_iter()).collect();

// Доступ к значениям
let team_name = String::from("Синяя");
let score = scores.get(&team_name); // Option<&i32>

// Перебор пар ключ-значение
for (key, value) in &scores {
    println!("{}: {}", key, value);
}

// Обновление значения
scores.insert(String::from("Синяя"), 25); // Перезаписывает существующее значение

// Вставка только если ключ отсутствует
scores.entry(String::from("Жёлтая")).or_insert(50);
scores.entry(String::from("Зелёная")).or_insert(50);

// Обновление на основе старого значения
let text = "hello world wonderful world";
let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

Итераторы

// Создание итератора
let v = vec![1, 2, 3];
let mut iter = v.iter(); // Итератор по ссылкам &T

// Ручная итерация
assert_eq!(iter.next(), Some(&1));
assert_eq!(iter.next(), Some(&2));
assert_eq!(iter.next(), Some(&3));
assert_eq!(iter.next(), None);

// Итераторы по значению
let v = vec![1, 2, 3];
let iter = v.into_iter(); // Потребляет коллекцию, возвращает T
let iter = v.iter_mut(); // Итератор по изменяемым ссылкам &mut T

// Методы-потребители (consuming adaptors)
let v = vec![1, 2, 3];
let total: i32 = v.iter().sum(); // 6

// Методы-итераторы (iterator adaptors)
let v = vec![1, 2, 3];
let v2: Vec<_> = v.iter().map(|x| x + 1).collect(); // [2, 3, 4]

// Фильтрация
let v = vec![1, 2, 3, 4, 5];
let even: Vec<_> = v.iter().filter(|&&x| x % 2 == 0).collect(); // [2, 4]

// Цепочка методов
let sum: i32 = v.iter()
    .filter(|&&x| x % 2 == 0)
    .map(|&x| x * x)
    .sum(); // 2*2 + 4*4 = 20

Умные указатели: Box, Rc, RefCell

Box

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

// Создание Box
let b = Box::new(5);
println!("b = {}", b);

// Рекурсивные типы данных с Box
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));

Rc

Rc<T> (Reference Counting) - умный указатель для случаев, когда значение может иметь несколько владельцев.

use std::rc::Rc;

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

use List::{Cons, Nil};

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("Счётчик ссылок после создания a = {}", Rc::strong_count(&a)); // 1

let b = Cons(3, Rc::clone(&a));
println!("Счётчик ссылок после создания b = {}", Rc::strong_count(&a)); // 2

{
    let c = Cons(4, Rc::clone(&a));
    println!("Счётчик ссылок после создания c = {}", Rc::strong_count(&a)); // 3
}

println!("Счётчик ссылок после выхода c из области видимости = {}", Rc::strong_count(&a)); // 2

RefCell

RefCell<T> - умный указатель для внутренней изменяемости, проверяющий правила заимствования во время выполнения, а не компиляции.

use std::cell::RefCell;

// Структура с внутренней изменяемостью
struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}

impl MockMessenger {
    fn new() -> MockMessenger {
        MockMessenger {
            sent_messages: RefCell::new(vec![]),
        }
    }

    fn send_message(&self, message: &str) {
        // Хотя self неизменяемый, мы можем изменить содержимое sent_messages
        self.sent_messages.borrow_mut().push(String::from(message));
    }
}

let mock = MockMessenger::new();
mock.send_message("hello");
mock.send_message("world");

assert_eq!(mock.sent_messages.borrow().len(), 2);

Комбинирование умных указателей

use std::rc::Rc;
use std::cell::RefCell;

// Структура с множественным владением и внутренней изменяемостью
#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use List::{Cons, Nil};

let value = Rc::new(RefCell::new(5));

let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

// Изменение значения через любую ссылку
*value.borrow_mut() += 10;

println!("a после = {:?}", a); // Значение в a теперь 15
println!("b после = {:?}", b); // b также видит изменение
println!("c после = {:?}", c); // c также видит изменение

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

Потоки (Threads)

use std::thread;
use std::time::Duration;

// Создание потока
let handle = thread::spawn(|| {
    for i in 1..10 {
        println!("привет {} из порожденного потока!", i);
        thread::sleep(Duration::from_millis(1));
    }
});

// Код в основном потоке
for i in 1..5 {
    println!("привет {} из основного потока!", i);
    thread::sleep(Duration::from_millis(1));
}

// Ожидание завершения порожденного потока
handle.join().unwrap();

// Захват переменных из окружения с помощью move
let v = vec![1, 2, 3];

let handle = thread::spawn(move || {
    println!("Вот вектор: {:?}", v);
});

handle.join().unwrap();

Передача сообщений между потоками

use std::thread;
use std::sync::mpsc;

// Создание канала (multiple producer, single consumer)
let (tx, rx) = mpsc::channel();

// Создание потока-отправителя
thread::spawn(move || {
    let val = String::from("привет");
    tx.send(val).unwrap();
    // val теперь недоступна, т.к. перемещена
});

// Получение значения в основном потоке
let received = rx.recv().unwrap();
println!("Получено: {}", received);

// Отправка нескольких значений
let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    let vals = vec![
        String::from("привет"),
        String::from("из"),
        String::from("потока"),
    ];

    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

// Получение значений как итератора
for received in rx {
    println!("Получено: {}", received);
}

// Несколько отправителей
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();

thread::spawn(move || {
    tx.send(String::from("привет от tx")).unwrap();
});

thread::spawn(move || {
    tx1.send(String::from("привет от tx1")).unwrap();
});

for received in rx {
    println!("Получено: {}", received);
}

Разделяемое состояние

use std::sync::{Mutex, Arc};
use std::thread;

// Мьютекс для безопасного доступа к данным
let m = Mutex::new(5);

{
    let mut num = m.lock().unwrap();
    *num = 6;
}

println!("m = {:?}", m);

// Мьютекс с несколькими потоками
let counter = Arc::new(Mutex::new(0)); // Arc - атомарный Rc для потоков
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    });
    handles.push(handle);
}

for handle in handles {
    handle.join().unwrap();
}

println!("Результат: {}", *counter.lock().unwrap());

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

Futures и async/await

use futures::executor::block_on;

// Асинхронная функция
async fn hello_world() {
    println!("hello, world!");
}

// Выполнение асинхронной функции
fn main() {
    let future = hello_world(); // Не выполняется сразу
    block_on(future); // Ожидает завершения future
}

// Асинхронные функции, возвращающие значения
async fn get_number() -> u32 {
    42
}

async fn print_number() {
    let number = get_number().await;
    println!("Число: {}", number);
}

// Параллельное выполнение futures
use futures::join;

async fn parallel_tasks() {
    let task1 = async {
        println!("Задача 1");
    };
    
    let task2 = async {
        println!("Задача 2");
    };
    
    join!(task1, task2);
}

Tokio - популярная библиотека для асинхронного программирования

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // Запуск задачи в фоновом режиме
    let handle = tokio::spawn(async {
        sleep(Duration::from_secs(1)).await;
        println!("Задача завершена");
        "результат"
    });
    
    // Ожидание результата
    let result = handle.await.unwrap();
    println!("Получен результат: {}", result);
    
    // Параллельные задачи
    let mut tasks = vec![];
    
    for i in 0..5 {
        let task = tokio::spawn(async move {
            sleep(Duration::from_millis(100 * i)).await;
            println!("Задача {} завершена", i);
            i
        });
        tasks.push(task);
    }
    
    // Ожидание всех задач
    for task in tasks {
        let result = task.await.unwrap();
        println!("Результат: {}", result);
    }
}

Модули и организация кода

Структура модулей

// 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/
├── main.rs       // точка входа бинарного проекта
├── lib.rs        // точка входа библиотеки
├── front_of_house.rs  // модуль front_of_house
└── front_of_house/
    ├── hosting.rs     // подмодуль hosting
    └── serving.rs     // подмодуль serving
// src/lib.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

// src/front_of_house.rs
pub mod hosting;
mod serving;

// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
fn seat_at_table() {}

Импорт и реэкспорт

// Импорт отдельных элементов
use std::collections::HashMap;
use std::io::{self, Write};

// Импорт всех публичных элементов
use std::collections::*;

// Переименование при импорте
use std::io::Result as IoResult;

// Реэкспорт (публичный импорт)
pub use crate::front_of_house::hosting;

Области видимости и приватность

mod back_of_house {
    // Публичная структура с некоторыми приватными полями
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }
    
    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("персики"),
            }
        }
    }
    
    // Публичное перечисление с публичными вариантами
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let mut meal = back_of_house::Breakfast::summer("Ржаной");
    meal.toast = String::from("Пшеничный");
    // meal.seasonal_fruit = String::from("черника"); // Ошибка! Приватное поле
    
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Макросы

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

Декларативные макросы (macro_rules!)

// Определение макроса
macro_rules! say_hello {
    // Шаблон без аргументов
    () => {
        println!("Привет!");
    };
    // Шаблон с одним аргументом
    ($name:expr) => {
        println!("Привет, {}!", $name);
    };
}

// Использование макроса
say_hello!(); // Привет!
say_hello!("Rust"); // Привет, Rust!

// Макрос с переменным числом аргументов
macro_rules! vec_of_strings {
    ($($x:expr),*) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x.to_string());
            )*
            temp_vec
        }
    };
}

let v = vec_of_strings![1, 2, 3]; // ["1", "2", "3"]

Процедурные макросы

Процедурные макросы действуют как функции, принимающие и возвращающие токены Rust. Есть три типа:

  1. Атрибутоподобные макросы
  2. Производные макросы
  3. Функциональные макросы
// Пример атрибутоподобного макроса
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
    // Реализация
}

// Использование
#[route(GET, "/")]
fn index() {
    // ...
}

// Пример производного макроса
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Реализация
}

// Использование
#[derive(HelloMacro)]
struct Pancakes;

// Пример функционального макроса
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    // Реализация
}

// Использование
let sql = sql!(SELECT * FROM posts WHERE id=1);

Тестирование

Модульные тесты

// В файле с кодом
pub fn add_two(a: i32) -> i32 {
    a + 2
}

// Модуль тестов
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(4, add_two(2));
    }
    
    #[test]
    fn it_fails() {
        assert_eq!(5, add_two(2));
    }
    
    // Тест с сообщением об ошибке
    #[test]
    fn it_works_with_message() {
        let result = add_two(2);
        assert_eq!(result, 4, "2 + 2 должно равняться 4, получено {}", result);
    }
    
    // Проверка на панику
    #[test]
    #[should_panic]
    fn it_panics() {
        panic!("Этот тест должен вызвать панику");
    }
    
    // Проверка на конкретное сообщение паники
    #[test]
    #[should_panic(expected = "Деление на ноль")]
    fn it_panics_with_message() {
        divide(10, 0);
    }
    
    // Тест с Result
    #[test]
    fn it_works_result() -> Result<(), String> {
        if add_two(2) == 4 {
            Ok(())
        } else {
            Err(String::from("2 + 2 не равно 4"))
        }
    }
    
    // Игнорирование теста
    #[test]
    #[ignore]
    fn expensive_test() {
        // Долгий тест, который мы хотим пропустить
    }
}

Интеграционные тесты

Структура каталогов для интеграционных тестов:

my_crate/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    ├── integration_test.rs
    ├── common/
    │   └── mod.rs
    └── api_tests/
        └── api.rs

Пример интеграционного теста:

// tests/integration_test.rs

// Импорт библиотеки
use my_crate;

// Вспомогательная функция
mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, my_crate::add_two(2));
}

Вспомогательный модуль для интеграционных тестов:

// tests/common/mod.rs

pub fn setup() {
    // Код настройки, общий для нескольких тестов
}

Запуск тестов

# Запуск всех тестов
cargo test

# Запуск конкретного теста
cargo test it_works

# Запуск тестов, содержащих определённую строку в имени
cargo test add

# Запуск игнорируемых тестов
cargo test -- --ignored

# Запуск всех тестов, включая игнорируемые
cargo test -- --include-ignored

# Вывод stdout во время тестов
cargo test -- --nocapture

# Запуск тестов в одном потоке
cargo test -- --test-threads=1

# Запуск только интеграционных тестов
cargo test --test integration_test

Документационные тесты

Rust позволяет писать тесты прямо в документации:

/// Adds two to the number given.
///
/// # Examples
///
/// ```
/// let result = my_crate::add_two(2);
/// assert_eq!(result, 4);
/// ```
pub fn add_two(a: i32) -> i32 {
    a + 2
}

Запуск документационных тестов:

cargo test --doc

Итоговое сравнение Rust и Java

Основные концепции

Концепция Java Rust
Управление памятью Сборщик мусора Система владения с временами жизни
Многопоточность Потоки, синхронизация Потоки, каналы, Mutex, Arc
Обработка ошибок Исключения Result и Option
Обобщённые типы Дженерики с стиранием типов Дженерики с монотипизацией
Наследование Классы и интерфейсы Трейты и композиция
Null-значения null Option
Изменяемость По умолчанию изменяемые По умолчанию неизменяемые
Проверка типов Во время компиляции Во время компиляции

Синтаксические соответствия

Java Rust
public class MyClass {} pub struct MyClass {}
interface MyInterface {} trait MyTrait {}
enum Color { RED, GREEN, BLUE } enum Color { Red, Green, Blue }
Optional<T> Option<T>
try/catch Result<T, E> с ? оператором
ArrayList<T> Vec<T>
HashMap<K, V> HashMap<K, V>
String String
String.format("%s %d", s, n) format!("{} {}", s, n)
public static void main(String[] args) fn main()
for (int i = 0; i < 10; i++) for i in 0..10
for (Item item : items) for item in &items
items.stream().map(...).filter(...) items.iter().map(...).filter(...)
new Thread(() -> { ... }).start() `thread::spawn(
synchronized (obj) { ... } let _guard = mutex.lock().unwrap(); ...

Преимущества Rust перед Java

  1. Безопасность памяти без сборщика мусора: Rust обеспечивает безопасность памяти без накладных расходов сборщика мусора.

  2. Отсутствие null-указателей: Использование Option<T> вместо null устраняет ошибки NullPointerException.

  3. Контроль над изменяемостью: Неизменяемость по умолчанию помогает избежать многих ошибок.

  4. Выразительная система типов: Перечисления с данными, сопоставление с образцом и времена жизни делают код более надёжным.

  5. Производительность: Rust обычно работает быстрее Java благодаря отсутствию виртуальной машины и сборщика мусора.

  6. Отсутствие исключений: Явная обработка ошибок с помощью Result<T, E> делает код более предсказуемым.

  7. Макросы: Мощная система макросов позволяет писать более выразительный код.

  8. Низкоуровневый контроль: Rust позволяет работать с памятью напрямую, когда это необходимо.

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

  1. Система владения и заимствования: Самая сложная концепция для новичков в Rust.

  2. Времена жизни: Требуют понимания и практики.

  3. Отсутствие наследования классов: Необходимо привыкнуть к композиции и трейтам.

  4. Явная обработка ошибок: Требует больше кода, но делает его надёжнее.

  5. Компилятор как строгий учитель: Rust имеет более строгие правила, которые могут казаться ограничивающими.

Заключение

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

Ключ к успешному освоению Rust - это понимание системы владения и заимствования, а также принятие идиоматических подходов языка вместо попыток писать код в стиле Java. С практикой и терпением вы сможете использовать все преимущества Rust, создавая быстрый, безопасный и надёжный код.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment