Переключить оператор падения в C #? - PullRequest
343 голосов
/ 06 октября 2008

Падение оператора Switch - одна из моих главных причин любить конструкции switch против if/else if. Пример приведен здесь:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Умные люди съеживаются, потому что string[] s должны быть объявлены вне функции: ну, они есть, это только пример.

Компилятор завершается с ошибкой:

Control cannot fall through from one case label ('case 3:') to another
Control cannot fall through from one case label ('case 2:') to another

Почему? И есть ли способ получить такое поведение без трех if с?

Ответы [ 14 ]

608 голосов
/ 06 октября 2008

(Копирование / вставка ответа , который я предоставил в другом месте )

Падение через switch - case с может быть достигнуто без кода в case (см. case 0) или с помощью специальных goto case (см. case 1) или goto default ( см case 2) формы:

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}
40 голосов
/ 06 октября 2008

«Почему» - избегать случайных провалов, за что я благодарен. Это не редкий источник ошибок в C и Java.

Обходной путь должен использовать goto, например,

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

Общий дизайн переключателя / корпуса несколько неудачен, на мой взгляд. Он застрял слишком близко к C - есть некоторые полезные изменения, которые могут быть сделаны с точки зрения области видимости и т. Д. Возможно, был бы полезен более умный переключатель, который мог бы выполнять сопоставление с образцом и т. Д., Но это действительно переключение с переключателя на «проверку последовательности условий» - в этот момент может потребоваться другое имя.

26 голосов
/ 06 октября 2008

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

switch(value)
{
    case 1:// this is still legal
    case 2:
}
20 голосов
/ 09 декабря 2013

Чтобы добавить здесь ответы, я думаю, что стоит рассмотреть противоположный вопрос в связи с этим, а именно. почему C позволил провалиться в первую очередь?

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

  1. Предоставьте инструкции компьютеру.
  2. Оставьте запись о намерениях программиста.

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

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

Теперь, switch всегда можно было скомпилировать, преобразовав его в эквивалентную цепочку блоков if-else или аналогичных, но он был спроектирован как позволяющий компилировать в определенный общий шаблон сборки, где каждый принимает значение, вычисляет смещение из него (либо путем поиска таблицы, проиндексированной по совершенному хешу значения, либо по фактической арифметике значения *). На данный момент стоит отметить, что сегодня компиляция C # иногда превращает switch в эквивалент if-else, а иногда использует подход перехода на основе хеша (и аналогично с C, C ++ и другими языками с сопоставимым синтаксисом).

В этом случае есть две веские причины для разрешения провала:

  1. В любом случае это происходит естественным образом: если вы строите таблицу переходов в набор инструкций, а одна из более ранних групп инструкций не содержит какой-либо переход или возврат, то выполнение просто естественным образом переходит в следующая партия. Разрешение провала было тем, что «просто произошло бы», если бы вы превратили switch -использующий C в таблицу переходов - используя машинный код.

  2. Кодеры, которые писали на ассемблере, уже были использованы для эквивалента: при написании таблицы переходов вручную на ассемблере им пришлось бы учитывать, закончится ли данный блок кода возвратом, переходом за пределы стол, или просто перейдите к следующему блоку. Таким образом, добавление в кодер явного break, когда это было необходимо, было «естественным» и для кодера.

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

Четыре десятилетия спустя, хотя, по нескольким причинам, все не совсем так:

  1. Кодеры на C сегодня могут иметь небольшой опыт или вообще не иметь опыта сборки. Кодеры во многих других языках стиля C даже менее вероятно (особенно в Javascript!). Любая концепция «к чему люди привыкли со сборки» больше не актуальна.
  2. Улучшения в оптимизации означают, что вероятность того, что switch либо превратится в if-else, поскольку считается, что этот подход наиболее эффективен, либо превращается в особенно эзотерический вариант подхода с использованием таблицы переходов, выше. Соотношение между подходами более высокого и более низкого уровня не такое сильное, как раньше.
  3. Опыт показывает, что провал - это, как правило, случай с меньшинством, а не норма (исследование компилятора Sun показало, что 3% switch блоков используют провал, отличный от нескольких меток в одном блоке, и это Считалось, что вариант использования здесь означал, что эти 3% были на самом деле намного выше, чем обычно). Таким образом, изучаемый язык делает необычное более легким в обращении, чем обычный.
  4. Опыт показывает, что провалы, как правило, являются источником проблем как в тех случаях, когда это происходит случайно, так и в тех случаях, когда кто-то, обслуживающий код, пропускает правильный провал. Последнее является тонким дополнением к ошибкам, связанным с провалом, потому что даже если ваш код не содержит ошибок, ваш провал может все еще вызывать проблемы.

В связи с этими двумя последними пунктами рассмотрим следующую цитату из текущей редакции 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 #).

Таким образом, в итоге:

  1. C # больше не относится к неоптимизированному выводу компилятора так же непосредственно, как код C сделал 40 лет назад (и в наши дни C), что делает одно из вдохновляющих провалов несущественным.
  2. C # остается совместимым с C не только неявным break, для более легкого изучения языка теми, кто знаком с подобными языками, и более легким переносом.
  3. C # удаляет возможный источник ошибок или неправильно понятого кода, который был задокументирован как вызывающий проблемы в течение последних четырех десятилетий.
  4. C # делает существующую передовую практику с C (провал документа) осуществимой компилятором.
  5. C # делает необычный случай с более явным кодом, обычный случай с кодом, который просто пишет автоматически.
  6. C # использует тот же подход на основе goto для попадания в один и тот же блок из разных меток case, который используется в C. Он просто обобщает его на некоторые другие случаи.
  7. C # делает такой подход на основе goto более удобным и понятным, чем в C, позволяя операторам case выступать в качестве меток.

В целом, довольно разумное дизайнерское решение


* Некоторые формы Бейсика позволили бы делать подобные GOTO (x AND 7) * 50 + 240, который, хотя и хрупок, и, следовательно, особенно убедителен для запрета goto, служит для того, чтобы показать более высокий языковой эквивалент вида, который ниже -уровневый код может сделать переход, основанный на арифметике, на значении, что гораздо разумнее, когда это результат компиляции, а не что-то, что должно поддерживаться вручную. В частности, реализации устройства Даффа хорошо подходят для эквивалентного машинного кода или IL, потому что каждый блок инструкций часто будет одинаковой длины без необходимости добавления nop заполнителей.

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

15 голосов
/ 06 октября 2008

Вы можете «перейти к метке дела» http://www.blackwasp.co.uk/CSharpGoto.aspx

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

8 голосов
/ 06 октября 2008

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

Его можно использовать только в том случае, если в части кейса нет операторов, например:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}
4 голосов
/ 06 октября 2008

Они изменили поведение оператора switch (из C / Java / C ++) для c #. Я предполагаю, что причина была в том, что люди забыли о провале и были вызваны ошибки. В одной книге, которую я прочитал, говорится, что для симуляции используется goto, но это не похоже на хорошее решение для меня.

0 голосов
/ 07 октября 2014

переключатель (C # Reference) говорит

C # требует окончания секций переключателя, включая последнюю,

Так что вам также нужно добавить break; в ваш раздел default, иначе все равно будет ошибка компилятора.

0 голосов
/ 15 февраля 2014

Просто небольшое замечание, чтобы добавить, что компилятор для Xamarin на самом деле ошибся и допускает падение. Предположительно это было исправлено, но не было выпущено. Обнаружил это в каком-то коде, который на самом деле проваливался, и компилятор не жаловался.

0 голосов
/ 29 июля 2010

Вы можете добиться провала как c ++ с помощью ключевого слова goto.

EX:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}
...