Продвинутое собеседование по C#: как удивить работодателя и не провалиться
Собеседование на позицию Senior или Middle .NET-разработчика — это не просто проверка синтаксиса. Интервьюеры хотят видеть глубокое понимание внутренней работы платформы, умение писать производительный код и принимать архитектурные решения. В этой статье мы разберем ключевые темы и вопросы продвинутого уровня, которые помогут вам пройти техническое интервью на C#.
1. Асинхронное программирование: async/await и Task
Это, пожалуй, самая популярная тема на собеседованиях. Мало сказать, что вы знаете async/await. Нужно понимать, как оно работает под капотом.
Вопрос: Что такое SynchronizationContext и как он влияет на выполнение await?
Классический вопрос. SynchronizationContext — это абстракция, которая позволяет планировать выполнение кода в определенном контексте (например, в UI-потоке WPF или в пуле потоков ASP.NET). Когда вы используете await, после завершения асинхронной операции код по умолчанию пытается вернуться в исходный контекст. В консольных приложениях это пул потоков, а в UI — поток диспетчера. Понимание этого механизма помогает избежать дедлоков.
Вопрос: В чем разница между Task.Run и Task.Factory.StartNew?
На первый взгляд они похожи, но Task.Run — это упрощенная обертка. Task.Factory.StartNew дает больше контроля (например, можно задать TaskCreationOptions или TaskScheduler). Однако по умолчанию Task.Run использует TaskScheduler.Default (пул потоков), а Task.Factory.StartNew может использовать текущий планировщик, что иногда приводит к неожиданному поведению.
// Пример дедлока из-за SynchronizationContext (в UI приложении)
public async Task<string> GetDataAsync()
{
var result = await SomeAsyncMethod(); // захват контекста
return result.ToString(); // пытается вернуться в UI-поток
}
// Если вызывать .Result или .Wait() на задаче, возникнет deadlock
var data = GetDataAsync().Result; // ПЛОХО!
2. Управление памятью и Garbage Collector (GC)
Продвинутый разработчик должен понимать, как .NET управляет памятью, чтобы избегать утечек и оптимизировать производительность.
Вопрос: Объясните поколения GC (Gen 0, Gen 1, Gen 2) и Large Object Heap (LOH).
GC в .NET использует поколенческую модель. Объекты сначала попадают в Gen 0 (сборка происходит часто). Если они переживают сборку, переходят в Gen 1, затем в Gen 2. LOH предназначен для объектов размером более 85 000 байт. Он не сжимается по умолчанию (до .NET 4.5.1), что может вызвать фрагментацию. Вопрос может быть усложнен: "Как работает фоновая сборка мусора?" или "Что такое GC режимы: Workstation vs Server?"
Вопрос: Что такое финализация (Finalization) и как избежать связанных с ней проблем?
Финализаторы (~Finalizer()) замедляют сборку мусора, так как объекты с финализаторами попадают в очередь финализации и требуют двух проходов GC. Правильный паттерн — реализация IDisposable с паттерном Dispose.
public class ResourceHolder : IDisposable
{
private IntPtr _handle;
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Освобождаем управляемые ресурсы
}
// Освобождаем неуправляемые ресурсы (handle)
_disposed = true;
}
}
~ResourceHolder()
{
Dispose(false);
}
}
3. Делегаты, события и LINQ: от синтаксиса к пониманию
Эти темы часто проверяют на понимание "замыканий" и ленивых вычислений.
Вопрос: В чем разница между делегатом и событием?
Событие — это обертка над делегатом. Главное отличие: извне класса вы можете только подписаться или отписаться от события (+= / -=), но не можете вызвать его напрямую или перезаписать список подписчиков (=). Делегат же позволяет это делать. Это принцип инкапсуляции.
Вопрос: Как работает LINQ и что такое отложенное выполнение (deferred execution)?
LINQ строится на интерфейсах IEnumerable<T> и IQueryable<T>. Большинство методов (Where, Select, OrderBy) не выполняются сразу. Они возвращают итератор, который начнет выполнение только при переборе (foreach, ToList). Это позволяет строить цепочки запросов без лишних аллокаций. Понимание разницы между IEnumerable и IQueryable (работа с SQL через Expression Trees) — must have для продвинутого уровня.
// Отложенное выполнение
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var query = numbers.Where(n => n > 2); // Запрос не выполнен
numbers.Add(6); // Меняем исходную коллекцию
foreach (var num in query) // Только здесь выполняется фильтрация
{
Console.WriteLine(num); // Выведет 3,4,5,6
}
4. Многопоточность, блокировки и Concurrent Collections
Вопросы на эту тему показывают, умеете ли вы проектировать безопасные и масштабируемые системы.
Вопрос: В чем разница между lock, Monitor и Mutex?
lock — это синтаксический сахар для Monitor.Enter/Exit. Monitor предоставляет больше возможностей (TryEnter, Pulse, Wait). Mutex — это межпроцессная блокировка (работает на уровне ОС), он медленнее. Для синхронизации внутри одного процесса используйте lock или Monitor.
Вопрос: Какие Concurrent Collections вы знаете и когда их использовать?
ConcurrentBag<T> — для сценариев "один поток пишет, много читают".