C#: コレクションとジェネリクス

1. 一般的な概念と背景

ジェネリクスを使えば、型パラメータ化されたコードを記述でき、型安全性コードの再利用を実現できます。 ジェネリクスが導入される前は、ArrayListにはobjectの型が格納されていました。値型は追加時にボックス化され、取得時にアンボックス化されていたため、パフォーマンスが低下し、コンパイル時の型チェックも行われませんでした。

▶ サンプル

CSHARP
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);
}
▶ 試してみよう
TEXT 📖 参照専用
42
hello

ジェネリック版では、要素の型がコンパイル時に決定されるため、ボクシングやアンボクシングは不要であり、エラーはコンパイル時に検出されます:

CSHARP
using System;
using System.Collections.Generic;

List````<int>```` numbers = new List````<int>````();
numbers.Add(42);

foreach (int n in numbers)
{
    Console.WriteLine(n);
}
TEXT 📖 参照専用
42

2. リスト<T> 動的リスト

List<T>は、最も一般的に使用されている汎用コレクションであり、動的にサイズ変更可能な順序付きリストをサポートし、要素の追加、削除、検索、変更を行うための豊富なメソッドを備えています。

▶ サンプル

CSHARP
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)}");
▶ 試してみよう
TEXT 📖 参照専用
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) 要素の削除

CSHARP
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}");
TEXT 📖 参照専用
20, 40, 50
Count: 3

3. 辞書`<TKey, TValue>` 辞書

Dictionary<TKey, TValue>はキーと値のペアを格納し、キーによる高速な値の検索を可能にし、検索時間はほぼO(1)です。

▶ サンプル

CSHARP
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}");
}
▶ 試してみよう
TEXT 📖 参照専用
Dave not found
Count: 2
---Keys---
Alice
Bob
---Values---
90
92
---Pairs---
Alice: 90
Bob: 92

4. ジェネリックメソッドとジェネリッククラス

ジェネリクスはコレクションだけに限定されるものではなく、メソッドやクラスでも型パラメータを使用できるため、アルゴリズムを複数の型に適用することが可能です。

▶ サンプル

CSHARP
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"));
▶ 試してみよう
TEXT 📖 参照専用
7
banana

汎用クラスの例:

CSHARP
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);
TEXT 📖 参照専用
Box<Int32>: 42
Box<String>: hello

5. ジェネリック制約

キーワードwhereを使用すると、型パラメータに制約を課し、使用可能な型の範囲を制限することができます。

▶ サンプル

CSHARP
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}");
▶ 試してみよう
TEXT 📖 参照専用
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を使用して削除します。

▶ サンプル

CSHARP
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()}");
}
▶ 試してみよう
TEXT 📖 参照専用
Peek: first
Dequeue: first
Dequeue: second
Dequeue: third

7. Stack<T> Stack

スタックは、後入れ先出し(LIFO)のデータ構造です。Pushを使用して追加し、Popを使用して削除します。

▶ サンプル

CSHARP
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()}");
}
▶ 試してみよう
TEXT 📖 参照専用
Peek: 30
Pop: 30
Pop: 20
Pop: 10

8. コレクションの初期化子

コレクションの初期化子を使用すると、簡潔で直感的な構文を用いて、コレクションの作成時に直接要素を設定することができます。

▶ サンプル

CSHARP
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}");
}
▶ 試してみよう
TEXT 📖 参照専用
1, 2, 3, 4, 5
coffee: $3.5
tea: $2.8
juice: $4

9. IEnumerable およびイテレータの概要

IEnumerable<T>は、すべての反復可能なコレクションの基盤となるインターフェースであり、foreachループはこのインターフェースに依存しています。yield returnを使用すれば、カスタムイテレータを簡単に作成できます(詳細は第35課を参照)。

▶ サンプル

CSHARP
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);
}
▶ 試してみよう
TEXT 📖 参照専用
1
2
3
4
5
💡 yield returnは、メソッドをイテレータに変換します。呼び出しごとに1つの値を返し、そこで一時停止し、次の反復ではその一時停止地点から再開します。詳しい説明については、第35課を参照してください。

❓ よくある質問

Q List<T>と配列の違いは何ですか?
A List<T>は要素の動的な追加・削除に対応していますが、配列は長さが固定されています。 リストは内部的に配列で実装されており、サイズ変更が必要な場合は、自動的により大きな配列を作成し、要素をコピーします。
Q 辞書に重複するキーを追加するとどうなりますか?
A 重複するキーで Add メソッドを使用すると ArgumentException が発生します。インデクサー代入 dict[key] = value を使用すると、既存の値が上書きされます。
Q where T : classwhere T : structを併用することはできますか?
A いいえ、クラス制約では参照型が、構造体制約では値型が要求されるため、これらは互いに排他的です。
Q キューやスタックは foreach を使って走査できますか?
A はい、foreach による走査では要素は削除されず、単に読み込まれるだけです。要素を実際に削除するのは、dequeue や pop です。
Q なぜジェネリック制約where T : new()が必要なのですか?
A これにより、メソッド内でnew T()を使用してインスタンスを作成できることが保証されます。この制約がない場合、コンパイラはTに対してnew演算子の使用を許可しません。

📖 まとめ

📝 練習問題

  1. List<double>を作成し、5つのスコアを追加し、FindAllを使用して80以上のスコアをフィルタリングして出力する
  2. 首都と省の対応関係を3つ格納するDictionary<string, string>を作成し、都市名から省を検索する機能を実装する(TryGetValueを使用)。
  3. 値を [min, max] の範囲内に制限する汎用メソッド `T Clamp<T>(T value, T min, T max) where T : IComparable<T> を実装してください。
  4. Queue<string>を使用して印刷キューをシミュレートします。3つの文書名をキューに追加し、その後、それらを順番にキューから取り出して印刷します。
  5. yield returnを使用して、フィボナッチ数列の最初の10項を生成するIEnumerable<int>イテレータメソッドを作成してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%