StructVerbeteringen op laag niveau
Notitie
Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.
Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting-notities).
Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.
Championproblemen: https://github.com/dotnet/csharplang/issues/1147, https://github.com/dotnet/csharplang/issues/6476
Samenvatting
Dit voorstel is een samenvoeging van verschillende voorstellen voor struct
prestatieverbeteringen: ref
velden en de mogelijkheid om de standaardwaarden voor levensduur te overschrijven. Het doel is een ontwerp dat rekening houdt met de verschillende voorstellen om één overkoepelende set van functies te creëren voor laag-niveau struct
-verbeteringen.
Opmerking: in eerdere versies van deze specificatie werden de termen 'ref-safe-to-escape' en 'safe-to-escape' gebruikt, die zijn geïntroduceerd in de specificatie van de Span-veiligheid functie. Het ECMA-standaardcomité heeft de namen gewijzigd in respectievelijk "ref-safe-context" en "safe-context". De waarden van de veilige context zijn verfijnd om 'declaration-block', 'function-member' en 'caller-context' consistent te gebruiken. De speclets hadden verschillende formuleringen gebruikt voor deze termen en gebruikten ook "safe-to-return" als synoniem voor "caller-context". Deze speclet is bijgewerkt voor gebruik van de termen in de C# 7.3-standaard.
Niet alle functies die in dit document worden beschreven, zijn geïmplementeerd in C# 11. C# 11 bevat:
-
ref
velden enscoped
[UnscopedRef]
Deze functies blijven open voorstellen voor een toekomstige versie van C#:
-
ref
velden naarref struct
- Typen die beperkt zijn bij zonsondergang
Motivatie
Eerdere versies van C# hebben een aantal prestatiefuncties op laag niveau toegevoegd aan de taal: ref
retourneert, ref struct
, functiepointers, enzovoort... Deze ingeschakelde .NET-ontwikkelaars kunnen zeer goed presterende code schrijven terwijl ze de C#-taalregels blijven gebruiken voor type- en geheugenveiligheid. Ook is het maken van fundamentele prestatietypen in de .NET-bibliotheken toegestaan, zoals Span<T>
.
Naarmate deze functies terrein hebben gewonnen in het .NET-ecosysteem, hebben ontwikkelaars, zowel intern als extern, ons geïnformeerd over resterende wrijvingspunten in het ecosysteem. Plaatsen waar ze nog steeds moeten terugvallen op unsafe
-code om hun werk voor elkaar te krijgen of een runtime nodig hebben die speciale gevallen zoals Span<T>
kan afhandelen.
Vandaag wordt Span<T>
bereikt met behulp van het internal
type ByReference<T>
dat de runtime effectief als een ref
veld behandelt. Dit biedt het voordeel van ref
velden, maar met het nadeel dat de taal er geen veiligheidsverificatie voor biedt, zoals voor andere toepassingen van ref
. Verder kan alleen dotnet/runtime dit type gebruiken omdat het internal
is, zodat derden hun eigen primitieven niet kunnen ontwerpen op basis van ref
velden. Een deel van de motivatie voor dit werk is het verwijderen van ByReference<T>
en het gebruik van de juiste ref
velden in alle codebases.
Dit voorstel is van plan om deze problemen op te lossen door voort te bouwen op onze bestaande functies op laag niveau. In het bijzonder is het erop gericht het volgende te doen:
- Sta
ref struct
typen toe omref
velden te declareren. - Toestaan dat de runtime
Span<T>
volledig definieert met behulp van het C#-typesysteem en speciale casetypes zoalsByReference<T>
verwijdert. - Sta
struct
typen toe omref
naar hun velden terug te sturen. - Runtime toestaan om
unsafe
-gebruiken te verwijderen die worden veroorzaakt door beperkingen van leefduurstandaarden - De declaratie van veilige
fixed
buffers toestaan voor beheerde en onbeheerde typen instruct
Gedetailleerd ontwerp
De regels voor ref struct
veiligheid worden in het veiligheidsdocument met de eerdere termen gedefinieerd. Deze regels zijn opgenomen in de C# 7-norm in §9.7.2 en §16.4.12. In dit document worden de vereiste wijzigingen in dit document beschreven als gevolg van dit voorstel. Zodra deze wijzigingen zijn geaccepteerd als een goedgekeurde functie, worden deze wijzigingen in dat document opgenomen.
Zodra dit ontwerp is voltooid, is de definitie van de Span<T>
als volgt:
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;
}
}
Referentievelden en gespecificeerd opgeven
Met de taal kunnen ontwikkelaars ref
velden binnen een ref struct
declareren. Dit kan bijvoorbeeld handig zijn bij het inkapselen van grote, veranderlijke struct
exemplaren of het definiëren van high performance-typen, zoals Span<T>
in bibliotheken, naast de runtime.
ref struct S
{
public ref int Value;
}
Een ref
veld wordt verzonden naar metagegevens met behulp van de ELEMENT_TYPE_BYREF
handtekening. Dit is niet anders dan zoals we ref
lokale variabelen of ref
argumenten genereren.
ref int _field
wordt bijvoorbeeld verzonden als ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
. Hiervoor moeten we ECMA335 bijwerken om deze vermelding toe te staan, maar dit moet vrij eenvoudig zijn.
Ontwikkelaars kunnen een ref struct
met een ref
veld blijven initialiseren met behulp van de default
-expressie. In dat geval hebben alle gedeclareerde ref
velden de waarde null
. Elke poging om dergelijke velden te gebruiken, resulteert in een NullReferenceException
wordt gegenereerd.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Hoewel de C#-taal doet alsof een ref
geen null
kan zijn, is dit op runtimeniveau toegestaan en heeft het goed gedefinieerde semantiek. Ontwikkelaars die ref
velden in hun typen introduceren, moeten rekening houden met deze mogelijkheid en moeten sterk worden afgeraden om deze details te lekken in de gebruikscode. In plaats daarvan moeten ref
velden als niet-null worden gevalideerd met behulp van de runtime-helpers en worden gegooid wanneer een niet-geïnitialiseerde struct
onjuist wordt gebruikt.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Een ref
veld kan op de volgende manieren worden gecombineerd met readonly
modifiers:
-
readonly ref
: dit is een veld dat niet opnieuw kan worden toegewezen buiten een constructor ofinit
methoden. De waarde kan wel worden toegewezen, maar buiten die contexten -
ref readonly
: dit is een veld dat opnieuw kan worden toegewezen met een referentie, maar dat op geen enkel moment een waarde kan krijgen. Dit hoe eenin
parameter opnieuw kan worden toegewezen aan eenref
veld. -
readonly ref readonly
: een combinatie vanref readonly
enreadonly 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)
}
}
Een readonly ref struct
vereist dat ref
velden worden gedeclareerd readonly ref
. Er is geen vereiste dat ze worden aangegeven readonly ref readonly
. Hierdoor kan een readonly struct
indirecte mutaties hebben via een dergelijk veld, maar dat is niet anders dan een readonly
veld dat vandaag naar een referentietype wijst (meer details)
Er wordt een readonly ref
uitgegeven aan de metadata met de initonly
-vlag, net als bij elk ander veld. Een ref readonly
veld wordt toegeschreven aan System.Runtime.CompilerServices.IsReadOnlyAttribute
. Er wordt een readonly ref readonly
verzonden met beide items.
Deze functie vereist runtime-ondersteuning en wijzigingen in de ECMA-specificatie. Als zodanig worden deze alleen ingeschakeld wanneer de bijbehorende functievlag is ingesteld in corelib. Het probleem met het volgen van de exacte API wordt hier bijgehouden https://github.com/dotnet/runtime/issues/64165
De set wijzigingen in onze veilige contextregels die nodig zijn om ref
velden toe te staan, is klein en gericht. De regels houden al rekening met ref
velden die bestaan en worden gebruikt door API's. De wijzigingen moeten zich richten op slechts twee aspecten: hoe ze worden gemaakt en hoe ze opnieuw worden toegewezen.
Eerst moeten de regels voor het tot stand brengen van ref-safe-context waarden voor velden als volgt worden bijgewerkt voor ref
velden:
Een expressie in de vorm
ref e.F
ref-safe-context als volgt:
- Als
F
eenref
-veld is, dan is de ref-safe-context de veilige context vane
.- Anders als
e
van een verwijzingstype is, heeft deze ref-safe-context van aanroeper-context- Anders wordt de ref-safe-context genomen uit de ref-safe-context van
e
.
Dit vertegenwoordigt echter geen regelwijziging, omdat de regels altijd rekening hebben gehouden met de ref
-staat binnen een ref struct
. Dit is in feite hoe de ref
status in Span<T>
altijd heeft gewerkt en dat de verbruiksregels hiervoor correct rekening houden. De wijziging hier houdt in dat ontwikkelaars rechtstreeks toegang kunnen krijgen tot ref
velden, zodat zij dit doen volgens de bestaande regels die impliciet worden toegepast op Span<T>
.
Dit betekent wel dat ref
velden kunnen worden geretourneerd als ref
van een ref struct
, maar normale velden niet.
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;
}
Dit lijkt op het eerste gezicht een fout, maar dit is een opzettelijk ontwerppunt. Dit voorstel creëert echter geen nieuwe regel; in plaats daarvan erkent het de bestaande regels Span<T>
waarbinnen ontwikkelaars nu hun eigen ref
status kunnen verklaren.
Vervolgens dienen de regels voor het opnieuw toewijzen van referentie te worden aangepast in verband met de aanwezigheid van ref
-velden. Het primaire scenario voor referentietoewijzing is ref struct
-constructors die ref
-parameters opslaan in ref
-velden. De ondersteuning wordt algemener, maar dit is het kernscenario. Om dit te ondersteunen, worden de regels voor het opnieuw toewijzen van verwijzingen als volgt aangepast om rekening te houden met ref
-velden:
Regels voor opnieuw toewijzen van ref
De linkeroperand van de operator = ref
moet een expressie zijn die is gekoppeld aan een ref lokale variabele, een ref-parameter (anders dan this
), een out-parameter, , of een ref-veld.
Voor een hertoewijzing in de vorm
e1 = ref e2
moeten beide van de volgende waar zijn:
e2
moet ref-safe-context hebben minstens zo groot als de ref-safe-context vane1
e1
moet dezelfde veilige context hebben alse2
Opmerking
Dat betekent dat de gewenste Span<T>
constructor werkt zonder extra aantekeningen:
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;
}
}
De wijziging in hertoewijzingsregels betekent dat ref
parameters nu kunnen ontsnappen uit een methode als een ref
veld in een ref struct
waarde. Zoals besproken in de sectie compatibiliteitsoverwegingen kan dit de regels voor bestaande API's wijzigen die nooit zijn bedoeld voor ref
parameters om te ontsnappen als een ref
veld. De levensduurregels voor parameters zijn uitsluitend gebaseerd op hun declaratie niet op hun gebruik. Alle parameters ref
en in
hebben ref-safe-context van aanroepercontext en kunnen daarom nu worden geretourneerd door ref
of een ref
veld. Ter ondersteuning van API's met ref
parameters die kunnen ontsnappen of niet-ontsnappen, en zo de semantiek van C# 10-aanroepplaatsen herstellen, worden in de taal beperkte levensduuraanotaties geïntroduceerd.
scoped
modifier
Het trefwoord scoped
wordt gebruikt om de levensduur van een waarde te beperken. Deze kan worden toegepast op een ref
of een waarde die een ref struct
is en heeft als gevolg dat de levensduur van de ref-safe-context of veilige context, respectievelijk, beperkt wordt tot het functielid. Bijvoorbeeld:
Parameter of lokaal | ref-safe-context | veilige context |
---|---|---|
Span<int> s |
functielid | aanroepercontext |
scoped Span<int> s |
functielid | functielid |
ref Span<int> s |
aanroepercontext | aanroepercontext |
scoped ref Span<int> s |
functielid | aanroepercontext |
In deze relatie kan de ref-safe-context van een waarde nooit breder zijn dan de veilige context.
Hierdoor kunnen API's in C# 11 worden geannoteerd, zodat ze dezelfde regels hebben als 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]);
}
De scoped
aantekening betekent ook dat de parameter this
van een struct
nu kan worden gedefinieerd als scoped ref T
. Voorheen moest het speciaal worden behandeld in de regels als ref
-parameter die verschillende regels voor ref-safe-context had dan andere ref
parameters (zie alle verwijzingen naar het opnemen of uitsluiten van de ontvanger in de regels voor veilige contexten). Nu kan het worden uitgedrukt als een algemeen concept in de regels die ze verder vereenvoudigen.
De aantekening scoped
kan ook worden toegepast op de volgende locaties:
- locals: Deze aantekening stelt de levensduur in als veilige context, of ref-safe-context in het geval van een
ref
lokaal, tot van functie-element, ongeacht de levensduur van de initialisator.
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];
}
Andere toepassingen voor scoped
op de lokale bevolking worden besproken hieronder.
De scoped
-annotatie kan niet worden toegepast op een andere locatie, waaronder retourwaarden, velden, array-elementen, enz. Verder heeft scoped
invloed wanneer deze wordt toegepast op een ref
, in
of out
, maar heeft het alleen invloed wanneer deze wordt toegepast op waarden die ref struct
zijn. Het hebben van declaraties zoals scoped int
heeft geen invloed omdat een niet-ref struct
altijd veilig is om terug te keren. De compiler maakt een diagnose voor dergelijke gevallen om verwarring bij ontwikkelaars te voorkomen.
Het gedrag van out
parameters wijzigen
Om de impact van de compatibiliteitswijziging waarbij ref
- en in
-parameters als ref
-velden kunnen worden geretourneerd te verminderen, zullen de standaardinstelling ref-safe-context en de waarde voor out
-parameters worden aangepast naar functielid. Effectief zijn out
parameters impliciet scoped out
in de toekomst. Vanuit compatibiliteitsperspectief betekent dit dat ze niet kunnen worden geretourneerd door ref
:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Hierdoor wordt de flexibiliteit vergroot van API's die ref struct
waarden retourneren en out
parameters hebben, omdat de parameter niet langer door verwijzing hoeft te worden vastgelegd. Dit is belangrijk omdat het een gemeenschappelijk patroon is in api's voor lezerstijlen:
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);
}
De taal beschouwt argumenten die worden doorgegeven aan een out
parameter niet langer als retourneerbaar. Het behandelen van de invoer voor een out
parameter als returnable was zeer verwarrend voor ontwikkelaars. In wezen wordt de intentie van out
ondermijnd door ontwikkelaars te dwingen rekening te houden met de waarde die door de beller wordt doorgegeven, een waarde die nooit wordt gebruikt, behalve in talen die out
niet respecteren. Talen die ondersteuning bieden voor ref struct
moeten ervoor zorgen dat de oorspronkelijke waarde die wordt doorgegeven aan een out
parameter nooit wordt gelezen.
C# bereikt dit via de definitieve toewijzingsregels. Dat zowel onze regels voor veilige referentiecontext bereikt als het toestaan van bestaande code die out
-parameterwaarden toewijst en vervolgens retourneert.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Samen betekenen deze wijzigingen dat het argument voor een out
parameter niet bijdraagt aan veilige context of ref-safe-context waarden voor methodeaanroepen. Dit vermindert de algehele impact van ref
velden aanzienlijk en vereenvoudigt hoe ontwikkelaars nadenken over out
. Een argument voor een out
parameter draagt niet bij aan de return, het is gewoon een uitvoer.
Context veilig-context van declaratie-expressies afleiden
De veilige context van een declaratievariabele van een out
argument (M(x, out var y)
) of deconstructie ((var x, var y) = M()
) is de kleinste van het volgende:
- bellercontext
- Als de uit-variabele is gemarkeerd als
scoped
, dan betreft het een declaratieblok (dwz functielid of nauwer). - Als het type van de uit-variabele
ref struct
is, moet u rekening houden met alle argumenten van de omvattende aanroep, inclusief de ontvanger.-
veilige context van een argument waarbij de bijbehorende parameter niet
out
is en veilige context heeft van alleen-retourneren of breder - ref-safe-context van een argument waarbij de bijbehorende parameter ref-safe-context heeft van alleen-retourneren of breder
-
veilige context van een argument waarbij de bijbehorende parameter niet
Zie ook Voorbeelden van afgeleide safe-context van declaratie-expressies.
Parameters impliciet scoped
Over het algemeen zijn er twee ref
locatie die impliciet als scoped
worden gedeclareerd:
-
this
op eenstruct
-exemplaarmethode -
out
parameters
De regels voor een veilige context worden geschreven in termen van scoped ref
en ref
. Voor ref-veilige contextdoeleinden is een in
parameter gelijk aan ref
en out
gelijk is aan scoped ref
. Zowel in
als out
worden alleen specifiek aangeroepen wanneer het belangrijk is voor de semantische regel. Anders worden ze beschouwd als respectievelijk ref
en scoped ref
.
Bij het bespreken van de ref-safe-context van argumenten die overeenkomen met in
parameters, worden ze gegeneraliseerd als ref
argumenten in de specificatie. In het geval dat het argument een lwaarde is, is de ref-safe-context die van de lwaarde, anders is het functielid. Nogmaals, in
zal hier alleen worden aangeroepen wanneer het belangrijk is voor de semantiek van de huidige regel.
Alleen-voor-terugkeer veilige context
nl-NL: Het ontwerp vereist ook de introductie van een nieuwe veilige context: alleen-terugkeer. Dit is vergelijkbaar met aanroeperscontext in die zin dat het kan worden geretourneerd, maar het kan alleen worden geretourneerd via een return
verklaring.
De details van alleen-retourneren is dat het een context is die groter is dan functielid maar kleiner is dan aanroepercontext. Een expressie die aan een return
instructie wordt verstrekt, moet ten minste alleen-retourneren-zijn. Daardoor vallen de meeste bestaande regels weg. Toewijzing in een ref
-parameter van een expressie met een -veilige context van alleen-retourneren mislukt omdat het kleiner is dan de ref
-parameter's -veilige context, die een -aanroepercontextis. De noodzaak van deze nieuwe uitvluchtcontext wordt hieronder besproken .
Er zijn drie locaties die standaard alleen-retourneren:
- Een parameter
ref
ofin
heeft een ref-safe-context van alleen voor retournering. Dit wordt gedeeltelijk gedaan voorref struct
om onzinnige cyclische toewijzingsproblemen te voorkomen. Het wordt echter uniform gedaan om het model te vereenvoudigen en compatibiliteitswijzigingen te minimaliseren. - Een
out
parameter voor eenref struct
zal veilige context van alleen-terugkeerhebben. Hierdoor kunnen retour enout
even expressief zijn. Dit heeft geen onzinnig cyclische toewijzingsprobleem omdatout
implicietscoped
is, zodat de ref-safe-context nog steeds kleiner is dan de veilige context. - Een
this
parameter voor eenstruct
constructor heeft een veilige context van alleen-retourneren. Dit valt weg omdat het gemodelleerd is als eenout
-parameter.
Elke expressie of instructie die expliciet een waarde van een methode of lambda retourneert, moet een veilige contexthebben, en, indien van toepassing, een ref-safe-context, van ten minste alleen-retourneren. Dit omvat return
instructies, leden met expressies en lambda-expressies.
Evenzo moet elke toewijzing aan een out
een veilige context hebben van ten minste alleen-voor-terugkeer. Dit is echter geen speciaal geval, dit volgt alleen uit de bestaande toewijzingsregels.
Opmerking: Een expressie waarvan het type geen ref struct
is, heeft altijd een veilige context van aanroepercontext.
Regels voor methode-aanroep
De regels voor veilige context voor methode-aanroep worden op verschillende manieren bijgewerkt. De eerste is door de impact die scoped
heeft op argumenten te herkennen. Voor een gegeven argument expr
die wordt doorgegeven aan parameter p
:
- Als
p
scoped ref
is, draagtexpr
bij het beschouwen van argumenten niet bij aan ref-safe-context .- Als
p
scoped
is, draagtexpr
niet bij aan veilige context bij het overwegen van argumenten.- Als
p
out
is, draagtexpr
niet bij aan ref-safe-context of veilige contextvoor meer informatie
De taal "draagt niet bij" betekent dat de argumenten eenvoudigweg niet worden overwogen bij het berekenen van respectievelijk de ref-veilige-context of de veilige-context waarde van de methode. Dat komt doordat de waarden niet kunnen bijdragen aan die levensduur, omdat de scoped
aantekening dit voorkomt.
De regels voor het aanroepen van methoden kunnen nu worden vereenvoudigd. In het geval van struct
hoeft de ontvanger niet langer speciaal te worden behandeld, het is nu gewoon een scoped ref T
. De waarderegels moeten worden gewijzigd om rekening te houden met ref
veld retourneert:
Een waarde die het resultaat is van een methode-aanroep
e1.M(e2, ...)
, waarbijM()
geen ref-to-ref-struct retourneert, heeft een veilige context genomen uit de kleinste van de volgende opties:
- De aanroepercontext
- Wanneer de return een
ref struct
is, dragen alle argumentexpressies bij aan de veilige context.- Wanneer de return een
ref struct
is, wordt de ref-safe-context bijgedragen door alleref
argumentenAls
M()
wel ref-to-ref-struct retourneert, is de veilige context hetzelfde als de veilige context van alle argumenten die ref-to-ref-struct zijn. Het is een fout als er meerdere argumenten zijn met verschillende veilige contexten omdat methodeargumenten moeten overeenkomen met.
De ref
oproepregels kunnen worden vereenvoudigd tot:
Een waarde die het resultaat is van een methode-aanroep
ref e1.M(e2, ...)
, waarbijM()
geen ref-to-ref-struct retourneert, is ref-safe-context de smalste van de volgende contexten:
- De aanroepercontext
- De veilige context bijgedragen door alle argumentexpressies
- De ref-safe-context bijgedragen door alle
ref
argumentenIndien
M()
inderdaad ref-to-ref-struct retourneert, is de ref-safe-context de smalste ref-safe-context die door alle argumenten wordt bijgedragen die ref-to-ref-struct zijn.
Met deze regel kunnen we nu de twee varianten van gewenste methoden definiëren:
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);
}
Regels voor object-initialisatiefuncties
De veilige context van een object-initialisatie-expressie is het smalst van:
- De veilige context van de constructor-aanroep.
- De safe-context en ref-safe-context van argumenten naar lid-initialisator-indexeerders die naar de ontvanger kunnen ontsnappen.
- De veilige context van de RHS van toewijzingen in lid-initializers aan niet-readonly setters of ref-safe-context in geval van verw-toewijzing.
Een andere manier om dit te modelleren is te bedenken dat elk argument voor een lid-initialisatiefunctie dat aan de ontvanger kan worden toegewezen, beschouwd kan worden als een argument voor de constructor. Dit komt doordat de initialisatiefunctie van het lid effectief een constructor-aanroep is.
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);
Deze modellering is belangrijk omdat het laat zien dat onze MAMM- speciaal rekening moet houden met ledeninitialisatie. Houd er rekening mee dat dit specifieke geval illegaal moet zijn, omdat het toelaat dat een waarde binnen een smallere veilige context wordt toegewezen aan een bredere context.
Methodeargumenten moeten overeenkomen
De aanwezigheid van ref
velden betekent dat de regels met betrekking tot methodeargumenten moeten worden bijgewerkt, omdat een ref
parameter nu kan worden opgeslagen als een veld binnen een ref struct
argument van de methode. Voorheen hoefde de regel alleen rekening te houden met een andere ref struct
die als veld werd opgeslagen. De gevolgen hiervan worden besproken in de compatibiliteitsoverwegingen. De nieuwe regel is ...
Voor elke methode-aanroep
e.M(a1, a2, ... aN)
- Bereken de smalste veilige context van:
- aanroepercontext
- De veilige -context van alle argumenten
- De ref-safe-context van alle ref-argumenten waarvan de bijbehorende parameters een ref-safe-context hebben van aanroepercontext
- Alle
ref
argumenten vanref struct
typen moeten toewijsbaar zijn met een waarde binnen die veilige context. Dit is een geval waarinref
niet generaliseert omin
enout
te omvatten
Voor elke methode-aanroep
e.M(a1, a2, ... aN)
- Bereken de smalste veilige context van:
- aanroepercontext
- De veilige -context van alle argumenten
- De ref-safe-context van alle ref-argumenten waarvan de bijbehorende parameters niet
scoped
- Alle
out
argumenten vanref struct
typen moeten toewijsbaar zijn met een waarde binnen die veilige context.
Door de aanwezigheid van scoped
kunnen ontwikkelaars de wrijving die deze regel creëert verminderen door parameters te markeren die niet worden geretourneerd als scoped
. Hiermee verwijdert u de argumenten uit (1) in beide bovenstaande gevallen en biedt u meer flexibiliteit voor bellers.
De impact van deze wijziging wordt dieper besproken hieronder. Ontwikkelaars kunnen over het algemeen oproepsites flexibeler maken door niet-ontsnappende ref-achtige waarden te annoteren met scoped
.
Afwijking van parameterbereik
Het scoped
-modifier en [UnscopedRef]
-attribuut (zie hieronder) op parameters hebben ook invloed op onze objectoverschrijvingen, interface-implementaties en delegate
conversieregels. De handtekening voor een override, interface-implementatie of delegate
-conversie kan:
-
scoped
toevoegen aan eenref
- ofin
parameter -
scoped
toevoegen aan eenref struct
parameter -
[UnscopedRef]
verwijderen uit eenout
parameter -
[UnscopedRef]
verwijderen uit eenref
parameter van eenref struct
type
Elk ander verschil met betrekking tot scoped
of [UnscopedRef]
wordt als een afwijking beschouwd.
De compiler zal een diagnose rapporteren voor onveilige scoped mismatches bij overrides, interface-implementaties en delegate-conversies wanneer aan de volgende voorwaarden wordt voldaan:
- De methode heeft een
ref
- ofout
parameter vanref struct
type met een mismatch bij het toevoegen van[UnscopedRef]
(niet verwijderen vanscoped
). (In dit geval is een onzinnige cyclische toewijzing mogelijk, dus er zijn geen andere parameters nodig.) - Of beide zijn waar:
- De methode retourneert een
ref struct
of retourneert eenref
ofref readonly
, of de methode heeft eenref
ofout
parameter vanref struct
type. - De methode heeft ten minste één extra
ref
,in
ofout
parameter, of een parameter vanref struct
type.
- De methode retourneert een
De diagnose wordt in andere gevallen niet gerapporteerd omdat:
- De methoden met dergelijke handtekeningen kunnen de doorgegeven referenties niet vastleggen, dus een afgebakende mismatch is niet gevaarlijk.
- Dit zijn onder andere veelvoorkomende en eenvoudige scenario's (zoals gewone oude
out
-parameters die worden gebruikt in handtekeningen vanTryParse
-methoden) en het rapporteren van bereikverschillen, alleen omdat ze worden gebruikt in taalversie 11 (en dus deout
-parameter een andere reikwijdte heeft), zou verwarring veroorzaken.
De diagnose wordt gerapporteerd als een fout als de niet-overeenkomende handtekeningen beide C#11 ref veilige contextregels gebruiken; anders is de diagnose een waarschuwing.
De bereik-mismatch waarschuwing kan worden gerapporteerd voor een module die is gecompileerd volgens de C#7.2 ref veilige contextregels waarbij scoped
niet beschikbaar is. In sommige gevallen kan het nodig zijn om de waarschuwing te onderdrukken als de andere niet-overeenkomende handtekening niet kan worden gewijzigd.
De scoped
-modificator en [UnscopedRef]
-attribuut hebben ook de volgende gevolgen voor methodehandtekeningen:
- Het kenmerk
scoped
modifier en[UnscopedRef]
heeft geen invloed op het verbergen - Overbelastingen kunnen niet alleen verschillen op
scoped
of[UnscopedRef]
De sectie over ref
veld en scoped
is lang, dus ik wilde afsluiten met een korte samenvatting van de voorgenomen ingrijpende wijzigingen.
- Een waarde met ref-safe-context voor de aanroepcontext kan via het veld
ref
ofref
worden geretourneerd. - Een
out
parameter zou een veilige context hebben in de functieleden .
Gedetailleerde opmerkingen:
- Een
ref
veld kan alleen worden gedeclareerd binnen eenref struct
- Een
ref
veld kan niet worden gedeclareerdstatic
,volatile
ofconst
- Een
ref
-veld heeft geen type datref struct
is - Het generatieproces van de referentieassembly moet de aanwezigheid van een
ref
veld in eenref struct
behouden - Een
readonly ref struct
moet deref
velden declareren alsreadonly ref
- Voor by-ref-waarden moet de
scoped
-modificator vóórin
,out
ofref
staan. - Het document met beveiligingsregels voor spanen wordt bijgewerkt zoals beschreven in dit document
- De nieuwe regels voor een veilige context zullen in werking treden wanneer aan een van de voorwaarden is voldaan.
- De kernbibliotheek bevat de functievlag die ondersteuning voor
ref
velden aangeeft - De
langversion
waarde is 11 of hoger
- De kernbibliotheek bevat de functievlag die ondersteuning voor
Syntaxis
13.6.2 Lokale variabeledeclaraties: toegevoegd 'scoped'?
.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 De for
verklaring: indirect 'scoped'?
van local_variable_declaration
toegevoegd .
13.9.5 De foreach
-instructie: toegevoegd 'scoped'?
.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Argument bevat: 'scoped'?
toegevoegd voor out
declaratievariabele.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
12.7 Deconstruction-expressies:
[TBD]
15.6.2 Methodeparameters: 'scoped'?
toegevoegd aan 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 Delegeringsdeclaraties: 'scoped'?
indirect toegevoegd aan fixed_parameter
.
12.19 Anonieme functie-expressies: toegevoegd 'scoped'?
.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Typen die beperkt zijn bij zonsondergang
De compiler heeft een concept van een verzameling "beperkte typen" die grotendeels ongedocumenteerd is. Deze typen kregen een speciale status omdat er in C# 1.0 geen manier voor algemeen gebruik was om hun gedrag uit te drukken. Met name het feit dat de typen verwijzingen naar de uitvoeringsstack kunnen bevatten. In plaats daarvan had de compiler speciale kennis van hen en beperkte hun gebruik tot manieren die altijd veilig zouden zijn: geen terugkeerwaarden toegestaan, niet gebruiken als array-elementen, niet gebruiken in generics, enzovoort ...
Zodra ref
velden beschikbaar zijn en uitgebreid ter ondersteuning van ref struct
kunnen deze typen correct worden gedefinieerd in C# met behulp van een combinatie van ref struct
en ref
velden. Wanneer de compiler detecteert dat een runtime ondersteuning biedt voor ref
velden, heeft deze dus geen idee meer van beperkte typen. In plaats daarvan worden de typen gebruikt die in de code zijn gedefinieerd.
Ter ondersteuning hiervan worden onze regels voor veilige context voor ref als volgt bijgewerkt:
-
__makeref
wordt behandeld als een methode met de handtekeningstatic TypedReference __makeref<T>(ref T value)
-
__refvalue
wordt behandeld als een methode met de signatuurstatic ref T __refvalue<T>(TypedReference tr)
. De expressie__refvalue(tr, int)
gebruikt effectief het tweede argument als de typeparameter. -
__arglist
als parameter zal een ref-safe-context en veilige context van functielidhebben. -
__arglist(...)
als expressie beschikt over een ref-safe-context en veilige context van functielid.
Conforme runtimes zorgen ervoor dat TypedReference
, RuntimeArgumentHandle
en ArgIterator
worden gedefinieerd als ref struct
. Verdere TypedReference
moet worden weergegeven als een ref
veld op een ref struct
voor elk mogelijk type (deze kan elke waarde opslaan). In combinatie met de bovenstaande regels zorgt u ervoor dat verwijzingen naar de stack niet na hun levensduur ontsnappen.
Opmerking: strikt genomen is dit een compiler-implementatiedetails versus een deel van de taal. Maar gezien de relatie met ref
velden wordt het opgenomen in het taalvoorstel voor eenvoud.
Zonder reikwijdte opgeven
Een van de meest opvallende wrijvingspunten is het niet kunnen retourneren van velden door ref
in het geval van leden van een struct
. Dit betekent dat ontwikkelaars geen ref
retourmethoden/eigenschappen kunnen maken en rechtstreeks velden moeten weergeven. Dit vermindert de bruikbaarheid van ref
-retouren in struct
, waar ze vaak het meest gewenst zijn.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
De rationale voor deze standaardwaarde is redelijk, maar er is niets inherent mis met een struct
ontsnappen this
op basis van verwijzing, het is gewoon de standaardinstelling die wordt gekozen door de regels voor veilige contextverwijzingen.
Om dit op te lossen, biedt de taal het tegenovergestelde van de scoped
levensduuraantekening door een UnscopedRefAttribute
te ondersteunen. Dit kan worden toegepast op elke ref
en het verandert de ref-safe-context in één niveau breder dan de standaardinstelling. Bijvoorbeeld:
UnscopedRef toegepast op | Oorspronkelijke ref-safe-context | Nieuwe ref-safe-context |
---|---|---|
instantie-lid | functielid | alleen retour |
in
/
ref parameter |
alleen retour | bellercontext |
out parameter |
functielid | alleen retour |
Wanneer u [UnscopedRef]
toepast op een instantiemethode van een struct
heeft dit invloed op het wijzigen van de impliciete this
parameter. Dit betekent dat this
fungeert als een niet-geannoteerde ref
van hetzelfde type.
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;
}
De aantekening kan ook worden geplaatst op out
parameters om ze te herstellen naar C# 10 gedrag.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Voor de regels van veilige contextverwijzing wordt een dergelijke [UnscopedRef] out
als een ref
beschouwd. Vergelijkbaar met hoe in
wordt beschouwd als ref
voor levensduurdoeleinden.
De [UnscopedRef]
-aantekening wordt niet toegestaan voor init
leden en constructors binnen struct
. Deze leden zijn al bijzonder met betrekking tot ref
semantiek wanneer ze readonly
leden als veranderlijk zien. Dit betekent dat het brengen van ref
aan die leden, als een eenvoudige ref
, niet ref readonly
. Dit is toegestaan binnen de grenzen van constructors en init
. Het toestaan van [UnscopedRef]
zou een dergelijke ref
op onjuiste wijze buiten de constructor laten ontsnappen en mutaties toestaan nadat de readonly
-semantiek had plaatsgevonden.
Het kenmerktype heeft de volgende definitie:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Gedetailleerde opmerkingen:
- Een instantiemethode of eigenschap die is geannoteerd met
[UnscopedRef]
heeft ref-safe-context vanthis
ingesteld op de aanroepercontext. - Een lid dat is geannoteerd met
[UnscopedRef]
kan geen interface implementeren. - Het is een fout om
[UnscopedRef]
te gebruiken op- Een lid dat niet is gedeclareerd op een
struct
- Een
static
-lid,init
-lid of constructeur van eenstruct
- Een parameter gemarkeerd met
scoped
- Een parameter doorgegeven als waarde
- Een parameter die wordt doorgegeven door een verwijzing die niet impliciet is beperkt
- Een lid dat niet is gedeclareerd op een
ScopedRefAttribute
De scoped
aantekeningen worden verzonden naar metagegevens via het type System.Runtime.CompilerServices.ScopedRefAttribute
kenmerk. Het kenmerk wordt vergeleken met de naamruimte-gekwalificeerde naam, zodat de definitie niet hoeft te worden weergegeven in een specifieke assembly.
Het ScopedRefAttribute
type is alleen bedoeld voor compilergebruik. Het is niet toegestaan in de bron. De typedeclaratie wordt gesynthetiseerd door de compiler als deze nog niet is opgenomen in de compilatie.
Het type heeft de volgende definitie:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
De compiler zal dit kenmerk toepassen op de parameter met scoped
syntax. Dit wordt alleen verzonden wanneer de syntaxis ervoor zorgt dat de waarde verschilt van de standaardstatus.
scoped out
leidt er bijvoorbeeld toe dat er geen kenmerk wordt verzonden.
RefSafetyRulesAttribute
Er zijn verschillende verschillen in de ref safe context regels tussen C#7.2 en C#11. Elk van deze verschillen kan leiden tot wijzigingen die fouten veroorzaken bij het opnieuw compileren met C#11 ten opzichte van verwijzingen die zijn gecompileerd met C#10 of eerder.
- niet-gescoopte
ref
/in
/out
-parameters kunnen buiten een methode-aanroep gebruikt worden als eenref
-veld van eenref struct
in C#11, niet in C#7.2 -
out
parameters impliciet zijn ingesteld in C#11 en niet in C#7.2 -
ref
/in
parameters voorref struct
typen zijn impliciet beperkt in C#11 en niet-beperkt in C#7.2
Om de kans te verkleinen dat wijzigingen fouten veroorzaken bij het opnieuw compileren met C#11, werken we de C#11-compiler bij om de regels voor veilige contextverwijzingen te gebruiken voor het aanroepen van methoden die overeenkomen met de regels die zijn gebruikt voor het analyseren van de methodedeclaratie. Bij het analyseren van een aanroep naar een methode die is gecompileerd met een oudere compiler, gebruikt de C#11-compiler in feite C#7.2 veilige contextregels.
Om dit in te schakelen, verzendt de compiler een nieuw [module: RefSafetyRules(11)]
kenmerk wanneer de module wordt gecompileerd met -langversion:11
of hoger of gecompileerd met een corlib met de functievlag voor ref
velden.
Het kenmerkargument geeft de taalversie aan van de veiligheidscontext regels die werden gebruikt toen de module werd gecompileerd.
De versie is momenteel opgelost op 11
ongeacht de werkelijke taalversie die aan de compiler is doorgegeven.
De verwachting is dat toekomstige versies van de compiler de regels voor veilige contextreferentie bijwerken en attributen met verschillende versies genereren.
Als de compiler een module laadt die een [module: RefSafetyRules(version)]
bevat met een andere version
dan 11
, rapporteert de compiler een waarschuwing voor de niet-herkende versie als er aanroepen naar methoden zijn gedeclareerd in die module.
Wanneer de C#11-compiler een methode-aanroep analyseert:
- Als de module met de methodedeclaratie
[module: RefSafetyRules(version)]
bevat, ongeachtversion
, wordt de methodeaanroep geanalyseerd met C#11-regels. - Als de module met de methodedeclaratie afkomstig is van de bron en is gecompileerd met
-langversion:11
of met een corlib met de functievlag voorref
velden, wordt de methodeaanroep geanalyseerd met C#11-regels. - Als de module die de methodedeclaratie bevat, verwijst naar
System.Runtime { ver: 7.0 }
, wordt de methodeaanroep geanalyseerd volgens de C#11-regels. Deze regel is een tijdelijke beperking voor modules die zijn gecompileerd met eerdere previews van C#11/.NET 7 en worden later verwijderd. - Anders wordt de methode-aanroep geanalyseerd met C#7.2-regels.
Een pre-C#11-compiler negeert alle RefSafetyRulesAttribute
en analyseert alleen methode-aanroepen met C#7.2-regels.
De RefSafetyRulesAttribute
wordt gematcht door namespace-gekwalificeerde naam, zodat de definitie niet hoeft voor te komen in een specifieke assembly.
Het RefSafetyRulesAttribute
type is alleen bedoeld voor compilergebruik. Het is niet toegestaan in de bron. De typedeclaratie wordt gesynthetiseerd door de compiler als deze nog niet is opgenomen in de compilatie.
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;
}
}
Veilige buffers met vaste grootte
Veilige buffers met vaste grootte zijn niet geleverd in C# 11. Deze functie kan worden geïmplementeerd in een toekomstige versie van C#.
De taal zal de beperkingen voor arrays met vaste grootte versoepelen, zodat ze kunnen worden gedeclareerd in veilige code en het elementtype beheerd of onbeheerd kan zijn. Dit maakt typen zoals de volgende toegestaan:
internal struct CharBuffer
{
internal char Data[128];
}
Deze declaraties definiëren, net als hun unsafe
tegenhangers, een reeks N
elementen in het type waarin ze zich bevinden. Deze leden kunnen worden benaderd met een indexer en kunnen ook worden geconverteerd naar Span<T>
en ReadOnlySpan<T>
instanties.
Bij het indexeren in een fixed
buffer van het type T
moet rekening worden gehouden met de readonly
status van de container. Als de container is readonly
, retourneert de indexeerfunctie ref readonly T
anders wordt ref T
geretourneerd.
Het openen van een fixed
buffer zonder een indexeerfunctie heeft geen natuurlijk type, maar het is converteerbaar naar Span<T>
typen. In het geval dat de container readonly
is, wordt de buffer impliciet geconverteerd naar ReadOnlySpan<T>
. Anders kan deze impliciet worden geconverteerd naar Span<T>
of ReadOnlySpan<T>
(de Span<T>
-conversie wordt beschouwd als beter).
Het resulterende Span<T>
exemplaar heeft een lengte die gelijk is aan de grootte die is gedeclareerd op de fixed
buffer. De veilige context van de geretourneerde waarde is gelijk aan de veilige context van de container, net zoals wanneer de back-upgegevens als veld zijn geopend.
Voor elke fixed
-declaratie in een type waarin het elementtype T
is, genereert de taal een overeenkomstige get
-indexeringsmethode waarvan het retourtype ref T
is. De indexeerfunctie wordt geannoteerd met het kenmerk [UnscopedRef]
, omdat de implementatie velden van het declaratietype retourneert. De toegankelijkheid van het lid komt overeen met de toegankelijkheid in het veld fixed
.
De handtekening van de indexeerfunctie voor CharBuffer.Data
is bijvoorbeeld het volgende:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Als de opgegeven index buiten de gedeclareerde grenzen van de fixed
matrix valt, wordt er een IndexOutOfRangeException
gegenereerd. In het geval dat er een constante waarde wordt opgegeven, wordt deze vervangen door een directe verwijzing naar het juiste element. Tenzij de constante buiten de gedeclareerde grenzen valt, in welk geval een compilatietijdfout zou optreden.
Er wordt ook een benoemde accessor gegenereerd voor elke fixed
-buffer die door middel van by-value-get
- en set
-bewerkingen voorziet. Dit betekent dat fixed
buffers meer op bestaande matrixsemantiek lijken door een ref
accessor en byval-get
- en set
-bewerkingen te hebben. Dit betekent dat compilers dezelfde flexibiliteit hebben bij het genereren van code die fixed
-buffers gebruikt als bij het gebruik van arrays. Hierdoor kunnen bewerkingen zoals await
over fixed
buffers eenvoudiger worden verzonden.
Dit heeft ook het extra voordeel dat fixed
-buffers gemakkelijker kunnen worden gebruikt vanuit andere talen. Benoemde indexeerfuncties zijn functies die bestaan sinds de 1.0-versie van .NET. Zelfs talen die een benoemde indexeerfunctie niet rechtstreeks kunnen verzenden, kunnen ze over het algemeen gebruiken (C# is eigenlijk een goed voorbeeld hiervan).
De achtergrondopslag voor de buffer wordt gegenereerd met behulp van het kenmerk [InlineArray]
. Dit is een mechanisme dat wordt besproken in probleem 12320, waardoor het mogelijk is om de volgorde van velden van hetzelfde type efficiënt te declareren. Dit specifieke probleem wordt nog steeds actief besproken, en de verwachting is dat de implementatie van deze functie zal volgen, afhankelijk van hoe die discussie verloopt.
Initializers met ref
waarden in new
en with
expressies
In sectie 12.8.17.3 Object initializers, werken we de grammatica bij naar:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
In de sectie voor with
expressiewerken we de grammatica bij naar:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
De linkeroperand van de toewijzing moet een expressie zijn die is gekoppeld aan een referentieveld.
De rechteroperand moet een expressie zijn die een lvalue oplevert die een waarde van hetzelfde type aangeeft als de linkeroperand.
We voegen een vergelijkbare regel toe aan bij lokale hertoewijzing:
Als de linkeroperand een beschrijfbare verwijzing is (dat wil zeggen dat deze iets anders aanwijst dan een ref readonly
veld), moet de rechteroperand een schrijfbare lvalue zijn.
De escaperegels voor constructor-aanroepen blijven:
Een
new
-expressie die een constructor aanroept, voldoet aan dezelfde regels als een methode-aanroep die wordt geacht het geconstrueerde type te retourneren.
De hierboven bijgewerkte regels van methode-aanroep:
Een rvalue die het gevolg is van een aanroepmethode
e1.M(e2, ...)
heeft veilige context van de kleinste van de volgende contexten:
- De aanroepercontext
- De veilige context bijgedragen door alle argumentexpressies
- Wanneer de terugkeerwaarde een
ref struct
is, wordt ref-safe-context bijgedragen door alleref
argumenten.
Voor een new
-expressie met initialisatoren tellen de initializer-expressies als argumenten (ze dragen hun veilige context) en de ref
initializer-expressies als ref
argumenten (recursief dragen ze hun ref-safe-contextbij).
Wijzigingen in onveilige context
Aanwijzertypen (sectie 23.3) worden uitgebreid zodat beheerde typen kunnen worden gebruikt als verwijzingstype.
Dergelijke aanwijzertypen worden geschreven als een beheerd type, gevolgd door een *
token. Ze geven een waarschuwing.
Het adres van de operator (sectie 23.6.5) is versoepeld om een variabele met een beheerd type als operand te accepteren.
De fixed
-instructie (sectie 23.7) is versoepeld om fixed_pointer_initializer te accepteren die het adres is van een variabele van het beheerde type T
of dat een expressie is van een array_type met elementen van een beheerd type T
.
De initializer voor stacktoewijzing (sectie 12.8.22) is eveneens versoepeld.
Overwegingen
Er zijn overwegingen die andere onderdelen van de ontwikkelingsstack moeten overwegen bij het evalueren van deze functie.
Compatibiliteitsoverwegingen
De uitdaging in dit voorstel zijn de compatibiliteitsimplicaties die dit ontwerp heeft voor onze bestaande veiligheidsregels, of §9.7.2. Hoewel deze regels het concept van een ref struct
met ref
velden volledig ondersteunen, staan ze geen API's toe, behalve stackalloc
, om ref
status vast te leggen die naar de stack verwijst. De ref-veilige contextregels hebben een harde aannameof §16.4.12.8 dat er geen constructor van de vorm Span(ref T value)
bestaat. Dat betekent dat de veiligheidsregels geen rekening houden met een ref
parameter die als een ref
veld kan ontsnappen, waardoor code zoals hieronder wordt toegestaan.
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);
}
Er zijn drie manieren waarop een ref
parameter kan ontsnappen uit een methode-aanroep:
- Op waarde retourneren
- Door
ref
retourneren - Door het
ref
-veld inref struct
dat wordt geretourneerd of doorgegeven alsref
/out
parameter
De bestaande regels maken alleen rekening met (1) en (2). Ze houden geen rekening met (3), daarom worden hiaten zoals het terugsturen van lokale gegevens als ref
-velden niet in aanmerking genomen. Dit ontwerp moet de regels wijzigen om rekening te houden met (3). Dit heeft een kleine invloed op de compatibiliteit voor bestaande API's. Dit heeft specifiek invloed op API's met de volgende eigenschappen.
- Een
ref struct
in de handtekening hebben- Waarbij
ref struct
een retourtype is enref
ofout
een parameter. - Heeft een extra
in
ofref
parameter met uitzondering van de ontvanger
- Waarbij
In C# 10 hoefden aanroepers van dergelijke API's zich nooit zorgen te maken over het feit dat de ref
input van de status voor de API zou kunnen worden opgenomen als een ref
veld. Dat maakte het mogelijk dat er verschillende patronen veilig in C# 10 konden bestaan, die onveilig zullen zijn in C# 11 vanwege de mogelijkheid voor de ref
-staat om te ontsnappen als een ref
-veld. Bijvoorbeeld:
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]);
}
De impact van dit compatibiliteitsonderbreking is naar verwachting zeer klein. De door de API-structuur veroorzaakte impact had weinig zin bij gebrek aan ref
-velden, waardoor het onwaarschijnlijk is dat klanten er veel van hebben gemaakt. Experimenten waarbij hulpprogramma's worden gebruikt om de structuur van deze API te herkennen in bestaande opslagplaatsen, ondersteunen die bewering. De enige opslagplaats met aanzienlijke aantallen van deze vorm is dotnet/runtime en dat komt doordat die opslagplaats ref
velden kan maken via het ByReference<T>
intrinsieke type.
Zelfs zo moet het ontwerp rekening houden met dergelijke API's, omdat het een geldig patroon uitdrukt, maar niet een gemeenschappelijk patroon. Daarom moet het ontwerp ontwikkelaars de hulpprogramma's geven om de bestaande levensduurregels te herstellen bij het upgraden naar C# 10. Het moet mechanismen bieden waarmee ontwikkelaars specifiek ref
-parameters kunnen markeren als niet-ontsnappingsmogelijk door ref
- of ref
-veld. Hierdoor kunnen klanten API's definiëren in C# 11 met dezelfde C# 10-aanroepregels.
Referentieassemblies
Een referentieassembly voor een compilatie met behulp van functies die in dit voorstel worden beschreven, moet de elementen behouden die informatie over de veilige context overbrengen. Dit betekent dat alle kenmerken voor levensduuraantekeningen in de oorspronkelijke positie moeten worden bewaard. Elke poging om ze te vervangen of weg te laten, kan leiden tot ongeldige referentieassemblies.
Het weergeven van ref
velden is genuanceerder. In het ideale geval wordt een ref
veld weergegeven in een referentieassembly, net zoals elk ander veld. Een ref
veld vertegenwoordigt echter een wijziging in de metagegevensindeling en kan problemen veroorzaken met hulpprogrammaketens die niet worden bijgewerkt om inzicht te krijgen in deze metagegevenswijziging. Een concreet voorbeeld is C++/CLI die waarschijnlijk een fout veroorzaakt als er een ref
veld wordt gebruikt. Daarom is het handig als de ref
-velden kunnen worden weggelaten van referentie-assemblies in onze basisbibliotheken.
Een ref
veld zelf heeft geen invloed op regels voor veilige refcontext. Als concreet voorbeeld, stel dat de bestaande Span<T>
-definitie wordt omgedraaid om een ref
-veld te gebruiken, dan heeft dit geen invloed op het verbruik. Daarom kan de ref
zelf veilig worden weggelaten. Een ref
veld heeft echter andere gevolgen voor het verbruik dat behouden moet blijven:
- Een
ref struct
met eenref
veld wordt nooit beschouwd alsunmanaged
- Het type
ref
veld heeft invloed op oneindige algemene uitbreidingsregels. Dus als het type van eenref
veld een typeparameter bevat die moet worden bewaard
Gezien deze regels is hier een geldige transformatie van de referentieassembly voor een 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
}
Aantekeningen
Levensduur wordt het meest op natuurlijke wijze uitgedrukt met behulp van typen. De levensduur van een bepaald programma is veilig wanneer het type levensduur wordt gecontroleerd. Hoewel de syntaxis van C# impliciet levensduur toevoegt aan waarden, is er een onderliggend typesysteem dat de fundamentele regels hier beschrijft. Het is vaak gemakkelijker om de implicatie van wijzigingen in het ontwerp te bespreken in termen van deze regels, zodat ze hier ter discussie worden opgenomen.
Houd er rekening mee dat dit geen volledige 100% documentatie is. Het documenteren van elk gedrag is hier geen doel. In plaats daarvan is het bedoeld om een algemeen begrip en gemeenschappelijke terminologie vast te stellen door middel waarvan het model, en mogelijke wijzigingen ervan, kunnen worden besproken.
Meestal is het niet nodig om rechtstreeks over levenstypen te praten. De uitzonderingen zijn plaatsen waar de levensduur kan variëren afhankelijk van bepaalde 'instantiërings'-locaties. Dit is een soort polymorfisme en we noemen deze verschillende levensduurn "algemene levensduur", vertegenwoordigd als algemene parameters. C# biedt geen syntaxis voor het uitdrukken van levensduur generics, dus definiëren we een impliciete 'vertaling' van C# naar een uitgebreide, verlaagde taal die expliciete generieke parameters bevat.
In de onderstaande voorbeelden wordt gebruikgemaakt van de benoemde levensduur. De syntaxis $a
verwijst naar een levensduur met de naam a
. Het is een levensduur die op zichzelf geen betekenis heeft, maar een relatie kan krijgen met andere levensduur via de where $a : $b
syntaxis. Hiermee wordt vastgesteld dat $a
converteerbaar is naar $b
. Het kan helpen om dit te zien als het vaststellen dat $a
een levensduur heeft die minstens zo lang is als $b
.
Hieronder vindt u enkele vooraf gedefinieerde levensduurn voor gemak en beknoptheid:
-
$heap
: dit is de levensduur van elke waarde die op de heap bestaat. Deze is beschikbaar in alle contexten en methode-signatures. -
$local
: dit is de levensduur van elke waarde die aanwezig is op de methodestack. Het is in feite een tijdelijke aanduiding voor functielid. Het is impliciet gedefinieerd in methoden en kan worden weergegeven in methodehandtekeningen, met uitzondering van elke uitvoerpositie. -
$ro
: naam plaatsaanduiding voor , alleen -
$cm
: naamaanduiding voor bellercontext
Er zijn enkele vooraf gedefinieerde relaties tussen levensduur:
-
where $heap : $a
voor alle levensduur$a
where $cm : $ro
-
where $x : $local
voor alle vooraf gedefinieerde levensduur. Door de gebruiker gedefinieerde levensduur heeft geen relatie met lokale, tenzij expliciet gedefinieerd.
Levensduurvariabelen wanneer deze zijn gedefinieerd voor typen kunnen invariant of covariant zijn. Deze worden uitgedrukt met dezelfde syntaxis als algemene parameters:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
De levensduurparameter $this
voor typedefinities is niet vooraf gedefinieerd, maar er zijn wel enkele regels aan gekoppeld wanneer deze is gedefinieerd:
- Dit moet de eerste levensduurparameter zijn.
- Het moet covariant zijn:
out $this
. - De levensduur van
ref
velden moet converteerbaar zijn naar$this
- De
$this
levensduur van alle niet-referentievelden moet$heap
of$this
zijn.
De levensduur van een verw wordt uitgedrukt door een levensduurargument aan de verw te geven. Bijvoorbeeld een ref
die verwijst naar de heap wordt uitgedrukt als ref<$heap>
.
Bij het definiëren van een constructor in het model wordt de naam new
gebruikt voor de methode. Het is nodig om een parameterlijst te hebben voor de geretourneerde waarde en de constructorargumenten. Dit is nodig om de relatie tussen constructor-invoer en de samengestelde waarde uit te drukken. In plaats van Span<$a><$ro>
gebruikt het model in plaats daarvan Span<$a> new<$ro>
. Het type this
in de constructor, inclusief levensduur, is de gedefinieerde retourwaarde.
De basisregels voor de levensduur worden gedefinieerd als:
- Alle levensduuren worden syntactisch uitgedrukt als algemene argumenten, die vóór typeargumenten komen. Dit geldt voor vooraf gedefinieerde levensduur, behalve
$heap
en$local
. - Alle typen
T
die niet eenref struct
zijn, hebben impliciet de levensduur vanT<$heap>
. Dit is impliciet. In elk voorbeeld hoeft u geenint<$heap>
te schrijven. - Voor een
ref
veld dat is gedefinieerd alsref<$l0> T<$l1, $l2, ... $ln>
:- Alle levensduur
$l1
tot$ln
moet invariant zijn. - De levensduur van
$l0
moet converteerbaar zijn naar$this
- Alle levensduur
- Voor een
ref
gedefinieerd alsref<$a> T<$b, ...>
moet$b
converteerbaar zijn naar$a
- De
ref
van een variabele heeft een levensduur gedefinieerd door:- Voor een
ref
lokaal, parameter, veld of retour van het typeref<$a> T
de levensduur is$a
-
$heap
voor alle verwijzingstypen en -velden met referentietypen -
$local
voor alles anders
- Voor een
- Een toewijzing of retourzending is legaal wanneer de conversie van het onderliggende type juridisch is
- Levensduren van expressies kunnen expliciet worden gemaakt door gebruik te maken van cast-aantekeningen.
-
(T<$a> expr)
de levensduur van de waarde wordt expliciet$a
voorT<...>
-
ref<$a> (T<$b>)expr
de levensduur van de waarde is$b
voorT<...>
en de levensduur van de referentie is$a
.
-
Voor de levensduurregels wordt een ref
beschouwd als onderdeel van het type expressie voor conversies. Het wordt logisch weergegeven door ref<$a> T<...>
te converteren naar ref<$a, T<...>>
waar $a
covariant is en T
invariant is.
Vervolgens gaan we de regels definiëren waarmee we de C#-syntaxis kunnen toewijzen aan het onderliggende model.
Een type zonder expliciete levensduurparameters wordt behandeld alsof out $this
is gedefinieerd en toegepast op alle velden van dat type. Een type met een ref
veld moet expliciete levensduurparameters definiëren.
Deze regels bestaan ter ondersteuning van onze bestaande invariant die T
kan worden toegewezen aan scoped T
voor alle typen. Dat komt neer op T<$a, ...>
toe te wijzen aan T<$local, ...>
voor alle levensduren die bekend staan als omzetbaar naar $local
. Verder ondersteunt dit andere items, zoals de mogelijkheid om Span<T>
van de heap toe te wijzen aan items op de stapel. Dit sluit typen uit waarbij velden een verschillende levensduur hebben voor niet-referentie waarden, maar dat is de realiteit van C# tegenwoordig. Het wijzigen daarvan zou een significante wijziging van C#-regels vereisen die moeten worden uitgewerkt.
Het type this
voor een type S<out $this, ...>
in een instantiemethode wordt impliciet gedefinieerd als het volgende:
- Voor de normale instantiemethode:
ref<$local> S<$cm, ...>
- Een methode geannoteerd met
[UnscopedRef]
:ref<$ro> S<$cm, ...>
Het ontbreken van een expliciete this
parameter dwingt de impliciete regels hier af. Voor complexe voorbeelden en discussies kunt u schrijven als een static
methode en this
een expliciete parameter maken.
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) { }
}
De syntaxis van de C#-methode wordt op de volgende manieren in kaart gebracht in het model:
-
ref
parameters hebben een ref-levensduur van$ro
- parameters van het type
ref struct
hebben een levensduur van$cm
- ref-returns hebben een ref-levensduur van
$ro
- retourneert van type
ref struct
een waarde met een levensduur van$ro
-
scoped
voor een parameter ofref
verandert de levensduur van de ref in$local
Laten we een eenvoudig voorbeeld bekijken ter illustratie van het model:
ref int M1(ref int i) => ...
// Maps to the following.
ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
// okay: has ref lifetime $ro which is equal to $ro
return ref i;
// okay: has ref lifetime $heap which convertible $ro
int[] array = new int[42];
return ref array[0];
// error: has ref lifetime $local which has no conversion to $a hence
// it's illegal
int local = 42;
return ref local;
}
Laten we nu hetzelfde voorbeeld verkennen met behulp van een 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;
}
Laten we nu eens kijken hoe dit helpt bij het probleem van cyclische zelftoewijzing:
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;
}
}
Laten we nu eens kijken hoe dit helpt bij het probleem met de domme parameter voor vastleggen:
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;
}
}
Openstaande kwesties
Het ontwerp wijzigen om compatibiliteitseinden te voorkomen
Dit ontwerp stelt verschillende compatibiliteitseinden voor met onze bestaande regels voor ref-safe-context. Hoewel wordt aangenomen dat de wijzigingen minimaal van invloed zijn, werd er aanzienlijke aandacht besteed aan een ontwerp dat geen ingrijpende veranderingen had.
Het ontwerp met compatibiliteitsbehoud was echter aanzienlijk complexer dan deze. Om de compatibiliteit van ref
-velden te behouden, zijn verschillende levensduren nodig voor de mogelijkheid om terug te keren door middel van het ref
-veld en het ref
-veld. In wezen moeten we ref-field-safe-context voor alle parameters van een methode bijhouden. Dit moet worden berekend voor alle expressies en bijgehouden in alle waarden vrijwel overal waar ref-safe-context vandaag wordt bijgehouden.
Verder heeft deze waarde relaties met ref-safe-context. Het is bijvoorbeeld onzinnig dat een waarde kan worden geretourneerd als een ref
-veld, maar niet rechtstreeks als ref
. Dat is omdat ref
-velden al door ref
kunnen worden geretourneerd (deref
-toestand in een ref struct
kan door ref
worden geretourneerd, zelfs wanneer de omhullende waarde dat niet kan). Daarom moeten de regels voortdurend worden aangepast om ervoor te zorgen dat deze waarden verstandig zijn ten opzichte van elkaar.
Dit betekent ook dat de taal syntaxis nodig heeft om ref
parameters weer te geven die op drie verschillende manieren kunnen worden geretourneerd: op ref
veld, op ref
en op waarde. De standaardinstelling kan worden geretourneerd door ref
. In de toekomst wordt verwacht dat een natuurlijker verloop, vooral wanneer ref struct
betrokken zijn, via het ref
-veld of ref
plaatsvindt. Dit betekent dat voor nieuwe API's standaard een extra syntaxisaantekening is vereist. Dit is ongewenst.
Deze compatibiliteitswijzigingen hebben echter invloed op methoden met de volgende eigenschappen:
- Een
Span<T>
ofref struct
- Waarbij
ref struct
een retourtype is enref
ofout
een parameter. - Heeft een extra
in
ofref
parameter (exclusief de ontvanger)
- Waarbij
Om inzicht te hebben in de impact is het handig om API's op te splitsen in categorieën:
- Wilt u dat consumenten rekening houden met het feit dat
ref
wordt vastgelegd als eenref
-veld? Een uitstekend voorbeeld zijn deSpan(ref T value)
-constructors - Niet willen dat consumenten rekening houden met het feit dat
ref
wordt vastgelegd als eenref
-veld. Deze categorieën worden echter onderverdeeld in twee categorieën- Onveilige API's. Dit zijn API's binnen de
Unsafe
enMemoryMarshal
typen, waarvanMemoryMarshal.CreateSpan
het meest prominent is. Deze API's leggen deref
onveilig vast, maar ze zijn ook bekend als onveilige API's. - Veilige API's. Dit zijn API's die
ref
parameters voor efficiëntie gebruiken, maar deze worden nergens vastgelegd. Voorbeelden zijn klein, maar één isAsnDecoder.ReadEnumeratedBytes
- Onveilige API's. Dit zijn API's binnen de
Deze wijziging heeft voornamelijk voordelen (1) hierboven. Deze zullen naar verwachting het merendeel van de API's vormen die een ref
nemen en een ref struct
retourneren. De wijzigingen hebben negatieve gevolgen voor (2.1) en (2.2), aangezien de bestaande oproepsemantiek wordt verbroken door veranderingen in de levensduurregels.
De API's in categorie (2.1) zijn echter grotendeels geschreven door Microsoft of door ontwikkelaars die het meeste kunnen profiteren van ref
velden (de Tanner's van de wereld). Het is redelijk om ervan uit te gaan dat deze klasse ontwikkelaars bereid zou zijn om een compatibiliteitsbelasting te accepteren bij een upgrade naar C# 11 in de vorm van enkele aantekeningen om de bestaande semantiek te behouden, als ref
-velden in ruil daarvoor worden geboden.
De API's in categorie (2.2) zijn het grootste probleem. Het is onbekend hoeveel dergelijke API's bestaan en het is onduidelijk of deze meer/minder frequent zouden zijn in code van derden. De verwachting is dat er een zeer klein aantal van hen is, vooral als we de compat break bij out
meenemen. Zoekopdrachten hebben tot nu toe een zeer klein aantal hiervan op public
oppervlakte onthuld. Dit is een moeilijk patroon om naar te zoeken, omdat hiervoor semantische analyse is vereist. Voordat u deze wijziging uitvoert, is een op hulpprogramma's gebaseerde benadering nodig om de veronderstellingen te controleren die van invloed zijn op een klein aantal bekende gevallen.
Voor beide gevallen in categorie (2) is de oplossing eenvoudig. De ref
parameters die niet als vastlegbaar willen worden beschouwd, moeten scoped
toevoegen aan de ref
. In (2.1) dwingt dit waarschijnlijk ook af dat de ontwikkelaar Unsafe
of MemoryMarshal
gebruikt, maar dat wordt verwacht voor onveilige stijl-API's.
Idealiter kan de taal de impact van stille brekende wijzigingen verminderen door een waarschuwing te geven wanneer een API stil in het problematische gedrag valt. Dat zou een methode zijn die zowel een ref
gebruikt, ref struct
retourneert, maar de ref
in de ref struct
niet daadwerkelijk vastlegt. De compiler kan in dat geval een diagnose uitgeven om ontwikkelaars te informeren dat dergelijke ref
moeten worden geannoteerd als scoped ref
.
Beslissing Dit ontwerp kan worden gerealiseerd, maar de resulterende functie is moeilijker te gebruiken, tot het punt waarop het besluit is genomen om een compatibiliteitsbreuk te accepteren.
Decision De compiler geeft een waarschuwing wanneer een methode voldoet aan de criteria, maar de parameter ref
niet als ref
veld vastlegt. Dit moet klanten bij de upgrade waarschuwen over de potentiële problemen die ze veroorzaken.
Trefwoorden versus kenmerken
Dit ontwerp vraagt om het gebruik van kenmerken om aantekeningen te maken bij de nieuwe levensduurregels. Dit kan ook net zo eenvoudig met contextuele trefwoorden zijn gedaan.
[DoesNotEscape]
kan bijvoorbeeld worden toegewezen aan scoped
. Trefwoorden, zelfs de contextuele, moeten over het algemeen voldoen aan een zeer hoge norm voor opname. Ze nemen waardevolle taalruimte in beslag en zijn prominentere onderdelen van de taal. Deze functie, hoewel waardevol, zal een minderheid van C#-ontwikkelaars dienen.
Op het eerste gezicht lijkt dat in het voordeel te zijn van het niet gebruiken van trefwoorden, maar er zijn twee belangrijke punten om rekening mee te houden:
- De aantekeningen zijn van invloed op programma-semantiek. Dat attributen invloed hebben op de semantiek van het programma is een grens die C# liever niet overschrijdt, en het is onduidelijk of dit de eigenschap is die zou rechtvaardigen dat de taal die stap zou moeten maken.
- De ontwikkelaars die deze functie waarschijnlijk gaan gebruiken, overlappen sterk met de ontwikkelaars die functiepointers gebruiken. Deze functie, die ook door een minderheid van ontwikkelaars wordt gebruikt, verantwoordde een nieuwe syntaxis en die beslissing wordt nog steeds als terecht beschouwd.
Dit betekent dat de syntaxis moet worden overwogen.
Een ruwe schets van de syntaxis is:
-
[RefDoesNotEscape]
komt overeen metscoped ref
-
[DoesNotEscape]
komt overeen metscoped
-
[RefDoesEscape]
komt overeen metunscoped
Decision Gebruik syntaxis voor scoped
en scoped ref
; gebruik attribuut voor unscoped
.
Vaste buffer-locals toestaan
Dit ontwerp biedt veilige fixed
buffers die elk type kunnen ondersteunen. Een mogelijke uitbreiding is dat dergelijke fixed
buffers als lokale variabelen kunnen worden gedeclareerd. Hierdoor kunnen een aantal bestaande stackalloc
bewerkingen worden vervangen door een fixed
buffer. Het zou ook de reeks scenario's uitbreiden waarin we stackstijltoewijzingen kunnen hebben, omdat stackalloc
beperkt is tot onbeheerde elementtypen, terwijl fixed
-buffers dat niet zijn.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Dit houdt samen, maar vereist wel dat we de syntaxis voor de lokale bevolking een beetje uitbreiden. Onduidelijk of dit de extra complexiteit waard is of niet. Mogelijk kunnen we nu niet beslissen en later terugkomen als er voldoende behoefte wordt aangetoond.
Voorbeeld van waar dit nuttig zou zijn: https://github.com/dotnet/runtime/pull/34149
Besluit dit voorlopig uitstellen
Modreqs gebruiken of niet
Er moet een beslissing worden genomen of methoden die zijn gemarkeerd met nieuwe levensduur-attributen, wel of niet omgezet zouden moeten worden in modreq
in uitvoer. Er zou effectief een 1:1 mapping tussen aantekeningen en modreq
zijn als deze aanpak zou worden gevolgd.
De reden voor het toevoegen van een modreq
is dat de kenmerken de semantiek van regels voor ref-veilige context wijzigen. Alleen talen die deze semantiek begrijpen, moeten de betreffende methoden aanroepen. Verder, wanneer ze worden toegepast op OHI-scenario's, vormen de levensduren een contract dat alle afgeleide methoden moeten implementeren. Het bestaan van de aantekeningen zonder modreq
kan leiden tot situaties waarin virtual
methodiketens met conflicterende levensduuraantekeningen worden geladen (dit kan gebeuren als slechts één deel van virtual
-keten wordt gecompileerd en het andere niet).
In het eerste referentiecontextwerk is geen gebruikgemaakt van modreq
, maar in plaats daarvan werd gebruikgemaakt van talen en het framework om dit te begrijpen. Tegelijkertijd vormen alle elementen die bijdragen aan de regels voor de ref-veilige context een sterk onderdeel van de methodehandtekening: ref
, in
, ref struct
, enzovoort ... Elke wijziging in de bestaande regels van een methode resulteert daarom al in een binaire wijziging in de handtekening. Om de nieuwe levensduurannotaties dezelfde impact te geven, zullen zij modreq
handhaving nodig hebben.
Het probleem is of dit overkill is. Het heeft wel de negatieve invloed dat het flexibeler maken van handtekeningen, bijvoorbeeld het toevoegen van [DoesNotEscape]
aan een parameter, resulteert in een binaire compatibiliteitswijziging. Dat levert het nadeel op dat frameworks zoals BCL waarschijnlijk in de loop van de tijd dergelijke handtekeningen niet kunnen versoepelen. Het kan worden beperkt tot een bepaalde mate door een benadering te nemen die de taal doet met in
parameters en alleen modreq
in virtuele posities toe te passen.
Decision Gebruik modreq
niet in metagegevens. Het verschil tussen out
en ref
is niet modreq
, maar ze hebben nu verschillende waarden voor ref-veilige context. Er is hier geen echt voordeel aan het halfslachtig afdwingen van de regels met modreq
.
Multidimensionale vaste buffers toestaan
Moet het ontwerp voor fixed
buffers worden uitgebreid naar multidimensionale stijl arrays? Het is in wezen mogelijk om declaraties als volgt toe te staan:
struct Dimensions
{
int array[42, 13];
}
Beslissing Nu niet toestaan
In strijd met de afgebakende grenzen
De runtime repository heeft verschillende niet-openbare API's die ref
parameters vastleggen als ref
velden. Deze zijn onveilig omdat de levensduur van de resulterende waarde niet wordt bijgehouden. Bijvoorbeeld de Span<T>(ref T value, int length)
constructor.
Het merendeel van deze API's zal er waarschijnlijk voor kiezen om de juiste levensduurregistratie te hebben voor het resultaat, wat eenvoudig bereikt wordt door een update naar C# 11. Een paar willen echter hun huidige semantiek behouden om de retourwaarde niet bij te houden, omdat hun hele intentie onveilig is. De meest opvallende voorbeelden zijn MemoryMarshal.CreateSpan
en MemoryMarshal.CreateReadOnlySpan
. Dit wordt bereikt door de parameters te markeren als scoped
.
Dit betekent dat de runtime een vastgesteld patroon nodig heeft voor het onveilig verwijderen van scoped
uit een parameter:
-
Unsafe.AsRef<T>(in T value)
kan het bestaande doel uitbreiden door over te schakelen naarscoped in T value
. Hierdoor kunnen zowelin
alsscoped
worden verwijderd uit parameters. Het wordt vervolgens de universele methode voor het "verwijderen van ref-veiligheid" - Introduceer een nieuwe methode waarvan het hele doel is om
scoped
te verwijderen:ref T Unsafe.AsUnscoped<T>(scoped in T value)
. Hierdoor wordt ookin
verwijderd, omdat, als dit niet zo zou zijn, bellers nog steeds een combinatie van methode-aanroepen nodig hebben om de referentie-veiligheid te verwijderen, op welk punt de bestaande oplossing waarschijnlijk voldoende is.
Wordt dit standaard niet weergegeven?
Het ontwerp heeft slechts twee locaties die standaard zijn scoped
:
-
this
isscoped ref
-
out
isscoped ref
De beslissing over out
is het aanzienlijk verminderen van de belasting van ref
velden en tegelijkertijd is het een meer natuurlijke standaardwaarde. Hiermee kunnen ontwikkelaars out
beschouwen als gegevens die alleen naar buiten stromen, terwijl als het ref
is, de regels rekening moeten houden met gegevensstromen in beide richtingen. Dit leidt tot aanzienlijke verwarring bij ontwikkelaars.
De beslissing over this
is ongewenst omdat het betekent dat een struct
een veld niet kan retourneren door ref
. Dit is een belangrijk scenario voor ontwikkelaars met hoge prestaties en het kenmerk [UnscopedRef]
is in wezen toegevoegd voor dit ene scenario.
Trefwoorden hebben hoge eisen en het toevoegen ervan voor slechts één scenario is verdacht. Daarom werd nagedacht of we dit trefwoord helemaal konden vermijden door this
gewoon standaard ref
te maken en niet scoped ref
. Alle leden die this
nodig hebben om scoped ref
kunnen dit doen door de methode scoped
te markeren (aangezien een methode kan worden gemarkeerd readonly
om vandaag een readonly ref
te maken).
Op een normale struct
is dit meestal een positieve verandering, omdat het alleen compatibiliteitsproblemen introduceert wanneer een lid een ref
returnwaarde heeft. Er zijn zeer weinig van deze methoden en een hulpprogramma kan deze herkennen en ze snel converteren naar scoped
leden.
Op een ref struct
deze wijziging leidt tot aanzienlijk grotere compatibiliteitsproblemen. Houd rekening met het volgende:
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;
}
}
In wezen zou het betekenen dat alle instantiemethode-aanroepen op veranderlijkeref struct
lokale bevolking illegaal zou zijn, tenzij de lokalen verder zijn gemarkeerd als scoped
. De regels moeten rekening houden met het geval waarin velden opnieuw zijn toegewezen aan andere velden in this
. Een readonly ref struct
heeft dit probleem niet omdat de readonly
eigenschap het opnieuw toewijzen van referenties verhindert. Toch zou dit een significante wijziging zijn die de achterwaartse compatibiliteit breekt, omdat dit van invloed zou zijn op vrijwel elke bestaande veranderlijke ref struct
.
Een readonly ref struct
is echter nog steeds problematisch zodra we uitbreiden om ref
velden bij ref struct
te betrekken. Dit maakt hetzelfde basisprobleem mogelijk door de opname te verplaatsen naar de waarde van het ref
veld:
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);
}
}
Er is nagedacht over het idee dat this
verschillende standaardinstellingen hebben op basis van het type van struct
of een lid. Bijvoorbeeld:
-
this
alsref
:struct
,readonly ref struct
ofreadonly member
-
this
alsscoped ref
:ref struct
ofreadonly ref struct
met hetref
-veld naarref struct
Dit minimaliseert compatibiliteitsonderbrekingen en maximaliseert flexibiliteit, maar ten koste van het compliceren van het verhaal voor klanten. Het probleem wordt ook niet volledig opgelost omdat toekomstige functies, zoals veilige fixed
buffers, vereisen dat een veranderlijke ref struct
ref
retourneert voor velden die niet alleen door dit ontwerp werken, omdat deze in de scoped ref
categorie zouden vallen.
Decision Keep this
as scoped ref
. Dat betekent dat de voorgaande sneaky-voorbeelden compilerfouten produceren.
refvelden naar refstruct
Met deze functie wordt een nieuwe set regels voor ref-veilige context geopend, omdat hiermee een ref
veld naar een ref struct
kan verwijzen. Deze algemene aard van ByReference<T>
betekende dat de runtime tot nu toe geen dergelijke constructie kon hebben. Als gevolg hiervan worden al onze regels geschreven onder de veronderstelling dat dit niet mogelijk is. De functie ref
veld gaat grotendeels niet over het maken van nieuwe regels, maar het codificeren van de bestaande regels in ons systeem. Het toestaan van ref
velden tot ref struct
vereist dat we nieuwe regels codificeren, omdat er verschillende nieuwe scenario's zijn om rekening mee te houden.
De eerste is dat een readonly ref
nu in staat is om ref
status op te slaan. Bijvoorbeeld:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Dit betekent dat bij het nadenken over methodeargumenten, regels moeten overeenkomen, waarbij we moeten overwegen dat readonly ref T
mogelijke methodeuitvoer kan zijn wanneer T
mogelijk een ref
veld heeft voor een ref struct
.
Het tweede probleem is dat taal moet rekening houden met een nieuw type veilige context: ref-field-safe-context. Alle ref struct
die een ref
veld transitief bevatten, hebben een ander escapebereik dat de waarden in de ref
veld(en) vertegenwoordigt. In het geval van meerdere ref
velden kunnen ze gezamenlijk worden bijgehouden als één waarde. De standaardwaarde voor deze parameters is oproepercontext.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Deze waarde is niet gerelateerd aan de veilige context van de container; omdat de containercontext kleiner wordt, heeft dit geen invloed op de ref-field-safe-context van de ref
veldwaarden. Verder kan de veilige context van het referentieveld nooit kleiner zijn dan de veilige context van de container.
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];
}
Deze ref-field-safe-context heeft in wezen altijd al bestaan. Tot nu toe konden ref
velden alleen verwijzen naar normale struct
vandaar dat de velden triviaal zijn samengevouwen tot aanroepercontext. Ter ondersteuning van ref
velden om onze bestaande regels te ref struct
, moet worden bijgewerkt om rekening te houden met deze nieuwe ref-safe-context.
Ten derde moeten de regels voor de aanwijzing van refs worden bijgewerkt om er voor te zorgen dat we de context van het ref-veld niet schenden voor de waarden. Voor x.e1 = ref e2
, waarbij het type van e1
een ref struct
is, moet de ref-field-safe-context gelijk zijn.
Deze problemen zijn zeer oplosbaar. Het compilerteam heeft een aantal versies van deze regels geschetst en ze komen grotendeels voort uit onze bestaande analyse. Het probleem is dat er geen gebruikscode is voor dergelijke regels die helpt hun juistheid en bruikbaarheid te bewijzen. Dit maakt ons erg aarzelend om ondersteuning toe te voegen vanwege de angst dat we verkeerde standaardwaarden kiezen en de runtime terugzetten in de bruikbaarheidshoek wanneer het hiervan profiteert. Deze zorg is bijzonder sterk omdat .NET 8 ons waarschijnlijk in deze richting duwt met allow T: ref struct
en Span<Span<T>>
. De regels zouden beter worden geschreven in combinatie met de consumptiecode.
Decision Uitstel waardoor ref
veld kan ref struct
tot .NET 8 waar we scenario's hebben die de regels rond deze scenario's kunnen bepalen. Dit is niet geïmplementeerd vanaf .NET 9
Wat maakt C# 11.0?
De functies die in dit document worden beschreven, hoeven niet in één pas te worden geïmplementeerd. In plaats daarvan kunnen ze worden geïmplementeerd in fasen in verschillende taalreleases in de volgende buckets:
-
ref
velden enscoped
[UnscopedRef]
-
ref
velden naarref struct
- Typen die beperkt zijn bij zonsondergang
- buffers met vaste grootte
Wat in welke release wordt geïmplementeerd, is slechts een afbakeningsactiviteit.
Beslissing Alleen (1) en (2) hebben C# 11.0 gehaald. De rest wordt overwogen in toekomstige versies van C#.
Toekomstige overwegingen
Geavanceerde levensduuraantekeningen
De levensduuraantekeningen in dit voorstel zijn beperkt omdat ze ontwikkelaars in staat stellen het standaard ontsnappingsgedrag van waarden te wijzigen of te voorkomen. Dit voegt krachtige flexibiliteit toe aan ons model, maar het verandert niet de set relaties die kunnen worden uitgedrukt. In de kern is het C#-model nog steeds effectief binair: kan een waarde worden geretourneerd of niet?
Hierdoor kunnen beperkte levensduurrelaties worden begrepen. Een waarde die niet kan worden geretourneerd vanuit een methode heeft bijvoorbeeld een kleinere levensduur dan een waarde die kan worden geretourneerd vanuit een methode. Er is echter geen manier om de levensduurrelatie tussen waarden te beschrijven die kunnen worden geretourneerd vanuit een methode. In het bijzonder is er geen manier om te zeggen dat één waarde een grotere levensduur heeft dan de andere zodra deze is vastgesteld, beide kunnen worden geretourneerd vanuit een methode. De volgende stap in onze evolutie zou zijn om toe te staan dat dergelijke relaties worden beschreven.
Met andere methoden zoals Rust kan dit type relatie worden uitgedrukt en kunnen daarom complexere scoped
stijlbewerkingen worden geïmplementeerd. Onze taal kan op dezelfde manier profiteren als een dergelijke functie is opgenomen. Op dit moment is er geen motiverende druk om dit te doen, maar als er in de toekomst wel is, kan ons scoped
model op een redelijk eenvoudige manier worden uitgebreid om het op te nemen.
Aan elke scoped
kan een benoemde levensduur worden toegewezen door een algemeen stijlargument toe te voegen aan de syntaxis.
scoped<'a>
is bijvoorbeeld een waarde met levensduur 'a
. Beperkingen zoals where
kunnen vervolgens worden gebruikt om de relaties tussen deze levensduur te beschrijven.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Deze methode definieert twee levensduuren 'a
en 'b
en hun relatie, met name dat 'b
groter is dan 'a
. Hierdoor kan het aanroepende element gedetailleerdere regels bevatten om te bepalen hoe waarden veilig kunnen worden doorgegeven aan methoden, in tegenstelling tot de meer grofkorrelige regels die tegenwoordig bestaan.
Verwante informatie
Kwesties
De volgende problemen hebben allemaal betrekking op dit voorstel:
- 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
Voorstellen
De volgende voorstellen hebben betrekking op dit voorstel:
Bestaande voorbeelden
Dit specifieke codefragment vereist de 'unsafe' modus vanwege problemen met het doorgeven van een Span<T>
die op de stack kan worden geplaatst voor een instantiemethode op een ref struct
. Hoewel deze parameter niet is vastgelegd, moet de taal aannemen dat dit zo is, en veroorzaakt daardoor onnodig wrijving.
Dit fragment wil een parameter wijzigen door elementen van de gegevens te escapen. De ontsnapte gegevens kunnen op de stack worden toegewezen voor efficiëntie. Hoewel de parameter niet is ontsnapt, wijst de compiler deze een veilige context toe van buiten de omsluitmethode omdat deze een parameter is. Dit betekent dat als u stacktoewijzing wilt gebruiken, de implementatie unsafe
moet gebruiken om terug te geven aan de parameter nadat de gegevens zijn verwerkt.
Leuke voorbeelden
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;
}
}
Zuinigheidslijst
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;
}
}
}
}
Voorbeelden en notities
Hieronder ziet u een reeks voorbeelden die laten zien hoe en waarom de regels werken zoals ze dat doen. Inbegrepen zijn verschillende voorbeelden van gevaarlijk gedrag en hoe de regels voorkomen dat ze optreden. Het is belangrijk om deze in gedachten te houden bij het aanbrengen van aanpassingen aan het voorstel.
Herindeling van referenties en aanroeplocaties
Laten zien hoe hertoewijzing en methode aanroep samenwerken.
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;
}
}
Herassignatie van referenties en onveilige escape-sequenties
De reden voor de volgende regel in de regels voor hertoewijzing is in eerste instantie mogelijk niet duidelijk:
e1
moet dezelfde veilige context hebben alse2
Dit komt doordat de levensduur van de waarden die worden verwezen door ref
locaties invariant zijn. De indirectie voorkomt dat we hier enige vorm van variatie toestaan, zelfs niet naar kortere lifetimes. Als smaling is toegestaan, wordt de volgende onveilige code geopend:
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];
}
Voor een ref
naar niet-ref struct
is deze regel triviaal vervuld, aangezien alle waarden dezelfde veilige contexthebben. Deze regel komt alleen in het spel wanneer de waarde een ref struct
is.
Dit gedrag van ref
zal ook belangrijk zijn in een toekomst waarin ref
-velden ref struct
kunnen.
gelimiteerde lokale variabelen
Het gebruik van scoped
op lokalen is vooral nuttig voor codepatronen die waarden conditioneel toewijzen met verschillende veilige -contexten aan lokalen. Dit betekent dat code niet langer hoeft te vertrouwen op initialisatie trucs zoals = stackalloc byte[0]
om een lokale veilige context te definiëren, maar nu gewoon scoped
kan gebruiken.
// 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];
}
Dit patroon komt vaak voor in code op laag niveau. Wanneer ref struct
Span<T>
betrokken is, kan de bovenstaande truc worden gebruikt. Het is echter niet van toepassing op andere ref struct
-typen en kan ertoe leiden dat laag-niveau code moet terugvallen op unsafe
om het onvermogen te omzeilen om de levensduur correct te specificeren.
gescopeerde parameterwaarden
Een bron van herhaalde wrijving in code op laag niveau is dat de standaard uitzondering voor parameters te veel toestaat. Ze zijn veilige context voor de aanroepercontext. Dit is een verstandige standaard omdat deze is afgestemd op de coderingspatronen van .NET als geheel. In code op laag niveau is er echter een groter gebruik van ref struct
en deze standaardinstelling kan wrijving veroorzaken met andere onderdelen van de regels voor veilige contextverwijzing.
Het belangrijkste wrijvingspunt treedt op omdat de methodeargumenten met de regel moeten overeenkomen. Deze regel wordt meestal gebruikt voor exemplaarmethoden op ref struct
waarbij ten minste één parameter ook een ref struct
is. Dit is een algemeen patroon in code op laag niveau, waarbij ref struct
typen vaak gebruikmaken van Span<T>
parameters in hun methoden. Dit gebeurt bijvoorbeeld bij elke schrijfstijl ref struct
die gebruikmaakt van Span<T>
om buffers door te sturen.
Deze regel bestaat om scenario's als de volgende te voorkomen:
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);
}
}
Deze regel bestaat in wezen omdat er in de taal van wordt uitgegaan dat alle invoer voor een methode ontsnappen naar hun maximaal toegestane veilige context. Wanneer er ref
of out
parameters zijn, inclusief de ontvangers, is het mogelijk dat de invoer kan ontsnappen als velden van die ref
waarden (zoals in RS.Set
hierboven).
Hoewel er in de praktijk veel van zulke methoden zijn die ref struct
als parameters doorgeven zonder de intentie om deze in de uitvoer vast te leggen. Het is slechts een waarde die wordt gebruikt binnen de huidige methode. Bijvoorbeeld:
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))
{
...
}
}
}
Om deze code op laag niveau te omzeilen, worden unsafe
trucs toegepast om tegen de compiler te liegen over de levensduur van ref struct
. Dit vermindert de waardepropositie van ref struct
aanzienlijk, omdat het bedoeld is om unsafe
te vermijden en tegelijk hoogpresterende code te blijven schrijven.
Dit is waar scoped
een effectief hulpmiddel is voor ref struct
parameters, omdat deze worden uitgesloten van overweging om te worden geretourneerd door de methode, volgens de bijgewerkte regel dat methodeargumenten moeten overeenkomen met regel. Een ref struct
parameter die wordt verbruikt, maar nooit geretourneerd, kan worden gelabeld als scoped
om oproepsites flexibeler te maken.
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))
{
...
}
}
}
Voorkomen van moeilijke verwijzingstoewijzing bij alleen-lezen mutatie
Wanneer een ref
in een readonly
-veld binnen een constructor of init
-lid wordt opgenomen, is het type ref
, niet ref readonly
. Dit is een langdurig gedrag waarmee code als volgt kan worden gebruikt:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Dat vormt echter een potentieel probleem als een dergelijke ref
in een ref
veld op hetzelfde type kon worden opgeslagen. Het zou directe mutatie van een readonly struct
binnen een instantie-lid mogelijk maken.
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++;
}
}
Het voorstel verhindert dit echter omdat het in strijd is met de regels voor veilige contexten. Houd rekening met het volgende:
- De ref-safe-context van
this
is functielid, en de safe-context is de aanroepercontext. Dit zijn beide standaardinstellingen voorthis
in eenstruct
-element. - De ref-safe-context van
i
is functielid. Dit valt buiten de regels voor de levensduur van het veld. Specifiek regel 4.
Op dat moment is de regel r = ref i
illegaal volgens de regels voor hertoewijzing .
Deze regels waren niet bedoeld om dit gedrag te voorkomen, maar doen dit als neveneffect. Het is belangrijk om dit in gedachten te houden voor elke toekomstige regelupdate om de impact op scenario's als deze te evalueren.
Onzinnige cyclische toewijzing
Een aspect waarmee dit ontwerp worstelt, is hoe vrij een ref
kan worden geretourneerd vanuit een methode. Het toestaan dat alle ref
net zo vrij worden geretourneerd als normale waarden, is waarschijnlijk wat de meeste ontwikkelaars intuïtief verwachten. Het maakt echter pathologische scenario's mogelijk waarmee de compiler rekening moet houden bij het berekenen van de veiligheid van refs. Houd rekening met het volgende:
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;
}
}
Dit is geen codepatroon dat ontwikkelaars verwachten te gebruiken. Maar wanneer een ref
kan worden geretourneerd met dezelfde levensduur als een waarde, is het toegestaan volgens de regels. De compiler moet rekening houden met alle juridische gevallen bij het evalueren van een methode-aanroep en dit leidt ertoe dat dergelijke API's effectief onbruikbaar zijn.
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);
}
Om deze API's bruikbaar te maken, zorgt de compiler ervoor dat de ref
levensduur van een ref
parameter kleiner is dan de levensduur van verwijzingen in de bijbehorende parameterwaarde. Dit is de reden waarom ref-safe-context voor ref
tot ref struct
als alleen-retourneren moet zijn en out
als aanroepercontextmoet zijn. Dat voorkomt cyclische toewijzing vanwege het verschil in levensduur.
Houd er rekening mee dat [UnscopedRef]
de ref-safe-context van ref
waarden aan ref struct
aanroepercontext bevordert en daarom cyclische toewijzing mogelijk maakt en een viral gebruik van [UnscopedRef]
de oproepketen afdrijt:
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;
}
}
Op dezelfde manier staat [UnscopedRef] out
een cyclische toewijzing toe, omdat de parameter zowel als ref-safe-context heeft van alleen-retourneren-.
Het bevorderen van [UnscopedRef] ref
naar in de aanroepen context is nuttig wanneer het type . Er moet op worden gelet dat we de regels eenvoudig willen houden, zodat ze geen onderscheid maken tussen verwijzingen naar ref en niet-ref-structuren.
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;
}
}
In termen van geavanceerde aantekeningen maakt het [UnscopedRef]
ontwerp het volgende:
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 niet diepgaand zijn via ref-velden
Bekijk het onderstaande codevoorbeeld:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Bij het ontwerpen van de regels voor ref
velden op readonly
instanties in een vacuüm kunnen de regels geldig zodanig worden ontworpen dat het bovenstaande legaal of illegaal is. In wezen kan readonly
geldig diep gaan door een ref
veld of kan het enkel van toepassing zijn op de ref
. Alleen toepassen op ref
voorkomt hernieuwde toewijzing van de referentie, maar staat normale toewijzing toe die de waarde waarnaar verwezen wordt wijzigt.
Dit ontwerp bestaat echter niet in een vacuüm, maar het ontwerpt regels voor typen die al effectief ref
velden hebben. De meest prominente hiervan, Span<T>
, heeft al een sterke afhankelijkheid van readonly
niet diep is hier. Het primaire scenario is de mogelijkheid om toe te wijzen aan het ref
veld via een readonly
instantie.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Dit betekent dat we de ondiepe interpretatie van readonly
moeten kiezen.
Modelleringsconstructeurs
Een subtiele ontwerpvraag is: Hoe zijn constructorslichamen gemodelleerd voor referentieveiligheid? Hoe wordt de volgende constructor in wezen geanalyseerd?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Er zijn ongeveer twee benaderingen:
- Model als een
static
methode waarbijthis
een lokale locatie is waar de veilige context is aanroepercontext - Model als een
static
methode waarbijthis
eenout
parameter is.
Een constructor moet verder voldoen aan de volgende invarianten:
- Zorg ervoor dat
ref
parameters kunnen worden vastgelegd alsref
velden. - Zorg ervoor dat
ref
naar velden vanthis
niet kan worden ontsnapt viaref
parameters. Dat zou lastige toewijzing van referentiesschenden.
Het doel is om het formulier te kiezen dat voldoet aan onze invarianten zonder dat er speciale regels voor constructors zijn opgenomen. Aangezien het beste model voor constructors is om this
te beschouwen als een out
parameter. De alleen aard van de out
geeft ons de mogelijkheid om te voldoen aan alle hierboven genoemde invarianten zonder speciale behuizing:
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;
}
Methodeargumenten moeten overeenkomen
De methodeargumenten moeten overeenkomen met de regel is een veelvoorkomende bron van verwarring voor ontwikkelaars. Het is een regel met een aantal speciale gevallen die moeilijk te begrijpen zijn, tenzij u bekend bent met de redenering achter de regel. Om de redenen voor de regel beter te begrijpen, vereenvoudigen we ref-safe-context en safe-context tot simpelweg context.
Methoden kunnen redelijk vrij de toestand retourneren die als parameters aan hen is doorgegeven. In wezen kan elke bereikbare status die niet aan een scope is gebonden worden geretourneerd (inclusief terugkeer via ref
). Dit kan rechtstreeks worden geretourneerd via een return
instructie of indirect door deze toe te wijzen aan een ref
waarde.
Directe retouren vormen niet veel problemen voor referentie-veiligheid. De compiler hoeft alleen maar alle retourbare invoer voor een methode te bekijken en beperkt vervolgens de retourwaarde tot de minimale context van de invoer. Die retourwaarde gaat vervolgens door de normale verwerking.
Indirecte retouren vormen een aanzienlijk probleem omdat alle ref
zowel een invoer als een uitvoer voor de methode zijn. Deze uitvoer heeft al een bekende -context. De compiler kan geen nieuwe afleiden, maar moet ze op hun huidige niveau overwegen. Dat betekent dat de compiler elke ref
moet bekijken die kan worden toegewezen in de aangeroepen methode, zijn contextevalueert, en vervolgens controleert of geen retourbare invoer naar de methode een kleinere context heeft dan die ref
. Als er een dergelijk geval bestaat, moet de methode-aanroep illegaal zijn omdat deze ref
veiligheid kan schenden.
Methodeargumenten moeten overeenkomen met het proces waarmee de compiler deze veiligheidscontrole bevestigt.
Een andere manier om dit te evalueren, die ontwikkelaars vaak gemakkelijker vinden, is om de volgende oefening uit te voeren:
- Bekijk de methodedefinitie, en identificeer alle plaatsen waar de status indirect kan worden teruggegeven: a. Veranderlijke
ref
parameters die verwijzen naarref struct
b. Veranderlijkeref
-parameters met ref-toewijsbareref
-velden c. Toewijsbareref
parameters ofwelref
velden die naarref struct
verwijzen (recursief overwegen) - Bekijk de aanroepplaats a. Identificeer de contexten die aansluiten op de hierboven genoemde locaties b. Identificeer de contexten van alle invoer voor de methode die retourneerbaar zijn (en niet overeenkomen met
scoped
-parameters).
Als een waarde in 2.b kleiner is dan 2.a, moet de methode-aanroep ongeldig zijn. Laten we een paar voorbeelden bekijken om de regels te illustreren:
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);
}
}
Als we kijken naar de oproep naar F0
, laten we (1) en (2) doornemen. De parameters met mogelijk indirect rendement zijn a
en b
omdat beide rechtstreeks kunnen worden toegewezen. De argumenten die aan deze parameters voldoen, zijn:
-
a
die overeenkomt metx
dat context van aanroeper-context heeft -
b
dat overeenkomt mety
met context van functielid
De verzameling retourneerbare invoer voor de methode is
-
x
met escape-scope van aanroepercontext -
ref x
met escape-scope van aanroepercontext -
y
met escape-scope van functielid
De waarde ref y
is niet retourneerbaar omdat deze wordt toegewezen aan een scoped ref
daarom wordt deze niet beschouwd als invoer. Maar aangezien er ten minste één invoer is met een kleiner escapebereik (y
argument) dan een van de uitvoerwaarden (x
argument) is de methode-aanroep ongeldig.
Een andere variatie is het volgende:
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);
}
}
Opnieuw worden de parameters met mogelijk indirect rendement a
en b
omdat beide rechtstreeks kunnen worden toegewezen. Maar b
kan worden uitgesloten omdat deze niet verwijst naar een ref struct
kan daarom niet worden gebruikt om ref
status op te slaan. We hebben dus:
-
a
die overeenkomt metx
dat context van aanroeper-context heeft
De verzameling retourneerbare invoeren voor de methode is:
-
x
met context van aanroepercontext -
ref x
met context van aanroepercontext -
ref y
met context van functielid
Aangezien er ten minste één invoer is met een kleiner escapebereik (ref y
argument) dan een van de uitvoerwaarden (x
argument) is de methode-aanroep ongeldig.
Dit is de logica die probeert samen te vatten dat de methode-argumenten moeten overeenkomen met de regel. Het gaat verder omdat het zowel scoped
als een manier beschouwt om invoer uit overwegingen te verwijderen en readonly
als een manier om ref
als uitvoer te verwijderen (kan niet worden toegewezen aan een readonly ref
, zodat het geen bron van uitvoer kan zijn). Deze speciale gevallen voegen complexiteit toe aan de regels, maar dit wordt gedaan ten behoeve van de ontwikkelaar. De compiler probeert alle invoer en uitvoer die hij weet dat niet kan bijdragen aan het resultaat te verwijderen, zodat ontwikkelaars maximale flexibiliteit hebben bij het aanroepen van een lidfunctie. Net als overbelastingsresolutie is het de moeite waard om onze regels complexer te maken wanneer het meer flexibiliteit voor consumenten creëert.
Voorbeelden van afgeleide veilige-context van declaratie-uitdrukkingen
Gerelateerd aan afleiden van declaratie-expressies.
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).
}
}
Houd er rekening mee dat de lokale context die het resultaat is van de scoped
modifier de smalste is die mogelijk kan worden gebruikt voor de variabele. Dit betekent dat de expressie verwijst naar variabelen die alleen in een beperktere context worden gedeclareerd dan de expressie.
C# feature specifications