コレクション【C#】

C#文法:コレクション:C05_Listのコンストラクタ_Capacity

前回に続いて、List<T>のコンストラクターを見ていきます。List<T>のコンストラクターには、引数なしのもの、IEnumerable<T>を受け取るもののほかに、int型の引数を1つ受け取るものがあります。この引数には「capacity」という名前が付いています。今回は、このcapacityが何を意味しているのか、そしてどんなときに指定するのかを確認していきます。

int型の引数を受け取るコンストラクター

まず、新しくdListという変数を用意して、List<string>を生成してみます。new List<string>( と入力したところでシグネチャのツールチップを確認すると、オーバーロードの2番目に「List<string>(int capacity)」が表示されます。

図1 List<string>(int capacity) のコンストラクター

図1 List<string>(int capacity) のコンストラクター

説明文には「The number of elements that the new list can initially store」とあり、新しいリストが最初に格納できる要素数を指定する引数であることがわかります。ここでは5を指定しておきます。

var dList = new List<string>(5);
//内部的には配列で、それを使いやすくしたのがList<T>
//あらかじめ少し多めに領域を確保して、
//Addのたびにリサイズしなくていいようにしているという狙い

List<T>の内部は配列

capacityを理解するには、List<T>の中身を知っておく必要があります。List<T>というのは、配列を使いやすくしたものです。内部的には配列を持っています。

図2 内部的には配列で、それを使いやすくしたのがList<T>

図2 内部的には配列で、それを使いやすくしたのがList<T>

配列は、値を追加したり削除したりするのが面倒だという話をこれまでにしてきました。要素数が固定なので、増やしたいときにはArray.Resizeなどで作り直す必要があったわけです。そのあたりをやりやすくしてくれているのがList<T>ですが、中で使っているのはやはり配列です。

そこでList<T>は、内部の配列をある程度多めに確保しておくという作りになっています。Addされるたびに毎回Resizeしていたのではパフォーマンスに悪いので、あらかじめドカッと領域を取っておき、「実際に使っているのは何件」「確保している領域は何件」という2つの数字を別々に管理しているのです。

図3 Capacityのイメージ(3件使用中、領域は5件分)

図3 Capacityのイメージ(3件使用中、領域は5件分)

上の図でいえば、値が入っているのは「AA」「BBB」「C」の3件ですが、領域としては5件分が確保されています。この「実際に使っている件数」がCount、「内部的に確保している領域の件数」がCapacityです。

CountとCapacityを確認する

それでは実際に動かして確認してみます。capacityに5を指定した状態で実行し、ウォッチウィンドウにdList.CountとdList.Capacityを登録します。

図4 Countは0、Capacityは5

図4 Countは0、Capacityは5

まだ1件もAddしていないので、Countは当然0件です。一方でCapacityは5になっています。これが内部的に確保している領域で、実際に使っているのは0件、ということです。コードで書くと次のようなイメージになります。

var dList = new List<string>(5);

var count = dList.Count;        // 0(実際に使っている件数)
var capacity = dList.Capacity;  // 5(内部的に確保している領域)

capacityを指定しない場合は自動拡張される

次に、引数の5を消して、capacityを指定しない形にしてみます。指定しなかった場合は、必要に応じて勝手に拡張されるようになっています。確認のために、Addを1行追加してブレークポイントを置きます。

var dList = new List<string>();
//内部的には配列で、それを使いやすくしたのがList<T>
//あらかじめ少し多めに領域を確保して、
//Addのたびにリサイズしなくていいようにしているという狙い
dList.Add("AAA");

図5 capacityを指定せず、Addを1行追加したコード

図5 capacityを指定せず、Addを1行追加したコード

この状態で実行し、Addの手前で止めてウォッチウィンドウを見ると、CountもCapacityも0になっています。

図6 Add前はCountもCapacityも0

図6 Add前はCountもCapacityも0

ここからステップ実行でAddを1回実行します。

図7 1件AddするとCountは1、Capacityは4になる

図7 1件AddするとCountは1、Capacityは4になる

Countは1になり、Capacityは4になりました。1件追加しただけですが、領域は4件分が確保されています。このように少し多めに取っておいて、それを超えるときにまたリサイズしてくれる、という作りになっているわけです。

var dList = new List<string>();
// Count = 0, Capacity = 0

dList.Add("AAA");
// Count = 1, Capacity = 4(少し多めに確保される)

つまり、何もしなければ基本的には自動拡張されます。普段のプログラミングでcapacityを意識する必要はありません。

capacityを指定したほうがよい場面

では、どんなときにcapacityを指定するのかというと、パフォーマンスが出ない場合です。たとえば1万件というように、大量にAddすることがあらかじめわかっている場合を考えてみます。自動拡張にまかせていると、領域が足りなくなるたびにこまめにリサイズが発生し、その分パフォーマンスが落ちてしまいます。

そういう場合は、コンストラクターの引数に1000や10000といった値を書いておきます。

図8 capacityに10000を指定する

図8 capacityに10000を指定する

var dList = new List<string>(10000);

こうしておくと、内部的には最初から10000件分の配列がドカッと準備されるので、途中でリサイズが発生しなくなり、その分パフォーマンスが上がる可能性があります。コードにも次のようにコメントを残しておきます。

dList.Add("AAA");
//基本的には指定しなくていいが、
//パフォーマンスが出ない場合、
//この値を、必要な値で最初から確保しておけば、
//チューニングできる可能性がある。

図9 基本的には指定しなくてよいが、チューニングの手段になる

図9 基本的には指定しなくてよいが、チューニングの手段になる

まとめると、capacityは基本的には指定しなくてかまいません。ただし、パフォーマンスが出ないときに、必要な分量を最初からドカッと取っておけばチューニングできる可能性がある、ということを覚えておいてください。

capacityを超えてAddするとどうなるか

最後に、指定したcapacityを超えてAddした場合の動きを確認しておきます。capacityを5に戻し、”11″から”66″までの6件をAddするコードにします。領域は5件分ですが、Addするのは6件です。

var dList = new List<string>(5);
//内部的には配列で、それを使いやすくしたのがList<T>
//あらかじめ少し多めに領域を確保して、
//Addのたびにリサイズしなくていいようにしているという狙い
dList.Add("11");
//基本的には指定しなくていいが、
//パフォーマンスが出ない場合、
//この値を、必要な値で最初から確保しておけば、
//チューニングできる可能性がある。
dList.Add("22");
dList.Add("33");
dList.Add("44");
dList.Add("55");
dList.Add("66");

図10 capacityを5にして6件Addするコード

図10 capacityを5にして6件Addするコード

実行して最初のAddの手前で止めると、Countは0、Capacityは5です。ここまでは先ほどと同じです。

図11 Add前はCountが0、Capacityが5

図11 Add前はCountが0、Capacityが5

ここからステップ実行で1件ずつAddしていきます。5件目のAddが終わった時点では、Countが5、Capacityも5です。確保した領域をちょうど使い切った状態で、Capacityはまだ変わっていません。

図12 5件目まではCapacityは5のまま

図12 5件目まではCapacityは5のまま

そして6件目のAddを実行します。

図13 6件目のAddでCapacityが10に拡張される

図13 6件目のAddでCapacityが10に拡張される

Countが6になり、Capacityは10に拡張されました。値の変化を整理すると次のようになります。

var dList = new List<string>(5);  // Count = 0, Capacity = 5
dList.Add("11");                  // Count = 1, Capacity = 5
dList.Add("22");                  // Count = 2, Capacity = 5
dList.Add("33");                  // Count = 3, Capacity = 5
dList.Add("44");                  // Count = 4, Capacity = 5
dList.Add("55");                  // Count = 5, Capacity = 5
dList.Add("66");                  // Count = 6, Capacity = 10(自動拡張)

最初に5と指定していたからといって、それをオーバーしたらエラーになる、ということではありません。capacityはあくまで「最初に確保しておく領域」であって上限ではないので、超えた場合はこれまでどおり自動拡張されます。そこは安心して使ってください。

まとめ

List<T>のコンストラクターにint型の値を渡すと、内部の配列として最初に確保する領域(Capacity)を指定できます。Countが「実際に使っている件数」であるのに対し、Capacityは「内部的に確保している領域の件数」です。指定しなくても自動拡張されるので基本的には指定不要ですが、大量にAddすることがわかっていてパフォーマンスが出ない場合には、必要な件数を最初から確保しておくことでチューニングできる可能性があります。また、指定した値を超えてAddしてもエラーにはならず、自動的に拡張されます。Capacityの意味と使い方は、以上のとおりです。

C#文法:コレクション

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る

A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る