Как стать автором
Обновить

Комментарии 19

Да не, это костыль. Нужно, чтобы допили нормально поддержку switch expression'а в новых версиях C#, а не делать работу за разработчиков языка. Да понятно, что там есть дурное поведение, когда enum «ничему не равен» потому что «так надо было», но это точно бага, а не фича.
Полностью поддерживаю. Но к великому сожалению, далеко не все могут использовать новую версию NET. Бывают легаси проекты, где приходится жить с тем, что есть.

Так, а что же не так то с enum и switch? Хорошо бы пример привести, где не так, и потом, где все стало так. А то вы простой и понятный пример со switch превратили в кучу непонятного кода с интерфейсами.
З.Ы. Я, понимаю зачем все это (не уверен, правда в самом решении, так как C# не знаю), но думаю, многим будет непонятно.

У решарпера есть правило о том, что switch должен быть исчерпывающим, его можно поставить в «error» вместо «hint» и не пропускать билд с ошибками.

Инспекции решарпера бесплатны, если запускать из комманд лайна на билд сервере.
У решарпера есть правило о том, что switch должен быть исчерпывающим

Но ведь если есть default throw, как в примере на картине, это в глазах линтера разве не исчерпывающий switch? С другой стороны, раз там enum, то лучше без default обходиться в таких местах.

нет, наличие «default» всё равно не считается «исчерпывающим switch», для enum только перебор всех элементов. По этому оно спасает от ошибок расширения enum.

На наличие «default» во всех switch есть другое правило.
При добавлении нового значения в enum необходимо:

Добавить метод в интерфейс.
Добавить его использование в метод расширение.

Просто хотел добавить, что такое решение подойдет, если все или почти все значения enum нужно обработать не дефолтным способом. А если нужно обработать только малую часть enum, то решение со switch будет лучше, т.к. будет меньше boilerplate кода.

Для этого можно написать абстрактный класс, который будет вызывать общий метод для нужных case

Что же тогда надо будет городить если у вас 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 и пробегаешься по списку, правишь, где что надо.
1)Это не решит проблему банальной невнимательности.
2)А также не решит проблему, если существует несколько solution и человек не знает о них/забыл и не добавил логику. Узнать об этом можно будет только во время исполнения.

С таким подходом, проект просто не соберется

А о чем статья? Этому паттерну уже триста лет.

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


Возможно вам стоит приложить усилия не к уменьшению последствия проблемы, а решить саму проблему? Я предлагаю вам попытаться избавиться от свичей. К сожалению я не могу вам предложить серебряную пулю. Решение придётся искать своё в каждом конкретном случае. Но возможно вам следует присмотреться к паттерну состояние.

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

До чего же бедный язык 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. Выглядит примерно так:


  1. Есть enum:
    - enum Test {
    -   A
    - }
  2. Есть 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 увеличилось.



    Конечно же здесь есть недостатки:


    1. Добавляется ещё одна проверка в switch — overhead в runtime
    2. Можно применять только в случае, когда элементы в enum — возрастающие числа, а Last — последний элемент
    3. Нужно помнить, зачем в enum элемент Last

    Но тут уже нужно самим решить, стоит оно того или нет в конкретном случае.


    P.S. если кто-то знает, как отключить в markdown хабра удаление пробелов в начале строки, welcome

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории