C#: コレクションとジェネリクス
1. 一般的な概念と背景
ジェネリクスを使えば、型パラメータ化されたコードを記述でき、型安全性とコードの再利用を実現できます。 ジェネリクスが導入される前は、ArrayListにはobjectの型が格納されていました。値型は追加時にボックス化され、取得時にアンボックス化されていたため、パフォーマンスが低下し、コンパイル時の型チェックも行われませんでした。
▶ サンプル
using System;
using System.Collections;
ArrayList list = new ArrayList();
list.Add(42);
list.Add("hello");
int value = (int)list[0];
foreach (object item in list)
{
Console.WriteLine(item);
}
42
hello
ジェネリック版では、要素の型がコンパイル時に決定されるため、ボクシングやアンボクシングは不要であり、エラーはコンパイル時に検出されます:
using System;
using System.Collections.Generic;
List````<int>```` numbers = new List````<int>````();
numbers.Add(42);
foreach (int n in numbers)
{
Console.WriteLine(n);
}
42
2. リスト<T> 動的リスト
List<T>は、最も一般的に使用されている汎用コレクションであり、動的にサイズ変更可能な順序付きリストをサポートし、要素の追加、削除、検索、変更を行うための豊富なメソッドを備えています。
▶ サンプル
using System;
using System.Collections.Generic;
List````<string>```` fruits = new List````<string>```` { "apple", "banana", "cherry" };
fruits.Add("date");
fruits.AddRange(new string[] { "elderberry", "fig" });
fruits.Insert(1, "blueberry");
bool hasApple = fruits.Contains("apple");
int idx = fruits.IndexOf("cherry");
string found = fruits.Find(f => f.StartsWith("b"));
List````<string>```` allB = fruits.FindAll(f => f.StartsWith("b"));
fruits.Sort();
fruits.ForEach(f => Console.WriteLine(f));
Console.WriteLine("---");
Console.WriteLine($"Count: {fruits.Count}");
Console.WriteLine($"Has apple: {hasApple}");
Console.WriteLine($"Cherry index: {idx}");
Console.WriteLine($"First b-word: {found}");
Console.WriteLine($"All b-words: {string.Join(", ", allB)}");
apple
blueberry
banana
cherry
date
elderberry
fig
---
Count: 7
Has apple: True
Cherry index: 3
First b-word: blueberry
All b-words: blueberry, banana
(1) 要素の削除
using System;
using System.Collections.Generic;
List````<int>```` nums = new List````<int>```` { 10, 20, 30, 40, 50 };
nums.Remove(30);
nums.RemoveAt(0);
Console.WriteLine(string.Join(", ", nums));
Console.WriteLine($"Count: {nums.Count}");
20, 40, 50
Count: 3
3. 辞書`<TKey, TValue>` 辞書
Dictionary<TKey, TValue>はキーと値のペアを格納し、キーによる高速な値の検索を可能にし、検索時間はほぼO(1)です。
▶ サンプル
using System;
using System.Collections.Generic;
Dictionary`<string, int>` scores = new Dictionary`<string, int>`
{
{ "Alice", 90 },
{ "Bob", 85 }
};
scores.Add("Charlie", 78);
scores["Bob"] = 92;
bool exists = scores.ContainsKey("Alice");
if (scores.TryGetValue("Dave", out int daveScore))
{
Console.WriteLine($"Dave: {daveScore}");
}
else
{
Console.WriteLine("Dave not found");
}
scores.Remove("Charlie");
Console.WriteLine($"Count: {scores.Count}");
Console.WriteLine("---Keys---");
foreach (string key in scores.Keys)
{
Console.WriteLine(key);
}
Console.WriteLine("---Values---");
foreach (int val in scores.Values)
{
Console.WriteLine(val);
}
Console.WriteLine("---Pairs---");
foreach (KeyValuePair`<string, int>` kv in scores)
{
Console.WriteLine($"{kv.Key}: {kv.Value}");
}
Dave not found
Count: 2
---Keys---
Alice
Bob
---Values---
90
92
---Pairs---
Alice: 90
Bob: 92
4. ジェネリックメソッドとジェネリッククラス
ジェネリクスはコレクションだけに限定されるものではなく、メソッドやクラスでも型パラメータを使用できるため、アルゴリズムを複数の型に適用することが可能です。
▶ サンプル
using System;
T Max```<T>```(T a, T b) where T : IComparable```<T>```
{
return a.CompareTo(b) >= 0 ? a : b;
}
Console.WriteLine(Max(3, 7));
Console.WriteLine(Max("apple", "banana"));
7
banana
汎用クラスの例:
using System;
class Box```<T>```
{
private T _value;
public Box(T value)
{
_value = value;
}
public T Value => _value;
public override string ToString() => $"Box<{typeof(T).Name}>: {_value}";
}
Box````<int>```` intBox = new Box````<int>````(42);
Box````<string>```` strBox = new Box````<string>````("hello");
Console.WriteLine(intBox);
Console.WriteLine(strBox);
Box<Int32>: 42
Box<String>: hello
5. ジェネリック制約
キーワードwhereを使用すると、型パラメータに制約を課し、使用可能な型の範囲を制限することができます。
▶ サンプル
using System;
class Repository```<T>``` where T : class, new()
{
public T Create()
{
T instance = new T();
Console.WriteLine($"Created: {typeof(T).Name}");
return instance;
}
}
class Player
{
public string Name { get; set; } = "Unknown";
}
Repository<Player> repo = new Repository<Player>();
Player p = repo.Create();
Console.WriteLine($"Name: {p.Name}");
Created: Player
Name: Unknown
一般的な制約事項の参照:
| 制約 | 説明 |
|---|---|
where T : class |
T は参照型でなければならない |
where T : struct |
T は値型でなければならない |
where T : new() |
T には引数なしのパブリックコンストラクタが必須である |
where T : IFoo |
T は IFoo インターフェースを実装しなければならない |
where T : Base |
T は基底クラスから継承しなければならない |
6. キュー<T> キュー
キューは先入れ先出し(FIFO)のデータ構造です。Enqueueを使用して追加し、Dequeueを使用して削除します。
▶ サンプル
using System;
using System.Collections.Generic;
Queue````<string>```` queue = new Queue````<string>````();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue("third");
string front = queue.Peek();
Console.WriteLine($"Peek: {front}");
while (queue.Count > 0)
{
Console.WriteLine($"Dequeue: {queue.Dequeue()}");
}
Peek: first
Dequeue: first
Dequeue: second
Dequeue: third
7. Stack<T> Stack
スタックは、後入れ先出し(LIFO)のデータ構造です。Pushを使用して追加し、Popを使用して削除します。
▶ サンプル
using System;
using System.Collections.Generic;
Stack````<int>```` stack = new Stack````<int>````();
stack.Push(10);
stack.Push(20);
stack.Push(30);
int top = stack.Peek();
Console.WriteLine($"Peek: {top}");
while (stack.Count > 0)
{
Console.WriteLine($"Pop: {stack.Pop()}");
}
Peek: 30
Pop: 30
Pop: 20
Pop: 10
8. コレクションの初期化子
コレクションの初期化子を使用すると、簡潔で直感的な構文を用いて、コレクションの作成時に直接要素を設定することができます。
▶ サンプル
using System;
using System.Collections.Generic;
List````<int>```` nums = new List````<int>```` { 1, 2, 3, 4, 5 };
Dictionary`<string, double>` prices = new Dictionary`<string, double>`
{
{ "coffee", 3.5 },
{ "tea", 2.8 },
{ "juice", 4.0 }
};
Console.WriteLine(string.Join(", ", nums));
foreach (var kv in prices)
{
Console.WriteLine($"{kv.Key}: ${kv.Value}");
}
1, 2, 3, 4, 5
coffee: $3.5
tea: $2.8
juice: $4
9. IEnumerable およびイテレータの概要
IEnumerable<T>は、すべての反復可能なコレクションの基盤となるインターフェースであり、foreachループはこのインターフェースに依存しています。yield returnを使用すれば、カスタムイテレータを簡単に作成できます(詳細は第35課を参照)。
▶ サンプル
using System;
using System.Collections.Generic;
IEnumerable````<int>```` GetRange(int start, int end)
{
for (int i = start; i <= end; i++)
{
yield return i;
}
}
foreach (int n in GetRange(1, 5))
{
Console.WriteLine(n);
}
1
2
3
4
5
yield returnは、メソッドをイテレータに変換します。呼び出しごとに1つの値を返し、そこで一時停止し、次の反復ではその一時停止地点から再開します。詳しい説明については、第35課を参照してください。
❓ よくある質問
<T>と配列の違いは何ですか?<T>は要素の動的な追加・削除に対応していますが、配列は長さが固定されています。 リストは内部的に配列で実装されており、サイズ変更が必要な場合は、自動的により大きな配列を作成し、要素をコピーします。dict[key] = value を使用すると、既存の値が上書きされます。where T : classとwhere T : structを併用することはできますか?where T : new()が必要なのですか?new T()を使用してインスタンスを作成できることが保証されます。この制約がない場合、コンパイラはTに対してnew演算子の使用を許可しません。📖 まとめ
- ジェネリックは型安全性とコードの再利用性を提供し、ボクシングやアンボクシングによるオーバーヘッドを回避します
List<T>は、追加、削除、検索、変更、ソート、検索などの豊富な操作をサポートする動的配列です。Dictionary<TKey, TValue>はキーと値のペアを格納し、O(1)の検索性能を実現します- ジェネリックメソッドやジェネリッククラスにより、アルゴリズムやデータ構造を複数の型に適用できるようになる
where制約により、型パラメータの範囲が制限されます:クラス、構造体、new()、インターフェース、基底クラスQueue<T>は先入れ先出し、Stack<T>は後入れ先出し- コレクションの初期化子を使用すると、コレクションの作成や要素の追加が簡単になります
IEnumerable<T>はトラバーサル処理の基盤であり、yield returnはイテレータを作成できる
📝 練習問題
List<double>を作成し、5つのスコアを追加し、FindAllを使用して80以上のスコアをフィルタリングして出力する- 首都と省の対応関係を3つ格納する
Dictionary<string, string>を作成し、都市名から省を検索する機能を実装する(TryGetValueを使用)。 - 値を [min, max] の範囲内に制限する汎用メソッド `T Clamp
<T>(T value, T min, T max) where T : IComparable<T>を実装してください。 Queue<string>を使用して印刷キューをシミュレートします。3つの文書名をキューに追加し、その後、それらを順番にキューから取り出して印刷します。yield returnを使用して、フィボナッチ数列の最初の10項を生成するIEnumerable<int>イテレータメソッドを作成してください。