ForEachメソッドとforeach文のどちらを使うか【C# List】

C#を使っていると似たような機能が出てきます。

同じような機能ならどっちを使っていいかわからない。

似ているがゆえに利用する側としては困ることがあります。

今回は繰り返しの強い味方Listにおける、foreach文とForEachメソッド編です。

両者の違いを確認しつつ、最終的にはforeach文を使うほうが柔軟性がある
ということを紹介する内容となっています。

なぜForEachメソッドではないのでしょうか?

早速、本文を見ていきましょう。

柔軟性があるforeach一択

冒頭でも述べましたが、結論から行きましょう。

Listを使用する場合、Listに実装されているForEachメソッドよりもforeach文を柔軟性の面からお勧めします。

  • ForEachメソッドはスキップできない
  • ForEachメソッドは途中で止められない

ForEachメソッドはスキップできない

ForEachメソッドはforeach文で使用できるcontinueを使用することができません。

エラー CS0139 break または continue に対応するループがありません

それゆえ、処理が不要なものに対しても一律同じ処理を行うため、
無駄な処理を行ってしまいます。

foreach文であれば処理をスキップできます。


Listの要素数が少なければ、あるいは処理が軽ければ特に気にならないレベルです。

しかし、一回の処理に数秒~数十秒かかってくるパターンはどうでしょう。
continueで無駄な処理を飛ばすことができれば、それだけで数秒の時短ができます。

ForEachメソッドは途中で止められない

foreachにあってForEachメソッドにないものそれがbreakです。

ForEachメソッドでbreakを利用しようとするとcontinue同様のエラーが発生します。

breakの機能くらい知っているよという方が多いでしょうが、改めて機能を確認しましょう。

foreach文のなかでbreakを使用するとその時点でループを終了します。

何がうれしいのでしょうか。

continue同様に、breakも必要な要素に対してのみ処理を実行できるということです。

foreach文ではそれができます。


この例では、「3」探し当てたのであれば、あとはゴミデータです。
breakでループを抜け出してしまいましょう。

このように、無駄な処理を省ける点でbreakを使用できるforeachに軍配があります。

性能的にはどっちでもOK

ForEachメソッドかforeach文を使うか。
気になるポイントのひとつは性能です。

私の手元環境では10万回繰り返す作業をしたとき
0.2%程度foreachが早い結果になりました。

ほぼ、誤差の範囲ではないでしょうか。

環境に依存するのと、書いているコードに依存するので
あくまで、参考程度にお考えください。

ちなみに、一番早かったのはforeachでも、ForEach()でもなく、
For文で扱ったときでした。(ForEach()より1,2%早い結果に)

ただ、ギリギリのパフォーマンスを追及する場面でもない限りは、どれを使用しても問題ないでしょう。

もし、最高峰のパフォーマンスを追及するのであれば、foreachかForEachかを検討するよりも、
ロジックをみなおしたり、思い切ってC#よりも高速な言語への変更を検討したほうが効果が高そうです。

ForEachメソッドをおすすめしにくい理由

ForEachについてはさらにおすすめしにくい理由があります。

  • Listのほかの機能との競合
  • 非同期メソッドを扱いづらい

それぞれみていきましょう。

Listには他の便利機能が多いから

ForEachメソッドを利用しづらい背景には、Listが持ついくつものメソッドの存在があります。

ForEachメソッドは「繰り返し」をする機能です。というか「繰り返し」だけです。

一方で以下の一文を加えると

using System.Linq;

Listで使える「ForEach + アルファ」の機能が解放されます。

それでは「ForEach + アルファ」の付加価値のあるメソッドを見ていきましょう。

不要なデータを排除、Where,FindAll

Whereメソッド、FindAllメソッドはフィルター機能です。

不要なものを排除するという、使い方としてはForEachメソッド + if文になります。
ForEachメソッド +continue相当の動きとも言えます。

Whereメソッド、FindAllメソッドは
今回のforeach文とForEachメソッドと同じように、似た機能ですね。

Where(),FindAll()の違いについて紹介している記事もありますので、
興味がありましたらどうぞ。

Listのデータを使って別のオブジェクトを生成、Select

Listから別のListを生成する機能がSelectメソッドです。

この別のオブジェクトへの変換は一例で、柔軟に別のオブジェクトを生成できます。

こちらの機能を使えばForEachメソッドで一つずつ要素を取り出す作業をもやってくれる上に
別の要素に変形までしてくれます。

類似機能としては、SelectManyメソッドがありますね。
Listの中にListを基準に別のオブジェクトのリストを生成できます。

ForEachメソッド以外の付加価値のあるメソッドを使おう

今回はいくつかのメソッドを紹介しました。
これらの「ForEach+アルファ」の機能を使うには少しうれしい副次効果があります。

目的が明確になる

「ForEach+アルファ」の機能を使うと目的が明確になります。

ForEachメソッドは繰り返しをするのですけれども、
繰り返しが目的かといわれると違います。

重要な目的、「繰り返した結果何をしたいか」はコードを読まないといけません。

「if文があった、変数が定義されて・・・」と中身を理解する必要があります。

それゆえ、ForEachメソッドが使用されたコードを読む場合は、
コードを読み終えた最後に目的がわかります。

しかし、WhereメソッドやSelectメソッドを使えばどうでしょう。

Whereメソッドは不要なレコードを排除することがきまっています。

読み手はWhereメソッドをみれば「排除」とわかるため、
あとはどんな条件で排除しようとしているのかに注目すればよくなります。

「ForEach+アルファ」の機能を使えば、最初に目的がわかるため読み手へのストレスを減らせるメリットがあるのです。

コード量が減る

また、「ForEach+アルファ」の機能を使うとコード量が減る効果もあります。

例えば、Selectメソッド相当の動きをするには、変数を用意であったり、
別のリストに詰め替える作業を自分で書かなければなりません。


「ForEach+アルファ」の機能を使って書いてみましょう。


「ForEach+アルファ」のメソッドたちは追加の機能部分が省略されているのでその分減るわけです。

今回紹介した例は、正直微々たる効果かもしれません。

しかしながら、複雑なコードになれば、なるほどコード量の減少量を実感しやすくなるでしょう。

ForEachメソッドの出番は少ない

Listは「繰り返し」に関する操作がメインです。

「繰り返し」そのもののForEachメソッドは機能提供するわけですが、
やはりそれよりも高機能のメソッドが用意されているため、
必然に別のメソッドに置き換えられていきます。

それゆえ、ForEachメソッドの出番も減っていきます。

もし、ForEachメソッドを使っているコードを見たのであれば、
他のListメソッドでシンプルに書き換えられないか検討する価値ありです。

非同期メソッドを扱いづらい

もうひとつForEachメソッドをおすすめしにくい理由があります。

ForEachメソッドと非同期メソッドを組み合わせたときの癖です。

ForEachメソッドをつかって非同期メソッドを使うときはいくつか注意点があります。

  • foreach文と同じ感覚で使うとコンパイルエラー
  • コンパイルエラーを回避する方法
  • コンパイルエラーを回避すると処理方法が変わる罠

それでは注意点を見ていきましょう。

foreach文と同じ感覚で使うとコンパイルエラー

ForEachメソッドはforeach文と似ているわけですけれども、
非同期メソッドを同じように呼び出すことはできません。


ちなに、 Delay1sec(i)は一秒待つ単純な処理。待つ処理の前後でその時の時間を出力します。

こんな感じ。


foreach文の場合では同じ関数の中でasync/awaitのペアになっているためエラーにはなりません。

しかし、ForEachメソッドの場合は違います。

エラー CS4034 await’ 演算子は、非同期の ラムダ式 でのみ使用できます。この ラムダ式 を ‘async’ 修飾子でマークすることを検討してください。

ForEachメソッドで、foreach文と同じように記述すると、コンパイルエラー(CS4034)が発生します。

これはForEachメソッドの中身は「別の関数」として扱われるため、

MainメソッドについているasyncとDelay1secメソッドのawaitが対になっていないのが原因です。

コンパイルエラーを回避する方法

ではForEachメソッドのなかで非同期メソッドを待機するには、どのように書けばよいのかというと、
ForEachメソッドの中でasync/awaitのペアを書けばOKです。

コンパイルエラーを回避することができます。

コンパイルエラーを回避すると処理方法が変わる罠

ただし、コンパイルエラーを回避して、めでたしめでたしとは行きません。

foreach文とForEachメソッドで処理方法が変わっているからです。

待機する場所が変わることで処理が変わる

こちら、実行結果です。
見るからに変わりました。

まず待機終了後の「Delay1sec end」がForEachメソッドの場合一つもありませんね。

次に実行時の時間の違いに注目してください。

foreach文の場合は1秒まってから次の出力がされています。

しかし、ForEach文の場合は1秒待たずに次の出力が開始されています。

この2つの違いは、ForEachメソッドのasync/awaitの位置が変わり「待つ場所」が変わったことが原因です。

見方によってはお手軽並列処理にもみえなくもないですけれども、

ForEachメソッドの中で非同期メソッドを使用すると、
非同期メソッドが完了するまでにアプリが終了するなんてことが起きます

もし、並列処理をするにしても、C#にはほかの方法が用意されているので、
わざわざForEachメソッドに手を出さないほうが良いでしょう。

要約すると、ForEachメソッドを使うよりも簡潔に記述できる便利機能があるのと、
ForEachメソッドを使うと思わぬバグに遭遇する可能性があるため、

どちらかというとあまり使用しないメソッドになっています。

まとめ

今回の記事を最後に振り返ります。

  • ForEachメソッドとforeachなら、foreachを使う
  • ForEachメソッドを使うなら、別のメソッドで置き換えれないか検討
  • ForEachメソッドは非同期処理との併用に癖があるため注意

柔軟性、可読性の点からもForEachメソッドはあまり使用しないほうが良いでしょう。

非同期処理をするにしても、癖がありますので、思わぬバグを生むかもしれません。

それでも、使用する場合は細心の注意を払いたいですね。

では。