Ulepszenia struktury niskiego poziomu
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 .
Streszczenie
Ta propozycja jest agregacją kilku różnych propozycji dotyczących ulepszeń wydajności struct
: ref
pól i możliwości zastąpienia domyślnych wartości okresu trwałości. Celem jest projekt, który uwzględnia różne propozycje tworzenia pojedynczego nadrzędnego zestawu funkcji na potrzeby ulepszeń struct
niskiego poziomu.
Uwaga: Poprzednie wersje tej specyfikacji używały terminów "ref-safe-to-escape" i "safe-to-escape", które zostały wprowadzone w specyfikacji funkcji bezpieczeństwa
Span. Komisja Standardowa ECMA zmieniła nazwy na "ref-safe-context" i "safe-context", odpowiednio. Wartości bezpiecznego kontekstu zostały uściślone w celu spójnego używania "bloku deklaracji", "elementu funkcji" i "kontekstu wywołującego". Speclety używały różnych sformułowań dla tych terminów, a także używały "bezpiecznego powrotu" jako synonimu dla "caller-context". Ten speclet został zaktualizowany, aby używać terminów w standardzie C# 7.3.
Nie wszystkie funkcje opisane w tym dokumencie zostały zaimplementowane w języku C# 11. C# 11 obejmuje:
-
ref
pola iscoped
[UnscopedRef]
Te funkcje pozostają otwartymi propozycjami przyszłej wersji języka C#:
-
ref
pola doref struct
- Typy z ograniczeniami o zachodzie słońca
Motywacja
Wcześniejsze wersje języka C# dodały do języka szereg funkcji wydajności niskiego poziomu: ref
zwraca, ref struct
, wskaźniki funkcji itp. ... Dzięki temu deweloperzy platformy .NET mogą pisać wysoce wydajny kod, kontynuując korzystanie z reguł języka C# na potrzeby bezpieczeństwa typów i pamięci. Pozwoliło to również na tworzenie podstawowych typów wydajności w bibliotekach platformy .NET, takich jak Span<T>
.
Ponieważ te funkcje zyskały popularność w ekosystemie platformy .NET, deweloperzy, zarówno wewnętrzni, jak i zewnętrzni, dostarczają nam informacje na temat pozostałych kwestii problematycznych w ekosystemie. Miejsca, w których nadal muszą porzucać kod unsafe
, aby wykonać pracę, lub wymagać środowiska uruchomieniowego do specjalnych typów przypadków, takich jak Span<T>
.
Obecnie Span<T>
jest realizowane przy użyciu typu internal
ByReference<T>
, który środowisko uruchomieniowe skutecznie traktuje jako pole ref
. Zapewnia to korzyści z ref
pól, ale z wadą, że język nie zapewnia weryfikacji bezpieczeństwa, podobnie jak w przypadku innych zastosowań ref
. Ponadto tylko dotnet/runtime może używać tego typu, ponieważ jest to internal
, więc strony trzecie nie mogą projektować własnych prymitywów na podstawie pól ref
. Częścią motywacji do tej pracy jest usunięcie ByReference<T>
i użycie odpowiednich pól ref
we wszystkich bazach kodu.
Ten wniosek planuje rozwiązać te problemy, opierając się na istniejących funkcjach niskiego poziomu. W szczególności ma na celu:
- Zezwalaj typom
ref struct
na deklarowanie pólref
. - Zezwól środowisku uruchomieniowemu na pełne zdefiniowanie
Span<T>
przy użyciu systemu typów języka C# i usunięcie specjalnego typu przypadku, takiego jakByReference<T>
- Zezwolić typom
struct
na zwracanieref
do ich pól. - Pozwól środowisku uruchomieniowemu usunąć użycia
unsafe
spowodowane ograniczeniami domyślnych ustawień okresów istnienia. - Zezwalanie na deklarowanie bezpiecznych buforów
fixed
dla typów zarządzanych i niezarządzanych wstruct
Szczegółowy projekt
Reguły bezpieczeństwa ref struct
są zdefiniowane w dokumencie bezpieczeństwa zakresu przy użyciu poprzednich terminów. Zasady te zostały włączone do standardu C# 7 w §9.7.2 i §16.4.12. W tym dokumencie opisano wymagane zmiany w tym dokumencie w wyniku tej propozycji. Po zaakceptowaniu jako zatwierdzonej funkcji te zmiany zostaną włączone do tego dokumentu.
Po zakończeniu tego projektu definicja Span<T>
będzie następująca:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
// This constructor does not exist today but will be added as a part
// of changing Span<T> to have ref fields. It is a convenient, and
// safe, way to create a length one span over a stack value that today
// requires unsafe code.
public Span(ref T value)
{
_field = ref value;
_length = 1;
}
}
Podaj pola ref i zakres
Język umożliwi deweloperom deklarowanie pól ref
wewnątrz ref struct
. Może to być przydatne na przykład w przypadku enkapsulacji dużych modyfikowalnych wystąpień struct
lub definiowania typów wysokiej wydajności, takich jak Span<T>
w bibliotekach obok środowiska uruchomieniowego.
ref struct S
{
public ref int Value;
}
Pole ref
będzie emitowane do metadanych przy użyciu podpisu ELEMENT_TYPE_BYREF
. Nie różni się to od tego, jak emitujemy lokalnych ref
lub argument ref
. Na przykład ref int _field
będą emitowane jako ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. Będzie to wymagało aktualizacji ECMA335, aby zezwolić na ten wpis, ale powinno to być raczej proste.
Deweloperzy mogą nadal inicjować ref struct
z użyciem pola ref
i wyrażenia default
, co spowoduje, że wszystkie zadeklarowane pola ref
będą miały wartość null
. Każda próba użycia takich pól spowoduje zgłoszenie błędu NullReferenceException
.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Język C# udaje, że ref
nie może stać się null
, co jest legalne na poziomie środowiska uruchomieniowego i ma dobrze zdefiniowaną semantykę. Deweloperzy, którzy wprowadzają ref
pola do swoich typów, muszą mieć świadomość tej możliwości i należy ich zdecydowanie odradzać przed wyciekaniem tego szczegółu do kodu używającego tych typów. Zamiast tego pola ref
powinny być sprawdzane pod kątem niezerowych wartości z użyciem pomocników środowiska uruchomieniowego oraz, z wyrzuceniem błędu, gdy nieprawidłowo używana jest niezainicjowana struct
.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Pole ref
można połączyć z modyfikatorami readonly
w następujący sposób:
-
readonly ref
: jest to pole, którego nie można ponownie przypisać poza konstruktorem lub metodamiinit
. Może być przypisana wartość również w innych kontekstach. -
ref readonly
: jest to pole, które można ponownie przypisać jako referencję, ale nie można przypisać mu wartości w żadnym momencie. W ten sposób można ponownie przypisać parametrin
do polaref
. -
readonly ref readonly
: kombinacjaref readonly
ireadonly ref
.
ref struct ReadOnlyExample
{
ref readonly int Field1;
readonly ref int Field2;
readonly ref readonly int Field3;
void Uses(int[] array)
{
Field1 = ref array[0]; // Okay
Field1 = array[0]; // Error: can't assign ref readonly value (value is readonly)
Field2 = ref array[0]; // Error: can't repoint readonly ref
Field2 = array[0]; // Okay
Field3 = ref array[0]; // Error: can't repoint readonly ref
Field3 = array[0]; // Error: can't assign ref readonly value (value is readonly)
}
}
readonly ref struct
będzie wymagać, aby pola ref
zostały zadeklarowane readonly ref
. Nie ma wymogu, aby zostały zadeklarowane readonly ref readonly
. Pozwala to readonly struct
mieć mutacje pośrednie za pośrednictwem takiego pola, ale nie różni się to od pola readonly
wskazującego typ referencyjny dzisiaj (więcej szczegółów)
readonly ref
zostanie emitowany do metadanych przy użyciu flagi initonly
, tak samo jak każde inne pole. Pole ref readonly
będzie posiadać atrybut System.Runtime.CompilerServices.IsReadOnlyAttribute
.
readonly ref readonly
zostanie wyemitowany z obydwoma elementami.
Ta funkcja wymaga obsługi środowiska uruchomieniowego i zmian specyfikacji ECMA. W związku z tym zostaną one włączone tylko wtedy, gdy odpowiednia flaga funkcji zostanie ustawiona w pliku corelib. Problem śledzący dokładny interfejs API jest śledzony tutaj https://github.com/dotnet/runtime/issues/64165
Zestaw zmian w naszych regułach bezpiecznego kontekstu niezbędnych do zezwolenia na pola ref
jest mały i specyficzny. Reguły już uwzględniają pola ref
, które istnieją i są wykorzystywane z interfejsów API. Zmiany muszą skupić się tylko na dwóch aspektach: sposobie ich tworzenia i sposobie, w jaki są ponownie przypisywane.
Najpierw należy zaktualizować reguły ustanawiania wartości ref-safe-context dla pól ref
w następujący sposób:
Wyrażenie w formularzu
ref e.F
ref-safe-context w następujący sposób:
- Jeśli
F
jest polemref
, jego ref-safe-context jest bezpieczny kontekste
.- W przeciwnym razie jeśli
e
jest typu referencyjnego, posiada kontekst bezpieczny dla referencji w kontekście wywołującym- W przeciwnym razie kontekst bezpieczeństwa odniesienia jest pobierany z kontekstu bezpieczeństwa odniesienia
e
.
Nie reprezentuje to jednak zmiany reguły, ponieważ reguły zawsze uwzględniały stan ref
w ref struct
. Tak właśnie działał stan ref
w Span<T>
od zawsze, a zasady zużycia prawidłowo to uwzględniają. Ta zmiana ma na celu umożliwienie deweloperom bezpośredniego dostępu do pól ref
oraz zapewnienie, że robią to zgodnie z już istniejącymi zasadami stosowanymi do Span<T>
.
Oznacza to jednak, że pola ref
mogą być zwracane jako ref
z ref struct
, natomiast normalne pola nie mają takiej możliwości.
ref struct RS
{
ref int _refField;
int _field;
// Okay: this falls into bullet one above.
public ref int Prop1 => ref _refField;
// Error: This is bullet four above and the ref-safe-context of `this`
// in a `struct` is function-member.
public ref int Prop2 => ref _field;
}
Może to wydawać się błędem na pierwszy rzut oka, ale jest to celowy punkt projektowania. Ponownie jednak, nie jest to nowa reguła tworzona przez tę propozycję; zamiast tego jest to uznanie istniejących reguł Span<T>
, które były przestrzegane dotąd, aby deweloperzy mogli zadeklarować własny stan ref
.
Następnie należy dostosować reguły ponownego przypisania ref, aby uwzględnić obecność pól ref
. Podstawowym scenariuszem ponownego przypisania ref jest ref struct
konstruktory przechowujące parametry ref
w polach ref
. Obsługa będzie bardziej ogólna, ale jest to podstawowy scenariusz. Aby to wesprzeć, reguły ponownego przypisania ref zostaną dostosowane do uwzględnienia pól ref
w następujący sposób:
Reguły ponownego przypisania ref
Lewy operand operatora = ref
musi być wyrażeniem, które wiąże się ze zmienną lokalną ref, parametrem ref (innym niż this
), parametrem out, lub polem ref.
W przypadku ponownego przypisania ref w formularzu
e1 = ref e2
oba następujące elementy muszą mieć wartość true:
e2
musi mieć ref-safe-context co najmniej tak duży jak ref-safe-context ze1
e1
musi mieć taki sam kontekst bezpieczny jake2
Uwaga
Oznacza to, że żądany konstruktor Span<T>
działa bez dodatkowej adnotacji:
readonly ref struct Span<T>
{
readonly ref T _field;
readonly int _length;
public Span(ref T value)
{
// Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The
// safe-context of `this` is *return-only* and ref-safe-context of `value` is
// *caller-context* hence this is legal.
_field = ref value;
_length = 1;
}
}
Zmiana reguł ponownego przypisania ref oznacza, że parametry ref
mogą teraz uciec od metody jako pola ref
w wartości ref struct
. Zgodnie z opisem w sekcji dotyczącym zagadnień zgodności, może to zmienić zasady dla istniejących interfejsów API, które nigdy nie miały być przeznaczone do tego, aby parametry ref
były traktowane jako pole ref
. Reguły okresu istnienia parametrów są oparte wyłącznie na ich deklaracji, a nie na ich użyciu. Wszystkie parametry ref
i in
mają ref-safe-context kontekstu wywołującego, dlatego mogą być teraz zwracane przez ref
lub pole ref
. Aby obsługiwać interfejsy API z ref
parametrami, które mogą być 'escaping' lub 'non-escaping', i tym samym aby przywrócić semantykę miejsca wywołań języka C# 10, język zostanie wprowadzony z ograniczonymi adnotacjami okresu istnienia.
modyfikator scoped
Słowo kluczowe scoped
będzie używane do ograniczania okresu istnienia wartości. Można go zastosować do ref
lub wartości, która jest ref struct
i ma wpływ na ograniczenie ref-safe-context lub bezpiecznego kontekstu okresu istnienia odpowiednio do składowej funkcji. Na przykład:
Parametr lub lokalny | ref-safe-context | bezpieczny kontekst |
---|---|---|
Span<int> s |
element funkcji | kontekstu wywołującego |
scoped Span<int> s |
element funkcji | element funkcji |
ref Span<int> s |
kontekstu wywołującego | kontekstu wywołującego |
scoped ref Span<int> s |
element funkcji | kontekstu wywołującego |
W tej relacji kontekst bezpieczny wartości nigdy nie może być szerszy niż bezpieczny kontekst.
Dzięki temu interfejsy API w języku C# 11 mogą być oznaczone adnotacjami, tak aby miały te same reguły co C# 10:
Span<int> CreateSpan(scoped ref int parameter)
{
// Just as with C# 10, the implementation of this method isn't relevant to callers.
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 and legal in C# 11 due to scoped ref
return CreateSpan(ref parameter);
// Legal in C# 10 and legal in C# 11 due to scoped ref
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 and legal in C# 11 due to scoped ref
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Adnotacja scoped
oznacza również, że parametr this
elementu struct
można teraz zdefiniować jako scoped ref T
. Wcześniej trzeba było stosować specjalne przypadki w regułach jako parametr ref
, który miał różne reguły kontekstu ref niż inne parametry ref
(zobacz wszystkie odwołania do dołączania lub wykluczania odbiornika w regułach bezpiecznego kontekstu). Teraz można je wyrazić jako ogólną koncepcję w ramach reguł, które jeszcze bardziej je upraszczają.
Adnotacja scoped
można również zastosować do następujących lokalizacji:
- ustawienia lokalne: ta adnotacja ustawia okres istnienia jako bezpieczny kontekstlub kontekst bezpieczny dla referencji w przypadku lokalnego
ref
, dla członka funkcji niezależnie od okresu istnienia inicjatora.
Span<int> ScopedLocalExamples()
{
// Error: `span` has a safe-context of *function-member*. That is true even though the
// initializer has a safe-context of *caller-context*. The annotation overrides the
// initializer
scoped Span<int> span = default;
return span;
// Okay: the initializer has safe-context of *caller-context* hence so does `span2`
// and the return is legal.
Span<int> span2 = default;
return span2;
// The declarations of `span3` and `span4` are functionally identical because the
// initializer has a safe-context of *function-member* meaning the `scoped` annotation
// is effectively implied on `span3`
Span<int> span3 = stackalloc int[42];
scoped Span<int> span4 = stackalloc int[42];
}
Inne zastosowania scoped
w ustawieniach lokalnych zostały omówione poniżej.
Adnotacja scoped
nie może być stosowana do żadnej innej lokalizacji, w tym do zwracania, pól, elementów tablicy itp. Ponadto scoped
ma wpływ, gdy jest stosowana do dowolnych ref
, in
lub out
, ale tylko wtedy, gdy jest stosowana do wartości, które są ref struct
. Posiadanie deklaracji takich jak scoped int
nie ma wpływu, ponieważ nie ref struct
jest zawsze bezpieczny do powrotu. Kompilator utworzy diagnostykę dla takich przypadków, aby uniknąć nieporozumień deweloperów.
Zmienianie zachowania parametrów out
Aby dodatkowo ograniczyć wpływ zmiany dotyczącej zgodności związaną z możliwością zwracania parametrów out
parametry są niejawnie scoped out
na przyszłość. Z punktu widzenia zgodności oznacza to, że nie można ich zwrócić przy użyciu ref
.
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
To zwiększy elastyczność interfejsów API, które zwracają ref struct
wartości i mają out
parametry, ponieważ nie trzeba już uwzględniać parametru przechwytywanego przez referencję. Jest to ważne, ponieważ jest to typowy wzorzec w API stylu czytelnika.
Span<byte> Read(Span<byte> buffer, out int read)
{
// ..
}
Span<byte> Use()
{
var buffer = new byte[256];
// If we keep current `out` ref-safe-context this is an error. The language must consider
// the `read` parameter as returnable as a `ref` field
//
// If we change `out` ref-safe-context this is legal. The language does not consider the
// `read` parameter to be returnable hence this is safe
int read;
return Read(buffer, out read);
}
Język nie będzie już traktować argumentów przekazywanych do parametru out
jako możliwych do zwrócenia. Traktowanie danych wejściowych dla parametru out
jako możliwych do zwrotu było niezwykle mylące dla deweloperów. Zasadniczo odwraca intencję out
, zmuszając deweloperów do rozważenia wartości przekazanej przez obiekt wywołujący, która w przeciwnym razie nigdy nie jest używana, z wyjątkiem języków, które nie respektują out
. W przyszłości języki obsługujące ref struct
muszą upewnić się, że oryginalna wartość przekazana do parametru out
nigdy nie jest odczytywana.
Język C# osiąga to za pośrednictwem określonych reguł przypisania. To spełnia zarówno nasze zasady bezpiecznego kontekstu ref, jak i umożliwia działanie istniejącego kodu, który przypisuje wartości parametrów i następnie zwraca out
.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Te zmiany oznaczają, że argument parametru out
nie przyczynia się do wartości bezpiecznego kontekstu ani wartości ref-bezpiecznego kontekstu podczas wywołań metod. Znacznie zmniejsza to całkowity wpływ pól ref
, a także upraszcza sposób, w jaki programiści myślą o out
. Argument parametru out
nie przyczynia się do zwracania, jest to po prostu dane wyjściowe.
Wnioskowanie bezpiecznego kontekstu wyrażeń deklaracji
- kontekst wywołujący
- Jeśli zmienna out jest oznaczona
scoped
, to znajduje się w obrębie deklaracji (tj. element składowy funkcji lub bardziej szczegółowy). - jeśli typ zmiennej out jest
ref struct
, należy wziąć pod uwagę wszystkie argumenty zawierające wywołanie, w tym odbiornik:bezpieczny kontekst dowolnego argumentu, w którym odpowiadający mu parametr nie jesti ma bezpieczny kontekst tylko do powrotulub szerszy - ref-safe-context dowolnego argumentu, w którym odpowiadający mu parametr ma ref-safe-contextzwracanych tylko lub szerszy
Zobacz również Przykłady wnioskowanych bezpiecznego kontekstu wyrażeń deklaracji.
Niejawnie scoped
parametrów
ogólnie istnieją dwie lokalizacje ref
, które są niejawnie zadeklarowane jako scoped
:
-
this
w metodzie instancjistruct
- parametry
out
Reguły bezpiecznego kontekstu ref zostaną napisane pod względem scoped ref
i ref
. W celach kontekstu bezpiecznego ref parametr in
jest odpowiednikiem ref
, a out
jest odpowiednikiem scoped ref
. Zarówno in
, jak i out
będą wywoływane tylko wtedy, gdy jest to szczególnie ważne dla semantyki reguły. W przeciwnym razie są one traktowane jako ref
i scoped ref
odpowiednio.
Podczas omawiania ref-safe-context dla argumentów odpowiadających parametrom in
, będą one uogólnione jako argumenty ref
w specyfikacji. Jeśli argument to lvalue, wówczas ref-safe-context jest takim lvalue, w przeciwnym razie jest to członek funkcji. Ponownie in
będzie wywołane tutaj tylko wtedy, gdy jest to ważne do semantyki bieżącej reguły.
Kontekst bezpieczny tylko dla powrotu
Projekt wymaga również wprowadzenia nowego bezpiecznego kontekstu: tylko do odczytu. Jest to podobne do kontekstu wywołującego
Szczegóły tylko do zwracania polegają na tym, że jest to kontekst większy niż funkcja członkowska , ale mniejszy niż kontekst wywołującego . Wyrażenie podane w instrukcji return
musi być co najmniej zwracane tylko. W związku z tym większość istniejących reguł wypada. Na przykład przypisanie parametru
Istnieją trzy lokalizacje, które domyślnie tylko do zwrócenia:
- Parametr
ref
lubin
będzie miał tylko do zwrotu. Jest to wykonywane częściowo dlaref struct
, aby zapobiec głupim cyklicznym problemom z przypisywaniem. Odbywa się to jednak równomiernie, aby uprościć model, a także zminimalizować zmiany zgodności. - Parametr
out
dlaref struct
będzie miał bezpieczny kontekst przy zwracaniu tylko. To pozwala na to, aby zarówno return, jak iout
były równie wyraziste. Nie ma tu głupiego problemu z cyklicznym przypisaniem, ponieważout
jest niejawniescoped
, więc kontekst bezpieczny dla referencji jest nadal mniejszy niż bezpieczny kontekst . - Parametr
this
konstruktorastruct
będzie miał bezpieczny konteksttylko do zwracania. Wynika to z faktu, że jest modelowane jako parametryout
.
Każde wyrażenie lub instrukcja, która jawnie zwraca wartość z metody lub wyrażenia lambda, musi mieć safe-context, a jeżeli ma to zastosowanie ref-safe-context, co najmniej tylko do zwracania. Obejmuje to return
instrukcje, członków zdefiniowanych za pomocą wyrażeń oraz wyrażenia lambda.
Podobnie każde przypisanie do out
musi mieć bezpieczny kontekst co najmniej tylko do zwracania. Nie jest to jednak szczególny przypadek, wynika to tylko z istniejących reguł przypisania.
Uwaga: wyrażenie, którego typ nie jest typem ref struct
, zawsze ma bezpieczny kontekstkontekstu wywołującego.
Reguły wywołania metody
Reguły bezpiecznego kontekstu ref dla wywołania metody zostaną zaktualizowane na kilka sposobów. Pierwszy z nich polega na rozpoznaniu wpływu, jaki scoped
ma na argumenty. Dla danego argumentu expr
, który jest przekazywany do parametru p
:
- Jeśli
p
jestscoped ref
,expr
nie przyczynia się do kontekstu bezpiecznego dla ref podczas rozważania argumentów.- Jeśli
p
jestscoped
,expr
nie wnosi do bezpiecznego kontekstu przy rozważaniu argumentów.- Jeśli
p
jestout
,expr
nie przyczynia się do bezpiecznego kontekstu ani bezpiecznego kontekstuwięcej szczegółów
Język "nie przyczynia się" oznacza, że argumenty nie są po prostu brane pod uwagę podczas obliczania ref-safe-context lub wartości bezpiecznego kontekstu zwracanej odpowiednio wartości metody. Wynika to z faktu, że wartości nie mogą przyczynić się do tego okresu istnienia, ponieważ adnotacja scoped
uniemożliwia jej działanie.
Reguły wywołania metody można teraz uprościć. Odbiornik nie musi już być specjalny, w przypadku struct
jest to teraz po prostu scoped ref T
. Reguły wartości muszą ulec zmianie, aby uwzględnić zwracane wartości pola ref
.
Wartość wynikająca z wywołania metody
e1.M(e2, ...)
, gdzieM()
nie zwraca struktury ref-to-ref, ma bezpieczny kontekst pobrany z najwęższego z następujących elementów:
- kontekst wywołujący
- Gdy wartość zwracana to
ref struct
, do bezpiecznego kontekstu przyczyniają się wszystkie wyrażenia argumentów.- Kiedy zwrot jest
ref struct
, kontekst bezpieczny dla ref tworzony przez wszystkie argumentyref
Jeśli
M()
zwraca strukturę ref-to-ref-struct, bezpieczny kontekst jest taki sam jak bezpieczny kontekst wszystkich argumentów, które są ref-to-ref-struct. Jest to błąd, jeśli istnieje wiele argumentów z różnymi bezpiecznymi kontekstami, ponieważ argumenty metody muszą być zgodne z.
Reguły wywoływania ref
można uprościć do następujących czynności:
Wartość wynikająca z wywołania metody
ref e1.M(e2, ...)
, gdzieM()
nie zwraca struktury odwołującej-się-do-struktury, jest w kontekście bezpiecznym dla referencji najwęższym z następujących kontekstów:
- kontekst wywołujący
- bezpieczny kontekst dostarczony przez wszystkie wyrażenia argumentów
ref-safe-context wprowadziły wszystkie argumenty Jeśli
M()
zwraca ref-to-ref-struct, ref-safe-context jest najwęższym ref-safe-context definiowanym przez wszystkie argumenty, które są ref-to-ref-struct.
Ta reguła pozwala teraz zdefiniować dwa warianty żądanych metod:
Span<int> CreateWithoutCapture(scoped ref int value)
{
// Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *function-member* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> CreateAndCapture(ref int value)
{
// Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
// of the ref argument. That is the *caller-context* for value hence this is not allowed.
return new Span<int>(ref value);
}
Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
// Okay: the safe-context of `span` is *caller-context* hence this is legal.
return span;
// Okay: the local `refLocal` has a ref-safe-context of *function-member* and a
// safe-context of *caller-context*. In the call below it is passed to a
// parameter that is `scoped ref` which means it does not contribute
// ref-safe-context. It only contributes its safe-context hence the returned
// rvalue ends up as safe-context of *caller-context*
Span<int> local = default;
ref Span<int> refLocal = ref local;
return ComplexScopedRefExample(ref refLocal);
// Error: similar analysis as above but the safe-context of `stackLocal` is
// *function-member* hence this is illegal
Span<int> stackLocal = stackalloc int[42];
return ComplexScopedRefExample(ref stackLocal);
}
Reguły inicjatorów obiektów
bezpieczny kontekst wyrażenia inicjatora obiektów jest najbardziej ograniczony.
- bezpieczny kontekst wywołania konstruktora.
- bezpieczny kontekst i kontekst bezpieczny ref dla argumentów do indeksatorów inicjujących członków, które mogą wyciekać do odbiornika.
- bezpieczny kontekst RHS przypisań w inicjatorach składowych do nieczytanych zestawów lub ref-safe-context w przypadku przypisania odwołania.
Innym sposobem modelowania tego jest myślenie o każdym argumencie inicjalizatora składowego, który można przypisać do odbiornika, jako o argumencie konstruktora. Jest to spowodowane tym, że inicjator składowy jest efektywnie wywołaniem konstruktora.
Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
Field = stackSpan;
}
// Can be modeled as
var x = new S(ref heapSpan, stackSpan);
Modelowanie jest ważne, ponieważ pokazuje, że nasze MAMM muszą być uwzględniane w szczególny sposób dla inicjatorów składowych. Należy wziąć pod uwagę, że ten konkretny przypadek powinien być nielegalny, ponieważ umożliwia przypisanie wartości z węższym bezpiecznym kontekstem do szerszego.
Argumenty metody muszą być zgodne
Obecność pól ref
oznacza, że reguły dotyczące argumentów metody muszą zostać zaktualizowane, ponieważ parametr ref
może być teraz przechowywany jako pole w argumencie ref struct
metody. Wcześniej reguła musiała rozważyć tylko inne ref struct
przechowywane jako pole. Wpływ tego omówiono w kwestie zgodności. Nowa reguła to ...
W przypadku dowolnego wywołania metody
e.M(a1, a2, ... aN)
- Oblicz najwęższy bezpieczny kontekst z:
- kontekstu wywołującego
- bezpieczny kontekst wszystkich argumentów
- ref-safe-context wszystkich argumentów ref, których odpowiednie parametry mają ref-safe-contextkontekstu wywołującego
- Wszystkie argumenty typu
ref
o charakterystyceref struct
muszą być możliwe do przypisania wartości z tego bezpiecznego kontekstu. Jest to przypadek, w którymref
nie uogólniać, aby uwzględnićin
iout
W przypadku dowolnego wywołania metody
e.M(a1, a2, ... aN)
- Oblicz najwęższy bezpieczny kontekst z:
- kontekstu wywołującego
- bezpieczny kontekst wszystkich argumentów
- Kontekst ref-safe-context wszystkich argumentów ref, dla których odpowiadające im parametry nie są
scoped
- Wszystkie argumenty typu
out
o charakterystyceref struct
muszą być możliwe do przypisania wartości z tego bezpiecznego kontekstu.
Obecność scoped
pozwala deweloperom zmniejszyć tarcie tworzone przez tę regułę, oznaczając parametry, które nie są zwracane, jako scoped
. Spowoduje to usunięcie ich parametrów z (1) w obu powyższych przypadkach i zapewni większą elastyczność dzwoniących.
Wpływ tej zmiany omówiono bardziej szczegółowo poniżej. Ogólnie rzecz biorąc, pozwoli to deweloperom na bardziej elastyczne tworzenie miejsc wywołań poprzez adnotację wartości przypominających odwołania, które nie uciekają, przy użyciu scoped
.
Wariancja zakresu parametrów
Modyfikator scoped
i atrybut [UnscopedRef]
(zobacz poniżej) również ma wpływ na nasze zastępowanie obiektów, implementację interfejsu i reguły konwersji delegate
. Sygnatura zastąpienia, implementacji interfejsu lub konwersji delegate
może:
- Dodawanie
scoped
do parametruref
lubin
- Dodawanie
scoped
do parametruref struct
- Usuwanie
[UnscopedRef]
z parametruout
- Usuń
[UnscopedRef]
z parametruref
typuref struct
Wszelkie inne różnice w odniesieniu do scoped
lub [UnscopedRef]
są uznawane za niezgodność.
Kompilator zgłosi diagnostykę niebezpiecznych niezgodności w zakresie między przesłonięciami, implementacjami interfejsu i konwersjami delegowanymi w następujących przypadkach:
- Metoda ma parametr
ref
lubout
typuref struct
z niezgodnością przy dodawaniu[UnscopedRef]
(nie usuwającscoped
). (W tym przypadku możliwe jest bezsensowne cykliczne przypisanie, dlatego nie są potrzebne żadne inne parametry). - Albo oba te elementy są prawdziwe:
- Metoda zwraca
ref struct
lub zwracaref
lubref readonly
albo metoda ma parametrref
lubout
typuref struct
. - Metoda ma co najmniej jeden dodatkowy parametr
ref
,in
lubout
albo parametr typuref struct
.
- Metoda zwraca
Diagnostyka nie jest zgłaszana w innych przypadkach, ponieważ:
- Metody z takimi podpisami nie mogą przechwytywać przekazanych odwołań, więc niezgodności związane z zakresem nie są niebezpieczne.
- Obejmują one bardzo typowe i proste scenariusze (np. zwykłe, stare parametry
out
używane w sygnaturach metodTryParse
). Raportowanie rozbieżności w zakresie tylko dlatego, że są one używane w wersji 11 języka (i w związku z tym parametrout
ma inny zakres) byłoby mylące.
Diagnostyka jest zgłaszana jako błąd , jeśli niezgodne podpisy używają reguł bezpiecznego kontekstu ref w języku C#11. W przeciwnym razie diagnostyka jest ostrzeżeniem .
Ostrzeżenie dotyczące niezgodności zakresowej może być zgłaszane w module skompilowanym przy użyciu reguł kontekstu bezpiecznego dla ref w języku C#7.2, gdzie scoped
jest niedostępne. W niektórych przypadkach może być konieczne pominięcie ostrzeżenia, jeśli nie można zmodyfikować innego niezgodnego podpisu.
Modyfikator scoped
i atrybut [UnscopedRef]
mają również następujący wpływ na podpisy metody:
- Modyfikator
scoped
i atrybut[UnscopedRef]
nie mają wpływu na ukrywanie - Przeciążenia nie mogą się różnić tylko w przypadku
scoped
lub[UnscopedRef]
Sekcja dotycząca pola ref
i scoped
jest długa, dlatego warto zakończyć ją krótkim podsumowaniem proponowanych zmian powodujących niezgodność.
- Wartość, która ma ref-safe-context w kontekście wywołującego , może być zwrócona przez pole
ref
lubref
. - Parametr
out
będzie miał bezpieczny kontekstskładowych funkcji .
Szczegółowe uwagi:
- Pole
ref
można zadeklarować tylko wewnątrzref struct
- Pole
ref
nie można zadeklarować jakostatic
,volatile
lubconst
- Pole
ref
nie może mieć typu, który jestref struct
- Proces generowania zestawu odniesienia musi zachować obecność pola
ref
wewnątrzref struct
-
readonly ref struct
musi zadeklarować swoje polaref
jakoreadonly ref
- W przypadku wartości by-ref modyfikator
scoped
musi pojawić się przedin
,out
lubref
- Dokument dotyczący zasad bezpieczeństwa dla zakresu zostanie zaktualizowany zgodnie z opisem w tym dokumencie
- Nowe reguły bezpiecznego kontekstu "ref" będą obowiązywać, gdy
- Biblioteka podstawowa zawiera flagę funkcji wskazującą obsługę pól
ref
- Wartość
langversion
wynosi 11 lub więcej
- Biblioteka podstawowa zawiera flagę funkcji wskazującą obsługę pól
Składnia
13.6.2 Deklaracje zmiennych lokalnych: dodano 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 Instrukcja for
: dodano pośrednio 'scoped'?
z local_variable_declaration
.
13.9.5 Instrukcja foreach
: dodano 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Listy argumentów: dodano 'scoped'?
dla deklaracji zmiennej out
.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
[TBD]
15.6.2 Parametry metody: dodano 'scoped'?
do parameter_modifier
.
fixed_parameter
: attributes? parameter_modifier? type identifier default_argument?
;
parameter_modifier
| 'this' 'scoped'? parameter_mode_modifier?
| 'scoped' parameter_mode_modifier?
| parameter_mode_modifier
;
parameter_mode_modifier
: 'in'
| 'ref'
| 'out'
;
20.2 Deklaracje delegatów: dodano 'scoped'?
pośrednio z fixed_parameter
.
12.19 Anonimowe wyrażenia funkcji: dodano 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Typy z ograniczeniami o zachodzie słońca
Kompilator ma koncepcję zestawu "typów ograniczonych", który jest w dużej mierze nieudokumentowany. Te typy miały specjalny status, ponieważ w języku C# 1.0 nie było ogólnego sposobu, aby wyrazić ich zachowanie. W szczególności fakt, że typy mogą zawierać odwołania do stosu wywołań. Zamiast tego kompilator miał specjalną wiedzę na ich temat i ograniczył ich użycie do sposobów, które zawsze byłyby bezpieczne: niedozwolone instrukcje zwrotu, nie mogą być używane jako elementy tablic, nie mogą być używane w kontekstach ogólnych itp.
Gdy pola ref
są dostępne i rozszerzone do obsługi ref struct
te typy można poprawnie zdefiniować w języku C#, używając kombinacji pól ref struct
i ref
. W związku z tym gdy kompilator wykryje, że środowisko uruchomieniowe obsługuje ref
pola, nie będzie już mieć pojęcia typów ograniczonych. Zamiast tego użyje typów zdefiniowanych w kodzie.
Nasze reguły bezpiecznego kontekstu ref zostaną zaktualizowane w następujący sposób, aby to wspierać:
-
__makeref
będzie traktowana jako metoda z sygnaturąstatic TypedReference __makeref<T>(ref T value)
-
__refvalue
będzie traktowana jako metoda z podpisemstatic ref T __refvalue<T>(TypedReference tr)
. Wyrażenie__refvalue(tr, int)
będzie efektywnie używać drugiego argumentu jako parametru typu. jako parametr będzie miał kontekstu bezpiecznego i bezpiecznego kontekstu składowych funkcji .jako wyrażenie będzie miało bezpiecznego kontekstu i bezpiecznego kontekstu składowych funkcji .
Zgodne środowiska uruchomieniowe zapewnią, że TypedReference
, RuntimeArgumentHandle
i ArgIterator
są zdefiniowane jako ref struct
. Dalsze TypedReference
należy wyświetlić jako pole ref
do ref struct
dla dowolnego możliwego typu (może przechowywać dowolną wartość). W połączeniu z powyższymi zasadami zapewnia, że odwołania do stosu nie wydostaną się poza ich czas życia.
Uwaga: ściśle mówiąc, są to szczegóły implementacji kompilatora, a nie część języka. Jednak zważywszy na relacje z polami ref
, uwzględnia się go w propozycji języka dla uproszczenia.
Podaj niezakresowy
Jednym z najbardziej godnych uwagi punktów tarcia jest niezdolność do zwracania pól przez ref
w przypadku elementów członkowskich struct
. Oznacza to, że deweloperzy nie mogą tworzyć metod/właściwości zwracających wartość ref
i muszą sięgnąć po bezpośrednie uwidacznianie pól. To zmniejsza użyteczność zwrotów ref
w struct
, gdzie są one często najbardziej pożądane.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
Uzasadnienie dla tego domyślnego jest uzasadnione, ale z założenia nie ma nic złego w przypadku struct
ucieczki this
przez odwołanie, jest to po prostu ustawienie domyślne wybrane przez reguły bezpiecznego kontekstu ref.
Aby rozwiązać ten problem, język programowania zapewni przeciwieństwo adnotacji okresu istnienia scoped
poprzez wsparcie dla UnscopedRefAttribute
. Można to zastosować do dowolnego
UnscopedRef zastosowane do | Oryginalny kontekst ref-safe-context | Nowe ref-safe-context |
---|---|---|
członek instancji | składowa funkcji | wyłącznie do zwrotu |
parametr in / ref |
wyłącznie do zwrotu | kontekst wywołujący |
parametr out |
składowa funkcji | wyłącznie do zwrotu |
Zastosowanie [UnscopedRef]
do metody instancji struct
ma wpływ na modyfikację niejawnego parametru this
. Oznacza to, że this
działa jako nieoznaczony element ref
tego samego rodzaju.
struct S
{
int field;
// Error: `field` has the ref-safe-context of `this` which is *function-member* because
// it is a `scoped ref`
ref int Prop1 => ref field;
// Okay: `field` has the ref-safe-context of `this` which is *caller-context* because
// it is a `ref`
[UnscopedRef] ref int Prop1 => ref field;
}
Adnotację można również umieścić na parametrach out
, aby przywrócić je do zachowania języka C# 10.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Dla reguł bezpiecznego kontekstu ref, taki [UnscopedRef] out
jest traktowany po prostu jako ref
. Podobnie jak in
jest uznawany za ref
w celach życiowych.
Adnotacja [UnscopedRef]
będzie niedozwolona dla członków init
i konstruktorów wewnątrz struct
. Członkowie ci są już wyjątkowi w odniesieniu do semantyki ref
, ponieważ postrzegają członków readonly
jako zmiennych. Oznacza to, że przekazanie ref
tym członkom wydaje się prostym ref
, a nie ref readonly
. Jest to dozwolone w granicach konstruktorów i init
. Zezwolenie [UnscopedRef]
pozwoliłoby takie ref
na niepoprawne opuszczenie poza konstruktor i zezwolić na mutację po tym, jak semantyka readonly
miała miejsce.
Typ atrybutu będzie miał następującą definicję:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Szczegółowe uwagi:
- Metoda wystąpienia lub właściwość z adnotacjami z
[UnscopedRef]
ma ref-safe-contextthis
ustawioną na kontekstu wywołującego. - Członek z adnotacją
[UnscopedRef]
nie może zaimplementować interfejsu. - Jest to błąd podczas używania
[UnscopedRef]
- Członek, który nie jest zadeklarowany w
struct
- Członek
static
, członekinit
lub konstruktor wstruct
- Parametr oznaczony
scoped
- Parametr przekazany przez wartość
- Parametr przekazywany jako referencja, który nie jest niejawnie zadeklarowany
- Członek, który nie jest zadeklarowany w
ScopedRefAttribute
Adnotacje scoped
zostaną zapisywane w metadanych za pośrednictwem atrybutu typu System.Runtime.CompilerServices.ScopedRefAttribute
. Atrybut będzie zgodny z nazwą kwalifikowaną przestrzeni nazw, więc definicja nie musi być wyświetlana w żadnym konkretnym zestawie.
Typ ScopedRefAttribute
jest przeznaczony tylko dla kompilatora — nie jest dozwolony w źródle. Deklaracja typu jest syntetyzowana przez kompilator, jeśli nie została jeszcze uwzględniona w kompilacji.
Typ będzie miał następującą definicję:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Kompilator emituje ten atrybut na parametrze przy użyciu składni scoped
. Będzie to emitowane tylko wtedy, gdy składnia powoduje, że wartość różni się od stanu domyślnego. Na przykład scoped out
nie spowoduje emitowania atrybutu.
RefSafetyRulesAttribute
Istnieje kilka różnic w reguły bezpiecznego kontekstu ref między C#7.2 i C#11. Każda z tych różnic może spowodować istotne zmiany podczas ponownej kompilacji przy użyciu C#11 względem odwołań skompilowanych przy użyciu C#10 lub wcześniejszej wersji.
- Niezakresowe parametry
ref
/in
/out
mogą wyjść poza wywołanie metody jako poleref
obiekturef struct
w języku C#11, co nie jest możliwe w C#7.2. - parametry
out
są niejawnie ograniczone w języku C#11 i niezakresowe w języku C#7.2 -
ref
/in
parametry do typówref struct
posiadają domyślne zakresy w C#11, a są bez zakresu w C#7.2
Aby zmniejszyć prawdopodobieństwo zmiany powodującej niezgodność podczas ponownego kompilowania przy użyciu języka C#11, zaktualizujemy kompilator języka C#11, aby użyć reguł bezpiecznego kontekstu ref wywołania metody, które zgodne z regułami używanymi do analizowania deklaracji metody. Zasadniczo podczas analizowania wywołania metody skompilowanej przy użyciu starszego kompilatora kompilator języka C#11 będzie używać reguł bezpiecznego kontekstu C#7.2.
Aby to włączyć, kompilator wyemituje nowy atrybut [module: RefSafetyRules(11)]
, gdy moduł zostanie skompilowany z użyciem -langversion:11
lub nowszej wersji albo skompilowany z corlibem zawierającym flagę funkcji dla pól ref
.
Argument atrybutu wskazuje wersję języka ref safe context reguł używanych podczas kompilowania modułu.
Wersja jest obecnie stała w 11
niezależnie od rzeczywistej wersji językowej przekazanej do kompilatora.
Oczekuje się, że przyszłe wersje kompilatora uaktualnią zasady bezpiecznego kontekstu ref i wygenerują atrybuty z różnymi wersjami.
Jeśli kompilator ładuje moduł zawierający [module: RefSafetyRules(version)]
z version
innym niż 11
, kompilator zgłosi ostrzeżenie dla nierozpoznanej wersji, jeśli istnieją wywołania metod zadeklarowanych w tym module.
Gdy kompilator języka C#11 analizuje wywołanie metody:
- Jeśli moduł zawierający deklarację metody zawiera
[module: RefSafetyRules(version)]
, niezależnie odversion
, wywołanie metody jest analizowane przy użyciu reguł języka C#11. - Jeśli moduł zawierający deklarację metody pochodzi ze źródła i skompilowany przy użyciu
-langversion:11
lub z corlibem zawierającym flagę funkcji dla pólref
, wywołanie metody jest analizowane przy użyciu reguł języka C#11. - Jeśli moduł zawierający deklarację metody odwołuje się do
System.Runtime { ver: 7.0 }
, wywołanie metody jest analizowane przy użyciu reguł języka C#11. Ta reguła jest tymczasowym ograniczeniem ryzyka dla modułów skompilowanych we wcześniejszych wersjach zapoznawczych języka C#11 / .NET 7 i zostanie usunięta później. - W przeciwnym razie wywołanie metody jest analizowane przy użyciu reguł języka C#7.2.
Kompilator pre-C#11 zignoruje wszystkie RefSafetyRulesAttribute
i przeanalizuje wywołania metod tylko przy użyciu reguł języka C#7.2.
RefSafetyRulesAttribute
będzie zgodna z nazwą kwalifikowaną przestrzeni nazw, więc definicja nie musi być wyświetlana w żadnym konkretnym zestawie.
Typ RefSafetyRulesAttribute
jest przeznaczony tylko dla kompilatora — nie jest dozwolony w źródle. Deklaracja typu jest syntetyzowana przez kompilator, jeśli nie została jeszcze uwzględniona w kompilacji.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public RefSafetyRulesAttribute(int version) { Version = version; }
public readonly int Version;
}
}
Bufory o stałym rozmiarze zaprojektowane z myślą o bezpieczeństwie
Bezpieczne bufory o stałym rozmiarze nie zostały dostarczone w C# 11. Tę funkcję można zaimplementować w przyszłej wersji języka C#.
Język złagodzi ograniczenia dotyczące tablic o stałym rozmiarze, tak aby można je było zadeklarować w bezpiecznym kodzie, a typ elementu może być zarządzany lub niezarządzany. Spowoduje to, że następujące typy staną się legalne:
internal struct CharBuffer
{
internal char Data[128];
}
Deklaracje te, podobnie jak ich odpowiedniki unsafe
, definiują sekwencję elementów N
w zawierającym typie. Dostęp do tych członków można uzyskać za pomocą indeksatora, a także można je przekonwertować na wystąpienia Span<T>
i ReadOnlySpan<T>
.
Podczas indeksowania w buforze fixed
typu T
należy wziąć pod uwagę stan readonly
kontenera. Jeśli kontener jest readonly
, indeksator zwraca ref readonly T
w przeciwnym razie zwraca ref T
.
Uzyskiwanie dostępu do buforu fixed
bez indeksatora nie ma typu naturalnego, jest jednak możliwe do konwersji na typy Span<T>
. W przypadku, gdy kontener jest readonly
, bufor jest niejawnie konwertowany na ReadOnlySpan<T>
, w przeciwnym razie może być niejawnie konwertowany na Span<T>
lub ReadOnlySpan<T>
(konwersja Span<T>
jest uważana za lepszą).
Wynikowa instancja Span<T>
będzie miała długość równą rozmiarowi zadeklarowanemu na buforze fixed
.
bezpieczny kontekst zwróconej wartości będzie równy bezpiecznemu kontekstowi kontenera, tak jak gdyby dostęp do danych źródłowych był uzyskiwany jako pole.
Dla każdej deklaracji fixed
w typie z elementem T
, język wygeneruje odpowiadającą metodę indeksatora tylko get
, którego typ zwracany jest ref T
. Indeksator zostanie oznaczony adnotacją z atrybutem [UnscopedRef]
, ponieważ implementacja będzie zwracać pola typu deklaratywnego. Dostępność członka będzie zgodna z dostępnością w polu fixed
.
Na przykład podpis indeksatora dla CharBuffer.Data
będzie następujący:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Jeśli podany indeks znajduje się poza zadeklarowaną granicą tablicy fixed
, zostanie zgłoszony IndexOutOfRangeException
. W przypadku podania stałej wartości zostanie zastąpiona bezpośrednim odwołaniem do odpowiedniego elementu. Jeśli stała nie znajduje się poza zadeklarowaną granicą, w tym przypadku wystąpi błąd czasu kompilacji.
Dla każdego buforu fixed
zostanie również wygenerowany nazwany akcesor, który umożliwia wykonywanie operacji get
i set
przez wartość. Oznacza to, że fixed
będą bardziej przypominać istniejącą semantykę tablic dzięki użyciu akcesora ref
oraz operacjom przeprowadzanym przez wartość get
i set
. Oznacza to, że kompilatory będą miały taką samą elastyczność podczas emitowania kodu korzystającego z buforów fixed
, jak podczas korzystania z tablic. To powinno ułatwić emitowanie operacji, takich jak await
na fixed
bufory.
Ma to również dodatkową korzyść, co sprawi, że bufory fixed
będą łatwiejsze do wykorzystania w innych językach. Nazwane indeksatory to funkcja, która istniała od wersji 1.0 platformy .NET. Nawet języki, które nie mogą bezpośrednio emitować nazwanego indeksatora, mogą z nich ogólnie korzystać (C# jest dobrym przykładem tego).
Magazyn zapasowy buforu zostanie wygenerowany przy użyciu atrybutu [InlineArray]
. Jest to mechanizm omówiony w problem 12320, który pozwala w szczególności na efektywne deklarowanie sekwencji pól tego samego typu. Ten konkretny problem jest nadal przedmiotem aktywnej dyskusji i oczekuje się, że implementacja tej funkcji będzie zależeć od wyników tej dyskusji.
Inicjatory z wartościami ref
w wyrażeniach new
i with
W sekcji 12.8.17.3 inicjatory obiektów, aktualizujemy gramatykę do:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
W sekcji dla wyrażenia with
zaktualizujemy gramatykę na:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
Lewy operand przypisania musi być wyrażeniem, które wiąże się z polem ref.
Prawy operand musi być wyrażeniem, które zwraca wartość lvalue określającą wartość tego samego typu co lewy operand.
Dodajemy podobną regułę do ref lokalne przypisanie:
Jeśli lewy operand jest zapisywalnym ref (tj. wyznacza coś innego niż pole ref readonly
), prawy operand musi być zapisywalną wartością lvalue.
Reguły ucieczki dla wywołań konstruktora pozostają:
Wyrażenie
new
, które wywołuje konstruktor, przestrzega tych samych reguł co wywołanie metody, które zwraca konstruowany typ.
Mianowicie reguły wywołania metody zaktualizowane powyżej:
Wartość rvalue wynikająca z wywołania metody
e1.M(e2, ...)
ma bezpieczny kontekst z najmniejszego z następujących kontekstów:
- kontekst wywołujący
- bezpieczny kontekst dostarczony przez wszystkie wyrażenia argumentów
- Gdy zwrot jest
ref struct
, to kontekst ref-safe jest wynikiem wkładu wszystkich argumentówref
.
W przypadku wyrażenia new
z inicjatorami, wyrażenia inicjalizujące są liczone jako argumenty (przyczyniają się do swojego bezpiecznego kontekstu) i wyrażenia inicjalizujące ref
są liczone jako argumenty ref
(przyczyniają się one do swojego ref-safe-context), rekursywnie.
Zmiany w niebezpiecznym kontekście
Typy wskaźników (sekcja23.3) zostały rozszerzone, aby umożliwiać wykorzystywanie typów zarządzanych jako typów odniesienia.
Takie typy wskaźników są zapisywane jako typ zarządzany, po którym następuje token *
. Generują ostrzeżenie.
Operator address-of ( sekcja23.6.5) jest zrelaksowany do akceptowania zmiennej o typie zarządzanym jako operand.
Instrukcja fixed
(sekcja23.7) została złagodzona, aby akceptować fixed_pointer_initializer, który jest adresem zmiennej typu zarządzanego T
lub wyrażeniem typu array_type z elementami typu zarządzanego T
.
Inicjalizator alokacji stosu (sekcja12.8.22) jest podobnie złagodzony.
Zagadnienia dotyczące
Inne elementy stosu rozwojowego powinny być brane pod uwagę podczas oceniania tej funkcji.
Zagadnienia dotyczące zgodności
Wyzwaniem w tym wniosku jest wpływ zgodności tego projektu na nasze istniejące obejmują zasady bezpieczeństwa, lub §9.7.2. Podczas gdy te reguły w pełni wspierają koncepcję ref struct
mającego pola ref
, nie pozwalają one na to, aby interfejsy API, z wyjątkiem stackalloc
, przechwytywały stan ref
odnoszący się do stosu. Reguły kontekstu bezpiecznego ref mają twarde założenielub §16.4.12.8, że konstruktor formularza Span(ref T value)
nie istnieje. Oznacza to, że reguły bezpieczeństwa nie uwzględniają parametru ref
, który może uciec jako pole ref
, dlatego umożliwia kod podobny do poniższego.
Span<int> CreateSpanOfInt()
{
// This is legal according to the 7.2 span rules because they do not account
// for a constructor in the form Span(ref T value) existing.
int local = 42;
return new Span<int>(ref local);
}
W rzeczywistości istnieją trzy sposoby, aby parametr ref
uciekł z wywołania metody:
- Według wartości zwracanej
- Do
ref
powrócić - Na podstawie pola
ref
wref struct
, które jest zwracane lub przekazywane jako parametrref
/out
Istniejące reguły uwzględniają tylko (1) i (2). Nie uwzględniają one (3), dlatego luki, takie jak zwracanie zmiennych lokalnych jako pola ref
, nie są brane pod uwagę. Ten projekt musi zmienić zasady, aby uwzględnić (3). Będzie to miało niewielki wpływ na zgodność z istniejącymi interfejsami API. W szczególności będzie to miało wpływ na interfejsy API, które mają następujące właściwości.
- Posiadanie
ref struct
w podpisie- Gdzie
ref struct
jest typem zwracanym,ref
lubout
parametru - Z wyłączeniem odbiornika, ma dodatkowy parametr
in
lubref
.
- Gdzie
W języku C# 10 osoby wywołujące takie interfejsy API nigdy nie musiały brać pod uwagę, że wejście do stanu ref
w interfejsie API mogło zostać przechwycone jako pole ref
. To pozwala na istnienie kilku wzorców, które są bezpieczne w C# 10, ale staną się niebezpieczne w C# 11 ze względu na możliwość ucieczki stanu ref
jako pole ref
. Na przykład:
Span<int> CreateSpan(ref int parameter)
{
// The implementation of this method is irrelevant when considering the lifetime of the
// returned Span<T>. The ref safe context rules only look at the method signature, not the
// implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
// to escape by ref in this method
}
Span<int> BadUseExamples(int parameter)
{
// Legal in C# 10 but would be illegal with ref fields
return CreateSpan(ref parameter);
// Legal in C# 10 but would be illegal with ref fields
int local = 42;
return CreateSpan(ref local);
// Legal in C# 10 but would be illegal with ref fields
Span<int> span = stackalloc int[42];
return CreateSpan(ref span[0]);
}
Oczekuje się, że wpływ tej przerwy zgodności będzie bardzo mały. Kształt interfejsu API, na który ma wpływ, nie ma sensu w przypadku braku pól ref
, dlatego jest mało prawdopodobne, aby klienci utworzyli wiele z nich. Eksperymenty uruchamiające narzędzia do odnajdywania tej struktury API w istniejących repozytoriach potwierdzają tę tezę. Jedynym repozytorium z jakąkolwiek znaczącą liczbą tego kształtu jest dotnet/runtime, a to dlatego, że to repozytorium może tworzyć pola ref
za pośrednictwem typu wbudowanego ByReference<T>
.
Mimo to projekt musi uwzględniać istnienie takich interfejsów API, ponieważ wyrażają one prawidłowy wzorzec, po prostu nieczęsto spotykany. Dlatego projekt musi dostarczyć deweloperom narzędzi do przywrócenia istniejących reguł cyklu życia przy aktualizacji do C# 10. W szczególności musi zawierać mechanizmy, które umożliwiają deweloperom dodawanie adnotacji do parametrów ref
, ponieważ nie mogą uciec przez pole ref
lub ref
. Dzięki niemu klienci mogą definiować interfejsy API w języku C# 11, które mają te same reguły miejsca wywołania w języku C# 10.
Zestawy odwołań
Zestaw referencyjny do kompilacji z użyciem funkcji opisanych w tej propozycji musi utrzymywać elementy, które zawierają informacje o bezpiecznym kontekście "ref". Oznacza to, że wszystkie atrybuty adnotacji dotyczących okresu istnienia muszą być zachowane w ich pierwotnej pozycji. Każda próba zastąpienia lub pominięcia ich może prowadzić do nieprawidłowych zestawów odwołań.
Reprezentacja pól ref
jest bardziej zniuansowana. Najlepiej, aby pole ref
było wyświetlane w zestawie odniesienia, tak jak w przypadku każdego innego pola. Jednak pole ref
reprezentuje zmianę formatu metadanych i może to powodować problemy z łańcuchami narzędzi, które nie są aktualizowane, aby zrozumieć tę zmianę metadanych. Konkretny przykład to C++/CLI, który prawdopodobnie zgłosi błąd, jeśli przetwarza pole ref
. W związku z tym jest to korzystne, jeśli ref
pola można pominąć z zestawów odwołań w naszych podstawowych bibliotekach.
Samo pole ref
nie ma wpływu na reguły bezpiecznego kontekstu ref. Jako konkretny przykład należy wziąć pod uwagę, że przerzucanie istniejącej definicji Span<T>
do używania pola ref
nie ma wpływu na zużycie. W związku z tym ref
można bezpiecznie pominąć. Jednak pole ref
ma inne skutki dla zużycia, które muszą zostać zachowane.
-
ref struct
, który ma poleref
, nigdy nie jest uznawany zaunmanaged
- Typ pola
ref
ma wpływ na nieskończone ogólne reguły rozszerzania. W związku z tym, jeśli typ polaref
zawiera parametr typu, który musi zostać zachowany
Biorąc pod uwagę te reguły, jest to prawidłowa transformacja zestawu referencyjnego dla ref struct
:
// Impl assembly
ref struct S<T>
{
ref T _field;
}
// Ref assembly
ref struct S<T>
{
object _o; // force managed
T _f; // maintain generic expansion protections
}
Adnotacje
Okresy istnienia są najbardziej naturalnie wyrażane przy użyciu typów. Bezpieczeństwo okresów istnienia danego programu jest zapewnione, gdy sprawdzanie zgodności typów okresów istnienia jest zakończone powodzeniem. Chociaż składnia języka C# niejawnie dodaje okresy istnienia do wartości, istnieje podstawowy system typów, który opisuje tutaj podstawowe reguły. Często łatwiej jest omówić implikację zmian w projekcie pod względem tych zasad, więc są one zawarte tutaj w celu dyskusji.
Należy pamiętać, że nie jest to 100% kompletna dokumentacja. Dokumentowanie każdego pojedynczego zachowania nie jest tutaj celem. Zamiast tego ma to na celu ustalenie ogólnego zrozumienia i wspólnego czasownika, za pomocą którego można omówić model i potencjalne zmiany.
Zazwyczaj nie trzeba bezpośrednio mówić o typach okresu istnienia. Wyjątki to miejsca, gdzie okresy istnienia mogą się różnić w zależności od konkretnych miejsc "instancji". Jest to rodzaj polimorfizmu i nazywamy te różne okresy istnienia "ogólne okresy istnienia", reprezentowane jako parametry ogólne. Język C# nie udostępnia składni dla ogólnych parametrów dotyczących czasu życia, dlatego definiujemy niejawną translację z języka C# na rozszerzony język niższego poziomu, który zawiera jawne parametry ogólne.
W poniższych przykładach użyto nazwanych cykli życia. Składnia $a
odnosi się do okresu życia o nazwie a
. Jest to okres istnienia, który sam nie ma znaczenia, ale może mieć relację z innymi okresami istnienia za pośrednictwem składni where $a : $b
. Określa to, że $a
jest konwertowany na $b
. Może być pomocne myślenie o tym jako o określeniu, że $a
jest okresem istnienia co najmniej tak długim, jak $b
.
Istnieje kilka wstępnie zdefiniowanych okresów istnienia dla wygody i zwięzłości poniżej:
-
$heap
: to jest okres trwania każdej wartości znajdującej się w stercie. Jest ona dostępna we wszystkich kontekstach i sygnaturach metod. -
$local
: jest to okres istnienia dowolnej wartości, która istnieje na stosie metody. Jest to w rzeczywistości symbol zastępczy nazwy dla członka funkcji. Jest ona niejawnie zdefiniowana w metodach i może być wyświetlana w podpisach metod z wyjątkiem dowolnej pozycji wyjściowej. -
$ro
: symbol zastępczy nazwy dla zwraca tylko -
$cm
: identyfikator zastępczy dla kontekstu wywołującego
Istnieje kilka wstępnie zdefiniowanych relacji między okresami istnienia:
-
where $heap : $a
dla wszystkich okresów żywotności$a
where $cm : $ro
-
where $x : $local
dla wszystkich wstępnie zdefiniowanych okresów istnienia. Okresy istnienia zdefiniowane przez użytkownika nie mają związku z lokalnymi, chyba że zostaną one jawnie określone.
Zmienne okresu istnienia zdefiniowane dla typów mogą być niezmienne lub kowariantne. Są one wyrażane przy użyciu tej samej składni co parametry ogólne:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Parametr okresu istnienia $this
definicji typów jest nie wstępnie zdefiniowany, ale ma kilka skojarzonych z nim reguł:
- Musi to być pierwszy parametr okresu istnienia.
- Musi być kowariantny:
out $this
. - Okres istnienia pól
ref
musi być konwertowany na$this
- Okres istnienia
$this
wszystkich pól innych niż ref musi być$heap
lub$this
.
Czas życia odwołania jest wyrażany przez podanie argumentu czasu życia do odwołania. Na przykład ref
, które odnosi się do sterty, jest przedstawiane jako ref<$heap>
.
Podczas definiowania konstruktora w modelu nazwa new
będzie używana dla metody . Konieczne jest posiadanie listy parametrów dla zwróconej wartości, a także argumentów konstruktora. Jest to konieczne, aby wyrazić relację między danymi wejściowymi konstruktora a skonstruowaną wartością. Zamiast Span<$a><$ro>
model będzie używać Span<$a> new<$ro>
. Typ this
w konstruktorze, łącznie z okresami istnienia, będzie stanowił zdefiniowaną wartość zwracaną.
Podstawowe zasady okresu trwałości są definiowane jako:
- Wszystkie okresy istnienia są wyrażane składniowo jako argumenty ogólne, pochodzące przed argumentami typu. Dotyczy to wstępnie zdefiniowanych okresów istnienia z wyjątkiem
$heap
i$local
. - Wszystkie typy
T
, które nie sąref struct
, niejawnie mają okres istnieniaT<$heap>
. Jest to niejawne, nie ma potrzeby pisaniaint<$heap>
w każdym przykładzie. - W przypadku pola
ref
zdefiniowanego jakoref<$l0> T<$l1, $l2, ... $ln>
:- Wszystkie okresy istnienia
$l1
do$ln
należy pozostawić niezmiennymi. - Okres istnienia
$l0
musi być konwertowany na$this
- Wszystkie okresy istnienia
- Dla
ref
zdefiniowanego jakoref<$a> T<$b, ...>
,$b
musi być konwertowane na$a
-
ref
zmiennej ma okres istnienia zdefiniowany przez:- W przypadku
ref
lokalnego, parametru, pola lub zwrotu typuref<$a> T
czas życia to$a
-
$heap
dla wszystkich typów odwołań i typów pól referencyjnych -
$local
dla wszystkiego innego
- W przypadku
- Przypisanie lub zwrot jest legalne, gdy konwersja typu bazowego jest legalna
- Czas życia wyrażeń można uczynić bardziej przejrzystym, stosując adnotacje rzutowania.
-
(T<$a> expr)
okres istnienia wartości jest jawnie$a
dlaT<...>
-
ref<$a> (T<$b>)expr
okres istnienia wartości wynosi$b
dlaT<...>
, a okres istnienia referencji wynosi$a
.
-
W przypadku reguł okresu istnienia ref
jest traktowana jako część typu wyrażenia na potrzeby konwersji. Jest on logicznie reprezentowany przez konwersję ref<$a> T<...>
na ref<$a, T<...>>
, gdzie $a
jest kowariantna i T
jest niezmienna.
Następnie zdefiniujmy reguły, które umożliwiają mapowanie składni języka C# na bazowy model.
W celu zachowania zwięzłości, typ, który nie ma jawnych parametrów życia, jest traktowany tak, jakby out $this
jest zdefiniowany i zastosowany do wszystkich pól typu. Typ z polem ref
musi definiować jawne parametry okresu istnienia.
Te zasady istnieją, aby wspierać nasze istniejące założenie niezmienności, że T
można przypisać do scoped T
dla wszystkich typów. Odwzorowuje się to w ten sposób, że T<$a, ...>
można przypisać do T<$local, ...>
dla wszystkich okresów życia, które można przekonwertować na $local
. Ponadto obsługuje to inne elementy, takie jak możliwość przypisania Span<T>
z sterta do tych na stosie. Wyklucza to typy, w których pola mają różne okresy istnienia wartości innych niż ref, ale jest to rzeczywistość języka C#. Zmiana wymagałaby znacznej zmiany reguł języka C#, które musiałyby zostać opracowane.
Typ this
dla typu S<out $this, ...>
wewnątrz metody wystąpienia jest niejawnie zdefiniowany jako następujący:
- W przypadku metody wystąpienia normalnego:
ref<$local> S<$cm, ...>
- Na przykład metoda oznaczona
[UnscopedRef]
:ref<$ro> S<$cm, ...>
Brak jawnego parametru this
wymusza w tym miejscu niejawne reguły. W przypadku złożonych przykładów i dyskusji rozważ zastosowanie metody static
i uczynienie z this
jawnego parametru.
ref struct S<out $this>
{
// Implicit this can make discussion confusing
void M<$ro, $cm>(ref<$ro> S<$cm> s) { }
// Rewrite as explicit this to simplify discussion
static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}
Składnia metody języka C# odpowiada modelowi w następujący sposób:
- parametry
ref
mają ref okres istnienia$ro
- parametry typu
ref struct
mają taki czas trwania$cm
- Zwroty referencyjne mają okres życia
$ro
- zwracane wartości typu
ref struct
mają okres istnienia wartości$ro
-
scoped
w parametrze lubref
zmienia czas życia referencji na$local
Przyjrzyjmy się prostemu przykładzie, który demonstruje model.
ref int M1(ref int i) => ...
// Maps to the following.
ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
// okay: has ref lifetime $ro which is equal to $ro
return ref i;
// okay: has ref lifetime $heap which convertible $ro
int[] array = new int[42];
return ref array[0];
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Teraz przyjrzyjmy się temu samemu przykładowi przy użyciu ref struct
:
ref struct S
{
ref int Field;
S(ref int f)
{
Field = ref f;
}
}
S M2(ref int i, S span1, scoped S span2) => ...
// Maps to
ref struct S<out $this>
{
// Implicitly
ref<$this> int Field;
S<$ro> new<$ro>(ref<$ro> int f)
{
Field = ref f;
}
}
S<$ro> M2<$ro>(
ref<$ro> int i,
S<$ro> span1)
S<$local> span2)
{
// okay: types match exactly
return span1;
// error: has lifetime $local which has no conversion to $ro
return span2;
// okay: type S<$heap> has a conversion to S<$ro> because $heap has a
// conversion to $ro and the first lifetime parameter of S<> is covariant
return default(S<$heap>)
// okay: the ref lifetime of ref $i is $ro so this is just an
// identity conversion
S<$ro> local = new S<$ro>(ref $i);
return local;
int[] array = new int[42];
// okay: S<$heap> is convertible to S<$ro>
return new S<$heap>(ref<$heap> array[0]);
// okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These
// are convertible.
return new S<$ro>(ref<$heap> array[0]);
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Następnie zobaczmy, jak pomaga to w przypadku cyklicznego problemu z przypisywaniem własnym:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
s.refField = ref s.field;
}
}
// Maps to
ref struct S<out $this>
{
int field;
ref<$this> int refField;
static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
{
// error: the types work out here to ref<$cm> int = ref<$ro> int and that is
// illegal as $ro has no conversion to $cm (the relationship is the other direction)
s.refField = ref<$ro> s.field;
}
}
Następnie zobaczmy, jak to pomaga w przypadku problemu z głupim przechwytywaniem parametrów:
ref struct S
{
ref int refField;
void Use(ref int parameter)
{
// error: this needs to be an error else every call to this.Use(ref local) would fail
// because compiler would assume the `ref` was captured by ref.
this.refField = ref parameter;
}
}
// Maps to
ref struct S<out $this>
{
ref<$this> int refField;
// Using static form of this method signature so the type of this is explicit.
static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
{
// error: the types here are:
// - refField is ref<$cm> int
// - ref parameter is ref<$ro> int
// That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and
// hence this reassignment is illegal
@this.refField = ref<$ro> parameter;
}
}
Otwarte problemy
Zmień projekt, aby uniknąć przerw zgodności
Ten projekt proponuje kilka naruszeń zgodności z istniejącymi regułami ref-safe-context. Mimo że uważa się, że zmiany mają minimalny wpływ, znaczną uwagę poświęcono projektowi, który nie wprowadzał żadnych zmian powodujących niezgodności.
Projekt zachowujący kompatybilność był jednak znacznie bardziej złożony niż ten. Aby zachować pola ref
zgodności, muszą mieć różne okresy istnienia, aby umożliwić zwracanie ich przez ref
i zwracanie ich przez pole ref
. Zasadniczo wymaga to dostarczenia ref-field-safe-context śledzenia wszystkich parametrów w metodzie. Należy to obliczyć dla wszystkich wyrażeń i śledzić we wszystkich wartościach praktycznie wszędzie tam, gdzie obecnie śledzony jest kontekst ref-safe-context.
Ponadto ta wartość ma relacje z ref-safe-context. Na przykład nie jest sensyczne, aby wartość mogła zostać zwrócona jako pole ref
, ale nie bezpośrednio jako ref
. Wynika to z faktu, że pola ref
mogą być już trywialnie zwracane przez ref
(stanref
w ref struct
może być zwracany przez ref
nawet wtedy, gdy zawierająca wartość nie może). W związku z tym przepisy wymagają dalszego stałego dostosowania, aby zapewnić, że te wartości są rozsądne w odniesieniu do siebie nawzajem.
Oznacza to również, że język musi mieć składnię do reprezentowania parametrów ref
, które mogą być zwracane na trzy różne sposoby: przez pole ref
, przez ref
i przez wartość. Wartość domyślna jest zwracana przez ref
. W przyszłości jednak bardziej naturalnego powrotu, szczególnie gdy ref struct
są zaangażowane, oczekuje się przez pole ref
lub ref
. Oznacza to, że nowe interfejsy API wymagają domyślnie dodatkowej adnotacji składniowej. Jest to niepożądane.
Te zmiany kompatybilności jednak będą miały wpływ na metody, które mają następujące właściwości:
- Miej
Span<T>
alboref struct
- Gdzie
ref struct
jest typem zwracanym,ref
lubout
parametru - Ma dodatkowy parametr
in
lubref
(z wyłączeniem odbiornika)
- Gdzie
Aby zrozumieć wpływ, warto podzielić interfejsy API na kategorie:
- Zależy Ci, aby użytkownicy uwzględniali
ref
jako poleref
. Typowy przykład to konstruktorySpan(ref T value)
- Nie należy, aby konsumenci traktowali
ref
jako poleref
. Są one jednak podzielone na dwie kategorie- Niebezpieczne interfejsy API. Są to interfejsy API wewnątrz typów
Unsafe
iMemoryMarshal
, z którychMemoryMarshal.CreateSpan
jest najbardziej wyróżniający się. Te interfejsy API przechwytująref
niebezpiecznie, ale są one również znane jako niebezpieczne interfejsy API. - Bezpieczne interfejsy API. Są to interfejsy API, które przyjmują parametry
ref
dla wydajności, ale w rzeczywistości nie są one nigdzie przechwytywane. Przykłady są małe, ale jeden z nich toAsnDecoder.ReadEnumeratedBytes
- Niebezpieczne interfejsy API. Są to interfejsy API wewnątrz typów
Ta zmiana przynosi przede wszystkim korzyści (1) powyżej. Oczekuje się, że stanowią one większość interfejsów API, które przyjmują ref
i zwracają ref struct
w przyszłości. Zmiany negatywnie wpływają na (2.1) i (2.2), ponieważ zmieniają się reguły okresu istnienia, co zakłóca istniejącą semantykę wywołań.
Interfejsy API w kategorii (2.1) są jednak w dużej mierze tworzone przez firmę Microsoft lub przez tych deweloperów, którzy odnoszą największe korzyści z pól ref
(takich jak np. Tanner). Należy założyć, że ta klasa deweloperów byłaby skłonna do przyjęcia podatku kompatybilności przy uaktualnieniu do języka C# 11 w formie kilku adnotacji, aby zachować istniejącą semantykę, jeśli w zamian dostarczono by pola ref
.
API w kategorii (2.2) są największym problemem. Nie wiadomo, ile takich interfejsów API istnieje i nie jest jasne, czy byłyby one częstsze lub rzadziej występowały w kodzie strony trzeciej. Oczekuje się, że ich liczba będzie bardzo mała, szczególnie jeśli weźmiemy pod uwagę przerwę zgodności przy out
. Jak dotąd, wyszukiwania ujawniły bardzo małą liczbę tych obiektów na powierzchni public
. Jest to trudny wzorzec do wyszukania, ponieważ wymaga analizy semantycznej. Przed wprowadzeniem tej zmiany potrzebne byłoby podejście oparte na narzędziach, aby zweryfikować założenia dotyczące wpływu na niewielką liczbę znanych przypadków.
W obu przypadkach wspomnianych w kategorii (2) poprawka jest prosta. Parametry ref
, które nie chcą być uznawane za przechwytywalne, muszą dodać scoped
do ref
. W wersji (2.1) prawdopodobnie wymusi to również na deweloperze użycie Unsafe
lub MemoryMarshal
, ale jest to oczekiwane w przypadku niebezpiecznych interfejsów API stylizacji.
W idealnym przypadku język mógłby zmniejszyć wpływ cichych zmian powodujących niekompatybilność, wydając ostrzeżenie, gdy interfejs API cicho przechodzi w kłopotliwe zachowanie. Byłaby to metoda, która przyjmuje ref
, zwraca ref struct
, ale nie przechwytuje ref
w ref struct
. Kompilator może wydać ostrzeżenie w tym przypadku, informując programistów, że ref
powinno być oznaczone jako scoped ref
.
Decyzja Ten projekt można zrealizować, ale wynikowa funkcja jest bardziej skomplikowana w użyciu, co doprowadziło do decyzji o złamaniu zgodności.
Decision Kompilator wyświetli ostrzeżenie, gdy metoda spełnia kryteria, ale nie przechwytuje parametru ref
jako pola ref
. Powinno to odpowiednio ostrzegać klientów podczas uaktualnienia o potencjalnych problemach, które mogą się pojawić.
Słowa kluczowe a atrybuty
Ten projekt wymaga użycia atrybutów do dodawania adnotacji do nowych zasad dotyczących okresu życia. Można to również zrobić tak samo łatwo z kontekstowymi słowami kluczowymi. Na przykład [DoesNotEscape]
można przyporządkować do scoped
. Jednak słowa kluczowe, nawet te kontekstowe, zazwyczaj muszą spełniać bardzo wysoki próg, by zostać uwzględnionymi. Zajmują ważne miejsce w języku i stanowią bardziej znaczące jego części. Ta funkcja, choć cenna, będzie obsługiwać mniejszość deweloperów języka C#.
Na pierwszy rzut oka, to wydaje się sugerować niewykorzystywanie słów kluczowych, ale istnieją dwie ważne kwestie do rozważenia:
- Adnotacje będą wpływać na semantykę programu. C# jest niechętny do wprowadzania atrybutów, które wpływają na semantykę programu, i nie jest jasne, czy ta funkcja jest powodem, dla którego język powinien podjąć taki krok.
- Deweloperzy, którzy najprawdopodobniej będą używać tej funkcji, silnie pokrywają się z grupą deweloperów korzystających ze wskaźników funkcji. Ta funkcja, choć używana również przez mniejszość deweloperów, uzasadniała nową składnię i ta decyzja jest nadal postrzegana jako rozsądna.
Razem oznacza to, że należy wziąć pod uwagę składnię.
Przybliżony szkic składni to:
-
[RefDoesNotEscape]
mapuje nascoped ref
-
[DoesNotEscape]
mapuje nascoped
-
[RefDoesEscape]
mapuje naunscoped
decyzja użyj składni dla scoped
i scoped ref
; użyj atrybutu dla unscoped
.
Zezwalaj na lokalne bufor stałe
Ten projekt umożliwia bezpieczne bufory fixed
, które mogą obsługiwać dowolny typ. Jedno z możliwych rozszerzeń pozwala na deklarowanie takich buforów fixed
jako zmiennych lokalnych. Umożliwiłoby to zastąpienie wielu istniejących operacji stackalloc
buforem fixed
. Rozszerzyłoby to również zestaw scenariuszy, w których moglibyśmy mieć alokacje w stylu stosu, ponieważ stackalloc
jest ograniczony do niezarządzanych typów elementarnych, podczas gdy bufory fixed
nie są.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
To się trzyma razem, ale wymaga od nas nieznacznego rozszerzenia składni dla zmiennych lokalnych. Niejasne, czy jest to lub nie jest warte dodatkowej złożoności. Możliwe, że moglibyśmy teraz zdecydować się na 'nie' i powrócić do tego później, jeśli zostanie wykazana wystarczająca potrzeba.
Przykład tego, gdzie byłoby korzystne: https://github.com/dotnet/runtime/pull/34149
decyzja wstrzymać się z tym na razie
Czy użyć modreqs, czy nie?
Należy podjąć decyzję, jeśli metody oznaczone nowymi atrybutami okresu istnienia powinny lub nie powinny być tłumaczone na modreq
w emitie. Byłoby skuteczne mapowanie 1:1 między adnotacjami a modreq
, gdyby to podejście zostało zastosowane.
Uzasadnieniem dodawania modreq
jest to, że atrybuty zmieniają semantykę reguł bezpiecznego kontekstu ref. Tylko języki, które rozumieją te semantyki, powinny wywoływać te metody. Ponadto w przypadku scenariuszy OHI, czas życia staje się kontraktem, który muszą implementować wszystkie metody pochodne. Gdy adnotacje istnieją bez modreq
, może to prowadzić do sytuacji, w których ładowane są virtual
łańcuchy metod z kolidującymi adnotacjami dotyczącymi okresu istnienia (może się to zdarzyć, jeśli tylko jedna część łańcucha virtual
jest skompilowana, a druga nie).
Początkowa praca w kontekście bezpiecznym dla odwołań nie korzystała z modreq
, ale zamiast tego opierała się na językach i ramach systemowych, aby zrozumieć. Jednocześnie wszystkie elementy, które przyczyniają się do reguł bezpiecznego kontekstu ref, są silną częścią podpisu metody: ref
, in
, ref struct
itp. W związku z tym każda zmiana istniejących reguł metody już powoduje zmianę binarną podpisu. Aby nowe adnotacje okresu istnienia miały taki sam wpływ, potrzebne będzie ich wymuszanie za pomocą modreq
.
Problemem jest to, czy to jest przesadne. Negatywne działanie polega na tym, że uczynienie podpisów bardziej elastycznymi, na przykład przez dodanie [DoesNotEscape]
do parametru, spowoduje zmianę zgodności binarnej. Ten kompromis oznacza, że struktury, takie jak BCL, prawdopodobnie nie będą w stanie w przyszłości złagodzić takich podpisów. Można to częściowo złagodzić, stosując podejście, które język przyjmuje względem parametrów in
, i stosując modreq
tylko w pozycjach wirtualnych.
Decyzja Nie używaj modreq
w metadanych. Różnica między out
a ref
nie polega na modreq
, lecz na tym, że teraz mają różne wartości kontekstu ref. Nie ma żadnych rzeczywistych korzyści tylko do połowy wymuszania zasad z modreq
tutaj.
Zezwalaj na wielowymiarowe stałe bufory
Czy projekt buforów fixed
powinien zostać rozszerzony w celu uwzględnienia wielowymiarowych tablic stylizacji? Zasadniczo zezwalanie na deklaracje podobne do następujących:
struct Dimensions
{
int array[42, 13];
}
decyzja Nie zezwalaj teraz
Naruszenie zakresu
Repozytorium środowiska uruchomieniowego zawiera kilka wewnętrznych API, które przechwytują parametry ref
jako pola ref
. Są one niebezpieczne, ponieważ okres istnienia wynikowej wartości nie jest śledzony. Na przykład konstruktor Span<T>(ref T value, int length)
.
Większość tych interfejsów API prawdopodobnie zdecyduje się na odpowiednie śledzenie okresu istnienia zwracanych wartości, co zostanie osiągnięte po prostu przez aktualizację do C# 11. Niektórzy jednak będą chcieli zachować swoją obecną semantykę niesledzenia wartości zwracanej, ponieważ ich intencja polega na byciu niebezpiecznymi. Najbardziej istotne przykłady to MemoryMarshal.CreateSpan
i MemoryMarshal.CreateReadOnlySpan
. Zostanie to osiągnięte przez oznaczenie parametrów jako scoped
.
Oznacza to, że środowisko uruchomieniowe wymaga ustalonego wzorca niebezpiecznego usuwania scoped
z parametru:
-
Unsafe.AsRef<T>(in T value)
może rozszerzyć swój istniejący cel, zmieniając się nascoped in T value
. Pozwoliłoby to zarówno usunąćin
, jak iscoped
z parametrów. Następnie staje się uniwersalną metodą usuwania zabezpieczeń odwołań. - Wprowadzenie nowej metody, której całym celem jest usunięcie
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Spowoduje to usunięciein
również dlatego, że jeśli nie, osoby wywołujące nadal potrzebują kombinacji wywołań metod w celu "usunięcia bezpieczeństwa ref", w którym to momencie istniejące rozwiązanie jest prawdopodobnie wystarczające.
Czy domyślnie pozbawić zakreślenia?
Projekt ma tylko dwie lokalizacje, które domyślnie mają wartość scoped
.
-
this
jestscoped ref
-
out
jestscoped ref
Decyzja dotycząca out
ma na celu znaczne zmniejszenie obciążenia kompatybilności dla pól ref
, a jednocześnie jest bardziej naturalnym domyślnym wyborem. Dzięki temu deweloperzy mogą traktować out
jako dane przepływające tylko na zewnątrz, podczas gdy w przypadku ref
, reguły muszą uwzględniać przepływ danych w obu kierunkach. Prowadzi to do poważnych nieporozumień deweloperów.
Decyzja w sprawie this
jest niepożądana, ponieważ oznacza, że struct
nie może zwrócić pola za pomocą ref
. Jest to ważny scenariusz dla deweloperów skoncentrowanych na wysokiej wydajności, a atrybut [UnscopedRef]
został dodany specjalnie dla tego jednego scenariusza.
Słowa kluczowe mają wysokie wymagania, a dodanie ich w jednym scenariuszu budzi wątpliwości. W związku z tym rozważano, czy w ogóle można uniknąć tego słowa kluczowego, tworząc this
po prostu ref
domyślnie, a nie scoped ref
. Wszyscy członkowie, którzy potrzebują, aby this
było scoped ref
, mogą to zrobić, oznaczając metodę scoped
(jako metodę można oznaczyć readonly
w celu stworzenia readonly ref
dzisiaj).
Na normalnym struct
jest to głównie pozytywna zmiana, ponieważ wprowadza problemy z zgodnością tylko wtedy, gdy członek ma powrót ref
. Istnieje bardzo kilka z tych metod i narzędzie może je wykryć i przekonwertować na scoped
członków szybko.
W ref struct
ta zmiana wprowadza znacznie większe problemy z zgodnością. Rozważ następujące kwestie:
ref struct Sneaky
{
int Field;
ref int RefField;
public void SelfAssign()
{
// This pattern of ref reassign to fields on this inside instance methods would now
// completely legal.
RefField = ref Field;
}
static Sneaky UseExample()
{
Sneaky local = default;
// Error: this is illegal, and must be illegal, by our existing rules as the
// ref-safe-context of local is now an input into method arguments must match.
local.SelfAssign();
// This would be dangerous as local now has a dangerous `ref` but the above
// prevents us from getting here.
return local;
}
}
Zasadniczo oznaczałoby to, że wszystkie wywołania metod instancji w lokalne zmienneref struct
modyfikowalne byłyby nielegalne, chyba że lokalne zostałyby dodatkowo oznaczone jako scoped
. Reguły muszą uwzględnić sytuację, w której pola zostały przypisane ponownie do innych pól w this
.
readonly ref struct
nie ma tego problemu, ponieważ readonly
charakter zapobiega ponownemu przypisaniu ref. Nadal byłaby to znacząca zmiana powodująca niekompatybilność wsteczną, ponieważ wpłynęłaby na praktycznie wszystkie istniejące modyfikowalne ref struct
.
Jednak readonly ref struct
nadal jest problematyczny, gdy rozszerzamy się na posiadanie pól od ref
do ref struct
. Rozwiązanie umożliwia rozwiązanie tego samego podstawowego problemu poprzez przeniesienie przechwytywania do wartości pola ref
.
readonly ref struct ReadOnlySneaky
{
readonly int Field;
readonly ref ReadOnlySpan<int> Span;
public void SelfAssign()
{
// Instance method captures a ref to itself
Span = new ReadOnlySpan<int>(ref Field, 1);
}
}
Zastanawiano się nad pomysłem, aby this
miało różne wartości domyślne w zależności od typu struct
lub członka. Na przykład:
-
this
jakoref
:struct
,readonly ref struct
lubreadonly member
-
this
jakoscoped ref
:ref struct
lubreadonly ref struct
z polemref
doref struct
Minimalizuje to przerwy w kompatybilności i maksymalizuje elastyczność, ale kosztem komplikowania sytuacji dla klientów. Nie rozwiązuje to również w pełni problemu, ponieważ przyszłe funkcje, takie jak bezpieczne fixed
, wymagają, aby modyfikowalne ref struct
miały ref
zwracane dla pól, które nie działają tylko w ramach tego projektu, ponieważ należą do kategorii scoped ref
.
decyzja zachować this
jako scoped ref
. Oznacza to, że powyższe podstępne przykłady generują błędy kompilatora.
ref pola do ref struktura
Ta funkcja otwiera nowy zestaw reguł bezpiecznego kontekstu odniesienia, ponieważ umożliwia polu ref
odwołanie się do ref struct
. Ten ogólny charakter ByReference<T>
oznaczał, że do tej pory środowisko uruchomieniowe nie mogło mieć takiej konstrukcji. W rezultacie wszystkie nasze zasady są napisane zgodnie z założeniem, że nie jest to możliwe. Funkcja pola ref
w znacznej mierze nie chodzi o tworzenie nowych reguł, ale o kodyfikowanie istniejących reguł w naszym systemie. Zezwolenie na ref
pól do ref struct
wymaga od nas ujednolicania nowych reguł, ponieważ należy wziąć pod uwagę kilka nowych scenariuszy.
Po pierwsze, readonly ref
jest teraz w stanie przechowywać stan ref
. Na przykład:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Oznacza to, że podczas analizowania argumentów metody pod kątem zgodności z regułami, należy rozważyć, że readonly ref T
jest potencjalnym wynikiem metody, gdy T
potencjalnie ma pole ref
prowadzące do ref struct
.
Drugim problemem jest język musi rozważyć nowy typ bezpiecznego kontekstu: ref-field-safe-context. Wszystkie ref struct
, które przechodnio zawierają pole ref
, mają inny zakres wyjścia reprezentujący wartości w polach ref
. W przypadku wielu pól ref
można je zbiorczo śledzić jako pojedynczą wartość. Wartość domyślna dla parametrów to kontekst wywołujący.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Ta wartość nie jest powiązana z bezpiecznym kontekstem kontenera; czyli, gdy kontekst kontenera staje się mniejszy, nie ma to wpływu na ref-field-safe-context wartości pól ref
. Ponadto kontekst bezpiecznego pola odniesienia nigdy nie może być mniejszy niż kontekst bezpieczny kontenera.
ref struct Nested
{
ref Span<int> Span;
}
void M(ref Nested nested)
{
scoped ref Nested refLocal = ref nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is illegal
refLocal.Span = stackalloc int[42];
scoped Nested valLocal = nested;
// the ref-field-safe-context of local is still *caller-context* which means the following
// is still illegal
valLocal.Span = stackalloc int[42];
}
Ten ref-field-safe-context zasadniczo zawsze istniał. Do tej pory pola ref
mogły wskazywać tylko na normalne struct
, dlatego zostały trywialnie zredukowane do kontekstu wywołującego . Aby obsługiwać pola ref
i ref struct
, należy zaktualizować istniejące reguły, aby uwzględniały ten nowy ref-safe-context.
Po trzecie, należy zaktualizować reguły ponownego przypisania ref, aby upewnić się, że nie naruszamy kontekstu ref-field-context dla wartości . Zasadniczo dla
Te problemy są łatwo rozwiązywalne. Zespół kompilatora naszkicował kilka wersji tych reguł i w dużej mierze wynikają one z naszej istniejącej analizy. Problem polega na tym, że dla takich reguł nie ma używanego kodu, który pomaga udowodnić poprawność i użyteczność. To sprawia, że jesteśmy bardzo niezdecydowani, aby dodać wsparcie z obawy, że wybierzemy nieprawidłowe wartości domyślne i wpędzimy środowisko uruchomieniowe w trudności z użytecznością, gdy zacznie z tego korzystać. Ten problem jest szczególnie silny, ponieważ .NET 8 prawdopodobnie kieruje nas w tym kierunku z allow T: ref struct
i Span<Span<T>>
. Reguły byłyby lepiej napisane, gdyby zostały stworzone w połączeniu z kodem konsumpcji.
decyzja Opóźnienie zezwalające ref
pole na ref struct
do platformy .NET 8, w których mamy scenariusze, które pomogą w kierowania regułami dotyczącymi tych scenariuszy. Nie zostało to zaimplementowane na platformie .NET 9
Co wprowadzi C# 11.0?
Funkcje opisane w tym dokumencie nie muszą być implementowane w jednym etapie. Zamiast tego można je zaimplementować etapami w ramach kilku wydań językowych w następujących kategoriach:
-
ref
pola iscoped
[UnscopedRef]
-
ref
pola doref struct
- Typy z ograniczeniami o zachodzie słońca
- bufory o stałym rozmiarze
To, co jest implementowane w danym wydaniu, jest jedynie ćwiczeniem określania zakresu.
decyzje dotyczące tylko (1) i (2) składają się na C# 11.0. Pozostałe będą brane pod uwagę w przyszłych wersjach języka C#.
Przyszłe zagadnienia
Zaawansowane adnotacje dotyczące okresu życia
Adnotacje dotyczące cyklu życia w tej propozycji są ograniczone w tym sensie, że umożliwiają deweloperom zmianę domyślnego zachowania dotyczącego ucieczki znaków wartości. Zapewnia to zaawansowaną elastyczność naszego modelu, ale nie zmienia radykalnie zestawu relacji, które można wyrazić. W rdzeniu model języka C# jest nadal skutecznie binarny: czy można zwrócić wartość, czy nie?
Pozwala to zrozumieć ograniczone relacje okresu istnienia. Na przykład wartość, której nie można zwrócić z metody, ma mniejszy okres istnienia niż wartość, którą można zwrócić z metody. Nie ma jednak możliwości opisania relacji okresu istnienia między wartościami, które mogą być zwracane z metody. W szczególności nie ma sposobu, aby stwierdzić, że jedna wartość ma dłuższy okres istnienia niż druga, gdy ustalono, że obie mogą być zwrócone z metody. Następnym krokiem w naszej ewolucji życia byłoby umożliwienie opisania takich relacji.
Inne metody, takie jak Rust, umożliwiają wyrażenia tego typu relacji, a tym samym mogą implementować bardziej złożone operacje scoped
stylu. Nasz język może podobnie przynieść korzyści, jeśli ta funkcja została uwzględniona. W tej chwili nie ma presji, aby to zrobić, ale jeśli pojawi się w przyszłości, nasz model scoped
można rozszerzyć, aby uwzględnić to w dość prosty sposób.
Każdemu scoped
można przypisać nazwany czas życia, dodając argument typu ogólnego do składni. Na przykład scoped<'a>
to wartość, która ma okres istnienia 'a
. Ograniczenia, takie jak where
, mogą być następnie używane do opisywania relacji między tymi okresami istnienia.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Ta metoda definiuje dwa okresy istnienia 'a
i 'b
i ich relację, w szczególności, że 'b
jest większa niż 'a
. Dzięki temu miejsce wywołania może mieć bardziej szczegółowe zasady dotyczące bezpiecznego przekazywania wartości do metod w porównaniu ze zgrubnymi zasadami obecnie.
Powiązane informacje
Problemy
Wszystkie następujące problemy są związane z tą propozycją:
- https://github.com/dotnet/csharplang/issues/1130
- https://github.com/dotnet/csharplang/issues/1147
- https://github.com/dotnet/csharplang/issues/992
- https://github.com/dotnet/csharplang/issues/1314
- https://github.com/dotnet/csharplang/issues/2208
- https://github.com/dotnet/runtime/issues/32060
- https://github.com/dotnet/runtime/issues/61135
- https://github.com/dotnet/csharplang/discussions/78
Propozycje
Następujące propozycje są związane z tą propozycją:
Istniejące przykłady
Ten konkretny fragment kodu wymaga użycia niebezpiecznego kodu, ponieważ ujawnia problemy związane z przekazywaniem Span<T>
, które mogą być przypisane do metody instancyjnej na ref struct
. Mimo że ten parametr nie jest przechwytywany, język musi zakładać, że jest to i dlatego niepotrzebnie powoduje tarcia tutaj.
Ten fragment kodu chce mutować parametr przez ucieczkę elementów danych. Dane mogą być efektywnie przydzielane na stosie. Mimo że parametr nie zostanie usunięty, kompilator przypisuje mu bezpieczny kontekst poza otaczającej metody, ponieważ jest to parametr. Oznacza to, że aby można było używać alokacji stosu, implementacja musi używać unsafe
w celu przypisania z powrotem do parametru po ucieczce danych.
Zabawne próbki
ReadOnlySpan<T>
public readonly ref struct ReadOnlySpan<T>
{
readonly ref readonly T _value;
readonly int _length;
public ReadOnlySpan(in T value)
{
_value = ref value;
_length = 1;
}
}
Lista oszczędna
struct FrugalList<T>
{
private T _item0;
private T _item1;
private T _item2;
public int Count = 3;
public FrugalList(){}
public ref T this[int index]
{
[UnscopedRef] get
{
switch (index)
{
case 0: return ref _item0;
case 1: return ref _item1;
case 2: return ref _item2;
default: throw null;
}
}
}
}
Przykłady i notatki
Poniżej przedstawiono zestaw przykładów pokazujących, jak i dlaczego reguły działają tak, jak działają. Dołączono kilka przykładów pokazujących niebezpieczne zachowania i sposób, w jaki reguły uniemożliwiają im działanie. Ważne jest, aby pamiętać o nich podczas wprowadzania zmian w propozycji.
Ponowne przypisywanie referencji i miejsca wywołań
Demonstrujemy, jak ponowne przypisanie i wywołanie metody współpracują ze sobą.
ref struct RS
{
ref int _refField;
public ref int Prop => ref _refField;
public RS(int[] array)
{
_refField = ref array[0];
}
public RS(ref int i)
{
_refField = ref i;
}
public RS CreateRS() => ...;
public ref int M1(RS rs)
{
// The call site arguments for Prop contribute here:
// - `rs` contributes no ref-safe-context as the corresponding parameter,
// which is `this`, is `scoped ref`
// - `rs` contribute safe-context of *caller-context*
//
// This is an lvalue invocation and the arguments contribute only safe-context
// values of *caller-context*. That means `local1` has ref-safe-context of
// *caller-context*
ref int local1 = ref rs.Prop;
// Okay: this is legal because `local` has ref-safe-context of *caller-context*
return ref local1;
// The arguments contribute here:
// - `this` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `this` contributes safe-context of *caller-context*
//
// This is an rvalue invocation and following those rules the safe-context of
// `local2` will be *caller-context*
RS local2 = CreateRS();
// Okay: this follows the same analysis as `ref rs.Prop` above
return ref local2.Prop;
// The arguments contribute here:
// - `local3` contributes ref-safe-context of *function-member*
// - `local3` contributes safe-context of *caller-context*
//
// This is an rvalue invocation which returns a `ref struct` and following those
// rules the safe-context of `local4` will be *function-member*
int local3 = 42;
var local4 = new RS(ref local3);
// Error:
// The arguments contribute here:
// - `local4` contributes no ref-safe-context as the corresponding parameter
// is `scoped ref`
// - `local4` contributes safe-context of *function-member*
//
// This is an lvalue invocation and following those rules the ref-safe-context
// of the return is *function-member*
return ref local4.Prop;
}
}
Ponowne przypisywanie ref i niebezpieczne ucieczki
Powód następującej linii w regułach ponownego przypisania odniesienia może nie być oczywisty na pierwszy rzut oka:
e1
musi mieć taki sam bezpieczny kontekst jake2
Dzieje się tak, ponieważ okres istnienia wartości wskazywanych przez lokalizacje ref
jest niezmienny. Pośrednictwo uniemożliwia nam zezwolenie na jakąkolwiek wariancję w tym miejscu, nawet w przypadku węższych czasów życia. Jeśli zawężenie jest dozwolone, zostanie otwarty następujący niebezpieczny kod:
void Example(ref Span<int> p)
{
Span<int> local = stackalloc int[42];
ref Span<int> refLocal = ref local;
// Error:
// The safe-context of refLocal is narrower than p. For a non-ref reassignment
// this would be allowed as its safe to assign wider lifetimes to narrower ones.
// In the case of ref reassignment though this rule prevents it as the
// safe-context values are different.
refLocal = ref p;
// If it were allowed this would be legal as the safe-context of refLocal
// is *caller-context* and that is satisfied by stackalloc. At the same time
// it would be assigning through p and escaping the stackalloc to the calling
// method
//
// This is equivalent of saying p = stackalloc int[13]!!!
refLocal = stackalloc int[13];
}
Dla przejścia z ref
do nie ref struct
ta reguła jest trywialnie spełniona, ponieważ wszystkie wartości mają ten sam bezpieczny kontekst. Ta reguła naprawdę wchodzi w grę tylko wtedy, gdy wartość jest ref struct
.
To zachowanie ref
będzie również ważne w przyszłości, w której pozwolimy, aby pola ref
mogły ref struct
.
lokalne o określonym zakresie
Użycie scoped
w zmiennych lokalnych będzie szczególnie przydatne w przypadku wzorców kodu, które warunkowo przypisują wartości z różnymi bezpiecznymi kontekstami do zmiennych lokalnych. Oznacza to, że kod nie musi już polegać na trikach inicjalizacyjnych, takich jak = stackalloc byte[0]
do zdefiniowania lokalnego bezpiecznego kontekstu, lecz teraz można po prostu użyć scoped
.
// Old way
// Span<byte> span = stackalloc byte[0];
// New way
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
span = stackalloc byte[len];
}
else
{
span = new byte[len];
}
Ten wzorzec występuje często w kodzie niskiego poziomu. Gdy zaangażowany ref struct
jest Span<T>
można użyć powyższej sztuczki. Nie ma jednak zastosowania do innych typów ref struct
i może spowodować, że kod niskopoziomowy będzie musiał uciec się do unsafe
, aby obejść problem z określeniem czasu życia.
wartości parametrów o określonym zakresie
Jednym ze źródeł powtarzających się tarć w kodzie niskiego poziomu jest to, że domyślne traktowanie parametrów jest zbyt liberalne. Są one w bezpiecznym kontekście wobec kontekstu wywołującego. Jest to rozsądna wartość domyślna, ponieważ jest zgodna ze schematami kodowania platformy .NET jako całości. W kodzie niskiego poziomu istnieje większe użycie ref struct
i to ustawienie domyślne może powodować tarcie z innymi częściami reguł bezpiecznego kontekstu ref.
Główny punkt tarcia występuje dlatego, że argumenty metody muszą być zgodne z regułą. Ta reguła najczęściej wchodzi w grę z metodami wystąpień w ref struct
, gdzie co najmniej jeden parametr jest również ref struct
. Jest to typowy wzorzec w kodzie niskiego poziomu, w którym typy ref struct
często wykorzystują parametry Span<T>
w swoich metodach. Na przykład wystąpi w każdym stylu pisania ref struct
, który używa Span<T>
do przekazywania buforów.
Ta reguła istnieje, aby zapobiec scenariuszom, takim jak następujące:
ref struct RS
{
Span<int> _field;
void Set(Span<int> p)
{
_field = p;
}
static void DangerousCode(ref RS p)
{
Span<int> span = stackalloc int[] { 42 };
// Error: if allowed this would let the method return a reference to
// the stack
p.Set(span);
}
}
Zasadniczo ta reguła istnieje, ponieważ język musi zakładać, że wszystkie dane wejściowe do metody są traktowane jako uciekające w maksymalnym dozwolonym bezpiecznym kontekście . W przypadku parametrów ref
lub out
, w tym odbiorników, dane wejściowe mogą uciekać jako pola tych wartości ref
(jak to się dzieje w RS.Set
powyżej).
W praktyce istnieje wiele takich metod, które przekazują ref struct
jako parametry, które nigdy nie zamierzają przechwytywać ich w danych wyjściowych. Jest to tylko wartość używana w bieżącej metodzie. Na przykład:
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Error: The safe-context of `span` is function-member
// while `reader` is outside function-member hence this fails
// by the above rule.
if (reader.TextEquals(span))
{
...
}
}
}
Aby wykorzystać ten kod niskiego poziomu, będzie się uciekać do sztuczek unsafe
, aby okłamać kompilator w kwestii cyklu życia ich ref struct
. Znacznie zmniejsza to wartość ref struct
, ponieważ mają one być środkiem, aby uniknąć unsafe
podczas pisania kodu o wysokiej wydajności.
W tym przypadku scoped
jest skutecznym narzędziem do zarządzania parametrami ref struct
, ponieważ eliminuje je z rozważania jako zwracane przez metodę. Zgodnie z zaktualizowaną regułą zgodności, argumenty metody muszą być zgodne z zasadą. Parametr ref struct
, który jest używany, ale nigdy nie zwracany, może być oznaczony jako scoped
, aby zwiększyć elastyczność miejsc wywołań.
ref struct JsonReader
{
Span<char> _buffer;
int _position;
internal bool TextEquals(scoped ReadOnlySpan<char> text)
{
var current = _buffer.Slice(_position, text.Length);
return current == text;
}
}
class C
{
static void M(ref JsonReader reader)
{
Span<char> span = stackalloc char[4];
span[0] = 'd';
span[1] = 'o';
span[2] = 'g';
// Okay: the compiler never considers `span` as capturable here hence it doesn't
// contribute to the method arguments must match rule
if (reader.TextEquals(span))
{
...
}
}
}
Zapobieganie trudnemu przypisaniu ref z mutacji readonly
Gdy ref
zostanie przeniesiony do pola readonly
w konstruktorze lub init
składowym, typ jest ref
, a nie ref readonly
. Jest to długotrwałe zachowanie, które umożliwia kod podobny do następującego:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
To jednak stanowi potencjalny problem, jeśli taki ref
można było przechowywać w polu ref
w tym samym typie. Pozwoliłoby to na bezpośrednią mutację readonly struct
z członka instancji:
readonly ref struct S
{
readonly int i;
readonly ref int r;
public S()
{
i = 0;
// Error: `i` has a narrower scope than `r`
r = ref i;
}
public void Oops()
{
r++;
}
}
Wniosek uniemożliwia to jednak, ponieważ narusza zasady bezpiecznego kontekstu ref. Rozważ następujące kwestie:
-
kontekstu bezpiecznego
this
to element funkcji, a w bezpiecznym kontekście jest kontekst wywołujący. Są one standardem dlathis
wstruct
elementu członkowskiego. -
ref-safe-context
i
jest funkcji-członkowskiej. Wynika to z reguł dotyczących czasów życia pól . W szczególności reguła 4.
W tym momencie linia r = ref i
jest niezgodna z regułami przypisania referencji .
Zasady te nie miały na celu zapobiegania temu zachowaniu, ale robią to jako efekt uboczny. Należy pamiętać o każdej przyszłej aktualizacji reguły, aby ocenić wpływ na takie scenariusze.
Głupie przypisanie cykliczne
Jednym aspektem, z jakim zmagał się ten projekt, jest to, jak swobodnie można zwrócić ref
z metody. Zezwalanie na zwracanie wszystkich ref
równie swobodnie jak normalnych wartości jest tym, czego programiści intuicyjnie oczekują. Jednak pozwala to na patologiczne scenariusze, które kompilator musi wziąć pod uwagę podczas obliczania bezpieczeństwa ref. Rozważ następujące kwestie:
ref struct S
{
int field;
ref int refField;
static void SelfAssign(ref S s)
{
// Error: s.field can only escape the current method through a return statement
s.refField = ref s.field;
}
}
Nie jest to wzorzec kodu, którego oczekujemy od deweloperów. Jednak gdy ref
można zwrócić z tym samym okresem istnienia co wartość, jest to zgodne z zasadami. Kompilator musi wziąć pod uwagę wszystkie możliwe przypadki podczas oceniania wywołania metody, co prowadzi do tego, że takie interfejsy API stają się praktycznie nieużywalne.
void M(ref S s)
{
...
}
void Usage()
{
// safe-context to caller-context
S local = default;
// Error: compiler is forced to assume the worst and concludes a self assignment
// is possible here and must issue an error.
M(ref local);
}
Aby te interfejsy API były użyteczne, kompilator zapewnia, że czas życia ref
dla parametru ref
jest krótszy niż czas życia jakichkolwiek odwołań w powiązanej wartości parametru. Jest to uzasadnienie kontekstu bezpiecznego do ref struct
być out
tylko do zwrotu, a kontekstu wywołującego. Uniemożliwia to przypisanie cykliczne z powodu różnicy w czasach życia.
Należy pamiętać, że [UnscopedRef]
promuje poprzezref-safe-context dowolne wartości ref
do ref struct
do kontekstu wywołującego , co umożliwia cykliczne przypisywanie i wymusza rozpowszechnione użycie [UnscopedRef]
w górę łańcucha wywołań:
S F()
{
S local = new();
// Error: self assignment possible inside `S.M`.
S.M(ref local);
return local;
}
ref struct S
{
int field;
ref int refField;
public static void M([UnscopedRef] ref S s)
{
// Allowed: s has both safe-context and ref-safe-context of caller-context
s.refField = ref s.field;
}
}
Podobnie [UnscopedRef] out
zezwala na przypisanie cykliczne, ponieważ parametr ma zarówno bezpiecznego kontekstu, jak i ref-safe-context tylko zwracanych.
Promowanie [UnscopedRef] ref
do kontekstu wywołującego jest przydatne, gdy typ nie jest ref struct
(należy pamiętać, że chcemy zachować reguły proste, aby nie rozróżniały odwołań do ref a nie ref struktury):
int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2
static S F([UnscopedRef] ref int x)
{
S local = new();
local.M(ref x);
return local;
}
ref struct S
{
public ref int RefField;
public void M([UnscopedRef] ref int data)
{
RefField = ref data;
}
}
Jeśli chodzi o zaawansowane adnotacje, projekt [UnscopedRef]
tworzy następujące elementy:
ref struct S { }
// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)
// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
where 'b >= 'a
readonly nie można stosować w pełni na polach ref
Rozważmy poniższy przykładowy kod:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Podczas projektowania reguł dla pól ref
w wystąpieniach readonly
w próżni, reguły mogą być zaprojektowane w taki sposób, że powyższe jest legalne lub nielegalne. Zasadniczo readonly
może być głęboko za pośrednictwem pola ref
lub może mieć zastosowanie tylko do ref
. Zastosowanie tylko do ref
uniemożliwia ponowne przypisanie ref, ale umożliwia normalne przypisanie, które zmienia wartość, do której odwołuje się odwołanie.
Ten projekt nie istnieje jednak w próżni, projektuje zasady dla typów, które mają już skuteczne pola ref
. Najbardziej widoczne z nich, Span<T>
, ma już silną zależność od tego, że readonly
nie jest tutaj zbyt głęboko. Jego podstawowym scenariuszem jest możliwość przypisania do pola ref
za pośrednictwem wystąpienia readonly
.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Oznacza to, że musimy wybrać płytkią interpretację readonly
.
Konstruktory modelowania
Jednym z subtelnych pytań projektowych jest: W jaki sposób ciała konstruktorów są modelowane pod kątem bezpieczeństwa referencji? Zasadniczo jak jest analizowany następujący konstruktor?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Istnieją mniej więcej dwa podejścia:
- Model jako metoda
static
, w którejthis
jest lokalny, w kontekście którego jego bezpieczny kontekst jest kontekstem wywołującym - Model jako metoda
static
, w którejthis
jest parametremout
.
Ponadto konstruktor musi spełniać następujące niezmienne elementy:
- Upewnij się, że parametry
ref
można przechwycić jako polaref
. - Upewnij się, że
ref
do pólthis
nie można uciec za pomocą parametrówref
. To naruszałoby trudne przypisania ref.
Intencją jest wybranie formularza, który spełnia nasze niezmienności bez wprowadzenia żadnych specjalnych reguł dla konstruktorów. Biorąc pod uwagę, że najlepszy model dla konstruktorów traktuje this
jako parametr out
.
zwracać tylko charakter out
pozwala nam spełnić wszystkie zmienne powyżej bez specjalnej wielkości liter:
public static void ctor(out S @this, ref int f)
{
// The ref-safe-context of `ref f` is *return-only* which is also the
// safe-context of `this.field` hence this assignment is allowed
@this.field = ref f;
}
Argumenty metody muszą być zgodne
Reguła zgodności argumentów metody jest typowym źródłem nieporozumień dla deweloperów. Jest to reguła, która ma wiele specjalnych przypadków, które trudno zrozumieć, chyba że znasz rozumowanie reguły. Aby lepiej zrozumieć przyczyny reguły, uprościmy kontekst ref-bezpieczny i bezpieczny kontekst do po prostu kontekstu.
Metody mogą stosunkowo dowolnie zwracać stan przekazany do nich jako parametr. Zasadniczo każdy osiągalny stan, który jest niezakresowy, może zostać zwrócony (w tym zwracany przez ref
). Można to zwrócić bezpośrednio za pomocą instrukcji return
lub pośrednio, przypisując je do wartości ref
.
Zwroty bezpośrednie nie stanowią wielu problemów z bezpieczeństwem ref. Kompilator po prostu musi przyjrzeć się wszystkim danym wejściowym możliwym do zwrócenia do metody, a następnie skutecznie ogranicza zwracaną wartość do minimalnego kontekstu danych wejściowych. Ta wartość zwracana następnie przechodzi przez normalne przetwarzanie.
Zwroty pośrednie stanowią istotny problem, ponieważ wszystkie ref
są zarówno danymi wejściowymi, jak i wyjściowymi metody. Te dane wyjściowe mają już znany kontekst . Kompilator nie może wywnioskować nowych wniosków, musi je wziąć pod uwagę na obecnym poziomie. Oznacza to, że kompilator musi przyjrzeć się każdemu pojedynczemu ref
, który można przypisać w wywoływanej metodzie, ocenić jego kontekst , a następnie zweryfikować, czy żadne zwracalne dane wejściowe do metody nie mają mniejszego kontekstu niż ref
. Jeśli taki przypadek istnieje, wywołanie metody musi być niedozwolone, ponieważ może naruszać bezpieczeństwo ref
.
Argumenty metody muszą być zgodne; jest to proces, dzięki któremu kompilator sprawdza tę kontrolę bezpieczeństwa.
Innym sposobem oceny tego, co jest często łatwiejsze do rozważenia przez deweloperów, jest wykonanie następującego ćwiczenia:
- Przyjrzyj się definicji metody, zidentyfikuj wszystkie miejsca, gdzie można pośrednio zwrócić stan: a. Modyfikowalne parametry
ref
wskazujące naref struct
b. Modyfikowalne parametryref
z przypisywalnymi do ref polamiref
. Parametry przypisywalneref
lub polaref
wskazujące naref struct
(rozważ rekursję) - Przyjrzyj się lokacji wywołania. Zidentyfikuj konteksty, które są zgodne z lokalizacjami określonymi powyżej b. Zidentyfikuj konteksty wszystkich danych wejściowych metody, które są zwracane (nie są zgodne z parametrami
scoped
)
Jeśli jakakolwiek wartość w 2.b jest mniejsza niż 2.a, wywołanie metody musi być niedozwolone. Przyjrzyjmy się kilku przykładom, aby zilustrować reguły:
ref struct R { }
class Program
{
static void F0(ref R a, scoped ref R b) => throw null;
static void F1(ref R x, scoped R y)
{
F0(ref x, ref y);
}
}
Patrząc na wywołanie do F0
, przejdźmy przez punkty (1) i (2). Parametry z możliwością zwrotu pośredniego są a
i b
, ponieważ oba te parametry można przypisać bezpośrednio. Argumenty, które są zgodne z tymi parametrami:
-
a
, który mapuje nax
, posiadający kontekst z kontekstu wywołującego -
b
mapowania nay
z kontekstem składowych funkcji
Zestaw zwracanych danych wejściowych do metody to
-
x
z zakresu ucieczki kontekstu wywołującego -
ref x
z zakresu ucieczki kontekstu wywołującego -
y
z zakresu ucieczki składowej funkcji
Wartość ref y
nie jest zwracana, ponieważ jest przyporządkowana do scoped ref
, dlatego nie jest traktowana jako dane wejściowe. Jednak biorąc pod uwagę, że istnieje co najmniej jedno dane wejściowe z mniejszym zakresu ucieczki ( argumenty
) niż jeden z danych wyjściowych ( argumentx
), wywołanie metody jest niedozwolone.
Inna odmiana jest następująca:
ref struct R { }
class Program
{
static void F0(ref R a, ref int b) => throw null;
static void F1(ref R x)
{
int y = 42;
F0(ref x, ref y);
}
}
Parametry z możliwością zwrotu pośredniego to ponownie a
i b
, ponieważ oba można przypisać bezpośrednio. Ale b
można wykluczyć, ponieważ nie wskazuje ref struct
dlatego nie może być używany do przechowywania stanu ref
. W ten sposób mamy:
-
a
, który mapuje nax
, posiadający kontekst z kontekstu wywołującego
Zestaw zwracanych danych wejściowych do metody to:
-
x
z kontekstemkontekstu wywołującego -
ref x
z kontekstemkontekstu wywołującego -
ref y
z kontekstem elementu funkcji
Biorąc pod uwagę, że istnieje co najmniej jedno dane wejściowe z mniejszym zakresu ucieczki ( argumentref y
) niż jeden z danych wyjściowych ( argumentx
), wywołanie metody jest niedozwolone.
Logika ta ma obejmować zasadę, według której argumenty metody muszą być zgodne z regułą. To idzie dalej, ponieważ traktuje scoped
jako sposób na usunięcie danych wejściowych z rozważań, a readonly
jako sposób na usunięcie ref
jako wyjścia (nie można przypisać do readonly ref
, więc nie może być źródłem wyjścia). Te specjalne przypadki powodują dodanie złożoności do reguł, ale robi się to z korzyścią dla dewelopera. Kompilator dąży do usunięcia wszystkich danych wejściowych i wyjściowych, o których wie, że nie przyczynią się do wyniku, aby zapewnić deweloperom maksymalną elastyczność podczas wywoływania członka. Podobnie jak rozwiązanie przeciążenia, warto zwiększyć złożoność naszych reguł, tworząc większą elastyczność dla konsumentów.
Przykłady wnioskowanych bezpiecznego kontekstu wyrażeń deklaracji
Związane z Określanie kontekstu bezpiecznego wyrażeń deklaracji.
ref struct RS
{
public RS(ref int x) { } // assumed to be able to capture 'x'
static void M0(RS input, out RS output) => output = input;
static void M1()
{
var i = 0;
var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M2(RS rs1)
{
M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
}
static void M3(RS rs1)
{
M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
}
}
Należy pamiętać, że kontekst lokalny, który wynika z modyfikatora scoped
, jest najwęższy, który może być używany dla zmiennej — każdy węższy oznacza, że wyrażenie odnosi się do zmiennych, które są deklarowane tylko w węższym kontekście niż wyrażenie.
C# feature specifications