Содержание

Что изменилось и в какой версии

Фича Версия
Primary constructors (классы) C# 12
Collection expressions C# 12
ref readonly параметры C# 12
params коллекции C# 13
lock объект (System.Threading.Lock) C# 13
field keyword C# 14
Extension members C# 14
null-conditional assignment (??=) улучшения C# 14

C# 14 поставляется с .NET 10 (ноябрь 2025). C# 12 — с .NET 8, C# 13 — с .NET 9. Если проект ещё на .NET Framework — сначала миграция на .NET 10.

Primary Constructors (C# 12)

До C# 12 в каждом классе был шаблонный boilerplate: поле, конструктор, присвоение.

// Было
public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
    {
        _repo = repo;
        _logger = logger;
    }
}

// Стало (C# 12)
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
    public async Task<Order?> GetAsync(int id)
    {
        logger.LogInformation("Fetching order {Id}", id);
        return await repo.GetByIdAsync(id);
    }
}

Параметры primary constructor доступны во всём теле класса как captured variables. Важно: это не поля, а захваченные переменные — их нет в this.

Когда использовать: сервисы с DI, репозитории, любой класс где конструктор только присваивает зависимости.

Когда не использовать: если нужна логика в конструкторе — проверки, трансформации — лучше оставить явный конструктор.

Collection Expressions (C# 12)

Единый синтаксис для любых коллекций:

// Массив
int[] nums = [1, 2, 3, 4, 5];

// List<T>
List<string> names = ["Alice", "Bob", "Charlie"];

// Spread оператор (..) — объединение коллекций
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] all = [..first, ..second]; // [1, 2, 3, 4, 5, 6]

// В параметрах метода
ProcessItems(["item1", "item2"]);

// Пустая коллекция
IReadOnlyList<int> empty = [];

Компилятор выбирает оптимальный способ создания коллекции. [] для пустых коллекций эффективнее new List<T>() или Array.Empty<T>().

Records (C# 9+, улучшения в C# 12–14)

Records — неизменяемые типы-значения для данных. В .NET стеке заменяют DTO-классы:

// Record class (reference type)
public record CreateOrderRequest(
    int UserId,
    List<OrderItem> Items,
    string? PromoCode
);

// Record struct (value type, стек-аллокация)
public readonly record struct Money(decimal Amount, string Currency);

// With-expression для изменения
var original = new Money(100m, "RUB");
var doubled = original with { Amount = 200m };

// Деструктуризация
var (amount, currency) = original;

Когда использовать records:

  • Request/Response DTO в API
  • Value objects в доменной модели (Money, Email, Address)
  • Конфигурационные объекты

Не используй records для: сущностей EF Core (проблемы с отслеживанием изменений), объектов с изменяемым состоянием.

Pattern Matching — полный арсенал

// Switch expression с паттернами (C# 8-12)
public decimal CalcDiscount(Customer customer) => customer switch
{
    { IsVip: true, TotalOrders: > 100 } => 0.20m,
    { IsVip: true }                      => 0.10m,
    { TotalOrders: > 50 }               => 0.05m,
    _                                    => 0m
};

// List patterns (C# 11)
public string Describe(int[] arr) => arr switch
{
    []          => "empty",
    [var x]     => $"one element: {x}",
    [var x, var y] => $"two elements: {x}, {y}",
    [var first, .., var last] => $"starts {first}, ends {last}"
};

// Type patterns с when
public string Process(object obj) => obj switch
{
    string s when s.Length > 100 => "long string",
    string s                      => $"string: {s}",
    int n when n < 0              => "negative",
    int n                         => $"positive: {n}",
    null                          => "null",
    _                             => "unknown"
};

params коллекции (C# 13)

Раньше params работал только с массивами. Теперь с любой коллекцией:

// C# 12 и раньше — только массив
public void Log(string message, params object[] args) { ... }

// C# 13 — любая коллекция
public void Log(string message, params IEnumerable<object> args) { ... }
public void AddTags(params ReadOnlySpan<string> tags) { ... } // без heap-аллокации

// Вызов без изменений
Log("User {0} logged in", userId);
AddTags("backend", "api", "v2");

params ReadOnlySpan<T> особенно полезен для hot paths — не аллоцирует память в куче.

field keyword (C# 14)

Позволяет добавить логику в auto-property без объявления явного backing field:

// Было — нужно объявлять поле вручную
private string _name = string.Empty;
public string Name
{
    get => _name;
    set => _name = value?.Trim() ?? string.Empty;
}

// Стало (C# 14)
public string Name
{
    get;
    set => field = value?.Trim() ?? string.Empty;
}

// С валидацией
public int Age
{
    get;
    set
    {
        if (value < 0 || value > 150)
            throw new ArgumentOutOfRangeException(nameof(Age));
        field = value;
    }
}

Extension Members (C# 14)

Самая крупная фича C# 14. Позволяет добавлять не только методы, но и свойства, индексаторы к существующим типам:

// Было: только методы расширения
public static class StringExtensions
{
    public static bool IsValidEmail(this string s) =>
        s.Contains('@') && s.Contains('.');
}

// Стало: extension блок с методами И свойствами (C# 14)
extension(string s)
{
    // Свойство расширения
    public bool IsValidEmail => s.Contains('@') && s.Contains('.');
    public bool IsEmpty => string.IsNullOrWhiteSpace(s);

    // Метод расширения
    public string Truncate(int maxLength) =>
        s.Length <= maxLength ? s : s[..maxLength] + "...";
}

// Использование
string email = "[email protected]";
if (email.IsValidEmail)
{
    Console.WriteLine(email.Truncate(20));
}

Extension для интерфейсов — удобно для обогащения без наследования:

extension(IEnumerable<Order> orders)
{
    public decimal TotalRevenue => orders.Sum(o => o.Amount);
    public IEnumerable<Order> ForUser(int userId) =>
        orders.Where(o => o.UserId == userId);
}

// Использование
decimal revenue = dbOrders.TotalRevenue;
var myOrders = dbOrders.ForUser(currentUserId);

Null Safety — новые возможности

// Null-conditional assignment (C# 14)
// Присваивает только если левая часть не null
user?.Name = "Alice"; // присвоит только если user != null

// Compound null-coalescing
config.Timeout ??= TimeSpan.FromSeconds(30); // уже был в C# 8
config.BaseUrl ??= "https://api.example.com";

// Required members (C# 11) — гарантия инициализации
public class UserDto
{
    public required string Email { get; init; }
    public required string Name { get; init; }
    public string? Phone { get; init; } // опционально
}

// Компилятор требует инициализации required-свойств
var dto = new UserDto { Email = "[email protected]", Name = "Alice" }; // OK
var dto2 = new UserDto { Email = "[email protected]" }; // ОШИБКА компиляции

Raw String Literals (C# 11)

// Раньше — экранирование кошмар
string json = "{\"name\": \"Alice\", \"age\": 30}";
string path = "C:\\Users\\Alice\\Documents";

// Теперь — raw string literals
string json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

// С интерполяцией (двойные $$ чтобы экранировать {})
string name = "Alice";

string template = $$"""
    {
        "name": "{{name}}",
        "timestamp": "{{DateTime.UtcNow:O}}"
    }
    """;

Особенно удобно для SQL, JSON-шаблонов, regex.

Когда применять в реальных проектах

Сразу: primary constructors (уменьшают boilerplate), collection expressions (читаемость), raw string literals (SQL/JSON).

По ситуации: records для DTO и value objects, pattern matching вместо длинных if-else цепочек.

C# 14 (только .NET 10): field keyword — когда нужна логика в property без полного поля. Extension members — для обогащения библиотечных типов.

💡 Используй анализатор dotnet-upgrade-assistant и Roslyn analyzers — они подскажут места где можно применить новые паттерны автоматически. Массовый рефакторинг под новый синтаксис удобно отдать Claude Code со скиллом /csharp-modernize.

Поделиться: