Почему оператор switch был разработан с учетом необходимости перерыва?

140

Учитывая простой оператор switch

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

Отсутствие оператора break в случае 2 означает, что выполнение будет продолжено внутри кода для случая 3. Это не случайно; так оно и было задумано. Почему было принято это решение? Какие преимущества это дает по сравнению с семантикой автоматического разбиения блоков? В чем было обоснование?

EvilTeach
источник

Ответы:

152

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

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

Питер Ван дер Линден приводит доводы в своей книге «Экспертное программирование на языке Си»:

Мы проанализировали исходные тексты компилятора Sun C, чтобы увидеть, как часто использовались провалы по умолчанию. Передняя часть компилятора Sun ANSI C имеет 244 оператора переключения, каждый из которых имеет в среднем семь случаев. Падение происходит всего в 3% всех этих случаев.

Другими словами, нормальное поведение переключателя неверно в 97% случаев. Дело не только в компиляторе - напротив, там, где в этом анализе использовался провал, это часто было для ситуаций, которые чаще возникают в компиляторе, чем в другом программном обеспечении, например, при компиляции операторов, которые могут иметь один или два операнда. :

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

Провал кейса настолько широко признан как дефект, что существует даже специальное соглашение о комментариях, показанное выше, в котором говорится, что lint «это действительно один из тех 3% случаев, когда провал был желательным».

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

Жаль, что Java не воспользовалась возможностью вырваться из семантики C.

Майкл Берр
источник
Действительно, я думаю, они пошли на простоту реализации. Некоторые другие языки поддерживали более сложные случаи (диапазоны, множественные значения, строки ...) за счет, возможно, эффективности.
PhiLho
Вероятно, Java не хотела разрушать привычки и сеять путаницу. Для другого поведения им пришлось бы использовать другую семантику. Так или иначе, Java-дизайнеры потеряли ряд возможностей оторваться от C.
PhiLho,
@PhiLho - Думаю, вы, наверное, ближе всего к истине с «простотой реализации».
Майкл Берр,
Если вы используете явные переходы через GOTO, разве не так продуктивно просто использовать серию операторов IF?
DevinB
3
даже Паскаль реализовал свой переключатель без перерыва. как мог компилятор-программист C не думать об этом @@
Чан Ле
30

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

Итак, c делает то же самое ...

dmckee --- котенок экс-модератора
источник
2
Я знаю, что многие люди так говорят, но я думаю, что это не полная картина; C также часто является уродливым интерфейсом для антипаттернов сборки. 1. Уродливо: утверждения вместо выражений. «Декларация отражает использование». Прославленный копипаст , как в механизме модульность и портативность. Тип системы способ слишком сложным; поощряет ошибки. NULL. (Продолжение в следующем комментарии.)
Харрисон
1
2. Антипаттерны: не предоставляет "чистые" помеченные объединения, одну из двух фундаментальных идиом сборки данных. (Knuth, том 1, глава 2) Вместо этого «немаркированные союзы» - неидиоматический взлом. Этот выбор мешал людям думать о данных на протяжении десятилетий. И NUL-терминальные строки - едва ли не худшая идея.
Харрисон
@HarrisonKlaperman: Нет идеального метода хранения строк. Подавляющее большинство проблем, связанных со строками с завершающим нулем, не возникло бы, если бы подпрограммы, которые принимали строки, также принимали параметр максимальной длины, и возникли бы с подпрограммами, которые сохраняют строки с маркировкой длины в буферах фиксированного размера без принятия параметра размера буфера. . Дизайн оператора switch, в котором регистры представляют собой просто метки, может показаться современным глазам странным, но он не хуже, чем дизайн цикла Fortran DO.
supercat
Если я решу написать таблицу переходов в сборке, я возьму значение case и волшебным образом превращу его в индекс таблицы переходов, перейду в это место и выполню код. После этого я не перейду к следующему делу. Я перейду к единому адресу, который является выходом из всех дел. Мысль о том, что я прыгну или упаду в тело следующего дела, глупая. У этого шаблона нет варианта использования.
EvilTeach
2
В то время как в настоящее время люди больше используются для очистки и самозащиты идо и языковых функций, которые не позволяют выстрелить себе в ногу, это воспоминание из области, где байты были дорогими (C появился еще до 1970 года). если ваш код должен умещаться в пределах 1024 байтов, вы испытаете сильное желание повторно использовать фрагменты кода. Повторное использование кода, начиная с разных точек входа, имеющих один и тот же конец, является одним из механизмов достижения этого.
rpy
23

Очевидно, что для реализации устройства Даффа:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    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);
    }
}
FlySwat
источник
3
Я люблю Устройство Даффа. Так элегантно и быстро.
dicroce
7
да, но должно ли оно появляться каждый раз, когда на SO есть оператор switch?
billjamesdev
Вы пропустили две закрывающие фигурные скобки ;-).
Toon Krijthe
19

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

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

Если бы переключатель был спроектирован так, чтобы неявно срабатывать после каждого случая, у вас не было бы выбора. Конструкция корпуса выключателя была спроектирована так, чтобы быть более гибкой.

Билл Ящерица
источник
12
Вы можете представить себе переключатель, который неявно ломается, но имеет ключевое слово «fallthrough». Неудобно, но работоспособно.
dmckee --- котенок экс-модератора
Как бы это было лучше? Я полагаю, что оператор case работает таким образом чаще, чем «блок кода на случай» ... это блок if / then / else.
billjamesdev
Я бы добавил, что в вопросе вложения областей видимости {} в каждом блоке case добавляют путаницы, поскольку они выглядят как стиль оператора while.
Мэтью Смит,
1
@Bill: Было бы хуже, я думаю, но это касается жалобы, поданной Майком Б: это падение (за исключением нескольких случаев того же) - редкое событие и не должно быть поведением по умолчанию.
dmckee --- котенок экс-модератора
1
Я оставил фигурные скобки для краткости, так как они должны присутствовать в нескольких утверждениях. Я согласен с тем, что провал следует использовать только тогда, когда случаи точно такие же. Это чертовски сбивает с толку, когда кейсы основываются на предыдущих кейсах с использованием провалов.
Bill the Lizard
15

Операторы case в операторах switch - это просто метки.

При включении значения, оператор переключатель по существу делает Гот на метку со значением соответствия.

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

Что касается причины, по которой это было реализовано таким образом, то в некоторых сценариях может быть полезен провальный характер оператора switch. Например:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;
ЛеопардКожаТаблеткакоробкашляпа
источник
2
Есть две большие проблемы: 1) Забыть там, breakгде нужно. 2) Если порядок операторов case был изменен, провал может привести к запуску неправильного case. Таким образом, я считаю, что C # обрабатывает намного лучше (явно goto caseдля провалов, за исключением пустых меток case).
Дэниел Роуз
@DanielRose: 1) breakв C # тоже есть способы забыть - самый простой - это когда вы не хотите, чтобы a caseделал что-либо, но забываете добавить break(возможно, вы так увлеклись пояснительным комментарием или вас перезвонили по какому-то другая задача): выполнение просто упадет до caseследующего. 2) обнадеживает goto caseпоощрение неструктурированного "спагетти" кодирования - вы можете получить случайные циклы ( case A: ... goto case B; case B: ... ; goto case A;), особенно когда случаи разделены в файле и их трудно найти в сочетании. В C ++ локализован провал.
Тони Делрой
8

Чтобы разрешить такие вещи, как:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

Думайте о caseключевом слове как о gotoярлыке, и это получается намного естественнее.

R .. GitHub НЕ ПОМОГАЕТ ICE
источник
8
То, что if(0)в конце первого случая является злом, и должно использоваться только в том случае, если целью является запутывание кода.
Дэниел Роуз
12
Я думаю, весь этот ответ был упражнением в злодействе. :-)
R .. GitHub НЕ ПОМОГАЕТ ICE
3

Это исключает дублирование кода, когда в нескольких случаях необходимо выполнить один и тот же код (или один и тот же код последовательно).

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

Грег Роджерс
источник
2

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

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}
Martti K
источник
0

Как многие здесь указали, это позволяет одному блоку кода работать в нескольких случаях. Это должно быть более частым явлением для ваших операторов switch, чем «блок кода на случай», который вы указываете в своем примере.

Если у вас есть блок кода для каждого случая без провалов, возможно, вам следует рассмотреть возможность использования блока if-elseif-else, поскольку это может показаться более подходящим.

Billjamesdev
источник