Förbättringar av låg nivå-strukturer
Not
Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.
Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader återspeglas i de relevanta LDM-anteckningarna (Language Design Meeting).
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Sammanfattning
Det här förslaget är en sammanställning av flera olika förslag för struct
prestandaförbättringar: ref
fält och förmågan att åsidosätta standardvärden för livslängd. Målet är en design som tar hänsyn till de olika förslagen för att skapa en enda övergripande uppsättning av funktioner för lågnivåförbättringar för struct
.
Obs! Tidigare versioner av den här specifikationen använde termerna "ref-safe-to-escape" och "safe-to-escape", som introducerades i Span safety funktionalitetsbeskrivning. ECMA-standardkommittén ändrat namnen till "ref-safe-context" respektive "safe-context". Värdena för den säkra kontexten har förfinats så att de använder "declaration-block", "function-member" och "caller-context" konsekvent. Specletarna hade använt olika formuleringar för dessa termer och även använt "safe-to-return" som synonym för "caller-context". Den här specifikationen har uppdaterats för att använda termerna i C# 7.3-standarden.
Alla funktioner som beskrivs i det här dokumentet har inte implementerats i C# 11. C# 11 innehåller:
-
ref
fält ochscoped
[UnscopedRef]
Dessa funktioner är fortfarande öppna förslag för en framtida version av C#:
-
ref
fält tillref struct
- Begränsade typer av solnedgångar
Motivation
Tidigare versioner av C# har lagt till ett antal prestandafunktioner på låg nivå till språket: ref
returnerar, ref struct
, funktionspekare osv. ... Dessa gjorde det möjligt för .NET-utvecklare att skriva kod med hög prestanda samtidigt som de fortsatte att använda C#-språkreglerna för typ- och minnessäkerhet. Det gjorde det också möjligt att skapa grundläggande prestandatyper i .NET-bibliotek som Span<T>
.
Eftersom dessa funktioner har fått fäste i .NET-ekosystemet har utvecklare, både interna och externa, gett oss information om återstående friktionspunkter i ekosystemet. Platser där de fortfarande behöver gå ner till unsafe
-kod för att få sitt arbete gjort, eller där körningen behöver hantera specialfall för typer som Span<T>
.
I dag utförs Span<T>
med hjälp av den internal
typ ByReference<T>
som körningen effektivt behandlar som ett ref
fält. Detta ger fördelen med ref
fält men med nackdelen att språket inte ger någon säkerhetsverifiering för det, som det gör för andra användningar av ref
. Dessutom kan endast dotnet/runtime använda den här typen eftersom det är internal
, så att tredje part inte kan utforma sina egna primitiver baserat på ref
fält. En del av motivationen för det här arbetet är att ta bort ByReference<T>
och använda korrekta ref
fält i alla kodbaser.
Det här förslaget planerar att ta itu med dessa problem genom att bygga vidare på våra befintliga lågnivåfunktioner. Mer specifikt syftar det till att:
- Tillåt
ref struct
typer att deklareraref
fält. - Tillåt körningen att helt definiera
Span<T>
med hjälp av C#-typsystemet och ta bort specialfallstyp somByReference<T>
- Tillåt att
struct
typer returnerarref
till sina fält. - Tillåt körningstid att ta bort
unsafe
användningsområden som orsakas av begränsningar i livstidens standardinställningar - Tillåt deklaration av säkra
fixed
buffertar för hanterade och ohanterade typer istruct
Detaljerad design
Reglerna för ref struct
-säkerhet definieras i säkerhetsdokumentet med hjälp av tidigare termer. Dessa regler har införlivats i C# 7-standarden i §9.7.2 och §16.4.12. Det här dokumentet beskriver de nödvändiga ändringarna i det här dokumentet som ett resultat av det här förslaget. När de har godkänts som en godkänd funktion införlivas ändringarna i dokumentet.
När den här designen är klar blir vår Span<T>
definition följande:
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;
}
}
Ange referensfält och avgränsningar
Språket gör det möjligt för utvecklare att deklarera ref
fält inom en ref struct
. Till exempel kan detta vara användbart när du kapslar in stora ändringsbara struct
-instans eller definierar högprestandatyper som Span<T>
i bibliotek utöver körningen.
ref struct S
{
public ref int Value;
}
Ett ref
-fält kommer att emitteras i metadata med ELEMENT_TYPE_BYREF
-signaturen. Det här skiljer sig inte från hur vi emitterar ref
lokaler eller ref
argument. Till exempel genereras ref int _field
som ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. Detta kräver att vi uppdaterar ECMA335 för att tillåta den här posten, men det bör vara ganska enkelt.
Utvecklare kan fortsätta att initiera en ref struct
med ett ref
-fält genom default
-uttrycket, och i så fall kommer alla deklarerade ref
-fält att ha värdet null
. Alla försök att använda sådana fält resulterar i att en NullReferenceException
genereras.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Medan C#-språket låtsas att en ref
inte kan vara null
är detta tillåtet på körningsnivå och har väldefinierad semantik. Utvecklare som introducerar ref
fält i sina typer måste vara medvetna om den här möjligheten och bör starkt avrådas från att läcka den här informationen till koden som använder dem. I stället bör ref
fält valideras som icke-null med hjälp av runtime-hjälparna och utlösa när en oinitierad struct
används felaktigt.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Ett ref
fält kan kombineras med readonly
modifierare på följande sätt:
-
readonly ref
: det här är ett fält som inte kan tilldelas om utanför en konstruktör ellerinit
-metoder. Det kan tilldelas ett värde men utanför dessa sammanhang -
ref readonly
: det här är ett fält vars referens kan tilldelas om, men som inte kan tilldelas något värde vid något tillfälle. Så här kan enin
-parameter omrefereras till ettref
-fält. -
readonly ref readonly
: en kombination avref readonly
ochreadonly 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)
}
}
En readonly ref struct
kommer att kräva att ref
fält deklareras som readonly ref
. Det finns inget krav på att de deklareras readonly ref readonly
. Detta gör det möjligt för en readonly struct
att ha indirekta mutationer via ett sådant fält, men det skiljer sig inte från ett readonly
fält som pekade på en referenstyp idag (mer information)
En readonly ref
skickas till metadata med flaggan initonly
, precis som vilket annat fält som helst. Ett fält ref readonly
tillskrivs System.Runtime.CompilerServices.IsReadOnlyAttribute
. En readonly ref readonly
genereras med båda objekten.
Den här funktionen kräver körningsstöd och ändringar i ECMA-specifikationen. Därför aktiveras dessa endast när motsvarande funktionsflagga anges i corelib. Problemet med att spåra det exakta API:et spåras här https://github.com/dotnet/runtime/issues/64165
Den uppsättning ändringar av våra säkra kontextregler som krävs för att tillåta ref
fält är små och målinriktade. Reglerna tar redan hänsyn till att ref
-fält existerar och konsumeras via API:er. Ändringarna behöver bara fokusera på två aspekter: hur de skapas och hur de tilldelas om.
Först måste reglerna som fastställer referenssäker kontext värden för fält uppdateras för ref
fält på följande sätt:
Ett uttryck i formuläret
ref e.F
ref-safe-context på följande sätt:
- Om
F
är ettref
-fält är dess ref-säkra-kontextsäkra-kontext före
.- Annars om
e
är av en referenstyp, har den referenssäker kontext av uppringningskontext- Annars tas dess ref-safe-context från ref-safe-context av
e
.
Detta representerar dock inte en regeländring eftersom reglerna alltid har redovisat ref
tillstånd som ska finnas i en ref struct
. Detta är i själva verket hur ref
-tillståndet i Span<T>
alltid har fungerat, och förbrukningsreglerna beaktar detta korrekt. Ändringen här handlar bara om att utvecklare ska kunna komma åt ref
fält direkt och se till att de gör det enligt befintliga regler som implicit tillämpas på Span<T>
.
Detta innebär dock att ref
-fält kan returneras som ref
från en ref struct
, men normala fält kan inte returneras.
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;
}
Detta kan verka som ett fel vid första anblicken, men det här är en avsiktlig designpunkt. Även om detta inte är en ny regel som skapas av det här förslaget, erkänner det i stället de befintliga reglerna som Span<T>
har följt hittills, nu när utvecklare kan deklarera sina egna ref
-tillstånd.
Därefter måste reglerna för omtilldelning av referens justeras för förekomsten av ref
fält. Det primära scenariot för referenstilldelning är ref struct
konstruktorer som lagrar ref
parametrar i ref
fält. Stödet blir mer allmänt, men det här är kärnscenariot. För att stödja detta kommer reglerna för omtilldelning att justeras för att inkludera ref
-fält på följande sätt:
Referensregler för omtilldelning
Den vänstra operanden för operatorn = ref
måste vara ett uttryck som binder till en lokal referensvariabel, en referensparameter (förutom this
), en out-parameter, eller ett referensfält.
För en referenstilldelning i formuläret
e1 = ref e2
måste båda följande vara sanna:
e2
måste ha referenssäker kontext minst lika stor som referenssäker kontext före1
e1
måste ha samma safe-context some2
Note
Det innebär att den önskade Span<T>
konstruktorn fungerar utan någon extra anteckning:
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;
}
}
Ändringen av reglerna för återtilldelning innebär att ref
-parametrar nu kan undkomma från en metod som ett ref
-fält i ett ref struct
-värde. Som diskuterats i avsnittet kompatibilitetsöverväganden, kan detta ändra reglerna för befintliga API:er som aldrig var avsedda för ref
-parameter att läcka som ett ref
-fält. Livslängdsreglerna för parametrar baseras enbart på deras deklaration, inte på deras användning. Alla parametrar för ref
och in
har referenssäker kontext av anroparkontext och kan därför nu returneras av ref
eller ett ref
fält. För att stötta API:er som har ref
-parametrar som kan undkomma eller inte kan undkomma, och därmed återställa C# 10-anropsställetik, kommer språket att införa begränsade livstidsanteckningar.
scoped
modifierare
Nyckelordet scoped
används för att begränsa livslängden för ett värde. Det kan tillämpas på en ref
eller ett värde som är en ref struct
och har effekten att begränsa referenssäker kontext eller säker kontext livslängd till funktionsmedlem. Till exempel:
Parameter eller lokal | ref-safe-context | säker kontext |
---|---|---|
Span<int> s |
funktionsmedlem | anropskontext |
scoped Span<int> s |
funktionsmedlem | funktionsmedlem |
ref Span<int> s |
anropskontext | anropskontext |
scoped ref Span<int> s |
funktionsmedlem | anropskontext |
I den här relationen kan referenssäker kontext av ett värde aldrig vara bredare än den säker kontext.
Detta gör att API:er i C# 11 kan kommenteras så att de har samma regler som 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]);
}
Den scoped
kommentaren innebär också att parametern this
för en struct
nu kan definieras som scoped ref T
. Tidigare behövde detta hanteras som ett specialfall i reglerna som ref
parameter som hade andra ref-safe-context regler än de övriga ref
parametrarna (se alla referenser till hur mottagaren inkluderas eller exkluderas i reglerna för säker kontext). Nu kan det uttryckas som ett allmänt begrepp i de regler som ytterligare förenklar dem.
Den scoped
annoteringen kan också tillämpas på följande platser:
- locals(locals): Denna annotering anger livslängd till safe-context, eller referenssäker kontext i händelse av en
ref
lokal, för en funktionsmedlem oavsett livslängden för initieraren.
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];
}
Andra användningsområden för scoped
på lokalbefolkningen beskrivs nedan.
Den scoped
-anteckningen kan inte tillämpas på några andra platser, inklusive returvärden, fält, matriselement osv. Även om scoped
har inverkan när den tillämpas på alla ref
, in
eller out
, har den endast effekt när den tillämpas på värden som är ref struct
. Att ha deklarationer som scoped int
har ingen inverkan eftersom en icke-ref struct
alltid är säker att återvända. Kompilatorn skapar en diagnostik för sådana fall för att undvika utvecklarförvirring.
Ändra beteendet för out
parametrar
För att ytterligare begränsa påverkan av kompatibilitetsförändringen där ref
och in
parametrar görs returnerbara som ref
fält, kommer språket att ändra standardvärdet för ref-safe-context för out
parametrar till att vara funktionsmedlem. I praktiken är out
parametrar underförstått scoped out
framöver. Ur ett kompatibilitetsperspektiv innebär det att de inte kan returneras av ref
:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Detta ökar flexibiliteten för API:er som returnerar ref struct
värden och har out
parametrar eftersom den inte längre behöver överväga att parametern registreras med referens. Detta är viktigt eftersom det är ett vanligt mönster i API:er i läsarformat:
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);
}
Språket anser inte längre att argument som skickas till en out
parameter kan returneras. Att behandla indata till en out
parameter som returnerbar var mycket förvirrande för utvecklare. Det undergräver i huvudsak avsikten med out
genom att tvinga utvecklare att ta hänsyn till det värde som den som anropar skickar, vilket aldrig används utom i språk som inte respekterar out
. Framöver måste språk som stöder ref struct
se till att det ursprungliga värdet som skickas till en out
-parameter aldrig läsas.
C# uppnår detta via sina bestämda tilldelningsregler. Att uppnå våra regler för en referenssäker kontext samt tillåta befintlig kod som tilldelar och sedan returnerar värden i out
-parametrar.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Tillsammans innebär dessa ändringar att argumentet till en out
-parameter inte bidrar med safe-context- eller ref-safe-context-värden till metodanrop. Detta minskar avsevärt den övergripande kompatibilitetseffekten för ref
fält samt förenklar hur utvecklare tänker på out
. Ett argument till en out
parameter bidrar inte till returen, det är bara utdata.
Härled säker kontext av deklarationsuttryck
säker kontext för en deklarationsvariabel från ett out
argument (M(x, out var y)
) eller dekonstruering ((var x, var y) = M()
) är den snävaste av följande:
- uppringarens sammanhang
- om utvariabeln är markerad
scoped
, då är deklarationsblock (dvs. funktionsmedlem eller smalare). - om ut-variabelns typ är
ref struct
bör du överväga alla argument för det innehållande anropet, inklusive mottagaren:-
säker kontext av alla argument, där motsvarande parameter inte är
out
och har säker kontext av endast retur eller bredare - referenssäker kontext för alla argument där motsvarande parameter har referenssäker kontext av eller bredare
-
säker kontext av alla argument, där motsvarande parameter inte är
Se också exempel på härledd säker kontext i deklarationsuttryck.
Implicita scoped
parametrar
Totalt finns det två ref
platser som implicit deklareras som scoped
:
-
this
på enstruct
-instansmetod -
out
parametrar
Referenssäkerhetskontextreglerna skrivs i termer av scoped ref
och ref
. I referenssäker kontext motsvarar en in
parameter ref
och out
motsvarar scoped ref
. Både in
och out
kommer endast att framhävas när det är viktigt för regelns semantik. Annars betraktas de bara som ref
respektive scoped ref
.
När du diskuterar ref-safe-context, i argument som motsvarar in
parametrar, generaliseras de som ref
argument i specifikationen. Om argumentet är en lvalue, är ref-safe-context lvaluen, annars är det function-member. Återigen kommer in
bara att anropas här när det är viktigt för den aktuella regelns semantik.
Säkerhetskontext endast för retur
Designen kräver dessutom att en ny säker kontext införs: återfinns endast. Detta liknar anroparkontext eftersom det kan returneras, men det kan bara returneras via en return
-instruktion.
Informationen om returendast är att det är en kontext som är större än funktionselement men mindre än anroparekontext. Ett uttryck som tillhandahålls till en return
-instruktion måste vara minst endast returnera. Som sådan blir de flesta av de befintliga reglerna irrelevanta. Tilldelning till en ref
-parameter från ett uttryck med en säker kontext av returneringsbegränsad misslyckas eftersom den är mindre än ref
parameterns säker kontext som är anroparkontext. Behovet av den här nya flyktkontexten diskuteras nedan.
Det finns tre platser där standarden är för endast retur:
- En
ref
- ellerin
-parameter har en referenssäker kontext. Detta görs delvis förref struct
för att förhindra problem med fåniga cykliska tilldelningar. Det görs dock enhetligt för att förenkla modellen samt minimera kompatibilitetsändringar. - En
out
-parameter för enref struct
kommer att ha säker kontext av endast för retur. Detta gör att retur ochout
kan vara lika uttrycksfulla. Detta lider inte av det fåniga cykliska tilldelningsproblemet eftersomout
implicit ärscoped
, vilket innebär att ref-safe-context fortfarande är mindre än safe-context. - En
this
parameter för enstruct
konstruktor har en säker kontext. Detta faller ut på grund av att modelleras somout
parametrar.
Alla uttryck eller uttryck som uttryckligen returnerar ett värde från en metod eller lambda måste ha en safe-context, och om tillämpligt en ref-safe-context, med minst return-only. Det inkluderar return
-instruktioner, medlemmar med uttryckskropp och lambda-uttryck.
På samma sätt måste alla tilldelningar till en out
ha en säker kontext som är åtminstone return-only. Detta är dock inte ett specialfall, detta följer bara från de befintliga tilldelningsreglerna.
Obs! Ett uttryck vars typ inte är en ref struct
typ har alltid en säker kontextanroparkontext.
Regler för metodanrop
Referenssäkerhetskontextreglerna för metodanrop uppdateras på flera sätt. Den första är genom att känna igen den inverkan som scoped
har på argument. För ett visst argument expr
som skickas till parametern p
:
- Om
p
ärscoped ref
bidrarexpr
inte referenssäker kontext när du överväger argument.- Om
p
ärscoped
så bidrarexpr
inte till en säker kontext när du överväger argument.- Om
p
ärout
bidrarexpr
inte till referenssäkerhetskontext eller säkerhetskontextfler detaljer
Språket "bidrar inte" innebär att argumenten helt enkelt inte beaktas vid beräkning av ref-safe-context eller safe-context-värdet för metodens returvärde. Det beror på att värdena inte kan bidra till den livslängden eftersom scoped
-annotationen förhindrar detta.
Metodanropsreglerna kan nu förenklas. Mottagaren behöver inte längre behandlas som ett speciellt fall, i fallet med struct
är det nu bara en scoped ref T
. Värdereglerna behöver ändras för att ta hänsyn till ref
fältretur:
Ett värde som härrör från metodanropet
e1.M(e2, ...)
, därM()
inte returnerar ref-to-ref-struct, har en säkerhetskontext hämtad från den snävaste av följande:
- samtalskontext
- När returen är en
ref struct
safe-context som har bidragit med alla argumentuttryck- När returvärdet är en
ref struct
så är ref-safe-context bidragit av allaref
argumentOm
M()
returnerar ref-to-ref-struct är safe-context densamma som safe-context för alla argument som är ref-to-ref-struct. Det är ett fel om det finns flera argument med olika safe-context på grund av metodargument måste matcha.
Du kan förenkla ref
anropsregler till:
Ett värde som härrör från en metodanrop
ref e1.M(e2, ...)
, därM()
inte returnerar ref-to-ref-struct, är ref-safe-context den smalaste av följande kontexter:
- samtalskontext
- Det safe-context som alla argumentuttryck bidrar till
- referenssäker kontext bidragit med alla
ref
argumentOm
M()
returnerar ref-to-ref-struct är ref-safe-context den smalaste ref-safe-context som alla argument som är ref-to-ref-struct bidrar med.
Med den här regeln kan vi nu definiera de två varianterna av önskade metoder:
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);
}
Regler för objektinitierare
Det safe-context- för ett objektinitieringsuttryck är smalast av:
- för konstruktoranropet.
- safe-context och ref-safe-context argument till medlemsinitieringsindexerare som kan komma undan till mottagaren.
- safe-context av RHS för tilldelningar i medlemsinitialiserare till icke-readonly inställningar eller ref-safe-context vid referenstilldelning.
Ett annat sätt att modellera detta är att tänka på alla argument till en medlemsinitierare som kan tilldelas mottagaren som ett argument till konstruktorn. Det beror på att medlemsinitieraren i praktiken är ett konstruktoranrop.
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);
Den här modelleringen är viktig eftersom den visar att vår MAMM- måste ta särskilt hänsyn till medlemsinitierare. Tänk på att det här specifika fallet måste vara olagligt eftersom det tillåter att ett värde med en smalare säker kontext tilldelas till ett högre.
Metodargumenten måste överensstämma
Förekomsten av ref
fält innebär att reglerna kring metodargument måste matcha måste uppdateras eftersom en ref
parameter nu kan lagras som ett fält i ett ref struct
argument till metoden. Tidigare behövde regeln bara ta hänsyn till att lagra en annan ref struct
som ett fält. Effekten av detta diskuteras i kompatibilitetsövervägandena. Den nya regeln är ...
För alla metodanrop
e.M(a1, a2, ... aN)
- Beräkna den smalaste säkra -kontexten från:
- anropskontext
- den säkra kontexten för alla argument
- referenssäker kontext för alla referensargument vars motsvarande parametrar har en referenssäker kontext av anroparkontext
- Alla
ref
argument av typernaref struct
måste kunna tilldelas ett värde i den -säkra kontexten. Det här är ett fall därref
inte generalisera för att inkluderain
ochout
För alla metodanrop
e.M(a1, a2, ... aN)
- Beräkna den smalaste säkra -kontexten från:
- anropskontext
- den säkra kontexten för alla argument
- referenssäker kontext för alla referensargument vars motsvarande parametrar inte är
scoped
- Alla
out
argument av typernaref struct
måste kunna tilldelas ett värde i den -säkra kontexten.
Förekomsten av scoped
gör det möjligt för utvecklare att minska friktionen som den här regeln skapar genom att markera parametrar som inte returneras som scoped
. Detta tar bort deras argument från (1) i båda fallen ovan och ger större flexibilitet för anropare.
Effekten av den här ändringen beskrivs djupare nedan. Sammantaget gör detta att utvecklare kan göra anropsplatser mer flexibla genom att ange icke-eskaperande, referensliknande värden med scoped
.
Parameteromfångsavvikelse
Attributet scoped
och [UnscopedRef]
-modifieraren (se nedan) på parametrar påverkar även åsidosättandet av objekt, implementering av gränssnitt och delegate
-konverteringsregler. Signaturen för en åsidosättning, gränssnittsimplementering eller delegate
-konvertering kan:
- Lägg till
scoped
i enref
- ellerin
-parameter - Lägga till
scoped
i enref struct
-parameter - Ta bort
[UnscopedRef]
från enout
-parameter - Ta bort
[UnscopedRef]
från enref
parameter avref struct
typ
Alla andra skillnader när det gäller scoped
eller [UnscopedRef]
anses vara en matchningsfel.
Kompilatorn rapporterar ett diagnostiskt meddelande för osäkra omfångsskillnader vid åsidosättningar, gränssnittsimplementeringar och delegeringskonverteringar.
- Metoden har en
ref
- ellerout
-parameter avref struct
typ med ett matchningsfel för att lägga till[UnscopedRef]
(tar inte bortscoped
). (I det här fallet är en fånig cyklisk tilldelning möjlig, därför behövs inga andra parametrar.) - Eller båda dessa är sanna:
- Metoden returnerar en
ref struct
eller returnerar enref
ellerref readonly
, eller så har metoden enref
ellerout
parameter avref struct
typ. - Metoden har minst en ytterligare
ref
,in
ellerout
parameter eller en parameter avref struct
typ.
- Metoden returnerar en
Diagnostiken rapporteras inte i andra fall eftersom:
- Metoder med sådana signaturer kan inte fånga referenserna som skickas in, så eventuella omfångsfel är inte farliga.
- Dessa omfattar mycket vanliga och enkla scenarier (t.ex. vanliga gamla
out
-parametrar som används iTryParse
-metodsignaturer) och att rapportera matchningar inom scope bara för att de används i språkversion 11 (och därmed harout
-parametern olika scope) skulle vara förvirrande.
Diagnostiken rapporteras som ett fel om både de felmatchade signaturerna använder C#11 referenssäkra kontextregler. Annars är diagnostiken en varning.
Den begränsade felmatchningsvarningen kan rapporteras på en modul som kompilerats med C#7.2 ref säkra kontextregler där scoped
inte är tillgängligt. I vissa sådana fall kan det vara nödvändigt att ignorera varningen om den andra felaktiga signaturen inte kan ändras.
Attributet scoped
modifier och [UnscopedRef]
har också följande effekter på metodsignaturer:
- Modifiern
scoped
och attributet[UnscopedRef]
påverkar inte döljande - Överbelastningar kan inte bara skilja sig åt på
scoped
eller[UnscopedRef]
Avsnittet om ref
-fältet och scoped
är långt, och jag ville avsluta med en kort sammanfattning av de föreslagna ändringarna med brytande karaktär.
- Ett värde som har referenssäker kontext från till anroparkontext kan returneras av fälten
ref
ellerref
. - En
out
parameter skulle ha en säker kontext av funktionsmedlem.
Detaljerade anteckningar:
- Ett
ref
fält kan bara deklareras i enref struct
- Ett
ref
fält kan inte deklarerasstatic
,volatile
ellerconst
- Ett
ref
fält får inte ha en typ som ärref struct
- Referenssammansättningens genereringsprocess måste bevara förekomsten av ett
ref
-fält i enref struct
-enhet - En
readonly ref struct
måste deklarera sinaref
fält somreadonly ref
- För by-ref-värden måste
scoped
-modifieraren visas förein
,out
ellerref
- Dokumentet om säkerhetsregler för spännvidd kommer att uppdateras enligt beskrivningen i det här dokumentet.
- De nya reglerna för referenssäkra kontexter kommer att gälla när antingen
- Kärnbiblioteket innehåller funktionsflaggan som anger stöd för
ref
fält - Värdet
langversion
är 11 eller högre
- Kärnbiblioteket innehåller funktionsflaggan som anger stöd för
Syntax
13.6.2 Lokala variabeldeklarationer: har lagts till 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 for
-instruktionen: lade till 'scoped'?
indirekt från local_variable_declaration
.
13.9.5 Uttalandet foreach
: har lagts till 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Argumentlistor: lade till 'scoped'?
för out
deklarationsvariabel.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
[TBD]
15.6.2 Metodparametrar: lade till 'scoped'?
i 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 Delegatdeklarationer: läggs till 'scoped'?
indirektfixed_parameter
.
12.19 Anonyma funktionsuttryck: har lagts till 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Begränsade typer av solnedgångar
Kompilatorn har ett koncept för en uppsättning "begränsade typer" som till stor del är odokumenterade. Dessa typer fick en särskild status eftersom det i C# 1.0 inte fanns något allmänt sätt att uttrycka sitt beteende. Framför allt faktumet att typerna kan innehålla referenser till exekveringsstacken. Istället hade kompilatorn särskild kunskap om dem och begränsade deras användning på sätt som alltid är säkra: tillät inte återvändande, kan inte användas som matriselement, kan inte användas i generiska typer, etc.
När ref
fält är tillgängliga och utökade för att stödja ref struct
kan dessa typer definieras korrekt i C# med hjälp av en kombination av ref struct
och ref
fält. Därför, när kompilatorn upptäcker att en körtid stöder ref
-fält, kommer den inte längre att ha någon uppfattning om begränsade typer. Den använder i stället typerna som de definieras i koden.
För att stödja detta kommer våra referenssäkra kontextregler att uppdateras på följande sätt:
-
__makeref
behandlas som en metod med signaturenstatic TypedReference __makeref<T>(ref T value)
-
__refvalue
behandlas som en metod med signaturenstatic ref T __refvalue<T>(TypedReference tr)
. Uttrycket__refvalue(tr, int)
använder effektivt det andra argumentet som typparameter. -
__arglist
som parameter har en ref-safe-context och safe-context för funktionsmedlem. -
__arglist(...)
som uttryck kommer att ha en referenssäker kontext och en säker kontext för funktionsmedlem .
För att säkerställa att körningarna överensstämmer ser du till att TypedReference
, RuntimeArgumentHandle
och ArgIterator
definieras som ref struct
. Ytterligare TypedReference
måste anses ha ett ref
fält till en ref struct
för alla möjliga typer (det kan lagra valfritt värde). I kombination med ovanstående regler kommer att säkerställa att referenser till stacken inte går utanför deras livslängd.
Obs! Strikt sett är detta en implementeringsinformation för kompilatorn jämfört med en del av språket. Men med tanke på relationen med ref
fält tas det med i språkförslaget för enkelhetens skull.
Ange utan begränsning
En av de mest anmärkningsvärda friktionspunkterna är oförmågan att returnera fält av ref
i instansmedlemmar i en struct
. Det innebär att utvecklare inte kan skapa ref
returnerande metoder och egenskaper och måste istället exponera fält direkt. Detta minskar nyttan med ref
i struct
, där det ofta är som mest önskvärt.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
Den motiveringen för den här standardinställningen är rimlig, men det är inget fel med en struct
som flyr this
som referens, det är helt enkelt den standard som väljs av referenssäkerhetskontextreglerna.
För att åtgärda detta kommer språket att ge motsatsen till scoped
livstidsannotering genom att stödja en UnscopedRefAttribute
. Detta kan tillämpas på alla ref
och det kommer att ändra referenssäkerhetskontext till att vara en nivå bredare än standardinställningen. Till exempel:
UnscopedRef tillämpas på | Ursprunglig referenssäker kontext | Ny referenssäker kontext |
---|---|---|
instansmedlem | funktionsmedlem | endast retur |
in
/
ref parameter |
endast retur | uppringarens sammanhang |
out -parametern |
funktionsmedlem | endast retur |
När [UnscopedRef]
tillämpas på en instansmetod för en struct
påverkas det av att den implicita this
parametern ändras. Det innebär att this
fungerar som en ej kommenterad ref
av samma typ.
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;
}
Anteckningen kan också placeras på out
parametrar för att återställa dem till C# 10-beteende.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
För ändamålet med säkra kontextregler anses en [UnscopedRef] out
helt enkelt vara en ref
. Liknar hur in
anses vara ref
för livslängdsändamål.
Annoteringen [UnscopedRef]
kommer inte att tillåtas på init
medlemmar och konstruktorer i struct
. Dessa medlemmar är redan speciella när det gäller ref
semantik eftersom de anser att readonly
medlemmar är föränderliga. När ref
tas till dessa medlemmar, visas det som en enkel ref
och inte ref readonly
. Detta tillåts inom gränserna för konstruktorer och init
. Att tillåta [UnscopedRef]
skulle tillåta en sådan ref
att felaktigt fly utanför konstruktorn och tillåta mutation efter att readonly
semantik hade ägt rum.
Attributtypen har följande definition:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Detaljerade anteckningar:
- En instansmetod eller egenskap som har kommenterats med
[UnscopedRef]
har referenssäker kontext förthis
inställd på anroparkontext. - En medlem som har kommenterats med
[UnscopedRef]
kan inte implementera ett gränssnitt. - Det är ett fel att använda
[UnscopedRef]
på- En medlem som inte är angiven på en
struct
- En
static
medlem,init
medlem eller konstruktor på enstruct
- Parametern är markerad
scoped
- En parameter som passerades som värde
- En parameter som skickas via referens som inte är implicit ändringsbegränsad
- En medlem som inte är angiven på en
ScopedRefAttribute
De scoped
anteckningarna skickas till metadata via attributet type System.Runtime.CompilerServices.ScopedRefAttribute
. Attributet matchas med namnområdeskvalificerat namn, så definitionen behöver inte visas i någon specifik sammansättning.
Den ScopedRefAttribute
typen är endast för kompilatoranvändning – den är inte tillåten i källan. Typdeklarationen syntetiseras av kompilatorn om den inte redan ingår i kompilatorn.
Typen har följande definition:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Kompilatorn genererar det här attributet på parametern med scoped
syntax. Detta genereras endast när syntaxen gör att värdet skiljer sig från standardtillståndet. Till exempel orsakar scoped out
att inget attribut genereras.
RefSafetyRulesAttribute
Det finns flera skillnader i referenssäker kontext regler mellan C#7.2 och C#11. Någon av dessa skillnader kan leda till icke-bakåtkompatibla ändringar vid omkompilering med C#11 mot referenser som kompilerats med C#10 eller tidigare.
- unscoped
ref
/in
/out
parametrar kan undkomma ett metodanrop som ettref
fält i enref struct
i C#11, inte i C#7.2 -
out
parametrar är implicit avgränsade i C#11 och utan avgränsning i C#7.2 -
ref
/in
parametrar förref struct
typer är implicit begränsade inom skopet i C#11 och ospecificerade i C#7.2
För att minska risken för icke-bakåtkompatibla ändringar vid omkompilering med C#11 uppdaterar vi C#11-kompilatorn för att använda referenssäkra kontextregler för metodanrop som matchar de regler som användes för att analysera metoddeklarationen. När du analyserar ett anrop till en metod som kompilerats med en äldre kompilator, använder C#11-kompilatorn i princip säkerhetskontextreglerna för C#7.2.
För att aktivera detta genererar kompilatorn ett nytt [module: RefSafetyRules(11)]
-attribut när modulen kompileras med -langversion:11
eller högre eller kompileras med en corlib som innehåller funktionsflaggan för ref
fält.
Argumentet till attributet anger språkversionen av referenssäker kontext regler som användes när modulen kompilerades.
Versionen är för närvarande fast på 11
oavsett den faktiska språkversion som skickas till kompilatorn.
Förväntningen är att framtida versioner av kompilatorn uppdaterar referenssäkra kontextregler och genererar attribut med distinkta versioner.
Om kompilatorn läser in en modul som innehåller en [module: RefSafetyRules(version)]
med en annan version
än 11
rapporterar kompilatorn en varning för den okända versionen om det finns några anrop till metoder som deklarerats i modulen.
När C#11-kompilatorn analyserar ett metodanrop:
- Om modulen som innehåller metoddeklarationen innehåller
[module: RefSafetyRules(version)]
, oavsettversion
, analyseras metodanropet med C#11-regler. - Om modulen som innehåller metoddeklarationen kommer från källan och kompileras med
-langversion:11
eller med en corlib som innehåller funktionsflaggan förref
fält analyseras metodanropet med C#11-regler. - Om modulen som innehåller metoddeklarationen refererar till
System.Runtime { ver: 7.0 }
analyseras metodanropet med C#11-regler. Den här regeln är en tillfällig åtgärd för moduler som kompilerats med tidigare förhandsversioner av C#11/.NET 7 och tas bort senare. - Annars analyseras metodanropet med C#7.2-regler.
En pre-C#11-kompilator ignorerar alla RefSafetyRulesAttribute
och analyserar endast metodanrop med C#7.2-regler.
RefSafetyRulesAttribute
matchas med namnområdeskvalificerat namn, så definitionen behöver inte visas i någon specifik sammansättning.
Den RefSafetyRulesAttribute
typen är endast för kompilatoranvändning – den är inte tillåten i källan. Typdeklarationen syntetiseras av kompilatorn om den inte redan ingår i kompilatorn.
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;
}
}
Säkra buffertar med fast storlek
Säkra buffertar med fast storlek levererades inte i C# 11. Den här funktionen kan implementeras i en framtida version av C#.
Språket minskar begränsningarna för matriser med fast storlek så att de kan deklareras i säker kod och elementtypen kan hanteras eller ohanteras. Detta gör typer som följande juridiska:
internal struct CharBuffer
{
internal char Data[128];
}
Dessa deklarationer, ungefär som deras unsafe
motsvarigheter, definierar en sekvens med N
element i den innehållande typen. Dessa medlemmar kan nås med en indexerare och kan också konverteras till Span<T>
och ReadOnlySpan<T>
instanser.
När du indexerar till en fixed
buffert av typen T
måste containerns readonly
tillstånd beaktas. Om containern är readonly
returnerar indexeraren ref readonly T
annars returneras ref T
.
Åtkomst till en fixed
buffert utan en indexerare har ingen naturlig typ, men den kan konverteras till Span<T>
typer. Om containern är readonly
kan bufferten implicit konverteras till ReadOnlySpan<T>
, annars kan den implicit konverteras till Span<T>
eller ReadOnlySpan<T>
(konverteringen Span<T>
anses bättre).
Den resulterande Span<T>
-instansen har en längd som är lika med den storlek som deklareras i fixed
bufferten.
safe-context för det returnerade värdet är lika med safe-context för containern, precis som om den underliggande datan skulle användas som ett fält.
För varje fixed
deklaration i en typ där elementtypen är T
genererar språket en motsvarande get
endast indexeringsmetod vars returtyp är ref T
. Indexeraren kommenteras med attributet [UnscopedRef]
eftersom implementeringen returnerar fält av deklareringstypen. Tillgängligheten för medlemmen matchar tillgängligheten i fältet fixed
.
Till exempel är indexerarens signatur för CharBuffer.Data
följande:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Om det angivna indexet ligger utanför de deklarerade gränserna för den fixed
matrisen genereras en IndexOutOfRangeException
. Om ett konstant värde anges ersätts det med en direkt referens till lämpligt element. Om inte konstanten ligger utanför de deklarerade gränserna skulle ett kompileringstidsfel inträffa.
Det kommer också att genereras en namngiven tillgångsmetod för varje fixed
buffert som tillhandahåller värdebaserade get
och set
operationer. Det innebär att fixed
-buffertar kommer att mer likna befintliga matrissemantiker genom att ha en ref
-accessor samt byval get
- och set
-operationer. Det innebär att kompilatorer har samma flexibilitet när de genererar kod som förbrukar fixed
buffertar som när de använder matriser. Detta bör göra operationer som await
över fixed
buffertar enkelt att generera.
Detta har också den extra fördelen att det gör fixed
buffertar enklare att använda från andra språk. Namngivna indexerare är en funktion som har funnits sedan 1.0-versionen av .NET. Även språk som inte direkt kan generera en namngiven indexerare kan i allmänhet använda dem (C# är faktiskt ett bra exempel på detta).
Den tillhörande lagringen för bufferten genereras med hjälp av attributet [InlineArray]
. Det här är en mekanism som beskrivs i ärende 12320 som specifikt möjliggör att en sekvens av fält av samma typ deklareras effektivt. Den här frågan är fortfarande under aktiv diskussion och förväntningen är att implementeringen av den här funktionen kommer att ske beroende på hur den diskussionen utspelar sig.
Initierare med värden på ref
i uttryck new
och with
I avsnittet 12.8.17.3 Object initializersuppdaterar vi grammatiken till:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
I avsnittet för with
uttryckuppdaterar vi grammatiken till:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
Den vänstra operanden i tilldelningen måste vara ett uttryck som binder till ett referensfält.
Den högra operanden måste vara ett uttryck som ger en lvalue som anger ett värde av samma typ som den vänstra operanden.
Vi lägger till en liknande regel i lokal referenstilldelning:
Om den vänstra operanden är en skrivbar referens (dvs. den anger något annat än ett ref readonly
fält), måste den högra operanden vara en skrivbar lvalue.
Undantagsreglerna för konstruktoranrop kvarstår:
Ett
new
uttryck som anropar en konstruktor följer samma regler som ett metodanrop som anses returnera den typ som skapas.
Det vill säga reglerna för metodanrop uppdaterade ovan:
Ett rvalue som härrör från ett metodanrop
e1.M(e2, ...)
har säker kontext från den minsta av följande kontexter:
- samtalskontext
- Det safe-context som alla argumentuttryck bidrar till
- När returen är
ref struct
och "referenssäker kontext" som allaref
argument har bidragit till
För ett new
uttryck med initierare räknas initieraruttrycken som argument (de bidrar med sina safe-context) och ref
initializer-uttryck räknas som ref
argument (de bidrar med sina ref-safe-context), rekursivt.
Ändringar i osäker kontext
Pekartyper (avsnitt 23.3) utökas för att tillåta hanterade typer som referenstyp.
Sådana pekartyper skrivs som en hanterad typ följt av en *
token. De skapar en varning.
Adressoperatorn (avsnitt 23.6.5) utökas för att acceptera en variabel med en hanterad typ som operand.
Instruktionen fixed
(avsnitt 23.7) görs flexibel för att tillåta fixed_pointer_initializer som är adressen till en variabel av hanterad typ T
eller som är ett uttryck av en array_type med element av en hanterad typ T
.
Stackallokeringsinitieraren (avsnitt 12.8.22) är lika avslappnad.
Överväganden
Det finns överväganden som andra delar av utvecklingsstacken bör tänka på när du utvärderar den här funktionen.
Överväganden för kompatibilitet
Utmaningen i detta förslag är konsekvenserna för kompatibiliteten som denna design medför för våra befintliga säkerhetsreglernas tillämpningsområde, eller §9.7.2. Även om dessa regler helt stöder konceptet att ref struct
har ref
fält, tillåter de inte att API:er, annat än stackalloc
, samlar in tillstånd för ref
som hänför sig till stacken. Reglerna för en säker referenskontext har ett hårt antagande, eller §16.4.12.8 att en konstruktor av typen Span(ref T value)
inte finns. Det innebär att säkerhetsreglerna inte tar hänsyn till att en ref
parameter kan komma undan som ett ref
fält, vilket innebär att den tillåter kod som följande.
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);
}
Det finns i själva verket tre sätt för en ref
parameter att fly från ett metodanrop:
- Efter värderetur
- Retur senast
ref
- Genom
ref
fält iref struct
som returneras eller passeras somref
/out
parameter
De befintliga reglerna står endast för (1) och (2). De tar inte hänsyn till (3), och på grund av detta redovisas inte luckor, såsom att återkommande lokala värden kommer tillbaka som ref
-fält. Den här designen måste ändra reglerna för att ta hänsyn till (3). Detta har en liten inverkan på kompatibiliteten för befintliga API:er. Mer specifikt påverkar det API:er som har följande egenskaper.
- Ha en
ref struct
i signaturen- Om
ref struct
är en returtyp,ref
ellerout
parameter - Har ytterligare en
in
- ellerref
parameter förutom mottagaren
- Om
I C# 10 behövde anropare av sådana API:er aldrig överväga att ref
tillståndsindata till API:et kunde samlas in som ett ref
fält. Det gjorde det möjligt för flera mönster att existera, säkert i C# 10, som kommer att vara osäkra i C# 11 på grund av möjligheten att ref
-tillståndet kan läcka som ett ref
-fält. Till exempel:
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]);
}
Effekten av den här kompatibilitetsbrytningen förväntas vara mycket liten. Den påverkade API-formen var föga meningsfull i avsaknad av ref
fält, därför är det osannolikt att kunderna skapade många av dessa. Experiment som använder verktyg för att identifiera denna API-struktur i befintliga lagringsplatser stöder den försäkran. Den enda lagringsplatsen med betydande antal av den här formen är dotnet/runtime och det beror på att lagringsplatsen kan skapa ref
fält via den ByReference<T>
inbyggda typen.
Trots detta måste designen ta hänsyn till sådana API:er som finns eftersom den uttrycker ett giltigt mönster, men inte ett vanligt. Därför måste designen ge utvecklare verktygen för att återställa befintliga livslängdsregler vid uppgradering till C# 10. Mer specifikt måste det tillhandahålla mekanismer som gör det möjligt för utvecklare att märka ref
-parametrar som inte kan undkomma via ref
eller ref
-fält. Det gör att kunder kan definiera API:er i C# 11 som har samma C# 10-anropswebbplatsregler.
Referenssammansättningar
En referenssammansättning för en kompilering med funktioner som beskrivs i det här förslaget måste underhålla de element som förmedlar referenssäker kontextinformation. Det innebär att alla livstidsanteckningsattribut måste bevaras i sin ursprungliga position. Alla försök att ersätta eller utelämna dem kan leda till ogiltiga referenssammansättningar.
Det är mer nyanserat att representera ref
-fältet. Helst skulle ett ref
fält visas i en referenssammansättning, precis som andra fält. Ett ref
-fält representerar dock en ändring av metadataformatet och som kan orsaka problem med verktygskedjor som inte uppdateras för att förstå den här metadataändringen. Ett konkret exempel är C++/CLI som sannolikt kommer att fela om det använder ett ref
fält. Därför är det fördelaktigt om ref
fält kan utelämnas från referenssammansättningar i våra kärnbibliotek.
Ett ref
-fält i sig har ingen inverkan på referenssäkra kontextregler. Ett konkret exempel är att det inte påverkar förbrukningen att ändra den befintliga Span<T>
definitionen för att använda ett ref
fält. Därför kan själva ref
utelämnas på ett säkert sätt. Ett ref
fält har dock andra effekter på förbrukningen som måste bevaras:
- Ett
ref struct
som har ettref
fält anses aldrig varaunmanaged
- Typen av
ref
-fältet påverkar oändliga allmänna expansionsregler. Om typen av ettref
fält innehåller en typparameter som måste bevaras
Med dessa regler i åtanke, här är en giltig referensmonteringstransformation för en 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
}
Anteckningar
Livslängder uttrycks mest naturligt med hjälp av typer. Livslängderna i ett givet program är säkra när livslängdstyperna klarar typkontrollen. Även om syntaxen för C# implicit lägger till livslängder till värden, finns det ett underliggande typsystem som beskriver de grundläggande reglerna här. Det är ofta lättare att diskutera konsekvenserna av ändringar i designen när det gäller dessa regler så att de tas med här för diskussions skull.
Observera att detta inte är avsett att vara en 100% fullständig dokumentation. Att dokumentera varje enskilt beteende är inte ett mål här. I stället är det tänkt att upprätta en allmän förståelse och ett gemensamt verbiage som modellen, och potentiella ändringar av den, kan diskuteras med.
Vanligtvis är det inte nödvändigt att direkt prata om livslängdstyper. Undantagen är platser där livslängden kan variera beroende på specifika instansieringsplatser. Detta är ett slags polymorfism och vi kallar dessa varierande livslängder för "generiska livslängder", som representeras som generiska parametrar. C# tillhandahåller inte syntax för att uttrycka generiska livstidsparametrar, så vi definierar en implicit "översättning" från C# till ett utökat och förenklat språk som innehåller explicita generiska parametrar.
I exemplen nedan används namngivna livslängder. Syntaxen $a
refererar till en livslängd med namnet a
. Det är en livslängd som inte har någon betydelse av sig själv men som kan ges en relation till andra livslängder via where $a : $b
syntax. Detta fastställer att $a
är konvertibel till $b
. Det kan hjälpa att tänka på detta som att etablera att $a
har en livslängd åtminstone så lång som $b
.
Det finns några fördefinierade livslängder för bekvämlighet och korthet nedan:
-
$heap
: det här är livslängden för vilket värde som helst som finns på heap. Den är tillgänglig i alla kontexter och metodsignaturer. -
$local
: det här är livslängden för alla värden som finns i metodstacken. Det är i själva verket en platshållare för namnet på funktionens medlem . Den definieras implicit i metoder och kan visas i metodsignaturer förutom för alla utdatapositioner. -
$ro
: namnplatshållare för returnerar endast -
$cm
: namnplatshållare för uppringarkontext
Det finns några fördefinierade relationer mellan livslängder:
-
where $heap : $a
för alla livslängder$a
where $cm : $ro
-
where $x : $local
för alla fördefinierade livslängder. Användardefinierade livslängder har ingen relation till lokal om inte uttryckligen definierats.
Livslängdsvariabler när de definieras för typer kan vara invarianta eller covarianta. Dessa uttrycks med samma syntax som generiska parametrar:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Livslängdsparametern $this
för typdefinitioner är inte fördefinierad, men den har några regler som är associerade med den när den definieras:
- Det måste vara den första livslängdsparametern.
- Den måste vara covariant:
out $this
. - Livslängden för
ref
fält måste konverteras till$this
- Den
$this
livslängden för alla icke-referensfält måste vara$heap
eller$this
.
En referens livslängd uttrycks genom att ett livstidsargument ges till referensen. Till exempel uttrycks en ref
som refererar till högen som ref<$heap>
.
När du definierar en konstruktor i modellen används namnet new
för metoden. Det är nödvändigt att ha en parameterlista för det returnerade värdet samt konstruktorargumenten. Detta är nödvändigt för att uttrycka relationen mellan konstruktorindata och det konstruerade värdet. I stället för att ha Span<$a><$ro>
använder modellen Span<$a> new<$ro>
i stället. Typen av this
i konstruktorn, tillsammans med livslängder, kommer att vara det definierade returvärdet.
De grundläggande reglerna för livslängden definieras som:
- Alla livstider uttrycks syntaktiskt som generiska argument, som kommer före typargument. Detta gäller för fördefinierade livslängder förutom
$heap
och$local
. - Alla typer
T
som inte är enref struct
har implicit livslängden förT<$heap>
. Detta är implicit, det finns inget behov av att skrivaint<$heap>
i varje exempel. - För ett
ref
fält som definierats somref<$l0> T<$l1, $l2, ... $ln>
:- Alla livslängder
$l1
genom$ln
måste vara invarianta. - Livslängden för
$l0
måste konverteras till$this
- Alla livslängder
- För en
ref
som definieras somref<$a> T<$b, ...>
måste$b
konverteras till$a
-
ref
för en variabel har en livslängd som definieras av:- För en
ref
lokal, parameter, fält eller retur av typenref<$a> T
livslängden är$a
-
$heap
för alla referenstyper och fält för referenstyper -
$local
för allt annat
- För en
- En tilldelning eller retur är laglig när den underliggande typkonverteringen är laglig
- Livslängder för uttryck kan göras tydliga med hjälp av cast-annoteringar.
-
(T<$a> expr)
värdets livslängd uttryckligen$a
förT<...>
-
ref<$a> (T<$b>)expr
värdets livslängd är$b
förT<...>
och referenslivslängden är$a
.
-
I fråga om livslängdsregler anses en ref
vara en del av uttryckets typ för konverteringsändamål. Det representeras logiskt genom att konvertera ref<$a> T<...>
till ref<$a, T<...>>
där $a
är covariant och T
är invariant.
Nu ska vi definiera de regler som gör att vi kan mappa C#-syntaxen till den underliggande modellen.
För korthets skull är en typ som inte har några explicita livslängdsparametrar behandlas som om det finns en definierad out $this
som tillämpas på alla fält av typen. En typ med ett ref
fält måste definiera explicita livslängdsparametrar.
Dessa regler finns för att stödja vår befintliga invariant som T
kan tilldelas till scoped T
för alla typer. Det mappar ned till T<$a, ...>
som kan tilldelas till T<$local, ...>
för alla livslängder som är kända för att vara konvertibla till $local
. Dessutom stöder detta andra objekt som att kunna tilldela Span<T>
från heapen till dem på stacken. Detta exkluderar typer där fält har olika livslängder för icke-ref-värden, men det är verkligheten för C# idag. Ändringar som skulle kräva en betydande ändring av C#-regler som skulle behöva mappas ut.
Typen av this
för en typ S<out $this, ...>
i en instansmetod definieras implicit som följande:
- För normal instansmetod:
ref<$local> S<$cm, ...>
- Till exempel en metod som har kommenterats med
[UnscopedRef]
:ref<$ro> S<$cm, ...>
Avsaknaden av en explicit this
parameter tvingar fram implicita regler här. För komplexa exempel och diskussioner bör du överväga att skriva som en static
metod och göra this
till en explicit parameter.
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) { }
}
C#-metodens syntax mappar till modellen på följande sätt:
-
ref
parametrar har en referenslivslängd på$ro
- parametrar av typen
ref struct
har denna livslängd på$cm
- referensreturer har en referenslivslängd på
$ro
- Resultat av typen
ref struct
har en livstid av värdet$ro
-
scoped
på en parameter ellerref
ändrar referenslivslängden till$local
Med tanke på att vi ska utforska ett enkelt exempel som visar modellen här:
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;
}
Nu ska vi utforska samma exempel med hjälp av en 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;
}
Nu ska vi se hur detta hjälper till med problemet med cyklisk självtilldelning:
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;
}
}
Nu ska vi se hur detta hjälper till med problemet med den fåniga insamlingsparametern:
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;
}
}
Öppna problem
Ändra designen för att undvika kompatibilitetsbrytningar
Den här designen föreslår flera kompatibilitetsbrytningar med våra befintliga regler för referenssäker kontext. Även om ändringarna tros ha minimal påverkan beaktades en design som inte hade några kritiska ändringar.
Den kompatibilitetsbevarande designen var dock betydligt mer komplex än den här. För att upprätthålla kompatibilitet måste ref
-fält ha olika livslängder för att kunna returneras via ref
- respektive ref
-fältet. I stort sett kräver det att vi tillhandahåller referensfältsäker kontext spårning för alla parametrar till en metod. Detta måste beräknas för alla uttryck och följas upp i alla värden, praktiskt taget överallt där ref-safe-context för närvarande spåras.
Dessutom har det här värdet relationer med ref-safe-context. Det är till exempel icke-sensiskt att ha ett värde som kan returneras som ett ref
fält men inte direkt som ref
. Det beror på att ref
fält kan returneras enkelt av ref
redan (ref
tillstånd i en ref struct
kan returneras av ref
även när det innehållande värdet inte kan). Därför behöver reglerna ytterligare en ständig justering för att säkerställa att dessa värden är förnuftiga när det gäller varandra.
Det innebär också att språket behöver syntax för att representera ref
parametrar som kan returneras på tre olika sätt: efter ref
fält, efter ref
och efter värde. Standardvärdet är returnerbart av ref
. Framöver förväntas dock den mer naturliga avkastningen, särskilt när ref struct
är inblandade, vara från ref
-fältet eller ref
. Det innebär att nya API:er kräver en extra syntaxanteckning för att vara korrekt som standard. Det här är oönskat.
Dessa kompatibilitetsändringar påverkar dock metoder som har följande egenskaper:
- Ha en
Span<T>
ellerref struct
- Om
ref struct
är en returtyp,ref
ellerout
parameter - Har ytterligare en
in
ellerref
parameter (exklusive mottagaren)
- Om
För att förstå effekten är det bra att dela upp API:er i kategorier:
- Vill att användarna ska ta hänsyn till att
ref
registreras som ettref
fält. Det främsta exemplet ärSpan(ref T value)
-konstruktorer - Användarna ska inte behöva ta hänsyn till att
ref
blir registrerat som ettref
-fält. Dessa delas dock in i två kategorier- Osäkra API:er. Dessa är API:er i
Unsafe
- ochMemoryMarshal
-typerna, av vilkaMemoryMarshal.CreateSpan
är den mest framstående. Dessa API:er avbildarref
osäkert, men de är också kända för att vara osäkra API:er. - Säkra API:er. Det här är API:er som tar
ref
parametrar för effektivitet, men det noteras faktiskt inte någonstans. Exemplen är små, men ett ärAsnDecoder.ReadEnumeratedBytes
- Osäkra API:er. Dessa är API:er i
Den här ändringen gynnar främst (1) ovan. Dessa förväntas utgöra majoriteten av API:er som tar en ref
och returnerar en ref struct
framöver. Ändringarna påverkar negativt (2.1) och (2.2) eftersom det bryter mot den befintliga anropande semantiken eftersom livslängdsreglerna ändras.
API:erna i kategorin (2.1) är dock till stor del författade av Microsoft eller av utvecklare som har mest nytta av ref
fält (Tanner's of the world). Det är rimligt att anta att den här klassen av utvecklare skulle vara benägen att acceptera en kompatibilitetsavgift vid uppgradering till C# 11 i form av några annotationer för att behålla den befintliga semantiken om ref
-fält erbjöds som motprestation.
API:erna i kategorin (2.2) är det största problemet. Det är okänt hur många sådana API:er som finns och det är oklart om dessa skulle vara mer/mindre frekventa i tredjepartskod. Förväntningarna är att det finns ett mycket litet antal av dem, särskilt om vi tar kompatibilitetspausen på out
. Sökningar hittills har upptäckt ett mycket litet antal sådana som finns i ytan public
. Det här är ett svårt mönster att söka efter eftersom det kräver semantisk analys. Innan du gör den här ändringen krävs en verktygsbaserad metod för att verifiera antagandena kring detta som påverkar ett litet antal kända fall.
För båda fallen i kategori (2) så är korrigeringen enkel. De ref
parametrar som inte vill anses vara avbildningsbara måste lägga till scoped
i ref
. I (2.1) kommer detta sannolikt också att tvinga utvecklaren att använda Unsafe
eller MemoryMarshal
men det förväntas för API:er med osäkert format.
Helst kan språket minska effekten av problematiska ändringar utan att det märks genom att utfärda en varning när ett API utan förvarning börjar uppvisa problematiskt beteende. Det skulle vara en metod som både tar en ref
, returnerar ref struct
, men inte fångar upp ref
i ref struct
. Kompilatorn kan i så fall utfärda en diagnostik som informerar utvecklare att ref
ska anoteras som scoped ref
i stället.
Beslut Den här designen kan uppnås, men den resulterande funktionen är svårare att använda, vilket ledde till beslutet att göra ett kompatibilitetsbrott.
Beslut Kompilatorn ger en varning när en metod uppfyller kriterierna men inte avbildar parametern ref
som ett ref
fält. Detta bör på lämpligt sätt varna kunder vid uppgradering om de potentiella problem som de skapar
Nyckelord jämfört med attribut
Den här designen kräver att du använder attribut för att kommentera de nya livslängdsreglerna. Detta kunde också ha gjorts lika enkelt med kontextuella nyckelord. Till exempel kan [DoesNotEscape]
mappa till scoped
. Men nyckelord, även de kontextuella, måste i allmänhet uppfylla mycket höga krav för att inkluderas. De tar upp värdefullt språkutrymme och är mer framträdande delar av språket. Den här funktionen, även om den är värdefull, kommer att tjäna en minoritet av C#-utvecklare.
På ytan kan det verka gynna att inte använda nyckelord, men det finns två viktiga punkter att tänka på:
- Anteckningarna påverkar programmets semantik. Att låta attribut påverka programsemantiken är en gräns som C# är ovilligt att överskrida, och det är oklart om detta är funktionen som bör motivera att språket tar det steget.
- De utvecklare som är mest benägna att använda den här funktionen samverkar starkt med den uppsättning utvecklare som använder funktionspekare. Den funktionen, som också används av en minoritet av utvecklare, motiverade en ny syntax och det beslutet ses fortfarande som sunt.
Sammantaget innebär det att syntaxen bör beaktas.
En grov skiss av syntaxen skulle vara:
-
[RefDoesNotEscape]
mappar tillscoped ref
-
[DoesNotEscape]
mappar tillscoped
-
[RefDoesEscape]
mappar tillunscoped
Decision Use syntax for scoped
and scoped ref
; använd attributet för unscoped
.
Tillåt fasta buffertlokaler
Den här designen möjliggör säkra fixed
buffertar som kan stödja alla typer. Ett möjligt tillägg här är att tillåta att sådana fixed
buffertar deklareras som lokala variabler. Detta skulle göra det möjligt att ersätta ett antal befintliga stackalloc
åtgärder med en fixed
buffert. Det skulle också utöka uppsättningen scenarier där vi kan ha allokeringar i stackstil, eftersom stackalloc
är begränsad till ohanterade elementtyper, men fixed
-buffertar inte är det.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Detta håller ihop men kräver att vi utökar syntaxen för lokalbefolkningen lite. Oklart om detta är eller inte är värt den extra komplexiteten. Möjligt att vi kan besluta nej för tillfället och ta tillbaka senare om det finns tillräckligt med behov.
Exempel på var detta skulle vara fördelaktigt: https://github.com/dotnet/runtime/pull/34149
beslut avvakta med detta för tillfället
Att använda modreqs eller inte
Ett beslut måste fattas om metoder som har markerats med nya livslängdsattribut ska översättas till modreq
i avsändare. Det skulle i praktiken finnas en 1:1-mappning mellan anteckningar och modreq
om den här metoden användes.
Anledningen till att lägga till en modreq
är att attributen ändrar semantiken för referenssäkra kontextregler. Endast språk som förstår dessa semantik bör anropa metoderna i fråga. När de tillämpas på OHI-scenarier blir livslängden ett kontrakt som alla härledda metoder måste implementera. Att ha anteckningarna utan modreq
kan leda till situationer där virtual
metodkedjor med motstridiga livslängdsanteckningar laddas (kan inträffa om endast en del av virtual
kedjan kompileras och den andra inte är kompilerad).
Det inledande referenssäkra kontextarbetet använde inte modreq
utan förlitade sig i stället på språk och ramverket för att förstå. Samtidigt, även om alla element som bidrar till de referenssäkra kontextreglerna är en stark del av metodens signatur: ref
, in
, ref struct
, etc ... Därför resulterar varje ändring av de befintliga reglerna för en metod i en binär ändring av signaturen. För att ge de nya livstidsanvisningarna samma effekt behöver de modreq
tillämpning.
Frågan är om det här är överdrivet eller inte. Det har den negativa effekten att när man gör signaturer mer flexibla, till exempel genom att lägga till [DoesNotEscape]
till en parameter, resulterar det i en ändring av binär kompatibilitet. Den kompromissen innebär att ramverk som BCL med tiden sannolikt inte kommer att kunna lätta på sådana signaturer. Det kan mildras i viss utsträckning genom att använda några av de tillvägagångssätt språket använder för in
-parametrar och endast tillämpa modreq
på virtuella platser.
Beslut Använd inte modreq
i metadata. Skillnaden mellan out
och ref
är inte modreq
men de har nu olika referenssäkra kontextvärden. Det finns ingen verklig fördel med att bara tillämpa reglerna delvis i den här situationen med modreq
.
Tillåt flerdimensionella fasta buffertar
Ska designen för fixed
-buffertar utökas till att omfatta flerdimensionella stilmatriser? Tillåter i princip deklarationer som följande:
struct Dimensions
{
int array[42, 13];
}
Beslut Tillåt inte för tillfället
Bryter mot tilldelad omfattning
Runtime-lagringsplatsen har flera icke-offentliga API:er som samlar in ref
parametrar som ref
fält. Dessa är osäkra eftersom livslängden för det resulterande värdet inte spåras. Till exempel konstruktorn Span<T>(ref T value, int length)
.
De flesta av dessa API:er kommer sannolikt att välja att ha korrekt spårning av livslängd för returvärdena, vilket uppnås helt enkelt genom att uppdatera till C# 11. Några få vill dock behålla sin nuvarande semantik och inte spåra returvärdet eftersom deras hela avsikt är att vara osäkra. De mest anmärkningsvärda exemplen är MemoryMarshal.CreateSpan
och MemoryMarshal.CreateReadOnlySpan
. Detta uppnås genom att parametrarna markeras som scoped
.
Det innebär att körningen behöver ett etablerat mönster för att ta bort scoped
från en parameter på ett osäkert sätt:
-
Unsafe.AsRef<T>(in T value)
kan utöka sitt befintliga syfte genom att ändra tillscoped in T value
. Detta skulle göra det möjligt att både ta bortin
ochscoped
från parametrar. Det blir sedan den universella metoden "ta bort säkerheten för referenser" - Introducera en ny metod vars hela syfte är att ta bort
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Detta tar också bortin
eftersom, om det inte gjorde det, skulle anropare fortfarande behöva en kombination av metodanrop för att "ta bort referenssäkerhet", och då är den befintliga lösningen sannolikt tillräcklig.
Ska detta vara ospecificerat som standard?
Designen har bara två platser som är scoped
som standard:
-
this
ärscoped ref
-
out
ärscoped ref
Beslutet om out
är att avsevärt minska belastningen av kompatibilitet för ref
-fält och samtidigt är det en mer naturlig standardinställning. Det gör att utvecklare faktiskt kan tänka på out
som data som flödar utåt, medan om det är ref
, måste reglerna överväga data som flödar i båda riktningarna. Detta leder till betydande utvecklarförvirring.
Beslutet om this
är oönskat eftersom det innebär att en struct
inte kan returnera ett fält med ref
. Det här är ett viktigt scenario för utvecklare med hög perf och attributet [UnscopedRef]
har lagts till i huvudsak för det här scenariot.
Nyckelord har en hög standard och att lägga till det för ett enskilt scenario är misstänkt. Som sådant övervägdes det huruvida vi kunde undvika detta nyckelord helt och hållet genom att helt enkelt göra this
till ref
som standard istället för scoped ref
. Alla medlemmar som behöver att this
ska vara scoped ref
kan uppnå detta genom att markera metoden scoped
(då en metod kan markeras readonly
för att skapa en readonly ref
idag).
På ett normalt struct
är detta till största delen en positiv förändring eftersom det bara introducerar kompatibilitetsproblem när en medlem har en ref
återkomst. Det finns mycket få av dessa metoder och ett verktyg kan upptäcka dessa och konvertera dem till scoped
medlemmar snabbt.
På en ref struct
introducerar den här ändringen betydligt större kompatibilitetsproblem. Tänk på följande:
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;
}
}
I princip skulle det innebära att alla anrop av instansmetoder på föränderligaref struct
lokala variablerna skulle vara ogiltiga om inte den lokala variabeln markerades ytterligare som scoped
. Reglerna måste ta hänsyn till det fall där fälten har omtilldelats till andra fält i this
. Ett readonly ref struct
har inte det här problemet eftersom readonly
:s natur förhindrar referensompekning. Ändå skulle detta vara en betydande bakåtkompatibel ändring eftersom det skulle påverka praktiskt taget alla befintliga föränderliga ref struct
.
Ett readonly ref struct
är dock fortfarande problematiskt när vi utökar med ref
-fält till ref struct
. Det löser det grundläggande problemet genom att flytta fångsten till värdet för fältet 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);
}
}
Man övervägde tanken på att this
skulle ha olika standardinställningar beroende på typen av struct
eller medlem. Till exempel:
-
this
somref
:struct
,readonly ref struct
ellerreadonly member
-
this
somscoped ref
:ref struct
ellerreadonly ref struct
medref
fält tillref struct
Detta minimerar kompatibilitetsavbrott och maximerar flexibiliteten, men på bekostnad av att komplicera berättelsen för kunderna. Det löser inte heller problemet helt eftersom framtida funktioner, till exempel säkra fixed
buffertar, kräver att en föränderlig ref struct
har ref
returnerar för fält som inte fungerar enbart med den här designen eftersom den skulle tillhöra kategorin scoped ref
.
Beslut Behåll this
som scoped ref
. Det innebär att de föregående lömska exemplen skapar kompilatorfel.
referensfält till ref-struktur
Den här funktionen öppnar en ny uppsättning referenssäkra kontextregler eftersom det gör att ett ref
fält kan referera till en ref struct
. Den generiska naturen hos ByReference<T>
innebar att körningstiden hittills inte kunde ha en sådan konstruktion. Som ett resultat är alla våra regler skrivna under antagandet att detta inte är möjligt. Den ref
fältfunktionen handlar till stor del inte om att skapa nya regler utan att kodifiera befintliga regler i vårt system. För att tillåta ref
fält att ref struct
måste vi kodifiera nya regler eftersom det finns flera nya scenarier att överväga.
Det första är att en readonly ref
nu kan lagra ett ref
-tillstånd. Till exempel:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Det innebär att när vi tänker på att metodargument måste överensstämma med regler, måste vi överväga att readonly ref T
är en potentiell metodutdata när T
potentiellt har ett ref
-fält till en ref struct
.
Det andra problemet är att språket måste ha en ny typ av säker kontext: referensfältsäker kontext. Alla ref struct
som transitivt innehåller ett ref
-fält har ett annat flyktomfång som representerar värdena i ref
-fält. När det gäller flera ref
fält kan de spåras kollektivt som ett enda värde. Standardvärdet för detta för parametrar är anroparkontext.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Det här värdet är inte relaterat till containerns safe-context-. eftersom containerkontexten blir mindre påverkar den inte referensfältsäker kontext för ref
fältvärden. Dessutom kan ref-field-safe-context aldrig vara mindre än containerns safe-context-.
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];
}
Det här ref-field-safe-context har i princip alltid funnits. Fram tills nu kunde ref
fälten bara peka på vanliga struct
därför sammanföll det enkelt med anroparkontext. För att stödja ref
fält i ref struct
behöver våra befintliga regler uppdateras för att beakta detta nya ref-safe-context.
För det tredje måste reglerna för omtilldelning av referens uppdateras för att säkerställa att vi inte bryter mot referensfältkontext för värdena. För x.e1 = ref e2
där typen av e1
är en ref struct
måste ref-field-safe-context vara lika.
Dessa problem är mycket lösbara. Kompilatorteamet har skissat upp några versioner av dessa regler och de faller till stor del ut från vår befintliga analys. Problemet är att det inte finns någon användningskod för sådana regler som hjälper till att bevisa att det finns korrekthet och användbarhet. Detta gör oss väldigt tveksama att lägga till stöd på grund av rädslan för att vi ska välja fel standardvärden och backa körningen i användbarhetshörnet när det drar nytta av detta. Denna oro är särskilt stark eftersom .NET 8 sannolikt driver oss i den här riktningen med allow T: ref struct
och Span<Span<T>>
. Reglerna skulle skrivas bättre om det görs tillsammans med konsumtionskoden.
Beslut Fördröjning som möjliggör ref
fältet att ref struct
till .NET 8, där vi har scenarier som bidrar till att utforma reglerna kring dessa scenarier. Detta har inte implementerats från och med .NET 9
Vad innebär C# 11.0?
De funktioner som beskrivs i det här dokumentet behöver inte implementeras i ett enda pass. I stället kan de implementeras i faser i flera språkversioner i följande bucketar:
-
ref
fält ochscoped
[UnscopedRef]
-
ref
fält tillref struct
- Begränsade typer av solnedgångar
- buffertar med fast storlek
Det som implementeras i vilken version är bara en avgränsningsövning.
Endast beslut (1) och (2) gjorde C# 11.0. Resten kommer att beaktas i framtida versioner av C#.
Framtida överväganden
Avancerade livstidsanteckningar
Livslängdsanteckningarna i det här förslaget är begränsade eftersom de gör det möjligt för utvecklare att ändra standardbeteendet escape/don't escape för värden. Detta ger kraftfull flexibilitet i vår modell, men det ändrar inte radikalt den uppsättning relationer som kan uttryckas. I grunden är C#-modellen fortfarande i praktiken binär: kan ett värde returneras eller inte?
Det gör att begränsade livslängdsrelationer kan förstås. Till exempel har ett värde som inte kan returneras från en metod en mindre livslängd än ett som kan returneras från en metod. Det finns dock inget sätt att beskriva livslängdsrelationen mellan värden som kan returneras från en metod. Mer specifikt finns det inget sätt att säga att ett värde har en större livslängd än det andra när det har upprättats kan båda returneras från en metod. Nästa steg i vår livsutveckling skulle vara att tillåta att sådana relationer beskrivs.
Andra metoder som Rust tillåter att den här typen av relation uttrycks och kan därför implementera mer komplexa scoped
stilåtgärder. Vårt språk skulle på liknande sätt kunna dra nytta av en sådan funktion. För närvarande finns det inget motiverande tryck att göra detta, men om det finns i framtiden kan vår scoped
modell utökas till att omfatta den på ett ganska rakt framåt sätt.
Varje scoped
kan tilldelas en namngiven livslängd genom att lägga till ett allmänt formatargument i syntaxen. Till exempel är scoped<'a>
ett värde som har livslängden 'a
. Begränsningar som where
kan sedan användas för att beskriva relationerna mellan dessa livslängder.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Den här metoden definierar två livslängder 'a
och 'b
och deras relation, särskilt att 'b
är större än 'a
. På så sätt kan anropsplatsen ha mer detaljerade regler för hur värden på ett säkert sätt kan överföras till metoder jämfört med de mer grova korniga regler som finns i dag.
Relaterad information
Frågor
Följande problem är alla relaterade till det här förslaget:
- 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
Förslag
Följande förslag gäller detta förslag:
Befintliga exempel
Just det här kodavsnittet kräver användning av 'unsafe' eftersom det kan stöta på problem med att skicka vidare en Span<T>
som kan stackallokeras till en instansmetod på en ref struct
. Även om den här parametern inte fångas in måste språket anta att det är det och därmed orsakar friktion i onödan här.
Det här kodfragmentet vill mutera en parameter genom att ta bort dataelement. Undantagna data kan stackallokeras för effektivitet. Även om parametern inte är undantagen tilldelar kompilatorn den en säker kontext utanför omslutningsmetoden eftersom det är en parameter. Detta innebär att för att använda stackallokering måste implementeringen använda unsafe
för att tilldela tillbaka till parametern efter att datan har avslutats.
Roliga exempel
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;
}
}
Sparsam lista
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;
}
}
}
}
Exempel och anteckningar
Nedan visas en uppsättning exempel som visar hur och varför reglerna fungerar som de ska. Det finns flera exempel som visar farliga beteenden och hur reglerna förhindrar att de inträffar. Det är viktigt att ha dessa i åtanke när du gör justeringar i förslaget.
Referenstilldelnings- och samtalswebbplatser
Visar hur referenstilldelning och metodanrop fungerar tillsammans.
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;
}
}
Referenstilldelning och osäkra flyktsekvenser
Orsaken till följande rad i referenstilldelningsregler kanske inte är uppenbar vid första anblicken:
e1
måste ha samma säkra kontext some2
Det beror på att livslängden för de värden som pekas på av ref
platser är invarianta. Indirektionen hindrar oss från att tillåta någon form av varians här, även till smalare livslängder. Om en begränsning är tillåten öppnar det följande osäkra kod:
void Example(ref Span<int> p)
{
Span<int> local = stackalloc int[42];
ref Span<int> refLocal = ref local;
// Error:
// The safe-context of refLocal is narrower than p. For a non-ref reassignment
// this would be allowed as its safe to assign wider lifetimes to narrower ones.
// In the case of ref reassignment though this rule prevents it as the
// safe-context values are different.
refLocal = ref p;
// If it were allowed this would be legal as the safe-context of refLocal
// is *caller-context* and that is satisfied by stackalloc. At the same time
// it would be assigning through p and escaping the stackalloc to the calling
// method
//
// This is equivalent of saying p = stackalloc int[13]!!!
refLocal = stackalloc int[13];
}
För en ref
till icke-ref struct
är den här regeln enkelt uppfylld eftersom värdena alla har samma säker kontext. Den här regeln spelar egentligen bara in när värdet är en ref struct
.
Det här beteendet hos ref
kommer också att vara viktigt i framtiden där vi tillåter ref
-fälten att ref struct
.
begränsade lokala platser
Användningen av scoped
på lokala variabler är särskilt användbar för kodmönster som villkorligt tilldelar värden med olika säker-kontext till lokala variabler. Det innebär att kod inte längre behöver förlita sig på initieringstricks som = stackalloc byte[0]
för att definiera en lokal säker kontext utan kan nu helt enkelt använda 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];
}
Det här mönstret visas ofta i lågnivåkod. När ref struct
är Span<T>
, kan ovanstående trick användas. Det är dock inte tillämpligt för andra ref struct
typer och kan leda till att kod på låg nivå behöver tillgripa unsafe
för att kringgå oförmågan att korrekt ange livslängden.
parametervärden inom ett specifikt område
En källa till upprepad friktion i kod på låg nivå är att standard escape-sekvensen för parametrar är förlåtande. De är i en säker kontext till anroparkontexten . Detta är en lämplig standard eftersom det stämmer med kodningsmönstren för .NET som helhet. I lågnivåkod finns det dock en större användning av ref struct
och den här standardinställningen kan orsaka friktion med andra delar av referenssäkerhetskontextreglerna.
Den huvudsakliga friktionspunkten inträffar på grund av metodargument måste matcha regel. Den här regeln spelar oftast in med instansmetoder på ref struct
där minst en parameter också är en ref struct
. Det här är ett vanligt mönster i lågnivåkod där ref struct
typer ofta använder Span<T>
parametrar i sina metoder. Det sker till exempel på alla skrivstilar ref struct
som använder Span<T>
för att skicka runt buffertar.
Den här regeln finns för att förhindra scenarier som följande:
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);
}
}
I princip finns den här regeln eftersom språket måste förutsätta att alla indata till en metod inte uppfyller deras högsta tillåtna säker kontext. När det finns ref
eller out
parametrar, inklusive mottagarna, är det möjligt för indata att fly som fält i dessa ref
värden (som händer i RS.Set
ovan).
I praktiken finns det många sådana metoder som skickar ref struct
som parametrar som aldrig har för avsikt att samla in dem i utdata. Det är bara ett värde som används i den aktuella metoden. Till exempel:
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))
{
...
}
}
}
För att kunna kringgå denna lågnivåkod används unsafe
-knep för att lura kompilatorn om livstiden för ref struct
. Detta minskar avsevärt värdeförslaget för ref struct
eftersom de är avsedda att vara ett sätt att undvika unsafe
samtidigt som du fortsätter att skriva kod med höga prestanda.
Det är här scoped
är ett effektivt verktyg för ref struct
parametrar eftersom det tar bort dem från övervägandet eftersom de returneras från metoden enligt de uppdaterade metodargumenten måste matcha regeln. En ref struct
parameter som förbrukas, men aldrig returneras, kan märkas som scoped
för att göra anropswebbplatser mer flexibla.
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))
{
...
}
}
}
Förhindra knepig referenstilldelning från readonly mutation
När en ref
tas till ett readonly
fält i en konstruktor eller init
medlem är typen ref
inte ref readonly
. Det här är ett långvarigt beteende som tillåter kod som följande:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Det innebär dock ett potentiellt problem om en sådan ref
kunde lagras i ett ref
fält på samma typ. Det skulle möjliggöra direkt mutation av en readonly struct
från en instansmedlem:
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++;
}
}
Förslaget förhindrar dock detta eftersom det strider mot reglerna för referenssäker kontext. Tänk på följande:
-
referenssäker kontext för
this
är funktionsmedlem och säker kontext är anroparkontext. Dessa är båda standard förthis
i enstruct
medlem. -
referenssäker kontext för
i
är funktionsmedlem. Detta faller ut från fältlivslängdsregler. Specifikt regel 4.
Då är raden r = ref i
ogiltig av referenstilldelningsregler.
Dessa regler var inte avsedda att förhindra det här beteendet men gör det som en bieffekt. Det är viktigt att ha detta i åtanke för alla framtida regeluppdateringar för att utvärdera påverkan på scenarier som detta.
Fånig cyklisk tilldelning
En aspekt som den här designen hade problem med är hur en ref
kan returneras fritt från en metod. Att tillåta att alla ref
returneras lika fritt som normala värden är förmodligen vad de flesta utvecklare intuitivt förväntar sig. Det möjliggör dock patologiska scenarier som kompilatorn måste tänka på vid beräkning av referenssäkerhet. Tänk på följande:
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;
}
}
Det här är inte ett kodmönster som vi förväntar oss att utvecklare ska använda. Men när en ref
kan returneras med samma livslängd som ett värde är det lagligt enligt reglerna. Kompilatorn måste ta hänsyn till alla rättsfall vid utvärdering av ett metodanrop och detta leder till att sådana API:er i praktiken är oanvändbara.
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);
}
För att göra dessa API:er användbara säkerställer kompilatorn att den ref
livslängden för en ref
parameter är mindre än livslängden för alla referenser i det associerade parametervärdet. Det här är anledningen till att ska vara en referenssäker kontext för ref
till ref struct
ska vara endast för retur och out
ska vara anroparkontext. Det förhindrar cyklisk tilldelning på grund av skillnaden i livslängd.
Observera att [UnscopedRef]
främjarref-safe-context för alla ref
till ref struct
värden till anroparkontext, och därmed möjliggör det cyklisk tilldelning och tvingar fram en viral användning av [UnscopedRef]
upp genom samtalskedjan:
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;
}
}
På samma sätt tillåter [UnscopedRef] out
en cyklisk tilldelning eftersom parametern har både safe-context och ref-safe-context för return-only.
Det är användbart att uppgradera [UnscopedRef] ref
till anropskontexten när typen är inte en ref struct
(observera att vi vill hålla reglerna enkla så att de inte skiljer mellan referenser till ref kontra icke-ref strukturer):
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;
}
}
När det gäller avancerade anteckningar skapar [UnscopedRef]
design följande:
ref struct S { }
// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)
// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
where 'b >= 'a
readonly kan inte vara djupt genom referensfält
Överväg kodexemplet nedan:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
När du utformar reglerna för ref
-fält på readonly
-instanser i ett vakuum kan reglerna utformas giltiga så att det ovanstående är tillåtet eller förbjudet. I princip kan readonly
vara djup genom ett ref
-fält eller endast gälla för ref
. Att endast tillämpa på ref
förhindrar att referensen omplaceras, men tillåter normal tilldelning, vilket förändrar det refererade värdet.
Den här designen finns dock inte i ett vakuum, den utformar regler för typer som redan har ref
fält. Den mest framträdande av vilka, Span<T>
, har redan ett starkt beroende av att readonly
inte är djup här. Det primära scenariot är möjligheten att tilldela till fältet ref
via en readonly
instans.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Det innebär att vi måste välja den ytliga tolkningen av readonly
.
Modelleringskonstruktorer
En subtil designfråga är: Hur modelleras konstruktorkroppar för referenssäkerhet? I princip hur analyseras följande konstruktor?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Det finns ungefär två metoder:
- Modell som en
static
-metod därthis
är en lokal där dess är - Modellera som en
static
metod därthis
är enout
parameter.
Ytterligare en konstruktor måste uppfylla följande invarianter:
- Kontrollera att
ref
parametrar kan samlas in somref
fält. - Säkerställ att
ref
till fälten ithis
inte kan undkommas viaref
parametrarna. Det skulle strida mot komplicerad referenstilldelning.
Avsikten är att välja det formulär som uppfyller våra invarianter utan att införa några särskilda regler för konstruktorer. Med tanke på att den bästa modellen för konstruktorer visar this
som en out
parameter.
returnerar endast typ av out
tillåter oss att tillfredsställa alla invarianter ovan utan något särskilt hölje:
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;
}
Metodargumenten måste överensstämma
Metodargumenten måste matcha regeln är en vanlig källa till förvirring för utvecklare. Det är en regel som har ett antal specialfall som är svåra att förstå om du inte är bekant med resonemanget bakom regeln. För att bättre förstå orsakerna till regeln förenklar vi referenssäker kontext och säker kontext att helt enkelt kontext.
Metoder kan ganska liberalt returnera tillstånd som skickas till dem som parametrar. I stort sett kan alla nåbara tillstånd som inte är scoped returneras (inklusive återvändandet av ref
). Detta kan returneras direkt via en return
-instruktion eller indirekt genom att tilldela till ett ref
värde.
Direktreturer utgör inte några större problem för referenssäkerheten. Kompilatorn behöver helt enkelt titta på alla returnerbara indata till en metod och sedan effektivt begränsa returvärdet till det minsta kontext av indata. Det returnerade värdet genomgår sedan normal bearbetning.
Indirekta returer utgör ett betydande problem eftersom alla ref
både är indata och utdata till metoden. Dessa utdata har redan en känd kontext. Kompilatorn kan inte härleda nya, utan måste överväga dem på den aktuella nivån. Det innebär att kompilatorn måste titta på varje enskild ref
som kan tilldelas i den anropade metoden, utvärdera kontextoch sedan kontrollera att inga returnerbara indata till metoden har en mindre kontext än den ref
. Om något sådant fall finns måste metodanropet vara olagligt eftersom det kan strida mot ref
säkerhet.
Metodargumenten måste matcha, vilket är en säkerhetskontroll som kompilatorn utför.
Ett annat sätt att utvärdera detta som ofta är enklare för utvecklare att tänka på är att göra följande övning:
- Titta på metoddefinitionen och identifiera alla platser där tillståndet indirekt kan returneras: a. Föränderliga
ref
parametrar som pekar påref struct
b. Föränderligaref
parametrar med referenstilldeladeref
fält c. Tilldelningsbararef
-parametrar ellerref
-fält som pekar påref struct
(överväg detta rekursivt) - Titta på samtalswebbplatsen a. Identifiera de kontexter som överensstämmer med de platser som identifieras ovan b. Identifiera sammanhangen för alla indata till metoden som kan returneras (alignera inte med
scoped
-parametrar)
Om något värde i 2.b är mindre än 2.a måste metodanropet vara ogiltigt. Låt oss titta på några exempel för att illustrera reglerna:
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);
}
}
Genom att titta på samtalet till F0
, låt oss gå igenom (1) och (2). Parametrarna med potential för indirekt avkastning är a
och b
eftersom båda kan tilldelas direkt. Argumenten som överensstämmer med dessa parametrar är:
-
a
som mappas tillx
och har kontexten av samtalskontexten -
b
som mappas tilly
som har med kontext av funktionsmedlem
Uppsättningen returnerbara indata till metoden är
-
x
med escape-scope av i uppringarkontext -
ref x
med escape-scope av i uppringarkontext -
y
med undflykt-område för funktionsmedlem
Värdet ref y
kan inte returneras eftersom det mappas till en scoped ref
därför betraktas det inte som indata. Men med tanke på att det finns minst en indata med ett mindre escape-omfång (y
argument) än en av utdata (x
argument) så är metodanropet olagligt.
En annan variant är följande:
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);
}
}
Återigen är parametrarna med potential för indirekt retur a
och b
eftersom båda kan tilldelas direkt. Men b
kan undantas eftersom det inte pekar på en ref struct
, och därför inte kan användas för att lagra ref
tillstånd. Därför har vi:
-
a
som mappas tillx
och har kontexten av samtalskontexten
Uppsättningen returnerbara indata till metoden är:
-
x
med kontext av anropar-kontext -
ref x
med kontext av anropar-kontext -
ref y
med kontext av funktionsmedlem
Eftersom det finns minst en indata med ett mindre escape-omfång (ref y
argument) än ett av utdata (x
argument) är metodanropet ogiltigt.
Det här är den logik som regeln att metodargumenten måste matcha försöker omfatta. Det går längre genom att använda både scoped
och readonly
som sätt att ta bort indata från övervägande respektive ta bort ref
som utdata (kan inte tilldela till en readonly ref
så det inte kan vara en källa till utdata). De här specialfallen lägger till komplexitet i reglerna, men det görs till förmån för utvecklaren. Kompilatorn försöker ta bort alla indata och utdata som den vet inte kan bidra till resultatet för att ge utvecklarna maximal flexibilitet när de anropar en medlem. Precis som överbelastningslösning är det värt att göra våra regler mer komplexa när det skapar mer flexibilitet för konsumenterna.
Exempel på härledda säker kontext av deklarationsuttryck
Relaterat till Slutsats säker kontext av deklarationsuttryck.
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).
}
}
Observera att den lokala kontext som är resultatet av den scoped
modifieraren är den smalaste som kan användas för variabeln– att vara smalare skulle innebära att uttrycket refererar till variabler som endast deklareras i en smalare kontext än uttrycket.
C# feature specifications