Allow ref and unsafe in iterators and async

Hinweis

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den relevanten Sprachentwurfsbesprechungen (LDM)-Notizen erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/1331

Zusammenfassung

Vereinheitlichen Sie das Verhalten zwischen Iteratoren und asynchronen Methoden. Dies gilt insbesondere in folgenden Fällen:

  • Zulassen von ref/ref struct Lokalen und unsafe Blöcken in Iteratoren und asynchronen Methoden, sofern sie in Codesegmenten ohne oder .yieldawait
  • Warnen Sie im yield Inneren lock.

Motivation

Es ist nicht erforderlich, Lokale und Blöcke in asynchronen/iterator-Methoden zu verbietenref/ref struct, wenn sie nicht quer unsafe verwendet werden oder yield, da sie nicht aufgegriffen werden müssen.await

async void M()
{
    await ...;
    ref int x = ...; // error previously, proposed to be allowed
    x.ToString();
    await ...;
    // x.ToString(); // still error
}

Bahnbrechende Änderungen

Es gibt keine wesentlichen Änderungen in der Sprachspezifikation, aber es gibt eine unterbrechungsbedingte Änderung der Roslyn-Implementierung (aufgrund einer Spezifikationsverletzung).

Roslyn verstößt gegen den Teil der Spezifikation, der besagt, dass Iteratoren einen sicheren Kontext (§13.3.1) einführen. Wenn beispielsweise eine unsafe class Iteratormethode vorhanden ist, die eine lokale Funktion enthält, erbt die lokale Funktion den unsicheren Kontext von der Klasse, obwohl sie aufgrund der Iteratormethode in einem sicheren Kontext vorhanden sein sollte. Tatsächlich hat die gesamte Iteratormethode den unsicheren Kontext in Roslyn geerbt, es war einfach nicht zulässig, unsichere Konstrukte in Iteratoren zu verwenden. In LangVersion >= 13iteratoren wird ein sicherer Kontext richtig eingeführt, da wir unsichere Konstrukte in Iteratoren zulassen möchten.

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

Hinweis:

  • Die Unterbrechung kann einfach durch Hinzufügen des unsafe Modifizierers zur lokalen Funktion bearbeitet werden.
  • Dies wirkt sich nicht auf Lambdas aus, da sie den "Iteratorkontext" erben, und daher war es unmöglich, unsichere Konstrukte darin zu verwenden.

Detailliertes Design

Die folgenden Änderungen sind an LangVersion gebunden, d. h. C# 12 und niedriger, lassen weiterhin ref-like locals und Blocks in asynchronen Methoden und unsafe Iteratoren zu, und C# 13 hebt diese Einschränkungen wie unten beschrieben auf. Spezifikationsklärungen, die mit der vorhandenen Roslyn-Implementierung übereinstimmen, sollten jedoch in allen LangVersions enthalten sein.

§13.3.1 Blöcke > Allgemein:

Ein Block , der eine oder yield mehrere Anweisungen (§13.15) enthält, wird als Iteratorblock bezeichnet, auch wenn diese yield Anweisungen nur indirekt in geschachtelten Blöcken enthalten sind (ausgenommen geschachtelte Lambdas und lokale Funktionen).

[...]

Es handelt sich um einen Kompilierungszeitfehler für einen Iteratorblock, der einen unsicheren Kontext (§23.2) enthält. Ein Iteratorblock definiert immer einen sicheren Kontext, auch wenn seine Deklaration in einem unsicheren Kontext geschachtelt ist. Der Iteratorblock, der zum Implementieren eines Iterators (§15.14) verwendet wird, definiert immer einen sicheren Kontext, auch wenn die Iteratordeklaration in einem unsicheren Kontext geschachtelt ist.

Aus dieser Spezifikation folgt es auch:

  • Wenn eine Iteratordeklaration mit dem unsafe Modifizierer gekennzeichnet ist, befindet sich die Signatur in einem unsicheren Bereich, aber der Iteratorblock, der zum Implementieren dieses Iterators verwendet wird, definiert weiterhin einen sicheren Bereich.
  • Der set Accessor einer Iteratoreigenschaft oder eines Indexers (d. h. der get Accessor wird über einen Iteratorblock implementiert) "erbt" seinen sicheren/unsicheren Bereich von der Deklaration.
  • Dies wirkt sich nicht auf Partielle Deklarationen ohne Implementierung aus, da sie nur Signaturen sind und keinen Iteratortext aufweisen können.

Beachten Sie, dass es in C# 12 ein Fehler ist, eine Iteratormethode mit dem unsafe Modifizierer gekennzeichnet zu haben, die in C# 13 aufgrund der Spezifikationsänderung jedoch zulässig ist.

Beispiel:

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 Ref local variable declarations:

Es handelt sich um einen Kompilierungsfehler, um eine lokale Referenzvariable oder eine Variable eines ref struct Typs innerhalb einer methode zu deklarieren, die mit dem method_modifierasync deklariert wurde, oder innerhalb eines Iterators (§15.14).Es handelt sich um einen Kompilierungszeitfehler, um eine lokale Referenzvariable oder eine Variable eines ref struct Typs über Ausdrücke oder yield return Anweisungen hinweg await zu deklarieren und zu verwenden (auch implizit in compilersynthetisiertem Code). Genauer gesagt wird der Fehler durch den folgenden Mechanismus gesteuert: Nach einem await Ausdruck (§12.9.8) oder einer yield return Anweisung (§13.15) werden alle lokalen Referenzvariablen und Variablen eines ref struct Typs im Bereich als definitiv nicht zugewiesen (§9.4) betrachtet.

Beachten Sie, dass dieser Fehler nicht in unsafe Kontexten wie einigen anderen Verweissicherheitsfehlern auf eine Warnung herabgestuft wird. Das liegt daran, dass diese verweisähnlichen Gebietsschemas nicht in unsafe Kontexten bearbeitet werden können, ohne sich auf Implementierungsdetails der Funktionsweise des Zustandscomputers zu verlassen, daher liegt dieser Fehler außerhalb der Grenzen, die wir in Kontexten auf unsafe Warnungen herabstufen möchten.

§15.14.1 Iteratoren > Allgemein:

Wenn ein Funktionselement mithilfe eines Iteratorblocks implementiert wird, handelt es sich um einen Kompilierungszeitfehler für die formale Parameterliste des Funktionsmememers, um beliebige in, ref readonly, , outoder ref Parameter oder einen Parameter eines ref struct Typs oder zeigertyps anzugeben.

Es ist keine Änderung der Spezifikation erforderlich, um Blöcke zuzulassen unsafe , die keine Blöcke in asynchronen Methoden enthalten await, da die Spezifikation nie unzulässige Blöcke in asynchronen unsafe Methoden enthält. Die Spezifikation sollte jedoch immer innerhalb von await Blöcken nicht zulässig unsafe sein (sie war in yieldunsafe wie oben erwähnt bereits nicht zulässig), daher schlagen wir die folgende Änderung der Spezifikation vor:

§15.15.1 Asynchrone Funktionen > Allgemein:

Es handelt sich um einen Kompilierungszeitfehler für die formale Parameterliste einer asynchronen Funktion, um beliebige inParameter outoder ref Parameter eines ref struct Typs anzugeben.

Es handelt sich um einen Kompilierungszeitfehler für einen unsicheren Kontext (§23.2), um einen await Ausdruck (§12.9.8) oder eine yield return Anweisung (§13.15) zu enthalten.

§23.6.5 Die Anschrift des Betreibers:

Ein Kompilierungszeitfehler wird gemeldet, um eine Adresse eines lokalen Parameters oder eines Parameters in einem Iterator zu übernehmen.

Derzeit ist das Verwenden einer Adresse eines lokalen parameters oder eines Parameters in einer asynchronen Methode eine Warnung in der Warnungswelle C# 12.


Beachten Sie, dass mehr Konstrukte dank ref zulässiger Innerhalb von Segmenten ohne await und yield in asynchronen/iterator-Methoden funktionieren können, obwohl keine Spezifikationsänderung speziell für sie erforderlich ist, da alle aus den oben genannten Spezifikationsänderungen herausfallen:

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();
    }
}

Alternativen

  • ref / ref structLocals können nur in Blöcken (§13.3.1yield

    // 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();
    }
    
  • yield return innen lock könnte ein Fehler (wie await innen lock ) oder eine Warnungswelle sein, aber das wäre eine fehlerhafte Änderung: https://github.com/dotnet/roslyn/issues/72443. Beachten Sie, dass die neue Lockobjektbasierte lockyield return

  • Variablen innerhalb von asynchronen oder Iteratormethoden sollten nicht "fixiert" sein, sondern "verschiebebar", wenn sie auf Felder des Zustandsautomaten (ähnlich wie erfasste Variablen) verschoben werden müssen. Beachten Sie, dass dies ein bereits vorhandener Fehler in der Spezifikation unabhängig vom rest des Vorschlags ist, da unsafe Blöcke innerhalb async von Methoden immer zulässig waren. Es gibt derzeit eine Warnung dafür in C# 12-Warnwelle und macht ihn zu einem Fehler, der zu einer fehlerhaften Änderung führen würde.

    §23.4 Feste und verschiebebare Variablen:

    Genauer gesagt ist eine feste Variable eine der folgenden Variablen:

    • Derzeit gibt es eine Warnung in C# 12-Warnwelle für die Adressierung von asynchronen Methoden und einen vorgeschlagenen Fehler für adressierte Iteratoren, die für LangVersion 13+ gemeldet wurden (muss in früheren Versionen nicht gemeldet werden, da es unmöglich war, unsicheren Code in Iteratoren zu verwenden). Wir konnten beides entspannen, um nur auf Variablen anzuwenden, die tatsächlich angehalten werden, nicht auf alle Lokalen und Parameter.

    • Es könnte möglich sein, fixed die Adresse einer heistierten oder erfassten Variablen abzurufen, obwohl die Tatsache, dass es sich bei diesen Feldern um ein Implementierungsdetail handelt, in anderen Implementierungen möglicherweise nicht verwendet fixed werden kann. Beachten Sie, dass wir nur vorschlagen, auch verschobene Variablen als "moveable" zu berücksichtigen, aber erfasste Variablen waren bereits "verschiebebar" und fixed waren für sie nicht zulässig.

  • Wir können innerhalb await außer innerhalb von / Anweisungen zulassenyieldunsafefixed(Compiler kann Variablen nicht über Methodengrenzen hinweg anheften). Dies kann zu unerwartetem Verhalten führen, z stackalloc . B. wie im geschachtelten Aufzählungspunkt unten beschrieben. Andernfalls wird das Heben von Zeigern auch heute in einigen Szenarien unterstützt (es gibt ein Beispiel unten im Zusammenhang mit Zeigern als Argumente), sodass es keine anderen Einschränkungen geben sollte, um dies zuzulassen.

    • Wir konnten die unsichere Variante der stackalloc asynchronen/iterator-Methoden nicht zulassen, da der vom Stapel zugewiesene Puffer nicht über await/yield Anweisungen hinweg lebt. Es ist nicht notwendig, weil unsicherer Code von Design nicht verhindert wird, dass "nach der kostenlosen Verwendung" verwendet wird. Beachten Sie, dass wir auch unsicher stackalloc zulassen können, sofern sie nicht verwendet await/yieldwird, aber das kann schwierig zu analysieren sein (der resultierende Zeiger kann in einer beliebigen Zeigervariable übergeben werden). Oder wir könnten dies fixed in asynchronen/iterator-Methoden erfordern. Dies würde davon abhalten, es querawait/yield zu verwenden, würde aber nicht mit der Semantik übereinstimmen, da fixed der stackalloc Ausdruck kein verschiebebarer Wert ist. (Beachten Sie, dass es nicht unmöglich wärefixed.)
  • Iterator- und asynchrone Methoden können Zeigerparameter aufweisen. Sie müssten aufgetragen werden, aber das sollte kein Problem sein, da hebezeige Zeiger auch heute unterstützt werden, zum Beispiel:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • Der Vorschlag hält derzeit (und erweitert/klärt) die bereits vorhandene Spezifikation, dass Iteratormethoden einen sicheren Kontext beginnen, auch wenn sie sich in einem unsicheren Kontext befinden. Eine Iteratormethode ist z. B. kein unsicherer Kontext, auch wenn sie in einer Klasse definiert ist, die den unsafe Modifizierer aufweist. Alternativ könnten iteratoren den unsafe Modifizierer wie andere Methoden "erben".

    • Vorteil: Entfernt komplexität aus der Spezifikation und Implementierung.
    • Vorteil: Richtet Iteratoren mit asynchronen Methoden (einer der Motivationen des Features) aus.
    • Nachteil: Iteratoren innerhalb unsicherer Klassen konnten keine Anweisungen enthalten yield return , solche Iteratoren müssten in einer separaten partiellen Klassendeklaration ohne den unsafe Modifizierer definiert werden.
    • Nachteil: Dies wäre eine bahnbrechende Änderung in LangVersion=13 (Iteratoren in unsicheren Klassen sind in C# 12 zulässig).
  • Anstelle eines Iterators, der nur einen sicheren Kontext für den Textkörper definiert, könnte die gesamte Signatur ein sicherer Kontext sein. Dies ist inkonsistent mit der restlichen Sprache in diesem Text, in der Regel keine Deklarationen betroffen, aber hier wäre eine Deklaration entweder sicher oder unsicher, je nachdem, ob der Text ein Iterator ist oder nicht. Es wäre auch eine unterbrechungslose Änderung in LangVersion=13 wie in C# 12 Iteratorsignaturen unsicher (sie können z. B. Zeigerarrayparameter enthalten).

  • Anwenden des unsafe Modifizierers auf einen Iterator:

    • Kann sowohl den Textkörper als auch die Signatur beeinflussen. Solche Iteratoren wären nicht sehr nützlich, obwohl ihre unsicheren Körper nicht enthalten yield returnkonnten, sie könnten nur yield breaks haben.
    • Es kann sich um einen Fehler handeln LangVersion >= 13LangVersion <= 12 , da es nicht sehr nützlich ist, ein unsicheres Iteratorelement zu haben, da nur ein Zeigerarrayparameter oder unsichere Setter ohne zusätzlichen unsicheren Block vorhanden sein kann. Normale Zeigerargumente könnten jedoch in Zukunft zulässig sein.
  • Roslyn breaking change:

    • Wir könnten das aktuelle Verhalten beibehalten (und sogar die Spezifikation ändern, um sie abzugleichen), indem wir beispielsweise den sicheren Kontext in der Iteratormethode einführen, dann aber in der lokalen Funktion auf den unsicheren Kontext zurücksetzen.
    • Oder wir konnten alle LangVersions brechen, nicht nur 13 und höher.
    • Es ist auch möglich, die Regeln drastischer zu vereinfachen, indem Iteratoren unsicheren Kontext erben, wie alle anderen Methoden. Oben erläutert. Kann in allen LangVersions oder nur für LangVersion >= 13.

Planungsbesprechungen

  • 2024-06-03: Überprüfung der Spezifikation nach der Implementierung