Freigeben über


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:

  1. ref-Felder und scoped
  2. [UnscopedRef]

Diese Features bleiben offene Vorschläge für eine zukünftige Version von C#:

  1. ref-Felder zu ref struct
  2. 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 wie ByReference<T> entfernt.
  • Erlauben, dass struct-Typen ref 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 in struct

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 structdeklarieren. 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_I4emittiert. 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 von init-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 ein in-Parameter einem ref-Feld neu zugewiesen werden.
  • readonly ref readonly: Eine Kombination aus ref readonly und readonly 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 refdeklariert 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.Fref-safe-context wie folgt:

  1. Wenn F ein ref-Feld ist, ist sein ref-safe-context der sichere Kontext von e.
  2. Andernfalls, falls e von einem Referenztyp ist, verfügt es über den ref-safe-context im Aufrufer-Kontext.
  3. 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 structexistiert. 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:

  1. e2 muss einen ref-safe-context haben, der mindestens so groß wie der ref-safe-context von e1 ist.
  2. e1 muss den gleichen sicheren Kontext wie e2Hinweis 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

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 einer struct 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- oder in-Parameter weist einen ref-safe-context- von Nur Rückgabe auf. Dies geschieht teilweise für ref 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 eine ref struct hat safe-context im Fall von Nur-Rückgabe. Dies ermöglicht es, die Rückgabe und out gleich ausdrucksstark zu machen. Dies hat nicht das dumme zyklische Zuordnungsproblem, da out implizit scoped ist, sodass der ref-safe-context immer noch kleiner ist als der safe-context.
  • Ein this-Parameter für einen struct-Konstruktor hat einen safe-context von Nur-Rückgabe. Dies ergibt sich daraus, dass es als out-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:

  1. Wenn pscoped ref ist, trägt expr nicht zu ref-safe-context bei, wenn man Argumente prüft.
  2. Wenn pscoped ist, trägt expr nicht zu safe-context bei, wenn man Argumente prüft.
  3. Wenn pout ist, trägt expr 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 dem M() keine ref-to-ref-struct zurückgibt, weist einen safe-context aus den engsten der folgenden Elemente ab:

  1. Der caller-context
  2. Wenn die Rückgabe ein ref struct ist, wird der safe-context von allen Argumentausdrücken beigetragen.
  3. Wenn die Rückgabe eine ref struct ist, wird der ref-safe-context von allen ref Argumenten beigesteuert

Wenn 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 dem M() keine ref-to-ref-struct zurückgibt, ist safe-context, der engste der folgenden Kontexte:

  1. Der caller-context
  2. Der safe-context, der von allen Argumentausdrücken beigetragen wird
  3. Der ref-safe-context, der von allen refArgumenten beigetragen wird

Wenn 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:

  1. Der safe-context des Konstruktoraufrufs.
  2. Der safe-context und ref-safe-context von Argumenten für Member-Initialisierer-Indexer, die zum Empfänger wechseln können.
  3. 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)

  1. 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.
  2. Alle ref-Argumente von ref struct-Typen müssen durch einen Wert mit diesem safe-context zugewiesen werden können. Dies ist ein Fall, in dem refnicht verallgemeinert werden kann und in und out einschließt.

Für jeden Methodenaufruf e.M(a1, a2, ... aN)

  1. 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.
  2. Alle out-Argumente von ref 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 scopedzurü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 einem ref- oder in-Parameter hinzu
  • Fügen Sie scoped zu einem ref struct-Parameter hinzu
  • Entferne [UnscopedRef] aus einem out-Parameter
  • Entfernen Sie [UnscopedRef] von einem ref-Parameter eines ref 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- oder out-Parameter des Typs ref struct mit einer Nichtübereinstimmung beim Hinzufügen von [UnscopedRef] (nicht entfernen scoped). (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 einen ref oder ref readonly zurück, oder die Methode verfügt über einen ref- oder out-Parameter vom Typ ref struct.
    • Die Methode verfügt über mindestens einen zusätzlichen ref-, in- oder out-Parameter oder einen Parameter vom Typ ref struct.

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 in TryParse-Methodensignaturen verwendet werden). Ein Bericht über Bereichsunterschiede, nur weil sie in Sprachversion 11 eingesetzt werden und der out-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 das ref-Feld zurückzugeben.
  • Ein out-Parameter würde einen safe-context eines function-member haben.

Detaillierte Notizen

  • Ein ref-Feld kann nur innerhalb eines ref struct deklariert werden.
  • Ein ref-Feld kann nicht static, volatile oder const deklariert werden.
  • Ein ref-Feld darf keinen Typ aufweisen, der ref struct
  • Der Erstellungsprozess der Referenzbaugruppe muss das Vorhandensein eines ref-Felds innerhalb eines ref struct sicherstellen.
  • Ein readonly ref struct muss seine ref-Felder als readonly ref deklarieren
  • Bei by-ref-Werten muss der Modifikator scoped vor in, out, oder ref 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.

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 Signatur static TypedReference __makeref<T>(ref T value) behandelt.
  • __refvalue wird als Methode mit der Signatur static 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 structdefiniert 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 von this 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 einem struct
    • 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

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.

  1. Nicht bereichsgebundene ref/in/out-Parameter können möglicherweise einem Methodenaufruf als ein ref-Feld einer ref struct in C#11 entweichen, nicht jedoch in C#7.2.
  2. out-Parameter werden implizit in C#11 und nicht in C#7.2 erfasst.
  3. ref / in-Parameter für ref 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 von version, 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ür ref-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 Tzurü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:

  1. Der caller-context
  2. Der safe-context, der von allen Argumentausdrücken beigetragen wird
  3. Wenn die Rückgabe eine ref struct ist, dann wird ref-safe-context von allen ref 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:

  1. Durch Wertübergabe
  2. Durch ref Rückgabe
  3. Durch das ref-Feld in ref struct, das als ref / 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- oder out-Parameter.
    • Verfügt über einen zusätzlichen in- oder ref-Parameter, ausgenommen für den Empfänger

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 einem ref-Feld wird niemals als unmanaged betrachtet
  • Der Typ des Felds ref wirkt sich auf unendliche generische Erweiterungsregeln aus. Wenn der Typ eines ref-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 kein ref struct sind, haben implizit eine Lebensdauer von T<$heap>. Dies ist implizit, es ist nicht erforderlich, in jedem Beispiel int<$heap> zu schreiben.
  • Für ein ref-Feld, das als ref<$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.
  • Für eine refdefiniert als ref<$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ückgabetyps ref<$a> Tist die Lebensdauer $a
    • $heap für alle Referenztypen und Felder von Referenztypen
    • $local für alles andere
  • 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ür T<...>
    • ref<$a> (T<$b>)expr ist die Wertlebensdauer $b für T<...> 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 $localkonvertiert 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 oder ref ä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 refzurü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> oder ref struct
    • Dabei handelt es sich bei der ref struct um einen Rückgabetyp, ref- oder out-Parameter.
    • Verfügt über einen zusätzlichen in- oder ref-Parameter (ausgenommen für den Empfänger)

Um die Auswirkungen zu verstehen, ist es hilfreich, APIs in Kategorien aufzuteilen:

  1. Möchten Sie, dass Verbraucher berücksichtigen, dass ref als ein ref-Feld erfasst wird? Paradebeispiel sind die Span(ref T value)-Konstrukteure
  2. Möchten Sie nicht, dass Verbraucher berücksichtigen, dass ref als ein ref-Feld erfasst wird? Diese werden jedoch in zwei Kategorien unterteilt.
    1. Unsichere APIs. Dies sind APIs innerhalb der Unsafe- und MemoryMarshal-Typen, von denen MemoryMarshal.CreateSpan der prominenteste ist. Diese APIs fangen ref unsicher ab, sind aber auch als unsichere APIs bekannt.
    2. 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 ist AsnDecoder.ReadEnumeratedBytes

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 refakzeptiert 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:

  1. 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.
  2. 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] wird scoped ref zugeordnet.
  • [DoesNotEscape] wird scoped zugeordnet.
  • [RefDoesEscape] wird unscoped 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.

  1. Unsafe.AsRef<T>(in T value) könnte seinen bestehenden Zweck erweitern, indem er zu scoped in T value wechselt. Dies würde es ermöglichen, in und scoped aus den Parametern zu entfernen. Es wird dann zur universellen "remove ref safety"-Methode
  2. 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 auch in 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 ist scoped ref
  • out ist scoped 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 als ref: struct, readonly ref struct oder readonly member
  • this als scoped ref: ref struct oder readonly ref struct mit ref Feld zum ref 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:

  1. ref-Felder und scoped
  2. [UnscopedRef]
  3. ref-Felder zu ref struct
  4. Sunset-beschränkte Typen
  5. 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 'aaufweist. 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.

Probleme

Die folgenden Probleme beziehen sich alle auf diesen Vorschlag:

Vorschläge

Die folgenden Vorschläge beziehen sich auf diesen Vorschlag:

Vorhandene Beispiele

Utf8JsonReader

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.

Utf8JsonWriter

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 wie e2 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 structist. 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ür this in einem struct-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 refref 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:

  1. Modell als static-Methode, wobei this lokal ist und der safe-context dem caller-context entspricht.
  2. Modell als Methode static, wobei this ein Parameter out ist.

Darüber hinaus muss ein Konstruktor die folgenden Invarianten erfüllen:

  1. Stellen Sie sicher, dass ref-Parameter als ref-Felder erfasst werden können.
  2. Stellen Sie sicher, dass ref für Felder von this nicht durch ref-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:

  1. 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 auf ref struct b verweisen. Veränderbare ref-Parameter mit Ref-zuweisbaren ref-Feldern. Zuweisbare ref-Parameter oder ref-Felder, die auf ref struct zeigen (rekursiv betrachtet)
  2. 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, das x zugeordnet ist, das den -Kontext des caller-context hat
  • b, das y 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-context
  • ref x mit escape-scope von caller-context
  • y 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, das x zugeordnet ist, das den -Kontext des caller-context hat

Die zurückgebbaren Eingaben für die Methode sind:

  • x mit Kontext von caller-context
  • ref x mit Kontext von caller-context
  • ref 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.