Enum, record, tuple, деконструкция

↑ 1.3 C♯: основы языка · ← Предыдущая · → Следующая

Обязательно~6 мин чтенияУровень: junior

Зачем это на собесе

Эти четыре конструкции постоянно встречаются в современном C#-коде: enum для наборов значений, record для DTO и value object, tuple для возврата нескольких значений, деконструкция для разбора. Спросят про [Flags], отличие record от class, with-выражение и про то, когда кортеж плохая идея.

Объяснение

Enum

Именованный набор целочисленных констант (по умолчанию int, значения с 0).

public enum AppointmentStatus { Booked = 1, Arrived = 2, Cancelled = 3 }
 
var s = AppointmentStatus.Booked;
Enum.TryParse<AppointmentStatus>("Arrived", ignoreCase: true, out var parsed);
Enum.IsDefined(typeof(AppointmentStatus), 99);   // false
  • Базовый тип можно задать: enum Level : byte { ... }.
  • [Flags] — набор битовых флагов, значения степени двойки; комбинируются |, проверяются HasFlag или &.
[Flags]
public enum Permissions { None = 0, Read = 1, Write = 2, Delete = 4, All = Read | Write | Delete }
 
var p = Permissions.Read | Permissions.Write;
bool canWrite = p.HasFlag(Permissions.Write);   // true
  • Приведение (AppointmentStatus)42 компилируется и не бросает исключение, даже если такого значения нет — проверяйте Enum.IsDefined на входе из внешнего мира.
  • В API числовые значения enum — часть контракта: не меняйте и не переупорядочивайте существующие значения. В JSON удобнее строки (JsonStringEnumConverter).

Record

Ссылочный тип (record / record class) или значимый (record struct) с равенством по значению и удобными возможностями «из коробки».

public sealed record PatientDto(Guid Id, string LastName, DateOnly BirthDate);
 
var a = new PatientDto(id, "Иванов", new DateOnly(1990, 5, 1));
var b = a with { LastName = "Петров" };     // неразрушающая копия
Console.WriteLine(a == new PatientDto(id, "Иванов", new DateOnly(1990, 5, 1)));  // true, сравнение по значению
Console.WriteLine(a);                       // PatientDto { Id = ..., LastName = Иванов, ... }

Что генерирует компилятор: конструктор, свойства (для позиционных — init), Equals/GetHashCode, ==/!=, ToString, Deconstruct, метод Clone для with.

  • Позиционный синтаксис record R(int A, string B) даёт неизменяемые свойства; обычный синтаксис record R { public int A { get; init; } } — тоже.
  • Наследование возможно только между record: record Student(...) : Person(...).
  • record struct (C# 10): значимый тип; readonly record struct — иммутабельный (рекомендуется для value object).
  • Подходит: DTO, сообщения, value object, ключи словарей, результаты. Не подходит: сущности EF Core с изменяемым состоянием (там равенство по Id, а не по значениям).

Tuple

(int Count, decimal Sum) Aggregate(IEnumerable<Item> items) =>
    (items.Count(), items.Sum(i => i.Price));
 
var (count, sum) = Aggregate(items);          // деконструкция
var t = Aggregate(items);
Console.WriteLine($"{t.Count}: {t.Sum}");     // именованные элементы
  • ValueTuple — структура, элементы именованные только на этапе компиляции. Дёшево и без аллокаций.
  • System.Tuple — старый ссылочный тип, элементы Item1, Item2; в новом коде не нужен.
  • Сравнение кортежей по значению: (1, "a") == (1, "a").

Деконструкция

var (id, name) = patient;                     // требует метод Deconstruct или позиционный record
public void Deconstruct(out Guid id, out string name) { id = Id; name = Name; }
 
var (x, _) = point;                           // discard — ненужный элемент
foreach (var (key, value) in dictionary) { }  // KeyValuePair деконструируется
(a, b) = (b, a);                              // обмен значений без временной переменной

Нюансы и подводные камни

  • Enum без значения 0: default(MyEnum) равен 0 и будет «неопределённым» значением. Добавляйте None/Unknown = 0 или начинайте с осмысленного значения.
  • [Flags] без степеней двойки даст неожиданные комбинации.
  • Enum.ToString() и Enum.Parse используют рефлексию и медленнее ручного switch; на горячем пути — switch или генерация.
  • record с изменяемыми свойствами ({ get; set; }) ломает предположение об иммутабельности: with копирует, но оригинал можно изменить.
  • Равенство record для полей-коллекций — по ссылке, а не по содержимому: List<int> в record сравнивается как ссылка.
  • Кортежи хороши для локальных возвратов; в публичном API лучше именованный тип: у кортежа нет собственного смысла, его нельзя расширить и сложнее версионировать.
  • with работает только с record и struct (с C# 10), у обычного класса его нет.

Примеры для практики

  • Сделать readonly record struct Money(decimal Amount, string Currency) с оператором сложения, запретить сложение разных валют.
  • Enum [Flags] прав доступа с методами Grant и Revoke.
  • Метод, возвращающий (bool Success, string? Error), и его замена на Result-тип.

Вопросы с ответами

Связанные темы