
Привет, Хабр! В 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% при первом пополнении.

