Комментарии 19
Так, а что же не так то с enum и switch? Хорошо бы пример привести, где не так, и потом, где все стало так. А то вы простой и понятный пример со switch превратили в кучу непонятного кода с интерфейсами.
З.Ы. Я, понимаю зачем все это (не уверен, правда в самом решении, так как C# не знаю), но думаю, многим будет непонятно.
Инспекции решарпера бесплатны, если запускать из комманд лайна на билд сервере.
У решарпера есть правило о том, что switch должен быть исчерпывающим
Но ведь если есть default throw
, как в примере на картине, это в глазах линтера разве не исчерпывающий switch? С другой стороны, раз там enum, то лучше без default обходиться в таких местах.
При добавлении нового значения в enum необходимо:
Добавить метод в интерфейс.
Добавить его использование в метод расширение.
Просто хотел добавить, что такое решение подойдет, если все или почти все значения enum нужно обработать не дефолтным способом. А если нужно обработать только малую часть enum, то решение со switch будет лучше, т.к. будет меньше boilerplate кода.
Что же тогда надо будет городить если у вас flag enum...
Вам, на самом деле не нужны эти костыли, ReSharper или что-то еще.
Все гораздо проще
enum MyEnum { A, B, C }
var e = (MyEnum)new Random().Next(0, 2);
Console.WriteLine(e switch
{
MyEnum.A => "A",
MyEnum.B => "B"
});
Здесь появляется warning
CS8509 The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'ConsoleApp1.Program.MyEnum.C' is not covered
В .editorconfig'е переводите этот warning в ошибку.
Профит!
Часто ли у вас было такое, что вы добавляли новое значение в enum и потом тратили часы на то, чтобы найти все места его использования, а затем добавить новый case, чтобы не получить ArgumentOutOfRangeException во время исполнения?
Никогда не приходилось тратить на это часы. Давишь Shift+F12 и пробегаешься по списку, правишь, где что надо.
А о чем статья? Этому паттерну уже триста лет.
Я не уверен, что эту реализацию можно называть посетителем. Мотивация посетителя в том, чтобы выбирать алгоритм поведения в зависимости от типа объекта которого он посещает. Достигается это за счет того, что посещаемый объект сам выбирает метод, который может его обработать. В данном же случае вы ориентируетесь по значению.
Возможно вам стоит приложить усилия не к уменьшению последствия проблемы, а решить саму проблему? Я предлагаю вам попытаться избавиться от свичей. К сожалению я не могу вам предложить серебряную пулю. Решение придётся искать своё в каждом конкретном случае. Но возможно вам следует присмотреться к паттерну состояние.
Если нужно при добавлении нового элемента добавлять его обработку в разные методы — возможно вы неправильно декомпозировали задачу и нужно превратить элемент в интерфейс, который будет иметь реализации для разных типов
Тогда останется только один свич — при создании типа объектов
До чего же бедный язык C#, раз приходится в нём громыхать связанными идентификаторами (Invoice — VisitInvoice) и создавать ошибкоопасные порядковые аргументы функции Match?
enum MyEnum {
Value1,
Value2,
Value25, // добавили
Value3
}
static T Match<T>(this MyEnum self,
Func<T> on1,
Func<T> on25, // добавили не туда
Func<T> on2,
Func<T> on3) { ..... }
.....
var answer = my_enum.Match(
() => "one",
() => "two",
() => "three",
() => "two and half" // добавили не туда
);
Вы спросите, как такие детские ошибки можно вообще допустить?
Да как нефиг делать. Рефакторинг, мать его.
1) Добавили Value25 в конец энума, в конец функции, в конец всех вызовов функции
2) Потом переместили в энуме и функции (естественно, проворонили использование — сигнатура-то не поменялась)
3) А потом криво автоматически смёржили в системе контроля версий. И вот уже не только использования, но и сам энум с функцией разъехались.
Есть и ещё один в меру костыльный вариант, но который не требует наличия того же решарпера. По сути, вся задача сводится к тому, чтобы при добавлении нового элемента в enum, switch кидал ошибку при компиляции. Добиться этого можно, добавив дополнительное значение в enum и проверку времени компиляции в switch. Выглядит примерно так:
- Есть enum:
- enum Test { - A - }
- Есть switch с элементами enum:
- Test value = Test.A; - switch (value) { - case Test.A: - ... - break; - default: - throw new ArgumentOutOfRangeException(); - }
3.1. Добавляем в enum элемент
Last
:
- enum Test { - A, - Last - }
В нашем случае элемент Last — число 1, т.к. по умолчанию члены enum — константные целые числа от 0 по возрастанию.
3.2. Добавим специальный код в switch:
- Test value = Test.A; - switch (value) { - case Test.A: - ... - break; - case Test.Last: - const int _ = 1 / (1 / (int) Test.Last) - throw new ArgumentException(); - default: - throw new ArgumentOutOfRangeException(); - }
Выражение
1 / (1 / (int) Test.Last)
будет вычислено во время компиляции. В случае, если Last будет больше второго числа (1 в скобках), то второе частное будет равно 0, что приведёт к делению на 0 во время компиляции и выкинет ошибку. Как только решили добавить новое значение в enum, его нужно добавить перед Last, чтобы значение Last увеличилось.
Конечно же здесь есть недостатки:
- Добавляется ещё одна проверка в switch — overhead в runtime
- Можно применять только в случае, когда элементы в enum — возрастающие числа, а
Last
— последний элемент - Нужно помнить, зачем в enum элемент
Last
Но тут уже нужно самим решить, стоит оно того или нет в конкретном случае.
P.S. если кто-то знает, как отключить в markdown хабра удаление пробелов в начале строки, welcome
Enum и switch, и что с ними не так