次の方法で共有


ref readonly パラメーター

手記

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

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

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

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

概要

パラメーター宣言サイト修飾子の ref readonly を許可し、次のように callite ルールを変更します。

Callsite 注釈 ref パラメーター ref readonly パラメーター in パラメーター out パラメーター
ref 許可されます。 許可 警告 エラー
in エラー 許可 許可されます。 エラー
out エラー エラー エラー 許可されます。
注釈なし エラー 警告 許可されます。 エラー

(既存のルールに 1 つの変更があることに注意してください。ref callsite 注釈を伴う in パラメーターは、エラーの代わりに警告を生成します)。

引数の値の規則を次のように変更します。

値の種類 ref パラメーター ref readonly パラメーター in パラメーター out パラメーター
rvalue エラー 警告 許可された エラー
lvalue 許可されます。 許可 許可されます。 許可されます。

左辺値は変数を意味し(つまり、場所を持つ値は書き込み可能/代入可能である必要はありません)、右辺値は任意の種類の値を意味します。

モチベーション

C# 7.2 では、読み取り専用参照を渡す方法として in パラメーターが導入されました。 in パラメーターは左辺値と右辺値の両方を許可し、呼び出し元で注釈を付けずに使用できます。 ただし、パラメーターから参照をキャプチャまたは返す API では、右辺値を禁止し、参照がキャプチャされていることを呼び出し箇所で示すことを強制したいと考えています。 ref readonly パラメーターは、右辺値と共に使用されたり、呼び出し側で注釈がなく使用されたりする場合に警告を発するため、そのようなケースで理想的です。

さらに、読み取り専用の参照のみが必要な API もありますが、以下が使用されます。

  • ref パラメーターは、in が使用可能になる前に導入されたので、in への変更はソースとバイナリの破壊的変更になります (例: QueryInterface、または
  • 右辺値を渡すことが実際には意味を成さない場合でも、読み取り専用の参照を受け入れるための in パラメーター (例: ReadOnlySpan<T>..ctor(in T value))
  • 渡された参照を変更しない場合でも右辺値を許可しないようにする ref パラメーター (例: Unsafe.IsNullRef)。

これらの API は、ユーザーを中断することなく、ref readonly パラメーターに移行できます。 バイナリの互換性の詳細については、提案された メタデータエンコードを参照してください。 具体的には、以下を変更します。

  • refref readonly は、仮想メソッドのバイナリ破壊的変更のみになります。
  • refin は、仮想メソッドのバイナリ破壊的変更でもありますが、ソースの破壊的変更ではありません (in パラメーターに渡される ref 引数に対してのみ警告するようにルールが変更されるため)。
  • inref readonly は破壊的変更ではありません (ただし、呼び出し位置の注釈や右辺値を指定しないと警告が発生します)。
    • これは、古いコンパイラ バージョンを使用するユーザーのソース破壊的変更であることに注意してください (ref readonly パラメーターを ref パラメーターとして解釈するため、呼び出し先で in を禁止するか、注釈を付けないようにします)。また、LangVersion <= 11 を持つ新しいコンパイラ バージョン (以前のコンパイラ バージョンとの整合性のため、対応する引数が ref 修飾子で渡されない限り、ref readonly パラメーターがサポートされないことを示すエラーが生成されます)。

反対方向では、以下を変更します。

  • ref readonlyref は、ソース破壊的変更になる可能性があります (ref 呼び出し先の注釈のみが使用され、引数として読み取り専用参照のみが使用されている場合を除く)、および仮想メソッドのバイナリ破壊的変更。
  • ref readonlyin は破壊的変更ではありません (ただし、コールサイト注釈 ref を付けると警告が発生します)。

上記で説明した規則はメソッドシグネチャには適用されますが、シグネチャの委任には適用されないことに注意してください。 たとえば、ref をデリゲートシグネチャで in に変更すると、ソースの破壊的変更になる可能性があります (ユーザーがそのデリゲート型に ref パラメーターを持つメソッドを割り当てる場合、API の変更後にエラーになります)。

詳細な設計

一般に、ref readonly パラメーターのルールは、提案in パラメーターに対して指定と同じですが、この提案で明示的に変更されている場合は除きます。

パラメーター宣言

文法を変更する必要はありません。 パラメーターには、修飾子 ref readonly が許可されます。 通常のメソッドとは別に、ref readonly はインデクサー パラメーターに対して許可されますが (inrefとは異なり)、演算子パラメーターでは許可されません (refinとは異なります)。

既定のパラメーター値は、右辺値を渡すことと同じであるため、警告を含む ref readonly パラメーターに対して許可されます。 これにより、API 作成者は、ソースの破壊的変更を導入することなく、既定値 in パラメーターを ref readonly パラメーターに変更できます。

値の種類のチェック

引数修飾子 refref readonly パラメーターに対して許可されている場合でも、値の種類チェックに関しては何も変わりません、すなわち、

  • ref は割り当て可能な値でのみ使用できます。
  • 読み取り専用参照を渡すには、代わりに in 引数修飾子を使用する必要があります。
  • 右辺値を渡すには、修飾子なしで使用する必要があります (この提案の概要で説明されているように、ref readonly パラメーターに対する警告が発生します)。

オーバーロードの解決

オーバーロード解決では、この提案の概要の表に示されているように、ref/ref readonly/in/呼び出し位置なしとパラメータ修飾子の混合が許可されます。つまり、すべての許可および警告ケースは、オーバーロードの解決中に候補として考慮されます。 具体的には、in パラメーターを持つメソッドが、refとしてマークされた対応する引数の呼び出しと一致する既存の動作が変更されています。この変更は LangVersion でゲートされます。

ただし、呼び出し位置修飾子を持たない引数を ref readonly パラメーターに渡す場合の警告は、パラメーターが次に当てはまる場合は表示されません。

  • 拡張メソッドの呼び出しにおけるレシーバー
  • カスタム コレクション初期化子または挿入文字列ハンドラーの一部として暗黙的に使用されます。

引数修飾子がない場合は、値ごとのオーバーロード ref readonly オーバーロードよりも優先されます (in パラメーターの動作は同じです)。

メソッドの変換

同様に、匿名関数 [§10.7] とメソッド グループ [§10.8] 変換の目的で、これらの修飾子は互換性があるとみなされます (ただし、異なる修飾子間で許可される変換の結果は警告になります)。

  • ターゲット メソッドの ref readonly パラメーターは、デリゲートの in または ref パラメーターと一致することが許可されます。
  • ターゲット メソッドの in パラメーターは、ref readonly または LangVersion に依存してデリゲートの ref パラメーターと一致させることができます。
  • 注: ターゲット メソッドの refパラメーターをデリゲートの in または ref readonly パラメーターと照合することは許可されていません

例えば:

DIn dIn = (ref int p) => { }; // error: cannot match `ref` to `in`
DRef dRef = (in int p) => { }; // warning: mismatch between `in` and `ref`
DRR dRR = (ref int p) => { }; // error: cannot match `ref` to `ref readonly`
dRR = (in int p) => { }; // warning: mismatch between `in` and `ref readonly`
dIn = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `in`
dRef = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `ref`
delegate void DIn(in int p);
delegate void DRef(ref int p);
delegate void DRR(ref readonly int p);

関数ポインター変換の動作には変更がないことに注意してください。 参照型修飾子の間に不一致がある場合、暗黙的な関数ポインター変換は許可されず、明示的なキャストは常に警告なしで許可されます。

署名の一致

1 つの型で宣言されたメンバーは、ref/out/in/ref readonlyによってのみシグネチャを異なすことはできません。 署名照合の他の目的 (非表示またはオーバーライドなど) の場合、ref readonlyin 修飾子と交換できますが、その結果、宣言サイト [§7.6] で警告が表示されます。 これは、partial 宣言を実装と照合する場合や、インターセプターシグネチャとインターセプト署名を照合する場合には適用されません。 ref/in および ref readonly/ref 修飾子ペアのオーバーライドには変更がないことに注意してください。シグネチャはバイナリ互換性がないため、交換できません。 一貫性の場合、他の署名照合の目的 (非表示など) についても同じことが当てはまります。

メタデータ エンコード

リマインダーとして、

  • ref パラメーターはプレーン byref 型 (IL のT&) として出力されます。
  • in パラメーターは、ref に似ていますが、System.Runtime.CompilerServices.IsReadOnlyAttributeで注釈が付けられます。 C# 7.3 以降では、[in] と仮想の場合は modreq(System.Runtime.InteropServices.InAttribute)も出力されます。

ref readonly パラメーターは、[in] T&として出力され、さらに次の属性で注釈が付きます。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class RequiresLocationAttribute : Attribute
    {
    }
}

さらに、仮想の場合は、in パラメーターとのバイナリ互換性を確保するために、modreq(System.Runtime.InteropServices.InAttribute) で出力されます。 in パラメーターとは異なり、メタデータ サイズの増加を回避し、古いコンパイラ バージョンで ref readonly パラメーターを ref パラメーターとして解釈するために、ref readonly パラメーターに [IsReadOnly] は出力されません (そのため、refref readonly は、異なるコンパイラ バージョン間でもソース破壊的変更になることはありません)。

RequiresLocationAttribute は名前空間修飾名で照合され、コンパイルにまだ含まれていない場合はコンパイラによって合成されます。

ソースで属性を指定すると、ParamArrayAttributeと同様に、パラメーターに適用される場合はエラーになります。

関数ポインター

関数ポインターでは、in パラメーターは modreq(System.Runtime.InteropServices.InAttribute) と共に出力されます (関数ポインターの提案 参照)。 ref readonly パラメーターはその modreqなしで出力されますが、代わりに modopt(System.Runtime.CompilerServices.RequiresLocationAttribute)で出力されます。 以前のバージョンのコンパイラでは、modopt が無視されるため、ref readonly パラメーターが ref パラメーターとして解釈されます (前述のように、ref readonly パラメーターを持つ通常のメソッドの以前のコンパイラ動作と一致します)、modopt を認識する新しいコンパイラ バージョンでは、ref readonly パラメーターを認識して、変換 および呼び出し 呼び出し中に警告を出力します。 以前のコンパイラ バージョンとの一貫性を保つため、LangVersion <= 11 を使用する新しいコンパイラ バージョンでは、対応する引数が ref 修飾子で渡されない限り、ref readonly パラメーターがサポートされていないエラーが報告されます。

パブリック API の一部である場合、関数ポインターシグネチャの修飾子を変更するのはバイナリ区切りであるため、ref または inref readonlyに変更する場合はバイナリ区切りになります。 ただし、通常のメソッドと同様に、in から ref readonly に変更するとき (in 呼び出し位置修飾子でポインタを呼び出す場合)、LangVersion <= 11 を持つ呼び出し元に対してのみソース ブレークが発生します。

重大な変更

オーバーロード解決の ref/in 不一致緩和では、次の例で示す動作の破壊的変更が発生します。

class C
{
    string M(in int i) => "C";
    static void Main()
    {
        int i = 5;
        System.Console.Write(new C().M(ref i));
    }
}
static class E
{
    public static string M(this C c, ref int i) => "E";
}

C# 11 では、呼び出しは E.Mにバインドされるため、"E" が出力されます。 C# 12 では、C.M はバインド (警告あり) が許可されており、適用可能な候補があるため、拡張スコープは検索されません。そのため、"C" が出力されます。

同じ理由により、ソースの破壊的変更もあります。 次の例では、C# 11 で "1" が出力されますが、C# 12 であいまいエラーが発生してコンパイルに失敗します。

var i = 5;
System.Console.Write(C.M(null, ref i));

interface I1 { }
interface I2 { }
static class C
{
    public static string M(I1 o, ref int x) => "1";
    public static string M(I2 o, in int x) => "2";
}

上記の例では、メソッド呼び出しの中断を示していますが、オーバーロード解決の変更によって発生するため、メソッド変換でも同様にトリガーできます。

選択肢

パラメーター宣言

API 作成者は、カスタム属性を持つ左辺値のみを受け入れるように設計された in パラメーターに注釈を付け、不適切な使用法にフラグを設定するアナライザーを提供できます。 API 作成者は、rvalue を禁止するために ref パラメーターを使用することを選択した既存の API の署名を変更することができなくなります。 このような API の呼び出し元は、ref readonly 変数にのみアクセスできる場合でも、ref を取得するために追加の作業を実行する必要があります。 これらの API を ref から [RequiresLocation] in に変更することは、ソースの破壊的変更になります (また、仮想メソッドの場合は、バイナリ破壊的変更でもあります)。

修飾子 ref readonlyを許可する代わりに、コンパイラは特別な属性 ([RequiresLocation]など) がパラメーターに適用されるタイミングを認識できます。 これは LDM 2022-04-25 で説明しました。これはアナライザーではなく言語機能であると判断するため、次のようになります。

値の種類のチェック

C++の暗黙的な byref パラメーターと同様に、ref readonly パラメーターに修飾子を指定せずに左辺値を渡すことは、警告なしで許可できます。 これは LDM 2022-05-11で説明しました。ref readonly パラメーターの主な動機は、これらのパラメーターからの参照をキャプチャまたは返す API であるため、何らかのマーカーが良いことです。

ref readonly に右辺値を渡すと、警告ではなくエラーになる可能性があります。 これは当初、LDM 2022-04-25で受け入れられましたが、後の電子メールのディスカッションでは、ユーザーを中断することなく既存の API を変更できなくなるため、この問題は緩和されました。

in は、ref readonly パラメーターの "自然な" 呼び出しサイト修飾子である可能性があり、ref を使用すると警告が発生する可能性があります。 これにより、一貫性のあるコード スタイルが保証され、呼び出し先で参照が読み取り専用であることが明らかになります (refとは異なります)。 当初は LDM 2022-04-25で受け入れられました。 ただし、警告は、API 作成者が ref から ref readonlyに移行するための摩擦点になる可能性があります。 また、inref readonly+利便性の特徴として再定義されているため、これはLDM 2022-05-11 で却下されました。

保留中の LDM レビュー

C# 12 では、次のオプションは実装されていません。 これらは引き続き潜在的な提案です。

パラメーター宣言

修飾子 (ref readonlyではなくreadonly ref) の逆順序を使用できます。 これは、readonly ref 戻り値とフィールドの動作 (逆順序が許可されていないか、それぞれ異なる何かを意味する) と矛盾し、将来実装される場合は読み取り時のパラメーターと競合する可能性があります。

既定のパラメーター値は、ref readonly パラメーターのエラーである可能性があります。

値の種類のチェック

右辺値を ref readonly パラメーターに渡すときや、呼び出し元注釈とパラメーター修飾子が一致しない場合は、警告の代わりにエラーが生成される可能性があります。 同様に、属性の代わりに特殊な modreq を使用して、ref readonly パラメーターがバイナリ レベルのパラメーター in 区別されるようにすることができます。 これにより、より強力な保証が提供されるため、新しい API には適していますが、破壊的変更を導入できない既存のランタイム API での導入は防止されます。

値の種類のチェックを緩和して、ref を介して in/ref readonly パラメーターに読み取り参照を渡せるようにすることができます。 これは、ref 代入と ref 戻り値が現在どのように機能するかと似ています。また、ソース式の ref 修飾子を介して参照を読み取り時に渡すこともできます。 ただし、ref は通常、ターゲットが ref readonlyとして宣言されている場所に近いので、引数とパラメーター修飾子が通常は離れている呼び出しとは異なり、参照を読み取り専用として渡すことは明らかです。 さらに、は、inを可能にする引数とは異なり、ref 修飾子のみを できます。そのため、inref は引数に対して交換可能になります。または、ユーザーがコードの一貫性を保ちたい場合、in は実質的に廃止されます (ref 代入と ref 戻り値に許可される唯一の修飾子であるため、ref を使用する可能性があります)。

オーバーロードの解決

オーバーロードの解決、オーバーライド、および変換により、ref readonly 修飾子と in 修飾子の互換性が禁止される可能性があります。

既存の in パラメーターのオーバーロード解決の変更は無条件に受け取る可能性がありますが (LangVersion は考慮されません)、これは重大な変更になります。

ref readonly レシーバーを使用して拡張メソッドを呼び出すと、呼び出し元修飾子のない非拡張呼び出しの場合と同様に、"引数 1 を ref または in キーワードで渡す必要があります" という警告が発生する可能性があります (ユーザーは、拡張メソッドの呼び出しを静的メソッド呼び出しに変換することで、このような警告を修正できます)。 カスタム コレクション初期化子または挿入文字列ハンドラーをref readonlyパラメーターと共に使用する際に、同じ警告が報告される可能性がありますが、ユーザーはそれを回避することができませんでした。

ref readonly オーバーロードは、callite 修飾子がない場合やあいまいエラーが発生する可能性がある場合に、値ごとのオーバーロードよりも優先される可能性があります。

メソッドの変換

ターゲット メソッド ref パラメーターがデリゲートの inref readonly パラメーターと一致することを許可できます。 これにより、API 作成者は、たとえばデリゲートシグネチャで ref から in に変更しても、ユーザーへの影響を与えずに済むことが可能になります(これは通常のメソッドシグネチャで許容されている変更と一貫しています)。 ただし、警告だけで次のような readonly 保証違反が発生します。

class Program
{
    static readonly int f = 123;
    static void Main()
    {
        var d = (in int x) => { };
        d = (ref int x) => { x = 42; }; // warning: mismatch between `ref` and `in`
        d(f); // changes value of `f` even though it is `readonly`!
        System.Console.WriteLine(f); // prints 42
    }
}

関数ポインター変換は、ref readonly/ref/in の不一致に警告する可能性がありますが、LangVersion でこれをゲートする場合は、今日の型変換ではコンパイルへのアクセスが必要ないため、かなりの実装投資が必要になります。 さらに、不一致は現在エラーですが、ユーザーが必要に応じて不一致を許可するキャストを追加するのは簡単です。

メタデータ エンコード

In 属性や Out 属性と同様に、ソースで RequiresLocationAttribute を指定できます。 または、IsReadOnly 属性と同様に、さらに設計空間を維持するために単なるパラメーター以外のコンテキストで適用するとエラーになる可能性があります。

関数ポインター ref readonly パラメーターは、異なる modopt/modreq の組み合わせで出力できます (この表の "ソース区切り" は、LangVersion <= 11を持つ呼び出し元を意味します)。

修飾子 コンパイル全体で認識できる 古いコンパイラでは、次のように表示されます。 refref readonly inref readonly
modreq(In) modopt(RequiresLocation) はい in バイナリ、ソース ブレーク バイナリ ブレーク
modreq(In) いいえ in バイナリ、ソース ブレーク わかりました
modreq(RequiresLocation) はい サポートされていない バイナリ、ソース ブレーク バイナリ、ソース ブレーク
modopt(RequiresLocation) はい ref バイナリ ブレーク バイナリ、ソース ブレーク

ref readonly パラメーターの [RequiresLocation] 属性と [IsReadOnly] 属性の両方を出力できます。 その後、inref readonly 古いコンパイラ バージョンでも破壊的変更になりませんが、refref readonly は、古いコンパイラ バージョンのソース破壊的変更になります (ref readonlyinと解釈し、ref 修飾子を禁止するため)、および LangVersion <= 11 を持つ新しいコンパイラ バージョン (整合性のため)。

LangVersion <= 11 の動作は、古いコンパイラ バージョンの動作とは異なる場合があります。 たとえば、ref readonly パラメーターが呼び出されるたびにエラーになる場合があります (callite で ref 修飾子を使用している場合でも)、エラーなしで常に許可される場合があります。

破壊的変更

この提案では、動作の破壊的変更を受け入れることを提案します。これは、ヒットすることはまれであり、LangVersion によって制限されており、ユーザーは拡張メソッドを明示的に呼び出すことによってそれを回避できるためです。 代わりに、次の方法で軽減できます。

  • ref/in の不一致を禁止する (in がまだ使用できなかったため、ref を使用していた古い API の in への移行のみが妨げる)
  • この提案で導入された ref 種類の不一致がある場合、オーバーロード解決ルールを変更して、より良い一致 (以下に指定するより良いルールによって決定される) を探し続けます。
    • または、他の不一致ではなく、refin の不一致 (ref readonlyref/in/by-value) に対してのみ続行します。
ベターネスが支配する

次の例では、現在、Mの 3 回の呼び出しで 3 つのあいまいエラーが発生しています。 あいまいさを解決するための新しい改善ルールを追加できます。 これにより、前に説明したソースの破壊的変更も解決されます。 1つの方法は、例として print 221 を作成することです。これは、ref readonly パラメーターが引数 in に一致する場合に警告となるものであり、in パラメーターの場合には修飾子なしで呼び出すことが許可されているためです。

interface I1 { }
interface I2 { }
class C
{
    static string M(I1 o, in int i) => "1";
    static string M(I2 o, ref readonly int i) => "2";
    static void Main()
    {
        int i = 5;
        System.Console.Write(M(null, ref i));
        System.Console.Write(M(null, in i));
        System.Console.Write(M(null, i));
    }
}

他の引数修飾子を使用して引数を渡すことで改善できた可能性があるパラメーターが、最適化ルールにより悪化としてマークされる可能性があります。 言い換えると、ユーザーは、対応する引数修飾子を変更することで、常に悪いパラメーターをより適切なパラメーターに変換できる必要があります。 たとえば、引数を in渡す場合、ref readonly パラメーターは in パラメーターよりも優先されます。これは、ユーザーが引数を値渡しして in パラメーターを選択できるためです。 このルールは、現在有効な値渡し/in 優先ルールの拡張にすぎません(これは最終的なオーバーロード解決ルールであり、そのパラメーターのいずれかが優れていて、いずれのパラメーターも他のオーバーロードの対応するパラメーターより劣らない場合、オーバーロード全体が優れていると見なされます)。

引数 より良いパラメーター より悪いパラメーター
ref/in ref readonly in
ref ref ref readonly/in
by-value by-value/in ref readonly
in in ref

メソッドの変換も同様に処理する必要があります。 次の例では、現在、2 つのデリゲートの割り当てに対して 2 つのあいまいさのエラーが発生しています。 新しい改良ルールでは、refness 修飾子が対応するターゲット デリゲート パラメーターの refness 修飾子と一致するメソッド パラメーターを、一致しないメソッド パラメーターよりも優先できます。 したがって、次の例では 12出力されます。

class C
{
    void M(I1 o, ref readonly int x) => System.Console.Write("1");
    void M(I2 o, ref int x) => System.Console.Write("2");
    void Run()
    {
        D1 m1 = this.M;
        D2 m2 = this.M; // currently ambiguous

        var i = 5;
        m1(null, in i);
        m2(null, ref i);
    }
    static void Main() => new C().Run();
}
interface I1 { }
interface I2 { }
class X : I1, I2 { }
delegate void D1(X s, ref readonly int x);
delegate void D2(X s, ref int x);

デザイン会議

  • LDM 2022-04-25: 機能は受け入れ可能
  • LDM 2022-05-09: ディスカッションは 3 つの部分に分かれています
  • LDM 2022-05-11: ref を許可し、ref readonly パラメーターに対する呼び出し箇所の注釈なし