web-dev-qa-db-ja.com

for vs. foreach vs. LINQ

Visual Studioでコードを記述するとき、ReSharper(神の加護に感謝します!)は、以前のforループをよりコンパクトなforeachフォームに変更するように提案することがよくあります。

そして、多くの場合、この変更を受け入れると、ReSharperが一歩前進し、光沢のあるLINQフォームで再度変更するように提案します。

だから、私は疑問に思います:これらの改善にはいくつかのreal利点がありますか?かなり単純なコードの実行では、(明らかに)速度の向上は見られませんが、コードが次第に読みにくくなっていくのがわかります...だから私は疑問に思います:それだけの価値があるのでしょうか?

87
beccoblu

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]
_

データは、_IEnumerable<string>_を返すListCities()メソッドによってロードされます。 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;
    }
}
_

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

  • foreachを使用すると、ListCities()が1回呼び出され、47項目が生成されます。
  • forを使用する場合、ListCities()は94回呼び出され、全体で28153項目を生成します。

どうした?

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

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

  • cities.Count()が呼び出されるたび(47回)、

  • cities.ElementAt(i)が呼び出されるたび(47回)。

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

もちろん、私のコードは偏っています。私はIEnumerableの遅延を意図的に使用し、ListCities()を繰り返し呼び出すように記述しました。初心者がそれをすることは決してないことに気付くでしょう。

  • _IEnumerable<T>_にはプロパティCountはなく、メソッドCount()のみがあります。メソッドの呼び出しは恐ろしく、その結果はキャッシュされず、for (; ...; )ブロックには適さないことが予想されます。

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

おそらく、ほとんどの初心者は、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()allデータをデータベースからロードするという事実によるものです。 foreachは「ボストン」より前の都市のみを要求しましたが、新しいforではすべての都市を取得してメモリに保存する必要があります。 575の短い文字列の場合、おそらくそれほど大きな違いはありませんが、数十億のレコードを含むテーブルから数行のみを取得した場合はどうなるでしょうか。

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

foreachは、whileループに近いです。以前使用したコード:

_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は、同様の無限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();
                    }
                }
            }
        }
    }
}
_

そして結果:

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

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

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

データは1回呼び出され、575項目が生成されました。

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

データは1回呼び出され、47項目が生成されました。

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

データは1回呼び出され、47項目が生成されました。

LINQと従来の方法

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

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

確信が持てない場合は、件名について私の 前の回答 で、機能的方法と非機能的方法の両方で書かれたコードの読みやすさを比較してください。

140

Forとforeachの違いについては、すでにいくつかのすばらしい説明があります。 LINQの役割については、いくつかの大きな誤解があります。

LINQ構文は、C#に関数型プログラミングの近似を提供する単なる構文上の砂糖ではありません。 LINQは、C#に対するそのすべての利点を含む機能構成を提供します。 IListの代わりにIEnumerableを返すことと組み合わせて、LINQは反復の遅延実行を提供します。人々が今やっていることは、そのような関数から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戻り構文 を使用して、遅延列挙を作成します。

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の機能を使用して、複雑なクエリの実行を必要になるまで延期します。

19
Michael Brown

しかし、コードがどんどん読みにくくなっているのがわかります

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

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

何が行われているのかが明確になるように。私たちはあなたがより読みやすいと思うものをあなたに伝えることはできません。しかし、私がここで作成した例では、自分のバイアスのいくつかを検出できるかもしれません...

13
AakashM

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

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

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

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

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

_var foo = bar.Distinct();
_

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

これが関数型プログラミングの力です。そして、Clojure、F#、C#(関数型プログラミングの考え方で書かれた)などの言語のコードが、必須のコードよりも3倍から10倍小さいことがよくある理由です。

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

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

7