可変長サイズのコレクションである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
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は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拡張の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"」は捨てられる
最後は、任意のロジックで要素を変換したい場合だ。それには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"
値型を追加するときのパフォーマンスに大きな差がある。パフォーマンスの点でも、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}ミリ秒")
' 参照型を追加するより速かった
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]
Copyright© Digital Advantage Corp. All Rights Reserved.