SprintCode.pro

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

Super

Собеседование на C#-разработчика: вопросы с разбором

14 мин чтения
собеседование
карьера
подготовка

Коротко

БлокЧто проверяют
Типызначимые и ссылочные, боксинг
Память и 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, иначе на каждой итерации создаётся новый объект.

Пройди собеседование в топ-компанию
Платформа для подготовки

Решай алгоритмические задачи как профи

✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Программист за работой

Блок 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)уникальность
SortedDictionaryO(log n)O(log n)нужен порядок
Queue / StackO(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 ломает аутентификацию — типичный вопрос на собеседовании.

Что спрашивают чаще всего

  1. Разница значимых и ссылочных типов, боксинг.
  2. Как работает GC, зачем IDisposable.
  3. Что делает await, почему async void плохо.
  4. Дедлок при .Result.
  5. Отложенное выполнение LINQ.
  6. Времена жизни в DI и ловушка Scoped-в-Singleton.
  7. string неизменяем, зачем StringBuilder.

Что запомнить

  • Значимые типы копируются по значению, ссылочные — по ссылке; боксинг незаметен, но дорог.
  • GC управляет памятью, IDisposable — неуправляемыми ресурсами.
  • await не создаёт поток, а освобождает его; .Result в контексте синхронизации даёт дедлок.
  • async void только для обработчиков событий.
  • LINQ выполняется отложенно — материализуйте результат, если перебираете несколько раз.
  • Не внедряйте Scoped-сервисы в Singleton.