"Ref en onveilig toestaan in iterators en asynchroon gebruik"

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante notities van de Language Design Meeting (LDM) .

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Problemen met kampioen: https://github.com/dotnet/csharplang/issues/1331

Samenvatting

Het gedrag tussen iterators en asynchrone methoden samenvoegen. Specifiek:

  • Sta ref/ref struct lokale variabelen en unsafe blokken toe in iterators en asynchrone methoden, op voorwaarde dat ze worden gebruikt in codesegmenten zonder yield of await.
  • Waarschuw voor yield binnen lock.

Motivatie

Het is niet nodig om ref/ref struct lokale variabelen en unsafe blokken in asynchrone/iteratormethoden uit te sluiten als ze niet worden gebruikt in yield of await, omdat ze niet gehesen hoeven te worden.

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

Belangrijke wijzigingen

Er zijn geen belangrijke wijzigingen in de taalspecificatie, maar er is één belangrijke wijziging in de Roslyn-implementatie (vanwege een schending van de specificaties).

Roslyn schendt het deel van de specificatie dat iterators een veilige context introduceren (§13.3.1). Als er bijvoorbeeld een unsafe class is met een iteratormethode die een lokale functie bevat, neemt die lokale functie de onveilige context over van de klasse, hoewel deze in een veilige context moet zijn geweest vanwege de iterator-methode. In feite heeft de hele iteratormethode de onveilige context overgenomen in Roslyn, het was gewoon niet toegestaan om onveilige constructies in iterators te gebruiken. In LangVersion >= 13zullen iterators een veilige context correct introduceren, omdat we in de toekomst onveilige constructies in iterators willen toestaan.

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

Notitie:

  • De onderbreking kan eenvoudig worden omzeild door de unsafe modifier toe te voegen aan de lokale functie.
  • Dit heeft geen invloed op lambdas omdat ze de 'iteratorcontext' overnemen en daarom was het onmogelijk om onveilige constructies erin te gebruiken.

Gedetailleerd ontwerp

De volgende wijzigingen zijn gekoppeld aan LangVersion, dat wil zeggen dat C# 12 en lager ref-achtige variabelen en unsafe blokken in asynchrone methoden en iterators blijven verbieden, en dat C# 13 deze beperkingen zal opheffen zoals hieronder beschreven. Specificatieclarificaties die in lijn zijn met de bestaande Roslyn-implementatie, moeten echter van toepassing zijn op alle LangVersions.

§13.3.1 Blokken > Algemeen:

Een blok met een of meer yield instructies (§13.15) wordt een iteratorblok genoemd, zelfs als deze yield instructies slechts indirect zijn opgenomen in geneste blokken (met uitzondering van geneste lambdas en lokale functies).

[...]

Het is een compilatiefout voor een iteratorblok dat een onveilige context bevat (§23.2). Een iteratorblok definieert altijd een veilige context, zelfs wanneer de declaratie is genest in een onveilige context. Het iteratorblok dat wordt gebruikt voor het implementeren van een iterator (§15.14) definieert altijd een veilige context, zelfs wanneer de iteratordeclaratie is genest in een onveilige context.

Uit deze specificatie volgt het ook:

  • Als een iteratordeclaratie is gemarkeerd met de unsafe modifier, bevindt de handtekening zich in een onveilig bereik, maar het iteratorblok dat wordt gebruikt om deze iterator te implementeren, definieert nog steeds een veilig bereik.
  • De set accessor van een iterator-eigenschap of indexeerfunctie (d.w.z. de get accessor wordt geïmplementeerd via een iteratorblok) "erft" zijn veilige/onveilige kader van de declaratie.
  • Dit heeft geen invloed op gedeeltelijke declaraties zonder implementatie omdat ze alleen handtekeningen zijn en geen iteratorbody kunnen hebben.

In C# 12 is het een fout om een iteratormethode te hebben die is gemarkeerd met de unsafe modifier, maar dat is toegestaan in C# 13 vanwege de wijziging van de specificatie.

Bijvoorbeeld:

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 Verwijzing naar lokale variabele-declaraties:

Het is een compilatiefout om een lokale ref-variabele of een variabele van een ref struct type te declareren binnen een methode die is gedeclareerd met de method_modifierasync, of binnen een iterator (§15.14).Het is een compileer- en gebruiksfout (zelfs impliciet in door de compiler gegenereerde code) om een lokale ref-variabele of een variabele van een ref struct type over await expressies of yield return instructies te verklaren en te gebruiken. Om precies te zijn, wordt de fout veroorzaakt door het volgende mechanisme: na een await expressie (§12.9.8) of een yield return instructie (§13.15) worden alle ref-variabelen en variabelen van een ref struct type in het bereik als definitief niet-toegewezen beschouwd (§9.4).

Houd er rekening mee dat deze fout niet wordt gereduceerd tot een waarschuwing in unsafe-contexten, zoals sommige andere referentieveiligheidsfouten in . Dat komt doordat deze ref-achtige lokale variabelen niet kunnen worden gemanipuleerd in unsafe contexten zonder te vertrouwen op implementatiedetails van hoe het herschrijven van de toestandsmachine werkt, waardoor deze fout buiten de criteria valt voor wat we willen verlagen naar waarschuwingen in unsafe contexten.

§15.14.1 Iterators > Algemeen:

Wanneer een functielid wordt geïmplementeerd met behulp van een iteratorblok, is het een compilatiefout voor de formele parameterlijst van het functielid om eventuele in, ref readonly, outof ref parameters, of een parameter van een ref struct type of een aanwijzertypeop te geven.

Er is geen wijziging in de specificatie nodig om unsafe blokken toe te staan die geen awaitin asynchrone methoden bevatten, omdat de specificatie nooit unsafe blokken in asynchrone methoden heeft toegestaan. De specificatie had echter altijd moeten verbieden dat await binnen unsafe-blokken verschijnt (het had al bepaald dat yield in unsafe in §13.3.1, zoals hierboven vermeld), wij stellen daarom de volgende wijziging voor in de specificatie:

§15.15.1 Asynchrone Functies > Algemeen:

Het is een compilatiefout voor de formele parameterlijst van een asynchrone functie om eventuele in, outof ref parameters of een parameter van een ref struct type op te geven.

Het is een compilatiefout voor een onveilige context (§23.2) om een await-expressie (§12.9.8) of een yield return instructie (§13.15) te bevatten.

§23.6.5 Het adres van de operator:

Er wordt een compilatiefout gerapporteerd voor het nemen van het adres van een lokale variabele of een parameter in een iterator.

Op dit moment wordt het nemen van een adres van een lokale of parameter in een asynchrone methode een waarschuwing in C# 12 waarschuwingsgolf.


Houd er rekening mee dat meer constructies mogelijk zijn dankzij ref dat binnen segmenten is toegestaan zonder await en yield in asynchrone/iterator-methoden, ook al zijn er geen specifieke wijzigingen voor hen nodig in de specificaties, omdat alles voortkomt uit de bovengenoemde specificatiewijzigingen.

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

Alternatieven

  • ref / ref struct lokalen kunnen alleen in blokken toegestaan zijn (§13.3.1) die geen await/yieldbevatten:

    // 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 binnen lock kan een fout zijn (zoals await binnen lock) of een waarschuwing voor een waarschuwingsgolf, maar dat zou een ingrijpende wijziging zijn: https://github.com/dotnet/roslyn/issues/72443. Houd er rekening mee dat de nieuwe Lock-objectgebaseerde lock compilatiefouten rapporteert voor yield returns in de hoofdtekst, omdat een dergelijke lock-instructie gelijk is aan een using op een ref struct die yield returns in de hoofdtekst niet toekent.

  • Variabelen binnen asynchrone of iteratormethoden mogen niet 'vast' zijn, maar eerder 'verplaatsbaar' als ze moeten worden gehesen naar velden van de statusmachine (vergelijkbaar met vastgelegde variabelen). Houd er rekening mee dat dit een bestaande bug in de specificatie is die onafhankelijk is van de rest van het voorstel, omdat unsafe blokken binnen async methoden altijd zijn toegestaan. Er is momenteel een waarschuwing hiervoor in C# 12-waarschuwingsgolf, en een fout zou een belangrijke wijziging zijn.

    §23.4 Vaste en verplaatsbare variabelen:

    In precieze termen is een vaste variabele een van de volgende:

    • Een variabele die voortvloeit uit een simple_name (§12.8.4) die verwijst naar een lokale variabele, waardeparameter, of parametermatrix, tenzij de variabele wordt vastgelegd door een anonieme functie (§12.19.6.2) of een lokale functie (§13.6.4) of de variabele moet worden gehesen als onderdeel van een asynchrone methode (§15.15) of een iterator (§15.14).
    • [...]
    • Momenteel hebben we een bestaande waarschuwing op C# 12 waarschuwingsniveau voor 'address-of' in asynchrone methoden en een voorgestelde fout voor 'address-of' in iterators die is gerapporteerd voor LangVersion 13 en hoger (hoeft niet te worden gerapporteerd in eerdere versies omdat het onmogelijk was om onveilige code in iterators te gebruiken). We zouden beide kunnen versoepelen om alleen van toepassing te zijn op variabelen die daadwerkelijk worden gehesen, niet op alle lokale variabelen en parameters.

    • Het kan mogelijk zijn om fixed te gebruiken om het adres van een gehesiste of vastgelegde variabele op te halen, hoewel het feit dat dit velden zijn een implementatiedetails is, zodat het in andere implementaties mogelijk niet mogelijk is om fixed erop te gebruiken. Houd er rekening mee dat we alleen overwegen om gelifte variabelen ook als "verplaatsbaar" te beschouwen, maar dat vastgelegde variabelen al "verplaatsbaar" waren en dat fixed niet voor hen was toegestaan.

  • We kunnen await/yield binnen unsafe toestaan, behalve binnen fixed-instructies (compiler kan variabelen niet vastmaken aan de grenzen van de methode). Dit kan leiden tot onverwachte gedrag, bijvoorbeeld in de buurt van stackalloc zoals beschreven in de onderliggende opsomming hieronder. Anders wordt het verplaatsen van pointers zelfs tegenwoordig in sommige scenario's ondersteund (zoals in een voorbeeld hieronder met betrekking tot pointers als argumenten), dus er mogen geen andere beperkingen in het toestaan hiervan zijn.

    • We zouden de onveilige variant van stackalloc in asynchrone/iteratormethoden kunnen verbieden, omdat de buffer die aan de stack is toegewezen niet blijft bestaan tijdens await/yield instructies. Het voelt niet nodig omdat onveilige code per ontwerp niet "gebruik na gratis" verhindert. Houd er rekening mee dat we ook onveilige stackalloc kunnen toestaan, mits deze niet wordt gebruikt in await/yield, maar dat kan lastig zijn om te analyseren (de resulterende aanwijzer kan worden doorgegeven in elke aanwijzervariabele). Ofwel zouden we kunnen vereisen dat het fixed is in asynchrone/iterator-methoden. Dat zou await/yield ontmoedigen, maar zou niet overeenkomen met de semantiek van fixed omdat de stackalloc expressie geen verplaatsbare waarde is. (Houd er rekening mee dat het niet onmogelijk is om het stackalloc resultaat in await/yield te gebruiken, net zoals u elke fixed aanwijzer vandaag kunt opslaan in een andere aanwijzervariabele en deze buiten het fixed blok kunt gebruiken.)
  • Iterator- en asynchrone methoden kunnen aanwijzerparameters hebben. Ze moeten geheven worden, maar dat moet geen probleem zijn omdat zelfs vandaag de dag het hijsen van verwijzingen wordt ondersteund, bijvoorbeeld:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • Het voorstel behoudt (en breidt/verduidelijkt) de reeds bestaande specificatie bij dat iteratormethoden een veilige context beginnen, zelfs als ze zich in een onveilige context bevinden. Een iteratormethode is bijvoorbeeld geen onveilige context, zelfs niet als deze is gedefinieerd in een klasse met de unsafe modifier. U kunt er ook voor zorgen dat iterators de unsafe modifier overnemen zoals bij andere methoden.

    • Voordeel: verwijdert complexiteit uit de specificatie en implementatie.
    • Voordeel: iterators uitlijnen met asynchrone methoden (een van de motivaties van de functie).
    • Nadeel: iterators binnen onveilige klassen kunnen geen yield return instructies bevatten, dergelijke iterators moeten worden gedefinieerd in een afzonderlijke gedeeltelijke klassedeclaratie zonder de unsafe modifier.
    • Nadeel: dit is een belangrijke wijziging in LangVersion=13 (iterators in onveilige klassen zijn toegestaan in C# 12).
  • In plaats van een iterator die alleen een veilige context voor de hoofdtekst definieert, kan de hele handtekening een veilige context zijn. Dat is niet consistent met de rest van de taal waarin body's normaal gesproken geen invloed hebben op declaraties, maar hier zou een declaratie veilig of onveilig zijn, afhankelijk van of de body een iterator is of niet. Het zou ook een belangrijke wijziging zijn in LangVersion=13, zoals in C# 12 iterator-handtekeningen onveilig zijn (ze kunnen bijvoorbeeld aanwijzermatrixparameters bevatten).

  • De unsafe wijziging toepassen op een iterator:

    • Kan zowel de hoofdtekst als de handtekening beïnvloeden. Dergelijke iterators zouden echter niet erg nuttig zijn omdat hun onveilige inhoud geen yield return's kon bevatten; ze konden alleen yield break's bevatten.
    • Er zou een fout kunnen zijn in LangVersion >= 13 zoals in LangVersion <= 12, omdat het niet erg nuttig is een onveilig iterator-lid te hebben; dit staat alleen pointer array parameters of onveilige setters toe zonder een extra onveilig blok. Normale aanwijzerargumenten kunnen echter in de toekomst worden toegestaan.
  • Roslyn-belangrijke wijziging:

    • We kunnen het huidige gedrag behouden (en zelfs de specificatie wijzigen zodat deze overeenkomt) bijvoorbeeld door de veilige context in de iterator-methode te introduceren, maar vervolgens terug te keren naar de onveilige context in de lokale functie.
    • Of we kunnen alle LangVersions breken, niet alleen 13 en nieuwer.
    • Het is ook mogelijk om de regels drastisch te vereenvoudigen door iterators onveilige context te laten overnemen zoals alle andere methoden doen. Hierboven besproken. Kan worden uitgevoerd in alle LangVersions of alleen voor LangVersion >= 13.

Ontwerpvergaderingen

  • 2024-06-03: evaluatie na implementatie van de speclet