Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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 blokiunsafe
w iteratorach i metodach asynchronicznych, jeżeli są one używane w segmentach kodu bez żadnychyield
lubawait
. - Ostrzegaj o
yield
wewnątrzlock
.
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 >= 13
iteratory 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.
Blok zawierający co najmniej jedną instrukcję
yield
(§13.15) jest nazywany blokiem iteratora, nawet jeśli instrukcjeyield
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. jegoget
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 typuJest to błąd czasu kompilacji zadeklarowanie i używanie (nawet implicite w kodzie generowanym przez kompilator) zmiennej lokalnej ref lub zmiennej typuref struct
w ramach metody zadeklarowanej przy użyciu method_modifierasync
lub w iteratorze (§15.14).ref struct
w wyrażeniachawait
lub instrukcjachyield return
. Dokładniej mówiąc, błąd jest spowodowany następującym mechanizmem: po wyrażeniuawait
(§12.9.8) lub instrukcjiyield return
(§13.15), wszystkie zmienne lokalne ref i zmienne typuref 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
.
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
,out
lubref
, albo parametr typuref struct
lub typu wskaźnikowego.
Nie trzeba wprowadzać żadnych zmian w specyfikacji, aby umożliwić blokom unsafe
, które nie zawierają bloków await
w 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 typuref struct
.Błędem czasu kompilacji w niebezpiecznym kontekście (§23.2) jest zawieranie wyrażenia
await
(§12.9.8) lub instrukcjiyield return
(§13.15).
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ątrzlock
może być błędem (takim jakawait
wewnątrzlock
) 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 nowyLock
obiektowylock
zgłasza błędy czasu kompilacji dlayield return
wewnątrz swojej treści, ponieważ taka instrukcjalock
jest równoważnausing
wref struct
, co nie pozwala nayield return
w 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 metodasync
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życiefixed
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 nichfixed
nie było dozwolone.
Moglibyśmy zezwolić na
await
/yield
wewnątrzunsafe
, z wyjątkiem wewnątrz instrukcjifixed
(kompilator nie może przypinać zmiennych między granicami metody). Może to prowadzić do nieoczekiwanego zachowania, na przykład związanego zstackalloc
, 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 instrukcjiawait
/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 niebezpiecznestackalloc
, pod warunkiem że nie jest używane wawait
/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łofixed
w metodach asynchronicznych/iteratorze. To zniechęcić używania go wawait
/yield
, ale nie pasuje do semantykifixed
, ponieważ wyrażeniestackalloc
nie jest wartością ruchomą. (Należy pamiętać, że nie byłoby niemożliwe, aby użyć wynikustackalloc
wawait
/yield
podobnie, jak można zapisać dowolny wskaźnikfixed
dzisiaj do innej zmiennej wskaźnika i użyć go poza blokiemfixed
).
- Możemy uniemożliwić niebezpieczny wariant
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ą" modyfikatorunsafe
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 modyfikatoraunsafe
. - 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 return
s, mogli mieć tylkoyield break
s. - Może to być błąd w
LangVersion >= 13
, tak jak wLangVersion <= 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.
- 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ć
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
C# feature specifications