Подготовка к алгоритмическим задачам

Собеседование на C#-разработчика: вопросы с разбором
Коротко
| Блок | Что проверяют |
|---|---|
| Типы | значимые и ссылочные, боксинг |
| Память и GC | поколения, IDisposable, финализаторы |
| Асинхронность | async/await, Task, дедлоки |
| Коллекции | List, Dictionary, IEnumerable |
| LINQ | отложенное выполнение |
| ASP.NET Core | внедрение зависимостей, middleware |
Блок 1. Типы значений и ссылок
В чём разница
Значимые типы (struct, int, bool, enum) хранят данные напрямую и обычно живут в стеке. Ссылочные (class, string, массивы) хранят ссылку на объект в куче.
struct PointStruct { public int X; } class PointClass { public int X; } var s1 = new PointStruct { X = 1 }; var s2 = s1; // копия s2.X = 99; // s1.X == 1 — оригинал не изменился var c1 = new PointClass { X = 1 }; var c2 = c1; // копия ССЫЛКИ c2.X = 99; // c1.X == 99 — тот же объект
Что такое боксинг
Упаковка значимого типа в объект на куче при приведении к object или интерфейсу.
int number = 42; object boxed = number; // боксинг: выделение памяти в куче int back = (int)boxed; // анбоксинг
Боксинг незаметен, но дорог. Классический пример вреда:
// боксинг на каждом элементе ArrayList list = new ArrayList(); for (int i = 0; i < 1_000_000; i++) list.Add(i); // без боксинга — обобщённая коллекция List<int> generic = new List<int>();
Именно ради устранения боксинга в C# 2.0 появились обобщения.
Почему string ведёт себя как значимый тип
string — ссылочный тип, но неизменяемый. Любая операция создаёт новую строку.
string s = "a"; s += "b"; // создана новая строка, старая пойдёт в мусор
Отсюда классический совет: конкатенация в цикле — через StringBuilder, иначе на каждой итерации создаётся новый объект.
Решай алгоритмические задачи как профи

Блок 2. Память и сборка мусора
Как работает GC
Куча делится на три поколения. Новые объекты попадают в поколение 0. Пережившие сборку переходят в первое, затем во второе.
Логика в том, что большинство объектов умирает молодыми, поэтому чаще всего собирается только нулевое поколение — это дёшево.
Отдельно существует куча больших объектов (LOH) для объектов больше 85 КБ. Она собирается редко и не уплотняется, что приводит к фрагментации.
IDisposable и using
GC управляет только памятью. Файлы, сокеты, соединения с БД — неуправляемые ресурсы, их нужно освобождать явно.
using (var connection = new SqlConnection(cs)) { connection.Open(); } // Dispose вызовется даже при исключении // C# 8: краткая форма using var connection = new SqlConnection(cs);
Что проверяют: понимаете ли вы, что Dispose не про память, а про ресурсы, и что финализатор — не замена Dispose (он вызывается недетерминированно и замедляет сборку).
Блок 3. Асинхронность
Что делает await
Не создаёт поток. Он освобождает текущий поток на время ожидания и возобновляет выполнение по завершении операции.
public async Task<string> LoadAsync() { var response = await httpClient.GetAsync(url); // поток свободен return await response.Content.ReadAsStringAsync(); }
Почему async void — плохо
async void Bad() { } // исключение нельзя поймать, нельзя дождаться async Task Good() { } // правильно
Исключение из async void не попадает в вызывающий код и роняет процесс. Единственное допустимое применение — обработчики событий.
Классический дедлок
// в ASP.NET (старом) или WPF это повесит приложение public string Get() { return LoadAsync().Result; // блокировка контекста }
.Result и .Wait() блокируют поток, который нужен для продолжения await. Получается взаимная блокировка.
Правильно — async до самого верха. Если совсем нельзя, ConfigureAwait(false) в библиотечном коде снимает привязку к контексту синхронизации.
Это один из самых частых вопросов на позициях middle и выше.
Task.Run: когда нужен
// НЕ нужен: операция и так асинхронная await Task.Run(() => httpClient.GetAsync(url)); // нужен: тяжёлые вычисления, чтобы не блокировать UI await Task.Run(() => HeavyComputation());
Правило: Task.Run — для загрузки процессора, обычный await — для ввода-вывода.
Блок 4. Коллекции
| Коллекция | Поиск | Вставка | Когда |
|---|---|---|---|
List<T> | O(n) | O(1) в конец | по умолчанию |
Dictionary<K,V> | O(1) | O(1) | поиск по ключу |
HashSet<T> | O(1) | O(1) | уникальность |
SortedDictionary | O(log n) | O(log n) | нужен порядок |
Queue / Stack | — | O(1) | FIFO / LIFO |
IEnumerable, ICollection, IList
IEnumerable<T> — только перебор. ICollection<T> добавляет Count, Add, Remove. IList<T> добавляет доступ по индексу.
Практическое правило: принимайте максимально общий тип, возвращайте конкретный. Параметр метода — IEnumerable<T>, чтобы принять что угодно.
Блок 5. LINQ
Отложенное выполнение
Главная особенность и главный источник багов.
var query = list.Where(x => x > 5); // запрос НЕ выполнен list.Add(10); var result = query.ToList(); // выполняется здесь, увидит 10
Запрос выполняется в момент перебора: ToList(), Count(), First(), foreach.
Отсюда классическая ошибка — множественный перебор:
// запрос выполнится ТРИ раза if (query.Any()) Console.WriteLine(query.Count() + " " + query.First()); // правильно: материализовать один раз var items = query.ToList();
При работе с базой данных это означает три обращения вместо одного.
First против FirstOrDefault
First() бросает исключение, если ничего не найдено. FirstOrDefault() возвращает значение по умолчанию (null для ссылочных типов, 0 для int).
Тонкость: для int вернётся 0, что может быть валидным значением. Отличить «не найдено» от «нашли ноль» тогда невозможно — нужен int?.
Блок 6. ASP.NET Core
Времена жизни зависимостей
| Время жизни | Экземпляр |
|---|---|
Singleton | один на приложение |
Scoped | один на HTTP-запрос |
Transient | новый при каждом запросе зависимости |
Классическая ловушка: внедрение Scoped в Singleton. Scoped-сервис останется жить дольше своего запроса, что приводит к утечкам и гонкам. Например, DbContext (Scoped) внутри синглтона — источник трудноуловимых багов.
Middleware
Конвейер обработки запроса. Порядок регистрации определяет порядок выполнения.
app.UseRouting(); app.UseAuthentication(); // до авторизации app.UseAuthorization(); app.MapControllers();
Перестановка UseAuthentication и UseAuthorization ломает аутентификацию — типичный вопрос на собеседовании.
Что спрашивают чаще всего
- Разница значимых и ссылочных типов, боксинг.
- Как работает GC, зачем
IDisposable. - Что делает
await, почемуasync voidплохо. - Дедлок при
.Result. - Отложенное выполнение LINQ.
- Времена жизни в DI и ловушка Scoped-в-Singleton.
stringнеизменяем, зачемStringBuilder.
Что запомнить
- Значимые типы копируются по значению, ссылочные — по ссылке; боксинг незаметен, но дорог.
- GC управляет памятью,
IDisposable— неуправляемыми ресурсами. awaitне создаёт поток, а освобождает его;.Resultв контексте синхронизации даёт дедлок.async voidтолько для обработчиков событий.- LINQ выполняется отложенно — материализуйте результат, если перебираете несколько раз.
- Не внедряйте Scoped-сервисы в Singleton.
