Вопросы по языку C#, рантайму CLR, сборке мусора (GC), асинхронности (async/await, Tasks), LINQ и ASP.NET Core.
* **Хранение в памяти**: Типы значений (структуры `struct`, перечисления `enum`, базовые числовые типы) хранятся там, где были объявлены: в стеке (Stack) при локальном объявлении в методе, либо в куче как часть содержащего их объекта. Ссылочные типы (классы `class`, интерфейсы, делегаты, массивы) всегда хранятся в управляемой куче (Managed Heap), а переменная содержит лишь ссылку на область памяти. * **Копирование**: При присвоении переменной типа значения создается полная копия данных. При присвоении переменной ссылочного типа копируется только ссылка на тот же объект. * **Наследование**: Классы поддерживают наследование, структуры — нет (все структуры неявно наследуются от `System.ValueType`).
Сборщик мусора в .NET автоматически управляет памятью, освобождая ее от объектов, на которые больше нет активных ссылок. * **Поколения (Generations)**: Память делится на три поколения для оптимизации производительности: * **Поколение 0**: Новые, короткоживущие объекты (локальные переменные). Сборка мусора происходит чаще всего и быстрее всего. * **Поколение 1**: Буферная зона между короткими и длинными объектами. * **Поколение 2**: Долгоживущие объекты (статические переменные, синглтоны). Сборка происходит реже всего. * **LOH (Large Object Heap)**: Объекты размером более 85,000 байт аллоцируются в специальной куче больших объектов. Сборка мусора в LOH происходит только во время сборки поколения 2 и по умолчанию не дефрагментирует память (из-за дороговизны копирования больших блоков).
* **`ref`**: Передает аргумент по ссылке. Переменная должна быть обязательно инициализирована перед передачей в метод. Метод может как читать, так и изменять ее значение. * **`out`**: Передает аргумент по ссылке. Переменная не обязательно должна быть инициализирована перед вызовом. Метод **обязан** присвоить переменной значение перед выходом из метода. * **`in`**: Передает аргумент по ссылке, но делает его доступным **только для чтения** внутри метода. Позволяет избежать копирования больших структур (`struct`) для оптимизации производительности.
* **`string`**: Неизменяемый (immutable) тип данных. Любая операция над строкой (конкатенация, замена символов) создает новый объект строки в куче, что при частых операциях сильно нагружает сборщик мусора. * **`StringBuilder`**: Изменяемый класс для работы со строками. Под капотом хранит буфер символов. Изменения происходят внутри существующего буфера, что экономит память и процессорное время при частых конкатенациях.
Интерфейс `IDisposable` с методом `Dispose()` используется для явного освобождения неуправляемых ресурсов (файловые дескрипторы, сетевые сокеты, соединения с БД). * **Конструкция `using`**: Является синтаксическим сахаром над блоком `try-finally`. В блоке `finally` гарантированно вызывается метод `Dispose()`, даже если внутри `try` произошло исключение. * **Современный синтаксис (C# 8.0+)**: `using var resource = new MyResource();` автоматически вызовет `Dispose()` при выходе переменной из области видимости метода.
* **Наследование**: Класс может наследоваться только от одного абстрактного класса, но реализовывать множество интерфейсов. * **Конструкторы**: Абстрактные классы могут иметь конструкторы, интерфейсы — нет. * **Поля**: Абстрактные классы могут иметь любые поля (состояние). Интерфейсы до C# 8.0 не могли иметь полей и реализаций. Начиная с C# 8.0, интерфейсы поддерживают реализации методов по умолчанию (Default Interface Members), но по-прежнему не могут иметь полей экземпляра.
Компилятор C# преобразует методы, помеченные как `async`, в конечный автомат (State Machine), реализующий интерфейс `IAsyncStateMachine`. 1. При встрече ключевого слова `await` метод проверяет, завершена ли асинхронная задача. 2. Если задача завершена, выполнение продолжается синхронно. 3. Если задача не завершена, конечный автомат сохраняет текущее состояние метода (локальные переменные, точку возврата) и регистрирует продолжение задачи. 4. Поток освобождается для выполнения других задач. Когда асинхронная операция завершается, рантайм восстанавливает выполнение конечного автомата (в контексте синхронизации, если он настроен).
* **`Task`**: Ссылочный тип (класс). Каждое создание Task приводит к аллокации памяти в управляемой куче. Это несет накладные расходы. * **`ValueTask`**: Тип значения (структура). Используется для оптимизации производительности в сценариях, когда метод завершается синхронно в большинстве случаев (например, чтение данных из кэша). Если результат уже готов, аллокации в куче не происходит. Если асинхронная операция действительно требуется, под капотом создается стандартный `Task`.
LINQ (Language Integrated Query) — синтаксис запросов к различным источникам данных (коллекциям, БД, XML). * **Deferred Execution (отложенное выполнение)**: Запрос не выполняется в момент его объявления. Он выполняется только тогда, когда начинается перебор элементов (например, через цикл `foreach` или вызов методов вроде `ToList()`). Большинство операций LINQ (например, `Where`, `Select`) ленивы. * **Immediate Execution (немедленное выполнение)**: Запрос выполняется немедленно, возвращая готовую коллекцию или вычисленное значение. Вызывается методами преобразования (`ToList()`, `ToArray()`, `ToDictionary()`) или агрегации (`Count()`, `First()`, `Sum()`).
* **`IEnumerable`**: Подходит для коллекций в оперативной памяти (In-Memory). Запросы к нему выполняются на стороне клиента (все данные загружаются в память перед фильтрацией). * **`IQueryable`**: Наследует `IEnumerable`. Подходит для внешних источников данных (баз данных). Запросы транслируются в дерево выражений (Expression Tree), которое провайдер (например, Entity Framework) переводит в SQL-запрос и выполняет на стороне СУБД. * **`IList`**: Наследует `IEnumerable`. Предоставляет доступ к элементам по индексу, а также методы для изменения коллекции (добавление, удаление элементов).
* **Boxing (упаковка)**: Процесс преобразования типа значения в тип `object` или в тип интерфейса, который он реализует. Память под значение выделяется в управляемой куче, данные копируются, и создается ссылка на этот объект. * **Unboxing (распаковка)**: Обратный процесс получения типа значения из объекта. Проверяется соответствие типов, и значение копируется из кучи обратно в стек. * **Производительность**: Данные операции очень дороги, так как упаковка вызывает аллокации в куче и увеличивает нагрузку на сборщик мусора. Рекомендуется избегать упаковки с помощью использования обобщений (Generics).
* **`delegate`**: Базовый тип для объявления указателей на методы. * **`Action`**: Встроенный делегат для методов, которые выполняют действие и **не возвращают** значение (`void`). Может принимать до 16 аргументов. * **`Func`**: Встроенный делегат для методов, которые принимают аргументы и **возвращают** значение. Последний generic-параметр указывает на тип возвращаемого значения. * **`Predicate`**: Встроенный делегат для методов, которые принимают один аргумент и возвращают логическое значение `bool`. Эквивалентен `Func<T, bool>`.
Рефлексия — это механизм изучения метаданных сборки, модулей и типов во время выполнения приложения (через класс `Type`). Позволяет динамически создавать экземпляры типов, вызывать методы (в том числе приватные) и исследовать атрибуты.
SOLID — это набор из 5 принципов объектно-ориентированного проектирования: * **S**ingle Responsibility (Единственная ответственность): класс отвечает за одну задачу. * **O**pen/Closed (Открытость/закрытость): открыт для расширения, закрыт для изменения. * **L**iskov Substitution (Подстановка Лисков): наследники должны сохранять поведение базового класса. * **I**nterface Segregation (Разделение интерфейса): лучше много мелких специализированных интерфейсов, чем один универсальный. * **D**ependency Inversion (Инверсия зависимостей): модули верхних уровней не должны зависеть от нижних; все должны зависеть от абстракций.
* **Перегрузка (Overload)**: Создание методов с одинаковым именем, но разными сигнатурами (типами или количеством параметров) в рамках одного класса. Разрешается на этапе компиляции (статический полиморфизм). * **Переопределение (Override)**: Изменение реализации виртуального метода (`virtual`) или абстрактного метода базового класса в классе-наследнике. Разрешается на этапе выполнения программы (динамический полиморфизм).
* **Делегат**: Это строго типизированный указатель на один или несколько методов. Его можно вызывать напрямую и передавать как параметр. * **Событие (Event)**: Это обертка над делегатом (паттерн Издатель-Подписчик). Событие накладывает ограничения: подписчики могут подписываться (`+=`) и отписываться (`-=`), но вызывать событие может исключительно тот класс, в котором оно было объявлено.
* **`static`**: Указывает, что член принадлежит самому классу, а не его экземплярам. Общий для всех объектов данного типа. * **`const`**: Константа времени компиляции (compile-time). Значение должно быть определено при объявлении и вшивается компилятором прямо в код вызова. * **`readonly`**: Константа времени выполнения (run-time). Значение может быть присвоено при объявлении или внутри конструктора класса, после чего изменить его нельзя.
ASP.NET Core содержит встроенный контейнер внедрения зависимостей (IoC контейнер). Поддерживаются три жизненных цикла (Service Lifetimes): * **Transient** (временный): Экземпляр службы создается каждый раз, когда запрашивается из контейнера. * **Scoped** (в рамках запроса): Экземпляр создается один раз на каждый HTTP-запрос. Идеален для работы со службами СУБД (например, `DbContext`). * **Singleton** (одиночка): Экземпляр создается при первом обращении (или при старте приложения) и живет все время работы приложения.
Middleware (компоненты промежуточного ПО) — это блоки кода, выстроенные в конвейер (pipeline) обработки HTTP-запросов и ответов. Каждый компонент может обработать запрос, передать его следующему компоненту в очереди (`next.Invoke()`) или прервать выполнение конвейера (short-circuit).
Маршрутизация сопоставляет HTTP-запросы с методами контроллеров. Бывает двух типов: * **Конвенциональная (Convention-based)**: Маршруты задаются глобальными правилами при старте приложения (редко для Web API). * **Атрибутивная (Attribute Routing)**: Маршруты объявляются непосредственно над контроллерами и их методами с помощью атрибутов `[Route("api/[controller]")]`, `[HttpGet]`, `[HttpPost]`. Это предпочтительный способ для API.
* В старых версиях (до .NET 6) использовался класс `Startup.cs`: * `ConfigureServices`: Регистрация зависимостей в IoC-контейнере. * `Configure`: Настройка конвейера Middleware для обработки запросов. * В современных версиях (.NET 6/8) используется шаблон Minimal APIs в одном файле `Program.cs`. Регистрация служб происходит через `builder.Services.Add...()`, а конвейер Middleware настраивается через вызовы методов объекта `app` (например, `app.UseMiddleware()`).
Kestrel — это кроссплатформенный быстрый веб-сервер для .NET с высокой производительностью, встроенный по умолчанию. Обычно используется как внутренний сервер за обратным прокси-сервером (Nginx, IIS, Apache), который берет на себя функции защиты, балансировки нагрузки и обработки TLS/SSL.
* **Middleware**: Работает на низком уровне конвейера HTTP-запросов. Не имеет доступа к контексту MVC (контроллерам, действиям, привязке моделей). * **Filters (Фильтры)**: Интегрированы в инфраструктуру MVC/API. Выполняются после того, как маршрутизация определила вызываемый контроллер. Имеют доступ к метаданным метода, результату выполнения и привязанным моделям.
`record` — это новый ссылочный тип данных (структуры также поддерживают рекорды с C# 10), предназначенный для хранения неизменяемых данных с поддержкой логики сравнения по значениям (а не по ссылкам). По умолчанию генерирует методы сравнения, хэш-код, метод `ToString()` и поддерживает синтаксис клонирования с изменениями (`with`).
* **`struct`**: Полноценная структура (тип значения), поля которой по умолчанию могут изменяться. * **`readonly struct`**: Структура, компилятор которой гарантирует, что все ее поля объявлены как `readonly` (неизменяемые). Позволяет оптимизировать передачу структуры в функции по ссылке (`in`), гарантируя отсутствие побочных эффектов мутации.
* **`is`**: Проверяет совместимость типов объекта. Возвращает `true`, если объект может быть приведен к указанному типу, иначе `false`. Поддерживает pattern matching (например, `if (obj is string s)`). * **`as`**: Выполняет приведение типов. В случае успеха возвращает объект приведенного типа, в случае неудачи возвращает `nil` (`null`), не генерируя исключение.
В C# деструктор синтаксически записывается как `~MyClass()` и транслируется компилятором в переопределенный метод `Finalize()`. Вызывается сборщиком мусора перед удалением объекта. Вызовы недетерминированы, нагружают GC, поэтому в C# рекомендуется использовать `IDisposable` и `Dispose()`.
Многоадресный (Multicast) делегат — это делегат, который может ссылаться на несколько методов одновременно. При вызове такого делегата все привязанные к нему методы вызываются последовательно. Подписка на методы осуществляется оператором `+=`, отписка — `-=`.
`Nullable<T>` — это generic-структура, позволяющая типам значений принимать состояние `null`. Синтаксический сахар — `int?`. Реализована через поля `HasValue` (логический флаг наличия значения) и `Value` (само сохраненное значение).
`ThreadPool` — это коллекция фоновых рабочих потоков, управляемая рантаймом CLR. Вместо того чтобы тратить дорогие ресурсы на создание и уничтожение потока операционной системы под каждую задачу, задачи ставятся в очередь пула потоков, который распределяет их по уже созданным и ожидающим потокам.
Сборщик мусора .NET делит кучу на три поколения для оптимизации времени сборки: * **Gen 0**: Новые короткоживущие объекты (локальные переменные). Сборка происходит часто и очень быстро. * **Gen 1**: Буферная зона для объектов, переживших сборку в Gen 0. * **Gen 2**: Долгоживущие объекты (синглтоны, статические поля). Сборка происходит редко (Full GC). * **LOH (Large Object Heap)**: Объекты размером > 85,000 байт аллоцируются здесь напрямую. LOH не уплотняется по умолчанию (только очищается), что может вести к фрагментации памяти. * **POH (Pinned Object Heap)**: Введена в .NET 5 для объектов, закрепленных в памяти (pinned), чтобы GC не перемещал их.
* **`Task`**: Ссылочный тип (класс). Каждая асинхронная операция с `Task` выделяет объект в куче, создавая нагрузку на GC. * **`ValueTask`**: Структура (Value Type). Если метод завершается синхронно (например, результат уже закэширован), использование `ValueTask` не выделяет память в куче. * **Рекомендация**: Использовать `ValueTask` для высокопроизводительного кода, где методы часто возвращают результат мгновенно без реального асинхронного ожидания.
Ключевое слово `yield return` преобразуется компилятором в конечный автомат (State Machine). Компилятор генерирует скрытый класс, реализующий интерфейсы `IEnumerable` и `IEnumerator`. Выполнение метода приостанавливается на каждой инструкции `yield return` и возобновляется только тогда, когда вызывающий код вызывает метод `MoveNext()` у итератора. Это позволяет лениво генерировать последовательности без выделения памяти под всю коллекцию.
`SynchronizationContext` — класс, управляющий контекстом выполнения потока. Он возвращает выполнение кода в определенный поток (например, UI-поток в WPF/WinForms при вызове `await`). В ASP.NET Core контекст синхронизации был удален для повышения производительности, поэтому вызовы `ConfigureAwait(false)` в веб-приложениях .NET Core больше не являются строго обязательными, хотя остаются хорошей практикой для библиотек.
`Span<T>` — это `ref struct` (выделяется только в стеке), представляющий непрерывную область памяти (участок массива, стек или нативную память). Он позволяет работать со срезами данных (slices) без копирования памяти и аллокаций в куче. `ReadOnlySpan<T>` используется для безопасной оптимизации работы со строками (метод `AsSpan`), заменяя медленный `Substring`.
* **`Span<T>`**: Является `ref struct`, поэтому его нельзя сохранять в полях обычных классов, передавать в асинхронные методы (через `yield` или `await`) или лямбда-выражения. * **`Memory<T>`**: Обычная структура, не имеющая ограничений `ref struct`. Ее можно хранить в куче и передавать через границы асинхронных вызовов. При необходимости из `Memory<T>` можно в любой момент получить `Span<T>` через свойство `.Span`.
При компиляции методы с `async` преобразуются компилятором в структуру конечного автомата, реализующую интерфейс `IAsyncStateMachine`. Локальные переменные метода переносятся в поля структуры. Компилятор оборачивает вызовы в `AsyncTaskMethodBuilder`. При достижении `await` регистрируется колбэк на продолжение операции, и поток освобождается. После завершения асинхронной задачи стейт-машина возобновляет работу.
* **`IQueryable`**: Запрос строится в виде дерева выражений (Expression Tree). Фильтрация (`Where`) выполняется на сервере базы данных. SQL запрос генерируется в момент материализации. * **`IEnumerable`**: Подходит для коллекций в памяти. При работе с БД запрос материализуется (данные скачиваются в память приложения), а последующая фильтрация происходит в оперативной памяти. * **`IList`**: Представляет собой полностью материализованную коллекцию в памяти с возможностью изменения и доступа по индексу.
Проблема N+1 возникает, когда для связанных сущностей EF Core выполняет отдельные запросы к БД. **Решения**: * Использовать метод `.Include()` (Eager Loading) для загрузки связанных данных в одном SQL-запросе. * Использовать проекции `.Select()` для выбора только нужных полей. * Разрешить Split Queries (`.AsSplitQuery()`) для больших графов сущностей, чтобы избежать раздувания результирующей таблицы из-за JOIN-ов.
По умолчанию EF Core отслеживает все изменения загруженных сущностей в кэше отслеживания (Change Tracker). При вызове `SaveChanges()` измененные данные сохраняются в БД. Метод `AsNoTracking()` отключает это отслеживание. Это снижает потребление оперативной памяти и ускоряет выполнение запросов. Применяется для сценариев «только чтение» (например, отображение списков или отчетов).
Deferred Execution означает, что сам запрос LINQ не выполняется в момент его объявления. Переменная запроса лишь хранит инструкции выполнения. Физический запрос к БД или коллекции происходит только тогда, когда начинается перечисление результатов: в цикле `foreach`, при вызове методов материализации (`ToList()`, `ToArray()`, `Count()`, `First()`).
* **Тип (Value vs Reference)**: `struct` — тип значения (хранится в стеке или внутри содержащего объекта в куче). `class` — ссылочный тип (выделяется в куче, в стеке хранится ссылка). * **Копирование**: При присвоении структуры создается ее полная копия. При присвоении класса копируется только ссылка. * **Наследование**: Классы поддерживают наследование. Структуры не могут наследоваться от других типов (кроме интерфейсов).
* **`readonly struct`**: Структура, все поля которой помечены как `readonly`. Гарантирует неизменяемость (immutability) и позволяет компилятору избегать защитного копирования (defensive copying) при передаче с модификатором `in`. * **`ref struct`**: Структура, которая может находиться только в стеке. Ее нельзя помещать в кучу (нельзя использовать как тип поля в обычных классах, упаковывать в object, использовать в async-методах). Используется для `Span<T>`.
* **Boxing (упаковка)**: Процесс преобразования типа значения (struct) в ссылочный тип (object или интерфейс). Значение копируется из стека в кучу, создавая новый объект. * **Unboxing (распаковка)**: Обратный процесс извлечения типа значения из объекта в куче. * **Проблема**: Упаковка и распаковка снижают производительность из-за выделения памяти в куче и нагружают сборщик мусора.
* **`Delegate`**: Базовый тип для безопасных указателей на методы. * **`Action`**: Предопределенный системный делегат, который принимает параметры (или без них) и не возвращает значение (`void`). * **`Func`**: Системный делегат, который принимает параметры и обязательно возвращает значение (последний генерик-параметр — тип результата). * **`Predicate`**: Системный делегат, принимающий один параметр и возвращающий `bool`.
Паттерн Dispose используется для своевременного освобождения неуправляемых ресурсов (файлы, сокеты, соединения). Стандартный паттерн: ```csharp public class ResourceHolder : IDisposable { private bool _disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // Отключаем вызов деструктора } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // Освобождаем управляемые ресурсы } // Освобождаем неуправляемые ресурсы _disposed = true; } ~ResourceHolder() { Dispose(false); } } ```
Source Generators (генераторы исходного кода) — это фича компилятора Roslyn, позволяющая анализировать пользовательский код во время компиляции и генерировать дополнительные файлы исходного кода на C#, которые компилируются вместе с основным проектом. Это заменяет медленную рантайм-рефлексию на быструю кодогенерацию на этапе сборки.
* **`lock`**: Синтаксический сахар над классом `Monitor`. Блокирует критическую секцию для одного потока. Работает в рамках одного процесса. * **`Monitor`**: Предоставляет больше контроля, чем lock (методы `Pulse`, `Wait` для уведомлений потоков). * **`SemaphoreSlim`**: Легковесный семафор, поддерживающий асинхронное ожидание блокировки с помощью `WaitAsync()`. Это критично в асинхронном коде, так как `lock` нельзя использовать внутри `await` блоков.
`System.Threading.Channels` — библиотека высокопроизводительных очередей для обмена данными между потоками по паттерну Производитель-Потребитель (Producer-Consumer). Работает быстрее и эффективнее, чем `BlockingCollection`, так как полностью поддерживает асинхронность и не блокирует потоки при ожидании новых элементов.
`record` (появились в C# 9) — это ссылочные или значимые типы с упором на неизменяемость и семантику сравнения значений: * **Сравнение по значению**: Два разных объекта `record` с одинаковыми свойствами будут равны по `Equals()` (в отличие от классов, которые сравниваются по ссылке). * **Неизменяемость**: Свойства по умолчанию генерируются с модификатором `init`. * **Конструкция `with`**: Поддерживают легкое клонирование с изменением части свойств (`var newRec = rec with { Age = 30 };`).