01Стандартная библиотека C# и .NET (BCL)
Коллекции и структуры данных C#/.NET
1. System.Collections.Generic — Полный обзор
Основные коллекции и их назначение
| Коллекция | Назначение | Операции |
|---|---|---|
List<T> | Динамический массив, доступ по индексу | Добавление в конец, поиск по индексу |
Dictionary<TKey, TValue> | Хеш-таблица, ключ-значение | Быстрый поиск/вставка/удаление по ключу |
HashSet<T> | Уникальные элементы, операции над множествами | Проверка Contains, объединение, пересечение |
SortedSet<T> | Отсортированные уникальные элементы | Запросы по диапазону, порядковые операции |
LinkedList<T> | Двусвязный список | Вставка/удаление в середине |
Queue<T> | Очередь FIFO | Enqueue/Dequeue |
Stack<T> | Стек LIFO | Push/Pop |
Производительность обобщённых и необобщённых коллекций
// Обобщённая — типобезопасная, без боксинга
List<int> genericList = new() { 5, 9, 1, 4 };
// Необобщённая — требует боксинга/анбоксинга, ~75x медленнее
ArrayList nonGenericList = new() { 5, 9, 1, 4 };
Ключевые преимущества: Типобезопасность на этапе компиляции; отсутствие боксинга/анбоксинга для значимых типов (снижает нагрузку на GC); производительность: сортировка List<int> ~75x быстрее, чем ArrayList.
2. Временная сложность операций
List<T>
| Операция | Сложность | Примечания |
|---|---|---|
Индексатор [] | O(1) | Прямой доступ к массиву |
| Add (в конец) | O(1)* | Амортизированная, O(n) при изменении размера |
| Insert (по индексу) | O(n) | Сдвиг элементов |
| RemoveAt | O(n) | Сдвиг элементов |
| Remove (по значению) | O(n) | Линейный поиск + сдвиг |
| Contains | O(n) | Линейный поиск |
| Sort | O(n log n) | IntroSort |
| BinarySearch | O(log n) | Требует отсортированного списка |
Dictionary<TKey, TValue>
| Операция | Сложность | Примечания |
|---|---|---|
Индексатор [] (get/set) | O(1)* | Хеш-таблица с цепочками |
| TryGetValue | O(1)* | Амортизированная |
| Add | O(1)* | O(n) при изменении размера |
| Remove | O(1)* | — |
| ContainsKey | O(1)* | — |
*При равномерном распределении хеш-кодов. Худший случай (все ключи в одной корзине): O(n).
HashSet<T>
| Операция | Сложность | Примечания |
|---|---|---|
| Add | O(1)* | O(n) при изменении размера |
| Contains | O(1)* | — |
| Remove | O(1)* | — |
| SetEquals, IsSubsetOf и др. | O(n) | Перебор элементов |
SortedSet<T>
| Операция | Сложность | Примечания |
|---|---|---|
| Add | O(log n) | Балансировка красно-чёрного дерева |
| Contains | O(log n) | — |
| Remove | O(log n) | — |
| GetViewBetween | O(log n) | Создаёт представление без копирования |
| Min / Max | O(1) | Левый/правый край дерева |
LinkedList<T>
| Операция | Сложность | Примечания |
|---|---|---|
| AddFirst/AddLast | O(1) | — |
| InsertAfter/Before | O(1) | При наличии ссылки на узел |
| Find (по значению) | O(n) | Линейный обход |
| Remove (по узлу) | O(1) | — |
| Remove (по значению) | O(n) | Поиск + удаление |
Queue<T> и Stack<T>
Обе используют кольцевой массив:
| Операция | Queue<T> | Stack<T> |
|---|---|---|
| Enqueue / Push | O(1)* | O(1)* |
| Dequeue / Pop | O(1)* | O(1) |
| Peek | O(1) | O(1) |
3. Внутреннее устройство коллекций
List<T> — динамический массив со стратегией роста 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++;
}
}
Ключевые моменты: Начальная ёмкость: 0 (пустой _emptyArray); первый Add → ёмкость становится 4; каждое последующее изменение размера → ёмкость удваивается (4→8→16→32...); для 10 млн элементов требуется всего ~24 операции изменения размера.
Оптимизация: если размер известен — установите ёмкость заранее:
var list = new List<int>(expectedCount); // Избегает всех операций изменения размера
Dictionary<TKey, TValue> — хеш-таблица с цепочками (цепочки коллизий)
.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();
}
Почему цепочки вместо открытой адресации?
- Цепочки лучше справляются с высокими коэффициентами загрузки (>70%).
- Открытая адресация (используется в старом
Hashtable) страдает от кластеризации при плохих хешах. - Цепочки дают более стабильную и предсказуемую производительность при росте коллекции.
SortedSet<T> — красно-чёрное дерево
public class SortedSet<T>
{
private struct Node
{
public T Item; // Значение
public Node Left; // Левый потомок (меньше)
public Node Right; // Правый потомок (больше)
public Node Parent; // Родитель
internal bool Color; // true = Красный, false = Чёрный
}
}
Свойства красно-чёрного дерева: Каждый узел красный или чёрный; корень всегда чёрный; красный узел не может иметь красный потомок; все пути от узла до листьев содержат одинаковое количество чёрных узлов. Гарантия: высота дерева ≤ 2*log2(n+1), обеспечивая O(log n) для всех операций.
LinkedList<T>, Queue<T>, Stack<T>
LinkedList<T> — двусвязный список; LinkedListNode<T> — публичная обёртка; вставка/удаление по ссылке на узел: O(1); поиск по значению: O(n).
Queue<T> и Stack<T> — кольцевой массив:
private T[] _array;
private int _head; // Индекс первого элемента
private int _tail; // Индекс следующего свободного слота
private int _size;
4. Параллельные коллекции (System.Collections.Concurrent)
ConcurrentDictionary<TKey, TValue>
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) — несколько сегментов блокируются независимо, позволяя параллельные операции с разными ключами.
5. Неизменяемые коллекции (System.Collections.Immutable)
| Изменяемая | Неизменяемый эквивалент | Особенности |
|---|---|---|
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) — заморозка без копирования
6. Span<T> vs List<T> — Когда что использовать
| Критерий | List<T> | Span<T> |
|---|---|---|
| Выделение памяти | Куча (GC) | Стек / inline (без GC) |
| Размер | Динамический (изменение размера) | Фиксированный при создании |
| Совместимость с async | Да | Нет (ref struct) |
| Хранение в поле класса | Да | Нет |
| Срез | GetRange() → новый объект | Slice() → представление без выделения памяти |
7. IEnumerable<T> vs IReadOnlyCollection<T> vs IReadOnlyList<T>
| Интерфейс | Индексатор | Count | Перебор | Изменяемый? |
|---|---|---|---|---|
IEnumerable<T> | Нет | Нет | Да | Неизвестно |
IReadOnlyCollection<T> | Нет | Да | Да | Нет |
IReadOnlyList<T> | Да O(1)* | Да | Да | Нет |
8. Практические упражнения
Бенчмарк: List<T> vs ArrayPool<T>
using System.Buffers;
using System.Diagnostics;
using BenchmarkDotNet.Attributes;
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 быстрее для одноразовых операций.
9. Контрольные вопросы
- Как работает
GetHashCode()и почему важна правильная реализация? - В чём разница между
IEnumerable<T>,IReadOnlyCollection<T>,IReadOnlyList<T>? - Когда следует использовать неизменяемые коллекции?
LINQ и функциональное программирование в C#/.NET
1. Ленивое выполнение vs немедленное выполнение
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
Ключевой момент: Каждое перечисление повторно выполняет всю цепочку запросов. Если исходные данные изменяются между перечислениями, результаты будут отличаться.
2. 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 (завершено)
}
Промежуточная коллекция не создаётся — значения генерируются по требованию; состояние сохраняется между вызовами MoveNext().
3. IEnumerable<T> vs IQueryable<T>
| Аспект | IEnumerable<T> | IQueryable<T> |
|---|---|---|
| Место выполнения | В памяти (на клиенте) | Удалённо (база данных, сервис) |
| Представление запроса | Делегат Func<> | Дерево выражений Expression<Func<>> |
| Фильтрация | Загружает все данные, фильтрует в памяти | Переводит в SQL/нативный запрос |
| Методы расширения | System.Linq.Enumerable | System.Linq.Queryable |
4. Стандартные операторы запросов
Проекция: Select — преобразование; SelectMany — сглаживание вложенных коллекций; OfType<T> — фильтрация по типу.
Фильтрация: Where, Distinct, Except, Intersect.
Группировка: GroupBy, ToLookup, GroupJoin.
Соединение:
var joined = customers
.Join(orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { Customer = c, Order = o });
Агрегация: Sum, Average, Min, Max, Count, Aggregate.
5. ValueTuple vs Tuple
| Аспект | Tuple<T1,T2,...> | (T1, T2, ...) / ValueTuple |
|---|---|---|
| Тип | Ссылочный тип (class) | Значимый тип (struct) |
| Имена полей | Всегда Item1, Item2... | Настраиваемые имена |
| Производительность | Выделение в куче (GC) | Стек/inline (без GC) |
| Деконструкция | Нет | Да |
| Доступно с | .NET 4.0 | .NET Core 2.0 / .NET Standard 2.1 |
6. Expression<T> — Как работает и где применяется
// Лямбда как КОД (скомпилирована в делегат)
Func<int, bool> code = x => x > 5;
code(10); // True
// Лямбда как ДАННЫЕ (дерево выражений)
Expression<Func<int, bool>> data = x => x > 5;
// Сначала нужно скомпилировать:
Func<int, bool> compiled = data.Compile();
7. Бенчмарк: LINQ vs foreach для 1 млн элементов
| Метод | Среднее время | Выделено памяти |
|---|---|---|
| ForeachSum | ~1мс | 0 Б |
| LinqSum | ~3-5мс | ~20 КБ |
| ParallelLinqSum | ~2-3мс | ~100 КБ |
Ключевой вывод: foreach быстрее для простых операций. LINQ добавляет накладные расходы на выделение делегатов.
Практика
Контрольные вопросы
- Что такое deferred execution и как это влияет на производительность?
- В чём разница между
First(),FirstOrDefault(),Single(),SingleOrDefault()? - Как работает
Expression<T>и где применяется?
Делегаты, события и выражения в C#/.NET
1. Встроенные типы делегатов
| Делегат | Возвращаемый тип | Лучше всего для |
|---|---|---|
Func<T, TResult> | TResult | Преобразования, вычисления, LINQ Select |
Action<T> | void | Побочные эффекты, обратные вызовы |
Predicate<T> | bool | Условия, фильтрация (List.Find, List.RemoveAll) |
Comparison<T> | int | Сортировка пользовательских коллекций |
2. Мультикаст-делегаты — внутреннее устройство
Мультикаст-делегат — это связанный список делегатов:
// [Delegate3] → [Delegate2] → [Delegate1] → null
// ^первый ^добавлен вторым ^добавлен первым (голова)
Action<string> handlers = Message1;
handlers += Message2;
handlers += Message3;
handlers -= Message1;
handlers("Hello"); // Вызывает: Message2, Message3
Ключевые моменты: += добавляет в начало списка (O(1)); порядок вызова: в порядке добавления (FIFO); с Func — возвращается только возвращаемое значение последнего делегата!
3. События — паттерн издатель/подписчик
public class Publisher
{
public event Action<string> MessageReceived;
public void SendMessage(string message)
{
// Потокобезопасный паттерн вызова
MessageReceived?.Invoke(message);
}
}
4. Expression<T> — построение и компиляция деревьев выражений
// Программное построение: 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);
5. Динамическая компиляция через Expression.Compile()
| Подход | Первый вызов | Последующие вызовы |
|---|---|---|
| Рефлексия | ~100нс | ~100нс |
| Expression.Compile() + кэш | ~5мкс | ~5нс |
| Прямой вызов | ~1нс | ~1нс |
Практика
Сопоставление с образцом и современные возможности C#
1. Эволюция сопоставления с образцом
// Паттерны типов (C# 7)
if (value is int i)
Console.WriteLine($"Целое число: {i}");
// Реляционные паттерны (C# 9)
string Classify(double value) => value switch
{
< -100.0 => "Очень низко",
< 0.0 => "Низко",
0.0 => "Ноль",
> 0.0 and <= 100.0 => "Нормально",
> 100.0 => "Высоко",
_ => "Неизвестно",
};
// Паттерны свойств (C# 8)
if (person is { Age: >= 18, Email: not null })
Console.WriteLine("Взрослый с email");
// Рекурсивные паттерны (C# 9)
bool IsAnyEndOnXAxis(Segment seg) =>
seg is { Start: { Y: 0 } } or { End: { Y: 0 } };
// Паттерны списков (C# 8)
int[] CheckArray(int[] arr) => arr switch
{
[0, 1, 2, ..] => new[] { 1 },
[.. , 9, 8, 7] => new[] { 2 },
[1, .., 9] => new[] { 3 },
[var first, ..] => new[] { first },
[] => Array.Empty<int>(),
};
2. switch Expression vs switch Statement
| Аспект | switch Statement | switch Expression |
|---|---|---|
| Возвращает значение | Нет | Да |
| Требует break/goto | Да | Нет |
| Проверка полноты | Нет | Предупреждение компилятора |
| Безопасность null | Ручная проверка | Паттерн null работает естественно |
3. Типы record — равенство по значению и неразрушающее изменение
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!
Неразрушающее изменение с выражениями with:
var updated = original with { Age = 31 };
Console.WriteLine(original.Age); // 30 (не изменился!)
4. ref struct, required, file-scoped namespaces
ref struct — структуры, которые должны находиться на стеке — не могут быть боксированы. required modifier (C# 11+) — принудительная инициализация свойств на этапе компиляции.
5. Source Generators
Генерация кода во время компиляции: Исходный код → [Source Generator] → Дополнительный код → Компилятор → IL.
| Аспект | ISourceGenerator | IIncrementalSourceGenerator |
|---|---|---|
| Выполняется при | Полная компиляция | Только изменённые файлы |
| Производительность | Медленнее | Быстрее (инкрементально) |
| Рекомендация | Наследие | Используйте это |
Практика
Span<T>, Memory<T> и stackalloc
1. Span<T> — срез без выделения памяти
Span<T> — это ref struct, представляющий непрерывную область памяти любого размера без выделения в куче:
// Из массива — Span просматривает существующий массив (без копирования!)
int[] array = { 1, 2, 3, 4, 5 };
Span<int> spanFromArr = array;
// Из сегмента массива — представление части без выделения памяти
Span<int> slice = array.AsSpan(1, 3); // [2, 3, 4] — без копирования!
// Из памяти стека — действительно нулевое выделение
Span<int> stackSpan = stackalloc int[100];
// Из строки — доступ к символам без выделения памяти
string text = "Hello, World!";
ReadOnlySpan<char> spanFromStr = text;
2. Memory<T> — альтернатива Span<T> на основе кучи
| Возможность | Span<T> | Memory<T> |
|---|---|---|
| Тип | ref struct | struct (указатель + длина) |
| Совместимость с async | Нет | Да |
| Может быть полем класса | Нет | Да |
| Получить Span | Н/Д | Свойство .Span |
3. ArrayPool<T>.Shared — аренда массивов
int[] rented = ArrayPool<int>.Shared.Rent(100);
try
{
for (int i = 0; i < 100; i++) rented[i] = i * 2;
Process(rented.AsSpan(0, 100));
}
finally
{
ArrayPool<int>.Shared.Return(rented);
}
4. stackalloc — выделение на стеке
Span<byte> buffer = stackalloc byte[1024]; // 1KB на стеке
Span<int> initialized = stackalloc int[5] { 1, 2, 3, 4, 5 };
// Очистка — содержимое стека не определено!
Span<byte> dirty = stackalloc byte[100];
dirty.Clear();
5. ReadOnlySequence<T> — discontinuous буферы
ReadOnlySequence<T> представляет последовательность блоков памяти (возможно, не непрерывных). Идеально для сетевых и файловых потоков.
foreach (var segment in sequence)
{
Span<byte> span = segment.Span;
ProcessChunk(span);
}
Практика
Reflection и Emit в C#/.NET
1. System.Reflection — основные типы
// Получение объектов Type
Type t1 = typeof(int);
Type t2 = typeof(List<string>);
Type t3 = someObject.GetType();
// Информация о типе
Console.WriteLine(t1.Name); // "Int32"
Console.WriteLine(t1.IsValueType); // true
// Получение свойств и методов
PropertyInfo[] props = type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
MethodInfo method = type.GetMethod("PublicMethod");
2. Стоимость рефлексии — кэширование и CreateDelegate
| Метод | Время на вызов | Выделено памяти |
|---|---|---|
| Прямой доступ | ~1нс | 0 Б |
| PropertyInfo.GetValue (кэшированный) | ~20-50нс | 0 Б |
| Delegate.CreateDelegate | ~5-10нс | 0 Б |
| Expression.Compile getter | ~5-10нс | 0 Б |
| Рефлексия без кэша | ~100-500нс | варируется |
// Создание быстрого делегата-геттера
var getter = DelegateFactory.CreateGetter<Person, string>("Name");
string name = getter(person); // ~5нс — скорость, близкая к прямому доступу!
3. System.Reflection.Emit — динамическая генерация кода
// Создание динамической сборки
AssemblyName asmName = new("DynamicAssembly");
AssemblyBuilder asmBuilder = AssemblyBuilder.DefineDynamicAssembly(
asmName, AssemblyBuilderAccess.Run);
// Определение типа
TypeBuilder typeBuilder = module.DefineType("DynamicGreeter", TypeAttributes.Public | TypeAttributes.Class);
// Генерация IL
ILGenerator il = methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello, ");
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Call, typeof(string).GetMethod("Concat")!);
il.Emit(OpCodes.Ret);
// Создание типа
Type createdType = typeBuilder.CreateType();
Практика
Атрибуты и метаданные в C#/.NET
1. Пользовательские атрибуты
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyAttribute : Attribute
{
public string Description { get; }
public MyAttribute(string description) => Description = description;
}
[My("Это сущность Person")]
public class Person { }
2. Чтение метаданных: во время выполнения vs на этапе компиляции
| Аспект | Рефлексия (runtime) | Roslyn (compile-time) |
|---|---|---|
| Когда выполняется | Во время выполнения | Во время компиляции |
| Производительность | Стоимость при каждом вызове | Нулевая стоимость runtime |
| Варианты использования | Валидация, DI, сериализация | Генерация кода, анализаторы |
3. API Roslyn — SyntaxTree и SemanticModel
SyntaxTree tree = CSharpSyntaxTree.ParseText(source);
Compilation compilation = CSharpCompilation.Create("MyAssembly", syntaxTrees: new[] { tree });
SemanticModel semanticModel = compilation.GetSemanticModel(tree);
// Разрешение типов
INamedTypeSymbol symbol = semanticModel.GetDeclaredSymbol(classNode)!;
Console.WriteLine(symbol.ToDisplayString()); // global::Person
Практика
Внутренности BCL и среда выполнения
1. Понимание исходного кода BCL
| Ресурс | URL | Назначение |
|---|---|---|
| .NET Runtime (GitHub) | github.com/dotnet/runtime | Основные библиотеки, среда выполнения, JIT |
| Reference Source | referencesource.microsoft.com | Исходный код .NET Framework 4.8 |
| SharpLab | sharplab.io | Просмотр IL/state machine |
2. ObjectPool<T> — внутреннее устройство
var pool = new ObjectPool<MyResource>(() => new MyResource(), maxRetained: 100);
MyResource resource = pool.Rent();
try { resource.DoWork(); }
finally { pool.Return(resource, shouldReset: true); }
3. Lazy<T> — режимы потокобезопасности
| Режим | Потокобезопасность | Производительность |
|---|---|---|
None | Не потокобезопасно | Быстрее всего |
PublicationOnly | Первый побеждает | Быстро |
ExecutionAndPublication | Подобно мьютексу | Медленнее |
4. ConditionalWeakTable<TKey, TValue> — слабые ссылки
ConditionalWeakTable хранит слабые ссылки на ключи и сильные ссылки на значения. Когда ключ собирается GC, связанное значение также удаляется.
5. AsyncLocal<T> vs [ThreadStatic]
| Аспект | [ThreadStatic] | AsyncLocal<T> |
|---|---|---|
| Область хранения | На поток ОС | На контекст выполнения |
| Работает с async/await | ❌ Значение теряется | ✅ Передаётся через продолжения |
| Безопасность ThreadPool | ⚠️ Старые значения | ✅ Чистый на async-контекст |
| Лучше всего для | Наследие синхронного кода | Современные async-приложения |
6. Внутреннее устройство параллельных коллекций
ConcurrentDictionary — сегментная блокировка (~16 параллельных записей); ConcurrentQueue — очередь Майкла-Скотта без блокировок (CAS/CompareExchange).
Практика
Контрольная точка модуля 1
Создать высокопроизводительную библиотеку коллекций с:
- Zero-allocation API через
Span<T> - Кастомными comparer'ами и hash code providers
- Полным LINQ-compatible интерфейсом
- Benchmark сравнением с BCL аналогами
- Все benchmark'и показывают конкурентную производительность
- Code coverage > 90%
- Документация XML comments для всех public API