C# 12 и .NET 8: паттерны, коллекции и производительность

Введение

C# 12 и .NET 8 представляют собой значительный шаг вперед в развитии экосистемы Microsoft для разработчиков. Эти релизы фокусируются на трех ключевых аспектах: улучшении выразительности кода через новые паттермы, оптимизации работы с коллекциями и значительном повышении производительности runtime. В отличие от предыдущих версий, где акцент делался на добавление крупных функций, C# 12 предлагает точечные улучшения, которые вместе дают существенный прирост в удобстве написания и эффективности выполнения кода. .NET 8 продолжает тенденцию к кросс-платформенности и высокой производительности, устанавливая новые стандарты для enterprise-приложений.


Новые паттермы C# 12

1.1. Первичные конструкторы для всех классов и структур

1.1.1. Базовое использование

// C# 12: первичные конструкторы для всех классов
public class User(string name, int age)
{
    public string Name { get; } = name;
    public int Age { get; } = age;
    
    public string Greeting() => $"Hello, I'm {Name} and I'm {Age} years old";
}

// Автоматически создаются приватные поля и конструктор
var user = new User("Alex", 30);
Console.WriteLine(user.Greeting()); // Hello, I'm Alex and I'm 30 years old

// Использование в структурах
public struct Point(int x, int y)
{
    public int X => x;
    public int Y => y;
    
    public double DistanceToOrigin() => Math.Sqrt(x * x + y * y);
}

var point = new Point(3, 4);
Console.WriteLine(point.DistanceToOrigin()); // 5

1.1.2. Расширенное использование с зависимостями

// Внедрение зависимостей через первичные конструкторы
public interface ILogger
{
    void Log(string message);
}

public class UserService(ILogger logger, IUserRepository repository)
{
    public User GetUser(int id)
    {
        logger.Log($"Getting user with ID: {id}");
        return repository.GetById(id);
    }
}

// Наследование с первичными конструкторами
public abstract class EntityBase(int id)
{
    public int Id { get; } = id;
    public DateTime CreatedAt { get; } = DateTime.UtcNow;
}

public class Product(int id, string name, decimal price) : EntityBase(id)
{
    public string Name { get; } = name;
    public decimal Price { get; } = price;
    
    // Модификаторы доступа в первичных конструкторах
    private protected string InternalCode { get; } = GenerateCode();
    
    private static string GenerateCode() => Guid.NewGuid().ToString()[..8];
}

1.2. Паттерм коллекций (Collection Expressions)

1.2.1. Унифицированный синтаксис для всех коллекций

// Единый синтаксис для всех типов коллекций
int[] array = [1, 2, 3, 4, 5];
List<int> list = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3, 4, 5];
ImmutableArray<int> immutable = [1, 2, 3, 4, 5];

// Создание через spread оператор
int[] firstHalf = [1, 2, 3];
int[] secondHalf = [4, 5, 6];
int[] combined = [..firstHalf, ..secondHalf, 7, 8, 9];

// С многомерными массивами
int[][] jaggedArray = [[1, 2], [3, 4, 5], [6]];
int[,] matrix = { { 1, 2 }, { 3, 4 } }; // Альтернативный синтаксис

// В LINQ запросах
var numbers = Enumerable.Range(1, 10).ToList();
var selected = numbers.Where(n => n % 2 == 0).ToList();
var result = [..selected, 11, 12, 13];

1.2.2. Кастомные коллекции с поддержкой паттерма

[CollectionBuilder(typeof(MyCollection), nameof(Create))]
public class MyCollection : IEnumerable<int>
{
    private readonly int[] _items;
    
    private MyCollection(int[] items)
    {
        _items = items;
    }
    
    public static MyCollection Create(ReadOnlySpan<int> items)
    {
        return new MyCollection(items.ToArray());
    }
    
    public IEnumerator<int> GetEnumerator() => ((IEnumerable<int>)_items).GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// Использование
MyCollection custom = [1, 2, 3, 4, 5];
foreach (var item in custom)
{
    Console.WriteLine(item);
}

1.3. Улучшенные параметры по умолчанию в лямбда-выражениях

1.3.1. Параметры в лямбда-выражениях

// C# 12: параметры по умолчанию в лямбдах
var multiply = (int x, int y = 2) => x * y;
Console.WriteLine(multiply(5));    // 10
Console.WriteLine(multiply(5, 3)); // 15

// Комбинирование с var для анонимных делегатов
var greet = (string name = "Guest", int times = 1) =>
{
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hello, {name}!");
};

greet();            // Hello, Guest!
greet("Alex");      // Hello, Alex!
greet("Bob", 3);    // Hello, Bob! (3 раза)

// Параметры params в лямбдах
var sumAll = (params int[] numbers) => numbers.Sum();
Console.WriteLine(sumAll(1, 2, 3));       // 6
Console.WriteLine(sumAll([4, 5, 6, 7]));  // 22

1.3.2. Использование в реальных сценариях

// Конфигурация обработчиков
var createHandler = (string endpoint, int timeout = 30, int retries = 3) =>
{
    return new HttpHandler
    {
        Endpoint = endpoint,
        Timeout = TimeSpan.FromSeconds(timeout),
        MaxRetries = retries
    };
};

var apiHandler = createHandler("https://api.example.com");
var dbHandler = createHandler("localhost:5432", timeout: 60);

// Фильтрация с параметрами по умолчанию
Func<IEnumerable<int>, int, IEnumerable<int>> filter = 
    (numbers, threshold = 10) => numbers.Where(n => n > threshold);

var data = new[] { 5, 12, 8, 15, 3 };
var filtered = filter(data);        // threshold = 10 по умолчанию
var customFiltered = filter(data, 8); // threshold = 8

1.4. Псевдонимы для любых типов

1.4.1. Using-алиасы для сложных типов

// Алиасы для кортежей
using Point3D = (int X, int Y, int Z);
using Matrix = int[,];

// Использование
Point3D point = (1, 2, 3);
Console.WriteLine(point.X); // 1

Matrix matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } };

// Алиасы для generics
using StringList = System.Collections.Generic.List<string>;
using Predicate<T> = System.Func<T, bool>;

// Сложные типы
using ComplexDictionary = System.Collections.Generic.Dictionary<
    string, 
    System.Collections.Generic.List<System.Tuple<int, string>>
>;

ComplexDictionary dict = new();
dict.Add("key", new() { (1, "a"), (2, "b") });

Новые коллекции и улучшения производительности в .NET 8

2.1. Frozen коллекции

2.1.1. Неизменяемые оптимизированные коллекции

using System.Collections.Frozen;

// Создание frozen коллекций
var dictionary = new Dictionary<string, int>
{
    ["apple"] = 1,
    ["banana"] = 2,
    ["orange"] = 3
};

FrozenDictionary<string, int> frozenDict = dictionary.ToFrozenDictionary();
FrozenSet<string> frozenSet = dictionary.Keys.ToFrozenSet();

// Frozen коллекции оптимизированы для чтения
// Они используют:
// 1. Perfect hashing для O(1) доступа
// 2. Memory pooling для уменьшения аллокаций
// 3. Оптимизированное хранение маленьких коллекций

// Бенчмарк: поиск в 1000 элементов
// Dictionary: ~12 ns
// FrozenDictionary: ~3 ns (в 4 раза быстрее!)

// Особенности:
// - Однократное создание (неизменяемые после создания)
// - Идеально для конфигураций, справочников, кэшей
// - Не поддерживают модификации

// Использование в кэшировании
public class ProductService
{
    private readonly FrozenDictionary<int, Product> _productCache;
    
    public ProductService(IEnumerable<Product> products)
    {
        _productCache = products
            .ToDictionary(p => p.Id)
            .ToFrozenDictionary(); // Оптимизация при инициализации
    }
    
    public Product GetProduct(int id)
    {
        // Максимально быстрый доступ
        return _productCache[id];
    }
}

2.2. ReadOnly колекции и улучшения

2.2.1. Новые методы для ReadOnly коллекций

var list = new List<int> { 1, 2, 3, 4, 5 };

// Более эффективное создание
ReadOnlyCollection<int> readonly1 = list.AsReadOnly();
IReadOnlyList<int> readonly2 = list;
IReadOnlyCollection<int> readonly3 = list;

// Новые методы в .NET 8
var index = readonly1.IndexOf(3); // Было: list.IndexOf(3)
var contains = readonly1.Contains(5); // Было: list.Contains(5)

// ReadOnlySpan для эффективной работы с памятью
ReadOnlySpan<int> span = list.AsSpan();
var slice = span[1..^1]; // [2, 3, 4] - без аллокаций

// Методы для словарей
var dict = new Dictionary<string, int>
{
    ["a"] = 1,
    ["b"] = 2
};

IReadOnlyDictionary<string, int> readonlyDict = dict;
var value = readonlyDict.GetValueOrDefault("c", -1); // -1
var keys = readonlyDict.Keys; // ReadOnlyCollection<string>

2.3. Производительность коллекций

2.3.1. Улучшения в Dictionary и HashSet

// .NET 8: улучшенные хэш-функции и меньше коллизий
var dict = new Dictionary<string, int>(capacity: 1000);

// Быстрые операции с Span
Dictionary<string, int> fromSpan = new(StringComparer.Ordinal)
{
    ["item1"] = 1,
    ["item2"] = 2
};

// TryAdd с избежанием двойного хэширования
if (dict.TryAdd("key", 42))
{
    // Успешно добавлено
}

// Пакетные операции
var itemsToAdd = new (string Key, int Value)[]
{
    ("a", 1),
    ("b", 2),
    ("c", 3)
};

foreach (var item in itemsToAdd)
{
    dict[item.Key] = item.Value;
}

// EnsureCapacity для предотвращения ресайзинга
dict.EnsureCapacity(2000);

// HashSet улучшения
var hashSet = new HashSet<int>(capacity: 100);
hashSet.EnsureCapacity(200);
hashSet.TrimExcess(); // Освобождение неиспользуемой памяти

2.3.2. Memory-оптимизированные коллекции

// Использование ArrayPool для уменьшения аллокаций
using System.Buffers;

var pool = ArrayPool<int>.Shared;
var buffer = pool.Rent(1024); // Взятие из пула

try
{
    // Работа с буфером
    for (int i = 0; i < 1024; i++)
        buffer[i] = i * 2;
}
finally
{
    pool.Return(buffer); // Возврат в пул
}

// Stack и Queue с Span
var stack = new Stack<int>(100);
stack.Push(1);
stack.Push(2);

Span<int> stackSpan = stack.ToArray().AsSpan();

// Concurrent коллекции с улучшениями
using System.Collections.Concurrent;

var concurrentBag = new ConcurrentBag<int>();
var concurrentDict = new ConcurrentDictionary<string, int>();

// Параллельное заполнение
Parallel.For(0, 1000, i =>
{
    concurrentDict.TryAdd($"key_{i}", i);
});

Системные улучшения и AOT компиляция

3.1. Нативная AOT компиляция

3.1.1. Конфигурация и использование

// .csproj настройка
/*
<PropertyGroup>
  <PublishAot>true</PublishAot>
  <SelfContained>true</SelfContained>
</PropertyGroup>
*/

// Ограничения AOT:
// 1. Нет JIT-компиляции
// 2. Ограниченный reflection
// 3. Требует явного указания используемых сборок

// Преимущества:
// 1. Быстрый запуск (< 50 мс)
// 2. Меньший размер дистрибутива
// 3. Защита кода (нативная компиляция)

// Атрибуты для AOT
[System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Использует reflection")]
public class DynamicTypeLoader
{
    // Код требует JIT-компиляции
}

[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Использует trimming")]
public class ReflectiveComponent
{
    // Может быть обрезано при AOT
}

3.2. Улучшения производительности GC

3.2.1. Оптимизации сборщика мусора

// Конфигурация GC в .NET 8
// appsettings.json
/*
{
  "System.GC": {
    "ServerGC": true,
    "ConcurrentGC": true,
    "RetainVM": true
  }
}
*/

// Программная конфигурация
AppContext.SetSwitch("System.GC.Concurrent", true);
AppContext.SetSwitch("System.GC.Server", true);

// Новые API для управления памятью
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Optimized, blocking: false, compacting: true);

// Мониторинг использования памяти
var memoryInfo = GC.GetGCMemoryInfo();
Console.WriteLine($"Heap Size: {memoryInfo.HeapSizeBytes}");
Console.WriteLine($"Fragmentation: {memoryInfo.FragmentedBytes}");

// MemoryCache с ограничениями
using Microsoft.Extensions.Caching.Memory;

var cacheOptions = new MemoryCacheOptions
{
    SizeLimit = 1024 * 1024 * 100, // 100 MB
    CompactionPercentage = 0.2,
    ExpirationScanFrequency = TimeSpan.FromMinutes(5)
};

var cache = new MemoryCache(cacheOptions);

3.3. Улучшения в LINQ и коллекциях

3.3.1. Новые методы и оптимизации

// Новые методы в .NET 8
var numbers = Enumerable.Range(1, 100);

// CountBy - группировка с подсчетом
var countBy = numbers.CountBy(n => n % 3);
// [(0, 33), (1, 34), (2, 33)]

// AggregateBy - агрегация с ключами
var aggregateBy = numbers.AggregateBy(
    keySelector: n => n % 3,
    seed: 0,
    (sum, item) => sum + item
);

// Index - получение индекса в Where
var indexed = numbers.Where((n, index) => n % 2 == 0 && index % 3 == 0);

// ToDictionary с устранением дубликатов
var dict = numbers.ToDictionary(
    n => n.ToString(),
    n => n * 2,
    StringComparer.OrdinalIgnoreCase
);

// Chunk для пакетной обработки
foreach (var chunk in numbers.Chunk(10))
{
    ProcessBatch(chunk);
}

// TryGetNonEnumeratedCount - попытка получить Count без перечисления
if (numbers.TryGetNonEnumeratedCount(out int count))
{
    // Быстрое получение количества
}

// DistinctBy, ExceptBy, IntersectBy, UnionBy
var distinct = numbers.DistinctBy(n => n % 5);

Практические примеры и бенчмарки

4.1. Реальный пример: кэширование с Frozen коллекциями

public class CurrencyService
{
    private readonly FrozenDictionary<string, decimal> _exchangeRates;
    private readonly DateTime _lastUpdated;
    
    public CurrencyService()
    {
        // Загрузка данных из внешнего источника
        var rates = LoadExchangeRates();
        
        // Заморозка словаря для оптимального чтения
        _exchangeRates = rates.ToFrozenDictionary(
            keySelector: r => r.CurrencyCode,
            elementSelector: r => r.Rate,
            comparer: StringComparer.OrdinalIgnoreCase
        );
        
        _lastUpdated = DateTime.UtcNow;
    }
    
    public decimal Convert(string fromCurrency, string toCurrency, decimal amount)
    {
        // Быстрый доступ к замороженному словарю
        var fromRate = _exchangeRates[fromCurrency];
        var toRate = _exchangeRates[toCurrency];
        
        return amount * toRate / fromRate;
    }
    
    public bool IsCurrencySupported(string currencyCode)
    {
        // O(1) проверка благодаря perfect hashing
        return _exchangeRates.ContainsKey(currencyCode);
    }
    
    private IEnumerable<ExchangeRate> LoadExchangeRates()
    {
        // Имитация загрузки из БД/API
        return new[]
        {
            new ExchangeRate("USD", 1.00m),
            new ExchangeRate("EUR", 0.85m),
            new ExchangeRate("GBP", 0.73m),
            new ExchangeRate("JPY", 110.50m)
        };
    }
}

public record ExchangeRate(string CurrencyCode, decimal Rate);

4.2. Бенчмарк: сравнение производительности

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class CollectionsBenchmark
{
    private readonly Dictionary<string, int> _dictionary;
    private readonly FrozenDictionary<string, int> _frozenDictionary;
    
    public CollectionsBenchmark()
    {
        var data = Enumerable.Range(1, 1000)
            .ToDictionary(i => $"key_{i}", i => i);
        
        _dictionary = data;
        _frozenDictionary = data.ToFrozenDictionary();
    }
    
    [Benchmark]
    public int DictionaryLookup()
    {
        int sum = 0;
        for (int i = 1; i <= 1000; i++)
        {
            if (_dictionary.TryGetValue($"key_{i}", out int value))
                sum += value;
        }
        return sum;
    }
    
    [Benchmark]
    public int FrozenDictionaryLookup()
    {
        int sum = 0;
        for (int i = 1; i <= 1000; i++)
        {
            if (_frozenDictionary.TryGetValue($"key_{i}", out int value))
                sum += value;
        }
        return sum;
    }
}

// Результаты бенчмарка:
// | Method                 | Mean     | Allocated |
// |-----------------------|----------|-----------|
// | DictionaryLookup      | 1.234 μs | 0 B       |
// | FrozenDictionaryLookup| 0.456 μs | 0 B       | (~2.7x быстрее)

4.3. Использование новых фич в ASP.NET Core

// Program.cs с использованием C# 12
var builder = WebApplication.CreateBuilder(args);

// Коллекции в конфигурации
string[] allowedOrigins = ["https://localhost:3000", "https://app.example.com"];
string[] adminEmails = ["admin@example.com", "superadmin@example.com"];

builder.Services.AddCors(options =>
{
    options.AddPolicy("Allowed", policy =>
    {
        policy.WithOrigins([..allowedOrigins])
              .AllowAnyMethod()
              .AllowAnyHeader();
    });
});

// Первичные конструкторы в сервисах
public class UserService(ILogger<UserService> logger, IUserRepository repository)
{
    public async Task<User> GetUserAsync(int id)
    {
        logger.LogInformation("Getting user {UserId}", id);
        return await repository.GetByIdAsync(id);
    }
}

// Minimal API с новым синтаксисом
var app = builder.Build();

app.MapGet("/users/{id}", async (int id, UserService service) =>
{
    var user = await service.GetUserAsync(id);
    return user is not null ? Results.Ok(user) : Results.NotFound();
});

app.MapPost("/process", async (ProcessRequest request) =>
{
    // Использование новых коллекций
    var results = new List<ProcessResult>();
    
    foreach (var item in request.Items)
    {
        var result = ProcessItem(item);
        results.Add(result);
    }
    
    return Results.Ok([..results]); // Collection expression
});

app.Run();

public record ProcessRequest(ProcessItem[] Items);
public record ProcessItem(string Name, int Value);
public record ProcessResult(string Name, bool Success, DateTime ProcessedAt);

Заключение

C# 12 и .NET 8 представляют собой сфокусированное обновление, которое приносит значительные улучшения в трех ключевых областях: выразительность кода, работа с коллекциями и общая производительность. Эти релизы демонстрируют зрелость платформы, где вместо добавления революционных возможностей разработчики получают тщательно проработанные улучшения, которые имеют немедленное практическое применение.

Для максимальной отдачи от этих обновлений рекомендуется постепенное внедрение: начать с использования первичных конструкторов и нового синтаксиса коллекций, затем проанализировать возможности применения Frozen коллекций для статических данных, и наконец, рассмотреть переход на AOT компиляцию для критичных к производительности микросервисов и нативных приложений.