ここではC#のジェネリックを学習します。もしくはジェネリック型と呼びます。
また、C++だとテンプレートと呼ばれ、機能的にはC#よりも優れてます。
ジェネリック(generic)
ジェネリックとは、汎用的な型を利用してメソッドやクラスの処理を共通化する機能です。
そして、この時に利用する型をジェネリック型(generic type)と言います。
Genericは英語で汎用とか一般を意味します(たぶん)。
汎用的に使える型って意味ですか?
そうだよ(適当)。
雑...
ジェネリックを理解するにあたり、次のようなint型の数値を入れ替えるメソッドを考えます。
このメソッドは、2つのint型の引数をref参照で受け取り、参照先の数値を入れ替えます。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
Console.WriteLine($"i1={i1}, i2={i2}");
Swap(ref i1, ref i2);
Console.WriteLine($"i1={i1}, i2={i2}");
}
// int型の数値を入れ替える
internal static void Swap(ref int a, ref int b)
{
var tmp = a;
a = b;
b = tmp;
}
}
}
さらにdouble型の数値を入れ替えるメソッドを考えましょう。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
Swap(ref i1, ref i2);
Swap(ref d1, ref d2);
Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
}
// int型の数値を入れ替える
internal static void Swap(ref int a, ref int b)
{
var tmp = a;
a = b;
b = tmp;
}
// double型の数値を入れ替える
internal static void Swap(ref double a, ref double b)
{
var tmp = a;
a = b;
b = tmp;
}
}
}
では、ここで作成したint型とdouble型の2つのメソッドを比較してみましょう。
// int型の数値を入れ替える
internal static void Swap(ref int a, ref int b)
{
var tmp = a;
a = b;
b = tmp;
}
// double型の数値を入れ替える
internal static void Swap(ref double a, ref double b)
{
var tmp = a;
a = b;
b = tmp;
}
ほぼ同じですね。
仮に追加でchar型の値を入れ替えたいと思ったらどうする?
また同じようなメソッドを作ります。
そういう時に便利なのがジェネリックだよ。
先程の例のような、どう見ても同じような処理で型が違うパターンを解決してくれるのがジェネリックです。
それでは早速ジェネリックを利用したパターンに書き換えてみましょう。次のコードがジェネリック版です。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
// ジェネリック型の呼び出し
Swap<int>(ref i1, ref i2);
Swap<double>(ref d1, ref d2);
Console.WriteLine($"i1={i1}, i2={i2}, d1={d1}, d2={d2}");
}
// ジェネリック型
internal static void Swap<T>(ref T a, ref T b)
{
var tmp = a;
a = b;
b = tmp;
}
}
}
まず、次の箇所がジェネリック型メソッドの定義です。この<T>がジェネリック型を意味します。
// ジェネリック型
internal static void Swap<T>(ref T a, ref T b)
{
var tmp = a;
a = b;
b = tmp;
}
そして、このメソッドを呼び出してる部分がこれ。<>で引数の型を指定します。
// ジェネリック型の呼び出し
Swap<int>(ref i1, ref i2);
Swap<double>(ref d1, ref d2);
ちなみに呼び出し時は型推論できるため、<型>は書かなくても動きます。
// ジェネリック型の呼び出し
Swap(ref i1, ref i2);
Swap(ref d1, ref d2);
このように<T>を利用することで、型だけ異なる同じような処理を共通化できます。
また、<T>のTは単なる名前です。別の名前、例えば<Type>でも普通に動きます。
ただ、ほぼほぼ<T>と書きます。for文のi,j,kみたいな感じです。
このジェネリック型を利用した記述は2つあり、メソッドまたはクラスを対象にジェネリック型を定義します。
両者の違いはジェネリック型の有効範囲です。メソッドならメソッド内、クラスならクラス内が対象です。
ジェネリック型のメソッド(Generic methods)
これが先程のパターンです。ジェネリック型の範囲はメソッド内のみとなります。
// ジェネリック型のメソッド
internal static void Swap<T>(ref T a, ref T b)
{
var tmp = a;
a = b;
b = tmp;
}
ジェネリック型のクラス(generic classes)
こちらはクラスにジェネリック型を指定したパターンです。この場合の有効範囲はクラス内の全てです。
つまり、フィールドやプロパティも共通のジェネリック型を利用することができます。
namespace Sample
{
internal class Program
{
// ジェネリック型のクラス
internal class View<T>
{
private T value1;
private T value2;
public View(T value1, T value2)
{
this.value1 = value1;
this.value2 = value2;
}
public void Display()
{
Console.WriteLine($"value1={this.value1}, value2={this.value2}");
}
}
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
// ジェネリック型のインスタンスを生成
var v1 = new View<int>(i1, i2);
var v2 = new View<double>(d1, d2);
v1.Display();
v2.Display();
}
}
}
クラスの定義と一緒に<T>と書くことで、そのクラス内では定義したT型(ジェネリック型)を利用できます。
// ジェネリック型のクラス
internal class View<T>
{
private T value1;
private T value2;
public View(T value1, T value2)
{
this.value1 = value1;
this.value2 = value2;
}
public void Display()
{
Console.WriteLine($"value1={this.value1}, value2={this.value2}");
}
}
インスタンスの生成もメソッドと同じく<>で型を指定します。
なお、こちらは型推論は不可能。必ず利用する型名を記述します。
// ジェネリック型のインスタンスを生成
var v1 = new View<int>(i1, i2);
var v2 = new View<double>(d1, d2);
whereキーワード
whereキーワードは、ジェネリック型に対して制約を付加するために利用します。
制約と聞くとマイナスなイメージですが、C#ではこれを使わないとジェネリックが機能しません。
先に次のようなメソッドを考えます。
2つの数値を引数で受け取り、それを合算した値を返すメソッドです。
つまり単なる足し算です。ジェネリック型を利用すれば汎用的なメソッドを記述できそうですね。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
// ジェネリック型の呼び出し
var i = Addition<int>(i1, i2);
var d = Addition<double>(d1, d2);
Console.WriteLine($"i1+i2={i}, d1+d2={d}");
}
// ジェネリック型の足し算
internal static T Addition<T>(T a, T b)
{
return a + b;
}
}
}
こんな感じ。それでは実行してみよう。
これビルドエラーになります。
それが正しい。
実はこの記述では動きません。C++とかだと動くのですが、C#は型に厳しいことが影響してビルドエラーになります。
その理由は凄く単純です。ジェネリック型、つまりT型が足し算(+演算子)できる保証がないからです。
当然ですが、ジェネリック型はどんな型でも受け入れ可能です。その時に+演算子を利用できる保証がないのでビルドエラーになりました。
それを解決してくれるのがwhereキーワードです。これは受け入れ可能な型を絞り、演算可能なジェネリック型であることをコンパイラに示します。
逆を言えば、ジェネリック型の範囲から演算が不可能な範囲を除外します。これにより絶対に演算できることが保証されるとビルドエラーが消えます。
では、実際にwhereキーワードを使って、先程のパターンを改善しましょう。
namespace Sample
{
internal class Program
{
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
// ジェネリック型の呼び出し
var i = Addition<int>(i1, i2);
var d = Addition<double>(d1, d2);
Console.WriteLine($"{i1}+{i2}={i}, {d1}+{d2}={d}");
}
// ジェネリック型の足し算
internal static T Addition<T>(T a, T b)
where T : System.Numerics.INumber<T>
{
return a + b;
}
}
}
ポイントはwhereでINumber型を指定しました。よって、このジェネリック型のメソッドはINumber型を実装するクラスしか受け入れできません。
where T : System.Numerics.INumber<T>
このようにwhereで範囲を絞ることで、必要とする機能を有することをコンパイラに証明します。
ちなみにジェネリック型のクラスに記述する場合はこんな感じ。単に記述する場所が違うだけです。
namespace Sample
{
internal class Program
{
// ジェネリック型のクラス
internal class AdditionCalculator<T>
where T : System.Numerics.INumber<T>
{
private T value1;
private T value2;
public AdditionCalculator(T value1, T value2)
{
this.value1 = value1;
this.value2 = value2;
}
public T Calculate()
{
return this.value1 + this.value2;
}
}
static void Main(string[] args)
{
int i1 = 0;
int i2 = 8;
double d1 = 12.34;
double d2 = 56.78;
// ジェネリック型のインスタンスを生成
var ac1 = new AdditionCalculator<int>(i1, i2);
var ac2 = new AdditionCalculator<double>(d1, d2);
Console.WriteLine($"{i1}+{i2}={ac1.Calculate()}, {d1}+{d2}={ac2.Calculate()}");
}
}
}
このwhereキーワードには、直接的なクラスやインターフェイスだけではなく、何らかのクラスであるとか、nullではないって大雑把な記述もできます。
記述可能なパターンの例は以下です。もっと複雑な絞り込みが必要なら公式ドキュメントを確認してください。
where T : struct | T型はrecord struct型を含むnull非許容値型である。 |
where T : class | T型は参照型である。 |
where T : notnull | T型はnull非許容値型である。 |
where T : unmanaged | T型はnull非許容値型かつunmanaged型である。 |
where T : <基底クラス名> | T型は指定した基底クラスか派生クラスである。 |
where T : <インターフェイス名> | T型は指定したインターフェイスを実装している。 |
公式ドキュメントhttps://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters
https://learn.microsoft.com/en-us/dotnet/standard/generics/math
また、whereキーワードに記述する制約は,(カンマ)を利用して複数個を指定することができます。
ただし、条件が矛盾するような内容を記述するとビルドエラーになります。
where T : unmanaged, System.Numerics.INumber<T>
あとがき
このジェネリック型ですが、Library制作者でもない限り自分で作ることは殆どありません。
大抵の人は.NETに定義された大量のジェネリック型を利用するために使います。
ここでは.NETに定義されたジェネリック型を実際に利用してみましょう。なお、利用することが目的なので、1つ1つのジェネリック型については深く解説しません。System.Collections.Generic名前空間コレクション系のジェネリック型が定義されてる空間です。C#でジェネリック型と言えば、ここです。と言っても、ジェネリック型はあくまで総称なので、他の名前空間にも色々と定義されてます。この名前空間の中にも複数のジェネリック型が存在しますが、今回はList型と...
◆ C#に関する学習コンテンツ
この記事は参考になりましたか?
コメント