Все специализации

C# и .NET разработчик: вопросы на собеседовании

60 вопросов с разбором ответов — те формулировки, которые действительно встречаются на интервью.

Bearer-токен — это стандартный способ аутентификации в HTTP, используемый в OAuth 2.0 и других протоколах. Клиент отправляет токен в заголовке Authorization с префиксом Bearer.

Сервер проверяет валидность токена и, если он действителен, предоставляет доступ к защищенному ресурсу. Токен обычно представляет собой строку, например, JWT (JSON Web Token), содержащую информацию о пользователе и его правах.

Преимущества:

Простота реализации.

Безопасность при использовании вместе с SSL/TLS.

Низкая нагрузка на сервер за счет отсутствия сессий на стороне сервера (статуйность).

Недостатки:

Угроза кражи токена (компрометация данных).

Необходимость реализации механизма отзыва токенов при краже или выходе пользователя.

Применяется в RESTful API, Single Page Applications, мобильных приложениях.

Основное отличие Action и Func заключается в возвращаемом значении:

Action: Делегат, который принимает от 0 до 16 параметров, но не возвращает никакого значения (возвращает void).

Func: Делегат, который принимает от 0 до 16 входных параметров и возвращает значение указанного типа. Тип возвращаемого значения указывается в последнем параметре <TResult>.

Примеры:

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

Основные характеристики и области применения:

Динамическая генерация прокси-объектов: Интерсепторы часто используются совместно с библиотеками для динамической генерации прокси-объектов (например, Castle DynamicProxy, NProxy). Прокси-объект оборачивает целевой объект и перехватывает все обращения к нему.

Аспектно-ориентированное программирование (АОП): Интерсепторы являются одним из ключевых механизмов для реализации АОП, позволяя выделить сквозные аспекты (логирование, кеширование, безопасность, обработка транзакций) и применить их к множеству объектов без изменения исходного кода этих объектов.

Мокирование и тестирование: Используется для создания мок-объектов или стабов в юнит-тестах, позволяя эмулировать поведение реальных зависимостей.

Добавление поведения во время выполнения: Позволяет добавлять или изменять поведение объектов без необходимости модификации их исходного кода или использования наследования.

Пример реализации с использованием Castle DynamicProxy (один из популярных фреймворков):

Определение интерфейса интерсептора:

// Интерфейс для интерсептора

public interface IInterceptor

{

void Intercept(IInvocation invocation);

}

Реализация интерсептора:

// Пример реализации интерсептора для логирования

public class LoggingInterceptor : IInterceptor

{

public void Intercept(IInvocation invocation)

{

Console.WriteLine($"Вызов метода: {invocation.Method.Name}");

try

{

invocation.Proceed(); // Вызов оригинального метода

}

catch (Exception ex)

{

Console.WriteLine($"Ошибка в методе {invocation.Method.Name}: {ex.Message}");

throw;

}

Console.WriteLine($"Метод {invocation.Method.Name} завершен.");

}

}

Применение интерсептора:

// Использование DynamicProxy для создания прокси-объекта с интерсептором

using Castle.DynamicProxy;

// Пример целевого класса

public class TargetClass

{

public virtual void SomeMethod(int value)

{

Console.WriteLine($"Внутри TargetClass.SomeMethod с значением: {value}");

}

}

// Создание прокси-генератора и интерсептора

var generator = new ProxyGenerator();

var interceptor = new LoggingInterceptor();

// Создание прокси-объекта, который будет перехватывать вызовы TargetClass

var proxyObject = generator.CreateClassProxy<TargetClass>(interceptor);

// Вызов метода через прокси-объект

proxyObject.SomeMethod(123);

В данном примере, при вызове proxyObject.SomeMethod(123), управление сначала передается в метод Intercept интерсептора LoggingInterceptor. Интерсептор выполняет логирование до и после вызова оригинального метода с помощью invocation.Proceed().

I/O bound задача тратит большую часть времени на ожидание завершения операций ввода-вывода (сеть, диск, база данных). CPU bound задача тратит большую часть времени на выполнение вычислений процессором.

Примеры:

I/O bound: Чтение большого файла с диска, отправка запроса к внешнему API, работа с базой данных.

CPU bound: Криптографические вычисления, обработка изображений, сложные математические расчеты.

Для I/O bound задач часто используется асинхронное программирование (async/await) для освобождения потока во время ожидания. Для CPU bound задач эффективнее использовать многопоточность (Task.Run, Parallel.ForEach) для распараллеливания вычислений между ядрами процессора.

Нет, нельзя. Блок lock требует синхронного выполнения кода внутри него, чтобы гарантировать эксклюзивный доступ к ресурсу. await вызывает приостановку выполнения и возврат управления вызывающему коду, нарушая монопольный доступ, который обеспечивает lock. Это может привести к неопределенному поведению и взаимоблокировкам (deadlocks).

Для асинхронных операций следует использовать асинхронные примитивы синхронизации, такие как SemaphoreSlim.

Область видимости в C# определяет доступность переменных, методов и типов в определенной части программы. Это ограничение на то, где в коде можно использовать определенный идентификатор.

Основные области видимости:

Блок кода (Block Scope): Переменные, объявленные внутри фигурных скобок { }, доступны только внутри этого блока.

Метод (Method Scope): Переменные и параметры, объявленные внутри метода, доступны только внутри этого метода.

Класс/Структура (Class/Struct Scope): Члены класса или структуры (поля, свойства, методы) доступны в зависимости от модификаторов доступа (public, private, protected, internal).

Пространство имен (Namespace Scope): Типы, объявленные в пространстве имен, доступны в этом пространстве имен и в других пространствах имен, где использовано using.

Сборка (Assembly Scope): Типы с модификатором internal доступны только внутри текущей сборки.

Пример области видимости блока кода:

В .NET, куча (heap) делится на два основных поколения: Поколение 0 и Поколение 1. При необходимости, создается Поколение 2.

Поколение 0 (Gen 0): Вновь созданные объекты размещаются здесь. Это самая молодая и наименьшая куча. Сборщик мусора (GC) чаще всего проверяет и очищает это поколение. Объекты, которые переживают сборку в Gen 0, перемещаются в Gen 1.

Поколение 1 (Gen 1): Содержит объекты, пережившие сборку в Gen 0. GC проверяет это поколение реже, чем Gen 0. Объекты, пережившие сборку в Gen 1, перемещаются в Gen 2.

Поколение 2 (Gen 2): Содержит долгоживущие объекты из Gen 1, а также большие объекты. GC проверяет это поколение реже всего. Сборка в Gen 2 является наиболее затратной по времени.

Large Object Heap (LOH): Специальная область кучи для объектов размером 85 КБ или более. Объекты в LOH не перемещаются GC, так как их перемещение очень дорого. Очистка LOH происходит вместе со сборкой Gen 2.

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

DTO (Data Transfer Object) — это простой объект, предназначенный для передачи данных между подсистемами или процессами. Он не содержит бизнес-логики, только свойства для хранения данных. Используется для уменьшения накладных расходов при удаленных вызовах и улучшения сериализации.

Основные характеристики DTO:

Простая структура, обычно содержащая только публичные свойства.

Отсутствие бизнес-логики.

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

Помогает избежать чрезмерной выборки (over-fetching) или недостаточной выборки (under-fetching) данных при передаче.

Пример DTO:

При сравнении ссылочных типов в C# по умолчанию сравниваются ссылки на объекты в памяти, а не содержимое самих объектов.

Это означает, что object1 == object2 будет истинно только в том случае, если object1 и object2 указывают на один и тот же экземпляр в куче.

Для сравнения содержимого объектов ссылочных типов используются:

Переопределение метода Object.Equals().

Реализация интерфейса IEquatable<T>.

Использование статического метода Object.Equals().

Использование статического метода Object.ReferenceEquals().

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

Основные подходы и шаги:

Анализ существующего метода: Понять его назначение, зависимости, места вызова и влияние на систему.

Определение цели миграции: Рефакторинг, изменение ответственности, перенос в другой класс/модуль.

Создание нового метода (если применимо): С новым именем, сигнатурой или в другом месте.

Перенос логики: Скопировать или переписать код из старого метода в новый.

Обновление мест вызова: Изменить код во всех местах, где вызывался старый метод, чтобы он вызывал новый.

Тестирование: Убедиться, что новое поведение корректно и не ввело регрессий.

Удаление старого метода: После подтверждения работоспособности, удалить старый метод (предварительно можно пометить его атрибутом [Obsolete]).

Примеры сценариев миграции:

Перенос в другой класс: Если метод выполняет логику, которая больше соответствует другому классу.// Old class

public class OriginalClass

{

public void OldMethod()

{

// Some logic

}

}

// New class

public class NewClass

{

public void NewMethod()

{

// Moved logic from OldMethod

}

}

// Usage update

// Before: new OriginalClass().OldMethod();

// After: new NewClass().NewMethod();

Изменение сигнатуры: Добавление, удаление или изменение типов параметров.// Before

public void Process(int data) { /* ... */ }

// After

public void Process(string name, int data) { /* ... */ }

// Usage update

// Before: Process(123);

// After: Process("Example", 123);

Разделение на несколько методов: Если метод стал слишком большим и выполняет несколько несвязанных задач.// Before

public void BigMethod()

{

// Step 1

// Step 2

// Step 3

}

// After

public void MainMethod()

{

Step1();

Step2();

Step3();

}

private void Step1() { /* ... */ }

private void Step2() { /* ... */ }

private void Step3() { /* ... */ }

Инструменты для автоматизации:

Современные IDE, такие как Visual Studio, предоставляют мощные инструменты рефакторинга, которые могут автоматизировать многие шаги миграции, например:

"Rename" (Переименовать)

"Move to another file/class" (Переместить в другой файл/класс)

"Extract method" (Извлечь метод)

"Change signature" (Изменить сигнатуру)ё

Внедрение зависимостей (DI) — это шаблон проектирования для инверсии управления (IoC), который позволяет создавать слабосвязанный код. В ASP.NET Core DI является встроенной функцией, позволяющей легко управлять зависимостями между классами.

Основные компоненты DI в ASP.NET Core:

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

Контейнер DI: Объект, отвечающий за разрешение зависимостей и создание экземпляров сервисов.

Время жизни сервисов: Определяет, когда и как создаются экземпляры сервисов:

Transient: Новый экземпляр создается при каждом запросе сервиса.

Scoped: Один экземпляр создается на каждый запрос (для веб-приложений — на каждый HTTP-запрос).

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

Преимущества использования DI:

Слабая связанность: Классы зависят от абстракций (интерфейсов), а не от конкретных реализаций.

Тестируемость: Легко заменять реальные зависимости на "заглушки" (mocks) или "муляжи" (stubs) при юнит-тестировании.

Расширяемость: Легко заменять реализации сервисов без изменения кода, зависящего от них.

Управляемость зависимостями: Контейнер DI управляет жизненным циклом и созданием экземпляров.

Пример регистрации сервисов в Startup.cs (или Program.cs в .NET 6+):

Slim-версии (SemaphoreSlim) оптимизированы для работы в одном процессе и не поддерживают синхронизацию между процессами или экземплярами AppDomain. Обычные семафоры (Semaphore) являются именованными, что позволяет использовать их для межпроцессной синхронизации.

Основные отличия:

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

Индекс B3 (или B3 Index) — это фондовый индекс Бразильской фондовой биржи BOMF:B3. Он отражает средневзвешенную цену акций крупнейших и наиболее ликвидных компаний, торгующихся на бирже B3. Является основным индикатором состояния бразильского фондового рынка.

Интерфейсы в C# используются для достижения следующих целей:

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

// Определение интерфейса

public interface IDrawable

{

void Draw();

int LineThickness { get; set; }

}

// Класс, реализующий интерфейс

public class Circle : IDrawable

{

public int LineThickness { get; set; }

public void Draw()

{

// Логика рисования круга

}

}

Полиморфизм: Использование интерфейсов позволяет работать с объектами разных типов через общий интерфейсный тип. Это достигается за счет того, что любая ссылка на объект, реализующий интерфейс, может быть приведена к типу этого интерфейса.

// Метод, работающий с любым объектом, реализующим IDrawable

public void Render(IDrawable item)

{

item.Draw(); // Вызывается конкретная реализация метода Draw

}

// Использование:

IDrawable circle = new Circle();

Render(circle); // Будет вызвана реализация Draw() из класса Circle

Multiple Inheritance (множественное наследование): C# не поддерживает множественное наследование классов, но позволяет классу или структуре реализовать несколько интерфейсов. Это позволяет типу иметь поведение, определенное в разных "контрактах".

public interface ISaveable

{

void Save();

}

public interface ILoadable

{

void Load();

}

// Класс, реализующий несколько интерфейсов

public class ConfigurationManager : ISaveable, ILoadable

{

public void Save()

{

// Логика сохранения

}

public void Load()

{

// Логика загрузки

}

}

Слабая связанность (Decoupling): Использование интерфейсов позволяет снизить зависимость между компонентами. Вместо того чтобы зависеть от конкретного класса, компонент может зависеть от интерфейса. Это облегчает замену реализаций и тестирование.

public interface ILogger

{

void LogMessage(string message);

}

public class FileLogger : ILogger

{

public void LogMessage(string message)

{

// Логика записи в файл

}

}

public class DatabaseLogger : ILogger

{

{

// Логика записи в базу данных

}

}

public class ProcessingEngine

{

private readonly ILogger _logger;

// Зависимость от интерфейса, а не от конкретной реализации

public ProcessingEngine(ILogger logger)

{

_logger = logger;

}

public void ProcessData()

{

// ...

_logger.LogMessage("Данные обработаны.");

}

}

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

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

Count - количество фактически содержащихся элементов в коллекции.

Capacity - максимальное количество элементов, которое коллекция может вместить до того, как потребуется перераспределение памяти.

Например, для List<T>:

Зацепление (coupl ing) — это степень связанности между модулями, классами или компонентами программы. Высокое зацепление означает, что одно изменение в одном компоненте, скорее всего, потребует изменений в другом.

Виды зацепления (от сильного к слабому):

Контентное зацепление: Модуль обращается или изменяет внутреннее содержимое другого модуля.

Общее зацепление: Модули используют общие глобальные данные.

Внешнее зацепление: Модули зависят от формата данных, передаваемых извне, или от внешних устройств/интерфейсов.

Управляющее зацепление: Один модуль передает управляющие флаги другому модулю, влияя на его логику.

Штамповое зацепление: Один модуль передает другому структуру данных, но второй модуль использует только часть этой структуры.

Зацепление по данным: Один модуль передает другому только необходимые ему данные.

Зацепление по сообщениям (декларативное): Модули взаимодействуют путем передачи сообщений без знания внутренней структуры друг друга (характерно для событий, веб-сервисов).

Отсутствие зацепления: Модули совершенно независимы.

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

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

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

Основные различия:

Назначение: Clone создает новый массив, а CopyTo заполняет существующий.

Возвращаемое значение: Clone возвращает Object (требуется приведение), CopyTo имеет тип void.

Поверхностная копия: Оба метода выполняют поверхностную копию.

Гибкость: CopyTo позволяет указать начальный индекс в целевом массиве.

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

В C# асинхронное программирование реализуется с помощью ключевых слов async и await. Метод, помеченный как async, может содержать оператор await, который указывает на точку приостановки выполнения до завершения асинхронной операции.

Основные преимущества:

Отзывчивость: UI остается доступным во время выполнения длительных операций.

Масштабируемость: Приложения могут обрабатывать больше параллельных запросов, не используя много потоков.

Эффективность: Меньшее потребление системных ресурсов.

Пример использования:

Ключевые компоненты:

async: Модификатор метода, указывающий, что метод может содержать оператор await.

await: Оператор, используемый внутри async метода, который приостанавливает выполнение метода до завершения ожидаемой задачи (Task или Task<TResult>).

Task: Объект, представляющий асинхронную операцию, которая может быть завершена в будущем.

Task<TResult>: Объект, представляющий асинхронную операцию, которая возвращает результат типа TResult.

Асинхронное программирование в C# основано на модели Task-based Asynchronous Pattern (TAP).

.NET Core - это бесплатная, кроссплатформенная платформа с открытым исходным кодом для создания различных типов приложений, включая:

Веб-приложения и сервисы

Микросервисы

Консольные приложения

Библиотеки

Основные отличия и преимущества по сравнению с .NET Framework:

Кроссплатформенность: Работает на Windows, macOS и Linux.

Открытый исходный код: Разрабатывается и поддерживается сообществом и Microsoft.

Высокая производительность: Оптимизирован для современных рабочих нагрузок.

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

Модульность: Состоит из небольших, независимых NuGet-пакетов.

Архитектура .NET Core базируется на:

CoreCLR (Core Common Language Runtime): Виртуальная машина, отвечающая за выполнение кода.

Библиотеки классов .NET: Набор стандартных библиотек для различных задач.

SDK (Software Development Kit): Инструменты для разработки, сборки и публикации приложений.

Пример простого консольного приложения на .NET Core:

Паттерн Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру.

В C# это обычно реализуется через приватный конструктор и статическое свойство, которое создает и хранит единственный объект:

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

Конструкция using в C# разворачивается в блок try...finally.

Объект, используемый в конструкции using, должен реализовывать интерфейс System.IDisposable. Метод Dispose() вызывается в блоке finally, что гарантирует освобождение неуправляемых ресурсов (например, файловых дескрипторов, соединений с базой данных) даже в случае возникновения исключения.

Разворачивание using не связано с конкретной областью памяти (например, стеком или кучей), а управляет временем жизни и освобождением ресурсов, которые могут быть расположены как в стеке (для структур), так и в куче (для классов).

Dispose вызывается явно разработчиком или средой выполнения (например, using) для освобождения неуправляемых ресурсов. Finalize (через деструктор в C#) вызывается сборщиком мусора при уничтожении объекта, если тот не был освобожден явно.

Пример использования using (автоматический вызов Dispose):

Пример класса с IDisposable и деструктором:

Метод Dispose должен быть реализован для всех классов, управляющих неуправляемыми ресурсами (файлы, сетевые соединения, дескрипторы и т.д.). Finalize следует использовать как запасной вариант освобождения неуправляемых ресурсов в случае, если Dispose не был вызван. Классы, реализующие Finalize, должны подавлять его вызов в Dispose(true), чтобы избежать двойного освобождения ресурсов.

Метод Dispose из интерфейса IDisposable предназначен для явного освобождения неуправляемых ресурсов (файловых дескрипторов, соединений с базами данных, сетевых сокетов и т.д.), используемых объектом.

Гарантирует немедленное освобождение ресурсов, в отличие от сборщика мусора, который работает недетерминированно.

Используется в связке с оператором using для автоматического вызова Dispose при выходе из блока.

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

Применяется для предотвращения утечек ресурсов и обеспечения своевременного их освобождения.

Переопределение (override) используется для реализации полиморфизма, позволяя классу-наследнику предоставить свою реализацию виртуального, абстрактного или унаследованного метода. Ключевое слово virtual в базовом классе и override в наследнике обязательны.

Сокрытие (hiding, shadowing) методом с тем же именем в классе-наследнике метода из базового класса. Метод базового класса сохраняется, но доступ к нему из экземпляра наследника осуществляется через явное приведение типа или ключевое слово base. Используется ключевое слово new в наследнике.

Частичные методы — это средство C# для определения метода в одной части класса, структуры или интерфейса partial и реализации его в другой части.

Особенности:

Объявляются с ключевым словом partial.

Не имеют модификаторов доступа (неявно private).

Не могут возвращать значение, кроме void (до C# 7.0). В C# 7.0 и выше могут возвращать любой тип, но должны иметь реализацию.

Могут иметь параметры ref.

Не могут иметь параметры out.

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

Если объявленный частичный метод не имеет реализации, компилятор удаляет как объявление, так и все места его вызова.

Использование:

Генерация кода: Для разделения кода, генерируемого автоматическими инструментами (например, Entity Framework, Windows Forms Designer), от пользовательской логики. Сгенерированная часть объявляет частичный метод, а разработчик реализует его в другом файле.

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

Пример:

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

Основные особенности:

Потокобезопасность: Все операции, изменяющие состояние словаря (добавление, удаление, обновление), являются атомарными.

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

Отсутствие блокировок при чтении: Операции чтения (GetValueOrDefault, TryGetValue) не блокируют операции записи.

Примеры использования:

Кэширование данных, доступных нескольким потокам.

Счетчики или агрегаторы в многопоточных приложениях.

Реализация пулов объектов.

Пример добавления элемента:

Пример получения элемента:

Пример обновления или добавления элемента:

В WPF Action Executor и Action Executing относятся к механизму выполнения команд:

Action Executor: Объект, который инициирует выполнение команды (например, клик по кнопке).

Action Executing: Событие или метод, который вызывается в ответ на Action Executor. Это место, где содержится логика выполнения команды.

Этот паттерн позволяет разделить интерфейс пользователя от бизнес-логики. Action Executor (например, элемент управления) просто сообщает, что действие должно быть выполнено, а Action Executing (метод в ViewModel или Code-behind) реализует, как оно должно быть выполнено.

Любые типы данных, для которых корректно реализованы методы Equals и GetHashCode.

Value types (структуры): По умолчанию реализуют Equals и GetHashCode на основе полей.

Reference types (классы): По умолчанию используют сравнение ссылок и хэш-код на основе адреса объекта. Для использования в качестве ключа нужно переопределить Equals и GetHashCode так, чтобы они сравнивали по значению.

Immutable types (вроде string): Являются хорошими кандидатами, так как их значение не меняется, что гарантирует постоянство хэш-кода.

АУС-2, или Асинхронное Управление Состоянием 2-й версии, представляет собой набор паттернов и рекомендаций для управления состоянием в асинхронных приложениях на C#. Он направлен на упрощение работы с кодом, содержащим множество асинхронных операций, и обеспечение его предсказуемости и устойчивости. Основные принципы АУС-2 включают:

Четкое разделение ответственностей. Состояние приложения четко отделяется от логики, которая его изменяет.

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

Асинхронность по своей природе. Все операции изменения состояния и обработки внешних воздействий рассматриваются как потенциально асинхронные.

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

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

Типичными компонентами архитектуры, следующей принципам АУС-2, могут быть:

Состояние (State): Объект(ы), представляющий(ие) текущее состояние приложения или его части.

Действие (Action/Command): Объект, описывающий намерение изменить состояние.

Обработчик (Handler/Reducer): Функция или объект, принимающий текущее состояние и действие, и возвращающий новое состояние. Именно в обработчике происходит вся логика изменения состояния.

Преимущества использования АУС-2:

Улучшенная тестируемость. Благодаря четкому разделению и инкапсуляции, логика изменения состояния становится легко тестируемой.

Предсказуемость. Изменения состояния происходят только через определенные механизмы, что делает поведение приложения более понятным и предсказуемым.

Удобство отладки. Легче отследить, как и почему изменилось состояние приложения.

Масштабируемость. Архитектура хорошо масштабируется по мере роста сложности приложения.

Пример простой реализации принципа (без использования полного фреймворка):

АУС-2 — это не конкретная библиотека, а скорее набор принципов, которые могут быть реализованы с использованием различных паттернов (например, Redux-подобные) и библиотек в экосистеме .NET.

Да, использовал. Абстрактные классы необходимы для определения общего базового поведения и структуры для производных классов, когда сам базовый класс не предназначен для создания экземпляров.

Они полезны, когда:

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

Необходимо предотвратить создание экземпляров базового класса.

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

Пример использования:

Отличия ArrayList и List в C# заключаются в следующем:

Типизация:

ArrayList хранит элементы типа object (не типизированная коллекция), что требует приведения типа при извлечении элементов и снижает типобезопасность.

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

Типизация:

Производительность:

Добавление и извлечение элементов в ArrayList может быть медленнее из-за необходимости упаковки (boxing) и распаковки (unboxing) значимых типов.

List<T> обычно быстрее, особенно для значимых типов, так как отсутствует упаковка/распаковка.

Производительность:

Безопасность типов:

ArrayList может содержать элементы разных типов, что может привести к ошибкам во время выполнения.

List<T> обеспечивает безопасность типов на этапе компиляции.

Безопасность типов:

Реализация:

ArrayList находится в пространстве имен System.Collections.

List<T> находится в пространстве имен System.Collections.Generic.

Реализация:

Использование:

ArrayList считается устаревшим и не рекомендуется к использованию в новом коде.

List<T> является предпочтительным выбором для большинства сценариев.

Использование:

Пример использования:

Таблица сравнения:

MVVM (Model-View-ViewModel) — это архитектурный паттерн, используемый при разработке пользовательских интерфейсов, особенно в WPF, Xamarin.Forms и Avalonia. Он разделяет приложение на три взаимосвязанных уровня:

Model: Представляет бизнес-логику и данные приложения. Модель не знает о View и ViewModel.

View: Представляет пользовательский интерфейс. Отображает данные из ViewModel и передает действия пользователя к ViewModel. View не содержит логики работы с данными или принятия решений.

ViewModel: Действует как посредник между Model и View. Предоставляет данные из Model в удобном для View формате и обрабатывает команды от View. ViewModel инкапсулирует логику представления и не имеет прямой ссылки на View.

Ключевые принципы MVVM:

Разделение ответственности (Separation of Concerns): Каждый уровень имеет свою четкую роль.

Data Binding: View связывается с ViewModel через привязки данных, что позволяет View автоматически обновляться при изменении данных в ViewModel и наоборот.

Commands: Действия пользователя во View (например, нажатие кнопки) передаются в ViewModel через команды, что обеспечивает слабую связанность между View и ViewModel.

Observable Collections: Для коллекций данных, которые должны быть обновлены в View при изменении, используются ObservableCollection.

Преимущества MVVM:

Улучшенная тестируемость (ViewModel легко тестируется без UI).

Упрощенная разработка UI (дизайнер может работать над View независимо от логики ViewModel).

Лучшая поддерживаемость кода.

Повторное использование ViewModel между различными View (например, для мобильных и настольных приложений).

Пример структуры:

Отложенное выполнение (Deferred Execution) LINQ-запросов означает, что запрос не выполняется сразу при его объявлении, а только в момент перечисления его результатов. Это достигается за счет возврата объекта IEnumerable<T> или IQueryable<T>, представляющего собой описание запроса.

Немедленное выполнение (Immediate Execution), напротив, происходит сразу при объявлении запроса. Это характерно для методов LINQ, которые возвращают скалярное значение (например, Count(), Sum()) или конкретный тип коллекции (например, ToList(), ToArray()).

Отличия в таблице:

Метод расширения (Extension Method) - это статический метод, который может быть вызван так, как будто он является методом расширяемого типа, хотя фактически он определен вне этого типа.

Основные особенности:

Это статический метод, объявленный в статическом классе.

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

Методы расширения не имеют доступа к приватным и защищенным членам расширяемого типа.

Они позволяют добавлять функциональность к существующим типам без изменения их исходного кода или использования наследования.

Пример создания и использования:

Определение статического класса и статического метода расширения:

public static class StringExtensions // Статический класс

{

public static string ToTitleCase(this string input) // Метод расширения для типа string

{

if (string.IsNullOrEmpty(input))

{

return input;

}

// Простая логика преобразования к Title Case

return System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLower());

}

}

Использование метода расширения:

string myString = "hello world";

string titleCaseString = myString.ToTitleCase(); // Вызов как если бы это был обычный метод string

// titleCaseString теперь содержит "Hello World"

Service Locator - это паттерн проектирования, который предоставляет глобальную точку доступа к сервисам (объектам, выполняющим определенные задачи), не требуя от клиентского кода знать конкретные типы реализаций этих сервисов. Он инкапсулирует процесс получения необходимого сервиса.

Преимущества Service Locator:

Централизованный доступ: Упрощает получение сервисов из любого места приложения.

Гибкость: Позволяет легко менять реализации сервисов без изменения клиентского кода.

Недостатки Service Locator (основная причина критики):

Скрытые зависимости: Клиентский код не объявляет свои зависимости явно, что затрудняет понимание того, какие сервисы необходимы.

Тестирование: Сложно тестировать классы, использующие Service Locator, так как зависимости не могут быть легко подменены (mocked).

Нарушение принципа инверсии зависимостей (DIP): Вместо того, чтобы получать зависимости через конструктор или свойства, класс активно их запрашивает.

Трудности с управлением жизненным циклом: Service Locator часто не умеет управлять жизненным циклом зарегистрированных сервисов (например, создавать singleton'ы или transient объекты).

Пример простой реализации:

Пример использования:

В современной C#-разработке Service Locator часто заменяется использованием Inversion of Control (IoC) контейнеров, которые предоставляют полноценный Dependency Injection (DI), что является более предпочтительным подходом для управления зависимостями и повышения тестируемости кода. IoC-контейнеры позволяют явно объявлять зависимости и автоматически внедрять их, следуя принципам DIP.

Boxing — это преобразование переменной типа значимости (value type) в объектную ссылку (object type) или ссылочный тип, реализованный этим типом значимости (например, interface). Это происходит неявно.

Unboxing — это явное преобразование object type обратно в value type. Происходит явным приведением типа.

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

Принципы работы сборщика мусора:

Отслеживание объектов: Определяет объекты, которые еще доступны из корневых узлов (статические поля, локальные переменные на стеке).

Сборка мусора: Если на объект нет ссылок, сборщик мусора помечает его как подлежащий удалению.

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

Поколения объектов:

Поколение 0: Содержит недавно созданные, короткоживущие объекты. Сборка происходит часто.

Поколение 1: Содержит объекты, пережившие одну сборку поколения 0. Сборка происходит реже.

Поколение 2: Содержит долгоживущие объекты. Сборка происходит редко.

Принудительный вызов GC (используется редко и с осторожностью):

Основной механизм сборки мусора в .NET - помечающий и сжимающий алгоритм.

lock в C# — это синтаксический сахар для использования Monitor.Enter и Monitor.Exit, обернутых в блок try-finally для гарантированного освобождения блокировки. Он предоставляет эксклюзивный доступ к ресурсу для одного потока. По сути, это простейший примитив синхронизации на уровне объекта.

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

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

Основные отличия представлены в таблице:

Индекс — это структура данных (обычно B-дерево), которая хранит небольшую часть данных таблицы и ссылки на соответствующие строки в основной таблице.

Процесс оптимизации:

Быстрый поиск: Вместо полного сканирования всей таблицы (full table scan), система управления базами данных (СУБД) просматривает индекс. Индекс отсортирован и оптимизирован для быстрого поиска по определенным столбцам.

Уменьшение объема читаемых данных: Индекс содержит только проиндексированные столбцы и указатели, что существенно уменьшает объем данных, которые нужно прочитать с диска по сравнению с чтением всей строки или всей страницы данных.

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

Оптимизация JOIN-операций: При соединении таблиц (JOIN) по проиндексированным столбцам, СУБД может использовать индекс для быстрого нахождения соответствующих строк в другой таблице, вместо выполнения вложенных циклов по всей таблице.

Пример:

Без индекса по столбцу LastName для запроса SELECT * FROM Users WHERE LastName = 'Иванов', СУБД пришлось бы прочитать каждую строку в таблице Users и проверить значение LastName.

С индексом по LastName, СУБД быстро находит записи, где LastName равен 'Иванов', просматривая отсортированный индекс, а затем использует указатели из индекса, чтобы получить полные данные строк из основной таблицы.

Использую наследование умеренно. Оно полезно для реализации принципа Don't Repeat Yourself (DRY) и создания иерархий типов, но чрезмерное использование может привести к жестким связям и проблемам с тестированием. Предпочитаю композицию или интерфейсы, когда это возможно.

Примеры использования наследования:

Реализация шаблонного метода.

Создание базовых классов для обработки общих логических блоков, например, в UI-компонентах или обработчиках команд.

Избегаю наследования, когда это создает "алмаз" проблем или когда подкласс не является истинным "является" отношением к базовому классу (Liskov Substitution Principle). Вместо этого использую интерфейсы для определения контрактов и композицию для сборки функциональности из более мелких, независимых объектов.

Разделение куч LOH и SOH в .NET оптимизирует управление памятью для объектов разного размера.

Small Object Heap (SOH)

Хранит объекты размером до 85 КБ.

Использует генерационный сборщик мусора (Generational Garbage Collection - GC).

Объекты, созданные недавно, попадают в поколение 0. Выжившие объекты перемещаются в поколение 1, затем в поколение 2.

Сборка мусора происходит чаще и эффективнее для небольших объектов.

Large Object Heap (LOH)

Хранит объекты размером 85 КБ и более.

Не является генерационным в том же смысле, что SOH. Обычно очищается только при полной сборке мусора (поколение 2).

Выделение памяти на LOH может быть медленнее из-за поиска непрерывного блока памяти.

Фрагментация LOH может привести к проблемам с выделением больших объектов, даже если суммарно свободной памяти достаточно.

Объекты на LOH не перемещаются GC по умолчанию (с .NET Framework 4.5.1 появилась возможность сжимать LOH, но это отключено по умолчанию и имеет свои затраты). Это предотвращает дорогостоящее копирование больших объемов данных.

Преимущества разделения

Оптимизация сборки мусора: Генерационный GC на SOH гораздо эффективнее для небольших, короткоживущих объектов. Перемещение объектов на SOH относительно быстро.

Снижение фрагментации SOH: Перемещение больших объектов было бы крайне затратным и приводило бы к сильной фрагментации SOH. Размещение их на отдельной куче LOH решает эту проблему.

Минимизация накладных расходов: Перемещение больших объектов на LOH привело бы к значительным накладным расходам. Неперемещение их GC (по умолчанию) избавляет от этого.

Улучшение производительности выделения памяти: Выделение на SOH часто происходит очень быстро (с помощью указателя), в то время как на LOH может потребоваться поиск подходящего свободного блока. Разделение позволяет сохранить высокую скорость выделения для большинства (малых) объектов.

var используется для неявной типизации локальных переменных. Компилятор определяет тип переменной во время компиляции. После определения тип переменной становится фиксированным и не может быть изменен.

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

Сравнение:

Разница в сохранении стека вызовов исключения.

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

throw повторно выбрасывает то же самое исключение, сохраняя весь исходный стек вызовов. Это предпочтительный способ обработки исключений для логирования или дополнительной обработки без потери контекста ошибки.

Потоки (Thread) представляют собой низкоуровневую абстракцию операционной системы для выполнения кода параллельно. Задачи (Task) — более высокоуровневая концепция в .NET, основанная на пуле потоков, упрощающая асинхронное и параллельное программирование.

Основные отличия:

Уровень абстракции: Thread напрямую работает с потоками ОС. Task — это абстракция, управляемая планировщиком (обычно ThreadPool).

Управление: Управление потоками вручную (создание, запуск, остановка) может быть сложным. Task управляется планировщиком, что упрощает их использование.

Ресурсы: Создание нового Thread требует больше системных ресурсов, чем использование Task из пула потоков.

Возвращаемые значения и исключения: Task имеет встроенные механизмы для получения возвращаемых значений (Task<TResult>) и обработки необработанных исключений. Для Thread это требует ручной реализации.

Асинхронность: Task тесно интегрирован с ключевыми словами async и await, что значительно упрощает написание асинхронного кода. Thread для асинхронности требует ручного использования колбэков или других механизмов.

Отмена: Отмена Task поддерживается через CancellationToken. Отмена Thread требует ручной реализации механизмов прерывания.

Пример создания и ожидания:

Связь "один к одному" в реляционной базе данных реализуется с помощью уникального ограничения и внешнего ключа. Наиболее распространенные способы:

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

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

Пример с общим первичным ключом (в SQL Server):

Пример с уникальным внешним ключом (в SQL Server):

Выбор между подходами зависит от семантики данных и удобства доступа. Подключение по общему первичному ключу часто используется, когда данные второй таблицы являются необязательным расширением первой. Уникальный внешний ключ более гибок, если первичные ключи таблиц не совпадают изначально.

В контексте структур данных, "куча" (heap) чаще всего относится к бинарной куче. Однако, существуют и другие разновидности:

Бинарная куча (Binary Heap): Наиболее распространенный тип, основанный на бинарном дереве. Удовлетворяет свойству кучи: для максимальной кучи родительский узел всегда больше или равен своим потомкам, для минимальной кучи - меньше или равен. Обычно реализуется на базе массива.

Фибоначчиева куча (Fibonacci Heap): Более сложная структура, использующая деревья и представляющая собой набор деревьев, удовлетворяющих свойству минимальной кучи на корневом уровне. Обеспечивает более быстрые амортизированные временные показатели для некоторых операций (например, decrease key).

Биномиальная куча (Binomial Heap): Представляет собой набор биномиальных деревьев. Также поддерживает эффективное слияние куч.

Двоичная купа (Binary Heap - в русскоязычной литературе иногда так называют): Синоним бинарной кучи.

Куча-k (d-ary heap): Обобщение бинарной кучи, где каждый узел имеет до k потомков.

Сопряженная куча (Pairing Heap): Реализует операции вставки и объединения эффективно, часто используется для алгоритмов слияния.

Наиболее важна и часто используется бинарная куча.

Трекинг (Entity Tracking) — это механизм в Entity Framework Core, который отслеживает изменения, внесенные в объекты сущностей, загруженные из базы данных. Context хранит информацию о состоянии каждой загруженной сущности и может генерировать SQL-запросы для соответствующего обновления базы данных при сохранении изменений (DbContext.SaveChanges()).

Основные состояния сущностей:

Added: Сущность была добавлена в контекст, но еще не существует в базе данных.

Unchanged: Сущность была загружена из базы данных, и ее свойства не были изменены.

Modified: Некоторые свойства сущности были изменены с момента ее загрузки из базы данных.

Deleted: Сущность была помечена для удаления из базы данных.

Detached: Сущность не отслеживается контекстом.

При работе с LINQ-запросами по умолчанию сущности отслеживаются. Это позволяет Entity Framework автоматически определять, какие изменения нужно сохранить.

Пример явного указания отслеживания:

Отключение трекинга с помощью AsNoTracking() может повысить производительность при выполнении запросов, если вам не нужно сохранять изменения в базе данных, так как EF не тратит ресурсы на отслеживание состояния сущностей.

Примитивы синхронизации в C# используются для управления доступом к общим ресурсам между несколькими потоками. Они предотвращают состояния гонки и обеспечивают потокобезопасность.

Основные цели:

Взаимное исключение (Mutual exclusion): Гарантирует, что только один поток одновременно может получить доступ к критической секции кода.

Синхронизация потоков (Thread synchronization): Позволяет потокам координировать свои действия, например, ждать завершения другого потока или уведомлять о готовности данных.

Примеры примитивов синхронизации:

lock (Monitor)

Mutex

Semaphore / SemaphoreSlim

AutoResetEvent / ManualResetEvent

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

В C# есть несколько категорий коллекций:

Необобщенные (Non-generic): Работают с типом object и требуют приведения типов.

ArrayList - динамический массив.

Hashtable - коллекция пар ключ-значение на основе хеш-таблицы.

Stack - стековая коллекция (LIFO).

Queue - очередная коллекция (FIFO).

SortedList - коллекция пар ключ-значение, отсортированная по ключу.

Обобщенные (Generic): Предоставляют типобезопасность и лучшую производительность.

List<T> - типобезопасный динамический массив.

Dictionary<TKey, TValue> - типобезопасная коллекция пар ключ-значение, основанная на хеш-таблице.

Stack<T> - типобезопасная стековая коллекция (LIFO).

Queue<T> - типобезопасная очередная коллекция (FIFO).

HashSet<T> - коллекция уникальных элементов без определенного порядка.

SortedList<TKey, TValue> - типобезопасная коллекция пар ключ-значение, отсортированная по ключу.

SortedDictionary<TKey, TValue> - типобезопасная коллекция пар ключ-значение, отсортированная по ключу в виде бинарного дерева поиска.

LinkedList<T> - двусвязный список.

Потокобезопасные (Thread-safe): Для использования в многопоточной среде.

ConcurrentBag<T> - неупорядоченная коллекция элементов, потокобезопасная.

ConcurrentDictionary<TKey, TValue> - потокобезопасный словарь.

ConcurrentQueue<T> - потокобезопасная очередь (FIFO).

ConcurrentStack<T> - потокобезопасный стек (LIFO).

BlockingCollection<T> - реализует блокирующие операции добавления и извлечения из коллекции.

Специализированные:

ObservableCollection<T> - коллекция, которая уведомляет об изменениях (для привязки данных).

BitArray - компактное представление битов.

NameValueCollection - коллекция пар ключ-значение, ключи не являются уникальными.

Специализированные:

И большинство из них реализуют различные интерфейсы, такие как IEnumerable, ICollection, IList, IDictionary.

Пример использования List<T>:

Пример использования Dictionary<TKey, TValue>:

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

Пример:

Без virtual и override вызов метода был бы статическим и не учитывал бы тип объекта во время выполнения.

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

Основные цели:

Детерминированное освобождение ресурсов: Позволяет явно освободить неуправляемые ресурсы немедленно, а не ждать неопределенного момента, когда сборщик мусора вызовет финализатор (если он есть).

Предотвращение утечек ресурсов: Гарантирует, что критические ресурсы будут освобождены после использования, даже если происходят исключения.

Использование совместно с ключевым словом using: Конструкция using автоматически вызывает метод Dispose() объекта, реализующего IDisposable, по окончании блока кода, обеспечивая надежное освобождение.

Реализация включает метод Dispose() без параметров, который выполняет освобождение ресурсов. Часто паттерн включает финализатор (деструктор) для освобождения ресурсов в случае, если Dispose() не был вызван явно, но рекомендуется избегать финализаторов, полагаясь на using.

В контексте C#, ключевое слово transient не является частью синтаксиса языка. Вероятно, имеется в виду концепция Transient Lifetime в системах управления зависимостями (DI-контейнерах).

При Transient Lifetime каждый раз, когда запрашивается экземпляр сервиса, DI-контейнер создает новый экземпляр этого класса.

Особенности Transient Lifetime:

Новый экземпляр при каждом запросе: Изолирует использование сервиса, предотвращая нежелательные побочные эффекты между различными потребителями.

Высокие накладные расходы: Создание и уничтожение объекта происходит чаще.

Подходит для легковесных, stateless сервисов: Где нет необходимости сохранять состояние между вызовами.

Пример регистрации Transient сервиса в .NET Core/5+:

Сравнение Lifetime Scopes (Transient, Scoped, Singleton):

Самый простой и распространённый способ — использование публичного статического свойства с ленивой инициализацией.

Другой вариант — с использованием статического конструктора и Eager Initialization:

С использованием Lazy<T> для ленивой и потокобезопасной инициализации:

Ссылочные типы хранят адрес в памяти, где лежат данные. Значимые типы хранят сами данные непосредственно в переменной.

Ссылочные типы:

Наследуются от System.Object.

Размещаются в куче (heap).

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

По умолчанию инициализируются null.

Включают классы (class), интерфейсы (interface), делегаты (delegate), массивы (array), строки (string).

Значимые типы:

Наследуются от System.ValueType (который, в свою очередь, наследуется от System.Object).

Размещаются в стеке (stack) или внутри объекта, если являются полем ссылочного типа.

При присваивании переменной значимого типа другой переменной, копируются сами данные.

По умолчанию инициализируются нулевым значением (0 для чисел, false для bool, пустое значение для struct).

Включают структуры (struct), перечисления (enum), встроенные типы данных (int, float, bool и т.д.).

Пример:

Жизненный цикл запроса в ASP.NET Core включает следующие стадии:

Прием запроса: Запрос поступает на веб-сервер (например, Kestrel, IIS).

Создание HttpContext: Web-сервер создает объект HttpContext для представления текущего запроса и ответа.

Middleware Pipeline: HttpContext проходит через конвейер промежуточного ПО (Middleware). Каждое промежуточное ПО может обрабатывать запрос, изменять его или передавать следующему в конвейере. Порядок промежуточного ПО имеет значение.

Endpoint Routing: Система маршрутизации определяет конечную точку (например, метод контроллера, Razor Page) на основе URL запроса.

Endpoint Execution: Выполняется конечная точка. Контроллер или Razor Page обрабатывает запрос, выполняет бизнес-логику и формирует результат.

Result Execution: Результат (например, View, JSON, файл) обрабатывается и записывается в ответ.

Отправка ответа: Сформированный ответ отправляется обратно клиенту.

Ключевые компоненты, реализующие эти этапы:

Web Server: Принимает запросы (Kestrel, IIS, Nginx, Apache).

Middleware: Компоненты, формирующие конвейер обработки запросов (например, Static Files Middleware, Authentication Middleware, Routing Middleware).

Routing Middleware: Определяет, какая конечная точка будет обрабатывать запрос.

Endpoint Execution: Запуск кода, связанного с выбранной конечной точкой (например, метод контроллера, обработчик Razor Page).

Result Execution: Обработка результата, возвращенного конечной точкой.

Архитектура промежуточного ПО (Middleware) является центральной концепцией жизненного цикла запроса в ASP.NET Core. Каждое промежуточное ПО представляет собой класс с методом Invoke или InvokeAsync, который принимает HttpContext и делегат RequestDelegate (ссылку на следующее промежуточное ПО в конвейере).

В рамках экосистемы Entity Framework Core существуют три основных approaches (подхода) для работы со схемой базы данных:

Database First: Схема базы данных уже существует, и EF Core генерирует классы моделей (entities) и DbContext на основе этой схемы.

Преимущества: Быстрый старт при наличии готовой базы данных, схема полностью контролируется базой.

Недостатки: Изменения в базе данных требуют повторной генерации кода, что может приводить к перезаписи кастомизированного кода.

// Пример команды для генерации из существующей БД

// Scaffold-DbContext "Server=(localdb)\\mssqllocaldb;Database=MyDatabase;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

Code First: Вы определяете модели (entities) и DbContext в коде на C#. EF Core создает или обновляет схему базы данных на основе этих определений с помощью миграций.

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

Недостатки: Требует управления миграциями, изменения в схеме всегда начинаются с изменений в коде.

// Пример класса модели в подходе Code First

public class Product

{

public int Id { get; set; }

public string Name { get; set; }

public decimal Price { get; set; }

}

Model First: Этот подход, хотя и исторически присутствовал в более старых версиях EF (пре-Core), в EF Core не является нативным и самостоятельным подходом в том же смысле, как Database First или Code First. По сути, Model First в EF Core сводится к написанию моделей в коде (как в Code First) и последующей генерации базы данных на их основе. Нет отдельного визуального дизайнера моделей, как было в EF 6. Поэтому, в контексте EF Core, его часто рассматривают как частный случай Code First, где вы вручную описываете модель без использования готовой базы данных.

Основные характеристики подходов:

Выбор подхода зависит от начальных условий проекта и предпочтений команды. Code First является наиболее распространенным подходом в новых проектах на EF Core благодаря гибкости и интеграции с процессом разработки.

Маршрутизация в ASP.NET сопоставляет входящие HTTP-запросы с исполняемым кодом (обработчиками, например, методами контроллеров или Razor Pages). Она определяет, какой код должен быть выполнен в ответ на конкретный URL.

Основные компоненты и концепции:

Шаблон маршрута (Route Template): Определяет структуру URL, которую маршрутизатор должен сопоставить. Может включать литералы и параметры.

// Пример шаблона маршрута

"{controller=Home}/{action=Index}/{id?}"

Литералы (Home, Index) - статические части URL.

Параметры (controller, action, id) - переменные части URL, заключенные в фигурные скобки.

Опциональные параметры (id?) - могут присутствовать или отсутствовать в URL.

Ограничения маршрута (Route Constraints): Определяют правила для значений параметров маршрута (например, тип данных, регулярное выражение).

// Пример ограничения на параметр id

"{controller}/{action}/{id:int}" // id должен быть целым числом

Значения по умолчанию (Default Values): Присваивают значения параметрам маршрута, если они не указаны в URL.

Порядок маршрутов (Route Order): Маршрутизатор проверяет маршруты в порядке их добавления. Первый совпавший маршрут обрабатывает запрос. Более специфичные маршруты должны быть определены перед более общими.

Генерация URL (URL Generation): Маршрутизация также используется для создания URL-адресов на основе шаблонов маршрутов и предоставленных значений параметров.

В ASP.NET Core маршрутизация конфигурируется в методе Configure класса Startup (или в Program.cs).

Пример конфигурации маршрутизации:

Как происходит сопоставление:

Входящий HTTP-запрос поступает в конвейер обработки запросов ASP.NET Core.

Компонент UseRouting() добавляет middleware маршрутизации в конвейер.

Middleware маршрутизации анализирует URL запроса.

Он перебирает зарегистрированные маршруты по порядку.

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

Найденный обработчик (Endpoint) сохраняется в контексте запроса.

Компонент UseEndpoints() выполняет Endpoint, найденный маршрутизатором. Для MVC это приводит к выполнению соответствующего метода действия контроллера.

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

Сравнение строк в C# может происходить несколькими способами:

Оператор ==: Для строковых литералов и если строки имеют одинаковое содержимое, оператор == вернет true. В отличие от других ссылочных типов, для строк оператор == перегружен для сравнения содержимого, а не ссылок.

Метод Equals(): Этот метод также сравнивает содержимое строк по умолчанию. Он рекомендован для явного сравнения содержимого и может быть более надежным, особенно при сравнении с null. Есть перегрузки для сравнения с учетом регистра и культуры.

Метод String.Compare(): Статический метод, который сравнивает две строки и возвращает целочисленное значение, указывающее на отношение порядка между строками. Позволяет указать различные параметры сравнения (регистр, культура и др.).

Метод String.CompareOrdinal(): Сравнивает строки побайтово, без учета культурных особенностей.

Метод String.CompareTo(): Метод экземпляра, который сравнивает текущую строку с другой строкой или объектом. Аналогичен String.Compare(), но вызывается на объекте строки.

Ссылки объектов строк не сравниваются напрямую оператором == или методом Equals() (без пользовательской перегрузки) при обычном сравнении содержимого. Хотя строки являются ссылочными типами, для удобства работы с ними оператор == и метод Equals() специально перегружены для сравнения именно содержимого.

Упаковка (boxing) — это неявное преобразование типа значения в ссылочный тип object или любой другой ссылочный тип, производный от object. При этом значение помещается в новую область памяти в управляемой куче.

Распаковка (unboxing) — это явное преобразование из типа object в тип значения. Сначала происходит проверка, является ли объект экземпляром нужного типа значения, затем из кучи копируется его поле данных.

Примеры:

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

Пример с обобщениями:

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

FirstOrDefault() возвращает первый элемент последовательности, удовлетворяющий условию, или значение по умолчанию (null для ссылочных типов, 0 для числовых типов), если таких элементов нет.

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

А если вопрос прозвучит не так, как вы готовились?

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

Посмотреть ИзиСобес