C#とJavaでループするクールな方法は、ループにCスタイルの代わりにforeachを使用することです。
Cスタイルよりもこのスタイルを好むべき理由はありますか?
これら2つのケースに特に興味がありますが、ポイントを説明するために必要な数のケースに対処してください。
私が考えることができる2つの主な理由は次のとおりです。
1)基になるコンテナタイプから抽象化します。これは、たとえば、コンテナを変更するときにコンテナ内のすべてのアイテムをループするコードを変更する必要がないことを意味します-goal of "do this for meansではなく、コンテナ内のすべてのアイテム」。
2)オフバイワンエラーの可能性を排除します。
リスト内の各アイテムに対して操作を実行するという点では、次のように言うのは直感的です。
for(Item item: lst)
{
op(item);
}
反復子を使用して手動で行うのではなく、読者に意図を完全に表現します。アイテムを検索するための同上。
あなたがレストランの主任シェフであり、ビュッフェ用に巨大なオムレツを準備していると想像してください。あなたは、1ダースの卵のカートンを2人のキッチンスタッフのそれぞれに渡し、文字通りひびを入れるように伝えます。
最初のボウルはボウルを設置し、木枠を開き、各卵を順番につかみます-左から右へ、上列、次に下列で-ボウルの側面にぶつけてから、ボウルに入れます。最終的に彼は卵を使い果たします。よくできた仕事。
2つ目はボウルを設置し、木枠を開き、ダッシュで一枚の紙とペンを手に入れます。彼は、卵のカートンのコンパートメントの横に0から11までの数字と、紙の上に0を書きます。彼は紙の番号を見て、0というラベルの付いたコンパートメントを見つけ、卵を取り除き、ボウルに割ります。彼は再び紙の0を見て、「0 + 1 = 1」と考え、0を取り消し、紙に1を書き込みます。彼はコンパートメント1から卵をつかみ、割れます。といった具合に、12番が紙に載り、卵がもうないことを(見ずに!)知っているまで。よくできた仕事。
2番目の男の頭が少し台無しになったと思いますか?
高水準言語で作業することのポイントは、コンピューターの用語で物事を記述する必要を避け、自分の用語でそれらを記述することができるようにすることです。言語が高レベルであるほど、これは真実です。ループ内でカウンターをインクリメントすると、本当にやりたいこと、つまり各要素を処理することから気が散ることになります。
それに加えて、リンクリスト型の構造は、カウンターをインクリメントしてインデックスを作成しても効率的に処理できません。「インデックス作成」とは、カウントを最初からやり直すことを意味します。 Cでは、ループ「カウンター」へのポインターを使用してそれを逆参照することにより、自分で作成したリンクリストを処理できます。 「イテレータ」を使用して、これを現代のC++(およびC#とJavaである程度)で行うことができますが、これはまだ間接性の問題を抱えています。
最後に、一部の言語は、実際にループを記述するが「リスト内の各項目に対して操作を実行する」または「リスト内の項目を検索する」という考えがぞっとするほど高いレベルですヘッドシェフが最初のキッチンスタッフにすべての卵が割れていることを確認する方法を伝える必要がないのと同じ方法)。そのループ構造を設定する関数が提供されており、検索の場合、高次関数、または場合によっては比較値を使用して、ループ内で何をするかを指示します。 (実際、これらのことはC++で行うことができますが、インターフェースはやや不格好です。)
foreach
はよりシンプルで読みやすい
リンクされたリストのような構造に対してより効率的です
すべてのコレクションがランダムアクセスをサポートしているわけではありません。 HashSet<T>
またはDictionary<TKey, TValue>.KeysCollection
を反復処理する唯一の方法はforeach
です。
foreach
を使用すると、追加の一時変数なしでメソッドから返されたコレクションを反復処理できます。
foreach(var thingy in SomeMethodCall(arguments)) { ... }
私にとっての利点の1つは、次のような間違いを犯しにくいことです。
for(int i = 0; i < maxi; i++) {
for(int j = 0; j < maxj; i++) {
...
}
}
更新:これはバグが発生する1つの方法です。合計する
int sum = 0;
for(int i = 0; i < maxi; i++) {
sum += a[i];
}
その後、さらに集約することを決定します。そこで、ループを別のループでラップします。
int total = 0;
for(int i = 0; i < maxi; i++) {
int sum = 0;
for(int i = 0; i < maxi; i++) {
sum += a[i];
}
total += sum;
}
コンパイルはもちろん失敗するので、手作業で編集します
int total = 0;
for(int i = 0; i < maxi; i++) {
int sum = 0;
for(int j = 0; j < maxj; i++) {
sum += a[i];
}
total += sum;
}
現在、コードには少なくとも2つの誤りがあり(maxiとmaxjを混乱させた場合はそれ以上)、実行時エラーによってのみ検出されます。そして、あなたがテストを書かないなら...そしてそれはまれなコードです-これは誰か他の人を噛むでしょう-ひどく。
そのため、内部ループをメソッドに抽出することをお勧めします。
int total = 0;
for(int i = 0; i < maxi; i++) {
total += totalTime(maxj);
}
private int totalTime(int maxi) {
int sum = 0;
for(int i = 0; i < maxi; i++) {
sum += a[i];
}
return sum;
}
より読みやすくなりました。
foreach
は、説明したような単純なシナリオを含め、すべてのシナリオ[1]でfor
と同じように機能します。
ただし、foreach
には、for
よりもパフォーマンスに関連しない特定の利点があります。
i
を保持する必要はありません(これは、ループを促進する以外の目的はありません)。また、現在の値を変数にフェッチする必要はありません。ループ構造はすでにそれを処理しました。foreach
を使用すると、配列ではないシーケンスを簡単に繰り返すことができます。 for
を使用して非配列順序シーケンス(たとえば、マップ/ディクショナリ)をループするには、少し異なる方法でコードを記述する必要があります。 foreach
は、すべてのケースで同じです。したがって、ご覧のとおり、foreach
はmostの状況で使用するのに「より良い」です。
ただし、他の目的でi
の値が必要な場合、または配列であることがわかっているデータ構造を処理している場合(および配列である実際の特定の理由がある場合)、より詳細なfor
が提供する機能が進むべき道です。
[1]「すべてのシナリオで」とは、実際には「コレクションが反復されやすいすべてのシナリオ」を意味し、実際には「ほとんどのシナリオ」になります(以下のコメントを参照)。ただし、反復に適さないコレクションを含む反復シナリオを設計する必要があると思います。
Stuart Golodetzが答えたように、それは抽象化です。
i
の値を他の目的に使用するのではなく、i
をインデックスとしてのみ使用している場合
String[] lines = getLines();
for( int i = 0 ; i < 10 ; ++i ) {
System.out.println( "line " + i + lines[i] ) ;
}
i
の現在の値を知る必要はありません。エラーが発生する可能性があるだけです。
Line[] pages = getPages();
for( int i = 0 ; i < 10 ; ++i ) {
for( int j = 0 ; j < 10 ; ++i )
System.out.println( "page " + i + "line " + j + page[i].getLines()[j];
}
アンドリュー・ケーニッヒが言うように、「抽象化は選択的な無知」です。コレクションの反復方法の詳細を知る必要がない場合は、それらの詳細を無視する方法を見つければ、より堅牢なコードを作成できます。
Foreachを使用する理由:
i++
を忘れた)が忍び寄るのを防ぎます。ループを台無しにする方法はたくさんありますが、foreachループを台無しにする方法は多くありません。for
ループは、場合によっては不可能な場合もあります(たとえば、IEnumerable<T>
のようにインデックス付けできないIList<T>
がある場合)。for
を使用する理由:
IEnumerable<T>
を実装しません-foreach
は列挙可能なものに対してのみ動作します。foreach
は、宛先配列スロットのアドレス指定に使用できるインデックス変数を提供しません。 for
は、そのような場合に意味のある唯一のものです。質問でリストする2つのケースは、どちらのループを使用しても実質的に同じです。最初のループではリストの最後まで繰り返し、2番目のループではアイテムが見つかったらbreak;
あなたは探している。
foreach
をさらに説明するために、このループ:
IEnumerable<Something> bar = ...;
foreach (var foo in bar) {
// do stuff
}
以下の構文糖衣です:
IEnumerable<Something> bar = ...;
IEnumerator<Something> e = bar.GetEnumerator();
try {
Something foo;
while (e.MoveNext()) {
foo = e.Current;
// do stuff
}
} finally {
((IDisposable)e).Dispose();
}
IEnumerableを実装するコレクションを反復処理する場合は、foreachを使用する方が自然です。反復の次のメンバーは、最後に到達するためのテストが完了すると同時に割り当てられるためです。例えば。、
foreach (string day in week) {/* Do something with the day ... */}
より簡単です
for (int i = 0; i < week.Length; i++) { day = week[i]; /* Use day ... */ }
クラスのIEnumerableの実装でforループを使用することもできます。 GetEnumerator()の実装に、ループの本文でC#yieldキーワードを使用させるだけです。
yield return my_array[i];
Java
には、指定した両方のループタイプがあります。必要に応じて、いずれかのforループバリアントを使用できます。あなたのニーズはこんな感じです
最初のケースでは、古典的な(cスタイル)forループを使用する必要があります。ただし、2番目の場合は、foreach
ループを使用する必要があります。
foreach
ループは、最初のケースでも使用できます。ただし、その場合は独自のインデックスを維持する必要があります。
いくつかの理由が考えられます
can't mess up indexes
、モバイル環境でもコンパイラーの最適化がなく、ループに対して大雑把に書かれているため、各ループは1のみを行うため、いくつかの境界チェックを行うことができます。can't change data input size
(要素の追加/削除)を繰り返します。あなたのコードはそれを簡単に壊しません。データをフィルタリングまたは変換する必要がある場合は、他のループを使用します。iterable interface
(Java)またはIEnumerable
(c#)を拡張します。boiler plate
、たとえばXMLを解析する場合、SAXとStAXの違いは、最初に要素を参照するDOMのメモリ内コピーが必要です。リストごとにそれぞれのアイテムを検索している場合、ほとんどの場合、間違って検索していることに注意してください。一緒に検索をスキップするには、hashmap
またはbimap
の使用を検討してください。
プログラマーがfor loop
なので for each
イテレータを使用すると、要素をスキップするという一般的なバグが存在します。そのため、そのシーンではより安全です。
for ( Iterator<T> elements = input.iterator(); elements.hasNext(); ) {
// Inside here, nothing stops programmer from calling `element.next();`
// more then once.
}
少なくともJavaでforeachを使用しない理由の1つは、最終的にガベージコレクションされるイテレータオブジェクトを作成することです。したがって、ガベージコレクションを回避するコードを作成する場合は、ただし、イテレータを作成しないため、純粋な配列の場合は問題ありません。
foreach
で必要なことを実行できる場合は、それを使用します。そうでない場合-たとえば、何らかの理由でインデックス変数自体が必要な場合-for
を使用します。シンプル!
(そして、あなたの2つのシナリオは、for
またはforeach
のどちらでも同様に可能です。)
foreachは、重いコレクションを実装するために桁違いに遅くなります。
証拠があります。これらは私の発見です
次の簡単なプロファイラーを使用して、パフォーマンスをテストしました
static void Main(string[] args)
{
DateTime start = DateTime.Now;
List<string> names = new List<string>();
Enumerable.Range(1, 1000).ToList().ForEach(c => names.Add("Name = " + c.ToString()));
for (int i = 0; i < 100; i++)
{
//For the for loop. Uncomment the other when you want to profile foreach loop
//and comment this one
//for (int j = 0; j < names.Count; j++)
// Console.WriteLine(names[j]);
//for the foreach loop
foreach (string n in names)
{
Console.WriteLine(n);
}
}
DateTime end = DateTime.Now;
Console.WriteLine("Time taken = " + end.Subtract(start).TotalMilliseconds + " milli seconds");
そして、私は次の結果を得ました
所要時間= 11320.73ミリ秒(ループ用)
所要時間= 11742.3296ミリ秒(foreachループ)
ほとんどのアイテムがカバーされているように見えます...以下は、特定の質問に関して言及されていない追加のメモです。これらは、スタイル設定とは対照的に、厳しいルールです。
リスト内の各アイテムに対して操作を実行したい
foreach
ループでは、反復変数の値を変更できません。したがって、リスト内の特定のアイテムの値を変更する場合は、for
を使用する必要があります。
また、「クール」な方法はLINQを使用することです。興味があれば検索できるリソースがたくさんあります。
きれいなコードについて言えば、foreachステートメントはforステートメントよりもずっと読みやすいです!
Linq(C#)はほぼ同じことができますが、初心者の開発者はそれらを読むのに苦労する傾向があります!