01Стандартная библиотека C# и .NET (BCL)

Уровень 1: Foundation

Коллекции и структуры данных C#/.NET

Содержание

  • Обзор System.Collections.Generic
  • Временная сложность операций
  • Внутреннее устройство коллекций
  • Параллельные коллекции
  • Неизменяемые коллекции
  • Span vs List
  • Практические рекомендации

System.Collections.Generic — Полный обзор

Основные коллекции и их назначение

КоллекцияНазначениеОперации
List<T>Динамический массив, доступ по индексуДобавление в конец, поиск по индексу
Dictionary<TKey, TValue>Хеш-таблица, ключ-значениеБыстрый поиск/вставка/удаление по ключу
HashSet<T>Уникальные элементы, операции над множествамиПроверка Contains, объединение, пересечение
SortedSet<T>Отсортированные уникальные элементыЗапросы по диапазону, порядковые операции
LinkedList<T>Двусвязный списокВставка/удаление в середине
Queue<T>Очередь FIFOEnqueue/Dequeue
Stack<T>Стек LIFOPush/Pop

Производительность обобщённых и необобщённых коллекций

// Обобщённая — типобезопасная, без боксинга
        List<int> genericList = new() { 5, 9, 1, 4 };

        // Необобщённая — требует боксинга/анбоксинга, ~75x медленнее
        ArrayList nonGenericList = new() { 5, 9, 1, 4 };

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

  • Типобезопасность на этапе компиляции
  • Отсутствие боксинга/анбоксинга для значимых типов (снижает нагрузку на GC)
  • Производительность: сортировка List<int> ~75x быстрее, чем ArrayList

Временная сложность операций

List

ОперацияСложностьПримечания
Индексатор []O(1)Прямой доступ к массиву
Add (в конец)O(1)*Амортизированная, O(n) при изменении размера
Insert (по индексу)O(n)Сдвиг элементов
RemoveAtO(n)Сдвиг элементов
Remove (по значению)O(n)Линейный поиск + сдвиг
ContainsO(n)Линейный поиск
SortO(n log n)IntroSort
BinarySearchO(log n)Требует отсортированного списка

Dictionary

ОперацияСложностьПримечания
Индексатор [] (get/set)O(1)*Хеш-таблица с цепочками
TryGetValueO(1)*Амортизированная
AddO(1)*O(n) при изменении размера
RemoveO(1)*
ContainsKeyO(1)*

*При равномерном распределении хеш-кодов. Худший случай (все ключи в одной корзине): O(n).

HashSet

ОперацияСложностьПримечания
AddO(1)*O(n) при изменении размера
ContainsO(1)*
RemoveO(1)*
SetEquals, IsSubsetOf и др.O(n)Перебор элементов

SortedSet

ОперацияСложностьПримечания
AddO(log n)Балансировка красно-чёрного дерева
ContainsO(log n)
RemoveO(log n)
GetViewBetweenO(log n)Создаёт представление без копирования
Min / MaxO(1)Левый/правый край дерева

LinkedList

ОперацияСложностьПримечания
AddFirst/AddLastO(1)
InsertAfter/BeforeO(1)При наличии ссылки на узел
Find (по значению)O(n)Линейный обход
Remove (по узлу)O(1)
Remove (по значению)O(n)Поиск + удаление

Queue и Stack

Обе используют кольцевой массив:

ОперацияQueueStack
Enqueue / PushO(1)*O(1)*
Dequeue / PopO(1)*O(1)
PeekO(1)O(1)

*O(n) при изменении размера внутреннего массива.


Внутреннее устройство коллекций

List — динамический массив со стратегией роста x2

public class List<T>
        {
            private const int DefaultCapacity = 4;
            private T[] _items;      // Внутренний массив
            private int _size;        // Фактическое количество элементов
            private int _version;     // Версия для обнаружения изменений при итерации

            public void Add(T item)
            {
                if (_size == _items.Length)
                    EnsureCapacity(_size + 1);  // Изменение размера: удвоение ёмкости
                _items[_size++] = item;
                _version++;
            }

            private void EnsureCapacity(int min)
            {
                if (_items.Length < min)
                {
                    int newCapacity = _items.Length == 0 
                        ? DefaultCapacity      // Первый элемент -> ёмкость = 4
                        : _items.Length * 2;   // Далее: удвоение (4->8->16->32...)
            
                    if ((uint)newCapacity > Array.MaxArrayLength)
                        newCapacity = Array.MaxArrayLength;
                    if (newCapacity < min)
                        newCapacity = min;
            
                    Capacity = newCapacity;  // Создаёт новый массив + Array.Copy
                }
            }
        }

Ключевые моменты:

  • Начальная ёмкость: 0 (пустой _emptyArray)
  • Первый Add -> ёмкость становится 4
  • Каждое последующее изменение размера -> ёмкость удваивается
  • Изменение размера создаёт новый массив и копирует все элементы через Array.Copy (нативный, на основе указателей)
  • Для 10 млн элементов требуется всего ~24 операции изменения размера

Оптимизация: если размер известен — установите ёмкость заранее:

var list = new List<int>(expectedCount); // Избегает всех операций изменения размера

Dictionary — хеш-таблица с цепочками (цепочки коллизий)

.NET использует раздельные цепочки, а не открытую адресацию. В .NET Core / .NET 5+ структура была оптимизирована для значительного сокращения потребления памяти и ускорения вычислений.

public class Dictionary<TKey, TValue> where TKey : notnull
        {
            private struct Entry
            {
                public uint hashCode; // Полный 32-битный хеш-код ключа (uint). В .NET Core/5+ знак не маскируется
                public int next;      // Индекс следующей записи в цепочке, -1 если последняя
                public TKey key;
                public TValue value;
            }

            // Оптимизация компактных (byte-sized) корзин (начиная с .NET Core 2.1):
            // Вместо фиксированного int[] (4 байта на корзину) тип массива корзин выбирается динамически:
            // - byte[] для емкостей < 256 (1 байт на корзину)
            // - ushort[] для емкостей < 65536 (2 байта на корзину)
            // - int[] для больших емкостей (4 байта на корзину)
            private object? _buckets;   // Хранит массив корзин (byte[], ushort[] или int[])
            private Entry[] _entries;   // Все записи (включая удалённые)
            private int _count;         // Количество активных записей
            private int _freeList;      // Индекс первого свободного слота в entries
            private int _freeCount;     // Количество свободных слотов
        }

Алгоритм поиска:

private ref TValue FindValue(TKey key)
        {
            if (_buckets != null)
            {
                uint hashCode = (uint)_comparer.GetHashCode(key);
        
                // В .NET Core / 5+ деление по модулю оптимизировано через умножение (FastMod) с помощью специального множителя.
                // Ниже показана логическая цепочка обхода коллизий:
                int i = GetBucket(hashCode);
                Entry[] entries = _entries;
                while ((uint)i < (uint)entries.Length)
                {
                    if (entries[i].hashCode == hashCode && _comparer.Equals(entries[i].key, key))
                        return ref entries[i].value;  // Найдено!
                    i = entries[i].next;
                }
            }
            throw new KeyNotFoundException();
        }

Алгоритм вставки:

  1. Вычислить 32-битный хеш-код ключа (как uint).
  2. Найти индекс корзины (с использованием оптимизированного деления по модулю через FastMod).
  3. Обход цепочки — если ключ найден, заменить значение.
  4. Если нет -> взять свободный слот из списка удалённых (freeList) или расширить массив _entries и заново пересчитать _buckets.
  5. Вставить запись в начало цепочки корзины.

Изменение размера: когда _count == _entries.Length:

  • Новый размер = ближайшее простое число > нового счётчика (HashHelpers.ExpandPrime).
  • Все записи повторно хешируются в новые корзины. Тип массива _buckets может повыситься (например, с byte[] до ushort[]).
  • Для ключей string: после определённого порога коллизий переключается на RandomizedStringEqualityComparer (защита от DoS-атак через искусственное создание хеш-коллизий).

Почему цепочки вместо открытой адресации?

  • Цепочки лучше справляются с высокими коэффициентами загрузки (>70%).
  • Открытая адресация (используется в старом Hashtable) страдает от кластеризации при плохих хешах.
  • Цепочки дают более стабильную и предсказуемую производительность при росте коллекции.

HashSet — та же структура, что и Dictionary

HashSet<T> внутренне использует ту же архитектуру хеш-таблицы с цепочками.

SortedSet — красно-чёрное дерево

public class SortedSet<T> : ISortedSet<T>, ISet<T>
        {
            private struct Node
            {
                public T Item;           // Значение
                public Node Left;        // Левый потомок (меньше)
                public Node Right;       // Правый потомок (больше)
                public Node Parent;      // Родитель
                internal bool Color;     // true = Красный, false = Чёрный
            }
        }

Свойства красно-чёрного дерева:

  1. Каждый узел красный или чёрный
  2. Корень всегда чёрный
  3. Красный узел не может иметь красный потомок (нет двух последовательных красных)
  4. Все пути от узла до листьев содержат одинаковое количество чёрных узлов

Гарантия: высота дерева <= 2*log2(n+1), обеспечивая O(log n) для всех операций.

LinkedList, Queue, Stack

LinkedList — двусвязный список:

private class Node
        {
            internal T _value;
            internal Node _next;
            internal Node _prev;
            internal LinkedList _list;
        }
  • LinkedListNode<T> — публичная обёртка вокруг внутреннего узла
  • Вставка/удаление по ссылке на узел: O(1)
  • Поиск по значению: O(n)

Queue и Stack — кольцевой массив:

private T[] _array;
        private int _head;      // Индекс первого элемента
        private int _tail;      // Индекс следующего свободного слота
        private int _size;

Параллельные коллекции (System.Collections.Concurrent)

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

СценарийКоллекция
Несколько потоков производителей/потребителейConcurrentQueue<T>, ConcurrentBag<T>
Кэш с частым чтением, редкой записьюConcurrentDictionary<TKey, TValue>
Потокобезопасный стекConcurrentStack<T>
Без блокировок, только добавлениеImmutableArray<T> + замена ссылок

ConcurrentDictionary

var dict = new ConcurrentDictionary<string, int>();

        // Атомарная вставка или обновление
        dict.AddOrUpdate("key", 
            newValue: 1,
            updateValueFactory: (_, oldValue) => oldValue + 1);

        // Атомарное получение или добавление
        var value = dict.GetOrAdd("key", k => ComputeExpensiveValue(k));

        // Безопасное удаление
        dict.TryRemove("key", out int removedValue);

Внутреннее устройство: сегментная блокировка (lock striping) — несколько сегментов блокируются независимо, позволяя параллельные операции с разными ключами.

ConcurrentQueue и ConcurrentStack

Обе используют алгоритмы без блокировок (CompareExchange / CAS).


Неизменяемые коллекции (System.Collections.Immutable)

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

  • Функциональное программирование (без побочных эффектов)
  • Многопоточное чтение без блокировок
  • Структуры данных, неизменяемые после создания (DAG, AST)
  • Кэширование: безопасно передавать между потоками

Основные типы

ИзменяемаяНеизменяемый эквивалентОсобенности
List<T>ImmutableList<T>Персистентная структура данных
Dictionary<K,V>ImmutableDictionary<K,V>Hash array mapped trie (HAMT)
HashSet<T>ImmutableHashSet<T>HAMT
SortedSet<T>ImmutableSortedSet<T>Персистентное красно-чёрное дерево

Паттерн Builder для эффективности

var builder = ImmutableHashSet.CreateBuilder<string>();
        foreach (var item in largeCollection)
            builder.Add(item);  // O(1) — изменяемый builder

        var frozen = builder.ToImmutable();  // O(1) — заморозка без копирования

HAMT (Hash Array Mapped Trie)

ImmutableDictionary и ImmutableHashSet используют HAMT — trie с коэффициентом ветвления 32 (5 бит хеш-кода на уровень). Это даёт:

  • O(log32 n) для всех операций ~ O(1) на практике (максимум 5 уровней для миллиарда элементов)
  • Структурное разделение: изменение одного элемента создаёт только новый путь от корня до листа

Span vs List — Когда что использовать

Сравнение

КритерийList<T>Span<T>
Выделение памятиКуча (GC)Стек / inline (без GC)
РазмерДинамический (изменение размера)Фиксированный при создании
Совместимость с asyncДаНет (ref struct)
Хранение в поле классаДаНет
СрезGetRange() -> новый объектSlice() -> представление без выделения памяти

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

// Парсинг — одноразовая обработка без выделений памяти
        public int ParseInt32(ReadOnlySpan<char> value) { /* ... */ }

        // Обработка буфера — работа с частью данных
        Span<byte> buffer = stackalloc byte[1024];
        Process(buffer.Slice(0, 512));  // Представление без выделения памяти

        // Высокопроизводительные критические участки
        public void Transform(Span<float> data)
        {
            for (int i = 0; i < data.Length; i++)
                data[i] *= 2.0f;
        }

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

// Коллекция, которая растёт/уменьшается
        var items = new List<string>();
        items.Add("new item");

        // Хранение между вызовами методов
        public class Service
        {
            private readonly List<int> _cache = new();  // Span<T> здесь невозможен
        }

        // Асинхронный контекст
        public async Task ProcessAsync()
        {
            var list = new List<byte>();  // Span<T> нельзя использовать в async/await
        }

GetHashCode() — Правильная реализация

Правила

  1. Детерминированность: один объект -> один хеш в течение жизни
  2. Согласованность с Equals: a.Equals(b) -> a.GetHashCode() == b.GetHashCode()
  3. Равномерное распределение: равномерное распределение для лучшей производительности хеш-таблиц

Примеры

// Плохо — все объекты имеют одинаковый хеш
        public override int GetHashCode() => 42;

        // Хорошо — составной хеш через структуру HashCode
        public override int GetHashCode()
            => HashCode.Combine(FirstName, LastName, BirthDate);

        // Для record — генерируется автоматически
        public record Person(string FirstName, string LastName, DateTime BirthDate);

IEnumerable vs IReadOnlyCollection vs IReadOnlyList

ИнтерфейсИндексаторCountПереборИзменяемый?
IEnumerable<T>НетНетДаНеизвестно
IReadOnlyCollection<T>НетДаДаНет
IReadOnlyList<T>Да O(1)*ДаДаНет
ICollection<T>НетДаДаДа (Add/Remove)
IList<T>Да O(1)*ДаДаДа (Add/Remove/Insert)

Что возвращать

// Минимальный контракт — только перебор
        public IEnumerable<Product> GetProducts() => _products;

        // Нужен размер заранее (для прогресс-бара, ёмкости)
        public IReadOnlyCollection<Product> GetProducts() => _products;

        // Нужен доступ по индексу без раскрытия изменяемости
        public IReadOnlyList<Product> GetProducts() => _products.AsReadOnly();

        // Не делайте так — раскрывает внутреннюю реализацию
        public List<Product> GetProducts() => _internalList;

Практика

  • [ ] Изучить материал

Бенчмарк: List vs ArrayPool

using System.Buffers;
        using System.Diagnostics;

        public class ListVsArrayPoolBenchmark
        {
            [Benchmark]
            public int ListApproach()
            {
                var list = new List<int>(1000);
                for (int i = 0; i < 1000; i++) list.Add(i);
                int sum = 0;
                foreach (var x in list) sum += x;
                return sum;
            }

            [Benchmark]
            public int ArrayPoolApproach()
            {
                var array = ArrayPool<int>.Shared.Rent(1000);
                try
                {
                    for (int i = 0; i < 1000; i++) array[i] = i;
                    int sum = 0;
                    for (int i = 0; i < 1000; i++) sum += array[i];
                    return sum;
                }
                finally
                {
                    ArrayPool<int>.Shared.Return(array);
                }
            }
        }

Результаты: ArrayPool выигрывает при повторном использовании (меньше нагрузка на GC), List быстрее для одноразовых операций.

Пользовательская коллекция с Comparer

public class CaseInsensitiveStringDictionary<TValue>
            : Dictionary<string, TValue>
        {
            public CaseInsensitiveStringDictionary()
                : base(StringComparer.OrdinalIgnoreCase) { }

            public CaseInsensitiveStringDictionary(int capacity)
                : base(capacity, StringComparer.OrdinalIgnoreCase) { }
        }

Сводная таблица: Какую коллекцию выбрать

ЗадачаРекомендуемая коллекция
Последовательность с доступом по индексуList<T>
Ключ -> значение (быстрый поиск)Dictionary<TKey, TValue>
Уникальные элементы без порядкаHashSet<T>
Отсортированные уникальные элементыSortedSet<T>
Обработка FIFOQueue<T> / ConcurrentQueue<T>
Обработка LIFOStack<T> / ConcurrentStack<T>
Вставка в середину по узлуLinkedList<T>
Многопоточный производитель/потребительПараллельные коллекции
Срез без выделения памятиSpan<T> / Memory<T>
Потокобезопасное чтениеНеизменяемые коллекции

Контрольные вопросы

  1. Как работает GetHashCode() и почему важна правильная реализация?
  2. В чём разница между IEnumerable, IReadOnlyCollection, IReadOnlyList?
  3. Когда следует использовать неизменяемые коллекции?

LINQ и функциональное программирование в C#/.NET

Содержание

  • Ленивое выполнение vs немедленное выполнение
  • yield return — внутреннее устройство state machine
  • IEnumerable vs IQueryable
  • Стандартные операторы запросов
  • ValueTuple vs Tuple
  • Пользовательские операторы LINQ
  • Деревья выражений

Ленивое выполнение vs немедленное выполнение

Отложенное (ленивое) выполнение

Большинство операторов LINQ используют отложенное выполнение — запрос не выполняется при определении, а выполняется при перечислении:

var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

        // Определение запроса — выполнения ЕЩЁ НЕТ
        var query = numbers
            .Where(n => n % 2 == 0)
            .Select(n => n * n);

        // Выполнение происходит здесь (во время перечисления)
        foreach (var item in query)
            Console.WriteLine(item);  // 4, 16, 36, 64, 100

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

Немедленное выполнение

Операторы, возвращающие одно значение или материализующие коллекцию, выполняются немедленно:

Немедленные (возвращают одно значение)Материализующие (возвращают коллекцию)
Count(), LongCount()ToList(), ToArray()
Sum(), Average(), Min(), Max()ToDictionary(), ToLookup()
First(), FirstOrDefault()ToHashSet()
Single(), SingleOrDefault()
Any(), All()
Contains()
ElementAt(), Last()
var numbers = new[] { 1, 2, 3, 4, 5 };

        // Эти выполняются немедленно:
        int count = numbers.Count();           // 5
        int sum = numbers.Sum();               // 15
        bool hasEven = numbers.Any(n => n % 2 == 0);  // true

        // Материализация принудительно выполняет и кэширует результаты:
        var list = numbers.Where(n => n > 2).ToList();  // [3, 4, 5] — выполнено один раз

Принудительное выполнение для избежания повторного вычисления

var db = GetDatabaseContext();

        // ПЛОХО: Запрос выполняется дважды (два вызова к БД)
        var query = db.Users.Where(u => u.IsActive);
        Console.WriteLine(query.Count());   // Первое выполнение
        foreach (var user in query)         // Второе выполнение!
            Console.WriteLine(user.Name);

        // ХОРОШО: Материализация один раз, повторное использование результатов
        var users = db.Users.Where(u => u.IsActive).ToList();  // Один вызов к БД
        Console.WriteLine(users.Count());
        foreach (var user in users)
            Console.WriteLine(user.Name);

yield return — внутреннее устройство state machine

Как это работает

yield return создаёт блок итератора, который компилятор преобразует в класс state machine, реализующий IEnumerator<T>:

public static IEnumerable<int> GetNumbers()
        {
            yield return 1;   // Состояние 0
            yield return 2;   // Состояние 1
            yield return 3;   // Состояние 2
                              // Состояние -1 (завершено)
        }

Эквивалент IL, генерируемый компилятором:

// Что генерирует компилятор:
        sealed class <GetNumbers>d__0 : IEnumerable<int>, IEnumerator<int>
        {
            int <>1__state;        // Текущее состояние (-1 = завершено, 0..N = точки yield)
            int <>2__current;      // Текущее возвращаемое значение
            bool <>4__this;

            public bool MoveNext()
            {
                switch (<>1__state)
                {
                    case 0:
                        <>1__state = -1;
                        <>2__current = 1;
                        <>1__state = 1;
                        return true;
                    case 1:
                        <>1__state = -1;
                        <>2__current = 2;
                        <>1__state = 2;
                        return true;
                    case 2:
                        <>1__state = -1;
                        <>2__current = 3;
                        <>1__state = 3;
                        return true;
                    default:
                        return false;  // Конец итерации
                }
            }

            public int Current => <>2__current;
            // Реализации Dispose(), Reset()...
        }

Ключевые характеристики:

  • Промежуточная коллекция не создаётся — значения генерируются по требованию
  • Состояние сохраняется между вызовами MoveNext()
  • Обработка исключений с try/finally генерирует дополнительные состояния для очистки
  • Объект итератора реализует и IEnumerable<T>, и IEnumerator<T>

Практический пример: бесконечная последовательность

public static IEnumerable<long> Fibonacci()
        {
            long a = 0, b = 1;
            while (true)
            {
                yield return a;
                (a, b) = (b, a + b);
            }
        }

        // Использование — берёте только то, что нужно:
        var first10 = Fibonacci().Take(10).ToList();
        // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

IEnumerable vs IQueryable — отложенное выполнение и деревья выражений

Ключевые отличия

АспектIEnumerable<T>IQueryable<T>
Место выполненияВ памяти (на клиенте)Удалённо (база данных, сервис)
Представление запросаДелегат (Func<>) скомпилирован в вызов методаДерево выражений (Expression<Func<>>) как структура данных
ФильтрацияЗагружает все данные, фильтрует в памятиПереводит в SQL/нативный запрос, фильтрует на сервере
Методы расширенияSystem.Linq.Enumerable (статический класс)System.Linq.Queryable (статический класс)
Производительность для больших данныхПлохая (загружает всё)Хорошая (переводит в оптимизированный запрос)

Как работает IQueryable

// IEnumerable — делегаты выполняются в памяти
        IEnumerable<int> enumerableQuery = numbers.Where(n => n > 5);
        // Использует: Enumerable.Where(source, predicate) где predicate — Func<int, bool>

        // IQueryable — дерево выражений отправляется провайдеру
        IQueryable<int> queryableQuery = numbers.AsQueryable().Where(n => n > 5);
        // Использует: Queryable.Where(source, predicate) где predicate — Expression<Func<int, bool>>

Структура дерева выражений для n => n > 5:

Lambda: n => n > 5
        ├── Parameter: n (int)
        └── Body: BinaryExpression (GreaterThan)
            ├── Left: ParameterExpression (n)
            └── Right: ConstantExpression (5)

Пример с Entity Framework

// ПЛОХО — загружает ВСЕХ пользователей в память, затем фильтрует в C#
        var bad = db.Users.AsEnumerable()      // Материализует всю таблицу!
            .Where(u => u.City == "London")    // Фильтр в памяти
            .ToList();

        // ХОРОШО — переводится в SQL: SELECT * FROM Users WHERE City = 'London'
        var good = db.Users
            .Where(u => u.City == "London")    // Дерево выражений -> перевод в SQL
            .ToList();                          // Выполнение оптимизированного запроса

Когда преобразование имеет значение

// Порядок цепочки важен!
        var result1 = db.Products
            .Where(p => p.Price > 100)         // Фильтр SQL (эффективно)
            .AsEnumerable()                     // Переключение на выполнение в памяти
            .OrderBy(p => p.Name.Length);       // Сортировка в памяти (SQL не может это легко перевести)

        // vs. всё в SQL:
        var result2 = db.Products
            .Where(p => p.Price > 100)
            .OrderBy(p => p.Name)              // SQL ORDER BY
            .ToList();

Стандартные операторы запросов — полный справочник

Проекция

ОператорОписаниеПример
SelectПреобразование каждого элемента.Select(x => x.ToUpper())
SelectManyСглаживание вложенных коллекций.SelectMany(u => u.Orders)
OfType<T>Фильтрация по типу.OfType<Customer>()

Фильтрация

ОператорОписаниеПример
WhereФильтрация по предикату.Where(x => x > 0)
DistinctУдаление дубликатов.Distinct()
ExceptРазность множеств.Except(otherSet)
IntersectПересечение множеств.Intersect(otherSet)

Группировка

ОператорОписаниеПример
GroupByГруппировка по ключу.GroupBy(x => x.Category)
ToLookupСоздание таблицы поиска.ToLookup(x => x.Key)
GroupJoinГрупповое соединение.GroupJoin(...)

Соединение

// Синтаксис методов LINQ
        var joined = customers
            .Join(orders,
                c => c.Id,           // Селектор ключа клиента
                o => o.CustomerId,   // Селектор ключа заказа
                (c, o) => new { Customer = c, Order = o });  // Селектор результата

        // Синтаксис запросов LINQ (эквивалентно)
        var joined2 = from c in customers
                      join o in orders on c.Id equals o.CustomerId
                      select new { Customer = c, Order = o };

Агрегация

ОператорОписаниеПример
SumСумма значений.Select(x => x.Price).Sum()
AverageСреднее значение.Average(x => x.Score)
Min / MaxМин/макс значение.Min(x => x.Age)
Count / LongCountКоличество элементов.Count(x => x.Active)
AggregateПользовательская аккумуляцияСм. ниже

Aggregate — мощный паттерн аккумуляции

// Базовый: сведение к одному значению
        var numbers = new[] { 1, 2, 3, 4, 5 };
        var sum = numbers.Aggregate((acc, x) => acc + x);  // 15

        // С начальным значением: аккумуляция в сложный результат
        var result = words.Aggregate(
            new Dictionary<string, int>(),  // Начальное значение
            (dict, word) =>                 // Аккумулятор
            {
                dict[word] = dict.GetValueOrDefault(word) + 1;
                return dict;
            });

        // С селектором результата: преобразование финального аккумулятора
        var longest = words.Aggregate(
            "",                             // Начальное значение (пустая строка)
            (longest, word) =>              // Аккумулятор
                word.Length > longest.Length ? word : longest,
            longest => longest.ToUpper());  // Селектор результата

Scan (кумулятивные операции)

.NET не имеет встроенного оператора Scan, но его можно реализовать:

public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(
            this IEnumerable<TSource> source,
            TAccumulate seed,
            Func<TAccumulate, TSource, TAccumulate> func)
        {
            yield return seed;
            foreach (var item in source)
            {
                seed = func(seed, item);
                yield return seed;
            }
        }

        // Использование: кумулятивная сумма
        var numbers = new[] { 1, 2, 3, 4, 5 };
        var cumulative = numbers.Scan(0, (acc, x) => acc + x);
        // [0, 1, 3, 6, 10, 15]

Сортировка

ОператорОписаниеПример
OrderByСортировка по возрастанию.OrderBy(x => x.Name)
OrderByDescendingСортировка по убыванию.OrderByDescending(x => x.Age)
ThenBy / ThenByDescendingВторичная сортировка.OrderBy(...).ThenBy(...)
ReverseОбратный порядок.Reverse()

Разбиение

ОператорОписаниеПример
Take(n)Первые n элементов.Take(10)
Skip(n)Пропустить первые n.Skip(10)
TakeWhileБрать, пока предикат истинен.TakeWhile(x => x > 0)
SkipWhileПропускать, пока предикат истинен.SkipWhile(x => x == 0)

Операторы элементов

ОператорОписаниеВыбрасывает исключение при пустой?
First()Первый элемент, соответствующий предикатуДа (InvalidOperationException)
FirstOrDefault()Первый или default(T)Нет
Single()Ровно один элементДа (если 0 или >1 элементов)
SingleOrDefault()Один или none, иначе исключениеДа (если >1 элементов)

Ключевое отличие: First возвращает первое совпадение; Single утверждает, что совпадение ровно одно. Используйте Single когда ожидаете уникальность (например, поиск по первичному ключу).


ValueTuple vs Tuple — Когда что использовать

Сравнение

АспектTuple<T1,T2,...>(T1, T2, ...) / ValueTuple
ТипСсылочный тип (class)Значимый тип (struct)
НеизменяемостьНеизменяемыйИзменяемый (поля публичны и могут меняться)
Имена полейВсегда Item1, Item2...Настраиваемые имена
ПроизводительностьВыделение в куче (GC)Стек/inline (без GC для небольших кортежей)
ДеконструкцияНетДа (var (a, b) = result)
Доступно с.NET 4.0.NET Core 2.0 / .NET Standard 2.1

Примеры использования

// Старый Tuple — многословный, выделение в куче
        Tuple<string, int> oldStyle = Tuple.Create("Alice", 30);
        Console.WriteLine(oldStyle.Item1);  // Alice

        // ValueTuple — лаконичный, выделение на стеке
        (string name, int age) newStyle = ("Bob", 25);
        Console.WriteLine(newStyle.name);   // Bob

        // Деконструкция
        var (n, a) = GetPerson();
        Console.WriteLine($"{n}, {a}");

        // Out кортежи (C# 7+)
        if (int.TryParse("42", out int result))
            Console.WriteLine(result);

        // Несколько возвращаемых значений
        public static (bool success, string message) Validate(string input)
        {
            if (string.IsNullOrEmpty(input))
                return (false, "Input cannot be empty");
            return (true, "Valid");
        }

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

  • ValueTuple: Выбор по умолчанию для большинства сценариев — лучшая производительность, чистый синтаксис
  • Tuple: Только при таргетировании .NET Framework < 4.7 или когда нужна семантика ссылочного типа (кэширование в словарях с null-значениями)

Пользовательский оператор LINQ через метод расширения

public static class CustomLinqExtensions
        {
            /// <summary>
            /// Применяет функцию к каждому элементу, передавая результат вперёд.
            /// Аналогично Scala's scan / Haskell's scanl.
            /// </summary>
            public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(
                this IEnumerable<TSource> source,
                TAccumulate seed,
                Func<TAccumulate, TSource, TAccumulate> accumulator)
            {
                if (source == null) throw new ArgumentNullException(nameof(source));
                if (accumulator == null) throw new ArgumentNullException(nameof(accumulator));

                return ScanIterator(source, seed, accumulator);
            }

            private static IEnumerable<TAccumulate> ScanIterator<TSource, TAccumulate>(
                IEnumerable<TSource> source,
                TAccumulate seed,
                Func<TAccumulate, TSource, TAccumulate> accumulator)
            {
                TAccumulate state = seed;
                yield return state;

                foreach (var item in source)
                {
                    state = accumulator(state, item);
                    yield return state;
                }
            }

            /// <summary>
            /// Группирует элементы в чанки указанного размера.
            /// </summary>
            public static IEnumerable<IList<T>> Chunk<T>(this IEnumerable<T> source, int size)
            {
                if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
        
                using var enumerator = source.GetEnumerator();
                while (enumerator.MoveNext())
                {
                    yield return TakeChunk(enumerator, size);
                }

                static IList<T> TakeChunk(IEnumerator<T> enumrator, int chunkSize)
                {
                    var chunk = new List<T>(chunkSize);
                    chunk.Add(enumrator.Current);
                    while (chunk.Count < chunkSize && enumrator.MoveNext())
                        chunk.Add(enumrator.Current);
                    return chunk;
                }
            }
        }

        // Использование:
        var numbers = Enumerable.Range(1, 10);
        var cumulativeSums = numbers.Scan(0, (acc, x) => acc + x).ToList();
        // [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55]

        var chunks = Enumerable.Range(1, 7).Chunk(3).ToList();
        // [[1,2,3], [4,5,6], [7]]

Бенчмарк: LINQ vs foreach для 1 млн элементов

using System.Diagnostics;
        using BenchmarkDotNet.Attributes;

        [MemoryDiagnoser]
        public class LinqVsForeachBenchmark
        {
            private int[] _data;

            [GlobalSetup]
            public void Setup()
            {
                _data = Enumerable.Range(0, 1_000_000).ToArray();
            }

            [Benchmark]
            public long ForeachSum()
            {
                long sum = 0;
                foreach (var x in _data)
                    if (x % 3 == 0)
                        sum += x * 2;
                return sum;
            }

            [Benchmark]
            public long LinqSum()
            {
                return _data
                    .Where(x => x % 3 == 0)
                    .Select(x => x * 2)
                    .Sum();
            }

            [Benchmark]
            public long ParallelLinqSum()
            {
                return _data.AsParallel()
                    .Where(x => x % 3 == 0)
                    .Select(x => x * 2)
                    .Sum();
            }
        }

Типичные результаты (Release mode):

МетодСреднее времяВыделено памяти
ForeachSum~1мс0 Б
LinqSum~3-5мс~20 КБ (выделение делегатов)
ParallelLinqSum~2-3мс~100 КБ (накладные расходы многопоточности)

Ключевой вывод: foreach быстрее для простых операций. LINQ добавляет накладные расходы на выделение делегатов для каждого оператора. PLINQ помогает с CPU-bound задачами, но имеет накладные расходы на многопоточность.


Expression — Как работает и где применяется

Лямбда как код vs лямбда как данные

// Лямбда как КОД (скомпилирована в делегат)
        Func<int, bool> code = x => x > 5;
        code(10);  // True — выполняется напрямую

        // Лямбда как ДАННЫЕ (дерево выражений)
        Expression<Func<int, bool>> data = x => x > 5;
        // Нельзя вызвать: data(10); // ОШИБКА КОМПИЛЯЦИИ
        // Сначала нужно скомпилировать:
        Func<int, bool> compiled = data.Compile();
        compiled(10);  // True

Структура дерева выражений

Expression<Func<int, int, int>> expr = (x, y) => x + y * 2;

        // Внутренняя структура дерева:
        // LambdaExpression
        // ├── Parameters: [ParameterExpression(x), ParameterExpression(y)]
        // └── Body: BinaryExpression(Add)
        //     ├── Left: ParameterExpression(x)
        //     └── Right: BinaryExpression(Multiply)
        //         ├── Left: ParameterExpression(y)
        //         └── Right: ConstantExpression(2)

Динамическая компиляция

// Построение дерева выражений программно
        var param = Expression.Parameter(typeof(int), "x");
        var constant = Expression.Constant(10);
        var body = Expression.GreaterThan(param, constant);
        var lambda = Expression.Lambda<Func<int, bool>>(body, param);

        // Компиляция в делегат (использует DynamicMethod + генерацию IL)
        Func<int, bool> predicate = lambda.Compile();
        Console.WriteLine(predicate(15));  // True

Где применяются деревья выражений

ФреймворкПрименение
Entity Framework / LINQ to SQLПеревод C# выражений в SQL-запросы
Dynamic LINQПостроение запросов из строк во время выполнения
ORM (Dapper, NHibernate)Перевод запросов
Фреймворки валидацииКомпиляция правил валидации один раз, повторное использование
RPC/сериализацияАнализ паттернов доступа к свойствам

Простой построитель запросов в стиле ORM

public static class ExpressionHelper
        {
            // Извлечение имени свойства из выражения: x => x.Name -> "Name"
            public static string GetPropertyName<T, TProp>(Expression<Func<T, TProp>> expr)
            {
                var body = expr.Body as MemberExpression
                    ?? (expr.Body as UnaryExpression)?.Operand as MemberExpression;
        
                return body?.Member.Name ?? throw new ArgumentException("Not a member access");
            }

            // Извлечение константного значения: x => x == "test" -> "test"
            public static object GetConstantValue(Expression expr)
            {
                return (expr as ConstantExpression)?.Value
                    ?? (expr as UnaryExpression)?.Operand as ConstantExpression?.Value;
            }
        }

        // Использование:
        string propName = ExpressionHelper.GetPropertyName<User, string>(u => u.Name);  // "Name"

Практика

  • [ ] Изучить материал

Контрольные вопросы

  1. Что такое отложенное выполнение и как оно влияет на производительность?
  2. Разница между First(), FirstOrDefault(), Single(), SingleOrDefault()?
  3. Как работает Expression и где применяется?

Делегаты, события и выражения в C#/.NET

Содержание

  • Встроенные типы делегатов: Action, Func, Predicate, Comparison
  • Мультикаст-делегаты — внутреннее устройство списка вызовов
  • События — паттерн издатель/подписчик, потокобезопасность
  • Expression — построение и компиляция деревьев выражений
  • Динамическая компиляция через Expression.Compile()
  • Практические упражнения

Встроенные типы делегатов

Func — функции с возвращаемым значением

// Обобщённые определения (до 16 параметров + возвращаемый тип)
        public delegate TResult Func<TResult>();
        public delegate TResult Func<T, TResult>(T arg);
        public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
        public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3);
        // ... до Func<T1,...,T16, TResult>

        // Примеры использования
        Func<string, int> parse = int.Parse;                    // Преобразование группы методов
        Func<int, int, int> add = (a, b) => a + b;              // Лямбда-выражение
        Func<string, int> length = s => s.Length;               // Ещё одна лямбда

        // В LINQ
        var names = new[] { "Alice", "Bob", "Charlie" };
        var uppercased = names.Select((Func<string, string>)(s => s.ToUpper()));

Action — функции без возвращаемого значения

public delegate void Action();
        public delegate void Action<T>(T arg);
        public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
        // ... до Action<T1,...,T16>

        // Примеры использования
        Action<string> log = Console.WriteLine;
        Action<int, string> logWithLevel = (level, msg) => 
            Console.WriteLine($"[{level}] {msg}");

        // В LINQ
        names.ForEach((Action<string>)(n => Console.WriteLine(n)));

        // Распространённый паттерн: обратные вызовы
        public void Retry(Action action, int maxRetries = 3)
        {
            for (int i = 0; i < maxRetries; i++)
            {
                try { action(); return; }
                catch { if (i == maxRetries - 1) throw; }
            }
        }

Predicate — булева функция

public delegate bool Predicate<T>(T obj);

        // Примеры использования
        Predicate<string> isNotEmpty = s => !string.IsNullOrEmpty(s);
        Predicate<int> isEven = n => n % 2 == 0;

        // В LINQ (хотя Where использует Func<T, bool>)
        var list = new List<int> { 1, 2, 3, 4, 5 };
        int firstEven = list.Find(isEven);           // List.Find использует Predicate<T>
        int index = list.FindIndex(isEven);          // List.FindIndex использует Predicate<T>
        bool exists = list.Exists(isEven);           // List.Exists использует Predicate<T>

        // Примечание: Predicate<T> функционально эквивалентен Func<T, bool>
        // но имеет семантическое значение: «этот делегат проверяет условие»

Comparison — функция сравнения

public delegate int Comparison<T>(T x, T y);
        // Возвращает: < 0 если x < y, 0 если x == y, > 0 если x > y

        // Примеры использования
        Comparison<string> lengthCompare = (a, b) => a.Length.CompareTo(b.Length);

        var words = new List<string> { "banana", "pie", "apple" };
        words.Sort(lengthCompare);  // ["pie", "banana", "apple"] — отсортировано по длине

        // vs IComparer<T>:
        public class LengthComparer : IComparer<string>
        {
            public int Compare(string x, string y) => x.Length.CompareTo(y.Length);
        }

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

ДелегатВозвращаемый типЛучше всего подходит для
Func<T, TResult>TResultПреобразования, вычисления, LINQ Select
Action<T>voidПобочные эффекты, обратные вызовы, обработчики событий (без аргументов)
Predicate<T>boolУсловия, фильтрация (List.Find, List.RemoveAll)
Comparison<T>intСортировка пользовательских коллекций

Мультикаст-делегаты — внутреннее устройство списка вызовов

Как работает мультикастинг

Делегат может хранить ссылки на несколько методов (список вызовов):

Action<string> handlers = Message1;
        handlers += Message2;  // Добавить в список вызовов
        handlers += Message3;
        handlers -= Message1;  // Удалить из списка

        handlers("Hello");  // Вызывает: Message2("Hello"), Message3("Hello")

        void Message1(string msg) => Console.WriteLine($"M1: {msg}");
        void Message2(string msg) => Console.WriteLine($"M2: {msg}");
        void Message3(string msg) => Console.WriteLine($"M3: {msg}");

Внутренняя структура

// Упрощённая внутренняя структура System.Delegate
        // Упрощённая внутренняя структура System.MulticastDelegate (в современном .NET)
        public abstract class MulticastDelegate : Delegate
        {
            // Вместо связанного списка, современный .NET использует массив для хранения цепочки вызовов
            private object _invocationList; // Массив делегатов Delegate[], если элементов > 1
            private IntPtr _invocationCount;
        }

        // Мультикаст-делегат — это МАССИВ ВЫЗОВОВ (в современном .NET), а не связанный список:
        // _invocationList = [Delegate1, Delegate2, Delegate3]
        // При вызове делегаты обходятся циклом for по массиву.

Ключевые моменты:

  • += добавляет в начало списка (O(1))
  • -= удаляет из любого места списка (O(n), поиск по равенству ссылок)
  • Порядок вызова: последний добавленный → первый добавленный (LIFO для цепочки, но на самом деле порядок инвертируется при вызове)
  • На самом деле: делегаты вызываются в порядке их добавления (FIFO)

Возвращаемые значения и исключения

// С Func — возвращается возвращаемое значение ТОЛЬКО ПОСЛЕДНЕГО делегата!
        Func<int, int> f = x => x + 1;
        f += x => x * 2;
        f += x => x - 3;
        var result = f(10);  // Возвращает 7 (только последний: 10 - 3). Результаты первых двух отбрасываются!

        // С исключениями — первое исключение останавливает вызов
        Func<int, int> f2 = x => x + 1;
        f2 += x => throw new InvalidOperationException("Boom!");
        f2 += x => x * 2;  // Никогда не будет вызван, если второй выбросит исключение

        try { f2(5); } catch (InvalidOperationException) { /* поймано */ }

        // Чтобы получить все возвращаемые значения, используйте GetInvocationList():
        foreach (Func<int, int> individual in f.GetInvocationList())
        {
            Console.WriteLine(individual(10));  // 11, 20, 7 — каждый результат захвачен
        }

Лучшие практики

  • Используйте Action для мультикаста (возвращаемое значение всё равно отбрасывается)
  • Используйте Func только когда уверены, что вызов одиночный
  • Для получения всех результатов: перебирайте GetInvocationList()
  • Будьте осторожны с обработкой исключений — одно исключение останавливает цепочку

События — паттерн издатель/подписчик и потокобезопасность

Базовый паттерн событий

public class Publisher
        {
            // Объявление события (компилятор генерирует приватное поле делегата + аксессоры add/remove)
            public event Action<string> MessageReceived;

            public void SendMessage(string message)
            {
                // Потокобезопасный паттерн вызова
                MessageReceived?.Invoke(message);
        
                // Альтернатива (более потокобезопасная в старых .NET):
                // var handler = MessageReceived;
                // handler?.Invoke(message);
            }
        }

        public class Subscriber
        {
            public void Subscribe(Publisher publisher)
            {
                publisher.MessageReceived += OnMessage;
            }

            private void OnMessage(string message)
            {
                Console.WriteLine($"Получено: {message}");
            }
        }

Паттерн EventHandler и EventArgs (традиционный .NET)

// Пользовательские аргументы события
        public class TemperatureChangedEventArgs : EventArgs
        {
            public double OldTemperature { get; }
            public double NewTemperature { get; }

            public TemperatureChangedEventArgs(double oldTemp, double newTemp)
            {
                OldTemperature = oldTemp;
                NewTemperature = newTemp;
            }
        }

        // Издатель с традиционным паттерном
        public class Thermostat : IDisposable
        {
            // EventHandler<T> эквивалентен delegate void Handler(object sender, T e)
            public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;

            private double _temperature;
            public double Temperature
            {
                get => _temperature;
                set
                {
                    if (_temperature == value) return;
                    var oldTemp = _temperature;
                    _temperature = value;
            
                    // Вызов события (virtual для тестируемости в производных классах)
                    OnTemperatureChanged(oldTemp, value);
                }
            }

            protected virtual void OnTemperatureChanged(double oldTemp, double newTemp)
            {
                TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs(oldTemp, newTemp));
            }

            public void Dispose()
            {
                TemperatureChanged = null;  // Позволяет подписчикам быть собранными GC
            }
        }

Потокобезопасность событий

public class ThreadSafePublisher
        {
            private event Action<string> _messageReceived;

            public event Action<string> MessageReceived
            {
                add { lock (_lock) _messageReceived += value; }
                remove { lock (_lock) _messageReceived -= value; }
            }
            private readonly object _lock = new();

            public void Publish(string message)
            {
                // Паттерн снимка: копирование ссылки в локальную переменную
                // Предотвращает состояние гонки, когда обработчик становится null между проверкой и вызовом
                var handlers = _messageReceived;
                if (handlers != null)
                    handlers(message);
        
                // В .NET Core+: null-условный оператор достаточно потокобезопасен для большинства случаев
                // _messageReceived?.Invoke(message);
            }
        }

Сценарий состояния гонки:

Поток A:                    Поток B:
        _проверка handler (не null)  handler -= отписка  (теперь null!)
        _вызов handler              → NullReferenceException!

Решение: снимок в локальную переменную (как показано выше).

Асинхронные обработчики событий

public class AsyncPublisher
        {
            public event Func<Task> AsyncEvent;

            public async Task RaiseAsync()
            {
                var handlers = AsyncEvent?.GetInvocationList()
                    .Cast<Func<Task>>()
                    .Select(h => h())  // Запуск всех задач
                    .ToArray();
        
                if (handlers != null)
                    await Task.WhenAll(handlers);  // Ожидание всех
            }
        }

Expression — построение и компиляция деревьев выражений

Лямбда как структура данных

// Обычный делегат — сразу компилируется в IL
        Func<int, bool> del = x => x > 5;

        // Дерево выражений — захвачено как структура данных
        Expression<Func<int, bool>> expr = x => x > 5;

        // Дерево выражений представляет:
        // Lambda [x] -> BinaryExpression(GreaterThan)
        //   Left: ParameterExpression [x]
        //   Right: ConstantExpression [5]

Программное построение деревьев выражений

using System.Linq.Expressions;

        // Построение: x => x > 5
        var parameter = Expression.Parameter(typeof(int), "x");
        var constant = Expression.Constant(5);
        var body = Expression.GreaterThan(parameter, constant);
        var lambda = Expression.Lambda<Func<int, bool>>(body, parameter);

        Console.WriteLine(lambda);  // x => (x > 5)

        // Построение: (x, y) => x + y * 2
        var p1 = Expression.Parameter(typeof(int), "x");
        var p2 = Expression.Parameter(typeof(int), "y");
        var multiply = Expression.Multiply(p2, Expression.Constant(2));
        var add = Expression.Add(p1, multiply);
        var lambda2 = Expression.Lambda<Func<int, int, int>>(add, p1, p2);

        Console.WriteLine(lambda2);  // (x, y) => (x + (y * 2))

Распространённые типы узлов выражений

ТипОписаниеПример
ParameterExpressionПараметр методаx в x => x > 5
ConstantExpressionКонстантное значение5 в x => x > 5
BinaryExpressionБинарная операция>, <, +, -, ==, &&, `\\`
UnaryExpressionУнарная операция!, -, ++, --, преобразование
MemberExpressionДоступ к свойству/полюx.Name в x => x.Name
MethodCallExpressionВызов методаx.ToString() в x => x.ToString()
ConditionalExpressionТернарный операторx > 0 ? "pos" : "neg"
LambdaExpressionОбёртка лямбдыВсё выражение целиком
NewExpressionСоздание объектаnew Person()
InvocationExpressionВызов делегатаfunc(x)

Паттерн Expression Visitor

public class ExpressionReplacer : ExpressionVisitor
        {
            private readonly Expression _from;
            private readonly Expression _to;

            public ExpressionReplacer(Expression from, Expression to)
            {
                _from = from;
                _to = to;
            }

            protected override Expression VisitExpression(Expression node)
            {
                if (node == _from)
                    return _to;
                return base.VisitExpression(node);
            }
        }

        // Использование: замена параметра в выражении
        var param = Expression.Parameter(typeof(int), "x");
        var expr = Expression.Lambda<Func<int, bool>>(
            Expression.GreaterThan(param, Expression.Constant(5)), param);

        var newParam = Expression.Parameter(typeof(int), "y");
        var replacer = new ExpressionReplacer(param, newParam);
        var newExpr = (LambdaExpression)replacer.Visit(expr);
        Console.WriteLine(newExpr);  // y => (y > 5)

Динамическая компиляция через Expression.Compile()

Как работает Compile

Expression<Func<int, bool>> expr = x => x > 5;

        // Компилирует дерево выражений в делегат с использованием DynamicMethod + генерации IL
        Func<int, bool> compiled = expr.Compile();

        Console.WriteLine(compiled(10));   // True
        Console.WriteLine(compiled(3));    // False

        // Скомпилированный делегат можно кэшировать и переиспользовать:
        var cache = new Dictionary<string, Func<object, bool>>();

        public Func<object, bool> GetOrCreatePredicate(string fieldName, object value)
        {
            if (cache.TryGetValue(fieldName, out var existing))
                return existing;  // Переиспользование скомпилированного делегата — быстро!

            var param = Expression.Parameter(typeof(object), "x");
            var convert = Expression.Convert(param, GetFieldType(fieldName));
            var fieldAccess = Expression.Property(convert, fieldName);
            var constant = Expression.Constant(value, GetFieldType(fieldName));
            var equal = Expression.Equal(fieldAccess, constant);
            var lambda = Expression.Lambda<Func<object, bool>>(equal, param);

            var compiled = lambda.Compile();  // Генерация IL происходит один раз
            cache[fieldName] = compiled;
            return compiled;
        }

Сравнение производительности

ПодходПервый вызовПоследующие вызовыПримечания
Рефлексия (PropertyInfo.GetValue)~100нс~100нсМедленно, но стабильно
Expression.Compile() + кэш~5мкс (первая компиляция)~5нсБыстро после компиляции
Delegate.CreateDelegate~5мкс~5нсАналогично скомпилированному выражению
Прямой вызов метода~1нс~1нсБазовый уровень

Динамический построитель предикатов для фильтрации данных

public static class PredicateBuilder
        {
            public static Expression<Func<T, bool>> True<T>() => _ => true;
            public static Expression<Func<T, bool>> False<T>() => _ => false;

            public static Expression<Func<T, bool>> Or<T>(
                this Expression<Func<T, bool>> expr1,
                Expression<Func<T, bool>> expr2)
            {
                var invoked = Expression.Invoke(expr2, expr1.Parameters);
                return Expression.Lambda<Func<T, bool>>(
                    Expression.OrElse(expr1.Body, invoked), expr1.Parameters);
            }

            public static Expression<Func<T, bool>> And<T>(
                this Expression<Func<T, bool>> expr1,
                Expression<Func<T, bool>> expr2)
            {
                var invoked = Expression.Invoke(expr2, expr1.Parameters);
                return Expression.Lambda<Func<T, bool>>(
                    Expression.AndAlso(expr1.Body, invoked), expr1.Parameters);
            }
        }

        // Использование: динамическое построение сложных предикатов
        Expression<Func<Product, bool>> predicate = PredicateBuilder.True<Product>();

        if (minPrice.HasValue)
            predicate = predicate.And(p => p.Price >= minPrice.Value);
        if (!string.IsNullOrEmpty(category))
            predicate = predicate.And(p => p.Category == category);
        if (inStockOnly)
            predicate = predicate.And(p => p.Stock > 0);

        var results = db.Products.Where(predicate).ToList();
        // Генерирует: WHERE Price >= ? AND Category = ? AND Stock > 0

Практика

  • [ ] Изучить материал

Упражнение 1: EventAggregator с поддержкой async

public interface IEventAggregator : IDisposable
        {
            void Publish<TEvent>(TEvent @event) where TEvent : notnull;
            Task PublishAsync<TEvent>(TEvent @event) where TEvent : notnull;
            IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : notnull;
            IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : notnull;
        }

        public class EventAggregator : IEventAggregator
        {
            private readonly Dictionary<Type, Delegate> _handlers = new();
            private readonly object _lock = new();

            public void Publish<TEvent>(TEvent @event) where TEvent : notnull
            {
                var eventType = typeof(TEvent);
                lock (_lock)
                {
                    if (!_handlers.TryGetValue(eventType, out var handler))
                        return;
                }

                foreach (Action<TEvent> h in (Delegate)handler)
                    h(@event);
            }

            public async Task PublishAsync<TEvent>(TEvent @event) where TEvent : notnull
            {
                var eventType = typeof(TEvent);
                List<Task> tasks;

                lock (_lock)
                {
                    if (!_handlers.TryGetValue(eventType, out var handler))
                        return;
                
                    tasks = ((Delegate)handler).GetInvocationList()
                            .Cast<Func<TEvent, Task>>()
                            .Select(h => h(@event))
                            .ToList();
                }

                await Task.WhenAll(tasks);
            }

            public IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : notnull
            {
                var eventType = typeof(TEvent);
                lock (_lock)
                {
                    _handlers[eventType] = Delegate.Combine(
                        _handlers.GetValueOrDefault(eventType), handler);
                }

                return new SubscriptionHandle(this, eventType, handler);
            }

            public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : notnull
            {
                var eventType = typeof(TEvent);
                lock (_lock)
                {
                    _handlers[eventType] = Delegate.Combine(
                        _handlers.GetValueOrDefault(eventType), handler);
                }

                return new SubscriptionHandle(this, eventType, handler);
            }

            private void Unsubscribe<TEvent>(Delegate handler) where TEvent : notnull
            {
                lock (_lock)
                {
                    var eventType = typeof(TEvent);
                    _handlers[eventType] = Delegate.Remove(
                        _handlers.GetValueOrDefault(eventType), handler);
                }
            }

            private class SubscriptionHandle : IDisposable
            {
                readonly EventAggregator _aggregator;
                readonly Type _eventType;
                readonly Delegate _handler;

                public SubscriptionHandle(EventAggregator aggregator, Type eventType, Delegate handler)
                {
                    _aggregator = aggregator;
                    _eventType = eventType;
                    _handler = handler;
                }

                public void Dispose()
                {
                    // Использование рефлексии для вызова обобщённого Unsubscribe
                    var method = typeof(EventAggregator)
                        .GetMethod(nameof(Unsubscribe), BindingFlags.NonPublic | BindingFlags.Instance)!
                        .MakeGenericMethod(_eventType);
                    method.Invoke(_aggregator, new[] { _handler });
                }
            }

            public void Dispose()
            {
                _handlers.Clear();
            }
        }

Упражнение 2: Простой построитель запросов в стиле ORM на деревьях выражений

См. раздел «Динамический построитель предикатов» выше для основы. Полный построитель запросов расширит это:

  • Генерация SQL из деревьев выражений (ExpressionVisitor)
  • Извлечение параметров и построение команд
  • Маппинг результатов обратно в сущности

Сводная таблица

КонцепцияКлючевой вывод
Func<T>Используйте для вычислений, возвращающих значения
Action<T>Используйте для побочных эффектов, обратных вызовов
Predicate<T>Семантический эквивалент Func<T, bool>, используется методами List
Мультикаст-делегатыСвязанный список методов; сохраняется только возвращаемое значение последнего Func
СобытияИздатель/подписчик с потокобезопасным паттерном снимка
Деревья выраженийКод как данные — позволяет перевод запросов (EF Core и др.)
Compile()Преобразует дерево выражений в IL-делегат — кэшируйте для производительности

Сопоставление с образцом и современные возможности C#

Содержание

  • Паттерны свойств, рекурсивные паттерны, реляционные паттерны
  • switch expression vs switch statement
  • Типы record — равенство по значению, неразрушающее изменение, выражения with
  • ref struct, модификатор required, namespaces на уровне файла
  • Source Generators — базовое понимание
  • Практические упражнения

Эволюция сопоставления с образцом

Паттерны типов (C# 7)

// Базовый паттерн типа
        void Process(object value)
        {
            if (value is int i)
                Console.WriteLine($"Целое число: {i}");
    
            if (value is string s && s.Length > 0)
                Console.WriteLine($"Непустая строка: {s}");
        }

Паттерны констант (C# 8)

void Check(object value)
        {
            if (value is null)         // Паттерн null (безопаснее, чем == null)
                Console.WriteLine("Null");
    
            if (value is 42)           // Паттерн константы
                Console.WriteLine("Ответ!");
    
            if (value is "hello")      // Строковая константа
                Console.WriteLine("Приветствие");
    
            if (value is not null)     // Отрицательный паттерн (C# 9)
                Console.WriteLine("Не null");
        }

Реляционные паттерны (C# 9)

string Classify(double value) => value switch
        {
            < -100.0 => "Очень низко",
            < 0.0    => "Низко",
            0.0      => "Ноль",
            > 0.0 and <= 100.0 => "Нормально",
            > 100.0  => "Высоко",
            _        => "Неизвестно (NaN?)",
        };

        // Составные паттерны с 'and', 'or', 'not'
        bool IsLetterGrade(char c) => c is >= 'A' and <= 'F';
        bool IsVowel(char c) => c is 'a' or 'e' or 'i' or 'o' or 'u';
        bool IsNotControlChar(char c) => c is not < ' ';

Паттерны свойств (C# 8)

public record Person(string Name, int Age, string Email);

        void Describe(Person person)
        {
            if (person is { Age: >= 18, Email: not null })
                Console.WriteLine("Взрослый с email");
    
            if (person is { Name: "Alice", Age: > 25 })
                Console.WriteLine("Alice старше 25");
        }

        // В switch expressions:
        string Categorize(Person p) => p switch
        {
            { Age: < 0 }         => "Некорректно",
            { Age: >= 0, Age: < 13 }   => "Ребёнок",
            { Age: >= 13, Age: < 18 }  => "Подросток",
            { Age: >= 18, Age: < 65 }  => "Взрослый",
            { Age: >= 65 }        => "Пожилой",
            null                  => "Неизвестно",
        };

Рекурсивные паттерны (C# 9)

Паттерны внутри паттернов — сопоставление вложенных структур:

public record Point(int X, int Y);
        public record Segment(Point Start, Point End);
        public record Circle(Point Center, double Radius);

        // Проверка, находится ли любая конечная отрезка на оси X
        bool IsAnyEndOnXAxis(Segment seg) =>
            seg is { Start: { Y: 0 } } or { End: { Y: 0 } };

        // Сопоставление конкретных фигур со свойствами
        string DescribeShape(object shape) => shape switch
        {
            Circle { Radius: > 10, Center: { X: 0, Y: 0 } } => "Большой круг в начале координат",
            Circle { Radius: var r } when r > 0 => $"Круг с радиусом {r}",
            Segment { Start: { X: sx }, End: { X: sx } } => "Вертикальный отрезок",  // Одинаковый X = вертикальный
            Segment { Start: var s, End: var e } => $"Отрезок от ({s}) до ({e})",
            _ => "Неизвестная фигура",
        };

        // Сопоставление с образцом списков (C# 11)
        int[] CheckArray(int[] arr) => arr switch
        {
            [0, 1, 2, ..] => new[] { 1 },      // Начинается с 0, 1, 2
            [.. , 9, 8, 7] => new[] { 2 },     // Заканчивается на 9, 8, 7
            [1, .., 9] => new[] { 3 },         // Начинается с 1, заканчивается на 9
            [var first, ..] => new[] { first }, // Захват первого элемента
            [] => Array.Empty<int>(),          // Пустой массив
        };

Паттерн var

// Захват любого значения (всегда совпадает)
        if (value is var _)        // Всегда true, значение отбрасывается
            Console.WriteLine("Имеет значение");

        if (person is { Name: var name })  // Захват свойства
            Console.WriteLine($"Имя: {name}");

        // В switch — привязка к переменной для использования в результате
        string Describe(object obj) => obj switch
        {
            int var i when i > 0   => $"Положительное: {i}",
            int var i              => $"Неположительное: {i}",
            string var s           => $"Строка длины {s.Length}",
            null                   => "Null",
            var _                  => obj.GetType().Name,  // Запасной вариант
        };

switch Expression vs switch Statement

switch Statement (традиционный)

// C# 7+ с сопоставлением с образцом в case
        switch (value)
        {
            case null:
                Console.WriteLine("Null");
                break;
            case int n when n > 0:
                Console.WriteLine($"Положительное: {n}");
                break;
            case int n when n < 0:
                Console.WriteLine($"Отрицательное: {n}");
                break;
            case int _:
                Console.WriteLine("Ноль");
                break;
            case string s:
                Console.WriteLine($"Строка: {s}");
                break;
            default:
                Console.WriteLine("Другое");
                break;  // Обязательно! (нет проваливания без goto)
        }

switch Expression (C# 8+)

// Более лаконичный, ориентированный на выражения, проверка полноты
        string result = value switch
        {
            null                    => "Null",
            int n when n > 0        => $"Положительное: {n}",
            int n when n < 0        => $"Отрицательное: {n}",
            int _                   => "Ноль",
            string s                => $"Строка: {s}",
            _                       => "Другое",   // Ветка по умолчанию (discard подчёркивание)
        };

        // break не нужен, нет проваливания, возвращает значение напрямую

Сравнение

Аспектswitch Statementswitch Expression
Возвращает значениеНет (использует операторы)Да (выражение)
Требует break/gotoДаНет
Проверка полнотыНетПредупреждение компилятора, если паттерны не покрывают все случаи (с record)
Порядок паттерновСверху внизСверху вниз
Безопасность nullРучная проверка nullПаттерн null работает естественно
Ограничение сложностиБез ограниченийЛучше для простой/средней логики

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

  • switch expression: Возврат значения на основе паттернов, лаконичные преобразования
  • switch statement: Сложная логика с побочными эффектами, несколько операторов на case, операции ввода/вывода

Типы record — равенство по значению и неразрушающее изменение

Что такое record?

Record обеспечивают равенство по значению для ссылочных типов:

// Record class (ссылочный тип с семантикой значений)
        public record Person(string Name, int Age);

        var p1 = new Person("Alice", 30);
        var p2 = new Person("Alice", 30);

        Console.WriteLine(p1 == p2);       // True! (равенство по значению)
        Console.WriteLine(p1.Equals(p2));  // True!
        Console.WriteLine(p1.GetHashCode() == p2.GetHashCode());  // True!

        // vs обычный class:
        public class ClassicPerson
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        var c1 = new ClassicPerson { Name = "Alice", Age = 30 };
        var c2 = new ClassicPerson { Name = "Alice", Age = 30 };
        Console.WriteLine(c1 == c2);       // False! (равенство по ссылке)

Генерируемые члены

Компилятор генерирует для public record Person(string Name, int Age):

  • Первичный конструктор с параметрами
  • Init-only свойства для каждого параметра
  • Equals(), GetHashCode(), ToString() — на основе значений
  • Метод <Clone>() для неразрушающего изменения (with)

Неразрушающее изменение с выражениями with (C# 9+)

public record Person(string Name, int Age, string Email);

        var original = new Person("Alice", 30, "alice@example.com");
        var updated = original with { Age = 31 };           // Новый экземпляр, Age изменён
        var alsoUpdated = original with { Name = "Bob" };   // Ещё один новый экземпляр

        Console.WriteLine(original.Age);    // 30 (не изменился!)
        Console.WriteLine(updated.Age);     // 31
        Console.WriteLine(original == updated);  // False (разные значения)

        // Несколько свойств:
        var fullyUpdated = original with 
        { 
            Name = "Alice Smith", 
            Age = 31,
            Email = "alice.smith@example.com"
        };

Record Struct vs Record Class

// Record class — ссылочный тип с равенством по значению
        public record PersonClass(string Name, int Age);

        // Record struct — значимый тип с равенством по значению (без GC для небольших записей)
        public record struct PersonStruct(string Name, int Age);

        var rc1 = new PersonClass("Alice", 30);
        var rc2 = rc1 with { Age = 31 };  // Новое выделение в куче

        var rs1 = new PersonStruct("Alice", 30);
        var rs2 = rs1 with { Age = 31 };  // Копирование на стеке (без нагрузки на GC)
Аспектrecord classrecord struct
ТипСсылочныйЗначимый
РавенствоПо значениюПо значению
ПамятьВыделение в кучеСтек/inline
Выражение withНовый объект в кучеКопирование + изменение
НаследованиеПоддерживает record A : BБез наследования
NullabilityМожет быть nullНе может быть null

Наследование record

public record Person(string Name, int Age);
        public record Employee(string Name, int Age, string Department) : Person(Name, Age);

        var emp = new Employee("Alice", 30, "Engineering");
        Console.WriteLine(emp.Name);       // Alice (унаследованное свойство)
        Console.WriteLine(emp.Department); // Engineering

        // Выражение with работает через наследование:
        var promoted = emp with { Department = "Management" };

Позиционная деконструкция

public record Point(int X, int Y);

        var point = new Point(3, 4);
        var (x, y) = point;  // x = 3, y = 4

        // Пользовательский деконструктор:
        public record Person(string FirstName, string LastName, int Age)
        {
            public void Deconstruct(out string Name, out int Age)
            {
                Name = $"{FirstName} {LastName}";
                Age = Age;
            }
        }

        var (name, age) = new Person("Alice", "Smith", 30);
        // name = "Alice Smith", age = 30

ref struct, модификатор required, namespaces на уровне файла

ref struct (C# 7.2+)

Структуры, которые должны находиться на стеке — не могут быть боксированы:

public ref struct BufferReader
        {
            private readonly Span<byte> _buffer;
            private int _position;

            public BufferReader(Span<byte> buffer)
            {
                _buffer = buffer;
                _position = 0;
            }

            public bool TryReadInt32(out int value)
            {
                if (_position + sizeof(int) > _buffer.Length)
                {
                    value = default;
                    return false;
                }
                value = MemoryMarshal.Read<int>(_buffer.Slice(_position));
                _position += sizeof(int);
                return true;
            }
        }

        // Ограничения для ref struct:
        // 1. Нельзя хранить в куче (нет поля класса, нет реализации интерфейсов)
        // 2. Нельзя использовать как параметр обобщённого типа (если T не тоже ref struct)
        // 3. Нельзя боксировать (нет преобразования в object, нет приведения к интерфейсу)
        // 4. Нельзя использовать с async/await (state machine — это класс)
        // 5. Нельзя реализовывать интерфейсы

        // Корректное использование:
        void Process(Span<byte> data)
        {
            var reader = new BufferReader(data);  // Находится на стеке
            if (reader.TryReadInt32(out int value))
                Console.WriteLine(value);
        }  // Reader уничтожается при выходе из области видимости — GC не нужен!

Когда использовать ref struct: Высокопроизводительные сценарии, где нужно гарантировать отсутствие выделения в куче и работа с Span<T>.

Модификатор required (C# 11+)

Принудительная инициализация свойства/поля на этапе компиляции:

public class Configuration
        {
            public required string ConnectionString { get; init; }
            public required int Timeout { get; init; }
            public string? OptionalSetting { get; init; }  // Не обязательное — может быть null
        }

        // Обязательно инициализировать обязательные свойства:
        var config = new Configuration
        {
            ConnectionString = "Server=localhost",  // Обязательное
            Timeout = 30                            // Обязательное
        };

        // ОШИБКА КОМПИЛЯЦИИ: отсутствует обязательное свойство
        // var badConfig = new Configuration { ConnectionString = "test" };

        // Также работает с первичными конструкторами (C# 12):
        public class Config2(
            required string ConnectionString,
            required int Timeout)
        {
            public string ConnStr => ConnectionString;
            public int Tmout => Timeout;
        }

Namespaces на уровне файла (C# 10+)

Уменьшение отступов и boilerplate-кода:

// Традиционный namespace (дополнительный уровень отступа):
        namespace MyCompany.MyProject.Services
        {
            public class UserService
            {
                // ... всё с дополнительным отступом
            }
        }

        // Namespace на уровне файла (без дополнительного отступа):
        namespace MyCompany.MyProject.Services;

        public class UserService
        {
            // Без дополнительного отступа!
        }

        // Директивы using всё ещё работают:
        using System;
        using System.Collections.Generic;

        namespace MyCompany.MyProject.Services;  // Объявление namespace после using

        public class AnotherService { }

Source Generators — базовое понимание

Что такое Source Generators?

Генерация кода во время компиляции, выполняющаяся в процессе компиляции:

Исходный код → [Анализатор + Source Generator] → Дополнительный код → Компилятор → IL

Ключевые характеристики:

  • Выполняется во время компиляции (не во время выполнения, как Reflection.Emit)
  • Генерирует исходный код C# (не IL напрямую)
  • Нулевые накладные расходы во время выполнения (сгенерированный код компилируется нормально)
  • Интеграция с IDE (IntelliSense видит сгенерированный код)

Базовая структура Source Generator

using Microsoft.CodeAnalysis;
        using Microsoft.CodeAnalysis.CSharp;
        using Microsoft.CodeAnalysis.CSharp.Syntax;
        using System.Collections.Immutable;
        using System.Linq;
        using System.Text;

        [Generator]
        public record EqualsGetHashCodeGenerator : IIncrementalSourceGenerator
        {
            public void Initialize(IncrementalGeneratorInitializationContext context)
            {
                // Шаг 1: Сбор кандидатов (классы/структуры с [GenerateEquality])
                var candidates = context.SyntaxProvider
                    .CreateSyntaxProvider(
                        predicate: static (s, c) => s is ClassDeclarationSyntax { Modifiers.Text: "public" },
                        transform: static (ctx, ct) =>
                        {
                            var classDecl = (ClassDeclarationSyntax)ctx.Node;
                            var semanticModel = ctx.SemanticModel;
                            var symbol = semanticModel.GetDeclaredSymbol(classDecl, ct);
                            return symbol?.GetAttributes()
                                .Any(a => a.AttributeClass?.Name == "GenerateEqualityAttribute") == true
                                ? classDecl 
                                : null;
                        })
                    .Where(static m => m is not null);

                // Шаг 2: Генерация кода для каждого кандидата
                context.RegisterSourceOutput(candidates, 
                    static (ctx, node) => Execute(node!, ctx));
            }

            private static void Execute(ClassDeclarationSyntax node, SourceProductionContext ctx)
            {
                var className = node.Identifier.Text;
                var namespaceName = node.Parent is NamespaceDeclarationSyntax ns ? ns.Name.ToString() : "";
        
                var source = $$"""
                    // <auto-generated/>
                    using System;
                    using System.Diagnostics.CodeAnalysis;
            
                    {{(!string.IsNullOrEmpty(namespaceName) ? $"namespace {namespaceName}" : "file")}};
            
                    public static partial class {className}Equality
                    {{"{"}}
                        public static bool Equals<{className}>({className} a, {className} b)
                        {{"{"}}
                            if (ReferenceEquals(a, b)) return true;
                            if (a is null || b is null) return false;
                            return a.Equals(b);
                        {"}"}
                    {"}"}
                    """;

                ctx.AddSource($"{className}Equality.Generated", source);
            }
        }

Инкрементальные vs обычные Source Generators

АспектISourceGeneratorIIncrementalSourceGenerator
Выполняется приПолная компиляцияТолько изменённые файлы
ПроизводительностьМедленнее (перезапуск при каждой компиляции)Быстрее (инкрементально)
СложностьПроще APIСложнее настройка
РекомендацияНаследиеИспользуйте это для новых проектов

Распространённые варианты использования

  • Автогенерация Equals/GetHashCode (до появления record)
  • Генерация мапперов DTO (как режим code-gen AutoMapper)
  • Создание команд/объектов из атрибутов (как MediatR, McMaster.Extensions.CommandLineUtils)
  • Контракты сериализации (как Source Generator System.Text.Json)
  • Регистрация контейнера внедрения зависимостей

Практика

  • [ ] Изучить материал

Упражнение 1: Паттерн Command с сопоставлением с образцом

public abstract record Command(string Id);
        public record CreateOrderCommand(string Id, string ProductId, int Quantity) : Command(Id);
        public record CancelOrderCommand(string Id, string OrderId) : Command(Id);
        public record UpdatePriceCommand(string Id, string ProductId, decimal NewPrice) : Command(Id);

        public class CommandHandler
        {
            public string Handle(Command command) => command switch
            {
                CreateOrderCommand { Quantity: < 1 } 
                    => throw new ArgumentException("Quantity must be at least 1"),
        
                CreateOrderCommand c 
                    => $"Created order for {c.ProductId} x{c.Quantity}",
        
                CancelOrderCommand c when string.IsNullOrEmpty(c.OrderId)
                    => throw new ArgumentException("OrderId is required"),
        
                CancelOrderCommand c 
                    => $"Cancelled order {c.OrderId}",
        
                UpdatePriceCommand c when c.NewPrice < 0
                    => throw new ArgumentException("Price cannot be negative"),
        
                UpdatePriceCommand c 
                    => $"Updated price of {c.ProductId} to {c.NewPrice:C}",
        
                _ => throw new NotSupportedException($"Unknown command: {command.GetType().Name}")
            };
        }

Упражнение 2: Слой DTO с типами record

// DTO запросов API
        public record CreateProductRequest(
            required string Name,
            required decimal Price,
            required string Category,
            int Stock = 0);

        public record UpdateProductRequest(
            required Guid Id,
            string? Name = null,
            decimal? Price = null,
            string? Category = null,
            int? Stock = null);

        // DTO ответов API
        public record ProductResponse(
            Guid Id,
            string Name,
            decimal Price,
            string Category,
            int Stock,
            DateTime CreatedAt);

        // Маппинг с выражением with и сопоставлением с образцом:
        public static class ProductMapper
        {
            public static ProductResponse ToResponse(Domain.Product product) =>
                new(
                    Id: product.Id,
                    Name: product.Name,
                    Price: product.Price,
                    Category: product.Category,
                    Stock: product.Stock,
                    CreatedAt: product.CreatedAt
                );

            public static Domain.Product ToDomain(CreateProductRequest request) =>
                new(
                    Id: Guid.NewGuid(),
                    Name: request.Name,
                    Price: request.Price,
                    Category: request.Category,
                    Stock: request.Stock,
                    CreatedAt: DateTime.UtcNow
                );

            // Частичное обновление с использованием паттерна with-expression:
            public static Domain.Product ApplyUpdate(Domain.Product product, UpdateProductRequest request) =>
                product with
                {
                    Name = request.Name ?? product.Name,
                    Price = request.Price ?? product.Price,
                    Category = request.Category ?? product.Category,
                    Stock = request.Stock ?? product.Stock
                };
        }

Сводная таблица

ВозможностьВерсия C#Назначение
Паттерны типов7Сопоставление и приведение типов за один шаг
Паттерны констант/null8Безопасное сопоставление конкретных значений
Паттерны свойств8Прямое сопоставление свойств объекта
Реляционные паттерны9Сравнение с <, >, <=, >=
Логические паттерны (and/or/not)9Комбинирование паттернов
Рекурсивные паттерны9Вложенное сопоставление с образцом
Паттерны списков11Сопоставление структуры массива/коллекции
switch expressions8Лаконичное присваивание значений на основе паттернов
record9Равенство по значению для ссылочных типов
Выражения with9Неразрушающее изменение
ref struct7.2Структуры только на стеке (нулевой GC)
Модификатор required11Принудительная инициализация свойств
Namespaces на уровне файла10Уменьшение отступов
Source Generators9Генерация кода во время компиляции

Span, Memory и stackalloc — программирование без выделения памяти

Содержание

  • Span — срез без выделения памяти, ограничения ref struct
  • Memory — альтернатива на основе кучи, MemoryPool
  • ArrayPool.Shared — аренда массивов
  • stackalloc — выделение на стеке, ограничения
  • ReadOnlySequence — для сетевых и файловых потоков
  • Практические упражнения

Span — срез без выделения памяти

Что такое Span?

Span<T> — это ref struct, представляющий непрерывную область памяти любого размера без выделения в куче:

public readonly ref struct Span<T>
        {
            public T this[int index] { get; set; }
            public int Length { get; }
            public bool IsEmpty { get; }
    
            public Span<T> Slice(int start);
            public Span<T> Slice(int start, int length);
            public void Clear();
            public void CopyTo(Span<T> destination);
            public T Min() => MinValue;  // .NET 7+
            public T Max() => MaxValue;  // .NET 7+
        }

Создание Span из разных источников

// Из массива — Span просматривает существующий массив (без копирования!)
        int[] array = { 1, 2, 3, 4, 5 };
        Span<int> spanFromArr = array;                    // Неявное преобразование
        Span<int> spanFromArr2 = new Span<int>(array);    // Явное

        // Из сегмента массива — представление части без выделения памяти
        Span<int> slice = array.AsSpan(1, 3);             // [2, 3, 4] — без копирования!

        // Из памяти стека — действительно нулевое выделение (вообще без кучи)
        Span<int> stackSpan = stackalloc int[100];        // 400 байт на стеке
        for (int i = 0; i < stackSpan.Length; i++)
            stackSpan[i] = i * 2;

        // Из строки — доступ к символам без выделения памяти
        string text = "Hello, World!";
        ReadOnlySpan<char> spanFromStr = text;            // Без преобразования кодировки!

        // Из IntPtr/unsafe памяти (для сценариев P/Invoke)
        unsafe
        {
            byte* ptr = ...;
            Span<byte> spanFromPtr = new Span<byte>(ptr, length);
        }

Срез без выделения памяти

Ключевое преимущество — срез создаёт представление, а не копию:

int[] data = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        // Традиционный подход — ВЫДЕЛЯЕТ новый массив для каждого среза
        int[] slice1 = data.Skip(2).Take(3).ToArray();    // Выделяет int[3] в куче
        int[] slice2 = data.Skip(5).Take(3).ToArray();    // Выделяет ещё один int[3]

        // Подход с Span — НУЛЬ выделений, только указатель + длина
        Span<int> span1 = data.AsSpan(2, 3);              // Представление: [2, 3, 4]
        Span<int> span2 = data.AsSpan(5, 3);              // Представление: [5, 6, 7]

        // Изменение span изменяет исходный массив!
        span1[0] = 99;
        Console.WriteLine(data[2]);  // 99 — та же память!

Ограничения ref struct

Поскольку Span<T> — это ref struct, у него строгие ограничения:

МожноНельзя
Использовать как локальную переменнуюХранить как поле обычного класса или структуры (только в другом ref struct)
Использовать как параметр или возвращаемый тип методаИспользовать как параметр обобщённого типа (List<Span<int>> — ОШИБКА)
Использовать в синхронных методахИспользовать в async методах или yield return итераторах
Передавать по ссылке (ref, in, out)Реализовывать интерфейсы
Хранить указатели (через ref поля с C# 11)Боксировать (приводить к object, dynamic или интерфейсу)
// КОРРЕКТНО:
        void Process(Span<int> data) { /* ... */ }

        int[] arr = new int[100];
        Process(arr.AsSpan());  // Работает!

        // НЕКОРРЕКТНО:
        public class MyClass
        {
            private Span<int> _data;  // ОШИБКА КОМПИЛЯЦИИ: ref struct не может быть полем
    
            async Task ProcessAsync(Span<int> data)  // ОШИБКА КОМПИЛЯЦИИ: нельзя использовать в async
            {
                await Something();
            }
    
            IEnumerable<Span<int>> GetSpans()  // ОШИБКА КОМПИЛЯЦИИ: не может быть обобщённым параметром
            {
                yield return new Span<int>();
            }
        }

        // ОБХОДНОЙ ПУТЬ для async: используйте Memory<T> вместо (см. следующий раздел)

Практические операции со Span

// Парсинг без выделения памяти
        public static bool TryParseInt32(ReadOnlySpan<char> text, out int result)
        {
            if (text.IsEmpty) { result = 0; return false; }
    
            bool negative = text[0] == '-';
            var digits = negative ? text.Slice(1) : text;
    
            long value = 0;
            foreach (var c in digits)
            {
                if (c < '0' || c > '9') { result = 0; return false; }
                value = value * 10 + (c - '0');
            }
    
            result = (int)(negative ? -value : value);
            return true;
        }

        // Бинарный поиск на Span
        public static int BinarySearch(Span<int> sorted, int target)
        {
            int left = 0, right = sorted.Length - 1;
            while (left <= right)
            {
                int mid = left + (right - left) / 2;
                if (sorted[mid] == target) return mid;
                if (sorted[mid] < target) left = mid + 1;
                else right = mid - 1;
            }
            return -1;
        }

        // Строковые операции без выделения памяти
        public static bool StartsWith(ReadOnlySpan<char> text, ReadOnlySpan<char> prefix)
        {
            if (text.Length < prefix.Length) return false;
            return text.Slice(0, prefix.Length).SequenceEqual(prefix);
        }

        // Разбиение строки на span (парсинг CSV без выделения памяти!)
        public static int Split(ReadOnlySpan<char> source, char separator, Span<ReadOnlySpan<char>> destination)
        {
            int count = 0;
            var start = source;
    
            while (count < destination.Length)
            {
                var index = start.IndexOf(separator);
                if (index == -1)
                {
                    destination[count++] = start;
                    break;
                }
                destination[count++] = start.Slice(0, index);
                start = start.Slice(index + 1);
            }
            return count;
        }

Memory — альтернатива Span на основе кучи

Зачем нужен Memory?

Memory<T> — это дружественный к куче эквивалент Span<T>:

ВозможностьSpanMemory
Типref structstruct (содержит указатель + длину)
Совместимость с asyncНетДа
Может быть полем классаНетДа
Может реализовывать интерфейсыН/ДДа (IMemoryOwner<T> с пулом)
Получить представление SpanН/ДСвойство .Span
Проверка перекрытия памятиИсключение времени выполненияИсключение времени выполнения

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

// Из массива — оборачивает ссылку на массив (без копирования)
        int[] array = { 1, 2, 3, 4, 5 };
        Memory<int> memory = array;                    // Неявное преобразование
        Memory<int> memory2 = new Memory<int>(array);  // Явное

        // Срез — всё ещё без выделения памяти (только смещение + длина)
        Memory<int> slice = memory.Slice(1, 3);        // [2, 3, 4]

        // Получение Span для обработки
        Span<int> span = memory.Span;                  // Преобразование в Span для операций
        span[0] *= 2;                                  // Изменяет исходный массив

        // Memory<T> в async контексте — ЭТО РАБОТАЕТ!
        public async Task ProcessAsync(Memory<byte> data)
        {
            await SomeIoOperation();
    
            // Обработка памяти после await
            var span = data.Span;
            for (int i = 0; i < span.Length; i++)
                span[i] ^= 0xFF;
        }

        // Memory<T> как поле класса — ЭТО РАБОТАЕТ!
        public class DataProcessor
        {
            private Memory<byte> _buffer;              // Корректно!
    
            public DataProcessor(int size)
            {
                _buffer = new byte[size];              // Выделяется один раз, оборачивается в Memory
            }
    
            public void Process()
            {
                var span = _buffer.Span;               // Получение представления Span для обработки
                // ... работа со span
            }
        }

IMemoryOwner — Disposable память

// Аренда памяти из пула, освобождение по завершении
        using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024);
        Memory<byte> memory = owner.Memory;            // Арендованная память
        Span<byte> span = memory.Span;                 // Обработка

        // При выходе из блока 'using' память автоматически возвращается в пул

MemoryPool — эффективное управление большими буферами

// Общий пул (глобальный, потокобезопасный)
        using IMemoryOwner<byte> owner1 = MemoryPool<byte>.Shared.Rent(1024);
        Span<byte> buffer1 = owner1.Memory.Span;

        // Аренда большего буфера (пул округляет до степени 2: 1024 -> 1024, 1500 -> 2048)
        using IMemoryOwner<byte> owner2 = MemoryPool<byte>.Shared.Rent(1500);
        Console.WriteLine(owner2.Memory.Length);  // >= 1500 (вероятно 2048 или 4096)

        // Пользовательский пул с определённым размером блока
        var pool = new MemoryPool<byte>(
            maxRetainedBlocks: 10,
            blockSize: 4096,          // Каждый блок 4KB
            maxBlockSize: 65536       // Максимальный размер блока (64KB)
        );

        using var owner = pool.Rent(2000);  // Получает один блок 4KB
        // ... использование owner.Memory.Span ...
        // Возврат в пул при освобождении — GC не нужен!

Преимущества MemoryPool:

  • Снижает нагрузку на GC за счёт повторного использования буферов
  • Пре-аллоцированные блоки избегают задержек выделения памяти
  • Идеально для операций ввода/вывода (сеть, обработка файлов)

ArrayPool.Shared — аренда массивов

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

// Аренда массива из пула
        int[] rented = ArrayPool<int>.Shared.Rent(100);  
        Console.WriteLine(rented.Length);  // >= 100 (пул округляет: вероятно 128, 256 или 512)

        try
        {
            // Использование массива обычным образом
            for (int i = 0; i < 100; i++)
                rented[i] = i * 2;
    
            Process(rented.AsSpan(0, 100));  // Используйте только нужную часть
        }
        finally
        {
            // КРИТИЧНО: Вернуть массив в пул по завершении!
            ArrayPool<int>.Shared.Return(rented);
    
            // Или с флагом очистки (для конфиденциальных данных):
            // ArrayPool<int>.Shared.Return(rented, clearArray: true);
        }

Паттерн using (предпочтительный)

// Обёртка в IDisposable для автоматического возврата
        public struct ArrayPoolRent<T> : IDisposable
        {
            private readonly T[] _array;
            private readonly ArrayPool<T> _pool;
    
            public ArrayPoolRent(ArrayPool<T> pool, int minimumLength)
            {
                _pool = pool;
                _array = pool.Rent(minimumLength);
            }
    
            public Span<T> Value => _array;
    
            public void Dispose() => _pool.Return(_array);
        }

        // Использование:
        using var rent = new ArrayPoolRent<byte>(ArrayPool<byte>.Shared, 1024);
        Span<byte> buffer = rent.Value;
        // ... обработка ...
        // Автоматически возвращается при выходе из области видимости!

Когда использовать ArrayPool vs новый массив

СценарийРекомендацияПричина
Одноразовое небольшое выделение (< 1KB)new T[n]Накладные расходы пула не оправданы
Повторные большие выделения (> 4KB)ArrayPool<T>Значительная экономия GC
Критический участок с частым выделением/освобождениемArrayPool<T>Устраняет нагрузку на GC
Буфер для операций ввода/выводаArrayPool<T> или MemoryPool<T>Стандартный паттерн в .NET I/O
Данные должны быть обнулены после использованияReturn(array, clearArray: true)Конфиденциальные данные

stackalloc — выделение на стеке

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

// Выделение на стеке (без GC!)
        Span<byte> buffer = stackalloc byte[1024];     // 1KB на стеке
        Span<int> numbers = stackalloc int[100];       // 400 байт на стеке

        // Инициализация значениями (C# 7.3+)
        Span<int> initialized = stackalloc int[5] { 1, 2, 3, 4, 5 };

        // Очистка перед использованием (память стека НЕ обнуляется!)
        Span<byte> dirty = stackalloc byte[100];
        dirty.Clear();  // Обязательно: содержимое стека не определено!

        // Динамический размер (определяется во время выполнения)
        int length = GetLength();
        Span<char> dynamicBuffer = stackalloc char[length];

Ограничения

ОграничениеДетали
Размер должен быть int во время компиляции или выполненияНельзя использовать размеры long
Максимальный практический размер~1MB (лимит стека обычно 1-8MB)
Нельзя напрямую выделять ссылочные типыТолько значимые типы (byte, int, структуры)
Время жизни памяти = область видимости методаНедействительно после возврата из метода
Нельзя возвращать из методаИспользуйте параметр Span<T> или возврат Span<T> в структуре
Нельзя использовать с asyncФрейм стека может не существовать после await

stackalloc с ref Returns (C# 7.2+)

// Возврат ссылки на память, выделенную на стеке — ОПАСНО!
        // Безопасно только когда вызыватель использует её в том же вызове:
        public ref int GetFirstElement(Span<int> data)
        {
            return ref data[0];  // Возвращает ссылку в Span вызывающего
        }

        // Безопасный паттерн: обработка в области видимости метода
        public int ProcessSmallArray(int[] source, int count)
        {
            Span<int> temp = stackalloc int[Math.Min(count, 1024)];
    
            for (int i = 0; i < Math.Min(count, temp.Length); i++)
                temp[i] = source[i] * 2;
    
            int sum = 0;
            foreach (var x in temp)
                sum += x;
            return sum;  // Возврат вычисленного значения, а не ссылки на память стека!
        }

considerations размера стека

// Размеры стека потоков по умолчанию:
        // - Windows: 1 MB на поток (32-бит), 4 MB (64-бит)
        // - Потоки ThreadPool .NET: настраиваются через <Thread_UseAllKindsOfCpus/>

        // Безопасные паттерны использования:
        Span<byte> small = stackalloc byte[4096];     // ✅ 4KB — безопасно
        Span<int> medium = stackalloc int[1000];      // ✅ 4KB — безопасно  
        Span<double> large = stackalloc double[100000]; // ⚠️ 800KB — рискованно на стеке по умолчанию

        // Для больших буферов используйте ArrayPool или MemoryPool:
        using var owner = MemoryPool<byte>.Shared.Rent(1_000_000);  // ✅ Безопасно для больших данных

ReadOnlySequence — discontinuous буферы

Проблема, которую Span не может решить

Span<T> требует непрерывной памяти. Но сетевой и файловый ввод/вывод часто создают фрагментированные буферы:

Сетевой приём: [chunk1][gap][chunk2][gap][chunk3]
                            ^^^^^^^^              ^^^^^^^^
                            Не непрерывно!         Нельзя использовать один Span<T>

Решение ReadOnlySequence

// Представляет последовательность блоков памяти (возможно, не непрерывных)
        ReadOnlySequence<byte> sequence = ...;  // Из сетевого потока, файлового читателя и т.д.

        // Перебор всех данных по фрагментам:
        foreach (var segment in sequence)
        {
            Span<byte> span = segment.Span;     // Каждый сегмент — непрерывный Memory<T>
            ProcessChunk(span);
        }

        // Получение позиции для отслеживания прогресса чтения:
        SequencePosition position = sequence.Start;
        while (sequence.TryGet(ref position, out Memory<byte> chunk, isEnd: false))
        {
            Process(chunk.Span);
        }

        // Проверка, являются ли данные непрерывными (один блок):
        if (sequence.IsSingleSegment)
        {
            Span<byte> allData = sequence.First.Span;  // Прямой доступ!
        }

        // Срез по сегментам (представление части без копирования):
        ReadOnlySequence<byte> slice = sequence.Slice(10, 20);  // Байты 10-29 по фрагментам

        // Преобразование в непрерывный массив (ВЫДЕЛЯЕТ память — используйте редко):
        byte[] contiguous = sequence.ToArray();  // Только когда действительно нужно всё сразу

Практический пример: парсер сетевого протокола

public class HttpParser
        {
            private ReadOnlySequence<byte> _buffer;
    
            public ParseResult TryParseRequest(ReadOnlySequence<byte> input)
            {
                _buffer = input;
        
                // Поиск терминатора строки HTTP-запроса (\r\n\r\n)
                var terminator = new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };
                var consumed = FindTerminator(terminator);
        
                if (consumed == null)
                    return ParseResult.Incomplete;  // Нужно больше данных
        
                var headerSection = _buffer.Slice(0, *consumed);
                var bodyStart = _buffer.GetPosition(*consumed, _buffer.Start);
        
                // Парсинг заголовков из span (может охватывать несколько сегментов!)
                ParseHeaders(headerSection);
        
                return ParseResult.Complete(bodyStart);
            }
    
            private int? FindTerminator(ReadOnlySpan<byte> terminator)
            {
                foreach (var segment in _buffer)
                {
                    var index = segment.Span.IndexOf(terminator);
                    if (index != -1)
                        return (int)_buffer.GetPosition(index, segment.Start).Value;
                }
                return null;  // Терминатор не найден — нужно больше данных
            }
        }

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

СценарийИспользуйте
Обработка сетевого потока✅ Основной вариант использования
Потоковое чтение файлов (большие файлы)✅ Когда файл может быть фрагментирован
Парсинг протоколов (HTTP, gRPC и т.д.)✅ Стандартный паттерн
Непрерывные данные в памяти❌ Используйте Span вместо этого
Небольшие буферы фиксированного размера❌ Накладные расходы не оправданы

Практика

  • [ ] Изучить материал

Упражнение 1: Парсер CSV без выделения памяти с Span

public ref struct CsvParser
        {
            private readonly ReadOnlySpan<char> _data;
            private int _position;
    
            public CsvParser(ReadOnlySpan<char> data)
            {
                _data = data;
                _position = 0;
            }
    
            public bool TryReadRow(Span<ReadOnlySpan<char>> fields, int maxFields)
            {
                int fieldCount = 0;
        
                while (_position < _data.Length && fieldCount < maxFields)
                {
                    var remaining = _data.Slice(_position);
                    var commaIndex = remaining.IndexOf(',');
                    var newlineIndex = remaining.IndexOf('\n');
            
                    int end;
                    if (commaIndex != -1 && (newlineIndex == -1 || commaIndex < newlineIndex))
                    {
                        fields[fieldCount++] = remaining.Slice(0, commaIndex);
                        _position += commaIndex + 1;
                    }
                    else if (newlineIndex != -1)
                    {
                        fields[fieldCount++] = remaining.Slice(0, newlineIndex);
                        _position += newlineIndex + 1;
                        break;  // Конец строки
                    }
                    else
                    {
                        fields[fieldCount++] = remaining;
                        _position = _data.Length;
                        break;  // Последнее поле без новой строки
                    }
                }
        
                return fieldCount > 0;
            }
        }

        // Использование — НУЛЬ выделений для всей обработки CSV:
        void ProcessCsv(ReadOnlySpan<char> csvData)
        {
            var parser = new CsvParser(csvData);
    
            // На практике используйте массив span фиксированного размера:
            Span<ReadOnlySpan<char>> fields = stackalloc ReadOnlySpan<char>[100];
    
            while (parser.TryReadRow(fields, 100))
            {
                foreach (var field in fields[..GetActualFieldCount()])
                    Process(field.Trim());
            }
        }

Упражнение 2: Бенчмарк — манипуляция строками с Span и без

using System.Diagnostics;
        using BenchmarkDotNet.Attributes;

        [MemoryDiagnoser]
        public class StringManipulationBenchmark
        {
            private string _input = new string('a', 10000);
    
            [Benchmark(Baseline = true)]
            public int CountCharTraditional()
            {
                int count = 0;
                for (int i = 0; i < _input.Length; i++)
                    if (_input[i] == 'a') count++;
                return count;
            }
    
            [Benchmark]
            public int CountCharWithSpan()
            {
                ReadOnlySpan<char> span = _input;
                int count = 0;
                foreach (var c in span)
                    if (c == 'a') count++;
                return count;
            }
    
            [Benchmark]
            public string UpperTraditional() => _input.ToUpper();  // Выделяет новую строку
    
            [Benchmark]
            public int UpperWithSpan()
            {
                Span<char> dest = stackalloc char[_input.Length];
                ReadOnlySpan<char> src = _input;
        
                for (int i = 0; i < src.Length; i++)
                    dest[i] = char.ToUpper(src[i]);
        
                return dest.Length;  // Без выделения строки!
            }
        }

Упражнение 3: Оптимизация нагрузки на GC с MemoryPool

public class OptimizedJsonSerializer
        {
            private static readonly MemoryPool<byte> _pool = MemoryPool<byte>.Shared;
    
            public byte[] Serialize(object obj)
            {
                // Аренда буфера из пула вместо выделения нового каждый раз
                using var owner = _pool.Rent(EstimateSize(obj));
                Span<byte> buffer = owner.Memory.Span;
        
                int written = WriteJson(obj, buffer);  // Пользовательская сериализация
        
                // Копирование результата (только использованную часть) в возвращаемый массив
                byte[] result = new byte[written];
                buffer.Slice(0, written).CopyTo(result);
                return result;
            }
    
            private int WriteJson(object obj, Span<byte> buffer)
            {
                // ... логика сериализации, записывающая в span ...
                // Возвращает количество записанных байт
                throw new NotImplementedException();
            }
    
            private int EstimateSize(object obj) => 4096;  // Эвристика
        }

Сводная таблица: Выбор правильного инструмента

ПотребностьИспользуйтеПочему
Обработка части массива без копированияSpan<T> из массиваПредставление без выделения памяти
Небольшой фиксированный буфер, без GCstackalloc + Span<T>Память стека, автоочистка
Буфер в async методеMemory<T>Не ref struct
Буфер как поле классаMemory<T>Можно хранить в классе
Повторное использование больших буферовArrayPool<T> или MemoryPool<T>Снижает нагрузку на GC
Обработка фрагментированных данных ввода/выводаReadOnlySequence<T>Обрабатывает не непрерывную память
Парсинг строк без выделения памятиReadOnlySpan<char> из строкиНе нужно преобразование кодировки
Возврат буфера из методаIMemoryOwner<T> или массивSpan нельзя вернуть напрямую

Быстрая справка: сравнение выделений памяти

// ВЫДЕЛЯЕТ в куче (нагрузка на GC):
        byte[] arr = new byte[1024];           // ~1KB выделение GC
        string s = "hello".ToUpper();          // Новое выделение строки
        var list = new List<int>();            // Выделение List + внутреннего массива

        // НУЛЬ выделения в куче:
        Span<byte> span = stackalloc byte[1024];     // Только стек
        ReadOnlySpan<char> rspan = someString;       // Представление существующей строки
        Memory<byte> mem = ArrayPool<byte>.Shared.Rent(1024);  // Пул (не GC)

Рефлексия и Emit в C#/.NET

Содержание

  • System.Reflection — Type, MemberInfo, MethodInfo, PropertyInfo
  • Стоимость рефлексии — кэширование, MethodInfo.CreateDelegate()
  • System.Reflection.Emit — динамическая генерация кода
  • Практические упражнения: быстрый маппер объектов, DI-контейнер, динамический прокси

System.Reflection — основные типы

Обнаружение типов и интроспекция

// Получение объектов Type
        Type t1 = typeof(int);                          // Тип, известный на этапе компиляции
        Type t2 = typeof(List<string>);                 // Обобщённый тип
        Type t3 = "System.String".GetType();            // Имя типа во время выполнения (не рекомендуется)
        Type t4 = someObject.GetType();                 // Тип экземпляра во время выполнения

        // Информация о типе
        Console.WriteLine(t1.Name);          // "Int32"
        Console.WriteLine(t1.FullName);      // "System.Int32"
        Console.WriteLine(t1.Namespace);     // "System"
        Console.WriteLine(t1.Assembly);      // "System.Private.CoreLib, ..."
        Console.WriteLine(t1.IsClass);       // false (это структура)
        Console.WriteLine(t1.IsValueType);   // true
        Console.WriteLine(t1.IsGenericType); // false

        // Анализ обобщённого типа
        Type listType = typeof(List<>);                    // Открытый обобщённый тип
        Console.WriteLine(listType.IsGenericTypeDefinition);  // true

        Type constructedList = typeof(List<string>);       // Построенный обобщённый тип
        Console.WriteLine(constructedList.IsGenericType);   // true
        Console.WriteLine(constructedList.IsGenericTypeDefinition);  // false
        Type[] typeArgs = constructedList.GetGenericArguments();  // [System.String]

MemberInfo — доступ к членам

public class SampleClass
        {
            public string PublicProperty { get; set; }
            private int _privateField;
    
            public void PublicMethod() { }
            private void PrivateMethod() { }
        }

        Type type = typeof(SampleClass);

        // Получение всех свойств (включая унаследованные)
        PropertyInfo[] props = type.GetProperties(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        // Получение конкретного свойства
        PropertyInfo prop = type.GetProperty("PublicProperty", 
            BindingFlags.Instance | BindingFlags.Public);

        // Получение всех методов
        MethodInfo[] methods = type.GetMethods(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        // Получение конкретного метода (с типами параметров для перегрузок)
        MethodInfo method = type.GetMethod("PublicMethod");
        MethodInfo overload = type.GetMethod("MethodName", new[] { typeof(int), typeof(string) });

        // Получение всех полей
        FieldInfo[] fields = type.GetFields(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        // Получение конструкторов
        ConstructorInfo[] ctors = type.GetConstructors();

PropertyInfo — чтение и запись свойств

public class Person
        {
            public string Name { get; set; }
            public int Age { get; private set; }
        }

        var person = new Person { Name = "Alice", Age = 30 };
        Type type = typeof(Person);

        // Чтение значения свойства
        PropertyInfo nameProp = type.GetProperty("Name");
        string name = (string)nameProp.GetValue(person);           // "Alice"
        string name2 = (string)nameProp.GetValue(person, null);    // Аргументы индексатора для индексированных свойств

        // Запись значения свойства
        nameProp.SetValue(person, "Bob", null);                    // person.Name = "Bob"

        // Проверка доступности
        Console.WriteLine(nameProp.CanWrite);                      // true
        var ageProp = type.GetProperty("Age");
        Console.WriteLine(ageProp.CanWrite);                       // false (приватный сеттер)

        // Получение атрибутов свойства
        object[] attrs = nameProp.GetCustomAttributes(false);

MethodInfo — динамический вызов методов

public class Calculator
        {
            public int Add(int a, int b) => a + b;
            public static string Greet(string name) => $"Hello, {name}!";
        }

        Type calcType = typeof(Calculator);

        // Вызов экземпляра метода
        var calculator = new Calculator();
        MethodInfo addMethod = calcType.GetMethod("Add");
        object result = addMethod.Invoke(calculator, new object[] { 3, 4 });  // 7

        // Вызов статического метода (передайте null для экземпляра)
        MethodInfo greetMethod = calcType.GetMethod("Greet", 
            BindingFlags.Static | BindingFlags.Public);
        string greeting = (string)greetMethod.Invoke(null, new object[] { "World" });  // "Hello, World!"

        // Вызов с необязательными параметрами
        MethodInfo methodWithOptional = calcType.GetMethod("SomeMethod");
        object optResult = methodWithOptional.Invoke(instance, 
            bindingAttr: BindingFlags.Default, 
            binder: null, 
            parameters: new object[] { "arg1", Type.Missing },  // Пропуск необязательного параметра
            culture: null);

        // Получение типа возвращаемого значения и параметров метода
        Console.WriteLine(addMethod.ReturnType);           // System.Int32
        ParameterInfo[] @params = addMethod.GetParameters(); // [int a, int b]

BindingFlags — управление поиском членов

// Распространённые комбинации:
        BindingFlags flags = BindingFlags.Instance | BindingFlags.Public;        // Публичные члены экземпляра
        flags |= BindingFlags.NonPublic;                                          // + Приватные/protected/internal
        flags |= BindingFlags.Static;                                             // + Статические члены
        flags |= BindingFlags.DeclaredOnly;                                       // Только объявленные на этом типе (не унаследованные)

        // Типичное использование:
        var allMembers = type.GetMembers(
            BindingFlags.Instance | 
            BindingFlags.Static | 
            BindingFlags.Public | 
            BindingFlags.NonPublic);

        var publicInstanceProps = type.GetProperties(
            BindingFlags.Instance | BindingFlags.Public);

        var privateFields = type.GetFields(
            BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);

Стоимость рефлексии — кэширование и CreateDelegate

Проблема: рефлексия медленная

using System.Diagnostics;

        public class ReflectionBenchmark
        {
            private readonly Person _person = new() { Name = "Alice", Age = 30 };
            private readonly Type _type = typeof(Person);
    
            // Прямой доступ — базовый уровень (~1нс)
            public string DirectAccess() => _person.Name;
    
            // Рефлексия без кэширования — очень медленно (~100-500нс на вызов)
            public string ReflectionNoCache()
            {
                var prop = typeof(Person).GetProperty("Name");  // Поиск каждый раз!
                return (string)prop.GetValue(_person);
            }
    
            // Кэшированный PropertyInfo — быстрее (~20-50нс)
            private static readonly PropertyInfo _nameProp = 
                typeof(Person).GetProperty("Name")!;
    
            public string ReflectionCached() => (string)_nameProp.GetValue(_person);
    
            // Скомпилированный делегат — скорость, близкая к прямой (~5-10нс)
            private static readonly Func<Person, string> _getName = 
                CreateGetter<Person, string>("Name");
    
            public string DelegateAccess() => _getName(_person);
        }

Сравнение производительности

МетодВремя на вызовВыделено памятиПримечания
Прямой доступ к свойству~1нс0 ББазовый уровень
PropertyInfo.GetValue (кэшированный)~20-50нс0 БВсё ещё медленно для значимых типов (боксинг)
Delegate.CreateDelegate~5-10нс0 БСкорость, близкая к прямой
Expression.Compile getter~5-10нс0 БТо же, что и CreateDelegate
Рефлексия без кэша~100-500нсварьируетсяВключает накладные расходы поиска члена

Кэширование PropertyInfo/MethodInfo

public static class ReflectionCache
        {
            private static readonly ConditionalWeakTable<Type, Dictionary<string, PropertyInfo>> 
                _propertyCache = new();
    
            public static PropertyInfo GetCachedProperty(Type type, string propertyName)
            {
                var dict = _propertyCache.GetOrCreateValue(type);
                lock (dict)
                {
                    if (!dict.TryGetValue(propertyName, out var prop))
                    {
                        prop = type.GetProperty(propertyName, 
                            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
                        dict[propertyName] = prop;
                    }
                    return prop!;
                }
            }
        }

        // Использование:
        var prop = ReflectionCache.GetCachedProperty(typeof(Person), "Name");
        string value = (string)prop.GetValue(person);  // Быстро после первого вызова на тип/свойство

MethodInfo.CreateDelegate() — быстрый динамический вызов

public static class DelegateFactory
        {
            // Создание быстрого делегата-геттера для любого свойства
            public static Func<T, TResult> CreateGetter<T, TResult>(string propertyName)
            {
                var prop = typeof(T).GetProperty(propertyName, 
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        
                if (prop == null || prop.PropertyType != typeof(TResult))
                    throw new ArgumentException($"Property '{propertyName}' of type TResult not found on {typeof(T)}");
        
                // Создание делегата открытого экземпляра — без выделения замыкания!
                var getter = prop.GetMethod ?? throw new InvalidOperationException("No getter");
                return (Func<T, TResult>)Delegate.CreateDelegate(
                    typeof(Func<T, TResult>), getter);
            }
    
            // Создание быстрого делегата-сеттера для любого свойства
            public static Action<T, TValue> CreateSetter<T, TValue>(string propertyName)
            {
                var prop = typeof(T).GetProperty(propertyName, 
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        
                if (prop == null)
                    throw new ArgumentException($"Property '{propertyName}' not found");
        
                var setter = prop.SetMethod ?? throw new InvalidOperationException("No setter");
                return (Action<T, TValue>)Delegate.CreateDelegate(
                    typeof(Action<T, TValue>), setter);
            }
    
            // Создание делегата для статического метода
            public static Func<TResult> CreateStaticGetter<TType, TResult>(string methodName)
            {
                var method = typeof(TType).GetMethod(methodName, 
                    BindingFlags.Static | BindingFlags.Public);
        
                return (Func<TResult>)Delegate.CreateDelegate(
                    typeof(Func<TResult>), method);
            }
        }

        // Использование:
        var getName = DelegateFactory.CreateGetter<Person, string>("Name");
        var setAge = DelegateFactory.CreateSetter<Person, int>("Age");

        string name = getName(person);     // ~5нс — скорость, близкая к прямому доступу!
        setAge(person, 31);                // Быстрая динамическая установка свойства

Альтернатива Expression.Compile()

public static Func<T, TResult> CompileGetter<T, TResult>(Expression<Func<T, TResult>> expr)
        {
            return expr.Compile();  // Генерирует оптимизированный IL-делегат
        }

        // Использование:
        var getter = CompileGetter<Person, string>(p => p.Name);
        string name = getter(person);  // ~5нс — та же скорость, что и CreateDelegate

System.Reflection.Emit — динамическая генерация кода

Что такое Reflection.Emit?

System.Reflection.Emit позволяет генерировать новые типы и методы во время выполнения, испуская инструкции IL (Intermediate Language):

Определение сборки → Определение модуля → Определение типа → Определение методов → Генерация IL → CreateType()

Базовый пример: создание динамического типа

using System.Reflection;
        using System.Reflection.Emit;
        using System.Collections.ObjectModel;

        // Шаг 1: Создание динамической сборки
        AssemblyName asmName = new("DynamicAssembly");
        AssemblyBuilder asmBuilder = AssemblyBuilder.DefineDynamicAssembly(
            asmName, AssemblyBuilderAccess.Run);

        ModuleBuilder module = asmBuilder.DefineDynamicModule("DynamicModule");

        // Шаг 2: Определение типа
        TypeBuilder typeBuilder = module.DefineType(
            "DynamicGreeter", 
            TypeAttributes.Public | TypeAttributes.Class);

        // Шаг 3: Добавление поля
        FieldBuilder nameField = typeBuilder.DefineField("_name", typeof(string), 
            FieldAttributes.Private);

        // Шаг 4: Добавление конструктора
        ConstructorBuilder ctor = typeBuilder.DefineConstructor(
            MethodAttributes.Public, 
            CallingConventions.Standard, 
            new[] { typeof(string) });

        ILGenerator ctorIl = ctor.GetILGenerator();
        ctorIl.Emit(OpCodes.Ldarg_0);              // Загрузка 'this'
        ctorIl.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyArray)!);  // Вызов base()
        ctorIl.Emit(OpCodes.Ldarg_0);              // Загрузка 'this'
        ctorIl.Emit(OpCodes.Ldarg_1);              // Загрузка параметра 'name'
        ctorIl.Emit(OpCodes.Stfld, nameField);     // Сохранение в поле _name
        ctorIl.Emit(OpCodes.Ret);                  // Возврат

        // Шаг 5: Добавление метода
        MethodBuilder greetMethod = typeBuilder.DefineMethod(
            "Greet", 
            MethodAttributes.Public | MethodAttributes.Static,
            typeof(string), 
            new[] { typeof(string) });

        ILGenerator methodIl = greetMethod.GetILGenerator();
        methodIl.Emit(OpCodes.Ldstr, "Hello, ");   // Push строкового литерала
        methodIl.Emit(OpCodes.Ldarg_0);            // Загрузка параметра 'name'
        methodIl.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", 
            new[] { typeof(string), typeof(string) })!);  // Вызов string.Concat
        methodIl.Emit(OpCodes.Ret);

        // Шаг 6: Создание типа
        Type greeterType = typeBuilder.CreateType();

        // Использование динамического типа!
        var instance = Activator.CreateInstance(greeterType, "World");
        string result = (string)greeterType.GetMethod("Greet")!.Invoke(null, new[] { "Alice" });
        Console.WriteLine(result);  // "Hello, Alice"

Справочник распространённых IL-опкодов

ОпкодОписаниеЭффект на стеке
Ldarg_0Ldarg_3Загрузка аргумента 0-3push arg
Ldarg_S byteЗагрузка короткого аргументаpush arg
Ldarga / Ldarga_SЗагрузка адреса аргументаpush &arg
Ldloc_0Ldloc_3Загрузка локальной переменной 0-3push var
Stloc_0Stloc_3Сохранение в локальную переменную 0-3pop val
Ldc_I4 int32Push 32-битной целочисленной константыpush int
Ldc_I8 int64Push 64-битной целочисленной константыpush long
Ldc_R4 float32Push 32-битной float константыpush float
Ldc_R8 float64Push 64-битной double константыpush double
Ldstr stringPush строкового литералаpush string
Ldfld FieldInfoЗагрузка значения поля из объектаpop obj, push val
Stfld FieldInfoСохранение значения в поле объектаpop val, pop obj
Ldsfld FieldInfoЗагрузка значения статического поляpush val
Stsfld FieldInfoСохранение значения в статическое полеpop val
Call MethodInfoВызов метода (виртуальный/статический)pop args, push ret
Callvirt MethodInfoВызов виртуального метода (с проверкой null)pop obj+args, push ret
Newobj ConstructorInfoСоздание нового объектаpop args, push obj
RetВозврат из метода
Br targetБезусловный переход
Brfalse targetПереход, если ноль/falsepop val
Brtrue targetПереход, если не ноль/truepop val

Генерация обобщённого типа с Emit

// Определение обобщённого типа: public class Cache<TKey, TValue> where TKey : notnull
        TypeBuilder cacheType = module.DefineType(
            "Cache`2", 
            TypeAttributes.Public | TypeAttributes.Class,
            baseType: typeof(object),
            generics: new[] { typeof(TKey).GetGenericArguments()[0], /* ... */ });

        // На самом деле, для параметров обобщённого типа:
        GenericTypeParameterBuilder tKey = cacheType.DefineGenericParameter(
            "TKey", GenericParameterAttributes.None);
        tKey.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

        GenericTypeParameterBuilder tValue = cacheType.DefineGenericParameter(
            "TValue", GenericParameterAttributes.None);

        // Добавление ограничений: where TKey : notnull
        tKey.SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);

Практика

  • [ ] Изучить материал

Упражнение 1: Быстрый маппер объектов с рефлексией + кэшированием делегатов

public class FastObjectMapper
        {
            private static readonly ConditionalWeakTable<Type, PropertyMap> _cache = new();
    
            public class PropertyMap
            {
                public string[] PropertyNames;
                public Func<object, object>[] Getters;
                public Action<object, object>[] Setters;
            }
    
            public TDestination Map<TSource, TDestination>(TSource source)
            {
                var dest = Activator.CreateInstance<TDestination>();
        
                var srcType = typeof(TSource);
                var dstType = typeof(TDestination);
        
                // Получение или создание карты свойств
                var map = GetOrCreateMap(srcType, dstType);
        
                for (int i = 0; i < map.PropertyNames.Length; i++)
                {
                    object value = map.Getters[i](source);
                    if (value != null)
                        map.Setters[i](dest, value);
                }
        
                return dest;
            }
    
            private PropertyMap GetOrCreateMap(Type srcType, Type dstType)
            {
                var cacheKey = Tuple.Create(srcType, dstType);
        
                // Упрощено — в продакшене используйте составной ключ
                var srcMap = _cache.GetOrCreateValue(srcType);
                // ... полная реализация кэшировала бы по паре (src, dst)
        
                throw new NotImplementedException("Требуется полная реализация кэширования");
            }
        }

        // Более простая версия с прямым созданием делегатов:
        public static class SimpleMapper
        {
            public static TDestination Map<TSource, TDestination>(TSource source) 
                where TSource : class where TDestination : class, new()
            {
                var dest = new TDestination();
        
                foreach (var srcProp in typeof(TSource).GetProperties(BindingFlags.Instance | BindingFlags.Public))
                {
                    var dstProp = typeof(TDestination).GetProperty(srcProp.Name, srcProp.PropertyType);
                    if (dstProp != null && dstProp.CanWrite)
                    {
                        var value = srcProp.GetValue(source);
                        dstProp.SetValue(dest, value);
                    }
                }
        
                return dest;
            }
        }

Упражнение 2: Простой DI-контейнер с разрешением на основе рефлексии

public interface IServiceContainer : IDisposable
        {
            TService Resolve<TService>() where TService : notnull;
            object Resolve(Type serviceType);
            void Register<TService, TImplementation>() 
                where TImplementation : class, TService;
            void RegisterInstance<TService>(TService instance) where TService : notnull;
        }

        public class SimpleContainer : IServiceContainer
        {
            private readonly Dictionary<Type, ServiceRegistration> _registrations = new();
    
            enum ServiceLifetime { Transient, Singleton }
    
            class ServiceRegistration
            {
                public Type ImplementationType;
                public object Instance;  // Для регистрации экземпляра
                public object SingletonInstance;  // Кэшированный синглтон
                public ServiceLifetime Lifetime;
                public Func<IServiceContainer, object> Factory;
            }
    
            public void Register<TService, TImplementation>() 
                where TImplementation : class, TService
            {
                _registrations[typeof(TService)] = new ServiceRegistration
                {
                    ImplementationType = typeof(TImplementation),
                    Lifetime = ServiceLifetime.Transient
                };
            }
    
            public void RegisterInstance<TService>(TService instance) where TService : notnull
            {
                _registrations[typeof(TService)] = new ServiceRegistration
                {
                    Instance = instance,
                    Lifetime = ServiceLifetime.Singleton
                };
            }
    
            public TService Resolve<TService>() where TService : notnull
            {
                return (TService)Resolve(typeof(TService));
            }
    
            public object Resolve(Type serviceType)
            {
                if (!_registrations.TryGetValue(serviceType, out var registration))
                    throw new InvalidOperationException($"No registration for {serviceType}");
        
                // Регистрация экземпляра — возврат напрямую
                if (registration.Instance != null)
                    return registration.Instance;
        
                // Синглтон — создание один раз и кэширование
                if (registration.Lifetime == ServiceLifetime.Singleton)
                {
                    if (registration.SingletonInstance == null)
                        registration.SingletonInstance = CreateInstance(registration, serviceType);
                    return registration.SingletonInstance;
                }
        
                // Transient — создание нового каждый раз
                return CreateInstance(registration, serviceType);
            }
    
            private object CreateInstance(ServiceRegistration reg, Type serviceType)
            {
                if (reg.Factory != null)
                    return reg.Factory(this);
        
                var implType = reg.ImplementationType ?? serviceType;
        
                // Поиск конструктора с наибольшим количеством параметров, которые можно разрешить
                var constructors = implType.GetConstructors(
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        
                ConstructorInfo bestCtor = null;
                int maxParams = -1;
        
                foreach (ConstructorInfo ctor in constructors)
                {
                    var parameters = ctor.GetParameters();
                    bool canResolve = true;
            
                    foreach (var param in parameters)
                    {
                        if (!_registrations.ContainsKey(param.ParameterType))
                        {
                            canResolve = false;
                            break;
                        }
                    }
            
                    if (canResolve && parameters.Length > maxParams)
                    {
                        bestCtor = ctor;
                        maxParams = parameters.Length;
                    }
                }
        
                if (bestCtor == null)
                    throw new InvalidOperationException($"Cannot resolve constructor for {implType}");
        
                // Рекурсивное разрешение параметров конструктора
                var parameters = bestCtor.GetParameters();
                object[] resolvedParams = new object[parameters.Length];
        
                for (int i = 0; i < parameters.Length; i++)
                    resolvedParams[i] = Resolve(parameters[i].ParameterType);
        
                return bestCtor.Invoke(resolvedParams);
            }
    
            public void Dispose() { /* Очистка disposables */ }
        }

        // Использование:
        var container = new SimpleContainer();
        container.Register<IUserService, UserService>();
        container.Register<IRepository, UserRepository>();

        var service = container.Resolve<IUserService>();  // Создаёт UserService с внедрёнными зависимостями

Упражнение 3: Динамический прокси для AOP (паттерн интерцептора)

public class DynamicProxyGenerator
        {
            public static T CreateProxy<T>(IInterceptor interceptor) where T : class
            {
                var interfaceType = typeof(T);
        
                if (!interfaceType.IsInterface)
                    throw new ArgumentException("Можно создавать прокси только для интерфейсов");
        
                // Создание динамической сборки
                var asmName = new AssemblyName($"Proxy_{interfaceType.Name}");
                var assembly = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
                var module = assembly.DefineDynamicModule("ProxyModule");
        
                // Определение типа прокси, реализующего интерфейс
                var proxyType = module.DefineType(
                    $"Proxy_{interfaceType.Name}", 
                    TypeAttributes.Public | TypeAttributes.Class,
                    typeof(object),
                    new[] { interfaceType });
        
                // Добавление поля интерцептора
                var interceptorField = proxyType.DefineField(
                    "_interceptor", typeof(IInterceptor), FieldAttributes.Private);
        
                // Добавление конструктора
                var ctor = proxyType.DefineConstructor(
                    MethodAttributes.Public, 
                    CallingConventions.Standard, 
                    new[] { typeof(IInterceptor) });
        
                var ctorIl = ctor.GetILGenerator();
                ctorIl.Emit(OpCodes.Ldarg_0);
                ctorIl.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyArray));
                ctorIl.Emit(OpCodes.Ldarg_0);
                ctorIl.Emit(OpCodes.Ldarg_1);
                ctorIl.Emit(OpCodes.Stfld, interceptorField);
                ctorIl.Emit(OpCodes.Ret);
        
                // Генерация прокси-методов для каждого метода интерфейса
                foreach (MethodInfo method in interfaceType.GetMethods())
                {
                    if (method.IsSpecialName && 
                        (method.Name.StartsWith("get_") || method.Name.StartsWith("set_")))
                        continue;  // Пропуск свойств
            
                    GenerateProxyMethod(proxyType, interceptorField, method);
                }
        
                Type createdType = proxyType.CreateType();
                return (T)Activator.CreateInstance(createdType, interceptor);
            }
    
            private static void GenerateProxyMethod(
                TypeBuilder typeBuilder, 
                FieldInfo interceptorField, 
                MethodInfo targetMethod)
            {
                var methodBuilder = typeBuilder.DefineMethod(
                    targetMethod.Name,
                    MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final,
                    targetMethod.CallingConvention,
                    targetMethod.ReturnType,
                    targetMethod.GetParameters().Select(p => p.ParameterType).ToArray());
        
                var il = methodBuilder.GetILGenerator();
        
                // Генерация: return _interceptor.Intercept(this, methodInfo, arguments);
                // Упрощено — реальная реализация требует правильной обработки аргументов
        
                if (targetMethod.ReturnType == typeof(void))
                    il.Emit(OpCodes.Ret);
                else
                    il.Emit(OpCodes.Ldnull);  // Заглушка
        
                il.Emit(OpCodes.Ret);
            }
        }

        public interface IInterceptor
        {
            object Intercept(object proxy, MethodInfo method, object[] arguments);
        }

        public class LoggingInterceptor : IInterceptor
        {
            public object Intercept(object proxy, MethodInfo method, object[] arguments)
            {
                Console.WriteLine($"Вызов: {method.Name}");
                var startTime = Stopwatch.GetTimestamp();
        
                try
                {
                    var result = method.Invoke(proxy, arguments);
                    var elapsed = (Stopwatch.GetTimestamp() - startTime) / Stopwatch.Frequency * 1000;
                    Console.WriteLine($"Завершено: {method.Name} за {elapsed:F2}мс");
                    return result;
                }
                catch (TargetInvocationException ex)
                {
                    Console.WriteLine($"Ошибка в {method.Name}: {ex.InnerException?.Message}");
                    throw;
                }
            }
        }

Сводная таблица: Когда что использовать

СценарийРекомендуемый подходПроизводительность
Одноразовая инспекция типаType.GetProperties() и т.д.Приемлемо (одноразовая стоимость)
Повторный доступ к свойствамКэширование PropertyInfo~20-50нс на доступ
Доступ к свойствам в критическом участкеDelegate.CreateDelegate или Expression.Compile~5-10нс на доступ
Создание динамического типа во время выполненияReflection.EmitСложно, но мощно
Генерация кода на этапе компиляцииSource Generators (предпочтительнее Emit)Нулевая стоимость во время выполнения
Простой маппинг объектовДеревья выражений + скомпилированные делегатыСкорость, близкая к прямой
DI-контейнерРефлексия для создания + кэширование делегатовДостаточно для большинства случаев

Эмпирическое правило производительности

Прямой доступ:          ~1нс    ████████████████████ (быстрее всего)
        Скомпилированный делегат:      ~5-10нс  ████████████████
        Кэшированный PropertyInfo:    ~20-50нс   ███████████
        Рефлексия без кэша:    ~100-500нс ████

Правило: Если вызов рефлексии находится в критическом участке (вызывается > 1000 раз/сек), скомпилируйте его в делегат. В противном случае достаточно кэширования PropertyInfo.


Атрибуты и метаданные в C#/.NET

Содержание

  • Пользовательские атрибуты — наследование, AllowMultiple, AttributeUsage
  • Чтение метаданных во время выполнения vs на этапе компиляции
  • API Roslyn — SyntaxTree, SemanticModel
  • Практические упражнения: валидация на основе атрибутов, анализатор Roslyn

Пользовательские атрибуты

Что такое атрибуты?

Атрибуты — это аннотации метаданных, прикрепляемые к элементам кода (классы, методы, свойства, параметры и т.д.), которые можно читать во время выполнения через рефлексию или на этапе компиляции через Roslyn:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
        public class MyAttribute : Attribute
        {
            public string Description { get; }
    
            public MyAttribute(string description)
            {
                Description = description;
            }
        }

        // Использование:
        [My("Это сущность Person")]
        public class Person
        {
            [My("Полное имя человека")]
            public string Name { get; set; }
        }

AttributeUsage — управление применением атрибутов

[AttributeUsage(
            AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method,  // Допустимые цели
            AllowMultiple = true,                                                         // Можно применять несколько раз
            Inherited = false)]                                                          // Не наследуется производными классами
        public class ExampleAttribute : Attribute { }

        // Варианты AttributeTargets:
        // - All              — Любой элемент программы
        // - Assembly         — Уровень сборки
        // - Module           — Уровень модуля  
        // - Class            — Классы
        // - Struct           — Структуры
        // - Enum             — Перечисления
        // - Constructor      — Конструкторы
        // - Method           — Методы
        // - Property         — Свойства
        // - Field            — Поля
        // - Event            — События
        // - Parameter        — Параметры методов
        // - ReturnValue      — Возвращаемые значения
        // - GenericParameter — Параметры обобщённых типов

        // Примеры:
        [Example]                    // Однократное использование (по умолчанию)
        [Example, Example]           // Многократное использование (если AllowMultiple = true)

        public class BaseClass
        {
            [Example]                // При Inherited = false, производные классы не наследуют это
            public void Method() { }
        }

        public class DerivedClass : BaseClass { }  // Метод не будет иметь атрибут [Example]

Чтение атрибутов во время выполнения

public class Person
        {
            [Obsolete("Используйте FullName вместо этого", false)]
            public string Name { get; set; }
    
            [Required]
            [StringLength(100, MinimumLength = 1)]
            public string FullName { get; set; }
        }

        // Проверка, имеет ли тип атрибут
        bool hasAttribute = typeof(Person).IsDefined(typeof(ObsoleteAttribute), inherit: true);

        // Получение всех атрибутов определённого типа
        var obsoleteAttrs = typeof(Person).GetCustomAttributes(typeof(ObsoleteAttribute), false);

        // Получение конкретного атрибута (нужна проверка на null)
        var obsolete = typeof(Person).GetProperty("Name")!
            .GetCustomAttribute<ObsoleteAttribute>();

        if (obsolete != null)
        {
            Console.WriteLine(obsolete.Message);           // "Используйте FullName вместо этого"
            Console.WriteLine(obsolete.IsError);            // false
        }

        // Получение всех атрибутов (любого типа)
        object[] allAttrs = typeof(Person).GetCustomAttributes(false);

        // Получение атрибутов с наследованием
        var inheritedAttrs = typeof(DerivedClass).GetCustomAttributes(inherit: true);

        // Проверка атрибутов параметров
        foreach (var method in typeof(Person).GetMethods())
        {
            foreach (var param in method.GetParameters())
            {
                var notNullAttr = param.GetCustomAttribute<NotNullAttribute>();
                if (notNullAttr != null)
                    Console.WriteLine($"Параметр '{param.Name}' не должен быть null");
            }
        }

Справочник встроенных атрибутов

АтрибутНазначениеПример
[Obsolete]Пометка устаревших членов[Obsolete("Используйте NewMethod")]
[Serializable]Включение бинарной сериализации[Serializable] public class Data
[Flags]Перечисление как битовые флаги[Flags] enum Permissions { Read = 1, Write = 2 }
[Conditional]Условный вызов метода[Conditional("DEBUG")] void DebugLog()
[DllImport]P/Invoke нативных методов[DllImport("kernel32")] static extern int GetVersion()
[JsonProperty]Управление JSON-сериализацией[JsonProperty("user_name")] public string Username
[NotNull], [CanBeNull]Аннотации nullability[return: NotNull] string GetName()
[Pure]Метод не имеет побочных эффектов[Pure] int Calculate(int a, int b)
[MethodImpl]Управление реализацией метода[MethodImpl(MethodImplOptions.AggressiveInlining)]

Пример пользовательского атрибута: основа фреймворка валидации

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, 
            AllowMultiple = false, Inherited = true)]
        public abstract class ValidationAttribute : Attribute
        {
            public string ErrorMessage { get; set; } = "Валидация не пройдена.";
            public abstract bool IsValid(object? value);
        }

        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
        public class RequiredAttribute : ValidationAttribute
        {
            public override bool IsValid(object? value) => value != null;
        }

        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
        public class RangeAttribute : ValidationAttribute
        {
            public double Minimum { get; }
            public double Maximum { get; }
    
            public RangeAttribute(double minimum, double maximum)
            {
                Minimum = minimum;
                Maximum = maximum;
            }
    
            public override bool IsValid(object? value)
            {
                if (value is IComparable comparable)
                    return comparable.CompareTo(Minimum) >= 0 && 
                           comparable.CompareTo(Maximum) <= 0;
                return false;
            }
        }

        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
        public class StringLengthAttribute : ValidationAttribute
        {
            public int MinimumLength { get; } = 0;
            public int MaximumLength { get; }
    
            public StringLengthAttribute(int maximumLength)
            {
                MaximumLength = maximumLength;
            }
    
            public override bool IsValid(object? value)
            {
                if (value == null) return true;  // Проверка null обрабатывается [Required]
                if (value is string str)
                    return str.Length >= MinimumLength && str.Length <= MaximumLength;
                return false;
            }
        }

        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
        public class RegularExpressionAttribute : ValidationAttribute
        {
            public string Pattern { get; }
    
            public RegularExpressionAttribute(string pattern)
            {
                Pattern = pattern;
            }
    
            public override bool IsValid(object? value)
            {
                if (value == null) return true;
                return Regex.IsMatch(value.ToString()!, Pattern);
            }
        }

        // Использование:
        public class CreateUserRequest
        {
            [Required]
            [StringLength(50, MinimumLength = 1)]
            public string Username { get; set; } = "";
    
            [Required]
            [RegularExpression(@"^[^@]+@[^@]+\.[^@]+$")]
            public string Email { get; set; } = "";
    
            [Range(0, 150)]
            public int? Age { get; set; }
        }

Чтение метаданных: во время выполнения vs на этапе компиляции

Рефлексия во время выполнения (System.Reflection)

// Чтение атрибутов во время выполнения — гибко, но имеет стоимость производительности
        public static List<ValidationResult> Validate(object obj)
        {
            var results = new List<ValidationResult>();
            Type type = obj.GetType();
    
            foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                var attrs = prop.GetCustomAttributes(typeof(ValidationAttribute), false)
                    .Cast<ValidationAttribute>()
                    .ToList();
        
                object? value = prop.GetValue(obj);
        
                foreach (var attr in attrs)
                {
                    if (!attr.IsValid(value))
                        results.Add(new ValidationResult(prop.Name, attr.ErrorMessage));
                }
            }
    
            return results;
        }

        // Производительность: ~5-20мкс на валидацию объекта (накладные расходы рефлексии)

На этапе компиляции с Roslyn (Microsoft.CodeAnalysis)

Roslyn предоставляет доступ к структуре и семантике кода на этапе компиляции:

АспектРефлексия во время выполненияRoslyn (на этапе компиляции)
Когда выполняетсяВо время выполнения приложенияВо время компиляции
Влияние на производительностьСтоимость во время выполненияНулевая стоимость во время выполнения
Может изменять поведениеДа, динамические решенияНет, код уже скомпилирован
Варианты использованияВалидация, DI, сериализацияГенерация кода, анализаторы, инструменты рефакторинга
APISystem.ReflectionMicrosoft.CodeAnalysis

Обзор архитектуры Roslyn

Исходный код (.cs файлы)
            ↓
        SyntaxTree (AST — абстрактное синтаксическое дерево)
            ↓
        Compilation + SemanticModel (Информация о типах, поток данных)
            ↓
        IL → Нативный код (через JIT/AOT)

Ключевые компоненты:

  • SyntaxTree: Дерево разбора исходного кода (только структура, без типов)
  • SyntaxNode: Отдельный узел дерева (объявление класса, метод, выражение и т.д.)
  • SyntaxToken: Лексические токены (идентификаторы, ключевые слова, операторы)
  • SemanticModel: Информация о типах, разрешение символов, анализ потока данных
  • Compilation: Представляет полную единицу компиляции со всеми ссылками

API Roslyn — SyntaxTree и SemanticModel

Парсинг исходного кода

using Microsoft.CodeAnalysis;
        using Microsoft.CodeAnalysis.CSharp;
        using Microsoft.CodeAnalysis.CSharp.Syntax;
        using Microsoft.CodeAnalysis.Text;

        string source = @"
        public class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
    
            public void Greet() 
            {
                Console.WriteLine(""Hello, "" + Name);
            }
        }";

        // Парсинг в SyntaxTree (AST) — пока без информации о типах
        SyntaxTree tree = CSharpSyntaxTree.ParseText(source);
        CompilationUnitSyntax root = tree.GetRoot();

        // Навигация по синтаксическому дереву
        foreach (var member in root.DescendantNodes().OfType<ClassDeclarationSyntax>())
        {
            Console.WriteLine($"Класс: {member.Identifier.Text}");
    
            foreach (var prop in member.Members.OfType<PropertyDeclarationSyntax>())
            {
                var type = prop.Type.ToString();
                var name = prop.Identifier.Text;
                Console.WriteLine($"  Свойство: {type} {name}");
            }
    
            foreach (var method in member.Members.OfType<MethodDeclarationSyntax>())
            {
                Console.WriteLine($"  Метод: {method.ReturnType} {method.Identifier.Text}()");
        
                // Анализ тела метода
                if (method.Body != null)
                {
                    foreach (var stmt in method.Body.Statements)
                        Console.WriteLine($"    Оператор: {stmt.GetType().Name}");
                }
            }
        }

        // Вывод:
        // Класс: Person
        //   Свойство: string Name
        //   Свойство: int Age
        //   Метод: void Greet()
        //     Оператор: ExpressionStatementSyntax

SemanticModel — информация о типах и разрешение символов

// Создание компиляции для получения семантической информации
        MetadataReference[] references = new[]
        {
            MetadataReference.CreateFromFile(typeof(object).Assembly.Location),       // System.Private.CoreLib
            MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),      // System.Console
            // Добавьте другие по мере необходимости...
        };

        Compilation compilation = CSharpCompilation.Create(
            "MyAssembly",
            syntaxTrees: new[] { tree },
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        // Получение семантической модели для дерева
        SemanticModel semanticModel = compilation.GetSemanticModel(tree);

        // Теперь можно разрешать типы и символы!
        var classNode = root.DescendantNodes().OfType<ClassDeclarationSyntax>()
            .First(c => c.Identifier.Text == "Person");

        // Получение символа (информации о типе) для класса
        INamedTypeSymbol personSymbol = semanticModel.GetDeclaredSymbol(classNode)!;
        Console.WriteLine($"Полное имя: {personSymbol.ToDisplayString()}");  // global::Person
        Console.WriteLine($"Базовый тип: {personSymbol.BaseType}");                       // System.Object

        // Анализ типов свойств
        foreach (var prop in classNode.Members.OfType<PropertyDeclarationSyntax>())
        {
            IPropertySymbol propSymbol = semanticModel.GetDeclaredSymbol(prop)!;
            Console.WriteLine($"  {propSymbol.Type.ToDisplayString()} {propSymbol.Name}");
    
            // Проверка nullability
            Console.WriteLine($"    NullableAnnotation: {propSymbol.NullableAnnotation}");
        }

        // Анализ вызовов методов и их целей
        foreach (var invocation in root.DescendantNodes().OfType<InvocationExpressionSyntax>())
        {
            IMethodSymbol methodSymbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
            if (methodSymbol != null)
                Console.WriteLine($"Вызов: {methodSymbol.ToDisplayString()}");
        }

Типы символов в Roslyn

Тип символаПредставляетПример
ITypeSymbolЛюбой типint, string, пользовательские классы
INamedTypeSymbolИменованные типы (класс, структура, enum, интерфейс)Person, List<T>
IArrayTypeSymbolТипы массивовint[], string[,]
IMethodSymbolМетоды, аксессоры свойств, операторыToString(), get_Name()
IFieldSymbolПоля и значения enum_cache, Color.Red
IPropertySymbolСвойстваName { get; set; }
IParameterSymbolПараметры методовint count в Method(int count)
IEventSymbolСобытияevent Action Changed
INamespaceSymbolПространства имёнSystem.Collections.Generic

Анализ потока данных и управления

var method = root.DescendantNodes().OfType<MethodDeclarationSyntax>().First();
        var body = method.Body!;

        // Поток данных: какие переменные читаются/записываются в операторе
        DataFlowAnalysis dataFlow = semanticModel.AnalyzeDataFlow(body);

        Console.WriteLine("Входные (захваченные) переменные:");
        foreach (var symbol in dataFlow.DataFlowsIn)
            Console.WriteLine($"  {symbol}");

        Console.WriteLine("Выходные (используемые снаружи) переменные:");
        foreach (var symbol in dataFlow.DataFlowsOut)
            Console.WriteLine($"  {symbol}");

        // Поток управления: ветвления, циклы, обработка исключений
        ControlFlowAnalysis controlFlow = semanticModel.AnalyzeControlFlow(body);

        Console.WriteLine($"Точки возврата: {controlFlow.ReturnPoints.Length}");
        Console.WriteLine($"Точки выброса исключений: {controlFlow.ThrowPoints.Length}");

Практика

  • [ ] Изучить материал

Упражнение 1: Фреймворк валидации на основе атрибутов

public class ValidationResult
        {
            public string PropertyName { get; }
            public string ErrorMessage { get; }
    
            public ValidationResult(string propertyName, string errorMessage)
            {
                PropertyName = propertyName;
                ErrorMessage = errorMessage;
            }
        }

        public static class Validator
        {
            // Валидация объекта с использованием рефлексии + кэшированных делегатов
            private static readonly ConditionalWeakTable<Type, ValidationPlan> _cache = new();
    
            public class ValidationPlan
            {
                public List<PropertyValidation> Validations = new();
        
                public class PropertyValidation
                {
                    public string PropertyName;
                    public Func<object?, object?> Getter;
                    public List<ValidationAttribute> Attributes;
                }
            }
    
            public static IEnumerable<ValidationResult> Validate<T>(T obj) where T : class
            {
                var plan = _cache.GetOrCreateValue(typeof(T));
        
                if (plan.Validations.Count == 0)
                {
                    // Построение плана валидации с использованием рефлексии + скомпилированных делегатов
                    Type type = typeof(T);
            
                    foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
                    {
                        var attrs = prop.GetCustomAttributes(typeof(ValidationAttribute), false)
                            .Cast<ValidationAttribute>()
                            .ToList();
                
                        if (attrs.Count == 0) continue;
                
                        // Компиляция делегата-геттера для производительности
                        var param = Expression.Parameter(typeof(T), "x");
                        var propertyAccess = Expression.Property(param, prop);
                        var lambda = Expression.Lambda<Func<T, object?>>(
                            Expression.Convert(propertyAccess, typeof(object)), param);
                
                        plan.Validations.Add(new ValidationPlan.PropertyValidation
                        {
                            PropertyName = prop.Name,
                            Getter = obj2 => ((Func<T, object?>)lambda)((T)(object)obj2),
                            Attributes = attrs
                        });
                    }
                }
        
                // Выполнение валидаций
                foreach (var validation in plan.Validations)
                {
                    object? value = validation.Getter(obj);
            
                    foreach (var attr in validation.Attributes)
                    {
                        if (!attr.IsValid(value))
                            yield return new ValidationResult(validation.PropertyName, attr.ErrorMessage);
                    }
                }
            }
        }

        // Использование:
        var request = new CreateUserRequest
        {
            Username = "",           // Не проходит [Required] и [StringLength MinimumLength=1]
            Email = "invalid",       // Не проходит [RegularExpression]
            Age = 200               // Не проходит [Range(0, 150)]
        };

        var errors = Validator.Validate(request).ToList();
        foreach (var error in errors)
            Console.WriteLine($"{error.PropertyName}: {error.ErrorMessage}");

        // Вывод:
        // Username: Валидация не пройдена.
        // Email: Валидация не пройдена.
        // Age: Валидация не пройдена.

Упражнение 2: Анализатор Roslyn для обнаружения антипаттернов

using Microsoft.CodeAnalysis;
        using Microsoft.CodeAnalysis.CSharp;
        using Microsoft.CodeAnalysis.CSharp.Syntax;
        using Microsoft.CodeAnalysis.Diagnostics;
        using System.Collections.Immutable;

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class ConsoleWriteLineInLoopAnalyzer : DiagnosticAnalyzer
        {
            public const string DiagnosticId = "PERF001";
            private const string Title = "Console.WriteLine в цикле";
            private const string MessageFormat = "Console.WriteLine внутри цикла может вызвать проблемы с производительностью. Рассмотрите буферизацию вывода.";
            private const string Category = "Performance";
    
            private static readonly DiagnosticDescriptor Rule = new(
                id: DiagnosticId,
                title: Title,
                messageFormat: MessageFormat,
                category: Category,
                defaultSeverity: DiagnosticSeverity.Warning,
                isEnabledByDefault: true);
    
            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => 
                ImmutableArray.Create(Rule);
    
            public override void Initialize(AnalysisContext context)
            {
                context.EnableConcurrentExecution();
                context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        
                // Анализ операторов выражений
                context.RegisterSyntaxNodeAction(AnalyzeNode, 
                    SyntaxKind.ExpressionStatement);
            }
    
            private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
            {
                var statement = (ExpressionStatementSyntax)context.Node;
        
                // Проверка, является ли это вызовом Console.WriteLine
                if (statement.Expression is not InvocationExpressionSyntax invocation)
                    return;
        
                // Получение символа вызываемого метода
                var info = context.SemanticModel.GetSymbolInfo(invocation);
                var methodSymbol = info.Symbol as IMethodSymbol;
        
                if (methodSymbol?.ContainingType.ToDisplayString() != "System.Console")
                    return;
        
                if (!methodSymbol.Name.StartsWith("Write"))
                    return;
        
                // Проверка, находимся ли мы внутри цикла
                var parent = statement.Parent;
                while (parent != null)
                {
                    if (parent is ForStatementSyntax || 
                        parent is WhileStatementSyntax || 
                        parent is DoStatementSyntax ||
                        parent is ForEachStatementSyntax)
                    {
                        // Создание диагностического сообщения
                        var diagnostic = Diagnostic.Create(
                            Rule, statement.GetLocation());
                        context.ReportDiagnostic(diagnostic);
                        return;
                    }
            
                    // Остановка на границе метода
                    if (parent is MethodDeclarationSyntax || 
                        parent is LocalFunctionStatementSyntax)
                        return;
            
                    parent = parent.Parent;
                }
            }
        }

        // Дополнительные анализаторы для распространённых антипаттернов:

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class StringConcatInLoopAnalyzer : DiagnosticAnalyzer
        {
            // Обнаруживает: string result += item; внутри циклов
            // Предлагает: используйте StringBuilder вместо этого
    
            public const string DiagnosticId = "PERF002";
    
            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => 
                ImmutableArray.Create(new DiagnosticDescriptor(
                    DiagnosticId, "Конкатенация строк в цикле",
                    "Используйте StringBuilder для конкатенации строк в циклах", "Performance",
                    DiagnosticSeverity.Warning, true));
    
            public override void Initialize(AnalysisContext context)
            {
                context.RegisterSyntaxNodeAction(ctx =>
                {
                    var stmt = (ExpressionStatementSyntax)ctx.Node;
            
                    if (stmt.Expression is not AssignmentExpressionSyntax assignment)
                        return;
            
                    if (assignment.Kind() != SyntaxKind.AddAssignmentExpression)
                        return;
            
                    // Проверка, является ли левая часть строковой переменной
                    var leftType = ctx.SemanticModel.GetTypeInfo(assignment.Left).Type;
                    if (leftType?.ToDisplayString() != "string")
                        return;
            
                    // Проверка, находимся ли внутри цикла
                    if (IsInsideLoop(stmt))
                        ctx.ReportDiagnostic(Diagnostic.Create(
                            SupportedDiagnostics[0], stmt.GetLocation()));
                }, SyntaxKind.ExpressionStatement);
            }
    
            private static bool IsInsideLoop(SyntaxNode node)
            {
                var parent = node.Parent;
                while (parent != null)
                {
                    if (parent is ForStatementSyntax || parent is WhileStatementSyntax ||
                        parent is DoStatementSyntax || parent is ForEachStatementSyntax)
                        return true;
                    if (parent is MethodDeclarationSyntax) return false;
                    parent = parent.Parent;
                }
                return false;
            }
        }

        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class UnusedParameterAnalyzer : DiagnosticAnalyzer
        {
            // Обнаруживает: параметры метода, которые никогда не используются в теле
    
            public const string DiagnosticId = "STYLE001";
    
            public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => 
                ImmutableArray.Create(new DiagnosticDescriptor(
                    DiagnosticId, "Неиспользуемый параметр",
                    "Параметр '{0}' объявлен, но никогда не используется. Рассмотрите его удаление или добавьте префикс подчёркивания.",
                    "Style", DiagnosticSeverity.Info, true));
    
            public override void Initialize(AnalysisContext context)
            {
                context.RegisterSyntaxNodeAction(ctx =>
                {
                    var method = (MethodDeclarationSyntax)ctx.Node;
            
                    foreach (var param in method.ParameterList.Parameters)
                    {
                        if (param.Identifier.Text.StartsWith("_"))
                            continue;  // Префикс подчёркивания указывает на намеренное неиспользование
                
                        // Проверка, используется ли параметр в теле
                        string paramName = param.Identifier.Text;
                        bool isUsed = method.DescendantTokens()
                            .SkipWhile(t => t != param.Identifier)  // Пропуск до объявления
                            .Skip(1)                                // Пропуск самого объявления
                            .Any(t => t.IsKind(SyntaxKind.IdentifierToken) && 
                                     t.Text == paramName);
                
                        if (!isUsed)
                            ctx.ReportDiagnostic(Diagnostic.Create(
                                SupportedDiagnostics[0], param.Identifier.GetLocation(), paramName));
                    }
                }, SyntaxKind.MethodDeclaration);
            }
        }

Сводная таблица

КонцепцияКлючевой вывод
Пользовательские атрибутыАннотации метаданных для использования во время выполнения и на этапе компиляции
AttributeUsageУправляет, где атрибут может применяться (цели, многократность, наследование)
Рефлексия во время выполненияЧтение атрибутов во время выполнения — гибко, но имеет стоимость производительности
Скомпилированные делегатыКэшируйте PropertyInfo или компилируйте в делегат для критических участков
Roslyn SyntaxTreeПредставление AST структуры исходного кода
Roslyn SemanticModelИнформация о типах и разрешение символов
DiagnosticAnalyzerАнализ кода на этапе компиляции (предупреждения, ошибки, предложения)
SourceGeneratorГенерация кода на этапе компиляции (нулевые накладные расходы во время выполнения)

Когда использовать каждый подход

ЦельЛучший инструмент
Валидация/маппинг во время выполненияАтрибуты + рефлексия с кэшированием делегатов
Обеспечение качества кодаRoslyn DiagnosticAnalyzer
Устранение boilerplate-кодаRoslyn Source Generator
Расширения IDE/инструменты рефакторингаRoslyn Workspace API
Генерация документацииАтрибуты + обработка на этапе компиляции
AOP/интерцепцияDynamicProxy (Emit) или Source Generators

Внутренности BCL и среда выполнения — экспертный уровень

Содержание

  • Ключевые типы в Reference Source / репозитории .NET GitHub
  • Внутреннее устройство ObjectPool
  • Lazy — режимы потокобезопасности, стратегии инициализации
  • ConditionalWeakTable — слабые ссылки без нагрузки на GC
  • AsyncLocal vs [ThreadStatic]
  • Внутреннее устройство параллельных коллекций
  • Практические упражнения

Понимание исходного кода BCL

Где найти исходный код .NET

РесурсURLНазначение
.NET Runtime (GitHub)github.com/dotnet/runtimeОсновные библиотеки, среда выполнения, JIT
Reference Source (наследие)referencesource.microsoft.comИсходный код .NET Framework 4.8
.NET Fiddledotnetfiddle.netБыстрые онлайн-эксперименты
SharpLabsharplab.ioПросмотр IL/state machine, генерируемых компилятором

Ключевые пути в репозитории dotnet/runtime

src/
        ├── libraries/
        │   ├── System.Private.CoreLib/    # Основная BCL (System.*, System.Collections.*)
        │   │   └── src/System/Collections/Generic/
        │   │       ├── Dictionary.cs      # Реализация Dictionary<TKey,TValue>
        │   │       ├── List.cs            # Реализация List<T>  
        │   │       ├── HashSet.cs         # Реализация HashSet<T>
        │   │       └── SortedSet.cs       # SortedSet<T> (красно-чёрное дерево)
        │   │
        │   ├── System.Collections.Concurrent/  # Потокобезопасные коллекции
        │   │   ├── ConcurrentDictionary.cs
        │   │   ├── ConcurrentQueue.cs
        │   │   └── ConcurrentStack.cs
        │   │
        │   └── System.Threading/          # Примитивы многопоточности
        │       ├── Objects/ObjectPool.cs
        │       └── LazyInitializer.cs
        │
        ├── src/coreclr/                   # Среда выполнения CLR (наследие, перемещается в native AOT)
        └── src/corefx/                    # Кроссплатформенные библиотеки классов (объединены в libraries/)

Чтение исходного кода Dictionary — ключевые выводы

Из src/libraries/System.Private.CoreLib/src/System/Collections/Generic/Dictionary.cs:

Ключевые внутренние структуры:

// Структура Entry — хранит одну пару ключ-значение со ссылкой на цепочку коллизий
        private struct Entry
        {
            public int hashCode;      // Младшие 31 бит хеш-кода, -1 если не используется
            public int next;          // Индекс следующей записи в цепочке (-1 = последняя)
            public TKey key;
            public TValue value;
        }

        // Параллельные массивы для хеш-таблицы
        private int[]? _buckets;       // buckets[hash % length] → индекс первой записи
        private Entry[]? _entries;     // Все записи (включая логически удалённые)
        private int _count;            // Общее количество активных записей
        private int _freeList;         // Голова списка свободных записей (-1 = нет)
        private int _freeCount;        // Количество свободных записей

Ключевая оптимизация: свободный список для удалённых записей

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

private void Insert(TKey key, TValue value, bool add)
        {
            // ... вычисление хеша ...
    
            int index;
            if (_freeCount > 0)
            {
                // Повторное использование освобождённого слота — избегает расширения массива!
                index = _freeList;
                _freeList = _entries[index].next;  // Следующий свободный слот
                _entries[index].next = -1;
                _freeCount--;
            }
            else if (_count == _entries.Length)
            {
                // Нет свободных слотов — нужно изменить размер
                Resize();
                // Пересчёт корзины после изменения размера
                targetBucket = hashCode % _buckets!.Length;
                index = _count;
            }
            else
            {
                // Обычный случай: добавление в конец
                index = _count;
            }
    
            _count++;
            _entries[index].hashCode = hashCode;
            _entries[index].next = _buckets![targetBucket];  // Добавление в начало цепочки
            _entries[index].key = key;
            _entries[index].value = value;
            _buckets[targetBucket] = index;
            _version++;
        }

Почему это важно: Этот паттерн свободного списка означает, что удаление и повторное добавление тех же ключей не вызывает повторных изменений размера — распространённый сценарий в реализациях кэша.

HashHelpers — выбор простых чисел для массивов корзин

Dictionary использует простые числа для размеров массивов корзин, чтобы минимизировать хеш-коллизии:

internal static class HashHelpers
        {
            // Первые 39 простых чисел от 1 до 16777619
            internal static readonly int[] Primes = 
            {
                3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 
                239, 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 
                2333, 2801, 3371, 4051, 4861, 5839, 7009, 8419, 10103, 12143, 
                14591, 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 
                75431, 90523, 108631, 130363, 156437, 187751, 225307, 270371, 
                324449, 389357, 467237, 560689, 672827, 807403, 968897, 
                1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 
                4166287, 4999559, 5999471, 7199369
            };

            // Расширение до следующего простого числа, которое может вместить как минимум 'oldSize' записей
            internal static int ExpandPrime(int oldValue)
            {
                if (oldValue > Primes[Primes.Length - 1])
                    return oldValue;  // Уже за пределами нашей таблицы — использовать как есть
        
                for (int i = 0; i < Primes.Length; i++)
                {
                    int prime = Primes[i];
                    if (prime >= oldValue) return prime;
                }
                return oldValue;
            }

            // Порог коэффициента загрузки: изменение размера, когда счётчик достигает ~85% ёмкости
            internal const int HashCollisionThreshold = 100;  
        }

Внутреннее устройство ObjectPool

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

using System.Threading;

        // Создание пула с функцией-фабрикой
        var pool = new ObjectPool<MyResource>(
            factory: () => new MyResource(),     // Создание нового, когда пул пуст
            maxRetained: 100);                   // Максимальное количество объектов в пуле

        // Аренда объекта из пула
        MyResource resource = pool.Rent();
        try
        {
            // Использование ресурса...
            resource.DoWork();
        }
        finally
        {
            // Возврат в пул (опционально сброс состояния)
            pool.Return(resource, shouldReset: true);
        }

        // Политика пула: управление созданием и удержанием
        var policyPool = new ObjectPool<MyResource>(new DefaultPooledObjectPolicy<MyResource>());

Внутренняя структура

public class ObjectPool<T> where T : class
        {
            // Потокобезопасный стек для пула объектов
            private readonly ConcurrentStack<T?> _pool;
    
            // Функция-фабрика для создания новых экземпляров
            private readonly Func<T> _factory;
    
            // Максимальное количество объектов для удержания в пуле
            private readonly int? _maxRetained;
    
            public ObjectPool(Func<T> factory, int? maxRetained = null)
            {
                _factory = factory;
                _maxRetained = maxRetained;
                _pool = new ConcurrentStack<T?>();
            }
    
            public T Rent()
            {
                // Попытка получения из пула (pop без блокировки)
                if (_pool.TryPop(out var resource))
                    return resource!;
        
                // Пул пуст — создание нового экземпляра
                return _factory();
            }
    
            public void Return(T? obj, bool shouldReset = true)
            {
                if (obj == null) return;
        
                // Проверка ограничения ёмкости
                if (_maxRetained.HasValue && _pool.Count >= _maxRetained.Value)
                    return;  // Отбрасывание — пул заполнен
        
                // Опциональный сброс состояния объекта перед возвратом
                if (shouldReset && obj is IPooledObject pooled)
                    pooled.Reset();
        
                _pool.Push(obj);  // Push без блокировки
            }
        }

        // Паттерн политики фабрики:
        public interface IObjectPolicy<T>
        {
            T Create();
            bool Return(T obj);  // Возвращает false, если объект должен быть отброшен
        }

        public class DefaultPooledObjectPolicy<T> : IObjectPolicy<T> where T : new()
        {
            public T Create() => new T();
            public bool Return(T obj) => true;  // Всегда принимать возврат
        }

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

СценарийРекомендация
Дорогостоящее создание объектов (подключения к БД, сетевые сокеты)✅ Отлично подходит
Частое выделение/освобождение в критических участках✅ Снижает нагрузку на GC
Объекты со сбрасываемым состоянием✅ Возврат после Reset()
Простые значимые типы❌ Используйте stackalloc или ArrayPool вместо этого
Объекты, которые нельзя безопасно переиспользовать❌ Пулинг вызывает ошибки

Lazy — режимы потокобезопасности и стратегии инициализации

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

// По умолчанию: только публикация (не полностью потокобезопасно для значения)
        var lazy1 = new Lazy<MyExpensiveObject>(() => new MyExpensiveObject());

        // Потокобезопасно: только один поток инициализирует, остальные ждут
        var lazy2 = new Lazy<MyExpensiveObject>(
            () => new MyExpensiveObject(), 
            LazyThreadSafetyMode.ExecutionAndPublication);

        // Потокобезопасно с повторной попыткой при исключении
        var lazy3 = new Lazy<MyExpensiveObject>(
            () => new MyExpensiveObject(), 
            LazyThreadSafetyMode.PublicationOnly);

        // Доступ — инициализация происходит только при первом доступе
        MyExpensiveObject obj = lazy1.Value;  // Запускает выполнение фабрики
        MyExpensiveObject obj2 = lazy1.Value; // Возвращает кэшированный экземпляр (без повторного выполнения)

        // Проверка, инициализировано ли
        bool isReady = lazy1.IsValueCreated;

Варианты LazyThreadSafetyMode

РежимПотокобезопасностьПроизводительностьПоведение при исключении
NoneНе потокобезопасноБыстрее всегоН/Д (только однопоточный)
PublicationOnlyПервый побеждаетБыстроПроигравшие потоки получают исключение или значение победителя
ExecutionAndPublicationПодобно мьютексуМедленнее (блокировка)Повторная попытка при исключении

Детали внутренней реализации

// Упрощённые внутренности Lazy<T>:
        public class Lazy<T> where T : class
        {
            private T? _value;                          // Кэшированное значение
            private readonly Func<T> _valueFactory;     // Делегат фабрики
            private readonly object _lock = new();      // Для режима ExecutionAndPublication
            private volatile bool _isValueCreated;      // Флаг двойной проверки блокировки
    
            public T Value
            {
                get
                {
                    if (_isValueCreated)
                        return _value!;  // Быстрый путь: значение уже создано
            
                    return LazyInitializer.EnsureInitialized(ref _value, _lock, _valueFactory);
                }
            }
    
            public bool IsValueCreated => _isValueCreated;
        }

        // LazyInitializer — API более низкого уровня для большего контроля:
        public static class LazyInitializer
        {
            // Гарантирует, что поле инициализировано ровно один раз (потокобезопасно)
            public static T EnsureInitialized<T>(ref T target, Func<T> valueFactory) 
                where T : class
    
            // С явным объектом блокировки (избегает выделения одного на каждый Lazy<T>)
            public static T EnsureInitialized<T>(ref T target, object syncLock, Func<T> valueFactory)
    
            // Для значимых типов (использует боксинг внутренне)
            public static TValue EnsureInitialized<TTarget, TValue>(ref TTarget target, 
                Func<TValue> valueFactory) where TValue : struct
        }

        // Использование LazyInitializer напрямую:
        public class MyClass
        {
            private ExpensiveResource? _resource;
            private readonly object _resourceLock = new();
    
            public ExpensiveResource Resource
            {
                get => LazyInitializer.EnsureInitialized(
                    ref _resource, 
                    _resourceLock, 
                    () => new ExpensiveResource());
            }
        }

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

СценарийРекомендация
Дорогостоящий объект, создаваемый редко или никогда✅ Идеально подходит
DI-контейнер с ленивыми зависимостями✅ Стандартный паттерн
Потокобезопасная инициализация синглтона✅ Используйте ExecutionAndPublication
Простое кэширование вычисленного значения✅ Или используйте паттерн Memoize
Значение всегда нужно немедленно❌ Просто создайте его напрямую

ConditionalWeakTable — слабые ссылки без нагрузки на GC

Проблема, которую она решает

// ПРОБЛЕМА: обычный Dictionary предотвращает сборку ключей GC!
        var cache = new Dictionary<object, ComputedValue>();

        object key = new object();
        cache[key] = ComputeExpensiveValue(key);

        key = null;  // Ключ всё ещё ссылается словарём — НИКОГДА не будет собран!
        // Даже после удаления всех внешних ссылок, словарь хранит сильную ссылку

        // РЕШЕНИЕ: ConditionalWeakTable позволяет ключам быть собранными GC, когда нет других ссылок
        var weakCache = new ConditionalWeakTable<object, ComputedValue>();

Как это работает

ConditionalWeakTable хранит слабые ссылки на ключи и сильные ссылки на значения. Когда ключ собирается сборщиком мусора, связанное с ним значение также удаляется:

using System.Runtime.CompilerServices;

        var table = new ConditionalWeakTable<MyObject, ComputedData>();

        // Добавление записи — ключ удерживается слабо, значение удерживается сильно (пока ключ жив)
        MyObject obj = new MyObject();
        table.Add(obj, new ComputedData());

        // Или использование GetValueOrCreate (паттерн ленивой инициализации):
        ComputedData data = table.GetValueOrCreate(obj, 
            factory: key => ComputeExpensiveValue(key));

        // Когда последняя сильная ссылка на 'obj' исчезает:
        obj = null;
        GC.Collect();  // Принудительная сборка для демонстрации

        // Теперь и obj, И его ComputedData собраны!
        // Запись автоматически удаляется из таблицы.

Внутренняя структура (упрощённо)

public class ConditionalWeakTable<TKey, TValue> where TKey : class
        {
            // Каждая запись имеет слабую ссылку на ключ
            private class Entry
            {
                internal WeakReference<TKey> Key;   // Слабая ссылка — позволяет GC
                internal TValue Value;               // Сильная ссылка — живёт, пока жив ключ
                internal int Generation;             // Для синхронизации финализатора
            }
    
            // Финализатор удаляет записи, ключи которых были собраны
            ~ConditionalWeakTable()
            {
                // Очистка оставшихся записей во время финализации
            }
        }

Распространённые варианты использования

СценарийПаттерн
Данные расширения для объектов, которыми вы не владеетеConditionalWeakTable<ExternalType, MyExtensionData>
Кэширование на экземпляр с автоматической очисткойGetValueOrCreate(key, factory)
Интерцепторы AOP, хранящие состояние на проксированных объектахМаппинг прокси → состояние интерцептора
Отслеживание обработчиков событий без утечек памятиАльтернатива паттерну слабых событий

Практический пример: паттерн данных расширения

public static class ObjectExtensions
        {
            // Хранение произвольных данных на любом объекте без изменения его типа
            private static readonly ConditionalWeakTable<object, Dictionary<string, object>> 
                _extensionData = new();
    
            public static void SetExtensionData<T>(this T obj, string key, object value) where T : class
            {
                var dict = _extensionData.GetValueOrCreate(obj, 
                    _ => new Dictionary<string, object>());
                dict[key] = value;
            }
    
            public static object? GetExtensionData<T>(this T obj, string key) where T : class
            {
                if (_extensionData.TryGetValue(obj, out var dict))
                    return dict.GetValueOrDefault(key);
                return null;
            }
        }

        // Использование:
        var externalObject = GetSomeExternalObject();  // Тип, который мы не можем изменить
        externalObject.SetExtensionData("computedValue", ExpensiveComputation());
        var cached = externalObject.GetExtensionData("computedValue");  // Быстрый поиск

        // Когда externalObject собирается GC, все данные расширения автоматически очищаются!

AsyncLocal vs [ThreadStatic]

ThreadStatic — традиционное локальное хранилище потока

public class MyClass
        {
            [ThreadStatic]
            private static int _threadLocalValue;
    
            public static void Demo()
            {
                _threadLocalValue = Environment.CurrentManagedThreadId;
                Console.WriteLine($"Поток {Environment.CurrentManagedThreadId}: {_threadLocalValue}");
            }
        }

        // Ограничения:
        // 1. Каждый поток получает свою копию (хорошо)
        // 2. Значение НЕ передаётся через границы async/await (проблема!)
        // 3. Потоки ThreadPool могут переиспользоваться — старые значения от предыдущего использования

AsyncLocal — хранилище с учётом ExecutionContext

public static class RequestContext
        {
            private static readonly AsyncLocal<Dictionary<string, object>> _local = new();
    
            private static Dictionary<string, object> Current
            {
                get => _local.Value ??= new Dictionary<string, object>();
                set => _local.Value = value;
            }
    
            public static void Set(string key, object value) => Current[key] = value;
    
            public static T? Get<T>(string key) => 
                Current.TryGetValue(key, out var val) ? (T?)val : default;
        }

        // Использование в middleware ASP.NET Core:
        public class RequestContextMiddleware
        {
            private readonly RequestDelegate _next;
    
            public async Task Invoke(HttpContext context)
            {
                // Установка значений, которые передаются через всю цепочку async-вызовов
                RequestContext.Set("RequestId", context.TraceIdentifier);
                RequestContext.Set("UserId", context.User.FindFirst("id")?.Value);
        
                await _next(context);  // Значения доступны во всех async-продолжениях!
            }
        }

        // В любом downstream-сервисе (даже после множества await):
        public class MyService
        {
            public async Task DoWork()
            {
                await SomeAsyncOperation();
                string requestId = RequestContext.Get<string>("RequestId");  // Всё ещё доступно!
        
                await AnotherAsyncOperation();
                string userId = RequestContext.Get<string>("UserId");  // Всё ещё корректно передаётся!
            }
        }

Ключевые отличия

Аспект[ThreadStatic]AsyncLocal
Область храненияНа поток ОСНа контекст выполнения (передаётся через async)
Работает с async/await❌ Значение теряется после await✅ Значение передаётся через продолжения
Безопасность ThreadPool⚠️ Старые значения от переиспользованных потоков✅ Чистый на async-контекст
ПроизводительностьБыстрее (прямой доступ к TLS)Небольшие накладные расходы (копирование контекста)
Уведомление об изменении значенияНетДа (обратный вызов ValueChanged)
Лучше всего подходит дляНаследие синхронного кода, нативное взаимодействиеСовременные async-приложения

Обратный вызов AsyncLocal ValueChanged

private static readonly AsyncLocal<string> _operationName = new();

        // Отслеживание изменений значения (полезно для логирования/диагностики)
        _operationName.ValueChanged += (sender, args) =>
        {
            Console.WriteLine($"Операция изменилась с '{args.OldValue}' на '{args.NewValue}'");
        };

        // Использование с паттерном области видимости:
        public async Task ScopedOperation(string name)
        {
            var previous = _operationName.Value;
            _operationName.Value = name;  // Вызывает обратный вызов ValueChanged
    
            try
            {
                await DoWork();  // Все async-продолжения видят новое значение
            }
            finally
            {
                _operationName.Value = previous;  // Восстановление предыдущего значения (вызывает обратный вызов)
            }
        }

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

СценарийРекомендация
Современное async-приложениеAsyncLocal — всегда
Контекст запроса ASP.NET CoreAsyncLocal (или HttpContext через DI)
Наследие синхронного кода[ThreadStatic] приемлемо
Нативное взаимодействие, требующее TLS[ThreadStatic] или ThreadLocalStorageAttribute
ID корреляции распределённой трассировкиAsyncLocal

Внутреннее устройство параллельных коллекций

ConcurrentDictionary — сегментная блокировка

// Упрощённые внутренности:
        public class ConcurrentDictionary<TKey, TValue>
        {
            // Несколько сегментов, каждый со своей блокировкой
            private Tables _tables;
    
            private struct Tables
            {
                internal readonly IEqualityComparer<TKey> comparisons;
                internal readonly int locksCount;           // Количество полос блокировки (обычно 16)
                internal readonly object[] locks;           // Объекты блокировок на полосу
                internal volatile Node[]? buckets;          // Корзины хеш-таблицы
                internal volatile int[]? countPerLock;      // Счётчик на полосу (для быстрого Count)
            }
    
            private struct Node
            {
                internal readonly TKey key;
                internal readonly TValue value;
                internal int next;                          // Ссылка цепочки коллизий
                internal int hashCode;                      // Хеш-код для быстрого сравнения
            }
    
            // Определение, какую полосу блокировки использовать на основе хеш-кода
            private int GetLockIndexFromHashCode(int hashCode) => 
                (uint)hashCode % (uint)_tables.locksCount;
        }

Ключевой вывод: Сегментная блокировка позволяет ~16 параллельных записей (одна на полосу), значительно увеличивая пропускную способность записи по сравнению с одной глобальной блокировкой.

ConcurrentQueue — очередь Майкла-Скотта без блокировок

Использует операции CompareExchange (CAS) для enqueue/dequeue без блокировок:

// Упрощённые внутренности очереди Майкла-Скотта:
        public class ConcurrentQueue<T>
        {
            private class Node
            {
                internal T item;
                internal volatile Node? next;  // Volatile для упорядочивания памяти
            }
    
            private volatile Node _head = new Node();  // Фиктивный головной узел
            private volatile Node _tail = _head;       // Указатель хвоста
    
            public void Enqueue(T item)
            {
                var newNode = new Node { item = item, next = null };
        
                // Без блокировки: атомарная привязка нового узла к хвосту
                Node oldTail;
                do
                {
                    oldTail = _tail;
                } while (Interlocked.CompareExchange(
                    ref oldTail.next, newNode, null) != null);  // Цикл CAS
        
                // Попытка продвижения указателя хвоста (лучшие усилия, не критично для корректности)
                Interlocked.CompareExchange(ref _tail, newNode, oldTail);
            }
    
            public bool TryDequeue([MaybeNullWhen(false)] out T? result)
            {
                Node oldHead;
                do
                {
                    oldHead = _head;
                    Node? next = oldHead.next;
            
                    if (_head != oldHead) continue;  // Голова изменилась, повтор
        
                    if (next == null)
                    {
                        // Очередь пуста
                        result = default;
                        return false;
                    }
            
                    result = next.item;
                } while (Interlocked.CompareExchange(ref _head, next, oldHead) != oldHead);
        
                return true;
            }
        }

Практика

  • [ ] Изучить материал

Упражнение 1: Пользовательский пул объектов с паттерном аренды ресурсов

public interface IPooledResource : IDisposable
        {
            void Reset();  // Сброс состояния для повторного использования
        }

        public class PooledObjectPool<T> where T : class, IPooledResource, new()
        {
            private readonly ConcurrentStack<T> _pool = new();
            private readonly int _maxSize;
            private int _totalCreated;
    
            public PooledObjectPool(int maxSize = 100)
            {
                _maxSize = maxSize;
            }
    
            public Lease Rent()
            {
                T? resource;
        
                if (_pool.TryPop(out resource))
                {
                    resource.Reset();  // Сброс состояния перед повторным использованием
                }
                else
                {
                    // Создание нового, если в пределах лимита
                    if (Interlocked.Increment(ref _totalCreated) > _maxSize)
                        throw new InvalidOperationException("Пул исчерпан");
            
                    resource = new T();
                }
        
                return new Lease(this, resource);
            }
    
            private void Return(T resource)
            {
                if (_pool.Count < _maxSize)
                    _pool.Push(resource);
                // Иначе: отбрасывание (пусть GC собирает)
            }
    
            // Паттерн Lease гарантирует, что ресурс всегда возвращается
            public readonly struct Lease : IDisposable
            {
                private readonly PooledObjectPool<T> _pool;
                private T? _resource;
        
                internal Lease(PooledObjectPool<T> pool, T resource)
                {
                    _pool = pool;
                    _resource = resource;
                }
        
                public T Resource => _resource!;
        
                public void Dispose()
                {
                    if (_resource != null)
                    {
                        _resource.Dispose();
                        _pool.Return(_resource);
                        _resource = null;
                    }
                }
            }
        }

        // Использование:
        var pool = new PooledObjectPool<DbConnectionWrapper>(maxSize: 50);

        using var lease = pool.Rent();
        lease.Resource.ExecuteQuery("SELECT ...");
        // Автоматически возвращается в пул, когда lease выходит из области видимости!

Упражнение 2: Диагностический инструмент для паттернов выделения памяти

public static class AllocationDiagnostics
        {
            // Измерение выделений памяти в блоке кода (современный точный метод)
            public static long MeasureAllocatedBytes(Action action)
            {
                long before = GC.GetAllocatedBytesForCurrentThread();
                action();
                long after = GC.GetAllocatedBytesForCurrentThread();
                return after - before;
            }
    
            // Альтернатива со сборкой мусора (для изоляции от других процессов)
            public static long MeasureAllocatedBytesAccurate(Action action)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
        
                long before = GC.GetAllocatedBytesForCurrentThread();
                action();
                long after = GC.GetAllocatedBytesForCurrentThread();
        
                return Math.Max(0, after - before);
            }
    
            // Сравнение паттернов выделения между двумя реализациями
            public static void CompareAllocations(
                string name1, Action impl1, 
                string name2, Action impl2, 
                int iterations = 100)
            {
                long total1 = 0, total2 = 0;
        
                for (int i = 0; i < iterations; i++)
                {
                    total1 += MeasureAllocatedBytes(impl1);
                    total2 += MeasureAllocatedBytes(impl2);
                }
        
                Console.WriteLine($"{name1}: {total1 / iterations:N0} байт/итерация (среднее)");
                Console.WriteLine($"{name2}: {total2 / iterations:N0} байт/итерация (среднее)");
                Console.WriteLine($"Соотношение: {(double)total1 / Math.Max(1, total2):F2}x");
            }
    
            // Статистика поколений GC
            public static void PrintGcStats()
            {
                Console.WriteLine($"Сборки Gen 0: {GC.CollectionCount(0)}");
                Console.WriteLine($"Сборки Gen 1: {GC.CollectionCount(1)}");
                Console.WriteLine($"Сборки Gen 2: {GC.CollectionCount(2)}");
                Console.WriteLine($"Общая память: {GC.GetTotalMemory(false):N0} байт");
                Console.WriteLine($"Server GC: {GC.IsServerGC}");
                Console.WriteLine($"Максимальное поколение: {GC.MaxGeneration}");
            }
        }

        // Использование:
        AllocationDiagnostics.CompareAllocations(
            "List<T>", 
            () => { var list = new List<int>(); for (int i = 0; i < 1000; i++) list.Add(i); },
            "ArrayPool",
            () => { 
                var arr = ArrayPool<int>.Shared.Rent(1000);
                for (int i = 0; i < 1000; i++) arr[i] = i;
                ArrayPool<int>.Shared.Return(arr);
            });

Контрольная точка модуля — высокопроизводительная библиотека коллекций

Сводка требований проекта

Создать высокопроизводительную библиотеку коллекций с:

  1. API без выделения памяти через Span
  2. - Методы принимают параметры Span<T> / ReadOnlySpan<T> - Внутренние буферы используют stackalloc или ArrayPool<T> - Без промежуточных выделений памяти во время операций
  1. Пользовательские компараторы и провайдеры хеш-кодов
  2. - Поддержка пользовательских IEqualityComparer<T> - Специализированные компараторы для распространённых типов (регистронезависимые строки и т.д.) - Настраиваемая хеш-функция для устойчивости к коллизиям
  1. Полный LINQ-совместимый интерфейс
  2. - Реализация IEnumerable<T> где возможно - Методы расширения для операций на основе Span - Паттерны отложенного выполнения с yield return
  1. Сравнительный бенчмарк с эквивалентами BCL
  2. - Использование BenchmarkDotNet для точных измерений - Сравнение с List, Dictionary, HashSet - Измерение как пропускной способности, так и скорости выделения памяти

Критерии оценки

  • Все бенчмарки показывают конкурентоспособную производительность по сравнению с BCL
  • Покрытие кода > 90%
  • XML-документация для всего публичного API