20 примеров кода на Rust: чистый и идиоматичный код от функций до traits и многопоточности

20 примеров кода на Rust с разбором: итераторы, Option и Result, enum, traits, generics, работа с файлами и многопоточность. Учимся писать чистый и идиоматичный Rust.

Rust ценят не только за производительность и безопасность памяти. Язык подталкивает разработчика к чистому коду: ошибки обрабатываются явно, зависимости видны, а состояние сложно изменить случайно. Лучше всего это понять на практике, поэтому ниже собраны примеры кода на Rust с коротким разбором каждого приёма.

Все примеры кода на Rust в подборке — практические, чистые и идиоматичные. Они подойдут для начинающих и тех, кто переходит с других языков: простые функции, итераторы, Option и Result, enum и pattern matching, HashMap, traits, generics, работа с файлами и многопоточность через каналы. Примеры используют только стандартную библиотеку, без сторонних крейтов. Подробнее каждая тема разобрана в официальной книге The Rust Programming Language.


1. Простая функция вместо лишней логики в main

fn greet(name: &str) -> String {
    format!("Привет, {name}!")
}

fn main() {
    println!("{}", greet("Rust"));
}

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

Вместо того чтобы складывать всю бизнес-логику в main, её лучше разделять на отдельные функции.


2. Сумма чисел через итератор

fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}

fn main() {
    let numbers = [10, 20, 30, 40];

    println!("{}", sum(&numbers));
}

Необязательно писать:

let mut result = 0;

for number in numbers {
    result += number;
}

Когда операция стандартная, выразительный метод sum() обычно читается лучше.


3. Фильтрация чётных чисел

fn even_numbers(numbers: &[i32]) -> Vec<i32> {
    numbers
        .iter()
        .copied()
        .filter(|number| number % 2 == 0)
        .collect()
}

fn main() {
    let numbers = [1, 2, 3, 4, 5, 6];

    println!("{:?}", even_numbers(&numbers));
}

Цепочка:

iter → filter → collect

явно показывает последовательность преобразований данных.


4. Option вместо магического значения

Плохой подход — возвращать -1, если значение не найдено.

Rust предлагает Option.

fn find_max(numbers: &[i32]) -> Option<i32> {
    numbers.iter().copied().max()
}

fn main() {
    let numbers = [10, 50, 20];

    match find_max(&numbers) {
        Some(value) => println!("Максимум: {value}"),
        None => println!("Список пуст"),
    }
}

Теперь тип функции сразу говорит:

результата может не быть.


5. Result для обработки ошибок

fn parse_age(value: &str) -> Result<u8, String> {
    let age = value
        .parse::<u8>()
        .map_err(|_| "Возраст должен быть числом".to_string())?;

    Ok(age)
}

fn main() {
    match parse_age("35") {
        Ok(age) => println!("Возраст: {age}"),
        Err(error) => println!("Ошибка: {error}"),
    }
}

Оператор ? позволяет передавать ошибку выше без вложенных match.


6. Проверка входных данных

fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
    if b == 0.0 {
        return Err("Деление на ноль");
    }

    Ok(a / b)
}

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("{result}"),
        Err(error) => println!("{error}"),
    }
}

У функции понятный контракт:

Result<f64, &'static str>

Она либо возвращает результат, либо объясняет причину ошибки.


7. Структура вместо набора несвязанных переменных

struct User {
    name: String,
    age: u8,
}

impl User {
    fn new(name: impl Into<String>, age: u8) -> Self {
        Self {
            name: name.into(),
            age,
        }
    }

    fn description(&self) -> String {
        format!("{} — {} лет", self.name, self.age)
    }
}

fn main() {
    let user = User::new("Alex", 30);

    println!("{}", user.description());
}

Связанные данные и поведение находятся в одном месте.


8. enum вместо строковых статусов

Не стоит хранить статус так:

let status = "active";

Лучше создать тип:

enum Status {
    Active,
    Blocked,
    Deleted,
}

fn status_message(status: Status) -> &'static str {
    match status {
        Status::Active => "Аккаунт активен",
        Status::Blocked => "Аккаунт заблокирован",
        Status::Deleted => "Аккаунт удалён",
    }
}

fn main() {
    println!("{}", status_message(Status::Active));
}

Теперь невозможно случайно передать:

"acitve"

вместо:

"active"

Компилятор контролирует допустимые состояния.


9. Pattern Matching без длинных if

fn describe_number(number: i32) -> &'static str {
    match number {
        0 => "ноль",
        1..=9 => "маленькое число",
        10..=99 => "двузначное число",
        _ => "большое число",
    }
}

fn main() {
    println!("{}", describe_number(42));
}

match — одна из самых сильных конструкций Rust.

Особенно хорошо она работает вместе с enum.


10. Подсчёт слов через HashMap

use std::collections::HashMap;

fn count_words(text: &str) -> HashMap<&str, usize> {
    let mut counts = HashMap::new();

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

    counts
}

fn main() {
    let text = "rust code rust clean code";

    println!("{:?}", count_words(text));
}

Здесь хорошо используется API:

entry()

вместо ручной проверки существования ключа.


11. Удаление повторяющихся значений

use std::collections::HashSet;

fn unique(numbers: &[i32]) -> Vec<i32> {
    let mut seen = HashSet::new();

    numbers
        .iter()
        .copied()
        .filter(|number| seen.insert(*number))
        .collect()
}

fn main() {
    let numbers = [1, 2, 2, 3, 3, 4];

    println!("{:?}", unique(&numbers));
}

Результат:

[1, 2, 3, 4]

Здесь HashSet напрямую выражает задачу — отслеживать уже встречавшиеся значения.


12. Сортировка структур

#[derive(Debug)]
struct Product {
    name: String,
    price: u32,
}

fn main() {
    let mut products = vec![
        Product {
            name: "Keyboard".into(),
            price: 100,
        },
        Product {
            name: "Mouse".into(),
            price: 50,
        },
        Product {
            name: "Monitor".into(),
            price: 300,
        },
    ];

    products.sort_by_key(|product| product.price);

    println!("{products:#?}");
}

sort_by_key() обычно понятнее ручного написания функции сравнения.


13. Поиск объекта через find

#[derive(Debug)]
struct User {
    id: u64,
    name: String,
}

fn find_user(users: &[User], id: u64) -> Option<&User> {
    users.iter().find(|user| user.id == id)
}

fn main() {
    let users = vec![
        User {
            id: 1,
            name: "Alice".into(),
        },
        User {
            id: 2,
            name: "Bob".into(),
        },
    ];

    if let Some(user) = find_user(&users, 2) {
        println!("{}", user.name);
    }
}

Комбинация Option + find() позволяет выразить операцию буквально одной строкой.


14. map вместо изменения данных вручную

fn double(numbers: &[i32]) -> Vec<i32> {
    numbers
        .iter()
        .map(|number| number * 2)
        .collect()
}

fn main() {
    println!("{:?}", double(&[1, 2, 3]));
}

Получаем:

[2, 4, 6]

Исходная коллекция при этом не изменяется.


15. filter_map для преобразования только корректных данных

fn parse_numbers(values: &[&str]) -> Vec<i32> {
    values
        .iter()
        .filter_map(|value| value.parse().ok())
        .collect()
}

fn main() {
    let values = ["10", "hello", "20", "rust", "30"];

    println!("{:?}", parse_numbers(&values));
}

Результат:

[10, 20, 30]

filter_map() одновременно фильтрует неподходящие элементы и преобразует подходящие.


16. Trait для общего поведения

trait Area {
    fn area(&self) -> f64;
}

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

struct Circle {
    radius: f64,
}

impl Area for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

impl Area for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius.powi(2)
    }
}

fn print_area(shape: &impl Area) {
    println!("Площадь: {:.2}", shape.area());
}

fn main() {
    let rectangle = Rectangle {
        width: 10.0,
        height: 5.0,
    };

    let circle = Circle {
        radius: 3.0,
    };

    print_area(&rectangle);
    print_area(&circle);
}

Traits позволяют программировать относительно поведения, а не конкретной реализации.


17. Generics без дублирования функций

fn first<T>(items: &[T]) -> Option<&T> {
    items.first()
}

fn main() {
    let numbers = [10, 20, 30];
    let words = ["Rust", "Go", "Python"];

    println!("{:?}", first(&numbers));
    println!("{:?}", first(&words));
}

Одна функция работает с любым типом.

Это проще, чем создавать:

first_number()
first_string()
first_user()

и десятки одинаковых реализаций.


18. Чтение файла с корректной передачей ошибок

use std::fs;
use std::io;

fn read_config(path: &str) -> Result<String, io::Error> {
    fs::read_to_string(path)
}

fn main() -> Result<(), io::Error> {
    let config = read_config("config.txt")?;

    println!("{config}");

    Ok(())
}

main тоже может возвращать Result.

Это позволяет избежать:

unwrap()

там, где ошибка действительно возможна.


19. Парсинг строки в собственный тип

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

use std::str::FromStr;

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

impl FromStr for Point {
    type Err = &'static str;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let (x, y) = value
            .split_once(',')
            .ok_or("Ожидался формат x,y")?;

        let x = x.trim().parse().map_err(|_| "Некорректный x")?;
        let y = y.trim().parse().map_err(|_| "Некорректный y")?;

        Ok(Self { x, y })
    }
}

fn main() -> Result<(), &'static str> {
    let point: Point = "10,20".parse()?;

    println!("{point:?}");

    Ok(())
}

После реализации FromStr работает стандартный:

"10,20".parse()

Это хороший пример использования возможностей стандартной библиотеки вместо собственного API.


20. Многопоточность через каналы

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

fn main() {
    let (sender, receiver) = mpsc::channel();

    let handle = thread::spawn(move || {
        let result = 40 + 2;

        sender
            .send(result)
            .expect("receiver должен существовать");
    });

    let result = receiver
        .recv()
        .expect("worker должен отправить результат");

    handle.join().expect("worker завершился с ошибкой");

    println!("Результат: {result}");
}

Потоки не обязаны напрямую разделять изменяемое состояние.

В Rust часто удобнее мыслить так:

поток → сообщение → канал → другой поток

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


Что означает «чистый код» в Rust: 6 принципов

Примеры кода на Rust выше опираются на несколько принципов идиоматичного стиля:

Используйте типы для описания возможных состояний.

Вместо:

String

для статуса лучше использовать:

enum Status

Не используйте специальные значения для ошибок.

Вместо:

-1
null
"error"

используйте:

Option<T>
Result<T, E>

Избегайте лишнего clone().

Rust заставляет явно думать о владении данными. Если функции достаточно ссылки:

fn process(data: &str)

нет необходимости передавать новый String.

Предпочитайте стандартные итераторы.

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

map()
filter()
find()
any()
all()
fold()
sum()
collect()

Не злоупотребляйте unwrap().

Код:

value.parse().unwrap()

может аварийно завершить программу.

Для реального приложения чаще правильнее:

value.parse()?

или явная обработка ошибки.


Разбор: примеры кода на Rust «до» и «после»

Возьмём типичную задачу. Так часто выглядят первые примеры кода на Rust у новичков:

fn get_user_age(value: String) -> i32 {
    let age = value.parse::<i32>();

    if age.is_ok() {
        return age.unwrap();
    }

    -1
}

Но более идиоматичный вариант:

fn parse_age(value: &str) -> Result<u8, std::num::ParseIntError> {
    value.parse()
}

Использование:

fn main() {
    match parse_age("35") {
        Ok(age) => println!("Возраст: {age}"),
        Err(error) => println!("Ошибка: {error}"),
    }
}

Вторая версия:

  • короче;
  • не копирует строку без необходимости;
  • не использует магическое значение -1;
  • не вызывает unwrap();
  • явно описывает ошибку через тип.

Какие возможности Rust стоит изучить дальше

Примеры кода на Rust из этой статьи покрывают базу. Дальше имеет смысл перейти к темам:

ownershipborrowinglifetimesOption/Resultiteratorstraitsgenericsmodulesasync/awaitTokio.

Именно после понимания этих механизмов Rust начинает ощущаться не как язык, который «мешает компилировать программу», а как язык, в котором компилятор помогает поддерживать архитектуру кода в порядке.

Полезно почитать по теме на UPROGER: миграция с Go на Rust — честный гайд, 25 практических примеров кода на Java для сравнения подходов и все материалы в разделе Rust.

Частые вопросы о чистом коде на Rust

Подойдут ли эти примеры кода на Rust для начинающих?

Да. Примеры идут от простого к сложному: сначала функции и итераторы, затем Option, Result, enum, traits и generics, в конце — работа с файлами и многопоточность. Для старта достаточно установить Rust через rustup и запускать код командой cargo run.

Чем Option отличается от Result в Rust?

Option описывает значение, которого может не быть (Some или None). Result описывает операцию, которая может завершиться ошибкой (Ok или Err), и хранит причину ошибки.

Почему в Rust не стоит часто использовать unwrap()?

unwrap() вызывает panic и аварийно завершает программу, если внутри None или Err. В учебных примерах это допустимо, но в рабочем коде лучше использовать оператор ?, match, unwrap_or или expect с понятным сообщением.

Что такое идиоматичный код на Rust?

Это код, который использует сильные стороны языка: типы для описания состояний, явную обработку ошибок через Result, итераторы вместо ручных циклов, заимствование вместо лишнего clone() и traits для общего поведения.

Что изучать после этих примеров?

Ownership и borrowing, lifetimes, модули и крейты, тесты через cargo test, а затем асинхронное программирование с async/await и Tokio.


Итог: как писать чистый код на Rust

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

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

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

Сохраните подборку в закладки: эти 20 примеров кода на Rust удобно использовать как шпаргалку при code review и при переходе на Rust с Python, Go, Java или C++.

Поделиться:

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *