вторник, 19 мая 2026 г.

Поговорим немного о sealed

    На прошлой работе мы с коллегами спорили о необходимости использования ключевого слова sealed для закрытия/запечатывания классов.


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

public static partial class FooUseCase
{
   public record Command(long ExecutionId) : IUseCaseCommand<Result>;
}
 
// ...
 
public static partial class FooUseCase
{
   public class Handler : IUseCaseCommandHandler<Command>
   { }
}

     Модификатор sealed уже давно существует в языке C#, но в современных версиях платформы .NET он получил дополнительный вес за счёт оптимизаций JIT.

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

public sealed class AccountService
{
   private readonly IDbConnection _connection;  
}
 
// Ошибка компиляции: нельзя наследоваться от sealed-типа
public class DirectAccountService : AccountService
{ }

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

     А теперь о самом интересном: почему это влияет на производительность?

     Когда класс открыт для наследования, JIT не может точно знать, какой именно метод будет вызван в рантайме. Поэтому он генерит косвенный вызов через vtable

; Открытый класс — несколько обращений к памяти перед вызовом
mov eax, [edx]
mov eax, [eax+0x28]
call dword ptr [eax+0x10]

     А вот уже для sealed-класса JIT точно знает, какой метод будет вызван. Он убирает обращение к vtable, встраивает тело метода прямо в место вызова и оставляет минимум сгенерированных инструкций:

; Sealed-класс — только проверка на null и возврат
cmp [edx], dl
ret

     Дополнительно ускоряются (из-за дальнейшей ненадобности прохождения по всему дереву наследования):
1) операторы is и as
2) поиск референсов на класс в IDE

     Но есть и кейсы, когда ключевое слово sealed перестает быть волшебной палочкой по улучшению производительности:
1) если вы юзаете ORM-ку, которая не сможет расширить прокси-классы в рантайме от сущности;
2) аналогичная проблема случится в либах при мокировании. Особенно если будете пробовать мокать не интерфейсы, а сами sealed-классы (чего делать крайне не рекомендую).

     Многие архитекторы и эксперты по производительности рекомендуют такой подход. В официальной документации Microsoft также есть анализатор, который советует запечатывать классы. В сообществе github ведутся дискуссии о добавлении в язык C# возможности запечатывания классов по умолчанию (sealed-by-default), многие участники высказываются в поддержку этой идеи. Также у Microsoft есть правило анализа кода CA1852: Seal internal types.

     Считаю, что в 99% случаев модификатор sealed защищает дизайн от случайных расширений и даёт прирост производительности без дополнительных вложений.

Ссылки на источники:

воскресенье, 20 июля 2025 г.

var или не var? Да будет холивар!

      Когда работал в прошлой компании, один из разработчиков ну наотрез отказывался при объявлении переменных от ключевого слова var, предпочитая явно указывать тип. Естественно, не наш метод сразу начинать ругаться, бить горшки, писать многострочные гневные или уничижительные комментарии в merge-реквестах (про грамотный процесс code review есть несколько статей на хабре, крайне рекомандую ознакомиться на досуге). Но мы немного отвлеклись от основной темы статьи. Так как используем var? Постараемся закрыть этот вопрос раз и навсегда!
     С появлением ключевого слова var появилась возможность уменьшить количество шаблонного кода и писать более компактные выражения. Напомню, что это произошло достаточно давно, аж в 2007 году и ещё в C# версии 3.0.
     Вместо явного написания громоздких типов можно писать лаконичный код:

// Вместо List<Dictionary<string, List<Tuple<int, DateTime>>>> data = GetResultData();
var data = GetResultData(); 
     
    Также var не делает переменную динамической, это всё тот же строго типизируемый код.
    Но есть и сильный аргумент против использования var. На примере кода выше разработчику, проводящему ревью, совершенно неочевидно, какой тип имеет переменная data. int? double? string? CustomInfo? Тип, получаемый из функции неочевиден и это в разы ухудшает читаемость кода.
    var — полезный инструмент, но требует дисциплины. При очевидном типе он упрощает код, при неочевидном — мешает пониманию.
    Как пример отличного применения var:

var rows = new List<string>();
    
    А вы сторонник понятного использования var? Пишите комменты, рад буду дискуссиям.
    

среда, 2 апреля 2025 г.

Задача на удаление дубликатов, на размышление даётся 30 секунд

    Не так давно проходил собеседование в одну компанию, где одной из задач было удаление дубликатов элементов отсортированного списка. Наверняка вы сейчас скажете "Да это ж легкотня" или "Да я такое 100 раз делал!". И будете правы. Но нюансы есть всегда.

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

    Но вернемся к задаче. Итак, нам дан такой метод:

private static void RemoveDuplicates(ref List<int> items)
{
   // ...
}

  Способ 1. Давайте заюзаем SortedSet, положим туда все элементы списка и его вернем из метода? Звучит логично, но тут я наталкиваюсь на 1-е ограничение по задаче: сигнатуру метода менять нельзя. В таком случае надо заново выделять память под другой список (с уже удаленными после SortedSet-a элементами).

private static void RemoveDuplicates(ref List<int> items)
{
    var temp = new SortedSet<int>();
    foreach (var item in items)
    {
        temp.Add(item);
    }
    
    items = temp.ToList();       
}

  И тут получаю 2-е ограничение: элементов может быть миллион. А теперь представим, сколько аллокаций памяти в куче будет при таком весьма бешенном размещении элементов коллекций. Т.к. там будет располагаться исходный массив, множество элементов SortedSet, а также результирующий список. Звучит не очень, ищем способ получше.

  Способ 2. В цикле за O(n) проходимся по исходному списку и просто удаляем элемент, если он равен предыдущему. Вроде бы просто как дважды два, реализуем:
 
private static void RemoveDuplicates(ref List<int> items)
{
    for (int i = 1; i < items.Count; i++)
    {
        if (items[i] == items[i-1])
        {
            items.RemoveAt(i);
            i--;
        }
    }        
}

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

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

    Способ 4. А теперь разберем корректный солюшн. Обойдёмся без вспомогательных коллекций и удаления элементов списка в цикле. Для этого нам нужно запастись терпением двумя вспомогательными индексами. 1-й из них будет предназначен для чтения, 2-й - для записи. И с самого начала у нас writeIndex будет "отставать" на 1 позицию от readIndex. И теперь, проходя по циклу и встречая первый каждый новый уникальный элемент, мы будем записывать его на новое место, предварительно увеличив writeIndex. Ну и конечно надо не забыть удалить лишние элементы из массива.
    

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

private static void RemoveDuplicates(ref List<int> items)
{
    var writeIndex = 0;
    for (int readIndex = 1; readIndex < items.Count; readIndex++)
    {
        if (items[writeIndex] != items[readIndex])
        {
            items[++writeIndex] = items[readIndex];
        }
    }

    items.RemoveRange(writeIndex + 1, items.Count - writeIndex - 1);
}

    Чем же хорош данный способ? Он работает строго за O(n) операций, нет дополнительных выделений памяти и операция удаления у нас только одна.

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

понедельник, 20 января 2025 г.

Опасность с FirstOrDefault надвигается, чувствую я

    Привет, дорогой мой читатель. Наверняка, если вы часто пишете на .net, то очень часто сталкивались с методом FirstOrDefault из пространства имён System.Linq.


    Метод выполняет очевидную задачу, однако есть у него не самая очевидная проблема. Метод FirstOrDefault в LINQ может стать источником путаницы. Он возвращает первый элемент последовательности или значение по умолчанию для типа, если элементов нет. Для ссылочных типов и Nullable<T> это null, а для значимых типов (например, short, int, double и т.д.) — это их дефолтное значение (0, false и т.д.).
    Например, вызов FirstOrDefault для пустого списка List<short> вернёт 0, что может быть неожиданно, если 0 имеет смысловое значение в вашем коде.

var numbers = new List<short> { };
var result = numbers.FirstOrDefault();
Console.WriteLine(result); // Программа выведет: 0

    Чтобы избежать такой ошибки, рекомендуется явно проверять коллекцию на присутствие элементов перед вызовом метода или использовать DefaultIfEmpty, чтобы задать своё значение по умолчанию. Например:

var result = numbers.DefaultIfEmpty(null).FirstOrDefault();

    Таким образом мы можем гарантировать, что даже для пустой коллекции результат будет null, а не дефолтное значение используемого типа. Такой подход делает код понятным и избавляет от скрытых проблем с обработкой "пустых" результатов. Также метод DefaultIfEmpty можно и нужно применять в Linq-выражениях запросов к базе данных, когда требуется связать 2 таблицы левым соединением.

Источники:
1) https://learn.microsoft.com/ru-ru/dotnet/api/system.linq.enumerable.firstordefault?view=net-9.0

вторник, 7 января 2025 г.

Да кто такой этот ваш ValueTask?!

   Всем привет! Ну вот и подходят к концу новогодние праздники. Решил потихоньку приподнять лицо из оливье и попробовать, попытаться объяснить описать возможности класса ValueTask (оставляю ссылку на официальную документацию).

    Но давайте сначала разберёмся с Task в C#
  
    В .NET Framework 4 появилось пространство System.Threading.Tasks, а с ним и класс Task. Этот тип и порождённый от него Task<TResult> долго дожидались, пока их признают стандартами в .NET в роли ключевых аспектов модели асинхронного программирования, которая была представлена в C# 5 с его операторами async/await. 
    Task является очень гибким классом и это даёт ряд преимуществ. Например, можно выполнить await несколько раз для любого количества потребителей одновременно. А можно положить его в коллекцию (dictionary) для повторных await в будущем, чтобы использовать его как кеш результатов асинхронных вызовов. Также можно заблокировать выполнение, ожидая завершения Task, если такое понадобится. И также можно написать и применить разнообразные операции над объектами Task (иногда их называют «комбинаторами»), например, «когда любая» (“when any”) для асинхронного ожидания первого завершения из нескольких Task.
     Но эта гибкость становится лишней в наиболее часто встречающемся случае: просто вызвать асинхронную операцию и дождаться выполнения задачи:

TResult result = await SomeOperationAsync();
UseResult(result);

     Здесь не понадобится многократного ожидания выполнения участка кода. Нет необходимости обеспечить конкуррентность ожиданий и выполнения синхронной блокировки или написания комбинаторов. Мы просто ждём выполнения promise асинхронной операции. В конце концов, это так, как мы пишем синхронный код (например, TResult result = SomeOperation();), и это обычным образом переводится на язык async/await.
     Более того, у Task есть потенциальная слабая сторона, особенно когда создаётся большое количество его экземпляров, а большая пропускная способность и производительность являются ключевыми требованиями – Task является классом. Это означает, что любая операция, которой понадобился Task, вынуждена создавать и размещать объект в хипе, а чем больше объектов создаётся, тем больше работы для сборщика мусора (GC), и на эту работу расходуются ресурсы, которые мы могли бы потратить на что-то более полезное.

    А при чём тут ValueTask?
    Всё это потребовало реализации в .NET Core 2.0 нового типа, который доступен в предыдущих версиях .NET в пакете NuGet System.Threading.Tasks.Extensions: ValueTask<TResult>. Он был создан в .NET Core 2.0 как структура, способная обернуть как TResult, так и Task<TResult>. Это означает, что её можно возвращать из async метода, и, если этот метод выполнится синхронно и успешно, никакого объекта в куче размещать не надо: вы можете просто инициализировать эту структуру ValueTask<TResult> значением TResult и вернуть. Только в случае асинхронного выполнения объект Task<TResult> будет размещён, а ValueTask<TResult> обернёт его (чтобы минимизировать размер структуры и оптимизировать случай успешного исполнения, async метод, который завершается с неподдерживаемым исключением, также будет размещать Task<TResult>, так что ValueTask<TResult> так же просто обернёт Task<TResult>, а не будет таскать с собой дополнительное поле для хранения Exception).

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

Итого, можно следующим образом констатировать особенности использования ValueTask:

1️⃣Повторное ожидание запрещено: вызывать await несколько раз для одного и того же ValueTask нельзя, так как это может привести к неожиданным результатам.
❌ Так делать нельзя:
ValueTask<int> task = GetValueAsync();
int value1 = await task;
int value2 = await task; // Тут будет ошибка

2️⃣Конвертация в Task: если требуется передать ValueTask в API, которое ожидает Task, можно вызвать метод .AsTask():
Task<int> task = GetValueAsync().AsTask();

3️⃣Когда использовать ValueTask:
✅Операция часто завершается синхронно.
✅Создание объекта Task может быть слишком накладным.
✅Вы пишете библиотеку с высокой производительностью.

4️⃣Когда НЕ использовать ValueTask:
❌Операция всегда асинхронна.
❌Производительность не является критически важной.
❌Приложение не оптимизировано под работу с ValueTask.

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

Источники:

понедельник, 2 декабря 2024 г.

Надоели многочисленные if , условные операции и switch? Тогда мы идём к вам!

Приветствую, уважаемый читатель моего блога! Как-то давно меня не было в уличных гонках не выпускал новые статьи. На то были разные причины личного характера, но мы сегодня не об этом. 

Уверен, что каждый уважающий себя разработчик за свою карьеру писал выражения ветвления, которые не ограничиваются 1-2 условиями, а содержат многочисленные условия, проверки каких-либо выражений. Зачастую такие участки кода разрастаются ещё больше, обрастают дополнительной бизнес-логикой. Чем могут причинить не меньшую боль, чем известное растение-сорняк (фото ниже):


Существует множество паттернов проектирования, которые позволят минимизировать количество таких выражений. В зависимости от ситуации, можно использовать как поведенческие паттерны "Стратегия", "Состояние", да даже структурный "Декоратор" иногда может подойти. Но сегодня хотел бы рассмотреть с вами ещё один поведенческий вариант - "Цепочка обязанностей".

Начнём с определения. Паттерн "Цепочка обязанностей" позволяет создавать цепочку объектов для обработки запроса или выполнения задачи. Как и многие другие поведенческие паттерны, "Цепочка обязанностей" основывается на том, чтобы сформировать отдельные поведения в объекты-обработчики. Таким образом паттерн подразумевает связывание объектов-обработчиков в одну цепь.

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

Схема паттерна выглядит следующим образом (прилагается ниже):


где Client - отправляет запрос объекту Handler, Handler - определяет интерфейс для обработки запроса. Также может определять ссылку на следующий обработчик запроса, а ConcreteHandler1 и ConcreteHandler2 - конкретные обработчики, которые реализуют функционал обработки запроса.

Рассмотрим реализацию паттерна в .NET на практическом примере.

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

  • VIP - 20%,
  • постоянный клиент - 10%,
  • новый клиент - 5%.
  • иначе не применять скидку.
Конечно же мы изначально могли бы написать такой код (и скорее всего написали бы) с помощью цепочки операторов if (можно и switch expression):

public decimal CalculateTotalOrderSummary(Customer customer, decimal price)
{
  if (customer.IsVIP)
    return price * 0.8m;
  
  if (customer.IsRegular)
    return price * 0.9m;

  if (customer.IsNew)
    return price * 0.95m;

  return price;
}

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

1. Создадим абстрактный класс обработчика DiscountHandler, который определяет общий интерфейс для всех обработчиков скидок:

public abstract class DiscountHandler
{
  protected DiscountHandler _next;

  public DiscountHandler SetNextHandler(DiscountHandler next)
  {
    _next = next;
    return next;
  }

  public abstract decimal CalculateTotalOrderSummary(Customer customer, decimal price);
}

2. Теперь пора реализовать обработчики для каждого типа скидки, т.е. производные классы от DiscountHandler. Каждый обработчик будет отвечать за определённое правило и решать, применять ли скидку или передавать запрос следующему обработчику:

public sealed class VIPDiscountHandler : DiscountHandler
{
  public override decimal CalculateTotalOrderSummary(Customer customer, decimal price)
  {
    if (customer.IsVIP)
      return price * 0.8m;

    return _next?.CalculateDiscount(customer, price) ?? price;
  }
}

По аналогии можно реализовать и остальные, но оставлю это за рамками статьи.

3. Создадим цепочку связанных обработчиков:

var discountHandler = new VIPDiscountHandler();

discountHandler
.SetNextHandler(new RegularDiscountHandler()) .SetNextHandler(new NewCustomerDiscountHandler()) .SetNextHandler(new NoDiscountHandler());

И вот теперь можно вызвать цепочку, обратившись к методу CalculateTotalOrderSummary первого обработчика:

decimal totalOrder = discountHandler
 .CalculateTotalOrderSummary(customer, price);

Самое время разобраться в преимуществах и недостатках нового подхода.

Плюсы паттерна, в чём-то совпадают с принципами SOLID:

1. Гибкость
Позволяет динамически изменять или расширять цепочку (добавлять или удалять обработчики), не затрагивая другие части кода.

2. Слабая связанность
Каждому обработчику нужно знать только о своем непосредственном преемнике, что минимизирует зависимости.

3. Единая ответственность
Отделяет классы, которые вызывают операции, от классов, которые выполняют операции.

Минусы подхода:

1. Запрос может остаться необработанным
Если ни один из обработчиков в цепочке не может обработать запрос, он может остаться необработанным, что приведет к неожиданному поведению. Важно иметь обработчик по умолчанию или способ обработки таких сценариев.

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

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

Источники:
- Тепляков С. "Паттерны проектирования на платформе .NET." — СПб.: Питер, 2015. Глава 7.
https://refactoring.guru/ru/design-patterns/chain-of-responsibility
https://thecodeman.net/posts/chain-responsibility-pattern

понедельник, 15 июля 2024 г.

Что не так с Activator.CreateInstance(Type) ?

  Будьте аккуратны при использовании Activator.CreateInstance(Type). Он может вернуть null


  Для справкиActivator.CreateInstance(Type) позволяет создать экземпляр типа, не зная тип во время компиляции. Например, можно создать экземпляр типа на основе файла конфигурации или ввода пользователя. Всегда, когда нельзя использовать ключевое слово new, вы можете использовать Activator.CreateInstance(Type).

  Но есть одна маленькая особенность: все мы ожидаем на выходе получить не-null объект, поскольку явно запрашиваем создание нового экземпляра, но это не всегда так. Для типов Nullable<T> Activator.CreateInstance(Type) возвращает значение null.

object? value = Activator.CreateInstance(typeof(int?));
Console.WriteLine(value is null); // true

Стоит отметить, что такое же поведение наблюдается при использовании ключевого слова new:

object? value = new int?();
Console.WriteLine(value is null); // true

Однако можно использовать оператор подавления null (!), чтобы подавить предупреждение о том, что значение может быть null, если уверены, что тип не может быть Nullable<T>:

object? value = Activator.CreateInstance(typeof(object))!;
Console.WriteLine(value.GetHashCode());
Console.WriteLine(value is null); // всегда false

Источник:
https://www.meziantou.net/activator-createinstance-type-may-return-null.htm