Vylepšení datových struktur nízké úrovně
Poznámka
Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.
Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách z příslušné schůzky návrhu jazyka (LDM) .
Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .
Shrnutí
Tento návrh je agregace několika návrhů pro zlepšení výkonu struct
: ref
polí a možnost přepsat výchozí nastavení životnosti. Cílem je návrh, který bere v úvahu různé návrhy k vytvoření jedné nadlimitní sady funkcí pro vylepšení nízké úrovně struct
.
Poznámka: Předchozí verze této specifikace používaly termíny „ref-safe-to-escape“ a „safe-to-escape“, které byly zavedeny ve specifikaci funkce Span safety. Standardní výbor ECMA změnil názvy na "ref-safe-context" a "safe-context". Hodnoty bezpečného kontextu byly upřesněny tak, aby konzistentně používaly "declaration-block", "function-member" a "caller-context". Speclety používaly pro tyto termíny různé formulace a také používaly "bezpečný návrat" jako synonymum pro "kontext volajícího". Tato specifikace byla aktualizována tak, aby používala termíny ve standardu C# 7.3.
Ne všechny funkce uvedené v tomto dokumentu byly implementovány v jazyce C# 11. C# 11 zahrnuje:
-
ref
polí ascoped
[UnscopedRef]
Tyto funkce zůstávají otevřené návrhy pro budoucí verzi jazyka C#:
-
ref
polí proref struct
- Typy s omezenými západy slunce
Motivace
Dřívější verze jazyka C# přidaly do jazyka řadu funkcí nízké úrovně výkonu: ref
vrací, ref struct
, ukazatele funkcí atd. ... Tito vývojáři .NET umožnili psát vysoce výkonný kód a současně nadále využívat pravidla jazyka C# pro zabezpečení typů a paměti. Umožnil také vytváření základních typů výkonu v knihovnách .NET, jako je Span<T>
.
Vzhledem k tomu, že tyto funkce získaly oblibu mezi vývojáři v ekosystému .NET, jak interními, tak externími, poskytovali nám informace o zbývajících třecích bodech v ekosystému. Místa, kde stále potřebují vypustit kód unsafe
, aby se jejich práce dokončila, nebo vyžadují modul runtime pro speciální typy případů, jako je Span<T>
.
Dnes Span<T>
se dosahuje pomocí typu internal
ByReference<T>
, se kterým modul runtime efektivně pracuje jako s polem ref
. To poskytuje výhodu ref
polí, ale nevýhodou je, že jazyk neposkytuje pro toto žádné bezpečnostní ověření, zatímco pro jiné využití ref
ano. Tento typ lze dále použít pouze dotnet/runtime, protože je internal
, takže třetí strany nemohou navrhnout své vlastní primitivy na základě ref
polí. Součástí motivace k této práci je odebrat ByReference<T>
a použít správná pole ref
ve všech základech kódu.
Tento návrh má v úmyslu řešit tyto problémy prostřednictvím našich stávajících základních funkcí. Konkrétně má za cíl:
- Povolit
ref struct
typům deklarovat poleref
. - Povolit modulu runtime plně definovat
Span<T>
pomocí systému typů C# a odebrat speciální typ případu, jako jeByReference<T>
- Povolte
struct
typům vrátitref
na svá pole. - Povolit modulu runtime odebrat
unsafe
použití způsobenými omezeními výchozích hodnot životnosti - Povolit deklaraci bezpečných vyrovnávacích pamětí
fixed
pro spravované a nespravované typy vstruct
Podrobný návrh
Pravidla pro bezpečnost ref struct
jsou definována v bezpečnostním dokumentu rozsahu s použitím předchozích termínů. Tato pravidla byla začleněna do normy C# 7 v §9.7.2 a §16.4.12. Tento dokument popisuje požadované změny tohoto dokumentu v důsledku tohoto návrhu. Po přijetí jako schválené funkce se tyto změny začlení do tohoto dokumentu.
Po dokončení tohoto návrhu bude definice Span<T>
následující:
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;
}
}
Zadejte pole ref a vymezený obor
Jazyk umožní vývojářům deklarovat pole ref
uvnitř ref struct
. To může být užitečné například při zapouzdření velkých proměnlivých struct
instancí nebo definování vysoce výkonných typů, jako je Span<T>
v knihovnách kromě modulu runtime.
ref struct S
{
public ref int Value;
}
Pole ref
se vygeneruje do metadat pomocí podpisu ELEMENT_TYPE_BYREF
. To se nijak neliší od způsobu generování ref
místních hodnot nebo argumentů ref
. Například ref int _field
bude emitováno jako ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. To bude vyžadovat, abychom aktualizovali ECMA335, aby tuto položku povolili, ale mělo by to být poměrně jednoduché.
Vývojáři mohou pokračovat v inicializaci ref struct
s polem ref
pomocí výrazu default
v takovém případě budou mít všechna deklarovaná pole ref
hodnotu null
. Při každém pokusu o použití těchto polí dojde k vyvolání NullReferenceException
.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Zatímco jazyk C# předstíří, že ref
nemůže být null
to je legální na úrovni modulu runtime a má dobře definovanou sémantiku. Vývojáři, kteří do svých typů zavádějí pole ref
, si musí být vědomi této možnosti a měli by být důrazně zabránit úniku těchto podrobností do využívání kódu. Místo toho by ref
pole měla být ověřena jako nenulová pomocí pomocných rutin modulu runtime a vyvolání v případě nesprávného použití neinicializovaného 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
lze kombinovat s modifikátory readonly
následujícími způsoby:
-
readonly ref
: toto je pole, které nelze znovu přiřadit mimo konstruktor neboinit
metody. Hodnota může být přiřazena, i když mimo tyto kontexty. -
ref readonly
: jedná se o pole, které může být znovu přiřazeno, ale nemůže být přiřazena hodnota v žádném okamžiku. Tímto způsobem může být parametrin
znovu přiřazen k poliref
. -
readonly ref readonly
: kombinaceref readonly
areadonly 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
bude vyžadovat, aby byla ref
pole deklarována jako readonly ref
. Není nutné, aby byly deklarovány readonly ref readonly
. To umožňuje, aby readonly struct
měly nepřímé mutace prostřednictvím takového pole, ale to se nijak neliší od pole readonly
, které dnes odkazuje na referenční typ (více podrobností)
Do metadat se vygeneruje readonly ref
pomocí příznaku initonly
, který odpovídá jakémukoli jinému poli. Pole ref readonly
bude přiřazeno s System.Runtime.CompilerServices.IsReadOnlyAttribute
.
readonly ref readonly
se vygeneruje s oběma položkami.
Tato funkce vyžaduje podporu modulu runtime a změny specifikace ECMA. Tyto funkce budou povoleny pouze v případě, že je v Corelibu nastaven odpovídající příznak dané funkce. Tento problém, který sleduje přesné rozhraní API, je sledován zde https://github.com/dotnet/runtime/issues/64165.
Sada změn pravidel bezpečného kontextu nezbytná pro povolení ref
polí je malá a cílová. Pravidla již zahrnují ref
pole, která existují a využívají se přes rozhraní API. Změny se musí zaměřit jen na dva aspekty: jak vznikají a jak jsou znovu přiřazovány.
Nejprve je potřeba aktualizovat pravidla pro
Následujícím způsobem: výraz v podobě
ref e.F
ref-safe-context.
- Pokud je
F
polemref
, pak jeho ref-safe-context je bezpečný kontexte
.- Jinak pokud je
e
typu odkazu, má ref-bezpečný-kontext z kontextu volajícího- Jinak se jeho ref-safe-context bere z ref-safe-context u
e
.
To nepředstavuje změnu pravidla, i když pravidla vždy zohlednila, že ref
stav existuje uvnitř ref struct
. Takto ve skutečnosti vždy fungoval stav ref
v Span<T>
a pravidla spotřeby to správně zohledňují. Tato změna pouze umožňuje vývojářům mít přímý přístup k polím ref
a zajistit, že dodržují stávající pravidla implicitně platící pro Span<T>
.
To znamená, že pole ref
se dají vrátit jako ref
z ref struct
, ale normální pole nemohou.
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;
}
Může to na první pohled vypadat jako chyba, ale jedná se o záměrný návrhový bod. Ačkoli se tímto návrhem nevytváří nové pravidlo, uznávají stávající pravidla Span<T>
, aby vývojáři mohli deklarovat svůj vlastní stav ref
.
Dále je potřeba upravit pravidla pro znovupřiřazení odkazů s ohledem na přítomnost polí ref
. Primárním scénářem pro opětovné přiřazení ref je uložení parametrů ref struct
do polí ref
pomocí ref
konstruktorů. Podpora bude obecnější, ale toto je základní scénář. Aby to bylo podpořeno, budou pravidla pro přerozdělení referencí upravena tak, aby zohledňovala pole ref
, následovně:
Pravidla opětovného přiřazení odkazu
Levý operand operátoru = ref
musí být výraz, který je vázán na místní proměnnou ref, parametr ref (jiný než this
), výstupní parametr, nebo ref pole.
Pro opětovné přiřazení odkazu ve formuláři
e1 = ref e2
musí být splněny obě následující podmínky:
e2
musí mít ref-safe-context alespoň tak velký jako ref-safe-contexte1
e1
musí mít stejný bezpečný kontext jakoe2
Poznámka
To znamená, že požadovaný konstruktor Span<T>
funguje bez další poznámky:
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;
}
}
Změna pravidel opětovného přiřazení odkazu znamená, že parametry ref
teď můžou utéct z metody jako pole ref
v hodnotě ref struct
. Jak je popsáno v části , aspekty kompatibility mohou změnit pravidla pro existující rozhraní API, které nikdy nezamýšlely, aby parametry ref
unikly jako pole ref
. Pravidla životnosti parametrů jsou založena výhradně na jejich deklaraci, nikoli na jejich použití. Všechny parametry ref
parametry, které mohou být escapující nebo neeskapující, a tím obnovit sémantiku míst volání v C# 10, bude jazyk zavádět anotace s omezenou životností.
modifikátor scoped
Klíčové slovo scoped
se použije k omezení životnosti hodnoty. Lze jej použít na ref
nebo hodnotu, která je ref struct
a má vliv na omezení životnosti kontextu ref-safe-context nebo safe-context na člena funkce . Například:
Parametr nebo lokální | ref-safe-context | bezpečný kontext |
---|---|---|
Span<int> s |
člen funkce | kontextu volajícího |
scoped Span<int> s |
člen funkce | člen funkce |
ref Span<int> s |
kontextu volajícího | kontextu volajícího |
scoped ref Span<int> s |
člen funkce | kontextu volajícího |
V této relaci ref-safe-kontext hodnoty nemůže být nikdy širší než bezpečného kontextu.
To umožňuje, aby rozhraní API v jazyce C# 11 byla opatřena poznámkami tak, aby měla stejná pravidla jako 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]);
}
Anotace scoped
také znamená, že parametr this
u struct
lze nyní definovat jako scoped ref T
. Dříve musela být v pravidlech aplikována speciální úprava pro parametr ref
, který měl odlišná pravidla ref-safe-context než ostatní parametry ref
(viz všechny odkazy na zahrnutí nebo vyloučení příjemce v pravidlech bezpečného kontextu). Nyní se dá vyjádřit jako obecný koncept v rámci pravidel, která je dále zjednodušuje.
Poznámka scoped
může být také použita na následující místa:
- locals: Tato poznámka nastaví životnost bezpečného kontextunebo ref-safe-context v případě
ref
místního člena funkce bez ohledu na dobu životnosti inicializátoru.
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];
}
Další použití pro scoped
v místních prostředích jsou popsány níže.
scoped
poznámku nelze použít na žádné jiné místo, včetně návratových hodnot, polí, prvků pole atd. Kromě toho má scoped
vliv při použití na jakýkoli ref
, in
nebo out
, ale pouze tehdy, pokud je použit na hodnoty, které jsou ref struct
. Deklarace jako scoped int
nemají žádný vliv, protože návrat ne-ref struct
je vždy bezpečný. Kompilátor vytvoří diagnostiku pro takové případy, aby se zabránilo nejasnostem vývojářů.
Změna chování parametrů out
Chcete-li dále omezit dopad změny kompatibility vracející parametry ref
a in
jako pole ref
, jazyk změní výchozí hodnotu ref-safe-context pro parametry out
na funkčně-členskou hodnotu. Parametry out
se implicitně scoped out
do budoucna. Jakmile se podíváme z pohledu kompatibility, to znamená, že je nelze vrátit pomocí ref
:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Tím se zvýší flexibilita rozhraní API, která vracejí ref struct
hodnoty a mají parametry out
, protože už nemusí brát v úvahu, že se parametr zachytává odkazem. To je důležité, protože se jedná o běžný vzor v rozhraních API pro styl čtečky:
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);
}
Jazyk už nebude považovat argumenty předané parametru out
za vrátitelné. Zpracování vstupu na parametr out
jako návratové bylo pro vývojáře velmi matoucí. V podstatě odvrací záměr out
tím, že vývojářům vynutí vzít v úvahu hodnotu předanou volajícím, která se nikdy nepoužívá s výjimkou jazyků, které nerespektují out
. V budoucnu musí jazyky, které podporují ref struct
zajistit, aby původní hodnota předaná parametru out
nebyla nikdy přečtená.
Jazyk C# toho dosahuje prostřednictvím určitých pravidel přiřazení. Tím se nejen dosáhne dodržování našich pravidel pro bezpečný kontext referencí, ale také se umožní stávajícímu kódu přiřadit a následně vrátit hodnoty parametrů out
.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Tyto změny společně znamenají, že argument parametru out
nepřispívá k hodnotám kontextově bezpečným nebo kontextově bezpečným pro reference při vyvolání metody. To výrazně snižuje celkový dopad na kompatibilitu polí ref
a také zjednodušuje, jak vývojáři uvažují o out
. Argument out
parametru nepřispívá k návratové hodnotě, jedná se o pouhý výstup.
Odvození bezpečného kontextu deklarativních výrazů
- kontext volajícího
- Pokud je proměnná označena
scoped
, pak je deklarativního bloku (tj. jako člen funkce nebo něco specifičtějšího). - pokud je typ proměnné
ref struct
, zvažte všechny argumenty obsahující vyvolání, včetně příjemce:-
bezpečného kontextu libovolného argumentu, kde odpovídající parametr není
out
a má bezpečný kontextnávratových nebo širších - ref-safe-context jakéhokoli argumentu, kde má odpovídající parametr ref-safe-contextreturn-only nebo širší
-
bezpečného kontextu libovolného argumentu, kde odpovídající parametr není
Viz také Příklady odvozených bezpečného kontextu výrazů deklarací.
Implicitně scoped
parametry
Celkově existují dvě ref
místa, která jsou implicitně určena jako scoped
:
- U metody instance
this
nastruct
- parametry
out
Pravidla bezpečného kontextu ref budou vyjádřena prostřednictvím scoped ref
a ref
. Pro účely bezpečného kontextu ref je parametr in
ekvivalentní ref
a out
je ekvivalentní scoped ref
.
in
i out
budou výslovně zmíněna pouze v případech, kdy je to důležité pro sémantiku pravidla. Jinak se považují pouze za ref
a scoped ref
.
Při diskusi o ref-safe-kontextu argumentů, které odpovídají parametrům in
, budou ve specifikaci generalizovány jako ref
argumenty. Pokud je argument lvalue, pak ref-safe-kontext odpovídá lvalue, jinak je to člen funkce. Znovu in
bude uvedena pouze tehdy, když je důležité pro význam aktuálního pravidla.
Bezpečný kontext pouze pro vrácení
Návrh také vyžaduje, aby bylo zavedeno nové bezpečné prostředí: pouze pro návrat. To se podobá kontextu volajícího v tom, že se dá vrátit, ale ji lze pouze vrátit prostřednictvím příkazu return
.
Podrobnosti pro pouze pro návrat je, že je to kontext, který je větší než funkční člen , ale menší než kontext volajícího . Výraz zadaný pro příkaz return
musí být alespoň návratový. Jako taková většina stávajících pravidel padá. Například přiřazení k parametru
Jsou tři umístění, která jsou ve výchozím nastavení určena pouze pro vracení :
- Parametr
nebo bude mít návratového sbezpečným kontextem. To se provádí částečně pro ref struct
, aby se zabránilo hloupým cyklickým problémům s přiřazením. Provádí se to jednotně, aby se zjednodušil model i minimalizovaly změny kompatibility. - Parametr
out
proref struct
bude mít bezpečné kontextovénávratu . To umožňuje, aby návrat aout
byly stejně výrazné. To nemá problém s hloupým cyklickým přiřazením, protožeout
je implicitněscoped
, takže kontext bezpečného odkazu je stále menší než bezpečný kontext. - Parametr
konstruktoru bude mít návratového bezpečného kontextu . Dochází k tomu kvůli modelování jako parametrů out
.
Jakýkoli výraz nebo příkaz, který explicitně vrací hodnotu z metody nebo lambda, musí mít bezpečný kontexta pokud je k dispozici, alespoň ref-bezpečný kontext, pouze pro návrat. To zahrnuje return
příkazy, výrazy bodované členy a výrazy lambda.
Stejně tak každé přiřazení k out
musí mít bezpečný kontext alespoň pouze pro návrat. Nejedná se ale o zvláštní případ, který následuje pouze z existujících pravidel přiřazení.
Poznámka: Výraz, jehož typ není typem ref struct
, má vždy bezpečný kontext volajícího pro kontext .
Pravidla pro vyvolání metody
Pravidla kontextu bezpečného odkazu pro vyvolání metody se aktualizují několika způsoby. Prvním je rozpoznání dopadu, který scoped
má na argumenty. Pro daný argument expr
, který se předá parametru p
:
- Pokud je
p
scoped ref
,expr
nepřispívá kontextu ref-safe-context při zvažování argumentů.- Pokud je
p
scoped
,expr
nepřispívá bezpečnému kontextu při zvažování argumentů.- Pokud je
p
out
,expr
nepřispívá k referenčnímu bezpečnému kontextu nebo bezpečnému kontextuvíce podrobností
Jazyk "nepřispívá" znamená, že argumenty se prostě neberou v úvahu při výpočtu hodnoty vrácení metody, konkrétně ref-bezpečného kontextu, nebo bezpečného kontextu. Je to proto, že hodnoty nemohou přispět k této životnosti, protože poznámka scoped
tomu brání.
Pravidla volání metody se teď dají zjednodušit. Příjemce už nemusí být považován za zvláštní případ, v případě struct
je teď jednoduše scoped ref T
. Pravidla hodnot se musí změnit, aby zohlednila návraty pole ref
:
Hodnota vyplývající z vyvolání metody
e1.M(e2, ...)
, kdeM()
nevrací strukturu ref-to-ref, má bezpečný kontext převzatý z nejužší z následujících možností:
- Kontext volajícího
- Pokud je návratová hodnota
ref struct
a bezpečný kontext, na který přispěly všechny výrazy argumentů.- Pokud je návrat
, pak ref-safe-context přispěný všemi argumenty . Pokud
M()
vrátí ref-to-ref-strukturu, bezpečný kontext je stejný jako bezpečný kontext všech argumentů, které jsou ref-to-ref-struktura. Jedná se o chybu, pokud existuje více argumentů s různým bezpečným kontextem , protože argumenty metody musí být stejné jako.
Pravidla volání ref
se dají zjednodušit na:
Hodnota vyplývající z vyvolání metody
ref e1.M(e2, ...)
, kdeM()
nevrací strukturu ref-to-ref,, je ref-safe-context nejužší z následujících kontextů:
- Kontext volajícího
- Bezpečný kontext, k němuž přispěly všechny výrazy argumentů.
- ref-safe-context byla přispívána všemi argumenty
ref
Pokud
M()
vrátí ref-to-ref-struct, ref-safe-context je nejužší ref-safe-context přispívající všemi argumenty, které jsou ref-to-ref-struct.
Toto pravidlo teď umožňuje definovat dvě varianty požadovaných 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);
}
Pravidla pro inicializátory objektů
bezpečný kontext výrazu inicializátoru objektu je nejužší z:
- bezpečný kontext volání konstruktoru.
- bezpečného kontextu a argumentů indexerům inicializátoru členů, které mohou utéct do příjemce.
- bezpečné kontextové RHS přiřazení v inicializátorech členů na nečtené settery nebo ref-safe-context v případě přiřazení odkazu.
Dalším způsobem modelování je považovat jakýkoli argument pro inicializaci člena, který lze přiřadit příjemci, za argument konstruktoru. Důvodem je, že inicializátor členu je efektivní volání konstruktoru.
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);
Toto modelování je důležité, protože ukazuje, že naše MAMM musí zvlášť zohlednit inicializátory členů. Vezměte v úvahu, že tento konkrétní případ musí být neplatný, protože umožňuje, aby byla hodnota s užším bezpečným kontextem přiřazena k vyšší.
Argumenty metody se musí shodovat.
Přítomnost polí ref
znamená, že je nutné aktualizovat pravidla týkající se argumentů metody, protože parametr ref
nyní může být uložen jako pole v argumentu metody ref struct
. Dříve pravidlo muselo zvážit pouze další ref struct
, které se ukládá jako pole. Tento dopad je řešen v ohledech na kompatibilitu. Nové pravidlo je ...
Pro jakoukoli metodu vyvolání
e.M(a1, a2, ... aN)
- Výpočet nejužšího bezpečného kontextu z:
- kontextu volajícího
- bezpečný kontext všech argumentů
- kontextu ref-safe-context všech argumentů ref, jejichž odpovídající parametry mají kontextu kontextu volajícího
- Všechny argumenty
ref
typůref struct
musí být přiřaditelné pomocí hodnoty s tím bezpečným kontextem. Toto je případ, kdyref
nemůže zobecnit zahrnutíin
aout
.
Pro jakoukoli metodu vyvolání
e.M(a1, a2, ... aN)
- Výpočet nejužšího bezpečného kontextu z:
- kontextu volajícího
- bezpečný kontext všech argumentů
- Kontext ref-safe všech argumentů ref, jejichž odpovídající parametry nejsou
scoped
- Všechny argumenty
out
typůref struct
musí být přiřaditelné pomocí hodnoty s tím bezpečným kontextem.
Přítomnost scoped
umožňuje vývojářům snížit tření tohoto pravidla tím, že označí parametry, které nejsou vráceny jako scoped
. Tím se odstraní argumenty z položky (1) v obou výše uvedených případech a volajícím zajistí větší flexibilitu.
Dopad této změny je podrobněji popsán níže. Celkově to umožní vývojářům učinit volací místa flexibilnější tím, že budou označovat neunikající hodnoty podobné referencím pomocí scoped
.
Rozptyl rozsahu parametrů
Modifikátor scoped
a atribut [UnscopedRef]
(viz níže) mají vliv také na přepisování našich objektů, implementaci rozhraní a pravidla pro převod delegate
. Podpis pro přepsání, implementaci rozhraní nebo konverzi delegate
může:
- Přidání
scoped
do parametruref
neboin
- Přidání
scoped
do parametruref struct
- Odstraňte
[UnscopedRef]
z parametruout
- Odebrání
[UnscopedRef]
z parametruref
typuref struct
Jakýkoli jiný rozdíl s ohledem na scoped
nebo [UnscopedRef]
se považuje za neshodu.
Kompilátor oznámí diagnostiku nebezpečných neshod s rozsahem v případě přepsání, implementací rozhraní a delegování převodů v následujících případech:
- Metoda vrátí
ref struct
,ref
neboref readonly
, nebo má parametrref
čiout
typuref struct
. - Metoda má alespoň jeden další
ref
,in
neboout
parametr nebo parametr typuref struct
.
Výše uvedená pravidla ignorují parametry this
, protože ref struct
metody instance nelze použít pro přepsání, implementace rozhraní nebo delegování převodů.
Diagnostika je hlášena jako chyba, pokud oba neodpovídající podpisy používají pravidla bezpečného kontextu ref C#11; jinak je diagnostika varování.
Upozornění na neshodu s vymezeným oborem může být hlášeno v modulu kompilovaném pomocí pravidel bezpečného kontextu C#7.2, kde scoped
není k dispozici. V některých takových případech může být nutné potlačit upozornění, pokud se jiný neshodovaný podpis nedá změnit.
Modifikátor scoped
a atribut [UnscopedRef]
mají také následující účinky na podpisy metody:
- Modifikátor
scoped
a atribut[UnscopedRef]
nemají vliv na skrytí - Přetížení se nemohou lišit pouze u
scoped
nebo[UnscopedRef]
Část o poli ref
a scoped
je dlouhá, takže ji chci uzavřít stručným souhrnem navrhovaných nekompatibilních změn:
- Hodnota, která má
kontextu ref-safe-context na kontext volajícího, je možné vrátit nebo pole. - Parametr
out
by měl bezpečný kontext člena funkce .
Podrobné poznámky:
- Pole
ref
lze deklarovat pouze uvnitřref struct
- Pole
ref
nelze deklarovatstatic
,volatile
aniconst
- Pole
ref
nemůže mít typ, který jeref struct
- Proces generování referenčního sestavení musí zachovat přítomnost pole
ref
uvnitřref struct
-
readonly ref struct
musí deklarovatref
pole jakoreadonly ref
- U referenčních hodnot musí být modifikátor
scoped
uveden předin
,out
neboref
- Dokument bezpečnostních pravidel bude aktualizován, jak je uvedeno v tomto dokumentu.
- Nová pravidla bezpečného kontextu 'ref' budou platit, pokud
- Základní knihovna obsahuje příznak funkce označující podporu polí
ref
. - Hodnota
langversion
je 11 nebo vyšší.
- Základní knihovna obsahuje příznak funkce označující podporu polí
Syntax
13.6.2 Deklarace místních proměnných: přidáno 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 Prohlášení for
: přidáno 'scoped'?
nepřímo z local_variable_declaration
.
13.9.5 Příkaz foreach
: přidán 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Seznamy argumentů: přidáno 'scoped'?
pro deklaraci proměnné out
.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
[TBD]
Parametry metody 15.6.2: přidán '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 Deklarace delegátů: přidány 'scoped'?
nepřímo z fixed_parameter
.
12.19 Anonymní funkční výrazy: přidáno 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Typy s omezenými západy slunce
Kompilátor má koncept sady "omezených typů", která je z velké části nezdokumentována. Těmto typům byl udělen zvláštní stav, protože v jazyce C# 1.0 neexistuje způsob vyjádření jejich chování pro obecné účely. Zejména skutečnost, že typy mohou obsahovat odkazy na zásobník spouštění. Místo toho měl kompilátor speciální znalosti o nich a omezil jejich použití na způsoby, které by vždy byly bezpečné: nepovolené návraty, nelze použít jako prvky pole, nelze použít v obecných typech atd . .
Jakmile jsou pole ref
dostupná a rozšířená tak, aby podporovala ref struct
tyto typy lze v jazyce C# správně definovat pomocí kombinace ref struct
a ref
polí. Proto když kompilátor zjistí, že modul runtime podporuje ref
polí, už nebude mít představu o omezených typech. Místo toho použije typy, které jsou definovány v kódu.
Abychom toto podpořili, naše pravidla bezpečného kontextu odkazů budou aktualizována následujícím způsobem:
-
__makeref
se bude považovat za metodu s podpisemstatic TypedReference __makeref<T>(ref T value)
-
__refvalue
bude považován za metodu se signaturoustatic ref T __refvalue<T>(TypedReference tr)
. Výraz__refvalue(tr, int)
efektivně použije druhý argument jako parametr typu. jako parametr bude mít ref-safe-context a bezpečný kontext členů funkce . -
__arglist(...)
jako výraz bude mít referenčně bezpečný kontext a bezpečný kontext člena funkce .
Odpovídající moduly runtime zajistí, aby TypedReference
, RuntimeArgumentHandle
a ArgIterator
byly definovány jako ref struct
. Další TypedReference
se musí zobrazit jako pole ref
do ref struct
pro libovolný možný typ (může ukládat libovolnou hodnotu). To v kombinaci s výše uvedenými pravidly zajistí, že odkazy na zásobník neuniknou mimo dobu své platnosti.
Poznámka: Přísně řečeno je to podrobnosti implementace kompilátoru vs. část jazyka. Nicméně vzhledem k vztahu s poli ref
je to zahrnuto do návrhu jazyků pro zjednodušení.
Poskytněte bez rozsahu
Jedním z nejvýraznějších třecí bodů je nemožnost vrátit pole ref
v případě členů struct
. To znamená, že vývojáři nemohou vytvářet ref
metody vracející hodnoty nebo atributy a vlastnosti a musí se uchylovat k přímému odhalení polí. To snižuje užitečnost vratek ref
v struct
, kde je často nejžádanější.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
odůvodnění pro tuto výchozí hodnotu je rozumné, ale s struct
útěkem this
pomocí reference není nic inherentně špatného; je to jednoduše výchozí hodnota zvolená pravidly bezpečného kontextu ref.
K vyřešení tohoto problému jazyk poskytne opak poznámky o životnosti scoped
podporou UnscopedRefAttribute
. To se dá použít u libovolného ref
a změní ref-safe-context na jednu úroveň širší než výchozí. Například:
Neomezený odkaz použít na | Původní bezpečný kontext ref | Nový kontext ref-safe-context |
---|---|---|
člen instance | funkce-člen | pouze pro návrat |
parametr in / ref |
pouze pro návrat | kontext volajícího |
parametr out |
funkce-člen | pouze pro návrat |
Při použití [UnscopedRef]
na metodu instance struct
má vliv na úpravu implicitního this
parametru. To znamená, že this
působí jako ref
stejného typu bez označení.
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;
}
Poznámku lze také umístit na parametry out
a obnovit je do chování jazyka C# 10.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Pro účely pravidel bezpečného kontextu ref se taková [UnscopedRef] out
považuje za ref
. Podobá se tomu, jak se in
považuje za ref
pro účely hodnocení životnosti.
[UnscopedRef]
poznámka bude zakázána pro init
členy a konstruktory uvnitř struct
. Tito členové jsou již speciální s ohledem na ref
sémantiku, protože členy readonly
považují za proměnlivé. To znamená, že se ref
těmto členům zobrazí jako jednoduchý ref
nikoli ref readonly
. To je povoleno v mezích konstruktorů a init
. Povolení [UnscopedRef]
by takové ref
umožnilo nesprávně utéct mimo konstruktor a povolit mutaci po provedení readonly
sémantiky.
Typ atributu bude mít následující definici:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Podrobné poznámky:
- Metoda instance nebo vlastnost opatřená poznámkami
má kontextu ref-safe-context nastaven na kontext volajícího . - Člen anotovaný
[UnscopedRef]
nemůže implementovat rozhraní. - Je chyba používat
[UnscopedRef]
na- Člen, který není deklarován v
struct
- Člen
static
neboinit
, nebo konstruktor vstruct
- Parametr označený
scoped
- Parametr předaný hodnotou
- Parametr předaný odkazem, který není implicitně vymezen
- Člen, který není deklarován v
ScopedRefAttribute
Poznámky scoped
budou zahrnuty do metadat prostřednictvím atributu typu System.Runtime.CompilerServices.ScopedRefAttribute
. Atribut se bude shodovat s názvem kvalifikovaným názvovým prostorem, takže definice nemusí být uvedena v žádném konkrétním sestavení.
Typ ScopedRefAttribute
je určen pouze pro použití kompilátoru – ve zdroji není povolený. Deklarace typu je syntetizována kompilátorem, pokud ještě není součástí kompilace.
Typ bude mít následující definici:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Kompilátor vygeneruje tento atribut u parametru se syntaxí scoped
. Tato funkce se vygeneruje pouze v případě, že syntaxe způsobí, že se hodnota liší od výchozího stavu. Například scoped out
způsobí, že se nevygeneruje žádný atribut.
RefSafetyRulesAttribute
Existuje několik rozdílů v bezpečném kontextu pravidla mezi C#7.2 a C#11. Některé z těchto rozdílů můžou vést k zásadním změnám při rekompilování pomocí C#11 proti odkazům zkompilovanými v jazyce C#10 nebo starším.
- neskopované parametry
ref
/in
/out
mohou uniknout z volání metody jako položkaref
objekturef struct
v C#11, ne v C#7.2. -
out
parametry mají implicitně vymezenou platnost v jazyce C#11 a v jazyce C#7.2 jsou bez vymezené platnosti. -
ref
/in
parametry pro typyref struct
jsou implicitně vymezené v jazyce C#11 a neomezené v jazyce C#7.2.
Abychom snížili pravděpodobnost zásadních změn při rekompilování pomocí C#11, aktualizujeme kompilátor C#11 tak, aby používal pravidla ref bezpečného kontextu pro vyvolání metody, která odpovídají pravidlům použitým k analýze deklarace metody. V podstatě, když se analyzuje volání metody zkompilované pomocí staršího kompilátoru, kompilátor C#11 použije pravidla ref bezpečného kontextu jazyka C#7.2.
Aby to bylo možné, kompilátor vygeneruje nový atribut [module: RefSafetyRules(11)]
, když je modul zkompilován s -langversion:11
nebo vyšší nebo zkompilován pomocí corlibu obsahujícího příznak funkce pro pole ref
.
Argument atributu označuje jazyková verze ref bezpečný kontext pravidla použitá při kompilaci modulu.
Verze je aktuálně opravena v 11
bez ohledu na skutečnou jazykovou verzi předanou kompilátoru.
Očekávání spočívá v tom, že budoucí verze kompilátoru aktualizují pravidla kontextu bezpečného odkazu a vygenerují atributy s odlišnými verzemi.
Pokud kompilátor načte modul, který obsahuje [module: RefSafetyRules(version)]
s jinou version
než 11
, kompilátor oznámí upozornění pro nerozpoznanou verzi, pokud existují nějaká volání metod deklarovaných v daném modulu.
Když kompilátor C#11 analyzuje volání metody:
- Pokud modul obsahující deklaraci metody obsahuje
[module: RefSafetyRules(version)]
, bez ohledu naversion
, volání metody se analyzuje pomocí pravidel C#11. - Pokud je modul obsahující deklaraci metody ze zdroje a zkompilován pomocí
-langversion:11
nebo pomocí corlibu obsahujícího příznak funkce pro poleref
, volání metody se analyzuje pomocí pravidel C#11. - Pokud modul obsahující deklaraci metody odkazuje
System.Runtime { ver: 7.0 }
, volání metody se analyzuje pomocí pravidel C#11. Toto pravidlo je dočasné zmírnění rizik pro moduly zkompilované se staršími verzemi Preview C#11 / .NET 7 a později se odeberou. - V opačném případě se volání metody analyzuje pomocí pravidel C#7.2.
Kompilátor pre-C#11 bude ignorovat všechny RefSafetyRulesAttribute
a analyzovat volání metod pouze pomocí pravidel C#7.2.
RefSafetyRulesAttribute
se bude shodovat s názvem kvalifikovaným oborem názvů, takže se definice nemusí zobrazovat v žádném konkrétním sestavení.
Typ RefSafetyRulesAttribute
je určen pouze pro použití kompilátoru – ve zdroji není povolený. Deklarace typu je syntetizována kompilátorem, pokud ještě není součástí kompilace.
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;
}
}
Bezpečné vyrovnávací paměti pevné velikosti
Bezpečné vyrovnávací paměti s pevnou velikostí nebyly zahrnuty v jazyce C# 11. Tuto funkci je možné implementovat v budoucí verzi jazyka C#.
Jazyk uvolní omezení polí s pevnou velikostí tak, aby je bylo možné deklarovat v bezpečném kódu a typ prvku lze spravovat nebo nespravovat. Tímto se typy, jako jsou následující, stanou legálními:
internal struct CharBuffer
{
internal char Data[128];
}
Tyto deklarace, podobně jako jejich ekvivalenty unsafe
, definují posloupnost N
prvků v obsahujícím typu. K těmto členům je možné přistupovat pomocí indexeru a lze je také převést na Span<T>
a ReadOnlySpan<T>
instance.
Při indexování do vyrovnávací paměti fixed
typu T
je potřeba vzít v úvahu readonly
stav kontejneru. Pokud je kontejner readonly
, vrátí indexer ref readonly T
jinak vrátí ref T
.
Přístup k vyrovnávací paměti fixed
bez indexeru nemá žádný přirozený typ, avšak je možné ji převést na typy Span<T>
. V případě, že je kontejner readonly
, je možné vyrovnávací paměť implicitně převést na ReadOnlySpan<T>
, jinak se může implicitně převést na Span<T>
nebo ReadOnlySpan<T>
(přičemž převod na Span<T>
se považuje za lepší).
Výsledná Span<T>
instance bude mít délku rovnu velikosti deklarované ve vyrovnávací paměti fixed
.
bezpečný kontext vrácené hodnoty bude roven bezpečnému kontextu kontejneru, stejně jako kdyby byla podpůrná data přístupná jako pole.
Pro každou deklaraci fixed
v rámci typu, kde je typ prvku T
, jazyk vygeneruje odpovídající metodu indexeru pouze get
, jejíž návratový typ je ref T
. Indexer bude označen atributem [UnscopedRef]
, protože implementace bude vracet pole deklarujícího typu. Přístupnost člena bude odpovídat přístupnosti v poli fixed
.
Například podpis indexeru pro CharBuffer.Data
bude následující:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Pokud je zadaný index mimo hranice deklarované pro pole fixed
, bude vyvolána výjimka IndexOutOfRangeException
. V případě, že je zadaná konstantní hodnota, bude nahrazena přímým odkazem na příslušný prvek. Pokud není konstanta mimo deklarované hranice, v takovém případě by došlo k chybě času kompilace.
Pro každou vyrovnávací paměť fixed
se vygeneruje také pojmenovaný přístupový prvek, který poskytuje operace get
a set
podle hodnoty. To znamená, že fixed
vyrovnávací paměti budou blíže připomínat stávající sémantiku pole tím, že mají ref
příslušenství, stejně jako vedlejší get
a set
operace. To znamená, že kompilátory budou mít stejnou flexibilitu při generování kódu, který využívá vyrovnávací paměti fixed
, jako když využívají pole. To by mělo usnadnit generování operací, jako je await
, nad bufferem fixed
.
To má také přidanou výhodu, že fixed
vyrovnávací paměti se dají snáze používat v jiných jazycích. Pojmenované indexery jsou funkce, která existuje od verze 1.0 rozhraní .NET. Dokonce i jazyky, které nemohou přímo emitovat pojmenovaný indexer, je obecně spotřebovávají (C# je ve skutečnosti dobrým příkladem).
Záložní úložiště pro vyrovnávací paměť se vygeneruje pomocí atributu [InlineArray]
. Toto je mechanismus diskutovaný v problému 12320, který konkrétně umožňuje efektivně deklarovat posloupnost polí stejného typu. Tento konkrétní problém je stále předmětem aktivní diskuze a očekává se, že implementace této funkce bude následovat podle jejího závěru.
Inicializátory s hodnotami ref
ve výrazech new
a with
V části 12.8.17.3 Inicializátory objektů, aktualizujeme gramatiku na:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
V části pro výraz with
aktualizujeme gramatiku na:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
Levý operand přiřazení musí být výraz, který je svázán s polem typu ref.
Pravý operand musí být výraz, který vrací lvalue označující hodnotu stejného typu jako levý operand.
Přidáme podobné pravidlo pro přiřazení ref místní.
Pokud je levý operand odkazem umožňujícím zápis (tj. označuje cokoli jiného než pole ref readonly
), musí být pravý operand zapisovatelným lvalue.
Úniková pravidla pro vyvolání konstruktoru zůstanou:
Výraz
new
, který vyvolá konstruktor, dodržuje stejná pravidla jako volání metody, která je považována za vracející vytvářený typ.
Konkrétně pravidla vyvolání metody aktualizována výše:
Hodnota rvalue vyplývající z vyvolání metody
e1.M(e2, ...)
má bezpečný kontext z nejmenších následujících kontextů:
- Kontext volajícího
- Bezpečný kontext, k němuž přispěly všechny výrazy argumentů.
- Když je návrat
ref struct
pak ref-safe-kontext přispěl všemi argumentyref
U výrazu new
s inicializátory se výrazy inicializátoru počítají jako argumenty (přispívají svým bezpečným kontextem) a výrazy inicializátoru ref
se počítají jako argumenty ref
(přispívají svým ref-bezpečným kontextem), rekurzivně.
Změny v nebezpečném kontextu
Typy ukazatelů (oddíl 23.3) jsou rozšířeny tak, aby umožňovaly spravované typy jako odkazující typ.
Tyto typy ukazatelů se zapisují jako spravovaný typ následovaný tokenem *
. Vygenerují upozornění.
Operátor adresy (oddílu 23.6.5) je uvolněný pro přijetí proměnné se spravovaným typem jako operandem.
Příkaz fixed
(oddíl 23.7) je uvolněný, aby přijímal fixed_pointer_initializer, který je adresou proměnné spravovaného typu T
nebo výrazem array_type s prvky spravovaného typu T
.
Inicializátor přidělování zásobníku (oddíl 12.8.22) je obdobně zjednodušený.
Úvahy
Při vyhodnocování této funkce by měly být zvažovány i další části vývojového zásobníku.
Úvahy o kompatibilitě
Výzvou v tomto návrhu je dopad na kompatibilitu, který tento návrh má na stávající pravidla bezpečnosti, nebo na stávající §9.7.2. Ačkoli tato pravidla plně podporují koncept ref struct
s ref
poli, nedovolují API, s výjimkou stackalloc
, zachytit ref
stav, který odkazuje na zásobník. Pravidla bezpečného kontextu ref mají pevný předpokladnebo §16.4.12.8, že konstruktor formuláře Span(ref T value)
neexistuje. To znamená, že bezpečnostní pravidla nepočítá, že parametr ref
může utéct jako pole ref
, a proto umožňuje kód podobný následujícímu.
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);
}
V podstatě existují tři způsoby, jak může ref
parametr uniknout během vyvolání metody:
- Podle návratu hodnoty
- Podle
ref
return - Pole
ref
veref struct
, které je vráceno nebo předáno jako parametrref
/out
Stávající pravidla pouze představují (1) a (2). Nezohlední (3), a proto nejsou zohledněny mezery, jako je vrácení místních proměnných v polích ref
. Tento návrh musí změnit pravidla, aby zohlednil (3). To bude mít malý dopad na kompatibilitu stávajících rozhraní API. Konkrétně to bude mít vliv na rozhraní API, která mají následující vlastnosti.
- Mít něco jako
ref struct
v podpisu- Kde je
ref struct
návratový typ,ref
nebo parametrout
- Má další parametr
in
neboref
s výjimkou příjemce.
- Kde je
V C# 10 volající takových API nikdy nemuseli zvažovat, že vstup ref
do rozhraní API mohl být zachycen jako pole ref
. To umožnilo existenci několika vzorců v jazyce C# 10, které budou v jazyce C# 11 nebezpečné, kvůli možnosti, že stav ref
může uniknout jako pole ref
. Například:
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]);
}
Očekává se, že dopad tohoto přerušení kompatibility bude velmi malý. Ovlivněný tvar rozhraní API nemá smysl v nepřítomnosti ref
polí, proto je nepravděpodobné, že zákazníci vytvořili mnoho z těchto polí. Probíhají experimenty s nástroji ke zjištění této struktury API nad stávajícími úložišti, které tuto domněnku podporují. Jediným úložištěm s významnými počty tohoto obrazce je dotnet/runtime a je to proto, že toto úložiště může vytvářet ref
pole prostřednictvím intrinsického typu ByReference<T>
.
Návrh i tak musí zohledňovat existenci takových rozhraní API, protože vyjadřují platný vzor, avšak ne běžný. Návrh proto musí vývojářům poskytnout nástroje k obnovení stávajících pravidel životnosti při upgradu na C# 10. Konkrétně musí poskytovat mechanismy, které vývojářům umožňují anotovat parametry ref
tak, aby nemohly uniknout prostřednictvím pole ref
nebo ref
. To zákazníkům umožňuje definovat rozhraní API v jazyce C# 11, která mají stejná pravidla volání C# 10.
Referenční sestavení
Referenční sestavení pro kompilaci pomocí funkcí popsaných v tomto návrhu musí udržovat prvky, které poskytují informace o bezpečném kontextu ref. To znamená, že všechny atributy anotace životnosti musí být zachovány v původní pozici. Jakýkoli pokus o nahrazení nebo vynechání může vést k neplatným referenčním sestavám.
Reprezentace ref
polí je subtilnější. V ideálním případě by se pole ref
zobrazovalo v referenčním sestavení stejně jako jakékoli jiné pole. Pole ref
ale představuje změnu formátu metadat a může způsobovat problémy s řetězy nástrojů, které se neaktualizují, aby porozuměly této změně metadat. Konkrétním příkladem je C++/CLI, které pravděpodobně vyvolá chybu, pokud zpracovává pole ref
. Proto je výhodné, pokud ref
pole mohou být vynechána z referenčních sestavení v základních knihovnách.
Pole ref
samo o sobě nemá žádný vliv na pravidla bezpečného kontextu ref. Jako konkrétní příklad zvažte, že překlopení existující definice Span<T>
na použití pole ref
nemá žádný vliv na spotřebu. Proto je možné ref
samotné bezpečně vynechat. Pole ref
ale má další dopady na spotřebu, které je potřeba zachovat:
-
ref struct
, která má poleref
, se nikdy nepovažuje zaunmanaged
- Typ pole
ref
má vliv na nekonečná obecná rozšiřující pravidla. Proto pokud typ poleref
obsahuje parametr typu, který musí být zachován.
Na základě těchto pravidel je zde platná referenční transformace sestavení pro 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
}
Anotace
Životní doby jsou nejpřirozeněji vyjadřovány pomocí typů. Životnost daného programu je bezpečná, když kontrola typů životnosti. I když syntaxe jazyka C# implicitně přidává životnosti k hodnotám, existuje základní systém typů, který zde popisuje základní pravidla. Často je jednodušší diskutovat o implikaci změn návrhu z hlediska těchto pravidel, aby byly zahrnuty do diskuze.
Všimněte si, že to není 100% kompletní dokumentace. Dokumentování každého chování není tady cílem. Místo toho je vhodné vytvořit obecné porozumění a společnou verzi, pomocí které lze model a potenciální změny modelu prodiskutovat.
Obvykle není nutné přímo mluvit o typech života. Výjimky jsou místa, kde se životnosti můžou lišit v závislosti na konkrétním místě vytváření instance. Jedná se o druh polymorfismu a tyto různé životnosti nazýváme "obecné životnosti", které jsou reprezentovány jako obecné parametry. Jazyk C# neposkytuje syntaxi pro vyjádření obecných typů životnosti, takže definujeme implicitní "překlad" z jazyka C# do rozšířeného dolního jazyka, který obsahuje explicitní obecné parametry.
Následující příklady využívají pojmenované doby života. Syntaxe $a
odkazuje na dobu životnosti pojmenovanou a
. Jedná se o život, který sám o sobě nemá žádný význam, ale může být dán do vztahu s jinými životy prostřednictvím syntaxe where $a : $b
. To stanoví, že $a
je konvertibilní na $b
. Může pomoci si představit, že $a
má životnost alespoň tak dlouhou jako $b
.
Existuje několik předdefinovaných životností pro usnadnění a stručnost níže:
-
$heap
: jedná se o životnost jakékoli hodnoty, která existuje v haldě. Je k dispozici ve všech kontextech a signaturách metod. -
$local
: jedná se o dobu života jakékoli hodnoty, která existuje v zásobníku metody. Je to vlastně zástupce názvu pročlena funkce. Je implicitně definován v metodách a může se objevit v podpisech metod s výjimkou jakékoli výstupní pozice. -
$ro
: držitel jmen pro vrácení pouze -
$cm
: jmenný zástupce pro volajícího-kontext
Mezi životnostmi existuje několik předdefinovaných relací:
-
where $heap : $a
pro všechny životnosti$a
where $cm : $ro
-
where $x : $local
pro všechny předdefinované doby životnosti. Uživatelem definované životnosti nemají žádný vztah k místnímu prostředí, pokud nejsou explicitně definovány.
Proměnné životnosti, pokud jsou definovány u typů, mohou být invariantní nebo kovariantní. Jsou vyjádřeny pomocí stejné syntaxe jako obecné parametry:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Parametr životnosti $this
definic typu je není předdefinovaný, ale má k němu přidružená několik pravidel, když je definován:
- Musí to být první parametr životnosti.
- Musí být kovariantní:
out $this
. - Životnost polí
ref
musí být konvertibilní na$this
- Životnost
$this
všech polí, která nejsou ref, musí být$heap
nebo$this
.
Životnost reference se vyjadřuje poskytnutím argumentu životnosti referenci. Například ref
, která odkazuje na heap, je vyjádřena jako ref<$heap>
.
Při definování konstruktoru v modelu se název new
použije pro metodu. Je nutné mít seznam parametrů pro vrácenou hodnotu a také argumenty konstruktoru. To je nezbytné k vyjádření vztahu mezi vstupy konstruktoru a konstruovanou hodnotou. Místo Span<$a><$ro>
bude model místo toho používat Span<$a> new<$ro>
. Typ this
v konstruktoru, včetně životnosti, bude určovat definovanou návratovou hodnotu.
Základní pravidla životnosti jsou definována takto:
- Všechny životní doby jsou vyjádřeny syntakticky jako generické argumenty, které se nacházejí před argumenty typu. To platí pro předdefinované životnosti s výjimkou
$heap
a$local
. - Všechny typy
T
, které nejsouref struct
implicitně mají životnostT<$heap>
. To je implicitní, není nutné psátint<$heap>
v každé ukázce. - Pro pole
ref
definované jakoref<$l0> T<$l1, $l2, ... $ln>
:- Všechny životnosti
$l1
až$ln
musí být invariantní. - Životnost
$l0
musí být konvertibilní na$this
- Všechny životnosti
- U
ref
definovaného jakoref<$a> T<$b, ...>
musí být$b
konvertibilní na$a
-
ref
proměnné má dobu života definovanou:- U
ref
místního, parametru, pole nebo návratu typuref<$a> T
je životnost$a
-
$heap
pro všechny odkazové typy a pole referenčních typů -
$local
pro všechno ostatní
- U
- Přiřazení nebo vrácení je legální, pokud je konverze základního typu legální.
- Životnost výrazů lze explicitně vyjádřit pomocí přetypovacích anotací:
-
(T<$a> expr)
hodnota má explicitní životnost$a
proT<...>
-
ref<$a> (T<$b>)expr
životnost hodnoty je$b
proT<...>
a životnost ref je$a
.
-
Pro účely pravidel životnosti je ref
považováno za součást typu výrazu v rámci konverzí. Logicky je reprezentována převodem ref<$a> T<...>
na ref<$a, T<...>>
, kde $a
je kovariantní a T
je invariantní.
Teď nadefinujme pravidla, která nám umožňují namapovat syntaxi jazyka C# na podkladový model.
V zájmu stručnosti se s typem, který nemá žádné explicitní parametry životnosti, zachází, jako by měl definovaný out $this
, který se aplikuje na všechna pole typu. Typ s polem ref
musí definovat explicitní parametry životnosti.
Tato pravidla podporují naši stávající invariantní funkci, kterou lze T
přiřadit ke scoped T
pro všechny typy. To odpovídá tomu, že T<$a, ...>
lze přiřadit T<$local, ...>
pro všechny doby životnosti, které jsou známy jako konvertibilní na $local
. Dále to podporuje další položky, jako je možnost přiřazovat Span<T>
z haldy těm, které jsou v zásobníku. Tím se vyloučí typy, u kterých pole mají odlišné životnosti pro hodnoty, které nejsou ref, ale to je realita jazyka C#. Změna by vyžadovala významnou úpravu pravidel jazyka C#, která by se musela podrobně naplánovat.
Typ this
pro typ S<out $this, ...>
uvnitř metody instance je implicitně definován jako následující:
- Pro klasickou metodu instance:
ref<$local> S<$cm, ...>
- Například metoda s poznámkami
[UnscopedRef]
:ref<$ro> S<$cm, ...>
Nedostatek explicitního parametru this
vyžaduje použití implicitních pravidel. U komplexních vzorků a diskuzí zvažte zvážit psaní jako metodu static
a nastavit this
jako explicitní parametr.
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) { }
}
Syntaxe metody C# je na model namapována následujícími způsoby:
- parametry
ref
mají životnost ref$ro
- Parametry typu
ref struct
mají takovou životnost$cm
- Vracení hodnot ref má životnost ref
$ro
- návraty typu
ref struct
mají životnost hodnoty$ro
-
scoped
u parametru neboref
změní životnost odkazu tak, aby byla$local
Nyní se podívejme na jednoduchý příklad, který zde ukazuje 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;
}
Teď se podíváme na stejný příklad pomocí 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;
}
Teď se podíváme, jak to pomůže s cyklickým problémem vlastního přiřazení:
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;
}
}
Dále se podíváme, jak to pomůže s hloupým problémem ohledně parametrů zachycení:
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;
}
}
Otevřené problémy
Změna návrhu tak, aby nedocházelo k přerušení kompatibility
Tento návrh předkládá několik neslučitelných změn s našimi stávajícími pravidly pro bezpečný kontext referencí. I když se předpokládá, že změny mají minimální dopad, byla věnována značná pozornost návrhu, který neobsahoval žádné zásadní změny.
Návrh zachovávající kompatibilitu byl ale mnohem složitější než tento. Aby bylo možné zachovat kompatibilitu, pole ref
potřebují jedinečné životnostní hodnoty, aby se mohly vrátit prostřednictvím pole ref
a prostřednictvím pole ref
. V podstatě to vyžaduje, abychom poskytli ref-field-safe-context sledování všech parametrů pro metodu. To je potřeba vypočítat pro všechny výrazy a sledovat ve všech hodnotách prakticky všude, kde ref-safe-context je sledován dnes.
Dále má tato hodnota vztahy s kontextem bezpečného odkazu . Například není smysluplné mít hodnotu, která může být vrácena jako pole ref
, ale ne přímo jako ref
. Je to proto, že ref
pole mohou být triviálně vrácena ref
(ref
stav v ref struct
lze vrátit ref
, i když obsahující hodnotu nelze vrátit). Pravidla proto dále potřebují konstantní úpravy, aby se zajistilo, že tyto hodnoty jsou rozumné vzhledem k sobě navzájem.
Také to znamená, že jazyk potřebuje syntaxi k reprezentaci ref
parametrů, které se mohou vrátit třemi různými způsoby: podle ref
pole, pomocí ref
a podle hodnoty. Je možné vrátit se k výchozímu nastavení pomocí ref
. V budoucnu se očekává, že přirozenější návrat, zejména když se jedná o ref struct
, bude přes pole ref
nebo ref
. To znamená, že nová rozhraní API vyžadují dodatečnou syntaktickou anotaci, aby byla ve výchozím nastavení správná. To je nežádoucí.
Tyto změny kompatibility však ovlivní metody s následujícími vlastnostmi.
- Mít
Span<T>
neboref struct
- Kde je
ref struct
návratový typ,ref
nebo parametrout
- Má další parametr
in
neboref
(s výjimkou příjemce).
- Kde je
Abychom pochopili dopad, je užitečné rozdělit rozhraní API do kategorií:
- Chceme, aby spotřebitelé zohlednili, že
ref
je zachyceno jako poleref
. Příkladem jsou konstruktorySpan(ref T value)
- Nepřejeme si, aby zákazníci brali v úvahu
ref
, které bylo zachyceno jako poleref
. Ty se ale dělí do dvou kategorií.- Nebezpečná rozhraní API Jedná se o rozhraní API uvnitř typů
Unsafe
aMemoryMarshal
, přičemž nejvýraznější jeMemoryMarshal.CreateSpan
. Tato rozhraní API zachytávajíref
nebezpečně, ale jsou také známá jako nebezpečná rozhraní API. - Bezpečná rozhraní API. Jedná se o rozhraní API, která přijímají
ref
parametrů pro efektivitu, ale ve skutečnosti nejsou nikde zaznamenány. Příklady jsou malé, ale jeden jeAsnDecoder.ReadEnumeratedBytes
- Nebezpečná rozhraní API Jedná se o rozhraní API uvnitř typů
Tato změna primárně přináší výhody (1) výše. Očekává se, že většinu rozhraní API, která přijímají ref
a vracejí ref struct
, budou tvořit v budoucnu. Změny negativně ovlivňují (2.1) a (2.2), protože narušují stávající sémantiku volání v důsledku změny pravidel životnosti.
Rozhraní API v kategorii (2.1) jsou z velké části tvořená Microsoftem nebo vývojáři, kteří nejvíce těží z výhod polí ref
(těmi, kteří jsou jako Tanner ve světě). Je rozumné předpokládat, že tato třída vývojářů by byla přívětivá k dani z kompatibility při upgradu na C# 11 ve formě několika anotací pro zachování stávající sémantiky, pokud by byla na oplátku poskytována pole ref
.
Největší problém jsou rozhraní API v kategorii (2.2). Není známo, kolik takových rozhraní API existuje, a není jasné, jestli by to bylo více nebo méně časté v kódu třetí strany. Očekává se, že jejich počet je velmi malý, obzvláště pokud dojde k narušení kompatibility v případě out
. Dosavadní hledání odhalilo, že na ploše public
existuje velmi malý počet těchto. Je to těžké hledat, protože vyžaduje sémantickou analýzu. Před provedením této změny by byl potřeba přístup založený na nástrojích k ověření předpokladů souvisejících s tímto dopadem na malý počet známých případů.
U obou případů v kategorii (2) je oprava jednoduchá. Parametry ref
, které nechtějí být považovány za zachytitelné, musí do scoped
přidat ref
. V (2.1) to pravděpodobně také přiměje vývojáře použít Unsafe
nebo MemoryMarshal
, což se očekává u nejištěných rozhraní stylu API.
V ideálním případě může jazyk snížit dopad neznatelných změn způsobujících problémy tím, že vydá upozornění, když rozhraní API nepozorovaně se dostane do problematického chování. To by byla metoda, která vezme ref
, vrátí ref struct
, ale ve skutečnosti nezachytí ref
v ref struct
. Kompilátor by mohl v takovém případě vystavit diagnostiku informující vývojáře, ref
by měla být místo toho označena jako scoped ref
.
rozhodnutí Tento návrh lze dosáhnout, ale výsledná funkce je obtížnější použít do té míry, že bylo rozhodnuto přistoupit k narušení kompatibility.
Rozhodnutí Kompilátor zobrazí upozornění, když metoda splňuje kritéria, ale nezachytí parametr ref
jako pole ref
. To by mělo zákazníky při upgradu vhodně upozornit na potenciální problémy, které vytvářejí.
Klíčová slova vs. atributy
Tento návrh požaduje použití atributů pro anotaci nových pravidel životnosti. To se také dalo udělat stejně snadno pomocí kontextových klíčových slov. Například [DoesNotEscape]
může být mapováno na scoped
. Ale klíčová slova, i kontextová, obecně musí splňovat velmi vysokou laťku pro zahrnutí. Zabírají cenné místo v jazyce a jsou jeho výraznějšími součástmi. Tato funkce, i když je cenná, bude sloužit menšině vývojářů v jazyce C#.
Na první pohled se zdá, že není třeba používat klíčová slova, ale je třeba zvážit dva důležité body:
- Poznámky budou mít vliv na sémantiku programu. Mít atributy, které ovlivňují sémantiku programu, je hranice, kterou se C# zdráhá překročit, a není jasné, zda by tato funkce měla být dostatečným důvodem k tomu, aby jazyk tento krok učinil.
- Pravděpodobně vývojáři, kteří používají tuto funkci, se významně překrývají se skupinou vývojářů používajících ukazatele na funkce. Tato funkce, která je zároveň používána menšinou vývojářů, vyžadovala novou syntaxi a toto rozhodnutí je stále považováno za opodstatněné.
Celkově to znamená, že syntaxe by měla být zvážena.
Hrubá skica syntaxe by byla:
-
[RefDoesNotEscape]
odpovídáscoped ref
-
[DoesNotEscape]
odpovídáscoped
-
[RefDoesEscape]
odpovídáunscoped
Rozhodnutí Použít syntaxi pro scoped
a scoped ref
; použít atribut pro unscoped
.
Povolit pevné místní vyrovnávací paměti
Tento návrh umožňuje bezpečné fixed
buffery, které podporují jakýkoli typ. Jedním z možných rozšíření je povolit deklaraci takových fixed
vyrovnávacích pamětí jako místních proměnných. To by umožnilo nahrazení několika existujících operací stackalloc
vyrovnávací pamětí fixed
. Také by se rozšířila sada scénářů, kdy bychom mohli provádět alokace ve stylu zásobníku, protože stackalloc
je omezený na nespravované typy prvků, zatímco fixed
vyrovnávací paměť není.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
To je celistvé, ale vyžaduje, abychom trochu rozšířili syntaxi pro lokály. Nejasné, jestli je to nebo nestojí za další složitost. Mohli bychom se prozatím rozhodnout pro ne a vrátit se k tomu později, pokud bude prokázána dostatečná potřeba.
Příklad, kde by to bylo výhodné: https://github.com/dotnet/runtime/pull/34149
Rozhodnutí zatím odložte
Použít modreqs nebo ne
Je třeba rozhodnout, zda by metody označené novými atributy životnosti měly nebo neměly být přeloženy jako modreq
při výstupu v emit. Kdyby byl tento přístup použit, existovalo by v podstatě mapování 1:1 mezi poznámkami a modreq
.
Odůvodněním přidání modreq
jsou atributy, které mění sémantiku pravidel bezpečného kontextu ref. Příslušné metody by měly volat pouze jazyky, které těmto sémantikům rozumí. Při použití ve scénářích OHI se životnosti stanou kontraktem, který musí implementovat všechny odvozené metody. Pokud existují anotace bez modreq
, může to vést k situacím, kdy jsou načteny řetězce metod virtual
s konfliktními anotacemi životnosti (což může nastat, pokud je zkompilována jen jedna část řetězce virtual
a druhá není).
Počáteční práce s bezpečným kontextem ref nepoužíla modreq
, ale místo toho využívala jazyky a architekturu k pochopení. Ve stejnou dobu jsou všechny prvky, které přispívají k pravidlům bezpečného kontextu ref, silnou součástí podpisu metody: ref
, in
, ref struct
atd ... Proto jakákoli změna stávajících pravidel metody již vede k binární změně podpisu. Pokud chcete novým poznámkám o životnosti dát stejný dopad, bude potřeba modreq
vynucení.
Otázkou je, zda je to přehnané. Negativní dopad spočívá v tom, že zvýšení flexibility podpisů, například přidáním [DoesNotEscape]
do parametru, povede ke změně binární kompatibility. Tento kompromis znamená, že postupem času pravděpodobně rámce jako BCL nebudou moci takové podpisy uvolnit. Lze to zmírnit do určité míry použitím přístupu podobného tomu, jak jazyk pracuje s parametry in
, a použitím modreq
pouze ve virtuálních pozicích.
Rozhodnutí Nepoužívejte modreq
v metadatech. Rozdíl mezi out
a ref
není modreq
, ale nyní mají různé hodnoty bezpečné ref kontextu. Neexistuje žádná skutečná výhoda částečného vynucování pravidel s modreq
zde.
Povolit multidimenzionální pevné vyrovnávací paměti
Má být návrh vyrovnávacích pamětí fixed
rozšířen tak, aby zahrnoval vícerozměrná pole stylů? V podstatě umožňuje deklarace jako následující:
struct Dimensions
{
int array[42, 13];
}
rozhodnutí Prozatím nepovolit
Porušení vymezeného rozsahu
Úložiště modulu runtime obsahuje několik neveřejných rozhraní API, která zaznamenávají parametry ref
jako pole ref
. Jsou nebezpečné, protože životnost výsledné hodnoty není sledována. Například konstruktor Span<T>(ref T value, int length)
.
Většina těchto rozhraní API se pravděpodobně rozhodne, že bude mít správné sledování životnosti u vrácené hodnoty, čehož lze dosáhnout tím, že se jednoduše aktualizuje na C# 11. Několik z nich ale bude chtít zachovat jejich aktuální sémantiku, aby nesledovaly návratovou hodnotu, protože jejich celý záměr je nebezpečný. Nejdůležitějšími příklady jsou MemoryMarshal.CreateSpan
a MemoryMarshal.CreateReadOnlySpan
. Toho dosáhnete tak, že parametry označíte jako scoped
.
To znamená, že modul runtime potřebuje zavedený vzor pro nebezpečné odebrání scoped
z parametru:
-
Unsafe.AsRef<T>(in T value)
by mohl rozšířit svůj stávající účel změnou nascoped in T value
. To by umožnilo odebratin
iscoped
z parametrů. Pak se stane univerzální metodou "remove ref safety" (odebrat bezpečnost ref). - Zavést novou metodu, jejíž celý účel je odstranit
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Tím se odebere iin
, protože kdyby tomu tak nebylo, volající by stále potřebovali kombinaci volání metod ke "zrušení bezpečnosti ref", a v tom okamžiku je stávající řešení pravděpodobně postačující.
Zruší se výchozí rozsah?
Návrh má ve výchozím nastavení pouze dvě umístění, která jsou scoped
:
-
this
jescoped ref
-
out
jescoped ref
Rozhodnutí o out
je výrazně snížit kompatní zátěž ref
polí a zároveň je přirozenějším výchozím nastavením. Umožňuje vývojářům vnímat out
jako tok dat směřující pouze ven, zatímco pokud jde o ref
, pravidla musí zohlednit tok dat v obou směrech. To vede k významným nejasnostem vývojářů.
Rozhodnutí o this
je nežádoucí, protože to znamená, že struct
nemůže vrátit pole pomocí ref
. Jedná se o důležitý scénář pro vývojáře s vysokým výkonem a atribut [UnscopedRef]
byl v podstatě přidán pro tento scénář.
Klíčová slova mají vysoké nároky a jejich přidání pouze pro jeden scénář je podezřelé. Uvažovalo se, zda bychom se mohli vyhnout tomuto klíčovému slovu tím, že by se this
stalo jednoduše ref
ve výchozím nastavení a ne scoped ref
. Všichni členové, kteří potřebují, aby this
bylo scoped ref
, by to mohli udělat označením metody scoped
(stejně jako je dnes možné označit metodu readonly
pro vytvoření readonly ref
).
Za normálních okolností u struct
je to většinou pozitivní změna, protože představuje pouze problémy s kompatibilitou, když má člen vracející se ref
. "Těchto metod je velmi málo a nástroj by je mohl rychle odhalit a převést na scoped
členy."
Na ref struct
tato změna přináší výrazně větší problémy s kompatibilitou. Vezměte v úvahu následující skutečnosti:
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;
}
}
V podstatě by to znamenalo, že všechna volání instanční metody na proměnnéthis
.
readonly ref struct
tento problém nemá, protože readonly
příroda brání opětovnému přiřazení odkazu. Stále by to byla významná zpětně nekompatibilní změna, protože by ovlivnila prakticky všechny existující proměnlivé ref struct
.
A readonly ref struct
je stále problematické, jakmile rozšíříme pole ref
až po ref struct
. Umožňuje řešení stejného základního problému tím, že jednoduše přenáší zachycení do hodnoty pole 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);
}
}
Bylo uvažováno o myšlence, že this
má různé výchozí hodnoty na základě typu struct
nebo člena. Například:
-
this
jakoref
:struct
,readonly ref struct
neboreadonly member
-
this
jakoscoped ref
:ref struct
neboreadonly ref struct
s polemref
proref struct
Tím se minimalizuje přerušení kompatibility a maximalizuje flexibilita, ale za cenu komplikování scénáře pro zákazníky. Tento problém rovněž neřeší do plné míry, protože budoucí funkce, jako jsou bezpečné vyrovnávací paměti fixed
, vyžadují, aby proměnlivé ref struct
měly návraty ref
pro pole, která nefungují pouze dle tohoto návrhu, jelikož by spadala do kategorie scoped ref
.
Rozhodnutí zachovat this
jako scoped ref
. To znamená, že předchozí záludné příklady vytvářejí chyby kompilátoru.
ref pole na ref struktura
Tato funkce otevírá novou sadu bezpečnostních pravidel pro referenční kontext, protože umožňuje poli ref
odkazovat na ref struct
. Tato obecná povaha ByReference<T>
znamenala, že runtime až dosud nemohl mít takovou konstrukci. V důsledku toho jsou všechna naše pravidla napsána za předpokladu, že to není možné. Funkce pole ref
do značné míry nesděluje nová pravidla, ale kodifikuje stávající pravidla v našem systému. Povolení polí ref
do ref struct
vyžaduje, abychom zakotvili nová pravidla, protože je několik nových scénářů, které je potřeba zvážit.
První věc je, že readonly ref
je nyní schopný uložit stav ref
. Například:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
To znamená, že když uvažujeme o argumentech metody, které musí odpovídat pravidlům, musíme vzít v úvahu, že readonly ref T
je potenciální výstup metody, pokud T
potenciálně obsahuje pole ref
směrem k ref struct
.
Druhý problém je, že jazyk musí zvážit nový typ bezpečného kontextu: ref-field-safe-context. Všechny ref struct
, které tranzitivně obsahují pole ref
, mají jiný únikový obor představující hodnotu (hodnoty) v polích ref
. Pokud existuje více polí ref
, mohou být sledována souhrnně jako jedna hodnota. Výchozí hodnota pro tyto parametry je kontext volajícího.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Tato hodnota nesouvisí s bezpečným kontextem kontejneru; to znamená, že když se kontext kontejneru zmenšuje, nemá to žádný vliv na ref-field-safe-context hodnot ref
polí. Dále kontext ref-pole nemůže být nikdy menší než bezpečný kontext kontejneru.
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];
}
Tato kontextová ref-field-safe-context v podstatě existovala. Až dosud ref
pole mohla odkazovat pouze na normální struct
, proto byla jednoduše zredukována na kontext volajícího. Aby bylo možné podporovat pole ref
pro ref struct
, musí být naše stávající pravidla aktualizována, aby zohledňovala tento nový ref-safe-context .
Za třetí je potřeba aktualizovat pravidla pro opětovné přiřazení odkazu, abychom zajistili, že nedojde k porušení kontextu ref-field-context pro hodnoty. V podstatě pro x.e1 = ref e2
, kde je typ e1
ref struct
, musí být ref-field-safe-context stejný.
Tyto problémy jsou velmi řešitelné. Tým kompilátoru navrhl několik různých verzí těchto pravidel, které z velké části vyplývají z naší stávající analýzy. Problém spočívá v tom, že neexistuje žádný kód pro taková pravidla, která pomáhají prokázat správnost a použitelnost. To nás velmi zdráhá přidat podporu kvůli obavám, že zvolíme nesprávné výchozí hodnoty a dostaneme runtime do problému s použitelností, když to bude využito. Tento problém je obzvláště závažný, protože .NET 8 nás pravděpodobně povede tímto směrem s allow T: ref struct
a Span<Span<T>>
. Bylo by lepší, kdyby byla pravidla napsána ve spojení s kódem spotřeby.
rozhodnutí Zpoždění umožňující ref
pole ref struct
až do .NET 8, kdy máme scénáře, které vám pomůžou řídit pravidla kolem těchto scénářů. Toto nebylo implementováno ve verzi .NET 9.
Co bude součástí C# 11.0?
Funkce popsané v tomto dokumentu není nutné implementovat v jediném průchodu. Místo toho je možné je implementovat ve fázích v několika jazykových verzích v následujících kontejnerech:
-
ref
polí ascoped
[UnscopedRef]
-
ref
polí proref struct
- Typy s omezenými západy slunce
- vyrovnávací paměti s pevnou velikostí
Které funkce se implementují v jaké verzi, je pouze proces určování rozsahu.
Rozhodnutí Pouze (1) a (2) se dostaly do C# 11.0. Zbytek se bude brát v úvahu v budoucích verzích jazyka C#.
Důležité informace o budoucnosti
Rozšířené poznámky k životnímu cyklu
Poznámky k životnosti v tomto návrhu jsou omezené v tom, že umožňují vývojářům změnit výchozí chování hodnot, zda mají být nebo nemají být unikátně upraveny (escape). To do našeho modelu přidává silnou flexibilitu, ale nijak výrazně nemění sadu relací, které je možné vyjádřit. V jádru je model jazyka C# v zásadě stále binární: může být hodnota vrácena, nebo ne?
To umožňuje pochopení relací s omezenou životností. Například hodnota, kterou nelze vrátit z metody, má menší životnost než ta, která může být vrácena z metody. Neexistuje způsob, jak popsat vztah trvání mezi hodnotami, které mohou být vráceny z metody. Konkrétně neexistuje způsob, jak říci, že jedna hodnota má delší životnost než druhá, jakmile je zjištěno, že obě mohou být vráceny z metody. Dalším krokem v našem životním vývoji by bylo umožnit, aby tyto vztahy byly popsány.
Jiné metody, jako Rust, umožňují vyjádřit tento typ relace a proto dokážou implementovat složitější operace stylu scoped
. Náš jazyk by mohl mít podobné výhody, pokud by byla taková funkce zahrnuta. V tuto chvíli není žádný motivační tlak dělat to, ale pokud v budoucnu bude, náš model scoped
by mohl být rozšířen tak, aby to zahrnul poměrně přímočarým způsobem.
Každé scoped
může být přiděleno pojmenovaná doba trvání přidáním obecného stylového argumentu do syntaxe. Například scoped<'a>
je hodnota, která má životnost 'a
. Omezení, jako je where
, se pak dají použít k popisu vztahů mezi těmito životnostmi.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Tato metoda definuje dvě životnosti 'a
a 'b
a jejich vztah, konkrétně že 'b
je větší než 'a
. Toto umožňuje místu volání mít podrobnější pravidla pro bezpečné předávání hodnot do metod, oproti hrubším odstupňovaným pravidlům, která jsou dnes k dispozici.
Související informace
Problémy
Všechny tyto otázky souvisejí s tímto návrhem:
- 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
Návrhy
K tomuto návrhu se vztahují následující návrhy:
Existující ukázky
Tento konkrétní fragment kódu vyžaduje nebezpečné, protože narazí na problémy s předáváním Span<T>
, které lze přidělit metodě instance v ref struct
. I když tento parametr není zachycený, jazyk musí předpokládat, že tomu tak je, a proto zbytečně způsobuje tření.
Tento fragment kódu chce změnit parametr odstraněním speciálních znaků z dat. Uvolněná data mohou být umístěna na zásobník pro zvýšení efektivity. I když parametr není řídicím znakem, kompilátor ho přiřadí bezpečné kontextové mimo uzavřenou metodu, protože se jedná o parametr. To znamená, že aby bylo možné použít přidělení zásobníku, musí implementace používat unsafe
, aby se po zapouzdření dat vrátila k parametru.
Zábavné ukázky
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;
}
}
Šetrný seznam
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;
}
}
}
}
Příklady a poznámky
Níže najdete sadu příkladů, které ukazují, jak a proč pravidla fungují tak, jak fungují. Součástí je několik příkladů, které ukazují nebezpečné chování a jak jim pravidla brání v jejich vzniku. Při úpravách návrhu je důležité je mít na paměti.
Přiřazení reference a místa volání
Demonstrující, jak opětovné přiřazení ref a vyvolání metody vzájemně spolupracují.
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;
}
}
Opětovné přiřazení reference a nebezpečné úniky
Důvod pro následující řádek v pravidlech přeřazení odkazů nemusí být na první pohled zřejmý:
e1
musí mít stejné bezpečné kontextové jakoe2
Důvodem je to, že životnost hodnot, na které odkazuje ref
umístění, je invariantní. Zprostředkování nám brání povolit jakoukoli variabilitu, dokonce i směrem k užším životnostem. Pokud je zúžení povoleno, otevře se následující nebezpečný kód:
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];
}
Pro ref
a ne ref struct
je toto pravidlo triviálně splněno, protože všechny hodnoty mají stejný bezpečný kontext. Toto pravidlo skutečně přichází do hry pouze v případě, že hodnota je ref struct
.
Toto chování ref
bude také důležité v budoucnu, kdy povolíme pole ref
ref struct
.
místní prostředí s vymezeným oborem
Použití scoped
u místních proměnných bude zvlášť užitečné pro vzory kódu, které podmíněně přiřazují hodnoty s různým bezpečným kontextem těmto proměnným. Znamená to, že kód už nemusí spoléhat na inicializační triky, jako je třeba použití = stackalloc byte[0]
pro definování lokálního bezpečného kontextu , ale nyní může jednoduše použít 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];
}
Tento vzor se často objevuje v kódu nízké úrovně. Když je ref struct
zapojen do Span<T>
, lze použít výše uvedený trik. Nevztahuje se ale na jiné typy ref struct
a může vést k tomu, že se kód nízké úrovně musí uchýlit k unsafe
, aby se obešlou nemožnost správně určit životnost.
hodnoty parametrů s vymezeným oborem
Jedním ze zdrojů opakovaného tření v programovém kódu nízké úrovně je, že výchozí unik pro parametry je povolující. Jsou ref struct
a toto výchozí nastavení může způsobit konflikt s jinými částmi pravidel bezpečného kontextu ref.
K hlavnímu třecímu bodu dochází, protože argumenty metody musí odpovídat pravidlu. Toto pravidlo se nejčastěji uplatňuje u metod instance na ref struct
, kde je alespoň jeden parametr také ref struct
. Jedná se o běžný vzor v kódu nízké úrovně, kde ref struct
typy běžně využívají parametry Span<T>
ve svých metodách. Například k tomu dojde v jakémkoli stylu zápisu ref struct
, který používá Span<T>
pro předávání vyrovnávacích pamětí.
Toto pravidlo existuje, aby se zabránilo scénářům, jako jsou následující:
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);
}
}
V podstatě toto pravidlo existuje, protože jazyk musí předpokládat, že všechny vstupy do metody uniknou do svého maximálního povoleného bezpečného kontextu. Pokud existují parametry ref
nebo out
, včetně přijímačů, je možné, že vstupy se objeví ve formě polí těchto hodnot ref
, jak je znázorněno výše u RS.Set
.
V praxi existuje mnoho takových metod, které předávají ref struct
jako parametry, které je nikdy nechystají zachytit ve výstupu. Jedná se pouze o hodnotu, která se používá v rámci aktuální metody. Například:
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 bylo možné obejít tento nízkoúrovňový kód, bude se uchylovat k trikům unsafe
, které podvádějí kompilátor ohledně životnosti jejich ref struct
. Tím se výrazně sníží hodnota ref struct
, protože jsou to prostředky, které umožňují vyhnout se unsafe
zatímco pokračujete v psaní vysoce výkonného kódu.
To je místo, kde scoped
je účinný nástroj pro parametry ref struct
, protože je vyloučí z úvah o návratu z metody, v souladu s aktualizovanými pravidly pro argumenty metody musí odpovídat pravidlu. Parametr ref struct
, který se spotřebovává, ale nikdy nevrací, je možné označit jako scoped
, aby byly weby volání flexibilnější.
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))
{
...
}
}
}
Zabránění složitému přiřazení odkazu z mutace omezené pouze na čtení
Když je ref
přenesen do pole readonly
v konstruktoru nebo členu init
, typ je ref
a ne ref readonly
. Toto je dlouhodobé chování, které umožňuje kód podobný následujícímu:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
To ale představuje potenciální problém, pokud by takový ref
mohl být uložen do pole ref
stejného typu. Umožnilo by přímou mutaci readonly struct
z člena instance:
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++;
}
}
Návrh to ale brání, protože porušuje pravidla bezpečného kontextu ref. Vezměte v úvahu následující skutečnosti:
-
bezpečného kontextu
this
je člen funkce a bezpečného kontextu je kontext volajícího. Oba jsou standardní prothis
vestruct
členu. -
ref-safe-context z
i
je členem funkce. To vyplývá z pravidel životnosti pole . Konkrétně pravidlo 4.
V tomto okamžiku je řádek r = ref i
neplatný podle pravidel opětovného přiřazení .
Tato pravidla nebyla určena k zabránění tomuto chování, ale jako vedlejší účinek. Je důležité mít na paměti, že každá budoucí aktualizace pravidel vyhodnotí dopad na takové scénáře.
Hloupé cyklické přiřazení
Jeden aspekt, se kterým se tento návrh potýkal, je to, jak volně lze vrátit ref
z metody. Pravděpodobně většina vývojářů intuitivně očekává, že vrácení všech ref
jako běžných hodnot bude povoleno. Umožňuje však patologickým scénářům, které kompilátor musí při výpočtu bezpečnosti ref zvážit. Vezměte v úvahu následující skutečnosti:
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;
}
}
Nejedná se o vzor kódu, který očekáváme, že budou používat všichni vývojáři. I když je možné vrátit ref
se stejnou životností jako hodnota, je to legální podle pravidel. Kompilátor musí při vyhodnocování volání metody zvážit všechny právní případy, což vede k efektivnímu nepoužitelnému rozhraní API.
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 bylo možné tato rozhraní API použít, kompilátor zajistí, aby ref
životnost parametru ref
byl menší než životnost odkazů v přidružené hodnotě parametru. To je důvod, proč mít
Upozorňujeme, že [UnscopedRef]
podporuje přenos kontextu ref-safe-context libovolných ref
až na ref struct
hodnoty do kontextu volajícího. Proto to umožňuje cyklické přiřazení a nutí k virálnímu použití [UnscopedRef]
v rámci volacího řetězce.
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;
}
}
Podobně
Zvýšení úrovně [UnscopedRef] ref
na v kontextu volajícího je užitečné, pokud typ neníref struct
(mějte na paměti, že chceme zachovat jednoduchá pravidla, aby nerozlišovaly odkazy na odkaz a struktury bez odkazu):
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;
}
}
Z hlediska pokročilých poznámek vytvoří návrh [UnscopedRef]
následující:
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
Pole označená jen pro čtení nelze zpracovávat do hloubky přes referenční pole.
Podívejte se na následující ukázku kódu:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Při navrhování pravidel pro ref
pole na instancích readonly
ve vakuu lze platná pravidla navrhnout tak, aby to, co je výše uvedeno, bylo legální nebo nelegální. V podstatě readonly
může být platný hluboko v rámci ref
pole nebo se může vztahovat pouze na ref
. Použití pouze pro ref
zabraňuje opětovnému přiřazení odkazu, ale umožňuje normální přiřazení, které změní odkazovanou hodnotu.
Tento návrh však neexistuje izolovaně, ale navrhuje pravidla pro typy, které již fakticky mají pole ref
. Nejvýznamnější z nich, Span<T>
, již má silnou závislost na tom, že readonly
zde není příliš hluboko. Primárním scénářem je možnost přiřadit pole ref
přes instanci 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;
}
}
}
To znamená, že musíme zvolit povrchní výklad readonly
.
Konstruktory modelování
Jednou z drobných otázek návrhu je, jak jsou těla konstruktorů modelována pro bezpečnost ref. Jak se v podstatě analyzuje následující konstruktor?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Existují zhruba dva přístupy:
- Model jako metoda
static
, kdethis
je místní, kde bezpečný kontext je kontext volajícího - Model jako metoda
static
, kdethis
je parametrout
.
Dále musí konstruktor splňovat následující invarianty:
- Ujistěte se, že
ref
parametry lze zachytit jako poleref
. - Ujistěte se, že
ref
k polímthis
nelze uvést pomocí parametrůref
. To by porušilo komplikované přiřazení odkazu.
Záměrem je vybrat formulář, který splňuje naše invarianty bez zavedení jakýchkoli zvláštních pravidel pro konstruktory. Vzhledem k tomu, že nejlepší model pro konstruktory zobrazuje this
jako parametr out
. Pouze vrací charakter out
nám umožňuje uspokojovat všechny výše uvedené invarianty bez nutnosti zvláštního zpracování.
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 se musí shodovat.
Argumenty metody musí odpovídat pravidlu, což je běžným zdrojem nejasností pro vývojáře. Jedná se o pravidlo, které má řadu zvláštních případů, které jsou obtížně pochopitelné, pokud neznáte odůvodnění pravidla. Pro lepší pochopení důvodů pro toto pravidlo zjednodušíme ref-bezpečný-kontext a bezpečný-kontext na jednoduše kontext.
Metody mohou poměrně svobodně vracet stav předaný jim jako parametry. V podstatě jakýkoli dosažitelný stav, který je bez rozsahu, může být vrácen (včetně vrácení pomocí ref
). To lze vrátit přímo příkazem return
nebo nepřímo přiřazením k hodnotě ref
.
Přímé návraty nepředstavují mnoho problémů s bezpečností ref. Kompilátor se jednoduše musí podívat na všechny návratové vstupy metody a pak efektivně omezuje návratovou hodnotu na minimální kontext vstupu. Tato vrácená hodnota pak prochází normálním zpracováním.
Nepřímé návraty představují významný problém, protože všechny ref
jsou vstupem i výstupem metody. Tyto výstupy již mají známý kontext . Kompilátor nemůže odvodit nové, musí je zvážit na aktuální úrovni. To znamená, že kompilátor se musí podívat na každé ref
, které lze přiřadit v volané metodě, vyhodnotit jejich kontext , a pak ověřit, že žádný vstup do metody nemá menší kontext než jakýkoli ref
. Pokud takový případ existuje, volání metody musí být nezákonné, protože by mohlo narušit ref
bezpečnost.
Argumenty metody se musí shodovat s procesem, pomocí kterého kompilátor tuto bezpečnostní kontrolu používá.
Dalším způsobem, jak to vyhodnotit, což je pro vývojáře často jednodušší, je provést následující cvičení:
- Podívejte se na definici metody identifikujte všechna místa, kde může být stav nepřímo vrácen: a. Proměnlivé parametry
ref
ukazující naref struct
b. Proměnlivé parametryref
s poli přiřaditelnými pomocí klíčového slova refref
. Přidělitelné parametryref
nebo poleref
, které ukazují naref struct
(vezměte v úvahu rekurzi). - Podívejte se na místo volání a. Identifikujte kontexty, které jsou v souladu s umístěními identifikovanými nad b. Identifikujte kontexty všech vstupů metody, které je možné vrátit (nezarovnávejte se s parametry
scoped
).
Pokud je jakákoli hodnota v 2.b menší než 2.a, volání metody musí být neplatné. Podívejme se na několik příkladů, které ilustrují pravidla:
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);
}
}
Podívejme se na volání F0
a projděme si body (1) a (2). Parametry s potenciálem nepřímého vrácení jsou a
a b
, protože je možné je přímo přiřadit. Argumenty, které odpovídají těmto parametrům, jsou:
, které se mapuje na , má kontext volajícího-kontextu -
b
, který je mapován nay
, a který má kontext členu funkce
Sada návratového vstupu metody je
s řídicího rozsahu kontextu volajícího s řídicího rozsahu kontextu volajícího -
y
s únikovým-rozsahem člena funkce
Hodnota ref y
není vrátitelná, protože se mapuje na scoped ref
proto se nepovažuje za vstup. Vzhledem k tomu, že existuje alespoň jeden vstup s menším únikovým rozsahem (y
argument) než jeden z výstupů (x
argument), je volání metody neplatné.
Jiná varianta je následující:
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);
}
}
Opět jsou parametry s potenciálem nepřímého vrácení a
a b
, protože oba můžou být přímo přiřazeny. Ale b
lze vyloučit, protože neodkazuje na ref struct
proto nelze použít k ukládání ref
stavu. Máme tedy:
, které se mapuje na , má kontext volajícího-kontextu
Sada vstupů, které lze vrátit metodou, jsou:
s kontextem kontextu volajícího s kontextem kontextu volajícího -
ref y
s kontext funkce-člen
Vzhledem k tomu, že existuje alespoň jeden vstup s menším řídicím rozsahem (ref y
argument) než jeden z výstupů (x
argument), volání metody je neplatné.
Toto je logika, kterou argumenty metody musí shodovat s pravidlem, se snaží zahrnout. V této souvislosti se uvažuje jak scoped
jako způsob odstranění vstupů z úvahy, tak readonly
jako způsob odstranění ref
jako výstupu (nelze přiřadit do readonly ref
, takže nemůže být zdrojem výstupu). Tyto zvláštní případy přidávají do pravidel složitost, ale dělají se tak pro výhodu vývojáře. Kompilátor se snaží odebrat všechny vstupy a výstupy, které ví, že nemůže přispět k výsledku, aby vývojáři při volání člena dosáhli maximální flexibility. Podobně jako řešení přetížení stojí za to, aby naše pravidla byla složitější, když vytváří větší flexibilitu pro uživatele.
Příklady bezpečného kontextu odvozených výrazů deklarace
Související s odvozením bezpečného kontextu výrazů deklarace.
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).
}
}
Všimněte si, že místní kontext, který je výsledkem modifikátoru scoped
, je nejužší, který by mohl být použit pro proměnnou, což by znamenalo, že výraz odkazuje na proměnné deklarované pouze v užším kontextu než samotný výraz.
C# feature specifications