Чому оператор перемикання розрахований на необхідність перерви?


139

Дано просту заяву про перемикання

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

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

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

Відсутність заяви про перерву у випадку 2 означає, що виконання триватиме всередині коду для випадку 3. Це не випадковість; це було розроблено саме так. Чому було прийнято таке рішення? Яку користь це забезпечує від автоматичного семантичного розриву для блоків? Що було обґрунтуванням?

Відповіді:


150

Здається, багато відповідей зосереджуються на здатності пропасти як причину вимагати breakзаяви.

Я вважаю, що це була просто помилка, багато в чому пов'язана з тим, що коли проектувався C, не було майже такого досвіду, як використовувати ці конструкції.

Петер Ван дер Лінден говорить про це у своїй книзі "Експертне програмування на 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;
}

Випадок випадків настільки широко визнається дефектом, що існує навіть специфічна конвенція про коментарі, показана вище, що говорить про те, що "це дійсно один із тих 3% випадків, коли провал був бажаний".

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

Дуже погано Java не скористалася можливістю відірватися від семантики С.


Дійсно, я думаю, що вони пішли на простоту реалізації. Деякі інші мови підтримували більш складні випадки (діапазони, кілька значень, рядки ...) ціною, можливо, ефективності.
PhiLho

Java, мабуть, не хотіла порушувати звички і поширювати плутанину. Для різної поведінки їм доведеться використовувати різну семантику. Дизайнери Java втратили чимало можливостей відірватися від C, у будь-якому випадку.
PhiLho

@PhiLho - Я думаю, ти, мабуть, найближчий до істини з "простотою реалізації".
Майкл Берр

Якщо ви використовуєте явні стрибки через GOTO, чи не настільки ж продуктивно використовувати лише ряд тверджень IF?
DevinB

3
навіть Паскаль здійснює свій перемикач без перерви. як міг компілятор-кодер C не думати про це @@
Чан Ле

30

Багато в чому c - це просто чистий інтерфейс до стандартних ідіом складання. Під час написання керованого керування потоком керування таблицею програміст має можливість вибиватися або вистрибувати з "керуючої структури", а для вистрибування потрібна чітка інструкція.

Отже, c робить те саме ...


1
Я знаю, що багато людей говорять про це, але я думаю, що це не повна картина; C також часто є некрасивим інтерфейсом для складання антиформатів. 1. Некрасиво: заяви замість виразів. "Декларація відображає використання." Прославлений копіпаст , як в механізмі модульність і портативність. Тип системи спосіб занадто складним; заохочує помилок. NULL. (Продовження в наступному коментарі.)
Гаррісон

2. Анти-візерунки: Не передбачено "чистих" позначених об'єднань, однієї з двох основних ідіом представлення даних збірки. (Кнут, т. 1, гл. 2) Натомість "немечені союзи", неідіоматичний хак. Цей вибір десятиліттями міркував над тим, як люди думають про дані. І NUL-закінчені рядки - це якраз найгірша ідея.
Гаррісон

@HarrisonKlaperman: Жоден метод зберігання рядків не є ідеальним. Переважна більшість проблем, пов’язаних з нульовими завершеними рядками, не виникало б, якщо підпрограми, які приймають рядки, також приймають параметр максимальної довжини, і виникатимуть з підпрограмами, які зберігають рядки, позначені довжиною, в буфери фіксованого розміру, не приймаючи параметр розміру буфера . Дизайн заяви перемикача, коли корпуси просто мітки, може здатися дивним для сучасних очей, але це не гірше, ніж дизайн DOпетлі Fortran .
supercat

Якщо я вирішив написати таблицю стрибків у збірці, я візьму значення регістру і магічним чином перетворять його в підписник таблиці стрибків, перейду до цього місця та виконаю код. Після цього я не вскочу в наступний випадок. Я перескочу на єдину адресу, яка є виходом з усіх справ. Думка про те, що я стрибну чи впаду, хоча в тіло наступного випадку, є дурною. Для цього шаблону немає жодного випадку використання.
EvilTeach

2
Хоча в наш час люди більше використовуються для очищення і самозахисту ідолів та мовних особливостей, які заважають собі стріляти в ногу, це спогад з тієї області, де байти були дорогими (C почався ще до 1970 року). якщо ваш код повинен вміщуватися в межах 1024 байтів, ви відчуєте велику загрозу повторного використання фрагментів коду. Повторне використання коду, починаючи з різних точок входу, що мають один і той же кінець, є одним із механізмів досягнення цього.
рпі

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);
    }
}

3
Я люблю пристрій Даффа. Так елегантно, і нечестиво швидко.
dicroce

7
так, але чи потрібно це показувати кожного разу, коли з’являється заява про переключення на SO?
billjamesdev

Ви пропустили два закриваючих фігурні дужки ;-).
Toon Krijthe

18

Якщо справи були розроблені неявно, то ви не могли б провалитися.

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
Ви можете зобразити перемикач, який ламається неявно, але має ключове слово "провал". Незграбний, але працездатний.
dmckee --- кошеня колишнього модератора

Як би це було краще? Я б уявив, що випадок випадку працює таким чином частіше, ніж "блок коду на випадок" ... це блок if / then / else.
billjamesdev

Я додам, що у питанні, вкладені області {} у кожному випадку блоку додають плутанину, оскільки це виглядає як стиль висловлювання "while".
Меттью Сміт

1
@Bill: Думаю, було б гірше, але це стосуватиметься скарги, яку висунув Майк Б: хоча падіння (окрім кількох випадків однакових) є рідкісною подією і не повинно бути поведінкою за замовчуванням.
dmckee --- кошеня колишнього модератора

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

15

Оператори справи в операторах переключення - це просто мітки.

Коли ви включаєте значення, оператор switch по суті робить goto на мітку зі значенням відповідності.

Це означає, що перерва необхідний, щоб не переходити до коду під наступною міткою.

Що стосується причини, по якій він був реалізований таким чином - характер пропускання оператора перемикання може бути корисним у деяких сценаріях. Наприклад:

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) Якщо в заявах справи зміниться їх порядок, це може призвести до неправильного запуску справи. Таким чином, я вважаю, що керування C # набагато краще (явне goto caseдля прориву, за винятком порожніх міток).
Даніель Роуз

@DanielRose: 1) Є кілька способів забути breakв C # - найпростіший варіант - це коли ти не хочеш 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мітку, і воно набагато природніше.


8
Це if(0)в кінці першого випадку є злим, і його слід використовувати лише в тому випадку, якщо мета - придушити код.
Даніель Роуз

11
Думаю, вся ця відповідь була вправою бути злим. :-)
R .. GitHub ЗАСТОСУЄТЬСЯ ДО ЛОСУ

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);
}

0

Як багато хто вказав тут, це дозволити одному блоку коду працювати в декількох випадках. Це повинно бути бути більш поширеним явищем для ваших операторів переключення, ніж "блок коду на випадок", який ви вказали у своєму прикладі.

Якщо у вас є код коду на кожний випадок без пропускання, можливо, вам слід розглянути можливість використання блоку if-elseif-else, як це здасться більш підходящим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.