ジェネリック型数値演算のサポート(Generic math support)
C# 11では、ジェネリック型数値演算(Generic math)のサポートのために、いくつかの機能が導入されました。
ジェネリック型数値演算とは
ジェネリック型数値演算とは、.NET 7で利用できるようになった数値演算関連のジェネリックインタフェースにより、数値型(intやfloatなど)の処理ロジックをジェネリクスを用いて記述できるようにすることです。これまでは、合計や平均といった処理ロジックを数値型ごとに用意する必要があり、効率的と言えませんでした。主なジェネリックインタフェースは以下の通りで、System.Numerics名前空間にて提供されます。
- INumber<TSelf>…数値型を定義するインタフェース
- IBinaryNumber<TSelf>…バイナリ形式で表される数値型を定義するインタフェース
- IBinaryInteger<TSelf>…バイナリ形式で表される整数型を定義するインタフェース
- IFloatingPoint<TSelf>…浮動小数点数型を定義するインタフェース
- IFloatingPointIeee754<TSelf>…IEEE754規格による浮動小数点数型を定義するインタフェース
ここでTSelfとは、対象の数値型を表します。基本となるのはINumber<TSelf>インタフェースで、これから派生する整数型のためのIBinaryNumber<TSelf>とIBinaryInteger<TSelf>、浮動小数点数型のためのIFloatingPoint<TSelf>とIFloatingPointIeee754<TSelf>があります。INumber<TSelf>インタフェースは、比較演算や算術演算などのための数多くのインタフェースを継承しており、これらによって演算に関する機能の利用が保証されています(図1)。
例えば以下のようにINumber<T>を実装する型T(System.SByte、System.int64、System.Doubleなど)について、平均値を求めるジェネリックな静的メソッドを定義することができるようになります。AdditiveIdentityプロパティによってゼロに相当する値を取得したり、CreateTruncatingメソッドによって引数から値を生成(表現できない範囲は切り捨てる)したりできます。
using System.Numerics; // INumber<T>に必要 // 型Tの平均値を求めるメソッド static T average<T>(IEnumerable<T> nums) where T : INumber<T> // INumber<T>を実装する型に限定 { var sum = T.AdditiveIdentity; // T型の0に相当する値を取得 var count = T.CreateTruncating(nums.Count()); // 要素数をT型として取得 foreach (var i in nums) sum += i; // 総和を求める return sum / count; // 平均値を返す } // averageメソッドは、あらゆる数値型のコレクションを受け入れる Console.WriteLine(average(new byte[] { 1, 2, 3, 4, 5 })); Console.WriteLine(average(new int[] { 5, 6, 7, 8, 9 })); Console.WriteLine(average(new decimal[] { 10, 20, 30, 40, 50 })); Console.WriteLine(average(new float[] { 1.0F, 2.0F, 3.0F, 4.0F, 5.0F })); Console.WriteLine(average(new double[] { 5.0, 6.0, 7.0, 8.0, 9.0 }));
C# 10までは、数値型に応じたメソッド定義がそれぞれ必要だったので、コードの重複が発生するなど煩雑になりがちでした。これをジェネリック型としてまとめることができ、メソッドの記述がシンプルになります。
なお、Generic mathそのものは機能の名称ではなく、あくまでも数値型の処理のための一概念となっています。以降に紹介するいくつかの新機能により、Generic mathは実現されています。Generic mathの利用において、これらの理解は必須ではありませんが、C# 11における重要な変更なので一通り紹介します。
インタフェースの静的仮想メンバ(static virtual members in interfaces)
C# 11では、インタフェースの静的なメンバに対して、抽象(abstract)と仮想(virtual)の指定が可能になりました。
一般的なインタフェースとは、クラスが実装しなければならないメソッドのシグネチャのみを定めたものです。インタフェースはクラスに継承され、そのクラスがメソッドの実装を行います。インタフェースで定義されるメソッドは、実体を持たないことから抽象メソッドと呼ばれます。
これはインタフェースの基本を表したものですが、C#ではいくつかの機能緩和が施されており、インタフェースで持てるものが変化しています。特にC# 8以降では、以下のように変化してきました。
- メソッド、プロパティ、インデクサ、イベントのアクセサの実装を持てる
- アクセシビリティを明示的に指定できる
- 静的なメンバを持てる
本来、インタフェースは実装を持たないことになっていますが、既定としての実装を持てるようになっています。これは、「インタフェースのデフォルト実装」(default implementations of interfaces)と呼ばれます。
また、インタフェースのメンバは基本的にpublicであったのを、クラスのようなアクセシビリティ(たとえばprotectedなど)を指定できるようになっています。さらに、静的なメンバまで持てるようになっており、定数や演算子のメソッドをインタフェースに実装することができます。
ただし、C# 10までは静的メンバを抽象メンバや仮想メンバにすることはできないという制約がありました。これが演算子のオーバーロードが必要になるGeneric mathにおいては不都合ということで、C# 11においては静的な抽象メソッドが定義可能となっています。これにより、ジェネリック型ごとに演算子をオーバロードできます。図1で示したように、INumberインタフェースはIAdditionOperatorsインタフェースを継承していますが、例えば加算演算子のための抽象メソッドは以下のように定義されています。
interface IAdditionOperators<TSelf, TOther, TResult> { // 加算演算子のための抽象メソッド public static abstract TResult operator + (TSelf left, TOther right); }
checked演算子(checked user defined operators)
C# 11では、演算子のオーバロードにおいてcheckedキーワードを付加することで、オーバフロー時の例外の発生の有無を切り替えられるようになりました。
C#では、オーバフロー(桁あふれ)の発生時に例外を発生するか、コンパイルオプション/checked+で指定することができます。この指定を、コードの一部にのみ適用するようにできるのが、checkedキーワードです。checkedを指定されたブロックや式は、その部分でだけオーバフローがチェックされるようになります。例えば、以下のように記述します。
checked { sbyte a = 0x61; // 97 sbyte b = 0x62; // 98 sbyte x = (sbyte)(a + b); // 195なので符号付きbyteではオーバフロー例外 } あるいは sbyte a = 0x61; // 97 sbyte b = 0x62; // 98 sbyte x = checked((sbyte)(a + b)); // 195なのでオーバフロー例外
ただしC# 10までは、オーバフロー例外を発生させられるのは組み込みの整数型のみとされていました。これがGeneric mathのために、C# 11ではユーザ定義の数値型(ただし整数型に準ずる型に限る)において演算時のオーバフロー例外の発生を制御できるようになりました。Generic mathでは、ジェネリック型における演算のオーバフロー例外の発生を制御できるようになります。
なお、checkedキーワードを指定した演算子オーバロードを定義する場合には、以下のようにcheckedキーワードなしの演算子のオーバーロードも必須となります。
public static Sample operator +(Sample a, Sample b) => default; public static Sample operator checked +(Sample a, Sample b) => default;
シフト要件の緩和(relaxed shift operators)
C# 11では、シフト演算子における右辺のデータ型の制限がなくなりました。
C# 10までは、「>>」や「<<」といったシフト演算子の右辺のデータ型はintのみと決まっていました。例えば以下のような4Lというlong型の指定はできませんでした。
int ivalue = 32767; int ishifted = ivalue >> 4L; // エラー
現実的にはシフト回数はintで十分なため、このような制約も問題なかったのですが、Generic mathでは数値型がジェネリクスでなければならないため、intしか認めないという制約には不都合が生じることになります。
そこで、C# 11では任意のデータ型(ただしint型に変換可能なもの)を指定できるようにすることで、右辺への型パラメータによる指定が可能になりました。例えば以下は、C# 11から記述できるようになる演算子オーバーロードの書式です。C# 10までは、引数yの型はintのみが指定できました。Generic mathでは、TSelf型(intなどの型パラメータになっている型)によるジェネリクス型を右辺に指定できるようになっています。
public static Sample operator <<(Sample x, Sample y) => default;
符号なし右シフト演算子(unsigned right-shift operator)
C# 11では、整数型の符号付きと符号なしにかかわらず、符号なし右シフトすなわち論理右シフトとする演算子「>>>」が追加されました。
右シフト演算子の通常の動作は、オペランドの符号の有無によって符号なし、あるいは符号ありの右シフト操作を行うというものです。符号なしの右シフト、すなわち論理右シフトとしたければ、オペランドのデータ型の符号なし版(intならuint)にキャストし、右シフト演算することになります。
int ivalue = 32767; uint ishifted = (uint)ivalue >> 4; long lvalue = 0x100000000L; ulong lshifted = (ulong)lvalue >> 16;
このように、対応する符号なしの型が自明であれば何の問題もないのですが、ジェネリック型では型引数に対応する符号なし型を取得することができない、という問題が発生します。例えば、以下は符号なしのTという意味のunsigned T型でのキャストを指定しようとしていますが、実際にはこのような記述はできません。
(T)((unsigned T)s >> bits) // このような記述はできない
そこで、ジェネリック対応とするときには、Tの型にかかわらず、強制的な符号なしの右シフト演算が必要となります。それが、C# 11で導入された「>>>」演算子です。これにより、ジェネリクス型における型パラメータにかかわらず、常に符号なしの右シフト(論理右シフト)演算を行うことができるようになります。Generic mathにおいては、キャストの使えない状況で符号付きの整数型に対して論理右シフト演算を記述しなければならないときに、これが問題なくできるようになります。上記のような演算は、以下のように明示的な符号なしの右シフト演算とできます。
(T)(s >>> bits)
まとめ
今回は、C# 11における新機能のうち、オブジェクトの初期化やジェネリック型数値演算に関連する機能を中心に紹介しました。
次回は、refフィールドといった参照系の機能を中心に、switch文などで使えるパターンマッチングの拡充などについて紹介します。