.NET Разработчик
6.53K subscribers
442 photos
3 videos
14 files
2.12K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День четыреста пятьдесят второй. #MoreEffectiveCSharp
9. Тонкости GetHashCode(). Окончание
Для переопределения GetHashCode нужно наложить некоторые ограничения на тип. В идеале он должен быть неизменяемым. Посмотрим ещё раз на правила:
1. Если два объекта равны по методу Equals(), они должны возвращать одинаковый хэш. Следовательно, любые данные, используемые для генерации хеш-кода, также должны участвовать в проверке на равенство.

2. Возвращаемое значение GetHashCode() не должно изменяться. Допустим, у нас есть класс, в котором мы переопределили GetHashCode:
public class Customer {
public Customer(string name) => Name = name;
public string Name { get; set; }
// другие свойства
public override int GetHashCode()
=> Name.GetHashCode();
}

Мы разместили объект Customer в HashSet, a затем решили изменить значение Name:
var cs = new HashSet<Customer>();
var c = new Customer("Ivanov");

cs.Add(c);
Console.WriteLine(
$"{cs.Contains(c)}, всего: {cs.Count}");
c.Name = "Petrov";
Console.WriteLine(
$"{cs.Contains(c)}, всего: {cs.Count}");
Вывод:
True, всего: 1
False, всего: 1

Хэш-код объекта изменился. Объект по-прежнему находится в коллекции, но теперь его невозможно найти. Единственный способ удовлетворить правилу 2 - определить хеш-функцию, которая будет возвращать значение на основе некоторого инвариантного свойства или свойств объекта. System.Object соблюдает это правило, используя идентификатор объекта, который не изменяется.

3. GetHashCode() должен генерировать случайное распределение среди всех целых чисел для всех входных данных. Здесь не существует волшебной формулы. Если тип содержит некоторые изменяемые поля, исключите их из вычислений.

Microsoft предоставляет хороший универсальный генератор хэш-кодов. Просто скопируйте значения свойств/полей (соблюдая правила выше) в анонимный тип и хешируйте его:
public override int GetHashCode() 
=> new { PropA, PropB, PropC, … }.GetHashCode();
Это будет работать для любого количества свойств. Здесь не происходит боксинга, и используется алгоритм, уже реализованный для анонимных типов.

Для C# 7+ можно использовать кортеж. Это сэкономит несколько нажатий клавиш и, что более важно, выполняется исключительно на стеке (без мусора):
public override int GetHashCode()
=> (PropA, PropB, PropC, …).GetHashCode();

Источники:
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 10.
-
https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode
День четыреста шестьдесят шестой. #MoreEffectiveCSharp
10. Избегайте Операторов Приведения в Публичных API
Операторы приведения вводят своего рода взаимозаменяемость между классами. То есть один класс может быть заменён другим. Это может быть преимуществом: объект производного класса может быть заменён объектом его базового класса, как в классическом примере полиморфизма.

Когда вы определяете оператор преобразования для вашего типа, вы сообщаете компилятору, что ваш тип может быть заменён на целевой тип. Эти замены часто приводят к тонким ошибкам, потому что ваш тип, вероятно, не является идеальной заменой целевого типа. Методы, изменяющие состояние целевого типа, не будут отражаться на вашем типе. А если ваш оператор приведения возвращает временный объект, изменения и вовсе будут утеряны.

Рассмотрим объекты круг (Circle) и эллипс (Ellipse), производные от абстрактного класса фигуры (Shape). Допустим, вы решили сохранить эту иерархию классов, но вам может потребоваться использовать круг вместо эллипса в некоторых случаях (ведь каждый круг является эллипсом), и вы решили реализовать оператор приведения круга к эллипсу:
public class Circle : Shape {
private Point center;
private double radius;
public Circle(Point c, double r) {
center = c;
radius = r;
}
static public implicit operator Ellipse(Circle c) {
return new Ellipse(c.center, c.center, c.radius, c.radius);
}
}
Оператор неявного приведения будет вызываться всякий раз, когда один тип необходимо преобразовать в другой тип. Оператор явного (explicit) приведения вызывается только когда программист явно использует оператор приведения:
Ellipse e = (Ellipse)circle.

Теперь, можно использовать Circle в любом месте, где ожидается Ellipse, и приведение произойдёт автоматически:
public static double ComputeArea(Ellipse e) 
=> e.R1 * e.R2 * Math.PI;
Circle c1 = new Circle(new Point(3.0, 0), 5.0f);
ComputeArea(c1);
Вот что подразумевалось под взаимозаменяемостью: круг можно использовать вместо эллипса, и всё работает. Однако рассмотрим следующий метод, «сплющивающий» эллипс:
public static void Flatten(Ellipse e) {
e.R1 /= 2;
e.R2 *= 2;
}
Circle c = new Circle(new Point(3.0, 0), 5.0);
Flatten(c);

Этот код не сработает. Выполнится неявное приведение, и метод Flatten() изменит новый временный объект эллипса, который тут же попадёт в мусор. Исходный круг c при этом не изменится.

Вместо приведения используйте конструктор, принимающий исходный тип в качестве параметра. Определим в классе Ellipse конструктор, принимающий Circle как параметр и создающий эллипс из круга. Тогда код выше будет выглядеть так:
Circle c = new Circle(new Point(3.0, 0), 5.0);
Flatten(new Ellipse(c));
Большинство программистов увидят проблему: любые модификации эллипса в методе Flatten() теряются. Они исправят проблему, создав новый объект:
Circle c = new Circle(new Point(3.0, 0), 5.0);
Ellipse e = new Ellipse(c);
Flatten(e);
Переменная e будет содержать сплющенный эллипс. Заменив оператор преобразования на конструктор, мы не потеряли никакой функциональности, а только сделали очевидным, что при приведении создаётся новый объект.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 11.
День четыреста семьдесят первый. #MoreEffectiveCSharp
11. Используйте Необязательные Параметры, Чтобы Минимизировать Перегрузки Методов
C# позволяет указывать аргументы метода по позиции или по имени. Разработчики, вызывающие ваш API, могут использовать именованные параметры, хотите вы того или нет. Следующий метод:
private void SetName(string lastName, string firstName) {…}
можно вызвать с именованными параметрами:
SetName(lastName: "Иванов", firstName: "Иван");
Это гарантирует, что при прочтении этого кода не возникнет вопроса, находятся ли параметры в правильном порядке. Разработчики будут использовать именованные параметры всякий раз, когда это повышает читаемость кода (фактически, всегда, когда метод принимает несколько параметров одного типа).

Изменение имён параметров проявляется интересным образом. Имена параметров хранятся в MSIL только в месте вызова, но не в вызываемом коде. Вы можете изменить имена параметров и выпустить новую версию сборки, не нарушив работу её пользователей. Разработчики, использующие сборку, увидят проблему только когда решат перекомпилировать свой код, используя обновлённую версию. Существующие клиентские сборки продолжат работать правильно. Предположим, что вы изменили SetName():
public void SetName(string last, string first)
Вы можете скомпилировать и выпустить новую версию этой сборки. Любые сборки, которые вызывают этот метод, будут продолжать работать, но, когда разработчики сошлются на обновлённую сборку, следующий код больше не скомпилируется:
SetName(lastName: "Иванов", firstName: "Иван");

Применение именованных параметров совместно с необязательными параметрами (со значениями по умолчанию) позволяет клиенту API указывать только те параметры, которые он хочет переопределить. Это проще, чем использовать множество перегрузок. Например, метод:
public void SetName(string last = "Иванов", 
string first = "Иван") {…}
можно вызвать, либо вообще без параметров:
SetName();
либо переопределив только имя:
SetName(first: "Олег");

При этом изменение значений по умолчанию параметров метода потребует перекомпиляции вызывающего кода для отражения изменений. Существующие вызывающие сборки продолжат использовать предыдущее значение по умолчанию.

Добавление параметров вызовет ошибку времени выполнения в клиентском коде. Необязательные параметры реализованы аналогично именованным параметрам. Вызывающий код будет содержать аннотации в MSIL, отражающие существование значений по умолчанию и их значения. Там эти значения будут использоваться для всех необязательных параметров, значения для которых не указаны явно. Следовательно добавление в метод параметров (даже со значениями по умолчанию) вызовет в клиентском коде ошибку времени выполнения. Однако добавление параметров со значениями по умолчанию не вызовет ошибку при перекомпиляции клиентского кода.

Итого:
- Для вашего первоначального релиза используйте необязательные и именованные параметры, чтобы предусмотреть любые комбинации параметров, которые ваши пользователи захотят использовать.
- При последующих изменениях создавайте перегрузки методов для добавления дополнительных параметров. Таким образом, существующие клиентские приложения продолжат работать.
- При последующих изменениях избегайте изменения имён параметров в открытых или защищённых методах. Теперь они являются частью вашего публичного интерфейса.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 12.
День четыреста восемьдесят четвёртый. #MoreEffectiveCSharp
12. Ограничивайте Видимость Ваших Типов
Не каждый ваш тип должен быть публичным. Типам нужно давать наименьшую необходимую видимость для достижения вашей цели. Часто меньшую, чем кажется. Внутренние или приватные классы могут реализовывать публичные интерфейсы. Все клиенты могут получать доступ к функциональности, определённой в открытых интерфейсах и реализованной в закрытом типе. Обычно большинство программистов постоянно создают открытые классы, не задумываясь об альтернативах. Это очень просто, но лучше тщательно продумать, где будет использоваться новый тип. Он предназначен для всех клиентов или в основном используется внутри этой сборки? Многим классам достаточно быть внутренними, защищёнными, закрытыми или вложенными в другой класс. Чем меньше видимость, тем меньше мест, откуда можно получить доступ к коду, и меньше мест, которые потребуется изменить при обновлении системы.

Попробуйте создать публичный интерфейс и классы с меньшей видимостью. Внутренние (internal) классы необоснованно редко используются для ограничения области видимости типов. Предоставление функциональности через интерфейсы позволяет создавать внутренние классы, не ограничивая их использование извне. Использование внутренних классов позволяют заменить один класс на другой, если он реализует тот же интерфейс.

Рассмотрим класс, который проверяет формат номера телефона (8(xxx)xxx-xx-xx):
public class PhoneValidator {
public bool Validate(PhoneNumber ph) { … }
}
Всё работает хорошо, но спустя некоторое время требуется, чтобы проверялись и международные номера с кодом страны (+x(xxx)-xxx-xx-xx).
Вместо того, чтобы добавлять функциональность в этот класс, и использовать его напрямую, лучше уменьшить связанность между классами. Создадим интерфейс для проверки любого телефонного номера:
public interface IPhoneValidator {
bool Validate(PhoneNumber ph);
}
Затем создадим внутренние классы для проверки местных и международных номеров, реализующие этот интерфейс:
internal class LocalPhoneValidator : IPhoneValidator {…}
internal class InternationalPhoneValidator : IPhoneValidator {…}

Наконец нужен фабричный метод для создания нужного класса в зависимости от типа номера телефона:
public static IPhoneValidator CreateValidator(PhoneTypes type) {
switch (type) {
case PhoneTypes.Local:
return new LocalPhoneValidator();
case PhoneTypes.International:
default:
return new InternationalPhoneValidator();
}
}
Общую функциональность проверки можно поместить в абстрактный базовый класс.

Преимущества:
- Вне сборки виден только интерфейс, специфические классы видны только внутри сборки.
- Можно добавлять и изменять классы проверки, не ломая другие сборки в системе.
- Чем меньше открытых типов, тем меньше открытых методов, для которых нужно писать тесты.
- Под открытый API интерфейса можно создавать mock-объекты для тестирования.

Те классы и интерфейсы, которые вы публикуете для внешнего мира, являются вашим контрактом, и вы должны поддерживать его. Чем обширнее этот публичный контракт, тем более ограниченными будут возможности развития. Чем меньше открытых типов, тем больше будет вариантов для изменений и расширения функционала.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 13.
День пятьсот первый. #MoreEffectiveCSharp
13. Предпочитайте Реализацию Интерфейсов Наследованию
Абстрактные базовые классы предоставляют общего предка для иерархии классов. Интерфейс описывает группу связанных методов, содержащих функционал, который должен быть реализован типом. У каждого подхода своё применение, и они не взаимозаменяемы. Интерфейсы - это способ объявления контракта. Абстрактные базовые классы предоставляют общую абстракцию для набора связанных типов. Наследование означает «является» (описывает, что такое объект), а интерфейс означает «ведёт себя как» (описание одного из поведений объекта).

Особенности интерфейсов:
1. Определяют многократно используемое поведение.
2. Могут быть параметром или возвращаемым значением.
3. Могут быть реализованы несвязанными типами.
4. Другим разработчикам легче реализовать интерфейс, чем наследовать от созданного вами базового класса.
5. Вы не можете обеспечить реализацию методов в интерфейсе. Однако можно создать методы расширения для него. Они будут частью любого типа, который реализует интерфейс.
6. Добавление нового члена к интерфейсу сломает все классы, которые его реализуют. Каждый разработчик должен будет обновить тип, чтобы включить новый член. В C#8 в интерфейсе можно определить реализацию по умолчанию, но этот приём нужно использовать с осторожностью. Если вы обнаружите, что вам нужно добавить функциональность в интерфейс, создайте новый и наследуйте от существующего интерфейса.

Особенности абстрактных базовых классов:
- могут предоставлять некоторую реализацию для производных типов в дополнение к описанию общего поведения, обеспечивая общее повторное использование реализации.
- при добавлении нового метода в базовый класс, все производные классы автоматически и неявно обновляются.
Выбор между абстрактным базовым классом и интерфейсом - это вопрос о том, как лучше поддерживать абстракции с течением времени. Интерфейсы фиксированы, а базовые классы могут быть расширены.

Интерфейсы как параметры методов
Рассмотрим два метода:
public static void Print<T>(IEnumerable<T> col) {
foreach (T o in col)
Console.WriteLine(o);
}
public static void Print(MyCollection col) {
foreach (var o in col)
Console.WriteLine(o);
}
Любой тип, который поддерживает IEnumerable<T> (List<T>, SortedList<T>, любой массив и результаты любого запроса LINQ), может использовать первый метод. Второй метод гораздо менее пригоден для повторного использования. Использование интерфейсов в качестве типов параметров метода гораздо более универсально и намного проще для повторного использования.

Интерфейсы как возвращаемые значения
Допустим, ваш класс имеет открытый метод, который возвращает коллекцию объектов:
public List<SomeClass> DataSequence => sequence;
private List<SomeClass> sequence = new List<SomeClass>();
Это создаёт сразу две проблемы:
1. Если вы захотите изменить тип возвращаемого значения со списка на массив или сортированный список, это нарушит код, т.к. это изменит открытый интерфейс вашего класса. Такое изменение заставляет вас делать гораздо больше изменений в системе, чем необходимо: вам нужно будет поменять все места, где происходит обращение к этому методу.
2. Вторая проблема в том, что List<T> предоставляет множество методов для изменения содержащихся в нём данных. Клиенты вашего класса смогут удалять, изменять или даже заменять элементы списка. Почти наверняка вы этого не хотите. К счастью, вы можете ограничить их возможности, вернув вместо ссылки на некоторый внутренний объект интерфейс IEnumerable<SomeClass>. Используя интерфейсы в качестве типа возвращаемого значения, вы можете выбирать, какие методы и свойства для работы с предоставляемыми данными вы хотите открыть для клиентов класса.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 14.
День пятьсот шестой. #MoreEffectiveCSharp
Новый Паттерн для Логирования Исключений
Логируйте исключения из фильтра исключений, а не из блока catch.
Современные системы логирования поддерживают расширенные контекстные журналы. Вы можете добавлять поля данных в сообщения журнала, а затем использовать их при отладке. Например, отфильтровать журнал по диапазону кодов HTTP или показать только ошибки FileNotFound у пользователя Steve.

Помимо этого, современные системы поддерживают области логирования (см. BeginScope), что позволяет добавлять дополнительную информацию к сообщению в лог из разных частей метода, которая будет записана при вызове метода записи в журнал.

Большинство методов не логгируют исключения, они выбрасывают их вверх по стеку, где исключение регистрируется на более высоком уровне. Проблема этого подхода заключается в том, что область логгирования теряется после разворачивания стека:
try {
MyMethod();
}
catch (Exception e) {
logger.LogError(e, "");

}

Когда генерируется исключение, среда выполнения будет искать в стеке соответствующий обработчик. Cтек разворачивается до найденной точки и выполняется блок catch. Таким образом, дополнительная информация в области логирования, добавленная в методе MyMethod, будет потеряна при логировании исключения из блока catch.

Решение
Фильтры исключений выполняется там, где выбрасывается исключение, а не там, где оно перехватывается. Это происходит до разворачивания стека, поэтому область логирования сохраняется.
Фильтр исключений должен возвращать булево значение, указывающее, подходит ли блок catch под фильтр. В нашем случае логирование - просто побочный эффект, который не влияет на соответствие блока catch фильтру. Определим два метода, возвращающие true или false в зависимости от того, нужно нам «проглотить» исключение, либо выбросить его выше по стеку:

1. Rethrow можно использовать, когда catch не содержит ничего, кроме throw;. В таком случае Rethrow логгирует исключение, вернёт false, блок catch не выполнится, а среда выполнения продолжит искать обработчик выше по стеку:
try {
MyMethod();
}
catch (Exception e) when
(Rethrow(()=>logger.LogError(e, ""))
{
throw;
}
public static bool Rethrow(Action action) {
action();
return false;
}

2. Другой сценарий, когда catch обрабатывает исключение. В этом случае метод Handle логирует исключение, вернёт true, и исключение будет обработано в блоке catch:
try {
MyMethod();
}
catch (Exception e) when
(Handle(()=>logger.LogError(e, ""))
{
// обработка исключения
}
public static bool Handle(Action action) {
action();
return true;
}

Источник: https://blog.stephencleary.com/2020/06/a-new-pattern-for-exception-logging.html
День пятьсот сороковой. #MoreEffectiveCSharp
14. Различайте Интерфейсные и Абстрактные Методы
На первый взгляд реализация интерфейса выглядит так же, как и переопределение абстрактной функции. Но есть различия:
- Реализация абстрактного члена базового класса должна быть виртуальной; реализация члена интерфейса – не обязательно.
- Интерфейсы могут быть реализованы явно, что скроет их реализацию от открытого интерфейса класса.

Рассмотрим варианты реализации простого интерфейса в иерархии классов:
interface IMessage {
void Message();
}
public class MyClass : IMessage {
public void Message() => WriteLine(nameof(MyClass));
}
Метод Message() является частью публичного интерфейса MyClass, а также может быть вызван через приведение к IMessage. Теперь добавим производный класс:
public class MyDerivedClass : MyClass {
public new void Message() =>
WriteLine(nameof(MyDerivedClass));
}
Нам пришлось добавить ключевое слово new. MyClass.Message() не является виртуальным, его нельзя переопределить. Класс MyDerived создаёт новый метод Message, который не переопределяет MyClass.Message, а скрывает его. Однако MyClass.Message по-прежнему доступен через IMessage:
MyDerivedClass d = new MyDerivedClass();
d.Message(); // выведет "MyDerivedClass"
IMessage m = d as IMessage;
m.Message(); // выведет "MyClass"

Приведение к IMessage вызывает базовую реализацию. Если доступа к базовому классу нет, можно реализовать интерфейс и в производном классе:
public class MyDerivedClass : MyClass, IMessage {…}
Тогда поведение изменится:
m.Message(); // выведет "MyDerivedClass"
Ключевое слово new всё равно придётся использовать. Базовая реализация будет доступна через апкаст:
MyClass b = d;
b.Message(); // выведет "MyClass"

Если доступ к базовому классу есть, объявите интерфейсный метод виртуальным, а в производных классах используйте ключевое слово override. Тогда переопределённая версия метода будет вызываться всегда: и из производного класса, и после приведения как к базовому классу, так и к интерфейсу.

Также можно реализовать интерфейс без фактической реализации его методов:
public abstract class MyClass : IMessage {
public abstract void Message();
}
Тогда все конкретные производные типы должны переопределить и предоставить собственную реализацию Message(). Интерфейс IMessage является частью объявления MyClass, но реализация методов откладывается до каждого конкретного производного класса.

Можно реализовать паттерн Шаблонный метод:
public class MyClass : IMessage {
protected virtual void OnMessage() {}
public void Message() {
OnMessage();
WriteLine(nameof(MyClass));
}
}
Любой производный класс может переопределить OnMessage() и добавить свой код в реализацию контракта IMessage.

Реализация интерфейсов дает больше возможностей, чем создание и переопределение виртуальных функций. Вы можете точно решить, как и когда производные классы могут изменять поведение по умолчанию членов интерфейса. Методы интерфейса - это не виртуальные методы, а отдельный контракт.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 15.
День пятьсот шестьдесят четвёртый. #MoreEffectiveCSharp
16. Реализуйте Паттерн Событий для Уведомлений. Начало
Паттерн событий в .NET - это лишь соглашения о синтаксисе в паттерне Наблюдатель. События определяют уведомления для вашего типа. Они основаны на делегатах, чтобы обеспечить типобезопасные сигнатуры функций для обработчиков событий. События используются, когда ваш тип должен общаться с несколькими клиентами, чтобы информировать их о происходящем в системе.

Например, вы создаёте класс, который действует как диспетчер всех сообщений в приложении. Он принимает все сообщения из источников в вашем приложении и отправляет их всем заинтересованным клиентам. Клиенты могут быть подключены к консоли, базе данных, системному журналу или какому-либо другому механизму:
public class Logger {
static Logger() {
Singleton = new Logger();
}
private Logger(){}
public static Logger Singleton { get; }
// Определяем событие:
public event EventHandler<LoggerEventArgs> Log;
// добавляем сообщение
public void AddMsg(int priority, string msg) =>
Log?.Invoke(this, new LoggerEventArgs(priority, msg));
}
Метод AddMsg использует оператор ?., который позволяет убедиться, что событие возникает только если на него подписаны наблюдатели.
LoggerEventArgs содержит свойства для приоритета и текста сообщения:
public class LoggerEventArgs : EventArgs {
public LoggerEventArgs(int priority, string msg) {
Priority = priority;
Message = msg;
}
public int Priority { get; }
public string Message { get; }
}
Внутри класса Logger поле event определяет обработчик событий. Компилятор видит определение публичного поля event и создаёт для вас операторы Add (+=) и Remove (-=) для подписки и отписки, которые гарантированно являются потокобезопасными.

Следующий упрощённый класс является примером подписчика. Он подписывается в статическом конструкторе и передаёт делегат, направляющий все сообщения в консоль:
class ConsoleLogger {
static ConsoleLogger() =>
Logger.Singleton.Log += (sender, msg) =>
Console.WriteLine("{0}:\t{1}",
msg.Priority.ToString(),
msg.Message);
}
События уведомляют любое количество заинтересованных клиентов о том, что что-то произошло. При этом классу Logger не требуется никаких предварительных знаний о том, какие объекты заинтересованы в регистрации событий.
Пример использования:
var cl = new ConsoleLogger();
Logger.Singleton.AddMsg(1, "test message");

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

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 15.
День пятьсот шестьдесят пятый. #MoreEffectiveCSharp
16. Реализуйте Паттерн Событий для Уведомлений. Окончание
Начало
Класс Logger содержал только одно событие. Есть классы, которые имеют большое количество событий (у компонентов UI их может быть до сотни). Тогда использовать отдельное поле для каждого события неприемлемо. В приложении фактически используется лишь небольшая часть из них. В этом случае можно создавать объекты событий во время выполнения только при необходимости. Добавим в класс Logger указание подсистемы, отправляющей сообщения. Клиенты будут регистрироваться на сообщения определённой подсистемы. Обновлённый метод AddMsg() теперь принимает строковый параметр, обозначающий подсистему, отправившую сообщение. Кроме того, будет вызываться событие с ключом в виде пустой строки для подписчиков на сообщения от всех подсистем:
public class Logger {
private static EventHandlerList
Handlers = new EventHandlerList();

static public void AddLogger(string system,
EventHandler<LoggerEventArgs> ev) =>
Handlers.AddHandler(system, ev);

static public void RemoveLogger(string system,
EventHandler<LoggerEventArgs> ev) =>
Handlers.RemoveHandler(system, ev);

static public void AddMsg(string system,
int priority, string msg){
if (!string.IsNullOrEmpty(system)) {
EventHandler<LoggerEventArgs> handler =
Handlers[system] as
EventHandler<LoggerEventArgs>;

LoggerEventArgs args =
new LoggerEventArgs(priority, msg);
handler?.Invoke(null, args);

// для получателей всех сообщений
handler = Handlers[""] as
EventHandler<LoggerEventArgs>;
handler?.Invoke(null, args);
}
}
}
В этом примере отдельные обработчики событий хранятся в коллекции EventHandlerList. Клиенты подписываются на события подсистемы через AddLogger, передавая ему строковое имя подсистемы и свой делегат обработки сообщения. Первый вызов создаёт событие для подсистемы, последующие вызовы используют это событие. К сожалению, не существует обобщённой версии EventHandlerList, поэтому в AddMsg приходится использовать приведение типов для элементов коллекции.

Вместо EventHandlerList можно использовать словарь Dictionary<string, EventHandler<LoggerEventArgs>>, что добавит немного кода, но позволит использовать строгую типизацию:

static public void AddLogger(string system,
EventHandler<LoggerEventArgs> ev) {
if (Handlers.ContainsKey(system))
Handlers[system] += ev;
else
Handlers.Add(system, ev);
}

static public void AddMsg(…) {
if (string.IsNullOrEmpty(system)) {
EventHandler<LoggerEventArgs> handler = null;
Handlers.TryGetValue(system, out handler);

}
}

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 15.
День пятьсот девяносто седьмой. #MoreEffectiveCSharp
17. Избегайте Возврата Ссылок на Внутренний Класс
Вы можете думать, что свойство только для чтения доступно только для чтения и вызывающие объекты не могут его изменять. К сожалению, так получается не всегда. Если свойство возвращает ссылочный тип, вызывающий код может получить доступ к любому общедоступному члену этого объекта, включая те, которые изменяют его состояние. Например:
public class MyObject {
public MyObject() {
Data = new List<ImportantData>();
}
public List<ImportantData> Data { get; }

}
Получаем доступ к списку и удаляем его элементы. Это поведение не предусмотрено, но и не запрещено:
var stuff = obj.Data;
stuff.Clear();
Таким образом свойство только для чтения - дыра в продуманной инкапсуляции вашего класса. Не говоря уже о том, что создатель класса рассматривает это свойство как неизменяемое и не ожидает подвоха.

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

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

1. Типы значений копируются, когда клиенты обращаются к ним через свойство. Любые изменения копии, полученной клиентами вашего класса, не влияют на внутреннее состояние вашего объекта.

2. Неизменяемые типы, такие как System.String, также безопасны. Вы можете возвращать строки или любые неизменяемые типы, зная, что ни один клиент вашего класса не сможет их изменить.

3. Интерфейсы позволяют клиентам получать доступ к подмножеству функционала вашего внутреннего члена. Предоставляя функциональность через интерфейс, вы сводите к минимуму вероятность того, что ваши внутренние данные изменятся не так, как вы предполагали. Клиенты могут получить доступ к внутреннему объекту через предоставленный вами интерфейс, который не будет включать в себя все функции класса. В нашем случае публичное свойство можно представлять клиентам в виде IEnumerable<T> вместо List<T>.

4. Последний вариант – предоставить объект-оболочку, минимизирующую варианты доступа к содержащемуся объекту. В .NET представлены различные типы неизменяемых коллекций, которые это поддерживают. Тип System.Collections.ObjectModel.ReadOnlyCollection<T> - это стандартный способ обернуть коллекцию для предоставления во вне данных только для чтения:
public class MyObject {
private List<ImportantData> listOfData =
new List<ImportantData>();

public ReadOnlyCollection<ImportantData> Data =>
new ReadOnlyCollection<ImportantData>(listOfData);

}

Итого
Предоставление ссылочных типов через общедоступный интерфейс позволяет пользователям вашего объекта изменять его внутренние компоненты, не используя методы и свойства, которые вы определили. Необходимо изменить интерфейсы вашего класса, чтобы учесть, что вы отдаёте ссылки, а не значения. Ваши клиенты могут вызывать любые методы предоставленных им объектов. Ограничьте доступ, предоставляя внутренние данные с помощью интерфейсов, оболочек или типов значений.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 17.
День шестьсот девяносто восьмой. #MoreEffectiveCSharp
18. Предпочитайте Переопределения Обработчикам Событий
Многие классы .NET предоставляют два разных способа обработки событий. Вы можете подписать обработчик событий или переопределить виртуальную функцию базового класса. Внутри производных классов вы всегда должны переопределять виртуальную функцию. Ограничьте использование обработчиков событий реагированием на события в несвязанных объектах.

Рассмотрим код приложения WPF, где нужно реагировать на события нажатия мыши. Вы можете переопределить метод OnMouseDown():
public partial class MainWindow : Window
{
//…
protected override void OnMouseDown(MouseButtonEventArgs e)
{
DoMouseThings(e);
base.OnMouseDown(e);
}
}

Либо вы можете подписать обработчик события, что требует изменений как в файле C#, так и в файле XAML:
<Window x:Class="WpfApp1.MainWindow"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
MouseDown="OnMouseDownHandler">
<Grid></Grid>
</Window>

public partial class MainWindow : Window
{
//…
private void OnMouseDownHandler(
object sender, MouseButtonEventArgs e)
{
DoMouseThings(e);
}
}

Следует предпочесть первое решение, и вот почему. При переопределении функции сначала вызывается ваш обработчик, а вызов функции базового класса отвечает за вызов любых обработчиков событий в цепочке. В некоторых редких случаях вам может потребоваться заменить поведение по умолчанию, чтобы другие обработчики не вызывались, но зачастую поведение по умолчанию – то, что вам нужно. Кроме того, в первом варианте нужно поддерживать изменение в одном месте, а во втором – в двух.

Хорошо, но подписка на события зачем-то добавлена. Зачем?
1. Переопределения предназначены для производных классов. Но сторонние классы должны использовать механизм событий.
2. XAML поддерживает декларативную подписку на события. То есть дизайнер приложения в визуальном редакторе может выбрать действие при нажатии мыши, если оно доступно, и не добавлять работы программисту.
3. Это позволяет подписываться на события во время выполнения. Вы можете подключить разные обработчики событий, в зависимости от обстоятельств выполнения программы.
4. Возможно подключить несколько обработчиков к одному событию.

Итого
Когда у вас есть одна функция, которая обрабатывает одно событие в производном классе, переопределение является лучшим подходом. Это решение легче поддерживать, оно с большей вероятностью сохранит корректность с течением времени и более эффективно. Оставьте обработчики событий для других целей.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 18.
День семьсот одиннадцатый. #MoreEffectiveCSharp
19. Избегайте Перегрузки Методов, Определённых в Базовых Классах
Когда базовый класс выбирает имя члена, он назначает определённый семантический смысл этому имени. Ни при каких обстоятельствах производный класс не должен использовать то же самое имя для других целей. И всё же есть много причин, по которым производный класс может захотеть использовать то же имя. Например, реализовать ту же семантику другим способом или с другими параметрами. Но вы не должны перегружать методы, объявленные в базовом классе.

Правила разрешения перегрузки сложны по определению. Возможными кандидатами могут быть методы класса, любого из его базовых классов, методы расширения класса или интерфейсов, которые он реализует. Добавьте к этому обобщённые методы и обобщённые методы расширения, методы с необязательными параметрами, и уже вообще непонятно, сможет ли хоть кто-нибудь поручиться, какой из методов будет вызван. Вы действительно хотите ещё больше усложнить ситуацию?

Добавление перегрузки для метода базового класса увеличивает вероятность того, что ваша интерпретация спецификации не совпадёт с интерпретацией компилятора, и, безусловно, запутает ваших пользователей. Решение простое: выберите другое имя метода.

Рассмотрим пару примеров:
public class Fruit { }
public class Apple : Fruit { }
Вот класс с методом, использующим производный параметр (Apple):
public class Animal {
public void Eat(Apple food) =>
WriteLine("Animal.Eat");
}
var obj1 = new Animal();
obj1.Eat(new Apple());
Понятно, что это выведет "Animal.Eat". Добавим производный класс с перегруженным методом с параметром базового типа:
public class Monkey : Animal {
public void Eat(Fruit food) =>
WriteLine("Monkey.Eat");
}
Итак, что выведет следующий код?
var obj2 = new Monkey();
obj2.Eat(new Apple());
obj2.Eat(new Fruit());

Оба вызова выведут "Monkey.Eat". Всегда в первую очередь вызывается метод производного класса, даже если в базовом классе есть более подходящий кандидат. Смысл в том, что автор производного класса лучше знает сценарий использования, поэтому производному методу отдаётся предпочтение. А если вот так:
Animal obj3 = new Monkey();
obj3.Eat(new Apple());
Смотрим внимательно: тип времени компиляции obj3 - Animal (базовый), хотя, во время выполнения тип будет Monkey (производный). Метод Eat не виртуальный, поэтому obj3.Eat() должен использовать Animal.Eat.

Больше ада! Добавим дженериков:
public class Animal {

public void Consume(IEnumerable<Apple> food) =>
WriteLine("Animal.Consume");
}
И перегрузку с коллекцией базового типа в производном классе:
public class Monkey : Animal {

public void Consume(IEnumerable<Fruit> food) =>
WriteLine("Monkey.Consume");
}
var food = new List<Apple> { new Apple(), new Apple() };
var obj2 = new Monkey();
obj2.Consume(food);

Что будет выведено на этот раз? Начиная с C#4.0 обобщённые интерфейсы поддерживают ковариантность и контравариантность. Это означает, что Monkey.Consume является кандидатом для IEnumerable<Apple>, хотя формально тип его параметра IEnumerable<Fruit>. Однако более ранние версии C# не поддерживают вариантности, и в них обобщённые параметры инвариантны. В этом случае единственным кандидатом будет Animal.Consume.

Да, вы можете удивить друзей на вечеринке программистов глубокими познаниями логики разрешения перегрузок в C#. Но не ожидайте, что пользователи вашего API будут иметь такие подробные знания, чтобы правильно использовать ваш API. Просто не перегружайте методы, объявленные в базовом классе. Это не представляет никакой ценности и только приведёт ваших пользователей в замешательство.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 19.
День семьсот двадцать четвёртый. #MoreEffectiveCSharp
20. Учитывайте, Что События Усиливают Связанность Объектов во Время Выполнения
Кажется, что события предоставляют способ полностью отделить ваш класс от классов, которые он должен уведомлять. Вы определяете тип события и позволяете подписываться на него. Внутри вашего класса вы вызываете событие. Ваш класс ничего не знает о подписчиках и не налагает ограничений на них. Код можно расширить, создавая любое необходимое поведение при возникновении этих событий. Однако не всё так просто.

Начнём с того, что некоторые типы аргументов событий содержат флаги состояния, которые предписывают вашему классу выполнять определённые операции.
public class WorkerEngine {
public event
EventHandler<WorkerEventArgs> OnProgress;
public void DoLotsOfStuff() {
for (int i = 0; i < 100; i++) {
SomeWork();
var args = new WorkerEventArgs();
args.Percent = i;
OnProgress?.Invoke(this, args);
if (args.Cancel) return;
}
}
private void SomeWork(){…}
}
Теперь все подписчики на это событие связаны. Если один подписчик запросит отмену операции, установив Cancel в true, другой может отменить это. Таким образом, последний подписчик в цепочке может переопределить действие любого предыдущего. Невозможно заставить иметь только одного подписчика, и нет способа гарантировать, что делегат какого-то из подписчиков будет выполнен последним. Вы можете изменить аргументы события, чтобы гарантировать, что после установки флага отмены ни один подписчик не сможет его выключить:
public class WorkerEventArgs : EventArgs {
public int Percent { get; set; }
public bool Cancel { get; private set; }
public void RequestCancel() {
Cancel = true;
}
}
Это изменение сработает в этом случае, но так можно сделать не всегда. Если вам нужно убедиться, что есть ровно один подписчик, придётся выбрать другой способ связи классов. Например, определить интерфейс и вызывать метод интерфейса вместо события. Или запрашивать делегат подписчика в качестве параметра метода. Затем этот единственный подписчик может решить, хочет ли он поддерживать несколько подписчиков и как организовать семантику запросов на отмену.

Во время выполнения возникает ещё одна форма связи между источником события и подписчиками. Источник содержит ссылку на делегат, который предоставляет подписчик. Время жизни объекта подписчика теперь будет соответствовать времени жизни объекта источника. Источник будет вызывать обработчик подписчика всякий раз, когда происходит событие. Но это не должно продолжаться после удаления подписчика. То есть подписчикам на события необходимо реализовать паттерн Disposable и отписываться от события в методе Dispose(). В противном случае подписчики продолжат существовать, поскольку в источнике будут ссылки на их делегаты.

Это еще один случай, когда связывание во время выполнения может вам дорого обойтись несмотря на то, что связь кажется более слабой. Связь на основе событий ослабляет статическую связь между типами, но за счет более тесной связи между источником события и подписчиками во время выполнения.

Вы должны учесть эти проблемы в своем дизайне, чтобы использовать события.

Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 20.
День 1191.
Подборка тегов, используемых в постах на канале, чтобы облегчить поиск. Не могу гарантировать, что все 1190 постов идеально и корректно помечены тегами, но всё-таки, эта подборка должна помочь.

Общие
Эти посты на совершенно разные темы, помечены этими тегами только с целью различать общую направленность поста.

#ЗаметкиНаПолях – технические посты. Краткие описания теории, особенности языка C# и платформы .NET, примеры кода, и т.п.

#Шпаргалка - примеры кода, команды для утилит и т.п.

#Юмор – шутки, комиксы и просто весёлые тексты или ссылки на видео.

#Оффтоп – всё прочее.


Специализированные
Эти теги более тематические, выделяют основную тему поста.

#Карьера – советы по повышению продуктивности, карьерному росту, прохождению собеседований и т.п.

#Книги – обзоры книг, которые (чаще всего) я лично прочитал, либо ещё нет, но советую прочитать.

#Курсы – обзоры и ссылки на онлайн курсы.

#МоиИнструменты – различные программы, утилиты и расширения IDE, которые я использую в работе.

#ЧтоНовенького – новости из мира .NET.


Узкоспециализированные
Эти теги относятся к определённой узкой теме.

#AsyncTips – серия постов из книги Стивена Клири “Конкурентность в C#”
#AsyncAwaitFAQ – серия постов “Самые Частые Ошибки при Работе с async/await.”

#BestPractices – советы по лучшим практикам, паттернам разработки.

#DesignPatterns – всё о паттернах проектирования, SOLID, IDEALS и т.п.

#DotNetAZ – серия постов с описанием терминов из мира .NET.

#GC – серия постов “Топ Вопросов о Памяти в .NET.” от Конрада Кокосы.

#MoreEffectiveCSharp – серия постов из книги Билла Вагнера “More Effective C#”.

#Testing – всё о тестировании кода.

#TipsAndTricks – советы и трюки, в основном по функционалу Visual Studio.

#Quiz - опросы в виде викторины.

#97Вещей – серия постов из книги “97 Вещей, Которые Должен Знать Каждый Программист”.

#ВопросыНаСобеседовании – тег говорит сам за себя, самые часто задаваемые вопросы на собеседовании по C#, ASP.NET и .NET.
#ЗадачиНаСобеседовании – похоже на вопросы, но здесь больше приводятся практические задачи. Чаще всего это 2 поста: собственно задача и ответ с разбором.

#КакСтатьСеньором – серия постов «Как Стать Сеньором» с советами о продвижении по карьерной лестнице.

Помимо этого, можно просто воспользоваться поиском по постам и попробовать найти то, что вам нужно.
1👍60👎1