コレクション【C#】

C#文法:コレクション:C01A_ArrayList

ここからはリストを扱っていきます。本命は次回以降で解説するList<T>ですが、その前に、C#の初期(.NET Framework 1.0)から存在するArrayListを取り上げます。ArrayListは現在では基本的に使わないクラスですが、歴史を紐解き、どのような不便さがあったのかを理解しておくと、List<T>がなぜ今の形になっているのかがよく分かるようになります。

テスト用のボタンを追加する

これまでと同じように、フォームにボタンを1つ追加します。Textは「ArrayList」としておきます。配置したらボタンをダブルクリックして、クリックイベント(button13_Click)を生成します。

図1 フォームに「ArrayList」ボタンを追加する

図1 フォームに「ArrayList」ボタンを追加する

前回の振り返り:配列のリサイズは面倒

前回は、配列の要素数を変更するにはArray.Resizeを使う必要があり、増減のたびに手間がかかる、書きづらい、というお話をしました。

図2 前回作成した配列のリサイズのコード

図2 前回作成した配列のリサイズのコード

リスト1 前回の振り返り(配列のリサイズ)

//配列のリサイズ
//増減のたびに手間がかかる
int[] hai = new int[3] { 10, 20, 30 };
Array.Resize(ref hai, hai.Length + 1);
hai[hai.Length - 1] = 999;

1件追加するだけでも、サイズを1つ広げてから最後の位置に値を設定する、という2段階の記述が必要でした。途中の要素を削除する場合はさらに面倒になります。

ArrayListとは

ArrayListはC#の初期から存在するクラスで、これを使うと要素数の増減を簡単に書くことができます。ただし、次の2つの問題があります。

1つ目は、型安全(タイプセーフ)ではないという問題です。2つ目は、値を使用するときにキャストが必要になるという問題です。

そのため、基本的にはArrayListは使わず、この後に解説するList<T>などを使うことになります。今回は「どういう不便さがあるのか」を体験することが目的です。まずはイベントの中に、今回のポイントをコメントとして書いておきます。

図3 ArrayListの特徴をコメントとして記述する

図3 ArrayListの特徴をコメントとして記述する

リスト2 ArrayListの特徴(コメント)

private void button13_Click(object sender, EventArgs e)
{
    //ArrayList
    //配列と異なり、要素の増減が簡単だが
    //型安全(type-safe)でない、
    //使用時にキャストが必要などのデメリットあり
}

ArrayListを生成する

それではArrayListを生成してみます。ArrayListはSystem.Collections名前空間に属しているため、ファイルの先頭にusingを追加します。

図4 using System.Collections; を追加する

図4 using System.Collections; を追加する

リスト3 usingの追加

using System.Collections;

usingを追加したら、ArrayListをnewします。配列と違って、要素数を指定する必要はありません。

リスト4 ArrayListの生成

ArrayList arrayList = new ArrayList();

Addで要素を追加する

ArrayListにはAddメソッドがあり、これを呼び出すだけで要素を追加できます。10、20、30、40、50の5件を追加してみます。

図5 Addで5件の値を追加する

図5 Addで5件の値を追加する

リスト5 Addによる追加

ArrayList arrayList = new ArrayList();
arrayList.Add(10);
arrayList.Add(20);
arrayList.Add(30);
arrayList.Add(40);
arrayList.Add(50);

配列のArray.Resizeと比べると、非常にシンプルに書けることが分かります。ただし、見てのとおり型をどこにも指定していないので、実は何でも入ってしまいます。たとえば30の部分を文字列の"AAA"に書き換えても、コンパイルエラーにはなりません。

図6 数値の並びに文字列を混ぜてもエラーにならない

図6 数値の並びに文字列を混ぜてもエラーにならない

この問題は後ほど詳しく見ていきます。いったん元に戻し、まずは普通に数値だけを入れた状態で使ってみます。

RemoveAtでインデックスを指定して削除する

削除したい場合はRemoveAtが使えます。引数にインデックスを指定すると、その位置の要素が削除されます。RemoveAt(1)とすれば、インデックスの1番目が消えます。

図7 RemoveAt(1) を追加する

図7 RemoveAt(1) を追加する

リスト6 RemoveAtによる削除

arrayList.RemoveAt(1);   //インデックス1番目(20)を削除

RemoveAtの行にブレークポイントを置いて実行し、ウォッチウィンドウでarrayListの中身を確認します。この時点ではCountが5で、10から50までの5件が入っています。

図8 RemoveAt実行前:Count = 5(10〜50)

図8 RemoveAt実行前:Count = 5(10〜50)

ステップ実行でRemoveAt(1)を実行すると、Countが4になり、インデックス1番目にあった20が消えました。後ろの要素は自動的に前に詰められています。

図9 RemoveAt実行後:20が消えて Count = 4 になる

図9 RemoveAt実行後:20が消えて Count = 4 になる

Removeで値を指定して削除する

インデックスではなく値を指定して削除したい場合はRemoveを使います。Removeは、指定した値と最初に一致した要素を1つ削除します。30を消したい場合は次のように書きます。

図10 Remove(object? obj):値を指定して削除するメソッド

図10 Remove(object? obj):値を指定して削除するメソッド

リスト7 Removeによる削除

arrayList.RemoveAt(1);   //インデックス指定で削除(20が消える)
arrayList.Remove(30);    //値指定で削除(30が消える)

実行すると、まずRemoveAtで20が消え、続いてRemove(30)で30も消えます。残りは10、40、50の3件です。このように、ArrayListは削除が非常にやりやすくなっています。

図11 Remove(30)実行後:10、40、50の3件(Count = 3)

図11 Remove(30)実行後:10、40、50の3件(Count = 3)

Clearですべて削除する

Clearを呼び出すと、すべての要素が削除されます。

リスト8 Clearによる全削除

arrayList.Clear();       //全件削除

図12 Clear実行後:Count = 0 になる

図12 Clear実行後:Count = 0 になる

ここまで見てきたように、ArrayListではAdd・RemoveAt・Remove・Clearを呼ぶだけで、要素の増減が簡単にできます。

問題点1:型安全(タイプセーフ)でない

ここからがデメリットの話です。先ほど触れたように、ArrayListは型を指定していません。要するに、要素の中身は1個1個すべてobjectとして扱われています。そのため、数値の123を追加した後に、文字列の"AAA"を追加することもできてしまいます。

リスト9 型が違っても入ってしまう

//型が違っても入ってしまう(問題点1:型安全(type-safe))
arrayList.Add(123);
arrayList.Add("AAA");

これでも普通に動作します。ウォッチウィンドウで確認すると、0番目にobject {int}の123、1番目にobject {string}の”AAA”が、ごちゃ混ぜの状態で入っていることが分かります。

図13 123(int)と "AAA"(string)が同じArrayListに入っている

図13 123(int)と “AAA”(string)が同じArrayListに入っている

このように、型が違っても入ってしまうことを「型安全でない」「タイプセーフでない」と言います。

プログラミングに慣れていない方には「何でも入る方が便利なのでは」と見えるかもしれません。しかし、使う側からすると実は非常に不便です。その変数に何が入っているかが保証されていないからです。たとえばfor文でぐるぐると回しながら要素の中身を足していく、という計算をしようとしても、途中に文字列が混ざっていたらエラーになってしまいます。数値なら数値、と決まっている方が安心して使えます。それが型安全(タイプセーフ)と言われているものです。

図14 問題点1のコメントを追記する

図14 問題点1のコメントを追記する

リスト10 for文で合計を計算しようとした場合(参考)

int total = 0;
for (int i = 0; i < arrayList.Count; i++)
{
    //1番目の "AAA" をintに変換しようとした時点で
    //実行時エラー(InvalidCastException)になる
    total += (int)arrayList[i];
}

問題点2:使用時にキャストが必要

さらに、値を使うときにキャストが必要という問題があります。今、インデックスの0番目には123、1番目には”AAA”が入っています。0番目をint型の変数に取り出そうとして、そのまま代入するとコンパイルエラー(CS0266)になります。インデクサarrayList[0]の戻り値がobjectだからです。

図15 CS0266:型 'object' を 'int' に暗黙的に変換できません

図15 CS0266:型 ‘object’ を ‘int’ に暗黙的に変換できません

リスト11 そのままでは代入できない

int a = arrayList[0];        //コンパイルエラー(CS0266)

objectとして入っているので、intならintにきちんと戻してあげる必要があります。

リスト12 キャストして取り出す

int a = (int)arrayList[0];   //objectからintへキャスト

これで実行すると、変数aに123が入っていることを確認できます。

図16 キャストすることで a に 123 が入る

図16 キャストすることで a に 123 が入る

使えるようにはなりましたが、値を取り出すたびに毎回キャストが必要なので面倒です。なお、キャストの下に表示されている緑の波線はnull許容に関する警告で、今回の説明には影響しません。

間違ったキャストは実行時エラーになる

さらに良くないのが、キャストを間違えた場合です。インデックスの1番目には”AAA”が入っていますが、これを誤ってintでキャストするコードを書いてみます。

図17 1番目("AAA")を誤ってintでキャストする

図17 1番目(”AAA”)を誤ってintでキャストする

リスト13 間違ったキャスト

int a = (int)arrayList[0];   //OK:中身はint
int b = (int)arrayList[1];   //コンパイルは通るが、中身はstring

このコードはコンパイルが通ってしまいます。しかし実行すると、InvalidCastExceptionの例外が発生します。

図18 実行時にInvalidCastExceptionが発生する

図18 実行時にInvalidCastExceptionが発生する

型安全であれば、そもそもキャストする必要がなく、「中身は全部intだな」と思って使えます。ところがキャストが必要な場合は、コードのミスが実行してみないと分かりません。つまり実行時エラーになります。

コンパイルエラーと実行時エラー

プログラミングでは、基本的にミスはコンパイルエラーではじくのが理想で、実行時エラーはできるだけ起きない方がよいとされています。

動かしてみないとエラーにならないということは、リリースしてからお客さんが気づく、というパターンが起こり得るということです。それがバグです。一方、コンパイルでエラーになってくれれば、コンパイルが通らない以上リリースができないので、お客さんに届く前に必ずエラーは取り除かれます。

ですから「できるだけコンパイルエラーにしていく」というのが重要なポイントになります。タイプセーフでないということは、その実行時エラーが増えるということです。

正しい型でキャストする

インデックスの1番目はstringなので、正しくはstringでキャストします。

図19 stringでキャストするように修正する

図19 stringでキャストするように修正する

リスト14 正しい型でキャストする

int a = (int)arrayList[0];
string b = (string)arrayList[1];

これなら正しくキャストできます。ただし、どの位置にどの型が入っているかをプログラマーが覚えておかなければならない、という状況は変わりません。

まとめ

最後に、今回の内容をコメントとして整理しておきます。間違ったキャストの行はコメントアウトして残しています。

図20 問題点2のコメントを追記した最終的なコード

図20 問題点2のコメントを追記した最終的なコード

リスト15 今回の最終的なコード

private void button13_Click(object sender, EventArgs e)
{
    //ArrayList
    //配列と異なり、要素の増減が簡単だが
    //型安全(type-safe)でない、
    //使用時にキャストが必要などのデメリットあり
    ArrayList arrayList = new ArrayList();
    arrayList.Add(10);
    arrayList.Add(20);
    arrayList.Add(30);
    arrayList.Add(40);
    arrayList.Add(50);

    arrayList.RemoveAt(1);
    arrayList.Remove(30);
    arrayList.Clear();

    //型が違っても入ってしまう(問題点1:型安全(type-safe))
    arrayList.Add(123);
    arrayList.Add("AAA");
    //なんでも入るのはいいことに見えるが
    //使う側からしたら、不便。forで回しながら、
    //計算するとかできない。

    //キャストが必要(問題点2:パフォーマンス・記述コスト)
    int a = (int)arrayList[0];
    //このようなコードミスをコンパイルで検出できない
    //(実行時エラーとなる)
    //int b = (int)arrayList[1];
    string b = (string)arrayList[1];
}

ArrayListは、Add・RemoveAt・Remove・Clearによって要素の増減が簡単にできる一方で、次のデメリットがあります。

  • 型安全(タイプセーフ)でないため、型が違う値も入ってしまう
  • 使用時に毎回キャストが必要で、記述コストがかかり、パフォーマンスも悪くなる
  • キャストのミスをコンパイルで検出できず、実行時エラーになる

パフォーマンスについて補足すると、intのような値型をobjectとして格納するときにはボックス化、取り出してキャストするときにはボックス化解除という処理が毎回発生します。これがArrayListの速度面での不利につながっています。

配列は型安全ですが増減が面倒、ArrayListは増減が簡単ですが型安全ではありません。次回からは、この両方のデメリットを解消したList<T>を見ていきます。

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新機能」を観る