Udostępnij za pośrednictwem


Zezwól na ref i unsafe w iteratorach i asynchronicznie

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są przechwytywane w odpowiednich spotkania projektowego języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Problem z czempionem: https://github.com/dotnet/csharplang/issues/1331

Streszczenie

Ujednolicenie zachowania między iteratorami i metodami asynchronicznymi. Specyficznie:

  • Zezwalaj na lokalne ref/ref struct oraz bloki unsafe w iteratorach i metodach asynchronicznych, jeżeli są one używane w segmentach kodu bez żadnych yield lub await.
  • Ostrzegaj o yield wewnątrz lock.

Motywacja

Nie trzeba zabraniać lokalnych ref/ref struct i bloków unsafe w metodach asynchronicznych/iteracyjnych, jeśli nie są używane w yield czy await, ponieważ nie muszą być wynoszone na zewnątrz.

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

Zmiany powodujące niezgodność

Nie ma żadnych zmian wprowadzających niezgodność w specyfikacji języka, ale istnieje jedna zmiana wprowadzająca niezgodność w implementacji Roslyn (ze względu na pogwałcenie specyfikacji).

Roslyn narusza część specyfikacji, która stwierdza, że iteratory wprowadzają bezpieczny kontekst (§13.3.1). Jeśli na przykład istnieje unsafe class z metodą iteratora, która zawiera funkcję lokalną, funkcja lokalna dziedziczy niebezpieczny kontekst z klasy, chociaż powinna znajdować się w bezpiecznym kontekście zgodnie z specyfikacją ze względu na metodę iteratora. W rzeczywistości cała metoda iteratora odziedziczyła niebezpieczny kontekst w Roslyn, ale po prostu nie zezwalano na używanie żadnych niebezpiecznych konstrukcji w iteratorach. W LangVersion >= 13iteratory poprawnie wprowadzają bezpieczny kontekst, ponieważ chcemy zezwolić na niebezpieczne konstrukcje w iteratorach.

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

Uwaga:

  • Przerwę można obejść, dodając modyfikator unsafe do funkcji lokalnej.
  • Nie ma to wpływu na lambdy, ponieważ "dziedziczą" kontekst "iteratora" i dlatego nie można było używać niebezpiecznych konstrukcji wewnątrz nich.

Szczegółowy projekt

Następujące zmiany są powiązane z wersją języka LangVersion, tj. C# 12 i niższe będą nadal nie zezwalać na zmienne lokalne typów ref-like i bloki unsafe w metodach asynchronicznych i iteratorach, a C# 13 zniesie te ograniczenia, jak opisano poniżej. Jednak objaśnienia specyfikacji, które pasują do istniejącej implementacji Roslyn, powinny obowiązywać we wszystkich LangVersions.

§13.3.1 Bloki > ogólne:

Blok zawierający co najmniej jedną instrukcję yield (§13.15) jest nazywany blokiem iteratora, nawet jeśli instrukcje yield znajdują się tylko pośrednio w zagnieżdżonych blokach (z wyłączeniem zagnieżdżonych wyrażeń lambd i funkcji lokalnych).

[...]

Jest to błąd czasu kompilacji dla bloku iteratora zawierającego niebezpieczny kontekst (§23.2). Blok iteratora zawsze definiuje bezpieczny kontekst, nawet jeśli jego deklaracja jest zagnieżdżona w niebezpiecznym kontekście. Blok iteratora używany do implementowania iteratora (§15.14) zawsze definiuje bezpieczny kontekst, nawet jeśli deklaracja iteratora jest zagnieżdżona w niebezpiecznym kontekście.

Z tej specyfikacji wynika również:

  • Jeśli deklaracja iteratora jest oznaczona modyfikatorem unsafe, podpis znajduje się w niebezpiecznym zakresie, ale blok iteratora używany do implementowania tego iteratora nadal definiuje bezpieczny zakres.
  • Akcesorium set właściwości iteratora lub indeksatora (tj. jego get akcesor jest implementowany za pośrednictwem bloku iteratora) "dziedziczy" jego bezpieczny/niebezpieczny zakres z deklaracji.
  • Nie ma to wpływu na częściowe deklaracje bez implementacji, ponieważ są one tylko sygnaturami i nie mogą mieć korpusu iteratora.

Należy pamiętać, że w języku C# 12 występuje błąd podczas oznaczania metody iteratora unsafe modyfikatorem, ale jest to dozwolone w języku C# 13 ze względu na zmianę specyfikacji.

Na przykład:

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 Odnośnik do deklaracji zmiennych lokalnych:

Jest to błąd czasu kompilacji zadeklarowanie zmiennej lokalnej ref lub zmiennej typu ref struct w ramach metody zadeklarowanej przy użyciu method_modifierasynclub w iteratorze (§15.14).Jest to błąd czasu kompilacji zadeklarowanie i używanie (nawet implicite w kodzie generowanym przez kompilator) zmiennej lokalnej ref lub zmiennej typu ref struct w wyrażeniach await lub instrukcjach yield return. Dokładniej mówiąc, błąd jest spowodowany następującym mechanizmem: po wyrażeniu await (§12.9.8) lub instrukcji yield return (§13.15), wszystkie zmienne lokalne ref i zmienne typu ref struct w zakresie są uważane za zdecydowanie nieprzypisane (§9.4).

Należy pamiętać, że ten błąd nie został obniżony do ostrzeżenia w unsafe kontekstach, takich jak inne błędy bezpieczeństwa ref. Wynika to z faktu, że tych lokalnych zmiennych podobnych do ref nie można manipulować w kontekstach unsafe bez polegania na szczegółach implementacyjnych dotyczacych działania przepisania maszyny stanów, dlatego ten błąd wykracza poza to, co chcemy zredukować do ostrzeżeń w kontekstach unsafe.

§15.14.1 Iteratory > Ogólne:

Gdy element członkowski funkcji jest implementowany przy użyciu bloku iteratora, występuje błąd kompilacji, jeśli formalna lista parametrów tego elementu zawiera jakiekolwiek parametry in, ref readonly, outlub ref, albo parametr typu ref structlub typu wskaźnikowego.

Nie trzeba wprowadzać żadnych zmian w specyfikacji, aby umożliwić blokom unsafe, które nie zawierają bloków awaitw metodach asynchronicznych, ponieważ specyfikacja nigdy nie zabraniała bloków unsafe w metodach asynchronicznych. Jednak specyfikacja powinna była zawsze zabraniać await wewnątrz bloków unsafe (już zabraniały yield w unsafe w §13.3.1, jak wspomniano powyżej), więc proponujemy następującą zmianę specyfikacji:

pl-PL: §15.15.1 Asynchroniczne Funkcje > Ogólne:

Dla formalnej listy parametrów funkcji asynchronicznej błąd czasu kompilacji występuje, gdy zawiera ona jakiekolwiek parametry in, out, ref, czy też dowolny parametr typu ref struct.

Błędem czasu kompilacji w niebezpiecznym kontekście (§23.2) jest zawieranie wyrażenia await (§12.9.8) lub instrukcji yield return (§13.15).

§23.6.5 Adres operatora:

Zgłoszony zostanie błąd czasu kompilacji za próbę odczytania adresu zmiennej lokalnej lub parametru w iteratorze.

Obecnie pobranie adresu zmiennej lokalnej lub parametru w metodzie asynchronicznej jest ostrzeżeniem w fali ostrzeżeńw C# 12.


Należy pamiętać, że więcej konstrukcji może działać dzięki temu, że ref jest dozwolone wewnątrz segmentów bez await i yield w metodach asynchronicznych/iteracyjnych, mimo że żadna zmiana specyfikacji nie jest potrzebna specjalnie dla nich, ponieważ wszystkie wynikają z wyżej wymienionych zmian specyfikacji.

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

Alternatywy

  • ref / ref struct lokalne mogą być dozwolone tylko w blokach (§13.3.1), które nie zawierają 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 wewnątrz lock może być błędem (takim jak await wewnątrz lock) lub ostrzeżeniem o fali ostrzegawczej, ale może to być zmiana powodująca niezgodność: https://github.com/dotnet/roslyn/issues/72443. Należy pamiętać, że nowy Lockobiektowy lock zgłasza błędy czasu kompilacji dla yield returnwewnątrz swojej treści, ponieważ taka instrukcja lock jest równoważna using w ref struct, co nie pozwala na yield returnw jego treści.

  • Zmienne wewnątrz metod asynchronicznych lub iteratora nie powinny być "niezmienne", ale raczej "ruchome", jeśli muszą być przeniesione do pól maszyny stanu, podobnie jak przechwycone zmienne. Należy pamiętać, że jest to wcześniej istniejący błąd w specyfikacji, który jest niezależny od reszty propozycji, ponieważ bloki unsafe wewnątrz metod async były zawsze dozwolone. Obecnie istnieje ostrzeżenie w ramach fali ostrzegawczej w C# 12, a przekształcenie tego w błąd byłoby zmianą powodującą niezgodność.

    §23.4 Stałe i przenoszone zmienne:

    Mówiąc dokładnie, zmienna stała jest jedną z następujących wartości:

    • Zmienna wynikająca z simple_name (§12.8.4), która odwołuje się do zmiennej lokalnej, parametru wartości, lub tablica parametrów, chyba że zmienna jest przechwytywana przez funkcję anonimową (§12.19.6.2) lub funkcja lokalna (§13.6.4) lub zmienna musi być wciągnięta w ramach asynchronicznego (§15.15) lub iteratora (§15.14) metody.
    • [...]
    • Obecnie mamy istniejące ostrzeżenie w języku C# 12 dla adres-of w metodach asynchronicznych oraz proponowany błąd dla adres-of w iteratorach zgłoszonego dla LangVersion 13+ (nie musi być zgłaszany w wcześniejszych wersjach, ponieważ nie było możliwe użycie niebezpiecznego kodu w iteratorach). Można by złagodzić oba te warunki, aby zastosować je tylko do zmiennych, które są rzeczywiście wzniesione, a nie do wszystkich zmiennych lokalnych i parametrów.

    • Można użyć fixed, aby uzyskać adres wciągniętych lub przechwyconych zmiennych, chociaż fakt, że są to pola, jest szczegółem implementacji, więc w innych implementacjach może nie być możliwe użycie fixed na nich. Należy pamiętać, że proponujemy jedynie rozważyć również zmienne przenoszone na górę jako "przenoszalne", ale przechwycone zmienne były już "przenoszalne", a dla nich fixed nie było dozwolone.

  • Moglibyśmy zezwolić na await/yield wewnątrz unsafe, z wyjątkiem wewnątrz instrukcji fixed (kompilator nie może przypinać zmiennych między granicami metody). Może to prowadzić do nieoczekiwanego zachowania, na przykład związanego z stackalloc, jak opisano w zagnieżdżonym punkcie poniżej. Co więcej, podnoszenie wskaźników jest wspierane nawet dzisiaj w niektórych sytuacjach (poniżej znajduje się przykład związany z użyciem wskaźników jako argumentów), więc nie powinno być innych ograniczeń dla zezwolenia na to.

    • Możemy uniemożliwić niebezpieczny wariant stackalloc w metodach asynchronicznych i iteracyjnych, ponieważ bufor przydzielony na stosie nie jest aktywny podczas instrukcji await/yield. Nie jest to konieczne, ponieważ niebezpieczny kod z założenia nie zapobiega "użyciu po zwolnieniu". Należy zauważyć, że moglibyśmy również zezwolić na niebezpieczne stackalloc, pod warunkiem że nie jest używane w await/yield, ale może to być trudne do przeanalizowania (wynikowy wskaźnik może być przekazywany w dowolnej zmiennej wskaźnika). Lub moglibyśmy wymagać, aby było fixed w metodach asynchronicznych/iteratorze. To zniechęcić używania go w await/yield, ale nie pasuje do semantyki fixed, ponieważ wyrażenie stackalloc nie jest wartością ruchomą. (Należy pamiętać, że nie byłoby niemożliwe, aby użyć wyniku stackalloc w await/yield podobnie, jak można zapisać dowolny wskaźnik fixed dzisiaj do innej zmiennej wskaźnika i użyć go poza blokiem fixed).
  • Metody iteracyjne i asynchroniczne mogą mieć parametry wskaźnika. Muszą być wciągane, ale nie powinno to być problemem, ponieważ wskaźniki wciągania są obsługiwane także dzisiaj, na przykład:

    unsafe public void* M(void* p)
    {
        var d = () => p;
        return d();
    }
    
  • Wniosek utrzymuje obecnie (i rozszerza/wyjaśnia) istniejącą specyfikację, że metody iteracyjne rozpoczynają bezpieczny kontekst, nawet jeśli znajdują się w niebezpiecznym kontekście. Na przykład metoda iteratora nie jest niebezpiecznym kontekstem, nawet jeśli jest zdefiniowana w klasie, która ma modyfikator unsafe. Alternatywnie możemy sprawić, że iteratory "dziedziczą" modyfikator unsafe tak jak inne metody.

    • Zaleta: eliminuje złożoność specyfikacji i implementacji.
    • Zaleta: dopasowuje iteratory do metod asynchronicznych (jedną z motywacji funkcji).
    • Wadą: iteratory wewnątrz niebezpiecznych klas nie mogły zawierać instrukcji yield return, takie iteratory musiałyby być zdefiniowane w oddzielnej deklaracji klasy częściowej bez modyfikatora unsafe.
    • Wada: spowodowałoby to niezgodność w wersji języka LangVersion=13 (iteratory w niebezpiecznych klasach są dozwolone w C# 12).
  • Zamiast iteratora, który definiuje bezpieczny kontekst tylko dla treści, cała sygnatura mogłaby być bezpiecznym kontekstem. Jest to niezgodne z resztą języka w tym organie zwykle nie wpływa na deklaracje, ale tutaj deklaracja byłaby bezpieczna lub niebezpieczna w zależności od tego, czy organ jest iteratorem, czy nie. Byłaby to również zmiana powodująca niezgodność w LangVersion=13, ponieważ w C# 12 podpisy iteratorów są niebezpieczne (mogą zawierać na przykład parametry tablicy wskaźników).

  • Zastosowanie modyfikatora unsafe do iteratora:

    • Może mieć wpływ na ciało, jak również na podpis. Takie iteratory nie byłyby jednak bardzo przydatne, ponieważ ich niebezpieczne ciała nie mogły zawierać yield returns, mogli mieć tylko yield breaks.
    • Może to być błąd w LangVersion >= 13, tak jak w LangVersion <= 12, ponieważ nie jest zbyt użyteczne, aby mieć niebezpiecznego członka iteratora, ponieważ umożliwia to tylko posiadanie parametrów tablicy wskaźników lub niebezpiecznych setterów bez dodatkowego niebezpiecznego bloku. Ale zwykłe argumenty wskaźnika mogą być dozwolone w przyszłości.
  • Zmiana powodująca niezgodność Roslyn:

    • Możemy zachować bieżące zachowanie (a nawet zmodyfikować specyfikację, aby ją dopasować), wprowadzając bezpieczny kontekst w metodzie iteratora, ale następnie przywracając niebezpieczny kontekst w funkcji lokalnej.
    • Albo moglibyśmy złamać wszystkie LangVersions, nie tylko 13 i nowsze.
    • Istnieje również możliwość bardziej drastycznego uproszczenia reguł poprzez spowodowanie, że iteratory dziedziczą niebezpieczny kontekst, podobnie jak robią to wszystkie inne metody. Omówione powyżej. Można to zrobić we wszystkich wersjach językowych lub tylko dla LangVersion >= 13.

Spotkania dotyczące projektowania

  • 2024-06-03: przegląd po wdrożeniu specyfikacji