for vs. foreach vs. LINQ


86

Visual Studioでコードを書くとき、ReSharper(神のご加護を!)はしばしば、古い学校のforループをよりコンパクトなforeach形式に変更することを勧めます。

多くの場合、この変更を受け入れると、ReSharperは一歩前進し、光沢のあるLINQ形式で再度変更することを提案します。

だから、私は疑問に思う:これらの改善には、いくつかの本当の利点がありますか?非常に単純なコード実行では、速度の向上は見られませんが(明らかに)、コードがますます読みにくくなっているのがわかります。


2
ご注意ください-SQL構文に精通している場合、LINQ構文は実際にはかなり読みやすいです。また、LINQには2つの形式(SQLに似たラムダ式とチェーンメソッド)があり、学習しやすくなります。ReSharperの提案が読みにくいように見えるかもしれません。
シャウナ

3
経験則として、既知の長さの配列または反復回数が関係する同様のケースで作業しない限り、通常foreachを使用します。LINQ化については、通常、ReSharperがforeachで作成するものを確認します。結果のLINQステートメントが整頓されている/些細な/読みやすい場合はそれを使用し、それ以外の場合は元に戻します。要件が変更された場合に元の非LINQロジックを書き直すのが面倒な場合、またはLINQステートメントが抽象化されているロジックをきめ細かくデバッグする必要がある場合は、LINQを実行せずにそのままにしておきます形。
エドヘイスティングス

よくある間違いの1つforeachは、列挙中にコレクションから項目を削除することです。通常for、最後の要素から開始するにはループが必要です。
-Slai

Øredev2013-Jessica Kerr-Functional Principles for Object-Oriented Developersから価値を得るかもしれません。Linqは、「宣言型スタイル」というタイトルで、33分後すぐにプレゼンテーションに入ります。
Theraot

回答:


139

forforeach

これらの2つの構成要素は非常に類似しており、どちらも次のように交換可能であるという一般的な混乱があります。

foreach (var c in collection)
{
    DoSomething(c);
}

そして:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

両方のキーワードが同じ3文字で始まるという事実は、意味的には同じという意味ではありません。この混乱は、特に初心者にとって非常にエラーが発生しやすいものです。コレクションを反復処理し、要素を使用して何かを実行するには、次のようにしforeachます。forあなたは本当にあなたが何をしているのかを知っていない限り、この目的のため使用する必要はなく、使用すべきではありません

例でそれの何が悪いのか見てみましょう。最後に、結果を収集するために使用されるデモアプリケーションの完全なコードがあります。

この例では、「ボストン」に遭遇する前に、データベースからいくつかのデータ、より正確にはAdventure Worksの都市を名前順にロードしています。次のSQLクエリが使用されます。

select distinct [City] from [Person].[Address] order by [City]

データは、ListCities()を返すメソッドによってロードされますIEnumerable<string>。これはforeach次のようなものです。

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

for両方で交換可能と仮定して、で書き直しましょう:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

どちらも同じ都市を返しますが、大きな違いがあります。

  • を使用するとforeachListCities()1回呼び出され、47個のアイテムが生成されます。
  • を使用するとforListCities()94回呼び出され、全体で28153個のアイテムが生成されます。

どうした?

IEnumerableある怠惰。結果が必要なときにのみ作業を行うことを意味します。遅延評価は非常に便利な概念ですが、特に結果が複数回使用される場合に、結果が必要になる瞬間を見逃しやすいという事実など、いくつかの注意事項があります。

aの場合foreach、結果は一度だけ要求されます。上記の誤って記述されたコードで実装された aの場合、結果は94回要求さfor れます( 47×2)。

  • 毎回cities.Count()(47回)呼び出され、

  • 毎回cities.ElementAt(i)呼び出されます(47回)。

データベースを1回ではなく94回クエリするのはひどいですが、最悪の事態ではありません。たとえば、selectテーブルの行も挿入するクエリがクエリの前にある場合にどうなるかを想像してください。そうです、以前にクラッシュしない限りfor、データベースを2,147,483,647回呼び出します。

もちろん、私のコードは偏っています。私はの怠inessを故意に使用し、IEnumerable繰り返し電話する方法でそれを書きましたListCities()。以下の理由により、初心者は決してそうしないことに注意することができます。

  • ザ・は、IEnumerable<T>プロパティを持っていないCountが、唯一の方法Count()。メソッドの呼び出しは恐ろしく、その結果はキャッシュされず、for (; ...; )ブロックに適さないことが予想されます。

  • インデックスは使用できません。LINQ拡張メソッドIEnumerable<T>を見つけるのは明らかではありませんElementAt

おそらくほとんどの初心者はListCities()、のような結果を慣れ親しんだものに変換するでしょうList<T>

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

それでも、このコードはforeach代替とは大きく異なります。繰り返しますが、同じ結果が得られListCities()、今回はメソッドが1回だけ呼び出されますが、575個のアイテムが生成されますがforeach、の場合は47個のアイテムしか生成されません。

違いは事実から来ているToList()原因のすべてのデータがデータベースからロードされます。foreach「ボストン」の前に都市のみをリクエストしましたが、新しいforバージョンではすべての都市を取得してメモリに保存する必要があります。575個の短い文字列では、おそらく大きな違いはありませんが、数十億のレコードを含むテーブルから数行のみを取得する場合はどうでしょうか。

ではforeach、本当に何ですか?

foreachwhileループに近い。以前に使用したコード:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

単純に次のものに置き換えることができます:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

どちらも同じILを生成します。両方とも同じ結果になります。どちらも同じ副作用があります。もちろん、これwhileは同様のinfinite forで書き換えることができますが、さらに長くなり、エラーが発生しやすくなります。より読みやすいと思うものを自由に選択できます。

自分でテストしたいですか?完全なコードは次のとおりです。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

そして結果:

--- for ---
アビンドンアルバニーアレクサンドリアアルハンブラ[...]ボンボルドーボストン

データは94回と呼ばれ、28153個のアイテムが生成されました。

---リスト付き---
アビンドンアルバニーアレクサンドリアアルハンブラ[...]ボンボルドーボストン

データは1回呼び出され、575個のアイテムが生成されました。

--- while ---
アビンドンアルバニーアレクサンドリアアルハンブラ[...]ボンボルドーボストン

データは1回呼び出され、47個のアイテムが生成されました。

--- foreach ---
アビンドンアルバニーアレクサンドリアアルハンブラ[...]ボンボルドーボストン

データは1回呼び出され、47個のアイテムが生成されました。

LINQと従来の方法

LINQについては、C#FPのものではなく、Haskellのような実際のFP言語- 関数型プログラミング(FP)を学びたい場合があります。関数型言語には、コードを表現および表示するための特定の方法があります。状況によっては、機能しないパラダイムよりも優れています。

FPは、リストの操作に関して非常に優れていることが知られています(リストはに関係のない総称としてのリストList<T>)。この事実を考えると、リストに関しては、C#コードをより機能的な方法で表現できることは、かなり良いことです。

納得できない場合は、このテーマに関する前回の回答で、機能的および非機能的な方法で記述されたコードの可読性を比較してください。


1
ListCities()の例に関する質問。なぜ一度だけ実行されるのですか?私は過去に利回りのリターンを教えるために問題がありませんでした。
ダンテ

1
彼は、IEnumerableから1つの結果しか取得しないと言っているわけではありません-SQLクエリ(メソッドの高価な部分)は1回しか実行されないと言っている-これは良いことです。その後、クエリからすべての結果を読み取り、生成します。
HappyCat

9
@Giorgio:この質問は理解できますが、言語のセマンティクスを初心者が混乱させるものにパンダーしても、非常に効果的な言語は残されません。
スティーブンエヴァーズ

4
LINQは単なるセマンティックシュガーではありません。遅延実行を提供します。そして、IQueryables(例:Entity Framework)の場合、反復されるまでクエリを渡して構成することができます(つまり、返されたIQueryableにwhere句を追加すると、そのwhere句を含めるためにSQLが反復時にサーバーに渡されます)サーバーへのフィルタリングのオフロード)。
マイケルブラウン

8
この答えが好きなのと同じように、この例は多少不自然だと思います。最後の要約は、実際には不一致が意図的に壊れたコードの結果である場合、それforeachがより効率的であることを示唆してforいます。回答の徹底はそれ自体を引き換えますが、偶然の観察者がどのように間違った結論に到達するかは容易にわかります。
ロバートハーベイ

19

forとforeachの違いについては、すでにいくつかの素晴らしい説明があります。LINQの役割についていくつかの大きな不実表示があります。

LINQ構文は、C#に近似する関数型プログラミングを提供する単なる構文糖ではありません。LINQは、C#にそのすべての利点を含む機能的な構造を提供します。LINQは、IListではなくIEnumerableを返すことと組み合わせて、反復の遅延実行を提供します。現在人々が通常しているのは、そのような関数からIListを作成して返すことです

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

代わりに、yield return構文を使用して、遅延列挙を作成します。

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

これで、ToListを実行するか、繰り返し処理するまで、列挙は発生しません。そして、必要な場合にのみ発生します(スタックオーバーフローの問題のないFibbonaciの列挙です)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

フィボナッチ関数でforeachを実行すると、46のシーケンスが返されます。30番目が必要な場合は、それだけで計算されます

var thirtiethFib=Fibonacci().Skip(29).Take(1);

ラムダ式(IQueryableおよびIQueryProviderコンストラクトと組み合わせることで、さまざまなデータセットに対するクエリの機能的構成が可能になります。IQueryProviderは渡されたものを解釈する責任があります。式、およびソースのネイティブコンストラクトを使用したクエリの作成と実行)。ここでは詳細を詳しく説明しませんが、ここにSQLクエリプロバイダーを作成する方法を示す一連のブログ投稿があります

要約すると、関数のコンシューマーが単純な反復を実行する場合、IListよりもIEnumerableを返すことをお勧めします。そして、LINQの機能を使用して、複雑なクエリの実行が必要になるまで延期します。


13

しかし、コードがどんどん読みにくくなっているのを見ることができます

読みやすさは見る人の目にあります。一部の人々は言うかもしれません

var common = list1.Intersect(list2);

完全に読み取り可能です。他の人は、これは不透明だと言うかもしれませんし、好むでしょう

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

何をしているのかを明確にすることとして。もっと読みやすいと思うものを伝えることはできません。しかし、私がここで構築した例では、私自身のバイアスのいくつかを検出できるかもしれません...


28
正直なところ、Linqは意図を客観的に読みやすくする一方で、forループはメカニズムを客観的に読みやすくするということです。
jk。

16
for-for-ifバージョンは、intersectionバージョンよりも読みやすいと言っている人からできるだけ早く実行します。
コナミマン

3
@Konamiman-それは、人が「読みやすさ」を考えるときに何を探すかにかかっています。jk。のコメントはこれを完全に示しています。ループは、最終結果がどのように得られるを簡単に確認できるという意味で読みやすくなりますが、LINQは最終結果がどうあるべきかで読みやすくなります。
シャウナ

2
そのため、ループが実装に組み込まれ、どこでもIntersectを使用します。
R.マルティーニョフェルナンデス

8
@Shauna:他のいくつかのことを行うメソッド内のforループバージョンを想像してください。それは混乱です。したがって、当然、それを独自のメソッドに分割します。読みやすさの点では、これはIEnumerable <T> .Intersectと同じですが、フレームワークの機能を複製し、維持するコードを増やしました。唯一の言い訳は、動作上の理由でカスタム実装が必要な場合ですが、ここでは読みやすさについてのみ説明しています。
ミスコ

7

LINQとforeach実際の違いは、命令型と宣言型の2つの異なるプログラミングスタイルに要約されます。

  • 必須:このスタイルでは、コンピューターに「これを行う...今、これを行う...今、これを行う、これを行う」と伝えます。一度に1ステップずつプログラムをフィードします。

  • 宣言的:このスタイルでは、コンピューターに結果をどのようにしたいかを伝え、そこに到達する方法を見つけさせます。

これら2つのスタイルの典型的な例は、アセンブリコード(またはC)とSQLの比較です。アセンブリでは、(文字通り)一度に1つずつ指示を出します。SQLでは、データを結合する方法と、そのデータから得られる結果を表現します。

宣言型プログラミングの素晴らしい副作用は、それが少し高いレベルになる傾向があることです。これにより、コードを変更することなく、プラットフォームをユーザーの下で進化させることができます。例えば:

var foo = bar.Distinct();

ここで何が起きてるの?Distinctは1つのコアを使用していますか?二?50歳?私たちは知りませんし、気にしません。.NET開発者は、コードの更新後にコードが魔法のように高速化できるのと同じ目的を実行し続ける限り、いつでも書き換えることができます。

これが関数型プログラミングの力です。また、Clojure、F#、C#(関数型プログラミングマインドセットで記述された)などの言語のコードが、命令型のコードよりも3倍から10倍小さいことがよくある理由がわかります。

最後に、C#ではほとんどの場合、これによりデータを変更しないコードを記述できるため、宣言スタイルが好きです。上記の例でDistinct()は、バーを変更せず、データの新しいコピーを返します。これは、バーが何であれ、どこから来たとしても、突然変化しないことを意味します。

他のポスターが言っているように、関数型プログラミングを学びましょう。それはあなたの人生を変えるでしょう。そして、可能であれば、真の関数型プログラミング言語で実行してください。私はClojureを好みますが、F#とHaskellも優れた選択肢です。


2
LINQ処理は、実際に繰り返し処理されるまで延期されます。 var foo = bar.Distinct()本質的には、またはIEnumerator<T>を呼び出すまでです。これは重要な違いです。なぜなら、それを知らないと、バグを理解するのが難しくなるからです。.ToList().ToArray()
ベリンロリチュ

-5

チームの他の開発者はLINQを読むことができますか?

そうでない場合は使用しないでください。そうしないと、次の2つのいずれかが発生します。

  1. あなたのコードは維持できなくなります
  2. すべてのコードとそれに依存するすべてを維持することにこだわるでしょう

for eachループはリストを反復処理するのに最適ですが、それがあなたのしていることではない場合は使用しないでください。


11
うーん、単一のプロジェクトではこれが答えかもしれないことを感謝しますが、中長期的にはスタッフを訓練する必要があります。
jk。

21
実際には、起こりうる3番目のことがあります。他の開発者は少しの努力をして、実際に新しい便利なことを学ぶことができます。それは前代未聞ではありません。
エリックキング

6
@InvertedLlama開発者が新しい言語の概念を理解するために正式なトレーニングを必要とする会社にいた場合、新しい会社を見つけることを考えています。
ワイアットバーネット

13
おそらく、ライブラリを使用してそのような態度を取り払うことができますが、コア言語機能に関しては、それは削減されません。フレームワークを選択できます。しかし、優れた.NETプログラマーは、言語およびコアプラットフォーム(System。*)のすべての機能を理解する必要があります。そして、あなたはLinqを使用せずにEFを適切に使用することさえできないと考えると、私は言わなければなりません...あなたが.NETプログラマーであり、Linqを知らないなら、あなたは無能です。
ティモシーボールドリッジ

7
これにはすでに十分な下票がありますので、それには追加しませんが、無知/無能な同僚を支持する議論は決して有効なものではありません。
スティーブンエバーズ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.