Listクラス
Listクラスとは
複数の同じデータ型をまとめて扱うには配列を使用する方法があります。
これは便利な機能ですが、C#ではListクラスという、配列をさらに便利にしたようなものが用意されています。
using System;
using System.Collections.Generic; //←これが必要
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
//配列
int[] arrNum = new int[] { 1, 2, 3, 4, 5 };
//Listクラス
List<int> lstNum = new List<int>() { 1, 2, 3, 4, 5 };
//Listの要素数は「Count」プロパティで取得
for(int i = 0; i < lstNum.Count; i++)
{
//各要素へのアクセスは配列と同じ
Console.WriteLine(lstNum[i]);
}
Console.ReadLine();
}
}
}
1 2 3 4 5
Listクラスを使用するには、コードの先頭にusing System.Collections.Generic;
と記述する必要があります。
Visual Studioでプロジェクトを作成すれば自動で追加されていると思いますが、念のため確認してください。
これは名前空間の導入なので、必須ではありませんが記述しないと完全修飾名で指定する必要があります。
詳しくは名前空間を参照してください。
なお、最近のバージョンのVisual Studioでは暗黙的なusingディレクティブが有効な場合があります。
14行目でList型の変数を宣言しています。
Listクラスは以下の形式で使用します。
List<データ型> 変数名 = new List<データ型>();
List<データ型> 変数名 = new List<データ型>() { 初期化子 };
List<データ型> 変数名 = new List<データ型> { 初期化子 };
「List」に続く「<>」の中に、扱いたいデータ型を指定します。
次に変数名を書き、続いて= new List<データ型>
と記述します。
最後に丸括弧()
を記述します。
これを忘れるとエラーになるので注意してください。
List型変数の宣言はここまでですが、配列の時と同じように続いて初期化子(初期化リスト)を与えることができます。
書き方も配列と同じなので難しいことはないでしょう。
初期化子を使用する場合は丸括弧は省略できます。
配列は初期化時に要素数を数値で指定できましたが、Listの場合は宣言時の要素数の指定というのはありません。
宣言時に値を与えなければ要素数はゼロになります。
こうして宣言または初期化されたListは、配列とほぼ同じように扱うことができます。
各要素へのアクセスは添字演算子[]
で行います。
Listを含め、クラス型の変数のことはインスタンスと呼ばれることが多いです。
オブジェクトとも呼ばれます。
ジェネリックコレクション
ListクラスはSystem.Collections.Generic
名前空間に属する機能です。
コレクション(Collections)というのは配列のようにデータをまとめて扱う機能のことです。
ジェネリック(Generic)というのはデータ型に依存しない動作を提供する機能です。
この二つの特徴を持つものがジェネリックコレクションです。
ジェネリックについて詳しくはジェネリックの項で説明します。
ここでは簡単に、複数のデータをまとめて扱う場合に、配列よりもさらに便利な機能を提供するモノ、くらいの認識で大丈夫です。
「クラス」は特定の機能を実現するための機能(メソッドとか)がひとまとめにされているモノです。
ジェネリック対応のクラスは、その変数の宣言時にデータ型を「引数」に指定します。
これは通常の引数とは別に型引数という形式で渡します。
以下のコードの<データ型>
の部分が型引数の指定です。
List<データ型> 変数名 = new List<データ型>();
//int型を型引数に指定する場合
List<int> lstNum = new List<int>();
この変数lstNum
はint型を扱うListクラス型の変数(List<int>
型)として宣言されます。
型推論が便利
上記の記述方法はList<int>
を二回も書かないといけないので、少し冗長です。
このような場合は型推論を用いるとスッキリします。
var lstNum = new List<int>() { 1, 2, 3, 4, 5 };
配列との違い
Listクラスと配列との最大の違いは、動的に要素を追加、削除できるという点です。
List<int> lst = new List<int>() { 0, 1, 2, 3 };
//「9」を追加
lst.Add(9);
foreach (var n in lst)
Console.Write("{0}, ", n);
Console.WriteLine();
//「2」を削除
lst.Remove(2);
foreach (var n in lst)
Console.Write("{0}, ", n);
Console.ReadLine();
0, 1, 2, 3, 9, 0, 1, 3, 9,
これと同じことを配列で実現しようとするとなかなか面倒な手順を踏む必要がありますが、Listクラスならばメソッド一つで簡単に実現でき、なおかつ高速に動作します。
あらかじめ要素数が決まっていて増減することがない場合は、Listクラスよりも配列を使用したほうが高速に動作する可能性があります。
とはいえ体感できるような速度差が出ることはほぼないので、多くの場合で配列の代わりにListクラスを使用しても問題ありません。
Listクラスのメソッドの具体的な操作方法は次ページ(Listクラスのメソッド)で解説します。
要素数
配列はLength
プロパティで要素数を取得しますが、ListクラスはCount
プロパティで要素数を取得します。
List<int> lstNum = new List<int>() { 1, 2, 3, 4, 5 };
int count = lstNum.Count; //5
容量(capacity)
Listクラスは動的に要素を追加できますが、追加するたびに新たにメモリ領域を確保しているわけではありません。
Listクラスは最初にある程度のサイズのメモリ領域を確保しておき、要素の追加によってメモリが足りなくなった場合に新たにメモリが確保されます。
この「あらかじめ確保しているメモリサイズ」はListの容量といいます。
Listの容量はCapacity
プロパティで取得および設定ができます。
また、List型変数の宣言時にサイズを指定して作成することもできます。
static void ShowCapacityAndCount(List<int> lst)
{
Console.WriteLine("Capacity: {0}", lst.Capacity);
Console.WriteLine("Count: {0}", lst.Count);
Console.WriteLine();
}
static void Main(string[] args)
{
//Capacityが5のListを作成
List<int> lst = new List<int>(5);
ShowCapacityAndCount(lst);
lst.Add(1);
lst.Add(2);
lst.Add(3);
ShowCapacityAndCount(lst);
lst.Add(4);
lst.Add(5);
ShowCapacityAndCount(lst);
lst.Add(6);
ShowCapacityAndCount(lst);
}
Capacity: 5 Count: 0 Capacity: 5 Count: 3 Capacity: 5 Count: 5 Capacity: 10 Count: 6
Count
の値がCapacity
の値を超えると、メモリの再確保が行われCapacity
が増やされます。
メモリの確保はそこそこ重い処理なので、使用する最大要素数があらかじめわかっている場合は最初にそのサイズの容量を確保しておくことでプログラムを高速化できます。
なおCapacity
プロパティの値は常にCount
以上の値です。
Capacity
プロパティにCount
未満の値を設定すると実行時エラーとなります。
Listのコピー
Listクラスは配列と同じく参照型です。
(値型と参照型を参照)
つまり、List型変数に別のList型変数を代入しただけではコピーにはなりません。
List<int> lst1 = new List<int>() { 1, 2, 3 };
//「lst2」は「lst1」の別名になるだけ
List<int> lst2 = lst1;
Listクラスをコピーするには、変数の宣言時の丸括弧内にコピー元となるListクラスの変数を渡すという方法があります。
これはコンストラクターによる初期化、といいます。
List<int> lst1 = new List<int>() { 1, 2, 3 };
//lst1の複製を作る
List<int> lst2 = new List<int>(lst1);
//コピー元を書き換えてみる
lst1[1] = 9;
foreach (var n in lst1)
Console.Write("{0}, ", n);
Console.WriteLine();
foreach (var n in lst2)
Console.Write("{0}, ", n);
1, 9, 3, 1, 2, 3,
全ての要素がコピーされるので、コピー元を書き換えてもコピー先には影響しません。
その他、ToList
メソッドを呼び出すことでもコピーできます。
ToList
メソッドの使用にはコード先頭にusing System.Linq;
が必要です。
using System;
using System.Collections.Generic;
using System.Linq; //←これが必要
namespace ConsoleApplication1
{
//以降省略
List<int> lst1 = new List<int>() { 1, 2, 3 };
//lst1の複製を作る
List<int> lst2 = lst1.ToList();
ただし、これらの方法によるコピーはシャローコピーです。
参照型を要素とするListクラスの複製はアドレスのコピーに過ぎないので注意してください。
詳しくはシャローコピーとディープコピーを参照してください。
要素の削除時の注意点
Listクラスの要素は任意に削除することもできますが、いくつかの注意点があります。
なお、以下のサンプルコードで使用しているRemoveAt
メソッドは、引数に指定した番号の要素を削除するメソッドです。
詳細は次ページのRemoveAt
で解説します。
削除で要素がずれることを考慮する
Listクラスの要素を削除すると、削除した要素以降の要素がひとつずつ前に詰められます。
削除メソッドを単体で使用する場合は問題ありませんが、ループ文で削除メソッドを使用する場合は注意が必要です。
例えば、string型Listクラスの要素から文字数が6未満の要素をすべて削除するつもりで以下のようなコードを書いたとします。
static void Main(string[] args)
{
var lst = new List<string>()
{ "Apple", "Grape", "Orange", "Strawberry", "Peach" };
//Listから文字数が6未満の要素を削除するつもり
for (int i = 0; i < lst.Count; i++)
{
if (lst[i].Length < 6)
lst.RemoveAt(i);
}
for (int i = 0; i < lst.Count; i++)
{
Console.WriteLine(lst[i]);
}
}
Grape Orange Strawberry
このコードは「Grape」は5文字なのに削除されずに残ってしまっています。
これは、先頭の「Apple」が削除された際に、後ろの要素が前に詰められているために起こります。
つまり「Apple」が削除されるとそれ以降の要素が前にずれるので、「Grape」の要素番号は「0」になっています。
にもかかわらず、ループカウンタ(変数i
)は先に進んでしまうので、「Grape」の削除判定が行われることなく処理が次に移ってしまっているのです。
これを正しく動作するように書き直すと以下のようになります。
static void Main(string[] args)
{
var lst = new List<string>()
{ "Apple", "Grape", "Orange", "Strawberry", "Peach" };
//Listから文字数が6未満の要素を削除する
for (int i = lst.Count - 1; i >= 0; i--)
{
if (lst[i].Length < 6)
lst.RemoveAt(i);
}
for (int i = 0; i < lst.Count; i++)
{
Console.WriteLine(lst[i]);
}
}
Orange Strawberry
ループを要素の先頭から行うのではなく、末尾から先頭に向けて行うようにします。
こうすることで、要素が削除されて前に詰められても影響なく削除処理ができます。
「ループで削除処理を行う場合は末尾から」ということは覚えておきましょう。
foreachで削除しない
foreach文では削除系のメソッドは使用できません。
文法的には書けるのですが、実行時エラー(例外)が発生します。
要素の削除ができないというよりも変更が禁止されているので、追加系のメソッドも使用できません。