web-dev-qa-db-ja.com

`goto`ブードゥーを回避しますか?

処理するいくつかのケースがあるswitch構造があります。 switchenum上で動作し、結合された値によって重複コードの問題を引き起こします。

// 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構造に分解して、代わりに1行の組み合わせを処理しています。

// 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構造のアイデアについて考えました。

C#はフォールスルーを許可しないため、その場合はgotoを使用する必要があります。ただし、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キーワードに関連する汚名があります。私はこれをきれいにするより良い方法があるはずだと確信しています。


私の質問

読みやすさと保守性に影響を与えずにこの特定のケースを処理するより良い方法はありますか?

47

gotoステートメントではコードが読みにくいと思います。 enumを別の方法で構造化することをお勧めします。たとえば、enumがビットフィールドであり、各ビットが選択肢の1つを表す場合、次のようになります。

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

これには、ビットフィールドを正しく設定し、Flags属性でマークするためにmyEnumを設定するコードが必要です。しかし、あなたはあなたの例の列挙型の値を次のように変更することでそれを行うことができます:

[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ですが、アイデアはわかります)。組み合わせが頻繁に一緒に設定される可能性がある場合は、ビット単位のORを使用して組み合わせに名前を付けることもできます。

174
user1118321

問題の根本的なIMOは、このコードが存在するべきではないということです。

明らかに、3つの独立した条件と、それらの条件に該当する場合に実行する3つの独立したアクションがあります。それで、何をすべきか(それらを列挙型に難読化するかどうかに関係なく)を伝えるために3つのブールフラグを必要とする1つのコードにまとめられ、3つの独立したものの組み合わせを行うのはなぜですか?単一責任の原則は、ここで休みをとっているようです。

が属する3つの関数(つまり、アクションを実行する必要があることがわかった場所)への呼び出しを配置し​​、これらの例のコードをごみ箱に委託します。

3つのフラグではなく10のフラグとアクションがある場合、この種類のコードを拡張して1024の異なる組み合わせを処理しますか?私は望みません!同じ理由で、1024が多すぎる場合、8も多すぎます。

136
alephzero

gotosを使用しないは、コンピュータサイエンスの概念 "lies to children" の1つです。それは99%の確率で適切なアドバイスであり、まれではないため、新しいコーダーに「使用しないでください」と説明すれば、誰にとってもはるかに優れています。

すべきの場合、それらは使用されますか? いくつかのシナリオ がありますが、ヒットしているように見える基本的なシナリオは次のとおりです:ステートマシンをコーディングしている場合。アルゴリズムの構造化された構造化された表現がステートマシンよりも優れていない場合、コード内のその自然な表現には非構造化ブランチが含まれ、間違いなく構造化されないものについて実行できることは多くありません。ステートマシン自体more少ないというよりは、あいまいです。

コンパイラの作成者はこれを知っているため、LALRパーサーを実装するほとんどのコンパイラのソースコードはこのためです* 後藤が含まれています。ただし、実際に自分の字句解析プログラムと構文解析プログラムを実際にコーディングする人はほとんどいません。

*-IIRC、ジャンプテーブルやその他の非構造化制御ステートメントに頼ることなく、LALL文法を完全に再帰的降下を実装することが可能であるため、真に反言する場合、これは1つの方法です。


次の質問は、「この例はそのようなケースの1つですか?」です。

私が見ているのは、現在の状態の処理に応じて、3つの異なる次の状態があることです。それらの1つ(「デフォルト」)は単なるコード行であるため、技術的には、適用される状態の最後にそのコード行を追加することで、コードを取り除くことができます。これにより、次の2つの状態が可能になります。

残りの1つ(「3つ」)は、私が見ることができる1つの場所から分岐しているだけです。したがって、同じ方法でそれを取り除くことができます。次のようなコードになります。

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

しかし、これもあなたが提供したおもちゃの例でした。 「デフォルト」に重要な量のコードが含まれている場合、「3」は複数の状態から(または最も重要な)に移行し、さらにメンテナンスが追加または複雑になる可能性があります状態、あなたは正直なところ、gotoを使用する方が良いでしょう(そして、何らかの理由がある場合を除いて、状態マシンの性質を隠すenum-case構造を取り除くことさえ可能です)滞在)。

29
T.E.D.

最良の答えは多態性を使用です。

IMOは、ifの要素をより明確にしますそして間違いなくより短い

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

gotoはおそらく私の58番目の選択肢です...

26
user949300

これはなぜですか:

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、私は同意します。これはハックです(私はC#開発者ではないので、コードを失礼します)。しかし、効率の観点から、これは必要ですか? 列挙型を配列インデックスとして使用 は有効なC#です。

10

フラグを使用できない、または使用したくない場合は、末尾再帰関数を使用します。 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;
   }
}
7
Philipp

受け入れられた解決策は問題なく、問題に対する具体的な解決策です。しかし、私は代替の、より抽象的なソリューションを提案したいと思います。

私の経験では、ロジックのフローを定義するための列挙型の使用は、多くの場合、クラス設計が不十分であることを示すコード臭です。

私は、昨年取り組んだコードでこれが起こっている実例に遭遇しました。元の開発者は、インポートとエクスポートの両方のロジックを実行する単一のクラスを作成し、列挙型に基づいて2つを切り替えました。現在、コードは類似しており、重複するコードがいくつかありましたが、コードが非常に異なり、コードの読み取りが著しく困難になり、テストが事実上不可能になりました。私はそれを2つの別々のクラスにリファクタリングすることになりました。これにより、両方が簡略化され、実際には、報告されていない多数のバグを特定して排除できました。

繰り返しますが、列挙型を使用してロジックのフローを制御することは、しばしば設計上の問題であると述べなければなりません。一般的なケースでは、Enumは主に、可能な値が明確に定義されているタイプセーフで消費者に優しい値を提供するために使用する必要があります。これらは、ロジック制御メカニズムとしてよりも、プロパティ(たとえば、テーブルの列ID)として使用する方が適切です。

質問で提示された問題を考えてみましょう。ここでのコンテキストや、この列挙型が何を表しているのか本当にわかりません。カードを引くのですか?絵を描く?血を引く?順序は重要ですか?また、パフォーマンスの重要性もわかりません。パフォーマンスやメモリが重要な場合、このソリューションはおそらく望んでいるものではありません。

いずれにせよ、列挙型を考えてみましょう:

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

ここにあるのは、さまざまなビジネス概念を表すいくつかの異なる列挙値です。

代わりに使用できるのは、物事を単純化するための抽象化です。

次のインターフェースについて考えてみましょう。

public interface IExample
{
  void Draw();
}

次に、これを抽象クラスとして実装できます。

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

図面1、2、3を表す具体的なクラスを持つことができます(議論のために、ロジックは異なります)。これらは上記で定義された基本クラスを使用する可能性がありますが、私はDrawOneの概念が列挙型で表される概念とは異なると想定しています:

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

そして今、私たちは他のクラスにロジックを提供するために構成されるかもしれない3つの別々のクラスを持っています。

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()呼び出しを見つけるのはどれほど簡単ですか?また、順序が重要な場合は、描画呼び出しの順序も簡単に確認および追跡できます。

このアプローチの欠点:

  • 提示された8つのオプションすべてに個別のクラスが必要です
  • これはより多くのメモリを使用します
  • これにより、コードが表面的に大きくなります
  • 時々このアプローチは不可能ですが、それに対するいくつかのバリエーションがあります

このアプローチの利点:

  • これらの各クラスは完全にテスト可能です。なぜなら
  • 描画メソッドの循環的複雑度は低いです(そして理論的には、必要に応じてDrawOne、DrawTwo、またはDrawThreeクラスをモックすることができます)
  • 描画メソッドは理解可能です-開発者は、メソッドの機能を理解するためのノットに頭を縛る必要はありません。
  • バグは見つけやすく、書くのが難しい
  • クラスはより高レベルのクラスに構成されます。つまり、ThreeThreeThreeクラスを定義するのは簡単です

ケース文に複雑なロジック制御コードを書き込む必要があると感じるときはいつでも、この(または同様の)アプローチを検討してください。今後、あなたはあなたが幸せになります。

2
Stephen

あなたがこれほど多くの選択肢を持っている場合(そしてあなたが言うようにさらに多くの選択肢がある場合)、おそらくそれはコードではなくデータです。

列挙値をアクションにマッピングするディクショナリを作成します。関数として、またはアクションを表すより単純な列挙型として表現されます。次に、コードを簡単な辞書ルックアップに要約してから、関数値を呼び出すか、簡略化された選択肢を切り替えることができます。

0
alexis

ここでスイッチを使用することに専念している場合、各ケースを個別に処理すると、コードは実際に高速になります

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

いずれの場合も、単一の算術演算のみが実行されます

また、gotoの使用を検討している場合は他の人が述べたように、おそらくアルゴリズムを再考する必要があります(ただし、C#で大文字と小文字が区別されないことがgotoを使用する理由になる可能性があります)。エドガーダイクストラの有名な論文「有害と見なされるGo Toステートメント」を参照してください。

0
CaptianObvious

特定の例では、列挙に本当に必要なのは各ステップのdo/do-notインジケーターだけだったので、3つのifステートメントを書き直すソリューションはswitchよりも望ましい、そしてあなたがそれを受け入れられた答えにしたのは良いことです。

しかし、notがうまく機能するより複雑なロジックがある場合でも、gotoステートメントのswitchsは混乱を招きます。私はむしろこのようなものを見たいです:

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
David K

最近、gotoキーワードを嫌う理由が本当にあるのかどうかはわかりません。これは間違いなく古風で、99%のユースケースでは必要ありませんが、それは理由による言語の機能です。

gotoキーワードを嫌う理由は、次のようなコードです

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

おっとっと!それは明らかにうまくいきません。 message変数がそのコードパスで定義されていません。したがって、C#はそれを渡しません。しかし、それは暗黙的かもしれません。検討する

object.setMessage("Hello World!");

label:
object.display();

そして、displayWriteLineステートメントが含まれていると仮定します。

gotoはコードパスを覆い隠すため、この種のバグを見つけるのは難しい場合があります。

これは簡単な例です。実際の例はそれほど明白ではないと仮定します。 label:messageの使用の間に50行のコードがある場合があります。

言語は、gotoの使用方法を制限し、ブロックから降りるだけでこれを修正するのに役立ちます。しかし、C#gotoはそのように制限されていません。コードを飛び越えることができます。さらに、gotoを制限する場合は、名前を変更することもできます。他の言語は、番号(終了するブロックの数)またはラベル(ブロックの1つ)のいずれかを使用して、breakを使用してブロックから降順します。

gotoの概念は、低レベルの機械語命令です。しかし、私たちがより高いレベルの言語を持っている理由は、私たちがより高いレベルの抽象化に制限されるためです。変数スコープ。

そうは言っても、gotoステートメント内でC#switchを使用してケース間をジャンプする場合、それは無害です。各ケースはすでにエントリポイントです。 gotoのこの無害な使用法をより危険な形式で覆い隠すので、それをgotoと呼ぶことはそのような状況ではばかげていると私はまだ思います。彼らはそのためにcontinueのようなものを使うことをむしろ望みます。しかし、何らかの理由で、彼らは言語を書く前に私に尋ねるのを忘れていました。

0
mdfst13