次の方法で共有


低レベルの構造体の機能向上

メモ

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

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

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

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

まとめ

この提案は、struct パフォーマンス向上のためのいくつかの異なる提案 (ref フィールドと有効期間の既定値をオーバーライドする機能) を集約したものです。 その目標は、低レベルの struct 改善のための単一の包括的な機能セットを作成するためのさまざまな提案を考慮した設計となることです。

注: この仕様の以前のバージョンでは、Span safety 機能仕様で導入された用語「ref-safe-to-escape」および「safe-to-escape」が使用されていました。 ECMA 標準委員会 は、それぞれ名前を "ref-safe-context""safe-context"に変更しました。 セーフ コンテキストの値は、「declaration-block」、「function-member」、「caller-context」を一貫して使用するように調整されています。 スペックレットでは、これらの用語に異なる言い回しが使用されており、"呼び出し元コンテキスト" の同義語として "安全に復帰する" も使われていました。 この仕様は、C# 7.3 標準の用語を使用するように更新されました。

このドキュメントで説明されているすべての機能が C# 11 で実装されているわけではありません。 C# 11 には次のものが含まれます。

  1. ref フィールドと scoped
  2. [UnscopedRef]

これらの機能は引き続き、C# のこれからのバージョンに関するオープンな提案となります。

  1. ref フィールドを ref struct にする
  2. 制限付きの型の使用停止

目的

以前のバージョンの C# では、ref 戻り値、ref struct、関数ポインターなど、さまざまな低レベルのパフォーマンス機能が言語に追加されていました。これにより、.NET 開発者は、型とメモリの安全性のために C# 言語規則を引き続き活用しながら、高パフォーマンスのコードを記述できていました。 また、Span<T>などの .NET ライブラリで基本的なパフォーマンスの種類を作成することもできました。

これらの機能が .NET エコシステム開発者に引き継がれているため、内部と外部の両方で、エコシステム内に残っている摩擦点に関する情報が提供されています。 作業を完了するためにunsafe コードに移行する必要がある場所、またはランタイムでSpan<T>のような特定の型を特別に扱う必要がある場所。

現在では、Span<T> は、ランタイムが効果的に internal フィールドとして扱う ByReference<T>ref を使用して実現されています。 これにより、ref フィールドの利点が提供されますが、refの他の用途と同様に、言語が安全検証を提供しないという欠点があります。 さらに、この型は internal であるために dotnet/runtime のみを使用できることにより、サード パーティは ref フィールドに基づいて独自のプリミティブを設計することができません。 この作業の動機のひとつは、ByReference<T> を削除し、すべてのコード ベースで適切な ref フィールドを使用することです。

この提案は、既存の低レベル機能を基にしてこれらの問題に対処することを計画しています。 具体的には、以下を目的としています。

  • ref struct 型が ref フィールドを宣言できるようにすること。
  • ランタイムが C# 型システムを使用して Span<T> を完全に定義し、ByReference<T> などの特殊なケースの種類を削除できるようにすること。
  • struct 型がフィールドに ref をリターンできるようにすること。
  • 有効期間の既定値の制限に起因する unsafe の使用をランタイムで削除できるようにすること。
  • struct でマネージド型と非マネージド型の安全な fixed バッファを宣言できるようにすること。

詳細な設計

ref struct の安全性に関するルールは、上記の用語を使用して span の安全性に関するドキュメントで定義されています。 これらの規則は、§9.7.2 および §16.4.12の C# 7 標準に組み込まれています。 このドキュメントでは、この提案の結果として、このドキュメントに必要な変更について説明します。 これらの変更が承認された機能として受け入れられると、そのドキュメントに組み込まれるようになります。

この設計が完了すると、Span<T> の定義は次のようになります。

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    // This constructor does not exist today but will be added as a part 
    // of changing Span<T> to have ref fields. It is a convenient, and
    // safe, way to create a length one span over a stack value that today 
    // requires unsafe code.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

ref フィールドと scoped を指定する

この言語により、開発者は ref内に ref struct フィールドを宣言できるようになります。 これは、大規模な変更可能な struct インスタンスをカプセル化する場合や、ランタイム以外のライブラリで Span<T> などの高パフォーマンスの種類を定義する場合などに便利です。

ref struct S 
{
    public ref int Value;
}

ref フィールドは、ELEMENT_TYPE_BYREF 署名を使用してメタデータに出力されます。 つまり、ref ローカルや ref 引数を出力する方法と何ら変わりはありません。 たとえば、ref int _fieldELEMENT_TYPE_BYREF ELEMENT_TYPE_I4として出力されます。 これにより、このエントリを許可するために ECMA335 を更新する必要があり、これはかなり簡単にできるはずです。

開発者は、ref struct 式を使用して、ref フィールドで default を引き続き初期化することができます。その場合、宣言されたすべての ref フィールドの値は null となります。 このようなフィールドを使用しようとすると、NullReferenceException がスローされます。

ref struct S 
{
    public ref int Value;
}

S local = default;
local.Value.ToString(); // throws NullReferenceException

C# 言語は、refnull とはならないと主張しますが、これはランタイム レベルでは有効であり、セマンティクスが明確に定義されています。 型に ref フィールドを導入する開発者は、この可能性に注意して、使用コードにこの詳細が漏出しないよう注意することが強く推奨されます。 代わりに、ref フィールドは、ランタイム ヘルパーを使用して null 以外として検証し、初期化されていない struct が誤って使用された場合は例外をスローする必要があります。

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

次の方法で ref フィールドを readonly 修飾子と組み合わせることができます。

  • readonly ref: これは、コンストラクターまたは init メソッドの外部では ref 再割り当てができないフィールドです。 これらのコンテキストの外部で値として割り当てることができます
  • ref readonly: これは再割り当て可能なフィールドですが、どの時点でも値として割り当てることはできません。 これにより、in パラメーターを ref フィールドに ref 再割り当てできます。
  • readonly ref readonly:ref readonlyreadonly ref の組み合わせです。
ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // Okay
        Field1 = array[0];      // Error: can't assign ref readonly value (value is readonly)
        Field2 = ref array[0];  // Error: can't repoint readonly ref
        Field2 = array[0];      // Okay
        Field3 = ref array[0];  // Error: can't repoint readonly ref
        Field3 = array[0];      // Error: can't assign ref readonly value (value is readonly)
    }
}

readonly ref struct の場合、ref フィールドは readonly refとして宣言する必要があります。 readonly ref readonly として宣言する必要はありません。 これにより、readonly struct がそのようなフィールドを介して間接的な変異を持つことができるようになりますが、現在レファレンス型を指している readonly フィールドと変わりません (詳細)

readonly ref は、他のフィールドと同じ initonly フラグを使用してメタデータに出力されます。 ref readonly フィールドは、System.Runtime.CompilerServices.IsReadOnlyAttribute で属性付けされます。 readonly ref readonly は、両方の項目で出力されます。

この機能には、ランタイムのサポートと ECMA 仕様への変更が必要です。そのため、これらは対応する機能フラグが corelib で設定されている場合にのみ有効になります。 正確な API の追跡に関する問題はこちらで追跡されています https://github.com/dotnet/runtime/issues/64165

ref フィールドを許可するために必要となる安全なコンテキスト ルールに対する一連の変更は、小さく対象を絞ったものです。 ルールは既に、既存の ref フィールドと API から使用されるフィールドを考慮に入れています。 変更は、作成方法と ref 再割り当て方法の 2 つの側面にのみ焦点を当てる必要があります。

まず、ref-safe-context の値を確立する規則を、次のように ref フィールドに対して更新する必要があります。

ref e.Fref-safe-context の形式としての表現は、次の通りです。

  1. Fref フィールドである場合、ref-safe-contexte です。
  2. それ以外で、e が参照型の場合は、呼び出し元コンテキストref-safe-context になります。
  3. それ以外の場合、その ref-safe-context は、e から取得されます。

これは、ルールが常に ref 内に存在する ref struct 状態を考慮するため、ルールの変更を表すものではありません。 実際には、refSpan<T> 状態がこれまでずっとこのように機能しており、消費ルールがこれを正しく考慮しています。 ここでの変更は、開発者が ref フィールドに直接アクセスして、Span<T>に暗黙的に適用される既存のルールによって確実に行われるようにすることを求めているだけです。

これは、ref フィールドを ref から ref struct として返すことができるが、通常のフィールドでは返すことができないことを意味します。

ref struct RS
{
    ref int _refField;
    int _field;

    // Okay: this falls into bullet one above. 
    public ref int Prop1 => ref _refField;

    // Error: This is bullet four above and the ref-safe-context of `this`
    // in a `struct` is function-member.
    public ref int Prop2 => ref _field;
}

これは一見するとエラーのように見えるかもしれませんが、意図的な設計ポイントです。 ただし、これはこの提案によって作成される新しいルールではなく、開発者が独自の Span<T> 状態を宣言できるという、これまでの ref の動作の既存のルールを確認するものです。

次に、ref フィールドが存在するように ref 再割り当てのルールを調整する必要があります。 ref 再割り当ての主なシナリオは、ref struct パラメーターを ref フィールドに格納するコンストラクター ref です。 サポートはより一般的になりますが、これがコアなシナリオです。 これをサポートするために、ref 再割り当てのルールは、次のように ref フィールドを考慮するように調整されます。

ref 再割り当てルール

= ref 演算子の左オペランドは、ref ローカル変数、ref パラメーター (this以外)、out パラメーター、、または ref フィールドにバインドする式であることが必要です。

e1 = ref e2形式での ref 再割り当ての場合、次の両方が true である必要があります。

  1. e2ref-safe-context は、e1ref-safe-context と同じ大きさ以上である必要があります
  2. e1は、e2 と同じsafe-contextを持つことが必要です注記

つまり、必要な Span<T> コンストラクターは、追加の注釈なしで動作します。

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    public Span(ref T value)
    {
        // Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The 
        // safe-context of `this` is *return-only* and ref-safe-context of `value` is 
        // *caller-context* hence this is legal.
        _field = ref value;
        _length = 1;
    }
}

ref 再割り当て規則の変更により、パラメーター ref はメソッドから、ref 値の ref struct フィールドとしてエスケープできるようになりました。 互換性に関する考慮事項のセクションで説明したように、これによって、ref パラメーターが ref フィールドとしてエスケープされることを意図していなかった既存の API のルールが変更される可能性があります。 パラメーターの有効期間ルールは、使用ではなく宣言のみに基づいています。 すべての ref および in パラメーターは、呼び出し元コンテキスト の ref-safe-context を持っているため、ref または ref フィールドから返すことが可能になりました。 APIがエスケープ型または非エスケープ型の ref パラメータをサポートし、これにより C# 10 呼び出しサイト セマンティクスを復元するために、言語では有効期間の制限付き注釈が導入されます

scoped 修飾子

キーワード scoped は、値の有効期間を制限するために使用されます。 これは、ref または ref struct である値に適用でき、それぞれ ref-safe-context または safe-context の有効期間を function-member に制限する効力があります。 次に例を示します。

パラメーターまたはローカル ref-safe-context 安全なコンテキスト
Span<int> s function-member caller-context
scoped Span<int> s function-member function-member
ref Span<int> s caller-context caller-context
scoped ref Span<int> s function-member caller-context

この関係では、値の ref-safe-context が、セーフ コンテキストを超えることはありません。

これにより、C# 11 の API に注釈を付け、C# 10 と同じルールを持つことができます。

Span<int> CreateSpan(scoped ref int parameter)
{
    // Just as with C# 10, the implementation of this method isn't relevant to callers.
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 and legal in C# 11 due to scoped ref
    return CreateSpan(ref parameter);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

scoped 注釈は、thisstruct パラメーターを scoped ref T として定義できるようになったことも示しています。 以前は、他の ref パラメーターとは異なる ref-safe-context ルールを持つ ref パラメーターとして、ルールで特殊なケースを指定する必要がありました (セーフ コンテキスト ルールにレシーバーを含めたり、除外したりするすべてのレファレンスを参照)。 これで、ルール全体を通して一般的な概念として表現できるようになり、より簡略化されます。

scoped 注釈は、次の場所にも適用できます。

  • ローカル: この注釈は、初期化子の有効期間に関係なく、function-member の有効期間を safe-context (ref ローカルの場合は ref-safe-context) として設定します。
Span<int> ScopedLocalExamples()
{
    // Error: `span` has a safe-context of *function-member*. That is true even though the 
    // initializer has a safe-context of *caller-context*. The annotation overrides the 
    // initializer
    scoped Span<int> span = default;
    return span;

    // Okay: the initializer has safe-context of *caller-context* hence so does `span2` 
    // and the return is legal.
    Span<int> span2 = default;
    return span2;

    // The declarations of `span3` and `span4` are functionally identical because the 
    // initializer has a safe-context of *function-member* meaning the `scoped` annotation
    // is effectively implied on `span3`
    Span<int> span3 = stackalloc int[42];
    scoped Span<int> span4 = stackalloc int[42];
}

ローカルでの scoped のその他の用途については、以下の説明します。

scoped 注釈は、戻り値、フィールド、配列要素などの他の場所には適用できません。さらに、scoped は、refin、または out に適用される場合にのみ影響しますが、値が ref struct である場合にのみ影響します。 scoped int は常に安全に返せるため、ref struct のような宣言があっても影響はありません。 コンパイラは、開発者の混乱を避けるために、このようなケースの診断を作成します。

out パラメーターの動作を変更する

および パラメーターを フィールドとして返す互換性変更の影響をさらに制限するために、言語では、 パラメーターの既定の ref-safe-context 値が、関数メンバーとして されるように変更されます。 実質的に、out パラメーターは今後暗黙的に scoped out されます。 互換性の観点から、これは ref によって返すことができないことを意味します。

ref int Sneaky(out int i) 
{
    i = 42;

    // Error: ref-safe-context of out is now function-member
    return ref i;
}

これにより、ref struct 値を返し、out パラメーターを持つ API の柔軟性が向上します。これは、パラメータが参照によってキャプチャされることを考慮する必要がなくなったためです。 これは、リーダー スタイル API の一般的なパターンであるために重要です。

Span<byte> Read(Span<byte> buffer, out int read)
{
    // .. 
}

Span<byte> Use()
{
    var buffer = new byte[256];

    // If we keep current `out` ref-safe-context this is an error. The language must consider
    // the `read` parameter as returnable as a `ref` field
    //
    // If we change `out` ref-safe-context this is legal. The language does not consider the 
    // `read` parameter to be returnable hence this is safe
    int read;
    return Read(buffer, out read);
}

また、言語では、out パラメーターに渡された引数を返すことができなくなります。 out パラメーターへの入力を戻し可能として扱うことは、開発者に多大な混乱を引き起こしました。 これは基本的に、out を考慮しない言語以外では使用されない呼び出し元によって渡される値を開発者が検討するよう強制することで、out の意図を覆します。 今後、ref struct をサポートする言語では、out パラメーターに渡された元の値を読み取らないようにする必要があります。

C# では、明確な割り当てルールを使用してこれを実現します。 これは、ref 安全コンテキストルールを遵守するだけでなく、パラメーター値 out を代入して返す既存のコードも許可します。

Span<int> StrangeButLegal(out Span<int> span)
{
    span = default;
    return span;
}

これらの変更を組み合わせることで、 パラメーターへの引数がメソッド呼び出し時にセーフ コンテキスト や ref-safe-context の値を として提供しないことを意味します。 これにより、ref フィールドの全体的な互換性への影響が大幅に軽減され、開発者が out について考える方法が簡略化されます。 out パラメーターの引数は戻り値には影響せず、単なる出力です。

宣言式の safe-context を推論する

引数 (out) または分解 (M(x, out var y)) からの宣言変数の (var x, var y) = M() は、次のうち最も範囲が狭いものになります。

  • 発信者コンテクスト
  • out 変数が scoped にマークされている場合は、declaration-block (つまり、function-member またはそれより狭い範囲)。
  • out 変数の型が ref struct である場合、受信側を含めて含まれる呼び出しに対するすべての引数を考慮してください:
    • 対応するパラメーターが out ではなく、safe-context の範囲が return-only 以上である引数の safe-context
    • 対応するパラメーターの ref-safe-context の範囲が return-only 以上である引数の ref-safe-context

宣言式の推論された safe-context の例」も参照してください。

暗黙的 scoped パラメーター

全体的には、scoped として暗黙的に宣言されている 2 つの ref ロケーションがあります。

  • this インスタンスメソッドの struct
  • out パラメーター

ref セーフ コンテキストルールは、scoped refref の観点から記述されます。 ref セーフ コンテキストの場合、in パラメーターは ref に相当し、outscoped ref と同等です。 inout の両方は、ルールのセマンティックにとって重要な場合にのみ特別に呼び出されます。 それ以外の場合は、それぞれ ref および scoped ref と見なされます。

パラメーターに対応する引数の in について説明する場合、仕様では ref 引数として一般化されます。引数が lvalue の場合、ref-safe-context は lvalue に対する値になり、それ以外の場合は function-member になります。 ここでも、in は、現在のルールのセマンティックにとって重要な場合にのみ、ここで呼び出されます。

戻り専用のセーフ コンテキスト

デザインでは、新しいセーフ コンテキストの導入も必要です: 戻り専用。 これは、返すことができるという点で caller-context に似ていますが、その場合に返すことができるのは return ステートメント経由のみです。

return-only のコンテキストは、function-member より大きく、caller-context より小さい範囲になります。 return ステートメントに提供される式は、return-only 以上である必要があります。 そのため、ほとんどの既存のルールは適用されません。たとえば、refreturn-only である式から パラメーターへの割り当ては、ref パラメーターの safe-context (caller-context) よりも小さいため失敗します。 この新しいエスケープ コンテキストの必要性については、後ほど説明します。

以下の 3 か所は、既定で return-only となります:

  • ref または in パラメーターの ref-safe-context は、return-only になります。 これは、ref structの問題を防ぐために で部分的に行われます。 これは、モデルを簡略化して、互換性の変更を最小限に抑えるために、均一に行われます。
  • outref struct パラメーターの safe-contextreturn-only になります。 これにより、戻り値と out を同等に表現できます。 これは、out が暗黙のうちに scoped に相当するため、ref-safe-contextセーフコンテキストのよりも小さく、循環的な割り当ての問題を避けられます。
  • struct コンストラクターの this パラメーターの safe-contextreturn-only になります。 out パラメーターとしてモデル化されているため、これは除外されます。

メソッドまたはラムダから値を明示的に返す式またはステートメントには、セーフ コンテキスト が必要です。また、ref-safe-context に該当する場合は、少なくとも戻り専用が必要です。 これには、return ステートメント、式本体のメンバー、およびラムダ式が含まれます。

同様に、out への割り当てには、return-only 以上の safe-context が必要です。 ただし、これは特殊なケースではなく、既存の割り当てルールに従っているだけです。

注: 型が 型ではない式は、常に呼び出し元コンテキストの セーフ コンテキスト に含まれます。

メソッド呼び出しのルール

メソッド呼び出しの ref セーフ コンテキスト ルールは、いくつかの方法で更新されます。 1 つ目は、scoped が引数に与える影響を認識する方法です。 パラメーター p に渡される引数 expr の場合:

  1. pscoped ref の場合、safe-context を算定する際の引数として expr は考慮されません。
  2. pscoped の場合、safe-context を算定する際の引数として expr は考慮されません。
  3. pout の場合、ref-safe-context または safe-context を算定する際の引数として expr は考慮されません (詳細はこちら)。

“"貢献しない" という表現は、メソッドの返り値の計算において、それぞれref-safe-context またはセーフコンテキスト の値を算出するときに、引数が考慮されないことを意味します。” これは、scoped 注釈によって阻止され、その有効期間に対して値を考慮できないためです。

メソッド呼び出しルールを簡略化できるようになりました。 受信側を特別なケースにする必要はなくなり、struct の場合には単に scoped ref T になりました。 値ルールは、ref フィールドの戻り値を考慮するように変更する必要があります。

メソッド呼び出し e1.M(e2, ...) (M() が ref-to-ref-struct を返さないもの) から得られる値には、次の最も範囲が狭いものから取得される safe-context があります。

  1. caller-context
  2. すべての引数式によって提供される ref struct の場合、戻り値は です。
  3. 戻り値が ref struct の場合、ref-safe-context にはすべての ref 引数が考慮されます

M() が ref-to-ref-struct を返す場合、safe-context は、ref-to-ref-struct であるすべての引数の safe-context と同じです。 メソッド引数が一致する必要があるため、異なる safe-context を持つ複数の引数がある場合はエラーです。

ref 呼び出しルールは、次のように簡略化できます。

メソッド呼び出し ref e1.M(e2, ...) (M() が ref-safe-context を返さない) から得られる値は、次のコンテキストの中で最も範囲が狭い ref-safe-context です。

  1. caller-context
  2. safe-context は、すべての引数式で提供されます
  3. すべての ref 引数が考慮された ref-safe-context

M() が ref-to-ref-struct を返す場合、ref-safe-context は、ref-to-ref-struct であるすべての引数が考慮された最も狭い範囲の ref-safe-context になります。

このルールでは、必要なメソッドの 2 つの変形を定義できるようになりました。

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *function-member* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> CreateAndCapture(ref int value)
{
    // Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *caller-context* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
    // Okay: the safe-context of `span` is *caller-context* hence this is legal.
    return span;

    // Okay: the local `refLocal` has a ref-safe-context of *function-member* and a 
    // safe-context of *caller-context*. In the call below it is passed to a 
    // parameter that is `scoped ref` which means it does not contribute 
    // ref-safe-context. It only contributes its safe-context hence the returned
    // rvalue ends up as safe-context of *caller-context*
    Span<int> local = default;
    ref Span<int> refLocal = ref local;
    return ComplexScopedRefExample(ref refLocal);

    // Error: similar analysis as above but the safe-context of `stackLocal` is 
    // *function-member* hence this is illegal
    Span<int> stackLocal = stackalloc int[42];
    return ComplexScopedRefExample(ref stackLocal);
}

オブジェクト初期化子のルール

オブジェクト初期化子式の safe-context は、次の中で最も狭い範囲のものになります。

  1. コンストラクター呼び出しの safe-context
  2. 受信者にエスケープできるメンバー初期化インデクサーへの引数の safe-context および ref-safe-context
  3. 非読み取り専用セッターに対するメンバー初期化子内の割り当ての RHS の safe-context (ref 割り当ての場合は ref-safe-context)。

これをモデル化するもう 1 つの方法は、受信側に割り当てることができるメンバー初期化子への引数をコンストラクターの引数と考える方法です。 これは、メンバー初期化子が実質的にはコンストラクター呼び出しであることによります。

Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
    Field = stackSpan;
}

// Can be modeled as 
var x = new S(ref heapSpan, stackSpan);

このモデリングは、MAMM がメンバー初期化子を特別に考慮する必要があることを示しているため、重要です。 この特定のケースは、より狭い safe-context を持つ値をより高いものに割り当てることができるため、無効であることが必要であることを検討してください。

メソッド引数が一致することが必要です

ref フィールドが存在するということは、ref パラメーターをメソッドの ref struct 引数にフィールドとして格納できるようになったためにメソッド引数一致に関するルールを更新する必要があることを意味します。 以前の場合、ルールはフィールドとして格納されている別の ref struct のみを考慮する必要がありました。 これによる影響については、「互換性に関する考慮事項」で説明します。 新しいルールは ...

任意のメソッド呼び出し e.M(a1, a2, ... aN) が対象

  1. 最も狭い safe-context を以下から算定します。
    • caller-context
    • すべての引数の safe-context
    • 対応するパラメーターの ref-safe-contextcaller-context であるすべての ref 引数の ref-safe-context
  2. ref struct 型のすべての ref 引数は、その safe-context を持つ値によって割り当て可能であることが必要です。 これは refin を含むようにoutケースです

任意のメソッド呼び出し e.M(a1, a2, ... aN) が対象

  1. 最も狭い safe-context を以下から算定します。
    • caller-context
    • すべての引数の safe-context
    • 対応するパラメーターが scoped でないすべての ref 引数の ref-safe-context
  2. ref struct 型のすべての out 引数は、その safe-context を持つ値によって割り当て可能であることが必要です。

scoped の存在により、開発者は、scoped として返されないパラメーターをマークすることで、このルールによって生じる摩擦を軽減できます。 これにより、上記の両方のケースで (1) から引数が削除され、呼び出し元の柔軟性が向上します。

この変更の影響については、以下で詳しく説明します。 これにより、開発者は、非エスケープ型の ref-like 値に scoped で注釈を付けることで、呼び出しサイトの柔軟性を高めることができるようになります。

パラメータースコープのバリアンス

パラメーターの scoped 修飾子と [UnscopedRef] 属性 (以下を参照) は、オブジェクトのオーバーライド、インターフェイスの実装、delegate 変換ルールにも影響します。 オーバーライド、インターフェイス実装、または delegate 変換のシグネチャは、次のことができます。

  • ref または in パラメーターに scoped を追加すること
  • scopedref struct パラメーターに追加すること
  • [UnscopedRef]out パラメーターから削除すること
  • [UnscopedRef]ref struct 型の ref パラメーターから削除すること

scoped または [UnscopedRef] に関するその他の違いは、不一致と見なされます。

コンパイラは、オーバーライド、インターフェイスの実装、デリゲートの変換において、および に関して安全でないスコープの不一致を検出した場合、診断を報告します。

  • このメソッドには、[UnscopedRef] の追加が一致しない (scoped を削除するのではなく) ref struct 型の ref または out パラメーターがあります。 (このケースでは愚かな循環割り当てが可能であるため、他のパラメーターは必要ありません。)
  • または、次の両方が当てはまります。
    • メソッドは ref struct を返すか、ref または ref readonly を返すか、メソッドに ref struct 型の ref または out パラメーターがあります。
    • メソッドには、少なくとも 1 つの追加の refin、または out パラメーター、または ref struct 型のパラメーターがあります。

診断は、次の理由で他のケースでは報告されません。

  • このようなシグネチャを持つメソッドは、渡された ref をキャプチャできないため、スコープの不一致は危険ではありません。
  • これには、非常に一般的で単純なシナリオ (たとえば、TryParse メソッドシグネチャで使用される単純な古い out パラメーター) が含まれ、言語バージョン 11 全体で使用される (したがって、out パラメーターのスコープが異なる) という理由だけでスコープの不一致を報告すると混乱する可能性があります。

不一致のシグネチャが両方とも C#11 ref safe コンテキスト ルールを使用している場合、診断はエラーとして報告されます。それ以外の場合、診断は警告です。

スコープの不一致の警告は、scoped を使用できない C#7.2 ref セーフ コンテキスト ルールでコンパイルされたモジュールで報告されることがあります。 このような場合、他の不一致の署名を変更できない場合に警告を抑制することが必要になることがあります。

また、scoped 修飾子と [UnscopedRef] 属性は、メソッドのシグネチャに次のような影響を与えます。

  • scoped 修飾子と [UnscopedRef] 属性は非表示に影響しないこと
  • オーバーロードは、scoped または [UnscopedRef] でのみ異なることはできないこと

ref フィールドと scoped のセクションは長いため、提案された破壊的変更の簡単な概要で締めくくることを望んでいました。

  • ref-safe-context を持つ値 は、の呼び出し元コンテキスト に対して、 または フィールドから返されます。
  • out パラメーターは、関数メンバーセーフコンテキスト を持つでしょう。

注記:

  • ref フィールドは、ref struct 内でのみ宣言できます
  • ref フィールドは、staticvolatile または const で宣言できません
  • ref フィールドには、ref struct 型を持つことはできません。
  • 参照アセンブリ生成プロセスでは、ref struct 内に ref フィールドが存在することを保持する必要があります
  • readonly ref struct は、その ref フィールドを readonly ref として宣言する必要があります
  • by-ref 値の場合、scopedin、または out の前に ref 修飾子を指定する必要があります
  • span の安全規則のドキュメントは、このドキュメントの記載に従って更新されます
  • 新しい ref safe コンテキスト ルールは、次のいずれかの場合に有効になります
    • コア ライブラリには、ref フィールドのサポートを示す機能フラグが含まれている
    • langversion 値が 11 以上である

構文

13.6.2 ローカル変数の宣言: 'scoped'? を追加。

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 for ステートメント: local_variable_declaration から間接的に 'scoped'? が追加されました。

13.9.5 foreach ステートメント: 'scoped'? を追加。

foreach_statement
    : 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
      embedded_statement
    ;

12.6.2 引数のリスト: out 宣言変数の 'scoped'? を追加。

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' ('scoped'? local_variable_type)? identifier
    ;

12.7 分解表現:

[TBD]

15.6.2 メソッドパラメーター: parameter_modifier'scoped'? を追加。

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

parameter_modifier
    | 'this' 'scoped'? parameter_mode_modifier?
    | 'scoped' parameter_mode_modifier?
    | parameter_mode_modifier
    ;

parameter_mode_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

20.2 デリゲート宣言: fixed_parameter から間接的に 'scoped'? が追加されました。

12.19 匿名関数式: 'scoped'? を追加。

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

制限付きの型の使用停止

コンパイラには、主に文書化されていない一連の「制限された型」の概念があります。 C# 1.0 では動作を表す汎用的な方法がなかったため、これらの型には特別なステータスが与えられていました。 特に、型に実行スタックへの参照を含めることができるということです。 代わりに、コンパイラはそれらに関する特別な知識を持ち、常に安全な方法にその使用を制限しました。許可されていない戻り値、配列要素として使用できない、ジェネリックなどで使用することはできませんでした。

ref フィールドが利用可能になり、ref struct をサポートするように拡張された場合、これらの型は ref struct フィールドと ref フィールドを組み合わせて C# で正しく定義することができます。 したがって、ランタイムが ref フィールドをサポートしていることをコンパイラが検出すると、制限された型の概念はなくなります。 代わりに、コードで定義されている型を使用します。

これをサポートするために、ref セーフ コンテキスト ルールは次のように更新されます。

  • __makerefはシグネチャ static TypedReference __makeref<T>(ref T value) を持つメソッドとして扱われる
  • __refvalue はシグネチャ static ref T __refvalue<T>(TypedReference tr) を持つメソッドとして扱われる。 式 __refvalue(tr, int) は、2 番目の引数を型パラメーターとして効果的に使用します。
  • パラメーターとして __arglist には、ref-safe-context と、関数メンバーセーフコンテキスト があります。
  • 式としての __arglist(...) には、function-memberref-safe-contextsafe-context が含まれます。

ランタイムに準拠することで、TypedReferenceRuntimeArgumentHandle、および ArgIteratorref struct として定義されます。 さらに TypedReference は、どのような型であっても(すべての値を格納できる)、ref に対して ref struct フィールドを持つものとして見なされる必要があります。 上記のルールと組み合わせることで、スタックへの参照が有効期間を超えてエスケープされないようにします。

注: 厳密に言えば、これはコンパイラ実装の詳細と言語の一部です。 ただし、ref フィールドとの関係を考えると、わかりやすくするために言語提案に含まれています。

スコープなしを提供する

注目すべき摩擦点の 1 つは、refのインスタンス メンバーにおいて struct でフィールドを返せないことです。 つまり、開発者はメソッド / プロパティを返す ref を作成できないため、フィールドを直接公開する必要があります。 これにより、最も必要とされることが多い refstruct 戻り値の有用性が低下します。

struct S
{
    int _field;

    // Error: this, and hence _field, can't return by ref
    public ref int Prop => ref _field;
}

この既定値の根拠は妥当ですが、参照によって struct エスケープ this に本質的な問題はありません。これは、ref セーフ コンテキスト ルールによって選択された既定値にすぎません。

これを修正するため、言語は UnscopedRefAttribute をサポートすることによって、scoped 有効期間注釈の反対を提供します。 これは任意の ref に適用でき、ref-safe-context を既定値よりも 1 レベル広く変更します。 次に例を示します。

UnscopedRef の適用対象 元の ref-safe-context 新しい ref-safe-context
インスタンス メンバー function-member return-only
in / ref パラメーター return-only 発信者コンテクスト
out パラメーター function-member return-only

struct のインスタンス メソッドに [UnscopedRef] を適用すると、暗黙的な this パラメーターを変更するという影響を与えます。 つまり、this は、同じ型の注釈なしの ref として機能します。

struct S
{
    int field; 

    // Error: `field` has the ref-safe-context of `this` which is *function-member* because 
    // it is a `scoped ref`
    ref int Prop1 => ref field;

    // Okay: `field` has the ref-safe-context of `this` which is *caller-context* because 
    // it is a `ref`
    [UnscopedRef] ref int Prop1 => ref field;
}

注釈を out パラメーターに配置して、C# 10 の動作に復元することもできます。

ref int SneakyOut([UnscopedRef] out int i)
{
    i = 42;
    return ref i;
}

ref セーフ コンテキスト ルールの目的上、このような [UnscopedRef] out は単に ref と見なされます。 in が有効期間の目的で ref と見なされるのと同様です。

[UnscopedRef] 注釈は、struct 内の init メンバーとコンストラクターでは許可されません。 それらのメンバーは、ref のセマンティクスにおいて、readonly のメンバーを可変と見なすことから、既に特別な存在です。 つまり、これらのメンバーに対する ref の取得は、ref readonly ではなく、単純に ref となります。 これは、コンストラクターと init の境界内で許可されます。 [UnscopedRef] を許可すると、このような ref がコンストラクターの外部で誤ってエスケープされ、readonly セマンティクスの発生後に変更が許可されることになります。

属性型には、次の定義があります。

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(
        AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class UnscopedRefAttribute : Attribute
    {
    }
}

注記:

  • [UnscopedRef] で注釈が付けられたインスタンス メソッドまたはプロパティでは、thiscaller-context に設定されています。
  • [UnscopedRef] で注釈が付けられたメンバーは、インターフェイスを実装できません。
  • 以下での [UnscopedRef] の使用はエラーです
    • struct に宣言されていないメンバー
    • static メンバー、structinit メンバーまたはコンストラクター
    • scoped としてマークされたパラメーター
    • 値で渡されるパラメーター
    • 暗黙的にスコープが設定されていない参照によって渡されるパラメーター

ScopedRefAttribute

scoped 注釈は、型 System.Runtime.CompilerServices.ScopedRefAttribute 属性を使用してメタデータに出力されます。 属性は名前空間修飾名で照合されるため、定義を特定のアセンブリに含める必要はありません。

ScopedRefAttribute 型はコンパイラでのみ使用され、ソースでは使用できません。 コンパイルにまだ含まれていない場合には、型宣言はコンパイラによって合成されます。

型の定義は次のとおりです。

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

コンパイラは、scoped 構文を使用してパラメーターにこの属性を出力します。 これは、構文によって値が既定の状態と異なる場合にのみ出力されます。 たとえば、scoped out では属性が出力されません。

RefSafetyRulesAttribute

C#7.2 と C#11 の間の ref セーフ コンテキスト ルールにはいくつかの違いがあります。 これらの違いにより、C#10 以前でコンパイルされた参照に対して C#11 で再コンパイルすると、破壊的変更が発生する可能性があります。

  1. スコープなしの ref/in/out パラメーターは、C#7.2 ではなく C#11 の ref structref フィールドとしてメソッド呼び出しをエスケープできます
  2. out パラメーターは C#11 で暗黙的にスコープ指定され、C#7.2 ではスコープなしです
  3. ref / inパラメーター(ref struct 型への)は、C#11 では暗黙的にスコープ指定され、C#7.2 ではスコープなしです

C#11 を使用して再コンパイルする際に大規模な変更が発生する可能性を減らすために、C#11 コンパイラを更新し、メソッド呼び出し に ref セーフ コンテキスト規則 を適用します。これは、メソッド宣言の分析に使用された規則 と一致するものです。 基本的に、古いコンパイラでコンパイルされたメソッドの呼び出しを分析する場合、C#11 コンパイラは C#7.2 ref セーフ コンテキスト ルールを使用します。

これを有効にするために、モジュールが -langversion:11 以上でコンパイルされるか、ref フィールドの機能フラグを含む corlib でコンパイルされるときに、コンパイラによって新しい [module: RefSafetyRules(11)] 属性が出力されます。

属性の引数は、モジュールのコンパイル時に使用される ref セーフ コンテキスト ルールの言語バージョンを示します。 バージョンは現在、コンパイラに渡される実際の言語バージョンに関係なく、11 で修正されています。

コンパイラのこれからのバージョンでは、ref セーフ コンテキスト ルールが更新され、異なるバージョンの属性が出力される予定です。

コンパイラが、11 以外の version を持つ [module: RefSafetyRules(version)] を含むモジュールを読み込む場合、そのモジュールで宣言されているメソッドの呼び出しがある場合、コンパイラは認識されないバージョンの警告を報告します。

C#11 コンパイラがメソッド呼び出しを分析する場合 :

  • メソッド宣言を含むモジュールに version に関係なく、[module: RefSafetyRules(version)] が含まれている場合、メソッド呼び出しは C#11 ルールで分析されます。
  • メソッド宣言を含むモジュールがソースからのものであり、-langversion:11 または ref フィールドの機能フラグを含む corlib でコンパイルされた場合、メソッド呼び出しは C#11 ルールで分析されます。
  • メソッド宣言を含むモジュールが System.Runtime { ver: 7.0 } を参照している場合、メソッド呼び出しは C#11 ルールで分析されます。 このルールは、C#11/ .NET 7 の以前のプレビューでコンパイルされたモジュールの一時的な軽減策であり、後で削除されます。
  • それ以外の場合、メソッド呼び出しは C#7.2 ルールで分析されます。

C#11 より以前のコンパイラは、RefSafetyRulesAttribute を無視し、C#7.2 ルールでのみメソッド呼び出しを分析します。

RefSafetyRulesAttribute は名前空間修飾名で照合されるため、定義を特定のアセンブリに含める必要はありません。

RefSafetyRulesAttribute 型はコンパイラでのみ使用され、ソースでは使用できません。 コンパイルにまだ含まれていない場合には、型宣言はコンパイラによって合成されます。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
    internal sealed class RefSafetyRulesAttribute : Attribute
    {
        public RefSafetyRulesAttribute(int version) { Version = version; }
        public readonly int Version;
    }
}

安全な固定サイズ バッファー

C# 11 では安全な固定サイズ バッファーが配信されませんでした。 この機能は、C# のこれからのバージョンで実装される可能性があります。

この言語では、固定サイズの配列に対する制限が緩和され、安全なコードで宣言でき、要素型をマネージド型またはアンマネージド型にすることができます。 これにより、次のような型が有効になります。

internal struct CharBuffer
{
    internal char Data[128];
}

これらの宣言は、unsafe カウンター パーツと同様に、包含型の N 要素のシーケンスを定義します。 これらのメンバーはインデクサーを使用してアクセスすることができ、Span<T> インスタンスと ReadOnlySpan<T> インスタンスに変換することもできます。

T 型の fixed バッファーにインデックスを作成する場合、コンテナーの readonly 状態を考慮する必要があります。 コンテナーが readonly である場合、インデクサーは ref readonly T を返します。それ以外の場合は、ref T を返します。

インデクサーなしで fixed バッファーにアクセスする場合には自然な型はありませんが、Span<T> 型に変換することができます。 コンテナーが readonly である場合、バッファーは暗黙的に ReadOnlySpan<T> に変換できます。それ以外の場合は、暗黙的に Span<T> または ReadOnlySpan<T> に変換できます (Span<T> への変換が最良であると考えられる)。

結果の Span<T> インスタンスの長さは、fixed バッファーで宣言されたサイズと同じになります。 戻り値の safe-context は、バッキング データがフィールドとしてアクセスされた場合と同様に、コンテナーの safe-context と同じになります。

要素型が fixed である型内の各 T 宣言について、対応する get のみのインデクサー メソッドが生成されます。このインデクサー メソッドの戻り型は ref T です。 インデクサーには、宣言する型のフィールドが実装によって返されるため、[UnscopedRef] 属性で注釈が付けられます。 メンバーのアクセシビリティは、fixed フィールドのアクセシビリティと一致します。

たとえば、CharBuffer.Data のインデクサーのシグネチャは次のようになります。

[UnscopedRef] internal ref char DataIndexer(int index) => ...;

指定されたインデックスが fixed 配列の宣言境界外にある場合は、IndexOutOfRangeException がスローされます。 定数値が指定されている場合は、適切な要素への直接参照に置き換えられます。 定数が宣言された範囲外でない限り、コンパイルのタイム エラーが発生します。

また、各 fixed バッファーに対して、値の get および set 操作を提供する名前付きアクセサーも生成されます。 つまり、fixed バッファーは、ref アクセサーと値渡しの get および set 操作を持つことによって、既存の配列セマンティクスにとてもよく類似しています。 コンパイラは、配列を消費する場合と同様に、fixed バッファーを消費するコードを生成する際に同じ柔軟性を持ちます。 これにより、await のような操作を fixed バッファーに対して生成しやすくなります。

さらに、fixed バッファーは他の言語からも使いやすくなるという利点があります。 名前付きインデクサーは、.NET の 1.0 リリース以降に存在する機能です。 名前付きインデクサーを直接出力できない言語でも、通常はそれらを使用できます (C# はこの例の実際の良い例です)。

バッファーのバッキング ストレージは、[InlineArray] 属性を使用して生成されます。 これは、問題 12320 で検討したメカニズムであり、同じ型のフィールドのシーケンスを効率的に宣言する場合に特に有効です。 この特定の問題はまだ検討中であり、この機能の実装は検討の結果によっておこなわれるものと予測されます。

new および with 式内の ref 値を持つ初期化子

セクション 12.8.17.3 オブジェクト初期化子では、文法を以下のように更新します。

initializer_value
    : 'ref' expression // added
    | expression
    | object_or_collection_initializer
    ;

with セクションにおける式の文法を次のように更新します。

member_initializer
    : identifier '=' 'ref' expression // added
    | identifier '=' expression
    ;

代入の左オペランドは、ref フィールドにバインドする式であることが必要です。
右オペランドは、左オペランドと同じ型の値を指定する lvalue を生成する式である必要があります。

ref ローカル再割り当てにも同様のルールを追加します。
左のオペランドが書き込み可能な ref (つまり、ref readonly フィールド以外のものを指定する) である場合、右のオペランドは書き込み可能な lvalue である必要があります。

コンストラクター呼び出しのエスケープ ルールは次のようなままになります。

コンストラクターを呼び出す new 式は、構築される型を返すと見なされるメソッド呼び出しと同じルールに従います。

つまり、上記で更新されたメソッド呼び出しのルールは次のとおりです。

メソッド呼び出し e1.M(e2, ...) から得られる rvalue は、以下のコンテキストのうち最小範囲の safe-context になります。

  1. caller-context
  2. safe-context は、すべての引数式で提供されます
  3. 戻り値が ref struct の場合、ref-safe-context にはすべての ref 引数が考慮されます

初期化子を伴う new 式の場合、初期化子式は引数として数えられ、セーフコンテキストを提供します。また、ref 初期化子式は ref 引数として数えられ、再帰的にref-セーフコンテキストを提供します。

unsafe コンテキストの変更

ポインター型 (セクション 23.3) は、マネージド型を参照型として使用できるように拡張されています。 このようなポインター型は、マネージド型に * トークンが続くものとして書き込まれます。 これによって警告を生成します。

address-of 演算子 (セクション 23.6.5) は、オペランドとしてマネージド型を持つ変数を受け入れるように緩和されています。

fixed ステートメント (セクション 23.7) は、マネージド型 の変数のアドレス、またはマネージド型 Tの要素を持つ array_type の式である T を受け入れように緩和されています。

スタック割り当て初期化子 (セクション 12.8.22) も同様に緩和されています。

考慮事項

この機能を評価する際に、開発スタックの他の部分で考慮すべき事項があります。

互換性に関する考慮事項

この提案の課題は、この設計が既存のスパン安全ルール、または §9.7.2に与える互換性への影響です。 一方でこれらのルールは、stackalloc 以外の API がスタックを参照する ref 状態をキャプチャすることを許可しない ref フィールドを持つ ref struct の概念を完全にサポートします。 ref セーフコンテキストルールには、形式 のコンストラクターが存在しないという ハード想定または §16.4.12.8 Span(ref T value) があります。 つまり、安全規則は、ref パラメーターが ref フィールドとしてエスケープする可能性を考慮していないため、その結果、次のようなコードが許されてしまいます。

Span<int> CreateSpanOfInt()
{
    // This is legal according to the 7.2 span rules because they do not account
    // for a constructor in the form Span(ref T value) existing. 
    int local = 42;
    return new Span<int>(ref local);
}

実質的に、ref パラメーターをメソッド呼び出しからエスケープするには、次の 3 つの方法があります。

  1. 値を返す
  2. ref を返す
  3. refref struct フィールドを使用する (ref / out パラメーターとして返すか渡す)

既存のルールは、(1) と (2) のみを考慮します。 (3) は考慮されていないため、ref フィールドとしてローカルを返すなどのギャップは考慮されていません。 この設計では、(3) を考慮するようにルールを変更する必要があります。 これにより、既存の API の互換性への影響が少なくなります。 具体的には、次のプロパティを持つ API に影響します。

  • 署名には ref struct が含まれている
    • ref struct が戻り値の型、ref、または out パラメーターである場合
    • 受信側を除く追加の in または ref パラメーターがある場合

このような API の C# 10 呼び出し元では、API への ref 状態入力を ref フィールドとしてキャプチャできると考える必要はありませんでした。 そのため、C# 10 では複数のパターンが安全に存在することができました。これは、ref 状態が ref フィールドとしてエスケープできるため、C# 11 では安全ではありません。 次に例を示します。

Span<int> CreateSpan(ref int parameter)
{
    // The implementation of this method is irrelevant when considering the lifetime of the 
    // returned Span<T>. The ref safe context rules only look at the method signature, not the 
    // implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
    // to escape by ref in this method
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 but would be illegal with ref fields
    return CreateSpan(ref parameter);

    // Legal in C# 10 but would be illegal with ref fields
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 but would be illegal with ref fields
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

この互換性の中断への影響は非常に少ないと予想されます。 影響を受けた API の形状は、ref フィールドがない場合はほとんど意味をなさないため、お客様がこれらを多く作成する可能性はほとんどありません。 既存のリポジトリに対してこのAPIの形状を検出するツールを用いた実験により、その断定が裏付けられます。 この図形の重要な数を持つ唯一のリポジトリは dotnet/runtime です。これは、そのリポジトリが ByReference<T> 組み込み型を介して ref フィールドを作成できるためです。

それでも、設計は一般的なパターンではなく、有効なパターンを表しているため、このような API を考慮する必要があります。 そのため、C# 10 にアップグレードするときに既存の有効期間ルールを復元するためのツールを開発者に提供する必要があります。 具体的には、開発者が ref または ref フィールドでエスケープできない ref パラメーターに注釈を付けるためのメカニズムを提供する必要があります。 これにより、同じ C# 10 呼び出しルールを持つ C# 11 の API を定義できます。

参照アセンブリ

この提案で説明されている機能を使用するコンパイルの参照アセンブリでは、ref safe context 情報を伝達する要素を保持する必要があります。 つまり、すべての有効期間注釈属性を元の位置に保持することが必要です。 置換または省略しようとすると、無効な参照アセンブリが発生することがあります。

ref フィールドの表現は、より微妙です。 他のフィールドと同様に、ref フィールドが参照アセンブリに表示されることが理想的です。 ただし、ref フィールドはメタデータ形式の変更を表し、このメタデータの変更を理解するために更新されないツール チェーンの問題を引き起こす可能性があります。 具体的な例は C++/CLI です。ここでは ref フィールドを使用するとエラーが発生する可能性があります。 そのため、コア ライブラリの参照アセンブリから ref フィールドを省略できる場合に便利です。

ref フィールド自体は、ref safe context ルールには影響しません。 具体的な例として、既存の Span<T> 定義を反転して ref フィールドを使用しても、使用には影響を及ぼさないことを考えてください。 そのため、ref 自体は安全に省略できます。 ただし、ref フィールドには、保持する必要がある使用に対するその他の影響があります。

  • ref フィールドを持つ ref struct は、unmanaged とは見なされません
  • ref フィールドの型は、無限ジェネリック拡張ルールに影響します。 したがって、ref フィールドの型に型パラメータが含まれている場合には、これを保持することが必要です

これらのルールを考えると、ref struct の有効な参照アセンブリ変換は次のとおりです。

// Impl assembly 
ref struct S<T>
{
    ref T _field;
}

// Ref assembly 
ref struct S<T>
{
    object _o; // force managed 
    T _f; // maintain generic expansion protections
}

注釈

有効期間は、型を使用することで最も自然に表現されます。 特定のプログラムの有効期間は、有効期間型の型のチェック時に安全です。 C# の構文は暗黙的に値に有効期間を追加しますが、ここでは基本的なルールを記述する基になる型システムがあります。 多くの場合、これらのルールの観点からデザインに対する変更の影響について説明する方が簡単であるため、検討を考慮してここに含めます。

これは 100% 完全なドキュメントではないことに注意してください。 ここでは、すべての動作を文書化することが目標ではありません。 むしろ、モデルとそれへの潜在的な変更について議論できるような一般的な理解と一般的な言葉遣いを確立することを目的としています。

通常、有効期間の種類について直接説明する必要はありません。 例外としては、有効期間が特定の「インスタンス化」サイトによって異なる可能性がある場所です。 これはポリモーフィズムの一種であり、これらのさまざまな有効期間をジェネリック パラメーターとして表される「ジェネリック有効期間」と呼ばれます。 C# には有効期間ジェネリックを表す構文が用意されていないため、明示的なジェネリック パラメーターを含む拡張された下位言語への C# からの暗黙的な「翻訳」を定義します。

次の例では、指定の有効期間を使用します。 構文 $a は、a という名前の有効期間を参照します。 それ自体は意味を持たない有効期間ですが、where $a : $b 構文を使用して他の有効期間との関係を与えることができます。 これにより、$a$b に変換できることが確立されます。 これは、$a の有効期間が少なくとも $bと同じくらい長いと考えることが役立つかもしれません。

利便性と簡潔さを考慮して、いくつかの定義済みの有効期間を以下に示します。

  • $heap: これは、ヒープに存在するすべての値の有効期間です。 すべてのコンテキストとメソッド シグネチャで使用できます。
  • $local: これは、メソッド スタックに存在するすべての値の有効期間です。 これは実質的に、function-member の名前のプレースホルダーです。 これはメソッドで暗黙的に定義され、出力位置を除くメソッド シグネチャに表示できます。
  • $ro: return only の名前のプレースホルダー
  • $cm: caller-context の名前のプレースホルダー

ライフタイム間には、いくつかの事前定義されたリレーションシップがあります。

  • where $heap : $a は、すべての有効期間の $a
  • where $cm : $ro
  • where $x : $local は、定義済みのすべての有効期間。 ユーザー定義の有効期間は、明示的に定義されていない限り、ローカルとの関係はありません。

型で定義されている有効期間変数は、不変または共変にすることができます。 これらは、ジェネリック パラメーターと同じ構文を使用して表されます。

// $this is covariant
// $a is invariant
ref struct S<out $this, $a> 

型定義の有効期間パラメーター $this は 定義済みではありませんが、定義時にいくつかのルールが関連付けられています。

  • これは、最初の有効期間パラメーターであることが必要です。
  • これは共変である必要があります: out $this.
  • ref フィールドの有効期間は、$this に変換できることが必要です
  • ref 以外のすべてのフィールドの $this 有効期間は、$heap または $this であることが必要です。

ref の有効期間は、ref に有効期間引数を指定することによって表されます。たとえば、ヒープを参照する ref は、ref<$heap> として表されます。

モデルでコンストラクターを定義するときに、メソッドに new という名前が使用されます。 戻り値とコンストラクター引数のパラメーター リストがあることが必要です。 これは、コンストラクターの入力と構築された値の間の関係を表すために必要となります。 モデルは、Span<$a><$ro> ではなく、代わりに Span<$a> new<$ro> を使用します。 コンストラクター内の this の型 (有効期間を含む) は、定義された戻り値になります。

有効期間の基本ルールは次のように定義されます。

  • すべての有効期間は、型引数の前に来るジェネリック引数として構文的に表現されます。 これは、$heap$local を除く定義済みの有効期間に当てはまります。
  • ref struct ではないすべての型 T は、暗黙的に T<$heap> の有効期間を持ちます。 これは暗黙的であり、すべてのサンプルに int<$heap> を記述する必要はありません。
  • ref<$l0> T<$l1, $l2, ... $ln> として定義された ref フィールドの場合:
    • すべての有効期間 $l1 から $ln は不変でなければなりません。
    • $l0 の有効期間は、$this に変換できることが必要です
  • ref<$a> T<$b, ...> として定義されている ref の場合、$b$a に変換できることが必要です
  • 変数の ref には、次の値で定義された有効期間があります。
    • ref ローカル、パラメーター、フィールド、または戻り値の形式が ref<$a> T の場合、有効期間は $a です。
    • $heap は、すべての参照型と、参照型のフィールド
    • $local は、その他すべて
  • 基になる型変換が有効な場合、代入または戻り値は有効です
  • 式の有効期間は、キャスト注釈を使用して明示化することができます。
    • (T<$a> expr) では、$a に対する値の有効期間は明示的に T<...> になります。
    • ref<$a> (T<$b>)expr の値の有効期間は $b に対して T<...> であり、ref の有効期間は $aです。

有効期間ルールでは、変換のために ref が式の型の一部と見なされます。 $a が共変で、T が不変である ref<$a, T<...>>ref<$a> T<...> を変換することによって論理的に表されます。

次に、C# 構文を基になるモデルにマップできるルールを定義しましょう。

簡潔にするために、明示的な有効期間パラメーターを持たない型は、out $this 定義されていて、その型のすべてのフィールドに適用されてかのように扱います。 ref フィールドを持つ型では、明示的な有効期間パラメーターを定義する必要があります。

これらのルールは、すべての型の scoped TT を割り当てることができる既存の不変性をサポートするために存在します。 これは、T<$a, ...> に変換可能であることがわかっているすべての有効期間にわたって T<$local, ...>$local に割り当て可能であることを意味します。 さらに、ヒープからスタック上の項目に Span<T> を割り当てることができるなど、他の項目もサポートされます。 ref 以外の値についてフィールドの有効期間が異なる型は除外されますが、それが今日の C# の現実です。 変更を行うには、マップする必要がある C# ルールを大幅に変更する必要があります。

インスタンス メソッド内の型 S<out $this, ...>this の型は、次のように暗黙的に定義されます。

  • 通常のインスタンス メソッドの場合: ref<$local> S<$cm, ...>
  • [UnscopedRef] で注釈が付けられたインスタンス メソッドの場合: ref<$ro> S<$cm, ...>

明示的な this パラメーターがない場合、ここでは暗黙的なルールが強制されます。 複雑なサンプルやディスカッションでは、static メソッドとして記述し、this を明示的なパラメーターにすることを検討してください。

ref struct S<out $this>
{
    // Implicit this can make discussion confusing 
    void M<$ro, $cm>(ref<$ro> S<$cm> s) {  }

    // Rewrite as explicit this to simplify discussion
    static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}

C# メソッドの構文は、次の方法でモデルにマップされます。

  • ref パラメーターには $ro の ref 有効期間があります
  • ref struct 型のパラメーターは、$cm の有効期間があります。
  • ref 戻り値の ref の有効期間は $ro
  • ref struct 型の戻り値の有効期間は $ro
  • パラメーターや scoped に対して ref を適用すると、ref の有効期間が $local に変更されます。

ここでモデルを示す簡単な例を見てみましょう。

ref int M1(ref int i) => ...

// Maps to the following. 

ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
    // okay: has ref lifetime $ro which is equal to $ro
    return ref i;

    // okay: has ref lifetime $heap which convertible $ro
    int[] array = new int[42];
    return ref array[0];

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

次に、ref struct を使用して同じ例を調べてみましょう。

ref struct S
{
    ref int Field;

    S(ref int f)
    {
        Field = ref f;
    }
}

S M2(ref int i, S span1, scoped S span2) => ...

// Maps to 

ref struct S<out $this>
{
    // Implicitly 
    ref<$this> int Field;

    S<$ro> new<$ro>(ref<$ro> int f)
    {
        Field = ref f;
    }
}

S<$ro> M2<$ro>(
    ref<$ro> int i,
    S<$ro> span1)
    S<$local> span2)
{
    // okay: types match exactly
    return span1;

    // error: has lifetime $local which has no conversion to $ro
    return span2;

    // okay: type S<$heap> has a conversion to S<$ro> because $heap has a
    // conversion to $ro and the first lifetime parameter of S<> is covariant
    return default(S<$heap>)

    // okay: the ref lifetime of ref $i is $ro so this is just an 
    // identity conversion
    S<$ro> local = new S<$ro>(ref $i);
    return local;

    int[] array = new int[42];
    // okay: S<$heap> is convertible to S<$ro>
    return new S<$heap>(ref<$heap> array[0]);

    // okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These 
    // are convertible.
    return new S<$ro>(ref<$heap> array[0]);

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

次に、これが循環自己割り当ての問題にどのように役立つのかを見てみましょう。

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        s.refField = ref s.field;
    }
}

// Maps to 

ref struct S<out $this>
{
    int field;
    ref<$this> int refField;

    static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
    {
        // error: the types work out here to ref<$cm> int = ref<$ro> int and that is 
        // illegal as $ro has no conversion to $cm (the relationship is the other direction)
        s.refField = ref<$ro> s.field;
    }
}

次に、これが愚かなキャプチャ パラメーターの問題にどのように役立つのかを見てみましょう。

ref struct S
{
    ref int refField;

    void Use(ref int parameter)
    {
        // error: this needs to be an error else every call to this.Use(ref local) would fail 
        // because compiler would assume the `ref` was captured by ref.
        this.refField = ref parameter;
    }
}

// Maps to 

ref struct S<out $this>
{
    ref<$this> int refField;
    
    // Using static form of this method signature so the type of this is explicit. 
    static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
    {
        // error: the types here are:
        //  - refField is ref<$cm> int
        //  - ref parameter is ref<$ro> int
        // That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and 
        // hence this reassignment is illegal
        @this.refField = ref<$ro> parameter;
    }
}

オープン中の問題

互換性の問題を回避するために設計を変更する

この設計では、既存の ref-safe-context ルールとの互換性の中断がいくつか提案されています。 中断は影響を最小限に抑えると考えられていますが、破壊的変更のない設計に対する重要な考慮事項となっていました。

しかし、互換性を維持する設計は、このものよりもかなり複雑でした。 互換性を保つためには、refフィールドがrefフィールドで戻るための有効期間と、refフィールドで戻るための有効期間を個別に設定する必要があります。 基本的に、メソッドのすべてのパラメーターに対して ref-field-safe-context 追跡を提供する必要があります。 すべての式に対してこれを算定し、現行で ref-safe-context が追跡されているほぼすべての場所ですべての値を追跡する必要があります。

さらに、この値はref-safe-context と関係があります。 たとえば、値を ref フィールドとして返すことができますが、ref として直接返さないのは賢明ではありません。 これは、ref フィールドがref によって既に簡単に返される可能性があるためです (refref struct 状態は、その価値が格納されていない場合でも ref によって返されることができます)。 したがって、これらの値が互いに適切であることを保証するために、ルールにはさらに一定の調整が必要です。

また、ref フィールド、ref、値による 3 つの方法で返すことができる ref パラメーターを表す構文が言語に必要であることを意味します。 既定値は、ref によって返されます。 今後は、特に ref struct が関与している場合、より自然な復帰が ref フィールドまたは ref によって行われることが予想されます。 つまり、新しい API では、既定で正しい構文注釈が追加されていることが必要です。 これは望ましくありません。

ただし、これらの互換性の変更は、次のプロパティを持つメソッドに影響します。

  • Span<T> または ref struct を用意してください
    • ref struct が戻り値の型、ref、または out パラメーターである場合
    • 受信側を除く追加の in または ref パラメーターがある場合

影響を理解するには、API をカテゴリに分割すると便利です。

  1. コンシューマーで refref フィールドとしてキャプチャされることを考慮する。 主な例は Span(ref T value) コンストラクターです
  2. コンシューマーで refref フィールドとしてキャプチャされることを考慮しない。 これらは 2 つのカテゴリに分かれています
    1. 安全でない API。 これらは、Unsafe 型と MemoryMarshal 型内の API であり、MemoryMarshal.CreateSpan が最も顕著です。 これらの API は ref を安全ではなくキャプチャし、安全でない API とも呼ばれます。
    2. 安全な API。 これらは、効率性のために ref パラメーターを受け取る API ですが、実際にはどこにもキャプチャされません。 例は小さいですが、一例は AsnDecoder.ReadEnumeratedBytes です。

この変更は主に上記の (1) に利益をもたらします。 これらは、今後 ref を取得し、ref struct を返す API の大部分を構成することが期待されます。 有効期間ルールが変更することにより、既存の呼び出し元セマンティクスが中断されるため、変更は (2.1) と (2.2) に悪影響を及ぼします。

カテゴリ (2.1) の API は、主に Microsoft または ref フィールドから最も利益を得る傾向がある開発者(「Tannerのような人々」)によって作成されています。 このクラスの開発者は、代わりに ref フィールドが提供される場合、既存のセマンティクスを保持するために、いくつかの注釈の形で C# 11 へのアップグレード時に互換性コストが発生することを予想できます。

カテゴリ (2.2) の API が最大の問題です。 このような API がいくつ存在するかは不明であり、サード パーティのコードで頻度が多いか少ないかは不明です。 これらの数は非常に少ないと予想されます (特に out で互換性を譲歩する場合)。 これまでの検索では、public の領域にこれらの存在がとても少ないことが明らかになっています。 セマンティック分析が必要なので、検索をパターン化するには困難が伴います。 この変更を行う前に、少数の既知のケースに影響を与えるこの問題に関する前提条件を確認するために、ツール ベースのアプローチが必要になります。

カテゴリ (2) のどちらの場合も、修正は簡単です。 キャプチャ可能と見なしたくない ref パラメーターは、refscoped を追加する必要があります。 (2.1) では、開発者は Unsafe または MemoryMarshal を使用するように強制される可能性がありますが、これは安全でないスタイルの API に必要です。

API が面倒な動作に陥ったときに警告を発行することで、言語がサイレント破壊的変更の影響を軽減できることが理想的です。 これは両方とも ref を受け取り、ref struct を返しますが、実際には ref structref をキャプチャしないメソッドです。 コンパイラは、この場合、このような ref に代わりに scoped ref として注釈を付ける必要があることを開発者に通知する診断を発行することができます。

決定 この設計は実現可能ですが、その機能の使用が非常に困難であるため、互換性を破棄する決定が下されました。

決定 メソッドは条件を満たしているが、ref パラメーターを ref フィールドとしてキャプチャしない場合、コンパイラは警告を発行します。 これにより、お客様が作成している潜在的な問題について、アップグレード時に適切に警告します。

キーワードと属性

この設計では、属性を使用して新しい有効期間ルールに注釈を付ける必要があります。 これはまた、コンテキスト キーワードでも同様に簡単に実行できていました。 たとえば、[DoesNotEscape]scoped にマップできます。 しかしながら、キーワードはたとえコンテキストに依存するものであっても、一般に、包含するために非常に高い基準を満たす必要があります。 彼らは貴重な言語資源を占め、言語の中でより目立つ存在です。 この機能は価値あるものの、少数の C# 開発者に役立つものです。

表面的にはキーワードを使用しない長点があるように見えますが、考慮すべき重要な点が 2 つあります。

  1. 注釈はプログラムセマンティクスに影響します。 属性がプログラムセマンティクスに影響を与えるのは、C# がクロスに消極的であり、これがそのステップを実行する言語を正当化する必要がある機能かどうかは不明です。
  2. この機能を使用する可能性が最も高い開発者は、関数ポインターを使用する一連の開発者と強く対立します。 この機能は少数派の開発者にも使用されていますが、新しい構文が必要であり、その決定は依然として支持されています。

これは、構文を考慮する必要があることを意味します。

構文の大まかなスケッチは次のようになります。

  • [RefDoesNotEscape]scoped ref にマップする
  • [DoesNotEscape]scoped にマップする
  • [RefDoesEscape]unscoped にマップする

決定 scopedscoped ref の構文を使用し、unscoped に属性を使用します。

固定バッファー ローカルを許可する

この設計により、任意の型をサポートできる安全な fixed バッファーが可能になります。 ここで考えられる拡張機能の 1 つは、このような fixed バッファーをローカル変数として宣言できるようにすることです。 これにより、既存の stackalloc 操作の数を fixed バッファーに置き換えることができます。 また、stackalloc はアンマネージド要素型に制限されますが、fixed バッファーはそうではないため、スタック スタイルの割り当てを行うことができるシナリオのセットも拡張されます。

class FixedBufferLocals
{
    void Example()
    {
        Span<int> span = stackalloc int[42];
        int buffer[42];
    }
}

これはまとめて保持されますが、ローカルの構文を少し拡張する必要があります。 さらなる複雑化への価値があるかは不明です。 可能であれば、現時点では何も決定せずに、十分なニーズが表れた場合に後から戻すことができます。

これが有益な場合の例: https://github.com/dotnet/runtime/pull/34149

決定 現時点ではこれを保留します

modreqs を使用するかどうかについて

新しい有効期間属性でマークされたメソッドを、出力で modreq に変換するかどうかを決定する必要があります。 この方法を使用した場合、注釈と modreq の間には実質的に 1 対 1 のマッピングが存在します。

modreq を追加する理由は、属性によって ref safe コンテキスト ルールのセマンティクスが変更されるというものです。 これらのセマンティクスを理解している言語のみが、問題のメソッドを呼び出す必要があります。 さらに、OHI シナリオに適用すると、有効期間はすべての派生メソッドが実装する必要があるコントラクトになります。 modreq なしで注釈が存在すると、有効期間の注釈が競合する virtual メソッド チェーンが読み込まれる場合があります (virtual チェーンの一部のみがコンパイルされ、もう一方がコンパイルされていない場合に発生する可能性)。

最初の ref safe コンテキストの作業では、modreq は使用されず、代わりに理解する言語とフレームワークに依存していました。 同時に、ref safe context ルールに寄与するすべての要素は、メソッドシグネチャの強力な部分です :refinref struct などです。そのため、メソッドの既存の規則を変更すると、既にシグネチャがバイナリに変更されます。 新しい有効期間注釈に同様の影響を与えるためには、modreq の施行が必要です。

懸念事項は、これが過剰であるかどうかです。 パラメーターに [DoesNotEscape] を追加するとバイナリ互換性の変更が発生し、シグネチャの柔軟性が高まるという悪影響があります。 それによるトレードオフは、時間が経つにつれて、BCL などのフレームワークではそのような署名を緩めることができなくなる可能性があることを意味します。 これは、言語が in パラメーターで行うアプローチを取ることである程度軽減でき、仮想位置にのみ modreq を適用できます。

決定 メタデータで modreq を使用しないこと。 outref の違いは modreq ではありませんが、ref safe context 値が異なっています。 ここで modreq ルールを半分だけ適用しても実際のメリットはありません。

多次元固定バッファーを許可する

fixed バッファーの設計を拡張して、多次元スタイルの配列を含める必要があるでしょうか? 基本的に、次のような宣言を許可します。

struct Dimensions
{
    int array[42, 13];
}

決定 現時点では許可しない

scoped への違反

ランタイムリポジトリには、非公開のAPIがいくつかあり、ref パラメーターを ref フィールドとしてキャプチャします。 結果の値の有効期間が追跡されないため、これらは安全ではありません。 Span<T>(ref T value, int length) コンストラクターはその例です。

これらの API の大部分は、C# 11 に更新するだけで実現されるリターンで適切な有効期間の追跡を行う可能性があります。 ただし、意図全体が安全ではないため、戻り値を追跡しないという現在のセマンティクスを維持したいと考える人も少なくないでしょう。 最も注目すべき例は、MemoryMarshal.CreateSpanMemoryMarshal.CreateReadOnlySpan です。 これは、パラメーターを scoped としてマークすることで実現します。

つまり、ランタイムには、パラメーターから安全でない scoped を削除するための確立されたパターンが必要です。

  1. Unsafe.AsRef<T>(in T value)scoped in T value に変更することで、既存の目的を拡張できます。 これにより、パラメーターから inscoped の両方を削除できます。 これにより、普遍的な "ref safety の削除" メソッドになります。
  2. scoped を削除することを目的とする新しい方法を紹介します: ref T Unsafe.AsUnscoped<T>(scoped in T value)。 これにより、in も削除されます。そうしないと、呼び出し元はメソッド呼び出しを組み合わせて「ref safety を削除」する必要があり、その時点で既存のソリューションで十分である可能性が高いためです。

デフォルトでスコープをなしにしますか?

デザインには、既定で scoped である 2 つの場所しかありません。

  • thisscoped ref
  • outscoped ref

out に関する決定は、ref フィールドの互換性の負担を大幅に軽減することであり、同時により自然な既定値にすることです。 これにより、開発者は実際に out を、ref にようにデータが外部に流れるものとして考えます。そして、ルールでは双方向に流れるデータを考慮する必要があります。 これにより、開発者への多大な混乱が生じます。

this に関する決定は、structref でフィールドを返すことができないため望ましくありません。 これは高パフォーマンスの開発者にとって重要なシナリオであり、[UnscopedRef] 属性は基本的にこの 1 つのシナリオに対して追加されています。

キーワードには高度な制約があり、1 つのシナリオに対してキーワードを追加することは疑わしいものです。 このキーワードを回避するために、this をデフォルトで ref にして、scoped refにはしないという選択肢を検討しました。 thisscoped ref にする必要があるメンバーは、メソッドを scoped としてマークすることによってそうすることができます(今日、メソッドを readonly としてマークして readonly ref を作成することができるように)。

通常の struct では、これは主に肯定的な変化であり、互換性の問題が生じるのはメンバーが ref から戻ってきた時だけです。 このようなメソッドは非常に少なく、ツールを使用すれば、これらを検出してすぐに scoped メンバーに変換できます。

ref struct では、この変更により、大幅に大きな互換性の問題が発生します。 以下、具体例に沿って説明します。

ref struct Sneaky
{
    int Field;
    ref int RefField;

    public void SelfAssign()
    {
        // This pattern of ref reassign to fields on this inside instance methods would now
        // completely legal.
        RefField = ref Field;
    }

    static Sneaky UseExample()
    {
        Sneaky local = default;

        // Error: this is illegal, and must be illegal, by our existing rules as the 
        // ref-safe-context of local is now an input into method arguments must match. 
        local.SelfAssign();

        // This would be dangerous as local now has a dangerous `ref` but the above 
        // prevents us from getting here.
        return local;
    }
}

基本的には、ローカルが scoped としてさらにマークされていない限り、変更可能なref structローカルにおけるすべてのインスタンス メソッドの呼び出しは無効になります。 ルールでは、フィールドが this の他のフィールドに再割り当てされた場合を考慮する必要があります。 readonly の性質により再割り当てが禁止されるため、readonly ref struct にはこの問題はありません。 それでも、これは実質的にすべての既存の可変 ref struct に影響するため、重要な下位互換性を損なう変更になります。

ただし、readonly ref struct 構造体に ref フィールドを追加すると、ref struct には依然として問題が残ります。 キャプチャを ref フィールドの値に移動するだけで、同じ基本的な問題が発生します。

readonly ref struct ReadOnlySneaky
{
    readonly int Field;
    readonly ref ReadOnlySpan<int> Span;

    public void SelfAssign()
    {
        // Instance method captures a ref to itself
        Span = new ReadOnlySpan<int>(ref Field, 1);
    }
}

struct またはメンバーの種類に基づいて、this の既定値が異なるという考えもありました。 次に例を示します。

  • ref としての this: structreadonly ref struct、またはreadonly member
  • this としての scoped ref: ref struct または readonly ref struct (ref フィールドを ref struct に)

これにより、互換性の中断が最小限に抑えられ、柔軟性が最大限に高まりますが、お客様側の事情が複雑化する負担が生じます。 将来の機能としての安全な fixed バッファーのような場合、変更可能な ref struct はこの設計だけでは機能しないフィールドに対して ref を戻す必要がありますが、それはscoped refカテゴリに分類されるため、この問題は完全には解決されません。

決定 thisscoped ref として維持します。 つまり、上記のずるい例ではコンパイラ エラーが発生します。

ref フィールドから ref struct へ

この機能により、ref フィールドが ref struct を参照できるため、ref safe context ルールの新しいセットが開拓されます。 この一般的な ByReference<T> の性質は、これまでランタイムがそのようなコンストラクトを持つことができなかったことを意味しました。 その結果、すべてのルールは、これが不可能であると仮定して書かれています。 ref フィールド機能は、主に新しいルールを作成するのではなく、システム内の既存のルールを体系化します。 ref フィールドの ref struct を許可するには、いくつかの新しいシナリオを考慮する必要があるため、新しいルールを体系化する必要があります。

1 つ目は、readonly refref 状態を格納できるようになったということです。 次に例を示します。

readonly ref struct Container
{
    readonly ref Span<int> Span;

    void Store(Span<int> span)
    {
        Span = span;
    }
}

つまり、メソッド引数一致ルールを考慮すると、Tref structref フィールドを持つ可能性がある場合に、readonly ref T が潜在的なメソッド出力であると考える必要があります。

2つ目の問題は、新しい種類の安全なコンテキストを言語で考慮しなければならないことです: ref-field-safe-contextref フィールドが推移的に含まれるすべての ref struct には、ref フィールド内の値を表す別のエスケープ スコープがあります。 複数の ref フィールドの場合は、1 つの値としてまとめて追跡できます。 パラメーターの既定値は、caller-context です。

ref struct Nested
{
    ref Span<int> Span;
}

Span<int> M(ref Nested nested) => nested.Span;

この値は、コンテナの safe-context とは関係ありません。つまり、コンテナ コンテキストが小さくなるため、ref フィールド値の ref-field-safe-context には影響しません。 さらに、ref-field-safe-context がコンテナーの セーフコンテキスト より小さくなることは決してありません。

ref struct Nested
{
    ref Span<int> Span;
}

void M(ref Nested nested)
{
    scoped ref Nested refLocal = ref nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is illegal
    refLocal.Span = stackalloc int[42];

    scoped Nested valLocal = nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is still illegal
    valLocal.Span = stackalloc int[42];
}

この ref-field-safe-context は基本的に常に存在しています。 これまで、ref フィールドは通常の struct のみを指すことができたため、caller-context に簡単に縮小されていました。 既存のルールを更新して、ref フィールドから ref struct までをサポートし、この新しい ref-safe-contextを考慮に入れる必要があります。

3 番目に、値の ref-field-context に違反しないように、ref の再割り当てのルールを更新する必要があります。 基本的に、e1 の型が ref struct である x.e1 = ref e2 では、ref-field-safe-context が等しいことが必要です。

これらの問題はとても解決可能です。 コンパイラ チームは、これらのルールのいくつかのバージョンをスケッチしており、その多くは既存の分析から除外されています。 問題は、正確性と使いやすさを証明するのに役立つそのようなルールのコードが使用されていないことです。 これにより、間違った既定値を選択し、これを利用するときにランタイムを使いやすさ枠に戻す恐れがあるため、サポートを追加することがとても躊躇されます。 .NET 8 は、allow T: ref structSpan<Span<T>> を使用してこの方向にプッシュする可能性があるため、この懸念は特に高まります。 ルールは、従量課金コードと組み合わせて行う場合に、より適切に記述されます。

Decisionref フィールドを ref struct できるようにすることを、.NET 8 においてこれらのシナリオに関するルールを推進するためのシナリオが整うまで遅らせます。 これは .NET 9 の時点では実装されていません

C# 11.0 に何が含まれるか?

このドキュメントで説明する機能を 1 回のパスで実装する必要はありません。 代わりに、次のバケット内の複数の言語リリースにわたって段階的に実装できます。

  1. ref フィールドと scoped
  2. [UnscopedRef]
  3. ref フィールドを ref struct にする
  4. 制限付きの型の使用停止
  5. 固定サイズ バッファー

どのリリースで何が実装されるかは、範囲設定の問題ということになります。

決定 C# 11.0 には (1) および (2) のみが適用されました。 残りの部分は、これからのバージョンの C# で検討されます。

将来の注意事項

高度な有効期間注釈

この提案のライフタイム注釈は、開発者がデフォルトのエスケープまたは非エスケープの動作を値に対して変更できるという点で制限されています。 これにより、モデルに強力な柔軟性が追加されますが、表現できるリレーションシップのセットは根本的に変更されません。 コアでは、C# モデルはまだ実質的にバイナリです。値を返すことができるでしょうか?

これにより、限られた有効期間の関係を理解できます。 たとえば、メソッドから返されない値の有効期間は、メソッドから返すことができる値よりも短くなります。 ただし、メソッドから返すことができる値間の有効期間の関係を記述する方法はありません。 具体的には、1 つの値の有効期間が他の値よりも長いと言うことはありません。確立されると、両方をメソッドから返すことができます。 有効期間の進化への次のステップとしては、そのような関係を記述できるようにすることです。

Rust などの他のメソッドでは、この種のリレーションシップを表現できるため、より複雑な scoped スタイル操作を実装できます。 このような機能が含まれている場合、言語も同様にメリットがあります。 現時点では、これを行うための意欲的な原動力はありませんが、将来的には、scoped モデルを拡張することで、かなり簡単に含めることができます。

構文にジェネリック スタイル引数を追加することで、すべての scoped に名前付き有効期間を割り当てることができます。 たとえば、scoped<'a> は有効期間 'a を持つ値です。 その後、where などの制約を使用して、これらの有効期間間の関係を記述できます。

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
  where 'b >= 'a
{
    s.Span = span;
}

このメソッドは、'a'b の 2 つの有効期間とその関係を定義します。具体的には、'b'a より大きくなります。 これにより、呼び出し側は、現在存在するより粗い規則に比べて、メソッドに値を安全に渡すためのより詳細な規則を持つことができます。

問題

次の問題はすべて、この提案に関連しています。

Proposals

次の提案は、この提案に関連しています。

既存のサンプル

Utf8JsonReader

この特定のスニペットでは、ref struct のインスタンス メソッドにスタック割り当て可能な Span<T> を渡す際に問題が発生するため、安全ではありません。 このパラメーターがキャプチャされていない場合でも、言語はそれを想定する必要があるため、ここでは不必要に摩擦を引き起こします。

Utf8JsonWriter

このスニペットでは、データの要素をエスケープしてパラメーターを変更する必要があります。 エスケープされたデータは、効率のためにスタック割り当て可能です。 パラメーターがエスケープされていない場合でも、コンパイラはそれがパラメーターであることから、包絡メソッドの外側にあるセーフコンテキスト を割り当てます。 つまり、スタック割り当てを使用するには、この実装ではデータをエスケープした後にパラメーターに再度割り当てるために unsafe を使用する必要があります。

楽しいサンプル

ReadOnlySpan<T>

public readonly ref struct ReadOnlySpan<T>
{
    readonly ref readonly T _value;
    readonly int _length;

    public ReadOnlySpan(in T value)
    {
        _value = ref value;
        _length = 1;
    }
}

倹約リスト

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public FrugalList(){}

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

例と注意事項

ルールが動作する方法と理由を示す一連の例を次に示します。 危険な動作を示すいくつかの例と、ルールによってそれらの動作が妨げる方法が含まれています。 提案を調整するときは、これらの点に留意することが重要です。

ref の再割り当てと呼び出しサイト

ref 再割り当て メソッド呼び出しがどのように および と協力して動作するかを示します。

ref struct RS
{
    ref int _refField;

    public ref int Prop => ref _refField;

    public RS(int[] array)
    {
        _refField = ref array[0];
    }

    public RS(ref int i)
    {
        _refField = ref i;
    }

    public RS CreateRS() => ...;

    public ref int M1(RS rs)
    {
        // The call site arguments for Prop contribute here:
        //   - `rs` contributes no ref-safe-context as the corresponding parameter, 
        //      which is `this`, is `scoped ref`
        //   - `rs` contribute safe-context of *caller-context*
        // 
        // This is an lvalue invocation and the arguments contribute only safe-context 
        // values of *caller-context*. That means `local1` has ref-safe-context of 
        // *caller-context*
        ref int local1 = ref rs.Prop;

        // Okay: this is legal because `local` has ref-safe-context of *caller-context*
        return ref local1;

        // The arguments contribute here:
        //   - `this` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `this` contributes safe-context of *caller-context*
        //
        // This is an rvalue invocation and following those rules the safe-context of 
        // `local2` will be *caller-context*
        RS local2 = CreateRS();

        // Okay: this follows the same analysis as `ref rs.Prop` above
        return ref local2.Prop;

        // The arguments contribute here:
        //   - `local3` contributes ref-safe-context of *function-member*
        //   - `local3` contributes safe-context of *caller-context*
        // 
        // This is an rvalue invocation which returns a `ref struct` and following those 
        // rules the safe-context of `local4` will be *function-member*
        int local3 = 42;
        var local4 = new RS(ref local3);

        // Error: 
        // The arguments contribute here:
        //   - `local4` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `local4` contributes safe-context of *function-member*
        // 
        // This is an lvalue invocation and following those rules the ref-safe-context 
        // of the return is *function-member*
        return ref local4.Prop;
    }
}

ref の再割り当てと安全でないエスケープ

参照再割り当てルールの以下の行の理由は、一見すると明らかでない場合があります。

e1には、e2 と同じ safe-context が必要です

これは、ref の場所によって指される値の有効期間が不変であるためです。 間接処理により、たとえ有効期間が短い場合でも、ここではいかなる種類のバリエーションも許容できなくなります。 縮小が許可されている場合は、次の安全でないコードが開きます。

void Example(ref Span<int> p)
{
    Span<int> local = stackalloc int[42];
    ref Span<int> refLocal = ref local;

    // Error:
    // The safe-context of refLocal is narrower than p. For a non-ref reassignment 
    // this would be allowed as its safe to assign wider lifetimes to narrower ones.
    // In the case of ref reassignment though this rule prevents it as the 
    // safe-context values are different.
    refLocal = ref p;

    // If it were allowed this would be legal as the safe-context of refLocal
    // is *caller-context* and that is satisfied by stackalloc. At the same time
    // it would be assigning through p and escaping the stackalloc to the calling
    // method
    // 
    // This is equivalent of saying p = stackalloc int[13]!!! 
    refLocal = stackalloc int[13];
}

ref struct 以外の ref の場合、値はすべて同じ safe-context を持つため、このルールは簡単に満たされます。 このルールは、値が ref struct の場合にのみ有効になります。

ref のこの動作は、ref フィールドを ref struct としてこれから許可するためにも重要です。

scoped ローカル

ローカルで scoped を使用することは、さまざまな safe-context を持つ値を条件付きでローカルに割り当てるコード パターンに特に役立ちます。 これは、コードがローカルの safe-context を定義するために = stackalloc byte[0] のような初期化トリックに頼る必要がなくなり、scoped を使用するだけになったことを意味します。

// Old way 
// Span<byte> span = stackalloc byte[0];
// New way 
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
    span = stackalloc byte[len];
}
else
{
    span = new byte[len];
}

このパターンは、低レベルのコードで頻繁に発生します。 関係する ref structSpan<T> である場合は、上記のトリックを使用することができます。 ただし、他の ref struct 型には適用されず、有効期間を適切に指定できないことを回避するために unsafe に頼る必要がある低レベルのコードが発生する可能性があります。

scoped パラメーター値

低レベルのコードで繰り返される摩擦の原因の 1 つは、パラメーターの既定のエスケープが許容的であることです。 これらは caller-context に対する safe-context です。 これは、.NET 全体のコーディング パターンに対応しているため、適切な既定値です。 低レベルのコードでは、ref struct を多く使用することになりますが、この既定値は ref セーフ コンテキストルールの他の部分との摩擦を引き起こす可能性があります。

最大の摩擦点は、メソッド引数が一致する必要があるというルールによって発生します。 このルールは、最も一般的に、少なくとも 1 つのパラメーターが ref struct でもある ref struct のインスタンス メソッドで使用されます。 これは、ref struct 型がメソッドで Span<T> パラメーターを一般的に活用する低レベル コードの一般的なパターンです。 たとえば、バッファーを渡すために ref struct を使用するすべてのライター スタイルの Span<T> で発生します。

このルールは、次のようなシナリオを防ぐために存在します。

ref struct RS
{
    Span<int> _field;
    void Set(Span<int> p)
    {
        _field = p;
    }

    static void DangerousCode(ref RS p)
    {
        Span<int> span = stackalloc int[] { 42 };

        // Error: if allowed this would let the method return a reference to 
        // the stack
        p.Set(span);
    }
}

基本的に、このルールは、メソッドへのすべての入力が許容される最大の safe-contexにエスケープすることを前提としなければならないためです。 受信側を含む ref パラメーターまたは out パラメーターがある場合、入力がそれらの ref 値のフィールドとしてエスケープされる可能性があります (上記の RS.Set のように)。

実際には、ref struct を出力にキャプチャしないパラメーターとして渡すメソッドは多くあります。 これは、現在のメソッド内で使用される値にすぎません。 次に例を示します。

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Error: The safe-context of `span` is function-member 
        // while `reader` is outside function-member hence this fails
        // by the above rule.
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

この低レベルのコードを回避するには、ref struct の有効期間についてコンパイラにうそをつく unsafe トリックに頼ります。 これにより、ref struct の価値提案が大幅に削減されます。これは、高パフォーマンスコードを記述し続けながら unsafe を回避するための手段であるためです。

ここで、scopedref struct パラメーターに対して効果的なツールとなります。これは、更新されたメソッド引数が一致する必要があるルールに従って、メソッドから返されるパラメーターが考慮から除外されるためです。 呼び出しサイトの柔軟性を高めるため、使用されるが返されない ref struct パラメーターには、scoped のラベルを付けることができます。

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(scoped ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Okay: the compiler never considers `span` as capturable here hence it doesn't
        // contribute to the method arguments must match rule
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

読み取り専用の変更による不明瞭な ref 割り当ての防止

ref がコンストラクターまたは readonly メンバーの init フィールドに取られるとき、その型は ref であり、ref readonlyではありません。 これは、次のようなコードを可能にする長期的な動作です。

struct S
{
    readonly int i; 

    public S(string s)
    {
        M(ref i);
    }

    static void M(ref int i) { }
}

しかし、そのような ref を同じ型の ref フィールドに格納できた場合、それは潜在的な問題を引き起こします。 これにより、インスタンス メンバーからの readonly struct を直接変更できます。

readonly ref struct S
{ 
    readonly int i; 
    readonly ref int r; 
    public S()
    {
        i = 0;
        // Error: `i` has a narrower scope than `r`
        r = ref i;
    }

    public void Oops()
    {
        r++;
    }
}

ただし、これは ref safe context ルールに違反するため、提案により回避されます。 以下、具体例に沿って説明します。

  • ref-safe-context関数メンバー であり、セーフコンテキスト は 呼び出し元コンテキストです。 this メンバーにおける struct の標準はどちらもこれらです。
  • リファレンス安全コンテキスト は、の関数メンバー です。 これは、フィールドの有効期間ルールから除外されます。 具体的にはルール 4 です。

その時点で、行 r = ref i は、ref 再割り当てルール違反です。

これらのルールは、この動作を防ぐことを意図したものではありませんが、副作用としてこのように作用します。 このようなシナリオへの影響を評価するために、今後のルールの更新に関しては、この点に留意することが重要です。

愚かな循環割り当て

この設計で苦労した側面の 1 つは、メソッドから ref をどれだけ自由に返すことができるかです。 すべての ref を通常の値のように自由に返せるようにすることは、ほとんどの開発者が直感的に期待していることでしょう。 ただし、ref 安全性を計算するときにコンパイラが考慮する必要がある病理学的なシナリオが可能になります。 以下、具体例に沿って説明します。

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        // Error: s.field can only escape the current method through a return statement
        s.refField = ref s.field;
    }
}

これは、開発者が使用することを期待するコード パターンではありません。 ただし、値と同じ有効期間で ref を返すことができる場合は、ルールに基づいて有効です。 コンパイラはメソッド呼び出しを評価するときにすべての訴訟を考慮する必要があり、これにより、このような API が効果的に使用できなくなります。

void M(ref S s)
{
    ...
}

void Usage()
{
    // safe-context to caller-context
    S local = default; 

    // Error: compiler is forced to assume the worst and concludes a self assignment
    // is possible here and must issue an error.
    M(ref local);
}

これらの API を使用できるようにするために、コンパイラは、ref パラメーターの ref 有効期間が関連付けられているパラメーター値内の参照の有効期間よりも小さいことを保証します。 これが、ref struct に対する refref-safe-contextreturn-only にし、outcaller-context にする理由です。 これにより、有効期間の違いにより、循環割り当てが防止されます。

[UnscopedRef] は、ref struct の値に対する refref-safe-contextcaller-context昇格させるため、循環的な割り当てが可能になり、呼び出しチェーンの上位で [UnscopedRef] のバイラル使用が強制されることに注意してください。

S F()
{
    S local = new();
    // Error: self assignment possible inside `S.M`.
    S.M(ref local);
    return local;
}

ref struct S
{
    int field;
    ref int refField;

    public static void M([UnscopedRef] ref S s)
    {
        // Allowed: s has both safe-context and ref-safe-context of caller-context
        s.refField = ref s.field;
    }
}

同様に、[UnscopedRef] out では、パラメーターに safe-contextreturn-onlyref-safe-context の両方があるため、循環割り当てが許可されます。

[UnscopedRef] refcaller-context に昇格することは、型が ref struct場合に便利です (ref 構造体と非 ref 構造体が区別されないようにルールを単純に保つ必要があることに注意してください)。

int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2

static S F([UnscopedRef] ref int x)
{
    S local = new();
    local.M(ref x);
    return local;
}

ref struct S
{
    public ref int RefField;

    public void M([UnscopedRef] ref int data)
    {
        RefField = ref data;
    }
}

高度な注釈の観点から、[UnscopedRef] 設計では次のものが作成されます。

ref struct S { }

// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)

// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
  where 'b >= 'a

readonly の深度は ref フィールド以下に到達しない

次のコード サンプルについて考えてみましょう。

ref struct S
{
    ref int Field;

    readonly void Method()
    {
        // Legal or illegal?
        Field = 42;
    }
}

バキュームの readonly インスタンスの ref フィールドのルールを設計する場合、上記が準拠または違反になるようにルールを有効に設計できます。 基本的に readonly は、有効に ref フィールドを介して深くすることも、ref にのみ適用することもできます。 ref にのみ適用すると、参照の再割り当てができなくなりますが、参照先の値を変更する通常の割り当ては許可されます。

この設計は孤立して存在するわけではなく、既に効果的にrefフィールドを持っている型に対するルールを設計しています。 最も顕著なものとして、Span<T>は、ここで readonly が深くないことに既に強く依存しています。 その主なシナリオは、readonly インスタンスを介して ref フィールドに割り当てる機能です。

readonly ref struct SpanOfOne
{
    readonly ref int Field;

    public ref int this[int index]
    {
        get
        {
            if (index != 1)
                throw new Exception();
            return ref Field;
        }
    }
}

つまり、readonly の浅い解釈を選択する必要があります。

モデリング コンストラクター

設計上の小さな疑問点として考慮する点は、コンストラクター本体を ef safety のためにどのようにモデル化するかです。 基本的に、次のコンストラクターはどのように分析されるのでしょうか?

ref struct S
{
    ref int field;

    public S(ref int f)
    {
        field = ref f;
    }
}

大まかに次の 2 通りのアプローチがあります。

  1. static がローカルで、その thisの呼び出し元コンテキスト である場合、 メソッドとしてモデル化する
  2. thisout パラメーターである static メソッドとしてのモデル化。

さらに、コンストラクターは次のインバリアントを満たす必要があります。

  1. ref パラメーターを ref フィールドとしてキャプチャできることを確認します。
  2. ref のフィールドへの thisref パラメーターを介してエスケープできないことを確認します。 これは、不明瞭な ref 割り当てに違反します。

目的は、コンストラクターに特別なルールを導入せずに、不変条件を満たすフォームを選択することです。 コンストラクターに最適なモデルは、out パラメーターとして this を表示することです。 out の性質により、特別なケーシングなしで上記のすべての不変条件を満たすことができます。

public static void ctor(out S @this, ref int f)
{
    // The ref-safe-context of `ref f` is *return-only* which is also the 
    // safe-context of `this.field` hence this assignment is allowed
    @this.field = ref f;
}

メソッド引数が一致することが必要です

メソッド引数一致ルールは、開発者にとってよくある混乱の原因です。 ルールの背後にある推論に慣れていない限り、理解しにくい特殊なケースが多くあるルールです。 ルールの理由をよりよく理解するために、ref-safe-context safe-context を、単に コンテキストに簡略化します。

メソッドは、パラメーターとして渡された状態をかなり自由に返すことができます。 基本的に、スコープ外の到達可能な状態を返すことができます (ref による返しを含む)。 これは、return ステートメントを介して直接返すか、ref 値に割り当てることによって間接的に返すことができます。

直接的な戻りでは、ref の安全性に関する問題はあまり発生しません。 コンパイラは、メソッドに対するすべての戻り可能な入力を見るだけで、戻り値が入力の最小コンテキストに効果的に制限されます。 その戻り値には、通常の処理が行われます。

すべての ref がメソッドへの入力と出力の両方であるため、間接戻り値は大きな問題となります。 これらの出力には、既に既知のコンテキストがあります。 コンパイラは新しいものを推測することはできず、現在のレベルでそれらを考慮する必要があります。 つまり、コンパイラは、呼び出されたメソッドで割り当て可能なすべての を調べ、そのコンテキストにおける を評価し、メソッドに返される入力が より小さい のコンテキスト を持たないことを確認する必要があります。 このようなケースが存在する場合、メソッド呼び出しは、ref の安全性に違反する可能性があるため、無効となることが必要です。

メソッド引数一致は、コンパイラがこの安全性チェックをアサートするプロセスです。

これを評価する別の方法としては、開発者が考慮する方が多くの場合により簡単です。これは、次の演習を実行することです。

  1. メソッド定義を見て、状態を間接的に返すことができるすべての場所を特定します。 ref struct b を指す変更可能な ref パラメーター。 変更可能な ref パラメーターと ref 割り当て可能な ref フィールド c. ref を指す割り当て可能な ref パラメーターまたは ref struct フィールド(再帰的に考慮する)
  2. 呼び出しサイトを参照してください。a. 上記の b で識別された場所に一列に並んだコンテキストを識別します。 メソッドにおいて返すことができるすべての入力のコンテキストを識別します(scoped パラメーターと一致しない)。

2.b の値が 2.a より小さい場合、メソッド呼び出しは無効であることが必要です。 ルールを示すいくつかの例を見てみましょう。

ref struct R { }

class Program
{
    static void F0(ref R a, scoped ref R b) => throw null;

    static void F1(ref R x, scoped R y)
    {
        F0(ref x, ref y);
    }
}

F0 への呼び出しを見ると、(1) と (2) を確認できます。 間接的な戻り値の可能性があるパラメーターは a および b であり、どちらも直接割り当てることができます。 これらのパラメーターに合う引数は次のとおりです。

  • a (xcontext を持つ にマップされる)
  • b (ycontext を持つ にマップされる)

メソッドに返される一連の入力は次のとおりです。

  • x (caller-contextescape-scope)
  • ref x (caller-contextescape-scope)
  • y (function-memberescape-scope)

ref y 値は scoped ref にマップされるために入力とは見なされず、戻り値は返されません。 ただし、出力の 1 つ (x 引数) より小さいエスケープ スコープ (y 引数) がある入力が少なくとも 1 つあるため、メソッド呼び出しは無効です。

別のバリエーションを次に示します。

ref struct R { }

class Program
{
    static void F0(ref R a, ref int b) => throw null;

    static void F1(ref R x)
    {
        int y = 42;
        F0(ref x, ref y);
    }
}

間接的なリターンの可能性があるパラメーターは、abです。どちらも直接割り当てることができます。 ただし、bref struct を指さず、ref 状態を格納するために使用できないため除外できます。 したがって、次のようになります。

  • a (xcontext を持つ にマップされる)

メソッドに返される一連の入力は次のとおりです。

  • x (caller-contextcontext)
  • ref x (caller-contextcontext)
  • ref y (function-membercontext)

出力の 1 つ (x 引数) より小さいエスケープ スコープ (ref y 引数) がある入力が少なくとも 1 つあるため、メソッド呼び出しは無効です。

これは、メソッド引数一致ルールのロジックが包含しようとしているものです。 scoped は入力を考慮から削除する方法として、readonlyref を出力から削除する方法として、両方を考慮してさらに進んでいます(readonly ref に割り当てることができないので、出力のソースとして使用できません)。 これらの特殊なケースでは、ルールが複雑になりますが、開発者のために行われます。 コンパイラは、メンバーを呼び出すときに開発者に最大限の柔軟性を与えるために、結果に貢献できないことがわかっているすべての入力と出力を削除することを試みます。 オーバーロードの解決と同様に、コンシューマーの柔軟性を高める場合は、ルールをより複雑にする価値があります。

宣言式の推論された safe-context の例

宣言式の safe-context の推論に関連します。

ref struct RS
{
    public RS(ref int x) { } // assumed to be able to capture 'x'

    static void M0(RS input, out RS output) => output = input;

    static void M1()
    {
        var i = 0;
        var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M3(RS rs1)
    {
        M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
    }
}

scoped 修飾子の結果として得られるローカル コンテキストが最も狭く、変数に使用できる可能性が最も狭い点に注意してください。つまり、式は式よりも狭いコンテキストでのみ宣言されている変数を参照します。