Оператор Switch fallthrough в C#?

переключатель заявление fallthrough является одним из моих личных основных причин для любви 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

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

14 ответов


(копировать / вставить ответ я дал в другом месте)

падения через switch -cases можно достичь, не имея кода в 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;
}

"почему" -это избежать случайного провала, за что я благодарен. Это не редкий источник ошибок в 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 - есть некоторые полезные изменения, которые могут быть сделаны с точки зрения охвата и т. д. Возможно, более умный переключатель, который мог бы сделать сопоставление шаблонов и т. д., Был бы полезен, но это действительно меняется от переключателя до "проверить последовательность условий" - в этот момент, возможно, будет вызвано другое имя.


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

switch(value)
{
    case 1:// this is still legal
    case 2:
}

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

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

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

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

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

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

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

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

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

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

таким образом, тот факт, что 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. (Конечно, есть средства и способы избежать этого с разными conditionals, но это верно почти для всего, что связано с этим вопросом). Как таковой 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 заявления в качестве метки.

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


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

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


вы можете перейти к делу Лабель http://www.blackwasp.co.uk/CSharpGoto.aspx

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


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

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

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}

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


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


оператор перехода, такой как перерыв требуется после каждого блока case, включая последний блок, является ли он оператор case или значение по умолчанию заявление. За одним исключением (в отличие от оператор c++ switch), C# не поддерживать неявное сорвется с один ярлык дела к другому. Единица исключение - если оператор case имеет никакой код.

-- C# switch() документация


C# не поддерживает падение с помощью операторов switch/case. Не знаю почему, но на самом деле нет никакой поддержки. связь


вы можете достичь падения, как 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;
}

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


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

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

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


вы забыли добавить оператор "break;" в Случай 3. В случае 2 вы записали его в блок if. Поэтому попробуйте следующее:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}