День двести одиннадцатый. #Оффтоп
Периодически смотрю видео от канадского блоггера Stefan Mischook https://www.youtube.com/user/killerphp/ Видео не связаны с .Net от слова совсем. Скорее это пространные рассуждения о программистской жизни, технологиях, IT индустрии, общих принципах программирования и несколько видеоуроков, например, по HTML и CSS. Интересно чисто для развлечения и расширения кругозора.
Ну, а чтобы привлечь внимание .Net сообщества, вот одно из последних видео с рассуждениями о перспективах разработки в сфере Microsoft: https://www.youtube.com/watch?v=6zJTLiNxi3k
PS: Видео, конечно, на английском.
Периодически смотрю видео от канадского блоггера 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:
Если поместить точку останова на первую строку и выполнить код до неё в отладчике, стек вызовов покажет, что вы находитесь в событии
Если поместить точку останова после выражения await и снова запустить код, то (предполагая, что потребовалось использовать продолжение) в стеке вызовов больше не будет метода Button.OnClick. Этот метод давно завершился. Поначалу такое изменение стека вызовов может шокировать, но это необходимо для работы асинхронного кода. Работа асинхронных методов достигается компилятором путём создания сложного конечного автомата.
Продолжение следует…
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 5.
Многопоточность.
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("http://csharpindepth.com");
label.Text = text.Length.ToString();
}
…
}
string text = await client.GetStringAsync("http://csharpindepth.com");можно написать подробнее:
Task<string> task = client.GetStringAsync("http://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. Продолжение
Определение асинхронных методов
Синтаксис для объявления асинхронного метода точно такой же, как и для любого другого метода, за исключением того, что он должен включать ключевое слово
Асинхронные функции ограничены следующими типами возврата:
-
Типы
Хотя тип результата асинхронных методов довольно жестко ограничен, большинство других аспектов не отличаются от обычных методов: асинхронные методы могут быть обобщёнными, статическими или нестатическими и определять любые модификаторы доступа.
Однако существуют ограничения на параметры. Ни один из параметров асинхронного метода не может использовать модификаторы
Продолжение следует…
Источник: 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
Синтаксис выражения
На выражения
1. Во-первых, выражения должны быть «ожидаемыми» (awaitable), то есть реализовывать паттерн awaitable (о нём далее).
2. Их можно использовать только в асинхронных методах и асинхронных анонимных функциях. Даже внутри асинхронного метода нельзя использовать оператор
3. Оператор
4. Запрещено использовать
Дело в том, что монитор, используемый оператором
5. Всегда было возможно использовать
-
Продолжение следует…
Источник: 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 используется для определения типов, которые можно использовать с оператором
1.
2. Awaiter должен реализовывать
3. Awaiter должен иметь свойство
4. Awaiter должен иметь метод
5. Этим не обязательно быть открытыми, но они должны быть доступны из асинхронного метода, в котором используется await.
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.
Возврат или завершение
Для асинхронных методов есть разница между возвратом из метода и завершением метода. Возврат из асинхронного метода может производиться несколько раз, когда у метода больше нет работы, которую он может сделать на данный момент. А завершиться метод может только один раз.
Замечание: до первого выражения
Продолжение следует…
Источник: 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 предпринимает некоторые шаги, чтобы опыт обработки асинхронных сбоев был максимально похож на синхронные сбои.
Метод
- Свойство
- Свойство
- Метод
- Свойство
Если задача отменяется через
При вызове
Предположим, вы хотите выполнить некоторую работу в асинхронном методе после проверки параметра на ненулевое значение:
Во многих ситуациях такая жадная проверка аргументов может быть полезна, да и разница во времени может быть не критична, однако, это в любом случае нужно иметь в виду.
Продолжение следует…
Источник: 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.me/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. Продолжение
Асинхронные анонимные функции
Асинхронные анонимные функции создаются, как и любой другой анонимный метод или лямбда-выражение, просто добавлением модификатора
Пользовательские типы заданий
В C#5 и 6 асинхронные функции могут возвращать только
Тип
В чем преимущество
В большинстве асинхронных методов это наиболее вероятный случай. В этих случаях
Однако в некоторых случаях, ожидание уже завершенной задачи является наиболее вероятным исходом. И именно здесь полезен
Продолжение следует…
Источник: 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, тем лучше.
Метод
2. Используйте параллельное выполнение, где это возможно.
Рассмотрим два примера. Этот:
3. Избегайте смешивания синхронного и асинхронного кода
Правильно реализовать логику, когда часть кода является синхронной, а другие части асинхронными, довольно сложно. Переключение между этими двумя подходами сопряжено с трудностями. Если у вас есть сетевая библиотека, которая предоставляет только синхронные операции, написать асинхронную оболочку для этих операций сложно, то же самое и наоборот.
В частности, следует помнить об опасности использования
4. Реализуйте отмену операции, где это возможно
Асинхронный код имеет преимущество перед синхронным ещё и в возможности отмены операции через токен отмены. Большинство асинхронных API предоставляют возможность передавать токен отмены в качестве параметра. Не пренебрегайте этой возможностью, даже если отмена не требуется на данном этапе разработки.
5. Тестирование асинхронного кода
Большинство фреймворков модульных тестов поддерживают асинхронные тесты:
При тестировании асинхронного кода часто требуется создать задачу, которая уже выполнена, с конкретным результатом или ошибкой. Здесь полезны методы
Для большей гибкости можно использовать
Источник: 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
Советы по разработке членов типов
Перегрузка членов
Перегрузка членов - создание двух или более членов типа с одинаковым именем, которые отличаются только количеством или типом параметров. Например, в следующем случае метод
✅ ИСПОЛЬЗУЙТЕ описательные имена параметров и указывайте значение по умолчанию, используемое в более коротких перегрузках.
❌ ИЗБЕГАЙТЕ произвольного именования параметров в перегрузках. Если параметр в одной перегрузке представляет то же входное значение, что и параметр в другой перегрузке, они должны называться одинаково.
✅ ИСПОЛЬЗУЙТЕ одинаковый порядок параметров в перегруженных членах. Параметры с одинаковыми именами должны отображаться в одной и той же позиции во всех перегрузках.
✅ ИСПОЛЬЗУЙТЕ ключевое слово virtual только для самой длинной версии члена (если требуется расширяемость). Короткие перегрузки должны просто вызывать более длинные версии.
❌ ИЗБЕГАЙТЕ использования модификаторов ref или out в перегруженных членах. Некоторые языки запрещают вызовы таких членов. Кроме того, такие перегрузки обычно имеют совершенно разную семантику и, вероятно, должны быть не перегрузками, а двумя разными методами.
❌ ИЗБЕГАЙТЕ перегрузок с параметрами в одинаковых позициях и схожих типов, но с разной семантикой.
✅ ИСПОЛЬЗУЙТЕ возможность передавать значение null по умолчанию для дополнительных аргументов.
✅ ИСПОЛЬЗУЙТЕ перегруженные члены вместо членов с аргументами по умолчанию. Аргументы по умолчанию не соответствуют CLS.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке членов типов
Перегрузка членов
Перегрузка членов - создание двух или более членов типа с одинаковым именем, которые отличаются только количеством или типом параметров. Например, в следующем случае метод
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/
Советы по разработке членов типов
Разработка свойств
✅ ИСПОЛЬЗУЙТЕ свойства только для чтения, если вызывающая сторона не может изменять значение свойства. Если тип свойства является изменяемым ссылочным типом, значение свойства можно изменить, даже если оно доступно только для чтения.
❌ ИЗБЕГАЙТЕ предоставления свойств только для записи, либо тех, где метод записи имеет более широкий уровень доступа, чем метод чтения. В этом случае используйте метод вместо свойства.
✅ ИСПОЛЬЗУЙТЕ разумные значения по умолчанию для свойств, гарантируя, что они не приводят к дыре в безопасности или неэффективному коду.
❌ ИЗБЕГАЙТЕ необходимости установки свойств в определённом порядке, даже если это приводит к временному недопустимому состоянию объекта. Если два или более свойств взаимосвязаны, обычно есть точка, в которой некоторые значения одного свойства могут быть недопустимыми при определённых значениях других свойств того же объекта. В таких случаях исключения, возникающие из-за недопустимого состояния, следует отложить до тех пор, пока взаимосвязанные свойства не будут фактически использованы вместе.
✅ ИСПОЛЬЗУЙТЕ сохранение предыдущего значение, если метод записи свойства выдает исключение.
❌ ИЗБЕГАЙТЕ выдачи исключений из методов чтения свойств. Методы чтения свойств должны быть простыми операциями и не должны иметь никаких предварительных условий. Если аксессор свойства может выдать исключение, свойство, вероятно, следует заменить методом. Обратите внимание, что это правило не применяется к индексаторам, где могут быть исключения в результате проверки аргументов.
События для уведомления об изменении свойства
Иногда полезно предоставить событие, уведомляющее пользователя об изменениях в значении свойства. Например, 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/
Советы по разработке членов типов
Разработка индексаторов
Индексатор (индексированное свойство) - это специальное свойство, которое может иметь параметры и может вызываться со специальным синтаксисом, аналогичным индексу массива. Индексаторы следует использовать только в 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/
Рефакторинг с 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/
Советы по разработке членов типов
Разработка конструкторов
Существует два вида конструкторов: конструкторы типов и конструкторы экземпляров. Конструкторы типов являются статическими и запускаются CLR перед первым использованием типа. Конструкторы экземпляра запускаются при создании экземпляра типа. Конструкторы типов не могут принимать параметров.
Конструкторы являются наиболее естественным способом создания экземпляров типа. Большинство разработчиков будут искать и пытаться использовать конструктор, прежде чем они рассмотрят альтернативные способы создания экземпляров (например, фабричный метод).
✅ ИСПОЛЬЗУЙТЕ простые конструкторы. Простой конструктор имеет очень небольшое количество параметров примитивных типов или перечислений.
✅ ИСПОЛЬЗУЙТЕ параметры конструктора для установки основных свойств экземпляра. Не должно быть никакой разницы в семантике между использованием пустого конструктора, за которым следует установка свойств, и использованием конструктора с несколькими аргументами.
✅ ИСПОЛЬЗУЙТЕ одно и то же имя для параметров конструктора и свойства, если параметры конструктора используются для простой установки свойства. Единственной разницей между такими параметрами и свойствами может быть регистр.
✅ ИСПОЛЬЗУЙТЕ минимальный код в конструкторе. Конструкторы не должны выполнять иную работу, кроме сохранения параметров конструктора в свойствах экземпляра. Любая другая работа должна быть отложена до тех пор, пока она не потребуется.
✅ ИСПОЛЬЗУЙТЕ выбрасывание исключений из конструкторов экземпляров, если это уместно.
✅ ИСПОЛЬЗУЙТЕ явное объявление открытого конструктора без параметров в классах, если такой конструктор требуется. Если вы явно не объявляете какие-либо конструкторы для типа, многие языки (такие как C#) автоматически добавят открытый конструктор без параметров. (Абстрактные классы получают защищенный конструктор.) Добавление параметризованного конструктора в класс не позволяет компилятору добавлять конструктор без параметров. Это часто приводит к случайным изменениям, ломающим код.
⚠️ РАССМОТРИТЕ использование статического фабричного метода вместо конструктора, если семантика желаемой операции не подразумевает непосредственного создания экземпляра, или если обращение к конструктору кажется неестественным.
❌ ИЗБЕГАЙТЕ явного определения конструкторов без параметров в структурах. Это ускоряет создание массивов структур, поскольку если конструктор без параметров не определён, его не нужно запускать для каждого элемента массива. Компилятор C# не позволяет структурам иметь конструкторы без параметров.
❌ ИЗБЕГАЙТЕ вызова виртуальных членов объекта внутри его конструктора. Это приводит к вызову наиболее производного переопределения члена, даже если конструктор наиболее производного типа еще не выполнен полностью.
Конструкторы типов
✅ ИСПОЛЬЗУЙТЕ закрытые статические конструкторы. CLR вызывает статический конструктор перед созданием первого экземпляра типа или вызовом любых статических членов этого типа. Пользователь не может контролировать, когда вызывается статический конструктор. Если статический конструктор не является закрытым, он может вызываться кодом, отличным от CLR. В зависимости от операций, выполняемых в конструкторе, это может приводить к неожиданному поведению. Компилятор C# заставляет статические конструкторы быть закрытыми.
❌ ИЗБЕГАЙТЕ выброса исключений из статических конструкторов. Если исключение возникает в конструкторе типа, тип не может использоваться в текущем домене приложения.
⚠️ РАССМОТРИТЕ инициализацию статических полей в строку, без явного использования статического конструктора, потому что среда выполнения способна оптимизировать производительность типов, которые не имеют явно определённого статического конструктора.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать шестой. #BestPractices
Советы по разработке членов типов
Разработка событий
События являются наиболее часто используемой формой обратных вызовов. Существует две группы событий: события, возникающие до изменения состояния системы, называемые пред-событиями, и события, возникающие после изменения состояния, называемые пост-событиями. Например, пред-событие
✅ ИСПОЛЬЗУЙТЕ термин raise для событий вместо fire или trigger.
✅ ИСПОЛЬЗУЙТЕ
⚠️ РАССМОТРИТЕ использование классов, производных от
✅ ИСПОЛЬЗУЙТЕ защищенный виртуальный метод, для вызова каждого события. Это применимо только к нестатическим событиям в открытых классах, но не к структурам, закрытым классам или статическим событиям. Цель такого метода - предоставить производному классу способ обработки события с использованием переопределения. Переопределение - это более гибкий, быстрый и более естественный способ обработки событий базового класса в производных классах. По соглашению имя метода должно начинаться с
✅ ИСПОЛЬЗУЙТЕ один параметр для передачи аргументов события. Параметр должен быть назван
❌ ИЗБЕГАЙТЕ передачи
❌ ИЗБЕГАЙТЕ передачи
⚠️ РАССМОТРИТЕ вызов событий, которые конечный пользователь может отменить. Это относится только к пред-событиями. Используйте
Пользовательские обработчики событий
Есть случаи, когда
✅ ИСПОЛЬЗУЙТЕ
✅ ИСПОЛЬЗУЙТЕ
✅ ИСПОЛЬЗУЙТЕ
❌ ИЗБЕГАЙТЕ добавления более двух параметров в обработчик события.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке членов типов
Разработка событий
События являются наиболее часто используемой формой обратных вызовов. Существует две группы событий: события, возникающие до изменения состояния системы, называемые пред-событиями, и события, возникающие после изменения состояния, называемые пост-событиями. Например, пред-событие
Form.Closing
вызывается перед закрытием формы, пост-событие Form.Closed
вызывается после закрытия формы.✅ ИСПОЛЬЗУЙТЕ термин raise для событий вместо fire или trigger.
✅ ИСПОЛЬЗУЙТЕ
System.EventHandler<TEventArgs>
вместо того, чтобы вручную создавать новые делегаты для использования в качестве обработчиков событий.⚠️ РАССМОТРИТЕ использование классов, производных от
EventArgs
, в качестве аргумента события, кроме случаев, когда вы абсолютно уверены, что событию никогда не потребуется передавать какие-либо данные в метод обработки, и в этом случае вы можете напрямую использовать тип EventArgs
. Если вы напрямую используете EventArgs
, вы никогда не сможете добавить какие-либо данные для передачи в обработчик события без нарушения совместимости. Если вы используете подкласс, даже если изначально он полностью пуст, вы сможете добавлять свойства в подкласс при необходимости.✅ ИСПОЛЬЗУЙТЕ защищенный виртуальный метод, для вызова каждого события. Это применимо только к нестатическим событиям в открытых классах, но не к структурам, закрытым классам или статическим событиям. Цель такого метода - предоставить производному классу способ обработки события с использованием переопределения. Переопределение - это более гибкий, быстрый и более естественный способ обработки событий базового класса в производных классах. По соглашению имя метода должно начинаться с
On
и сопровождаться названием события. Производный класс может не вызывать базовую реализацию метода в переопределении. Будьте готовы к этому, не включая в базовый метод код который требуется для корректной работы базового класса.✅ ИСПОЛЬЗУЙТЕ один параметр для передачи аргументов события. Параметр должен быть назван
e
и должен иметь тип класса аргументов события.❌ ИЗБЕГАЙТЕ передачи
null
в качестве параметра отправителя при возникновении нестатического события. Но используйте null
при возникновении статического события.❌ ИЗБЕГАЙТЕ передачи
null
в качестве параметра аргументов события. Следует передавать EventArgs.Empty
, если вы не хотите передавать какие-либо данные в метод обработки событий. Разработчики ожидают, что этот параметр не будет null
.⚠️ РАССМОТРИТЕ вызов событий, которые конечный пользователь может отменить. Это относится только к пред-событиями. Используйте
System.ComponentModel.CancelEventArgs
или его подкласс в качестве аргумента события, чтобы позволить конечному пользователю отменять события.Пользовательские обработчики событий
Есть случаи, когда
EventHandler<T>
не может использоваться, например, когда инфраструктура должна работать с более ранними версиями CLR, которые не поддерживают обобщения. В таких случаях вам может потребоваться спроектировать и разработать пользовательский делегат обработчика событий.✅ ИСПОЛЬЗУЙТЕ
void
как тип результата для событий. Обработчик событий может вызывать несколько методов обработки событий, возможно, для нескольких объектов. Если бы методы обработки событий могли возвращать значение, для каждого вызова события было бы несколько возвращаемых значений.✅ ИСПОЛЬЗУЙТЕ
object
в качестве типа первого параметра обработчика события и называйте его sender
.✅ ИСПОЛЬЗУЙТЕ
System.EventArgs
или его подкласс в качестве типа второго параметра обработчика событий и называйте его e
.❌ ИЗБЕГАЙТЕ добавления более двух параметров в обработчик события.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать седьмой. #BestPractices
Советы по разработке членов типов
Разработка полей
Принцип инкапсуляции является одним из наиболее важных понятий в объектно-ориентированной разработке. Этот принцип гласит, что данные, хранящиеся внутри объекта, должны быть доступны только этому объекту. Полезный способ интерпретации этого принципа состоит в том, что класс должен быть спроектирован так, чтобы изменения полей класса (имени или типа поля) могли быть сделаны так, чтобы не ломать никакой код, кроме кода членов самого класса. Эта интерпретация подразумевает, что все поля должны быть закрытыми. Мы исключаем константы и статические поля только для чтения из этого строгого ограничения, потому что такие поля по определению почти никогда не изменяются.
❌ ИЗБЕГАЙТЕ предоставления открытых или защищённых полей экземпляра. Для доступа к полям нужно предоставлять свойства.
✅ ИСПОЛЬЗУЙТЕ константные поля для констант, которые никогда не изменятся. Компилятор записывает значения полей const непосредственно в вызывающий код. Следовательно, значения const никогда не могут быть изменены без риска нарушения совместимости.
✅ ИСПОЛЬЗУЙТЕ открытые статические поля только для чтения для предопределенных экземпляров объекта.
❌ ИЗБЕГАЙТЕ присваивания экземпляров изменяемых типов полям только для чтения. Изменяемый тип - это тип с экземплярами, которые могут быть изменены после их создания. Например, массивы, большинство коллекций и потоков являются изменяемыми типами, но
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке членов типов
Разработка полей
Принцип инкапсуляции является одним из наиболее важных понятий в объектно-ориентированной разработке. Этот принцип гласит, что данные, хранящиеся внутри объекта, должны быть доступны только этому объекту. Полезный способ интерпретации этого принципа состоит в том, что класс должен быть спроектирован так, чтобы изменения полей класса (имени или типа поля) могли быть сделаны так, чтобы не ломать никакой код, кроме кода членов самого класса. Эта интерпретация подразумевает, что все поля должны быть закрытыми. Мы исключаем константы и статические поля только для чтения из этого строгого ограничения, потому что такие поля по определению почти никогда не изменяются.
❌ ИЗБЕГАЙТЕ предоставления открытых или защищённых полей экземпляра. Для доступа к полям нужно предоставлять свойства.
✅ ИСПОЛЬЗУЙТЕ константные поля для констант, которые никогда не изменятся. Компилятор записывает значения полей const непосредственно в вызывающий код. Следовательно, значения const никогда не могут быть изменены без риска нарушения совместимости.
✅ ИСПОЛЬЗУЙТЕ открытые статические поля только для чтения для предопределенных экземпляров объекта.
❌ ИЗБЕГАЙТЕ присваивания экземпляров изменяемых типов полям только для чтения. Изменяемый тип - это тип с экземплярами, которые могут быть изменены после их создания. Например, массивы, большинство коллекций и потоков являются изменяемыми типами, но
System.Int32
, System.Uri
и System.String
являются неизменяемыми. Модификатор только для чтения в поле ссылочного типа предотвращает замену экземпляра, сохраненного в поле, но не препятствует изменению данных экземпляра поля путем вызова членов (свойств или методов), изменяющих экземпляр.Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать восьмой. #BestPractices
Советы по разработке членов типов
Разработка методов расширения
Методы расширения – это функция языка, которая позволяет вызывать статические методы с использованием синтаксиса вызова метода экземпляра. Эти методы должны принимать хотя бы один параметр, представляющий экземпляр, с которым должен работать метод. Класс, который определяет такие методы расширения, называется классом-спонсором, и он должен быть объявлен как статический. Чтобы использовать методы расширения, необходимо импортировать пространство имен, определяющее класс-спонсор.
❌ ИЗБЕГАЙТЕ беспорядочного определения методов расширения, особенно для типов, которыми вы не владеете. Если вы сами разрабатываете исходный код типа, попробуйте вместо этого использовать обычные методы экземпляра. Если вы не являетесь владельцем типа и хотите добавить метод, будьте очень осторожны. Широкое использование методов расширения может привести к загромождению API-интерфейсов типов, которые не предназначены для использования этих методов.
⚠️ РАССМОТРИТЕ использование методов расширения в следующих сценариях:
- Предоставление вспомогательной функциональности для каждой реализации интерфейса, если эта функциональность может быть объяснена в терминах основного интерфейса. Это связано с тем, что конкретные реализации не могут быть назначены интерфейсам (до С#8). Например, операторы LINQ to Objects реализованы как методы расширения для всех типов
- Когда метод экземпляра вводит зависимость от некоторого типа, но такая зависимость нарушает правила управления зависимостями. Например, зависимость
❌ ИЗБЕГАЙТЕ определения методов расширения для
❌ ИЗБЕГАЙТЕ помещения методов расширения в то же пространство имен, что и расширяемый тип, если это делается не для добавления методов к интерфейсам или для управления зависимостями.
⚠️ РАССМОТРИТЕ определение методов расширения в том же пространстве имен, что и расширяемый тип, если тип является интерфейсом и если методы расширения предназначены для использования в большинстве или во всех случаях.
❌ ИЗБЕГАЙТЕ определения двух или более методов расширения с одной и той же сигнатурой, даже если они находятся в разных пространствах имен.
❌ ИЗБЕГАЙТЕ определения методов расширения в пространствах имен, обычно связанных с другими функциями, чем та, что реализует метод расширения. Вместо этого определите их в пространстве имен, связанном с объектом, которому они принадлежат.
❌ ИЗБЕГАЙТЕ общего именования пространств имен, выделенных для методов расширения (например,
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке членов типов
Разработка методов расширения
Методы расширения – это функция языка, которая позволяет вызывать статические методы с использованием синтаксиса вызова метода экземпляра. Эти методы должны принимать хотя бы один параметр, представляющий экземпляр, с которым должен работать метод. Класс, который определяет такие методы расширения, называется классом-спонсором, и он должен быть объявлен как статический. Чтобы использовать методы расширения, необходимо импортировать пространство имен, определяющее класс-спонсор.
❌ ИЗБЕГАЙТЕ беспорядочного определения методов расширения, особенно для типов, которыми вы не владеете. Если вы сами разрабатываете исходный код типа, попробуйте вместо этого использовать обычные методы экземпляра. Если вы не являетесь владельцем типа и хотите добавить метод, будьте очень осторожны. Широкое использование методов расширения может привести к загромождению API-интерфейсов типов, которые не предназначены для использования этих методов.
⚠️ РАССМОТРИТЕ использование методов расширения в следующих сценариях:
- Предоставление вспомогательной функциональности для каждой реализации интерфейса, если эта функциональность может быть объяснена в терминах основного интерфейса. Это связано с тем, что конкретные реализации не могут быть назначены интерфейсам (до С#8). Например, операторы LINQ to Objects реализованы как методы расширения для всех типов
IEnumerable<T>
. Таким образом, любая реализация IEnumerable<>
автоматически поддерживает LINQ.- Когда метод экземпляра вводит зависимость от некоторого типа, но такая зависимость нарушает правила управления зависимостями. Например, зависимость
String
от System.Uri
, вероятно, нежелательна, и поэтому метод экземпляра String.ToUri()
, возвращающий System.Uri
, был бы неправильным дизайном с точки зрения управления зависимостями. Статический метод расширения Uri.ToUri(this string str)
, возвращающий System.Uri
, был бы намного лучше.❌ ИЗБЕГАЙТЕ определения методов расширения для
System.Object
.❌ ИЗБЕГАЙТЕ помещения методов расширения в то же пространство имен, что и расширяемый тип, если это делается не для добавления методов к интерфейсам или для управления зависимостями.
⚠️ РАССМОТРИТЕ определение методов расширения в том же пространстве имен, что и расширяемый тип, если тип является интерфейсом и если методы расширения предназначены для использования в большинстве или во всех случаях.
❌ ИЗБЕГАЙТЕ определения двух или более методов расширения с одной и той же сигнатурой, даже если они находятся в разных пространствах имен.
❌ ИЗБЕГАЙТЕ определения методов расширения в пространствах имен, обычно связанных с другими функциями, чем та, что реализует метод расширения. Вместо этого определите их в пространстве имен, связанном с объектом, которому они принадлежат.
❌ ИЗБЕГАЙТЕ общего именования пространств имен, выделенных для методов расширения (например,
Extensions
). Вместо этого используйте описательное имя (например, Routing
).Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать девятый. #BestPractices
Советы по разработке членов типов
Перегрузка операторов
Перегрузки операторов позволяют типам фреймворка выглядеть так, как если бы они были встроенными примитивными типами. Хотя в некоторых ситуациях это допустимо и полезно, их следует использовать с осторожностью. Существует много случаев злоупотребления, например, когда разработчики использовали перегрузку операторов для операций, которые должны быть простыми методами.
⚠️ РАССМОТРИТЕ определение перегрузки операторов в типе, который должен выглядеть как примитивный тип. Например, для
❌ ИЗБЕГАЙТЕ определения перегрузок операторов, за исключением типов, которые должны выглядеть как примитивные (встроенные) типы.
✅ ИСПОЛЬЗУЙТЕ перегрузки операторов в структурах, которые представляют числа (например,
✅ ИСПОЛЬЗУЙТЕ перегрузки операторов только в тех случаях, когда очевидно, каким будет результат операции. Например, имеет смысл вычесть один
❌ ИЗБЕГАЙТЕ перегрузки оператора, если хотя бы один из операндов не относится к типу, определяющему перегрузку.
✅ ИСПОЛЬЗУЙТЕ симметричную перегрузку операторов. Например, если вы перегружаете оператор
⚠️ РАССМОТРИТЕ предоставление методов с понятными именами, которые соответствуют каждому перегруженному оператору. Многие языки не поддерживают перегрузку операторов. По этой причине рекомендуется, чтобы типы, которые перегружали операторы, включали вторичный метод с соответствующим именем, обеспечивающим эквивалентную функциональность, например,
Перегрузка оператора
Операторы преобразования
Операторы преобразования - это унарные операторы, которые допускают преобразование из одного типа в другой. Операторы должны быть определены как статические члены либо для операнда, либо для типа возвращаемого значения. Существует два типа операторов преобразования: неявные и явные.
❌ ИЗБЕГАЙТЕ предоставления оператора преобразования, если конечные пользователи не ожидают такого преобразования.
❌ ИЗБЕГАЙТЕ определения оператора преобразования вне домена типа. Например,
❌ ИЗБЕГАЙТЕ предоставления оператора неявного преобразования, если преобразование потенциально приводит к потерям. Например, не должно быть неявного преобразования из
❌ ИЗБЕГАЙТЕ выброса исключений из неявных преобразований. Конечным пользователям очень трудно понять, что происходит, потому что они могут не знать, что происходит преобразование.
✅ ИСПОЛЬЗУЙТЕ выброс
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке членов типов
Перегрузка операторов
Перегрузки операторов позволяют типам фреймворка выглядеть так, как если бы они были встроенными примитивными типами. Хотя в некоторых ситуациях это допустимо и полезно, их следует использовать с осторожностью. Существует много случаев злоупотребления, например, когда разработчики использовали перегрузку операторов для операций, которые должны быть простыми методами.
⚠️ РАССМОТРИТЕ определение перегрузки операторов в типе, который должен выглядеть как примитивный тип. Например, для
System.String
определены операторы ==
и !=
.❌ ИЗБЕГАЙТЕ определения перегрузок операторов, за исключением типов, которые должны выглядеть как примитивные (встроенные) типы.
✅ ИСПОЛЬЗУЙТЕ перегрузки операторов в структурах, которые представляют числа (например,
System.Decimal
).✅ ИСПОЛЬЗУЙТЕ перегрузки операторов только в тех случаях, когда очевидно, каким будет результат операции. Например, имеет смысл вычесть один
DateTime
из другого DateTime
и получить TimeSpan
. Однако нецелесообразно использовать оператор логического объединения для объединения двух запросов к базе данных или использовать оператор сдвига для записи в поток.❌ ИЗБЕГАЙТЕ перегрузки оператора, если хотя бы один из операндов не относится к типу, определяющему перегрузку.
✅ ИСПОЛЬЗУЙТЕ симметричную перегрузку операторов. Например, если вы перегружаете оператор
==
, вам также следует перегрузить оператор !=
. Точно так же, если вы перегружаете оператор <
, вы также должны перегружать оператор >
и так далее.⚠️ РАССМОТРИТЕ предоставление методов с понятными именами, которые соответствуют каждому перегруженному оператору. Многие языки не поддерживают перегрузку операторов. По этой причине рекомендуется, чтобы типы, которые перегружали операторы, включали вторичный метод с соответствующим именем, обеспечивающим эквивалентную функциональность, например,
+
- Add
, ==
- Equals
, ++
- Increment
, &
- BitwiseAnd
и т.п.Перегрузка оператора
==
довольно сложна. Семантика оператора должна быть совместима с несколькими другими членами, такими как Object.Equals
.Операторы преобразования
Операторы преобразования - это унарные операторы, которые допускают преобразование из одного типа в другой. Операторы должны быть определены как статические члены либо для операнда, либо для типа возвращаемого значения. Существует два типа операторов преобразования: неявные и явные.
❌ ИЗБЕГАЙТЕ предоставления оператора преобразования, если конечные пользователи не ожидают такого преобразования.
❌ ИЗБЕГАЙТЕ определения оператора преобразования вне домена типа. Например,
Int32
, Double
и Decimal
являются числовыми типами, а DateTime
- нет. Следовательно, не должно быть оператора преобразования для преобразования Double (long)
в DateTime
. В таком случае более предпочтительным является конструктор.❌ ИЗБЕГАЙТЕ предоставления оператора неявного преобразования, если преобразование потенциально приводит к потерям. Например, не должно быть неявного преобразования из
Double
в Int32
, потому что Double
имеет более широкий диапазон, чем Int32
. Явный оператор преобразования может быть предоставлен, даже если преобразование потенциально связано с потерями.❌ ИЗБЕГАЙТЕ выброса исключений из неявных преобразований. Конечным пользователям очень трудно понять, что происходит, потому что они могут не знать, что происходит преобразование.
✅ ИСПОЛЬЗУЙТЕ выброс
System.InvalidCastException
, если вызов оператора приведения приводит к конвертации с потерями, а контракт оператора не допускает конвертации с потерями.Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести тридцатый. #BestPractices
Советы по разработке членов типов
Разработка параметров. Начало
✅ ИСПОЛЬЗУЙТЕ наименее производный тип параметра, который обеспечивает функциональность, требуемую членом. Предположим, что вы хотите разработать метод, который перечисляет коллекцию и выводит каждый элемент в консоль. Такой метод должен принимать
❌ ИЗБЕГАЙТЕ резервирования параметров. Если в какой-либо будущей версии требуется больше входных данных для члена, можно добавить перегруженную версию.
❌ ИЗБЕГАЙТЕ публичных методов, которые принимают указатели, массивы указателей или многомерные массивы в качестве параметров. Указатели и многомерные массивы относительно сложно использовать должным образом. Практически во всех случаях API могут быть изменены, чтобы не принимать эти типы в качестве параметров.
✅ НЕОБХОДИМО размещать все out параметры после всех параметров, передаваемых по значению, и всех ref параметров (исключая массивы параметров), даже если это приводит к несогласованности в порядке параметров между перегрузками. Выходные параметры можно рассматривать как дополнительные возвращаемые значения, а их группировка в конце облегчает понимание сигнатуры метода.
✅ НЕОБХОДИМО быть последовательным в именовании параметров при переопределении членов или реализации элементов интерфейса. Это лучше передает связь между методами.
Выбор между перечислениями и булевыми параметрами
✅ ИСПОЛЬЗУЙТЕ перечисления, если член имеет два или более булевых параметра.
❌ ИЗБЕГАЙТЕ булевых значений, кроме случаев, когда вы абсолютно уверены, что никогда не понадобится больше двух значений. Перечисления дают вам некоторое пространство для добавления значений в будущем.
⚠️ РАССМОТРИТЕ использование булевых значений для параметров конструктора, которые действительно имеют только два состояния и просто используются для инициализации булевых свойств.
Валидация аргументов
✅ ИСПОЛЬЗУЙТЕ валидацию аргументов открытых, защищенных или явно реализованных членов. Выбрасывайте
✅ ИСПОЛЬЗУЙТЕ
✅ ИСПОЛЬЗУЙТЕ проверку параметров-перечислений. Не предполагайте, что аргументы перечисления будут в диапазоне, определенном перечислением. CLR позволяет преобразовывать любое целочисленное значение в значение перечисления, даже если это значение не определено в перечислении. При этом использование
❗️ ЗАМЕТЬТЕ, что изменяемые аргументы (например, ссылочные типы) могут измениться после их проверки. Если член чувствителен к безопасности, сделайте копию аргумента, а затем проверьте и обработайте его.
Советы по разработке членов типов
Разработка параметров. Начало
✅ ИСПОЛЬЗУЙТЕ наименее производный тип параметра, который обеспечивает функциональность, требуемую членом. Предположим, что вы хотите разработать метод, который перечисляет коллекцию и выводит каждый элемент в консоль. Такой метод должен принимать
IEnumerable
в качестве параметра, а не ArrayList
или IList
.❌ ИЗБЕГАЙТЕ резервирования параметров. Если в какой-либо будущей версии требуется больше входных данных для члена, можно добавить перегруженную версию.
❌ ИЗБЕГАЙТЕ публичных методов, которые принимают указатели, массивы указателей или многомерные массивы в качестве параметров. Указатели и многомерные массивы относительно сложно использовать должным образом. Практически во всех случаях API могут быть изменены, чтобы не принимать эти типы в качестве параметров.
✅ НЕОБХОДИМО размещать все out параметры после всех параметров, передаваемых по значению, и всех ref параметров (исключая массивы параметров), даже если это приводит к несогласованности в порядке параметров между перегрузками. Выходные параметры можно рассматривать как дополнительные возвращаемые значения, а их группировка в конце облегчает понимание сигнатуры метода.
✅ НЕОБХОДИМО быть последовательным в именовании параметров при переопределении членов или реализации элементов интерфейса. Это лучше передает связь между методами.
Выбор между перечислениями и булевыми параметрами
✅ ИСПОЛЬЗУЙТЕ перечисления, если член имеет два или более булевых параметра.
❌ ИЗБЕГАЙТЕ булевых значений, кроме случаев, когда вы абсолютно уверены, что никогда не понадобится больше двух значений. Перечисления дают вам некоторое пространство для добавления значений в будущем.
⚠️ РАССМОТРИТЕ использование булевых значений для параметров конструктора, которые действительно имеют только два состояния и просто используются для инициализации булевых свойств.
Валидация аргументов
✅ ИСПОЛЬЗУЙТЕ валидацию аргументов открытых, защищенных или явно реализованных членов. Выбрасывайте
System.ArgumentException
или один из его подклассов, если валидация завершается неудачей. Обратите внимание, что фактическая проверка не обязательно должна происходить в открытом или защищенном члене. Она может выполняться на более низком в закрытом методе. Суть в том, чтобы все методы интерфейса, которые предоставляются конечным пользователям, проверяли аргументы.✅ ИСПОЛЬЗУЙТЕ
ArgumentNullException
, если в качестве аргумента передан null
, а параметр не поддерживает это значение.✅ ИСПОЛЬЗУЙТЕ проверку параметров-перечислений. Не предполагайте, что аргументы перечисления будут в диапазоне, определенном перечислением. CLR позволяет преобразовывать любое целочисленное значение в значение перечисления, даже если это значение не определено в перечислении. При этом использование
Enum.IsDefined
для проверки диапазона очень неэффективно с точки зрения производительности.❗️ ЗАМЕТЬТЕ, что изменяемые аргументы (например, ссылочные типы) могут измениться после их проверки. Если член чувствителен к безопасности, сделайте копию аргумента, а затем проверьте и обработайте его.