Переключить оператор падения в C #?

365

Падение заявления переключателя - одна из моих личных главных причин любить 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 должны быть объявлены вне функции: ну, они есть, это только пример.

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

Контроль не может перейти от одной метки кейса ('case 3:') к другой
Элемент управления не может перейти от одной метки кейса ('case 2:') к другой

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

Мэтью Шарли
источник

Ответы:

648

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

Падение 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;
}
Алекс Лиман
источник
121
Я думаю, что в данном конкретном случае goto не считается вредным.
Томас Оуэнс
78
Черт, я программировал на C # с первых дней существования 1.0, и до сих пор никогда не видел этого. Просто идет, чтобы показать, вы узнаете что-то новое каждый день.
Эрик Форбс
37
Это все хорошо, Эрик. Единственная причина, по которой я знал об этом, заключается в том, что я ботаник-теоретик, который читает спецификации ECMA-334 с помощью лупы.
Алекс Лиман
13
@Dancrumb: На момент написания этой функции C # еще не добавлял «мягких» ключевых слов (таких как «yield», «var», «from» и «select»), поэтому у них было три реальных варианта: 1 ) сделать «падение» жестким ключевым словом (вы не можете использовать его в качестве имени переменной), 2) написать код, необходимый для поддержки такого мягкого ключевого слова, 3) использовать уже зарезервированные ключевые слова. # 1 был большой проблемой для тех, кто переносил код; № 2 был довольно большой инженерной задачей, насколько я понимаю; и вариант, с которым они пошли, # 3 имел дополнительное преимущество: другие разработчики, читающие код после факта, могли узнать об этой функции из базовой концепции goto
Алекс Лиман
10
Все это говорит о новых / специальных ключевых словах для явного падения. Разве они не могли просто использовать ключевое слово continue? Другими словами. Вырваться из выключателя или перейти к следующему случаю (провалиться).
Таль Эвен-Тов
44

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

Джон Скит
источник
1
В этом и заключается разница между switch и if / elseif. Параметр Switch предназначен для проверки различных состояний одной переменной, тогда как if / elseif может использоваться для проверки любого количества подключенных объектов, но не обязательно одной или той же переменной.
Мэтью Шарли
2
Если бы это было для предотвращения случайного провала, я чувствую, что предупреждение компилятора было бы лучше. Точно так же, как у вас, если в вашем заявлении if есть задание:if (result = true) { }
Tal Even-Tov
3
@ TalEven-Tov: предупреждения компилятора действительно должны быть в тех случаях, когда вы почти всегда можете исправить код, чтобы он был лучше. Лично я предпочел бы неявное нарушение, так что это не было бы проблемой для начала, но это другой вопрос.
Джон Скит
26

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

switch(value)
{
    case 1:// this is still legal
    case 2:
}
Coincoin
источник
23
Я хорошо никогда не понимаю, почему это не «случай 1, 2:»
BCS
11
@ Дэвид Пфеффер: Да, и так case 1, 2:на языках, которые позволяют это. То, что я никогда не пойму, - это то, почему любой современный язык не хотел бы позволить это.
BCS
@BCS с оператором goto, несколько вариантов, разделенных запятыми, могут быть сложными для обработки?
penguat
@pengut: было бы точнее сказать, что case 1, 2:это один ярлык, но с несколькими именами. - FWIW, я думаю, что большинство языков, которые запрещают проваливаться, не являются специальным регистром с указателем «последовательные метки регистра», а рассматривают метки регистра как аннотацию к следующему оператору и требуют последнего оператора перед оператором, помеченным (один или больше) кейсы для прыжков.
БКС,
22

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

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

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

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

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

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

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

  1. В любом случае это происходит естественным образом: если вы строите таблицу переходов в набор инструкций, а одна из более ранних групп инструкций не содержит какой-либо переход или возврат, то выполнение просто естественным образом переходит в следующий пакет. Разрешение switchпровала было бы то, что «просто произошло бы», если бы вы превратили -using 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 # построен на уже нормальном способе справиться с одной ситуацией, когда мы хотим использовать более одного блока кода в a 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, служит для показа эквивалента на более высоком языке, чем способ, которым код низкого уровня может совершить переход на основе арифметическое значение, которое гораздо более разумно, когда это результат компиляции, а не что-то, что должно быть сохранено вручную. Реализации Устройства Даффа, в частности, хорошо подходят для эквивалентного машинного кода или IL, потому что каждый блок инструкций часто будет одинаковой длины без необходимости добавления nopнаполнителей.

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

Джон Ханна
источник
17

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

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

Kenny
источник
8

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

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

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}
Biri
источник
4

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

кругозор
источник
2
C # поддерживает Goto, но не Fallthrough? Ух ты. И это не только те. C # - единственный известный мне язык, который ведет себя таким образом.
Мэтью Шарли
Поначалу мне это не совсем нравилось, но «падение» - это действительно рецепт катастрофы (особенно среди младших программистов.) Как уже отмечали многие, C # по-прежнему допускает падение для пустых строк (что составляет большинство случаи.) "Кенни" опубликовал ссылку, которая подчеркивает элегантное использование Goto с switch-case.
Крендель
На мой взгляд, это не так уж важно. 99% времени я не хочу проваливаться, и в прошлом меня жгли жуки.
Кен
1
«Это не похоже на хорошее решение для меня» - жаль слышать это о вас, потому что это то goto case, для чего. Его преимущество перед прорывом в том, что оно явное. То, что некоторые люди здесь возражают против goto caseтого, чтобы просто показать, что они были внушены против "goto" без какого-либо понимания проблемы и неспособны думать самостоятельно. Когда Дейкстра написал «GOTO рассмотрено как вредное», он обращался к языкам, у которых не было никаких других средств изменения потока управления.
Джим Балтер
@JimBalter, а затем сколько людей, которые цитируют Дейкстру, цитируют Кнута, что «преждевременная оптимизация - корень всего зла», хотя эта цитата была, когда Кнут явно писал о том, насколько полезным gotoможет быть оптимизация кода?
Джон Ханна
1

Вы можете добиться провала как 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;
}
Дай Тран
источник
9
Если бы только кто-то опубликовал это два года назад!
0

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

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

Powerlord
источник
1
Я понимаю, что это поведение задокументировано, я хочу знать, ПОЧЕМУ так оно и есть, и любые альтернативы, чтобы получить старое поведение.
Мэтью Шарли
0

После каждого оператора case требуется оператор break или goto, даже если это случай по умолчанию.

Шилпа Гулати
источник
2
Если бы только кто-то опубликовал это два года назад!
1
@ Полди, это было забавно в первый раз ... Шилпа, тебе не нужно делать перерыв или переход для каждого случая, только для каждого случая со своим собственным кодом. У вас может быть несколько случаев, когда код делится просто отлично.
Maverick
0

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


источник
0

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

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

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

gm2008
источник
Спасибо, это помогло мне;)
CareTaker22
-1

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

BFree
источник
Это так неправильно. Смотрите другие ответы ... Эффект неявного падения может быть получен с помощью явного goto case . Это был намеренный, мудрый выбор дизайна дизайнерами C #.
Джим Балтер
-11

Вы забыли добавить «перерыв»; утверждение в случае 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");
}
Маркус
источник
2
Это приводит к неправильному выводу. Я оставил выключатели в дизайне. Вопрос в том, почему компилятор C # видит это как ошибку, когда почти ни в одном другом языке нет такого ограничения.
Мэтью Шарли
5
Какая потрясающая неспособность понять. И у вас было 5 лет, чтобы удалить это и до сих пор не сделали этого? Mindboggling.
Джим Балтер