C#ジェネリックスのマスター
C#のジェネリックスは、その導入以来、言語の不可欠な一部となっており、その仕組みがすべての開発者に完全に理解されていなくても、多くの利点を提供しています。 動画"How To Create Generics in C#, Including New Features"では、ティム・コーリーがジェネリックがなぜ重要なのか、どのようにジェネリックを作成するのかを説明し、その実用的なアプリケーションを実演しています。
この記事では、C#ジェネリックスの包括的なガイドを提供し、このトピックに関するティム・コーリーのビデオからの貴重な洞察を提供します。 型安全性、パフォーマンスの利点、実用的なアプリケーションを含むジェネリックの基礎をカバーしています。 この記事では、ジェネリック・メソッド、クラス、インターフェースの作成、およびジェネリックへの制約の適用についても説明します。 さらに、実際の使用例と、効率的で型安全なコードを書くためにジェネリックを使用することの重要性を強調します。
はじめに
C#ジェネリックスは、開発者があらゆるデータ型で動作するクラス、メソッド、インターフェイス、およびコレクションを定義できるようにすることで、柔軟で再利用可能な、型安全なコードを作成するための強力な方法を提供します。 ジェネリッククラスまたはジェネリックメソッドを使用することで、開発者は、任意のデータ型を表すことができるジェネリック型パラメータ(例えば、T)を定義することができます。 これにより、コードの重複をなくし、コンパイル時に型の安全性を確保しながらコードの再利用を向上させます。List や Dictionary<TKey, TValue> のようなジェネリックコレクションクラスはさまざまなデータ型の効率的な処理を可能にし、ジェネリックインターフェースやジェネリックデリゲートは複数の型パラメータと共に動作するカスタムジェネリック型の作成を可能にします。 ジェネリックを活用することで、開発者はコードの効率を最大化し、ジェネリックでないクラスの欠点を最小化することができます。 この柔軟性により、パフォーマンスや型安全性を犠牲にすることなく、より再利用性の高いコードを作成することができます。
Tim at (0:00)は、C#でジェネリックスが広く使われていることを強調し、このトピックを紹介しています。 なぜジェネリックが不可欠なのかを説明し、ジェネリックの作成方法と効果的な使用方法を示すことを目指しています。
プロジェクトの作成
Timはまず、"GenericsDemoApp"という名前の新しいコンソール・アプリケーションを作成し、UIに気を取られることなくジェネリックスのデモだけに集中します。 彼は、.NET 8とVisual Studio 2022を使用してプロジェクトをセットアップします。
ジェネリックスの基本
Tim は (2:22) にて、ジェネリクスの概念を説明するために List クラスを使用した例から始めます。 ジェネリックスは、コレクションが保持できる要素の型を指定することを可能にし、型の安全性を提供し、実行時のエラーを回避します。
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };List<int> numbers = new List<int> { 1, 2, 3 };
List<string> strings = new List<string> { "Tim", "Corey", "Sue" };List は、numbers リストには整数のみが追加でき、strings リストには文字列のみが追加できることを保証します。
タイプの安全性と効率性
Timは、ジェネリックスが提供する型安全の重要性を強調します。 コンパイラは設計時に型をチェックし、型の不一致を防ぎ、安全なコード実行を保証します。 また、ジェネリックスは、ボックス化やアンボックス化の必要性を回避し、より効率的なコードに導きます。
非ジェネリックコレクションの非効率性
非一般的なコレクションを使用することの非効率性を示すために、TimはListを作成し、それがどのように異なるタイプのオブジェクトを保持できるかを示します。
List<object> objects = new List<object> { "Tim", 4, 3.6m };List<object> objects = new List<object> { "Tim", 4, 3.6m };非ジェネリック・コレクションを使用すると、型の不一致や、ボクシングとアンボクシングによる非効率につながる可能性があることを説明しています。
パフォーマンス比較
(6:15)のTimは、100万項目を追加するためにList
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 1; i <= 1_000_000; i++)
{
objects.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of object elapsed time: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Restart();
for (int i = 1; i <= 1_000_000; i++)
{
numbers.Add(i);
}
stopwatch.Stop();
Console.WriteLine($"List of int elapsed time: {stopwatch.ElapsedMilliseconds} ms");その結果、List

タイプチェッカー・メソッドの作成
(10:14)のTimは、TypeCheckerという名前のジェネリックメソッドを作成する方法を示している。 このメソッドは、与えられた値の型をチェックしてそれを出力し、ジェネリックスの柔軟性とパワーを説明します。
public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}public static void TypeChecker<t>(T value)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {value}");
}TypeChecker メソッドは、typeof 演算子を使用してジェネリック・パラメータ T の型を決定し、型と値の両方を出力します。
タイプチェッカーメソッドの使用
Timは、さまざまなタイプの引数でTypeCheckerメソッドを呼び出す例を示している。
TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1TypeChecker(1); // Type: System.Int32, Value: 1
TypeChecker("Tim"); // Type: System.String, Value: Tim
TypeChecker(1.1); // Type: System.Double, Value: 1.1TypeCheckerメソッドに様々な型を渡すことで、Timはジェネリックがいかにシームレスに異なるデータ型を扱えるかを示している。
汎用クラスを作成する:より良いリスト
Timは(16:25)で、BetterListという名前のジェネリック・クラスの作成に移る。このクラスは指定された型のリストをカプセル化し、追加機能を提供する。
public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}public class BetterList<t>
{
private List<t> data = new List<t>();
public void AddToList(T value)
{
data.Add(value);
Console.WriteLine($"{value} has been added to the list");
}
}BetterList クラスには、非公開の List およびリストに値を追加し、追加を示すメッセージを印刷する AddToList メソッドが含まれています。
ベター・リスト・クラスの使用
Timは、BetterListクラスをさまざまな型で使用する例を示しています。
BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));BetterList<int> betterNumbers = new BetterList<int>();
betterNumbers.AddToList(5);
BetterList<PersonRecord> people = new BetterList<PersonRecord>();
people.AddToList(new PersonRecord("Tim", "Corey"));これらの例では、BetterList
ジェネリック インターフェイスを作成する
Timは(21:48)で、IImportanceという名前の汎用インターフェースのアイデアを紹介している。 このインタフェースは、2つの値のどちらが重要かを決定する方法を定義します。
public interface IImportance<t>
{
T MostImportant(T a, T b);
}public interface IImportance<t>
{
T MostImportant(T a, T b);
}ジェネリック インターフェイスの実装
Timは、さまざまな型に対してこのインターフェイスを実装する方法を示します。彼は整数の実装から始めます。
public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}public class EvaluateImportance : IImportance<int>
{
public int MostImportant(int a, int b)
{
return a > b ? a : b;
}
}次に、文字列のインターフェイスを実装し、文字列の長さを使って重要度を決定する。
public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}public class EvaluateStringImportance : IImportance<string>
{
public string MostImportant(string a, string b)
{
return a.Length > b.Length ? a : b;
}
}これらの実装は、同じインターフェイスを、それぞれのタイプに固有のロジックを持つ異なるタイプにどのように適用できるかを示しています。
ジェネリクスに制約を適用する
(25:21)のTimは、ジェネリックスに制約を適用する方法を説明し、ジェネリックスが特定の条件を満たすことを保証します。 例えば、ジェネリック型は、空のコンストラクタを持つことや、特定のインターフェースを実装することを制約することができます。
public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}public class SampleClass<t> where T : new()
{
// Class implementation
}
public class SampleClassWithInterface<t> where T : IImportance<t>
{
// Class implementation
}これらの制約は、汎用型が必要な基準を満たしていることを保証し、実行時のエラーを防ぎ、型の安全性を高めるのに役立ちます。
マイクロソフトによる INumber の実装
Timは、MicrosoftがINumberインタフェースを使用して数値演算を制約する方法について説明します。 これは、汎用型に対する加算や減算などの算術演算を可能にします。
public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}public class MathOperations<t> where T : INumber<t>
{
public T Add(T x, T y)
{
return x + y;
}
}ジェネリック型 T を INumber に制約することで、その型が数値演算をサポートすることを保証します。
異なる数値型でジェネリックスを使用する
Timは(33:55)でMathOperationsクラスについて説明し、ジェネリックスが倍数や小数などの異なる数値型でどのように使用できるかを示します。
Timは、整数と倍数のMathOperationsのインスタンスを作成する方法を示しています:
MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8MathOperations<int> intMath = new MathOperations<int>();
Console.WriteLine(intMath.Add(1, 4)); // Outputs: 5
MathOperations<double> doubleMath = new MathOperations<double>();
Console.WriteLine(doubleMath.Add(1.5, 4.3)); // Outputs: 5.8これは、ジェネリックスの柔軟性を示すもので、異なる数値型を同じクラス内でシームレスに扱うことができます。
さまざまな数値型を扱う
ティムは、異なる数値型を混ぜてはいけないことを示すことで、型安全の重要性を強調している。例えば、doubleをintegerに加えようとすると、コンパイル時にエラーになる。
// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));// This will result in a compile-time error
// Console.WriteLine(intMath.Add(1.5, 4));タイプ変換のオーバーヘッドを回避する
Timは、型変換に伴うオーバーヘッドを避けるためにジェネリックスを使用する利点について説明します。 例えば、整数を倍数に変換して数学演算を行い、再び整数に戻すにはコストがかかります。 ジェネリックスを使用することで、ネイティブの型に対する直接操作が可能になり、パフォーマンスと精度が保たれます。
ジェネリックの実際
Timは、ジェネリックを使用する際には注意するようアドバイスし、開発者がジェネリックを適切に使用し、使い過ぎないようにすることを推奨しています。 ジェネリックスの利点である、型安全性、ボックス化とアンボックス化の削減、コンパイル時のチェック、コードの可読性の向上などを強調しています。
彼はまた、ジェネリクスが List や Dictionary<TKey, TValue> のようなコレクション、およびさまざまな型を事前に特定する必要なく処理できるログフレームワークに広く使用されていることを指摘しています。
結論
Tim CoreyによるC#における高度なジェネリックの詳細な探求は、その実用的なアプリケーションと利点に関する貴重な洞察を提供します。 ジェネリックスの理解を深め、実際の例を見たい場合は、C# Generics に関する Tim Corey の詳細なビデオをぜひご覧ください。 明確な説明と実践的なデモンストレーションにより、コンセプトを完全に把握し、自身のプロジェクトに効果的に適用することができます。

