.NET Tutorial
Junior6 мин чтенияВопрос 15 из 17

Enum, кортежи, деконструкция и record — когда что использовать?

Enum

C#
public enum OrderStatus
{
    New = 0,
    Paid = 1,
    Shipped = 2,
    Delivered = 3,
    Cancelled = 10            // значения можно задавать явно
}

var status = OrderStatus.Paid;
if (status == OrderStatus.Paid) Ship();

string name = status.ToString();                         // "Paid"
int code = (int)status;                                  // 1
var parsed = Enum.Parse<OrderStatus>("Shipped");         // из строки
bool ok = Enum.TryParse<OrderStatus>("paid", ignoreCase: true, out var s2);
OrderStatus[] all = Enum.GetValues<OrderStatus>();
switch по enum
string Label(OrderStatus s) => s switch
{
    OrderStatus.New => "Новый",
    OrderStatus.Paid => "Оплачен",
    OrderStatus.Shipped => "Отправлен",
    OrderStatus.Delivered => "Доставлен",
    OrderStatus.Cancelled => "Отменён",
    _ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
};

Flags

C#
[Flags]
public enum Permissions
{
    None   = 0,
    Read   = 1,        // 0001
    Write  = 2,        // 0010
    Delete = 4,        // 0100
    Admin  = 8,        // 1000
    Editor = Read | Write          // комбинации можно объявлять заранее
}

var p = Permissions.Read | Permissions.Write;     // 0011
bool canWrite = p.HasFlag(Permissions.Write);     // true
bool canDelete = (p & Permissions.Delete) != 0;   // false — побитовая проверка
p |= Permissions.Delete;                          // добавить право
p &= ~Permissions.Write;                          // убрать право
Console.WriteLine(p);                             // "Read, Delete" — благодаря [Flags]

да

Read = 0001

Read OR Write = 0011

Write = 0010

0011 AND Delete (0100)
= 0000?

права Delete нет

да

Read = 0001

Read OR Write = 0011

Write = 0010

0011 AND Delete (0100)
= 0000?

права Delete нет

Флаги как биты одного числа

Кортежи

C#
(int Min, int Max) Range(int[] values) => (values.Min(), values.Max());

var r = Range([5, 1, 9]);
Console.WriteLine($"{r.Min}..{r.Max}");     // 1..9

var (min, max) = Range([5, 1, 9]);          // деконструкция
var (_, onlyMax) = Range([5, 1, 9]);        // discard ненужного

(a, b) = (b, a);                            // обмен значений без временной переменной

var dict = new Dictionary<(int Year, int Month), decimal>();   // составной ключ
dict[(2025, 9)] = 1_500_000m;

ValueTuple — значимый тип с полями Item1, Item2; имена (Min, Max) существуют только для компилятора. Кортежи сравниваются по значению (== с C# 7.3).

Анонимные типы

C#
var summary = orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new { CustomerId = g.Key, Count = g.Count(), Total = g.Sum(o => o.Total) })
    .OrderByDescending(x => x.Total)
    .ToList();

Console.WriteLine(summary[0].Total);     // свойства только для чтения, Equals по значению

Анонимный тип — класс, сгенерированный компилятором, с неизменяемыми свойствами. Используется в пределах метода; вернуть его можно только как object или dynamic — тогда лучше объявить record.

Record

C#
public record ProductDto(Guid Id, string Name, decimal Price);    // позиционный record

var a = new ProductDto(id, "Чайник", 420_000m);
var b = a with { Price = 399_000m };       // копия с изменённым свойством
Console.WriteLine(a == b);                 // False — сравнение по значениям
Console.WriteLine(a);                      // ProductDto { Id = ..., Name = Чайник, Price = 420000 }
var (pid, pname, _) = a;                   // деконструкция

Подробнее о record — в разделе «C# и .NET углублённо».

Что выбрать

да

нет

да

нет

да

нет

Набор фиксированных
именованных вариантов?

enum
(Flags — если комбинируются)

Данные живут только
внутри одного метода (LINQ)?

Анонимный тип

Это временная пара значений
между 1–2 методами?

Кортеж (int Min, int Max)

record / record struct
(DTO, Value Object)

да

нет

да

нет

да

нет

Набор фиксированных
именованных вариантов?

enum
(Flags — если комбинируются)

Данные живут только
внутри одного метода (LINQ)?

Анонимный тип

Это временная пара значений
между 1–2 методами?

Кортеж (int Min, int Max)

record / record struct
(DTO, Value Object)

Выбор способа сгруппировать данные
enum Кортеж Анонимный тип record
Назначение Набор констант Несколько значений Временная проекция Модель данных
Можно вернуть из метода Да Да Нет (без потери типа) Да
Равенство По значению По значению По значению По значению
Изменяемость — Изменяемые поля Неизменяемый Неизменяемый (позиционный)
Тип Значимый Значимый Ссылочный Ссылочный (record struct — значимый)

Вопросы на засыпку

Как хранить enum в базе данных — числом или строкой?

Число компактнее и быстрее, но при изменении порядка или удалении значений смысл старых записей меняется, а в БД непонятно, что значит 3. Строка читаема и устойчива к перестановке, но больше места и опасна при переименовании. В EF Core: HasConversion<string>(). Если числом — задавайте значения явно и никогда их не меняйте.

Почему у Flags-значений степени двойки?

Каждое значение должно занимать отдельный бит, чтобы комбинации не пересекались: 1, 2, 4, 8… Если бы Write = 3, комбинация Read | Write была бы неотличима от одного Write.

Чем Tuple отличается от ValueTuple?

System.Tuple<T1, T2> (старый) — ссылочный, неизменяемый, свойства только Item1, Item2. ValueTuple (синтаксис (int, string)) — значимый, с изменяемыми полями, поддерживает имена элементов и деконструкцию. В современном коде используют ValueTuple.

Когда кортеж — плохая идея?

Когда он попадает в публичный API или живёт долго: (string, string, int) ничего не говорит читателю, имена элементов теряются при сериализации и рефлексии, нельзя добавить проверку или метод. Тогда объявите record с понятным именем.

Итог

enum заменяет магические числа именованными константами (с [Flags] — комбинируемыми битами), кортежи удобны для возврата нескольких значений без отдельного класса, анонимные типы — для проекций внутри метода, а record — для полноценных моделей данных с равенством по значению. Выбирайте по тому, насколько далеко и долго данные путешествуют по коду.