オーバーロード解決の優先順位
メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/7706
まとめ
新しい属性 System.Runtime.CompilerServices.OverloadResolutionPriority
を導入します。この属性により、API 作成者は単一型内でオーバーロードの相対的な優先順位を調整できます。これは、通常であればあいまいと見なされたり、C# のオーバーロード解決規則では選択されないような API であったりしても、API 利用者を特定の API の使用へと導くための手段となります。
目的
API 開発者は、非推奨となったメンバーをどのように扱うべきかという問題によく直面します。 下位互換性のために、多くのユーザーは既存のメンバーを保持し、ObsoleteAttribute
を恒久的にエラーに設定します。これは、実行時にバイナリをアップグレードするコンシューマーが中断されるのを避けるためです。 この問題は特にプラグイン システムで顕著です。プラグイン作成者は、プラグインが実行される環境を制御できないためです。 環境の作成者は、古いメソッドを残しつつも、新しく開発されるコードからのアクセスをブロックしたいと考える場合があります。 しかし、ObsoleteAttribute
だけでは、この目的のために不十分です。 型またはメンバーはオーバーロード解決の中でまだ参照されており、他にもっと適切な選択肢がある場合でも、意図しないオーバーロード解決の失敗を引き起こす可能性があります。しかし、その選択肢が古いメンバーとあいまいになっているか、または古いメンバーが存在すると、適切なメンバーを考慮することなく、早期にオーバーロード解決が終わることがあります。 この目的のためには、API 開発者がオーバーロード解決のあいまいさを解消し、API の進化に対応しながら、ユーザーをパフォーマンスの良い API に誘導しつつ、ユーザー エクスペリエンスを損なわない方法を提供する必要があります。
Base Class Libraries (BCL) チームには、この属性が便利であることを証明するいくつかの事例があります。 いくつかの (仮説的な) 例を次に示します。
Debug.Assert
を使用するCallerArgumentExpression
のオーバーロードの作成。その目的は、アサートされている式を取得して、メッセージに含められるようにし、既存のオーバーロードよりも優先されるようにするためです。string.IndexOf(string, StringComparison = Ordinal)
をstring.IndexOf(string)
よりも優先させる。 これは潜在的な互換性の問題として議論が必要ですが、より適切な既定であり、ユーザーの意図に沿っている可能性が高いという考え方もあります。- この提案と
CallerAssemblyAttribute
を組み合わせることで、呼び出し元の情報を暗黙的に受け取るメソッドを使用して、コストのかかるスタック ウォークを回避できます。 たとえば、現在Assembly.Load(AssemblyName)
` はこの仕組みを使用していますが、さらに効率的になる可能性があります。 -
Microsoft.Extensions.Primitives.StringValues
は、string
とstring[]
の両方への暗黙的な変換を公開しています。 このため、params string[]
とparams ReadOnlySpan<string>
の両方がオーバーロードとして定義されているメソッドに渡された場合、どちらを使用するべきかあいまいになります。 この属性を使用して、一方のオーバーロードを優先させることであいまいさを回避できます。
詳細な設計
オーバーロード解決の優先順位
新しい概念である overload_resolution_priorityは、メソッド グループを解決する際に使用されます。 overload_resolution_priority は 32 ビットの整数値です。 すべてのメソッドは既定で overload_resolution_priority が 0 に設定されており、OverloadResolutionPriorityAttribute
を適用することで変更できます。 C# の仕様書のセクション §12.6.4.1 を次のように更新します (太字が変更点)。
候補関数メンバーと引数リストが特定されると、最適な関数メンバーの選択はすべてのケースで同じです。
- まず、候補関数メンバーのセットは、指定された引数リスト (§12.6.4.2) に適用できる関数メンバーに縮小されます。 この縮小セットが空の場合、コンパイル時エラーが発生します。
- 候補メンバーの縮小セットは、宣言元の型ごとにグループ化されます。 グループごとに:
- 候補となる関数メンバーは、overload_resolution_priorityに基づいて順序付けされます。 メンバーがオーバーライドである場合、overload_resolution_priority は、そのメンバーの、派生が最も少ない宣言から取得されます。
- 宣言する型のグループ内で見つかる、overload_resolution_priority が最も高いものよりも低いすべてのメンバーが削除されます。
- 縮小されたグループが再結合されて、適用可能な候補関数メンバーの最終セットが作成されます。
- 次に、適用可能な候補関数メンバーのセットから最適な関数メンバーが選択されます。 セットに含まれる関数メンバーが 1 つだけの場合、その関数メンバーが最適な関数メンバーになります。 それ以外の場合、各関数メンバーが §12.6.4.3の ルールを使用して他のすべての関数メンバーと比較される場合、特定の引数リストに関して他のすべての関数メンバーよりも優れている 1 つの関数メンバーが最適な関数メンバーになります。 他のすべての関数メンバーよりも優れた関数メンバーが 1 つだけ存在しない場合、関数メンバーの呼び出しはあいまいになり、バインド時エラーが発生します。
たとえば、この機能により、次のコード スニペットが「Array」ではなく「Span」を出力するようになります。
using System.Runtime.CompilerServices;
var d = new C1();
int[] arr = [1, 2, 3];
d.M(arr); // Prints "Span"
class C1
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority
public void M(int[] a) => Console.WriteLine("Array");
}
この変更の効果として、最も派生した型を選択するための絞り込みと同様に、オーバーロード解決の優先順位による最終的な絞り込みが追加されます。 この絞り込みはオーバーロード解決プロセスの最終段階で行われることから、基本型のメンバーを派生型のメンバーよりも高い優先順位にすることはできないことを意味します。 これは意図的な設計であり、基本型が常に派生型よりも優位に立とうとする際限のない競争を防ぐためのものです。 次に例を示します。
using System.Runtime.CompilerServices;
var d = new Derived();
d.M([1, 2, 3]); // Prints "Derived", because members from Base are not considered due to finding an applicable member in Derived
class Base
{
[OverloadResolutionPriority(1)]
public void M(ReadOnlySpan<int> s) => Console.WriteLine("Base");
}
class Derived : Base
{
public void M(int[] a) => Console.WriteLine("Derived");
}
負の値を使用することも可能で、これにより特定のオーバーロードを他のすべての既定オーバーロードよりも劣位に位置付けることができます。
メンバーの overload_resolution_priority は、そのメンバーの、最も派生が少ない宣言から取得されます。 overload_resolution_priority は、型メンバーが実装できるインターフェイス メンバーから継承されることも推論されることもなく、もしメンバー Mx
がインターフェイス メンバー Mi
を実装している場合でも、Mx
と Mi
が異なる overload_resolution_prioritiesを持つとき、警告は発行されません。
注: この規則の意図は、
params
修飾子の動作を模倣することにあります。
System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute
BCL に次の属性を導入します。
namespace System.Runtime.CompilerServices;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class OverloadResolutionPriorityAttribute(int priority) : Attribute
{
public int Priority => priority;
}
C# のすべてのメソッドは、OverloadResolutionPriorityAttribute
属性が指定されていない限り、既定で overload_resolution_priority が 0 に設定されます。 指定された属性が設定されている場合、彼らの overload_resolution_priority は属性の最初の引数として与えられた整数値になります。
OverloadResolutionPriorityAttribute
を次の場所に適用すると、エラーになります。
- 非インデクサー プロパティ
- プロパティ、インデクサー、またはイベント アクセサー
- 変換演算子
- ラムダ
- ローカル関数
- ファイナライザー
- 静的コンストラクター
これらの場所でメタデータに見つかった属性は C# によって無視されます。
基本メソッドをオーバーライドするなど、属性が無視される場所に OverloadResolutionPriorityAttribute
を適用するとエラーになります。これは、優先順位が最も派生していないメンバーの宣言から読み取られるためです。
注: これは意図的に、無視された際に再指定または追加が可能な
params
修飾子の動作とは異なります。
メンバーの呼び出し可否
OverloadResolutionPriorityAttribute
に関する重要な注意点として、特定のメンバーをソース コードから実質的に呼び出せなくなる可能性があります。 次に例を示します。
using System.Runtime.CompilerServices;
int i = 1;
var c = new C3();
c.M1(i); // Will call C3.M1(long), even though there's an identity conversion for M1(int)
c.M2(i); // Will call C3.M2(int, string), even though C3.M1(int) has less default parameters
class C3
{
public void M1(int i) {}
[OverloadResolutionPriority(1)]
public void M1(long l) {}
[Conditional("DEBUG")]
public void M2(int i) {}
[OverloadResolutionPriority(1), Conditional("DEBUG")]
public void M2(int i, [CallerArgumentExpression(nameof(i))] string s = "") {}
public void M3(string s) {}
[OverloadResolutionPriority(1)]
public void M3(object o) {}
}
これらの例では、既定の優先順位のオーバーロードは事実上使用できなくなり、いくつかの追加の手順を踏まないと呼び出せなくなります。
- メソッドをデリゲートに変換し、そのデリゲートを使用する。
M3(object)
がM3(string)
より優先される場合など、一部の参照型の共変シナリオでは、この戦略は失敗します。M2
のような条件付きメソッドも、この戦略では呼び出せません。条件付きメソッドはデリゲートに変換できないためです。
UnsafeAccessor
ランタイム機能を使用して、一致するシグネチャを介して呼び出す。- リフレクションを使用して手動でメソッドへの参照を取得し、それを呼び出す。
- 再コンパイルされていないコードは、引き続き古いメソッドを呼び出す。
- IL を手動で書く場合は、メソッドの呼び出し方を自由に指定できます。
未解決の質問
拡張メソッドのグループ化 (回答済み)
現在の仕様では、拡張メソッドは自身の型の中でのみ優先順位が決定されます。 次に例を示します。
new C2().M([1, 2, 3]); // Will print Ext2 ReadOnlySpan
static class Ext1
{
[OverloadResolutionPriority(1)]
public static void M(this C2 c, Span<int> s) => Console.WriteLine("Ext1 Span");
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext1 ReadOnlySpan");
}
static class Ext2
{
[OverloadResolutionPriority(0)]
public static void M(this C2 c, ReadOnlySpan<int> s) => Console.WriteLine("Ext2 ReadOnlySpan");
}
class C2 {}
拡張メソッドのオーバーロード解決時に、宣言型でソートせず、同じスコープ内のすべての拡張メソッドを考慮すべきでしょうか?
答え
常にグループ化します。 上記の例では、Ext2 ReadOnlySpan
が表示されます
オーバーライドにおける属性の継承 (回答)
属性は引き継がれるべきでしょうか? そうしない場合、オーバーライドするメンバーの優先順位はどうなりますか?
仮想メンバーに属性が指定されている場合、そのメンバーをオーバーライドする際に属性を再指定する必要がありますか?
答え
この属性は引き継がれません。 オーバーロード解決の優先順位を決定する際、最も派生していないメンバーの宣言を基準とします。
オーバーライド時のエラーまたは警告 (回答済み)
class Base
{
[OverloadResolutionPriority(1)] public virtual void M() {}
}
class Derived
{
[OverloadResolutionPriority(2)] public override void M() {} // Warn or error for the useless and ignored attribute?
}
無視されるようなコンテキスト、例えばオーバーライドの場合に、OverloadResolutionPriorityAttribute
を適用する際にはどうすればよいでしょうか。
- 何もしない、静かに無視されるままにする。
- 属性が無視されることを警告する。
- 属性の適用が許可されないことをエラーとして報告する。
将来、オーバーライドでこの属性の指定を許可する余地が必要になると考えられる場合、3 が最も慎重なアプローチです。
答え
3 のアプローチを採用し、この属性が適用されるべきでない場所では適用をブロックします。
暗黙的なインターフェイス実装 (回答済み)
暗黙的なインターフェイス実装の動作はどうなるべきでしょうか? OverloadResolutionPriority
を指定する必要がありますか? 暗黙的な実装で優先順位が指定されていない場合、コンパイラの動作はどうなるべきでしょうか? このケースは、インターフェイス ライブラリが更新されても実装が更新されない場合に、ほぼ確実に発生します。 従来の params
の動作を踏襲し、値を指定せず、引き継がないようにします。
using System;
var c = new C();
c.M(1, 2, 3); // error CS1501: No overload for method 'M' takes 3 arguments
((I)c).M(1, 2, 3);
interface I
{
void M(params int[] ints);
}
class C : I
{
public void M(int[] ints) { Console.WriteLine("params"); }
}
次のような選択肢があります。
params
に従う。OverloadResolutionPriorityAttribute
は暗黙的に引き継がれず、また指定も必須とはなりません。- 属性を暗黙的に引き継ぐ。
- 属性を暗黙的に引き継がず、呼び出し箇所で指定を必須とする。
- これによりさらに疑問が生じます。コンパイル済みの参照がある場合、このシナリオに対するコンパイラの動作はどうなるべきでしょうか?
答え
1 を採用します。
その他のアプリケーション エラー (回答済み)
や のように、確認する必要がある場所がいくつかあります。 これには次のようなものがあります。
- 変換演算子 - 仕様上、変換演算子がオーバーロード解決の対象になるとは明記されていないため、実装ではこれらのメンバーへの適用をブロックしています。 これは確認すべきでしょうか?
- ラムダ式 - 同様に、ラムダ式はオーバーロード解決の対象にならないため、実装ではこれらをブロックしています。 これは確認すべきでしょうか?
- デストラクター - ここでも、現在ブロックしています。
- 静的コンストラクター - これも現在ブロックされています。
- ローカル関数 - これらは現在ブロックされていません。これらは実際にオーバーロード解決を受けているため、ユーザーがオーバーロードすることはできません。 これは、オーバーロードされていない型のメンバーにこの属性を適用してもエラーにならない仕様と似ています。 この動作を確認する必要がありますか?
答え
上記のすべての場所でこの属性の適用はブロックされます。
Langversion の動作 (回答済み)
現在の実装では、OverloadResolutionPriorityAttribute
が適用される場合にのみ langversion エラーが発生し、実際に何らかの影響を与える場合には発生しません。 この決定が行われた理由は次のとおりです。BCL は (現在および今後にわたって) この属性を使用する API を追加していく予定です。ユーザーが手動で言語バージョンを C# 12 以前に戻した場合、これらのメンバーが表示され、言語バージョンの動作によって次のいずれかの状況が発生します。
- C# 13 未満でこの属性を無視した場合、属性がないと API が真にあいまいとなり、あいまいさエラーが発生します。
- 属性が結果に影響を与えることでエラーが発生した場合、API が使用できないというエラーに直面します。 これは特に問題となります。なぜなら .NET 9 では
Debug.Assert(bool)
の優先順位が下げられているためです。 - 解像度を黙って変更すると、あるコンパイラ・バージョンはこの属性を理解しているが、別のコンパイラ・バージョンは理解していない場合、異なるコンパイラ・バージョン間で動作が異なる可能性があります。
最後の動作が選択されました。これは、最も前方互換性があるためですが、変更の結果は一部のユーザーにとって予想外となる可能性があります。 この仕様を確認すべきでしょうか、それとも他の選択肢を選ぶべきでしょうか?
答え
選択肢 1 を選び、以前の言語バージョンでは属性を暗黙的に無視します。
代替
以前の提案は、対象を見えなくするという強引な BinaryCompatOnlyAttribute
のアプローチを指定しようとするものでした。 ただし、多くの困難な実装上の問題があります。これは、提案が強力すぎて実用的でなくなり(例えば、古い API のテストができなくなる)、または元の目標の一部を達成できないほど弱くなる(例えば、新しい API を呼び出す際に、あいまいとされる API を持てなくなる)ことを意味します。 そのバージョンを以下に示します。
BinaryCompatOnlyAttribute を提案 (旧提案)
BinaryCompatOnlyAttribute
詳細な設計
System.BinaryCompatOnlyAttribute
新しい予約属性を導入します。
namespace System;
// Excludes Assembly, GenericParameter, Module, Parameter, ReturnValue
[AttributeUsage(AttributeTargets.Class
| AttributeTargets.Constructor
| AttributeTargets.Delegate
| AttributeTargets.Enum
| AttributeTargets.Event
| AttributeTargets.Field
| AttributeTargets.Interface
| AttributeTargets.Method
| AttributeTargets.Property
| AttributeTargets.Struct,
AllowMultiple = false,
Inherited = false)]
public class BinaryCompatOnlyAttribute : Attribute {}
この属性を型メンバーに適用すると、そのメンバーはコンパイラによってあらゆる場所でアクセス不可として扱われ、メンバー検索、オーバーロード解決、その他の類似の処理に関与しなくなります。
アクセシビリティ ドメイン
以下のように、§7.5.3 アクセシビリティ ドメインを更新します。
メンバーのアクセシビリティ ドメインは、メンバーへのアクセスが許可されるプログラム テキストの範囲 (場合によっては重複する) を指します。 メンバーのアクセシビリティ ドメインを定義するために、型の中で宣言されていないメンバーはトップレベル、型の中で宣言されているメンバーはネストされていると見なされます。 さらに、プログラムのプログラム テキストとは、プログラムのすべてのコンパイル単位に含まれるすべてのテキストとして定義され、型のプログラム テキストとは、その型 (場合によってはその型の中にネストされた型も含む) の型宣言 (type_declarations) に含まれるすべてのテキストとして定義されます。
組み込み型 (
object
、int
、double
など) のアクセシビリティ ドメインは無制限です。プログラム
P
内で宣言されたトップレベルのバインドされていない型T
(§8.4.4) のアクセシビリティ ドメインは次のように定義されます。
T
にBinaryCompatOnlyAttribute
が付与されている場合、T
のアクセシビリティ ドメインはP
のプログラム テキストおよびP
を参照するあらゆるプログラムに対して完全にアクセス不可になります。T
の宣言されたアクセシビリティが public の場合、T
のアクセシビリティ ドメインはP
のプログラム テキストおよびP
を参照するすべてのプログラムになります。T
に対して宣言されているアクセシビリティが internal の場合、T
のアクセシビリティ ドメインはP
のプログラム テキストになります。注: これらの定義から、トップレベルの非バインド型のアクセシビリティ ドメインは、常にその型が宣言されているプログラムのプログラム テキスト以上となることがわかります。 注釈
構築型
T<A₁, ..., Aₑ>
のアクセシビリティ ドメインは、非バインド ジェネリック型T
のアクセシビリティ ドメインと、型引数A₁, ..., Aₑ
のアクセシビリティ ドメインとの積集合になります。プログラム
P
内の型T
で宣言されたネスト メンバーM
のアクセシビリティ ドメインは、次のように定義されます (M
自体も型である可能性があります)。
M
にBinaryCompatOnlyAttribute
が付与されている場合、M
のアクセシビリティ ドメインはP
のプログラム テキストおよびP
を参照するあらゆるプログラムに対して完全にアクセス不可になります。M
に対して宣言されたアクセシビリティがpublic
の場合、M
のアクセシビリティ ドメインはT
のアクセシビリティ ドメインになります。M
の宣言されたアクセシビリティがprotected internal
の場合、D
をP
のプログラム テキストと、P
外でT
を引き継ぐすべての型のプログラム テキストとの和集合とします。M
のアクセシビリティ ドメインはT
のアクセシビリティ ドメインとD
の積集合になります。M
の宣言されたアクセシビリティがprivate protected
なら、D
をP
のプログラム テキストとT
のプログラム テキスト、およびT
から派生した任意の型の積集合とします。M
のアクセシビリティ ドメインはT
のアクセシビリティ ドメインとD
の積集合になります。M
の宣言されたアクセシビリティがprotected
の場合、D
をT
のプログラム テキストと、T
から派生したすべての型のプログラム テキストとの和集合とします。M
のアクセシビリティ ドメインはT
のアクセシビリティ ドメインとD
の積集合になります。M
に対して宣言されているアクセシビリティがinternal
の場合、M
のアクセシビリティ ドメインは、T
のアクセシビリティ ドメインとP
のプログラム テキストとの積集合になります。M
に対して宣言されているアクセシビリティがprivate
の場合、M
のアクセシビリティ ドメインはT
のプログラム テキストになります。
これらの追加の目的は、BinaryCompatOnlyAttribute
を付与されたメンバーが、すべての場所に対して完全にアクセス不可になるように設定することです。それらのメンバーは、メンバー検索に参加できず、プログラムの残りの部分に影響を与えることもできません。 結果として、このようなメンバーはインターフェイス メンバーとしての実装ができず、互いに呼び出すこともできず、さらに仮想メソッドとしてのオーバーライドや、メンバーの隠蔽、インターフェイス メンバーとしての実装もできません。 この制約が厳しすぎるかどうかは、以下のいくつかの未解決の質問の対象となっています。
未解決の質問
仮想メソッドとオーバーライド
仮想メソッドが BinaryCompatOnly
とマークされている場合、どうすればよいのでしょうか? 派生クラスでのオーバーライドは、現在のアセンブリに含まれていない場合さえあります。また、たとえば戻り値の型のみが異なるメソッドの新しいバージョンをユーザーが導入しようとしている場合もあります。これは、C# では通常オーバーロードとして許可されません。 再コンパイル時に、そのメソッドの以前のオーバーライドはどうなるのでしょうか? BinaryCompatOnly
としてもマークされている場合、BinaryCompatOnly
メンバーをオーバーライドすることは許可されていますか?
同じ DLL 内で使用する
この提案では、BinaryCompatOnly
メンバーは、現在コンパイル中のアセンブリ内でも、どこからも不可視であるとしています。 それは厳密すぎますか、または BinaryCompatAttribute
のメンバーが相互に連結する必要がありますか?
インターフェイス メンバーを暗黙的に実装する
BinaryCompatOnly
メンバーはインターフェイス メンバーを実装できるようにすべきでしょうか? それをすることを防ぐべきでしょうか。 これにより、ユーザーが暗黙のインターフェイス実装を BinaryCompatOnly
に変換しようとする際には、明示的なインターフェイス実装も提供する必要があります。この際、BinaryCompatOnly
メンバーと同じ内容を明示的なインターフェイス実装として複製することが求められます。これは、明示的なインターフェイス実装では元のメンバーを見ることができなくなるためです。
BinaryCompatOnly
がマークされたインターフェースメンバーの実装
インターフェイス メンバーが BinaryCompatOnly
とマークされている場合、どうすればよいのでしょうか? 型は依然としてそのメンバーの実装を提供する必要があります。結局のところ、インターフェイス メンバーは BinaryCompatOnly
とマークできないと単純に規定するしかないかもしれません。
C# feature specifications