手記
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/1331
概要
反復子と非同期メソッドの動作を統一します。 具体的には:
- コード セグメントで
yield
やawait
を使用しない場合に限り、反復子および非同期メソッドでref
/ref struct
ローカルとunsafe
ブロックを許可します。 lock
内のyield
について警告します。
モチベーション
yield
または await
間で使用されていない場合、ref
/ref struct
ローカルおよび unsafe
ブロックを、非同期/反復子メソッドで禁止する必要はありません。これは、ホイストする必要がないためです。
async void M()
{
await ...;
ref int x = ...; // error previously, proposed to be allowed
x.ToString();
await ...;
// x.ToString(); // still error
}
重大な変更
言語仕様に破壊的変更はありませんが、Roslyn の実装には 1 つの破壊的変更があります (仕様違反が原因)。
Roslyn は、反復子が安全なコンテキスト (§13.3.1) を導入することを示す仕様の部分に違反します。
たとえば、ローカル関数を含む反復子メソッドを持つ unsafe class
がある場合、そのローカル関数はクラスから安全でないコンテキストを継承しますが、反復子メソッドが原因で仕様に従って安全なコンテキストに存在する必要があります。
実際、反復子メソッド全体が Roslyn の安全でないコンテキストを継承しました。反復子で安全でないコンストラクトを使用することは許可されていませんでした。
LangVersion >= 13
では、安全でないコンストラクトを反復子で許可するため、安全なコンテキストが正しく導入されます。
unsafe class C // unsafe context
{
System.Collections.Generic.IEnumerable<int> M() // an iterator
{
yield return 1;
local();
async void local()
{
int* p = null; // allowed in C# 12; error in C# 13 (breaking change)
await Task.Yield(); // error in C# 12, allowed in C# 13
}
}
}
手記:
- 中断は、
unsafe
修飾子をローカル関数に追加するだけで回避できます。 - ラムダは「イテレータコンテキスト」を「継承」することで影響を受けないため、その中で安全でないコンストラクトを使用することは不可能でした。
詳細な設計
次の変更は LangVersion に関連付けられています。つまり、C# 12 以前では、非同期メソッドと反復子では ref のようなローカルブロックと unsafe
ブロックが禁止され続け、C# 13 では以下に説明するようにこれらの制限が解除されます。
ただし、既存の Roslyn 実装に一致する仕様の明確化は、すべての LangVersions に保持する必要があります。
1 つ以上の
yield
ステートメント (§13.15) を含む ブロック は反復子ブロックと呼ばれ、yield
ステートメントが入れ子になったブロックにのみ間接的に含まれている場合でも (入れ子になったラムダとローカル関数を除く)。[...]
反復子ブロックに安全でないコンテキスト (§23.2) が含まれている場合のコンパイル時エラーです。 反復子ブロックは、安全でないコンテキストで宣言が入れ子になっている場合でも、常に安全なコンテキストを定義します。反復子の実装に使用される反復子ブロック (§15.14) は、反復子宣言が安全でないコンテキストで入れ子になっている場合でも、常に安全なコンテキストを定義します。
この仕様では、次のことも行います。
- 反復子宣言が
unsafe
修飾子でマークされている場合、シグネチャは安全でないスコープ内にありますが、その反復子を実装するために使用される反復子ブロックは安全なスコープを定義します。 - 反復子プロパティまたはインデクサーの
set
アクセサー (つまり、そのget
アクセサーは反復子ブロックを介して実装されます) は、宣言から安全/安全でないスコープを "継承" します。 - これは、シグネチャのみであり、反復子本体を持つことができないため、実装のない部分宣言には影響しません。
C# 12 では、unsafe
修飾子でマークされた反復子メソッドを持つことはエラーですが、仕様の変更により C# 13 で許可されることに注意してください。
例えば:
using System.Collections.Generic;
using System.Threading.Tasks;
class A : System.Attribute { }
unsafe partial class C1
{ // unsafe context
[/* unsafe context */ A]
IEnumerable<int> M1(
/* unsafe context */ int*[] x)
{ // safe context (this is the iterator block implementing the iterator)
yield return 1;
}
IEnumerable<int> M2()
{ // safe context (this is the iterator block implementing the iterator)
unsafe
{ // unsafe context
{ // unsafe context (this is *not* the block implementing the iterator)
yield return 1; // error: `yield return` in unsafe context
}
}
}
[/* unsafe context */ A]
unsafe IEnumerable<int> M3(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
[/* unsafe context */ A]
IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
[/* unsafe context */ A]
unsafe IEnumerable<int> this[
/* unsafe context */ long*[] x]
{ // unsafe context (the iterator declaration is unsafe)
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
IEnumerable<int> M4()
{
yield return 1;
var lam1 = async () =>
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // error in both C# 12 and C# 13 (unsafe in iterator)
};
unsafe
{
var lam2 = () =>
{ // unsafe context, lambda cannot be an iterator
yield return 1; // error: yield cannot be used in lambda
};
}
async void local()
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // allowed in C# 12, error in C# 13 (breaking change in Roslyn)
}
local();
}
public partial IEnumerable<int> M5() // unsafe context (inherits from parent)
{ // safe context
yield return 1;
}
}
partial class C1
{
public partial IEnumerable<int> M5(); // safe context (inherits from parent)
}
class C2
{ // safe context
[/* unsafe context */ A]
unsafe IEnumerable<int> M(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
unsafe IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
}
method_modifierref ローカル変数またはasync
で宣言されたメソッド内、またはイテレータ(§15.14)内で ref ローカル変数またはref struct
型の変数を宣言するのは、コンパイル時エラーです。ref struct
型の変数を、await
式やyield return
ステートメントを跨いで宣言して使用するのは、コンパイル時エラーです。より具体的には、エラーは以下のプロセスによって発生します。await
式(§12.9.8)またはyield return
ステートメント(§13.15)の後、スコープ内のすべての ref ローカル変数とref struct
型の変数は、確定的に未割り当てと見なされます(§9.4)。
このエラーは、他の ref 安全性エラー などの unsafe
コンテキストでは警告にダウングレードされないことに注意してください。
これは、ステートマシン書き換えの実装詳細に依存せずに、refのようなローカルをunsafe
コンテキストで操作できないために発生するエラーであり、このため、unsafe
コンテキストで警告に変更する基準を超えています。
関数メンバーが反復子ブロックを使用して実装されている場合、関数メンバーの仮パラメーター リストで、
in
、ref readonly
、out
、またはref
パラメーター、またはref struct
型のパラメーター またはポインター型を指定するコンパイル時エラーです。
非同期メソッドに await
を含まない unsafe
ブロックを許可するために、仕様を変更する必要はありません。これは、スペックが非同期メソッドで unsafe
ブロックを許可したことがないためです。
ただし、仕様は常に unsafe
ブロック内の await
を禁止する必要があります (前述のように、§13.3.1 の unsafe
で既に yield
を禁止していました)、次の仕様の変更を提案します。
in
、out
、またはref
パラメーター、またはref struct
型のパラメーターを指定するのは、非同期関数の仮パラメーター リストのコンパイル時エラーです。安全でないコンテキスト (§23.2) に
await
式 (§12.9.8) またはyield return
ステートメント (§13.15) が含まれている場合のコンパイル時エラーです。
反復子内のローカルまたはパラメーターのアドレスを取得すると、コンパイル時エラーが報告されます。
現時点では、非同期メソッドでローカルまたはパラメーターのアドレスを取得することは、C# 12 警告ウェーブの警告に当たります。
前述の仕様の変更からすべて除外されるため、仕様の変更は特に必要ありませんが、非同期/反復子メソッドで await
や yield
を行わずにセグメント内で許可される ref
により、より多くのコンストラクトが機能する可能性があることに注意してください。
using System.Threading.Tasks;
ref struct R
{
public ref int Current { get { ... }};
public bool MoveNext() => false;
public void Dispose() { }
}
class C
{
public R GetEnumerator() => new R();
async void M()
{
await Task.Yield();
using (new R()) { } // allowed under this proposal
foreach (var x in new C()) { } // allowed under this proposal
foreach (ref int x in new C()) { } // allowed under this proposal
lock (new System.Threading.Lock()) { } // allowed under this proposal
await Task.Yield();
}
}
選択肢
ref
/ref struct
ローカルは、await
/yield
を含まないブロック (§13.3.1) でのみ許可できます。// error always since `x` is declared/used both before and after `await` { ref int x = ...; await Task.Yield(); x.ToString(); } // allowed as proposed (`x` does not need to be hoisted as it is not used after `await`) // but alternatively could be an error (`await` in the same block) { ref int x = ...; x.ToString(); await Task.Yield(); }
lock
内のyield return
は、エラー (lock
内のawait
など) または警告波の警告である可能性がありますが、これは重大な変更になります:https://github.com/dotnet/roslyn/issues/72443. 新しいLock
-object-basedlock
レポートでは、本文でyield return
に対してコンパイル時エラーが報告されることに注意してください。これは、このようなlock
ステートメントは、その本体でyield return
を許可しないref struct
のusing
に相当するためです。非同期メソッドまたは反復子メソッド内の変数は、ステート マシンのフィールド (キャプチャされた変数と同様に) にホイストする必要がある場合は、"固定" ではなく"移動可能" にする必要があります。 これは、
async
メソッド内のunsafe
ブロックが常に許可されているため、提案の残りの部分に依存しない仕様の既存のバグであることに注意してください。 現在、C# 12 警告ウェーブではこれに対する警告がされており、エラーにすることは破壊的変更になります。正確に言うと、固定変数は次のいずれかです。
- ローカル変数、値パラメータ、またはパラメータ配列を参照する simple_name (§12.8.4) から得られる変数 (変数が匿名関数によってキャプチャされる場合を除く (§12.19.6.2)) またはローカル関数 (§13.6.4)、あるいは変数は、非同期 (§15.15) または反復子 (§15.14) メソッドの一部としてホイストする必要があります。
- [...]
現在、C# 12の警告波には、非同期メソッド内のアドレス演算子に関する既存の警告が含まれており、LangVersion 13以降では繰り返し処理の中でのアドレス演算子に対するエラーが提案されています(以前のバージョンでは、繰り返し処理で安全でないコードを使用することが不可能であったため、報告する必要はありませんでした)。 これらの両方を緩和して、すべてのローカルとパラメーターではなく、実際にホイストされた変数にのみ適用できます。
fixed
を使用してホイストまたはキャプチャされた変数のアドレスを取得することもできますが、フィールドであるという事実は実装の詳細であるため、他の実装ではfixed
を使用できない可能性があります。 ホイスト変数も "移動可能" と見なすだけで、キャプチャされた変数は既に "移動可能" であり、fixed
は許可されていないことに注意してください。
fixed
ステートメント内を除き、unsafe
内のawait
/yield
を許可することもできます (コンパイラは、メソッドの境界を越えて変数をピン留めすることはできません)。 その結果、次の入れ子になった箇条書きの説明のように、たとえばstackalloc
の周辺で予期しない動作が発生する可能性があります。 それ以外の場合、一部のシナリオでは現在でもホイスト ポインターがサポートされるため (引数としてのポインターに関連する例を以下に示します)、これを許可する他の制限はありません。- スタック割り当てバッファーは
await
/yield
ステートメント間で存在しないため、非同期/反復子メソッドのstackalloc
の安全でないバリアントを禁止できます。 設計上の安全でないコードは"自由後の使用"を妨げるわけではないので、必要と感じません。await
/yield
間で使用されない場合は安全でないstackalloc
を許可することもできますが、分析が困難な場合があります (結果のポインターは任意のポインター変数で渡すことができます)。 非同期/反復子メソッドでfixed
とする必要があります。 それは、 がawait
/yield
間で使用するのを妨げますが、stackalloc
式は移動可能な値ではないため、fixed
のセマンティクスに一致しません。 (stackalloc
の結果をawait
/yield
全体で使用することは不可能ではないことに注意してください。これは、現在のfixed
ポインターを別のポインター変数に保存してfixed
ブロックの外部で使用できるのと同様です。)
- スタック割り当てバッファーは
反復子メソッドと非同期メソッドは、ポインター パラメーターを持つことができます。 それらは吊り上げる必要がありますが、今日でもポインターの吊り上げはサポートされているため、それは問題ではありません。
unsafe public void* M(void* p) { var d = () => p; return d(); }
現在、この提案は、反復子メソッドが安全でないコンテキストにある場合でも安全なコンテキストを開始する既存の仕様を保持 (および拡張/明確化) します。 たとえば、反復子メソッドは、
unsafe
修飾子を持つクラスで定義されている場合でも、安全でないコンテキストではありません。 または、他のメソッドと同様に、反復子がunsafe
修飾子を継承するようにすることもできます。- 利点: 仕様と実装から複雑さを取り除く。
- 利点: 反復子を非同期メソッド (特徴の動機の 1 つ) に合わせます。
- 欠点: 安全でないクラス内の反復子は
yield return
ステートメントを含めませんでした。このような反復子は、unsafe
修飾子なしで別の部分クラス宣言で定義する必要があります。 - 欠点: これは LangVersion=13 の破壊的変更です (安全でないクラスの反復子は C# 12 で許可されます)。
本体のみの安全なコンテキストを定義する反復子の代わりに、シグネチャ全体が安全なコンテキストになる可能性があります。 これは、通常、本体が宣言に影響を与えないという点で、言語の残りの部分と矛盾していますが、ここでは、本体が反復子であるかどうかに応じて、宣言は安全か安全ではありません。 C# 12 反復子シグネチャは安全ではありません (たとえば、ポインター配列パラメーターを含めることができます) と同様に、LangVersion=13 の破壊的変更でもあります。
反復子に
unsafe
修飾子を適用する:- 本文と署名に影響を及ぼす可能性があります。 このような反復子は、安全でない本体に
yield return
を含むことができず、yield break
しか含められないため、あまり役に立ちません。 LangVersion <= 12
と同様にLangVersion >= 13
でもエラーになる可能性があります。安全でない反復子メンバーを持つことは、追加の安全でないブロックなしでポインタ配列パラメータまたは安全でないセッターを持つことしか許可されないため、あまり役に立ちません。 ただし、将来、通常のポインター引数が許可される可能性があります。
- 本文と署名に影響を及ぼす可能性があります。 このような反復子は、安全でない本体に
Roslyn の破壊的変更:
- たとえば、反復子メソッドに安全なコンテキストを導入した後、ローカル関数の安全でないコンテキストに戻すことで、現在の動作を保持できます (また、それに一致するように仕様を変更することもできます)。
- または、13 以降だけでなく、すべての LangVersion を中断することもできます。
- 反復子が他のすべてのメソッドと同様に安全でないコンテキストを継承するようにすることで、ルールをより大幅に簡略化することもできます。 上述の内容について議論しました。
すべての言語バージョンで行うことも、単に
LangVersion >= 13
のためだけに行うことも可能です。
デザイン会議
- 2024-06-03: speclet の実装後のレビュー
C# feature specifications