Почему мы должны использовать break в switch?

74

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

Почему мы должны написать что-то вроде этого:

switch(a)
{
    case 1:
        result = 'one';
        break;
    case 2:
        result = 'two';
        break;
    default:
        result = 'not determined';
        break;
}

(заметил это в PHP и JS; возможно, есть много других языков, использующих это)

Если switchесть альтернатива if, почему мы не можем использовать ту же конструкцию, что и для if? То есть:

switch(a)
{
    case 1:
    {
        result = 'one';
    }
    case 2:
    {
        result = 'two';
    }
    default:
    {
        result = 'not determined';
    }
}

Говорят, что breakпредотвращает выполнение блока, следующего за текущим. Но действительно ли кто-то сталкивался с ситуацией, когда возникла необходимость выполнения текущего и последующих блоков? Я не Для меня breakвсегда есть. В каждом блоке. В каждом коде.

trejder
источник
1
Как отмечалось в большинстве ответов, это связано с «провалом», когда несколько условий направляются через одни и те же блоки «тогда». Однако это зависит от языка - например, RPG обрабатывает CASEоператор эквивалентно гигантскому блоку if / elseif.
Заводная муза
34
Это было плохое решение, принятое C-проектировщиками (как и многие решения, оно было сделано для того, чтобы сделать переход от сборки -> C и перевод обратно, проще, а не для простоты программирования) , которое тогда, к сожалению, было унаследовано на других языках на основе Си. Используя простое правило "Всегда делайте общий случай по умолчанию !!" мы можем видеть, что поведение по умолчанию должно было нарушаться с отдельным ключевым словом для провала, так как это, безусловно, самый распространенный случай.
BlueRaja - Дэнни Пфлюгофт
4
Я несколько раз использовал провал, хотя, возможно, оператор переключения на моем предпочтительном языке в то время (C) мог быть разработан, чтобы избежать этого; например, я сделал, case 'a': case 'A': case 'b': case 'B'но в основном потому, что я не могу сделать case in [ 'a', 'A', 'b', 'B' ]. Немного лучший вопрос, в моем текущем предпочтительном языке (C #), разрыв обязателен , и не существует неявного падения; забывание break- синтаксическая ошибка ...: \
KutuluMike
1
Провал с реальным кодом часто встречается в реализациях парсера. case TOKEN_A: /*set flag*/; case TOKEN_B: /*consume token*/; break; case TOKEN_C: /*...*/
Стоп Harm Моника
1
@ BlueRaja-DannyPflughoeft: C также был спроектирован так, чтобы его было легко компилировать. «Испускать прыжок, если breakон присутствует где-либо» - это значительно более простое правило для реализации, чем «Не испускать прыжок, если fallthroughон присутствует в switch».
Джон Пурди

Ответы:

95

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

Что касается того, почему C был разработан таким образом, он, вероятно, вытекает из концепции C как «переносной сборки». switchУтверждение в основном это абстракция таблицы отраслевой и таблица филиала также имеет неявное падение-через и требует дополнительной инструкции перехода , чтобы избежать этого.

Таким образом, дизайнеры C также решили оставить семантику ассемблера по умолчанию.

Майкл Боргвардт
источник
3
VB.NET является примером языка, который изменил его. Нет опасности того, что он попадет в дело.
тыкай
1
Tcl - еще один язык, который никогда не дойдет до следующего пункта. Практически никогда не хотел этого делать (в отличие от C, где это полезно при написании определенных типов программ).
Donal Fellows
4
Языки предков C, B и более старая BCPL , имеют (имели?) Операторы switch; BCPL пишется по буквам switchon.
Кит Томпсон
Заявления Руби по делу не проваливаются; Вы можете получить похожее поведение, имея два тестовых значения в одном when.
Натан Лонг
86

Потому что switchэто не альтернатива if ... elseзаявлениям на этих языках.

Используя switch, мы можем сопоставить более одного условия за раз, что высоко ценится в некоторых случаях.

Пример:

public Season SeasonFromMonth(Month month)
{
    Season season;
    switch (month)
    {
        case Month.December:
        case Month.January:
        case Month.February:
            season = Season.Winter;
            break;

        case Month.March:
        case Month.April:
        case Month.May:
            season = Season.Spring;
            break;

        case Month.June:
        case Month.July:
        case Month.August:
            season = Season.Summer;
            break;

        default:
            season = Season.Autumn;
            break;
    }

    return season;
}
Сатиш Пандей
источник
15
more than one block ... unwanted in most casesЯ не согласен с вами.
Сатиш Пандей
2
@DanielB Это зависит от языка, хотя; Операторы переключения C # не допускают такой возможности.
Энди
5
@ Andy Мы говорим о провале еще? Операторы C # switch определенно позволяют это (см. 3-й пример), отсюда и необходимость break. Но да, насколько я знаю, устройство Даффа не работает в C #.
Даниэль Б
8
@DanielB Да, но компилятор не допустит условие, код, а затем другое условие без перерыва. Вы можете сложить несколько условий без кода между ними, или вам нужен перерыв. По вашей ссылке C# does not support an implicit fall through from one case label to another. Вы можете провалиться, но нет шансов случайно забыть перерыв.
Энди
3
@ Мацеманн .. Я теперь мы можем сделать все, используя вещи, if..elseно в некоторых ситуациях switch..caseбыло бы предпочтительнее. Приведенный выше пример будет немного сложным, если мы используем if-else.
Сатиш Пандей
15

Это было задано в связи с переполнением стека в контексте C: почему для оператора switch был нужен разрыв?

Подводя итог принятому ответу, это была, вероятно, ошибка. Большинство других языков, вероятно, просто следовали за C. Однако, некоторые языки, такие как C #, похоже, исправили это, допустив провал - но только когда программист явно говорит об этом (источник: ссылка выше, я сам не говорю на C #) ,

Тапио
источник
Google Go делает то же самое, что и IIRC (допускает падение, если это явно указано).
Лев
PHP тоже. И чем больше вы смотрите на полученный код, тем неприятнее он вас чувствует. Мне нравится /* FALLTHRU */комментарий в ответе, связанный @Tapio выше, чтобы доказать, что вы это имели в виду.
DaveP
1
Утверждение C # goto case, как и ссылка, не иллюстрирует, почему это работает так хорошо. Вы по-прежнему можете составлять несколько случаев, как описано выше, но как только вы вставляете код, вам нужно иметь явное выражение goto case whateverдля перехода к следующему случаю, или вы получите ошибку компилятора. Если вы спросите меня, из двух, возможность суммировать дела, не беспокоясь о непреднамеренном провале из-за халатности, намного лучше, чем возможность явного провала с помощью goto case.
Джеспер
9

Я отвечу на примере. Если вы хотите указать количество дней в каждом месяце года, очевидно, что в некоторых месяцах 31, а в 30 - 1 28/29. Это будет выглядеть так,

switch(month) {
    case 4:
    case 6:
    case 9:
    case 11;
        days = 30;
        break;
    case 2:
        //Find out if is leap year( divisible by 4 and all that other stuff)
        days = 28 or 29;
        break;
    default:
        days = 31;
}

Это пример, когда несколько случаев имеют одинаковый эффект и все сгруппированы вместе. Очевидно, была причина выбора ключевого слова break, а не конструкции if ... else if .

Главное, на что следует обратить внимание, это то, что оператор switch во многих похожих случаях - это не if ... else if ... else if ... else для каждого из случаев, а скорее if (1, 2, 3) ... еще, если (4,5,6) еще ...

Awemo
источник
2
Ваш пример выглядит более многословно, чем ifбез какой-либо выгоды. Возможно, если бы в вашем примере было назначено имя или выполнялась работа по конкретному месяцу в дополнение к провалу, полезность провала была бы более очевидной? (не комментируя, ДОЛЖЕН ли кто-либо быть на первом месте)
Горацио
1
Это правда. Однако я просто пытался показать случаи, когда можно было бы использовать провал. Очевидно, было бы намного лучше, если бы каждый месяц нужно было что-то делать индивидуально.
Awemo
8

Существуют две ситуации, когда может произойти «провал» из одного дела в другое - пустой случай:

switch ( ... )
{
  case 1:
  case 2:
    do_something_for_1_and_2();
    break;
  ...
}

и непустой случай

switch ( ... )
{
  case 1:
    do_something_for_1();
    /* Deliberately fall through */
  case 2:
    do_something_for_1_and_2();
    break;
...
}

Несмотря на ссылку на устройство Даффа , законные экземпляры для второго случая очень редки и, как правило, запрещены стандартами кодирования и помечены во время статического анализа. И там, где это найдено, это чаще всего происходит из-за упущения а break.

Первое совершенно разумно и распространено.

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

Жаль, что панель ISO C выглядит более озабоченной добавлением новых (нежелательных) и плохо определенных функций в язык, а не исправлением неопределенных, неуказанных или определяемых реализацией функций, не говоря уже о нелогичности.

Андрей
источник
2
Слава Богу, ISO C не вносит серьезных изменений в язык!
Джек Эйдли
4

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

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

Рекомендуется заменить breakсоответствующий комментарий, если в кейсе есть какой-либо код. В противном случае кто-то исправит недостающее breakи внесет ошибку.

BillThor
источник
Спасибо за пример устройства Даффа (+1). Я не знал об этом.
Трейдер
3

В C, где, как представляется, источник, блок кода switchоператора не является специальной конструкцией. Это обычный блок кода, такой же, как блок под ifоператором.

switch ()
{
}

if ()
{
}

caseи defaultметки перехода внутри этого блока, особенно связанные с switch. Они обрабатываются так же, как обычные метки перехода goto. Здесь важно одно конкретное правило: метки перехода могут быть практически везде в коде, не прерывая поток кода.

Как обычный блок кода, он не должен быть составным оператором. Метки тоже не обязательны. Это действительные switchутверждения в C:

switch (a)
    case 1: Foo();

switch (a)
    Foo();

switch (a)
{
    Foo();
}

Сам стандарт C дает это в качестве примера (6.8.4.2):

switch (expr) 
{ 
    int i = 4; 
    f(i); 
  case 0: 
    i=17; 
    /*falls through into default code */ 
  default: 
    printf("%d\n", i); 
} 

Во фрагменте искусственной программы объект с идентификатором i существует с автоматической продолжительностью хранения (в пределах блока), но никогда не инициализируется, и, таким образом, если управляющее выражение имеет ненулевое значение, вызов функции printf получит доступ к неопределенному значению. Аналогично, вызов функции f не может быть достигнут.

Кроме того, defaultэто тоже метка перехода, и, следовательно, она может быть где угодно, без необходимости быть последним случаем.

Это также объясняет устройство Даффа:

switch (count % 8) {
    case 0: do {  *to = *from++;
    case 7:       *to = *from++;
    case 6:       *to = *from++;
    case 5:       *to = *from++;
    case 4:       *to = *from++;
    case 3:       *to = *from++;
    case 2:       *to = *from++;
    case 1:       *to = *from++;
            } while(--n > 0);
}

Почему провал? Потому что в нормальном потоке кода в нормальном блоке кода ожидается переход к следующему оператору , так же, как вы ожидаете его в ifблоке кода.

if (a == b)
{
    Foo();
    /* "Fall-through" to Bar expected here. */
    Bar();
}

switch (a)
{
    case 1: 
        Foo();
        /* Automatic break would violate expected code execution semantics. */
    case 2:
        Bar();
}

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

Интересный дополнительный вопрос из всего этого состоит в том, что следующие вложенные операторы печатают «Готово». или нет.

int a = 10;

switch (a)
{
    switch (a)
    {
        case 10: printf("Done.\n");
    }
}

Стандарт C заботится об этом (6.8.4.2.4):

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

Безопасный
источник
1

Несколько человек уже упомянули идею соответствия множественным условиям, что время от времени очень ценно. Однако способность соответствовать нескольким условиям не обязательно требует, чтобы вы выполняли одно и то же с каждым соответствующим условием. Учтите следующее:

switch (someCase)
{
    case 1:
    case 2:
        doSomething1And2();
        break;

    case 3:
        doSomething3();

    case 4:
        doSomething3And4();
        break;

    default:
        throw new Error("Invalid Case");
}

Есть два разных способа сопоставления множества условий. При выполнении условий 1 и 2 они просто переходят к одному и тому же фрагменту кода и делают одно и то же. С условиями 3 и 4, однако, хотя они оба заканчиваются вызовом doSomething3And4(), только 3 вызова doSomething3().

Panzercrisis
источник
Поздняя вечеринка, но это абсолютно не «1 и 2», как следует из названия функции: есть три разных состояния, которые могут привести к case 2запуску кода , поэтому, если мы хотим быть правильными (и мы делаем) реальное имя функции должно быть doSomethingBecause_1_OR_2_OR_1AND2()или что-то в этом роде (хотя законный код, в котором неизменяемый соответствует двум разным случаям, хотя хорошо написанный код практически отсутствует, так что, по крайней мере, так должно быть doSomethingBecause1or2())
Майк 'Pomax' Kamermans
Другими словами, doSomething[ThatShouldBeDoneInTheCaseOf]1And[InTheCaseOf]2(). Это не относится к логическому и / или.
Panzercrisis
Я знаю, что это не так, но с учетом того, почему люди путаются по поводу такого рода кода, он должен полностью объяснить, что это такое «и», потому что полагаться на то, что кто-то угадывает «естественный и» против «логический конец» в ответе, который работает только когда точно объяснено, нужно больше объяснения в самом ответе или переписать имя функции, чтобы убрать эту неоднозначность =)
Майк 'Pomax' Kamermans
1

Чтобы ответить на два ваших вопроса.

Зачем C нужны перерывы?

Все сводится к корням Cs как «переносному ассемблеру». Где псевдо-код, как это было распространено:

    targets=(addr1,addr2,addr3);
    opt = 1  ## or 0 or 2
switch:
    br targets[opt]  ## go to addr2 
addr1:
    do 1stuff
    br switchend
addr2:
    do 2stuff
    br switchend
addr3
    do 3stuff
switchend:
    ......

Инструкция switch была разработана для обеспечения аналогичной функциональности на более высоком уровне.

У нас когда-нибудь есть выключатели без перерывов?

Да, это довольно часто, и есть несколько вариантов использования;

Во-первых, вы можете предпринять одно и то же действие в нескольких случаях. Мы делаем это, укладывая чехлы друг на друга:

case 6:
case 9:
    // six or nine code

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

case 9:
    crashed()
    newstate=10;
case 10:
    claim_damage();
    break;
Джеймс Андерсон
источник
-2

Оператор Break - это оператор прыжка, который позволяет пользователю выйти из ближайшего включающего переключателя (для вашего случая), в то время как, do, for или foreach. это так же просто, как это.

Джаспер
источник
-3

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

Если вам нужен тот редкий случай, когда вы хотите перейти к следующему случаю - просто заявите это с помощью continueзаявления.

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

ethans
источник
2
Я не уверен, что понимаю ваше предложение относительно заявления о продолжении. Continue имеет совсем другое значение в C и C ++, и если бы новый язык был похож на C, но использовал ключевое слово иногда как с C, а иногда и таким альтернативным способом, я думаю, что путаница будет царить. Хотя могут быть некоторые проблемы с переходом из одного помеченного блока в другой, мне нравится использование нескольких случаев с одинаковой обработкой с общим блоком кода.
DeveloperDon
C имеет давнюю традицию использования одних и тех же ключевых слов для нескольких целей. Подумайте *, &, и staticт.д.
idrougge