Подготовка к C# backend интервью

Вопросы и ответы на C# собеседовании 2026

Сфокусированная подборка C# вопросов для backend интервью. Ответы включают объяснение, нюансы и короткие примеры кода для устного разбора.

Начать C# mock interview на русском Практика технического интервью на английскомРежим, в котором не носители языка могут потренироваться проходить интервью.

Типы и основы языка

1Чем значимые типы отличаются от ссылочных?

Значимая переменная обычно содержит само значение, поэтому присваивание копирует значение. Переменная ссылочного типа содержит ссылку на объект, поэтому присваивание копирует ссылку, и обе переменные могут видеть изменение одного объекта. Важный нюанс: нельзя упрощать до 'value types всегда на stack, reference types всегда в heap'. Место хранения зависит от времени жизни и контекста: локальное значение метода может жить на стеке, а значимый тип в поле класса, элементе массива, замыкании, boxing или async state machine живет вместе с соответствующим объектом.

int a = 10;
int b = a;
b = 20;

// a == 10

var first = new User { Name = "Ann" };
var second = first;
second.Name = "Kate";

// first.Name тоже "Kate"

2Чем class, struct и record отличаются друг от друга?

class — ссылочный тип с identity объекта. struct — значимый тип, который лучше подходит для небольших неизменяемых значений: координат, диапазонов, дат, идентификаторов. record class — ссылочный тип с value-based equality; record struct — значимый вариант record. Record — не отдельный базовый тип CLR, а синтаксис компилятора, который генерирует equality, GetHashCode, ToString, deconstruction и поддержку копирования, включая оператор with для non-destructive mutation. В backend-коде records удобны для DTO-like моделей, а большие mutable structs нежелательны из-за копирования и неожиданных изменений копий.

public record User(int Id, string Name);

var first = new User(1, "Alice");
var second = new User(1, "Alice");

Console.WriteLine(first == second); // true

3Чем параметры ref, out и in отличаются?

ref передает аргумент по ссылке, значение должно быть инициализировано до вызова, а метод может читать и менять его. out тоже передает по ссылке, но метод обязан присвоить значение перед выходом. in передает readonly-ссылку и может избежать копирования больших struct, но не гарантирует полную неизменяемость: mutable structs и вызовы не-readonly методов могут вызвать defensive copies. На уровне IL ref, out и in являются managed byref параметрами с разными правилами C#.

void Increment(ref int value) => value++;

bool TryRead(string text, out int value) =>
    int.TryParse(text, out value);

double Length(in Vector vector) =>
    Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);

4Что такое boxing и unboxing?

Boxing упаковывает value type в object или другой reference-typed контейнер: CLR создает объект и копирует туда значение. Unboxing извлекает значение обратно и требует точного совместимого типа. Частый boxing создает allocations и нагрузку на GC. Generics помогают избежать многих boxing-сценариев, потому что List<int> хранит int без упаковки каждого элемента в object.

int number = 42;
object boxed = number;      // boxing
int restored = (int)boxed;  // unboxing

object value = 42;
// long wrong = (long)value; // InvalidCastException
long ok = (long)(int)value;

5Что такое nullable value types и nullable reference types?

Nullable value types создаются через Nullable<T> или T? и являются реальной runtime-оберткой для value type. Nullable reference types — в основном механизм compiler analysis: string? говорит компилятору, что ссылка может быть null, но не создает новый runtime type. Оператор ! только подавляет warning и не защищает от NullReferenceException.

int? age = null;

string name = "Alice";
string? optionalName = null;

var length = optionalName?.Length;
var actualName = optionalName ?? "Unknown";
optionalName ??= "Default";

string trusted = optionalName!;

6Чем const, readonly и static readonly отличаются?

const — compile-time constant только для primitive types, enum, string или null; значение подставляется в код использующей сборки при компиляции. readonly — instance field, который можно присвоить при объявлении или в constructor. static readonly — одно поле на тип, присваиваемое при объявлении или в static constructor. Для публичных значений, которые могут измениться в новой версии библиотеки, безопаснее static readonly, а не public const: без перекомпиляции клиент может сохранить старое запеченное значение const.

public const int MaxAttempts = 3;

private readonly Guid _id = Guid.NewGuid();

public static readonly TimeSpan Timeout =
    TimeSpan.FromSeconds(30);

7Как работают Equals, == и GetHashCode?

У обычных classes Equals и == по умолчанию сравнивают ссылки. Многие built-in types и records сравниваются по значению. Если переопределяется Equals, нужно согласованно переопределить GetHashCode: если a.Equals(b) true, hash codes обязаны совпадать. Обратное неверно. Нельзя менять поля, участвующие в equality/hash, пока объект является key в Dictionary или элементом HashSet.

public sealed class User : IEquatable<User>
{
    public required int Id { get; init; }

    public bool Equals(User? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is User other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();
}

8Чем override отличается от new?

override переопределяет virtual behavior, поэтому вызываемый метод зависит от фактического runtime type объекта. new скрывает member базового класса, и выбор зависит от static type переменной. Для polymorphism обычно нужен virtual/override. Скрытие через new используется реже и может запутывать поведение.

class Base
{
    public virtual void First() => Console.WriteLine("Base.First");
    public void Second() => Console.WriteLine("Base.Second");
}

class Derived : Base
{
    public override void First() => Console.WriteLine("Derived.First");
    public new void Second() => Console.WriteLine("Derived.Second");
}

Base value = new Derived();
value.First();  // Derived.First
value.Second(); // Base.Second

9Чем abstract class отличается от interface?

Abstract class может содержать state, constructors, fields, implemented methods, abstract methods и protected members. Interface описывает contract; современные interfaces могут иметь default implementations и static abstract members, но не предназначены для обычного instance state. Класс наследуется только от одного base class, но реализует несколько interfaces. Abstract class выбирают для общей основы и реализации, interface — для capability и boundaries.

public interface IAsyncRepository<T>
{
    Task<T?> FindAsync(
        int id,
        CancellationToken cancellationToken);
}

Generics, коллекции и LINQ

10Что такое generics и зачем они нужны?

Generics позволяют писать reusable type-safe code. Они переносят проверки типов на compile time, убирают ручные casts и могут избежать boxing для value types. Generic constraints описывают требования к параметру типа: класс, структура, реализация интерфейса, базовый класс, неуправляемый тип (unmanaged) или публичный конструктор без параметров.

public T? Find<T>(
    IEnumerable<T> items,
    Predicate<T> predicate)
{
    return items.FirstOrDefault(item => predicate(item));
}

public T Create<T>() where T : class, new()
{
    return new T();
}

11Что такое covariance и contravariance?

Covariance позволяет использовать более конкретный produced type там, где ожидается более общий, например IEnumerable<string> как IEnumerable<object>. Она обозначается out. Contravariance работает для принимаемых значений в обратном направлении и обозначается in. Вариативность поддерживается только для interfaces и delegates со ссылочными типами (reference types): IEnumerable<int> нельзя привести к IEnumerable<object>, потому что для этого потребовался бы boxing; List<string> нельзя присвоить List<object>.

IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // covariance

Action<object> printObject = Console.WriteLine;
Action<string> printString = printObject; // contravariance

interface IProducer<out T> { T Produce(); }
interface IConsumer<in T> { void Consume(T value); }

12Чем IEnumerable<T> отличается от IQueryable<T>?

IEnumerable<T> работает с .NET enumeration, а LINQ-операторы обычно принимают delegates вроде Func<T, bool>. IQueryable<T> хранит expression tree, например Expression<Func<T, bool>>, который provider вроде Entity Framework Core может преобразовать в SQL. Ранний ToList материализует данные и переносит дальнейшую фильтрацию в память. Не каждое C# expression можно перевести в SQL. Не стоит без необходимости протаскивать IQueryable через все слои приложения.

IEnumerable<User> loaded = loadedUsers;
var inMemoryAdults = loaded.Where(x => x.Age >= 18);

IQueryable<User> query = dbContext.Users;
var adults = await query
    .Where(x => x.Age >= 18)
    .ToListAsync();

13Что такое deferred execution в LINQ?

Многие LINQ-операции строят query, но не выполняют его сразу. Выполнение начинается при итерации (enumeration) или terminal operation: ToList, Count, First. Поэтому изменения источника данных до итерации могут повлиять на результат, повторная итерация может повторить вычисление или SQL query, а exception может возникнуть не при создании query, а при перечислении.

var query = numbers.Where(x => x > 10);

numbers.Add(42);

foreach (var number in query)
{
    Console.WriteLine(number);
}

var materialized = query.ToList();

14Чем отличаются array, List<T>, Dictionary<TKey,TValue> и HashSet<T>?

Array имеет фиксированный размер и быстрый indexed access. List<T> — dynamic array: доступ по индексу O(1), Add в конец работает за амортизированное O(1), потому что при росте capacity выделяется новый массив и копируются элементы, а вставка в середину O(n). Dictionary<TKey,TValue> хранит key-value pairs и обычно дает O(1) lookup по ключу. HashSet<T> хранит уникальные элементы и подходит для membership checks. Для hash collections критичны корректные Equals и GetHashCode.

var ids = new HashSet<int> { 1, 2, 3 };

if (ids.Contains(userId))
{
    // Fast membership check
}

15Чем отличаются IEnumerable<T>, ICollection<T>, IList<T> и IReadOnlyCollection<T>?

IEnumerable<T> обещает только последовательное перечисление. ICollection<T> добавляет Count и mutation methods вроде Add/Remove. IList<T> добавляет indexed access. IReadOnlyCollection<T> дает enumeration и Count, но не mutation через этот interface. В API лучше возвращать самый узкий полезный contract, но read-only interface не гарантирует deep immutability внутреннего объекта.

private readonly List<Order> _orders = new();

public IReadOnlyCollection<Order> Orders => _orders;

16Как работает yield return?

yield return создает iterator с deferred execution. Компилятор преобразует метод в state machine, а значения производятся по одному при MoveNext. Это позволяет не создавать промежуточные коллекции и работать с большими sequences. Ловушки — lifetime и multiple enumeration: нельзя возвращать lazy sequence, который зависит от уже disposed ресурса, например закрытого DbContext или stream; Count, а затем foreach могут выполнить iterator дважды, повторив side effects или I/O.

IEnumerable<int> GetEvenNumbers(IEnumerable<int> source)
{
    foreach (var number in source)
    {
        if (number % 2 == 0)
            yield return number;
    }
}

Delegates, events и exceptions

17Что такое delegates и lambda expressions?

Delegate — type-safe ссылка на method. Lambda expressions позволяют коротко описывать inline functions и часто присваиваются Func, Action или Predicate. Lambda захватывает переменную, а не снимок ее значения, поэтому может увидеть более позднее значение из цикла или последующего кода. Если lambda ничего не захватывает, compiler может закешировать static delegate и избежать closure allocation. В performance-sensitive code allocations на closures и увеличенное lifetime объектов могут иметь значение.

public delegate int Operation(int x, int y);

Operation operation = (x, y) => x + y;
Console.WriteLine(operation(2, 3));

int factor = 10;
Func<int, int> multiply = x => x * factor;

18Чем event отличается от delegate?

Event обычно построен поверх delegate, но ограничивает внешний доступ. Внешний код может subscribe/unsubscribe, но вызвать event может только owner class. Public delegate field позволил бы вызывающему коду заменить invocation list или вызвать delegate напрямую. Events также создают ссылку от publisher к subscriber, поэтому long-lived publisher может вызвать memory leak, если subscriber не отписался.

public event EventHandler<OrderEventArgs>? OrderCreated;

service.OrderCreated += Handler;
service.OrderCreated -= Handler;

OrderCreated?.Invoke(this, args);

19Как правильно обрабатывать exceptions в C#?

Exceptions используют для исключительных ситуаций, а не для обычного control flow. Ловите только те exceptions, которые можете обработать, избегайте empty catch, добавляйте context при wrapping и не раскрывайте internal details пользователю. Для повторного выброса используйте throw;, чтобы сохранить original stack trace. Если нужно обернуть старую ошибку в новую, передавайте исходное исключение как InnerException: throw new DomainException("...", ex). Для ожидаемых неудач лучше TryParse-style API.

try
{
    await service.ProcessAsync();
}
catch (ValidationException exception)
{
    // Expected domain error
}
finally
{
    // Cleanup
}

catch
{
    throw; // preserves stack trace
}

20Что такое exception filters?

Exception filters позволяют выполнить catch только при дополнительном условии. Filter проверяется до stack unwinding и до входа в catch body, поэтому если when возвращает false, исходный стек остается нетронутым для диагностики и crash dumps. Filters полезны для HTTP status codes, database error codes, разделения transient/permanent errors и conditional logging. Условие filter должно быть простым и без сложных side effects.

try
{
    await SendAsync();
}
catch (HttpRequestException ex)
    when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Handle only 404
}