1. なんでコレクションにインターフェースが必要なの?
正直に言うと、今まで具体的な型(List<int>とかDictionary<string, string>)でコードを書いてた時は、全部シンプルで分かりやすかったよね。でも、もし急にList<T>をHashSet<T>に変えたくなったら?ユニークな値だけ必要になったとか。あるいは、どっちのコレクション型でも動くメソッドを作りたくなったら?どうする?
共通の入り口を作りたい――つまり「ユニバーサルなコネクタ」が必要。C#ではインターフェースがその役割を果たしてる。インターフェースはコレクションが持つべきルール(メソッドやプロパティ)を定義してる。インターフェースを実装してれば、その操作ができるってこと。
スマホの充電みたいなもんだよ。USB-Cの標準コネクタがあれば、スマホも最新の掃除機も、電動歯ブラシだって充電できる(暗い部屋で間違えなければね)。
実践例
例えば、どんな数字のコレクションでも合計を計算するメソッドを書きたいとする。インターフェースなしだとこうなる:
// このメソッドは配列だけに使える:
static int SumArray(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
// こっちはList<int>専用:
static int SumList(List<int> list)
{
int sum = 0;
for (int i = 0; i < list.Count; i++)
sum += list[i];
return sum;
}
めっちゃ不便!コードはほぼ同じなのに。IEnumerable<int>インターフェースを使えば、もっと汎用的にできるよ:
static int SumAll(IEnumerable<int> collection)
{
int sum = 0;
foreach (var number in collection)
sum += number;
return sum;
}
これで、このメソッドはIEnumerable<int>を実装してるどんなコレクション型でも受け取れる。配列でもリストでもセットでもOK。
2. コレクション用の主なインターフェース
.NETではコレクション用にいくつか重要なインターフェースがある。ここではざっくり紹介するね――詳しい実装は今後のレクチャーで!
| インターフェース | 用途 | このインターフェースを実装してるコレクション例 |
|---|---|---|
|
要素の列挙(foreach) | 全部:, , 配列 |
|
変更可能なセット(Add, Remove, Count) | , , |
|
インデックスアクセス、変更 | , 配列 |
|
「キー-値」ペアの操作 | , |
|
セット(ユニークな要素) | , |
P.S. 今は分かりやすくするために、これらのインターフェース間の「継承の階層」は省略してるよ。誰が誰を継承してるかは、また後で!
ビジュアル化:コレクションインターフェースのツリー
IEnumerable<T>
^
|
ICollection<T>
^ ^
| |
IList<T> ISet<T>
| |
List<T> HashSet<T>
辞書の場合:
IEnumerable<KeyValuePair<K,V>>
^
|
ICollection<KeyValuePair<K,V>>
^
|
IDictionary<K,V>
|
Dictionary<K,V>
3. 実際の現場でインターフェースが役立つ理由
柔軟性
メソッドやクラスが具体的な型(List<int>)じゃなくて、インターフェース(IEnumerable<int>)で受け取るようにすると、そのインターフェースを実装してるどんなコレクションでも渡せる。結果、プログラムが柔軟で拡張しやすくなるよ。(「iPhoneだけ」じゃなくて、Bluetooth付きならどのスマホでもOK、みたいな感じ!)
static void PrintAll(IEnumerable<string> collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
これで、どんなリスト、配列、セット、LINQの結果でも要素を表示できる!
統一化
もし自作のコレクション型(例えば、超ニッチな用途のコレクション)を作るなら、必要なインターフェースを実装しよう。そうすれば、他人のコードでも自分のコレクションが使える。だから.NETではよく「実装じゃなくてインターフェースでプログラミングしよう」って言われるんだ。
標準ツールとの互換性
標準インターフェースをサポートしてれば、自作コレクションもLINQやソート、シリアライズとか.NETのいろんな機能と連携できる。
4. List<T>はリストでもコレクションでも列挙でもある
列挙についてちょっとだけ
IEnumerable<T>は.NETで一番大事で基本的なコレクションインターフェース。たった一つのメソッド――GetEnumerator()――を定義してて、これがコレクションをループするEnumeratorオブジェクトを返す。これがあるから、こんな風に書ける:
foreach (var name in names)
{
Console.WriteLine(name);
}
ここでnamesはリストでもセットでも配列でもOK。foreachは裏でGetEnumerator()を使ってる、ちょっとしたシンタックスシュガーなんだ。
IEnumerable(<T>なし)はもっと古い非ジェネリック版。今はほぼ常にジェネリック版を使おう。
Listクラスの宣言シンタックス
List<T>クラスの宣言をちょっと覗いてみると、こんな感じ:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...
つまり、List<T>オブジェクトはこう使える:
- インデックスでアクセスできるリスト(IList<T>)
- 要素の追加・削除ができるコレクション(ICollection<T>)
- ループできる単なる要素の集まり(IEnumerable<T>)
例:
List<string> names = new List<string> { "アンナ", "ボリス", "ヴァーシャ" };
// IEnumerableとして - ループできる
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);
// ICollectionとして - Countが取れる
ICollection<string> asCollection = names;
int count = asCollection.Count;
// IListとして - インデックスアクセスできる
IList<string> asList = names;
string firstItem = asList[0];
5. アプリの例
例えば、ユーザーとその役割を管理するシンプルなアプリがあるとする(前回のレクチャーで作り始めたやつね):
// ユーザークラス
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
ユーザーはIEnumerable<User>を実装してるどんなコレクションでも保存できる:
List<User> listUsers = new List<User> { new User { Name = "アンナ", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "ボリス", Age = 25 } };
// 全ユーザーの名前を表示するメソッド
static void PrintUserNames(IEnumerable<User> users)
{
foreach (var user in users)
Console.WriteLine(user.Name);
}
// いろんなコレクション型で使える:
PrintUserNames(listUsers);
PrintUserNames(setUsers);
これでアプリがめっちゃ柔軟になる――魔法もカスタムオーバーロードも不要!
6. よくあるミスや落とし穴
一番ありがちなミスは、インターフェースで十分なのに具体的な型を使っちゃうこと:
// ダメ(実装にガチガチに依存してる)
void DoSomethingWithList(List<int> numbers) { ... }
// ベター(どんなコレクションでもOK)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }
もう一つの罠は、インターフェースは明示的に定義された操作しかできないってことを忘れること。例えばIEnumerable<T>では要素数(Count)は分からない。知りたければICollection<T>を使うか、自分で数えるしかないよ。
GO TO FULL VERSION