День сто девяносто шестой. #ЗаметкиНаПолях
Обработка блоков finally в итераторах
Рассмотрим следующий код:
- Если считать выполнение прерываемым на каждом вызове
- Если считать, что на самом деле на каждом
Выполним этот код:
На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
Обработка блоков finally в итераторах
Рассмотрим следующий код:
static IEnumerable<string> Iterator()Когда будет выполнен блок
{
try
{
Console.WriteLine("Перед первым yield");
yield return "первый";
Console.WriteLine("После первого yield");
yield return "второй";
Console.WriteLine("После второго yield");
}
finally
{
Console.WriteLine("Внутри finally");
}
}
finally
: - Если считать выполнение прерываемым на каждом вызове
yield return
, тогда логически они внутри блока try
, и нет надобности выполнять блок finally
каждый раз.- Если считать, что на самом деле на каждом
yield return
вызывается метод MoveNext()
итератора, то можно решить, что происходит выход из try
, и тогда finally
должен выполняться.Выполним этот код:
foreach (string value in Iterator())Вывод:
{
Console.WriteLine("Значение: {0}", value);
}
Перед первым yieldТаким образом, блок
Значение: первый
После первого yield
Значение: второй
После второго yield
Внутри finally
finally
выполнится только после окончания итерации. Это подходит под концепцию ленивого выполнения. Пока ничего сложного. Но что если мы прервём итерацию после первого значения? Выполнится ли блок finally
?На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать
MoveNext()
один раз, то блок finally
действительно никогда не будет выполнен. Однако, если использовать итератор внутри цикла foreach
, как это чаще всего происходит, то компилятор использует скрытый блок using
вокруг цикла. При выходе из блока using
происходит вызов метода Dispose()
итератора, и, соответственно, вызов всех блоков finally
. Таким образом, результатом выполнения следующего кода:foreach (string value in Iterator())будет:
{
Console.WriteLine("Значение: {0}", value);
break;
}
Перед первым yieldЭто важно при итерации по объектам, которые требуют уничтожения, таким как обработчики файлов, для предотвращения утечки ресурсов. То есть, если вы в цикле итератора читаете все строки файла, то, даже если вы прерываете цикл на середине, либо в середине цикла возникнет ошибка, файл в любом случае будет корректно закрыт.
Значение: первый
Внутри finally
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
День сто девяносто седьмой. #оффтоп
Иногда мне кажется, что самых рукожопых разработчиков мелкомягкие не увольняют, а отправляют в ссылку на проект скайпа.
Иногда мне кажется, что самых рукожопых разработчиков мелкомягкие не увольняют, а отправляют в ссылку на проект скайпа.
День сто девяносто восьмой. #ЗаметкиНаПолях
Частичные методы
В C# 3 добавлена дополнительный функционал для частичных классов - частичные методы. Это методы, объявленные без тела в одной части, а затем опционально реализованные в другой части. Частичные методы неявно являются
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
Частичные методы
В C# 3 добавлена дополнительный функционал для частичных классов - частичные методы. Это методы, объявленные без тела в одной части, а затем опционально реализованные в другой части. Частичные методы неявно являются
private
, должны возвращать void и не иметь out
параметров (можно использовать параметры ref
). Во время компиляции сохраняются только частичные методы, имеющие реализации; если частичный метод не был реализован, он и все его вызовы удаляются. Это звучит странно, но позволяет автоматически сгенерированному коду предоставлять точки перехвата (hooks) для добавления дополнительной логики в коде, написанном вручную. Это может быть реально полезно. В примере ниже объявлены два частичных метода: partial class PartialMethodsDemoВо втором файле частичного класса один из методов реализован, а другой – нет:
{
public PartialMethodsDemo()
{
OnConstruction();
}
public override string ToString()
{
string ret = "Original return value";
CustomizeToString(ref ret);
return ret;
}
partial void OnConstruction();
partial void CustomizeToString(ref string text);
}
partial class PartialMethodsDemoВ листинге первая часть кода, скорее всего, будет сгенерирована автоматически, объявляет два метода, позволяющие обеспечить дополнительное поведение в конструкторе и при получении строкового представления объекта. Вторая часть соответствует написанному вручную коду, который не требует дополнительной логики в конструкторе, но хочет изменить строковое представление, возвращаемое
{
partial void CustomizeToString(ref string text)
{
text += " - customized!";
}
}
ToString()
. Несмотря на то, что метод CustomizeToString
не может возвращать значение напрямую, он может передавать информацию вызывающему его методу через ref
параметр. Поскольку OnConstruction
не реализован, он и вызовы его удаляются компилятором.Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
This media is not supported in your browser
VIEW IN TELEGRAM
День сто девяносто девятый. #ЧтоНовенького
Поиск по коду в панели быстрого поиска
В Visual Studio 2019 версии 16.3, которая доступна для предварительного скачивания и должна выйти в конце сентября, добавлена возможность поиска по коду через панель быстрого поиска (Ctrl + Q). Теперь можно искать классы и члены классов в коде C# и VB. Результаты будут отображаться при вводе запроса, а также на отдельной вкладке Code.
Также возможен camel-case поиск. Это позволяет вводить только заглавные буквы имени класса или члена вместо полного имени.
Источник: https://devblogs.microsoft.com/visualstudio/code-recent-items-and-template-search-in-visual-studio/
Поиск по коду в панели быстрого поиска
В Visual Studio 2019 версии 16.3, которая доступна для предварительного скачивания и должна выйти в конце сентября, добавлена возможность поиска по коду через панель быстрого поиска (Ctrl + Q). Теперь можно искать классы и члены классов в коде C# и VB. Результаты будут отображаться при вводе запроса, а также на отдельной вкладке Code.
Также возможен camel-case поиск. Это позволяет вводить только заглавные буквы имени класса или члена вместо полного имени.
Источник: https://devblogs.microsoft.com/visualstudio/code-recent-items-and-template-search-in-visual-studio/
День двухсотый. #ЗаметкиНаПолях
Псевдонимы пространств имён
Псевдонимы пространств имён используются, когда вам нужно обратиться к типам с одинаковым именем из разных пространств имён. Следующий пример показывает, как один метод может обратиться к классам Button из Windows Forms и ASP.NET Web Forms.
Несмотря на то, что типы крайне редко объявляются в глобальном пространстве имён, его можно использовать как «корневое» пространство имён. В C#2 введён псевдоним
Внешние псевдонимы
В крайнем случае, когда два типа с одинаковыми именами находятся в пространствах имен с одинаковыми именами, но предоставлены в разных сборках, можно воспользоваться внешними псевдонимами, используя ключевые слова
Псевдонимы пространств имён
Псевдонимы пространств имён используются, когда вам нужно обратиться к типам с одинаковым именем из разных пространств имён. Следующий пример показывает, как один метод может обратиться к классам Button из Windows Forms и ASP.NET Web Forms.
using System;Это хорошо работает до тех пор, пока не появляется пространство имён с именем
using WinForms = System.Windows.Forms;
using WebForms = System.Web.UI.WebControls;
class Test
{
static void Main()
{
Console.WriteLine(typeof(WinForms.Button));
Console.WriteLine(typeof(WebForms.Button));
}
}
WinForms
. Чтобы различать названия пространств имён и их псевдонимы в C#2 введены квалификаторы псевдонимов пространств имён, представляющие из себя просто пару двоеточий. Таким образом, код выше можно переписать так:static void Main()Псевдоним глобального пространства имён
{
Console.WriteLine(typeof(WinForms::Button));
Console.WriteLine(typeof(WebForms::Button));
}
Несмотря на то, что типы крайне редко объявляются в глобальном пространстве имён, его можно использовать как «корневое» пространство имён. В C#2 введён псевдоним
global
, обозначающий глобальное пространство имён. Например, при использовании типа DateTime
, если он объявлен во нескольких пространствах имён, то обратиться к системному типу можно, использовав следующую конструкцию global::System.DateTime
. Использовать псевдоним глобального пространства имён полезно, например, при написании автоматических генераторов кода, когда есть большая вероятность коллизий сгенерированных названий пространств имён и типов.Внешние псевдонимы
В крайнем случае, когда два типа с одинаковыми именами находятся в пространствах имен с одинаковыми именами, но предоставлены в разных сборках, можно воспользоваться внешними псевдонимами, используя ключевые слова
extern alias
:extern alias GridV1;Связать файл сборки с именем внешнего псевдонима можно либо в файле конфигурации, либо указав параметр
extern alias GridV2;
using GridV1::Grid;
using GridV2::Grid;
/r
компилятору в командной строке:/r:GridV1=grid.dllИсточник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
/r:GridV2=grid20.dll
День двести первый. #BestPractices
Советы по разработке типов
1. Выбор между классом и структурой
Хорошее понимание различий в поведении ссылочных и значимых типов имеет решающее значение при принятии решения в этом вопросе:
- Инициализация и уничтожение значимых типов обычно дешевле.
- Инициализация и уничтожение массивов значимых типов намного дешевле.
- При приведении значимого типа к ссылочному или к интерфейсному типу и обратно выполняется упаковка и распаковка. Это отрицательно влияет на кучу, скорость сборки мусора и общую производительность.
- Ссылочные типы копируются по ссылке, а значимые по значению, т.е. присвоение большого объекта ссылочного типа дешевле, чем значимого.
- Ссылочные типы передаются по ссылке, то есть изменение экземпляра ссылочного типа изменяет все переменные, которые ссылаются на него. Когда же изменяется переменная значимого типа, это не влияет на копии элемента. Поэтому во избежание путаницы структуры делают неизменяемыми.
Большинство типов должны быть классами (ссылочными типами). Однако есть ситуации, когда характеристики значимого типа подойдут для использования структуры (struct).
⚠️ РАССМОТРИТЕ определение объекта как структуры, а не класса, если экземпляры типа небольшие и живут недолго, либо включены в другие объекты.
✅ ИСПОЛЬЗУЙТЕ структуры, когда тип обладает всеми следующими характеристиками:
- Логически представляет из себя единое значение.
- Размер экземпляра типа меньше 16 байт.
- Он неизменяем.
- Его не придётся часто упаковывать.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
1. Выбор между классом и структурой
Хорошее понимание различий в поведении ссылочных и значимых типов имеет решающее значение при принятии решения в этом вопросе:
- Инициализация и уничтожение значимых типов обычно дешевле.
- Инициализация и уничтожение массивов значимых типов намного дешевле.
- При приведении значимого типа к ссылочному или к интерфейсному типу и обратно выполняется упаковка и распаковка. Это отрицательно влияет на кучу, скорость сборки мусора и общую производительность.
- Ссылочные типы копируются по ссылке, а значимые по значению, т.е. присвоение большого объекта ссылочного типа дешевле, чем значимого.
- Ссылочные типы передаются по ссылке, то есть изменение экземпляра ссылочного типа изменяет все переменные, которые ссылаются на него. Когда же изменяется переменная значимого типа, это не влияет на копии элемента. Поэтому во избежание путаницы структуры делают неизменяемыми.
Большинство типов должны быть классами (ссылочными типами). Однако есть ситуации, когда характеристики значимого типа подойдут для использования структуры (struct).
⚠️ РАССМОТРИТЕ определение объекта как структуры, а не класса, если экземпляры типа небольшие и живут недолго, либо включены в другие объекты.
✅ ИСПОЛЬЗУЙТЕ структуры, когда тип обладает всеми следующими характеристиками:
- Логически представляет из себя единое значение.
- Размер экземпляра типа меньше 16 байт.
- Он неизменяем.
- Его не придётся часто упаковывать.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести второй. #BestPractices
Советы по разработке типов
2. Разработка абстрактных классов
❌ ИЗБЕГАЙТЕ определения открытых (
✅ ИСПОЛЬЗУЙТЕ защищённые (
✅ ИСПОЛЬЗУЙТЕ хотя бы один конкретный тип, наследующий от каждого абстрактного класса. Это помогает проверить дизайн абстрактного класса. Например,
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
2. Разработка абстрактных классов
❌ ИЗБЕГАЙТЕ определения открытых (
public
) или защищённых внутренних (protected internal
) конструкторов в абстрактных типах. Конструкторы должны быть открытыми, только если пользователям нужно будет создавать экземпляры типа. Поскольку вы не можете создавать экземпляры абстрактного типа, абстрактный тип с открытым конструктором неправильно спроектирован и вводит пользователей класса в заблуждение.✅ ИСПОЛЬЗУЙТЕ защищённые (
protected
) или внутренние (internal
) конструкторы в абстрактных классах. Защищенный конструктор является более распространенным и позволяет базовому классу выполнять собственную инициализацию при создании подтипов. Внутренний конструктор может использоваться для ограничения конкретных реализаций абстрактного класса только сборкой, определяющей класс.✅ ИСПОЛЬЗУЙТЕ хотя бы один конкретный тип, наследующий от каждого абстрактного класса. Это помогает проверить дизайн абстрактного класса. Например,
System.IO.FileStream
является реализацией абстрактного класса System.IO.Stream
.Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести третий. #ЗаметкиНаПолях
Захват переменных лямбда-выражениями
Внутри лямбда-выражения вы можете использовать любую переменную, которую вы могли бы использовать в обычном коде в данном месте. Это может быть статическое поле, поле экземпляра (если вы пишете лямбда-выражение внутри экземплярного метода), переменная this, параметры метода или локальные переменные. Все они называются захваченными переменными, потому что они объявленны вне непосредственного контекста лямбда-выражения. Для сравнения параметры лямбда-выражения или локальные переменные, объявленные внутри лямбда-выражения не являются захваченными переменными. В следующем примере показан захват различных переменных лямбда-выражением:
-
захватывается лямбда-выражением.
-
-
-
-
-
Важно понимать, что лямбда-выражение захватывает сами переменные, а не значения переменных в момент создания делегата. Если вы изменили какую-либо из захваченных переменных между временем создания делегата и его вызовом, лямбда-выражение «увидит» эти изменения. Аналогично, лямбда-выражение может изменить значение захваченных переменных. Более того, в этом случае каждый последующий вызов лямбда-выражения будет «видеть» изменения захваченных переменных, сделанные предыдущими вызовами.
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 3.
Захват переменных лямбда-выражениями
Внутри лямбда-выражения вы можете использовать любую переменную, которую вы могли бы использовать в обычном коде в данном месте. Это может быть статическое поле, поле экземпляра (если вы пишете лямбда-выражение внутри экземплярного метода), переменная this, параметры метода или локальные переменные. Все они называются захваченными переменными, потому что они объявленны вне непосредственного контекста лямбда-выражения. Для сравнения параметры лямбда-выражения или локальные переменные, объявленные внутри лямбда-выражения не являются захваченными переменными. В следующем примере показан захват различных переменных лямбда-выражением:
class CapturedVariablesDemo…
{
private string instanceField = "instance field";
public Action<string> CreateAction(string methodParameter)
{
string methodLocal = "method local";
string uncaptured = "uncaptured local";
Action<string> action = lambdaParameter =>
{
string lambdaLocal = "lambda local";
Console.WriteLine("Instance field: {0}", instanceField);
Console.WriteLine("Method parameter: {0}", methodParameter);
Console.WriteLine("Method local: {0}", methodLocal);
Console.WriteLine("Lambda parameter: {0}", lambdaParameter);
Console.WriteLine("Lambda local: {0}", lambdaLocal);
};
methodLocal = "modified method local";
return action;
}
}
var demo = new CapturedVariablesDemo();Вывод:
Action<string> action = demo.CreateAction("method argument");
action("lambda argument");
Instance field: instance fieldВ этом примере задействованы следующие переменные:
Method parameter: method argument
Method local: modified method local
Lambda parameter: lambda argument
Lambda local: lambda local
-
instanceField
- поле экземпляра класса CapturedVariablesDemo
,захватывается лямбда-выражением.
-
methodParameter
- параметр метода CreateAction
, захватывается лямбда-выражением.-
methodLocal
- локальная переменная метода CreateAction
, захватывается и изменяется лямбда-выражением.-
uncaptured
- локальная переменная метода CreateAction
, не используется лямбда-выражением, поэтому не захватывается им.-
lambdaParameter
- параметр лямбда-выражения, не является захваченной переменной.-
lambdaLocal
- локальная переменная в лямбда-выражении, не является захваченной переменной.Важно понимать, что лямбда-выражение захватывает сами переменные, а не значения переменных в момент создания делегата. Если вы изменили какую-либо из захваченных переменных между временем создания делегата и его вызовом, лямбда-выражение «увидит» эти изменения. Аналогично, лямбда-выражение может изменить значение захваченных переменных. Более того, в этом случае каждый последующий вызов лямбда-выражения будет «видеть» изменения захваченных переменных, сделанные предыдущими вызовами.
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 3.
День двести четвёртый. #BestPractices
Советы по разработке типов
3. Разработка Статических Классов
Статический класс содержит только статические члены (кроме экземплярных членов, унаследованных от
Статические классы - это компромисс между чисто объектно-ориентированным дизайном и простотой. Они обычно используются для предоставления ярлыков для других операций (например,
✅ ИСПОЛЬЗУЙТЕ статические классы в исключительных случаях. Статические классы должны использоваться только как вспомогательные классы для объектно-ориентированного ядра платформы.
❌ ИЗБЕГАЙТЕ использования статических классов для хранения разнородного функционала.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
3. Разработка Статических Классов
Статический класс содержит только статические члены (кроме экземплярных членов, унаследованных от
System.Object
и, возможно, приватного конструктора). Некоторые языки предоставляют встроенную поддержку статических классов. В C# статический класс неявно закрытый (sealed), абстрактный (abstract), и никакие экземплярные члены не могут быть переопределены или объявлены.Статические классы - это компромисс между чисто объектно-ориентированным дизайном и простотой. Они обычно используются для предоставления ярлыков для других операций (например,
System.IO.File
), хранителем методов расширения или функциональных возможностей, для которых реализация полностью объектно-ориентированной оболочки невозможно (например, System.Environment
).✅ ИСПОЛЬЗУЙТЕ статические классы в исключительных случаях. Статические классы должны использоваться только как вспомогательные классы для объектно-ориентированного ядра платформы.
❌ ИЗБЕГАЙТЕ использования статических классов для хранения разнородного функционала.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
This media is not supported in your browser
VIEW IN 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/
Советы по разработке типов
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/
Docs
Framework Design Guidelines
See framework design guidelines for designing libraries that extend and interact with .NET, to ensure API consistency and ease of use.
День двести седьмой. #BestPractices
Советы по разработке типов
5. Разработка структур
Значимый тип общего назначения чаще всего называют структурой (ключевое слово struct).
❌ ИЗБЕГАЙТЕ конструкторов без параметров для структур. Это позволяет создавать массивы структур без необходимости запуска конструктора для каждого элемента массива. C# не позволяет структурам иметь конструкторы без параметров.
❌ ИЗБЕГАЙТЕ изменяемых структур. С изменяемыми структурами возникает несколько проблем. Например, когда аксессор get свойства возвращает структуру, вызывающая сторона получает копию. Поскольку копия создаётся неявно, разработчики могут не знать, что они изменяют копию, а не исходное значение. Кроме того, некоторые языки (в частности, динамические языки) имеют проблемы с использованием изменяемых значимых типов, потому что даже локальные переменные, при разыменовании, приводят к созданию копии.
⚠️ УБЕДИТЕСЬ, что состояние, в котором все данные экземпляра установлены на ноль, ложь или null (в зависимости от ситуации), является допустимым. Это предотвращает случайное создание недопустимых экземпляров при создании массива структур.
✅ ИСПОЛЬЗУЙТЕ реализацию IEquatable<T> для структур. Метод Object.Equals для значимых типов приводит к упаковке, и его реализация по умолчанию не очень эффективна, поскольку использует отражение. Метод Equals может иметь гораздо лучшую производительность и может быть реализован так, чтобы это не приводило к упаковке.
❌ ИЗБЕГАЙТЕ явного наследования от ValueType. На самом деле, большинство языков запрещают это.
В общем случае структуры могут быть очень полезны, но их следует использовать только для небольших, единичных неизменяемых значений, которые не будут часто упаковываться.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
5. Разработка структур
Значимый тип общего назначения чаще всего называют структурой (ключевое слово struct).
❌ ИЗБЕГАЙТЕ конструкторов без параметров для структур. Это позволяет создавать массивы структур без необходимости запуска конструктора для каждого элемента массива. C# не позволяет структурам иметь конструкторы без параметров.
❌ ИЗБЕГАЙТЕ изменяемых структур. С изменяемыми структурами возникает несколько проблем. Например, когда аксессор get свойства возвращает структуру, вызывающая сторона получает копию. Поскольку копия создаётся неявно, разработчики могут не знать, что они изменяют копию, а не исходное значение. Кроме того, некоторые языки (в частности, динамические языки) имеют проблемы с использованием изменяемых значимых типов, потому что даже локальные переменные, при разыменовании, приводят к созданию копии.
⚠️ УБЕДИТЕСЬ, что состояние, в котором все данные экземпляра установлены на ноль, ложь или null (в зависимости от ситуации), является допустимым. Это предотвращает случайное создание недопустимых экземпляров при создании массива структур.
✅ ИСПОЛЬЗУЙТЕ реализацию IEquatable<T> для структур. Метод Object.Equals для значимых типов приводит к упаковке, и его реализация по умолчанию не очень эффективна, поскольку использует отражение. Метод Equals может иметь гораздо лучшую производительность и может быть реализован так, чтобы это не приводило к упаковке.
❌ ИЗБЕГАЙТЕ явного наследования от ValueType. На самом деле, большинство языков запрещают это.
В общем случае структуры могут быть очень полезны, но их следует использовать только для небольших, единичных неизменяемых значений, которые не будут часто упаковываться.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Docs
Framework Design Guidelines
See framework design guidelines for designing libraries that extend and interact with .NET, to ensure API consistency and ease of use.
День двести восьмой. #BestPractices
Советы по разработке типов
6. Разработка перечислений. Начало
Перечисления - это особый значимый тип.
Виды перечислений:
- Простые - небольшие замкнутые наборы вариантов (например, набор цветов).
- Битовые флаги - предназначены для поддержки побитовых операций над значениями (например, список опций).
✅ ИСПОЛЬЗУЙТЕ существительные в единственном числе для простых перечислений и во множественном для битовых флагов.
❌ ИЗБЕГАЙТЕ наследования от
✅ ИСПОЛЬЗУЙТЕ перечисление для строгого ввода параметров, свойств и возвращаемых значений, представляющих наборы значений.
✅ ИСПОЛЬЗУЙТЕ перечисление вместо статических констант.
❌ ИЗБЕГАЙТЕ использования перечислений для открытых наборов (таких как версии операционной системы, имена ваших друзей и т.д.).
❌ ИЗБЕГАЙТЕ создания зарезервированных значений перечисления, которые предназначены для будущего использования. Вы всегда можете просто добавить значения к существующему перечислению на более позднем этапе. Зарезервированные значения просто загрязняют набор реальных значений и приводят к ошибкам при использовании.
❌ ИЗБЕГАЙТЕ создания перечисления только с одним значением. Обычной практикой обеспечения будущей расширяемости API является добавление зарезервированных параметров в сигнатуры методов. Такие зарезервированные параметры могут быть выражены в виде перечисления с одним значением по умолчанию. Это неверная практика для управляемых API. Перегрузка метода позволяет добавлять параметры в будущих выпусках.
❌ ИЗБЕГАЙТЕ включения сервисных значений в перечисления. Несмотря на то, что они иногда полезны для разработчиков фреймворка, сервисные значения сбивают с толку его пользователей. Они используются для отслеживания состояния перечисления, а не являются одним из значений из набора, представляемого перечислением.
✅ ИСПОЛЬЗУЙТЕ нулевое значение для простых перечислений. Вы можете назвать это значение «None» («Значение отсутствует»). Если такое значение не подходит для данного конкретного перечисления, наиболее распространенному значению по умолчанию для перечисления должно быть присвоено базовое значение ноль.
⚠️ РАССМОТРИТЕ использование Int32 (по умолчанию в большинстве языков) в качестве базового типа перечисления, если не выполняется одно из следующих условий:
- Перечисление представляет собой битовые флаги, и у вас более 32 флагов или вы ожидаете, что их будет больше в будущем.
- Базовый тип отличается от Int32 для облегчения взаимодействия с неуправляемым кодом, ожидающим перечисления разных размеров.
- Меньший по размеру базовый тип приведёт к значительной экономии места. Если вы ожидаете, что перечисление будет использоваться главным образом в качестве аргумента в логике кода, размер не имеет большого значения. Экономия по размеру может быть значительной, если ожидается, что:
- перечисление будет использоваться как поле в очень часто создаваемой структуре или классе.
- пользователи будут создавать большие массивы или коллекции экземпляров перечисления.
- большое количество экземпляров перечисления будет сериализовываться.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
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. Разработка перечислений. Окончание
Разработка битовых флагов
✅ ИСПОЛЬЗУЙТЕ атрибут
✅ ИСПОЛЬЗУЙТЕ степени двойки для значений битовых флагов, чтобы их можно было свободно комбинировать с помощью операции побитового ИЛИ.
⚠️ РАССМОТРИТЕ предоставление специальных значений для часто используемых комбинаций флагов. Побитовые операции не должны требоваться для простых задач. Например:
❌ ИЗБЕГАЙТЕ создания битовых флагов, где определенные комбинации значений недопустимы.
✅ ИСПОЛЬЗУЙТЕ название
❌ ИЗБЕГАЙТЕ использования нулевого значения битовых флагов, если только оно не означает «все флаги сняты» и не названо соответствующим образом.
Добавление значения к перечислению
Очень часто обнаруживается, что вам нужно добавить значения в перечисление после того, как оно уже выпущено в одной из версий программы. Существует потенциальная проблема совместимости приложений при возвращении нового значения из существующего API, поскольку плохо написанные приложения могут неправильно обрабатывать новое значение.
⚠️ РАССМОТРИТЕ добавление значений к перечислениям, несмотря на небольшой риск несовместимости.
Если у вас есть реальные данные о несовместимости приложений, вызванной добавлением значений в перечисление, рассмотрите возможность добавления нового API, который возвращает и новые, и старые значения, и пометьте старый API как устаревший, который должен продолжать возвращать только старые значения. Это обеспечит совместимость существующих приложений.
Продолжение следует…
Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
Советы по разработке типов
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/
Советы по разработке типов
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: Видео, конечно, на английском.
Периодически смотрю видео от канадского блоггера 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("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. Продолжение
Определение асинхронных методов
Синтаксис для объявления асинхронного метода точно такой же, как и для любого другого метода, за исключением того, что он должен включать ключевое слово
Асинхронные функции ограничены следующими типами возврата:
-
Типы
Хотя тип результата асинхронных методов довольно жестко ограничен, большинство других аспектов не отличаются от обычных методов: асинхронные методы могут быть обобщёнными, статическими или нестатическими и определять любые модификаторы доступа.
Однако существуют ограничения на параметры. Ни один из параметров асинхронного метода не может использовать модификаторы
Продолжение следует…
Источник: 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.