次の方法で共有


反復子と非同期で ref と unsafe を許可する

手記

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

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

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

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

概要

反復子と非同期メソッドの動作を統一します。 具体的には:

  • コード セグメントで yieldawaitを使用しない場合に限り、反復子および非同期メソッドで 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 に保持する必要があります。

§13.3.1 ブロック > 一般:

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 */ }
    }
}

§13.6.2.4 参照ローカル変数宣言:

method_modifierasyncで宣言されたメソッド内、またはイテレータ(§15.14)内で ref ローカル変数または ref struct 型の変数を宣言するのは、コンパイル時エラーです。ref ローカル変数または ref struct 型の変数を、await 式や yield return ステートメントを跨いで宣言して使用するのは、コンパイル時エラーです。より具体的には、エラーは以下のプロセスによって発生します。await 式(§12.9.8)または yield return ステートメント(§13.15)の後、スコープ内のすべての ref ローカル変数と ref struct 型の変数は、確定的に未割り当てと見なされます(§9.4)。

このエラーは、他の ref 安全性エラー などの unsafe コンテキストでは警告にダウングレードされないことに注意してください。 これは、ステートマシン書き換えの実装詳細に依存せずに、refのようなローカルをunsafeコンテキストで操作できないために発生するエラーであり、このため、unsafeコンテキストで警告に変更する基準を超えています。

§15.14.1 反復子 > 一般:

関数メンバーが反復子ブロックを使用して実装されている場合、関数メンバーの仮パラメーター リストで、inref readonlyout、または ref パラメーター、または ref struct 型のパラメーター またはポインター型を指定するコンパイル時エラーです。

非同期メソッドに awaitを含まない unsafe ブロックを許可するために、仕様を変更する必要はありません。これは、スペックが非同期メソッドで unsafe ブロックを許可したことがないためです。 ただし、仕様は常に unsafe ブロック内の await を禁止する必要があります (前述のように、§13.3.1unsafe で既に yield を禁止していました)、次の仕様の変更を提案します。

§15.15.1 非同期関数 > 一般:

inout、または ref パラメーター、または ref struct 型のパラメーターを指定するのは、非同期関数の仮パラメーター リストのコンパイル時エラーです。

安全でないコンテキスト (§23.2) に await 式 (§12.9.8) または yield return ステートメント (§13.15) が含まれている場合のコンパイル時エラーです。

§23.6.5 演算子のアドレス:

反復子内のローカルまたはパラメーターのアドレスを取得すると、コンパイル時エラーが報告されます。

現時点では、非同期メソッドでローカルまたはパラメーターのアドレスを取得することは、C# 12 警告ウェーブの警告に当たります。


前述の仕様の変更からすべて除外されるため、仕様の変更は特に必要ありませんが、非同期/反復子メソッドで awaityield を行わずにセグメント内で許可される 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-based lock レポートでは、本文で yield return に対してコンパイル時エラーが報告されることに注意してください。これは、このような lock ステートメントは、その本体で yield return を許可しない ref structusing に相当するためです。

  • 非同期メソッドまたは反復子メソッド内の変数は、ステート マシンのフィールド (キャプチャされた変数と同様に) にホイストする必要がある場合は、"固定" ではなく"移動可能" にする必要があります。 これは、async メソッド内の unsafe ブロックが常に許可されているため、提案の残りの部分に依存しない仕様の既存のバグであることに注意してください。 現在、C# 12 警告ウェーブではこれに対する警告がされており、エラーにすることは破壊的変更になります。

    §23.4 固定変数と移動可能変数:

    正確に言うと、固定変数は次のいずれかです。

    • ローカル変数、値パラメータ、またはパラメータ配列を参照する 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のためだけに行うことも可能です。

デザイン会議