Что такое record и pattern matching в современных версиях C#?

Введение: 

С выходом C# 9 и последующих версий произошла тихая революция в языке, которая кардинально изменила подход к работе с данными и логикой. Две ключевые возможности — records и pattern matching — превратили C# из просто объектно-ориентированного языка в язык, где функциональное программирование стало first-class citizen.

Почему эти функции изменили правила игры?

  • Records устраняют до 80% шаблонного кода для data-классов

  • Pattern matching делает сложные условия читаемыми и поддерживаемыми

  • Иммутабельность по умолчанию предотвращает целый класс ошибок

  • Выразительность кода приближается к F# при сохранении мощи .NET

Что вы получите, освоив эти инструменты?

  • Код, который выглядит как описание бизнес-логики, а не технических деталей

  • В 3 раза меньше строк кода в моделях и сервисах

  • Автоматическую проверку на полноту обработки всех случаев

  • Производительность на уровне низкоуровневых оптимизаций


1. Records: неизменяемые данные с минимальным кодом

1.1. Базовый record

public record Person(string FirstName, string LastName);

1.2. Использование

var person = new Person(«John», «Doe»);
var (firstName, lastName) = person;

1.3. Наследование records

public record Employee(string FirstName, string LastName, string Department)
: Person(FirstName, LastName);

2. Pattern Matching: элегантные условия

2.1. Простой pattern matching

public string GetDescription(object obj) => obj switch
{
Person p => $»Person: {p.FirstName} {p.LastName}»,
Employee e => $»Employee: {e.Department}»,
_ => «Unknown»
};

2.2. Property patterns

public string CheckEmployee(Employee emp) => emp switch
{
{ Department: «IT», FirstName: «John» } => «IT John»,
{ Department: «HR» } => «HR Employee»,
_ => «Other»
};

2.3. Рекурсивные patterns

public decimal CalculateTax(Income income) => income switch
{
{ Amount: < 1000 } => 0,
{ Amount: >= 1000 and < 5000 } => income.Amount * 0.1m,
{ Amount: >= 5000 } => income.Amount * 0.2m
};

3. Комбинация records и pattern matching

3.1. Deconstruction в patterns

public string ProcessPerson(Person person) => person switch
{
(«John», «Doe») => «Known person»,
(var first, var last) => $»New person: {first} {last}»
};

3.2. Сложные сценарии

public record Order(Product Product, int Quantity);
public record Product(string Name, decimal Price);

public decimal CalculateDiscount(Order order) => order switch
{
{ Product: { Price: > 100 }, Quantity: > 5 } => 0.2m,
{ Product.Name: «Premium» } => 0.15m,
_ => 0.1m
};

4. Продвинутые возможности

4.1. Positional records с методами

public record Vector(double X, double Y)
{
public double Magnitude => Math.Sqrt(X * X + Y * Y);
public Vector Normalize() => this with { X = X / Magnitude, Y = Y / Magnitude };
}

4.2. Recursive patterns

public bool IsValidNode(TreeNode node) => node switch
{
{ Value: null } => false,
{ Left: null, Right: null } => true,
{ Left: var left, Right: var right } => IsValidNode(left) && IsValidNode(right)
};

5. Производительность и оптимизация

5.1. Records vs классы

// Record
public record Point(int X, int Y);

// Класс с аналогичной функциональностью
public class PointClass
{
public int X { get; }
public int Y { get; }

public PointClass(int x, int y) => (X, Y) = (x, y);

public override bool Equals(object obj) => /* шаблонный код */;
public override int GetHashCode() => /* шаблонный код */;
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

5.2. Компиляторная оптимизация

public bool IsSpecialPoint(Point point) => point is (0, 0) or (1, 1) or (-1, -1);

6. Практические примеры использования в реальных проектах

6.1. Обработка API-ответов с pattern matching

public async Task<ActionResult> HandleResponse(HttpResponseMessage response)
{
return response switch
{
{ StatusCode: HttpStatusCode.OK } content =>
Ok(await content.Content.ReadFromJsonAsync<Order>()),
{ StatusCode: HttpStatusCode.NotFound } => NotFound(),
{ StatusCode: HttpStatusCode.Unauthorized } => Unauthorized(),
_ => StatusCode((int)response.StatusCode)
};
}

6.2. Модели данных с records для DTO

public record ApiResponse<T>(T Data, bool Success, string ErrorMessage = «»);
public record UserDto(Guid Id, string Email, DateTime CreatedAt);
public record CreateUserRequest(string Email, string Password);

6.3. Валидация входящих запросов

public ValidationResult ValidateRequest(CreateUserRequest request) => request switch
{
{ Email: null or «» } => ValidationResult.Invalid(«Email required»),
{ Password: null or «» } => ValidationResult.Invalid(«Password required»),
{ Password.Length: < 8 } => ValidationResult.Invalid(«Password too short»),
{ Email: var email } when !email.Contains(‘@’) => ValidationResult.Invalid(«Invalid email»),
_ => ValidationResult.Valid()
};

7. Работа с коллекциями и LINQ

7.1. Фильтрация с property patterns

var highValueOrders = orders
.Where(order => order is { Amount: > 1000, Status: OrderStatus.Completed })
.ToList();

7.2. Преобразование данных

var userSummaries = users
.Select(user => user switch
{
{ Age: >= 18 } adult => new AdultSummary(adult.Name, adult.Income),
{ Age: < 18 } minor => new MinorSummary(minor.Name, minor.Grade),
_ => throw new InvalidOperationException()
});

8. Обработка ошибок и исключения

8.1. Pattern matching в блоках catch

try
{
await ProcessOrderAsync(order);
}
catch (Exception ex) when (ex is ApiException { StatusCode: 404 })
{
return HandleNotFound();
}
catch (ApiException { StatusCode: 500 } apiEx)
{
return HandleServerError(apiEx);
}

8.2. Result pattern для обработки ошибок

public Result<Order, Error> ProcessOrder(Order order) => order switch
{
{ Items: [] } => Error.EmptyOrder,
{ Customer: { IsBlocked: true } } => Error.BlockedCustomer,
{ Total: > 10000 } => Error.AmountTooLarge,
_ => order with { Status = OrderStatus.Processing }
};

9. Оптимизация производительности

9.1. Использование sealed records

public sealed record PaymentResult(bool Success, string TransactionId);

9.2. Struct records для value semantics

public readonly record struct Point3d(double X, double Y, double Z);

9.3. Компиляторная оптимизация switch expressions

public decimal CalculateShipping(Order order) => order switch
{
{ Weight: <= 1 } => 5.0m,
{ Weight: <= 5 } => 10.0m,
{ Weight: <= 10 } => 15.0m,
{ Weight: > 10 } => 20.0m
};

10. Интеграция с другими технологиями

10.1. Entity Framework Core с records

public record OrderDetail(Guid OrderId, string ProductName, int Quantity);

modelBuilder.Entity<Order>()
.OwnsMany(o => o.Details, d =>
{
d.ToJson();
});

10.2. Serialization с System.Text.Json

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};

var json = JsonSerializer.Serialize(user, options);
var user = JsonSerializer.Deserialize<User>(json, options);


Заключение

Records и pattern matching — это не просто новые синтаксические возможности C#, а фундаментальный сдвиг в парадигме разработки. Они демонстрируют эволюцию языка в сторону функционального программирования, сохраняя при этом все преимущества объектно-ориентированного подхода.

Ключевые преимущества, которые мы рассмотрели:

  • Выразительность — код становится более читаемым и лаконичным

  • Безопасность — иммутабельность и exhaustive matching предотвращают целые классы ошибок

  • Производительность — встроенные оптимизации компилятора работают на вас

  • Поддержка — полная интеграция с экосистемой .NET и популярными библиотеками

Эти инструменты уже стали стандартом де-факто в современных .NET проектах и их освоение — обязательное требование для профессионального разработчика в 2025 году.


Для дальнейшего изучения

  1. Property Patterns в C# 10 — углубленное изучение рекурсивных паттернов

  2. Record Structs — особенности работы с value-type records

  3. Source Generators для Records — автоматическая генерация кода

  4. Pattern Matching в LINQ — комбинация с запросами

  5. Оптимизация производительности — бенчмаркинг различных подходов

  6. Миграция с классов на Records — лучшие практики рефакторинга

  7. F# Influence — изучение корней этих возможностей для deeper understanding