Значимая переменная обычно содержит само значение, поэтому присваивание копирует значение. Переменная ссылочного типа содержит ссылку на объект, поэтому присваивание копирует ссылку, и обе переменные могут видеть изменение одного объекта. Важный нюанс: нельзя упрощать до '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<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 через все слои приложения.
Многие 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 внутреннего объекта.
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 не отписался.
Exceptions используют для исключительных ситуаций, а не для обычного control flow. Ловите только те exceptions, которые можете обработать, избегайте empty catch, добавляйте context при wrapping и не раскрывайте internal details пользователю. Для повторного выброса используйте throw;, чтобы сохранить original stack trace. Если нужно обернуть старую ошибку в новую, передавайте исходное исключение как InnerException: throw new DomainException("...", ex). Для ожидаемых неудач лучше TryParse-style API.
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
}
Другие вопросы для backend интервью
Переходите между технологиями или вернитесь в общий hub подготовки.