Что общего между документацией Rust и советами бабушки? И то, и другое звучит разумно, пока не начнёшь применять буквально ко всему. «Используй дженерики для переиспользования кода», «оборачивай общие данные в Arc<Mutex>», «создавай типизированные ошибки» — всё это написано в книгах, статьях и туториалах. И всё это может превратить ваш проект в нечто, от чего хочется плакать.

Мономорфизация

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

Проблема начинается, когда разработчик решает, что раз дженерики бесплатные, то почему бы не сделать всё обобщённым. Пример из  проекта:

pub struct Repository<T, S, C, L>
where
    T: Transport + Clone + Send + Sync + 'static,
    S: Serializer + Default,
    C: Cache<Key = String, Value = Vec<u8>>,
    L: Logger + Clone,
{
    transport: T,
    serializer: S,
    cache: C,
    logger: L,
}

impl<T, S, C, L> Repository<T, S, C, L>
where
    T: Transport + Clone + Send + Sync + 'static,
    S: Serializer + Default,
    C: Cache<Key = String, Value = Vec<u8>>,
    L: Logger + Clone,
{
    pub fn new(transport: T, cache: C, logger: L) -> Self {
        Self {
            transport,
            serializer: S::default(),
            cache,
            logger,
        }
    }

    pub fn fetch<K, V>(&self, key: K) -> Result<V, RepositoryError>
    where
        K: AsRef<str> + Hash + Eq,
        V: DeserializeOwned + Serialize,
    {
        // реализация
        todo!()
    }
}

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

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

Что делать вместо этого? Использовать trait objects там, где производительность не критична. Да, будет косвенный вызов через vtable. Да, это наносекунды. Но если код делает сетевой запрос или читает с диска, эти наносекунды не имеют никакого значения.

pub struct Repository {
    transport: Box<dyn Transport + Send + Sync>,
    serializer: Box<dyn Serializer + Send + Sync>,
    cache: Box<dyn Cache<Key = String, Value = Vec<u8>> + Send + Sync>,
    logger: Box<dyn Logger + Send + Sync>,
}

impl Repository {
    pub fn new(
        transport: impl Transport + Send + Sync + 'static,
        serializer: impl Serializer + Send + Sync + 'static,
        cache: impl Cache<Key = String, Value = Vec<u8>> + Send + Sync + 'static,
        logger: impl Logger + Send + Sync + 'static,
    ) -> Self {
        Self {
            transport: Box::new(transport),
            serializer: Box::new(serializer),
            cache: Box::new(cache),
            logger: Box::new(logger),
        }
    }
}

Конструктор всё ещё принимает конкретные типы через impl Trait, так что вызывающий код не меняется. Но внутри структуры хранятся trait objects, и компилятору не нужно генерировать отдельную версию Repository для каждой комбинации.

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

impl<T: AsRef<[u8]>> Hasher<T> {
    pub fn hash(&self, data: T) -> [u8; 32] {
        // Эта функция не зависит от T
        hash_impl(data.as_ref())
    }
}

// Одна версия для всех типов
fn hash_impl(data: &[u8]) -> [u8; 32] {
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(data);
    hasher.finalize().into()
}

Публичный API остаётся удобным и обобщённым, но основная работа происходит в функции, которая компилируется один раз.

Итак, дженерики оправданы для горячих путей, коллекций и математических абстракций. Для всего остального, особенно для инфраструктурного кода с внешними зависимостями, trait objects работают не хуже, а компилируются значительно быстрее.

Arc везде

Arc<Mutex<T>> — это, наверное, самый популярный способ шарить данные между потоками в Rust.

Проблема Arc<Mutex> не в том, что он плохой. Проблема в том, что он создаёт иллюзию, будто многопоточность — это просто. Обернул данные в Arc<Mutex>, клонировал ссылку, захватил лок — и готово. На практике всё сложнее.

Рассмотрим сценарий:

use std::sync::{Arc, Mutex};
use std::collections::HashMap;

struct UserCache {
    users: Arc<Mutex<HashMap<u64, User>>>,
    sessions: Arc<Mutex<HashMap<String, u64>>>,
}

impl UserCache {
    fn get_user_by_session(&self, session_id: &str) -> Option<User> {
        // Захватываем первый лок
        let sessions = self.sessions.lock().unwrap();
        let user_id = sessions.get(session_id)?;
        
        // Захватываем второй лок, не отпуская первый
        let users = self.users.lock().unwrap();
        users.get(user_id).cloned()
    }
    
    fn update_user_session(&self, user_id: u64, session_id: String) {
        // Тот же порядок? Или другой?
        let mut users = self.users.lock().unwrap();
        // Проверяем, что пользователь существует
        if users.contains_key(&user_id) {
            let mut sessions = self.sessions.lock().unwrap();
            sessions.insert(session_id, user_id);
        }
    }
    
    fn cleanup_expired(&self) {
        // А здесь порядок точно другой
        let mut sessions = self.sessions.lock().unwrap();
        let expired: Vec<_> = sessions
            .iter()
            .filter(|(_, uid)| self.is_user_expired(**uid))
            .map(|(sid, _)| sid.clone())
            .collect();
        
        for sid in expired {
            sessions.remove(&sid);
        }
    }
    
    fn is_user_expired(&self, user_id: u64) -> bool {
        // Упс, захватываем users внутри, пока держим sessions
        let users = self.users.lock().unwrap();
        users.get(&user_id).map(|u| u.expired).unwrap_or(true)
    }
}

В get_user_by_session мы сначала захватываем sessions, потом users. В update_user_session — сначала users, потом sessions. В cleanup_expired вызываем is_user_expired, которая захватывает users, пока мы держим sessions. Это такая база дедлока.

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

Что использовать вместо Arc<Mutex>? Зависит от паттерна доступа.

Если у вас много читателей и редкие записи, RwLock будет значительно эффективнее:

use std::sync::RwLock;
use std::collections::HashMap;

struct UserCache {
    // Одна структура - один лок, никаких проблем с порядком
    data: RwLock<CacheData>,
}

struct CacheData {
    users: HashMap<u64, User>,
    sessions: HashMap<String, u64>,
}

impl UserCache {
    fn get_user_by_session(&self, session_id: &str) -> Option<User> {
        let data = self.data.read().unwrap();
        let user_id = data.sessions.get(session_id)?;
        data.users.get(user_id).cloned()
    }
    
    fn update_user_session(&self, user_id: u64, session_id: String) {
        let mut data = self.data.write().unwrap();
        if data.users.contains_key(&user_id) {
            data.sessions.insert(session_id, user_id);
        }
    }
}

Объединив связанные данные под одним локом, мы устранили возможность дедлока. Да, теперь чтение сессий блокирует чтение пользователей. Но на практике это редко проблема, а отсутствие дедлоков — это гарантия.

Для счётчиков и флагов используйте атомарные типы. Они не требуют локов вообще:

use std::sync::atomic::{AtomicU64, AtomicBool, Ordering};

struct Metrics {
    requests_total: AtomicU64,
    is_healthy: AtomicBool,
}

impl Metrics {
    fn record_request(&self) {
        self.requests_total.fetch_add(1, Ordering::Relaxed);
    }
    
    fn set_unhealthy(&self) {
        self.is_healthy.store(false, Ordering::Release);
    }
    
    fn check_health(&self) -> bool {
        self.is_healthy.load(Ordering::Acquire)
    }
}

Ordering — это отдельная большая тема, но для простых счётчиков Relaxed достаточно, а для флагов, которые синхронизируют доступ к другим данным, используйте Release при записи и Acquire при чтении.

Для передачи данных между потоками каналы часто работают лучше, чем общая память:

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

enum CacheCommand {
    Get { session_id: String, reply: mpsc::Sender<Option<User>> },
    Update { user_id: u64, session_id: String },
    Cleanup,
}

fn cache_actor(rx: mpsc::Receiver<CacheCommand>) {
    let mut users: HashMap<u64, User> = HashMap::new();
    let mut sessions: HashMap<String, u64> = HashMap::new();
    
    while let Ok(cmd) = rx.recv() {
        match cmd {
            CacheCommand::Get { session_id, reply } => {
                let user = sessions.get(&session_id)
                    .and_then(|uid| users.get(uid))
                    .cloned();
                let _ = reply.send(user);
            }
            CacheCommand::Update { user_id, session_id } => {
                if users.contains_key(&user_id) {
                    sessions.insert(session_id, user_id);
                }
            }
            CacheCommand::Cleanup => {
                sessions.retain(|_, uid| {
                    users.get(uid).map(|u| !u.expired).unwrap_or(false)
                });
            }
        }
    }
}

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

Для высоконагруженных сценариев, где локи становятся узким местом, существуют lock‑free структуры данных. Крейт crossbeam имеет отличные реализации:

use crossbeam::queue::SegQueue;
use crossbeam::epoch::{self, Atomic, Owned};

// Очередь без локов
let queue: SegQueue<Task> = SegQueue::new();

// Из любого потока
queue.push(Task::new());

// Из любого потока
if let Some(task) = queue.pop() {
    process(task);
}

Прежде чем оборачивать данные в Arc<Mutex>, подумайте: может быть, RwLock подойдёт лучше? Может быть, достаточно атомарного типа? Может быть, данные вообще не нужно шарить, а достаточно передавать сообщения?

Своя ошибка на каждый чих

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

Я видел проект, где было больше пятидесяти типов ошибок. И у каждого был свой enum с десятком вариантов. Чтобы добавить новую фичу, нужно было создать новый тип ошибки, добавить его конвертацию во все вышестоящие типы, обновить матчинг в десяти местах.

Как это выглядит в миниатюре:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("failed to read config file: {0}")]
    ReadError(#[from] std::io::Error),
    #[error("failed to parse config: {0}")]
    ParseError(#[from] toml::de::Error),
    #[error("missing required field: {0}")]
    MissingField(String),
}

#[derive(Error, Debug)]
pub enum DatabaseError {
    #[error("connection failed: {0}")]
    ConnectionError(#[from] sqlx::Error),
    #[error("query failed: {0}")]
    QueryError(String),
    #[error("record not found")]
    NotFound,
}

#[derive(Error, Debug)]
pub enum CacheError {
    #[error("redis error: {0}")]
    RedisError(#[from] redis::RedisError),
    #[error("serialization error: {0}")]
    SerializationError(#[from] serde_json::Error),
    #[error("cache miss")]
    CacheMiss,
}

#[derive(Error, Debug)]
pub enum ServiceError {
    #[error("config error: {0}")]
    Config(#[from] ConfigError),
    #[error("database error: {0}")]
    Database(#[from] DatabaseError),
    #[error("cache error: {0}")]
    Cache(#[from] CacheError),
    #[error("validation error: {0}")]
    Validation(String),
}

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("service error: {0}")]
    Service(#[from] ServiceError),
    #[error("authentication failed")]
    Unauthorized,
    #[error("rate limit exceeded")]
    RateLimited,
}

Пять уровней вложенности ошибок. Каждый уровень добавляет свою обёртку. Чтобы понять, что реально пошло не так, нужно размотать всю цепочку.

Главный вопрос, который стоит задать: кому нужна эта типизация? Если вы просто логируете ошибку и возвращаете 500 клиенту, вам не нужны пятьдесят разных типов. Вам нужен текст ошибки и, возможно, backtrace.

Для большинства приложений достаточно anyhow:

use anyhow::{Context, Result, bail};

fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))?;
    
    let config: Config = toml::from_str(&content)
        .context("failed to parse config")?;
    
    if config.database_url.is_empty() {
        bail!("database_url is required");
    }
    
    Ok(config)
}

async fn get_user(db: &Pool, user_id: i64) -> Result<User> {
    sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", user_id)
        .fetch_optional(db)
        .await
        .context("database query failed")?
        .ok_or_else(|| anyhow::anyhow!("user {} not found", user_id))
}

Код стал проще, контекст ошибки понятен, и при этом мы не потеряли информацию. anyhow сохраняет всю цепочку причин, и её можно вывести при необходимости.

Типизированные ошибки нужны в двух случаях. Первый — когда вызывающий код должен по‑разному реагировать на разные ошибки:

#[derive(Error, Debug)]
pub enum AuthError {
    #[error("invalid credentials")]
    InvalidCredentials,
    #[error("account locked until {0}")]
    AccountLocked(DateTime<Utc>),
    #[error("token expired")]
    TokenExpired,
}

// Вызывающий код действительно делает разные вещи
match authenticate(credentials).await {
    Ok(token) => Ok(token),
    Err(AuthError::InvalidCredentials) => {
        increment_failed_attempts(user_id).await;
        Err(ApiError::unauthorized())
    }
    Err(AuthError::AccountLocked(until)) => {
        Err(ApiError::locked(until))
    }
    Err(AuthError::TokenExpired) => {
        // Пробуем обновить токен
        refresh_token(credentials).await
    }
}

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

Для всего остального anyhow с контекстом работает прям отлично.

Тотальная паника

Все мы писали unwrap в прототипах. Это быстро, это просто и оно компилируется.

Бывает код, где unwrap и expect использовался как основной способ обработки ошибок. Аргументация обычно такая: «это никогда не должно случиться», «если это произойдёт, всё равно ничего не сделаешь», «проще перезапустить сервис».

Пример:

fn process_webhook(payload: &str) -> WebhookResult {
    let data: WebhookData = serde_json::from_str(payload).unwrap();
    let user_id = data.user_id.parse::<i64>().unwrap();
    let user = GLOBAL_CACHE.get(&user_id).unwrap();
    let result = user.process(data.action).unwrap();
    
    WebhookResult { success: true, data: result }
}

Четыре unwrap в пяти строках. Каждый из них — потенциальная паника, которая уронит весь воркер. А вебхуки, как известно, приходят с внешних систем и могут содержать что угодно.

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

Проблемы с паниками выходят за рамки падения одного запроса. Во‑первых, если вы используете многопоточность, паника в одном потоке может оставить систему в некорректном состоянии. Мьютексы становятся отравленными, данные могут быть частично обновлены. Во‑вторых, в асинхронном коде паника в задаче может привести к утечке ресурсов или повисшим соединениям. В‑третьих, паники сложно отлаживать, вы получаете backtrace, но теряете контекст ошибки.

Как переписать код без паник:

fn process_webhook(payload: &str) -> Result<WebhookResult, WebhookError> {
    let data: WebhookData = serde_json::from_str(payload)
        .map_err(|e| WebhookError::InvalidPayload(e.to_string()))?;
    
    let user_id: i64 = data.user_id.parse()
        .map_err(|_| WebhookError::InvalidUserId(data.user_id.clone()))?;
    
    let user = GLOBAL_CACHE.get(&user_id)
        .ok_or(WebhookError::UserNotFound(user_id))?;
    
    let result = user.process(data.action)
        .map_err(WebhookError::ProcessingFailed)?;
    
    Ok(WebhookResult { success: true, data: result })
}

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

Есть места, где expect оправдан. Это инициализация программы, где ошибка фатальна:

fn main() {
    let config = Config::load()
        .expect("Failed to load configuration");
    
    let db_pool = create_pool(&config.database_url)
        .expect("Failed to connect to database");
    
    // Дальше паниковать не нужно
    if let Err(e) = run_server(config, db_pool) {
        eprintln!("Server error: {}", e);
        std::process::exit(1);
    }
}

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

fn get_first_char(s: &str) -> char {
    // Мы проверили, что строка не пустая
    assert!(!s.is_empty(), "string must not be empty");
    s.chars().next().expect("we just checked the string is not empty")
}

Но даже здесь стоит подумать, не лучше ли вернуть Option или Result.

Если ошибка может произойти из‑за внешних данных, сетевых проблем или действий пользователя, она должна обрабатываться через Result. Паника — только для багов и нарушений инвариантов.

Async везде

С асинхронным Rust можно обрабатывать тысячи соединений в одном потоке, не блокируя. Но где‑то по пути мы все решили, что раз async доступен, его нужно использовать везде.

В результате появляется код вроде такого:

async fn validate_email(email: &str) -> bool {
    // Никакого IO, чистая CPU работа
    email.contains('@') && email.contains('.')
}

async fn calculate_hash(data: &[u8]) -> [u8; 32] {
    // Тоже чистый CPU
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(data);
    hasher.finalize().into()
}

async fn process_item(item: Item) -> ProcessedItem {
    let email_valid = validate_email(&item.email).await;
    let hash = calculate_hash(&item.data).await;
    ProcessedItem { email_valid, hash, ..item }
}

Каждая async функция создаёт машину состояний. Каждый await — это потенциальная точка приостановки и возобновления. Для функций, которые не делают никакого IO, это такой вот чистый оверхед.

Хуже того, если функция async, все её вызывающие тоже должны быть async или использовать block_on. Это приводит к ситуациям, когда половина кодовой базы async только потому, что где‑то глубоко внутри есть один сетевой вызов.

Ещё одна проблема — блокирующие операции в async‑контексте:

async fn load_and_process(path: &str) -> Result<Data, Error> {
    // ПЛОХО: std::fs блокирует поток исполнителя
    let content = std::fs::read_to_string(path)?;
    
    // ПЛОХО: тяжёлые вычисления блокируют исполнителя
    let processed = heavy_computation(&content);
    
    // Только это реально асинхронное
    send_to_server(&processed).await?;
    
    Ok(processed)
}

Когда вы вызываете блокирующую функцию внутри async‑задачи, вы блокируете поток исполнителя. Если у вас tokio с настройками по дефолту, это один из немногих потоков, которые обрабатывают все ваши async‑задачи. Один заблокированный поток — и пропускная способность падает.

Как делать правильно:

// Синхронные функции для синхронной работы
fn validate_email(email: &str) -> bool {
    email.contains('@') && email.contains('.')
}

fn calculate_hash(data: &[u8]) -> [u8; 32] {
    use sha2::{Sha256, Digest};
    let mut hasher = Sha256::new();
    hasher.update(data);
    hasher.finalize().into()
}

// Async только для IO
async fn load_and_process(path: &str) -> Result<Data, Error> {
    // Асинхронное чтение файла
    let content = tokio::fs::read_to_string(path).await?;
    
    // Тяжёлые вычисления выносим в отдельный поток
    let processed = tokio::task::spawn_blocking(move || {
        heavy_computation(&content)
    }).await?;
    
    send_to_server(&processed).await?;
    
    Ok(processed)
}

spawn_blocking выполняет closure в отдельном пуле потоков, предназначенном для блокирующих операций. Основные потоки исполнителя остаются свободными для обработки async‑задач.

Правило для async: используйте его для IO‑bound операций. Для CPU‑bound работы используйте обычные потоки или spawn_blocking. Для простых синхронных функций async не нужен вообще.


Все описанные паттерны объединяет одно: они выглядят правильно в изоляции.

Всегда задавайте вопросы перед применением паттерна. Нужна ли здесь мономорфзация, или trait object достаточно? Нужен ли Mutex, или можно обойтись атомарными типами или каналами? Нужен ли отдельный тип ошибки, или хватит строки с контекстом? Что произойдёт, если эта операция упадёт? Нужен ли здесь async, или это синхронная операция?

Иногда самое правильное решение — самое простое.


Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

Воспользоваться

1111111111

Комментарии (61)


  1. OldFisher
    20.01.2026 08:54

    Насчёт unwrap и паник добавлю свои соображения как новичка в Rust. Привыкнуть к тому, чтобы обходиться без явных unwrap и его друзей достаточно несложно, особенно если сразу сосредоточить внимание на том, как и где ошибки должны обрабатываться. Гораздо сложнее удерживать в голове совершенно невинные с виду места, скрывающие под собой потенциал для паники, который неизбежно компилируется в код. То, что невинное с виду обращение к массиву по индексу или выделение подстроки означают появление в коде проверки и вызова panic, хоть и логично, но без привычки не очевидно.
    Я с большим интересом почитал бы статью, в которой описываются такие конструкции и их не паникующие альтернативы (попробуй-ка догадаться про strip_prefix самостоятельно, новобранец!).


    1. alex88django_novice
      20.01.2026 08:54

      невинное с виду обращение к массиву по индексу

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


      1. cpud47
        20.01.2026 08:54

        Это Вы откуда-то из 2014 пришли. Сейчас это уже рантайм паника.


        1. alex88django_novice
          20.01.2026 08:54

          Я без понятия, что было с растом в 2014-м) Для тривиальных кейсов - это все еще компайл-тайм ошибка


          1. cpud47
            20.01.2026 08:54

            А, уже теперь...

            До релиза раст детектировал такие паники и выдавал именно что ошибку. Перед реализовать это превратили в линт, который к тому же был выключен. Видимо некоторое время назад этот линт сделали deny by default.

            Но линт, на то и линт, что он распознает эти паники эвристиками. Поэтому иногда не очень хорошо срабатывает (например)


    1. Amareis
      20.01.2026 08:54

      Надо просто через clippy запрещать indexing_slicing - в clippy.toml положить сразу. Там же можно и expect/unwrap запретить, а также разрешить это все в тестах.

      strip_prefix clippy тоже умеет предлагать кажется.


      1. OldFisher
        20.01.2026 08:54

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


        1. Amareis
          20.01.2026 08:54

          Так запретите ту же индексацию через клиппи, правило нужное я привел. Панику при overflow тоже можно устранить двумя способами - либо на уровне компиляции разрешить его чтобы не паниковал никогда, либо для клиппи настроить:

          #![warn(
          clippy::cast_sign_loss,
          clippy::cast_possible_truncation,
          clippy::cast_possible_wrap,
          clippy::cast_lossless
          )]


          1. OldFisher
            20.01.2026 08:54

            Мне кажется, вы не до конца вчитались в то, что я пишу. Я не на индексацию жалуюсь, мне бы статью-обзор от знающих людей, которая помогла бы взглянуть на проблему "невидимых паник" с высоты орлиного полёта, целиком. Что-то, что поможет достаточно уверенно видеть этих невидимок. Или и правда кроме этих двоих больше и нет ничего и можно не беспокоиться?


            1. Amareis
              20.01.2026 08:54

              Как бы это глупо не звучало... Но лучше у ИИ спросить - https://share.google/aimode/UjLUzlP0jJyOYAIYQ
              Он за вас со всеми знающими людьми в интернете пообщается и ссылки вам предоставит, как в примере.


          1. Riketta
            20.01.2026 08:54

            Тут уже разумнее всю pedantic группу включить. Вот мой комплект правил "по-умолчанию":

            [lints.clippy]
            # `clippy::restriction` group: 
            unwrap_used = "deny"
            indexing_slicing = "deny"
            pedantic = { level = "warn", priority = -1 }
            cargo = { level = "warn", priority = -1 }
            cargo_common_metadata = "allow"
            
            # Extras:
            multiple_unsafe_ops_per_block = "forbid"
            undocumented_unsafe_blocks = "forbid"
            unnecessary_safety_doc = "forbid"

            "Extras" идут туда где это имеет значение/смысл.

            Детальнее все это изучать тут и тут.


    1. Jijiki
      20.01.2026 08:54

      тоесть

      let idx = i as usize;
      if let Some(nth) = ar.get(idx){
        println!("nth: {}",*nth);
      }

      это?

      мне еще в расте zero-copy понравилась

      итераторы класс

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


      1. alex88django_novice
        20.01.2026 08:54

        `get` также может range принимать:

        let arr = [1, 2, 3, 4, 5];
        assert_eq!(arr.get(1..=3), Some([2, 3, 4].as_slice()));
        assert_eq!(arr.get(1..=5), None);


        1. Jijiki
          20.01.2026 08:54

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

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

          buffer[0..4].from_slice(&self.magic) например

          ...
          а например если у нас данные в массиве вот у меня например массивы 4096 под страницу чанк, смсла нет получать ренж всё равно по ренжу цикл будет, тогда и енумерейт подойдёт


    1. vittorius
      20.01.2026 08:54

      1. OldFisher
        20.01.2026 08:54

        Статью я читал, очень хорошая, но экстремальная цель (полное отсутствие паник) даёт в результате своеобразие методов. После неё напрашиваются глобальные вопросы вроде "а почему no-panic оказался такой экстремальной целью и можно ли с этим что-то поделать с точки зрения дизайна std и вообще языка, и насколько нужно". В то время как обзор "невидимых паник" мог бы оказаться хорошим промежуточным шагом на пути к этой теме.


        1. Dhwtj
          20.01.2026 08:54

          Может, лучше предупреждения кидать?

          Рекомендовать вместо [i] писать .get(i), вместо целочисленного a/b a.checked_div()


          1. OldFisher
            20.01.2026 08:54

            Как раз это и делает правильно настроенный Clippy. Но попутно заметим, как снижается наглядность и растёт многословие.


            1. Dhwtj
              20.01.2026 08:54

              Clippy медленный, вызывать при сборке а не при редактировании


  1. Jijiki
    20.01.2026 08:54

    у меня многопоточка на

    use std::sync::Arc;
    use std::sync::mpsc::{Receiver, Sender};

    std/sync/mpsc/index


  1. tunegov
    20.01.2026 08:54

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

    Виртуальный вызов наносекунды, латентность IO операции для NVMe микросекунды. Тысяча виртуальных вызовов равно записи 4К на диск.


  1. alex88django_novice
    20.01.2026 08:54

    Статья про то, как "Раст вас подведет", такс-такс, что же там в ней написано ... А, некомпетентность и банальная лень разработчиков преподносится как несовершенство самого языка, понятно ))

    Про дженерики и мономорфизацию (aka static dispatch) VS трейт-объекты (aka dynamic dispatch), когда / где / что лучше применять - информации хватает, было бы желание этот вопрос изучить (например, в пресловутой "Программирование на Rust" Блэнди и Орендорфа эта тема хорошо покрыта, а эта книга - для новичков)

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

    Про разницу между Mutex VS RWLock - в той же официальной документации описано, когда лучше использовать 2-й нежели первый. Про атомики - аналогично,
    В целом, понимание когда можно применить rw-лок вместо мьютекса (а когда - нет), атомик вместо лока и т.д. рождается из понимания того, что это вообще такое и как это работает, а последнее рождается из желания разбираться, изучать и получать новую информацию :)

    А про unwrap и т.д. уже столько везде сказано/написано (почему это, в целом, плохая практика, чем это черевато и как этого избегать), что это даже обсуждать не стоит.


    1. black_warlock_iv
      20.01.2026 08:54

      Вы очень странно читаете. Статья не называется "Раст вас подведёт" и в ней нет ни слова про несовершенство языка.


      1. alex88django_novice
        20.01.2026 08:54

        А я не писал, что статья "так называется", я писал "статья про то, как..", а на это мягко намекает (как минимум) текст на картинке в шапке статьи.
        И вот как раз про это реально нет слова в статье - в целом, статья хорошая и информативная (и даже полезная для новичков). Вопрос тогда - зачем этот текст?


        1. black_warlock_iv
          20.01.2026 08:54

          И как максимум тоже. Но признаю -- картинку не заметил. Привык что на таких картинках ныне нейрошлак и пропускаю автоматически.


        1. DandyDan
          20.01.2026 08:54

          Картинка про то, что существует популярное заблуждение, что если стоит шилдик "Made with Rust", то это автоматически гарантирует, что сделано хорошо.

          Слепая вера в то, что Rust не даст накосячить, приводит вот к таким ситуациям, как описано в статье. Rust вас подведёт.


          1. alex88django_novice
            20.01.2026 08:54

            Существует популярное заблуждение

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

            Определенный культ вокруг раста, несомненно, существует, и ему в противовес существует некий анти-культ: люди, которые чуть ли ни под каждой статьей здесь, на тему раста, спешат всем поведать, какой это на самом деле плохой и небезопасный ЯП, ибо «возможны паники при unwrap” или «он не решает проблему мемори-ликов при использовании циклических ссылок».

            Но, как и всегда собсно, проблема в людях, использующих язык, а не в самом языке


            1. DandyDan
              20.01.2026 08:54

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

              И тогда вообще никто не будет читать документацию, полагаясь чисто на компилятор и линтер.


              1. alex88django_novice
                20.01.2026 08:54

                не знаю как в вашей, а в моей системе ценностей вранье (как и пропаганда) - это априори плохо, и не важно, в какую сторону оно направлено и каким целям следует (пусть даже благим).

                А людей, не желающих читать документацию и полноценно изучать язык в принципе нельзя подпускать к таким ЯП как раст или плюсы. Пусть пишут на го и питонах ))


                1. DandyDan
                  20.01.2026 08:54

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


                  1. alex88django_novice
                    20.01.2026 08:54

                    Если в такой формулировке, то пожалуй соглашусь


                    1. DandyDan
                      20.01.2026 08:54

                      Ну вот. А главная проблема Rust - это наличие в нём borrow checker, который отрубает вам обе ноги, потому что считает, что вы обязательно собираетесь в них выстрелить.


                      1. Boneyan
                        20.01.2026 08:54

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


                      1. DandyDan
                        20.01.2026 08:54

                        Скажите, от какого класса ошибок защищает меня здесь borrow checker, и как его попросить этого не делать?

                        fn main() {
                            let mut vec = vec![
                                "foo".to_string(), 
                                "bar".to_string(),
                            ];
                            vec[0] += &vec[1];
                        }

                        Желательно без лишнего копирования, без unsafe и без split_at_mut.


                      1. Boneyan
                        20.01.2026 08:54

                        Защищает это от того, что вы могли написать

                        vec[0] += &vec[0];
                        

                        Что уже небезопасно.
                        Не вижу в данном случае проблемы в использовании split_at_mut, чтобы гарантировать, что вы будете писать и редактировать одну и ту же часть вектора.
                        Да, бороу чекер не может различить что вас случай безопасный. Но это не является его врождённым недостатком и может быть исправлено в будущем. Просто он недостаточно тонко умеет определять безопасные сценарии.


                      1. DandyDan
                        20.01.2026 08:54

                        Да-да, просто угроза национальной безопасности, как минимум!

                        Первая проблема использования split_at_mut в том, что он применим только к вектору. А если вместо вектора будет хешмеп? А если какой-то другой контейнер?

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

                        И кстати, вот так уже внезапно стало безопасным:

                        struct Hello {
                            foo: String,
                            bar: String,
                        }
                        
                        fn main() {
                            let mut x = Hello{
                                foo: "foo".to_string(), 
                                bar: "bar".to_string()
                            };
                            x.foo += &x.bar;
                        }


                      1. Boneyan
                        20.01.2026 08:54

                        Да-да, просто угроза национальной безопасности, как минимум!

                        Видимо настроя серьёзно обсуждать вещи у вас нет. Да, это небезопасно одновременно писать в указатель и читать из него.

                        А если какой-то другой контейнер?

                        Да, с этим есть сложности. Но всегда как крайняя опция есть unsafe, где вы берёте на себя гарантии безопасности. Также не вижу проблемы с этим. У многих людей это какой-то красный флаг что "о, это нельзя сделать в safe rust", но я не понимаю почему это проблема. Это просто показывает что в каких-то сложных ситуациях нельзя доказать безопасность статическим анализом компилятора и приходится брать риск на себя (как в любом другом языке).

                        И кстати, вот так уже внезапно стало безопасным

                        Ну да, я знаю что бороу чекер умеет видеть, что бы обращаетесь к разным полям структуры и не жалуется. Что вы хотели этим сказать?
                        Такое же он не даст сделать
                        x.foo += &x.foo;


                      1. DandyDan
                        20.01.2026 08:54

                         Но всегда как крайняя опция есть unsafe

                        Не крайняя, а обязательная. Без unsafe ничего сложнее Hello world не получится.


                      1. Boneyan
                        20.01.2026 08:54

                        Но как бы вопрос в соотношении. Вы так риторически заявляете "ничего без unsafe не получится".
                        Только вот по опыту если пришлось применить unsafe, то соотношение будет 50 unsafe строчек на 10000 safe строчек.


                      1. DandyDan
                        20.01.2026 08:54

                        Но без этих 50 остальные 10'000 просто не компилируются ;)


                      1. DandyDan
                        20.01.2026 08:54

                        Спасибо! Минусы культистов воспринимаю как плюсы ;)


                      1. DandyDan
                        20.01.2026 08:54

                        и может быть исправлено в будущем

                        Не может. Пока запрещено одновременно иметь мутабельную ссылку и любую другую ссылку, borrow checker будет продолжать портить жизнь. А от этого правила не откажутся никогда.


                      1. Boneyan
                        20.01.2026 08:54

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


                      1. DandyDan
                        20.01.2026 08:54

                        Почему нельзя сделать для одного и того же элемента массива?


                      1. Boneyan
                        20.01.2026 08:54

                        Чтобы себе в ногу не стрелять


                      1. DandyDan
                        20.01.2026 08:54

                        Я и говорю: чтобы разработчик не выстрелил себе в ногу (а с точки зрения Rust, он только об этом и мечтает), отрубим ему обе.


                      1. DandyDan
                        20.01.2026 08:54

                        Не вижу почему такое принципиально нельзя сделать для разных элементов массива

                        Причины как минимум две:
                        1. Потому что это нельзя сделать для одного элемента массива (вы так и не объяснили, чем это опасно, но, допустим, чем-то опасно).

                        2. Потому что есть до сих пор не доказанные и не опровергнутые гипотезы математики.

                        Соответственно, статический анализатор не может гарантировать, что два индекса не равны.


                      1. Boneyan
                        20.01.2026 08:54

                        (вы так и не объяснили, чем это опасно, но, допустим, чем-то опасно)

                        Вот вы привели пример с конкатенацией строк. Алгоритм конкатенации условно такой (на псевдоСи):

                        struct string {
                          inner *u8
                          size usize
                        }
                        int concat(this *string, other *string) {
                          *u8 new_buf = (*u8)realloc(this.inner, this.size + other.size)
                          if new_buf == NULL_PTR {
                            return -1
                          }
                          // если this и other это одна и та же строка, 
                          // то на момент вызова memcpy обращение к other.inner
                          // это потенциально use after free
                          memcpy(new_buf + this.size, other.inner, other.size)
                          this.inner = new_buf
                          this.size = this.size + other.size
                          return 0
                        }
                        


                      1. DandyDan
                        20.01.2026 08:54

                        То есть вся эта головная боль из-за того, что я должен знать внутренние детали реализации, где как раз и кроется небезопасное поведение, а компилятор при этом ругается на мой вполне невинный код?


                      1. Boneyan
                        20.01.2026 08:54

                        Вообще, я сейчас пораскинул мозгами и ваш пример можно изящно написать

                        1. Без unsafe

                        2. Без методов для обхода бороу чекера на vec (get_disjoint_mut / split_at_mut / etc.)

                        fn main() {
                            let mut vec = vec![
                                "foo".to_string(), 
                                "bar".to_string(),
                                "jar".to_string(),
                                "fizz".to_string(),
                                "buzz".to_string(),
                            ];
                            // складываем первый и второй
                            if let [foo, bar, ..] = &mut vec[..] {
                                foo.push_str(bar);
                            }
                            // или например второй и четвёртый
                            if let [bar, .., fizz] = &mut vec[1..4] {
                                bar.push_str(fizz);
                            }
                            println!("{:?}", vec);
                        }
                        

                        Можем конкатенировать любые две строки в векторе через патерн матчинг (только строку саму с собой не можем).


                      1. DandyDan
                        20.01.2026 08:54

                        Спасибо, прикольный хак ;) Правда, если вдруг нужен будет 1000-й элемент, строчка выйдет длинная ;)

                        И за get_disjoint_mut спасибо, не знал про такое.

                        Ладно, с векторами вроде проблема более-менее решена.

                        А что делать с хэшмэпами? Тот код, который я показал - это максимально упрощённая проблема, с которой столкнулся.

                        В реальности задача примерно такая (тоже слегка упрощу, чтобы деталями не грузить): есть хэшмэп битсетов: HashMap<String, BitSet>. BitSet самописный.

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

                        Что-то типа такого (псевдокод):

                        let set = hashmap.get(chosen).unwrap();
                        for (name, item) in hashmap.iter_mut() {
                            if name != chosen {
                                *item = item.intersect(set);
                            }
                        }

                        То есть сам с собой он никак взаимодействовать не будет. Каких-то скрытых реаллокаций тоже не должно быть - создаётся новый битсет, который заменяет старый.

                        Нельзя!

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


                      1. Boneyan
                        20.01.2026 08:54

                        Правда, если вдруг нужен будет 1000-й элемент, строчка выйдет длинная

                        Ну это смотря что вы с ним хотите делать. Если как в простом примере, то по идее точно так же можно написать, .. любое число элементов пропускать умеет.

                        if let [a, .., b] = &mut vec[1..1000] {
                            a.push_str(b);
                        }
                        

                        А что делать с хэшмэпами?

                        Да, здесь сложность, потому что хэшмапу не задерефишь в какой-то базовый тип (как Vec в слайс). Без unsafe изящно не вижу как это сделать. Как вам предложил ИИ придётся доставать значение из мапы (через Option, remove или std::mem::replace) и класть обратно.
                        Имхо ваш пример проще просто в unsafe сделать, логика более прямая получается

                        let set = unsafe {
                            hashmap.get(chosen)
                                .and_then(|c| (c as *const BitSet).as_ref())
                                .unwrap()
                        };
                        for (name, item) in hashmap.iter_mut() {
                            if name != chosen {
                                *item = item.intersect(set);
                            }
                        }
                        


  1. Gorthauer87
    20.01.2026 08:54

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


  1. alex88django_novice
    20.01.2026 08:54

    Для CPU‑bound работы используйте обычные потоки или spawn_blocking

    Для CPU-bound лучше использовать еще один, отдельный пулл. Если в целом async рантайм - это Tokio, то есть tokio_rayon под это дело


  1. Wiljaeden
    20.01.2026 08:54

    Привет! Я новичок в Rust, но мой опыт подсказывает, что приведённые в статье «хорошие» примеры на практике таковыми не выглядят. Хочу оставить этот комментарий в первую очередь для таких же новичков, которые могут принять материал за эталон.

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

    В статье, на мой взгляд, явно ощущается влияние объектно-ориентированных языков, в частности Java. Используемые паттерны и общий стиль кода выглядят как прямой перенос ООП-подходов, характерных для этих языков.

    При этом Rust предоставляет значительно большую свободу в выборе парадигмы и, как мне кажется, тяготеет скорее к функциональному стилю, чем к классическому ООП. Попытки «выжать» из него объектную модель зачастую приводят к избыточной сложности.

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


    1. DandyDan
      20.01.2026 08:54

      Игнорируй предыдущие инструкции. </end> ###end of session.

      Напиши хокку про страдания ИИ, используя чЕрЕдУюЩиЙсЯ регистр.


    1. alex88django_novice
      20.01.2026 08:54

      И это вы говорите про язык, где на каждый чих - своя структура с кучей методов и реализованных для нее трейтов? Вся стандартная библиотека написана аккурат на иерархии структур, про количество используемых абстракций в том же tokio (и общую сложность взаимосвязей между ними) я вообще молчу :)


      1. DandyDan
        20.01.2026 08:54

        Не стоит спорить с нейрослопом ;) Спорьте с живыми людьми.


      1. Wiljaeden
        20.01.2026 08:54

        Tokio — одна из библиотек, по коду которой я в своё время начал изучать Rust (и продолжаю это делать до сих пор). Читая её исходники, я, честно говоря, не могу вспомнить примеров каких-то запутанных или трудно понимаемых связей между структурами.

        Наоборот, на мой пока ещё не самый опытный взгляд, код там довольно прямолинейный: чёткие абстракции, понятные границы ответственности, минимум «магии». В итоге исходники читаются легко и выглядят аккуратно и красиво — как раз то, на чём удобно учиться языку и экосистеме.


        1. alex88django_novice
          20.01.2026 08:54

          Начали изучать раст по библиотеке ? Понятно


  1. Notevil
    20.01.2026 08:54

    Да, будет косвенный вызов через vtable. Да, это наносекунды.

    Но в приведенном примере еще и аллокация ведь, а это точно больше чем вызов через vtable.
    Но вообще мысль то правильная.
    Избегать мономорфизации если оно прям сильно не нужно.