Чтобы добавить здесь ответы, я думаю, что стоит рассмотреть противоположный вопрос в связи с этим, а именно. почему C позволил провалиться в первую очередь?
Любой язык программирования, конечно, преследует две цели:
- Предоставьте инструкции к компьютеру.
- Оставьте запись о намерениях программиста.
Поэтому создание любого языка программирования - это баланс между тем, как наилучшим образом служить этим двум целям. С одной стороны, чем проще превратить в компьютерные инструкции (будь то машинный код, байт-код, такой как IL, или инструкции интерпретируются при исполнении), тем больше вероятность того, что процесс компиляции или интерпретации будет эффективным, надежным и компактный в выходе. В крайнем случае, эта цель приводит к тому, что мы просто пишем на ассемблере, IL или даже на необработанных кодах операций, потому что самая простая компиляция - это когда компиляция вообще отсутствует.
И наоборот, чем больше язык выражает намерение программиста, а не средства, использованные для этой цели, тем более понятна программа как при написании, так и во время обслуживания.
Теперь switch
его всегда можно было скомпилировать, преобразовав его в эквивалентную цепочку if-else
блоков или аналогичную, но он был спроектирован как позволяющий компилировать в конкретный общий шаблон сборки, где каждый принимает значение, вычисляет смещение из него (будь то путем просмотра таблицы индексируется идеальным хэшем значения или фактической арифметикой значения *). Стоит отметить, что сегодня компиляция C # иногда превращается switch
в эквивалент if-else
, а иногда использует подход перехода на основе хеша (и аналогично с C, C ++ и другими языками с сопоставимым синтаксисом).
В этом случае есть две веские причины для разрешения провала:
В любом случае это происходит естественным образом: если вы строите таблицу переходов в набор инструкций, а одна из более ранних групп инструкций не содержит какой-либо переход или возврат, то выполнение просто естественным образом переходит в следующий пакет. Разрешение switch
провала было бы то, что «просто произошло бы», если бы вы превратили -using 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 # построен на уже нормальном способе справиться с одной ситуацией, когда мы хотим использовать более одного блока кода в a 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
операторам выступать в качестве меток.
В общем, довольно разумное дизайнерское решение
* Некоторые формы BASIC позволяют делать то, GOTO (x AND 7) * 50 + 240
что, будучи хрупким и, следовательно, особенно убедительным аргументом в пользу запрета goto
, служит для показа эквивалента на более высоком языке, чем способ, которым код низкого уровня может совершить переход на основе арифметическое значение, которое гораздо более разумно, когда это результат компиляции, а не что-то, что должно быть сохранено вручную. Реализации Устройства Даффа, в частности, хорошо подходят для эквивалентного машинного кода или IL, потому что каждый блок инструкций часто будет одинаковой длины без необходимости добавления nop
наполнителей.
† Устройство Даффа снова появляется здесь, как разумное исключение. Тот факт, что с этим и аналогичными шаблонами происходит повторение операций, делает использование сквозного перехода относительно ясным даже без явного комментария на этот счет.