次の方法で共有


インターフェイスの静的抽象メンバー

メモ

この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオンの課題: https://github.com/dotnet/csharplang/issues/4436

まとめ

インターフェイスは、実装するクラスや構造体が明示的または暗黙的に実装しなければならない抽象的な静的メンバーを指定することができます。 メンバーは、インターフェイスによって制約されている型パラメーターからアクセスできます。

目的

現在のところ、静的メンバを抽象化して、その静的メンバを定義する型にまたがって適用される一般化されたコードを記述する方法はありません。 これは、静的フォームにのみ存在するメンバーの種類 (特に演算子) で特に問題があります。

この機能により、特定の演算子の存在を指定するインターフェイス制約によって表される、数値型に対する汎用アルゴリズムが可能になります。 したがって、アルゴリズムはそのような演算子を用いて表現することができます。

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 IAddable.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

構文

インターフェイス メンバー

この機能により、静的なインターフェイス メンバーを仮想メンバーとして宣言できるようになります。

C# 11 より前のルール

C# 11 より前のバージョンでは、インターフェイス内のインスタンス メンバーは暗黙的に抽象 (または既定の実装がある場合は仮想) ですが、任意で abstract (または virtual) 修飾子を持つことができます。 仮想でないインスタンス メンバーは、明示的に sealed としてマークする必要があります。

現在、静的インターフェイス メンバーは暗黙的に非仮想であり、abstractvirtual、または sealed 修飾子を許可しません。

提案

抽象静的メンバー

フィールド以外の静的インターフェイス メンバーは、abstract 修飾子を持つこともできます。 抽象静的メンバーは、本体を持つことが許可されていません (またプロパティの場合、アクセサーは本体を持つことが許可されていません)。

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
    static abstract bool operator ==(T l, T r);
    static abstract bool operator !=(T l, T r);
    static abstract implicit operator T(string s);
    static abstract explicit operator string(T t);
}
仮想静的メンバー

フィールド以外の静的インターフェイス メンバーは、virtual 修飾子を持つこともできます。 仮想静的メンバーには本文が必要です。

interface I<T> where T : I<T>
{
    static virtual void M() {}
    static virtual T P { get; set; }
    static virtual event Action E;
    static virtual T operator +(T l, T r) { throw new NotImplementedException(); }
}
明示的に非仮想の静的メンバー

非仮想インスタンス メンバーとの対称性を考慮し、静的メンバー(フィールドを除く)は、既定では非仮想であるにもかかわらず、省略可能な sealed 修飾子を許可すべきです。

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

インターフェイス メンバーの実装

現在のルール

クラスや構造体は、インターフェイスの抽象インスタンス メンバーを暗黙的または明示的に実装できます。 暗黙的に実装されたインターフェイス メンバーは、クラスや構造体の通常の (仮想または非仮想の) メンバー宣言であり、結果としてインターフェイス メンバーも実装しているだけです。 メンバーは基底クラスから継承できるため、クラス宣言内に存在しない場合もあります。

明示的に実装されたインターフェイス メンバーは、修飾名を使用して、対象のインターフェイス メンバーを識別します。 実装は、クラスや構造体のメンバーとして直接アクセスすることはできず、インターフェイスを通じてのみアクセスできます。

提案

静的抽象インターフェイス メンバーを暗黙的に実装するために、クラスや構造体に新しい構文は必要ありません。 既存の静的メンバー宣言がその目的を果たします。

静的抽象インターフェイス メンバーの明示的な実装では、修飾名と static 修飾子を使用します。

class C : I<C>
{
    string _s;
    public C(string s) => _s = s;
    static void I<C>.M() => Console.WriteLine("Implementation");
    static C I<C>.P { get; set; }
    static event Action I<C>.E // event declaration must use field accessor syntax
    {
        add { ... }
        remove { ... }
    }
    static C I<C>.operator +(C l, C r) => new C($"{l._s} {r._s}");
    static bool I<C>.operator ==(C l, C r) => l._s == r._s;
    static bool I<C>.operator !=(C l, C r) => l._s != r._s;
    static implicit I<C>.operator C(string s) => new C(s);
    static explicit I<C>.operator string(C c) => c._s;
}

Semantics

演算子の制限

現在、すべての単項および二項演算子の宣言には、オペランドの少なくとも 1 つが T 型または T?である必要があります。ここで、T は囲む型のインスタンス型です。

これらの要件を緩和し、制限されたオペランドが「外側の型のインスタンス型」としてカウントされる型パラメーターであることが許可される必要があります。

型パラメーター Tが「外側の型のインスタンスタイプ」としてカウントされるためには、次の要件を満たす必要があります。

  • T は、演算子宣言が行われるインターフェイスの直接の型パラメーターです。
  • T、仕様書が「インスタンス型」と呼ぶもの、つまり型引数として使用される独自の型パラメーターを持つ周囲のインターフェースによって直接制約されています。

等価演算子と型変換

== 演算子と != 演算子の抽象/仮想宣言、および暗黙的および明示的な変換演算子の抽象/仮想宣言がインターフェイスで許可されます。 派生インターフェイスもそれらを実装できます。

== 演算子と != 演算子の場合、少なくとも 1 つのパラメーター型は、前のセクションで定義された「囲む型のインスタンス型」として扱われる型パラメーターである必要があります。

静的抽象メンバーの実装

クラスや構造体内の静的メンバー宣言が静的抽象インターフェイス メンバーを実装していると見なされる条件、およびその際に適用される要件は、インスタンス メンバーの場合と同じです。

TBD: ここには、まだ考慮されていない追加の規則や異なる規則が必要になる可能性があります。

型引数としてのインターフェイス

https://github.com/dotnet/csharplang/issues/5955 で提起された問題について議論し、インターフェイスを型引数として使用することに関する制限を追加することを決定しました (詳細は https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-28.md#type-hole-in-static-abstracts を参照)。 https://github.com/dotnet/csharplang/issues/5955 で提案され、LDM によって承認された制限事項は次のとおりです。

静的抽象/仮想メンバーを含むまたは継承するインターフェイスであって、最も具体的な実装を持たないものは、型引数として使用することができません。 すべての静的抽象/仮想メンバーに最も具体的な実装がある場合、そのインターフェイスは型引数として使用することができます。

静的抽象インターフェイス メンバーへのアクセス

静的抽象インターフェイス メンバー M は、型パラメーター T に対して、式 T.M を使用してアクセスできます。このとき、T はインターフェイス I によって制約され、MI のアクセス可能な静的抽象メンバーである必要があります。

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t + T.P;
}

実行時に使用される実際のメンバー実装は、型引数として提供された実際の型に存在するメンバー実装です。

C c = M<C>(); // The static members of C get called

クエリ式は構文的な書き換えとして仕様化されているため、使用するクエリ演算子に対応する静的メンバーを持っている限り、クエリ ソースとして任意のを使用できます。 言い換えると、構文さえ適合していれば、使用できます。 この動作は元々の LINQ で意図されたものでも重要なものでもなかったと考えており、型パラメーターでこれをサポートするための対応を行う予定はありません。 もしそのようなシナリオが存在する場合は、当社の耳に入ることでしょう。その時点で採用するかどうかを選択します。

差異安全性 §18.2.3.2

変性の安全性ルールは、静的抽象メンバーのシグネチャに適用する必要があります。 https://github.com/dotnet/csharplang/blob/main/proposals/variance-safety-for-static-interface-members.md#variance-safety で提案される追加は次から調整する必要があります。

これらの制限は、静的メンバーの宣言内での型の使用には適用されません。

送信先

これらの制限は、非仮想、非抽象静的メンバーの宣言内での型の使用には適用されません。

§10.5.4 ユーザー定義の暗黙的変換

次の箇条書き

  • SS₀、および T₀を特定します。
    • E に型がある場合、その型は S です。
    • S または T が null 許容値型である場合、それらの基になる型を Sᵢ および Tᵢ とし、そうでない場合は、Sᵢ および Tᵢ をそれぞれ S および T とします。
    • Sᵢ または Tᵢ が型パラメーターである場合、それらの有効な基底クラスを S₀ および T₀ とし、そうでない場合は、S₀ および T₀ をそれぞれ Sₓ および Tᵢ とします。
  • ユーザー定義の変換演算子が考慮される型のセット (D) を検索します。 このセットは、S0 (S0 がクラスまたは構造体の場合)、S0 の基底クラス (S0 がクラスの場合)、および T0 (T0 がクラスまたは構造体の場合) で構成されます。
  • 適用可能なユーザー定義およびリフトされた変換演算子のセット U を検索します。 このセットは、D内のクラスまたは構造体によって宣言された、型 S を包含する型から T を包含する型への変換を行うユーザー定義およびリフトされた暗黙的変換演算子で構成されます。 If U が空である場合、その変換は未定義であり、コンパイル時エラーが発生します。

は次のように調整されます。

  • SS₀、および T₀を特定します。
    • E に型がある場合、その型は S です。
    • S または T が null 許容値型である場合、それらの基になる型を Sᵢ および Tᵢ とし、そうでない場合は、Sᵢ および Tᵢ をそれぞれ S および T とします。
    • Sᵢ または Tᵢ が型パラメーターである場合、それらの有効な基底クラスを S₀ および T₀ とし、そうでない場合は、S₀ および T₀ をそれぞれ Sₓ および Tᵢ とします。
  • 適用可能なユーザー定義およびリフトされた変換演算子のセット U を検索します。
    • ユーザー定義の変換演算子が考慮される型のセット (D1) を検索します。 このセットは、S0 (S0 がクラスまたは構造体の場合)、S0 の基底クラス (S0 がクラスの場合)、および T0 (T0 がクラスまたは構造体の場合) で構成されます。
    • 適用可能なユーザー定義およびリフトされた変換演算子のセット U1 を検索します。 このセットは、D1内のクラスまたは構造体によって宣言された、型 S を包含する型から T を包含する型への変換を行うユーザー定義およびリフトされた暗黙的変換演算子で構成されます。
    • U1 が空でない場合、UU1 です。 それ以外の場合:
      • ユーザー定義の変換演算子が考慮される型のセット (D2) を検索します。 このセットは、Sᵢ の有効なインターフェイス セット とその基底インターフェイス (Sᵢ が型パラメーターである場合)、および Tᵢ の有効なインターフェイス セット (Tᵢ が型パラメーターである場合) で構成されます。
      • 適用可能なユーザー定義およびリフトされた変換演算子のセット U2 を検索します。 このセットは、D2 内のインターフェイスによって宣言された、型 S を包含する型から T を包含する型への変換を行うユーザー定義およびリフトされた暗黙的変換演算子で構成されます。
      • U2 が空でない場合、UU2 です。
  • If U が空である場合、その変換は未定義であり、コンパイル時エラーが発生します。

§10.3.9 ユーザー定義の明示的変換

次の箇条書き

  • SS₀、および T₀を特定します。
    • E に型がある場合、その型は S です。
    • S または T が null 許容値型である場合、それらの基になる型を Sᵢ および Tᵢ とし、そうでない場合は、Sᵢ および Tᵢ をそれぞれ S および T とします。
    • Sᵢ または Tᵢ が型パラメーターである場合、それらの有効な基底クラスを S₀ および T₀ とし、そうでない場合は、S₀ および T₀ をそれぞれ Sᵢ および Tᵢ とします。
  • ユーザー定義の変換演算子が考慮される型のセット (D) を検索します。 このセットは、S0 (S0 がクラスまたは構造体の場合)、S0 の基底クラス (S0 がクラスの場合)、T0 (T0 がクラスまたは構造体の場合)、および T0 の基底クラス (T0 がクラスの場合) で構成されます。
  • 適用可能なユーザー定義およびリフトされた変換演算子のセット U を検索します。 このセットは、D 内のクラスまたは構造体によって宣言されたユーザー定義およびリフトされた暗黙的または明示的な変換演算子で構成され、S を包含する、またはそれに包含される型から、T を包含する、またはそれに包含される型に変換します。 If U が空である場合、その変換は未定義であり、コンパイル時エラーが発生します。

は次のように調整されます。

  • SS₀、および T₀を特定します。
    • E に型がある場合、その型は S です。
    • S または T が null 許容値型である場合、それらの基になる型を Sᵢ および Tᵢ とし、そうでない場合は、Sᵢ および Tᵢ をそれぞれ S および T とします。
    • Sᵢ または Tᵢ が型パラメーターである場合、それらの有効な基底クラスを S₀ および T₀ とし、そうでない場合は、S₀ および T₀ をそれぞれ Sᵢ および Tᵢ とします。
  • 適用可能なユーザー定義およびリフトされた変換演算子のセット U を検索します。
    • ユーザー定義の変換演算子が考慮される型のセット (D1) を検索します。 このセットは、S0 (S0 がクラスまたは構造体の場合)、S0 の基底クラス (S0 がクラスの場合)、T0 (T0 がクラスまたは構造体の場合)、および T0 の基底クラス (T0 がクラスの場合) で構成されます。
    • 適用可能なユーザー定義およびリフトされた変換演算子のセット U1 を検索します。 このセットは、D1 内のクラスまたは構造体によって宣言されたユーザー定義およびリフトされた暗黙的または明示的な変換演算子で構成され、S を包含する、またはそれに包含される型から、T を包含する、またはそれに包含される型に変換します。
    • U1 が空でない場合、UU1 です。 それ以外の場合:
      • ユーザー定義の変換演算子が考慮される型のセット (D2) を検索します。 このセットは、Sᵢ の有効なインターフェイス セット とその基底インターフェイス (Sᵢ が型パラメーターである場合)、および Tᵢ の有効なインターフェイス セット とその基底インターフェイス (Tᵢ が型パラメーターである場合) で構成されます。
      • 適用可能なユーザー定義およびリフトされた変換演算子のセット U2 を検索します。 このセットは、D2 内のインターフェイスによって宣言されたユーザー定義およびリフトされた暗黙的または明示的な変換演算子で構成され、S を包含する、またはそれに包含される型から、T を包含する、またはそれに包含される型に変換します。
      • U2 が空でない場合、UU2 です。
  • If U が空である場合、その変換は未定義であり、コンパイル時エラーが発生します。

既定の実装

この提案に対する追加として、インターフェイス内の静的仮想メンバーが、インスタンスの仮想/抽象メンバーと同様に既定の実装を持つことを許可します。

ここでの複雑さの 1 つは、既定の実装が他の静的仮想メンバーを "仮想的に" 呼び出す場合です。 静的仮想メンバーをインターフェイス上で直接呼び出すことを許可するには、現在の静的メソッドが実際に呼び出された "自己" 型を表す隠れた型パラメーターを渡す必要があります。 これは複雑で、コストがかかり、さらに混乱を招く可能性があります。

静的仮想メンバーを型パラメーターでのみ呼び出せるという現在の提案の制限を維持する、より単純なバージョンについて説明しました。 静的仮想メンバーを持つインターフェイスは、多くの場合、"自己" 型を表す明示的な型パラメーターを持つため、これは大きな損失にはなりません。他の静的仮想メンバーはその自己型で単に呼び出すことができます。 このバージョンははるかにシンプルで、実現可能と思われます。

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-01-24.md#default-implementations-of-abstract-statics において、https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/default-interface-methods.md で確立されたルールに従って、静的メンバーの既定の実装をサポートすることを決定しました。また、それらのルールを拡張することも視野に入れています。

パターン マッチング

次のコードを考えたとき、ユーザーが (定数パターンがインラインで記述されている場合のように) "True" が出力されることを期待するのは自然です。

M(1.0);

static void M<T>(T t) where T : INumberBase<T>
{
    Console.WriteLine(t is 1); // Error. Cannot use a numeric constant
    Console.WriteLine((t is int i) && (i is 1)); 
}

しかし、パターンの入力型が doubleはないため、定数 1 パターンは最初に受け取った Tint と型チェックします。 これは直感的ではないため、将来の C# バージョンで INumberBase<T> から派生した型に対する数値のマッチング処理が改善されるまでブロックされます。 これを行うために、"数値" はすべて INumberBase<T> から派生する型と明確に認識し、パターンが表現できない数値型 (たとえば、INumberBase<T> に制約された型パラメーターや INumberBase<T> から継承されたユーザー定義の数値型) に対する数値定数パターンのマッチングをブロックすることにします。

正式には、定数パターンの パターン互換性の定義に例外を追加します。

定数パターンは、定数値に対して式の値をテストします。 定数には、リテラル、宣言された const 変数の名前、列挙定数など、任意の定数式を指定できます。 入力値がオープン型でない場合、定数式は一致した式の型に暗黙的に変換されます。入力値の型が定数式の型とパターン互換がない場合、パターン マッチング操作はエラーになります。 定数式が数値で、入力値が System.Numerics.INumberBase<T> から継承された型であり、定数式から入力値の型への定数変換が存在しない場合、パターンマッチング操作はエラーになります。

また、関係パターンにも同様の例外を追加します。

入力が、入力を左オペランドとして、指定された定数を右オペランドとして適用できる、適切な組み込みバイナリ関係演算子が定義されている型である場合、その演算子の評価は関係パターンの意味として解釈されます。 それ以外の場合は、明示的な null 許容変換またはボックス化解除変換を使用して、入力を式の型に変換します。 そのような変換が存在しない場合、コンパイル時エラーとなります。 入力型が System.Numerics.INumberBase<T> に制約された型パラメーター、またはそれから継承された型であり、適切な組み込みの二項関係演算子が定義されていない場合、コンパイル時エラーとなります。 変換が失敗した場合、一致はマッチしないと見なされます。 変換が成功した場合、パターンマッチング操作の結果は、式 ”e OP v” の評価結果となります。ここで、e は変換された入力、OP は関係演算子、v は定数式です。

デメリット

  • "静的抽象" は新しい概念であり、C# の概念的負担を意味のある形で増加させます。
  • これは構築が容易な機能ではありません。 その価値があるかどうかを慎重に検討する必要があります。

代替

構造的制約

別の方法としては、"構造的制約" を直接持ち、型パラメーターに特定の演算子が存在することを明示的に要求する方法があります。 この方法のデメリットは、毎回記述する必要があることです。 名前付き制約を持つ方が良さそうです。 - これは全く新しい種類の制約ですが、提案されている機能は既存のインターフェイス制約の概念を利用しています。 - 演算子に対してのみ簡単に機能し、他の種類の静的メンバーには簡単には適用できません。

未解決の質問

静的抽象インターフェイスと静的クラス

詳細については、「https://github.com/dotnet/csharplang/issues/5783」および「https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes」を参照してください。

デザイン会議