はじめに
先日の VisualStudio の更新によって、C#11
と .NET 7
が扱える環境が整ったので、.NET 7
でついに導入された Generic Math と呼ばれるものについてメモを書いていきます。
Generic Math とは?
.NET
には以前から IEquatable<T>
や IComparable<T>
があったものの、ジェネリクスなクラス・構造体に対して算術演算子や比較演算子を用いることができなかったのですが、それができるようになりました。
public T Max<T>(T a, T b) where T : INumber<T> {
return (a > b) ? a : b;
}
この Generic Math は、C#11
でインターフェースに静的仮想メンバーを定義できるようになったのと同時に実装されました。
C#11
の新機能によって、インターフェースに以下のような定義をすることが可能となりました。
public interface ISomething {
public static abstract int ParseString(string s);
public static abstract operator +(int l, int r);
public static abstract explicit operator int(long val);
}
ISomething
を継承したクラス・構造体は、上記で定義されている静的メンバをすべて実装する必要があります。
Generic Math で追加されたインターフェース
Generic Math によって、かなり多くのインターフェースが新たに追加されています。
これによって、公式ドキュメントの System.Int32
の説明が載っている ページ を見てもわかる通り、継承されているインターフェースの量が大変なことになっています。
各インターフェースが定義する演算子
算術演算子
以下のインターフェースの型パラメーターは、TSelf (演算子) TOther = TResult
の関係になっています。
-
IAdditionOperators<TSelf, TOther, TResult>
--+
演算子 -
ISubtractionOperators<TSelf, TOther, TResult>
---
演算子 -
IMultiplyOperators<TSelf, TOther, TResult>
--*
演算子 -
IDivisionOperators<TSelf, TOther, TResult>
--/
演算子 -
IMudulusOperators<TSelf, TOther, TResult>
--%
演算子
以下のインターフェースの型パラメーターは、TSelf(演算子) = TSelf
または (演算子)TSelf = TSelf
の関係になっています。
-
IIncrementOperators<TSelf>
--++
演算子 -
IDecrementOperators<TSelf>
----
演算子
単項算術演算子
これは、以下のような文脈で使用されます。 (-foo
の部分)
public void Example() {
int foo = 15;
Console.WriteLine(-foo); // -15 が出力される
}
以下のインターフェースの型パラメーターは、(演算子)TSelf = TSelf
の関係になっています。
-
IUnaryPlusOperators<TSelf>
--+
演算子 -
IUnaryNegationOperatos<TSelf>
---
演算子
ビット演算子
以下のインターフェースの型パラメーターは、TSelf (演算子) TOther = TResult
の関係になっています。
-
IBitwiseOperators<TSelf, TOther, TResult>
--&
,|
,^
,~
演算子 -
IShiftOperators<TSelf, TOther, TResult>
--<<
,>>
,<<<
演算子
比較演算子
以下のインターフェースの型パラメーターは、TSelf (演算子) TOther = TResult
の関係になっています。
ただし、TResult
には基本的に bool
が入ります。
-
IComparisonOperators<TSelf, TOther, TResult>
-->
,>=
,<
,<=
演算子 -
IEqualityOperators<TSelf, TOther, TResult>
--==
,!=
演算子
用途に応じた制約の決め方
INumber<TSelf>
を制約に指定しておけば、算術演算子や比較演算子が利用できるので、 それを指定すれば基本的には大丈夫だと思われます。
整数だけ
IBinaryInteger<TSelf>
を制約に加えると、整数型のみが指定できるようになります。
IBinaryInteger<TSelf>
の TSelf
には INumber<TSelf>
の制約も掛けられているので、 INumber<TSelf>
を制約に加える必要はないです。
(コードを見た時の分かりやすさを重視するなら INumber<TSelf>
も制約に加えてもいいかもしれないです)
符号付きの数だけ
ISignedNumber<TSelf>
を制約に加えると、符号付きの型のみが指定できるようになります。
ISignedNumber<TSelf>
の TSelf
には INumber<TSelf>
ではなく INumberBase<TSelf>
の制約が掛けられているので、 INumber<TSelf>
で使用できる演算子やメソッドなどを利用したい場合はそれを制約に加える必要があります。
符号無しの数だけ
IUnsignedNumber<TSelf>
を制約に加えると、符号無しの型のみが指定できるようになります。
ISignedNumber<TSelf>
と同じく、 TSelf
には INumberBase<TSelf>
の制約が掛けられています。
何が何を継承してるのかさっぱり分かない問題
どのインターフェースが何を継承しえているのか、その関係が全然分からず困っていましたが、ufcppさんがGitHubでわかりやすい図を作られていたのでここで共有させてもらおうと思います。
以下のリンクから該当ページに飛べます。
https://github.com/ufcpp/UfcppSample/issues/354#issuecomment-1166550011