Вводная


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

Немного нюансов


Речь ниже пойдет о сравнении оператора switch и его конкретной реализаций на языке Java и операторами if в виде оппонентов этой конструкции. О том, что у оператора switch имеется туз в рукаве — производительность (за редким исключением), пожалуй, знает каждый и этот момент рассматриваться не будет — так как крыть его нечем. Но все же, что не так?

1. Многословность и громоздкость


Среди причин, отталкивающих меня от использования оператора switch — это многословность и громоздкость самой конструкции, только взгляните — switch, case, break и default, причем опускаю за скобки, что между ними, наверняка, еще встретится return и throw. Не подумайте, что призываю не использовать служебные слова языка java или избегать их большого количества в коде, нет, лишь считаю, что простые вещи не должны быть многословными. Вот небольшой пример того, о чем говорю:

    public List<String> getTypes1(Channel channel) {
        switch (channel) {
            case INTERNET:
                return Arrays.asList("SMS", "MAC");
            case HOME_PHONE:
            case DEVICE_PHONE:
                return Arrays.asList("CHECK_LIST");
            default:
                throw new IllegalArgumentException("Неподдерживаемый канал связи " + channel);
        }
    }
 

вариант с if

    public List<String> getTypes2(Channel channel) {
        if (INTERNET == channel) {
            return Arrays.asList("SMS", "MAC");
        }
        if (HOME_PHONE == channel || DEVICE_PHONE == channel) {
            return Arrays.asList("CHECK_LIST");
        }
        throw new IllegalArgumentException("Неподдерживаемый канал связи " + channel);
    }

Итого для меня: switch/case/default VS if.

Если спросить, кому какой нравится больше код, то большинство отдаст предпочтение 1ому варианту, в то время как по мне, он — многословный. Речь о рефакторинге кода здесь не идет, все мы знаем, что можно было бы использовать константу типа EnumMap или IdentityHashMap для поиска списка по ключу channel или вообще убрать нужные данные в сам Channel, хотя это решение дискуссионное. Но вернемся.

2. Отступы


Возможно в академических примерах использование оператора swicth — это единственная часть кода, но тот код, с которым приходится сталкиваться, поддерживать — это более сложный, обросший проверками, хаками, где-то просто предметной сложностью код. И лично меня, каждый отступ в таком месте напрягает. Обратимся к 'живому' примеру (убрал лишнее, но суть осталась).

     public static List<String> getReference1(Request request, DataSource ds) {
         switch (request.getType()) {
             case "enum_ref":
                 return EnumRefLoader.getReference(ds);
             case "client_ref":
             case "any_client_ref":
                 switch (request.getName()) {
                     case "client_types":
                         return ClientRefLoader.getClientTypes(ds);
                     case "mailboxes":
                         return MailboxesRefLoader.getMailboxes(ds);
                     default:
                         return ClientRefLoader.getClientReference(request.getName(), ds);
                 }
             case "simple_ref":
                 return ReferenceLoader.getReference(request, ds);
         }
         throw new IllegalStateException("Неподдерживаемый тип справочника: " + request.getType());
     }

вариант с if

    public static List<String> getReference2(Request request, DataSource ds) {
        if ("enum_ref".equals(request.getType())) {
            return EnumRefLoader.getReference(ds);
        }
        if ("simple_ref".equals(request.getType())) {
            return ReferenceLoader.getReference(request, ds);
        }
        boolean selectByName = "client_ref".equals(request.getType()) || "any_client_ref".equals(request.getType());
        if (!selectByName) {
            throw new IllegalStateException("Неподдерживаемый тип справочника: " + request.getType());
        }
        if ("client_types".equals(request.getName())) {
            return ClientRefLoader.getClientTypes(ds);
        }
        if ("mailboxes".equals(request.getName())) {
            return MailboxesRefLoader.getMailboxes(ds);
        }
        return ClientRefLoader.getClientReference(request.getName(), ds);
    }

Итого для меня: 5 отступов VS 2.

Но опять, кому какой нравится вариант? Большинство отдаст предпочтение getReference1.
Отдельно стоит отметить, что количество отступов еще зависит от выбранного стиля форматирования кода.

3. Проверка на null


Если используется оператор switch со строками или енумами параметр выбора необходимо проверять на null. Вернемся к getTypes примерам.

    // switch: проверка на null нужна
    public List<String> getTypes1(Channel channel) {
        // Проверка на null
        if (channel == null) {
            throw new IllegalArgumentException("Канал связи должен быть задан");
        }
        switch (channel) {
            case INTERNET:
                return Arrays.asList("SMS", "MAC");
            case HOME_PHONE:
            case DEVICE_PHONE:
                return Arrays.asList("CHECK_LIST");
            default:
                throw new IllegalArgumentException("Неподдерживаемый канал связи " + channel);
        }
    }
    // if: можно обойтись без проверки на null
    public List<String> getTypes2(Channel channel) {
        if (INTERNET == channel) {
            return Arrays.asList("SMS", "MAC");
        }
        if (HOME_PHONE == channel || DEVICE_PHONE == channel) {
            return Arrays.asList("CHECK_LIST");
        }
        throw new IllegalArgumentException("Неподдерживаемый канал связи " + channel);
    }

Итого для меня: лишний код.

Даже если Вы абсолютно уверенны сейчас в том, что null 'не придет', это абсолютно не значит, что так будет всегда. Я проанализировал корпоративный багтрэк и нашел этому утверждению подтверждение. Справедливости ради стоит отметить, что структура кода выраженная через if не лишена этой проблемы, зачастую константы для сравнения используются справа, а не слева, например, name.equals(«John»), вместо «John».equals(name). Но в рамках данной статьи, по этому пункту, хотел сказать, что при прочих равных, подход со switch раздувается проверкой на null, при if же проверка не нужна. Добавлю еще, что статические анализаторы кода легко справляются с возможными null-багами.

4. Разношерстность


Очень часто, при длительном сопровождении кода, кодовая база раздувается и можно легко встретить код, похожий на следующий:

public static void doSomeWork(Channel channel, String cond) {
    Logger log = getLogger();
    //...
    switch (channel) {
        //...
        case INTERNET:
            // Со временем появляется такая проверка
            if ("fix-price".equals(cond)) {
                // ...
                log.info("Your tariff");
                return;
            }
            // Еще под выбор?
            // ...
            break;
        //...
    }
    //...
}

Итого для меня: разный стиль.

Раньше был 'чистенький' switch, а теперь switch + if. Происходит, как я называю, смешение стилей, часть кода 'выбора' выражена через switch, часть через if. Разумеется никто не запрещает использовать if и switch вместе, если это не касается операции выбора/под выбора, как в приведенном примере.

5. Чей break?


При использовании оператора switch, в case блоках может появиться цикл или на оборот, в цикле — switch, со своими прерываниями процесса обработки. Вопрос, чей break, господа?

public static List<String> whoseBreak(List<String> states) {
    List<String> results = new ArrayList<>();
    for (String state : states) {
        Result result = process(state);
        switch (result.getCode()) {
            case "OK":
                if (result.hasId()) {
                    results.add(result.getId());
                    // Надуманный случай, но break чей-то?
                    break;
                }
                if (result.getInnerMessage() != null) {
                    results.add(result.getInnerMessage());
                    // Вот это поворот
                    continue;
                }
                // ...
                break;
            case "NOTHING":
                results.add("SKIP");
                break;
            case "ERROR":
                results.add(result.getErroMessage());
                break;
            default:
                throw new IllegalArgumentException("Неверный код: " + result.getCode());
        }
    }
    return results;
}

Итого для меня: читаемость кода понижается.

Если честно, встречались и куда более запутанные примеры кода.

6. Неуместность


В java 7 появилась возможность использовать оператор switch со строками. Когда наша компания перешла на java 7 — это был настоящий Switch-Бум. Может по этому, а может и по другой причине, но во многих проектах встречаются похожие заготовки:

public String resolveType(String type) {
    switch (type) {
        case "Java 7 же?":
            return "Ага";
        default:
            throw new IllegalArgumentException("Люблю switch, жаль, что нет такого варианта с case " + type);
    }
}

Итого для меня: появляются неуместные конструкции.

7. Гламурный switch под аккомпанемент хардкода


Немного юмора не помешает.

public class Hit {

    public static enum Variant {
        ZERO, ONE, TWO, THREE
    }

    public static void switchBanter(Variant variant) {
        int shift = 0;
        ZERO: ONE: while (variant != null) {
            shift++;
            switch (variant) {
                default: {
                    THREE: {
                        System.out.println("default");
                        break THREE;
                    }
                    break;
                }
                case ONE: {
                     TWO: {
                     THREE: for (int index = shift; index <= 4; index++) {
                            System.out.println("one");
                            switch (index) {
                                case 1: continue ONE;
                                case 2: break TWO;
                                case 3: continue THREE;
                                case 4: break ZERO;
                            }
                        }
                    }
                    continue ONE;
                }
                case TWO: {
                     TWO: {
                        System.out.println("two");
                        if (variant == THREE) {
                            continue;
                        }
                        break TWO;
                    }
                    break ZERO;
                }
            }
            variant = null;
        }
    }

    public static void main(String[] args) {
        switchBanter(ONE);
    }
}

Без комментариев.

Заключение


Я не призываю Вас отказываться от оператора switch, местами он действительно хорош собой и лапша из if/if-else/else та еще каша. Но черезмерное его использование где ни попадя, может вызывать недовольство у других разработчиков. И я один из них.

Отдельно хотелось отметить, что c точки зрения понимания кода, у меня нет никаких проблем с switch/case — смысл написанного ясен, но вот с точки зрения восприятия эстетической красоты — есть.

И напоследок. Используйте то, что Вам приходится по душе, опуская за скобки навязанные мнения, главное, чтобы Ваш код был простым, рабочим, красивым и надежным. Всего хорошего.
Поделиться с друзьями
-->

Комментарии (67)


  1. Sirikid
    29.01.2017 00:44
    +8

    А как вам такой финт?


    fun getTypes1(channel: Channel): List<String> = when (channel) {
        INTERNET -> listOf("SMS", "MAC")
        HOME_PHONE, DEVICE_PHONE -> listOf("CHECK_LIST")
        else -> throw IllegalArgumentException("Неподдерживаемый канал связи " + channel)
    }


    1. reforms
      29.01.2017 12:34
      +1

      Финт прекрасен: краток и лаконичен.
      Косвенно Вы поднимаете более глубокую тему сравнения языков и их возможностей.


      1. Sirikid
        29.01.2017 16:37
        +1

        Скорее тему бездумного заимствования конструкций при проектировании языков, когда проектировалась Java аналоги when уже были.


    1. fogone
      29.01.2017 19:14
      +1

      Ещё интерполяция для формирования сообщения нужна


  1. AlexZaharow
    29.01.2017 00:49
    +4

    Подброшу дровишек. Есть такая штука, как «java break label»:

    public class Test {
    
      public static void main(String[] argv) {
        lab1: for (int i=0; i<10; i++) {
          for (int j=0; j<10; j++) {
            if (j>=5 && i>5)
              break lab1;
            System.out.println(i + " / " + j);
          }
        }
      }
    }
    

    Не часто пользуюсь, но в принципе местами бывает удобна. На if-ах это сделать уже значительно сложнее без дополнительных переменных.
    Тоже самое есть и в JavaScript: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Statements/label
    var i, j;
    
    loop1:
    for (i = 0; i < 3; i++) {      //Первый цикл, обозначенный меткой "loop1"
       loop2:
       for (j = 0; j < 3; j++) {   //Второй цикл, обозначенный меткой "loop2"
          if (i == 1 && j == 1) {
             continue loop1;
          }
          console.log("i = " + i + ", j = " + j);
       }
    }
    


    1. TheShock
      29.01.2017 06:28
      +7

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


    1. charypopper
      29.01.2017 12:26
      +13

      Хорошая попытка goto, но нет)


    1. reforms
      29.01.2017 12:42
      +3

      На if-ах это сделать уже значительно сложнее без дополнительных переменных.

      Согласен, но тоже верно и про switch-case?


  1. Cryvage
    29.01.2017 02:59
    +11

    ИМХО проблемы надуманные. Большой блок switch всегда можно разбить на отдельные функции, а в маленьком всё вышеописанное не проявляется. Громоздкость, вложенность, отступы, разношерстность, путаница с break — всё это решается декомпозицией, особенно в switch, который by design разбит на независимые участки кода. Он прямо так и напрашивается на декомпозицию.
    Отдельно можно сказать про проверку на null. Она нужна в любом случае, т.к. передача null в метод это, как правило, ошибка, и об этом надо сообщить. Что бы ни было внутри метода, switch, if, или просто «Hello World», а проверка входных данных должна быть.


    1. maxpsyhos
      29.01.2017 08:25
      +2

      Бывает так, что в таком случае надо делать функции с 10-ю входными и 5-ю выходными переменными, причём в каждом case разными, и с javadoc длиннее кода раз в 5. И это только ещё сильнее всё запутывает.


      1. DeadKnight
        29.01.2017 12:32
        +11

        Если ты не пишешь на языке низкого уровня, то появление функции «с 10-ю входными и 5-ю выходными переменными» говорит только об ужасном дизайне. Тут уже не о выборе меджу switch и if говорить надо.


    1. reforms
      29.01.2017 13:17
      +2

      ИМХО проблемы надуманные

      Я всячески старался избегать слова проблема, больше старался подчеркивать личная неприязнь, но справдливости ради скажу, что проблемы if VS switch находятся при review кода, как в одну так и в другую стороны.

      Отдельно можно сказать про проверку на null. Она нужна в любом случае, т.к. передача null в метод ...

      Бесспорно! Но жизнь такова, что многие проверки не делают и в результате получаем, в одном случае крайне неинформативную ошибку NPE, а в другом просто недостаточно информативную (если из примера «Неподдерживаемый канал связи null»)


  1. Shortki
    29.01.2017 03:49
    +8

    В случаях когда switch больше семантически соответствует алгоритму правильнее использовать такую конструкцию.

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

    Однако ничто не заставит писать код “как правильно”, когда хочется “как хочется".


  1. maaGames
    29.01.2017 06:40
    +12

    У if-else есть один большой недостаток: ошибка копипасты. В switch невозможно по ошибке написать два одинаковых case, в if-else это случается постоянно.
    А в целом, всему своё место.


    1. reforms
      29.01.2017 13:21
      +2

      Голосую +1. Лично на это нарывался.


    1. Borz
      29.01.2017 14:37
      +1

      в нормальных IDE подобные ошибки подсвечиваются


      1. maaGames
        29.01.2017 15:11
        +2

        Вы про if? Возможно, зависит от языка, но на С/С++ такие ошибки только PVS и прочие статические анализаторы находят, сама IDE бессильна.


      1. PsyHaSTe
        29.01.2017 18:41
        +2

        А как будете ловить ошибку, что вместо одной переменной сравнили с другой?

            public List<String> getTypes2(Channel channel) {
                Channel modifiedChannel = DoSomethingWith(channel);
                if (INTERNET == modifiedChannel) {
                    return Arrays.asList("SMS", "MAC");
                }
                if (HOME_PHONE == modifiedChannel || DEVICE_PHONE == channel) { // <- сравниваем не ту ссылку
                    return Arrays.asList("CHECK_LIST");
                }
                throw new IllegalArgumentException("Неподдерживаемый канал связи " + channel);
            }
        

        Ни один статический анализатор тут ошибку не найдет.


        1. Borz
          29.01.2017 18:47
          +1

          зато тесты найдут. либо через юнит либо через мутацию


          1. PsyHaSTe
            29.01.2017 19:32
            +2

            Ну то есть ответ «никак, покрою тестами».


            1. Borz
              29.01.2017 19:43

              да.


              P.S. вы подменили смысл моего первого комментария — одно дело, когда в последующем if есть условие ранее проверенное другим if (например следствие copy-paste) и совсем другое неявная логическая ошибка в условиях (может вам именно такая проверка и нужна была)


              1. PsyHaSTe
                29.01.2017 22:58

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

                Мы же все понимаем, что копипаста это плохо, вторая посылка заключается в том, что switch избавляет от копипасты: мы пишем только один раз имя переменной и возможные паттерны, а в if'е мы должны в каждой ветке скопипастить имя и не дай Бог не ошибиться. Единственное, что может расстраивать, это многословность swtich case, но сама конструкция в этом не виновата.


  1. Borz
    29.01.2017 10:01
    +1

    в силу предпочтений линейного кода против древовидного внутри одного метода, для себя давно решил, что switch только в тех случаях, когда внутри нет return — в остальных случаях строго if.
    Да и чаще всего, логика несколько сложнее чем просто проверка 1-in-1 константы, и тут снова if в выигрыше


  1. omickron
    29.01.2017 10:50
    +1

    Я обычно избегаю обоих конструкций, и инкапсулирую логику в отдельный метод в enum.
    И тогда выходит очень аккуратно.


    ConnectionType connectionType = ConnectionType.LIST;
    return connectionType.getRows();


    У каждого элемента enum своя реализация метода. И проверка на null не нужна, и код читабельнее.


    1. TargetSan
      29.01.2017 15:17
      +1

      А ещё гудбай статически просчитанный диспатч. Или надеяться на JIT.


      1. omickron
        29.01.2017 15:26
        +1

        Если в выражениях switch логика сложнее константы, ваш аргумент теряет смысл.


        1. TargetSan
          29.01.2017 15:30
          +1

          А Java умеет в метках диспатч-веток сложные неконстантные выражения?


          1. omickron
            29.01.2017 16:49
            +1

            Для case требуются только константы. Но я не об этом.
            Вы говорите о том, что в случае использования switch-case есть вероятность того, что компилятор сможет просчитать, куда передать управление дальше.
            Если я правильно вас понял, и не ошибаюсь, это возможно в случае, если в ответах case будут константы:

            case 4: retrurn "some";
            

            В случае более сложной логики вроде
            case 4: 
              if (someFlag) 
                service.run(something)
            

            говорить о статическом диспатче не приходится.

            Но если честно, я давно не встречал switch-case, который возвращает константу.
            Подскажите примером, для чего это может требоваться?


  1. mib
    29.01.2017 12:37
    +2

    На сколько я помню, в первобытных сях оператор «свитч» был предназначен для ассемблера, в котором есть схожий оператор, обладающий лучшим быстродействием чем цепочка сравнений (условий)


    1. TargetSan
      29.01.2017 15:16
      +5

      Скорее всего побуду Кэпом. Это называется Dispatch Table и напрямую в Асм вроде как не выражается. А выражается в джамп с вычисляемым адресом, перед которым стоит проверка что джамп не улетит в космос.


  1. bormotov
    29.01.2017 20:16
    -1

    Не возникало мысли, что дело не в самих конструкциях и том, как именно их используют, а в языке в целом?


    1. maximw
      29.01.2017 21:03

      В каком именно из языков, имеющих в арсенале и switch, и if?


      1. bormotov
        29.01.2017 22:31

        в любом.


  1. aamonster
    29.01.2017 20:49
    +2

    Странно, что никто не вспомнил старый добрый паскаль. Уже больше 15 лет, как перешёл на C-подобные языки, но каждый раз, как пишу switch (и иногда for) — с тоской думаю о том, что просрали язык, где это было сделано по уму...


    1. Sirikid
      29.01.2017 21:51

      Есть как минимум две актуальные реализации Pascal (FPC и Delphi), во многих современных (и не очень) языках есть for-in циклы и switch с break по дефолту.


      1. aamonster
        29.01.2017 22:23

        Да, с for сейчас за счёт всевозможных foreach полегче стало.
        Switch с break по дефолту — гуд, но ещё желательна адекватная запись (уж на что 20 лет назад апологеты C ругали паскаль за многословность — такой кучи слов case в нём нет) и поддержка диапазонов (типа 0..31).
        Радует запись в Haskell, но он всё же весьма специфичен и не мейнстрим ни разу.
        Актуальные реализации паскаля — это прекрасно, но будем смотреть правде в глаза: паскаль мёртв. Его вытеснили другие языки. 2% распространённости — это агония, вероятно, на поддержке старых проектов.


        А вот украсть из него (в линейку си-подобных) удачные решения — было бы неплохо.


        1. Sirikid
          29.01.2017 23:00

          Посмотрите на Go, Swift, Kotlin — все украдено до нас.


    1. PsyHaSTe
      29.01.2017 23:00

      У паскаля было несколько плюсов, но было очень много минусов — раз. Хейлсберг все лучшие идеи паскаля перенес и развил в C# — никто не забыт, ничто не забыто — два.


      1. aamonster
        29.01.2017 23:27

        Ну, справедливости ради — не все. Тот же ущербный C-шный синтаксис switch-case остался, добавилась лишь защита от совсем уж дурацких ошибок.
        С for понятно — foreach решает практически все задачи, кроме циклов в вычмате.


        1. PsyHaSTe
          30.01.2017 01:40

          В C# 1.0 switch сперли из С по понятным причинам, но в следующей версии будет полноценный pattern matching:

          Expr Simplify(Expr e)
          {
            switch (e) {
              case Mult(Const(0), *): return Const(0);
              case Mult(*, Const(0)): return Const(0);
              case Mult(Const(1), var x): return Simplify(x);
              case Mult(var x, Const(1)): return Simplify(x);
              case Mult(Const(var l), Const(var r)): return Const(l*r);
              case Add(Const(0), var x): return Simplify(x);
              case Add(var x, Const(0)): return Simplify(x);
              case Add(Const(var l), Const(var r)): return Const(l+r);
              case Neg(Const(var k)): return Const(-k);
              default: return e;
            }
          }
          

          и облегченный вариант (когда все ветки возвращают значение):
          var areas =
              from primitive in primitives
              let area = primitive switch (
                  case Line l: 0,
                  case Rectangle r: r.Width * r.Height,
                  case Circle c: Math.PI * c.Radius * c.Radius,
                  case *: throw new ApplicationException()
              )
              select new { Primitive = primitive, Area = area };
          

          Хотя копи-пастить case раздражает, но в целом ничего супер-страшного это не несет.


          1. aamonster
            30.01.2017 07:31

            Взять из ML (F#) pattern matching — конечно, тоже польза, но он хорош для функционального стиля.
            А я говорил про совсем простую вещь для императивщины — диапазоны значений в case.
            Ну и убрать слово case у меток, как ненужное.


            1. PsyHaSTe
              30.01.2017 16:42

              А чем он плох для императивного стиля?


              1. aamonster
                30.01.2017 20:12

                Ничем не плох, но не покрывает некоторые нужды.
                К примеру, привычная задача посимвольной обработки строки. Характерный код на паскале:


                case c of
                  #0..#31   : skip_char();
                  ' '       : process_space();
                  '0'..'9'  : process_digit(c);
                  'A'..'Z'  : process_alpha(c);
                end;

                Как записать просто и понятно? (да, я в курсе, что жизнь стала сложнее и символ по нынешним временам может занимать 4 байта, а то и более, но мне хотя бы в рамках ASCII)


                1. PsyHaSTe
                  30.01.2017 20:56

                  Да точно так же. Pattern matching не заставляет все ветки кода возвращать значения, они спокойно могут делать какую-то работу.

                  switch(shape)
                  {
                      case Circle c:
                          WriteLine($"circle with radius {c.Radius}");
                          break;
                      case Rectangle s when (s.Length == s.Height):
                          WriteLine($"{s.Length} x {s.Height} square");
                          break;
                      case Rectangle r:
                          WriteLine($"{r.Length} x {r.Height} rectangle");
                          break;
                      default:
                          WriteLine("<unknown shape>");
                          break;
                      case null:
                          throw new ArgumentNullException(nameof(shape));
                  }
                  


                  1. aamonster
                    30.01.2017 21:55

                    Это всё прекрасно и отлично решает свои задачи (диспатч по типу и декомпозиция объекта — просто, понятно, без оверхеда и места для глупых ошибок).
                    Но я говорил о более простых вещах. Обратите внимание на диапазоны в моём примере. #0..#31, '0'..'9', 'A'..'Z'. Дико бесит, что в C вместо них сделали какой-то невнятный костыль с проваливанием в следующий кейс, если не написал break.


                    1. PsyHaSTe
                      30.01.2017 22:36

                      Ну так используйте: case char c when c > '0' && c < '9' ...


                      1. aamonster
                        30.01.2017 23:11

                        И вот мы пришли к
                        if (c>='0' && c<='9')
                        с которого начинали :-)
                        Серьёзно: case не нужен (c у нас всегда char, это не часть алгебраического типа), when от if ничем не отличается (у нас императивщина, не забыли?)


                        1. PsyHaSTe
                          30.01.2017 23:14

                          ну тем, что в if'е мы используем любое значение, а тут мы можем только локальный скоуп использовать.


                    1. Maccimo
                      30.01.2017 23:40
                      +1

                      Обратите внимание на диапазоны в моём примере. #0..#31, '0'..'9', 'A'..'Z'.

                      При желании для любой пары ЯП можно подобрать пример, в котором один язык выглядит удобнее другого. Для сколь-нибудь сложного парсинга и удобнее и логичнее использовать генераторы парсеров.


                      Дико бесит, что в C вместо них сделали какой-то невнятный костыль с проваливанием в следующий кейс

                      Линейное исполнение программы теперь называется "костылём"?


                      1. PsyHaSTe
                        30.01.2017 23:48

                        Линейное исполнение программы теперь называется "костылём"?
                        Учитывая, что во всех последющих языках вроде джавы и шарпа от этого отказались (в том же шарпе это вызывает ошибку компиляции), то стоит задуматься о "линейности" goto-like оператора.


                        1. grossws
                          31.01.2017 11:28

                          Учитывая, что во всех последющих языках вроде джавы и шарпа от этого отказались (в том же шарпе это вызывает ошибку компиляции), то стоит задуматься о "линейности" goto-like оператора.

                          Ага, в Java отказались. Видимо все ставят break/return в ветках case по привычке, а всякие статические анализаторы распознают специальный комментарий fallthrough просто смеха ради.


                          1. PsyHaSTe
                            31.01.2017 13:04

                            Так с чем спорите-то, если я написал, что от fallthrough во всех последющих языках отказались, и break стал ненужным потому что без него в том же шарпе программа вообще не компилируется?


                      1. aamonster
                        31.01.2017 09:42

                        При желании для любой пары ЯП можно подобрать пример, в котором один язык выглядит удобнее другого.

                        Ну да. Когда я перешёл с Паскаля/Дельфи на C++ — я получил кучу вкусностей. И некоторое количество минусов (собственно, связанных с языком всего три: case, for, with; ну, плюс тормоза компиляции из-за подключения модулей через include). Смена используемых библиотек повлияла куда сильнее.


                1. Sirikid
                  31.01.2017 04:15

                  Взять из ML (F#) pattern matching — конечно, тоже польза, но он хорош для функционального стиля.
                  А чем он плох для императивного стиля?

                  Тут дело даже не в парадигме языка, паттерн матчинг это выбор одного из конструкторов алгебраического типа, switch-case/case-of это паттерн матчинг по частному случаю алгебраического типа — перечисляемому типу. Если в языке нету полноценных алгебраических типов то и полноценный паттерн матчинг ему не нужен. cc: PsyHaSTe


                  Ну и убрать слово case у меток, как ненужное.

                  Когда я писал на Pascal как раз отсутствие какой-нибудь общей метки у веток вызывало больше всего неудобств, особенно если ветка была не просто вызовом функции и приходилось писать операторные скобки.


                  Имхо лучше всего проблему switch решили в Kotlin, новый оператор — по сути скорее if, чем switch, во многих случаях удобнее их обоих.


                  1. aamonster
                    31.01.2017 09:35

                    Да, пожалуй, как раз то, чего хотелось бы (особенно учитывая обобщение на не-константные выражения, чего порой не хватало в Паскале и Си). Но, к сожалению, как и Паскаль — тоже не мейнстрим.


                  1. PsyHaSTe
                    31.01.2017 13:06

                    ООП не обязательно имеет алгебраические типы, хотя с ними конечно же удобнее.
                    Оператор этот удобнее, но с точки зрения команды родного мне шарпа ввод новых ключевых слов они ОЧЕНЬ не любят. Поэтому и придумывают 100500 контекстов уже существующих конструкций. И не любят новый синтаксис вводить (вроде стрелочек в этом примере), хотя это и выглядит естественнее.


                    1. Sirikid
                      31.01.2017 14:42

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


          1. Maccimo
            30.01.2017 20:58

            99% того, что было в C# 1.0 спёрто из Java по понятным причинам, а уже Java позаимствовал эти идеи из C/C++.


            Хотя копи-пастить case раздражает

            «Копипастить», «раздражает», ключевое слово из 4 букв, вы серьёзно?


            1. PsyHaSTe
              30.01.2017 21:40

              99% того, что было в C# 1.0 спёрто из Java по понятным причинам, а уже Java позаимствовал эти идеи из C/C++.

              Тот же Рихтер считает, что родословная шарпа идет напрямую из С++, хотя и под влиянием джавы.
              «Копипастить», «раздражает», ключевое слово из 4 букв, вы серьёзно?

              Из 4, из 10 или из 2: какая разница? Все равно оно не несет никакого смысла. Никому не стало бы хуже от:
              switch(shape)
              {
                  Circle c:
                      WriteLine($"circle with radius {c.Radius}");
                      break;
                  Rectangle s when (s.Length == s.Height):
                      WriteLine($"{s.Length} x {s.Height} square");
                      break;
                  Rectangle r:
                      WriteLine($"{r.Length} x {r.Height} rectangle");
                      break;
                  default:
                      WriteLine("<unknown shape>");
                      break;
              }
              

              Но, как и в большинстве таких случаях, причина: историческая, а не потому, что так лучше. К примеру про приоритет побитового И в if-е:
              Let me sum up:

              • & and | are almost always used as arithmetic operators, and therefore should have higher precedence than equality, just like the other arithmetic operators.
              • The «lazy» && and || have lower precedence than equality. That's a good thing. For consistency, the «eager» & and | operators should have lower precedence as well, right?
              • By that argument, && and & should both have higher precedence than || and |, but that's not the case either.

              Conclusion: It's a mess. Why does C# do it this way? Because that's how C does it. Why? I give you the words of the late designer of C, Dennis Ritchie:

              In retrospect it would have been better to go ahead and change the precedence of & to higher than ==, but it seemed safer just to split & and && without moving & past an existing operator. (After all, we had several hundred kilobytes of source code, and maybe [three] installations....)

              Ritchie's wry remark illustrates the lesson. To avoid the cost of fixing a few thousand lines of code on a handful of machines, we ended up with this design error repeated in many successor languages that now have a corpus of who-knows-how-many billion lines of code. If you're going to make a backward-compatibility-breaking change, no time is better than now; things will be worse in the future.


              Кстати, то же самое относится и к копипасте брейков в конце.


  1. TOBBOT
    30.01.2017 09:18

    Примеры не убедительные (особенно в начале статьи). Мне больше импонирует приведенный код с использованием SWITCH. Читался он на порядок быстрее конструкций с IF.


  1. AlexTheLost
    30.01.2017 09:21
    +2

    Красота это pattern matching. А по поводу switch, когда писал на Java всегда использовал if. Так и не нашел варианта когда он полезен.
    Переход на Scala для меня решил проблему боли от ненужного и невыразительного кода который приходится писать используя Java.


  1. speakingfish
    30.01.2017 11:39
    +1

    Тема визиторов не раскрыта:

        public List<String> getTypes1(Channel channel) {
            channel.visit(new Channel.UnsupportedVisitor<List<String>>() {
                @Override List<String> visitInternet() {
                    return Arrays.asList("SMS", "MAC");
                }
                @Override List<String> visitHomePhone() {
                    return Arrays.asList("CHECK_LIST");
                }
                @Override List<String> visitDevicePhone() {
                    return visitHomePhone();
                }
                });
        }
    


    1. speakingfish
      30.01.2017 12:42

      Только визиторы могут сделать контролируемое поведение веток ветвления:
      1. Визиторы могут обязывать реализовывать все ветки. (реализация интерфейса/абстрактного класса визитора)
      2. Визиторы могут бросать исключение если ветка нереализована. (перекрытие дефолтной реализации визитора, бросающего исключение во всех ветках)
      Никакие языковые конструкции организации ветвления не могут обязать писать код одним из перечисленных выше способов.


  1. breakoffbrain
    30.01.2017 12:29

    код в пункте 5 явно нуждается в рефакторинге и код после каждого case можно было вынести в отдельную функцию. статья в целом заставляет задуматься, но никаких преимуществ в использовании многочисленных if лично я не увидел. Опять же, тот же switch всегда можно «спрятать» в отдельной функции.


  1. Durimar123
    30.01.2017 12:31

    Солидарен с автором — swith по сравнению с multyIF он как более правильный, но его синтаксис делает его одновременно и плохо читаемым и много паразитного текста набирать надо.


    1. PsyHaSTe
      30.01.2017 16:44
      +1

      Текст обычно набирает IDE, проблема возникает, когда человеку его потом читать приходится. Но обычно через пару лет разработки подобные паттерны игнорируются и становится пофиг. Хотя уменьшение бойлерплейт-кода, конечно, всегда радует.


  1. guai
    31.01.2017 13:31
    +1

    Еще б вы лишние фигурные скобки не ставили — совсем красиво было. А то каждый раз сидишь и гадаешь, автор использует дурацкие стиль; или он туда планировал что-то вписать, но забыл; или он оттуда что-то убрал, но забыл убрать скобки.