.NET Разработчик
6.51K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День двести шестой. #BestPractices
Советы по разработке типов
4. Разработка интерфейсов

Хотя большинство API лучше всего моделировать с использованием классов и структур, в некоторых случаях интерфейсы являются более подходящим или даже единственным вариантом:
- Реализация эффекта множественного наследования. CLR не поддерживает множественное наследование, но позволяет типам реализовывать один или несколько интерфейсов в дополнение к наследованию от базового класса. Например, IDisposable - это интерфейс, который позволяет типам поддерживать освобождение ресурсов независимо от их собственной иерархии наследования.
- Создание общего интерфейса, который может поддерживаться несколькими типами, включая некоторые значимые типы. Значимые типы не могут наследоваться, но могут реализовывать интерфейсы, поэтому использование интерфейса является единственным вариантом для обеспечения общего поведения.
ИСПОЛЬЗУЙТЕ интерфейс, если вам нужен общий API, который будет поддерживаться набором типов, включая значимые типы.
⚠️ РАССМОТРИТЕ использование интерфейса, если вам необходимо поддерживать функциональность для типов, которые уже наследуются от какого-либо другого типа.
ИЗБЕГАЙТЕ использования маркерных интерфейсов (интерфейсов без членов). Если вам нужно пометить класс как имеющий определенную характеристику (маркер), используйте пользовательский атрибут, а не интерфейс.
ИСПОЛЬЗУЙТЕ хотя бы один тип, реализующий интерфейс. Это помогает проверить реализацию интерфейса. Например, List<T> является реализацией интерфейса IList<T>.
ИСПОЛЬЗУЙТЕ хотя бы один API, использующий каждый определенный вами интерфейс (метод, принимающий интерфейс в качестве параметра или свойство интерфейсного типа). Это помогает проверить реализацию интерфейса. Например, List<T>.Sort использует интерфейс System.Collections.Generic.IComparer<T>.
ИЗБЕГАЙТЕ добавления членов в интерфейс, выпущенный в предыдущей версии, если вы поставляете библиотеку. Это нарушит все реализации этого интерфейса. Вы должны создать новый интерфейс, чтобы избежать проблем при переходе от одной версии к другой.
ЗАМЕЧАНИЕ: Начиная с C#8 в таких случаях для интерфейсных методов можно использовать реализацию по умолчанию.

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести седьмой. #BestPractices
Советы по разработке типов
5. Разработка структур

Значимый тип общего назначения чаще всего называют структурой (ключевое слово struct).

ИЗБЕГАЙТЕ конструкторов без параметров для структур. Это позволяет создавать массивы структур без необходимости запуска конструктора для каждого элемента массива. C# не позволяет структурам иметь конструкторы без параметров.
ИЗБЕГАЙТЕ изменяемых структур. С изменяемыми структурами возникает несколько проблем. Например, когда аксессор get свойства возвращает структуру, вызывающая сторона получает копию. Поскольку копия создаётся неявно, разработчики могут не знать, что они изменяют копию, а не исходное значение. Кроме того, некоторые языки (в частности, динамические языки) имеют проблемы с использованием изменяемых значимых типов, потому что даже локальные переменные, при разыменовании, приводят к созданию копии.
⚠️ УБЕДИТЕСЬ, что состояние, в котором все данные экземпляра установлены на ноль, ложь или null (в зависимости от ситуации), является допустимым. Это предотвращает случайное создание недопустимых экземпляров при создании массива структур.
ИСПОЛЬЗУЙТЕ реализацию IEquatable<T> для структур. Метод Object.Equals для значимых типов приводит к упаковке, и его реализация по умолчанию не очень эффективна, поскольку использует отражение. Метод Equals может иметь гораздо лучшую производительность и может быть реализован так, чтобы это не приводило к упаковке.
ИЗБЕГАЙТЕ явного наследования от ValueType. На самом деле, большинство языков запрещают это.

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

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести восьмой. #BestPractices
Советы по разработке типов
6. Разработка перечислений. Начало
Перечисления - это особый значимый тип.
Виды перечислений:
- Простые - небольшие замкнутые наборы вариантов (например, набор цветов).
- Битовые флаги - предназначены для поддержки побитовых операций над значениями (например, список опций).

ИСПОЛЬЗУЙТЕ существительные в единственном числе для простых перечислений и во множественном для битовых флагов.
ИЗБЕГАЙТЕ наследования от System.Enum напрямую. System.Enum - это специальный тип, используемый CLR для создания пользовательских перечислений. Большинство языков программирования предоставляют специальный элемент, который дает вам доступ к этой функциональности. В C# для определения перечисления используется ключевое слово enum.
ИСПОЛЬЗУЙТЕ перечисление для строгого ввода параметров, свойств и возвращаемых значений, представляющих наборы значений.
ИСПОЛЬЗУЙТЕ перечисление вместо статических констант.
ИЗБЕГАЙТЕ использования перечислений для открытых наборов (таких как версии операционной системы, имена ваших друзей и т.д.).
ИЗБЕГАЙТЕ создания зарезервированных значений перечисления, которые предназначены для будущего использования. Вы всегда можете просто добавить значения к существующему перечислению на более позднем этапе. Зарезервированные значения просто загрязняют набор реальных значений и приводят к ошибкам при использовании.
ИЗБЕГАЙТЕ создания перечисления только с одним значением. Обычной практикой обеспечения будущей расширяемости API является добавление зарезервированных параметров в сигнатуры методов. Такие зарезервированные параметры могут быть выражены в виде перечисления с одним значением по умолчанию. Это неверная практика для управляемых API. Перегрузка метода позволяет добавлять параметры в будущих выпусках.
ИЗБЕГАЙТЕ включения сервисных значений в перечисления. Несмотря на то, что они иногда полезны для разработчиков фреймворка, сервисные значения сбивают с толку его пользователей. Они используются для отслеживания состояния перечисления, а не являются одним из значений из набора, представляемого перечислением.
ИСПОЛЬЗУЙТЕ нулевое значение для простых перечислений. Вы можете назвать это значение «None» («Значение отсутствует»). Если такое значение не подходит для данного конкретного перечисления, наиболее распространенному значению по умолчанию для перечисления должно быть присвоено базовое значение ноль.
⚠️ РАССМОТРИТЕ использование Int32 (по умолчанию в большинстве языков) в качестве базового типа перечисления, если не выполняется одно из следующих условий:
- Перечисление представляет собой битовые флаги, и у вас более 32 флагов или вы ожидаете, что их будет больше в будущем.
- Базовый тип отличается от Int32 для облегчения взаимодействия с неуправляемым кодом, ожидающим перечисления разных размеров.
- Меньший по размеру базовый тип приведёт к значительной экономии места. Если вы ожидаете, что перечисление будет использоваться главным образом в качестве аргумента в логике кода, размер не имеет большого значения. Экономия по размеру может быть значительной, если ожидается, что:
- перечисление будет использоваться как поле в очень часто создаваемой структуре или классе.
- пользователи будут создавать большие массивы или коллекции экземпляров перечисления.
- большое количество экземпляров перечисления будет сериализовываться.

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести девятый. #BestPractices
Советы по разработке типов
6. Разработка перечислений. Окончание
Разработка битовых флагов
ИСПОЛЬЗУЙТЕ атрибут System.FlagsAttribute для пометки битовых флагов. Не применяйте этот атрибут к простым перечислениям.
ИСПОЛЬЗУЙТЕ степени двойки для значений битовых флагов, чтобы их можно было свободно комбинировать с помощью операции побитового ИЛИ.
⚠️ РАССМОТРИТЕ предоставление специальных значений для часто используемых комбинаций флагов. Побитовые операции не должны требоваться для простых задач. Например: ReadWrite (комбинация значений Read и Write).
ИЗБЕГАЙТЕ создания битовых флагов, где определенные комбинации значений недопустимы.
ИСПОЛЬЗУЙТЕ название None для нулевого значения битовых флагов. В этом случае оно всегда должно означать «все флаги сняты».
ИЗБЕГАЙТЕ использования нулевого значения битовых флагов, если только оно не означает «все флаги сняты» и не названо соответствующим образом.

Добавление значения к перечислению
Очень часто обнаруживается, что вам нужно добавить значения в перечисление после того, как оно уже выпущено в одной из версий программы. Существует потенциальная проблема совместимости приложений при возвращении нового значения из существующего API, поскольку плохо написанные приложения могут неправильно обрабатывать новое значение.
⚠️ РАССМОТРИТЕ добавление значений к перечислениям, несмотря на небольшой риск несовместимости.
Если у вас есть реальные данные о несовместимости приложений, вызванной добавлением значений в перечисление, рассмотрите возможность добавления нового API, который возвращает и новые, и старые значения, и пометьте старый API как устаревший, который должен продолжать возвращать только старые значения. Это обеспечит совместимость существующих приложений.

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести десятый. #BestPractices
Советы по разработке типов
7. Вложенные Типы
Вложенный тип - это тип, определенный в области действия другого типа, который называется включающим типом. Вложенный тип имеет доступ ко всем членам своего включающего типа. Например, он имеет доступ к закрытым (private) полям, определенным во включающем типе, и к защищенным (protected) полям, определенным во всех потомках включающего типа.
Как правило, вложенные типы следует использовать с осторожностью. На это есть несколько причин. Некоторые разработчики не полностью знакомы с концепцией. Они могут, например, иметь проблемы с синтаксисом объявления переменных вложенных типов. Вложенные типы также очень тесно связаны со своими включающими типами и сами по себе не подходят для типов общего назначения.
Вложенные типы лучше всего подходят для моделирования деталей реализации включающих их типов. Конечный пользователь редко должен объявлять переменные вложенного типа и почти никогда не должен явно создавать экземпляры вложенных типов. Например, перечислитель коллекции (enumerator) может быть вложенным типом этой коллекции. Перечислители обычно создаются включающим типом, и, поскольку многие языки поддерживают оператор foreach, переменные перечислителя редко должны объявляться конечным пользователем.

ИСПОЛЬЗУЙТЕ вложенные типы, когда связь между вложенным типом и его внешним типом такова, что желательно иметь доступ к членам типа.
ИЗБЕГАЙТЕ использования открытых (public) вложенных типов для логической группировки типов; используйте для этого пространства имен.
ИЗБЕГАЙТЕ открытых вложенных типов в публичных API. Единственное исключение - если переменные вложенного типа необходимо объявлять только в редких случаях, например, при создании подклассов или при расширенной настройке.
ИЗБЕГАЙТЕ использования вложенных типов, если на тип, скорее всего, будут ссылаться извне включающим его типа. Например, перечисление, передаваемое методу, определенному в классе, не должно определяться как вложенный тип класса.
ИЗБЕГАЙТЕ использования вложенных типов, если они должны создаваться клиентским кодом. Если тип имеет открытый конструктор, он, скорее всего, не должен быть вложенным. Если тип может быть создан, значит он должен быть самостоятельным (вы можете создавать его, работать с ним и уничтожать его, не используя включающий тип). Внутренние типы не должны широко использоваться за пределами внешнего типа вне контекста внешнего типа.
ИЗБЕГАЙТЕ определения вложенного типа как члена интерфейса. Многие языки не поддерживают такую конструкцию.

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести одиннадцатый. #Оффтоп
Периодически смотрю видео от канадского блоггера Stefan Mischook https://www.youtube.com/user/killerphp/ Видео не связаны с .Net от слова совсем. Скорее это пространные рассуждения о программистской жизни, технологиях, IT индустрии, общих принципах программирования и несколько видеоуроков, например, по HTML и CSS. Интересно чисто для развлечения и расширения кругозора.
Ну, а чтобы привлечь внимание .Net сообщества, вот одно из последних видео с рассуждениями о перспективах разработки в сфере Microsoft: https://www.youtube.com/watch?v=6zJTLiNxi3k

PS: Видео, конечно, на английском.
День двести двенадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Начало
В C#5 введено понятие асинхронной функции. Это метод (анонимная функция) объявленный с модификатором async, который может использовать выражения await для ожидания операций. Если операция, которую ожидает выражение, ещё не завершена, асинхронная функция немедленно возвращает управление, а затем продолжается с того места, на котором остановилась, когда значение становится доступным (в соответствующем потоке). Естественный ход программы, при котором следующий оператор не выполняется, пока текущий оператор не завершится, сохраняется, но без блокировки потока. В примере ниже приложение Windows Forms извлекает текст из заданного URL и отображает длину HTML кода в элементе Label:
public class AsyncIntro : Form
{
private static readonly HttpClient client = new HttpClient();
private readonly Label label;
private readonly Button button;

public AsyncIntro()
{

button.Click += DisplayWebSiteLength;

}
async void DisplayWebSiteLength(object sender, EventArgs e)
{
label.Text = "Fetching...";
string text = await client.GetStringAsync("https://csharpindepth.com");
label.Text = text.Length.ToString();
}

}

Краткую строку:
string text = await client.GetStringAsync("https://csharpindepth.com");
можно написать подробнее:
Task<string> task = client.GetStringAsync("https://csharpindepth.com");
string text = await task;

Заметьте, что тип задачи - Task<string>, но тип выражения await - просто string. В этом смысле оператор await выполняет операцию развёртывания (когда ожидаемое значение типа Task<TResult>).

Если поместить точку останова на первую строку и выполнить код до неё в отладчике, стек вызовов покажет, что вы находитесь в событии Button.OnClick. Когда вы достигаете await, код проверяет, доступен ли результат, и, если это не так (в данном случае почти всегда), он планирует выполнение продолжения (continuation) после завершения операции. Продолжение – это фактически функция обратного вызова, которая выполняется, когда асинхронная операция завершена (аналогично методу ContinueWith класса Task). Продолжение поддерживает состояние метода (захватывает все доступные переменные), а также выполняется в том же потоке (в данном случае в потоке пользовательского интерфейса).
Если поместить точку останова после выражения await и снова запустить код, то (предполагая, что потребовалось использовать продолжение) в стеке вызовов больше не будет метода Button.OnClick. Этот метод давно завершился. Поначалу такое изменение стека вызовов может шокировать, но это необходимо для работы асинхронного кода. Работа асинхронных методов достигается компилятором путём создания сложного конечного автомата.

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести тринадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Определение асинхронных методов
Синтаксис для объявления асинхронного метода точно такой же, как и для любого другого метода, за исключением того, что он должен включать ключевое слово async в любом месте перед типом возврата:
public static async Task<int> FooAsync() { ... }
public async static Task<int> FooAsync() { ... }
async public Task<int> FooAsync() { ... }
public async virtual Task<int> FooAsync() { ... }

Тут есть небольшой секрет: разработчикам языка вообще не нужно было требовать включать слово async. Схожим образом компилятор интерпретирует yield return или yield break. Компилятор мог бы обнаруживать await внутри метода и использовать его для перехода в асинхронный режим. Но использование ключевого слова async значительно облегчает чтение кода. Оно сообщает вам, что этот метод асинхронный и в нём нужно искать выражение await.
Асинхронные функции ограничены следующими типами возврата:
- void
- Task
- Task<TResult>
- ValueTask<TResult> (C#7+ об этом типе позже)

Типы Task и Task<TResult> представляют операцию, которая может быть еще незавершена; Task<TResult> наследует от Task. Task<TResult> представляет операцию, которая возвращает значение типа TResult, а Task не возвращает результата. Однако лучше возвращать Task вместо void, поскольку Task позволяет вызывающему коду добавить собственные продолжения к возвращённой задаче, определять, выполнена ли задача и т.п. Возможность возврата void оставлена для совместимости с обработчиками событий (см. метод DisplayWebSiteLength в предыдущем посте). Подписка на события – это, пожалуй, единственный случай, когда рекомендуется возвращать void из асинхронного метода.
Хотя тип результата асинхронных методов довольно жестко ограничен, большинство других аспектов не отличаются от обычных методов: асинхронные методы могут быть обобщёнными, статическими или нестатическими и определять любые модификаторы доступа.
Однако существуют ограничения на параметры. Ни один из параметров асинхронного метода не может использовать модификаторы out или ref, т.к. они предназначены для возврата информации в вызывающий метод, что не имеет смысла при асинхронном вызове. Кроме того, не могут использоваться типы указателей.

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести четырнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Выражения await
Синтаксис выражения await прост: за оператором await следует другое выражение, которое возвращает значение. Вы можете ожидать результата вызова метода, переменной или свойства. Также выражение не обязано быть простым: можно объединить вызовы методов и дождаться результата:
int result = await foo.Bar().Baz();
Приоритет оператора await ниже, чем у точки, поэтому этот код эквивалентен следующему:
int result = await (foo.Bar().Baz());

Ограничения выражений await
На выражения await накладываются некоторые ограничения.
1. Во-первых, выражения должны быть «ожидаемыми» (awaitable), то есть реализовывать паттерн awaitable (о нём далее).
2. Их можно использовать только в асинхронных методах и асинхронных анонимных функциях. Даже внутри асинхронного метода нельзя использовать оператор await в анонимной функции, если она не обозначена как асинхронная.
3. Оператор await запрещён в небезопасном контексте. Это не означает, что вы не можете использовать небезопасный код в асинхронном методе; вы просто не можете использовать оператор await в этой части.
4. Запрещено использовать await внутри блокировки (lock). Если вам когда-нибудь потребуется блокировка ресурса на время выполнения асинхронной операции, вам следует изменить код. Не пытайтесь обойти ограничения компилятора, вызывая Monitor.TryEnter и Monitor.Exit вручную с помощью блока try/finally. Если это жизненно необходимо сделать, попробуйте использовать SemaphoreSlim с его метод WaitAsync.
Дело в том, что монитор, используемый оператором lock, может быть освобождён только тем же потоком, который первоначально его получил, в то время, как весьма вероятно, что поток, выполняющий код перед выражением await, будет отличаться от потока, выполняющего код после него. Либо во время ожидания первоначальный поток будет использован для выполнения какого-либо другого кода. По сути, оператор lock и асинхронность несовместимы.
5. Всегда было возможно использовать await в блоке try, который имеет только блок finally, а, следовательно, и в операторе using. Но до C#6 нельзя было использовать await в следующих блоках:
- try с блоком catch
- catch
- finally
Начиная с C#6 эти ограничения сняты.

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести пятнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Паттерн awaitable
Паттерн awaitable используется для определения типов, которые можно использовать с оператором await. Можно было бы ожидать интерфейса вроде IDisposable для оператора using. Однако поддержка await основана на шаблоне. Допустим, есть выражение типа T, которое необходимо ожидать. Компилятор выполняет следующие проверки:
1. T должен иметь метод GetAwaiter() без параметров, либо должен существовать метод расширения, принимающий один параметр типа T. Метод GetAwaiter не должен быть пустым. Тип возвращаемого значения метода называется awaiter («ожидатель»).
2. Awaiter должен реализовывать System.Runtime.INotifyCompletion, имеющий единственный метод: void OnCompleted(Action).
3. Awaiter должен иметь свойство IsCompleted типа bool.
4. Awaiter должен иметь метод GetResult() без параметров.
5. Этим не обязательно быть открытыми, но они должны быть доступны из асинхронного метода, в котором используется await.
6. Тип результата выражения await определяется типом результата метода GetResult. Если это тип void, значит await не возвращает значения.

Рассмотрим статический метод Task.Yield(). В отличие от большинства других методов класса Task, метод Yield() возвращает не задачу, структуру YieldAwaitable. Вот упрощенная версия задействованных типов:
public class Task
{

public static YieldAwaitable Yield();
}
public struct YieldAwaitable
{
public YieldAwaiter GetAwaiter();

public struct YieldAwaiter : INotifyCompletion
{
public bool IsCompleted { get; }
public void OnCompleted(Action continuation);
public void GetResult();
}
}
YieldAwaitable следует паттерну awaitable, описанному ранее. Поэтому можно вызвать:
await Task.Yield();
GetResult структуры YieldAwaiter возвращает void, поэтому код выше не имеет результата. То есть следующий код недопустим:
var result = await Task.Yield();

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести шестнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Асинхронный поток выполнения
Когда поток выполнения достигает выражения await, есть два варианта:
1. Ожидаемая асинхронная операция уже завершена.
В этом случае процесс продолжается, как обычно. Если операция завершилась неудачно и захватила исключение, генерируется исключение. В противном случае возвращается результат (например, извлекается string из Task<string>), и вы переходите к следующей строке программы. Все это делается без какого-либо переключения контекста потока или создания продолжений.
2. Ожидаемая асинхронная операция ещё не завершена.
В этом случае метод асинхронно ожидает завершения операции, а затем продолжает работу в соответствующем контексте. Это асинхронное ожидание означает, что метод не выполняется дальше. К асинхронной операции прикрепляется продолжение, содержащее остаток кода асинхронного метода, а сам метод возвращается. Инфраструктура гарантирует, что продолжение выполняется в нужном потоке: обычно это либо поток из пула (где не имеет значения, какой поток используется), либо поток пользовательского интерфейса. С точки зрения разработчика, можно представить, что метод приостанавливается до тех пор, пока асинхронная операция не завершится. Компилятор гарантирует, что все локальные переменные, используемые в методе, будут иметь те же значения, как это происходит с блоками итераторов при yield return.

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

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

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести семнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Обработка исключений
Когда вы ожидаете завершения асинхронной операции, то возможно, она давным-давно завершилась неудачей в совершенно другом потоке. В этом случае обычный синхронный способ распространения исключений в стеке не подходит. Вместо этого инфраструктура async/await предпринимает некоторые шаги, чтобы опыт обработки асинхронных сбоев был максимально похож на синхронные сбои.
Метод GetResult() ожидателя предназначен как для извлечения возвращаемого значения, если оно есть, так и для распространения любых исключений, возникших в асинхронной операции. Это не так просто, как кажется, потому что в асинхронном мире одна задача может состоять из несколько операций, которые приведут к нескольким сбоям.
Task и Task<TResult> указывают на сбои несколькими способами:
- Свойство Status становится Faulted, если асинхронная операция не выполнена (а IsFaulted возвращает значение true).
- Свойство Exception возвращает AggregateException, которое содержит все (возможно несколько) исключений, которые привели к сбою задачи, либо null если задача завершилась успешно.
- Метод Wait() выбрасывает исключение AggregateException, если задача заканчивается неудачей.
- Свойство Result для Task<TResult> (которое также приводит к ожиданию завершения задачи) аналогично выбрасывает AggregateException.
Если задача отменяется через CancellationToken, метод Wait() и свойство Result генерируют исключение AggregateException, содержащее исключение OperationCanceledException, но при этом свойство Status устанавливается в Canceled (см. пост про отмену задания https://t.iss.one/NetDeveloperDiary/219).
При вызове await, если задача завершается неудачей или отменяется, выбрасывается исключение, но не AggregateException. Вместо этого для удобства выбрасывается первое исключение из коллекции AggregateException. В большинстве случаев это то, что вам нужно. Однако это может привести к потере информации. Если в задаче возникает несколько исключений, GetResult выдаёт только первое из них. Возможно, вы захотите переписать код, чтобы при сбое вызывающая сторона могла перехватить AggregateException и изучить все причины сбоя. Некоторые методы фреймворка делают это. Например, Task.WhenAll(), который асинхронно ожидает завершения всех задач, указанных в аргументе метода. Например:
var tasks = new Task<string>[]{ … };

Task<string[]> results = Task.WhenAll(tasks);
foreach (var result in results.Result) {…}
Если какие-либо из задач дают сбой, результатом вызова results.Result является AggregateException, который будет содержать исключения из всех задач, завершившихся неудачей. Но если вы вызовете await для Task.WhenAll(), вы увидите только первое исключение:
string[] results = await Task.WhenAll(tasks);

Наиболее важный момент, который следует отметить в отношении исключений, заключается в том, что асинхронный метод никогда не генерирует исключение напрямую. Даже если первое, что делает тело метода, это генерирует исключение, метод вернет задачу в статусе Faulted.
Предположим, вы хотите выполнить некоторую работу в асинхронном методе после проверки параметра на ненулевое значение:
Task<int> task = DoSomeWork(null);

int result = await task;
Если вы проверяете параметры внутри асинхронного метода DoSomeWork, вызывающая сторона не получит никакого уведомления об ошибке до вызова await задачи.
Во многих ситуациях такая жадная проверка аргументов может быть полезна, да и разница во времени может быть не критична, однако, это в любом случае нужно иметь в виду.

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести восемнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Асинхронные анонимные функции
Асинхронные анонимные функции создаются, как и любой другой анонимный метод или лямбда-выражение, просто добавлением модификатора async в начале:
Func<Task> lambda = async () => await Task.Delay(1000);
Func<Task<int>> anonMethod = async delegate()
{
Console.WriteLine("Started");
await Task.Delay(1000);
Console.WriteLine("Finished");
return 10;
};
Делегат должен иметь сигнатуру с типом возврата, подходящим для асинхронного метода (void, Task, Task <TResult> в C#5 и 6 или пользовательским типом задачи в C#7). Он может захватывать переменные, как и другие анонимные функции, и иметь параметры. Кроме того, асинхронная операция не запускается до тех пор, пока не будет вызван делегат, а несколько вызовов делегата создают несколько операций.

Пользовательские типы заданий
В C#5 и 6 асинхронные функции могут возвращать только void, Task или Task <TResult>. C#7 слегка ослабляет это ограничение и позволяет любому типу, оформленному особым образом, использоваться в качестве возвращаемого типа для асинхронных функций.
Тип System.Threading.ValueTask<TResult> присутствует в стандартной комплектации только в инфраструктуре netcoreapp2.0, но он также доступен в NuGet пакете System.Threading.Tasks.Extensions. Он используется в 99,9% случаев. Хотя, можно создать собственный пользовательский тип задания.
ValueTask<TResult> прост: он похож на Task<TResult>, но это значимый тип. Он имеет метод AsTask, который позволяет вам получить из него обычную задачу, но в большинстве случаев он используется c await, аналогично Task<TResult>.
В чем преимущество ValueTask<TResult>? Всё сводится к выделению памяти в куче и сборке мусора. Task<TResult> является классом, и хотя в некоторых случаях асинхронная инфраструктура повторно использует завершенные объекты Task<TResult>, большинству асинхронных методов потребуется создавать новый объект Task<TResult>. Размещение объектов в .NET достаточно дёшево, поэтому во многих случаях вам не нужно об этом беспокоиться, но если вы делаете это много раз или работаете в условиях жестких ограничений производительности, вы можете избежать такого размещения, когда это возможно. Если асинхронный метод использует выражение await для чего-то незавершённого, выделение объектов неизбежно. Метод немедленно возвращается, но должен запланировать продолжение для выполнения оставшейся части метода после завершения ожидаемой операции.
В большинстве асинхронных методов это наиболее вероятный случай. В этих случаях ValueTask<TResult> не даёт никаких преимуществ и может даже быть немного дороже.
Однако в некоторых случаях, ожидание уже завершенной задачи является наиболее вероятным исходом. И именно здесь полезен ValueTask<TResult>. Например, чтение с использованием буфера (когда размер буфера много больше размера разового чтения). В редких случаях, когда буфер пустой, мы ожидаем завершения чтения в буфер. В остальных случаях (чаще всего) ожидания не требуется, асинхронный метод выполняется без продолжения, и тогда ValueTask<TResult> выигрывает.

Продолжение следует…

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
День двести девятнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Окончание
Советы по использованию
1. Избегайте захвата контекста с помощью ConfigureAwait
По умолчанию при вызове await захватывается контекст текущего потока, чтобы продолжение метода исполнялось в том же потоке. При вызове асинхронного метода из потока UI – это то, что нужно.
Но если это библиотечный код (или код в приложении, которое не касается UI), не нужно возвращаться в поток UI, даже если метод вызван из него. Вообще, чем меньше кода выполняется в потоке UI, тем лучше.
Метод ConfigureAwait принимает параметр, который определяет, будет ли ожидаемая операция захватывать контекст. На практике он почти всегда вызывается со значением false:
string text = await client.GetStringAsync(url).ConfigureAwait(false);
Результатом вызова ConfigureAwait(false) является то, что продолжение не будет запланировано для исходного контекста синхронизации; оно будет выполняться в потоке из пула.
2. Используйте параллельное выполнение, где это возможно.
Рассмотрим два примера. Этот:
Task<decimal> rateTask = employee.GetRateAsync();
decimal rate = await rateTask;
Task<int> hoursTask = timeSheet.GetHoursAsync(employee.Id);
int hours = await hoursTask;
AddPayment(rate * hours);
И этот:
Task<decimal> rateTask = employee.GetRateAsync();
Task<int> hoursTask = timeSheet.GetHoursAsync(employee.Id);
AddPayment(await rateTask * await hoursTask);
В дополнение к тому, что второй кусок кода короче, он вводит параллелизм. Обе задачи могут быть запущены независимо, потому что вам не нужен результат первой задачи для выполнения второй. Это не означает, что асинхронная инфраструктура создаст больше потоков. Например, если две асинхронные операции являются веб-службами, оба запроса к веб-службам могут выполняться без блокировки каких-либо потоков.
3. Избегайте смешивания синхронного и асинхронного кода
Правильно реализовать логику, когда часть кода является синхронной, а другие части асинхронными, довольно сложно. Переключение между этими двумя подходами сопряжено с трудностями. Если у вас есть сетевая библиотека, которая предоставляет только синхронные операции, написать асинхронную оболочку для этих операций сложно, то же самое и наоборот.
В частности, следует помнить об опасности использования Task<TResult>.Result и Task.Wait() для синхронного получения результата асинхронной операции. Это может легко привести к тупику (deadlock). Чаще всего, когда асинхронная операция требует выполнения продолжения в заблокированном потоке, вызвавшем, например, Task.Wait().
4. Реализуйте отмену операции, где это возможно
Асинхронный код имеет преимущество перед синхронным ещё и в возможности отмены операции через токен отмены. Большинство асинхронных API предоставляют возможность передавать токен отмены в качестве параметра. Не пренебрегайте этой возможностью, даже если отмена не требуется на данном этапе разработки.
5. Тестирование асинхронного кода
Большинство фреймворков модульных тестов поддерживают асинхронные тесты:
[Test]
public async Task FooAsync() {…}
Среды тестирования часто предоставляют метод Assert.ThrowsAsync для проверки того, что вызов асинхронного метода возвращает задачу, которая завершается с ошибкой.
При тестировании асинхронного кода часто требуется создать задачу, которая уже выполнена, с конкретным результатом или ошибкой. Здесь полезны методы Task.FromResult, Task.FromException и Task.FromCanceled.
Для большей гибкости можно использовать TaskCompletionSource<TResult>. Этот тип позволяет вам создать задачу, представляющую текущую операцию, а затем установить результат (включая любое исключение или отмену) позже, таким образом завершив задачу. Это чрезвычайно полезно, когда вы хотите вернуть задачу из mock-зависимости, но сделать так, чтобы она завершалась позднее в коде теста.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
👍1
День двести двадцатый. #юмор
День двести двадцать первый. #BestPractices
Советы по разработке членов типов
Перегрузка членов
Перегрузка членов - создание двух или более членов типа с одинаковым именем, которые отличаются только количеством или типом параметров. Например, в следующем случае метод WriteLine перегружен:
public static class Console {
public void WriteLine();
public void WriteLine(string value);
public void WriteLine(bool value);
...
}
Поскольку параметры могут иметь только методы, конструкторы и индексаторы, только эти члены могут быть перегружены. Перегрузка является одним из наиболее важных методов повышения удобства использования, производительности и читаемости многоразовых библиотек. Перегрузка по числу параметров позволяет предоставлять более простые версии конструкторов и методов. Перегрузка по типам параметров позволяет использовать одно и то же имя для членов, выполняющих идентичные операции с различными типами.

ИСПОЛЬЗУЙТЕ описательные имена параметров и указывайте значение по умолчанию, используемое в более коротких перегрузках.
ИЗБЕГАЙТЕ произвольного именования параметров в перегрузках. Если параметр в одной перегрузке представляет то же входное значение, что и параметр в другой перегрузке, они должны называться одинаково.
ИСПОЛЬЗУЙТЕ одинаковый порядок параметров в перегруженных членах. Параметры с одинаковыми именами должны отображаться в одной и той же позиции во всех перегрузках.
ИСПОЛЬЗУЙТЕ ключевое слово virtual только для самой длинной версии члена (если требуется расширяемость). Короткие перегрузки должны просто вызывать более длинные версии.
ИЗБЕГАЙТЕ использования модификаторов ref или out в перегруженных членах. Некоторые языки запрещают вызовы таких членов. Кроме того, такие перегрузки обычно имеют совершенно разную семантику и, вероятно, должны быть не перегрузками, а двумя разными методами.
ИЗБЕГАЙТЕ перегрузок с параметрами в одинаковых позициях и схожих типов, но с разной семантикой.
ИСПОЛЬЗУЙТЕ возможность передавать значение null по умолчанию для дополнительных аргументов.
ИСПОЛЬЗУЙТЕ перегруженные члены вместо членов с аргументами по умолчанию. Аргументы по умолчанию не соответствуют CLS.

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать второй. #BestPractices
Советы по разработке членов типов
Разработка свойств
ИСПОЛЬЗУЙТЕ свойства только для чтения, если вызывающая сторона не может изменять значение свойства. Если тип свойства является изменяемым ссылочным типом, значение свойства можно изменить, даже если оно доступно только для чтения.
ИЗБЕГАЙТЕ предоставления свойств только для записи, либо тех, где метод записи имеет более широкий уровень доступа, чем метод чтения. В этом случае используйте метод вместо свойства.
ИСПОЛЬЗУЙТЕ разумные значения по умолчанию для свойств, гарантируя, что они не приводят к дыре в безопасности или неэффективному коду.
ИЗБЕГАЙТЕ необходимости установки свойств в определённом порядке, даже если это приводит к временному недопустимому состоянию объекта. Если два или более свойств взаимосвязаны, обычно есть точка, в которой некоторые значения одного свойства могут быть недопустимыми при определённых значениях других свойств того же объекта. В таких случаях исключения, возникающие из-за недопустимого состояния, следует отложить до тех пор, пока взаимосвязанные свойства не будут фактически использованы вместе.
ИСПОЛЬЗУЙТЕ сохранение предыдущего значение, если метод записи свойства выдает исключение.
ИЗБЕГАЙТЕ выдачи исключений из методов чтения свойств. Методы чтения свойств должны быть простыми операциями и не должны иметь никаких предварительных условий. Если аксессор свойства может выдать исключение, свойство, вероятно, следует заменить методом. Обратите внимание, что это правило не применяется к индексаторам, где могут быть исключения в результате проверки аргументов.

События для уведомления об изменении свойства
Иногда полезно предоставить событие, уведомляющее пользователя об изменениях в значении свойства. Например, System.Windows.Forms.Control вызывает событие TextChanged после изменения значения его свойства Text.
⚠️ РАССМОТРИТЕ создание события для уведомлений об изменении свойства в высокоуровневых API (например, пользовательских элементах интерфейса). Однако вряд ли стоит создавать такие события для низкоуровневых API, таких как базовые типы или коллекции.
⚠️ РАССМОТРИТЕ создание события для уведомлений об изменении свойства, когда значение свойства изменяется под воздействием внешних сил (иным способом, чем при вызове методов объекта). В этом случае событие вызывается, чтобы сообщить разработчику, что значение изменяется или изменилось. Например, свойство Text элемента управления TextBox. Когда пользователь вводит текст в TextBox, значение свойства автоматически изменяется.

Продолжение следует…

Источник:
https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать третий. #BestPractices
Советы по разработке членов типов
Разработка индексаторов
Индексатор (индексированное свойство) - это специальное свойство, которое может иметь параметры и может вызываться со специальным синтаксисом, аналогичным индексу массива. Индексаторы следует использовать только в API, которые предоставляют доступ к элементам в логической коллекции. Например, строка представляет собой набор символов, и индексатор в System.String был добавлен для доступа к ее символам.

⚠️ РАССМОТРИТЕ использование индексаторов для предоставления доступа к данным, хранящимся во внутреннем массиве.
⚠️ РАССМОТРИТЕ предоставление индексаторов для типов, представляющих коллекции предметов.
ИЗБЕГАЙТЕ использования индексаторов с более чем одним параметром, кроме исключительных случаев. Если в проекте требуется несколько параметров индексатора, проверьте, действительно ли свойство представляет собой средство доступа к логической коллекции. Если это не так, используйте вместо этого метод.
ИЗБЕГАЙТЕ индексаторов с типами параметров, отличными от int, long, string, object или enum. Если для разработки требуются другие типы параметров, тщательно оцените, действительно ли API представляет средство доступа к логической коллекции. Если это не так, используйте метод.
ИСПОЛЬЗУЙТЕ имя Item для индексатора, если нет однозначно лучшего имени (например, см. Chars[Int32] в System.String). В C# индексаторы по умолчанию называются Item. Атрибут IndexerNameAttribute можно использовать для настройки этого имени.
ИЗБЕГАЙТЕ создания индексатора и метода, которые семантически эквивалентны.

Продолжение следует…

Источник:
https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать четвёртый. #ЧтоНовенького
Рефакторинг с IntelliCode!
Теперь о принципе Don’t Repeat Yourself вам будут напоминать не только нёрды-сениоры, но и Visual Studio. В версии 16.3 IntelliCode будет отслеживать изменения, отмечать повторяющийся код и предлагать внести изменения во всех аналогичных местах.
Это не просто отслеживание изменений в тексте. IntelliCode знает о синтаксической структуре вашего кода, что позволяет обнаруживать случаи, когда имена переменных отличаются, но основная структура изменений та же (см. картинку).
Функция доступна в виде превью в Visual Studio 16.3 Preview 3 и по умолчанию отключена. Перейдите в Tools > Options > IntelliCode > General, там в блоке Preview features установите инструменты рефакторинга в Enabled. После этого перезапустите IDE.

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать пятый. #BestPractices
Советы по разработке членов типов
Разработка конструкторов
Существует два вида конструкторов: конструкторы типов и конструкторы экземпляров. Конструкторы типов являются статическими и запускаются CLR перед первым использованием типа. Конструкторы экземпляра запускаются при создании экземпляра типа. Конструкторы типов не могут принимать параметров.
Конструкторы являются наиболее естественным способом создания экземпляров типа. Большинство разработчиков будут искать и пытаться использовать конструктор, прежде чем они рассмотрят альтернативные способы создания экземпляров (например, фабричный метод).

ИСПОЛЬЗУЙТЕ простые конструкторы. Простой конструктор имеет очень небольшое количество параметров примитивных типов или перечислений.
ИСПОЛЬЗУЙТЕ параметры конструктора для установки основных свойств экземпляра. Не должно быть никакой разницы в семантике между использованием пустого конструктора, за которым следует установка свойств, и использованием конструктора с несколькими аргументами.
ИСПОЛЬЗУЙТЕ одно и то же имя для параметров конструктора и свойства, если параметры конструктора используются для простой установки свойства. Единственной разницей между такими параметрами и свойствами может быть регистр.
ИСПОЛЬЗУЙТЕ минимальный код в конструкторе. Конструкторы не должны выполнять иную работу, кроме сохранения параметров конструктора в свойствах экземпляра. Любая другая работа должна быть отложена до тех пор, пока она не потребуется.
ИСПОЛЬЗУЙТЕ выбрасывание исключений из конструкторов экземпляров, если это уместно.
ИСПОЛЬЗУЙТЕ явное объявление открытого конструктора без параметров в классах, если такой конструктор требуется. Если вы явно не объявляете какие-либо конструкторы для типа, многие языки (такие как C#) автоматически добавят открытый конструктор без параметров. (Абстрактные классы получают защищенный конструктор.) Добавление параметризованного конструктора в класс не позволяет компилятору добавлять конструктор без параметров. Это часто приводит к случайным изменениям, ломающим код.
⚠️ РАССМОТРИТЕ использование статического фабричного метода вместо конструктора, если семантика желаемой операции не подразумевает непосредственного создания экземпляра, или если обращение к конструктору кажется неестественным.
ИЗБЕГАЙТЕ явного определения конструкторов без параметров в структурах. Это ускоряет создание массивов структур, поскольку если конструктор без параметров не определён, его не нужно запускать для каждого элемента массива. Компилятор C# не позволяет структурам иметь конструкторы без параметров.
ИЗБЕГАЙТЕ вызова виртуальных членов объекта внутри его конструктора. Это приводит к вызову наиболее производного переопределения члена, даже если конструктор наиболее производного типа еще не выполнен полностью.

Конструкторы типов
ИСПОЛЬЗУЙТЕ закрытые статические конструкторы. CLR вызывает статический конструктор перед созданием первого экземпляра типа или вызовом любых статических членов этого типа. Пользователь не может контролировать, когда вызывается статический конструктор. Если статический конструктор не является закрытым, он может вызываться кодом, отличным от CLR. В зависимости от операций, выполняемых в конструкторе, это может приводить к неожиданному поведению. Компилятор C# заставляет статические конструкторы быть закрытыми.
ИЗБЕГАЙТЕ выброса исключений из статических конструкторов. Если исключение возникает в конструкторе типа, тип не может использоваться в текущем домене приложения.
⚠️ РАССМОТРИТЕ инициализацию статических полей в строку, без явного использования статического конструктора, потому что среда выполнения способна оптимизировать производительность типов, которые не имеют явно определённого статического конструктора.

Продолжение следует…

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/