Evitando o vodu `goto`?


47

Eu tenho uma switchestrutura que tem vários casos para lidar. O switchopera sobre um enumque coloca a questão do código duplicado através de valores combinados:

// 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.
}

Atualmente, a switchestrutura lida com cada valor separadamente:

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

Você entendeu a idéia. Atualmente, eu tenho isso dividido em uma ifestrutura empilhada para manipular combinações com uma única linha:

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

Isso coloca a questão de avaliações lógicas incrivelmente longas, que são confusas de ler e difíceis de manter. Depois de refatorá-lo, comecei a pensar em alternativas e a pensar em uma switchestrutura com repercussão entre os casos.

Eu tenho que usar um gotonesse caso, pois C#não permite falhas. No entanto, evita cadeias lógicas incrivelmente longas, mesmo que salte pela switchestrutura e ainda gera duplicação de código.

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

Isso ainda não é uma solução suficientemente limpa e há um estigma associado à gotopalavra - chave que eu gostaria de evitar. Tenho certeza de que deve haver uma maneira melhor de limpar isso.


Minha pergunta

Existe uma maneira melhor de lidar com esse caso específico sem afetar a legibilidade e a manutenção?


28
parece que você quer ter um grande debate. Mas você precisará de um exemplo melhor de um bom caso para usar o goto. flag enum resolve esse problema de maneira ordenada, mesmo que o seu exemplo de instrução if não fosse aceitável e pareça bom para mim
Ewan

5
@ Ewan Ninguém usa o goto. Quando foi a última vez que você olhou para o código-fonte do kernel Linux?
John Douma 22/01

6
Você usa gotoquando a estrutura de alto nível não existe no seu idioma. Às vezes eu gostaria que houvesse um fallthru; palavra-chave para se livrar desse uso específico de, gotomas tudo bem.
Joshua

25
Em termos gerais, se você sentir a necessidade de usar um gotoem uma linguagem de alto nível como C #, provavelmente ignorou muitas outras (e melhores) alternativas de design e / ou implementação. Altamente desencorajado.
code_dredd 22/01

11
Usar "goto case" em C # é diferente de usar um "goto" mais geral, porque é assim que você realiza explicações explícitas.
Hammerite 22/01

Respostas:


175

Acho o código difícil de ler com as gotoinstruções. Eu recomendaria estruturar o seu de maneira enumdiferente. Por exemplo, se você enumfosse um campo de bits em que cada bit representasse uma das opções, poderia ser assim:

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

O atributo Flags indica ao compilador que você está configurando valores que não se sobrepõem. O código que chama esse código pode definir o bit apropriado. Você pode fazer algo assim para deixar claro o que está acontecendo:

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

Isso requer o código configurado myEnumpara definir os campos de bits corretamente e marcado com o atributo Flags. Mas você pode fazer isso alterando os valores das enumerações no seu exemplo para:

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

Ao escrever um número no formulário 0bxxxx, você o especifica em binário. Assim, você pode ver que definimos o bit 1, 2 ou 3 (bem, tecnicamente 0, 1 ou 2, mas você entendeu). Você também pode nomear combinações usando um OR bit a bit, se as combinações puderem ser frequentemente definidas juntas.


5
Ele aparece assim ! Mas deve ser C # 7.0 ou posterior, eu acho.
user1118321

31
De qualquer forma, também se poderia fazê-lo como este: public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Não há necessidade de insistir no C # 7 +.
Deduplicator

8
Você pode usar o Enum.HasFlag
2010

13
O [Flags]atributo não sinaliza nada para o compilador. É por isso que você ainda tem que declarar explicitamente os valores enum como potências de 2.
Joe Sewell

19
@UKMonkey Eu discordo veementemente. HasFlagé um termo descritivo e explícito para a operação que está sendo executada e abstrai a implementação da funcionalidade. Usar &porque é comum em outros idiomas não faz mais sentido do que usar um bytetipo em vez de declarar um enum.
BJ Myers

136

Na OMI, a raiz do problema é que esse pedaço de código nem deveria existir.

Aparentemente, você tem três condições independentes e três ações independentes a serem executadas se essas condições forem verdadeiras. Então, por que tudo isso está sendo canalizado em um pedaço de código que precisa de três sinalizadores booleanos para dizer o que fazer (se você os obscurece ou não em uma enumeração) e faz alguma combinação de três coisas independentes? O princípio da responsabilidade única parece ter um dia de folga aqui.

Faça as chamadas para as três funções em que pertencem (ou seja, onde você descobre a necessidade de executar as ações) e consigne o código nesses exemplos à lixeira.

Se houvesse dez sinalizadores e ações e não três, você estenderia esse tipo de código para lidar com 1024 combinações diferentes? Espero que não! Se 1024 é demais, 8 também é demais, pelo mesmo motivo.


10
De fato, existem muitas APIs bem projetadas que possuem todos os tipos de sinalizadores, opções e vários outros parâmetros. Alguns podem ser repassados, alguns têm efeitos diretos e outros podem ser ignorados, sem interromper o SRP. Enfim, a função pode até estar decodificando alguma entrada externa, quem sabe? Além disso, a reductio ad absurdum geralmente leva a resultados absurdos, especialmente se o ponto de partida não for tão sólido assim.
Deduplicator 22/01

9
Votou com esta resposta. Se você quer apenas discutir o GOTO, é uma pergunta diferente da que você fez. Com base nas evidências apresentadas, você tem três requisitos disjuntos, que não devem ser agregados. Do ponto de vista do SE, afirmo que o alephzero's é a resposta correta.

8
@Duplicador Uma olhada nessa confusão de algumas, mas nem todas as combinações de 1,2 e 3 mostra que essa não é uma "API bem projetada".
user949300 22/01

4
Tanto isso. As outras respostas realmente não melhoram o que está em questão. Esse código precisa ser reescrito. Não há nada errado em escrever mais funções.
only_pro 22/01

3
Adoro esta resposta, especialmente "Se 1024 é demais, 8 também é demais". Mas é essencialmente "usar polimorfismo", não é? E minha resposta (mais fraca) está ficando muito ruim por dizer isso. Posso contratar sua pessoa de relações públicas? :-)
user949300 23/01

29

Nunca use gotos é um dos conceitos de "mentiras para crianças" da ciência da computação. É o conselho certo 99% do tempo, e os horários em que não são tão raros e especializados que são muito melhores para todos se forem explicados aos novos codificadores como "não os use".

Então, quando eles devem ser usados? Existem alguns cenários , mas o básico que você parece estar enfrentando é: quando você está codificando uma máquina de estado . Se não há expressão melhor organizada e estruturada do seu algoritmo do que uma máquina de estado, sua expressão natural no código envolve ramificações não estruturadas, e não há muito que possa ser feito sobre o que não pode ser discutido na estrutura do máquina de estado em si mais obscura, em vez de menos.

Os escritores de compiladores sabem disso, e é por isso que o código fonte da maioria dos compiladores que implementam analisadores LALR * contém gotos. No entanto, poucas pessoas realmente codificam seus próprios analisadores e analisadores lexicais.

* - No IIRC, é possível implementar gramáticas LALL de descida totalmente recursiva sem recorrer a tabelas de salto ou outras instruções de controle não estruturadas; portanto, se você é realmente anti-goto, essa é uma saída.


Agora, a próxima pergunta é: "Este exemplo é um desses casos?"

O que estou vendo é que você tem três próximos estados possíveis possíveis, dependendo do processamento do estado atual. Como um deles ("padrão") é apenas uma linha de código, tecnicamente você pode se livrar dele aderindo a essa linha de código no final dos estados aos quais se aplica. Isso reduziria você a 2 possíveis próximos estados.

Um dos restantes ("Três") é ramificado apenas de um lugar que eu posso ver. Então você pode se livrar da mesma maneira. Você terminaria com um código assim:

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

No entanto, novamente este foi um exemplo de brinquedo que você forneceu. Nos casos em que "padrão" possui uma quantidade não trivial de código, "três" é transferido para vários estados ou (o mais importante) é provável que uma manutenção adicional inclua ou complique os estados , seria melhor você honestamente usando gotos (e talvez até se livrando da estrutura enum-case que oculta a natureza das máquinas de estado das coisas, a menos que haja uma boa razão para que ela permaneça).


2
@PerpetualJ - Bem, certamente estou feliz que você encontrou algo mais limpo. Ainda pode ser interessante, se essa é realmente a sua situação, experimentar com os gotos (sem caso ou enum. Apenas rotulados como blocos e gotos) e ver como eles se comparam na legibilidade. Dito isto, a opção "goto" não precisa apenas ser mais legível, mas é mais legível para evitar argumentos e tentativas "consertadas" de outros desenvolvedores, então parece provável que você encontrou a melhor opção.
TED

4
Não acredito que você esteja "melhor usando gotos" se "é provável que mais manutenção adicione ou complique os estados". Você realmente está afirmando que o código espaguete é mais fácil de manter do que o código estruturado? Isso vai contra ~ 40 anos de prática.
user949300 22/01

5
@ user949300 - Não é prática contra a construção de máquinas de estado, não. Você pode simplesmente ler meu comentário anterior sobre esta resposta para obter um exemplo do mundo real. Se você tentar usar falhas de caso C, que impõem uma estrutura artificial (sua ordem linear) em um algoritmo de máquina de estado que não possui essa restrição inerentemente, você se encontrará com complexidade geometricamente crescente sempre que precisar reorganizar todas as suas pedidos para acomodar um novo estado. Se você apenas codifica estados e transições, como o algoritmo exige, isso não acontece. Novos estados são triviais para adicionar.
TED

8
@ user949300 - Novamente, "~ 40 anos de prática" na construção de compiladores mostra que os gotos são realmente a melhor maneira para partes desse domínio de problemas, e é por isso que, se você olhar o código-fonte do compilador, quase sempre encontrará gotos.
TED

3
@ user949300 O código espaguete não aparece apenas inerentemente ao usar funções ou convenções específicas. Pode aparecer em qualquer idioma, função ou convenção a qualquer momento. A causa é usar o idioma, função ou convenção em um aplicativo para o qual não foi planejado e fazer com que ele se incline para trás para retirá-lo. Sempre use a ferramenta certa para o trabalho e, sim, às vezes isso significa usar goto.
Abion47 23/01

26

A melhor resposta é usar polimorfismo .

Outra resposta, que, na IMO, torna as coisas mais claras e sem dúvida mais curtas :

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

goto é provavelmente a minha 58ª escolha aqui ...


5
+1 por sugerir polimorfismo, embora idealmente isso possa simplificar o código do cliente para apenas "Call ();", usando tell don't ask - para empurrar a lógica de decisão para longe do cliente consumidor e para o mecanismo e hierarquia de classes.
Erik Eidt 21/01

12
Proclamar qualquer coisa que a melhor vista não seja vista é certamente muito corajoso. Mas sem informações adicionais, sugiro um pouco de ceticismo e mantendo a mente aberta. Talvez sofra de explosão combinatória?
Deduplicator

Uau, acho que esta é a primeira vez que sou criticada por sugerir polimorfismo. :-)
user949300 22/01

25
Algumas pessoas, quando confrontadas com um problema, dizem: "Vou usar o polimorfismo". Agora eles têm dois problemas, e polimorfismo.
Lightness Races com Monica

8
Na verdade, fiz minha tese de mestrado sobre o uso do polimorfismo de tempo de execução para livrar-me dos gotos nas máquinas de estado dos compiladores. Isso é bastante factível, mas, desde então, descobri que na prática não vale o esforço na maioria dos casos. Ele requer uma tonelada de código de configuração OO, e é claro que tudo pode acabar com bugs (e que esta resposta omite).
TED

10

Por que não isso:

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

OK, eu concordo, isso é uma tolice (eu não sou um desenvolvedor de C #, desculpe-me pelo código), mas do ponto de vista da eficiência, isso é necessário? Usar enumerações como índice de matriz é C # válido.


3
Sim. Outro exemplo de substituição de código por dados (geralmente desejável, por velocidade, estrutura e capacidade de manutenção).
Peter - Restabelece Monica

2
Essa é uma excelente ideia para a edição mais recente da pergunta, sem dúvida. E pode ser usado para ir do primeiro enum (uma gama densa de valores) às bandeiras de ações mais ou menos independentes que precisam ser realizadas em geral.
Deduplicator

Eu não tenho acesso a um compilador C #, mas talvez o código possa ser mais seguro usando esse tipo de código int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };:?
Laurent Grégoire

7

Se você não pode ou não deseja usar sinalizadores, use uma função recursiva de cauda. No modo de liberação de 64 bits, o compilador produzirá um código muito semelhante à sua gotodeclaração. Você simplesmente não precisa lidar com isso.

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

Essa é uma solução única! +1 Acho que não preciso fazer dessa maneira, mas ótimo para futuros leitores!
PerpetualJ

2

A solução aceita é boa e é uma solução concreta para o seu problema. No entanto, gostaria de propor uma solução alternativa e mais abstrata.

Na minha experiência, o uso de enums para definir o fluxo da lógica é um cheiro de código, pois geralmente é um sinal de design de classe ruim.

Encontrei um exemplo real disso acontecendo no código em que trabalhei no ano passado. O desenvolvedor original criou uma classe única que importava e exportava a lógica e alternava entre as duas com base em uma enumeração. Agora, o código era semelhante e tinha algum código duplicado, mas era diferente o suficiente para tornar o código significativamente mais difícil de ler e praticamente impossível de testar. Acabei refatorando isso em duas classes separadas, o que simplificou as duas e, na verdade, me permitiu identificar e eliminar uma série de bugs não relatados.

Mais uma vez, devo declarar que o uso de enums para controlar o fluxo da lógica geralmente é um problema de design. No caso geral, as enums devem ser usadas principalmente para fornecer valores de tipo seguro e amigáveis ​​ao consumidor, onde os valores possíveis estão claramente definidos. Eles são mais usados ​​como uma propriedade (por exemplo, como um ID de coluna em uma tabela) do que como um mecanismo de controle lógico.

Vamos considerar o problema apresentado na pergunta. Eu realmente não sei o contexto aqui, ou o que esse enum representa. É desenhar cartas? Desenhando imagens? Desenhando sangue? A ordem é importante? Eu também não sei o quão importante é o desempenho. Se o desempenho ou a memória forem críticos, provavelmente esta solução não será a desejada.

De qualquer forma, vamos considerar o enum:

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

O que temos aqui são vários valores diferentes de enumerações que representam diferentes conceitos de negócios.

Em vez disso, o que poderíamos usar são abstrações para simplificar as coisas.

Vamos considerar a seguinte interface:

public interface IExample
{
  void Draw();
}

Podemos então implementar isso como uma classe abstrata:

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

Podemos ter uma classe concreta para representar o desenho um, dois e três (que, por razões de argumento, têm uma lógica diferente). Eles poderiam usar a classe base definida acima, mas suponho que o conceito DrawOne seja diferente do conceito representado pela enumeração:

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

E agora temos três classes separadas que podem ser compostas para fornecer a lógica para as outras classes.

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

Essa abordagem é muito mais detalhada. Mas tem vantagens.

Considere a seguinte classe, que contém um erro:

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

Quão mais simples é identificar a chamada drawThree.Draw () ausente? E se a ordem é importante, a ordem das chamadas também é muito fácil de ver e seguir.

Desvantagens para esta abordagem:

  • Cada uma das oito opções apresentadas requer uma classe separada
  • Isso usará mais memória
  • Isso tornará seu código superficialmente maior
  • Às vezes, essa abordagem não é possível, embora possa haver alguma variação nela.

Vantagens dessa abordagem:

  • Cada uma dessas classes é completamente testável; Porque
  • A complexidade ciclomática dos métodos de desenho é baixa (e, em teoria, eu poderia zombar das classes DrawOne, DrawTwo ou DrawThree, se necessário)
  • Os métodos de desenho são compreensíveis - um desenvolvedor não precisa amarrar o cérebro para descobrir o que o método faz
  • Os erros são fáceis de detectar e difíceis de escrever
  • As classes são compostas em classes de mais alto nível, o que significa que é fácil definir uma classe ThreeThreeThree

Considere essa abordagem (ou similar) sempre que sentir a necessidade de ter qualquer código de controle lógico complexo escrito em instruções de caso. Futuro, você será feliz que você fez.


Esta é uma resposta muito bem escrita e eu concordo inteiramente com a abordagem. No meu caso particular, algo tão detalhado seria exagerado até o grau infinito, considerando o código trivial por trás de cada um. Para colocá-lo em perspectiva, imagine que o código está simplesmente executando um Console.WriteLine (n). Isso é praticamente o equivalente ao que o código com o qual eu estava trabalhando estava fazendo. Em 90% de todos os outros casos, sua solução é definitivamente a melhor resposta ao seguir o POO.
PerpetualJ

Sim, ponto justo, essa abordagem não é útil em todas as situações. Eu queria colocá-lo aqui, porque é comum os desenvolvedores se fixarem em uma abordagem específica para resolver um problema e, às vezes, vale a pena dar um passo atrás e procurar alternativas.
Stephen

Eu concordo completamente com isso, é realmente a razão pela qual acabei aqui porque estava tentando resolver um problema de escalabilidade com algo que outro desenvolvedor escreveu por hábito.
PerpetualJ

0

Se você pretende usar uma opção aqui, seu código será realmente mais rápido se você lidar com cada caso separadamente

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

apenas uma única operação aritmética é executada em cada caso

Além disso, como outros já afirmaram, se você está pensando em usar o gotos, provavelmente deve repensar seu algoritmo (embora eu admita que a falta de casos em c # s possa ser um motivo para usar o goto). Veja o famoso artigo de Edgar Dijkstra "Ir para a declaração considerada prejudicial"


3
Eu diria que essa é uma ótima idéia para alguém que se depara com um assunto mais simples, por isso, obrigado por publicá-lo. No meu caso, não é tão simples.
PerpetualJ

Além disso, hoje não temos exatamente os problemas que Edgar teve no momento de escrever seu artigo; Hoje em dia, a maioria das pessoas nem sequer tem um motivo válido para odiar o ir além, pois isso dificulta a leitura do código. Bem, com toda a honestidade, se for abusado, sim, caso contrário, ele ficará preso ao método de contenção, para que você não possa criar código de espaguete. Por que fazer um esforço para solucionar um recurso de um idioma? Eu diria que o goto é arcaico e que você deve pensar em outras soluções em 99% dos casos de uso; mas se você precisar, é para isso que serve.
PerpetualJ

Isso não escalará bem quando houver muitos booleanos.
user949300 23/01

0

Para o seu exemplo em particular, uma vez que tudo o que você realmente queria da enumeração era uma Do / Do-not indicador para cada uma das etapas, a solução que reescreve suas três ifdeclarações é preferível a um switch, e é bom que você fez isso a resposta aceita .

Mas se você tivesse uma lógica mais complexa que não funcionasse de maneira tão limpa, ainda acho os gotos na switchdeclaração confusos. Prefiro ver algo assim:

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

Isso não é perfeito, mas acho que é melhor assim do que com os gotos. Se as seqüências de eventos são tão longas e se duplicam tanto que realmente não faz sentido especificar a sequência completa para cada caso, eu prefiro uma sub-rotina do que gotopara reduzir a duplicação de código.


0

Não tenho certeza se alguém realmente tem um motivo para odiar a palavra-chave goto nos dias de hoje. É definitivamente arcaico e não é necessário em 99% dos casos de uso, mas é um recurso do idioma por um motivo.

O motivo para odiar a gotopalavra-chave é código como

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Opa! Isso claramente não vai funcionar. A messagevariável não está definida nesse caminho de código. Portanto, o C # não passa isso. Mas isso pode estar implícito. Considerar

object.setMessage("Hello World!");

label:
object.display();

E suponha que displayentão tenha a WriteLinedeclaração.

Esse tipo de bug pode ser difícil de encontrar porque gotoobscurece o caminho do código.

Este é um exemplo simplificado. Suponha que um exemplo real não seria tão óbvio. Pode haver cinquenta linhas de código entre label:e o uso de message.

O idioma pode ajudar a corrigir isso, limitando como gotopode ser usado, descendo apenas dos blocos. Mas o C # gotonão é limitado assim. Pode pular o código. Além disso, se você pretende limitar goto, é melhor mudar o nome. Outros idiomas são usados breakpara descer dos blocos, com um número (de blocos para sair) ou um rótulo (em um dos blocos).

O conceito de gotoé uma instrução de linguagem de máquina de baixo nível. Mas toda a razão pela qual temos linguagens de nível superior é para nos limitarmos às abstrações de nível superior, por exemplo, escopo variável.

Tudo isso dito, se você usar o C # gotodentro de uma switchinstrução para ir de um caso para outro, é razoavelmente inofensivo. Cada caso já é um ponto de entrada. Ainda acho que chamá-lo de gotobobo nessa situação, pois confunde esse uso inofensivo gotocom formas mais perigosas. Eu preferiria que eles usassem algo assim continuepara isso. Mas, por alguma razão, eles se esqueceram de me perguntar antes de escreverem o idioma.


2
No C#idioma, você não pode pular sobre declarações de variáveis. Para prova, inicie um aplicativo de console e insira o código que você forneceu em sua postagem. Você receberá um erro de compilador no seu rótulo informando que o erro é o uso da variável local não atribuída 'message' . Nos idiomas em que isso é permitido, é uma preocupação válida, mas não no idioma C #.
PerpetualJ

0

Quando você tem tantas opções (e ainda mais, como você diz), talvez não seja código, mas dados.

Crie um dicionário de mapeamento de valores de enumeração para ações, expresso como funções ou como um tipo de enumeração mais simples, representando as ações. Em seguida, seu código pode ser resumido em uma simples pesquisa de dicionário, seguida pela chamada do valor da função ou pela alteração das opções simplificadas.


-7

Use um loop e a diferença entre interromper e continuar.

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);
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.