将来の.NETの新機能として検討されているのが型クラスだ。shapeと拡張の提案で“shapes”として言及されるように、これによって.NETジェネリクスの可能性は飛躍的に向上する。Mads Torgersen氏は型クラスについてこう述べる。
インターフェイスはオブジェクトのshapeと型のインスタンスである値を抽象化する。型クラスの背後にあるアイディアは本質的に、型のshapeを自身の代わりに抽象化することだ。さらに、あるインターフェイスを実装するという宣言を通じて型をオプトインする必要がある場合、他者が別のコードに型クラスを実装することが可能となる。
型クラスは、インターフェイスにまつわる長年の問題を解決する。インターフェイスは静的関数や演算子のオーバーロードを扱うことができない。これにより、全ての異なる数値型を計算するために同じ関数を数値計算ライブラリで何度も宣言しなければならない、といった問題につながる。
Mads氏は続ける。
一般的に、"shape"宣言はインターフェイスの宣言と非常によく似ている。以下を除いて。
ほぼ全ての種類のメンバー(静的メンバーを含む)を定義できる
拡張によって実装できる
特定の場所においてのみ型のように扱える
最後の制限は重要だ。shapeは型ではない。代わりに、shapeの第一の目的は、ジェネリックな制約として使用されることだ。型引数が適切なshapeをもつことを制限し、一方でジェネリックの宣言の本体にはそのshapeの使用を許容する。
shapeの考えに密接に関連するのが、改良された拡張の文法だ。ただの拡張メソッドというよりも、拡張コンストラクトは型クラスのためのほぼ全てを提供できる。次の単純化された例を考えよう。
public shape SNumber<T>
{
static T operator +(T t1, T t2);
static T operator -(T t1, T t2);
static T Zero { get; }
}
Int32型はすでにこのほとんどを提供するが、zeroプロパティが不足している。拡張はこれを修正可能だ。
public extension IntGroup of int : SNumber<int>
{
public static int Zero => 0;
}
したがって次のように利用できる。
public static AddAll<T>(T[] ts) where T : SNumber<T> // shape used as constraint
{
var result = T.Zero; // Making use of the shape's Zero property
foreach (var t in ts) { result += t; } // Making use of the shape's + operator
return result;
}
実装
この実装は、インターフェイスと構造体において多少のトリックが必要だ。
- shapeはインターフェイスに変換される。このとき、各メンバー(静的メンバーを含む)はインターフェイスのインスタンスメンバーになる。
- 拡張は構造体に変換される。このとき、各メンバー(静的メンバーを含む)は構造体のインスタンスメンバーになる。
- 拡張が1つ以上のshapeを実装するとき、基底の構造体はそれらshapeの基底のインターフェイスを実装する。
上で述べた構造体はよく“witness struct”と呼ばれる。その存在は、クラスがshapeのルールに従うことを証明する。あるいは言い換えると、そのクラスは型クラスに含まれる。
これはコンパイラーが変換したあとの同じAddAllメソッドだ。
public static T AddAll<T, Impl>(T[] ts) where Impl : struct, SNumber<T>
{
var impl = new Impl();
var result = impl.Zero;
foreach (var t in ts) { result = impl.op_Addition(result, t); }
return result;
}
前述のwitness structはAddAllメソッドに必要な機能を提供するために用いられる。構造体はその型を用いてメソッドを直接呼ぶか、または必要に応じて拡張コンストラクトを使用できる
クラスとインターフェイス内でのshapeの実装
クラスは、基底クラスを継承したりインターフェイスを実装したりするのと同じ文法で明示的にshapeを実装できる。それによりコンパイラーは一致するwitness structを提供する。
インターフェイスはshapeの要求を満たすようにマークをつけることもできる。これが例だ。
public extension Comparable<T> of IComparable<T> : SComparable<T> ;
Comparable<T>とこの理論的な型クラスは1対1対応するため、拡張コンストラクトのために本体を記述する必要はない。
ジェネリック型
ジェネリック型は自身に問題があることを証明する。ジェネリックメソッドと同様に、shapeや型クラスを型制約としてジェネリッククラスに加えると、追加の型パラメーターが必要になる。ジェネリッククラスにおける型パラメーターの数はその名前の一部であるため、他の同じ名前を共有するジェネリック型との衝突につながる可能性がある。
shapeの拡張
拡張コンストラクトはshapeの実装だけでなく、それらを拡張することもできる。つまり既存のshapeにメソッドや静的関数、演算子を追加することが可能だ。拡張メソッドと同様、基底の型で直接定義されているかのように、文法は同じだ。
批判
全体として、この機能に対するフィードバックはポジティブなものだ。しかし、いくつかの変更も要求されている。例えば、shapeはいまのところ明示的に実装される必要がある。開発者の中には、追加の拡張メソッドが与えられたクラスやインターフェイスに必要ないのであれば、shapeはコンパイラーによって暗黙的に実装されたほうが好みだという人もいる。Mads氏はこのことについて、いくつかの問題を引用した。
これにより、同じ型に同じ方法で適用されるような同じshapeをwitness化するために、多くの構造体型が生成されるようになるかもしれない。そのため生成される型をむやみに急増させてしまうリスクがある。可能性としては、これはコンパイラーが賢くなって1アセンブリにつき1つしか生成しないようになるようになれば取り除くことができるかもしれない。しかし我々は匿名型から、この類の重複排除は難しく、エラーと隣合わせであることを学んでいる。
もしジェネリック型にshape制約の型パラメーターを保持できるようにした場合は、同一のものに複数のwitness structを保持させることによって、インスタンス化されたジェネリック型が異なる型同一性をもつようになり、互換性がなくなる。
shapeと拡張がいかに密接に結合しているかに関する懸念もある。これにより、実現までの道のりを混乱させることが判明してしまうかもしれないと考えられている。
Mads氏はこれに対して答えた。
合成について: 私の提案における"extensions"は確かに複数の懸念を合成している。
[…]
私は、これらの目的全てに対応する同一の言語メカニズムに対して述べるべきことは多くあると思うが、それらは結局のところ、密接に関連している。しかし、明確に両者を分割している提案を見ることは役に立つはずだ。よりエレガントなところへ続くだろう。
Rate this Article
- Editor Review
- Chief Editor Action