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

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 из этой статьи покрывают базу. Дальше имеет смысл перейти к темам:
ownership → borrowing → lifetimes → Option/Result → iterators → traits → generics → modules → async/await → Tokio.
Именно после понимания этих механизмов 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++.





