covariant の戻り値
メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/49
まとめ
共変戻り値型への対応 具体的には、あるメソッドをオーバーライドして、そのオーバーライド対象であるメソッドよりもさらに派生した戻り値の型を宣言できるようにし、同様に、読み取り専用プロパティのオーバーライドでも、より派生した型を宣言できるようにします。 より強い派生型で定義されるオーバーライド宣言では、その基本型で定義されているオーバーライドの戻り値型と同等以上に具体的な型を返す必要があります。 このメソッドやプロパティの呼び出し元は、呼び出し時、より具体的な戻り値の型を静的に受け取ります。
目的
「オーバーライドはオーバーライドされたメソッドと同じ型を返さなければならない」という言語の制約を回避するために、異なるメソッド名を考え出す必要があるというのが、コードではよくあるパターンです。
この機能はファクトリ パターンで便利です。 たとえば、Roslyn のコード ベースでは、次のようになります。
class Compilation ...
{
public virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
public override CSharpCompilation WithOptions(Options options)...
}
詳細な設計
これは、C#における共変戻り値型の仕様です。 私たちの意図は、メソッドのオーバーライドが、そのオーバーライド元のメソッドよりも派生した戻り値の型を返すようにすることです。同様に、読み取り専用プロパティがより派生した戻り値の型を返すようにオーバーライドすることも許可することです。 メソッドやプロパティの呼び出し元は、呼び出し時、より具体的な戻り値の型を静的に受け取り、より強い派生型で定義されるオーバーライドでは、その基本型で定義されているオーバーライドの戻り値の型と同等以上に具体的な型を返す必要があります。
クラス メソッドのオーバーライド
クラスのオーバーライド (§15.6.5) メソッドに関する既存の制約
- オーバーライド メソッドとオーバーライドされる基底メソッドの戻り値の型は同一である必要があります。
次に変更
- オーバーライド メソッドには、ID 変換によって変換可能な戻り値の型が必要です。または (メソッドに ref return ではなく値 return がある場合は §13.1.0.5 を参照) オーバーライドされた基底メソッドの戻り値の型への暗黙的な参照変換が必要です。
その一覧に次の要件が新たに追加されています。
- オーバーライド メソッドには、ID 変換によって変換できる戻り値の型が必要です。または (メソッドに ref return、§13.1.0.5 ではなく値 return がある場合) オーバーライド メソッドの (直接または間接的な) 基本型で宣言されているオーバーライドされた基底メソッドのすべてのオーバーライドの戻り値の型への暗黙的な参照変換が必要です。
- オーバーライド メソッドの戻り値の型とオーバーライド メソッドのアクセシビリティは少なくとも同一である必要があります (§7.5.3)。
この制約により、private
クラスのオーバーライド メソッドの戻り値の型も private
であることが許可されます。 ただし、戻り値の型が public
であるためには、public
型の public
オーバーライド メソッドが必要です。
クラスのプロパティとインデクサーのオーバーライド
クラスのオーバーライド (§15.6.5) プロパティに関する既存の制約
オーバーライドするプロパティ宣言では、継承されたプロパティと完全に同じアクセシビリティ修飾子と名前を指定しなければならず、
オーバーライドするプロパティと継承されたプロパティの型の間には恒等変換が必要です。 継承されたプロパティに 1 つのアクセサーしかない場合 (つまり、継承されたプロパティが読み取り専用または書き込み専用である場合)、オーバーライドするプロパティはそのアクセサーのみを含める必要があります。 継承されたプロパティに両方のアクセサーが含まれている場合 (つまり、継承されたプロパティが読み書き可能な場合)、オーバーライドするプロパティにはどちらか一方または両方のアクセサーを含めることができます。
次に変更
オーバーライドするプロパティ宣言では、継承されたプロパティとまったく同じアクセシビリティ修飾子と名前を指定する必要があり、ID 変換が行われる必要があります。または (継承されたプロパティが読み取り専用で、ref return§13.1.0.5 ではなく値 return がある場合) オーバーライド プロパティの型から継承されたプロパティの型への暗黙的な参照変換が必要です。 継承されたプロパティに 1 つのアクセサーしかない場合 (つまり、継承されたプロパティが読み取り専用または書き込み専用である場合)、オーバーライドするプロパティはそのアクセサーのみを含める必要があります。 継承されたプロパティに両方のアクセサーが含まれている場合 (つまり、継承されたプロパティが読み書き可能な場合)、オーバーライドするプロパティにはどちらか一方または両方のアクセサーを含めることができます。 オーバーライドするプロパティの型とオーバーライドするプロパティのアクセシビリティは少なくとも同一である必要があります (§7.5.3).)。
以下のドラフト仕様の残りの部分では、後で検討するインターフェイス メソッドの共変戻り値型のさらなる拡張を提案しています。
インターフェイス メソッド、プロパティ、およびインデクサー オーバーライド
C# 8.0 での DIM 機能の追加により、インターフェイスで許可されるメンバーの種類が拡張され、共変戻り値に対応する override
メンバーのサポートも追加されます。 これらは、クラス用に指定されている override
メンバーの規則に従いますが、次の点が異なります。
クラスにおける次のテキスト:
override 宣言によってオーバーライドされるメソッドを、オーバーライドされる基底メソッドと言います。 クラス
C
で宣言されたオーバーライド メソッドM
について、オーバーライドされる基底メソッドは、C
の直接の基底クラスから始めて、C
の各連続する直接の基底クラスを順にたどり、型引数の置換後にM
とシグネチャが一致するアクセス可能なメソッドが少なくとも 1 つ見つかるまで調べることで決定されます。
インターフェースには、対応する仕様が与えられます。
override 宣言によってオーバーライドされるメソッドを、オーバーライドされる基底メソッドと言います。 インターフェイス
I
で宣言されたオーバーライド メソッドM
について、オーバーライドされる基底メソッドは、I
の直接または間接の基底インターフェイスをそれぞれ調べ、型引数の置換後にM
とシグネチャが一致するアクセス可能なメソッドを宣言するインターフェイスの集合を収集することで決定されます。 この一連のインターフェイスに 最も派生した型があり、このセット内のすべての型からその型への ID または暗黙の参照変換が可能で、その型が一意なメソッド宣言を含む場合、それが オーバーライドされた基本メソッドです。
同様に、§15.7.6 仮想、シール、オーバーライド、および抽象アクセサーでクラスに指定されているように、インターフェイス内の override
プロパティとインデクサーが許可されています。
名前検索
クラス override
宣言が存在する場合の名前参照では、識別子の修飾子の型(または修飾子が存在しない場合は override
)から開始されるクラス階層内で、最も派生した this
宣言からのメンバー詳細を反映させて、名前参照の結果を変更します。 たとえば、セクション12.6.2.2「対応するパラメーター」では
クラスで定義されている仮想メソッドとインデクサーの場合、パラメーター リストは、静的に受け取る側から開始し、その基底クラスを検索するときに検出された関数メンバーの最初の宣言またはオーバーライドから選択されます。
これに次を追加します
インターフェイスで定義されている仮想メソッドとインデクサーについて、パラメーター リストは、関数メンバーの宣言またはオーバーライドを含む型のうち、最も派生した型で見つかった関数メンバーの宣言またはオーバーライドから選択されます。 そのような型が一意に存在しない場合は、コンパイル時エラーとなります。
プロパティまたはインデクサーアクセスの結果型については、既存のテキスト
I
がインスタンス プロパティを識別する場合、結果は、E
の関連付けられたインスタンス式と、プロパティの型である関連付けられた型を持つプロパティ アクセスになります。T
がクラス型の場合、関連付けられている型は、T
で始まり、その基底クラスを検索するときに見つかったプロパティの最初の宣言またはオーバーライドから選択されます。
は次で拡張されます
T
がインターフェイス型の場合、関連する型は、T
の最も派生したインターフェイス、またはその直接または間接的な基本インターフェイスにあるプロパティの宣言またはオーバーライドから選択されます。 そのような型が一意に存在しない場合は、コンパイル時エラーとなります。
同様の変更を「§12.8.12.3 インデクサー アクセス」にも適用する必要があります。
§12.8.10 呼び出し式では既存のテキストを拡張しています
- それ以外の場合、結果は値であり、その関連付けられている型は、メソッドまたはデリゲートの戻り値の型となります。 呼び出しがインスタンス メソッドの場合、受け取り側がクラス型
T
の場合、関連付けられている型は、T
で開始し、基底クラスを検索するときに見つかったメソッドの最初の宣言またはオーバーライドから選択されます。
代入
インスタンス メソッドの呼び出しで、受け取り側がインターフェイス型
T
の場合、関連付けられている型は、T
とその直接および間接の基底インターフェイスの中で、最も派生したインターフェイスで見つかったメソッドの宣言またはオーバーライドから選択されます。 そのような型が一意に存在しない場合は、コンパイル時エラーとなります。
暗黙のインターフェイスの実装
仕様の変更対象のセクション
インターフェイスのマッピングの目的上、クラスメンバー
A
がインターフェイス メンバーB
に一致するのは、次の条件を満たす場合です。
A
とB
がメソッドであり、A
とB
の名前、型、および仮引数リストが同一である場合。A
とB
がプロパティであり、A
とB
の名前および型が同一で、A
とB
のアクセサーが同一である場合 (A
がインターフェイス メンバーの明示的な実装でない場合は、追加のアクセサーが許可されます)。A
とB
がイベントであり、A
とB
の名前および型が同一である場合。A
とB
がインデクサーであり、A
とB
の型および仮引数リストが同一で、A
とB
のアクセサーが同一である場合 (A
がインターフェイス メンバーの明示的な実装でない場合は、追加のアクセサーが許可されます)。
は次のように変更されます:
インターフェイスのマッピングの目的上、クラスメンバー
A
がインターフェイス メンバーB
に一致するのは、次の条件を満たす場合です。
A
とB
がメソッドであり、A
とB
の名前および仮引数リストが同一で、A
の戻り値型が恒等変換または暗黙の参照変換によりB
の戻り値の型に変換できる場合。A
とB
がプロパティであり、A
とB
の名前が同一で、A
とB
のアクセサーが同一で (A
がインターフェイス メンバーの明示的な実装でない場合は、追加のアクセサーが許可されます)、A
の型が恒等変換により、またはA
が読み取り専用プロパティであれば暗黙の参照変換により、B
の戻り値の型に変換できる場合。A
とB
がイベントであり、A
とB
の名前および型が同一である場合。A
とB
がインデクサーであり、A
とB
の仮引数リストが同一で、A
とB
のアクセサーが同一で (A
がインターフェイス メンバーの明示的な実装でない場合、追加のアクセサーが許可されます)、A
の型が恒等変換により、またはB
が読み取り専用インデクサーであれば暗黙の参照変換により、A
の戻り値の型に変換できる場合。
これは技術的には互換性を破る変更です。以下のプログラムは現在 "C1.M" を出力しますが、提案された修正の下では "C2.M" を出力することになります。
using System;
interface I1 { object M(); }
class C1 : I1 { public object M() { return "C1.M"; } }
class C2 : C1, I1 { public new string M() { return "C2.M"; } }
class Program
{
static void Main()
{
I1 i = new C2();
Console.WriteLine(i.M());
}
}
この仕様変更により破壊的変更が生じるため、暗黙の実装での共変戻り値型のサポートを見送ることを検討する可能性があります。
インターフェイスの実装に関する制約
明示的なインターフェイス実装では、基底インターフェイスのオーバーライドで宣言された戻り値の型と同等の派生の戻り値の型を宣言する必要があるというルールが必要になります。
API の互換性への影響
TBD
オープン中の問題
仕様では、呼び出し元がより具体的な戻り値の型を取得する方法について言及していません。 おそらく、それは、呼び出し側が最も派生したオーバーライドのパラメーター仕様を取得するのと同様の方法で行われるでしょう。
次のインターフェイスがある場合:
interface I1 { I1 M(); }
interface I2 { I2 M(); }
interface I3: I1, I2 { override I3 M(); }
I3
では、メソッド I1.M()
と I2.M()
がマージされています。 I3
を実装する場合、両方のメソッドを一緒に実装する必要があります。
一般的に、元のメソッドを参照する明示的な実装が必要です。 クラスでの問題は次のとおりです
class C : I1, I2, I3
{
C IN.M();
}
ここでそれはどういう意味でしょうか? N とは何であるべきか?
I1.M
または I2.M
のいずれか (ただし両方ではない) の実装を許可し、それを両方の実装として扱うことを提案します。
デメリット
- [ ] どの言語においても変更にはそれに見合うだけの価値が必要です。
- [ ] 継承階層が深い場合でも、パフォーマンスが適切であることを確認する必要があります。
- [ ] 古いコンパイラーで生成された新しい IL を使用する場合でも、コンパイル時の変換処理に起因する副作用が言語のセマンティクスに影響しないようにする必要があります。
代替
ソースで許可するために、言語ルールを少し緩和します
// Possible alternative. This was not implemented.
abstract class Cloneable
{
public abstract Cloneable Clone();
}
class Digit : Cloneable
{
public override Cloneable Clone()
{
return this.Clone();
}
public new Digit Clone() // Error: 'Digit' already defines a member called 'Clone' with the same parameter types
{
return this;
}
}
未解決の質問
- [ ] この機能を使用してコンパイルされた API は、言語の古いバージョンでどのように動作するのでしょうか?
デザイン ミーティング
- https://github.com/dotnet/roslyn/issues/357にていくつかの議論。
- https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-01-08.md
- C# 9.0 においてクラス メソッドのオーバーライドのみをサポートするという決定に向けたオフライン ディスカッション。
C# feature specifications