Избегать "goto" вуду?

47

У меня есть switchструктура, которая должна обрабатывать несколько случаев. switchРаботает над enumкоторой ставит вопрос о дублировании кода через объединенные значения:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

В настоящее время switchструктура обрабатывает каждое значение отдельно:

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

Вы поняли идею там. В настоящее время у меня это разбито на составную ifструктуру для обработки комбинаций одной строкой:

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

Это ставит вопрос о невероятно длинных логических оценках, которые непонятны для чтения и которые трудно поддерживать. После рефакторинга это я начал думать об альтернативах и думал об идее switchструктуры с провалом между случаями.

Я должен использовать gotoв этом случае, так C#как не позволяет провалиться. Тем не менее, он предотвращает невероятно длинные логические цепочки, даже если он перепрыгивает в switchструктуре, и все равно вносит дублирование кода.

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

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


Мой вопрос

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

PerpetualJ
источник
28
Похоже, вы хотите провести большую дискуссию. Но вам понадобится лучший пример хорошего случая, чтобы использовать goto. flag enum аккуратно решает эту проблему, даже если ваш пример выражения if был неприемлем, и он мне
подходит
5
@ Иван Никто не использует goto. Когда вы в последний раз просматривали исходный код ядра Linux?
Джон Дума
6
Вы используете, gotoкогда структура высокого уровня не существует на вашем языке. Я иногда хотел бы, чтобы был fallthru; ключевое слово, чтобы избавиться от этого конкретного использования, gotoну да ладно.
Джошуа
25
В общих чертах, если вы чувствуете необходимость использовать язык gotoвысокого уровня, такой как C #, то вы, вероятно, упустили множество других (и лучших) вариантов проектирования и / или реализации. Очень обескуражен.
code_dredd
11
Использование "goto case" в C # отличается от использования более общего "goto", потому что это то, как вы выполняете явное падение.
Хаммерите

Ответы:

175

Я нахожу код трудно читать с gotoзаявлениями. Я бы порекомендовал структурировать ваш по- enumдругому. Например, если вы enumпредставляли собой битовое поле, где каждый бит представлял один из вариантов, это могло бы выглядеть так:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

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

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

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

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

Когда вы пишете число в форме 0bxxxx, вы указываете его в двоичном виде. Итак, вы можете видеть, что мы установили бит 1, 2 или 3 (ну, технически 0, 1 или 2, но вы поняли идею). Вы также можете называть комбинации с помощью побитового ИЛИ, если комбинации могут часто устанавливаться вместе.

user1118321
источник
5
Это кажется так ! Но это должно быть C # 7.0 или более поздняя версия.
user1118321
31
Во всяком случае, можно было бы также сделать это следующим образом : public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Не нужно настаивать на C # 7 +.
Дедупликатор
8
Вы можете использовать Enum.HasFlag
IMil
13
[Flags]Атрибут ничего компилятору не сигнализировать. Вот почему вы все еще должны явно объявить значения перечисления как степени 2.
Джо Сьюэлл
19
@UKMonkey Я категорически не согласен. HasFlagявляется описательным и явным термином для выполняемой операции и абстрагирует реализацию от функциональности. Использование, &потому что оно распространено в других языках, не имеет больше смысла, чем использование byteтипа вместо объявления enum.
Би Джей Майерс
136

IMO корень проблемы в том, что этот кусок кода даже не должен существовать.

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

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

Если бы было десять флагов и действий не три, вы бы расширили этот вид кода для обработки 1024 различных комбинаций? Надеюсь нет! Если 1024 слишком много, 8 тоже слишком много по той же причине.

alephzero
источник
10
На самом деле, есть много хорошо разработанных API, которые имеют всевозможные флаги, опции и другие параметры. Некоторые могут быть переданы, некоторые имеют прямые последствия, а третьи могут быть проигнорированы, не нарушая SRP. Во всяком случае, функция может даже декодировать какой-то внешний ввод, кто знает? Кроме того, сокращение до абсурда часто приводит к абсурдным результатам, особенно если отправная точка даже не настолько прочна.
Дедупликатор
9
Проголосовал этот ответ. Если вы просто хотите обсудить GOTO, это другой вопрос, чем тот, который вы задали. На основании представленных доказательств у вас есть три непересекающихся требования, которые не следует объединять. С точки зрения SE, я утверждаю, что alephzero - правильный ответ.
8
@Deduplicator Один взгляд на эту путаницу некоторых, но не всех комбинаций 1,2 и 3 показывает, что это не «хорошо разработанный API».
user949300
4
Вот так много. Другие ответы не улучшают то, что в вопросе. Этот код нуждается в переписывании. Нет ничего плохого в том, чтобы писать больше функций.
Извиниться и восстановить Монику
3
Мне нравится этот ответ, особенно «Если 1024 слишком много, 8 тоже слишком много». Но это по сути "использование полиморфизма", не так ли? И мой (более слабый) ответ получает много дерьма за это. Могу ли я нанять вашего пиарщика? :-)
user949300
29

Никогда не используйте gotos - это одна из концепций информатики "лгать детям" . Это правильный совет в 99% случаев, а иногда это не так редко и специализировано, что гораздо лучше для всех, если его просто объяснить новым кодировщикам как «не используйте их».

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

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

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


Теперь следующий вопрос: «Является ли этот пример одним из таких случаев?»

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

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

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

Однако, опять же, это был игрушечный пример, который вы предоставили. В случаях, когда «default» содержит нетривиальный объем кода, «три» переводится из нескольких состояний или (что наиболее важно) дальнейшее сопровождение может добавить или усложнить состояния , вам, честно говоря, будет лучше использование gotos (и, возможно, даже избавление от структуры enum-case, которая скрывает сущность конечного автомата вещей, если только для этого нет какой-либо веской причины).

ТЕД
источник
2
@PerpetualJ - Ну, я, конечно, рад, что ты нашел что-то чище. Возможно, все еще будет интересно, если это действительно ваша ситуация, попробовать это с gotos (без case или enum. Просто помеченные блоки и gotos) и посмотреть, как они сравниваются в удобочитаемости. При этом опция «goto» должна быть не только более удобочитаемой, но и более читаемой, чтобы не допускать аргументов и попыток «исправить» других разработчиков, так что, скорее всего, вы нашли лучший вариант.
TED
4
Я не верю, что вам "лучше использовать gotos", если "дальнейшее обслуживание может добавить или усложнить состояния". Вы действительно утверждаете, что спагетти-код легче поддерживать, чем структурированный код? Это противоречит ~ 40 годам практики.
user949300
5
@ user949300 - Это не практика против создания конечных автоматов. Вы можете просто прочитать мой предыдущий комментарий к этому ответу для примера из реальной жизни. Если вы попытаетесь использовать аварийные ситуации в C, которые накладывают искусственную структуру (их линейный порядок) на алгоритм конечного автомата, который по сути не имеет такого ограничения, вы будете сталкиваться с геометрически возрастающей сложностью каждый раз, когда вам придется переставлять все свои заказы для размещения нового государства. Если вы просто кодируете состояния и переходы, как требует алгоритм, этого не произойдет. Новые состояния тривиально добавить.
TED
8
@ user949300 - Опять же, "~ 40-летний опыт" создания компиляторов показывает, что gotos - действительно лучший способ для частей этой проблемной области, поэтому, если вы посмотрите на исходный код компилятора, вы почти всегда найдете gotos.
TED
3
@ user949300 Спагетти-код не просто присутствует при использовании определенных функций или соглашений. Он может появиться на любом языке, функции или соглашении в любое время. Причиной является использование указанного языка, функции или соглашения в приложении, для которого оно не было предназначено, и заставление его наклоняться назад, чтобы выполнить его. Всегда используйте правильный инструмент для работы, и да, иногда это означает использование goto.
Abion47
26

Лучший ответ - использовать полиморфизм .

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

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto вероятно, мой 58-й выбор здесь ...

user949300
источник
5
+1 за предложение полиморфизма, хотя в идеале это могло бы упростить клиентский код до просто «Call ();», используя Tell not ask - оттолкнуть логику принятия решений от потребляющего клиента в механизм и иерархию классов.
Эрик Эйдт
12
Провозглашать что-либо лучшим невидимым зрением, безусловно, очень смело Но без дополнительной информации я предлагаю немного скептицизма и открытости. Возможно, он страдает от комбинаторного взрыва?
Дедупликатор
Ух ты, я думаю, это первый раз, когда за меня отказались от предположения о полиморфизме. :-)
user949300
25
Некоторые люди, столкнувшись с проблемой, говорят: «Я просто использую полиморфизм». Теперь у них две проблемы и полиморфизм.
Легкость гонки с Моникой
8
Я фактически защитил магистерскую диссертацию об использовании полиморфизма во время выполнения, чтобы избавиться от gotos в конечных автоматах компиляторов. Это вполне выполнимо, но с тех пор я обнаружил, что на практике это не стоит усилий в большинстве случаев. Требуется тонна кода OO-setup, и все это, конечно, может привести к ошибкам (и которые этот ответ опускает).
TED
10

Почему бы не это:

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

Хорошо, я согласен, это хакерство (кстати, я не разработчик C #, поэтому извините за код), но с точки зрения эффективности это обязательно? Использование перечислений в качестве индекса массива является допустимым C #.

Лоран Грегуар
источник
3
Да. Еще один пример замены кода данными (что обычно желательно для скорости, структуры и удобства обслуживания).
Питер - Восстановить Монику
2
Это отличная идея для самого последнего выпуска вопроса, без сомнения. И его можно использовать для перехода от первого перечисления (плотного диапазона значений) к признакам более или менее независимых действий, которые должны быть выполнены в целом.
Дедупликатор
У меня нет доступа к # компилятор C, но , возможно , код может быть более безопасным , используя этот вид кода: int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Лоран Грегуар
7

Если вы не можете или не хотите использовать флаги, используйте хвостовую рекурсивную функцию. В 64-битном режиме выпуска компилятор будет генерировать код, который очень похож на ваш gotoоператор. Вы просто не должны иметь дело с этим.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}
Philipp
источник
Это уникальное решение! +1 Я не думаю, что мне нужно делать это таким образом, но отлично подходит для будущих читателей!
PerpetualJ
2

Принятое решение прекрасно и является конкретным решением вашей проблемы. Однако я хотел бы предложить альтернативное, более абстрактное решение.

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

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

Еще раз, я должен сказать, что использование перечислений для управления потоком логики часто является проблемой проектирования. В общем случае, Enums должны использоваться главным образом для обеспечения безопасных для типов, удобных для потребителя значений, где возможные значения четко определены. Их лучше использовать в качестве свойства (например, в качестве идентификатора столбца в таблице), чем в качестве механизма логического управления.

Давайте рассмотрим проблему, представленную в вопросе. Я действительно не знаю контекст здесь, или что представляет это перечисление. Это рисование карт? Рисование картин? Рисовать кровь? Важен ли порядок? Я также не знаю, насколько важна производительность. Если производительность или память имеют решающее значение, то это решение, вероятно, не будет тем, которое вам нужно.

В любом случае, давайте рассмотрим перечисление:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

Здесь мы имеем ряд различных значений enum, представляющих разные бизнес-концепции.

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

Давайте рассмотрим следующий интерфейс:

public interface IExample
{
  void Draw();
}

Затем мы можем реализовать это как абстрактный класс:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

У нас может быть конкретный класс для представления чертежей один, два и три (которые ради аргумента имеют разную логику). Они могут потенциально использовать базовый класс, определенный выше, но я предполагаю, что концепция DrawOne отличается от концепции, представленной enum:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

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

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

Этот подход гораздо более многословен. Но у этого есть преимущества.

Рассмотрим следующий класс, который содержит ошибку:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

Насколько проще обнаружить отсутствующий вызов drawThree.Draw ()? И, если порядок важен, порядок вызовов при розыгрыше также очень легко увидеть и проследить.

Недостатки этого подхода:

  • Каждый из восьми представленных вариантов требует отдельного класса
  • Это будет использовать больше памяти
  • Это сделает ваш код внешне больше
  • Иногда такой подход невозможен, хотя возможны некоторые отклонения от него.

Преимущества этого подхода:

  • Каждый из этих классов полностью тестируемый; потому что
  • Цикломатическая сложность методов рисования низкая (и теоретически я мог бы при необходимости высмеивать классы DrawOne, DrawTwo или DrawThree)
  • Методы розыгрыша понятны - разработчик не должен связывать свой мозг узлами, выясняя, что делает метод
  • Ошибки легко обнаружить и сложно написать
  • Классы объединяются в классы более высокого уровня, что означает, что определение класса ThreeThreeThree легко

Рассматривайте этот (или аналогичный) подход всякий раз, когда вы чувствуете необходимость иметь какой-либо сложный логический управляющий код, записанный в операторах case. Будущее, вы будете счастливы, что сделали.

Стивен
источник
Это очень хорошо написанный ответ, и я полностью согласен с подходом. В моем конкретном случае что-то такое многословное было бы излишним до бесконечности, учитывая тривиальный код каждого из них. Чтобы представить это в перспективе, представьте, что код просто выполняет Console.WriteLine (n). Это практически эквивалентно тому, что делал код, с которым я работал. В 90% всех других случаев ваше решение определенно является лучшим ответом при ООП.
PerpetualJ
Да, справедливо, этот подход не полезен в любой ситуации. Я хотел поместить это здесь, потому что разработчики часто зацикливаются на одном конкретном подходе к решению проблемы, а иногда стоит отступить назад и искать альтернативы.
Стивен
Я полностью согласен с этим, именно поэтому я оказался здесь, потому что пытался решить проблему масштабируемости с помощью чего-то, что другой разработчик написал по привычке.
PerpetualJ
0

Если вы намереваетесь использовать здесь переключатель, ваш код будет на самом деле быстрее, если вы будете обрабатывать каждый случай отдельно

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

в каждом случае выполняется только одна арифметическая операция

также, как другие заявили, если вы планируете использовать gotos, вам, вероятно, следует переосмыслить свой алгоритм (хотя я признаю, что отсутствие падений в C #, хотя и может быть причиной для использования goto). См. Знаменитую статью Эдгара Дейкстры «Перейти к заявлению, которое считается вредным»

CaptianObvious
источник
3
Я бы сказал, что это отличная идея для тех, кто столкнулся с более простым вопросом, поэтому спасибо за публикацию. В моем случае это не так просто.
PerpetualJ
Кроме того, у нас сегодня точно нет проблем, которые были у Эдгара во время написания его статьи; у большинства людей в настоящее время нет даже веской причины ненавидеть goto, кроме того, что код трудно читать. Что ж, если честно, если этим злоупотребляют, тогда да, в противном случае он попадает в ловушку содержащего метода, так что вы не сможете действительно создать код для спагетти. Зачем тратить усилия, чтобы обойти особенность языка? Я бы сказал, что goto является архаичным, и что вы должны думать о других решениях в 99% случаев; но если вам это нужно, вот для чего оно.
PerpetualJ
Это не будет хорошо масштабироваться, когда есть много логических значений.
user949300
0

Для вашего конкретного примера, поскольку все, что вы действительно хотели получить из перечисления, это указатель «делать / не делать» для каждого из шагов, решение, которое переписывает ваши три ifутверждения, предпочтительнее, чем «a» switch, и хорошо, что вы сделали его принятым ответом. ,

Но если у вас была какая-то более сложная логика, которая не сработала так чисто, то я все же нахожу gotoв switchутверждениях смущение. Я бы предпочел увидеть что-то вроде этого:

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

Это не идеально, но я думаю, что так лучше, чем с gotos. Если последовательности событий настолько длинные и настолько дублируют друг друга, что на самом деле не имеет смысла прописывать полную последовательность для каждого случая, я бы предпочел подпрограмму, а не gotoдля уменьшения дублирования кода.

Дэвид К
источник
0

Я не уверен, что у кого-то действительно есть причина ненавидеть ключевое слово goto в эти дни. Это определенно архаично и не требуется в 99% случаев, но это особенность языка по определенной причине.

Причиной ненависти этого gotoключевого слова является код

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

К сожалению! Это явно не сработает. messageПеременная не определена в этом пути кода. Так что C # не пройдет этого. Но это может быть неявным. Рассмотреть возможность

object.setMessage("Hello World!");

label:
object.display();

И предположим, что displayтогда в нем есть WriteLineутверждение.

Этот тип ошибки может быть трудно найти, потому что gotoзатеняет путь к коду.

Это упрощенный пример. Предположим, что реальный пример не будет столь же очевидным. Там может быть пятьдесят строк кода между label:и использованием message.

Язык может помочь исправить это, ограничивая gotoиспользование, только спускаясь из блоков. Но C # gotoне ограничен этим. Он может перепрыгивать через код. Кроме того, если вы собираетесь ограничить goto, это также хорошо, чтобы изменить имя. Другие языки используют breakдля спуска из блоков, либо с номером (из блоков для выхода), либо с меткой (на одном из блоков).

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

Все это говорит о том, что если вы используете C # gotoвнутри switchоператора для перехода от случая к случаю, это достаточно безопасно. Каждый случай уже является точкой входа. Я все еще думаю, что называть это gotoглупо в этой ситуации, так как это связывает это безвредное использование gotoс более опасными формами. Я бы предпочел, чтобы они использовали что-то подобное continueдля этого. Но по какой-то причине они забыли спросить меня, прежде чем они написали язык.

mdfst13
источник
2
На C#языке вы не можете перепрыгивать через объявления переменных. Для доказательства запустите консольное приложение и введите код, который вы указали в своем сообщении. Вы получите ошибку компилятора в вашей метке, утверждающую, что ошибка заключается в использовании неназначенной локальной переменной 'message' . На языках, где это разрешено, это актуально, но не на языке C #.
PerpetualJ
0

Когда у вас есть много вариантов (и даже больше, как вы говорите), тогда, возможно, это не код, а данные.

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

Alexis
источник
-7

Используйте цикл и разницу между разрывом и продолжением.

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
QuentinUK
источник