ArrayListとListの違いとは?[C#/VB]:.NET TIPS
可変長サイズのコレクションであるArrayListクラスとList<T>クラスの違い、ArrayListからList<T>への変換、パフォーマンス、どちらを使用すべきかについてまとめた。
配列のように使えて長さは可変のコレクションが、.NET Frameworkには2つある。ArrayListクラス(System.Collections名前空間)とList<T>クラス(System.Collections.Generic名前空間)だ。両者の違いは何だろうか? そして、どちらを使うべきだろうか? 結論から言っておくと、Listを使うべきなのだ。本稿では、両者の使い方や変換方法、そしてパフォーマンスについて解説していく。
特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、List<T>クラスは.NET Frameworkバージョン2.0から利用できるが、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。また、サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using static System.Console;
Option Strict On
Imports System.Console
また、サンプルコードの中で、次のメソッドを使っている。コレクションの内容をコンソールに出力するためのものだ。
static void WriteAllItems(IEnumerable collection)
{
var strings = new List<string>();
foreach (object o in collection)
{
if (o is string s)
strings.Add($"\"{s}\"");
else
strings.Add(o?.ToString());
}
WriteLine(string.Join(", ", strings));
}
Sub WriteAllItems(collection As IEnumerable)
Dim strings = New List(Of String)()
For Each o As Object In collection
If (TypeOf o Is String) Then
strings.Add($"""{o}""")
Else
strings.Add(o?.ToString())
End If
Next
WriteLine(String.Join(", ", strings))
End Sub
C#の「if (o is string s)」という書き方は、C# 7で追加されたis演算子の型パターンと呼ばれるものだ。詳しくは「特集:C# 7の新機能詳説:第3回 型による分岐の改良 (2/2)」をご覧いただきたい。また、「o?.ToString()」という書き方は、Null条件演算子を使ったものだ。こちらの詳細は「.NET TIPS:構文:nullチェックを簡潔に記述するには?」をご覧いただきたい。
ArrayListとListの違いとは?
ArrayListコレクション(.NET Frameworkの最初からある)のジェネリック版がList<T>コレクションだ。どちらも主要なメソッドとして、Add/Remove/CopyTo/Sortメソッドなどを持っている。また、配列と同じようにインデクサーで各要素にアクセスできる(ただし、配列とは違って2次元や3次元などの多次元配列は作れない。ジャグ配列のように、要素としてコレクションを持たせることは可能)。
使う上での大きな違いは、ArrayListクラスの要素はObject型であり、List<T>クラスの要素の型はList<T>クラスをインスタンス化するときに決まるということだ(次のコード)。
ArrayListクラスの要素にはどんな型でも格納できるが、取り出すときには必ずキャストしなければならない(Visual BasicではOption StrictをOnにした場合)。
List<T>クラスの要素には特定の型しか格納できない代わりに、取り出すときのキャストは不要だ(ただし、インスタンスを作るときにObject型を指定すれば、ArrayListクラスと同じことになる)。
使い勝手の点では、List<T>クラスが優位であろう。
// ArrayListの生成と要素の追加
var al = new ArrayList();
al.Add(1); // 整数を追加
al.Add("one"); // 文字列を追加
// ArrayListから要素を取り出す
//int al0 = al[0]; // コンパイルエラー
int al0 = (int)al[0]; // キャストが必要
string al1 = al[1] as string; // キャストが必要
// Listの生成と要素の追加
var intList = new List<int>(); // int型だけを入れられるリスト
intList.Add(1); // 整数だけが追加できる
//intList.Add("one"); // 文字列を入れようとするとコンパイルエラー
var stringList = new List<string>(); // string型だけを入れられるリスト
//stringList.Add(1); // 整数を入れようとするとコンパイルエラー
stringList.Add("one"); // 文字列だけが追加できる
// Listから要素を取り出す
int list0 = intList[0]; // キャストは不要
string list1 = stringList[0]; // キャストは不要
' ArrayListの生成と要素の追加
Dim al = New ArrayList()
al.Add(1) ' 整数を追加
al.Add("one") ' 文字列を追加
' ArrayListから要素を取り出す
'Dim al0 As Integer = al(0) ' Option Strict Onではコンパイルエラー
Dim al0 As Integer = CInt(al(0)) ' Option Strict Onではキャストが必要
Dim al1 As String = TryCast(al(1), String) ' Option Strict Onではキャストが必要
' Listの生成と要素の追加
Dim intList = New List(Of Integer)() ' Integer型だけを入れられるリスト
intList.Add(1) ' 整数だけが追加できる
'intList.Add("one") ' 文字列を入れようとするとOption Strict Onではコンパイルエラー
Dim stringList = New List(Of String)() ' String型だけを入れられるリスト
'stringList.Add(1) ' 整数を入れようとするとOption Strict Onではコンパイルエラー
stringList.Add("one") ' 文字列だけが追加できる
' Listから要素を取り出す
Dim list0 As Integer = intList(0) ' キャストは不要
Dim list1 As String = stringList(0) ' キャストは不要
要素の追加と取り出しを行う例だ。
ArrayListは整数でも文字列でも要素に追加できて便利そうだが、それは型引数にObjectを指定したList<T>(List<Object>)でも同じことである。
むしろArrayListでは、入っているものが全て同じ型だと分かっていても、取り出すときにいちいちキャストしなければならない。
ArrayListをListに変換するには?
ArrayListはListに変換できる。その際には格納している要素の型をキャストすることになるが、どのようにキャストするかで方法が異なってくる。
まず、型が違ったら例外が出てほしい場合だ。それにはLINQ拡張(System.Linq名前空間のEnumerableクラスにある拡張メソッド)のCast<T>拡張メソッドを使う(次のコード)。ただし、これはキャストといっても、暗黙の型変換は行われないので注意してほしい。格納されている型とCast<T>の型引数は一致している必要がある。
// 整数だけが入っているArrayListは、List<int>に変換可能
var al1 = new ArrayList { 1, 2, 3, };
List<int> list1 = al1.Cast<int>().ToList();
WriteAllItems(list1);
// 出力:1, 2, 3
// 整数と文字が入っているArrayListは、List<int>に変換できない
var al2 = new ArrayList { 1, "two", 3, };
try
{
List<int> list2 = al2.Cast<int>().ToList();
}
catch (Exception e)
{
WriteLine(e.GetType().Name);
// 出力:InvalidCastException
}
' 整数だけが入っているArrayListは、List(Of Integer)に変換可能
Dim al1 = New ArrayList From {1, 2, 3}
Dim list1 As List(Of Integer) = al1.Cast(Of Integer)().ToList()
WriteAllItems(list1)
' 出力:1, 2, 3
' 整数と文字が入っているArrayListは、List(Of Integer)に変換できない
Dim al2 = New ArrayList From {1, "two", 3}
Try
Dim list2 As List(Of Integer) = al2.Cast(Of Integer)().ToList()
Catch ex As Exception
WriteLine(ex.GetType().Name)
' 出力:InvalidCastException
End Try
LINQ拡張のCast<int>拡張メソッドを使えば、ArrayListをIEnumerable<int>型のコレクションに変換できる。さらにLINQ拡張のToList拡張メソッドを使ってList<int>型に変換する。
ただし、Cast<T>の型引数に指定した型(この例ではint型)と異なる型がArrayListに入っていると、変換に失敗してInvalidCastException例外が発生する。
なお、WriteAllItemsメソッドは本稿冒頭に記載してある。
次に、型が違ったらその要素は変換せずに捨ててしまえばよい場合だ。それにはLINQ拡張のOfType<T>拡張メソッドを使う(次のコード)。
var al2 = new ArrayList { 1, "two", 3, };
List<int> list2 = al2.OfType<int>().ToList();
WriteAllItems(list2);
// 出力:1, 3
// 変換できなかった2番目の要素「"two"」は捨てられる
Dim al2 = New ArrayList From {1, "two", 3}
Dim list2 As List(Of Integer) = al2.OfType(Of Integer)().ToList()
WriteAllItems(list2)
' 出力:1, 3
' 変換できなかった2番目の要素「"two"」は捨てられる
OfType<T>拡張メソッドの型引数に指定した型(この例ではint型)と異なる型がArrayListに入っていると、その要素は捨てられる。
最後は、任意のロジックで要素を変換したい場合だ。それにはLINQ拡張のSelect拡張メソッドを使う(次のコード)。
var al2 = new ArrayList { 1, "two", 3, };
List<string> list2
= al2.Cast<object>().Select(o => o?.ToString()).ToList();
WriteAllItems(list2);
// 出力:"1", "two", "3"
Dim al2 = New ArrayList From {1, "two", 3}
Dim list2 As List(Of String) _
= al2.Cast(Of Object)().Select(Function(o) o?.ToString()).ToList()
WriteAllItems(list2)
' 出力:"1", "two", "3"
ここでは、ArrayListに入っているオブジェクトのToStringメソッドを使って文字列に変換している。
ArrayListではSelect拡張メソッドを使えないので、先にCast<object>拡張メソッドを使ってIEnumerable<object>型に変換し、それからSelect拡張メソッドで各要素を文字列に変換し、最後にToList拡張メソッドでList<string>型にしている。
なお、「o?.ToString()」という書き方は、本稿冒頭に記載したWriteAllItemsメソッドの解説を参照。
ArrayListとListのパフォーマンスは?
値型を追加するときのパフォーマンスに大きな差がある。パフォーマンスの点でも、List<T>クラスが優れているのだ。
なぜかというと、ArrayListに格納できるのはObject型であるためである。参照型を追加するときは問題にならないが、値型を追加するときにObject型へ変換するのはとても高コストなのだ(値型をObject型に変換することを「ボックス化」という)。
実際に、次のようなコードをリリースビルドして何回か実行してみてもらいたい。値型を追加するときの速度は、List<T>クラスの方が1桁ほど速いと分かるだろう。要素が1万個くらいまでであれば実用上の差は分からない程度の速度ではあるが、ArrayListでなければならない理由がない限り、ArrayListを使う意義はないのである。
const int COUNT = 10_000_000; // 追加する要素数(1千万個)
var sw = new Stopwatch(); // 経過時間測定用のタイマー
var o = new object(); // 参照型を追加するときに使うオブジェクト
// ArrayListに参照型を追加する
var al1 = new ArrayList();
sw.Restart();
for (int i = 0; i < COUNT; i++)
al1.Add(o);
sw.Stop();
WriteLine($"ArrayListにobject:{sw.ElapsedMilliseconds}ミリ秒");
// 筆者の環境では0.1〜0.2秒であった。以降、この時間を基準にコメントする
al1 = null;
GC.Collect();
// List<object>に参照型を追加する
var list1 = new List<object>();
sw.Restart();
for (int i = 0; i < COUNT; i++)
list1.Add(o);
sw.Stop();
WriteLine($"List<object>にobject:{sw.ElapsedMilliseconds}ミリ秒");
// ArrayListとほぼ同じ時間であった
list1 = null;
GC.Collect();
// ArrayListに値型を追加する
var al2 = new ArrayList();
sw.Restart();
for (int i = 0; i < COUNT; i++)
al2.Add(i);
sw.Stop();
WriteLine($"ArrayListにint:{sw.ElapsedMilliseconds}ミリ秒");
// 参照型を追加するより1桁遅かった
al2 = null;
GC.Collect();
// List<int>にint型(値型)を追加する
var list2 = new List<int>();
sw.Restart();
for (int i = 0; i < COUNT; i++)
list2.Add(i);
sw.Stop();
WriteLine($"List<int>にint:{sw.ElapsedMilliseconds}ミリ秒");
// 参照型を追加するより速かった
Const COUNT As Integer = 10_000_000 ' 追加する要素数(1千万個)
Dim sw = New Stopwatch() ' 経過時間測定用のタイマー
Dim o = New Object() ' 参照型を追加するときに使うオブジェクト
' ArrayListに参照型を追加する
Dim al1 = New ArrayList()
sw.Restart()
For i As Integer = 0 To COUNT - 1
al1.Add(o)
Next
sw.Stop()
WriteLine($"ArrayListにobject:{sw.ElapsedMilliseconds}ミリ秒")
' 筆者の環境では0.1〜0.2秒であった。以降、この時間を基準にコメントする
al1 = Nothing
GC.Collect()
' List(Of Object)に参照型を追加する
Dim list1 = New List(Of Object)()
sw.Restart()
For i As Integer = 0 To COUNT - 1
list1.Add(o)
Next
sw.Stop()
WriteLine($"List<object>にobject:{sw.ElapsedMilliseconds}ミリ秒")
' ArrayListとほぼ同じ時間であった
list1 = Nothing
GC.Collect()
' ArrayListに値型を追加する
Dim al2 = New ArrayList()
sw.Restart()
For i As Integer = 0 To COUNT - 1
al2.Add(i)
Next
sw.Stop()
WriteLine($"ArrayListにint:{sw.ElapsedMilliseconds}ミリ秒")
' 参照型を追加するより1桁遅かった
al2 = Nothing
GC.Collect()
' List(Of Integer)にInteger型(値型)を追加する
Dim list2 = New List(Of Integer)()
sw.Restart()
For i As Integer = 0 To COUNT - 1
list2.Add(i)
Next
sw.Stop()
WriteLine($"List<int>にint:{sw.ElapsedMilliseconds}ミリ秒")
' 参照型を追加するより速かった
ArrayList/List<T>とも、インスタンス化するときの引数にサイズを指定すると速くなるが、劇的に差が縮まるわけではない。
なお、このコードでは非同期実行されるガベージコレクションが間に合わないこともあるようだ。そのときは「GC.Collect();」の後ろに「Task.Run(async () => await Task.Delay(1000)).Wait();」などと時間待ちをするコードを追加してほしい。
まとめ
List<T>クラスはArrayListクラスのジェネリック版である。使い勝手からも、パフォーマンスからも、特別な理由がない限りはジェネリック版のList<T>クラスを使おう。
利用可能バージョン:.NET Framework 2.0以降
カテゴリ:クラス・ライブラリ 処理対象:コレクション
使用ライブラリ:ArrayListクラス(System.Collections名前空間)
使用ライブラリ:Listクラス(System.Collections.Generic名前空間)
関連TIPS:構文:コレクションのインスタンス化と同時に要素を追加するには?[C#/VB]
関連TIPS:Listに要素を追加/挿入するには?[C#/VB]
関連TIPS:配列のサイズを変更するには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]
関連TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]
■この記事と関連性の高い別の.NET TIPS
- Listの要素を検索するには?[C#/VB]
- Listの各要素を処理するには?[C#/VB]
- 配列のサイズを変更するには?
- Listの要素を並べ替えるには?[C#/VB]
- Listに要素を追加/挿入するには?[C#/VB]
関連記事
Copyright© Digital Advantage Corp. All Rights Reserved.