まえがき
最近、業務でC#を学ぶ機会があり、Goを書いてきたエンジニアの視点でC#を整理しています。
今回は ジェネリクス(Generics) について学んだ内容をまとめます。
なお、本記事はGoとC#の優劣を比較するものではありません。それぞれの言語の特徴や考え方の違いを理解することを目的としています。
ジェネリクスについて
ジェネリクスとは、型を後から指定できるようにし、同じ処理を複数の型で再利用するための仕組みです。
ジェネリクスを使うことで、例えば同じロジックを複数の型で利用できるため重複コードを削減することができます。
Goは、v1.18が2022年3月にリリースされて初めてジェネリクスが正式に導入されました。歴史はまだ浅いですが、個人的にはジェネリクスを使ったシステムも見るようになってきたかなという印象です。
ただ、C#を学び始めるとかなりの頻度で目にするようになりました。
特に、
List<T>
Dictionary<TKey, TValue>
Task<T>
などは実装していると日常的に利用するため、避けて通れない重要な機能かなと思います。
ジェネリックメソッド
まずは理解しやすいジェネリックメソッドからになります。C#もGoも基本的な文法としては同じです。
C#
// 引数(value)の型で返すメソッド
static T Echo<T>(T value)
{
return value;
}
分解すると以下のようになります。
| 要素 | 説明 |
|---|---|
<T> |
型パラメータ |
T value |
引数valueの型 |
T |
戻り値の型 |
Echo<T> |
ジェネリックメソッド |
例えば、
Echo("Hello");
を呼び出した場合は、
static string Echo(string value);
として扱うことができます。
Go
Goでは以下のようになります。
func Echo[T any](value T) T {
return value
}
分解すると以下のようになります。
| 要素 | 説明 |
|---|---|
[T any] |
型パラメータ T を定義 (any は「任意の型」を表す) |
value T |
引数valueの型 |
T |
戻り値の型 |
Echo[T any] |
ジェネリックメソッド |
例えば、
message := Echo("Hello")
を呼び出した場合は、
func Echo(value string) string
として扱うことができます。
ジェネリッククラス
C#
C#ではクラスそのものをジェネリック化できます。
public class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
}
var intBox = new Box<int>(100);
上記のように初期化することで、以下のクラスのように扱うことができます。
public class Box
{
public int Value { get; set; }
public Box(int value)
{
Value = value;
}
}
Go
Goでは構造体をジェネリック化できます。
type Box[T any] struct {
Value T
}
intBox := Box[int]{
Value: 123
}
上記のように初期化することで、以下の構造体のように扱うことができます。
type Box struct {
Value int
}
型制約(Constraints)
ジェネリクスでは、どんな型でも受け付けるだけではなく、特定の条件を満たす型だけ受け付けることもできます。
これを型制約(constraints)と呼びます。
今回の型制約が両言語での大きな違いかなと思います。
C#
型引数(T)に対する制約は、where T : 制約 という書き方で指定できます。
// 参照型のみを引数にとるメソッド
static void ConstraintMethod<T>(T value)
where T : class
{
// ...
return;
}
型引数(T)に対して
where T : class
と指定することで、参照型のみ利用可能になります。
ConstraintMethod(new Box<int>(123)); // 参照型(Box<int>)を引数に指定
よく使う制約
| 制約 | 内容 |
|---|---|
| where T : class | 参照型のみ |
| where T : struct | 値(非nullable)型のみ |
| where T : new() | 引数なしコンストラクタ必須 |
| where T : <基底クラス> | 特定の基底クラス派生に限定 |
| where T : <インターフェース> | 特定インターフェース実装 or 特定インターフェース自体を要求 |
Go
Go の制約の仕組みには「インターフェース」として表現します。C# のようにジェネリック型に制約を直接定義するような書き方は無いです。
まとめ
- ジェネリクスは、Goにとっては任意の機能、C#にとっては使用用途の多い重要な機能
- ジェネリックメソッドやジェネリッククラスの考え方はGoと似ている
- 型制約はC#では
where、Goでは「インターフェース」で表現する
個人的な感想
Goでもジェネリクスを利用するようになりましたが、ジェネリクスが無くても実装で大きく困ることはないです。
ただ、C#を学んでいて最も感じたのは、「ジェネリクスが実装の中にあるのは当たり前」という点でした。
Goでは共通アルゴリズムやデータ構造で活用する印象ですが、
C#では
List<T>
Dictionary<TKey, TValue>
Task<T>
Repository<T>
など、フレームワークやライブラリ全体でジェネリクスが活用されています。
個人的には、
- ジェネリックメソッド
- ジェネリッククラス
-
whereによる制約
あたりを学んだことで、C#のライブラリやフレームワークのコードがかなり読みやすくなった気がします。
次回以降もGoエンジニアの視点で、C#の特徴やGoとの違いを整理しながら学んでいこうと思います。