Tillåt ref och unsafe i iteratorer och asynkron

Obs

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningarna (språkkonstruktionsmöte, Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-problem: https://github.com/dotnet/csharplang/issues/1331

Sammanfattning

Förena beteendet mellan iteratorer och asynkrona metoder. Specifikt:

  • Tillåt ref/ref struct lokala och unsafe block i iteratorer och asynkrona metoder förutsatt att de används i kodsegment utan yield eller await.
  • Varna om yield inuti lock.

Motivation

Det är inte nödvändigt att förbjuda ref/ref struct lokala variabler och unsafe block i asynkrona/iteratormetoder om de inte används över yield eller await, eftersom de inte behöver höjas.

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

Brytande ändringar

Det finns inga icke-bakåtkompatibla ändringar i språkspecifikationen, men det finns en icke-bakåtkompatibel ändring i Roslyn-implementeringen (på grund av en specifikationsöverträdelse).

Roslyn bryter mot den del av specifikationen som anger att iteratorer inför en säker kontext (§13.3.1). Om det till exempel finns en unsafe class med en iteratormetod som innehåller en lokal funktion ärver den lokala funktionen den osäkra kontexten från klassen, även om den borde ha varit i en säker kontext enligt specifikationen på grund av iteratormetoden. Faktum är att hela iteratormetoden ärvde den osäkra kontexten i Roslyn, det var bara otillåtet att använda osäkra konstruktioner i iteratorer. I LangVersion >= 13introducerar iteratorer korrekt en säker kontext eftersom vi vill tillåta osäkra konstruktioner i iteratorer.

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

Observera:

  • Pausen kan bara bearbetas genom att lägga till unsafe-modifieraren i den lokala funktionen.
  • Detta påverkar inte lambdas eftersom de "ärver" "iteratorkontexten" och därför var det omöjligt att använda osäkra konstruktioner inuti dem.

Detaljerad design

Följande ändringar är knutna till LangVersion, d.v.s. C# 12 och lägre kommer att fortsätta att tillåta ref-liknande lokalbefolkningen och unsafe block i asynkrona metoder och iteratorer, och C# 13 lyfter dessa begränsningar enligt beskrivningen nedan. Specifikationsförtydliganden som matchar den befintliga Roslyn-implementeringen bör dock gälla för alla språkversioner.

§13.3.1 Block > Allmänt:

Ett block som innehåller en eller flera yield -instruktioner (§13.15) kallas ett iteratorblock, även om dessa yield-instruktioner endast indirekt finns i kapslade block (exklusive kapslade lambdas och lokala funktioner).

[...]

Det är ett kompileringsfel för ett iteratorblock som innehåller en osäker kontext (§23.2). Ett iteratorblock definierar alltid en säker kontext, även när dess deklaration är kapslad i en osäker kontext. Iteratorblocket som används för att implementera en iterator (§15.14) definierar alltid en säker kontext, även när iteratordeklarationen är kapslad i ett osäkert sammanhang.

Från den här specifikationen följer den också:

  • Om en iteratordeklaration markeras med unsafe-modifieraren finns signaturen i ett osäkert omfång, men iteratorblocket som används för att implementera iteratorn definierar fortfarande ett säkert omfång.
  • Den set-accessorn för en iteratoregenskap eller indexerare (dvs. dess get-accessor implementeras via ett iteratorblock) "ärver" dess säkra/osäkra omfång från deklarationen.
  • Detta påverkar inte partiella deklarationer utan implementering eftersom de bara är signaturer och inte kan ha en iteratortext.

Observera att det i C# 12 är ett fel att ha en iteratormetod markerad med unsafe-modifieraren, men det tillåts i C# 13 på grund av specifikationsändringen.

Till exempel:

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 lokala variabeldeklarationer:

Det är ett kompileringsfel att deklarera en lokal referensvariabel, eller en variabel av ref struct typ, inom en metod som deklarerats med method_modifierasynceller inom en iterator (§15.14).Det är ett kompileringsfel att deklarera och använda (även implicit i kompilatorsyntetiserad kod) en referens lokal variabel eller en variabel av en ref struct typ över await uttryck eller yield return-instruktioner. Mer exakt drivs felet av följande mekanism: efter ett await uttryck (§12.9.8) eller en yield return -instruktion (§13.15), anses alla ref lokala variabler och variabler av en ref struct typ i omfång definitivt vara otilldelade (§9.4).

Observera att det här felet inte nedgraderas till en varning i unsafe kontexter som andra referenssäkerhetsfel. Det beror på att dessa referensliknande lokala variabler inte kan manipuleras i unsafe-kontexter utan att förlita sig på implementeringsdetaljer om hur omskrivningen av tillståndsmaskinen fungerar. Därför faller det här felet utanför vad vi vill nedgradera till varningar i unsafe-kontexter.

§15.14.1 Iteratorer > Allmänt:

När en funktionsmedlem implementeras med hjälp av ett iteratorblock, är det ett kompileringsfel för den formella parameterlistan för funktionsmedlemmen att specificera några in, ref readonly, outeller ref parametrar, eller en parameter av typen ref struct, eller av en pekartyp.

Ingen ändring i specifikationen krävs för att tillåta unsafe block som inte innehåller awaits i asynkrona metoder, eftersom specifikationen aldrig har otillåtna unsafe block i asynkrona metoder. Specifikationen bör dock alltid ha förbjudit await inuti unsafe block (den hade redan otillåtit yield i unsafe i §13.3.1 enligt ovan), så vi föreslår följande ändring av specifikationen:

§15.15.1 Asynkrona Funktioner > Allmänt:

Det är ett kompileringsfel för den formella parameterlistan för en asynkron funktion för att ange eventuella in, outeller ref parametrar eller någon parameter av ref struct typ.

Det är ett kompileringsfel för ett osäkert sammanhang (§23.2) att innehålla ett await uttryck (§12.9.8) eller en yield return -instruktion (§13.15).

§23.6.5 Operatörens adress:

Ett kompileringsfel rapporteras för att ta en adress till en lokal eller en parameter i en iterator.

Att ta adressen till en lokal variabel eller en parameter i en asynkron metod är för närvarande en varning i C# 12-varningsvågen.


Observera att fler konstruktionstyper kan fungera eftersom ref tillåts inom segment utan await och yield i asynkrona/iteratormetoder, även om ingen specifikationsändring krävs specifikt för dem eftersom dessa ändringar alla härrör från de ovan nämnda specifikationsändringarna.

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

Alternativ

  • ref / ref struct lokala variabler får endast tillåtas i block (§13.3.1) som inte innehåller await/yield:

    // 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 inuti lock kan vara ett fel (som await inuti lock är) eller en varningsvågsvarning, men det skulle vara en icke-bakåtkompatibel ändring: https://github.com/dotnet/roslyn/issues/72443. Observera att de nya Lock-objektbaserade lock-rapporterna ger kompileringstidsfel för yield returns i sin brödtext, eftersom sådan lock-instruktion motsvarar en using på en ref struct och inte tillåter yield returns i sin brödtext.

  • Variabler i asynkrona metoder eller iteratormetoder bör inte vara "fasta" utan snarare "flyttbara" om de behöver hissas till fält på tillståndsdatorn (på samma sätt som insamlade variabler). Observera att detta är en befintlig bugg i specifikationen oberoende av resten av förslaget eftersom unsafe block inuti async metoder alltid var tillåtna. Det finns för närvarande en varning för detta i C# 12-varningsvågen och att göra det till ett fel skulle vara en icke-bakåtkompatibel ändring.

    §23.4 Fasta och flyttbara variabler:

    I exakta termer är en fast variabel något av följande:

    • En variabel som härrör från en simple_name (§12.8.4) som refererar till en lokal variabel, värdeparameter, eller parametermatris, såvida inte variabeln fångas upp av en anonym funktion (§12.19.6.2) eller en lokal funktion (§13.6.4) eller variabeln måste hissas som en del av en asynkron (§15.15) eller en iterator (§15.14) metod.
    • [...]
    • För närvarande har vi en befintlig varning i C# 12-varningskategori för adressering i asynkrona metoder och ett föreslaget fel för adressering i iteratorer som rapporterats för LangVersion 13+ (behöver inte rapporteras i tidigare versioner eftersom det var omöjligt att använda osäker kod i iteratorer). Vi kan lätta på båda dessa för att endast gälla variabler som faktiskt hissas, inte alla lokala variabler och parametrar.

    • Det kan vara möjligt att använda fixed för att hämta adressen för en hissad eller infångad variabel, även om det faktum att det är fält är en implementeringsinformation, så i andra implementeringar kanske det inte går att använda fixed på dem. Observera att vi bara föreslår att även hissade variabler ska anses vara "flyttbara", men insamlade variabler var redan "flyttbara" och fixed tilläts inte för dem.

  • Vi kan tillåta await/yield inuti unsafe förutom i fixed -instruktioner (kompilatorn kan inte fästa variabler över metodgränser). Det kan resultera i ett oväntat beteende, till exempel omkring stackalloc, så som beskrivs i den kapslade punkten nedan. Annars stöds lyftpekare även i dag i vissa scenarier (det finns ett exempel nedan som rör pekare som argument), så det bör inte finnas några andra begränsningar för att tillåta detta.

    • Vi kan inte tillåta den osäkra varianten av stackalloc i asynkrona/iteratormetoder, eftersom den stackallokerade bufferten inte finns i await/yield-instruktioner. Det känns inte nödvändigt eftersom osäker kod i designen inte förhindrar "användning efter frigöring". Observera att vi också kan tillåta osäkra stackalloc förutsatt att den inte används i await/yield, men det kan vara svårt att analysera (den resulterande pekaren kan skickas runt i valfri pekarvariabel). Eller så kan vi kräva att det ska vara fixed i asynkrona/iteratormetoder. Det skulle avskräcka att använda det över await/yield men skulle inte matcha semantiken i fixed eftersom stackalloc-uttrycket inte är ett flyttbart värde. (Observera att det inte skulle vara omöjligt att använda stackalloc resultat över await/yield på samma sätt som du kan spara alla fixed pekare i dag i en annan pekarvariabel och använda den utanför fixed-blocket.)
  • Iterator- och asynkrona metoder kan tillåtas ha pekarparametrar. De skulle behöva hissas, men det bör inte vara ett problem eftersom lyftpekare stöds även i dag, till exempel:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • Förslaget behåller för närvarande (och utökar/förtydligar) den befintliga specifikationen att iteratormetoder startar en säker kontext även om de befinner sig i ett osäkert sammanhang. En iteratormetod är till exempel inte en osäker kontext även om den definieras i en klass som har unsafe-modifieraren. Alternativt kan vi få iteratorer att ärva unsafe-modifieraren som andra metoder gör.

    • Fördel: tar bort komplexiteten från specifikationen och implementeringen.
    • Fördel: anpassar iteratorer med asynkrona metoder (en av drivkrafterna för denna funktion).
    • Nackdel: iteratorer i osäkra klasser kunde inte innehålla yield return-instruktioner, sådana iteratorer måste definieras i en separat partiell klassdeklaration utan unsafe modifieraren.
    • Nackdel: detta skulle vara en icke-bakåtkompatibel ändring i LangVersion=13 (iteratorer i osäkra klasser tillåts i C# 12).
  • I stället för en iterator som definierar en säker kontext endast för brödtexten kan hela signaturen vara en säker kontext. Detta är oförenligt med resten av språket i att organ normalt inte påverkar deklarationer, men här skulle en deklaration vara antingen säker eller osäker beroende på om kroppen är en iterator eller inte. Det skulle också vara en icke-bakåtkompatibel ändring i LangVersion=13 eftersom iteratorsignaturerna i C# 12 är osäkra (de kan till exempel innehålla parametrar för pekarmatrisen).

  • Tillämpa unsafe-modifieraren på en iterator:

    • Kan påverka både brödtexten och signaturen. Sådana iteratorer skulle dock inte vara särskilt användbara eftersom deras osäkra sektioner inte kunde innehålla yield return:er, de kunde bara ha yield break:er.
    • Kan vara ett fel i LangVersion >= 13 som det är i LangVersion <= 12 eftersom det inte är särskilt användbart att ha en osäker iteratormedlem eftersom det bara tillåter en att ha pekarmatrisparametrar eller osäkra setters utan ytterligare osäkert block. Men normala pekarargument kan tillåtas i framtiden.
  • Roslyn brytande ändring:

    • Vi kan bevara det aktuella beteendet (och till och med ändra specifikationen så att den matchar den) till exempel genom att introducera den säkra kontexten i iteratormetoden men sedan återgå till den osäkra kontexten i den lokala funktionen.
    • Eller så kan vi bryta alla LangVersions, inte bara 13 och senare.
    • Det är också möjligt att mer drastiskt förenkla reglerna genom att göra iteratorer ärva osäker kontext, precis som alla andra metoder gör. Beskrivs ovan. Det kan göras i alla språkversioner eller bara för LangVersion >= 13.

Designa möten

  • 2024-06-03: granskning efter implementeringen av specifikationen