for
対 foreach
これらの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;
}
}
どちらも同じ都市を返しますが、大きな違いがあります。
- を使用すると
foreach
、ListCities()
1回呼び出され、47個のアイテムが生成されます。
- を使用すると
for
、ListCities()
94回呼び出され、全体で28153個のアイテムが生成されます。
どうした?
IEnumerable
ある怠惰。結果が必要なときにのみ作業を行うことを意味します。遅延評価は非常に便利な概念ですが、特に結果が複数回使用される場合に、結果が必要になる瞬間を見逃しやすいという事実など、いくつかの注意事項があります。
aの場合foreach
、結果は一度だけ要求されます。上記の誤って記述されたコードで実装された aの場合、結果は94回要求さfor
れます( 47×2)。
データベースを1回ではなく94回クエリするのはひどいですが、最悪の事態ではありません。たとえば、select
テーブルの行も挿入するクエリがクエリの前にある場合にどうなるかを想像してください。そうです、以前にクラッシュしない限りfor
、データベースを2,147,483,647回呼び出します。
もちろん、私のコードは偏っています。私はの怠inessを故意に使用し、IEnumerable
繰り返し電話する方法でそれを書きましたListCities()
。以下の理由により、初心者は決してそうしないことに注意することができます。
おそらくほとんどの初心者は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
、本当に何ですか?
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
は同様の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#コードをより機能的な方法で表現できることは、かなり良いことです。
納得できない場合は、このテーマに関する前回の回答で、機能的および非機能的な方法で記述されたコードの可読性を比較してください。