コレクション【C#】

C#文法:コレクション:D03_インタフェースの階層構造

今回は「インタフェースの階層構造」というお話をしていきます。前回のレクチャーでは「クライアントができる操作を制限させる」というテーマを扱いましたが、今回の内容はそれに通じるお話です。コレクションに関するインタフェースがどのように分かれていて、どのような関係になっているのかを、.NETの実際の定義を見ながら確認していきましょう。

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

これまでのレクチャーと同じように、フォームにボタンを1つ追加します。ボタンの表示文字は「インタフェースの階層構造」としておき、ダブルクリックしてクリックイベントを生成します。

図1 「インタフェースの階層構造」ボタンをフォームに追加する

図1 「インタフェースの階層構造」ボタンをフォームに追加する

今回生成されるのはbutton19_Clickというイベントハンドラです。今回のレクチャーでは実行して動作を確認するコードは書かず、このメソッドの中にコメントでポイントを整理しながら、.NETが用意しているインタフェースの定義を読んでいきます。

コレクションインタフェースは大きく分けて3つ

コレクションに関するインタフェースは、大きく分けて3つあります。IEnumerable<T>、ICollection<T>、IList<T>の3つです。まずはこの3つの名前をコメントとして書いておきます。

リスト1 3つのコレクションインタフェースをコメントに書く

private void button19_Click(object sender, EventArgs e)
{
    //インタフェースの階層構造
    //2.クライアントができる操作を制限させる
    //IEnumerable<T>
    //ICollection<T>
    //IList<T>
}

図2 IEnumerable<T>・ICollection<T>・IList<T>の3つがある

図2 IEnumerable<T>・ICollection<T>・IList<T>の3つがある

インタフェース分離の原則

なぜ3つに分かれているのでしょうか。これは「インタフェース分離の原則」にも当てはまる考え方です。1つのインタフェースにすべての機能を詰め込むのではなく、小さい単位で分かれている、ということです。

リスト2 3つに分かれている理由を書き足す

//インタフェース分離の原則
//1つのインタフェースにすべてを詰め込むのではなく
//小さい単位で分かれている

図3 インタフェース分離の原則:小さい単位で分かれている

図3 インタフェース分離の原則:小さい単位で分かれている

では、それぞれのインタフェースがどのような「小さい単位」を担当しているのかを、1つずつ見ていきましょう。

IEnumerable<T>:繰り返しだけができる

まずIEnumerable<T>です。これは基本的にforeachなどでぐるぐると回す、繰り返しだけを担当するインタフェースです。要素を順番に1つずつ取り出していく、ということしか基本的にはできません。

図4 IEnumerable<T>はforeachによる繰り返しを担当する

図4 IEnumerable<T>はforeachによる繰り返しを担当する

たとえばList<int>をIEnumerable<int>型の変数で受け取ると、次のようにforeachで要素を順番に取り出すことはできますが、要素を追加したり、インデックスを指定して取り出したりすることはできなくなります。

リスト3 IEnumerable<T>でできること・できないこと

IEnumerable<int> values = new List<int> { 10, 20, 30 };

// foreachで順番に要素を取り出すことはできる
foreach (var value in values)
{
    Console.WriteLine(value);
}

// values.Add(40);       // コンパイルエラー:Addは定義されていない
// var x = values[0];    // コンパイルエラー:インデクサは定義されていない

なお、<T>が付いているかどうかは、型指定ができるかどうかの違いです。<T>のないIEnumerableも、<T>のあるIEnumerable<T>も存在しますが、基本的にはIEnumerable<T>を使うことになります。型指定をしないということは、ArrayListを使うときと同じ型変換の問題が出てくるからです。

リスト4 型指定のないIEnumerableは型変換が必要になる

// 非ジェネリックのIEnumerable:要素はobject型として取り出される
IEnumerable items = new ArrayList { 10, 20, 30 };
foreach (object item in items)
{
    int value = (int)item;   // 使う側で型変換(キャスト)が必要
}

// ジェネリックのIEnumerable<T>:要素はT型(ここではint)で取り出される
IEnumerable<int> values = new List<int> { 10, 20, 30 };
foreach (int value in values)
{
    // 型変換は不要
}

インタフェースの中身を見たいときは、エディター上でIEnumerable<T>と入力し、その上にカーソルを置いてF12キーを押します。すると定義にジャンプできます。

図5 定義を見るために、いったんIEnumerable<T>と入力する

図5 定義を見るために、いったんIEnumerable<T>と入力する

図6 F12キーでIEnumerable<T>の定義にジャンプする

図6 F12キーでIEnumerable<T>の定義にジャンプする

中を見ると、GetEnumeratorというメソッドが1つあるだけです。IEnumerator<T>を返す、つまり「要素を順番に返すもの」を返す、というだけのインタフェースになっています。

図7 IEnumerable<T>にはGetEnumeratorメソッドだけが定義されている

図7 IEnumerable<T>にはGetEnumeratorメソッドだけが定義されている

実際の定義には属性やコメントなどが付いていますが、要点だけを抜き出すと次のようになります。

リスト5 IEnumerable<T>の定義(要点のみ抜粋)

public interface IEnumerable<out T> : IEnumerable
{
    // 要素を順番に返すためのIEnumerator<T>を返す
    new IEnumerator<T> GetEnumerator();
}

ここで、名前が似ていて少しややこしいのですが、Enumerator(イニュメレーター)とEnumerable(イニュメラブル)という2つの言葉が出てきます。これはGoFのデザインパターンでいうところのIteratorパターン、つまり「要素を繰り返し取り出す」ためのパターンです。その仕組みを持っているのがIEnumerableだ、ということになります。

とりあえずここでは、IEnumerable<T>は「foreachなどで繰り返して要素を取ってくることだけができるもの」と思っておいてください。

ICollection<T>:件数の取得と要素の追加・削除ができる

次にICollection<T>です。同じようにF12キーで定義を見てみると、ICollection<T>はIEnumerable<T>を継承していることがわかります。つまり、IEnumerable<T>の機能にプラスしてICollection<T>自身の機能を持っている、ということになります。

図8 ICollection<T>はIEnumerable<T>を継承している

図8 ICollection<T>はIEnumerable<T>を継承している

中身を順に見ていくと、Count、IsReadOnly、Add、Clear、Contains、CopyTo、Removeが定義されています。

図9 ICollection<T>の先頭にはCountプロパティが定義されている

図9 ICollection<T>の先頭にはCountプロパティが定義されている

図10 下にスクロールするとAddなどのメソッドが並んでいる

図10 下にスクロールするとAddなどのメソッドが並んでいる

リスト6 ICollection<T>の定義(要点のみ抜粋)

public interface ICollection<T> : IEnumerable<T>
{
    int Count { get; }
    bool IsReadOnly { get; }

    void Add(T item);
    void Clear();
    bool Contains(T item);
    void CopyTo(T[] array, int arrayIndex);
    bool Remove(T item);
}

イメージとしては、「件数(Count)が取れる」ことと、「Add系、つまり要素の追加や削除ができる」ことがICollection<T>の役割です。これもコメントに書き足しておきます。

図11 ICollection<T>の役割をコメントに書き足す

図11 ICollection<T>の役割をコメントに書き足す

ICollection<T>型の変数で受け取った場合は、次のようなコードが書けます。繰り返しに加えて件数の取得と追加・削除ができますが、インデックスを指定したアクセスはまだできません。

リスト7 ICollection<T>でできること・できないこと

ICollection<int> values = new List<int> { 10, 20, 30 };

values.Add(40);                      // 要素の追加
values.Remove(10);                   // 要素の削除
bool exists = values.Contains(20);   // 含まれているかどうか
int count = values.Count;            // 件数の取得

// IEnumerable<T>を継承しているので、foreachも使える
foreach (var value in values)
{
    Console.WriteLine(value);
}

// var x = values[0];    // コンパイルエラー:インデクサは定義されていない

IList<T>:インデックス指定ができる

最後にIList<T>です。IList<T>はICollection<T>を継承しています。そしてICollection<T>はIEnumerable<T>を継承していました。つまり、ここも階層構造になっています。

図12 IList<T>はICollection<T>を継承し、ICollection<T>はIEnumerable<T>を継承している

図12 IList<T>はICollection<T>を継承し、ICollection<T>はIEnumerable<T>を継承している

IList<T>は、IEnumerable<T>とICollection<T>のフル機能に、自分自身で定義したものを加えたインタフェースということになります。3つがバラバラに存在しているというよりは、機能がどんどん増えていくイメージです。IEnumerable<T>プラスICollection<T>の機能、さらにそれにプラスしてIList<T>の機能、という形です。

ではIList<T>自身は何を追加しているのかというと、「インデクサ」という概念が入ってきます。this[int index]という定義がそれで、角括弧で指定したインデックスの要素を取り出すことができるようになります。

図13 IList<T>にはインデクサ(this[int index])が定義されている

図13 IList<T>にはインデクサ(this[int index])が定義されている

ほかにもIndexOfなどがありますが、要するに「インデックス指定でアクセスできるもの」がIList<T>ということです。

リスト8 IList<T>の定義(要点のみ抜粋)

public interface IList<T> : ICollection<T>
{
    // インデクサ:インデックスを指定して要素を取得・設定する
    T this[int index] { get; set; }

    int IndexOf(T item);
    void Insert(int index, T item);
    void RemoveAt(int index);
}

リスト9 IList<T>でできること

IList<int> values = new List<int> { 10, 20, 30 };

int first = values[0];          // インデックス指定で取得
values[1] = 99;                 // インデックス指定で設定
int index = values.IndexOf(30); // 要素の位置を調べる
values.Insert(0, 5);            // 位置を指定して挿入
values.RemoveAt(0);             // 位置を指定して削除

// ICollection<T>の機能も、IEnumerable<T>の機能もすべて使える
values.Add(40);
int count = values.Count;
foreach (var value in values)
{
    Console.WriteLine(value);
}

IList<T>の主な機能は「インデックス指定ができる」ということです。これもコメントに書いて、今回のまとめのコメントが完成します。

図14 3つのインタフェースの役割をコメントで整理した状態

図14 3つのインタフェースの役割をコメントで整理した状態

リスト10 今回書いたコメントの全体

private void button19_Click(object sender, EventArgs e)
{
    //インタフェースの階層構造
    //2.クライアントができる操作を制限させる
    //IEnumerable<T>
    //ICollection<T>
    //IList<T>

    //インタフェース分離の原則
    //1つのインタフェースにすべてを詰め込むのではなく
    //小さい単位で分かれている
    //IEnumerable<T>:foreach、繰り返し
    //ICollection<T>:Count,Add系、要素の追加削除
    //IList<T>:インデックス指定ができる
}

階層構造を整理する

ここまでの内容を表にまとめると次のようになります。下に行くほど、上のインタフェースの機能をすべて引き継いだうえで、新しい機能が追加されていきます。

インタフェース 継承元 追加される主な機能
IEnumerable<T> IEnumerable foreachによる繰り返し(GetEnumerator)
ICollection<T> IEnumerable<T> Count、Add、Remove、Clear、Containsなど
IList<T> ICollection<T> インデクサ、IndexOf、Insert、RemoveAt

List<T>はこの3つのインタフェースをすべて実装しているため、同じ1つのリストを、どのインタフェースの型としても受け取ることができます。どの型で受け取るかによって、使える操作の範囲が変わります。

リスト11 同じList<T>を3つのインタフェースで受け取る

var list = new List<int> { 10, 20, 30 };

IEnumerable<int> a = list;   // 繰り返しだけ
ICollection<int> b = list;   // 繰り返し + 件数・追加・削除
IList<int> c = list;         // 繰り返し + 件数・追加・削除 + インデックス指定

まとめ

このように、「繰り返しだけ」「要素の追加・削除だけ」「インデックス指定だけ」という形でインタフェースを細かく分けておくことで、必要な機能だけに限定して使うことができます。

そしてこの設計が、前回お話しした「クライアントができる操作を制限させる」ということにつながっていきます。

図15 前回のコメント「2.クライアントができる操作を制限させる」につながる

図15 前回のコメント「2.クライアントができる操作を制限させる」につながる

次回は、このインタフェースの階層構造を使って、クライアントができる操作を実際に制限していく方法を見ていきます。

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