Перейти к основному содержимому

200 вопросов по C#

200 вопросов по C#

Основы C#: типы данных, переменные, операторы

Вопрос

Что такое C#?

Ответ

C# — это объектно-ориентированный, типобезопасный язык программирования, разработанный компанией Microsoft как часть платформы .NET. Он поддерживает множественные парадигмы: императивное, объектно-ориентированное, функциональное и компонентное программирование.


Вопрос

Какие существуют категории типов данных в C#?

Ответ

В C# все типы делятся на две категории:

  • Значимые типы (value types) — хранят данные непосредственно в стеке или внутри содержащего объекта. К ним относятся структуры (struct), перечисления (enum) и встроенные числовые типы (int, bool, char и т.д.).
  • Ссылочные типы (reference types) — хранят ссылку на объект в управляемой куче. К ним относятся классы (class), интерфейсы (interface), делегаты (delegate), массивы и строки (string).

Вопрос

Чем отличается int от Int32?

Ответ

int — это псевдоним ключевого слова языка C# для типа System.Int32. Они полностью эквивалентны и компилируются в один и тот же IL-код. Использование int предпочтительнее по стилю кода в C#.


Вопрос

Что такое var и когда его можно использовать?

Ответ

var — это ключевое слово для неявного определения типа локальной переменной. Компилятор выводит тип на основе выражения инициализации. Переменная, объявленная с var, должна быть инициализирована при объявлении. Пример:

var number = 42; // тип выведен как int

Вопрос

Можно ли изменить значение переменной, объявленной через const?

Ответ

Нет. Переменная, объявленная с модификатором const, является compile-time константой. Её значение должно быть известно на этапе компиляции и не может быть изменено в течение выполнения программы.


Вопрос

Чем отличается const от readonly?

Ответ

  • const задаёт compile-time константу. Значение должно быть литералом и неизменяемо во всех контекстах.
  • readonly позволяет инициализировать поле либо при объявлении, либо в конструкторе. Значение становится неизменяемым после завершения конструирования объекта. Это runtime-константа.

Пример:

public class Example
{
public const int CompileTime = 100;
public readonly int RunTime;

public Example(int value)
{
RunTime = value; // допустимо только в конструкторе
}
}

Вопрос

Что такое nullable-типы и как их объявляют?

Ответ

Nullable-типы позволяют значимым типам принимать значение null. Объявляются с помощью суффикса ? или явно как Nullable<T>.
Пример:

int? nullableInt = null;
Nullable<bool> flag = true;

Вопрос

Как проверить, имеет ли nullable-переменная значение?

Ответ

Используется свойство HasValue или сравнение с null.
Пример:

int? x = null;
if (x.HasValue)
{
Console.WriteLine(x.Value);
}
// или
if (x != null)
{
Console.WriteLine(x);
}

Вопрос

Что делает оператор ???

Ответ

Оператор ?? возвращает левый операнд, если он не равен null; в противном случае — правый операнд. Называется оператором объединения с null.
Пример:

int? a = null;
int b = a ?? 0; // b = 0

Вопрос

Что такое ??=?

Ответ

Оператор ??= присваивает значение правого операнда левой переменной, только если она равна null.
Пример:

string name = null;
name ??= "Default";
// name теперь "Default"

Вопрос

Какие арифметические операторы есть в C#?

Ответ

C# поддерживает стандартные арифметические операторы:

  • + (сложение)
  • - (вычитание)
  • * (умножение)
  • / (деление)
  • % (остаток от деления)

Для целочисленного деления результат усекается к нулю.


Вопрос

Что такое переполнение и как с ним работать в C#?

Ответ

Переполнение возникает, когда результат арифметической операции выходит за пределы диапазона типа. По умолчанию C# не проверяет переполнение для целочисленных операций. Для включения проверки используются ключевые слова checked и unchecked.
Пример:

int max = int.MaxValue;
int result = checked(max + 1); // вызовет OverflowException

Вопрос

Что такое неявное и явное преобразование типов?

Ответ

  • Неявное преобразование происходит автоматически, когда нет риска потери данных (например, intlong).
  • Явное преобразование (приведение типов) требуется, когда возможна потеря данных (например, doubleint). Выполняется с помощью оператора (Type)value.

Пример:

int a = 10;
long b = a; // неявное
double c = 10.7;
int d = (int)c; // явное, d = 10

Вопрос

Что делает оператор is?

Ответ

Оператор is проверяет, совместим ли объект с заданным типом. Возвращает true, если объект не null и может быть приведён к указанному типу.
Пример:

object obj = "hello";
if (obj is string)
{
Console.WriteLine("Это строка");
}

Вопрос

Что такое pattern matching с использованием is?

Ответ

Начиная с C# 7.0, оператор is поддерживает сопоставление с образцом (pattern matching). Позволяет одновременно проверить тип и извлечь значение.
Пример:

object obj = 42;
if (obj is int i && i > 0)
{
Console.WriteLine($"Положительное число: {i}");
}

Вопрос

Что делает оператор as?

Ответ

Оператор as выполняет безопасное приведение ссылочного типа. Если приведение невозможно, возвращается null, а не выбрасывается исключение.
Пример:

object obj = "text";
string s = obj as string; // s = "text"
object num = 123;
string t = num as string; // t = null

Вопрос

Можно ли использовать as с значимыми типами?

Ответ

Нет, оператор as работает только со ссылочными типами и nullable-типами. Для значимых типов без ? использование as вызовет ошибку компиляции.


Вопрос

Что такое литералы и какие бывают виды?

Ответ

Литералы — это значения, записанные непосредственно в коде. В C# поддерживаются:

  • Целочисленные (42, 0xFF)
  • Вещественные (3.14, 1.2e3)
  • Строковые ("hello")
  • Символьные ('A')
  • Логические (true, false)
  • Null (null)
  • Вербальные строки (@"C:\path")
  • Интерполированные строки ($"Value: {x}")

Вопрос

Что такое интерполированная строка?

Ответ

Интерполированная строка начинается с $ и позволяет внедрять выражения в фигурных скобках. Выражения вычисляются во время выполнения.
Пример:

string name = "Alice";
int age = 30;
string message = $"Имя: {name}, возраст: {age}";

Вопрос

Что такое verbatim-строка и как её объявить?

Ответ

Verbatim-строка (дословная строка) объявляется с префиксом @ и интерпретирует все символы буквально, включая обратные слэши. Переводы строк сохраняются.
Пример:

string path = @"C:\Users\Name\Documents";

Управляющие конструкции: условия, циклы, исключения

Вопрос

Какие условные операторы есть в C#?

Ответ

В C# поддерживаются следующие условные операторы:

  • if / else if / else — для выполнения кода при истинности условия;
  • тернарный оператор ?: — сокращённая форма if-else для выражений;
  • switch — для выбора одного из множества вариантов на основе значения выражения.

Пример:

int x = 10;
string result = x > 5 ? "Больше" : "Меньше или равно";

Вопрос

Что делает тернарный оператор ?:?

Ответ

Тернарный оператор ?: вычисляет условие и возвращает одно из двух значений в зависимости от его истинности. Синтаксис:
условие ? значение_если_истина : значение_если_ложь.

Пример:

bool isAdult = age >= 18;
string status = isAdult ? "Совершеннолетний" : "Несовершеннолетний";

Вопрос

Можно ли использовать несколько условий в одном if?

Ответ

Да. Несколько условий объединяются логическими операторами:

  • && — логическое И (оба условия должны быть истинны);
  • || — логическое ИЛИ (хотя бы одно условие истинно);
  • ! — логическое НЕ (инвертирует результат).

Пример:

if (age >= 18 && hasLicense)
{
Console.WriteLine("Можно водить");
}

Вопрос

Что такое короткозамкнутое вычисление (short-circuit evaluation)?

Ответ

Короткозамкнутое вычисление означает, что при использовании && или || второе условие не вычисляется, если результат уже определён первым.

  • Для &&: если первое условие false, второе не проверяется.
  • Для ||: если первое условие true, второе не проверяется.

Это повышает производительность и предотвращает ошибки (например, обращение к null).


Вопрос

Как работает оператор switch в C#?

Ответ

Оператор switch сравнивает значение выражения с набором меток case. При совпадении выполняется соответствующий блок кода. По умолчанию после выполнения одного case управление не переходит к следующему (нет «проваливания»), если явно не указано goto case.

Начиная с C# 7.0, switch поддерживает сопоставление с образцом (pattern matching).
Пример:

int day = 3;
switch (day)
{
case 1: Console.WriteLine("Понедельник"); break;
case 2: Console.WriteLine("Вторник"); break;
case 3: Console.WriteLine("Среда"); break;
default: Console.WriteLine("Неизвестный день"); break;
}

Вопрос

Можно ли использовать строки в switch?

Ответ

Да. Начиная с C# 7.0, switch поддерживает строки, а также другие ссылочные типы. Сравнение строк выполняется с учётом регистра (используется ==).

Пример:

string role = "admin";
switch (role)
{
case "admin": Console.WriteLine("Администратор"); break;
case "user": Console.WriteLine("Пользователь"); break;
}

Вопрос

Что такое switch с выражением (switch expression)?

Ответ

Начиная с C# 8.0, доступен синтаксис switch как выражение, возвращающее значение. Он использует стрелочную нотацию => и не требует break.

Пример:

var message = day switch
{
1 => "Понедельник",
2 => "Вторник",
3 => "Среда",
_ => "Неизвестный день"
};

Вопрос

Какие циклы есть в C#?

Ответ

В C# поддерживаются следующие циклы:

  • for — цикл с известным числом итераций;
  • while — цикл с предусловием;
  • do-while — цикл с постусловием;
  • foreach — цикл по элементам коллекции, реализующей IEnumerable.

Вопрос

В чём разница между while и do-while?

Ответ

  • while проверяет условие до выполнения тела цикла. Если условие изначально ложно, тело не выполнится ни разу.
  • do-while выполняет тело цикла хотя бы один раз, а затем проверяет условие.

Пример:

int i = 0;
do
{
Console.WriteLine(i++);
} while (i < 3); // выведет 0, 1, 2

Вопрос

Что делают ключевые слова break и continue?

Ответ

  • break немедленно завершает выполнение ближайшего цикла или switch.
  • continue прерывает текущую итерацию цикла и переходит к следующей.

Пример:

for (int i = 0; i < 10; i++)
{
if (i == 5) continue; // пропустить 5
if (i == 8) break; // остановиться на 8
Console.WriteLine(i);
}
// Выведет: 0, 1, 2, 3, 4, 6, 7

Вопрос

Можно ли использовать метки и goto в C#?

Ответ

Да, C# поддерживает оператор goto, но его использование не рекомендуется, так как он нарушает структурированность кода и усложняет чтение. Допустимые случаи — выход из вложенных циклов или переходы внутри switch.

Пример:

switch (value)
{
case 1:
goto case 2;
case 2:
Console.WriteLine("Обработка 1 или 2");
break;
}

Вопрос

Что такое исключения в C#?

Ответ

Исключения — это объекты, представляющие ошибочные или нестандартные ситуации во время выполнения программы. Они позволяют отделить обработку ошибок от основной логики.

Все исключения наследуются от класса System.Exception.


Вопрос

Как обрабатываются исключения в C#?

Ответ

Исключения обрабатываются с помощью конструкции try-catch-finally:

  • try — блок кода, в котором может возникнуть исключение;
  • catch — блок обработки исключения (может быть несколько для разных типов);
  • finally — блок, который выполняется всегда, независимо от того, было ли исключение.

Пример:

try
{
int result = 10 / 0;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Деление на ноль: " + ex.Message);
}
finally
{
Console.WriteLine("Завершение");
}

Вопрос

Можно ли перехватывать все исключения одним catch?

Ответ

Да. Блок catch без указания типа перехватывает любые исключения:

catch { /* обработка */ }

Однако такой подход не рекомендуется, так как скрывает детали ошибки и затрудняет отладку. Лучше перехватывать конкретные типы исключений.


Вопрос

Как выбрасывать исключение вручную?

Ответ

Для выбрасывания исключения используется ключевое слово throw.

Пример:

if (name == null)
{
throw new ArgumentNullException(nameof(name));
}

Вопрос

Что такое throw без аргумента?

Ответ

throw; внутри блока catch повторно выбрасывает текущее исключение, сохраняя стек вызовов. Это полезно для логирования ошибки с последующей передачей её выше по стеку.

Пример:

catch (Exception ex)
{
Log(ex);
throw; // сохраняет оригинальный стек
}

Вопрос

Какие встроенные типы исключений есть в .NET?

Ответ

Некоторые распространённые исключения:

  • ArgumentException, ArgumentNullException, ArgumentOutOfRangeException — ошибки аргументов;
  • InvalidOperationException — недопустимая операция в текущем состоянии;
  • NullReferenceException — попытка использовать null;
  • IndexOutOfRangeException — выход за границы массива;
  • FileNotFoundException — файл не найден.

Рекомендуется использовать наиболее специфичное исключение для ситуации.


Вопрос

Что такое пользовательские исключения?

Ответ

Пользовательские исключения — это классы, наследуемые от Exception (или его подклассов), предназначенные для представления специфичных ошибок домена.

Пример:

public class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public InsufficientFundsException(decimal balance)
: base($"Недостаточно средств. Баланс: {balance}")
{
Balance = balance;
}
}

Вопрос

Когда следует использовать исключения?

Ответ

Исключения следует использовать только для исключительных ситуаций, а не для управления потоком выполнения. Например, деление на ноль — исключение, а проверка наличия элемента в коллекции — нет.


Методы, параметры и сигнатуры функций

Вопрос

Что такое метод в C#?

Ответ

Метод — это блок кода, содержащий последовательность инструкций, который выполняет определённую задачу. Метод объявляется внутри класса или структуры и имеет имя, возвращаемый тип, список параметров и тело.

Пример:

public int Add(int a, int b)
{
return a + b;
}

Вопрос

Из каких частей состоит сигнатура метода?

Ответ

Сигнатура метода включает:

  • имя метода;
  • количество, порядок и типы параметров.

Возвращаемый тип, модификаторы доступа (public, private) и имена параметров не входят в сигнатуру.


Вопрос

Что такое перегрузка методов?

Ответ

Перегрузка методов — это возможность объявить несколько методов с одинаковым именем, но разными сигнатурами (разное количество или типы параметров). Компилятор выбирает подходящий метод на основе переданных аргументов.

Пример:

void Print(string message) { ... }
void Print(string message, int times) { ... }

Вопрос

Можно ли перегружать методы только по возвращаемому типу?

Ответ

Нет. Перегрузка по возвращаемому типу невозможна, так как он не входит в сигнатуру метода. Попытка сделать это приведёт к ошибке компиляции.


Вопрос

Что делает ключевое слово return?

Ответ

Ключевое слово return завершает выполнение метода и возвращает значение вызывающему коду. В методах с возвращаемым типом void return может использоваться без значения для досрочного выхода.

Пример:

public string GetMessage(bool isValid)
{
if (!isValid) return "Неверно";
return "Верно";
}

Вопрос

Что такое параметры по умолчанию?

Ответ

Параметры по умолчанию позволяют задавать значение, которое будет использовано, если аргумент не передан при вызове. Объявляются с помощью синтаксиса = значение.

Пример:

void Greet(string name, string greeting = "Привет")
{
Console.WriteLine($"{greeting}, {name}!");
}
// Greet("Алиса") → "Привет, Алиса!"

Вопрос

Где должны располагаться параметры по умолчанию в списке параметров?

Ответ

Параметры по умолчанию должны идти после всех обязательных параметров. Нельзя объявить обязательный параметр после параметра со значением по умолчанию.


Вопрос

Что такое именованные аргументы?

Ответ

Именованные аргументы позволяют передавать аргументы методу по имени параметра, а не по позиции. Это повышает читаемость и гибкость вызова.

Пример:

Greet(greeting: "Здравствуйте", name: "Иван");

Именованные аргументы можно комбинировать с позиционными, но все позиционные должны идти первыми.


Вопрос

Что делает модификатор ref?

Ответ

Модификатор ref передаёт аргумент по ссылке, а не по значению. Это означает, что изменения внутри метода влияют на исходную переменную. Переменная должна быть инициализирована до передачи.

Пример:

void Increment(ref int value)
{
value++;
}
int x = 5;
Increment(ref x); // x теперь 6

Вопрос

Что делает модификатор out?

Ответ

Модификатор out также передаёт аргумент по ссылке, но не требует инициализации до вызова. Метод обязан присвоить значение параметру out до возврата.

Пример:

bool TryParseInt(string input, out int result)
{
return int.TryParse(input, out result);
}
// Вызов:
if (TryParseInt("123", out int number))
{
Console.WriteLine(number); // 123
}

Вопрос

В чём разница между ref и out?

Ответ

  • ref: переменная должна быть инициализирована до вызова; метод может читать и изменять её.
  • out: переменная не обязана быть инициализирована; метод обязан присвоить ей значение, но не может читать до этого.

Оба передают ссылку на переменную, а не копию значения.


Вопрос

Что такое params?

Ответ

Ключевое слово params позволяет методу принимать переменное число аргументов одного типа. Параметр с params должен быть последним в списке и иметь тип массива.

Пример:

void PrintAll(params string[] messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}
// Вызов:
PrintAll("A", "B", "C");

Вопрос

Можно ли передавать массив в метод с params?

Ответ

Да. Массив можно передать напрямую, и он будет обработан как единый аргумент params.

Пример:

string[] arr = { "X", "Y" };
PrintAll(arr); // корректно

Вопрос

Что такое локальные функции?

Ответ

Локальные функции — это методы, объявленные внутри другого метода. Они видны только в теле содержащего метода и могут обращаться к его локальным переменным.

Пример:

int Calculate()
{
int Factorial(int n) => n <= 1 ? 1 : n * Factorial(n - 1);
return Factorial(5);
}

Вопрос

Что такое выражения-члены (expression-bodied members)?

Ответ

Выражения-члены — это сокращённый синтаксис для методов, свойств и других членов, тело которых состоит из одного выражения. Используется оператор =>.

Пример:

public int Square(int x) => x * x;
public string Name => _name ?? "Неизвестно";

Вопрос

Можно ли использовать async с методами, возвращающими void?

Ответ

Технически можно, но не рекомендуется, кроме случаев обработчиков событий. Такие методы называются «fire-and-forget» и не позволяют отлавливать исключения или ожидать завершения. Лучше возвращать Task.


Вопрос

Что возвращает асинхронный метод без return?

Ответ

Асинхронный метод, объявленный как async Task, возвращает объект Task, представляющий фоновую операцию. Если метод ничего не возвращает, используется Task; если возвращает значение — Task<T>.


Вопрос

Как работает await?

Ответ

Оператор await приостанавливает выполнение метода до завершения асинхронной операции, представленной объектом Task или Task<T>. После завершения метод возобновляется, и результат (если есть) становится доступен.

Пример:

async Task<string> FetchDataAsync()
{
HttpClient client = new();
string data = await client.GetStringAsync("https://example.com");
return data;
}

Вопрос

Что такое метод расширения?

Ответ

Метод расширения — это статический метод в статическом классе, который позволяет «добавлять» методы к существующим типам без изменения их исходного кода. Первый параметр помечается ключевым словом this.

Пример:

public static class StringExtensions
{
public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s);
}
// Использование:
string text = null;
bool empty = text.IsNullOrEmpty(); // вызов как экземплярный метод

Вопрос

Могут ли методы расширения обращаться к приватным членам типа?

Ответ

Нет. Методы расширения работают только с публичным интерфейсом типа, так как они не являются частью самого типа и не имеют доступа к его внутреннему состоянию.


Классы, объекты и инкапсуляция

Вопрос

Что такое класс в C#?

Ответ

Класс — это ссылочный тип, описывающий структуру и поведение объектов. Он определяет поля, свойства, методы, события и другие члены. Класс служит шаблоном для создания экземпляров (объектов).

Пример:

public class Person
{
public string Name { get; set; }
public void Introduce() => Console.WriteLine($"Привет, я {Name}");
}

Вопрос

Что такое объект?

Ответ

Объект — это экземпляр класса, созданный в управляемой куче с помощью оператора new. Каждый объект содержит своё состояние (значения полей) и может вызывать методы своего типа.

Пример:

Person p = new Person();
p.Name = "Алиса";
p.Introduce(); // Привет, я Алиса

Вопрос

Что такое инкапсуляция?

Ответ

Инкапсуляция — это принцип ООП, заключающийся в сокрытии внутренней реализации объекта и предоставлении только необходимого интерфейса для взаимодействия. В C# достигается с помощью модификаторов доступа (private, protected, internal, public).


Вопрос

Какие модификаторы доступа есть в C#?

Ответ

В C# существуют следующие модификаторы доступа:

  • public — доступен из любого кода;
  • private — доступен только внутри содержащего типа;
  • protected — доступен внутри содержащего типа и его производных классов;
  • internal — доступен внутри той же сборки;
  • protected internal — объединяет protected и internal;
  • private protected — доступен только в производных классах той же сборки.

Вопрос

Что такое поле?

Ответ

Поле — это переменная, объявленная на уровне класса или структуры. Оно хранит состояние объекта. Поля обычно объявляются как private, а доступ к ним предоставляется через свойства.

Пример:

public class Counter
{
private int _count; // поле
public int Count => _count; // свойство для чтения
}

Вопрос

Что такое свойство?

Ответ

Свойство — это член класса, предоставляющий гибкий механизм для чтения, записи или вычисления значения частного поля. Состоит из аксессоров get и/или set.

Пример:

public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException();
}

Вопрос

Что такое автоматическое свойство?

Ответ

Автоматическое свойство — это сокращённая форма свойства, при которой компилятор автоматически создаёт скрытое поле. Используется, когда логика get/set тривиальна.

Пример:

public string Email { get; set; }

Можно задать разные модификаторы доступа для get и set:

public string Id { get; private set; }

Вопрос

Что такое конструктор?

Ответ

Конструктор — это специальный метод, вызываемый при создании экземпляра класса. Он используется для инициализации объекта. Имя конструктора совпадает с именем класса, и он не имеет возвращаемого типа.

Пример:

public class Book
{
public string Title { get; }
public Book(string title)
{
Title = title ?? throw new ArgumentNullException(nameof(title));
}
}

Вопрос

Можно ли иметь несколько конструкторов в классе?

Ответ

Да. Класс может содержать несколько конструкторов с разными сигнатурами — это называется перегрузка конструкторов. Один конструктор может вызывать другой с помощью this(...).

Пример:

public Book() : this("Без названия") { }
public Book(string title) => Title = title;

Вопрос

Что такое статический конструктор?

Ответ

Статический конструктор вызывается один раз при первом обращении к типу. Он используется для инициализации статических членов. Не принимает параметров и не может иметь модификатор доступа.

Пример:

static MyClass()
{
StaticField = InitializeOnce();
}

Вопрос

Что такое деструктор?

Ответ

Деструктор (или финализатор) — это метод, вызываемый сборщиком мусора перед уничтожением объекта. Объявляется с тильдой: ~ClassName(). Используется редко, преимущественно для освобождения неуправляемых ресурсов.

Пример:

~MyClass()
{
// освобождение неуправляемых ресурсов
}

Современный подход — реализация IDisposable.


Вопрос

Что такое this?

Ответ

Ключевое слово this ссылается на текущий экземпляр класса. Используется для:

  • разрешения конфликтов имён между параметрами и полями;
  • передачи текущего объекта в другие методы;
  • вызова других конструкторов через this(...).

Пример:

public Person(string name)
{
this.name = name; // this.name — поле, name — параметр
}

Вопрос

Что такое partial-класс?

Ответ

Ключевое слово partial позволяет разделить определение класса на несколько файлов. Все части объединяются во время компиляции в один тип. Часто используется в генерируемом коде (например, Windows Forms, Entity Framework).

Пример:

// File1.cs
partial class DataService { void MethodA() { } }

// File2.cs
partial class DataService { void MethodB() { } }

Вопрос

Можно ли сделать partial метод?

Ответ

Да. partial-метод объявляется в одной части partial-класса и может быть реализован в другой. Если реализация отсутствует, компилятор удаляет все вызовы этого метода. Используется в сценариях генерации кода.

Ограничения:

  • должен возвращать void;
  • не может иметь out-параметров;
  • доступен только внутри partial-класса.

Вопрос

Что такое запись (record)?

Ответ

record — это ссылочный тип, введённый в C# 9.0, предназначенный для представления неизменяемых значений. Автоматически реализует семантику равенства на основе значений полей, а не ссылок.

Пример:

public record Person(string FirstName, string LastName);

Эквивалентно:

public class Person : IEquatable<Person>
{
public string FirstName { get; init; }
public string LastName { get; init; }
// + переопределённые Equals, GetHashCode, ToString
}

Вопрос

Чем отличается record от class?

Ответ

  • record по умолчанию неизменяем (init вместо set);
  • реализует значение-ориентированное равенство (Equals сравнивает поля);
  • поддерживает синтаксис с первичным конструктором;
  • предоставляет метод With() для создания копии с изменёнными полями.

Вопрос

Можно ли наследовать record?

Ответ

Да. record может наследоваться от другого record, но не от class, и наоборот. Наследование record сохраняет семантику равенства на основе значений.


Вопрос

Что такое init?

Ответ

Модификатор init разрешает присваивание свойству только во время инициализации объекта (в конструкторе или при создании с инициализатором). После этого свойство становится доступным только для чтения.

Пример:

var p = new Person { Name = "Алиса" }; // допустимо
p.Name = "Боб"; // ошибка компиляции

Наследование, полиморфизм и абстракция

Вопрос

Что такое наследование в C#?

Ответ

Наследование — это механизм, позволяющий одному классу (производному) унаследовать члены другого класса (базового). Это обеспечивает повторное использование кода и установление иерархии типов. В C# поддерживается только одиночное наследование классов.

Пример:

public class Animal
{
public virtual void Speak() => Console.WriteLine("Животное издаёт звук");
}

public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Гав!");
}

Вопрос

Можно ли наследовать от нескольких классов?

Ответ

Нет. C# не поддерживает множественное наследование классов. Класс может наследовать только от одного базового класса. Однако он может реализовывать множество интерфейсов.


Вопрос

Что делает ключевое слово base?

Ответ

Ключевое слово base используется для обращения к членам базового класса из производного. Чаще всего применяется для вызова конструктора базового класса или переопределённых методов.

Пример:

public class Dog : Animal
{
public Dog(string name) : base() { }

public override void Speak()
{
base.Speak(); // вызов метода базового класса
Console.WriteLine("Гав!");
}
}

Вопрос

Что такое виртуальный метод?

Ответ

Виртуальный метод — это метод, помеченный модификатором virtual, который может быть переопределён в производном классе с помощью override. Это основа для реализации полиморфизма времени выполнения.

Пример:

public virtual void Move() => Console.WriteLine("Движется");

Вопрос

Что делает модификатор override?

Ответ

Модификатор override указывает, что метод в производном классе заменяет виртуальный или абстрактный метод базового класса. Сигнатура должна точно совпадать.

Пример:

public override void Move() => Console.WriteLine("Бежит быстро");

Вопрос

Что делает модификатор sealed при применении к методу?

Ответ

Модификатор sealed запрещает дальнейшее переопределение виртуального метода в производных классах. Используется вместе с override.

Пример:

public sealed override void Move() { ... }

Вопрос

Что делает модификатор sealed при применении к классу?

Ответ

Класс, помеченный как sealed, не может быть унаследован. Это повышает безопасность и производительность, так как компилятор может выполнять оптимизации.

Пример:

public sealed class UtilityClass { ... }
// class MyUtility : UtilityClass — ошибка компиляции

Вопрос

Что такое полиморфизм?

Ответ

Полиморфизм — это способность объектов разных типов обрабатываться через единый интерфейс. В C# реализуется через виртуальные методы, абстрактные классы и интерфейсы. Позволяет писать гибкий и расширяемый код.

Пример:

Animal a = new Dog();
a.Speak(); // вызовется Dog.Speak(), несмотря на тип переменной Animal

Вопрос

Что такое абстрактный класс?

Ответ

Абстрактный класс — это класс, помеченный как abstract, который не может быть создан напрямую. Он может содержать как обычные, так и абстрактные методы. Предназначен для использования в качестве базового класса.

Пример:

public abstract class Shape
{
public abstract double Area { get; }
}

Вопрос

Что такое абстрактный метод?

Ответ

Абстрактный метод — это метод без реализации, объявленный в абстрактном классе с модификатором abstract. Все неабстрактные производные классы обязаны предоставить реализацию с помощью override.

Пример:

public abstract void Draw();

Вопрос

Может ли абстрактный класс содержать неабстрактные методы?

Ответ

Да. Абстрактный класс может содержать как абстрактные, так и полностью реализованные методы, свойства, поля и конструкторы.


Вопрос

Что такое интерфейс?

Ответ

Интерфейс — это контракт, определяющий набор методов, свойств, событий или индексаторов, которые должны быть реализованы классом или структурой. Не содержит реализации (до C# 8.0).

Пример:

public interface ILogger
{
void Log(string message);
}

Вопрос

Может ли класс реализовывать несколько интерфейсов?

Ответ

Да. Класс может реализовывать любое количество интерфейсов, разделяя их запятыми в объявлении.

Пример:

public class FileLogger : ILogger, IDisposable
{
public void Log(string message) { ... }
public void Dispose() { ... }
}

Вопрос

Что изменилось в интерфейсах начиная с C# 8.0?

Ответ

Начиная с C# 8.0, интерфейсы могут содержать:

  • реализации методов по умолчанию;
  • статические члены;
  • приватные методы;
  • виртуальные методы.

Это позволяет эволюционировать интерфейсы без нарушения существующего кода.

Пример:

public interface ILogger
{
void Log(string message);
void LogError(string error) => Log($"[ERROR] {error}");
}

Вопрос

Можно ли использовать модификаторы доступа в интерфейсе?

Ответ

Да, но с ограничениями. По умолчанию все члены интерфейса public. Начиная с C# 8.0, можно использовать private, protected, internal, virtual, sealed, static, но не abstract (все члены без реализации считаются абстрактными автоматически).


Вопрос

Что такое явная реализация интерфейса?

Ответ

Явная реализация интерфейса — это способ реализовать член интерфейса так, чтобы он был доступен только через ссылку на интерфейс, а не через экземпляр класса. Используется для разрешения конфликтов имён при реализации нескольких интерфейсов.

Пример:

public class MultiLogger : ILogger, IEventLogger
{
void ILogger.Log(string message) => Console.WriteLine($"Log: {message}");
void IEventLogger.Log(string event) => Console.WriteLine($"Event: {event}");
}

Вызов:

var logger = new MultiLogger();
((ILogger)logger).Log("test"); // требуется приведение

Вопрос

В чём разница между абстрактным классом и интерфейсом?

Ответ

  • Абстрактный класс может содержать состояние (поля), конструкторы и частичную реализацию; интерфейс — только контракт (до C# 8.0).
  • Класс может наследовать только один абстрактный класс, но реализовывать множество интерфейсов.
  • Абстрактный класс используется для «является» (is-a) отношений; интерфейс — для «может делать» (can-do).

Вопрос

Что такое композиция и чем она отличается от наследования?

Ответ

Композиция — это отношение «имеет» (has-a), при котором один объект содержит другой как поле. В отличие от наследования, композиция обеспечивает большую гибкость, лучшую тестируемость и избегает жёсткой иерархии. Рекомендуется предпочитать композицию наследованию, если нет чёткой семантики «является».

Пример:

public class Car
{
private Engine _engine; // композиция
}

Коллекции, перечисления и LINQ

Вопрос

Что такое массив в C#?

Ответ

Массив — это фиксированная по размеру структура данных, хранящая элементы одного типа в непрерывном блоке памяти. Индексация начинается с 0. Массивы являются ссылочными типами и наследуются от System.Array.

Пример:

int[] numbers = new int[5];
numbers[0] = 10;

Вопрос

Можно ли изменить размер массива после создания?

Ответ

Нет. Размер массива фиксирован при создании. Для динамического изменения размера используются коллекции, такие как List<T>.


Вопрос

Что такое List<T>?

Ответ

List<T> — это динамический массив из пространства имён System.Collections.Generic. Он автоматически увеличивает свою ёмкость при добавлении элементов и предоставляет методы для вставки, удаления и поиска.

Пример:

var names = new List<string>();
names.Add("Алиса");
names.RemoveAt(0);

Вопрос

В чём разница между List<T> и массивом?

Ответ

  • Массив имеет фиксированный размер; List<T> — динамический.
  • Массив работает быстрее при известном размере и прямом доступе; List<T> удобнее для частых изменений.
  • List<T> предоставляет богатый API (Add, Remove, Find и т.д.), массив — только индексатор и свойства Length, Rank.

Вопрос

Что такое Dictionary<TKey, TValue>?

Ответ

Dictionary<TKey, TValue> — это коллекция пар «ключ-значение», обеспечивающая быстрый поиск по ключу (в среднем O(1)). Ключи должны быть уникальными и не равны null.

Пример:

var ages = new Dictionary<string, int>();
ages["Алиса"] = 30;
if (ages.TryGetValue("Алиса", out int age))
Console.WriteLine(age);

Вопрос

Что делает метод TryGetValue в словаре?

Ответ

Метод TryGetValue безопасно пытается получить значение по ключу. Если ключ существует, возвращает true и записывает значение в out-параметр; иначе — false без исключения.

Это предпочтительнее прямого обращения через индексатор, который выбрасывает KeyNotFoundException.


Вопрос

Какие основные интерфейсы коллекций есть в .NET?

Ответ

Основные интерфейсы:

  • IEnumerable<T> — поддержка перебора (foreach);
  • ICollection<T> — добавление, удаление, подсчёт элементов;
  • IList<T> — доступ по индексу, вставка, удаление по позиции;
  • IDictionary<TKey, TValue> — работа с парами «ключ-значение».

Вопрос

Что такое IEnumerable<T>?

Ответ

IEnumerable<T> — это интерфейс, представляющий последовательность элементов, по которой можно пройти с помощью foreach. Он содержит единственный метод GetEnumerator(), возвращающий объект, реализующий IEnumerator<T>.


Вопрос

Что такое ленивая оценка (lazy evaluation) в контексте IEnumerable<T>?

Ответ

Ленивая оценка означает, что данные не вычисляются до тех пор, пока не будет выполнен запрос к последовательности (например, через foreach или .ToList()). Это позволяет строить эффективные цепочки операций без промежуточного хранения данных.

Пример:

var query = numbers.Where(x => x > 10); // ничего не вычислено
foreach (var n in query) { ... } // вычисление происходит здесь

Вопрос

Что такое yield return?

Ответ

yield return используется в методе, возвращающем IEnumerable<T> или IEnumerator<T>, чтобы пошагово генерировать элементы последовательности. Компилятор автоматически создаёт состояние машины для итератора.

Пример:

public IEnumerable<int> GetEvenNumbers(int max)
{
for (int i = 0; i <= max; i += 2)
yield return i;
}

Вопрос

Что такое HashSet<T>?

Ответ

HashSet<T> — это коллекция уникальных элементов, оптимизированная для операций проверки наличия (Contains), добавления и удаления. Основана на хэш-таблице, не сохраняет порядок элементов.

Пример:

var tags = new HashSet<string>();
tags.Add("C#");
bool hasTag = tags.Contains("C#"); // true

Вопрос

Что такое Queue<T> и Stack<T>?

Ответ

  • Queue<T> реализует очередь по принципу FIFO (первым пришёл — первым ушёл). Основные методы: Enqueue, Dequeue, Peek.
  • Stack<T> реализует стек по принципу LIFO (последним пришёл — первым ушёл). Основные методы: Push, Pop, Peek.

Вопрос

Что такое LINQ?

Ответ

LINQ (Language Integrated Query) — это набор функций и синтаксических конструкций в C#, позволяющих выполнять запросы к коллекциям, базам данных, XML и другим источникам данных единообразным способом.

Поддерживает два синтаксиса:

  • Синтаксис запроса (похож на SQL);
  • Синтаксис методов (цепочки расширяющих методов).

Вопрос

Приведите пример LINQ-запроса в синтаксисе методов.

Ответ

var result = numbers
.Where(x => x > 0)
.OrderBy(x => x)
.Select(x => x * 2)
.ToList();

Вопрос

Приведите пример LINQ-запроса в синтаксисе запроса.

Ответ

var result = (from x in numbers
where x > 0
orderby x
select x * 2).ToList();

Оба синтаксиса компилируются в одинаковый IL-код.


Вопрос

Какие стандартные операторы LINQ вы знаете?

Ответ

Основные операторы:

  • Фильтрация: Where;
  • Проекция: Select;
  • Сортировка: OrderBy, ThenBy, Reverse;
  • Агрегация: Count, Sum, Min, Max, Average;
  • Разбиение: Take, Skip, TakeWhile, SkipWhile;
  • Объединение: Concat, Union, Intersect, Except;
  • Группировка: GroupBy;
  • Преобразование: ToList, ToArray, ToDictionary.

Вопрос

Что делает метод SelectMany?

Ответ

SelectMany применяет функцию ко всем элементам последовательности и объединяет полученные подпоследовательности в одну плоскую последовательность. Аналог flatMap в других языках.

Пример:

var words = new[] { "hello", "world" };
var chars = words.SelectMany(w => w); // 'h','e','l','l','o','w','o','r','l','d'

Вопрос

Что такое анонимные типы и как они связаны с LINQ?

Ответ

Анонимные типы — это компиляторно-генерируемые классы без имени, используемые для временного хранения набора свойств. Часто применяются в LINQ для проекции данных.

Пример:

var people = from p in persons
select new { p.Name, p.Age };

Тип создаётся автоматически, все свойства — только для чтения.


Вопрос

Что такое deferred execution (отложенное выполнение) в LINQ?

Ответ

Отложенное выполнение означает, что LINQ-запрос не выполняется в момент его определения, а только при перечислении результата (например, через foreach или вызов .ToList()). Это позволяет повторно использовать запрос с актуальными данными.


Вопрос

Как заставить LINQ-запрос выполниться немедленно?

Ответ

Вызвать метод, выполняющий материализацию:

  • .ToList()
  • .ToArray()
  • .ToDictionary()
  • .Count()
  • .First(), .Single() и другие операторы, возвращающие скаляр.

Вопрос

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

Ответ

  • IEnumerable<T> выполняет запросы в памяти (LINQ to Objects).
  • IQueryable<T> представляет запрос, который может быть преобразован в другой язык (например, SQL в Entity Framework). Он содержит выражение (Expression) и провайдер (IQueryProvider), которые используются для построения и выполнения запроса на стороне источника данных.

Вопрос

Можно ли изменять коллекцию во время foreach?

Ответ

Нет. Изменение коллекции (добавление, удаление элементов) во время перечисления с помощью foreach приведёт к исключению InvalidOperationException. Это защита от неконсистентного состояния.


Делегаты, лямбда-выражения и события

Вопрос

Что такое делегат?

Ответ

Делегат — это тип, представляющий ссылку на метод с определённой сигнатурой. Он позволяет передавать методы как параметры, сохранять их в переменных и вызывать позже. Делегаты являются основой для событий, обратных вызовов и LINQ.

Пример:

public delegate void MessageHandler(string message);

MessageHandler handler = Console.WriteLine;
handler("Привет");

Вопрос

Какие встроенные делегаты есть в .NET?

Ответ

Основные встроенные делегаты:

  • Action — представляет метод без возвращаемого значения (до 16 параметров);
  • Func<T> — представляет метод с возвращаемым значением (последний тип — возвращаемый, до 17 параметров);
  • Predicate<T> — специализированный Func<T, bool>, часто используется для фильтрации.

Пример:

Func<int, int, int> add = (a, b) => a + b;
Action<string> print = s => Console.WriteLine(s);

Вопрос

Что такое лямбда-выражение?

Ответ

Лямбда-выражение — это анонимная функция, которую можно использовать для создания делегатов или выражений дерева выражений. Синтаксис: (параметры) => тело.

Пример:

Func<int, bool> isEven = x => x % 2 == 0;

Вопрос

В чём разница между лямбда-выражением и анонимным методом?

Ответ

Анонимный метод — это более старый синтаксис (C# 2.0) для создания делегата без имени:

delegate(int x) { return x > 0; }

Лямбда-выражение короче, поддерживает вывод типов и может компилироваться в делегат или Expression<T>. Лямбды предпочтительнее в современном коде.


Вопрос

Может ли лямбда-выражение захватывать переменные из внешней области?

Ответ

Да. Лямбда может захватывать локальные переменные и параметры окружающего метода. Такие переменные «захватываются» по ссылке, и их жизненный цикл продлевается до уничтожения делегата.

Пример:

int factor = 10;
Func<int, int> multiply = x => x * factor;
factor = 20;
Console.WriteLine(multiply(3)); // 60

Вопрос

Что такое замыкание?

Ответ

Замыкание — это комбинация функции и окружения, в котором она была определена. В C# возникает, когда лямбда или анонимный метод захватывает внешние переменные. Компилятор создаёт скрытый класс для хранения этих переменных.


Вопрос

Что такое событие (event)?

Ответ

Событие — это специальный член класса, позволяющий подписываться на уведомления от объекта. Оно ограничивает доступ к делегату: извне можно только добавлять (+=) или удалять (-=) обработчики, но нельзя вызвать событие или присвоить ему значение напрямую.

Пример:

public event Action OnCompleted;

OnCompleted?.Invoke(); // вызов внутри класса

Вопрос

Как объявить событие с пользовательским делегатом?

Ответ

Обычно используется стандартный шаблон с делегатом EventHandler<TEventArgs>:

public class WorkCompletedEventArgs : EventArgs
{
public int Result { get; }
public WorkCompletedEventArgs(int result) => Result = result;
}

public event EventHandler<WorkCompletedEventArgs> WorkCompleted;

protected virtual void OnWorkCompleted(int result)
{
WorkCompleted?.Invoke(this, new WorkCompletedEventArgs(result));
}

Вопрос

Почему события используют EventHandler вместо произвольного делегата?

Ответ

EventHandler и EventHandler<TEventArgs> — это стандартные делегаты, принятые в .NET для обеспечения единообразия. Они передают отправителя события (object sender) и аргументы (EventArgs), что упрощает обработку и совместимость.


Вопрос

Что делает оператор ?. при вызове события?

Ответ

Оператор ?. (null-conditional operator) проверяет, есть ли подписчики на событие. Если событие null (никто не подписан), вызов пропускается, и исключение не возникает.

Пример:

OnCompleted?.Invoke(); // безопасный вызов

Вопрос

Можно ли вызывать событие извне класса?

Ответ

Нет. Событие можно вызывать только из того класса, в котором оно объявлено. Это гарантирует инкапсуляцию: внешний код может только подписываться или отписываться.


Вопрос

Что такое многоадресный делегат?

Ответ

Многоадресный делегат — это делегат, который может ссылаться на несколько методов. При вызове все методы выполняются последовательно в порядке добавления. Создаётся с помощью + или +=.

Пример:

Action a = () => Console.Write("A");
Action b = () => Console.Write("B");
Action combined = a + b;
combined(); // "AB"

Вопрос

Что происходит при исключении в одном из методов многоадресного делегата?

Ответ

Если один из методов выбрасывает исключение, выполнение последующих методов в цепочке прерывается, и исключение распространяется дальше. Для безопасного вызова всех методов требуется ручной перебор через GetInvocationList().


Вопрос

Что возвращает GetInvocationList()?

Ответ

Метод GetInvocationList() возвращает массив объектов Delegate, представляющих каждый метод в цепочке вызова делегата. Позволяет выполнять методы по одному с обработкой исключений.

Пример:

foreach (Action handler in myDelegate.GetInvocationList())
{
try { handler(); }
catch (Exception ex) { Log(ex); }
}

Вопрос

Что такое дерево выражений (Expression<T>)?

Ответ

Дерево выражений — это структура данных, представляющая код в виде дерева, а не скомпилированного IL. Используется для анализа, модификации и преобразования кода во время выполнения (например, в Entity Framework для генерации SQL).

Пример:

Expression<Func<int, bool>> expr = x => x > 5;
// expr содержит объектную модель условия "x > 5"

Вопрос

Можно ли преобразовать лямбду в Expression<T> и в делегат одновременно?

Ответ

Нет. Компилятор выбирает один из двух вариантов на основе типа переменной:

Func<int, bool> f = x => x > 5;        // делегат
Expression<Func<int, bool>> e = x => x > 5; // дерево выражений

Эти типы несовместимы напрямую, но можно скомпилировать выражение в делегат: e.Compile().


Асинхронное программирование и многопоточность

Вопрос

Что такое Task?

Ответ

Task — это класс из пространства имён System.Threading.Tasks, представляющий асинхронную операцию без возвращаемого значения. Task<T> представляет операцию с результатом типа T. Оба используются в модели async/await.

Пример:

async Task<string> DownloadAsync()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://example.com");
}

Вопрос

Что делает ключевое слово async?

Ответ

Ключевое слово async указывает, что метод содержит асинхронные операции и может использовать await. Оно не делает метод автоматически асинхронным — оно лишь разрешает использование await внутри тела метода.


Вопрос

Что делает оператор await?

Ответ

Оператор await приостанавливает выполнение метода до завершения задачи (Task или Task<T>), не блокируя поток вызова. После завершения задачи метод возобновляется, и результат (если есть) становится доступен.


Вопрос

Может ли метод быть async void?

Ответ

Технически да, но не рекомендуется, кроме обработчиков событий. Такие методы называются «fire-and-forget»: исключения из них не могут быть перехвачены вызывающим кодом и приводят к аварийному завершению приложения.

Лучше всегда возвращать Task или Task<T>.


Вопрос

Что такое ValueTask и когда его использовать?

Ответ

ValueTask и ValueTask<T> — это структуры, предназначенные для оптимизации случаев, когда асинхронная операция часто завершается синхронно (например, возвращается кэшированное значение). Они уменьшают аллокации по сравнению с Task<T>.

Используются в библиотеках, где важна производительность. Не следует использовать их без профилирования.


Вопрос

Что такое контекст синхронизации (SynchronizationContext)?

Ответ

Контекст синхронизации определяет, в каком потоке будет возобновлено выполнение после await. В UI-приложениях (WPF, WinForms) он возвращает управление в UI-поток. В консольных приложениях и ASP.NET Core контекста нет, и продолжение может выполняться в любом потоке пула.


Вопрос

Что делает ConfigureAwait(false)?

Ответ

ConfigureAwait(false) указывает, что после await методу не нужно возвращаться в исходный контекст синхронизации. Это повышает производительность и предотвращает взаимоблокировки, особенно в библиотечном коде.

Пример:

var data = await httpClient.GetStringAsync(url).ConfigureAwait(false);

В ASP.NET Core эффект минимален, но в библиотеках рекомендуется использовать ConfigureAwait(false).


Вопрос

Как отменить асинхронную операцию?

Ответ

Для отмены используется CancellationToken. Метод принимает токен и периодически проверяет его состояние. Вызывающий код создаёт CancellationTokenSource и вызывает Cancel().

Пример:

async Task ProcessAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(100, ct); // поддерживает отмену
}
}

Вопрос

Что такое CancellationTokenSource?

Ответ

CancellationTokenSource — это объект, который создаёт и управляет CancellationToken. Он позволяет запросить отмену через метод Cancel() или CancelAfter(). Все подписчики на связанный токен получают уведомление об отмене.


Вопрос

Поддерживают ли все асинхронные методы отмену?

Ответ

Нет. Поддержка отмены должна быть явно реализована разработчиком или предоставлена API (например, HttpClient, Stream). Если метод не принимает CancellationToken, отменить его нельзя.


Вопрос

Что такое параллелизм и чем он отличается от асинхронности?

Ответ

  • Параллелизм — выполнение нескольких вычислений одновременно (на разных ядрах). Цель — ускорение CPU-нагруженных задач.
  • Асинхронность — неблокирующее ожидание внешних ресурсов (I/O: диск, сеть). Цель — эффективное использование потоков, а не ускорение вычислений.

Пример:

  • Параллелизм: Parallel.For
  • Асинхронность: await File.ReadAllTextAsync()

Вопрос

Как запустить код в фоновом потоке?

Ответ

Используйте Task.Run для передачи CPU-нагруженной работы в пул потоков:

await Task.Run(() => HeavyComputation());

Не используйте Task.Run для I/O-операций — они уже асинхронны и не требуют потока.


Вопрос

Что такое гонка данных (race condition)?

Ответ

Гонка данных возникает, когда два или более потока одновременно обращаются к общему ресурсу, и хотя бы один из них изменяет данные. Результат зависит от порядка выполнения, что приводит к неопределённому поведению.


Вопрос

Как предотвратить гонку данных?

Ответ

Использовать синхронизацию:

  • lock — для взаимного исключения;
  • Monitor, Mutex, Semaphore — для продвинутых сценариев;
  • потокобезопасные коллекции (ConcurrentQueue<T>, ConcurrentDictionary<TKey, TValue>);
  • неизменяемые объекты.

Пример:

private readonly object _lock = new();
lock (_lock)
{
_counter++;
}

Вопрос

Что такое lock и как он работает?

Ответ

lock обеспечивает взаимное исключение: только один поток может войти в защищённый блок. Он работает на основе объекта-монитора. Объект для блокировки должен быть приватным и неизменяемым.


Вопрос

Можно ли использовать this или typeof(MyClass) в lock?

Ответ

Нет. Использование this, публичных полей или typeof(...) в lock опасно, так как другие части кода могут использовать тот же объект для блокировки, что приведёт к взаимоблокировке. Всегда используйте приватное поле:

private readonly object _syncRoot = new();

Вопрос

Что такое взаимоблокировка (deadlock)?

Ответ

Взаимоблокировка возникает, когда два или более потоков ожидают ресурсы, удерживаемые друг другом, и ни один не может продолжить выполнение. Классический пример — поток A держит блокировку X и ждёт Y, поток B держит Y и ждёт X.


Вопрос

Как избежать взаимоблокировок?

Ответ

  • Всегда запрашивать блокировки в одном и том же порядке;
  • Минимизировать время удержания блокировок;
  • Использовать тайм-ауты (Monitor.TryEnter);
  • Предпочитать асинхронные примитивы (SemaphoreSlim.WaitAsync).

Вопрос

Что такое потокобезопасная коллекция?

Ответ

Потокобезопасная коллекция — это коллекция, которая корректно работает при одновременном доступе из нескольких потоков без внешней синхронизации. Примеры:

  • ConcurrentQueue<T>
  • ConcurrentStack<T>
  • ConcurrentBag<T>
  • ConcurrentDictionary<TKey, TValue>

Вопрос

Что делает Interlocked?

Ответ

Класс Interlocked предоставляет атомарные операции над переменными (например, инкремент, обмен, сравнение с заменой) без использования блокировок. Используется для высокопроизводительной синхронизации.

Пример:

int counter = 0;
Interlocked.Increment(ref counter);

Управление ресурсами, память и сборка мусора

Вопрос

Что такое управляемая и неуправляемая память в .NET?

Ответ

  • Управляемая память — это память, выделенная и контролируемая средой CLR (Common Language Runtime). Объекты в ней автоматически освобождаются сборщиком мусора.
  • Неуправляемая память — это ресурсы вне контроля CLR: дескрипторы файлов, сокеты, COM-объекты, нативные библиотеки. Их необходимо освобождать вручную.

Вопрос

Что такое сборщик мусора (Garbage Collector, GC)?

Ответ

Сборщик мусора — это компонент CLR, который автоматически освобождает память, занятую объектами, на которые больше нет ссылок. Он работает в фоновом режиме, группирует объекты по поколениям (Gen 0, Gen 1, Gen 2) и оптимизирует сборку на основе частоты использования.


Вопрос

Какие поколения есть в GC и зачем они нужны?

Ответ

Объекты делятся на три поколения:

  • Gen 0 — новые объекты. Сборка происходит часто и быстро.
  • Gen 1 — объекты, пережившие одну сборку. Промежуточный уровень.
  • Gen 2 — долгоживущие объекты. Сборка происходит редко и дорого.

Такая стратегия повышает производительность: большинство объектов умирают молодыми.


Вопрос

Что такое финализатор (~ClassName) и когда он вызывается?

Ответ

Финализатор — это метод, вызываемый GC перед уничтожением объекта, содержащего неуправляемые ресурсы. Его выполнение не гарантировано по времени и может быть отложено или вообще не произойти (например, при аварийном завершении).

Из-за этого финализаторы считаются устаревшим механизмом.


Вопрос

Что такое шаблон IDisposable?

Ответ

Шаблон IDisposable — это стандартный способ детерминированного освобождения ресурсов. Класс реализует интерфейс IDisposable с методом Dispose(), который вызывается явно или через оператор using.

Пример:

public class FileStream : IDisposable
{
public void Dispose()
{
// освободить ресурсы
}
}

Вопрос

Как работает оператор using?

Ответ

Оператор using гарантирует вызов Dispose() по выходу из блока, даже при возникновении исключения. Компилятор преобразует его в try-finally.

Пример:

using (var file = new StreamReader("data.txt"))
{
var content = file.ReadToEnd();
} // file.Dispose() вызван автоматически

Начиная с C# 8.0 доступен using без фигурных скобок (declaration-only):

using var file = new StreamReader("data.txt");
// Dispose вызовется в конце области видимости

Вопрос

Когда следует реализовывать IDisposable?

Ответ

Реализуйте IDisposable, если класс владеет:

  • неуправляемыми ресурсами (файлы, сокеты, хэндлы);
  • управляемыми объектами, реализующими IDisposable (например, Stream, DbContext).

Вопрос

Нужно ли реализовывать финализатор, если есть IDisposable?

Ответ

Только если класс напрямую владеет неуправляемыми ресурсами. В этом случае используется гибридный шаблон:

  • Dispose() вызывает Dispose(true) и подавляет финализацию (GC.SuppressFinalize(this));
  • финализатор вызывает Dispose(false) для экстренного освобождения.

В большинстве случаев достаточно только IDisposable.


Вопрос

Что делает GC.SuppressFinalize(this)?

Ответ

Метод GC.SuppressFinalize(this) сообщает GC, что финализатор для данного объекта вызывать не нужно, так как ресурсы уже освобождены через Dispose(). Это повышает производительность, избегая лишней работы в очереди финализации.


Вопрос

Что такое утечка памяти в управляемом коде?

Ответ

Утечка памяти в .NET возникает не из-за забытого free, а из-за непреднамеренного удержания ссылок на объекты, которые больше не нужны. Примеры:

  • статические коллекции, в которые добавляются объекты и никогда не удаляются;
  • события, на которые подписались, но не отписались;
  • кэши без ограничения размера.

Вопрос

Как избежать утечек памяти при работе с событиями?

Ответ

Отписываться от событий, когда подписчик больше не нужен:

publisher.Event -= handler;

Или использовать слабые ссылки (WeakReference) в продвинутых сценариях.


Вопрос

Что такое Span<T> и Memory<T>?

Ответ

Span<T> и Memory<T> — это типы, введённые в .NET Core для безопасной и эффективной работы с непрерывными участками памяти (стек, управляемая куча, неуправляемая память).

  • Span<T> — ref-struct, работает только в стеке, не может быть полем или возвращаемым значением асинхронного метода.
  • Memory<T> — обёртка над Span<T>, может жить в куче.

Используются для минимизации аллокаций (zero-allocation APIs).


Вопрос

Можно ли использовать Span<T> в асинхронных методах?

Ответ

Нет. Span<T> — это ref-struct, который не может покидать стек. Поэтому его нельзя использовать в качестве поля, параметра лямбды, возвращаемого значения async-метода или в yield return.

Для таких случаев используйте Memory<T> и получайте Span<T> внутри синхронного контекста.


Вопрос

Что такое аллокация и почему её стоит минимизировать?

Ответ

Аллокация — это выделение памяти в управляемой куче. Частые аллокации приводят к:

  • увеличению давления на GC;
  • частым сборкам мусора;
  • снижению производительности.

Минимизация достигается через повторное использование объектов, пулы (ArrayPool<T>, ObjectPool<T>), структуры и Span<T>.


Вопрос

Что такое ArrayPool<T>?

Ответ

ArrayPool<T> — это пул массивов, позволяющий повторно использовать временные массивы вместо их постоянного создания и уничтожения. Особенно полезен в высоконагруженных сценариях.

Пример:

var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
try
{
// использовать buffer
}
finally
{
pool.Return(buffer);
}

Вопрос

Влияет ли вызов GC.Collect() на производительность?

Ответ

Да. Принудительный вызов GC.Collect() останавливает все потоки (stop-the-world pause) и выполняет полную сборку. Это резко снижает производительность. Использовать его следует только в крайних случаях (например, после загрузки большого объёма данных в UI-приложении).


Вопрос

Что такое «давление на GC» (GC pressure)?

Ответ

Давление на GC — это частота и объём аллокаций, которые заставляют сборщик мусора работать чаще. Высокое давление приводит к снижению пропускной способности и увеличению задержек. Оптимизация направлена на его снижение.


Рефлексия, атрибуты и динамические возможности

Вопрос

Что такое рефлексия в C#?

Ответ

Рефлексия — это механизм, позволяющий во время выполнения анализировать метаданные сборок, типов, методов, свойств и других членов, а также динамически создавать экземпляры, вызывать методы и получать/устанавливать значения.

Основные классы находятся в пространстве имён System.Reflection.


Вопрос

Как получить объект Type для типа?

Ответ

Существует несколько способов:

  • Через оператор typeof(): Type t = typeof(string);
  • Через метод GetType() у экземпляра: Type t = "hello".GetType();
  • Через статический метод Type.GetType("FullName").

Вопрос

Как создать экземпляр типа с помощью рефлексии?

Ответ

Используется метод Activator.CreateInstance:

Type type = typeof(Person);
object instance = Activator.CreateInstance(type);
// или с параметрами:
object instance = Activator.CreateInstance(type, "Алиса", 30);

Можно также использовать ConstructorInfo.Invoke() после получения конструктора через type.GetConstructor(...).


Вопрос

Как вызвать метод объекта с помощью рефлексии?

Ответ

  1. Получить MethodInfo через type.GetMethod("MethodName").
  2. Вызвать method.Invoke(instance, arguments).

Пример:

MethodInfo method = typeof(Console).GetMethod("WriteLine", new[] { typeof(string) });
method.Invoke(null, new object[] { "Привет" });

Вопрос

Как получить значение свойства с помощью рефлексии?

Ответ

Используется PropertyInfo.GetValue(instance):

PropertyInfo prop = typeof(Person).GetProperty("Name");
string name = (string)prop.GetValue(personInstance);

Для установки значения — SetValue(instance, value).


Вопрос

Что такое атрибуты в C#?

Ответ

Атрибуты — это специальные классы, наследуемые от System.Attribute, которые позволяют добавлять декларативную метаинформацию к коду (типам, методам, свойствам и т.д.). Эта информация сохраняется в метаданных сборки и может быть прочитана через рефлексию.


Вопрос

Как объявить пользовательский атрибут?

Ответ

Создайте класс, унаследованный от Attribute, и пометьте его как AttributeUsage, если нужно ограничить применение:

[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute
{
public string Level { get; }
public LogAttribute(string level) => Level = level;
}

Применение:

[Log("Debug")]
public void Process() { }

Вопрос

Как проверить наличие атрибута у метода?

Ответ

Используется метод GetCustomAttribute<T>() или IsDefined:

var method = typeof(MyClass).GetMethod("Process");
var attr = method.GetCustomAttribute<LogAttribute>();
if (attr != null)
{
Console.WriteLine(attr.Level);
}

Вопрос

Что делает AttributeUsage?

Ответ

AttributeUsage указывает, к каким элементам кода можно применять атрибут. Параметр ValidOn задаёт допустимые цели: Class, Method, Property, Assembly и т.д. Также можно разрешить множественное применение (AllowMultiple = true).


Вопрос

Что такое dynamic?

Ответ

Ключевое слово dynamic отключает статическую проверку типов во время компиляции. Все операции с dynamic разрешаются во время выполнения через DLR (Dynamic Language Runtime).

Пример:

dynamic obj = GetUnknownObject();
obj.DoSomething(); // проверка происходит в runtime

Вопрос

В чём опасность использования dynamic?

Ответ

  • Ошибки типов обнаруживаются только во время выполнения;
  • Потеря IntelliSense и рефакторинга;
  • Снижение производительности из-за накладных расходов DLR.

Используется редко: при взаимодействии с COM, динамическими языками (Python, JavaScript через interop) или нестрого типизированными API.


Вопрос

Что такое DLR (Dynamic Language Runtime)?

Ответ

DLR — это подсистема .NET, обеспечивающая поддержку динамических языков и операций. Она предоставляет кэширование, привязку вызовов и совместимость между динамическими и статическими частями кода.


Вопрос

Можно ли использовать рефлексию вместо dynamic?

Ответ

Да, но dynamic проще и чище для последовательных вызовов. Рефлексия требует явного получения MethodInfo, PropertyInfo и вызова Invoke, что громоздко. Однако рефлексия даёт больше контроля и лучше подходит для однократных операций.


Вопрос

Что такое late binding?

Ответ

Late binding (позднее связывание) — это разрешение вызовов методов и доступа к свойствам во время выполнения, а не на этапе компиляции. В C# реализуется через dynamic или рефлексию.


Вопрос

Как повысить производительность рефлексии?

Ответ

  • Кэшировать MethodInfo, PropertyInfo и другие объекты рефлексии;
  • Использовать делегаты, созданные через Delegate.CreateDelegate;
  • Применять исходниковые генераторы (source generators) в современных проектах;
  • Избегать рефлексии в горячих путях без профилирования.

Вопрос

Что такое source generators?

Ответ

Source generators — это компоненты времени компиляции (начиная с C# 9.0), которые анализируют код и генерируют дополнительный C#-код до компиляции. Они заменяют многие сценарии рефлексии, обеспечивая нулевые накладные расходы во время выполнения.

Примеры: сериализаторы, мапперы, логгеры.


Вопрос

Можно ли изменить поведение метода через рефлексию?

Ответ

Нет. Рефлексия позволяет вызывать существующие методы, но не изменять их IL-код или логику. Для изменения поведения используются AOP-библиотеки (например, Castle DynamicProxy, Fody) или перехватчики.


Сборки, компиляция, метаданные и выполнение

Вопрос

Что такое сборка (assembly) в .NET?

Ответ

Сборка — это основная единица развёртывания и версионирования в .NET. Она представляет собой файл с расширением .dll или .exe, содержащий скомпилированный IL-код, метаданные и манифест. Сборка может состоять из одного или нескольких модулей.


Вопрос

Что содержится в манифесте сборки?

Ответ

Манифест сборки содержит:

  • имя сборки (включая версию, культуру, публичный ключ);
  • список зависимостей от других сборок;
  • информацию о типах, экспортируемых сборкой;
  • разрешения безопасности (в старых версиях .NET Framework).

Вопрос

Что такое метаданные в .NET?

Ответ

Метаданные — это табличная информация о типах, методах, свойствах, параметрах и других элементах, встроенная в каждую сборку. Они позволяют среде выполнения загружать типы, обеспечивать безопасность типов и поддерживать рефлексию.


Вопрос

Как происходит компиляция C#-кода?

Ответ

C#-код компилируется в два этапа:

  1. Компилятор Roslyn преобразует исходный код в промежуточный язык (IL, Intermediate Language) и упаковывает его в сборку вместе с метаданными.
  2. JIT-компилятор (Just-In-Time) во время выполнения преобразует IL в машинный код, специфичный для текущей архитектуры процессора.

Вопрос

Что такое JIT-компиляция?

Ответ

JIT (Just-In-Time) — это компиляция IL-кода в нативный машинный код непосредственно перед первым вызовом метода. Это позволяет оптимизировать код под конкретное оборудование и ОС, но добавляет накладные расходы при первом запуске.


Вопрос

Что такое AOT-компиляция и где она используется?

Ответ

AOT (Ahead-Of-Time) — это компиляция всего IL-кода в нативный машинный код до выполнения программы. Используется в:

  • .NET Native (UWP);
  • iOS-приложениях (из-за ограничений Apple);
  • Native AOT (.NET 7+), позволяющем создавать полностью самодостаточные исполняемые файлы без JIT.

Преимущества: быстрый запуск, меньший объём памяти, отсутствие зависимости от CLR.


Вопрос

Что такое домен приложения (AppDomain)?

Ответ

AppDomain — это изолированная среда выполнения внутри одного процесса, использовавшаяся в .NET Framework для загрузки и выгрузки сборок без перезапуска процесса. В .NET Core и .NET 5+ AppDomain устарел и не поддерживается (кроме ограниченной эмуляции).

Современная альтернатива — отдельные процессы или сборки, загружаемые через AssemblyLoadContext.


Вопрос

Что такое AssemblyLoadContext?

Ответ

AssemblyLoadContext — это механизм в .NET Core/.NET 5+, позволяющий изолированно загружать и выгружать сборки. Он заменяет AppDomain и даёт контроль над разрешением зависимостей и жизненным циклом сборок.

Пример:

var context = new AssemblyLoadContext("Plugin", isCollectible: true);
var assembly = context.LoadFromAssemblyPath("plugin.dll");
// ... использование
context.Unload();
GC.Collect(); // сборка может быть выгружена

Вопрос

Как загрузить сборку во время выполнения?

Ответ

Используется статический метод Assembly.LoadFrom или Assembly.LoadFile, либо через AssemblyLoadContext:

Assembly asm = Assembly.LoadFrom("MyLibrary.dll");
Type type = asm.GetType("MyLibrary.MyClass");
object instance = Activator.CreateInstance(type);

Вопрос

Что делает атрибут InternalsVisibleTo?

Ответ

Атрибут InternalsVisibleTo предоставляет доступ к членам с модификатором internal из другой сборки. Часто используется для тестовых проектов.

Пример в AssemblyInfo.cs или в основном файле:

[assembly: InternalsVisibleTo("MyApp.Tests")]

Теперь сборка MyApp.Tests может использовать internal-типы из текущей сборки.


Вопрос

Что такое сильное имя (strong name) сборки?

Ответ

Сильное имя — это уникальный идентификатор сборки, создаваемый с помощью криптографического ключа. Оно включает: имя, версию, культуру и публичный ключ. Обеспечивает целостность и предотвращает подмену сборки.

В современных проектах (.NET Core+) сильные имена используются редко; предпочтение отдаётся простому имени и управлению версиями через NuGet.


Вопрос

Что такое GAC (Global Assembly Cache)?

Ответ

GAC — это системное хранилище для общих сборок с сильными именами в .NET Framework. Позволяло избежать «DLL hell». В .NET Core и .NET 5+ GAC не используется; зависимости разрешаются через локальные папки или NuGet.


Вопрос

Как проверить, является ли сборка управляемой?

Ответ

Управляемая сборка содержит таблицу метаданных и IL-код. Можно проверить с помощью утилит вроде ildasm, dotnet dump или программно через Module.GetPEKind().


Вопрос

Что такое portable PDB?

Ответ

Portable PDB — это формат отладочной информации, совместимый между платформами (в отличие от старого Windows-only PDB). Содержит данные о строках, переменных и исходном коде для отладки и анализа стека вызовов.

Используется по умолчанию в современных проектах.


Вопрос

Можно ли декомпилировать сборку обратно в C#?

Ответ

Да. Существуют инструменты (ILSpy, dotPeek, dnSpy), которые могут восстановить читаемый C#-код из IL и метаданных. Однако имена переменных, комментарии и некоторые оптимизации могут быть утеряны.

Для защиты используется обфускация (например, через ConfuserEx, Dotfuscator).


Современные возможности C# (8.0 – 12.0)

Вопрос

Что такое nullable reference types?

Ответ

Nullable reference types — это функция, введённая в C# 8.0, которая позволяет статически анализировать потенциальные ошибки, связанные с null для ссылочных типов. По умолчанию ссылочные типы считаются не-nullable, а для допущения null используется суффикс ?.

Пример:

string name = "Алиса";     // не может быть null
string? nickname = null; // может быть null

Требует включения в проект через <Nullable>enable</Nullable>.


Вопрос

Как включить поддержку nullable reference types?

Ответ

Добавьте в файл проекта (.csproj) следующее свойство:

<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

После этого компилятор будет выдавать предупреждения при возможном разыменовании null.


Вопрос

Что делает оператор ! (null-forgiving operator)?

Ответ

Оператор ! сообщает компилятору, что выражение точно не равно null, даже если анализ показывает обратное. Используется для подавления предупреждений, когда разработчик уверен в безопасности.

Пример:

string? input = GetInput();
string safe = input!; // "я знаю, что input не null"

Следует использовать осторожно — ошибка приведёт к NullReferenceException.


Вопрос

Что такое pattern matching и какие его формы есть в C#?

Ответ

Pattern matching — это механизм сопоставления значения с шаблоном. Поддерживаемые формы:

  • is-pattern: if (obj is string s) { ... }
  • switch-expression: var result = value switch { 1 => "один", _ => "много" };
  • property pattern: if (person is { Age: > 18, Name: not null })
  • positional pattern (с кортежами или деконструкторами): if (point is (0, 0))
  • relational patterns: >, <, >=, <= внутри switch или is.

Вопрос

Что такое property pattern?

Ответ

Property pattern позволяет проверять значения свойств объекта без извлечения:

if (person is { Name: "Алиса", Age: >= 18 })
{
Console.WriteLine("Взрослая Алиса");
}

Эквивалентно:

if (person.Name == "Алиса" && person.Age >= 18)

Но безопаснее при возможном null.


Вопрос

Что такое записи (record) и зачем они нужны?

Ответ

record — это ссылочный тип, оптимизированный для представления неизменяемых данных. Автоматически реализует:

  • значение-ориентированное равенство (Equals, GetHashCode);
  • метод ToString() с выводом полей;
  • метод With() для создания копии с изменёнными свойствами.

Пример:

public record Person(string FirstName, string LastName);
var p1 = new Person("Алиса", "Смирнова");
var p2 = p1 with { LastName = "Петрова" };

Вопрос

Можно ли сделать запись изменяемой?

Ответ

Да, но это нарушает основную идею record. По умолчанию свойства в первичном конструкторе получают модификатор init. Чтобы сделать их изменяемыми, нужно объявить явно:

public record MutablePerson
{
public string Name { get; set; }
}

Такой подход теряет преимущества неизменяемости.


Вопрос

Что такое init-only свойства?

Ответ

Свойства с модификатором init могут быть установлены только во время инициализации объекта — в конструкторе или через инициализатор объекта. После этого они становятся доступны только для чтения.

Пример:

public class ImmutablePoint
{
public int X { get; init; }
public int Y { get; init; }
}

var p = new ImmutablePoint { X = 10, Y = 20 }; // допустимо
// p.X = 5; // ошибка компиляции

Вопрос

Что такое top-level statements?

Ответ

Top-level statements — это синтаксис, позволяющий писать код программы прямо в файле без объявления класса и метода Main. Компилятор автоматически создаёт скрытый класс и точку входа.

Пример (Program.cs):

Console.WriteLine("Привет, мир!");

Упрощает написание небольших программ и скриптов.


Вопрос

Что такое глобальные using-директивы?

Ответ

Глобальные using-директивы применяются ко всему проекту и объявляются в любом файле с префиксом global::.

Пример:

global using System;
global using static System.Console;

Можно также включить через файл проекта:

<ItemGroup>
<Using Include="System" />
<Using Include="System.Collections.Generic" />
</ItemGroup>

Вопрос

Что такое file-scoped namespaces?

Ответ

File-scoped namespace — это сокращённый синтаксис объявления пространства имён в одном файле без фигурных скобок.

Вместо:

namespace MyCompany.MyApp
{
class Program { }
}

Пишут:

namespace MyCompany.MyApp;

class Program { }

Уменьшает уровень вложенности и улучшает читаемость.


Вопрос

Что такое первичные конструкторы для классов (C# 12)?

Ответ

Начиная с C# 12, первичные конструкторы, ранее доступные только для record, можно использовать в обычных классах и структурах.

Пример:

public class Person(string firstName, string lastName)
{
public string FullName => $"{firstName} {lastName}";
}

Параметры конструктора становятся доступны во всём теле класса как поля только для чтения.


Вопрос

Что такое коллекции с начальным значением (collection expressions)?

Ответ

Начиная с C# 12, появился единый синтаксис для создания массивов, списков и других коллекций без указания типа:

int[] a = [1, 2, 3];
List<int> b = [1, 2, 3];
ReadOnlySpan<int> c = [1, 2, 3];

Используется квадратная скобка без new. Компилятор выбирает наиболее подходящий тип.


Вопрос

Что такое alias-любые типы (using alias = ...) с обобщениями?

Ответ

Начиная с C# 12, можно создавать псевдонимы для обобщённых типов:

using IntList = List<int>;
using StringMap = Dictionary<string, object>;

Это упрощает использование сложных типов и повышает читаемость.


Вопрос

Что такое интерполированные строки с обработкой времени компиляции?

Ответ

Начиная с C# 10, интерполированные строки могут быть обработаны генераторами исходного кода во время компиляции. Это позволяет создавать эффективные API без аллокаций (например, логгеры, SQL-билдеры).

Пример:

Logger.Log($"Пользователь {name} вошёл в систему");

Может быть преобразован в вызов без создания строки.


Вопрос

Что такое required members?

Ответ

Модификатор required указывает, что свойство обязательно должно быть установлено при инициализации объекта. Компилятор проверяет, что все required-свойства заданы.

Пример:

public class Person
{
public required string Name { get; init; }
}

var p = new Person { Name = "Алиса" }; // OK
var q = new Person(); // ошибка компиляции

Вопрос

Что такое автоприведение делегатов (delegate conversion improvements)?

Ответ

Начиная с C# 10, компилятор автоматически преобразует методы в делегаты без явного указания типа, если сигнатура совпадает:

void Print(string s) => Console.WriteLine(s);
Action<string> action = Print; // раньше требовалось new Action<string>(Print)

Упрощает передачу методов как делегатов.


Практические сценарии, сравнения и лучшие практики

Вопрос

В чём разница между struct и class?

Ответ

  • struct — значимый тип, хранится в стеке (или внутри объекта), копируется при присваивании, не поддерживает наследование (только интерфейсы).
  • class — ссылочный тип, хранится в куче, передаётся по ссылке, поддерживает наследование и полиморфизм.

Используйте struct для маленьких, неизменяемых, данных-значений (например, Point, DateTime). Используйте class для сложных объектов с поведением и жизненным циклом.


Вопрос

Когда следует использовать struct?

Ответ

Используйте struct, если:

  • тип представляет одно значение (логически атомарен);
  • размер меньше 16 байт;
  • он неизменяем;
  • он нечасто упаковывается (boxing).

Нарушение этих правил может привести к снижению производительности из-за частого копирования.


Вопрос

Что такое упаковка (boxing) и распаковка (unboxing)?

Ответ

  • Упаковка — преобразование значимого типа в ссылочный (object или интерфейс). Создаётся объект в куче.
  • Распаковка — обратное преобразование из object в значимый тип. Требует точного совпадения типов.

Пример:

int x = 10;
object obj = x; // boxing
int y = (int)obj; // unboxing

Частая упаковка снижает производительность.


Вопрос

Почему string является ссылочным типом, но ведёт себя как значимый?

Ответ

string — ссылочный тип, но он неизменяем (immutable). Любая операция (например, конкатенация) создаёт новый экземпляр строки. Это даёт семантику, похожую на значимые типы, но с преимуществами пула строк и эффективного управления памятью.


Вопрос

Что такое интернирование строк?

Ответ

Интернирование — это механизм, при котором одинаковые строковые литералы в коде ссылаются на один и тот же объект в памяти. Это экономит память и ускоряет сравнение (через ссылки).

Метод string.Intern() добавляет строку в пул; string.IsInterned() проверяет наличие.


Вопрос

Как правильно сравнивать строки?

Ответ

Используйте перегруженный оператор == или методы Equals с указанием StringComparison:

if (str1.Equals(str2, StringComparison.OrdinalIgnoreCase))

Избегайте == при работе с пользовательским вводом без указания культуры. Никогда не используйте ReferenceEquals для сравнения содержимого.


Вопрос

Что такое StringBuilder и когда его использовать?

Ответ

StringBuilder — это изменяемая строка, оптимизированная для множественных модификаций. Используется, когда выполняется много конкатенаций (особенно в цикле).

Пример:

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);
string result = sb.ToString();

Для 1–2 конкатенаций обычные строки эффективнее.


Вопрос

В чём разница между == и .Equals()?

Ответ

  • Для ссылочных типов == по умолчанию сравнивает ссылки (адреса), а .Equals() — содержимое (если переопределён).
  • Для значимых типов оба сравнивают значения.
  • Для string оператор == переопределён и сравнивает содержимое.

Рекомендуется использовать .Equals() с явным указанием сравнения, особенно для строк.


Вопрос

Как реализовать корректное равенство для своего класса?

Ответ

  1. Переопределите Equals(object) и GetHashCode().
  2. Реализуйте IEquatable<T> для типобезопасного сравнения.
  3. Убедитесь, что GetHashCode() возвращает одинаковое значение для равных объектов.
  4. Не меняйте поля, участвующие в GetHashCode(), после помещения объекта в хэш-коллекцию.

Вопрос

Почему важно переопределять GetHashCode() вместе с Equals()?

Ответ

Коллекции, основанные на хэшах (Dictionary, HashSet), используют GetHashCode() для быстрого поиска. Если два равных объекта возвращают разные хэши, коллекция не сможет найти элемент, что нарушит её логику.


Вопрос

Что такое сериализация и какие её виды есть в .NET?

Ответ

Сериализация — это преобразование объекта в последовательность байтов для сохранения или передачи. Основные виды:

  • Binary serialization (устарела, небезопасна);
  • JSON (System.Text.Json, Newtonsoft.Json);
  • XML (XmlSerializer);
  • DataContract (WCF);
  • Source-generated serializers (.NET 7+).

Предпочтителен System.Text.Json для новых проектов.


Вопрос

Как сделать класс сериализуемым в System.Text.Json?

Ответ

Класс должен иметь публичный конструктор и публичные свойства. Атрибуты не обязательны, но можно использовать [JsonPropertyName] для настройки имён.

Пример:

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

Для неизменяемых типов используйте первичный конструктор или JsonConstructor.


Вопрос

Что такое readonly struct?

Ответ

readonly struct гарантирует, что ни одно поле структуры не будет изменено после создания. Это предотвращает неявное копирование при вызове методов и повышает производительность.

Пример:

public readonly struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
}

Вопрос

Что такое in-параметры?

Ответ

Модификатор in передаёт аргумент по ссылке, но запрещает его изменение. Используется для больших структур, чтобы избежать копирования без риска побочных эффектов.

Пример:

void Process(in Point p) { ... }

Вызов:

Process(myPoint); // без ref, безопасно

Вопрос

Как избежать аллокаций в горячих путях?

Ответ

  • Используйте Span<T>, Memory<T>, ArrayPool<T>;
  • Избегайте замыканий, захватывающих переменные;
  • Предпочитайте структуры для маленьких объектов;
  • Используйте ValueTask вместо Task при частом синхронном завершении;
  • Избегайте LINQ в критических участках (используйте циклы).

Вопрос

Что такое defensive copying?

Ответ

Defensive copying — это неявное копирование структуры компилятором, когда есть риск её изменения. Происходит, например, при вызове метода экземпляра у readonly struct через переменную (а не через литерал). Может снизить производительность.

Решение — помечать методы как readonly:

public readonly double DistanceTo(in Point other) { ... }

Вопрос

Как правильно обрабатывать исключения в библиотечном коде?

Ответ

  • Выбрасывайте конкретные исключения (ArgumentNullException, InvalidOperationException);
  • Не подавляйте исключения без логирования;
  • Используйте шаблон TryXXX для ожидаемых сбоев (TryParse);
  • Не используйте исключения для управления потоком выполнения.

Вопрос

Что такое идиома «fail fast»?

Ответ

«Fail fast» — это принцип, согласно которому ошибка должна быть обнаружена и сообщена как можно раньше. В C# это достигается проверкой входных параметров в начале метода и выбрасыванием исключений при нарушении контракта.


Вопрос

Как выбрать между List<T>, Array, IReadOnlyList<T> для возвращаемого значения?

Ответ

  • Возвращайте IReadOnlyList<T> или IEnumerable<T>, если вызывающий код не должен изменять коллекцию.
  • Возвращайте Array только если данные действительно фиксированы и производительность критична.
  • Избегайте возврата List<T> из публичных API — это раскрывает реализацию и позволяет изменять состояние.

Вопрос

Почему нельзя использовать DateTime.Now в юнит-тестах?

Ответ

DateTime.Now зависит от системных часов, что делает тесты недетерминированными. Вместо этого внедряйте зависимость (например, IClock) или используйте DateTime.UtcNow с контролируемым временем в тестах.


Вопрос

Как обеспечить потокобезопасность в одиночке (Singleton)?

Ответ

Используйте ленивую инициализацию с Lazy<T>:

public sealed class Singleton
{
private static readonly Lazy<Singleton> _instance = new(() => new());
public static Singleton Instance => _instance.Value;
}

Это потокобезопасно, лениво и эффективно.


Вопрос

Что такое ConfigureAwait(false) и почему его нужно использовать в библиотеках?

Ответ

ConfigureAwait(false) предотвращает захват контекста синхронизации, что избегает взаимоблокировок и снижает накладные расходы. В библиотечном коде контекст не важен, поэтому его следует отключать.


Вопрос

Как избежать мёртвого кода и неиспользуемых зависимостей?

Ответ

  • Регулярно используйте анализаторы кода (Roslyn analyzers);
  • Включите предупреждения компилятора как ошибки;
  • Проводите code review;
  • Используйте инструменты вроде ReSharper, SonarQube.

Вопрос

Что такое идиоматичный C#?

Ответ

Идиоматичный C# — это код, следующий принятым соглашениям языка:

  • использование свойств вместо публичных полей;
  • применение using для ресурсов;
  • предпочтение композиции наследованию;
  • использование var при очевидном типе;
  • применение современных конструкций (record, pattern matching, top-level statements).

Такой код легче читать, поддерживать и проверять.