ジェネリック型とメソッド

ヒント

ソフトウェアの開発は初めてですか? 最初に、 作業の開始 に関するチュートリアルから始めます。 List<T>などのコレクションを使用するとすぐにジェネリックを目にすることになります。

別の言語で経験がありますか? C# ジェネリックは、Javaのジェネリックや C++ のテンプレートに似ていますが、完全なランタイム型情報と型消去はありません。 C# 固有のパターンを探すために、コレクション式のセクションと共変性と反変性のセクションをざっと見てください。

ジェネリックを 使用すると、完全な型セーフを維持しながら、任意の型で動作するコードを記述できます。 intstring、および必要な他のすべての型に対して個別のクラスまたはメソッドを記述する代わりに、1 つ以上の型パラメーター (TTKeyTValueなど) を使用して 1 つのバージョンを記述し、実際の型を使用するときに指定します。 コンパイラはコンパイル時に型をチェックするため、ランタイム キャストやリスク InvalidCastExceptionは必要ありません。

ジェネリックは日常的な C# で常に発生します。 コレクション、非同期戻り値の型、デリゲート、LINQ はすべてジェネリック型に依存しています。

List<int> scores = [95, 87, 72, 91];
Dictionary<string, decimal> prices = new()
{
    ["Widget"] = 19.99m,
    ["Gadget"] = 29.99m
};
Task<string> greeting = Task.FromResult("Hello, generics!");
Func<int, bool> isPositive = n => n > 0;

Console.WriteLine($"First score: {scores[0]}");
Console.WriteLine($"Widget price: {prices["Widget"]:C}");
Console.WriteLine($"Greeting: {await greeting}");
Console.WriteLine($"Is 5 positive? {isPositive(5)}");

いずれの場合も、山かっこ (<int><string><Product>) の型引数は、それが保持または操作するデータの種類をジェネリック型に指示します。 コンパイラによってタイプ セーフが強制されます。 誤って stringList<int>に追加することはできません。

ジェネリック型の使用

多くの場合、独自のクラス ライブラリからジェネリック型consumeを作成するのではなく、.NET クラス ライブラリから作成します。 次のセクションでは、使用する最も一般的なジェネリック型を示します。

ジェネリック コレクション

System.Collections.Generic名前空間は、タイプ セーフなコレクション クラスを提供します。 ArrayListのような非ジェネリック コレクションの代わりに、常にこれらのコレクションを使用します。

// A strongly typed list of strings
List<string> names = ["Alice", "Bob", "Carol"];
names.Add("Dave");
// names.Add(42); // Compile-time error: can't add an int to List<string>

// A dictionary mapping string keys to int values
var inventory = new Dictionary<string, int>
{
    ["Apples"] = 50,
    ["Oranges"] = 30
};
inventory["Bananas"] = 25;

// A set that prevents duplicates
HashSet<int> uniqueIds = [1, 2, 3, 1, 2];
Console.WriteLine($"Unique count: {uniqueIds.Count}"); // 3

// A FIFO queue
Queue<string> tasks = new();
tasks.Enqueue("Build");
tasks.Enqueue("Test");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Build

ジェネリック コレクションは、エラーがコンパイル時に発生するため、実行時の型エラーを防ぎます。 これらのコレクションでは、値型のボックス化を回避することでも、パフォーマンスが向上します。

ジェネリック メソッド

ジェネリック メソッドは、独自の型パラメーターを宣言します。 コンパイラは、渡す値から型引数を 推論 することが多いため、明示的に指定する必要はありません。

static void Print<T>(T value) =>
    Console.WriteLine($"Value: {value}");

Print(42);        // Compiler infers T as int
Print("hello");   // Compiler infers T as string
Print(3.14);      // Compiler infers T as double

呼び出しPrint(42)では、コンパイラは引数からTとしてintを推論します。 Print<int>(42)は明示的に記述できますが、型推論ではコードがクリーンな状態が維持されます。

コレクション式

コレクション式 (C# 12) は、コレクションを作成するための簡潔な構文を提供します。 コンストラクター呼び出しまたは初期化子構文の代わりに角かっこを使用します。

// Create a list with a collection expression
List<string> fruits = ["Apple", "Banana", "Cherry"];

// Create an array
int[] numbers = [1, 2, 3, 4, 5];

// Works with any supported collection type
IReadOnlyList<double> temperatures = [72.0, 68.5, 75.3];

Console.WriteLine($"Fruits: {string.Join(", ", fruits)}");
Console.WriteLine($"Numbers: {string.Join(", ", numbers)}");
Console.WriteLine($"Temps: {string.Join(", ", temperatures)}");

スプレッド演算子 (..) は、あるコレクションの要素を別のコレクションにインライン化します。これは、シーケンスを組み合わせる場合に便利です。

List<int> first = [1, 2, 3];
List<int> second = [4, 5, 6];

// Spread both lists into a new combined list
List<int> combined = [.. first, .. second];
Console.WriteLine(string.Join(", ", combined));
// Output: 1, 2, 3, 4, 5, 6

// Add extra elements alongside spreads
List<int> withExtras = [0, .. first, 99, .. second];
Console.WriteLine(string.Join(", ", withExtras));
// Output: 0, 1, 2, 3, 99, 4, 5, 6

コレクション式は、配列、 List<T>Span<T>ImmutableArray<T>、およびコレクション ビルダー パターンをサポートする任意の型で動作します。 完全な構文リファレンスについては、「 コレクション式」を参照してください。

ディクショナリの初期化

インデクサー初期化子を使用して、ディクショナリを簡潔に初期化できます。 この構文では、角かっこを使用してキーと値のペアを設定します。

Dictionary<string, int> scores = new()
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Carol"] = 92
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

ディクショナリをマージするには、ディクショナリをコピーしてオーバーライドを適用します。

Dictionary<string, int> defaults = new()
{
    ["Timeout"] = 30,
    ["Retries"] = 3
};
Dictionary<string, int> overrides = new()
{
    ["Timeout"] = 60
};

// Merge defaults and overrides into a new dictionary
Dictionary<string, int> config = new(defaults);
foreach (var (key, value) in overrides)
{
    config[key] = value;
}

Console.WriteLine($"Timeout: {config["Timeout"]}");  // 60
Console.WriteLine($"Retries: {config["Retries"]}");   // 3

型制約

制約は 、ジェネリック型またはメソッドが受け入れる型引数を制限します。 制約を使用すると、 object 単独では使用できない型パラメーターに対してメソッドまたはアクセス プロパティを呼び出します。

static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

static T CreateDefault<T>() where T : new() => new T();

var list = CreateDefault<List<int>>(); // Creates an empty List<int>
Console.WriteLine($"Empty list count: {list.Count}"); // 0

最も一般的な制約は次のとおりです。

制約 説明
where T : class T は参照型である必要があります
where T : struct T は非 null 値型である必要があります
where T : new() T パラメーターなしのパブリック コンストラクターが必要です
where T : BaseClass TBaseClass から派生する必要があります
where T : IInterface TIInterface を実装する必要があります

where T : class, IComparable<T>, new()制約を組み合わせることができます。 あまり一般的でない制約には、特殊なシナリオの where T : System.Enumwhere T : System.Delegate、および where T : unmanaged が含まれます。 完全な一覧については、「 型パラメーターの制約」を参照してください。

共変性と反変性

共変性反変性 は、ジェネリック型が継承でどのように動作するかを表します。 これらは、最初に指定したよりも派生型の引数を使用できるか、より少ない派生型引数を使用できるかを決定します。

// Covariance: IEnumerable<Dog> can be used as IEnumerable<Animal>
// because IEnumerable<out T> is covariant
List<Dog> dogs = [new("Rex"), new("Buddy")];
IEnumerable<Animal> animals = dogs; // Allowed because Dog derives from Animal

foreach (var animal in animals)
{
    Console.WriteLine(animal.Name);
}

// Contravariance: Action<Animal> can be used as Action<Dog>
// because Action<in T> is contravariant
Action<Animal> printAnimal = a => Console.WriteLine($"Animal: {a.Name}");
Action<Dog> printDog = printAnimal; // Allowed because any Animal handler can handle Dog

printDog(new Dog("Spot"));
  • 共変性 (out T): IEnumerable<Dog>IEnumerable<Animal>から派生しているため、Dogが予想される場所でAnimalを使用できます。 型パラメーターの out キーワードを使用すると、これを有効にできます。 共変型パラメーターは、出力位置 (戻り値の型) にのみ表示できます。
  • 反変性 (in T): Action<Animal>を処理するアクションでもAction<Dog>を処理できるため、Animalが予想される場合は、Dogを使用できます。 in キーワードを使用すると、これを有効にできます。 反変型パラメーターは、入力位置 (パラメーター) でのみ使用できます。

多くの組み込みインターフェイスとデリゲートは既にバリアント型です。 IEnumerable<out T>IReadOnlyList<out T>Func<out TResult>Action<in T>。 これらの型を使用する場合は、分散を自動的に利用できます。 バリアント インターフェイスとデリゲートの設計の詳細な処理については、 共変性と反変性に関するページを参照してください。

独自のジェネリック型を作成する

独自のジェネリック クラス、構造体、インターフェイス、およびメソッドを定義できます。 次の例は、簡単な汎用リンク リストを示しています。 実際には、 List<T> または別の組み込みコレクションを使用します。

public class GenericList<T>
{
    private class Node(T data)
    {
        public T Data { get; set; } = data;
        public Node? Next { get; set; }
    }

    private Node? head;

    public void AddHead(T data)
    {
        var node = new Node(data) { Next = head };
        head = node;
    }

    public IEnumerator<T> GetEnumerator()
    {
        var current = head;
        while (current is not null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}
var list = new GenericList<int>();
for (var i = 0; i < 5; i++)
{
    list.AddHead(i);
}

foreach (var item in list)
{
    Console.Write($"{item} ");
}
Console.WriteLine();
// Output: 4 3 2 1 0

ジェネリック型はクラスに限定されません。 ジェネリック interfacestruct、および record 型を定義できます。 ジェネリック アルゴリズムと複雑な制約の組み合わせの設計の詳細については、「.NET のGenerics」を参照してください。

こちらも参照ください