Povolit referenční a *unsafe* v iterátorech a asynchronních funkcích.

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách ze schůzky o návrhu jazyka (LDM) č. .

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Problém šampiona: https://github.com/dotnet/csharplang/issues/1331

Shrnutí

Sjednoťte chování mezi iterátory a asynchronními metodami. Konkrétně:

  • Povolte ref/ref struct místní prostředí a bloky unsafe v iterátorech a asynchronních metodách za předpokladu, že se používají v segmentech kódu bez jakýchkoli yield nebo await.
  • Varovat před yield uvnitř lock.

Motivace

Není nutné zakázat ref/ref struct lokální proměnné a unsafe bloky v asynchronních nebo iterátorových metodách, pokud se nepoužívají napříč yield nebo await, protože není nutné je přesouvat nahoru.

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

Zásadní změny

Ve specifikaci jazyka nejsou žádné zásadní změny, ale v implementaci Roslyn došlo k jedné zásadní změně (kvůli porušení specifikace).

Roslyn porušuje část specifikace, která uvádí, že iterátory představují bezpečný kontext (§13.3.1). Například pokud existuje unsafe class s metodou iterátoru, která obsahuje lokální funkci, pak tato lokální funkce dědí nebezpečný kontext z třídy, ačkoli by měl být v bezpečném kontextu vzhledem k metodě iterátoru podle specifikace. Ve skutečnosti celá metoda iterátoru zdědila nebezpečný kontext v Roslynu, bylo prostě zakázáno používat všechny nebezpečné konstruktory v iterátorech. V LangVersion >= 13iterátory správně zavádějí bezpečný kontext, protože chceme povolit nebezpečné konstrukce v iterátorech.

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

Poznámka:

  • Lze se tomu vyhnout jednoduše přidáním modifikátoru unsafe do místní funkce.
  • To nemá vliv na lambda, protože "dědí" kontext "iterátoru", a proto nebylo možné používat nebezpečné konstrukty uvnitř nich.

Podrobný návrh

Následující změny jsou vázány na LangVersion, tj. C# 12 a nižší budou nadále zakázány ref-like locals a unsafe bloky v asynchronních metodách a iterátory a C# 13 tato omezení zruší, jak je popsáno níže. Ačkoli upřesnění specifikací, která odpovídají stávající implementaci Roslyn, by měla platit ve všech verzích jazyka.

§13.3.1 Bloky > Obecné:

Blok , který obsahuje jeden nebo více příkazů yield (§13.15) se nazývá blok iterátoru, i když jsou tyto příkazy yield obsaženy pouze nepřímo v vnořených blocích (s výjimkou vnořených lambda a místních funkcí).

[...]

Jedná se o chybu v době kompilace, kdy blok iterátoru obsahuje nebezpečný kontext (§23.2). Blok iterátoru vždy definuje bezpečný kontext, i když je jeho deklarace vnořena do nebezpečného kontextu. Blok iterátoru použitý k implementaci iterátoru (§15.14) vždy definuje bezpečný kontext, i když je deklarace iterátoru vnořená do nebezpečného kontextu.

Z této specifikace také následuje:

  • Pokud je deklarace iterátoru označena modifikátorem unsafe, podpis je v nebezpečném oboru, ale blok iterátoru použitý k implementaci tohoto iterátoru stále definuje bezpečný obor.
  • Přístupová metoda set vlastnosti iterátoru nebo indexeru (tj. jeho get přístupová metoda je implementována prostřednictvím bloku iterátoru) přejímá svůj bezpečný/nebezpečný obor z deklarace.
  • To nemá vliv na částečné deklarace bez implementace, protože jsou pouze signatury a nemohou mít tělo iterátoru.

Všimněte si, že v jazyce C# 12 se jedná o chybu, že metoda iterátoru je označená modifikátorem unsafe, ale je povolená v jazyce C# 13 kvůli změně specifikace.

Například:

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 Referenční deklarace místních proměnných:

Jedná se o chybu v době kompilace deklarování místní proměnné ref nebo proměnné typu ref struct v rámci metody deklarované pomocí method_modifierasync, nebo v rámci iterátoru (§15.14).Jedná se o chybu v době kompilace, která deklaruje a používá (i implicitně v syntetizovaném kódu kompilátoru) místní proměnnou ref nebo proměnnou typu ref struct napříč výrazy await nebo příkazy yield return. Přesněji řečeno, chyba je řízena následujícím mechanismem: za výrazem await (§12.9.8) nebo yield return příkazem (§13.15), všechny místní proměnné ref a proměnné ref struct typu v rozsahu jsou považovány za jednoznačně nepřiřazené (§9.4).

Všimněte si, že tato chyba není převedena na upozornění v kontextech unsafe, jako některé jiné chyby zabezpečení ref . Důvodem je to, že tyto místní hodnoty podobné odkazu nelze manipulovat v kontextu unsafe, aniž byste se museli spoléhat na podrobnosti implementace fungování přepisu stavového počítače, a proto tato chyba spadá mimo hranice toho, co chceme downgradovat na upozornění v unsafe kontextech.

§15.14.1 Iterátory > Obecné:

Pokud je člen funkce implementován pomocí bloku iterátoru, jedná se o chybu v době kompilace pro formální seznam parametrů člena funkce k určení libovolného in, ref readonly, outnebo ref parametrů nebo parametr ref struct typu nebo typu ukazatele.

Není nutná žádná změna specifikace, aby bylo možné povolit unsafe bloků, které neobsahují awaitv asynchronních metodách, protože specifikace nikdy nepovolila unsafe bloků v asynchronních metodách. Specifikace však měla vždy zakazovat await uvnitř unsafe bloků (již zakázala yield v unsafe v §13.3.1, jak bylo citováno výše), takže navrhujeme následující změnu specifikace:

§15.15.1 Asynchronní funkce > Obecné:

Jedná se o chybu v době kompilace pro formální seznam parametrů asynchronní funkce k určení libovolného in, outnebo ref parametrů nebo libovolného parametru typu ref struct.

Jedná se o chybu při kompilaci, když nebezpečný kontext (§23.2) obsahuje výraz await (§12.9.8) nebo příkaz yield return (§13.15).

§23.6.5 Adresa operátora:

Při kompilaci bude hlášena chyba při získání adresy lokální proměnné nebo parametru v iterátoru.

V současné době je pořízení adresy místního nebo parametru v asynchronní metodě upozornění ve vlně upozornění C# 12.


Všimněte si, že díky povolení ref uvnitř segmentů bez await a yield v asynchronních/iterátorových metodách může fungovat více konstrukcí, i když není potřebná žádná specifická změna specifikace pro ně, protože to vše vyplývá z výše uvedených změn specifikace.

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

Alternativy

  • ref / ref struct lokály mohou být povoleny pouze v blocích (§13.3.1), které neobsahují 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 uvnitř lock může být chyba (například await uvnitř lock) nebo upozornění na vlnovou vlnu, ale to by byla zásadní změna: https://github.com/dotnet/roslyn/issues/72443. Všimněte si, že nový Lockobjektový lock hlásí chyby při kompilaci pro yield returnv jeho těle, protože takový lock příkaz je ekvivalentní using na ref struct, který vylučuje yield returnv těle.

  • Proměnné uvnitř asynchronních nebo iterátorových metod by neměly být "pevné", ale "přesunoutelné", pokud je potřeba je přesunout do polí stavového počítače (podobně jako zachycené proměnné). Všimněte si, že se jedná o již existující chybu ve specifikaci nezávisle na zbytku návrhu, protože unsafe bloky uvnitř async metod byly vždy povoleny. V současné době je upozornění pro toto ve varovné vlně C# 12 a učinění z něj chyby by představovalo narušení kompatibility.

    §23.4 Pevné a pohyblivé proměnné:

    Pevná proměnná je přesně jedna z následujících možností:

    • Proměnná vyplývající z simple_name (§12.8.4), která odkazuje na místní proměnnou, parametr hodnoty, nebo pole parametrů, pokud proměnná není zachycena anonymní funkcí (§12.19.6.2) nebo místní funkce (§13.6.4) nebo proměnná musí být v rámci asynchronní metody (§15.15) nebo iterátor (§15.14) metody.
    • [...]
    • V současné době máme v rámci vlny varování C# 12 varování týkající se operátoru adresy v asynchronních metodách a navrhovanou chybu pro operátor adresy v iterátorech, hlášenou pro LangVersion 13+ (není nutné ji hlásit v dřívějších verzích, protože v iterátorech nebylo možné používat nebezpečný kód). Mohli bychom uvolnit obě omezení, aby se vztahovala pouze na proměnné, které jsou skutečně zvednuté, nikoli na všechny lokální proměnné a parametry.

    • Je možné použít fixed k získání adresy zvýšené nebo zachycené proměnné, i když skutečnost, že jsou to pole v detailech implementace, znamená, že v jiných implementacích nemusí být možné na ně použít fixed. Všimněte si, že navrhujeme zvážit také vyzdvižené proměnné jako „přesouvatelné“, ale zachycené proměnné již byly považovány za „přesouvatelné“ a fixed pro ně nebylo povoleno.

  • Můžeme povolit await/yield uvnitř unsafe s výjimkou příkazů fixed (kompilátor nemůže připnout proměnné přes hranice metody). To může mít za následek neočekávané chování, například kolem stackalloc, jak je popsáno ve vnořeném odrážkovém bodu uvedeném níže. V některých scénářích se podpora natahování ukazatelů používá i dnes (níže je uveden příklad související s ukazateli jako argumenty), takže by neměla existovat žádná další omezení, která by to umožňovala.

    • Mohli bychom zakázat nebezpečnou variantu stackalloc v asynchronních/iterátorových metodách, protože vyrovnávací paměť alokovaná na zásobníku nepřetrvává přes příkazy await/yield. Není to nutné, protože nebezpečný kód záměrně nezabrání "použití po volném". Všimněte si, že bychom také mohli povolit nebezpečný stackalloc za předpokladu, že se nepoužívá napříč await/yield, ale to by mohlo být obtížné analyzovat (výsledný ukazatel může být přenášen v libovolné proměnné ukazatele). Nebo bychom mohli požadovat, aby fixed bylo v asynchronních/iterátorových metodách. To by await/yield nedoporučilo používat, ale neodpovídalo by sémantice fixed, protože výraz stackalloc není přemístitelná hodnota. (Všimněte si, že by nebylo možné použít výsledek stackalloc napříč await/yield podobně, jak můžete uložit jakýkoli ukazatel fixed dnes do jiné proměnné ukazatele a použít ho mimo blok fixed.)
  • Iterátor a asynchronní metody můžou mít parametry ukazatele. Musely by být zvednuti, ale nemělo by to být problém, protože zvedání ukazatelů je podporováno i dnes, například:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • Návrh v současné době zachovává (a rozšiřuje/objasňuje) stávající specifikaci, kterou metody iterátoru začínají bezpečným kontextem, i když jsou v nebezpečném kontextu. Například metoda iterátoru není nebezpečný kontext, i když je definován ve třídě, která má modifikátor unsafe. Alternativně bychom mohli nechat iterátory „dědit“ modifikátor unsafe, stejně jako to dělají jiné metody.

    • Výhoda: Eliminuje složitost specifikace a implementace.
    • Výhoda: sladí iterátory s asynchronními metodami (to je jedna z motivací této funkce).
    • Nevýhodou: iterátory uvnitř nebezpečných tříd nemohly obsahovat yield return příkazy, takové iterátory by musely být definovány v samostatné částečné deklaraci třídy bez modifikátoru unsafe.
    • Nevýhodou: Jedná se o zásadní změnu v jazyce LangVersion=13 (iterátory v nebezpečných třídách jsou povoleny v jazyce C# 12).
  • Místo iterátoru, který definuje bezpečný kontext pouze pro tělo, může být celý podpis bezpečným kontextem. To je nekonzistentní se zbytkem jazyka v těchto orgánech, které obvykle neovlivňují deklarace, ale zde by deklarace byla buď bezpečná, nebo nebezpečná v závislosti na tom, zda je tělo iterátorem, nebo ne. Ve verzi LangVersion=13 by došlo také k nekompatibilní změně, protože podpisy iterátoru v C# 12 nejsou bezpečné (například mohou obsahovat parametry pole ukazatelů).

  • Použití modifikátoru unsafe na iterátor:

    • Může mít vliv na tělo i podpis. Takové iterátory by však nebyly příliš užitečné, protože jejich nebezpečný kód nemohl obsahovat yield return, mohly by mít pouze yield break.
    • Může být chyba v LangVersion >= 13 stejně jako je v LangVersion <= 12, protože není příliš užitečné mít nebezpečného iterátora jako člena, protože to umožňuje mít pouze parametry pole s ukazateli nebo nebezpečné settery bez dalšího nebezpečného bloku. V budoucnu by ale mohly být povoleny argumenty běžného ukazatele.
  • Změna narušující kompatibilitu v Roslynu:

    • Mohli bychom zachovat aktuální chování (a dokonce i upravit specifikaci tak, aby odpovídala), například zavedením bezpečného kontextu v metodě iterátoru, ale pak se vrátit k nebezpečnému kontextu v místní funkci.
    • Nebo bychom mohli přerušit všechny verze jazyka, nejen 13 a novější.
    • Je také možné výrazněji zjednodušit pravidla, tak, že iterátory převezmou nezajištěný kontext stejně jako všechny ostatní metody. Probírané výše. Může být provedeno ve všech LangVersions nebo jen pro LangVersion >= 13.

Designérské schůzky