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

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День двести восемнадцатый. #ЗаметкиНаПолях
Многопоточность.
9. Async/await. Продолжение
Асинхронные анонимные функции
Асинхронные анонимные функции создаются, как и любой другой анонимный метод или лямбда-выражение, просто добавлением модификатора async в начале:
Func<Task> lambda = async () => await Task.Delay(1000);
Func<Task<int>> anonMethod = async delegate()
{
Console.WriteLine("Started");
await Task.Delay(1000);
Console.WriteLine("Finished");
return 10;
};
Делегат должен иметь сигнатуру с типом возврата, подходящим для асинхронного метода (void, Task, Task <TResult> в C#5 и 6 или пользовательским типом задачи в C#7). Он может захватывать переменные, как и другие анонимные функции, и иметь параметры. Кроме того, асинхронная операция не запускается до тех пор, пока не будет вызван делегат, а несколько вызовов делегата создают несколько операций.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести двадцать шестой. #BestPractices
Советы по разработке членов типов
Разработка событий
События являются наиболее часто используемой формой обратных вызовов. Существует две группы событий: события, возникающие до изменения состояния системы, называемые пред-событиями, и события, возникающие после изменения состояния, называемые пост-событиями. Например, пред-событие 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 никогда не могут быть изменены без риска нарушения совместимости.
ИСПОЛЬЗУЙТЕ открытые статические поля только для чтения для предопределенных экземпляров объекта.
ИЗБЕГАЙТЕ присваивания экземпляров изменяемых типов полям только для чтения. Изменяемый тип - это тип с экземплярами, которые могут быть изменены после их создания. Например, массивы, большинство коллекций и потоков являются изменяемыми типами, но System.Int32, System.Uri и System.String являются неизменяемыми. Модификатор только для чтения в поле ссылочного типа предотвращает замену экземпляра, сохраненного в поле, но не препятствует изменению данных экземпляра поля путем вызова членов (свойств или методов), изменяющих экземпляр.

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

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

ИЗБЕГАЙТЕ беспорядочного определения методов расширения, особенно для типов, которыми вы не владеете. Если вы сами разрабатываете исходный код типа, попробуйте вместо этого использовать обычные методы экземпляра. Если вы не являетесь владельцем типа и хотите добавить метод, будьте очень осторожны. Широкое использование методов расширения может привести к загромождению 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
Советы по разработке членов типов
Перегрузка операторов
Перегрузки операторов позволяют типам фреймворка выглядеть так, как если бы они были встроенными примитивными типами. Хотя в некоторых ситуациях это допустимо и полезно, их следует использовать с осторожностью. Существует много случаев злоупотребления, например, когда разработчики использовали перегрузку операторов для операций, которые должны быть простыми методами.

⚠️ РАССМОТРИТЕ определение перегрузки операторов в типе, который должен выглядеть как примитивный тип. Например, для 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
Советы по разработке членов типов
Разработка параметров. Начало
ИСПОЛЬЗУЙТЕ наименее производный тип параметра, который обеспечивает функциональность, требуемую членом. Предположим, что вы хотите разработать метод, который перечисляет коллекцию и выводит каждый элемент в консоль. Такой метод должен принимать IEnumerable в качестве параметра, а не ArrayList или IList.
ИЗБЕГАЙТЕ резервирования параметров. Если в какой-либо будущей версии требуется больше входных данных для члена, можно добавить перегруженную версию.
ИЗБЕГАЙТЕ публичных методов, которые принимают указатели, массивы указателей или многомерные массивы в качестве параметров. Указатели и многомерные массивы относительно сложно использовать должным образом. Практически во всех случаях API могут быть изменены, чтобы не принимать эти типы в качестве параметров.
НЕОБХОДИМО размещать все out параметры после всех параметров, передаваемых по значению, и всех ref параметров (исключая массивы параметров), даже если это приводит к несогласованности в порядке параметров между перегрузками. Выходные параметры можно рассматривать как дополнительные возвращаемые значения, а их группировка в конце облегчает понимание сигнатуры метода.
НЕОБХОДИМО быть последовательным в именовании параметров при переопределении членов или реализации элементов интерфейса. Это лучше передает связь между методами.

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

Валидация аргументов
ИСПОЛЬЗУЙТЕ валидацию аргументов открытых, защищенных или явно реализованных членов. Выбрасывайте System.ArgumentException или один из его подклассов, если валидация завершается неудачей. Обратите внимание, что фактическая проверка не обязательно должна происходить в открытом или защищенном члене. Она может выполняться на более низком в закрытом методе. Суть в том, чтобы все методы интерфейса, которые предоставляются конечным пользователям, проверяли аргументы.
ИСПОЛЬЗУЙТЕ ArgumentNullException, если в качестве аргумента передан null, а параметр не поддерживает это значение.
ИСПОЛЬЗУЙТЕ проверку параметров-перечислений. Не предполагайте, что аргументы перечисления будут в диапазоне, определенном перечислением. CLR позволяет преобразовывать любое целочисленное значение в значение перечисления, даже если это значение не определено в перечислении. При этом использование Enum.IsDefined для проверки диапазона очень неэффективно с точки зрения производительности.
❗️ ЗАМЕТЬТЕ, что изменяемые аргументы (например, ссылочные типы) могут измениться после их проверки. Если член чувствителен к безопасности, сделайте копию аргумента, а затем проверьте и обработайте его.
Передача параметров
Группы параметров по способу передачи в метод:
1. Параметры, передаваемые по значению. Член получает копию фактически переданного аргумента. Если аргумент является значимым типом, копия аргумента помещается в стек. Если аргумент является ссылочным типом, в стек помещается копия ссылки на него. Самые популярные языки CLR, такие как C#, VB.NET и C++, по умолчанию передают параметры по значению.
2. Ref параметры. Член получает ссылку на фактический переданный аргумент. Если аргумент является значимым типом, ссылка на аргумент помещается в стек. Если аргумент является ссылочным типом, в стек помещается ссылка на ссылку. Ref параметры могут использоваться, чтобы позволить члену изменять аргументы, переданные вызывающей стороной.
3. Out параметры. Аналогичны ref параметрам с некоторыми небольшими отличиями. Параметр изначально считается неопределённым и не может быть прочитан в теле члена, пока ему не присвоено какое-либо значение. Кроме того, параметру должно быть присвоено некоторое значение, прежде чем член вернёт управление.

ИЗБЕГАЙТЕ использования параметры out или ref, т.к. это требует опыта работы с указателями, понимания различий между значимыми и ссылочными типами и работы с методами с несколькими возвращаемыми значениями. Кроме того, разработчики фреймворков, проектирующие для широкой аудитории, не должны ожидать от пользователей фреймворка полного понимания различий и принципов работы с out или ref параметрами.
ИЗБЕГАЙТЕ передачи ссылочных типов по ссылке. Есть лишь несколько исключений, например, метод, который используется для обмена ссылками.

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

Источник:
https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести тридцать первый. #BestPractices
Советы по разработке членов типов
Разработка параметров. Окончание
Члены с переменным числом параметров
Члены, которые могут принимать переменное число аргументов, создаются путем предоставления параметра-массива. Например, String предоставляет следующий метод:
public class String {
public static string Format(string format, object[] parameters);
}
Пользователь может вызвать метод String.Format следующим образом:
String.Format("Файл {0} не найден в {1}", new object[] { filename, directory });
Добавление ключевого слова params к параметру-массиву изменяет обеспечивает упрощённое создание временного массива:
public class String {
public static string Format(string format, params object[] parameters);
}
Это позволяет пользователю вызывать метод, передавая элементы массива непосредственно в список аргументов:
String.Format("Файл {0} не найден в {1}", filename, directory);
Обратите внимание, что ключевое слово params можно добавить только к последнему параметру.

⚠️ РАССМОТРИТЕ добавление ключевого слова params к параметру-массиву, если вы ожидаете, что конечные пользователи будут передавать массивы с небольшим количеством элементов. Если ожидается, что обычно будет передаваться много элементов, пользователи, вероятно, не будут передавать все эти элементы по одиночке, поэтому ключевое слово params не нужно.
ИЗБЕГАЙТЕ использования params, если вызывающая сторона почти всегда будет иметь данные уже в массиве. Например, члены с параметрами байтового массива почти никогда не будут вызываться путем передачи им отдельных байтов. По этой причине такие параметры в .NET Framework не используют ключевое слово params.
ИЗБЕГАЙТЕ использования params, если массив изменяется членом, принимающим параметр params. Из-за того, что многие компиляторы превращают аргументы params во временный массив, любые изменения в таком массиве будут потеряны.
⚠️ РАССМОТРИТЕ использование ключевого слова params в простой перегрузке, даже если более сложная перегрузка не может его использовать. Решите, оценят ли пользователи наличие массива params в одной перегрузке, даже если он будет не во всех перегрузках.
НЕОБХОДИМО так упорядочить параметры, чтобы можно было использовать ключевое слово params.
⚠️ РАССМОТРИТЕ использование специальных перегрузок для вызовов с небольшим количеством аргументов в чрезвычайно чувствительных к производительности API. Это позволяет избежать создания объектов массива, когда API вызывается с небольшим количеством аргументов.
❗️ ЗАМЕТЬТЕ, что null может быть передан в качестве аргумента params. Вы должны проверить, что массив не равен null перед его обработкой.

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

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести тридцать второй. #юмор
День двести тридцать третий. #ЗаметкиНаПолях
Форматирование строк
Статический метод Format класса string принимает составную строку формата, включающую элементы формата, а затем аргументы для замены этих элементов. Например:
string.Format("Привет, {0}.", name);
имеет один элемент формата {0} и заменяет его на значение переменной name. Каждый элемент формата в составной строке формата указывает индекс аргумента, который должен быть отформатирован, но он также может указывать следующие параметры для форматирования значения:
- Выравнивание – указывается минимальная ширина элемента. При этом выравнивание по правому краю обозначается положительным значением, а; выравнивание по левому краю - отрицательным. Указывается через запятую после индекса элемента.
- Строка формата для значения. Чаще всего используется для значений даты и времени или чисел. Например, чтобы отформатировать дату в соответствии с ISO-8601, вы можете использовать строку формата "yyyy-MM-dd". Чтобы отформатировать число в качестве значения валюты, вы можете использовать строку формата "C". Значение строки формата зависит от типа форматируемого значения и указывается после двоеточия.
Например:
decimal price = 95.25m;
string tag = string.Format("Цена: {0,9:C}", price);
Console.Write(tag);
Выведет:
Цена:    $95.25
Формат оставляет 9 символов для значения, выравнивая его по правому краю, а также добавляет символ валюты.
Для вывода в консоль отформатированной строки Console.Write можно использовать непосредственно аналогично string.Format:
Console.Write("Цена: {0,9:C}", price);

Локализация
В общих чертах, локализация - это задача убедиться, что ваш код работает правильно для всех ваших пользователей, независимо от того, где они находятся. В .Net для целей локализации применяется класс CultureInfo. Он содержит настройки культур для языка (вариации языка) или страны: летоисчисление, название дней недели и месяцев, разделители чисел, символ валюты и т.п.
Следующий код выведет в консоль список всех культур, сохранённых на данной машине, и отформатирует дату в соответствии с каждой культурой:
var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
var birthDate = new DateTime(1986, 2, 18);
foreach (var culture in cultures)
{
string text = string.Format(
culture, "{0,-15} {1,12:d}", culture.Name, birthDate);
Console.WriteLine(text);
}
Заметьте, что в отличие от обычных случаев, когда дополнительный необязательный параметр добавляется в конец списка в перегруженной версии метода, культура в методе string.Format указывается первым значением. Это связано с тем, что последним значением указан параметр-массив для аргументов замены (см. https://t.iss.one/NetDeveloperDiary/269).
Чтобы создать объект культуры, нужно использовать метод CultureInfo.GetCultureInfo, передав в него имя культуры. Например, CultureInfo.GetCultureInfo("ru-RU") для русского языка. Кроме того, можно использовать текущую культуру, установленную на машине с помощью статического свойства CultureInfo.CurrentCulture. Это значение используется по умолчанию в методе string.Format. Однако его следует использовать с осторожностью, если предполагается, что приложение будет запускаться на разных компьютерах, т.к. культуры по умолчанию на них могут различаться. Для передачи данных между компьютерами, например, даты в формате ISO-8601 (yyyy-MM-dd), можно использовать инвариантную культуру через статическое свойство CultureInfo.InvariantCulture.

Дословные (verbatim) строки
Символ @, поставленный перед строковым литералом, сообщает конструктору строки, что нужно игнорировать символы перевода строки и символы экранирования. При этом, чтобы использовать двойные кавычки в буквальной строке, надо удвоить символ. Таким образом строку "C:\\\"Program Files\"\\IIS" можно записать как @"C:\""Program Files""\IIS".
Интерполированные строки
В C#6 форматирование строк упрощено с введением интерполированных строк. Специальный знак $ идентифицирует строковый литерал как интерполированную строку. Вместо индекса элемента формата используется выражение интерполяции. Пример выше можно переписать вот так:
string tag = $"Цена: {price,9:C}";
Замечания:
- Чтобы включить в интерполированную строку фигурные скобки, используйте повторение символа "{{" и "}}".
- Чтобы использовать тернарный условный оператор, надо заключить его в скобки: $"{name} is {age} year{(age == 1 ? "" : "s")} old."
- Буквальные интерполированные строки до C#8 обозначались как $@"...", начиная с C#8 порядок символов $ и @ значения не имеет.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 9.
День двести тридцать четвёртый. #ЗаметкиНаПолях
Использование nameof
С точки зрения синтаксиса, оператор nameof похож на typeof, за исключением того, что идентификатор в скобках не обязательно должен быть типом. Почему же лучше использовать nameof вместо строковых литералов? Это надёжнее. Если вы сделаете опечатку в строковом литерале, вам нечего укажет на неё, тогда как, если вы сделаете опечатку в имени операнда, вы получите ошибку во время компиляции. Кроме того, компилятор знает, что аргумент nameof связан с элементом или переменной. Если вы переименуете переменную, используя инструмент рефакторинга, имя аргумента тоже изменится. Однако заметьте, что компилятор по-прежнему не сможет определить проблему, если вы ссылаетесь на другой элемент с похожим именем.

Примеры использования
1. Валидация аргументов
При программировании по контракту каждый метод проверяет входящие данные. Это называется предусловиями. Например, далее используется статический метод класса Preconditions для проверки аргумента на null:
public void SomeMethod(string name)
{
Preconditions.CheckNotNull(name, nameof(name));

}
2. Уведомление об изменении калькулируемого свойства
Предположим, что есть класс Rectangle со свойствами Height и Width для чтения и записи и свойство только для чтения Area. Полезно иметь возможность вызвать событие для свойства Area и указать имя свойства безопасным способом:
public class Rectangle : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public double Width
{
get { … }
set
{

RaisePropertyChanged();
RaisePropertyChanged(nameof(Area));
}
}

}
3. Атрибуты
Иногда атрибуты ссылаются на другие члены, чтобы обозначить, как члены связаны друг с другом. Это используется, например, в MVC DataAnnotations, где можно с помощью атрибутов обозначать ограничения, накладываемые на модель, например, требование обязательности поля, если поставлен флажок. NUnit позволяет параметризировать тесты значениями из поля или свойства, используя атрибут TestCaseSource. В Entity Framework достаточно распространено иметь в классе два свойства: одно для внешнего ключа, а другое - для сущности, которую представляет этот ключ:
public class Employee
{
[ForeignKey(nameof(Employer))]
public Guid EmployerId { get; set; }
public Company Employer { get; set; }
}

Особенности использования nameof
1. Указание членов других типов
Стоит помнить, что nameof возвращает только имя члена, без имени класса. Например, nameof(Cultures.AllCultures) вернёт "AllCultures"
2. Обобщения
При использовании typeof, и typeof(List<string>), и typeof(List<>) допустимы и выдают разные результаты. При использовании nameof, во-первых, нельзя использовать тип без аргумента типа nameof(List<>), а во-вторых, и nameof(Action<string>) и nameof(Action<string, string>) вернут просто "Action".
Кроме того, тип параметра не выводится во время выполнения, а используется просто имя типа параметра. То есть для
static string Method<T>() => nameof(T);
И Method<Guid>() и Method<Button>() вернут просто "T".
3. Использование псевдонимов
При использовании псевдонимов nameof также выводит имя псевдонима, а не обозначенного им типа. Следующий код выведет GuidAlias, а не Guid:
using GuidAlias = System.Guid;

Console.WriteLine(nameof(GuidAlias));
4. Предопределённые псевдонимы, массивы и обнуляемые значимые типы
Оператор nameof не может быть использован с предопределёнными псевдонимами (int, char, long, и т.п.), с суффиксом ? у обнуляемых типов или с массивами. То есть все следующие варианты не будут компилироваться:
nameof(float)
nameof(Guid?)
nameof(String[])

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 9.
👍1
День двести тридцать пятый. #ЗаметкиНаПолях
Использование using static
Директива using static служит для упрощённого импорта следующих членов классов:
1. Статические поля, свойства и методы. Канонический пример – использование класса Math:
using static System.Math;

// перевод градусов в радианы, используя Math.PI
double radians = degrees * PI / 180;
// площадь треугольника через Math.Sin
double area = 1/2 * sideA * sideB * Sin(angle);
2. Значения перечислений. Это полезно, если в коде много раз используются значения перечисления. В примере ниже перебираются значения перечисления HttpStatusCode. Добавив using static, в операторе switch можно опускать имя перечисления:
using static System.Net.HttpStatusCode;
...
switch (response.StatusCode)
{
case OK: …
case TemporaryRedirect: …
case Redirect: …
case RedirectMethod: …
case NotFound: …
default: …
}
3. Вложенные типы.
Вложенные типы чаще используются в сгенерированном коде. Однако, если вы ими пользуетесь, возможность напрямую их импортировать может упростить написание кода.

Методы расширения и using static
Два важных момента взаимодействия методов расширения и директивы using static:
1. Методы расширения из одного типа могут быть импортированы с использованием директивы using static с этим типом без импорта каких-либо методов расширения из остальной части данного пространства имен. Например, класс System.Linq.Queryable содержит методы расширения для деревьев выражений IQueryable<T>, а класс System.Linq.Enumerable содержит методы расширения для делегатов, принимающих IEnumerable<T>. Поскольку IQueryable<T> наследуется от IEnumerable<T>, используя обычную директиву using для System.Linq, вы можете использовать методы расширения, принимающие делегаты в IQueryable<T>, хотя обычно этого не требуется. Далее показано, как использование статической директивы только для System.Linq.Queryable приводит к тому, что методы расширения из System.Linq.Enumerable не подключаются:
using System.Linq.Expressions;
using static System.Linq.Queryable;
...
var query = new[] { "a", "bc", "d" }.AsQueryable();
Expression<Func<string, bool>> expr = x => x.Length > 1;
Func<string, bool> del = x => x.Length > 1;
var valid = query.Where(expr);
var invalid = query.Where(del);
Последняя строка приводит к ошибке компиляции, поскольку методу Where передаётся делегат, а не дерево выражений. Таким образом, если у вас в проекте есть файл с методами расширения для различных типов проекта, можно разделить методы расширения, относящиеся к разным типам, на разные классы расширений и импортировать каждый класс по отдельности с помощью using static.
2. Методы расширения, импортированные из типа, нельзя вызывать, как статический методы (как выше вызывался метод Sin класса Math). Вместо этого вы должны вызывать их, как если бы они были экземплярами методов расширяемого типа. Например, метод расширения Enumerable.Count:
using System.Collections.Generic;
using static System.Linq.Enumerable;

IEnumerable<string> strings = new[] { "a", "b", "c" };
int valid = strings.Count();
int invalid = Count(strings);
Последняя строка приведёт к ошибке компиляции.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 10.