Verbesserungen bei Strukturen auf niedriger Ebene
Hinweis
Dieser Artikel ist eine Feature-Spezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.
Es kann einige Abweichungen zwischen der Feature-Spezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede sind in den relevanten Language Design Meeting (LDM) Notizen festgehalten.
Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.
Champion Issues: https://github.com/dotnet/csharplang/issues/1147, https://github.com/dotnet/csharplang/issues/6476
Zusammenfassung
Dieser Vorschlag ist eine Zusammenführung mehrerer unterschiedlicher Vorschläge zur struct
Leistungsverbesserung: ref
Felder und die Möglichkeit, Lebensdauerstandards zu überschreiben. Das Ziel besteht darin, ein Design zu entwerfen, das die verschiedenen Vorschläge berücksichtigt, um einen einzigen übergeordneten Funktionssatz für struct
-Verbesserungen auf niedriger Ebene zu schaffen.
Hinweis: In früheren Versionen dieser Spezifikation wurden die Begriffe"ref-safe-to-escape" und"safe-to-escape" verwendet, die in der Spezifikation der Funktion Span safety eingeführt wurden. Das ECMA-Standardkomitee änderte die Bezeichnungen in „ref-safe-context" bzw. „safe-context". Die Werte von safe-context wurden dahingehend verfeinert, dass "declaration-block", "function-member" und "caller-context" einheitlich verwendet werden. Die Speclets hatten unterschiedliche Formulierungen für diese Begriffe verwendet und auch"safe-to-return" als Synonym für"caller-context" benutzt. Diese Spezifikation wurde aktualisiert, um die Begriffe im C# 7.3-Standard zu verwenden.
Nicht alle in diesem Dokument beschriebenen Features wurden in C# 11 implementiert. C# 11 umfasst:
ref
-Felder undscoped
[UnscopedRef]
Diese Features bleiben offene Vorschläge für eine zukünftige Version von C#:
ref
-Felder zuref struct
- Sunset-beschränkte Typen
Motivation
Frühere Versionen von C# haben der Sprache eine Reihe von leistungsorientierten Funktionen auf niedriger Ebene hinzugefügt: ref
-Rückgaben, ref struct
, Funktionszeiger usw. ... Diese ermöglichten es .NET-Entwicklern, sehr leistungsfähigen Code zu schreiben, während sie weiterhin die C#-Sprachregeln für Typ- sowie Speichersicherheit nutzen. Außerdem wurde die Erstellung grundlegender Leistungstypen in den .NET-Bibliotheken wie Span<T>
erlaubt.
Nachdem diese Features im .NET-Ökosystem an Bedeutung gewonnen haben, haben uns Entwickler, sowohl intern als auch extern, Informationen zu verbleibenden Reibungspunkten im Ökosystem bereitgestellt. Stellen, an denen sie immer noch auf den unsafe
-Code zurückgreifen müssen, um ihre Arbeit zu erledigen, oder bei denen die Laufzeit besondere Fallbehandlungen für Typen wie Span<T>
erfordert.
Heute wird Span<T>
mithilfe des internal
-TypsByReference<T>
erreicht, den die Laufzeit effektiv als ein ref
-Feld behandelt. Dies bietet zwar den Vorteil von ref
-Feldern, hat jedoch den Nachteil, dass die Sprache keine Sicherheitsüberprüfung durchführt, wie es bei anderen Verwendungen von ref
der Fall ist. Außerdem kann nur dotnet/runtime diesen Typ verwenden, da er internal
ist, sodass Drittanbieter keine eigenen Grundtypen basierend auf ref
-Feldern entwerfen können. Ein Teil der Motivation für diese Arbeit besteht darin, ByReference<T>
zu entfernen und korrekte ref
-Felder in allen Codebasen zu verwenden.
Dieser Vorschlag plant, diese Probleme zu beheben, indem er auf unseren bestehenden Features auf niedriger Ebene aufbaut. Konkret zielt es darauf ab:
- Zulassen, dass
ref struct
Typen-ref
-Felder deklarieren. - Zulassen, dass die Laufzeit
Span<T>
vollständig mit dem C#-Typsystem definiert und den Sonderfalltyp wieByReference<T>
entfernt. - Erlauben, dass
struct
-Typenref
zu ihren Feldern zurückzugeben. - Zulassen, dass zur Laufzeit
unsafe
-Verwendungen aufgrund von Einschränkungen der Lebensdauer-Standardwerte entfernt werden. - Zulassen der Deklaration sicherer
fixed
-Puffer für verwaltete und nicht verwaltete Typen instruct
Detailliertes Design
Die Regeln für die ref struct
-Sicherheit werden im -Sicherheitsdokument definiert und verwenden die vorherigen Begriffe. Diese Regeln wurden in §9.7.2 und §16.4.12 in den C#7-Standard übernommen. Dieses Dokument beschreibt die erforderlichen Änderungen an diesem Dokument als Ergebnis dieses Vorschlags. Sobald diese Änderungen als genehmigtes Feature akzeptiert wurden, werden diese Änderungen in dieses Dokument integriert.
Sobald dieses Design abgeschlossen ist, lautet unsere Span<T>
-Definition wie folgt:
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;
}
}
Geben Sie Ref-Felder und Scoped an
Mit der Sprache können Entwickler ref
-Felder innerhalb einer ref struct
deklarieren. Dies kann z. B. hilfreich sein, wenn Sie große änderbare struct
-Instanzen kapseln oder leistungsstarke Typen wie Span<T>
in Bibliotheken außerhalb der Laufzeitumgebung definieren.
ref struct S
{
public ref int Value;
}
Ein ref
-Feld wird mithilfe der ELEMENT_TYPE_BYREF
-Signatur in den Metadaten ausgegeben. Dies unterscheidet sich nicht davon, wie wir ref
lokale Variablen oder ref
Argumente ausgeben. Beispielsweise wird ref int _field
als ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4
emittiert. Dies wird erfordern, dass wir ECMA335 aktualisieren, um diesen Eintrag zuzulassen, aber das sollte ziemlich unkompliziert sein.
Entwickler können nach wie vor eine ref struct
mit einem ref
-Feld initialisieren, indem sie den default
-Ausdruck verwenden, wobei alle deklarierten ref
-Felder den Wert null
haben. Jeder Versuch, solche Felder zu verwenden, führt zur Ausgabe einer NullReferenceException
.
ref struct S
{
public ref int Value;
}
S local = default;
local.Value.ToString(); // throws NullReferenceException
Während die C#-Sprache vorgibt, dass ein ref
nicht null
sein kann, ist dies auf Laufzeitebene zulässig und hat gut definierte Semantiken. Entwickler, die ref
-Felder in ihre Typen einführen, müssen sich dieser Möglichkeit bewusst sein und sollten dringend davon abgeraten werden, dieses Detail in den verbrauchenden Code zu verwenden. Stattdessen sollten ref
-Felder mithilfe der Laufzeithilfsprogramme als nicht null überprüft werden, und ausgelöst werden, wenn ein nicht initialisiertes struct
falsch verwendet wird.
ref struct S1
{
private ref int Value;
public int GetValue()
{
if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
{
throw new InvalidOperationException(...);
}
return Value;
}
}
Ein ref
-Feld kann auf folgende Weise mit readonly
-Modifizierern kombiniert werden:
readonly ref
: dies ist ein Feld, das außerhalb eines Konstruktors oder voninit
-Methoden nicht neu zugewiesen werden kann. Er kann jedoch außerhalb dieser Kontexte zugewiesen werden.ref readonly
: dies ist ein Feld, das neu referenziert werden kann, dem aber zu keinem Zeitpunkt ein Wert zugewiesen werden kann. So kann einin
-Parameter einemref
-Feld neu zugewiesen werden.-
readonly ref readonly
: Eine Kombination ausref readonly
undreadonly 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)
}
}
Ein readonly ref struct
erfordert, dass ref
-Felder als readonly ref
deklariert werden. Es besteht keine Anforderung, dass sie als readonly ref readonly
deklariert werden müssen. Dies ermöglicht es einem readonly struct
, indirekte Mutationen über ein solches Feld zu haben, aber das unterscheidet sich nicht von einem readonly
-Feld, das heute auf einen Referenztyp verweist (weitere Details).
Ein readonly ref
wird mithilfe des initonly
-Flags an Metadaten wie jedes andere Feld ausgegeben. Ein ref readonly
-Feld wird mit System.Runtime.CompilerServices.IsReadOnlyAttribute
attribuiert. Ein readonly ref readonly
wird mit beiden Elementen ausgegeben.
Dieses Feature erfordert Laufzeitunterstützung und Änderungen an der ECMA-Spezifikation. Daher werden diese nur aktiviert, wenn das entsprechende Feature-Flag in der Corelib festgelegt wird. Das Problem bezüglich der genauen API wird hier https://github.com/dotnet/runtime/issues/64165 verfolgt
Der Umfang der Änderungen an unseren sicheren Kontextregeln, die notwendig sind, um die ref
-Felder zuzulassen, ist klein und gezielt. Die Regeln berücksichtigen bereits vorhandene ref
-Felder und deren Nutzung durch APIs. Die Änderungen müssen sich nur auf zwei Aspekte konzentrieren: wie sie erstellt werden und wie sie neu zugewiesen werden.
Zunächst müssen die Regeln, die bezugssicheren Kontext-Werte für Felder festlegen, für ref
-Felder wie folgt aktualisiert werden:
Ein Ausdruck in der Form
ref e.F
ref-safe-context wie folgt:
- Wenn
F
einref
-Feld ist, ist sein ref-safe-context der sichere Kontext vone
.- Andernfalls, falls
e
von einem Referenztyp ist, verfügt es über den ref-safe-context im Aufrufer-Kontext.- Andernfalls wird der ref-safe-context aus dem ref-safe-context von
e
übernommen.
Dies stellt keine Regeländerung dar, da die Regeln immer berücksichtigt haben, dass der ref
-Zustand innerhalb eines ref struct
existiert. Tatsächlich hat der ref
-Zustand in Span<T>
immer so funktioniert, und die Verbrauchsregeln berücksichtigen dies korrekt. Die Änderung hier soll lediglich den Entwicklern ermöglichen, direkt auf ref
-Felder zuzugreifen und sicherstellen, dass sie dabei den bestehenden, implizit auf Span<T>
angewandten Regeln folgen.
Dies bedeutet jedoch, dass ref
-Felder als ref
von einem ref struct
zurückgegeben werden können, normale Felder jedoch nicht.
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;
}
Dies mag auf den ersten Blick wie ein Fehler erscheinen, aber dies ist ein bewusster Entwurfspunkt. Allerdings ist dies keine neue Regel, die durch diesen Vorschlag geschaffen wird, sondern es wird vielmehr den bestehenden Regeln Span<T>
Rechnung getragen, sodass Entwickler ihren eigenen ref
Zustand deklarieren können.
Als Nächstes müssen die Regeln für die Neuzuweisung von Referenzen an das Vorhandensein von ref
-Feldern angepasst werden. Das primäre Szenario für die Neuzuweisung von Referenzen ist ref struct
Konstruktoren, die ref
-Parameter in ref
-Felder speichern. Die Unterstützung wird allgemeiner sein, aber dies ist das Kernszenario. Um dies zu ermöglichen, werden die Regeln für die Neuzuweisung von Referenzen wie folgt angepasst, um ref
-Felder zu berücksichtigen.
Regeln für die Neuzuweisung von Referenzen
Der linke Operand des = ref
-Operators muss ein Ausdruck sein, der an eine ref-Lokale Variable, einen ref-Parameter (außer this
), einen out-Parameter, oder ein ref-Feld gebunden ist.
Für eine Neuzuweisung eines Bezugs im Formular
e1 = ref e2
muss beides "true" sein:
e2
muss einen ref-safe-context haben, der mindestens so groß wie der ref-safe-context vone1
ist.e1
muss den gleichen sicheren Kontext wiee2
Hinweis haben.
Das bedeutet, dass der gewünschte Span<T>
-Konstruktor ohne zusätzliche Annotation funktioniert.
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;
}
}
Die Änderung an den Regeln für Ref-Neuzuweisung bedeutet, dass ref
-Parameter jetzt als ref
-Feld in einem Wert von ref struct
aus einer Methode entweichen können. Wie im Abschnitt Kompatibilitätsüberlegungen besprochen, kann dies die Regeln für vorhandene APIs ändern, die nie dafür vorgesehen waren, dass ref
-Parameter als ref
-Feld entkommen sollen. Die Lebensdauerregeln für Parameter basieren ausschließlich auf ihrer Deklaration, nicht auf deren Verwendung. Alle Parameter ref
und in
verfügen über ref-safe-context von caller-context und können damit von ref
oder einem ref
-Feld zurückgegeben werden. Um APIs mit ref
-Parametern zu unterstützen, die flüchtig oder nicht-flüchtig sein können, und damit die C# 10-Aufrufstellensemantik wiederherzustellen, wird die Sprache eingeschränkte Lebenszeitannotationen einführen.
scoped
-Modifizierer
Das Schlüsselwort scoped
wird verwendet, um die Lebensdauer eines Werts einzuschränken. Sie kann auf einen ref
oder einen Wert angewendet werden, der ein ref struct
ist, und hat die Wirkung, die Lebensdauer des ref-safe-context oder des safe-context jeweils auf das Funktionsmitglied einzuschränken. Zum Beispiel:
Parameter oder Lokal | ref-safe-context | sicherer Kontext |
---|---|---|
Span<int> s |
Funktionsmitglied | caller-context |
scoped Span<int> s |
Funktionsmitglied | Funktionsmitglied |
ref Span<int> s |
caller-context | caller-context |
scoped ref Span<int> s |
Funktionsmitglied | caller-context |
In dieser Beziehung kann der ref-safe-context eines Werts niemals breiter als der safe-context sein.
Dadurch können APIs in C# 11 kommentiert werden, sodass sie dieselben Regeln wie C# 10 aufweisen:
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]);
}
Die scoped
-Annotation bedeutet auch, dass der this
-Parameter eines struct
jetzt als scoped ref T
definiert werden kann. Previously it had to be special cased in the rules as ref
parameter that had different ref-safe-context-Regeln galten als für andere ref
-Parameter (siehe alle Verweise auf das Einschließen oder Ausschließen des Empfängers in den Regeln für den sicheren Kontext). Jetzt kann es als allgemeines Konzept in allen Regeln ausgedrückt werden, die sie weiter vereinfachen.
Die scoped
-Anmerkung kann auch auf die folgenden Orte angewendet werden:
- locals: Diese Anmerkung legt die Lebensdauer als safe-context oder ref-safe-context im Fall eines lokalen
ref
auf Funktionsmitglied fest, unabhängig von der Initialisierungsdauer.
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 Verwendungsmöglichkeiten für scoped
für Lokale werden unten erläutert.
Die scoped
-Anmerkung kann an keinem anderen Ort angewendet werden, einschließlich Rückgabewerten, Feldern, Array-Elementen usw. Während scoped
Auswirkungen hat, wenn diese auf ref
, in
oder out
angewendet wird, hat sie nur Auswirkungen, wenn sie auf Werte angewendet wird, die ref struct
sind. Das Vorhandensein von Deklarationen wie scoped int
hat keine Auswirkungen, da es immer sicher ist, ein nicht-ref struct
zurückzugeben. Der Compiler erstellt eine Diagnose für solche Fälle, um Verwirrung durch Entwickler zu vermeiden.
Ändern des Verhaltens von out
-Parametern
Um die Auswirkungen der Kompatibilitätsänderung, die Rückgabefähigkeit von ref
und in
Parametern als ref
Felder weiter einzuschränken, ändert die Sprache den Standardwert ref-safe-context für out
Parameter, die als Funktionsmitglied dienen sollen. Effektiv werden die out
-Parameter künftig implizit scoped out
gesetzt. Aus der Sicht der Kompatibilität bedeutet dies, dass sie nicht von ref
zurückgegeben werden können:
ref int Sneaky(out int i)
{
i = 42;
// Error: ref-safe-context of out is now function-member
return ref i;
}
Dadurch wird die Flexibilität von APIs erhöht, die ref struct
-Werte zurückgeben und über out
- Parameter verfügen, da der Parameter nicht mehr durch Referenz erfasst werden muss. Dies ist wichtig, da es sich um ein gängiges Muster in Lesestil-APIs handelt:
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);
}
Die Sprache berücksichtigt auch keine Argumente mehr, die an einen out
-Parameter übergeben werden, als rückgebbar. Die Annahme, dass die Eingabe an einen out
-Parameter als Rückgabe behandelt wird, war für Entwickler äußerst verwirrend. Es subvertiert im Wesentlichen die Absicht von out
, indem Entwickler gezwungen werden, den vom Aufrufer übergebenen Wert zu berücksichtigen, der niemals verwendet wird, außer in Sprachen, die out
nicht respektieren. Zukünftige Sprachen, die ref struct
unterstützen, müssen sicherstellen, dass der ursprüngliche Wert, der an einen out
-Parameter übergeben wird, nie gelesen wird.
C# erreicht dies über bestimmte Zuordnungsregeln. Dieses erreicht sowohl unsere ref safe context-Regeln als auch die Möglichkeit für vorhandenen Code, der out
-Parameterwerte zuweist und anschließend zurückgibt.
Span<int> StrangeButLegal(out Span<int> span)
{
span = default;
return span;
}
Insgesamt bedeuten diese Änderungen, dass das Argument eines out
-Parameters nicht zu den Werten safe-context oder ref-safe-context für Methodenaufrufe beiträgt. Dies reduziert die allgemeinen Kompatibilitätsauswirkungen von ref
-Feldern erheblich und vereinfacht die Überlegungen der Entwickler zu out
. Ein Argument für einen out
-Parameter trägt nicht zur Rückgabe bei, es dient lediglich als Ausgabeparameter.
safe-context von Deklarationsausdrücken ableiten
Der safe-context einer Deklarationsvariablen aus einem out
Argument (M(x, out var y)
) oder einer Dekonstruktion ((var x, var y) = M()
) ist der engste der folgenden:
- Anrufer-Kontext
- Wenn die Variable markiert ist,
scoped
erfolgt der Deklarationsblock (d. h. Funktionsmitglied oder schmaler). - wenn der Typ der Variablen
ref struct
ist, berücksichtigen Sie alle Argumente für den enthaltenden Aufruf, einschließlich des Empfängers:- safe-context eines beliebigen Arguments, dessen entsprechender Parameter nicht
out
ist und einen safe-context von return-only oder weiter hat - ref-safe-context eines beliebigen Arguments, dessen entsprechender Parameter einen ref-safe-context von return-only oder weiter hat
- safe-context eines beliebigen Arguments, dessen entsprechender Parameter nicht
Siehe auch Beispiele für abgeleiteten save-context von Deklarationsausdrücken.
Implizite scoped
-Parameter
Insgesamt gibt es zwei ref
Orte, die implizit als scoped
deklariert werden:
this
bei einerstruct
Instanzmethode-
out
Parameter
Die Regeln für den referenzsicheren Kontext werden in Bezug auf scoped ref
und ref
formuliert. Aus referenzsicheren Kontextgründen ist ein in
-Parameter gleichbedeutend mit ref
und out
gleichbedeutend mit scoped ref
. Sowohl in
als auch out
werden nur dann ausdrücklich erwähnt, wenn es für die Semantik der Regel wichtig ist. Andernfalls werden sie nur als ref
bzw. scoped ref
betrachtet.
Bei der Erläuterung des ref-safe-context von Argumenten, die in
-Parametern entsprechen, werden sie als ref
-Argumente in der Spezifikation generalisiert. Im Fall, dass das Argument ein L-Wert ist, dann ist der ref-safe-context der L-Wert, andernfalls ist es ein Funktionselement. Auch hier wird in
nur dann aufgerufen, wenn es für die Semantik der aktuellen Regel wichtig ist.
Nur sicherer Kontext zurückgeben
Das Design erfordert auch die Einführung eines neuen sicheren Kontexts: Nur-Rückgabe. Dies ähnelt dem caller-context darin, dass es zurückgegeben werden kann, jedoch nur über eine return
-Anweisung zurückgegeben werden kann.
Die Details von return-only sind, dass es sich um einen Kontext handelt, der größer ist als function-member, aber kleiner als caller-context. Ein Ausdruck, der einer return
-Anweisung bereitgestellt wird, muss mindestens return-only sein. Somit fallen die meisten bestehenden Regeln außer Kraft. Beispielsweise schlägt die Zuweisung an einen ref
-Parameter aus einem Ausdruck mit einem safe-context von return-only fehl, da dieser kleiner ist als derref
safe-context- des Parameters, der caller-context ist. Die Notwendigkeit dieses neuen Escapekontexts wird unten erörtert.
Es gibt drei Standorte, bei denen standardmäßig die Option Nur Rückgabe aktiviert ist:
- Ein
ref
- oderin
-Parameter weist einen ref-safe-context- von Nur Rückgabe auf. Dies geschieht teilweise fürref struct
, um alberne zyklische Zuordnungsprobleme zu vermeiden. Es wird jedoch einheitlich durchgeführt, um das Modell zu vereinfachen und Kompatibilitätsänderungen zu minimieren. - Ein
out
-Parameter für eineref struct
hat safe-context im Fall von Nur-Rückgabe. Dies ermöglicht es, die Rückgabe undout
gleich ausdrucksstark zu machen. Dies hat nicht das dumme zyklische Zuordnungsproblem, daout
implizitscoped
ist, sodass der ref-safe-context immer noch kleiner ist als der safe-context. - Ein
this
-Parameter für einenstruct
-Konstruktor hat einen safe-context von Nur-Rückgabe. Dies ergibt sich daraus, dass es alsout
-Parameter modelliert wird.
Jeder Ausdruck oder jede Anweisung, die explizit einen Wert von einer Methode oder einem Lambda zurückgibt, muss einen safe-context und ggf. einen ref-safe-context von mindestens Nur Rückgabe. Dazu gehören return
-Anweisungen, Ausdruckskörperglieder und Lambda-Ausdrücke.
Ebenso muss jede Zuordnung zu einem out
einen safe-context mindestens Nur Rückgabeaufweisen. Dies ist jedoch kein Sonderfall, dies folgt nur aus den vorhandenen Zuordnungsregeln.
Hinweis: Ein Ausdruck, dessen Typ keinref struct
Typ ist, verfügt immer über einen safe-context vom Typ Aufruferkontext-.
Regeln für Methodenaufrufe
Die Regeln für den referenzsicheren Kontext für Methodenaufrufe werden auf verschiedene Arten aktualisiert. Der erste besteht darin, die Auswirkungen zu erkennen, die scoped
auf Argumente hat. Für ein bestimmtes Argument expr
, das an Parameter p
übergeben wird:
- Wenn
p
scoped ref
ist, trägtexpr
nicht zu ref-safe-context bei, wenn man Argumente prüft.- Wenn
p
scoped
ist, trägtexpr
nicht zu safe-context bei, wenn man Argumente prüft.- Wenn
p
out
ist, trägtexpr
nicht zu ref-safe-context oder safe-context weitere Details bei.
Die Formulierung "trägt nicht bei" bedeutet, dass die Argumente beim Berechnen des -ref-safe-context--Werts oder des -safe-context--Werts des Rückgabewerts der Methode nicht berücksichtigt werden. Das liegt daran, dass die Werte nicht zu dieser Lebensdauer beitragen können, da die scoped
Annotation es verhindert.
Die Regeln für Methodenaufrufe können jetzt vereinfacht werden. Der Empfänger muss keine Sonderbehandlung mehr erfahren; im Fall von struct
ist es jetzt einfach ein scoped ref T
. Die Wertregeln müssen geändert werden, um Rückgaben des Felds ref
zu berücksichtigen.
Ein Wert, der sich aus einem Methodenaufruf
e1.M(e2, ...)
ergibt, bei demM()
keine ref-to-ref-struct zurückgibt, weist einen safe-context aus den engsten der folgenden Elemente ab:
- Der caller-context
- Wenn die Rückgabe ein
ref struct
ist, wird der safe-context von allen Argumentausdrücken beigetragen.- Wenn die Rückgabe eine
ref struct
ist, wird der ref-safe-context von allenref
Argumenten beigesteuertWenn
M()
ref-to-ref-struct zurückgibt, ist der safe-context derselbe wie der safe-context aller Argumente, die ref-to-ref-struct sind. Es ist ein Fehler, wenn mehrere Argumente mit unterschiedlichen safe-context aufgrund von Methodenargumenten übereinstimmen müssen.
Die ref
-Aufrufregeln können vereinfacht werden:
Ein Wert, der sich aus einem Methodenaufruf
ref e1.M(e2, ...)
ergibt, bei demM()
keine ref-to-ref-struct zurückgibt, ist safe-context, der engste der folgenden Kontexte:
- Der caller-context
- Der safe-context, der von allen Argumentausdrücken beigetragen wird
- Der ref-safe-context, der von allen
ref
Argumenten beigetragen wirdWenn
M()
ref-to-ref-struct zurückgibt, ist der ref-safe-context- der engste ref-safe-context, der von allen Argumenten beigesteuert wird, die als ref-to-ref-struct vorliegen.
Mit dieser Regel können wir nun die beiden Varianten der gewünschten Methoden definieren:
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);
}
Regeln für Objektinitialisierer
Der safe-context eines Objektinitialisierungsausdrucks ist der schmalste von:
- Der safe-context des Konstruktoraufrufs.
- Der safe-context und ref-safe-context von Argumenten für Member-Initialisierer-Indexer, die zum Empfänger wechseln können.
- Der safe-context der rechten Seite von Zuweisungen in Memberinitialisierern an nicht schreibgeschützte Setter oder der ref-safe-context im Fall einer Ref-Zuweisung.
Eine andere Möglichkeit, dies zu modellieren, besteht darin, sich jedes Argument für einen Memberinitialisierer, das dem Empfänger zugewiesen werden kann, als Argument für den Konstruktor vorzustellen. Dies liegt daran, dass der Memberinitializer effektiv ein Konstruktoraufruf ist.
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);
Diese Modellierung ist wichtig, da sie zeigt, dass unsere MAMM speziell für Memberinitialisierer berücksichtigen müssen. Beachten Sie, dass dieser Fall illegal sein muss, da ein Wert mit einem schmaleren safe-context- einem höheren zugewiesen werden kann.
Methodenargumente müssen übereinstimmen
Das Vorhandensein von ref
-Feldern bedeutet, dass die Regeln für Methodenargumente aktualisiert werden müssen, da ein ref
-Parameter nun als Feld in einem ref struct
-Argument der Methode gespeichert werden kann. Zuvor musste die Regel nur berücksichtigen, dass eine weitere ref struct
als Feld gespeichert wird. Die Auswirkungen werden in den Kompatibilitätsüberlegungen behandelt. Die neue Regel ist ...
Für jeden Methodenaufruf
e.M(a1, a2, ... aN)
- Berechnen Sie den schmalsten safe-context aus:
- caller-context
- Der safe-context aller Argumente
- Die ref-safe-context- aller Bezugsargumente, deren entsprechende Parameter einen ref-safe-context- mit einem caller-context haben.
- Alle
ref
-Argumente vonref struct
-Typen müssen durch einen Wert mit diesem safe-context zugewiesen werden können. Dies ist ein Fall, in demref
nicht verallgemeinert werden kann undin
undout
einschließt.
Für jeden Methodenaufruf
e.M(a1, a2, ... aN)
- Berechnen Sie den schmalsten safe-context aus:
- caller-context
- Der safe-context aller Argumente
- Der ref-safe-context aller Referenzargumente, deren entsprechende Parameter nicht
scoped
sind.- Alle
out
-Argumente vonref struct
-Typen müssen durch einen Wert mit diesem safe-contextzugewiesen werden können.
Durch das Vorhandensein von scoped
können Entwickler die Reibung verringern, die diese Regel erstellt, indem Parameter markiert werden, die nicht als scoped
zurückgegeben werden. Dadurch werden ihre Argumente, wie in (1) beschrieben, in beiden oben genannten Fällen entfernt, und es bietet den Anrufern mehr Flexibilität.
Die Auswirkungen dieser Änderung werden tiefer nachfolgend erörtert. Dies ermöglicht es Entwicklern, Aufrufstellen flexibler zu gestalten, indem sie nicht-ausbrechende Ref-ähnliche Werte mit scoped
annotieren.
Parameterbereichsabweichung
Der scoped
-Modifikator und das [UnscopedRef]
-Attribut (siehe unten) für Parameter wirken sich auch auf unsere Regeln zur Außerkraftsetzung, Schnittstellenimplementierung und delegate
-Konvertierung aus. Die Signatur für eine Überschreibung, Schnittstellenimplementierung oder delegate
-Konvertierung kann:
- Fügen Sie
scoped
zu einemref
- oderin
-Parameter hinzu - Fügen Sie
scoped
zu einemref struct
-Parameter hinzu - Entferne
[UnscopedRef]
aus einemout
-Parameter - Entfernen Sie
[UnscopedRef]
von einemref
-Parameter einesref struct
-Typs
Jeder andere Unterschied in Bezug auf scoped
oder [UnscopedRef]
wird als Nichtübereinstimmung betrachtet.
Der Compiler meldet eine Diagnose für unsichere Bereichsübereinstimmungen bei Überschreibungen, Schnittstellenimplementierungen und Delegaten-Konvertierungen, wenn:
- Die Methode verfügt über einen
ref
- oderout
-Parameter des Typsref struct
mit einer Nichtübereinstimmung beim Hinzufügen von[UnscopedRef]
(nicht entfernenscoped
). (In diesem Fall ist eine triviale zyklische Zuordnung möglich, daher sind keine anderen Parameter erforderlich.) - Oder beides ist wahr:
- Die Methode gibt einen
ref struct
zurück oder gibt einenref
oderref readonly
zurück, oder die Methode verfügt über einenref
- oderout
-Parameter vom Typref struct
. - Die Methode verfügt über mindestens einen zusätzlichen
ref
-,in
- oderout
-Parameter oder einen Parameter vom Typref struct
.
- Die Methode gibt einen
Die Diagnose wird in anderen Fällen nicht geliefert, weil:
- Die Methoden mit solchen Signaturen können die übergebenen Refs nicht erfassen, daher ist ein Bereichsfehler unproblematisch.
- Dazu gehören sehr häufige und einfache Szenarien (z. B. einfache alte
out
-Parameter, die inTryParse
-Methodensignaturen verwendet werden). Ein Bericht über Bereichsunterschiede, nur weil sie in Sprachversion 11 eingesetzt werden und derout
-Parameter daher einen anderen Geltungsbereich hat, wäre verwirrend.
Die Diagnose wird als Fehler gemeldet, wenn die nicht übereinstimmenden Signaturen beide die Regeln für den sicheren Kontext von C#11 verwenden; andernfalls handelt es sich um eine Warnung.
Die Bereichsübereinstimmungswarnung kann für ein Modul gemeldet werden, das mit den C#7.2-Regeln für sicheren Kontext kompiliert wurde, wobei scoped
nicht verfügbar ist. In einigen Fällen kann es erforderlich sein, die Warnung zu unterdrücken, wenn die andere nicht übereinstimmende Signatur nicht geändert werden kann.
Der scoped
Modifizierer und das [UnscopedRef]
-Attribut haben die folgenden Auswirkungen auf Methodensignaturen:
- Der
scoped
-Modifizierer und das[UnscopedRef]
-Attribut wirken sich nicht auf das Ausblenden aus. - Überladungen können sich nicht nur bei
scoped
oder[UnscopedRef]
unterscheiden.
Wir möchten den langen Abschnitt über das ref
-Feld und scoped
mit einer kurzen Zusammenfassung der vorgeschlagenen einschneidenden Änderungen abschließen.
- Ein Wert mit ref-safe-context- für den caller-context ist durch
ref
oder dasref
-Feld zurückzugeben. - Ein
out
-Parameter würde einen safe-context eines function-member haben.
Detaillierte Notizen
- Ein
ref
-Feld kann nur innerhalb einesref struct
deklariert werden. - Ein
ref
-Feld kann nichtstatic
,volatile
oderconst
deklariert werden. - Ein
ref
-Feld darf keinen Typ aufweisen, derref struct
- Der Erstellungsprozess der Referenzbaugruppe muss das Vorhandensein eines
ref
-Felds innerhalb einesref struct
sicherstellen. - Ein
readonly ref struct
muss seineref
-Felder alsreadonly ref
deklarieren - Bei by-ref-Werten muss der Modifikator
scoped
vorin
,out
, oderref
stehen - Das Dokument für Spannsicherheitsregeln wird wie in diesem Dokument dargelegt aktualisiert.
- Die neuen Regeln für den bezugssicheren Kontext werden in Kraft treten, wenn eine der folgenden Regeln gilt:
- Die Kernbibliothek enthält das Feature-Flag, das die Unterstützung für
ref
-Felder angibt. - Der Wert
langversion
ist 11 oder höher.
- Die Kernbibliothek enthält das Feature-Flag, das die Unterstützung für
Syntax
13.6.2 Lokale Variablendeklarationen: 'scoped'?
hinzugefügt.
local_variable_declaration
: 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
;
local_variable_mode_modifier
: 'ref' 'readonly'?
;
13.9.4 Die for
Aussage: 'scoped'?
indirekt aus local_variable_declaration
hinzugefügt.
13.9.5 Die foreach
Aussage: 'scoped'?
hinzugefügt.
foreach_statement
: 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
embedded_statement
;
12.6.2 Argumentlisten: 'scoped'?
für Deklarationsvariable out
hinzugefügt.
argument_value
: expression
| 'in' variable_reference
| 'ref' variable_reference
| 'out' ('scoped'? local_variable_type)? identifier
;
12.7 Dekonstruktion-Ausdrücke:
[TBD]
15.6.2 Methodenparameter: 'scoped'?
zu parameter_modifier
hinzugefügt.
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 Delegiertenerklärungen: 'scoped'?
indirekt aus fixed_parameter
hinzugefügt.
12.19 Anonyme Funktionsausdrücke: 'scoped'?
hinzugefügt.
explicit_anonymous_function_parameter
: 'scoped'? anonymous_function_parameter_modifier? type identifier
;
anonymous_function_parameter_modifier
: 'in'
| 'ref'
| 'out'
;
Sunset-beschränkte Typen
Der Compiler hat ein Konzept einer Gruppe von "eingeschränkten Typen", die weitgehend nicht dokumentiert ist. Diese Typen erhielten einen besonderen Status, da in C# 1.0 keine allgemeine Möglichkeit zum Ausdrücken ihres Verhaltens gegeben wurde. Vor allem die Tatsache, dass die Typen Verweise auf den Ausführungsstapel enthalten können. Stattdessen hatte der Compiler spezielle Kenntnisse über sie und beschränkte ihre Verwendung auf Weisen, die immer sicher wären: unzulässige Rückgaben, kann nicht als Arrayelemente verwendet werden, kann nicht in Generika verwendet werden, usw.
Sobald ref
Felder verfügbar sind und erweitert werden, um ref struct
zu unterstützen, können diese Typen mithilfe einer Kombination aus ref struct
und ref
Feldern ordnungsgemäß in C# definiert werden. Wenn der Compiler erkennt, dass eine Laufzeit ref
-Felder unterstützt, verfügt er daher nicht mehr über einen Begriff eingeschränkter Typen. Stattdessen werden die Typen verwendet, wie sie im Code definiert sind.
Um dies zu unterstützen, werden unsere Regeln für den sicheren Verweiskontext wie folgt aktualisiert:
-
__makeref
wird als Methode mit der Signaturstatic TypedReference __makeref<T>(ref T value)
behandelt. __refvalue
wird als Methode mit der Signaturstatic ref T __refvalue<T>(TypedReference tr)
behandelt. Der Ausdruck__refvalue(tr, int)
verwendet effektiv das zweite Argument als Typparameter.__arglist
als Parameter verfügt über einen ref-safe-context und einen safe-context des Funktionsmitglieds.__arglist(...)
als Ausdruck hat einen ref-safe-context und einen safe-context des Funktionsmitglieds.
Kompatible Laufzeiten stellen sicher, dass TypedReference
, RuntimeArgumentHandle
und ArgIterator
als ref struct
definiert sind. Weitere TypedReference
müssen als ein ref
-Feld zu einem ref struct
für jeden möglichen Typ betrachtet werden (es kann einen beliebigen Wert speichern). Durch die Kombination mit den oben genannten Regeln wird sichergestellt, dass Referenzen auf den Speicherstapel nicht über ihre vorgesehene Lebensdauer hinaus entweichen.
Hinweis: Dies ist streng genommen ein Compilerimplementierungsdetail im Vergleich zu einem Teil der Sprache. Aufgrund der Beziehung zu ref
-Feldern wird sie jedoch aus Gründen der Einfachheit in den Sprachvorschlag aufgenommen.
Unbegrenztes Bereitstellen
Einer der wichtigsten Reibungspunkte ist die Unfähigkeit, Felder von ref
in Instanzmitgliedern einer struct
zurückgeben zu können. Dies bedeutet, dass Entwickler keine Methoden/Eigenschaften erstellen können, die ref
zurückgeben, und darauf zurückgreifen müssen, Felder direkt zugänglich zu machen. Dadurch wird die Nützlichkeit von ref
in struct
reduziert, wo sie häufig am meisten gewünscht wird.
struct S
{
int _field;
// Error: this, and hence _field, can't return by ref
public ref int Prop => ref _field;
}
Die Begründung für diesen Standard ist vernünftig, es ist jedoch nicht grundsätzlich falsch daran, dass ein struct
durch Referenz this
maskiert wird. Dies ist lediglich der von den Regeln für den referenzsicheren Kontext gewählte Standard.
Um dies zu beheben, stellt die Sprache das Gegenteil der scoped
Lebensdauer-Annotation bereit, indem sie eine UnscopedRefAttribute
unterstützt. Dies kann auf jedes ref
angewendet werden und ändert den ref-safe-context um eine Ebene breiter als die Standardeinstellung. Zum Beispiel:
UnscopedRef angewendet auf | Ursprünglicher ref-safe-context | Neuer ref-safe-context |
---|---|---|
Instanzmitglied | Funktionsmitglied | Nur-Rückgabe |
in / ref -Parameter |
Nur-Rückgabe | Anrufer-Kontext |
out Parameter |
Funktionsmitglied | Nur-Rückgabe |
Wenn Sie [UnscopedRef]
auf eine Instanzmethode eines struct
anwenden, hat dies Auswirkungen auf die Änderung des impliziten this
-Parameters. Dies bedeutet, dass this
als nicht kommentierte ref
desselben Typs fungiert.
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;
}
Die Anmerkung kann auch auf out
-Parameter platziert werden, um sie auf das Verhalten von C# 10 zurückzusetzen.
ref int SneakyOut([UnscopedRef] out int i)
{
i = 42;
return ref i;
}
Für die Zwecke der ref safe context-Regeln wird eine solche [UnscopedRef] out
einfach als ref
betrachtet. Ähnlich wie in
für Lebenszeitzwecke als ref
betrachtet wird.
Die [UnscopedRef]
Annotation ist für die init
-Mitglieder und Konstruktoren innerhalb von struct
unzulässig. Diese Mitglieder sind bereits in Bezug auf die ref
-Semantik besonders, da sie readonly
-Mitglieder als veränderlich betrachten. Dies bedeutet, dass ref
diesen Mitgliedern als einfache ref
und nicht als ref readonly
angezeigt wird. Dies ist innerhalb der Grenzen von Konstruktoren und init
zulässig. Wenn [UnscopedRef]
zugelassen würde, könnte eine solche ref
fälschlicherweise außerhalb des Konstruktors entkommen und Mutationen zulassen, nachdem die readonly
-Semantik stattgefunden hat.
Der Attributtyp hat die folgende Definition:
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(
AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
AllowMultiple = false,
Inherited = false)]
public sealed class UnscopedRefAttribute : Attribute
{
}
}
Detaillierte Notizen
- Eine mit
[UnscopedRef]
kommentierte Instanzmethode oder Eigenschaft hat ref-safe-context vonthis
auf den caller-context gesetzt. - Ein Mitglied, das mit
[UnscopedRef]
kommentiert ist, kann keine Schnittstelle implementieren. - Es ist ein Fehler,
[UnscopedRef]
zu verwenden auf- Ein Mitglied, das nicht in einer
struct
deklariert ist - Ein
static
-Mitglied,init
-Mitglied oder Konstruktor von einemstruct
- Ein Parameter, der mit
scoped
markiert ist - Ein als Wert übergebener Parameter
- Ein Parameter, der durch Verweis übergeben wird, der nicht implizit einem Bereich zugeordnet ist
- Ein Mitglied, das nicht in einer
ScopedRefAttribute
Die scoped
-Anmerkungen werden über das Attribut des Typs System.Runtime.CompilerServices.ScopedRefAttribute
in Metadaten ausgegeben. Das Attribut wird mit dem namespace-qualifizierten Namen verglichen, sodass die Definition nicht in einer bestimmten Assembly vorkommen muss.
Der ScopedRefAttribute
-Typ ist nur für die Compilerverwendung vorgesehen – er ist in der Quelle nicht zulässig. Die Typdeklaration wird vom Compiler synthetisiert, wenn sie noch nicht in der Kompilierung enthalten ist.
Der Typ hat die folgende Definition:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class ScopedRefAttribute : Attribute
{
}
}
Der Compiler gibt dieses Attribut für den Parameter mit der Syntax scoped
aus. Dies wird nur ausgegeben, wenn die Syntax bewirkt, dass sich der Wert vom Standardzustand unterscheidet. Beispielsweise führt scoped out
dazu, dass kein Attribut ausgegeben wird.
RefSafetyRulesAttribute
Es gibt mehrere Unterschiede in den Regeln für den ref safe context zwischen C#7.2 und C#11. Alle diese Unterschiede können zu schwerwiegenden Änderungen führen, wenn eine Neukompilierung mit C#11 anhand von Referenzen erfolgt, die mit C#10 oder früher kompiliert wurden.
- Nicht bereichsgebundene
ref
/in
/out
-Parameter können möglicherweise einem Methodenaufruf als einref
-Feld einerref struct
in C#11 entweichen, nicht jedoch in C#7.2. -
out
-Parameter werden implizit in C#11 und nicht in C#7.2 erfasst. -
ref
/in
-Parameter fürref struct
-Typen werden implizit in C#11 und nicht in C#7.2 erfasst.
Um die Wahrscheinlichkeit potenzieller Inkompatibilitäten beim erneuten Kompilieren mit C#11 zu verringern, werden wir den C#11-Compiler aktualisieren, sodass die Regeln für den verweissicheren Kontext bei Methodenaufrufen verwendet werden, die den Regeln entsprechen, die zur Analyse der Methodendeklaration genutzt wurden. Im Wesentlichen verwendet der C#11-Compiler beim Analysieren eines Aufrufs einer Methode, die mit einem älteren Compiler kompiliert wurde, C#7.2 ref safe context rules.
Um dies zu aktivieren, gibt der Compiler ein neues [module: RefSafetyRules(11)]
-Attribut aus, wenn das Modul mit -langversion:11
oder höher kompiliert wird oder wenn es mit einer Corlib kompiliert wird, die das Feature-Flag für ref
-Felder enthält.
Das Argument für das Attribut gibt die Sprachversion der ref safe context-Regeln an, die beim Kompilieren des Moduls verwendet wurden.
Die Version ist derzeit auf 11
festgelegt, unabhängig von der tatsächlichen Sprachversion, die an den Compiler übergeben wird.
Die Erwartung besteht darin, dass zukünftige Versionen des Compilers die ref safe context Regeln aktualisieren und Attribute mit verschiedenen Versionen ausgeben.
Wenn der Compiler ein Modul lädt, das eine [module: RefSafetyRules(version)]
mit einem anderen version
als 11
enthält, meldet der Compiler eine Warnung für die nicht erkannte Version, wenn in diesem Modul deklarierte Methoden aufgerufen werden.
Wenn der C#11-Compiler einen Methodenaufruf analysiert:
- Wenn das Modul, das die Methodendeklaration enthält,
[module: RefSafetyRules(version)]
enthält , unabhängig vonversion
, wird der Methodenaufruf mit C#11-Regeln analysiert. - Wenn das Modul, das die Methodendeklaration enthält, aus der Quelle stammt und mit
-langversion:11
oder mit einer Corlib kompiliert wird, die das Feature-Flag fürref
-Felder enthält, wird der Methodenaufruf mit C#11-Regeln analysiert. - Wenn das Modul, das die Methodendeklaration enthält, auf
System.Runtime { ver: 7.0 }
verweist, wird der Methodenaufruf mit C#11-Regeln analysiert. Diese Regel ist eine temporäre Entschärfung für Module, die mit früheren Vorschauen von C#11 / .NET 7 kompiliert wurden und später entfernt werden. - Andernfalls wird der Methodenaufruf mit C#7.2-Regeln analysiert.
Ein Compiler vor C#11 ignoriert alle RefSafetyRulesAttribute
und analysiert Methodenaufrufe nur mit C#7.2-Regeln.
Das RefSafetyRulesAttribute
wird mit dem namespace-qualifizierten Namen verglichen, sodass die Definition nicht in einer bestimmten Assembly vorkommen muss.
Der RefSafetyRulesAttribute
-Typ ist nur für die Compilerverwendung vorgesehen – er ist in der Quelle nicht zulässig. Die Typdeklaration wird vom Compiler synthetisiert, wenn sie noch nicht in der Kompilierung enthalten ist.
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;
}
}
Sichere Puffer mit fester Größe
Sichere Puffer mit fester Größe wurden in C# 11 nicht bereitgestellt. Dieses Feature kann in einer zukünftigen Version von C# implementiert werden.
Die Sprache wird die Einschränkungen für Arrays mit fester Größe so lockern, dass sie im sicheren Code deklariert werden können und der Elementtyp verwaltet oder unverwaltet sein kann. Dadurch werden Typen wie die folgenden zulässig:
internal struct CharBuffer
{
internal char Data[128];
}
Diese Deklarationen, ähnlich wie ihre unsafe
Gegenstücke, definieren eine Abfolge von N
-Elementen im enthaltenden Typen. Auf diese Member kann mit einem Indexer zugegriffen werden, und sie können auch in Span<T>
- und ReadOnlySpan<T>
-Instanzen konvertiert werden.
Bei der Indexierung in einem fixed
-Puffer vom Typ T
muss der readonly
-Zustand des Containers berücksichtigt werden. Wenn der Container readonly
ist, gibt der Indexer ref readonly T
zurück, andernfalls gibt er ref T
zurück.
Der Zugriff auf einen fixed
-Puffer ohne Indexer hat keinen natürlichen Typ, ist jedoch in Span<T>
-Typen konvertierbar. Falls der Container readonly
ist, ist der Puffer implizit in ReadOnlySpan<T>
konvertierbar und kann andernfalls implizit in Span<T>
oder ReadOnlySpan<T>
konvertiert werden (die Span<T>
-Konvertierung ist als bessere angesehen).
Die resultierende Span<T>
-Instanz hat eine Länge, die der im fixed
-Puffer deklarierten Größe entspricht. Der safe-context des zurückgegebenen Wertes entspricht dem safe-context des Containers, so wie es der Fall wäre, wenn der Zugriff auf die Sicherungsdaten als Feld erfolgt.
Für jede fixed
-Deklaration in einem Typ, in dem der Elementtyp T
, generiert die Sprache eine entsprechende get
nur Indexermethode, deren Rückgabetyp ref T
ist. Der Indexer wird mit dem Attribut [UnscopedRef]
kommentiert, da die Implementierung Felder des deklarierenden Typs zurückgibt. Die Zugänglichkeit des Mitglieds entspricht der Zugänglichkeit im Feld fixed
.
Die Signatur des Indexers für CharBuffer.Data
lautet beispielsweise wie folgt:
[UnscopedRef] internal ref char DataIndexer(int index) => ...;
Wenn sich der angegebene Index außerhalb der deklarierten Grenzen des fixed
-Arrays befindet, wird eine IndexOutOfRangeException
ausgegeben. Wenn ein Konstantenwert angegeben wird, wird er durch einen direkten Verweis auf das entsprechende Element ersetzt. Es sei denn, die Konstante liegt außerhalb der deklarierten Grenzen, in diesem Fall tritt ein Kompilierungszeitfehler auf.
Es wird auch ein benannter Accessor für jeden fixed
-Puffer generiert, der die Wertoperationen get
und set
unterstützt. Dies bedeutet, dass fixed
-Puffer stärker der bestehenden Arraysemantik ähneln, indem ein ref
-Accessor sowie die get
- und set
-Vorgänge verwendet werden. Dies bedeutet, dass Compiler die gleiche Flexibilität haben, wenn sie Code generieren, der fixed
-Puffer nutzt, wie bei der Nutzung von Arrays. Dadurch sollten Vorgänge wie await
über fixed
Puffer einfacher ausgegeben werden.
Dies hat auch den zusätzlichen Vorteil, dass fixed
-Puffer dadurch einfacher aus anderen Sprachen verwendet werden können. Benannte Indexer sind ein Feature, das seit der 1.0-Version von .NET vorhanden ist. Auch Sprachen, die einen benannten Indexer nicht direkt emittieren können, können sie in der Regel nutzen (C# ist wirklich ein gutes Beispiel dafür).
Der Sicherungsspeicher für den Puffer wird mithilfe des Attributs [InlineArray]
generiert. Dies ist ein Mechanismus, der in Problem 12320 erörtert wird und speziell ermöglicht, eine Sequenz von Feldern desselben Typs effizient zu deklarieren. Dieses spezielle Thema wird noch aktiv diskutiert, und die Erwartung besteht darin, dass die Implementierung dieses Features nach dieser Diskussion folgt.
Initialisierer mit ref
-Werten in new
- und with
-Ausdrücken
Im Abschnitt 12.8.17.3 Objektinitialisierer aktualisieren wir die Grammatik auf:
initializer_value
: 'ref' expression // added
| expression
| object_or_collection_initializer
;
Im Abschnitt für with
Ausdruck aktualisieren wir die Grammatik auf:
member_initializer
: identifier '=' 'ref' expression // added
| identifier '=' expression
;
Der linke Operand der Zuweisung muss ein Ausdruck sein, der an ein Referenzfeld gebunden ist.
Der rechte Operand muss ein Ausdruck sein, der ein L-Wert ergibt und einen Wert des gleichen Typs wie der linke Operand bezeichnet.
Wir fügen eine ähnliche Regel zur lokalen Referenzneuzuweisung hinzu:
Wenn der linke Operand ein beschreibbarer Verweis ist (d. h. er bezeichnet etwas anderes als ein ref readonly
-Feld), muss der rechte Operand ein beschreibbares L-Wert sein.
Die Escaperegeln für Konstruktoraufrufe bleiben:
Ein
new
-Ausdruck, der einen Konstruktor aufruft, unterliegt denselben Regeln wie ein Methodenaufruf, von dem angenommen wird, dass er den erstellten Typ zurückgibt.
Nämlich die oben aktualisierten Regeln des Methodenaufrufs:
Ein rvalue, das sich aus einem Methodenaufruf
e1.M(e2, ...)
ergibt, hat den sicheren Kontext aus dem kleinsten der folgenden Kontexte:
- Der caller-context
- Der safe-context, der von allen Argumentausdrücken beigetragen wird
- Wenn die Rückgabe eine
ref struct
ist, dann wird ref-safe-context von allenref
Argumenten beigesteuert
Bei einem new
-Ausdruck mit Initialisierern zählen die Initialisierungsausdrücke als Argumente (sie tragen zu ihrem safe-context bei), und die ref
-Initialisierungsausdrücke zählen rekursiv als ref
-Argumente (sie tragen zu ihrem ref-safe-context bei).
Änderungen im unsicheren Kontext
Zeigertypen (Abschnitt 23.3) werden erweitert, um verwaltete Typen als Referent-Typ zuzulassen.
Solche Zeigertypen werden als verwalteter Typ geschrieben, gefolgt von einem *
-Token. Sie erzeugen eine Warnung.
Der Adress-of-Operator (Abschnitt 23.6.5) wurde erweitert, um eine Variable mit einem verwalteten Typ als Operanden zu akzeptieren.
Die fixed
-Anweisung (Abschnitt 23.7) wird erweitert, um fixed_pointer_initializer zu akzeptieren, d. h. die Adresse einer Variablen vom verwalteten Typ T
oder ein Ausdruck eines array_type mit Elementen eines verwalteten Typs T
.
Der Initialisierer für die Stapelzuweisung (Abschnitt 12.8.22) wurde ähnlich erweitert.
Überlegungen
Es gibt Überlegungen, die andere Teile des Entwicklungsstapels berücksichtigen sollten, wenn sie dieses Feature auswerten.
Überlegungen zur Kompatibilität
Die Herausforderung in diesem Vorschlag sind die Auswirkungen auf die Kompatibilität mit unseren bestehenden Spannweitensicherheitsregeln bzw. §9.7.2. Während diese Regeln das Konzept einer ref struct
mit ref
-Feldern vollständig unterstützen, erlauben sie es APIs außer stackalloc
nicht, den ref
-Zustand zu erfassen, der sich auf den Stapel bezieht. Die Regeln für den bezugssicheren Kontext beinhalten eine harte Annahme oder §16.4.12.8, dass ein Konstruktor der Form Span(ref T value)
nicht existiert. Dies bedeutet, dass die Sicherheitsregeln keinen ref
-Parameter berücksichtigen, der als ref
-Feld entkommen kann, sodass Code wie folgt möglich ist.
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);
}
Tatsächlich gibt es drei Möglichkeiten für einen ref
-Parameter, einen Methodenaufruf zu verlassen:
- Durch Wertübergabe
- Durch
ref
Rückgabe - Durch das
ref
-Feld inref struct
, das alsref
/out
-Parameter zurückgegeben oder übergeben wird.
Die vorhandenen Regeln berücksichtigen nur (1) und (2). Sie berücksichtigen (3) daher nicht, sodass Lücken wie die Rückgabe von lokalen Variablen als ref
-Felder nicht berücksichtigt werden. Dieser Entwurf muss die Regeln ändern, um (3) zu berücksichtigen. Dies wird geringfügige Auswirkungen auf die Kompatibilität vorhandener APIs haben. Insbesondere wirkt es sich auf APIs aus, die über die folgenden Eigenschaften verfügen.
- Haben Sie eine
ref struct
in der Signatur- Dabei handelt es sich bei der
ref struct
um einen Rückgabetyp,ref
- oderout
-Parameter. - Verfügt über einen zusätzlichen
in
- oderref
-Parameter, ausgenommen für den Empfänger
- Dabei handelt es sich bei der
In C# 10 mussten Aufrufer solcher APIs nie berücksichtigen, dass die ref
-Zustandseingabe für die API als ein Feld ref
erfasst werden konnte. Dies erlaubte, dass mehrere Muster sicher in C# 10 vorhanden waren, die aufgrund der Möglichkeit, dass der ref
-Zustand als ref
-Feld entkommen kann, in C# 11 unsicher sind. Zum Beispiel:
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]);
}
Die Auswirkungen dieser Kompatibilitätsunterbrechung werden voraussichtlich sehr klein sein. Die betroffene API-Struktur ergab wenig Sinn in Abwesenheit von ref
-Feldern, weshalb es unwahrscheinlich ist, dass Kunden viele davon erstellt haben. Experimente, die Tools verwenden, um diese API-Struktur in vorhandenen Repositorys zu erkennen, stützen diese Behauptung. Das einzige Repository mit signifikanten Anzahlen dieser Form ist dotnet/runtime, was daran liegt, dass dieses Repository ref
-Felder über den intrinsischen Typ ByReference<T>
erstellen kann.
Dennoch muss das Design solche APIs berücksichtigen, da es ein gültiges Muster ausdrückt, auch wenn es kein gängiges ist. Daher muss der Entwurf Entwicklern die Tools zum Wiederherstellen der vorhandenen Lebensdauerregeln beim Upgrade auf C# 10 geben. Insbesondere müssen Mechanismen bereitgestellt werden, mit denen Entwickler ref
-Parameter annotieren können, um zu kennzeichnen, dass sie nicht durch das ref
- oder ref
-Feld entweichen können. Auf diese Weise können Kunden APIs in C# 11 definieren, die dieselben C#10-Aufruferegeln aufweisen.
Verweisassemblys
Eine Referenzassembly für eine Kompilierung mithilfe der in diesem Vorschlag beschriebenen Features muss die Elemente beibehalten, die referenzsichere Kontextinformationen vermitteln. Das bedeutet, dass alle Annotationsattribute über die gesamte Lebensdauer an ihrer ursprünglichen Position erhalten bleiben müssen. Jeder Versuch, sie zu ersetzen oder auszulassen, kann zu ungültigen Referenzassemblies führen.
Das Darstellen von ref
-Feldern ist differenzierter. Im Idealfall würde ein ref
-Feld in einer Referenzbaugruppe wie auch jedes andere Feld erscheinen. Ein ref
-Feld stellt jedoch eine Änderung des Metadatenformats dar und kann Probleme mit Toolketten verursachen, die nicht aktualisiert werden, um diese Metadatenänderung zu verstehen. Ein konkretes Beispiel ist C++/CLI, das wahrscheinlich einen Fehler erzeugen wird, wenn ein ref
-Feld verarbeitet wird. Daher ist es vorteilhaft, wenn ref
-Felder aus Referenzassemblies in unseren Kernbibliotheken weggelassen werden können.
Ein ref
-Feld allein hat keine Auswirkungen auf ref-sichere Kontextregeln. Nehmen wir ein konkretes Beispiel: Eine Änderung der vorhandenen Span<T>
-Definition zur Nutzung eines ref
-Feldes hat keine Auswirkungen auf den Verbrauch. Daher kann die ref
selbst sicher weggelassen werden. Ein ref
-Feld hat jedoch andere Auswirkungen auf den Verbrauch, die beibehalten werden müssen.
- Ein
ref struct
mit einemref
-Feld wird niemals alsunmanaged
betrachtet - Der Typ des Felds
ref
wirkt sich auf unendliche generische Erweiterungsregeln aus. Wenn der Typ einesref
-Felds daher einen Typparameter enthält, der beibehalten werden muss
Gemäß diesen Regeln ist dies eine gültige Transformation der Referenzassembly für eine 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
}
Anmerkungen
Lebensdauern werden am natürlichsten mit Typen ausgedrückt. Die Lebensdauer eines bestimmten Programms ist sicher, wenn die Typprüfung für Lebensdauertypen erfolgreich ist. Während die Syntax von C# implizit Lebensdauer zu Werten hinzufügt, gibt es ein zugrunde liegendes Typsystem, das die grundlegenden Regeln hier beschreibt. Es ist oft einfacher, die Auswirkungen von Änderungen am Entwurf in Bezug auf diese Regeln zu diskutieren, damit sie hier zur Diskussion einbezogen werden.
Beachten Sie, dass dies keine 100 % vollständige Dokumentation sein soll. Das Dokumentieren jedes einzelne Verhalten ist hier kein Ziel. Stattdessen soll ein allgemeines Verständnis und eine gemeinsame Terminologie festgelegt werden, um das Modell und mögliche Änderungen daran diskutieren zu können.
In der Regel ist es nicht notwendig, direkt über Lebensdauer-Typen zu sprechen. Ausnahmen sind Orte, an denen die Lebensdauer je nach bestimmten „Instanziierungs“-Sites variieren kann. Dies ist eine Art Polymorphismus, und wir nennen diese unterschiedlichen Lebensdauern „generische Lebensdauer”, dargestellt als generische Parameter. C# stellt keine Syntax zum Ausdrücken von Lebensdauergenerika bereit. Daher definieren wir eine implizite "Übersetzung" von C# in eine erweiterte tiefere Sprache, die explizite generische Parameter enthält.
In den folgenden Beispielen werden benannte Lebensdauern verwendet. Die Syntax $a
bezieht sich auf eine Lebensdauer namens a
. Es handelt sich um eine Lebenszeit, die für sich allein genommen keine Bedeutung hat, der jedoch über die where $a : $b
-Syntax eine Beziehung zu anderen Lebenszeiten gegeben werden kann. Dadurch wird festgelegt, dass $a
in $b
konvertierbar ist. Es kann dabei helfen zu verstehen, dass $a
eine Lebensdauer hat, die zumindest so lang ist wie $b
.
Es gibt einige vordefinierte Lebensdauern, die der Einfachheit und Kürze dienen:
-
$heap
: Dies ist die Lebensdauer eines Werts, der auf dem Heap vorhanden ist. Sie ist in allen Kontexten und Methodensignaturen verfügbar. -
$local
: Dies ist die Lebensdauer eines Werts, der auf dem Methodenstapel vorhanden ist. Sie ist tatsächlich ein Platzhalter für den Namen des Funktionselements. Sie ist implizit in Methoden definiert und kann in Methodensignaturen angezeigt werden, mit Ausnahme jeder Ausgabeposition. -
$ro
: Namensplatzhalter für nur Rückgabe -
$cm
: Namensplatzhalter für caller-context
Es gibt einige vordefinierte Beziehungen zwischen Lebenszeiten:
where $heap : $a
für alle Lebensdauern$a
where $cm : $ro
where $x : $local
für alle vorgegebenen Lebensdauern. Benutzerdefinierte Lebensdauern haben keine Beziehung zu einem lokalen Kontext, es sei denn, dies wird explizit definiert.
Lebensdauervariablen, die für Typen definiert werden, können invariant oder kovariant sein. Diese werden mit der gleichen Syntax wie generische Parameter ausgedrückt:
// $this is covariant
// $a is invariant
ref struct S<out $this, $a>
Der Parameter für die Lebensdauer $this
bei Typdefinitionen ist nicht vordefiniert, es sind jedoch einige Regeln damit verbunden, wenn er definiert ist.
- Er muss der erste Lebensdauerparameter sein.
- Es muss kovariant sein:
out $this
. - Die Lebensdauer von
ref
-Feldern muss in$this
umwandelbar sein. - Die
$this
Lebensdauer aller Nicht-Referenzfelder muss$heap
oder$this
sein.
Die Lebensdauer eines Bezugs wird durch die Angabe eines Lebensdauerarguments für den Bezug ausgedrückt. Ein Beispiel dafür ist ein ref
, das auf den Heap verweist und als ref<$heap>
dargestellt wird.
Beim Definieren eines Konstruktors im Modell wird der Name new
für die Methode verwendet. Es ist erforderlich, dass eine Parameterliste für den zurückgegebenen Wert sowie die Konstruktorargumente vorhanden ist. Dies ist erforderlich, um die Beziehung zwischen Konstruktoreingaben und dem konstruierten Wert auszudrücken. Statt Span<$a><$ro>
wird das Modell Span<$a> new<$ro>
verwenden. Der Typ der this
im Konstruktor, einschließlich Lebensdauern, ist der definierte Rückgabewert.
Die grundlegenden Regeln für die Lebensdauer sind wie folgt definiert:
- Alle Lebensdauern werden syntaktisch als generische Argumente ausgedrückt, die vor den Typargumenten stehen. Dies gilt für vordefinierte Lebensdauern außer
$heap
und$local
. - Alle Typen
T
, die keinref struct
sind, haben implizit eine Lebensdauer vonT<$heap>
. Dies ist implizit, es ist nicht erforderlich, in jedem Beispielint<$heap>
zu schreiben. - Für ein
ref
-Feld, das alsref<$l0> T<$l1, $l2, ... $ln>
definiert ist:- Alle Lebensdauern
$l1
bis$ln
müssen unveränderlich sein. - Die Lebensdauer von
$l0
muss in$this
umwandelbar sein.
- Alle Lebensdauern
- Für eine
ref
definiert alsref<$a> T<$b, ...>
, muss$b
in$a
konvertierbar sein - Die
ref
einer Variable weist eine definierte Lebensdauer auf:- Für ein
ref
lokalen, Parameter- oder Rückgabetypsref<$a> T
ist die Lebensdauer$a
-
$heap
für alle Referenztypen und Felder von Referenztypen $local
für alles andere
- Für ein
- Eine Zuordnung oder Rückgabe ist legal, wenn die zugrunde liegende Typkonvertierung legal ist.
- Die Lebensdauer von Ausdrücken kann mithilfe von Umwandlungsanmerkungen explizit gemacht werden:
(T<$a> expr)
die Wertlebensdauer ist explizit$a
fürT<...>
ref<$a> (T<$b>)expr
ist die Wertlebensdauer$b
fürT<...>
und die Bezugsdauer ist$a
.
Für die Zwecke der Lebensdauerregeln wird eine ref
für Konvertierungszwecke als Teil des Typs des Ausdrucks betrachtet. Es wird logisch dargestellt, indem ref<$a> T<...>
in ref<$a, T<...>>
konvertiert wird, wobei $a
kovariant ist und T
invariant ist.
Als Nächstes definieren wir die Regeln, mit denen wir die C#-Syntax dem zugrunde liegenden Modell zuordnen können.
Der Kürze halber wird ein Typ ohne explizite Lebensdauerparameter behandelt, als wäre out $this
definiert und auf alle Felder des Typs angewendet. Ein Typ mit einem ref
-Feld muss explizite Lebensdauerparameter definieren.
Diese Regeln existieren, um unser vorhandenes Invariant zu unterstützen, dass T
allen Typen scoped T
zugewiesen werden kann. Dies lässt sich darauf übertragen, dass T<$a, ...>
für T<$local, ...>
zugewiesen wird, und zwar für alle bekannten Lebensdauern, von denen bekannt ist, dass sie in $local
konvertiert sind. Darüber hinaus werden andere Elemente unterstützt, z. B. die Möglichkeit, Span<T>
vom Heap zu denen im Stapel zuzuweisen. Dies schließt Typen aus, bei denen Felder unterschiedliche Lebensdauern für Nicht-Bezugswerte aufweisen, aber das ist die Realität von C# heute. Um dies zu ändern, wären erhebliche Änderungen der C#-Regeln erforderlich, die neu geplant werden müssten.
Der Typ von this
ist implizit wie folgt für einen Typ S<out $this, ...>
wie folgt innerhalb einer Instanzmethode definiert:
- Für normale Instanzmethode:
ref<$local> S<$cm, ...>
- Beispielmethode, die mit
[UnscopedRef]
kommentiert wurde:ref<$ro> S<$cm, ...>
Der Mangel an expliziten this
-Parametern erzwingt hier die impliziten Regeln. Bei komplexen Beispielen und Diskussionen sollten Sie das Schreiben als static
-Methode betrachten und this
als expliziten Parameter festlegen.
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) { }
}
Die C#-Methodensyntax ist dem Modell auf folgende Weise zugeordnet:
-
ref
-Parameter haben eine Referenzlebensdauer von$ro
- Parameter vom Typ
ref struct
haben diese Lebensdauer von$cm
- Ref-Rückgabewerte haben eine Lebensdauer von
$ro
. - Rückgaben vom Typ
ref struct
haben eine Wertlebensdauer von$ro
. scoped
bei einem Parameter oderref
ändert die Lebensdauer zu$local
Angesichts dessen, lassen Sie uns ein einfaches Beispiel ansehen, das das Modell hier veranschaulicht:
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;
}
Sehen wir uns nun dasselbe Beispiel mit einem ref struct
an:
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;
}
Nun sehen wir, wie dies beim zyklischen Selbstzuweisungsproblem hilft.
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;
}
}
Als nächstes sehen wir, wie dies bei dem Problem mit den Erfassungsparametern hilft:
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;
}
}
Offene Probleme
Entwurf ändern, um Kompatibilitätsbrüche zu vermeiden
Dieser Entwurf schlägt mehrere Kompatibilitätsbrüche mit unseren vorhandenen Regeln für den ref-safe-context vor. Auch wenn davon ausgegangen wird, dass die Unterbrechungen nur minimale Auswirkungen haben, wurde großer Wert auf ein Design gelegt, das keine bahnbrechenden Änderungen aufweist.
Das kompatibilitätserhaltende Design war jedoch wesentlich komplexer als dieses. Um die Kompatibilität von ref
-Feldern zu bewahren, benötigen sie unterschiedliche Lebensdauern, damit sie sowohl durch ref
als auch durch das ref
-Feld zurückgegeben werden können. Im Wesentlichen müssen wir für alle Parameter einer Methode ref-field-safe-context Tracking bereitstellen. Dies muss für alle Ausdrücke berechnet und in allen Werten praktisch überall so nachverfolgt werden, wie ref-safe-context heute nachverfolgt wird.
Darüber hinaus verfügt dieser Wert über Beziehungen mit ref-safe-context. Es ist beispielsweise unsinnig, dass ein Wert als ref
-Feld zurückgegeben werden kann, aber nicht direkt als ref
. Das liegt daran, dass ref
-Felder bereits trivial von ref
zurückgegeben werden können (der Zustand ref
in einem ref struct
kann von ref
zurückgegeben werden, auch wenn der enthaltene Wert dies nicht kann). Daher brauchen die Regeln eine ständige Anpassung, um sicherzustellen, dass diese Werte in Bezug aufeinander sinnvoll sind.
Außerdem bedeutet dies, dass die Sprache Syntax benötigt, um ref
Parameter darzustellen, die auf drei verschiedene Arten zurückgegeben werden können: durch das ref
-Feld, durch ref
und durch den Wert. Die Standardeinstellung, die von ref
zurückgegeben werden kann. In Zukunft wird zwar die natürlichere Rendite, insbesondere dann, wenn ref struct
einbezogen werden, voraussichtlich von ref
-Feld oder ref
sein. Das bedeutet, dass neue APIs standardmäßig eine zusätzliche Syntaxanmerkung korrigieren müssen. Dies ist unerwünscht.
Diese Compat-Änderungen wirken sich jedoch auf Methoden aus, die die folgenden Eigenschaften aufweisen:
- Haben Sie eine
Span<T>
oderref struct
- Dabei handelt es sich bei der
ref struct
um einen Rückgabetyp,ref
- oderout
-Parameter. - Verfügt über einen zusätzlichen
in
- oderref
-Parameter (ausgenommen für den Empfänger)
- Dabei handelt es sich bei der
Um die Auswirkungen zu verstehen, ist es hilfreich, APIs in Kategorien aufzuteilen:
- Möchten Sie, dass Verbraucher berücksichtigen, dass
ref
als einref
-Feld erfasst wird? Paradebeispiel sind dieSpan(ref T value)
-Konstrukteure - Möchten Sie nicht, dass Verbraucher berücksichtigen, dass
ref
als einref
-Feld erfasst wird? Diese werden jedoch in zwei Kategorien unterteilt.- Unsichere APIs. Dies sind APIs innerhalb der
Unsafe
- undMemoryMarshal
-Typen, von denenMemoryMarshal.CreateSpan
der prominenteste ist. Diese APIs fangenref
unsicher ab, sind aber auch als unsichere APIs bekannt. - Sichere APIs. Hierbei handelt es sich um APIs, die
ref
-Parameter zur Effizienzsteigerung übernehmen, aber nirgendwo tatsächlich erfasst werden. Beispiele sind klein, aber eines istAsnDecoder.ReadEnumeratedBytes
- Unsichere APIs. Dies sind APIs innerhalb der
Diese Änderung kommt in erster Linie (1) zugute. Es wird erwartet, dass in Zukunft die meisten APIs eine ref
annehmen und eine ref struct
zurückgeben. Die Änderungen wirken sich negativ auf (2.1) und (2.2) aus, da die bestehende Anrufsemantik gestört wird und sich die Lebensdauerregeln ändern.
Die APIs in Kategorie (2.1) werden jedoch weitgehend von Microsoft oder Entwicklern erstellt, die am meisten von ref
-Feldern (der Tanner der Welt) profitieren. Es ist vernünftig anzunehmen, dass diese Klasse von Entwicklern bereit wäre, eine Kompatibilitätsabgabe für das Upgrade auf C# 11 in Form einiger Anmerkungen zur Beibehaltung der vorhandenen Semantik zu akzeptieren, wenn im Gegenzug ref
-Felder bereitgestellt würden.
Die APIs in Kategorie (2.2) sind das größte Problem. Es ist nicht bekannt, wie viele solcher APIs es gibt, und es ist unklar, ob diese im Code von Drittanbietern öfter oder seltener vorkommen würden. Die Erwartung besteht darin, dass es eine sehr kleine Anzahl von ihnen gibt, insbesondere wenn wir den Kompatibilitätsbruch auf out
vornehmen. Bisher haben Untersuchungen eine sehr kleine Anzahl dieser auf der public
-Oberfläche ergeben. Dies ist jedoch ein schwieriges Muster, da es eine semantische Analyse erfordert. Bevor diese Änderung vorgenommen wird, wäre ein toolbasierter Ansatz erforderlich, um die Annahmen diesbezüglich zu überprüfen, da diese Änderung eine kleine Anzahl bekannter Fälle betrifft.
Bei beiden Fällen in Kategorie (2) ist die Lösung jedoch einfach. Die ref
-Parameter, die nicht als erfassbar betrachtet werden sollen, müssen zu scoped
noch ref
hinzufügen. In (2.1) wird dies den Entwickler wahrscheinlich auch dazu zwingen, Unsafe
oder MemoryMarshal
zu verwenden, aber das wird für unsichere Stil-APIs erwartet.
Im Idealfall könnte die Sprache die Auswirkungen stillschweigender Änderungen verringern, indem sie eine Warnung ausgibt, wenn eine API stillschweigend problematisches Verhalten an den Tag legt. Dies wäre eine Methode, die sowohl eine ref
akzeptiert und ref struct
zurückgibt, jedoch nicht tatsächlich die ref
in der ref struct
erfasst. Der Compiler könnte in diesem Fall eine Diagnose ausgeben, die Entwickler darüber informiert, dass ref
stattdessen als scoped ref
annotiert werden sollte.
Entscheidung Dieses Design kann zwar erreicht werden, die daraus resultierende Funktion ist jedoch schwieriger zu verwenden, sodass die Entscheidung für eine Kompatibilitätspause getroffen wurde.
Decision Der Compiler gibt eine Warnung an, wenn eine Methode die Kriterien erfüllt, aber den ref
-Parameter nicht als ref
-Feld erfasst. Dies sollte Kunden beim Aufrüsten entsprechend vor den potenziellen Problemen warnen, die sie verursachen können.
Schlüsselwörter im Vergleich zu Attributen
Dieses Design erfordert die Verwendung von Attributen, um die neuen Lebensdauerregeln zu annotieren. Dies hätte genauso einfach mit kontextuellen Schlüsselwörtern erledigt werden können. Beispielsweise könnte [DoesNotEscape]
dann scoped
zugeordnet werden. Schlüsselwörter, auch kontextbezogene, müssen jedoch im Allgemeinen einen sehr hohen Standard für die Aufnahme erfüllen. Sie nehmen wertvollen Platz in der Sprache ein und sind wichtigere Bestandteile der Sprache. Dieses Feature wird zwar wertvoll sein, aber nur einer Minderheit der C#-Entwickler dienen.
Auf den ersten Blick scheint es, als solle man keine Schlüsselwörter verwenden, aber es gibt zwei wichtige Punkte zu beachten:
- Die Anmerkungen wirken sich auf die Programmsemantik aus. Da C# nur zögerlich dabei ist, Attribute die Programmsemantik beeinflussen zu lassen, bleibt unklar, ob dies das Merkmal ist, das diesen Schritt für die Sprache rechtfertigen sollte.
- Die Entwickler, die dieses Feature höchstwahrscheinlich nutzen, überschneiden sich stark mit der Gruppe von Entwicklern, die Funktionszeiger verwenden. Dieses Feature, das auch von einer Minderheit von Entwicklern verwendet wird, hat eine neue Syntax gerechtfertigt, und diese Entscheidung wird nach wie vor als solide erachtet.
Zusammengenommen bedeutet dies, dass die Syntax berücksichtigt werden sollte.
Eine grobe Skizze der Syntax wäre:
[RefDoesNotEscape]
wirdscoped ref
zugeordnet.[DoesNotEscape]
wirdscoped
zugeordnet.[RefDoesEscape]
wirdunscoped
zugeordnet.
Entscheidung Syntax für scoped
und scoped ref
verwenden; Attribut für unscoped
verwenden.
Zulassen von lokalen festen Puffern
Dieses Design ermöglicht sichere fixed
-Puffer, die jeden Typ unterstützen können. Eine mögliche Erweiterung hier ist, dass solche fixed
-Puffer als lokale Variablen deklariert werden können. Dies würde es ermöglichen, eine Reihe vorhandener stackalloc
-Vorgänge durch einen fixed
-Puffer zu ersetzen. Es würde auch den Satz von Szenarien erweitern, in denen wir stapelbasierte Zuweisungen verwenden könnten, da stackalloc
auf nicht verwaltete Elementtypen beschränkt ist, während fixed
-Puffer dies nicht sind.
class FixedBufferLocals
{
void Example()
{
Span<int> span = stackalloc int[42];
int buffer[42];
}
}
Das funktioniert, erfordert aber, dass wir die Syntax für lokale Variablen ein wenig erweitern. Unklar, ob dies die zusätzliche Komplexität wert ist. Möglich, dass wir jetzt nicht entscheiden und später zurückkehren könnten, wenn ausreichend Bedarf nachgewiesen wird.
Beispiel dafür, wo dies von Vorteil wäre: https://github.com/dotnet/runtime/pull/34149
Beschluss dies vorerst zurückstellen
modreqs verwenden oder nicht
Es muss entschieden werden, ob mit neuen Lebensdauerattributen markierte Methoden bei der Ausgabe in modreq
übersetzt werden sollen oder nicht. Es gäbe effektiv eine 1:1-Zuordnung zwischen Anmerkungen und modreq
, wenn dieser Ansatz gewählt würde.
Der Grund für das Hinzufügen einer modreq
ist, dass die Attribute die Semantik der 'ref safe context'-Regeln ändern. Nur Sprachen, die diese Semantik verstehen, sollten die betreffenden Methoden aufrufen. Wenn sie auf OHI-Szenarien angewendet werden, werden die Lebenszyklen zu einem Vertrag, den alle abgeleiteten Methoden implementieren müssen. Wenn Annotationen ohne modreq
vorhanden sind, kann es zu Situationen kommen, in denen virtual
-Methodenketten mit widersprüchlichen Lebensdaueranmerkungen geladen werden (dies kann geschehen, wenn nur ein Teil der virtual
-Kette kompiliert wird und der andere nicht).
Die anfängliche Arbeit für den sicheren Kontext hat nicht modreq
verwendet, sondern vertraute stattdessen auf Sprachen und das Framework, um den sicheren Kontext zu verstehen. Gleichzeitig sind jedoch alle Elemente, die zu den Regeln für den ref-safe-context beitragen, ein wesentlicher Bestandteil der Methodensignatur: ref
, in
, ref struct
, usw. Daher führt jede Änderung an den vorhandenen Regeln einer Methode bereits jetzt zu einer binären Änderung der Signatur. Um den neuen Lebensdaueranmerkungen die gleichen Auswirkungen zu verleihen, benötigen sie modreq
-Durchsetzung.
Die Sorge ist, ob dies überlastend ist. Es hat die negative Auswirkung, dass Signaturen flexibler werden, zum Beispiel durch das Hinzufügen von [DoesNotEscape]
zu einem Parameter, was zu einer binären Kompatibilitätsänderung führt. Dieser Kompromiss bedeutet, dass Frameworks wie BCL im Laufe der Zeit wahrscheinlich nicht in der Lage sein werden, derartige Signaturen zu lockern. Es könnte zu einem gewissen Grad gemildert werden, indem ein Ansatz gewählt wird, den die Sprache mit in
-Parametern verfolgt, und modreq
ausschließlich in virtuellen Positionen angewendet wird.
Entscheidung Verwenden Sie modreq
nicht in den Metadaten. Der Unterschied zwischen out
und ref
ist nicht modreq
, aber sie haben jetzt unterschiedliche referenzsichere Kontextwerte. Es gibt keinen wirklichen Vorteil, die Regeln mit modreq
nur zur Hälfte durchzusetzen.
Zulassen von mehrdimensionalen festen Puffern
Soll der Entwurf für fixed
Puffer erweitert werden, damit mehrdimensionale Stilarrays einbezogen werden? Im Wesentlichen erlaubt es Deklarationen wie die folgenden:
struct Dimensions
{
int array[42, 13];
}
Entscheidung Vorerst nicht zulassen
Verstoß gegen den Geltungsbereich
Das Laufzeit-Repository verfügt über mehrere nicht öffentliche APIs, die ref
-Parameter als ref
-Felder erfassen. Diese sind unsicher, da die Lebensdauer des resultierenden Werts nicht nachverfolgt wird. Beispiel: der Span<T>(ref T value, int length)
-Konstruktor.
Die Mehrheit dieser APIs wird sich wahrscheinlich dafür entscheiden, eine ordnungsgemäße Lebensdauernachverfolgung für die Rückgabe zu implementieren, was einfach durch die Aktualisierung auf C# 11 erreicht wird. Einige möchten jedoch ihre aktuelle Semantik beibehalten und den Rückgabewert nicht verfolgen, da ihre gesamte Absicht darin besteht, unsicher zu sein. Die wichtigsten Beispiele sind MemoryMarshal.CreateSpan
und MemoryMarshal.CreateReadOnlySpan
. Dies wird erreicht, indem die Parameter als scoped
markiert werden.
Dies bedeutet, dass die Laufzeitumgebung ein etabliertes Muster benötigt, um scoped
unsicher aus einem Parameter zu entfernen.
-
Unsafe.AsRef<T>(in T value)
könnte seinen bestehenden Zweck erweitern, indem er zuscoped in T value
wechselt. Dies würde es ermöglichen,in
undscoped
aus den Parametern zu entfernen. Es wird dann zur universellen "remove ref safety"-Methode - Führen Sie eine neue Methode ein, deren gesamtes Ziel darin besteht,
scoped
:ref T Unsafe.AsUnscoped<T>(scoped in T value)
zu entfernen. Dadurch wird auchin
entfernt, da die Anrufer andernfalls immer noch eine Kombination aus Methodenaufrufen benötigen, um die „remove ref safety“ zu entfernen. An diesem Punkt ist die vorhandene Lösung wahrscheinlich ausreichend.
Dies wurde standardmäßig deaktiviert?
Das Design verfügt nur über zwei Positionen, die standardmäßig scoped
sind:
-
this
istscoped ref
-
out
istscoped ref
Die Entscheidung über out
hat das Ziel, die Kompatibilitätsbelastung der ref
-Felder erheblich zu reduzieren und gleichzeitig einen natürlicheren Standard zu bieten. Es ermöglicht Entwicklern, tatsächlich zu bedenken, dass bei out
die Daten nur nach außen fließen, während bei ref
die Regeln den Datenfluss in beide Richtungen berücksichtigen müssen. Dies führt zu erheblicher Verwirrung bei Entwicklern.
Die Entscheidung über this
ist nicht erwünscht, da dies bedeutet, dass ein struct
ein Feld nicht mittels ref
zurückgeben kann. Dies ist ein wichtiges Szenario für Hochleistungsentwickler, und das Attribut [UnscopedRef]
wurde im Wesentlichen für dieses Szenario hinzugefügt.
Schlüsselwörter haben einen hohen Standard, und das Hinzufügen für ein einzelnes Szenario ist fragwürdig. Daher wurde darüber nachgedacht, ob wir dieses Schlüsselwort ganz vermeiden könnten, indem wir this
standardmäßig einfach ref
und nicht scoped ref
machen. Alle Mitglieder, die this
zu scoped ref
machen müssen, können dies tun, indem sie die Methode scoped
markieren (da eine Methode heute als readonly
gekennzeichnet werden kann, um eine readonly ref
zu erstellen).
Bei einer normalen struct
ist dies meist eine positive Änderung, da Kompatibilitätsprobleme nur dann auftreten, wenn ein Mitglied eine Rückgabe von ref
hat. Es gibt sehr wenige dieser Methoden, und ein Tool könnte diese schnell erkennen und in scoped
Mitglieder umwandeln.
Auf einem ref struct
führt diese Änderung zu erheblich größeren Kompatibilitätsproblemen. Beachten Sie Folgendes:
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;
}
}
Im Wesentlichen würde es bedeuten, dass alle Instanzmethodenaufrufe auf änderbareref struct
Lokalvariablen illegal wären, es sei denn, die lokale Variable wurde weiter als scoped
gekennzeichnet. Die Regeln müssen den Fall berücksichtigen, in dem Felder in this
anderen Feldern neu zugewiesen wurden. Ein readonly ref struct
hat dieses Problem nicht, da die readonly
Beschaffenheit die Neuzuweisung von Referenzen verhindert. Es wäre jedoch eine bedeutende Änderung, die die Abwärtskompatibilität beeinträchtigt, da sie sich praktisch auf jede vorhandene änderbare ref struct
auswirken würde.
Ein readonly ref struct
ist jedoch immer noch problematisch, wenn wir die Felder ref
bis ref struct
erweitern. Es ermöglicht die gleiche grundlegende Problemlösung, indem der Erfassungswert einfach in das Feld ref
verschoben wird.
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);
}
}
Einige zogen die Idee in Betracht, dass this
unterschiedliche Standardwerte je nach Typ von struct
oder Element haben könnte. Zum Beispiel:
-
this
alsref
:struct
,readonly ref struct
oderreadonly member
-
this
alsscoped ref
:ref struct
oderreadonly ref struct
mitref
Feld zumref struct
Dies minimiert Kompatibilitätsprobleme und maximiert die Flexibilität, erschwert jedoch das Verständnis für die Kunden. Außerdem lässt sich das Problem nicht vollständig lösen, da zukünftige Features wie sichere fixed
Puffer erfordern, dass ein änderbarer ref struct
ref
Rückgaben für Felder aufweisen, die von diesem Entwurf nicht allein funktionieren, da sie in die Kategorie scoped ref
fallen würden.
Entscheidung Behalten this
als scoped ref
. Dies bedeutet, dass die vorherigen heimtückischen Beispiele Compilerfehler erzeugen.
ref fields zu ref struct
Diese Funktion eröffnet einen neuen Satz von sicheren Kontextregeln, weil ein ref
-Feld auf ein ref struct
verweisen kann. Diese generische Natur von ByReference<T>
bedeutete, dass die Laufzeit bisher kein solches Konstrukt besitzen konnte. Daher werden alle unsere Regeln unter der Annahme geschrieben, dass dies nicht möglich ist. In der ref
-Feldmerkmal geht es hauptsächlich nicht darum, neue Regeln festzulegen, sondern die bestehenden Regeln in unserem System zu kodifizieren. Das Zulassen von ref
-Feldern bis ref struct
erfordert die Kodifizierung neuer Regeln, da mehrere neue Szenarios berücksichtigt werden müssen.
Der erste Punkt ist, dass ein readonly ref
jetzt in der Lage ist, den ref
-Zustand zu speichern. Zum Beispiel:
readonly ref struct Container
{
readonly ref Span<int> Span;
void Store(Span<int> span)
{
Span = span;
}
}
Dies bedeutet, dass beim Überlegen über Regeln für Methodenargumente berücksichtigt werden muss, dass readonly ref T
eine potenzielle Methodenausgabe ist, wenn T
möglicherweise ein ref
-Feld zu einem ref struct
hat.
Das zweite Problem ist, dass die Sprache eine neue Art von sicherem Kontext Berücksichtigung finden muss: ref-field-safe-context. Alle ref struct
, die transitiv ein ref
-Feld enthalten, haben einen anderen Fluchtbereich, der die Werte im ref
-Feld repräsentiert. Bei mehreren ref
-Feldern können sie gemeinsam als einzelner Wert nachverfolgt werden. Der Standardwert für diese Parameter ist caller-context.
ref struct Nested
{
ref Span<int> Span;
}
Span<int> M(ref Nested nested) => nested.Span;
Dieser Wert bezieht sich nicht auf den safe-context des Containers; wenn der Kontext des Containers kleiner wird, hat dies keine Auswirkungen auf den ref-field-safe-context der ref
Feldwerte. Darüber hinaus kann der ref-field-safe-context niemals kleiner sein als der safe-context des Containers.
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];
}
Dieser ref-field-safe-context hat im Wesentlichen immer existiert. Bis jetzt konnten ref
-Felder nur auf normale struct
zeigen, daher wurde es trivial auf caller-context reduziert. Um die Unterstützung von ref
-Feldern bis zu ref struct
zu gewährleisten, müssen unsere bestehenden Regeln aktualisiert werden, um den neuen ref-safe-context zu berücksichtigen.
Drittens müssen die Regeln für die Neuzuweisung von refs aktualisiert werden, um sicherzustellen, dass wir nicht gegen ref-field-context für die Werte verstoßen. Im Wesentlichen gilt für x.e1 = ref e2
, bei denen der Typ von e1
ein ref struct
ist, dass der ref-field-safe-context gleich sein muss.
Diese Probleme sind sehr lösbar. Das Compilerteam hat einige Versionen dieser Regeln skizziert, und sie ergeben sich weitgehend aus unserer vorhandenen Analyse. Das Problem besteht darin, dass für solche Regeln kein Code verbraucht wird, der dabei hilft, die Richtigkeit und Nutzbarkeit zu beweisen. Dies macht uns sehr zögerlich, Unterstützung hinzuzufügen, aus Angst, dass wir falsche Standardwerte wählen und die Laufzeit in eine unpraktische Lage bringen, wenn diese Möglichkeit tatsächlich genutzt wird. Diese Sorge ist besonders stark, da .NET 8 uns mit allow T: ref struct
und Span<Span<T>>
wahrscheinlich in diese Richtung bewegt. Die Regeln wären besser verfasst, wenn sie in Verbindung mit dem Nutzungscode geschrieben würden.
Entscheidung Verzögern der Zulassung des Felds ref
zu ref struct
bis .NET 8, wo wir Szenarien haben, die dabei helfen, die Regeln für diese Szenarien festzulegen. Dies wurde seit .NET 9 nicht implementiert.
Was wird C# 11.0 ausmachen?
Die in diesem Dokument beschriebenen Funktionen müssen nicht in einem einzigen Durchlauf implementiert werden. Stattdessen können sie in Phasen über mehrere Sprachversionen hinweg in den folgenden Kategorien implementiert werden:
ref
-Felder undscoped
[UnscopedRef]
ref
-Felder zuref struct
- Sunset-beschränkte Typen
- Puffer fester Größe
Was in welcher Veröffentlichung implementiert wird, ist lediglich eine Übung zum Festlegen des Umfangs.
Entscheidung Nur (1) und (2) haben es in C# 11.0 geschafft. Der Rest wird in zukünftigen Versionen von C# berücksichtigt.
Überlegungen für die Zukunft
Erweiterte Lebensdaueranmerkungen
Die Lebenszeitanmerkungen in diesem Vorschlag sind beschränkt, da Entwickler das Standardverhalten des Escape- und Nicht-Escape-Verhaltens von Werten ändern können. Dies fügt unserem Modell leistungsstarke Flexibilität hinzu, ändert aber nicht radikal die Gruppe von Beziehungen, die ausgedrückt werden können. Im Kern ist das C#-Modell immer noch binär: Kann ein Wert zurückgegeben werden oder nicht?
Dies ermöglicht es, begrenzte lebenslange Beziehungen zu verstehen. Ein Wert, der nicht von einer Methode zurückgegeben werden kann, hat beispielsweise eine geringere Lebensdauer als ein Wert, der von einer Methode zurückgegeben werden kann. Es gibt keine Möglichkeit, die Lebensdauerbeziehung zwischen Werten zu beschreiben, die jedoch von einer Methode zurückgegeben werden können. Genauer gesagt gibt es keine Möglichkeit festzustellen, dass ein Wert eine größere Lebensdauer hat als der andere, sobald feststeht, dass beide aus einer Methode zurückgegeben werden können. Der nächste Schritt in unserer Lebensentwicklung wäre es, solche Beziehungen zu beschreiben.
Andere Methoden wie Rust ermöglichen es, diese Art von Beziehung auszudrücken und somit komplexere scoped
-Stilvorgänge zu implementieren. Unsere Sprache könnte ähnlich profitieren, wenn ein solches Feature enthalten wäre. Im Moment gibt es keinen motivierenden Druck, dies zu tun, aber falls es in Zukunft dazu kommt, könnte unser scoped
Modell auf recht unkomplizierte Weise erweitert werden, um es einzubeziehen.
Jedes scoped
könnte einem benannten Lebenszyklus zugeordnet werden, indem der Syntax ein generisches Argument hinzugefügt wird. Beispielsweise ist scoped<'a>
ein Wert, der die Lebensdauer 'a
aufweist. Einschränkungen wie where
können dann verwendet werden, um die Beziehungen zwischen diesen Lebensdauern zu beschreiben.
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
where 'b >= 'a
{
s.Span = span;
}
Diese Methode definiert zwei Lebensdauern 'a
und 'b
und deren Beziehung, insbesondere, dass 'b
größer als 'a
ist. Dies ermöglicht es der Aufrufstelle, präzisere Regeln dafür zu haben, wie Werte sicher in Methoden übergeben werden können, im Vergleich zu den grob abgestuften Regeln, die heute vorhanden sind.
Verwandte Informationen
Probleme
Die folgenden Probleme beziehen sich alle auf diesen Vorschlag:
- 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
Vorschläge
Die folgenden Vorschläge beziehen sich auf diesen Vorschlag:
Vorhandene Beispiele
Dieser bestimmte Codeausschnitt benötigt den unsicheren Modus, weil es Probleme mit der Übergabe eines Span<T>
gibt, der auf dem Stack einer Instanzmethode auf einer ref struct
zugeordnet werden kann. Auch wenn dieser Parameter nicht erfasst wird, muss die Sprache davon ausgehen, dass er es ist, und verursacht daher unnötigerweise Probleme.
Dieser Codeausschnitt möchte einen Parameter stummschalten, indem Elemente der Daten ausgeblendet werden. Die Escapedaten können aus Effizienzgründen stapelallokiert werden. Auch wenn der Parameter nicht maskiert wird, weist ihm der Compiler einen safe-context außerhalb der umschließenden Methode zu, da es sich um einen Parameter handelt. Dies bedeutet, dass die Implementierung zum Verwenden der Stapelzuordnung unsafe
verwenden muss, um den Parameter nach dem Ausfangen der Daten wieder zuzuweisen.
Lustige Beispiele
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;
}
}
Sparsame Liste
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;
}
}
}
}
Beispiele und Notizen
Im Folgenden finden Sie eine Reihe von Beispielen, die zeigen, wie und warum die Regeln wie sie funktionieren. Dazu gehören mehrere Beispiele, die gefährliche Verhaltensweisen zeigen und wie die Regeln verhindern, dass sie auftreten. Es ist wichtig, diese beim Vornehmen von Anpassungen an den Vorschlag zu berücksichtigen.
Ref-Neuzuweisung und Anrufseiten
Demonstration, wie Ref-Neuzuweisung und Methodenaufruf zusammenarbeiten.
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;
}
}
Ref-Neuzuweisung und unsichere Escapes
Der Grund für die nachfolgende Zeile in den Ref-Neuzuweisungsregeln kann auf den ersten Blick nicht offensichtlich sein.
e1
muss den gleichen safe-context wiee2
haben
Dies liegt daran, dass die Lebensdauer der Werte, auf die von ref
Orten verwiesen wird, invariant ist. Die Indirektion verhindert, dass wir hier irgendeine Art von Varianz zulassen, selbst bei kürzeren Lebensdauern. Wenn die Eingrenzung zulässig ist, wird der folgende unsichere Code geöffnet:
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];
}
Damit eine ref
keine ref struct
ist, wird diese Regel trivialerweise erfüllt, da alle Werte im selben safe-context liegen. Diese Regel kommt eigentlich nur zum Tragen, wenn der Wert ref struct
ist.
Dieses Verhalten von ref
wird auch in Zukunft wichtig sein, wenn wirref
-Felder bis ref struct
zulassen.
Gültigkeitsbereich lokal
Die Verwendung von scoped
auf lokalen Variablen ist besonders hilfreich für Codemuster, die bedingt Werte mit unterschiedlichen safe-context lokal zuweisen. Das bedeutet, dass der Code nicht mehr auf Initialisierungstricks wie = stackalloc byte[0]
angewiesen ist, um einen lokalen safe-context zu definieren, sondern jetzt einfach scoped
verwenden kann.
// 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];
}
Dieses Muster tritt häufig in Code auf niedrigem Niveau auf. Wenn es sich bei der beteiligten ref struct
um Span<T>
handelt, kann der oben genannte Trick verwendet werden. Es gilt jedoch nicht für andere ref struct
-Typen und kann dazu führen, dass Code auf niedriger Ebene auf unsafe
zurückgreifen muss, um die Unfähigkeit zu umgehen, die Lebensdauer ordnungsgemäß anzugeben.
Bereichsparameterwerte
Eine Quelle wiederholter Reibungspunkte in Low-Level-Code ist die permissive Standard-Escape-Funktion für Parameter. Sie sind safe-context an den caller-context. Dies ist ein vernünftiger Standardwert, da sie mit den Codierungsmustern von .NET als Ganzes in Einklang steht. In Code auf niedriger Ebene gibt es jedoch eine größere Nutzung von ref struct
, und dieser Standardwert kann zu Reibungen mit anderen Teilen der Ref-Safe-Kontextregeln führen.
Der Hauptreibungspunkt entsteht, weil die Argumente der Methodik mit der Regel übereinstimmen müssen. Diese Regel kommt am häufigsten bei Instanzenmethoden auf ref struct
zur Anwendung, wenn mindestens ein Parameter auch ein ref struct
ist. Dies ist ein gängiges Muster in Code auf niedriger Ebene, bei dem ref struct
-Typen häufig Span<T>
-Parameter in ihren Methoden nutzen. Es tritt beispielsweise bei jedem Schreibstil ref struct
auf, der Span<T>
zum Übergeben von Puffern verwendet.
Diese Regel ist vorhanden, um Szenarien wie die folgenden zu verhindern:
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);
}
}
Im Wesentlichen existiert diese Regel, da in der Sprache davon ausgegangen werden muss, dass alle Eingaben in eine Methode in ihren maximal zulässigen safe-context entweichen. Wenn es ref
- oder out
-Parameter gibt, einschließlich der Empfänger, können die Eingaben als Felder dieser ref
-Werte entweichen (wie es oben in RS.Set
geschieht).
In der Praxis gibt es jedoch viele solche Methoden, die ref struct
als Parameter übergeben, aber nie beabsichtigen, sie in der Ausgabe zu erfassen. Es handelt sich nur um einen Wert, der innerhalb der aktuellen Methode verwendet wird. Zum Beispiel:
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))
{
...
}
}
}
Um mit diesem Code auf niedriger Ebene umzugehen, wird unsafe
-Tricks verwenden, um den Compiler über die Lebensdauer von ref struct
zu täuschen. Dadurch wird das Wertversprechen von ref struct
erheblich reduziert, da sie ein Mittel sein sollen, um unsafe
zu vermeiden, während weiterhin Hochleistungscode geschrieben wird.
Hier ist scoped
ein effektives Tool für ref struct
-Parameter, da diese aus der Berücksichtigung entfernt werden, da sie gemäß den aktualisierten Methodenargumenten aus der Methode zurückgegeben werden, die Regel entsprechen müssen. Ein ref struct
-Parameter, der genutzt, aber nie zurückgegeben wird, kann als scoped
bezeichnet werden, um Aufrufwebsites flexibler zu gestalten.
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))
{
...
}
}
}
Verhindern der schwierigen Referenzzuweisung durch eine schreibgeschützte Mutation
Wenn ein ref
zu einem readonly
-Feld in einem Konstruktor oder init
Member genommen wird, ist der Typ ref
und nicht ref readonly
. Dies ist ein langjähriges Verhalten, das Code wie das folgende zulässt:
struct S
{
readonly int i;
public S(string s)
{
M(ref i);
}
static void M(ref int i) { }
}
Dies stellt jedoch ein potenzielles Problem dar, wenn eine solche ref
in einem ref
-Feld vom gleichen Typ gespeichert werden könnte. Es würde eine direkte Mutation einer readonly struct
aus einem Instanzmitglied ermöglichen:
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++;
}
}
Der Vorschlag verhindert dies jedoch, weil er gegen die Regeln für den verweissicheren Kontext verstößt. Beachten Sie Folgendes:
- Der ref-safe-context von
this
ist function-member, und safe-context ist caller-context. Dies sind beide Standard fürthis
in einemstruct
-Mitglied. - Die ref-sichere Kontext- des
i
ist function-member. Dies ergibt sich aus den Regeln zur Feldlebensdauer. Insbesondere Regel 4.
An diesem Punkt ist die Zeile r = ref i
gemäß den Regeln zur Referenzneuzuweisung unzulässig.
Diese Regeln waren nicht dazu gedacht, dieses Verhalten zu verhindern, tun dies jedoch als Nebeneffekt. Es ist wichtig, dies bei zukünftigen Regel-Updates zu berücksichtigen, um die Auswirkungen auf Szenarien wie dies auszuwerten.
Komische zyklische Zuordnung
Ein Aspekt dieses Designs ist, wie frei ein ref
von einer Methode zurückgegeben werden kann. Es wird wohl erwartet, dass alle ref
genauso frei wie normale Werte zurückgegeben werden können, was die meisten Entwickler intuitiv erwarten. Es ermöglicht jedoch pathologische Szenarien, die der Compiler bei der Berechnung der Referenzsicherheit berücksichtigen muss. Beachten Sie Folgendes:
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;
}
}
Dies ist kein Codemuster, das von Entwicklern verwendet werden soll. Aber wenn ein ref
mit der gleichen Lebensdauer wie ein Wert zurückgegeben werden kann, ist es gemäß den Regeln erlaubt. Der Compiler muss beim Auswerten eines Methodenaufrufs alle Rechtsfälle berücksichtigen, und dies führt dazu, dass solche APIs effektiv unbrauchbar sind.
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);
}
Um diese APIs nutzbar zu machen, stellt der Compiler sicher, dass die ref
-Lebensdauer für einen ref
-Parameter kürzer ist als die Lebensdauer aller Referenzen im zugeordneten Parameterwert. Aus diesem Grund muss der ref-safe-context für ref
ref struct
ein return-only und out
ein caller-context sein. Dies verhindert aufgrund des Unterschieds in der Lebensdauer eine zyklische Zuordnung.
Beachten Sie, dass[UnscopedRef]
den ref-safe-context aller ref
bis ref struct
Werte zu caller-context heraufstuft und somit eine zyklische Zuweisung ermöglicht und eine virale Verwendung von [UnscopedRef]
in der Aufrufkette erzwingt:
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;
}
}
Ebenso ermöglicht [UnscopedRef] out
eine zyklische Zuordnung, da der Parameter sowohl safe-context als auch ref-safe-context mit return-only hat.
Die Heraufstufung von [UnscopedRef] ref
zum caller-context ist sinnvoll, wenn der Typ keinref struct
ist (beachten Sie, dass wir die Regeln einfach halten möchten, sodass sie nicht zwischen Verweisen auf Verweisstrukturen und Nicht-Verweisstrukturen unterscheiden):
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 Bezug auf erweiterte Annotationen erstellt das [UnscopedRef]
Design Folgendes:
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 kann nicht tief durch Ref-Felder sein
Betrachten Sie das folgende Codebeispiel:
ref struct S
{
ref int Field;
readonly void Method()
{
// Legal or illegal?
Field = 42;
}
}
Bei der Gestaltung der Regeln für ref
-Felder auf readonly
-Instanzen in einem Vakuum können die Regeln so gestaltet werden, dass die oben genannten Regeln entweder legal oder illegal sein können. Im Wesentlichen kann readonly
gültig tief in einem ref
-Feld sein oder nur auf die ref
angewendet werden. Wird nur auf die ref
angewendet, verhindert es die Neuzuweisung von Referenzen, erlaubt aber eine normale Zuweisung, die den referenzierten Wert ändert.
Dieses Design existiert jedoch nicht im Vakuum; es entwickelt Regeln für Typen, die bereits effektiv über ref
-Felder verfügen. Von diesen ist die am meisten herausragende, Span<T>
, bereits stark davon abhängig, dass readonly
hier nicht tief ist. Das primäre Szenario ist die Möglichkeit, das Feld ref
über eine readonly
-Instanz zuzuweisen.
readonly ref struct SpanOfOne
{
readonly ref int Field;
public ref int this[int index]
{
get
{
if (index != 1)
throw new Exception();
return ref Field;
}
}
}
Dies bedeutet, dass wir die flache Interpretation von readonly
wählen müssen.
Modellieren von Konstruktoren
Eine subtile Designfrage ist: Wie werden die Körper von Konstruktoren im Hinblick auf die Referenzsicherheit modelliert? Wie wird der folgende Konstruktor im Wesentlichen analysiert?
ref struct S
{
ref int field;
public S(ref int f)
{
field = ref f;
}
}
Es gibt im Wesentlichen zwei Ansätze:
- Modell als
static
-Methode, wobeithis
lokal ist und der safe-context dem caller-context entspricht. - Modell als Methode
static
, wobeithis
ein Parameterout
ist.
Darüber hinaus muss ein Konstruktor die folgenden Invarianten erfüllen:
- Stellen Sie sicher, dass
ref
-Parameter alsref
-Felder erfasst werden können. - Stellen Sie sicher, dass
ref
für Felder vonthis
nicht durchref
-Parameter maskiert werden können. Das würde gegen schwierigen Ref-Zuweisung verstoßen.
Die Absicht besteht darin, das Formular zu wählen, das unseren Invarianten entspricht, ohne dass spezielle Regeln für Konstruktoren einzuführen sind. Angesichts der Tatsache, dass das beste Modell für Konstruktoren darin besteht, this
als out
-Parameter zu betrachten. Die return only Natur der out
ermöglicht es uns, alle oben genannten Invarianten ohne spezielle Groß-/Kleinschreibung zu befriedigen:
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;
}
Methodenargumente müssen übereinstimmen
Die Regel, dass die Methodenargumente übereinstimmen müssen, ist eine häufige Verwirrungsquelle für Entwickler. Es handelt sich um eine Regel mit einer Reihe von Sonderfällen, die schwer zu verstehen sind, wenn man nicht mit den Gründen für diese Regel vertraut ist. Um die Gründe für die Regel besser zu verstehen, werden wir ref-safe-context und safe-context auf einfach context vereinfachen.
Methoden können ziemlich problemlos den Zustand zurückgeben, der ihnen als Parameter übergeben wird. Im Wesentlichen kann jeder erreichbare Zustand zurückgegeben werden, der nicht bereichslos ist (einschließlich der Rückgabe durch ref
). Dies kann direkt über eine return
-Anweisung oder indirekt durch Zuweisen zu einem ref
-Wert zurückgegeben werden.
Direkte Rückgaben stellen für die Referenzsicherheit nicht viele Probleme dar. Der Compiler muss lediglich alle rückgabefähigen Argumente einer Methode betrachten und beschränkt dann effektiv den Rückgabewert auf den minimalen Kontext der Eingabe. Dieser Rückgabewert durchläuft dann die normale Verarbeitung.
Indirekte Rückgaben stellen ein erhebliches Problem dar, da alle ref
sowohl eine Eingabe als auch eine Ausgabe für die Methode sind. Diese Outputs haben bereits einen bekannten Kontext. Der Compiler kann keine neuen ableiten, er muss sie auf ihrer aktuellen Ebene berücksichtigen. Das bedeutet, dass der Compiler alle einzelnen ref
untersuchen muss, die in der aufgerufenen Methode zugewiesen werden können, deren Kontext ausgewertet werden muss, und zu überprüfen, dass keine rückgabefähige Eingabe für die Methode einen kleineren Kontext hat als dieser ref
. Wenn ein solcher Fall vorhanden ist, muss der Methodenaufruf illegal sein, da er gegen die ref
-Sicherheit verstoßen könnte.
Der Abgleich der Methodenargumente ist der Vorgang, bei dem der Compiler diese Sicherheitsüberprüfung durchführt.
Eine andere Möglichkeit, dies zu bewerten, was für Entwickler oft einfacher zu berücksichtigen ist, ist die folgende Übung:
- Sehen Sie sich die Methodendefinition an, um alle Orte zu identifizieren, an denen der Zustand indirekt zurückgegeben werden kann: a. Änderbare
ref
-Parameter, die aufref struct
b verweisen. Veränderbareref
-Parameter mit Ref-zuweisbarenref
-Feldern. Zuweisbareref
-Parameter oderref
-Felder, die aufref struct
zeigen (rekursiv betrachtet) - Sehen Sie sich die Aufrufstelle a an. Identifizieren Sie die Kontexte, die mit den oben angegebenen Orten in Einklang stehen. Identifizieren Sie die Kontexte aller Eingaben für die Methode, die zurückgegeben werden dürfen und nicht mit
scoped
-Parametern übereinstimmen.
Wenn ein Wert in 2.b kleiner als 2.a ist, muss der Methodenaufruf unzulässig sein. Sehen wir uns ein paar Beispiele an, um die Regeln zu veranschaulichen:
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);
}
}
Betrachten wir den Anruf von F0
und gehen wir (1) und (2) durch. Die Parameter mit potenzieller indirekter Rückgabe sind a
und b
, da beide direkt zugewiesen werden können. Die Argumente, die diesen Parametern entsprechen, sind:
a
, dasx
zugeordnet ist, das den -Kontext des caller-context hatb
, dasy
zugeordnet ist, das mit Kontext des function-member verbunden ist
Die zurückgebbaren Eingaben für die Methode sind
x
mit escape-scope von caller-contextref x
mit escape-scope von caller-contexty
mit escape-scope von function-member
Der Wert ref y
kann nicht zurückgegeben werden, da er einem scoped ref
zugeordnet ist, daher wird er nicht als Eingabe betrachtet. Da es jedoch mindestens eine Eingabe mit einem kleineren escape scope (y
-Argument) als bei einer der Ausgaben (x
-Argument) gibt, ist der Methodenaufruf nicht erlaubt.
Eine andere Variante ist die folgende:
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);
}
}
Auch hier haben die Parameter a
und b
das Potenzial für indirekte Rückgaben, da beide direkt zugewiesen werden können. Jedoch b
kann ausgeschlossen werden, da es nicht auf eine ref struct
verweist und daher nicht zum Speichern des ref
-Zustands verwendet werden kann. So haben wir:
a
, dasx
zugeordnet ist, das den -Kontext des caller-context hat
Die zurückgebbaren Eingaben für die Methode sind:
x
mit Kontext von caller-contextref x
mit Kontext von caller-contextref y
mit Kontext von function-member
Da es mindestens eine Eingabe mit einem kleineren escape scope (ref y
-Argument) als bei einer der Ausgaben (x
-Argument) gibt, ist der Methodenaufruf nicht erlaubt.
Dies ist die Logik, die die Regel „Methodenargumente müssen übereinstimmen“ umfassen soll. Es geht noch weiter, da es sowohl scoped
als eine Möglichkeit zum Entfernen von Eingaben aus der Betrachtung als auch readonly
eine Möglichkeit zum Entfernen von ref
als Ausgabe betrachtet (eine Zuweisung in readonly ref
ist nicht möglich, sodass es keine Ausgabequelle sein kann). Diese Sonderfälle machen die Regeln zwar komplizierter, aber sie sind zum Vorteil des Entwicklers. Der Compiler versucht, alle Eingaben und Ausgaben zu entfernen, von denen er weiß, dass sie nicht zum Ergebnis beitragen können, um den Entwicklern maximale Flexibilität beim Aufrufen eines Mitglieds zu ermöglichen. Ähnlich wie die Überlastungsauflösung lohnt es sich, unsere Regeln komplexer zu gestalten, wenn sie mehr Flexibilität für die Verbraucher schafft.
Beispiele für den abgeleiteten safe-context von Deklarationsausdrücken
Im Zusammenhang mit Infer safe-context von Deklarationsausdrücken.
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).
}
}
Beachten Sie, dass der lokale Kontext, der sich aus dem Modifikator scoped
ergibt, der engste ist, der für die Variable verwendet werden kann. Ein engerer Kontext würde bedeuten, dass sich der Ausdruck auf Variablen bezieht, die nur in einem engeren Kontext als dem Ausdruck deklariert sind.
C# feature specifications