これまでジェネリックメソッドとジェネリッククラスを紹介してきました。「なぜ型引数に対して制約を付け加える必要があるのか?と考える人も多いかと思います。「」では「どんな型でもOKとしてしまうのは不安です。」と一文だけしか触れませんでした。
ここでは、その「なぜ」につい考えてみたいと思います。
型に対して制約を加えるということは「型引数の前提条件」を作ることができるので重要になります。ジェネリックメソッドやジェネリッククラスでは「where T : IComparable」を多用してきたので、まずはその例で考えてみます。
IComparable のインターフェースの定義を確認してみると以下のように記載されています(説明文などは削除して簡略化しています)。
namespace System
{
public interface IComparable
{
int CompareTo(object? obj);
}
}
IComparable インターフェース には CompareTo メソッドが定義されています。インターフェースは「契約」であるため、IComapable が定義されているということは「CompareTo メソッドが必ず実装されている」という前提になります(コンポジション)。
static T Max(T a, T b) where T : IComparable
{
return a.CompareTo(b) >= 0 ? a : b;
}
上記のようにジェネリックメソッドを定義しており、制約として IComparable を課しているというのは、逆に言うと「CompareTo メソッドが必ず定義されている」ということであり「どんな引数が来ても CompareTo メソッドは使用できる」ということを示しています。
この前提があるため、int, double, float などを型引数として与えたクラスやメソッドは CompareTo メソッドを軸に処理を組み立てることができるということに他ならないのです。
static T MinusMethod(T x, T y)
{
return x - y;
}
例えば上記のようにメソッドを定義しているとしましょう。型引数に対する制約がない場合、T x と T y は、演算子「-」に対応するメソッドが定義されているかわかりせん。
演算子に対応する処理を持っていない型も存在するからです。
こういった制約のない条件での定義は不具合を生む可能性もありますし、メソッドやクラスとして共通項を持たないため煩雑な共通化しかできなくなります。このような不安定な前提ではメソッドを使用するのは怖いですよね。
このような事態を防ぐためにも制約が必要で、制約によって「型の安全性」「タイプセーフ」が保たれるのです。最低限の前提がしっかり定義されていることで、安全な共通化をおこなうことができるということになります。
型制約の種類
型制約でよく使われる代表的なサンプルを一覧で紹介します。以下の型制約を知っておくだけでも、ジェネリックとして使用する分には十分と思うのでしっかりと押さえておきましょう。
定義 | 意味 |
---|---|
where T : struct | 値型のみ |
where T : class | 参照型のみ |
where T : クラス名 | 指定したクラス、もしくは派生クラス |
where T : インターフェース名 | 指定したインターフェースを持つクラス |
以上でジェネリックの基礎的な解説が完了です。ジェネリックは「型が異なるが処理内容が同じであるもの」を共通化することができる便利な機能であるため、知っておいて損のない記述方法です。
単純にコーディング作業を行う上ではあまり使うことはないかもしれませんが、共通的なAPIを作成するような立場になった場合、型制約などを考慮しながらメソッドやクラスを定義していくスキルは必須になるはずです。
ジェネリックは単なるプログラミングから考えるとより抽象度の高いコーディング作業になりますが、共通化することでコード数を抑えることができ、かつ型制約などで強固で安全性の高い設計を行うことができるようになります。