алгебраические типы данныхalgebraic data types

Structs & Enums

struct — тип-произведение (record с именованными или позиционными полями). enum в Rust — настоящий тип-сумма ("tagged union"): у каждого варианта может быть собственный, отдельный набор данных. Это фундаментально мощнее, чем C-style перечисление или C++ enum class, которые лишь именованные целочисленные константы.struct is a product type (a record with named or positional fields). enum in Rust is a genuine sum type (a "tagged union"): each variant can carry its own distinct payload of data. This is fundamentally more powerful than a C-style enum or a C++ enum class, which are only named integer constants.

основа языкаcore of the language частый вопрос: чем Rust enum отличается от C++ enum classcommon question: how does a Rust enum differ from a C++ enum class

Что этоWhat it is

struct определяет тип-произведение (product type) — запись с именованными полями, кортежная структура с позиционными полями, или unit-структура вообще без полей — концептуально то же самое, что структура/класс в C++ только с полями данных. enum в Rust — настоящий тип-сумма (tagged union): каждый вариант может опционально нести собственные, отличные от других вариантов данные, например enum Shape { Circle(f64), Rectangle { width: f64, height: f64 }, Point }. Это фундаментально мощнее, чем C-style перечисление (или C++11 enum class), которое — лишь именованная целочисленная константа без возможности нести данные для конкретного варианта.struct defines a product type — a record with named fields, a tuple struct with positional fields, or a unit struct with no fields at all — conceptually identical to a C++ struct/class with only data members. enum in Rust is a genuine sum type (a tagged union): each variant can optionally carry its own distinct payload of data, e.g. enum Shape { Circle(f64), Rectangle { width: f64, height: f64 }, Point }. This is fundamentally more powerful than a C-style enum (or a C++11 enum class), which is only a named integer constant with no ability to carry per-variant data.

Зачем ввелиWhy it was introduced

C-style перечисления упирались в жёсткий потолок выразительности:C-style enums ran into a hard ceiling of expressiveness:

01

C-style enum — только "один из N именованных вариантов", без данныхA C-style enum is only "one of N named states", with no data

C-style перечисления (и C++ enum/enum class) могут выразить только "одно из N именованных константных состояний". Как только варианту нужны ассоциированные данные (HTTP-ответ — либо Ok(Response), либо Error(ErrorCode, String)), C/C++-код исторически прибегал к структуре с полем-дискриминантом плюс union.C-style enums (and C++'s enum/enum class) can only express "one of N named constant states". The moment a variant needs associated data (an HTTP response is either Ok(Response) or Error(ErrorCode, String)), C/C++ code has historically resorted to a struct with a discriminant enum field plus a union.

02

Современный C++ пришёл к std::variant десятилетиями позжеModern C++ arrived at std::variant decades later

В современном C++ для этого есть std::variant, появившийся только в C++17 — спустя десятилетия после того, как языки-предшественники Rust вроде ML/Haskell получили это нативно, и Rust принёс эту идею в мейнстримный системный язык с версии 1.0.Modern C++ has std::variant for this, arriving only in C++17 — decades after Rust-predecessor languages like ML/Haskell had this natively, and Rust brought the idea to a mainstream systems language from 1.0.

03

enum делает "один из нескольких вариантов с данными" полноценной конструкциейenum makes "one of several alternatives with data" a first-class construct

Rust-овский enum делает "один из нескольких вариантов, возможно каждый со своими данными" полноценной, сопоставимой по паттернам конструкцией языка, а не обходным путём поверх union и дискриминанта.Rust's enum makes "one of several alternatives, each possibly with its own data" a first-class, pattern-matchable construct, instead of a workaround built on a union and a discriminant.

Как это работает под капотомHow it works under the hood

Три формы struct — обычная (именованные поля), кортежная (позиционные поля) и unit (без полей):The three struct forms — regular (named fields), tuple (positional fields), and unit (no fields):

struct Point2D { x: f64, y: f64 }   // обычная структура// regular struct
struct Point(f64, f64);              // кортежная структура// tuple struct
struct Marker;                       // unit-структура, без полей// unit struct, no fields

enum как настоящий sum-тип — у каждого варианта свои данные:enum as a genuine sum type — each variant carries its own data:

enum Shape {
    Circle(f64),                          // радиус// radius
    Rectangle { width: f64, height: f64 },
    Point,                               // вариант без данных// a variant with no data
}

Option<T> и Result<T, E> (см. error-handling.html) — сами по себе обычные enum'ы из стандартной библиотеки, а не какая-то компиляторная магия. Вот фактическое определение Option:Option<T> and Result<T, E> (see error-handling.html) are themselves just ordinary enums defined in the standard library, not special compiler magic. Here's Option's actual definition:

enum Option<T> {
    Some(T),
    None,
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Протокол сетевых сообщений как enum с данными на вариантA network message protocol as an enum with per-variant payloads

Каждый вариант несёт ровно те данные, которые ему нужны, и обрабатывается исчерпывающе через match (см. pattern-matching.html) — компилятор проверит, что все варианты обработаны.Each variant carries exactly the data it needs, and is processed exhaustively via match (see pattern-matching.html) — the compiler verifies every variant is handled.

enum Message {
    Connect { user_id: u64 },
    Disconnect,
    Data(Vec<u8>),
}

fn handle(msg: Message) {
    match msg {
        Message::Connect { user_id } => println!("connect {}", user_id),
        Message::Disconnect => println!("disconnect"),
        Message::Data(bytes) => println!("{} bytes", bytes.len()),
    }
}
02

Кортежная структура как newtype для типобезопасностиA tuple struct as a newtype for type safety

struct UserId(u64); предотвращает случайное смешивание UserId с несвязанным u64 в вызовах функций — целиком на этапе компиляции, без рантайм-цены.struct UserId(u64); prevents accidentally mixing a UserId with an unrelated u64 in function calls — entirely at compile time, at zero runtime cost.

struct UserId(u64);
struct ProductId(u64);

fn get_user(id: UserId) { /* ... */ }

let uid = UserId(42);
let pid = ProductId(42);
get_user(uid);      // OK// OK
// get_user(pid);  // ОШИБКА: ожидался UserId, передан ProductId// get_user(pid);  // ERROR: expected UserId, found ProductId
03

#[derive(...)] — генерация реализации трейта компилятором, не рефлексия#[derive(...)] — a compiler-generated trait impl, not reflection

#[derive(Debug, Clone, PartialEq)] генерирует реализацию трейта на этапе компиляции — это derive-макрос (подробнее в macros.html), а не рантайм-рефлексия, как в языках с отражением типов.#[derive(Debug, Clone, PartialEq)] generates a trait implementation at compile time — this is a derive macro (covered in depth in macros.html), not runtime reflection as in languages with type introspection.

#[derive(Debug, Clone, PartialEq)]
struct Config { name: String, retries: u32 }

let a = Config { name: String::from("svc"), retries: 3 };
let b = a.clone();
println!("{} {:?}", a == b, a);  // == и {:?} работают благодаря derive// == and {:?} work thanks to derive
04

Rust enum против C++ struct+union — исторический workaround наглядноRust enum vs. C++ struct+union — the historical workaround, made concrete

Rust-овский enum сразу даёт то, что C++ до variant собирал вручную из дискриминанта и union — с риском забыть проверить дискриминант перед доступом к нужному полю union.A Rust enum gives directly what pre-variant C++ assembled by hand from a discriminant and a union — with the risk of forgetting to check the discriminant before accessing the right union field.

// C++ до variant: дискриминант + union, легко забыть проверить тег// C++ pre-variant: discriminant + union, easy to forget to check the tag
// enum class Tag { Circle, Rectangle }; struct Shape { Tag tag; union { double radius; struct { double w, h; }; }; };

enum Shape {                     // Rust: тег и данные — одна проверяемая конструкция// Rust: tag and data are one checked construct
    Circle(f64),
    Rectangle { width: f64, height: f64 },
}

ГраблиGotchas

Размер enum определяется его крупнейшим вариантомAn enum's size is determined by its largest variant

Размер enum в памяти равен размеру крупнейшего варианта плюс тег-дискриминант — enum с одним огромным, редко используемым вариантом и несколькими мелкими частыми платит за размер крупнейшего варианта везде, где хранится значение этого типа. Стандартное решение — обернуть крупный вариант в Box.An enum's in-memory size equals the size of its largest variant plus a discriminant tag — an enum with one huge, rarely-used variant and several small common ones pays the size cost of the largest variant everywhere a value of that type is stored. The standard fix is to Box the large variant's payload.

Newtype и его внутренний тип — совершенно разные типы для проверки типовA newtype and its inner type are entirely different types to the type checker

UserId(u64) и u64 не конвертируются друг в друга неявно ни в одну сторону — любое преобразование требует явного доступа к полю .0 либо реализации From/Into. Это и есть весь смысл (типобезопасность), но удивляет тех, кто ждёт неявного числоподобного поведения.UserId(u64) and u64 do not implicitly convert in either direction — any conversion must go through explicit .0 field access or a From/Into implementation. That's the whole point (type safety), but it surprises people expecting implicit numeric-like behavior.

derive требует, чтобы КАЖДОЕ поле тоже реализовывало трейтderive requires every field to also implement the trait

Деривация PartialEq/Eq/Hash и подобных требует, чтобы тип каждого поля тоже реализовывал этот трейт — деривация не собирается (ошибка компиляции, а не молчаливый пропуск), если хоть одно поле не подходит. Частая ранняя путаница: структура с полем f64 и попытка вывести Eq — floats реализуют только PartialEq, но не Eq, из-за NaN.Deriving PartialEq/Eq/Hash and similar requires every field's type to also implement that trait — deriving fails to compile (not silently skipped) if any field doesn't qualify. A common early confusion: a struct with an f64 field and an attempt to derive Eq — floats only implement PartialEq, not Eq, because of NaN.