понедельник, 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-я классами и теперь не будет сложности в их использовании на практике.

Источники: