List<T> は、要素の追加や削除に合わせて伸び縮みする、順序付きのコレクションです。System.Collections.Generic にあり、配列と同じようにインデックスでアクセスでき(list[0])、C#で「ものの集まり」を扱うときの標準的な選択肢です。
リストの作成と要素の追加
山かっこの中の型引数が、リストが何を保持するかを表します:List<string>、List<int>、List<Order>。
出力:
2
Zoe, Ana, Ben, Chloe, Dev
Ana
Ana B.
3
Count はメソッドではなくプロパティです(配列は Length を使い、LINQには Count() メソッドがありますが、リストではプロパティを使います)。Insert は後ろの要素をすべて1つずつずらすので、大きなリストの先頭に挿入すると、サイズに比例した時間がかかります。末尾への Add は低コストな操作です。
要素の削除
要素について何がわかっているかに応じて、削除の方法は4つあります。
出力:
True: 85, 62, 40, 91, 55
False
62, 40, 91, 55
2 removed: 62, 91
Caught ArgumentOutOfRangeException
0
Remove は値を受け取り、最初に一致したものだけを削除します。ある値のすべてのコピーを削除するには RemoveAll(s => s == 40) を使います。リストで不正なインデックスを使うと、list[i] でも RemoveAt(i) でも ArgumentOutOfRangeException が投げられます(配列では代わりに IndexOutOfRangeException)。RemoveRange(index, count) はまとまった範囲を削除します。
CountとCapacity
リストは要素を内部の配列に保持しています。Capacity はその配列のサイズで、Count は使われている枠の数です。Count が Capacity に達すると、次の Add が2倍の大きさの配列を確保し、すべてをコピーします。
出力:
Count 0, Capacity 0
Count 1, Capacity 4
Count 2, Capacity 4
Count 3, Capacity 4
Count 4, Capacity 4
Count 5, Capacity 8
Count 6, Capacity 8
Count 7, Capacity 8
Count 8, Capacity 8
Count 9, Capacity 16
Count 0, Capacity 1000
2倍にしていくので、ほとんどの Add の呼び出しは空いている枠に書き込むだけです。おおよその要素数がわかっているなら、コンストラクターにその数を渡すと途中のコピーを省けます。new List<int>(1000) でも Count は0なので、list[0] は例外になることに注意してください。容量は場所であって、要素ではありません。
検索:Contains、IndexOf、Find、Exists
出力:
True
2
-1
32.00
2 cheap items
False
2
True
False
Find は最初に一致したものを返し、何も一致しなければその型の既定値(クラスなら null)を返すので、結果を使う前に確認します。最後の行が False と表示されるのは、Contains が Equals を使い、Equals をオーバーライドしていないクラスは参照を比較するからです。同じフィールドを持つ新しい Product は別のオブジェクトです。代わりに Exists や Find でプロパティによって検索するか、クラスで Equals と GetHashCode をオーバーライドします。
これらのメソッドはどれもリストを先頭からたどるので、各呼び出しはO(n)です。キーによる検索を繰り返すなら、Dictionary なら定数時間で答えが出ます。
リストの並べ替え
Sort() は要素の自然な順序でその場で並べ替えます。他の基準で並べ替えるには、比較のラムダを渡します。
出力:
1, 2, 5, 9
9, 5, 2, 1
Ben 95, Ana 120, Chloe 150
Chloe 150, Ben 95, Ana 120
Ben 95, Ana 120, Chloe 150
比較は、a を先にすべきなら負の数、b を先にすべきなら正の数、同順ならゼロを返します。CompareTo はまさにそれを返します。実際には Sort とLINQの OrderBy の2つの違いが重要です。
Sortはリストを変更します。OrderByは新しいシーケンスを返し、リストには触れません。Sortは安定ではありません。等しいと比較される要素が入れ替わることがあります。OrderByは安定で、ThenByで2つ目のキーを追加できます:players.OrderBy(p => p.Score).ThenBy(p => p.Name)。
自分で作ったクラスのリストに比較なしで Sort() を呼ぶと、リストは要素の並べ方を知らないので InvalidOperationException が投げられます。比較を渡すか、クラスに IComparable<T> を実装します。
Reverse() はその場で反転します。System.Linq にも Reverse 拡張メソッドがありますが、List<T> の list.Reverse() は何も返さないその場で反転する版が選ばれます。
ループと反復中の削除
foreach はすべての要素を順に読みます。for ループならインデックスも得られます。できないのは、同じリストの foreach の途中で要素を追加したり削除したりすることです。
出力:
Caught InvalidOperationException
120, 80, 60
120, 80, 60
例外は InvalidOperationException で、メッセージは「Collection was modified; enumeration operation may not execute.」です。前から進む for ループは例外を投げませんが、削除のたびにすべてが1つ前のインデックスにずれるので、削除した要素の直後の要素を何も言わずに飛ばしてしまいます。逆順にループすればこれを避けられます。3つ目の直し方は、コピーを反復することです:foreach (var t in orders.ToList())。
foreach の中で要素のプロパティを変更すること(たとえば order.Status = "sent")は問題ありません。要素の追加や削除、そしてインデクサーによる置き換え(orders[i] = 0)は列挙子を壊します。
リストと配列の変換
出力:
4
3
4, 4, 4, 4
Oslo / Lima / Pune / Kyiv
どちらの方向も要素を新しい格納場所にコピーするので、後でリストを変更しても配列には影響しません。ConvertAll は、LINQの Select(...).ToList() のリスト版です。Console.WriteLine(list) でリストを表示すると型名(System.Collections.Generic.List`1[System.String])が表示されるので、string.Join を使います。
早見表
| やりたいこと | コード |
|---|---|
| 作成 | var l = new List<int>(); または new List<int> { 1, 2 } |
| 末尾に追加 | l.Add(x)、l.AddRange(items) |
| 位置を指定して挿入 | l.Insert(i, x) |
| 最初に一致したものを削除 | l.Remove(x)(bool を返す) |
| インデックスで削除 | l.RemoveAt(i) |
| 条件で削除 | l.RemoveAll(x => ...)(削除数を返す) |
| 要素数 | l.Count |
| 含まれているか | l.Contains(x)、l.Exists(x => ...) |
| 検索 | l.Find(...)、l.FindAll(...)、l.FindIndex(...) |
| その場で並べ替え | l.Sort()、l.Sort((a, b) => ...) |
| 並べ替えたコピー | l.OrderBy(x => ...).ToList() |
| 配列に変換 | l.ToArray() |
よくある間違い
foreachの中で削除する。InvalidOperationExceptionが投げられます。RemoveAllか逆順のforループを使います。Remove(x)がすべてのコピーを削除すると思う。 削除するのは最初の1つです。RemoveAllを使います。EqualsのないオブジェクトにContainsを使う。 参照が比較されます。Existsでプロパティによって検索します。- 容量を指定して作ったリストにインデックスでアクセスする。
new List<int>(10)は空です。先に要素を追加します。 Sortが同順の要素の順序を保つと思う。 保ちません。等しい要素の順序が重要ならOrderByを使います。- リストを共有するつもりで代入し、変更に驚く。
var copy = list;は同じリストです。new List<T>(list)がコピーです。
よくある質問
C#のListに要素を追加するには?
list.Add(item) は末尾に1つ追加し、list.AddRange(otherCollection) は複数を追加し、list.Insert(index, item) は指定した位置に要素を置いて残りを後ろにずらします。作成時に埋めることもできます:var names = new List<string> { "Ana", "Ben" };。
C#のListから要素を削除するには?
Remove(value) は最初に一致した要素を削除し、見つかれば true を返します。RemoveAt(index) は位置で削除します。RemoveAll(x => condition) は一致するすべての要素を削除し、削除した数を返します。Clear() はリストを空にします。
C#のListに値が含まれているか確認するには?
list.Contains(value) は true か false を返します。Equals で比較するので、自分で作ったクラスでは、Equals をオーバーライドしていない限り参照が比較されます。プロパティで確認するには、list.Exists(p => p.Name == "Ana") かLINQの list.Any(...) を使います。
C#のListをプロパティで並べ替えるには?
Sort に比較を渡します。people.Sort((a, b) => a.Age.CompareTo(b.Age)) はその場で並べ替えます。降順なら a と b を入れ替えます。あるいは、新しい並べ替え済みのシーケンスを返し、等しい要素の元の順序を保つLINQを使います:people.OrderBy(p => p.Age).ToList()。
「Collection was modified; enumeration operation may not execute」になるのはなぜですか?
foreach が同じリストを反復している最中に、要素を追加、削除、または置き換え(list[i] = x)たので、列挙子が次のステップで InvalidOperationException を投げたのです。list.RemoveAll(condition) を使うか、for ループで逆順にループするか、list.ToList() のようなコピーを反復します。
CountとCapacityの違いは何ですか?
Count はリストの要素数です。Capacity は内部の配列のサイズで、少なくとも Count 以上です。配列がいっぱいになると、リストは2倍の大きさの新しい配列を確保して要素をコピーするので、Add は平均すると高速です。