低レベルの構造体の機能向上
メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/1147、https://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 には次のものが含まれます。
ref
フィールドとscoped
[UnscopedRef]
これらの機能は引き続き、C# のこれからのバージョンに関するオープンな提案となります。
ref
フィールドをref struct
にする- 制限付きの型の使用停止
目的
以前のバージョンの 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 _field
は ELEMENT_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# 言語は、ref
が null
とはならないと主張しますが、これはランタイム レベルでは有効であり、セマンティクスが明確に定義されています。 型に 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 readonly
とreadonly 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.F
ref-safe-context の形式としての表現は、次の通りです。
F
がref
フィールドである場合、ref-safe-context は のe
です。- それ以外で、
e
が参照型の場合は、呼び出し元コンテキストの ref-safe-context になります。- それ以外の場合、その ref-safe-context は、 の
e
から取得されます。
これは、ルールが常に ref
内に存在する ref struct
状態を考慮するため、ルールの変更を表すものではありません。 実際には、ref
の Span<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 である必要があります。
e2
の ref-safe-context は、e1
の ref-safe-context と同じ大きさ以上である必要があります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
注釈は、this
の struct
パラメーターを 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
は、ref
、in
、または out
に適用される場合にのみ影響しますが、値が ref struct
である場合にのみ影響します。 scoped int
は常に安全に返せるため、ref struct
のような宣言があっても影響はありません。 コンパイラは、開発者の混乱を避けるために、このようなケースの診断を作成します。
out
パラメーターの動作を変更する
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
フィールドの全体的な互換性への影響が大幅に軽減され、開発者が 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 ref
と ref
の観点から記述されます。 ref セーフ コンテキストの場合、in
パラメーターは ref
に相当し、out
は scoped ref
と同等です。 in
と out
の両方は、ルールのセマンティックにとって重要な場合にのみ特別に呼び出されます。 それ以外の場合は、それぞれ 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 以上である必要があります。 そのため、ほとんどの既存のルールは適用されません。たとえば、ref
が return-only である式から パラメーターへの割り当ては、ref
パラメーターの safe-context (caller-context) よりも小さいため失敗します。 この新しいエスケープ コンテキストの必要性については、後ほど説明します。
以下の 3 か所は、既定で return-only となります:
ref
またはin
パラメーターの ref-safe-context は、return-only になります。 これは、ref struct
の問題を防ぐために で部分的に行われます。 これは、モデルを簡略化して、互換性の変更を最小限に抑えるために、均一に行われます。out
のref struct
パラメーターの safe-context は return-only になります。 これにより、戻り値とout
を同等に表現できます。 これは、out
が暗黙のうちにscoped
に相当するため、ref-safe-context は セーフコンテキストのよりも小さく、循環的な割り当ての問題を避けられます。struct
コンストラクターのthis
パラメーターの safe-context は return-only になります。out
パラメーターとしてモデル化されているため、これは除外されます。
メソッドまたはラムダから値を明示的に返す式またはステートメントには、セーフ コンテキスト が必要です。また、ref-safe-context に該当する場合は、少なくとも戻り専用が必要です。 これには、return
ステートメント、式本体のメンバー、およびラムダ式が含まれます。
同様に、out
への割り当てには、return-only 以上の safe-context が必要です。 ただし、これは特殊なケースではなく、既存の割り当てルールに従っているだけです。
注: 型が
メソッド呼び出しのルール
メソッド呼び出しの ref セーフ コンテキスト ルールは、いくつかの方法で更新されます。 1 つ目は、scoped
が引数に与える影響を認識する方法です。 パラメーター p
に渡される引数 expr
の場合:
p
がscoped ref
の場合、safe-context を算定する際の引数としてexpr
は考慮されません。p
がscoped
の場合、safe-context を算定する際の引数としてexpr
は考慮されません。p
がout
の場合、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 があります。
- caller-context
- すべての引数式によって提供される
ref struct
の場合、戻り値は です。- 戻り値が
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 です。
- caller-context
- safe-context は、すべての引数式で提供されます
- すべての
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 は、次の中で最も狭い範囲のものになります。
- コンストラクター呼び出しの safe-context。
- 受信者にエスケープできるメンバー初期化インデクサーへの引数の safe-context および ref-safe-context。
- 非読み取り専用セッターに対するメンバー初期化子内の割り当ての 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)
が対象
- 最も狭い safe-context を以下から算定します。
- caller-context
- すべての引数の safe-context
- 対応するパラメーターの ref-safe-context が caller-context であるすべての ref 引数の ref-safe-context
ref struct
型のすべてのref
引数は、その safe-context を持つ値によって割り当て可能であることが必要です。 これはref
が とin
を含むようにout
ケースです
任意のメソッド呼び出し
e.M(a1, a2, ... aN)
が対象
- 最も狭い safe-context を以下から算定します。
- caller-context
- すべての引数の safe-context
- 対応するパラメーターが
scoped
でないすべての ref 引数の ref-safe-contextref struct
型のすべてのout
引数は、その safe-context を持つ値によって割り当て可能であることが必要です。
scoped
の存在により、開発者は、scoped
として返されないパラメーターをマークすることで、このルールによって生じる摩擦を軽減できます。 これにより、上記の両方のケースで (1) から引数が削除され、呼び出し元の柔軟性が向上します。
この変更の影響については、以下で詳しく説明します。 これにより、開発者は、非エスケープ型の ref-like 値に scoped
で注釈を付けることで、呼び出しサイトの柔軟性を高めることができるようになります。
パラメータースコープのバリアンス
パラメーターの scoped
修飾子と [UnscopedRef]
属性 (以下を参照) は、オブジェクトのオーバーライド、インターフェイスの実装、delegate
変換ルールにも影響します。 オーバーライド、インターフェイス実装、または delegate
変換のシグネチャは、次のことができます。
ref
またはin
パラメーターにscoped
を追加することscoped
をref 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 つの追加の
ref
、in
、または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
フィールドは、static
、volatile
またはconst
で宣言できませんref
フィールドには、ref struct
型を持つことはできません。- 参照アセンブリ生成プロセスでは、
ref struct
内にref
フィールドが存在することを保持する必要があります readonly ref struct
は、そのref
フィールドをreadonly ref
として宣言する必要があります- by-ref 値の場合、
scoped
、in
、または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
;
[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-member の ref-safe-context と safe-context が含まれます。
ランタイムに準拠することで、TypedReference
、RuntimeArgumentHandle
、および ArgIterator
が ref struct
として定義されます。 さらに TypedReference
は、どのような型であっても(すべての値を格納できる)、ref
に対して ref struct
フィールドを持つものとして見なされる必要があります。 上記のルールと組み合わせることで、スタックへの参照が有効期間を超えてエスケープされないようにします。
注: 厳密に言えば、これはコンパイラ実装の詳細と言語の一部です。 ただし、ref
フィールドとの関係を考えると、わかりやすくするために言語提案に含まれています。
スコープなしを提供する
注目すべき摩擦点の 1 つは、ref
のインスタンス メンバーにおいて struct
でフィールドを返せないことです。 つまり、開発者はメソッド / プロパティを返す ref
を作成できないため、フィールドを直接公開する必要があります。 これにより、最も必要とされることが多い ref
の struct
戻り値の有用性が低下します。
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]
で注釈が付けられたインスタンス メソッドまたはプロパティでは、 のthis
が caller-context に設定されています。[UnscopedRef]
で注釈が付けられたメンバーは、インターフェイスを実装できません。- 以下での
[UnscopedRef]
の使用はエラーですstruct
に宣言されていないメンバーstatic
メンバー、struct
のinit
メンバーまたはコンストラクター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 で再コンパイルすると、破壊的変更が発生する可能性があります。
- スコープなしの
ref
/in
/out
パラメーターは、C#7.2 ではなく C#11 のref struct
のref
フィールドとしてメソッド呼び出しをエスケープできます -
out
パラメーターは C#11 で暗黙的にスコープ指定され、C#7.2 ではスコープなしです -
ref
/in
パラメーター(ref struct
型への)は、C#11 では暗黙的にスコープ指定され、C#7.2 ではスコープなしです
C#11 を使用して再コンパイルする際に大規模な変更が発生する可能性を減らすために、C#11 コンパイラを更新し、メソッド呼び出し に 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 になります。
- caller-context
- safe-context は、すべての引数式で提供されます
- 戻り値が
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 つの方法があります。
- 値を返す
ref
を返すref
のref 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 T
に T
を割り当てることができる既存の不変性をサポートするために存在します。 これは、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
によって既に簡単に返される可能性があるためです (ref
のref struct
状態は、その価値が格納されていない場合でも ref
によって返されることができます)。 したがって、これらの値が互いに適切であることを保証するために、ルールにはさらに一定の調整が必要です。
また、ref
フィールド、ref
、値による 3 つの方法で返すことができる ref
パラメーターを表す構文が言語に必要であることを意味します。 既定値は、ref
によって返されます。 今後は、特に ref struct
が関与している場合、より自然な復帰が ref
フィールドまたは ref
によって行われることが予想されます。 つまり、新しい API では、既定で正しい構文注釈が追加されていることが必要です。 これは望ましくありません。
ただし、これらの互換性の変更は、次のプロパティを持つメソッドに影響します。
Span<T>
またはref struct
を用意してくださいref struct
が戻り値の型、ref
、またはout
パラメーターである場合- 受信側を除く追加の
in
またはref
パラメーターがある場合
影響を理解するには、API をカテゴリに分割すると便利です。
- コンシューマーで
ref
がref
フィールドとしてキャプチャされることを考慮する。 主な例はSpan(ref T value)
コンストラクターです - コンシューマーで
ref
がref
フィールドとしてキャプチャされることを考慮しない。 これらは 2 つのカテゴリに分かれています- 安全でない API。 これらは、
Unsafe
型とMemoryMarshal
型内の API であり、MemoryMarshal.CreateSpan
が最も顕著です。 これらの API はref
を安全ではなくキャプチャし、安全でない API とも呼ばれます。 - 安全な API。 これらは、効率性のために
ref
パラメーターを受け取る API ですが、実際にはどこにもキャプチャされません。 例は小さいですが、一例はAsnDecoder.ReadEnumeratedBytes
です。
- 安全でない API。 これらは、
この変更は主に上記の (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
パラメーターは、ref
に scoped
を追加する必要があります。 (2.1) では、開発者は Unsafe
または MemoryMarshal
を使用するように強制される可能性がありますが、これは安全でないスタイルの API に必要です。
API が面倒な動作に陥ったときに警告を発行することで、言語がサイレント破壊的変更の影響を軽減できることが理想的です。 これは両方とも ref
を受け取り、ref struct
を返しますが、実際には ref struct
の ref
をキャプチャしないメソッドです。 コンパイラは、この場合、このような ref
に代わりに scoped ref
として注釈を付ける必要があることを開発者に通知する診断を発行することができます。
決定 この設計は実現可能ですが、その機能の使用が非常に困難であるため、互換性を破棄する決定が下されました。
決定 メソッドは条件を満たしているが、ref
パラメーターを ref
フィールドとしてキャプチャしない場合、コンパイラは警告を発行します。 これにより、お客様が作成している潜在的な問題について、アップグレード時に適切に警告します。
キーワードと属性
この設計では、属性を使用して新しい有効期間ルールに注釈を付ける必要があります。 これはまた、コンテキスト キーワードでも同様に簡単に実行できていました。 たとえば、[DoesNotEscape]
は scoped
にマップできます。 しかしながら、キーワードはたとえコンテキストに依存するものであっても、一般に、包含するために非常に高い基準を満たす必要があります。 彼らは貴重な言語資源を占め、言語の中でより目立つ存在です。 この機能は価値あるものの、少数の C# 開発者に役立つものです。
表面的にはキーワードを使用しない長点があるように見えますが、考慮すべき重要な点が 2 つあります。
- 注釈はプログラムセマンティクスに影響します。 属性がプログラムセマンティクスに影響を与えるのは、C# がクロスに消極的であり、これがそのステップを実行する言語を正当化する必要がある機能かどうかは不明です。
- この機能を使用する可能性が最も高い開発者は、関数ポインターを使用する一連の開発者と強く対立します。 この機能は少数派の開発者にも使用されていますが、新しい構文が必要であり、その決定は依然として支持されています。
これは、構文を考慮する必要があることを意味します。
構文の大まかなスケッチは次のようになります。
[RefDoesNotEscape]
をscoped ref
にマップする[DoesNotEscape]
をscoped
にマップする[RefDoesEscape]
をunscoped
にマップする
決定 scoped
と scoped 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 ルールに寄与するすべての要素は、メソッドシグネチャの強力な部分です :ref
、in
、ref struct
などです。そのため、メソッドの既存の規則を変更すると、既にシグネチャがバイナリに変更されます。 新しい有効期間注釈に同様の影響を与えるためには、modreq
の施行が必要です。
懸念事項は、これが過剰であるかどうかです。 パラメーターに [DoesNotEscape]
を追加するとバイナリ互換性の変更が発生し、シグネチャの柔軟性が高まるという悪影響があります。 それによるトレードオフは、時間が経つにつれて、BCL などのフレームワークではそのような署名を緩めることができなくなる可能性があることを意味します。 これは、言語が in
パラメーターで行うアプローチを取ることである程度軽減でき、仮想位置にのみ modreq
を適用できます。
決定 メタデータで modreq
を使用しないこと。 out
と ref
の違いは 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.CreateSpan
と MemoryMarshal.CreateReadOnlySpan
です。 これは、パラメーターを scoped
としてマークすることで実現します。
つまり、ランタイムには、パラメーターから安全でない scoped
を削除するための確立されたパターンが必要です。
-
Unsafe.AsRef<T>(in T value)
はscoped in T value
に変更することで、既存の目的を拡張できます。 これにより、パラメーターからin
とscoped
の両方を削除できます。 これにより、普遍的な "ref safety の削除" メソッドになります。 scoped
を削除することを目的とする新しい方法を紹介します:ref T Unsafe.AsUnscoped<T>(scoped in T value)
。 これにより、in
も削除されます。そうしないと、呼び出し元はメソッド呼び出しを組み合わせて「ref safety を削除」する必要があり、その時点で既存のソリューションで十分である可能性が高いためです。
デフォルトでスコープをなしにしますか?
デザインには、既定で scoped
である 2 つの場所しかありません。
-
this
はscoped ref
-
out
はscoped ref
out
に関する決定は、ref
フィールドの互換性の負担を大幅に軽減することであり、同時により自然な既定値にすることです。 これにより、開発者は実際に out
を、ref
にようにデータが外部に流れるものとして考えます。そして、ルールでは双方向に流れるデータを考慮する必要があります。 これにより、開発者への多大な混乱が生じます。
this
に関する決定は、struct
が ref
でフィールドを返すことができないため望ましくありません。 これは高パフォーマンスの開発者にとって重要なシナリオであり、[UnscopedRef]
属性は基本的にこの 1 つのシナリオに対して追加されています。
キーワードには高度な制約があり、1 つのシナリオに対してキーワードを追加することは疑わしいものです。 このキーワードを回避するために、this
をデフォルトで ref
にして、scoped ref
にはしないという選択肢を検討しました。 this
を scoped 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
:struct
、readonly ref struct
、またはreadonly member
this
としてのscoped ref
:ref struct
またはreadonly ref struct
(ref
フィールドをref struct
に)
これにより、互換性の中断が最小限に抑えられ、柔軟性が最大限に高まりますが、お客様側の事情が複雑化する負担が生じます。 将来の機能としての安全な fixed
バッファーのような場合、変更可能な ref struct
はこの設計だけでは機能しないフィールドに対して ref
を戻す必要がありますが、それはscoped ref
カテゴリに分類されるため、この問題は完全には解決されません。
決定 this
を scoped ref
として維持します。 つまり、上記のずるい例ではコンパイラ エラーが発生します。
ref フィールドから ref struct へ
この機能により、ref
フィールドが ref struct
を参照できるため、ref safe context ルールの新しいセットが開拓されます。 この一般的な ByReference<T>
の性質は、これまでランタイムがそのようなコンストラクトを持つことができなかったことを意味しました。 その結果、すべてのルールは、これが不可能であると仮定して書かれています。 ref
フィールド機能は、主に新しいルールを作成するのではなく、システム内の既存のルールを体系化します。 ref
フィールドの ref struct
を許可するには、いくつかの新しいシナリオを考慮する必要があるため、新しいルールを体系化する必要があります。
1 つ目は、readonly ref
が ref
状態を格納できるようになったということです。 次に例を示します。
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
つまり、メソッド引数一致ルールを考慮すると、T
が ref struct
に ref
フィールドを持つ可能性がある場合に、readonly ref T
が潜在的なメソッド出力であると考える必要があります。
2つ目の問題は、新しい種類の安全なコンテキストを言語で考慮しなければならないことです: ref-field-safe-context。 ref
フィールドが推移的に含まれるすべての 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 struct
と Span<Span<T>>
を使用してこの方向にプッシュする可能性があるため、この懸念は特に高まります。 ルールは、従量課金コードと組み合わせて行う場合に、より適切に記述されます。
Decisionref
フィールドを ref struct
できるようにすることを、.NET 8 においてこれらのシナリオに関するルールを推進するためのシナリオが整うまで遅らせます。 これは .NET 9 の時点では実装されていません
C# 11.0 に何が含まれるか?
このドキュメントで説明する機能を 1 回のパスで実装する必要はありません。 代わりに、次のバケット内の複数の言語リリースにわたって段階的に実装できます。
ref
フィールドとscoped
[UnscopedRef]
ref
フィールドをref struct
にする- 制限付きの型の使用停止
- 固定サイズ バッファー
どのリリースで何が実装されるかは、範囲設定の問題ということになります。
決定 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
より大きくなります。 これにより、呼び出し側は、現在存在するより粗い規則に比べて、メソッドに値を安全に渡すためのより詳細な規則を持つことができます。
関連情報
問題
次の問題はすべて、この提案に関連しています。
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Proposals
次の提案は、この提案に関連しています。
既存のサンプル
この特定のスニペットでは、ref struct
のインスタンス メソッドにスタック割り当て可能な Span<T>
を渡す際に問題が発生するため、安全ではありません。 このパラメーターがキャプチャされていない場合でも、言語はそれを想定する必要があるため、ここでは不必要に摩擦を引き起こします。
このスニペットでは、データの要素をエスケープしてパラメーターを変更する必要があります。 エスケープされたデータは、効率のためにスタック割り当て可能です。 パラメーターがエスケープされていない場合でも、コンパイラはそれがパラメーターであることから、包絡メソッドの外側にあるセーフコンテキスト を割り当てます。 つまり、スタック割り当てを使用するには、この実装ではデータをエスケープした後にパラメーターに再度割り当てるために 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 struct
が Span<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
を回避するための手段であるためです。
ここで、scoped
は ref 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
に対する ref
の ref-safe-context を return-only にし、out
を caller-context にする理由です。 これにより、有効期間の違いにより、循環割り当てが防止されます。
[UnscopedRef]
は、ref struct
の値に対する ref
の ref-safe-context を caller-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-context と return-only の ref-safe-context の両方があるため、循環割り当てが許可されます。
[UnscopedRef] ref
を caller-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 通りのアプローチがあります。
static
がローカルで、そのthis
が の呼び出し元コンテキスト である場合、 メソッドとしてモデル化するthis
がout
パラメーターであるstatic
メソッドとしてのモデル化。
さらに、コンストラクターは次のインバリアントを満たす必要があります。
ref
パラメーターをref
フィールドとしてキャプチャできることを確認します。ref
のフィールドへのthis
がref
パラメーターを介してエスケープできないことを確認します。 これは、不明瞭な 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
の安全性に違反する可能性があるため、無効となることが必要です。
メソッド引数一致は、コンパイラがこの安全性チェックをアサートするプロセスです。
これを評価する別の方法としては、開発者が考慮する方が多くの場合により簡単です。これは、次の演習を実行することです。
- メソッド定義を見て、状態を間接的に返すことができるすべての場所を特定します。
ref struct
b を指す変更可能なref
パラメーター。 変更可能なref
パラメーターと ref 割り当て可能なref
フィールド c.ref
を指す割り当て可能なref
パラメーターまたはref struct
フィールド(再帰的に考慮する) - 呼び出しサイトを参照してください。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
(x
の context を持つ にマップされる)b
(y
の context を持つ にマップされる)
メソッドに返される一連の入力は次のとおりです。
x
(caller-context の escape-scope)ref x
(caller-context の escape-scope)-
y
(function-member の escape-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);
}
}
間接的なリターンの可能性があるパラメーターは、a
とb
です。どちらも直接割り当てることができます。 ただし、b
は ref struct
を指さず、ref
状態を格納するために使用できないため除外できます。 したがって、次のようになります。
a
(x
の context を持つ にマップされる)
メソッドに返される一連の入力は次のとおりです。
-
x
(caller-context の context) -
ref x
(caller-context の context) ref y
(function-member の context)
出力の 1 つ (x
引数) より小さいエスケープ スコープ (ref y
引数) がある入力が少なくとも 1 つあるため、メソッド呼び出しは無効です。
これは、メソッド引数一致ルールのロジックが包含しようとしているものです。 scoped
は入力を考慮から削除する方法として、readonly
は ref
を出力から削除する方法として、両方を考慮してさらに進んでいます(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
修飾子の結果として得られるローカル コンテキストが最も狭く、変数に使用できる可能性が最も狭い点に注意してください。つまり、式は式よりも狭いコンテキストでのみ宣言されている変数を参照します。
C# feature specifications