Привет, Хабр! В Rust есть тип, у которого нет ни одного возможного значения. Звучит необычно. Но я однажды столкнулся с этим самым никогда‑типом и понял — без него жить в Rust уже не хочется! Что это такое и зачем нужно — разберём подробно. По ходу дела упомянем и связанные фичи: Infallible, новоявленные макросы вроде matches!, разные фишки для оптимизации кода и FFI, про которые часто не догадываешься.

Тип «никогда» (!) и связанные с ним фишки

Впервые про тип ! (он же never type, «ничего не возвращает») мы читаем так: «тип вычислений, которые никогда не приводят к значению». Например, функция fn exit(code: i32) -> ! вообще завершает процесс и никогда не возвращается. Аналогично, panic!() или бесконечный loop {} тоже по сути «возвращают» !, потому что код после них недостижим. Что удивительно, выражения типа break, continue или return тоже обладают типом !: например, можно написать:

let x: ! = { return 123; };

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

Пример:

let num: u32 = match get_number() {
    Some(n) => n,
    None    => break, // break имеет тип `!`, и его можно подмешать к u32
};

Ветка None никогда не вернёт число, и это безопасно: ничего не случится, мы прервали цикл, не стали доставать u32.

fn forever() -> ! {
    loop {
        // бесконечный цикл
    }
}

Или

fn cant_fail() -> ! {
    panic!("Упс!");
}

Обе этих функции по факту возвращают «ничего», они нибудь запустят panic!, либо зависнут в бесконечном цикле, так что из них фактически ничего не возвращается. Благодаря этому можно писать код вроде:

let ok: Result<u32, Infallible> = Ok(5);
let value: u32 = match ok {
    Ok(v) => v,
    Err(e) => match e { /* пусто, e имеет тип ! */ },
};

Err(e) никогда не случится, потому что Infallible, а о нём чуть позже — тип без вариантов. Функция match e {} компилируется, потому что компилятор видит: «о, в этом месте у нас ! — ни одного пути, ничего не надо возвращать, всё ок».

Главная фича: ! приводится к любому типу. То есть выражение ! можно положить в любой контекст. Например, рассмотрим Result<T, !> — это результат, который либо Ok(T), либо Err типа !. Но Err не может быть, так что на практике Result<T, !> ведёт себя как просто T. Можно применить, когда мы в обобщённом коде хотим сказать «здесь просто не может быть ошибки». Пример: стандартный трейт FromStr у String. Пишем:

use std::str::FromStr;
let Ok(s) = String::from_str("hello");

Поскольку в этом Result<String, !> вариант Err с ! просто невозможен, мы можем писать «разворачивание» сразу через Ok(v), не заботясь об ошибке. Как говорится в доке, «так как Err содержит !, оно никогда не произойдёт; можно матчиться по Result<T,!> исключительно на Ok».

А что такое Infallible?

Это как раз этот самый «тип без вариантов», определённый в std::convert (на самом деле сейчас это enum Infallible {}). В документации видим: «Тип ошибки для ошибок, которые никогда не происходят. Поскольку этот enum не имеет вариантов, значение этого типа никогда не может существовать». Фактически Infallible — это стенд‑ин для будущего !: в будущем Rust собирается сделать pub type Infallible = !, то есть полностью заменить на never‑type. Уже сейчас можно писать так:

use std::convert::Infallible;
fn can_never_fail() -> Result<(), Infallible> {
    Ok(())
}

А компилятор не будет ругаться, что мы не обрабатываем Err, потому что его попросту нет. Более того, в Rust 1.92 появилось обновление: Result<(), Infallible> и ему подобное больше не забрасывает предупреждение unused_must_use. Раньше нужно было писать let = cannever_fail();, чтобы потушить варнинг, теперь компилятор сам понимает, что Infallible означает невозможность ошибки.

Кстати, ! ещё помогает компилятору доказывать полное покрытие match. Допустим, есть enum Opt { Some(i32), None }, и мы пишем:

match opt {
    Opt::Some(v) => println!("{}", v),
    Opt::None    => println!("none"),
}

здесь всё чётко и по делу.

Но если у нас был бы enum R { Ok(u32), Err(Infallible) }, то match автоматом поймёт, что ветка Err покрыта всегда, и не потребует её писать. То есть R::Err(x) просто никогда не случится. В старые времена без ! пришлось бы заводить пустой матч по Err(e) => match e {}, а теперь компилятор всё сам подсказывает.

matches! — простой способ проверить шаблон

Писал я как‑то фильтрацию по вектору и устал от громоздких проверок if let. Захотелось просто проверить, совпадает ли переменная с этим шаблоном. Для этого в Rust стабилизировали макрос matches!. Он выглядит так: matches!(expr, Pattern). Возвращает bool — true, если expr подходит под Pattern. Поддерживаются любые шаблоны, включая | и if‑гварды.

До появления matches! код выглядел обычно так:

let mut count = 0;
for maybe in values {
    if let Some(x) = maybe {
        if x > 10 {
            count += 1;
        }
    }
}

А с matches! все круче:

let count = values.iter()
    .filter(|x| matches!(x, Some(n) if *n > 10))
    .count();

Макрос принимает шаблон точно так же, как вы пишете в match. Например:

assert!(matches!(foo, Some(a) | None if cfg!(feature = "test")));

будет проверять: «foo соответствует Some(a) или None при заданной конфигурации».

Конечно, у matches! есть нюанс, он не позволяет захватывать значения из паттерна. То есть matches!(opt, Some(x)) скажет нам «да/нет», но не даст саму x. Если нужен x,то всё равно придётся использовать if let Some(x) = opt, или match. Макрос по сути лишь синтаксический сахар для match с игнорируемыми ветками. Но он очень облегчает код, где нам нужен только булев результат. Например, вместо

if let Ok(err) = result {
    // здесь не хотим работать с err, нам важно лишь, что это Ok
}

можно написать просто:

if matches!(result, Ok(_)) {
    // ясно, что result это Ok(_)
}

и ничего лишнего.

Один из рабочих примеров, где matches! поможет: проверка состояний enum или фильтрация. Допустим, есть enum State { Init, Running, Stopped } и в цикле мы хотим подсчитать только Running. Без matches!:

let mut cnt = 0;
for s in states {
    if let State::Running = s {
        cnt += 1;
    }
}

С matches! — чище:

let cnt = states.iter().filter(|s| matches!(s, State::Running)).count();

Ещё matches! поддерживает гварды, так что можно писать matches!(x, Some(v) if v > 10) без проблем.

std::hint::black_box() — хитрые оптимизации для бенчмарков

Ещё одна нестандартная вещичка, макрос/функция black_box из std::hint. Он был стабилизирован в Rust 1.66 (раньше был доступен как test::black_box в nightly). Что он делает? По сути ничего интересного! Берёт значение и возвращает его. Зачем тогда нужен? Ответ — чтобы помешать компилятору оптимизировать ваш код слишком сильно (что?).

В документации так и написано: «функция‑идентичность, которая намекает компилятору быть максимально пессимистичным насчёт того, что он может делать с этим значением». Иначе говоря, компилятор будет считать, что black_box(x) может использовать x любым возможным образом (без нарушения UB), и поэтому не станет выкидывать код или переменные, связанные с ним.

К примеру есть функция, заполняющая Vec, и без black_box весь цикл просто вычищается оптимизатором. Добавляя black_box, мы говорим: «вот указатель v.as_ptr(), с ним может быть что угодно», и компилятор вставляет лишние инструкции, чтобы «имитировать» использование.

Например, без black_box может быть код:

fn push_four(v: &mut Vec<i32>) {
    for i in 0..4 {
        v.push(i);
    }
}

После оптимизации push_four может вообще не создать значимые инструкции, если результат не используется. А с black_box:

use std::hint::black_box;
fn push_four(v: &mut Vec<i32>) {
    for i in 0..4 {
        v.push(i);
        black_box(v.as_ptr()); // намекаем, что указатель может быть куда-то использован
    }
}

Компилятор теперь вынужден «думать», что v.as_ptr() передаётся в некий extern‑код, и оставляет цикл в сборке.

А вы сами профилируйте и решайте, нужны ли эти приемчики.

Прозрачные обёртки: #[repr(transparent)]

Допустим, пишете структуру‑новый тип, чтобы сделать код чище, например:

struct UserId(u64);

Кажется, пусть будет так. Но при передаче через FFI к C это без repr(transparent) не гарантируется быть просто u64. На некоторых ABI новый тип может вели себя иначе, чем чистый u64. И тут поможет #[repr(transparent)]. Как гласит документация, repr(transparent) гарантирует: макет нового типа совпадает с макетом его единственного поля. То есть, например:

#[repr(transparent)]
struct Handle(u32);

будет иметь тот же набор байт и выравнивание, что и обычный u32. Это значит, если в C объявлено void do_something(u32), мы можем спокойно передать туда Handle (даже по указателю), вызов будет корректным.

Отличие repr(transparent) от просто repr(C) на одном поле в том, что эти гарантии именно формализованы. Без атрибута Rust не обещает навсегда вести себя именно как C, компилятор может со временем перестроить layout оптимально. Но repr(transparent) говорит: «гарантирую стабильно вести себя как внутренний тип по всем ABI». Например, RFC 1758 указывает, что без него на ARM64 функция, возвращающая структуру из одного f64, компилируется иначе, чем функция возвращающая просто f64. С repr(transparent) мы даём понять компилятору: везде пусть это точно как f64. В спецификациях приводится пример:

#[repr(transparent)]
struct Fancy(f64);
extern "C" { fn get_double() -> Fancy; } 
// — обработка, как f64, на всех платформах:contentReference[oaicite:24]{index=24}.

Без transparent такой FFI может упасть (segment fault) на ARM64, потому что возвращение Fancy без атрибута ведётся «косвенно» (с указателем), тогда как f64 улетает в регистре.

В коде на Rust repr(transparent) нужен обычно для «type‑safe» обёрток над указателями, идентификаторами и тому подобное. Например,

#[repr(transparent)]
struct FileHandle(i32);

сделает FileHandle точно таким же, как i32 внутри. Или обёртки системных дескрипторов: в UNIX socket и дескрипторы файлов — i32. Теперь эти типы несут смысловую нагрузку (чтобы не перепутать), но ABI‑совместимы.

Если к прозрачному типу добавить ещё не‑NZST поле (не‑нуль‑размерный), компилятор упадёт. Можно добавлять поля‑заглушки нулевого размера PhantomData — это не вредит, потому что PhantomData<T> не влияет на макет. Например:

#[repr(transparent)]
struct TransparentWrapper {
    data: u64,
    marker: std::marker::PhantomData<MyType>,
}

всё ещё «прозрачен» — гарантируется только data в памяти.

Интересно, что сами стандартные типы используют эту фичу, например std::num::NonZeroU64. Его объявление pub type NonZeroU64 = NonZero<u64>;отмечено как repr(transparent). Это нужно, чтобы Option<NonZeroU64> занимал не больше, чем u64 (0 в u64 зарезервировано для None).

Документ говорит: «Option<NonZeroU64> гарантированно совместим с u64, включая в FFI». То есть Rust не будет подсаживать лишний байт или выравнивание. И действительно, проверим:

use std::num::NonZeroU32;
assert_eq!(std::mem::size_of::<Option<NonZeroU32>>(), std::mem::size_of::<u32>());

Это называется niche optimization: одно «режимное» значение освобождает место для Option. Rust зовёт это «null pointer optimization» — NonZeroU64 без Option имеет тот же размер и выравнивание, что и Option<NonZeroU64>.

Главный нюансик в том, что если не объявлять #[repr(transparent)], один‑единственный доп. нечувствительный тип (даже новыйtype со repr(C)) может вести себя иначе в C/ABI. Например,

#[repr(C)]
struct Foo(f64);

на ARM64 функции fn f() -> Foo возвратят значение непрямо. А с repr(transparent) эта проблема решается.

std::mem::transmute_copy() — клонирование битовым образом

Теперь про очень нижнеуровневую штучку. Все знают std::mem::transmute<T,U>, но есть ещё transmute_copy. Отличие: transmute требует, чтобы размеры типов совпадали. А transmute_copy позволит читать из источника на количество байт, равное размеру целевого типа. Он берёт указатель на T и читает оттуда size_of::<U>() байт, выдавая U.

Например, взяв байты в [u8; 1] и прочитав из них структуру с одним u8:

#[repr(C)]
struct Foo { bar: u8 }
let bytes = [10u8];
let foo: Foo = unsafe { std::mem::transmute_copy(&bytes) };
assert_eq!(foo.bar, 10);

transmute_copy интерпретирует &T как &U и читает значение, не перемещая (unsafe code).

Казалось бы, опасно, но это полезно при парсинге бинарных данных. Допустим, есть [u8; 4] — и мы хотим прочитать заголовок сетевого пакета:

#[repr(C)]
struct Header { a: u16, b: u16 }
let bytes: [u8; 4] = [0x34, 0x12, 0x78, 0x56]; // little endian: a=0x1234, b=0x5678
let hdr: Header = unsafe { std::mem::transmute_copy(&bytes) };
assert_eq!(hdr.a, 0x1234);
assert_eq!(hdr.b, 0x5678);

При этом transmute_copy просто копирует нужное число байт. Если size_of::<U>() > size_of::<T>(), такое чтение выходит за пределы исходных данных — UB! Если U больше, чем T, это UB.

Поэтому transmute_copy часто комбинируют с MaybeUninit. Если вы читаете частично инициализируете поля структуры, вы можете сперва положить части данных в MaybeUninit<[u8; N]>, а потом безопасно слить в структуру нужного размера. Но если можно, лучше использовать безопасные аналоги: from_ne_bytes для массивов фиксированного размера, библиотеки вроде bytemuck или даже std::ptr::read_unaligned. transmute_copy — крайний случай, когда вы действительно хотите буквально сурово скопировать битовый кусок.

transmute_copy(&x) работает как «скопировать sizeof(U) байт из места x.


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

Удачи!


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

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

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


  1. chicory-ru
    20.02.2026 08:59

    Что значит loop не возвращает значения? Он то, как раз, и может это делать. let x = loop { break 123; };


    1. Pavel_Agafonov
      20.02.2026 08:59

      В чем претензия? loop {} имеет тип ! by default. О том, что из loop нельзя никогда ничего возвращать, не говорится


      1. chicory-ru
        20.02.2026 08:59

        "бесконечный loop {} тоже по сути «возвращают» !, потому что код после них недостижим. Что удивительно, выражения типа breakcontinue или return тоже обладают типом !" вот.


        1. VyacheslavHere
          20.02.2026 08:59

          Но ведь это действительно так. Можете посмотреть крейты rustc_infer и rustc_hir_typeck.


    1. OldFisher
      20.02.2026 08:59

      del


  1. sokoloid
    20.02.2026 08:59

    По сути ОК, но перевод Г (странно, что не помечен как перевод, т.к. терминология странная). Не использовал из этого только black_box. transmute_copy возможно где-то даже полезней transmute, т.к. последний по факту имеет на выходе тот же результат, что и match, а зачем использовать unsafe, если safe дает тот же результат. См. https://rust.godbolt.org/z/bf88bzxW1 (ссылка из другой статьи на Habr). Да, где то в blanket имплементациях будет удобен transmute, просто чтобы не писать много кода.


  1. blackyblack
    20.02.2026 08:59

    В чем смысл работать с Result, как будто в нем не может быть ошибки? Логичнее в таком случае просто не оборачивать значение в Result.

    Также, если мы хотим проверить результат на Ok и само значение нам не важно, то проще вызвать is_ok() без всяких matches!


    1. Mingun
      20.02.2026 08:59

      В чем смысл работать с Result, как будто в нем не может быть ошибки?

      В generic-контексте (точнее, когда из generic контекста возвращаемся в свой уже определенный контекст). Да даже пример привели в типажом FromStr


      1. blackyblack
        20.02.2026 08:59

        По сути костыль какой-то. Есть трейты From и TryFrom, где логично возвращается чистое значение или обернутое в Result. Надо было или по аналогии делать FromStr и TryFromStr, либо вообще отказаться от этого трейта и делать через From.


        1. KivApple
          20.02.2026 08:59

          Это не так работает. Представьте у вас есть какая-то функция принимающая аргумент/возвращающая результат TryFromStr, чтобы в неё можно было передавать всякие числа и более сложные типы (если она будет принимать FromStr, то это сильно ограничит типы, которые в неё можно передать, а тем временем она по смыслу вполне может работать с ошибками, например, игнорировать или возвращать их вызывателю). Но при этом в неё можно передать и строку (алгоритм не потеряет смысл). В этом случае ошибки быть не может, но success path алгоритма останется прежним.

          А если бы str не реализовывал TryFromStr, то нельзя было бы его передать. Нужно было бы городить отдельную функцию, которая бы принимала типы, которые можно преобразовать из строки всегда без ошибок.

          Зачем?


        1. sokoloid
          20.02.2026 08:59

          Откройте реализацию From/TryFrom и посмотрите внимательней. Для всех имплементаций From, автоматически через blanket реализуется и TryFrom. TryFrom требует указания ассоциированного типа type Error. И вот как раз для blanket имплементаций там будет type Error = Infallible; Объяснять зачем так?


  1. Format-X22
    20.02.2026 08:59

    Never тип есть в TypeScript. Получается не нестандартный.

    matches! действительно сокращает количество строк кода. Но, честно говоря, засилие везде макросов это боль, потому что попробуй разобраться как он работает, а там внутри может быть лютое мясо. Ну и IDE в шоке бывает. Лучше на уровне языка чтобы это добавили. Про отладку как в той песне «а отладка даёт представленье о вечности», только там про С было, но тут пожестче может быть.


  1. AndrewChe
    20.02.2026 08:59

    Видел использование black_box в бэнчмарках, без них оптимизатор всё съедает при компиляции.


  1. lesha108
    20.02.2026 08:59

    black_box наверное полезен для работы с железом? прерывания всякие, DMA


    1. andreymal
      20.02.2026 08:59

      Документация black_box строго запрещает так делать


  1. kamaz1
    20.02.2026 08:59

    Мало пишу на расте и matches не пользуюсь, наверно полезен. Но пример из статьи вполне себе переписывается в функциональном стиле без matches

    let a:Vec<Option<u32>> = vec![Some(1),Some(11),None];
    let count = a.iter()
        .filter_map(|x| *x)  
        .filter(|x| *x > 10)
        .count();



    1. sokoloid
      20.02.2026 08:59

      Конкретно тут да, тоже обратил внимание, но во многих местах действительно удобен, вот пример из реального кода:

      if !matches!(buf.len(), 17 | 33 | 16 | 32) {
          return Err(InvalidBufLength("..."));
      }

      Clippy даже подсказывает о местах где лучше применить matches!() вместо match.


      1. blackyblack
        20.02.2026 08:59

        Так это без макросов нормально делается:

        if ![17, 33, 16, 32].contains(&buf.len()) {


        1. sokoloid
          20.02.2026 08:59

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

          Еще, о чем не сказано в статье. При использовании крейта появляется еще assert_matches!, позволяет более компактно тесты писать.


          1. blackyblack
            20.02.2026 08:59

            О, прикольно. У меня везде assert!(matches!) в тестах. Можно везде упростить.


  1. oeditus
    20.02.2026 08:59

    Конечно, у matches! есть нюанс, он не позволяет захватывать значения из паттерна. То есть matches!(opt, Some(x)) скажет нам «да/нет», но не даст саму x. Если нужен x,то всё равно придётся использовать if let Some(x) = opt, или match.

    Просто никогда, ни при каких обстоятельствах, не нужно делать два прохода (filtercount) когда можно обойтись одним (fold).

    Тогда и значение можно запросто положить в аккумулятор, если надо.


  1. pentagra
    20.02.2026 08:59

    Толи из-за того, что я не знаю Rust (от слова совсем), толи из-за подачи материала, но прочитав про тип "никогда", сложилось впечатление, что это надо записать в раздел "как выстрелить себе в ногу", а не в "вам понравится"


  1. P40b0s
    20.02.2026 08:59

    Ещё удобно при использовании '?' в Result сначало написать inspect_err() тогда ошибка залогируется на этой строке и её проще найти, а то бывает что '? ' пробрасывает ошибку аж в main и бывает тяжело понять откуда она идет)


  1. kitako4
    20.02.2026 08:59

    .