C# 初級講座
VB2019 Visual Studio 2022

第22回 List と foreach

2022/11/13

この記事が対象とする製品・バージョン

Visual Studio 2022 Visual Studio 2022 対象です。
Visual Studio 2019 Visual Studio 2019 対象です。
Visual Studio 2017 Visual Studio 2017 対象外ですが、参考になります。
Visual Studio 2015 Visual Studio 2015 対象外ですが、参考になります。
Visual Studio 2013 Visual Studio 2013 対象外ですが、参考になります。
Visual Studio 2012 Visual Studio 2012 対象外ですが、参考になります。
Visual Studio 2010 Visual Studio 2010 対象外ですが、参考になります。
Visual Studio 2008 Visual Studio 2008 対象外ですが、参考になります。
Visual Studio 2005 Visual Studio 2005 対象外ですが、参考になります。
Visual Studio.NET 2003 Visual Studio.NET 2003 × 対象外です。
Visual Studio.NET 2002 Visual Studio.NET (2002) × 対象外です。
  Visual Studio Code 対象外ですが、参考になります。

 

目次

1.Listと配列

1-1.Listの概要

複数の値をひとまとめに管理できる仕組みを少し整理しましょう。

配列とコレクションがその代表です。他にも C# には このような仕組みがいくつかあります。上の図はかなり大雑把ですが関連する仕組みを分類してみたものです。

コレクションには List、 Dictionary、Stack、Queue (読み方:List=リスト、Dictionary=ディクショナリー、Stack=スタック、Queue=キュー)などいろいろな種類があります。最もよく使われるのは List と Dictionary です。

配列とコレクションは IEnumerableインターフェース(読み方:アイエニューマラブル)を実装しているという特徴があります。IEnumerableやインタフェースについては初級講座のもっと後の回で扱う予定です。この特徴を持つオブジェクトは今回説明する foreach を使用できます。(foreachを使用できるものはこれに限りませんが、大部分はこれです。)

 

今回の主役は List です。

List の単純なプログラム例も振り返っておきます。

var countries = new List<string>();
countries.Add("アラビア");
countries.Add("インド");
countries.Add("ウズベキスタン");

System.Diagnostics.Debug.WriteLine(countries[2]); //ウズベキスタン と出力されます。

Debug.WriteLineで出力される場所

この例では「アラビア」、「インド」、「ウズベキスタン」という3つの値を1つの変数 countries で保持しています。

 

1-2.配列との違い

Listと同じようなことは配列でもできますが、Listの方が扱いやすいことが多いです。

試しに上の例を配列を使って書き換えて比べてみましょう。

var countries = new string[3];
countries[0] = "アラビア";
countries[1] = "インド";
countries[2] = "ウズベキスタン";

System.Diagnostics.Debug.WriteLine(countries[2]); //ウズベキスタン と出力されます。

Debug.WriteLineで出力される場所

配列は使う前に要素の数を決める必要があります。[3] がそれです。Listの場合はその必要がないので何も考える必要がなく楽です。

さらに、この例には登場しませんが、後になって4つ目の要素や5つ目の要素が必要になった時、配列では要素数を増やすために Array.Resizeメソッドを呼び出す必要があり面倒です。一方、Listの場合は、もともと要素数を指定する必要がなく自動的に拡張されるので、こういうことも考える必要がありません。

自動的に要素数が拡張されるためこのように事前に指定する必要はありません。

要素を追加するときは、配列の場合、何番目に追加するのかいちいち [0] や [1] のようにインデックスを指定する必要がありますが、List の場合、単純に Add などのメソッドを呼び出すだけで楽です。

一方、保持している要素を取得する場合は配列もListも同様に何番目の要素であるかインデックスで指定します。最後に「ウズベキスタン」を取得するところでは、どちらも countries[2] と書いています。

List以外のコレクションの場合、使い方は少し異なりますが、だいたい同じ発想で操作できるようになっており配列より楽です。

配列との比較を表にまとめておきます。この表に記載しているのは一例です。後で説明するものも含めています。

  配列 List
要素の数 使う前に決める
var names = new string[3];
(何もする必要なし)
要素数の拡張 Array.Resize(ref names, 100); (何もする必要なし)
要素の型 宣言時に指定する
var names = new string[3];
宣言時に指定する
var names = new List<string>();
要素の追加 インデックスを指定して追加する
names[0] = "A";
names[1] = "B";
Addメソッドを使用する
names.Add("A");
names.Add("B");
要素の挿入
(途中への追加)
(通常の操作ではできない) Insertメソッドを使用する
names.Insert(1, "X"); //2番目に挿入
names.Insert(3, "Y"); //4番目の挿入
要素の削除 (通常の操作ではできない) Removeメソッド か RemoveAtメソッドを使用する。
names.Remove("B"); //"B"を削除
names.RemoveAt(1); //2番目を削除
要素の検索 Containsメソッドで存在確認。
Array.IndexOfメソッドで位置を取得。
Containsメソッドで存在確認。
IndexOfメソッドで位置を取得。
要素の取得 string name = names[0]; string name = names[0];

 

1-3.List と 配列の変換

List と 配列はよく似ているのでお互いに簡単に変換できます。

List を配列に変換するには ToArray メソッドを使用します。

var list = new List<string>();
list.Add("アラビア");
list.Add("インド");
list.Add("ウズベキスタン");

//listの内容をそっくりコピーした配列 array を生成します。
string[] array = list.ToArray();

 

配列をListに変換するには ToListメソッドを使用します。

var array = new string[3];
array[0] = "アラビア";
array[1] = "インド";
array[2] = "ウズベキスタン";

//配列 array の内容をそっくりコピーしたListを生成します。
List<string> list = array.ToList();

 

ToArray と ToList はListと配列以外でも使用できる場合があり、簡単に対象を List や 配列に変換できます。

 

1-4.List と 配列 をどう使い分ければよいのか?

どちらを使うべきか迷ったら List を使ってください。

  配列 List
迷ったときは   List を使います。
プログラムは楽? 楽ではない
処理は速い? だいたい速い だいたいの場合配列より遅いけど、気にするほどではない。
歴史的には (多分)古い (多分)新しい

 

List と 配列は似たようなものなので、どちらを使っても目的を達成できる場合が多いです。Listを選択しておけば、プログラムが楽になります。

フレームワークの機能の中には配列を引数にとったり、戻り値で返すものがあります。その場合でも、自分ではListを作っておいて、必要に応じて ToArray や ToList で 変換するのが楽です。つまり、配列を選択する場合はほとんどありません。特に処理する内容がなくて配列でもプログラムが楽ならば、配列をそのまま扱うのもアリです。

 

配列を選択する数少ない理由の1つは、性能が非常に重視されている場合です。配列とListでは、配列の方が少しだけ処理速度が速いです。通常、この速度の差は問題になりません。プロが作成するアプリケーションや業務システムでも普通は気にしないレベルです。

一部の種類のプログラムでは、このほんのちょっとの速度の差が重要になる場合があります。画像や動画などの大規模なバイナリーデータを処理する場合や、リアルタイムで稼働するゲームや通信を大量に処理する場合などです。こういった処理ではほんのちょっとの差が積み重なって大きな差になります。このような場合は、配列を選択することがあります。

なお、要素数自体を拡張・縮小するような処理はListの方が高速です。常に配列の方が高速というわけでもないので、良く知らない初心者が速くしたいという理由だけで配列を採用するのはたいてい良いアイディアではありません。良く知らない初心者の場合、配列かListかの選択以外にもっと速度に影響する何かがきっとあるはずです。

 

歴史的には、配列の方が多分古いです。配列とリストはどちらもC#登場以前からありました。C#の歴史のうえでは2002年のバージョン1の時点で配列は存在し、Listは存在しませんでした。ただ、Listとほぼ同じ動作をする ArrayList というクラスを使用できました。ArrayListは今(2022年)でも使えますが、Listの方が優れているためArrayListを今選択する理由はありません。当初のC#には配列以外に要素の型を指定するする手段がなく、ArrayListはすべての要素をobject型で扱います。2005年にジェネリックという機能が導入され、要素の型を指定することが可能になりました。これに伴ってArrayListにとって変わって List が登場し、今に至ります。

配列 C# 1.0 (2002年)からありました。
当時、要素の型を指定できる唯一の手段でした。
ArrayList C# 1.0 (2002年)からありました。
今(2022年)で言う List のような存在ですが、要素の型を指定する手段がなかったため
すべての要素は object 型でした。
List 2005年に登場しました。
ジェネリック機能の導入に伴い、要素の型を指定できるようになったので、ArrayListを駆逐しました。

 

配列は連続したメモリを確保して、同じサイズで区切って値を格納するので、個々の値へのアクセスが非常に高速です。(n番目の値がメモリのどの位置に存在するか簡単な計算ですぐわかります。)

この特質上、配列は最初にメモリをどのくらい確保する必要があるのかを示す必要があります。それが C# では最初に要素数を指定する必要がある理由です。また隣のメモリ領域に何があるのかわからないので最初に指定した以上の要素数を確保することは本質的に不可能、要素の挿入や削除など確保しているメモリサイズの変更が必要になる操作も本質的に不可能です。プログラムで疑似的に配列を拡張したり、配列の要素数を拡張したり削除ということは可能ですが、それは実際には新しい配列を作成しており、非効率的な処理です。

昔のプログラミング言語には配列しかないものがあり、配列の方が基本的な存在と認識されています。

 

2.Listの使い方

2-1.List の生成

List はシンプルで汎用的なコレクションです。Listを使えば配列でできることはほとんど可能であり、しかも配列より使いやすいです。

このクラスは System.Collections.Generic 名前空間に属しています。この名前空間には List の他にも代表的なコレクションが属しています。

既に説明していることもありますが、基本的な使い方をまとめておきます。

まず、Listのインスタンスを作成するときには、< > を使って要素の型を指定します。

たとえば、string型の要素を持つListを作る場合は、このようにします。


var myList = new List<string>();

int 型ならこのようにします。


var myList = new List<int>();

もちろん、普通の C# のルールに従うので、var ではなく、型の名前を使った宣言もできます。


List<string> myList = new List<int>();

.NET 5以上であれば、同じ型名を繰り返さないで簡略化した new も可能です。これも List の機能ではなく、C#の全般的な機能です。


List<string> myList = new();

 

2-2.要素の追加

追加したい要素が初めから決まっているならば、コレクション初期化子という特別な構文を使えます。


var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

ソースコード中、区切りの位置では自由に改行できるので、1行が長くなる場合こんな風に書くことを好む人たちもいます。

var stars = new List<string> { 
                                 "地球", 
                                 "火星", 
                                 "スピカ", 
                                 "ベテルギウス"
                             };

 

コレクション初期化子を使わないで項目を追加するなら、既に何度も出てきているように Add メソッドを使うのが一般的です。

var stars = new List<string>();

stars.Add("地球");
stars.Add("火星");
stars.Add("スピカ");
stars.Add("ベテルギウス");

//Listの内容を , で結合して 「地球,火星,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

この例の場合、stars は List<string> で宣言されているので、 Add で追加できるのは string つまり、文字列です。

なお、string.Join は文字列型のListの内容を連結して1つの文字列にできる便利なメソッドです。

 

AddRangeメソッド(読み方:AddRange=アドレンジ)を別の配列の要素をまるごとコピーしてListに追加できます。

var planets = new string[] { "地球", "火星" };
var fixeds = new string[] { "スピカ", "ベテルギウス" };

var stars = new List<string>();
stars.AddRange(planets); //「地球」「火星」をコピーして追加
stars.AddRange(fixeds); //「スピカ」「ベテルギウス」をコピーして追加

//Listの内容を , で結合して 「地球,火星,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

 

配列を直接AddRangeの引数に指定する書き方もできます。これはまるでコレクション初期化子を後から使うような感じに使えます。

var stars = new List<string>();
stars.AddRange(new string[] { "地球", "火星", "スピカ", "ベテルギウス"}); 

//Listの内容を , で結合して 「地球,火星,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

 

AddRangeには別のListの内容をコピーして追加する機能もあります。さきほどの例と違って planets と fixeds が List であることにご注意ください。

var planets = new List<string> { "地球", "火星" };
var fixeds = new List<string> { "スピカ", "ベテルギウス" };

var stars = new List<string>();
stars.AddRange(planets); //「地球」「火星」をコピーして追加
stars.AddRange(fixeds); //「スピカ」「ベテルギウス」をコピーして追加

//Listの内容を , で結合して 「地球,火星,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

 

Add や AddRange は List の最後に要素を追加します。

途中に要素を追加したい場合は Insertメソッド (読み方:Insert=インサート)を使用します。

var stars = new List<string>();

stars.Add("火星");
stars.Add("スピカ");
stars.Add("ベテルギウス");

System.Diagnostics.Debug.WriteLine(stars[0]); //火星

stars.Insert(0, "地球"); //0番目の要素として「地球」を挿入

System.Diagnostics.Debug.WriteLine(stars[0]); //地球

Debug.WriteLineで出力される場所

なお、InsertRange というメソッドを使うと AddRange と同じように複数の要素を挿入できます。

 

2-3.要素の削除

要素を削除したい場合は Removeメソッド か RemoveAtメソッド です(読み方:Remove=リムーブ、RemoveAt=リムーブアット)。

Removeは値を指定して要素を削除します。次の例は「火星」が消えます。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

stars.Remove("火星");

//Listの内容を , で結合して 「地球,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

Listには同じ値を複数含むことができることにご注意ください。「火星」が複数ある場合、Removeメソッドで削除されるのは最初の「火星」だけです。

 

RemoveAt はインデックスを指定して要素を削除します。次の例は インデックスが1の要素(=火星)が消えます。インデックスは0から始まるので、この例では「地球」がインデックス0,火星がインデックス1です。火星を削除すると繰り上がって、スピカがインデックス1になります。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

stars.RemoveAt(1);

//Listの内容を , で結合して 「地球,スピカ,ベテルギウス」と出力します。
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

 

2-4.要素の変更

追加(Add,AddRange)・挿入(Insert,InsertRange)・削除(Remove, RemoveAt) を紹介しました。ところで、要素の変更はどうやってやるのでしょうか?これには、特別なメソッドは必要ありません。ただ普通の変数のように扱うだけです。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

stars[0] = "水星"; // 「地球」が「水星」になります。
stars[2] += "(おとめ座)"; //「スピカ」が「スピカ(おとめ座)」になります。
stars[3] = "オリオン座の" + stars[3]; //「オリオン座のベテルギウス」

 

2-5.要素の検索

Containsメソッド(読み方:Contains=コンテインズ)を使うと、要素がListに含まれているか判断できます。

スピカが含まれているかは次のように判断できます。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

if (stars.Contains("スピカ"))
{
    System.Diagnostics.Debug.WriteLine("スピカは含まれています。");
}

Debug.WriteLineで出力される場所

 

あまり、出番はありませんが、何番目に含まれているかまで知りたい場合は IndexOf (読み方:IndexOf=インデックスオブ)を使います。要素が含まれていない場合、IndexOf は -1 を返します。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

int index = stars.IndexOf("スピカ");

if (index >= 0)
{
    System.Diagnostics.Debug.WriteLine($"スピカは {index} 番目に含まれています。");
}

Debug.WriteLineで出力される場所

複数の「スピカ」がList内に存在する場合、この例のようにIndexOf を使うと最初のスピカの位置を返します。LastIndexOfメソッド(読み方:LastIndexOf=ラストインデックスオブ)はIndexOfと同じような使い方ができますが、最後のスピカの位置を返します。途中のスピカを調べたいときは何番目のインデックスから検索を開始するか指定できるIndexOfのオーバーロードが使用できます。

 

2-6.要素の並び替え

並び変えたいときは Sort メソッド(読み方:Sort=ソート)です。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

stars.Sort();

//スピカ,ベテルギウス,火星,地球
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

Sortメソッドでは引数を使って、独自の並び順を指定することもできますが、初心者向けの使い方ではありません。

 

最後に、Reverseメソッド(読み方:Reverse=リバース)を紹介します。これはListの内容を逆順にします。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

stars.Reverse();

//ベテルギウス,スピカ,火星,地球
System.Diagnostics.Debug.WriteLine(string.Join(",", stars));

Debug.WriteLineで出力される場所

 

この他にもたくさんの機能があります。

 

2-7.デバッグ時の List の内容の確認方法

プログラムが思ったように動作しない場合、プログラムを一時停止させて、何が起こっているかいろいろと調べることができます。このようにうまくうごかないプログラムを調査して修正していく作業を「デバッグ」と呼びます。

プログラムを一時停止する一番簡単な方法はブレークポイントの設定です。ブレークポイントを設定すると、実行がその行に到達した時に自動的に実行が一時停止します。

この赤茶色の●がブレークポイントです。有効なブレークポイントが設定されている行は赤茶色の色で表示されます。

ブレークポイントを設定するには、この画像の赤茶色の丸の位置をマウスでクリックするか、設定したい行にカーソルがある状態でキーボードの F9 を押します。(キーボードの設定によっては異なる場合があります。)

ブレークポイントがある状態でプログラムを実行して、実行がブレークポイントの行に到達すると、その行が黄色くなって実行が一時停止します。

この状態のとき、黄色の行はまだ実行されていません。

この状態で変数の上にマウスをホバーすると、変数の内容を確認することができます。これで List の中身もわかります。

この動画のようにデバッグツールバーのステップインボタンをクリックするか、キーボードのF11を押すとこの状態から1行だけ実行されます。(キーボードの設定によっては異なる場合があります。)

デバッグルールバーは、一時停止すると自動的に表示されます。もし表示されない場合は、[表示]メニューから[ツール バー] - [デバッグ] を選択してください。

これでもう1度Listの内容を見るとちゃんと変わっているのがわかりますね。

他にもローカルウィンドウで変数の内容を確認することもできます。このウィンドウも一時停止すると自動的に表示されます。もし表示されない場合は、[デバッグ]メニューから[ウィンドウ] - [ローカル]を選択してください。

 

3.List の列挙

3-1.foreach

Listの要素をすべて列挙する方法はいくつかあります。for や foreach (読み方:foreach=フォーイーチ)などの構文を使ったり、ForEachメソッド(読み方:ForEach=フォーイーチ)を使うなどの方法があります。

列挙とは、各要素を順番に取得することです。たいていはそれぞれの要素を使って何かの処理を行います。説明用のサンプルでは単にそれぞれの要素を表示したり出力したりするものが多いです。

ここでも話を列挙に絞るため、処理自体はDebug.WriteLineを使った単純な出力だけを行うことにしてみます。

まずは、foreachの構文を使った列挙の例を紹介します。foreachは重要な構文なのでいつでも使えるように習得しておく必要があります。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

//変数 star は型推論により string型になります。
foreach (var star in stars)
{
    System.Diagnostics.Debug.WriteLine(star);
}

Debug.WriteLineで出力される場所

foreach はListの要素を1個ずつ取り出して { } 内の処理を実行します。 取り出した要素はforeachの構文内で宣言された変数に格納されます。上記の例では star です。{ } 内の処理が1行しかない場合は { } は省略できますが、常に { } を付けるようにしておいたほうが統一感がありわかりやすいので、私はお勧めです。

結果として、この例では、まず "地球" が取り出され、変数 star に格納されます。そして、 { } 内の処理が実行されます。 { } 内では star を出力する処理が記述されているので "地球" が出力されます。次に "火星" が取り出され、同様に出力されます。これを全部の要素で繰り返します。

最終的には次のように出力されます。

地球
火星
スピカ
ベテルギウス

取り出した結果を格納する変数(上記の例では star) は var で宣言して型推論させることが多いです。この例では stars が stringを要素に持つListとして宣言されているので、ここからvar star は string型に決定します。自分で string star のように明示的に型を記述することもできます。型をわかりやすく書くためにあえて var ではなく、具体的な型を書くこともあります。

 

foreach は List に専用の構文というわけではなく、配列やList以外のコレクションなど、複数の値を持つもののほとんどに適用できます。

そのうえ、foreach は for と比べて使い方シンプルなのが特徴です。foreach と for のどちらでも処理が記述できる場合は、シンプルな方の foreach を採用しましょう。

なお、説明は割愛しますが、foreach でも for と同じように break と continue が使用できます。

 

3-2.for

for でも同じ処理を記述できます。少し複雑になるのでお勧めはしません。比較のために例を載せておきます。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

for (int i = 0; i < stars.Count; i++)
{
    var star = stars[i];
    System.Diagnostics.Debug.WriteLine(star);
}

Debug.WriteLineで出力される場所

Listの要素の数は Count プロパティで取得できます。カウンター変数 i が start.Count より小さい限り処理を繰り返すように記述することで、全要素を対象にすることができます。

for は foreachと違って、今何周目を処理しているかをカウンター変数を使って簡単に取得できるので、これが必要な場合は for を採用することがあります。

 

3-3.ForEachメソッド

foreachの構文とほぼ同じことができる ForEach というメソッドがあります。

次の例は上記の例と同じで、starsの全要素を出力します。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

//変数 star は型推論により string型になります。
stars.ForEach(star => System.Diagnostics.Debug.WriteLine(star));

Debug.WriteLineで出力される場所

ちょっと、わかりやすく改行したり { } を補って、次のように書いても同じです。

var stars = new List<string> { "地球", "火星", "スピカ", "ベテルギウス"};

//変数 star は型推論により string型になります。
stars.ForEach(star =>
{
    System.Diagnostics.Debug.WriteLine(star);
});

Debug.WriteLineで出力される場所

ForEachメソッドの引数には「値」ではなく、「処理」を指定します。C# では「処理」を指定するときにラムダ式という機能が便利に使えます。

この例では、ForEachメソッドの引数に 「Debug.WriteLine を実行する処理」をラムダ式で指定しています。 => という見慣れない演算子は「アロー演算子」と呼び、これが出てくるとラムダ式が使われている合図です。

ラムダ式については、初級講座のもっと後の方で説明する予定です。最近(2022年現在)では、ラムダ式を使って処理を記述するのが人気があり、foreachの構文でできることを、ForEachメソッドなどのラムダ式で書いてしまう人もそこそこいるように感じます。

 

4.foreachのあれこれ

4-1.Visual Studio の入力支援

基本的な構文は Visual Studio の入力支援を受けることができます。

foreach の場合、キーボードから foreach と入力して TAB を2回押すと自動的に構文が挿入されます。その後、変数の部分などを自分のプログラムに合うように変えれば簡単にプログラムできます。

この動画では 「fore」まで入力して TAB を2回押しています。はじめ var が選択された状態になります。型を明示する場合はキーボードから型を入力します。今回は var のままで TAB を押しています。変数名は star にしたいのでキーボードから入力しました。ここでもう1度 TAB を押すと 列挙する対象を指定するところに飛ぶので stars と入力しました。

マウス操作でも似たようなことができます。コードエディターを右クリックして、[スニペット] - [ブロックの挿入] から foreach をダブルクリックするとforeachの構文が挿入されます。

 

4-2.foreach内でListの変更はできません

foreach の処理の中で Listの要素の追加や削除などはできません。

たとえば、数値型のListから50以下の要素を削除したい場合、次のようにはプログラムできません。

var values = new List<int> { 81, 32, 45, 76, 10, 59, 2 };

foreach (var value in values)
{
    if (value <= 50)
    {
        //この処理を実行すると例外が発生します。
        //foreach内でListの要素を追加・削除できません!
        values.Remove(value);
    }
}

System.Diagnostics.Debug.WriteLine(String.Join(",", values));

Debug.WriteLineで出力される場所

私の環境でVisual Studio 200で実行すると、Removeメソッドを実行した次の周の開始直前 foreach の行の 「in」を強調した状態で実行が停止し、次の例外が出力されました。

System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'

日本語に訳すと「コレクションが修正されたので、列挙操作の実行はできません。」というような内容です。

 

この問題の最も簡単な解決方法は、対象のList自体ではなく、そのListをコピーしたものを列挙することです。ToList メソッドを使用すると List のコピーが簡単に作れます。こうすると列挙しているListと変更しているListは別の存在になるので、列挙中に変更できます。次の例は想定通り動作します。

var values = new List<int> { 81, 32, 45, 76, 10, 59, 2 };

foreach (var value in values.ToList())
{
    if (value <= 50)
    {
        //この処理を正常に動作します。
        values.Remove(value);
    }
}

System.Diagnostics.Debug.WriteLine(String.Join(",", values));

 

なお、この例の場合に限って言うとforeachの構文を使わないで RemoveAll メソッド(読み方:RemoveAll=リムーブオール)を使って同じことをもっと短いプログラムで実現できます。

var values = new List<int> { 81, 32, 45, 76, 10, 59, 2 };

values.RemoveAll(value => value <= 50);

System.Diagnostics.Debug.WriteLine(String.Join(",", values));

RemoveAllメソッドも引数にラムダ式をとります。ここでは、削除する要素を判定する処理(value <= 50)をラムダ式として指定しています。

前述したようにラムダ式については初級講座のもっと後の方で扱う予定なので、現時点ではこんな書き方もあるんだなぁくらいに見ておいていただければと思います。

 

4-3.ファイルの列挙

少し実践的なforeachの例も紹介しておきます。

foreach は配列にもListにもその他の似たような物にも使用できて、含まれているものすべてを列挙するので、何かしら複数のものを処理するときにはたいてい役立ちます。

次の例では C:\Window にあるファイルを列挙します。ファイルやフォルダーを列挙して何か処理をするというのはよくあります。

この例では、更新日付が1年以上古いファイルを出力します。ただし、システムファイルは除外します。

var folder = new System.IO.DirectoryInfo(@"C:\windows");

foreach(System.IO.FileInfo file in folder.EnumerateFiles())
{

    if (file.Attributes.HasFlag(FileAttributes.System))
    {
        //システムファイルは対象外
        continue;
    }


    if (file.LastWriteTime < DateTime.Now.AddYears(-1))
    {
        System.Diagnostics.Debug.WriteLine(file.FullName);
    }
}

ファイルの一覧を取得する方法はいくつかあります。この例ではDirectoryInfoクラス(読み方:DirectoryInfo=ディレクトリーインフォ)の EnumerateFilesメソッド(読み方:EnumerateFiles=エニューマレイトファイルズ)を使っています。

EnumerateFilesメソッドを使うと、それぞれのファイルが FileInfo型で取得されるのでこれを foreach を使って1個ずつ処理します。foreach の行ではFileInfo型を明示して変数fileを宣言しています。この部分は var file と書いて型推論させてもOKです。ただ、そうすると、はじめて見る人がstringなのか何なのかわかりにくいと思ったので、あえてFileInfo型を明記しました。

ファイルの処理については、今回説明したいテーマではないのですが、せっかくなので簡単に補足しておきます。

FileInfoクラスは1つのファイルに関するさまざまな情報や操作を行える便利なクラスです。ファイルをコピーしたり移動したり削除したり、更新日や作成日を取得したり、といった操作が可能です。

ここでは、まず Attributesプロパティ(読み方:Attributes=アトリビュートス)を使って、属性を取得し、システムファイルかどうか確認しています。システム属性の場合 continue を使用して、そのファイルは飛ばして次のファイルを処理します。

2つ目の if 文では 最終更新日を LastWriteTimeプロパティ(読み方:LastWriteTime=ラストライトタイム)で取得して、これが今から1年前より古いかどうか判断しています。古ければ FullName(読み方:FullName=フルネイム)を出力します。FullNameとはパスも含んだファイル名で、たとえば、C:\windows\hh.exe のようなものです。

 

ところで、DirectoryInfo.EnumerateFilesメソッドの戻り値は実は List ではありません。実際にこのメソッドを呼び出すと FileSystemEnumerable という型の値を受け取りますが、この型は気にしないで良いことになっています。多分、C#にすごく詳しいプロの人に聞いても EnumerateFilesメソッドがFileSystemEnumerable型の値を返すということは即答できないと思います。私も即答できません。

リファレンスにも書いていません。気にするなということです。

DirectoryInfo.EnumerateFiles メソッド (System.IO) | Microsoft Learn

なんで気にしなくてよいかというと、foreach で1個ずつ要素を返してくれるからです。そうであれば、それがListであろうが、その他の何かであろうが同じようにプログラムできるので、型の名前などどうでもよいではありませんか。

ToListメソッドやToArrayメソッドも使えるのでどうしても必要であれば、Listや配列に変換することもできます。FileSystemEnumerable に限らずこのような存在はたくさんあります。

 

 

 

5.練習問題

問1.次のプログラムを作りたい場合、配列とListのどちらを採用すべきでしょうか?
・要件1:ユーザーは複数のファイルを指定します。
・要件2:後でファイルを追加したり削除したりできます。
・要件3:ファイルの指定が完了すると、指定されたファイルをクラウドに送信します。
配列
List

 

問2.コレクション初期化子の使い方で正しいものはどれでしょうか?
① var values = new List<int>(1, 2, 3);
② var values = new List<int>{1, 2, 3};
③ var values = new List<int>[1, 2, 3];

 

問3.Environment.GetLogicalDrivesメソッドは、CドライブやDドライブなどのドライブの配列を返します。これを利用して、実行環境で有効なドライブの一覧を出力するプログラムを作ります。□にあてはまるのは何でしょうか?
□ (string drive in Environment.GetLogicalDrives())
{
    System.Diagnostics.Debug.WriteLine(drive);
}
for
foreach
for each

 

次の回は、もう1つのよく使われるコレクション Dictionary を扱います。

C#初級講座講座 次の回へ