.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
День двести пятьдесят восьмой. #ЗаметкиНаПолях
Использование потокобезопасных коллекций. Продолжение
ConcurrentDictionary
Словарь предоставляет хранилище данных, проиндексированное по ключу. ConcurrentDictionary<TKey, TValue> может использоваться несколькими параллельными задачами. Действия над словарем выполняются атомарно. Другими словами, действие по обновлению элемента в словаре не может быть прервано действием из другой задачи. ConcurrentDictionary предоставляет некоторые дополнительные методы, которые необходимы, когда словарь используется несколькими задачами.
ConcurrentDictionary<string, int> ages = new ConcurrentDictionary<string, int>();
if (ages.TryAdd("Иван", 21))
Console.WriteLine("Иван успешно добавлен.");
Console.WriteLine("Возраст Ивана: {0}", ages["Иван"]);
// Пытаемся изменить возраст с 21 на 22
if (ages.TryUpdate("Иван", 22, 21))
Console.WriteLine("Возраст успешно обновлён");
Console.WriteLine("Новый возраст Ивана: {0}", ages["Иван"]);
// Увеличиваем возраст, используя фабричный метод
Console.WriteLine("Возраст Ивана обновлён до: {0}",
ages.AddOrUpdate("Иван", 1,
(name,age) => age = age+1));
Console.WriteLine("Новый возраст Ивана: {0}", ages["Иван"]);
Метод TryAdd пытается добавить новый элемент. Если элемент уже существует, метод TryAdd возвращает false. Метод TryUpdate поставляется с ключом обновляемого элемента, новым значением, которое должно быть сохранено в элементе, и значением, которое должно быть перезаписано. В приведенном выше примере возраст элемента «Иван», будет обновлен до 22, только если существующее значение равно 21. Это позволяет программе обновлять элемент, только если он имеет ожидаемое значение.
Метод AddOrUpdate позволяет предоставить поведение, которое будет выполнять обновление заданного элемента или добавлять новый элемент, если он ещё не существует. В приведённом выше примере добавление элемента «Иван» не будет выполнено при вызове AddOrUpdate, поскольку элемент уже существует. Вместо этого выполняется пользовательский делегат, передаваемый в качестве третьего аргумента, который увеличивает возраст элемента «Иван» на 1.
Метод GetOrAdd позволяет получить существующее значение для указанного ключа или, если ключ не существует, вы добавить пару ключ/значение. Методу GetOrAdd также можно передать делегат для задания значения.
ConcurrentDictionary предназначен для многопоточных сценариев. Вам не нужно использовать блокировки в своем коде для добавления или удаления элементов из коллекции. Однако один поток всегда может извлечь значение, а другой поток - немедленно обновить коллекцию, задав этому же ключу новое значение.
Кроме того, хотя все методы ConcurrentDictionary являются потокобезопасными, не все методы являются атомарными, в частности GetOrAdd и AddOrUpdate. Пользовательский делегат, который передается этим методам, вызывается вне внутренней блокировки словаря (это делается для того, чтобы неизвестный код не блокировал все потоки). Следовательно, возможна следующая последовательность событий:
1) Поток A вызывает GetOrAdd, не находит элемента и создаёт новый элемент для добавления, вызывая делегат.
2) Поток B одновременно вызывает GetOrAdd, вызывается его делегат, и он достигает внутренней блокировки словаря перед потоком A, поэтому его новая пара ключ-значение добавляется в словарь.
3) Продолжается выполнение делегата из потока A, он достигает блокировки, но теперь видит, что элемент уже существует.
4) Поток A выполняет «Get» и возвращает данные, которые были ранее добавлены потоком B.
Поэтому нет гарантии, что данные, возвращаемые GetOrAdd, являются теми же данными, которые создаются в делегате потока. Подобная последовательность событий может происходить и при вызове AddOrUpdate.

Источники:
- Rob Miles “Exam Ref 70-483 Programming in C#”. 2nd ed - Pearson Education, Inc., 2019. Глава 1.
-
https://docs.microsoft.com/dotnet/standard/collections/thread-safe/how-to-add-and-remove-items
День двести пятьдесят девятый. #ЗаметкиНаПолях
Использование потокобезопасных коллекций. Окончание
BlockingCollection<Т>
С точки зрения разработки лучше всего реализовывать паттерн «производитель/потребитель», то есть рассматривать задачи в многопоточном приложении как либо производителя, либо потребителя данных. Задача, которая и производит, и потребляет данные, уязвима для ситуаций «взаимной блокировки». Если задача A ожидает чего-то, создаваемого задачей B, а задача B ожидает чего-то, создаваемого задачей A, и ни одна из задач не может быть запущена.
Класс BlockingCollection<T> обеспечивает потокобезопасное средство добавления и извлечения элементов из хранилища данных. Он называется блокирующей коллекцией, потому что действие Take блокирует задачу, если нет элементов, которые необходимо извлечь. Попытки добавить элементы в заполненную коллекцию также блокируются. Кроме того, можно установить верхний предел размера коллекции.
Следующий код создаёт поток, который пытается добавить 5 элементов в коллекцию BlockingCollection из 3 элементов. После добавления 3-го элемента этот поток блокируется. Программа также создаёт поток, который извлекает элементы из коллекции. Как только поток чтения начинает работать и извлекает некоторые элементы из коллекции, поток записи может продолжиться:
BlockingCollection<int> data = new BlockingCollection<int>(3);
Task.Run(() =>
{
for(int i=1;i<=5;i++)
{
data.Add(i);
Console.WriteLine("Число {0} добавлено.", i);
}
data.CompleteAdding();
});
Console.ReadKey();
Console.WriteLine("Чтение элементов");
Task.Run(() =>
{
while (!data.IsCompleted)
{
try
{
int v = data.Take();
Console.WriteLine("Число {0} прочитано.", v);
}
catch (InvalidOperationException) { }
}
});
Console.ReadKey();


Пример вывода:
Число 1 добавлено
Число 2 добавлено
Число 3 добавлено
Чтение элементов
Число 1 прочитано
Число 2 прочитано
Число 3 прочитано
Число 4 добавлено
Число 5 добавлено
Число 4 прочитано
Число 5 прочитано

Задача добавления вызывает метод CompleteAdding(), после добавления последнего элемента. Это предотвращает добавление новых элементов в коллекцию. Извлекающая задача использует свойство IsCompleted, чтобы определить, когда прекратить получать элементы. Свойство IsCompleted возвращает true, когда коллекция пуста и вызван CompleteAdding. Операция извлечения выполняется внутри конструкции try-catch, т.к. метод Take может выбросить исключение, если между проверкой IsCompleted и вызовом Take добавляющая задача вызовет CompleteAdding. То есть получающая задача попытается выполнить извлечение из коллекции, которая была помечена как завершённая.
BlockingCollection также предоставляет методы TryAdd и TryTake, которые используются для попытки выполнения действия. Они возвращает true, если действие выполнено успешно. Кроме того, им можно передать максимальное время ожидания завершения действия и токен отмены. Класс BlockingCollection выступает в качестве оболочки для других потокобезопасных классов коллекций, включая ConcurrentQueue (используется по умолчанию), ConcurrentStack и ConcurrentBag.

Источник: Rob Miles “Exam Ref 70-483 Programming in C#”. 2nd ed - Pearson Education, Inc., 2019. Глава 1.
This media is not supported in your browser
VIEW IN TELEGRAM
День двести шестидесятый. #ЧтоНовенького
Вертикальная панель вкладок
В Microsoft объявили о выпуске Visual Studio 2019 version 16.4 Preview 2.
Отображение вкладок в вертикальной панели было одной из самых востребованных функций у сообщества разработчиков. Реализация вертикальной панели вкладок, начиная с Visual Studio 2019 v16.4 Preview 2, является одним из многих шагов, позволяющих значительно улучшить управление документами. Выше приведён краткий пример того, как выглядит новая функция.

Источник: https://devblogs.microsoft.com/visualstudio/fall-sports-pumpkin-spice-and-visual-studio-2019-v16-4-preview-2/
День двести шестьдесят первый. #Оффтоп #97Вещей
97 Вещей, Которые Должен Знать Каждый Программист
3. Спросите себя: «Что бы сделал пользователь?» (Вы не пользователь)
Мы все склонны полагать, что другие люди думают так же, как мы. Но это не так. Психологи называют это ложным консенсусом. Когда люди думают или действуют не так, как мы, мы думаем (пусть подсознательно), что с ними что-то не так.
Это предубеждение объясняет, почему программистам так сложно поставить себя на место пользователя. Пользователи не думают, как программисты. Для начала они проводят гораздо меньше времени за компьютерами. Они не знают и не заботятся о том, как работает компьютер. Это означает, что они не могут использовать ни одну из техник решения проблем, знакомых программистам. Они не распознают шаблоны и подсказки, которые программисты используют для работы с интерфейсом.
Лучший способ узнать, что думает пользователь, - понаблюдать. Попросите пользователя выполнить задачу, используя программный продукт, аналогичный тому, который вы разрабатываете. Убедитесь, что задача реальная: «Сложите столбец чисел» - нормально; «Рассчитайте свои расходы за последний месяц» - отлично. Избегайте слишком специфических задач, таких как «Можете ли вы выбрать эти ячейки электронной таблицы и ввести формулу СУММ() ниже?» - в этом вопросе заложена подсказка. Попросите пользователя рассказать о своем прогрессе. Не перебивайте. Не пытайтесь помочь. Продолжайте спрашивать себя: «Почему он это делает?» или «Почему она этого не делает?»
Первое, что вы заметите, это то, что пользователи делают основные вещи схожим образом. Они пытаются выполнять задачи в одном и том же порядке и делают одни и те же ошибки в одних и тех же местах. Вы должны проектировать вокруг этого основного поведения. Это отличается от совещаний разработчиков, когда все слушают, как кто-то говорит: «А что, если пользователь захочет ...?» Наблюдение за пользователями устраняет эту путаницу.
Вы увидите, что происходит, когда пользователи застревают. Когда вы застряли, вы оглядываетесь вокруг. Когда пользователи застревают, они сужают фокус. Им становится все труднее видеть решения в других местах на экране. Это одна из причин, почему текст справки - плохое решение для плохого дизайна пользовательского интерфейса. Если вам нужно разместить инструкции для пользователя, обязательно поместите их рядом с проблемными областями. Узкий фокус внимания пользователя - это то, почему подсказки более полезны, чем отдельные страницы справки.
Пользователи, как правило, путаются. Они найдут способ, который будет работать, и будут придерживаться его, независимо от того, насколько он запутан. Лучше предоставить один действительно очевидный способ работы, чем два или три коротких пути.
Вы также обнаружите, что существует разрыв между тем, что пользователи говорят, что они хотят, и тем, что они реально делают. В этом и проблема, так как обычный способ сбора требований пользователей - это опрос. Вот почему лучший способ определить требования - это наблюдать за пользователями. За час наблюдения за пользователями вы получите больше информации, чем за день выспрашивания чего они хотят.

Источник: https://www.oreilly.com/library/view/97-things-every/9780596809515/
День двести шестьдесят второй. #ЧтоНовенького
Новинки Visual Studio 2019 version 16.4 Preview 2
Обновления окна терминала
Добавлена возможность создавать несколько экземпляров терминала и автоматически создавать профили для командной строки разработчика, Developer PowerShell и любых дистрибутивов WSL, доступных на вашем компьютере. Автоматическое создание профиля будет происходить при первом запуске или с помощью кнопки восстановления профилей.

Окно Инструментов Контейнера
Эта функция была доступна только в качестве расширения в Visual Studio Marketplace. Но из-за большой популярности её включили в саму IDE. Окно позволяет просматривать, проверять, останавливать, запускать и удалять образы и контейнеры Docker на локальном компьютере (см. картинку ниже). Кроме того, вы можете просматривать папки и файлы в запущенных контейнерах, открывать окно терминала и просматривать журналы.

Источник: https://devblogs.microsoft.com/visualstudio/fall-sports-pumpkin-spice-and-visual-studio-2019-v16-4-preview-2/
День двести шестьдесят третий. #юмор
"Выберите все квадраты с багами." Это однозначно лучшая каптча, которую я видел.
День двести шестьдесят четвёртый. #ЗаметкиНаПолях
Использование кортежей. Начало
Ранее я уже писал, что в C# 7 упрощён синтаксис использования кортежей. Кортежи можно использовать как параметры методов, переопределять методы, используя кортежи с разными типами и количеством значений, приводить кортежи с разными типами значений, проверять кортежи на равенство и многое другое. Но это не значит, что теперь обязательно их использовать везде. Далее рассмотрим некоторые альтернативы кортежам, их плюсы и минусы.
1. Тип System.Tuple<…>
Типы System.Tuple<…> в .NET 4 являются неизменяемыми ссылочными типами, хотя типы элементов в них могут быть изменяемыми.
Недостатки:
- отсутствие какой-либо языковой интеграции (сложнее для создания, более длинные имена типов, отсутствует поддержка приведений);
- обращаться к элементам можно только по строгим именам вида ItemX;
- кортежи ссылочного типа ведут себя как полноценные объекты, а не как контейнеры значений, что может быть как хорошо, так и плохо, в зависимости от контекста.
Преимущества:
- копирование ссылки на большой объект Tuple<…> более эффективно, чем копирование значимых типов кортежей ValueTuple<…>, которое включает в себя копирование всех значений элементов. Это также влияет на безопасную многопоточность: копирование ссылки является атомарным, тогда как копирование кортежа ValueTuple - нет.

2. Анонимные типы
Анонимные типы были введены как часть LINQ, и это остаётся основным вариантом их использования. Можно использовать их для обычных переменных в методе, но это встречается крайне редко.
Большинство преимуществ анонимных типов также присутствуют в кортежах C# 7: именованные элементы, вывод имени элементов, естественное сравнение и чёткое строковое представление.
Недостатки:
- нельзя вернуть анонимный тип из методов или свойств, не потеряв при этом безопасность типов (использование в типе dynamic).
Преимущества:
- анонимные типы поддерживаются внешними поставщиками LINQ (для баз данных и т.д.). Литералы кортежей в настоящее время не могут быть использованы в деревьях выражений.
- анонимные типы могут быть более эффективными в некоторых контекстах благодаря передаче по ссылке. Хотя, в большинстве случаев это вряд ли будет проблемой, и тот факт, что кортежи не создают никаких объектов для очистки сборщиком мусора, даёт им преимущество в эффективности.

3. Именованные типы
Кортежи - это просто контейнеры переменных. В них нет инкапсуляции; они не несут никакого значения, кроме того, которое вы подразумеваете при работе с ними. Иногда это именно то, что надо, но остерегайтесь использовать их слишком широко. Рассмотрим кортеж (double, double). Он может быть использован как:
- двумерные декартовы координаты (x, y)
- двумерные полярные координаты (радиус, угол)
- одномерный отрезок (начало, конец)
- любой другой вариант (тысячи их).
Каждый из этих вариантов использования подразумевает различные операции над ним при моделировании в качестве типа. Вам не нужно беспокоиться о выведении имён элементов или о том, что пользователи случайно используют, например, декартовы координаты в качестве полярных координат. Если вам нужна простая временная группировка значений, кортежи подойдут. Но если вы обнаружите, что вы используете одну и ту же форму кортежа в нескольких местах кода, я бы рекомендовал заменить её на именованный тип.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 11.
День двести шестьдесят пятый. #ЗаметкиНаПолях
Заметки по использованию кортежей
Замечание: что здесь приведены умозаключения о вариантах использования кортежей, поскольку реальной практики их применения ещё не так много.
1. Непубличные API и легко изменяемый код
Пока сообщество не имеет достаточно опыта использования кортежей в публичных API, не набило шишек и не выработало лучших практик использования, лучше будет с ними не экспериментировать. Если же вы контролируете (и можете произвольно изменять) весь код, который взаимодействует с вашим API, можно попробовать. Но не стоит ставить себя в ситуацию, когда вы возвращаете кортеж из открытого метода только потому, что это легко сделать, чтобы потом оказалось, что значения придётся инкапсулировать. Именованный тип требует больше усилий по проектированию и реализации, но результат вряд ли будет более сложным для использования вызывающей стороной. Кортежи в основном удобны для автора, но не для пользователя.

2. Локальные переменные
Кортежи в первую очередь предназначены для того, чтобы из метода можно было возвращать несколько значений без использования параметров или специально созданного типа. Но это не значит, что это единственное место, где вы можете их использовать. Внутри метода нет ничего необычного в том, чтобы иметь естественные группы переменных. Вы можете часто замечать, что переменные имеют общий префикс. Поэтому следующий код:
string bestPlayer;
int bestScore;
foreach (var game in games)
{
if (game.Score > bestScore)
{
bestPlayer = game.Player;
bestScore = game.Score;
}
}
Можно переписать так:
(string player, int score) best = (null, -1);
foreach (var game in games)
{
if (game.Score > best.score)
{
best = (game.Player, game.Score);
}
}

3. Поля
Сказанное выше про локальные переменные можно применить и к полям класса. Однако существует несколько ограничений и предостережений:
- Элементы кортежа могут по-прежнему назначаться в конструкторе индивидуально, но, если вы инициализируете не все элементы, никакого предупреждения не будет.
- Либо все поле кортежа будут доступны только для чтения, либо нет. Если у вас есть группа связанных полей, некоторые из которых доступны только для чтения, а некоторые нет, вам придётся либо отказаться от использования модификатора readonly, либо не использовать кортежи.
- Если некоторые из полей являются автоматически сгенерированными, поддерживающими автоматически реализованные свойства, вам потребуется написать полные свойства, чтобы использовать кортеж.

4. Кортежи и dynamic не дружат
Есть подозрение, что кортежи и тип dynamic и так не будут особо пересекаться. Но всё же стоит помнить о двух проблемах, связанных с доступом к элементам:
- Связыватель типа dynamic не знает об именах элементов
Имена элементов кортежа в основном касаются времени компиляции. Так как динамическое связывание происходит только во время выполнения, следующий код (несмотря на то, что выглядит безобидно):
dynamic tuple = (x: 10, y: 20);
Console.WriteLine(tuple.x);
приведёт к ошибке времени выполнения:
'System.ValueTuple<int,int>' не содержит определения для 'x'
Если изменить код на вывод tuple.Item1, он сработает (по крайней мере, для первых 8 элементов).
- Связыватель типа dynamic (пока) не знает об элементах кортежей далее 8-го
Для кортежей длиннее 8 элементов компилятор использует ValueTuple<…>, где восьмым элементом является ещё один кортеж. Таким образом 9й элемент – это первый элемент кортежа, находящегося в 8-м элементе исходного кортежа. Таким образом, следующий код скомпилируется:
var tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9);
Console.WriteLine(tuple.Item9);
dynamic d = tuple;
Console.WriteLine(d.Item9);
Но на настоящий момент приводит к ошибке времени выполнения:
'System.ValueTuple<int,int,int,int,int,int,int,System.ValueTuple<int,int>>' не содержит определения для 'Item9'
Возможно, это будет исправлено в новых версиях компилятора.

Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 11.
День двести шестьдесят шестой. #ЧтоНовенького
.Net Conf 2019
В конце сентября прошла конференция Microsoft по .Net разработке .Net Conf 2019. И недавно наша любимая корпорация выкатила целый плейлист с выступлениями.
Его будет достаточно, чтобы занять себя как минимум на пару недель. Самому очень хочется глянуть многое из представленного. Осталось найти время.
https://www.youtube.com/playlist?list=PLReL099Y5nRd04p81Q7p5TtyjCrj9tz1t

PS: Естественно всё English only. Сорян.
День двести шестьдесят седьмой. #Оффтоп #97Вещей
97 Вещей, Которые Должен Знать Каждый Программист
4. Автоматизируйте стандарт кодирования
У вас наверняка тоже такое бывало. В начале проекта у всех много добрых намерений. Многие из этих намерений документируются. Те, что касаются кода, попадают в стандарт кодирования проекта. На первом собрании тимлид просматривает документ и, в лучшем случае, все соглашаются, что они попытаются следовать ему. Однако, как только проект начинается, все эти благие намерения отпадают одно за другим. Когда проект, наконец, завершается, в коде полный бардак, и никто не знает, как это произошло.
Где вы свернули не туда? Вероятно, уже на первом собрании. Одни участники проекта не обратили внимания, другие неправильно поняли, третьи вообще не согласились и сразу решили делать по-своему. Оставшиеся всё поняли и согласились, но, когда дедлайн стал поджимать, от стандартов пришлось отступить. Правильно отформатированный код не получит очков от клиента, который хочет больше функциональности. Кроме того, следование стандарту кодирования может быть довольно скучной задачей, если оно не автоматизировано. Просто попробуйте навести порядок в классе, чтобы убедиться в этом.
Но если всё так сложно, зачем мы вообще вводим стандарт кодирования? Одна из причин форматирования кода единообразным способом заключается в том, что никто не может «владеть» частью кода, просто отформатировав его по-своему. Мы можем запретить разработчикам использовать определенные антипаттерны, чтобы избежать некоторых распространенных ошибок. Стандарт кодирования должен облегчить работу в проекте и поддерживать скорость разработки от начала до конца. Из этого следует, что все должны согласиться с принятым стандартом кодирования: если один разработчик использует три пробела для отступов кода, а другой использует четыре, ничего хорошего не выйдет.
Существует множество инструментов, которые можно использовать для составления отчетов о качестве кода, а также для документирования и поддержания стандарта кодирования, но это не панацея. Стандарт должен быть автоматизирован и применён везде, где это возможно. Вот несколько примеров:
- Сделайте форматирование кода является частью процесса сборки, чтобы оно выполнялось при каждой компиляции.
- Используйте статические анализаторы кода для обнаружения антипаттернов. Если они найдены, отменяйте сборку, как при критической ошибке.
- Научитесь настраивать эти инструменты, чтобы вы могли обнаруживать свои собственные специфические для проекта антипаттерны.
- Измеряйте не только охват кода тестами, но и автоматически проверяйте результаты. Опять же, отменяйте сборку, если покрытие тестами слишком слабое.
Попробуйте применить это ко всему, что вы считаете важным. Вы не сможете автоматизировать всё, что вас действительно волнует. Для того, что вы не можете автоматически проверять или исправить, создайте набор правил, дополняющих автоматизированный контроль. Однако смиритесь с тем, что и вы, и ваши коллеги не всегда будут им следовать.
Наконец, стандарт кодирования должен быть гибким. По мере развития проекта потребности меняются, и то, что казалось логичным вначале, не обязательно останется правильным несколько месяцев спустя.

Источник: https://www.oreilly.com/library/view/97-things-every/9780596809515/
День двести шестьдесят восьмой. #ЗаметкиНаПолях
Нетривиальный пример использования явной реализации интерфейса
Про то, что такое явная реализация интерфейса, я писал достаточно давно. Однако на днях попался интересный пример использования, который захотелось записать на будущее.
Когда класс реализует интерфейс, он обязан содержать методы с сигнатурами, которые соответствуют указанным в интерфейсе. Вы можете использовать явную реализацию интерфейса, чтобы сделать методы, реализующие интерфейс, видимыми только тогда, когда к объекту обращаются через интерфейсную ссылку.
Например, вы можете создать интерфейс IPrintable, который определяет методы, используемые для печати любого объекта. Это хорошая идея, потому что теперь у принтера можно попросить напечатать любой объект класса, реализующего IPrintable. Если подумать, то методы интерфейса IPrintable имеют смысл только тогда, когда они используются чем-то, пытающимся напечатать объект. Не имеет смысла вызывать методы печати в каком-либо ином контексте, кроме как через ссылку на IPrintable. Этого можно добиться, сделав реализацию методов печати явной, то есть добавив имя интерфейса к объявлению тела метода.
Следующий код показывает класс Report, который реализует интерфейс IPrintable. Класс Report содержит два метода: GetPrintableText и GetTitle, которые объявлены в интерфейсе IPrintable.
interface IPrintable
{
string GetPrintableText(int page);
string GetTitle();
}
class Report : IPrintable
{
string IPrintable.GetPrintableText(int page)
{
return "Текст отчёта";
}
string IPrintable.GetTitle()
{
return "Заголовок отчёта";
}
}
Теперь единственный способ получить доступ к этим методам в экземпляре отчёта – приведя отчёт к IPrintable. На рисунке ниже показан эффект в Visual Studio: методы GetPrintableText и GetTitle скрыты от IntelliSense, если обращаться через класс Report напрямую, но при обращении через IPrintable они видны.
Таким образом можно спрятать методы, не относящиеся непосредственно к функционалу класса. Потребители класса-отчёта (кроме принтера) не смогут использовать эти методы, т.к. они им не нужны. Более того, используя частичные (partial) классы, можно и сам код этих методов вынести в отдельный файл, чтобы он не загромождал код основного функционала.

Источник: Rob Miles “Exam Ref 70-483 Programming in C#”. 2nd ed - Pearson Education, Inc., 2019. Глава 2.
День двести шестьдесят девятый. #BestPractices
Разработка Исключений
Выброс Исключений
Ошибка выполнения возникает всякий раз, когда член класса не может сделать то, для чего он был предназначен. Например, если метод OpenFile не может вернуть дескриптор открытого файла вызывающей стороне, это будет считаться ошибкой выполнения. Большинству разработчиков удобно использовать исключения для ошибок использования, таких как деление на ноль или нулевые ссылки. В .Net Framework исключения используются для всех ошибок, включая ошибки выполнения.
ИЗБЕГАЙТЕ возвращения кодов ошибок из методов.
ИСПОЛЬЗУЙТЕ исключения для сообщения об ошибках выполнения. Исключения являются основным средством сообщения об ошибках в фреймворках.
⚠️ РАССМОТРИТЕ вариант завершение процесса, вызвав System.Environment.FailFast вместо выброса исключения, если ваш код сталкивается с ситуацией, когда он небезопасен для дальнейшего выполнения.
ИЗБЕГАЙТЕ использования исключений для контроля нормального потока выполнения.

За исключением сбоев системы и операций, в которых возможно возникновение состояния гонки, разработчики фреймворков должны создавать такой API, чтобы пользователи могли писать код, который не генерирует исключения. Например, вы можете предоставить способ проверки предварительных условий перед вызовом метода. Метод, используемый для проверки предварительных условий другого метода, часто называют тестером, а метод, фактически выполняющий работу, называют исполнителем.

⚠️ РАССМОТРИТЕ последствия для производительности при использовании исключений. Частый выброс исключений может заметно повлиять на производительность большинства приложений.
❗️ НЕОБХОДИМО задокументировать все исключения, которые могут выбрасывать открытые члены вследствие нарушения контракта кода (а не из-за сбоя системы), и рассматривать их как часть контракта. Исключения, являющиеся частью контракта, не должны изменяться от одной версии к другой (то есть не должен изменяться тип исключения, и не должны добавляться новые исключения).
ИЗБЕГАЙТЕ создания открытых членов, выбрасывающих или не выбрасывающих исключение в зависимости от какой-либо настройки.
ИЗБЕГАЙТЕ открытых членов, возвращающих исключение, либо задающих исключение out параметру. Возврат исключений из общедоступных API вместо их выбрасывания сводит на нет многие преимущества системы обработки исключений.
⚠️ РАССМОТРИТЕ использование методов-конструкторов исключений. Обычно из разных мест кода выбрасывается одно и то же исключение. Чтобы избежать раздувания кода, используйте вспомогательные методы, которые создают исключение и инициализируют его свойства. Кроме того, члены, которые генерируют исключения, не встраиваются компилятором. Перемещение оператора throw внутрь вспомогательного метода может позволить встроить исходный член.
ИЗБЕГАЙТЕ выбрасывания исключений из блоков фильтрации исключений. Когда фильтр исключений выбрасывает новое исключение, оно перехватывается CLR, а фильтр возвращает false. Это поведение неотличимо от фильтра, который успешно выполняется и возвращает false, поэтому такое исключение очень трудно отладить.
ИЗБЕГАЙТЕ явного выброса исключения из блоков finally. Но из блока finally допустимо вызывать методы, которые могут выбрасывать исключения.

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести семидесятый. #BestPractices
Разработка Исключений
Использование Стандартных Типов Исключений
Далее описываются стандартные исключения, предоставляемые платформой, и подробности их использования. Список ни в коем случае не является исчерпывающим. Обратитесь к справочной документации по .NET Framework для использования других типов исключений.

Exception и SystemException
ИЗБЕГАЙТЕ выбрасывания System.Exception или System.SystemException.
ИЗБЕГАЙТЕ перехватывания System.Exception или System.SystemException во внутреннем коде, если только вы не собираетесь их повторно выбросить.
ИЗБЕГАЙТЕ перехватывания System.Exception или System.SystemException, за исключением обработчиков исключений верхнего уровня.

ApplicationException
ИЗБЕГАЙТЕ выбрасывания и наследования от ApplicationException.

InvalidOperationException
ИСПОЛЬЗУЙТЕ InvalidOperationException, если объект находится в неподходящем состоянии.

ArgumentException, ArgumentNullException и ArgumentOutOfRangeException
ИСПОЛЬЗУЙТЕ ArgumentException или один из его подтипов, если члену передаётся неверный аргумент. Отдавайте предпочтение наиболее производному типу исключения, если это возможно.
ИСПОЛЬЗУЙТЕ свойство ParamName при создании одного из подклассов ArgumentException. Это свойство представляет имя параметра, вызвавшего исключение. Обратите внимание, что это свойство также может быть установлено с использованием одной из перегрузок конструктора.
ИСПОЛЬЗУЙТЕ ключевое слово value для имени неявного параметра-значения в установщике свойства.

NullReferenceException, IndexOutOfRangeException и AccessViolationException
ИЗБЕГАЙТЕ явного или неявного выбрасывания исключений типа NullReferenceException, AccessViolationException или IndexOutOfRangeException в публично вызываемом API. Эти типы исключения зарезервированы, выбрасываются механизмом выполнения и в большинстве случаев указывают на ошибку в коде. Выполняйте проверку аргументов, чтобы избежать выброса этих исключений. Выброс этих исключений раскрывает подробности реализации вашего метода, которая может меняться со временем.

StackOverflowException
ИЗБЕГАЙТЕ явного выбрасывания StackOverflowException. Это исключение должно явно выбрасываться только CLR.
ИЗБЕГАЙТЕ перехвата StackOverflowException. Практически невозможно написать согласованный управляемый код при наличии произвольных переполнений стека. Неуправляемые части CLR остаются согласованными благодаря использованию проверок для перемещения переполнений стека в четко определенные места, а не путем восстановлений после произвольных переполнений стека.

OutOfMemoryException
ИЗБЕГАЙТЕ явного выбрасывания OutOfMemoryException. Это исключение должно выбрасываться только инфраструктурой CLR.

ComException, SEHException и ExecutionEngineException
ИЗБЕГАЙТЕ явного выбрасывания COMException, ExecutionEngineException или SEHException. Эти исключения должны выбрасываться только инфраструктурой CLR.

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

Паттерн Тестер-Исполнитель (Tester-Doer)
Иногда производительность элемента, генерирующего исключение, можно улучшить, разбив его на две части. Рассмотрим на метод Add интерфейса ICollection<T>:
ICollection<int> numbers = …
numbers.Add(1);
Метод Add выбрасывает исключение, если коллекция доступна только для чтения. Это может быть проблемой производительности в сценариях, где ожидается, что вызов метода, часто будет заканчиваться неудачей. Один из способов сгладить проблему - проверять, доступна ли коллекция для записи, прежде чем добавлять значение:
ICollection<int> numbers = …

if (!numbers.IsReadOnly)
numbers.Add(1);
Член класса, используемый для проверки условия (в нашем примере свойство IsReadOnly), называется тестером. Член, используемый для выполнения операции, потенциально приводящей к исключению (в нашем примере метод Add), называется исполнителем.

⚠️ РАССМОТРИТЕ использование паттерна Tester-Doer для членов класса, которые могут генерировать исключения в распространённых сценариях, чтобы избежать проблем производительности, связанных с исключениями.

Паттерн Try-Parse
Для API, чрезвычайно чувствительных к производительности, можно использовать еще более быстрый паттерн Try-Parse. Он требует изменения имени члена, чтобы оно отражало суть того, что он будет делать. Например, DateTime определяет метод Parse, который выбрасывает исключение в случае сбоя парсинга строки. И метод TryParse, который пытается выполнить парсинг, но возвращает false, если он завешается неудачей, а в случае успеха возвращает результат в out параметре.
public struct DateTime
{
public static DateTime Parse(string dateTime) { … }
public static bool TryParse(string dateTime, out DateTime result) { … }
}
При использовании этого паттерна важно строго определить функциональность блока try. Если в методе произойдёт сбой по какой-либо иной причине, кроме определённой в блоке try, метод всё равно должен выбросить соответствующее исключение.

⚠️ РАССМОТРИТЕ использование паттерна Try-Parse для членов, которые могут генерировать исключения в распространённых сценариях, чтобы избежать проблем производительности, связанных с исключениями.
ИСПОЛЬЗУЙТЕ префикс «Try» и логический тип возвращаемого значения для методов, реализующих этот паттерн.
❗️ НЕОБХОДИМО добавить и обычный метод, генерирующий исключение, для каждого метода, использующего паттерн Try-Parse.

Источник: https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/
День двести семьдесят второй. #Оффтоп #97Вещей
97 Вещей, Которые Должен Знать Каждый Программист
5. Красота в простоте
Следующую цитату особенно полезно знать всем разработчикам программного обеспечения:

Красота стиля, гармония, грация и хороший ритм зависят от простоты.
- Платон

В одном предложении собраны ценности, к которым мы, разработчики программного обеспечения, должны стремиться:
- Читаемость
- Сопровождаемость
- Скорость разработки
- Красота
Платон говорит нам, что фактором, способствующим всем этим качествам, является простота.
Что такое красивый код? Это потенциально очень субъективный вопрос. Восприятие красоты в значительной степени зависит от жизненного опыта, так же, как и любое наше восприятие чего-либо. Люди, получившие образование в области искусства, воспринимают красоту (или, по крайней мере, подходят к ней) иначе, чем технари. Специалисты по искусству склонны подходить к красоте в программном обеспечении, сравнивая программное обеспечение с произведениями искусства, в то время как технари склонны говорить о симметрии и золотом сечении, пытаясь привести вещи к формулам. По моему опыту простота является основой большинства аргументов с обеих сторон.
Подумайте об исходном коде, который вы изучали. Если вы не потратили время на изучение кода других людей, прекратите читать это прямо сейчас и найдите для изучения какой-нибудь открытый исходный код. Шутки в сторону! Я серьезно! Найдите в Интернете какой-нибудь код на выбранном вами языке, написанный известным, признанным экспертом.
Код, который я считаю красивым, имеет ряд общих свойств. Главным среди них является простота. Я считаю, что независимо от того, насколько сложным является приложение или система вообще, отдельные части должны быть простыми: простые объекты, решающие единственную задачу, содержащие такие же простые, чёткие методы с описательными именами. Некоторые люди думают, что идея иметь короткие методы из 5–10 строк кода является экстремальной, а в некоторых языках этого довольно тяжело достичь. Но я всё равно считаю, что такая краткость является желанной целью.
Суть в том, что красивый код - это простой код. Каждая отдельная часть остается простой с простыми обязанностями и простыми связями с другими частями системы. Таким образом, мы можем поддерживать наши системы с течением времени с помощью чистого, простого, тестируемого кода, обеспечивая высокую скорость разработки в течение всей жизни проекта.
Красота рождается и живёт в простоте.

Источник: https://www.oreilly.com/library/view/97-things-every/9780596809515/
День двести семьдесят третий. #юмор
Работает - не трогай!
День двести семьдесят четвёртый. #ЗаметкиНаПолях
Сопоставление с шаблоном. Начало
Основная идея шаблона - проверить определенный аспект значения и использовать результат этого теста для выполнения другого действия. Да, это похоже на if, но шаблоны, как правило, используются либо для расширения возможностей условия, либо для расширения возможностей внутри самого действия, выбранного на основе шаблона. Ещё раз, сопоставление с шаблоном не позволяет вам сделать что-то, что вы не могли сделать раньше; оно позволяет вам выразить то же самое намерение более чётко.
Предположим, у вас есть абстрактный класс Shape, который определяет абстрактное свойство Area и производные классы Rectangle, Circle и Triangle. К сожалению, для вашего текущего приложения вам не нужна площадь фигуры; вам нужен её периметр. Допустим, вы не можете изменить классы, добавив в них свойство Perimeter, но вы знаете, как его вычислить для всех интересующих вас классов. До C#7 метод Perimeter мог бы выглядеть примерно так:
static double Perimeter(Shape shape)
{
if (shape == null)
throw new ArgumentNullException(nameof(shape));

Rectangle rect = shape as Rectangle;
if (rect != null)
return 2 * (rect.Height + rect.Width);

Circle circle = shape as Circle;
if (circle != null)
return 2 * PI * circle.Radius;

Triangle triangle = shape as Triangle;
if (triangle != null)
return triangle.SideA + triangle.SideB + triangle.SideC;

throw new ArgumentException($"Периметр типа {shape.GetType()} неизвестен", nameof(shape));
}
Это некрасиво, слишком длинно и слишком много повторяющегося кода. Один и тот же шаблон «проверить, является ли фигура определённым типом, а затем использовать свойства этого типа», встречается три раза. Важно отметить, что, хотя здесь есть несколько операторов if, тело каждого из них возвращает значение, поэтому вы всегда выбираете только один вариант выполнения. Тот же код может быть написан на C#7 с использованием шаблонов в операторе switch:
static double Perimeter(Shape shape)
{
switch (shape)
{
case null:
throw new ArgumentNullException(nameof(shape));
case Rectangle rect:
return 2 * (rect.Height + rect.Width);
case Circle circle:
return 2 * PI * circle.Radius;
case Triangle tri:
return tri.SideA + tri.SideB + tri.SideC;
default:
throw new ArgumentException(...);
}
}
Здесь вас иногда интересует только значение (для случая null), а иногда интересует тип значения (прямоугольник, круг или треугольник). При сопоставлении по типу также вводится новая переменная этого типа, которая используется для вычисления периметра. В C#7.0 представлены три вида шаблонов: констант, типов и шаблон var. О них далее.

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

Источник: Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 12.
День двести семьдесят пятый. #ЗаметкиНаПолях
Сопоставление с шаблоном. Продолжение
В C#7 представлены три вида шаблонов: шаблоны констант, шаблоны типов и шаблон var. Использовать сопоставление с шаблоном можно в двух контекстах: в операторе is и операторе switch. Каждый шаблон пытается соответствовать входным данных. Это может быть любое выражение, не содержащее указателей.

Шаблоны констант
Шаблон констант соответствует своему названию: он представляет собой константное выражение времени компиляции, которое затем проверяется на равенство с входными данными. Если и входные данные, и константа являются целочисленными выражениями, они сравниваются с помощью ==. В противном случае вызывается статический метод object.Equals, который позволяет безопасно проверять на null. Например:
static void Match(object input)
{
if (input is "hello")
Console.WriteLine("Это строка hello");
else if (input is 10)
Console.WriteLine("Это значение int 10");
else
Console.WriteLine("Совпадений не найдено");
}
static void Main()
{
Match("hello");
Match(10);
Match(10L);
}
Вывод в основном понятен, кроме последней строки:
Это строка hello
Это значение int 10
Совпадений не найдено
Если целые числа сравниваются с использованием ==, почему последний вызов Match(10L) не находит совпадений? Дело в том, что тип ввода во время компиляции не является целочисленным типом, это объект, поэтому компилятор генерирует код, эквивалентный вызову object.Equals(x, 10). А он возвращает false, т.к. значение x представляет собой упакованное Int64, а не упакованное Int32.

Шаблоны типов
Шаблон типа состоит из типа и идентификатора, похожего на объявление переменной. Шаблон соответствует, если входной параметр является значением этого типа, что похоже на обычный оператор is. Преимущество использования шаблона в том, что он также вводит новую переменную шаблона этого типа, инициализируемую значением, если шаблон соответствует. Если шаблон не совпадёт, переменная всё равно будет существовать, но не будет определена. Если на входе null, он не будет соответствовать ни одному типу. Также можно использовать пустую переменную _, тогда переменная не будет создана. Пример использования шаблона типа был в предыдущем посте.
Шаблон типа обычно используется для замены либо комбинации as/if, либо, блока if с приведением типов. Тип, указанный в шаблоне типа, не может быть обнуляемым значимым типом. То есть нельзя написать:
if (value is int? t) 
...
Но можно использовать параметр типа, который в свою очередь может быть обнуляемым значимым типом:
static void CheckType<T>(object value)
{
if (value is T t)
Console.WriteLine(t);
}
CheckType<int?>(null);
CheckType<int?>(5);
В этом случае шаблон найдёт соответствие только тогда, когда значение не равно null.

Шаблон var
Шаблон var выглядит как шаблон типа, но в качестве типа используется var:
someExpression is var x
Как и шаблоны типов, он вводит новую переменную, но ничего не проверяет. Он всегда совпадает (даже с null), в результате чего получается новая переменная с тем же типом и значением, что и у входного параметра. Поэтому этот шаблон бесполезен в операторе is, а используется в операторе switch в качестве последнего варианта, когда все остальные шаблоны не совпали.

В C#8 введены возможности сопоставления с шаблоном ещё более расширены.

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

Источник: Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 12.
День двести семьдесят шестой. #ЗаметкиНаПолях
Сопоставление с шаблоном. Окончание
Охранные выражения
Помимо простого сравнения с шаблоном в блоке case вы можете использовать новый фрагмент синтаксиса – охранное выражение после ключевого слова when:
case pattern when expression:
Выражение должно соответствовать булеву значению, так же как условие оператора if. Тело case будет выполнено, только если выражение истинно. Выражение может использовать шаблоны, тем самым вводя дополнительные переменные шаблона. Например:
switch (obj)
{
case Shape shape when shape.Area == 0:

break;
case Rectangle rect when rect.Area > 0:

break;
case Circle circle when circle.Area > 0:

break;
case Shape shape:

break;
case null:

break;
default:

break;
}
Когда вы используете охранные выражения, вполне возможно использовать один и тот же шаблон несколько раз, потому что при первом совпадении с шаблоном охранное выражение может вернуть false.

Область видимости
Областью видимости переменной шаблона является блок кода, включающий выражение. У этого есть как преимущества, так и недостатки. Недостатком является то, что переменная шаблона может быть не определена, если совпадения с шаблоном не произойдёт. То есть использовать переменную text внутри блока if безопасно, а после - нет:
if (input is string text)
{
Console.WriteLine(text);
}
С другой стороны, переменная определяется сразу же в шаблоне, таким образом можно использовать следующую конструкцию:
if (input is int x && x > 100)
{
Console.WriteLine($"Это большое целое число: {x}");
}
Вы можете использовать x после &&, потому что это выражение оценивается, только если первый операнд оценивается как истина. Можно использовать x внутри оператора if, потому что оно выполняется, только если оба операнда истинны.
Если переменная объявляется непосредственно в теле case, область действия этой переменной - весь оператор switch, включая другие блоки case. Но это не включает переменные, объявленные в самих блоках case. Область действия этих переменных - только тело соответствующего блока case.

Порядок оценки операторов switch при сопоставлении с шаблоном
Почти во всех ситуациях порядок блоков case при сравнении с константами не имеет значения, потому что каждый блок соответствует одному постоянному значению, а используемые константы должны быть разными. С шаблонами это не так. Порядок оценки оператора switch на основе шаблона можно описать так:
- Каждое условие оценивается в порядке следования в коде.
- Тело блока default выполняется только после того, как все остальные условия были проверены, независимо от того, где оно расположено в операторе switch.
Тут на помощь приходит компилятор: случай без охранного выражения всегда выполнится, если шаблон типа совпадёт. Компилятор знает об этом, поэтому, если вы поместите выше случаи, «скрывающие» те, что следуют за ними, компилятор сообщит об ошибке.

Источник: Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 12.
День двести семьдесят седьмой. #Оффтоп #97Вещей
97 Вещей, Которые Должен Знать Каждый Программист
6. Перед началом рефакторинга
В какой-то момент каждому программисту приходится делать рефакторинг уже существующего кода. Но, прежде чем сделать это, подумайте о следующем, так как это может сэкономить вам и другим много времени (и боли):
1. Наилучший подход к реструктуризации начинается с оценки существующей кодовой базы и тестов, написанных для этого кода. Это поможет вам понять сильные и слабые стороны кода в его нынешнем виде, чтобы вы могли сохранить сильные стороны и избежать ошибок. Мы все думаем, что можем сделать лучше, чем то, что уже существует… пока не получаем нечто такое же или даже хуже того, что было, потому что мы не смогли извлечь уроки из ошибок существующей системы.
2. Избегайте соблазна переписать всё. Лучше всего использовать как можно больше существующего кода. Независимо от того, насколько он уродлив, он уже был протестирован, проверен и т. д. Выбрасывая старый код, особенно если он был в продакшене, вы выбрасываете месяцы (или годы) проверенного, закаленного в боях кода, который, возможно, имел определённые обходные пути и обрабатывал ошибки, о которых вы не знаете. Если вы не принимаете это во внимание, новый код, который вы пишете, может в конечном итоге выдавать те же загадочные ошибки, которые были исправлены в старом коде. Это впустую потратит время, усилия и знания, полученные за эти годы.
3. Несколько последовательных изменений лучше, чем одно большое. Инкрементные изменения позволяют легче оценить влияние на систему с помощью обратной связи, например, от тестов. Неприятно видеть сотню неудачных тестов после внесения изменений. Это разочаровывает и раздражает, что, в свою очередь, может привести к неправильным решениям. Пару неудачных тестов за раз легче исправить, что приводит к более управляемому подходу.
4. После каждой итерации разработки важно убедиться, что существующие тесты успешно проходят. Добавьте новые тесты, если существующих тестов недостаточно, чтобы покрыть внесённые вами изменения. Не выбрасывайте тесты старого кода без должного анализа. На первый взгляд, некоторые из этих тестов могут показаться неприменимыми к вашему новому дизайну, но стоит разобраться в причинах, по которым этот конкретный тест был добавлен.
5. Личные предпочтения и эго не должны мешать. Если что-то работает, зачем это чинить? То, что стиль или структура кода не соответствуют вашим личным предпочтениям, не является уважительной причиной для рефакторинга. Уверенность, что вы могли бы написать лучше, чем предыдущий программист, также не является веским основанием.
6. Новая технология не является достаточной причиной для рефакторинга. Одна из худших причин для рефакторинга заключается в том, что текущий код не использует все крутые технологии, которыми мы располагаем сегодня, и уверенность, что новый язык или инфраструктура могут делать вещи более элегантно. Если анализ затрат и выгод не покажет, что новый язык или структура приведут к значительным улучшениям в функциональности, удобстве обслуживания или производительности, лучше оставить всё как есть.
7. Помните, что люди ошибаются. Реструктуризация не всегда гарантирует, что новый код будет лучше или хотя бы таким же хорошим, как предыдущая версия. Это неприятно, но такова человеческая природа.

Источник: https://www.oreilly.com/library/view/97-things-every/9780596809515/