День триста девяносто первый. #MoreEffectiveCSharp
3. Использование значимых и ссылочных типов
Вы должны решить, как будут вести себя все экземпляры вашего типа. Это важное решение, которое нужно принять при создании, потому что изменение структуры на класс может поломать довольно много кода самым непредсказуемым образом.
Правильный выбор зависит от того, как вы собираетесь использовать новый тип. Структуры не полиморфны, они должны быть небольшими и лучше подходят для хранения данных, которыми манипулирует приложение. Классы могут быть полиморфными, формируют иерархию типов приложения и должны использоваться для определения поведения приложения. Подумайте об ожидаемых обязанностях вашего нового типа и, исходя из этого, решите, какой тип создать.
Например, тип используется как возвращаемое значение из метода:
Теперь немного подробнее рассмотрим, как эти типы хранятся в памяти, а также вопросы производительности, связанные с моделями хранения:
Решение использовать структуру или класс является очень важным. Изменение структуры на класс в готовом коде влечёт за собой неприятные последствия. Допустим вы создали структуру работника, хранящую размер заработной платы. Затем вы решаете, что могут быть разные классы сотрудников: продавцы получают комиссионные, а менеджеры получают бонусы. Вы решаете изменить структуру на класс. Это нарушит большую часть клиентского кода, использующего структуру. Возврат по значению становится возвратом по ссылке. Параметры, которые передавались по значению, теперь передаются по ссылке. Помимо хранения элементов данных, которые определяют сотрудника, вы добавили обязанности: расчёт зарплаты. Обязанности — это ответственность классов. Классы могут легко определять полиморфные реализации общих обязанностей; структуры должны ограничиваться хранением значений.
Документация по .NET рекомендует рассматривать размер типа как определяющий фактор между типами значений и ссылочными типами. На самом деле, гораздо лучшим фактором является способ использования типа. Если вы ответите «да» на все эти вопросы, создавайте структуру:
1. Основная обязанность типа - хранение данных?
2. Можно ли сделать этот тип неизменяемым?
3. Ожидается, что тип будет маленьким?
4. Открытый интерфейс типа содержит только свойства для доступа к данным?
5. У типа никогда не будет подклассов?
6. Тип никогда не будет использоваться полиморфно?
Если вы сомневаетесь относительно ожидаемого использования типа, используйте ссылочный тип.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 4.
  3. Использование значимых и ссылочных типов
Вы должны решить, как будут вести себя все экземпляры вашего типа. Это важное решение, которое нужно принять при создании, потому что изменение структуры на класс может поломать довольно много кода самым непредсказуемым образом.
Правильный выбор зависит от того, как вы собираетесь использовать новый тип. Структуры не полиморфны, они должны быть небольшими и лучше подходят для хранения данных, которыми манипулирует приложение. Классы могут быть полиморфными, формируют иерархию типов приложения и должны использоваться для определения поведения приложения. Подумайте об ожидаемых обязанностях вашего нового типа и, исходя из этого, решите, какой тип создать.
Например, тип используется как возвращаемое значение из метода:
private MyData myData;Если
public MyData Foo() => myData;
MyData v = Foo();
TotalSum += v.Value;
MyData — значимый тип, содержимое возврата копируется в переменную v. А если MyData — ссылочный тип, то экспортируется ссылка на внутреннюю переменную. Вы нарушаете принцип инкапсуляции, что может позволить клиентам изменять объект по ссылке, обходя ваш API. Другой вариант:public MyData Foo2 () => myData.CreateCopy();Теперь
v — это копия исходных данных myData. В куче создаются два объекта. Исчезла проблема раскрытия внутренних данных, но мы создали дополнительный объект в куче. В общем, это неэффективно. Типы, которые используются для экспорта данных с помощью открытых методов и свойств, должны быть значимыми типами.Теперь немного подробнее рассмотрим, как эти типы хранятся в памяти, а также вопросы производительности, связанные с моделями хранения:
public class C {
  private MyType a = new MyType();
  private MyType b = new MyType();
  …
}
C c = new C();
Сколько объектов создано? Насколько они большие? Это зависит. Если MyType — структура, выделен один объект, размер которого в два раза больше размера MyType. А если MyType — класс, выделено 3 объекта: для типа C (8 байт в 32-разрядной системе) и 2 для типа MyType. Разница возникает из-за того, что структуры хранятся внутри объекта, а каждая переменная ссылочного типа содержит ссылку, и требует выделения дополнительного места в хранилище. Это особенно важно, если вы собираетесь выделить место под массив. Массив структур выделится за 1 раз, массив ссылочных типов изначально будет заполнен null, но потребует дополнительного выделения места под каждый элемент при его инициализации.Решение использовать структуру или класс является очень важным. Изменение структуры на класс в готовом коде влечёт за собой неприятные последствия. Допустим вы создали структуру работника, хранящую размер заработной платы. Затем вы решаете, что могут быть разные классы сотрудников: продавцы получают комиссионные, а менеджеры получают бонусы. Вы решаете изменить структуру на класс. Это нарушит большую часть клиентского кода, использующего структуру. Возврат по значению становится возвратом по ссылке. Параметры, которые передавались по значению, теперь передаются по ссылке. Помимо хранения элементов данных, которые определяют сотрудника, вы добавили обязанности: расчёт зарплаты. Обязанности — это ответственность классов. Классы могут легко определять полиморфные реализации общих обязанностей; структуры должны ограничиваться хранением значений.
Документация по .NET рекомендует рассматривать размер типа как определяющий фактор между типами значений и ссылочными типами. На самом деле, гораздо лучшим фактором является способ использования типа. Если вы ответите «да» на все эти вопросы, создавайте структуру:
1. Основная обязанность типа - хранение данных?
2. Можно ли сделать этот тип неизменяемым?
3. Ожидается, что тип будет маленьким?
4. Открытый интерфейс типа содержит только свойства для доступа к данным?
5. У типа никогда не будет подклассов?
6. Тип никогда не будет использоваться полиморфно?
Если вы сомневаетесь относительно ожидаемого использования типа, используйте ссылочный тип.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 4.
День четырёхсотый. #MoreEffectiveCSharp
5. Убедитесь, что 0 является допустимым состоянием для значимых типов
Инициализация по умолчанию в .NET устанавливает все значения в 0. Невозможно запретить другим программистам создавать экземпляр значимого типа со значениями по умолчанию. Сделайте 0 значением по умолчанию для типа.
Особый случай - перечисления. Никогда не создавайте перечисление, которое не содержит 0 в качестве допустимого значения:
Кроме того, это влияет на объекты, содержащие перечисление:
✅ По возможности выберите наилучшее значение по умолчанию для 0, либо добавьте специальное неинициализированное значение
✅ Для перечислений, являющихся битовыми флагами, убедитесь, что 0 является действительным значением и означает «отсутствие всех флагов». Подробнее о битовых флагах.
⚠️ Перечисления - лишь обёртка для набора целочисленных констант. Если это не обеспечивает нужную вам абстракцию, следует рассмотреть возможность использования другой конструкции языка.
Другая распространённая проблема состоит в инициализации ссылочных полей структур:
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 5.
  5. Убедитесь, что 0 является допустимым состоянием для значимых типов
Инициализация по умолчанию в .NET устанавливает все значения в 0. Невозможно запретить другим программистам создавать экземпляр значимого типа со значениями по умолчанию. Сделайте 0 значением по умолчанию для типа.
Особый случай - перечисления. Никогда не создавайте перечисление, которое не содержит 0 в качестве допустимого значения:
public enum PlanetИ
{
// Явно назначенные значения
// По умолчанию начинаются с 0
Mercury = 1,
Venus = 2,
Earth = 3,
…
}
Planet sphere = new Planet();
var anotherSphere = default(Planet);
sphere, и anotherSphere имеют значение 0, которое не является допустимым. Любой код, предполагающий, что перечисления ограничены определённым набором значений, возможно, будет работать неверно. Кроме того, это влияет на объекты, содержащие перечисление:
public struct PlanetData
{
private Planet planet;
private double magnitude; //яркость
}
var data = new PlanetData();
data имеет яркость 0, что нормально, но значение планеты недопустимое.✅ По возможности выберите наилучшее значение по умолчанию для 0, либо добавьте специальное неинициализированное значение
None = 0, которое можно будет изменить позже.✅ Для перечислений, являющихся битовыми флагами, убедитесь, что 0 является действительным значением и означает «отсутствие всех флагов». Подробнее о битовых флагах.
⚠️ Перечисления - лишь обёртка для набора целочисленных констант. Если это не обеспечивает нужную вам абстракцию, следует рассмотреть возможность использования другой конструкции языка.
Другая распространённая проблема состоит в инициализации ссылочных полей структур:
public struct LogMessage
{
public int ErrLevel;
public string Message;
}
LogMessage myMsg = new LogMessage();
myMsg содержит null в поле Message. Невозможно принудительно инициировать поле другим значением, но можно локализовать проблему, используя свойства. Добавьте логику в свойство, чтобы возвращать пустую строку вместо null:public struct LogMessageИспользуйте свойство и в коде типа. Это позволит локализовать проверку на
{
public int ErrLevel;
private string msg;
public string Message
{
get => msg ?? string.Empty;
set => msg = value;
}
}
null в одном месте. Аксессор свойства почти наверняка будет встроен при компиляции, поэтому вы получите эффективный код и сведёте к минимуму ошибки.Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 5.
День четыреcта пятый. #MoreEffectiveCSharp
6. Убедитесь, что свойства ведут себя как данные
Свойства ведут себя двояко. Снаружи они похожи на простой доступ к данным, однако изнутри это методы. Такое поведение может вызвать соблазн создать свойство, выполняющее некоторую работу перед выдачей результата. Однако имейте в виду, что клиенты класса ждут от свойств некоторого определённого поведения:
1. Последующие вызовы аксессора (
2. Аксессор свойства не будет выполнять большую работу. Доступ к данным никогда не должен быть дорогой операцией. Аналогично, методы доступа к набору свойств, вероятно, будут выполнять некоторую проверку, но их вызов не должен быть дорогим. Например, в цикле
если бы свойство
Соответствовать таким ожиданиям клиентов не сложно:
1. Используйте автосвойства.
2. Реализуйте проверку значения свойства в мутаторе (
3. Простые математические расчёты (расстояние до точки по координатам или площадь фигуры) никак не влияют на производительность, поэтому их можно безболезненно включить в аксессор.
Однако, если расчёт значения дорог, нужно продумать доступ к нему:
1. Получение однократно и сохранение в кэше
*
2. Аналогичный способ с использованием типа Lazy<T>:
3. Предыдущие примеры предполагают, что значение в БД никто не изменяет. В противном случае, если требуется как получать, так и изменять значение в БД, доступ лучше реализовать через методы с понятными именами (например,
Наконец, имейте в виду, что отладчики могут автоматически вызывать методы доступа к свойству для отображения значения при отладке. Если аксессор выбрасывает исключение, занимает много времени или изменяет внутреннее состояние приложения, это усложнит ваши сеансы отладки. В этом случае в Visual Studio можно использовать трюк «Определение значения без побочных эффектов».
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 6.
  6. Убедитесь, что свойства ведут себя как данные
Свойства ведут себя двояко. Снаружи они похожи на простой доступ к данным, однако изнутри это методы. Такое поведение может вызвать соблазн создать свойство, выполняющее некоторую работу перед выдачей результата. Однако имейте в виду, что клиенты класса ждут от свойств некоторого определённого поведения:
1. Последующие вызовы аксессора (
get) без каких-либо промежуточных действий должны приводить к тому же результату (понятно, что это не относится к случаю изменения свойства другими потоками).2. Аксессор свойства не будет выполнять большую работу. Доступ к данным никогда не должен быть дорогой операцией. Аналогично, методы доступа к набору свойств, вероятно, будут выполнять некоторую проверку, но их вызов не должен быть дорогим. Например, в цикле
for(int i=0;i<arr.Length;i++) если бы свойство
Length считало количество элементов на каждой итерации, такой простой цикл выполнялся бы в квадратичное время, и никто бы его не использовал.Соответствовать таким ожиданиям клиентов не сложно:
1. Используйте автосвойства.
2. Реализуйте проверку значения свойства в мутаторе (
set), а не в аксессоре (get). Таким образом она будет выполняться 1 раз при записи, а не при каждом чтении значения. 3. Простые математические расчёты (расстояние до точки по координатам или площадь фигуры) никак не влияют на производительность, поэтому их можно безболезненно включить в аксессор.
Однако, если расчёт значения дорог, нужно продумать доступ к нему:
// Плохая реализацияПользователи не ожидают, что доступ к свойству потребует обращения к базе данных. Поэтому API нужно изменить. Есть несколько способов:
public class MyType {
public string Name => GetFromDB();
}
1. Получение однократно и сохранение в кэше
public class MyType {
  private string name;
  public string Name => (name != null) ? 
    name : GetFromDB();
}*
GetFromDB в этом случае устанавливает значение name.2. Аналогичный способ с использованием типа Lazy<T>:
public class MyType {
  private Lazy<string> lazyName;
  public MyType() {
    lazyName = new Lazy<string>(() => GetFromDB());
  }
  public string Name => lazyObjectName.Value;
}
Это хорошо работает, когда свойство Name требуется только изредка. Вы не извлекаете значение, если оно не нужно. Но первый обратившийся к нему «страдает за всех». Если к свойству обращаются часто, можно рассмотреть вариант получения значения в конструкторе сразу при создании экземпляра.3. Предыдущие примеры предполагают, что значение в БД никто не изменяет. В противном случае, если требуется как получать, так и изменять значение в БД, доступ лучше реализовать через методы с понятными именами (например,
LoadFromDatabase и SaveToDatabase), чтобы клиентам был очевиден объём работы, требующийся для этого. Наконец, имейте в виду, что отладчики могут автоматически вызывать методы доступа к свойству для отображения значения при отладке. Если аксессор выбрасывает исключение, занимает много времени или изменяет внутреннее состояние приложения, это усложнит ваши сеансы отладки. В этом случае в Visual Studio можно использовать трюк «Определение значения без побочных эффектов».
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 6.
День четыреста тридцать первый. #MoreEffectiveCSharp
7. Ограничивайте Область Действия Типа, Используя Анонимные Типы
Для создания пользовательских типов, представляющих объекты и структуры данных программы, вы можете выбрать классы, структуры, кортежи или анонимные типы. Классы и структуры настолько богаты в смысле выражения ваших замыслов, что заставляют многих разработчиков выбирать их, не рассматривая другие возможности. Вы можете написать более читаемый код, используя более простые конструкции: анонимные типы или кортежи.
Анонимные типы — это генерируемые компилятором неизменяемые ссылочные типы, которые можно объявить так:
- новый закрытый (sealed) класс,
- этот новый тип является неизменяемым,
- тип имеет два открытых свойства только для чтения X и Y.
Преимущества
1. Код короче и меньше подвержен ошибкам.
2. Область действия анонимного типа ограничена методом, в котором он определён. Таким образом тип не загрязняет пространство имён, а ясно показывает другим разработчикам, что он используется для промежуточных вычислений только в пределах этого единственного метода.
3. Для обычных классов вы не можете задавать значения свойств только для чтения через инициализатор объекта, так как это позволяют делать анонимные типы.
4. Компилятор создаёт оптимизированный код. Всякий раз, когда вы создаёте такой же анонимный тип, компилятор не генерирует новый тип, а использует уже существующий*.
*Примечание: 1) очевидно, что это происходит, только если несколько копий анонимного типа объявлены в одной сборке, 2) имена, типы и порядок свойств анонимных типов должны совпадать.
5. Анонимные типы могут использоваться как составные ключи. Предположим, что вам нужно сгруппировать клиентов по продавцу и почтовому индексу. Вы можете выполнить запрос:
Очевидным недостатком использования анонимных типов является то, что вы не знаете названия типа, а значит не можете использовать его в качестве параметра метода или возвращаемого значения. Тем не менее, есть способы работы с отдельными объектами или последовательностями анонимных типов, используя обобщённые методы и лямбда выражения. Например, имея обобщённый метод преобразования:
Совместимость типов C# обычно основана на имени типа и называется номинативной типизацией. Кортежи используют структурную типизацию, а не номинативную, чтобы определить, относятся ли разные объекты к одному и тому же типу. Кортежи полагаются на свою «форму», а не на имя для определения конкретного типа. Таким образом любой кортеж, который содержит 2 целых числа, будет того же типа, что кортеж point выше. Заметки по использованию кортежей.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 7.
  7. Ограничивайте Область Действия Типа, Используя Анонимные Типы
Для создания пользовательских типов, представляющих объекты и структуры данных программы, вы можете выбрать классы, структуры, кортежи или анонимные типы. Классы и структуры настолько богаты в смысле выражения ваших замыслов, что заставляют многих разработчиков выбирать их, не рассматривая другие возможности. Вы можете написать более читаемый код, используя более простые конструкции: анонимные типы или кортежи.
Анонимные типы — это генерируемые компилятором неизменяемые ссылочные типы, которые можно объявить так:
var point = new {X = 5, Y = 67};
Вы указали компилятору, что вам нужен- новый закрытый (sealed) класс,
- этот новый тип является неизменяемым,
- тип имеет два открытых свойства только для чтения X и Y.
Преимущества
1. Код короче и меньше подвержен ошибкам.
2. Область действия анонимного типа ограничена методом, в котором он определён. Таким образом тип не загрязняет пространство имён, а ясно показывает другим разработчикам, что он используется для промежуточных вычислений только в пределах этого единственного метода.
3. Для обычных классов вы не можете задавать значения свойств только для чтения через инициализатор объекта, так как это позволяют делать анонимные типы.
4. Компилятор создаёт оптимизированный код. Всякий раз, когда вы создаёте такой же анонимный тип, компилятор не генерирует новый тип, а использует уже существующий*.
*Примечание: 1) очевидно, что это происходит, только если несколько копий анонимного типа объявлены в одной сборке, 2) имена, типы и порядок свойств анонимных типов должны совпадать.
5. Анонимные типы могут использоваться как составные ключи. Предположим, что вам нужно сгруппировать клиентов по продавцу и почтовому индексу. Вы можете выполнить запрос:
var query = from c in customersОн создаст словарь, в котором ключами будут пары
group c by new { c.SalesRep, c.ZipCode };
SalesRep и ZipCode, а значениями - списки клиентов.Очевидным недостатком использования анонимных типов является то, что вы не знаете названия типа, а значит не можете использовать его в качестве параметра метода или возвращаемого значения. Тем не менее, есть способы работы с отдельными объектами или последовательностями анонимных типов, используя обобщённые методы и лямбда выражения. Например, имея обобщённый метод преобразования:
static T Transform<T> (T e, Func<T, T> func) {
  return func(e);
}
можно удвоить значения X и Y для точки, передав анонимный тип и функцию преобразования в метод Transform:var p1 = new { X = 5, Y = 67 };
var p2 = Transform(p1, (p) => new { X=p.X*2, Y=p.Y*2 });
Кортежи являются изменяемыми значимыми типами с открытыми полями.var point = (X: 5, Y: 67);Создание экземпляра кортежа не генерирует новый тип, как создание нового анонимного типа. Вместо этого создаётся одна из структур
ValueTuple (их несколько в зависимости от количества элементов кортежа). ValueTuple содержит методы проверки на равенство, сравнение и метод ToString(), который печатает значение каждого поля кортежа.Совместимость типов C# обычно основана на имени типа и называется номинативной типизацией. Кортежи используют структурную типизацию, а не номинативную, чтобы определить, относятся ли разные объекты к одному и тому же типу. Кортежи полагаются на свою «форму», а не на имя для определения конкретного типа. Таким образом любой кортеж, который содержит 2 целых числа, будет того же типа, что кортеж point выше. Заметки по использованию кортежей.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 7.
День четыреста сорок четвёртый. #MoreEffectiveCSharp
8. Различные Концепции Равенства. Начало
Когда вы создаёте собственные типы (классы или структуры), вы определяете, что означает равенство для этого типа. C# предоставляет четыре метода, которые определяют, «равны» ли два разных объекта:
1.
1.
2.
Ни первый, ни второй метод не следует переопределять, поскольку они и так делают то, что должны.
Прежде, чем обсудить переопределение других двух методов, кратко рассмотрим математические свойства равенства. Вы должны убедиться, что ваше определение и реализация соответствуют ожиданиям других программистов. Модульные тесты для типов, которые переопределяют
- Рефлексивность (любой объект равен самому себе): независимо от типа, a = a всегда верно.
- Симметричность (порядок не имеет значения): если a = b, то b = a, если a <> b, то b <> a).
- Транзитивность: если a = b и b = c, то a = c.
Продолжение следует…
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
  8. Различные Концепции Равенства. Начало
Когда вы создаёте собственные типы (классы или структуры), вы определяете, что означает равенство для этого типа. C# предоставляет четыре метода, которые определяют, «равны» ли два разных объекта:
1.
static bool ReferenceEquals (object left, object right);2.
static bool Equals (object left, object right);3.
virtual bool Equals(object right);4.
static bool operator ==(MyClass left, MyClass right);Первые два переопределять не следует. Чаще всего переопределяют экземплярный метод
Equals() для обеспечения семантики равенства в типе. Также иногда переопределяется оператор ==, обычно для структур. Между этими четырьмя методами есть взаимосвязь, поэтому при изменении одного вы можете повлиять на поведение других. Кроме того, типы, переопределяющие Equals(), должны реализовывать IEquatable<T>. Типы, которые реализуют семантику сравнения входящих в них элементов (массивы, кортежи), должны реализовывать интерфейс IStructuralEquatable.1.
Object.ReferenceEquals() возвращает true, если две ссылки ссылаются на один и тот же объект. Независимо от того, являются ли сравниваемые типы ссылочными типами или типами значений, этот метод всегда проверяет идентичность объекта, а не его содержимое. ReferenceEquals() всегда возвращает false для значимых типов из-за того, что происходит упаковка значений.2.
Object.Equals() проверяет, равны ли две ссылки, когда вы не знаете тип времени выполнения двух аргументов. Как он это делает? Он делегирует проверку равенства одному из переданных ему параметров. Метод реализован примерно так:public static bool Equals(object left, object right) {
  // проверка ссылочного равенства
  if (Object.ReferenceEquals(left, right) )
    return true;
  // вариант с двумя null учтён выше
  if (Object.ReferenceEquals(left, null) ||
      Object.ReferenceEquals(right, null))
    return false;
  
  return left.Equals(right);
}
Как видите, метод делегирует проверку равенства экземплярному методу Equals() левого аргумента, таким образом используя правила проверки на равенство этого типа.Ни первый, ни второй метод не следует переопределять, поскольку они и так делают то, что должны.
Прежде, чем обсудить переопределение других двух методов, кратко рассмотрим математические свойства равенства. Вы должны убедиться, что ваше определение и реализация соответствуют ожиданиям других программистов. Модульные тесты для типов, которые переопределяют
Equals(), должны гарантировать, что реализация соблюдает эти правила:- Рефлексивность (любой объект равен самому себе): независимо от типа, a = a всегда верно.
- Симметричность (порядок не имеет значения): если a = b, то b = a, если a <> b, то b <> a).
- Транзитивность: если a = b и b = c, то a = c.
Продолжение следует…
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
День четыреста сорок пятый. #MoreEffectiveCSharp
8. Различные Концепции Равенства. Продолжение
Первая часть
3. Экземплярный метод
Источники:
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
- https://codewithshadman.com/csharp-data-types-and-object-tips/
  8. Различные Концепции Равенства. Продолжение
Первая часть
3. Экземплярный метод
Equals() переопределяют, когда поведение по умолчанию не соответствует семантике типа. Метод Object.Equals() по умолчанию ведёт себя точно так же, как Object.ReferenceEquals(), то есть проверяет ссылочное равенство. Но, например, System.ValueType (базовый класс для всех типов значений) переопределяет Object.Equals(): две переменные значимого типа равны, если они одного типа и имеют одинаковое содержимое. К сожалению, базовая реализация метода ValueType.Equals() не всегда эффективна. Если структура содержит ссылочный тип, для сравнения используется рефлексия:struct StructNoRef {
  public int X { get; set; }
  public int Y { get; set; }
}
struct StructWithRef {
  public int X { get; set; }
  public int Y { get; set; }
  public string Description { get; set; }
}
…
var stopwatch = new Stopwatch();
var data1 = new StructNoRef();
var data2 = new StructNoRef();
stopwatch.Start();
for (int i = 0; i < 1000000; i++)
  data1.Equals(data2);
stopwatch.Stop();
WriteLine("StructNoRef: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
var data3 = new StructWithRef();
var data4 = new StructWithRef();
stopwatch.Start();
for (int i = 0; i < 1000000; i++)
  data3.Equals(data4);
stopwatch.Stop();
WriteLine("StructWithRef: " + stopwatch.ElapsedMilliseconds);
Вывод:StructNoRef: 66Проверка на равенство довольно часто вызывается в программах, поэтому её производительность не стоит игнорировать. Почти всегда вы можете написать намного более быстрое переопределение
StructWithRef: 1077
Equals() для любой структуры. Переопределим метод для StructWithRef:struct StructWithRef {
  /// … 
  public override bool Equals(object obj) {
    if (!(obj is StructWithRef))
      return false;
    var other = (StructWithRef)obj;
    return X == other.X && 
      Y == other.Y && 
      Description == other.Description;
  }
}
Вывод:StructNoRef: 61Окончание следует…
StructWithRef: 81
Источники:
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
- https://codewithshadman.com/csharp-data-types-and-object-tips/
День четыреста сорок шестой. #MoreEffectiveCSharp
8. Различные Концепции Равенства. Окончание
Первая часть, Вторая часть
Для ссылочных типов переопределённый метод
Примечание: переопределение метода
4. Оператор
Наконец,
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
  8. Различные Концепции Равенства. Окончание
Первая часть, Вторая часть
Для ссылочных типов переопределённый метод
Equals должен следовать предопределенному поведению, чтобы избежать странных сюрпризов для пользователей класса. Кроме того, нужно реализовать интерфейс IEquatable<T>. Вот стандартный шаблон:public class Foo : IEquatable<Foo> {
  public override bool Equals(object right) {
    if (object.ReferenceEquals(right, null))
      return false;
    if (object.ReferenceEquals(this, right))
      return true;
    if (this.GetType() != right.GetType())
      return false;
    
    return this.Equals(right as Foo);
  }
  // IEquatable<Foo>
  public bool Equals(Foo other) { … }
}
Метод не должен генерировать исключения, т.к. это не имеет особого смысла. Две ссылки либо равны, либо не равны, что может пойти не так? Просто верните false. Сначала проверяется, не является ли аргумент null (CLR не даст нам вызвать a.Equals(b), если a == null, будет выброшено NullReferenceException). Далее проверяется равенство ссылок. Обратите внимание, что для проверки принадлежности объектов к одному типу, вызывается GetType. Дело в том, что недостаточно проверить, можно ли привести right к типу Foo (например, if(right is Foo) {…}, как в примере из предыдущего поста со структурами). right может быть производным типом от Foo, значит его можно привести к базовому типу и проверка вернёт true. Однако базовый класс нельзя привести к производному. Таким образом нарушается правило симметричности равенства: a.Equals(b) и b.Equals(a) будут давать разные результаты. Далее метод делегирует проверку на равенство безопасному к типам методу Equals(Foo) - реализации интерфейса IEquatable<Foo>.Примечание: переопределение метода
Equals() также предполагает переопределение метода GetHashCode(). Об этом в будущих постах.4. Оператор
== переопределяется для значимых типов по тем же причинам, что и метод Equals() и обычно просто вызывает этот метод. Что касается ссылочных типов, в этом случае оператор == переопределяется редко, т.к. предполагается, что для всех ссылочных типов он реализует семантику проверки на ссылочное равенство.Наконец,
IStructuralEquatable реализуют System.Array и классы Tuple<>. Он позволяет им сравнивать объекты друг с другом, не заботясь о деталях семантики сравнения содержащихся в них элементов, потому что реализация метода Equals(Object, IEqualityComparer) принимает компаратор, который отвечает за это.Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 9.
День четыреста пятьдесят первый. #MoreEffectiveCSharp
9. Тонкости GetHashCode(). Начало
Контейнеры используют хэш-коды для оптимизации поиска по коллекции, так что он занимает время близкое к постоянному, независимо от размера. Если вы определяете тип, который никогда не будет использоваться в качестве ключа в контейнере, ничего делать не нужно: ссылочные типы будут иметь правильный хеш-код, значимые типы должны быть неизменяемыми, и в этом случае реализация по умолчанию всегда будет работать. В большинстве типов, которые вы создаёте, лучшим подходом будет игнорирование существования
В .NET каждый объект имеет хеш-код, определённый в
1. Если два объекта равны (как определено экземплярным методом
Версия оператора
2. Для любого объекта
3. Хеш-функция должна генерировать равномерное распределение среди всех целых чисел для всех типичных входных наборов. Это более-менее соблюдается для
Написание правильной и эффективной хэш-функции требует глубоких знаний о типе, чтобы обеспечить соблюдение правила 3. Стандартные версии не эффективны, потому что должны обеспечивать лучшее поведение по умолчанию, практически не зная вашего конкретного типа.
Для типов-значений стандартная версия
Окончание следует…
Источники:
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 10.
- https://habr.com/ru/company/microsoft/blog/418515/
  9. Тонкости GetHashCode(). Начало
GetHashCode() – одна из тех функций, которую, по возможности, не стоит реализовывать самостоятельно. Она используется только в одном месте: для определения значения хеш-функции для ключей в коллекциях на основе хеша (обычно это HashSet<T> или Dictionary<K,V>). Есть ряд проблем с реализацией GetHashCode() в базовом классе. Для ссылочных типов она работает, но не очень эффективно. Для типов значений есть специальные оптимизации, если он не имеет ссылочных полей, в противном случае функция работает медленно и не всегда верно. Но дело не только в этом. Скорее всего, вы не сможете самостоятельно реализовать GetHashCode() и эффективно, и правильно.Контейнеры используют хэш-коды для оптимизации поиска по коллекции, так что он занимает время близкое к постоянному, независимо от размера. Если вы определяете тип, который никогда не будет использоваться в качестве ключа в контейнере, ничего делать не нужно: ссылочные типы будут иметь правильный хеш-код, значимые типы должны быть неизменяемыми, и в этом случае реализация по умолчанию всегда будет работать. В большинстве типов, которые вы создаёте, лучшим подходом будет игнорирование существования
GetHashCode().В .NET каждый объект имеет хеш-код, определённый в
System.Object.GetHashCode(). Любая перегрузка GetHashCode() должна следовать трём правилам:1. Если два объекта равны (как определено экземплярным методом
Equals()), они должны генерировать один и тот же хеш-код.Версия оператора
== в System.Object проверяет идентичность объектов. GetHashCode() возвращает внутреннее поле - идентификатор объекта, и это правило работает. Однако, если вы переопределили метод Equals(), нужно переопределить и GetHashCode(), чтобы обеспечить соблюдение правила.2. Для любого объекта
A хеш-код должен быть инвариантен. То есть, независимо от того, какие методы вызываются на объекте, A.GetHashCode() всегда должен возвращать одно и то же значение. К примеру, изменение значения поля не должно приводить к изменению хэш-кода.3. Хеш-функция должна генерировать равномерное распределение среди всех целых чисел для всех типичных входных наборов. Это более-менее соблюдается для
System.Object.Написание правильной и эффективной хэш-функции требует глубоких знаний о типе, чтобы обеспечить соблюдение правила 3. Стандартные версии не эффективны, потому что должны обеспечивать лучшее поведение по умолчанию, практически не зная вашего конкретного типа.
Для типов-значений стандартная версия
GetHashCode возвращает хэш-код только первого ненулевого поля, смешивая его с идентификатором типа. Проблема в том, что в этом случае начинает играть роль порядок полей типа. Если значение первого поля экземпляров одинаково, то хэш-функция будет выдавать одинаковый результат. Таким образом, если большинство экземпляров типа имеют одинаковое значение первого поля, то производительность поиска по хэш-набору или хэш-таблице из таких элементов резко упадет (см. тест в статье на Хабре).Окончание следует…
Источники:
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 10.
- https://habr.com/ru/company/microsoft/blog/418515/
День четыреста пятьдесят второй. #MoreEffectiveCSharp
9. Тонкости GetHashCode(). Окончание
Для переопределения
1. Если два объекта равны по методу Equals(), они должны возвращать одинаковый хэш. Следовательно, любые данные, используемые для генерации хеш-кода, также должны участвовать в проверке на равенство.
2. Возвращаемое значение GetHashCode() не должно изменяться. Допустим, у нас есть класс, в котором мы переопределили
3. GetHashCode() должен генерировать случайное распределение среди всех целых чисел для всех входных данных. Здесь не существует волшебной формулы. Если тип содержит некоторые изменяемые поля, исключите их из вычислений.
Microsoft предоставляет хороший универсальный генератор хэш-кодов. Просто скопируйте значения свойств/полей (соблюдая правила выше) в анонимный тип и хешируйте его:
Для C# 7+ можно использовать кортеж. Это сэкономит несколько нажатий клавиш и, что более важно, выполняется исключительно на стеке (без мусора):
- Bill Wagner “More Effective C#”. – 2nd ed. Глава 10.
- https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode
  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Хэш-код объекта изменился. Объект по-прежнему находится в коллекции, но теперь его невозможно найти. Единственный способ удовлетворить правилу 2 - определить хеш-функцию, которая будет возвращать значение на основе некоторого инвариантного свойства или свойств объекта.
False, всего: 1
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
Операторы приведения вводят своего рода взаимозаменяемость между классами. То есть один класс может быть заменён другим. Это может быть преимуществом: объект производного класса может быть заменён объектом его базового класса, как в классическом примере полиморфизма.
Когда вы определяете оператор преобразования для вашего типа, вы сообщаете компилятору, что ваш тип может быть заменён на целевой тип. Эти замены часто приводят к тонким ошибкам, потому что ваш тип, вероятно, не является идеальной заменой целевого типа. Методы, изменяющие состояние целевого типа, не будут отражаться на вашем типе. А если ваш оператор приведения возвращает временный объект, изменения и вовсе будут утеряны.
Рассмотрим объекты круг (
Вместо приведения используйте конструктор, принимающий исходный тип в качестве параметра. Определим в классе
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 11.
  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, могут использовать именованные параметры, хотите вы того или нет. Следующий метод:
Изменение имён параметров проявляется интересным образом. Имена параметров хранятся в MSIL только в месте вызова, но не в вызываемом коде. Вы можете изменить имена параметров и выпустить новую версию сборки, не нарушив работу её пользователей. Разработчики, использующие сборку, увидят проблему только когда решат перекомпилировать свой код, используя обновлённую версию. Существующие клиентские сборки продолжат работать правильно. Предположим, что вы изменили
Добавление параметров вызовет ошибку времени выполнения в клиентском коде. Необязательные параметры реализованы аналогично именованным параметрам. Вызывающий код будет содержать аннотации в MSIL, отражающие существование значений по умолчанию и их значения. Там эти значения будут использоваться для всех необязательных параметров, значения для которых не указаны явно. Следовательно добавление в метод параметров (даже со значениями по умолчанию) вызовет в клиентском коде ошибку времени выполнения. Однако добавление параметров со значениями по умолчанию не вызовет ошибку при перекомпиляции клиентского кода.
Итого:
- Для вашего первоначального релиза используйте необязательные и именованные параметры, чтобы предусмотреть любые комбинации параметров, которые ваши пользователи захотят использовать.
- При последующих изменениях создавайте перегрузки методов для добавления дополнительных параметров. Таким образом, существующие клиентские приложения продолжат работать.
- При последующих изменениях избегайте изменения имён параметров в открытых или защищённых методах. Теперь они являются частью вашего публичного интерфейса.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 12.
  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. Ограничивайте Видимость Ваших Типов
Не каждый ваш тип должен быть публичным. Типам нужно давать наименьшую необходимую видимость для достижения вашей цели. Часто меньшую, чем кажется. Внутренние или приватные классы могут реализовывать публичные интерфейсы. Все клиенты могут получать доступ к функциональности, определённой в открытых интерфейсах и реализованной в закрытом типе. Обычно большинство программистов постоянно создают открытые классы, не задумываясь об альтернативах. Это очень просто, но лучше тщательно продумать, где будет использоваться новый тип. Он предназначен для всех клиентов или в основном используется внутри этой сборки? Многим классам достаточно быть внутренними, защищёнными, закрытыми или вложенными в другой класс. Чем меньше видимость, тем меньше мест, откуда можно получить доступ к коду, и меньше мест, которые потребуется изменить при обновлении системы.
Попробуйте создать публичный интерфейс и классы с меньшей видимостью. Внутренние (
Рассмотрим класс, который проверяет формат номера телефона (
Вместо того, чтобы добавлять функциональность в этот класс, и использовать его напрямую, лучше уменьшить связанность между классами. Создадим интерфейс для проверки любого телефонного номера:
Преимущества:
- Вне сборки виден только интерфейс, специфические классы видны только внутри сборки.
- Можно добавлять и изменять классы проверки, не ломая другие сборки в системе.
- Чем меньше открытых типов, тем меньше открытых методов, для которых нужно писать тесты.
- Под открытый API интерфейса можно создавать mock-объекты для тестирования.
Те классы и интерфейсы, которые вы публикуете для внешнего мира, являются вашим контрактом, и вы должны поддерживать его. Чем обширнее этот публичный контракт, тем более ограниченными будут возможности развития. Чем меньше открытых типов, тем больше будет вариантов для изменений и расширения функционала.
Источник: Bill Wagner “More Effective C#”. – 2nd ed. Глава 13.
  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.
