алгебраические типы данныхalgebraic data types
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.
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.
C-style перечисления упирались в жёсткий потолок выразительности:C-style enums ran into a hard ceiling of expressiveness:
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.
В современном 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.
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.
Три формы 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,
}
Каждый вариант несёт ровно те данные, которые ему нужны, и обрабатывается исчерпывающе через 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()),
}
}
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
#[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
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 },
}
Размер 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.
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.
Деривация 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.