web-dev-qa-db-ja.com

ネストされたループから抜け出す

別のループ内にネストされたforループがある場合、両方のループ(内側と外側)から可能な限り迅速に効率的に抜け出すにはどうすればよいですか?

ブール値を使用してから別のメソッドに移動する必要はありませんが、外側のループの後にコードの最初の行を実行するだけです。

これをすばやく簡単に行う方法は何ですか?

ありがとう


私は、例外は安くない/真に例外的な状況などでのみスローされるべきだと考えていました。したがって、このソリューションはパフォーマンスの観点からは良いとは思いません。

.NET(anonメソッド)の新しい機能を利用して、非常に基本的なことを行うのは正しいとは思いません。

そのため、tvon(申し訳ありませんが完全なユーザー名を綴ることはできません!)にはニースのソリューションがあります。

Marc:anonメソッドの素晴らしい使用法。これも素晴らしいですが、anonメソッドをサポートするバージョンの.NET/C#を使用しない仕事に就く可能性があるため、従来のアプローチも知っておく必要があります。

193
GurdeepS

まあ、goto、しかしそれは見苦しく、常に可能とは限りません。ループをメソッド(または非メソッド)に配置し、returnを使用してメインコードに戻ることもできます。

    // goto
    for (int i = 0; i < 100; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            goto Foo; // yeuck!
        }
    }
Foo:
    Console.WriteLine("Hi");

対:

// anon-method
Action work = delegate
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits anon-method
        }
    }
};
work(); // execute anon-method
Console.WriteLine("Hi");

C#7では「ローカル関数」を取得する必要があることに注意してください(構文tbdなど)。これは次のような動作をすることを意味します。

// local function (declared **inside** another method)
void Work()
{
    for (int x = 0; x < 100; x++)
    {
        for (int y = 0; y < 100; y++)
        {
            return; // exits local function
        }
    }
};
Work(); // execute local function
Console.WriteLine("Hi");
189
Marc Gravell

Cでよく使用されるアプローチのC#適応-ループ条件の外側の外側のループ変数の値を設定します(つまり、int変数INT_MAX -1を使用するforループが適切な選択であることがよくあります)。

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        if (exit_condition)
        {
            // cause the outer loop to break:
            // use i = INT_MAX - 1; otherwise i++ == INT_MIN < 100 and loop will continue 
            i = int.MaxValue - 1;
            Console.WriteLine("Hi");
            // break the inner loop
            break;
        }
    }
    // if you have code in outer loop it will execute after break from inner loop    
}

コードの注記にあるように、breakは外側のループの次の反復に魔法のようにジャンプしません。したがって、内側のループの外側にコードがある場合、このアプローチにはさらにチェックが必要です。そのような場合は他のソリューションを検討してください。

このアプローチは、forおよびwhileループで機能しますが、foreachでは機能しません。 foreachの場合、非表示の列挙子にコードでアクセスできないため、変更できません(そして、IEnumeratorに "MoveToEnd"メソッドがない場合でも)。

インラインコメントの著者への謝辞:
i = INT_MAX - 1による提案 メタ
for/foreachygoe によるコメント。
適切なIntMax by jmbpiano
blizpasta による内部ループ後のコードに関する注意

93

この解決策はC#には適用されません

他の言語でこの質問を見つけた人のために、Javascript、Java、およびDはラベル付きブレークと継続を許可します

outer: while(fn1())
{
   while(fn2())
   {
     if(fn3()) continue outer;
     if(fn4()) break outer;
   }
}
39
BCS

外側のループに適切なガードを使用してください。ブレークする前に、内側のループにガードを設定します。

bool exitedInner = false;

for (int i = 0; i < N && !exitedInner; ++i) {

    .... some outer loop stuff

    for (int j = 0; j < M; ++j) {

        if (sometest) {
            exitedInner = true;
            break;
        }
    }
    if (!exitedInner) {
       ... more outer loop stuff
    }
}

さらに良いことに、内側のループをメソッドに抽象化し、falseが返されたら外側のループを終了します。

for (int i = 0; i < N; ++i) {

    .... some outer loop stuff

    if (!doInner(i, N, M)) {
       break;
    }

    ... more outer loop stuff
}
26
tvanfosson

これについては引用しないでください。ただし、MSDNで提案されているように goto を使用できます。両方のループの各反復でチェックされるフラグを含めるなど、他のソリューションがあります。最後に、問題に対する本当に重い解決策として例外を使用できます。

後藤:

for ( int i = 0; i < 10; ++i ) {
   for ( int j = 0; j < 10; ++j ) {
      // code
      if ( break_condition ) goto End;
      // more code
   }
}
End: ;

調子:

bool exit = false;
for ( int i = 0; i < 10 && !exit; ++i ) {
   for ( int j = 0; j < 10 && !exit; ++j ) {
      // code
      if ( break_condition ) {
         exit = true;
         break; // or continue
      }
      // more code
   }
}

例外:

try {
    for ( int i = 0; i < 10 && !exit; ++i ) {
       for ( int j = 0; j < 10 && !exit; ++j ) {
          // code
          if ( break_condition ) {
             throw new Exception()
          }
          // more code
       }
    }
catch ( Exception e ) {}

ネストされたforループをプライベートメソッドにリファクタリングすることは可能ですか?そうすれば、ループから抜けるためにメソッドから単に「戻る」ことができます。

15
NoizWaves

gotoステートメントを嫌う人が多いように思えるので、これを少し修正する必要があると感じました。

gotoについて人々が持っている「感情」は、最終的にはコードの理解と(潜在的なパフォーマンスへの影響についての)(誤解)に要約されると信じています。したがって、質問に答える前に、最初にコンパイル方法の詳細について説明します。

ご存じのとおり、C#はILにコンパイルされ、SSAコンパイラを使用してアセンブラにコンパイルされます。これがどのように機能するかについて少し洞察し、質問自体に答えてみます。

C#からILへ

まず、C#コードが必要です。簡単に始めましょう:

foreach (var item in array)
{
    // ... 
    break;
    // ...
}

この手順を段階的に実行して、内部で何が起こるのかを説明します。

最初の翻訳:foreachから同等のforループへ(注:ここでは配列を使用しています。IDisposableの詳細を取得したくないためです。この場合、IEnumerableも使用する必要があります)。

for (int i=0; i<array.Length; ++i)
{
    var item = array[i];
    // ...
    break;
    // ...
}

2番目の翻訳:forおよびbreakは、より簡単な同等のものに翻訳されます。

int i=0;
while (i < array.Length)
{
    var item = array[i];
    // ...
    break;
    // ...
    ++i;
}

3番目の翻訳(これはILコードに相当):breakwhileをブランチに変更します:

    int i=0; // for initialization

startLoop:
    if (i >= array.Length) // for condition
    {
        goto exitLoop;
    }
    var item = array[i];
    // ...
    goto exitLoop; // break
    // ...
    ++i;           // for post-expression
    goto startLoop; 

コンパイラーはこれらのことを単一のステップで行いますが、プロセスの洞察を提供します。 C#プログラムから進化したILコードは、最後のC#コードのリテラル変換です。ここで確認できます: https://dotnetfiddle.net/QaiLRz (「view IL」をクリック)

ここで確認したことの1つは、プロセス中にコードがより複雑になることです。これを観察する最も簡単な方法は、同じことを確認するためにより多くのコードが必要になるという事実です。また、foreachforwhile、およびbreakは実際にはgotoの省略形であると主張する場合もありますが、これは部分的には正しいことです。

ILからアセンブラーへ

.NET JITコンパイラはSSAコンパイラです。ここでは、SSAフォームの詳細と最適化コンパイラの作成方法については詳しく説明しませんが、多すぎますが、何が起こるかについての基本的な理解を与えることができます。理解を深めるために、コンパイラの最適化について読み始めることをお勧めします(簡単な紹介としてこの本が好きです: http://ssabook.gforge.inria.fr/latest/book.pdf )およびLLVM(llvm.org)。

すべての最適化コンパイラは、コードがeasyであり、予測可能パターンに従うという事実に依存しています。 FORループの場合、グラフ理論を使用して分岐を分析し、分岐内のcycliなどを最適化します(逆方向の分岐など)。

ただし、ループを実装するための前方分岐があります。ご想像のとおり、これは実際、次のようにJITが修正する最初のステップの1つです。

    int i=0; // for initialization

    if (i >= array.Length) // for condition
    {
        goto endOfLoop;
    }

startLoop:
    var item = array[i];
    // ...
    goto endOfLoop; // break
    // ...
    ++i;           // for post-expression

    if (i >= array.Length) // for condition
    {
        goto startLoop;
    }

endOfLoop:
    // ...

ご覧のとおり、後方ループがあり、これが小さなループです。ここでまだ厄介なのは、breakステートメントのために終わったブランチだけです。場合によっては、これを同じ方法で移動できますが、別の場合はそのままになります。

では、なぜコンパイラはこれを行うのですか?ループを展開できれば、ベクトル化できるかもしれません。定数が追加されていることを証明できる場合もあります。つまり、ループ全体が空中に消えることがあります。要約すると、パターンを予測可能にする(分岐を予測可能にする)ことで、特定の条件がループ内で保持されることを証明できます。つまり、JIT最適化中に魔法をかけることができます。

ただし、ブランチはこれらのNiceの予測可能なパターンを壊す傾向があり、それはオプティマイザーのようなものなので、ちょっと嫌いです。破壊、続行、後発-これらはすべて、これらの予測可能なパターンを破壊することを意図しているため、実際には「ナイス」ではありません。

また、この時点で、単純なforeachの方が、あちこちにある一連のgotoステートメントよりも予測しやすいことを理解する必要があります。 (1)読みやすさと(2)オプティマイザーの観点から見ると、どちらも優れたソリューションです。

言及する価値のあるもう1つのことは、コンパイラーを最適化してレジスターを変数に割り当てることと非常に関連があることです(register allocationと呼ばれるプロセス)。ご存知かもしれませんが、CPUには限られた数のレジスタしかなく、それらはハードウェアの中で最も高速なメモリです。最も内側のループにあるコードで使用される変数は、レジスターが割り当てられる可能性が高くなりますが、ループ外の変数はそれほど重要ではありません(おそらく、このコードはヒットが少ないためです)。

ヘルプ、複雑すぎる...どうすればいいですか?

一番下の行は、自由に使える言語構成体を常に使用する必要があるということです。これは通常、コンパイラーの予測可能なパターンを(暗黙的に)構築します。可能であれば、奇妙な分岐を避けてください(具体的には、breakcontinuegoto、または何もない途中のreturn)。

ここでの良いニュースは、これらの予測可能なパターンは、読みやすく(人間にとって)、見つけやすい(コンパイラにとって)の両方です。

これらのパターンの1つはSESEと呼ばれ、これは単一エントリ単一出口の略です。

そして今、本当の質問に行き着きます。

次のようなものがあると想像してください。

// a is a variable.

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...

     if (i*j > a) 
     {
        // break everything
     }
  }
}

これを予測可能なパターンにする最も簡単な方法は、単にifを完全に削除することです。

int i, j;
for (i=0; i<100 && i*j <= a; ++i) 
{
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
}

また、メソッドを2つのメソッドに分割することもできます。

// Outer loop in method 1:

for (i=0; i<100 && processInner(i); ++i) 
{
}

private bool processInner(int i)
{
  int j;
  for (j=0; j<100 && i*j <= a; ++j)
  {
     // ...
  }
  return i*j<=a;
}

一時変数?良い、悪い、またはいですか?

ループ内からブール値を返すことを決定するかもしれません(しかし、私は個人的にSESEフォームを好みます。それはコンパイラがそれを見る方法であり、読みやすいと思うからです)。

一部の人々は、一時変数を使用する方がクリーンだと考え、次のような解決策を提案します。

bool more = true;
for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j) 
  {
     // ...
     if (i*j > a) { more = false; break; } // yuck.
     // ...
  }
  if (!more) { break; } // yuck.
  // ...
}
// ...

私は個人的にこのアプローチに反対しています。コードのコンパイル方法をもう一度見てください。次に、これがこれらのニースで予測可能なパターンで何をするかを考えてください。写真をゲット?

それでは、つづりましょう。起こることはそれです:

  • コンパイラはすべてをブランチとして書き出します。
  • 最適化ステップとして、コンパイラーは、制御フローでのみ使用される奇妙なmore変数を削除しようとして、データフロー分析を行います。
  • 成功した場合、変数moreはプログラムから削除され、分岐のみが残ります。これらのブランチは最適化されるため、内側のループからブランチを1つだけ取得します。
  • 失敗した場合、変数moreは間違いなく最も内側のループで使用されるため、コンパイラーがそれを最適化しないと、レジスターに割り当てられる可能性が高くなります(貴重なレジスターメモリを消費します)。

要約すると、コンパイラーのオプティマイザーは、moreが制御フローにのみ使用されていること、そして最良の場合のシナリオは、外側のforループの外側の単一ブランチに変換します。

言い換えれば、最良のシナリオは、これと同等の結果になるということです。

for (int i=0; i<100; ++i) 
{
  for (int j=0; j<100; ++j)
  {
     // ...
     if (i*j > a) { goto exitLoop; } // perhaps add a comment
     // ...
  }
  // ...
}
exitLoop:

// ...

これについての私の個人的な意見は非常に単純です。これが私たちがずっと意図していたものであるなら、コンパイラと読みやすさの両方のために世界をより簡単にし、それをすぐに書きましょう。

tl; dr:

結論:

  • 可能であれば、forループで単純な条件を使用します。できるだけ自由に使える高レベルの言語構成要素に固執してください。
  • すべてが失敗し、gotoまたはbool moreのいずれかが残っている場合は、前者を選択してください。
12
atlaste

関数/メソッドを考慮してアーリーリターンを使用するか、ループをwhile句に再配置します。 goto/exceptions/whateverは確かにここでは適切ではありません。

def do_until_equal():
  foreach a:
    foreach b:
      if a==b: return
11
Dustin Getz

クイック、ニース、ブール値の使用なし、gotoの使用なし、C#の組み合わせを求めました。あなたはあなたが望むことをするすべての可能な方法を除外しました。

最も迅速でleastい方法は、gotoを使用することです。

9

コードを独自の関数に抽象化して、早期リターンを使用するよりも、ニースが早い場合もありますが、早期リターンは悪です:)

public void GetIndexOf(Transform transform, out int outX, out int outY)
{
    outX = -1;
    outY = -1;

    for (int x = 0; x < Columns.Length; x++)
    {
        var column = Columns[x];

        for (int y = 0; y < column.Transforms.Length; y++)
        {
            if(column.Transforms[y] == transform)
            {
                outX = x;
                outY = y;

                return;
            }
        }
    }
}
5
Sky

状況によっては、内部ループの後でコードを実行しない場合にのみ、これを行うことができます。

for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < 100; j++)
    {
        i = 100;
        break;
    }
}

それは特別なものではありませんが、問題によっては最も簡単な解決策かもしれません。

2
Chris Bartow

「break」を使用する多くの例を見ましたが、「continue」を使用する例はありません。

それでも、内側のループには何らかのフラグが必要です。

while( some_condition )
{
    // outer loop stuff
    ...

    bool get_out = false;
    for(...)
    {
        // inner loop stuff
        ...

        get_out = true;
        break;
    }

    if( get_out )
    {
        some_condition=false;
        continue;
    }

    // more out loop stuff
    ...

}
2
dviljoen

数十年前にCでbreakを初めて見たので、この問題に悩まされました。私は、いくつかの言語拡張が壊れる拡張機能を持っていることを望んでいました。

break; // our trusty friend, breaks out of current looping construct.
break 2; // breaks out of the current and it's parent looping construct.
break 3; // breaks out of 3 looping constructs.
break all; // totally decimates any looping constructs in force.
1
Jesse C. Slicer

私は学生時代から、gotoなしでコードで何でもできることが数学的に証明可能であると言われたことを覚えています(つまり、gotoが唯一の答えである状況はありません)。だから、私は決してgotoを使用しません(私の個人的な好みだけで、私が正しいか間違っていることを示唆していない)

とにかく、ネストされたループから抜け出すには、次のようなことをします。

var isDone = false;
for (var x in collectionX) {
    for (var y in collectionY) {
        for (var z in collectionZ) {
            if (conditionMet) {
                // some code
                isDone = true;
            }
            if (isDone)
                break;
        }
        if (isDone) 
            break;
    }
    if (isDone)
        break;
}

...私のような人が反ゴト「ファンボーイ」であるのを助けることを願っています:)

0
MG123

それが私がやった方法です。まだ回避策です。

foreach (var substring in substrings) {
  //To be used to break from 1st loop.
  int breaker=1;
  foreach (char c in substring) {
    if (char.IsLetter(c)) {
      Console.WriteLine(line.IndexOf(c));
      \\setting condition to break from 1st loop.
      breaker=9;
      break;
    }
  }
  if (breaker==9) {
    break;
  }
}
0
Garvit Arora