1. 主要インターフェースの分類
もし「インターフェースってアーキテクトとか“クリーンコード”好きのためのおもちゃでしょ?実務ではなくても大丈夫じゃん」と思ってたら、ちょっとビックリするかも:本気のC#プロジェクトはインターフェースだらけなんだよ。なぜかって?だって.NET標準ライブラリのほぼすべての部分がインターフェースでできてるから!インターフェースなしじゃコレクションも扱えないし、ファイルも読めないし、LINQでコレクションをフィルタすることすらできない。
インターフェースは.NETにおけるポリモーフィズムと拡張性の土台で、フレームワークの「ブロック」がどう組み合わさるかを決めてるんだ。
.NET標準ライブラリは本当にインターフェースだらけ。溺れないように、こんな感じで分類してみたよ(もちろん全部じゃない、全部カバーするには人生が足りない!):
| カテゴリ | インターフェース | 何のため? |
|---|---|---|
| コレクション | , , , , |
ループ、変更、インデックスアクセス、キーと値のペア操作 |
| リソース操作 | |
リソース解放(ファイル、接続、ストリーム) |
| 比較 | , , |
並び替え、比較、オブジェクトのユニーク性 |
| シリアライズ | |
オブジェクトをバイトストリームに変換(または逆) |
| LINQとクエリ | , |
複雑なクエリのサポート(例:DBへのクエリ) |
| 非同期処理 | , |
非同期ループとリソース解放 |
| イベントと通知 | , |
プロパティやコレクションの変更に反応 |
| 日付と時間 | |
カスタムな文字列フォーマット |
| データ構造 | , |
コレクションやタプルのディープ比較 |
| ストリーム/入出力 | , , , |
ストリーム操作、push/pull通知 |
重要! 上記の多くのインターフェースは型パラメータを持ってる:List<string>みたいなやつ。普通はコレクションやコレクション系インターフェースで使うよInt[] → List<int>。詳しくはコレクションの章でやるから、今は「そうなんだ」くらいでOK。
2. コレクションのインターフェース
IEnumerableとIEnumerator — foreachのパスポート
.NETのほぼすべてのコレクションはIEnumerableか、そのジェネリック版IEnumerable<T>を実装してる。このインターフェースがあるから、あの魔法のforeach構文が使えるんだよ:
List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers) // List<int>がIEnumerable<int>を実装してるから動く!
{
Console.WriteLine(n);
}
このインターフェースの最小バージョンはこんな感じ:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumeratorは実際にコレクションを歩く「ループ担当」インターフェースだよ:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
実際の開発ではめっちゃよくIEnumerable<T>型のパラメータや戻り値を使うことになる。例えば、配列から偶数だけ返すメソッド:
public IEnumerable<int> GetEvenNumbers(int[] array)
{
foreach (var x in array)
if (x % 2 == 0) yield return x;
}
そう、yieldキーワードが魔法をかけてくれて、インターフェースの実装を自動でやってくれるんだ。詳しくは後で!
ICollectionとIList — インデックスアクセスや変更ができるコレクション
単なるループだけじゃなくて、要素の追加・削除やインデックスアクセスもしたいなら、もっと専門的なインターフェースが必要:
- ICollection<T> — AddやRemoveメソッド、Countプロパティが追加される。
- IList<T> — インデックスアクセス(this[int index])など、さらに操作が増える。
public void PrintFirstItem(IList<string> list)
{
if (list.Count > 0)
Console.WriteLine(list[0]);
}
List<T>はIEnumerable<T>もICollection<T>もIList<T>も実装してる。だから、シンプルなリストとしても、いろんなメソッドを使うリッチなコレクションとしても扱えるんだ。
IDictionary<TKey, TValue> — 「キーと値」のペア
辞書(dictionary)が好き?(よく使うよね!)なら、IDictionary<TKey, TValue>インターフェースを使おう。これがあれば、キーから値を取得したり、その逆もできる。
public void PrintAllPairs(IDictionary<string, int> ages)
{
foreach (var pair in ages)
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
ここでagesはDictionary<string, int>でもSortedDictionary<string, int>でもOK。大事なのは、このインターフェースを実装してること!
3. IDisposableインターフェース:リソースの正しい扱い方
入出力、ファイル操作、ネットワーク接続、DB操作…全部IDisposableインターフェースに頼ってる。このインターフェースは「アンマネージリソースがあるなら、使い終わったらちゃんと“お片付け”してね」っていう超大事な契約。そう、あのusing構文を可能にしてるやつだよ:
using (StreamReader reader = new StreamReader("file.txt"))
{
// ファイルを使って、usingの後は自動でクローズされる!
string line = reader.ReadLine();
}
インターフェース自体は超シンプル:
public interface IDisposable
{
void Dispose();
}
でも「本気」のライブラリはみんなこれを実装してる!詳しいベストプラクティスは公式ドキュメントを見てね。
4. 比較とユニーク性のためのインターフェース
IComparable<T>とIComparer<T>
自作オブジェクトのコレクションをソートしたい?その場合、オブジェクト同士を比較できる必要がある。そこでIComparable<T>インターフェースの出番:
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public int CompareTo(Student? other)
{
// スコアの降順でソート
if (other == null) return 1;
return other.Score.CompareTo(this.Score);
}
}
// これで学生をソートできる:
var students = new List<Student> { ... };
students.Sort();
詳しくはMicrosoftのドキュメントを見てね。
IComparer<T>はクラスの外で比較方法を定義したいときに使う。例えば、たまに名前順、たまにスコア順でソートしたい場合とか:
public class NameComparer : IComparer<Student>
{
public int Compare(Student? x, Student? y)
{
return string.Compare(x?.Name, y?.Name);
}
}
IEquatable<T> — 等価性の比較
自作オブジェクトをHashSet<T>で使いたい(つまりコレクション内で「ユニーク」にしたい)?IEquatable<T>を実装しよう:
public class Person : IEquatable<Person>
{
public string Name { get; set; }
public bool Equals(Person? other)
{
if (other == null) return false;
return this.Name == other.Name;
}
}
5. イベントと通知のためのインターフェース
「オブジェクトのプロパティが変わったらプログラムが“気付く”ようにしたい」って思ったことある?例えば、ユーザーが名字を変えたらUIが自動で更新されるとか。そんなときに使うのがINotifyPropertyChangedインターフェース。GUIアプリ(WPFやXamarinとか)で超人気。
インターフェースのシグネチャはシンプル:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
実装例:
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
}
これでUIのバインディングはNameが変わるたびに自動で更新される。もっと知りたかったら公式ドキュメントを見てみて。
6. その他便利なインターフェース
IFormattableインターフェース:柔軟なフォーマット
オブジェクトをきれいに、いろんなパターンで文字列化したい(例えば小数点以下の桁数を変えたい)なら、IFormattableを実装しよう:
public class Temperature : IFormattable
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
public string ToString(string? format, IFormatProvider? formatProvider)
{
// シンプルに'C'か'F'で表示を切り替えるだけ
if (format == "F")
return $"{Celsius * 9 / 5 + 32} F";
return $"{Celsius} C";
}
}
これでtemp.ToString("F", null)やtemp.ToString("C", null)が呼べる。だからDateTimeやdouble、decimalなどの組み込み型も柔軟なフォーマットができるんだ。
非同期処理のためのインターフェース
C#に非同期処理が登場してから、非同期の世界用の新しいインターフェースが必要になった。例えば、オブジェクトが非同期でリソースを解放するならIAsyncDisposableを実装しよう:
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
これでusingの代わりにawait usingが書ける!
非同期イテレーション(await foreach)にはIAsyncEnumerable<T>が使われる。これでメインスレッドをブロックせずに要素をループできる。例えば大きなファイルを分割して読むときや、ネットからデータストリームを受け取るときに使う。詳しくはレベル58で :P
シリアライズ用インターフェース:ISerializable
オブジェクトをバイトストリームにして保存・送信したい(例:ネットワーク越しに)なら、.NETはISerializableインターフェースを用意してる。最近はもっと便利な仕組み(属性や既成のシリアライザー)があるからあまり使わないけど、一応紹介。シグネチャはこんな感じ:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
7. インターフェースの実践:リアルなアプリでの使い方
例えば、図書館の本管理用コンソールアプリを作るとしよう(まあ、誰かがやらなきゃね!)。本のリストを表示したり、ソートやフィルタ、データの保存・読み込みができるようにしたい。インターフェースの知識があれば:
- いろんなコレクション型をIEnumerable<Book>で返せるから、配列かリストか気にせず使える。
- いろんな基準でソートできる:IComparable<Book>でタイトル順、IComparer<Book>で著者順など。
- ファイル操作で効率よくリソース解放:IDisposableで。
- ジャンルや著者で本をフィルタ:LINQで、IEnumerable<T>を実装してればOK。
- アプリの拡張も簡単 — 例えばファイル保存からDB接続に切り替えたいときも、クラスがインターフェース(IBookStorage)経由なら楽勝。
8. よくあるミスと注意点
初心者がよくやるミスの一つは、変数やメソッド引数の型に具体的な実装("List")を使っちゃうこと。インターフェース("IEnumerable"、"IList"など)を使おう。覚えておいて:「インターフェースでプログラミング」すれば、コードは柔軟で拡張しやすくなるよ。
どのレベルのインターフェースを使うかも注意が必要。例えば、ループだけできればいいメソッドならIEnumerable<T>を使って、IList<T>みたいな余計な制約はかけない方がいい。
もう一つの注意点:クラスがたくさんのインターフェースを実装してて、同じ名前のメソッドやプロパティがかぶってる場合は、明示的なインターフェース実装(explicit implementation)が必要になる。じゃないとコンパイラが混乱しちゃうよ。
GO TO FULL VERSION