Чтобы добавить здесь ответы, я думаю, что стоит рассмотреть противоположный вопрос в связи с этим, а именно. почему C позволил провалиться в первую очередь?
Любой язык программирования, конечно, преследует две цели:
- Предоставьте инструкции компьютеру.
- Оставьте запись о намерениях программиста.
Поэтому создание любого языка программирования является балансом между тем, как наилучшим образом служить этим двум целям. С одной стороны, чем проще превратить в компьютерные инструкции (будь то машинный код, байт-код, такой как IL, или инструкции интерпретируются при исполнении), тем больше вероятность того, что процесс компиляции или интерпретации будет эффективным, надежным и компактный в выходе. В крайнем случае, эта цель приводит к тому, что мы просто пишем на ассемблере, IL или даже на необработанных кодах операций, потому что самая простая компиляция - это когда компиляция вообще отсутствует.
И наоборот, чем больше язык выражает намерение программиста, а не средства, использованные для этой цели, тем более понятна программа как при написании, так и во время обслуживания.
Теперь, switch
всегда можно было скомпилировать, преобразовав его в эквивалентную цепочку блоков if-else
или аналогичных, но он был спроектирован как позволяющий компилировать в определенный общий шаблон сборки, где каждый принимает значение, вычисляет смещение из него (либо путем поиска таблицы, проиндексированной по совершенному хешу значения, либо по фактической арифметике значения *). На данный момент стоит отметить, что сегодня компиляция C # иногда превращает switch
в эквивалент if-else
, а иногда использует подход перехода на основе хеша (и аналогично с C, C ++ и другими языками с сопоставимым синтаксисом).
В этом случае есть две веские причины для разрешения провала:
В любом случае это происходит естественным образом: если вы строите таблицу переходов в набор инструкций, а одна из более ранних групп инструкций не содержит какой-либо переход или возврат, то выполнение просто естественным образом переходит в следующая партия. Разрешение провала было тем, что «просто произошло бы», если бы вы превратили switch
-использующий C в таблицу переходов - используя машинный код.
Кодеры, которые писали на ассемблере, уже были использованы для эквивалента: при написании таблицы переходов вручную на ассемблере им пришлось бы учитывать, закончится ли данный блок кода возвратом, переходом за пределы стол, или просто перейдите к следующему блоку. Таким образом, добавление в кодер явного break
, когда это было необходимо, было «естественным» и для кодера.
В то время это была разумная попытка сбалансировать две цели компьютерного языка, поскольку они касаются как создаваемого машинного кода, так и выразительности исходного кода.
Четыре десятилетия спустя, хотя, по нескольким причинам, все не совсем так:
- Кодеры на C сегодня могут иметь небольшой опыт или вообще не иметь опыта сборки. Кодеры во многих других языках стиля C даже менее вероятно (особенно в Javascript!). Любая концепция «к чему люди привыкли со сборки» больше не актуальна.
- Улучшения в оптимизации означают, что вероятность того, что
switch
либо превратится в if-else
, поскольку считается, что этот подход наиболее эффективен, либо превращается в особенно эзотерический вариант подхода с использованием таблицы переходов, выше. Соотношение между подходами более высокого и более низкого уровня не такое сильное, как раньше.
- Опыт показывает, что провал - это, как правило, случай с меньшинством, а не норма (исследование компилятора Sun показало, что 3%
switch
блоков используют провал, отличный от нескольких меток в одном блоке, и это Считалось, что вариант использования здесь означал, что эти 3% были на самом деле намного выше, чем обычно). Таким образом, изучаемый язык делает необычное более легким в обращении, чем обычный.
- Опыт показывает, что провалы, как правило, являются источником проблем как в тех случаях, когда это происходит случайно, так и в тех случаях, когда кто-то, обслуживающий код, пропускает правильный провал. Последнее является тонким дополнением к ошибкам, связанным с провалом, потому что даже если ваш код не содержит ошибок, ваш провал может все еще вызывать проблемы.
В связи с этими двумя последними пунктами рассмотрим следующую цитату из текущей редакции K & R:
Переход от одного случая к другому не является надежным, поскольку он подвержен распаду при изменении программы. За исключением нескольких меток для одного вычисления, сквозные значения следует использовать с осторожностью и комментировать.
В порядке, поставьте перед последним регистром разрыв (по умолчанию здесь), даже если это логически не нужно. Когда-нибудь, когда в конце будет добавлен еще один случай, этот кусочек защитного программирования спасет вас.
Итак, изо рта лошади провал в C проблематичен. Хорошей практикой всегда является документирование ошибок с комментариями, что является применением общего принципа, согласно которому следует документировать, когда кто-то делает что-то необычное, потому что это поможет вам избежать последующего изучения кода и / или сделать ваш код похожим на него. есть ошибка новичка в этом, когда это на самом деле правильно.
И когда вы думаете об этом, код такой:
switch(x)
{
case 1:
foo();
/* FALLTHRU */
case 2:
bar();
break;
}
Является ли добавлением чего-либо, чтобы сделать явный провал в коде, это просто не то, что может быть обнаружено (или чье отсутствие может быть обнаружено) компилятором.
Таким образом, тот факт, что on должен быть явным с переходом в C #, не добавляет никаких штрафов тем, кто хорошо писал на других языках стиля C, так как они уже были бы явными в своих переходах . †
Наконец, использование goto
здесь уже является нормой для C и других таких языков:
switch(x)
{
case 0:
case 1:
case 2:
foo();
goto below_six;
case 3:
bar();
goto below_six;
case 4:
baz();
/* FALLTHRU */
case 5:
below_six:
qux();
break;
default:
quux();
}
В этом случае, когда мы хотим, чтобы блок был включен в код, выполняемый для значения, отличного от только того, которое приводит его к предыдущему блоку, мы уже должны использовать goto
. (Конечно, есть способы и способы избежать этого с помощью различных условных обозначений, но это верно практически обо всем, что касается этого вопроса). Таким образом, C # построен на уже нормальном способе справиться с одной ситуацией, когда мы хотим использовать более одного блока кода в switch
, и просто обобщить его, чтобы охватить также и провал. Это также сделало оба случая более удобными и самодокументируемыми, поскольку мы должны добавить новую метку в C, но можем использовать case
в качестве метки в C #. В C # мы можем избавиться от метки below_six
и использовать goto case 5
, что более понятно относительно того, что мы делаем. (Мы также должны были бы добавить break
для default
, который я пропустил, просто чтобы вышеуказанный код C явно не был кодом C #).
Таким образом, в итоге:
- C # больше не относится к неоптимизированному выводу компилятора так же непосредственно, как код C сделал 40 лет назад (и в наши дни C), что делает одно из вдохновляющих провалов несущественным.
- C # остается совместимым с C не только неявным
break
, для более легкого изучения языка теми, кто знаком с подобными языками, и более легким переносом.
- C # удаляет возможный источник ошибок или неправильно понятого кода, который был задокументирован как вызывающий проблемы в течение последних четырех десятилетий.
- C # делает существующую передовую практику с C (провал документа) осуществимой компилятором.
- C # делает необычный случай с более явным кодом, обычный случай с кодом, который просто пишет автоматически.
- C # использует тот же подход на основе
goto
для попадания в один и тот же блок из разных меток case
, который используется в C. Он просто обобщает его на некоторые другие случаи.
- C # делает такой подход на основе
goto
более удобным и понятным, чем в C, позволяя операторам case
выступать в качестве меток.
В целом, довольно разумное дизайнерское решение
* Некоторые формы Бейсика позволили бы делать подобные GOTO (x AND 7) * 50 + 240
, который, хотя и хрупок, и, следовательно, особенно убедителен для запрета goto
, служит для того, чтобы показать более высокий языковой эквивалент вида, который ниже -уровневый код может сделать переход, основанный на арифметике, на значении, что гораздо разумнее, когда это результат компиляции, а не что-то, что должно поддерживаться вручную. В частности, реализации устройства Даффа хорошо подходят для эквивалентного машинного кода или IL, потому что каждый блок инструкций часто будет одинаковой длины без необходимости добавления nop
заполнителей.
† Устройство Даффа снова появляется здесь как разумное исключение. Тот факт, что с этим и схожими шаблонами происходит повторение операций, делает использование сквозного перехода относительно ясным даже без явного комментария на этот счет.