> С какими типами данных чаще всего работали: списки, словари, массивы (C#)

Уровень: senior · Роль: backend · Язык: C# · Категория: Технические вопросы

Компании: ATLASстартап

Стек: C# /.NET

> Пример ответа

Короткий ответ

В C# чаще всего работал со списками (List<T>) и словарями (Dictionary<TKey, TValue>). Массивы (T[]) использовал реже - в основном для фиксированных буферов, низкоуровневых операций или hot path, где важна производительность без аллокаций. Словари - основной инструмент для lookup-сценариев, списки - для динамических коллекций с последовательным доступом. Также активно использовал HashSet<T> для проверки уникальности и IReadOnlyCollection<T> для контрактов API.

Подробное объяснение

Выбор типа данных в C# определяется не только семантикой, но и особенностями runtime:

  • List<T> - динамический массив с амортизированной O(1) вставкой в конец. Индексация O(1), вставка/удаление в середине - O(n). Использую как основную коллекцию по умолчанию.
  • Dictionary<TKey, TValue> - hash table, O(1) в среднем для lookup/insert/delete. Критично правильно выбирать comparer (default vs ordinal для строк). При большом количестве коллизий деградирует до O(n).
  • T[] - фиксированный размер, минимальный overhead, cache-friendly. Использую для Span<T>, stackalloc, буферов, сериализации.
  • HashSet<T> - по сути dictionary без значений, полезен для deduplication и set operations.
  • LinkedList<T> - практически не использую, так как в большинстве случаев List<T> с индексами эффективнее из-за cache locality.
  • ConcurrentDictionary<TKey, TValue> - для многопоточных сценариев, но с осторожностью: lock-free внутри, но операции не атомарны без дополнительной синхронизации.

Важный trade-off: List<T> аллоцирует массив с запасом (capacity), что может приводить к лишней памяти. Для больших коллекций с известным размером лучше сразу задавать capacity или использовать массивы.

На практике

В реальных backend-задачах на C#:

  • Для запросов к БД через EF Core - IQueryable<T> → List<T> после материализации.
  • Для кэшей - ConcurrentDictionary<TKey, TValue> или MemoryCache.
  • Для обработки пайплайнов - IEnumerable<T> с ленивой оценкой, но осторожно: множественные итерации по IEnumerable могут привести к повторному выполнению запроса.
  • Для high-performance сценариев - ArrayPool<T>, Span<T>, Memory<T> вместо массивов и списков.
  • Для API-контрактов - IReadOnlyList<T> и IReadOnlyDictionary<TKey, TValue> чтобы не раскрывать изменяемость.

Типичный паттерн: получить данные из БД → List<T>, построить Dictionary<TKey, TValue> для lookup, вернуть IReadOnlyCollection<T> наружу.

Пример кода

CSHARP
// Типичный сценарий: маппинг и lookup
public class OrderService
{
private readonly Dictionary<Guid, Order> _ordersById = new();
private readonly List<Order> _recentOrders = new();
private readonly HashSet<Guid> _processedOrderIds = new();
public void ProcessOrders(IEnumerable<Order> orders)
{
foreach (var order in orders)
{
if (!_processedOrderIds.Add(order.Id))
continue; // уже обработан
_ordersById[order.Id] = order;
_recentOrders.Add(order);
}
}
public Order? FindOrder(Guid id) =>
_ordersById.GetValueOrDefault(id);
// Для hot path - массив и Span
public double CalculateAverage(int[] values)
{
Span<int> span = values;
long sum = 0;
foreach (var v in span) sum += v;
return (double)sum / values.Length;
}
}

Как отвечать на собеседовании

Начните с прямого ответа: List<T> и Dictionary<TKey, TValue> - основные. Затем объясните критерии выбора: сложность операций, аллокации, cache locality, потокобезопасность. Приведите конкретные примеры из практики: кэширование, маппинг DTO, обработка пайплайнов. Упомяните, что массивы используете для fixed-size буферов и Span<T>. Покажите понимание внутреннего устройства: capacity, resize, hash collisions. Если спросят про LinkedList<T> - объясните, почему он редко нужен. Завершите упоминанием современных API: ImmutableArray<T>, FrozenDictionary<TKey, TValue> в .NET 8+.

Что проверяет интервьюер

  • Понимание сложности операций и trade-off между коллекциями.
  • Умение выбирать структуру данных под задачу, а не по привычке.
  • Знание внутреннего устройства: capacity, resize, hash collisions, comparer.
  • Осведомленность о современных API: Span<T>, Memory<T>, FrozenDictionary.
  • Понимание потокобезопасности и когда нужен ConcurrentDictionary.
  • Умение объяснить, почему List<T> часто лучше LinkedList<T> в .NET.

Типичные ошибки

  • Говорить "массивы" как основной тип, не упоминая List<T> - это сразу выдаёт слабое знание C#.
  • Не различать IEnumerable<T> и List<T>: множественная итерация по IEnumerable - частая ошибка.
  • Использовать Dictionary<TKey, TValue> для маленьких коллекций (до ~10 элементов) - линейный поиск по массиву быстрее.
  • Забывать про HashSet<T> для проверки уникальности - вместо этого используют List.Contains(), что даёт O(n).
  • Не упоминать capacity: new List<T>(n) и new Dictionary<TKey, TValue>(n) экономят реаллокации.
  • Путать ConcurrentDictionary с потокобезопасностью отдельных операций - составные операции требуют внешней синхронизации.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью